first commit
This commit is contained in:
156
async-agent-lab/scripts/drive.ts
Normal file
156
async-agent-lab/scripts/drive.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
// 命令行驱动:对真实模型跑一个级别,打印时间线,把整次运行存进 recordings/。
|
||||
//
|
||||
// node scripts/drive.ts --level 1 [--steer 4000:"顺便说下 status 在哪些文件里用到"]
|
||||
// node scripts/drive.ts --level 4 --answer idle:保留一版 # 等 Agent 停在依赖处再回答
|
||||
// node scripts/drive.ts --level 4 --answer 3000:直接移除 # 提问后 3 秒就回答
|
||||
//
|
||||
// 结束条件:Agent 空闲,并且没有在跑的后台命令、没有待回答的问题。
|
||||
import { mkdirSync, writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { parseArgs } from 'node:util';
|
||||
import { loadProvider, PROJECT_ROOT } from '../agent/config.ts';
|
||||
import { createLLM } from '../agent/llm.ts';
|
||||
import { Run } from '../agent/run.ts';
|
||||
import type { TraceEvent } from '../shared/types.ts';
|
||||
|
||||
const { values } = parseArgs({
|
||||
options: {
|
||||
level: { type: 'string', default: '1' },
|
||||
answer: { type: 'string' }, // idle:选项 或 <毫秒>:选项
|
||||
steer: { type: 'string' }, // <毫秒>:消息,或 test+<毫秒>:消息(第一次 npm test 启动后多少毫秒插话)
|
||||
out: { type: 'string' },
|
||||
'idle-ms': { type: 'string', default: '1500' },
|
||||
},
|
||||
});
|
||||
|
||||
const run = new Run(createLLM(loadProvider()), Number(values.level));
|
||||
const t0 = Date.now();
|
||||
const stamp = () => `${((Date.now() - t0) / 1000).toFixed(1).padStart(5)}s`;
|
||||
const log = (text: string) => console.log(`${stamp()} ${text}`);
|
||||
const short = (text: string, n = 110) => text.replace(/\s+/g, ' ').slice(0, n);
|
||||
|
||||
let busy = false;
|
||||
let turnIdleSince = 0;
|
||||
let streamed = '';
|
||||
|
||||
const [answerWhen, ...answerRest] = (values.answer ?? '').split(':');
|
||||
const answerText = answerRest.join(':');
|
||||
|
||||
run.subscribe((event: TraceEvent) => {
|
||||
switch (event.type) {
|
||||
case 'model.start':
|
||||
busy = true;
|
||||
streamed = '';
|
||||
log(`▶ 模型请求 #${event.n}(历史 ${event.messageCount} 条)`);
|
||||
break;
|
||||
case 'model.delta':
|
||||
if (event.content) streamed += event.content;
|
||||
break;
|
||||
case 'model.end': {
|
||||
const calls = event.message.tool_calls?.map((c) => `${c.function.name}(${short(c.function.arguments, 70)})`) ?? [];
|
||||
log(`◀ 模型返回 #${event.n} ttft=${event.ttftMs}ms ${short(streamed)}${calls.length ? `\n 工具调用:${calls.join(' | ')}` : ''}`);
|
||||
if (!calls.length) {
|
||||
busy = false;
|
||||
turnIdleSince = Date.now();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'model.error':
|
||||
log(`✖ 模型错误 #${event.n} ${event.error}`);
|
||||
break;
|
||||
case 'tool.start':
|
||||
log(` ├ 工具开始 ${event.name} ${event.callId.slice(-6)}`);
|
||||
break;
|
||||
case 'tool.end':
|
||||
log(` └ 工具返回 ${event.callId.slice(-6)} ${short(event.result, 90)}`);
|
||||
break;
|
||||
case 'job.start':
|
||||
log(` ◆ 后台任务 ${event.jobId} 启动:${event.command}`);
|
||||
break;
|
||||
case 'job.end':
|
||||
log(` ◆ 后台任务 ${event.jobId} 退出 exit=${event.exitCode} ${(event.durationMs / 1000).toFixed(1)}s`);
|
||||
break;
|
||||
case 'question.ask':
|
||||
log(` ? 提问 ${event.questionId}(${event.mode})${event.question} [${event.options.join(' / ')}]`);
|
||||
scheduleAnswer(event.questionId);
|
||||
break;
|
||||
case 'question.answer':
|
||||
log(` ! 用户回答 ${event.questionId}:${event.answer}`);
|
||||
break;
|
||||
case 'inbox.push':
|
||||
log(` ✉ 收件箱 +${event.item.kind}${event.item.ref ? `(${event.item.ref})` : ''}`);
|
||||
break;
|
||||
case 'inbox.deliver':
|
||||
log(` ✉ 收件箱 → 历史:${event.ids.join(', ')}`);
|
||||
break;
|
||||
case 'run.stop':
|
||||
if (event.reason === 'error') log(`✖ 运行出错:${event.error}`);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
function scheduleAnswer(questionId: string) {
|
||||
if (!answerWhen) return;
|
||||
const fire = () => run.agent.answerQuestion(questionId, answerText);
|
||||
if (answerWhen === 'idle') {
|
||||
const timer = setInterval(() => {
|
||||
if (!busy && turnIdleSince && Date.now() - turnIdleSince > 1500) {
|
||||
clearInterval(timer);
|
||||
fire();
|
||||
}
|
||||
}, 200);
|
||||
} else {
|
||||
setTimeout(fire, Number(answerWhen));
|
||||
}
|
||||
}
|
||||
|
||||
if (values.steer) {
|
||||
const [when, ...rest] = values.steer.split(':');
|
||||
const text = rest.join(':');
|
||||
const fire = () => {
|
||||
log(` ✉ 用户插话:${text}`);
|
||||
run.agent.sendUserMessage(text);
|
||||
};
|
||||
if (when.startsWith('test+')) {
|
||||
const delay = Number(when.slice(5));
|
||||
let armed = true;
|
||||
run.subscribe((event) => {
|
||||
const isTest = (event.type === 'tool.start' && /npm (run )?test/.test(String((event.args as any)?.command ?? ''))) ||
|
||||
(event.type === 'job.start' && /npm (run )?test/.test(event.command));
|
||||
if (armed && isTest) {
|
||||
armed = false;
|
||||
setTimeout(fire, delay);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setTimeout(fire, Number(when));
|
||||
}
|
||||
}
|
||||
|
||||
log(`运行 ${run.runId},工作区 ${run.workspace}`);
|
||||
run.start();
|
||||
|
||||
const idleMs = Number(values['idle-ms']);
|
||||
await new Promise<void>((resolve) => {
|
||||
const timer = setInterval(() => {
|
||||
const tools = run.agent.tools;
|
||||
const settled =
|
||||
!busy && turnIdleSince && Date.now() - turnIdleSince > idleMs &&
|
||||
tools.runningJobs.size === 0 && tools.pendingQuestions.size === 0;
|
||||
const failed = run.events.some((e) => e.type === 'run.stop');
|
||||
if (settled || failed) {
|
||||
clearInterval(timer);
|
||||
resolve();
|
||||
}
|
||||
}, 300);
|
||||
});
|
||||
|
||||
run.stop();
|
||||
const lastDiff = [...run.events].reverse().find((e) => e.type === 'workspace.diff');
|
||||
console.log('\n===== 最终 diff =====\n' + (lastDiff && 'diff' in lastDiff ? lastDiff.diff : '(无)'));
|
||||
const dir = join(PROJECT_ROOT, 'recordings');
|
||||
mkdirSync(dir, { recursive: true });
|
||||
const file = join(dir, `${values.out ?? run.runId}.json`);
|
||||
writeFileSync(file, JSON.stringify(run.recording(), null, 1));
|
||||
console.log(`录制已保存:${file}`);
|
||||
process.exit(0);
|
||||
Reference in New Issue
Block a user