多智能体、协作与后台流程
这个阶段像主会话旁边的“调度室”,不是自己回答完就结束,而是在主循环中帮忙开小助手、派活、传话、等待和收尾。登记表记录有哪些代理在跑,闸门和停车场限制数量,避免资源被占满。创建、恢复、关闭、打断、发消息、等待等工具让主代理能像带团队一样协作。还有审查助手、CSV 批量派工、记忆后台整理、技能文件监视等流程,把结果再汇回主会话。
本阶段涉及的状态11
reg-agent-graph多智能体和 fork 线程之间谁带起谁、父子关系是否还活着的关系图。reg-skills-catalog当前能用的技能目录,包括技能来自哪里、说明文件在哪里、是否允许自动调用。reg-memory-store系统保存和检索长期记忆的地方,包含记忆文件、目录、搜索结果和后台整理状态。reg-background-job-queue后台刷新云配置、同步目录、整理记忆、清理连接、检查更新等异步任务的队列和运行状态。reg-cloud-task-state云端任务、任务列表、默认环境、尝试次数、状态和结果在本地界面与后端之间同步的那份状态。reg-agent-orchestration-runtime多智能体运行时的活跃代理登记、并发闸门、等待区、代理间消息和等待结果状态。reg-filesystem-watch-state文件系统监听订阅、被监控路径、最近快照、变更去抖和待通知事件的共享状态。reg-review-workflow-state代码审查请求的目标、审查要求、辅助审查代理进度、发现项和最终审查结果状态。reg-async-runtime-shutdown全局异步运行时、任务执行句柄、取消信号和关机协调状态,供服务器、后台任务、工具执行和清理流程共用。reg-batch-dispatch-stateCSV 等批量派工流程中的输入批次、逐项代理分配、执行进度、失败项和汇总结果状态。reg-extension-lifecycle-state已加载扩展在账户、线程、回合和工具生命周期中注册的贡献、回调上下文和运行中结果状态。
Agent 控制基础
这些文件定义核心 agent 子系统,包括角色应用、注册表记录、目标解析、生命周期编排、执行限制、驻留管理,以及会话可见的生命周期消息。
core/src/agent/mod.rs源码 ↗
可以把这个文件理解成一个办公室门口的指示牌:真正干活的人不在这里,而是在 agent_resolver、control、registry、role、status 这些房间里。这个文件告诉 Rust 编译器(也就是把代码变成程序的工具)这些房间存在,并决定哪些房间或工具能被同一个项目里的其他地方使用。它还把几个常用东西重新导出,比如 AgentControl、AgentStatus,以及和“线程生成深度”有关的检查函数。这样别的代码不用知道这些东西具体藏在哪个子文件里,只要从 agent 这个入口拿就行。没有它,agent 相关代码会更分散,引用路径也会更乱,维护起来更容易出错。
core/src/agent/role.rs源码 ↗
可以把这个文件想成“给新员工发工牌和岗位说明”的地方。系统要新开一个代理时,调用者可能会指定角色名;如果没指定,就用 default。这个文件先从用户配置和内置配置里找到这个角色,再读取角色对应的配置文件,把它当成一层优先级很高的配置盖到原有配置上。这里的“配置层”就像几张透明纸叠在一起,越上面的字越算数。它还特别小心:如果角色配置没有明确指定 model_provider(模型提供方)或 service_tier(服务档位),就保留当前会话已经选好的值,避免子代理悄悄退回默认值。文件里还会生成 spawn-agent 工具能展示给模型看的角色说明,并保存几种内置角色,比如 default、explorer、worker。
apply_role_to_config38–54 ↗
async fn apply_role_to_config(
config: &mut Config,
role_name: Option<&str>,
) -> Result<(), String>
作用:这是给外部调用的主入口:把指定的代理角色应用到当前配置上。如果没有传角色名,它会自动用 default;如果角色不存在或配置坏了,就返回给调用方一个能理解的错误。
数据流:进去的是一份可修改的 Config 和一个可选角色名。它先决定实际角色名,再去找对应的角色声明;找到后交给 apply_role_to_config_inner 真正加载和叠加配置。成功时,传进来的 Config 会被改成带角色效果的新配置;失败时输出错误文字。
调用关系:它会被 handle_spawn_agent 在创建子代理时调用,也会被测试用来确认角色覆盖生效。它自己不读文件、不重建配置,而是先调用 resolve_role_config 找角色,再把后续工作交给 apply_role_to_config_inner。
调用图:调用 2 个内部函数(apply_role_to_config_inner, resolve_role_config);被 3 处调用(new_default_turn_uses_config_aware_skills_for_role_overrides, handle_spawn_agent, handle_spawn_agent)。
apply_role_to_config_inner56–83 ↗
async fn apply_role_to_config_inner(
config: &mut Config,
role_name: &str,
role: &AgentRoleConfig,
) -> anyhow::Result<()>
作用:这个函数做真正的“套角色配置”。它会判断角色有没有配置文件;有的话就读取配置层,并决定哪些当前设置需要保留下来。
数据流:进去的是当前 Config、角色名和角色声明。它先看这个角色是不是用户自定义的,再读取角色配置文件;如果配置是空的,就什么都不改。否则它检查角色是否写了 model_provider 和 service_tier;没写就标记为需要保留当前值,最后重建出新的 Config 并覆盖原配置。
调用关系:它由 apply_role_to_config 调用。它把读配置文件的事交给 load_role_layer_toml,把重新合成完整配置的事交给 reload::build_next_config。
调用图:调用 1 个内部函数(load_role_layer_toml);被 1 处调用(apply_role_to_config);外部调用 1 个(build_next_config)。
load_role_layer_toml85–117 ↗
async fn load_role_layer_toml(
config: &Config,
config_file: &Path,
is_built_in: bool,
role_name: &str,
) -> anyhow::Result<TomlValue>
作用:这个函数负责把某个角色的配置文件读成系统能合并的 TOML 配置数据。TOML 是一种常见配置文件格式,长得像 key = value。
数据流:进去的是当前 Config、配置文件路径、是否内置角色、角色名。内置角色会从程序打包进去的文本里取配置;用户角色会从磁盘读取文件,并按代理角色文件的规则解析。解析后它会先验证配置能不能被系统理解,再把相对路径转换成明确路径,最后输出一份 TOML 数据。
调用关系:它只在 apply_role_to_config_inner 需要角色配置文件时运行。它会调用内置配置查找、磁盘读取、角色文件解析、配置反序列化和相对路径修正这些底层工具。
调用图:调用 3 个内部函数(resolve_relative_paths_in_config_toml, parse_agent_role_file_contents, deserialize_config_toml_with_base);被 1 处调用(apply_role_to_config_inner);外部调用 5 个(parent, anyhow!, config_file_contents, read_to_string, from_str)。
resolve_role_config119–127 ↗
fn resolve_role_config(
config: &'a Config,
role_name: &str,
) -> Option<&'a AgentRoleConfig>
作用:这个函数按名字查找角色声明。它先找用户自己配置的角色,找不到再找程序内置角色。
数据流:进去的是完整 Config 和角色名。它查看 config.agent_roles 里的用户角色;如果没有,就查看 built_in::configs 提供的内置角色表。出来的是一个角色声明的引用,或者没有找到时返回空。
调用关系:它被 apply_role_to_config 用在流程最前面,决定后面到底有没有角色可应用。因为用户角色优先,所以用户可以用同名角色覆盖内置定义。
调用图:被 1 处调用(apply_role_to_config)。
reload::build_next_config132–154 ↗
async fn build_next_config(
config: &Config,
role_layer_toml: TomlValue,
preserve_current_provider: bool,
preserve_current_service_tier: bool,
) -> anyhow::Result<C
作用:这个函数把“旧配置 + 角色配置层 + 需要保留的运行时选择”合成一份新的完整配置。
数据流:进去的是当前 Config、角色 TOML 配置层,以及两个布尔标记:是否保留当前模型提供方、是否保留当前服务档位。它先构造新的配置层栈,再算出合并后的有效配置,最后调用配置加载器重新生成 Config。出来的是一份新的 Config。
调用关系:它是 apply_role_to_config_inner 的重建配置步骤。它内部依次使用 build_config_layer_stack、deserialize_effective_config 和 reload_overrides,然后把最终加载工作交给 Config::load_config_with_layer_stack。
调用图:外部调用 4 个(load_config_with_layer_stack, build_config_layer_stack, deserialize_effective_config, reload_overrides)。
reload::build_config_layer_stack156–167 ↗
fn build_config_layer_stack(
config: &Config,
role_layer_toml: &TomlValue,
) -> anyhow::Result<ConfigLayerStack>
作用:这个函数负责把角色配置插进原来的配置层列表里,并保持配置层的优先级顺序。
数据流:进去的是当前 Config 和角色配置 TOML。它先复制已有配置层,再把角色配置包装成一层新的 SessionFlags 配置层,然后按层名顺序插入,最后创建新的 ConfigLayerStack。出来的是新的配置层栈。
调用关系:它服务于 reload::build_next_config。它会使用 existing_layers 取出现有层,用 role_layer 包装角色层,再由 insert_layer 放到合适位置。
调用图:调用 1 个内部函数(new);外部调用 4 个(clone, existing_layers, insert_layer, role_layer)。
reload::deserialize_effective_config169–177 ↗
fn deserialize_effective_config(
config: &Config,
config_layer_stack: &ConfigLayerStack,
) -> anyhow::Result<ConfigToml>
作用:这个函数把已经叠好的配置层转换成 ConfigToml,也就是系统加载配置前使用的结构化配置。
数据流:进去的是当前 Config 和新的配置层栈。它从配置层栈中取出合并后的有效 TOML,再以 codex_home 作为路径基准进行反序列化检查。出来的是 ConfigToml。
调用关系:它被 reload::build_next_config 用在重新加载前。它的作用是确认多层配置合起来以后仍然是一份系统认识的配置。
调用图:调用 2 个内部函数(effective_config, deserialize_config_toml_with_base)。
reload::existing_layers179–189 ↗
fn existing_layers(config: &Config) -> Vec<ConfigLayerEntry>
作用:这个函数把当前配置里已经存在的所有配置层复制出来,作为之后插入角色层的基础。
数据流:进去的是当前 Config。它按“低优先级在前,高优先级在后”的顺序取出配置层,并且连被禁用的层也包含进去,然后复制成一个普通列表。出来的是 ConfigLayerEntry 列表。
调用关系:它被 reload::build_config_layer_stack 使用,是重建配置层栈的第一步。没有它,角色层就没法在保留原配置历史的基础上插进去。
reload::insert_layer191–195 ↗
fn insert_layer(layers: &mut Vec<ConfigLayerEntry>, layer: ConfigLayerEntry)
作用:这个函数把一层新配置插入到配置层列表的正确位置,避免破坏原有排序。
数据流:进去的是可修改的配置层列表和要插入的新层。它找到第一个层名大于新层名的位置,把新层插进去。出来没有单独返回值,但传入的列表会被改好。
调用关系:它被 reload::build_config_layer_stack 调用,用来把 role_layer 生成的角色配置层塞进 existing_layers 得到的旧列表里。
reload::role_layer197–199 ↗
fn role_layer(role_layer_toml: TomlValue) -> ConfigLayerEntry
作用:这个函数把角色的 TOML 配置包装成一个正式的配置层,并标记它来自 SessionFlags,也就是会话启动时的高优先级设置。
数据流:进去的是角色 TOML。它创建一个 ConfigLayerEntry,来源写成 ConfigLayerSource::SessionFlags,内容就是这份角色配置。出来的是可插入配置层栈的新配置层。
调用关系:它被 reload::build_config_layer_stack 调用。它让角色配置在系统看来和会话级参数一样有较高优先级。
调用图:调用 1 个内部函数(new)。
reload::reload_overrides201–214 ↗
fn reload_overrides(
config: &Config,
preserve_current_provider: bool,
preserve_current_service_tier: bool,
) -> ConfigOverrides
作用:这个函数准备重新加载配置时必须保留的运行时覆盖项。覆盖项就是“不管配置文件怎么写,这些当前值仍然要算数”的临时选择。
数据流:进去的是当前 Config,以及是否保留模型提供方和服务档位的两个标记。它总是保留当前工作目录和一些可执行文件路径;如果标记允许,也保留当前 model_provider 和 service_tier。出来的是 ConfigOverrides。
调用关系:它被 reload::build_next_config 调用,然后传给 Config::load_config_with_layer_stack。这样重新加载不会把调用者当前的关键运行时选择弄丢。
调用图:外部调用 1 个(default)。
spawn_tool_spec::build221–224 ↗
fn build(user_defined_agent_roles: &BTreeMap<String, AgentRoleConfig>) -> String
作用:这个函数生成 spawn-agent 工具说明里关于“可用角色”的文字。模型看到这段文字后,才知道可以创建哪些类型的代理。
数据流:进去的是用户自定义角色表。它取出内置角色表,再把内置和用户角色一起交给 build_from_configs 排版。出来是一整段可展示的说明文字。
调用关系:它是生成工具描述的对外入口。它不直接格式化每个角色,而是把合并和去重交给 spawn_tool_spec::build_from_configs。
调用图:外部调用 2 个(configs, build_from_configs)。
spawn_tool_spec::build_from_configs227–248 ↗
fn build_from_configs(
built_in_roles: &BTreeMap<String, AgentRoleConfig>,
user_defined_roles: &BTreeMap<String, AgentRoleConfig>,
) -> String
作用:这个函数把用户角色和内置角色合成一份“可用角色列表”,并处理同名去重。
数据流:进去的是内置角色表和用户角色表。它先加入用户角色,再加入未被同名用户角色覆盖的内置角色;每个角色交给 format_role 变成文字。出来是一段包含默认角色提示和所有角色说明的字符串。
调用关系:它由 spawn_tool_spec::build 调用。它把单个角色的描述工作交给 spawn_tool_spec::format_role,并确保用户配置比内置配置更优先。
调用图:外部调用 4 个(new, new, format_role, format!)。
spawn_tool_spec::format_role250–302 ↗
fn format_role(name: &str, declaration: &AgentRoleConfig) -> String
作用:这个函数把一个角色声明变成适合放进工具说明里的文字。如果角色锁定了模型、推理强度或服务档位,它也会把这些限制写出来。
数据流:进去的是角色名和角色声明。它先看有没有 description;有描述就输出名称和描述。若角色带配置文件,它还会尝试读取配置文件,找出 model、model_reasoning_effort、service_tier 这些固定设置,并追加提示。出来的是单个角色的说明文字。
调用关系:它被 spawn_tool_spec::build_from_configs 反复调用。它会优先尝试读取内置配置内容,读不到再尝试从磁盘读取用户配置文件。
调用图:外部调用 1 个(format!)。
built_in::configs309–370 ↗
fn configs() -> &'static BTreeMap<String, AgentRoleConfig>
作用:这个函数提供程序自带的角色声明表,比如 default、explorer、worker。它用缓存保存,避免每次都重新创建。
数据流:进去没有业务输入。第一次调用时,它创建一个按名字排序的角色表;之后再调用就直接返回同一份静态表。出来的是内置角色表的只读引用。
调用关系:它被 resolve_role_config 用来查找内置角色,也被 spawn_tool_spec::build 用来生成可用角色说明。它是内置角色定义的集中来源。
调用图:外部调用 1 个(new)。
built_in::config_file_contents373–381 ↗
fn config_file_contents(path: &Path) -> Option<&'static str>
作用:这个函数把内置角色声明里的配置文件名,解析成程序打包进去的配置文本。
数据流:进去的是一个路径。它把路径转成字符串,然后匹配 explorer.toml 或 awaiter.toml;匹配上就返回编译进程序的文件内容,匹配不上就返回空。它不会读磁盘。
调用关系:它被 load_role_layer_toml 用来加载内置角色配置,也被 spawn_tool_spec::format_role 用来读取内置角色的锁定设置说明。它让内置配置即使没有外部文件也能使用。
调用图:外部调用 2 个(to_str, include_str!)。
core/src/agent/registry.rs源码 ↗
Codex 支持一个会话里派生多个子 agent。问题是:如果不管数量,可能一下开太多线程;如果不管名字和路径,两个 agent 可能重名或占同一个位置。这个文件用 AgentRegistry 统一记账。它里面有互斥锁 Mutex(一把锁,防止多个任务同时改同一张表)保护当前活跃 agent 的信息,也有 AtomicUsize(可以被多个线程安全加减的数字)记录总数。创建子 agent 前,会先拿到 SpawnReservation,像餐厅先占座:如果后面成功,就 commit 正式登记;如果中途失败,这个预订对象被丢弃时会自动退座、退名额。文件还处理根线程登记、按路径找线程、列出活跃子 agent、记录最近任务内容,以及控制子 agent 嵌套深度。
format_agent_nickname44–61 ↗
fn format_agent_nickname(name: &str, nickname_reset_count: usize) -> String
作用:把一个普通名字变成可显示的 agent 昵称。如果昵称池用完并重置过,它会加上类似 “the 2nd”“the 3rd” 的后缀,避免再次撞名。
数据流:输入一个基础名字和当前昵称池重置次数 → 如果没重置过,就原样返回;如果重置过,就算出第几轮,并选对英文序数后缀 → 输出一个最终昵称字符串。
调用关系:它是昵称生成的小帮手。AgentRegistry::reserve_agent_nickname 在给新 agent 选名字时会调用它,用来把候选名字加工成当前这一轮可用的昵称。
调用图:被 1 处调用(reserve_agent_nickname);外部调用 1 个(format!)。
session_depth63–69 ↗
fn session_depth(session_source: &SessionSource) -> i32
作用:读取当前会话已经嵌套了几层子 agent。这里的“深度”可以理解成一棵树往下分叉了几层。
数据流:输入一个 SessionSource,也就是会话来源说明 → 如果它来自线程派生的子 agent,就取出里面记录的 depth;其他来源都当作 0 → 输出当前深度数字。
调用关系:它只负责读出当前层数。next_thread_spawn_depth 会调用它,然后在这个基础上算出下一个子 agent 应该处在第几层。
调用图:被 1 处调用(next_thread_spawn_depth)。
next_thread_spawn_depth71–73 ↗
fn next_thread_spawn_depth(session_source: &SessionSource) -> i32
作用:计算“如果现在再派生一个子 agent,它会是第几层”。这用于控制子 agent 不要无限套娃。
数据流:输入当前会话来源 → 先交给 session_depth 得到当前深度 → 再安全地加 1 → 输出新线程派生后的深度。
调用关系:它站在派生子 agent 前的检查环节。它依赖 session_depth 读当前层数,结果通常会再交给深度限制判断。
调用图:调用 1 个内部函数(session_depth)。
exceeds_thread_spawn_depth_limit75–77 ↗
fn exceeds_thread_spawn_depth_limit(depth: i32, max_depth: i32) -> bool
作用:判断一个准备使用的子 agent 深度是否超过上限。它是一个很直接的“有没有超标”检查。
数据流:输入当前深度和允许的最大深度 → 比较当前深度是否大于最大值 → 输出 true 或 false,表示是否超限。
调用关系:它通常用于创建子 agent 前的安全检查。上游先算出深度,再用它决定能不能继续派生。
AgentRegistry::reserve_spawn_slot80–97 ↗
fn reserve_spawn_slot(
self: &Arc<Self>,
max_threads: Option<usize>,
) -> Result<SpawnReservation>
作用:在真正创建子 agent 之前,先预占一个“名额”。如果系统设置了最多线程数,它会先确认还有空位。
数据流:输入可选的最大线程数 → 如果有上限,就调用 AgentRegistry::try_increment_spawned 尝试安全加一;如果没上限,就直接把总数加一 → 成功时输出一个 SpawnReservation 预订对象,失败时输出“达到 agent 上限”的错误。
调用关系:这是开新 agent 的第一道门。后续流程会拿着返回的 SpawnReservation 继续预订昵称和路径;如果创建成功会提交,如果失败,SpawnReservation::drop 会自动把名额退回。
调用图:调用 1 个内部函数(try_increment_spawned);外部调用 2 个(clone, fetch_add)。
AgentRegistry::release_spawned_thread99–119 ↗
fn release_spawned_thread(&self, thread_id: ThreadId)
作用:当一个已登记的子线程结束时,把它从活跃表里移除,并把占用的数量名额还回去。
数据流:输入结束的 ThreadId → 在 agent 表里找到对应记录并删除 → 如果删掉的是非根 agent,就把总数减一;如果没找到或是根 agent,就不扣数量 → 不返回数据,只修改登记表和计数。
调用关系:它用于线程生命周期的收尾阶段。创建时由预订和登记增加数量,结束时由它做清理,保证注册表不会一直以为旧 agent 还活着。
调用图:外部调用 1 个(fetch_sub)。
AgentRegistry::register_root_thread121–134 ↗
fn register_root_thread(&self, thread_id: ThreadId)
作用:登记当前会话的根线程,也就是整棵 agent 树的起点。根线程不算普通子 agent,但需要被记录,方便按路径定位。
数据流:输入根线程的 ThreadId → 锁住活跃表 → 如果根路径还没有记录,就插入一条带根路径和线程号的元数据 → 不返回数据,只补全注册表。
调用关系:它一般在会话开始或根线程确定时调用。后续按路径查 agent、构建 agent 树时,会依赖这条根记录。
AgentRegistry::agent_id_for_path136–143 ↗
fn agent_id_for_path(&self, agent_path: &AgentPath) -> Option<ThreadId>
作用:根据 agent 的路径找到对应线程号。路径像文件夹地址,线程号则是实际运行中的身份牌。
数据流:输入一个 AgentPath → 把路径转成可查表的字符串形式 → 在活跃 agent 表里查找 → 如果找到并且记录里有线程号,就输出 ThreadId;否则输出空。
调用关系:它是“按地址找人”的查询接口。其他需要把用户指定路径转换成真实线程的流程,会通过它拿到 ThreadId。
调用图:调用 1 个内部函数(as_str)。
AgentRegistry::agent_metadata_for_thread145–153 ↗
fn agent_metadata_for_thread(&self, thread_id: ThreadId) -> Option<AgentMetadata>
作用:根据线程号取出这个 agent 的完整登记信息,比如路径、昵称、角色和最近任务。
数据流:输入 ThreadId → 在所有 agent 记录里寻找线程号匹配的一条 → 找到就复制一份 AgentMetadata 输出,找不到就输出空。
调用关系:它是“按身份牌查档案”的查询接口。需要展示、路由或了解某个线程上下文时,会用它拿到元数据。
AgentRegistry::live_agents155–167 ↗
fn live_agents(&self) -> Vec<AgentMetadata>
作用:列出当前还活着的子 agent。它会特意排除根线程,因为这里关心的是被派生出来的子 agent。
数据流:不需要额外输入,只读取注册表 → 过滤出有线程号、且路径不是根路径的记录 → 输出这些 AgentMetadata 的列表。
调用关系:它用于查看当前会话里的子 agent 状态。界面、调度器或状态汇报功能可以用它得到一份活跃子 agent 清单。
AgentRegistry::update_last_task_message169–181 ↗
fn update_last_task_message(&self, thread_id: ThreadId, last_task_message: String)
作用:给某个 agent 记录“最近正在处理的任务消息”。这让外部能知道这个 agent 最近被派去做什么。
数据流:输入 ThreadId 和一段任务文字 → 在注册表里找到对应 agent → 把它的 last_task_message 更新为这段文字 → 不返回数据,只改记录。
调用关系:它在给 agent 分配或更新任务时使用。后续查询 AgentMetadata 或列出 live_agents 时,就能看到这条最近任务信息。
AgentRegistry::clear_last_task_message183–195 ↗
fn clear_last_task_message(&self, thread_id: ThreadId)
作用:清掉某个 agent 的最近任务记录。比如任务完成后,就不应该继续显示它还在做旧任务。
数据流:输入 ThreadId → 找到对应 agent 记录 → 把 last_task_message 设为空 → 不返回数据,只改注册表里的状态。
调用关系:它和 AgentRegistry::update_last_task_message 配套使用。一个负责写入当前任务,一个负责在任务结束或不再适用时清掉。
AgentRegistry::register_spawned_thread197–214 ↗
fn register_spawned_thread(&self, agent_metadata: AgentMetadata)
作用:把已经成功创建出来的子 agent 正式写进注册表。只有到这一步,预订才变成真正的登记。
数据流:输入一份 AgentMetadata → 如果里面没有线程号,就直接结束;如果有线程号,就选择路径作为表里的键,没有路径时用 thread:id 作为备用键 → 同时记录已用昵称,并把完整元数据插入活跃表。
调用关系:它由 SpawnReservation::commit 调用。前面 reserve_spawn_slot 等步骤只是占位,commit 成功后才通过它完成正式注册。
AgentRegistry::reserve_agent_nickname216–254 ↗
fn reserve_agent_nickname(&self, names: &[&str], preferred: Option<&str>) -> Option<String>
作用:为即将创建的 agent 预占一个昵称,避免多个 agent 叫同一个名字。也支持调用方指定一个偏好的名字。
数据流:输入候选名字列表和可选的首选名字 → 如果有首选名字就直接用;否则从候选名字里生成当前轮次的可用昵称,并随机挑一个 → 如果候选都用完,就清空已用昵称池、增加重置次数、记录一次监控指标,再生成带序号的新昵称 → 输出预占成功的昵称,或在没有候选时输出空。
调用关系:它是 SpawnReservation::reserve_agent_nickname_with_preference 背后的实际工作者。它会调用 format_agent_nickname 生成带后缀的名字,也会使用随机数从可用名字中挑选。
调用图:调用 1 个内部函数(format_agent_nickname);外部调用 2 个(global, rng)。
AgentRegistry::reserve_agent_path256–273 ↗
fn reserve_agent_path(&self, agent_path: &AgentPath) -> Result<()>
作用:为即将创建的 agent 预占一个路径,防止两个 agent 落在同一个树节点上。
数据流:输入一个 AgentPath → 锁住注册表并检查这个路径是否已存在 → 如果已存在,输出“不支持该操作,因为路径已存在”的错误;如果不存在,就插入一条还没有线程号的占位记录 → 成功时不输出额外数据,只留下占位。
调用关系:它由 SpawnReservation::reserve_agent_path 包装调用。创建 agent 前先占路径;如果后续失败,SpawnReservation::drop 会调用释放逻辑把这个空占位删掉。
调用图:外部调用 5 个(default, format!, clone, to_string, UnsupportedOperation)。
AgentRegistry::release_reserved_agent_path275–287 ↗
fn release_reserved_agent_path(&self, agent_path: &AgentPath)
作用:释放之前预占但没有真正创建成功的 agent 路径。它只会删掉“还没有线程号”的占位,避免误删真实 agent。
数据流:输入一个 AgentPath → 在注册表里查这个路径 → 如果记录存在且 agent_id 为空,说明只是预订位,就删除;如果已经有线程号,就不动 → 不返回数据,只可能清理注册表。
调用关系:它主要由 SpawnReservation::drop 在失败清理时调用。这样即使创建流程半路出错,也不会留下一个永远占着路径的假 agent。
调用图:调用 1 个内部函数(as_str)。
AgentRegistry::try_increment_spawned289–305 ↗
fn try_increment_spawned(&self, max_threads: usize) -> bool
作用:在有最大线程数限制时,安全地把已派生数量加一。它能应对多个线程同时抢名额的情况。
数据流:输入最大允许线程数 → 先读取当前数量 → 如果已经达到上限,输出 false;否则用原子比较交换 compare_exchange_weak(一种“确认没人改过再写入”的安全操作)尝试把数量加一 → 成功输出 true,若中途别人改了数量就重试。
调用关系:它只被 AgentRegistry::reserve_spawn_slot 调用,是预占线程名额时的核心限流动作。没有它,并发创建 agent 时可能会突破最大数量。
调用图:被 1 处调用(reserve_spawn_slot);外部调用 2 个(compare_exchange_weak, load)。
SpawnReservation::reserve_agent_nickname_with_preference316–329 ↗
fn reserve_agent_nickname_with_preference(
&mut self,
names: &[&str],
preferred: Option<&str>,
) -> Result<String>
作用:在一个创建预订里,为新 agent 预占昵称,并记住这个昵称属于本次预订。
数据流:输入候选昵称列表和可选首选昵称 → 调用注册表的昵称预占逻辑 → 如果拿不到昵称,就返回“没有可用昵称”的错误;如果成功,就把昵称存进这个 SpawnReservation,并把昵称返回给调用方。
调用关系:它会被 prepare_thread_spawn 调用,也就是准备派生线程的流程会用它先把名字占好。它把具体挑名字的活交给 AgentRegistry::reserve_agent_nickname。
调用图:被 1 处调用(prepare_thread_spawn)。
SpawnReservation::reserve_agent_path331–335 ↗
fn reserve_agent_path(&mut self, agent_path: &AgentPath) -> Result<()>
作用:在一个创建预订里,为新 agent 预占路径,并记住这个路径属于本次预订。
数据流:输入目标 AgentPath → 调用注册表的路径预占逻辑 → 成功后把路径复制到 SpawnReservation 里;失败就把错误传回去 → 输出成功或错误。
调用关系:它会被 prepare_thread_spawn 调用,用在真正创建线程之前。路径预占成功后,如果创建失败,SpawnReservation::drop 能知道该释放哪个路径。
调用图:被 1 处调用(prepare_thread_spawn);外部调用 1 个(clone)。
SpawnReservation::commit337–342 ↗
fn commit(mut self, agent_metadata: AgentMetadata)
作用:确认这次预订已经成功变成真实 agent。可以把它理解成“占座后人真的到了,于是正式入座”。
数据流:输入自身这个预订对象和完整 AgentMetadata → 清空预订里暂存的昵称和路径,表示不再需要失败回滚 → 调用 AgentRegistry::register_spawned_thread 正式登记线程 → 把 active 设为 false,防止析构时再退名额。
调用关系:它位于创建流程的成功结尾。前面 reserve_spawn_slot、reserve_agent_nickname_with_preference、reserve_agent_path 都是在准备;commit 负责把准备结果落成正式记录。
SpawnReservation::drop346–353 ↗
fn drop(&mut self)
作用:当预订对象被丢弃时自动做清理,防止失败的创建流程留下名额或路径垃圾。这是 Rust 的 Drop(对象结束生命时自动执行的收尾动作)。
数据流:输入即将被销毁的 SpawnReservation 自身 → 如果它仍然 active,说明还没 commit 成功;这时先释放预占路径,再把总线程计数减一 → 如果已经 commit,就什么也不退。
调用关系:它不是普通调用出来的函数,而是在 SpawnReservation 生命周期结束时自动运行。它是整个预订机制的保险丝:只要没成功提交,就自动回滚。
core/src/context/inter_agent_completion_message.rs源码 ↗
这个文件解决的是“多个代理互相交接结果时,消息长什么样”的问题。可以把它想成一张固定格式的交接单:上面写着任务名、谁发来的、正文是什么。InterAgentCompletionMessage 保存三样东西:task_name 是任务路径,sender 是发送者路径,payload 是实际内容。它还实现了 ContextualUserFragment,也就是“能被塞进对话上下文的一小段内容”的接口。系统需要它时,会问它:你在对话里算什么角色?它回答 assistant;你外面要不要加特殊标记?它回答不加;你的正文是什么?它把三样信息排成一段清楚的文本,并标明 Message Type: FINAL_ANSWER。这样后面的模型或流程不用猜这段话的来源和用途。
InterAgentCompletionMessage::new13–19 ↗
fn new(task_name: AgentPath, sender: AgentPath, payload: impl Into<String>) -> Self
作用:创建一条代理之间的完成消息。调用者给出任务名、发送者和消息内容,它把这些打包成一个 InterAgentCompletionMessage。
数据流:进去的是 task_name、sender 和 payload;payload 可以是字符串,也可以是能转成字符串的东西。函数把 payload 转成真正的 String,然后连同任务名和发送者一起放进新结构体里,出来的是一条完整的消息对象。
调用关系:它会被 format_inter_agent_completion_message 调用,通常是在系统准备把一个代理的最终回答整理成上下文片段时使用。它只负责“装盒”,不负责决定内容从哪里来,也不负责最后怎么展示。
调用图:被 1 处调用(format_inter_agent_completion_message);外部调用 1 个(into)。
InterAgentCompletionMessage::role23–25 ↗
fn role(&self) -> &'static str
作用:告诉上下文系统,这段消息在对话里应该用 assistant 这个角色出现。这里的 role 就像聊天记录里每句话前面的身份标签。
数据流:进去的是当前这条消息对象,但它不需要读取里面的任务名、发送者或正文。它直接返回固定文本 assistant,不改动任何数据。
调用关系:当系统把 InterAgentCompletionMessage 当作 ContextualUserFragment 使用时,会询问它的角色。这个结果会影响这段文本在对话上下文里被当成谁说的话。
InterAgentCompletionMessage::markers27–29 ↗
fn markers(&self) -> (&'static str, &'static str)
作用:告诉上下文系统,这段消息正文前后要不要加特殊边界标记。这个实现选择不加任何标记。
数据流:进去的是当前消息对象;函数不读取具体内容,而是转去调用 type_markers,拿到一对固定标记。出来的是两个空字符串,表示开头和结尾都不包额外符号。
调用关系:它是 ContextualUserFragment 接口的一部分。系统在拼上下文时会问每个片段有没有外包装;这里把决定交给 type_markers,保持实例方法和类型级默认值一致。
调用图:外部调用 1 个(type_markers)。
InterAgentCompletionMessage::type_markers31–33 ↗
fn type_markers() -> (&'static str, &'static str)
作用:给这种消息类型定义默认的前后标记。这里返回空字符串,意思是这种消息不需要像 XML 标签那样被包起来。
数据流:没有输入,也不看任何消息实例。它直接返回一对空字符串;不产生副作用,也不改动任何东西。
调用关系:markers 会使用它来得到实际标记。这样如果以后要给这类消息统一加边界,只需要改这里,所有实例都会跟着变。
core/src/session_prefix.rs源码 ↗
在这个系统里,主代理和子代理会一起干活。问题是,这些内部状态不能直接丢给模型看,必须变成一段清楚、固定格式的文字,放进用户角色消息里,但又不能被误解成真正的用户要求。这个文件就是做这件事的。它会把子代理状态包装成通知,把代理完成任务后的结果包装成“某代理发来的完成消息”,还会生成一行简单的子代理说明,比如“编号:昵称”。有个重要细节:如果子代理报错,错误内容可能很长,所以这里会按 token(模型理解文字时用的小单位)截断,避免把上下文撑爆;同时还会加一句下一步建议,提醒模型如果还需要这个代理,就重新派任务。正在运行、初始化中、被打断这类还没最终结束的状态不会生成完成消息,因为还没到该总结的时候。
format_subagent_notification_message20–25 ↗
fn format_subagent_notification_message(
agent_reference: &str,
status: &AgentStatus,
) -> String
作用:把某个子代理的当前状态做成一段给模型看的通知文字。别人调用它,是为了在聊天上下文里明确告诉模型“这个子代理现在是什么情况”。
数据流:输入是子代理的引用名和它的状态;函数先用这些信息创建一个 SubagentNotification(可以理解成一张标准格式的状态便签),状态会复制一份以免动到原来的数据;最后把这张便签渲染成普通字符串返回。
调用关系:它会被 maybe_start_completion_watcher 使用。也就是说,当系统开始关注某个子代理是否完成时,需要把子代理状态变化整理成模型可读的通知,这个函数就负责最后的文字包装。
调用图:调用 1 个内部函数(new);被 1 处调用(maybe_start_completion_watcher);外部调用 1 个(clone)。
format_inter_agent_completion_message27–44 ↗
fn format_inter_agent_completion_message(
task_name: AgentPath,
sender: AgentPath,
status: &AgentStatus,
) -> Option<String>
作用:把一个代理完成、报错、关闭或找不到等“最终结果”,整理成发给另一个代理看的完成消息。如果代理还没结束,它会选择不生成消息。
数据流:输入是任务名、发送者路径和代理状态;函数先判断状态:完成就取完成留言,没有留言就用空字符串;报错就先把错误文字截短,再加上“如果还需要它就重新派任务”的提示;关闭或找不到会变成简短说明;如果还在初始化、运行中或被打断,就返回 None,表示暂时没有可发的完成消息。最终如果有内容,就包成 InterAgentCompletionMessage 并渲染成字符串返回。
调用关系:它是多代理协作里“转交结果”的关键小零件。maybe_start_completion_watcher 会用它监听完成情况;multi_agent_v2_completion_queues_message_for_direct_parent 和 forward_child_completion_to_parent 会用它把子代理结果传给父代理;相关测试 multi_agent_v2_followup_task_completion_notifies_parent_on_every_turn 也验证了这种通知行为。
调用图:调用 1 个内部函数(new);被 4 处调用(maybe_start_completion_watcher, multi_agent_v2_completion_queues_message_for_direct_parent, forward_child_completion_to_parent, multi_agent_v2_followup_task_completion_notifies_parent_on_every_turn);外部调用 4 个(new, truncate_text, format!, Tokens)。
format_subagent_context_line50–58 ↗
fn format_subagent_context_line(
agent_reference: &str,
agent_nickname: Option<&str>,
) -> String
作用:生成一行简短的子代理介绍,通常用于上下文列表里。它让模型知道某个代理的引用名,以及可选的昵称。
数据流:输入是子代理引用名和一个可选昵称;如果昵称存在而且不是空字符串,就输出类似“- 引用名: 昵称”的一行;如果没有昵称或昵称为空,就只输出“- 引用名”。它不修改任何外部状态,只返回这行文字。
调用关系:这个函数不依赖本文件里的其他函数,主要作为格式化小工具使用。它把子代理身份压成一行稳定格式,方便上层代码把多个子代理列给模型看。
调用图:外部调用 1 个(format!)。
core/src/agent/control/execution.rs源码 ↗
可以把这个文件想成游乐场入口的计数器:进来一个正在执行的新子智能体,就把人数加一;结束时自动减一;如果人数已经到上限,就拒绝再放人。这里的“线程”可以理解成同时进行的智能体任务。文件先判断一个操作是不是会开始新一轮执行,比如用户输入,或会触发回合的智能体间消息。只有真的要开新回合时,才检查容量。如果这个线程本来已经有一轮在跑,就不重复占名额。限制只对 MultiAgentVersion::V2 的 SubAgent 会话生效,也就是新版多智能体里的子智能体。AgentExecutionLimiter 保存当前活跃数量和最大数量;AgentExecutionGuard 像一张入场票,创建时占一个名额,销毁时自动还回去,避免忘记释放。
AgentExecutionGuard::drop24–26 ↗
fn drop(&mut self)
作用:当一个执行名额不再使用时,自动把“正在运行的数量”减一。这样即使调用方忘了手动释放,Rust 对象销毁时也会把名额还回去。
数据流:进去的是即将被销毁的 AgentExecutionGuard,里面带着对应的限制器 → 它把限制器里的 active 计数减一 → 出来没有返回值,但系统记录的活跃执行数少了一个。
调用关系:AgentExecutionLimiter::guard 创建这个守卫时会先占一个名额;等任务结束、守卫离开作用范围时,这个 drop 会自动运行,完成收尾。
AgentControl::ensure_execution_capacity_for_op30–48 ↗
async fn ensure_execution_capacity_for_op(
&self,
thread_id: ThreadId,
op: &Op,
) -> CodexResult<()>
作用:在真正处理一个操作前,判断这个操作会不会开启新的智能体执行回合;如果会,就检查还有没有空余名额。它是更贴近“收到操作时该不该放行”的入口。
数据流:进去的是线程编号 thread_id 和一个操作 op → 它先用 op_starts_turn 判断这是不是会开启新回合的操作;不是的话直接放行 → 如果是,它找到对应线程,确认当前没有已在进行的回合,再读取配置和多智能体版本 → 最后把版本和会话来源交给 AgentControl::ensure_execution_capacity → 出来是成功放行,或返回“智能体数量达到上限”的错误。
调用关系:它通常在处理用户输入或智能体间消息前被调用。它自己负责收集上下文信息,然后把真正的容量判断交给 AgentControl::ensure_execution_capacity。
调用图:调用 2 个内部函数(ensure_execution_capacity, op_starts_turn)。
AgentControl::ensure_execution_capacity50–64 ↗
fn ensure_execution_capacity(
&self,
multi_agent_version: MultiAgentVersion,
session_source: &SessionSource,
) -> CodexResult<()>
作用:检查某个会话是否受执行数量限制;如果受限制,就看当前是否还有空位。它是容量规则的核心判断点。
数据流:进去的是多智能体版本 multi_agent_version 和会话来源 session_source → 它先用 is_execution_limited 判断这类会话是否需要限流 → 不需要就直接成功 → 需要的话读取最大允许数量,再看 AgentExecutionLimiter 当前是否还有容量 → 出来是成功,或带着最大数量返回 AgentLimitReached 错误。
调用关系:AgentControl::ensure_execution_capacity_for_op 会在确认某个操作要开启新回合后调用它。它不负责占名额,只负责说“现在能不能开始”。
调用图:调用 1 个内部函数(is_execution_limited);被 1 处调用(ensure_execution_capacity_for_op)。
AgentControl::execution_guard66–73 ↗
fn execution_guard(
&self,
multi_agent_version: MultiAgentVersion,
session_source: &SessionSource,
) -> Option<AgentExecutionGuard>
作用:在确实需要受限的会话开始执行时,生成一个“占用名额的守卫”。这个守卫活着,就代表一个执行名额正在被使用。
数据流:进去的是多智能体版本和会话来源 → 它用 is_execution_limited 判断是否需要限制 → 如果不需要,返回 None,表示不用占名额 → 如果需要,就复制限制器引用并调用 guard,占用一个名额 → 出来是 Some(AgentExecutionGuard) 或 None。
调用关系:它通常在容量检查通过、实际开始执行时使用。它把“占名额”和“结束时自动归还名额”的工作交给 AgentExecutionLimiter::guard 和 AgentExecutionGuard::drop。
调用图:调用 1 个内部函数(is_execution_limited)。
AgentExecutionLimiter::initialize77–79 ↗
fn initialize(&self, max_threads: usize)
作用:设置最多允许多少个受限智能体同时执行。这个值只会初始化一次,避免运行中被反复改来改去。
数据流:进去的是 max_threads,也就是最大同时执行数量 → 它尝试把这个数字放进 OnceLock(一次性存储格)里 → 如果之前没设置,就保存;如果已经设置过,就保持旧值 → 出来没有返回值,但限制器可能完成了上限初始化。
调用关系:它一般在系统启动或控制器初始化时被调用。后面的 AgentExecutionLimiter::max_threads 会读取这里设好的值。
调用图:外部调用 1 个(get_or_init)。
AgentExecutionLimiter::max_threads81–83 ↗
fn max_threads(&self) -> usize
作用:取出当前允许的最大同时执行数量。如果还没初始化,就当作几乎无限制处理。
数据流:进去的是限制器自身 → 它读取 OnceLock 里保存的最大数量 → 如果有值就返回该值;如果没有,就返回 usize::MAX,也就是一个非常大的数 → 出来是一个数字上限。
调用关系:AgentExecutionLimiter::has_capacity 会用它来比较当前活跃数量和上限。它让未初始化的情况不会误伤执行流程。
调用图:被 1 处调用(has_capacity);外部调用 1 个(get)。
AgentExecutionLimiter::has_capacity85–87 ↗
fn has_capacity(&self) -> bool
作用:判断现在是否还有空位可以启动新的受限执行。简单说,就是看“当前人数”有没有小于“最大人数”。
数据流:进去的是限制器自身 → 它读取 active,也就是当前活跃执行数;再通过 AgentExecutionLimiter::max_threads 取得上限 → 比较两者 → 出来是 true 或 false,表示能不能再放一个进去。
调用关系:AgentControl::ensure_execution_capacity 会用它做最终放行判断。它只看容量,不会修改计数。
调用图:调用 1 个内部函数(max_threads);外部调用 1 个(load)。
AgentExecutionLimiter::guard89–92 ↗
fn guard(self: Arc<Self>) -> AgentExecutionGuard
作用:真正占用一个执行名额,并返回一个会在结束时自动释放名额的守卫对象。
数据流:进去的是一个共享的限制器 Arc<Self>,Arc 可以理解成多人共用同一个对象的安全引用 → 它把 active 计数加一 → 然后把这个限制器装进 AgentExecutionGuard → 出来是一个守卫,代表这次执行已经占了一个名额。
调用关系:AgentControl::execution_guard 会在需要限制的会话开始执行时调用它。它创建的 AgentExecutionGuard 后续会靠 AgentExecutionGuard::drop 自动把计数减回去。
调用图:外部调用 1 个(fetch_add)。
op_starts_turn95–98 ↗
fn op_starts_turn(op: &Op) -> bool
作用:判断一个操作是否会开启新的执行回合。只有会开新回合的操作,才需要检查执行名额。
数据流:进去的是一个 Op 操作 → 它检查这个操作是不是用户输入,或者是不是带有 trigger_turn 标记的智能体间通信 → 如果符合就返回 true,否则返回 false。
调用关系:AgentControl::ensure_execution_capacity_for_op 一开始会调用它。这个小判断避免对无关操作做多余的容量检查。
调用图:被 1 处调用(ensure_execution_capacity_for_op);外部调用 1 个(matches!)。
is_execution_limited100–106 ↗
fn is_execution_limited(
multi_agent_version: MultiAgentVersion,
session_source: &SessionSource,
) -> bool
作用:判断某个会话类型是否应该受到“同时执行数量”的限制。当前规则很具体:只限制 V2 版本的子智能体。
数据流:进去的是多智能体版本和会话来源 → 它检查版本是否等于 MultiAgentVersion::V2,并且来源是否是 SessionSource::SubAgent → 两个条件都满足就返回 true,否则返回 false。
调用关系:AgentControl::ensure_execution_capacity 用它决定要不要检查容量;AgentControl::execution_guard 用它决定要不要占用名额。它把限流规则集中在一个地方,避免各处写出不一致的判断。
调用图:被 2 处调用(ensure_execution_capacity, execution_guard);外部调用 1 个(matches!)。
core/src/agent/control/residency.rs源码 ↗
V2 子代理线程可能会被保留下来,方便之后继续使用。但机器资源有限,不能无限留着。这个文件就维护一张“常驻名单”,并按最近使用情况排序:刚用过的排到后面,最久没用的排在前面。新线程要进来时,先尝试占一个预留名额;如果满了,就从最久没用的线程里找一个可以安全卸载的。所谓安全卸载,是指这个线程已经完成、出错或被中断,没有正在处理的回合,也没有待处理消息。卸载前还会确保需要落盘的内容已经准备好,再关闭线程并从线程管理器里移除。文件里还有一个小心设计:预留名额用 V2ResidencySlot 表示,如果创建线程失败而没有正式提交,这个名额会在对象销毁时自动归还,避免“占着坑不停车”。
V2ResidencySlot::commit33–36 ↗
fn commit(mut self, thread_id: ThreadId)
作用:把一个已经预留的常驻名额正式交给某个线程。简单说,就是“车真的停进来了,把临时占位变成正式登记”。
数据流:进去的是这个预留名额对象和一个线程编号 → 它把线程编号登记到常驻列表里,并把预留名额数量减掉 → 出来没有返回值,但内部状态变成“这个线程已常驻”,同时这个 slot 不再负责自动归还名额。
调用关系:它是预留流程的收尾动作。前面通常先通过 AgentControl::reserve_v2_residency_slot 拿到 V2ResidencySlot,等线程真正加载成功后,再调用它把名额提交给 V2Residency::commit_slot。
V2ResidencySlot::drop40–44 ↗
fn drop(&mut self)
作用:这是预留名额的自动清理保险。若名额被预留了但没有提交,它会在对象被丢弃时自动归还,防止系统以为名额还被占着。
数据流:进去的是即将销毁的 slot 自身状态 → 它检查 active 是否还为真 → 如果还没提交,就调用归还逻辑减少 pending_slots;如果已经提交过,就什么也不做。
调用关系:它配合 V2ResidencySlot::commit 使用。commit 会把 active 关掉,所以 drop 不会重复释放;如果调用方中途出错没有 commit,drop 会兜底调用 V2Residency::release_pending_slot。
AgentControl::reserve_v2_residency_slot48–60 ↗
async fn reserve_v2_residency_slot(
&self,
state: &Arc<ThreadManagerState>,
config: &Config,
protected_thread_id: Option<ThreadId>,
) -> CodexResult<V2ResidencySlot
作用:为一个新的 V2 常驻线程申请位置。它会先看配置里允许最多留多少个 V2 线程,然后让常驻管理器去占位或腾位。
数据流:进去的是线程管理器状态、配置,以及一个可保护的线程编号 → 它从配置读取 V2 最大线程数;如果没配,就当作没有上限 → 然后把容量和保护对象交给 V2Residency::reserve_slot,最后返回一个可提交或自动释放的 V2ResidencySlot,或者返回“线程数达到上限”的错误。
调用关系:这是 AgentControl 对外协调常驻名额的入口。它读取配置里的 effective_agent_max_threads,并把真正的排队、腾位、报错交给 V2Residency::reserve_slot。
调用图:外部调用 2 个(clone, effective_agent_max_threads)。
AgentControl::touch_loaded_v2_residency62–72 ↗
async fn touch_loaded_v2_residency(
&self,
state: &Arc<ThreadManagerState>,
thread_id: ThreadId,
)
作用:告诉常驻列表:某个已经加载的线程刚刚被用过。这样它就不会被当成最久没用的线程优先踢掉。
数据流:进去的是线程管理器状态和线程编号 → 它先尝试从线程管理器取出这个线程 → 如果这个线程符合 V2 子代理常驻条件,就把它在常驻列表里挪到最近使用的位置;如果不是或取不到,就不改任何东西。
调用关系:它在已有线程被访问或重新加载后使用。它会调用 is_resident_candidate 判断这个线程是否属于 V2 子代理常驻范围,符合才交给 V2Residency::touch 更新最近使用顺序。
调用图:调用 1 个内部函数(is_resident_candidate)。
AgentControl::forget_v2_residency74–76 ↗
fn forget_v2_residency(&self, thread_id: ThreadId)
作用:把某个线程从 V2 常驻名单里忘掉。通常在线程已经被删除、关闭或不该再占常驻位置时用。
数据流:进去的是线程编号 → 它让 V2Residency 从 residents 列表中删掉这个编号 → 出来没有返回值,但这个线程不再被当作常驻线程统计。
调用关系:这是 AgentControl 提供的清理入口。它把具体删除动作交给 V2Residency::remove,避免常驻列表里留下已经不存在的线程。
V2Residency::reserve_slot80–102 ↗
async fn reserve_slot(
self: Arc<Self>,
manager: &Arc<ThreadManagerState>,
capacity: usize,
protected_thread_id: Option<ThreadId>,
) -> CodexResult<V2ResidencySlot>
作用:这是申请常驻名额的核心流程。它要么直接占到一个空位,要么尝试卸载一个旧线程腾出位置,再不行就报满。
数据流:进去的是常驻管理器、线程管理器、容量上限和一个受保护线程编号 → 它循环尝试:先用 try_reserve_pending_slot 看能不能直接占位;如果不能,就用 try_unload_one_resident 试着卸掉一个可安全移除的旧线程 → 成功时返回一个 active 的 V2ResidencySlot;如果没有线程能卸,就返回 AgentLimitReached 错误。
调用关系:它由 AgentControl::reserve_v2_residency_slot 调用,是“占位或腾位”的总调度。它自己不直接判断每个线程状态,而是把快速占位交给 V2Residency::try_reserve_pending_slot,把找旧线程并卸载交给 V2Residency::try_unload_one_resident。
调用图:调用 2 个内部函数(try_reserve_pending_slot, try_unload_one_resident)。
V2Residency::try_reserve_pending_slot104–114 ↗
fn try_reserve_pending_slot(&self, capacity: usize) -> bool
作用:快速检查还有没有空名额,并先临时占一个。临时占位很重要,因为线程创建是异步的,中间不能让别人把名额抢走。
数据流:进去的是容量上限 → 它锁住内部状态,计算已常驻数量加上正在预留的数量是否达到上限 → 如果没满,就把 pending_slots 加一并返回 true;如果满了,状态不变并返回 false。
调用关系:它只被 V2Residency::reserve_slot 调用,是申请流程里的第一步。成功就不用卸载旧线程;失败才会进入 V2Residency::try_unload_one_resident。
调用图:被 1 处调用(reserve_slot)。
V2Residency::try_unload_one_resident116–150 ↗
async fn try_unload_one_resident(
&self,
manager: &Arc<ThreadManagerState>,
protected_thread_id: Option<ThreadId>,
) -> bool
作用:在名额满时,尝试找一个旧的、闲着的常驻线程卸载掉。它不会乱关正在干活的线程,也会避开被指定保护的线程。
数据流:进去的是线程管理器和可保护的线程编号 → 它先记录当前有多少常驻候选可扫描,然后反复取出最久没用的候选;取到后确认线程还存在、仍是常驻候选、并且 is_unloadable 认为它安全可卸 → 可卸时先确保输出材料落地,再关闭线程、等待结束,并从线程管理器移除;成功返回 true。遇到不可卸或关闭失败的线程,会把它重新标记为最近使用并继续找;全都不行就返回 false。
调用关系:它被 V2Residency::reserve_slot 在满员时调用。它用 V2Residency::resident_count 控制扫描范围,用 V2Residency::pop_lru_candidate 取最久没用的线程,用 is_unloadable 做安全检查,必要时用 V2Residency::touch 把不能卸的线程放回队尾;关闭失败时会通过 warn! 记录警告。
调用图:调用 4 个内部函数(pop_lru_candidate, resident_count, touch, is_unloadable);被 1 处调用(reserve_slot);外部调用 1 个(warn!)。
V2Residency::resident_count152–158 ↗
fn resident_count(&self) -> usize
作用:读取当前常驻列表里有多少线程。这里用来决定一次腾位最多扫描多少个候选,避免无限循环。
数据流:进去的是常驻管理器自身 → 它锁住内部状态并读取 residents 的长度 → 返回一个数字,表示当前登记的常驻线程数量。
调用关系:它被 V2Residency::try_unload_one_resident 调用。腾位流程会先用这个数量定一个扫描上限,因为扫描过程中候选可能被挪动顺序。
调用图:被 1 处调用(try_unload_one_resident)。
V2Residency::pop_lru_candidate160–175 ↗
fn pop_lru_candidate(&self, protected_thread_id: Option<ThreadId>) -> Option<ThreadId>
作用:从常驻列表里取出“最久没用”的线程候选。LRU 可以理解成“最近最少使用”,像清理冰箱时先处理最久没碰的东西。
数据流:进去的是一个可保护的线程编号 → 它从队列前面弹出候选;如果候选正好是受保护的线程,就把它放回队尾并继续找;如果找到不受保护的候选,就返回这个线程编号;如果没有合适的,就返回空。
调用关系:它被 V2Residency::try_unload_one_resident 调用,用来决定先尝试卸载谁。它只负责挑候选,不负责判断线程是否真的能安全关闭。
调用图:被 1 处调用(try_unload_one_resident)。
V2Residency::touch177–183 ↗
fn touch(&self, thread_id: ThreadId)
作用:把某个线程标记为刚刚使用过。这样它在“谁最久没用”的排序里会排到后面,不容易被优先卸载。
数据流:进去的是线程编号 → 它锁住常驻状态,然后调用 touch_resident,把这个编号从旧位置移除并放到队尾 → 出来没有返回值,但常驻列表的使用顺序被更新。
调用关系:它会在 AgentControl::touch_loaded_v2_residency 间接使用,也会被 V2Residency::try_unload_one_resident 调用。当某个候选不能卸载或关闭失败时,try_unload_one_resident 会用它把候选放回较新的位置。
调用图:调用 1 个内部函数(touch_resident);被 1 处调用(try_unload_one_resident)。
V2Residency::remove185–191 ↗
fn remove(&self, thread_id: ThreadId)
作用:从常驻列表中删除一个线程编号。它用于清掉已经不应再算作常驻的线程。
数据流:进去的是线程编号 → 它锁住内部状态,并过滤 residents 列表,把等于这个编号的项全部去掉 → 出来没有返回值,但列表里不再包含这个线程。
调用关系:它由 AgentControl::forget_v2_residency 调用。这个函数是常驻状态的底层删除动作,避免上层直接操作内部队列。
V2Residency::commit_slot193–200 ↗
fn commit_slot(&self, thread_id: ThreadId)
作用:把一个临时预留名额正式变成某个线程的常驻登记。它同时减少“正在预留”的数量,增加或刷新“已常驻”的记录。
数据流:进去的是线程编号 → 它锁住状态,把 pending_slots 安全地减一,然后调用 touch_resident 把这个线程放到常驻队列末尾 → 出来没有返回值,但预留名额变少,线程成为最近使用的常驻线程。
调用关系:它由 V2ResidencySlot::commit 调用,是 slot 提交时真正修改内部状态的地方。它复用 touch_resident 来保证同一个线程不会在队列里重复出现。
调用图:调用 1 个内部函数(touch_resident)。
V2Residency::release_pending_slot202–208 ↗
fn release_pending_slot(&self)
作用:归还一个没有用上的临时预留名额。它是失败路径上的保险,避免名额被永久虚占。
数据流:进去的是常驻管理器自身 → 它锁住状态,把 pending_slots 安全地减一;即使本来就是零,也不会减成负数 → 出来没有返回值,但可用名额恢复。
调用关系:它主要由 V2ResidencySlot::drop 调用。当调用方拿到 slot 后没有执行 V2ResidencySlot::commit,drop 就会用它清理预留状态。
touch_resident211–214 ↗
fn touch_resident(residents: &mut VecDeque<ThreadId>, thread_id: ThreadId)
作用:维护常驻队列里的“最近使用”顺序。它保证某个线程只出现一次,并把它放到队尾表示刚用过。
数据流:进去的是一个可修改的线程队列和一个线程编号 → 它先用 retain 删除队列里已有的同编号项,再用 push_back 把编号追加到队尾 → 出来没有返回值,但队列顺序变成最新状态。
调用关系:它是一个小工具函数,被 V2Residency::touch 和 V2Residency::commit_slot 共用。这样“刷新使用时间”和“正式登记线程”都遵守同一套排序规则。
调用图:被 2 处调用(commit_slot, touch);外部调用 2 个(push_back, retain)。
is_resident_candidate216–219 ↗
fn is_resident_candidate(thread: &CodexThread) -> bool
作用:判断一个线程是不是应该进入 V2 常驻名单。它只认 V2 版本,并且只认特定来源的子代理会话。
数据流:进去的是一个 CodexThread 线程对象 → 它读取线程的 multi_agent_version,并检查会话来源是否是 V2 可常驻来源 → 返回 true 或 false,表示这个线程是否值得纳入常驻管理。
调用关系:它被 AgentControl::touch_loaded_v2_residency 调用,用来避免把不相关线程放进常驻列表。它内部还会调用 is_v2_resident_session_source 来判断会话来源。
调用图:调用 2 个内部函数(is_v2_resident_session_source, multi_agent_version);被 1 处调用(touch_loaded_v2_residency)。
is_v2_resident_session_source221–223 ↗
fn is_v2_resident_session_source(session_source: &SessionSource) -> bool
作用:判断一个会话来源是否属于 V2 常驻关心的来源。这里目前只把 SubAgent,也就是子代理,会话算进去。
数据流:进去的是 SessionSource 会话来源 → 它用模式匹配检查来源是不是 SessionSource::SubAgent → 返回 true 表示是子代理来源,false 表示不是。
调用关系:它被 is_resident_candidate 调用,是候选判断中的来源过滤器。这样普通会话不会被误放进 V2 子代理常驻池。
调用图:被 1 处调用(is_resident_candidate);外部调用 1 个(matches!)。
is_unloadable225–236 ↗
async fn is_unloadable(thread: &CodexThread) -> bool
作用:判断一个线程现在能不能安全卸载。它要确认线程已经不在干活,也没有排队消息,才允许被关掉。
数据流:进去的是一个 CodexThread → 它异步读取线程状态,要求状态是已完成、已出错或已中断;接着检查当前没有 active_turn,也就是没有正在进行的一轮工作;最后检查输入队列里没有待处理邮箱消息 → 三项都满足返回 true,否则返回 false。
调用关系:它被 V2Residency::try_unload_one_resident 调用,是卸载前的安全门。只有它说可以卸,腾位流程才会继续做落盘、关闭线程、从管理器移除这些动作。
调用图:被 1 处调用(try_unload_one_resident);外部调用 1 个(matches!)。
core/src/agent/control/spawn.rs源码 ↗
可以把这里想成智能体世界里的“调度前台”。用户或某个父智能体要拉起一个子智能体时,它先检查名额够不够,再决定用哪套多智能体版本,继承哪些环境变量和执行规则,然后真正创建线程。线程就是一段可继续运行的对话任务。它还会记录父子关系,发通知让客户端知道新线程出现了,并把第一条任务送进去。如果是“分叉”,它会从父线程历史里挑出适合带走的内容,避免把工具调用、推理过程这类不该复制的中间痕迹带过去。如果是恢复旧线程,它会读保存下来的历史,把线程重新装回内存。V2 版本还多了“驻留”名额,像停车位一样,保证常驻线程不会无限占资源。
default_agent_nickname_list11–17 ↗
fn default_agent_nickname_list() -> Vec<&'static str>
作用:读取项目里内置的智能体昵称名单,做成一个可用列表。它的作用是:如果配置里没给某个角色指定昵称,就从默认名单里挑。
数据流:进去的是编译进程序的文本文件内容 → 它按行拆开,去掉空白,丢掉空行 → 出来的是一组干净的默认昵称。
调用关系:它是昵称选择的后备来源。agent_nickname_candidates 在找不到配置里的候选昵称时,会来这里拿默认名单。
调用图:被 1 处调用(agent_nickname_candidates)。
agent_nickname_candidates19–31 ↗
fn agent_nickname_candidates(config: &Config, role_name: Option<&str>) -> Vec<String>
作用:给某个智能体角色找一批可用昵称。这样创建子智能体时,不只是冷冰冰的线程号,而能有更容易识别的名字。
数据流:进去的是配置和可选的角色名 → 它先查这个角色有没有专门配置昵称候选;如果没有,就调用 default_agent_nickname_list 拿默认昵称 → 出来的是字符串形式的昵称列表。
调用关系:它被 prepare_thread_spawn 使用,用在准备子智能体出生信息的时候。它自己只负责“给名字候选”,不真正创建线程。
调用图:调用 1 个内部函数(default_agent_nickname_list);被 1 处调用(prepare_thread_spawn)。
keep_forked_rollout_item33–65 ↗
fn keep_forked_rollout_item(item: &RolloutItem, preserve_reference_context_item: bool) -> bool
作用:判断父线程历史里的某一项,分叉给子线程时要不要保留。它防止子线程继承太多杂乱或危险的中间过程。
数据流:进去的是一条历史记录,以及是否保留参考上下文的开关 → 它按记录类型检查:保留系统、开发者、用户消息和最终回答,丢掉工具调用、推理片段、智能体间通信等中间痕迹 → 出来的是 true 或 false,表示这条要不要带到分叉线程里。
调用关系:它服务于分叉流程。spawn_forked_thread 在整理父线程历史时,会用这个判断器筛掉不适合复制给新线程的内容。
is_multi_agent_v2_usage_hint_message67–81 ↗
fn is_multi_agent_v2_usage_hint_message(item: &ResponseItem, usage_hint_texts: &[String]) -> bool
作用:识别一条消息是不是多智能体 V2 的“使用提示语”。这些提示语有时需要从继承历史里删掉,避免重复提醒或把父智能体的提示带错地方。
数据流:进去的是一条回复项和一组提示语文本 → 它只认开发者消息,并且要求内容正好是一段输入文本 → 如果文本和任一提示语完全相同,就返回 true,否则返回 false。
调用关系:它在分叉时配合历史清理使用。spawn_forked_thread 会用它过滤掉旧的 V2 使用提示,再按需要给子智能体补上新的提示。
AgentControl::spawn_agent86–100 ↗
async fn spawn_agent(
&self,
config: Config,
initial_operation: Op,
session_source: Option<SessionSource>,
) -> CodexResult<ThreadId>
作用:这是测试里用的简化版创建智能体入口。调用者给配置、第一条操作和来源,它创建线程后只返回线程 ID。
数据流:进去的是配置、初始操作、可选会话来源 → 它把默认创建选项补上,调用 spawn_agent_internal 完整走一遍创建流程 → 出来的是新线程的编号;如果失败,就返回错误。
调用关系:它是 spawn_agent_internal 的轻量包装。测试代码不关心完整元数据时,会用它来快速拉起一个智能体。
调用图:调用 1 个内部函数(spawn_agent_internal);外部调用 2 个(pin, default)。
AgentControl::spawn_agent_with_metadata103–112 ↗
async fn spawn_agent_with_metadata(
&self,
config: Config,
initial_operation: Op,
session_source: Option<SessionSource>,
options: SpawnAgentOptions, // TODO(jif
作用:创建一个智能体线程,并把线程的元数据一起返回。调用者想知道昵称、路径、状态等附加信息时会用它。
数据流:进去的是配置、初始操作、来源和创建选项 → 它直接把这些交给 spawn_agent_internal → 出来的是 LiveAgent,里面包含线程 ID、元数据和当前状态。
调用关系:它是对外更完整的创建入口。真正复杂的创建步骤都在 spawn_agent_internal 里完成。
调用图:调用 1 个内部函数(spawn_agent_internal);外部调用 1 个(pin)。
AgentControl::ensure_v2_agent_loaded114–192 ↗
async fn ensure_v2_agent_loaded(
&self,
config: Config,
thread_id: ThreadId,
) -> CodexResult<()>
作用:确保一个多智能体 V2 线程已经加载到内存里。简单说,就是如果这个常驻智能体睡在磁盘记录里,就把它唤醒。
数据流:进去的是配置和线程 ID → 它先看线程是否已经在内存中;如果在,就刷新驻留状态;如果不在,就读取保存的线程历史、确认它确实是 V2、预约驻留名额,并按原来源恢复线程 → 成功后线程重新可用,失败则返回找不到线程或恢复错误。
调用关系:它处理 V2 驻留线程的按需加载。恢复过程中会构造 Resumed 历史,并在成功后通知系统“线程已创建/已加载”。
调用图:外部调用 2 个(ThreadNotFound, Resumed)。
AgentControl::spawn_agent_internal194–377 ↗
async fn spawn_agent_internal(
&self,
config: Config,
initial_operation: Op,
session_source: Option<SessionSource>,
options: SpawnAgentOptions,
) -> CodexRe
作用:这是新建智能体的核心流水线。它把限额检查、来源处理、线程创建、通知、记录父子关系、发送第一条任务这些步骤串起来。
数据流:进去的是配置、初始操作、可选来源和创建选项 → 它判断该用哪个多智能体版本,检查容量,必要时预约 V2 驻留名额,再准备继承的环境和执行策略;如果是子智能体,还准备昵称、路径等元数据;然后创建普通线程或调用 spawn_forked_thread 创建分叉线程;最后登记名额、发通知、记录父子边、发送初始操作 → 出来的是 LiveAgent,同时系统状态被更新。
调用关系:spawn_agent 和 spawn_agent_with_metadata 都把活交给它。它在遇到分叉模式时再交给 spawn_forked_thread;创建完成后还会触发通知、分析事件和非 V2 的完成监听。
调用图:调用 1 个内部函数(spawn_forked_thread);被 2 处调用(spawn_agent, spawn_agent_with_metadata);外部调用 5 个(pin, clone, effective_agent_max_threads, default, warn!)。
AgentControl::spawn_forked_thread379–521 ↗
async fn spawn_forked_thread(
&self,
state: &Arc<ThreadManagerState>,
config: Config,
session_source: SessionSource,
options: &SpawnAgentOptions,
inheri
作用:从父智能体的历史里复制出一个“分叉”子线程。它像从一本笔记里剪出有用页给新人看,而不是把草稿、工具记录和重复提示全塞过去。
数据流:进去的是线程管理状态、配置、会话来源、分叉选项、继承信息和多智能体版本 → 它先检查分叉必须有父线程、父调用 ID 和分叉模式;再刷新父线程历史到存储,读取父历史;如果只要最近几轮,就裁剪历史;接着过滤不该继承的记录和 V2 使用提示;必要时补上子智能体提示 → 出来的是一个基于整理后历史创建的新线程。
调用关系:它只在 spawn_agent_internal 发现本次创建是分叉时被调用。它最后把整理好的 Forked 初始历史交给线程管理器真正创建线程。
调用图:调用 1 个内部函数(build_developer_update_item);被 1 处调用(spawn_agent_internal);外部调用 7 个(new, clone, matches!, Fatal, Forked, ResponseItem, vec!)。
AgentControl::resume_agent_from_rollout524–600 ↗
async fn resume_agent_from_rollout(
&self,
config: Config,
thread_id: ThreadId,
session_source: SessionSource,
) -> CodexResult<ThreadId>
作用:从保存的 rollout 记录里恢复一个智能体线程。rollout 可以理解成这段对话任务的历史流水账。
数据流:进去的是配置、要恢复的线程 ID 和会话来源 → 它先恢复这个线程本身;如果是 V2 或当前功能配置走 V2,就到此结束;如果是旧版多智能体,它还会查数据库里仍然打开的子线程关系,按队列一路恢复后代线程 → 出来的是最初那个被恢复线程的 ID。
调用关系:它是批量恢复的外层入口。每恢复一个具体线程,都会调用 resume_single_agent_from_rollout;恢复旧版子树时,如果某个子线程失败,会记录警告并继续处理其他线程。
调用图:调用 1 个内部函数(resume_single_agent_from_rollout);外部调用 6 个(pin, from, clone, multi_agent_version_from_features, SubAgent, warn!)。
AgentControl::resume_single_agent_from_rollout602–713 ↗
async fn resume_single_agent_from_rollout(
&self,
config: Config,
thread_id: ThreadId,
session_source: SessionSource,
) -> CodexResult<(ThreadId, MultiAgentVersion)
作用:恢复单个智能体线程,不负责递归恢复它的孩子。它把磁盘上的历史重新变成内存里的可运行线程。
数据流:进去的是配置、线程 ID 和会话来源 → 它读取保存的线程和历史,包装成 Resumed 初始历史;判断多智能体版本和线程上限;预约创建名额;如果是子智能体,就尽量从数据库找回原来的昵称和角色;再继承环境与执行策略,调用线程管理器恢复线程 → 出来的是恢复后的线程 ID 和它使用的多智能体版本,同时会登记元数据、发通知、记录父子关系。
调用关系:它被 resume_agent_from_rollout 反复调用,用来恢复根线程和旧版的后代线程。恢复成功后,它走和新建线程类似的通知路径,让客户端和监听器能重新接上。
调用图:被 1 处调用(resume_agent_from_rollout);外部调用 5 个(clone, effective_agent_max_threads, clone, default, Resumed)。
core/src/agent/control.rs源码 ↗
可以把这里的 AgentControl 想成一个项目经理手里的通讯录和对讲机。没有它,主线程和子智能体之间就会乱:不知道谁是谁、谁是谁的孩子、消息该发给哪条线程、结束后该通知谁。它保存同一棵智能体树共用的会话编号,连到全局线程管理器,同时用 AgentRegistry 记录每个智能体的名字、路径、角色和最近任务。它还会在发消息前检查运行名额,避免同时跑太多任务;在线程死掉时清理登记;在列出智能体时按路径整理;在子智能体完成后,把结果以合适格式塞回父线程。
AgentControl::new107–112 ↗
fn new(manager: Weak<ThreadManagerState>) -> Self
作用:创建一个新的智能体控制器,让它之后能通过线程管理器去找线程、发操作、建子智能体。
数据流:输入是一根指向 ThreadManagerState 的弱引用,也就是“能找到总线程表,但不强行占住它”的指针。函数拿默认值填好其他字段,再把这根弱引用放进去,输出一个 AgentControl。
调用关系:它由 agent_control 创建流程调用。内部依赖默认构造,把登记表、驻留信息、运行限制器这些零件先按默认状态装好。
调用图:被 1 处调用(agent_control);外部调用 1 个(default)。
AgentControl::with_session_id114–118 ↗
fn with_session_id(mut self, session_id: SessionId, max_threads: usize) -> Self
作用:给已经创建好的控制器补上会话编号,并设置最多能同时运行多少个线程。
数据流:输入是一个 AgentControl、一个 SessionId 和最大线程数。它把会话编号写进控制器,再初始化运行数量限制器,最后把更新后的控制器交回去。
调用关系:它在 new 相关创建流程之后使用,相当于给刚造好的调度台贴上本次会话的标签,并装上并发上限这道保险。
调用图:被 1 处调用(new)。
AgentControl::session_id120–122 ↗
fn session_id(&self) -> SessionId
作用:读出这个控制器所属的会话编号。
数据流:输入是当前 AgentControl。它只读取内部保存的 session_id,不改任何东西,输出这个 SessionId。
调用关系:它由 new 相关流程使用,也可被需要确认“这棵智能体树属于哪个会话”的地方调用。
调用图:被 1 处调用(new)。
AgentControl::send_input125–135 ↗
async fn send_input(
&self,
agent_id: ThreadId,
initial_operation: Op,
) -> CodexResult<String>
作用:把用户输入或其它操作发给某个已存在的智能体线程。
数据流:输入是目标智能体的线程编号和要发送的操作。它先把弱引用升级成可用的线程管理器,再检查这个操作会不会超过运行名额;通过后,把真正发送工作交给 AgentControl::send_input_after_capacity_check,最后返回线程处理请求得到的字符串或错误。
调用关系:这是外部给智能体下达任务的主要入口。它先调用 AgentControl::upgrade 找到线程管理器,再把后续发送和登记最近任务的细节交给 AgentControl::send_input_after_capacity_check。
调用图:调用 2 个内部函数(send_input_after_capacity_check, upgrade)。
AgentControl::send_input_after_capacity_check137–165 ↗
async fn send_input_after_capacity_check(
&self,
agent_id: ThreadId,
state: &Arc<ThreadManagerState>,
initial_operation: Op,
) -> CodexResult<String>
作用:在确认运行名额够用之后,真正把操作送到目标线程,并更新这个智能体“最近在干什么”的简短说明。
数据流:输入是目标线程编号、线程管理器和操作。它先从操作里提取一段可显示的任务摘要:如果是智能体间消息就读消息内容,否则用 AgentControl 外的 render_input_preview 做预览;然后调用线程管理器发送操作。发送成功后,它把摘要写进登记表,或在没有可显示内容时清掉旧摘要;输出发送结果。
调用关系:它由 AgentControl::send_input 调用。它会用 last_task_message_from_communication、render_input_preview、non_empty_task_message 生成摘要,并把发送结果交给 AgentControl::handle_thread_request_result 做死亡线程清理。
调用图:调用 4 个内部函数(handle_thread_request_result, last_task_message_from_communication, non_empty_task_message, render_input_preview);被 1 处调用(send_input)。
AgentControl::send_inter_agent_communication167–188 ↗
async fn send_inter_agent_communication(
&self,
agent_id: ThreadId,
communication: InterAgentCommunication,
) -> CodexResult<String>
作用:把一个智能体写给另一个智能体的内部消息发出去。
数据流:输入是目标智能体编号和 InterAgentCommunication。它先准备最近任务摘要,再把消息包成 Op::InterAgentCommunication,检查运行名额,然后通过线程管理器发送。成功后更新或清空目标智能体的最近任务说明,最后返回发送结果。
调用关系:它是智能体之间对话的专用通道。它调用 AgentControl::upgrade 找到线程管理器,调用 last_task_message_from_communication 取摘要,并用 AgentControl::handle_thread_request_result 处理发送失败时的清理。
调用图:调用 3 个内部函数(handle_thread_request_result, upgrade, last_task_message_from_communication)。
AgentControl::interrupt_agent191–199 ↗
async fn interrupt_agent(&self, agent_id: ThreadId) -> CodexResult<String>
作用:打断某个智能体当前正在做的任务,就像按下“停止当前工作”按钮。
数据流:输入是目标智能体线程编号。它先找到线程管理器,然后向该线程发送 Op::Interrupt。发送后的结果会经过统一检查;如果发现内部智能体已经死了,会顺便清理登记。
调用关系:这是外部停止智能体工作的入口。它调用 AgentControl::upgrade 获取线程管理器,再把结果交给 AgentControl::handle_thread_request_result 做统一善后。
调用图:调用 2 个内部函数(handle_thread_request_result, upgrade)。
AgentControl::handle_thread_request_result201–213 ↗
async fn handle_thread_request_result(
&self,
agent_id: ThreadId,
state: &Arc<ThreadManagerState>,
result: CodexResult<String>,
) -> CodexResult<String>
作用:统一检查给线程发请求后的结果;如果发现目标内部智能体已经死掉,就把残留记录清掉。
数据流:输入是目标线程编号、线程管理器和刚刚发送请求得到的结果。它查看结果是不是 InternalAgentDied 这种“内部智能体死亡”错误;如果是,就从线程管理器移除线程,忘掉驻留记录,并释放登记表里的已生成线程。最后原样返回结果。
调用关系:它被 AgentControl::send_input_after_capacity_check、AgentControl::send_inter_agent_communication 和 AgentControl::interrupt_agent 调用。这样所有发请求的路径都共享同一套坏线程清理规则。
调用图:被 3 处调用(interrupt_agent, send_input_after_capacity_check, send_inter_agent_communication);外部调用 1 个(matches!)。
AgentControl::get_status216–225 ↗
async fn get_status(&self, agent_id: ThreadId) -> AgentStatus
作用:查询某个智能体现在是什么状态,比如运行中、完成、找不到等。
数据流:输入是智能体线程编号。它先尝试找到线程管理器,再从里面取出线程;任一步失败都返回 NotFound。成功时读取线程自己的状态并返回。
调用关系:它会调用 AgentControl::upgrade。完成监听器等地方会用它兜底:如果订阅状态失败,就直接查一次当前状态。
调用图:调用 1 个内部函数(upgrade)。
AgentControl::register_session_root227–235 ↗
fn register_session_root(
&self,
current_thread_id: ThreadId,
current_parent_thread_id: Option<ThreadId>,
)
作用:把当前线程登记成这棵智能体树的根线程,也就是“主线程”。
数据流:输入是当前线程编号和可选的父线程编号。如果没有父线程,说明这是根线程,于是把它登记进 AgentRegistry;如果有父线程,就什么也不做。
调用关系:它在会话根线程建立时使用。这个登记让后续按路径查找根智能体、列出主线程时有依据。
AgentControl::get_agent_metadata237–239 ↗
fn get_agent_metadata(&self, agent_id: ThreadId) -> Option<AgentMetadata>
作用:读取某个智能体的登记信息,比如路径、昵称、角色和最近任务。
数据流:输入是线程编号。它到 AgentRegistry 里查对应元数据;查到就返回 Some,查不到就返回 None,不改变任何状态。
调用关系:它是读取登记表的小窗口。其它需要展示或判断智能体身份的流程可以直接用它,而不用自己翻登记表。
AgentControl::ensure_agent_known241–245 ↗
fn ensure_agent_known(&self, agent_id: ThreadId) -> CodexResult<AgentMetadata>
作用:确认某个智能体确实在登记表里;如果不在,就给出“线程不存在”的错误。
数据流:输入是线程编号。它查 AgentRegistry,查到就返回元数据;查不到就构造 ThreadNotFound 错误返回。
调用关系:它适合用在后续操作必须依赖已知智能体的地方。它调用 ThreadNotFound 相关错误构造,把“查不到”变成明确错误。
调用图:外部调用 1 个(ThreadNotFound)。
AgentControl::list_live_agent_subtree_thread_ids247–254 ↗
async fn list_live_agent_subtree_thread_ids(
&self,
agent_id: ThreadId,
) -> CodexResult<Vec<ThreadId>>
作用:列出某个智能体以及它下面所有仍然活着的子孙线程编号。
数据流:输入是根智能体线程编号。它先把根编号放进列表,再调用 AgentControl::live_thread_spawn_descendants 找到所有活的后代,拼在一起返回。
调用关系:它是按子树做批量操作的基础。它把真正遍历父子关系的工作交给 AgentControl::live_thread_spawn_descendants。
调用图:调用 1 个内部函数(live_thread_spawn_descendants);外部调用 1 个(vec!)。
AgentControl::get_agent_config_snapshot256–267 ↗
async fn get_agent_config_snapshot(
&self,
agent_id: ThreadId,
) -> Option<ThreadConfigSnapshot>
作用:拿到某个智能体当前配置的一份快照,方便查看它是按什么设置运行的。
数据流:输入是线程编号。它先找到线程管理器,再取出对应线程;失败就返回 None。成功后向线程要配置快照并返回。
调用关系:它调用 AgentControl::upgrade。需要了解子智能体运行环境或恢复设置的流程会用到它。
调用图:调用 1 个内部函数(upgrade)。
AgentControl::resolve_agent_reference269–288 ↗
async fn resolve_agent_reference(
&self,
_current_thread_id: ThreadId,
current_session_source: &SessionSource,
agent_reference: &str,
) -> CodexResult<ThreadId>
作用:把用户写的智能体引用,比如相对路径名字,解析成真正的线程编号。
数据流:输入是当前线程编号、当前会话来源和一段引用文字。它先拿当前智能体路径,没有就用根路径;再把引用解析成完整 AgentPath。若登记表里有这个路径对应的线程编号,就返回;没有就返回 UnsupportedOperation 错误。
调用关系:它让用户或智能体不用记线程编号,只写路径名字即可。它依赖 SessionSource 的 get_agent_path,并用 AgentRegistry 完成路径到线程编号的查找。
调用图:外部调用 3 个(get_agent_path, format!, UnsupportedOperation)。
AgentControl::subscribe_status291–298 ↗
async fn subscribe_status(
&self,
agent_id: ThreadId,
) -> CodexResult<watch::Receiver<AgentStatus>>
作用:订阅某个智能体的状态变化,后续它一变就能收到通知。
数据流:输入是线程编号。它找到线程管理器和线程,然后从线程那里拿一个 watch::Receiver。watch 可以理解成“状态广播频道”,订阅者能看到当前值和后续变化。
调用关系:它调用 AgentControl::upgrade。AgentControl::maybe_start_completion_watcher 会用它等待子智能体变成最终状态。
调用图:调用 1 个内部函数(upgrade)。
AgentControl::format_environment_context_subagents300–320 ↗
async fn format_environment_context_subagents(
&self,
parent_thread_id: ThreadId,
) -> String
作用:生成一段给环境上下文看的文字,说明某个父线程下面有哪些子智能体可用。
数据流:输入是父线程编号。它先找这个父线程当前打开的子线程;找不到就返回空字符串。找到后,把每个子智能体的路径名或线程编号,加上昵称,格式化成一行行说明文字并拼起来。
调用关系:它调用 AgentControl::open_thread_spawn_children,并使用 format_subagent_context_line 生成每一行。它通常用于把“你有哪些子助手”提示给当前线程。
调用图:调用 1 个内部函数(open_thread_spawn_children);外部调用 1 个(new)。
AgentControl::list_agents322–395 ↗
async fn list_agents(
&self,
current_session_source: &SessionSource,
path_prefix: Option<&str>,
) -> CodexResult<Vec<ListedAgent>>
作用:列出当前这棵会话树里活着的智能体,以及它们的状态和最近任务。
数据流:输入是当前会话来源和可选路径前缀。它先解析前缀,再从登记表取出活智能体并排序;如果根线程匹配,也加入“Main thread”。随后逐个取线程状态,过滤掉不匹配或已经取不到线程的记录,最后返回 ListedAgent 列表。
调用关系:它调用 AgentControl::upgrade 找线程管理器,也用 agent_matches_prefix 做路径过滤。它是“显示当前有哪些智能体”的主要汇总函数。
调用图:调用 2 个内部函数(upgrade, root);外部调用 1 个(with_capacity)。
AgentControl::maybe_start_completion_watcher401–482 ↗
fn maybe_start_completion_watcher(
&self,
child_thread_id: ThreadId,
session_source: Option<SessionSource>,
child_reference: String,
child_agent_path: Option<Ag
作用:如果一个子智能体是由某个父线程生成的,就启动一个后台观察者,在子智能体结束时通知父线程。
数据流:输入是子线程编号、会话来源、子智能体显示名和可选路径。如果来源不是线程生成的子智能体,它直接退出。否则它启动一个异步任务,订阅子线程状态直到完成;完成后,如果是新版多智能体路径,就构造一条智能体间消息发给父线程;否则生成普通通知文字,直接塞进父线程的用户消息里。
调用关系:它调用 AgentControl::subscribe_status 等待状态,借助 is_final 判断是否结束,用 format_inter_agent_completion_message 或 format_subagent_notification_message 生成通知,并可能调用 AgentControl::send_inter_agent_communication。它通过 tokio::spawn 把等待过程放到后台。
调用图:调用 4 个内部函数(is_final, format_inter_agent_completion_message, format_subagent_notification_message, new);外部调用 2 个(new, spawn)。
AgentControl::prepare_thread_spawn485–522 ↗
fn prepare_thread_spawn(
&self,
reservation: &mut crate::agent::registry::SpawnReservation,
config: &Config,
parent_thread_id: ThreadId,
depth: i32,
age
作用:在真正创建子线程前,先把路径、昵称、角色和来源信息准备好并占位,避免两个子智能体抢同一个名字。
数据流:输入包括保留登记、配置、父线程编号、深度、可选路径、角色和偏好的昵称。如果这是第一层子线程,它会登记父线程为根;如果有路径,就先保留路径。然后根据配置和角色生成昵称候选,按偏好保留一个昵称,最后输出 SessionSource 和 AgentMetadata。
调用关系:它调用 spawn::agent_nickname_candidates 生成候选名,调用 reserve_agent_path 和 reserve_agent_nickname_with_preference 做占位。它是生成子智能体前的“取号排队”步骤。
调用图:调用 3 个内部函数(agent_nickname_candidates, reserve_agent_nickname_with_preference, reserve_agent_path);外部调用 1 个(SubAgent)。
AgentControl::upgrade524–528 ↗
fn upgrade(&self) -> CodexResult<Arc<ThreadManagerState>>
作用:把内部保存的弱引用升级成真正可用的线程管理器引用。
数据流:输入是当前 AgentControl。它尝试把 Weak<ThreadManagerState> 升级成 Arc<ThreadManagerState>;如果线程管理器已经被释放,就返回 UnsupportedOperation 错误。
调用关系:它被 AgentControl::send_input、AgentControl::send_inter_agent_communication、AgentControl::interrupt_agent、AgentControl::get_status、AgentControl::get_agent_config_snapshot、AgentControl::subscribe_status、AgentControl::list_agents 和 AgentControl::live_thread_spawn_children 调用。它是所有需要访问全局线程表的操作的第一道门。
调用图:被 8 处调用(get_agent_config_snapshot, get_status, interrupt_agent, list_agents, live_thread_spawn_children, send_input, send_inter_agent_communication, subscribe_status);外部调用 1 个(upgrade)。
AgentControl::inherited_environments_for_source530–552 ↗
async fn inherited_environments_for_source(
&self,
state: &Arc<ThreadManagerState>,
session_source: Option<&SessionSource>,
) -> Option<TurnEnvironmentSnapshot>
作用:如果当前线程是父线程生成的子智能体,就把父线程的环境选择快照拿来给子线程继承。
数据流:输入是线程管理器和可选会话来源。它只处理 ThreadSpawn 这种有父线程的来源;否则返回 None。若能找到父线程,就读取父线程服务里的 turn_environments 快照并返回。
调用关系:它通常在创建子线程时使用,用来保证子智能体看到的环境选择和父线程保持一致。它依赖父线程里的会话服务提供快照。
AgentControl::inherited_exec_policy_for_source554–576 ↗
async fn inherited_exec_policy_for_source(
&self,
state: &Arc<ThreadManagerState>,
session_source: Option<&SessionSource>,
child_config: &Config,
) -> Option<Arc<cr
作用:判断子智能体是否应该沿用父线程的执行策略;如果应该,就把父线程那套策略传给它。
数据流:输入是线程管理器、可选会话来源和子线程配置。它只处理由父线程生成的子智能体。找到父线程后,它读取父配置,再用 child_uses_parent_exec_policy 判断是否继承;如果不该继承就返回 None,应该继承就返回父线程 ExecPolicyManager 的共享引用。
调用关系:它调用 child_uses_parent_exec_policy,并克隆 Arc 引用。它在子线程创建阶段起作用,避免安全或执行规则在父子之间意外不一致。
调用图:调用 1 个内部函数(child_uses_parent_exec_policy);外部调用 1 个(clone)。
AgentControl::open_thread_spawn_children578–586 ↗
async fn open_thread_spawn_children(
&self,
parent_thread_id: ThreadId,
) -> CodexResult<Vec<(ThreadId, AgentMetadata)>>
作用:找出某个父线程下面仍然活着的直接子线程。
数据流:输入是父线程编号。它先调用 AgentControl::live_thread_spawn_children 得到“父线程到孩子列表”的总表,再取出这个父线程对应的那一组;没有就返回空列表。
调用关系:它被 AgentControl::format_environment_context_subagents 和 wait_for_live_thread_spawn_children 使用。它把构建总父子表的工作交给 AgentControl::live_thread_spawn_children。
调用图:调用 1 个内部函数(live_thread_spawn_children);被 2 处调用(format_environment_context_subagents, wait_for_live_thread_spawn_children)。
AgentControl::live_thread_spawn_children588–621 ↗
async fn live_thread_spawn_children(
&self,
) -> CodexResult<HashMap<ThreadId, Vec<(ThreadId, AgentMetadata)>>>
作用:整理当前所有仍然活着的“父线程生成子线程”关系。
数据流:它先通过 AgentControl::upgrade 找到线程管理器,再读取所有活的生成边。每条边都有父线程和子线程;它把它们放进一个 HashMap,并给子线程补上登记元数据。最后按路径和线程编号排序,返回这张父子关系表。
调用关系:它被 AgentControl::open_thread_spawn_children 和 AgentControl::live_thread_spawn_descendants 调用。它是所有“按父子关系看智能体”的基础数据来源。
调用图:调用 1 个内部函数(upgrade);被 2 处调用(live_thread_spawn_descendants, open_thread_spawn_children);外部调用 2 个(default, new)。
AgentControl::persist_thread_spawn_edge_for_source623–646 ↗
async fn persist_thread_spawn_edge_for_source(
&self,
thread: &crate::CodexThread,
child_thread_id: ThreadId,
session_source: Option<&SessionSource>,
)
作用:把“父线程生成了这个子线程”的关系写进状态数据库,方便之后恢复或查询。
数据流:输入是子线程对象、子线程编号和可选会话来源。它先从来源里取父线程编号;没有父线程就退出。再拿线程的状态数据库上下文;没有数据库也退出。拿到后写入一条 Open 状态的父子边;如果写失败,只记录警告,不中断主流程。
调用关系:它调用 state_db 获取数据库上下文,并在失败时用 warn! 打日志。它通常属于创建子线程后的持久化步骤。
AgentControl::live_thread_spawn_descendants648–672 ↗
async fn live_thread_spawn_descendants(
&self,
root_thread_id: ThreadId,
) -> CodexResult<Vec<ThreadId>>
作用:找出某个线程下面所有活着的子孙线程,不只是一层孩子。
数据流:输入是根线程编号。它先调用 AgentControl::live_thread_spawn_children 拿到所有父子关系,再用一个栈从根的孩子开始一路往下走;每发现一个孩子就放进结果,并继续查它的孩子。最后返回按遍历顺序得到的所有后代线程编号。
调用关系:它被 AgentControl::list_live_agent_subtree_thread_ids 调用。它把直接父子表扩展成完整子树列表。
调用图:调用 1 个内部函数(live_thread_spawn_children);被 1 处调用(list_live_agent_subtree_thread_ids);外部调用 1 个(new)。
agent_matches_prefix675–687 ↗
fn agent_matches_prefix(agent_path: Option<&AgentPath>, prefix: &AgentPath) -> bool
作用:判断某个智能体路径是否落在指定路径前缀下面。
数据流:输入是可选智能体路径和一个前缀路径。如果前缀是根路径,任何智能体都算匹配。否则,只有路径本身等于前缀,或路径字符串以“前缀/”开头,才返回 true。
调用关系:它被 AgentControl::list_agents 用来过滤列表。它调用 is_root 先处理根路径这个特殊情况。
调用图:调用 1 个内部函数(is_root)。
render_input_preview689–710 ↗
fn render_input_preview(initial_operation: &Op) -> String
作用:把一次输入操作转换成短短的可读预览,方便显示“这个智能体最近收到什么任务”。
数据流:输入是一个 Op。若是用户输入,它会逐项转换:文字保留原文,图片显示成 [image],本地图片带路径,技能和提及也显示名字与路径;多项用换行拼起来。若是智能体间消息,就直接用消息内容。其它操作返回空字符串。
调用关系:它被 AgentControl::send_input_after_capacity_check、handle_call 和 handle_spawn_agent 调用。它不真正执行输入,只负责把复杂输入变成适合展示的一句话或几行字。
调用图:被 3 处调用(send_input_after_capacity_check, handle_call, handle_spawn_agent);外部调用 1 个(new)。
last_task_message_from_communication712–717 ↗
fn last_task_message_from_communication(communication: &InterAgentCommunication) -> Option<String>
作用:从智能体间通信里提取可以公开显示的最近任务文字。
数据流:输入是一条 InterAgentCommunication。如果它带有 encrypted_content,也就是加密内容,就返回 None,避免泄露。否则取普通 content,并交给 non_empty_task_message 去掉空内容。
调用关系:它被 AgentControl::send_input_after_capacity_check 和 AgentControl::send_inter_agent_communication 调用。它和 non_empty_task_message 一起决定登记表里的最近任务要写什么。
调用图:调用 1 个内部函数(non_empty_task_message);被 2 处调用(send_input_after_capacity_check, send_inter_agent_communication)。
non_empty_task_message719–721 ↗
fn non_empty_task_message(message: String) -> Option<String>
作用:把空字符串过滤掉,只保留真正有内容的任务说明。
数据流:输入是一段字符串。如果字符串不是空的,就包装成 Some 返回;如果是空的,就返回 None。它不修改外部状态。
调用关系:它被 AgentControl::send_input_after_capacity_check 和 last_task_message_from_communication 使用,是更新最近任务说明前的小过滤器。
调用图:被 2 处调用(send_input_after_capacity_check, last_task_message_from_communication)。
thread_spawn_depth723–728 ↗
fn thread_spawn_depth(session_source: &SessionSource) -> Option<i32>
作用:从会话来源里读出子智能体生成的层级深度。
数据流:输入是 SessionSource。如果它表示一个由线程生成的子智能体,就返回里面记录的 depth;如果不是这种来源,就返回 None。
调用关系:它是读取会话来源的小工具。其它需要知道“这个智能体是第几层子任务”的流程可以用它来避免自己拆枚举。
core/src/agent/agent_resolver.rs源码 ↗
当一个工具要把事情交给某个 agent 时,不能只靠一段随便写的文字,系统需要一个准确的 ThreadId(线程编号,用来标识一条对话或 agent 工作线程)。这个文件先把当前会话的“根关系”登记到 agent 控制服务里,保证后面解析名字时知道它属于哪棵会话树。然后它看目标字符串本身是不是已经是合法的 ThreadId:如果是,就直接用;如果不是,就把这个目标当成引用、别名或名字,交给 agent_control 服务去查。查不到或服务不支持时,它不会让底层错误直接炸出来,而是把错误包装成可以回复给模型看的消息。这样工具调用时既能支持直接编号,也能支持更友好的名字写法。
resolve_agent_target8–29 ↗
async fn resolve_agent_target(
session: &Arc<Session>,
turn: &Arc<TurnContext>,
target: &str,
) -> Result<ThreadId, FunctionCallError>
作用:把一个面向工具的 agent 目标字符串,解析成真正的 ThreadId。调用方会在需要指定“把任务发给哪个 agent”时用它,避免后面拿着模糊名字做事。
数据流:进去的是当前 Session(会话状态)、TurnContext(这一轮对话的上下文)和 target(目标字符串)→ 它先登记当前会话根关系,再尝试把 target 直接当作 ThreadId 解析;如果不行,就带着当前线程、来源信息和 target 去 agent_control 服务查询 → 出来的是一个 ThreadId;如果失败,则返回 FunctionCallError,并尽量把错误变成模型能看懂的提示。
调用关系:它是这个文件的主入口。它先调用 register_session_root 做准备工作,再调用 ThreadId::from_string 尝试直接识别编号;直接识别失败时,它把解析工作交给 session.services.agent_control.resolve_agent_reference。也就是说,它像一个前台接待:先登记来访信息,能直接认出证件就放行,认不出就去后台查档案。
调用图:调用 2 个内部函数(register_session_root, from_string)。
register_session_root31–36 ↗
fn register_session_root(session: &Arc<Session>, turn: &Arc<TurnContext>)
作用:把当前会话线程和它的父线程关系登记到 agent 控制服务里。这样之后解析 agent 引用时,系统知道这些线程之间的上下级关系。
数据流:进去的是 Session 和 TurnContext → 它从 Session 里取当前 thread_id,从 TurnContext 里取 parent_thread_id → 它把这两个编号交给 agent_control.register_session_root 记录下来;它不返回新数据,只更新 agent 控制服务里的登记信息。
调用关系:它只被 resolve_agent_target 调用,是解析目标前的一步准备。没有这一步,后台服务在根据名字或引用查找 agent 时,可能缺少当前会话属于哪条父子线程链的信息。
调用图:被 1 处调用(resolve_agent_target)。
core/src/session/multi_agents.rs源码 ↗
这个文件很小,但位置很关键。它像一个“提示语分发员”:先看当前会话是不是多智能体 V2 版本,也就是新版的多智能体协作模式;再看配置里有没有打开使用提示。如果条件不满足,它就什么都不给。条件满足后,它会根据会话来源来选提示语:如果这是由线程启动出来的子智能体,就返回子智能体专用的提示;如果是命令行、VSCode、执行器、MCP、自定义入口等普通入口,就返回主智能体的提示。内部来源或其他子智能体来源则不给提示。这样做的好处是,同一套系统里不同角色不会收到混乱的说明,就像会议里主持人和分组成员拿到的任务卡不一样。
usage_hint_text6–31 ↗
fn usage_hint_text(
turn_context: &'a TurnContext,
session_source: &SessionSource,
) -> Option<&'a str>
作用:这个函数根据当前会话的上下文和来源,挑出一段应该展示给智能体的使用提示文字。如果当前模式、配置或来源不适合显示提示,它就返回空,表示不要加提示。
数据流:进去的是当前轮次上下文 turn_context,以及会话来源 session_source。它先读取上下文里的多智能体版本,只有版本是 V2 才继续;再读取配置里的 usage_hint_enabled,只有开关打开才继续;最后根据 session_source 判断是主智能体入口还是特定的子智能体入口,并从配置里取对应的提示文字。出来的是一段可借用的字符串,或者 None;它本身不修改任何数据。
调用关系:它会在 build_initial_context 构建初始上下文时被调用,也就是会话刚开始、系统准备给智能体第一批背景信息的时候。它不再调用其他项目函数,只负责做一次清晰的判断:现在该不该加提示、该加哪一种提示。
调用图:被 1 处调用(build_initial_context)。
协作工具接口
此组介绍共享的多 agent 工具约定和辅助工具,然后讲解 classic 和 V2 协作处理器,用于生成、传信、列出、中断、等待、恢复和关闭 agent。
core/src/tools/handlers/multi_agents_spec.rs源码 ↗
这个文件不真正执行“创建代理”或“等待代理”的动作,而是定义这些动作对外长什么样。可以把它想成餐厅菜单:后厨另有地方做菜,这里负责写清楚菜名、点菜格式、能不能加选项、最后会端出什么。它用 JSON Schema(一种描述 JSON 数据格式的规则)规定参数和返回值,避免模型乱填字段。文件里同时保留了 v1 和 v2 两套工具形态:v1 多包在 multi_agent_v1 命名空间里,v2 更多是直接函数工具。它还会根据配置隐藏模型、代理类型等高级选项,并把可选模型整理成说明文字。这样上层在组装工具列表时,只要调用这里的创建函数,就能得到一套清楚、可校验、适合交给模型使用的多代理工具说明。
WaitAgentTimeoutOptions::default39–45 ↗
fn default() -> Self
作用:给“等待代理”工具准备一组默认超时时间。这样调用方不需要每次都手写默认值、最小值和最大值。
数据流:进去没有额外输入 → 它从多代理通用常量里取默认等待时间、最短等待时间、最长等待时间 → 出来一个 WaitAgentTimeoutOptions 对象,后面会被用来写进 wait_agent 的参数说明里。
调用关系:这是等待工具的默认配置来源。创建 wait_agent 工具时如果调用方不特别指定时间范围,就可以用这份默认设置。
create_spawn_agent_tool_v148–78 ↗
fn create_spawn_agent_tool_v1(options: SpawnAgentToolOptions) -> ToolSpec
作用:生成 v1 版本的 spawn_agent 工具说明,也就是“创建一个子代理”的旧版菜单项。上层用它把创建子代理的能力暴露给模型。
数据流:进去的是 SpawnAgentToolOptions,里面有可用模型、代理类型说明、是否隐藏高级字段、是否加使用提示等 → 它先准备参数字段,比如初始任务、结构化输入、是否复制上下文、模型覆盖选项;必要时删除代理类型和模型相关字段 → 出来一个 ToolSpec,里面包含工具名、说明文字、参数规则和返回值规则。
调用关系:上层的 spec 组装工具清单时会调用它。它会借助 spawn_agent_common_properties_v1 准备参数字段,并在需要时交给 hide_spawn_agent_metadata_options 删掉不该展示的高级选项。
调用图:调用 2 个内部函数(hide_spawn_agent_metadata_options, spawn_agent_common_properties_v1);被 1 处调用(spec);外部调用 2 个(Namespace, vec!)。
create_spawn_agent_tool_v280–116 ↗
fn create_spawn_agent_tool_v2(options: SpawnAgentToolOptions) -> ToolSpec
作用:生成 v2 版本的 spawn_agent 工具说明。它要求给新代理起 task_name,让代理可以用任务路径来互相引用。
数据流:进去的是 SpawnAgentToolOptions → 它准备 v2 的参数字段,加入必填的 task_name 和 message,按配置决定是否隐藏模型、推理力度、服务档位等元信息 → 出来一个函数型 ToolSpec,包含参数校验规则和 v2 的返回值格式。
调用关系:上层的 spec 组装工具清单时会调用它。它会调用 spawn_agent_common_properties_v2 做基础参数,调用 hide_spawn_agent_metadata_options 隐藏高级字段,调用 spawn_agent_tool_description_v2 生成说明文字,并调用 spawn_agent_output_schema_v2 生成返回格式。
调用图:调用 6 个内部函数(hide_spawn_agent_metadata_options, spawn_agent_common_properties_v2, spawn_agent_output_schema_v2, spawn_agent_tool_description_v2, object, string);被 1 处调用(spec);外部调用 2 个(Function, vec!)。
create_send_input_tool_v1118–154 ↗
fn create_send_input_tool_v1() -> ToolSpec
作用:生成 v1 版本的 send_input 工具说明,用来给已经存在的代理发送输入。它支持普通文字,也支持更结构化的输入项。
数据流:进去没有业务数据 → 它写出 target、message、items、interrupt 这些参数的格式;target 是目标代理,interrupt 表示是否打断当前任务立即处理 → 出来一个带命名空间的 ToolSpec,并说明成功后会返回 submission_id。
调用关系:上层的 spec 组装工具清单时会调用它。它会调用 create_collab_input_items_schema 来描述 items 这种结构化输入数组。
调用图:调用 3 个内部函数(create_collab_input_items_schema, boolean, string);被 1 处调用(spec);外部调用 3 个(from, Namespace, vec!)。
create_send_message_tool156–186 ↗
fn create_send_message_tool() -> ToolSpec
作用:生成 send_message 工具说明,用来给已有代理投递一条消息,但不触发新一轮执行。它更像把便条塞进对方邮箱。
数据流:进去没有业务数据 → 它定义 target 和加密的 message 两个必填参数;message 标记为加密,表示内容需要按敏感消息处理 → 出来一个函数型 ToolSpec,没有额外返回结构。
调用关系:上层的 spec 组装工具清单时会调用它。它主要依赖 JsonSchema 的 string、object 等构造器来搭出参数规则。
调用图:调用 2 个内部函数(object, string);被 1 处调用(spec);外部调用 3 个(from, Function, vec!)。
create_followup_task_tool188–215 ↗
fn create_followup_task_tool() -> ToolSpec
作用:生成 followup_task 工具说明,用来给非根代理追加一个后续任务。和普通发消息不同,它可能让空闲代理开始新一轮工作。
数据流:进去没有业务数据 → 它定义目标 target 和加密 message 两个必填参数 → 出来一个函数型 ToolSpec,说明如果目标空闲就触发执行,如果正在运行就尽快在合适边界交付。
调用关系:上层的 spec 组装工具清单时会调用它。它和 create_send_message_tool 很像,但语义更强:不是只送消息,而是追加任务。
调用图:调用 2 个内部函数(object, string);被 1 处调用(spec);外部调用 3 个(from, Function, vec!)。
create_resume_agent_tool217–237 ↗
fn create_resume_agent_tool() -> ToolSpec
作用:生成 resume_agent 工具说明,用来恢复一个之前关闭的代理,让它重新能接收消息和等待查询。
数据流:进去没有业务数据 → 它定义 id 这个必填参数,也就是要恢复的代理编号 → 出来一个带命名空间的 ToolSpec,返回值里包含恢复后的状态。
调用关系:上层的 spec 组装工具清单时会调用它。它会配合 resume_agent_output_schema 描述返回的状态结构。
调用图:调用 1 个内部函数(string);被 1 处调用(spec);外部调用 3 个(from, Namespace, vec!)。
create_wait_agent_tool_v1239–253 ↗
fn create_wait_agent_tool_v1(options: WaitAgentTimeoutOptions) -> ToolSpec
作用:生成 v1 版本的 wait_agent 工具说明,用来等待指定代理完成。它适合“我现在卡住了,必须等某个子代理结果”的场景。
数据流:进去的是 WaitAgentTimeoutOptions,提供默认、最小、最大等待时间 → 它把 targets 和 timeout_ms 写成参数规则,并说明超时或完成时返回什么 → 出来一个带命名空间的 ToolSpec。
调用关系:上层的 spec 组装工具清单时会调用它。它把等待时间选项写进 wait_agent 的参数说明,并使用 wait 输出结构来告诉调用方会得到状态和是否超时。
create_wait_agent_tool_v2255–265 ↗
fn create_wait_agent_tool_v2(options: WaitAgentTimeoutOptions) -> ToolSpec
作用:生成 v2 版本的 wait_agent 工具说明。它不是只等指定代理完成,而是等任意活跃代理的邮箱更新、完成通知,或用户输入打断。
数据流:进去的是 WaitAgentTimeoutOptions → 它生成只有 timeout_ms 的参数规则,并生成简短消息加 timed_out 的返回结构 → 出来一个函数型 ToolSpec。
调用关系:上层的 spec 组装工具清单时会调用它。它把参数构造交给 wait_agent_tool_parameters_v2,把返回格式交给 wait_output_schema_v2。
调用图:调用 2 个内部函数(wait_agent_tool_parameters_v2, wait_output_schema_v2);被 1 处调用(spec);外部调用 1 个(Function)。
create_list_agents_tool267–286 ↗
fn create_list_agents_tool() -> ToolSpec
作用:生成 list_agents 工具说明,用来查看当前根线程树里还活着的代理。它可以按任务路径前缀过滤,方便只看某一支。
数据流:进去没有业务数据 → 它定义可选的 path_prefix 参数 → 出来一个函数型 ToolSpec,返回值是代理列表,每个代理带名字、状态和最近任务消息。
调用关系:上层的 spec 组装工具清单时会调用它。它调用 list_agents_output_schema 来规定列表结果长什么样。
调用图:调用 3 个内部函数(list_agents_output_schema, object, string);被 1 处调用(spec);外部调用 2 个(from, Function)。
create_close_agent_tool_v1288–308 ↗
fn create_close_agent_tool_v1() -> ToolSpec
作用:生成 v1 版本的 close_agent 工具说明,用来关闭不再需要的代理以及它的子代理。这样可以释放并发名额,避免代理一直占着资源。
数据流:进去没有业务数据 → 它定义 target 这个必填参数 → 出来一个带命名空间的 ToolSpec,返回关闭请求前观察到的代理状态。
调用关系:上层的 spec 组装工具清单时会调用它。它用 agent_previous_status_output_schema 这类结构来表达“关闭前状态”,让调用方知道关闭前代理是完成、运行、出错还是找不到。
调用图:调用 1 个内部函数(string);被 1 处调用(spec);外部调用 3 个(from, Namespace, vec!)。
create_interrupt_agent_tool_v2310–328 ↗
fn create_interrupt_agent_tool_v2() -> ToolSpec
作用:生成 v2 版本的 interrupt_agent 工具说明,用来打断某个代理当前正在做的一轮工作,但不把这个代理彻底关掉。
数据流:进去没有业务数据 → 它定义 target 这个必填参数,可以是代理 id 或规范任务名 → 出来一个函数型 ToolSpec,返回打断请求处理前的代理状态。
调用关系:上层的 spec 组装工具清单时会调用它。它调用 agent_previous_status_output_schema 来复用“返回之前状态”的格式。
调用图:调用 3 个内部函数(agent_previous_status_output_schema, object, string);被 1 处调用(spec);外部调用 3 个(from, Function, vec!)。
agent_status_output_schema330–359 ↗
fn agent_status_output_schema() -> Value
作用:定义代理状态的统一返回格式。这样所有工具说“代理状态”时,含义一致,不会各写各的。
数据流:进去没有输入 → 它生成一段 JSON 规则:状态可以是 pending_init、running、interrupted、shutdown、not_found 这些字符串,也可以是 completed 或 errored 对象 → 出来一个 serde_json::Value,供其他返回结构嵌入。
调用关系:这是很多输出格式的共同零件。列表、恢复、等待、关闭和打断等工具都需要表达代理状态时,会围绕这个格式来组织返回值。
调用图:外部调用 1 个(json!)。
spawn_agent_output_schema_v1361–377 ↗
fn spawn_agent_output_schema_v1() -> Value
作用:定义 v1 创建代理成功后的返回格式。它告诉调用方会拿到代理 id 和可能存在的昵称。
数据流:进去没有输入 → 它生成一个 JSON 对象规则,要求必须有 agent_id 和 nickname,且不允许多余字段 → 出来一段返回值 schema。
调用关系:它是 create_spawn_agent_tool_v1 的返回格式零件。创建 v1 工具说明时会把它放进 output_schema,让模型知道 spawn_agent 调完会得到什么。
调用图:外部调用 1 个(json!)。
spawn_agent_output_schema_v2379–409 ↗
fn spawn_agent_output_schema_v2(hide_agent_metadata: bool) -> Value
作用:定义 v2 创建代理成功后的返回格式。v2 更强调 task_name,也就是代理的规范任务名。
数据流:进去的是 hide_agent_metadata 布尔值 → 如果要隐藏元信息,它只返回 task_name;否则返回 task_name 和 nickname → 出来一段严格的 JSON 返回规则。
调用关系:create_spawn_agent_tool_v2 会调用它。它让同一个 v2 创建工具能按配置选择“只暴露任务名”或“同时暴露昵称”。
调用图:被 1 处调用(create_spawn_agent_tool_v2);外部调用 1 个(json!)。
send_input_output_schema411–423 ↗
fn send_input_output_schema() -> Value
作用:定义 v1 send_input 成功后的返回格式。它只关心这次输入排队后的 submission_id。
数据流:进去没有输入 → 它生成一个对象规则,要求必须有 submission_id,且不允许额外字段 → 出来一段 JSON schema。
调用关系:它是 create_send_input_tool_v1 的返回格式零件。发送输入后,系统用 submission_id 追踪这次提交。
调用图:外部调用 1 个(json!)。
list_agents_output_schema425–456 ↗
fn list_agents_output_schema() -> Value
作用:定义 list_agents 的返回格式。它规定代理列表里每一项要包含名字、状态和最近收到的任务消息。
数据流:进去没有输入 → 它生成 agents 数组的规则;数组里的每个对象都要有 agent_name、agent_status、last_task_message → 出来一段 JSON schema。
调用关系:create_list_agents_tool 会调用它。它还会嵌入 agent_status_output_schema,让列表里的状态和其他工具保持同一种说法。
调用图:被 1 处调用(create_list_agents_tool);外部调用 1 个(json!)。
resume_agent_output_schema458–467 ↗
fn resume_agent_output_schema() -> Value
作用:定义 resume_agent 的返回格式。恢复代理后,调用方最需要知道的就是这个代理现在是什么状态。
数据流:进去没有输入 → 它生成一个对象规则,里面必须有 status 字段 → 出来一段 JSON schema,其中 status 使用统一的代理状态格式。
调用关系:它是 create_resume_agent_tool 的返回格式零件。恢复动作执行后,返回状态可以帮助调用方判断代理是否真的可用。
调用图:外部调用 1 个(json!)。
wait_output_schema_v1469–486 ↗
fn wait_output_schema_v1() -> Value
作用:定义 v1 wait_agent 的返回格式。它会按代理 id 给出最终状态,并说明这次是不是因为超时才返回。
数据流:进去没有输入 → 它生成 status 和 timed_out 两个必填字段;status 是一个以代理 id 为键的对象,值是统一代理状态 → 出来一段 JSON schema。
调用关系:它是 create_wait_agent_tool_v1 的返回格式零件。等待多个目标时,调用方可以从 status 里看哪些代理已经到最终状态。
调用图:外部调用 1 个(json!)。
wait_output_schema_v2488–504 ↗
fn wait_output_schema_v2() -> Value
作用:定义 v2 wait_agent 的返回格式。v2 不直接返回代理最终内容,而是返回一段简短摘要和是否超时。
数据流:进去没有输入 → 它生成 message 和 timed_out 两个必填字段 → 出来一段 JSON schema。
调用关系:create_wait_agent_tool_v2 会调用它。v2 等待工具用它来保持返回结果轻量,只告诉调用方有没有更新、是否超时。
调用图:被 1 处调用(create_wait_agent_tool_v2);外部调用 1 个(json!)。
agent_previous_status_output_schema506–518 ↗
fn agent_previous_status_output_schema(previous_status_description: &str) -> Value
作用:生成“操作前代理状态”的返回格式。关闭或打断代理时,调用方需要知道代理在被处理前是什么状态。
数据流:进去的是 previous_status_description,也就是 previous_status 字段的人类说明文字 → 它把这段说明和统一代理状态格式合成一个对象规则 → 出来一段 JSON schema。
调用关系:create_interrupt_agent_tool_v2 会调用它,create_close_agent_tool_v1 也使用同样思路。它让“关闭前状态”和“打断前状态”可以共用一套结构,只换说明文字。
调用图:被 1 处调用(create_interrupt_agent_tool_v2);外部调用 1 个(json!)。
create_collab_input_items_schema520–553 ↗
fn create_collab_input_items_schema() -> JsonSchema
作用:定义结构化输入 items 的格式。它让消息不只是一段文字,还能明确包含图片、文件路径、技能、提及对象等内容。
数据流:进去没有输入 → 它定义数组里每个输入项可有哪些字段,比如 type、text、image_url、path、name → 出来一个 JsonSchema,表示 items 是一组结构化输入项。
调用关系:create_send_input_tool_v1 和 spawn_agent_common_properties_v1 会调用它。它是旧版发送输入和创建代理时支持复杂输入内容的共同零件。
调用图:调用 3 个内部函数(array, object, string);被 2 处调用(create_send_input_tool_v1, spawn_agent_common_properties_v1);外部调用 1 个(from)。
spawn_agent_common_properties_v1555–596 ↗
fn spawn_agent_common_properties_v1(agent_type_description: &str) -> BTreeMap<String, JsonSchema>
作用:准备 v1 spawn_agent 的通用参数字段。它把创建子代理时可能用到的选项集中在一个地方,避免到处重复写。
数据流:进去的是 agent_type_description,也就是代理类型字段的说明文字 → 它生成 message、items、agent_type、fork_context、model、reasoning_effort、service_tier 等字段规则 → 出来一个字段映射表,供工具参数对象使用。
调用关系:create_spawn_agent_tool_v1 会调用它。它还会调用 create_collab_input_items_schema,把结构化输入能力加到 v1 创建代理参数里。
调用图:调用 3 个内部函数(create_collab_input_items_schema, boolean, string);被 1 处调用(create_spawn_agent_tool_v1);外部调用 1 个(from)。
spawn_agent_common_properties_v2598–638 ↗
fn spawn_agent_common_properties_v2(agent_type_description: &str) -> BTreeMap<String, JsonSchema>
作用:准备 v2 spawn_agent 的通用参数字段。它相比 v1 去掉了 items,改用加密 message 和 fork_turns 这类更明确的上下文复制选项。
数据流:进去的是 agent_type_description → 它生成 message、agent_type、fork_turns、model、reasoning_effort、service_tier 等字段规则 → 出来一个字段映射表,后面还会被加入 task_name。
调用关系:create_spawn_agent_tool_v2 会调用它。它提供基础字段,create_spawn_agent_tool_v2 再补上 v2 必需的 task_name。
调用图:调用 1 个内部函数(string);被 1 处调用(create_spawn_agent_tool_v2);外部调用 1 个(from)。
hide_spawn_agent_metadata_options640–645 ↗
fn hide_spawn_agent_metadata_options(properties: &mut BTreeMap<String, JsonSchema>)
作用:从 spawn_agent 参数里删掉高级元信息选项。这样某些场景下模型只能填写任务内容,而不能选择代理类型、模型或服务档位。
数据流:进去的是可修改的参数字段表 → 它删除 agent_type、model、reasoning_effort、service_tier 四个字段 → 出来没有新对象,但原来的字段表被改小了。
调用关系:create_spawn_agent_tool_v1 和 create_spawn_agent_tool_v2 会在配置要求隐藏元信息时调用它。它像菜单上的遮盖贴纸,把不该点的高级选项盖掉。
调用图:被 2 处调用(create_spawn_agent_tool_v1, create_spawn_agent_tool_v2)。
spawn_agent_tool_description647–709 ↗
fn spawn_agent_tool_description(
available_models_description: Option<&str>,
inherited_model_guidance: Option<&str>,
return_value_description: &str,
include_usage_hint: bool,
usage
作用:生成 v1 spawn_agent 的说明文字。它把模型提示、继承模型规则、返回值说明和可选使用指南拼成一段给模型看的说明。
数据流:进去的是可用模型说明、是否继承模型的提醒、返回值说明、是否包含使用提示、以及自定义提示文字 → 它按条件拼接文本;如果有自定义提示就用自定义的,否则可能加入默认的委派规则 → 出来一段 String 说明文字。
调用关系:它服务于 v1 创建代理工具。create_spawn_agent_tool_v1 会用它写出工具 description,让模型知道什么时候该创建子代理、什么时候不该乱创建。
调用图:外部调用 1 个(format!)。
spawn_agent_tool_description_v2711–745 ↗
fn spawn_agent_tool_description_v2(
available_models_description: Option<&str>,
inherited_model_guidance: Option<&str>,
include_usage_hint: bool,
usage_hint_text: Option<String>,
) ->
作用:生成 v2 spawn_agent 的说明文字。它重点解释 task_name 的路径规则、子代理能力、模型继承和 fork_turns 的影响。
数据流:进去的是模型说明、继承模型提醒、是否包含使用提示、以及自定义提示文字 → 它拼出 v2 的工具说明;如果要求包含并提供了自定义提示,就追加进去 → 出来一段 String。
调用关系:create_spawn_agent_tool_v2 会调用它。它把 v2 的关键行为写清楚,尤其是相对任务名和规范任务名怎样互相引用。
调用图:被 1 处调用(create_spawn_agent_tool_v2);外部调用 1 个(format!)。
spawn_agent_models_description747–808 ↗
fn spawn_agent_models_description(models: &[ModelPreset]) -> String
作用:把可用模型列表整理成一段简短说明,告诉模型哪些模型可以作为覆盖选项。它会限制展示数量,避免说明文字太长。
数据流:进去的是 ModelPreset 列表 → 它筛出 show_in_picker 为真的模型,最多取 5 个;为每个模型写出名称、描述、推理力度选项和服务档位 → 出来一段可读的模型覆盖说明文字;如果没有可展示模型,就返回“没有可见模型覆盖”。
调用关系:创建 spawn_agent 工具说明时会用到它。它不决定真正能不能创建代理,只负责把可选模型信息写进工具描述里。
wait_agent_tool_parameters_v1810–836 ↗
fn wait_agent_tool_parameters_v1(options: WaitAgentTimeoutOptions) -> JsonSchema
作用:生成 v1 wait_agent 的参数规则。v1 要求明确给出要等待的代理列表。
数据流:进去的是 WaitAgentTimeoutOptions → 它生成 targets 数组和 timeout_ms 数字字段;timeout_ms 的说明里写入默认、最小、最大值 → 出来一个 JsonSchema 对象,并把 targets 标为必填。
调用关系:create_wait_agent_tool_v1 会用它来描述等待工具的入参。它把等待时间配置变成模型能看懂、也能被校验的参数说明。
调用图:调用 4 个内部函数(array, number, object, string);外部调用 3 个(from, format!, vec!)。
wait_agent_tool_parameters_v2838–848 ↗
fn wait_agent_tool_parameters_v2(options: WaitAgentTimeoutOptions) -> JsonSchema
作用:生成 v2 wait_agent 的参数规则。v2 不需要指定目标,只需要可选的超时时间。
数据流:进去的是 WaitAgentTimeoutOptions → 它生成 timeout_ms 一个可选数字字段,并把默认、最小、最大值写进说明 → 出来一个 JsonSchema 对象。
调用关系:create_wait_agent_tool_v2 会调用它。它体现了 v2 等待方式的变化:不是等某几个指定代理,而是等任意邮箱更新或中断事件。
调用图:调用 2 个内部函数(number, object);被 1 处调用(create_wait_agent_tool_v2);外部调用 2 个(from, format!)。
core/src/tools/handlers/multi_agents_common.rs源码 ↗
多代理协作可以理解成:一个主代理临时叫来几个“帮手代理”一起干活。这个文件不直接完成某个任务,而是给这些协作工具打地基。它会检查用户到底是发了一段文字,还是发了一组输入项;会把工具结果包装成模型能读懂的格式;会把底层错误翻译成模型和用户能理解的话;还会在创建或恢复子代理时,复制父代理当前正在使用的模型、权限、沙盒、工作目录等运行时设置。这里特别重要的一点是:配置不是简单复制旧文件配置,因为真实运行中模型、审批规则、目录等可能已经变了。这个文件就像“开新分店前的检查清单”,确保新代理拿到的身份、权限和资源都对,不会因为过期配置跑偏。
function_arguments34–41 ↗
fn function_arguments(payload: ToolPayload) -> Result<String, FunctionCallError>
作用:从工具调用的载荷里取出函数参数。它的作用是确认收到的确实是“函数工具”的输入,而不是别的格式。
数据流:进去的是一个 ToolPayload,也就是工具收到的一包数据。函数检查它是不是 Function 类型;如果是,就拿出里面的 arguments 字符串返回;如果不是,就生成一个 FunctionCallError,告诉模型这个协作处理器收到了不支持的载荷。
调用关系:这是协作工具处理请求时靠前的一道门卫。后面的解析逻辑只有在它确认输入格式正确后,才会继续工作;出错时它通过 RespondToModel 把问题反馈给模型。
调用图:外部调用 1 个(RespondToModel)。
tool_output_json_text43–50 ↗
fn tool_output_json_text(value: &T, tool_name: &str) -> String
作用:把任意可序列化的结果变成 JSON 文本。JSON 可以理解成一种通用的数据包装格式,方便模型和前端读取。
数据流:进去的是一个结果值和工具名。函数尝试用 serde_json 把结果转成字符串;成功时出来的是 JSON 字符串;失败时不会崩溃,而是返回一段 JSON 字符串,内容说明哪个工具的结果序列化失败以及原因。
调用关系:它是工具输出的基础包装器。tool_output_response_item 会调用它,先把结果变成文本,再放进模型回复项里。
调用图:被 1 处调用(tool_output_response_item);外部调用 1 个(to_string)。
tool_output_response_item52–64 ↗
fn tool_output_response_item(
call_id: &str,
payload: &ToolPayload,
value: &T,
success: Option<bool>,
tool_name: &str,
) -> ResponseInputItem
作用:把工具执行结果包装成模型下一步能接着读取的响应项。它相当于把“工具结果”装进标准信封。
数据流:进去的是调用编号 call_id、原始工具载荷 payload、要返回的结果 value、成功标记 success 和工具名。它先调用 tool_output_json_text 把结果变成 JSON 文本,再用 FunctionToolOutput::from_text 做成工具输出,最后转成 ResponseInputItem 返回。
调用关系:它把底层结果和模型对话流程接起来。协作工具执行完后会用这种标准响应项把结果交回给模型。
调用图:调用 2 个内部函数(from_text, tool_output_json_text)。
tool_output_code_mode_result66–73 ↗
fn tool_output_code_mode_result(value: &T, tool_name: &str) -> JsonValue
作用:把工具结果变成 JSON 值,而不是 JSON 文本。它适合需要直接拿结构化数据继续处理的场景。
数据流:进去的是一个可序列化的结果和工具名。函数尝试把结果转成 serde_json 的 JsonValue;成功就返回结构化 JSON 值;失败就返回一个字符串型 JSON 值,里面写明序列化失败的原因。
调用关系:它和 tool_output_json_text 类似,但输出形态不同。一个给文本响应用,一个给代码模式或结构化结果用。
调用图:外部调用 1 个(to_value)。
build_wait_agent_statuses75–110 ↗
fn build_wait_agent_statuses(
statuses: &HashMap<ThreadId, AgentStatus>,
receiver_agents: &[CollabAgentRef],
) -> Vec<CollabAgentStatusEntry>
作用:整理一批子代理的状态,让等待工具可以清楚地告诉模型:哪些代理现在在忙、完成了或出了问题。
数据流:进去的是按线程 ID 保存的状态表,以及接收方代理列表。函数先按接收方列表的顺序补上昵称和角色,再把状态表里那些不在接收方列表中的额外代理也加进去,并按线程 ID 排序。出来的是 CollabAgentStatusEntry 列表。
调用关系:它通常服务于“等待多个代理”的流程。等待逻辑拿到原始状态后,用它整理成更像报告的格式,方便模型理解每个代理是谁、在什么状态。
调用图:外部调用 4 个(with_capacity, new, with_capacity, len)。
collab_spawn_error112–120 ↗
fn collab_spawn_error(err: CodexErr) -> FunctionCallError
作用:把创建协作代理时的底层错误,翻译成模型能理解的错误消息。
数据流:进去的是 CodexErr,也就是系统内部错误。函数根据错误种类改写消息:如果线程管理器没了,就说协作管理器不可用;如果是不支持的操作,就直接说明;其他情况则包装成“创建协作代理失败”。出来的是 FunctionCallError。
调用关系:它出现在 spawn,也就是创建新子代理失败的时候。它不修复错误,而是把生硬的内部错误变成适合反馈给模型的说法。
调用图:外部调用 2 个(format!, RespondToModel)。
collab_agent_error122–135 ↗
fn collab_agent_error(agent_id: ThreadId, err: CodexErr) -> FunctionCallError
作用:把针对某个协作代理操作失败的错误,翻译成清楚的人话。
数据流:进去的是代理线程 ID 和 CodexErr。函数判断是代理不存在、代理已关闭、协作管理器不可用,还是其他错误;然后生成对应的 FunctionCallError。出来的是可以直接反馈给模型的错误。
调用关系:当工具要给某个子代理发消息、等待它、或读取它状态但失败时,会用这个函数统一表达错误,避免不同工具说法不一致。
调用图:外部调用 2 个(format!, RespondToModel)。
thread_spawn_source137–161 ↗
fn thread_spawn_source(
parent_thread_id: ThreadId,
parent_session_source: &SessionSource,
depth: i32,
agent_role: Option<&str>,
task_name: Option<String>,
) -> Result<SessionSourc
作用:为新创建的子代理生成“来源记录”。这份记录说明它是从哪个父线程生出来的、层级多深、扮演什么角色。
数据流:进去的是父线程 ID、父会话来源、深度、可选角色和可选任务名。函数会尝试把任务名接到父代理路径后面,形成新的代理路径;如果路径非法,就返回错误;成功时输出一个 SessionSource::SubAgent。
调用关系:创建子代理时需要知道它的来历。这个函数把这些来历信息组装好,再交给后续创建线程或会话的流程使用。
调用图:外部调用 1 个(SubAgent)。
parse_collab_input163–195 ↗
fn parse_collab_input(
message: Option<String>,
items: Option<Vec<UserInput>>,
) -> Result<Op, FunctionCallError>
作用:检查并转换要发给协作代理的输入。它保证调用者只能用一种输入方式:要么一段 message,要么一组 items。
数据流:进去的是可选 message 和可选 items。函数会拒绝两者都给、两者都不给、空 message、空 items;如果是 message,就包成一个文本 UserInput;如果是 items,就直接转成 Op。出来的是可发送给代理的 Op,或者一条清楚的错误。
调用关系:这是给子代理发消息前的输入检查器。它把外部传入的松散参数整理成系统内部统一的操作格式。
调用图:外部调用 2 个(RespondToModel, vec!)。
build_agent_spawn_config204–211 ↗
fn build_agent_spawn_config(
base_instructions: &BaseInstructions,
turn: &TurnContext,
) -> Result<Config, FunctionCallError>
作用:为新创建的子代理准备配置。它会从父代理当前状态出发,并写入新子代理要使用的基础指令。
数据流:进去的是 BaseInstructions 和当前 TurnContext。函数先调用 build_agent_shared_config 复制并刷新共享配置,然后把 base_instructions 填成传入的基础指令文本。出来的是可用于创建子代理的 Config。
调用关系:它是 spawn 子代理时的配置入口。真正复制运行时设置的活交给 build_agent_shared_config,它自己负责补上新代理的基础指令。
调用图:调用 1 个内部函数(build_agent_shared_config)。
build_agent_resume_config213–218 ↗
fn build_agent_resume_config(turn: &TurnContext) -> Result<Config, FunctionCallError>
作用:为恢复已有子代理准备配置。和新建不同,它不会强行覆盖基础指令,而是让基础指令继续来自原来的会话记录。
数据流:进去的是当前 TurnContext。函数调用 build_agent_shared_config 得到刷新后的配置,然后把 base_instructions 设为 None。出来的是恢复代理用的 Config。
调用关系:它是 resume,也就是恢复代理时的配置入口。它和 build_agent_spawn_config 共用 build_agent_shared_config,但在基础指令处理上故意不同。
调用图:调用 1 个内部函数(build_agent_shared_config)。
reject_full_fork_spawn_overrides237–248 ↗
fn reject_full_fork_spawn_overrides(
agent_type: Option<&str>,
model: Option<&str>,
reasoning_effort: Option<ReasoningEffort>,
) -> Result<(), FunctionCallError>
作用:在“完整历史分叉”创建代理时,禁止调用者改代理类型、模型或推理强度。完整历史分叉的意思是新代理继承父代理完整上下文,像从同一本账本复制出一个分支。
数据流:进去的是可选 agent_type、model 和 reasoning_effort。只要其中任何一个被提供,函数就返回错误,说明完整历史分叉必须继承父代理这些设置;如果都没提供,就返回成功。
调用关系:它是创建 full-history fork 子代理前的规则检查。这样可以避免一边说继承完整历史,一边又偷偷换模型或代理类型造成行为不一致。
调用图:外部调用 1 个(RespondToModel)。
apply_spawn_agent_runtime_overrides254–278 ↗
fn apply_spawn_agent_runtime_overrides(
config: &mut Config,
turn: &TurnContext,
) -> Result<(), FunctionCallError>
作用:把当前回合才知道的运行时设置写进子代理配置。运行时设置包括审批策略、命令环境规则、沙盒程序、当前工作目录和权限档位。
数据流:进去的是可修改的 Config 和 TurnContext。函数从 turn 读取审批策略、审批人、shell 环境策略、Linux 沙盒路径、当前目录和权限配置,并写入 config;如果审批策略或权限档位无效,就返回错误。出来时 config 已被更新。
调用关系:build_agent_shared_config 会调用它。它专门处理那些不能只靠旧配置文件复制的字段,保证子代理和父代理当前运行环境一致。
调用图:调用 1 个内部函数(permission_profile);被 1 处调用(build_agent_shared_config)。
apply_requested_spawn_agent_model_overrides280–329 ↗
async fn apply_requested_spawn_agent_model_overrides(
session: &Session,
turn: &TurnContext,
config: &mut Config,
requested_model: Option<&str>,
requested_reasoning_effort: Option<
作用:处理创建子代理时用户要求的模型或推理强度。推理强度可以理解成模型“想得多细”的档位。
数据流:进去的是 Session、TurnContext、可修改 Config、可选模型名和可选推理强度。如果没有请求就什么也不改;如果请求了模型,它先离线列出可用模型,用 find_spawn_agent_model_name 确认可用,再取模型信息,并验证推理强度是否支持;如果只请求推理强度,就用父代理当前模型的信息验证。出来时 config 里的模型和推理强度可能已更新。
调用关系:它在创建子代理、套用调用者指定模型参数时使用。它把模型名查找交给 find_spawn_agent_model_name,把推理强度合法性检查交给 validate_spawn_agent_reasoning_effort,并通过 models_manager 读取模型信息。
调用图:调用 2 个内部函数(find_spawn_agent_model_name, validate_spawn_agent_reasoning_effort);外部调用 1 个(to_models_manager_config)。
apply_spawn_agent_service_tier331–384 ↗
async fn apply_spawn_agent_service_tier(
session: &Session,
config: &mut Config,
parent_service_tier: Option<&str>,
requested_service_tier: Option<&str>,
) -> Result<(), FunctionCallEr
作用:为子代理选择并验证服务档位。服务档位可以理解成同一个模型的不同服务等级,比如更快或更普通的通道。
数据流:进去的是 Session、可修改 Config、父代理服务档位和请求的服务档位。函数先收集候选档位:配置里已有的、调用者请求的、父代理继承的;如果都没有,就清空 service_tier。否则它读取当前模型信息,确认请求档位是否被该模型支持,再从候选中挑第一个被支持的写回 config。
调用关系:它在子代理配置快完成时运行,用 models_manager 查询模型支持哪些服务档位。若请求了不支持的档位,它会通过 RespondToModel 返回清楚错误。
调用图:外部调用 3 个(to_models_manager_config, format!, RespondToModel)。
find_spawn_agent_model_name386–404 ↗
fn find_spawn_agent_model_name(
available_models: &[codex_protocol::openai_models::ModelPreset],
requested_model: &str,
) -> Result<String, FunctionCallError>
作用:确认创建子代理时请求的模型名是否在可用模型列表里。
数据流:进去的是可用模型列表和请求的模型名。函数遍历列表寻找完全匹配的模型;找到就返回规范模型名;找不到就把所有可用模型名拼出来,返回“未知模型”的错误。
调用关系:apply_requested_spawn_agent_model_overrides 会调用它。它负责第一步把模型名查准,后续才会继续读取模型详情和设置配置。
调用图:被 1 处调用(apply_requested_spawn_agent_model_overrides);外部调用 1 个(iter)。
validate_spawn_agent_reasoning_effort406–426 ↗
fn validate_spawn_agent_reasoning_effort(
model: &str,
supported_reasoning_levels: &[ReasoningEffortPreset],
requested_reasoning_effort: &ReasoningEffort,
) -> Result<(), FunctionCallError
作用:检查某个模型是否支持请求的推理强度,避免给模型一个它不认识的档位。
数据流:进去的是模型名、该模型支持的推理强度列表,以及请求的推理强度。函数遍历支持列表;如果找到匹配项就成功返回;否则把支持的档位列出来,返回一条说明不支持的错误。
调用关系:apply_requested_spawn_agent_model_overrides 会在设置模型推理强度前调用它。它像一道安全闸,防止错误配置流进真正的子代理运行流程。
调用图:被 1 处调用(apply_requested_spawn_agent_model_overrides);外部调用 3 个(format!, iter, RespondToModel)。
core/src/tools/handlers/multi_agents.rs源码 ↗
这个文件解决的是“一个代理不够用时,怎么安全地叫来子代理帮忙”的问题。可以把主代理想成项目负责人,子代理像临时加入的小组成员。这个文件本身不把所有活儿都做完,而是搭了一个门面:它统一暴露 Spawn、SendInput、Wait、Resume、Close 这些处理器,让外部只需要从这里找到多代理工具。它还提供几个小工具函数,用来把模型传来的代理编号字符串转成系统真正认识的 ThreadId(线程/会话标识),避免把错误编号传进内部造成混乱。另外,它给工具搜索结果加上“Multi-agent tools”这个来源说明,让系统在展示或查找工具时能知道这些工具是用来生成和管理子代理的。重要的是,子代理会跟随创建它的那一轮对话配置,并继承运行时状态,比如当前目录、沙箱和审批策略,这样它们不会脱离主流程乱跑。
parse_agent_id_target47–51 ↗
fn parse_agent_id_target(target: &str) -> Result<ThreadId, FunctionCallError>
作用:把模型给出的单个代理编号文字,转换成系统内部真正能用的 ThreadId。这样后续操作不会拿一段随便的字符串去找代理。
数据流:输入是一段代理编号字符串。它把这段字符串交给 ThreadId::from_string 检查和转换;如果格式正确,就得到一个 ThreadId;如果格式不对,就返回一个可以直接告诉模型的错误信息,说明这个代理编号无效。
调用关系:它位于多代理工具处理流程的入口检查阶段。凡是某个工具要指定一个已有子代理时,都会先需要这种转换;真正的字符串解析工作交给 ThreadId::from_string,它自己负责把错误包装成模型能理解的说法。
调用图:调用 1 个内部函数(from_string)。
parse_agent_id_targets53–66 ↗
fn parse_agent_id_targets(
targets: Vec<String>,
) -> Result<Vec<ThreadId>, FunctionCallError>
作用:把一组代理编号文字,批量转换成系统内部的 ThreadId 列表。它适合那些一次要等待、关闭或操作多个子代理的工具。
数据流:输入是一组字符串。如果这组字符串为空,它直接返回错误,告诉模型“agent ids must be non-empty”。如果不为空,它逐个处理这些编号;全部成功时输出 ThreadId 列表;中间任何一个编号不合法,就把错误返回出去,不继续假装成功。
调用关系:它是 parse_agent_id_target 的批量版本,处在多代理工具真正执行前的参数校验位置。遇到空列表时,它会用 FunctionCallError::RespondToModel 生成一条能反馈给模型的错误,避免后面的处理器收到没有目标的请求。
调用图:外部调用 1 个(RespondToModel)。
multi_agent_tool_search_info68–80 ↗
fn multi_agent_tool_search_info(
search_text: &str,
spec: codex_tools::ToolSpec,
) -> Option<ToolSearchInfo>
作用:为某个多代理工具生成一条“可被搜索和展示”的说明信息。这样系统或模型查工具时,能知道这个工具来自“Multi-agent tools”,用途是创建和管理子代理。
数据流:输入是搜索文字和一个工具规格 ToolSpec(工具的定义说明)。它把这些信息加上固定的来源名称和描述,然后交给 ToolSearchInfo::from_spec 生成搜索信息;结果可能是一条可用的 ToolSearchInfo,也可能因为规格不合适而没有结果。
调用关系:它服务于工具注册和工具搜索阶段,而不是具体执行某个子代理动作。它把包装搜索说明这件事交给 ToolSearchInfo::from_spec,自己主要负责补上多代理工具专属的来源标签。
调用图:调用 1 个内部函数(from_spec)。
core/src/tools/handlers/multi_agents/spawn.rs源码 ↗
这个文件可以理解成“派工按钮”的后台实现。模型调用 spawn_agent 时,这里会先读懂请求:要给新智能体什么任务、要不要指定角色、模型、推理强度,是否完整继承当前上下文。接着它会检查一件很重要的事:当前已经派生了多少层子智能体,超过上限就拒绝,防止像俄罗斯套娃一样无限开下去。通过检查后,它会发出“开始创建”的事件,准备新智能体的配置,套用角色和运行时设置,再调用 agent_control 真正创建新线程。创建完成或失败后,它还会发出“结束创建”的事件,记录新智能体的编号、昵称、角色、模型和状态。最后返回一个很小的结果:新智能体的 agent_id 和昵称。这个文件还把结果包装成日志、响应消息和代码模式能看懂的格式。
Handler::new20–22 ↗
fn new(options: SpawnAgentToolOptions) -> Self
作用:创建一个 spawn_agent 工具处理器,并把工具选项保存进去。外部注册工具时会用它来得到一个可执行的处理器。
数据流:进去的是 SpawnAgentToolOptions,也就是这个工具有哪些可配置选项;函数把这些选项放进 Handler 结构里;出来的是一个新的 Handler,之后可以被工具系统调用。
调用关系:它是这个处理器的入口式构造函数。创建好 Handler 后,工具系统会再调用它的 tool_name、spec、handle 等方法来识别和执行 spawn_agent。
Handler::tool_name26–28 ↗
fn tool_name(&self) -> ToolName
作用:告诉工具系统:这个处理器对应的工具名字叫 spawn_agent,而且属于多智能体工具的命名空间。命名空间可以理解成给工具名加一个分类前缀,避免重名。
数据流:它不需要外部输入,只读取固定的命名空间和工具短名;然后调用 namespaced 拼出完整工具名;返回给工具系统用于匹配调用。
调用关系:工具系统在登记或查找工具时会问它叫什么。这里把名字交给 namespaced 生成标准格式,保证和其他多智能体工具放在同一类里。
调用图:调用 1 个内部函数(namespaced)。
Handler::spec30–32 ↗
fn spec(&self) -> ToolSpec
作用:生成 spawn_agent 的工具说明书。这个说明书告诉模型:这个工具能接收哪些参数、参数长什么样、怎么调用。
数据流:进去的是 Handler 里保存的 options;函数先复制一份 options,避免改动原件;再交给 create_spawn_agent_tool_v1 生成 ToolSpec;出来的是工具规格说明。
调用关系:Handler::search_info 会调用它,把工具说明和搜索关键词放在一起。工具注册阶段也会依赖这类规格,让模型知道 spawn_agent 怎么用。
调用图:调用 1 个内部函数(create_spawn_agent_tool_v1);被 1 处调用(search_info);外部调用 1 个(clone)。
Handler::search_info34–39 ↗
fn search_info(&self) -> Option<ToolSearchInfo>
作用:提供这项工具的搜索线索,让系统在需要“派子智能体、并行工作、委派任务”时更容易找到 spawn_agent。
数据流:它准备一串和派生智能体有关的关键词;再调用 Handler::spec 拿到工具说明;最后把关键词和说明合成 ToolSearchInfo 返回,如果系统支持搜索就能用上。
调用关系:它站在工具发现流程里。它自己不执行任务,而是调用 spec 拿说明书,再把这份说明书配上关键词,方便上层把正确工具推荐给模型。
调用图:调用 1 个内部函数(spec)。
Handler::handle41–43 ↗
fn handle(&self, invocation: ToolInvocation) -> codex_tools::ToolExecutorFuture<'_>
作用:真正接住一次 spawn_agent 工具调用,并把它转交给异步函数 handle_spawn_agent 去执行。异步函数可以理解成“可能要等一会儿”的函数,因为创建智能体会涉及后台服务。
数据流:进去的是一次 ToolInvocation,里面有会话、当前轮次、参数和调用编号;函数把处理过程包进一个异步任务;出来的是一个未来会完成的结果,成功时会被包装成统一的工具输出。
调用关系:工具系统触发 spawn_agent 时会走到这里。它本身只是薄薄的一层转接,把主要工作交给 handle_spawn_agent,并用 pin 固定异步任务的位置,方便运行时安全地等待它完成。
调用图:调用 1 个内部函数(handle_spawn_agent);外部调用 1 个(pin)。
handle_spawn_agent46–210 ↗
async fn handle_spawn_agent(
invocation: ToolInvocation,
) -> Result<SpawnAgentResult, FunctionCallError>
作用:这是本文件的核心流程:检查并创建一个子智能体,然后把创建结果返回给模型。它负责防止开太深、套用角色和模型设置、发送开始/结束事件,并记录统计数据。
数据流:进去的是 ToolInvocation,里面包含当前会话、当前轮次、模型传来的参数和 call_id;函数先解析参数,整理给子智能体的任务内容,并用 render_input_preview 做一份可展示的任务预览;然后用 next_thread_spawn_depth 算下一层深度,用 exceeds_thread_spawn_depth_limit 检查是否超过上限,超过就用 RespondToModel 返回一句让模型自己完成任务的话;如果允许创建,它发送开始事件,构造子智能体配置,按需要套用角色 apply_role_to_config、模型、推理强度、服务档位和运行时设置;接着调用后台的 agent_control 创建子智能体;最后查询或整理新智能体的元信息,发送结束事件,记录遥测计数,返回 SpawnAgentResult,里面有新 agent_id 和可选昵称。
调用关系:Handler::handle 会把工具调用交给它。它是整个 spawn_agent 的调度中心:向下调用 render_input_preview 准备展示文本,调用深度检查函数防止无限派生,必要时调用 apply_role_to_config 套用角色,调用 now_unix_timestamp_ms 给事件打时间戳,并把真正创建智能体的工作交给 agent_control。
调用图:调用 3 个内部函数(render_input_preview, apply_role_to_config, now_unix_timestamp_ms);被 1 处调用(handle);外部调用 4 个(pin, exceeds_thread_spawn_depth_limit, next_thread_spawn_depth, RespondToModel)。
Handler::matches_kind213–215 ↗
fn matches_kind(&self, payload: &ToolPayload) -> bool
作用:判断传进来的工具载荷是不是函数调用格式。spawn_agent 只接受函数式工具调用,不处理别的载荷类型。
数据流:进去的是 ToolPayload;函数用 matches! 检查它是不是 Function 这一类;出来的是 true 或 false,告诉运行时这个处理器能不能接这次调用。
调用关系:它在工具运行时分派之前起过滤作用。只有载荷类型匹配时,运行时才会继续让 Handler::handle 处理,否则这次调用会交给别的处理器或被跳过。
调用图:外部调用 1 个(matches!)。
SpawnAgentResult::log_preview237–239 ↗
fn log_preview(&self) -> String
作用:把创建子智能体的结果变成适合写进日志的一小段文本。这样排查问题时能看到 spawn_agent 返回了什么。
数据流:进去的是 SpawnAgentResult,也就是 agent_id 和昵称;函数把它按 spawn_agent 的名字包装成 JSON 文本;出来的是一段可读的日志预览字符串,不改动原结果。
调用关系:工具执行成功后,日志系统会用它生成记录。它不参与创建智能体,只负责把结果翻译成日志友好的样子。
SpawnAgentResult::success_for_logging241–243 ↗
fn success_for_logging(&self) -> bool
作用:告诉日志系统:只要拿到了 SpawnAgentResult,就可以按成功来记录。这个结果类型本身代表工具已经成功返回。
数据流:它不读取外部输入,也不改动数据;直接返回 true;日志侧据此把这次工具输出标成成功。
调用关系:它是 ToolOutput 接口的一部分。创建流程完成后,统一的工具日志机制会询问这个方法,决定日志里的成功标记。
SpawnAgentResult::to_response_item245–247 ↗
fn to_response_item(&self, call_id: &str, payload: &ToolPayload) -> ResponseInputItem
作用:把创建结果转换成要发回模型的响应项。模型需要这种统一格式,才能知道工具调用已经结束,以及新智能体的编号是什么。
数据流:进去的是 call_id、原始工具载荷和 SpawnAgentResult;函数把这些信息合成 ResponseInputItem,并标明这是 spawn_agent 的成功输出;出来的是可以放回对话流里的响应对象。
调用关系:它位于工具执行之后、结果返回模型之前。统一工具框架会调用它,把普通 Rust 结构体转换成模型协议能接收的消息。
SpawnAgentResult::code_mode_result249–251 ↗
fn code_mode_result(&self, _payload: &ToolPayload) -> JsonValue
作用:把创建结果转换成代码模式能使用的 JSON 值。这样如果上层以结构化方式读取结果,也能直接拿到 agent_id 和昵称。
数据流:进去的是 SpawnAgentResult 和工具载荷;函数把结果按 spawn_agent 的格式转成 JsonValue;出来的是结构化 JSON,不改变原数据。
调用关系:它也是 ToolOutput 接口的一部分。普通对话返回会用 to_response_item,而代码模式或自动化流程需要结构化结果时会用这个方法。
core/src/tools/handlers/multi_agents/send_input.rs源码 ↗
这个文件像一个“转交便条的前台”。外部调用 send_input 工具时,它先看清楚便条要交给谁,也就是目标代理的编号;再把文字消息或更结构化的输入项整理成代理能读懂的格式。发送前,它会生成一段预览,方便系统日志和界面知道这次交互大概说了什么。如果目标代理已经有记录,它会确保这个代理已经被加载起来;如果调用方要求 interrupt,它还会先打断目标代理当前正在做的事。然后它会发出“交互开始”的事件,真正把输入送到目标代理,再查询目标代理的新状态,最后发出“交互结束”的事件,并把 submission_id 返回给调用方。这个 submission_id 可以理解成“投递回执号”,证明这次输入已经被系统接收。
Handler::tool_name10–12 ↗
fn tool_name(&self) -> ToolName
作用:告诉系统这个处理器对应哪个工具名。这里返回的是带命名空间的 multi-agent send_input,避免和别的同名工具混在一起。
数据流:进去的是这个处理器本身 → 它把固定的命名空间和工具短名“send_input”拼成一个正式工具名 → 出来的是系统可识别的 ToolName,没有改动外部状态。
调用关系:当工具系统登记或匹配工具时会问 Handler 的名字。它把生成名字这件事交给 namespaced,确保名字格式统一。
调用图:调用 1 个内部函数(namespaced)。
Handler::spec14–16 ↗
fn spec(&self) -> ToolSpec
作用:返回 send_input 这个工具的说明书。说明书会告诉模型或调用方:这个工具需要哪些参数、参数长什么样。
数据流:进去的是这个处理器本身 → 它调用 create_send_input_tool_v1 生成工具规格 → 出来的是 ToolSpec,也就是工具的参数和描述定义。
调用关系:工具注册时会用它拿到规格;Handler::search_info 也会调用它,把同一份规格拿去做搜索索引,避免说明书写两份导致不一致。
调用图:调用 1 个内部函数(create_send_input_tool_v1);被 1 处调用(search_info)。
Handler::search_info18–23 ↗
fn search_info(&self) -> Option<ToolSearchInfo>
作用:给工具搜索系统提供一些关键词和工具说明。这样当系统想找“给子代理发消息、继续任务、打断任务”这类能力时,能找到 send_input。
数据流:进去的是这个处理器本身 → 它准备一串和发送输入有关的关键词,并通过 Handler::spec 拿到工具说明 → 出来的是可选的 ToolSearchInfo,用于工具发现和检索。
调用关系:它依赖 Handler::spec 提供正式工具规格。整体上,它不是执行发送动作,而是帮助系统在需要时找到这个工具。
调用图:调用 1 个内部函数(spec)。
Handler::handle25–27 ↗
fn handle(&self, invocation: ToolInvocation) -> codex_tools::ToolExecutorFuture<'_>
作用:这是工具真正被调用时的入口。它把一次工具调用包装成异步任务,让系统可以等待发送过程完成。
数据流:进去的是 ToolInvocation,也就是一次工具调用的上下文、参数和调用编号 → 它调用 Handler::handle_call 做实际工作,并用 pin 把异步任务固定成系统需要的形式 → 出来的是一个未来会完成的结果。
调用关系:工具运行时会先进入 Handler::handle。它本身不做细活,而是把活交给 Handler::handle_call;pin 是为了适配 Rust 异步执行需要的包装形式。
调用图:调用 1 个内部函数(handle_call);外部调用 1 个(pin)。
Handler::handle_call31–111 ↗
async fn handle_call(
&self,
invocation: ToolInvocation,
) -> Result<Box<dyn crate::tools::context::ToolOutput>, FunctionCallError>
作用:这是 send_input 的核心流程:解析参数、找到目标代理、必要时加载和打断它、发送输入、记录开始和结束事件,最后返回投递回执号。
数据流:进去的是一次完整的工具调用,里面有会话、当前轮次、原始参数和 call_id → 它把参数解析成目标代理、消息内容和是否打断;把输入整理成代理可接收的项目;用 render_input_preview 生成预览;检查并加载目标代理;按需打断;用 now_unix_timestamp_ms 给开始和结束事件打时间戳;调用 agent_control 发送输入并查询状态 → 出来的是 SendInputResult,里面带 submission_id;同时系统事件流里多了“交互开始”和“交互结束”两条记录,目标代理也收到了新输入。
调用关系:它由 Handler::handle 调用,是整个文件里真正干活的函数。它会借助 render_input_preview 做日志预览,借助 now_unix_timestamp_ms 标记时间,并把加载、打断、发送、查状态这些实际动作交给 session.services.agent_control。
调用图:调用 2 个内部函数(render_input_preview, now_unix_timestamp_ms);被 1 处调用(handle)。
Handler::matches_kind115–117 ↗
fn matches_kind(&self, payload: &ToolPayload) -> bool
作用:判断某个工具载荷是不是这个处理器能处理的类型。这里它只接受函数调用形式的载荷。
数据流:进去的是一个 ToolPayload,也就是工具调用携带的数据外壳 → 它检查这个外壳是不是 Function 类型 → 出来的是 true 或 false,不改变任何数据。
调用关系:工具运行时在分派请求前会用它做一道门禁。它用 matches! 这个 Rust 匹配宏做快速判断,符合的请求才会继续交给 Handler::handle。
调用图:外部调用 1 个(matches!)。
SendInputResult::log_preview135–137 ↗
fn log_preview(&self) -> String
作用:把发送结果变成适合写进日志的一小段文本。这样排查问题时,能看到 send_input 返回了什么。
数据流:进去的是 SendInputResult,里面主要是 submission_id → 它把结果按 send_input 的名字包装成 JSON 风格的日志文本 → 出来的是一段字符串,不改变结果本身。
调用关系:工具执行完后,日志系统会用它生成可读记录。它属于 SendInputResult 对 ToolOutput 接口的实现,让这个结果能被统一记录。
SendInputResult::success_for_logging139–141 ↗
fn success_for_logging(&self) -> bool
作用:告诉日志系统:只要拿到了这个结果,就应该按成功记录。这里固定返回 true。
数据流:进去的是 SendInputResult → 它不检查更多内容,直接认为这是成功结果 → 出来的是 true,不改变任何状态。
调用关系:工具框架在写日志时会问结果是否成功。因为错误在生成 SendInputResult 之前就会被返回,所以走到这里就表示 send_input 已经成功接收。
SendInputResult::to_response_item143–145 ↗
fn to_response_item(&self, call_id: &str, payload: &ToolPayload) -> ResponseInputItem
作用:把内部结果转换成可以发回给调用方的响应项。调用方不需要懂内部结构,只会收到标准格式的工具输出。
数据流:进去的是 call_id、原始 ToolPayload 和 SendInputResult → 它把这些信息打包成 ResponseInputItem,并标记这次工具调用成功 → 出来的是一条标准响应项。
调用关系:工具框架准备回复模型或客户端时会用它。它把具体的 SendInputResult 接到统一的响应管道上,让不同工具的输出格式保持一致。
SendInputResult::code_mode_result147–149 ↗
fn code_mode_result(&self, _payload: &ToolPayload) -> JsonValue
作用:把结果转换成代码模式更方便读取的 JSON 值。代码模式可以理解成更偏机器读取的输出形式。
数据流:进去的是 SendInputResult 和原始 ToolPayload → 它忽略这里不需要的 payload,把结果按 send_input 的名字包装成 JSON 值 → 出来的是 JsonValue,不改变原始结果。
调用关系:当系统需要给代码模式返回结构化结果时会调用它。它和 log_preview、to_response_item 一样,都是让同一个发送结果适配不同输出场景。
core/src/tools/handlers/multi_agents/wait.rs源码 ↗
这个文件像一个“等人回信”的前台接待员。主代理调用 wait_agent 时,它先读懂请求里要等哪些代理、最多等多久;然后通知系统“我开始等了”,再去订阅这些代理的状态变化。状态这里可以理解成代理当前是还在忙、已经完成、出错、找不到等。如果目标已经是最终状态,它会马上返回;否则它会等到有代理进入最终状态,或时间耗尽。结束时它会再发一个“等待结束”的事件,并把每个已等到的代理状态整理成工具输出。一个重要细节是:它不是盲目一直等下去,超时时间会被检查和限制,防止一次工具调用把系统卡住太久。
Handler::new25–27 ↗
fn new(options: WaitAgentTimeoutOptions) -> Self
作用:创建一个 wait_agent 工具处理器,并把等待超时相关的配置放进去。外部在注册这个工具时会用它来生成可工作的处理器。
数据流:输入是一份 WaitAgentTimeoutOptions 超时配置 → 函数把它存进 Handler 结构里 → 输出一个新的 Handler,之后所有等待请求都会按这份配置生成工具说明。
调用关系:它是这个处理器的入口构造函数。系统启动或工具注册阶段会先用它造出 Handler,之后才会调用 tool_name、spec、handle 等方法。
Handler::tool_name31–33 ↗
fn tool_name(&self) -> ToolName
作用:告诉工具系统:这个处理器对应的工具名字叫 wait_agent,并且属于 multi-agent v1 这个命名空间。命名空间可以理解成给工具名加一个分类前缀,避免重名。
数据流:它不需要外部输入 → 调用 namespaced 把命名空间和 wait_agent 拼成标准工具名 → 返回这个工具名给工具注册和匹配流程使用。
调用关系:工具框架用它识别一次调用该交给哪个处理器。它把具体拼名字的工作交给 namespaced。
调用图:调用 1 个内部函数(namespaced)。
Handler::spec35–37 ↗
fn spec(&self) -> ToolSpec
作用:生成 wait_agent 的工具说明书,告诉模型这个工具能接收哪些参数、参数有什么限制。这样模型才知道应该怎么正确调用它。
数据流:输入是处理器里保存的超时选项 → 调用 create_wait_agent_tool_v1 生成工具规格 → 输出一份 ToolSpec,也就是工具说明。
调用关系:它会被工具注册流程使用,也会被 Handler::search_info 调用,用同一份说明生成可搜索信息,避免工具说明前后不一致。
调用图:调用 1 个内部函数(create_wait_agent_tool_v1);被 1 处调用(search_info)。
Handler::search_info39–44 ↗
fn search_info(&self) -> Option<ToolSearchInfo>
作用:提供一段搜索提示词,让系统在需要找工具时更容易搜到 wait_agent。这就像给工具贴上“等待、子代理、状态、超时”等标签。
数据流:它读取当前工具规格 → 把关键词和 spec 生成的工具说明一起交给搜索信息构造函数 → 返回可选的工具搜索信息。
调用关系:它在工具发现或检索阶段发挥作用。它会调用 Handler::spec,确保搜索信息对应的工具说明就是实际可调用的那一份。
调用图:调用 1 个内部函数(spec)。
Handler::handle46–48 ↗
fn handle(&self, invocation: ToolInvocation) -> codex_tools::ToolExecutorFuture<'_>
作用:接住一次真正的工具调用,并把它转成异步任务去执行。异步任务就是“先把活安排上,等网络、状态变化这类事情回来再继续”。
数据流:输入是一份 ToolInvocation,里面有会话、轮次、参数、调用编号等 → 函数把真正工作交给 handle_call,并用 pin 包成工具框架需要的异步返回形式 → 输出一个未来会完成的工具执行结果。
调用关系:工具框架收到 wait_agent 调用时会进入这里。它本身不做复杂判断,只负责把调用转交给 Handler::handle_call。
调用图:调用 1 个内部函数(handle_call);外部调用 1 个(pin)。
Handler::handle_call52–214 ↗
async fn handle_call(
&self,
invocation: ToolInvocation,
) -> Result<Box<dyn crate::tools::context::ToolOutput>, FunctionCallError>
作用:这是 wait_agent 的核心流程:解析要等的代理,检查超时时间,监听代理状态,最后返回“等到了什么”或“超时了”。它保证等待过程有开始事件、有结束事件,也不会无限期卡住。
数据流:输入是一整次工具调用,包括当前会话、当前轮次、原始参数和调用编号 → 它先把参数解析成目标代理和超时时间,再查询每个代理的昵称、角色、路径等展示信息;然后发送“开始等待”事件,订阅目标代理状态;如果发现目标已经结束、失败或不存在,就直接记录这些结果,否则启动多个等待任务,等到第一个最终状态出现或到达截止时间;最后整理成 WaitAgentResult,发送“等待结束”事件 → 输出一个工具结果,里面有已拿到的代理状态和是否超时,同时会在会话里留下开始、结束两类事件。
调用关系:它由 Handler::handle 调用,是整个文件的主干。它会用 is_final 判断状态是否已经结束,用 wait_for_final_status 等状态变化,用 now_unix_timestamp_ms 给事件打时间戳。它还会调用工具输出封装函数,把 WaitAgentResult 交回工具框架。
调用图:调用 3 个内部函数(is_final, wait_for_final_status, now_unix_timestamp_ms);被 1 处调用(handle);外部调用 8 个(from_millis, new, with_capacity, now, new, with_capacity, timeout_at, RespondToModel)。
Handler::matches_kind218–220 ↗
fn matches_kind(&self, payload: &ToolPayload) -> bool
作用:判断某个工具载荷是不是这个处理器能处理的类型。这里它只接受函数式工具调用,也就是 ToolPayload::Function。
数据流:输入是一份工具载荷 ToolPayload → 函数检查它是不是 Function 这种形态 → 输出 true 或 false,告诉运行时要不要把这次调用交给它。
调用关系:工具运行时在分派调用前会用它做一次筛选。它不调用本文件的核心等待逻辑,只负责把不合适的载荷挡在外面。
调用图:外部调用 1 个(matches!)。
WaitAgentResult::log_preview237–239 ↗
fn log_preview(&self) -> String
作用:把等待结果转成适合写日志的简短文本。这样开发者或系统日志不用展开内部结构,也能看懂这次 wait_agent 返回了什么。
数据流:输入是一个 WaitAgentResult → 函数把它按 wait_agent 这个工具名格式化成 JSON 文本 → 输出一段日志预览字符串。
调用关系:工具框架在记录工具输出时会用它。它不参与等待过程,只负责把结果变成可读的日志内容。
WaitAgentResult::success_for_logging241–243 ↗
fn success_for_logging(&self) -> bool
作用:告诉日志系统:只要能正常产出这个结果,就按成功记录。即使结果里显示超时,这个工具调用本身也算正常完成,因为超时是它预期会表达的一种结果。
数据流:它不读取额外输入,只看这是一个 WaitAgentResult 输出 → 直接返回 true → 日志层会把这次工具输出标成成功。
调用关系:工具框架写日志时会询问这个方法。它和 handle_call 的区别是:handle_call 负责实际等待,这里只负责日志上的成功标记。
WaitAgentResult::to_response_item245–247 ↗
fn to_response_item(&self, call_id: &str, payload: &ToolPayload) -> ResponseInputItem
作用:把等待结果包装成可以发回模型的响应项。模型需要看到结构化结果,才能继续决定下一步该做什么。
数据流:输入是调用编号、原始工具载荷和等待结果本身 → 函数把这些信息按工具响应格式打包 → 输出一个 ResponseInputItem,也就是会放进下一轮对话上下文的结果项。
调用关系:工具框架在把 handle_call 产出的结果回传给模型时会用它。它把通用打包工作交给工具输出辅助函数完成。
WaitAgentResult::code_mode_result249–251 ↗
fn code_mode_result(&self, _payload: &ToolPayload) -> JsonValue
作用:把等待结果转成代码模式更方便读取的 JSON 值。代码模式可以理解成更偏程序处理的返回格式,而不是只给人看的文字。
数据流:输入是等待结果和原始工具载荷 → 函数把结果按 wait_agent 的名字转换成 JSON 值 → 输出这份 JSON,供代码模式消费。
调用关系:当工具结果要进入代码模式流程时,框架会调用它。它不改变等待结果,只换一种包装和呈现方式。
wait_for_final_status254–274 ↗
async fn wait_for_final_status(
session: Arc<Session>,
thread_id: ThreadId,
mut status_rx: Receiver<AgentStatus>,
) -> Option<(ThreadId, AgentStatus)>
作用:等待某一个代理进入最终状态。最终状态指这个代理已经不会继续正常推进了,比如完成、失败或找不到这类结局。
数据流:输入是当前会话、要观察的代理线程编号,以及一个状态接收器;状态接收器可以理解成一根会不断送来最新状态的通知线 → 函数先看当前状态是否已经最终;如果还没结束,就一直等状态变化;如果通知线断了,就主动再去查询一次最新状态 → 输出 Some(线程编号, 最终状态),如果始终没有拿到最终状态则不会主动编造结果。
调用关系:它由 Handler::handle_call 为每个目标代理启动。它把单个代理的等待细节封装起来,让 handle_call 可以同时等多个代理,并在任意一个代理先结束时继续整理总结果。
调用图:调用 1 个内部函数(is_final);被 1 处调用(handle_call);外部调用 2 个(borrow, changed)。
core/src/tools/handlers/multi_agents/resume_agent.rs源码 ↗
这个文件像一个“重新接通分机”的按钮。外部模型调用 resume_agent 时,会传进来一个代理的 id,也就是某个子代理所在对话线程的编号。代码先检查这个编号是不是合法,再看当前代理还能不能继续派生子代理,避免代理一层套一层无限开下去。然后它会发出“开始恢复”的事件,方便界面或日志知道发生了什么;接着查询目标代理的状态。如果目标代理已经不在运行列表里,就尝试从之前保存的执行记录里把它恢复出来。最后它发出“恢复结束”的事件,记录最终状态,并把状态作为工具结果返回。这里还会记录一次统计指标,方便系统知道这个工具被用了多少次。
Handler::tool_name11–13 ↗
fn tool_name(&self) -> ToolName
作用:告诉工具系统:这个处理器对应的工具名字叫 resume_agent,并且属于多代理工具这一组。这样模型调用这个名字时,系统才知道该把请求交给它。
数据流:进去的是这个处理器本身,不需要额外输入 → 它把“多代理命名空间”和 resume_agent 这个名字拼成一个正式工具名 → 出来的是一个可被工具系统识别的工具名称。
调用关系:工具注册或匹配调用时会用到它。它内部通过 namespaced 生成带分组的名字,避免和别的同名工具撞车。
调用图:调用 1 个内部函数(namespaced)。
Handler::spec15–17 ↗
fn spec(&self) -> ToolSpec
作用:提供 resume_agent 这个工具的说明书。说明书会告诉模型这个工具需要什么参数、能做什么。
数据流:进去的是处理器本身 → 它调用 create_resume_agent_tool 生成工具规格 → 出来的是一份 ToolSpec,也就是工具系统能读懂的工具定义。
调用关系:工具系统需要展示或注册这个工具时会用它;Handler::search_info 也会调用它,把同一份说明书放进搜索信息里。
调用图:调用 1 个内部函数(create_resume_agent_tool);被 1 处调用(search_info)。
Handler::search_info19–24 ↗
fn search_info(&self) -> Option<ToolSearchInfo>
作用:给工具搜索准备关键词和工具说明,让模型或系统更容易在需要“恢复代理”时找到这个工具。
数据流:进去的是处理器本身 → 它准备一串包含 resume、agent、thread id 等含义的搜索词,并取来 Handler::spec 的工具说明 → 出来的是可选的搜索信息。
调用关系:它在工具发现阶段起作用。它把 Handler::spec 生成的正式说明书和一组便于搜索的词放在一起,帮助系统判断什么时候该推荐或使用 resume_agent。
调用图:调用 1 个内部函数(spec)。
Handler::handle26–28 ↗
fn handle(&self, invocation: ToolInvocation) -> codex_tools::ToolExecutorFuture<'_>
作用:真正接住一次 resume_agent 工具调用,并把活交给核心函数去做。它还把异步任务包装成工具系统需要的形式。
数据流:进去的是一次工具调用,里面有会话、当前轮次、参数和调用编号 → 它启动异步流程,调用 handle_resume_agent 完成恢复逻辑,并把结果包装成通用工具输出 → 出来的是一个将来会完成的异步结果。
调用关系:当模型实际调用 resume_agent 时,工具运行框架会进入这里。它自己不做复杂判断,只负责把请求转交给 handle_resume_agent。
调用图:调用 1 个内部函数(handle_resume_agent);外部调用 1 个(pin)。
handle_resume_agent31–138 ↗
async fn handle_resume_agent(
invocation: ToolInvocation,
) -> Result<ResumeAgentResult, FunctionCallError>
作用:这是本文件的核心流程:读取要恢复的代理 id,检查能不能恢复,必要时把关闭的代理重新拉起来,并返回它的新状态。
数据流:进去的是一次完整的工具调用,包含会话、当前对话轮次、参数、调用编号等信息 → 它解析参数里的 id,把它转成线程编号;读取目标代理的昵称和角色;计算当前代理派生深度,超过上限就直接告诉模型不要再开代理;随后发送“开始恢复”事件,查询目标状态,如果状态是找不到就调用 try_resume_closed_agent 尝试从旧记录恢复;无论成功失败都会发送“结束恢复”事件;如果有错误就返回错误,否则记录一次统计并产出 ResumeAgentResult → 出来的是目标代理的最终状态,外加事件日志和统计计数等副作用。
调用关系:Handler::handle 会调用它。它是整个 resume_agent 工具的总调度员:时间戳来自 now_unix_timestamp_ms,深度来自 next_thread_spawn_depth,真正恢复关闭代理的脏活交给 try_resume_closed_agent。
调用图:调用 3 个内部函数(try_resume_closed_agent, now_unix_timestamp_ms, from_string);被 1 处调用(handle);外部调用 4 个(pin, next_thread_spawn_depth, matches!, RespondToModel)。
Handler::matches_kind141–143 ↗
fn matches_kind(&self, payload: &ToolPayload) -> bool
作用:判断传进来的工具请求是不是“函数调用”这种形式。只有这种形式才适合交给这个处理器。
数据流:进去的是一个工具载荷,也就是模型发来的工具请求内容 → 它检查这个载荷是不是 Function 类型 → 出来的是 true 或 false,表示这个处理器要不要接这个请求。
调用关系:工具框架在分派请求前会用它筛选处理器。它只做一道门卫检查:不是函数调用,就不让 resume_agent 的后续流程接手。
调用图:外部调用 1 个(matches!)。
ResumeAgentResult::log_preview157–159 ↗
fn log_preview(&self) -> String
作用:生成一段适合写进日志的结果预览。这样开发者看日志时能快速知道 resume_agent 返回了什么。
数据流:进去的是恢复结果,主要包含代理状态 → 它把结果按 resume_agent 的名字整理成 JSON 文本,也就是一种结构化文本格式 → 出来的是一段日志用字符串。
调用关系:工具执行完后,日志系统会用它生成简短记录。它属于 ToolOutput 输出接口的一部分,配合其他输出函数一起把结果展示给不同地方。
ResumeAgentResult::success_for_logging161–163 ↗
fn success_for_logging(&self) -> bool
作用:告诉日志系统:只要走到这个结果对象,就按成功来记录。因为错误情况会在更早的位置以错误返回,不会生成这个结果。
数据流:进去的是恢复结果本身,但它不需要读取里面的状态 → 它固定认为这是一次成功的工具输出 → 出来的是 true。
调用关系:工具框架记录执行结果时会问它“这次算不算成功”。它和 log_preview 一起服务于日志记录。
ResumeAgentResult::to_response_item165–167 ↗
fn to_response_item(&self, call_id: &str, payload: &ToolPayload) -> ResponseInputItem
作用:把恢复结果转换成可以发回给模型的响应项。模型看到这个响应后,才能知道目标代理现在是什么状态。
数据流:进去的是调用编号、原始工具载荷和恢复结果 → 它把这些打包成标准响应格式,并标明这是 resume_agent 的成功输出 → 出来的是一个 ResponseInputItem,也就是后续能送回模型的消息块。
调用关系:工具框架在准备回复模型时会调用它。它把内部的 Rust 结果对象翻译成模型接口能理解的响应格式。
ResumeAgentResult::code_mode_result169–171 ↗
fn code_mode_result(&self, _payload: &ToolPayload) -> JsonValue
作用:为代码模式生成一份机器更容易读取的 JSON 结果。代码模式可以把它当成结构化数据继续处理。
数据流:进去的是恢复结果和原始工具载荷 → 它把结果按 resume_agent 的名字转换成 JSON 值 → 出来的是一个结构化 JSON 结果。
调用关系:当系统不是只给人看文字,而是要在代码模式里继续消费工具结果时,会用到它。它和 to_response_item 做的是同一份结果的不同包装。
try_resume_closed_agent174–195 ↗
async fn try_resume_closed_agent(
session: &Arc<Session>,
turn: &Arc<TurnContext>,
receiver_thread_id: ThreadId,
child_depth: i32,
) -> Result<(), FunctionCallError>
作用:尝试把一个已经关闭、当前状态里找不到的代理重新恢复出来。它专门处理“代理不在线但可能有旧记录可恢复”的情况。
数据流:进去的是当前会话、当前轮次、目标代理线程编号和新的派生深度 → 它先根据当前轮次构造恢复配置,再准备一份线程来源信息,说明是谁发起了这次恢复以及深度是多少;然后调用代理控制服务,从之前保存的执行记录里恢复目标代理 → 成功时出来的是空结果,表示恢复已完成;失败时把底层错误包装成适合协作代理工具返回的错误。
调用关系:handle_resume_agent 在发现目标代理状态是 NotFound 时才会调用它。它把真正恢复代理的工作交给 session.services.agent_control.resume_agent_from_rollout,自己负责准备配置、来源信息和错误翻译。
调用图:被 1 处调用(handle_resume_agent);外部调用 1 个(pin)。
core/src/tools/handlers/multi_agents/close_agent.rs源码 ↗
可以把这个文件想成“关机按钮”的后台处理器。外部调用 close_agent 时,它先确认请求是不是函数调用,再读取参数里的目标代理编号。接着它会查这个代理的资料和当前状态,并发出一个“开始关闭”的事件,方便界面或日志知道事情开始了。然后它通过 agent_control 这个代理控制中心去订阅或读取目标代理状态,再真正发出关闭命令。无论成功还是遇到部分错误,它都会尽量发出“关闭结束”的事件,里面带上代理昵称、角色和关闭前状态,方便事后追踪。最后返回 CloseAgentResult,告诉调用方:这个代理在被关闭前是什么状态。这个文件不直接做底层杀进程的活,而是负责把参数、状态、事件、错误和返回结果串起来。
Handler::tool_name10–12 ↗
fn tool_name(&self) -> ToolName
作用:告诉工具系统:这个处理器对应的工具名字是 multi-agent 命名空间下的 close_agent。这样外部请求 close_agent 时,系统才知道该找它来处理。
数据流:进去的是这个 Handler 本身,没有额外参数;它把固定的命名空间和工具名交给 namespaced 组装;出来的是一个标准化的 ToolName,供工具注册和匹配使用。
调用关系:它是工具识别流程的一部分,会在系统登记或查找工具时被调用。它把名字拼装这一步交给 namespaced,避免各处手写名字导致不一致。
调用图:调用 1 个内部函数(namespaced)。
Handler::spec14–16 ↗
fn spec(&self) -> ToolSpec
作用:给出 close_agent 这个工具的说明书,也就是它需要什么参数、长什么样、调用方该怎么填。没有这个说明,模型或前端就不知道怎么正确调用它。
数据流:进去的是 Handler 本身;它调用 create_close_agent_tool_v1 生成工具规格;出来的是 ToolSpec,也就是工具系统能读懂的描述信息。
调用关系:它会被工具系统用来展示或注册工具,也会被 Handler::search_info 调用,用来生成搜索信息。真正的规格内容由 create_close_agent_tool_v1 提供。
调用图:调用 1 个内部函数(create_close_agent_tool_v1);被 1 处调用(search_info)。
Handler::search_info18–23 ↗
fn search_info(&self) -> Option<ToolSearchInfo>
作用:给 close_agent 准备一份“搜索标签”,让系统在需要找相关工具时更容易找到它。比如用户说 close、shutdown、stop agent,都能更可能匹配到这个工具。
数据流:进去的是 Handler 本身;它先通过 spec 拿到工具说明,再把一串关键词和说明一起交给搜索信息生成逻辑;出来的是可选的 ToolSearchInfo,用于工具检索。
调用关系:它处在工具发现阶段,帮助系统从一堆工具里找到 close_agent。它依赖 Handler::spec,因为搜索信息要和真实工具规格保持一致。
调用图:调用 1 个内部函数(spec)。
Handler::handle25–27 ↗
fn handle(&self, invocation: ToolInvocation) -> codex_tools::ToolExecutorFuture<'_>
作用:这是 close_agent 工具被真正调用时的入口。它把一次工具调用包装成异步任务,然后交给核心函数 handle_close_agent 去做具体关闭工作。
数据流:进去的是 ToolInvocation,也就是一次工具调用的完整上下文;它创建一个异步执行块,并在里面调用 handle_close_agent;出来的是一个未来会完成的结果,成功时会被包装成统一的工具输出。
调用关系:工具运行框架会在收到 close_agent 调用时进入这里。它本身不细做关闭逻辑,而是把活交给 handle_close_agent,并用 pin 把异步任务固定好,方便运行时调度。
调用图:调用 1 个内部函数(handle_close_agent);外部调用 1 个(pin)。
handle_close_agent30–112 ↗
async fn handle_close_agent(
invocation: ToolInvocation,
) -> Result<CloseAgentResult, FunctionCallError>
作用:这是本文件最核心的函数,负责完整执行“关闭指定代理”的流程。它会解析目标、记录开始和结束事件、读取关闭前状态、调用控制中心关闭代理,并把结果或错误返回给调用方。
数据流:进去的是一次工具调用,里面有会话、当前轮次、参数和调用编号;它从参数里取出 target,解析成代理 ID,查代理资料,发送关闭开始事件,读取目标代理状态,然后调用 agent_control.close_agent 关闭它;出来的是 CloseAgentResult,里面记录关闭前状态,同时它也会向会话发送开始和结束事件。如果中途遇到代理控制错误,它会尽量发送结束事件后再返回错误。
调用关系:它由 Handler::handle 调用,是 close_agent 的实际执行者。它会用 now_unix_timestamp_ms 给事件打时间戳,用异步 pin 等待关闭动作完成,并和 session.services.agent_control 这个代理控制中心协作完成状态查询和关闭。
调用图:调用 1 个内部函数(now_unix_timestamp_ms);被 1 处调用(handle);外部调用 1 个(pin)。
Handler::matches_kind115–117 ↗
fn matches_kind(&self, payload: &ToolPayload) -> bool
作用:判断传进来的工具载荷是不是函数调用形式。close_agent 只接受函数调用,不处理别的载荷类型。
数据流:进去的是一个 ToolPayload;它检查这个载荷是不是 ToolPayload::Function;出来的是 true 或 false,表示这个 Handler 能不能接手。
调用关系:它在工具分发前后用于筛选请求。工具框架会靠它判断当前请求是否适合交给 close_agent 处理,判断本身通过 matches! 这个 Rust 匹配宏完成。
调用图:外部调用 1 个(matches!)。
CloseAgentResult::log_preview126–128 ↗
fn log_preview(&self) -> String
作用:把 close_agent 的成功结果整理成适合写进日志的简短文本。这样排查问题时,人能快速看懂这次关闭返回了什么。
数据流:进去的是 CloseAgentResult,主要包含 previous_status;它把这个结果按 close_agent 的名字转成 JSON 风格文本;出来的是一段字符串,用于日志预览。
调用关系:它属于工具输出流程的一部分。handle_close_agent 返回 CloseAgentResult 后,工具框架在记录日志时会调用它。
CloseAgentResult::success_for_logging130–132 ↗
fn success_for_logging(&self) -> bool
作用:告诉日志系统:这个结果代表一次成功调用。它让日志可以用成功状态来标记 close_agent 的输出。
数据流:进去的是 CloseAgentResult;它不需要读取复杂内容,直接返回 true;出来的是一个布尔值,表示日志里应按成功记录。
调用关系:它在工具结果写日志时被工具框架使用。因为 CloseAgentResult 只会在关闭流程成功后产生,所以这里固定返回成功。
CloseAgentResult::to_response_item134–136 ↗
fn to_response_item(&self, call_id: &str, payload: &ToolPayload) -> ResponseInputItem
作用:把 close_agent 的结果转换成协议响应项,也就是调用方真正会收到的那种统一格式。没有这一步,内部结果就不能规范地发回给外部。
数据流:进去的是结果本身、调用编号 call_id 和原始载荷 payload;它把这些和 close_agent 名字一起打包;出来的是 ResponseInputItem,供系统回传给调用方。
调用关系:它在工具执行完成、准备回应外部时被调用。它把 CloseAgentResult 从内部结构变成协议层能发送的响应格式。
CloseAgentResult::code_mode_result138–140 ↗
fn code_mode_result(&self, _payload: &ToolPayload) -> JsonValue
作用:把 close_agent 的结果转换成代码模式下使用的 JSON 值。这样如果调用方是代码化读取结果,就能拿到结构化数据,而不是只看文本。
数据流:进去的是 CloseAgentResult 和原始 payload;它主要使用结果内容,并按 close_agent 的名字生成 JSON;出来的是 JsonValue,方便程序继续处理。
调用关系:它在代码模式需要消费工具结果时被调用。它和日志、响应转换函数一样,都是 CloseAgentResult 对外展示自己的不同出口。
core/src/tools/handlers/multi_agents_v2.rs源码 ↗
这个文件本身不做大量细节工作,更像一个“前台服务台”:它把多智能体协作相关的小模块接进来,并把几个具体工具处理器重新导出,方便外部统一使用。比如有人要启动一个子智能体、给另一个智能体发消息、等待对方完成,外层代码可以从这里找到对应的处理器。文件里还有一个小但重要的辅助函数,用来把工具里产生的一条消息包装成系统认识的“智能体之间通信”。这里的“加密通信”可以理解为把便条装进信封,再交给指定收件人;这样系统不只是看到一段普通文字,而是知道是谁发的、发给谁、内容是什么,并且这条消息会触发接收方继续工作。
communication_from_tool_message43–55 ↗
fn communication_from_tool_message(
author: AgentPath,
recipient: AgentPath,
message: String,
) -> InterAgentCommunication
作用:把工具调用里生成的一段文字,变成一条正式的智能体间消息。调用它的人只要给出发送者、接收者和消息内容,就能得到系统可传递、可触发后续处理的通信对象。
数据流:进去的是发送者路径、接收者路径和一段消息文字。函数把这些信息交给 InterAgentCommunication::new_encrypted,创建一条加密的智能体通信,并把附件列表设为空,同时标记这条消息会触发接收方开始下一轮处理。出来的是一个 InterAgentCommunication 对象,也就是系统内部可识别的“从谁到谁的一封信”。
调用关系:它位在多智能体工具层和底层通信协议之间,像一个转换接头:上层工具拿到普通消息文本后,可以用它包装成协议对象;真正创建通信对象的活儿交给 InterAgentCommunication::new_encrypted,而底层构造过程中还会继续使用 new(ext) 之类的创建逻辑。
调用图:调用 1 个内部函数(new_encrypted);外部调用 1 个(new)。
core/src/tools/handlers/multi_agents_v2/message_tool.rs源码 ↗
在多智能体协作里,一个智能体可能只是留言,也可能希望对方马上继续干活。这个文件就是这条“传话通道”的公共部分。它先定义消息投递模式:只排队,或立刻触发对方新一轮工作。然后定义两个工具的输入格式,都需要目标和消息内容。真正发送前,它会拒绝空消息,避免把没意义的内容塞进系统;会把用户写的目标名字解析成具体的智能体线程;会确认目标智能体存在、已加载,并且在“后续任务”模式下禁止把任务发给根智能体。最后它生成一条智能体之间的通信,送入 agent_control,并发出一条活动事件,方便外部知道这次互动发生过。
MessageDeliveryMode::apply19–30 ↗
fn apply(self, communication: InterAgentCommunication) -> InterAgentCommunication
作用:这个函数给一条已经准备好的智能体通信盖上“是否立刻叫醒对方”的标记。有人发普通消息时它会让消息只排队;有人发后续任务时它会让目标智能体马上开始一轮处理。
数据流:进去的是一个投递模式和一条通信内容 → 它只改通信里的 trigger_turn 这个开关,也就是“是否触发新一轮工作” → 出来的是同一条通信的更新版本,内容不变,但叫醒方式已经确定。
调用关系:它由 handle_message_string_tool 在真正发送消息前调用。主流程先把作者、目标和正文组装成通信,再交给这个函数补上投递方式,最后才送给 agent_control 发送。
调用图:被 1 处调用(handle_message_string_tool)。
message_content49–56 ↗
fn message_content(message: String) -> Result<String, FunctionCallError>
作用:这个函数专门检查消息正文是不是空的。它防止智能体收到只有空格或完全没内容的消息,因为这种消息对协作没有意义,还可能让对方白跑一轮。
数据流:进去的是一段消息字符串 → 它先去掉首尾空白来判断有没有真实内容 → 如果是空的,就返回一个给模型看的错误;如果有内容,就把原消息原样返回。
调用关系:它是 handle_message_string_tool 的第一道关卡。只有消息通过这个检查,后面的找目标、加载智能体、发送通信这些步骤才会继续。它在发现空消息时会构造 RespondToModel 这类错误,也就是把问题直接反馈给调用工具的模型。
调用图:被 1 处调用(handle_message_string_tool);外部调用 1 个(RespondToModel)。
handle_message_string_tool59–126 ↗
async fn handle_message_string_tool(
invocation: ToolInvocation,
mode: MessageDeliveryMode,
target: String,
message: String,
) -> Result<FunctionToolOutput, FunctionCallError>
作用:这是这个文件的主流程函数,统一处理 send_message 和 followup_task 两个工具的发送过程。它把“检查消息、找到目标、准备目标、发送通信、记录事件”这一整套动作串起来。
数据流:进去的是一次工具调用信息、投递模式、目标名字和消息正文 → 它先检查消息,再把目标解析成具体线程,确认目标智能体存在并已加载;如果是后续任务,还会拦住发给根智能体的情况;接着它生成智能体间通信并按模式设置是否立刻触发;发送成功后,它向会话发出一条子智能体活动事件 → 出来的是一个空文本的工具结果,表示这次工具调用成功完成,同时系统状态里已经多了一条通信和一条活动记录。
调用关系:它被上层的 handle_call 调用,分别服务于 send_message 和 followup_task。它自己会调用 message_content 做内容检查,调用 MessageDeliveryMode::apply 设置投递方式,调用 now_unix_timestamp_ms 给活动事件打时间戳,最后用 FunctionToolOutput::from_text 包装成功结果。发送、加载、目标确认等实际动作则交给 session.services.agent_control。
调用图:调用 4 个内部函数(from_text, apply, message_content, now_unix_timestamp_ms);被 2 处调用(handle_call, handle_call);外部调用 2 个(new, RespondToModel)。
core/src/tools/handlers/multi_agents_v2/spawn.rs源码 ↗
可以把这个文件理解成“给 AI 团队派新人的按钮”。当模型调用 spawn_agent 时,这里会先读懂模型给的参数,比如要让新代理做什么、任务名是什么、要不要继承当前对话历史、是否指定角色或模型。然后它会检查这些参数是否合法,生成新代理的配置,把父会话的必要信息带过去,并决定第一条消息怎么发给新代理。如果只是普通文字消息,它会包装成代理之间的通信,让系统知道“这是父代理交给子代理的任务”。新代理启动成功后,它还会发一个事件通知前端或日志系统:“某个子代理已启动”,并记录一次统计数据。最后返回任务名和可选昵称;如果配置要求隐藏元信息,就只返回任务名。这里很重要,因为多代理不是简单多开一个聊天窗口,还要保证上下文继承、角色设置、模型覆盖、服务等级和审计记录都一致。
Handler::new20–22 ↗
fn new(options: SpawnAgentToolOptions) -> Self
作用:创建一个 spawn_agent 工具处理器,并把工具选项保存起来。外部在安装或注册这个工具时会用它。
数据流:进去的是 SpawnAgentToolOptions,也就是这个工具生成说明时需要用的选项;函数把它放进 Handler 结构里;出来的是一个可用的 Handler 实例,后面可以拿来响应工具调用。
调用关系:它是这个文件里处理器的构造入口。创建好之后,系统会通过同一个 Handler 去询问工具名字、工具规格,并在真正调用时执行 handle。
Handler::tool_name26–28 ↗
fn tool_name(&self) -> ToolName
作用:告诉工具系统:这个处理器对应的工具名叫 spawn_agent。工具系统靠这个名字把模型的调用分发到正确代码。
数据流:进去的是处理器自身;它调用 plain 生成一个普通工具名;出来的是 ToolName,内容就是 spawn_agent。
调用关系:当工具框架需要登记或匹配工具时会问它名字。它把名字交给 plain 包装,而真正执行工具时会走 Handler::handle。
调用图:调用 1 个内部函数(plain)。
Handler::spec30–32 ↗
fn spec(&self) -> ToolSpec
作用:生成 spawn_agent 这个工具给模型看的使用说明。模型需要这份说明,才知道可以传哪些参数、每个参数是什么意思。
数据流:进去的是处理器里保存的选项;它先复制一份选项,再交给 create_spawn_agent_tool_v2 生成工具规格;出来的是 ToolSpec,也就是工具的正式描述。
调用关系:工具注册或展示给模型前会调用它。它不自己拼说明,而是把工作交给 create_spawn_agent_tool_v2,这样工具定义集中在规格文件里。
调用图:调用 1 个内部函数(create_spawn_agent_tool_v2);外部调用 1 个(clone)。
Handler::handle34–36 ↗
fn handle(&self, invocation: ToolInvocation) -> codex_tools::ToolExecutorFuture<'_>
作用:真正接住一次 spawn_agent 工具调用,并把它变成异步任务执行。异步任务就是可能要等网络、服务或其他系统返回结果的工作。
数据流:进去的是一次 ToolInvocation,里面有会话、当前轮次、参数和调用编号;它把主要工作交给 handle_spawn_agent,再把成功结果包装成工具输出;出来的是一个未来会完成的结果。
调用关系:它是工具框架调用进来的入口。它自己不做复杂判断,只负责启动 handle_spawn_agent,并用 pin 把异步流程固定成框架需要的形式。
调用图:调用 1 个内部函数(handle_spawn_agent);外部调用 1 个(pin)。
handle_spawn_agent39–177 ↗
async fn handle_spawn_agent(
invocation: ToolInvocation,
) -> Result<SpawnAgentResult, FunctionCallError>
作用:完成“创建子代理”的完整流程:读参数、检查规则、准备配置、启动新代理、发事件、记统计、返回结果。这是本文件最核心的函数。
数据流:进去的是一次工具调用,里面包含当前会话、当前对话轮次、模型传来的 JSON 参数和调用编号;它解析出消息、任务名、角色、模型、推理强度、服务等级和继承历史方式,再根据当前会话生成子代理配置,必要时应用角色、模型和运行时覆盖,然后调用代理控制服务启动新线程;出来的是 SpawnAgentResult,包含新任务名和可能的昵称,同时它还会改动系统状态:创建一个新代理线程、发送子代理启动事件、写入遥测计数。
调用关系:它由 Handler::handle 在收到工具调用时使用。流程中它会调用 SpawnAgentArgs::fork_mode 判断是否继承父会话历史,调用 next_thread_spawn_depth 计算子代理层级,调用 apply_role_to_config 应用角色设置,调用 now_unix_timestamp_ms 给启动事件打时间戳,并最终把创建代理的事交给 session.services.agent_control.spawn_agent_with_metadata。
调用图:调用 2 个内部函数(apply_role_to_config, now_unix_timestamp_ms);被 1 处调用(handle);外部调用 4 个(pin, from, next_thread_spawn_depth, matches!)。
Handler::matches_kind180–182 ↗
fn matches_kind(&self, payload: &ToolPayload) -> bool
作用:判断传进来的工具载荷是不是函数调用格式。只有格式对了,这个处理器才应该处理它。
数据流:进去的是一个 ToolPayload,也就是工具调用带来的原始载荷;它检查这个载荷是否是 Function 类型;出来的是布尔值,表示匹配或不匹配。
调用关系:工具运行时在分发调用前会用它做筛选。它只做类型检查,确认能处理后,真正的执行会交给 Handler::handle。
调用图:外部调用 1 个(matches!)。
SpawnAgentArgs::fork_mode199–232 ↗
fn fork_mode(&self) -> Result<Option<SpawnAgentForkMode>, FunctionCallError>
作用:把用户传来的 fork_turns 参数翻译成系统能懂的“继承多少父会话历史”。这能避免子代理拿到错误上下文,比如本该从零开始却带上了全部历史。
数据流:进去的是已经解析好的 SpawnAgentArgs;它先拒绝旧参数 fork_context,再读取 fork_turns,空值默认按 all 处理,none 表示不继承,all 表示继承全部,数字字符串表示只继承最后 N 轮;出来的是可选的 SpawnAgentForkMode,如果参数非法就返回给模型看的错误消息。
调用关系:它在 handle_spawn_agent 早期被调用,因为后面的配置和参数限制都要看继承模式。它会构造 LastNTurns 表示只继承最近几轮,也会用 RespondToModel 把不合法的输入清楚地告诉模型。
调用图:外部调用 2 个(LastNTurns, RespondToModel)。
SpawnAgentResult::log_preview248–250 ↗
fn log_preview(&self) -> String
作用:生成一段适合写进日志的简短结果预览。这样排查问题时能看到工具返回了什么,但不用手写 JSON。
数据流:进去的是 SpawnAgentResult;它把结果交给通用的工具输出 JSON 格式化函数,并标上工具名 spawn_agent;出来的是一段字符串,用于日志预览。
调用关系:工具框架在记录工具执行结果时会调用它。它服务于日志展示,不参与创建子代理本身。
SpawnAgentResult::success_for_logging252–254 ↗
fn success_for_logging(&self) -> bool
作用:告诉日志系统:只要已经生成这个结果,就算成功。这里固定返回成功,方便日志统一标记。
数据流:进去的是结果对象本身;它不读取具体字段,也不改任何东西;出来的是 true,表示这次工具输出应按成功记录。
调用关系:工具框架在写执行日志时会问它成功与否。它和 log_preview 一起服务于工具结果记录。
SpawnAgentResult::to_response_item256–258 ↗
fn to_response_item(&self, call_id: &str, payload: &ToolPayload) -> ResponseInputItem
作用:把子代理启动结果转换成协议里能发回模型或上层系统的响应项。简单说,就是把内部结果装进标准回信信封。
数据流:进去的是调用编号、原始工具载荷和 SpawnAgentResult;它调用通用包装函数,带上工具名 spawn_agent 和成功标记;出来的是 ResponseInputItem,也就是系统协议能发送的响应内容。
调用关系:工具框架在准备把工具结果返回给模型时会调用它。它不决定结果内容,只负责把结果按统一格式封装。
SpawnAgentResult::code_mode_result260–262 ↗
fn code_mode_result(&self, _payload: &ToolPayload) -> JsonValue
作用:把结果转换成代码模式下使用的 JSON 值。代码模式需要结构化数据,而不是普通文字。
数据流:进去的是 SpawnAgentResult 和工具载荷;它忽略载荷,把结果按 spawn_agent 的工具输出格式转成 JSON;出来的是一个 JsonValue。
调用关系:当系统处在代码模式、需要机器可读结果时会调用它。它和 to_response_item 类似,都是输出适配层,只是面向的消费方不同。
core/src/tools/handlers/multi_agents_v2/send_message.rs源码 ↗
这个文件像一个“前台收件员”。外部有人调用 send_message 工具时,它先说明自己叫什么、工具长什么样,再接住这次调用。真正执行时,它会从调用内容里取出参数,把参数解析成 SendMessageArgs,也就是“发给谁”和“发什么”。然后它调用统一的消息发送函数 handle_message_string_tool,并指定 MessageDeliveryMode::QueueOnly,意思是只把消息放进队列,不在这里直接完成后续投递。这样做的好处是发送入口很薄、规则很统一:这个文件只管接单和转交,实际消息处理交给公共逻辑,避免每个消息工具各写一套。它还声明自己只接受函数式工具调用,也就是 payload 必须是 ToolPayload::Function 这种形态。
Handler::tool_name11–13 ↗
fn tool_name(&self) -> ToolName
作用:告诉系统这个处理器对应的工具名叫“send_message”。系统靠这个名字把一次工具调用分派到正确的处理器。
数据流:进去的是这个 Handler 本身,不需要读取复杂数据;它调用 ToolName::plain,把普通字符串“send_message”包装成系统认识的工具名;出来的是一个 ToolName,供注册和匹配工具时使用。
调用关系:这是工具识别流程的入口之一。系统查看各个处理器时会问它“你叫什么”,这个函数把名字交出来;它只把工作交给 plain 来创建标准工具名,不参与真正发消息。
调用图:调用 1 个内部函数(plain)。
Handler::spec15–17 ↗
fn spec(&self) -> ToolSpec
作用:告诉系统 send_message 工具的说明书是什么。说明书会描述这个工具需要哪些参数,让调用方知道该怎么正确填写。
数据流:进去的是 Handler 本身;它调用 create_send_message_tool 生成 send_message 的 ToolSpec,也就是工具规格;出来的是这份规格,供系统展示、校验或提供给模型使用。
调用关系:在工具准备或暴露给调用方时会用到它。它不自己拼说明书,而是交给 create_send_message_tool,保证这个工具的定义和别处的规范保持一致。
调用图:调用 1 个内部函数(create_send_message_tool)。
Handler::handle19–21 ↗
fn handle(&self, invocation: ToolInvocation) -> codex_tools::ToolExecutorFuture<'_>
作用:接住一次真正的 send_message 调用,并把它包装成异步任务。异步任务可以理解为“这件事稍后会完成”,适合发送消息这种可能要等待的操作。
数据流:进去的是一次 ToolInvocation,也就是一次工具调用的完整信息;它把调用交给 handle_call,并用 Box::pin 包起来,让系统能按统一格式等待结果;出来的是一个 ToolExecutorFuture,表示还没完成但可以被等待的执行过程。
调用关系:这是系统实际执行工具时会碰到的门面函数。它自己不解析参数、不发送消息,只负责把活儿转给 Handler::handle_call,并把返回形式整理成工具框架需要的样子。
调用图:调用 1 个内部函数(handle_call);外部调用 1 个(pin)。
Handler::handle_call25–39 ↗
async fn handle_call(
&self,
invocation: ToolInvocation,
) -> Result<Box<dyn crate::tools::context::ToolOutput>, FunctionCallError>
作用:真正处理 send_message 的调用:拿到参数,读出目标和消息,然后把消息按“只入队”的方式交给公共消息发送逻辑。
数据流:进去的是一次 ToolInvocation;它先从 invocation.payload 里取出函数参数,再解析成 SendMessageArgs,得到 target 和 message;接着调用 handle_message_string_tool,把原始调用、QueueOnly 模式、目标和消息一起交出去;出来的是工具输出,或者在参数不对、发送逻辑失败时返回错误。
调用关系:它由 Handler::handle 调用,是这个文件里真正干活的函数。它不自己维护队列,也不自己决定消息如何流转,而是把核心工作交给 handle_message_string_tool;最后再把结果包装成系统统一的工具输出。
调用图:调用 1 个内部函数(handle_message_string_tool);被 1 处调用(handle)。
Handler::matches_kind43–45 ↗
fn matches_kind(&self, payload: &ToolPayload) -> bool
作用:判断某个工具载荷是不是这个处理器能处理的类型。这里它只接受函数调用形式的载荷。
数据流:进去的是一个 ToolPayload;它用模式匹配检查这个载荷是不是 ToolPayload::Function;出来的是 true 或 false,告诉系统这个 Handler 是否适合处理它。
调用关系:这是工具运行时分派前的筛选步骤。系统拿到一个 payload 后,可以先问这个函数“你认不认这种格式”;如果是函数式调用,才继续走 send_message 的处理流程。
调用图:外部调用 1 个(matches!)。
core/src/tools/handlers/multi_agents_v2/followup_task.rs源码 ↗
可以把这个文件想成一个“前台接单员”。外部系统说:“我要调用 followup_task。”它先确认自己就是处理这个名字的工具,再提供这个工具的说明书,告诉系统该怎么调用。真正收到调用时,它会从调用内容里取出参数,把参数解析成 FollowupTaskArgs,也就是“发给谁”和“说什么”。然后它不自己发送消息,而是交给 handle_message_string_tool 这个统一的消息工具去做,并指定发送方式是 TriggerTurn,意思是这条消息会触发目标继续行动。这样做的好处是:后续任务和普通消息走同一条可靠通道,不会每个工具都各写一套发送逻辑。最后,matches_kind 只接受函数式调用,避免把不合适的输入误当成这个工具来处理。
Handler::tool_name11–13 ↗
fn tool_name(&self) -> ToolName
作用:告诉工具系统:这个处理器对应的工具名字叫 followup_task。系统靠这个名字把一次工具调用分派到正确的处理器。
数据流:它不需要外部输入,只生成一个普通的工具名 followup_task,然后把这个名字交还给调用方。它不改动任何状态。
调用关系:当工具系统登记或查找处理器时,会问这个处理器叫什么。这里通过 plain 创建标准格式的工具名,方便外层系统按名字匹配。
调用图:调用 1 个内部函数(plain)。
Handler::spec15–17 ↗
fn spec(&self) -> ToolSpec
作用:提供 followup_task 这个工具的“说明书”。这份说明书告诉模型或调用方:这个工具需要什么参数、能做什么。
数据流:它不读取调用内容,只调用 create_followup_task_tool 生成工具规格,然后返回出去。它不发送消息,也不执行任务。
调用关系:在工具列表被展示或注册时,系统会调用它拿到工具规格。具体规格由 create_followup_task_tool 生成,这个处理器只负责把规格交出去。
调用图:调用 1 个内部函数(create_followup_task_tool)。
Handler::handle19–21 ↗
fn handle(&self, invocation: ToolInvocation) -> codex_tools::ToolExecutorFuture<'_>
作用:这是收到 followup_task 调用后的入口函数。它把实际工作包装成一个异步任务,让系统可以等待处理结果而不阻塞当前流程。
数据流:输入是一份工具调用 invocation。它把这份调用交给 handle_call,再用 pin 固定成系统需要的异步返回形式,最后返回这个未来会完成的任务。
调用关系:外层工具运行器真正执行工具时会调用它。它本身不解析参数、不发消息,只是把工作转交给 Handler::handle_call,相当于把接到的单子递给后台处理。
调用图:调用 1 个内部函数(handle_call);外部调用 1 个(pin)。
Handler::handle_call25–39 ↗
async fn handle_call(
&self,
invocation: ToolInvocation,
) -> Result<Box<dyn crate::tools::context::ToolOutput>, FunctionCallError>
作用:这是这个文件里真正处理 followup_task 的地方。它把原始调用参数拆开,找出目标和消息内容,然后触发一次后续任务消息。
数据流:输入是一份 ToolInvocation,里面带着原始参数。它先从 payload 里取出函数参数,再解析成 FollowupTaskArgs,得到 target 和 message;接着调用 handle_message_string_tool,用 TriggerTurn 模式把消息发给目标;成功后把结果包装成统一的工具输出,失败则返回错误。
调用关系:它由 Handler::handle 调用,是实际干活的后台步骤。它不直接实现消息传递,而是把发送工作交给 handle_message_string_tool,这样 followup task 只是决定“发给谁、发什么、用会触发下一轮的方式发”。
调用图:调用 1 个内部函数(handle_message_string_tool);被 1 处调用(handle)。
Handler::matches_kind43–45 ↗
fn matches_kind(&self, payload: &ToolPayload) -> bool
作用:判断一份工具输入是不是这个处理器能处理的类型。它只接受函数调用形式的输入,避免错误数据进入后面的解析流程。
数据流:输入是一份 ToolPayload。它检查这份载荷是不是 Function 这一类;是就返回 true,不是就返回 false。它不会修改输入。
调用关系:工具运行时在分派调用前会用它做一道门禁。这里借助 matches! 做模式判断,只有通过检查的调用才会继续进入 handle 和 handle_call。
调用图:外部调用 1 个(matches!)。
core/src/tools/handlers/multi_agents_v2/interrupt_agent.rs源码 ↗
可以把这里的“agent”想成被派出去办事的临时助手。这个文件就是“喊停某个助手”的按钮背后的代码。它先声明这个工具叫什么、长什么样、什么时候能接收调用;真正执行时,会从工具请求里读出目标是谁,把用户给的目标名字解析成具体的代理编号。然后它会做几道安全检查:不能打断根代理,因为根代理不是被派生出来的助手;也不能让一个代理打断自己,否则流程会很混乱。确认目标存在后,它记录目标原来的状态,再请求系统中专门控制代理的组件去中断它。即使目标线程已经不见了,或内部代理已经死掉,这里也把它当成“已经不用再中断了”,不会把这种情况当成失败。最后,它会发出一条“这个子代理被中断了”的活动事件,并把目标之前的状态作为结果返回,方便调用方知道打断前它在做什么。
Handler::tool_name10–12 ↗
fn tool_name(&self) -> ToolName
作用:告诉工具系统:这个处理器对应的工具名叫“interrupt_agent”。工具系统靠这个名字把外部请求分发到正确的代码。
数据流:进去的是这个处理器本身,不需要读取额外数据 → 它用 plain 创建一个普通工具名 → 出来的是 ToolName,表示“interrupt_agent”这个名字。
调用关系:当工具框架登记或查找工具时会问 Handler 它叫什么;这里把名字交给 plain 包装成框架能识别的格式。
调用图:调用 1 个内部函数(plain)。
Handler::spec14–16 ↗
fn spec(&self) -> ToolSpec
作用:给出这个工具的说明书,也就是调用方应该传什么参数、这个工具大概怎么用。没有这份说明,模型或外部调用者就不知道该怎样正确发起“中断代理”的请求。
数据流:进去的是处理器本身 → 它调用 create_interrupt_agent_tool_v2 生成工具规格 → 出来的是 ToolSpec,供工具系统展示和校验使用。
调用关系:工具系统在暴露工具能力时会调用它;它把具体的规格创建工作交给 create_interrupt_agent_tool_v2。
调用图:调用 1 个内部函数(create_interrupt_agent_tool_v2)。
Handler::handle18–24 ↗
fn handle(&self, invocation: ToolInvocation) -> codex_tools::ToolExecutorFuture<'_>
作用:这是工具真正被调用时的入口。它把一次“interrupt_agent”调用包装成异步任务,让系统可以边等结果边继续处理其他事情。
数据流:进去的是一次 ToolInvocation,里面有会话、当前轮次、参数、调用编号等信息 → 它把调用交给 handle_interrupt_agent 执行,并把返回结果转换成统一的工具输出 → 出来的是一个未来才会完成的异步结果。
调用关系:工具框架在收到 interrupt_agent 调用后会进入这里;它自己不做复杂判断,而是把核心工作交给 handle_interrupt_agent,并用 pin 把异步任务固定成框架需要的形式。
调用图:调用 1 个内部函数(handle_interrupt_agent);外部调用 1 个(pin)。
handle_interrupt_agent27–91 ↗
async fn handle_interrupt_agent(
invocation: ToolInvocation,
) -> Result<InterruptAgentResult, FunctionCallError>
作用:这是本文件的核心函数,负责真正找到目标代理、检查能不能打断、执行中断,并把结果和事件发回系统。它像一个谨慎的调度员:先确认要叫停的是谁,再确认叫停这件事合不合理,最后才下命令。
数据流:进去的是一次工具调用,里面带着当前会话、当前轮次、工具参数和调用编号 → 它解析参数里的 target,找到对应代理编号,确认目标代理存在,并排除根代理和“自己打断自己”这两种不合理情况;然后读取目标当前状态,调用代理控制服务去中断它;接着用 now_unix_timestamp_ms 记录事件时间,发送“已中断”的子代理活动事件 → 出来的是 InterruptAgentResult,里面保存目标被打断前的状态;同时系统状态发生变化:目标代理收到中断请求,活动事件也被发出。
调用关系:Handler::handle 会把工具调用交给它。它是整个中断流程的主线:前半段负责理解和校验请求,后半段把中断命令交给代理控制服务;如果发现请求本身不合理,就用 RespondToModel 生成给模型看的错误说明。
调用图:调用 1 个内部函数(now_unix_timestamp_ms);被 1 处调用(handle);外部调用 1 个(RespondToModel)。
Handler::matches_kind94–96 ↗
fn matches_kind(&self, payload: &ToolPayload) -> bool
作用:判断传进来的工具载荷是不是函数调用格式。只有格式对了,这个处理器才应该接手。
数据流:进去的是一个 ToolPayload,也就是工具调用带来的原始载荷 → 它检查这个载荷是不是 Function 这一类 → 出来的是 true 或 false,表示是否匹配。
调用关系:工具运行时会用它来筛选处理器;它通过 matches! 这个模式匹配检查,快速决定当前载荷能不能交给 interrupt_agent 处理。
调用图:外部调用 1 个(matches!)。
InterruptAgentResult::log_preview111–113 ↗
fn log_preview(&self) -> String
作用:把中断结果整理成适合写日志的简短文本。这样开发者排查问题时,可以看到这次工具调用大致返回了什么。
数据流:进去的是 InterruptAgentResult,里面有目标代理之前的状态 → 它把结果按 interrupt_agent 的工具名格式化成日志文本 → 出来的是一段字符串。
调用关系:工具输出要写入日志时会调用它;它属于 InterruptAgentResult 这个结果对象的展示方式,不参与中断本身。
InterruptAgentResult::success_for_logging115–117 ↗
fn success_for_logging(&self) -> bool
作用:告诉日志系统:这个结果应该按成功来记录。这里固定返回成功,因为能走到这个结果对象,说明中断流程已经被认为完成了。
数据流:进去的是 InterruptAgentResult 本身 → 它不需要检查字段,直接给出 true → 出来的是一个布尔值,表示日志里记为成功。
调用关系:工具框架在记录执行结果时会问它成功与否;它只负责给日志一个结论,不会再调用其他流程。
InterruptAgentResult::to_response_item119–121 ↗
fn to_response_item(&self, call_id: &str, payload: &ToolPayload) -> ResponseInputItem
作用:把中断结果转换成可以发回给模型或上层调用方的响应条目。也就是说,它负责把内部结果包装成外部能看懂的回信。
数据流:进去的是调用编号 call_id、原始工具载荷 payload,以及 InterruptAgentResult → 它把这些信息按 interrupt_agent 的响应格式打包,并标记这次工具调用成功 → 出来的是 ResponseInputItem,供后续响应流程使用。
调用关系:工具框架准备把结果返回给调用方时会调用它;它不重新计算结果,只把已有结果装进统一的响应格式。
InterruptAgentResult::code_mode_result123–125 ↗
fn code_mode_result(&self, _payload: &ToolPayload) -> JsonValue
作用:把中断结果转换成代码模式下使用的 JSON 数据。JSON 可以理解成一种通用的数据包装格式,方便程序继续读取和处理。
数据流:进去的是 InterruptAgentResult 和原始 payload,但这里不需要使用 payload → 它把结果按 interrupt_agent 的名字转换成 JSON 值 → 出来的是 JsonValue,供代码模式消费。
调用关系:当系统处在代码模式、需要结构化结果而不是普通文本响应时会调用它;它负责结果格式转换,不参与代理中断流程。
core/src/tools/handlers/multi_agents_v2/list_agents.rs源码 ↗
这个文件像是 list_agents 这张“查询窗口”的接待员。外部发来一次工具调用后,Handler 先确认自己就是处理 list_agents 的人,再说明这个工具长什么样、需要什么参数。真正执行时,它会从调用内容里取出参数,其中 path_prefix 是可选的路径前缀,可以理解成“只看某个文件夹下面的代理”。接着它会告诉代理控制中心:当前会话和父线程是什么关系,方便后面按会话范围找东西。然后它向 agent_control 要代理列表,并把结果包装成 ListAgentsResult 返回。ListAgentsResult 不只是装数据,还知道怎么把结果写进日志、怎么变成工具响应、以及在代码模式下怎么输出。这里有个重要点:参数结构开启了 deny_unknown_fields,也就是调用者传了不认识的字段会被拒绝,能减少拼错参数却悄悄失败的问题。
Handler::tool_name9–11 ↗
fn tool_name(&self) -> ToolName
作用:告诉工具系统:这个处理器对应的工具名叫 list_agents。这样外部请求这个名字时,系统才能把请求交给它。
数据流:进去的是这个处理器本身,不需要额外输入 → 它用 plain 创建一个普通工具名 → 出来的是 ToolName,内容就是 list_agents。
调用关系:这是工具注册和匹配时会用到的身份牌。它把名字交给通用的 plain 构造函数,后续系统靠这个名字找到本文件里的处理器。
调用图:调用 1 个内部函数(plain)。
Handler::spec13–15 ↗
fn spec(&self) -> ToolSpec
作用:返回 list_agents 这个工具的规格说明,也就是它对外宣称自己能做什么、需要什么参数。调用方可以靠它知道怎么正确使用这个工具。
数据流:进去的是处理器本身 → 它调用 create_list_agents_tool 生成工具说明 → 出来的是一个 ToolSpec,相当于工具的说明书。
调用关系:它通常在工具被暴露给模型或客户端时使用。具体说明书的内容不在这里手写,而是交给 create_list_agents_tool 统一生成,避免多处定义不一致。
调用图:调用 1 个内部函数(create_list_agents_tool)。
Handler::handle17–19 ↗
fn handle(&self, invocation: ToolInvocation) -> codex_tools::ToolExecutorFuture<'_>
作用:接收一次真实的工具调用,并把它转成异步任务来执行。异步任务可以理解成“先把活儿派出去,等结果回来”,不会卡住整个系统。
数据流:进去的是一次 ToolInvocation,里面有会话、轮次、调用参数等信息 → 它把实际工作交给 handle_call,再用 pin 固定这个异步任务的位置 → 出来的是一个未来会完成的执行结果。
调用关系:这是工具运行时进入本处理器的主要入口。它本身不做业务判断,只是把调用包装好并交给 Handler::handle_call 去真正查询代理列表。
调用图:调用 1 个内部函数(handle_call);外部调用 1 个(pin)。
Handler::handle_call23–47 ↗
async fn handle_call(
&self,
invocation: ToolInvocation,
) -> Result<Box<dyn crate::tools::context::ToolOutput>, FunctionCallError>
作用:真正执行 list_agents 查询:解析调用参数,登记会话关系,向代理控制中心请求代理列表,然后把结果包装成工具输出。
数据流:进去的是一次工具调用,里面有 session、turn 和原始 payload → 它先从 payload 里取函数参数,再解析成 ListAgentsArgs;然后把当前线程和父线程关系登记到 agent_control;接着按会话来源和可选的 path_prefix 查询代理 → 出来的是装着代理列表的 ListAgentsResult;如果参数不对或查询失败,就返回对应错误。
调用关系:它由 Handler::handle 调用,是这个文件最核心的流程。它依赖通用参数解析函数来读输入,依赖 session.services.agent_control 来拿真实代理数据,最后把结果交给统一的工具输出包装函数,让外部能收到标准格式的响应。
调用图:被 1 处调用(handle)。
Handler::matches_kind51–53 ↗
fn matches_kind(&self, payload: &ToolPayload) -> bool
作用:判断某个工具调用的载荷是不是函数调用格式。只有格式对了,这个处理器才适合处理。
数据流:进去的是一个 ToolPayload → 它检查这个载荷是否是 Function 类型 → 出来的是 true 或 false,表示能不能由这个处理器接。
调用关系:这是运行时分发工具调用前的筛选步骤。它用 Rust 的 matches! 宏做简单判断,避免把非函数类的载荷误送进 list_agents 的处理流程。
调用图:外部调用 1 个(matches!)。
ListAgentsResult::log_preview68–70 ↗
fn log_preview(&self) -> String
作用:生成一段适合写进日志的结果预览。这样开发者或运维人员能看到这次 list_agents 大概返回了什么。
数据流:进去的是已经查询到的 ListAgentsResult → 它把结果按 list_agents 的名字转成 JSON 文本 → 出来的是一段字符串,用于日志展示。
调用关系:这是工具输出被记录时会调用的方法。它不重新计算代理列表,只负责把已有结果交给统一的 JSON 日志格式化工具。
ListAgentsResult::success_for_logging72–74 ↗
fn success_for_logging(&self) -> bool
作用:告诉日志系统:这个输出代表一次成功的工具调用。这里固定返回成功,因为能构造出这个结果就说明查询流程已经正常完成。
数据流:进去的是结果对象本身 → 它不读取具体代理内容,只直接给出成功标记 → 出来的是 true。
调用关系:这是日志记录流程中的状态标记。它配合 log_preview 使用,让日志既有内容预览,也知道这次调用应算成功。
ListAgentsResult::to_response_item76–78 ↗
fn to_response_item(&self, call_id: &str, payload: &ToolPayload) -> ResponseInputItem
作用:把代理列表结果转换成外部协议能接收的响应项。简单说,就是把内部数据打包成“可以发回给调用者”的格式。
数据流:进去的是结果对象、这次调用的 call_id 和原始 payload → 它把这些信息连同成功标记和工具名 list_agents 一起交给统一响应构造函数 → 出来的是一个 ResponseInputItem,用于返回给上层系统或模型。
调用关系:这是工具执行完成后生成正式回复的步骤。它不关心代理怎么查出来,只负责把 ListAgentsResult 交给通用的 tool_output_response_item,保持所有工具响应格式一致。
ListAgentsResult::code_mode_result80–82 ↗
fn code_mode_result(&self, _payload: &ToolPayload) -> JsonValue
作用:在代码模式下,把代理列表结果转换成 JSON 值。JSON 可以理解成一种常见的数据交换格式,方便程序继续读取和处理。
数据流:进去的是结果对象和原始载荷;这里不需要使用载荷内容 → 它把结果按 list_agents 的名字包装成代码模式需要的 JSON → 出来的是一个 JsonValue。
调用关系:这是给代码模式或自动化流程读取结果时用的出口。它和普通响应、日志预览走不同通道,但都基于同一个 ListAgentsResult,并交给统一的输出转换函数完成格式化。
core/src/tools/handlers/multi_agents_v2/wait.rs源码 ↗
这个文件像一个“等候室管理员”。当模型调用 wait_agent 时,它先读出模型给的等待时间,并检查这个时间不能太短也不能太长,防止模型乱填导致系统卡住或刷得太快。然后它订阅输入队列里的活动变化:如果已经有待处理的新消息,就立刻结束等待;如果没有,就最多等到指定时间。等待期间,它会发出“开始等待”和“结束等待”的事件,方便外部界面或日志知道系统正在等、什么时候等完。最后它把结果包装成工具输出,告诉模型是正常等到活动、被新输入打断,还是超时了。这里的“watch 接收器”可以理解成一个消息提示器:别人一有动静,它就响一下。
Handler::new18–20 ↗
fn new(options: WaitAgentTimeoutOptions) -> Self
作用:创建一个 wait_agent 工具处理器,并把等待工具的超时选项保存进去。别人要启用这个工具时,会先用它造出一个可用的处理器。
数据流:进去的是一份等待超时配置 → 函数把它放进 Handler 结构里 → 出来的是一个新的 Handler,后面可以用它处理 wait_agent 调用。
调用关系:它是这个处理器的入口构造函数。系统初始化工具列表时通常会先调用它,之后 tool_name、spec、handle 等函数才会围绕这个处理器工作。
Handler::tool_name24–26 ↗
fn tool_name(&self) -> ToolName
作用:告诉工具系统:这个处理器对应的工具名叫 wait_agent。这样模型请求 wait_agent 时,系统才能找到它。
数据流:进去的是这个处理器本身 → 函数生成一个普通工具名 wait_agent → 出来的是工具系统能识别的名字对象。
调用关系:它在工具注册或匹配时被使用。内部把名字创建工作交给 plain,相当于把普通文本变成系统统一使用的工具名格式。
调用图:调用 1 个内部函数(plain)。
Handler::spec28–30 ↗
fn spec(&self) -> ToolSpec
作用:生成 wait_agent 的工具说明书,告诉模型这个工具怎么调用、能填哪些参数。没有这份说明,模型就不知道等待工具的正确用法。
数据流:进去的是处理器里保存的超时选项 → 函数把这些选项交给 create_wait_agent_tool_v2 → 出来的是一份 ToolSpec,也就是工具规格说明。
调用关系:它在工具暴露给模型之前使用。真正拼装说明书的细节交给 create_wait_agent_tool_v2,这个函数只负责把当前配置传过去。
调用图:调用 1 个内部函数(create_wait_agent_tool_v2)。
Handler::handle32–34 ↗
fn handle(&self, invocation: ToolInvocation) -> codex_tools::ToolExecutorFuture<'_>
作用:接住一次模型发来的 wait_agent 调用,并把它转成异步任务去执行。异步任务的意思是:等待期间不会把整个程序堵死。
数据流:进去的是一次工具调用,里面有会话、当前轮次、参数和调用编号 → 函数把实际工作交给 handle_call,并用 pin 包成系统需要的异步返回形式 → 出来的是一个将来会完成的工具执行任务。
调用关系:它是工具执行框架直接调用的入口。它自己不做等待细节,而是立刻转交给 handle_call,让后者完成参数检查、订阅活动、等待和返回结果。
调用图:调用 1 个内部函数(handle_call);外部调用 1 个(pin)。
Handler::handle_call38–111 ↗
async fn handle_call(
&self,
invocation: ToolInvocation,
) -> Result<Box<dyn crate::tools::context::ToolOutput>, FunctionCallError>
作用:这是 wait_agent 的核心流程:解析等待时间,检查是否合法,开始监听新输入,等待到有动静或超时,然后返回结果。它保证等待行为受配置约束,不会无限等,也不会用离谱的时间。
数据流:进去的是一次完整工具调用,包含会话、当前轮次、模型传来的参数和调用编号 → 它先解析 timeout_ms,再用当前配置检查最小值、最大值和默认值;接着从输入队列拿到当前状态并订阅后续活动;然后发送“开始等待”事件,算出截止时间,调用 wait_for_activity 真正等待;等到结果后转成 WaitAgentResult,再发送“结束等待”事件 → 出来的是包装好的工具输出;如果等待时间不合法,则直接返回给模型一条错误说明。
调用关系:它由 Handler::handle 调用,是整条等待工具链的主干。它会调用 wait_for_activity 做实际等待,用 now_unix_timestamp_ms 记录事件时间,用 WaitAgentResult::from_outcome 把内部等待结果翻译成模型看得懂的输出。
调用图:调用 2 个内部函数(wait_for_activity, now_unix_timestamp_ms);被 1 处调用(handle);外部调用 7 个(from_millis, new, now, new, from_outcome, format!, RespondToModel)。
Handler::matches_kind115–117 ↗
fn matches_kind(&self, payload: &ToolPayload) -> bool
作用:判断传进来的工具请求是不是函数调用类型。这个处理器只接受函数式工具调用,不处理别的 payload 形态。
数据流:进去的是一个工具请求的原始载荷 → 函数检查它是否属于 ToolPayload::Function → 出来的是 true 或 false,表示这个处理器能不能接。
调用关系:它在工具运行时做分流判断。系统拿到一个工具请求后,会用这类函数确认该处理器是否适合处理这个请求。
调用图:外部调用 1 个(matches!)。
WaitAgentResult::from_outcome133–143 ↗
fn from_outcome(outcome: WaitOutcome) -> Self
作用:把内部等待结果翻译成最终返回给模型的结果文字。这样模型不用理解系统内部的枚举值,只看到清楚的人话。
数据流:进去的是一个 WaitOutcome,表示等到了邮箱活动、被新输入打断,或超时 → 函数选择对应提示语,并设置 timed_out 是否为真 → 出来的是一个 WaitAgentResult,包含消息和是否超时。
调用关系:它在 Handler::handle_call 等待结束后被使用。wait_for_activity 给出内部结果,它负责把这个结果换成工具输出结构。
WaitAgentResult::log_preview147–149 ↗
fn log_preview(&self) -> String
作用:生成一段适合写进日志里的简短预览,方便调试或查看历史时知道 wait_agent 返回了什么。
数据流:进去的是等待结果本身 → 函数把它按 wait_agent 工具输出的 JSON 文本格式整理 → 出来的是一段字符串日志预览。
调用关系:它是工具输出接口的一部分。日志系统需要展示工具结果时会调用它,具体格式化工作交给通用的 tool_output_json_text。
WaitAgentResult::success_for_logging151–153 ↗
fn success_for_logging(&self) -> bool
作用:告诉日志系统:这次工具调用在日志上应当算成功。即使等待超时,也不是程序错误,而是这个工具的一种正常结果。
数据流:进去的是等待结果本身 → 函数不需要检查内容,固定返回 true → 出来的是“记录为成功”的判断。
调用关系:它是工具输出接口的一部分。日志记录工具调用结果时会问它成功与否,这里统一回答成功,因为超时也属于预期行为。
WaitAgentResult::to_response_item155–157 ↗
fn to_response_item(&self, call_id: &str, payload: &ToolPayload) -> ResponseInputItem
作用:把等待结果变成可以发回给模型的响应项。模型最终看到的工具返回内容就是通过这里包装出来的。
数据流:进去的是调用编号、原始工具载荷和等待结果 → 函数把这些信息按 wait_agent 的响应格式组合起来 → 出来的是一个 ResponseInputItem,也就是可放回对话流里的工具结果项。
调用关系:它是工具输出接口的一部分。工具框架准备把结果交还给模型时会调用它,实际包装细节交给通用的 tool_output_response_item。
WaitAgentResult::code_mode_result159–161 ↗
fn code_mode_result(&self, _payload: &ToolPayload) -> JsonValue
作用:把等待结果转换成代码模式下使用的 JSON 值。这样在偏代码化的执行环境里,也能用结构化数据读取结果。
数据流:进去的是等待结果和原始工具载荷 → 函数忽略这里不需要的载荷,只把结果按 wait_agent 的代码模式格式转成 JSON → 出来的是一个 JSON 值。
调用关系:它是工具输出接口的一部分。代码模式需要结构化结果时会调用它,具体转换交给通用的 tool_output_code_mode_result。
wait_for_activity171–189 ↗
async fn wait_for_activity(
activity_rx: &mut tokio::sync::watch::Receiver<InputQueueActivity>,
pending_activity: Option<InputQueueActivity>,
deadline: Instant,
) -> WaitOutcome
作用:真正执行“等一等”的动作:如果已经有活动就马上返回;否则一直等到新活动出现或截止时间到。它是这个文件里最像闹钟的部分。
数据流:进去的是一个活动接收器、一个可能已经存在的待处理活动,以及等待截止时间 → 函数先看待处理活动;如果有,就直接判断是邮箱消息还是新输入打断;如果没有,就等待接收器变化,但最多等到截止时间;收到变化后读取最新活动 → 出来的是 WaitOutcome,说明等到了邮箱活动、被打断,还是超时。
调用关系:它由 Handler::handle_call 调用,专门负责等待本身。它使用 timeout_at 给等待加上截止时间,用 changed 等待活动变化,再用 borrow_and_update 读取最新活动并标记已处理。
调用图:被 1 处调用(handle_call);外部调用 3 个(borrow_and_update, changed, timeout_at)。
委托与审查工作流
这些文件涵盖更高层的嵌套会话工作流,用于运行委托的子 Codex 线程,将 code-mode 执行桥接到嵌套轮次,并实现受约束的 review-mode 子 agent。
core/src/codex_delegate.rs源码 ↗
可以把这个文件理解成“主助手和子助手之间的传话员兼安全员”。它会创建子 Codex,继承主会话的说明、环境、权限策略等,再开两条通道:一条把用户或上层发来的操作转给子助手,另一条把子助手产生的事件转回来。普通进展会直接转发,但危险或需要确认的事,比如执行命令、修改文件、申请权限、询问用户,会被拦下来交给主会话或 Guardian(守护审查器,用来替用户审查高风险操作)处理。它还特别照顾 MCP 工具调用,MCP 是让模型调用外部工具的一套协议;有些旧式批准请求信息不完整,所以这里会缓存工具调用上下文,之后补齐给审查器。取消或通道关闭时,它会尽量通知子助手停止,避免后台任务还在往已关闭的通道里发消息。
run_codex_thread_interactive69–174 ↗
async fn run_codex_thread_interactive(
config: Config,
auth_manager: Arc<AuthManager>,
models_manager: SharedModelsManager,
parent_session: Arc<Session>,
parent_ctx: Arc<TurnContex
作用:启动一个可以持续交互的子 Codex 线程,并把它包装成调用方能收发消息的对象。有人想让子助手长期做事、后续还能继续给它发指令时,会用这个函数。
数据流:进去的是配置、登录信息、模型管理器、父会话、父回合上下文、取消信号、子助手来源和可选历史记录。它创建收发通道,拿到父会话的用户说明、环境、权限策略等,调用 Codex::spawn 真正启动子线程,然后发出“子会话已开始”的统计事件,再启动两个后台任务:一个转发事件,一个转发操作。出来的是一个新的 Codex 包装对象;调用方拿它可以给子助手发操作,也可以读子助手的事件。
调用关系:这是交互式子线程的总入口。run_codex_thread_one_shot 会先调用它再追加一次性输入,spawn_guardian_review_session 也会用它开审查用的子会话。它把事件侧的工作交给 forward_events,把操作侧的工作交给 forward_ops,并依赖 Codex::spawn 完成真正的子会话创建。
调用图:调用 5 个内部函数(forward_events, forward_ops, spawn, emit_subagent_session_started, disabled);被 2 处调用(run_codex_thread_one_shot, spawn_guardian_review_session);外部调用 12 个(clone, new, pin, child_token, new, new, new, SubAgent, bounded, default (+2 more))。
run_codex_thread_one_shot180–259 ↗
async fn run_codex_thread_one_shot(
config: Config,
auth_manager: Arc<AuthManager>,
models_manager: SharedModelsManager,
input: Vec<UserInput>,
parent_session: Arc<Session>,
pa
作用:启动一个“一次性”的子 Codex:给它一段初始输入,让它跑完这一轮后自动收尾。适合只想问子助手一个问题或让它完成一个短任务的场景。
数据流:进去的是启动子线程所需的信息,加上一组用户输入、可选的最终 JSON 输出格式和可选历史记录。它先创建一个子取消信号,再调用 run_codex_thread_interactive 启动交互式子线程,马上提交初始输入。随后它搭一座事件桥,边把事件转给调用方,边观察是否出现完成或中止;一旦结束,就发送关闭操作并取消子任务。出来的 Codex 只能读事件,发送通道被故意关闭,避免调用方继续塞新任务。
调用关系:start_review_conversation 会用它发起一次性审查对话。它本身不重新造子线程,而是复用 run_codex_thread_interactive,然后用一个后台任务监听完成事件并负责自动关机。
调用图:调用 1 个内部函数(run_codex_thread_interactive);被 1 处调用(start_review_conversation);外部调用 7 个(clone, pin, child_token, default, bounded, matches!, spawn)。
forward_events261–400 ↗
async fn forward_events(
codex: Arc<Codex>,
tx_sub: Sender<Event>,
parent_session: Arc<Session>,
parent_ctx: Arc<TurnContext>,
pending_mcp_invocations: Arc<Mutex<HashMap<String, Mc
作用:持续读取子助手产生的事件,并决定哪些该给外部看,哪些该先交给父会话审批。它是子助手输出方向的“分拣员”。
数据流:进去的是子 Codex、给调用方的事件发送通道、父会话、父上下文、MCP 调用缓存和取消信号。它循环等待两件事:要么收到取消,要么读到子助手事件。普通事件会转发;计数和配置类噪音会丢掉;执行命令、打补丁、申请权限、询问用户这类事件会转给对应处理函数;MCP 工具开始时会缓存调用信息,结束时会清掉缓存。出来没有直接返回值,但它会不断把合适的事件送出,或在取消、出错时停止子助手。
调用关系:run_codex_thread_interactive 启动它作为后台任务。它处在子助手事件流的正中间,遇到需要批准的事会交给 handle_exec_approval、handle_patch_approval、handle_request_permissions、handle_request_user_input,普通事件则通过 forward_event_or_shutdown 送给调用方。
调用图:被 1 处调用(run_codex_thread_interactive);外部调用 3 个(cancelled, pin!, select!)。
shutdown_delegate403–418 ↗
async fn shutdown_delegate(codex: &Codex)
作用:请子助手停下来,并短暂清空它还没发完的事件。这样可以减少“接收方已经关门,发送方还在敲门”的错误。
数据流:进去的是一个 Codex。它先提交 Interrupt(打断当前工作),再提交 Shutdown(关闭会话),然后最多等 500 毫秒继续读取事件,直到看到回合完成或中止,或超时为止。出来没有业务结果;它的效果是尽力让子助手安静退出。
调用关系:forward_event_or_shutdown 在事件发不出去时会调用它;forward_events 在收到取消信号时也会用它收尾。它依赖 Codex 的 submit 和 next_event 来告诉子助手停机并排空尾部事件。
调用图:调用 2 个内部函数(next_event, submit);被 1 处调用(forward_event_or_shutdown);外部调用 3 个(from_millis, matches!, timeout)。
forward_event_or_shutdown420–433 ↗
async fn forward_event_or_shutdown(
codex: &Codex,
tx_sub: &Sender<Event>,
cancel_token: &CancellationToken,
event: Event,
) -> bool
作用:把一个子助手事件发给调用方;如果发不出去,就立刻关闭子助手。它保证事件通道断开时,后台子任务不会继续空转。
数据流:进去的是子 Codex、事件发送通道、取消信号和一个事件。它尝试在尊重取消信号的前提下发送事件。发送成功就返回 true;发送失败或被取消,就调用 shutdown_delegate 停掉子助手,并返回 false。
调用关系:它是 forward_events 转发普通事件时使用的小工具。forward_events 根据它返回的 true 或 false 决定继续循环还是退出;失败路径会把收尾工作交给 shutdown_delegate。
调用图:调用 1 个内部函数(shutdown_delegate);外部调用 1 个(send)。
forward_ops436–448 ↗
async fn forward_ops(
codex: Arc<Codex>,
rx_ops: Receiver<Submission>,
cancel_token_ops: CancellationToken,
)
作用:把调用方发来的操作一条条转交给子助手。它是输入方向的“传送带”。
数据流:进去的是子 Codex、操作接收通道和取消信号。它循环从通道里收 Submission(一次提交的操作),收到后用 submit_with_id 原样交给子助手。通道关闭、接收失败或取消时,它退出。出来没有直接结果,但子助手会收到这些操作并继续运行。
调用关系:run_codex_thread_interactive 会把它作为后台任务启动。调用方通过返回的 tx_sub 发送操作,forward_ops 负责把这些操作送进真正的子 Codex。
调用图:调用 1 个内部函数(recv);被 1 处调用(run_codex_thread_interactive)。
handle_exec_approval451–531 ↗
async fn handle_exec_approval(
codex: &Codex,
turn_id: String,
parent_session: &Arc<Session>,
parent_ctx: &Arc<TurnContext>,
event: ExecApprovalRequestEvent,
cancel_token: &Can
作用:处理子助手想执行命令时的批准请求。它不会让子助手自己决定,而是问父会话或 Guardian 后再回复。
数据流:进去的是子 Codex、当前事件编号、父会话、父上下文、执行命令批准事件和取消信号。它先取出命令、目录、原因、额外权限等信息;如果当前上下文要求走 Guardian,就创建审查请求并等待审查结果;否则就调用父会话的命令批准流程。等待期间如果取消,会返回 Abort。最后它把批准或拒绝结果用 ExecApproval 操作发回子助手。
调用关系:forward_events 在看到 ExecApprovalRequest 事件时会进入这里。它把等待结果的通用逻辑交给 await_approval_with_cancel;如果走 Guardian,会用 new_guardian_review_id 和 spawn_approval_request_review 创建审查。
调用图:调用 3 个内部函数(await_approval_with_cancel, submit, effective_approval_id);外部调用 5 个(clone, child_token, new_guardian_review_id, routes_approval_to_guardian, spawn_approval_request_review)。
handle_patch_approval534–635 ↗
async fn handle_patch_approval(
codex: &Codex,
_id: String,
parent_session: &Arc<Session>,
parent_ctx: &Arc<TurnContext>,
event: ApplyPatchApprovalRequestEvent,
cancel_token: &
作用:处理子助手想修改文件时的批准请求。它会把要改哪些文件、怎么改,整理后交给父会话或 Guardian 判断。
数据流:进去的是子 Codex、父会话、父上下文、补丁批准事件和取消信号。它取出文件变更、原因和授权范围;如果走 Guardian,就把新增、删除、更新、移动等变化拼成易审查的补丁文本,并生成受影响文件列表。若 Guardian 不处理或没有结果,就改问父会话的补丁批准流程。最终把 ReviewDecision 通过 PatchApproval 发回子助手。
调用关系:forward_events 碰到 ApplyPatchApprovalRequest 时会调用它。它和 handle_exec_approval 一样复用 await_approval_with_cancel 来处理“等批准但可能被取消”的情况,也可能通过 spawn_approval_request_review 启动 Guardian 审查。
调用图:调用 2 个内部函数(await_approval_with_cancel, submit);外部调用 5 个(clone, child_token, new_guardian_review_id, routes_approval_to_guardian, spawn_approval_request_review)。
handle_request_user_input637–673 ↗
async fn handle_request_user_input(
codex: &Codex,
id: String,
parent_session: &Arc<Session>,
parent_ctx: &Arc<TurnContext>,
pending_mcp_invocations: &Arc<Mutex<HashMap<String, Mcp
作用:处理子助手向用户提问的情况。它会先看看这个问题是不是可以由 Guardian 自动回答的 MCP 工具批准;不能自动处理时才真正转问用户。
数据流:进去的是子 Codex、问题事件编号、父会话、父上下文、待处理 MCP 调用缓存、用户输入请求事件和取消信号。它先调用 maybe_auto_review_mcp_request_user_input 尝试自动审查并生成回答;如果成功,就直接把回答交回子助手。否则它把问题包装成请求,让父会话去问用户,再通过 await_user_input_with_cancel 等待回答或取消。最后用 UserInputAnswer 把结果发回子助手。
调用关系:forward_events 在收到 RequestUserInput 事件时会调用它。它先把特殊 MCP 批准路径交给 maybe_auto_review_mcp_request_user_input,普通用户问题则交给父会话和 await_user_input_with_cancel。
调用图:调用 3 个内部函数(await_user_input_with_cancel, maybe_auto_review_mcp_request_user_input, submit)。
maybe_auto_review_mcp_request_user_input682–757 ↗
async fn maybe_auto_review_mcp_request_user_input(
parent_session: &Arc<Session>,
parent_ctx: &Arc<TurnContext>,
pending_mcp_invocations: &Arc<Mutex<HashMap<String, McpInvocation>>>,
e
作用:识别一种旧式 MCP 工具批准问题,并在 Guardian 可用时自动完成审查和回答。这样用户不用看到一条信息不完整的老式确认问题。
数据流:进去的是父会话、父上下文、MCP 调用缓存、用户输入事件和取消信号。它先找问题里是否有 MCP 工具批准标记;再用 call_id 从缓存里找回完整工具调用;接着查询工具元数据,判断是否应该走 Guardian。如果不符合条件,返回 None。符合时,它构造 Guardian 审查请求并等待决定,然后把批准、批准本会话、拒绝、超时或中止等结果翻译成该问题选项里的文字标签,最后返回一个 RequestUserInputResponse。
调用关系:handle_request_user_input 会先调用它试图自动处理。它需要 forward_events 之前缓存的 McpToolCallBegin 信息;审查请求由 build_guardian_mcp_tool_review_request 组装,等待和取消逻辑交给 await_approval_with_cancel。
调用图:调用 4 个内部函数(await_approval_with_cancel, build_guardian_mcp_tool_review_request, lookup_mcp_tool_metadata, mcp_approvals_reviewer);被 1 处调用(handle_request_user_input);外部调用 7 个(clone, child_token, from, new_guardian_review_id, routes_approval_to_guardian_with_reviewer, spawn_approval_request_review, vec!)。
handle_request_permissions759–792 ↗
async fn handle_request_permissions(
codex: &Codex,
parent_session: &Arc<Session>,
parent_ctx: &Arc<TurnContext>,
event: RequestPermissionsEvent,
cancel_token: &CancellationToken,
作用:处理子助手申请更多权限的请求,比如某个工作目录下需要额外访问能力。它把申请转给父会话,并把结果送回子助手。
数据流:进去的是子 Codex、父会话、父上下文、权限请求事件和取消信号。它取出 call_id、环境编号、理由和权限列表,并确定工作目录;如果事件没给目录,就用父上下文的当前目录。然后它调用父会话的权限申请流程,并通过 await_request_permissions_with_cancel 等待结果。最后用 RequestPermissionsResponse 操作把允许的权限和范围交还给子助手。
调用关系:forward_events 遇到 RequestPermissions 事件时会调用它。它把实际询问父会话的结果交给 await_request_permissions_with_cancel 包一层取消保护,再通过 Codex submit 回答子助手。
调用图:调用 2 个内部函数(await_request_permissions_with_cancel, submit);外部调用 1 个(clone)。
await_user_input_with_cancel794–818 ↗
async fn await_user_input_with_cancel(
fut: F,
parent_session: &Session,
sub_id: &str,
cancel_token: &CancellationToken,
) -> RequestUserInputResponse
作用:等待用户输入,但如果整个子任务被取消,就立刻给出一个空回答并通知父会话。它避免一个等待用户的问题永远挂在那里。
数据流:进去的是一个正在等待用户回答的任务、父会话、子订阅编号和取消信号。它同时等“用户回答到达”和“取消发生”。如果取消先发生,它构造一个没有答案的响应,通知父会话这个问题已经用空回答结束,并返回空响应;如果回答先到,就返回回答,没有回答则也补成空响应。
调用关系:handle_request_user_input 用它来等待普通用户问题的结果。它不处理问题内容,只负责把等待过程变成安全的“有结果或空结果”。
调用图:被 1 处调用(handle_request_user_input);外部调用 1 个(select!)。
await_request_permissions_with_cancel820–848 ↗
async fn await_request_permissions_with_cancel(
fut: F,
parent_session: &Session,
call_id: &str,
cancel_token: &CancellationToken,
) -> RequestPermissionsResponse
作用:等待权限申请的结果,但在取消时返回一份“不给额外权限”的安全默认值。这样子助手不会因为取消而误以为权限被批准。
数据流:进去的是一个等待权限响应的任务、父会话、申请编号和取消信号。它同时等响应和取消。如果取消先到,它生成空权限、只对当前回合有效的默认响应,并通知父会话;如果响应先到,就返回响应,没有响应则同样使用空权限默认值。
调用关系:handle_request_permissions 用它包住父会话的权限申请流程。它的职责是保证权限请求无论正常完成还是被取消,都有一个明确、安全的答复。
调用图:被 1 处调用(handle_request_permissions);外部调用 1 个(select!)。
await_approval_with_cancel851–876 ↗
async fn await_approval_with_cancel(
fut: F,
parent_session: &Session,
approval_id: &str,
cancel_token: &CancellationToken,
review_cancel_token: Option<&CancellationToken>,
) -> co
作用:等待批准结果,但如果任务被取消,就把结果改成 Abort(中止)。它是执行命令、改文件、MCP 工具审查这些批准流程共用的安全等待器。
数据流:进去的是一个会产出 ReviewDecision 的等待任务、父会话、批准编号、取消信号,以及可选的审查取消信号。它同时等批准结果和取消。若取消先到,它会顺手取消正在进行的 Guardian 审查,通知父会话该批准已中止,并返回 Abort;若批准先到,就直接返回批准决定。
调用关系:handle_exec_approval、handle_patch_approval 和 maybe_auto_review_mcp_request_user_input 都用它等待审查或批准。它把“等待外部决定”和“任务随时可能被取消”这件麻烦事集中处理,避免每个批准流程重复写一遍。
调用图:被 3 处调用(handle_exec_approval, handle_patch_approval, maybe_auto_review_mcp_request_user_input);外部调用 1 个(select!)。
core/src/tasks/review.rs源码 ↗
这个文件像一个“审查流程领班”。用户进入 review 后,它先把用户输入整理出来,然后开一个子代理(可以理解成临时请来的审查员)去看代码。为了避免审查员做不该做的事,它会关掉网页搜索、协作、多代理等能力,并强制使用审查专用提示词和“永不请求批准”的策略。子代理运行时会不断吐出事件(事件就是系统里传递状态和消息的小纸条),本文件会过滤掉不适合直接给用户看的中间消息,只保留必要内容。等审查结束,它会尝试把模型输出解析成结构化的审查结果;如果解析不了,也不会崩,而是把原文当作说明保存。最后它会发出“已退出审查模式”的事件,并把审查报告写进会话历史,确保前端和持久化记录都能看到同一份结果。
ReviewTask::new37–39 ↗
fn new() -> Self
作用:创建一个新的 ReviewTask。它本身不带复杂状态,就像拿出一张“审查任务卡”,后面调度器可以用这张卡启动审查流程。
数据流:进去没有额外参数 → 生成一个空的 ReviewTask 实例 → 出来的是可交给任务系统使用的审查任务对象,不修改任何外部数据。
调用关系:它会在 spawn_review_thread 准备启动审查线程时使用,也会在 abort_review_task_emits_exited_then_aborted_and_records_history 这类测试里使用,用来得到一个可运行或可测试的审查任务。
调用图:被 2 处调用(spawn_review_thread, abort_review_task_emits_exited_then_aborted_and_records_history)。
ReviewTask::kind43–45 ↗
fn kind(&self) -> TaskKind
作用:告诉任务系统:这个任务的类型是 Review,也就是审查任务。这样系统能把它和普通聊天、执行命令等任务区分开。
数据流:进去是当前 ReviewTask → 它不读取复杂状态,只返回固定的 TaskKind::Review → 外部调度器拿这个结果识别任务类别。
调用关系:这是 SessionTask 接口的一部分。任务框架在需要判断任务种类时会问它,后续统计、取消、状态展示都可以按“审查任务”来处理。
ReviewTask::span_name47–49 ↗
fn span_name(&self) -> &'static str
作用:给这段审查任务起一个监控用名字。span 可以理解成一段可追踪的运行区间,方便日志和性能追踪知道当前在跑什么。
数据流:进去是当前 ReviewTask → 返回固定字符串 "session_task.review" → 监控或追踪系统用这个名字标记审查流程。
调用关系:这是 SessionTask 接口的一部分。它不推动业务流程,但让外部观察工具能把这次运行归类到 review 任务上。
ReviewTask::run51–88 ↗
async fn run(
self: Arc<Self>,
session: Arc<SessionTaskContext>,
ctx: Arc<TurnContext>,
input: Vec<TurnInput>,
cancellation_token: CancellationToken,
) -> O
作用:真正执行一次审查任务。它负责统计一次审查、整理用户输入、启动子审查员、读取审查事件,并在没有被取消时退出审查模式。
数据流:进去有会话、当前轮上下文、用户输入列表和取消令牌(一种“随时叫停”的信号)→ 它先记录 telemetry 计数,再只抽取真实用户输入,忽略模型回复和代理间通信 → 调用 start_review_conversation 启动子对话,拿到事件接收器后交给 process_review_events 读取结果 → 如果取消令牌没有被触发,就调用 exit_review_mode 把结果或中断状态发回主会话 → 最后返回 None,不直接生成普通文本回复。
调用关系:它是 ReviewTask 的主流程。调度器启动审查任务时会进入这里;它把“开子对话”的活交给 start_review_conversation,把“读事件和拿结果”的活交给 process_review_events,把“通知用户并写历史”的活交给 exit_review_mode。
调用图:调用 3 个内部函数(exit_review_mode, process_review_events, start_review_conversation);外部调用 3 个(clone, is_cancelled, new)。
ReviewTask::abort90–92 ↗
async fn abort(&self, session: Arc<SessionTaskContext>, ctx: Arc<TurnContext>)
作用:在审查任务被外部强行停止时做收尾。它不会再等审查员输出结果,而是直接告诉系统:审查模式已退出,但没有有效审查结果。
数据流:进去有会话和当前轮上下文 → 它把 review_output 设为 None → 调用 exit_review_mode 发送中断说明、写入会话历史、通知前端退出审查模式。
调用关系:当任务系统决定中止 review 时会走这里。它复用 exit_review_mode,保证“正常结束”和“被取消”都走同一套退出和记录逻辑。
调用图:调用 1 个内部函数(exit_review_mode)。
start_review_conversation95–139 ↗
async fn start_review_conversation(
session: Arc<SessionTaskContext>,
ctx: Arc<TurnContext>,
input: Vec<UserInput>,
cancellation_token: CancellationToken,
) -> Option<async_channel::Re
作用:启动一个专门做代码审查的子代理对话。子代理可以理解成主助手临时分身,但这里会给它套上更严格的规则,只让它专心审查。
数据流:进去有会话、上下文、整理后的用户输入和取消令牌 → 它复制当前配置,关闭网页搜索、CSV 子进程、协作和多代理等能力,设置审查专用提示词,禁止请求用户批准,并选择 review_model 或当前模型 → 调用 run_codex_thread_one_shot 启动一次性子对话 → 如果启动成功,出来的是接收事件的通道;如果失败,出来是 None。
调用关系:ReviewTask::run 会调用它作为审查流程的第一步。它内部把真正运行模型线程的工作交给 run_codex_thread_one_shot,并用 allow_only 设置严格的审批策略;如果按设计不可能失败的约束出错,会 panic,也就是直接暴露严重内部错误。
调用图:调用 2 个内部函数(allow_only, run_codex_thread_one_shot);被 1 处调用(run);外部调用 1 个(panic!)。
process_review_events141–188 ↗
async fn process_review_events(
session: Arc<SessionTaskContext>,
ctx: Arc<TurnContext>,
receiver: async_channel::Receiver<Event>,
) -> Option<ReviewOutputEvent>
作用:读取子审查员发来的事件,并决定哪些转发给主会话、哪些藏起来、什么时候拿到最终审查结果。
数据流:进去有会话、上下文和事件接收器 → 它循环 recv 接收事件;普通助手消息会先暂存,避免把最后那段结构化输出提前露出;助手消息完成事件和内容增量会被压住;其他重要事件会转发给主会话 → 遇到 TurnComplete 时,从最后一条助手消息里解析 ReviewOutputEvent;遇到 TurnAborted 或通道关闭时返回 None。
调用关系:ReviewTask::run 在子对话启动成功后调用它。它处在子代理和主会话之间,像一个筛选器:既让用户看到必要事件,又保护 review 流程想要展示的结构化结果不被旧式消息机制打乱。
parse_review_output_event195–210 ↗
fn parse_review_output_event(text: &str) -> ReviewOutputEvent
作用:把审查模型吐出的文本变成系统能理解的结构化审查结果。模型有时会老老实实输出 JSON,有时会在 JSON 外面夹杂说明文字,这个函数负责尽量救回来。
数据流:进去是一段文本 → 它先尝试把整段文本当成 ReviewOutputEvent 的 JSON 解析;失败后,再找第一个 { 和最后一个 } 之间的内容当 JSON 解析;如果还是失败,就创建一个默认 ReviewOutputEvent,把原始文本放进 overall_explanation → 出来一定是一个 ReviewOutputEvent,不会因为格式差就丢掉审查内容。
调用关系:它服务于 process_review_events,在审查回合完成时把最后的模型消息转成正式结果。这样 exit_review_mode 后面可以统一按 ReviewOutputEvent 渲染和保存。
调用图:外部调用 1 个(default)。
exit_review_mode214–280 ↗
async fn exit_review_mode(
session: Arc<Session>,
review_output: Option<ReviewOutputEvent>,
ctx: Arc<TurnContext>,
)
作用:正式退出审查模式,并把审查结果通知用户、写入历史、触发持久化。它是正常完成和被中断时共同使用的收尾口。
数据流:进去有主会话、可选的 ReviewOutputEvent 和上下文 → 如果有审查结果,就把总体说明和发现列表拼成用户可读文本,并渲染成退出成功消息;如果没有结果,就生成“审查被中断”的提示 → 它先记录一条用户侧的 rollout 消息,再发送 ExitedReviewMode 事件给前端,然后记录并发出一条助手侧审查报告消息 → 最后调用 ensure_rollout_materialized,确保历史文件和相关元数据真的落地。
调用关系:ReviewTask::run 在审查正常结束且未取消时调用它,ReviewTask::abort 在强制中止时也调用它。它会借助 format_review_findings_block、render_review_output_text、render_review_exit_success 和 render_review_exit_interrupted 把结构化结果变成适合用户阅读和系统保存的内容。
调用图:调用 2 个内部函数(format_review_findings_block, render_review_output_text);被 2 处调用(abort, run);外部调用 6 个(new, render_review_exit_interrupted, render_review_exit_success, format!, ExitedReviewMode, vec!)。
core/src/tools/code_mode/delegate.rs源码 ↗
代码模式里,一个“单元格”(cell,可以理解成一块正在运行或展示的代码区域)可能会请求调用别的工具,也可能只是发一段通知文本。这个文件专门处理这些请求的排队、等待和转交。核心是 CodeModeDispatchBroker,它保存一条消息通道,也给每个单元格准备一个“闸门”:闸门没开,请求就先等着;闸门打开,才真正交给当前 turn(一轮模型回复/执行流程)处理。CodeModeDispatchWorker 是每一轮运行时启动的工人,负责从通道里取消息。真正执行工具时,它交给 ToolCallRuntime 和 call_nested_tool;只是通知时,它把文本包装成工具输出,塞回正在运行的会话里。这里还特别注意取消信号和单元格关闭:一旦取消,就不再继续等;单元格关闭,就清掉对应闸门,防止旧状态影响后面的请求。
CodeModeDispatchBroker::new31–38 ↗
fn new() -> Self
作用:创建一个新的代码模式调度器。它准备好消息通道和单元格闸门表,之后代码模式发来的工具调用和通知都会先进入这里。
数据流:进去没有业务输入 → 它新建一对发送/接收通道,并准备一个空的单元格状态表 → 出来一个可用的 CodeModeDispatchBroker,后续可以接收请求、启动工人、控制单元格是否可派发。
调用关系:这是整套调度机制的起点。外层创建代码模式相关对象时会调用它;之后 mark_cell_ready_for_dispatch、start_turn_worker、invoke_tool、notify 等操作都依赖它内部准备好的通道和闸门表。
CodeModeDispatchBroker::mark_cell_ready_for_dispatch40–42 ↗
fn mark_cell_ready_for_dispatch(&self, cell_id: &CellId)
作用:把某个单元格标记为“可以开始派发请求了”。这相当于打开闸门,让之前等着的工具调用或通知继续往下走。
数据流:进去一个 cell_id,也就是单元格编号 → 它找到或创建这个单元格对应的闸门,并把状态改成 true → 出来没有返回值,但等待这个单元格的任务会被唤醒。
调用关系:当外部确认某个代码单元格已经准备好时会调用它。它内部用 dispatch_gate 取到闸门;start_turn_worker 里的等待逻辑会因为这个闸门打开而继续执行消息。
调用图:调用 1 个内部函数(dispatch_gate)。
CodeModeDispatchBroker::close_cell44–46 ↗
fn close_cell(&self, cell_id: &CellId)
作用:关闭某个单元格对应的调度状态。它会把这个单元格的闸门从表里移走,避免已经结束的单元格继续影响新请求。
数据流:进去一个 cell_id → 它从闸门表里删除这个单元格的记录 → 出来没有返回值,但这个单元格的派发状态被清理掉了。
调用关系:CodeModeDispatchBroker::cell_closed 会调用它,表示代码模式通知核心系统:这个单元格已经结束了。它把具体清理工作交给 remove_dispatch_gate。
调用图:调用 1 个内部函数(remove_dispatch_gate);被 1 处调用(cell_closed)。
CodeModeDispatchBroker::start_turn_worker48–129 ↗
fn start_turn_worker(
&self,
exec: ExecContext,
router: Arc<ToolRouter>,
tracker: SharedTurnDiffTracker,
) -> CodeModeDispatchWorker
作用:为当前这一轮对话启动一个后台工人。这个工人不断从调度通道里取消息,等对应单元格准备好后,再执行工具或发送通知。
数据流:进去当前执行上下文 exec、工具路由器 router、以及本轮差异追踪器 tracker → 它创建 ToolCallRuntime 和 CoreTurnHost,复制消息接收端,启动一个异步后台任务循环收消息 → 出来一个 CodeModeDispatchWorker;只要这个对象还活着,后台工人就会继续服务当前 turn。
调用关系:这是 broker 和真正执行环境之间的桥。它会调用 wait_until_cell_ready_for_dispatch 等闸门打开;若等待被取消,会调用 remove_dispatch_gate 清理;准备好后,通知交给 CoreTurnHost::notify,工具调用交给 CoreTurnHost::invoke_tool,并且工具调用会再开一个异步任务执行,避免堵住调度循环。
调用图:调用 3 个内部函数(remove_dispatch_gate, wait_until_cell_ready_for_dispatch, new);外部调用 6 个(clone, new, clone, channel, select!, spawn)。
dispatch_gate132–144 ↗
fn dispatch_gate(
dispatch_gates: &Mutex<HashMap<CellId, watch::Sender<bool>>>,
cell_id: &CellId,
) -> watch::Sender<bool>
作用:取得某个单元格的“闸门”。如果这个单元格还没有闸门,它就现场建一个,默认是关闭状态。
数据流:进去一张受互斥锁保护的闸门表和一个 cell_id;互斥锁可以理解成一把锁,防止多个任务同时改同一张表 → 它锁住表,查找这个单元格的 watch 发送端;watch 是一种只保存最新状态、能通知等待者的异步通知工具 → 出来这个单元格的闸门发送端副本。
调用关系:mark_cell_ready_for_dispatch 用它来开闸;wait_until_cell_ready_for_dispatch 用它来订阅并等待闸门变化。它是所有单元格派发状态的统一入口。
调用图:被 2 处调用(mark_cell_ready_for_dispatch, wait_until_cell_ready_for_dispatch);外部调用 1 个(clone)。
remove_dispatch_gate146–155 ↗
fn remove_dispatch_gate(
dispatch_gates: &Mutex<HashMap<CellId, watch::Sender<bool>>>,
cell_id: &CellId,
)
作用:删除某个单元格的闸门。它用来清理已经关闭或取消的单元格状态。
数据流:进去闸门表和 cell_id → 它加锁打开表,把这个 cell_id 对应的记录移除 → 出来没有返回值,表里不再记着这个单元格。
调用关系:CodeModeDispatchBroker::close_cell 会直接调用它。start_turn_worker 在发现等待被取消时也会调用它,防止留下无用闸门。
调用图:被 2 处调用(close_cell, start_turn_worker)。
wait_until_cell_ready_for_dispatch157–179 ↗
async fn wait_until_cell_ready_for_dispatch(
dispatch_gates: &Mutex<HashMap<CellId, watch::Sender<bool>>>,
cell_id: &CellId,
cancellation_token: &CancellationToken,
) -> bool
作用:等待某个单元格变成“可以派发”。如果等待期间收到取消信号,它会立刻放弃。
数据流:进去闸门表、cell_id 和 cancellation_token;cancellation_token 是一个取消令牌,可以理解成“别干了”的信号 → 它先检查是否已经取消,然后订阅这个单元格的闸门状态,循环等待状态变成 true 或收到取消 → 出来一个布尔值:true 表示可以继续派发,false 表示取消、闸门失效或不能再等。
调用关系:start_turn_worker 在处理通知和工具调用前都会先调用它。只有它返回 true,消息才会交给 CoreTurnHost;返回 false 时,worker 会停止处理这条消息,并可能清理闸门。
调用图:调用 1 个内部函数(dispatch_gate);被 1 处调用(start_turn_worker);外部调用 2 个(is_cancelled, select!)。
CodeModeDispatchBroker::invoke_tool182–208 ↗
fn invoke_tool(
&'a self,
invocation: CodeModeNestedToolCall,
cancellation_token: CancellationToken,
) -> ToolInvocationFuture<'a>
作用:这是代码模式请求“调用一个嵌套工具”时走的入口。它不直接执行工具,而是把请求打包发给本轮后台工人,并等待结果。
数据流:进去一个 CodeModeNestedToolCall 和取消令牌 → 它先看是否已取消;没取消就创建一次性回复通道,把工具调用消息发进调度队列,然后同时等待工人回结果或取消信号 → 出来成功时是工具返回的 JSON 数据,失败时是说明原因的字符串。
调用关系:这个函数实现 CodeModeSessionDelegate 这个接口,所以代码模式会通过它把工具调用交给核心系统。它把活儿发送给 start_turn_worker 启动的后台循环;后台循环再等待单元格准备好,并最终交给 CoreTurnHost::invoke_tool 执行。
调用图:外部调用 6 个(pin, clone, is_cancelled, send, channel, select!)。
CodeModeDispatchBroker::notify210–240 ↗
fn notify(
&'a self,
call_id: String,
cell_id: CellId,
text: String,
cancellation_token: CancellationToken,
) -> NotificationFuture<'a>
作用:这是代码模式发通知文本时走的入口。它把通知放进调度队列,等对应单元格准备好后,再注入到当前会话里。
数据流:进去 call_id、cell_id、通知文本和取消令牌 → 它检查取消状态,创建一次性回复通道,把通知消息发给调度工人,然后等待成功确认或取消 → 出来成功时没有额外数据,失败时返回错误文字。
调用关系:这个函数同样是 CodeModeSessionDelegate 接口的一部分。代码模式调用它后,消息会被 start_turn_worker 的后台循环接收;后台循环等单元格开闸后,把文本交给 CoreTurnHost::notify 注入会话。
调用图:外部调用 6 个(pin, clone, is_cancelled, send, channel, select!)。
CodeModeDispatchBroker::cell_closed242–244 ↗
fn cell_closed(&self, cell_id: &CellId)
作用:告诉调度器:某个代码单元格已经关闭了。这样调度器就能清掉它的等待状态。
数据流:进去一个 cell_id → 它调用 close_cell → 出来没有返回值,但这个单元格的闸门记录会被移除。
调用关系:这是 CodeModeSessionDelegate 接口里的回调。代码模式在单元格结束时会触发它;它把具体工作交给 CodeModeDispatchBroker::close_cell。
调用图:调用 1 个内部函数(close_cell)。
CodeModeDispatchWorker::drop267–271 ↗
fn drop(&mut self)
作用:当本轮调度工人对象被销毁时,通知后台任务该停了。它是一个自动收尾动作,防止后台循环一直空转。
数据流:进去的是即将被释放的 CodeModeDispatchWorker 自身 → 它取出 shutdown_tx 这个一次性关闭信号发送端,并发送一个空信号 → 出来没有业务返回值,但后台 worker 的循环会收到信号并退出。
调用关系:Rust 在对象离开作用域时会自动调用 drop。start_turn_worker 返回的 CodeModeDispatchWorker 只要被丢弃,这里就会触发,从而让启动时 spawn 出来的后台任务优雅结束。
CoreTurnHost::invoke_tool280–293 ↗
async fn invoke_tool(
&self,
invocation: CodeModeNestedToolCall,
cancellation_token: CancellationToken,
) -> Result<JsonValue, String>
作用:真正把一个嵌套工具调用交给核心工具系统执行。它是调度工人背后的“执行按钮”。
数据流:进去工具调用内容 invocation 和取消令牌 → 它复制当前执行上下文和工具运行时,把这些交给 call_nested_tool → 出来成功时是工具返回的 JSON 值,失败时把错误转成普通字符串。
调用关系:start_turn_worker 在确认单元格已经准备好后,会异步调用它。它本身不决定何时执行,只负责把已经获准执行的请求转交给 call_nested_tool。
调用图:外部调用 3 个(clone, clone, call_nested_tool)。
CoreTurnHost::notify295–311 ↗
async fn notify(&self, call_id: String, cell_id: CellId, text: String) -> Result<(), String>
作用:把代码模式的一段通知文本变成当前会话里可见的工具输出。空白通知会被直接忽略。
数据流:进去 call_id、cell_id 和文本 → 它先去掉首尾空白检查,如果文本为空就直接成功;否则把文本包装成 CustomToolCallOutput,并注入正在运行的会话 → 出来成功时表示注入完成,失败时返回“没有活跃 turn”等错误说明。
调用关系:start_turn_worker 在处理 DispatchMessage::Notify 并等到单元格开闸后调用它。它依赖 exec.session.inject_if_running 把消息塞进当前活动会话,让外层能像收到工具结果一样看到这条通知。
调用图:外部调用 1 个(vec!)。
ext/guardian/src/lib.rs源码 ↗
这个文件解决的是 Guardian 扩展和主程序之间怎么配合的问题。Guardian 需要两件事:第一,知道当前线程对应的来源线程编号;第二,将来要开一个子代理时,能把请求交给主程序去真正创建。这里的 GuardianExtension 保存了主程序给它的“创建代理帮手”。GuardianThreadContext 则是放在线程里的小纸条,记着默认从哪个 ThreadId(线程编号)派生。在线程启动时,on_thread_start 会从线程存储里的 level_id 字符串尝试解析出 ThreadId,解析成功就把这张小纸条塞回线程存储里;解析失败就安静跳过,不让启动流程崩掉。最后 install 负责把 GuardianExtension 注册进扩展系统,让主程序在线程生命周期里能自动调用它。
GuardianExtension::new20–22 ↗
fn new(agent_spawner: S) -> Self
作用:创建一个 GuardianExtension,并把主程序提供的“创建子代理的帮手”保存进去。别人想让 Guardian 具备派生子代理的能力时,会先用这个函数把它组装好。
数据流:输入是一个 agent_spawner,也就是宿主提供的创建子代理工具 → 函数把它放进 GuardianExtension 这个结构里 → 输出一个可以被注册和使用的 GuardianExtension;除了保存这个工具,不做额外改动。
调用关系:install 在安装扩展时会调用它,先做出 GuardianExtension,再把它交给扩展注册表。它是 Guardian 扩展进入系统前的组装步骤。
调用图:被 1 处调用(install)。
GuardianExtension::spawn_subagent25–35 ↗
fn spawn_subagent(
&'a self,
forked_from_thread_id: ThreadId,
request: R,
) -> AgentSpawnFuture<'a, <S as AgentSpawner<R>>::Spawned, <S as AgentSpawner<R>>::Error>
作用:把 Guardian 想要创建子代理的请求转交给宿主。Guardian 自己不直接创建代理,而是请外部提供的 agent_spawner 来办。
数据流:输入包括来源线程的 ThreadId 和一份创建请求 request → 函数把这两个东西原样交给内部保存的 agent_spawner.spawn_subagent → 输出一个异步结果,也就是“创建子代理这件事将来完成后的结果”;它本身不改线程状态。
调用关系:当 Guardian 需要派生子代理时会走到这里。这个函数只是中转站,真正干活的是外部的 spawn_subagent(ext),所以它把 Guardian 的请求和宿主的实际创建能力接起来。
调用图:外部调用 1 个(spawn_subagent)。
GuardianThreadContext::forked_from_thread_id46–48 ↗
fn forked_from_thread_id(&self) -> ThreadId
作用:取出这个线程默认应该从哪个线程派生的编号。调用者用它来知道后续 Guardian 子代理应该以哪个 ThreadId 作为来源。
数据流:输入是一个已经存在的 GuardianThreadContext → 函数读取里面保存的 forked_from_thread_id → 输出这个 ThreadId;它不修改任何数据。
调用关系:GuardianThreadContext 通常是在线程启动时由 GuardianExtension::on_thread_start 放进线程存储的。之后需要知道派生来源的代码可以调用这个函数读取这张“小纸条”。
GuardianExtension::on_thread_start55–68 ↗
fn on_thread_start(
&'a self,
input: ThreadStartInput<'a, Config>,
) -> ExtensionFuture<'a, ()>
作用:在线程刚启动时,为 Guardian 准备好本线程的上下文信息。它会尝试把线程存储里的 level_id 转成真正的 ThreadId,并保存起来给以后使用。
数据流:输入是 ThreadStartInput,里面带着线程存储 thread_store → 函数读取 thread_store.level_id(),用 from_string 尝试把字符串解析成 ThreadId → 如果成功,就把 GuardianThreadContext 插入 thread_store;如果失败,就直接返回,不写入任何东西;输出是一个异步完成信号,没有普通返回值。
调用关系:它是线程生命周期回调的一部分,由扩展系统在线程启动时调用。内部会调用 from_string 做编号解析,并用 pin(ext) 包住异步任务;它准备好的 GuardianThreadContext 会被后续需要派生来源信息的代码读取。
调用图:调用 1 个内部函数(from_string);外部调用 1 个(pin)。
install72–77 ↗
fn install(registry: &mut ExtensionRegistryBuilder<Config>, agent_spawner: S)
作用:把 Guardian 扩展安装到扩展注册表里,让主程序知道线程启动时要通知它。没有这一步,GuardianExtension 即使写好了,也不会被系统自动调用。
数据流:输入是可修改的扩展注册表 registry,以及宿主提供的 agent_spawner → 函数先用 GuardianExtension::new 创建扩展对象,再用 Arc 包起来共享,最后调用 registry.thread_lifecycle_contributor 注册进去 → 输出没有返回值,但 registry 被更新,多了一个线程生命周期贡献者。
调用关系:这是外部接入 Guardian 的入口。它会调用 new 组装 GuardianExtension,也会调用 thread_lifecycle_contributor 把扩展挂到系统里;之后线程启动时,系统才会进一步调用 GuardianExtension::on_thread_start。
调用图:调用 2 个内部函数(thread_lifecycle_contributor, new);外部调用 1 个(new)。
CSV agent 作业
此组描述持久化的批处理作业工作流:从 CSV 行生成 worker agent,接收 worker 结果报告,并以导出的作业摘要完成。
core/src/tools/handlers/agent_jobs.rs源码 ↗
这个文件解决的是批量处理问题:用户给一份 CSV,每一行都要让一个代理去分析或加工。如果一行一行手动做,会很慢;如果没有统一调度,又容易超量启动、漏收结果、任务卡死。这里先检查当前会话能不能开子代理,再按限制算出最多同时跑几个。任务运行时,它从状态数据库里取待处理行,给每一行生成清楚的工作说明,启动子代理去做。子代理必须调用报告结果的工具,把 JSON 结果写回数据库。主循环会不断检查哪些代理完成了、哪些超时了、哪些被取消了,并在最后导出一份带状态和结果的新 CSV。它还会在进程重启后恢复已经在跑的任务,避免“做到一半全丢了”。
required_state_db100–106 ↗
fn required_state_db(
session: &Arc<Session>,
) -> Result<Arc<codex_state::StateRuntime>, FunctionCallError>
作用:确认当前会话有没有可用的状态数据库。这个数据库用来记住任务、每一行的状态和结果;没有它,批量任务就没地方存进度。
数据流:输入是一个会话对象 → 它尝试从会话里取出状态数据库 → 如果取到了就返回数据库;如果没有,就返回一个严重错误,告诉上层这个会话不能运行这类任务。
调用关系:它通常会被工具处理流程在真正创建或更新任务前调用。它不把工作交给别的函数,只是做一道入口检查,保证后面的任务循环有可靠的存储。
build_runner_options108–132 ↗
async fn build_runner_options(
session: &Arc<Session>,
turn: &Arc<TurnContext>,
requested_concurrency: Option<usize>,
) -> Result<JobRunnerOptions, FunctionCallError>
作用:为批量任务准备运行参数,尤其是“最多同时开几个子代理”和“子代理该用什么配置”。它防止在不允许多代理或线程额度用完时继续派工。
数据流:输入是会话、当前轮次上下文和用户请求的并发数 → 它读取多代理开关、线程上限和基础指令 → 用 normalize_concurrency 把并发数压到安全范围内,再调用 build_agent_spawn_config 生成子代理配置 → 输出 JobRunnerOptions,里面有并发上限和启动子代理所需配置;遇到不允许启动时返回给模型看的错误。
调用关系:它是任务运行前的准备步骤。它把并发计算交给 normalize_concurrency,把子代理配置构造交给 build_agent_spawn_config;后面的 run_agent_job_loop 会拿这些选项真正开始派活。
调用图:调用 1 个内部函数(normalize_concurrency);外部调用 2 个(build_agent_spawn_config, RespondToModel)。
normalize_concurrency134–142 ↗
fn normalize_concurrency(requested: Option<usize>, max_threads: Option<usize>) -> usize
作用:把用户想要的并发数变成安全可用的数字。它会保证至少是 1,也不会超过系统设定的最大值或当前会话的线程限制。
数据流:输入是用户请求的数量和系统允许的最大线程数 → 如果用户没填,就用默认值;如果太小就抬到 1,太大就压到固定上限 64,再按线程限制继续压低 → 输出最终可用的并发数。
调用关系:它只被 build_runner_options 调用,是启动任务前的“限速器”。这样 run_agent_job_loop 后面派发子代理时不用再反复判断用户给的数字是否危险。
调用图:被 1 处调用(build_runner_options)。
normalize_max_runtime_seconds144–154 ↗
fn normalize_max_runtime_seconds(requested: Option<u64>) -> Result<Option<u64>, FunctionCallError>
作用:检查用户给的单个任务最长运行时间是否合法。它允许用户不设置,但不允许设置成 0 秒这种没有意义的值。
数据流:输入是可选的秒数 → 如果没填,输出 None,表示使用默认超时时间;如果是 0,返回一个给模型看的错误;如果大于 0,就原样返回这个秒数。
调用关系:它属于创建任务时的参数校验工具。后续 job_runtime_timeout 会根据保存到任务里的值,决定每个子代理最多能跑多久。
调用图:外部调用 1 个(RespondToModel)。
run_agent_job_loop156–328 ↗
async fn run_agent_job_loop(
session: Arc<Session>,
turn: Arc<TurnContext>,
db: Arc<codex_state::StateRuntime>,
job_id: String,
options: JobRunnerOptions,
) -> anyhow::Result<()>
作用:这是批量代理任务的主循环,像调度员一样不断派发新工作、检查已完成工作、处理超时和取消,最后导出结果 CSV。
数据流:输入是会话、当前轮次、状态数据库、任务编号和运行选项 → 它从数据库读取任务,恢复之前已经在跑的条目,按并发上限领取待处理行,为每行生成提示词并启动子代理 → 运行中不断检查完成、失败、超时、取消等情况,更新数据库状态 → 结束时导出 CSV 快照;如果没有取消且导出成功,就把整个任务标记为完成。
调用关系:它是这个文件的核心发动机。它调用 build_worker_prompt 生成给子代理看的任务说明,调用 recover_running_items 恢复旧状态,调用 reap_stale_active_items 清理超时任务,调用 find_finished_threads 找完成的代理,调用 finalize_finished_item 收尾,最后调用 export_job_csv_snapshot 写出结果。
调用图:调用 8 个内部函数(build_worker_prompt, export_job_csv_snapshot, finalize_finished_item, find_finished_threads, job_runtime_timeout, reap_stale_active_items, recover_running_items, wait_for_status_change);外部调用 7 个(default, new, now, SubAgent, format!, Other, vec!)。
export_job_csv_snapshot330–345 ↗
async fn export_job_csv_snapshot(
db: Arc<codex_state::StateRuntime>,
job: &codex_state::AgentJob,
) -> anyhow::Result<()>
作用:把某个批量任务当前的所有行和处理结果写成一份 CSV 文件。它让用户不用进数据库,也能直接拿到表格形式的结果。
数据流:输入是状态数据库和任务信息 → 它读取该任务的所有条目,用 render_job_csv 拼成 CSV 文本,确保输出目录存在,然后把文件写到任务指定的路径 → 输出成功或失败;失败会带上具体原因。
调用关系:它由 run_agent_job_loop 在任务结束阶段调用。它把“怎么把数据排成 CSV”的细节交给 render_job_csv,自己负责从数据库取数据和落盘写文件。
调用图:调用 1 个内部函数(render_job_csv);被 1 处调用(run_agent_job_loop);外部调用 3 个(from, create_dir_all, write)。
recover_running_items347–425 ↗
async fn recover_running_items(
session: Arc<Session>,
db: Arc<codex_state::StateRuntime>,
job_id: &str,
active_items: &mut HashMap<ThreadId, ActiveJobItem>,
runtime_timeout: Durat
作用:在任务循环开始时,找回数据库里记录为“正在运行”的条目。这样即使程序中途重启,也不会简单地把正在做的事丢掉。
数据流:输入是会话、数据库、任务编号、当前活跃任务表和超时时间 → 它读取所有运行中的条目,检查是否已经超时、线程编号是否存在且合法、对应代理是否已经结束 → 超时或异常的标为失败,已经结束的交给 finalize_finished_item 收尾,仍在运行的放回 active_items 继续监控。
调用关系:它在 run_agent_job_loop 刚启动时被调用,是恢复现场的步骤。它会用 is_item_stale 判断是否过期,用 started_at_from_item 推算开始时间,用 finalize_finished_item 处理已经结束的代理。
调用图:调用 5 个内部函数(is_final, finalize_finished_item, is_item_stale, started_at_from_item, from_string);被 1 处调用(run_agent_job_loop);外部调用 1 个(format!)。
find_finished_threads427–439 ↗
async fn find_finished_threads(
session: Arc<Session>,
active_items: &HashMap<ThreadId, ActiveJobItem>,
) -> Vec<(ThreadId, String)>
作用:检查当前活跃的子代理里,哪些已经到达最终状态。最终状态可以理解为“这个代理不会再继续工作了”。
数据流:输入是会话和活跃任务表 → 它逐个读取每个子代理的状态,用 is_final 判断是否已经结束 → 输出一个列表,里面是已结束线程的编号和对应任务条目编号。
调用关系:它被 run_agent_job_loop 在每轮循环中调用。它把获取单个代理状态的细节交给 active_item_status,找到结束项后,主循环会交给 finalize_finished_item 做收尾。
调用图:调用 2 个内部函数(is_final, active_item_status);被 1 处调用(run_agent_job_loop);外部调用 1 个(new)。
active_item_status441–452 ↗
async fn active_item_status(
session: &Session,
thread_id: ThreadId,
item: &ActiveJobItem,
) -> AgentStatus
作用:取得某个活跃子代理的最新状态。它会优先用订阅到的状态更新,拿不到时再主动查询。
数据流:输入是会话、子代理线程编号和活跃条目信息 → 如果条目里有状态订阅并且已经变化,就直接读取订阅里的状态;否则调用代理控制服务查询当前状态 → 输出一个 AgentStatus,也就是代理当前处于运行、完成、失败等哪种状态。
调用关系:它服务于 find_finished_threads。这样上层不需要关心状态来自“推送通知”还是“主动查询”,只要拿到最终判断用的状态即可。
调用图:被 1 处调用(find_finished_threads)。
wait_for_status_change454–469 ↗
async fn wait_for_status_change(active_items: &HashMap<ThreadId, ActiveJobItem>)
作用:当主循环暂时没事可做时,等一小会儿,直到某个活跃子代理状态变化,或者到达轮询间隔。这样可以避免空转浪费资源。
数据流:输入是活跃任务表 → 它为每个有状态订阅的任务准备一个等待器;如果没有订阅,就睡眠固定的 250 毫秒;如果有订阅,就最多等 250 毫秒,看是否有任何一个状态变化 → 没有业务结果输出,只是让循环暂停一会儿。
调用关系:它被 run_agent_job_loop 在没有新进展时调用。它像调度员的短暂休息:不忙等,但也不会睡太久,保证任务状态能较快被发现。
调用图:被 1 处调用(run_agent_job_loop);外部调用 3 个(new, sleep, timeout)。
reap_stale_active_items471–499 ↗
async fn reap_stale_active_items(
session: Arc<Session>,
db: Arc<codex_state::StateRuntime>,
job_id: &str,
active_items: &mut HashMap<ThreadId, ActiveJobItem>,
runtime_timeout: Dur
作用:清理已经运行太久的活跃子代理。它防止某个子代理卡住后一直占着并发名额。
数据流:输入是会话、数据库、任务编号、活跃任务表和超时时间 → 它找出运行时长超过限制的条目 → 把这些条目标记为失败,尝试关闭对应子代理,并从活跃任务表里移除 → 输出布尔值,表示这轮是否清理了东西。
调用关系:它由 run_agent_job_loop 每轮调用。它和 recover_running_items 分工类似:recover_running_items 处理启动前数据库里遗留的运行项,这个函数处理当前内存里正在监控的运行项。
调用图:被 1 处调用(run_agent_job_loop);外部调用 2 个(new, format!)。
finalize_finished_item501–533 ↗
async fn finalize_finished_item(
session: Arc<Session>,
db: Arc<codex_state::StateRuntime>,
job_id: &str,
item_id: &str,
thread_id: ThreadId,
) -> anyhow::Result<()>
作用:为已经结束的子代理做最后检查和收尾。重点是确认它有没有按要求上报结果;没有上报就算失败。
数据流:输入是会话、数据库、任务编号、条目编号和子代理线程编号 → 它从数据库取该条目;如果条目还处于运行中,并且已经有 result_json,就标记完成;如果没有结果,就标记失败并写入错误原因 → 最后尝试关闭这个子代理。
调用关系:它被 run_agent_job_loop 和 recover_running_items 调用。前者用于正常运行中发现代理结束,后者用于恢复现场时发现代理已经结束;它是每个子任务离场前的统一出口。
调用图:被 2 处调用(recover_running_items, run_agent_job_loop);外部调用 1 个(matches!)。
build_worker_prompt535–566 ↗
fn build_worker_prompt(
job: &codex_state::AgentJob,
item: &codex_state::AgentJobItem,
) -> anyhow::Result<String>
作用:为某一行 CSV 数据生成给子代理看的完整任务说明。它会告诉子代理任务编号、条目编号、输入数据、期望结果格式,以及必须调用哪个工具上报结果。
数据流:输入是任务信息和某个任务条目 → 它先用 render_instruction_template 把用户指令里的占位符替换成这一行的数据,再把输出格式和行数据转成漂亮的 JSON 文本 → 输出一段完整提示词字符串。
调用关系:它由 run_agent_job_loop 在启动每个子代理前调用。生成的提示词会作为子代理的第一条用户输入,直接决定子代理知道自己该做什么、做完该如何报告。
调用图:调用 1 个内部函数(render_instruction_template);被 1 处调用(run_agent_job_loop);外部调用 2 个(format!, to_string_pretty)。
render_instruction_template568–591 ↗
fn render_instruction_template(instruction: &str, row_json: &Value) -> String
作用:把用户指令里的占位符替换成当前 CSV 行里的值。例如指令里写 {name},这一行有 name 字段,就换成对应内容。
数据流:输入是原始指令文本和这一行的 JSON 数据 → 它先保护双大括号,避免用户想写普通花括号时被误替换;然后对每个字段做 {字段名} 替换;最后把被保护的普通花括号还原 → 输出替换后的指令文本。
调用关系:它只被 build_worker_prompt 调用,是生成子代理提示词时的小模板引擎。它让同一条任务指令可以根据每一行数据自动变化。
调用图:被 1 处调用(build_worker_prompt);外部调用 2 个(as_object, format!)。
ensure_unique_headers593–603 ↗
fn ensure_unique_headers(headers: &[String]) -> Result<(), FunctionCallError>
作用:检查 CSV 表头有没有重复。重复表头会让一行数据转成 JSON 时字段互相覆盖或含义不清。
数据流:输入是一组表头字符串 → 它用一个集合记录已经见过的表头 → 如果发现重复,就返回一个给模型看的错误;如果全部唯一,就返回成功且不改动数据。
调用关系:它属于导入 CSV 创建任务时的校验步骤。它不依赖其他业务函数,只是在任务进入数据库前阻止有歧义的表格。
job_runtime_timeout605–609 ↗
fn job_runtime_timeout(job: &codex_state::AgentJob) -> Duration
作用:算出这个任务里每个子代理最多能运行多久。用户设置过就用用户的设置,否则用默认 30 分钟。
数据流:输入是任务记录 → 它查看任务里是否有 max_runtime_seconds → 有就转成 Duration,没有就返回默认超时时间 → 输出一个可用于比较运行时长的时间长度。
调用关系:它被 run_agent_job_loop 调用,用来传给恢复、监控和超时清理相关函数。这样所有地方使用同一套超时规则。
调用图:被 1 处调用(run_agent_job_loop)。
started_at_from_item611–619 ↗
fn started_at_from_item(item: &codex_state::AgentJobItem) -> Instant
作用:根据数据库里条目的更新时间,推算这个任务在当前进程里大概已经运行了多久。它主要用于恢复重启前就在跑的任务。
数据流:输入是一个任务条目 → 它用当前真实时间减去条目的 updated_at,得到大概年龄 → 再把这个年龄换算成当前进程可比较的 Instant;如果时间异常,就用现在作为开始时间 → 输出一个 started_at。
调用关系:它被 recover_running_items 调用。当恢复正在运行的条目时,系统需要一个本进程内的开始时间,后续 reap_stale_active_items 才能继续判断是否超时。
调用图:被 1 处调用(recover_running_items);外部调用 2 个(now, now)。
is_item_stale621–628 ↗
fn is_item_stale(item: &codex_state::AgentJobItem, runtime_timeout: Duration) -> bool
作用:判断数据库里的运行中条目是不是已经太旧,也就是很可能卡住或超过运行时间限制了。
数据流:输入是任务条目和允许的最长运行时间 → 它用当前时间减去条目的 updated_at 得到年龄 → 如果年龄大于等于超时时间,就输出 true;如果时间算不出来或还没超时,就输出 false。
调用关系:它被 recover_running_items 调用,用于处理程序重启后遗留的运行项。超时的条目会被标记失败,并尝试关闭对应子代理。
调用图:被 1 处调用(recover_running_items);外部调用 1 个(now)。
default_output_csv_path630–641 ↗
fn default_output_csv_path(input_csv_path: &AbsolutePathBuf, job_id: &str) -> AbsolutePathBuf
作用:在用户没指定输出文件时,自动生成一个结果 CSV 路径。生成的名字会带上任务编号的一小段,避免和别的任务混在一起。
数据流:输入是原始 CSV 的绝对路径和任务编号 → 它取原文件名主体,加上类似 agent-job-xxxx 的后缀,再放回原文件所在目录 → 输出一个新的绝对路径。
调用关系:它通常在创建 CSV 批量任务时使用。后续 export_job_csv_snapshot 会把结果写到这个路径。
parse_csv643–663 ↗
fn parse_csv(content: &str) -> Result<(Vec<String>, Vec<Vec<String>>), String>
作用:把 CSV 文件内容解析成表头和数据行。它还会跳过完全空白的行,并处理文件开头可能出现的特殊 BOM 标记。
数据流:输入是一整段 CSV 文本 → 它用 CSV 读取器读取第一行为表头,去掉第一个表头开头可能带的隐藏 BOM 字符;然后逐行读取数据,忽略所有单元格都为空的行 → 输出表头列表和行列表;解析失败时输出错误文字。
调用关系:它属于创建批量任务前的读取步骤。解析出的表头和行会用于生成数据库里的任务条目,也会被 render_job_csv 在导出时对应回原始列。
render_job_csv665–739 ↗
fn render_job_csv(
headers: &[String],
items: &[codex_state::AgentJobItem],
) -> Result<String, FunctionCallError>
作用:把任务条目列表渲染成最终 CSV 文本。它不仅保留原始输入列,还追加任务状态、错误信息、结果 JSON、完成时间等列。
数据流:输入是原始表头和所有任务条目 → 它先拼出输出表头,再逐条取出每行的原始字段值和任务元数据;每个单元格都经过 csv_escape,避免逗号、换行、引号破坏 CSV 格式 → 输出完整 CSV 字符串;如果某条 row_json 不是对象,就返回给模型看的错误。
调用关系:它被 export_job_csv_snapshot 调用,是导出文件的核心排版函数。它依赖 csv_escape 保证单元格安全,依赖 value_to_csv_string 把 JSON 值变成适合表格的文字。
调用图:调用 1 个内部函数(csv_escape);被 1 处调用(export_job_csv_snapshot);外部调用 2 个(new, new)。
value_to_csv_string741–749 ↗
csv_escape751–758 ↗
fn csv_escape(value: &str) -> String
作用:把一个文本值安全地放进 CSV 单元格。遇到逗号、换行或引号时,它会按 CSV 规则加引号和转义。
数据流:输入是一个字符串 → 如果里面没有特殊字符,就原样输出;如果有逗号、换行、回车或双引号,就把内部双引号变成两个双引号,再用外层双引号包起来 → 输出安全的 CSV 单元格文本。
调用关系:它被 render_job_csv 大量调用,用在表头和每个单元格上。它是防止导出的 CSV 被 Excel 或其他表格软件读乱的最后一道保护。
调用图:被 1 处调用(render_job_csv);外部调用 1 个(format!)。
core/src/tools/handlers/agent_jobs/report_agent_job_result.rs源码 ↗
可以把它想成“外包任务回执窗口”。别的代理或线程完成了某个 job 里的某个 item 后,会带着 job_id、item_id 和 result 来这里报到。这个文件先确认来的是函数式工具调用,而不是别的格式;再把参数从 JSON 字符串读出来;然后检查 result 必须是 JSON 对象,也就是一组键值信息,不能只是一个数字或一句话。接着它找到当前会话对应的状态数据库,把结果写进去,并记录是谁报告的。如果报告被接受,而且调用方还说 stop=true,它还会顺手把整个任务标记为取消,表示工作者请求停下。最后它把 accepted 这个布尔结果包装成工具输出返回给模型。这样系统既能收集分布式工作的成果,也能避免格式乱掉或把结果写到没有状态库的会话里。
ReportAgentJobResultHandler::tool_name17–19 ↗
fn tool_name(&self) -> ToolName
作用:告诉工具注册表:这个处理器对应的工具名叫 report_agent_job_result。别人只有用这个名字调用工具,才会路由到这里。
数据流:进去的是这个处理器自身 → 它用 ToolName::plain 包出一个普通工具名 → 出来的是系统可识别的工具名称,不改动外部数据。
调用关系:这是工具被登记和查找时会用到的身份证。它把名字交给 ToolName::plain 来规范化,后面的真正执行不在这里发生。
调用图:调用 1 个内部函数(plain)。
ReportAgentJobResultHandler::spec21–23 ↗
fn spec(&self) -> ToolSpec
作用:提供这个工具的说明书,也就是模型调用它时应该传哪些参数、参数长什么样。没有这份说明,模型就容易传错格式。
数据流:进去的是处理器自身 → 它调用 create_report_agent_job_result_tool 生成工具规格 → 出来的是 ToolSpec,供工具系统展示和校验使用。
调用关系:这是注册工具时使用的说明环节。它不自己拼规则,而是把工作交给 create_report_agent_job_result_tool,保证和其他地方定义的工具规格一致。
调用图:调用 1 个内部函数(create_report_agent_job_result_tool)。
ReportAgentJobResultHandler::handle25–27 ↗
fn handle(&self, invocation: ToolInvocation) -> codex_tools::ToolExecutorFuture<'_>
作用:这是工具真正被调用时的入口。它把一次调用包装成异步任务,因为写数据库这类事情可能需要等待。
数据流:进去的是一次 ToolInvocation,也就是一次工具调用的上下文和参数 → 它把 self.handle_call(invocation) 放进 Box::pin,变成统一的异步返回形式 → 出来的是一个将来会完成的执行结果。
调用关系:工具运行时调用它来开始处理请求。它自己不做具体判断,而是马上把活交给 ReportAgentJobResultHandler::handle_call;handle_call 也正是由它触发的。
调用图:调用 1 个内部函数(handle_call);外部调用 1 个(pin)。
ReportAgentJobResultHandler::handle_call31–49 ↗
async fn handle_call(
&self,
invocation: ToolInvocation,
) -> Result<Box<dyn crate::tools::context::ToolOutput>, FunctionCallError>
作用:把通用的工具调用拆开,只接受“函数参数”这种载荷,然后交给真正的业务函数去记录任务结果。它像门卫,先看来的包裹是不是正确类型。
数据流:进去的是 ToolInvocation,里面有会话 session 和载荷 payload → 它取出 session,并检查 payload 是否是 ToolPayload::Function;如果不是,就返回给模型一个可读错误 → 如果是,就拿到 arguments 字符串,调用 handle(session, arguments),再把结果包装成工具输出。
调用关系:它由 ReportAgentJobResultHandler::handle 启动,是异步执行链里的中转站。它会在格式不对时直接用 RespondToModel 返回错误;格式正确时,把核心工作交给文件里的 handle 函数。
调用图:调用 1 个内部函数(handle);被 1 处调用(handle);外部调用 1 个(RespondToModel)。
ReportAgentJobResultHandler::matches_kind53–55 ↗
fn matches_kind(&self, payload: &ToolPayload) -> bool
作用:判断这次工具载荷是不是它能处理的类型。这个处理器只接受函数调用形式的参数。
数据流:进去的是一个 ToolPayload 引用 → 它用 matches! 判断是否为 ToolPayload::Function → 出来是 true 或 false,不修改载荷内容。
调用关系:工具运行时可以用它先筛选合适的处理器。它和 handle_call 的检查互相呼应:前者提前判断,后者在真正执行前再次防呆。
调用图:外部调用 1 个(matches!)。
handle58–98 ↗
async fn handle(
session: Arc<Session>,
arguments: String,
) -> Result<FunctionToolOutput, FunctionCallError>
作用:这是记录代理任务结果的核心函数。它负责解析参数、校验结果格式、写入状态数据库,并把“是否接受”返回给调用方。
数据流:进去的是当前会话 session 和一段 JSON 参数字符串 arguments → 它把字符串解析成 ReportAgentJobResultArgs,确认 result 是 JSON 对象;再从 session 找到必须存在的状态数据库;然后用 job_id、item_id、当前线程 id 和 result 写入任务项结果;如果写入被接受且 stop 为 true,就尝试把任务标记为取消 → 最后把 accepted 序列化成 JSON 文本,包装成 FunctionToolOutput 返回。出错时,会返回给模型能看懂的错误;如果连结果序列化都失败,则返回致命错误。
调用关系:它由 ReportAgentJobResultHandler::handle_call 在参数类型确认后调用,是整个文件真正干活的地方。完成数据库写入后,它用 serde_json::to_string 生成返回文本,再用 FunctionToolOutput::from_text 交回工具系统。
调用图:调用 1 个内部函数(from_text);被 1 处调用(handle_call);外部调用 2 个(to_string, RespondToModel)。
core/src/tools/handlers/agent_jobs/spawn_agents_on_csv.rs源码 ↗
这个文件解决的是“同一种指令要对很多条表格数据重复执行”的问题。没有它,用户可能要一行一行手动让 agent 处理,既慢又容易漏。它先确认收到的是函数式工具调用,然后读取参数里的 CSV 路径、指令模板、并发数量等信息。接着它只允许在一个本地工作目录里运行,因为它要直接读写本机文件。它会读取 CSV,检查表头是否存在、表头是否重复、每行列数是否对得上,还会给每一行生成不重复的任务编号。之后它把这些任务写进状态数据库,启动任务运行器,让多个 worker 像流水线工人一样处理每一行。任务结束后,它确保结果 CSV 已经导出,并把任务编号、状态、输出路径、成功失败数量和错误摘要用 JSON 返回给模型。
SpawnAgentsOnCsvHandler::tool_name18–20 ↗
fn tool_name(&self) -> ToolName
作用:告诉工具注册系统:这个处理器对应的工具名字叫 spawn_agents_on_csv。这样模型请求这个名字时,系统才知道该找这个处理器。
数据流:它不需要外部输入,只是固定生成一个工具名 → 调用 plain 把普通字符串包装成系统认识的工具名 → 返回这个工具名,不改动任何状态。
调用关系:这是工具注册和匹配时会用到的小入口。它只负责报名字,真正的工具说明由 SpawnAgentsOnCsvHandler::spec 给出,真正执行则交给 SpawnAgentsOnCsvHandler::handle。
调用图:调用 1 个内部函数(plain)。
SpawnAgentsOnCsvHandler::spec22–24 ↗
fn spec(&self) -> ToolSpec
作用:返回这个工具的说明书,也就是模型能看到的参数格式和用途。没有它,模型就不知道该怎么正确调用这个 CSV 批量派活工具。
数据流:它不接收参数 → 调用 create_spawn_agents_on_csv_tool 生成工具规格 → 把这份规格返回给工具系统。
调用关系:它通常在工具列表被构建或暴露给模型时使用。它只提供“怎么调用”的说明,不执行任务;实际执行由 SpawnAgentsOnCsvHandler::handle 开始。
调用图:调用 1 个内部函数(create_spawn_agents_on_csv_tool)。
SpawnAgentsOnCsvHandler::handle26–28 ↗
fn handle(&self, invocation: ToolInvocation) -> codex_tools::ToolExecutorFuture<'_>
作用:这是工具被调用时的统一入口。它把一次工具调用包成异步任务,方便系统之后等待它完成。
数据流:输入是一整个 ToolInvocation,里面有会话、当前轮次和调用参数 → 它调用 handle_call 做实际检查和执行,并用 pin 固定这个异步任务,方便运行时管理 → 输出是一个未来会完成的任务句柄,本函数本身不直接产出最终结果。
调用关系:当模型真的调用 spawn_agents_on_csv 时,工具运行框架会走到这里。它不亲自处理 CSV,而是把工作交给 SpawnAgentsOnCsvHandler::handle_call。
调用图:调用 1 个内部函数(handle_call);外部调用 1 个(pin)。
SpawnAgentsOnCsvHandler::handle_call32–55 ↗
async fn handle_call(
&self,
invocation: ToolInvocation,
) -> Result<Box<dyn crate::tools::context::ToolOutput>, FunctionCallError>
作用:把通用的工具调用拆开,确认里面确实是函数参数,然后转交给真正干活的 handle。如果调用格式不对,它会给模型一个可读的错误。
数据流:输入是一次工具调用,里面包含 session、turn 和 payload → 它从 payload 里取出函数字符串参数;如果 payload 不是函数调用,就生成 RespondToModel 错误,让模型知道格式不支持 → 参数合法时,它调用文件里的 handle,再把返回结果包装成统一的工具输出。
调用关系:它位于工具框架和具体业务之间,像门口接待员:先验票,再把人带到真正办理窗口。它由 SpawnAgentsOnCsvHandler::handle 调起,之后把主要流程交给自由函数 handle。
调用图:调用 1 个内部函数(handle);被 1 处调用(handle);外部调用 1 个(RespondToModel)。
SpawnAgentsOnCsvHandler::matches_kind59–61 ↗
fn matches_kind(&self, payload: &ToolPayload) -> bool
作用:判断这个处理器能不能处理当前这种调用内容。这里它只接受函数式 payload,也就是带参数的工具调用。
数据流:输入是一个 ToolPayload → 它检查这个 payload 是否是 Function 这一类 → 返回 true 或 false,不读取文件,也不改状态。
调用关系:工具运行时在分发调用前会用它做筛选。它和 tool_name 一起帮助系统确认“这个请求是不是该交给这个处理器”。
调用图:外部调用 1 个(matches!)。
handle69–300 ↗
async fn handle(
session: Arc<Session>,
turn: Arc<TurnContext>,
arguments: String,
) -> Result<FunctionToolOutput, FunctionCallError>
作用:这是整个文件的核心流程:从 CSV 创建 agent job,运行到结束,并返回结果摘要。它把“读表、建任务、启动 worker、导出结果、汇报进度”串成一条完整流水线。
数据流:输入是当前会话、当前轮次上下文和一段 JSON 参数字符串 → 它先解析参数并检查指令不能为空;再找到本地工作目录和状态数据库;然后读取 CSV,解析表头和行,检查表头、列数和 ID 列;接着把每一行变成数据库里的任务项,并生成任务 ID、输出 CSV 路径、任务名和最大运行时间;之后它创建 agent job,构建运行器选项,标记任务为运行中,并调用任务循环跑完;如果中途失败,会把任务标记为失败并把错误返回给模型;最后它读取任务和进度,必要时导出结果 CSV,把状态、输出路径、成功失败数量和错误摘要序列化成 JSON 文本返回。
调用关系:它由 SpawnAgentsOnCsvHandler::handle_call 调用,是这个工具真正的发动机。它会先用 single_local_environment_cwd 确认能安全访问本地文件,再调用一批共享能力:解析参数、解析 CSV、写状态数据库、构建 runner、运行 agent job、导出 CSV。它本身主要负责排顺序和兜底错误。
调用图:调用 2 个内部函数(from_text, single_local_environment_cwd);被 1 处调用(handle_call);外部调用 10 个(new, from, new_v4, Object, with_capacity, format!, to_string, read_to_string, try_exists, RespondToModel)。
single_local_environment_cwd302–323 ↗
fn single_local_environment_cwd(turn: &TurnContext) -> Result<AbsolutePathBuf, FunctionCallError>
作用:确认当前调用只对应一个本地工作目录,并把这个目录转成系统可以直接读写的绝对路径。这个检查很重要,因为本工具要读输入 CSV、写输出 CSV,不能随便在远程环境里假装本地文件可用。
数据流:输入是当前轮次上下文 TurnContext,里面有本轮可用的运行环境 → 它检查环境数量必须刚好一个;再检查这个环境不能是远程环境;最后把当前工作目录转换成本机绝对路径 → 成功时返回这个路径,失败时返回给模型看的错误信息。
调用关系:它被核心 handle 在读 CSV 前调用,相当于开工前先确认施工现场就在本地。后面的 handle 会用它返回的目录拼出输入 CSV 和输出 CSV 的完整路径。
调用图:被 1 处调用(handle);外部调用 1 个(RespondToModel)。
记忆提取管线
这些文件实现异步启动管线,用于准备运行时上下文,执行 rollout 级记忆提取,并在后台工作流中进行全局整合。
memories/write/src/runtime.rs源码 ↗
这个文件主要有两套“上下文”。MemoryStartupContext 像一次记忆启动任务的工具箱,里面装着当前线程、线程管理器、登录管理器、模型提供方和统计上报工具。它能创建请求所需的模型信息,向模型流式发送第一阶段提示词,还能临时拉起一个“整理记忆”的内部代理线程,用完再关掉。StageOneRequestContext 则更像一次模型请求的小票,记录本次用的模型、推理强度、服务档位和遥测工具。这里的“遥测”就是运行时打点统计,比如耗时、次数、token 用量,方便事后知道系统慢在哪里、花了多少模型额度。一个重要细节是,流式请求会一边接收模型返回一边拼文字;如果模型没有用增量文字返回,它也会从完成的消息块里补取文本,避免拿不到结果。
StageOneRequestContext::start_timer56–58 ↗
fn start_timer(&self, name: &str) -> Option<codex_otel::Timer>
作用:为第一阶段模型请求启动一个计时器,用来统计某段操作花了多久。计时器启动失败时不会让主流程崩掉,而是返回空值。
数据流:进去的是一个计时名称;它把这个名称交给 session_telemetry,也就是本次会话的统计记录器;出来的是一个可选的 Timer,后续丢弃或结束时就能记录耗时,函数本身不改业务数据。
调用关系:它只是 StageOneRequestContext 上的便捷入口,内部把活儿交给遥测系统的 start_timer。需要给第一阶段请求某一步计时时,调用方会从这个上下文里拿它用。
调用图:调用 1 个内部函数(start_timer)。
StageOneRequestContext::counter60–62 ↗
fn counter(&self, name: &str, inc: i64, tags: &[(&str, &str)])
作用:给第一阶段请求记录一个计数,比如某件事发生了几次。它适合统计成功次数、失败次数这类简单数字。
数据流:进去的是计数器名字、要增加的数值和一组标签;它把这些原样交给 session_telemetry;出来没有返回值,但统计系统里的对应计数会被增加。
调用关系:emit_metrics 会调用它,把第一阶段请求中的指标写出去。它自己不计算指标,只负责把调用方给出的数字送到遥测系统的 counter。
调用图:调用 1 个内部函数(counter);被 1 处调用(emit_metrics)。
StageOneRequestContext::histogram64–66 ↗
fn histogram(&self, name: &str, value: i64, tags: &[(&str, &str)])
作用:记录第一阶段请求里的一个分布型数值,比如耗时、大小、token 数。直白说,它不只关心一次是多少,还方便事后看一堆请求的大致分布。
数据流:进去的是指标名字、数值和标签;它交给 session_telemetry 的 histogram;出来没有直接结果,但遥测系统会多一条可用于汇总分析的记录。
调用关系:emit_metrics 会用它上报阶段一相关的分布指标。它在流程中是一个薄薄的转接层,把业务代码和底层统计工具隔开。
调用图:调用 1 个内部函数(histogram);被 1 处调用(emit_metrics)。
MemoryStartupContext::new79–100 ↗
fn new(
thread_manager: Arc<ThreadManager>,
auth_manager: Arc<AuthManager>,
thread_id: ThreadId,
thread: Arc<CodexThread>,
config: &Config,
source: Sess
作用:创建一个正式运行用的 MemoryStartupContext。它会根据配置和登录管理器准备模型提供方,再把所有运行所需材料打包到上下文里。
数据流:进去的是线程管理器、登录管理器、线程编号、线程对象、配置和会话来源;它先从配置里创建模型提供方,再调用 new_with_provider 继续组装;出来的是一个可直接用于记忆启动流程的 MemoryStartupContext。
调用关系:start_memories_startup_task 会在启动记忆任务时调用它,测试 memories_startup_phase1_uses_live_thread_service_tier_and_detached_metadata 也会走这条正式路径。它把模型提供方创建好之后,把更细的初始化交给 new_with_provider。
调用图:被 2 处调用(start_memories_startup_task, memories_startup_phase1_uses_live_thread_service_tier_and_detached_metadata);外部调用 3 个(clone, new_with_provider, create_model_provider)。
MemoryStartupContext::new_for_testing103–121 ↗
fn new_for_testing(
thread_manager: Arc<ThreadManager>,
auth_manager: Arc<AuthManager>,
thread_id: ThreadId,
thread: Arc<CodexThread>,
config: &Config,
作用:创建一个测试用的 MemoryStartupContext,但允许测试自己传入假的或特制的模型提供方。这样测试不用真的连外部模型服务。
数据流:进去的是和正式构造类似的一组对象,外加一个现成的 provider;它不再创建 provider,而是直接把这些材料交给 new_with_provider;出来的是测试可控的 MemoryStartupContext。
调用关系:memory_startup_context_with_provider 会用它搭测试环境。它和 new 共用 new_with_provider,保证测试版和正式版除了 provider 来源不同,其余初始化尽量一致。
调用图:被 1 处调用(memory_startup_context_with_provider);外部调用 1 个(new_with_provider)。
MemoryStartupContext::new_with_provider123–164 ↗
fn new_with_provider(
thread_manager: Arc<ThreadManager>,
auth_manager: Arc<AuthManager>,
thread_id: ThreadId,
thread: Arc<CodexThread>,
config: &Config,
作用:这是实际组装 MemoryStartupContext 的地方。它把线程、登录、模型提供方和遥测信息放到一起,形成记忆启动流程的核心工具箱。
数据流:进去的是线程管理器、登录管理器、线程编号、线程对象、配置、会话来源和模型提供方;它读取缓存登录信息,取账号、邮箱、认证方式,收集认证环境信息和用户代理,再创建 SessionTelemetry;出来的是保存好这些字段的 MemoryStartupContext。
调用关系:new 和 new_for_testing 都把最后初始化交给它。它会调用 originator、collect_auth_env_telemetry、user_agent 和 SessionTelemetry 的 new,把“谁在用、从哪里来、用什么模型”这些统计背景先准备好。
调用图:调用 3 个内部函数(originator, collect_auth_env_telemetry, new);外部调用 1 个(user_agent)。
MemoryStartupContext::thread_id166–168 ↗
fn thread_id(&self) -> ThreadId
作用:返回当前记忆启动任务所属的线程编号。这个编号就像工单号,后面查状态、认领任务都要靠它。
数据流:进去不需要额外输入;它读取上下文里保存的 thread_id;出来就是这个 ThreadId,不改变任何状态。
调用关系:claim_startup_jobs 和 claim 会用它确认当前任务对应哪个线程。它是一个简单但关键的标识读取口。
调用图:被 2 处调用(claim_startup_jobs, claim)。
MemoryStartupContext::state_db170–172 ↗
fn state_db(&self) -> Option<Arc<StateRuntime>>
作用:拿到当前线程关联的状态数据库,如果有的话。状态数据库可以理解成记忆任务记录进度和结果的小账本。
数据流:进去不需要参数;它向 thread 查询 state_db;出来是一个可选的 StateRuntime,如果这个线程没有状态库就返回空。
调用关系:claim_startup_jobs、failed、no_output、success 和 prune 等流程会用它读写任务状态。它把状态库藏在哪个线程里的细节隔离开,让这些流程只管拿账本办事。
调用图:被 5 处调用(claim_startup_jobs, failed, no_output, success, prune)。
MemoryStartupContext::provider174–176 ↗
fn provider(&self) -> &dyn ModelProvider
作用:返回当前上下文里的模型提供方。模型提供方就是实际知道怎么连接模型服务的对象。
数据流:进去不需要参数;它从共享 provider 里取出一个普通引用;出来是 ModelProvider 接口引用,调用者可以用它访问模型相关能力,但这里不转移所有权。
调用关系:它内部只是调用 as_ref,把共享对象变成可使用的接口引用。需要直接碰模型提供方的代码,可以从 MemoryStartupContext 通过这个函数拿到入口。
调用图:外部调用 1 个(as_ref)。
MemoryStartupContext::counter178–180 ↗
fn counter(&self, name: &str, inc: i64, tags: &[(&str, &str)])
作用:给整个记忆启动上下文记录计数指标,比如认领了几个任务、成功或失败了几次。它让业务流程不用直接接触底层统计系统。
数据流:进去的是指标名、增加值和标签;它转交给 session_telemetry 的 counter;出来没有返回值,但统计记录会被更新。
调用关系:emit_metrics、claim、failed 和 succeed 会调用它,把记忆启动流程里的关键事件记下来。它的位置像统一的计数上报口。
调用图:调用 1 个内部函数(counter);被 4 处调用(emit_metrics, claim, failed, succeed)。
MemoryStartupContext::histogram182–184 ↗
fn histogram(&self, name: &str, value: i64, tags: &[(&str, &str)])
作用:给整个记忆启动上下文记录分布型指标,最常见的是 token 用量这类数值。这样之后可以看平均值、峰值或大致范围。
数据流:进去的是指标名、数值和标签;它交给 session_telemetry 的 histogram;出来没有返回值,但遥测系统会保存这次数值记录。
调用关系:emit_token_usage_metrics 会用它上报 token 使用情况。它把“怎么写统计”的细节集中起来,调用方只需要给数值。
调用图:调用 1 个内部函数(histogram);被 1 处调用(emit_token_usage_metrics)。
MemoryStartupContext::start_timer186–188 ↗
fn start_timer(&self, name: &str) -> Option<codex_otel::Timer>
作用:为记忆启动流程中的某一步启动计时。它用于排查慢在哪里,也能帮助衡量不同阶段的耗时。
数据流:进去的是计时器名字;它调用 session_telemetry.start_timer;出来是可选 Timer,创建失败就返回空,主流程不会因此中断。
调用关系:它是 MemoryStartupContext 级别的计时入口,底层交给遥测系统的 start_timer。任何拿着这个上下文的流程都可以用它给自己的步骤打表计时。
调用图:调用 1 个内部函数(start_timer)。
MemoryStartupContext::stage_one_request_context190–216 ↗
async fn stage_one_request_context(
&self,
config: &Config,
model_name: &str,
reasoning_effort: ReasoningEffort,
) -> StageOneRequestContext
作用:为“第一阶段”模型请求准备一张完整的小票:模型信息、推理配置、服务档位和统计上下文。没有这一步,后面发请求时就不知道该按什么模型参数来跑。
数据流:进去的是配置、模型名和推理强度;它读取当前线程的配置快照,向模型管理器查询模型信息,再决定推理摘要设置和服务档位;出来是 StageOneRequestContext,里面带着模型信息、带模型名的遥测对象、推理强度、摘要模式和服务档位。
调用关系:build_request_context 会调用它来准备阶段一请求。它会借助 thread_manager 的模型管理器查询模型资料,并把 session_telemetry 克隆成带当前模型名的版本,供后面的 stream_stage_one_prompt 使用。
调用图:被 1 处调用(build_request_context);外部调用 2 个(to_models_manager_config, clone)。
MemoryStartupContext::stream_stage_one_prompt218–290 ↗
async fn stream_stage_one_prompt(
&self,
config: &Config,
prompt: &Prompt,
context: &StageOneRequestContext,
) -> anyhow::Result<(String, Option<TokenUsage>)>
作用:把第一阶段提示词真正发给模型,并一边接收模型流式返回一边拼出最终文本。它还会带回 token 用量,方便统计模型花费。
数据流:进去的是配置、Prompt 和 StageOneRequestContext;它先解析安装编号,读取线程配置快照,创建 ModelClient 和客户端会话,再生成这次“脱离主会话的记忆请求”所需元数据;随后把提示词、模型信息、推理参数、服务档位和元数据发给模型流;它不断读取 ResponseEvent,把增量文本拼成 result,如果没有增量文本就尝试从完成的消息块里提取文字,遇到 Completed 时保存 token_usage 并结束;出来是最终文本和可选的 token 用量。
调用关系:sample 会调用它获取模型采样结果。它把工作交给 resolve_installation_id、detached_memory_responses_metadata、ModelClient、client_session.stream 和 content_items_to_text 等工具;自己负责把这些零件串起来,并把模型事件流整理成调用方能直接使用的普通字符串。
调用图:调用 3 个内部函数(new, from, disabled);被 1 处调用(sample);外部调用 7 个(clone, new, content_items_to_text, detached_memory_responses_metadata, resolve_installation_id, format!, to_string)。
MemoryStartupContext::spawn_consolidation_agent292–340 ↗
async fn spawn_consolidation_agent(
&self,
config: Config,
prompt: Vec<UserInput>,
) -> anyhow::Result<SpawnedConsolidationAgent>
作用:启动一个专门用于“整理记忆”的内部代理线程,并把整理提示词交给它。可以把它理解成临时请来一个后台助手,帮忙把记忆内容归并好。
数据流:进去的是一份 Config 和一组用户输入形式的 prompt;它先根据当前目录选择默认运行环境,然后通过 thread_manager 启动一个新的内部线程,来源标记为 MemoryConsolidation;接着把 prompt 作为 UserInput 提交给这个新线程;如果提交成功,出来的是 SpawnedConsolidationAgent,里面有新线程编号和线程对象;如果提交失败,它会先尝试关闭这个代理线程,再把错误返回。
调用关系:它会调用 thread_manager.start_thread_with_options 创建线程,并在提交失败时调用 shutdown_consolidation_agent 做清理,还可能用 warn! 记录清理失败。它负责“拉起后台助手”和“失败时收拾现场”这两件事。
调用图:调用 1 个内部函数(shutdown_consolidation_agent);外部调用 4 个(default, new, Internal, warn!)。
MemoryStartupContext::shutdown_consolidation_agent342–360 ↗
async fn shutdown_consolidation_agent(
&self,
agent: SpawnedConsolidationAgent,
) -> anyhow::Result<()>
作用:关闭前面启动的记忆整理代理线程,避免后台线程一直占着资源。它给关闭动作设了 10 秒上限,防止卡死。
数据流:进去的是 SpawnedConsolidationAgent;它拆出 thread_id 和 thread,先尝试从 thread_manager 移除这个线程,如果管理器里没有就用传入的线程对象;然后等待 shutdown_and_wait 完成,并用 timeout 限制最多等 10 秒;出来是成功的空结果,或者关闭超时、关闭失败的错误。
调用关系:spawn_consolidation_agent 在提交提示词失败时会调用它清理新线程,其他需要结束整理代理的流程也可以用它。它把移除线程和等待线程安全退出这两步包成一个可靠的收尾动作。
调用图:被 1 处调用(spawn_consolidation_agent);外部调用 2 个(from_secs, timeout)。
memories/write/src/start.rs源码 ↗
这个文件像一个“开工检查员”。用户开启一个根会话后,它不会马上无脑启动记忆流程,而是先看几件事:这是不是临时会话、记忆功能有没有打开、当前是不是主会话。只有都合格,才会创建一个启动上下文,也就是把后面要用到的线程、登录、配置、会话来源等信息打包好。接着它确认状态数据库可用;如果数据库都没有,就直接跳过,因为记忆流程需要地方记录状态。真正的工作被放进一个异步后台任务里执行,也就是不挡住主流程。后台任务会创建记忆目录,准备扩展说明文件,先清理旧记忆以控制数据库大小,然后检查速率限制,避免超过服务额度。通过检查后,它依次运行第一阶段和第二阶段的记忆启动流程。整体上,这个文件不直接生成记忆内容,而是决定“什么时候可以开始”,并把各个步骤按安全顺序串起来。
start_memories_startup_task22–79 ↗
fn start_memories_startup_task(
thread_manager: Arc<ThreadManager>,
auth_manager: Arc<AuthManager>,
thread_id: ThreadId,
thread: Arc<CodexThread>,
config: Arc<Config>,
source:
作用:这个函数在会话启动时尝试开启记忆系统的后台启动流程。它的作用是先把不该运行的情况拦住,再把真正耗时的记忆准备工作放到后台慢慢做。
数据流:进去的是线程管理器、登录管理器、线程编号、当前线程对象、配置和会话来源。它先读取配置和会话来源,判断是否是临时会话、记忆功能是否关闭、是否是非根代理;如果不合适,就什么也不改直接返回。合适时,它用这些输入创建 MemoryStartupContext,也就是后续流程共用的一包上下文信息;再检查状态数据库是否存在。之后它启动一个异步任务:创建记忆根目录,写入扩展说明,清理旧记忆,检查速率限制;如果限额不允许,就记录一次“因为限流跳过”的指标并结束;如果允许,就继续执行第一阶段和第二阶段。出来的结果不是一个返回值,而是后台任务被安排运行,并可能创建目录、写入说明文件、清理数据库、记录指标、推进记忆流程。
调用关系:它是记忆启动流水线的入口调度点,通常在根会话启动时被上层代码调用。它先调用 MemoryStartupContext::new 准备公共上下文,再把后面的活交给 tokio::spawn 启动后台任务。在后台任务里,它会调用 memory_root 找到记忆目录,调用 seed_extension_instructions 准备扩展说明,调用 phase1::prune 做清理,调用 guard::rate_limits_ok 做额度检查,最后依次把工作交给 phase1::run 和 phase2::run。它自己主要负责判断、排队和串联,不负责每个阶段的具体记忆处理。
调用图:调用 5 个内部函数(seed_extension_instructions, rate_limits_ok, prune, run, new);外部调用 9 个(clone, new, clone, is_non_root_agent, memory_root, run, create_dir_all, spawn, warn!)。
memories/write/src/phase1.rs源码 ↗
可以把这个文件理解成一条“记忆加工流水线”的前半段。程序启动时,它先去数据库里认领一批还没处理、又符合条件的对话记录,避免多个任务抢同一份活。然后它为这批任务准备统一的模型请求配置,再并发处理每条记录:读取对话文件,过滤掉不适合进入记忆的内容,比如开发者指令、AGENTS.md 说明、技能说明;保留真正有用的对话和子代理通信;再把内容发给模型,让模型按固定 JSON 格式返回 raw_memory、rollout_summary 和可选 slug。拿到结果后,它会把可能的密钥、令牌等秘密打码,再把成功、无输出或失败状态写回数据库。最后它统计处理数量和 token 用量,写入指标,方便观察这条流水线是否健康。文件里还包含清理旧的、没再使用的第一阶段记忆的逻辑。
run70–108 ↗
async fn run(context: Arc<MemoryStartupContext>, config: Arc<Config>)
作用:启动并串起第一阶段记忆提取的完整流程。它决定要处理哪些旧对话,安排并发执行,最后记录统计结果。
数据流:输入是启动上下文和全局配置 → 先构建模型请求环境,再从数据库认领候选任务 → 如果没有任务就记一条“跳过”指标;如果有任务就交给并发执行器处理 → 收集每个任务的成败和 token 用量,汇总后写指标和日志。
调用关系:这是本文件的主流程入口。它依次调用 build_request_context、claim_startup_jobs、run_jobs、aggregate_stats 和 emit_metrics,把认领、执行、统计这些零件串成一条流水线。
调用图:调用 5 个内部函数(aggregate_stats, build_request_context, claim_startup_jobs, emit_metrics, run_jobs);外部调用 1 个(info!)。
prune111–133 ↗
async fn prune(context: &MemoryStartupContext, config: &Config)
作用:清理太久没用的第一阶段原始记忆,防止数据库里堆满过期材料。它像定期打扫仓库,把已经没有价值的旧货清出去。
数据流:输入是启动上下文和配置 → 从上下文里拿数据库连接,读取配置里的最长闲置天数 → 要求数据库按保留策略删掉一批旧的 stage-1 输出 → 成功时写日志,失败时只警告,不让启动流程崩掉。
调用关系:它由 start_memories_startup_task 在记忆启动任务中调用。它不参与单个提取任务,而是在启动阶段做维护工作。
调用图:调用 1 个内部函数(state_db);被 1 处调用(start_memories_startup_task);外部调用 2 个(info!, warn!)。
output_schema136–147 ↗
fn output_schema() -> Value
作用:给模型规定输出格式,要求模型必须返回一份结构固定的 JSON。这样后面的代码才能放心解析,不用猜模型说了什么。
数据流:没有外部输入 → 构造一个 JSON schema(JSON 格式说明书),要求包含 rollout_summary、rollout_slug、raw_memory 三个字段,且不允许多余字段 → 返回这份格式约束。
调用关系:job::sample 在向模型发请求时会使用它,测试 job::tests::output_schema_requires_rollout_slug_and_keeps_it_nullable 也会检查它是否要求 slug 字段且允许为空。
调用图:被 2 处调用(sample, output_schema_requires_rollout_slug_and_keeps_it_nullable);外部调用 1 个(json!)。
claim_startup_jobs149–187 ↗
async fn claim_startup_jobs(
context: &MemoryStartupContext,
memories_config: &MemoriesConfig,
) -> Option<Vec<codex_state::Stage1JobClaim>>
作用:从数据库里认领一批适合在启动时处理的对话记录。认领的作用是防止多个后台任务同时处理同一条记录。
数据流:输入是启动上下文和记忆配置 → 从上下文拿数据库;整理允许处理的会话来源;带着扫描数量、最多认领数、年龄限制、空闲时间和租约时间去数据库申请任务 → 成功返回任务列表,失败或没有数据库时返回空结果并写警告。
调用关系:run 在正式处理前调用它。它只负责“拿活儿”,拿到的任务会继续交给 run_jobs 并发处理。
调用图:调用 2 个内部函数(state_db, thread_id);被 1 处调用(run);外部调用 1 个(warn!)。
build_request_context189–202 ↗
async fn build_request_context(
context: &MemoryStartupContext,
config: &Config,
) -> StageOneRequestContext
作用:准备第一阶段调用模型所需的请求上下文。它决定用哪个模型,以及用什么推理强度。
数据流:输入是启动上下文和全局配置 → 如果配置里指定了提取模型就用配置;否则询问当前服务提供方推荐的记忆提取模型 → 调用上下文方法创建 StageOneRequestContext → 返回给后续模型请求复用。
调用关系:run 一开始调用它。后面的 run_jobs 和 job::sample 都依赖这个上下文来发起模型请求、记录指标和计时。
调用图:调用 1 个内部函数(stage_one_request_context);被 1 处调用(run)。
run_jobs204–222 ↗
async fn run_jobs(
context: Arc<MemoryStartupContext>,
config: Arc<Config>,
claimed_candidates: Vec<codex_state::Stage1JobClaim>,
stage_one_context: StageOneRequestContext,
) -> Vec<Jo
作用:把已经认领的多条任务并发跑起来。这样启动时不用一条一条慢慢等,可以同时处理多份对话记录。
数据流:输入是共享的启动上下文、配置、任务列表和请求上下文 → 把任务列表变成异步任务流,为每条任务调用 job::run → 按固定并发上限同时执行 → 收集所有 JobResult 后返回。
调用关系:它由 run 调用,是“批量执行器”。真正处理单条对话的细节交给 job::run。
job::run227–280 ↗
async fn run(
context: &MemoryStartupContext,
config: &Config,
claim: codex_state::Stage1JobClaim,
stage_one_context: &StageOneRequestContext,
) -> JobResult
作用:处理一条具体的第一阶段记忆任务。它负责把一次认领到的对话变成数据库中的成功、无输出或失败记录。
数据流:输入是上下文、配置、一条数据库认领任务和请求上下文 → 调用 job::sample 读取对话并让模型提取记忆 → 如果提取失败,就标记失败;如果模型返回空记忆或空摘要,就标记无输出;否则把原始记忆、摘要、slug 和源更新时间写回数据库 → 返回该任务结果和 token 用量。
调用关系:run_jobs 会为每个认领任务调用它。它把模型提取交给 job::sample,把数据库状态更新交给 job::result::failed、job::result::no_output 或 job::result::success。
job::sample283–324 ↗
async fn sample(
context: &MemoryStartupContext,
config: &Config,
rollout_path: &Path,
rollout_cwd: &Path,
stage_one_context: &StageOneRequestContext,
) ->
作用:读取一份对话记录,整理成模型能看的输入,并真正调用模型提取记忆。它是单条任务里最核心的“提炼”步骤。
数据流:输入是对话文件路径、当时工作目录、配置和请求上下文 → 从磁盘加载 rollout 对话项;过滤和序列化适合记忆的内容;拼出用户消息和基础指令;附上 output_schema 约束模型输出 → 调用 stream_stage_one_prompt 获取模型返回 → 解析成 StageOneOutput,并把秘密信息打码 → 返回提取结果和 token 用量。
调用关系:job::run 调用它来完成实际提取。它依赖 output_schema 规定模型输出格式,依赖 job::serialize_filtered_rollout_response_items 清洗对话内容,再把请求交给上下文的 stream_stage_one_prompt。
调用图:调用 4 个内部函数(default, output_schema, stream_stage_one_prompt, load_rollout_items);外部调用 4 个(redact_secrets, serialize_filtered_rollout_response_items, from_str, vec!)。
job::result::failed329–347 ↗
async fn failed(
context: &MemoryStartupContext,
thread_id: codex_protocol::ThreadId,
ownership_token: &str,
reason: &str,
)
作用:把一条任务标记为失败,并记录失败原因。这样系统下次可以按重试规则再试,而不是悄悄丢掉。
数据流:输入是上下文、线程 ID、认领令牌和失败原因 → 写一条警告日志 → 如果数据库可用,就用认领令牌把这条 stage-1 任务标成失败,并设置重试延迟 → 不返回业务结果。
调用关系:job::run 在 job::sample 出错时调用它。它只负责失败状态落库,不再继续处理模型内容。
job::result::no_output349–368 ↗
async fn no_output(
context: &MemoryStartupContext,
thread_id: codex_protocol::ThreadId,
ownership_token: &str,
) -> JobOutcome
作用:把模型没有产出有效记忆的任务标记为“成功但无输出”。这表示任务处理过了,只是这段对话没什么可记。
数据流:输入是上下文、线程 ID 和认领令牌 → 如果数据库不可用,直接返回失败结果 → 否则要求数据库标记该任务为 succeeded_no_output → 数据库确认成功就返回 SucceededNoOutput,否则返回 Failed。
调用关系:job::run 在模型返回空 raw_memory 或空 rollout_summary 时调用它。它把“没内容可保存”和“程序失败”区分开。
调用图:调用 1 个内部函数(state_db)。
job::result::success370–400 ↗
async fn success(
context: &MemoryStartupContext,
thread_id: codex_protocol::ThreadId,
ownership_token: &str,
source_updated_at: i64,
raw_me
作用:把成功提取出的记忆写进数据库,并标记任务完成。它是单条任务最终落地成果的地方。
数据流:输入是上下文、线程 ID、认领令牌、源记录更新时间、原始记忆、摘要和可选 slug → 如果数据库不可用,返回失败 → 否则把这些结果交给数据库保存,并标记 stage-1 成功 → 数据库确认后返回 SucceededWithOutput,否则返回 Failed。
调用关系:job::run 在模型返回有效 raw_memory 和 rollout_summary 后调用它。它负责把 job::sample 的产物变成持久保存的数据。
调用图:调用 1 个内部函数(state_db)。
job::serialize_filtered_rollout_response_items404–424 ↗
fn serialize_filtered_rollout_response_items(
items: &[RolloutItem],
) -> codex_protocol::error::Result<String>
作用:把一串对话记录筛选、清洗后转成 JSON 字符串,准备塞进模型提示词里。它确保模型只看到适合用来生成记忆的内容。
数据流:输入是 rollout 里的多种记录项 → 保留可记忆的 ResponseItem,转换子代理通信,丢掉会话元信息、压缩记录、事件等不适合内容 → 序列化成 JSON 字符串 → 对字符串里的秘密信息打码 → 返回清洗后的文本。
调用关系:job::sample 在构造模型输入时调用它。多个测试也调用它,确认 AGENTS 指令会被去掉、环境信息会保留、秘密会被打码、子代理通信会被保留。
调用图:外部调用 3 个(redact_secrets, iter, to_string)。
job::sanitize_response_item_for_memories426–462 ↗
fn sanitize_response_item_for_memories(item: &ResponseItem) -> Option<ResponseItem>
作用:判断一条模型交互记录能不能进入记忆输入,并在必要时删掉其中不该记的片段。它像一道安检门。
数据流:输入是一条 ResponseItem → 如果不是普通消息,就交给 should_persist_response_item_for_memories 判断是否保留;如果是 developer 消息,直接丢掉;如果是 user 消息,就过滤掉 AGENTS.md 指令和 skill 说明等上下文片段 → 返回清洗后的消息,或返回空表示丢弃。
调用关系:job::serialize_filtered_rollout_response_items 在处理每条 ResponseItem 时使用它。它又会调用 job::is_memory_excluded_contextual_user_fragment 判断用户消息里的某个文本片段是否该删。
调用图:外部调用 2 个(should_persist_response_item_for_memories, clone)。
job::is_memory_excluded_contextual_user_fragment464–471 ↗
fn is_memory_excluded_contextual_user_fragment(content_item: &ContentItem) -> bool
作用:识别用户消息里哪些片段只是临时上下文,不该写进长期记忆。比如项目指令和技能说明,不代表用户真正想让系统记住的事实。
数据流:输入是一段 ContentItem → 如果不是输入文本,就认为不排除 → 如果是文本,就检查它是否被特定开头和结尾包住,比如 AGENTS.md instructions 到 </INSTRUCTIONS>,或 <skill> 到 </skill> → 返回 true 表示要排除,false 表示保留。
调用关系:job::sanitize_response_item_for_memories 在清洗用户消息内容时调用它。它把具体的文本边界判断交给 job::matches_marked_fragment。
调用图:外部调用 1 个(matches_marked_fragment)。
job::matches_marked_fragment473–483 ↗
fn matches_marked_fragment(text: &str, start_marker: &str, end_marker: &str) -> bool
作用:检查一段文本是否以指定标记开头、并以指定标记结尾。它用来识别整块被标签包起来的上下文内容。
数据流:输入是文本、起始标记和结束标记 → 去掉开头空白后检查是否匹配起始标记,去掉结尾空白后检查是否匹配结束标记,比较时忽略大小写 → 同时满足就返回 true,否则返回 false。
调用关系:job::is_memory_excluded_contextual_user_fragment 调用它两次,分别识别 AGENTS.md 指令块和 skill 技能块。
job::tests::classifies_memory_excluded_fragments490–523 ↗
fn classifies_memory_excluded_fragments()
作用:测试哪些用户文本片段会被排除在记忆之外。它防止以后改代码时误把项目指令或技能说明写进记忆。
数据流:输入是一组手写测试文本和预期结果 → 对每段文本调用 job::is_memory_excluded_contextual_user_fragment → 用断言确认 AGENTS.md 和 skill 会被排除,而环境上下文、子代理通知不会被排除。
调用关系:这是 job 模块内部的单元测试,专门保护 job::is_memory_excluded_contextual_user_fragment 和 job::matches_marked_fragment 的行为。
调用图:外部调用 1 个(assert_eq!)。
job::tests::output_schema_requires_rollout_slug_and_keeps_it_nullable526–565 ↗
fn output_schema_requires_rollout_slug_and_keeps_it_nullable()
作用:测试模型输出格式里必须声明 rollout_slug,而且它可以是字符串也可以是空值。这样模型即使没有 slug,也要明确给出 null。
数据流:输入来自 output_schema 生成的 schema → 取出 properties 和 required 列表 → 检查 rollout_slug 存在、被列为必填,并且类型允许 null 和 string → 断言这些规则都成立。
调用关系:这个测试直接调用 output_schema。它保护 job::sample 依赖的模型输出契约,避免后续解析行为被格式改动破坏。
调用图:调用 1 个内部函数(output_schema);外部调用 2 个(assert!, assert_eq!)。
aggregate_stats569–597 ↗
fn aggregate_stats(outcomes: Vec<JobResult>) -> Stats
作用:把每条任务的结果汇总成总数。它告诉系统这轮一共处理了多少、成功多少、失败多少,以及模型用了多少 token。
数据流:输入是一组 JobResult → 逐条查看 outcome,累计成功有输出、成功无输出和失败数量;如果某条有 token 用量,就加到总用量里 → 返回 Stats,若没有任何 token 数据则总用量为空。
调用关系:run 在所有任务完成后调用它,再把结果交给 emit_metrics。两个测试也调用它,确认计数和 token 累加正确。
调用图:被 3 处调用(run, count_outcomes_keeps_usage_empty_when_no_job_reports_it, count_outcomes_sums_token_usage_across_all_jobs);外部调用 1 个(default)。
emit_metrics599–660 ↗
fn emit_metrics(context: &StageOneRequestContext, counts: &Stats)
作用:把本轮第一阶段处理结果写成监控指标。运维或开发者可以靠这些指标知道记忆提取是否正常、模型消耗是否异常。
数据流:输入是请求上下文和汇总后的 Stats → 对认领数、成功数、无输出数、失败数分别打计数器;如果有 token 用量,就按总 token、输入 token、缓存输入 token、输出 token、推理输出 token 写直方图指标 → 不返回数据,只产生监控记录。
调用关系:run 在 aggregate_stats 之后调用它。它不改变任务状态,只负责把结果送进指标系统。
tests::serializes_memory_rollout_with_agents_removed_but_environment_kept670–738 ↗
fn serializes_memory_rollout_with_agents_removed_but_environment_kept()
作用:测试序列化对话时会删掉 AGENTS.md 指令和 skill 内容,但保留环境上下文和子代理通知。这样记忆输入不会混入项目规则,却还能保留有用场景。
数据流:输入是几条手工构造的用户消息 → 调用 job::serialize_filtered_rollout_response_items 序列化并再解析回来 → 检查结果只剩环境上下文和子代理消息,AGENTS 与 skill 片段已经消失。
调用关系:这是外层测试模块里的单元测试,主要保护 job::serialize_filtered_rollout_response_items、job::sanitize_response_item_for_memories 以及上下文过滤规则。
调用图:外部调用 5 个(assert_eq!, serialize_filtered_rollout_response_items, ResponseItem, from_str, vec!)。
tests::serializes_memory_rollout_redacts_secrets_before_prompt_upload741–759 ↗
fn serializes_memory_rollout_redacts_secrets_before_prompt_upload()
作用:测试在把对话发给模型前,里面的密钥会被打码。它防止敏感 token 被上传到模型请求里。
数据流:输入是一条包含类似 API key 的函数调用输出 → 调用 job::serialize_filtered_rollout_response_items → 检查序列化结果不再包含原始密钥,而包含 [REDACTED_SECRET] 占位符。
调用关系:这个测试保护 job::serialize_filtered_rollout_response_items 末尾的 redact_secrets 行为,确保 job::sample 发送给模型的内容已脱敏。
调用图:外部调用 4 个(assert!, serialize_filtered_rollout_response_items, Text, ResponseItem)。
tests::serializes_inter_agent_communications_for_memory762–790 ↗
fn serializes_inter_agent_communications_for_memory()
作用:测试子代理之间的通信会被转换成模型可读的输入并保留下来。因为这些通信可能包含完成任务的重要上下文。
数据流:输入是明文和加密两种 InterAgentCommunication → 调用 job::serialize_filtered_rollout_response_items 序列化,再解析为 ResponseItem 列表 → 检查结果等于通信对象自己转换出的模型输入项。
调用关系:这个测试覆盖 job::serialize_filtered_rollout_response_items 对 RolloutItem::InterAgentCommunication 的特殊处理,保证子代理消息不会被误删。
调用图:调用 3 个内部函数(root, new, new_encrypted);外部调用 6 个(new, assert_eq!, serialize_filtered_rollout_response_items, InterAgentCommunication, from_str, vec!)。
tests::count_outcomes_sums_token_usage_across_all_jobs793–835 ↗
fn count_outcomes_sums_token_usage_across_all_jobs()
作用:测试统计函数能正确汇总多条任务的结果和 token 用量。它确保监控数字不会少算或错算。
数据流:输入是三条手工构造的 JobResult:成功有输出、成功无输出、失败 → 调用 aggregate_stats → 检查 claimed、各类结果数量,以及 input、cached、output、reasoning、total token 的合计都正确。
调用关系:这个测试直接保护 aggregate_stats。run 依赖 aggregate_stats 的结果去写日志和指标,所以这里保证后续指标可靠。
调用图:调用 1 个内部函数(aggregate_stats);外部调用 2 个(assert_eq!, vec!)。
tests::count_outcomes_keeps_usage_empty_when_no_job_reports_it838–852 ↗
fn count_outcomes_keeps_usage_empty_when_no_job_reports_it()
作用:测试如果所有任务都没有报告 token 用量,汇总结果也应该保持为空,而不是伪造一个 0 用量。
数据流:输入是两条没有 token_usage 的 JobResult → 调用 aggregate_stats → 检查任务数正确,并确认 total_token_usage 是 None。
调用关系:这个测试同样保护 aggregate_stats,避免 emit_metrics 在没有真实 token 数据时写出误导性的用量指标。
调用图:调用 1 个内部函数(aggregate_stats);外部调用 2 个(assert_eq!, vec!)。
memories/write/src/phase2.rs源码 ↗
这个文件像一个严谨的仓库管理员。它先去数据库里抢一把“全局锁”(保证同一时间只有一个整理任务在跑),然后准备记忆工作目录,并给内部整理智能体配一套很安全的规则:只能写记忆目录、不能联网、不能再启动别的工具,也不能触发新的记忆生成。接着它从数据库取出需要整理的原始记忆,同步到磁盘上的记忆工作区,用 git 差异判断文件到底有没有变化。没变化就直接登记成功;有变化就把差异写出来,启动整理智能体去处理。智能体运行时,这里还会定期“续租”锁,防止别人误以为任务死了。等智能体完成后,它记录 token 用量,把当前工作区设为新的基准,再把任务标记成功;如果任何一步出错,就记录失败和原因,方便稍后重试。
run46–201 ↗
async fn run(context: Arc<MemoryStartupContext>, config: Arc<Config>)
作用:这是第二阶段记忆整理的主流程。它按固定顺序完成抢任务、准备工作区、取输入、启动整理智能体、记录结果这些步骤,避免多个整理任务互相踩踏。
数据流:进去的是启动上下文和全局配置;它先从上下文拿数据库,再根据配置找到记忆目录和筛选规则。随后它申请任务锁,读取第一阶段产出的记忆,把这些内容同步到工作区,检查是否产生文件变化;如果有变化,就写出差异、生成提示词、启动整理智能体。出来的结果不是直接返回数据,而是通过数据库任务状态、工作区文件、指标计数和后台智能体处理来体现。
调用关系:它由记忆启动任务或测试入口调用,是本文件的总指挥。它会把具体小活交给 job::claim、sync_phase2_workspace_inputs、get_watermark、agent::get_config、agent::get_prompt、agent::handle 和 emit_metrics;任何一步失败时,会交给 job::failed 记录失败。
调用图:调用 6 个内部函数(emit_metrics, get_watermark, sync_phase2_workspace_inputs, memory_workspace_diff, prepare_memory_workspace, write_workspace_diff);被 2 处调用(start_memories_startup_task, run_memory_phase_two_model_request_test);外部调用 9 个(clone, get_config, get_prompt, handle, memory_root, claim, failed, succeed, error!)。
sync_phase2_workspace_inputs203–212 ↗
async fn sync_phase2_workspace_inputs(
root: &Path,
raw_memories: &[Stage1Output],
) -> std::io::Result<()>
作用:这个函数把数据库里选中的原始记忆同步到磁盘上的记忆工作区。它确保整理智能体看到的是最新的一批输入文件。
数据流:进去的是记忆根目录和一组第一阶段记忆输出;它先统计数量,然后写入 rollout 摘要,重建原始记忆文件,最后清理旧的扩展资源。出来的是一个成功或失败的文件操作结果,同时磁盘工作区会被更新。
调用关系:它只被 run 调用,处在“拿到数据库输入之后、检查 git 差异之前”。它把数据库里的内容变成整理智能体能读的文件。
调用图:被 1 处调用(run);外部调用 4 个(prune_old_extension_resources, rebuild_raw_memories_file_from_memories, sync_rollout_summaries_from_memories, len)。
job::claim217–251 ↗
async fn claim(
context: &MemoryStartupContext,
db: &StateRuntime,
) -> Result<Claim, &'static str>
作用:这个函数负责抢占一次全局第二阶段整理任务。简单说,它是在问数据库:“现在能不能让我来整理记忆?”
数据流:进去的是启动上下文和状态数据库;它用当前线程身份去数据库申请任务租约,并读取上次处理到的水位线。如果抢到,就返回包含所有权 token 和水位线的 Claim;如果正在冷却、已有任务运行、暂时不能重试或数据库出错,就返回对应失败原因。
调用关系:run 一开始就需要它,因为后面会改记忆工作区,必须先确认自己有资格。它成功后,后续 job::failed、job::succeed 和心跳都会用这个 token 证明“这个任务还是我拥有”。
job::failed253–279 ↗
async fn failed(
context: &MemoryStartupContext,
db: &StateRuntime,
claim: &Claim,
reason: &'static str,
)
作用:这个函数把第二阶段任务标记为失败,并写明失败原因。这样系统之后可以按规则重试,而不是默默卡住。
数据流:进去的是上下文、数据库、任务 Claim 和失败原因;它先增加失败指标计数,再用 Claim 里的 token 去数据库标记任务失败,并设置重试等待时间。如果发现当前 token 已经不再拥有任务,还会尝试用“非拥有者失败标记”的方式兜底记录。
调用关系:run 和 agent::handle 在准备工作区、读取输入、启动智能体、提交结果等多处失败时都会调用它。它是整个流程的错误出口。
job::succeed281–294 ↗
async fn succeed(
context: &MemoryStartupContext,
db: &StateRuntime,
claim: &Claim,
completion_watermark: i64,
selected_outputs: &[codex_state::Stage1Output],
作用:这个函数把第二阶段整理任务标记为成功。它告诉数据库:这些输入已经整理过了,水位线可以往前推进。
数据流:进去的是上下文、数据库、任务 Claim、完成水位线、这次选中的输入和成功原因;它先记录成功指标,再用 token 请求数据库把任务设为成功,并保存处理到的新水位线。出来的是布尔值,表示数据库是否真的接受了这次成功标记。
调用关系:run 在发现工作区没有变化时会直接调用它;agent::handle 在智能体完成并成功重置工作区基准后也会调用它。它是正常流程的收尾动作。
agent::get_config301–348 ↗
fn get_config(config: &Config, provider: &dyn ModelProvider) -> Option<Config>
作用:这个函数给内部整理智能体制作一份“缩水且上锁”的配置。它的目的不是让智能体自由发挥,而是让它只能在记忆目录里安全地做整理。
数据流:进去的是用户原始配置和模型提供方;它复制一份配置,把工作目录改到记忆根目录,关闭记忆生成、记忆使用、应用说明、插件、协作、工具委派等能力,并设置永不请求人工批准。它还设置沙盒策略:只能写记忆目录、不能联网。最后选择整理用模型和推理强度。出来的是可用的智能体配置;如果沙盒策略设置失败,就返回空。
调用关系:run 在启动整理智能体之前调用它。它为 context.spawn_consolidation_agent 准备安全配置,防止整理任务反过来触发更多任务或访问不该访问的地方。
调用图:调用 1 个内部函数(allow_only);外部调用 4 个(new, clone, memory_root, vec!)。
agent::get_prompt350–356 ↗
fn get_prompt(root: &Path) -> Vec<UserInput>
作用:这个函数生成交给整理智能体的文字指令。也就是告诉智能体:请根据这个记忆工作区里的内容做合并整理。
数据流:进去的是记忆根目录;它调用构建提示词的函数,把目录信息变成一段文本,再包装成用户输入列表。出来的是可以直接发送给智能体的输入内容。
调用关系:run 在拿到安全配置后调用它,然后把得到的提示词交给 spawn_consolidation_agent。它连接了“准备文件”与“让智能体开始干活”这两步。
调用图:外部调用 2 个(build_consolidation_prompt, vec!)。
agent::handle360–450 ↗
fn handle(
context: Arc<MemoryStartupContext>,
claim: Claim,
new_watermark: i64,
selected_outputs: Vec<codex_state::Stage1Output>,
memory_root: codex_utils_abso
作用:这个函数接管已经启动的整理智能体,负责等它跑完、续租任务锁、记录 token 用量、提交成功或失败,并最后关闭智能体。它让主流程不用一直卡在那里等。
数据流:进去的是上下文、任务 Claim、新水位线、被选中的输入、记忆目录、已启动的智能体和总耗时计时器;它启动一个后台任务,循环等待智能体最终状态。若智能体完成,它读取 token 用量并上报指标,再确认自己还持有任务锁;确认后重置 git 基准并标记任务成功。若智能体失败、锁丢失或提交失败,就记录失败。最后它再启动一个清理任务关闭智能体线程。
调用关系:run 在成功启动智能体后调用它,并把后续收尾工作全部交给它。它内部会调用 agent::loop_agent 等待状态,也会调用 emit_token_usage_metrics、reset_memory_workspace_baseline、job::succeed 和 job::failed。
调用图:调用 2 个内部函数(emit_token_usage_metrics, reset_memory_workspace_baseline);外部调用 8 个(clone, failed, succeed, matches!, loop_agent, spawn, error!, warn!)。
agent::loop_agent452–517 ↗
async fn loop_agent(
db: Arc<StateRuntime>,
token: String,
thread_id: ThreadId,
thread: &codex_core::CodexThread,
) -> AgentStatus
作用:这个函数一边等待整理智能体结束,一边定期给数据库里的任务锁“续命”。如果锁续不上,它会认为任务不安全,立刻报错退出。
数据流:进去的是数据库、任务 token、智能体线程编号和智能体线程对象;它反复读取智能体状态,每秒轮询一次,同时按心跳间隔刷新数据库租约。若智能体进入完成、失败等最终状态,就返回这个状态;若线程提前退出、心跳失败或丢失所有权,就返回错误状态。
调用关系:它由 agent::handle 的后台任务调用。它依赖 is_final_agent_status 判断哪些状态算结束,是“智能体运行期间的看门人”。
调用图:调用 3 个内部函数(agent_status, wait_until_terminated, is_final_agent_status);外部调用 4 个(from_secs, pin!, select!, interval)。
get_watermark520–530 ↗
fn get_watermark(
claimed_watermark: i64,
latest_memories: &[codex_state::Stage1Output],
) -> i64
作用:这个函数计算本次整理完成后应该记录到哪里的“水位线”。水位线可以理解为书签,用来知道哪些输入已经处理过。
数据流:进去的是抢任务时拿到的旧水位线,以及这次选中的最新记忆列表;它查看这些记忆的更新时间,取最大的那个时间戳,并和旧水位线比较,保证水位线只会往前走不会倒退。出来的是新的完成水位线。
调用关系:run 在读取到本次输入后调用它。返回值之后会交给 job::succeed,用来更新数据库里的处理进度。
is_final_agent_status532–537 ↗
fn is_final_agent_status(status: &AgentStatus) -> bool
作用:这个函数判断智能体状态是不是已经结束。它把“等待初始化、运行中、被中断但还没最终结束”都看作未完成。
数据流:进去的是一个智能体状态;它检查这个状态是否属于仍需等待的几种状态。出来的是 true 或 false:true 表示可以停止等待,false 表示还要继续观察。
调用关系:agent::loop_agent 在每次读取智能体状态后都会用它判断是否该退出循环。它让状态判断集中在一个小地方,避免到处写重复规则。
调用图:被 1 处调用(loop_agent);外部调用 1 个(matches!)。
emit_metrics539–549 ↗
fn emit_metrics(context: &MemoryStartupContext, counters: Counters)
作用:这个函数记录第二阶段任务派发时的基本指标。指标就是给监控系统看的数字,方便知道任务有没有启动、输入量有多少。
数据流:进去的是启动上下文和计数器;如果输入数量大于 0,它记录输入条数;然后它记录一次“智能体已启动”的任务状态计数。它不返回业务数据,只把数字写到监控系统。
调用关系:run 在成功把智能体交给后台处理后调用它。它补上了流程层面的可观测信息,让运维或开发者能看到第二阶段确实被派发了。
emit_token_usage_metrics551–577 ↗
fn emit_token_usage_metrics(context: &MemoryStartupContext, token_usage: &TokenUsage)
作用:这个函数记录整理智能体消耗了多少 token。token 可以粗略理解为模型阅读和生成文字时使用的计量单位,常用于估算成本和负载。
数据流:进去的是上下文和 token 用量;它分别记录总 token、输入 token、缓存输入 token、输出 token、推理输出 token 等数值,并按类型打标签。出来的是监控系统里的多条直方图数据,函数本身不返回值。
调用关系:agent::handle 在智能体成功完成后调用它。它让系统能知道一次记忆整理到底花了多少模型资源。
调用图:调用 2 个内部函数(histogram, cached_input);被 1 处调用(handle)。
后台监视器
这个最后的后台组件会监视 skill 目录的变化,并向服务器运行时广播使缓存失效的通知。
app-server/src/skills_watcher.rs源码 ↗
这里的 SkillsWatcher 可以理解成一个“文件变动报警器”。它启动时先创建文件监视器,如果系统不支持或创建失败,就退回到一个什么都不做的监视器,避免整个服务器崩掉。它会把需要关注的目录注册进去,比如运行时额外加入的目录,或者某个线程配置会用到的本地技能目录。文件变化不会立刻每次都触发完整刷新,而是经过节流处理,也就是短时间内多次变化合并成一次,像门铃太频繁时只响一声,防止刷屏。后台循环收到变化后,会让 SkillsManager 清掉技能缓存,再通过 OutgoingMessageSender 发出 SkillsChanged 通知。它还用 CancellationToken(取消令牌,用来通知后台任务停下来)在关闭时结束监听。
SkillsWatcher::new37–58 ↗
fn new(
skills_manager: Arc<SkillsManager>,
outgoing: Arc<OutgoingMessageSender>,
) -> Arc<Self>
作用:创建一个技能文件监视器,并顺手启动后台监听循环。别人只要调用它,就能得到一个已经开始工作的 SkillsWatcher。
数据流:进去的是共享的 SkillsManager(技能管理器,用来清缓存)和 OutgoingMessageSender(发消息给外部的通道)→ 它尝试创建真正的 FileWatcher,失败就记录警告并改用不会报错的空监视器;然后添加订阅者,准备取消令牌,启动后台事件循环 → 出来的是一个 Arc 包起来的 SkillsWatcher,里面保存订阅者、当前运行时额外目录的注册记录,以及以后用来关停的取消令牌。
调用关系:这是整个文件的开工入口。创建阶段会把实际监听工作交给 SkillsWatcher::spawn_event_loop;后面注册目录的函数会继续使用这里保存下来的 subscriber。调用图里显示它会被上层的 new 流程使用,用来把技能监视能力接进服务器启动过程。
调用图:调用 3 个内部函数(new, noop, default);被 1 处调用(new);外部调用 5 个(new, new, new, spawn_event_loop, warn!)。
SkillsWatcher::shutdown60–62 ↗
fn shutdown(&self)
作用:通知后台监听任务停止工作。服务器关闭或不再需要监视技能文件时会用它,避免后台任务一直挂着。
数据流:进去的是这个 SkillsWatcher 自己保存的 shutdown_token → 它调用 cancel,把“该停了”的信号发出去 → 后台事件循环收到信号后会跳出循环;这个函数本身不返回特殊结果,只改变取消令牌的状态。
调用关系:它和 SkillsWatcher::spawn_event_loop 配成一对:spawn_event_loop 在后台等文件事件,同时也等这个取消信号;shutdown 发出信号,让后台循环有机会干净退出。
调用图:外部调用 1 个(cancel)。
SkillsWatcher::register_runtime_extra_roots64–78 ↗
fn register_runtime_extra_roots(&self, extra_roots: &[AbsolutePathBuf])
作用:把运行过程中临时增加的额外目录加入监视范围。比如用户或系统后来又告诉服务器“这些地方也可能有技能”,就靠它补上监听。
数据流:进去的是一组绝对路径 extra_roots → 它把每个路径包装成 WatchPath,并标记为 recursive,也就是连子文件夹一起看;然后交给 subscriber 注册 → 最后把新的 WatchRegistration 保存到互斥锁保护的字段里。互斥锁就是一把锁,防止两个任务同时改同一份注册记录。
调用关系:它不直接处理文件变化,只负责告诉文件监视器“这些目录也要看”。真正收到变化后的处理仍然在 SkillsWatcher::spawn_event_loop 里完成。保存 WatchRegistration 的意义是让旧注册能被替换和释放,避免一直监听过期目录。
调用图:调用 1 个内部函数(register_paths);外部调用 1 个(iter)。
SkillsWatcher::register_thread_config80–123 ↗
async fn register_thread_config(
&self,
config: &Config,
thread_manager: &ThreadManager,
environments: &[TurnEnvironmentSelection],
) -> WatchRegistration
作用:根据某个线程的配置,找出这个线程会用到哪些本地技能目录,并把这些目录加入监视范围。这样不同工作环境、插件和配置对应的技能变化,也能被及时发现。
数据流:进去的是 Config(配置)、ThreadManager(线程相关管理入口)和环境选择列表 → 它先取第一个环境选择;如果没有环境、环境不存在,或环境是远程的,就返回空注册,因为远程文件不适合用本地文件监视器看;如果是本地环境,它会根据插件配置算出插件带来的技能目录,再组合当前工作目录、配置层级和是否启用内置技能等信息,生成 SkillsLoadInput;接着向 SkillsManager 询问实际应该监视的技能根目录 → 出来的是 WatchRegistration,表示这些目录已经注册到文件监视器里。
调用关系:这是“按线程配置注册监听目录”的桥梁。它会向 ThreadManager 要环境管理器、插件管理器和技能管理器,把这些信息串起来,最后仍然通过 subscriber.register_paths 把目录交给底层文件监视器。它为 SkillsWatcher::spawn_event_loop 提供要监听的目录来源。
调用图:调用 6 个内部函数(new, environment_manager, plugins_manager, skills_manager, register_paths, default);外部调用 4 个(bundled_skills_enabled, plugins_config_input, first, warn!)。
SkillsWatcher::spawn_event_loop125–153 ↗
fn spawn_event_loop(
rx: Receiver,
skills_manager: Arc<SkillsManager>,
outgoing: Arc<OutgoingMessageSender>,
shutdown_token: CancellationToken,
)
作用:启动真正的后台监听循环:等文件变化,变化后清缓存并通知外部。它是这个文件里让“监视”实际发生的核心。
数据流:进去的是文件事件接收器 rx、SkillsManager、OutgoingMessageSender 和 shutdown_token → 它先把接收器包成 ThrottledWatchReceiver,用固定间隔做节流,避免短时间内重复触发;然后尝试拿当前 Tokio runtime。Tokio runtime 可以理解成 Rust 异步任务的调度器,没有它就没地方运行后台任务,只能记录警告并放弃监听 → 成功后它生成一个后台任务,循环等待两件事:要么收到关闭信号,要么收到文件变化;一旦收到文件变化,就清掉技能缓存,并发送 SkillsChanged 通知。
调用关系:SkillsWatcher::new 会调用它来启动后台工作。它接住 register_runtime_extra_roots 和 register_thread_config 注册目录后产生的文件事件;收到事件后,把缓存清理交给 SkillsManager,把通知发送交给 OutgoingMessageSender。SkillsWatcher::shutdown 则通过取消令牌让这个循环结束。
调用图:调用 1 个内部函数(new);外部调用 4 个(SkillsChanged, try_current, select!, warn!)。