Codex 系统手册

扩展和 hook 接口契约

stage-18.4.322 个文件

这一阶段像给扩展和插件准备的“插座说明书”,属于幕后支撑,不直接干主流程活儿。扩展 API 先规定能借宿主做什么,比如创建子代理、发事件、往当前对话塞内容、保存自己的小数据。贡献者接口说明插件能在哪些时刻插手:线程、工具调用、对话回合、MCP 配置等。记忆库、用户指令、IDE 上下文也统一了数据样子。hooks 部分则规定钩子怎么声明、触发、返回结果,并加载命令格式模板,让外部脚本按同一套规矩安全接入。

本阶段的文件22

扩展 API 门面

这些文件定义扩展 API crate 的顶层公共接口及其共享能力导出。

ext/extension-api/src/capabilities/agent.rs源码 ↗
domain_logicrequest handling

这个文件解决的是扩展和宿主之间的分工问题:扩展可能想从当前对话线程里再开一个“子代理”,就像一个员工把一部分任务交给另一个助手去做。但真正怎么创建、怎么连接、怎么返回结果,是宿主程序的事。这里用 AgentSpawner 这个 trait(可以理解成一份“能力协议”)规定:给我一个来源线程编号和一份请求,我会异步返回创建好的子代理,或者返回错误。异步的意思是这件事可能要等一会儿,不会卡住当前流程。AgentSpawnFuture 是这个异步结果的统一包装。最后的 impl 让普通函数或闭包也能直接当作 AgentSpawner 使用,方便宿主在创建扩展时注入一段具体的启动逻辑。

函数细节1
F::spawn_subagent31–37 ↗
fn spawn_subagent(
        &'a self,
        forked_from_thread_id: ThreadId,
        request: R,
    ) -> AgentSpawnFuture<'a, Self::Spawned, Self::Error>

作用:这个函数把“创建子代理”的请求转交给宿主传进来的函数或闭包。这样扩展只认 AgentSpawner 这套接口,不需要关心背后具体是谁在干活。

数据流:进去的是当前要从哪个线程分叉出来的 ThreadId,以及扩展准备好的 request 请求数据。函数本身不加工这些数据,而是直接调用保存的函数或闭包,把这两样东西传过去。出来的是一个异步结果:以后会得到创建好的子代理,或者得到错误信息;它自己不直接修改其他状态。

调用关系:它是 AgentSpawner 接口的适配层。当某个扩展需要创建子代理时,会通过 AgentSpawner 调用 spawn_subagent;如果宿主注入的是一个普通函数或闭包,这个实现就负责把接口调用接到那个函数或闭包上。真正的创建工作由被调用的函数或闭包完成。

ext/extension-api/src/capabilities/events.rs源码 ↗
io_transportcross-cutting

扩展有时需要告诉宿主程序:“我这边发生了一件事。”这个文件就是给这件事定规矩的。它定义了 ExtensionEventSink,也就是“事件投递口”:扩展把 Event 交进去,后面怎么保存、排序、发给谁、记不记日志,都由宿主决定。这里的 Event 是协议里的事件对象,可以理解成一张写好内容的通知单。这个接口是 fire-and-forget,意思是“交出去就不等结果”,像把信投进邮筒,不在原地等邮递员送完。文件里还放了 NoopExtensionEventSink,这是一个空实现:收到事件后直接丢掉。它的作用很实际:当宿主没有开放事件发送能力时,系统仍然能给扩展一个安全的接收器,扩展调用 emit 也不会报错,只是不会真的发出任何东西。

函数细节1
NoopExtensionEventSink::emit18–18 ↗
fn emit(&self, _event: Event)

作用:这个函数是“空事件接收器”的发送动作。别人把事件交给它时,它故意什么也不做,用来表示当前宿主不接收扩展事件。

数据流:进去的是一个 Event,也就是扩展想发给宿主的事件通知;函数接到后不读取、不保存、不转发,直接结束;出来没有返回值,也不会改动任何状态,事件等于被安全地忽略了。

调用关系:它实现了 ExtensionEventSink 这个事件投递接口,所以凡是代码只认“能 emit 事件的东西”时,都可以拿它来顶上。真正支持事件的宿主会提供别的实现;不支持时就用这个实现,让扩展代码照常调用 emit,但不会触发后续传输或记录。

ext/extension-api/src/capabilities/response_items.rs源码 ↗
domain_logicrequest handling / cross-cutting

有些插件不只是旁观,还想在模型这一轮回答还没结束时,临时加入一些模型能看到的新输入,好像会议中有人递了一张纸条给发言者。这个文件就是给这种能力定规矩:插件交出一组 ResponseInputItem(模型可见的输入条目),宿主如果支持,就把它们注入当前轮次;如果不支持,就原封不动退回来。这里的 Future 是“以后会完成的异步结果”,因为注入可能不是立刻完成。ResponseItemInjector 是宿主需要实现的接口。NoopResponseItemInjector 是兜底版本:它永远不注入,只把原来的内容作为错误结果返回。这样插件不用猜宿主支不支持,调用同一个接口就行,失败时还能拿回没被处理的内容。

函数细节1
NoopResponseItemInjector::inject_response_items27–32 ↗
fn inject_response_items(
        &'a self,
        items: Vec<ResponseInputItem>,
    ) -> ResponseItemInjectionFuture<'a>

作用:这是“不支持注入”时使用的实现。别人把想塞给模型的输入交给它,它不会丢掉这些输入,而是立刻告诉调用方:我没法处理,并把原东西还回去。

数据流:输入是一组 ResponseInputItem,也就是准备给模型看的输入条目。函数不检查、不修改、不保存这些条目,只把它们放进一个已经完成的异步结果里;结果是 Err(items),意思是“注入失败,东西还给你”。它本身不改变任何外部状态。

调用关系:当宿主程序没有开放“同一轮对话中插入模型输入”的能力时,就会把这个空实现交给插件使用。它内部用 ready 生成一个马上完成的异步结果,再用 pin 把这个结果包装成接口要求的 Future 形状;调用方收到失败后,可以决定改用别的方式处理这些输入。

调用图:外部调用 2 个(pin, ready)。

ext/extension-api/src/capabilities/mod.rs源码 ↗
data_modelcross-cutting

这个文件本身不做复杂运算,也没有函数。它的作用是把三个子模块接进来:agent、events、response_items,然后把里面重要的类型重新公开。可以把它理解成商场入口的导览牌:真正的店在里面,但入口处把常用店名列出来,顾客不用绕路找。这里公开的东西包括:启动 agent 的工具,agent 可以理解成扩展里能独立执行任务的小助手;事件接收器,用来让扩展把发生的事通知出去;以及响应内容注入器,用来往返回给用户的结果里加内容。它重要的地方在于统一了扩展 API 的外观,让外部代码只需要从 capabilities 这一层拿能力,而不用关心这些能力具体分散在哪些文件里。

ext/extension-api/src/lib.rs源码 ↗
othercross-cutting

可以把这个文件想成一家商场的总服务台。真正的商品放在 capabilities、contributors、registry、state、user_instructions 等不同柜台里,但来的人不需要知道每个柜台在哪,只要到这里就能拿到公开提供的东西。它先声明这些内部模块,然后用 pub use 把重要接口重新导出,比如扩展能注册什么工具、怎样参与一次对话、怎样提供用户指令、怎样保存扩展自己的数据等。这样做的好处是把“内部怎么分文件”和“外部怎么使用 API”分开:项目内部以后可以整理文件结构,外部调用者仍然面对一个稳定、清楚的入口。这个文件本身不执行流程,也不保存数据,它的价值在于定义边界:哪些东西是扩展作者可以依赖的公开契约。

贡献者契约

这些文件描述贡献者接口,以及扩展在参与运行时行为时接收的生命周期负载。

ext/extension-api/src/contributors/mcp.rs源码 ↗
data_modelconfig resolution / thread runtime setup

MCP 可以理解成一种让程序接入外部工具或服务的服务器机制。这个文件不真正启动服务器,而是规定扩展该怎么“提交配置建议”。它有一个 McpServerContributionContext,像一张工作单,里面放着宿主程序当前可见的配置,以及如果这次解析是针对某个正在运行的线程,还会带上这个线程一开始被冻结下来的输入。扩展读取这张工作单后,可以产出 McpServerContribution。这个枚举像三种操作票:Set 表示新增或替换一个命名服务器,SelectedPlugin 表示为当前线程选中的插件注册服务器,Remove 表示删掉一个命名服务器。这样做的好处是,扩展只描述想改什么,真正合并和应用配置的流程可以在别处统一处理,避免扩展直接乱改运行时配置。

函数细节5
McpServerContributionContext::clone18–20 ↗
fn clone(&self) -> Self

作用:复制一份 MCP 贡献上下文。因为这个上下文只借用已有资料,不拥有大块数据,所以复制它就像多拿一张指向同一份资料的便签。

数据流:进去的是当前上下文,里面有配置引用和可选的线程初始输入引用;它不重新创建配置,也不改任何内容,只把同样的引用原样放进一个新上下文;出来的是一份等价的新上下文。

调用关系:它配合 Rust 的 Clone 能力使用,让需要复制上下文的代码可以安全、轻量地传递它;真正读取配置和线程输入的工作仍由 config 和 thread_init 这些访问函数完成。

