子代理,递归的主循环
任务工具对模型呈现为一个叫 AgentTool 的工具,模型调用它,就能把一块工作整个委托出去。这个工具的实现演示了本工程最省笔墨的一个设计,子代理没有任何新机制,它就是再跑一遍主循环。runAgent 函数组装好提示词和工具池之后,直接 for await 消费同一个 query 生成器,第 2 章讲的循环原样复用,一层套一层,想套多深都行。
派生之前先造上下文。createSubagentContext 拿父循环的 ToolUseContext 加工出子代理用的版本,策略是快照式隔离。AppState 取当前值复制一份,abortController 换新的,readFileState 文件状态缓存换新的,工具池和权限规则按代理定义重新装配。隔离成什么样分两档,同步子代理共享父亲的 setAppState,写状态直接生效。异步子代理的 setAppState 干脆是空操作,父界面的消息列表不会被子代理污染,但保留一个专门的 setAppStateForTasks 通道通往最外层,注册后台任务这类必须让根知道的事情走这条道,源码在 ToolUseContext 的定义处有整段注释解释这个不对称。
共享侧同样讲究。子代理的系统提示词直接复用父亲本轮开始时渲染好的字节快照,ToolUseContext 里有个专门字段存放它。注释解释了原因,当场重新生成的话,远程配置冷热不同会导致两份提示词有细微差异,差一个字节,提示缓存全部失效,fork 出来的子代理就享受不到父亲的缓存命中。这个细节把提示缓存在这套架构里的分量摆得很清楚,为它值得在上下文里扛一份快照。
子代理的对话写进独立的 sidechain 文件,runAgent 里逐条转录,与主对话的记录物理分开。转录有类型过滤,assistant、user、progress 和压缩边界这几种记录在案,纯粹的流事件跳过,每条记录还带着上一条的 uuid,串成一条父链,事后查看一个子代理完整干过什么,打开它的 sidechain 顺着链读就行。子代理结束后产出的结果作为工具结果回到父亲的消息流,中间过程对父亲就是一个黑盒工具调用,这正是用子代理做大规模探索任务时节省主上下文的原理。
上下文对象上还有几个专为子代理准备的字段。agentId 和 agentType 标记这次调用属于哪个代理,hook 引擎靠它们区分主线程和子代理的调用,权限决策也能按代理类型给不同的规则。queryTracking 记录调用链的链号和深度,遥测事件里经常出现,一个任务套了几层代理一目了然。上一章提过的 localDenialTracking 也挂在这,异步子代理改不动全局状态,连续拒绝的计数就记在本地。
仓库里自带了一批内置代理定义,generalPurpose、explore、plan、verification、claudeCodeGuide 等,各自的提示词和工具白名单放在 AgentTool 的 built-in 目录。用户也可以在配置里自定义代理类型。另一个相关机制是 fork 子代理,runForkedAgent 走同样的隔离加递归套路,用途是那些必须字节级复刻父亲上下文的场景,上下文压缩的摘要生成就靠它,第 8 章接上。源码树里还有配套的 EnterWorktreeTool,子代理可以被放进独立的 git worktree 里干活,改动隔离在另一个目录,进一步防止并发代理互相踩脚。
把主循环写成可递归复用的生成器,收益在这里集中兑现。权限、压缩、工具调度这些能力,子代理一分钱没额外花就全都有了,新增一种代理形态只需要准备一份定义文件加一个上下文工厂。