import { TOOLS, SYSTEM, parseXMLCalls } from "./core.js"; import { normalizedCall, argumentExcerpt } from "./prompt.js"; export const MAX_TOOL_ROUNDS = 6; export function recoveryBudget(current, context) { return Math.min(context - 512, Math.max(8192, current * 2)); } export function toolFailure(call, error, { finish, budget, context }) { const truncated = finish === "length"; return { success: false, error: truncated ? "The tool call was cut off before its arguments were complete." : error.message, attempted_arguments: argumentExcerpt(call.function.arguments), instruction: truncated ? `Call ${call.function.name} again with a complete, self-contained result. You now have up to ${recoveryBudget(budget, context)} output tokens. Keep the implementation concise enough to finish.` : `Correct the arguments and call ${call.function.name} again. Do not claim the file exists until the tool succeeds.`, }; } // One shared loop drives the UI and regression tests, including error feedback. export async function runAgent({ chat, history, settings, signal, createArtifact, onEvent = () => {}, }) { let current = { ...settings }; let recovering = false; for (let round = 0; round < MAX_TOOL_ROUNDS; round++) { signal?.throwIfAborted(); const message = { content: "", reasoning: "", artifacts: [] }, calls = new Map(); let finish = null, timings = null, tokens = 0; await onEvent({ type: "start", round }); const messages = [ { role: "system", content: SYSTEM + (settings.system ? "\n\nUser preferences:\n" + settings.system : ""), }, ...history, ]; await chat( messages, current, settings.tools ? TOOLS : [], signal, async (chunk) => { const delta = chunk.choices?.[0]?.delta || {}; message.content += delta.content || ""; message.reasoning += delta.reasoning_content || ""; for (const part of delta.tool_calls || []) { const index = part.index ?? 0; let call = calls.get(index); if (!call) { call = { id: part.id || crypto.randomUUID(), type: "function", function: { name: "", arguments: "" }, }; calls.set(index, call); } if (part.id) call.id = part.id; call.function.name += part.function?.name || ""; call.function.arguments += part.function?.arguments || ""; } finish = chunk.choices?.[0]?.finish_reason || finish; timings = chunk.timings || timings; tokens = chunk.usage?.completion_tokens || tokens; await onEvent({ type: "chunk", chunk }); }, ); const toolCalls = calls.size ? [...calls.values()] : parseXMLCalls(message.content); if (!toolCalls.length && finish === "length" && settings.tools) { const match = message.content.match(/[\s\S]*?<\/think>/g, "") .trim(); history.push({ role: "assistant", content: visible, tool_calls: toolCalls.map(normalizedCall), }); let failed = false; for (const call of toolCalls) { signal?.throwIfAborted(); await onEvent({ type: "tool-start", name: call.function.name }); try { const args = JSON.parse(call.function.arguments); const artifact = await createArtifact(call.function.name, args); history.push({ role: "tool", tool_call_id: call.id, content: JSON.stringify({ success: true, filename: artifact.filename, artifact_id: artifact.id, preview: "Available in the chat. The user can preview and download this file.", }), }); await onEvent({ type: "tool-success", artifact, recovered: recovering, }); } catch (error) { failed = true; history.push({ role: "tool", tool_call_id: call.id, content: JSON.stringify( toolFailure(call, error, { finish, budget: current.maxTokens, context: settings.context, }), ), }); await onEvent({ type: "tool-error", error: error.message, retrying: round + 1 < MAX_TOOL_ROUNDS, }); } } await onEvent({ type: "tools-complete", content: visible }); if (failed) { recovering = true; current = { ...current, maxTokens: recoveryBudget(current.maxTokens, settings.context), thinking: false, }; } continue; } history.push({ role: "assistant", content: message.content }); await onEvent({ type: "done", finish }); return; } await onEvent({ type: "limit" }); }