From 74be3ecec1d89385e76a3716ee3c92903c61a12d Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Wed, 24 Jun 2026 22:32:21 +0800 Subject: [PATCH] =?UTF-8?q?feat(agent):=20L3=20=E8=AE=B0=E5=BF=86=E6=B3=A8?= =?UTF-8?q?=E5=85=A5=E2=80=94=E2=80=94=E5=AD=90=E4=BB=BB=E5=8A=A1=E5=B8=A6?= =?UTF-8?q?=E7=88=B6=E4=BB=BB=E5=8A=A1=E6=8B=86=E8=A7=A3=E6=84=8F=E5=9B=BE?= =?UTF-8?q?=20+=20=E5=85=84=E5=BC=9F=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - types.TaskContext + Task.context(daemon 装配、不持久化) - store.taskContextOf(taskId):父 plan + 同层兄弟概览(标题/状态/复杂度,排除自身) - orchestrator.claimOne:executor 子任务填充 context 下发(planner 跑在父任务上不需要) - runner.buildPrompt 注入「## 拆解背景」段(顺序:规范→背景→任务内容) - 仅 DB 读,不做 git/RAG 检索(成本控制) Co-Authored-By: Claude Opus 4.8 --- src/daemon/orchestrator.ts | 4 +++- src/executor/runner.ts | 22 ++++++++++++++++++++++ src/model/types.ts | 8 ++++++++ src/store/store.ts | 17 +++++++++++++++++ test/memory-inject.test.ts | 29 +++++++++++++++++++++++++++++ 5 files changed, 79 insertions(+), 1 deletion(-) diff --git a/src/daemon/orchestrator.ts b/src/daemon/orchestrator.ts index 53a4b90..4408886 100644 --- a/src/daemon/orchestrator.ts +++ b/src/daemon/orchestrator.ts @@ -176,10 +176,12 @@ export function createOrchestrator(store: Store, log: OrchestratorLogger, deps: // L1 记忆注入:重试任务带上「上次失败原因」,worker 端拼进 prompt(见 runner.buildPrompt*) const lastRunError = store.lastRunErrorOf(task.id); + // L3 拆解背景:executor 子任务带父任务意图 + 兄弟状态(planner 跑在父任务上,不需要) + const context = isPlanner ? null : store.taskContextOf(task.id); d.writeJobSpec({ runId: run.id, - task: { ...task, status: isPlanner ? task.status : 'executing', lastRunError }, + task: { ...task, status: isPlanner ? task.status : 'executing', lastRunError, context }, project, worktreeDir: dir, branch, runKind, }); diff --git a/src/executor/runner.ts b/src/executor/runner.ts index 18feb62..21eecd6 100644 --- a/src/executor/runner.ts +++ b/src/executor/runner.ts @@ -39,6 +39,27 @@ export function rulesHeaderLines(project?: Project, globalRules?: string | null) return out; } +/** + * L3 拆解背景注入:若任务带 context(父任务拆解意图 + 兄弟任务状态),生成背景段;否则空数组。 + * 由 daemon 在 claimOne 时把 store.taskContextOf 填进 task.context 下发(仅 executor 子任务)。 + */ +function contextLines(task: Task): string[] { + const ctx = task.context; + if (!ctx) return []; + const out: string[] = []; + const plan = ctx.parentPlan?.trim(); + const sibs = ctx.siblings ?? []; + if (!plan && sibs.length === 0) return []; + out.push('## 拆解背景(你是某个大任务拆出的子任务)'); + if (plan) out.push('父任务的拆解意图:', plan, ''); + if (sibs.length) { + out.push('同批兄弟子任务(注意衔接,不要重复或冲突):'); + for (const s of sibs) out.push(`- 「${s.title}」(${s.complexity},状态:${s.status})`); + out.push(''); + } + return out; +} + /** * L1 记忆注入:若任务带「上次失败原因」(重试任务),生成一段提醒;否则空数组。 * 由 daemon 在 claimOne 时把 store.lastRunErrorOf 填进 task.lastRunError 下发(见 orchestrator)。 @@ -64,6 +85,7 @@ export function buildPrompt(task: Task, project?: Project, globalRules?: string `任务 ID:${task.id}`, '', ...rulesHeaderLines(project, globalRules), + ...contextLines(task), '## 任务内容', body || '(无详细说明,按标题完成)', '', diff --git a/src/model/types.ts b/src/model/types.ts index a286e27..b409b09 100644 --- a/src/model/types.ts +++ b/src/model/types.ts @@ -65,6 +65,12 @@ export interface TaskResult { mergeTaskId: string | null; // 自动合并失败时建的最高优先级补救任务 id(幂等标记) } +/** L3 拆解背景:父任务拆解意图 + 同层兄弟任务概览(daemon 装配,注入子任务执行 prompt) */ +export interface TaskContext { + parentPlan?: string | null; + siblings?: Array<{ title: string; status: string; complexity: string }>; +} + export interface Task { id: Id; projectId: Id; @@ -85,6 +91,8 @@ export interface Task { retryBaseline: number; // 上次手动重投时已有的失败 run 数(重置重试计数用) nextEligibleAt: string | null; // 持久化退避:早于此时间不被领取(重试退避,重启不丢);null=即刻可领 lastRunError?: string | null; // needs_attention 时:最近一次失败 run 的错误信息(供审核区展示) + /** L3 拆解背景:daemon 在 claimOne 时装配进 job(父任务意图 + 兄弟任务状态),仅注入 prompt,不持久化 */ + context?: TaskContext | null; attachments?: Attachment[]; // 随任务提交的图片/文件(存 /tasks//attachments/) createdAt: string; updatedAt: string; diff --git a/src/store/store.ts b/src/store/store.ts index 483bdf4..d4e952d 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -887,6 +887,23 @@ export class Store { return row?.error ?? null; } + /** + * L3 拆解背景:取某任务的父任务拆解意图(parent.plan)+ 同层兄弟任务概览(标题/状态/复杂度)。 + * 无 parentId → null。仅做便宜的 DB 读,不做 git/RAG 检索。 + */ + taskContextOf(taskId: string): import('../model/types.js').TaskContext | null { + const row = this.getTaskRow(taskId); + if (!row || !row.parent_id) return null; + const parent = this.db.prepare(`SELECT plan FROM tasks WHERE id = ?`).get(row.parent_id) as { plan?: string | null } | undefined; + const sibs = this.db.prepare( + `SELECT title, status, complexity FROM tasks WHERE parent_id = ? AND id != ? ORDER BY created_at`, + ).all(row.parent_id, taskId) as Array<{ title: string; status: string; complexity: string }>; + return { + parentPlan: parent?.plan ?? null, + siblings: sibs, + }; + } + /** 列出所有处于审批闸状态(plan_review/spec_review/exec_review)的任务。 */ pendingApprovals(projectId?: string): Task[] { // 包含 needs_attention:让审核区展示失败任务供人工确认/重排 diff --git a/test/memory-inject.test.ts b/test/memory-inject.test.ts index 656b6b2..96ddc85 100644 --- a/test/memory-inject.test.ts +++ b/test/memory-inject.test.ts @@ -69,6 +69,35 @@ test('L4: globalRules 注入且排在项目规范之前', () => { assert.ok(p.indexOf('GLOBAL_RULE') < p.indexOf('任务内容')); }); +test('L3: context 注入父任务意图 + 兄弟状态,无 context 不注入', () => { + const t = fakeTask({ + context: { + parentPlan: '把登录拆成前后端', + siblings: [{ title: '前端表单', status: 'done', complexity: 'easy' }], + }, + }); + const p = buildPrompt(t); + assert.ok(p.includes('拆解背景')); + assert.ok(p.includes('把登录拆成前后端')); + assert.ok(p.includes('前端表单')); + assert.ok(p.indexOf('拆解背景') < p.indexOf('任务内容'), '背景在任务内容之前'); + assert.ok(!buildPrompt(fakeTask()).includes('拆解背景')); +}); + +test('taskContextOf 取父 plan + 兄弟概览(排除自身)', () => { + const s = new Store(':memory:'); + const pj = s.createProject({ name: 'l3', repoPath: '/tmp/l3-' + Math.random() }); + const parent = s.createTask({ projectId: pj.id, title: '大', complexity: 'hard' }); + s.setPlan(parent.id, '拆成 a 和 b'); + const a = s.createTask({ projectId: pj.id, parentId: parent.id, title: 'a', complexity: 'easy' }); + s.createTask({ projectId: pj.id, parentId: parent.id, title: 'b', complexity: 'easy' }); + assert.equal(s.taskContextOf(parent.id), null, '根任务无 parent → null'); + const ctxA = s.taskContextOf(a.id)!; + assert.equal(ctxA.parentPlan, '拆成 a 和 b'); + assert.deepEqual(ctxA.siblings!.map((x) => x.title), ['b'], '兄弟里不含自身 a'); + s.close(); +}); + test('lastRunErrorOf 取最近一次失败 run 的错误(任意 kind)', () => { const s = new Store(':memory:'); const p = s.createProject({ name: 'l1', repoPath: '/tmp/l1-' + Math.random() });