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

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

这篇可以抓住一句话:扩展会以不同形状投到这一次 turn 里,继续受 turn runtime 管理。 skills 进入上下文,plugins 提供能力根目录和包边界, MCP 进入工具暴露计划,subagents 进入线程控制面。

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

第七篇按六个问题拆:

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

一、先把四种能力形状分开

读扩展代码最容易卡住的地方,是几个词都会被泛泛地叫成“能力”。但在 Codex 里, 它们落到模型面前的形状不一样,也由不同 owner 管。

能力形状 用户看到的入口 进入 runtime 的方式 源码里要跟的 owner
工作方法 $article$image、显式 skill 选择。 先展示 skill catalog;被点名后读取完整 SKILL.md,作为 contextual user fragment 注入。 SkillsManagerbuild_skill_injections
能力包 Browser、GitHub、Documents 这类 plugin。 manifest 声明 skills、MCP servers、apps、hooks;加载后产出 effective roots 和 capability summaries。 PluginManifestPluginLoadOutcome
可调用工具 MCP server、app connector、dynamic tool。 转换成 model-visible tool spec;数量或配置触发时变成 deferred tools,由 tool_search 再发现。 build_mcp_tool_exposureToolSearchHandlerToolRouter
并行工作单元 spawn_agentsend_messagewait_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 里的 turn_skillsextension_datadynamic_tools runtime 在 turn 边界内维护;后续注入和工具规划读取它。 把“当前有哪些能力”固定成本轮事实。
上下文注入 $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, 但它们不会在同一层生效。先分清“进入上下文”“暴露工具”“启动另一条线程”,后面的源码就能顺着 owner 读。

二、TurnContext 先把入口留出来

第二篇讲 TurnContext 时,我们重点看上下文、权限和 token budget。第七篇要注意另一组字段: dynamic_toolsextension_dataturn_skillsmulti_agent_versionparent_thread_idsession_source。 它们让一次 turn 有地方承接外部能力。

make_turn_context 会把当前 turn 的配置、模型信息、权限 profile、动态工具、 extension data 和 skills outcome 放进同一个结构里。这里有两个细节很关键。 第一,HostLoadedSkills 被插入 extension_data,让 host extension 也能看到已加载的 skill 集合。第二,dynamic_tools 来自 session configuration, 后面工具规划阶段会把它们追加到工具 surface。

所以扩展入口并不是散落在各处临时读取。Codex 会在 turn 开始时先形成一个能力快照: 当前配置允许什么、已加载哪些 skill、有哪些 extension data、是否启用多 agent、 本轮权限边界是什么。后续注入和工具规划都围绕这份快照展开。

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

skill 的第一层很轻:Codex 会把可用 skill 的列表和使用规则放进 developer 上下文。 AvailableSkillsInstructions 只渲染 catalog 级别的信息,告诉模型有哪些 skill、 什么时候应该使用、怎么读取附加说明。

真正重的部分发生在用户点名之后。build_skills_and_plugins 从本轮 user input 里收集显式 skill mention,然后调用 build_skill_injections。这个函数会按 skill metadata 找到对应的文件系统,读取完整 SKILL.md,再把内容包装成 SkillInstructions。换句话说,完整技能说明是本轮被需要时才进入 prompt。

阶段 放进模型上下文的内容 为什么这样做
可用列表 skill name、description、path、触发规则。 让模型知道“可以选什么”,但不把所有长说明一次塞进上下文。
显式点名 完整 SKILL.md 内容。 用户或结构化输入已经确认需要它,这时才值得付出上下文成本。
注入形状 ContextualUserFragment 里的 SkillInstructions skill 是工作方法和约束,进入的是上下文层,不是工具调用层。

这也解释了为什么一个 skill 写得好不好,会直接影响文章或图的质量。runtime 只负责把它放到正确位置, 至于模型读到后如何执行,取决于 skill 里面是否把工作流、边界、验收方式写清楚。

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

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

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

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

加载后,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 工具进入模型前的一层选择: build_mcp_tool_exposure 会先筛出 model-visible 的 MCP tools, 再根据配置和数量决定直接暴露,还是作为 deferred tools 交给搜索工具。

代码里有一个很具体的阈值:DIRECT_MCP_TOOL_EXPOSURE_THRESHOLD 是 100。 如果启用了 search tool,并且配置要求始终 defer,或者待暴露工具数量达到阈值, Codex 就不把这一大批工具全塞到 direct tool list;它会提供 tool_search, 让模型按 query 找到本轮真正需要的工具规格。

工具来源 进入模型前的处理 运行时保护
普通 MCP tool 筛 model-visible,数量合适时直接进入 tool spec。 调用时仍走 McpHandler 和 MCP approval metadata。
大量 MCP tool 作为 deferred tool 建索引,先通过 tool_search 返回匹配工具。 减少上下文噪音,调用路径没有绕开工具审批。
app connector tool 需要 connector 在本轮允许并启用,才会进入可见集合。 tool metadata 里保留 connector 和 plugin 相关信息。
dynamic tool TurnContext.dynamic_tools 追加到工具规划。 仍然变成标准 handler,由 ToolRouter 分发。

这一层的价值很实际:工具多起来以后,问题不只是“能不能调用”,还有“模型面前应该先出现多少”。 direct exposure 适合少量稳定工具;deferred exposure 适合大工具库;tool_search 像一个按需打开的目录。

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

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

handle_spawn_agent 会解析 task_nameagent_typemodelreasoning_effortfork_turns 等参数。 之后它会从父 turn 构建子 agent 配置:复制必要的 runtime 字段,应用模型或 role 覆盖, 再把本轮权限 profile、sandbox、cwd、environment selection 等运行时边界带过去。

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

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

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

到这里可以把第七篇合回前六篇的路线图:用户输入进入 turn;TurnContext 固定本轮快照; 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 包边界,第四篇的工具系统。
“工具很多,先搜索工具” build_mcp_tool_exposureToolSearchHandler 第四篇的 tool spec 与 tool router。
“开一个 subagent 审稿” 看 multi-agent handlers、AgentControlSessionSource::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 字段变成真正的执行路径。

参考源码