委派与子代理
SubAgent 是由父智能体派生的临时子运行,默认继承父智能体的身份:相同的智能体别名、相同的 SecurityPolicy、相同的内存允许列表、相同的已配置模型提供方、相同的工具注册表。可通过追踪 span agent.<alias>.subagent.<run_id> 作为子运行进行审计。
子智能体不是一个独立的配置概念。schema 中不存在 [subagents.*] 配置块。每个子智能体的身份取决于是哪个父级的智能体循环创建了它。
何时使用 spawn_subagent 与 delegate
两个工具就在旁边,它们不可互换。
spawn_subagent:在其自身身份下再次运行同一个 agent,以执行一个聚焦的子任务。子 agent 会继承父 agent 的完整权限范围,但可能会有所收窄。当父 agent 希望将某个内部子任务从其主对话历史中独立出来,而不改变身份时,可使用此功能。delegate:将请求交给另一个已配置的 DIFFERENT 代理(通过别名命名)。目标代理以其自身的身份和模型提供方运行,但委派是有门控的:调用方的风险配置文件必须将delegation_policy mode = "allow"(默认是"forbidden"),并且目标必须可作为同一配置文件的同级代理或一个显式的delegates条目被访问。显式条目选择mode = "bounded"或mode = "independent",这决定调用方的工具上限是否仍然适用。用于让另一位已配置的专门代理来承担工作。参见下面的 Delegation gating。
本页面完整记录了 spawn_subagent 的端到端流程。delegate 位于 crates/zeroclaw-runtime/src/tools/delegate.rs,是一个独立的接口。
SubAgent 的实例化方式
两个生成点汇聚于 SubAgentSpawn(crates/zeroclaw-runtime/src/subagent/mod.rs:97):
- 通过 agent 循环:模型调用
spawn_subagent工具并传入一个prompt字符串。该工具像注册表中的其他工具一样进行注册(crates/zeroclaw-runtime/src/tools/mod.rs,SpawnSubagentTool::new)。 - 来自 cron:
JobType::Agent任务通过run_agent_job(crates/zeroclaw-runtime/src/cron/scheduler.rs)运行,它构建相同的SubAgentContext,但将子进程标记为顶层运行(而非 SubAgent),以便它本身可以派生一级 subagent。
两种路径都会调用:
#![allow(unused)]
fn main() {
SubAgentSpawn::for_agent(config, parent_alias)? // 解析父级身份
.build(SubAgentOverrides::default())? // 验证任何收窄
}
for_agent 读取父级的 risk_profile 和 [agents.<alias>.workspace.read_memory_from] 以构建继承的允许列表;父级自身的别名始终会被添加,因此 SubAgent 总能看到其父级自身的内存行。build 应用可选的范围收窄(参见下方权限继承)并返回经过验证的 SubAgentContext。
生命周期
同步、进程内、单个 tokio 运行时。任何内容都不会跨越进程边界。
- 父级的工具循环会分派
spawn_subagent。该工具读取其prompt参数,如果为空则拒绝执行。 - 该工具按顺序检查两个守卫:
- 深度为 1 的限制。 如果调用方运行本身就是一个 SubAgent(
AgentRunOverrides.is_subagent == true),则拒绝并返回"spawn_subagent: a subagent may not spawn its own subagents (depth-1 cap)"。SubAgent 不能递归。 - 风险配置文件工具门控。 如果父级的
[risk_profiles.<alias>].deny_all_tools为true,或者allowed_tools非空且未列出spawn_subagent,或者excluded_tools列出了它,则拒绝并给出包含父级别名的消息。省略或为空的allowed_tools会使该工具保持可用(即旧版无限制状态)。
- 深度为 1 的限制。 如果调用方运行本身就是一个 SubAgent(
- 该工具调用
SubAgentSpawn::for_agent+build。失败情况(未知的父级别名、升级覆盖)会以ToolResult { success: false, error: "subagent spawn failed: ..." }的形式呈现。 - 该工具构造
AgentRunOverrides { security, memory: None, is_subagent: true, suppress_memory_inject: true }(子进程的SubTurn来源已经跳过引擎记忆注入;该标志使这一退出选项显式化),并在键为subagent-<uuid>的跟踪作用域内等待crate::agent::run(crates/zeroclaw-runtime/src/agent/loop_.rs,pub async fn run)。父级的tool执行会阻塞,直到子进程返回。 - 子代理循环运行至完成。其工具注册表会重新构建,并将
is_subagent_caller: true传入它自己的SpawnSubagentTool,因此任何递归调用的尝试都会在同一个 depth-1 关卡处被拒绝。 - 子任务返回
Result<String>。父任务的spawn_subagent工具将其包装为:- 成功:
ToolResult { success: true, output: <子代理的最终响应>, error: None }。空输出将被替换为字面值"subagent completed without output"。 - 失败:
ToolResult { success: false, error: Some("subagent run failed: ...") }。
- 成功:
- 父级的工具循环会带着该
ToolResult在其对话上下文中继续运行。子级的中间轮次和工具调用不会重放到父级的历史记录中;只有最终响应会显示出来。
交付回上游的内容
一点说明:子代理的最终助手消息会以字符串形式包装在 ToolResult.output 中。
- 子代理的工具调用、中间推理回合及其执行的所有内存写入操作,均可在该子代理追踪范围(span)下的结构化日志中查看,但不会进入父代理的对话历史。
- 子会话存储在路径
subagent-<uuid>下(对于 cron 触发的运行则为cron-<uuid>)。这是对话历史记录的键,而非文件系统位置,它将子会话的历史记录与父会话的历史记录隔离开来。 - 子进程执行的内存写入会写入父进程的标识(在 SQL/Postgres 后端使用相同的 agent UUID;对于 Markdown 则使用相同的工作区目录)。通过 Cron 启动的运行会禁用
memory.auto_save,因此显式写入仍然有效,但常规的回忆不会累积。
同步委派没有向父级回传流式输出或部分进度的通道:长时间运行的 SubAgent 会在其整个运行期间阻塞父级的工具执行,且没有按调用设置超时的选项。后台委派则通过 check_result 和 await_sessions 报告有限的实时进度(见下方后台结果结构中的 progress 字段)。
执行树迭代预算
在目标的 [runtime_profiles.<alias>] 条目中设置 max_execution_tree_iterations,以限制单个前台执行树中工具循环迭代次数的总和上限。
[runtime_profiles.default]
max_execution_tree_iterations = 12
该字段为可选项。省略该字段将禁用总预算,而显式设置为 0 则无效。此预算与 max_tool_iterations 相互独立,后者仍作为每个智能体回合的本地单循环上限。
一个逻辑根轮次拥有一个原子预算句柄。前台 spawn_subagent、有界的同步或并行 delegate 目标,以及显式的实时 SOP 子工作都从该句柄中消耗预算;子任务仅当剩余槽位多于一个时才能预留。当根轮次只剩最后一个槽位时,它会消耗该槽位用于现有的无工具补全路径。子任务耗尽其视图时会返回失败的 ToolResult,从而允许父根轮次继续执行,并且并发预留不会使计数器回绕。
独立前台 delegate 目标使用其自身运行时配置的预算,而不是调用方的剩余配额。若省略目标的限制,即使调用方设有执行树预算,该独立回合也不受执行树预算限制。分离的后台 delegate 任务和对等回合同样会从接收方的运行时配置中创建新的根,而不是共享发起回合的预算。
单轮多次调用
智能体循环会应用每轮重复调用防护机制:在同一轮中以相同参数调用两次的工具,通常第二次调用会被跳过。spawn_subagent 和 delegate 不受该防护机制的限制(exempt)。使用相同的提示词启动多个调用(用于冗余、采样、扇出)是一种刻意的模式,而非意外的重复,因此每个相同的调用都会执行,每个结果都会被返回。如果没有这项豁免,只有第一个相同的调用会执行,也只有它的输出会传递给模型。
当启用并行工具执行时(运行时配置文件中的 parallel_tools = true),单轮中的多个 spawn_subagent 调用会并发运行,每个子代理的最终响应都会返回给父代理,并与其各自的工具调用相对应。delegate 通过 parallel: [...] 参数拥有自己显式的分发机制(参见 output-strings 部分);该路径会在各自独立的任务中生成每个目标,并聚合所有结果。
ZeroCode 对话记录检查
前台的 spawn_subagent 和 delegate 调用会显示可展开的转录卡片,其中包含任务、提供时的委派目标名称以及保留的结果文本。并行委派仍然是一次调用,并保留现有的每个目标结果块。其他参数(例如 context)仍在输入字段中可见。格式错误或过大的参数会回退到通用工具卡片。展开时保留工具显示的限制,即每个字段 8 KiB 或 100 行;复制操作使用原始保留条目,其结果可能已被截断至 16 KiB。
委派目标名称显示在卡片标题中,其后是任务内容以及一个 响应 部分。对于匹配的同步委派信封,返回的答案显示在最前面,提供方/模型元数据则保留在展开的 详情 中。其他结果格式保持不变。若缺少响应,则显示 未记录结果。卡片不会断言子任务当前正在运行或已成功,包括在恢复的历史记录或返回的诊断文本中也是如此。后台启动以及诸如 check_result 和 await_sessions 之类的管理操作仍使用通用卡片,因此启动确认不会被显示为子任务完成。
实时子任务检查仍需要将运行时元数据与父工具调用 ID 关联起来,包括子任务/会话标识、权威的生命周期和结果、时间戳,以及子工具事件或经授权的转录链接。当前的父工具通知不包含这些字段。ZeroCode 不会从结果文本、追踪日志、提示匹配或目标别名中推断这些信息;运行时控制平面仍是生命周期的权威管理方。这是 #8763 的部分实现,并非实时子任务监督。
权限继承
子代理(SubAgent)会逐字继承父级的权限,除非生成位置提供了用于收窄权限的 SubAgentOverrides。目前两个内置的生成位置都传入 SubAgentOverrides::default()(继承全部权限)。覆盖接口已发布并通过验证;未来由调用方提供的收窄路径可直接接入,无需运行时改动。
沿坐标轴逐一继承:
SecurityPolicy:通过Arc<SecurityPolicy>克隆继承。覆盖路径(SubAgentOverrides::policy = Some(policy))会运行SecurityPolicy::ensure_no_escalation_beyond(crates/zeroclaw-config/src/policy.rs),并拒绝任何为子代理添加父代理所不具备权限的字段。已验证的维度包括自治级别、allowed_roots(rw + ro + 只写)、allowed_commands、workspace_only、按父 ⊆ 子方向校验的 forbidden_paths、shell_env_passthrough、max_actions_per_hour、max_cost_per_day_cents、shell_timeout_secs、block_high_risk_commands以及require_approval_for_medium_risk。被拒绝时会串联一个精确的EscalationViolation,使诊断信息能够指明违规的字段。- 操作/成本预算:
PerSenderTracker由父级和子级通过Arc克隆共享。逐字继承路径:子级持有相同的Arc<SecurityPolicy>,因此对record_action()/record_cost()的写入会命中同一个桶。覆盖路径:SubAgentSpawn::build会显式地将父级的tracker字段复制到收窄后的子级策略中。SubAgent 无法通过生成(spawn)来绕过max_actions_per_hour或max_cost_per_day_cents,该限制是共享的。 - 工具注册表:子级注册表由
tools::all_tools_with_runtime在继承的策略下重新构建。随后该注册表会经过apply_policy_tool_filter(crates/zeroclaw-runtime/src/agent/loop_.rs)处理,凡名称未能通过任一关卡的工具都会被剔除:- 策略的
allowed_tools/excluded_tools(来源于父级的risk_profile)。 - 调用方传递给
agent::run的allowed_tools参数。spawn_subagent位于注册表中,但其为子级设置的is_subagent_caller标志为true,因此在任何 spawn 操作之前就会触发深度为 1 的拒绝。同一个is_subagent_caller标志会将model_switch从子级的注册表中完全移除:SubAgent 会原封不动地继承父级的模型(参见轴 5),并且不得能够在父级不知情的情况下切换正在使用的模型,因此该工具根本不会提供给它。
- 策略的
- 记忆白名单:父级的同级授权(来自
workspace.read_memory_from)加上父级自身的别名。类别限制保持精确,永远不会被 SubAgent 放宽;父级自身的别名始终会被重新加入,以确保 SubAgent 始终能看到父级的记录。当前的SubAgentOverrides::allowed_agent_aliases覆盖项会被校验为名称子集,因此它只能收窄可见的已授权同级。 - 模型提供方:继承自父级的
[agents.<alias>] model_provider解析结果。Temperature 来自父级的提供方条目(config.model_provider_for_agent(parent_alias).and_then(|e| e.temperature))。这种继承是强制性的,而不仅仅是默认值:model_switch已从 SubAgent 的工具注册表中排除(参见 axis 3),因此 SubAgent 无法切换自己的模型。若要在不同的模型上运行子任务,请使用delegate委派给某个model_provider指向该模型的同级 agent。 - 数据层的身份标识:在
agents表(SQL 后端)中使用相同的 UUID,对 Markdown 使用相同的工作区目录,使用相同的密钥存储。父级与子级的区别纯粹在于可观测性:单独的追踪 span 和单独的对话历史会话键。
用户如何引发火灾
这些工具不是你自己调用的,而是机器人在其回合内部进行调用。作为用户,你可以通过表述请求的方式来影响机器人的选择。这里没有特殊命令、没有斜杠语法,用户也无需输入任何 JSON。模型究竟选择 spawn_subagent 还是 delegate,取决于它的系统提示词、工具的 description 文本(对模型可见)以及用户的措辞。措辞只是施加影响,并不能强制决定。
可以被确定性保证的是可用性:父智能体注册表中不存在的工具不会被选中。风险配置文件的门控位于 [risk_profiles.<alias>].allowed_tools、[risk_profiles.<alias>].deny_all_tools 和 [risk_profiles.<alias>].excluded_tools 中。非空的 allowed_tools 列表必须包含 spawn_subagent 或 delegate,模型才能看到该工具;若省略 allowed_tools 或将其设为空列表,则除非 excluded_tools 指定了该工具,否则工具可用性不受限制;deny_all_tools = true 是显式的全部拒绝门控,会同时隐藏这两个工具(空的 allowed_tools 列表_并非_全部拒绝)。编辑配置后请重启守护进程。
端到端可验证的内容:
- 协议拥有的工具输出和拒绝字符串是 Rust 字面量契约。用户可见的终端补全失败交付属于 Fluent 目录契约:英文源文本的名称如下,定义相同键的非英文目录或磁盘覆盖项可能会以不同方式呈现该文本。
- 改变行为的具体配置项(
allowed_tools、max_delegation_depth等)。 - 用于限定子运行期间发出的所有内容范围的结构化追踪 span 形态。
这些文档中无法验证的内容:
- 当用户请求“生成一个子代理来……“时,你的特定 bot、特定模型、特定系统提示是否会选用该工具,这取决于具体情况。措辞会影响结果;结果因情况而异。如果 bot 没有选用该工具,最可靠的办法是用明确的指令扩展 bot 的系统提示(“当被要求执行一个聚焦的子任务时,使用
spawn_subagent工具”)。 - 机器人在最终回复中写给你的确切文本。机器人会读取工具的输出,并在此基础上生成自己的回复。工具的输出文本可能会被引用、改述或概括。
spawn_subagent:模型看到的拒绝字符串
这些内容是精确的,来源于 crates/zeroclaw-runtime/src/tools/spawn_subagent.rs。模型将其作为该工具的错误字符串接收并作出响应。用户可见的 bot 回复是模型接下来所写的内容;它通常会引用或复述这条拒绝信息。
- 空/缺失的
prompt参数:Missing or empty 'prompt' parameter - 调用方本身就是一个 SubAgent(深度上限为 1):
spawn_subagent: a subagent may not spawn its own subagents (depth-1 cap) - 父代理的风险配置工具门控排除了
spawn_subagent:spawn_subagent: refused: agent '<parent_alias>' risk_profile does not list spawn_subagent in allowed_tools - 未知父别名 / spawn 构建错误:
subagent spawn failed: <wrapped error> - 子运行返回错误:
subagent run failed: <wrapped error>
工具成功执行后,其输出即为子代理的最终响应文本。如果子代理返回空字符串,则输出为字面占位符:subagent completed without output。成功情况下没有固定的前缀可供 grep 检索。
spawn_subagent:如何验证它确实已触发
跟踪你的日志。工具生成的子进程运行在一个 scope! 内部,该作用域会发出一个名为 zeroclaw_scope 的跟踪跨度(目标为 zeroclaw_log_internal_scope),携带 agent_alias=<parent> 和 session_key=<uuid>。子进程运行期间发出的每一行日志都会携带这些字段。父进程自己的回合有其自己的 session_key;对于同一个 agent_alias,在回合中途出现一个新的 session_key 值,就是 SubAgent 已运行的信号。子进程的对话历史会话路径为 subagent-<uuid>(类文件系统标识符,与跟踪字段不同)。
由 Cron 启动的 agent 作业使用一个不同的、更明确的 span 名称:subagent(字面值),其字段为 category="cron"、agent_alias=<owning agent>、cron_job_id=<id>、run_id=<uuid>、spawn_site="cron"。Cron 路径很容易用 grep 检索:grep 'spawn_site="cron"' zeroclaw.log。请注意,由 cron 启动的运行是顶层的(is_subagent=false);它们自身可能会调用一次 spawn_subagent。
这是针对 agent-loop 派生路径的轻量信号。通过 attribution_span!(tool) 路由的专用「subagent started / completed」记录被作为代码侧的后续工作进行跟踪,一旦 agent loop 将工具执行包装在归因 span 中,工具内的每个 record! 都会自动携带 tool=spawn_subagent,于是这个问题就变成了一次简单的 grep。
委派门控
delegate 在 crates/zeroclaw-runtime/src/tools/delegate.rs 中会在目标 agent 运行前按以下顺序强制执行两道关卡:
-
delegation_policy.mode:调用方的风险配置文件必须允许委派。[risk_profiles.<alias>].delegation_policy默认为{ mode = "forbidden" };设置mode = "allow"才能允许委派。当被禁止时,拒绝信息为:调用者 "<caller>" 的风险配置文件 "<caller_profile>" 的 delegation_policy 禁止委派;将 [risk_profiles.<caller_profile>].delegation_policy mode 设置为 "allow"此项可在网关仪表盘和 zerocode 中通过 Config → Risk profiles →
<profile>→delegation_policy.mode编辑(一个 forbidden/allow 选择项)。 -
可达性:目标代理必须位于调用方的可达集合中,该集合由
Config::reachable_delegate_target_configs解析得到。可达集合是[agents.<caller>]上两个按代理划分的来源的并集,减去调用方本身:-
同档同伴:所有与调用方共享相同风险档案的其他代理,在
delegate_same_risk_profile = true(默认值)时纳入。将其设为false可使调用方退出自动允许同伴的行为。 -
显式名册:
delegates,一个可能为空的目标列表,调用方即使跨风险配置文件也可委派给这些目标。字符串条目便于手动编辑,并表示有界目标。对象条目则使该模式显式化:delegates = [ "reviewer", { agent = "sysadmin", mode = "independent" }, ]当配置保存时,每个条目都会以对象形式写入,带有
mode = "bounded"或mode = "independent"。在守护进程和 UI 二进制文件升级到支持委托模式的构建版本之前,不要推广这种配置结构。较旧的二进制文件预期delegates只包含字符串;对象条目会使该二进制文件中的agents部分无效,而弹性加载器会丢弃该部分,因此修复界面仍然可以启动。 当目标超出该集合时,拒绝会说明原因。例如:
delegate target “
<target>” 无法从 “<caller>” 到达:风险配置不同(调用方使用 “<caller_profile>”,目标使用 “<target_profile>”)。delegate_same_risk_profile 仅能到达具有相同风险配置的 agent;请在 [agents.<caller>].delegates 中添加带有所需 mode 的显式条目,或更改其中一个 agent 的 risk_profile。delegate target "<target>" 无法从 "<caller>" 访问:delegate_same_risk_profile 已禁用,且目标未列在 [agents.<caller>].delegates 中委托目标 "<target>" 无法从 "<caller>" 访问:目标代理已禁用有界目标会继承调用方的操作/成本追踪器,并在每一跳将有效上限收紧为调用方的值,因此无论目标自身的配置允许什么,调用方的操作预算都会约束其整个子树。派生的后台和并行工作进程会在首次准入之前恢复发起方的速率限制桶,从而使预算在派生边界之间保持按发送方划分,而不会回退到未限定范围的桶。成本上限在每一跳以相同方式合并,并在被委派的子循环内,依据目标代理自身的每日支出进行强制执行(参见下方的成本追踪段落)。当有界目标与调用方共享相同的风险配置文件时,它还会继承调用方的会话工作区边界。有界的 cross-profile 目标在可通过调用方的委派名册和
delegation_policy访问时被允许;它在目标解析后的策略下运行,而代理工具的可用性则受调用方工具注册表的上限约束。只有在使用
mode = "independent"显式列出时,才可使用独立目标。它仍然需要delegation_policy.mode = "allow"并通过delegates可达,但一旦被选中,它会在不受调用方的非升级上限、会话工作区覆盖或操作/成本跟踪器影响的情况下,解析目标代理自身的策略。成本追踪在每个委派的子循环中强制执行,无论是有界还是独立子循环。委派的子循环在归属于目标 Agent 别名的成本追踪范围内运行,因此其 token 用量会记入共享每日成本账本,并归属于该别名。目标的有效单跳上限来自
[runtime_profiles.<target>].max_cost_per_day_cents(值为0时继承全局限制),并与目标 Agent 自身在该账本上的当日支出进行比较。进程级的[cost] daily_limit_usd与月度限制则继续作用于共享总额;两者中较低者生效时,以共享限制为准。追踪范围类型(按 Agent 或共享上限,取自[cost] track_per_agent)、委派运行是否纳入范围([cost] enabled),以及其账本记录的按 Agent 归属,都在委派树的根委派时一次性确定,并由每个嵌套跳级继承:嵌套委派从其委派父级派生追踪器,因此树启动时所处的模式对整棵树生效;这些标志在运行中途重新加载,既不能使已纳入范围的运行脱离追踪,也不能在中途开始为未归属的运行添加归属。限制与定价映射在每一跳都保持实时:派生的追踪器与基础追踪器共享配置句柄,定价映射则根据解析后的配置重新构建,因此运营者重新加载全局[cost]限制后,运行中的委派会在下一次预算检查时受到约束。当目标自身的支出已超过其上限时,委派在调用目标的提供方之前即失败,并且拒绝结果以委派工具错误的形式返回给调用方,错误中会注明该 Agent。祖先的上限也覆盖其所委派的全部内容:每个有界跳级的预算范围都携带祖先链,因此有界后代的支出会计入每个祖先的单跳上限,即使该后代的账本记录仍归属于其自身的别名。独立目标则在其自身的单跳上限和共享全局限制下运行:它继承树的执行模式,但不继承调用方的上限或链路,因此调用方已耗尽的上限和其后代的累加器都无法限制该独立跳级。后代支出统计以进程生命周期和 UTC 日为单位:它会在 UTC 午夜随账本的每日总额一同滚动重置;守护进程重启会遗忘后代支出,但每个 Agent 自身的账本总额会持久保存。当[cost] track_per_agent = false时,不存在按 Agent 的每日总额,因此单跳上限退化为对共享每日总额的检查。非 Agent 化的委派目标(单次模型调用,无工具循环)同样在该范围内运行,但其调度路径目前不报告 token 用量,因此这些单次调用既不计价,也不进行预算检查。 -
工具模式中的 agent 参数说明里包含了宣传的 roster。它仅列出这个可达集合,而且只有在 delegation_policy.mode = "allow" 时才会列出。已禁用的 agent(enabled = false)永远不可达,无论是作为同一 profile 的同级节点,还是作为显式的 delegates 条目。
在有界代理委派中,子代理的工具取自调用方已经过策略过滤的注册表,并与目标自身的 allowed_tools 取交集。目标上省略或为空的 allowed_tools 表示“继承“:子代理使用调用方完整的可委派注册表运行,而不是被拒绝(空列表是遗留的无限制状态,而非全部拒绝)。deny_all_tools = true 是显式的全部拒绝开关,会产生空的子注册表。非空列表则与该注册表取交集。无论哪种情况,调用方的注册表都是上限:一个有界的跨配置文件目标,若其风险配置文件中列出了调用方从未被授予的工具,则不会获得该工具。因此,有界委派受工具边界约束,而非完整的 SecurityPolicy::ensure_no_escalation_beyond 检查。若交集为空,目标仍会收到一个普通的代理模型轮次,只是没有任何工具。
delegate 工具本身遵循同样的有界结构:除非目标的风险配置文件设置了 delegation_policy mode = "allow",否则它会从子注册表中被剔除(默认值 "forbidden" 保持当前不允许子委派的行为)。调用方侧的门控依然适用:调用方的 allowed_tools/excluded_tools 必须允许 delegate,且目标的工具策略不得排除它。一旦被授予,子级的 delegate 实例会绑定到目标的身份:其调用从目标自身的别名和 delegates 配置中解析 delegation_policy、可达性以及公布的可委派成员名单,而不是从委派的父级解析。每一次获准的委派跳转都会递增深度计数器(见下文)。
授予的实例适用两条审批与范围规则。第一,该实例为仅限委派:任务管理操作(check_result、list_results、cancel_task、await_sessions)会被拒绝,因为子实例的身份是临时的。任务行记录了调用方别名的委派链,因此子实例的每个祖先都会通过所有者检查;只有其自身的 delegate 工具具备任务管理能力的祖先(通常是根委派代理,因为有界的中间子实例同样仅限委派)才能据此执行操作,因此子实例会将任务 ID 逐级向上传递,直到找到这样的祖先。需要精确说明其边界:此规则仅限定委派 API 的范围。底层的 delegate_results/*.json 产物仍是普通的工作区文件,仅保存任务 ID 和输出,而所有者检查所管控的内容则保存在任务行中。第二,子代理循环没有操作员审批途径。如果目标的风险配置文件会对 delegate 发起提示(即受监督的默认设置,或工具列在 always_ask 中),子实例审批关卡会在 delegate 实现运行之前拒绝该调用,并报告当前没有可用的操作员决定。对于到达 delegate 实现的调用,该实现仍保留其自身的目标策略检查。因此,嵌套委派要求目标配置文件明确自动批准 delegate,或以完全自主模式运行;会发出提示的调用方配置文件则在其自身循环中继续提示,保持不变。
同样的无操作员边界也适用于有边界的 agentic 子代理中的工具调用。子循环在分发环节根据目标的风险配置文件创建一个新的非交互式审批管理器。需要提示的调用会在工具实现运行之前被拒绝,而明确列在 auto_approve 中的调用(或在完全自主模式下被允许的调用)可以继续执行。对于继承自调用方的命令类工具(shell、schedule、cron_add、cron_update 和 cron_run),目标配置文件的决策仅批准工具调用本身:它不提供或覆盖调用方命令策略所使用的命令专用审批,也不会改变该审批。调用方的命令策略仍然具有权威性,因此这些工具会继续执行其审批要求。继承的、受调用方上限约束的注册表保持不变:审批只评估通过能力过滤的调用;它不会授予额外的工具,也不会转发调用方的审批会话。
在独立的代理式委派中,子代理的工具由目标代理自身已配置的策略和运行时注册表构建,就像与该目标开启一个全新的会话。父注册表不作为上限。delegate 工具会从该子注册表中移除:独立委派不会通过另一次 delegate 调用进行递归。
委派深度受委派方父智能体的 runtime_profile.max_delegation_depth 限制。每个获准的跳转都会将该上限与下一个目标自身的 profile 上限取较小值(min)后继承,因此父级的上限约束其整个子树,且委派链只会收紧上限。将其设为 1,即允许顶层智能体进行一次委派跳转,且不再进行后续子委派。
智能体目标工具策略
如果目标代理的 [runtime_profiles.<target>].agentic = true,delegate 会从父级可用工具(mode = "bounded")或目标自身的运行时注册表(mode = "independent")构建目标子循环的工具注册表。随后,目标风险配置文件会对该注册表进行过滤:
- 留空或省略
[risk_profiles.<target_profile>].allowed_tools字段时,所选的注册表不受限制。deny_all_tools = true是显式的全部拒绝开关,会生成空的子注册表。 - 非空的
allowed_tools列表仅保留完全匹配的工具名称。 [risk_profiles.<target_profile>].excluded_tools始终从结果中移除。delegate会从子注册表中移除,除非授予了有界子委派:当有界目标的风险配置文件设置了delegation_policy mode = "allow"(且其调用方侧的门控允许该工具)时,该目标会获得一个绑定到目标、仅用于委派的实例;该实例还会在目标配置文件会提示委派时拒绝委派。独立子注册表始终会丢弃delegate。
此策略驻留在目标端,而不是调用方。同一配置文件的对等方使用共享风险配置文件。显式跨配置文件委托在可达性和委托策略门控之后使用目标的风险配置文件。受限的代理式委托仅接收与目标工具策略相交的、由调用方上限约束的工具注册表;独立的代理式委托接收目标拥有的工具注册表。缺少目标风险配置文件会在子循环开始前拒绝。一个配置好的配置文件即使使可执行的子工具数量为零,仍然允许一次不使用工具的正常模型轮次。
当目标配置的 Reliable 提供商链混合了支持原生工具和仅支持文本的候选提供商时,strict_tool_parsing = false 会在整个代理回合中使用一种文本/XML 工具协议,以便每个可到达的回退提供商都能执行工具。如果仍有有效工具且 strict_tool_parsing = true,ZeroClaw 会在发起提供商请求之前拒绝该混合链,因为严格解析禁止使用这种文本/XML 回退协议。统一链的行为不变:全原生链使用原生工具传输,而有意配置的全文本链遵循已配置的文本工具策略。
独立委托审批
独立的代理目标可以将工具审批请求发送给其自身风险配置中 approval_route 所指定的操作员。需同时设置 approver_channel 和非空的 approver_recipient,并确保该渠道处于启用状态,且已在运行中的守护进程中注册。调用方必须已在独立模式下授权该指定目标。审批路由不会授予委派权限或额外工具。
例如,使用 worker 风险配置文件的目标可以将其审批请求路由到已配置的运维渠道:
[risk_profiles.worker.approval_route]
approver_channel = "telegram.ops"
approver_recipient = "operations-room"
on_no_approver = "deny"
timeout_secs = 120
前台、后台以及并行独立的委派任务都使用同一个目标路由审批闸门。提示会标明目标和调用方,保留真实的工具名称和参数摘要,并使用已配置的接收人。只有获得真实批准后,才会允许目标自有的调用执行,但仍需遵守其既有的工具与命令策略。若被拒绝、缺少通道、审批界面不受支持、连接断开或超时,工具都不会执行。子任务的执行超时同样约束审批等待时间,并且可能先于路由超时到期。
完整的目标路由在该配置文件已针对目标自身的通道轮次进行配置时同样适用。对于 Supervised 独立子项,除非目标明确自动批准 shell,否则 shell 调用现在需要操作员做出决定。这也包括以前直接交由 shell 工具的命令策略处理的低风险命令。在没有通道注册表的进程中(例如一次性 CLI 运行),这些需要提示的调用会被拒绝。完全自治和明确的 auto_approve 条目保留其现有行为,而 always_ask 仍需要做出决定。只有当这符合预期的目标策略时,才移除该路由以恢复之前的非交互式 shell 路径。
每个子运行都有一个全新的审批管理器。“Always”(始终)应答仅在该子运行内生效,不能批准调用方的调用、之后委派的运行或并行的同级运行。即使在完全自主(Full autonomy)模式下,always_ask 中的工具仍需对每次调用做出决定。其他明确自动批准的工具则保持其目标配置文件的行为。
委派任务不会继承调用方的 CLI、ACP、RPC 或仪表板审批会话。即使设置了 on_no_approver = "inherit-originator",当已配置的审批方无法作出响应时,请求仍会被拒绝,因为独立的子任务没有可回退的原始审批通道。待处理的审批仅在进程内有效,子任务取消或超时后即被放弃;重启守护进程不会恢复该请求,也不会还原子任务的审批授权。
在没有完整目标路由的情况下,带有非空 always_ask 的独立目标会在执行前被拒绝,包括后台和并行准入。其他需要提示的调用仍会在工具分发时以失败关闭的方式拒绝。有界的智能体委派保持其现有的无操作员行为以及调用方拥有的命令策略。
delegate:模型看到的输出字符串
面向用户的失败字符串是本地化的 Fluent 消息。其英文权威来源是 crates/zeroclaw-runtime/locales/en/cli.ftl;下面的示例展示的是当前英文目录值,而不是线级字符串契约。本节未将其标记为 Fluent 键的其余字符串均为协议/工具输出。
-
同步成功:输出以
[Agent '<target>' (<provider_type>/<model>)]\n开头,后跟非空的目标代理响应。当目标通过已配置的提供商回退机制恢复时,其标头会改为标识所请求和实际提供的提供商/模型,例如[Agent 'reviewer' (requested: anthropic.primary/claude; served: openai.terra/gpt-5.6-terra, agentic)]。对于代理型目标,此归属信息描述的是生成最终响应的模型请求,而不是仅生成工具调用的较早请求。结果还会以本地化的delegate-provider-fallback-warning结尾。英文为:Warning: The delegated agent recovered through a provider fallback. Provider failure details were logged and omitted from this result.此归属信息和警告属于委派结果;不得将其呈现为调用方代理的回退。它们有意省略被拒绝的提供商的错误详细信息、端点和凭据。重试同一个已配置候选项不会产生此警告;即使后续已配置候选项的提供商和模型标签与第一个候选项相同,到达该候选项也会产生此警告。 -
终端空响应是同步失败:其 error 字段使用
cli-delegate-error-invalid-semantic-completion,并将agent_name设置为目标对象。英文表述为:Agent '<target>' failed: model provider returned an invalid semantic completion. -
其他同步失败:错误字段以
Agent '<target>' failed: <wrapped error>开头。如果所有已配置的提供商候选项均失败,<wrapped error>是 Reliable 按顺序生成的安全摘要,其中包含失败事件、重试次数、失败类别、阶段和固定的修复提示。不会将提供商响应正文、端点、别名、模型和凭据返回给调用方代理;需要更多详细信息时,请根据安装环境的常规操作员日志记录策略调查提供商尝试日志。结果仍然是错误,而不是恢复警告。 -
同步超时(当目标的运行时配置文件设置了
delegation_timeout_secs时):错误字段为Agent '<target>' timed out after <N>s。 -
后台生成成功:输出为三行字面量
Background task started for agent '<target>'. task_id: <uuid> Use action='check_result' with task_id='<uuid>' to retrieve the result.生命周期状态存储在
data/control_plane.db中。位于<workspace>/delegate_results/<uuid>.json的工作区产物仅包含任务标识和输出,它不是第二份状态记录。任务开始后,
check_result视图(以及await_sessions结果中的每个条目)会携带一个progress对象;从未记录过进度的行则为null。其字段包括:last_activity_at、iterations(已发出的模型请求数)、tools_completed、last_tool(包含name、started_at、finished_at、success:若存在仍在运行、finished_at为 null 的最新调用,则取该调用;否则取最后一个已完成的调用)、timeout_budget_secs(智能体超时时间,或对于没有工具循环的目标,则为单次调用超时时间)、recent_tools(最近 5 个已完成的调用)、receipt_tail(目前已收集的最后 5 个回执,格式为<tool>: <token>;关闭工具回执时为空),以及elapsed_secs(在读取时计算)。receipts 是在工具批次返回时收集的,因此某个调用可能在其回执出现之前就已被计数。单次调用的目标会将其唯一的一次请求计入
iterations。工具名称在所有出现的位置(包括 receipt tail)都会被截断至 64 个字符;带签名的令牌则保持完整。工具参数、工具结果、错误文本和提示词内容从不记录。进度信息存放在任务行中,因此它只对能够读取该任务输出的调用方可见。任务运行期间,其所在行的心跳至少每分钟刷新一次。
-
action="check_result"使用未知的任务 ID:错误为No result found for task_id '<uuid>'。 -
action="await_sessions"配合task_ids: [<uuid>, ...]一次等待多个后台结果文件。输出是一个 JSON 对象,包含status(complete或timeout)、completed、pending、missing、failed和results。timeout_ms默认值为 30000,且上限为 120000;发生超时时,工具会返回部分结果以及一条错误,说明一个或多个任务仍处于待处理或缺失状态。不允许重复的任务 ID。 -
并行扇出输出:以
[Parallel delegation: <N> agents]\n\n开头,后跟由\n\n分隔的各代理块,每个块以--- <target> (success=<bool>) ---\n开头。恢复的目标会在其自身块中保留请求目标与服务目标的归属信息以及通用回退警告。单个代理失败时,内部块为--- <target> (success=false) ---\nError: <wrapped error>。 -
未知的目标 agent:错误信息为
Unknown agent '<target>'. Available agents: <comma-separated list>。 -
深度超出限制(由父级的
runtime_profile.max_delegation_depth控制,默认值为 3):错误为Delegation depth limit reached (<depth>/<max>). -
未知操作:错误为
Unknown action '<value>'. Use delegate/check_result/list_results/cancel_task/await_sessions. -
具有
always_ask条目的风险配置文件的目标:没有完整显式审批路由的独立目标会返回delegate target "<target>" cannot run in independent mode from "<caller>": risk profile "<profile>" has always_ask entries (<list>). See ZeroClaw docs, "Delegation & SubAgents" > "What's not supported".有界智能体目标会返回同样的错误,但其中的消息为cannot run in bounded agentic mode。 -
智能体目标缺少目标风险配置时:错误为
Agent '<target>' is agentic but risk_profile '<target_profile>' is not configured。 -
Agentic 目标没有可执行的子工具:空工具集本身不会发出错误;目标会接收到一次正常的模型轮次,不带任何工具。
delegate:如何验证它确实已触发
delegate 目前不会生成专门的追踪 span。可观察的信号是目标 agent 的循环出现在日志中,它会继承父级工具调用分发所处的作用域。对于后台模式,请使用 check_result 或 await_sessions 来同时读取权威的生命周期状态和输出。这些操作以及列表和取消操作,仅限于记录链中包含调用方别名的 delegate 记录行。工作区产物可以确认输出已持久化,但不包含状态信息。启用工具回执时,后台 delegation 会使用一个回执作用域运行,该作用域与父级共享生成器,并使用各自的收集器,因此它们的工具结果会被签名且可验证,但不会追加到发起该轮次的回执块中。
(由 Cron 启动的 agent 作业属于独立的生成位置,使用上文所述的显式 subagent span;delegate 与 cron 并非同一路径。)
delegate:停止当前轮次会取消的内容
Stopping the turn that called delegate(例如 POST /api/sessions/{id}/abort,或通道中断)会波及该轮次启动的工作,即使子任务运行在各自的任务中,并且在父级被丢弃的工具 future 之后仍然存在。
- Agentic children(有界或独立)与发起它们的轮次的取消机制绑定,并随之停止。
- 并行子节点(
parallel: [...])中运行各自工具循环的子节点同样受此约束,且适用于每一层级。并行子节点若仅发起普通模型调用而不带工具循环,则不会在调用中途被中断:它会在该次调用自身的超时时间到达后结束。 - 后台任务(
background: true)不会绑定到任何层级的轮次,包括由前台启动的子任务所启动的后台任务。它们按设计在轮次结束后继续运行,只能通过cancel_task停止。
spawn_subagent 没有这些功能:它不支持后台或并行模式,并且其子任务会随父 future 一起被取消。
本页未涵盖的内容(有意为之)
- 示例对话记录。我在这里所写的任何关于“机器人会说什么”的描述都依赖于具体模型。机器人的回复取决于工具的输出、模型、系统提示词以及当前对话状态,而这些都不在本页面的控制范围之内。可验证的层面是工具返回的内容(如上所示)以及日志所捕获的内容。
- 一个专门的 “subagent fired” / “delegate fired” 日志标记。已作为代码侧的后续事项进行跟踪。目前,操作员通过上文描述的 scope 结构(即现有的结构性信号)以及 background-mode 结果文件进行验证。
在 spawn_subagent 和 delegate 之间进行选择
spawn_subagent | delegate | |
|---|---|---|
| 身份 | 与父级相同(相同 UUID,相同风险概况) | 目标代理的身份(不同的别名;同配置文件的对等代理或明确的跨配置文件委托方) |
| 权限模型 | 父级策略的逐字内容(或缩小后的子集) | 有界目标在目标策略下运行,并以调用方的 agentic 工具注册表为上限;独立目标在目标策略和目标自有注册表下运行 |
| 模型提供商 | 父级 | 目标智能体配置的提供商 |
| 生成深度 | 硬性上限为 1 | 最多 runtime_profile.max_delegation_depth(默认值为 3) |
| 后台模式 | 不支持 | background: true 返回一个 task_id |
| 并行分发 | 无内置参数;当 parallel_tools = true 时,单轮中的多次调用会并发执行 | parallel: [...] 并发运行多个目标 |
| 门控 | 省略或为空的 risk_profile.allowed_tools 会保留 spawn_subagent 可用;deny_all_tools = true 会禁用它;非空列表必须包含 spawn_subagent;excluded_tools 不得列出它 | 调用方省略或留空 allowed_tools 时,delegate 保持可用;deny_all_tools = true 会禁用它;非空列表必须包含 delegate;excluded_tools 不得列出它;调用方的 delegation_policy mode = "allow";且目标位于调用方的可达集合中(同配置文件的对等实例或显式的 delegates 条目)。受限目标只有在其自身风险配置文件设置了 delegation_policy mode = "allow" 时,才可以获得 delegate 工具本身;被授予的实例仅具备委派能力,并且当目标的配置文件会提示确认委派时,该实例会拒绝委派。 |
| 使用场景 | 应保持在同一身份内的内部子任务 | 希望由另一个已配置的专员(不同模型、不同别名)在有界或独立委派下负责该任务 |
不支持的内容
- 超过深度 1 的递归。 SubAgent 无法生成自己的 SubAgent。该限制是工具层面的硬性拒绝,而非预算限制。Cron 启动的运行从深度 0 开始,可以生成一级;由 agent-loop 启动的 SubAgent 处于深度 1,会拒绝进一步生成。
- 子代理的独立身份。 子代理共享父代理的 agent UUID。如需以不同身份运行,请使用
delegate移交给已配置的同级代理。 - 单次派生的时间预算。 不存在
timeout_secs参数。父级会阻塞等待子进程运行的全部时长;取消操作必须通过更广泛的中断范围来传递(对于delegate,参见停止当前轮次会取消什么)。 - 向父级流式传输进度。 父级在子级完成后将其最终响应视为单个字符串。
[agents.<alias>].subagent_*配置块。 验证器和覆盖类型今日已发布;面向操作员的配置层(用于贯通调用方定义的收窄逻辑)不在本次发布范围内。在该层落地之前,两个生成点均传递SubAgentOverrides::default()。- 源会话审批转发与有界审批。 独立委派者可以使用显式指定的目标端自有审批路由,但不能将审批转发到调用方的源 CLI、ACP、RPC 或仪表板会话。对于非空
always_ask的有界代理式目标,在执行前会被阻止,包括后台和并行委派。非代理式有界委派仍然可用,因为它无法执行子工具。