MiniCPM5-2B-WebGPU-Pi / app /src /agent.worker.mjs
deyunma's picture
Copy from townbox/MiniCPM5-2B-WebGPU-Pi
4ac3af3 verified
Raw History Blame Contribute Delete
6.68 kB
import { Agent } from '@earendil-works/pi-agent-core';
import { createInference, localModel } from './inference.mjs';
import { createWorkspace, initialFiles, legacyProductsCsv, legacyProductsJson, legacyCostProductsJson, legacySalesCsv, legacyWorkspaceReadme, legacyProfitReadme } from './workspace.mjs';
import { openStorage } from './storage.mjs';
import { isModelCached } from './download.mjs';
const emit = data => postMessage(data);
const inference = createInference(emit);
let workspace, agent, storage, busy = false, operation, turnCount = 0;
const systemPrompt = `You are Pi, a coding assistant powered by MiniCPM5-2B, a language model from OpenBMB.
The model, inference, tools, and workspace run entirely in the user's browser. Workspace files are stored in this browser, not on the user's operating-system filesystem.
Use your tools to inspect and change actual files. Never claim a command ran or a file changed without a successful tool result.
When the user explicitly asks you to use a tool, call that tool even if you already know the answer.
Working directory: /workspace. Use read, write, edit, and bash. Keep tool calls and answers concise.
The bash tool supports cat, ls, find, grep, sed, awk, jq, sort, wc, printf, pipes, redirects, and shell scripts.
Use jq for JSON and awk for CSV and calculations; python, python3, node, npm, native binaries, and network access are unavailable.
You can create and edit text files, including HTML, CSS, JavaScript, and shell scripts. HTML may reference external assets by URL; writing such a reference does not require network access from the shell. Do not claim an external asset was fetched or tested.
Interpret short follow-ups using the conversation and inspect the relevant files when needed.
Inspect files before editing them. After changing files, verify the result with a tool. Finish with a short factual answer.`;
async function snapshot() { return { version: 1, demoVersion: 4, files: await workspace.snapshot(), entries: await workspace.serialize(), messages: agent.state.messages }; }
async function persist() {
const state = await snapshot();
try { await storage?.save(state); } catch (e) { emit({ type: 'persistence_error', error: 'Changes are in memory, but browser storage failed: ' + e.message }); }
emit({ type: 'workspace', files: state.files });
return state;
}
const initialized = (async () => {
let saved;
try { storage = await openStorage(); saved = await storage.load(); }
catch (e) { emit({ type: 'persistence_error', error: 'Workspace saving is unavailable: ' + e.message }); }
workspace = createWorkspace(saved?.files, saved?.entries);
await workspace.ready;
const upgradeDemo = saved && (saved.demoVersion ?? 0) < 4;
if (upgradeDemo) {
// Move prices and quantities together only for a recognized untouched demo.
// Never remove a price column if its companion file was edited or deleted.
const paths = workspace.bash.fs.getAllPaths();
const csv = '/workspace/products.csv', json = '/workspace/products.json', sales = '/workspace/sales.csv';
const untouched = async (file, versions) => paths.includes(file) && (await workspace.bash.fs.lstat(file)).isFile && versions.includes(await workspace.read(file));
if (await untouched(sales, [legacySalesCsv])) {
const oldJson = await untouched(json, [legacyProductsJson, legacyCostProductsJson]);
const oldCsv = !paths.includes(json) && await untouched(csv, [legacyProductsCsv]);
const originalDemo = (saved.demoVersion ?? 0) < 1 && !paths.includes(json) && !paths.includes(csv);
if (oldJson || oldCsv || originalDemo) {
await workspace.write(sales, initialFiles[sales]);
await workspace.write(json, initialFiles[json]);
if (oldCsv) await workspace.bash.fs.rm(csv);
}
}
const readme = '/workspace/README.md';
if (await untouched(readme, [legacyWorkspaceReadme, legacyProfitReadme])) {
await workspace.write(readme, initialFiles[readme]);
}
}
agent = new Agent({ initialState: { model: localModel, thinkingLevel: 'medium', systemPrompt, tools: workspace.tools, messages: saved?.messages ?? [] },
streamFn: inference.streamFn, toolExecution: 'sequential',
afterToolCall: ({ result, isError }) => ({ isError: isError || (result.details?.exitCode !== undefined && result.details.exitCode !== 0) }),
shouldStopAfterTurn: ({ toolResults }) => {
const stop = ++turnCount >= 12 && toolResults.length > 0;
if (stop) emit({ type: 'notice', text: 'Paused after 12 agent turns. Send a follow-up to continue.' });
return stop;
},
});
agent.subscribe(async event => {
emit({ type: 'agent_event', event });
if (event.type === 'tool_execution_end' || event.type === 'agent_end') await persist();
});
const state = await snapshot();
if (upgradeDemo) {
try { await storage?.save(state); }
catch (e) { emit({ type: 'persistence_error', error: 'Changes are in memory, but browser storage failed: ' + e.message }); }
}
emit({ type: 'initialized', ...state });
})();
onmessage = async ({ data }) => {
const { id, action, args = {} } = data;
const reply = value => emit({ type: 'reply', id, value });
try {
await initialized;
if (action === 'stop') { operation?.abort(); agent.abort(); inference.stop(); return reply(true); }
if (action === 'snapshot') return reply(await snapshot());
if (action === 'cache_status') return reply(await isModelCached());
if (busy) throw Error('Wait for the current operation or stop it first.');
busy = true; operation = new AbortController(); emit({ type: 'busy', action, cachedOnly: args.cachedOnly === true });
try {
let value;
if (action === 'load') { await inference.load(args, operation.signal); value = inference.device; }
else if (action === 'prompt') {
turnCount = 0; await agent.prompt(args.text); value = await snapshot();
} else if (action === 'shell') {
value = await workspace.bash.exec(args.command, { signal: operation.signal }); await persist();
} else if (action === 'write') { await workspace.write(args.path, args.content); value = await persist(); }
else if (action === 'new_chat') { agent.reset(); value = await persist(); }
else if (action === 'read') value = await workspace.read(args.path);
else throw Error('Unknown action: ' + action);
reply(value);
} finally { busy = false; operation = undefined; emit({ type: 'idle' }); }
} catch (error) { emit({ type: 'reply', id, error: error?.name === 'AbortError' ? 'Stopped.' : String(error?.message ?? error), errorName: error?.name, errorCode: error?.code }); }
};