// 四个级别用同一个 Loop、同一份订单服务代码,每级只多一个能力。 import type { LevelInfo, QuestionMode } from '../shared/types.ts'; export type LevelConfig = LevelInfo & { backgroundBash: boolean; question: QuestionMode | null; systemPrompt: string; }; const BASE = `你是一个在本地代码仓库里工作的编码 Agent,工作目录是订单服务 order-service 的根目录。始终用中文回复。 - 用 bash 查看和搜索代码,用 edit_file 修改文件。先看 README.md 和 docs/api-contract.md 了解约定。 - 每次调用工具前,用一两句中文说明你接下来做什么、为什么。 - 只根据工具真实返回的内容下结论,没看到的结果不要编造。 - 任务完成后,用几句话汇报改了什么、测试结果如何。`; const BACKGROUND = ` - 完整的 npm test 大约要 15 秒。跑它时把 run_in_background 设为 true,命令会在后台运行,工具立刻返回 job_id,你可以接着做不依赖测试结果的事。 - 命令退出后,系统会发给你一条 消息,里面有退出码和输出摘要。在收到它之前,不要声称测试通过或失败。 - 和 只由系统发出。你绝对不要自己写出这些标签,也不要预测或模拟它们的内容。 - 不要用 sleep、循环查询之类的办法等后台结果。如果手上的事都做完了,只剩后台结果没回来,就说明你在等什么,然后结束这一轮回复(不再调用工具)。结果到达时你会被自动唤醒。`; const QUESTION_BLOCKING = ` - 需要用户做决定时调用 question。它会一直等到用户回答才返回,返回值就是用户的选择。`; const QUESTION_ASYNC = ` - 需要用户做决定时调用 question。它不会等回答,立刻返回 {"accepted":true,"question_id":...}。 - 用户回答后,答案会以 消息送达。 - 等待期间继续做不依赖答案的工作,例如查清所有调用方、跑基线测试、确认要改的文件。 - 走到依赖答案的那一步时,不要替用户假设答案,也不要用 sleep 等待。说明你停在哪一步、在等哪个问题,然后结束这一轮回复(不再调用工具)。答案到达时你会被自动唤醒。`; const TASK_DECIDED = '把订单接口 GET /orders/:id 返回体里的状态字段 status 换成 orderStatus(取值不变)。旧字段 status 保留一个版本,在代码里注明已废弃。仓库内的调用方改读 orderStatus,接口文档同步更新。动手前先跑一遍 npm test 确认基线,改完再跑一遍验证。'; const TASK_ASK = '把订单接口 GET /orders/:id 返回体里的状态字段 status 换成 orderStatus(取值不变)。仓库内的调用方改读 orderStatus,接口文档同步更新。旧字段 status 要不要保留一个版本会影响仓库外的调用方,这个请你一开始就先用 question 问我,选项给「保留一版」和「直接移除」。动手前先跑一遍 npm test 确认基线,改完再跑一遍验证。'; export const LEVELS: LevelConfig[] = [ { id: 1, name: '同步 Bash', title: '最小 Agent:消息历史 + 模型调用 + 同步 bash', adds: 'Loop 发出工具调用后等命令退出,把结果加进历史,再请求模型。', watch: '测试在输出日志,模型请求计数停在当前这一次。测试退出后,下一次请求才发出。这期间发的消息会进收件箱,等工具返回后才交给模型。', tools: ['bash', 'edit_file'], defaultTask: TASK_DECIDED, backgroundBash: false, question: null, systemPrompt: BASE, source: [ { file: 'agent/agent.ts', anchor: 'runTurn' }, { file: 'agent/tools.ts', anchor: 'runForeground' }, ], }, { id: 2, name: '后台 Bash', title: '同一个 bash 加 run_in_background', adds: '启动进程后先返回 job_id,命令退出时运行时把退出码和日志摘要放进收件箱,交回原会话。', watch: '测试还在跑,下一次模型请求已经发出,Agent 去查调用方。完成通知到达后,它才汇报测试结果。', tools: ['bash', 'edit_file'], defaultTask: TASK_DECIDED, backgroundBash: true, question: null, systemPrompt: BASE + BACKGROUND, source: [ { file: 'agent/tools.ts', anchor: 'startBackground' }, { file: 'agent/agent.ts', anchor: 'deliverInbox' }, ], }, { id: 3, name: '阻塞提问', title: '把等待对象换成人:AiWork 式阻塞 question', adds: 'question 工具一直等到用户回答才返回。答案就是工具结果。', watch: '问题卡挂着的时候,模型请求停在提问的那一次。这期间发的消息进了收件箱,但 Agent 不会继续工作。', tools: ['bash', 'edit_file', 'question'], defaultTask: TASK_ASK, backgroundBash: true, question: 'blocking', systemPrompt: BASE + BACKGROUND + QUESTION_BLOCKING, source: [{ file: 'agent/tools.ts', anchor: 'askBlocking' }], }, { id: 4, name: '异步提问', title: 'Codex 式异步 question:先回执,答案带问题 ID 回来', adds: 'question 立刻返回 accepted,答案作为带 question_id 的用户消息进收件箱。', watch: '回答之前 Agent 已经查完调用方,停在需要答案的那一步。选「保留一版」或「直接移除」后,它做对应的修改。提前回答的话,答案在下一次请求里被消费。', tools: ['bash', 'edit_file', 'question'], defaultTask: TASK_ASK, backgroundBash: true, question: 'async', systemPrompt: BASE + BACKGROUND + QUESTION_ASYNC, source: [{ file: 'agent/tools.ts', anchor: 'askAsync' }], }, ]; export function getLevel(id: number): LevelConfig { const level = LEVELS.find((item) => item.id === id); if (!level) throw new Error(`没有第 ${id} 级`); return level; }