阅读契约。跟踪同一条请求中的技能说明、插件资源、工具规格和子线程,辨认它们各自的读取与执行入口。 本文核实于 2026-09-20,依据公开源码 固定公开源码快照;不推断未公开的服务实现。

先想一个具体场景:你让 Codex 写一篇文章,并点名使用 $article 和 $image;同时本地装着 Browser、GitHub、Documents 这些 plugin; 这轮可能还会出现几十个 MCP 工具,甚至需要开一个 subagent 专门审稿。 如果把这些都理解成“给模型多塞一些东西”,源码很快会读乱。

Codex 的处理方式更像一个分拣台。有的能力进入 prompt,有的能力进入工具列表, 有的能力只提供安装或发现入口,有的能力直接变成一条新的线程。 它们最终都服务同一轮工作,但进入 runtime 的形状完全不同。

先记住四个具体动作:skills 把说明加入上下文,plugins 声明一组资源, MCP 把工具规格加入当前采样步骤的工具菜单,subagents 创建由父任务管理的子线程。

取材范围。 本文只描述 openai/codex 公开源码里可验证的 skill 发现与注入、plugin manifest 与 load outcome、MCP tool exposure、tool_search、dynamic tools、multi-agent tool list 和 AgentControl 行为。文中提到 app、plugin、subagent 时,指这些公开代码里的运行时结构。

第七篇按六个问题拆:

  1. 为什么要先区分“上下文能力”“工具能力”和“线程能力”?
  2. TurnContext 里哪些字段承接扩展输入?
  3. skill 从“可用目录”到“完整 SKILL.md 注入”经历了什么?
  4. plugin 作为能力包,怎样同时贡献 skills、MCP、apps 和 hooks?
  5. MCP 工具什么时候直接给模型,什么时候交给 tool_search?
  6. subagent 为什么会创建子线程,而不是只返回一段工具输出?

一、四种扩展走四条不同入口

读扩展代码最容易卡住的地方,是几个词都会被泛泛地叫成“能力”。但在 Codex 里, 它们交给模型或运行时的数据不同,也由不同模块处理。

能力形状 用户看到的入口 进入 runtime 的方式 对应源码入口
工作方法 $article、$image、显式 skill 选择。 先展示 skill catalog;被点名后读取所选技能的 main prompt,作为 contextual user fragment 注入。 HostSkillsSnapshot、load_skill_prompts。
能力包 Browser、GitHub、Documents 这类 plugin。 manifest 声明 skills、MCP servers、apps、hooks;加载后产出 effective roots 和 capability summaries。 PluginManifest、PluginLoadOutcome。
可调用工具 MCP server、app connector、dynamic tool。 转换成 model-visible tool spec;exposure 配置与工具模式允许时变成 deferred tools,由 tool_search 再发现。 apply_mcp_tool_exposure_policy、ToolSearchHandler、ToolRouter。
并行工作单元 spawn_agent、send_message、wait_agent。 创建或恢复子线程,复制本轮运行配置和权限设置,通过 inter-agent communication 回到父线程。 AgentControl、multi-agent handlers、SessionSource::SubAgent。

这张表先放在脑子里,后面看源码就不会混:skill 解决“这件事该怎么做”,plugin 解决“这些能力属于哪个包”, MCP 解决“模型能调用什么”,subagent 解决“是否需要另一条上下文和执行轨道”。

1.1 用同一条请求串起四种入口

可以拿一条真实工作流做代表性单元:用户说“用 $article 写一篇 Codex memory, 必要时用 Browser 查页面,再开一个 subagent 审稿”。这句话不会被 Codex 合并成一个万能能力; turn 开始后,它会被拆成几类有不同归属的材料。

阶段 这时的单元 谁能看或改 后续变化
原始输入 用户文本和显式 mention。 输入解析层识别 $article、Browser、subagent 需求。 只决定本轮要准备哪些入口,不直接产生工具结果。
能力快照 TurnContext 里的 skills_snapshot()、extension_data、dynamic_tools。 runtime 在 turn 边界内维护;后续注入和工具规划读取它。 保留技能来源;工具菜单由当前 step 捕获。
上下文注入 $article 展开的 SKILL.md。 模型可见,作为工作方法约束。 影响写作方式,但它本身不是可调用函数。
工具暴露 Browser 或其他 MCP/app tool spec。 模型只能看到 direct tools 或 tool_search 找回的 deferred tools。 真正调用时仍由 tool router 和权限层接管。
子线程 spawn_agent 创建的审稿任务。 父线程发起,子线程有自己的上下文和事件流。 结果通过 inter-agent communication 回到父线程。

