基于扩展的工具运行时与命名空间
这一阶段像系统的“工具大厅”,主要在对话进行中默默支撑模型干活。开局先由排班表决定哪些工具能露面,哪些只后台用。动态工具和扩展工具负责把外部临时能力、插件能力接进来。code-mode 让模型能跑一小段代码并等待结果。网页搜索、图片生成、记忆、技能、目标等扩展各自提供具体本事。工具搜索、看图片、查剩余上下文这些小帮手,则像标签、放大镜和油表,帮助模型安全地找到、使用、控制这些工具。
工具规划与核心适配器
这些文件构建每轮工具计划,并提供通过运行时暴露的动态、扩展支持、托管和辅助工具的核心适配器与规范。
core/src/tools/spec_plan.rs源码 ↗
模型不是永远能用同一批工具:有时可以跑命令,有时能搜索网页,有时能生成图片,有时还能派出子代理。这个文件的作用,就是在每一轮对话里临时拼出一张准确的“工具清单”和一套路由表。工具清单告诉模型“你现在能叫哪些工具”;路由表则像前台分诊台,等模型真的点了某个工具时,把请求交给对应处理器。它会先收集内置工具、命令行工具、MCP 工具(外部服务提供的工具)、插件工具、动态工具和模型托管工具,再处理特殊模式,比如 Code Mode(把很多工具包进一个代码执行入口里)。它还会避免重名工具、按命名空间合并工具,并根据权限、功能开关和模型能力隐藏不该出现的工具。没有它,模型可能看到不能用的工具,或者工具能执行却没被登记,整套调用流程就会乱。
PlannedTools::add108–113 ↗
fn add(&mut self, handler: T)
作用:把一个普通工具处理器加入待发布的工具池。别人会用它来登记一个之后可能给模型看、也可能只用于执行的工具。
数据流:进去的是一个具体工具处理器 → 它把处理器包进可共享的引用里 → 出来没有返回值,但 PlannedTools 里的运行时工具列表多了一项。
调用关系:各类 add_* 函数在挑选工具时会调用它,比如命令行工具、核心工具、MCP 工具、动态工具和插件工具。它是登记工具的最常用入口。
调用图:被 7 处调用(add_collaboration_tools, add_core_utility_tools, add_dynamic_tools, add_mcp_resource_tools, add_mcp_runtime_tools, add_shell_tools, append_extension_tool_executors);外部调用 1 个(new)。
PlannedTools::add_arc115–117 ↗
fn add_arc(&mut self, handler: PlannedRuntime)
作用:把已经包装好的共享工具处理器直接加入工具池。适合那些已经被别的逻辑包过一层的工具。
数据流:进去的是一个 Arc 包装的工具处理器,也就是可共享的工具对象 → 它直接塞进 runtimes 列表 → 工具池多了这个现成工具。
调用关系:多代理 V2 工具和工具搜索处理器会用它,因为这些工具可能已经经过命名空间替换或缓存复用,不需要再重新包装。
调用图:被 2 处调用(add_collaboration_tools, append_tool_search_executor)。
PlannedTools::add_with_exposure119–125 ↗
fn add_with_exposure(&mut self, handler: T, exposure: ToolExposure)
作用:加入工具时顺便指定它的“曝光方式”。曝光方式就是工具是直接给模型看、延后搜索出来,还是只隐藏在后台。
数据流:进去的是工具处理器和曝光规则 → 它先把工具包成共享对象,再套上曝光规则 → 最后把这个带规则的工具放进运行时列表。
调用关系:当某些工具需要特殊可见性时会用它,比如权限请求、新上下文窗口、延迟 MCP 工具和协作工具。add_dispatch_only 也靠它来添加隐藏工具。
调用图:调用 1 个内部函数(override_tool_exposure);被 4 处调用(add_dispatch_only, add_collaboration_tools, add_core_utility_tools, add_mcp_runtime_tools);外部调用 1 个(new)。
PlannedTools::add_dispatch_only127–132 ↗
fn add_dispatch_only(&mut self, handler: T)
作用:登记一个“只负责接单、不展示给模型看”的工具。常用于兼容旧工具名,避免老格式调用没人处理。
数据流:进去的是一个工具处理器 → 它调用 add_with_exposure,并把曝光设成 Hidden(隐藏)→ 工具能被路由执行,但不会出现在模型工具菜单里。
调用关系:add_shell_tools 在启用新版统一执行工具时,会用它把旧版 shell 工具也登记上,防止历史调用格式失效。
调用图:调用 1 个内部函数(add_with_exposure);被 1 处调用(add_shell_tools)。
PlannedTools::add_hosted_spec134–136 ↗
fn add_hosted_spec(&mut self, spec: ToolSpec)
作用:加入一种只需要给模型声明、不由本地路由执行的托管工具规格。比如由模型服务商直接处理的网页搜索或图片生成。
数据流:进去的是一个 ToolSpec(工具说明书)→ 它被放进 hosted_specs 列表 → 后面生成模型可见工具清单时会一起发给模型。
调用关系:add_tool_sources 会把 hosted_model_tool_specs 产出的托管工具逐个交给它保存。
调用图:被 1 处调用(add_tool_sources)。
PlannedTools::runtimes138–140 ↗
fn runtimes(&self) -> &[PlannedRuntime]
作用:查看当前已经登记的本地工具处理器列表。它不改数据,只给后续规划步骤参考。
数据流:进去的是 PlannedTools 自身 → 它返回 runtimes 的只读切片 → 调用者据此检查已有工具、找延迟工具或生成 Code Mode 包装工具。
调用关系:工具搜索、Code Mode 和插件工具追加逻辑都会先看这份列表,避免漏掉已有工具或产生重名冲突。
调用图:被 3 处调用(append_extension_tool_executors, append_tool_search_executor, prepend_code_mode_executors)。
build_tool_router157–165 ↗
fn build_tool_router(
turn_context: &TurnContext,
params: ToolRouterParams<'_>,
tool_search_handler_cache: &ToolSearchHandlerCache,
) -> ToolRouter
作用:生成最终的工具路由器。路由器可以理解成“工具总机”:模型发来工具调用时,它知道该转给哪个处理器。
数据流:进去的是当前轮上下文、各种外部工具参数和工具搜索缓存 → 它先构建模型可见工具说明和工具注册表 → 最后组合成 ToolRouter 返回。
调用关系:这是本文件对外的主要入口,由更上层的 turn_context 流程调用。它把细节交给 build_tool_specs_and_registry,再用 ToolRouter::from_parts 拼成最终对象。
调用图:调用 2 个内部函数(from_parts, build_tool_specs_and_registry);被 1 处调用(from_turn_context)。
build_tool_specs_and_registry168–198 ↗
fn build_tool_specs_and_registry(
turn_context: &TurnContext,
params: ToolRouterParams<'_>,
tool_search_handler_cache: &ToolSearchHandlerCache,
) -> (Vec<ToolSpec>, ToolRegistry)
作用:同时准备两样东西:给模型看的工具说明书,以及后台真正执行用的工具注册表。
数据流:进去的是当前轮上下文、MCP/插件/动态工具等参数和缓存 → 它创建规划上下文,收集工具源,补上工具搜索和 Code Mode 工具 → 输出工具说明列表和注册表。
调用关系:build_tool_router 会调用它。它是整个工具规划流程的主干,把 add_tool_sources、append_tool_search_executor、prepend_code_mode_executors 和最终构建步骤串起来。
调用图:调用 5 个内部函数(add_tool_sources, append_tool_search_executor, build_model_visible_specs_and_registry, prepend_code_mode_executors, wait_agent_timeout_options);被 1 处调用(build_tool_router);外部调用 3 个(default, build, new)。
build_model_visible_specs_and_registry201–239 ↗
fn build_model_visible_specs_and_registry(
turn_context: &TurnContext,
planned_tools: PlannedTools,
) -> (Vec<ToolSpec>, ToolRegistry)
作用:把已规划的工具变成“模型能看到的菜单”和“后台能执行的注册表”。它还会去重和合并命名空间。
数据流:进去的是当前上下文和 PlannedTools → 它遍历运行时工具,跳过重名工具,按曝光规则决定是否给模型看,再追加托管工具说明 → 输出模型可见规格和 ToolRegistry。
调用关系:build_tool_specs_and_registry 在工具收齐后调用它。它会用 spec_for_model_request 调整 Code Mode 下的工具说明,并用 merge_into_namespaces 整理命名空间工具。
调用图:调用 4 个内部函数(from_tools, is_hidden_by_code_mode_only, merge_into_namespaces, spec_for_model_request);被 1 处调用(build_tool_specs_and_registry);外部调用 2 个(new, new)。
spec_for_model_request241–258 ↗
fn spec_for_model_request(
turn_context: &TurnContext,
exposure: ToolExposure,
tool_name: &ToolName,
spec: ToolSpec,
) -> ToolSpec
作用:在发给模型前,按当前模式微调某个工具说明。尤其是在 Code Mode 下,把合适的工具说明改成更适合被代码模式嵌套调用的样子。
数据流:进去的是上下文、曝光方式、工具名和原始工具说明 → 它检查是否处于 Code Mode、是否被排除、是否属于可嵌套工具 → 输出原说明或增强后的说明。
调用关系:build_model_visible_specs_and_registry 给每个模型可见工具生成说明时会调用它。它会参考 is_excluded_from_code_mode,必要时交给 codex_code_mode 做增强。
调用图:调用 2 个内部函数(is_excluded_from_code_mode, name);被 1 处调用(build_model_visible_specs_and_registry);外部调用 3 个(is_code_mode_nested_tool, augment_tool_spec_for_code_mode, matches!)。
hosted_model_tool_specs260–295 ↗
fn hosted_model_tool_specs(context: &CoreToolPlanContext<'_>) -> Vec<ToolSpec>
作用:决定是否要把服务端托管的网页搜索、图片生成等工具告诉模型。托管工具是不一定经过本地工具路由执行的能力。
数据流:进去的是规划上下文 → 它查看模型是否使用 Responses Lite、服务商能力、功能开关、是否已有独立插件版本 → 输出一组托管工具说明。
调用关系:add_tool_sources 会调用它并把结果加入 PlannedTools。它会避开已经有独立网页搜索或图片生成插件的情况,防止同类工具重复出现。
调用图:调用 5 个内部函数(create_image_generation_tool, create_web_search_tool, image_generation_tool_enabled, standalone_image_generation_available, standalone_web_search_enabled);被 1 处调用(add_tool_sources);外部调用 1 个(new)。
search_tool_enabled297–299 ↗
fn search_tool_enabled(turn_context: &TurnContext) -> bool
作用:判断当前模型是否支持工具搜索。工具搜索就是先给模型一个搜索入口,让它按需发现延迟工具,而不是一次性展示所有工具。
数据流:进去的是当前轮上下文 → 它读取模型信息里的 supports_search_tool → 返回 true 或 false。
调用关系:协作工具、插件工具、Code Mode 和工具搜索追加逻辑都会用它,决定是否启用“延迟展示工具”的相关流程。
调用图:被 5 处调用(built_tools, add_collaboration_tools, append_extension_tool_executors, append_tool_search_executor, build_code_mode_executors)。
tool_suggest_enabled301–306 ↗
fn tool_suggest_enabled(turn_context: &TurnContext) -> bool
作用:判断是否允许模型建议安装可发现的插件工具。它要求多个功能开关同时打开。
数据流:进去的是当前轮上下文 → 它读取功能开关集合,检查 ToolSuggest、Apps、Plugins 是否都启用 → 返回是否可用。
调用关系:add_core_utility_tools 用它决定是否添加“列出可安装插件”和“请求安装插件”的工具。
调用图:被 2 处调用(built_tools, add_core_utility_tools)。
namespace_tools_enabled308–310 ↗
fn namespace_tools_enabled(turn_context: &TurnContext) -> bool
作用:判断当前服务商是否支持“命名空间工具”。命名空间像文件夹,可以把 web.run、agent.spawn 这类工具按类别收起来。
数据流:进去的是当前轮上下文 → 它读取 provider capabilities 里的 namespace_tools → 返回是否支持。
调用关系:多代理、工具搜索、独立网页搜索、独立图片生成和最终过滤命名空间工具时都会参考它。
调用图:被 5 处调用(add_collaboration_tools, append_extension_tool_executors, append_tool_search_executor, standalone_image_generation_model_visible, standalone_web_search_enabled)。
multi_agent_v2_enabled312–314 ↗
fn multi_agent_v2_enabled(turn_context: &TurnContext) -> bool
作用:判断当前是否使用第二版多代理工具。V2 是新版子代理管理方式,工具名和超时配置都可能不同。
数据流:进去的是当前轮上下文 → 它比较 multi_agent_version 是否等于 V2 → 返回布尔值。
调用关系:add_collaboration_tools 用它选择 V1 还是 V2 的代理工具;wait_agent_timeout_options 用它选择哪套等待超时配置。
调用图:被 2 处调用(add_collaboration_tools, wait_agent_timeout_options)。
collab_tools_enabled316–325 ↗
fn collab_tools_enabled(turn_context: &TurnContext) -> bool
作用:判断协作类工具,也就是派生子代理和管理子代理的工具,当前能不能使用。
数据流:进去的是当前轮上下文 → 它看多代理版本;禁用就返回 false,V2 直接允许,V1 还要检查子代理嵌套深度是否超限 → 返回是否允许协作工具。
调用关系:add_collaboration_tools 用它决定是否添加代理工具;agent_jobs_tools_enabled 也靠它保证批量代理任务不会在禁止协作时出现。
调用图:被 2 处调用(add_collaboration_tools, agent_jobs_tools_enabled);外部调用 2 个(exceeds_thread_spawn_depth_limit, next_thread_spawn_depth)。
agent_jobs_tools_enabled327–329 ↗
fn agent_jobs_tools_enabled(turn_context: &TurnContext) -> bool
作用:判断批量代理任务工具是否可用。这个工具可以从 CSV 一类表格里批量派出子代理。
数据流:进去的是当前轮上下文 → 它检查 SpawnCsv 功能开关,并确认协作工具本身可用 → 返回是否启用批量代理任务。
调用关系:add_collaboration_tools 用它决定是否添加 SpawnAgentsOnCsvHandler;agent_jobs_worker_tools_enabled 还会在它基础上继续判断当前是不是任务工人。
调用图:调用 1 个内部函数(collab_tools_enabled);被 2 处调用(add_collaboration_tools, agent_jobs_worker_tools_enabled)。
agent_jobs_worker_tools_enabled331–338 ↗
fn agent_jobs_worker_tools_enabled(turn_context: &TurnContext) -> bool
作用:判断当前会话是不是批量代理任务里的“工人代理”,从而是否需要上报任务结果的工具。
数据流:进去的是当前轮上下文 → 它先确认批量代理任务可用,再检查会话来源是否是标签以 agent_job: 开头的子代理 → 返回是否添加工人专用工具。
调用关系:add_collaboration_tools 在添加批量代理工具时调用它,只有真正的工人代理才会得到 ReportAgentJobResultHandler。
调用图:调用 1 个内部函数(agent_jobs_tools_enabled);被 1 处调用(add_collaboration_tools);外部调用 1 个(matches!)。
image_generation_tool_enabled340–346 ↗
fn image_generation_tool_enabled(turn_context: &TurnContext) -> bool
作用:判断是否应该启用图片生成工具。它要求运行环境支持,并且图片生成功能开关打开。
数据流:进去的是当前轮上下文 → 它先调用 image_generation_runtime_enabled 确认基础条件,再检查 ImageGeneration 功能开关 → 返回是否可用。
调用关系:hosted_model_tool_specs 用它决定是否添加服务端托管的图片生成工具。
调用图:调用 1 个内部函数(image_generation_runtime_enabled);被 1 处调用(hosted_model_tool_specs)。
image_generation_runtime_enabled348–358 ↗
fn image_generation_runtime_enabled(turn_context: &TurnContext) -> bool
作用:判断图片生成在当前账号、服务商和模型能力上是否真的跑得起来。
数据流:进去的是当前轮上下文 → 它检查登录是否使用 Codex 后端、服务商是否支持图片生成、模型输入能力是否包含图片 → 返回运行时是否满足条件。
调用关系:image_generation_tool_enabled 和 standalone_image_generation_model_visible 都用它作为基础门槛,避免把不能用的图片工具展示出去。
调用图:被 2 处调用(image_generation_tool_enabled, standalone_image_generation_model_visible)。
standalone_image_generation_model_visible360–370 ↗
fn standalone_image_generation_model_visible(turn_context: &TurnContext) -> bool
作用:判断独立插件版图片生成工具是否可以给模型看。这里说的独立版,是 image_gen.imagegen 这种扩展工具。
数据流:进去的是当前轮上下文 → 它先确认图片生成运行条件和命名空间工具都可用,再根据 Responses Lite 或 ImageGenExt 功能开关决定 → 返回是否可见。
调用关系:append_extension_tool_executors 用它过滤图片生成插件;standalone_image_generation_available 用它进一步判断插件是否实际存在。
调用图:调用 2 个内部函数(image_generation_runtime_enabled, namespace_tools_enabled);被 2 处调用(append_extension_tool_executors, standalone_image_generation_available)。
standalone_image_generation_available372–380 ↗
fn standalone_image_generation_available(
turn_context: &TurnContext,
extension_tools: &[Arc<dyn ToolExecutor<ExtensionToolCall>>],
) -> bool
作用:判断独立图片生成插件是否既允许展示、又真的在插件执行器列表里存在。
数据流:进去的是当前上下文和扩展工具执行器列表 → 它先检查模型可见条件,再查找名字为 image_gen.imagegen 的执行器 → 返回是否可用。
调用关系:hosted_model_tool_specs 用它避免重复添加托管图片生成工具:如果独立插件已经可用,就不再加托管版本。
调用图:调用 1 个内部函数(standalone_image_generation_model_visible);被 1 处调用(hosted_model_tool_specs)。
wait_agent_timeout_options382–396 ↗
fn wait_agent_timeout_options(turn_context: &TurnContext) -> WaitAgentTimeoutOptions
作用:给“等待子代理完成”的工具准备超时设置。超时就是最多等多久,防止一直卡住。
数据流:进去的是当前轮上下文 → 它判断是否是多代理 V2;是就读取配置里的默认、最小、最大等待时间,否则使用旧版常量 → 输出 WaitAgentTimeoutOptions。
调用关系:build_tool_specs_and_registry 创建规划上下文时调用它,后面 add_collaboration_tools 会把这套时间限制交给等待代理的处理器。
调用图:调用 1 个内部函数(multi_agent_v2_enabled);被 1 处调用(build_tool_specs_and_registry)。
agent_type_description398–409 ↗
fn agent_type_description(
turn_context: &TurnContext,
default_agent_type_description: &str,
) -> String
作用:生成“可以派哪类子代理”的说明文字。模型需要这段说明来知道该选什么代理角色。
数据流:进去的是当前上下文和默认说明 → 它根据配置里的 agent_roles 生成角色描述;如果生成结果为空,就退回默认说明 → 输出最终说明字符串。
调用关系:add_collaboration_tools 在创建派生代理工具时调用它,V1 和 V2 的 spawn agent 工具都会用到。
调用图:被 1 处调用(add_collaboration_tools);外部调用 1 个(build)。
is_excluded_from_code_mode423–431 ↗
fn is_excluded_from_code_mode(turn_context: &TurnContext, tool_name: &ToolName) -> bool
作用:判断某个工具是否被配置排除在 Code Mode 之外。配置可以按命名空间禁止某类工具被代码模式包装。
数据流:进去的是上下文和工具名 → 它查看工具是否有命名空间,并检查该命名空间是否在 code_mode.excluded_tool_namespaces 里 → 返回是否排除。
调用关系:build_code_mode_executors 和 spec_for_model_request 都调用它,分别用于决定包装哪些工具、以及是否增强工具说明。
调用图:被 2 处调用(build_code_mode_executors, spec_for_model_request)。
build_code_mode_executors433–493 ↗
fn build_code_mode_executors(
turn_context: &TurnContext,
executors: &[Arc<dyn CoreToolRuntime>],
) -> Vec<Arc<dyn CoreToolRuntime>>
作用:在 Code Mode 或 CodeModeOnly 下,生成代码模式专用的执行工具和等待工具。它像把多个小工具装进一个“万能工作台”。
数据流:进去的是当前上下文和已有工具执行器 → 它筛掉隐藏、只给模型直连、被配置排除的工具,收集可嵌套工具说明,整理命名空间描述并排序 → 输出 CodeModeExecuteHandler 和 CodeModeWaitHandler,或空列表。
调用关系:prepend_code_mode_executors 调用它,并把生成的 Code Mode 工具插到工具列表最前面。它还会根据 search_tool_enabled 判断是否提示有延迟工具可搜索。
调用图:调用 3 个内部函数(code_mode_namespace_descriptions, is_excluded_from_code_mode, search_tool_enabled);被 1 处调用(prepend_code_mode_executors);外部调用 5 个(new, collect_code_mode_exec_prompt_tool_definitions, matches!, once, vec!)。
merge_into_namespaces495–539 ↗
fn merge_into_namespaces(specs: Vec<ToolSpec>) -> Vec<ToolSpec>
作用:把同名命名空间的工具说明合并到一起。这样模型看到的是一个整齐的文件夹,而不是多个同名文件夹。
数据流:进去的是一串 ToolSpec → 它按命名空间名合并工具,补齐空描述,给命名空间里的函数按名字排序 → 输出整理后的 ToolSpec 列表。
调用关系:build_model_visible_specs_and_registry 在最终发给模型前调用它,避免插件、MCP、内置工具各自生成的同名命名空间分散开。
调用图:被 1 处调用(build_model_visible_specs_and_registry);外部调用 5 个(new, with_capacity, default_namespace_description, Namespace, unreachable!)。
code_mode_namespace_descriptions541–561 ↗
fn code_mode_namespace_descriptions(
specs: &[ToolSpec],
) -> BTreeMap<String, codex_code_mode::ToolNamespaceDescription>
作用:从工具说明里提取 Code Mode 需要的命名空间描述。这样代码模式知道每类工具的名字和用途。
数据流:进去的是工具说明列表 → 它只看命名空间类型的说明,收集每个命名空间的名称和描述,并用非空描述补空 → 输出按名字排列的描述表。
调用关系:build_code_mode_executors 调用它,然后用这些描述来生成 Code Mode 工具说明和排序规则。
调用图:被 1 处调用(build_code_mode_executors);外部调用 1 个(new)。
add_tool_sources564–575 ↗
fn add_tool_sources(context: &CoreToolPlanContext<'_>, planned_tools: &mut PlannedTools)
作用:把所有可能的工具来源统一收集起来。它是“到各个仓库取货”的总调度。
数据流:进去的是规划上下文和 PlannedTools → 它依次添加 shell、MCP 资源、核心工具、协作工具、MCP 运行工具、插件工具、动态工具和托管工具 → PlannedTools 被填满。
调用关系:build_tool_specs_and_registry 首先调用它来打底。后续的工具搜索和 Code Mode 都建立在它收集好的工具池之上。
调用图:调用 9 个内部函数(add_hosted_spec, add_collaboration_tools, add_core_utility_tools, add_dynamic_tools, add_extension_tools, add_mcp_resource_tools, add_mcp_runtime_tools, add_shell_tools, hosted_model_tool_specs);被 1 处调用(build_tool_specs_and_registry)。
standalone_web_search_enabled577–584 ↗
fn standalone_web_search_enabled(turn_context: &TurnContext) -> bool
作用:判断独立插件版网页搜索是否可以使用。独立版通常是 web.run 这种命名空间工具。
数据流:进去的是当前轮上下文 → 它要求支持命名空间工具,并且 Responses Lite 或 StandaloneWebSearch 功能开关启用 → 返回是否允许。
调用关系:hosted_model_tool_specs 用它决定是否还需要托管搜索;append_extension_tool_executors 用它决定 web.run 插件是否能登记。
调用图:调用 1 个内部函数(namespace_tools_enabled);被 2 处调用(append_extension_tool_executors, hosted_model_tool_specs)。
add_shell_tools586–624 ↗
fn add_shell_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut PlannedTools)
作用:根据当前环境和模型能力添加命令行相关工具。命令行工具让模型能执行 shell 命令、写标准输入等。
数据流:进去的是规划上下文和 PlannedTools → 它检查是否有可用环境,再读取权限、功能开关和 shell 类型 → 添加统一执行工具、旧 shell 工具或什么都不加。
调用关系:add_tool_sources 会调用它。若使用 UnifiedExec,它还会登记 WriteStdinHandler,并用 add_dispatch_only 保留旧 shell 工具作兼容。
调用图:调用 5 个内部函数(new, new, add, add_dispatch_only, unified_exec_should_include_shell_parameter);被 1 处调用(add_tool_sources);外部调用 3 个(shell_command_backend_for_features, shell_type_for_model_and_features, matches!)。
unified_exec_should_include_shell_parameter626–635 ↗
fn unified_exec_should_include_shell_parameter(turn_context: &TurnContext) -> bool
作用:判断新版统一执行工具的参数里是否需要显式带上 shell 类型。某些本地 zsh fork 模式下可以省略,但远程环境不能随便省。
数据流:进去的是当前轮上下文 → 它检查 unified_exec_shell_mode 是否是 ZshFork,以及当前是否有远程环境 → 返回是否应包含 shell 参数。
调用关系:add_shell_tools 创建 ExecCommandHandler 时调用它,用来决定工具说明和调用参数长什么样。
调用图:被 1 处调用(add_shell_tools);外部调用 1 个(matches!)。
add_mcp_resource_tools637–643 ↗
fn add_mcp_resource_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut PlannedTools)
作用:如果本轮有 MCP 工具,就添加浏览和读取 MCP 资源的辅助工具。MCP 可以理解成外部系统提供给模型的一组资源和工具接口。
数据流:进去的是规划上下文和 PlannedTools → 它检查 mcp_tools 是否存在 → 如果存在,就加入列资源、列资源模板、读资源三个工具。
调用关系:add_tool_sources 调用它。它只添加资源类通用工具,真正每个 MCP 工具的执行处理器由 add_mcp_runtime_tools 添加。
调用图:调用 1 个内部函数(add);被 1 处调用(add_tool_sources)。
add_core_utility_tools645–710 ↗
fn add_core_utility_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut PlannedTools)
作用:添加项目内置的通用小工具,比如计划工具、请求用户输入、请求权限、查看剩余上下文、睡眠、补丁应用、看图片等。
数据流:进去的是规划上下文和 PlannedTools → 它根据功能开关、模型能力和环境是否存在逐项判断 → 把符合条件的核心工具加入工具池,有些还会指定特殊曝光方式。
调用关系:add_tool_sources 调用它。它会用 tool_suggest_enabled 判断插件建议工具,用 can_request_original_image_detail 配置看图工具,并通过 add 或 add_with_exposure 登记工具。
调用图:调用 7 个内部函数(new, new, new, new, add, add_with_exposure, tool_suggest_enabled);被 1 处调用(add_tool_sources);外部调用 4 个(can_request_original_image_detail, collect_request_plugin_install_entries, request_user_input_available_modes, matches!)。
add_collaboration_tools712–798 ↗
fn add_collaboration_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut PlannedTools)
作用:添加多代理协作工具。也就是让模型可以创建、等待、打断、继续或关闭子代理,还可以处理批量代理任务。
数据流:进去的是规划上下文和 PlannedTools → 它先判断协作是否启用,再按 V1/V2 选择不同处理器和曝光方式,生成代理类型说明和等待超时配置 → 最后把相关代理工具加入工具池。
调用关系:add_tool_sources 调用它。它会调用 collab_tools_enabled、multi_agent_v2_enabled、agent_type_description、multi_agent_v2_handler 等函数,并在批量任务开启时添加 CSV 派生和结果上报工具。
调用图:调用 12 个内部函数(override_tool_exposure, add, add_arc, add_with_exposure, agent_jobs_tools_enabled, agent_jobs_worker_tools_enabled, agent_type_description, collab_tools_enabled, multi_agent_v2_enabled, multi_agent_v2_handler (+2 more));被 1 处调用(add_tool_sources);外部调用 4 个(new, new, new, new)。
add_mcp_runtime_tools800–824 ↗
fn add_mcp_runtime_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut PlannedTools)
作用:把 MCP 提供的每个具体工具变成本地可执行的处理器。这样模型调用 MCP 工具时,本地路由器知道怎么转发。
数据流:进去的是规划上下文和 PlannedTools → 它遍历立即可用和延迟可用的 MCP 工具,尝试创建 McpHandler → 成功就加入工具池,失败就记录警告并跳过。
调用关系:add_tool_sources 调用它。普通 MCP 工具用 add 登记,延迟 MCP 工具用 add_with_exposure 标成 Deferred,后面可能被工具搜索发现。
调用图:调用 3 个内部函数(new, add, add_with_exposure);被 1 处调用(add_tool_sources);外部调用 1 个(warn!)。
add_dynamic_tools826–856 ↗
fn add_dynamic_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut PlannedTools)
作用:添加运行时动态传进来的工具。动态工具不是固定写死在代码里的,而是本轮对话临时提供的。
数据流:进去的是规划上下文和 PlannedTools → 它遍历动态工具规格;普通函数工具直接转换,命名空间里的函数工具按所属命名空间转换 → 成功加入工具池,失败记录错误并跳过。
调用关系:add_tool_sources 调用它。它用 DynamicToolHandler 把协议层的动态工具说明变成核心工具运行时。
调用图:调用 3 个内部函数(new, new_in_namespace, add);被 1 处调用(add_tool_sources);外部调用 1 个(error!)。
add_extension_tools858–866 ↗
fn add_extension_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut PlannedTools)
作用:添加插件或扩展贡献的工具。这里不负责发现插件,只负责把已经准备好的插件执行器接入核心工具系统。
数据流:进去的是规划上下文和 PlannedTools → 它把当前上下文、插件执行器列表和工具池交给 append_extension_tool_executors → PlannedTools 可能新增多个扩展工具。
调用关系:add_tool_sources 调用它。真正的过滤、去重和适配工作都在 append_extension_tool_executors 里完成。
调用图:调用 1 个内部函数(append_extension_tool_executors);被 1 处调用(add_tool_sources)。
append_tool_search_executor869–890 ↗
fn append_tool_search_executor(
context: &CoreToolPlanContext<'_>,
planned_tools: &mut PlannedTools,
)
作用:在条件合适时添加“工具搜索”工具。它让模型先搜索延迟工具,而不是把所有工具一次性塞进提示里。
数据流:进去的是规划上下文和 PlannedTools → 它确认模型支持搜索且支持命名空间,再从已登记工具里收集 Deferred 工具的搜索信息 → 如果有可搜工具,就从缓存取或新建搜索处理器并加入工具池。
调用关系:build_tool_specs_and_registry 在收集完工具来源后调用它。它依赖 PlannedTools::runtimes 查看哪些工具是延迟曝光的。
调用图:调用 4 个内部函数(add_arc, runtimes, namespace_tools_enabled, search_tool_enabled);被 1 处调用(build_tool_specs_and_registry)。
prepend_code_mode_executors892–899 ↗
fn prepend_code_mode_executors(
context: &CoreToolPlanContext<'_>,
planned_tools: &mut PlannedTools,
)
作用:把 Code Mode 专用工具插到工具列表最前面。这样后续去重和展示时,代码模式入口优先存在。
数据流:进去的是规划上下文和 PlannedTools → 它读取当前已登记的工具,调用 build_code_mode_executors 生成代码模式工具 → 把这些工具插入 runtimes 开头。
调用关系:build_tool_specs_and_registry 在工具搜索之后调用它。它是 Code Mode 包装流程的最后插入点。
调用图:调用 2 个内部函数(runtimes, build_code_mode_executors);被 1 处调用(build_tool_specs_and_registry)。
append_extension_tool_executors901–953 ↗
fn append_extension_tool_executors(
turn_context: &TurnContext,
executors: &[Arc<dyn ToolExecutor<ExtensionToolCall>>],
planned_tools: &mut PlannedTools,
)
作用:把外部扩展工具执行器安全地接入 PlannedTools。它会过滤掉当前不该出现的扩展工具,并防止重名覆盖内置工具。
数据流:进去的是上下文、扩展执行器列表和 PlannedTools → 它先收集已占用工具名,再按网页搜索、图片生成、Code Mode、工具搜索等规则过滤 → 合格的执行器被包装成 ExtensionToolAdapter 加入工具池。
调用关系:add_extension_tools 调用它。它会参考 search_tool_enabled、namespace_tools_enabled、standalone_web_search_enabled 和 standalone_image_generation_model_visible,避免插件和内置/托管工具打架。
调用图:调用 9 个内部函数(new, add, runtimes, namespace_tools_enabled, search_tool_enabled, standalone_image_generation_model_visible, standalone_web_search_enabled, namespaced, plain);被 1 处调用(add_extension_tools);外部调用 2 个(matches!, warn!)。
multi_agent_v2_handler955–966 ↗
fn multi_agent_v2_handler(
handler: impl CoreToolRuntime + 'static,
namespace: Option<&str>,
) -> Arc<dyn CoreToolRuntime>
作用:给多代理 V2 工具按需套上自定义命名空间。命名空间就像把一组代理工具放进指定文件夹里。
数据流:进去的是一个代理工具处理器和可选命名空间 → 如果有命名空间,就创建 MultiAgentV2NamespaceOverride 包装它;如果没有,就直接返回原处理器的共享对象。
调用关系:add_collaboration_tools 创建 V2 代理工具时调用它。后续这些包装对象会被 add_arc 加入工具池。
调用图:被 1 处调用(add_collaboration_tools);外部调用 1 个(new)。
MultiAgentV2NamespaceOverride::tool_name974–976 ↗
fn tool_name(&self) -> ToolName
作用:返回包装后的工具名,把原工具名放到配置指定的命名空间下面。
数据流:进去的是包装器自身 → 它读取保存的 namespace 和内部处理器的原工具短名 → 输出一个带命名空间的新 ToolName。
调用关系:工具注册、去重和模型说明生成时会调用它。它让 V2 多代理工具在外面看起来属于新的工具文件夹。
调用图:调用 1 个内部函数(namespaced)。
MultiAgentV2NamespaceOverride::spec978–987 ↗
fn spec(&self) -> ToolSpec
作用:返回包装后的工具说明。如果内部工具原本是普通函数,它会把它包装成命名空间工具说明。
数据流:进去的是包装器自身 → 它拿到内部处理器的 spec;若是函数工具,就放进指定 namespace,并加上多代理 V2 的命名空间描述 → 输出新的 ToolSpec。
调用关系:build_model_visible_specs_and_registry 生成模型可见工具清单时会用到它。它保证工具名和工具说明的命名空间是一致的。
调用图:外部调用 2 个(Namespace, vec!)。
MultiAgentV2NamespaceOverride::exposure989–991 ↗
fn exposure(&self) -> ToolExposure
作用:返回内部工具原本的曝光方式。包装命名空间不改变工具该不该给模型看。
数据流:进去的是包装器自身 → 它询问内部处理器 exposure → 原样返回。
调用关系:工具规划阶段会用它判断工具是直接展示、延迟展示还是隐藏。这个方法让包装器对外表现得像原工具一样。
MultiAgentV2NamespaceOverride::supports_parallel_tool_calls993–995 ↗
fn supports_parallel_tool_calls(&self) -> bool
作用:说明这个包装后的工具是否支持并行调用。它完全沿用内部工具的能力。
数据流:进去的是包装器自身 → 它调用内部处理器的 supports_parallel_tool_calls → 返回同样的布尔结果。
调用关系:工具执行系统在判断能否同时跑多个工具调用时会用它。包装层不擅自改变并行能力。
MultiAgentV2NamespaceOverride::search_info997–999 ↗
fn search_info(&self) -> Option<ToolSearchInfo>
作用:返回内部工具的搜索信息。搜索信息用于工具搜索功能知道这个工具怎么被发现。
数据流:进去的是包装器自身 → 它向内部处理器要 search_info → 原样返回可能存在的搜索信息。
调用关系:append_tool_search_executor 收集延迟工具搜索信息时可能会用到它。命名空间包装不会丢掉搜索能力。
MultiAgentV2NamespaceOverride::handle1001–1003 ↗
fn handle(&self, invocation: ToolInvocation) -> codex_tools::ToolExecutorFuture<'_>
作用:真正执行工具调用时,把请求直接转交给内部处理器。包装器只改外观,不改实际干活的人。
数据流:进去的是 ToolInvocation(一次工具调用)→ 它把调用交给内部 handler.handle → 输出内部处理器返回的异步执行结果。
调用关系:ToolRouter 找到这个包装工具后会通过它执行。它是命名空间外壳通往真实多代理处理器的转接线。
MultiAgentV2NamespaceOverride::matches_kind1007–1009 ↗
fn matches_kind(&self, payload: &crate::tools::context::ToolPayload) -> bool
作用:判断某个工具调用载荷是否属于这个工具能处理的类型。它沿用内部处理器的判断。
数据流:进去的是工具调用载荷 → 它转交给内部处理器 matches_kind → 返回是否匹配。
调用关系:ToolRegistry 做路由匹配时会用它。包装层不会改变内部工具识别调用类型的规则。
MultiAgentV2NamespaceOverride::create_diff_consumer1011–1015 ↗
fn create_diff_consumer(
&self,
) -> Option<Box<dyn crate::tools::registry::ToolArgumentDiffConsumer>>
作用:如果内部工具支持逐步接收参数变化,这里把对应的接收器交出来。可以把它理解成“边输入边看差异”的辅助接口。
数据流:进去的是包装器自身 → 它调用内部处理器 create_diff_consumer → 返回可能存在的差异消费者对象。
调用关系:工具注册和执行系统需要参数流式处理时会用它。包装器只转发,不自己实现差异处理。
compare_code_mode_tools1018–1030 ↗
fn compare_code_mode_tools(
left: &codex_code_mode::ToolDefinition,
right: &codex_code_mode::ToolDefinition,
namespace_descriptions: &BTreeMap<String, codex_code_mode::ToolNamespaceDescrip
作用:给 Code Mode 里的工具定义排序。排序稳定后,模型看到的工具列表更整齐,也更容易复现。
数据流:进去的是左右两个工具定义和命名空间描述表 → 它先比较命名空间名,再比较原工具名,最后比较 Code Mode 内部名称 → 输出排序结果。
调用关系:build_code_mode_executors 收集完 Code Mode 可用工具后会用它排序。它依赖 code_mode_namespace_name 找到工具所属命名空间的显示名。
调用图:调用 1 个内部函数(code_mode_namespace_name)。
code_mode_namespace_name1032–1041 ↗
fn code_mode_namespace_name(
tool: &codex_code_mode::ToolDefinition,
namespace_descriptions: &'a BTreeMap<String, codex_code_mode::ToolNamespaceDescription>,
) -> Option<&'a str>
作用:查出某个 Code Mode 工具所属命名空间的显示名字。没有命名空间或找不到描述时,就返回空。
数据流:进去的是一个 Code Mode 工具定义和命名空间描述表 → 它读取工具名里的 namespace,再到描述表里查对应项 → 输出命名空间名字或 None。
调用关系:compare_code_mode_tools 调用它来决定排序时先按哪个命名空间比较。
调用图:被 1 处调用(compare_code_mode_tools)。
core/src/tools/hosted_spec.rs源码 ↗
这个文件像是在给模型准备“工具菜单”。图片生成比较简单:告诉系统输出图片要用什么格式,然后生成一份工具说明。网页搜索更讲究,因为它涉及能不能访问外网、只搜文字还是也搜图片、要不要带搜索过滤条件、用户位置、搜索上下文大小等设置。这里会先看网页搜索模式:缓存搜索表示不直接上外网,实时搜索表示可以访问外网,禁用或没配置就不创建搜索工具。这样做很重要,因为如果配置说不能联网,系统就不该偷偷给模型联网工具。最后,这些选项会被整理成统一的 ToolSpec,也就是“工具规格说明”,后续流程可以直接拿去交给托管模型。
create_image_generation_tool14–18 ↗
fn create_image_generation_tool(output_format: &str) -> ToolSpec
作用:这个函数创建一个“图片生成工具”的说明,告诉系统生成出来的图片应该是什么格式。有人在组装模型可用工具列表时会用它。
数据流:进去的是一个输出格式字符串,比如某种图片格式名称;函数把它复制成自己的字符串,塞进 ToolSpec::ImageGeneration 这个工具说明里;出来的是一份完整的图片生成工具规格,不会改动外部数据。
调用关系:它被 hosted_model_tool_specs 调用。也就是说,当系统在给托管模型整理“可用工具清单”时,如果需要图片生成能力,就会来这里生成对应的工具说明;这个函数本身不再把活交给别的项目函数,只做简单包装。
调用图:被 1 处调用(hosted_model_tool_specs)。
create_web_search_tool20–50 ↗
fn create_web_search_tool(options: WebSearchToolOptions<'_>) -> Option<ToolSpec>
作用:这个函数根据网页搜索配置,决定是否创建“网页搜索工具”的说明,以及这个工具能不能访问外网、能搜哪些内容、带哪些限制。它能防止禁用搜索时还把搜索工具交给模型。
数据流:进去的是 WebSearchToolOptions,里面有搜索模式、可选的搜索配置、以及搜索工具类型。函数先把搜索模式翻译成“是否允许外网访问”:缓存模式是不允许,实时模式是允许,禁用或没填就直接返回 None,表示不创建工具。接着它根据工具类型决定只搜文字,还是文字和图片都搜。最后它从配置里取出过滤条件、用户位置、搜索上下文大小等信息,组装成 ToolSpec::WebSearch 返回;它只读取配置,不修改配置。
调用关系:它被 hosted_model_tool_specs 调用,位置是在系统为托管模型准备工具清单的时候。调用者把当前配置交给它,它负责判断网页搜索工具到底该不该出现在清单里;如果该出现,它返回工具说明,如果不该出现,就返回 None,让上层不要加入这个工具。
调用图:被 1 处调用(hosted_model_tool_specs)。
core/src/tools/handlers/dynamic.rs源码 ↗
可以把这个文件想成一个“临时工具的前台接待”。模型说“我要调用某个工具,并给这些参数”,这里先检查这是不是普通函数式工具调用,再把参数从字符串解析成 JSON(一种常见的数据格式,像有层级的表格)。然后它在当前会话里登记一个“等回复的位置”,发出一个动态工具调用事件,让外部真正去执行工具。等外部把结果送回来后,它再把结果整理成模型需要的格式。这里还会记录调用开始和结束时间,并发送“请求”和“响应”两个事件,方便界面、日志或上层系统知道发生了什么。如果调用被取消或没有回复,它会明确告诉模型失败了,而不是假装成功。
DynamicToolHandler::new40–42 ↗
fn new(tool: &DynamicToolFunctionSpec) -> Option<Self>
作用:用一个没有命名空间的动态工具说明,创建一个可执行的工具处理器。外部在把动态工具加入系统时会用它。
数据流:进去的是一个动态工具的说明,里面有工具名、参数规则、是否延迟加载等信息;它把这些交给通用的创建流程;出来的是一个 DynamicToolHandler,或者如果说明无法转换就返回空。
调用关系:add_dynamic_tools 在添加普通动态工具时调用它。它自己不做细活,而是把工作交给 DynamicToolHandler::from_parts,保证有命名空间和没命名空间的工具走同一套创建规则。
调用图:被 1 处调用(add_dynamic_tools);外部调用 1 个(from_parts)。
DynamicToolHandler::new_in_namespace44–49 ↗
fn new_in_namespace(
namespace: &DynamicToolNamespaceSpec,
tool: &DynamicToolFunctionSpec,
) -> Option<Self>
作用:用一个命名空间和命名空间里的工具说明,创建一个动态工具处理器。命名空间可以理解成“工具分组”或“工具箱名字”。
数据流:进去的是工具箱说明和工具说明;它把两者一起交给通用创建流程;出来的是带完整工具名和工具规格的处理器,或者在转换失败时返回空。
调用关系:add_dynamic_tools 在添加带命名空间的动态工具时调用它。它和 DynamicToolHandler::new 一样,核心都交给 DynamicToolHandler::from_parts,避免两套逻辑不一致。
调用图:被 1 处调用(add_dynamic_tools);外部调用 1 个(from_parts)。
DynamicToolHandler::from_parts51–81 ↗
fn from_parts(
tool: &DynamicToolFunctionSpec,
namespace: Option<&DynamicToolNamespaceSpec>,
) -> Option<Self>
作用:这是创建动态工具处理器的核心工厂。它把协议里的动态工具描述,翻译成系统内部注册工具时需要的名字、规格和可见方式。
数据流:进去的是工具说明,以及可选的命名空间说明;它先拼出工具名,再把动态工具转换成 Responses API 风格的工具定义,如果有命名空间就包进一个工具箱规格里;出来的是完整的 DynamicToolHandler。如果转换失败,就返回空,表示这个工具不能注册。
调用关系:DynamicToolHandler::new 和 DynamicToolHandler::new_in_namespace 都依赖它。它会调用 dynamic_tool_to_responses_api_tool 做格式转换,也会在命名空间描述为空时用 default_namespace_description 补一个默认说明。
调用图:调用 1 个内部函数(new);外部调用 5 个(default_namespace_description, dynamic_tool_to_responses_api_tool, Function, Namespace, vec!)。
DynamicToolHandler::tool_name85–87 ↗
fn tool_name(&self) -> ToolName
作用:返回这个处理器代表的工具名字。系统用它来知道模型调用的是哪一个工具。
数据流:进去的是处理器本身;它复制一份内部保存的工具名;出来的是这份工具名,不改动原来的处理器。
调用关系:这是 ToolExecutor 接口的一部分。工具注册表或调度器需要识别工具时会调用它。
调用图:外部调用 1 个(clone)。
DynamicToolHandler::spec89–91 ↗
fn spec(&self) -> ToolSpec
作用:返回这个工具的完整说明,比如名字、描述、参数格式等。模型和工具搜索功能需要靠它知道工具怎么用。
数据流:进去的是处理器本身;它复制一份内部保存的工具规格;出来的是这份规格,不改动内部数据。
调用关系:这是 ToolExecutor 接口的一部分。DynamicToolHandler::search_info 会调用它,再把工具规格转换成可搜索的信息。
调用图:被 1 处调用(search_info);外部调用 1 个(clone)。
DynamicToolHandler::exposure93–95 ↗
fn exposure(&self) -> ToolExposure
作用:告诉系统这个工具是直接展示给模型,还是先延迟加载。这样可以控制工具是否一开始就出现在模型可用列表里。
数据流:进去的是处理器本身;它读取内部的 exposure 标记;出来的是 Direct 或 Deferred 这类可见性设置,不产生其他改动。
调用关系:这是 ToolExecutor 接口的一部分。工具注册和展示阶段会用它决定什么时候把工具暴露给模型。
DynamicToolHandler::search_info97–105 ↗
fn search_info(&self) -> Option<ToolSearchInfo>
作用:为这个动态工具生成搜索用的信息。这样系统在需要找工具时,能把“当前对话提供的动态工具”也纳入候选。
数据流:进去的是处理器本身;它先取出工具规格,再加上一段来源说明,说明这些工具来自当前 Codex 线程;出来的是可选的搜索信息,如果规格无法生成搜索信息就可能为空。
调用关系:这是 ToolExecutor 接口的一部分。它调用 DynamicToolHandler::spec 取得工具说明,再交给 ToolSearchInfo::from_tool_spec 转成搜索索引能用的形式。
调用图:调用 2 个内部函数(spec, from_tool_spec)。
DynamicToolHandler::handle107–109 ↗
fn handle(&self, invocation: ToolInvocation) -> codex_tools::ToolExecutorFuture<'_>
作用:这是工具真正被调用时的入口。它把一次工具调用包装成异步任务,让系统可以一边等待外部工具回复,一边继续处理别的事。
数据流:进去的是一次 ToolInvocation,里面有会话、当前轮次、调用编号和参数;它把实际处理交给 handle_call,并把结果包装成可等待的异步任务;出来的是一个未来会完成的工具执行结果。
调用关系:这是 ToolExecutor 接口规定的调用入口。调度器调用它后,它立即转交给 DynamicToolHandler::handle_call 做实际解析、发请求和收回复。
调用图:调用 1 个内部函数(handle_call);外部调用 1 个(pin)。
DynamicToolHandler::handle_call113–161 ↗
async fn handle_call(
&self,
invocation: ToolInvocation,
) -> Result<Box<dyn crate::tools::context::ToolOutput>, FunctionCallError>
作用:执行一次动态工具调用的主流程。它负责检查输入、解析参数、向外部请求执行工具,并把返回结果整理成模型能接收的格式。
数据流:进去的是一次工具调用;它取出会话、当前轮次、调用编号和负载,确认负载是函数参数,再用 parse_arguments 把参数解析成 JSON;随后调用 request_dynamic_tool 等外部回复;出来的是工具输出。如果负载不支持、参数坏了,或等待期间被取消,就返回一个会反馈给模型的错误。
调用关系:DynamicToolHandler::handle 会调用它。它是处理器内部最关键的一步:前半段整理模型给的输入,核心执行交给 request_dynamic_tool,最后用 FunctionToolOutput::from_content 和 boxed_tool_output 把外部结果包成统一的工具输出。
调用图:调用 4 个内部函数(from_content, boxed_tool_output, request_dynamic_tool, parse_arguments);被 1 处调用(handle);外部调用 2 个(clone, RespondToModel)。
request_dynamic_tool170–238 ↗
async fn request_dynamic_tool(
session: &Session,
turn_context: &TurnContext,
call_id: String,
tool_name: ToolName,
arguments: Value,
) -> Option<DynamicToolResponse>
作用:向当前会话外部发出动态工具调用请求,并等待对应的回复。它相当于在前台登记取餐号、通知后厨做菜、再等后厨把菜送回来。
数据流:进去的是会话、当前轮次、调用编号、工具名和参数;它先在当前活跃轮次里登记一个一次性回复通道,记录开始时间,然后发送 DynamicToolCallRequest 事件;等回复通道收到结果后,它再发送 DynamicToolCallResponse 事件,里面带成功结果或取消错误;出来的是外部返回的 DynamicToolResponse,若没收到则为空。
调用关系:DynamicToolHandler::handle_call 调用它来完成真正的外部交互。它依赖 session.send_event 把请求和响应事件送出去,也用 oneshot channel(一次性通道,只传一次消息)等待外部把结果传回当前调用。
调用图:调用 1 个内部函数(now_unix_timestamp_ms);被 1 处调用(handle_call);外部调用 8 个(now, clone, new, send_event, channel, DynamicToolCallRequest, DynamicToolCallResponse, warn!)。
tools/src/dynamic_tool.rs源码 ↗
系统里有些工具不是一开始写死的,而是运行时从外部传进来的,这类工具叫“动态工具”。这个文件就像一个翻译员:外部给的是 DynamicToolFunctionSpec,里面有工具名、说明、输入格式等信息;系统内部需要的是 ToolDefinition。parse_dynamic_tool 会把名字和说明原样带过去,把输入格式交给 parse_tool_input_schema 做检查和转换,如果格式不合法就返回错误。它还会保留 defer_loading,也就是“先登记、以后再加载”的标记。这里特意把 output_schema 设成 None,表示当前这个转换过程不处理输出格式。
parse_dynamic_tool5–15 ↗
fn parse_dynamic_tool(
tool: &DynamicToolFunctionSpec,
) -> Result<ToolDefinition, serde_json::Error>
作用:把一个外部传来的动态工具说明,变成系统内部使用的 ToolDefinition。别人需要把动态工具接入系统时,会调用它来完成格式转换和基本校验。
数据流:输入是一份 DynamicToolFunctionSpec,里面包含工具名、描述、输入规则和是否延迟加载等信息。函数复制工具名和描述,把输入规则交给 parse_tool_input_schema 转成内部能理解的格式;如果转换失败,就把 serde_json::Error 错误交出去。成功时输出一个 ToolDefinition,并把 output_schema 留空,同时保留 defer_loading 设置。
调用关系:它处在“外部工具说明进入系统”的入口转换位置。调用方把动态工具规格交给它,它自己不深挖输入规则的细节,而是把这部分工作交给 parse_tool_input_schema;等输入规则转换好后,它再组装出完整的内部工具定义。
调用图:外部调用 1 个(parse_tool_input_schema)。
tools/src/tool_search.rs源码 ↗
系统里有很多工具,每个工具都有名字、说明、参数等信息。光保存原始说明还不够,因为搜索时需要一段好匹配的文字,也需要一个“轻量版”的工具输出,避免一开始就加载太多东西。这个文件就做这件事:它从 ToolSpec(工具规格,描述一个工具或一组工具是什么)里提取搜索文字,把函数名、描述、参数说明都拼进去;同时把工具改成延迟加载,也就是先只放一个入口,真正细节以后再补。对于命名空间(一组工具的集合),它还会补默认描述,免得搜索时缺少关键信息。不能参与工具搜索的类型,比如图片生成、网页搜索等,会被跳过。
ToolSearchInfo::from_tool_spec22–28 ↗
fn from_tool_spec(
spec: ToolSpec,
source_info: Option<ToolSearchSourceInfo>,
) -> Option<Self>
作用:这个函数接收一份普通工具说明,自动生成一段默认搜索文字,然后把它变成搜索系统能用的 ToolSearchInfo。别人不用自己想该拿哪些字段做搜索词,直接交给它就行。
数据流:进去的是一个 ToolSpec 和可选的来源信息 source_info。它先调用 default_tool_search_text,从工具名字、描述、参数等内容里拼出搜索文本;然后把这段文本、原始工具说明和来源信息交给 ToolSearchInfo::from_spec。出来的是 Some(ToolSearchInfo),如果这种工具适合搜索;如果不适合,就会得到 None。
调用关系:它是最省心的入口之一。外部的 search_info 流程和测试 default_search_text_uses_model_visible_namespace_metadata_once 会用它来生成搜索信息。它自己不直接改工具细节,而是先找 default_tool_search_text 做“写标签”的事,再把转换工作交给 ToolSearchInfo::from_spec。
调用图:调用 1 个内部函数(default_tool_search_text);被 3 处调用(search_info, search_info, default_search_text_uses_model_visible_namespace_metadata_once);外部调用 1 个(from_spec)。
ToolSearchInfo::from_spec30–65 ↗
fn from_spec(
search_text: String,
spec: ToolSpec,
source_info: Option<ToolSearchSourceInfo>,
) -> Option<Self>
作用:这个函数把已经有搜索文字的工具说明,包装成真正的搜索条目。它还会把工具改成“先别完整加载”的形式,让搜索列表更轻、更安全。
数据流:进去的是 search_text、ToolSpec 和可选 source_info。它检查工具类型:如果是单个函数工具,就设置 defer_loading 为 true,并清掉 output_schema;如果是一组工具的命名空间,就先补空描述,再把里面每个函数工具也设成延迟加载并清掉输出结构。最后输出 Some(ToolSearchInfo)。如果输入是工具搜索、图片生成、网页搜索或自由格式工具,就不生成条目,输出 None。
调用关系:它是实际“装箱”的地方。ToolSearchInfo::from_tool_spec 会在自动生成搜索文字后调用它;外部的 search_info 和 multi_agent_tool_search_info 也会直接用它。它会用 default_namespace_description 给空描述的工具组补一句默认说明,并把结果包成 Function 或 Namespace 这种可加载工具规格。
调用图:被 2 处调用(search_info, multi_agent_tool_search_info);外部调用 3 个(default_namespace_description, Function, Namespace)。
default_tool_search_text68–98 ↗
fn default_tool_search_text(spec: &ToolSpec) -> String
作用:这个函数负责给一个工具自动写搜索用的文字。它把工具名、描述、参数说明等信息揉成一句长文本,方便之后被搜索命中。
数据流:进去的是一个 ToolSpec。它先建一个空的文字片段列表,然后按工具类型收集信息:函数工具会交给 append_function_search_text;命名空间会加入组名、组描述和每个函数的信息;工具搜索、图片生成、网页搜索、自由格式工具也各自加入能代表自己的关键词。最后把所有非空片段用空格连起来,输出一整段搜索文本。
调用关系:它是 ToolSearchInfo::from_tool_spec 的第一步,相当于先给工具贴可搜索标签。它会调用 append_function_search_text 处理函数工具的细节,也会调用 push_search_part 避免把空白内容塞进搜索文本。
调用图:调用 2 个内部函数(append_function_search_text, push_search_part);被 1 处调用(from_tool_spec);外部调用 1 个(new)。
append_function_search_text100–105 ↗
fn append_function_search_text(tool: &ResponsesApiTool, parts: &mut Vec<String>)
作用:这个函数专门收集“函数工具”的搜索关键词。它不只放原名字,还会把下划线改成空格,这样用户用自然语言搜索时更容易搜到。
数据流:进去的是一个 ResponsesApiTool 和一个正在累积文字片段的列表 parts。它把工具名、把下划线替换为空格后的工具名、工具描述依次加入列表;然后把参数结构交给 append_schema_search_text,继续收集参数里的说明。出来没有单独返回值,但 parts 会多出这些搜索片段。
调用关系:它被 default_tool_search_text 调用,用来处理单个函数工具或命名空间里的函数工具。它自己负责函数层面的文字,参数层面的更深内容则交给 append_schema_search_text;每次加入文字时都通过 push_search_part 过滤空内容。
调用图:调用 2 个内部函数(append_schema_search_text, push_search_part);被 1 处调用(default_tool_search_text)。
append_schema_search_text107–125 ↗
fn append_schema_search_text(schema: &JsonSchema, parts: &mut Vec<String>)
作用:这个函数从参数结构里挖出可搜索的文字。因为参数的名字和说明常常能解释工具能做什么,所以也要放进搜索标签里。
数据流:进去的是一个 JsonSchema(JSON 结构说明,用来描述参数长什么样)和文字片段列表 parts。它先加入当前结构的 description;如果有 properties,就把每个参数名加入,再递归处理这个参数自己的结构;如果有 items 或 any_of,也继续往下看。出来没有单独返回值,但 parts 会追加参数名、参数说明以及嵌套参数里的文字。
调用关系:它由 append_function_search_text 调用,负责深入参数内部找关键词。它像翻说明书目录一样,一层层往下翻;真正把文字放进列表时,仍交给 push_search_part 来做空白过滤。
调用图:调用 1 个内部函数(push_search_part);被 1 处调用(append_function_search_text)。
push_search_part127–132 ↗
fn push_search_part(parts: &mut Vec<String>, part: String)
作用:这个小函数负责把一段文字安全地加入搜索片段列表。它会先去掉前后空格,空字符串就不加,避免搜索文本里混进没用内容。
数据流:进去的是可修改的 parts 列表和一段 part 字符串。它先 trim,也就是去掉前后的空白;如果剩下内容不是空的,就转回字符串并加入 parts。它不返回新值,但会让 parts 在需要时多一个干净的片段。
调用关系:它是这个文件里最基础的清洁工。default_tool_search_text、append_function_search_text 和 append_schema_search_text 都会调用它,确保无论关键词来自工具名、描述还是参数结构,最后加入搜索文本前都先被简单清理。
调用图:被 3 处调用(append_function_search_text, append_schema_search_text, default_tool_search_text)。
core/src/tools/handlers/extension_tools.rs源码 ↗
扩展工具和核心工具本来用的是两套接口,就像两种插头不一样。这个文件里的 ExtensionToolAdapter 就是转接头:核心系统发来一次工具调用,它把会话历史、当前轮次、工作目录、沙箱权限(一组限制工具能访问哪些文件的规则)等信息整理成扩展能看懂的 ToolCall,再交给扩展执行。扩展在运行中可能想告诉用户“开始搜索了”“图片生成完成了”,CoreTurnItemEmitter 会把这些扩展消息转换成核心的 TurnItem,并通过 Session 发出去。完成消息还会经过 finalize_turn_item 做收尾,比如图片结果由核心保存到固定位置,避免扩展自己随便声明保存路径。文件后半部分是测试,用假扩展确认这些桥接、事件发布、权限和图片保存行为都没有跑偏。
ExtensionToolAdapter::new29–31 ↗
fn new(executor: Arc<dyn codex_tools::ToolExecutor<ExtensionToolCall>>) -> Self
作用:创建一个扩展工具的“转接器”。有人把扩展实现交进来后,核心系统就能用统一的工具接口来调用它。
数据流:输入是一份扩展工具执行器的共享引用 → 函数把它包进 ExtensionToolAdapter 里 → 输出一个核心可登记、可调用的工具对象,不额外改动别的状态。
调用关系:工具注册阶段 append_extension_tool_executors 会用它把扩展工具加入核心工具表;测试里也用它搭出假工具,检查后续调用和事件发布是否正常。
调用图:被 4 处调用(exposes_generic_hook_payloads, image_generation_publication_is_finalized_by_core, passes_turn_fields_and_scoped_turn_item_emitter_to_extension_call, append_extension_tool_executors)。
ExtensionToolAdapter::tool_name35–37 ↗
fn tool_name(&self) -> ToolName
作用:告诉核心系统这个扩展工具叫什么名字。名字用于匹配模型请求的工具调用。
数据流:输入是这个适配器自身 → 它直接询问里面包着的扩展执行器 → 返回扩展声明的工具名,不做修改。
调用关系:核心工具注册表或调用分发时会问它名字;它只是把问题转交给真正的扩展执行器。
ExtensionToolAdapter::spec39–41 ↗
fn spec(&self) -> ToolSpec
作用:拿到这个扩展工具的说明书。说明书包括工具描述、参数格式等,方便模型知道怎么调用。
数据流:输入是适配器自身 → 它向内部扩展执行器要 ToolSpec → 原样返回给核心系统。
调用关系:当核心要向模型暴露可用工具时会调用它;适配器不理解说明书内容,只负责传递。
ExtensionToolAdapter::exposure43–45 ↗
fn exposure(&self) -> crate::tools::registry::ToolExposure
作用:说明这个工具应该在什么场景下暴露给模型或系统使用。这样不是所有工具都会无条件出现。
数据流:输入是适配器自身 → 它读取内部扩展执行器的 exposure 设置 → 返回给核心工具框架。
调用关系:工具筛选和展示阶段会用到它;它保持扩展自己的暴露策略不变。
ExtensionToolAdapter::supports_parallel_tool_calls47–49 ↗
fn supports_parallel_tool_calls(&self) -> bool
作用:告诉核心这个扩展工具能不能并行调用。并行就是多个调用同时跑,适合无共享冲突的工具。
数据流:输入是适配器自身 → 它询问内部扩展执行器是否支持并行 → 返回一个 true 或 false。
调用关系:核心调度工具调用时会参考它,决定能不能把多个调用同时放出去;判断权来自扩展工具本身。
ExtensionToolAdapter::search_info51–53 ↗
fn search_info(&self) -> Option<ToolSearchInfo>
作用:如果这个扩展工具和搜索有关,就提供搜索展示需要的额外信息。没有相关信息时返回空。
数据流:输入是适配器自身 → 它向内部扩展执行器索取搜索信息 → 返回有或没有这份信息。
调用关系:核心在组织工具元数据或搜索类展示时会用到它;适配器只负责透传。
ExtensionToolAdapter::handle55–57 ↗
fn handle(&self, invocation: ToolInvocation) -> codex_tools::ToolExecutorFuture<'_>
作用:真正执行一次扩展工具调用。它把核心格式的调用翻译成扩展格式,然后交给扩展工具运行。
数据流:输入是一份 ToolInvocation,里面有会话、轮次、工具名、调用编号、参数等 → 它先调用 to_extension_call 补齐扩展需要的上下文 → 再调用内部扩展执行器的 handle → 输出扩展工具的结果或错误。
调用关系:核心工具框架在需要运行这个工具时会调用它;它把前半段翻译工作交给 to_extension_call,把真正业务交给扩展执行器。
调用图:调用 1 个内部函数(to_extension_call);外部调用 1 个(pin)。
ExtensionToolAdapter::matches_kind61–63 ↗
fn matches_kind(&self, payload: &ToolPayload) -> bool
作用:判断某个工具请求是不是这个适配器能处理的类型。这里它只接受普通函数式工具调用。
数据流:输入是工具载荷 ToolPayload → 它检查载荷是不是 Function 这一类 → 输出 true 或 false,不改变载荷。
调用关系:核心工具运行时在分发调用前会用它过滤;它避免把不合适的载荷交给扩展函数工具。
调用图:外部调用 1 个(matches!)。
extension_turn_item71–79 ↗
fn extension_turn_item(item: ExtensionTurnItem) -> TurnItem
作用:把扩展世界里的进度项转换成核心系统里的进度项。这样扩展发出的搜索或图片事件才能进入统一事件流。
数据流:输入是 ExtensionTurnItem → 如果是网页搜索,就转成核心 WebSearch;如果是图片生成,就先清掉扩展传来的 saved_path,再转成核心 ImageGeneration → 输出一个 TurnItem。
调用关系:CoreTurnItemEmitter::emit_started 和 CoreTurnItemEmitter::emit_completed 都会调用它;清掉图片路径是为了后面由核心统一决定文件保存位置。
调用图:被 2 处调用(emit_completed, emit_started);外部调用 2 个(ImageGeneration, WebSearch)。
CoreTurnItemEmitter::emit_started82–91 ↗
fn emit_started(&'a self, item: ExtensionTurnItem) -> TurnItemEmissionFuture<'a>
作用:发布扩展工具的“某个事项开始了”事件,比如开始网页搜索。它把扩展事件放进当前会话的事件流里。
数据流:输入是扩展发来的 ExtensionTurnItem → 它先尝试从弱引用找回 Session 和 TurnContext,找不到就安静退出 → 找到后转换成核心 TurnItem 并发送 started 事件。
调用关系:扩展工具运行时会通过 ToolCall 里的 turn_item_emitter 调它;它内部使用 extension_turn_item,并把发送工作交给 Session。
调用图:调用 1 个内部函数(extension_turn_item);外部调用 2 个(pin, upgrade)。
CoreTurnItemEmitter::emit_completed93–109 ↗
fn emit_completed(&'a self, item: ExtensionTurnItem) -> TurnItemEmissionFuture<'a>
作用:发布扩展工具的“某个事项完成了”事件,并让核心做必要收尾。比如图片生成完成后,核心会保存图片并填好最终路径。
数据流:输入是扩展发来的完成项 → 它找回当前会话和轮次,转换成核心 TurnItem → 调用 finalize_turn_item 让贡献器和核心收尾逻辑补充内容 → 最后发送 completed 事件。
调用关系:扩展工具完成搜索、图片等项目时会调它;它连接 extension_turn_item、finalize_turn_item 和 Session 的事件发布,是扩展事件进入核心的关键出口。
调用图:调用 2 个内部函数(finalize_turn_item, extension_turn_item);外部调用 3 个(pin, upgrade, Run)。
to_extension_call112–155 ↗
async fn to_extension_call(invocation: &ToolInvocation) -> ExtensionToolCall
作用:把核心内部的一次工具调用,打包成扩展工具能理解的调用对象。这一步决定扩展能看到哪些历史、目录和权限。
数据流:输入是 ToolInvocation → 它读取会话历史,收集每个运行环境的工作目录、文件系统和沙箱上下文,并应用本轮已经授予的文件权限 → 再放入调用编号、工具名、模型名、截断策略、载荷和事件发射器 → 输出 ExtensionToolCall。
调用关系:ExtensionToolAdapter::handle 在执行扩展前一定会调用它;它会调用 apply_granted_turn_permissions 计算权限,并创建 CoreTurnItemEmitter 供扩展回报进度。
调用图:调用 2 个内部函数(apply_granted_turn_permissions, new);被 1 处调用(handle);外部调用 3 个(downgrade, new, with_capacity)。
tests::StubExtensionExecutor::tool_name190–192 ↗
fn tool_name(&self) -> codex_tools::ToolName
作用:测试用的假扩展工具名字,固定叫 extension_echo。它让测试不用依赖真实扩展。
数据流:没有外部输入 → 函数构造一个普通工具名 extension_echo → 返回这个名字。
调用关系:测试创建 StubExtensionExecutor 后,适配器会像对待真实扩展一样询问它的名字。
调用图:调用 1 个内部函数(plain)。
tests::StubExtensionExecutor::spec194–211 ↗
fn spec(&self) -> codex_tools::ToolSpec
作用:给测试假工具提供一份参数说明书。它声明工具接收一个 message 字符串。
数据流:没有外部输入 → 函数用 JSON 描述参数格式,并解析成工具 schema → 返回一个 Function 类型的 ToolSpec。
调用关系:测试里核心适配器会读取这份说明,确认扩展工具的规格能正常透传。
调用图:外部调用 3 个(parse_tool_input_schema, json!, Function)。
tests::StubExtensionExecutor::handle213–220 ↗
fn handle(&self, _call: codex_tools::ToolCall) -> codex_tools::ToolExecutorFuture<'_>
作用:测试用的假执行函数,永远返回成功。它用来验证适配器外围逻辑,而不是验证真实工具行为。
数据流:输入是一份工具调用但不会使用 → 异步返回 JSON {"ok": true} → 不记录状态,也不发事件。
调用关系:exposes_generic_hook_payloads 测试通过这个简单执行器搭建工具,重点检查调用前后 hook 载荷是否正确。
tests::CapturingExtensionExecutor::tool_name228–230 ↗
fn tool_name(&self) -> codex_tools::ToolName
作用:测试用捕获型扩展工具的名字,也固定叫 extension_echo。这样测试能用同一个工具名触发它。
数据流:没有外部输入 → 构造 extension_echo 这个工具名 → 返回给适配器。
调用关系:passes_turn_fields_and_scoped_turn_item_emitter_to_extension_call 测试会通过适配器读取它,确保调用路由一致。
调用图:调用 1 个内部函数(plain)。
tests::CapturingExtensionExecutor::spec232–241 ↗
fn spec(&self) -> codex_tools::ToolSpec
作用:给捕获型假扩展提供一份很宽松的工具说明书。这里参数格式不重要,重点是捕获调用上下文。
数据流:没有外部输入 → 创建默认 JSON schema,并包装成 Function 工具说明 → 返回给核心适配器。
调用关系:它配合 CapturingExtensionExecutor::handle 使用,让测试能像真实工具一样完成注册和调用。
调用图:外部调用 2 个(default, Function)。
tests::CapturingExtensionExecutor::handle243–245 ↗
fn handle(&self, call: codex_tools::ToolCall) -> codex_tools::ToolExecutorFuture<'_>
作用:测试入口函数,把收到的扩展调用交给 handle_call。这样符合扩展执行器接口的异步形式。
数据流:输入是一份 codex_tools::ToolCall → 它把调用包进异步任务并转交给 handle_call → 输出 handle_call 的结果。
调用关系:适配器执行工具时会调用它;它本身只是薄薄一层,真正的测试动作在 handle_call。
调用图:调用 1 个内部函数(handle_call);外部调用 1 个(pin)。
tests::CapturingExtensionExecutor::handle_call249–268 ↗
async fn handle_call(
&self,
call: codex_tools::ToolCall,
) -> Result<Box<dyn codex_tools::ToolOutput>, codex_tools::FunctionCallError>
作用:测试扩展调用里到底带了什么信息,并顺便模拟一次网页搜索事件。它会把收到的 ToolCall 保存下来供断言检查。
数据流:输入是扩展 ToolCall → 它用 call_id 构造一个 WebSearch 进度项,先发 started 再发 completed,然后把整份调用存进互斥锁保护的共享变量 → 返回 JSON {"ok": true}。
调用关系:由 tests::CapturingExtensionExecutor::handle 调用;passes_turn_fields_and_scoped_turn_item_emitter_to_extension_call 靠它确认适配器传入了历史、模型、环境和事件发射器。
调用图:调用 1 个内部函数(new);被 1 处调用(handle);外部调用 3 个(new, json!, WebSearch)。
tests::exposes_generic_hook_payloads272–305 ↗
async fn exposes_generic_hook_payloads()
作用:验证扩展函数工具也能产生通用的调用前、调用后 hook 数据。hook 可以理解成工具运行前后给外部插件看的记录。
数据流:测试创建假会话、假轮次、StubExtensionExecutor 和一次带 message 参数的调用 → 调用核心运行时生成 pre 和 post payload → 断言里面的工具名、输入和输出 JSON 都符合预期。
调用关系:它使用 ExtensionToolAdapter::new 搭建被测对象;重点不是执行工具,而是确认 CoreToolRuntime 的默认 hook 包装能识别扩展函数载荷。
调用图:调用 5 个内部函数(make_session_and_context, new, new, plain, new);外部调用 5 个(new, assert_eq!, json!, new, new)。
tests::passes_turn_fields_and_scoped_turn_item_emitter_to_extension_call308–426 ↗
async fn passes_turn_fields_and_scoped_turn_item_emitter_to_extension_call()
作用:验证核心传给扩展的调用对象没有漏掉重要上下文。包括轮次编号、模型名、会话历史、环境沙箱,以及扩展发事件时是否只绑定当前轮次。
数据流:测试先创建带接收端的会话和轮次,写入一条历史消息,再用 CapturingExtensionExecutor 执行一次工具调用 → 执行器捕获 ToolCall 并发出搜索开始/完成事件 → 测试检查捕获到的字段和收到的事件都正确,并确认弱引用不会把会话和轮次额外留住。
调用关系:它通过 ExtensionToolAdapter::handle 间接覆盖 to_extension_call、CoreTurnItemEmitter::emit_started 和 emit_completed,是这个桥接文件最完整的一条集成测试。
调用图:调用 4 个内部函数(make_session_and_context_with_rx, new, new, plain);外部调用 13 个(clone, downgrade, new, new, assert!, assert_eq!, json!, panic!, from_ref, new (+3 more))。
tests::RecordExtensionTurnItemContributor::contribute436–446 ↗
fn contribute(
&'a self,
_thread_store: &'a ExtensionData,
turn_store: &'a ExtensionData,
_item: &'a mut TurnItem,
) -> codex_extension_api::Ext
作用:测试用的贡献器,作用是在扩展完成事件收尾时留下一个标记。贡献器可以理解成“事件发出前的补充加工插件”。
数据流:输入是线程级扩展数据、轮次级扩展数据和待修改的 TurnItem → 它在轮次数据里插入 ExtensionTurnItemContributorRan 标记 → 返回成功,不改 TurnItem 内容。
调用关系:extension_completion_runs_turn_item_contributors 会注册它;CoreTurnItemEmitter::emit_completed 里的 finalize_turn_item 应该触发它。
调用图:调用 1 个内部函数(insert);外部调用 1 个(pin)。
tests::extension_completion_runs_turn_item_contributors450–477 ↗
async fn extension_completion_runs_turn_item_contributors()
作用:验证扩展发布完成事件时,核心确实会运行 turn item 贡献器。否则扩展生态里依赖这些补充步骤的功能会失效。
数据流:测试创建会话和轮次,注册 RecordExtensionTurnItemContributor,然后直接调用 CoreTurnItemEmitter 的 emit_completed 发布一个搜索完成项 → 最后检查轮次扩展数据里是否出现贡献器写入的标记。
调用关系:它直接测试 CoreTurnItemEmitter::emit_completed 和 finalize_turn_item 的衔接,确保完成事件不只是发送,还会经过统一收尾流程。
调用图:调用 2 个内部函数(make_session_and_context, new);外部调用 5 个(downgrade, new, assert!, WebSearch, emit_completed)。
tests::ImageGenerationExtensionExecutor::tool_name480–482 ↗
fn tool_name(&self) -> codex_tools::ToolName
作用:测试用图片生成扩展的工具名,带有 image_gen 命名空间。命名空间可以理解成给工具名加前缀,避免和别的工具撞名。
数据流:没有外部输入 → 构造命名空间为 image_gen、短名为 imagegen 的工具名 → 返回给适配器。
调用关系:image_generation_publication_is_finalized_by_core 用它模拟图片生成类扩展工具。
调用图:调用 1 个内部函数(namespaced)。
tests::ImageGenerationExtensionExecutor::spec484–493 ↗
fn spec(&self) -> codex_tools::ToolSpec
作用:给测试图片生成工具提供说明书。参数格式使用默认 schema,因为测试重点是图片事件收尾。
数据流:没有外部输入 → 创建一个描述为 Generates an image 的 Function 工具说明 → 返回给核心适配器。
调用关系:它让 ImageGenerationExtensionExecutor 看起来像一个完整扩展工具,方便适配器正常调用。
调用图:外部调用 2 个(default, Function)。
tests::ImageGenerationExtensionExecutor::handle495–497 ↗
fn handle(&self, call: codex_tools::ToolCall) -> codex_tools::ToolExecutorFuture<'_>
作用:图片生成测试工具的执行入口,把调用转给 handle_call。它符合扩展执行器要求的异步接口。
数据流:输入是扩展 ToolCall → 包成异步任务并交给 handle_call → 输出 handle_call 的成功结果或错误。
调用关系:ExtensionToolAdapter::handle 调用这个入口;真正发图片开始和完成事件的动作在 handle_call。
调用图:调用 1 个内部函数(handle_call);外部调用 1 个(pin)。
tests::ImageGenerationExtensionExecutor::handle_call501–531 ↗
async fn handle_call(
&self,
call: codex_tools::ToolCall,
) -> Result<Box<dyn codex_tools::ToolOutput>, codex_tools::FunctionCallError>
作用:模拟一个图片生成扩展:先报告生成中,再报告完成。它故意在完成事件里带一个扩展自称的保存路径,用来测试核心会不会忽略它。
数据流:输入是扩展 ToolCall → 用 call_id 发出 ImageGeneration started 事件,再发出 completed 事件,完成项里包含 base64 结果和一个 claimed saved_path → 返回 JSON {"ok": true}。
调用关系:由 tests::ImageGenerationExtensionExecutor::handle 调用;image_generation_publication_is_finalized_by_core 依靠它确认 extension_turn_item 会清掉扩展路径,并由 finalize_turn_item 保存到核心指定路径。
调用图:调用 1 个内部函数(new);被 1 处调用(handle);外部调用 5 个(new, new, test_path_buf, json!, ImageGeneration)。
tests::image_generation_publication_is_finalized_by_core535–603 ↗
async fn image_generation_publication_is_finalized_by_core()
作用:验证扩展发布图片生成完成事件后,最终图片由核心保存,并且事件里的 saved_path 是核心决定的路径。这样可以避免扩展伪造或乱写保存位置。
数据流:测试创建会话、轮次和图片生成假扩展,预先计算核心应该保存到的路径,然后执行工具调用 → 接收图片开始和完成事件 → 检查完成项的路径是核心路径,并读取文件确认 base64 内容已经保存成 png 字节。
调用关系:它通过 ExtensionToolAdapter::new 和工具执行流程覆盖 ImageGenerationExtensionExecutor、CoreTurnItemEmitter::emit_completed、extension_turn_item 和 finalize_turn_item 的配合。
调用图:调用 5 个内部函数(make_session_and_context_with_rx, image_generation_artifact_path, new, new, namespaced);外部调用 7 个(new, assert!, assert_eq!, panic!, new, new, handle)。
core/src/tools/handlers/tool_search.rs源码 ↗
可以把这个文件理解成一个“工具目录的搜索柜台”。系统里可能有很多外部工具、动态工具,每个工具都有名字、说明和可加载的信息。如果全部塞给模型,既占空间,也容易让模型选错。这里的 ToolSearchHandler 会把这些工具的搜索文字做成一个 BM25 搜索索引(BM25 是一种按关键词相关度排序的常见文本搜索算法),然后暴露成一个名叫 tool_search 的工具。模型调用它时传入 query 和 limit,它会检查输入是否合法,搜索最匹配的工具,再把结果整理成可加载的工具说明返回。ToolSearchHandlerCache 则像一个小缓存盒:如果工具列表没变,就复用旧的搜索器;变了才重建。文件底部的测试确认两件事:缓存真的会复用,以及不同来源的工具搜索结果会被正确合并成命名空间,避免重复散乱。
ToolSearchHandlerCache::get_or_build36–56 ↗
fn get_or_build(&self, search_infos: Vec<ToolSearchInfo>) -> Arc<ToolSearchHandler>
作用:按给定的工具搜索信息拿到一个可用的搜索处理器。如果已有缓存且工具信息完全一样,就直接复用;如果工具变了,就重新建一个。
数据流:进去的是一组 ToolSearchInfo,也就是每个工具用于搜索和返回的信息。它先查看互斥锁保护的缓存(互斥锁就是一把锁,防止多个任务同时改同一份缓存),如果缓存里的工具信息一样,就返回同一个 Arc 包装的处理器(Arc 是可共享的引用计数指针)。如果不一样,它用 ToolSearchHandler::new 新建搜索器,再次确认缓存没有被别人同时更新,最后把新搜索器放进缓存并返回。
调用关系:这是外部运行时获取工具搜索器时最常走的入口。它内部反复通过 ToolSearchHandlerCache::cached 安全访问缓存,需要重建时把工作交给 ToolSearchHandler::new。这样上层不用关心搜索索引什么时候该复用、什么时候该刷新。
ToolSearchHandlerCache::cached58–63 ↗
fn cached(&self) -> std::sync::MutexGuard<'_, Option<Arc<ToolSearchHandler>>>
作用:安全地打开缓存这只“小盒子”,让调用者能读写里面保存的搜索处理器。
数据流:它不接收业务数据,只读取 self.cached 里的互斥锁。正常拿到锁时返回锁守卫;如果之前有线程在持锁时崩了导致锁被标记为“中毒”,它也会取回里面的数据继续用。出来的是一个 MutexGuard,调用者可以通过它查看或替换缓存内容。
调用关系:它只被 ToolSearchHandlerCache::get_or_build 使用,是缓存逻辑的底层小帮手。它把“锁可能中毒”的麻烦藏起来,让 get_or_build 可以专心判断要不要复用搜索器。
调用图:被 1 处调用(get_or_build)。
ToolSearchHandler::new67–87 ↗
fn new(search_infos: Vec<ToolSearchInfo>) -> Self
作用:根据当前所有可搜索工具,创建一个新的工具搜索处理器。它会同时准备工具自身的说明和真正用于查找的搜索索引。
数据流:进去的是一组 ToolSearchInfo。它先从里面取出来源信息,用 create_tool_search_tool 生成 tool_search 这个工具对外展示的规格说明;再把每个工具的 search_text 做成文档,并用 SearchEngineBuilder 建出英文 BM25 搜索引擎。出来的是一个 ToolSearchHandler,里面保存原始工具信息、工具规格和搜索引擎。
调用关系:ToolSearchHandlerCache::get_or_build 在缓存失效时会调用它来重建搜索器,测试 tests::mixed_search_results_coalesce_mcp_namespaces 也直接用它构造对象。它是后续 tool_name、spec、handle_call、search 等行为的基础。
调用图:调用 1 个内部函数(create_tool_search_tool);被 2 处调用(get_or_build, mixed_search_results_coalesce_mcp_namespaces);外部调用 1 个(with_documents)。
ToolSearchHandler::tool_name91–93 ↗
fn tool_name(&self) -> ToolName
作用:告诉系统这个处理器对应的工具名字,也就是固定的 tool_search。
数据流:它不需要外部输入,只读取常量 TOOL_SEARCH_TOOL_NAME,并用 ToolName::plain 包成系统认识的工具名对象。出来的是一个 ToolName。
调用关系:这是 ToolExecutor 接口的一部分,工具注册或调度时会问处理器“你叫什么”。它不做搜索,只负责让运行时能把一次 tool_search 调用路由到这个处理器。
调用图:调用 1 个内部函数(plain)。
ToolSearchHandler::spec95–97 ↗
fn spec(&self) -> ToolSpec
作用:返回 tool_search 工具的说明书,告诉模型这个工具怎么调用、有哪些参数。
数据流:它读取处理器里提前生成好的 spec,并克隆一份返回。返回的是 ToolSpec;克隆是为了让调用者拿到自己的副本,不直接改处理器内部保存的版本。
调用关系:这是 ToolExecutor 接口的一部分,系统向模型展示可用工具时会用到它。这个 spec 在 ToolSearchHandler::new 里创建,之后由这里对外提供。
调用图:外部调用 1 个(clone)。
ToolSearchHandler::supports_parallel_tool_calls99–101 ↗
fn supports_parallel_tool_calls(&self) -> bool
作用:声明这个工具可以并行调用,也就是多个搜索请求可以同时跑。
数据流:它不读取输入,也不改状态,固定返回 true。意思是搜索处理器内部不会因为并发查询而互相踩坏数据。
调用关系:这是 ToolExecutor 接口的一部分,调度器决定能不能同时发起多个工具调用时会看它。它配合只读搜索索引,让工具搜索成为安全的并发操作。
ToolSearchHandler::handle103–105 ↗
fn handle(&self, invocation: ToolInvocation) -> codex_tools::ToolExecutorFuture<'_>
作用:接住一次真实的工具调用,并把它包装成异步任务执行。异步任务可以理解成“先把活儿排上,等结果回来”。
数据流:进去的是 ToolInvocation,也就是一次工具调用的完整请求。它把真正处理逻辑交给 ToolSearchHandler::handle_call,然后用 Box::pin 包成系统要求的 future(future 是异步结果的占位对象)。出来的是一个将来会完成的工具执行结果。
调用关系:这是 ToolExecutor 接口里真正被调度器调用的方法。它本身很薄,只负责适配接口;具体检查参数、搜索和返回结果都由 ToolSearchHandler::handle_call 完成。
调用图:调用 1 个内部函数(handle_call);外部调用 1 个(pin)。
ToolSearchHandler::handle_call109–145 ↗
async fn handle_call(
&self,
invocation: ToolInvocation,
) -> Result<Box<dyn crate::tools::context::ToolOutput>, FunctionCallError>
作用:真正处理一次 tool_search 调用:检查参数、执行搜索、把结果包装成工具输出。
数据流:进去的是 ToolInvocation。它先取出 payload,确认这次调用确实是 ToolSearch;如果不是,就返回 Fatal 错误,表示系统接错了活。接着读取 query 和 limit:query 去掉前后空白后不能为空,limit 不给就用默认值,给 0 会返回给模型看的错误。若没有任何可搜索工具,直接返回空列表;否则调用 ToolSearchHandler::search 找工具,并用 boxed_tool_output 包成 ToolSearchOutput 返回。
调用关系:ToolSearchHandler::handle 会把请求交给它。它是外部调用进入本文件业务逻辑的核心关口:前半段负责把坏输入挡住,后半段把搜索工作交给 ToolSearchHandler::search,再把结果变成系统统一的工具输出。
调用图:调用 2 个内部函数(boxed_tool_output, search);被 1 处调用(handle);外部调用 4 个(new, format!, Fatal, RespondToModel)。
ToolSearchHandler::search151–164 ↗
fn search(
&self,
query: &str,
limit: usize,
) -> Result<Vec<LoadableToolSpec>, FunctionCallError>
作用:按关键词在工具目录里找最相关的工具,并限制返回数量。
数据流:进去的是 query 和 limit。它把 query 交给内部 BM25 搜索引擎,拿到按相关度排序的结果;每个结果只带文档 id,于是它用 id 回到 search_infos 里找到对应工具,再取出 ToolSearchEntry。最后把这些条目交给 ToolSearchHandler::search_output_tools,得到可返回给模型的 LoadableToolSpec 列表。
调用关系:它由 ToolSearchHandler::handle_call 在参数检查通过后调用。它负责“找哪些工具”,但不负责“如何把工具合并成最终格式”;这个整理工作交给 ToolSearchHandler::search_output_tools。
调用图:调用 1 个内部函数(search_output_tools);被 1 处调用(handle_call);外部调用 1 个(search)。
ToolSearchHandler::search_output_tools166–173 ↗
fn search_output_tools(
&self,
results: impl IntoIterator<Item = &'a ToolSearchEntry>,
) -> Result<Vec<LoadableToolSpec>, FunctionCallError>
作用:把搜索命中的工具条目整理成最终可加载的工具说明,并处理同一命名空间下工具的合并。
数据流:进去的是一批 ToolSearchEntry 引用。它取出每个条目里的 output,克隆出来,再交给 coalesce_loadable_tool_specs 合并。出来的是 Vec<LoadableToolSpec>;比如多个来自同一个 MCP 命名空间的工具,会被整理到同一个命名空间说明里,而不是零散重复返回。
调用关系:ToolSearchHandler::search 在拿到搜索命中后调用它。测试 tests::mixed_search_results_coalesce_mcp_namespaces 也直接验证它的整理效果,确保搜索结果对模型来说是干净、可加载的。
调用图:被 1 处调用(search);外部调用 2 个(into_iter, coalesce_loadable_tool_specs)。
tests::cache_reuses_handler_for_identical_search_infos_and_rebuilds_for_changes192–212 ↗
fn cache_reuses_handler_for_identical_search_infos_and_rebuilds_for_changes()
作用:验证缓存行为:工具信息没变时应该复用同一个搜索处理器,工具信息变了时必须重建。
数据流:测试先创建一个默认缓存,再造出一条 MCP 工具的搜索信息。第一次 get_or_build 得到 first,第二次用完全相同的信息得到 second,并检查它们是不是同一个 Arc 指向的对象。然后它修改工具的搜索文字,再调用 get_or_build,确认返回的是另一个对象。
调用关系:这是 ToolSearchHandlerCache::get_or_build 的安全网。它用真实转换出来的搜索信息模拟运行时场景,确保缓存不会无谓重建,也不会在工具列表已经变化时错误复用旧索引。
tests::mixed_search_results_coalesce_mcp_namespaces215–319 ↗
fn mixed_search_results_coalesce_mcp_namespaces()
作用:验证搜索结果里混有不同来源工具时,最终输出会被正确合并,尤其是同一个 MCP 命名空间下的工具会放在一起。
数据流:测试先构造一个动态工具命名空间和一个动态工具,再构造两个日历 MCP 工具。它把这些工具转换成 search_infos,创建 ToolSearchHandler,然后手动指定一个混合顺序的结果列表。接着调用 search_output_tools,最后用 assert_eq 检查输出是否是两个命名空间:一个 mcp__calendar,里面有两个日历工具;一个 codex_app,里面有动态工具。
调用关系:它直接调用 ToolSearchHandler::new 和 ToolSearchHandler::search_output_tools,重点保护“搜索结果整理”这一步。这样即使搜索命中顺序混杂,返回给模型的工具说明也仍然结构清楚。
调用图:调用 1 个内部函数(new);外部调用 4 个(new, assert_eq!, tool_info, json!)。
tests::tool_info321–342 ↗
fn tool_info(server_name: &str, tool_name: &str, description_prefix: &str) -> ToolInfo
作用:给测试快速造一个假的 MCP 工具信息,避免每个测试都重复写一大段样板数据。
数据流:进去的是 server_name、tool_name 和 description_prefix。它把这些字符串拼成 ToolInfo 需要的字段,包括服务器名、可调用名、命名空间名、工具描述和一个空参数 schema。出来的是一个 ToolInfo,测试可以再交给 McpHandler 转成搜索信息。
调用关系:它服务于本文件里的测试,尤其是缓存测试和混合结果合并测试。它不参与正式运行,只是让测试更像真实 MCP 工具,同时让测试代码更短、更好读。
core/src/tools/handlers/tool_search_spec.rs源码 ↗
可以把这个文件理解成给模型准备的一张“图书馆查询机使用说明”。有些工具不会提前全部展示给模型,而是先藏在可搜索的工具库里;模型需要时,就调用这里定义的 tool_search 去查。文件会把查询参数说清楚:必须有 query,也就是搜索词;可以有 limit,也就是最多返回多少个结果。它还会把当前启用的工具来源整理成一段人能读懂的文字,并且去掉重复来源:如果同一个来源出现多次,会尽量保留有说明的那一条。最后它生成一个 ToolSpec(工具规格,告诉系统和模型这个工具叫什么、怎么用、要传什么参数)。说明里还特别提醒:做 MCP(一种让模型连接外部工具和资源的协议)工具发现时,要用这个搜索工具,而不是直接列资源。
create_tool_search_tool7–62 ↗
fn create_tool_search_tool(
searchable_sources: &[ToolSearchSourceInfo],
default_limit: usize,
) -> ToolSpec
作用:创建“工具搜索”这个工具的完整规格。调用者给它一批可搜索的工具来源和默认返回数量,它就拼出模型能看懂、系统能执行的工具说明。
数据流:输入是一组工具来源信息,每个来源有名字和可选描述,以及一个默认的结果数量。函数先做参数表:query 是必填搜索词,limit 是可选数量限制;再把来源按名字合并,重复的来源只显示一次,并优先保留描述;然后把这些来源写进一段说明文字。输出是一个 ToolSpec::ToolSearch,里面包括客户端执行方式、工具介绍和参数结构;它不改外部数据,只返回新建好的规格。
调用关系:它通常在上层创建工具集合时被调用,也就是调用图里的 new 会用它来把搜索工具放进系统可用工具列表。它内部借助 JsonSchema::string、JsonSchema::number、JsonSchema::object 这些小工具来描述参数长什么样,再把所有内容包装成最终的工具规格。
调用图:调用 3 个内部函数(number, object, string);被 1 处调用(new);外部调用 4 个(from, new, format!, vec!)。
tests::create_tool_search_tool_deduplicates_and_renders_enabled_sources72–112 ↗
fn create_tool_search_tool_deduplicates_and_renders_enabled_sources()
作用:这是一个测试,用来确认工具搜索规格生成得对不对,特别是重复来源会不会被合并、来源说明会不会正确写进描述里。
数据流:测试先准备三个来源:两个同名的 Google Drive,其中一个有描述、一个没有,还有一个没有描述的 docs。然后调用 create_tool_search_tool,拿到实际生成的工具规格。最后用断言把实际结果和手写的期望结果逐字比较;如果描述、参数、默认数量或去重行为有任何不一致,测试就会失败。
调用关系:它只在测试运行时活跃,不参与正常程序流程。它通过 assert_eq! 检查 create_tool_search_tool 的输出,相当于给这个文件的核心行为立了一份“合同”,防止以后改代码时不小心把工具说明格式或去重规则改坏。
调用图:外部调用 1 个(assert_eq!)。
core/src/tools/handlers/view_image.rs源码 ↗
模型本身不能随便直接读电脑上的图片文件,所以需要一个安全的中间人。这个文件就是这个中间人:它接到模型提出的“查看这张图片”的请求后,先确认当前模型真的支持图片输入;再解析参数,比如图片路径、环境编号、想要高清还是原图;然后找到对应运行环境和当前目录,把相对路径变成真实路径。接着它通过沙箱(像一圈安全围栏,限制能读哪些文件)检查文件是否存在、是不是文件,并读取图片内容。读到后,它会按功能开关决定是直接包装成 data URL(一种把文件内容塞进文本里的格式),还是先解码、缩放后再包装。最后它记录一次“图片被查看”的事件,并返回一个 ViewImageOutput,这个输出会把图片作为模型下一步能理解的图片输入交回去。文件末尾的测试重点确认:日志不会泄露整张图片数据、非法 detail 会被拒绝、沙箱读文件会被正确使用。
ViewImageHandler::default37–44 ↗
fn default() -> Self
作用:创建一个默认的图片查看工具处理器。默认配置比较保守:不声明支持请求原图细节,也不把环境编号放进工具参数里。
数据流:进去不需要任何输入 → 它填好一份默认的 ViewImageToolOptions → 出来一个可以处理 view_image 调用的 ViewImageHandler。
调用关系:这个默认构造主要在测试里使用,让测试不用关心复杂配置,直接拿一个标准处理器来验证读取图片、拒绝错误参数等行为。
调用图:被 3 处调用(handle_accepts_explicit_high_detail, handle_passes_sandbox_context_for_local_filesystem_reads, handle_rejects_unsupported_detail)。
ViewImageHandler::new48–50 ↗
fn new(options: ViewImageToolOptions) -> Self
作用:按外部给定的选项创建图片查看工具处理器。系统注册核心工具时会用它,把当前能力开关传进来。
数据流:进去是一份 ViewImageToolOptions 配置 → 它把配置保存到处理器里 → 出来一个带指定行为的 ViewImageHandler。
调用关系:它被 add_core_utility_tools 使用,属于工具注册阶段的一环;注册好之后,真正的请求会走到这个处理器的 handle 和 handle_call。
调用图:被 1 处调用(add_core_utility_tools)。
ViewImageHandler::tool_name71–73 ↗
fn tool_name(&self) -> ToolName
作用:告诉工具系统:这个处理器对应的工具名字叫 view_image。这样模型调用 view_image 时,系统才能找到它。
数据流:进去是处理器自己 → 它用 plain 生成一个普通工具名 → 出来 ToolName,内容是 view_image。
调用关系:工具注册和分发时会问每个处理器叫什么名字;这个函数给出名字,后续模型请求才能被派发到本文件的处理流程。
调用图:调用 1 个内部函数(plain)。
ViewImageHandler::spec75–77 ↗
fn spec(&self) -> ToolSpec
作用:生成 view_image 工具的说明书。这个说明书告诉模型:这个工具能接收哪些参数、参数是什么意思。
数据流:进去是处理器里保存的选项 → 它调用 create_view_image_tool 按选项生成工具规格 → 出来一份 ToolSpec。
调用关系:工具暴露给模型前会调用它。它不负责执行,只负责把“怎么调用这个工具”说清楚;真正执行仍然交给 handle_call。
调用图:调用 1 个内部函数(create_view_image_tool)。
ViewImageHandler::supports_parallel_tool_calls79–81 ↗
fn supports_parallel_tool_calls(&self) -> bool
作用:说明这个工具允许并行调用。也就是说,多个查看图片请求可以同时进行,不必一个等一个。
数据流:进去是处理器自己 → 它直接返回 true → 工具调度器据此知道可以并发安排多个 view_image 调用。
调用关系:它服务于工具调度层。调度器在决定是否能同时跑多个工具请求时会参考这个结果。
ViewImageHandler::handle83–85 ↗
fn handle(&self, invocation: ToolInvocation) -> codex_tools::ToolExecutorFuture<'_>
作用:这是工具系统真正调用处理器时进入的门口。它把一次工具调用包装成异步任务,交给内部的 handle_call 做细活。
数据流:进去是一份 ToolInvocation,里面有会话、当前轮次、参数、调用编号等信息 → 它调用 handle_call 并用 Box::pin 固定成异步返回值 → 出来一个将来会完成的执行结果。
调用关系:工具调度器调用它;它自己不解析图片,也不读文件,而是把工作转交给 ViewImageHandler::handle_call。
调用图:调用 1 个内部函数(handle_call);外部调用 1 个(pin)。
ViewImageHandler::handle_call89–228 ↗
async fn handle_call(
&self,
invocation: ToolInvocation,
) -> Result<Box<dyn crate::tools::context::ToolOutput>, FunctionCallError>
作用:这是 view_image 的核心执行流程。它负责确认模型能看图、解析参数、安全读取文件、处理图片格式,并把图片返回给模型。
数据流:进去是一整次工具调用,包含模型能力、参数、环境、会话和调用编号 → 它先检查模型是否支持图片输入,再用 parse_arguments 读出路径和清晰度要求;接着用 resolve_tool_environment 找到运行环境,把路径转成绝对路径并建立沙箱;然后读取文件元数据和内容;之后按是否请求原图、模型是否允许原图、功能开关是否启用,选择直接生成 data URL 或先缩放处理;最后发出图片查看开始和完成事件 → 出来一个装着图片 data URL 和图片细节级别的 ViewImageOutput,或者返回一条能给模型看的错误信息。
调用关系:它由 ViewImageHandler::handle 调用,是整个文件最重要的函数。它把参数解析、环境选择、文件系统读取、图片转换、事件记录和输出封装串在一起;最后用 boxed_tool_output 把 ViewImageOutput 交回工具框架。
调用图:调用 4 个内部函数(boxed_tool_output, parse_arguments, resolve_tool_environment, from_abs_path);被 1 处调用(handle);外部调用 7 个(ImageView, data_url_from_bytes, load_for_prompt_bytes, can_request_original_image_detail, format!, matches!, RespondToModel)。
ViewImageOutput::log_preview239–241 ↗
fn log_preview(&self) -> String
作用:生成给日志看的简短说明,但故意不把图片内容写进日志。这样既能知道返回了图片,又不会让日志变得巨大或泄露图片数据。
数据流:进去是包含图片 data URL 的输出对象 → 它只计算字符串长度,并格式化成一句占位文本 → 出来类似“图片数据已省略,多少字节”的日志预览。
调用关系:工具框架记录执行结果时会用它。测试 tests::log_preview_omits_image_data 专门确认它不会把真实图片 data URL 打进日志。
调用图:外部调用 1 个(format!)。
ViewImageOutput::success_for_logging243–245 ↗
fn success_for_logging(&self) -> bool
作用:告诉日志系统这次工具输出代表成功。这里固定返回成功,因为能生成这个输出就说明图片已经成功准备好。
数据流:进去是输出对象 → 它不读取额外信息,直接返回 true → 日志层把这次工具调用标记为成功。
调用关系:它属于 ToolOutput 接口的一部分,工具框架在记录结果状态时会调用它。
ViewImageOutput::to_response_item247–262 ↗
fn to_response_item(&self, call_id: &str, _payload: &ToolPayload) -> ResponseInputItem
作用:把图片输出转换成模型协议能接收的响应项。简单说,就是把图片包装成“函数调用结果”,并标明这是一张输入图片。
数据流:进去是调用编号和原始工具负载 → 它复制图片 data URL,附上图片细节级别,组成 InputImage 内容项,再放进成功的函数输出结构里 → 出来一个 ResponseInputItem::FunctionCallOutput,可以发送回模型。
调用关系:当普通对话模式需要把工具结果回传给模型时,工具框架会调用它。它承接 handle_call 生成的 ViewImageOutput,把内部结果翻译成模型协议格式。
调用图:外部调用 2 个(ContentItems, vec!)。
ViewImageOutput::code_mode_result264–269 ↗
fn code_mode_result(&self, _payload: &ToolPayload) -> serde_json::Value
作用:把图片输出转换成代码模式更容易使用的 JSON 对象。返回里直接包含 image_url 和 detail。
数据流:进去是输出对象和工具负载 → 它把图片 data URL 与清晰度字段放进 JSON → 出来一个 JSON 值,形如 { "image_url": ..., "detail": ... }。
调用关系:代码模式或需要结构化结果的地方会用它。测试 tests::code_mode_result_returns_image_url_object 确认它返回的 JSON 形状正确。
调用图:外部调用 1 个(json!)。
tests::replace_primary_environment_cwd289–302 ↗
fn replace_primary_environment_cwd(turn: &mut crate::TurnContext, cwd: AbsolutePathBuf)
作用:测试用的小帮手,用来把当前轮次的默认工作目录换成临时目录。这样测试可以控制 view_image 到哪里找图片。
数据流:进去是一个可修改的轮次上下文和新的绝对路径 → 它取出第一个环境,保留环境编号、环境对象和 shell,只替换当前目录 → 出来没有单独返回值,但轮次里的主环境目录已经被改掉。
调用关系:图片读取相关测试会先调用它,把工作目录指到测试临时文件夹;之后再调用 ViewImageHandler::handle,让处理器从这个受控目录里找图片。
调用图:调用 2 个内部函数(new, from_abs_path)。
tests::log_preview_omits_image_data305–312 ↗
fn log_preview_omits_image_data()
作用:确认日志预览不会泄露或打印完整图片 data URL。图片内容可能很大也可能敏感,所以这里只应显示长度。
数据流:进去没有外部输入 → 测试创建一个带假图片 data URL 的 ViewImageOutput,调用 log_preview → 期望出来的文本是省略图片内容的占位说明。
调用关系:它直接验证 ViewImageOutput::log_preview。这个测试保护的是日志安全和日志体积,不涉及真实文件读取。
调用图:外部调用 1 个(assert_eq!)。
tests::code_mode_result_returns_image_url_object315–332 ↗
fn code_mode_result_returns_image_url_object()
作用:确认代码模式拿到的结果是一个包含图片地址和清晰度的 JSON 对象。这样调用方可以稳定地读取字段。
数据流:进去没有外部输入 → 测试构造一个 ViewImageOutput,传入一个函数型工具负载,调用 code_mode_result → 出来应等于预期 JSON,其中 detail 是默认的 high。
调用关系:它直接验证 ViewImageOutput::code_mode_result,确保工具结果在代码模式下的格式不会被无意改坏。
调用图:外部调用 1 个(assert_eq!)。
tests::handle_passes_sandbox_context_for_local_filesystem_reads335–367 ↗
async fn handle_passes_sandbox_context_for_local_filesystem_reads()
作用:确认读取本地图片时确实带上了沙箱限制。沙箱就像安全围栏,防止工具越权读文件。
数据流:进去没有外部输入 → 测试创建会话和轮次,把工作目录换到临时目录,写入一个假图片文件,并把权限设成只读;然后调用默认 ViewImageHandler 的 handle → 期望出来的是沙箱相关错误,说明文件系统读取经过了沙箱检查。
调用关系:它通过 ViewImageHandler::default 创建处理器,再走完整的 handle 到 handle_call 流程。这个测试重点不是图片格式,而是确认文件读取没有绕过安全上下文。
调用图:调用 5 个内部函数(make_session_and_context, default, new, read_only, plain);外部调用 9 个(new, new, assert!, replace_primary_environment_cwd, json!, panic!, write, tempdir, new)。
tests::handle_rejects_unsupported_detail370–395 ↗
async fn handle_rejects_unsupported_detail()
作用:确认 detail 参数只接受允许的值。比如传 low 时,工具应该明确拒绝,而不是偷偷当成别的意思。
数据流:进去没有外部输入 → 测试创建一次工具调用,参数里放入路径和非法的 detail: low,然后调用处理器 → 出来应是一条给模型看的错误信息,说明只支持 high 或 original。
调用关系:它走 ViewImageHandler::handle 到 handle_call,专门覆盖参数校验分支,防止以后有人把非法值静默接受。
调用图:调用 4 个内部函数(make_session_and_context, default, new, plain);外部调用 6 个(new, new, assert_eq!, json!, panic!, new)。
tests::handle_accepts_explicit_high_detail398–427 ↗
async fn handle_accepts_explicit_high_detail()
作用:确认显式写 detail: high 是合法的。即使图片内容不是真图片,错误也应该发生在图片处理阶段,而不是参数校验阶段。
数据流:进去没有外部输入 → 测试创建临时图片文件,把权限放开,调用 view_image 并传入 detail: high → 因为文件内容是假图片,最后出来应是“无法处理图片”的错误,而不是“不支持 detail”的错误。
调用关系:它使用 tests::replace_primary_environment_cwd 设置目录,再通过 ViewImageHandler::default 和 handle 走完整流程。这个测试和 tests::handle_rejects_unsupported_detail 配合,确认合法和非法 detail 的边界。
调用图:调用 4 个内部函数(make_session_and_context, default, new, plain);外部调用 9 个(new, new, assert!, replace_primary_environment_cwd, json!, panic!, write, tempdir, new)。
core/src/tools/handlers/view_image_spec.rs源码 ↗
这个文件像是在给“查看图片”工具写一张使用说明和表格模板。模型如果想看一张已经在本地磁盘上的图片,不能随口说“帮我打开”,而要按系统规定传入图片路径。这里的 create_view_image_tool 会生成这份工具说明:最基本一定要有 path,也就是图片文件路径;如果系统允许,还可以加 detail,让调用方要求“高质量”或“原始分辨率”;如果系统有多个运行环境,还可以加 environment_id,说明去哪个环境里找图片。文件还规定了工具执行完以后必须返回什么:一个 image_url,也就是能展示图片的数据地址,以及 detail,说明返回的是普通高质量版本还是原始版本。这样做的好处是,模型、工具执行器和外部接口都按同一份规矩说话,不容易因为字段名、必填项或返回格式不一致而出错。
create_view_image_tool15–50 ↗
fn create_view_image_tool(options: ViewImageToolOptions) -> ToolSpec
作用:这个函数创建“查看本地图片”工具的完整说明书。别人注册工具时会用它,让模型知道这个工具叫什么、什么时候该用、需要传哪些参数、会返回什么。
数据流:进去的是 ViewImageToolOptions,也就是两个开关:是否允许请求原始图片细节、是否需要带环境编号。函数先准备必填的 path 字段,再按开关决定要不要加入 detail 和 environment_id。最后它把这些输入规则、工具名字、工具描述、返回格式一起包装成 ToolSpec 返回;它不会真的打开图片,只是生成“怎么调用这个工具”的规范。
调用关系:它由上层的 spec 在准备工具清单时调用。它自己会请 view_image_output_schema 生成返回结果的格式说明,也会用 JsonSchema 的 object、string、string_enum 等辅助方法拼出输入参数的规则。可以把它理解成工具登记处里负责填写“查看图片”登记表的人。
调用图:调用 4 个内部函数(view_image_output_schema, object, string, string_enum);被 1 处调用(spec);外部调用 3 个(from, Function, vec!)。
view_image_output_schema52–69 ↗
fn view_image_output_schema() -> Value
作用:这个函数专门说明“查看图片”工具执行成功后应该返回什么格式。它保证调用方拿到的结果里一定有图片地址和清晰度提示。
数据流:它不需要外部输入。函数内部直接写出一份 JSON 结构:返回值必须是一个对象,里面要有 image_url 字符串和 detail 字符串;detail 只能是 high 或 original;并且不允许多塞未声明的字段。最后它把这份结构作为 JSON 值交出去。
调用关系:它只在 create_view_image_tool 组装工具说明时被调用。create_view_image_tool 负责整张工具说明书,而这个函数负责其中“输出结果长什么样”这一页,避免主函数里塞太多细节。
调用图:被 1 处调用(create_view_image_tool);外部调用 1 个(json!)。
core/src/tools/handlers/get_context_remaining.rs源码 ↗
可以把模型的上下文窗口想成一个背包:对话、文件片段、工具结果都会占空间。这个文件做的事,就是在模型问“背包还剩多大”时,算出还剩多少 token(token 可以粗略理解成模型读文字时使用的小单位)。核心是 GetContextRemainingHandler,它先确认这次调用真的是函数工具调用;如果不是,就返回一个错误,避免误用。然后它查看当前模型的最大上下文窗口是多少,再去会话里查询已经用了多少 token,最后用“总容量减去已用容量”算出剩余量,并保证结果不会变成负数。如果系统不知道总容量,它也不会瞎猜,而是返回“未知”。GetContextRemainingOutput 则负责把这个结果变成模型能读的文字、日志里能看的摘要,以及代码模式下能用的 JSON 数据。
GetContextRemainingOutput::new24–26 ↗
fn new(tokens_left: Option<i64>) -> Self
作用:创建一个“上下文剩余量”的结果对象。有人已经算出还剩多少 token,或者不知道剩多少时,就用它把这个值包起来,方便后面统一输出。
数据流:进去的是一个可有可无的数字:有数字表示剩余 token 数,没有数字表示未知。函数只是把这个数字放进 GetContextRemainingOutput 里。出来的是一个结果对象,不会额外读取或修改别的东西。
调用关系:它由 GetContextRemainingHandler::handle 在算完剩余容量后调用。handle 负责算账,GetContextRemainingOutput::new 负责把账单装进统一的输出盒子里。
调用图:被 1 处调用(handle)。
GetContextRemainingOutput::fragment28–35 ↗
fn fragment(&self) -> String
作用:把剩余 token 数变成一段模型和日志都能看懂的文字。它会区分“知道具体剩多少”和“目前不知道”两种情况。
数据流:进去的是这个输出对象里保存的 tokens_left。若有具体数字,它会创建 TokenBudgetRemainingContext 并渲染成文字;若没有数字,它会创建一个“未知剩余量”的上下文并渲染。出来的是一段字符串,不改动原对象。
调用关系:它被 GetContextRemainingOutput::log_preview 和 GetContextRemainingOutput::to_response_item 调用。也就是说,不管是给日志看,还是正式回给模型看,都复用同一套文字生成方式,避免两边说法不一致。
调用图:调用 2 个内部函数(new, unknown);被 2 处调用(log_preview, to_response_item)。
GetContextRemainingOutput::log_preview39–41 ↗
fn log_preview(&self) -> String
作用:生成一小段适合写进日志的预览文字。这样开发者或系统记录里能看到这个工具返回了什么大意。
数据流:进去的是当前输出对象。它调用 GetContextRemainingOutput::fragment 把剩余容量变成文字。出来的是这段字符串,不修改任何状态。
调用关系:它是 ToolOutput 这套输出接口的一部分,在工具结果需要被记录到日志时会用到。它把具体文字生成的活交给 fragment,自己只负责提供日志入口。
调用图:调用 1 个内部函数(fragment)。
GetContextRemainingOutput::success_for_logging43–45 ↗
fn success_for_logging(&self) -> bool
作用:告诉日志系统:这个工具结果应该被当作成功记录。因为只要能产生这个输出,无论剩余量是具体数字还是未知,都不是执行失败。
数据流:它不需要额外输入,也不读取 tokens_left。进去的是一次方法调用,出来固定是 true,表示成功。它不修改任何东西。
调用关系:它也是 ToolOutput 输出接口的一部分,在记录工具调用结果时被日志流程使用。它不把工作交给别的函数,因为判断很简单:这个输出永远算成功。
GetContextRemainingOutput::to_response_item47–50 ↗
fn to_response_item(&self, call_id: &str, payload: &ToolPayload) -> ResponseInputItem
作用:把剩余上下文信息包装成可以发回给模型的响应项。模型不会直接拿 Rust 对象,所以这里要把结果转换成协议里规定的格式。
数据流:进去的是调用编号 call_id、工具调用的 payload,以及当前输出对象。它先用 fragment 生成文字,再用 FunctionToolOutput::from_text 包成工具输出,最后转成 ResponseInputItem。出来的是一个模型能接收的响应项,不修改原对象。
调用关系:它在工具执行完、需要把结果交还给模型时使用。它依赖 fragment 生成正文,再交给 FunctionToolOutput::from_text 和后续转换函数完成协议包装。
GetContextRemainingOutput::code_mode_result52–56 ↗
fn code_mode_result(&self, _payload: &ToolPayload) -> JsonValue
作用:给代码模式提供一个结构化结果,也就是 JSON。相比普通文字,JSON 更适合程序继续读取和处理。
数据流:进去的是当前输出对象,payload 参数在这里不用。它把 tokens_left 放进一个 JSON 对象里,字段名是 tokens_left。出来的是 JSON 值;如果剩余量未知,这个字段就是空值。
调用关系:它是 ToolOutput 接口里面向代码模式的出口。普通模型回复走 to_response_item,代码模式需要机器可读数据时走这里。
调用图:外部调用 1 个(json!)。
GetContextRemainingHandler::tool_name62–64 ↗
fn tool_name(&self) -> ToolName
作用:告诉工具注册系统:这个处理器对应的工具名是什么。没有这个名字,系统就不知道模型请求 get_context_remaining 时该找谁来执行。
数据流:它不需要外部输入,只读取固定的工具名常量 GET_CONTEXT_REMAINING_TOOL_NAME。它把这个普通字符串包装成 ToolName 并返回,不修改状态。
调用关系:工具注册或查找时会调用它。它调用 ToolName::plain 来生成标准工具名,让 GetContextRemainingHandler 能和工具目录里的名字对上。
调用图:调用 1 个内部函数(plain)。
GetContextRemainingHandler::spec66–68 ↗
fn spec(&self) -> ToolSpec
作用:提供这个工具的说明书,也就是模型能看到的工具规格。规格会告诉模型这个工具叫什么、能做什么、怎么调用。
数据流:它不接收业务输入。调用 create_get_context_remaining_tool 生成 ToolSpec,然后返回。它不修改处理器状态。
调用关系:在系统向模型公布可用工具时会用到它。GetContextRemainingHandler 自己不手写规格,而是把生成规格的工作交给 get_context_remaining_spec 里的 create_get_context_remaining_tool。
调用图:调用 1 个内部函数(create_get_context_remaining_tool)。
GetContextRemainingHandler::handle70–92 ↗
fn handle(&self, invocation: ToolInvocation) -> codex_tools::ToolExecutorFuture<'_>
作用:真正执行 get_context_remaining 工具:检查调用是否合法,计算当前上下文还剩多少 token,然后返回结果。它是这个文件里最核心的函数。
数据流:进去的是一次工具调用 invocation,里面带着 payload、当前轮次 turn、会话 session 等信息。它先确认 payload 是函数工具调用;如果不是,就返回一个给模型看的错误。然后读取模型上下文窗口大小;如果不知道窗口大小,就返回 tokens_left 为空的结果。若知道窗口大小,它异步读取当前会话已经使用的 token 总数,用总窗口减去已用量,并用安全减法避免负数或溢出。出来的是一个包装好的工具输出,里面要么有剩余 token 数,要么表示未知。
调用关系:当模型实际调用 get_context_remaining 时,工具运行时会进入这里。它负责主流程,调用 GetContextRemainingOutput::new 生成结果对象,再用 boxed_tool_output 包成统一工具输出;遇到不支持的 payload 时,它用 FunctionCallError::RespondToModel 把错误反馈给模型。
调用图:调用 2 个内部函数(boxed_tool_output, new);外部调用 3 个(pin, matches!, RespondToModel)。
core/src/tools/handlers/agent_jobs_spec.rs源码 ↗
这个文件本身不真正读取 CSV,也不真正启动代理。它做的是“登记菜单”:告诉系统有哪些工具、工具叫什么、需要哪些参数、每个参数是什么意思。比如 spawn_agents_on_csv 像一个批量派工按钮:给它一个 CSV 文件路径和一段指令模板,它就能让每一行变成一个独立任务。report_agent_job_result 像工人交回来的回执单:子代理必须用它提交某一项任务的结果。文件里用 JSON Schema(可以理解成“JSON 数据的填写规则”)来规定参数格式,例如哪些字段必填、哪些字段可以省略、字段应是字符串、数字还是布尔值。这样做很重要:没有这些说明,外部模型或调用方就不知道该怎么正确使用这些工具,也不知道少填字段或填错类型时该怎么处理。
create_spawn_agents_on_csv_tool6–72 ↗
fn create_spawn_agents_on_csv_tool() -> ToolSpec
作用:这个函数生成 spawn_agents_on_csv 这个工具的说明书。别人调用这个工具时,可以用它把一个 CSV 表格拆成很多行任务,并为每一行启动一个子代理处理。
数据流:进去时没有业务输入;函数在内部拼出一份参数规则,说明需要 csv_path 和 instruction,还可以填写任务编号列、输出文件路径、并发数量、单个任务最长运行时间、结果格式等信息;最后出来的是一个 ToolSpec,也就是系统能注册和展示的工具定义。它不执行任务,只产出“该怎么调用这个工具”的规则。
调用关系:它会被上层的 spec 流程调用,用来把这个批量派工工具加入工具列表。函数内部把字符串、数字、对象这些参数规则组装好,再交给外部的 ToolSpec::Function 包成一个正式工具。真正的 CSV 处理和代理执行发生在别的地方,这里只负责把入口说明清楚。
调用图:调用 3 个内部函数(number, object, string);被 1 处调用(spec);外部调用 4 个(from, new, Function, vec!)。
create_report_agent_job_result_tool74–115 ↗
fn create_report_agent_job_result_tool() -> ToolSpec
作用:这个函数生成 report_agent_job_result 这个工具的说明书。它专门给子代理用,让子代理把某个任务项的结果交回主流程。
数据流:进去时没有业务输入;函数在内部建立一份参数规则,要求必须提供 job_id、item_id 和 result,还可以用 stop 表示是否提交结果后停止剩余任务;最后返回一个 ToolSpec,告诉系统这个工具叫什么、谁该用、调用时要带哪些数据。它不会保存结果,只定义“交回结果时表单该怎么填”。
调用关系:它同样由上层的 spec 流程调用,用来把结果上报工具注册进工具集合。它和 create_spawn_agents_on_csv_tool 是一对:前者定义“怎么派工”,这个函数定义“工人怎么回报”。函数内部用布尔值、字符串、对象等规则拼出参数表,再交给外部的 ToolSpec::Function 变成正式工具定义。
调用图:调用 3 个内部函数(boolean, object, string);被 1 处调用(spec);外部调用 4 个(from, new, Function, vec!)。
core/src/tools/handlers/new_context_window_spec.rs源码 ↗
可以把这个文件理解成一张“工具登记卡”。真正执行“开启新上下文窗口”的代码不在这里;这里做的是把工具的名字、用途说明、参数格式这些信息写清楚,方便系统把它注册给上层接口使用。new_context 这个工具不需要用户传任何参数,所以它的参数结构是一个空对象,并且不强制严格校验。这样做的好处是,调用方只要看到这张说明卡,就知道可以触发“开始一个新的上下文窗口”这个动作,而不用猜工具叫什么、要传什么。没有这个文件,系统可能就无法把这个工具正确暴露出去,外部也就不知道它存在。
create_new_context_window_tool8–17 ↗
fn create_new_context_window_tool() -> ToolSpec
作用:这个函数创建 new_context 工具的说明书。别人调用它时,会拿到一份标准格式的工具定义,用来注册或暴露“开启新上下文窗口”这个能力。
数据流:进去时不需要任何输入;函数内部把工具名设成 new_context,把说明文字设成“Start a new context window.”,再创建一个空的 JSON 参数格式,也就是表示这个工具不需要额外参数;出来的是一个 ToolSpec,也就是系统能识别的工具规格对象。
调用关系:它会在工具规格汇总流程里的 spec 调用时被用到。函数自己把基础信息交给 ResponsesApiTool 这样的工具描述结构,再用 JsonSchema::object 做出空参数说明,最后包装成 ToolSpec::Function 返回给上层注册流程。
调用图:调用 1 个内部函数(object);被 1 处调用(spec);外部调用 2 个(new, Function)。
core/src/tools/handlers/plan_spec.rs源码 ↗
这个文件像是在给“更新计划”这件事写一张表格模板。没有它,系统就不知道 update_plan 这个工具应该叫什么、该收哪些字段、哪些字段必须有、状态只能填哪些值。它先规定每个计划步骤都包含 step,也就是要做的事,以及 status,也就是状态;状态只能是 pending(待做)、in_progress(正在做)、completed(已完成)这三种。然后它规定整个工具输入里必须有 plan 这个步骤列表,也可以带一个可选的 explanation 说明。最后,它把这些规则包装成一个 ToolSpec,也就是“工具说明书”,供外层系统注册和调用。一个重要点是:描述里提醒同一时间最多只能有一个步骤处于进行中,但这里本身主要是在声明格式,不是真正执行计划更新。
create_update_plan_tool7–58 ↗
fn create_update_plan_tool() -> ToolSpec
作用:这个函数生成 update_plan 工具的说明书。外层系统用它来知道这个工具叫什么、用户或模型调用它时应该提供什么数据、哪些输入算合法。
数据流:进去时不需要外部参数。它在函数内部先拼出“单个计划步骤”的格式:步骤文字是字符串,状态是三个固定选项之一;再拼出整个输入的格式:必须有 plan 列表,可选有 explanation 说明。最后,它把这些格式和工具名称、说明文字一起打包,返回一个 ToolSpec;它不修改外部状态,只产出这份工具定义。
调用关系:在整个工具系统准备工具清单时,spec 会调用它来拿到 update_plan 的定义。它自己把具体格式交给 JsonSchema 的字符串、枚举、数组、对象这些构造方法来搭出来,最后交给 ToolSpec::Function 包装成可注册的工具。
调用图:调用 4 个内部函数(array, object, string, string_enum);被 1 处调用(spec);外部调用 3 个(from, Function, vec!)。
Code-mode 运行时集成
本组介绍 code-mode 子系统,并继续说明面向核心的规范和处理器,用于执行并等待代码单元。
code-mode/src/lib.rs源码 ↗
这个文件本身不做具体业务,也没有函数。它的作用更像一家店的前台:里面真正干活的代码在 runtime 和 service 两个模块里,但外部使用者只需要从这个库入口拿到公开出来的东西。它先声明了两个内部模块:runtime 和 service。然后把协议包 codex_code_mode_protocol 里的内容重新导出,也就是转手公开给使用者;同时还公开了 CodeModeService、InProcessCodeModeSessionProvider 和 NoopCodeModeSessionDelegate 这几个服务相关的类型。这样做的好处是,其他代码引用这个库时路径更短、更稳定。以后内部文件怎么拆、怎么改,外部调用者不一定需要跟着改。
code-mode/src/runtime/globals.rs源码 ↗
这个文件的作用很像在开门营业前整理房间:先把危险或不需要的东西收走,再把客人能用的工具摆到桌面上。这里的“房间”是 V8,也就是 Chrome 用的 JavaScript 引擎。脚本运行前,install_globals 会拿到 JavaScript 的全局对象,删除 console、Atomics、SharedArrayBuffer、WebAssembly 这些默认能力,避免脚本乱用或做超出设计范围的事。然后它创建 tools 对象,把当前启用的外部工具变成 JavaScript 函数;还创建 ALL_TOOLS,让脚本能看到有哪些工具及说明。最后它把 text、image、notify、exit 等辅助函数装进去。这些函数本身不在这里真正干活,而是连到 callbacks 里的回调函数,像按钮接到后面的机器。
install_globals14–47 ↗
fn install_globals(scope: &mut v8::PinScope<'_, '_>) -> Result<(), String>
作用:这是本文件的总开关,负责把 JavaScript 运行环境的全局对象整理成项目想要的样子。没有它,脚本要么拿不到项目提供的工具,要么还能碰到一些不该暴露的默认能力。
数据流:进去的是一个 V8 作用域,里面能找到当前 JavaScript 上下文和运行状态。它先取出全局对象,删除 console、Atomics、SharedArrayBuffer、WebAssembly;再调用其他小函数做出 tools、ALL_TOOLS 和一批辅助函数;最后逐个写回全局对象。成功时返回空结果,失败时返回一段说明哪里失败的错误文字。
调用关系:run_runtime 在启动 JavaScript 运行时会调用它。它自己不直接构造所有细节,而是把工作分给 build_tools_object、build_all_tools_value、helper_function、set_global 和 delete_global,像一个布置现场的负责人。
调用图:调用 5 个内部函数(build_all_tools_value, build_tools_object, delete_global, helper_function, set_global);被 1 处调用(run_runtime);外部调用 1 个(get_current_context)。
build_tools_object49–65 ↗
fn build_tools_object(
scope: &mut v8::PinScope<'s, '_>,
) -> Result<v8::Local<'s, v8::Object>, String>
作用:这个函数把当前允许使用的工具,做成 JavaScript 里的 tools 对象。这样脚本就能像调用 tools.xxx 一样调用外部工具。
数据流:进去的是 V8 作用域。它从 RuntimeState 里读取 enabled_tools,也就是当前启用的工具列表;然后为每个工具创建一个同名属性,属性值是一个会触发 tool_callback 的函数。出来的是一个 JavaScript 对象,里面装好了这些工具函数。
调用关系:install_globals 在准备全局变量时调用它。它每遇到一个工具,就交给 tool_function 创建对应的 JavaScript 函数,然后把这些函数放进 tools 对象。
调用图:调用 1 个内部函数(tool_function);被 1 处调用(install_globals);外部调用 2 个(new, new)。
build_all_tools_value67–99 ↗
fn build_all_tools_value(
scope: &mut v8::PinScope<'s, '_>,
) -> Result<v8::Local<'s, v8::Value>, String>
作用:这个函数做出 ALL_TOOLS,也就是一份给脚本看的“工具说明清单”。它不是用来调用工具的,而是让脚本知道有哪些工具、每个工具叫什么、是做什么的。
数据流:进去的是 V8 作用域。它从 RuntimeState 读取启用工具列表,为每个工具做一个小对象,里面有 name 和 description;再把这些小对象按顺序放进一个 JavaScript 数组。出来的是这个数组形式的值;如果中途创建字符串或写入数组失败,就返回错误文字。
调用关系:install_globals 会调用它,然后把结果挂到全局变量 ALL_TOOLS 上。它和 build_tools_object 配合:一个提供可调用的工具入口,一个提供给人和脚本看的工具目录。
调用图:被 1 处调用(install_globals);外部调用 3 个(new, new, new)。
helper_function101–117 ↗
fn helper_function(
scope: &mut v8::PinScope<'s, '_>,
name: &str,
callback: F,
) -> Result<v8::Local<'s, v8::Function>, String>
作用:这个函数把 Rust 里的回调函数包装成 JavaScript 能调用的函数。比如把 text_callback 包成全局的 text,把 exit_callback 包成全局的 exit。
数据流:进去的是 V8 作用域、要显示给 JavaScript 的函数名,以及真正要执行的回调。它先创建函数名字符串,再用 V8 的函数模板把回调包起来,并把函数名作为附带数据放进去。出来的是一个 JavaScript 函数;如果创建失败,就返回错误文字。
调用关系:install_globals 用它反复创建 clearTimeout、setTimeout、text、image、store、load、notify、yield_control、exit 等辅助函数。它是通用包装器,负责把后面的 callbacks 接到 JavaScript 世界。
调用图:被 1 处调用(install_globals);外部调用 2 个(builder, new)。
tool_function119–131 ↗
fn tool_function(
scope: &mut v8::PinScope<'s, '_>,
tool_index: usize,
) -> Result<v8::Local<'s, v8::Function>, String>
作用:这个函数给某一个外部工具创建一个 JavaScript 函数。它会把工具在列表里的编号藏进函数里,之后真正调用时就能知道用户点的是哪个工具。
数据流:进去的是 V8 作用域和工具编号 tool_index。它把编号转成字符串,作为函数的附带数据,然后用 tool_callback 创建一个 JavaScript 函数。出来的是这个工具函数;如果编号字符串或函数创建失败,就返回错误文字。
调用关系:build_tools_object 在遍历启用工具时调用它。它创建出来的函数会被放进 tools 对象里,之后脚本调用工具时,后续执行会走到 tool_callback。
调用图:被 1 处调用(build_tools_object);外部调用 2 个(builder, new)。
set_global133–146 ↗
fn set_global(
scope: &mut v8::PinScope<'s, '_>,
global: v8::Local<'s, v8::Object>,
name: &str,
value: v8::Local<'s, v8::Value>,
) -> Result<(), String>
作用:这个函数专门负责把一个值放到 JavaScript 的全局对象上。它像贴标签:把名字和对应的函数或对象挂到脚本一眼能看到的地方。
数据流:进去的是 V8 作用域、全局对象、要设置的名字和对应的值。它先把名字变成 JavaScript 字符串,再调用 V8 的 set 写入全局对象。写成功就返回空结果;写失败就返回带具体名字的错误文字。
调用关系:install_globals 在最后安装 tools、ALL_TOOLS 和各种辅助函数时反复调用它。它把前面构造好的零件真正装到全局对象上。
调用图:被 1 处调用(install_globals);外部调用 3 个(set, format!, new)。
delete_global148–160 ↗
fn delete_global(
scope: &mut v8::PinScope<'s, '_>,
global: v8::Local<'s, v8::Object>,
name: &str,
) -> Result<(), String>
作用:这个函数专门从 JavaScript 的全局对象里删掉某个名字。它用于在脚本开始前收起不希望脚本使用的默认功能。
数据流:进去的是 V8 作用域、全局对象和要删除的名字。它先把名字变成 JavaScript 字符串,再调用 V8 的 delete 删除对应属性。删除成功就返回空结果;删除失败就返回说明删哪个名字失败的错误文字。
调用关系:install_globals 一开始会调用它删除 console、Atomics、SharedArrayBuffer、WebAssembly。它是这个文件里“先清场、再布置”流程中的清场步骤。
调用图:被 1 处调用(install_globals);外部调用 3 个(delete, format!, new)。
code-mode/src/runtime/callbacks.rs源码 ↗
这份文件像一排服务窗口。脚本运行在 V8 里,V8 是执行 JavaScript 的引擎;Rust 运行时在外面掌控工具调用、事件发送和状态保存。这里的每个 callback 都是在脚本调用某个内置函数时被触发的“接线员”。它先把 JavaScript 传进来的值检查清楚,必要时转成 JSON 这种通用数据格式;如果参数不对,就抛出类型错误,避免脏数据继续往下走。然后它会把结果发成 RuntimeEvent,比如工具调用、输出内容、通知、让出控制权等。比较重要的是 tool_callback:它会立刻还给脚本一个 Promise(以后才会有结果的票据),同时把真正的工具请求发出去。store/load 则像一个小抽屉,用来保存可序列化的值。exit_callback 用一个特殊异常当“停车信号”,让外层运行循环知道该停了。
tool_callback13–72 ↗
fn tool_callback(
scope: &mut v8::PinScope<'_, '_>,
args: v8::FunctionCallbackArguments,
mut retval: v8::ReturnValue<v8::Value>,
)
作用:当脚本想调用外部工具时,这个函数负责把请求交给 Rust 运行时,并马上给脚本一张 Promise“取号单”。工具真正完成后,运行时再用这张单子把结果还回去。
数据流:进去的是 V8 传来的回调参数:隐藏数据里有工具编号,第一个参数是工具输入。它先把工具编号读出来,把输入从 JavaScript 值转成 JSON;再创建一个 Promise 和对应的 resolver,把 resolver 存进 pending_tool_calls;最后发出 RuntimeEvent::ToolCall。出来的是返回给脚本的 Promise,同时运行时状态里多了一条待完成的工具调用记录。
调用关系:它是脚本调用工具时进入 Rust 世界的入口。它会用 v8_value_to_json 做数据转换,用 throw_type_error 报参数错误,并把后续工作交给事件接收方;接收方完成工具后,会通过之前保存的 resolver 回填 Promise。
调用图:调用 2 个内部函数(throw_type_error, v8_value_to_json);外部调用 7 个(data, get, length, set, format!, new, new)。
text_callback74–97 ↗
fn text_callback(
scope: &mut v8::PinScope<'_, '_>,
args: v8::FunctionCallbackArguments,
mut retval: v8::ReturnValue<v8::Value>,
)
作用:当脚本要输出一段文字内容时,这个函数把任意传入值整理成文本,并发送给外层系统。它让脚本不用关心底层事件格式。
数据流:进去的是脚本传来的第一个值;没有传值时按 undefined 处理。它调用 serialize_output_text 把这个值变成可输出的字符串;成功后发送 RuntimeEvent::ContentItem,内容类型是 InputText。出来没有有意义的返回值,只返回 undefined,但外层会收到一条文字输出事件。
调用关系:它位于脚本输出和协议内容项之间。它依赖 serialize_output_text 做“值到文字”的整理,出错时用 throw_type_error 停住;整理好的内容交给 RuntimeState 里的 event_tx 发出去。
调用图:调用 2 个内部函数(serialize_output_text, throw_type_error);外部调用 5 个(get, length, set, ContentItem, undefined)。
image_callback99–130 ↗
fn image_callback(
scope: &mut v8::PinScope<'_, '_>,
args: v8::FunctionCallbackArguments,
mut retval: v8::ReturnValue<v8::Value>,
)
作用:当脚本要输出一张图片时,这个函数把传入的图片描述整理成统一格式,并允许附带一个清晰度之类的 detail 字符串。这样外层系统收到的图片总是规整的。
数据流:进去的是图片值,以及可选的第二个参数 detail。它先检查 detail 只能是字符串、null 或 undefined;然后调用 normalize_output_image 把图片值规范化成 FunctionCallOutputContentItem。出来是 undefined,但运行时会发送一条图片内容事件。
调用关系:它是普通图片输出的通道。它把参数检查做在前面,把真正的图片格式整理交给 normalize_output_image;整理成功后,通过 RuntimeEvent::ContentItem 交给外层。
调用图:调用 2 个内部函数(normalize_output_image, throw_type_error);外部调用 5 个(get, length, set, ContentItem, undefined)。
generated_image_callback132–162 ↗
fn generated_image_callback(
scope: &mut v8::PinScope<'_, '_>,
args: v8::FunctionCallbackArguments,
mut retval: v8::ReturnValue<v8::Value>,
)
作用:当脚本输出“图片生成工具”的结果时,这个函数既发送图片,也可能额外发送一段提示文字。它专门照顾那种结果对象里带 output_hint 的图片生成场景。
数据流:进去的是一个图片生成结果对象。它先用 generated_image_output_hint 读取可选的 output_hint 字段,再用 normalize_output_image 把同一个值整理成图片内容项;随后发送图片事件,如果有提示文字,再额外发送一条文字内容事件。出来是 undefined,副作用是外层收到一张图片和可能的一段说明。
调用关系:它是 generatedImage 这类脚本辅助函数背后的接线员。它先把读取提示文字的小活交给 generated_image_output_hint,再把图片整理交给 normalize_output_image,最后通过事件通道把两类内容送出去。
调用图:调用 3 个内部函数(generated_image_output_hint, normalize_output_image, throw_type_error);外部调用 5 个(get, length, set, ContentItem, undefined)。
generated_image_output_hint164–182 ↗
fn generated_image_output_hint(
scope: &mut v8::PinScope<'_, '_>,
value: v8::Local<'_, v8::Value>,
) -> Result<Option<String>, String>
作用:这个小函数只做一件事:从图片生成结果对象里读出 output_hint。它保证这个字段要么不存在,要么必须是字符串。
数据流:进去的是一个 V8 值。它先确认这个值能当对象用,然后读取对象上的 output_hint;如果字段不存在,就返回 None;如果字段是字符串,就返回 Some(字符串);如果类型不对,就返回错误文字。它不改动运行时状态。
调用关系:它只被 generated_image_callback 使用,是那个函数的参数检查和字段读取助手。它不负责发事件,只负责把 output_hint 这块数据安全地取出来。
调用图:被 1 处调用(generated_image_callback);外部调用 2 个(try_from, new)。
store_callback184–215 ↗
fn store_callback(
scope: &mut v8::PinScope<'_, '_>,
args: v8::FunctionCallbackArguments,
_retval: v8::ReturnValue<v8::Value>,
)
作用:当脚本想临时保存一个值,之后再取出来用时,这个函数把值放进运行时的小仓库。它只接受能安全转成 JSON 的普通数据,避免保存函数、复杂对象这类以后还原不了的东西。
数据流:进去的是 key 和 value。它先把 key 转成字符串,再把 value 用 v8_value_to_json 转成 JSON;如果值不能序列化,就抛出清楚的错误。成功后,它同时写入 stored_values 和 stored_value_writes,前者供之后读取,后者记录本轮写过什么。这个函数没有返回有用值。
调用关系:它是脚本写入持久化/临时状态的入口。它把复杂的 JavaScript 值筛成可保存的 JSON;读取的另一半由 load_callback 完成。
调用图:调用 2 个内部函数(throw_type_error, v8_value_to_json);外部调用 2 个(get, format!)。
load_callback217–242 ↗
fn load_callback(
scope: &mut v8::PinScope<'_, '_>,
args: v8::FunctionCallbackArguments,
mut retval: v8::ReturnValue<v8::Value>,
)
作用:当脚本想按 key 取回之前保存的值时,这个函数从运行时小仓库里读取,并转回 JavaScript 能用的值。找不到时不会报错,而是返回 undefined。
数据流:进去的是 key。它把 key 转成字符串,到 RuntimeState 的 stored_values 里查;如果没有,就返回 undefined;如果有,就用 json_to_v8 把 JSON 转成 V8 值并返回给脚本。它不修改存储内容。
调用关系:它和 store_callback 是一对:store 负责放进去,load 负责拿出来。它把数据格式还原的工作交给 json_to_v8,出错时用 throw_type_error 告诉脚本加载失败。
调用图:调用 2 个内部函数(json_to_v8, throw_type_error);外部调用 3 个(get, set, undefined)。
notify_callback244–272 ↗
fn notify_callback(
scope: &mut v8::PinScope<'_, '_>,
args: v8::FunctionCallbackArguments,
mut retval: v8::ReturnValue<v8::Value>,
)
作用:当脚本想给外层发一条即时提示消息时,这个函数负责发送通知。它要求消息不能为空,避免发出没有意义的空提示。
数据流:进去的是脚本传来的一个值;没有传值时按 undefined 处理。它先用 serialize_output_text 转成文本,再检查去掉空白后是否为空;如果有效,就发送 RuntimeEvent::Notify,并带上当前 tool_call_id。出来是 undefined,副作用是外层收到通知。
调用关系:它是脚本向宿主系统“打招呼”的通道。它复用 serialize_output_text 做文字整理,用 throw_type_error 阻止空消息,最后把通知交给 event_tx。
调用图:调用 2 个内部函数(serialize_output_text, throw_type_error);外部调用 4 个(get, length, set, undefined)。
set_timeout_callback274–288 ↗
fn set_timeout_callback(
scope: &mut v8::PinScope<'_, '_>,
args: v8::FunctionCallbackArguments,
mut retval: v8::ReturnValue<v8::Value>,
)
作用:这个函数给脚本提供类似 setTimeout 的能力:过一段时间后再执行某个动作。它把定时安排交给 timers 模块,并把定时器编号还给脚本。
数据流:进去的是脚本传来的定时器参数,比如回调和等待时间。它调用 timers::schedule_timeout 安排定时任务;成功后得到 timeout_id,并把它作为数字返回。失败时抛出类型错误,不会安排定时器。
调用关系:它是脚本使用定时器时的入口。它自己不维护时间轮或等待队列,而是把具体调度交给 schedule_timeout;脚本之后可以把返回的编号交给 clear_timeout_callback 取消。
调用图:调用 2 个内部函数(schedule_timeout, throw_type_error);外部调用 2 个(set, new)。
clear_timeout_callback290–301 ↗
fn clear_timeout_callback(
scope: &mut v8::PinScope<'_, '_>,
args: v8::FunctionCallbackArguments,
mut retval: v8::ReturnValue<v8::Value>,
)
作用:这个函数用来取消之前 set_timeout_callback 安排的定时任务。它让脚本可以说“刚才那个闹钟不用响了”。
数据流:进去的是脚本传来的取消参数,通常包含定时器编号。它调用 timers::clear_timeout 去查找并取消对应任务;如果参数不对或取消失败,就抛出类型错误。成功后返回 undefined。
调用关系:它是 set_timeout_callback 的配套函数。真正的取消动作在 timers::clear_timeout 里完成,它这里只负责把脚本请求接进来、处理错误、给脚本一个普通的 undefined 返回。
调用图:调用 2 个内部函数(clear_timeout, throw_type_error);外部调用 2 个(set, undefined)。
yield_control_callback303–311 ↗
fn yield_control_callback(
scope: &mut v8::PinScope<'_, '_>,
_args: v8::FunctionCallbackArguments,
_retval: v8::ReturnValue<v8::Value>,
)
作用:这个函数让脚本主动告诉运行时:我现在可以暂停一下,把控制权让出去。这样长时间运行的脚本不会一直霸占执行机会。
数据流:进去不需要实际参数。它从当前 V8 scope 里取 RuntimeState,如果能取到,就发送 RuntimeEvent::YieldRequested。它不返回有意义的值,也不直接暂停脚本;真正怎么让出控制权由外层事件处理决定。
调用关系:它是脚本和运行循环之间的礼貌让路信号。它不调用其他辅助函数,只把请求放进 event_tx,让外层调度逻辑看到后处理。
exit_callback313–324 ↗
fn exit_callback(
scope: &mut v8::PinScope<'_, '_>,
_args: v8::FunctionCallbackArguments,
_retval: v8::ReturnValue<v8::Value>,
)
作用:这个函数让脚本请求结束运行。它先在运行时状态里打上“要退出”的标记,然后抛出一个特殊异常,把执行流程从 V8 里打断出来。
数据流:进去不需要实际参数。它如果能拿到 RuntimeState,就把 exit_requested 设为 true;然后创建包含 EXIT_SENTINEL 的字符串异常并抛出。出来不是正常返回,而是通过异常让外层知道该停止。
调用关系:它是脚本主动终止的紧急停车按钮。它不把工作交给普通事件通道,而是用特殊异常作为信号;外层运行逻辑看到这个哨兵值,就能把它和普通程序错误区分开。
调用图:外部调用 2 个(throw_exception, new)。
core/src/tools/code_mode/mod.rs源码 ↗
可以把这个文件理解成“代码执行间的前台服务台”。模型说要运行代码时,它通过 CodeModeService 把请求交给真正的代码运行会话;代码运行中如果需要调用别的工具,它又把这个“嵌套工具调用”转交给普通工具路由器。等运行环境回消息后,这里会把原始结果翻译成统一的工具输出:先加上“脚本正在跑 / 已完成 / 失败 / 已终止”的状态,再加耗时,再清理不该给出的原图细节,最后按 token(模型能处理的文字单位)上限截断,避免一次输出把上下文塞爆。它还维护每一轮对话里的派发 worker,让代码模式只在允许的工具模式下启动。没有这个文件,代码模式会和普通工具系统脱节,结果格式不统一,也更容易出现无限自调用或超长输出这类麻烦。
is_exec_tool_name50–52 ↗
fn is_exec_tool_name(tool_name: &ToolName) -> bool
作用:判断一个工具名是不是公开给模型使用的代码执行工具 exec。这个检查很重要,因为代码模式不能再调用自己,否则可能像两面镜子互照一样无限套娃。
数据流:进去一个工具名 → 看它有没有命名空间、名字是不是代码模式的公开工具名 → 出来 true 或 false,表示它是不是那个特殊的 exec 工具。
调用关系:它主要被 call_nested_tool 使用;当代码运行过程中想调用别的工具时,先用它拦住“exec 调 exec”的危险情况。
调用图:被 1 处调用(call_nested_tool)。
CodeModeService::new66–74 ↗
fn new() -> Self
作用:创建代码模式服务,并把它和内部的派发中介连接起来。外部要使用代码模式时,通常先需要这个服务对象。
数据流:进去没有额外参数 → 新建一个 CodeModeDispatchBroker,再用这个 broker 创建真正的 codex_code_mode 服务会话 → 出来一个 CodeModeService,里面保存会话和派发中介。
调用关系:这是整个代码模式服务的组装点;它调用底层的 with_delegate 和 broker 的 new,把“运行代码”和“把嵌套工具调用派出去”两部分接好。
调用图:调用 2 个内部函数(with_delegate, new);外部调用 1 个(new)。
CodeModeService::execute76–81 ↗
async fn execute(
&self,
request: codex_code_mode::ExecuteRequest,
) -> Result<codex_code_mode::StartedCell, String>
作用:启动一段代码执行。调用者给它一个执行请求,它会交给底层代码模式会话去跑。
数据流:进去一个 ExecuteRequest → 先通过 session 确认代码模式可用 → 把请求交给底层会话执行 → 出来 StartedCell,表示一个代码单元已经启动,或者返回错误文字。
调用关系:它是对底层 CodeModeSession 的薄封装;真正干活的是 session 返回的会话对象,session 不存在时会直接报“code mode is unavailable”。
调用图:调用 1 个内部函数(session)。
CodeModeService::wait83–88 ↗
async fn wait(
&self,
request: codex_code_mode::WaitRequest,
) -> Result<codex_code_mode::WaitOutcome, String>
作用:等待某个正在运行的代码单元产出结果或阶段性输出。它让调用者不用直接接触底层运行会话。
数据流:进去一个 WaitRequest → 找到当前代码模式会话 → 调用底层 wait → 出来 WaitOutcome,表示等到了结果、让出执行权、或其他等待结果,也可能返回错误。
调用关系:它和 execute、terminate 一样,都先走 CodeModeService::session;这是对外提供的等待入口。
调用图:调用 1 个内部函数(session)。
CodeModeService::terminate90–95 ↗
async fn terminate(
&self,
cell_id: CellId,
) -> Result<codex_code_mode::WaitOutcome, String>
作用:停止某个正在运行的代码单元。用户或系统需要中断脚本时会走这里。
数据流:进去一个 cell_id,也就是代码单元的编号 → 找到当前代码模式会话 → 调用底层 terminate → 出来 WaitOutcome,描述终止后的状态,或返回错误。
调用关系:它通过 CodeModeService::session 找到底层会话,再把终止请求交过去;和 wait 使用的是同一套底层代码运行机制。
调用图:调用 1 个内部函数(session)。
CodeModeService::shutdown97–102 ↗
async fn shutdown(&self) -> Result<(), String>
作用:关闭代码模式服务,释放底层会话占用的资源。程序结束或代码模式不再需要时会用到。
数据流:进去没有参数 → 如果有底层会话,就调用它的 shutdown;如果没有会话,就什么也不做 → 出来成功或错误。
调用关系:它是收尾动作,不参与单次执行结果处理;负责把 CodeModeService 创建出来的底层运行服务安全关掉。
CodeModeService::mark_cell_ready_for_dispatch104–106 ↗
fn mark_cell_ready_for_dispatch(&self, cell_id: &codex_code_mode::CellId)
作用:标记某个代码单元已经准备好派发它发起的工具调用。也就是说,这个代码单元现在可以把活儿交给外部工具系统了。
数据流:进去一个 cell_id → 通知 dispatch_broker 这个代码单元可以派发 → 它不返回新数据,但改变 broker 内部的派发状态。
调用关系:它把状态变化告诉 CodeModeDispatchBroker;后续派发 worker 才能知道哪些代码单元的嵌套工具调用可以继续处理。
CodeModeService::finish_cell_dispatch108–110 ↗
fn finish_cell_dispatch(&self, cell_id: &CellId)
作用:告诉派发中介:某个代码单元的派发工作结束了,可以关掉相关通道或状态。这样可以避免旧任务一直占着位置。
数据流:进去一个 cell_id → 调用 dispatch_broker.close_cell → 不返回内容,但会清理这个代码单元在派发系统里的状态。
调用关系:它通常和 mark_cell_ready_for_dispatch 成对出现;一个表示可以开始派发,一个表示这个代码单元的派发可以收尾。
CodeModeService::start_turn_worker112–133 ↗
fn start_turn_worker(
&self,
session: &Arc<Session>,
turn: &Arc<TurnContext>,
router: Arc<ToolRouter>,
tracker: SharedTurnDiffTracker,
) -> Option<CodeModeD
作用:在一轮对话开始时,为代码模式启动一个派发 worker。这个 worker 像临时值班员,专门处理代码运行过程中冒出来的工具调用。
数据流:进去当前会话、当前轮次、工具路由器和变更追踪器 → 先检查本轮是否允许代码模式,以及代码模式服务是否可用 → 如果允许,就复制必要引用并启动 worker;否则返回 None。
调用关系:它连接 TurnContext、ToolRouter 和 CodeModeDispatchBroker;只有 ToolMode 是 CodeMode 或 CodeModeOnly 时才会启动,避免普通工具模式下误开代码模式派发。
CodeModeService::session135–139 ↗
fn session(&self) -> Result<&Arc<dyn CodeModeSession>, String>
作用:取出底层代码模式会话,并在不可用时给出清楚错误。它让 execute、wait、terminate 不用重复写同样的检查。
数据流:进去当前 CodeModeService → 查看里面的 session 是否存在 → 出来会话引用;如果没有,就出来错误字符串“code mode is unavailable”。
调用关系:它被 execute、wait、terminate 调用,是这些公开操作进入底层 CodeModeSession 前的守门员。
handle_runtime_response142–186 ↗
async fn handle_runtime_response(
exec: &ExecContext,
response: RuntimeResponse,
max_output_tokens: Option<usize>,
started_at: std::time::Instant,
) -> Result<FunctionToolOutput, Strin
作用:把代码运行环境返回的原始消息,整理成主系统统一的工具输出。它会加状态、加耗时、清理图片细节、截断超长内容,并标出成功或失败。
数据流:进去执行上下文、RuntimeResponse、最大输出 token 数和开始时间 → 先生成脚本状态文字,再把运行环境的内容转成工具输出项,按权限清理图片细节,必要时追加错误文字,截断过长输出,并在最前面插入状态和耗时 → 出来 FunctionToolOutput,供模型继续阅读。
调用关系:它是代码执行结果回到模型前的最后整理站;它依次调用 format_script_status、into_function_call_output_content_items、sanitize_runtime_image_detail、truncate_code_mode_result 和 prepend_script_status。
调用图:调用 6 个内部函数(format_script_status, prepend_script_status, into_function_call_output_content_items, sanitize_runtime_image_detail, truncate_code_mode_result, from_content);外部调用 2 个(elapsed, format!)。
sanitize_runtime_image_detail188–190 ↗
fn sanitize_runtime_image_detail(turn: &TurnContext, items: &mut [FunctionCallOutputContentItem])
作用:检查代码输出里的图片内容,去掉当前模型不应该请求或看到的原图细节。这样可以避免把过高细节的图片信息误传给不支持或不允许的模型。
数据流:进去当前轮次信息和一组输出项 → 根据 turn.model_info 判断能不能请求原图细节 → 调用清理函数原地修改这些输出项 → 不额外返回数据。
调用关系:它被 handle_runtime_response 调用;每次代码运行结果要发回模型前,都会经过这一步做图片细节安全处理。
调用图:被 1 处调用(handle_runtime_response);外部调用 2 个(can_request_original_image_detail, sanitize_original_image_detail)。
format_script_status192–206 ↗
fn format_script_status(response: &RuntimeResponse) -> String
作用:把运行环境的状态变成一句人能看懂的提示,比如“脚本正在运行”“已完成”“失败”。这句话会放在输出最前面。
数据流:进去一个 RuntimeResponse → 根据它是 Yielded、Terminated 还是 Result,并检查有没有错误文字 → 出来一段状态字符串。
调用关系:它被 handle_runtime_response 调用;后者再把这段状态交给 prepend_script_status 放进最终输出。
调用图:被 1 处调用(handle_runtime_response);外部调用 1 个(format!)。
prepend_script_status208–216 ↗
fn prepend_script_status(
content_items: &mut Vec<FunctionCallOutputContentItem>,
status: &str,
wall_time: Duration,
)
作用:在代码输出最前面加一个小标题,告诉读者脚本状态、花了多久、下面才是输出内容。这样模型和人都更容易理解这次执行发生了什么。
数据流:进去输出项列表、状态文字和耗时 → 把耗时四舍五入到 0.1 秒 → 拼成头部文本并插入到列表第一个位置 → 原来的输出被往后挪。
调用关系:它由 handle_runtime_response 调用,是最终结果包装的一步;前面的 format_script_status 负责给它状态文字。
调用图:被 1 处调用(handle_runtime_response);外部调用 2 个(as_secs_f32, format!)。
truncate_code_mode_result218–234 ↗
fn truncate_code_mode_result(
items: Vec<FunctionCallOutputContentItem>,
max_output_tokens: Option<usize>,
) -> Vec<FunctionCallOutputContentItem>
作用:把代码执行输出裁短到允许的大小,防止太长的日志或结果把模型上下文挤满。token 可以简单理解成模型读文字时的“小块单位”。
数据流:进去一组工具输出项和可选的最大 token 数 → 先算出实际上限并生成截断策略 → 如果全是文字,就用更适合文字的格式化截断;如果混有图片等其他内容,就用通用工具输出截断 → 出来裁剪后的输出项列表。
调用关系:它被 handle_runtime_response 调用;这是结果发回模型前的容量保护步骤,内部会用 resolve_max_tokens 和截断工具库。
调用图:调用 1 个内部函数(resolve_max_tokens);被 1 处调用(handle_runtime_response);外部调用 3 个(formatted_truncate_text_content_items_with_policy, truncate_function_output_items_with_policy, Tokens)。
call_nested_tool236–276 ↗
async fn call_nested_tool(
_exec: ExecContext,
tool_runtime: ToolCallRuntime,
invocation: CodeModeNestedToolCall,
cancellation_token: CancellationToken,
) -> Result<JsonValue, Function
作用:当代码模式里的一段代码想调用其他工具时,这个函数负责代它转交给主系统的工具运行器。它还会拦住代码模式调用自己,避免无限递归。
数据流:进去执行上下文、工具运行器、嵌套工具调用请求和取消令牌 → 拆出 cell_id、工具名、输入等信息 → 如果目标是 exec 自己就返回给模型的错误 → 否则把输入包装成 ToolPayload,生成一个新的工具调用 ID,交给工具运行器执行 → 出来适合代码模式使用的 JSON 结果,或工具错误。
调用关系:它是代码模式和普通工具系统之间的桥;先用 is_exec_tool_name 做安全检查,再用 build_nested_tool_payload 准备载荷,最后调用 handle_tool_call_with_source,并标明来源是 CodeMode。
调用图:调用 3 个内部函数(build_nested_tool_payload, is_exec_tool_name, handle_tool_call_with_source);外部调用 2 个(format!, RespondToModel)。
build_nested_tool_payload278–287 ↗
fn build_nested_tool_payload(
tool_kind: CodeModeToolKind,
tool_name: &ToolName,
input: Option<JsonValue>,
) -> Result<ToolPayload, String>
作用:根据工具类型,把代码模式传来的输入包装成主工具系统能识别的格式。不同工具吃的输入形状不一样,这里负责分流。
数据流:进去工具种类、工具名和可选 JSON 输入 → 如果是 Function 工具,就按函数参数对象处理;如果是 Freeform 工具,就按自由文本处理 → 出来 ToolPayload,或说明输入格式不对的错误文字。
调用关系:它被 call_nested_tool 使用,也被测试直接验证;内部把活儿交给 build_function_tool_payload 或 build_freeform_tool_payload。
调用图:调用 2 个内部函数(build_freeform_tool_payload, build_function_tool_payload);被 3 处调用(call_nested_tool, build_nested_tool_payload_uses_freeform_kind, build_nested_tool_payload_uses_function_kind)。
build_function_tool_payload289–295 ↗
fn build_function_tool_payload(
tool_name: &ToolName,
input: Option<JsonValue>,
) -> Result<ToolPayload, String>
作用:为“函数式工具”准备调用载荷。函数式工具需要一串 JSON 参数文本,就像填表单时要给出字段和值。
数据流:进去工具名和可选 JSON 输入 → 调用 serialize_function_tool_arguments 把参数变成字符串 → 包成 ToolPayload::Function → 出来可交给工具路由器的载荷,或错误。
调用关系:它由 build_nested_tool_payload 在工具种类为 Function 时调用;真正检查和序列化参数的工作交给 serialize_function_tool_arguments。
调用图:调用 1 个内部函数(serialize_function_tool_arguments);被 1 处调用(build_nested_tool_payload)。
serialize_function_tool_arguments297–309 ↗
fn serialize_function_tool_arguments(
tool_name: &ToolName,
input: Option<JsonValue>,
) -> Result<String, String>
作用:把函数式工具的 JSON 参数转成字符串,并确保参数必须是 JSON 对象。JSON 对象可以理解成一组“字段名: 值”的表格。
数据流:进去工具名和可选 JSON 输入 → 没有输入就变成空对象字符串 {};如果输入是对象,就序列化成 JSON 文本;如果是字符串、数字、数组等非对象,就返回“这个工具需要 JSON 对象”的错误 → 出来参数字符串或错误。
调用关系:它被 build_function_tool_payload 调用;这里是函数式嵌套工具输入格式的主要把关点。
调用图:被 1 处调用(build_function_tool_payload);外部调用 3 个(Object, format!, to_string)。
build_freeform_tool_payload311–319 ↗
fn build_freeform_tool_payload(
tool_name: &ToolName,
input: Option<JsonValue>,
) -> Result<ToolPayload, String>
作用:为“自由文本工具”准备调用载荷。这类工具不收字段表,而是直接收一段字符串输入。
数据流:进去工具名和可选 JSON 输入 → 如果输入是字符串,就取出字符串并包成 ToolPayload::Custom;否则返回“这个工具需要字符串输入”的错误 → 出来载荷或错误。
调用关系:它由 build_nested_tool_payload 在工具种类为 Freeform 时调用;和 build_function_tool_payload 分别处理两种工具输入风格。
调用图:被 1 处调用(build_nested_tool_payload);外部调用 1 个(format!)。
tests::build_nested_tool_payload_uses_function_kind332–346 ↗
fn build_nested_tool_payload_uses_function_kind()
作用:测试函数式嵌套工具会把 JSON 对象正确变成参数字符串。它防止以后改代码时把 Function 工具的输入格式弄坏。
数据流:进去测试里构造的工具种类 Function、工具名 example 和 JSON 对象 { value: 1 } → 调用 build_nested_tool_payload → 检查出来的是 Function 载荷,而且参数字符串等于 {"value":1}。
调用关系:这是 build_nested_tool_payload 的单元测试之一;它覆盖 Function 分支,并间接验证 build_function_tool_payload 和 serialize_function_tool_arguments。
调用图:调用 2 个内部函数(build_nested_tool_payload, plain);外部调用 3 个(assert_eq!, json!, panic!)。
tests::build_nested_tool_payload_uses_freeform_kind349–363 ↗
fn build_nested_tool_payload_uses_freeform_kind()
作用:测试自由文本嵌套工具会保留原始字符串输入。它确保 Freeform 工具不会被错误地当成 JSON 参数对象处理。
数据流:进去测试构造的工具种类 Freeform、工具名 example 和字符串 hello → 调用 build_nested_tool_payload → 检查出来的是 Custom 载荷,而且 input 仍然是 hello。
调用关系:这是 build_nested_tool_payload 的另一个单元测试;它覆盖 Freeform 分支,并间接验证 build_freeform_tool_payload。
调用图:调用 2 个内部函数(build_nested_tool_payload, plain);外部调用 3 个(assert_eq!, json!, panic!)。
tests::truncated_text_output_starts_with_warning366–382 ↗
fn truncated_text_output_starts_with_warning()
作用:测试文字输出被截断时,结果最前面会出现清楚的警告。这样模型不会误以为自己看到了完整输出。
数据流:进去一段很长的文本输出和很小的 token 上限 → 调用 truncate_code_mode_result → 检查返回内容包含截断警告、原始 token 数、总行数和被压缩后的文本片段。
调用关系:这是 truncate_code_mode_result 的安全网;它确认纯文本输出会走格式化截断路径,并保留提示信息。
调用图:外部调用 2 个(assert_eq!, vec!)。
core/src/tools/code_mode/execute_spec.rs源码 ↗
这个文件解决的是“怎么把代码模式执行能力交给工具系统使用”的问题。它创建一个自由文本工具,也就是输入不是固定表单,而是一段源码式文本。为了避免乱输,它给这段文本配了一份语法规则:可以是普通源码,也可以第一行写一个类似“// @exec:...”的执行提示,后面再跟真正内容。它还会根据当前启用的代码模式工具、命名空间说明、是否只允许代码模式、是否支持延后工具,生成一段人能看懂的工具描述。可以把它想成给机器做一张“使用说明牌”:名字、说明、允许的输入格式都写清楚。文件里的测试则像验收单,确认生成出来的说明牌和预期完全一致。
create_code_mode_tool7–37 ↗
fn create_code_mode_tool(
enabled_tools: &[CodeModeToolDefinition],
namespace_descriptions: &BTreeMap<String, codex_code_mode::ToolNamespaceDescription>,
code_mode_only: bool,
deferred
作用:这个函数用来生成代码模式的工具规格。调用方给它当前可用的工具和一些开关,它就返回一份系统能登记、模型能调用的工具说明。
数据流:进去的是已启用的代码模式工具列表、命名空间说明、两个布尔开关,以及函数内部写死的一段输入语法规则。它先调用外部的 build_exec_tool_description 生成工具说明文字,再把公开工具名、说明文字、语法类型、语法内容装进一个 FreeformTool,最后包成 ToolSpec::Freeform 返回。它不改外部数据,只产出一份新的工具规格。
调用关系:它处在“工具注册”这一步,通常由更上层的工具组装流程调用。它把生成说明文字的工作交给 codex_code_mode::build_exec_tool_description,又把结果放进 codex_tools 提供的 FreeformTool 结构里,让后面的工具系统可以统一识别和使用。
调用图:外部调用 2 个(build_exec_tool_description, Freeform)。
tests::create_code_mode_tool_matches_expected_spec46–87 ↗
fn create_code_mode_tool_matches_expected_spec()
作用:这是一个测试,确认 create_code_mode_tool 生成的工具规格没有走样。它保护的是工具名字、说明生成方式和输入语法这些关键约定。
数据流:进去的是测试里临时造出的一个示例工具 update_plan,以及空的命名空间说明和两个固定开关。测试调用 create_code_mode_tool 得到实际结果,再手写一份期望结果,用 assert_eq! 比较两边是否完全一样。测试通过表示生成逻辑符合预期;测试失败则说明输出格式、说明文字或语法规则被改坏了。
调用关系:它由 Rust 的测试运行器在跑测试时调用,不参与正常运行。它用 vec! 构造示例工具列表,用 assert_eq! 做最终检查,主要是在开发者改动 create_code_mode_tool 时及时报警。
调用图:外部调用 2 个(assert_eq!, vec!)。
core/src/tools/code_mode/execute_handler.rs源码 ↗
可以把这个文件想成一个“代码执行窗口”的前台接待员。模型调用公开工具时,传进来的应该是一段原始 JavaScript 文本;这里先确认它确实是代码模式的执行请求,再把文本解析成执行参数,比如代码本身、最多输出多少内容、运行到一半是否先让出结果。接着它把请求交给 code_mode_service,也就是实际运行代码的后台服务。运行开始后,它会登记一次代码单元追踪,方便之后知道哪次工具调用跑了哪段代码、返回了什么。如果运行第一次响应就结束了,它还会标记这次派发完成;如果只是“暂时让出”,代码还可能继续跑。最后,它调用统一的响应处理函数,把运行时返回的原始结果变成工具输出。没有这个文件,模型虽然能提出“执行代码”的请求,但系统不知道该如何验收、派发、追踪和返回结果。
CodeModeExecuteHandler::new22–27 ↗
fn new(spec: ToolSpec, nested_tool_specs: Vec<ToolSpec>) -> Self
作用:创建一个代码模式执行处理器。调用方把这个公开工具自己的说明书和它内部可用的嵌套工具说明书交进来,后面执行代码时会用到。
数据流:进去的是一个工具规格 spec 和一组 nested_tool_specs → 函数把它们原样放进 CodeModeExecuteHandler 结构里 → 出来的是一个已经准备好的处理器对象,不会启动执行,也不会改外部状态。
调用关系:这是整个处理器的组装入口。系统注册工具时会先用它造出 CodeModeExecuteHandler,之后 tool_name、spec、handle、matches_kind 等方法都基于这个对象工作。
CodeModeExecuteHandler::execute29–91 ↗
async fn execute(
&self,
session: std::sync::Arc<crate::session::session::Session>,
turn: std::sync::Arc<crate::session::turn_context::TurnContext>,
call_id: String,
作用:真正执行一段代码模式里的 JavaScript。它负责把源码解析成参数,交给运行服务,记录这次代码单元的追踪信息,并把运行结果变成工具输出。
数据流:进去的是当前会话 session、当前轮次 turn、工具调用编号 call_id 和代码文本 code → 它先用 parse_exec_source 解析代码和执行选项,再收集允许代码调用的内部工具定义,然后向 code_mode_service 发出 ExecuteRequest → 服务返回一个已启动的代码单元后,它记录追踪、标记可派发、等待第一次运行响应 → 如果响应不是“暂时让出”,它还记录结束并完成派发 → 最后把响应交给 handle_runtime_response,出来的是 FunctionToolOutput;如果解析或运行失败,就变成给模型看的错误。
调用关系:它是 handle_call 验证请求后交出的核心工作。它会调用外部的解析器 parse_exec_source、工具定义收集函数 collect_code_mode_tool_definitions,以及最后的 handle_runtime_response;自己不直接解释运行结果,而是把这部分交给统一响应处理流程。
调用图:被 1 处调用(handle_call);外部调用 5 个(parse_exec_source, collect_code_mode_tool_definitions, matches!, now, handle_runtime_response)。
CodeModeExecuteHandler::tool_name95–97 ↗
fn tool_name(&self) -> ToolName
作用:告诉工具系统,这个处理器对应哪个公开工具名。这样当模型调用这个名字时,系统才能找到它。
数据流:进去的是处理器自身 → 它把 PUBLIC_TOOL_NAME 包装成 ToolName → 出来的是工具注册和匹配时使用的名字,不改动任何状态。
调用关系:工具注册或调度时会问处理器“你叫什么”。这个函数通过 ToolName::plain 生成标准名字,让外层工具框架能把调用路由到 CodeModeExecuteHandler。
调用图:调用 1 个内部函数(plain)。
CodeModeExecuteHandler::spec99–101 ↗
fn spec(&self) -> ToolSpec
作用:返回这个工具的说明书。说明书告诉外部系统和模型,这个工具长什么样、怎么调用。
数据流:进去的是处理器自身 → 它复制一份保存的 spec → 出来的是 ToolSpec;原来的 spec 仍留在处理器里。
调用关系:工具框架在展示、注册或发送工具定义时会用它。它只提供元信息,不参与真正执行;真正执行由 handle 和 handle_call 往下推进。
调用图:外部调用 1 个(clone)。
CodeModeExecuteHandler::handle103–105 ↗
fn handle(&self, invocation: ToolInvocation) -> codex_tools::ToolExecutorFuture<'_>
作用:把一次工具调用变成一个异步任务。异步任务的意思是:代码可能要等运行结果,所以先返回一个“将来会完成的工作”。
数据流:进去的是 ToolInvocation,也就是一次完整的工具调用信息 → 它调用 handle_call 生成实际处理流程,并用 Box::pin 固定成工具框架需要的异步返回形式 → 出来的是 ToolExecutorFuture,稍后会产出工具输出或错误。
调用关系:这是工具框架正式调用处理器时进入的门面。它自己不检查参数、不执行代码,只把调用转交给 handle_call,并包装成框架约定的异步形态。
调用图:调用 1 个内部函数(handle_call);外部调用 1 个(pin)。
CodeModeExecuteHandler::handle_call109–131 ↗
async fn handle_call(
&self,
invocation: ToolInvocation,
) -> Result<Box<dyn crate::tools::context::ToolOutput>, FunctionCallError>
作用:检查这次工具调用是不是合法的代码执行请求;如果是,就交给 execute;如果不是,就返回一条能告诉模型哪里错了的错误信息。
数据流:进去的是 ToolInvocation,里面有会话、轮次、调用编号、工具名和载荷 payload → 它拆开这些信息,检查 payload 是否是 Custom 输入,并确认工具名符合代码执行工具名 → 合法时把 input 当作源码传给 execute,再把结果包装成通用工具输出;不合法时生成错误,说明这个工具期待的是原始 JavaScript 源码文本。
调用关系:handle 会把所有调用先送到这里。这里像门卫一样筛选请求:通过 is_exec_tool_name 和载荷类型判断能不能执行;通过后调用 execute,没通过就用 RespondToModel 把错误直接反馈给模型。
调用图:调用 1 个内部函数(execute);被 1 处调用(handle);外部调用 3 个(format!, is_exec_tool_name, RespondToModel)。
CodeModeExecuteHandler::matches_kind135–137 ↗
fn matches_kind(&self, payload: &ToolPayload) -> bool
作用:告诉运行时,这个处理器只接收自定义文本类的工具载荷。这样系统能在更早阶段判断某个请求是否可能交给它。
数据流:进去的是一个 ToolPayload 引用 → 它只检查这个载荷是不是 ToolPayload::Custom → 出来的是 true 或 false,不读取代码内容,也不修改任何东西。
调用关系:这是 CoreToolRuntime 接口的一部分,常用于工具分派前的粗略匹配。它只做类型层面的判断;更细的工具名检查和真正执行会在 handle_call 里继续完成。
调用图:外部调用 1 个(matches!)。
core/src/tools/code_mode/wait_spec.rs源码 ↗
在代码模式里,某段代码可能不会马上跑完,就像你点了一个耗时任务后,系统先给你一个排队号。这个文件做的事,就是给“拿着排队号继续等结果”这件事写一份机器能读懂的工具规格。它说明工具叫什么、用途是什么、必须传入哪个运行单元的编号 cell_id,还可以选择等多久、最多拿多少输出、要不要直接终止任务。这里用 JSON Schema(一种描述 JSON 参数长什么样的规则)来规定参数格式,避免调用方乱传字段或漏掉关键编号。文件里还带了一个测试,确保生成出来的工具规格和预期完全一致,防止以后有人改坏了接口而不知道。
create_wait_tool6–48 ↗
fn create_wait_tool() -> ToolSpec
作用:这个函数生成“wait”等待工具的完整规格。上层系统用它来注册工具,好让模型或外部 API 知道该怎么调用这个等待功能。
数据流:进去时不需要外部参数;它读取代码模式里预先定义好的工具名和说明文字,然后拼出一张参数表:cell_id 是必填的运行单元编号,yield_time_ms 是等待多久,max_tokens 是最多返回多少输出,terminate 是是否停止任务;最后把这些内容包装成一个 ToolSpec 返回出去。
调用关系:它会被上层的 spec 流程调用,用来把 wait 工具加入整套可用工具列表。函数内部把具体参数类型交给 JsonSchema 的 string、number、boolean、object 这些小工具来描述,最后交给 ToolSpec::Function 包成一个可注册的函数型工具。
调用图:调用 4 个内部函数(boolean, number, object, string);被 1 处调用(spec);外部调用 4 个(from, format!, Function, vec!)。
tests::create_wait_tool_matches_expected_spec56–104 ↗
fn create_wait_tool_matches_expected_spec()
作用:这个测试确认 create_wait_tool 生成的规格没有走样。它的作用像核对清单:工具名、说明、参数、必填项都必须和预期一模一样。
数据流:测试开始时调用 create_wait_tool 得到实际生成的工具规格;然后手写一份期望中的规格;最后用 assert_eq! 比较两边。如果有任何字段不同,测试就会失败,提醒开发者接口被改动了。
调用关系:它不是运行时业务流程的一部分,而是在测试时由测试框架执行。它主要保护 create_wait_tool,确保上层 spec 流程拿到的 wait 工具定义一直稳定可靠。
调用图:外部调用 1 个(assert_eq!)。
core/src/tools/code_mode/wait_handler.rs源码 ↗
在代码模式里,一段代码不是总能马上跑完。它可能还在计算,也可能已经有一部分输出,也可能需要被用户或模型叫停。这个文件就像一个“候车室管理员”:模型说要等哪个代码单元,它就去后台运行服务里查这个单元的状态;如果要求终止,它就去通知后台停止。它先把工具调用里的 JSON 参数读出来,比如代码单元编号、最多等多久、最多返回多少文字、是否终止。然后它调用 code mode service,也就是实际管理代码运行的服务。拿到结果后,如果发现这个代码单元已经结束或被终止,它还会记录追踪信息,并告诉系统这个单元的调度可以收尾。最后,它把运行结果整理成工具输出交回给上层。特别的是,这个 wait 工具不会触发普通工具调用前后的拦截钩子,因为它不是一个独立用户动作,而是代码运行流程自己需要的控制步骤。
default_wait_yield_time_ms34–36 ↗
fn default_wait_yield_time_ms() -> u64
作用:给 wait 工具提供默认等待时间。调用者没有指定要等多久时,就用这个默认值,避免无限等或立刻返回得太频繁。
数据流:进去没有参数 → 它读取代码模式里预先定义好的默认等待毫秒数 → 出来一个数字,表示默认要让运行中的代码再跑多久。
调用关系:它被 ExecWaitArgs 的反序列化默认值机制使用。也就是说,当 handle_call 解析参数时,如果输入里没有 yield_time_ms,这个函数提供兜底值。
parse_arguments38–45 ↗
fn parse_arguments(arguments: &str) -> Result<T, FunctionCallError>
作用:把工具调用传来的 JSON 字符串变成 Rust 里可用的参数对象。它的作用是把“文字说明”翻译成程序能安全读取的结构。
数据流:进去的是一段 JSON 字符串 → 它调用 JSON 解析器 from_str,尝试按指定类型读出字段 → 成功时出来一个参数对象;失败时出来一个给模型看的错误,说明函数参数解析失败。
调用关系:CodeModeWaitHandler::handle_call 在真正等待或终止代码单元前会先调用它。它把外部传来的原始参数变成 ExecWaitArgs,后面的等待时间、代码单元编号、是否终止都靠这个结果。
调用图:被 1 处调用(handle_call);外部调用 1 个(from_str)。
CodeModeWaitHandler::tool_name48–50 ↗
fn tool_name(&self) -> ToolName
作用:告诉工具系统:这个处理器对应的工具名字就是代码模式的 wait。这样当模型调用 wait 时,系统能找到这个处理器。
数据流:进去的是处理器本身 → 它用固定的等待工具名创建一个普通工具名 → 出来一个 ToolName,供注册表识别和匹配。
调用关系:工具注册和分发时会询问这个名字。它调用 plain 来构造不带特殊命名空间的工具名,确保普通的 wait 调用能被这个处理器接住。
调用图:调用 1 个内部函数(plain)。
CodeModeWaitHandler::spec52–54 ↗
fn spec(&self) -> ToolSpec
作用:返回 wait 工具的说明书。这个说明书告诉模型和工具系统:这个工具需要哪些参数、长什么样、怎么调用。
数据流:进去的是处理器本身 → 它调用 create_wait_tool 生成工具规格 → 出来一个 ToolSpec,也就是工具的结构化描述。
调用关系:工具系统展示或注册工具能力时会用到它。真正的规格创建工作交给 create_wait_tool,这个函数只负责把等待工具的规格接到处理器上。
调用图:调用 1 个内部函数(create_wait_tool)。
CodeModeWaitHandler::handle56–58 ↗
fn handle(&self, invocation: ToolInvocation) -> codex_tools::ToolExecutorFuture<'_>
作用:这是工具系统真正调用 wait 时进入的门面函数。它把一次工具调用包装成异步任务,让系统可以边等代码运行边继续用异步方式调度其他工作。
数据流:进去的是一次 ToolInvocation,里面有会话、轮次、工具名和参数 → 它把实际工作交给 handle_call,并用 pin 固定成一个可等待的异步结果 → 出来一个未来会完成的工具执行结果。
调用关系:工具分发器调用它,而它立刻转给 CodeModeWaitHandler::handle_call。可以把它理解成前台接待员:接到请求后,把单子交给真正办事的人。
调用图:调用 1 个内部函数(handle_call);外部调用 1 个(pin)。
CodeModeWaitHandler::handle_call62–132 ↗
async fn handle_call(
&self,
invocation: ToolInvocation,
) -> Result<Box<dyn crate::tools::context::ToolOutput>, FunctionCallError>
作用:这是 wait 工具的核心逻辑:确认调用是不是合法的 wait,解析参数,然后等待或终止指定代码单元,并把结果整理回给模型。
数据流:进去的是一次工具调用,包含会话、轮次、工具名和 JSON 参数 → 它先检查工具名是否真的是普通的 wait,再用 parse_arguments 读出 cell_id、等待时间、最大返回字数和是否终止;接着把 cell_id 包装成代码单元编号,记录开始时间;如果 terminate 为真,就请求后台终止该代码单元,否则请求后台等待一小段时间;后台返回结果后,如果代码单元已经结束或被终止,它会记录结束追踪并通知调度收尾;最后调用 handle_runtime_response 把运行结果变成工具输出 → 出来的是给上层的工具输出,或者一个会返回给模型的错误信息,同时可能改动追踪记录和代码单元调度状态。
调用关系:CodeModeWaitHandler::handle 会调用它。它是整个文件的中心,向下会用 parse_arguments 解析输入,用代码模式服务执行等待或终止,再把响应交给 handle_runtime_response 做最后包装。它还会在代码单元结束时通知追踪系统和调度系统,保证运行记录和内部状态不会悬着。
调用图:调用 2 个内部函数(new, parse_arguments);被 1 处调用(handle);外部调用 5 个(format!, matches!, now, handle_runtime_response, RespondToModel)。
CodeModeWaitHandler::pre_tool_use_payload136–142 ↗
fn pre_tool_use_payload(&self, _invocation: &ToolInvocation) -> Option<PreToolUsePayload>
作用:明确告诉系统:调用代码模式的 wait 之前,不要生成普通工具的“调用前钩子”信息。这样外部钩子不会阻塞或改写等待循环。
数据流:进去的是一次工具调用,但这个函数不读取里面的内容 → 它直接返回空值 → 结果是系统知道这次 wait 不需要调用前的额外处理。
调用关系:工具运行框架在工具执行前可能会问它要不要触发预处理。这里选择不触发,因为 wait 是代码单元运行过程的一部分,不应该像普通用户工具那样被拦截。
CodeModeWaitHandler::post_tool_use_payload144–152 ↗
fn post_tool_use_payload(
&self,
_invocation: &ToolInvocation,
_result: &dyn ToolOutput,
) -> Option<PostToolUsePayload>
作用:明确告诉系统:调用代码模式的 wait 之后,不要用普通工具的“调用后钩子”反馈替换结果。因为 wait 的返回值会直接影响代码模式下一步怎么走。
数据流:进去的是工具调用和已经产生的工具输出,但这个函数不改也不包装它们 → 它直接返回空值 → 结果是原始等待结果继续交给代码模式流程使用。
调用关系:工具运行框架在工具执行后可能会问它要不要追加或替换反馈。这里选择不做,因为 wait 的结果是控制代码运行流程的关键信号,不能被普通钩子内容覆盖。
扩展注册表与独立工具命名空间
这些文件定义共享扩展注册表,并接入用于网页搜索、图像生成和目标的具体独立命名空间。
ext/extension-api/src/registry.rs源码 ↗
可以把这个文件想成酒店前台的登记本。扩展想参与某些事情,比如改配置、补充上下文、提供工具、监听一次对话的开始和结束,就先把自己登记进来。登记时用的是 ExtensionRegistryBuilder,它是可修改的;等所有扩展装好后,build 会生成 ExtensionRegistry,它基本不再变,运行时只负责把名单交给需要的人。这里大量使用 Arc,也就是“共享指针”,让多处代码能安全共用同一个扩展对象,而不用复制一份。文件还提供一个默认的空登记处,给不安装扩展的主程序使用。一个重要行为是 approval_review:它会按登记顺序询问审批扩展,谁先给出决定就采用谁的结果。
ExtensionRegistryBuilder::default37–52 ↗
fn default() -> Self
作用:创建一个干净的扩展登记草稿。它会准备好所有空名单,并放入一个“什么都不做”的事件接收器,保证即使没有主程序提供接收器也不会出错。
数据流:进去没有额外输入 → 它新建各类空列表,并把 event_sink 设成 NoopExtensionEventSink → 出来一个可继续登记扩展的 ExtensionRegistryBuilder。
调用关系:这是 builder 的兜底起点。ExtensionRegistryBuilder::new 和 ExtensionRegistryBuilder::with_event_sink 都会借它先搭好基本结构,再按需要继续调整。
ExtensionRegistryBuilder::new57–59 ↗
fn new() -> Self
作用:给调用者一个新的空登记本。测试、会话创建和普通启动流程都会用它开始安装扩展。
数据流:进去没有输入 → 它直接调用 default 生成默认空登记本 → 出来一个 ExtensionRegistryBuilder,之后可以往里面加各种 contributor。
调用关系:它是最常见的创建入口。很多会话初始化和测试辅助代码会先调用它,再通过各种 *_contributor 方法把扩展能力登记进去,最后调用 build 定稿。
调用图:被 22 处调用(make_session_and_context, make_session_and_context_with_auth_config_home_and_rx, make_session_with_config_and_rx, make_session_with_history_source_and_agent_control_and_rx, prompt_extension_test_registry, session_new_fails_when_zsh_fork_enabled_without_packaged_zsh, plan_mode_uses_contributed_turn_item_for_last_agent_message, finalized_turn_item_defers_mailbox_for_contributed_visible_text, finalized_turn_item_keeps_mailbox_open_for_commentary_text, handle_non_tool_response_item_runs_turn_item_contributors_only_when_requested (+12 more));外部调用 1 个(default)。
ExtensionRegistryBuilder::with_event_sink62–67 ↗
fn with_event_sink(event_sink: Arc<dyn ExtensionEventSink>) -> Self
作用:创建一个空登记本,但使用主程序自己给的事件接收器。事件接收器可以理解成“消息投递口”,扩展可以把运行事件交给主程序。
数据流:进去一个 Arc<dyn ExtensionEventSink>,也就是可共享的事件接收器 → 它先拿默认空登记本,再把默认的空接收器替换成传入的接收器 → 出来一个带指定事件接收器的 builder。
调用关系:当主程序或测试想观察扩展发出的事件时会用它。后续安装扩展时,install_with_backend 等流程可以从 builder 里拿到这个 event_sink 传给扩展。
调用图:被 1 处调用(orchestrator_catalog_snapshot_caches_failure);外部调用 1 个(default)。
ExtensionRegistryBuilder::event_sink70–72 ↗
fn event_sink(&self) -> Arc<dyn ExtensionEventSink>
作用:把当前登记本里的事件接收器拿出来,通常是传给正在构造的扩展用。返回的是共享引用,不会把原来的接收器搬走。
数据流:进去是 builder 自己 → 它克隆 Arc;这里的克隆只是多一个共享把手,不复制真正对象 → 出来一个 Arc<dyn ExtensionEventSink>。
调用关系:扩展安装流程 install_with_backend、install_with_providers 会在创建扩展时调用它,让扩展之后能把事件发回主程序。
调用图:被 2 处调用(install_with_backend, install_with_providers);外部调用 1 个(clone)。
ExtensionRegistryBuilder::approval_review_contributor75–77 ↗
fn approval_review_contributor(&mut self, contributor: Arc<dyn ApprovalReviewContributor>)
作用:登记一个“审批评审”扩展。它可以在系统需要确认某个操作时,参与给出允许、拒绝或类似决定。
数据流:进去一个 ApprovalReviewContributor 的共享对象 → 它把这个对象追加到审批评审名单末尾 → builder 被修改,之后 build 会把这份名单带进最终 registry。
调用关系:这是安装审批类扩展的入口。最终运行时,ExtensionRegistry::approval_review 会按这里登记的顺序询问这些扩展。
ExtensionRegistryBuilder::thread_lifecycle_contributor80–85 ↗
fn thread_lifecycle_contributor(
&mut self,
contributor: Arc<dyn ThreadLifecycleContributor<C>>,
)
作用:登记一个关心“线程生命周期”的扩展。这里的线程可以理解成一段会话或工作线,扩展能在恢复、停止等时机参与。
数据流:进去一个 ThreadLifecycleContributor 的共享对象 → 它被放进 thread_lifecycle_contributors 列表 → 后续生成的 registry 会保存这份列表。
调用关系:多个 install 流程会调用它安装相关扩展。运行时 resume_thread、stop_thread 会从最终 registry 取出这些扩展并通知它们。
调用图:被 6 处调用(install_with_backend, install, install, install, install_with_providers, install)。
ExtensionRegistryBuilder::turn_lifecycle_contributor88–90 ↗
fn turn_lifecycle_contributor(&mut self, contributor: Arc<dyn TurnLifecycleContributor>)
作用:登记一个关心“单轮对话生命周期”的扩展。单轮对话就是用户发起一次请求,到系统完成这次回应的过程。
数据流:进去一个 TurnLifecycleContributor 的共享对象 → 它被追加到 turn_lifecycle_contributors 列表 → build 后运行时可以统一取用。
调用关系:install_with_backend 会用它装入这类扩展。之后 start_turn_with_mode、stop_turn、notify_turn_error 等流程会读取这份名单来发通知。
调用图:被 1 处调用(install_with_backend)。
ExtensionRegistryBuilder::config_contributor93–95 ↗
fn config_contributor(&mut self, contributor: Arc<dyn ConfigContributor<C>>)
作用:登记一个能补充或修改配置的扩展。配置就是系统启动和运行时要参考的设置。
数据流:进去一个 ConfigContributor 的共享对象 → 它被加入配置扩展列表 → 最终 registry 中会保留它,供配置相关流程读取。
调用关系:多个 install 流程会调用它。它把扩展接入配置阶段,让主系统可以在需要配置贡献时从 registry 取出这些对象。
调用图:被 5 处调用(install_with_backend, install, install, install_with_providers, install)。
ExtensionRegistryBuilder::token_usage_contributor98–100 ↗
fn token_usage_contributor(&mut self, contributor: Arc<dyn TokenUsageContributor>)
作用:登记一个关心 token 用量的扩展。token 可以粗略理解成模型处理文字时的计费和统计单位。
数据流:进去一个 TokenUsageContributor 的共享对象 → 它被追加到 token_usage_contributors 列表 → 运行时统计用量时可以通知它。
调用关系:install_with_backend 会登记这类扩展。之后 record_token_usage 会从 registry 取出名单,把用量记录交给它们。
调用图:被 1 处调用(install_with_backend)。
ExtensionRegistryBuilder::prompt_contributor103–105 ↗
fn prompt_contributor(&mut self, contributor: Arc<dyn ContextContributor>)
作用:登记一个能给提示词补充上下文的扩展。提示词就是发给模型看的说明和材料。
数据流:进去一个 ContextContributor 的共享对象 → 它被放入 context_contributors 列表 → 最终 registry 会把这份上下文贡献者名单暴露给提示词构建流程。
调用关系:多个 install 流程会用它接入上下文扩展。运行时 contribute_prompt 会通过 ExtensionRegistry::context_contributors 拿到这些扩展。
调用图:被 3 处调用(install, install, install_with_providers)。
ExtensionRegistryBuilder::mcp_server_contributor108–110 ↗
fn mcp_server_contributor(&mut self, contributor: Arc<dyn McpServerContributor<C>>)
作用:登记一个能提供运行时 MCP 服务器的扩展。MCP 是一种让模型工具和外部能力对接的协议,可以简单理解成“工具服务接口”。
数据流:进去一个 McpServerContributor 的共享对象 → 它被追加到 mcp_server_contributors 列表 → build 后这类服务器贡献会被保存在 registry 里。
调用关系:install 和 install_executor_plugins 等安装流程会调用它。后续需要启动或查询 MCP 服务时,会从 registry 拿这份名单。
调用图:被 2 处调用(install, install_executor_plugins)。
ExtensionRegistryBuilder::turn_input_contributor113–115 ↗
fn turn_input_contributor(&mut self, contributor: Arc<dyn TurnInputContributor>)
作用:登记一个能补充单轮输入的扩展。它可以在一次对话开始前,给这次输入加材料或做调整。
数据流:进去一个 TurnInputContributor 的共享对象 → 它被放进 turn_input_contributors 列表 → 最终 registry 会让对话输入流程能找到它。
调用关系:install_with_providers 会用它接入输入扩展。运行时如果要汇总一轮对话的输入,会从 registry 读取这些贡献者。
调用图:被 1 处调用(install_with_providers)。
ExtensionRegistryBuilder::tool_contributor118–120 ↗
fn tool_contributor(&mut self, contributor: Arc<dyn ToolContributor>)
作用:登记一个原生工具扩展。工具就是模型或系统可以调用的能力,比如执行某个内置动作。
数据流:进去一个 ToolContributor 的共享对象 → 它被追加到 tool_contributors 列表 → build 后工具收集流程能从 registry 里拿到它。
调用关系:多个 install 流程会登记工具扩展。运行时 tools 会通过 ExtensionRegistry::tool_contributors 取出所有工具贡献者。
调用图:被 5 处调用(install_with_backend, install, install, install_with_providers, install)。
ExtensionRegistryBuilder::tool_lifecycle_contributor123–125 ↗
fn tool_lifecycle_contributor(&mut self, contributor: Arc<dyn ToolLifecycleContributor>)
作用:登记一个关心工具执行生命周期的扩展。它可以在工具完成等时机收到通知。
数据流:进去一个 ToolLifecycleContributor 的共享对象 → 它被加入 tool_lifecycle_contributors 列表 → 最终 registry 保存这份通知对象名单。
调用关系:install_with_backend 会装入这类扩展。工具运行结束时,notify_tool_finish 会从 registry 取出它们来通知结果。
调用图:被 1 处调用(install_with_backend)。
ExtensionRegistryBuilder::turn_item_contributor128–130 ↗
fn turn_item_contributor(&mut self, contributor: Arc<dyn TurnItemContributor>)
作用:登记一个能提供“对话条目”的扩展。对话条目可以理解成一轮对话里按顺序出现的消息或可见内容片段。
数据流:进去一个 TurnItemContributor 的共享对象 → 它被追加到 turn_item_contributors 列表 → build 后这份有顺序的贡献者名单会保留下来。
调用关系:这是给对话内容生成流程预留的登记入口。最终 ExtensionRegistry::turn_item_contributors 会把这些对象交给需要组织对话条目的代码。
ExtensionRegistryBuilder::build133–148 ↗
fn build(self) -> ExtensionRegistry<C>
作用:把可修改的登记草稿封存成正式登记处。封存后外部主要只能读取,不能再随便往里加东西。
数据流:进去 builder 本身和里面已经收集好的所有列表 → 它把 event_sink 以及各类 contributor 列表搬进 ExtensionRegistry → 出来一个不可变的 ExtensionRegistry。
调用关系:这是扩展安装阶段的收尾动作。ExtensionRegistryBuilder::new 或 with_event_sink 创建登记本,各种登记方法填充它,最后 build 交给运行时使用。
ExtensionRegistry::event_sink169–171 ↗
fn event_sink(&self) -> Arc<dyn ExtensionEventSink>
作用:从正式登记处取出事件接收器。调用者拿到的是共享把手,可以继续传给别的组件。
数据流:进去是 registry 自己 → 它克隆内部 Arc,不复制真正的事件接收器 → 出来一个 Arc<dyn ExtensionEventSink>。
调用关系:这是运行期或后续组件需要事件投递口时使用的读取方法。它不改变 registry,只是把同一个接收器安全地分享出去。
调用图:外部调用 1 个(clone)。
ExtensionRegistry::thread_lifecycle_contributors174–176 ↗
fn thread_lifecycle_contributors(&self) -> &[Arc<dyn ThreadLifecycleContributor<C>>]
作用:取出所有已登记的线程生命周期扩展。调用者可以用这份名单在会话恢复或停止时逐个通知。
数据流:进去是 registry 自己 → 它返回内部 thread_lifecycle_contributors 的只读切片 → 出来一份不能直接改动的扩展名单。
调用关系:resume_thread 和 stop_thread 会调用它。registry 在这里像通讯录,只把名单交给真正发通知的流程。
调用图:被 2 处调用(resume_thread, stop_thread)。
ExtensionRegistry::turn_lifecycle_contributors179–181 ↗
fn turn_lifecycle_contributors(&self) -> &[Arc<dyn TurnLifecycleContributor>]
作用:取出所有已登记的单轮对话生命周期扩展。它们会在一轮对话开始、结束或出错时被使用。
数据流:进去是 registry 自己 → 它返回 turn_lifecycle_contributors 的只读列表 → 调用者得到可以遍历但不能修改的名单。
调用关系:start_turn_with_mode、stop_turn、notify_turn_error 会在对应时机调用它,把具体通知工作交给这些扩展。
调用图:被 3 处调用(notify_turn_error, start_turn_with_mode, stop_turn)。
ExtensionRegistry::config_contributors184–186 ↗
fn config_contributors(&self) -> &[Arc<dyn ConfigContributor<C>>]
作用:取出所有配置贡献者。配置流程可以据此让扩展补充系统设置。
数据流:进去是 registry 自己 → 它把 config_contributors 作为只读切片返回 → 出来是配置扩展名单,registry 本身不变。
调用关系:这是配置阶段读取扩展的出口。扩展先通过 builder 登记,之后配置相关代码从这里拿名单执行。
ExtensionRegistry::token_usage_contributors189–191 ↗
fn token_usage_contributors(&self) -> &[Arc<dyn TokenUsageContributor>]
作用:取出所有 token 用量贡献者。系统记录模型用量时会用到它们。
数据流:进去是 registry 自己 → 它返回 token_usage_contributors 的只读列表 → 调用者拿到名单后可以逐个记录或通知。
调用关系:record_token_usage 会调用它。registry 不负责计算用量,只负责告诉统计流程有哪些扩展想知道这件事。
调用图:被 1 处调用(record_token_usage)。
ExtensionRegistry::approval_review195–211 ↗
async fn approval_review(
&self,
session_store: &ExtensionData,
thread_store: &ExtensionData,
prompt: &str,
) -> Option<ReviewDecision>
作用:让已安装的审批扩展尝试对一个审批提示给出决定。它采用“谁先明确表态,就听谁的”的规则。
数据流:进去 session_store、thread_store 和 prompt;前两者是扩展可读的数据仓库,prompt 是要评审的文字 → 它按登记顺序异步询问每个 ApprovalReviewContributor → 如果某个扩展返回决定,就立刻返回 Some(decision);如果都不表态,就返回 None。
调用关系:它是这个 registry 少数真正会主动调扩展的方法。审批扩展通过 ExtensionRegistryBuilder::approval_review_contributor 登记,运行时需要审批判断时调用这里,由这里依次把问题交给扩展。
ExtensionRegistry::context_contributors214–216 ↗
fn context_contributors(&self) -> &[Arc<dyn ContextContributor>]
作用:取出所有上下文贡献者。它们可以给模型提示词补充额外背景材料。
数据流:进去是 registry 自己 → 它返回 context_contributors 的只读列表 → 提示词构建流程拿到名单后再向这些扩展要内容。
调用关系:contribute_prompt 会调用它。builder 阶段用 prompt_contributor 登记,真正拼提示词时从这里取出。
调用图:被 1 处调用(contribute_prompt)。
ExtensionRegistry::mcp_server_contributors219–221 ↗
fn mcp_server_contributors(&self) -> &[Arc<dyn McpServerContributor<C>>]
作用:取出所有 MCP 服务器贡献者。MCP 服务器贡献者能让系统在运行时接入外部工具服务。
数据流:进去是 registry 自己 → 它返回 mcp_server_contributors 的只读列表 → 调用者得到可以遍历的服务器贡献者名单。
调用关系:安装阶段通过 mcp_server_contributor 放入这些对象。后续需要建立或枚举 MCP 服务时,会从这个读取口拿到它们。
ExtensionRegistry::turn_input_contributors224–226 ↗
fn turn_input_contributors(&self) -> &[Arc<dyn TurnInputContributor>]
作用:取出所有单轮输入贡献者。它们可以在一次对话输入形成时补充内容。
数据流:进去是 registry 自己 → 它返回 turn_input_contributors 的只读列表 → 调用者拿到名单后决定如何合并这些输入贡献。
调用关系:安装阶段通过 turn_input_contributor 登记。对话开始前的输入准备流程会把这里当作扩展名单来源。
ExtensionRegistry::tool_contributors229–231 ↗
fn tool_contributors(&self) -> &[Arc<dyn ToolContributor>]
作用:取出所有工具贡献者。系统要知道有哪些原生工具可用时,会从这里拿名单。
数据流:进去是 registry 自己 → 它返回 tool_contributors 的只读列表 → 工具收集流程得到所有已登记的工具来源。
调用关系:tools 会调用它来汇总工具。扩展安装时先用 ExtensionRegistryBuilder::tool_contributor 登记,运行时再从这里读取。
调用图:被 1 处调用(tools)。
ExtensionRegistry::tool_lifecycle_contributors234–236 ↗
fn tool_lifecycle_contributors(&self) -> &[Arc<dyn ToolLifecycleContributor>]
作用:取出所有工具生命周期贡献者。它们关心工具执行完成等事件。
数据流:进去是 registry 自己 → 它返回 tool_lifecycle_contributors 的只读列表 → 调用者可以逐个通知工具运行状态。
调用关系:notify_tool_finish 会调用它。registry 只提供名单,具体怎么通知、通知什么结果由工具生命周期流程完成。
调用图:被 1 处调用(notify_tool_finish)。
ExtensionRegistry::turn_item_contributors239–241 ↗
fn turn_item_contributors(&self) -> &[Arc<dyn TurnItemContributor>]
作用:取出所有有顺序的对话条目贡献者。它们能向一轮对话里添加或提供消息片段。
数据流:进去是 registry 自己 → 它返回 turn_item_contributors 的只读列表 → 调用者拿到按登记顺序保存的贡献者名单。
调用关系:扩展通过 turn_item_contributor 登记后,生成或整理对话条目的流程会从这里读取它们。这里保留顺序很重要,因为对话内容的先后会影响最终呈现。
empty_extension_registry245–247 ↗
fn empty_extension_registry() -> Arc<ExtensionRegistry<C>>
作用:快速创建一个共享的空扩展登记处。给那些不安装任何扩展的主程序或测试使用,省得每次手写 builder、build。
数据流:进去没有输入 → 它创建新的 ExtensionRegistryBuilder,立刻 build 成空 registry,再包进 Arc 共享指针 → 出来一个 Arc<ExtensionRegistry<C>>。
调用关系:这是“没有扩展也要有个登记处”的便捷入口。它内部走和正常流程一样的 new 加 build,所以空场景和有扩展场景使用的是同一种 registry 结构。
ext/web-search/src/lib.rs源码 ↗
可以把这个文件想成一家店的前台。店里面有不同部门:extension 负责扩展安装和接入,history 可能记录搜索历史,output 负责整理输出结果,schema 定义数据格式,tool 提供具体工具能力。但外面的人不需要直接跑到每个部门里找人,只要从这里拿到 install 这个入口就行。这里没有写具体搜索逻辑,也没有函数实现;它的价值在于规定“这个包由哪些部分组成”和“外部能看见什么”。如果没有它,Rust 编译器就不知道这些子文件属于同一个库,别的代码也不能方便地调用这个 web-search 扩展的安装入口。
ext/web-search/src/extension.rs源码 ↗
这个文件像一个“插座转换头”:网页搜索工具本身在别处实现,而这里负责把它插进主程序。它先从用户配置里读出网页搜索是否开启、是不是 OpenAI 模型、搜索位置、搜索范围大小、允许访问哪些网站等信息。然后在线程开始时,把这些整理好的设置放进线程自己的小仓库里;配置变化时,也会更新这份设置。等系统问“现在有哪些工具可用”时,它会检查网页搜索是否真的可用:如果关闭了,或者不是支持的模型供应商,就不给工具;如果可用,就创建一个 WebSearchTool,并带上登录认证、模型供应商和搜索设置。这样做的好处是,主程序不用到处关心网页搜索的细节,只要通过扩展注册表统一发现工具即可。
WebSearchExtensionConfig::from38–47 ↗
fn from(config: &Config) -> Self
作用:把一整份全局配置,压缩成网页搜索扩展真正关心的那几项。有人需要判断网页搜索能不能用、该怎么搜时,就会用它生成这份专用配置。
数据流:进去的是 Config,也就是主程序的大配置。它读取网页搜索模式、模型供应商和网页搜索相关小配置;先判断当前模型供应商是不是 OpenAI,并且搜索模式没有被关闭;再调用 search_settings 生成具体搜索参数。出来的是 WebSearchExtensionConfig,里面有“是否可用”、供应商信息和搜索设置。
调用关系:它是配置转换的入口。线程刚开始时,WebSearchExtension::on_thread_start 会调用它,把当前配置存起来;用户改配置时,WebSearchExtension::on_config_changed 也会调用它,用新配置覆盖旧设置。它把更细的搜索参数整理工作交给 search_settings。
调用图:调用 1 个内部函数(search_settings);被 2 处调用(on_config_changed, on_thread_start)。
search_settings50–82 ↗
fn search_settings(config: &Config, web_search_mode: WebSearchMode) -> SearchSettings
作用:把用户写在配置里的网页搜索偏好,翻译成底层搜索接口能看懂的 SearchSettings。比如用户所在地、搜索内容多少、允许哪些网站、是否真的访问实时网络。
数据流:进去的是主配置 Config 和网页搜索模式 WebSearchMode。它从配置里取出可选的用户大概位置、搜索上下文大小、域名过滤规则等;把项目自己的配置类型转换成接口需要的类型;同时设置只允许直接调用搜索,并根据模式决定能不能访问外部实时网页。出来的是 SearchSettings,没配置的部分就使用默认值。
调用关系:它只被 WebSearchExtensionConfig::from 调用,是生成网页搜索专用配置时的内部帮手。它不创建工具,也不访问网络,只负责把“用户怎么希望搜索”这件事整理清楚。
WebSearchExtension::on_thread_start85–94 ↗
fn on_thread_start(
&'a self,
input: ThreadStartInput<'a, Config>,
) -> ExtensionFuture<'a, ()>
作用:在线程刚开始时,为这个线程准备好网页搜索配置。这里的线程可以理解成一次对话或一次工作上下文,它需要有自己的一份工具状态。
数据流:进去的是 ThreadStartInput,里面带着当前配置和线程专属的数据仓库。函数把 Config 交给 WebSearchExtensionConfig::from,得到网页搜索专用配置;然后把这份配置插入 thread_store。出来没有返回实际数据,但线程仓库被更新了。
调用关系:它是扩展生命周期的一部分,由扩展系统在线程启动时调用。它依赖 WebSearchExtensionConfig::from 做配置转换,后面的 WebSearchExtension::tools 会从同一个 thread_store 里取出这份配置,决定要不要提供搜索工具。
调用图:调用 1 个内部函数(from);外部调用 1 个(pin)。
WebSearchExtension::on_config_changed98–106 ↗
fn on_config_changed(
&self,
_session_store: &ExtensionData,
thread_store: &ExtensionData,
_previous_config: &Config,
new_config: &Config,
)
作用:当用户配置变化时,刷新网页搜索扩展保存的设置。这样用户刚改完开关、位置或搜索范围,后续工具发现就能用新规则。
数据流:进去的是会话仓库、线程仓库、旧配置和新配置;它忽略会话仓库和旧配置,只关心 new_config。它把新配置转换成 WebSearchExtensionConfig,并写入 thread_store。出来没有返回值,但线程里的网页搜索配置被替换成最新版本。
调用关系:它由扩展系统在配置变更时调用。它和 WebSearchExtension::on_thread_start 做的是同一类事:把 Config 变成扩展自己的配置并存起来。之后 WebSearchExtension::tools 读取的就是这份更新后的配置。
WebSearchExtension::tools110–130 ↗
fn tools(
&self,
session_store: &ExtensionData,
thread_store: &ExtensionData,
) -> Vec<Arc<dyn codex_extension_api::ToolExecutor<codex_extension_api::ToolCall>>>
作用:告诉主程序:当前是否应该提供网页搜索工具;如果应该,就把工具实例交出去。它是“搜索功能能不能出现在模型工具列表里”的最后一道门。
数据流:进去的是会话数据仓库和线程数据仓库。它先从 thread_store 取 WebSearchExtensionConfig;如果没有配置,或者配置说不可用,就返回空列表。若可用,它会用保存的供应商信息和登录管理器创建模型供应商,再创建 WebSearchTool,带上会话 ID 和搜索设置。出来的是一个工具列表,通常要么为空,要么包含一个网页搜索工具。
调用关系:它由扩展系统在收集可用工具时调用。它依赖之前 on_thread_start 或 on_config_changed 放进 thread_store 的配置;它还会把认证信息交给 create_model_provider,最终包装出 WebSearchTool 供模型调用。
install133–138 ↗
fn install(registry: &mut ExtensionRegistryBuilder<Config>, auth_manager: Arc<AuthManager>)
作用:把网页搜索扩展正式注册到扩展注册表里。没有这一步,主程序就不会知道网页搜索扩展存在,也不会在线程启动、配置变化或工具收集时调用它。
数据流:进去的是 ExtensionRegistryBuilder 和登录认证管理器 AuthManager。它创建一个 WebSearchExtension,并把同一个扩展对象分别注册成线程生命周期贡献者、配置贡献者和工具贡献者。出来没有返回值,但 registry builder 被加入了网页搜索扩展的三个入口。
调用关系:它通常在程序启动组装扩展时被调用。测试函数 tests::installed_extension_contributes_web_run_when_enabled 也会调用它,确认注册后真的能贡献网页搜索工具。它把具体工作交给扩展注册表的 thread_lifecycle_contributor、config_contributor 和 tool_contributor。
调用图:调用 3 个内部函数(config_contributor, thread_lifecycle_contributor, tool_contributor);被 1 处调用(installed_extension_contributes_web_run_when_enabled);外部调用 1 个(new)。
tests::installed_extension_contributes_web_run_when_enabled157–183 ↗
fn installed_extension_contributes_web_run_when_enabled()
作用:这是一个自动测试,用来确认:当网页搜索被标记为可用时,安装好的扩展确实会提供 web.run 这个工具,并且支持并行工具调用。
数据流:进去没有外部输入,测试自己搭建一个扩展注册表、一个假的 API key 登录、会话仓库和线程仓库。它调用 install 注册扩展,再手动往线程仓库放入一份可用的 WebSearchExtensionConfig;然后向注册表里的工具贡献者询问工具列表,取出工具名和是否支持并行调用。最后用断言比较结果,期望只得到一个带命名空间的网页搜索工具。
调用关系:它只在测试运行时活跃。它调用 install 来走真实注册流程,再间接触发 WebSearchExtension::tools 的行为,用来保护这个文件的核心承诺:启用时扩展会把网页搜索工具交给系统。
调用图:调用 5 个内部函数(new, install, from_auth_for_testing, from_api_key, create_openai_provider);外部调用 3 个(default, new, assert_eq!)。
ext/web-search/src/tool.rs源码 ↗
这个文件像一个“网页搜索前台接待员”。模型想上网查东西时,不是直接碰网络,而是调用这里定义的 WebSearchTool。它先告诉系统这个工具叫什么、长什么样、能不能并发用;真正被调用时,它会读取模型传来的参数,把空参数当成默认搜索命令,把 JSON 参数解析成搜索指令。接着它拿到模型服务提供方和认证信息,创建搜索客户端,通过网络把请求发出去。为了让用户界面知道发生了什么,它还会在搜索开始和结束时发出一条“网页搜索状态”记录。文件里也有一些小助手,用来判断这次动作是搜索、打开网页,还是在网页里找文字,方便界面展示更准确的说明。
WebSearchTool::tool_name43–45 ↗
fn tool_name(&self) -> ToolName
作用:告诉外部系统,这个工具的正式名字是 web.run。这样模型或工具调度器才知道应该用哪个名字来调用网页搜索。
数据流:进去的是这个工具本身 → 它把命名空间 web 和工具名 run 拼成一个带命名空间的工具名 → 出来的是 ToolName,供系统注册和匹配调用时使用。
调用关系:这是工具注册阶段会问的基本信息。它调用 namespaced 来生成规范名字,后面的 handle 只有在调用名匹配到这个工具时才会被触发。
调用图:调用 1 个内部函数(namespaced)。
WebSearchTool::spec47–66 ↗
fn spec(&self) -> ToolSpec
作用:生成这个网页搜索工具的“说明书”。里面写清楚工具叫啥、描述是什么、输入参数应该长什么样,方便模型知道怎么正确调用。
数据流:进去的是工具本身 → 它读取搜索命令的参数结构,把结构解析成工具输入 schema(也就是参数格式说明),再填入 web 命名空间和 run 工具描述 → 出来的是 ToolSpec,供平台展示和提供给模型使用;如果参数结构解析失败,会直接 panic,因为这代表开发时写错了固定 schema。
调用关系:这是工具暴露给模型前的重要步骤。它会调用 commands_schema 取得搜索命令格式,调用 parse_tool_input_schema_without_compaction 保留字段说明,再用 default_namespace_description 和 ResponsesApiTool 拼出完整工具定义。
调用图:调用 1 个内部函数(commands_schema);外部调用 5 个(parse_tool_input_schema_without_compaction, default_namespace_description, panic!, Namespace, vec!)。
WebSearchTool::exposure68–70 ↗
fn exposure(&self) -> ToolExposure
作用:说明这个工具是直接暴露给模型使用的,不需要再套一层中转。
数据流:进去的是工具本身 → 它不读取额外数据,直接返回 Direct → 出来的结果告诉调度系统:模型可以直接发起这个工具调用。
调用关系:这是工具注册和权限判断时会用到的小开关。它不调用别的函数,只给上层工具系统一个明确态度:网页搜索是直接可用的。
WebSearchTool::supports_parallel_tool_calls72–74 ↗
fn supports_parallel_tool_calls(&self) -> bool
作用:告诉系统,这个网页搜索工具允许同时处理多个调用。比如模型一次想查好几个问题时,不必强制一个等一个。
数据流:进去的是工具本身 → 它直接返回 true → 出来的布尔值让调度器知道可以并行安排搜索调用。
调用关系:这是调度器安排工具执行方式时看的能力声明。它不做实际搜索,只影响上层是否能同时启动多个 WebSearchTool::handle。
WebSearchTool::handle76–78 ↗
fn handle(&self, call: ToolCall) -> codex_extension_api::ToolExecutorFuture<'_>
作用:这是工具真正被调用时的入口。它把一次工具调用包装成异步任务,让搜索可以在等待网络返回时不堵住整个程序。
数据流:进去的是 ToolCall,也就是模型发来的这次工具调用 → 它把工作交给 WebSearchTool::handle_call,并用 pin 包成一个可等待的异步结果 → 出来的是 ToolExecutorFuture,之后会产出工具输出或错误。
调用关系:调度器匹配到 web.run 后会调用它。它自己不处理细节,只把活交给 WebSearchTool::handle_call,后者才负责解析参数、发网络请求和返回结果。
调用图:调用 1 个内部函数(handle_call);外部调用 1 个(pin)。
WebSearchTool::handle_call82–123 ↗
async fn handle_call(&self, call: ToolCall) -> Result<Box<dyn ToolOutput>, FunctionCallError>
作用:这是网页搜索的主流程。它把模型的工具调用变成真实的搜索 API 请求,并把搜索结果包装成工具输出。
数据流:进去的是一次 ToolCall,以及工具里保存的会话 ID、模型提供方、搜索设置 → 它先解析命令,判断动作类型;再获取 API 提供方和认证信息,创建 HTTP 客户端;然后取最近对话内容,组装 SearchRequest,发出开始事件,调用搜索服务,成功后发出完成事件 → 出来的是 SearchOutput;如果认证、网络或服务出错,会变成 Fatal 错误返回。
调用关系:它由 WebSearchTool::handle 启动,是整个文件的核心。它会调用 parse_commands 读参数,调用 command_action 判断界面该显示什么动作,调用 recent_input 带上近期上下文,调用 build_reqwest_client 和 SearchClient 走网络,最后用 web_search_item 发出搜索状态记录。
调用图:调用 8 个内部函数(new, new, recent_input, new, command_action, parse_commands, web_search_item, build_reqwest_client);被 1 处调用(handle);外部调用 6 个(new, new, api_auth, api_provider, clone, try_from)。
parse_commands126–134 ↗
fn parse_commands(call: &ToolCall) -> Result<SearchCommands, FunctionCallError>
作用:把模型传来的工具参数读成搜索命令。它还照顾了“参数为空”的情况,把空输入当作默认命令,而不是直接报错。
数据流:进去的是 ToolCall → 它先取出 function_arguments,也就是模型给工具的参数文本;如果文本去掉空白后为空,就返回默认 SearchCommands;否则把 JSON 文本解析成 SearchCommands → 出来的是可执行的搜索命令,或一个提示模型修正参数的错误。
调用关系:它由 WebSearchTool::handle_call 在发请求前调用。它把不可靠的文本参数变成结构化命令,后面的 command_action 和 SearchRequest 都依赖这个结果。
调用图:被 1 处调用(handle_call);外部调用 3 个(default, function_arguments, from_str)。
command_action136–163 ↗
fn command_action(commands: &SearchCommands) -> WebSearchAction
作用:判断这次网页工具调用“看起来是在做什么”。比如是搜索关键词、打开某个网址,还是在网页中查找某段文字。
数据流:进去的是 SearchCommands → 它按顺序查看 search_query、image_query、open、find 等字段;如果是搜索,就提取查询词;如果是打开或查找,会尽量确认 ref_id 是不是一个真实网址;如果都识别不出来,就归为 Other → 出来的是 WebSearchAction,用来描述这次操作。
调用关系:它由 WebSearchTool::handle_call 调用,主要服务于状态展示,而不是决定搜索 API 怎么执行。它会借助 query_action 处理一个或多个搜索词,也会借助 literal_url 判断字符串是不是 URL。
调用图:被 1 处调用(handle_call)。
query_action165–177 ↗
fn query_action(queries: &[SearchQuery]) -> Option<WebSearchAction>
作用:把一个或多个搜索词变成统一的“搜索动作”描述。一个词就显示单个查询,多个词就显示成一组查询。
数据流:进去的是 SearchQuery 列表 → 如果列表为空,返回 None;如果只有一个,就取出它的 q 字段作为单个 query;如果有多个,就把每个 q 收集成 queries 列表 → 出来的是可选的 WebSearchAction::Search。
调用关系:它是 command_action 的小帮手。command_action 发现命令里有普通搜索或图片搜索时,会用它把查询列表整理成界面能理解的动作说明。
调用图:外部调用 1 个(iter)。
literal_url179–181 ↗
fn literal_url(ref_id: &str) -> Option<String>
作用:判断一段文字是不是一个真正的网址。这样打开网页或页内查找时,界面不会把 turn0search0 这类内部引用误当成 URL。
数据流:进去的是 ref_id 字符串 → 它尝试用 URL 解析器解析;解析成功就原样返回这个字符串,解析失败就返回 None → 出来的是可选的网址文本。
调用关系:它服务于 command_action。command_action 在处理 open 和 find 时会用它区分真实网址和搜索结果引用编号,从而决定 WebSearchAction 里要不要带 url。
调用图:外部调用 1 个(parse)。
web_search_item183–189 ↗
fn web_search_item(call_id: &str, action: WebSearchAction) -> ExtensionTurnItem
作用:把一次网页搜索动作包装成可以写进对话回合里的记录。这样用户界面能显示“正在搜索什么”或“打开了哪个网页”。
数据流:进去的是 call_id 和 WebSearchAction → 它根据动作生成一段可读的查询/动作细节,再连同 ID 和动作一起放进 WebSearchItem,最后包成 ExtensionTurnItem::WebSearch → 出来的是一条扩展回合记录。
调用关系:它由 WebSearchTool::handle_call 在搜索开始和完成时调用。handle_call 用它生成事件内容,再交给 turn_item_emitter 发给外部界面或日志系统。
调用图:被 1 处调用(handle_call);外部调用 2 个(web_search_action_detail, WebSearch)。
ext/web-search/src/output.rs源码 ↗
这个文件很像一个“打包员”:网页搜索拿到一段文字后,不能直接丢给系统,而要装进统一的工具输出盒子里。SearchOutput 只保存一份搜索结果文本。它实现了 ToolOutput,也就是“工具输出必须会做的几件事”:给日志一个简短预览、告诉日志这次算成功、说明里面含有外部信息,以及最关键的,把搜索文本变成 ResponseInputItem。ResponseInputItem 可以理解成模型下一轮能读取的标准消息格式。这里还特意把搜索内容标记为 InputText,意思是按纯文本交给模型。测试部分则确认:输入一段搜索结果和一次调用编号,最后确实会生成带同样文字和编号的标准函数调用输出。
SearchOutput::new12–14 ↗
fn new(output: String) -> Self
作用:创建一个新的搜索输出对象,把搜索工具拿到的文字结果先保存起来。调用者用它来把普通字符串变成后续流程认识的 SearchOutput。
数据流:进去的是一段搜索结果文字 → 函数把这段文字放进 SearchOutput 的内部字段里 → 出来的是一个可被工具输出流程继续处理的 SearchOutput 对象,本身不做网络请求,也不改别的东西。
调用关系:网页搜索调用完成后,handle_call 会用它把结果包装起来;测试 tests::emits_plaintext_function_call_output 也会用它造一个样例输出,检查后续转换是否正确。
调用图:被 2 处调用(emits_plaintext_function_call_output, handle_call)。
SearchOutput::log_preview18–20 ↗
fn log_preview(&self) -> String
作用:给日志系统一段固定的简短提示,表示这里有一份独立的网页搜索输出。它不把真实搜索内容写进预览里,避免日志里塞进大段文本。
数据流:进去的是当前 SearchOutput 对象,但函数不读取里面的搜索正文 → 直接生成固定文字 [standalone web search output] → 出来的是这段日志预览字符串,不改变对象内容。
调用关系:当统一的工具输出框架想记录“这次工具产出了什么”时,会调用这个函数拿一个安全、简短的说明;它不再把工作交给其他函数。
SearchOutput::success_for_logging22–24 ↗
fn success_for_logging(&self) -> bool
作用:告诉日志系统,这类搜索输出在记录时应当被看作成功结果。这样日志里能把它和失败输出区分开。
数据流:进去的是当前输出对象,但函数不检查具体搜索文字 → 直接返回 true,也就是“成功” → 不修改任何数据。
调用关系:统一的工具输出记录流程会用它判断日志里该标成成功还是失败;这里逻辑很简单,不依赖其他函数。
SearchOutput::contains_external_context26–28 ↗
fn contains_external_context(&self) -> bool
作用:说明这份输出包含外部上下文,也就是来自网页搜索的外部信息。这个标记很重要,因为系统需要知道模型接下来看到的内容不是自己原本就有的,而是工具从外面取回来的。
数据流:进去的是当前输出对象,但函数不读取正文 → 直接返回 true,表示包含外部信息 → 不改变搜索结果,也不产生额外内容。
调用关系:工具输出框架在判断内容来源时会用到它;它让上层流程知道这份文本应按“外部搜索资料”对待。
SearchOutput::to_response_item30–39 ↗
fn to_response_item(&self, call_id: &str, _payload: &ToolPayload) -> ResponseInputItem
作用:把保存的搜索结果转换成模型协议认识的响应条目。简单说,就是把“搜索出来的一段文字”装进“某次工具调用的输出消息”里。
数据流:进去的是当前 SearchOutput、一次工具调用编号 call_id,以及一个工具载荷参数;这里载荷参数没有被使用 → 函数复制调用编号,复制搜索正文,把正文放进 InputText,再用 FunctionCallOutputPayload::from_content_items 包成标准输出负载 → 出来的是一个 ResponseInputItem::FunctionCallOutput,表示“这是某次函数调用的返回内容”。
调用关系:这是本文件最关键的转换点。统一工具系统需要把结果交回对话协议时会调用它;它内部把文本列表交给 FunctionCallOutputPayload::from_content_items 来做标准包装。
调用图:调用 1 个内部函数(from_content_items);外部调用 1 个(vec!)。
tests::emits_plaintext_function_call_output54–73 ↗
fn emits_plaintext_function_call_output()
作用:这是一个测试,确认搜索输出会被按纯文本格式正确发出去。它防止以后有人改代码时,不小心把调用编号、文本内容或输出格式弄错。
数据流:进去没有外部输入,测试自己造出文字 search output 和调用编号 call-1 → 它用 SearchOutput::new 创建输出,再调用 to_response_item 转换 → 最后用断言比较实际结果和预期结果;如果不一样,测试失败。
调用关系:它专门验证 SearchOutput::new 和 SearchOutput::to_response_item 的配合是否正确。测试通过,说明搜索结果能被包装成系统协议要求的函数调用输出。
调用图:调用 1 个内部函数(new);外部调用 1 个(assert_eq!)。
ext/image-generation/src/lib.rs源码 ↗
这个文件像一本书的目录页:自己不做复杂工作,但告诉编译器这里有 backend、extension、tool 三个部分,并把 extension 里的 install 公开给外面用。这样别的代码不用知道内部文件怎么分,只要调用这个扩展提供的 install 就能把图片生成功能装进去。它还定义了两个固定名字:IMAGE_GEN_NAMESPACE 表示这个扩展使用的命名空间,IMAGEGEN_TOOL_NAME 表示图片生成工具的名字。固定名字很重要,因为系统里不同模块需要用同一套称呼来互相找到对方;如果各处随便写字符串,就容易拼错或不一致,导致工具注册不上、调用不到。
ext/image-generation/src/extension.rs源码 ↗
这个文件像一个“插座转换头”:图片生成工具本身在别的文件里,这里负责把它安全地插进整个 Codex 系统。它先根据当前配置判断图片生成能不能用,比如模型服务商是不是 OpenAI;再把这个判断结果存到当前对话线程的小仓库里。等系统询问“现在有哪些工具可以给模型用”时,它会再次检查:配置允许吗?当前登录方式是不是能走 Codex 后端?都满足才创建真正的 ImageGenerationTool。这样做的好处是,图片工具不会在不支持的模型、不合适的登录状态下冒出来,避免用户点了却用不了。最后的 install 函数把同一个扩展对象注册成三个角色:线程启动时参与、配置变化时参与、工具列表生成时参与。
ImageGenerationExtensionConfig::from35–42 ↗
fn from(config: &Config) -> Self
作用:这个函数把一份完整的全局配置,浓缩成图片生成扩展真正关心的几项信息。有人需要判断图片功能是否可用时,就用它生成一份简化版配置。
数据流:进去的是 Config,也就是系统当前的配置。它读取模型服务商、Codex 主目录等信息,判断模型服务商是不是 OpenAI;然后产出 ImageGenerationExtensionConfig,里面包含“是否可用”、服务商信息和本地目录位置。
调用关系:它在线程刚开始时由 ImageGenerationExtension::on_thread_start 调用,也在配置改变时由 ImageGenerationExtension::on_config_changed 调用。它相当于统一的“配置翻译器”,避免两个地方各自写一套判断规则。
调用图:被 2 处调用(on_config_changed, on_thread_start)。
ImageGenerationExtension::on_thread_start47–56 ↗
fn on_thread_start(
&'a self,
input: ThreadStartInput<'a, Config>,
) -> ExtensionFuture<'a, ()>
作用:这个函数在一个新对话线程开始时,先把图片生成相关的可用性信息准备好。这样后面系统问“有哪些工具可用”时,不用重新从大配置里翻找。
数据流:进去的是 ThreadStartInput,里面带着当前配置和线程自己的数据仓库。它调用 ImageGenerationExtensionConfig::from 把配置转成图片扩展的小配置,再把这份小配置塞进 thread_store。结果是当前线程记住了图片生成是否可用、该用哪个服务商、文件该放在哪里。
调用关系:它是线程生命周期的一部分,由扩展系统在线程启动时调用。它把具体判断交给 ImageGenerationExtensionConfig::from,自己主要负责把结果存进线程数据里;异步返回值通过 pin 包起来,方便系统按统一方式等待它完成。
调用图:调用 1 个内部函数(from);外部调用 1 个(pin)。
ImageGenerationExtension::on_config_changed61–69 ↗
fn on_config_changed(
&self,
_session_store: &ExtensionData,
thread_store: &ExtensionData,
_previous_config: &Config,
new_config: &Config,
)
作用:这个函数在对话线程的配置发生变化后,刷新图片生成工具的可用状态。否则用户切换模型或服务商后,工具列表可能还停留在旧状态。
数据流:进去的是旧配置、新配置,以及线程数据仓库。它不使用旧配置,而是拿新配置调用 ImageGenerationExtensionConfig::from,生成新的图片扩展配置,再写回 thread_store。之后这个线程里保存的图片生成状态就更新成最新的了。
调用关系:它由扩展系统在配置变更时调用。它和 ImageGenerationExtension::on_thread_start 使用同一个 ImageGenerationExtensionConfig::from,所以线程初始状态和后续刷新状态的判断标准是一致的。
ImageGenerationExtension::tools74–94 ↗
fn tools(
&self,
_session_store: &ExtensionData,
thread_store: &ExtensionData,
) -> Vec<Arc<dyn ToolExecutor<ToolCall>>>
作用:这个函数回答一个关键问题:当前线程里,要不要把“生成图片”这个工具交给模型使用。只有配置允许、登录方式也合适时,它才真正创建工具。
数据流:进去的是会话数据、线程数据,以及扩展里保存的登录管理器。它先从 thread_store 取出图片扩展配置;如果没有配置、配置说不可用,或者当前认证不能使用 Codex 后端,就返回空列表。条件都满足时,它创建模型服务提供者,再创建 CodexImagesBackend,最后包装成 ImageGenerationTool 放进列表返回。
调用关系:它在系统收集可用工具时被调用。前面的 ImageGenerationExtension::on_thread_start 或 ImageGenerationExtension::on_config_changed 会先把配置放进 thread_store;这个函数再读取那份配置,并把真正执行图片生成的工作交给 ImageGenerationTool 和 CodexImagesBackend。
install98–103 ↗
fn install(registry: &mut ExtensionRegistryBuilder<Config>, auth_manager: Arc<AuthManager>)
作用:这个函数负责安装图片生成扩展。没有它,扩展对象不会被注册,系统也就不知道线程启动、配置变化、工具生成时要通知图片生成这部分代码。
数据流:进去的是扩展注册表和登录管理器。它用登录管理器创建 ImageGenerationExtension,然后把同一个扩展对象分别注册为线程生命周期贡献者、配置贡献者和工具贡献者。结果是图片生成扩展正式接入系统运行流程。
调用关系:它通常在程序启动或扩展加载阶段被调用。它调用 registry 的 thread_lifecycle_contributor、config_contributor 和 tool_contributor,把后续工作交给 ImageGenerationExtension::on_thread_start、ImageGenerationExtension::on_config_changed 和 ImageGenerationExtension::tools。
调用图:调用 3 个内部函数(config_contributor, thread_lifecycle_contributor, tool_contributor);外部调用 1 个(new)。
ext/image-generation/src/tool.rs源码 ↗
这个文件像一个“图片制作窗口”。模型说要画什么,或者要拿哪几张图来改,它就先检查参数是不是合理:最多 5 张参考图,不能同时用“指定路径”和“最近几张图”两种方式。然后它决定是走“从零生成图片”,还是走“基于图片编辑”。如果需要本地参考图,它会通过当前工具环境读取文件,并转成图片 API 能吃的 data URL(一种把图片内容直接塞进字符串里的格式)。执行时,它会先告诉界面“图片任务开始了”,再调用真正的图片后端。成功后,它把返回的 base64 图片数据包装成工具输出;失败时,它把错误消息反馈给模型。最后,它还会算出一个建议的图片保存位置提示,方便后续代码模式或模型继续使用这张图。
ImageGenerationTool::new60–70 ↗
fn new(
backend: CodexImagesBackend,
codex_home: AbsolutePathBuf,
thread_id: String,
) -> Self
作用:创建一个图片生成工具实例。外面会把真正干活的图片后端、Codex 的主目录、当前对话线程编号交给它,之后每次图片请求都靠这些信息执行。
数据流:进去的是图片后端、主目录路径和线程编号 → 函数把它们存进 ImageGenerationTool 这个工具对象里 → 出来的是一个可以被工具系统注册和调用的图片工具。
调用关系:这是工具被装配时用的入口。它不调用别的业务函数,只是把后面 handle_call 需要用到的材料提前放好。
ImageGenerationTool::tool_name85–87 ↗
fn tool_name(&self) -> ToolName
作用:告诉工具系统:这个工具叫什么名字,并且属于哪个命名空间。命名空间可以理解成工具的“文件夹”,避免不同工具重名。
数据流:进去的是当前工具对象 → 它取固定的图片命名空间和工具名,组合成一个带命名空间的名字 → 出来的是工具系统能识别的 ToolName。
调用关系:工具系统在登记或匹配模型调用时会问它名字。它把组合名字这件事交给 namespaced 来做。
调用图:调用 1 个内部函数(namespaced)。
ImageGenerationTool::spec90–92 ↗
fn spec(&self) -> ToolSpec
作用:告诉模型这个工具怎么用:需要哪些参数、每个参数大概是什么意思。没有这个说明,模型就容易传错字段或不知道能不能带参考图。
数据流:进去的是当前工具对象 → 它调用 imagegen_tool_spec 生成完整的工具说明书 → 出来的是 ToolSpec,也就是暴露给模型看的接口定义。
调用关系:工具系统在向模型公布可用工具时会调用它。真正构造说明书的细节交给 imagegen_tool_spec。
调用图:调用 1 个内部函数(imagegen_tool_spec)。
ImageGenerationTool::exposure95–97 ↗
fn exposure(&self) -> ToolExposure
作用:说明这个图片工具可以被直接调用。也就是说,模型不需要绕一层别的工具,就能发起图片生成请求。
数据流:进去的是当前工具对象 → 它返回固定的暴露方式 Direct → 工具系统据此知道这个工具应直接出现在可用工具列表里。
调用关系:这是工具注册阶段的一部分。它不依赖别的函数,只给工具平台一个简单开关。
ImageGenerationTool::handle100–102 ↗
fn handle(&self, call: ToolCall) -> codex_extension_api::ToolExecutorFuture<'_>
作用:这是工具真正被调用时的统一入口。它把一次模型发来的工具调用,交给异步函数去慢慢处理,因为生成图片可能要等网络 API 返回。
数据流:进去的是一次 ToolCall,里面有模型传来的参数、历史记录、环境等 → 它把 handle_call 包成一个异步任务 → 出来的是工具系统可以等待的未来结果。
调用关系:工具系统收到图片工具调用时会先进这里。它本身只负责转交,实际解析参数、读图片、调用后端都由 handle_call 完成。
调用图:调用 1 个内部函数(handle_call);外部调用 1 个(pin)。
ImageGenerationTool::handle_call106–168 ↗
async fn handle_call(&self, call: ToolCall) -> Result<Box<dyn ToolOutput>, FunctionCallError>
作用:处理一次完整的图片请求:解析模型参数,决定生成还是编辑,调用图片后端,并把成功或失败结果报告回去。它是这个文件里最核心的执行流程。
数据流:进去的是一次工具调用,包含 JSON 参数、对话历史、可访问文件环境和回调发射器 → 它先用 parse_args 读参数,再用 request_for_call_args 组装图片 API 请求,然后发出“开始”事件,调用后端的 generate 或 edit,最后把返回的 base64 图片包装成 GeneratedImageOutput;如果出错,就发出“失败”事件并把错误告诉模型 → 出来的是工具输出,里面有图片 data URL 和可选的保存位置提示,同时界面/对话流也收到开始和完成状态。
调用关系:handle 会把调用交给它。它像总调度员一样,依次使用 parse_args、request_for_call_args、后端的 generate/edit,还会用外部函数算图片产物路径和输出提示。
调用图:调用 4 个内部函数(edit, generate, parse_args, request_for_call_args);被 1 处调用(handle);外部调用 6 个(new, new, extension_image_generation_output_hint, image_generation_artifact_path, RespondToModel, ImageGeneration)。
request_for_call_args177–246 ↗
async fn request_for_call_args(
args: &ImagegenArgs,
history: &[ResponseItem],
environments: &[ToolEnvironment],
) -> Result<ImageRequest, FunctionCallError>
作用:根据模型传来的参数,判断这次应该“生成新图”还是“编辑已有图”,并准备好图片 API 需要的请求对象。它也负责把不合理的参数尽早拦住。
数据流:进去的是解析后的参数、对话历史、工具运行环境 → 它检查参考图片数量,判断是否有路径、是否要使用最近几张历史图片;没有参考图就构造生成请求,有参考图就读取或查找图片并构造编辑请求;遇到数量超限、两种参考方式同时使用、历史图片不够等情况就返回给模型看的错误 → 出来的是 ImageRequest::Generate 或 ImageRequest::Edit。
调用关系:handle_call 在调用后端前会先找它要请求对象。它需要本地路径时会调用 image_url,需要历史图片时会调用 recent_images。
调用图:调用 2 个内部函数(image_url, recent_images);被 1 处调用(handle_call);外部调用 6 个(with_capacity, Edit, Generate, format!, RespondToModel, first)。
recent_images248–324 ↗
fn recent_images(history: &[ResponseItem], count: usize) -> Vec<ImageUrl>
作用:从对话历史里找出最近出现过的几张图片,供“基于最近图片继续编辑”使用。它让模型不用知道图片文件路径,也能引用刚刚生成或上传过的图。
数据流:进去的是完整对话历史和想要的图片数量 → 它先记住历史里真实发生过的工具调用编号,避免把无关输出误当成图片来源;再从后往前扫描消息、工具输出和图片生成记录,收集图片 URL;收够后再反转顺序,让返回结果保持从旧到新的自然顺序 → 出来的是最多指定数量的 ImageUrl 列表。
调用关系:request_for_call_args 在用户要求 num_last_images_to_include 时会调用它。它内部会用 output_image_urls 从工具输出内容里挑出图片。
调用图:调用 1 个内部函数(output_image_urls);被 1 处调用(request_for_call_args);外部调用 5 个(new, new, with_capacity, format!, iter)。
output_image_urls327–338 ↗
fn output_image_urls(output: &FunctionCallOutputPayload) -> impl Iterator<Item = String> + '_
作用:从一次工具输出里挑出图片地址,并按“最新的在前面”的顺序返回。这样历史扫描时可以优先拿到最近出现的图片。
数据流:进去的是工具输出负载 → 它展开里面的内容项,只保留图片项,跳过文字和加密内容 → 出来的是一个逐个吐出图片 URL 的迭代器。
调用关系:recent_images 在扫描函数工具或自定义工具的输出时会调用它。它只做筛选,不决定最终要几张图。
调用图:调用 1 个内部函数(content_items);被 1 处调用(recent_images)。
image_url340–367 ↗
async fn image_url(
path: &AbsolutePathBuf,
environment: &ToolEnvironment,
) -> Result<ImageUrl, FunctionCallError>
作用:把一个本地图片文件变成图片 API 可直接使用的图片地址字符串。这里的“地址”不是普通网址,而是 data URL,也就是把图片内容编码后放在字符串里。
数据流:进去的是绝对文件路径和当前工具环境 → 它把路径转成可读取的 URI,按沙箱规则读取文件字节,再用图片工具库检查和加载图片,最后转成 data URL → 出来的是 ImageUrl;如果文件读不到或图片处理失败,就返回能给模型看的错误。
调用关系:request_for_call_args 在模型提供 referenced_image_paths 时会逐个调用它。它依赖工具环境里的文件系统,也依赖 load_for_prompt_bytes 来把原始文件内容变成可发给模型/图片服务的格式。
调用图:调用 2 个内部函数(as_path, from_abs_path);被 1 处调用(request_for_call_args);外部调用 1 个(load_for_prompt_bytes)。
parse_args370–373 ↗
fn parse_args(call: &ToolCall) -> Result<ImagegenArgs, FunctionCallError>
作用:把模型传来的 JSON 字符串解析成程序内部好用的参数结构。它相当于先把“用户填的表单”读成固定格式。
数据流:进去的是一次工具调用 → 它取出函数参数字符串,并用 JSON 解析器转成 ImagegenArgs → 出来的是包含提示词、参考路径、最近图片数量的参数对象;如果 JSON 格式不对,就返回给模型看的错误。
调用关系:handle_call 一开始就会调用它。只有参数成功解析,后面的 request_for_call_args 才能继续判断生成还是编辑。
调用图:被 1 处调用(handle_call);外部调用 2 个(function_arguments, from_str)。
imagegen_tool_spec376–406 ↗
fn imagegen_tool_spec() -> ToolSpec
作用:生成暴露给模型的工具说明书,包括工具名、描述和输入参数格式。它决定模型看到的“图片工具使用规则”。
数据流:进去没有运行时输入,只使用代码里定义的参数结构和说明文本 → 它用 schema 生成器把 ImagegenArgs 变成 JSON Schema(可以理解成 JSON 参数的规则表),再抽出需要的字段,组装成图片命名空间下的函数工具定义 → 出来的是 ToolSpec。
调用关系:ImageGenerationTool::spec 会调用它。它把参数规则、工具描述和命名空间说明组合起来,供工具系统展示给模型。
调用图:被 1 处调用(spec);外部调用 7 个(new, draft2019_09, default_namespace_description, to_value, Namespace, unreachable!, vec!)。
GeneratedImageOutput::log_preview415–417 ↗
fn log_preview(&self) -> String
作用:给日志系统一个简短预览,避免把整张图片的 base64 字符串塞进日志。这样日志更清爽,也减少泄露大块图片数据的风险。
数据流:进去的是生成图片输出对象 → 它不读取真正图片内容,只返回固定文字 [generated image] → 出来的是日志里显示的简短字符串。
调用关系:工具框架在记录工具输出时会调用它。它和 code_mode_result、to_response_item 不同,只服务于日志预览。
GeneratedImageOutput::success_for_logging420–422 ↗
fn success_for_logging(&self) -> bool
作用:告诉日志系统:这个输出代表一次成功的图片工具执行。失败情况在前面已经作为错误返回了。
数据流:进去的是生成图片输出对象 → 它直接返回 true → 日志系统据此把这次工具调用记为成功。
调用关系:工具框架写日志时会问它是否成功。它不调用别的函数,也不改变图片结果。
GeneratedImageOutput::code_mode_result425–437 ↗
fn code_mode_result(&self, _payload: &ToolPayload) -> Value
作用:把生成的图片结果转换成代码模式能使用的对象。代码模式里的辅助函数可以从这里拿到图片 data URL 和保存提示。
数据流:进去的是生成图片输出对象和工具负载参数 → 它把 base64 图片前面加上 data:image/png;base64,,形成完整图片 URL;如果有输出提示,也一起放进对象 → 出来的是一个 JSON 对象,通常包含 image_url,可能还包含 output_hint。
调用关系:当工具结果要交给代码模式使用时,工具框架会调用它。它使用的是 handle_call 生成的 GeneratedImageOutput 里保存的图片数据和提示。
GeneratedImageOutput::to_response_item440–457 ↗
fn to_response_item(&self, call_id: &str, _payload: &ToolPayload) -> ResponseInputItem
作用:把生成图片包装成模型下一轮能看到的对话项。这样模型不只是知道“成功了”,还真的能在后续上下文里引用这张图。
数据流:进去的是调用编号、工具负载和生成图片输出对象 → 它把图片做成一个输入图片内容项,并标记默认图片细节;如果有保存位置提示,再追加一条文字内容;最后把这些内容放进函数调用输出,并标记成功 → 出来的是 ResponseInputItem::FunctionCallOutput。
调用关系:工具框架在把工具结果写回对话历史时会调用它。recent_images 之后扫描历史时,也可能从这类输出里重新找到图片。
调用图:外部调用 2 个(ContentItems, vec!)。
ext/goal/src/lib.rs源码 ↗
可以把这个文件想成一家店的前台。店里面有记账、分析、接口、事件、运行时、工具等不同房间,但外人不需要知道每个房间怎么走,只要从前台拿到能用的服务就行。这里先声明了 accounting、analytics、api、runtime、tool 等内部模块,让 Rust 编译器知道这些部分属于同一个扩展。然后它用 pub use 把常用的东西转交给外部使用,比如 GoalService 是操作目标的服务,GoalExtension 是安装这个扩展的入口,几个 *_TOOL_NAME 是工具名字常量,CreateGoalRequest 是创建目标时用的请求数据。没有这个文件,外部代码就要到处深入内部模块找东西,既麻烦也容易依赖到不该依赖的内部细节。
ext/goal/src/tool.rs源码 ↗
可以把这个文件理解成“目标工具的前台柜台”。模型来调用 get_goal、create_goal、update_goal 时,不会直接碰数据库,而是先到这里。这里会看调用的是哪种工具,解析参数,检查目标内容和预算是否合法,然后读写线程里的目标。创建目标时,它会顺手把空的线程预览填成目标文字,让列表里更容易看懂。更新目标时,它只允许模型把目标标成完成或受阻,像暂停、恢复、预算耗尽这类状态必须由用户或系统控制,防止模型越权。它还会把本轮消耗的时间和 token(可以理解成模型使用量)记到账上,发出“目标已更新”的事件,写入分析和指标。最后,它把内部保存格式转成对外协议格式,再用 JSON 返回给调用方。
GoalToolExecutor::get76–93 ↗
fn get(
thread_id: ThreadId,
state_db: Arc<codex_state::StateRuntime>,
accounting_state: Arc<GoalAccountingState>,
analytics: GoalAnalytics,
event_emitter: Goal
作用:创建一个专门执行“查看目标”工具的执行器。外部需要给某个线程配一个 get_goal 工具时,会用它来组装好所需的数据库、统计、事件等依赖。
数据流:进去的是线程编号、状态数据库、用量记账状态、分析器、事件发送器和指标器 → 它把这些东西原样存进一个 GoalToolExecutor,并把种类标记为 Get → 出来的是一个以后能处理“查看目标”调用的执行器。
调用关系:这是三个构造入口之一。后面框架调用这个执行器的 tool_name、spec 和 handle 时,会因为 kind 是 Get 而走查看目标那条路。
GoalToolExecutor::create95–112 ↗
fn create(
thread_id: ThreadId,
state_db: Arc<codex_state::StateRuntime>,
accounting_state: Arc<GoalAccountingState>,
analytics: GoalAnalytics,
event_emitter: G
作用:创建一个专门执行“新建目标”工具的执行器。它让同一套执行器结构可以被配置成 create_goal。
数据流:进去的是线程编号和各种共享服务 → 它保存这些依赖,并把工具种类设为 Create → 出来的是一个会处理创建目标请求的执行器。
调用关系:这是给工具注册阶段用的工厂方法。之后 ToolExecutor::handle 会根据这个 Create 标记,把请求交给 GoalToolExecutor::handle_create。
GoalToolExecutor::update114–131 ↗
fn update(
thread_id: ThreadId,
state_db: Arc<codex_state::StateRuntime>,
accounting_state: Arc<GoalAccountingState>,
analytics: GoalAnalytics,
event_emitter: G
作用:创建一个专门执行“更新目标状态”工具的执行器。主要用于让模型把当前目标标记为完成或受阻。
数据流:进去的是线程编号、状态库、记账、分析、事件和指标对象 → 它把它们装进执行器,并把 kind 设为 Update → 出来的是一个 update_goal 工具执行器。
调用关系:这是 update_goal 的入口配置函数。运行时 handle 会看到 kind 是 Update,于是转到 GoalToolExecutor::handle_update。
GoalToolExecutor::tool_name135–141 ↗
fn tool_name(&self) -> ToolName
作用:告诉外部框架:这个执行器对应哪个工具名字。没有它,框架就不知道模型调用的 get_goal、create_goal 或 update_goal 应该交给谁。
数据流:进去的是执行器自身,里面带着 kind → 它按 kind 选择对应的工具名常量,并用 ToolName::plain 包成框架认识的名字 → 出来的是工具名。
调用关系:工具框架会在注册或匹配调用时问它名字。它内部调用 plain,把普通字符串变成 codex_extension_api 使用的 ToolName。
调用图:调用 1 个内部函数(plain)。
GoalToolExecutor::spec143–149 ↗
fn spec(&self) -> ToolSpec
作用:返回这个工具的说明书。说明书会告诉模型这个工具能做什么、需要哪些参数。
数据流:进去的是执行器自身的 kind → 它选择创建查看、创建或更新工具说明的函数 → 出来的是 ToolSpec,也就是工具规格。
调用关系:工具注册或展示给模型时会调用它。它把具体说明书的生成工作交给 create_get_goal_tool、create_create_goal_tool 或 create_update_goal_tool。
调用图:调用 3 个内部函数(create_create_goal_tool, create_get_goal_tool, create_update_goal_tool)。
GoalToolExecutor::handle151–159 ↗
fn handle(&self, invocation: ToolCall) -> codex_extension_api::ToolExecutorFuture<'_>
作用:这是工具真正被调用时的总入口。它像分诊台一样,看请求属于哪种目标工具,再转给对应处理函数。
数据流:进去的是一次 ToolCall,也就是模型发来的工具调用 → 它创建一个异步任务,并按 kind 分派到查看、创建或更新处理 → 出来的是将来完成的工具结果,可能成功返回 JSON,也可能返回给模型看的错误。
调用关系:ToolExecutor 框架在模型调用工具时会调用它。它不会自己做具体业务,而是把活儿交给 GoalToolExecutor::handle_get、GoalToolExecutor::handle_create 或 GoalToolExecutor::handle_update。
调用图:调用 3 个内部函数(handle_create, handle_get, handle_update);外部调用 1 个(pin)。
GoalToolExecutor::handle_get163–178 ↗
async fn handle_get(
&self,
invocation: ToolCall,
) -> Result<Box<dyn ToolOutput>, FunctionCallError>
作用:处理“查看当前目标”的请求。它读取这个线程现在有没有目标,并把结果包装成工具返回值。
数据流:进去的是一次工具调用 → 它先读取并校验调用参数格式,然后从状态数据库取当前线程的目标,把内部目标格式转成对外格式 → 出来的是 JSON 工具输出;如果数据库读取失败,就返回一条模型能看到的错误。
调用关系:它由 GoalToolExecutor::handle 在 kind 为 Get 时调用。它最后调用 goal_response 来统一生成返回格式。
调用图:调用 1 个内部函数(goal_response);被 1 处调用(handle);外部调用 1 个(function_arguments)。
GoalToolExecutor::handle_create180–219 ↗
async fn handle_create(
&self,
invocation: ToolCall,
) -> Result<Box<dyn ToolOutput>, FunctionCallError>
作用:处理“创建目标”的请求。它负责把模型给出的目标文字和可选预算变成一条真正存下来的线程目标。
数据流:进去的是工具调用,里面带 JSON 参数 → 它解析参数,修剪目标文字两边空格,检查目标文字和 token 预算是否合法,然后向数据库插入新目标;成功后会填线程预览、标记本轮目标活跃、记录指标和分析事件、发出目标更新事件 → 出来的是包含新目标的 JSON 返回;如果已有未完成目标或输入不合法,就返回错误。
调用关系:它由 GoalToolExecutor::handle 在 Create 分支调用。过程中会用 parse_arguments、validate_goal_budget、fill_empty_thread_preview_if_possible、protocol_goal_from_state、emit_goal_updated_from_tool_call 和 goal_response,并通知 metrics 与 analytics。
调用图:调用 9 个内部函数(created, record_created, emit_goal_updated_from_tool_call, fill_empty_thread_preview_if_possible, goal_response, parse_arguments, protocol_goal_from_state, validate_goal_budget, validate_thread_goal_objective);被 1 处调用(handle);外部调用 2 个(function_arguments, Turn)。
GoalToolExecutor::handle_update221–291 ↗
async fn handle_update(
&self,
invocation: ToolCall,
) -> Result<Box<dyn ToolOutput>, FunctionCallError>
作用:处理“更新目标状态”的请求。它主要允许模型把目标标成完成或受阻,同时确保用量先结算好。
数据流:进去的是工具调用,参数里有目标的新状态 → 它解析参数,只接受 Complete 或 Blocked;然后先把当前活跃目标的时间和 token 用量记到账上,再读取旧状态用于指标比较,更新数据库里的目标状态,记录指标和分析,清掉本轮当前目标,发事件 → 出来的是更新后的目标 JSON;如果完成目标,还可能附带一段提醒模型汇报最终用量的话。
调用关系:它由 GoalToolExecutor::handle 在 Update 分支调用。它会先调用 account_active_goal_progress 做用量结算,再用 current_goal_status_for_metrics、state_status_from_protocol、protocol_goal_from_state、emit_goal_updated_from_tool_call 和 goal_response 完成更新流程。
调用图:调用 9 个内部函数(status_changed, record_terminal_if_status_changed, account_active_goal_progress, current_goal_status_for_metrics, emit_goal_updated_from_tool_call, goal_response, parse_arguments, protocol_goal_from_state, state_status_from_protocol);被 1 处调用(handle);外部调用 5 个(function_arguments, Turn, matches!, RespondToModel, unreachable!)。
GoalToolExecutor::emit_goal_updated_from_tool_call293–301 ↗
fn emit_goal_updated_from_tool_call(
&self,
invocation: &ToolCall,
turn_id: Option<String>,
goal: ThreadGoal,
)
作用:发送“目标已更新”的事件。这样界面、日志或其他监听者能知道目标变化了。
数据流:进去的是工具调用信息、可选的本轮编号和目标内容 → 它取出调用编号作为事件来源,把目标交给事件发送器 → 出去没有普通返回值,但系统里会多出一条 thread_goal_updated 事件。
调用关系:创建目标和更新目标成功后都会调用它。它把具体发送工作交给 event_emitter.thread_goal_updated。
调用图:调用 1 个内部函数(thread_goal_updated);被 2 处调用(handle_create, handle_update)。
GoalToolExecutor::account_active_goal_progress303–368 ↗
async fn account_active_goal_progress(
&self,
mode: codex_state::GoalAccountingMode,
event_id: &str,
budget_limited_goal_disposition: BudgetLimitedGoalDisposition,
作用:把当前活跃目标在这一轮里消耗的时间和 token 记到账上。它保证在改变目标状态前,目标的用量不会漏算。
数据流:进去的是记账模式、事件编号、以及遇到预算耗尽时该怎么处理 → 它先找当前轮次,再拿一个记账许可,读取这一轮的用量快照;然后向数据库登记时间和 token 增量,必要时记录指标、分析事件,更新本地记账状态,并发出目标更新事件 → 出来的是可能被更新后的目标;如果没有当前轮次、没有快照或数据库认为无需更新,就返回空。
调用关系:GoalToolExecutor::handle_update 在真正改状态前会调用它。它内部会用 current_goal_status_for_metrics、protocol_goal_from_state,并通知 metrics、analytics 和 event_emitter。
调用图:调用 6 个内部函数(status_changed, usage_accounted, thread_goal_updated, record_terminal_if_status_changed, current_goal_status_for_metrics, protocol_goal_from_state);被 1 处调用(handle_update);外部调用 1 个(Turn)。
GoalToolExecutor::current_goal_status_for_metrics370–389 ↗
async fn current_goal_status_for_metrics(
&self,
expected_goal_id: Option<&str>,
) -> Result<Option<codex_state::ThreadGoalStatus>, FunctionCallError>
作用:读取当前目标状态,主要给指标系统做“前后状态是否变化”的比较。它不是给用户看的,而是给统计用的。
数据流:进去的是一个可选的期望目标编号 → 它从数据库读取当前线程目标;如果传了期望编号,就确认读到的是同一个目标 → 出来的是目标旧状态,或者没有匹配目标时返回空;读取失败会变成给模型看的错误。
调用关系:GoalToolExecutor::handle_update 和 GoalToolExecutor::account_active_goal_progress 都会在更新前调用它,用来判断是否需要记录终态变化指标。
调用图:被 2 处调用(account_active_goal_progress, handle_update)。
parse_arguments392–398 ↗
fn parse_arguments(arguments: &str) -> Result<T, FunctionCallError>
作用:把工具调用里的 JSON 字符串变成 Rust 结构体。简单说,就是把模型传来的文字参数拆成程序能安全使用的字段。
数据流:进去的是一段 JSON 字符串 → 它用 serde_json::from_str 尝试解析成调用方指定的类型 → 出来的是解析好的参数对象;如果 JSON 格式不对,就返回一条模型能看到的错误。
调用关系:GoalToolExecutor::handle_create 用它解析创建目标参数,GoalToolExecutor::handle_update 用它解析更新状态参数。
调用图:被 2 处调用(handle_create, handle_update);外部调用 1 个(from_str)。
validate_goal_budget400–407 ↗
fn validate_goal_budget(value: Option<i64>) -> Result<(), String>
作用:检查目标预算是否合法。这里的预算如果填写了,就必须是正数,不能是 0 或负数。
数据流:进去的是一个可选的 token 预算 → 如果没有填,直接通过;如果填了且小于等于 0,就返回错误文字 → 出来是成功或失败,不会改动任何数据。
调用关系:GoalToolExecutor::handle_create 创建目标前会调用它,其他设置目标的流程 set_thread_goal 也会用它,防止坏预算进入系统。
调用图:被 2 处调用(set_thread_goal, handle_create)。
goal_response409–416 ↗
fn goal_response(
goal: Option<ThreadGoal>,
completion_budget_report: CompletionBudgetReport,
) -> Result<Box<dyn ToolOutput>, FunctionCallError>
作用:把目标结果统一包装成工具能返回的 JSON。这样查看、创建、更新目标时,返回格式保持一致。
数据流:进去的是可选目标和是否需要完成预算报告的模式 → 它先用 GoalToolResponse::new 组装响应对象,再把对象转成 JSON 值,最后包成 JsonToolOutput → 出来的是工具输出;如果序列化失败,会返回致命错误。
调用关系:GoalToolExecutor::handle_get、GoalToolExecutor::handle_create 和 GoalToolExecutor::handle_update 都用它收尾。它负责调用 GoalToolResponse::new 来算剩余 token 和可选报告。
调用图:调用 2 个内部函数(new, new);被 3 处调用(handle_create, handle_get, handle_update);外部调用 2 个(new, to_value)。
GoalToolResponse::new419–436 ↗
fn new(goal: Option<ThreadGoal>, report_mode: CompletionBudgetReport) -> Self
作用:生成目标工具返回体。它除了放入目标本身,还会顺手算出还剩多少 token,以及是否要提醒模型汇报最终用量。
数据流:进去的是一个可选目标和报告模式 → 它根据 token_budget 减 tokens_used 算 remaining_tokens,最低不低于 0;如果模式要求且目标已完成,就尝试生成 completion_budget_report → 出来的是 GoalToolResponse 结构体。
调用关系:goal_response 会调用它,然后再把这个结构体转成 JSON。completion_budget_report 的文字是否出现,主要由它在完成目标时决定。
调用图:被 1 处调用(goal_response)。
fill_empty_thread_preview_if_possible439–452 ↗
async fn fill_empty_thread_preview_if_possible(
state_db: &codex_state::StateRuntime,
thread_id: ThreadId,
goal: &codex_state::ThreadGoal,
)
作用:如果线程列表里的预览文字还是空的,就用目标内容填一下。这样用户在列表里看到的不是空白,而是这个线程想做什么。
数据流:进去的是状态数据库、线程编号和刚创建的目标 → 它尝试把线程预览设为目标 objective,但只在预览为空时生效 → 出去没有业务返回;如果设置失败,只写警告日志,不让创建目标失败。
调用关系:GoalToolExecutor::handle_create 创建目标后会调用它,set_thread_goal 这类外部设置流程也会调用它。它把实际写入交给 state_db.set_thread_preview_if_empty。
调用图:被 2 处调用(set_thread_goal, handle_create);外部调用 2 个(set_thread_preview_if_empty, warn!)。
protocol_goal_from_state454–465 ↗
fn protocol_goal_from_state(goal: codex_state::ThreadGoal) -> ThreadGoal
作用:把数据库里的目标格式转换成对外协议里的目标格式。内部存法和外部返回给模型或界面的格式不完全一样,所以需要这个翻译器。
数据流:进去的是 codex_state::ThreadGoal,也就是状态库里的目标 → 它复制线程编号、目标文字、预算、用量和时间戳,并把内部状态枚举转成协议状态枚举 → 出来的是 codex_protocol::protocol::ThreadGoal。
调用关系:很多地方都会用它,包括创建目标、记账更新、外部设置目标、空闲继续、停止目标等流程。它内部调用 protocol_status_from_state 来翻译状态。
调用图:调用 1 个内部函数(protocol_status_from_state);被 9 处调用(set_thread_goal, account_active_goal_progress, account_idle_goal_progress, apply_external_goal_set, continue_if_idle, stop_active_goal_for_turn, account_active_goal_progress, handle_create, handle_update)。
protocol_status_from_state467–476 ↗
fn protocol_status_from_state(status: codex_state::ThreadGoalStatus) -> ThreadGoalStatus
作用:把内部保存的目标状态翻译成对外协议使用的目标状态。比如内部的 Active 会变成协议里的 Active。
数据流:进去的是 codex_state::ThreadGoalStatus → 它逐个匹配可能的状态:活跃、暂停、受阻、用量受限、预算受限、完成 → 出来的是对应的 ThreadGoalStatus。
调用关系:protocol_goal_from_state 在转换整个目标时会调用它。它是状态格式转换里的小零件。
调用图:被 1 处调用(protocol_goal_from_state)。
state_status_from_protocol478–489 ↗
fn state_status_from_protocol(
status: ThreadGoalStatus,
) -> codex_state::ThreadGoalStatus
作用:把外部协议里的目标状态翻译回数据库内部使用的状态。更新目标时必须做这一步,数据库才认得这个状态。
数据流:进去的是 ThreadGoalStatus → 它按状态逐一映射到 codex_state::ThreadGoalStatus → 出来的是能写入状态库的内部状态。
调用关系:GoalToolExecutor::handle_update 在保存新状态前调用它。它和 protocol_status_from_state 是一对相反方向的翻译函数。
调用图:被 1 处调用(handle_update)。
completion_budget_report491–500 ↗
fn completion_budget_report(goal: &ThreadGoal) -> Option<String>
作用:在目标完成时,决定要不要给模型一段提示,让它向用户汇报最终用量。它不是实际报告,而是告诉模型该从结构化字段里取哪些数字。
数据流:进去的是一个对外目标 → 如果目标没有 token 预算且用时也不大于 0,就返回空;否则返回一段固定提示文字,要求模型汇报 token 使用量和耗时 → 出来的是可选的提示字符串。
调用关系:GoalToolResponse::new 在完成目标且要求包含报告时会用到它。这样 handle_update 完成目标后,返回里可能带上这段提醒。
Memories 命名空间与工作区支持
本组涵盖 memories 扩展,从 crate 入口到本地后端操作、工具封装器,以及内存写入流程使用的相关工作区准备。
ext/memories/src/lib.rs源码 ↗
可以把这个文件理解成“memories 扩展的目录页和规章牌”。它本身不做具体的记忆读写,但它把 backend、tools、schema、metrics 等模块接进来,让整个扩展能被编译成一个整体。同时,它把 extension::install 公开出去,外部系统只需要调用这个安装入口,就能启用这套记忆工具。文件里还定义了很多固定上限,比如列表最多 2000 条、搜索最多 200 条、读取最多 20000 个 token。token 可以粗略理解成模型读文字时使用的“字块单位”。这些限制很重要,因为没有上限的话,一次操作可能返回太多内容,既慢又浪费资源,甚至影响系统稳定性。最后,它还统一规定了工具命名空间和工具名字,像给每个工具挂上固定门牌号,方便别的地方准确找到它们。
ext/memories/src/local/ad_hoc_note.rs源码 ↗
这份代码像一个“本地笔记投递口”。外部传来一段笔记内容和一个文件名,它先检查文件名是不是规定格式,比如必须是“时间-简短名字.md”,名字不能太长,只能用小写字母、数字和短横线。这样做是为了让文件容易排序、容易找,也避免有人用奇怪路径写到别处。接着它确认保存目录存在:从后端根目录开始,一层层创建 extensions/ad_hoc/notes。每一层都会检查是不是普通目录,并拒绝符号链接(符号链接可以理解成“快捷方式”,可能把写入偷偷指向别的位置)。最后它用“只新建、不覆盖”的方式写文件;如果同名笔记已经存在,就明确报错。这样,临时记忆既能被保存下来,又不会误删旧内容或突破本地存储边界。
add_ad_hoc_note17–40 ↗
async fn add_ad_hoc_note(
backend: &LocalMemoriesBackend,
request: AddAdHocMemoryNoteRequest,
) -> Result<AddAdHocMemoryNoteResponse, MemoriesBackendError>
作用:这是添加临时记忆笔记的主入口。有人想把一段临时笔记保存到本地时,就会走这里;它负责先把输入查干净,再真正写入文件。
数据流:进去的是本地后端信息和一个请求,请求里有文件名和笔记正文。它先让 validate_filename 检查文件名,再拒绝空白笔记,然后让 ensure_notes_dir 准备好保存目录,最后用“新建文件”的方式写入正文。出来的是一个成功响应;如果文件名不合格、正文为空、目录有问题、文件已存在或写入失败,就出来对应错误。
调用关系:它位在整个临时笔记保存流程的最前面,外部同名的添加笔记流程会调用它。它自己不负责细查每个小规则,而是把文件名检查交给 validate_filename,把目录准备交给 ensure_notes_dir;等这些都过关后,它才打开文件并写入内容。
调用图:调用 2 个内部函数(ensure_notes_dir, validate_filename);被 1 处调用(add_ad_hoc_note);外部调用 1 个(new)。
ensure_notes_dir42–52 ↗
async fn ensure_notes_dir(
backend: &LocalMemoriesBackend,
) -> Result<std::path::PathBuf, MemoriesBackendError>
作用:这个函数负责确认临时笔记应该放的那串目录真的存在。它就像保存前先把抽屉和文件夹都摆好,避免后面写文件时找不到地方。
数据流:进去的是本地后端,里面带着存储根目录。它先确认根目录可用,然后依次拼上 extensions、ad_hoc、notes,每拼一层就调用 ensure_directory 检查或创建这一层。出来的是最终笔记目录的完整路径;如果任何一层不是安全的目录或创建失败,就返回错误。
调用关系:它只在 add_ad_hoc_note 准备写文件前被调用。它不直接碰笔记内容,只负责把保存位置准备好;具体每个目录是否安全、是否需要创建,则继续交给 ensure_directory。
调用图:调用 1 个内部函数(ensure_directory);被 1 处调用(add_ad_hoc_note)。
ensure_directory54–82 ↗
async fn ensure_directory(path: &Path) -> Result<(), MemoriesBackendError>
作用:这个函数保证某个路径是一个真实、安全的目录。它特别会拒绝符号链接,防止看起来像目录,实际却把程序带到别的地方。
数据流:进去的是一个路径。它先读取这个路径的元数据,也就是文件系统告诉它“这里是什么东西”;如果已经存在,就检查它不是符号链接,并且必须是目录。如果不存在,就创建目录,再重新读取元数据确认确实创建成功、不是符号链接、也是目录。出来是成功确认;如果路径不存在后仍找不到、不是目录、是危险链接或创建失败,就返回错误。
调用关系:ensure_notes_dir 每准备一层目录都会调用它。它会用 metadata_or_none 读取文件信息,用 reject_symlink 做安全检查,用 create_dir 真正建目录;发现路径类型不对时,会通过 invalid_path 生成清楚的错误。
调用图:调用 3 个内部函数(invalid_path, metadata_or_none, reject_symlink);被 1 处调用(ensure_notes_dir);外部调用 2 个(display, create_dir)。
validate_filename84–126 ↗
fn validate_filename(filename: &str) -> Result<(), MemoriesBackendError>
作用:这个函数检查临时笔记的文件名是否符合固定规矩。它的作用是挡住太长、格式混乱或可能带来安全问题的名字。
数据流:进去的是一个文件名字符串。它先检查总长度不能超过 128 字节,再要求必须以 .md 结尾;去掉 .md 后,前面必须像 YYYY-MM-DDTHH-MM-SS- 这样的时间前缀,后面必须有 1 到 80 字节的 slug,也就是便于阅读的短名字。slug 只能包含小写英文字母、数字和短横线。全部通过就返回成功;任何一步不合格就返回说明原因的文件名错误。
调用关系:add_ad_hoc_note 在写任何文件之前会先调用它,确保不合规的名字根本进不了写入阶段。它自己把时间前缀的细查交给 has_valid_timestamp_prefix,错误消息则通过 invalid_filename 生成。
调用图:调用 2 个内部函数(invalid_filename, has_valid_timestamp_prefix);被 1 处调用(add_ad_hoc_note)。
has_valid_timestamp_prefix128–143 ↗
fn has_valid_timestamp_prefix(stem: &str) -> bool
作用:这个函数判断文件名前半段是不是长得像规定的时间戳。它不解析真实日期,只检查数字和分隔符的位置是不是对。
数据流:进去的是去掉 .md 后的文件名主体。它把字符串当成字节来看,检查长度够不够,以及第 5、8、11、14、17、20 个关键位置是不是规定的 -, T, - 等分隔符;同时把年、月、日、时、分、秒这些位置交给 are_digits 确认全是数字。出来的是 true 或 false,表示这个时间前缀格式看起来是否合格。
调用关系:它由 validate_filename 调用,是文件名检查中的一个小关卡。它专门负责时间戳格式,把“这几段是不是数字”的重复判断交给 are_digits。
调用图:调用 1 个内部函数(are_digits);被 1 处调用(validate_filename)。
are_digits145–147 ↗
fn are_digits(bytes: &[u8]) -> bool
作用:这个小工具判断一段字节是不是全都是数字。它让时间戳检查不用反复写同样的判断。
数据流:进去的是一小段字节,比如年份的四个字符或月份的两个字符。它逐个查看每个字节是不是 ASCII 数字,也就是普通的 0 到 9。出来的是 true 或 false;它不改动任何外部数据。
调用关系:它被 has_valid_timestamp_prefix 用来检查时间戳里的年、月、日、时、分、秒。它是最底层的小零件,只回答“这一段是不是纯数字”这个问题。
调用图:被 1 处调用(has_valid_timestamp_prefix)。
ext/memories/src/local/list.rs源码 ↗
这个文件解决的是“用户想看某个记忆路径下面有哪些东西”的问题。它先把用户给的路径变成后端允许访问的真实本地路径,防止用户跑到根目录之外。然后它检查游标,也就是“从第几条结果继续看”的位置;游标不合法就直接报错。接着它读取目标路径的信息:如果目标不存在,就返回“找不到”;如果是符号链接,也就是一个指向别处的快捷入口,它会拒绝,避免绕过安全范围。如果目标是文件,就只返回这个文件;如果是文件夹,就读取里面的条目,按顺序排列,跳过隐藏文件、符号链接和不认识的类型,只留下普通文件和文件夹。最后它按照最大返回数量切一段结果,并给出下一个游标,方便调用方像翻页一样继续取。
list14–83 ↗
async fn list(
backend: &LocalMemoriesBackend,
request: ListMemoriesRequest,
) -> Result<ListMemoriesResponse, MemoriesBackendError>
作用:这个函数处理一次“列出本地记忆内容”的请求。它把一个路径下面的文件和文件夹整理成安全、有限、可分页的结果返回给调用方。
数据流:进去的是本地记忆后端和一个列表请求,请求里可能带着路径、最多要多少条、以及从哪里继续的游标。函数先把路径解析到允许访问的本地范围内,再读取文件信息;如果是文件就生成一条结果,如果是文件夹就读取目录、过滤隐藏项和符号链接,再把普通文件和文件夹做成条目。最后它按游标和最大数量截取结果,出来的是 ListMemoriesResponse,里面有本次返回的条目、是否被截断、以及下一次继续读取用的游标;如果路径不存在、游标错误或访问不安全,就返回 MemoriesBackendError。
调用关系:它位于本地记忆后端的“列目录”流程中,外层收到 list 请求时会走到这里。它自己不直接硬写所有细节,而是把路径解析交给 resolve_scoped_path,把读取元数据交给 metadata_or_none,把目录排序读取交给 read_sorted_dir_paths,把隐藏路径判断交给 is_hidden_path,把符号链接拒绝交给 reject_symlink,把显示给用户看的相对路径交给 display_relative_path;如果游标不对,则用 invalid_cursor 生成清楚的错误。
调用图:调用 7 个内部函数(invalid_cursor, metadata_or_none, resolve_scoped_path, display_relative_path, is_hidden_path, read_sorted_dir_paths, reject_symlink);被 1 处调用(list);外部调用 2 个(new, vec!)。
ext/memories/src/local/read.rs源码 ↗
这个文件解决的是“安全、可控地读本地记忆文件”的问题。外部请求会带一个文件路径、从第几行开始读、最多读多少行、最多读多少 token。这里先检查行号和行数这种基本参数,防止传入 0 这种没意义的值。然后它把用户给的路径变成后端允许访问的真实路径,确认文件存在,拒绝符号链接(可以理解成“指向别处的快捷方式”,避免绕过目录限制),并确认目标真的是文件而不是文件夹。之后它把整个文件读成字符串,算出开始字节和结束字节,再截出需要的那一段。最后还会按 token(大致可理解成模型处理文字时的小单位)再截断一次,并告诉调用方内容有没有被截短。整体像一个图书管理员:先确认你借的是馆内合法书本,再按你指定的页码和长度复印,不会让你一次拿走过多内容。
read12–51 ↗
async fn read(
backend: &LocalMemoriesBackend,
request: ReadMemoryRequest,
) -> Result<ReadMemoryResponse, MemoriesBackendError>
作用:这个函数执行一次真正的本地记忆读取请求。它负责把“我要读哪个文件、从哪一行开始、最多读多少”变成一个安全、有限量的读取结果。
数据流:输入是一个本地后端对象和一个读取请求;请求里有相对路径、起始行号、可选的最大行数、最大 token 数。它先检查参数,再通过后端解析出受限制的真实路径,读取文件信息,拒绝不存在的目标、符号链接和非文件目标。接着它把文件内容读进来,用 line_start_byte_offset 找到起始位置,用 line_end_byte_offset 找到结束位置,再用 truncate_text 按 token 上限缩短文本。输出是 ReadMemoryResponse,里面包含原路径、实际起始行号、返回内容,以及是否被截断;如果中途发现问题,就输出对应错误。
调用关系:它是这个文件的主入口,会被上层的读取流程调用来完成本地文件读取。它自己不亲自计算每个行边界,而是把起始行交给 line_start_byte_offset,把结束位置交给 line_end_byte_offset;路径解析、文件元数据查询、拒绝符号链接和文本截断则交给其他模块或外部工具函数完成。
调用图:调用 5 个内部函数(metadata_or_none, resolve_scoped_path, reject_symlink, line_end_byte_offset, line_start_byte_offset);被 1 处调用(read);外部调用 3 个(truncate_text, Tokens, read_to_string)。
line_start_byte_offset53–72 ↗
fn line_start_byte_offset(
content: &str,
line_offset: usize,
) -> Result<usize, MemoriesBackendError>
作用:这个函数把“第几行”转换成字符串里的“从第几个字节开始”。这样后面才能安全地从正确位置切出内容。
数据流:输入是一整段文件内容和一个从 1 开始数的行号。它如果看到行号是 1,就直接返回 0,表示从文件开头开始;否则它逐个字符找换行符,每遇到一个换行就把当前行数加一,直到到达目标行,返回那一行开头的字节位置。如果文件没有那么多行,就返回“行号超过文件长度”的错误。
调用关系:read 在读完文件后会调用它,用来确定截取内容的起点。它只负责找起点,不读文件,也不决定最多读多少;这些事情由 read 和 line_end_byte_offset 配合完成。
调用图:被 1 处调用(read)。
line_end_byte_offset74–90 ↗
fn line_end_byte_offset(content: &str, start_byte: usize, max_lines: Option<usize>) -> usize
作用:这个函数根据起始位置和“最多读多少行”,算出应该截到哪里。它让读取结果可以按行数限制住,不会无意间返回整个文件。
数据流:输入是一整段文件内容、已经算好的起始字节位置,以及一个可选的最大行数。如果没有最大行数,它直接返回文件末尾位置;如果有限制,它就从起始位置往后数换行符,数到指定行数后返回对应的结束字节。如果文件剩下的内容不够那么多行,就返回文件末尾。
调用关系:read 会在拿到起始位置后调用它,用来确定截取内容的终点。它和 line_start_byte_offset 像一把剪刀的两边:一个找从哪里开始剪,一个找剪到哪里结束,最后由 read 真正切出文本。
调用图:被 1 处调用(read)。
ext/memories/src/local/search.rs源码 ↗
这个文件像一个文件柜里的查找员。它先检查用户的搜索词是不是空的、搜索窗口是不是合理,再把用户给的路径变成安全的本地路径,避免跑到允许范围外。接着它会拒绝符号链接(类似快捷方式,可能绕到别的地方),跳过隐藏文件,并按目录顺序遍历文件。每个文本文件会被按行读取,然后按搜索模式判断:任意关键词出现、所有关键词在同一行、或所有关键词在相邻几行内出现。找到后,它不只返回命中的那一行,还会按要求带上前后几行,方便人看上下文。最后它会排序、分页,并给出 next_cursor,让调用方可以继续拿下一页结果。里面的 SearchMatcher 负责把“怎么比较文字”封装起来,比如是否区分大小写,是否做 normalized(把文字简化成只剩字母数字,方便更宽松地匹配)。
search17–89 ↗
async fn search(
backend: &LocalMemoriesBackend,
request: SearchMemoriesRequest,
) -> Result<SearchMemoriesResponse, MemoriesBackendError>
作用:这是本地搜索的一站式入口。外部发来搜索请求时,它负责检查请求、确定搜索起点、执行搜索、排序分页,然后组装成返回结果。
数据流:输入是一份 SearchMemoriesRequest,里面有关键词、路径、匹配方式、是否区分大小写、每页最多多少条等信息。它先清理关键词并拒绝空关键词,再通过 backend.resolve_scoped_path 把用户路径解析到安全范围内,读取元数据确认文件或目录存在,并用 reject_symlink 拒绝符号链接。之后它创建 SearchMatcher,把真正找文件的活交给 search_entries。拿到所有匹配后,它按路径和行号排序,根据 cursor 截取当前页,最后输出 SearchMemoriesResponse,同时可能带上 next_cursor 表示还有下一页。
调用关系:它处在整个搜索流程的最前面,是搜索请求的总调度员。它会调用 SearchMatcher::new 准备匹配规则,再调用 search_entries 去递归扫描文件和目录;遇到路径显示、元数据读取、非法 cursor 等细节时,则交给 display_relative_path、metadata_or_none、invalid_cursor 等辅助能力处理。
调用图:调用 7 个内部函数(invalid_cursor, metadata_or_none, resolve_scoped_path, display_relative_path, reject_symlink, new, search_entries);被 1 处调用(search);外部调用 2 个(new, matches!)。
search_entries91–128 ↗
async fn search_entries(
root: &Path,
current: &Path,
current_metadata: &std::fs::Metadata,
matcher: &SearchMatcher,
context_lines: usize,
matches: &mut Vec<MemorySearchMatch>,
作用:这个函数负责从一个起点开始,把需要搜索的文件找出来。起点可以是单个文件,也可以是一个目录。
数据流:输入是根目录、当前路径、当前路径的元数据、匹配器、上下文行数,以及一个用来收集结果的 matches 列表。它先判断当前路径是不是文件:如果是,就直接调用 search_file 搜它;如果是目录,就用一个待处理列表逐个打开目录,读取排序后的子路径,跳过隐藏路径、已经消失的路径和符号链接。遇到子目录就继续放进待处理列表,遇到普通文件就交给 search_file。输出没有单独返回搜索结果,而是把找到的结果追加到传入的 matches 里。
调用关系:它由 search 调用,是“找哪些文件需要搜”的中间层。它自己不判断关键词是否命中,只负责安全、稳妥地走遍目录树;真正读取文件内容和匹配文字的工作交给 search_file。
调用图:调用 4 个内部函数(metadata_or_none, is_hidden_path, read_sorted_dir_paths, search_file);被 1 处调用(search);外部调用 3 个(is_dir, is_file, vec!)。
search_file130–228 ↗
async fn search_file(
root: &Path,
path: &Path,
matcher: &SearchMatcher,
context_lines: usize,
matches: &mut Vec<MemorySearchMatch>,
) -> Result<(), MemoriesBackendError>
作用:这个函数负责搜索一个具体文件里的内容。它按行检查关键词,并根据不同匹配模式生成一条条搜索结果。
数据流:输入是根目录、文件路径、SearchMatcher、上下文行数,以及结果列表。它先用异步读文件的方式把文件读成字符串;如果文件不是合法文本,就安静跳过,不让搜索因为一个二进制文件坏掉。然后它把内容拆成一行一行,询问 matcher 每行命中了哪些关键词。若模式是 Any,就任意关键词命中即记录;若是 AllOnSameLine,就要求所有关键词都在同一行;若是 AllWithinLines,就在限定行数的窗口里寻找所有关键词,并去掉被更小窗口完全包含的大窗口。每次确认命中后,它调用 build_search_match 生成带路径、行号、上下文和命中词的结果,并追加到 matches。
调用关系:它由 search_entries 在发现普通文件时调用,是实际“读文件并找字”的核心。它依赖 SearchMatcher::matched_query_flags 判断每行是否命中,依赖 SearchMatcher::matched_queries 还原哪些原始关键词命中了,最后把格式化结果交给 build_search_match 生成。
调用图:调用 2 个内部函数(matched_queries, build_search_match);被 1 处调用(search_entries);外部调用 3 个(new, read_to_string, vec!)。
build_search_match230–251 ↗
fn build_search_match(
root: &Path,
path: &Path,
lines: &[&str],
match_start_index: usize,
match_end_index: usize,
context_lines: usize,
matched_queries: Vec<String>,
) ->
作用:这个函数把一次命中整理成对外可读的搜索结果。它会算出显示哪几行上下文、行号是多少、路径该怎么展示。
数据流:输入是根目录、文件路径、文件的所有行、命中的起止行、需要带的上下文行数,以及命中的关键词。它先把命中范围向前后扩展指定的上下文行数,并确保不会越过文件开头或结尾。然后它把这些行拼成一段文本,把真实路径转成相对显示路径,最后输出 MemorySearchMatch。
调用关系:它只在 search_file 确认某段文字确实匹配后被调用。search_file 负责判断“有没有命中”,它负责把“命中”包装成调用方和用户能看懂的一条结果。
调用图:调用 1 个内部函数(display_relative_path);被 1 处调用(search_file)。
SearchMatcher::new261–282 ↗
fn new(
queries: Vec<String>,
match_mode: SearchMatchMode,
case_sensitive: bool,
normalized: bool,
) -> Result<Self, MemoriesBackendError>
作用:这个函数创建一个搜索匹配器,也就是之后用来判断文字是否包含关键词的小工具。它会提前把关键词按大小写和规范化规则处理好,避免每行重复做准备工作。
数据流:输入是原始关键词列表、匹配模式、是否区分大小写、是否 normalized。它先用 SearchComparison::new 建立比较规则,再把每个关键词通过该规则预处理,比如转小写或只保留字母数字。如果处理后有关键词变成空字符串,就返回 EmptyQuery 错误;否则输出一个 SearchMatcher,里面同时保留原始关键词和处理后的关键词。
调用关系:它由 search 在正式扫描文件前调用。它内部依靠 SearchComparison::new 创建文字比较规则,之后 search_file 会通过这个 SearchMatcher 反复检查每一行内容。
SearchMatcher::matched_query_flags284–290 ↗
fn matched_query_flags(&self, line: &str) -> Vec<bool>
作用:这个函数检查一行文字分别命中了哪些关键词。返回的不是关键词文本,而是一串 true/false 标记,表示每个关键词是否出现。
数据流:输入是一行原始文本。它先调用 SearchComparison::prepare,把这一行按同样规则处理,比如转小写或去掉非字母数字字符。然后它把处理后的行和预处理过的关键词逐个比较,生成一个布尔列表:第几个位置为 true,就表示第几个关键词在这一行里出现了。
调用关系:它主要被 search_file 用在每一行上,是文件搜索时最频繁使用的判断动作。它把具体的文字预处理工作交给 SearchComparison::prepare,自己专注于“这一行包含哪些搜索词”。
调用图:调用 1 个内部函数(prepare)。
SearchMatcher::matched_queries292–298 ↗
fn matched_queries(&self, matched_query_flags: &[bool]) -> Vec<String>
作用:这个函数把 true/false 的命中标记翻译回原始关键词。这样返回结果里能告诉用户:这一条结果到底匹配了哪些搜索词。
数据流:输入是一组布尔标记,通常来自 matched_query_flags 或窗口合并后的结果。它把这些标记和原始 queries 一一配对,只挑出标记为 true 的关键词,复制成一个字符串列表输出。
调用关系:它由 search_file 在准备生成搜索结果时调用。search_file 先判断哪些词命中了,再用它拿到可展示的原始关键词,然后交给 build_search_match 放进最终结果。
调用图:被 1 处调用(search_file)。
SearchComparison::new308–313 ↗
fn new(case_sensitive: bool, normalized: bool) -> Self
作用:这个函数创建文字比较规则。它只记录两个开关:搜索时是否区分大小写,以及是否做 normalized 这种简化匹配。
数据流:输入是 case_sensitive 和 normalized 两个布尔值。它不读取文件,也不改外部状态,只把这两个值放进 SearchComparison 结构里并返回。
调用关系:它由 SearchMatcher::new 调用,是创建匹配器时的第一步。之后 SearchComparison::prepare 会根据这里保存的规则处理关键词和文件行。
调用图:被 1 处调用(new)。
SearchComparison::prepare315–335 ↗
fn prepare(self, value: &'a str) -> Cow<'a, str>
作用:这个函数把一段文字变成适合比较的样子。比如不区分大小写时会转成小写;开启 normalized 时,会只保留字母和数字。
数据流:输入是一段字符串。如果既区分大小写又不需要 normalized,它直接借用原字符串,不额外复制。否则它会按规则生成新字符串:可能先转小写,再过滤掉非字母数字字符。输出是 Cow 字符串,Cow 可以理解成“能不复制就不复制,需要改时才复制”的容器。
调用关系:它被 SearchMatcher::matched_query_flags 用来处理每一行,也在 SearchMatcher::new 里通过比较规则处理关键词。这样关键词和文件内容用同一套规则加工,比较结果才一致。
调用图:被 1 处调用(matched_query_flags);外部调用 2 个(Borrowed, Owned)。
ext/memories/src/tools/mod.rs源码 ↗
这份文件像“记忆工具箱”的总装台。真正干活的工具分散在 ad_hoc_note、list、read、search 几个小模块里,这里负责把它们装进同一个工具列表,并给每个工具接上同一个记忆后端和可选的指标记录器。记忆后端可以理解成保存和读取记忆的仓库,指标记录器用来统计工具使用情况。文件还统一生成工具名字和工具说明,这样外部系统看到的是一套带命名空间的规范工具,而不是一堆散乱函数。它也处理两个常见边角问题:一是把模型传来的 JSON 参数解析成程序能用的结构;二是把后端错误翻译成“告诉模型改参数”或“系统致命错误”。没有这些统一入口,各个工具就容易名字不一致、参数解析方式不一样,错误也会变得难判断。
memory_tools28–53 ↗
fn memory_tools(
backend: B,
metrics_client: Option<MetricsClient>,
) -> Vec<Arc<dyn ToolExecutor<ToolCall>>>
作用:把所有记忆相关工具一次性组装出来,交给外部系统注册和调用。调用方不用知道里面有几个具体工具,只要拿到这个列表就能启用整套记忆能力。
数据流:进去的是一个记忆后端和一个可选的指标记录器。函数把同一个后端复制给新增临时笔记、列出、读取、搜索这四个工具,并把它们放进一个列表里。出来的是一组可执行工具,每个工具都能独立响应一次工具调用。
调用关系:它在启动或装载记忆扩展时被 tools、memory_tool 这类上层装配代码调用。它自己不处理某次请求的细节,只负责把 ad_hoc_note、list、read、search 这些具体零件装好,然后交给外层使用。
调用图:被 2 处调用(tools, memory_tool);外部调用 1 个(vec!)。
memory_tool_name55–57 ↗
fn memory_tool_name(name: &str) -> ToolName
作用:给某个记忆工具生成带“记忆命名空间”的正式名字。这样可以避免工具名和别的扩展撞车,就像给门牌号加上小区名。
数据流:进去的是一个普通工具名字符串。函数把它和 MEMORY_TOOLS_NAMESPACE 这个固定命名空间合在一起。出来的是一个 ToolName,也就是系统内部认可的完整工具名。
调用关系:它调用 ToolName 的 namespaced 方法来完成拼接。它属于工具命名的共用入口,供具体记忆工具在声明自己身份时使用。
调用图:调用 1 个内部函数(namespaced)。
memory_function_tool59–78 ↗
fn memory_function_tool(
name: &str,
description: &str,
) -> ToolSpec
作用:生成某个记忆函数工具的说明书,包括名字、介绍、输入参数格式和输出格式。外部模型要正确调用工具,靠的就是这份说明。
数据流:进去的是工具名、工具描述,以及输入和输出两种数据类型。函数先根据输入类型生成 JSON 参数结构,再根据输出类型生成输出结构,然后把这些内容包装进记忆命名空间下的 ToolSpec。出来的是一份完整工具规格,外部系统可以拿它展示和校验工具。
调用关系:它调用 parse_tool_input_schema 来解析输入格式,调用 default_namespace_description 生成命名空间说明,再把工具放进 ResponsesApiNamespace。它是具体工具向外声明“我怎么被调用”的通用制作器。
调用图:外部调用 4 个(parse_tool_input_schema, default_namespace_description, Namespace, vec!)。
parse_args80–89 ↗
fn parse_args(call: &ToolCall) -> Result<T, FunctionCallError>
作用:把一次工具调用里带来的参数文本,变成 Rust 程序能直接使用的具体参数对象。它还会把参数写错的情况转换成可以回复给模型的错误。
数据流:进去的是 ToolCall,也就是一次工具调用记录。函数先取出 function_arguments 参数字符串;如果字符串是空的,就当成一个空 JSON 对象;如果不是空的,就按 JSON 解析。最后再把 JSON 转成调用方需要的类型。出来的是解析好的参数,或者一个提示模型参数有问题的 FunctionCallError。
调用关系:它先向 ToolCall 要参数,再把解析工作交给 serde_json 的 from_str 和 from_value。它位于“收到一次工具调用”和“真正执行工具动作”之间,主要负责把外部输入整理干净。
调用图:外部调用 5 个(Object, function_arguments, new, from_str, from_value)。
clamp_max_results91–93 ↗
fn clamp_max_results(requested: Option<usize>, default: usize, max: usize) -> usize
作用:限制一次最多返回多少条结果,防止调用方要得太多或给出不合理数字。它让结果数量始终落在安全范围内。
数据流:进去的是调用方请求的数量、默认数量和允许的最大数量。如果调用方没填,就用默认值;如果填了,就把它压到 1 到最大值之间。出来的是一个最终可用的结果数量。
调用关系:它是搜索、列表这类可能返回很多内容的工具会用到的安全阀。它不调用别的项目函数,只做一个简单但重要的数值收口。
backend_error_to_function_call95–113 ↗
fn backend_error_to_function_call(err: MemoriesBackendError) -> FunctionCallError
作用:把记忆后端产生的错误翻译成工具调用层能理解的错误。它区分“用户或模型参数不对,可以告诉模型改”以及“系统自己出问题了,应该算致命错误”。
数据流:进去的是 MemoriesBackendError,也就是保存、读取、搜索记忆时可能发生的错误。函数查看错误种类:路径无效、找不到文件、查询为空这类问题会变成 RespondToModel,意思是把错误告诉模型;磁盘读写之类 Io 错误会变成 Fatal,意思是系统级失败。出来的是 FunctionCallError,供工具调用框架统一处理。
调用关系:它调用错误的 to_string 生成可读消息,再根据情况包装成 RespondToModel 或 Fatal。它站在后端和外部工具协议之间,负责把底层错误翻译成调用方能采取行动的信号。
调用图:外部调用 3 个(to_string, Fatal, RespondToModel)。
ext/memories/src/tools/ad_hoc_note.rs源码 ↗
这个文件像一个前台接待员:外面来的工具调用不能直接写记忆文件,必须先经过它检查格式、整理材料,再交给真正保存数据的后端。它先规定输入必须包含两个东西:文件名和笔记内容。文件名有严格格式,比如带时间戳和小写短名,避免乱起名导致覆盖、混乱或路径问题;笔记内容不能为空。AddAdHocNoteTool 是这个工具的主体,它会告诉系统自己的名字、工具说明和输入输出格式。真正执行时,它会解析调用参数,把 filename 和 note 包装成后端能懂的请求,调用 backend.add_ad_hoc_note 保存笔记,然后记录一次指标数据(也就是给监控看的“这次工具调用成功了吗”)。如果后端报错,它会把错误转换成工具调用能返回的错误;如果成功,就把结果包装成 JSON 返回给调用方。
AddAdHocNoteTool::tool_name48–50 ↗
fn tool_name(&self) -> ToolName
作用:这个函数告诉外部系统:这个工具叫什么名字。系统靠这个名字把用户或模型发起的工具调用,匹配到正确的工具实现上。
数据流:进去的是这个工具对象本身 → 它读取固定的临时笔记工具名,并交给 memory_tool_name 包装成系统认可的工具名格式 → 出来的是一个 ToolName,用来注册和识别这个工具。
调用关系:它是工具接口的一部分,系统在发现或调用工具前会问它名字。它把具体的名字处理交给 memory_tool_name,这样记忆类工具的命名方式可以保持一致。
调用图:外部调用 1 个(memory_tool_name)。
AddAdHocNoteTool::spec52–57 ↗
fn spec(&self) -> ToolSpec
作用:这个函数给出工具的“说明书”:这个工具是干什么的、需要什么输入、会返回什么输出。调用方靠这份说明书知道应该怎样正确调用它。
数据流:进去的是这个工具对象本身 → 它把参数类型 AddAdHocNoteArgs、返回类型 AddAdHocMemoryNoteResponse、工具名和一段人类可读的描述组合起来 → 出来的是 ToolSpec,也就是工具规范。
调用关系:它是工具注册和展示时会用到的部分。它通过 memory_function_tool 生成统一格式的工具说明,让外部系统能自动知道 filename 和 note 这些字段的要求。
AddAdHocNoteTool::handle59–61 ↗
fn handle(&self, call: ToolCall) -> codex_extension_api::ToolExecutorFuture<'_>
作用:这个函数是工具真正被调用时的入口。它把同步看到的一次 ToolCall,转交给异步执行函数去慢慢处理,因为保存笔记可能需要等待后端操作完成。
数据流:进去的是一条工具调用 ToolCall → 它不直接做保存,而是调用 handle_call,并把这个异步任务固定成系统需要的 Future 形式 → 出来的是一个将来会完成的执行结果。
调用关系:它由工具运行框架调用,是接口要求实现的标准入口。它把实际工作交给 AddAdHocNoteTool::handle_call,自己只负责把异步流程包装成框架能接收的样子。
调用图:调用 1 个内部函数(handle_call);外部调用 1 个(pin)。
AddAdHocNoteTool::handle_call68–90 ↗
async fn handle_call(
&self,
call: ToolCall,
) -> Result<Box<dyn codex_extension_api::ToolOutput>, codex_extension_api::FunctionCallError>
作用:这个函数完成一次新增临时记忆笔记的全过程:读参数、请后端保存、记录成功或失败、把结果返回。它是这个文件里真正干活的核心。
数据流:进去的是一条 ToolCall,以及工具对象里保存的后端和可选指标客户端 → 它先复制一份后端句柄,再用 parse_args 把调用参数解析成 filename 和 note;接着把它们放进 AddAdHocMemoryNoteRequest,调用后端的 add_ad_hoc_note;随后用 record_tool_call 记录这次调用是否成功;如果失败,就把后端错误转成工具调用错误;如果成功,就把后端响应转成 JSON 输出 → 出来的是一个工具输出,或者一个能让调用方理解的错误。
调用关系:它由 AddAdHocNoteTool::handle 启动,是一次工具调用的主要流水线。它向下依赖后端真正写入笔记,向旁边通知指标系统记录结果,并用 JsonToolOutput 把成功响应包装成外部系统能读的 JSON。
调用图:调用 2 个内部函数(record_tool_call, new);被 1 处调用(handle);外部调用 4 个(clone, new, json!, parse_args)。
ext/memories/src/tools/list.rs源码 ↗
这个文件解决的是“怎么安全、规范地列目录”的问题。外部只需要传入路径、分页游标和最多返回多少条,这里会把这些参数读出来,检查格式,再交给真正存数据的后端去查。它还会限制一次最多返回多少结果,避免有人一口气要太多内容,把系统拖慢。可以把它想成一个图书馆前台:用户说“我想看这个书架下面有哪些书”,前台先确认请求写得对,再限制一次最多给多少条目录卡片,然后请仓库管理员去查。查完以后,它会记录一次工具调用的指标,比如这次是否成功、结果有没有被截断,方便之后观察系统有没有异常。最后,它把后端返回的列表包装成 JSON(一种常见的数据文本格式,方便机器读写)交回给调用者。如果后端出错,它会把后端错误翻译成工具调用能理解的错误。
ListTool::tool_name46–48 ↗
fn tool_name(&self) -> ToolName
作用:返回这个工具对外使用的名字,也就是调用者要点名使用“list memories”功能时看到的名字。这样系统才能把一次工具调用分派到这个文件里的实现。
数据流:它不需要外部输入,只读取固定的列表工具名常量 → 把这个名字交给通用的 memory_tool_name 做统一包装 → 输出一个 ToolName,供工具注册和匹配调用时使用。
调用关系:当工具框架想知道这个执行器叫什么时,会调用它。它把命名规则交给 memory_tool_name,这样所有 memory 工具的名字格式都保持一致。
调用图:外部调用 1 个(memory_tool_name)。
ListTool::spec50–55 ↗
fn spec(&self) -> ToolSpec
作用:告诉外部这个工具怎么用:它叫什么、用途是什么、参数长什么样、返回大概是什么格式。没有这个说明,调用者就不知道该传哪些字段。
数据流:它读取 ListArgs 这套参数定义和 ListMemoriesResponse 这套返回定义 → 加上一句人能看懂的说明文字 → 生成 ToolSpec,也就是工具说明书。
调用关系:工具注册或展示能力列表时会用到它。它通过 memory_function_tool 生成统一格式的工具规格,让这个 list 工具和其他 memories 工具看起来、用起来都一致。
ListTool::handle57–59 ↗
fn handle(&self, call: ToolCall) -> codex_extension_api::ToolExecutorFuture<'_>
作用:这是工具真正被调用时的入口。它把一次外部传来的 ToolCall 交给异步处理函数,让查询目录这件事可以在等待后端时不堵住系统。
数据流:输入是一条 ToolCall,也就是外部发来的工具调用请求 → 它把请求交给 handle_call,并用 pin 包起来形成可等待的异步任务 → 输出一个未来会完成的执行结果。
调用关系:工具框架收到调用后会进入这里。它自己不做具体业务,而是把活儿转交给 ListTool::handle_call;这样符合 ToolExecutor 要求的接口,同时把复杂流程放在专门的函数里。
调用图:调用 1 个内部函数(handle_call);外部调用 1 个(pin)。
ListTool::handle_call66–94 ↗
async fn handle_call(
&self,
call: ToolCall,
) -> Result<Box<dyn codex_extension_api::ToolOutput>, codex_extension_api::FunctionCallError>
作用:这是列目录工具的核心流程。它解析请求、限制返回数量、调用后端查询、记录监控指标,并把结果包装成 JSON 返回。
数据流:输入是一条 ToolCall,以及工具里保存的后端和可选的指标客户端 → 先解析出 path、cursor、max_results;再根据 path 算出监控用的范围标签;接着把 max_results 夹在默认值和最大允许值之间;然后调用 backend.list 查询目录 → 不管成功失败都会记录一次调用指标;如果失败,就把后端错误转换成工具错误;如果成功,就把列表响应变成 JSON 输出。同时它不会直接改记忆库内容,只是读取列表并记录指标。
调用关系:ListTool::handle 会把真正的请求交给它。它处在工具框架和 memories 后端之间:向下调用后端的 list 做实际查询,旁路调用 record_tool_call 记录监控,还使用 scope_from_optional_path、truncated_tag、clamp_max_results 等小工具整理参数和指标。
调用图:调用 4 个内部函数(record_tool_call, scope_from_optional_path, truncated_tag, new);被 1 处调用(handle);外部调用 5 个(clone, new, json!, clamp_max_results, parse_args)。
ext/memories/src/tools/read.rs源码 ↗
这个文件像一个“前台服务员”:别人想读某个记忆文件时,不直接碰存储后端,而是通过这里。它先声明这个工具叫什么、需要哪些参数,比如 path、从第几行开始读、最多读几行。真正收到调用后,它会解析参数,给缺省值,比如没说从哪行开始就从第 1 行开始;再把请求交给 MemoriesBackend,也就是实际存放和读取记忆的后端。读完后,它还会记录一次指标,说明这次工具调用是否成功、读到的内容有没有被截断。最后,它把成功结果转成 JSON;如果后端报错,就翻译成工具调用能理解的错误。这样做的好处是:外部只看到一个稳定、清楚的“读记忆”工具,不用知道里面文件怎么存、错误怎么处理、指标怎么记。
ReadTool::tool_name45–47 ↗
fn tool_name(&self) -> ToolName
作用:告诉工具系统:这个工具的正式名字是什么。这样外部发起调用时,系统才能把“读记忆”这个请求路由到这个工具上。
数据流:它不需要外部输入,只读取固定的读工具名称常量 → 交给 memory_tool_name 统一包装成记忆工具的名字 → 返回一个 ToolName,也就是工具系统识别用的名字。
调用关系:这是工具注册和识别时会用到的小入口。它把命名规则交给 memory_tool_name,确保这个工具和其他 memory 工具使用同一套命名方式。
调用图:外部调用 1 个(memory_tool_name)。
ReadTool::spec49–54 ↗
fn spec(&self) -> ToolSpec
作用:说明这个工具怎么用:需要哪些参数、返回什么样的数据、给人的说明文字是什么。可以把它理解成工具的“使用说明书”。
数据流:它读取 ReadArgs 这个参数格式和 ReadMemoryResponse 这个返回格式 → 用 memory_function_tool 生成一份标准工具说明 → 输出 ToolSpec,供调用方知道该怎样正确调用。
调用关系:工具系统在展示或注册工具能力时会用到它。它不实际读取文件,只负责把“读记忆文件”这件事说清楚,让外部调用者按正确格式传参数。
ReadTool::handle56–58 ↗
fn handle(&self, call: ToolCall) -> codex_extension_api::ToolExecutorFuture<'_>
作用:这是工具真正被调用时的入口。它把一次外部 ToolCall 包装成异步任务,让读取操作可以慢慢等后端完成,而不会卡住整个系统。
数据流:输入是一份 ToolCall,也就是外部发来的工具调用请求 → 它把请求交给 handle_call,并用 pin 固定这个异步任务的位置以便运行 → 返回一个未来会完成的执行结果。
调用关系:工具执行框架会先调用它。它自己不做读文件的细活,而是立刻转交给 handle_call;handle_call 才负责解析参数、访问后端和整理返回值。
调用图:调用 1 个内部函数(handle_call);外部调用 1 个(pin)。
ReadTool::handle_call65–91 ↗
async fn handle_call(
&self,
call: ToolCall,
) -> Result<Box<dyn codex_extension_api::ToolOutput>, codex_extension_api::FunctionCallError>
作用:完成一次“读取记忆文件”的完整流程:看懂请求、调用后端、记录指标、返回 JSON,或者把错误变成调用方能看懂的错误。
数据流:输入是一份 ToolCall,并读取工具里保存的 backend 和可选的 metrics_client → 先用 parse_args 解析出 path、line_offset、max_lines;再根据 path 算出指标用的 scope;然后构造 ReadMemoryRequest,补上默认起始行和最大 token 限制,交给 backend.read;接着用 record_tool_call 记录成功失败和是否截断;如果失败就转成 FunctionCallError,如果成功就用 JsonToolOutput 包成 JSON 输出 → 结果是一个工具输出对象,同时指标系统也留下了这次调用的记录。
调用关系:它由 ReadTool::handle 调起,是这个文件里真正干活的核心。它向下依赖后端读取数据,向旁边调用指标函数记录情况,最后把后端结果转换成工具框架能返回的格式。
调用图:调用 4 个内部函数(record_tool_call, scope_from_path, truncated_tag, new);被 1 处调用(handle);外部调用 4 个(clone, new, json!, parse_args)。
ext/memories/src/tools/search.rs源码 ↗
这个文件像一个“搜索窗口”的前台接待。外部只知道要调用一个搜索工具,并传入关键词、路径、大小写、最多返回多少条等条件;真正去文件里找内容的是后面的 memories 后端。这里先定义 SearchArgs,也就是工具允许用户填写的搜索表单,并限制一些明显不合理的值,比如查询词不能为空、最多结果数不能乱填。SearchTool 则把这个表单接进来:告诉系统自己的工具名和说明,收到调用后解析参数,补上默认值,把参数变成 SearchMemoriesRequest,交给 backend.search 去查。查完后,它还会记录一次指标数据,也就是“这次工具调用成功了吗、在哪个范围查的、结果有没有被截断”。如果后端报错,它会翻译成工具调用能理解的错误;如果成功,就把结果包装成 JSON(一种通用的数据格式)返回。
SearchTool::tool_name54–56 ↗
fn tool_name(&self) -> ToolName
作用:告诉外部系统,这个工具叫什么名字。这样调用方才能用正确的名字找到“搜索记忆”的功能。
数据流:它不需要外部输入,只读取固定的搜索工具名 SEARCH_TOOL_NAME → 交给 memory_tool_name 加上记忆工具统一的命名包装 → 返回一个 ToolName,作为这个工具对外登记和匹配时使用的名字。
调用关系:这是工具注册和识别时会用到的小入口。它把具体名字交给 memory_tool_name 统一处理,避免每个记忆工具各自乱起名。
调用图:外部调用 1 个(memory_tool_name)。
SearchTool::spec58–63 ↗
fn spec(&self) -> ToolSpec
作用:生成这个搜索工具的“说明书”。外部系统可以通过它知道这个工具需要哪些参数、会返回什么,以及它大概能做什么。
数据流:它使用 SearchArgs 作为输入参数格式,使用 SearchMemoriesResponse 作为输出格式 → 交给 memory_function_tool 生成标准工具规格 → 返回 ToolSpec,供工具平台展示、校验和调用。
调用关系:这是工具被注册或展示给调用方时用的。它不执行搜索,只把 SearchArgs 和返回类型包装成一份机器可读的工具说明。
SearchTool::handle65–67 ↗
fn handle(&self, call: ToolCall) -> codex_extension_api::ToolExecutorFuture<'_>
作用:这是外部真正调用搜索工具时进入的入口。它把一次工具调用交给异步处理流程去执行。
数据流:输入是一份 ToolCall,也就是外部发来的调用请求 → 它把 handle_call 包成一个可等待的异步任务 → 返回这个任务,让运行环境之后等待搜索完成。
调用关系:它是 ToolExecutor 接口要求提供的函数。自己不做具体搜索,而是立刻把工作转给 SearchTool::handle_call;使用 pin 是为了把异步任务固定好,方便运行时安全地执行。
调用图:调用 1 个内部函数(handle_call);外部调用 1 个(pin)。
SearchTool::handle_call74–92 ↗
async fn handle_call(
&self,
call: ToolCall,
) -> Result<Box<dyn codex_extension_api::ToolOutput>, codex_extension_api::FunctionCallError>
作用:这是一次搜索调用的主流程。它负责解析请求、调用后端搜索、记录指标,并把结果或错误转换成工具系统能理解的形式。
数据流:输入是一份 ToolCall,同时读取自身保存的 backend 和可选的 metrics_client → 先复制 backend,解析调用参数得到 SearchArgs,再根据 path 算出搜索范围标签 → 把 SearchArgs 转成后端请求并调用 backend.search → 根据成功或失败记录一次工具调用指标 → 如果失败,把后端错误翻译成 FunctionCallError;如果成功,把搜索结果包成 JSON 输出 → 最后返回一个 ToolOutput。
调用关系:它由 SearchTool::handle 调用,是这个文件里真正干活的核心。它向外接 parse_args,向内调用 SearchArgs::into_request 间接准备请求,再把任务交给后端搜索;搜索结束后还会通过 record_tool_call、scope_from_optional_path 和 truncated_tag 给监控系统留下记录。
调用图:调用 4 个内部函数(record_tool_call, scope_from_optional_path, truncated_tag, new);被 1 处调用(handle);外部调用 4 个(clone, new, json!, parse_args)。
SearchArgs::into_request96–111 ↗
fn into_request(self) -> SearchMemoriesRequest
作用:把外部传来的搜索参数整理成后端真正需要的搜索请求。它会补默认值,并把“最多返回多少条”限制在安全范围内。
数据流:输入是 SearchArgs,里面可能有查询词、匹配模式、路径、游标、上下文行数、大小写设置、是否规范化、最大结果数等 → 它逐项搬到 SearchMemoriesRequest 里;没有填写的选项会用默认值,比如匹配模式默认 Any,上下文行数默认 0,大小写默认敏感,normalized 默认 false → max_results 会经过 clamp_max_results,防止请求太大 → 输出一份干净、完整、后端能直接执行的 SearchMemoriesRequest。
调用关系:它是 handle_call 准备后端搜索请求时用的转换步骤。外部参数可能缺项或过大,后端不该直接面对这些不稳定输入,所以这里先把它们整理好再交给 backend.search。
调用图:外部调用 1 个(clamp_max_results)。
memories/write/src/workspace.rs源码 ↗
这个文件像一个整理书桌的人:开始工作前先确认桌子存在,把上次留下的草稿纸拿走,再确认桌上有一套可用的 Git 基线。这里的 Git 基线,可以理解成“上次确认过的干净版本”。之后系统可以拿当前文件和这份基线比较,知道记忆内容新增、修改或删除了什么。它还会生成一个叫 phase2_workspace_diff.md 的文件,里面用人和程序都容易读的方式列出变化。但这个文件本身只是临时提示材料,不是真正的记忆,所以每次比较和重置基线前都要先删掉它。文件里还有一个重要保护:差异内容太长时会截断,并且会小心地停在合法字符边界,避免把中文或 emoji 这类多字节字符切坏。
prepare_memory_workspace13–20 ↗
async fn prepare_memory_workspace(root: &Path) -> anyhow::Result<()>
作用:为记忆工作区做开工前准备:确保目录存在,清掉旧的差异文件,并确认里面有可用的 Git 基线。有人在正式处理记忆前会调用它,保证后面的比较不会从一片混乱开始。
数据流:输入是一个工作区路径 root。它先创建这个目录,哪怕目录已经存在也没关系;接着调用 remove_workspace_diff 删除旧的 phase2_workspace_diff.md;最后调用外部的 ensure_git_baseline_repository 检查或初始化 Git 基线。成功时不返回额外数据,只表示这个目录已经准备好;失败时带上“在哪个目录出错”的说明。
调用关系:它由 run 调用,处在整套记忆写入流程的开头。它自己不做具体的 Git 初始化细节,而是把清理临时差异文件的事交给 remove_workspace_diff,把 Git 基线准备交给 ensure_git_baseline_repository。这样后续的 memory_workspace_diff 才能放心比较变化。
调用图:调用 1 个内部函数(remove_workspace_diff);被 1 处调用(run);外部调用 2 个(ensure_git_baseline_repository, create_dir_all)。
memory_workspace_diff26–29 ↗
async fn memory_workspace_diff(root: &Path) -> anyhow::Result<GitBaselineDiff>
作用:读取当前记忆工作区相对 Git 基线的变化。它会先删掉旧的差异文件,避免把系统自己生成的文件也算成“记忆变化”。
数据流:输入是工作区路径 root。它先调用 remove_workspace_diff 清理 phase2_workspace_diff.md,然后调用外部的 diff_since_latest_init,把当前文件和最近一次基线做比较。输出是 GitBaselineDiff,也就是一份“哪些文件变了、具体差异是什么”的结果。
调用关系:它由 run 在需要查看记忆变化时调用。它前面通常会有 prepare_memory_workspace 做准备,后面可能把结果交给 write_workspace_diff 写成文件。它把真正计算差异的工作交给 diff_since_latest_init,自己负责先把会污染结果的临时文件移走。
调用图:调用 1 个内部函数(remove_workspace_diff);被 1 处调用(run);外部调用 1 个(diff_since_latest_init)。
write_workspace_diff32–37 ↗
async fn write_workspace_diff(root: &Path, diff: &GitBaselineDiff) -> anyhow::Result<()>
作用:把已经算好的工作区差异写入 phase2_workspace_diff.md。这个文件是给后续阶段先阅读的提示材料,告诉它当前记忆工作区发生了什么变化。
数据流:输入是工作区路径 root 和一份 GitBaselineDiff。它先拼出 phase2_workspace_diff.md 的完整路径,再调用 render_workspace_diff_file 把差异结果变成 Markdown 文本,最后用异步写文件的方式保存到磁盘。输出只是成功或失败;成功后磁盘上会多出或更新这份差异说明文件。
调用关系:它由 run 在已经拿到差异结果后调用。它不负责计算差异,只负责把 diff 包装成可读文件;具体文字格式交给 render_workspace_diff_file,真正落盘交给外部的 write。
调用图:调用 1 个内部函数(render_workspace_diff_file);被 1 处调用(run);外部调用 2 个(join, write)。
reset_memory_workspace_baseline43–46 ↗
async fn reset_memory_workspace_baseline(root: &Path) -> anyhow::Result<()>
作用:把当前记忆工作区标记成新的“干净版本”。这通常表示当前变化已经被接受,之后再比较时就从现在这一刻重新开始算。
数据流:输入是工作区路径 root。它先调用 remove_workspace_diff 删除临时生成的差异文件,避免这个提示文件被收进新基线;然后调用外部的 reset_git_repository 重置 Git 仓库基线。输出只是成功或失败;成功后当前真实记忆内容成为新的参考版本。
调用关系:它由 handle 调用,通常发生在某个处理动作决定“现在这些记忆就是新的基准”时。它把清理临时文件交给 remove_workspace_diff,把 Git 层面的重置交给 reset_git_repository。
调用图:调用 1 个内部函数(remove_workspace_diff);被 1 处调用(handle);外部调用 1 个(reset_git_repository)。
remove_workspace_diff53–61 ↗
async fn remove_workspace_diff(root: &Path) -> anyhow::Result<()>
作用:专门删除 phase2_workspace_diff.md 这个临时差异文件。它不会碰 .git 目录,也不会删除真正的记忆内容。
数据流:输入是工作区路径 root。它拼出 phase2_workspace_diff.md 的路径,然后尝试删除这个文件。如果文件本来就不存在,它也算成功;如果因为其他原因删不掉,就返回带上下文的错误。结果是这个临时文件不再影响后续操作。
调用关系:它是这个文件里多个流程共用的清洁步骤,被 prepare_memory_workspace、memory_workspace_diff 和 reset_memory_workspace_baseline 调用。它的位置像进门前擦掉黑板:不管接下来是准备、比较还是重设基线,都先确保旧提示文件不会搅局。
调用图:被 3 处调用(memory_workspace_diff, prepare_memory_workspace, reset_memory_workspace_baseline);外部调用 2 个(join, remove_file)。
render_workspace_diff_file63–82 ↗
fn render_workspace_diff_file(diff: &GitBaselineDiff) -> String
作用:把机器算出的差异结果变成一份 Markdown 文本。Markdown 是一种普通文本格式,标题、列表、代码块都能清楚显示。
数据流:输入是一份 GitBaselineDiff。它先写入固定标题和说明;如果没有变化,就写一行“- none”并结束;如果有变化,就列出每个变动文件的状态和路径,再把详细 diff 放进代码块里。详细 diff 会交给 append_bounded_diff 控制长度。输出是一整段可以直接写入 phase2_workspace_diff.md 的字符串。
调用关系:它只被 write_workspace_diff 调用,是“把差异结果变成人能读的文件内容”的中间环节。它会调用 diff.has_changes 判断有没有变化,也会调用 append_bounded_diff 追加详细差异,避免输出文件无限变大。
调用图:调用 2 个内部函数(has_changes, append_bounded_diff);被 1 处调用(write_workspace_diff);外部调用 2 个(from, format!)。
append_bounded_diff84–102 ↗
fn append_bounded_diff(rendered: &mut String, diff: &str)
作用:把详细差异追加到文本里,同时限制最大长度。这样即使改动很多,也不会生成一个过大的提示文件。
数据流:输入是一段正在组装的文本 rendered,以及原始 diff 字符串。如果 diff 没超过最大字节数,就原样追加,并补上结尾换行;如果太长,就先调用 previous_char_boundary 找到安全截断点,只追加前面一部分,再写上一句“已在多少字节处截断”的提示。它不返回新值,而是直接修改传进来的 rendered。
调用关系:它由 render_workspace_diff_file 调用,负责详细差异那一段的安全追加。遇到需要截断时,它把“在哪里切才不会切坏字符”的判断交给 previous_char_boundary。
调用图:调用 1 个内部函数(previous_char_boundary);被 1 处调用(render_workspace_diff_file);外部调用 1 个(format!)。
previous_char_boundary104–113 ↗
fn previous_char_boundary(value: &str, max_bytes: usize) -> usize
作用:找出一个字符串在指定字节数之前的安全切断位置。这个函数很小,但能防止把中文、emoji 等字符从中间切开,造成乱码或非法文本。
数据流:输入是字符串 value 和最大字节数 max_bytes。如果 max_bytes 已经超过字符串长度,就返回字符串末尾;否则从 max_bytes 开始往前找,直到找到一个合法字符边界。输出是一个字节位置,调用者可以放心用它来截取字符串。
调用关系:它只被 append_bounded_diff 调用,是差异内容截断时的安全工具。append_bounded_diff 负责决定要不要截断;一旦需要截断,就请它找出不会破坏字符的位置。
调用图:被 1 处调用(append_bounded_diff)。
技能子系统与扩展工具
这些文件展示核心端和扩展端的技能机制,涵盖共享 API、调用辅助器、提供方、来源路由、扩展接线以及对外暴露的技能工具。
core-skills/src/lib.rs源码 ↗
可以把这个文件理解成一家商场的总导览牌。真正卖东西的店铺在别的文件里,比如加载技能、渲染技能说明、处理远程技能、管理技能策略等;而这里负责告诉别人:这些店铺在哪里,哪些可以公开进入,哪些只给内部员工用。它用 pub mod 暴露公开模块,用 mod 保留内部模块,再用 pub use 把常用的类型和函数重新摆到库的门口,让调用方不必知道它们原本藏在哪个子文件里。没有这个文件,外部代码就很难方便、稳定地使用 core-skills 的能力;模块边界也会变乱。它本身不做具体业务计算,但它定义了这个库的公共 API,也就是别人怎么“正确地使用这套技能系统”。
core-skills/src/invocation_utils.rs源码 ↗
这份代码解决的是“用户没有明说要用哪个 skill,但命令里已经露出线索”的问题。比如用户执行 python skills/foo/scripts/a.py,或者 cat skills/foo/SKILL.md,系统就应该知道这和 foo 这个 skill 有关。文件先把已加载的 skill 做成两张查找表:一张按 scripts 目录找,一张按 skill 文档路径找。之后每来一条命令,它会先把工作目录和路径尽量变成真实路径,避免 ..、符号链接这类路径写法造成认错。然后它把命令拆成参数,先看是不是用 Python、Bash、Node 等运行脚本;如果不是,再看命令里有没有读文件的动作。可以把它理解成门卫:不要求用户报名字,而是根据“你进了哪个房间、拿了哪份文件”来判断你实际用的是哪个 skill。
build_implicit_skill_path_indexes10–29 ↗
fn build_implicit_skill_path_indexes(
skills: Vec<SkillMetadata>,
) -> (
HashMap<AbsolutePathBuf, SkillMetadata>,
HashMap<AbsolutePathBuf, SkillMetadata>,
)
作用:把一批 skill 整理成两本“地址簿”,方便之后快速判断某个路径属于哪个 skill。一本按 scripts 文件夹查,另一本按 skill 的说明文档查。
数据流:进去的是多个 SkillMetadata,也就是每个 skill 的基本资料和文档路径。它逐个取出 skill,把文档路径和同目录下的 scripts 路径尽量转成真实路径,然后分别放进两个 HashMap(像字典一样用路径直接查资料)。出来的是这两张索引表,后面检测命令时不用再一个个 skill 慢慢比对。
调用关系:这是后续识别工作的准备步骤。它会调用 canonicalize_if_exists 来统一路径写法;后面的 detect_skill_script_run 和 detect_skill_doc_read 会依赖这里建好的索引来快速找到对应 skill。
调用图:调用 1 个内部函数(canonicalize_if_exists);外部调用 1 个(new)。
detect_implicit_skill_invocation_for_command31–44 ↗
fn detect_implicit_skill_invocation_for_command(
outcome: &SkillLoadOutcome,
command: &str,
workdir: &AbsolutePathBuf,
) -> Option<SkillMetadata>
作用:这是对外的主入口:给它一条命令和当前工作目录,它判断这条命令是否隐含调用了某个 skill。
数据流:进去的是已加载 skill 的结果 SkillLoadOutcome、一整条命令字符串、以及当前工作目录。它先把工作目录尽量转成真实路径,再把命令拆成一个个参数;接着先检查是不是在运行 skill 的脚本,找不到再检查是不是在读取 skill 的文档。出来的是可能找到的 SkillMetadata;如果没有线索,就返回空。
调用关系:它像调度员一样串起整个检测流程。它会先用 tokenize_command 拆命令,再把结果交给 detect_skill_script_run;如果脚本检测没命中,再交给 detect_skill_doc_read。路径统一则交给 canonicalize_if_exists。
调用图:调用 4 个内部函数(canonicalize_if_exists, detect_skill_doc_read, detect_skill_script_run, tokenize_command)。
tokenize_command46–49 ↗
fn tokenize_command(command: &str) -> Vec<String>
作用:把一整条命令字符串拆成参数列表,尽量按 shell 的规则处理引号和空格。这样后面才能知道哪个词是解释器、哪个词是脚本或文件路径。
数据流:进去的是类似 python "my script.py" 的命令文本。它优先用 shlex 按命令行规则拆分;如果拆失败,就退回到最简单的按空白字符拆分。出来的是一组字符串参数。
调用关系:它由 detect_implicit_skill_invocation_for_command 调用,是所有后续判断的第一步。拆出来的参数会继续交给脚本检测和文档读取检测。
调用图:被 1 处调用(detect_implicit_skill_invocation_for_command);外部调用 1 个(split)。
script_run_token51–81 ↗
fn script_run_token(tokens: &[String]) -> Option<&str>
作用:判断一组命令参数看起来是不是“用某个运行器执行脚本”,并找出脚本路径。这里的运行器指 Python、Bash、Node 这类用来跑脚本的程序。
数据流:进去的是已经拆好的命令参数。它先看第一个参数是不是常见运行器,比如 python、bash、node,并会忽略路径前缀和 Windows 下的 .exe 后缀;然后跳过选项参数,找第一个真正像脚本的参数;如果它以 .py、.sh、.js 等脚本后缀结尾,就把这个脚本路径返回,否则返回空。
调用关系:它被 detect_skill_script_run 调用,专门负责从命令里提取“被运行的脚本”。为了准确识别 usr/bin/python 或 python.exe 这种写法,它会请 command_basename 帮忙取程序名。
调用图:调用 1 个内部函数(command_basename);被 1 处调用(detect_skill_script_run)。
detect_skill_script_run83–99 ↗
fn detect_skill_script_run(
outcome: &SkillLoadOutcome,
tokens: &[String],
workdir: &AbsolutePathBuf,
) -> Option<SkillMetadata>
作用:检查命令是不是运行了某个 skill 的 scripts 目录里的脚本。如果是,就返回这个 skill 的资料。
数据流:进去的是加载结果、命令参数、当前工作目录。它先用 script_run_token 找到脚本路径,再把相对路径接到工作目录上,并尽量转成真实路径;然后从这个脚本路径一路往上看父目录,看看是否命中之前建立的 scripts 目录索引。命中就返回对应 skill,没有就返回空。
调用关系:它由 detect_implicit_skill_invocation_for_command 优先调用,因为运行脚本是很强的 skill 使用信号。它依赖 script_run_token 找脚本,依赖 canonicalize_if_exists 统一路径,并使用 build_implicit_skill_path_indexes 建好的 implicit_skills_by_scripts_dir。
调用图:调用 3 个内部函数(canonicalize_if_exists, script_run_token, join);被 1 处调用(detect_implicit_skill_invocation_for_command);外部调用 1 个(new)。
detect_skill_doc_read101–116 ↗
fn detect_skill_doc_read(
outcome: &SkillLoadOutcome,
tokens: &[String],
workdir: &AbsolutePathBuf,
) -> Option<SkillMetadata>
作用:检查命令里是否有读取某个 skill 说明文档的动作。如果用户在读这个文档,系统就认为这可能是在使用对应 skill。
数据流:进去的是加载结果、命令参数、当前工作目录。它先用命令解析器 parse_command_impl 把参数理解成更具体的动作;如果发现其中有 Read,也就是读文件动作,就把读到的路径接到工作目录上并尽量转成真实路径。然后它拿这个路径去文档索引里查,查到就返回对应 skill,查不到就继续看下一条解析出的动作,最后可能返回空。
调用关系:它由 detect_implicit_skill_invocation_for_command 在脚本检测失败后调用。它把“理解命令是不是读文件”的工作交给 parse_command_impl,把路径统一交给 canonicalize_if_exists,并使用 build_implicit_skill_path_indexes 建好的 implicit_skills_by_doc_path。
调用图:调用 3 个内部函数(canonicalize_if_exists, parse_command_impl, join);被 1 处调用(detect_implicit_skill_invocation_for_command)。
command_basename118–124 ↗
fn command_basename(command: &str) -> String
作用:从一个命令路径里取出最后的程序名。比如把 /usr/bin/python3 变成 python3,方便判断它是不是常见脚本运行器。
数据流:进去的是一个命令字符串,可能是纯名字,也可能带目录路径。它把字符串当作路径看,尝试取最后一段文件名;如果取不到,就原样使用。出来的是一个普通字符串形式的程序名。
调用关系:它被 script_run_token 调用,是识别运行器的小工具。没有它,/usr/bin/python 这种带完整路径的命令就可能无法被当成 python 识别。
调用图:被 1 处调用(script_run_token);外部调用 1 个(new)。
canonicalize_if_exists126–128 ↗
fn canonicalize_if_exists(path: &AbsolutePathBuf) -> AbsolutePathBuf
作用:尽量把路径变成系统认识的真实路径;如果路径不存在,就保留原样。这样既能统一路径写法,又不会因为文件还不存在而报错中断。
数据流:进去的是一个绝对路径。它尝试调用系统的路径规范化能力,解析出真实路径,比如消掉 ..,也可能处理符号链接;如果成功,就返回规范化后的路径。如果失败,比如路径不存在,就返回原来的路径副本。
调用关系:这是全文件反复使用的路径清理工具。build_implicit_skill_path_indexes 用它整理索引路径,detect_implicit_skill_invocation_for_command 用它整理工作目录,detect_skill_script_run 和 detect_skill_doc_read 用它整理命令里出现的脚本或文档路径。
调用图:调用 1 个内部函数(canonicalize);被 4 处调用(build_implicit_skill_path_indexes, detect_implicit_skill_invocation_for_command, detect_skill_doc_read, detect_skill_script_run)。
core-skills/src/remote.rs源码 ↗
这个文件解决的是:技能不只存在本机,也可能放在 ChatGPT/Codex 后端服务器上。上层如果想看有哪些远程技能,或者把某个远程技能拉到本地,就会用这里的函数。它先确认用户用的是合适的 ChatGPT/Codex 后端登录方式,而不是普通 API key;然后拼出服务器地址和查询参数;再发 HTTP 请求(可以理解成向服务器敲门要数据)。查询列表时,它把服务器返回的 JSON(一种常见的文本数据格式)变成简单的技能摘要。下载技能时,它要求返回内容必须是 zip 压缩包,创建本地 skills 目录,并把压缩包解开。这里还特别小心文件路径:压缩包里的文件名不能带“返回上级目录”等危险写法,防止远程内容把文件写到不该写的地方。注释也说明了:这是低层客户端,目前主要是为以后接入产品界面预留。
as_query_scope33–40 ↗
fn as_query_scope(scope: RemoteSkillScope) -> Option<&'static str>
作用:把代码里的技能范围选项,翻译成服务器能看懂的查询字符串。有人要列远程技能时,需要用它告诉服务器想看哪一类技能。
数据流:进去的是一个 RemoteSkillScope,比如个人技能、示例技能、工作区共享技能 → 它用固定对应表把这个选项换成类似“personal”这样的短文本 → 出来的是可放进网址查询参数里的文本;如果以后某个范围不需要传给服务器,也可以返回空。
调用关系:它是 list_remote_skills 拼请求地址时的小翻译员。list_remote_skills 先决定要查哪个范围,再让它把范围翻成服务器接口需要的写法。
调用图:被 1 处调用(list_remote_skills)。
as_query_product_surface42–49 ↗
fn as_query_product_surface(product_surface: RemoteSkillProductSurface) -> &'static str
作用:把产品入口类型翻译成服务器接口需要的名字。比如同一个技能服务可能要知道请求来自 ChatGPT、Codex、API 还是 Atlas。
数据流:进去的是一个 RemoteSkillProductSurface 枚举值 → 它按固定规则换成“chatgpt”“codex”“api”或“atlas” → 出来的是一段查询参数文本,用来随请求发给服务器。
调用关系:它只服务于 list_remote_skills。列技能时,list_remote_skills 会用它标明当前产品场景,让服务器按对应场景返回合适的技能。
调用图:被 1 处调用(list_remote_skills)。
ensure_codex_backend_auth51–61 ↗
fn ensure_codex_backend_auth(auth: Option<&CodexAuth>) -> Result<&CodexAuth>
作用:检查当前登录信息是否能访问远程技能服务器。远程技能要求 ChatGPT/Codex 后端登录;如果没有登录,或只是 API key,它会直接拒绝。
数据流:进去的是可选的 CodexAuth 登录信息 → 它先看有没有登录,再看这个登录是不是走 Codex 后端 → 如果合格,就把登录信息原样交回去;如果不合格,就返回清楚的错误,不再继续联网。
调用关系:list_remote_skills 和 export_remote_skill 在真正发请求前都会先找它把关。它像门卫,只有确认身份合适,后面的请求构造和下载流程才会继续。
调用图:被 2 处调用(export_remote_skill, list_remote_skills);外部调用 1 个(bail!)。
list_remote_skills89–139 ↗
async fn list_remote_skills(
chatgpt_base_url: String,
auth: Option<&CodexAuth>,
scope: RemoteSkillScope,
product_surface: RemoteSkillProductSurface,
enabled: Option<bool>,
) -> Re
作用:向远程服务器询问“有哪些技能可用”,并把结果整理成简洁的技能列表。上层界面或命令如果要展示远程技能目录,就会用它。
数据流:进去的是服务器基础地址、登录信息、技能范围、产品入口类型,以及可选的启用状态过滤 → 它先校验登录,再拼出 /hazelnuts 接口地址,加入范围、产品入口、是否启用等查询参数,然后带着认证头发 GET 请求 → 服务器返回文本后,它检查 HTTP 状态是否成功,再把 JSON 解析成 RemoteSkillSummary 列表;如果请求失败或解析失败,就返回错误信息。
调用关系:它是远程技能“列表查询”的主流程。过程中会调用 ensure_codex_backend_auth 做身份检查,调用 as_query_product_surface 和 as_query_scope 准备查询参数,并使用外部的 HTTP 客户端和认证头工具完成实际联网。
调用图:调用 4 个内部函数(as_query_product_surface, as_query_scope, ensure_codex_backend_auth, build_reqwest_client);外部调用 5 个(bail!, auth_provider_from_auth, format!, from_str, vec!)。
export_remote_skill141–191 ↗
async fn export_remote_skill(
chatgpt_base_url: String,
codex_home: PathBuf,
auth: Option<&CodexAuth>,
skill_id: &str,
) -> Result<RemoteSkillDownloadResult>
作用:把服务器上的某个技能下载到本机。它不只是下载,还会确认文件真的是 zip 压缩包,并把内容安全地解压到本地技能目录。
数据流:进去的是服务器地址、本机 Codex 主目录、登录信息和技能 id → 它先校验登录,再请求 /hazelnuts/{skill_id}/export 下载内容 → 下载成功后检查内容是不是 zip;然后创建 codex_home/skills/skill_id 目录,把 zip 解压进去 → 出来的是 RemoteSkillDownloadResult,里面有技能 id 和本地保存路径;如果认证、下载、格式或解压任何一步出错,就返回错误。
调用关系:它是远程技能“下载导出”的主流程。它先请 ensure_codex_backend_auth 把关,再用 is_zip_payload 做格式检查,最后把耗时的解压工作交给 extract_zip_to_dir;解压放到阻塞任务里执行,是为了不堵住异步运行时。
调用图:调用 3 个内部函数(ensure_codex_backend_auth, is_zip_payload, build_reqwest_client);外部调用 8 个(join, from_utf8_lossy, bail!, auth_provider_from_auth, format!, create_dir_all, spawn_blocking, vec!)。
safe_join193–204 ↗
fn safe_join(base: &Path, name: &str) -> Result<PathBuf>
作用:把压缩包里的文件名安全地接到目标目录后面。它防止恶意文件名跳出目标目录,写到系统里不该写的位置。
数据流:进去的是一个安全的基础目录和压缩包内的文件名 → 它逐段检查路径,只允许普通文件名或普通子目录名,不允许绝对路径、上级目录等特殊写法 → 检查通过后输出完整路径;发现危险路径就返回错误。
调用关系:extract_zip_to_dir 在准备写每个文件前都会调用它。它是解压流程里的安全闸门,确保后续创建文件时不会被压缩包里的坏路径带偏。
调用图:被 1 处调用(extract_zip_to_dir);外部调用 3 个(join, new, bail!)。
is_zip_payload206–210 ↗
fn is_zip_payload(bytes: &[u8]) -> bool
作用:快速判断下载到的内容看起来是不是 zip 压缩包。这样可以避免把错误页面或其他乱七八糟的内容当成技能包解压。
数据流:进去的是一段下载到的原始字节 → 它检查开头几个字节是否符合 zip 文件常见的标记,也就是文件头签名 → 出来的是 true 或 false,表示是否像 zip。
调用关系:export_remote_skill 下载完成后会立刻调用它。只有它判断像 zip,下载流程才会进入创建目录和解压步骤。
调用图:被 1 处调用(export_remote_skill)。
extract_zip_to_dir212–240 ↗
fn extract_zip_to_dir(
bytes: Vec<u8>,
output_dir: &Path,
prefix_candidates: &[String],
) -> Result<()>
作用:把 zip 压缩包里的技能文件解开,写进指定目录。它会跳过目录项,整理文件名前缀,并逐个安全写入文件。
数据流:进去的是 zip 文件字节、输出目录,以及可能需要剥掉的顶层目录名前缀 → 它打开 zip,逐个读取里面的条目;目录条目跳过,文件条目先用 normalize_zip_name 整理名字,再用 safe_join 生成安全路径;接着创建父目录和目标文件,把内容复制进去 → 成功时没有额外返回值,但磁盘上会出现解压后的技能文件。
调用关系:它是 export_remote_skill 下载后的解压工人。export_remote_skill 把 zip 内容交给它;它再把文件名整理交给 normalize_zip_name,把路径安全检查交给 safe_join,然后用标准文件操作真正落盘。
调用图:调用 3 个内部函数(normalize_zip_name, safe_join, new);外部调用 4 个(create, create_dir_all, copy, new)。
normalize_zip_name242–259 ↗
fn normalize_zip_name(name: &str, prefix_candidates: &[String]) -> Option<String>
作用:整理 zip 里的文件名,让解压后的目录结构更干净。比如压缩包里常见的顶层 skill_id 文件夹,会被它剥掉。
数据流:进去的是压缩包内原始文件名,以及一组可能的顶层前缀 → 它先去掉开头的“./”,再检查文件名是否以某个前缀加斜杠开头;如果是,就去掉这层前缀 → 如果剩下的名字为空,返回空表示跳过;否则返回整理后的相对文件名。
调用关系:extract_zip_to_dir 处理每个 zip 文件条目时会先调用它。它负责把远程包里的包装目录去掉,让 safe_join 和后续写文件步骤拿到更合适的文件名。
调用图:被 1 处调用(extract_zip_to_dir);外部调用 1 个(format!)。
ext/skills/src/lib.rs源码 ↗
可以把这个文件想成一家店的前台。店后面有很多房间,比如配置、安装、技能来源、渲染、选择、状态、工具等,但外人不需要知道每个房间的具体位置。这个文件把内部模块先接进来,再挑出一些重要东西公开出去,比如 SkillsExtensionConfig、install、各种 SkillProvider。这样别的代码想使用“技能扩展”时,不用深入翻整个目录,只要从这个库的入口拿到这些公开接口就行。它本身不做复杂计算,也不保存数据;它的重要性在于划清边界:哪些能力是这个库对外承诺提供的,哪些实现细节留在库内部。如果没有它,外部代码会更难引用这些功能,内部结构也更容易被直接依赖,后续改代码会更麻烦。
ext/skills/src/provider.rs源码 ↗
这份文件本身不真正去加载技能,而是规定“别人应该怎么提供技能”。项目里的技能可能来自不同地方:比如执行器能看到的目录、宿主程序预先加载的技能、远程 MCP 资源等。为了不让上层代码分别写好几套取技能的办法,这里定义了 SkillProvider 这个特征(trait,可以理解成一份必须遵守的能力清单):任何技能来源只要实现它,就必须会“列出技能”“读取某个技能资源”“搜索技能内容”。文件还定义了几个请求数据结构,用来把一次查询需要的信息装好,例如本轮对话编号、允许访问的根目录、是否包含某类技能、要读哪个包里的哪个资源等。一个重要点是注释里强调的“权限边界”:哪个提供者列出来的资源,就必须通过同一个提供者去读或搜,不能偷偷把它变成本地路径绕过去。这能防止越权访问,像图书馆借书必须走对应柜台,不能自己进库房乱拿。
ext/skills/src/provider/executor.rs源码 ↗
这里的“执行环境”可以理解成一个隔开的工作空间,里面有自己的文件系统。这个文件做的事,就是拿到用户选中的环境目录,确认目录能用,再去里面扫描技能说明文件,把每个技能变成系统认识的目录条目。它还会按产品限制过滤技能,比如某些技能只给特定产品用。读技能内容时,它会先检查这个资源确实来自执行环境,并且资源和包名对得上,避免读错地方。路径也会被严格要求成绝对路径,像必须写清门牌号,不能只写“隔壁”。如果环境不存在、路径不合法、文件读不了,它不会假装成功,而是返回警告或错误,方便上层告诉用户哪里出了问题。
ExecutorSkillProvider::new_with_restriction_product38–46 ↗
fn new_with_restriction_product(
environment_manager: Arc<EnvironmentManager>,
restriction_product: Option<Product>,
) -> Self
作用:创建一个从执行环境读取技能的提供者。调用者可以顺便给它一个产品限制,让它以后只暴露适合这个产品的技能。
数据流:进去的是一个环境管理器,以及一个可选的产品限制 → 函数把它们保存到新的 ExecutorSkillProvider 里 → 出来的是一个可复用的技能提供者对象,本身不立刻扫描文件。
调用关系:它是这个提供者的入口构造函数,会被 refresh_test_state、new 和 selected_root_id_distinguishes_identical_executor_paths 这些创建流程或测试用到。后面的 list、read、search 都依赖这里保存下来的环境管理器和产品限制。
调用图:被 3 处调用(refresh_test_state, new, selected_root_id_distinguishes_identical_executor_paths)。
ExecutorSkillProvider::list50–109 ↗
fn list(&self, query: SkillListQuery) -> SkillProviderFuture<'_, SkillCatalog>
作用:列出执行环境里可用的技能,并整理成系统统一的技能目录。用户或上层功能需要展示“有哪些技能可用”时,就会走这里。
数据流:进去的是一个查询,里面带着用户选中的执行环境根目录 → 它逐个检查环境是否存在、路径是不是绝对路径,然后调用加载器去扫描技能,再按产品限制过滤结果 → 出来的是 SkillCatalog,也就是技能目录;遇到坏环境、坏路径或加载失败,会把说明写进目录的 warnings 里,而不是直接让整个列表崩掉。
调用关系:它是 SkillProvider 这个统一接口的一部分,负责把执行环境接到技能系统上。内部先用 executor_absolute_path 把路径把关,再交给 load_skills_from_roots 扫描技能,用 filter_skill_load_outcome_for_product 做产品过滤,最后把每个技能交给 catalog_entry_from_skill 变成目录条目。
调用图:调用 4 个内部函数(load_skills_from_roots, new, catalog_entry_from_skill, executor_absolute_path);外部调用 5 个(clone, pin, filter_skill_load_outcome_for_product, default, format!)。
ExecutorSkillProvider::read111–151 ↗
fn read(&self, request: SkillReadRequest) -> SkillProviderFuture<'_, SkillReadResult>
作用:读取某一个执行环境技能的实际文本内容。上层已经知道要打开哪个技能资源时,会用它把文件内容取出来。
数据流:进去的是一个读取请求,包含来源权限、包标识和资源标识 → 它先确认资源确实属于执行环境、包名和资源名匹配、资源绑定的环境还存在,再把真实文件路径转成文件系统能读的路径 → 出来的是 SkillReadResult,里面有原资源标识和文件文本;如果任何检查失败或文件读失败,就返回清楚的错误。
调用关系:它也是 SkillProvider 接口的一部分,通常发生在 list 已经列出技能之后。它不负责寻找所有技能,只负责安全地打开一个指定资源;真正读文件的活儿交给执行环境的 filesystem,并用 PathUri::from_abs_path 把路径转换成读文件接口需要的形式。
调用图:调用 2 个内部函数(new, from_abs_path);外部调用 2 个(pin, format!)。
ExecutorSkillProvider::search153–155 ↗
fn search(&self, _request: SkillSearchRequest) -> SkillProviderFuture<'_, SkillSearchResult>
作用:提供搜索接口,但这个执行环境提供者目前不做搜索。调用它会得到一个空的搜索结果。
数据流:进去的是搜索请求,但这里没有使用请求内容 → 函数直接生成默认的空 SkillSearchResult → 出来的是成功结果,只是里面没有匹配项,也不会改动任何状态。
调用关系:它是为了满足 SkillProvider 统一接口而存在。也就是说,上层可以对不同技能来源都调用 search,但执行环境这个来源暂时选择什么也不搜,只返回默认结果。
调用图:外部调用 2 个(pin, default)。
catalog_entry_from_skill158–194 ↗
fn catalog_entry_from_skill(
skill: &SkillMetadata,
enabled: bool,
authority: SkillAuthority,
selected_root_id: &str,
environment_id: &str,
) -> SkillCatalogEntry
作用:把底层扫描出来的技能元数据,包装成系统目录里的一条技能记录。这样上层不用关心文件路径和元数据细节,只看统一格式。
数据流:进去的是技能元数据、是否启用、来源权限、选中的根目录编号和环境编号 → 它生成展示用的 skill:// 路径,保留名称、描述、依赖等信息,并把真实文件路径绑定到环境资源标识里 → 出来的是 SkillCatalogEntry;如果技能被判定不可用,会标成禁用,如果不允许自动调用,会从提示里隐藏。
调用关系:它只被 list 调用,是扫描结果变成目录结果的最后一道包装工序。list 负责找到技能和判断能不能用,这个函数负责把单个技能做成上层能展示、能读取、能区分来源的条目。
调用图:调用 2 个内部函数(new, environment);被 1 处调用(list);外部调用 3 个(allows_implicit_invocation, new, format!)。
executor_absolute_path196–205 ↗
fn executor_absolute_path(path: &str) -> std::io::Result<AbsolutePathBuf>
作用:检查执行环境传来的路径是不是绝对路径,并把它变成系统认可的绝对路径类型。这样后续扫描不会因为模糊路径而读错地方。
数据流:进去的是一个路径字符串 → 它先转成普通路径,再检查是不是绝对路径;如果不是,就返回“必须是绝对路径”的错误;如果是,再交给 AbsolutePathBuf 做更严格的校验 → 出来的是安全可用的绝对路径,或一个输入不合法的错误。
调用关系:它被 list 在扫描每个选中根目录前调用,像门口保安一样先验票。只有通过这里检查的路径,才会被交给 load_skills_from_roots 去真正扫描技能文件。
调用图:调用 1 个内部函数(from_absolute_path_checked);被 1 处调用(list);外部调用 2 个(from, new)。
ext/skills/src/provider/host.rs源码 ↗
可以把这个文件理解成一个“转接头”。主程序已经把技能加载好了,里面有每个技能的名字、说明、文件路径、是否启用,以及加载失败的错误。扩展系统不想直接碰这些内部格式,所以这里的 HostSkillProvider 负责把它们翻译成扩展系统认识的格式。列技能时,它会把已加载技能变成目录条目,并把加载失败的地方变成警告。读技能时,它会按资源路径去已加载列表里找对应的技能文件,再请主程序读取文本。搜索功能目前不做真正搜索,只返回空结果。一个重要点是:这里刻意不重新加载、不缓存技能,因为技能从哪里来、怎么加载、哪些启用,都由核心主程序决定;这个文件只负责“把已有东西说成另一种语言”。
HostSkillProvider::new31–33 ↗
fn new() -> Self
作用:创建一个新的主机技能提供者。它本身不保存状态,所以创建出来只是拿到一个可以调用统一技能接口的空壳对象。
数据流:进去没有额外参数 → 它直接构造一个 HostSkillProvider → 出来一个新的提供者实例,不读取文件,也不改动任何外部数据。
调用关系:安装流程 install 会调用它,把这个提供者接入系统。之后真正列出或读取技能时,会通过这个实例调用 HostSkillProvider::list、HostSkillProvider::read 或 HostSkillProvider::search。
调用图:被 1 处调用(install)。
HostSkillProvider::list37–47 ↗
fn list(&self, query: SkillListQuery) -> SkillProviderFuture<'_, SkillCatalog>
作用:列出主程序已经加载好的技能,并把它们整理成扩展系统能显示和使用的技能目录。如果调用时没有带上已加载的主机技能,它会直接报错,因为它自己不会重新加载。
数据流:进去的是一个 SkillListQuery,其中必须包含 host,也就是主程序已经加载好的技能结果 → 它先检查 host 是否存在;存在就取出加载结果,交给 catalog_from_outcome 转成 SkillCatalog;不存在就生成错误 → 出来的是技能目录,里面有技能条目和加载警告,或是一条说明缺少主机技能的错误。
调用关系:外部想展示或枚举主机技能时会走这个统一的 SkillProvider 接口。这个函数把异步任务用 pin 包起来返回,并把真正的“加载结果转目录”工作交给 catalog_from_outcome;需要报错时会创建 SkillProviderError。
调用图:调用 2 个内部函数(new, catalog_from_outcome);外部调用 1 个(pin)。
HostSkillProvider::read49–82 ↗
fn read(&self, request: SkillReadRequest) -> SkillProviderFuture<'_, SkillReadResult>
作用:读取某一个主机技能的文本内容。它不是随便打开任意文件,而是只能读取已经被主程序加载过、能在清单里找到的技能资源。
数据流:进去的是 SkillReadRequest,里面有要读的 resource 路径,也必须带有 host 已加载技能 → 它先确认 host 存在,再在已加载技能列表里按技能文件路径匹配资源;为了兼容不同系统,还会把 Windows 的反斜杠路径换成斜杠再比一次 → 找到后调用 host_loaded_skills.read_skill_text 读取文本;找不到或读取失败就包装成错误 → 出来的是 SkillReadResult,包含原资源标识和文本内容,或是一条清楚说明原因的错误。
调用关系:当外部已经知道某个技能资源并想拿到正文时,会调用这个函数。它自己负责校验资源是否属于已加载技能,然后把真正读取文本的工作交给 host_loaded_skills.read_skill_text;遇到缺少 host、资源未加载、读取失败时,会通过 SkillProviderError::new 和 format! 生成可读错误信息。
HostSkillProvider::search84–86 ↗
fn search(&self, _request: SkillSearchRequest) -> SkillProviderFuture<'_, SkillSearchResult>
作用:提供搜索接口的占位实现。目前主机技能提供者不做搜索,所以无论请求是什么,都返回一个空的搜索结果。
数据流:进去的是 SkillSearchRequest,但当前没有使用它 → 它创建一个默认的 SkillSearchResult → 出来的是空搜索结果,不读取技能,也不改变状态。
调用关系:这是为了满足 SkillProvider 统一接口:别的提供者可能支持搜索,主机提供者目前不支持。调用方可以照常调用 search,但会得到默认空结果;函数内部只把这个结果包装成异步返回。
调用图:外部调用 2 个(pin, default)。
catalog_from_outcome89–110 ↗
fn catalog_from_outcome(outcome: &SkillLoadOutcome) -> SkillCatalog
作用:把核心主程序的技能加载结果,翻译成扩展系统使用的技能目录。它同时保留成功加载的技能,也把加载失败的地方写成警告,方便用户知道哪里出了问题。
数据流:进去的是 SkillLoadOutcome,也就是一次技能加载后的完整结果,里面有成功技能和错误列表 → 它先把每个加载错误改写成类似“某路径加载失败:原因”的警告;然后遍历 skills_with_enabled 给出的技能及其启用状态,把每个技能交给 catalog_entry_from_skill 变成目录条目并放进目录 → 出来的是 SkillCatalog,包含 entries 和 warnings。
调用关系:HostSkillProvider::list 会调用它来完成主要转换工作。它不关心请求从哪里来,只专心把加载结果拆开:错误变警告,技能变条目;具体每个技能条目怎么填,则交给 catalog_entry_from_skill。
调用图:调用 2 个内部函数(skills_with_enabled, catalog_entry_from_skill);被 1 处调用(list);外部调用 1 个(new)。
catalog_entry_from_skill112–134 ↗
fn catalog_entry_from_skill(skill: &SkillMetadata, enabled: bool) -> SkillCatalogEntry
作用:把单个技能的元信息变成目录里的一条记录。目录条目会包含技能名、说明、路径、依赖关系,以及它是否应该显示、是否启用。
数据流:进去的是一个 SkillMetadata 和一个 enabled 布尔值,enabled 表示这个技能当前是否启用 → 它取出技能文件路径作为包 ID 和资源 ID,把路径里的反斜杠换成斜杠作为更适合展示的路径;再填入名字、描述、短描述、依赖等信息;如果技能未启用,就把条目标成 disabled;如果技能不允许隐式调用,也就是不希望自动出现在提示里,就把它从提示展示中隐藏 → 出来的是一个完整的 SkillCatalogEntry。
调用关系:catalog_from_outcome 在处理每个已加载技能时会调用它。它会创建 SkillPackageId、SkillAuthority、SkillResourceId 等目录所需的小对象,并检查 allows_implicit_invocation 来决定是否隐藏条目;这样上层拿到目录后,就不需要再理解核心技能元数据的细节。
调用图:调用 3 个内部函数(new, new, new);被 1 处调用(catalog_from_outcome);外部调用 2 个(allows_implicit_invocation, new)。
ext/skills/src/provider/orchestrator.rs源码 ↗
这里的“技能”可以理解成系统可调用的一段说明或能力包;“编排器”是当前会话里提供这些技能资源的一方。这个文件的核心是 OrchestratorSkillProvider,它实现了统一的 SkillProvider 接口,让外部不用关心底层 MCP(Model Context Protocol,一种让程序互相提供资源的协议)怎么通信。列目录时,它只找指定 MCP 服务器里的技能资源,最多翻 10 页、最多收 100 个,避免卡死或拿太多。读取时,它会先确认资源确实属于对应技能包,防止拿错或越界读取。文件里还做了很多“门卫”工作:检查 URI 格式、限制名字和描述长度、清理换行和危险字符、限制内容大小。没有这些检查,坏数据、循环分页、超大内容或伪造资源都可能拖垮系统或造成混乱。
OrchestratorSkillProvider::new44–46 ↗
fn new() -> Self
作用:创建一个新的编排器技能提供者。它本身不带复杂状态,所以创建过程就像拿到一张空的服务窗口号码牌。
数据流:进去没有额外输入 → 它构造一个 OrchestratorSkillProvider 实例 → 出来一个可交给技能系统使用的提供者对象,不改动外部数据。
调用关系:它会被 thread_extensions 调用,用来把这个技能来源接入更大的扩展流程。后续真正干活的是同一个对象上的 list、read 和 search。
调用图:被 1 处调用(thread_extensions)。
OrchestratorSkillProvider::list50–150 ↗
fn list(&self, query: SkillListQuery) -> SkillProviderFuture<'_, SkillCatalog>
作用:向当前会话的 MCP 资源客户端询问:编排器提供了哪些技能。它会把原始资源整理成系统能识别的技能目录。
数据流:进去的是 SkillListQuery,里面可能带有 MCP 资源客户端 → 它先确认客户端存在、目标服务器存在,然后按页拉取资源,只接受 MIME 类型为 mcp/skill 的资源,并把合格资源交给 catalog_entry_from_resource 转成目录项 → 出来是 SkillCatalog;如果部分页面失败、数据不合格或数量被截断,会把提醒写进目录的 warnings。
调用关系:这是技能发现阶段的主入口。它内部会调用 catalog_entry_from_resource 解析单个资源,也会用异步超时工具限制总等待时间,避免外部资源服务一直不回应。调用方拿到目录后,才能知道有哪些技能可读。
调用图:调用 2 个内部函数(new, catalog_entry_from_resource);外部调用 6 个(pin, new, default, format!, now, timeout_at)。
OrchestratorSkillProvider::read152–216 ↗
fn read(&self, request: SkillReadRequest) -> SkillProviderFuture<'_, SkillReadResult>
作用:读取某个编排器技能的正文内容,通常就是技能包里的主提示文件。它重点防止读错来源、读错包、读到过大的内容。
数据流:进去的是 SkillReadRequest,包含技能来源、包 URI、资源 URI 和 MCP 客户端 → 它先检查这个请求确实属于编排器,再用 resource_belongs_to_package 确认资源在包下面,然后通过 MCP 客户端读取资源,只接受 URI 匹配的文本内容,并检查大小上限 → 出来是 SkillReadResult,里面有资源编号和文本内容;失败时返回带说明的错误。
调用关系:它在用户或系统真正需要打开某个技能时被调用。它把“这个资源能不能读”的判断交给 resource_belongs_to_package,把实际读取交给 MCP 客户端,并用超时保护整个读取过程。
调用图:调用 3 个内部函数(new, new, resource_belongs_to_package);外部调用 3 个(pin, format!, timeout)。
OrchestratorSkillProvider::search218–220 ↗
fn search(&self, _request: SkillSearchRequest) -> SkillProviderFuture<'_, SkillSearchResult>
作用:提供搜索接口,但这个编排器来源目前不支持真正搜索。它会直接返回一个空搜索结果。
数据流:进去的是 SkillSearchRequest,但当前不会使用里面的信息 → 它不查询任何外部资源,只创建默认的空结果 → 出来是空的 SkillSearchResult,不改动系统状态。
调用关系:它是为了满足 SkillProvider 统一接口而存在。调用方可以统一调用 search,但对这个提供者来说,实际可用能力主要是 list 和 read。
调用图:外部调用 2 个(pin, default)。
catalog_entry_from_resource223–249 ↗
fn catalog_entry_from_resource(resource: &Resource) -> Option<SkillCatalogEntry>
作用:把 MCP 返回的一条原始资源,变成系统技能目录里的一条正规记录。它像收货验货员,只把格式正确、信息完整的资源登记入库。
数据流:进去的是一个 Resource,里面有 URI、元数据和描述 → 它检查技能包 URI,读取 skill_name、plugin_name、source 等元数据,清理名字和描述,生成主文件 SKILL.md 的资源 URI → 出来是 SkillCatalogEntry;如果缺字段、格式坏、名字太长或有危险字符,就返回 None 表示跳过。
调用关系:它只由 OrchestratorSkillProvider::list 调用,用在发现技能的过程中。它会继续调用 validated_skill_uri、normalized_label、normalized_description 和 main_prompt_uri,把一条外部资源加工成安全可展示、可读取的目录项。
调用图:调用 7 个内部函数(new, new, new, main_prompt_uri, normalized_description, normalized_label, validated_skill_uri);被 1 处调用(list);外部调用 2 个(new, format!)。
validated_skill_uri251–253 ↗
fn validated_skill_uri(uri: &str, max_chars: usize) -> Option<&str>
作用:检查一个技能 URI 字符串是不是合格,并在合格时保留原字符串。URI 可以理解成资源地址,比如技能包住在哪里。
数据流:进去的是 URI 文本和最大字符数 → 它调用 validated_skill_url 做完整检查;如果通过,就返回原来的 URI 文本引用 → 如果不通过,返回 None,不让后续流程继续使用这个地址。
调用关系:它被 catalog_entry_from_resource 用来检查技能包地址。真正的地址规则由 validated_skill_url 负责,这个函数只是把“检查后仍要原字符串”的需求包了一层。
调用图:调用 1 个内部函数(validated_skill_url);被 1 处调用(catalog_entry_from_resource)。
validated_skill_url255–279 ↗
fn validated_skill_url(uri: &str, max_chars: usize) -> Option<Url>
作用:严格检查技能地址是否安全、规范。它不只看能不能解析,还要求地址形状符合这个系统认可的 skill://... 格式。
数据流:进去的是 URI 文本和长度上限 → 它先拒绝太长、含空白、控制字符或尖括号的文本,再用 Url::parse 解析,然后检查协议必须是 skill、必须有主机、不能带用户名密码端口查询片段,路径也不能有空段 → 出来是解析好的 Url;任何一项不合格就返回 None。
调用关系:它是多个安全检查的底层工具,被 validated_skill_uri 和 resource_belongs_to_package 调用。它把外部传来的地址先变成可信的结构,后面的目录生成和归属判断才有基础。
调用图:被 2 处调用(resource_belongs_to_package, validated_skill_uri);外部调用 1 个(parse)。
resource_belongs_to_package281–302 ↗
fn resource_belongs_to_package(package: &str, resource: &str) -> bool
作用:判断某个资源地址是不是真的属于某个技能包。它防止别人拿着一个包名,却去读另一个包里的文件。
数据流:进去的是包 URI 和资源 URI 两个字符串 → 它分别用 validated_skill_url 验证并解析,然后比较协议、主机和路径;资源路径必须比包路径更深,并且开头完全等于包路径 → 出来是 true 或 false,不读取外部资源,也不修改数据。
调用关系:它被 OrchestratorSkillProvider::read 在真正读取前调用。只有它判断通过,read 才会把请求交给 MCP 客户端去拿内容。
调用图:调用 1 个内部函数(validated_skill_url);被 1 处调用(read)。
normalized_label304–308 ↗
fn normalized_label(value: &str, max_chars: usize) -> Option<String>
作用:清理并检查技能名、插件名这类短标签。它确保这些名字是一行、不过长、非空,并且不含容易影响显示的字符。
数据流:进去的是原始文本和最大字符数 → 它先调用 normalized_single_line 把多余空白压成一行,再拒绝空字符串以及包含 &、<、> 的文本 → 出来是清理后的 String;不合格就返回 None。
调用关系:它被 catalog_entry_from_resource 用来处理 skill_name 和 plugin_name。目录项的名字能不能显示、会不会混乱,靠它先把关。
调用图:调用 1 个内部函数(normalized_single_line);被 1 处调用(catalog_entry_from_resource)。
normalized_description310–317 ↗
fn normalized_description(value: &str) -> Option<String>
作用:清理技能描述,并把可能干扰展示的特殊字符转成安全写法。比如把 < 变成 <,避免被当成标记。
数据流:进去的是原始描述文本 → 它先用 normalized_single_line 把描述整理成一行并限制长度,再替换 &、<、> 这些字符 → 出来是安全的描述字符串;如果太长或含控制字符,就返回 None。
调用关系:它被 catalog_entry_from_resource 用在生成目录项时。这样目录里展示的描述既简洁,又不会因为特殊字符破坏显示。
调用图:调用 1 个内部函数(normalized_single_line);被 1 处调用(catalog_entry_from_resource)。
normalized_single_line319–323 ↗
fn normalized_single_line(value: &str, max_chars: usize) -> Option<String>
作用:把一段文本压成干净的一行。它会把换行、多个空格等空白统一成单个空格,并检查长度。
数据流:进去的是原始文本和最大字符数 → 它按空白切开再用单个空格拼回去,然后确认字符数没超限、没有控制字符 → 出来是整理后的一行 String;不合格就返回 None。
调用关系:它是文本清理的基础小工具,被 normalized_label 和 normalized_description 调用。上层函数再根据名字或描述的不同用途继续做更具体的检查。
调用图:被 2 处调用(normalized_description, normalized_label)。
main_prompt_uri325–327 ↗
fn main_prompt_uri(package_uri: &str) -> String
作用:根据技能包地址拼出这个技能的主提示文件地址。约定的主文件名是 SKILL.md。
数据流:进去的是技能包 URI → 它去掉末尾多余的斜杠,再追加 /SKILL.md → 出来是主提示文件的完整资源 URI 字符串。
调用关系:它被 catalog_entry_from_resource 调用。目录项需要知道以后 read 应该读取哪个资源,而这个函数负责按统一约定生成那个资源地址。
调用图:被 1 处调用(catalog_entry_from_resource);外部调用 1 个(format!)。
ext/skills/src/sources.rs源码 ↗
系统里的技能可能来自不同地方:宿主程序、执行器、编排器,或者自定义来源。这个文件做的事,就是给每个来源贴上类型标签和名字,再把它们放进一个总列表里。需要列出技能时,它会按本轮请求的条件挑该问哪些来源;某个来源坏了,普通列目录时不会让整个流程崩掉,而是把问题写进警告里。需要读取或搜索某个具体技能时,它会根据技能声明的来源类型,只去问匹配的提供者;如果没配置对应来源,就返回清楚的错误。这里的“provider(提供者)”可以理解成真正存放或生成技能的人,而本文件只是负责把请求送到正确的人手里。
SkillProviderSource::new23–33 ↗
fn new(
kind: SkillSourceKind,
label: impl Into<String>,
provider: Arc<dyn SkillProvider>,
) -> Self
作用:创建一个技能来源记录。它把“来源种类”“显示用名字”和真正能提供技能的对象绑在一起。
数据流:输入一个来源种类、一个可转成文字的标签、一个共享的技能提供者 → 把标签转成字符串,并存进结构里 → 输出一个新的 SkillProviderSource,之后列表、读取、搜索都会靠它找到对应提供者。
调用关系:这是最底层的构造入口。SkillProviderSource::host、SkillProviderSource::executor 和 SkillProviderSource::orchestrator 都会借它来创建带固定类型的来源。
调用图:外部调用 1 个(into)。
SkillProviderSource::host35–37 ↗
fn host(label: impl Into<String>, provider: Arc<dyn SkillProvider>) -> Self
作用:快速创建“宿主程序技能”的来源。调用方不用自己手动填写 Host 这种来源类型。
数据流:输入一个标签和一个技能提供者 → 自动把来源种类设成 Host → 交给 SkillProviderSource::new 生成完整来源记录并返回。
调用关系:它是一个方便入口。SkillProviders::with_host_provider 添加宿主技能提供者时会调用它。
调用图:被 1 处调用(with_host_provider);外部调用 1 个(new)。
SkillProviderSource::executor39–41 ↗
fn executor(label: impl Into<String>, provider: Arc<dyn SkillProvider>) -> Self
作用:快速创建“执行器技能”的来源。执行器技能可以理解成由某个执行环境提供的能力。
数据流:输入标签和技能提供者 → 自动标记为 Executor 类型 → 调用 SkillProviderSource::new 组装成来源记录。
调用关系:它专门服务于添加执行器来源。SkillProviders::with_executor_provider 会用它生成来源后放进总列表。
调用图:被 1 处调用(with_executor_provider);外部调用 1 个(new)。
SkillProviderSource::orchestrator43–45 ↗
fn orchestrator(label: impl Into<String>, provider: Arc<dyn SkillProvider>) -> Self
作用:快速创建“编排器技能”的来源。编排器技能通常用于协调或安排其他能力。
数据流:输入标签和技能提供者 → 自动标记为 Orchestrator 类型 → 调用 SkillProviderSource::new 返回来源记录。
调用关系:它让添加编排器来源更简单。SkillProviders::with_orchestrator_provider 会调用它。
调用图:被 1 处调用(with_orchestrator_provider);外部调用 1 个(new)。
SkillProviderSource::should_list47–54 ↗
fn should_list(&self, query: &SkillListQuery) -> bool
作用:判断当前这一轮是否应该向这个来源列出技能。这样系统不会每次都问所有来源,能按需要挑选。
数据流:输入当前来源和一个技能列表查询条件 → 查看来源类型以及查询里的开关,比如是否包含宿主技能、是否有执行器根目录、是否包含编排器技能 → 输出 true 或 false,表示要不要列这个来源。
调用关系:它被 SkillProviders::list_for_turn 通过 SkillProviders::list_matching 间接使用,用来筛掉本轮不该出现的技能来源。
SkillProviderSource::owns_kind56–58 ↗
fn owns_kind(&self, kind: &SkillSourceKind) -> bool
作用:判断这个来源是否属于某个指定的来源种类。读取或搜索具体技能时,要靠它找对门牌号。
数据流:输入当前来源和目标来源种类 → 比较两者是否相同 → 输出 true 或 false。
调用关系:SkillProviders::read 和 SkillProviders::search 会用它过滤来源,只把请求发给和技能权威信息匹配的提供者。
SkillProviderSource::fmt62–68 ↗
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result
作用:定义调试时这个来源该怎么打印。它故意只打印来源种类和标签,不打印真正的提供者对象。
数据流:输入一个格式化器和当前来源 → 写入结构名、kind、label 这些便于排查的信息 → 输出格式化结果。
调用关系:这是 Rust 的 Debug 调试显示接口。日志或测试打印 SkillProviderSource 时会走到这里,帮助人看清当前注册了哪些来源。
调用图:外部调用 1 个(debug_struct)。
SkillProviders::new77–79 ↗
fn new() -> Self
作用:创建一个空的技能来源集合。调用方之后可以一步步往里面添加宿主、执行器或编排器提供者。
数据流:没有业务输入 → 使用默认值创建一个内部来源列表为空的 SkillProviders → 返回这个空集合。
调用关系:这是组装技能系统时常用的起点。thread_extensions、install 以及多个测试都会先创建它,再追加需要的提供者。
调用图:被 6 处调用(thread_extensions, install, orchestrator_catalog_snapshot_caches_failure, prompt_hidden_skill_can_still_be_invoked, root_qualified_locator_selects_only_the_matching_executor_skill, selected_executor_catalog_is_context_and_selected_entrypoint_is_turn_input);外部调用 1 个(default)。
SkillProviders::with_provider81–84 ↗
fn with_provider(mut self, source: SkillProviderSource) -> Self
作用:往技能来源集合里添加一个已经包装好的来源。它适合调用方自己已经准备好 SkillProviderSource 的情况。
数据流:输入当前集合和一个来源记录 → 把来源追加到内部列表末尾 → 返回更新后的集合,方便继续链式添加。
调用关系:这是通用添加入口。和几个专门的 with_host_provider、with_executor_provider 相比,它不替调用方决定来源类型。
SkillProviders::with_host_provider86–90 ↗
fn with_host_provider(mut self, provider: Arc<dyn SkillProvider>) -> Self
作用:把一个宿主程序技能提供者加入集合。宿主程序技能就是由主程序本身提供的能力。
数据流:输入当前集合和一个技能提供者 → 用固定标签 host 创建 Host 类型来源 → 把它追加到来源列表 → 返回更新后的集合。
调用关系:它调用 SkillProviderSource::host 来包装提供者。通常在系统启动或测试搭建技能环境时使用。
调用图:调用 1 个内部函数(host)。
SkillProviders::with_executor_provider92–96 ↗
fn with_executor_provider(mut self, provider: Arc<dyn SkillProvider>) -> Self
作用:把一个执行器技能提供者加入集合。这样后面如果本轮选择了执行器相关范围,就能列出或访问这些技能。
数据流:输入当前集合和一个技能提供者 → 用固定标签 executor 创建 Executor 类型来源 → 追加进内部列表 → 返回更新后的集合。
调用关系:它调用 SkillProviderSource::executor。之后 list_for_turn、read、search 可能会根据请求条件访问这个来源。
调用图:调用 1 个内部函数(executor)。
SkillProviders::with_orchestrator_provider98–102 ↗
fn with_orchestrator_provider(mut self, provider: Arc<dyn SkillProvider>) -> Self
作用:把一个编排器技能提供者加入集合。编排器技能通常是更高层的协调能力。
数据流:输入当前集合和一个技能提供者 → 用固定标签 orchestrator 创建 Orchestrator 类型来源 → 放入内部来源列表 → 返回更新后的集合。
调用关系:它调用 SkillProviderSource::orchestrator。后续 has_orchestrator_provider、list_orchestrator_for_turn 等流程会识别这个来源。
调用图:调用 1 个内部函数(orchestrator)。
SkillProviders::has_orchestrator_provider104–108 ↗
fn has_orchestrator_provider(&self) -> bool
作用:检查当前是否配置了编排器技能提供者。调用方可以据此决定是否启用某些编排器相关功能。
数据流:输入当前来源集合 → 遍历内部来源列表,查看有没有 Orchestrator 类型 → 输出 true 或 false。
调用关系:tools 模块会调用它,像先看工具箱里有没有某类工具,再决定要不要展示或使用相关能力。
调用图:被 1 处调用(tools)。
SkillProviders::list_for_turn110–113 ↗
async fn list_for_turn(&self, query: SkillListQuery) -> SkillCatalog
作用:列出当前这一轮对话或执行中应该可用的技能。它会按查询条件过滤来源,不是把所有技能一股脑拿出来。
数据流:输入一个技能列表查询 → 把查询传给 SkillProviders::list_matching,并使用每个来源自己的 should_list 判断 → 输出合并后的技能目录。
调用关系:list_skills 会调用它来准备本轮可见技能。它本身不直接问提供者,而是把通用列目录流程交给 SkillProviders::list_matching。
调用图:调用 1 个内部函数(list_matching);被 1 处调用(list_skills)。
SkillProviders::list_orchestrator_for_turn115–136 ↗
async fn list_orchestrator_for_turn(
&self,
query: SkillListQuery,
) -> SkillProviderResult<SkillCatalog>
作用:只列出编排器来源的技能,并且对错误更严格。任何编排器来源不可用,都会直接返回错误。
数据流:输入一个技能列表查询 → 创建空目录,逐个找到 Orchestrator 来源并调用它们的 list → 成功就合并目录;一旦失败,就把错误改写成带来源标签的说明并返回 → 最后输出完整编排器技能目录。
调用关系:list_skills 和 catalog 会在需要专门拿编排器技能时调用它。它不像普通 list_matching 那样把失败变成警告,因为编排器目录在这些场景里更关键。
调用图:被 2 处调用(list_skills, catalog);外部调用 2 个(default, clone)。
SkillProviders::list_matching138–154 ↗
async fn list_matching(
&self,
query: &SkillListQuery,
should_list: impl Fn(&SkillProviderSource) -> bool,
) -> SkillCatalog
作用:按给定规则列出一批匹配来源的技能,并合并成一个目录。它是普通列技能流程的核心循环。
数据流:输入查询条件和一个“是否要列这个来源”的判断函数 → 创建空技能目录 → 遍历所有符合条件的来源,调用各自 provider.list → 把成功结果合并,失败结果交给 extend_catalog 变成警告 → 输出合并后的目录。
调用关系:SkillProviders::list_for_turn 会调用它。它再把每个来源返回的结果交给 extend_catalog 处理,负责把多个来源拼成一个总目录。
调用图:调用 1 个内部函数(extend_catalog);被 1 处调用(list_for_turn);外部调用 2 个(default, clone)。
SkillProviders::read156–179 ↗
async fn read(
&self,
request: SkillReadRequest,
) -> Result<SkillReadResult, SkillProviderError>
作用:读取某个具体技能的完整内容。它会根据请求里写明的来源种类,只去问对应类型的提供者。
数据流:输入一个读取请求 → 从请求的 authority.kind 看目标来源种类 → 遍历匹配来源并调用 provider.read → 第一个成功结果直接返回;如果都失败,返回最后一个错误;如果根本没有匹配来源,返回“对应提供者未配置”的错误。
调用关系:read_skill 会调用它来真正拿技能内容。它依赖 SkillProviderSource::owns_kind 找到正确来源,并把实际读取动作交给具体 provider。
调用图:调用 1 个内部函数(new);被 1 处调用(read_skill);外部调用 2 个(clone, format!)。
SkillProviders::search181–204 ↗
async fn search(
&self,
request: SkillSearchRequest,
) -> Result<SkillSearchResult, SkillProviderError>
作用:在某个来源范围内搜索技能。它和读取类似,也是先按来源种类找提供者,再让提供者执行搜索。
数据流:输入一个搜索请求 → 根据请求里的 authority.kind 筛选来源 → 逐个调用匹配来源的 provider.search → 有成功结果就返回;都失败则返回最后错误;没有任何匹配来源则返回“提供者未配置”的错误。
调用关系:这是搜索技能的入口之一。它使用 SkillProviderSource::owns_kind 做路由,真正的搜索规则由具体 provider 决定。
extend_catalog207–218 ↗
fn extend_catalog(
catalog: &mut SkillCatalog,
result: Result<SkillCatalog, SkillProviderError>,
label: &str,
)
作用:把某个来源返回的技能目录并入总目录;如果这个来源失败,就把失败写成警告而不是立刻中断。
数据流:输入总目录、某个来源的结果、来源标签 → 如果结果成功,就把来源目录合并进总目录;如果失败,就往总目录的 warnings 里追加一条“某来源技能不可用”的提示 → 不返回新对象,而是直接修改传入的总目录。
调用关系:SkillProviders::list_matching 在处理每个来源结果时会调用它。它让普通列目录更有韧性:一个来源坏了,其他来源的技能仍然可以显示。
调用图:调用 1 个内部函数(extend);被 1 处调用(list_matching);外部调用 1 个(format!)。
ext/skills/src/tools/mod.rs源码 ↗
可以把这个文件看成“技能工具箱”的总装台。外面的系统想让模型使用技能时,先来这里拿到两个工具:一个负责列技能,一个负责读技能。文件里还准备了共享上下文,里面放着技能来源、可选的 MCP 资源客户端(MCP 可以理解成给程序取外部资源的一套通道)、以及当前对话线程的技能状态。具体工具处理请求时,会通过这个上下文拿到某一轮对话可用的技能目录。这里还统一规定了工具名字怎么加命名空间、输入输出的 JSON 格式怎么生成、调用参数怎么解析,以及技能句柄这种“标识符”必须非空、不能太长、不能有控制字符。这样做的好处是:模型传来的东西即使不规范,也能尽早变成清楚的错误,而不是在更深处悄悄坏掉。
skill_tools36–52 ↗
fn skill_tools(
providers: SkillProviders,
mcp_resources: Option<Arc<McpResourceClient>>,
thread_state: Arc<SkillsThreadState>,
) -> Vec<Arc<dyn ToolExecutor<ToolCall>>>
作用:创建并返回这一组“技能工具”,让上层系统可以把它们交给模型调用。没有它,列技能和读技能这两个能力就不会被注册到工具列表里。
数据流:进去的是技能提供者、可选的 MCP 资源客户端、以及当前线程的技能状态;它们先被装进一个共享的 SkillToolContext;然后分别交给列表工具和读取工具;出来的是一组可执行工具对象,供外层系统挂载使用。
调用关系:它由上层的 tools 流程调用,属于启动或装配阶段的小总管。它自己不真正列出或读取技能,只是把同一份上下文分给 list::ListTool 和 read::ReadTool,让后续请求处理时各自干活。
SkillToolContext::catalog62–81 ↗
async fn catalog(&self, turn_id: &str, authority: SkillToolAuthority) -> SkillCatalog
作用:根据当前调用者的权限,取出这一轮对话能看到的技能目录。它保证工具看到的是合适范围内的技能,而不是随便把所有技能都暴露出去。
数据流:进去的是当前轮次 turn_id 和权限标记;如果权限是 Orchestrator,它会让 providers 准备这一轮的编排器技能查询,再交给 thread_state 生成或复用技能目录快照;出来的是一个 SkillCatalog,也就是可用技能的清单。
调用关系:它在具体工具的 handle 处理过程中被调用,列表工具和读取工具都需要先知道当前有哪些技能。它会把获取技能来源的活交给 list_orchestrator_for_turn,并把快照维护交给 SkillsThreadState。
调用图:调用 1 个内部函数(list_orchestrator_for_turn);被 2 处调用(handle, handle);外部调用 1 个(new)。
SkillToolAuthority::from_authority91–98 ↗
fn from_authority(authority: &SkillAuthority) -> Option<Self>
作用:把系统里通用的技能权限,转换成这个工具模块自己认识的权限类型。它像门卫核对证件,只接受来自指定编排器来源的技能权限。
数据流:进去的是一个 SkillAuthority;函数把它和“编排器来源 + 指定 MCP 服务名”组成的标准权限做比较;匹配就输出 Some(Orchestrator),不匹配就输出 None。
调用关系:它会在把技能条目整理成可展示结果时被 listed_skill 使用。它依赖 SkillAuthority::new 构造标准参照物,用来判断这个技能是不是本工具允许表达的那一种来源。
调用图:调用 1 个内部函数(new);被 1 处调用(listed_skill)。
SkillToolAuthority::into_authority100–106 ↗
fn into_authority(self) -> SkillAuthority
作用:把这个模块自己的权限标记,还原成系统通用的 SkillAuthority。这样别的模块就不用懂这里的内部枚举,也能继续用统一的权限表示。
数据流:进去的是 SkillToolAuthority;目前只有 Orchestrator 一种;函数把它转换成“编排器来源 + 指定 MCP 服务名”的 SkillAuthority;出来的是系统通用权限对象。
调用关系:它是 from_authority 的反向转换工具。需要把工具层的权限重新交回技能目录或外部系统时,会用它生成标准 SkillAuthority。
调用图:调用 1 个内部函数(new)。
skill_tool_name109–111 ↗
fn skill_tool_name(name: &str) -> ToolName
作用:给技能工具生成带命名空间的正式名字。命名空间可以理解成“文件夹名”,避免不同工具都叫同一个普通名字时撞车。
数据流:进去的是一个短工具名,比如 list 或 read;函数把固定的 skills 命名空间和这个名字拼成 ToolName;出来的是系统认可的完整工具名。
调用关系:它是给各个技能子工具声明名字用的辅助零件。内部把真正的拼装工作交给 ToolName::namespaced,保证所有技能工具都归在同一个 skills 名下。
调用图:调用 1 个内部函数(namespaced)。
skill_function_tool113–129 ↗
fn skill_function_tool(name: &str, description: &str) -> ToolSpec
作用:生成一个可被 Responses API 使用的函数工具说明。简单说,它告诉模型:这个工具叫什么、干什么、输入要长什么样、输出会长什么样。
数据流:进去的是工具名、描述文字,以及输入和输出类型;函数根据这些类型生成 JSON Schema(JSON 数据格式说明书),再解析输入 schema,包成一个 ResponsesApiTool;最后放进 skills 命名空间里,输出 ToolSpec。
调用关系:它服务于具体的技能工具定义阶段。它会调用 schema 模块生成输入输出格式,调用 parse_tool_input_schema 检查输入格式能不能被工具系统理解,再用 default_namespace_description 给 skills 这个命名空间补上说明。
调用图:外部调用 4 个(parse_tool_input_schema, default_namespace_description, Namespace, vec!)。
parse_args131–140 ↗
fn parse_args(call: &ToolCall) -> Result<T, FunctionCallError>
作用:把模型传来的工具参数从 JSON 字符串变成 Rust 里的具体数据结构。这样后面的代码不用手动从字符串里一点点抠字段。
数据流:进去的是一次 ToolCall;函数先取出 function_arguments 字符串;如果是空白,就当成一个空 JSON 对象;否则按 JSON 解析;再把 JSON 值转换成调用者指定的类型;成功就返回该类型,失败就返回一个可以告诉模型的参数错误。
调用关系:它位于工具请求刚进入处理流程的位置,是具体 handle 代码常用的入口检查。它把读取原始参数的事交给 ToolCall,把 JSON 文本解析交给 serde_json,出错时包装成 FunctionCallError::RespondToModel,让模型知道该修正输入。
调用图:外部调用 5 个(Object, function_arguments, new, from_str, from_value)。
validate_handle142–150 ↗
fn validate_handle(name: &str, value: &str, max_bytes: usize) -> Result<(), FunctionCallError>
作用:检查技能句柄这类标识符是否安全、合理。句柄可以理解成“取东西用的编号”,它不能是空的、不能太长,也不能带看不见的控制字符。
数据流:进去的是字段名、字段值和最大字节数;函数调用 is_bounded_handle 做真正检查;通过就什么也不改并返回成功;不通过就生成一条明确的错误信息,告诉模型这个字段应该满足哪些要求。
调用关系:它是读取或定位技能前的防线。它把细节判断交给 is_bounded_handle,自己负责把失败原因变成 FunctionCallError::RespondToModel,方便工具处理流程把错误反馈给模型。
调用图:调用 1 个内部函数(is_bounded_handle);外部调用 2 个(format!, RespondToModel)。
is_bounded_handle152–154 ↗
fn is_bounded_handle(value: &str, max_bytes: usize) -> bool
作用:做最基础的句柄合法性判断。它只回答一个问题:这个字符串能不能作为受限长度的安全标识符使用。
数据流:进去的是一个字符串和最大字节数;函数检查它不是空字符串、字节长度不超过限制、并且没有控制字符;出来的是 true 或 false,不产生错误文字,也不改任何状态。
调用关系:它被 validate_handle 调用,是底层判断器。上层函数负责解释错误、告诉模型怎么改;这个函数只保持简单纯粹,像一把尺子一样量字符串合不合格。
调用图:被 1 处调用(validate_handle)。
external_json_output156–161 ↗
fn external_json_output(value: &T) -> Result<Box<dyn ToolOutput>, FunctionCallError>
作用:把工具结果包装成外部可见的 JSON 输出。也就是说,它把内部数据整理成模型和外部接口都能接收的标准工具返回值。
数据流:进去的是任何可以序列化的数据;函数先把它转成 serde_json::Value;如果转换失败,就返回致命错误,因为这说明程序自己的输出结构有问题;成功后创建 JsonToolOutput,并标记为 external context;出来的是 boxed ToolOutput。
调用关系:它处在工具处理流程的末尾,用来统一返回结果格式。它把数据转换交给 serde_json,把输出容器交给 JsonToolOutput,确保列表工具、读取工具等返回给外界的形状一致。
ext/skills/src/tools/list.rs源码 ↗
这个文件像一个“技能目录查询窗口”。外部系统调用 list 工具时,会带上想查询的权限来源,也就是 authority(可以理解成“谁拥有这些技能”)。代码先解析调用参数,再向上下文里的技能目录要数据,然后只挑出已经启用、并且属于这个 authority 的技能。每个技能会被整理成简单的返回项:权限、包句柄、名字、描述、主资源句柄。这里有两个保护动作很重要:第一,包句柄和主资源句柄必须长度受控,太长就不返回,避免把异常或过大的标识暴露出去;第二,目录里的警告最多只返回 4 条,每条最多 256 字节,避免输出被警告信息撑爆。最后,结果被包装成外部能读懂的 JSON 输出。
ListTool::tool_name55–57 ↗
fn tool_name(&self) -> ToolName
作用:告诉工具系统:这个工具的名字是什么。这里把基础名字“list”加工成技能工具专用的完整名字,避免和别的工具重名。
数据流:进去的是这个 ListTool 自身,但它不需要读取复杂状态 → 它把固定的工具名“list”交给统一的命名函数 → 出来的是一个 ToolName,也就是工具系统识别这个工具用的正式名字。
调用关系:当工具框架登记或查找工具时,会问 ListTool 要名字。它把命名这件事交给 skill_tool_name,这样所有技能工具的名字格式都保持一致。
调用图:外部调用 1 个(skill_tool_name)。
ListTool::spec59–64 ↗
fn spec(&self) -> ToolSpec
作用:给外部调用者说明这个工具怎么用:需要什么参数、会返回什么结果、这个工具是干什么的。可以把它理解成工具的“使用说明书”。
数据流:进去的是这个 ListTool 自身 → 它用 ListArgs 描述输入格式,用 ListResponse 描述输出格式,并附上一段人能读懂的说明 → 出来的是 ToolSpec,也就是工具框架展示和校验调用时需要的规格说明。
调用关系:工具系统在暴露工具能力时会读取这个规格。它不真正执行查询,只是提前告诉调用方:调用 list 时要传 authority,返回的是可用于 skills.read 的包句柄和主资源句柄。
ListTool::handle66–83 ↗
fn handle(&self, call: ToolCall) -> ToolExecutorFuture<'_>
作用:真正执行一次 list 调用。它接收外部请求,查技能目录,筛出能显示的技能,再返回 JSON 结果。
数据流:进去的是一次 ToolCall,也就是外部发来的工具调用,里面有本轮对话编号和参数 → 它先用 parse_args 解析参数,再按 turn_id 和 authority 去 context 里取技能目录;随后筛掉未启用或权限不匹配的技能,并把警告压缩到安全长度 → 出来的是外部 JSON 输出,同时没有直接修改技能目录本身。
调用关系:这是整个文件的主流程,工具框架收到 list 请求时会调用它。它向上下文索取 catalog,把每个目录项交给 listed_skill 转成可返回的样子,把警告交给 bounded_warnings 裁剪,最后交给 external_json_output 变成外部能接收的 JSON。
调用图:调用 2 个内部函数(catalog, bounded_warnings);外部调用 3 个(pin, external_json_output, parse_args)。
listed_skill86–101 ↗
fn listed_skill(entry: SkillCatalogEntry) -> Option<ListedSkill>
作用:把目录里的一个原始技能条目,转换成可以安全返回给外部的简化信息。如果这个条目不适合公开返回,就直接丢掉。
数据流:进去的是一个 SkillCatalogEntry,也就是目录中的技能记录 → 它先把内部 authority 转成外部工具认识的 authority,再检查包句柄和主资源句柄是否在允许长度内 → 出来的是 Some(ListedSkill) 表示可以展示,或者 None 表示这个技能不应该出现在结果里。
调用关系:ListTool::handle 在遍历技能目录时会间接用到它,把一条条内部记录变成返回列表里的条目。它依赖 from_authority 做权限形式转换,也依赖 is_bounded_handle 做长度保护,保证返回给外部的是受控数据。
调用图:调用 1 个内部函数(from_authority);外部调用 1 个(is_bounded_handle)。
bounded_warnings103–112 ↗
fn bounded_warnings(warnings: Vec<String>) -> Vec<String>
作用:把技能目录产生的警告信息缩短、限量后再返回。这样即使警告很多或很长,也不会让工具输出变得过大。
数据流:进去的是一组 warning 字符串 → 它最多取前 4 条,并把每条按 UTF-8 字节长度裁到最多 256 字节;UTF-8 是常见文字编码,这里裁剪时会避免把中文等字符切坏 → 出来的是一组更短、更安全的警告字符串。
调用关系:ListTool::handle 在组织最终响应时调用它。它只处理警告,不碰技能列表;这样主流程可以放心把目录警告带给外部,同时不会让警告信息喧宾夺主或撑爆输出。
调用图:被 1 处调用(handle)。
ext/skills/src/tools/read.rs源码 ↗
可以把这个文件理解成“技能资源阅览室的前台”。外部请求想读某份资料时,不能直接冲进仓库拿,而是先到这里登记:说明权限来源、技能包、资源编号。这里会先解析请求参数,再检查包名和资源名长度是否安全,还会查目录,确认这个技能包确实已经启用,并且属于请求里说的那个来源。确认没问题后,它才把真正的读取工作交给后面的技能提供者。读回来后,它还会做一次核对:提供者返回的资源必须就是刚才请求的那个,不能偷换。如果一切正常,它把资源名和内容包装成 JSON 返回给调用方;如果不正常,就给模型一个能理解的错误,而不是泄露内部细节。
ReadTool::tool_name47–49 ↗
fn tool_name(&self) -> ToolName
作用:这个函数告诉工具系统:这个工具的名字叫技能体系里的 read。别人要调用读取技能资源的能力时,就靠这个名字找到它。
数据流:进去的是这个 ReadTool 自身,但它不需要读取复杂状态;它把固定的名字 read 交给统一的命名函数加工;出来的是一个工具名,供外部工具框架识别和路由。
调用关系:它是工具注册和匹配时会用到的小入口。它把具体命名规则交给 skill_tool_name,这样 read 这个短名字会变成技能工具体系认可的正式名字。
调用图:外部调用 1 个(skill_tool_name)。
ReadTool::spec51–56 ↗
fn spec(&self) -> ToolSpec
作用:这个函数向外说明 read 工具怎么用:需要哪些参数,会返回什么样的数据,以及这个工具是读取已启用技能里的完整资源。它相当于给调用方看的“使用说明书”。
数据流:进去的是这个工具对象;它拿固定工具名、参数类型 ReadArgs、返回类型 ReadResponse,以及一段说明文字,生成一份工具规格;出来的是 ToolSpec,也就是工具系统能展示和校验的说明。
调用关系:它通常在工具被注册或暴露给模型时使用。它不直接读取资源,而是为后面的 ReadTool::handle 铺路,让调用方知道该传 authority、package、resource 这些信息。
ReadTool::handle58–111 ↗
fn handle(&self, call: ToolCall) -> ToolExecutorFuture<'_>
作用:这个函数是真正执行读取的地方。它接到一次工具调用后,先验明请求是否合法、资源是否属于已启用的技能包,然后才去读取内容并返回。
数据流:进去的是一次 ToolCall,里面有调用编号、轮次编号和模型传来的参数;它先用 parse_args 把参数变成 ReadArgs,再把 authority 转成内部可用的权限来源,接着用 validate_handle 检查 package 和 resource 这两个标识不要过长或不合规。之后它读取当前目录,确认请求的技能包已启用且来源匹配。通过检查后,它构造 SkillReadRequest,把读取任务交给 thread_state.read_skill。读回结果后,它核对返回的资源编号必须和请求一致,最后用 external_json_output 输出包含 resource 和 contents 的 JSON。过程中如果包不可用、读取失败或资源被替换,会返回对应错误。
调用关系:它是 read 工具被实际调用时的主流程。工具框架把 ToolCall 交给它;它自己负责把关和组织步骤,把参数解析交给 parse_args,把标识检查交给 validate_handle,把目录查询交给 context.catalog,把真正读取交给 thread_state.read_skill,最后把成功结果交给 external_json_output 统一包装。
调用图:调用 2 个内部函数(new, catalog);外部调用 7 个(pin, new, external_json_output, parse_args, validate_handle, Fatal, RespondToModel)。
ext/skills/src/extension.rs源码 ↗
可以把这个文件想成“技能功能的总插线板”。系统开新线程时,它先记下当前配置、可访问的目录,以及是否允许使用编排器技能。配置改了,它会更新这份状态。每轮用户说话前,它会去不同来源查有哪些技能可用,比如主机提供的技能、内置技能、编排器技能等;如果用户明确提到某个技能,它就读取这个技能的主说明,并把说明作为上下文交给模型。为了防止提示太长,它会截断过大的内容,并发出警告。它还会在合适的时候注册技能工具,让模型可以通过工具去查询或读取技能。没有这个文件,技能虽然可能存在,但系统不知道何时展示、何时加载、何时报警,也不知道怎么把它们接到对话流程里。
SkillsExtension::on_thread_start57–74 ↗
fn on_thread_start(&'a self, input: ThreadStartInput<'a, C>) -> ExtensionFuture<'a, ()>
作用:在线程刚开始时,为技能功能准备一份“本线程状态”。这份状态记录配置、用户选中的能力根目录,以及当前是否允许使用编排器技能。
数据流:输入是一段新线程启动信息,里面有配置、线程存储区和运行环境列表。它先从线程存储区读取已选中的能力根目录;再检查是否存在本地环境,如果有本地环境,就关闭编排器技能;最后用这些信息创建 SkillsThreadState,并放回线程存储区。结果是后面的技能查询、提示注入、工具注册都有地方读取同一份线程状态。
调用关系:这是扩展框架在线程启动时调用的第一步。它会创建新的 SkillsThreadState;后面的配置更新、提示贡献、每轮输入处理和工具注册都依赖这份状态。
调用图:调用 1 个内部函数(new);外部调用 1 个(pin)。
SkillsExtension::on_config_changed81–99 ↗
fn on_config_changed(
&self,
_session_store: &ExtensionData,
thread_store: &ExtensionData,
_previous_config: &C,
new_config: &C,
)
作用:当宿主程序的配置变化时,把技能扩展内部保存的配置同步更新。这样用户打开或关闭某些技能选项后,不用重启线程也能生效。
数据流:输入是旧配置、新配置、线程存储区等信息。它用传入的 config_from_host 函数把宿主配置转换成技能扩展自己的配置。如果线程里已经有 SkillsThreadState,就直接改里面的配置;如果还没有,就新建一份默认线程状态并插入。结果是线程存储区里的技能配置变成最新版本。
调用关系:扩展框架在配置变化时调用它。它不自己去列技能或读技能,只负责保证后续 SkillsExtension::contribute、SkillsExtension::tools 等步骤看到的是新配置。
SkillsExtension::tools148–167 ↗
fn tools(
&self,
session_store: &ExtensionData,
thread_store: &ExtensionData,
) -> Vec<Arc<dyn ToolExecutor<ToolCall>>>
作用:决定这一刻要不要把“技能工具”交给系统使用。技能工具可以理解成给模型的一组按钮,让模型能主动查询或读取编排器侧的技能。
数据流:输入是会话存储区和线程存储区。它先取出 SkillsThreadState;如果没有状态,直接返回空列表。然后检查是否真的有编排器技能提供者,以及本线程是否允许编排器技能;不满足也返回空列表。满足条件时,它用当前 providers、MCP 资源客户端和线程状态生成工具列表并返回。
调用关系:扩展框架在收集可用工具时调用它。它会先做开关检查,只有允许时才把工作交给 skill_tools 生成真正的工具执行器。
调用图:调用 2 个内部函数(has_orchestrator_provider, skill_tools);外部调用 2 个(new, clone)。
SkillsExtension::contribute174–291 ↗
fn contribute(
&'a self,
input: TurnInputContext,
session_store: &'a ExtensionData,
thread_store: &'a ExtensionData,
turn_store: &'a ExtensionData,
) -> Ext
作用:在每轮用户输入进入模型前,决定要补充哪些和技能有关的上下文。比如列出可用技能,或者把用户点名的技能说明直接加入本轮提示。
数据流:输入包括本轮用户内容、会话存储区、线程存储区和本轮临时存储区。它先读取线程状态和配置,再收集主机已加载技能,组装查询条件并调用 SkillsExtension::list_skills 得到技能目录。目录里有警告就通过 SkillsExtension::emit_warning 发出去。接着它从用户输入中找出明确提到的技能;如果配置允许,还会生成“可用技能列表”片段。对每个被点名的技能,它调用 SkillsExtension::read_main_prompt 读取主说明,必要时截断过长内容,再包装成 SkillInstructions 放进返回片段。最后,它把本轮的技能目录、选中的技能、警告、是否注入过主说明等信息写入 turn_store;如果涉及主机技能提示,也会把对应路径记录进去。输出是本轮要追加给模型的一组上下文片段。
调用关系:扩展框架在处理每轮用户输入时调用它。它是本文件里最核心的串联者:先让 SkillsExtension::list_skills 收集技能,再用 collect_explicit_skill_mentions 找用户点名的技能,然后用 SkillsExtension::read_main_prompt 读取内容,用渲染和截断函数整理文本,遇到问题就交给 SkillsExtension::emit_warning 发警告。
调用图:调用 9 个内部函数(insert, level_id, emit_warning, list_skills, read_main_prompt, available_skills_fragment, truncate_main_prompt_contents, truncate_utf8_to_bytes, collect_explicit_skill_mentions);外部调用 5 个(new, pin, new, default, format!)。
SkillsExtension::list_skills295–317 ↗
async fn list_skills(
&self,
mut query: SkillListQuery,
thread_state: &SkillsThreadState,
) -> SkillCatalog
作用:汇总本轮可用的技能目录。它会把普通技能来源和编排器技能来源合在一起,给后续展示和选择使用。
数据流:输入是一份 SkillListQuery 查询条件和当前线程状态。它先记下查询里是否要求包含编排器技能,然后临时关掉这个选项,让 providers.list_for_turn 先列出普通来源的技能。如果原本要求包含编排器技能,它再通过线程状态拿到编排器技能目录快照,并把这部分合并进总目录。输出是一份 SkillCatalog,里面有技能条目,也可能带有警告。
调用关系:它被 SkillsExtension::contribute 调用,用来给每轮输入准备技能目录。它自己不判断用户选了哪个技能,只负责把来自 list_for_turn 和 list_orchestrator_for_turn 的结果合成一份目录;编排器部分通过 orchestrator_catalog_snapshot 做缓存或快照式读取,避免每次都粗暴重建。
调用图:调用 3 个内部函数(list_for_turn, list_orchestrator_for_turn, orchestrator_catalog_snapshot);被 1 处调用(contribute);外部调用 1 个(clone)。
SkillsExtension::read_main_prompt319–339 ↗
async fn read_main_prompt(
&self,
entry: &SkillCatalogEntry,
host_loaded_skills: Option<Arc<HostLoadedSkills>>,
session_store: &ExtensionData,
thread_state: &Sk
作用:读取某个技能的主说明文件。主说明就是告诉模型“这个技能怎么用、适合什么场景”的核心文本。
数据流:输入是一个技能目录条目、可能存在的主机已加载技能、会话存储区和线程状态。它从条目里取出技能来源、包标识和主说明资源路径,再加上主机技能信息和 MCP 资源客户端,组成 SkillReadRequest。然后交给 thread_state.read_skill 去实际读取。成功时输出 SkillReadResult;失败时把错误转换成普通字符串返回。
调用关系:它被 SkillsExtension::contribute 在用户明确提到某个技能时调用。它不直接关心怎么展示文本,只负责把“读这个技能说明”的请求交给线程状态和 providers;读到的内容再由 contribute 截断、包装并放入本轮上下文。
调用图:调用 1 个内部函数(read_skill);被 1 处调用(contribute)。
SkillsExtension::emit_warning341–346 ↗
fn emit_warning(&self, turn_id: &str, message: String)
作用:把技能相关的警告发给外部系统。比如技能目录加载有问题,或者某个技能说明太长被截断,都通过它通知用户或宿主界面。
数据流:输入是本轮的 turn_id 和一段警告文字。它把这段文字包装成 WarningEvent,再放进 Event 里,最后交给 event_sink 发出去。结果是外部事件流里出现一条警告消息;函数本身不返回有用数据。
调用关系:它被 SkillsExtension::contribute 在发现目录警告、读取失败、内容被截断等情况时调用。它是本文件对外“喊一声出问题了”的统一出口。
调用图:被 1 处调用(contribute);外部调用 1 个(Warning)。
install349–360 ↗
fn install(
registry: &mut ExtensionRegistryBuilder<C>,
config_from_host: impl Fn(&C) -> SkillsExtensionConfig + Send + Sync + 'static,
)
作用:用默认方式安装技能扩展。调用者只要提供“如何从宿主配置里取出技能配置”的函数,就能把技能功能接入注册表。
数据流:输入是扩展注册表和一个配置转换函数。它创建默认的 SkillProviders,并额外加上 HostSkillProvider,也就是主机提供技能的来源。然后它把这些东西交给 install_with_providers。输出没有单独返回值,但注册表会被改动,之后系统就知道有这个技能扩展了。
调用关系:这是最常用的安装入口。它不亲自注册每一种贡献者,而是先准备默认 providers,再把真正注册工作交给 install_with_providers。
调用图:调用 3 个内部函数(install_with_providers, new, new);外部调用 1 个(new)。
install_with_providers362–379 ↗
fn install_with_providers(
registry: &mut ExtensionRegistryBuilder<C>,
providers: SkillProviders,
config_from_host: impl Fn(&C) -> SkillsExtensionConfig + Send + Sync + 'static,
)
作用:用指定的技能来源安装技能扩展。测试或特殊场景可以传入自定义 providers,而不是使用默认技能来源。
数据流:输入是扩展注册表、技能来源集合 providers,以及配置转换函数。它创建一个 SkillsExtension,保存 providers、事件发送器和配置转换函数。然后把同一个扩展对象分别注册成线程生命周期贡献者、配置贡献者、提示贡献者、每轮输入贡献者和工具贡献者。结果是注册表被填好,运行时会在不同阶段回调这个扩展。
调用关系:它被 install 调用,也可以被需要自定义 providers 的代码直接调用。它是把 SkillsExtension 接到扩展框架各个插槽上的地方:线程启动走 on_thread_start,配置变化走 on_config_changed,每轮输入走 contribute,工具收集走 tools。
调用图:调用 6 个内部函数(config_contributor, event_sink, prompt_contributor, thread_lifecycle_contributor, tool_contributor, turn_input_contributor);被 1 处调用(install);外部调用 1 个(new)。