minicpm5-2b-webgpu / src /agent.js
ProCreations's picture
Avoid Safari async-generator JIT crash; recover interrupted replies
594cfa2 verified
Raw History Blame Contribute Delete
5.56 kB
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" });
}