HearthNet / webagent /src /agent /runtime.js
GitHub Actions
feat: WebLLM browser agent with PeerJS mesh, HybridRAG, news signals, and easter-egg ticker
78cc96f
Raw
History Blame Contribute Delete
2.79 kB
// src/agent/runtime.js
// ReAct-style agent loop. Model-agnostic: works with any `llm.chat(...)` that
// streams tokens via onToken and resolves to a string or { text }.
import { DEFAULT_SYSTEM } from "./webllm-agent.js";
import { TOOL_HELP } from "./webllm-agent.js";
export function createAgentRuntime({ llm, tools, onLog, onToken, onState }) {
let running = false;
let aborter = null;
async function runTool(name, args) {
if (!tools[name]) throw new Error(`Unknown tool: ${name}`);
return await tools[name](args || {});
}
async function loop(userText, messages = [], maxIter = 8) {
if (running) return;
running = true;
aborter = new AbortController();
onState?.({ running: true });
const chat = [
{ role: "system", content: `${DEFAULT_SYSTEM}\n\nAvailable tools:\n${TOOL_HELP}` },
...messages,
{ role: "user", content: userText },
];
let last = "";
try {
for (let iter = 0; iter < maxIter; iter++) {
onLog?.(`iter ${iter + 1}/${maxIter}`);
last = "";
const out = await llm.chat({
messages: chat,
signal: aborter.signal,
stream: true,
temperature: 0.4,
max_tokens: 900,
onToken: (t) => {
last += t;
onToken?.(t);
},
});
const text = typeof out === "string" ? out : (out?.text || last);
const actionMatch = text.match(/action\s*:\s*(\{[\s\S]*?\})/i);
if (!actionMatch) {
onState?.({ running: false, final: text });
return text;
}
let action;
try {
action = JSON.parse(actionMatch[1]);
} catch {
chat.push({ role: "assistant", content: text });
chat.push({ role: "user", content: "Observation: action JSON parse error. Re-emit valid JSON." });
continue;
}
onLog?.(`tool: ${action.tool}(${JSON.stringify(stripTool(action))})`);
let result;
try {
result = await runTool(action.tool, action);
} catch (err) {
result = `tool error: ${err?.message || err}`;
}
chat.push({ role: "assistant", content: text });
chat.push({
role: "user",
content: `Observation from ${action.tool}: ${String(result).slice(0, 8000)}`,
});
}
onState?.({ running: false, final: last });
return last;
} finally {
running = false;
aborter = null;
onState?.({ running: false });
}
}
function stop() {
if (aborter) aborter.abort();
running = false;
onState?.({ running: false });
}
return { loop, stop, isRunning: () => running };
}
function stripTool(action) {
const { tool, ...rest } = action;
return rest;
}