这段流程比术语表更重要。一次用户请求里可以同时出现 skill、plugin、MCP 和 subagent, 但它们不会在同一层生效。先分清“进入上下文”“提供工具”“启动另一条线程”,再沿表中的函数读源码。

二、TurnContext 先把入口留出来

一次请求的扩展材料分成两层。TurnContext 保留 dynamic_tools、extension_data、多 agent 设置和 HostSkillsSnapshot;每次 sampling 的 StepContext 则捕获当时的模型、环境、MCP binding、设置和工具 router。

因此“这次 turn 有哪些技能”与“这一步模型看见哪些工具”不能混为同一个固定列表。skills snapshot 用于读取已选说明;MCP exposure 与 extension tool contributors 参与每一步的工具规划。已经发出的工具请求保留它所属步骤的上下文,后续菜单变化不会改写这个事实。

三、skills:先给目录,再按需展开全文

目录与正文仍然分开处理。AvailableSkillsInstructions 以 developer fragment 提供技能目录和用法,SkillInstructions 以 user fragment 提供被选中的技能内容。两者的 role、content kind 和标记不同。

读取入口已经移到 skills extension 与 host snapshot。build_skills_and_plugins 收集显式 mention,调用 skills_snapshot.load_skill_prompts;skills extension 的 contributor 还会按来源整理目录,从 provider 读取所选 main prompt。它可以处理 host、executor、bundled 与 orchestrator 等来源,不能假定每个技能都是本机一个可直接读取的文件。

阶段进入上下文的材料边界
可用目录名称、说明、来源和读取入口。按 metadata budget 控制长度,不预装全部正文。
显式选择从对应 provider 读取 main prompt。读取失败产生 warning;过长正文会截断并提示。
技能正文SkillInstructions user fragment。资源型技能还带 authority、package、main_resource,后续读取必须沿相同来源。

例如同样点名 $article,文件系统技能可以对应 SKILL.md;orchestrator 提供的技能则可能需要资源读取工具。模型最终获得工作说明,但说明的获取方式与工具执行权限仍然是两件事。

四、plugins:把一组能力打成包

plugin 解决的是另一个问题:一组能力往往不是单独一个 SKILL.md。例如 Browser plugin 可能同时带 skill、脚本、MCP server、app connector;GitHub plugin 可能同时提供 PR triage、 CI 排查和发布流程。源码里 PluginManifest.paths 把这些入口分开声明: skills、mcp_servers、apps、hooks。

pub struct PluginManifestPaths<Resource> {
    pub skills: Vec<Resource>,
    pub onboarding_skill: Option<Resource>,
    pub mcp_servers: Option<PluginManifestMcpServers<Resource>>,
    pub apps: Option<Resource>,
    pub hooks: Option<PluginManifestHooks<Resource>>,
}

这几行很适合作为插件心智模型:plugin manifest 只是把一组资源挂到同一个包名下面。 它没有把 skill、MCP、app 和 hook 合成一种新能力;相反,它保留了四条入口的差异, 后续加载器再分别把它们送到上下文、工具、app connector 或对应的 hook 执行点。

加载后,PluginLoadOutcome 会给出几类 effective 结果: skill roots、plugin skill roots、MCP servers、apps、hook sources 和 capability summaries。 capability summary 只保留对模型有用的短描述;真正能被调用的能力,仍然要走它各自的通道。

这里有个很容易忽略的区别:plugin 自己不是一个万能工具。源码给模型的 plugin 使用说明里明确强调, plugin 是 skills、MCP servers 和 apps 的本地 bundle。要解决任务,仍然使用它贡献出来的 skill、MCP 工具或 app tool。

build_skills_and_plugins 会先从当前配置加载 plugins,再解析显式 plugin mention。 如果用户点名某个 plugin,Codex 会拿到当前可用的 MCP/app inventory,用 build_plugin_injections 生成本轮 guidance。这样模型知道“这个包现在有哪些可用能力”, 但调用时还是回到具体工具或 skill。

五、MCP 与 tool_search:工具太多时先发现,再展开

