From cda601a73a58433b29e7e09b94c325a7efa7353f Mon Sep 17 00:00:00 2001 From: AirLongDian Date: Thu, 11 Jun 2026 15:24:30 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20spawn=5Fworkers=20=E8=BF=94=E5=9B=9E=20A?= =?UTF-8?q?gent=20=E6=8C=87=E4=BB=A4=EF=BC=88=E9=9D=9E=20Skill=EF=BC=89+?= =?UTF-8?q?=20run=5Fin=5Fbackground?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因:Skill 工具只加载指令到当前上下文,不会创建子 Agent。 Agent 工具默认阻塞调用,Eng 派发后卡住无法进入 monitor。 修复: - spawn_workers() 返回 Agent 调用参数:{description, subagent_type, prompt, run_in_background} - run_in_background: true 确保 Eng 不被子 Worker 阻塞 - commands/eng.md 明确标注 Agent 工具 + run_in_background 派发模式:Eng → Agent(run_in_background:true) × N → monitor 轮询 → merge Co-Authored-By: Claude Opus 4.7 --- commands/eng.md | 51 +++++++++++++++++++++++-------- lib/air_runtime/modes/eng_mode.py | 43 +++++++++++++++++++++++--- 2 files changed, 78 insertions(+), 16 deletions(-) diff --git a/commands/eng.md b/commands/eng.md index 05b1205..af3e3fd 100644 --- a/commands/eng.md +++ b/commands/eng.md @@ -56,20 +56,47 @@ python "$HOME/plugins/airplan/scripts/airplan.py" --mode eng --project . --sub d dispatch 返回 `{waveId, taskIds, dispatchPath}`。**然后按以下步骤操作,不可跳过**: 1. 从 dispatch 返回值中取 `taskIds` 列表 -2. 对 `taskIds` 中的**每个** `tid`,顺序执行: - a. 读 `AirPlan/state/airarc/reviews/task-graph.json`,从 `nodes[tid].task` 取任务描述文本 - b. 调用 `Skill` 工具启动子代理: - - `skill`: `"airplan-v2:do"` - - `args`: `"--sub enter --task-id {tid} --task-text '{task描述}' --project ."` - c. 每个 `Skill` 调用自动以 `fork_context=false` 运行,创建隔离的 Do Worker -3. 所有 Worker spawn 完成后,进入 monitor 状态 -4. Worker 完成后,对其 `result.json` 调用 `--sub merge --result ` -5. merge 后 `task-graph.json` 节点状态自动同步为 DONE,不会被重复派发 +2. 对 `taskIds` 中的**每个** `tid`,在同一条消息中并发调用 `Agent` 工具(**必须** `run_in_background: true`,否则 Eng 会被阻塞等待子 Agent 完成): + ``` + Agent( + description: "Do Worker: {tid}", + subagent_type: "general-purpose", + run_in_background: true, + prompt: """你是 AirDo Worker,任务 ID: {tid}。 + 项目路径: {project_root} + + ## 任务 + {task_text} + + ## 文件范围 + {files} + + ## 完成标准 + {done_when} + + ## 工作流程 + 1. 先运行 `python scripts/airplan.py --mode do --sub enter --task-id {tid} --task-text '{task_text}' --project .` 初始化 + 2. 读取项目文件,理解现有代码结构 + 3. 实现任务需求,修改/创建源代码文件 + 4. 完成后运行 `python scripts/airplan.py --mode do --sub finish --task-id {tid} --result ` + + ## 约束 + - 只修改属于此任务的文件 + - 完成后必须运行 finish 命令 + - 遇到无法解决的问题时返回 blocked 状态 + """ + ) + ``` +3. 每个 `Agent` 创建**独立子 Agent**,上下文不继承 Eng 对话(满足 INV-2 `fork_context=false`) +4. 所有 Worker spawn 完成后,进入 monitor 状态 +5. Worker 完成后,对其 `result.json` 调用 `--sub merge --result ` +6. merge 后 `task-graph.json` 节点状态自动同步为 DONE,不会被重复派发 **注意**: -- 一个 task 对应一次 `Skill("airplan", ...)` 调用,多个 task 可以在同一条消息中并发发起 -- task-text 从 task-graph.json 获取,不是猜的 -- Do Worker 子代理的 allowed-tools 是 `[Read, Glob, Grep, Bash, Write, Edit]`,Agent 框架自动应用 +- 使用 `Agent` 工具,不是 `Skill` 工具——`Skill` 只加载指令到当前上下文,不创建子 Agent +- `Agent` 创建的每个子 Agent 拥有独立上下文,天然满足上下文隔离 +- 多个 `Agent` 调用可以在同一条消息中并发发起 +- prompt 中的 task-text/files/done_when 从 `task-graph.json` 获取 ### monitor diff --git a/lib/air_runtime/modes/eng_mode.py b/lib/air_runtime/modes/eng_mode.py index 8d2b1cf..c7ac665 100644 --- a/lib/air_runtime/modes/eng_mode.py +++ b/lib/air_runtime/modes/eng_mode.py @@ -615,8 +615,12 @@ def _select_ready_tasks(project_root: Path, max_count: int) -> list[str]: def spawn_workers(project_root: Path, task_ids: list[str]) -> list[dict]: - """T-1.21: 为每个 ready 任务准备 spawn 指令,返回 Agent 可直接消费的 Skill 调用参数列表。 - 不实际启动子进程——启动由 Agent 框架的 Skill 工具完成。 + """T-1.21: 为每个 ready 任务准备 Agent 派发指令。 + + 使用 Agent 工具(非 Skill 工具)spawn 隔离子 Agent。 + 每个子 Agent 有自己的上下文,不继承 Eng 的完整对话。这正是 INV-2(fork_context=false)。 + + 返回 Agent 调用参数列表,Eng Agent 遍历列表逐个调用。 """ tg_json = airplan_root(project_root) / "state" / "airarc" / "reviews" / "task-graph.json" graph = TaskGraph.load(tg_json) if tg_json.exists() else TaskGraph() @@ -624,9 +628,40 @@ def spawn_workers(project_root: Path, task_ids: list[str]) -> list[dict]: for tid in task_ids: node = graph.nodes.get(tid) task_text = node.task if node else "" + files = node.files_dirs if node else "" + done_when = node.done_when if node else "" + + prompt_parts = [ + f"你是 AirDo Worker,任务 ID: {tid}。", + f"项目路径: {project_root}", + "", + f"## 任务", + f"{task_text}", + "", + f"## 文件范围", + f"{files}" if files else "(无限制)", + "", + f"## 完成标准", + f"{done_when}" if done_when else "编译通过,无回归", + "", + "## 工作流程", + "1. 先运行 `python scripts/airplan.py --mode do --sub enter --task-id {tid} --task-text '{task_text}' --project {project_root}` 初始化 Worker 状态", + "2. 读取项目文件,理解现有代码结构", + "3. 实现任务需求,修改/创建源代码文件", + "4. 完成后运行 `python scripts/airplan.py --mode do --sub finish --task-id {tid} --result AirPlan/state/airdo/tasks/{tid}/result.json`", + "", + "## 约束", + "- 只修改属于此任务的文件", + "- 完成后必须运行 finish 命令", + "- 遇到无法解决的问题时返回 blocked 状态", + ] + prompt = "\n".join(prompt_parts).format(tid=tid, task_text=task_text, project_root=project_root) + instructions.append({ - "skill": "airplan-v2:do", - "args": f"--sub enter --task-id {tid} --task-text '{task_text}' --project .", + "description": f"Do Worker: {tid}", + "subagent_type": "general-purpose", + "prompt": prompt, + "run_in_background": True, # 关键:后台运行,Eng 不阻塞 "taskId": tid, "taskText": task_text, })