Hermes 架构全解Hermes Architecture, in Full第 10 章 · 14 章Chapter 10 of 14
全文目录Contents
  1. 0 · 项目全景与代码地图
    1. 0.1 这个软件是什么
    2. 0.2 目录地图
    3. 0.3 从这张地图能读出的三件事
    4. 0.4 一条消息的完整旅程
    5. 0.5 全文章节索引
  2. 1 · 网关层
    1. 1.1 它解决什么问题
    2. 1.2 平台适配器抽象基类
    3. 1.3 消息事件的归一化
    4. 1.4 网关主循环里的那些防御机制
    5. 1.5 状态消息与进度反馈
    6. 1.6 中断与恢复
    7. 1.7 网关内置钩子
    8. 1.8 这一层的代价
  3. 2 · 身份与会话路由
    1. 2.1 Profile:一个进程承载多个身份
    2. 2.2 四级路由与「具体度」打分
    3. 2.3 显式路由被拒绝的情况
    4. 2.4 SOUL.md:人格文件
    5. 2.5 会话路由:跨平台的连续性
    6. 2.6 智能体实例的缓存
  4. 3 · 主循环 ★
    1. 3.1 循环入口:三重预算闸门
    2. 3.2 每轮的准备工作:build_turn_context
    3. 3.3 中途插话:/steer
    4. 3.4 墙上时钟预算的收尾提醒
    5. 3.5 中断检查
    6. 3.6 步骤回调:给网关的观测点
    7. 3.7 循环里的其他状态
    8. 3.8 循环退出原因
    9. 3.9 一个可选的旁路:Codex 应用服务运行时
  5. 4 · 工具系统
    1. 4.1 最重要的设计:实现与投放分离
    2. 4.2 工具集的组合与解析
    3. 4.3 工具的中心分发
    4. 4.4 参数强制矫正层
    5. 4.5 工具错误消息的净化
    6. 4.6 工具结果的观测与钩子
    7. 4.7 委派上下文识别
    8. 4.8 工具定义的缓存
  6. 5 · 审批与安全红线 ★
    1. 5.1 整体结构
    2. 5.2 12 条硬红线
    3. 5.3 真正的难点:区分「命令」和「数据」
    4. 5.4 引号遮蔽:但要给「真会执行的部分」留后门
    5. 5.5 引号不能成为绕过手段
    6. 5.6 敏感路径与写入目标
    7. 5.7 sudo 标准输入守卫
    8. 5.8 性能:预编译的理由
    9. 5.9 被拦截命令的留存
    10. 5.10 智能审批:可选的模型判断
    11. 5.11 上下文感知:不同场景不同策略
    12. 5.12 这一层的定位:最外面也最薄
  7. 6 · 执行环境
    1. 6.1 七种可选环境
    2. 6.2 必须诚实说明的一件事
    3. 6.3 抽象基类里的共用逻辑
    4. 6.4 文件同步
    5. 6.5 环境的选择时机
  8. 7 · 上下文引擎 ★
    1. 7.1 它定义的是什么
    2. 7.2 生命周期
    3. 7.3 三个必须实现的方法
    4. 7.4 最精辟的设计:select 和 compress 是两个正交动词
    5. 7.5 后置观察钩子
    6. 7.6 其他可选钩子
    7. 7.7 默认参数值
    8. 7.8 用户可见状态的可控性
    9. 7.9 内置实现的体量对比
  9. 8 · 记忆系统 ★
    1. 8.1 记忆提供者接口
    2. 8.2 生命周期与钩子
    3. 8.3 接口的版本化契约
    4. 8.4 琐碎提问过滤器
    5. 8.5 记忆使用指示器
    6. 8.6 内置的全息记忆
    7. 8.7 存储层与信任分
    8. 8.8 SQLite 状态层
    9. 8.9 三种记忆的分工总结
  10. 9 · 插件系统
    1. 9.1 三个发现来源
    2. 9.2 插件能提供什么
    3. 9.3 最重要的设计:区分「可叠加能力」与「互斥策略」
    4. 9.4 插件的存储
    5. 9.5 插件与工具集的联动
    6. 9.6 插件钩子
    7. 9.7 MCP:另一条扩展路径
    8. 9.8 这套扩展体系的整体形状
  11. 10 · 委派与多智能体
    1. 10.1 为什么需要委派
    2. 10.2 深度限制:只允许一层
    3. 10.3 并发限制
    4. 10.4 子智能体的工具限制
    5. 10.5 子智能体的审批策略
    6. 10.6 运行中的控制
    7. 10.7 亲缘关系检查
    8. 10.8 看板:智能体之间的协作
    9. 10.9 委派系统为什么有 5,071 行
  12. 11 · 模型供应商与凭据池
    1. 11.1 为什么不能只支持一家
    2. 11.2 供应商适配器清单
    3. 11.3 凭据池:核心机制
    4. 11.4 PooledCredential:单个凭据的状态
    5. 11.5 「持久化」为什么必要
    6. 11.6 与其他机制的配合
    7. 11.7 供应商抽象要抽象到哪一层
  13. 12 · 定时任务
    1. 12.1 「cron」是什么
    2. 12.2 定时智能体的场景
    3. 12.3 最重要的一个类:CronPromptInjectionBlocked
    4. 12.4 定时任务的工具集收窄
    5. 12.5 失败处理
    6. 12.6 防重复执行
    7. 12.7 为什么 scheduler.py 有 367 KB
    8. 12.8 定时任务在整个架构里的位置
  14. 13 · 技能系统
    1. 13.1 技能是什么
    2. 13.2 前置元数据逐字段解释
    3. 13.3 渐进式披露:技能系统的核心机制
    4. 13.4 技能的支撑设施
    5. 13.5 技能 vs 工具 vs 插件
    6. 13.6 技能系统的一个隐含设计:可组合
    7. 13.7 15 个技能类别
    8. 13.8 全文回顾:Hermes 的整体形状
  1. 0 · The Project at a Glance, and a Map of the Code
    1. 0.1 What This Software Is
    2. 0.2 Directory Map
    3. 0.3 Three Things You Can Read Off This Map
    4. 0.4 The Full Journey of One Message
    5. 0.5 Chapter Index
  2. 1 · The Gateway Layer
    1. 1.1 The Problem It Solves
    2. 1.2 The Platform Adapter Abstract Base Class
    3. 1.3 Normalizing Message Events
    4. 1.4 The Defensive Mechanisms in the Gateway Main Loop
    5. 1.5 Status Messages and Progress Feedback
    6. 1.6 Interruption and Recovery
    7. 1.7 Built-in Gateway Hooks
    8. 1.8 What This Layer Costs
  3. 2 · Identity and Session Routing
    1. 2.1 Profiles: One Process, Many Identities
    2. 2.2 Four-Level Routing and the “Specificity” Score
    3. 2.3 When an Explicit Route Is Rejected
    4. 2.4 SOUL.md: The Persona File
    5. 2.5 Session Routing: Continuity Across Platforms
    6. 2.6 Caching Agent Instances
  4. 3 · The Main Loop ★
    1. 3.1 The Loop Entry: A Triple Budget Gate
    2. 3.2 Per-Turn Preparation: build_turn_context
    3. 3.3 Mid-Turn Interjection: /steer
    4. 3.4 The Wall-Clock Budget Wrap-Up Reminder
    5. 3.5 The Interrupt Check
    6. 3.6 The Step Callback: An Observation Point for the Gateway
    7. 3.7 Other State Inside the Loop
    8. 3.8 Why the Loop Exited
    9. 3.9 An Optional Bypass: The Codex App Server Runtime
  5. 4 · The Tool System
    1. 4.1 The Most Important Design Decision: Implementation Separated from Exposure
    2. 4.2 Composing and Resolving Toolsets
    3. 4.3 Central Tool Dispatch
    4. 4.4 The Argument Coercion Layer
    5. 4.5 Sanitizing Tool Error Messages
    6. 4.6 Observing Tool Results, and Hooks
    7. 4.7 Recognizing a Delegation Context
    8. 4.8 Caching Tool Definitions
  6. 5 · Approval and Safety Red Lines ★
    1. 5.1 The Overall Structure
    2. 5.2 The 12 Hard Red Lines
    3. 5.3 The Real Difficulty: Telling “Command” from “Data”
    4. 5.4 Quote Masking: But Leave a Way In for “the Part That Really Executes”
    5. 5.5 Quoting Is Not a Bypass
    6. 5.6 Sensitive Paths and Write Targets
    7. 5.7 The sudo Stdin Guard
    8. 5.8 Performance: Why Precompile
    9. 5.9 Retaining Blocked Commands
    10. 5.10 Smart Approval: An Optional Model Judgment
    11. 5.11 Context Awareness: Different Situations, Different Policies
    12. 5.12 Where This Layer Sits: Outermost, and Thinnest
  7. 6 · Execution Environments
    1. 6.1 Seven Environments to Choose From
    2. 6.2 One Thing That Has to Be Said Plainly
    3. 6.3 Shared Logic in the Abstract Base Class
    4. 6.4 File Synchronization
    5. 6.5 When the Environment Is Chosen
  8. 7 · The Context Engine ★
    1. 7.1 What It Defines
    2. 7.2 Lifecycle
    3. 7.3 The Three Methods You Must Implement
    4. 7.4 The Sharpest Design Decision: select and compress Are Two Orthogonal Verbs
    5. 7.5 The Post-Turn Observation Hook
    6. 7.6 Other Optional Hooks
    7. 7.7 Default Parameter Values
    8. 7.8 Control Over User-Visible Status
    9. 7.9 Size Comparison of the Built-in Implementation
  9. 8 · The Memory System ★
    1. 8.1 The Memory Provider Interface
    2. 8.2 Lifecycle and Hooks
    3. 8.3 The Interface's Versioned Contract
    4. 8.4 The Trivial-Prompt Filter
    5. 8.5 The Memory-Usage Indicator
    6. 8.6 The Built-in Holographic Memory
    7. 8.7 The Storage Layer and Trust Scores
    8. 8.8 The SQLite State Layer
    9. 8.9 Summing Up the Division of Labor Among Three Kinds of Memory
  10. 9 · The Plugin System
    1. 9.1 Three Discovery Sources
    2. 9.2 What a Plugin Can Provide
    3. 9.3 The Most Important Design Decision: “Stackable Capabilities” vs. “Mutually Exclusive Strategies”
    4. 9.4 Plugin Storage
    5. 9.5 How Plugins Tie into Toolsets
    6. 9.6 Plugin Hooks
    7. 9.7 MCP: The Other Extension Path
    8. 9.8 The Overall Shape of This Extension System
  11. 10 · Delegation and Multi-Agent
    1. 10.1 Why Delegation Is Needed
    2. 10.2 The Depth Limit: One Level Only
    3. 10.3 Concurrency Limits
    4. 10.4 Tool Restrictions on Subagents
    5. 10.5 Approval Policy for Subagents
    6. 10.6 In-Flight Control
    7. 10.7 The Lineage Check
    8. 10.8 Kanban: Collaboration Between Agents
    9. 10.9 Why the Delegation System Is 5,071 Lines
  12. 11 · Model Providers and the Credential Pool
    1. 11.1 Why You Can't Support Just One
    2. 11.2 The Provider Adapter Roster
    3. 11.3 The Credential Pool: The Core Mechanism
    4. 11.4 PooledCredential: The State of a Single Credential
    5. 11.5 Why “Persistent” Is Necessary
    6. 11.6 How It Works with Other Mechanisms
    7. 11.7 How Deep the Provider Abstraction Should Go
  13. 12 · Scheduled Tasks (Cron)
    1. 12.1 What “cron” Is
    2. 12.2 Scenarios for Scheduled Agents
    3. 12.3 The Single Most Important Class: CronPromptInjectionBlocked
    4. 12.4 Narrowing the Toolset for Scheduled Tasks
    5. 12.5 Failure Handling
    6. 12.6 Preventing Duplicate Runs
    7. 12.7 Why scheduler.py Is 367 KB
    8. 12.8 Where Scheduled Tasks Sit in the Overall Architecture
  14. 13 · The Skill System
    1. 13.1 What a Skill Is
    2. 13.2 The Front Matter, Field by Field
    3. 13.3 Progressive Disclosure: The Core Mechanism of the Skill System
    4. 13.4 The Infrastructure Around Skills
    5. 13.5 Skills vs. Tools vs. Plugins
    6. 13.6 An Implicit Design in the Skill System: Composability
    7. 13.7 The 15 Skill Categories
    8. 13.8 Looking Back Across the Book: The Overall Shape of Hermes

