[models.subagents] 覆盖层把被委派的工作送到另一个目标,而父会话仍去自己的目的地。
请求这个 id 的 Task 子 agent、hook agent 或 workflow 子 agent 会被分流到覆盖层的目标;
父会话自己的回合从不读这张表,解析该条目的方式和没有这张表时完全一致。
覆盖层可以加在任意 [[models]] 条目上:带 [models.upstream_model] 映射的条目、带
[models.router] 表的条目,或者没有映射、经由 [[routes]] 解析的 id。这张表放在条目上
而不是放进路由器表里,是因为直接指定目的地的条目根本没有路由器表 —— Switchyard 的“带
subagents 的 passthrough”在这里恰好就是这样一个条目。
它有两种形态:
type |
子任务的目标来自 |
|---|---|
passthrough |
与其 agent 类型对应的 by_type,否则是 target —— 不调用裁判,不保存状态 |
llm_classifier |
读过被委派任务的裁判,每对 (会话, agent) 一次 |
什么算被委派的工作
上游只凭它自己的 claude_lineage 头部。shunt 先读 x-claude-code-request-class,只有在
该类别没有被发送时才回退到 agent id:
| 请求 | 走覆盖层吗? |
|---|---|
x-claude-code-request-class: subagent 或 workflow |
走 |
x-claude-code-request-class: main,即使带着 agent id |
不走 —— 主会话流量 |
x-claude-code-request-class: compaction 或 auxiliary |
不走 —— 框架自身的维护 |
没有类别头部,且带有非空的 x-claude-code-agent-id |
走 |
无论提示头部的开关如何,Claude Code 在每个被委派的回合上都会发送 x-claude-code-agent-id。
类别头部和 agent 类型头部则受开关控制:在 shunt 后面,只有客户端设置了
CLAUDE_CODE_GATEWAY_HINT_HEADERS=1 时才会发送(参见
客户端环境变量)。因此在默认部署下,每个 Task
子 agent 都走 target,要用上 by_type 就需要这个变量。
passthrough 形态
[[models]]
id = "claude-opus-4-8"
[models.upstream_model]
anthropic = "claude-opus-4-8"
[models.subagents]
type = "passthrough"
target = "claude-haiku-4-5"
by_type = { Explore = "claude-haiku-4-5", fork = "claude-sonnet-4-6", teammate = "claude-sonnet-4-6" }被委派的一轮去往与它的 x-claude-code-agent-type 相匹配的 by_type 条目;by_type 没有
为它的 agent 类型指定模型时去 target —— 没有发送 agent 类型头部时,所有被委派的回合也都
去 target。
by_type 的键 就是头部的字面值,精确匹配,大小写也算在内。内置 agent 的 id 原样传输
—— Explore、Plan、general-purpose、claude 和 fork(客户端只在
CLAUDE_CODE_FORK_SUBAGENT=1 下才提供它)。来自 .claude/agents/ 的项目 agent 会以
custom 到达,它自己的名字从不发送,所以 custom 是它唯一能匹配上的键。teammate 是
客户端对 Agent Teams 成员使用的字面取值,目前尚未在网络上观察到。
不保存状态。目标只由配置和请求的头部决定:没有会话固定、没有存储、不调用裁判。在带 路由器的 id 上,子任务在路由器运行之前就被分流走,所以子任务的回合从不按转录内容评分、 从不被固定,也从不触碰或推进父级的固定项与 dwell。
llm_classifier 形态
不再指定一个固定目标,而是由裁判读过被委派的任务之后,点名由哪个分组来处理。这里只有
mode = "custom",各个键就是 LLM 分类器
页面介绍的 custom 模式的键。
[models.subagents]
type = "llm_classifier"
mode = "custom"
models = { judge = ["claude-haiku-4-5"], capable = ["claude-opus-4-8"], efficient = ["claude-sonnet-4-6"], any = ["claude-sonnet-4-6", "claude-opus-4-8"] }
default_target = "efficient"
classify_trigger = "new_session"
max_output_tokens = 64
prompt = """
为这项被委派的任务恰好挑选一个目标。
- 代码评审、批评、审计、正确性分析,选 "capable"。
- 实现、调研、讲解以及其他被委派的工作,选 "efficient"。
只返回符合响应 schema 的 JSON。
"""
response_schema = '''
{"type": "object",
"properties": {"target": {"type": "string", "enum": ["capable", "efficient"]}},
"required": ["target"],
"additionalProperties": false}
'''
policy = { type = "target_selector", selector = "/target" }有三条规则与 [models.router] 形态不同:
classify_trigger默认是new_session,并且user_turn会被拒绝。被委派的子任务 就是一件事,所以目标只挑一次、之后一直沿用;按用户回合反复判定,只会为一个不可能改变的 决定持续消耗裁判调用。message_hash_fallback必须是false。 这里的分类本就按 (会话, agent) 取键,改用 第一条消息的哈希,会把同一个会话下两个不同的子任务绑到同一份判定上。- 父会话从不被分类。 什么算被委派的工作,与
passthrough形态完全一致。因此父会话的 回合、带 agent id 的main回合,以及compaction和auxiliary这两个类别,都像这张表 不存在一样解析该条目,也不会发出裁判调用。
六个每次调用的上限放在这张表上,因为发起
调用的正是它。裁判受与别处相同的约束 —— 一跳、不能是 passthrough 路由,并且调用方的凭证
槽位一个都不会随行(见裁判调用)。由于被委派的回合可能
去问裁判,这类回合的入站鉴权也会把覆盖层的目标和裁判一并纳入 —— 无法通过鉴权的被委派回合
会被拒绝,且一次裁判调用都不会发出。count_tokens 探测同样不会发出裁判调用:该子任务有
(会话, agent) 分配时就由该分配作答,否则由 default_target 分组的第一个模型作答。
客户端看到什么
被分流的一轮会带上 x-gateway-routed-model(目标)和 x-gateway-route-source —— 命中
by_type 时是 subagent_type,回退到 target 时是 subagent —— 并以
algorithm = "subagents" 计入 shunt.router.decisions。llm_classifier 形式则报告
classifier 自己的来源:裁判完成分类的那一轮是 llm-classifier,该子 agent 之后重放这一
分配的轮次是 classifier_retained,没有结论定夺这一轮时是 classifier_fail_open(见
LLM Classifier)。
没有请求的接口什么都解析不出来:/v1/models 发现和 shunt check 根本不带任何按模型的
目的地信息,GET /routes 也只在父级自身存在 [[routes]]/[models.router] 条目时才把它
列出来(交给 server.default_provider 解析的 id 在两个数组里都不会出现)。这三个接口都不会
解析出覆盖层分流到的目标,覆盖层自身也从不出现在 routers 数组里。
目标规则、校验错误以及每个键都在配置参考中。