阅读契约。跟踪同一条请求中的技能说明、插件资源、工具规格和子线程,辨认它们各自的读取与执行入口。 本文核实于 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 时,指这些公开代码里的运行时结构。
第七篇按六个问题拆:
- 为什么要先区分“上下文能力”“工具能力”和“线程能力”?
TurnContext里哪些字段承接扩展输入?- skill 从“可用目录”到“完整 SKILL.md 注入”经历了什么?
- plugin 作为能力包,怎样同时贡献 skills、MCP、apps 和 hooks?
- MCP 工具什么时候直接给模型,什么时候交给
tool_search? - 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 字段变成真正的执行路径。
参考源码
- openai/codex · 5c5308fc9a9e
- StepContext
- HostSkillsSnapshot / environments
- build_skills_and_plugins
- skills extension / selection / prompt reading
- AvailableSkillsInstructions / SkillInstructions
- PluginManifestPaths
- effective plugin resources
- MCP exposure policy
- deferred search registration
- subagent spawn / fork mode
- AgentControl