10 · 委派与多智能体

tools/delegate_tool.py,5,071 行。这是 Hermes 里最长的单个工具文件 —— 比整个审批系统的核心还长。这一章讲一个智能体怎么派另一个智能体去干活。

10.1 为什么需要委派

任务:"把这个项目的 30 个模块都加上类型注解" 不委派: 主智能体自己做 30 遍 → 每读一个模块,上下文就长一截 → 读到第 12 个模块时上下文爆了,触发压缩 → 压缩把前 11 个模块的细节丢了 → 后面的工作质量下降 委派: 主智能体派 30 个子智能体,每个只管一个模块 → 每个子智能体的上下文只有它自己那一个模块 → 主智能体只收到 30 条"完成了/失败了"的摘要 → 主智能体的上下文始终很短

委派的本质是「上下文分区」。

它不是为了「并行更快」(虽然确实更快),核心价值是:让每个子任务在一个干净、专注、不会被无关信息污染的上下文里执行,而主智能体只承担协调成本。

10.2 深度限制:只允许一层

MAX_DEPTH = 1

这一行是整个多智能体系统里最重要的一个常量。它的意思是:主智能体可以派子智能体,但子智能体不能再派孙智能体。

如果不限制深度会怎样

假设每个智能体可以派 10 个子智能体,不限深度: 深度 0:主智能体 1 个 深度 1:子智能体 10 个 深度 2:孙智能体 100 个 深度 3:曾孙智能体 1,000 个 深度 4: 10,000 个 ★ 指数爆炸。每一个都在烧钱、占内存、发 API 请求。 ★ 而且没有任何一个环节会"觉得不对" —— 每一层都只是 在做它被设计要做的事情。 更糟的是:调试时你根本不知道是哪一层出的问题, 因为日志里有一万个智能体在同时说话。

