// 三个角色共用的小工具: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 };