File size: 5,564 Bytes
c51a3e7 594cfa2 c51a3e7 594cfa2 c51a3e7 594cfa2 c51a3e7 594cfa2 c51a3e7 594cfa2 c51a3e7 594cfa2 c51a3e7 594cfa2 c51a3e7 594cfa2 c51a3e7 594cfa2 c51a3e7 594cfa2 c51a3e7 594cfa2 c51a3e7 594cfa2 c51a3e7 594cfa2 c51a3e7 594cfa2 c51a3e7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 | 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(/<function\s+name=["']([^"']+)["']/);
if (match)
toolCalls.push({
id: crypto.randomUUID(),
type: "function",
function: {
name: match[1],
arguments: message.content.slice(
message.content.indexOf("<function"),
),
},
});
}
await onEvent({ type: "response", finish, timings, tokens });
if (toolCalls.length && settings.tools) {
const visible = message.content
.replace(/<function\b[\s\S]*$/, "")
.replace(/<think>[\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" });
}
|