Skip to content

03|Agent 是接口,循环是实现:为什么扩展代码不许依赖循环内部 ​

先别去依赖那个看起来最像引擎的包 ​

第 01 章把默认循环实现标成了"司机"。第一次真要写扩展的人,几乎都会直接找它:它的说明文档第一句话就承认自己是"具体的 Agent 插件和循环驱动器",听起来就像引擎盖。于是界面代码、钩子、自动化协议桥、子代理调度,全都直接依赖这个具体实现,好用它来创建 Agent、发消息、在循环里插一刀。

这条路把第 00 章刚拆掉的"核心"又焊回去了。

正确的依赖方向很简单:Agent 接口拥有对外合同,默认循环实现只是它的一种实现;扩展插件必须依赖接口,司机才能换。 这条纪律推到全仓:扩展依赖"这个能力该长什么样"的定义,绝不直接依赖某一个具体实现。默认循环可以被整个换掉;界面、钩子、工具这些插件用的都是那份接口约定,不是某个具体司机。

这一章只证明一件事:在这个仓库里,"Agent"不是循环的别名。循环是满足 Agent 接口的一个插件。你依赖了司机的内部细节,接口就只是一句没人执行的注释。


1. 两个角色,一份合同,一个司机 ​

先给判断:一个包拥有对外合同,另一个包是这份合同的默认实现。搞反了,后面每一层扩展都会把循环的内部形状吸进自己的类型里。

拥有合同的那个包说得很绝:每个插件(界面、钩子、编排器)都针对这里定义的 Agent handle 编程——零循环依赖,所以循环可换。注册表在异步司机工作里携带发起方 Agent,而不依赖具体循环包的类型。

合同本身很穷。Agent 对外只交出这几件事:

  • 身份和世界:编号、配置、所属会话、状态、自己的上下文
  • 待办投影:inbox,一份可以观察的待处理消息视图
  • 投递:一个统一的发送方法,加上三个固定别名——追加下一轮、插入下一步、注入上下文(第 04 章会拆这三种投递的区别)
  • 控制:取消,以及等待空闲

注意这份清单里没有"跑一步"、没有"调模型"、没有"某种循环类型"。投递语义写在接口上,不写在循环实现里——现在只需要看见:这些方法属于 Agent 接口本身,不属于循环这个具体实现。

默认循环实现确实满足这份合同。但它立刻把自己的实现细节藏起来:内部的具体类、inbox 的可变结构、跑控细节,都是包内部的东西,对外导出的只有插件、服务和配置,没有留任何"逃生舱"让你绕过接口直接摸到内部对象。整个仓库里只有这一个包包含具体的循环逻辑,新行为进插件,不进这里。这句话和"不要依赖我的内部细节"是同一件事的两面。司机穷,是为了让别人不必认识它。

如果没有这层切开会怎样?默认实现会变成事实上的公共 API。界面开始依赖它的私有阶段划分,钩子开始依赖它的调度细节,换循环等于重写半个产品。接口文件还在,但已经没人理它。


2. 连"创建"都挂在接口上,不挂在司机上 ​

判断:创建一个 Agent 是接口这一层的能力,司机只是当前坐在"工厂槽位"里的那个实现。

这是好品味的那一刀。很多人会把"跑循环"和"造一个 Agent"焊在同一个类上,于是每个需要开子会话的插件都得依赖具体的循环包。这个仓库把工厂接口留在了 Agent 接口这一层:把创建能力放在接口上,是为了让自动化协议桥这类消费者只对着接口编程,不依赖具体的循环实现包。

工厂槽位只能有一个实现登记进去,第二个想登记会被直接拒绝。登记这件事本身走的是第 02 章那套可逆效果——司机在自己的初始化里把自己登记成工厂,一旦司机被卸载,工厂槽位会自动清空。