这是一个「用最简单的手段消灭一整类问题」的典型例子。

想做「智能地限制递归」很难:要估算成本、要判断任务复杂度、要有熔断机制、要有预算传递……

MAX_DEPTH = 1 一行代码就让整类问题不存在了。代价是失去了「深层任务分解」的能力 —— 但实践中,一层委派已经覆盖了绝大多数场景,而两层带来的复杂度是指数级的。

在做架构设计时,先问「能不能用一个硬限制消灭这类问题」,再考虑「怎么智能地处理这类问题」。

10.3 并发限制

_DEFAULT_MAX_CONCURRENT_CHILDREN = 10
_RECENT_SUBAGENTS_CAP = 200
常量作用
_DEFAULT_MAX_CONCURRENT_CHILDREN = 10 同时最多跑 10 个子智能体。第 11 个要排队。防止一次性打爆模型供应商的速率限制,也防止本机内存和文件句柄耗尽
_RECENT_SUBAGENTS_CAP = 200 「最近的子智能体」记录最多保留 200 条。这是给「查看子智能体状态」这类功能用的历史缓冲,超出就丢弃最老的。防止长时间运行的会话把内存吃光

这两个数值配合 MAX_DEPTH = 1,把整个多智能体系统的资源占用锁在一个可预测的范围内:任意时刻最多 1 + 10 = 11 个智能体在跑,历史记录最多 200 条。

