这篇想弄清楚几件事:一个子 agent 是由谁、按什么顺序造出来的?它的系统提示词、模型、工具集分别来自哪里?subprocess 和 in_process 两种执行方式差在哪?结果怎么回到主 agent?——以及:我想加一种自己的 agent 类型 / 自己的执行后端,该在哪一行下刀?
三层分工,只靠一个 dataclass 传话
多 agent 不是一个模块,是三层各管一段:coordinator/ 只回答「有哪些 agent 类型、它们长什么样」(定义层),tools/agent_tool.py 回答「这次派谁、带什么参数」(调度层),swarm/ 回答「用什么方式让它真的跑起来」(执行层)。三层之间只靠一个 dataclass————传话。
先看全程:一次 spawn 怎么走
主 agent 决定派活之后,数据流依次穿过四层:调度层 tools/agent_tool.py 查定义、组装 、拿执行器;定义层 coordinator/agent_definitions.py 按内置 → 用户 → 插件合并出 ;执行层 swarm/ 挑后端(in_process / tmux / subprocess)真正把任务跑起来;回收层 tasks/manager.py 用 管进程, 读输出、 hook 发通知。
下面的交互块可以逐层点开看职责与陷阱,并切换执行后端观察 的字段消耗:
第一段Leg 1调度层
tools/agent_tool.py这次派谁、带什么参数AgentTool.execute() 三步:① get_agent_definition(subagent_type) 查定义,查不到降级为裸 agent;② 组装 TeammateSpawnConfig——AgentDefinition 二十多个字段里只有 model / system_prompt / permissions 三个被取走;③ registry.get_executor("subprocess") 拿执行器并 spawn。
- 这一层的陷阱The trap here
- mode 参数不选后端。第 66 行硬编码 subprocess,mode 只是塞进 task_type 当标签——传 in_process_teammate 并不会走 in-process,因为 asyncio 内部 ID 用 task_* 工具查不到。刻意的取舍,不是 bug。
TeammateSpawnConfig(name, team, prompt, cwd, model, command, system_prompt, permissions, task_type)TeammateSpawnConfig 的字段去向Where the TeammateSpawnConfig fields go当前后端 subprocessbackend: subprocess
被读取 · 6 个Read · 6
- model
- system_prompt
- system_prompt_mode
- command
- task_type
- plan_mode_required
没人读 · 5 个Never read · 5
- name
- team
- parent_session_id
- color
- prompt
定义层:有哪些 agent,各长什么样
源码:src/openharness/coordinator/agent_definitions.py(975 行,其中约 400 行是内置提示词原文)。
核心是一个 Pydantic 模型 (agent_definitions.py:60)。字段可以分成五组:
- 身份 —
name/description:必填。description是给主 agent 模型看的「什么时候该派它」 - 提示词与工具 — /
tools/disallowed_tools:tools=None与tools=["*"]等价,都表示全开 - 模型与预算 —
model/effort/max_turns:model=None表示继承父进程 - 能力接入 —
skills/mcp_servers/required_mcp_servers/hooks:把第五、六、七篇的子系统按 agent 粒度接进来 - 生命周期 —
background/initial_prompt/memory/ /permission_mode:isolation="worktree"给它独立 git 工作树
三级合并,后写者赢
(agent_definitions.py:905)按 内置 → 用户 → 插件 的顺序往同一个 dict 里塞,key 是 name。所以:
- 内置
_BUILTIN_AGENTS(:510)—— general-purpose、Explore、Plan、verification、statusline-setup、claude-code-guide、worker - 用户
~/.openharness/agents/*.md—— 同名覆盖内置 - 插件 —— 同名覆盖用户
这跟第六篇里 Skills 的发现规则是同一个套路,但注意差别:Skills 有 bundled / user / extra dirs / project / plugin 五个来源、含项目级 .openharness/skills/;agent 定义只有三个来源,没有项目级目录。想给某个仓库配专属 agent,目前只能走插件。
一个用户 agent 长这样
放到 ~/.openharness/agents/db-reviewer.md,重启即生效——零行 Python:
---
name: db-reviewer
description: 审查数据库迁移脚本。当改动涉及 migrations/ 或 schema 时使用。
tools: Read, Grep, Glob, Bash
model: sonnet
effort: high
max_turns: 20
---
你是数据库迁移审查专家。只读地检查迁移脚本,重点关注:
可逆性、对存量数据的影响、锁表时长、索引缺失。
不要修改任何文件,把发现按严重度排序汇报。解析规则见 _parse_agent_frontmatter()(:633):YAML frontmatter 用 yaml.safe_load,解析失败会静默降级成朴素的 key: value 逐行切分——所以嵌套结构(hooks、mcpServers)写错缩进不会报错,只会悄悄丢掉。--- 之后的正文整体成为 。
get_agent_definition(name)(:948)每次调用都完整跑一遍 get_all_agent_definitions()——包括重新扫描磁盘目录和重新加载所有插件,没有任何缓存。单次 spawn 无所谓,但如果你的魔改要在循环里查定义(比如给模型动态列出可用 agent),记得自己在外面缓存一层。
调度层:这次派谁,带什么
源码:src/openharness/tools/agent_tool.py,全文只有 141 行,值得整篇读完。它做四件事:
- 校验
mode ∈ {local_agent, remote_agent, in_process_teammate} - 按
subagent_type查定义,查不到就是None,不报错(降级为无系统提示词的裸 agent) - 组装 ,调
executor.spawn(config) - 注册 hook 的完成监听器(回到第五篇的生命周期事件)
第 3 步是全篇最该盯住的十一行(agent_tool.py:68-79):
config = TeammateSpawnConfig(
name=agent_name,
team=team,
prompt=arguments.prompt,
cwd=str(context.cwd),
parent_session_id="main",
model=arguments.model or (agent_def.model if agent_def else None),
command=arguments.command,
system_prompt=agent_def.system_prompt if agent_def else None,
permissions=agent_def.permissions if agent_def else [],
task_type=arguments.mode,
)请数一下: 有二十多个字段,这里只有三个来自它(model / / permissions)。tools、disallowed_tools、permission_mode、max_turns、effort、skills、hooks、 全部止步于此。
第 66 行硬编码 registry.get_executor("subprocess"),mode 只是被塞进 task_type 当标签。所以传 mode="in_process_teammate" 并不会走 in-process 后端——代码注释说明了原因:in-process 任务返回的是 asyncio 内部 ID,task_* 系列工具查不到。这是刻意的取舍,不是 bug,但你魔改时必须知道。
执行层:怎么让它真的跑起来
源码:src/openharness/swarm/(约 4900 行,这篇只看两个后端 + 注册表)。
用哪个后端,按什么顺序判断
swarm/registry.py:128 的 ,三级优先:
- in_process —— 仅当之前 spawn 失败过、 被调用过
- tmux —— 在 tmux 会话里且后端已注册(当前未实现,见 :379 的
_register_defaults,tmux 注册被留白了) - subprocess —— 永远可用的兜底
后端是 Protocol(swarm/types.py:358 的 ),只要求几个成员:type、is_available()、spawn()、send_message()、shutdown()。这就是你加自定义后端的接缝。
两个后端,读的字段不一样
- 真开一个新进程:
python -m openharness --task-worker <flags> - 配置靠命令行参数传递(
build_inherited_cli_flags) - 加上继承的环境变量(
build_inherited_env_vars) - 通信:stdin 一行 JSON
- 崩溃隔离好;启动慢;跨平台稳
- 子进程被强制设
CLAUDE_CODE_COORDINATOR_MODE=0,避免递归进入协调者模式
- 同进程内一个
asyncio.Task,自己跑一遍 agent loop - 配置直接读 dataclass,不经过 CLI
- 通信:文件邮箱
~/.openharness/teams/<team>/agents/<id>/inbox/ - 有
TeammateAbortController支持协作式取消 - 启动快、能共享内存对象;一崩全崩
- 只在
supports_swarm_mailbox的平台注册
注意两者吃的配置字段并不相同:subprocess 用 model / / system_prompt_mode / plan_mode_required / command / task_type;in_process 用 name / team / parent_session_id / color / plan_mode_required / prompt——它连 都不读。同一个 dataclass,两个后端各取所需,没取的那些就是死字段。
subprocess 是怎么把配置变成命令行的
swarm/spawn_utils.py:109 的 ,逐条映射:
permission_mode="bypassPermissions"→--dangerously-skip-permissions:plan_mode_required=True时整段跳过——plan 优先于 bypasspermission_mode="acceptEdits"→--permission-mode acceptEdits:同上受 plan 压制model→--model X:值为"inherit"时不生成,改走环境变量- →
--system-prompt或--append-system-prompt:由system_prompt_mode == "append"决定 plugin_dirs→ 每个目录一条--plugin-dir:保证子进程看得见 inline 插件
所有值都过 shlex.quote()——因为这个列表最终可能被拼成 shell 字符串。你往里加新 flag 时必须保持这个习惯。
子 agent 的系统提示词不是主 agent 装配好递过去的,是子进程自己重跑一遍 build_runtime_system_prompt(),只不过多吃了一个 --system-prompt 覆盖。它自己发现自己的 CLAUDE.md、自己列自己的技能。这解释了为什么 omit_claude_md 需要是 上的一个字段。
最后一段:结果怎么回到主 Agent
SubprocessBackend.spawn() 不自己管进程,它调 BackgroundTaskManager.create_agent_task()(tasks/manager.py:49),拿回一个 。之后主 agent 有三条路知道结果:
- 主动轮询: /
task_list工具 →read_task_output(task_id, max_bytes=12000),注意有截断上限 - 被动通知:
register_completion_listener(),agent_tool.py:125 用它触发 hook - 结构化回传:coordinator_mode.py 的
format_task_notification()/parse_task_notification(),把结果包成<task-notification>XML 信封(含 status / summary / result / usage),XML 特殊字符走escape()
agent_tool.py:125-131 有个容易漏看的细节:注册监听器之后立刻又查一次任务状态,如果任务已经结束就手动补触发一次——这是在补竞态窗口(任务可能在注册完成前就跑完了)。你写任何「注册回调 + 异步完成」的代码都该抄这个模式。