调用方拿到的不是具体循环实现的实例,而是一份"句柄",这份句柄只有两件东西:那个裸接口实例,和一个释放方法。句柄被定位成"消费者能力"——握着注册表里那个裸 Agent 的观察者,不是所有权凭证。谁有权真正拆掉这个 Agent、谁只能观察,是接口上的所有权划分,不是司机内部的礼貌约定。

产品代码怎么遵守这条纪律,看它声明的运行时依赖比看注释更清楚:自动化协议桥、钩子桥这些运行时组件,声明的对等依赖是 Agent 接口本身,具体循环实现只出现在开发和测试依赖里,专门给测试搭一个能跑的司机用。进程内子代理创建孩子时,打的是父 Agent 自己持有的那份接口注册表,不是直接构造一个具体循环实例。

如果没有这一层会怎样?每个要开会话的表面都得依赖具体循环实现。自动化协议桥、界面、子代理、测试替身会把循环实现的构造细节吸进自己的 API。你以为自己换了一个循环实现,其实在换四五个包的运行时依赖关系。工厂槽把"谁会造"收成一个可逆的登记动作——这正是第 02 章的那套纪律用在"创建"这条边上。


3. 扩展面对事件和句柄,不面对司机的内部实现 ​

判断:你要拦截、投递、等待空闲,全部走 Agent 接口和它对外声明的一组事件。司机的内部实现没有给你留任何扩展点,那是故意的。

活的协调事件由接口这一层声明,插件不必依赖具体循环实现。换司机的合法路径也写得很清楚:实现 Agent 接口,经注册表挂上去;监听事件,不依赖具体循环包。

默认循环实现自己列出了"什么东西不该进循环内部":

  • 钩子和政策:挂在接口声明的检查点上,外加工具管道
  • 上下文压缩:挂在准入检查这个时刻,请求失败恢复挂在请求出错这个时刻
  • 模型重试:监听请求出错事件,返回"重试"决定
  • 沙箱、权限、计划模式:挂在工具管道上,不是循环内部分支
  • 子代理:循环外面的可替换能力,进程内的子代理用接口自己的创建能力和句柄

读这份清单的正确姿势不是背事件名。是看见一条切割线:循环内部只做"叫模型、跑工具、再叫一次";多出来的全部是旁边的插件。第 02 章的拦截链否决权,挂的就是这些接口声明的事件,不是具体循环实现内部某个可以打补丁的方法。

如果没有这层会怎样?每个人都会在循环内部加一个"很小的分支判断"。压缩分支、重试分支、权限分支、子代理分支。三个月后循环内部变成产品本身,接口变成遗物。"新行为进插件,不进这里"就会变成一句没人执行的说明文档。


4. 你现在该依赖什么 ​

写扩展之前先对这张表。打错依赖方向,就是在焊死司机。

你要做的事打哪里
监听创建、inbox、准入检查、请求失败接口层声明的一组事件
给人发一条消息、插入一步、塞上下文接口的发送方法及其三个固定别名
取消、等空闲接口的取消和空闲等待方法
程序化开一个会话接口的创建/恢复能力,拿到一份句柄
给某一个 Agent 挂工具或提示词那个 Agent 自己的作用域上下文(第 08 章)
换默认循环实现接口并登记为工厂;或实现接口后普通注册
组装整台产品组装层可以依赖具体实现,那是"作曲家"的特权,不是扩展的特权

测试代码里依赖具体循环实现是合法的——你要的是一台能跑的司机,不是产品耦合。运行时依赖里出现它,就是本章要挡住的那件事。

第 04 章会钻进司机肚子里看一次运行节拍具体是怎么走的。那是为了理解默认实现怎么履行合同,不是授权你去依赖肚子里的细节。读完 04,依赖方向仍然是这一章的表。


5. 结语:带走一句话 ​

循环可换,当且仅当没有任何重要的代码认识它的内部细节——创建走工厂槽位,说话走接口的发送方法,拦截走接口声明的事件;你依赖了具体循环实现的内部结构,司机就已经焊死了。