10.4 子智能体的工具限制

DELEGATE_BLOCKED_TOOLS

子智能体不能使用某些工具。最重要的一条是:子智能体不能再调用 delegate 工具 —— 这是 MAX_DEPTH = 1 在工具层面的强制实现。

注意这是「双重保险」:

· 逻辑层MAX_DEPTH = 1 在委派时检查深度
· 能力层DELEGATE_BLOCKED_TOOLS 让子智能体根本看不到 delegate 这个工具

第二层更彻底 —— 模型连「我可以委派」这个念头都不会有,因为工具列表里没有。不给能力,比给了能力再拦截更可靠。这和第 4 章「webhook 只投放 4 个只读工具」是同一个思路。

10.5 子智能体的审批策略

def _subagent_auto_deny(...)
def _subagent_auto_approve(...)

这里有一个必须解决的问题:子智能体跑起来后要求审批,谁来批?

场景:主智能体派了 10 个子智能体去改代码 子智能体 #3 想执行 `rm -rf build/` ↓ 审批系统说:"这需要人工确认" ↓ 可是…… · 用户可能不在(半夜跑的定时任务) · 就算在,10 个子智能体同时问,用户会疯 · 主智能体在等结果,全部卡住 → 需要一个"不问人"的策略
函数语义
_subagent_auto_deny 自动拒绝。子智能体收到「被拒绝」的结果,它可以换个方式做,或者报告失败。安全但可能卡住任务
_subagent_auto_approve 自动批准。不问人直接放行。能跑通但风险大

