Files
agent-loop/demo/shared.js
2026-11-12 15:29:12 +08:00

61 lines
3.0 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 三个角色共用的小工具:trace 记录、Session 规则。
// trace 是这次演示的“证据”:每个进程把自己做了什么追加到 TRACE_DIR/<角色>.jsonl。
const fs = require('node:fs');
const path = require('node:path');
function tracer(role) {
const dir = process.env.TRACE_DIR;
const file = dir ? path.join(dir, `${role}.jsonl`) : null;
return (kind, data = {}) => {
const rec = { t: Date.now(), role, pid: process.pid, host: process.env.HOST_LABEL || role, kind, ...data };
if (file) fs.appendFileSync(file, JSON.stringify(rec) + '\n');
return rec;
};
}
// 只看事件日志,判断这个 Session 下一步要做什么。
// 云端 Worker 和调度器都用它:Worker 不需要记住任何东西,日志就是全部状态。
// { kind: 'idle' } 没事可做
// { kind: 'dispatch', calls: [...] } 模型要的工具调用还没有结果,要发给设备
// { kind: 'model' } 有新输入(用户消息 / 工具结果 / 后台完成),该调模型了
function pendingWork(events) {
let lastModel = -1;
for (let i = events.length - 1; i >= 0; i--) {
if (events[i].type === 'model_response') { lastModel = i; break; }
}
if (lastModel >= 0) {
const calls = events[lastModel].content.filter((b) => b.type === 'tool_use');
const done = new Set(events.filter((e) => e.type === 'tool_result').map((e) => e.call_id));
const open = calls.filter((c) => !done.has(c.id));
if (open.length) return { kind: 'dispatch', calls: open };
}
const fresh = events.slice(lastModel + 1).some((e) => ['user_message', 'tool_result', 'job_finished'].includes(e.type));
return fresh ? { kind: 'model' } : { kind: 'idle' };
}
// 把事件日志翻译成模型 API 的 messages。每次调模型都从头重建,所以换一个进程来跑结果完全一样。
function rebuildMessages(events) {
const msgs = [];
const pushUser = (block) => {
const last = msgs[msgs.length - 1];
if (last && last.role === 'user') last.content.push(block);
else msgs.push({ role: 'user', content: [block] });
};
for (const e of events) {
if (e.type === 'user_message') pushUser({ type: 'text', text: e.text });
else if (e.type === 'model_response') msgs.push({ role: 'assistant', content: e.content });
else if (e.type === 'tool_result') {
const block = { type: 'tool_result', tool_use_id: e.call_id, content: e.output };
const last = msgs[msgs.length - 1];
// tool_result 必须紧跟在对应的 assistant 之后,排在同一轮里其它输入的前面
if (last && last.role === 'user') last.content.unshift(block);
else msgs.push({ role: 'user', content: [block] });
} else if (e.type === 'job_finished') {
pushUser({ type: 'text', text: `[后台任务完成] ${e.job_id}:\`${e.command}\` 退出码 ${e.exit_code}\n输出末尾:\n${e.tail}` });
}
}
return msgs;
}
module.exports = { tracer, pendingWork, rebuildMessages };