McpServerContributionContext::global27–32 ↗
fn global(config: &'a C) -> Self

作用:创建一个“不针对具体线程”的 MCP 解析上下文。有人只想根据全局配置决定服务器时,就会用它。

数据流:进去的是宿主程序的配置引用;它把这份配置放进上下文,并把线程初始输入设为没有;出来的是一个只包含全局配置视角的 McpServerContributionContext。

调用关系:runtime_config_with_context 会在构建运行时 MCP 配置时调用它,用来处理不依赖某个具体线程的贡献;后续扩展可以通过 config 读取配置,但 thread_init 会返回空。

调用图:被 1 处调用(runtime_config_with_context)。

McpServerContributionContext::for_thread35–40 ↗
fn for_thread(config: &'a C, thread_init: &'a ExtensionDataInit) -> Self

作用:创建一个“针对某个正在运行线程”的 MCP 解析上下文。它让扩展除了看全局配置,还能看到这个线程启动时的固定输入。

数据流:进去的是宿主配置引用和该线程的 ExtensionDataInit 引用;它把配置保存起来,并把线程初始输入包成可用状态;出来的是带线程信息的 McpServerContributionContext。

调用关系:runtime_config_with_context 和 selected_plugin_contributions 会在需要按线程生成 MCP 服务器配置时调用它;之后扩展的 contribute 过程可以用 config 看宿主配置,也可以用 thread_init 看这个线程最初带来的数据。

调用图:被 2 处调用(runtime_config_with_context, selected_plugin_contributions)。

McpServerContributionContext::config43–45 ↗
fn config(&self) -> &'a C

作用:取出上下文里宿主程序给扩展看的配置。扩展用它来判断该添加、替换或删除哪些 MCP 服务器。

数据流:进去的是一个上下文;它只读取里面保存的配置引用,不复制也不修改配置;出来的是同一份配置的只读引用。

调用关系:扩展执行 contribute 时会调用它来读取外部可见的配置,然后据此产出 McpServerContribution;它是扩展了解宿主配置的主要入口。

调用图:被 2 处调用(contribute, contribute)。

McpServerContributionContext::thread_init48–50 ↗
fn thread_init(&self) -> Option<&'a ExtensionDataInit>

作用:取出当前线程启动时被冻结的初始输入;如果这次解析不是针对线程,就明确返回没有。扩展用它来做按线程不同而不同的 MCP 配置。

数据流:进去的是一个上下文;它检查里面有没有线程初始输入;出来的是一个可选结果:有线程时给出只读引用,没有线程时给出空值,不改任何状态。

调用关系:扩展的 contribute 过程会调用它来判断自己是否处在线程级解析中,并读取线程专属输入;这个函数和 for_thread 配套使用,而 global 创建的上下文会让它返回空。

调用图:被 1 处调用(contribute)。

ext/extension-api/src/contributors/thread_lifecycle.rs源码 ↗
data_modelthread lifecycle

这里没有真正执行动作的函数,主要是几种输入数据结构。可以把它想成剧场里的“场次通知单”:线程刚开场、重新开场、暂时没活、准备收场时,工作人员需要知道不同的信息。ThreadStartInput 给的信息最多,因为启动时要知道宿主配置、会话从哪里来、有没有可长期保存的线程状态、选了哪些执行环境,以及两份数据存储。这里的 ExtensionData 可以理解成扩展可用的“储物柜”:session_store 是整个会话共用的柜子,thread_store 是当前线程自己的柜子。恢复、空闲、停止时,文件只传这两份储物柜,因为这些阶段通常不需要重新知道配置和环境。这个文件重要在于它把生命周期各阶段的“可见信息边界”说清楚,避免扩展在错误的时机依赖不存在的数据。

ext/extension-api/src/contributors/tool_lifecycle.rs源码 ↗
data_modelrequest handling

当模型要调用一个工具时,扩展插件可能想在开始前记日志、准备数据,或者在结束后统计成功失败。这个文件不真正执行工具,也不写业务流程;它只是定义这些回调要用的数据形状。比如 ToolCallSource 说明这次工具调用是模型直接发起的,还是代码模式在运行某个单元时顺带发起的。ToolCallOutcome 说明工具最后是正常完成、被策略拦住、失败,还是被取消。ToolStartInput 和 ToolFinishInput 则像两张通知单,里面带着会话、线程、本轮对话的存储区,以及 turn_id、call_id、工具名等定位信息。这样扩展拿到通知时,就知道“谁在什么时候调用了哪个工具,最后结果怎样”。

ext/extension-api/src/contributors/turn_input.rs源码 ↗
data_modelrequest handling

这份文件主要是数据结构定义,也就是规定信息要按什么格式装起来。这里的“一轮 turn”可以理解成用户发出一次请求后,系统准备处理这次请求的整个过程。TurnInputEnvironment 描述某个运行环境:它有稳定的环境编号、当前工作目录,以及它是不是本轮的主要环境。TurnInputContext 则把本轮更大的背景打包起来:本轮的编号、用户输入内容,以及按主机优先顺序排好的环境列表。这样做的好处是,给扩展插件提供信息时不会乱传一堆零散参数,而是像填表一样清楚、稳定。这个文件本身没有函数,也不做判断或读写文件;它的重要性在于统一“插件能看到的上下文长什么样”。

ext/extension-api/src/contributors/turn_lifecycle.rs源码 ↗
data_modelturn lifecycle

这里没有真正执行动作的函数,主要是几种输入数据结构。可以把一次 turn 理解成用户和系统之间的一轮处理,比如用户发出请求,系统开始工作,然后成功结束、被打断,或遇到错误。这个文件把这些关键时刻会用到的数据打包成固定格式:开始时有回合编号、协作模式、开始时的 token 用量快照,以及三层数据存放区;结束、中止、出错时也会带上对应的数据。这里的 token 可以简单理解成模型处理文字时使用的“计量单位”。三层 store 分别对应会话、线程、当前回合,像不同大小的抽屉:长期一点的数据放会话抽屉,当前线程的数据放线程抽屉,只属于这一回合的数据放回合抽屉。这样扩展写作者不用猜宿主会传什么,按这些结构取用就行。

ext/extension-api/src/contributors.rs源码 ↗
othercross-cutting

这个文件的重点不是自己做某个具体功能,而是给扩展系统定规矩。比如:一轮对话开始了、工具要运行了、配置变了、模型返回了 token 用量,主程序都可以通过这里定义的 trait(可以理解成“必须遵守的接口约定”)通知扩展。扩展也可以通过这些接口提供额外提示词片段、上下文、工具、MCP 服务器等。文件里还定义了 ExtensionFuture,它是“装在盒子里的异步任务”,让不同扩展都能用同一种方式把未来才完成的结果交回来。很多生命周期函数都有默认实现:什么也不做,只返回一个已经完成的异步任务。这很重要,因为扩展只需要实现自己关心的钩子,不必把所有入口都写一遍。整体上,它把主程序和扩展隔开:主程序给有限、稳定的信息,扩展在边界内补充能力,避免直接依赖核心运行时内部结构。

函数细节12
ThreadLifecycleContributor::on_thread_start80–85 ↗
fn on_thread_start(&'a self, input: ThreadStartInput<'a, C>) -> ExtensionFuture<'a, ()>

作用:这是线程级运行环境刚启动后的通知入口。扩展可以在这里给这个线程准备自己的私有状态,比如加载缓存或初始化记录;默认版本什么也不做。

数据流:进去的是 ThreadStartInput,里面带着主程序允许扩展看到的启动信息,以及 self 这个扩展对象 → 默认实现只是把这些值接住,避免编译器认为没用,然后不做任何改动 → 出来的是一个立刻完成的异步结果,线程数据没有被改变。

调用关系:主程序在线程作用域的存储准备好之后会调用它。默认实现只用 Box::pin 把一段空的 async 代码固定成统一的异步返回形式;真正有需求的扩展会覆盖这个函数,接手启动时的准备工作。

调用图:外部调用 1 个(pin)。

ThreadLifecycleContributor::on_thread_resume88–93 ↗
fn on_thread_resume(&'a self, input: ThreadResumeInput<'a>) -> ExtensionFuture<'a, ()>

作用:这是主程序从保存过的历史恢复一个线程运行环境后的通知。扩展可以用它重新接上自己的线程级状态;默认版本什么也不做。

数据流:进去的是 ThreadResumeInput,表示这次线程是从已有历史恢复来的 → 默认实现只接收 input 和 self,不读取、不写入任何东西 → 出来的是一个完成的异步结果,没有额外效果。

调用关系:主程序构造好从历史恢复的运行环境后会调用它。默认实现通过 Box::pin 返回空异步任务;需要恢复私有数据的扩展会重写它。

调用图:外部调用 1 个(pin)。

ThreadLifecycleContributor::on_thread_idle100–105 ↗
fn on_thread_idle(&'a self, input: ThreadIdleInput<'a>) -> ExtensionFuture<'a, ()>

作用:这是线程暂时没活干时给扩展的通知。扩展可以趁这个时机提交后续输入或做轻量收尾;默认版本什么也不做。

数据流:进去的是 ThreadIdleInput,说明线程已经处理完当前立刻要做的工作 → 默认实现只是接住这些信息,不提交新任务,也不改状态 → 出来的是一个完成的异步结果。

调用关系:主程序在线程工作队列暂时清空后会调用它。默认实现只用 Box::pin 包住空操作;如果扩展想在空闲时推动下一步,它会覆盖这个函数,但最终是否真的开始新一轮仍由主程序决定。

调用图:外部调用 1 个(pin)。

ThreadLifecycleContributor::on_thread_stop108–113 ↗
fn on_thread_stop(&'a self, input: ThreadStopInput<'a>) -> ExtensionFuture<'a, ()>

作用:这是线程运行环境即将被丢弃前的通知。扩展可以在这里保存或清理自己的线程级数据;默认版本什么也不做。

数据流:进去的是 ThreadStopInput,表示线程快要结束了 → 默认实现不保存、不清理,只把输入接住 → 出来的是一个完成的异步结果,主程序可以继续销毁线程运行环境。

调用关系:主程序在线程运行时和线程级存储被释放之前调用它。默认实现通过 Box::pin 返回空异步任务;真正需要落盘、刷新或释放资源的扩展会重写它。

调用图:外部调用 1 个(pin)。

TurnLifecycleContributor::on_turn_start124–129 ↗
fn on_turn_start(&'a self, input: TurnStartInput<'a>) -> ExtensionFuture<'a, ()>

作用:这是一次对话回合刚准备好、但还没真正运行前的通知。扩展可以在这里准备这一回合专用的状态;默认版本什么也不做。

数据流:进去的是 TurnStartInput,包含这一回合的标识和扩展可用的存储信息 → 默认实现只接收它,不写入任何回合数据 → 出来的是一个完成的异步结果。

调用关系:主程序创建好回合级扩展存储后、启动回合任务前会调用它。默认实现用 Box::pin 包一个空 async;扩展如果需要每回合初始化,就覆盖这里。

调用图:外部调用 1 个(pin)。

TurnLifecycleContributor::on_turn_stop132–137 ↗
fn on_turn_stop(&'a self, input: TurnStopInput<'a>) -> ExtensionFuture<'a, ()>

作用:这是一次对话回合正常完成、即将被清理前的通知。扩展可以用它做回合级收尾;默认版本什么也不做。

数据流:进去的是 TurnStopInput,表示这一回合已经结束 → 默认实现不读取结果、不清理数据,只返回空异步任务 → 出来的是一个完成信号,主程序随后可以丢弃回合运行时和存储。

调用关系:主程序在完成的回合运行时和回合存储被释放前调用它。默认实现只调用 Box::pin 固定空异步任务;需要收尾的扩展会重写。

调用图:外部调用 1 个(pin)。

TurnLifecycleContributor::on_turn_abort140–145 ↗
fn on_turn_abort(&'a self, input: TurnAbortInput<'a>) -> ExtensionFuture<'a, ()>

作用:这是一次正在运行的对话回合被中止后的通知。扩展可以据此取消自己的相关工作或记录原因;默认版本什么也不做。

数据流:进去的是 TurnAbortInput,表示回合已经被主程序中止 → 默认实现接住输入但不改变任何状态 → 出来的是一个完成的异步结果。

调用关系:主程序中止某个运行中的回合后会调用它。默认实现通过 Box::pin 返回空任务;如果扩展有后台动作、临时状态或审计记录,就可以覆盖它。

调用图:外部调用 1 个(pin)。

TurnLifecycleContributor::on_turn_error148–153 ↗
fn on_turn_error(&'a self, input: TurnErrorInput<'a>) -> ExtensionFuture<'a, ()>

作用:这是主程序发现某个运行中的对话回合出错时的通知。扩展可以用它记录错误或清理受影响的状态;默认版本什么也不做。

数据流:进去的是 TurnErrorInput,带着出错相关的信息 → 默认实现只把 self 和 input 放进空异步块里,不处理错误 → 出来的是一个完成的异步结果。

调用关系:主程序观察到回合错误时会调用它。默认实现只用 Box::pin 统一成异步返回;想做错误上报或恢复动作的扩展会重写这个入口。

调用图:外部调用 1 个(pin)。

ConfigContributor::on_config_changed179–186 ↗
fn on_config_changed(
        &self,
        _session_store: &ExtensionData,
        _thread_store: &ExtensionData,
        _previous_config: &C,
        _new_config: &C,
    )

作用:这是线程配置已经被主程序确认改变后的通知。扩展可以比较旧配置和新配置,更新自己的行为;默认版本什么也不做。

数据流:进去的是会话存储、线程存储、旧配置和新配置 → 默认实现完全不使用这些值,也不返回异步任务 → 出来没有结果,也不会改动任何扩展状态。

调用关系:主程序提交新的有效线程配置后会调用它。它是同步函数,也就是当场执行完;需要响应配置变化的扩展会覆盖它,比如重新读取开关或刷新策略。

TokenUsageContributor::on_token_usage196–207 ↗
fn on_token_usage(
        &'a self,
        _session_store: &'a ExtensionData,
        _thread_store: &'a ExtensionData,
        _turn_store: &'a ExtensionData,
        _token_usage: &'a TokenUsageIn

作用:这是模型返回 token 用量后给扩展的通知。token 可以简单理解成模型读写文字时的计量单位,扩展可用它做统计或预算提醒;默认版本什么也不做。

数据流:进去的是会话存储、线程存储、回合存储,以及 TokenUsageInfo 这份用量报告 → 默认实现只把这些输入打包接住,不统计、不写入、不发通知 → 出来的是一个完成的异步结果。

调用关系:主程序在更新内部缓存的 token 用量之后、通知客户端之前调用它。默认实现通过 Box::pin 返回空异步任务;真正关心费用、限额或监控的扩展会覆盖它。

调用图:外部调用 1 个(pin)。

ToolLifecycleContributor::on_tool_start227–229 ↗
fn on_tool_start(&'a self, _input: ToolStartInput<'a>) -> ToolLifecycleFuture<'a>

作用:这是一个工具调用已经被主程序接受、准备执行时的通知。扩展可以观察工具即将运行这件事,比如做审计或计时;默认版本什么也不做。

数据流:进去的是 ToolStartInput,描述这次工具调用的开始信息 → 默认实现不检查工具内容,也不改写输入 → 出来的是一个已经准备好的完成结果,表示可以继续执行工具。

调用关系:主程序接受工具调用后会调用它。默认实现用 std::future::ready 造出一个马上完成的任务,再用 Box::pin 固定成统一的 ToolLifecycleFuture;需要观察工具启动的扩展会重写。

调用图:外部调用 2 个(pin, ready)。

ToolLifecycleContributor::on_tool_finish232–234 ↗
fn on_tool_finish(&'a self, _input: ToolFinishInput<'a>) -> ToolLifecycleFuture<'a>

作用:这是一个工具调用结束后的通知,不管它是成功、失败、被拦截还是被取消。扩展可以用它记录结果或结束计时;默认版本什么也不做。

数据流:进去的是 ToolFinishInput,说明工具调用已经有了结局 → 默认实现不读取结果、不修改输出、不做记录 → 出来的是一个马上完成的异步结果。

调用关系:主程序在工具调用返回、被阻止、失败或取消之后会调用它。默认实现用 ready 生成立即完成的任务,并用 Box::pin 包成统一返回类型;需要审计或统计工具结果的扩展会覆盖它。

调用图:外部调用 2 个(pin, ready)。

扩展运行时支持类型

这些文件为扩展状态、主机提供的指令以及扩展驱动的事件发射提供稳定的支持契约。

ext/extension-api/src/state.rs源码 ↗
domain_logiccross-cutting

扩展运行时常常需要保存一点自己的状态,比如缓存、配置、计数器,或某个会话里的临时信息。这个文件做的事,就像给每个宿主对象配一个带标签的储物柜:标签不是字符串,而是 Rust 的类型本身,所以“存进去的是 A 类型,取出来也只能按 A 类型取”。ExtensionDataInit 是创建储物柜前先准备好的初始物品清单,可以被复制,但里面的值用 Arc(可共享的引用计数指针)共享,不会真的拷贝一大份。ExtensionData 是真正挂在某个宿主对象上的储物柜,带有 level_id 来说明它属于谁。内部用 Mutex(一把锁,防止多个任务同时改同一张表)保护 HashMap。值先被擦掉具体类型,统一当作 Any 保存;取出时再用 downcast_data 还原成正确类型。一个重要细节是:如果锁曾被持有者异常中断,这里会继续取回里面的数据,而不是直接崩掉。

函数细节12
ExtensionDataInit::new23–25 ↗
fn new() -> Self

作用:创建一份空的扩展初始数据清单。调用方可以先拿到这张空清单,再往里面放宿主提前准备好的数据。

数据流:进去没有额外输入 → 它使用默认值做出一个没有任何条目的 ExtensionDataInit → 出来是一份空的初始数据容器,之后可以继续插入内容。

调用关系:它是准备阶段常用的起点,会被 thread_start_task、selected_plugin_contributions 这类流程调用,用来先搭好一张空表。它内部只是走默认创建逻辑,不做复杂工作。

调用图:被 2 处调用(thread_start_task, selected_plugin_contributions);外部调用 1 个(default)。

ExtensionDataInit::insert28–35 ↗
fn insert(&mut self, value: T) -> Option<Arc<T>>

作用:把一个指定类型的值放进初始数据清单里。以后创建真正的 ExtensionData 时,这个值会跟着一起带进去。

数据流:进去一个值 value → 它用这个值的类型当钥匙,把值包进 Arc 方便共享,再放进 HashMap → 出来是之前同类型旧值的 Arc,如果以前没有就返回空;清单本身被更新。

调用关系:它通常在 seed_thread_state 这类“给线程或任务预先塞状态”的流程里使用。它只负责把初始值登记好,真正运行时的存取由 ExtensionData 接手。

调用图:被 1 处调用(seed_thread_state);外部调用 1 个(new)。

ExtensionDataInit::get38–44 ↗
fn get(&self) -> Option<Arc<T>>

作用:从初始数据清单里按类型取出宿主预先放好的值,但不创建真正可变的 ExtensionData 作用域。

数据流:进去一个想要的类型 T → 它在清单里用这个类型查找,找到后克隆 Arc 共享指针,再交给 downcast_data 还原成 Arc<T> → 出来是这个类型的共享值,找不到就返回空。

调用关系:它适合在还没进入完整扩展作用域时读取宿主给的输入。它把类型还原这一步交给 downcast_data,保证返回值是调用方要求的类型。

调用图:调用 1 个内部函数(downcast_data)。

ExtensionData::new56–58 ↗
fn new(level_id: impl Into<String>) -> Self

作用:为某个宿主对象创建一份空的扩展数据储物柜。调用方只要给一个宿主身份 level_id,就能得到可供扩展存取状态的容器。

数据流:进去一个 level_id → 它先创建空的 ExtensionDataInit,再调用 new_with_init → 出来是一个带宿主身份、但没有预置条目的 ExtensionData。

调用关系:它是最常见的创建入口,会被 spawn_review_thread、make_session_and_context、make_turn_context 等很多流程使用。它自己不直接组装细节,而是把真正创建工作交给 ExtensionData::new_with_init。

调用图:被 38 处调用(spawn_review_thread, new, handle_output_item_done_records_image_save_history_message, handle_output_item_done_skips_image_save_message_when_save_fails, make_session_and_context, make_session_and_context_with_auth_config_home_and_rx, tool_calls_reopen_mailbox_delivery_for_current_turn, make_turn_context, plan_mode_uses_contributed_turn_item_for_last_agent_message, finalized_turn_item_defers_mailbox_for_contributed_visible_text (+15 more));外部调用 2 个(new_with_init, default)。

ExtensionData::new_with_init61–66 ↗
fn new_with_init(level_id: impl Into<String>, init: ExtensionDataInit) -> Self

作用:用一份已有的初始数据清单创建真正的 ExtensionData。适合宿主已经提前准备好一些扩展需要的数据时使用。

数据流:进去一个 level_id 和一份 ExtensionDataInit → 它把 level_id 转成字符串,把 init 里的条目移进带锁的 HashMap → 出来是一个已经装好初始数据的 ExtensionData。

调用关系:它被 ExtensionData::new 调用,也可以作为更完整的创建方式使用。它把 ExtensionDataInit 这张“预备清单”变成运行时真正可读写的储物柜。

调用图:被 1 处调用(new);外部调用 2 个(into, new)。

ExtensionData::level_id69–71 ↗
fn level_id(&self) -> &str

作用:返回这份扩展数据属于哪个宿主对象。扩展或统计逻辑可以用它知道自己正在服务的是哪一层、哪一个对象或哪段上下文。

数据流:进去的是当前 ExtensionData 自身 → 它直接读取内部保存的 level_id 字符串引用 → 出来是宿主身份文本,不修改任何数据。

调用关系:它会在 contribute、on_token_usage 等流程中被使用,通常用于扩展贡献内容或记录用量时确认当前作用域是谁。它不调用别的核心函数,只是提供身份信息。

调用图:被 3 处调用(contribute, on_token_usage, contribute)。

ExtensionData::get74–80 ↗
fn get(&self) -> Option<Arc<T>>

作用:从运行中的扩展数据里按类型取一个已保存的值。调用方不用知道内部怎么存,只要知道自己要的类型。

数据流:进去一个目标类型 T 和当前 ExtensionData → 它先通过 entries 拿到带锁的表,再用类型编号查找,找到后克隆 Arc,并用 downcast_data 还原类型 → 出来是 Arc<T>,没有对应值就返回空。

调用关系:它是扩展读取自己状态的常规入口。它先依赖 ExtensionData::entries 安全进入内部表,再把类型恢复工作交给 downcast_data。

调用图:调用 2 个内部函数(entries, downcast_data)。

ExtensionData::get_or_init86–95 ↗
fn get_or_init(&self, init: impl FnOnce() -> T) -> Arc<T>

作用:按类型取值;如果还没有,就现场创建一个放进去再返回。它适合做“第一次用才初始化”的缓存或状态对象。

数据流:进去一个目标类型 T 和一个 init 创建函数 → 它锁住内部表,检查是否已有这个类型的值;没有就运行 init 生成新值并包进 Arc 保存;最后克隆共享指针并还原成 Arc<T> → 出来一定是这个类型的共享值,同时可能改变内部表。

调用关系:它是读和懒加载合在一起的入口。它通过 ExtensionData::entries 保护内部表,并用 downcast_data 返回正确类型;代码中特别提醒 init 会在锁里运行,所以应该很快,别在这里做重活。

调用图:调用 2 个内部函数(entries, downcast_data);外部调用 1 个(clone)。

ExtensionData::insert98–105 ↗
fn insert(&self, value: T) -> Option<Arc<T>>

作用:把一个指定类型的值放进运行中的 ExtensionData,替换掉同类型旧值。扩展可以用它保存或更新自己的状态。

数据流:进去一个 value → 它锁住内部表,用 value 的类型作为钥匙,把 value 包进 Arc 放入表中 → 出来是被替换掉的旧值 Arc<T>,如果没有旧值就返回空;内部表被更新。

调用关系:它会被 contribute、on_config_changed 等流程调用,说明扩展在贡献内容或配置变化时会写入状态。它把安全访问内部表的工作交给 ExtensionData::entries。

调用图:调用 1 个内部函数(entries);被 8 处调用(contribute, contribute, on_config_changed, on_config_changed, on_config_changed, contribute, on_config_changed, on_config_changed);外部调用 1 个(new)。

ExtensionData::remove108–113 ↗
fn remove(&self) -> Option<Arc<T>>

作用:从运行中的 ExtensionData 里按类型删掉一个值,并把删掉的值交还给调用方。适合清理不再需要的扩展状态。

数据流:进去一个目标类型 T → 它锁住内部表,用类型编号删除对应条目 → 出来是被删除的 Arc<T>,如果原来没有就返回空;内部表少了一项。

调用关系:它是存取操作中的清理入口。和 get、insert 一样,它先通过 ExtensionData::entries 拿到安全的内部表,再完成删除。

调用图:调用 1 个内部函数(entries)。

ExtensionData::entries115–117 ↗
fn entries(&self) -> std::sync::MutexGuard<'_, HashMap<TypeId, ErasedData>>

作用:安全地打开 ExtensionData 内部那张表。它像拿钥匙开柜门:拿到锁以后,其他线程就不能同时改这张表。

数据流:进去的是当前 ExtensionData → 它尝试锁住 Mutex;如果锁因为之前的异常而处于“中毒”状态,就取回里面的数据继续用 → 出来是一个 MutexGuard,也就是带锁访问 HashMap 的临时通行证。

调用关系:它是 get、get_or_init、insert、remove 的共同底层工具。外部不直接碰内部 HashMap,而是都通过它保证并发访问不会把数据改乱。

调用图:被 4 处调用(get, get_or_init, insert, remove)。

downcast_data120–128 ↗
fn downcast_data(value: ErasedData) -> Arc<T>

作用:把内部统一保存的“已擦除类型”的数据,还原成调用方真正想要的类型。它是类型安全储物柜能工作的关键一步。

数据流:进去一个 ErasedData,也就是 Arc<dyn Any + Send + Sync> 这样的通用盒子 → 它尝试把盒子还原成 Arc<T> → 出来是具体类型的 Arc<T>;如果类型对不上,会触发 unreachable,表示程序内部存取规则被破坏了。

调用关系:它被 ExtensionDataInit::get、ExtensionData::get、ExtensionData::get_or_init 等读取路径调用。因为所有值都是按 TypeId 放进去的,正常情况下还原一定成功;这个函数负责把内部通用格式变回外部好用的具体类型。

调用图:被 3 处调用(get, get_or_init, get);外部调用 1 个(unreachable!)。

ext/extension-api/src/user_instructions.rs源码 ↗
data_modelstartup

这个文件像一张交接单,规定用户指令从外部系统交到运行时手里时要带哪些信息。UserInstructions 里有真正给模型看的文字 text,也有 source,也就是这段文字来自哪个绝对文件路径,方便之后在 instructionSources 里说明来源。LoadedUserInstructions 表示一次加载的结果:可能真的拿到了指令,也可能没有;同时还能带上一些 warning,也就是“还能继续运行、但启动时应该告诉用户的小问题”。LoadUserInstructionsFuture 表示异步加载任务,意思是加载可能要等一下,不一定立刻完成。UserInstructionsProvider 是一个接口,谁想提供用户指令,就实现它的 load_user_instructions 方法。这样运行时启动根线程时,不用关心指令来自哪里,只要按这个接口取一份快照即可。

ext/goal/src/events.rs源码 ↗
io_transport目标更新时 / 事件发送阶段

这个文件定义了一个小工具 GoalEventEmitter,用来发送目标相关的事件。这里的“事件”可以理解成系统内部的一张通知单:某个线程的目标变了,就把线程编号、新目标、可选的回合编号一起写进通知单,再交给 ExtensionEventSink。ExtensionEventSink 是扩展系统提供的“事件出口”,也就是负责把通知真正送出去的地方。这个文件不判断目标对不对,也不计算进度,它只做一件事:把已经发生的目标更新,按协议要求包成 Event 和 EventMsg::ThreadGoalUpdated,然后发出去。这样做的好处是,别的代码只要调用一个简单方法,不用关心事件格式的细节,也不容易漏字段。

函数细节2
GoalEventEmitter::new15–17 ↗
fn new(sink: Arc<dyn ExtensionEventSink>) -> Self

作用:创建一个 GoalEventEmitter,把外部给的事件出口保存起来,后面就能通过它发送目标更新通知。

数据流:进去的是一个共享的事件出口 sink,也就是能把事件送到扩展系统外面的通道。函数把它放进 GoalEventEmitter 这个小对象里。出来的是一个新的发送器对象,之后调用它的方法就会使用同一个出口发消息。

调用关系:它会被 new_with_host_capabilities 调用,通常是在扩展或能力初始化时准备好“发事件的人”。它本身不发送事件,只是把后续发送事件所需的通道先装好。

调用图:被 1 处调用(new_with_host_capabilities)。

GoalEventEmitter::thread_goal_updated19–33 ↗
fn thread_goal_updated(
        &self,
        event_id: impl Into<String>,
        turn_id: Option<String>,
        goal: ThreadGoal,
    )

作用:把“某个线程的目标更新了”这件事发成一条标准事件,让系统其他部分或外部宿主能收到通知。

数据流:进去的是事件编号 event_id、可选的回合编号 turn_id,以及新的目标 goal。函数先把 event_id 转成字符串,再从 goal 里取出 thread_id,连同 turn_id 和完整的 goal 一起装进 ThreadGoalUpdatedEvent。然后它再把这个更新事件包进 EventMsg::ThreadGoalUpdated,最后交给 sink.emit 发出去。结果是没有返回值,但外部事件通道会收到一条目标更新通知。

调用关系:它会在 account_active_goal_progress 和 emit_goal_updated_from_tool_call 这些地方被调用,也就是当目标进度变化或工具调用导致目标更新时使用。它把上层代码传来的目标变化,交给协议层的 ThreadGoalUpdatedEvent 包装,再交给事件出口 sink.emit 真正发走。

调用图:被 2 处调用(account_active_goal_progress, emit_goal_updated_from_tool_call);外部调用 2 个(into, ThreadGoalUpdated)。

相邻扩展侧 schema

这些文件定义其他稳定的扩展消费边界,用于记忆存储和 IDE 上下文负载。

ext/memories/src/backend.rs源码 ↗
data_modelcross-cutting

这个文件像是在给记忆系统写“菜单”和“点单格式”。上层工具不需要知道记忆是存在本地文件夹里,还是以后换成远程服务,只要调用 MemoriesBackend 这套接口就行。文件里定义了几类请求和回应:比如新增临时笔记要给文件名和内容,列目录会返回条目列表,读文件会返回从哪一行开始的内容,搜索会返回命中的片段。它还定义了搜索方式,比如“任意关键词命中”“同一行都命中”“几行范围内都命中”。最后,MemoriesBackendError 把常见错误说清楚,比如路径不合法、游标不合法、文件不存在、读到的不是文件等。这样做的好处是:调用者拿到结果或错误时,能用统一方式理解发生了什么。

函数细节3
MemoriesBackendError::invalid_filename166–171 ↗
fn invalid_filename(filename: impl Into<String>, reason: impl Into<String>) -> Self

作用:这个函数用来快速生成“文件名不合法”的错误。调用者只要给出出问题的文件名和原因,就能得到一个格式统一的错误对象。

数据流:进去的是一个文件名和一段原因说明;函数把它们转换成真正的字符串;出来的是 MemoriesBackendError::InvalidFilename 这个错误,里面带着原文件名和为什么不合规。

调用关系:它通常在 validate_filename 检查文件名失败时被调用。它自己只做一件小事:借助 into 这种通用转换,把传进来的文本变成可保存的字符串,然后交还给上层错误处理流程。

调用图:被 1 处调用(validate_filename);外部调用 1 个(into)。

MemoriesBackendError::invalid_path173–178 ↗
fn invalid_path(path: impl Into<String>, reason: impl Into<String>) -> Self

作用:这个函数用来生成“路径不合法”的错误。路径就是记忆库里某个文件或文件夹的位置;如果它越界、格式不对,或者不符合存储规则,就会用这个错误说明原因。

数据流:进去的是路径和原因;函数把两者转成字符串;出来的是 MemoriesBackendError::InvalidPath,表示这个路径不能安全或正确地使用。

调用关系:它会被 resolve_scoped_path、ensure_directory、reject_symlink 这些检查路径安全性的步骤调用。也就是说,系统在解析路径、确认目录、拒绝符号链接(一种会跳到别处的特殊路径)时发现问题,就用它包装成统一错误。

调用图:被 3 处调用(resolve_scoped_path, ensure_directory, reject_symlink);外部调用 1 个(into)。

MemoriesBackendError::invalid_cursor180–185 ↗
fn invalid_cursor(cursor: impl Into<String>, reason: impl Into<String>) -> Self

作用:这个函数用来生成“游标不合法”的错误。游标可以理解成翻页用的小票据,用来告诉系统下一页从哪里继续;如果票据坏了或不认识,就会报这个错。

数据流:进去的是游标文本和错误原因;函数把它们转成字符串;出来的是 MemoriesBackendError::InvalidCursor,表示这次分页继续不了,因为传来的游标有问题。

调用关系:它会在 list 和 search 这类需要分页的操作里使用。列目录或搜索结果太多时,系统会用游标分批返回;如果下一次请求带来的游标解析失败,就由这个函数生成统一的错误。

调用图:被 2 处调用(list, search);外部调用 1 个(into)。

tui/src/ide_context.rs源码 ↗
data_modelrequest handling / cross-cutting

这个文件像是一张“编辑器现场情况表”的格式说明。TUI 支持 /ide 时,需要从 IDE(集成开发环境,也就是 VS Code 这类写代码的软件)拿到一些信息:当前正在看的文件、文件路径、选中的行列范围、已经打开的标签页等。这里的 IdeContextActiveFileFileDescriptorRangePosition 就是在规定这些信息该怎么装进程序里。它还用 serde(一个把 JSON 等外部数据转成 Rust 结构的工具)来自动读取外部传来的 camelCase 字段名。一个重要细节是:IDE 传来的数据可能比这里需要的更多,比如 fsPathprocessEnv,这个文件只取自己关心的部分,多余字段会被忽略,避免因为外部数据变多就出错。同时,它把真正负责“去 IDE 取信息”和“把信息塞进用户提示词”的函数从子模块转发出来,方便别的地方统一引用。

函数细节1
tests::deserializes_existing_ide_context_shape61–116 ↗
fn deserializes_existing_ide_context_shape()

作用:这个测试确认:现有 IDE 发来的 JSON 形状,能被正确读成本文件定义的 IdeContext。它也顺便证明,多出来但当前不用的字段不会把解析过程弄坏。

数据流:进去的是一段模拟 IDE 传来的 JSON,里面有当前文件、选区、打开的标签页,还有一些额外字段;测试用 serde_json::from_value 把它转换成 IdeContext;出来的结果会和手写的预期结构比较,确认只保留了需要的字段,并且每个字段都放到了正确位置。

调用关系:这个测试位于本文件内部,只在测试时运行。它调用外部的 json! 来造一份假数据,调用 from_value 来做 JSON 到 Rust 结构的转换,再用 assert_eq! 检查结果是否完全符合预期;它保护的是 /ide 功能依赖的数据格式,防止以后改结构时不小心破坏兼容性。

调用图:外部调用 3 个(assert_eq!, json!, from_value)。

Hook 公共契约

这些文件建立公共 hook 子系统接口,包括执行抽象、声明和事件命名空间组织。

hooks/src/declarations.rs源码 ↗
domain_logicconfig load / startup

插件可以声明 hook,也就是“某个事件发生时要自动执行的小动作”,比如工具调用前、会话开始时。这个文件做的事很像给这些小动作做目录卡片:每个处理器都会得到一个固定的 key,里面包含插件 ID、hook 文件路径、事件名、分组编号和处理器编号。这样即使之后要保存、展示或对照这些 hook,也不会因为运行时状态变化而认不出来。PluginHookDeclaration 是这张目录卡片的数据形状;plugin_hook_declarations 会遍历所有插件来源,把里面按事件和分组组织的 hook 一个个摊平,变成简单列表;plugin_hook_key_source 则负责先拼出 key 的前半段。

函数细节3
plugin_hook_declarations12–33 ↗
fn plugin_hook_declarations(hook_sources: &[PluginHookSource]) -> Vec<PluginHookDeclaration>

作用:把一批插件 hook 来源转换成简单明了的声明列表。别人用它时,不是要真正执行 hook,而是想知道“有哪些 hook、每个 hook 的稳定名字是什么、它属于哪个事件”。

数据流:进去的是多个 PluginHookSource,也就是插件提供的 hook 配置来源。函数先为每个来源拼出 key 的基础部分,再把配置里按事件、匹配分组、处理器位置排列的内容逐层展开;每遇到一个处理器,就生成一个 PluginHookDeclaration。出来的是一个列表,列表里每项都有 key 和 event_name,不会改动原始插件来源。

调用关系:它是这个文件的主入口。测试 tests::lists_declared_plugin_handlers_with_persisted_hook_keys 会用它验证生成的 key 是否稳定。它内部会先交给 plugin_hook_key_source 拼出“插件加文件路径”的部分,再调用外部的 hook_key 把事件名、分组编号和处理器编号补进去,形成最终 key。

调用图:调用 1 个内部函数(plugin_hook_key_source);被 1 处调用(lists_declared_plugin_handlers_with_persisted_hook_keys);外部调用 2 个(new, hook_key)。

plugin_hook_key_source35–37 ↗
fn plugin_hook_key_source(plugin_id: &str, source_relative_path: &str) -> String

作用:把插件 ID 和插件内 hook 文件的相对路径拼成一个 key 的基础来源。这个基础来源后面还会继续加事件名和编号,变成某个具体 hook 处理器的完整编号。

数据流:进去的是 plugin_id 和 source_relative_path 两段文字。函数用冒号把它们连起来,比如“demo@test:hooks/hooks.json”。出来的是这段拼好的字符串;它不读取别的状态,也不修改任何东西。

调用关系:plugin_hook_declarations 会用它来给每个插件来源先准备 key 的前缀。调用图里还显示 append_plugin_hook_sources 也会用它,说明别的地方在追加插件 hook 来源时,也需要用同一套规则生成稳定标识,避免不同流程拼出来的名字不一致。

调用图:被 2 处调用(plugin_hook_declarations, append_plugin_hook_sources);外部调用 1 个(format!)。

tests::lists_declared_plugin_handlers_with_persisted_hook_keys52–100 ↗
fn lists_declared_plugin_handlers_with_persisted_hook_keys()

作用:这是一个测试,用一个假的插件配置检查声明列表是否按预期生成。它重点确认:不同事件、不同处理器会得到固定且可保存的 key。

数据流:进去的是测试自己临时构造的一份插件 hook 配置:里面有一个 pre_tool_use 事件的两个处理器,还有一个 session_start 事件的一个处理器。测试把这份配置交给 plugin_hook_declarations,然后拿返回的声明列表和手写的期望列表比较。结果是如果两边完全一样,测试通过;如果 key 或事件名有差错,测试失败。

调用关系:它只在测试时运行,用来保护 plugin_hook_declarations 的行为别被后来改坏。它会准备测试路径、解析插件 ID、构造默认配置,再调用 plugin_hook_declarations,最后用断言比较结果。

调用图:调用 2 个内部函数(plugin_hook_declarations, parse);外部调用 4 个(default, assert_eq!, test_path_buf, vec!)。

hooks/src/events/mod.rs源码 ↗
othercross-cutting

这个文件本身不写具体功能,而是把 hooks 系统里的各种事件按名字挂出来。可以把它想成商场楼层指示牌:这里有通用事件代码 common,有压缩版事件 compact,也有工具使用前后、权限请求、会话开始、停止、用户提交提示词等事件模块。pub mod 表示这个模块可以被外面访问;pub(crate) mod common 表示 common 只在当前 crate(可以理解为当前 Rust 包)内部使用,不对更外层开放。它的重要性在于统一入口:别的代码只要从 events 这一层找,就能找到对应事件,而不用关心每个文件具体放在哪里。

hooks/src/types.rs源码 ↗
data_modelcross-cutting;在注册钩子、触发钩子、序列化钩子事件和测试格式稳定性时使用

钩子可以理解成“某件事发生后自动按一下的按钮”。这个文件先规定按钮函数 HookFn 的长相:给它一份 HookPayload,它异步返回 HookResult。HookResult 把结果分成三种:成功、失败但继续、失败并中止,这样上层能清楚知道后面的工作还要不要做。Hook 保存钩子的名字和真正要执行的函数,execute 会运行它并把名字和结果打包成 HookResponse。HookPayload 是传给钩子的事件包,里面有会话、当前目录、客户端、触发时间和具体事件。这里还特别规定时间序列化成稳定的 RFC3339 字符串,例如 2025-01-01T00:00:00Z,方便不同进程或不同版本之间可靠交换数据。最后的测试确保这份对外 JSON 格式不会无意间变掉。

函数细节5
HookResult::should_abort_operation27–29 ↗
fn should_abort_operation(&self) -> bool

作用:判断一次钩子失败后,整个操作是不是必须停下来。它把“失败但继续”和“失败并中止”这两种情况清楚地区分开。

数据流:进去的是一个 HookResult。函数只看它是不是 FailedAbort;如果是,就返回 true,表示后续操作应停止;否则返回 false,不改动任何数据。

调用关系:它是上层执行钩子流程时的判断开关。钩子执行完得到 HookResult 后,调用方可以用它决定是继续跑后面的钩子和主流程,还是立刻中止。内部用 matches! 这个匹配工具做简单判断。

调用图:外部调用 1 个(matches!)。

Hook::default45–50 ↗
fn default() -> Self

作用:提供一个安全的默认钩子。这个默认钩子名字叫 default,执行时什么也不做,只返回成功,适合需要占位但不想真的挂载逻辑的地方。

数据流:没有输入。函数创建一个 Hook,把 name 设成 default,并用 Arc::new 包住一个可共享的函数;这个函数收到任何 payload 都直接异步返回 HookResult::Success。最后输出这个默认 Hook。

调用关系:当代码需要一个 Hook 的默认值时会走这里。它把真正的执行逻辑交给那个内置的小函数,而这个小函数永远给出成功结果,所以不会阻断任何流程。

调用图:外部调用 1 个(new)。

Hook::execute54–59 ↗
async fn execute(&self, payload: &HookPayload) -> HookResponse

作用:真正运行一个钩子,并把“哪个钩子运行了”和“运行结果是什么”放在一起返回。这样调用方不用自己再去拼名字和结果。

数据流:进去的是当前 Hook 和一份 HookPayload。函数复制钩子的名字,调用 Hook 里保存的 func,把 payload 交给它并等待异步结果。出来的是 HookResponse,里面包含 hook_name 和 result;它不修改 payload。

调用关系:它位于钩子系统的执行点。上层在事件发生、需要触发某个钩子时调用它;它再把实际工作交给 HookFn,也就是注册进来的具体钩子函数,最后把结果整理成统一格式交还给上层。

serialize_triggered_at83–88 ↗
fn serialize_triggered_at(value: &DateTime<Utc>, serializer: S) -> Result<S::Ok, S::Error>

作用:把触发时间转换成稳定、好传输的字符串。它专门服务于 HookPayload 的 JSON 序列化,避免时间格式在不同地方变得不一致。

数据流:进去的是一个 UTC 时间和 serde 的 serializer(序列化器,就是把内存数据写成 JSON 等格式的工具)。函数先用 to_rfc3339_opts 把时间变成秒级 RFC3339 字符串,并使用 Z 表示 UTC;再用 serialize_str 写出去。出来的是序列化成功或失败的结果,不改动原时间。

调用关系:当 HookPayload 被 serde 转成 JSON 或其他文本格式时,这个函数会被自动调用来处理 triggered_at 字段。它把时间格式这件小但重要的事集中到一个地方,保证对外格式稳定。

调用图:外部调用 2 个(to_rfc3339_opts, serialize_str)。

tests::hook_payload_serializes_stable_wire_shape114–151 ↗
fn hook_payload_serializes_stable_wire_shape()

作用:检查 HookPayload 转成 JSON 后的样子是否固定不变。这个测试保护的是对外接口格式,防止以后改代码时不小心把字段名、时间格式或事件结构改坏。

数据流:进去没有外部输入。测试创建 session_id、thread_id、测试路径和固定时间,组装一个 HookPayload;再用 serde_json::to_value 把它转成 JSON 值;同时手写一份期望 JSON。最后用 assert_eq! 比较两者,若不同测试失败。

调用关系:它只在测试阶段运行。它会用 ThreadId::new 造 ID,用 test_path_buf 造测试路径,用 json! 写期望结果,用 to_value 触发真实序列化流程;这样 serialize_triggered_at 和 HookEvent 的 JSON 形状都会被一起验证。

调用图:调用 1 个内部函数(new);外部调用 5 个(assert_eq!, test_path_buf, json!, to_value, vec!)。

hooks/src/lib.rs源码 ↗
orchestrationcross-cutting

可以把这个文件理解成一家商店的前台。真正的货物放在后面的很多房间里,比如配置规则、事件定义、执行引擎、注册表、旧版通知等;这个前台把常用东西统一摆出来,让别的代码不用知道每个东西具体藏在哪个文件里。它列出了系统认识的 hook 事件名,比如工具使用前、权限请求、会话开始、停止等;也单独列出哪些事件的“匹配条件”有意义,因为不是所有事件都需要按工具名或来源来筛选。文件最后两个函数负责把事件名变成适合保存到配置状态里的短标签,并把来源、事件、分组编号、处理器编号拼成一个唯一的 key。没有这些统一出口和稳定 key,外部代码会到处引用内部细节,配置状态也更容易因为名字不一致而混乱。

函数细节2
hook_event_key_label84–97 ↗
fn hook_event_key_label(event_name: HookEventName) -> &'static str

作用:把程序内部使用的 hook 事件枚举,转换成保存状态时用的固定小写标签。有人需要给某类 hook 做持久化记录时,会用它来保证名字统一。

数据流:进去的是一个 HookEventName,也就是“发生了哪种 hook 事件”的内部代号;函数根据不同事件逐一对应到字符串,比如 PreToolUse 变成 pre_tool_use;出来的是一个固定的字符串切片,不改动任何外部数据。

调用关系:它是生成 hook 状态 key 的基础步骤。hook_key 会先调用它拿到事件标签,再把这个标签和来源、编号一起拼成完整 key;其他需要同一套事件命名规则的地方也可以直接依赖它。

hook_key100–110 ↗
fn hook_key(
    key_source: &str,
    event_name: HookEventName,
    group_index: usize,
    handler_index: usize,
) -> String

作用:为某一个被发现的 hook 处理器生成一个可以长期保存的唯一名字。这样系统下次读取配置状态时,能知道说的是哪一个 hook,而不是只知道一类事件。

数据流:进去的是 key_source(key 的来源说明)、event_name(事件类型)、group_index(第几个 hook 分组)和 handler_index(分组里的第几个处理器);它先用 hook_event_key_label 把事件类型变成稳定标签,再用格式化拼接成类似“来源:事件标签:分组号:处理器号”的字符串;出来的是这个完整 key,不修改传入的内容。

调用关系:它位于 hook 配置状态保存和查找的命名环节。上层代码在发现或登记 hook 处理器时会用它做身份证号码;它内部把事件命名这一步交给 hook_event_key_label,然后通过格式化字符串完成最终拼接。

调用图:外部调用 1 个(format!)。

Hook 传输 schema

这些文件定义序列化的 hook 命令结构,以及向引擎公开其生成的 JSON Schemas 的加载器。

hooks/src/schema.rs源码 ↗
data_modelcross-cutting;运行钩子时用于组装/解析数据,开发或测试时用于生成和校验 schema

这个文件像一份“表格模板大全”。程序在工具调用前、工具调用后、请求权限、会话开始、压缩上下文、子代理开始或结束等时刻,会把一包 JSON 数据交给钩子脚本,也会读回钩子脚本返回的 JSON。这里用 Rust 结构体定义每种输入和输出长什么样,并用 JSON Schema(给 JSON 文件用的格式说明书)生成可检查的 schema 文件。它还特别规定了一些字段只能是固定值,比如 hookEventName 必须正好是 PreToolUse 或 Stop;权限模式、压缩触发方式也只能从允许列表里选。另一个重要点是 NullableString,它允许路径这类字段既可以是字符串,也可以是 null。文件末尾的测试会重新生成 schema,并和仓库里保存的标准文件对比,防止接口格式被无意改坏。

函数细节36
NullableString::from_path44–46 ↗
fn from_path(path: Option<PathBuf>) -> Self

作用:把一个“可能存在、也可能不存在”的文件路径,变成可以写进 JSON 的字符串或 null。这样钩子脚本不用猜:没有路径时就是 null。

数据流:进去的是 Option<PathBuf>,也就是“有路径”或“没有路径” → 如果有路径,就把路径按人能看懂的样子转成字符串;如果没有,就保留为空 → 出来的是 NullableString,序列化成 JSON 时会是字符串或 null。

调用关系:很多构造钩子输入的地方会用它,比如工具调用前后、会话开始和运行流程里。它是把程序内部路径安全放进钩子 JSON 的入口小零件。

调用图:被 10 处调用(post_command_input_json, pre_command_input_json, build_command_input, command_input_json, command_input_json, run, run, run, new, subagent_context_fields_serialize_flat_and_omit_when_absent)。

NullableString::from_string48–50 ↗
fn from_string(value: Option<String>) -> Self

作用:把一个“可能为空”的普通字符串包装成 NullableString。常用于本来就已经拿到字符串、但仍然允许没有值的场景。

数据流:进去的是 Option<String> → 函数不改内容,只把它包进 NullableString → 出来后写成 JSON 时,有值就是字符串,没值就是 null。

调用关系:运行流程会在需要传递可选文字内容时调用它,比如最后一条助手消息这类可能不存在的信息。

调用图:被 1 处调用(run)。

NullableString::schema_name54–56 ↗
fn schema_name() -> String

作用:告诉 JSON Schema 生成器,这个自定义类型在 schema 里叫什么名字。名字固定叫 NullableString,方便生成的说明书可读、可引用。

数据流:不需要外部输入 → 直接返回固定名称 → 输出一个字符串 NullableString。

调用关系:这是 JsonSchema 接口的一部分,由 schema 生成工具在需要给 NullableString 命名时使用。

NullableString::json_schema58–63 ↗
fn json_schema(_gen: &mut SchemaGenerator) -> Schema

作用:告诉 JSON Schema 生成器:NullableString 允许两种 JSON 类型,字符串或 null。没有它,生成出来的 schema 可能会误以为这个字段只能是字符串。

数据流:进去的是 schema 生成器参数,但这里不需要读取它 → 创建一个 schema 对象,声明类型可以是 string 或 null → 返回这段 schema。

调用关系:当 schema 生成器遇到 NullableString 字段时会走到这里。它把实际序列化行为和对外文档保持一致。

调用图:外部调用 3 个(default, Object, vec!)。

SubagentCommandInputFields::from73–81 ↗
fn from(value: Option<&SubagentHookContext>) -> Self

作用:把“子代理上下文”里的代理编号和代理类型,提取成钩子输入里可直接使用的两个字段。没有子代理时,就给出空字段。

数据流:进去的是 Option<&SubagentHookContext> → 如果有上下文,就复制 agent_id 和 agent_type;如果没有,就使用默认空值 → 出来的是 SubagentCommandInputFields,里面两个字段都是可选的。

调用关系:工具调用、权限请求、压缩、用户提示等发生在子代理内部时会用它补充身份信息。测试也检查它在有值时会平铺进 JSON、没值时会省略。

调用图:被 7 处调用(post_command_input_json, pre_command_input_json, build_command_input, command_input_json, command_input_json, run, subagent_context_fields_serialize_flat_and_omit_when_absent);外部调用 1 个(default)。

SessionStartCommandInput::new499–516 ↗
fn new(
        session_id: impl Into<String>,
        transcript_path: Option<PathBuf>,
        cwd: impl Into<String>,
        model: impl Into<String>,
        permission_mode: impl Into<String>,

作用:快速创建“会话开始”钩子的输入数据。它帮调用者填好固定事件名 SessionStart,避免每个地方手写时写错。

数据流:进去的是会话编号、可选 transcript 路径、当前目录、模型名、权限模式和启动来源 → 把这些值转成字符串,并把路径交给 NullableString::from_path → 出来的是完整的 SessionStartCommandInput。

调用关系:运行流程在会话启动时会调用它,然后把生成的输入交给对应钩子。它把零散启动信息组装成统一 JSON 结构。

调用图:调用 1 个内部函数(from_path);被 1 处调用(run);外部调用 1 个(into)。

write_schema_fixtures597–683 ↗
fn write_schema_fixtures(schema_root: &Path) -> anyhow::Result<()>

作用:一次性生成所有钩子输入和输出的 JSON Schema 文件。它主要给开发和测试用,用来刷新或验证 schema 夹具文件。

数据流:进去的是 schema 根目录路径 → 先创建一个干净的 generated 子目录,然后逐个为每种钩子输入/输出生成 JSON,并写到固定文件名 → 成功时返回空结果,出错时返回错误。

调用关系:测试 generated_hook_schemas_match_fixtures 会调用它,确认重新生成的 schema 和仓库里的标准文件完全一致。它把 ensure_empty_dir、schema_json 和 write_schema 串成一条生成流水线。

调用图:调用 2 个内部函数(ensure_empty_dir, write_schema);被 1 处调用(generated_hook_schemas_match_fixtures);外部调用 1 个(join)。

write_schema685–688 ↗
fn write_schema(path: &Path, json: Vec<u8>) -> anyhow::Result<()>

作用:把一份已经生成好的 schema JSON 字节写到磁盘文件。它是最小的落盘动作。

数据流:进去的是目标路径和 JSON 字节 → 调用文件系统写入 → 成功返回空结果,失败返回文件写入错误。

调用关系:write_schema_fixtures 反复调用它,把每一种钩子的 schema 保存成单独文件。

调用图:被 1 处调用(write_schema_fixtures);外部调用 1 个(write)。

ensure_empty_dir690–696 ↗
fn ensure_empty_dir(dir: &Path) -> anyhow::Result<()>

作用:准备一个干净的输出目录。它会先删掉旧目录,再重新创建,避免旧 schema 文件残留造成误导。

数据流:进去的是目录路径 → 如果目录已存在就整个删除,然后创建新目录 → 出来是一个空目录,或返回删除/创建失败的错误。

调用关系:write_schema_fixtures 在开始写文件前先调用它,相当于先把工作台清空。

调用图:被 1 处调用(write_schema_fixtures);外部调用 3 个(exists, create_dir_all, remove_dir_all)。

schema_json698–706 ↗
fn schema_json() -> anyhow::Result<Vec<u8>>

作用:把某个 Rust 类型转换成漂亮、稳定的 JSON Schema 字节。漂亮是指有缩进;稳定是指字段排序固定,方便做文件对比。

数据流:进去的是一个类型参数 T,也就是“要为哪种结构生成 schema” → 先生成 RootSchema,再转成 JSON 值,接着排序所有对象字段,最后转成格式化后的字节 → 出来的是可写入文件的 JSON 内容。

调用关系:它位于 schema 生成链路中间:上游选择具体类型,下游把返回字节写到文件。canonicalize_json 专门帮它把 JSON 顺序固定下来。

调用图:调用 1 个内部函数(canonicalize_json);外部调用 2 个(to_value, to_vec_pretty)。

schema_for_type708–718 ↗
fn schema_for_type() -> RootSchema

作用:为指定 Rust 类型生成 JSON Schema 的原始结构。它使用 draft-07 版本,也就是一种常见的 JSON Schema 规范版本。

数据流:进去的是类型参数 T → 配置 schema 生成器,特别关闭“自动把 Option 加 null 类型”的设置 → 输出 RootSchema,供后续转成 JSON。

调用关系:schema_json 会先依靠它拿到 schema 原料,再继续做 JSON 化和排序。

调用图:外部调用 1 个(draft07)。

canonicalize_json720–734 ↗
fn canonicalize_json(value: &Value) -> Value

作用:把 JSON 对象里的键按固定顺序排序。这样同样内容每次生成出来都长得一样,测试比较时不会因为顺序不同而失败。

数据流:进去的是任意 JSON 值 → 如果是数组,就递归处理每一项;如果是对象,就按键名排序后递归处理子值;其他值直接复制 → 出来的是内容相同但顺序稳定的新 JSON 值。

调用关系:schema_json 在写出 schema 前调用它。它像整理文件夹一样,把所有 JSON 字段摆成固定顺序。

调用图:被 1 处调用(schema_json);外部调用 4 个(with_capacity, Array, Object, clone)。

session_start_hook_event_name_schema736–738 ↗
fn session_start_hook_event_name_schema(_gen: &mut SchemaGenerator) -> Schema

作用:生成一个只允许 hookEventName 等于 SessionStart 的 schema。这样会话开始钩子的事件名不能被写成别的值。

数据流:进去的是 schema 生成器参数,但不使用 → 调用 string_const_schema,传入 SessionStart → 出来的是“固定字符串”schema。

调用关系:它被 SessionStart 相关输入/输出结构通过 schemars 使用,底层工作交给 string_const_schema。

调用图:调用 1 个内部函数(string_const_schema)。

post_tool_use_hook_event_name_schema740–742 ↗
fn post_tool_use_hook_event_name_schema(_gen: &mut SchemaGenerator) -> Schema

作用:生成一个只允许事件名等于 PostToolUse 的 schema。它用于工具调用结束后的钩子格式说明。

数据流:进去的是未使用的生成器参数 → 把 PostToolUse 交给 string_const_schema → 输出固定值 schema。

调用关系:PostToolUse 的输入和专属输出字段会用到它,实际造 schema 的通用活儿由 string_const_schema 完成。

调用图:调用 1 个内部函数(string_const_schema)。

pre_compact_hook_event_name_schema744–746 ↗
fn pre_compact_hook_event_name_schema(_gen: &mut SchemaGenerator) -> Schema

作用:生成一个只允许事件名等于 PreCompact 的 schema。它用于压缩上下文之前触发的钩子。

数据流:进去的是未使用的生成器参数 → 传入 PreCompact 创建固定字符串规则 → 输出 schema。

调用关系:PreCompact 输入结构引用它;它再调用 string_const_schema 来避免重复写同样的 schema 代码。

调用图:调用 1 个内部函数(string_const_schema)。

post_compact_hook_event_name_schema748–750 ↗
fn post_compact_hook_event_name_schema(_gen: &mut SchemaGenerator) -> Schema

作用:生成一个只允许事件名等于 PostCompact 的 schema。它用于压缩上下文之后触发的钩子。

数据流:进去的是未使用的生成器参数 → 传入 PostCompact → 出来的是固定字符串 schema。

调用关系:PostCompact 输入结构通过它限制事件名,底层由 string_const_schema 统一实现。

调用图:调用 1 个内部函数(string_const_schema)。

pre_tool_use_hook_event_name_schema752–754 ↗
fn pre_tool_use_hook_event_name_schema(_gen: &mut SchemaGenerator) -> Schema

作用:生成一个只允许事件名等于 PreToolUse 的 schema。它保证工具调用前的钩子不会混成其他事件。

数据流:进去的是未使用的生成器参数 → 传入 PreToolUse → 输出固定字符串 schema。

调用关系:PreToolUse 输入和专属输出字段会使用它,它把具体事件值交给 string_const_schema。

调用图:调用 1 个内部函数(string_const_schema)。

permission_request_hook_event_name_schema756–758 ↗
fn permission_request_hook_event_name_schema(_gen: &mut SchemaGenerator) -> Schema

作用:生成一个只允许事件名等于 PermissionRequest 的 schema。它用于权限请求钩子的输入和输出说明。

数据流:进去的是未使用的生成器参数 → 传入 PermissionRequest → 返回固定字符串 schema。

调用关系:权限请求相关结构会引用它;测试也会确认输出 schema 里的 hookEventName 被限制成这个值。

调用图:调用 1 个内部函数(string_const_schema)。

user_prompt_submit_hook_event_name_schema760–762 ↗
fn user_prompt_submit_hook_event_name_schema(_gen: &mut SchemaGenerator) -> Schema

作用:生成一个只允许事件名等于 UserPromptSubmit 的 schema。它用于用户提交提示词时的钩子。

数据流:进去的是未使用的生成器参数 → 传入 UserPromptSubmit → 输出固定字符串 schema。

调用关系:用户提示提交相关输入/输出结构使用它,并通过 string_const_schema 生成实际规则。

调用图:调用 1 个内部函数(string_const_schema)。

subagent_start_hook_event_name_schema764–766 ↗
fn subagent_start_hook_event_name_schema(_gen: &mut SchemaGenerator) -> Schema

作用:生成一个只允许事件名等于 SubagentStart 的 schema。它用于子代理启动时的钩子。

数据流:进去的是未使用的生成器参数 → 传入 SubagentStart → 返回固定字符串 schema。

调用关系:子代理启动输入和输出结构会用它;它本身只是把固定值交给通用构造函数。

调用图:调用 1 个内部函数(string_const_schema)。

subagent_stop_hook_event_name_schema768–770 ↗
fn subagent_stop_hook_event_name_schema(_gen: &mut SchemaGenerator) -> Schema

作用:生成一个只允许事件名等于 SubagentStop 的 schema。它用于子代理结束时的钩子。

数据流:进去的是未使用的生成器参数 → 传入 SubagentStop → 输出固定字符串 schema。

调用关系:子代理停止输入结构引用它,最终由 string_const_schema 生成统一形状的固定值规则。

调用图:调用 1 个内部函数(string_const_schema)。

stop_hook_event_name_schema772–774 ↗
fn stop_hook_event_name_schema(_gen: &mut SchemaGenerator) -> Schema

作用:生成一个只允许事件名等于 Stop 的 schema。它用于主流程停止时触发的钩子。

数据流:进去的是未使用的生成器参数 → 传入 Stop → 输出固定字符串 schema。

调用关系:Stop 输入结构使用它,确保停止钩子的事件名在 schema 层面就被写死。

调用图:调用 1 个内部函数(string_const_schema)。

permission_mode_schema776–784 ↗
fn permission_mode_schema(_gen: &mut SchemaGenerator) -> Schema

作用:规定 permission_mode 字段只能是几种允许的权限模式。这样外部钩子看到的权限状态不会出现随便编的字符串。

数据流:进去的是未使用的生成器参数 → 把 default、acceptEdits、plan、dontAsk、bypassPermissions 这些允许值交给 string_enum_schema → 输出枚举 schema。

调用关系:多个钩子输入结构都会用它描述权限模式字段。它把“可选名单”的构造交给 string_enum_schema。

调用图:调用 1 个内部函数(string_enum_schema)。

session_start_source_schema786–788 ↗
fn session_start_source_schema(_gen: &mut SchemaGenerator) -> Schema

作用:规定会话开始的 source 字段只能表示几种来源,比如启动、恢复、清空或压缩。这样启动原因不会乱写。

数据流:进去的是未使用的生成器参数 → 把 startup、resume、clear、compact 交给 string_enum_schema → 返回枚举 schema。

调用关系:SessionStartCommandInput 的 source 字段会用它,底层仍由 string_enum_schema 生成规则。

调用图:调用 1 个内部函数(string_enum_schema)。

compaction_trigger_schema790–792 ↗
fn compaction_trigger_schema(_gen: &mut SchemaGenerator) -> Schema

作用:规定压缩触发原因只能是 manual 或 auto。也就是手动触发或自动触发,不接受其他写法。

数据流:进去的是未使用的生成器参数 → 把 manual 和 auto 交给 string_enum_schema → 输出枚举 schema。

调用关系:PreCompact 和 PostCompact 输入结构使用它,统一说明 trigger 字段的合法值。

调用图:调用 1 个内部函数(string_enum_schema)。

string_const_schema794–801 ↗
fn string_const_schema(value: &str) -> Schema

作用:生成“这个字符串必须等于某个固定值”的 schema。它是所有事件名限制的通用小工具。

数据流:进去的是固定字符串值 → 创建一个类型为 string 的 schema 对象,并设置 const 为这个值 → 返回 Schema 对象。

调用关系:所有 hook_event_name_schema 函数都会调用它。它把重复的固定值 schema 写法集中在一个地方,减少出错。

调用图:被 10 处调用(permission_request_hook_event_name_schema, post_compact_hook_event_name_schema, post_tool_use_hook_event_name_schema, pre_compact_hook_event_name_schema, pre_tool_use_hook_event_name_schema, session_start_hook_event_name_schema, stop_hook_event_name_schema, subagent_start_hook_event_name_schema, subagent_stop_hook_event_name_schema, user_prompt_submit_hook_event_name_schema);外部调用 3 个(default, Object, String)。

string_enum_schema803–815 ↗
fn string_enum_schema(values: &[&str]) -> Schema

作用:生成“这个字符串只能从一组选项里挑一个”的 schema。它用于权限模式、会话来源、压缩触发原因这类字段。

数据流:进去的是字符串切片列表 → 创建 string 类型 schema,并把每个允许值转成 JSON 字符串放进 enum_values → 返回 Schema 对象。

调用关系:permission_mode_schema、session_start_source_schema 和 compaction_trigger_schema 都调用它,用同一套方法表达允许列表。

调用图:被 3 处调用(compaction_trigger_schema, permission_mode_schema, session_start_source_schema);外部调用 2 个(default, Object)。

default_continue817–819 ↗
fn default_continue() -> bool

作用:给钩子输出里的 continue 字段提供默认值 true。也就是说,如果钩子没特别说要停,系统默认继续跑。

数据流:不需要输入 → 直接返回 true → 反序列化 HookUniversalOutputWire 时,如果 continue 缺失,就用这个结果补上。

调用关系:它通过 serde 的默认值机制被 HookUniversalOutputWire 使用,是钩子返回值解析时的安全默认行为。

tests::expected_fixture869–933 ↗
fn expected_fixture(name: &str) -> &'static str

作用:在测试里按文件名取出仓库中已经保存好的标准 schema 文本。它相当于测试用的“标准答案查询表”。

数据流:进去的是夹具文件名 → 根据文件名匹配到对应 include_str! 嵌入的 schema 文件内容;如果名字不认识就直接 panic → 输出标准 schema 字符串。

调用关系:generated_hook_schemas_match_fixtures 会调用它拿标准答案,再和刚生成的文件内容比较。

调用图:外部调用 2 个(include_str!, panic!)。

tests::normalize_newlines935–937 ↗
fn normalize_newlines(value: &str) -> String

作用:把 Windows 风格换行改成统一的 Unix 风格换行。这样测试不会因为操作系统换行差异而失败。

数据流:进去的是字符串 → 把所有 \r\n 替换成 \n → 输出换行统一后的新字符串。

调用关系:generated_hook_schemas_match_fixtures 在比较标准文件和生成文件前会调用它,先消除无关差异。

tests::assert_output_hook_event_name_const939–951 ↗
fn assert_output_hook_event_name_const(definition: &str, expected: &str)

作用:测试某个输出 schema 里的 hookEventName 是否被限制成指定固定值。它防止输出格式把事件名写得太宽松。

数据流:进去的是定义名和期望事件名,以及类型参数 T → 为 T 生成 schema 并解析成 JSON → 找到对应 definition 的 hookEventName 属性,断言它等于包含 const 和 string 类型的预期 JSON。

调用关系:hook_specific_output_event_names_are_event_specific_in_output_schemas 会多次调用它,逐个检查不同钩子的专属输出。

调用图:外部调用 2 个(assert_eq!, from_slice)。

tests::generated_hook_schemas_match_fixtures954–987 ↗
fn generated_hook_schemas_match_fixtures()

作用:确认当前代码生成的所有 schema,和仓库里保存的 schema 文件完全一致。它能及时发现接口格式被改动。

数据流:进去没有参数 → 创建临时目录,调用 write_schema_fixtures 生成 schema;然后逐个读取生成文件和标准文件,统一换行后比较 → 如果任何一个不同,测试失败。

调用关系:这是 schema 稳定性的总检查。它把 write_schema_fixtures、expected_fixture 和 normalize_newlines 串起来,模拟一次完整生成流程。

调用图:调用 1 个内部函数(write_schema_fixtures);外部调用 5 个(new, assert_eq!, expected_fixture, normalize_newlines, read_to_string)。

tests::hook_specific_output_event_names_are_event_specific_in_output_schemas990–1015 ↗
fn hook_specific_output_event_names_are_event_specific_in_output_schemas()

作用:检查每种“钩子专属输出”里的事件名都是对应事件的固定值。这样 PermissionRequest 的输出不会被 schema 允许写成 PreToolUse。

数据流:进去没有参数 → 对多个输出类型分别调用断言辅助函数,传入各自定义名和期望事件名 → 如果某个 schema 没有限死正确事件名,测试失败。

调用关系:它依赖 tests::assert_output_hook_event_name_const 做实际检查,是一组事件名约束的集中测试。

tests::turn_scoped_hook_inputs_include_codex_turn_id_extension1018–1082 ↗
fn turn_scoped_hook_inputs_include_codex_turn_id_extension()

作用:确认那些和某一轮对话相关的钩子输入都包含 turn_id 字段。turn_id 可以理解为“当前这一轮对话的编号”。

数据流:进去没有参数 → 为多种输入类型生成并解析 schema → 检查 properties 里 turn_id 是字符串,并且 required 必填列表里包含 turn_id → 不满足就测试失败。

调用关系:它保护 Codex 自己扩展出来的 turn_id 字段,防止以后改 schema 时不小心删掉。

调用图:外部调用 3 个(assert!, assert_eq!, from_slice)。

tests::subagent_context_fields_are_optional_for_hooks_that_run_inside_subagents1085–1107 ↗
fn subagent_context_fields_are_optional_for_hooks_that_run_inside_subagents()

作用:确认运行在子代理里的某些钩子,agent_id 和 agent_type 字段是可选的。也就是说主代理场景不会被迫提供子代理信息。

数据流:进去没有参数 → 为多种输入类型生成 schema 并解析 → 检查 agent_id 和 agent_type 的类型是字符串,但它们不在 required 必填列表里 → 如果变成必填,测试失败。

调用关系:它守住子代理上下文字段的兼容性,让同一类钩子既能在主流程用,也能在子代理内部用。

调用图:外部调用 3 个(assert!, assert_eq!, from_slice)。

tests::subagent_context_fields_serialize_flat_and_omit_when_absent1110–1165 ↗
fn subagent_context_fields_serialize_flat_and_omit_when_absent()

作用:检查子代理字段序列化时的两个细节:有子代理时字段直接平铺在 JSON 顶层;没有子代理时字段完全省略。

数据流:进去没有参数 → 先用 SubagentCommandInputFields::from 构造有子代理的输入,序列化后断言 JSON 包含 agent_id 和 agent_type;再构造无子代理的输入,序列化后断言这两个键不存在 → 不符合就测试失败。

调用关系:它同时覆盖 NullableString::from_path 和 SubagentCommandInputFields::from 的实际组合效果,确保钩子输入 JSON 长得符合外部脚本预期。

调用图:调用 2 个内部函数(from_path, from);外部调用 3 个(assert_eq!, json!, to_value)。

hooks/src/engine/schema_loader.rs源码 ↗
configstartup / cross-cutting

这个文件解决的是“钩子命令的数据格式要一致”的问题。钩子可以理解成系统在某些关键时刻自动触发的小程序,比如工具使用前后、会话开始、用户提交提示词等。每种钩子都有输入和输出,必须长得符合规定,否则后面的代码就可能看不懂。这里用 JSON Schema(JSON 数据的格式说明书)来保存这些规定。文件里的 GeneratedHookSchemas 像一个总目录,列出所有钩子的输入、输出 schema。generated_hook_schemas 会在第一次有人需要时,把编译进程序里的 schema 文本解析成 serde_json::Value(通用 JSON 值),之后就复用同一份,避免反复解析。OnceLock 是“一次性保险柜”:第一次装进去,以后所有人都拿同一份。parse_json_schema 负责把文本变成 JSON;如果生成出来的 schema 本身坏了,程序会直接报错,尽早暴露问题。

函数细节3
generated_hook_schemas29–113 ↗
fn generated_hook_schemas() -> &'static GeneratedHookSchemas

作用:一次性准备好所有生成出来的钩子 schema,并把它们作为全局共享的只读资料返回。别人需要校验钩子输入输出格式时,就从这里拿。

数据流:进去没有普通参数,但它会读取那些通过 include_str! 编进程序里的 schema JSON 文本。第一次调用时,它创建一个 GeneratedHookSchemas,把每个 schema 文本解析成 JSON 值;之后再调用时,不重新解析,直接返回之前存好的同一份静态引用。

调用关系:它是这个文件对外最重要的入口。运行时代码里的 new 会用到它来拿 schema,测试函数 tests::loads_generated_hook_schemas 也会调用它确认这些 schema 能正常加载。它内部依靠 OnceLock::new 这类一次性初始化工具,保证全程序只初始化一回。

调用图:被 2 处调用(new, loads_generated_hook_schemas);外部调用 1 个(new)。

parse_json_schema115–118 ↗
fn parse_json_schema(name: &str, schema: &str) -> Value

作用:把一段 schema 文本解析成真正可操作的 JSON 数据。它还会给错误加上 schema 名字,这样一旦某份生成文件坏了,排查时知道是哪一份。

数据流:进去的是 schema 的名字和 schema 的字符串内容。它调用 serde_json::from_str,把字符串按 JSON 规则解析;成功就输出 serde_json::Value,失败就立刻 panic,也就是让程序停止并报出“哪份生成 schema 无效”和具体错误。

调用关系:它是 generated_hook_schemas 里面的“小翻译员”:总加载函数负责收集所有 schema,而它负责逐个把文本翻译成 JSON 值。它把真正的解析工作交给外部库 serde_json::from_str。

调用图:外部调用 1 个(from_str)。

tests::loads_generated_hook_schemas126–149 ↗
fn loads_generated_hook_schemas()

作用:这是一个测试,确认所有生成的钩子 schema 都能被加载,并且顶层类型都是 object,也就是 JSON 对象。它用来防止生成文件损坏却没人发现。

数据流:进去没有参数。它调用 generated_hook_schemas 拿到全部 schema,然后逐个检查每份 schema 里的 type 字段是不是 object;如果有任何一份不是,assert_eq! 会让测试失败。

调用关系:它只在测试时运行。它站在使用者角度调用 generated_hook_schemas,等于做一次健康检查:加载器能不能工作、每份 schema 的基本形状对不对。具体比较动作交给 assert_eq! 这个测试断言宏完成。

调用图:调用 1 个内部函数(generated_hook_schemas);外部调用 1 个(assert_eq!)。