这是自动化系统里最难的一个权衡。

自动拒绝是安全的默认,但会让很多合法任务失败 —— 而且失败方式很隐蔽(子智能体报告「我做不到」,但真实原因是权限被拒)。

自动批准能跑通,但意味着红线之外的所有操作在无人监督下执行。第 5 章那 12 条红线依然生效(那是绝对禁止的),但「需要确认」这一档就被跳过了。

正确的做法是:让主智能体在委派时显式声明子智能体的权限档位,而不是有一个全局默认。做「只读分析」的子智能体应该自动拒绝一切写操作;做「批量重构」的子智能体则需要预先授权写文件。

10.6 运行中的控制

子智能体不是「发出去就不管了」。有三个控制接口:

def interrupt_subagent(...)    # 中断某个子智能体
def steer_subagent(...)        # 向运行中的子智能体注入指令
def set_spawn_paused(...)      # 暂停/恢复新子智能体的派生
接口用途
interrupt_subagent 发现某个子智能体走偏了 / 卡住了 / 在烧钱,单独把它停掉,不影响其他 9 个
steer_subagent 不打断,但插一句话。对应第 3 章讲过的 /steer 机制 —— 把新指令注入到最后一条工具消息里,让智能体在下一轮就能看到。
比如:「顺便也检查一下类型注解」
set_spawn_paused 暂停派生新的子智能体,但已在跑的继续。用途:发现整批任务方向不对时,先止血 —— 不再派新的,让已经跑起来的自然结束,然后重新规划

set_spawn_paused 这个设计值得单独说。

最朴素的做法只有「全部继续」和「全部杀掉」两种。但实际场景里最常见的是第三种:「别再开新的了,让手上的跑完」

这在运维上叫「排空」(drain)—— 优雅停机、滚动更新、限流降级都是这个模式。一个成熟的并发系统必须区分「停止接受新工作」和「终止现有工作」。

10.7 亲缘关系检查

def _is_descendant_of(..., max_hops: int = 8)

「判断智能体 A 是不是智能体 B 的后代」。用途:

  • 中断一个智能体时,要连带中断它的所有后代
  • 统计成本时,要把后代的花费算到祖先头上
  • 权限检查:某些操作只允许对自己的后代做

那个 max_hops = 8 是防御性的。既然 MAX_DEPTH = 1,理论上最多只需要查 1 跳。设成 8 是为了:

  • 兼容将来可能放宽的深度限制
  • 更重要的:万一数据里出现了环(A 的父亲是 B,B 的父亲是 A),这个上限保证函数一定会返回,而不是无限循环

这是「即使不变量被破坏,程序也不能挂死」的写法。

正常情况下永远不会走到第 8 跳。但如果某个 bug 导致父子关系成了环,有这个上限的版本会返回一个(可能错误的)答案并继续跑,没有上限的版本会把整个进程挂死

在遍历任何「理论上应该是树,但数据由运行时构造」的结构时,都要加这样一个跳数上限。

10.8 看板:智能体之间的协作

plugins/kanban/ 提供了另一种多智能体模式 —— 不是「派下去等结果」,而是「共享一块任务板」

kanban_create_task       创建任务
kanban_claim_task        认领任务
kanban_update_task       更新进度
kanban_complete_task     完成任务
kanban_list_tasks        查看任务列表
kanban_heartbeat         ★ 心跳
看板模式: ┌───────────────── 共享任务板 ─────────────────┐ │ #1 [待认领] 重构 auth 模块 │ │ #2 [进行中] 写单元测试 ← agent-B 认领 │ │ #3 [待认领] 更新文档 │ │ #4 [已完成] 修复登录 bug ← agent-A 完成 │ └──────────────────────────────────────────────┘ ↑ ↑ ↑ agent-A agent-B agent-C (各自独立,通过任务板协调,没有父子关系)
委派模式看板模式
关系父子 —— 主智能体明确指派对等 —— 谁有空谁认领
谁决定做什么主智能体各个智能体自己
生命周期子智能体做完就结束智能体长期存在,持续认领新任务
适合已知的、可分解的批量任务持续的、来源不定的工作流

