The two limits were compiled in: a task run stopped at AppData.HarnessMaxToolSteps (200) and a chat reply at AppData.BotReplyMaxToolSteps (8), so an install that needed a longer task had to rebuild. Both are now `app_settings` rows (`harness_max_tool_steps` and `bot_reply_max_tool_steps`), read at the two loop call sites through grabHarnessMaxToolSteps and grabReplyMaxToolSteps, and edited by the super admin on Settings > General beside the prompt appendix. Zero clears a limit back to the code default, like the bot file quota. readStepLimit keeps the saved number only when it is a whole number of at least 1 and no more than AppData.BotStepLimitMax (1000), so a blank, a zero or a typo falls back instead of stopping every run before its first turn, and a huge number still ends in the wrap-up report. runTextLoop keeps AppData.HarnessMaxToolSteps as its default, so it stays free of database reads and its tests run as they did.
255 lines
11 KiB
TypeScript
255 lines
11 KiB
TypeScript
import type { BUN_SQLITE_COBBLE_BOTS } from "@/db/types/db";
|
|
import {
|
|
ActionCheckNote,
|
|
BotFilesNote,
|
|
ComputerFilesNote,
|
|
ScheduledRunNote,
|
|
TasksNote,
|
|
TaskRunNote,
|
|
WorkspaceLearningNote,
|
|
} from "@/src/data/harness-prompts";
|
|
import { grabHarnessMaxToolSteps } from "@/src/functions/backend/db/app-settings/grab-run-limits";
|
|
import grabBotComputers from "@/src/functions/backend/db/computers/grab-bot-computers";
|
|
import grabBotPrompt from "@/src/functions/backend/db/bots/grab-bot-prompt";
|
|
import grabUserWorkspaces from "@/src/functions/backend/db/workspaces/grab-user-workspaces";
|
|
import runTextLoop from "@/src/harness/text/run-text-loop";
|
|
import openComputerTools from "@/src/harness/tools/computer/open-computer-tools";
|
|
import openBotExtensions, { type OpenBotExtensions } from "@/src/harness/tools/extensions/open-bot-extensions";
|
|
import openBotSkills, { type OpenBotSkills } from "@/src/harness/tools/skills/open-bot-skills";
|
|
import grabBotTools from "@/src/harness/tools/bot/grab-bot-tools";
|
|
import createWorkspaceTool from "@/src/harness/tools/bot/create-workspace-tool";
|
|
import updateWorkspaceTool from "@/src/harness/tools/bot/update-workspace-tool";
|
|
import askToTakeOverTool from "@/src/harness/tools/computer/ask-to-take-over-tool";
|
|
import importFileTool from "@/src/harness/tools/computer/import-file-tool";
|
|
import searchComputersTool from "@/src/harness/tools/computer/search-computers-tool";
|
|
import grabBotFileTools from "@/src/harness/tools/storage/grab-bot-file-tools";
|
|
import reportRunFailedTool from "@/src/harness/tools/bot/report-run-failed-tool";
|
|
import computerRef from "@/src/utils/computer-ref";
|
|
import grabRunScopes from "@/src/functions/backend/db/bot-files/grab-run-scopes";
|
|
import composeJobBrief from "./compose-job-brief";
|
|
import grabSessionCarryOver from "./grab-session-carry-over";
|
|
import jobSessionId from "@/src/functions/backend/db/bots-queue/job-session-id";
|
|
import withTakeOverRefs from "./with-take-over-refs";
|
|
import ScheduledRunFailedError from "./scheduled-run-failed-error";
|
|
import type { BotJobContext, BotJobOutcome } from "./types";
|
|
|
|
type Params = BotJobContext & {
|
|
bot: Pick<BUN_SQLITE_COBBLE_BOTS, "kind"> & { id: number };
|
|
/**
|
|
* The user whose conversation the job is in.
|
|
*/
|
|
user_id: number;
|
|
};
|
|
|
|
/**
|
|
* Works through a task in its session's workspace (if any): the bot
|
|
* follows its brief with its full prompt,
|
|
* its own tools, search_computers across all its computers, its MCP
|
|
* servers' and custom tools (`openBotExtensions`) and, when the job has a
|
|
* computer, that computer's tools (over MCP), until it answers. That answer is the report for the user.
|
|
* When only the user can go on (a login, a captcha), the bot asks them to
|
|
* take over (`ask_user_to_take_over`) and the report needs them.
|
|
* A scheduled run acts on its own (`ScheduledRunNote`); one that reports
|
|
* it failed (`report_run_failed`) throws, so the queue retries it.
|
|
* Every model turn is reported as a step (`report`).
|
|
*/
|
|
export default async function runBotTask({
|
|
bot,
|
|
user_id,
|
|
job,
|
|
params,
|
|
signal,
|
|
report,
|
|
}: Params): Promise<BotJobOutcome> {
|
|
const workspace_id = job.workspace_id || null;
|
|
|
|
const computer = job.computer_id
|
|
? await openComputerTools({
|
|
computer_id: job.computer_id,
|
|
bot_id: bot.id,
|
|
workspace_id,
|
|
signal,
|
|
})
|
|
: null;
|
|
|
|
let extensions: OpenBotExtensions | undefined;
|
|
let skills: OpenBotSkills | undefined;
|
|
|
|
try {
|
|
const session_id = await jobSessionId({ job });
|
|
const prompt = await grabBotPrompt({ bot_id: bot.id, user_id, workspace_id });
|
|
const computers = await grabBotComputers({ bot_id: bot.id, workspace_id });
|
|
|
|
/**
|
|
* A coordinator's task can also change workspaces: the findings
|
|
* it was asked to record go there, not only in the report.
|
|
*/
|
|
const coordinator_workspaces =
|
|
bot.kind == "coordinator"
|
|
? (await grabUserWorkspaces({ user_id })).filter((w) => !w.archived)
|
|
: [];
|
|
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 report.
|
|
*/
|
|
const attachments: number[] = [];
|
|
const scheduled = Boolean(params.schedule_id);
|
|
let failure = null as string | null;
|
|
|
|
/**
|
|
* A take-over defaults to the computer the task runs on.
|
|
*/
|
|
const take_over_computers = [
|
|
...computers.filter((c) => c.id == job.computer_id),
|
|
...computers.filter((c) => c.id != job.computer_id),
|
|
];
|
|
/**
|
|
* The files this run reaches, for its file tools, an import and the
|
|
* pictures it may set: one answer, where `grabRunScopes` decides.
|
|
*/
|
|
const scopes = await grabRunScopes({ bot, user_id, workspace_id });
|
|
|
|
const tools = [
|
|
...(await grabBotTools({
|
|
user_id,
|
|
bot_id: bot.id,
|
|
workspace_id,
|
|
computers,
|
|
session_id,
|
|
is_coordinator: bot.kind == "coordinator",
|
|
scopes,
|
|
})),
|
|
...(bot.kind == "coordinator"
|
|
? [
|
|
createWorkspaceTool({ user_id, workspaces: coordinator_workspaces }),
|
|
...(coordinator_workspaces.length
|
|
? [
|
|
updateWorkspaceTool({
|
|
user_id,
|
|
workspaces: coordinator_workspaces,
|
|
workspace_id,
|
|
scopes,
|
|
}),
|
|
]
|
|
: []),
|
|
]
|
|
: []),
|
|
...(computers.length
|
|
? [
|
|
searchComputersTool({ bot_id: bot.id, computers, workspace_id }),
|
|
askToTakeOverTool({
|
|
computers: take_over_computers,
|
|
in_task: true,
|
|
onAsk: ({ ref }) => take_overs.push(ref),
|
|
}),
|
|
]
|
|
: []),
|
|
...(scheduled ? [reportRunFailedTool({ onFail: ({ reason }) => (failure = reason) })] : []),
|
|
...(computer?.tools || []),
|
|
...(job.computer_id
|
|
? [
|
|
importFileTool({
|
|
computer_id: job.computer_id,
|
|
scopes,
|
|
bot_id: bot.id,
|
|
user_id,
|
|
}),
|
|
]
|
|
: []),
|
|
...(await grabBotFileTools({
|
|
bot,
|
|
user_id,
|
|
workspace_id,
|
|
session_id,
|
|
scopes,
|
|
onAttach: ({ media_id }) => attachments.push(media_id),
|
|
})),
|
|
];
|
|
|
|
extensions = await openBotExtensions({
|
|
bot_id: bot.id,
|
|
workspace_id,
|
|
computer_id: job.computer_id || null,
|
|
taken: tools.map((tool) => tool.name),
|
|
signal,
|
|
});
|
|
|
|
skills = openBotSkills({
|
|
skills: extensions.skills,
|
|
taken: [...tools, ...extensions.tools].map((tool) => tool.name),
|
|
});
|
|
|
|
const result = await runTextLoop({
|
|
system: [
|
|
prompt,
|
|
carry_over,
|
|
TaskRunNote,
|
|
scheduled ? ScheduledRunNote : "",
|
|
BotFilesNote,
|
|
ComputerFilesNote({ computer_id: job.computer_id || 0 }),
|
|
TasksNote,
|
|
ActionCheckNote,
|
|
workspace_id ? WorkspaceLearningNote : "",
|
|
bot.kind == "coordinator"
|
|
? `If this brief asked you to record something in a workspace (its context, or a new one), do it yourself with update_workspace or create_workspace before you report: the report alone changes nothing.`
|
|
: "",
|
|
computer
|
|
? `You're working on the computer "${computer.name}" (a Linux sandbox with a shell, files, code runners and a browser). Use its tools; don't ask the user to run anything. When only the user can go on (a login, a captcha, a code sent to them), ask them to take over with ask_user_to_take_over.`
|
|
: workspace_id
|
|
? `You have no computer for this task: this workspace has none. If the task needs one, stop and say so in the report; the user can add a computer to the workspace.`
|
|
: `You have no computer for this task.`,
|
|
computers.length > 1
|
|
? `search_computers runs a read-only command on every computer you have. When you name one in the report, write its ref, e.g. ${computerRef({ id: computers[0]!.id || 0 })}: the user can click it to switch you to that computer.`
|
|
: "",
|
|
computer?.envs.length
|
|
? `Environment variables set on this computer (use them by name, e.g. $KEY; never print their values):\n${computer.envs
|
|
.map((env) => `- ${env.key}${env.description ? `: ${env.description}` : ""}`)
|
|
.join("\n")}`
|
|
: "",
|
|
skills.note,
|
|
extensions.note,
|
|
]
|
|
.filter(Boolean)
|
|
.join("\n\n"),
|
|
messages: [
|
|
{
|
|
role: "user",
|
|
content: composeJobBrief({
|
|
title: job.title,
|
|
instructions: job.instructions,
|
|
params,
|
|
}),
|
|
},
|
|
],
|
|
tools: [...tools, ...extensions.tools, ...skills.tools],
|
|
max_steps: await grabHarnessMaxToolSteps(),
|
|
bot_id: bot.id,
|
|
workspace_id,
|
|
signal,
|
|
onStep: (step) =>
|
|
report({ ...step, text: step.text && computer ? computer.redact(step.text) : step.text }),
|
|
});
|
|
|
|
if (failure) {
|
|
throw new ScheduledRunFailedError(computer ? computer.redact(failure) : failure);
|
|
}
|
|
|
|
return {
|
|
...withTakeOverRefs({
|
|
text: computer ? computer.redact(result.text) : result.text,
|
|
refs: take_overs,
|
|
}),
|
|
attachments: attachments.length ? attachments : undefined,
|
|
};
|
|
} finally {
|
|
await extensions?.close();
|
|
await computer?.close();
|
|
}
|
|
}
|