MCP 工具注册后,apply_mcp_tool_exposure_policy 决定它出现在哪些工具面。源码把 server 与 app connector 的 omit_tools_from 合并,从 ToolExposures::ALL 中减去禁用面;direct_only_tool_namespaces 还会禁止对应 namespace 的 deferred 与 code-mode 暴露。

如果 search tool 已启用、该工具允许 deferred,并且当前 tool mode 允许该组合,运行时去掉 direct,保留 deferred;否则去掉 deferred。这里不再以“达到 100 个工具”作为分界。 实际结果可以是 Direct、Deferred、CodeModeOnly 或 Hidden 等形态,由配置与工具模式共同决定。

形状示例(省略不相关配置):
MCP 工具 + search enabled + deferred allowed -> deferred
同一工具 + omit_tools_from 含 deferred       -> direct(若允许)
所有 exposure 都被排除                       -> hidden

finalize_tool_router 只有在存在带搜索信息的 deferred 工具且 search 启用时,才注册 tool_search。这保证搜索确实有可发现的目标;搜索返回 schema 以后,真正调用仍然经过工具 router 与对应审批,发现能力不会绕过执行限制。

六、subagents:需要另一条线程时,才开并行轨道

多 agent 的入口看上去也像工具:spawn_agent、send_message、 followup_task、wait_agent、list_agents 都是在工具规划里暴露的。 但它们执行后的结果不止是一段工具返回值;运行时会创建一条由 AgentControl 管理的子线程。

handle_spawn_agent 解析 task_name、agent_type、model、reasoning_effort 和 fork_turns。它使用本次 step_context 构造 child config,再复制允许继承的权限、环境和运行设置;模型与 role 覆盖仍要通过对应校验。

真正生成子线程时,AgentControl.spawn_agent_with_metadata 会使用 SessionSource::SubAgent 和 ThreadSpawn metadata。 如果选择 fork history,它会先读取父线程的历史,再按 fork_turns 决定继承全部、 最近 N 轮,或者不继承历史。子 agent 结束后,completion watcher 会把结果作为 InterAgentCommunication 或用户消息送回父线程。

subagent 的重点在于多一条受控线程。它有自己的 context、status、 history 和 completion notification;但它的权限与环境来自发起调用的父 step 的运行时配置, 不会变成脱离主线程监管的后台过程。

七、把四条入口接回同一条主线

到这里可以把第七篇合回前六篇的路线图:用户输入进入 turn;TurnContext 保留扩展输入,StepContext 固定当前采样的设置、环境与工具菜单; skill 和 plugin guidance 先形成上下文注入;MCP、dynamic tools 和 collaboration tools 进入工具规划;工具执行、agent 活动和普通模型输出再继续变成事件,交给客户端转换代码。

你在使用时看到 源码里可以这样定位 接到前文哪一层
“用 $article 写一篇” 先看 collect_explicit_skill_mentions,再看 SkillInstructions 注入。 第二篇的上下文管理。
“启用 Browser / GitHub plugin” 先看 manifest 和 load outcome,再看它贡献的 skill roots、MCP、apps。 本篇的 plugin 打包方式,第四篇的工具系统。
“工具很多,先搜索工具” 看 apply_mcp_tool_exposure_policy 和 ToolSearchHandler。 第四篇的 tool spec 与 tool router。
“开一个 subagent 审稿” 看 multi-agent handlers、AgentControl、SessionSource::SubAgent。 第三篇的事件流,第六篇的客户端事件转换。

这样看,Codex 的扩展机制并不是给 runtime 外面套一层万能插件系统。它更像给一次 turn 增加几种受控入口:能写进 prompt 的写进 prompt,能变成工具的变成工具, 需要独立上下文的开成子线程,所有结果继续回到事件和记录。

八、读完第七篇,回头看一次完整请求

如果把前七篇连起来,一次普通请求已经可以这样读:入口把用户意图变成 typed operation; session 建立 turn;context manager 整理模型能看到的材料;tool spec 和 tool router 决定模型能调用什么;permission 和 sandbox 管住副作用;event stream 把运行事实投给客户端; extensions 和 multi-agent 则回答“额外能力怎样加入这条主线”。

第八篇继续顺着 hooks 和生命周期走:用户 prompt 提交前后、工具调用前后、 turn 停止时,Codex 还允许哪些代码在边界处补充上下文或拦截副作用。到那一层, plugin 里的 hooks 就会从 manifest 字段变成真正的执行路径。

参考源码