Skip to content

Latest commit

 

History

History
502 lines (379 loc) · 23.3 KB

File metadata and controls

502 lines (379 loc) · 23.3 KB

Sub-Agent 子代理系统

PyClaw 的子代理系统让主 agent 把自包含的任务委派给专用人格(persona):每个人格 拥有独立的上下文窗口、专属模型、工具白名单。每个人格就是一个带 YAML 前置元数据 的 Markdown 文件,无需安装插件、无需编写类、无需重新构建。

当前状态:子代理基础设施于 2026-05-18 上线。PyClaw 自身不打包任何 persona 文件(保持 MIT 许可证清洁),用户自行编写或导入。

TL;DR — 五分钟快速上手

  1. 把清单文件放到五个发现目录之一:

    <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 层。
  2. 编写一个最小清单:

    ---
    name: oracle
    description: 复杂架构与疑难调试的深度推理顾问。
    ---
    你是 Oracle。先复述问题、列出约束、逐步推理,最后把发现回传给主 agent。
  3. 重启 PyClaw。主 agent 现在就有 sub_agent 工具了。在聊天里说: 请咨询 oracle 关于 X 的问题

  4. 查看已安装的人格:

    /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 文件定义人格。

Manifest Schema 清单契约

PyClaw 的 AgentManifest 是跨 agent 生态 SKILL.md / agent-frontmatter 标准的 严格子集。必填字段:

  • name: str —— 小写字母 / 数字 / 连字符,1–64 字符,必须等于文件名 (去掉 .md)。例:oracle.mdname: oracle
  • description: str —— 1–1024 字符。主 agent 用它判断是否调用。要具体: 不要写"代码评审员",而要写"评审 PR diff 的 Python 类型安全、安全漏洞、 SOP 合规性;为每个问题给出 file:line"。

可选字段:

  • model: str | null —— provider/model id(如 anthropic/claude-opus-4-7)。 覆盖 pyclaw.jsonsubagent.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" 配置意味着跨生态字段如 modedisable-model-invocationpathshooks 都会静默通过。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,但三个记忆写工具(memorizeforgetupdate_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 解析路径。

Sandbox 共享

Sprint 3 的 SrtPolicy(macOS Seatbelt / Linux bwrap+seccomp)以零新代码 包裹子代理循环内的每次 bash 调用,因为 sandbox_policy 是按引用共享的。 父代理若运行在 sandbox.policy = "srt",每个子代理的 bash 调用都走同一个 srt wrapper。详见 sandbox guide

pyclaw.jsonproduction_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 命令

/agents list           # 按 source 标签分组的 persona 表 + 实际生效模型
/agents info oracle    # 完整清单 dump + 实际生效模型 + 文件路径
/agents reload         # 重新扫描全部 5 个发现目录(idle 防护)

/agents reload 在子代理调用进行中时拒绝执行,避免运行时 registry 变更。

License 注意事项(关键)

PyClaw 是 MIT 许可证。PyClaw 任何分发包都不包含 persona 文件。persona 从哪里来?

  1. 自己写 —— 最常见,最快。
  2. 从 MIT 来源导入 —— obra/superpowers 是 MIT 许可的精选 agent skills/personas 集合;把相关 .md 文件拖入 ~/.pyclaw/agents/
  3. 依据个人非商业例外条款手写 —— 某些 source-available 集合(如 OhMyOpenAgent 在 Sustainable Use License v1.0 下)允许个人非商业使用; 你可以受其结构启发手写 persona,但不能再分发它们的文件。

驱动约束:OhMyOpenAgent 的 Sustainable Use License v1.0 禁止 "derivative-to-compete distribution"。PyClaw 选择"只发基础设施,零打包 persona",保持 PyClaw 干净的 MIT 可发布性。

向前兼容(A → B)

本次发布是 option A'(基于文件的发现 + register(source=...) 的向前兼容 税)。未来的 add-plugin-protocol 变更(option B)通过以下方式扩展为统一 插件系统:

  1. 增加第 6 个发现 source —— ("plugin:<name>", iter)
  2. 插件 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,不能是 Oracleoracles
  • 检查文件以 .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 工具调用自动拒绝,与谁调用无关。 要允许写,父代理必须运行在 approvalyolo

跨生态字段被静默忽略disable-model-invocationmodepathshooks):

PyClaw 的 extra = "ignore" Pydantic 配置丢弃未知字段。识别清单见上文。 如果需要 PyClaw 识别新字段,请提 issue —— 通常是 AgentManifest 一行加。

审计日志 Schema

每次子代理派生在 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_namesubagent_sourceparent_session_idsubagent_depth),与 Sprint 1/3 字段(tierdecisiontier_sourceforced_serveruser_idrolesandbox_backend)并存。

outcome 枚举:successtimeouterrordepth_exceededtool_whitelist_violationabortedmax_turns_exceeded

并行 Fan-out(V1.5 Phase 1)

从 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 可不同)。

Fan-out 配置

配置项 默认值 范围 作用
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 审计字节兼容)。

Fail-soft 语义

若批次中某个子代理失败(异常 / 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 个桶执行:

  1. parallel(read 类工具,side_effect=False)—— 并发
  2. sequential(write 类工具,side_effect=True,非 sub_agent)—— 串行
  3. subagent_parallel(仅 sub_agent 工具调用)—— 并发

这意味着如果一个 bash 写操作依赖某个子代理的输出,必须分两轮发 (子代理放第 N 轮,消费者放第 N+1 轮)。frozen prefix 中的 supervisor advisory 提示词会告知 LLM 这一规则——见 system_prompt.pysubagent_advisory_section

reduce_subagent_results —— 主代理专属 reducer 工具(V1.5 Phase 1)

当 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_usedn_inputs、可选 fallback_reason

V1.5 Phase 3:跨代理 handoff(cross-agent handoff)

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)。

AgentManifest.can_handoff_to

可选白名单字段,二态语义(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 在解析时被拒(防止"声明可以转交但工具不可用"的死配置)。

request_handoff 内置工具

子代理可见、主代理不可见(dispatch-time block)。schema:

{
  "target": "reviewer",
  "task": "review the diff for security",
  "context_hint": "focus on auth.py"
}

校验顺序(首次失败即生效,"first attempt wins"):

  1. 格式校验target 匹配 ^[a-z][a-z0-9-]*$task 8-8192 codepoints + 至少 4 个不同字符;context_hint ≤16384 codepoints。格式失败不计入 dedup(允许重试)。
  2. Dedup:第一次进入步骤 3 之后就锁定,后续 request_handoff 在同 loop 中返回错误。
  3. Abort/timeout 丢弃:异常退出时不传播 metadata。
  4. Registry 存在性:target 必须在已注册 sub-agent 中。
  5. 白名单:如果 can_handoff_to 非空 list,target 必须在其中。
  6. 成功:写 _pending_handoffctx.extras["pending_handoff"],置 should_break_after_results=True,sub-agent loop 在本轮结束后干净退出。

链长度上限(cap)

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(前端可视化用)

Audit 事件

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

参见