PyClaw 的子代理系统让主 agent 把自包含的任务委派给专用人格(persona):每个人格 拥有独立的上下文窗口、专属模型、工具白名单。每个人格就是一个带 YAML 前置元数据 的 Markdown 文件,无需安装插件、无需编写类、无需重新构建。
当前状态:子代理基础设施于 2026-05-18 上线。PyClaw 自身不打包任何 persona 文件(保持 MIT 许可证清洁),用户自行编写或导入。
-
把清单文件放到五个发现目录之一:
<workspace>/agents/<name>.md ← 项目本地,最高优先级 <workspace>/.agents/agents/<name>.md ← 项目级(跨生态标准) <workspace>/.claude/agents/<name>.md ← 项目级(Claude Code 兼容) ~/.pyclaw/agents/<name>.md ← managed(PyClaw 用户共享) ~/.agents/agents/<name>.md ← personal 跨生态 fallback<workspace>在哪里 — 三种入口的可见性矩阵:层 路径 Web channel 多用户 飞书 channel CLI / library 1 personal ~/.agents/agents/✅ 跨用户共享 ✅ 跨 chat 共享 ✅ 2 managed ~/.pyclaw/agents/✅ 跨用户共享 (推荐) ✅ 跨 chat 共享 (推荐) ✅ 3 .claude <ws>/.claude/agents/✅ per-user override ✅ per-chat override ✅ project-local 4 .agents <ws>/.agents/agents/✅ per-user override ✅ per-chat override ✅ project-local 5 workspace <ws>/agents/✅ per-user override (最高优先级) ✅ per-chat override (最高优先级) ✅ project-local (最高优先级) <ws>由入口决定:- Web channel:
~/.pyclaw/workspaces/web_<user_id>/(D26 多租户隔离 — project root<repo>/agents/不可见) - 飞书 channel:
~/.pyclaw/workspaces/feishu_<chat_id>/ - CLI / library: 调用方传入的
workspace_path(常常是 project root, 这时<repo>/agents/直接命中第 5 层)
团队部署推荐 (Web channel 多用户场景):
- 团队共享 manifest →
~/.pyclaw/agents/(所有用户都能看到) - 用户个人 override →
~/.pyclaw/workspaces/web_<user>/agents/(仅本人可见, 同名覆盖共享层)
个人 dev 推荐 (CLI / library):
- 直接放 project root
<repo>/agents/即可,workspace_path默认就是 project root, 命中第 5 层。
- Web channel:
-
编写一个最小清单:
--- name: oracle description: 复杂架构与疑难调试的深度推理顾问。 --- 你是 Oracle。先复述问题、列出约束、逐步推理,最后把发现回传给主 agent。
-
重启 PyClaw。主 agent 现在就有
sub_agent工具了。在聊天里说:请咨询 oracle 关于 X 的问题。 -
查看已安装的人格:
/agents list /agents info oracle /agents reload # 新放入 .md 后重新扫描
单一的 770 行 agent 主循环在每次工具调用、每次记忆命中、每次用户来回时都会累积 上下文。复杂任务到第 20 轮以上时,prompt cache 污染和 context budget 压力会 明显降低推理质量。子代理通过每次委派独立的全新上下文解决这个问题:
- 架构 / 调试专家("oracle")—— 在难题出现时启动,把发现回传给主 agent
- 代码库研究员("explore")—— 并行启动做 grep / read / 阅读理解, 返回简洁的 file:line 列表
- 外部文档研究员("librarian")—— 启动后抓取 / 搜索 / 消化外部库文档
业界趋同的实现模式(Claude Code Task、OpenCode task + agent、Roo Code
custom modes、Gemini CLI invoke_agent)都是这一形态:用 YAML frontmatter
- Markdown 文件定义人格。
PyClaw 的 AgentManifest 是跨 agent 生态 SKILL.md / agent-frontmatter 标准的
严格子集。必填字段:
name: str—— 小写字母 / 数字 / 连字符,1–64 字符,必须等于文件名 (去掉.md)。例:oracle.md→name: oracle。description: str—— 1–1024 字符。主 agent 用它判断是否调用。要具体: 不要写"代码评审员",而要写"评审 PR diff 的 Python 类型安全、安全漏洞、 SOP 合规性;为每个问题给出 file:line"。
可选字段:
model: str | null—— provider/model id(如anthropic/claude-opus-4-7)。 覆盖pyclaw.json中subagent.model_routing的设置。allowed_tools: list[str]—— 工具名白名单;不设置时,子代理继承父代理 能访问的所有工具(除了始终禁用的记忆工具——见下文)。max_turns: int—— 子代理循环迭代上限。默认 30。timeout_seconds: float—— wall-clock 超时(硬上限 1800s)。默认 300。tools: list[str]——allowed_tools的别名(Anthropic 风格 frontmatter)。
Pydantic extra = "ignore" 配置意味着跨生态字段如 mode、
disable-model-invocation、paths、hooks 都会静默通过。PyClaw 实际识别的
字段就是上面列出的;其他字段有文档但不强制执行。
---
name: code-reviewer
description: 评审 PR diff 的 Python 类型安全、安全问题、SOP 违规。每个问题给出 file:line。返回结构化清单。
model: anthropic/claude-opus-4-7
allowed_tools: [read, grep, glob]
max_turns: 8
timeout_seconds: 180
---
你是资深 Python 代码评审员,深谙:
- 类型安全(无 `Any`、无未注明理由的 `# type: ignore`)
- 安全(无硬编码密钥、无 SSRF、无 SQL 注入)
- SOP 合规(符合 CLAUDE.md 的团队约定)
输出格式:
- 一行一个发现:`<file>:<line> — <严重度> — <描述>`
- 末尾给出 `OK to merge: yes/no`主 agent 调用 sub_agent(agent_name="oracle", task="...") 时,executor 会
基于父 ToolContext 构造新的 ToolContext,按引用共享 7 个字段,零覆盖
能力:
| 字段 | 是否继承 | 原因 |
|---|---|---|
permission_tier |
是(严格相等) | 子代理无法升级(read-only 父代理永远不会派生 yolo 子代理) |
sandbox_policy |
是(同实例) | Sprint 3 的 srt / Seatbelt / bwrap+seccomp 同样包裹子代理的每个 bash 调用 |
workspace_path |
是 | 子代理在同一 workspace 边界内读写 |
abort (asyncio.Event) |
是(同实例) | 父 abort 在 ~2s 内传递到子代理循环 |
user_id, role, user_profile |
是 | 审计链路不中断 |
subagent_depth |
递增 | 用于 depth-1 强制限制 |
session_id |
替换 | <父>#sub:<uuid7>,便于审计日志关联 |
记忆继承是只读的:4 层记忆搜索结果会作为 <memory> XML 注入到子代理的
system prompt,但三个记忆写工具(memorize、forget、update_working_memory)
在 executor 层被屏蔽,与 manifest.allowed_tools 无关。即使清单声明
allowed_tools: [memorize, ...] 也无法绕过 —— 子代理把发现回传给主 agent,
由主 agent 决定记忆什么。
按 tool-approval-tiers 能力规范,子代理严格继承父代理的有效权限层:
read-only父代理 → 子代理运行在read-only。子代理循环内的 write-class 工具调用按 Sprint 1 规则自动拒绝。approval父代理 → 子代理运行在approval。子代理循环内的工具调用走同一个ToolApprovalHook实例,作用域为子代理的session_id。yolo父代理 → 子代理运行在yolo。
SubAgentTool 自身的 tool_class="write",因为父代理无法分类子代理会传递
调用什么。这意味着:
read-only层下,sub_agent调用自动拒绝且不弹窗。Read-only session 无法 fan out 到可能开销巨大的子代理推理。approval层下,sub_agent评估通道的tools_requiring_approval列表。 运维可把"sub_agent"加到该列表,让派生需要用户批准。
详见 permissions guide,了解完整的层级模型以及子代理 调用如何嵌入 Sprint 3 的 per-call effective-tier 解析路径。
Sprint 3 的 SrtPolicy(macOS Seatbelt / Linux bwrap+seccomp)以零新代码
包裹子代理循环内的每次 bash 调用,因为 sandbox_policy 是按引用共享的。
父代理若运行在 sandbox.policy = "srt",每个子代理的 bash 调用都走同一个
srt wrapper。详见 sandbox guide。
pyclaw.json 中 production_require_sandbox = true 时,NoSandboxPolicy
拒绝加载 —— 子代理继承与主代理同样严格的 floor。
pyclaw.json 顶层 subagent 块(所有字段均可选):
{
"subagent": {
"enabled": true,
"workspaceSubagentsDir": "agents",
"projectSubagentsDir": ".agents/agents",
"projectClaudeSubagentsDir": ".claude/agents",
"personalSubagentsDir": "~/.agents/agents",
"managedSubagentsDir": "~/.pyclaw/agents",
"modelRouting": {
"oracle": "anthropic/claude-opus-4-7",
"explore": "anthropic/claude-haiku-4-5"
},
"defaultMaxTurns": 30,
"defaultTimeoutSeconds": 300,
"maxDepth": 1
}
}model_routing 让你按 persona 指定模型。解析顺序:
manifest.model > settings.modelRouting[name] > settings.agent.defaultModel。
高频简单 persona 用便宜模型(Haiku、Llama),把 Opus 留给 oracle 这类深度
推理。
maxDepth = 1 意味着子代理本身不能再派生子代理。V1 默认严格;未来如观察到
合理递归用例可升高。
/agents list # 按 source 标签分组的 persona 表 + 实际生效模型
/agents info oracle # 完整清单 dump + 实际生效模型 + 文件路径
/agents reload # 重新扫描全部 5 个发现目录(idle 防护)
/agents reload 在子代理调用进行中时拒绝执行,避免运行时 registry 变更。
PyClaw 是 MIT 许可证。PyClaw 任何分发包都不包含 persona 文件。persona 从哪里来?
- 自己写 —— 最常见,最快。
- 从 MIT 来源导入 ——
obra/superpowers是 MIT 许可的精选 agent skills/personas 集合;把相关.md文件拖入~/.pyclaw/agents/。 - 依据个人非商业例外条款手写 —— 某些 source-available 集合(如 OhMyOpenAgent 在 Sustainable Use License v1.0 下)允许个人非商业使用; 你可以受其结构启发手写 persona,但不能再分发它们的文件。
驱动约束:OhMyOpenAgent 的 Sustainable Use License v1.0 禁止 "derivative-to-compete distribution"。PyClaw 选择"只发基础设施,零打包 persona",保持 PyClaw 干净的 MIT 可发布性。
本次发布是 option A'(基于文件的发现 + register(source=...) 的向前兼容
税)。未来的 add-plugin-protocol 变更(option B)通过以下方式扩展为统一
插件系统:
- 增加第 6 个发现 source ——
("plugin:<name>", iter)。 - 插件 loader 调用
registry.register(name, manifest, source=f"plugin:{name}")。
本次变更付出的 5 LOC 税(公开的 register() API + source 参数 +
subagent_depth 字段)让插件触发条件(≥3 个第三方插件请求或 PyClaw 用户
≥ 10k)成熟时,option B 是零返工增量。详见
ROADMAP(私有)。
清单未出现在 /agents list:
- 检查文件名是否与 frontmatter
name匹配(区分大小写)。oracle.md必须name: oracle,不能是Oracle或oracles。 - 检查文件以
.md结尾。 - 检查它在目录顶层,不是嵌套在子目录里。
- 运行
/agents reload(或重启 PyClaw)。 - 检查日志中
subagent.discovery.parse_error警告。
子代理超时返回截断输出:
返回内容会以 [Sub-agent timed out at <duration>s with <turns> turns]\n
开头,让主 agent 看到截断信号。审计日志记 outcome: "timeout"。要么
提高 manifest.timeout_seconds,要么简化任务。
子代理调用 memorize 被拒:
这是设计 —— 见上文"记忆继承是只读的"。让子代理返回发现,主 agent 来
调用 memorize。
read-only 父代理下子代理无法 bash:
这是设计。Read-only 层对所有 write-class 工具调用自动拒绝,与谁调用无关。
要允许写,父代理必须运行在 approval 或 yolo。
跨生态字段被静默忽略(disable-model-invocation、mode、paths、
hooks):
PyClaw 的 extra = "ignore" Pydantic 配置丢弃未知字段。识别清单见上文。
如果需要 PyClaw 识别新字段,请提 issue —— 通常是 AgentManifest 一行加。
每次子代理派生在 pyclaw.audit.subagent logger 上发出一行 JSON:
{
"event": "subagent_invocation",
"event_type": "subagent_spawn",
"ts": "2026-05-18T19:30:45Z",
"subagent_name": "oracle",
"subagent_source": "managed",
"parent_session_id": "ses_abc",
"subagent_depth": 1,
"outcome": "success",
"turns": 12,
"tokens_input": 5432,
"tokens_output": 1234,
"duration_seconds": 23.4,
"model_used": "anthropic/claude-opus-4-7",
"tier_inherited": "approval"
}子代理循环内的工具调用在原有 pyclaw.audit.tool_approval channel 上携带
4 个增强字段(subagent_name、subagent_source、parent_session_id、
subagent_depth),与 Sprint 1/3 字段(tier、decision、tier_source、
forced_server、user_id、role、sandbox_backend)并存。
outcome 枚举:success、timeout、error、depth_exceeded、
tool_whitelist_violation、aborted、max_turns_exceeded。
从 V1.5 Phase 1(Sprint 10)起,主代理可以同一轮发出多个 sub_agent
工具调用,运行时通过 asyncio.gather 并发执行,把多个独立结果分别返回
给主代理。
示例:主代理在一轮里同时发 3 个 sub_agent 工具调用:
主代理发出: [
sub_agent(agent_name="oracle", task="research file A"),
sub_agent(agent_name="reviewer", task="review file B"),
sub_agent(agent_name="summarizer", task="summarize file C"),
]
运行时 fan-out 同时跑 3 个,按原索引顺序返回 3 个独立 ToolResult。
支持跨 persona fan-out(同一轮里 agent_name 可不同)。
| 配置项 | 默认值 | 范围 | 作用 |
|---|---|---|---|
subagent.max_parallel_per_turn |
5 |
1..20 |
每轮最多并发的子代理数。超出部分返回 [Sub-agent rate-limited] 错误。 |
同一并行批次共享一个 progress_batch_id(uuid4 hex),写入该批次每个
subagent_invocation 审计事件:
{"event":"subagent_invocation","subagent_name":"oracle","progress_batch_id":"abc...","..."}
{"event":"subagent_invocation","subagent_name":"reviewer","progress_batch_id":"abc...","..."}
{"event":"subagent_invocation","subagent_name":"summarizer","progress_batch_id":"abc...","..."}SOC 工具可 grep "progress_batch_id":"abc..." 回溯出同一轮 spawn 的全部
子代理。批次大小为 1 时该字段不出现(V1 审计字节兼容)。
若批次中某个子代理失败(异常 / timeout),兄弟子代理继续到完成。失败
子代理的结果包装为 is_error=True ToolResult,文本为:
[Sub-agent failed: <ExcType>] <message>一般异常[Sub-agent timed out at <N>s]超时(保持 V1 单调用格式)[Sub-agent aborted by parent] <name>父代理 abort 取消时
父代理的 abort 事件仍然取消所有批次成员(V1 invariant 5 保留)。
同一轮内,运行时按 3 个桶执行:
parallel(read 类工具,side_effect=False)—— 并发sequential(write 类工具,side_effect=True,非 sub_agent)—— 串行subagent_parallel(仅 sub_agent 工具调用)—— 并发
这意味着如果一个 bash 写操作依赖某个子代理的输出,必须分两轮发
(子代理放第 N 轮,消费者放第 N+1 轮)。frozen prefix 中的 supervisor
advisory 提示词会告知 LLM 这一规则——见 system_prompt.py 的
subagent_advisory_section。
当 fan-out 产生 N 个并行结果且结构 / 主题相似时,主代理可调
reduce_subagent_results(strategy=..., results=[...]) 把它们合并为
单条消息。
策略:
| 策略 | 行为 |
|---|---|
concat |
用 \n\n---\n\n 分隔符拼接。不会失败。 |
merge_json |
每条结果按 JSON 解析,合并为 JSON 数组。解析失败时回落 concat + 在审计字段 fallback_reason 记录原因。 |
vote |
返回出现次数最多的结果(并列时取第一个)。 |
first_success |
返回第一个非错误结果。错误检测仅靠 PyClaw 控制的前缀([Sub-agent failed:、[Sub-agent timed out at、[Sub-agent aborted by parent])。全部失败时返回聚合错误,以 \n---ALL FAILED---\n 分隔。 |
可见性:仅主代理可见。子代理调 reduce_subagent_results 会得到
error_result(call_id, "reduce_subagent_results is parent-agent-only; sub-agents return raw results")。
审计:每次 reducer 调用发出 pyclaw.audit.subagent.subagent_batch_reduced
事件,含 strategy_used、n_inputs、可选 fallback_reason。
Phase 3 引入 supervisor-mediated 链式委派:子代理 A 跑完后可通过 request_handoff 工具显式声明 "把这个任务交给 reviewer",主代理(supervisor)在下一轮 LLM 调用时看到 handoff_request metadata,然后自主决定是否派发下一个子代理。
- 保留 depth-1 不变量:handoff 是 supervisor-mediated(通过主代理派发新 sub_agent),不是 sub-agent 直接 spawn 嵌套子代理。所有 8 条 V1 不变量保留。
- Main LLM 完全自主:no auto-cascade。主代理可以 (1) 派发建议的 sub_agent;(2) 通过
reduce_subagent_results合并多个 handoff_request 后派发一个;(3) 直接忽略 handoff intent 自己回复。 - Session-isolated 状态:
pending_handoffs[session_id]严格按会话隔离,防止跨租户泄露。 - Redis 持久化 + worker failover hydrate:通过 sha256-hashed key 存到 Redis,TTL =
pending_handoff_ttl_turns × 600s(默认 1800s)。
可选白名单字段,二态语义(per Q2 follow-up):
---
name: coder
description: 写代码并把结果交给 reviewer
allowed_tools: [request_handoff]
can_handoff_to: [reviewer] # 仅可转交给 reviewer
---| 值 | 语义 |
|---|---|
不写 / None |
open —— 可转交给任何已注册的 sub-agent |
| 非空 list | whitelist —— 只能转交到列表中的目标 |
空列表 [] |
parse 时拒绝(语义模糊) |
跨字段一致性:当 can_handoff_to 设了非空 list 时,allowed_tools(如果也设了)必须包含 request_handoff,否则 manifest 在解析时被拒(防止"声明可以转交但工具不可用"的死配置)。
子代理可见、主代理不可见(dispatch-time block)。schema:
{
"target": "reviewer",
"task": "review the diff for security",
"context_hint": "focus on auth.py"
}校验顺序(首次失败即生效,"first attempt wins"):
- 格式校验:
target匹配^[a-z][a-z0-9-]*$;task8-8192 codepoints + 至少 4 个不同字符;context_hint≤16384 codepoints。格式失败不计入 dedup(允许重试)。 - Dedup:第一次进入步骤 3 之后就锁定,后续
request_handoff在同 loop 中返回错误。 - Abort/timeout 丢弃:异常退出时不传播 metadata。
- Registry 存在性:target 必须在已注册 sub-agent 中。
- 白名单:如果
can_handoff_to非空 list,target 必须在其中。 - 成功:写
_pending_handoff到ctx.extras["pending_handoff"],置should_break_after_results=True,sub-agent loop 在本轮结束后干净退出。
SubAgentSettings.max_handoff_chain_length(默认 5,范围 1-20)。Cap 用 effective_hop_index(不是 handoff_hop_index)—— 这关闭了 reducer-merge 反复重置的 bypass。
| 配置 | 行为 |
|---|---|
max_handoff_chain_length=5(默认) |
链最长 5 跳;第 6 跳被拒,主代理收到 error_result |
max_handoff_chain_length=1 |
"诊断模式":handoffs 在 origin 即被拦下;advisory 切换到 Variant D,明确告诉主代理 "DO NOT attempt to dispatch the suggested sub_agent" |
cap 超过时:
- 不派发新 sub_agent
- 主代理收到 error_result(含 hop number + cap + "decide directly" 指引)
- 发出
subagent_handoff_chain_terminated审计事件 - 发出合成
$synthetic:chain_cap_exceeded:<target>SubagentProgressBatch(前端可视化用)
Phase 3 新增 6 个事件类型(在现有 subagent_invocation event 基础上):
| 事件 | 触发时机 |
|---|---|
handoff_chain_started |
supervisor 把 handoff_request 插入 pending_handoffs |
subagent_handoff_chain_terminated |
cap 超过 |
handoff_request_expired |
pending entry 在 pending_handoff_ttl_turns 内未消费 |
handoff_request_lost |
worker failover 时检测到 chain_id 不可恢复(best-effort) |
handoff_chain_blocked_at_origin |
cap=1 short-circuit |
handoff_chain_recovered_from_legacy_extras |
rolling-deploy 时 effective_hop_index 缺失,fallback 到 source_hop_index |
subagent_invocation event 在 chain 上下文里增加:
handoff_chain_id(chain 标识)handoff_hop_index(链上的第几跳,0-based)effective_hop_index(cap 检查依据;merged 派发的累积上限)merged_from_chains(merge 派发时列出源 chain_ids,按(effective_hop_index DESC, subagent_name ASC, chain_id ASC)排序)
| 字段 | 默认 | 用途 |
|---|---|---|
subagent.handoff_enabled |
True |
总开关;False 时 advisory 不注入 Phase 3 段落,frozen prefix 跟 Phase 2 byte-identical |
subagent.max_handoff_chain_length |
5(1-20) |
链长上限 |
subagent.pending_handoff_ttl_turns |
3(1-10) |
pending entry 保留多少主代理 turn 后过期 |
subagent.handoff_reverse_lookup_enabled |
True |
是否在 Redis 存 reverse-lookup session_hash → session_id(隐私敏感场景可关) |
subagent.handoff_phase3_deploy_grace_window_s |
1800(0-86400) |
rolling-deploy 期间 handoff_chain_recovered_from_legacy_extras 事件标 recovery_reason: "rolling_deploy" 而非 unknown_extras_clobber |
- permissions guide —— 层级继承如何与子代理调用交互
- sandbox guide ——
srtfloor 如何作用于子代理bash调用 - skill-hub 兼容性 —— 子代理 persona 与 skill 的关系(不同关注点:skills 打包知识 + 工具依赖;sub-agents 打包 人格 + 隔离)
- English version