fix: spawn_workers 返回 Agent 指令(非 Skill)+ run_in_background

根因: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 <noreply@anthropic.com>
This commit is contained in:
AirLongDian
2026-06-11 15:24:30 +08:00
parent f3f991de2a
commit cda601a73a
2 changed files with 78 additions and 16 deletions

View File

@@ -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 <path>`
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 <result_path>`
## 约束
- 只修改属于此任务的文件
- 完成后必须运行 finish 命令
- 遇到无法解决的问题时返回 blocked 状态
"""
)
```
3. 每个 `Agent` 创建**独立子 Agent**,上下文不继承 Eng 对话(满足 INV-2 `fork_context=false`
4. 所有 Worker spawn 完成后,进入 monitor 状态
5. Worker 完成后,对其 `result.json` 调用 `--sub merge --result <path>`
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

View File

@@ -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-2fork_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,
})