kanban_heartbeat 为什么必须存在

agent-B 认领了任务 #2 ↓ agent-B 崩溃了 / 进程被杀 / 机器重启 ↓ 任务 #2 永远停在 "进行中" ↓ 没有别的智能体会去做它 —— 因为看起来"有人在做" ↓ ★ 任务永久丢失 解决:agent-B 每隔 N 秒调一次 kanban_heartbeat → 超过 N×k 秒没心跳,任务自动回到 "待认领"

任何「认领 - 执行 - 完成」的分布式任务系统,都必须有心跳或租约(lease)机制。

否则「认领了但没做完就死掉」的任务会永久卡住。这在消息队列、任务调度器、分布式锁里是同一个问题,解法也一样:认领是有时效的,需要持续续期。

10.9 委派系统为什么有 5,071 行

回到开头那个数字。真正做「派一个子智能体」的核心逻辑可能只要 200 行。剩下 4,800 行在做什么?

类别内容
生命周期管理创建、启动、监控、中断、清理、超时、僵尸回收
并发控制并发上限、排队、暂停派生、优先级
结果聚合收集 10 个子智能体的结果、处理部分失败、超时的怎么算
状态查询「现在有几个在跑」「花了多少钱」「卡在哪一步」
控制通道中断、注入指令、暂停 —— 每个都要跨进程/跨线程安全地送达
安全边界工具屏蔽、审批策略、深度检查、亲缘关系
可观测性每个子智能体的日志、成本、耗时都要单独记录并能关联回父任务
失败处理子智能体崩溃、模型报错、上下文爆炸、无限循环 —— 每种都要有对策

「让智能体调用智能体」的 demo 是 20 行,生产系统是 5,000 行。

这个 250 倍的差距全部来自「出问题时怎么办」。多智能体系统的难点从来不是「怎么派」,而是「派出去的东西失控了怎么收场」。

面试里如果被问到多智能体,能说清楚这一点,比能画出漂亮的架构图有用得多。

10 · Delegation and Multi-Agent

tools/delegate_tool.py, 5,071 lines. This is the longest single tool file in Hermes — longer than the core of the entire approval system. This chapter covers how one agent sends another agent off to do work.

10.1 Why Delegation Is Needed

Task: "Add type annotations to all 30 modules in this project" Without delegation: The main agent does it 30 times itself → every module it reads makes the context a bit longer → by module 12 the context overflows and triggers compaction → compaction throws away the details of the first 11 modules → the quality of the remaining work drops With delegation: The main agent spawns 30 subagents, each owning one module → each subagent's context holds only its own module → the main agent receives only 30 "done / failed" summaries → the main agent's context stays short throughout

Delegation is fundamentally “context partitioning.”

It isn't about “parallelism makes it faster” (though it does). The core value is: every subtask runs in a clean, focused context that can't be polluted by unrelated information, while the main agent bears only the coordination cost.

10.2 The Depth Limit: One Level Only

MAX_DEPTH = 1

This one line is the single most important constant in the whole multi-agent system. It means: the main agent may spawn subagents, but a subagent may not spawn grandchild agents.

What happens without a depth limit

Suppose every agent may spawn 10 subagents, with no depth limit: Depth 0: main agent 1 Depth 1: subagents 10 Depth 2: grandchild agents 100 Depth 3: great-grandchild agents 1,000 Depth 4: 10,000 ★ Exponential blowup. Every one of them burns money, eats memory, and fires API requests. ★ And no link in the chain will ever "sense something is wrong" — every layer is just doing exactly what it was designed to do. Worse: when debugging, you have no idea which layer went wrong, because ten thousand agents are talking at once in the logs.

This is a textbook case of “killing an entire class of problems with the simplest possible means.”

“Intelligently limiting recursion” is hard: you'd need to estimate cost, judge task complexity, build a circuit breaker, pass budgets down the tree…

And MAX_DEPTH = 1, one line of code, makes the whole class of problems disappear. The cost is losing “deep task decomposition” — but in practice, one level of delegation covers the vast majority of scenarios, and two levels bring exponential complexity.

