Add bots files handling logic

This commit is contained in:
2026-10-21 17:56:09 +01:00
parent b0af4db1bf
commit d17a4a7bdd
97 changed files with 5843 additions and 41 deletions
@@ -0,0 +1,89 @@
import { z } from "zod";
import { AppData } from "@/src/data/app-data";
import type { BotFilePurpose } from "@/src/dict/bot-file-dict";
import enqueueFileIngest from "@/src/file-ingest/enqueue-file-ingest";
import recordBotFile from "@/src/functions/backend/db/bot-files/record-bot-file";
import type { HarnessTool } from "../../types";
import {
describeScope,
fileRef,
findNamedScope,
formatBytes,
type StorageToolContext,
} from "./storage-tool-context";
type Args = {
path: string;
content: string;
scope?: string;
attach?: boolean;
purpose?: BotFilePurpose;
};
/**
* Writes a text file (markdown, CSV, JSON, code, plain text) into one of
* the bot's directories, making folders as needed. The file stays for
* later runs and becomes searchable.
*/
export default function writeFileTool(context: StorageToolContext): HarnessTool<Args> {
const own = context.scopes[0];
return {
name: "write_file",
description: `Save a text file for later: markdown, CSV, JSON, code or plain text. Give it a path like "reports/q3.md"; folders are made as needed. Files belong to ${
own?.scope == "workspace" ? "the workspace you're working in" : "you"
} and stay between runs: write a file before telling the user something is saved, and search_files to find one you wrote before. It is readable and searchable at once.${
context.scopes.length > 1
? ` "scope" picks a directory: ${context.scopes.map((s) => `${s.scope}:${s.owner_id}`).join(", ")}.`
: ""
}`,
schema: z.object({
path: z
.string()
.min(1)
.max(AppData.BotFilePathMaxChars)
.describe(`Where it goes, e.g. "notes.md" or "reports/q3.md"`),
content: z.string().max(AppData.BotFileWriteMaxChars),
scope: z.string().optional().describe(`"bot:<id>" or "workspace:<id>"`),
attach: z
.boolean()
.optional()
.describe(`Also send it to the user with this reply, as an attachment`),
purpose: z
.enum(["writing", "artifact"])
.optional()
.describe(`"writing" for notes and text, "artifact" for a finished file the user asked for`),
}),
run: async ({ args }) => {
const named = args.scope ? findNamedScope({ scopes: context.scopes, name: args.scope }) : own;
if (!named) {
return `You can't write there. Your directories: ${context.scopes
.map((s) => `${s.scope}:${s.owner_id}`)
.join(", ")}.`;
}
const file = await recordBotFile({
bot_id: context.bot_id,
scope: named.scope,
owner_id: named.owner_id,
path: args.path,
data: args.content,
purpose: args.purpose || "writing",
source: "bot-write",
user_id: context.user_id,
});
await enqueueFileIngest({ file_id: file.id! });
if (args.attach && file.media_id) {
context.onAttach?.({ media_id: Number(file.media_id), name: file.name || "" });
}
return [
`Saved ${file.path} (${formatBytes({ bytes: Number(file.size) || 0 })}) in ${describeScope(named)}. ${fileRef({ file_id: file.id! })}`,
"Tell the user the file's name and what's in it.",
].join("\n\n");
},
};
}