Files
cobble/src/bots-queue/run-bot-reply.ts
T
tben 80b6b6c1d7 Let a bot's reply link to the files it saved
Both runners now pass the files a run wrote back into the reply, and a
computer file ref renders as a chip like a bot file ref does, so a
reply can point at what it produced instead of naming a path.
2026-10-22 07:37:27 +01:00

172 lines
5.8 KiB
TypeScript

import type { BUN_SQLITE_COBBLE_BOTS } from "@/db/types/db";
import { AppData } from "@/src/data/app-data";
import {
ActionCheckNote,
BotFilesNote,
ChatReplyNote,
ComputerFilesNote,
CoordinatorReplyNote,
CoordinatorSoloNote,
TasksNote,
TaskReminderNote,
WorkspaceLearningNote,
} from "@/src/data/harness-prompts";
import grabBotMessages from "@/src/functions/backend/db/bots/grab-bot-messages";
import grabBotPrompt from "@/src/functions/backend/db/bots/grab-bot-prompt";
import grabBotComputers from "@/src/functions/backend/db/computers/grab-bot-computers";
import runTextLoop from "@/src/harness/text/run-text-loop";
import grabBotChatTools from "@/src/harness/tools/bot/grab-bot-chat-tools";
import grabBotFileTools from "@/src/harness/tools/storage/grab-bot-file-tools";
import openBotExtensions from "@/src/harness/tools/extensions/open-bot-extensions";
import botMessagesToHarness from "./bot-messages-to-harness";
import composeComputersNote from "./compose-computers-note";
import grabSessionCarryOver from "./grab-session-carry-over";
import jobSessionId from "@/src/functions/backend/db/bots-queue/job-session-id";
import loadAttachmentParts from "./load-attachment-parts";
import grabMessageRefNames from "@/src/functions/backend/db/bots/grab-message-ref-names";
import withTakeOverRefs from "./with-take-over-refs";
import type { BotJobContext, BotJobOutcome } from "./types";
type Params = BotJobContext & {
bot: BUN_SQLITE_COBBLE_BOTS & { id: number };
/**
* The user whose conversation the job is in.
*/
user_id: number;
};
/**
* Answers the user in chat: the bot reads the session's recent messages
* (and the files of the latest ones, `loadAttachmentParts`) and replies,
* knowing where it is (its workspace, if the session is in one), what
* happened in the session before (`grabSessionCarryOver`) and its
* computers (`composeComputersNote`), with its chat tools (memory,
* instructions, start_task, and for the coordinator delegate_task) plus
* its MCP servers' and custom tools (`openBotExtensions`, script tools on
* its current computer).
* Finishes silently when the last message is already the bot's, i.e. an
* earlier reply answered everything.
*/
export default async function runBotReply({
bot,
user_id,
job,
signal,
report,
}: Params): Promise<BotJobOutcome> {
const session_id = await jobSessionId({ job });
const workspace_id = job.workspace_id || null;
const history = await grabBotMessages({
session_id,
limit: AppData.BotReplyHistoryMessages,
});
if (history[history.length - 1]?.author != "user") {
return { result: "", silent: true };
}
const prompt = await grabBotPrompt({ bot_id: bot.id, user_id, workspace_id });
const computers = await grabBotComputers({ bot_id: bot.id, workspace_id });
/**
* A summary that can't be written now (no model free) is left out
* rather than holding up the reply.
*/
const carry_over = await grabSessionCarryOver({
session_id,
bot_id: bot.id,
workspace_id,
signal,
}).catch(() => "");
const take_overs: string[] = [];
/**
* Files the bot wrote and asked to send with its reply.
*/
const attachments: number[] = [];
const file_tools = await grabBotFileTools({
bot,
user_id,
workspace_id,
session_id,
onAttach: ({ media_id }) => attachments.push(media_id),
});
const tools = await grabBotChatTools({
bot,
user_id,
computers,
session_id,
workspace_id,
message_id: job.message_id || undefined,
onTakeOver: ({ ref }) => take_overs.push(ref),
});
const extensions = await openBotExtensions({
bot_id: bot.id,
workspace_id,
computer_id: computers[0]?.id || null,
taken: tools.map((tool) => tool.name),
signal,
});
/**
* delegate_task is only there while the coordinator has workers.
*/
const crew_note =
bot.kind != "coordinator"
? ""
: tools.some((tool) => tool.name == "delegate_task")
? CoordinatorReplyNote
: CoordinatorSoloNote;
try {
const result = await runTextLoop({
system: [
prompt,
carry_over,
ChatReplyNote,
BotFilesNote,
ComputerFilesNote({ computer_id: computers[0]?.id || 0, can_import: false }),
TasksNote,
TaskReminderNote,
ActionCheckNote,
workspace_id ? WorkspaceLearningNote : "",
composeComputersNote({
computers,
is_coordinator: bot.kind == "coordinator",
in_workspace: Boolean(workspace_id),
}),
crew_note,
extensions.note,
`Now: ${new Date().toUTCString()}.`,
]
.filter(Boolean)
.join("\n\n"),
messages: botMessagesToHarness({
messages: history,
parts: await loadAttachmentParts({ messages: history }),
refs: await grabMessageRefNames({
texts: history.filter((m) => m.author == "user").map((m) => m.text || ""),
user_id,
}),
}),
tools: [...tools, ...file_tools, ...extensions.tools],
max_steps: AppData.BotReplyMaxToolSteps,
bot_id: bot.id,
workspace_id,
signal,
onStep: report,
});
return {
...withTakeOverRefs({ text: result.text, refs: take_overs }),
attachments: attachments.length ? attachments : undefined,
};
} finally {
await extensions.close();
}
}