When designing architecture, first ask “can a hard limit eliminate this class of problems,” and only then consider “how do we handle this class intelligently.”

10.3 Concurrency Limits

_DEFAULT_MAX_CONCURRENT_CHILDREN = 10
_RECENT_SUBAGENTS_CAP = 200
ConstantRole
_DEFAULT_MAX_CONCURRENT_CHILDREN = 10 At most 10 subagents run at once. The 11th waits in line. This prevents blowing through the model provider's rate limit in one shot, and also keeps the local machine from running out of memory and file handles
_RECENT_SUBAGENTS_CAP = 200 The “recent subagents” record keeps at most 200 entries. This is a history buffer for features like “view subagent status”; beyond the cap, the oldest entries are dropped. It keeps long-running sessions from eating all the memory

Together with MAX_DEPTH = 1, these two numbers lock the resource footprint of the whole multi-agent system into a predictable range: at any moment, at most 1 + 10 = 11 agents are running, and the history holds at most 200 records.

10.4 Tool Restrictions on Subagents

DELEGATE_BLOCKED_TOOLS

Subagents cannot use certain tools. The most important rule: a subagent cannot call the delegate tool — this is MAX_DEPTH = 1 enforced at the tool layer.

Note that this is “belt and suspenders”:

· Logic layer: MAX_DEPTH = 1 checks the depth at delegation time
· Capability layer: DELEGATE_BLOCKED_TOOLS means the subagent never even sees the delegate tool

The second layer is more thorough — the model never gets the idea “I could delegate,” because the tool isn't on the list. Not granting a capability is more reliable than granting it and then intercepting. It is the same thinking as chapter 4's “webhooks get only 4 read-only tools.”

10.5 Approval Policy for Subagents

def _subagent_auto_deny(...)
def _subagent_auto_approve(...)

There is a problem here that has to be solved: when a running subagent asks for approval, who approves?

Scenario: the main agent spawns 10 subagents to change code Subagent #3 wants to run `rm -rf build/` ↓ The approval system says: "this needs human confirmation" ↓ But… · the user may not be around (a scheduled task running at 3 a.m.) · even if they are, 10 subagents asking at once would drive them mad · the main agent is waiting on results; everything stalls → a policy that "doesn't ask a human" is needed
FunctionSemantics
_subagent_auto_deny Automatically deny. The subagent receives a “denied” result; it can try another way or report failure. Safe, but may stall the task
_subagent_auto_approve Automatically approve. Let it through without asking anyone. Gets the job done, but risky

This is the hardest trade-off in any automated system.

