feat(agent): L1 记忆注入——重试任务带上次失败原因喂回 prompt

- store.lastRunErrorOf(taskId):取最近一次失败 run 的 error(任意 kind)
- orchestrator.claimOne:写 job.json 时填充 task.lastRunError 下发 worker
- runner 三处 prompt(executor/planner/conflict)注入「## 上次失败原因」段(截断 2000 字符)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-24 22:09:06 +08:00
parent 7a6c826610
commit ecea1ae9b6
4 changed files with 97 additions and 2 deletions
+4 -1
View File
@@ -174,9 +174,12 @@ export function createOrchestrator(store: Store, log: OrchestratorLogger, deps:
const run = store.startRun(task.id, dbKind, { worktree: isPlanner ? project.repoPath : dir, branch });
runId = run.id;
// L1 记忆注入:重试任务带上「上次失败原因」,worker 端拼进 prompt(见 runner.buildPrompt*
const lastRunError = store.lastRunErrorOf(task.id);
d.writeJobSpec({
runId: run.id,
task: { ...task, status: isPlanner ? task.status : 'executing' },
task: { ...task, status: isPlanner ? task.status : 'executing', lastRunError },
project, worktreeDir: dir, branch, runKind,
});
+23 -1
View File
@@ -25,7 +25,24 @@ const DEFAULT_MAX_TURNS = 100;
/** 若项目未配置 timeoutMs 时的全局兜底超时(30min */
const DEFAULT_TIMEOUT_MS = 30 * 60_000;
/** 组装任务提示词:标题 + operations/spec 全文 + 执行约束 */
/**
* L1 记忆注入:若任务带「上次失败原因」(重试任务),生成一段提醒;否则空数组。
* 由 daemon 在 claimOne 时把 store.lastRunErrorOf 填进 task.lastRunError 下发(见 orchestrator)。
*/
function lastErrorLines(task: Task): string[] {
const err = task.lastRunError?.trim();
if (!err) return [];
return [
'## 上次失败原因(这是重试任务,请勿重蹈覆辙)',
'上一次执行本任务的 agent 失败,错误信息如下。请先理解失败根因,针对性规避:',
'```',
err.length > 2000 ? err.slice(0, 2000) + '\n…(已截断)' : err,
'```',
'',
];
}
/** 组装任务提示词:标题 + operations/spec 全文 + 上次失败(L1) + 执行约束 */
export function buildPrompt(task: Task): string {
const body = task.operations ?? task.spec ?? task.plan ?? '';
return [
@@ -35,6 +52,7 @@ export function buildPrompt(task: Task): string {
'## 任务内容',
body || '(无详细说明,按标题完成)',
'',
...lastErrorLines(task),
'## 执行约束(必须遵守)',
'- 只在当前工作目录(git worktree)内改动文件,不得读写或修改 worktree 之外的任何文件。',
`- 完成后用 \`git add -A && git commit\` 提交全部改动,commit message 必须包含任务 ID「${task.id}」。`,
@@ -117,10 +135,12 @@ export type PlannerFn = (task: Task, project: Project, kind: PlanKind, runId: st
/** planner 提示词:只读代码库,spec=写改动方案;decompose=拆子任务并在末尾输出 fenced JSON。 */
export function buildPlannerPrompt(task: Task, kind: PlanKind): string {
const head = [`# 任务:${task.title}`, `任务 ID${task.id}`];
const errLines = lastErrorLines(task);
if (kind === 'spec') {
if (task.spec) head.push('', '## 现有方案草稿(可改进/替换)', task.spec);
return [
...head, '',
...errLines,
'## 你的角色:方案作者(只读,不改任何文件)',
'只读这个代码库,为上述任务写一份「具体改动方案」。不要编辑文件、不要执行有副作用的命令。',
'## 输出(markdown 正文)',
@@ -133,6 +153,7 @@ export function buildPlannerPrompt(task: Task, kind: PlanKind): string {
if (task.plan) head.push('', '## 现有分析草稿(可改进/替换)', task.plan);
return [
...head, '',
...errLines,
'## 你的角色:任务拆解者(只读,不改任何文件)',
`当前任务层级 depth=${task.depth ?? 1}(根=1),最多拆到 depth=4(剩余可拆层数 ${Math.max(0, 4 - (task.depth ?? 1))} 层)。`,
'只读这个代码库,分析上述(复杂)任务,拆成 2–6 个更小、可独立交付的子任务。不要编辑文件、不要执行有副作用的命令。',
@@ -189,6 +210,7 @@ export function buildConflictPrompt(task: Task, conflictFiles: string[]): string
'## 冲突文件',
conflictFiles.length ? conflictFiles.map((f) => `- ${f}`).join('\n') : '(以 git status 为准)',
'',
...lastErrorLines(task),
'## 执行约束(必须遵守)',
'- 逐个打开冲突文件,理解 <<<<<<< HEAD 与 >>>>>>> 两侧的意图,保留双方有价值的改动,不得丢弃任一方的实质内容。',
'- 解决完所有冲突后,用 `git add -A` 暂存,然后 `git commit`(直接用默认合并 commit message,不要修改)。',
+11
View File
@@ -866,6 +866,17 @@ export class Store {
return rows.map(rowToApproval);
}
/**
* 取某任务最近一次失败 run 的错误信息(任意 kindexecutor/planner)。供 L1 记忆注入:
* 重试任务时把"上次为何失败"喂回 agent prompt,避免重蹈覆辙。无失败 run 返回 null。
*/
lastRunErrorOf(taskId: string): string | null {
const row = this.db.prepare(
`SELECT error FROM runs WHERE task_id = ? AND status = 'failed' AND error IS NOT NULL ORDER BY ended_at DESC LIMIT 1`,
).get(taskId) as { error?: string | null } | undefined;
return row?.error ?? null;
}
/** 列出所有处于审批闸状态(plan_review/spec_review/exec_review)的任务。 */
pendingApprovals(projectId?: string): Task[] {
// 包含 needs_attention:让审核区展示失败任务供人工确认/重排