Add the harness, envs, per-bot models and chat attachments
- Harness: providers, model catalog synced hourly, automatic free picks, cooldowns, tool loop with context trimming, MCP computer tools - Bots queue runs replies and tasks through the harness; jobs wait (attempt refunded) while every model is resting - Envs: Settings > Environment, scoped general/workspace/computer, written to computers and redacted from tool output - Models: Settings > Models, per-harness provider select defaulting to OpenRouter, and a models modal on each bot's chat (the coordinator's edits the defaults) - Chat: thinking row while a reply runs, file attachments (upload, paste, drop) that replies read as images, audio and document text - Fix media uploads failing after a user's first file, and private files being served from the public media path Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
import { AppData } from "@/src/data/app-data";
|
||||
import HarnessError from "../harness-error";
|
||||
import resolveHarnessCandidates from "../resolve/resolve-harness-candidates";
|
||||
import runToolCall from "../tools/run-tool-call";
|
||||
import toolDefinition from "../tools/tool-definition";
|
||||
import type {
|
||||
HarnessCandidate,
|
||||
HarnessMessage,
|
||||
HarnessTool,
|
||||
HarnessToolCall,
|
||||
HarnessUsage,
|
||||
} from "../types";
|
||||
import fitTranscriptToContext from "./fit-transcript-to-context";
|
||||
import generateText from "./generate-text";
|
||||
|
||||
export type TextLoopStep = {
|
||||
index: number;
|
||||
text: string | null;
|
||||
tool_calls: HarnessToolCall[];
|
||||
candidate: HarnessCandidate;
|
||||
};
|
||||
|
||||
type Params = {
|
||||
/**
|
||||
* The bot's full prompt (`grabBotPrompt`).
|
||||
*/
|
||||
system: string;
|
||||
/**
|
||||
* The conversation so far, ending with what to do now.
|
||||
*/
|
||||
messages: HarnessMessage[];
|
||||
tools?: HarnessTool[];
|
||||
max_steps?: number;
|
||||
signal?: AbortSignal;
|
||||
/**
|
||||
* Called after each model turn, before its tools run. Use it for
|
||||
* progress and checkpoints.
|
||||
*/
|
||||
onStep?: (step: TextLoopStep) => Promise<void> | void;
|
||||
/**
|
||||
* The bot the run is for: its own models are used where it has them.
|
||||
*/
|
||||
bot_id?: number | string;
|
||||
generate?: typeof generateText;
|
||||
resolveCandidates?: () => Promise<HarnessCandidate[]>;
|
||||
};
|
||||
|
||||
export type TextLoopResult = {
|
||||
/**
|
||||
* The final answer for the user.
|
||||
*/
|
||||
text: string;
|
||||
/**
|
||||
* Everything after the system prompt, including tool turns.
|
||||
*/
|
||||
messages: HarnessMessage[];
|
||||
steps: number;
|
||||
candidate: HarnessCandidate;
|
||||
usage: HarnessUsage;
|
||||
};
|
||||
|
||||
const WrapUpPrompt = `You've used every step this run allows. Don't call any more tools. Report what you did, what is left, and anything you need from the user.`;
|
||||
|
||||
/**
|
||||
* The text harness at work: the model answers or calls tools, tool results
|
||||
* go back to it, and so on until it answers without calling a tool. Tools
|
||||
* run one at a time, in order. Stays on one model unless it fails. After
|
||||
* `max_steps` turns the model is asked to wrap up without tools.
|
||||
*/
|
||||
export default async function runTextLoop({
|
||||
system,
|
||||
messages,
|
||||
tools = [],
|
||||
max_steps = AppData.HarnessMaxToolSteps,
|
||||
signal = new AbortController().signal,
|
||||
onStep,
|
||||
bot_id,
|
||||
generate = generateText,
|
||||
resolveCandidates = () => resolveHarnessCandidates({ harness: "text", bot_id }),
|
||||
}: Params): Promise<TextLoopResult> {
|
||||
const candidates = await resolveCandidates();
|
||||
const definitions = tools.map((tool) => toolDefinition({ tool }));
|
||||
const media_cache = new Map<string, string>();
|
||||
const transcript: HarnessMessage[] = [...messages];
|
||||
const usage: Required<HarnessUsage> = {
|
||||
prompt_tokens: 0,
|
||||
completion_tokens: 0,
|
||||
cost: 0,
|
||||
};
|
||||
let prefer_key: string | undefined;
|
||||
|
||||
for (let index = 0; index <= max_steps; index++) {
|
||||
if (signal.aborted) {
|
||||
throw new HarnessError({ message: `Cancelled.`, code: "aborted" });
|
||||
}
|
||||
|
||||
const wrap_up = index == max_steps;
|
||||
|
||||
if (wrap_up) {
|
||||
transcript.push({ role: "user", content: WrapUpPrompt });
|
||||
}
|
||||
|
||||
/**
|
||||
* Sized for the model the run is on (or will start on). A fallback
|
||||
* with a smaller window gets a 400 and the next model is tried.
|
||||
*/
|
||||
const model = (candidates.find((c) => c.key == prefer_key) || candidates[0])?.model;
|
||||
|
||||
fitTranscriptToContext({
|
||||
system,
|
||||
transcript,
|
||||
context_length: model?.context_length || AppData.HarnessAutoMinContext,
|
||||
});
|
||||
|
||||
const answer = await generate({
|
||||
messages: [{ role: "system", content: system }, ...transcript],
|
||||
tools: wrap_up || !definitions.length ? undefined : definitions,
|
||||
candidates,
|
||||
prefer_key,
|
||||
signal,
|
||||
media_cache,
|
||||
bot_id,
|
||||
});
|
||||
|
||||
prefer_key = answer.candidate.key;
|
||||
usage.prompt_tokens += answer.usage?.prompt_tokens || 0;
|
||||
usage.completion_tokens += answer.usage?.completion_tokens || 0;
|
||||
usage.cost += answer.usage?.cost || 0;
|
||||
|
||||
const tool_calls = wrap_up ? [] : answer.tool_calls;
|
||||
|
||||
transcript.push({
|
||||
role: "assistant",
|
||||
content: answer.text,
|
||||
...(tool_calls.length ? { tool_calls } : {}),
|
||||
});
|
||||
|
||||
await onStep?.({
|
||||
index,
|
||||
text: answer.text,
|
||||
tool_calls,
|
||||
candidate: answer.candidate,
|
||||
});
|
||||
|
||||
if (!tool_calls.length) {
|
||||
return {
|
||||
text:
|
||||
answer.text ||
|
||||
(wrap_up ? `I ran out of steps before I could finish.` : ""),
|
||||
messages: transcript,
|
||||
steps: index + 1,
|
||||
candidate: answer.candidate,
|
||||
usage,
|
||||
};
|
||||
}
|
||||
|
||||
for (const call of tool_calls) {
|
||||
transcript.push({
|
||||
role: "tool",
|
||||
tool_call_id: call.id,
|
||||
content: await runToolCall({ call, tools, signal }),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Unreachable: the text loop ended without an answer.`);
|
||||
}
|
||||
Reference in New Issue
Block a user