Auto-deny is the safe default, but it makes many legitimate tasks fail — and fail in a hidden way (the subagent reports “I couldn't do it,” when the real reason is a denied permission).

Auto-approve gets things done, but it means every operation outside the red lines runs unsupervised. The 12 red lines from chapter 5 still hold (those are absolute prohibitions), but the “needs confirmation” tier gets skipped.

The right approach: have the main agent explicitly declare the subagent's permission tier at delegation time, instead of relying on one global default. A subagent doing “read-only analysis” should auto-deny every write; a subagent doing “bulk refactoring” needs write access pre-authorized.

10.6 In-Flight Control

Subagents aren't “fire and forget.” There are three control interfaces:

def interrupt_subagent(...)    # interrupt a specific subagent
def steer_subagent(...)        # inject an instruction into a running subagent
def set_spawn_paused(...)      # pause/resume spawning of new subagents
InterfacePurpose
interrupt_subagent When one subagent has gone off track / gotten stuck / is burning money, stop just that one without affecting the other 9
steer_subagent Don't interrupt, but slip in a line. This maps to the /steer mechanism from chapter 3 — the new instruction is injected into the last tool message so the agent sees it on its next turn.
For example: “while you're at it, check the type annotations too”
set_spawn_paused Pause spawning new subagents, but let the running ones continue. Use case: when you realize the whole batch is headed in the wrong direction, stop the bleeding first — spawn nothing new, let the ones already running finish naturally, then re-plan

The set_spawn_paused design deserves its own mention.

The naive approach offers only “continue everything” and “kill everything.” But the most common real-world need is a third option: “don't start anything new; let what's in flight finish.”

In operations this is called “draining” — graceful shutdown, rolling updates, and load-shedding all follow this pattern. A mature concurrent system must distinguish “stop accepting new work” from “terminate existing work.”

10.7 The Lineage Check

def _is_descendant_of(..., max_hops: int = 8)

“Determine whether agent A is a descendant of agent B.” Uses:

  • When interrupting an agent, interrupt all of its descendants along with it
  • When tallying cost, charge the descendants' spending to their ancestor
  • Permission checks: some operations are only allowed on your own descendants

That max_hops = 8 is defensive. Given MAX_DEPTH = 1, in theory you only ever need to check 1 hop. Setting it to 8 is for:

  • Compatibility with a depth limit that might be relaxed in the future
  • More importantly: if a cycle ever shows up in the data (A's parent is B, B's parent is A), this cap guarantees the function returns instead of looping forever

This is how you write “even if the invariant is broken, the program must not hang.”

Under normal conditions you never reach hop 8. But if some bug turns the parent-child relationship into a cycle, the capped version returns a (possibly wrong) answer and keeps running, while the uncapped version hangs the entire process.

Whenever you traverse a structure that “should theoretically be a tree, but whose data is built at runtime,” add a hop limit like this.

10.8 Kanban: Collaboration Between Agents

plugins/kanban/ offers a different multi-agent model — not “hand it down and wait for the result,” but “share one task board.”

kanban_create_task       create a task
kanban_claim_task        claim a task
kanban_update_task       update progress
kanban_complete_task     complete a task
kanban_list_tasks        view the task list
kanban_heartbeat         ★ heartbeat
Kanban mode: ┌───────────────────────── Shared task board ──────────────────────────┐ │ #1 [unclaimed] Refactor the auth module │ │ #2 [in progress] Write unit tests ← claimed by agent-B │ │ #3 [unclaimed] Update the docs │ │ #4 [done] Fix the login bug ← finished by agent-A │ └──────────────────────────────────────────────────────────────────────┘ ↑ ↑ ↑ agent-A agent-B agent-C (independent of each other, coordinated through the board, no parent-child relationship)
Delegation modeKanban mode
RelationshipParent-child — the main agent assigns explicitlyPeers — whoever is free claims the work
Who decides what to doThe main agentEach agent for itself
LifecycleA subagent ends when its work is doneAgents are long-lived and keep claiming new tasks
Suited toKnown, decomposable batch tasksOngoing workflows with unpredictable sources of work

Why kanban_heartbeat has to exist

agent-B claims task #2 ↓ agent-B crashes / the process is killed / the machine reboots ↓ Task #2 sits in "in progress" forever ↓ No other agent will pick it up — because it looks like "someone is on it" ↓ ★ The task is lost for good Fix: agent-B calls kanban_heartbeat every N seconds → no heartbeat for N×k seconds, and the task automatically returns to "unclaimed"

Any distributed task system built on “claim – execute – complete” must have a heartbeat or lease mechanism.

Otherwise, tasks that were “claimed but died before finishing” stay stuck forever. It's the same problem in message queues, task schedulers, and distributed locks, and the fix is the same: a claim has a time limit and must be continually renewed.

10.9 Why the Delegation System Is 5,071 Lines

Back to the number from the top. The core logic that actually “spawns a subagent” probably needs 200 lines. What are the other 4,800 doing?

CategoryContents
Lifecycle managementCreate, start, monitor, interrupt, clean up, time out, reap zombies
Concurrency controlConcurrency caps, queuing, paused spawning, priorities
Result aggregationCollecting results from 10 subagents, handling partial failures, deciding how to count the ones that timed out
Status queries“How many are running right now,” “how much has been spent,” “which step is it stuck on”
Control channelsInterrupt, inject instructions, pause — each has to be delivered safely across processes/threads
Safety boundariesTool blocking, approval policy, depth checks, lineage
ObservabilityEvery subagent's logs, cost, and elapsed time must be recorded separately and linkable back to the parent task
Failure handlingSubagent crashes, model errors, context blowups, infinite loops — each needs a countermeasure

The “agents calling agents” demo is 20 lines; the production system is 5,000.

That 250x gap comes entirely from “what to do when things go wrong.” The hard part of multi-agent systems has never been “how to spawn,” but “how to clean up when what you spawned runs out of control.”

If you're asked about multi-agent systems in an interview, being able to explain this clearly is worth far more than being able to draw a pretty architecture diagram.