第十一章 可扩展性:如何让 harness 长出新能力,而不失去控制

第十章讨论了 agent 如何跨进程、跨版本找回过去。恢复要求边界稳定,而扩展恰恰会不断引入新的指令、工具、流程和状态。 这形成了一组天然的张力:一个 harness 如果完全封闭,很快会跟不上真实业务;如果允许扩展随意进入 LOOP,又会失去安全、确定性和可恢复性。 Codex 的答案不是设计一个无所不能的“插件接口”,而是开放一组深浅不同、权力不同的扩展点:skills 教模型怎样做事,MCP 把外部服务接成工具,hooks 在生命周期边界介入,dynamic tools 把前端能力交给模型,plugin 负责打包和分发,Extension API 则让受信任的第一方组件参与更深的运行时生命周期。 本章不只回答“有哪些扩展方式”,更要回答四个问题:扩展改变什么、代码在哪里执行、何时对 LOOP 生效、谁有权限制它。


11.1 可扩展性不是“什么都能插”

很多系统把“可扩展”简单理解成:提供一个 plugin 目录,让第三方代码加载进主进程。这个方案容易实现,也最容易制造长期问题:

  • plugin 可以改共享内存,任何 bug 都可能拖垮整个 Session;
  • plugin 可以在采样中途修改工具清单,模型看到的 spec 与真正执行的 runtime 不再一致;
  • plugin 状态随意进入上下文,token 成本和提示注入风险失去边界;
  • plugin 更新以后,旧 rollout 不知道该按哪个版本恢复;
  • plugin 代码和 harness 共享全部权限,沙箱与审批可能被绕到背后。

Codex 采用的是相反的思路:

不允许扩展任意介入 LOOP,而只开放一组输入、输出、触发时机和权限边界都明确的扩展点。

第五章说过,LOOP 本身应该尽量“笨”:读取历史、调用模型、执行工具、回灌结果、判断是否继续。复杂能力都挂在边界上。可扩展性就是这条原则的工程化:扩展不能在循环里随便插一段代码,只能通过既定契约贡献某种东西。

常见贡献大致分为六类:

扩展想改变什么 对应机制 典型例子
agent 的工作方法 skills 教模型按团队规范排查线上故障
agent 能调用的外部能力 MCP 接入日历、数据库、代码平台
生命周期中的检查与自动化 hooks 工具执行前审查,停止前检查测试
前端程序独有的能力 dynamic tools 读取 IDE 当前选区、操作编辑器标签页
一组能力的安装与分发 plugin 把 skill、MCP、hooks 和界面信息打成一个包
harness 内部的深层行为 Extension API 贡献上下文、工具、生命周期处理器和 scoped state

这里已经能看到一个重要区别:plugin 不是最深的扩展层,甚至不是一种新的执行机制。 它主要是一个打包和分发单元;真正干活的仍然是 skill、MCP、hook 等已有机制。能深入参与 thread、turn、step 生命周期的,是由前端在构建运行时时注册的 Extension API。

把所有东西都叫“插件”,会掩盖三个决定安全边界的事实:

  1. 有的扩展只是文本,有的会执行代码;
  2. 有的代码运行在 harness 进程里,有的运行在外部服务或前端里;
  3. 有的只给模型建议,有的可以阻止工具或否决停止。

所以讨论可扩展性,第一步不是问“支不支持 plugin”,而是问:这个扩展拿到了哪一种权力?


11.2 一张扩展地图:深度、执行位置与控制权

可以把 Codex 的扩展机制画成从外到内的同心层。越靠内,越接近 LOOP,能力越强,信任要求也越高。

flowchart TB
    subgraph DIST["分发与配置层"]
        PL["plugin / marketplace<br/>打包、安装、版本与来源"]
        CFG["配置分层 / requirements<br/>选择、覆盖与硬约束"]
    end

    subgraph DECL["声明式扩展层"]
        SK["skills<br/>贡献 instruction 与资源"]
        HK["hooks<br/>在生命周期边界介入"]
        MCP["MCP<br/>贡献外部 tools / resources"]
        DT["dynamic tools<br/>贡献前端程序能力"]
    end

    subgraph CORE["运行时装配层"]
        EXT["Extension API<br/>贡献 context / tool / lifecycle / state"]
        ASM["统一装配与生命周期扩展点"]
    end

    subgraph LOOP["稳定内核"]
        L["LOOP<br/>历史 → 采样 → 行动 → 回灌"]
    end

    PL --> SK
    PL --> HK
    PL --> MCP
    CFG --> SK
    CFG --> HK
    CFG --> MCP
    CFG --> DT
    SK --> ASM
    HK --> ASM
    MCP --> ASM
    DT --> ASM
    EXT --> ASM
    ASM --> L

这张图表达的是集成深度,不是调用顺序。每一层解决的问题不同:

  • 分发与配置层回答“装什么、从哪里来、是否启用、上限是什么”;
  • 声明式扩展层回答“向系统贡献哪类能力”;
  • Extension API让受信任组件直接贡献强类型的生命周期行为;
  • 两类能力最终汇入同一套运行时装配、工具和上下文边界;
  • LOOP只消费最终结果,不需要知道能力来自内置实现、plugin 还是前端。

再换一个角度,按代码真正运行的位置看:

机制 主要内容 执行位置 能否直接改变外部世界
skill Markdown instruction、脚本与参考资料 instruction 由模型读取;脚本经工具系统执行 instruction 不能,脚本可以
MCP 远端或本地 server 提供的工具和资源 MCP server 进程或网络服务 可以
command hook 一条受配置约束的命令 harness 拉起的子进程 可以
MCP hook 对某个 MCP tool 的调用 MCP server 可以
dynamic tool spec + 前端实现 前端程序 可以
plugin manifest 与资源集合 自身通常不执行,所含能力各自执行 取决于所含能力
Extension API 强类型 contributor 与 harness 一起构建的受信任运行时 可以深度参与内核

这张表提醒我们:扩展的权限边界必须跟着执行位置走。 harness 的文件沙箱能约束自己启动的命令,却不能自动约束一个远端 MCP server,也不能约束 IDE 前端收到 dynamic tool 请求后做了什么。第八章的安全策略不是一张覆盖全世界的网;每个执行域都要有自己的鉴权、审批与审计。


11.3 三个阶段:发现、激活、冻结

可扩展系统最容易出现的一类 bug,是把“系统知道某项能力存在”“这次会话允许使用它”“模型这一轮可以调用它”混成同一件事。

Codex 把它们分成三个阶段:

11.3.1 Discovery:系统知道“可能有什么”

discovery 负责扫描和读取候选能力:

  • 在多个目录中发现 skills;
  • 从 marketplace 或本地目录发现 plugins;
  • 从配置中发现 MCP servers 和 hooks;
  • 从前端的 thread/start 请求中接收 dynamic tools;
  • 从运行时构建结果中拿到 Extension contributors。

此时只是建立 catalog。一个 skill 被扫描到,不代表已进入模型上下文;一个 plugin 出现在 marketplace,不代表已安装;一个 MCP server 有配置,不代表连接已建立。

discovery 必须有硬边界。目录递归深度、扫描目录数、条目数、单个资源大小、描述长度都要封顶。否则“扫描扩展”本身就可能变成无界 I/O,或让几千个 skill 的描述挤满上下文。

11.3.2 Activation:这次运行“准备用什么”

activation 把 catalog 与当前环境结合:

  • plugin 是否已安装、是否启用;
  • skill 是否被 policy 禁用;
  • project 配置是否因目录不可信而被忽略;
  • MCP server 是否允许启动、认证是否完成;
  • hook 是否已被用户信任;
  • 当前模型是否支持所需的工具形态;
  • 当前 thread、agent 身份和权限是否允许这项能力。

activation 是配置、信任和能力协商的交汇点。它回答的不是“存在吗”,而是“在这里可以用吗”。

11.3.3 Snapshot:这一轮模型“实际看到了什么”

activation 仍然不是最终执行边界。真正进入一次采样之前,harness 会在 step 边界冻结:

  • 本轮模型和推理参数;
  • 本轮可见工具 spec;
  • 工具名到 runtime 的路由;
  • MCP client、tool metadata、timeout 与 catalog revision;
  • 世界状态和扩展贡献的上下文。
flowchart LR
    D["Discovery<br/>候选 catalog"] --> A["Activation<br/>配置 + 信任 + 能力"]
    A --> S["Step Snapshot<br/>本轮冻结视图"]
    S --> M["模型采样"]
    M --> C["工具调用"]
    C -->|"只按冻结绑定执行"| R["结果回灌"]
    R --> N["下一个 step<br/>重新观察变化"]

这个三段式解决了一个关键一致性问题:假设模型刚看到 calendar.create_event,MCP server 随即刷新目录并把它换成了另一个版本。调用时如果去查“最新目录”,模型依据旧 spec 生成的参数可能被新 runtime 接收,后果不可预测。

Codex 的做法是把一次调用绑定到模型当时看到的 client、metadata、timeout 和 catalog revision。目录若在调用准备后发生变化,旧调用会被明确拒绝,而不是悄悄交给新版本执行。变化等到下一个 step 再进入快照。

这和第五章“变化只发生在边界”、第六章“spec 与 runtime 必须一致”是同一条原则:

支持动态刷新,不等于允许采样中的世界动态突变。


11.4 Skills:扩展的不是手,而是做事方法

skill 最容易被误解成“一个装着脚本的 plugin”。实际上,它首先是一份写给 agent 的方法说明:什么情况下使用、应该按什么步骤做、哪些资料按需读取、哪些脚本可以辅助执行。

一个典型 skill 可以包含:

  • SKILL.md:名称、描述、触发条件和完整工作流;
  • references/:规范、API 说明、领域知识;
  • scripts/:可复用的确定性操作;
  • assets/:模板和产物素材。

它扩展的核心是模型的 procedural knowledge(程序性知识):不是让模型多一只手,而是教它现有的手该按什么顺序使用。

11.4.1 Progressive disclosure:先给目录,再给正文

如果系统有一百个 skills,把一百份完整 SKILL.md 都塞进每次请求,第四章的上下文窗口会立刻被吃掉。Codex 使用 progressive disclosure:

  1. 初始只注入 skill 的名称、简短描述和 locator;
  2. 模型或 harness 判断某个 skill 相关时,再读取完整 SKILL.md
  3. skill 引用的 reference、script、asset 继续按需读取。
flowchart LR
    C["Skill catalog<br/>名称 + 描述 + locator"] -->|"判断相关"| M["读取 SKILL.md"]
    M -->|"工作流需要"| R["读取 reference"]
    M -->|"需要确定性操作"| S["运行 script"]
    M -->|"生成产物"| A["使用 asset"]

这和第六章 deferred tools 是同一种上下文经济学:

  • 先用一小段“广告”告诉模型能力存在;
  • 只有真正需要时才支付完整 token 成本;
  • 对目录、描述和单个资源设置硬上限。

区别在于,deferred tool 最终给模型一份可调用 spec;skill 最终给模型一份工作方法。一个解决“能做什么”,一个解决“应该怎么做”。

11.4.2 Skill 来源不是简单覆盖关系

skills 可以来自 repo、user、system、admin、plugin、executor、orchestrator 或运行时附加目录。来源越多,同名冲突就越常见。

一种危险做法是“高优先级目录静默覆盖低优先级目录”。这会让 $deploy 在不同机器上指向不同内容,用户却看不出差异。Codex 更强调来源和 locator:同名 skill 只有在能唯一确定时才适合按名字调用;有歧义时,应保留来源信息并要求明确选择。

例如,用户目录里有一个名为 deploy 的通用 skill,内容是“构建镜像并部署到 Kubernetes”;后来安装的公司发布 plugin 也带了一个 deploy skill,要求“创建发布单、等待审批,再通过内部平台上线”。两者名字相同,但流程、权限和副作用完全不同。

如果采用静默覆盖,用户输入 $deploy 后,实际执行哪套流程将取决于目录优先级:换台机器、进入另一个项目或调整 plugin 顺序,都可能让行为悄悄改变。保留来源和 locator 后,catalog 可以把它们表示为两个不同候选项:

deploy(来源:用户目录)
deploy(来源:company-release plugin)

此时 $deploy 不能被自动解释成其中任意一个。系统不应猜测,而应保留两个候选项,由调用方通过具体 path/locator 选定目标;选定后再读取对应的 SKILL.md

以本地 skill 为例,前端通常先通过 skills/list 取得每个候选项的真实路径,再在 turn/start 中同时发送用户可见文本和结构化 skill item:

{
  "method": "turn/start",
  "id": 33,
  "params": {
    "threadId": "thr_123",
    "input": [
      {
        "type": "text",
        "text": "$deploy 发布当前版本"
      },
      {
        "type": "skill",
        "name": "deploy",
        "path": "/resolved/company-release/skills/deploy/SKILL.md"
      }
    ]
  }
}

这里的 text 表达用户意图,skill item 则给 harness 一个不可歧义的选择。当前 app-server 协议中的字段名是 path;locator 是 catalog 中更宽泛的概念,还可以表示由 executor 或 orchestrator 管理的 package。harness 会优先按结构化 item 的 path 匹配已加载且启用的 skill,而不是仅凭 name 猜测。若 path 无效或对应 skill 已禁用,本次结构化选择会失效,也不会再偷偷退回同名 $deploy

名称负责让人记住能力,path/locator 才负责精确定位能力。

这里的原则与工具命名空间一致:扩展名称不只是显示文本,也是路由地址。地址有歧义,就不能假装它唯一。

11.4.3 Instruction 仍然是不可信输入

skill 是 instruction,不是系统权限。它可以建议模型运行某条命令,却不能绕过工具系统的路由、hook、审批和沙箱;skill 里的脚本也必须通过正常执行入口运行。

这点非常重要。若“安装 skill”就等于“允许其中脚本在主进程任意执行”,skill 会从知识包变成远程代码注入。Codex 把“读懂一个方法”和“执行一个动作”分开:前者进入上下文,后者仍要走第六章的六道关卡。

skills 因此是一种低耦合、高影响的扩展:它几乎不碰内核,却能显著改变 agent 行为。代价是效果依赖模型理解,不能像强类型程序一样保证每一步都执行。需要确定性约束时,应把规则放进 hook、policy 或工具 runtime,而不是只在 skill 里写一句“必须”。


11.5 MCP:把外部系统接到统一工具面

如果 skill 教 agent“怎样办理请假”,MCP 则真正提供“查询余额、创建审批、读取状态”的外部能力。

MCP 的价值不只是统一了工具调用格式,还统一了外部服务接入时的一组工程问题:

  • transport:本地 stdio 或远端 streamable HTTP;
  • authentication:OAuth、bearer token 或产品身份;
  • lifecycle:启动、连接复用、断线与重连;
  • discovery:tools、resources 与 templates;
  • control:启用/禁用、超时、tool allowlist/denylist;
  • interaction:elicitation,即外部服务反过来向用户索取信息;
  • provenance:能力来自哪个 server、哪个 plugin、哪个执行环境。

11.5.1 MCP server 不是“一个大工具”

一个 MCP server 更像一个能力域。它可以提供几十个 tools,也可以暴露 resources。harness 需要在几个粒度上分别做控制:

粒度 可以控制什么
server 是否启用、是否 required、如何启动、如何认证
catalog 哪些 tools 对当前环境可见
tool timeout、审批策略、是否允许并发
model exposure direct、deferred、namespace 或隐藏
call binding 本次调用绑定的 client 与 catalog revision

这解释了为什么“已连接 MCP”不等于“把全部工具塞给模型”。server 可能已启动,runtime 也已注册,但低频工具仍然 deferred,只有 tool_search 命中后才展开完整 spec。

11.5.2 Required 的含义是失败要响亮

有些 MCP 只是锦上添花:连接失败时少几个工具,任务仍可继续。有些则是业务前提,例如企业工单系统,没有它就不应假装能完成任务。

required 的价值不在于多重试几次,而在于改变失败语义:

  • optional server 启动失败,可以降级并告知模型能力不可用;
  • required server 启动失败,应阻止相关 Session 正常开始。

这是第一章“失败响亮”的扩展版。系统必须区分“能力暂时少了一项”和“运行前提根本不成立”,否则 agent 会在缺少关键能力时用猜测填空。

11.5.3 外部能力需要独立信任边界

MCP tool 的副作用发生在 server 一侧。即使 harness 自己处于只读沙箱,一个远端数据库工具仍可能执行写操作。因此 MCP 的安全不能只依赖本地沙箱,至少需要:

  • server 身份与 URL/command 约束;
  • tool allowlist/denylist;
  • per-tool approval;
  • 凭证最小权限;
  • 返回内容按外部不可信上下文处理;
  • 调用和结果的独立审计。

第八章说过,安全的两条轴是“能不能做”和“要不要问”。MCP 把这两条轴延伸到了进程之外:本地 harness 负责是否把请求发出去,远端服务仍要负责收到请求后允许做到什么。


11.6 Hooks:在生命周期边界上加入规则

skill 给模型建议,hook 则在确定的生命周期点运行。它适合处理那些不能只靠模型“记得做”的事情:

  • 每次命令执行前做合规检查;
  • 工具结束后记录审计信息;
  • 压缩前保存额外状态;
  • Session 开始时注入环境说明;
  • 模型准备停止时检查测试是否完成;
  • 子 agent 启动或结束时更新外部任务状态。

Codex 提供的 hook 事件覆盖了主要生命周期边界:

生命周期点 hook 典型用途
Session 开始/结束 SessionStart / SessionEnd 初始化、清理、记录
用户提交输入 UserPromptSubmit 补充上下文、输入检查
工具执行前后 PreToolUse / PostToolUse 拦截、改写、审计
请求权限 PermissionRequest 自动化审批或附加策略
压缩前后 PreCompact / PostCompact 保存或恢复扩展状态
子 agent 开始/停止 SubagentStart / SubagentStop 编排与约束
根 agent 准备停止 Stop 完成条件检查

11.6.1 Hook 是流程控制,不是任意插桩

每个 hook 都有固定输入和固定输出。以 PreToolUse 为例,它可以放行、拦截或按契约修改参数;Stop 可以放行、否决并提供续跑指令,或者要求收尾。它不能取得整个 Session 的可变引用,然后随意改历史。

这就是受控扩展点的价值:能力虽然有限,但影响范围可以推理、可以测试、可以持久化。

同一个事件的多个同步 handlers 可以并发运行,最后统一归并结果。这样独立的审计、策略和补充上下文不必串行等待。但归并必须有明确规则:冲突时谁优先、多个否决如何合并、多个参数修改能否共存,不能依赖异步完成顺序。

11.6.2 Sync 与 async 的权力不同

hook 可以同步等待,也可以作为后台任务运行:

  • sync hook 位于控制路径上,调用方会等它完成,因此可以阻止、修改或影响本次流程;
  • async hook 只适合通知、上报和低耦合自动化,不能在后台任务结束几秒后再“撤销”已经发生的工具调用。

这条限制看似保守,实际上是在保护因果关系。一个 hook 若要影响决策,就必须在决策发生前给出结果;错过边界后,它只能记录事实,不能改写过去。

11.6.3 信任必须由更高权限的层授予

command hook 会执行代码,MCP hook 会调用外部服务,都比纯 instruction 风险更高。因此 hook 不应“随配置出现就自动获得信任”。

Codex 会区分:

  • hook 的声明来自哪里;
  • 用户是否启用;
  • command 内容是否与受信任 hash 一致;
  • policy 是否只允许 managed hooks;
  • required managed hook 是否成功加载。

尤其重要的是:project 或 plugin 可以声明自己需要哪些 hooks,却不应自己修改“用户已经信任它”的状态。否则一个刚下载的仓库只要附带配置,就能同时提出命令并批准自己执行。

这与浏览器扩展安装时展示权限清单是同一个道理:能力声明和授权决定必须分属不同主体。


11.7 Dynamic tools:让前端程序成为执行者

有些能力只存在于前端:

  • IDE 当前选中了哪段代码;
  • 用户正在看的 diff 是哪一个;
  • 哪个编辑器 tab 处于激活状态;
  • 前端保存的本地草稿或设计选项;
  • 某个桌面应用提供的专属交互。

把这些能力复制进 harness 不现实,也会让内核依赖具体 UI。dynamic tools 提供了另一种结构:

  1. 前端在启动 thread 时声明 tool spec;
  2. harness 把 spec 注册进该 thread 的工具箱;
  3. 模型发起调用;
  4. harness 通过 app-server 向前端发送反向请求;
  5. 前端执行并返回 text、image 或 audio;
  6. harness 把结果归一化成工具结果 item,重新提交给 LOOP。
sequenceDiagram
    participant F as 前端程序
    participant A as app-server
    participant H as harness
    participant M as 模型

    F->>A: thread/start(dynamicTools)
    A->>H: 创建 thread,注册 spec
    H->>M: 本 step 工具清单
    M-->>H: 调用 editor.readSelection
    H->>A: item/tool/call(反向请求)
    A->>F: 请求执行
    F-->>A: text / image / audio
    A-->>H: dynamic tool response
    H->>M: 工具结果回灌

这是一种能力归前端所有、LOOP 仍由 harness 驱动的反向 RPC。前端不用实现 agent 循环,harness 也不用理解编辑器内部对象;双方只在 tool spec 与结果内容上达成契约。

11.7.1 一个实际例子:让 agent 查询前端已登录的工单系统

假设 IDE 已经登录公司工单系统,登录凭证只保存在 IDE 中。我们希望 agent 能查询工单,但不希望把 IDE 的认证状态和业务 SDK 搬进 harness。

前端先开启 experimental API capability,然后在创建 thread 时声明一个 tickets.lookup_ticket 工具。下面省略了与例子无关的 thread/start 字段:

{
  "method": "thread/start",
  "id": 10,
  "params": {
    "dynamicTools": [
      {
        "type": "namespace",
        "name": "tickets",
        "description": "查询当前用户有权限查看的工单",
        "tools": [
          {
            "type": "function",
            "name": "lookup_ticket",
            "description": "根据工单编号查询标题、状态和负责人",
            "deferLoading": false,
            "inputSchema": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "description": "工单编号,例如 ABC-123"
                }
              },
              "required": ["id"],
              "additionalProperties": false
            }
          }
        ]
      }
    ]
  }
}

harness 只保存并注册这份 spec,不知道工单 API 地址、认证方式或查询代码。用户随后说“查看 ABC-123 现在由谁处理”,模型根据 spec 生成对 tickets.lookup_ticket 的调用。app-server 不会自己访问工单系统,而是向声明该能力的前端发送反向 JSON-RPC 请求:

{
  "method": "item/tool/call",
  "id": 60,
  "params": {
    "threadId": "thr_123",
    "turnId": "turn_123",
    "callId": "call_123",
    "namespace": "tickets",
    "tool": "lookup_ticket",
    "arguments": {
      "id": "ABC-123"
    }
  }
}

前端收到请求后,用自己的 SDK 和登录态查询工单系统,再用同一个 JSON-RPC id 返回结果:

{
  "id": 60,
  "result": {
    "contentItems": [
      {
        "type": "inputText",
        "text": "ABC-123:支付回调超时;状态为处理中;负责人是李明。"
      }
    ],
    "success": true
  }
}

harness 将这段内容转换成标准工具结果 item,写入历史并交给下一轮 LOOP。模型随后可以回答:“ABC-123 正在处理中,负责人是李明。”

整个过程中三方职责是清楚的:

参与方 负责什么 不需要知道什么
模型 根据 spec 决定是否调用,并生成参数 工单 SDK、认证 token、前端实现
harness 冻结 spec、路由请求、关联 callId、回灌和持久化结果 工单系统的内部协议
前端 校验参数、使用当前登录态查询、返回结果 LOOP、上下文组装和模型流

app-server 还会围绕这次反向请求发出 item/starteditem/completed,因此 UI 能显示“正在查询工单”以及最终成功或失败状态。若前端查询失败,它仍应返回 success: false 和可解释的文本;这会作为一次失败观察回灌给模型,而不是让整个 thread 崩溃。

这个例子说明 dynamic tool 的关键不是“动态生成了一段代码”,而是:前端在运行时声明一份工具契约,并成为该工具的执行者;harness 继续负责 LOOP 和结果协议。

11.7.2 为什么 dynamic tools 属于 thread

dynamic tool 在 thread/start 时声明,而不是每次 turn 临时附带。原因有三点:

  1. 工具身份在整个对话中保持稳定,模型不会这一轮看到、下一轮无故消失;
  2. 定义可以写入 Session 元数据,resume 时知道旧 thread 依赖过哪些前端能力;
  3. thread 是前端订阅、反向请求和历史恢复的共同坐标。

但“定义被持久化”不代表“执行者也被恢复”。进程重启后,旧前端连接已经不存在。resume 只能重建 spec 和依赖事实,真正调用前仍要确认当前连接具备对应能力。

这正是第十章的边界:恢复契约,不复活连接。

11.7.3 前端是独立执行域

dynamic tool 不在 harness 内执行,所以本地沙箱无法观察它的副作用。安全责任要拆开:

  • harness 控制哪些 dynamic tools 对模型可见;若能力有高风险副作用,产品层还要显式增加 hook、policy 或审批;
  • app-server 负责反向请求的身份、关联 ID 和取消语义,调用层还要设置合理的 timeout;
  • 前端验证参数并限制实际操作;
  • 用户信任的是“这个前端提供这项能力”,不是只信任一段 tool 描述。

如果前端不响应,又没有 timeout 或断线处理,工具 future 就可能一直等待。因此反向请求不能静默丢失:传输层要有有界队列、过载错误、断线处理和轮次结束时的明确取消。第二章的双向协议在这里不再只是 UI 通信,而是扩展 runtime 的一部分。

11.7.4 同一套扩展语义,三种前端形态

dynamic tools 依赖 app-server 的双向协议,但 app-server 并不绑定某一种 UI 或传输。它可以服务三类前端:

前端形态 传输 适合场景 额外边界
进程内前端 bounded memory channel TUI、与 Codex 一同交付的桌面程序 无网络开销,但仍保留 typed request 和 JSON-RPC response envelope
本地进程前端 stdio JSONL IDE 插件、本地自动化 生命周期随子进程,天然单机
网络前端 WebSocket / local socket 远程 UI、多客户端控制面 握手、订阅、鉴权、背压和断线恢复

三种形态共享 thread、turn、item、反向请求和 dynamic tool 的语义。进程内模式也不会因为“大家都在同一个进程”就绕开协议直接改 Session;它只是把 socket 换成内存通道。这样 TUI、IDE 和远程前端对生命周期的理解不会分叉。

连接建立时,前端先通过 initialize handshake 声明身份与 capability。实验性 API 按连接 gating:一个前端明确表示能够理解某项实验字段后,服务端才向它开放。可扩展性因此不只是服务端“多提供一个方法”,还包括前端是否有能力正确处理它。

这里也能准确区分两种常被混淆的 API:

  • Extension API在受信任运行时内部注册 contributor,参与 context、tool 和 lifecycle;
  • app-server API让进程内或进程外前端通过稳定协议控制 thread,并用 dynamic tools 反向提供能力。

前者扩展“harness 内部如何工作”,后者扩展“哪些前端可以驱动 harness、前端能向模型提供什么”。dynamic tools 正是两者之间的桥:它从 app-server 协议进入,最终落到统一工具系统中执行和回灌。


11.8 Plugin:能力包,而不是内核代码注入

现在可以准确解释 plugin 了。一个 Codex plugin 主要包含:

  • skills;
  • MCP server 声明;
  • apps 或连接器信息;
  • hooks;
  • 面向 UI 的名称、描述和元数据。

它的 manifest 负责说明“这个包里有哪些能力、资源在哪里”,marketplace 负责发现、安装和更新,配置负责启用与裁剪。plugin 自身通常不获得一个“在主进程任意执行代码”的入口。

flowchart LR
    MP["Marketplace<br/>发现与版本"] --> I["安装 plugin"]
    I --> MF["读取 manifest"]
    MF --> SK["Skill roots"]
    MF --> MC["MCP servers"]
    MF --> HK["Hooks"]
    MF --> AP["Apps / UI metadata"]
    SK --> ACT["按配置与信任激活"]
    MC --> ACT
    HK --> ACT
    AP --> ACT
    ACT --> NT["通常从新 thread 生效"]

这样的设计牺牲了一部分“想怎么改就怎么改”的自由,换来四个重要收益。

11.8.1 能力可枚举

安装前可以回答:这个 plugin 带来哪些 skills、要连接哪些 MCP servers、会注册哪些 hooks。安全审查面对的是结构化清单,不是一段可能在加载时做任何事情的初始化代码。

11.8.2 权限可分开授予

用户可以启用 plugin,但禁用某个 MCP server;可以阅读其中的 skill,却不信任 command hook;组织 policy 还可以要求某些 managed hooks 必须存在。安装不再等于全权授权。

11.8.3 能力走统一管线

plugin 带来的 MCP tool 仍走工具注册、审批、超时和结果回灌;skill 仍受上下文预算约束;hook 仍受 lifecycle 契约与信任策略约束。plugin 不会因为“来自一个包”就获得旁路。

11.8.4 来源可以追踪

同一个 MCP server 或 skill 可能来自用户目录、project 或 plugin。保留 provenance 后,冲突提示、UI 展示、审计和卸载都能回答“这项能力从哪里来”。卸载 plugin 时也可以只移除它贡献的部分,而不误伤同名的用户能力。

plugin 因此更像一个声明式依赖包:它组合能力,却不重新定义能力的执行规则。这也是 plugin 与 Extension API 最重要的区别。


11.9 Extension API:真正深入生命周期的受信任接口

声明式机制覆盖了大多数生态扩展,但 harness 自身和第一方前端仍需要更深的组合能力,例如:

  • 在每个 step 注入结构化世界状态;
  • 为 thread 或 turn 创建私有状态;
  • 贡献内置级工具;
  • 观察 tool lifecycle;
  • 在 turn 开始、输入进入、item 完成时参与处理;
  • 贡献 MCP server 或 token usage;
  • 识别 skill invocation;
  • 在审批前做自动 review。

这些能力由 Extension API 提供。它不是运行时下载一段未知代码,而是在构建运行时时注册一组强类型 contributors。Registry 建好后保持不可变,后续 Session 只使用这份固定能力集合。

11.9.1 Contributor:一次只贡献一种职责

Extension API 没有一个万能的 on_everything(session)。不同职责由不同 contributor 表达:

Contributor 参与的职责
ContextContributor 向模型上下文贡献受预算约束的片段
ToolContributor 提供 session/thread/step 范围的工具
ToolLifecycleContributor 观察或介入工具生命周期
ThreadLifecycleContributor 处理 thread 创建、恢复与结束
TurnLifecycleContributor 处理 turn 开始、完成与中断
TurnInputContributor 处理进入 turn 的输入
TurnItemContributor 观察形成的 item
McpServerContributor 动态贡献 MCP server 配置
ConfigContributor 参与运行时配置构造
TokenUsageContributor 观察和聚合 token 用量
SkillInvocationContributor 识别和记录 skill 调用
ApprovalReviewContributor 对需要审批的动作做 review

职责拆开有两个好处:

  • 扩展只拿到完成该职责所需的最小上下文;
  • 执行顺序和归并语义可以针对每类贡献点单独定义。

有的 contributor 是按注册顺序累积,有的是 first-claim(第一个明确处理者接管),有的是 later-wins(后来的配置覆盖前者)。顺序本身是 API 契约,不能依赖 HashMap 遍历或异步完成先后。

11.9.2 Scoped state:状态属于生命周期,不属于全局单例

扩展经常需要状态。例如 skills 扩展要记住上一步模型看过哪个 catalog revision,工具扩展可能要记录 thread 级缓存,turn hook 需要保存当前轮次的临时判断。

把这些都放进全局单例会制造串话、泄漏和恢复困难。Extension API 按生命周期提供 scoped state:

  • session scope;
  • thread scope;
  • turn scope;
  • step scope。

每个扩展以自己的类型作为 key,读写自己的 typed data;harness 拥有 scope 的创建和销毁时机。扩展不需要把私有字段塞进核心 Session 类型,核心也不需要理解每个扩展的内部结构。

flowchart TD
    S["Session scope<br/>跨 thread 的共享状态"] --> T["Thread scope<br/>一段对话的状态"]
    T --> U["Turn scope<br/>一次用户任务"]
    U --> P["Step scope<br/>一次采样 + 行动"]
    P --> X["边界结束后销毁"]

这种状态模型的关键不是“方便存数据”,而是明确寿命:

  • step 缓存不能误活到下一个 step;
  • turn 判断不会污染下一次用户任务;
  • thread 状态不能假装旧内存还在,resume 时要按扩展契约重建;
  • 扩展之间通过类型隔离,避免字段名冲突。

11.9.3 为什么 Extension API 不等于 plugin API

Extension API 与 harness 同进程、同信任域,能够直接影响上下文、工具与生命周期。一处 panic、死锁或无界输出都可能伤及内核,因此它适合:

  • Codex 自带的第一方模块;
  • 与前端一同交付、经过编译和测试的受信任扩展;
  • 需要强类型、低延迟和深层生命周期参与的能力。

它不适合直接作为互联网 marketplace 的任意代码加载接口。公开生态优先使用 plugin 的声明式组合;只有当现有扩展机制确实表达不了需求,才应该增加新的强类型 contributor。

这是一条很重要的 API 演进原则:

先扩展能力模型,再扩展任意代码权限。


11.10 配置分层:谁覆盖谁,谁又不能被覆盖

扩展越多,配置来源越多:

  • 产品随包默认值;
  • 系统级配置;
  • 企业托管配置;
  • 用户配置;
  • profile;
  • project 配置;
  • 本次 Session 的 flags;
  • 兼容旧系统的 managed overrides。

Codex 会按层合并配置,高优先级覆盖低优先级,同时保留每个字段的来源和每层 fingerprint。概念上的顺序可以画成:

flowchart BT
    D["Packaged defaults"] --> S["System"]
    S --> E["Enterprise managed"]
    E --> U["User"]
    U --> P["Selected profile"]
    P --> R["Project"]
    R --> F["Session flags"]
    F --> L["Legacy managed overrides"]

箭头越往上,普通覆盖优先级越高。但这张图只描述“最后值从哪里来”,还没有描述“哪些值根本不允许”。

11.10.1 Overlay 解决偏好,requirements 解决边界

配置与 requirements 是两套正交机制:

  • 配置表达“我想怎么运行”;
  • requirements表达“最多允许怎么运行”。

例如用户配置想启用一个 HTTP MCP server,普通 overlay 可以决定 URL、timeout 和 enabled;企业 requirements 可以进一步规定:

  • 只允许连接指定域名;
  • 本地 server 只能执行特定 command;
  • 必须使用某种身份;
  • 某些 hooks 必须启用;
  • 某类能力完全禁止。

最终有效值不是“最高配置层获胜”,而是:

有效能力 = 合并后的配置 ∩ requirements 允许的范围

requirements 通常只能收紧,不能被 project、profile 或 Session flag 放宽。否则所谓企业策略只是一份更低优先级的建议。

11.10.2 Project 配置先过信任门

project 配置来自当前仓库,而仓库内容可能刚从互联网下载。若进入目录就自动执行其中声明的 hook、MCP command 或脚本,打开项目本身就变成了代码执行。

因此 project 配置只有在目录被信任后才生效;即使可信,某些高风险字段仍不应由 project 改写,例如模型服务地址、认证目标和本地通知命令。

这里再次出现“声明与授权分离”:

  • project 可以说“这个项目建议使用某个 skill”;
  • 用户决定是否信任 project 配置;
  • 更高层 requirements 决定即使用户信任,也有哪些事情绝对不能做。

11.10.3 来源信息是一等数据

只给 UI 一个最终布尔值 enabled=true 不够。用户需要知道:

  • 是哪个配置层启用了它;
  • 哪个 requirements 又把它禁用了;
  • plugin、project 还是用户目录提供了这项能力;
  • 当前值为什么无法修改。

因此配置系统要保留 origin、disabled reason 和 layer fingerprint。可解释性不是附加功能,而是多层控制系统能被人正确使用的前提。第九章说人的注意力是一种预算;最浪费注意力的 UI,就是让用户在十层配置里猜一个开关为什么不生效。


11.11 同一项能力,必须穿过四道门并留下回执

把前面的机制合起来,一项扩展能力要真正产生副作用,至少要过四道门;执行之后,还必须沿统一路径留下结果:

flowchart LR
    P["Provenance<br/>它从哪里来?"] --> T["Trust<br/>是否信任并启用?"]
    T --> V["Visibility<br/>本 step 模型看得到吗?"]
    V --> E["Execution<br/>调用是否获批并受约束?"]
    E --> O["Observation<br/>结果如何回灌与审计?"]

11.11.1 Provenance:来源边界

系统必须知道能力来自内置模块、用户目录、project、plugin、MCP server 还是前端程序。没有来源,就无法处理重名、卸载、审计和信任。

11.11.2 Trust:激活边界

存在不等于启用。project 是否可信、hook 是否授权、MCP 身份是否满足 requirements,都在这一层决定。

11.11.3 Visibility:模型边界

启用不等于每一步都展示。工具可能 deferred,skill 只展示 metadata,某些能力只对根 agent 或特定模型可见。这里控制 token 成本、选择复杂度和最小权限。

11.11.4 Execution:副作用边界

模型看见并调用后,仍要经过 hook、policy、approval、sandbox 或外部服务鉴权。dynamic tool 还要经过前端自己的验证。

最后还有 Observation:执行结果必须归一化成 item、进入历史、持久化并留下 telemetry。一个扩展若只会“做事”却不提供稳定结果和生命周期信号,就无法被 LOOP 正确回灌,也无法在第十章的 replay 中解释。

这四道边界构成了一条“最小权力链”。每一层都只回答一个问题,任何一层都不能替代其他层:

  • 来自官方 marketplace,不代表每次调用都无需审批;
  • 用户启用了 plugin,不代表其中所有 tools 都应直接暴露;
  • tool 没展示给模型,不代表历史里的旧调用不需要兼容执行;
  • 本地沙箱允许,不代表远端服务一定授权;
  • 调用成功,不代表结果可以不受预算地塞进上下文。

11.12 更新、恢复与兼容:扩展不能只考虑“现在能跑”

第十章提出了一个尖锐问题:旧 rollout 恢复时,今天的运行时应该如何理解昨天的扩展?

扩展系统至少要区分三类东西:

内容 是否适合持久化 resume 时怎么处理
已发生的输入、调用和结果 item 原样 replay,不重新执行
当时依赖的能力定义和关键元数据 视需要保存 重建上下文与兼容判断
client、连接、future、进程句柄 创建新运行时,旧对象作废

11.12.1 更新不能改写正在采样的 step

skill 文件监听、plugin 更新、MCP catalog 刷新都可以实时发生,但不能直接改变当前 step snapshot。合理的生效边界通常是:

  • skill catalog 变化:下一次上下文贡献时更新;
  • MCP tool catalog 变化:下一个 step 重新冻结;
  • plugin 安装或卸载:新 thread 最清晰,必要时显式刷新已有 Session;
  • hook 配置变化:在确定的生命周期边界重新装载;
  • Extension Registry:运行时启动后保持不可变,更新需要重建运行时。

越深的扩展,更新边界越保守。因为深层变化影响的不只是“多一个工具”,还可能改变状态布局和生命周期语义。

11.12.2 Replay 旧调用,不代表重新拥有旧能力

历史里可能有一个已经卸载 plugin 提供的工具调用。replay 只需要把“当时调用过什么、结果是什么”恢复进历史,不需要重新执行,也不要求当前 runtime 还保留该工具。

但如果模型在 resume 后想再次调用它,必须按当前 catalog 判断:

  • 当前仍可用:按新 step snapshot 正常调用;
  • 已卸载或被 policy 禁用:返回明确的不可用结果;
  • spec 已不兼容:不能把旧参数静默交给新 runtime。

这和第六章 Hidden 工具的演进姿态相呼应:必要时可以保留旧 runtime 处理兼容调用,但不再向模型展示。历史兼容与未来可见性是两件事。

11.12.3 扩展状态必须选择持久化承诺

Extension scoped state 默认是内存状态,不会因为用了 typed store 就自动可恢复。每个扩展都必须明确:

  • 这是可丢弃缓存,resume 时重算即可;
  • 这是可从 rollout 推导的 projection;
  • 这是必须写成 canonical fact 的业务状态;
  • 这是外部系统状态,只能重新查询;
  • 这是带副作用的中间态,需要幂等键或人工确认。

最危险的状态是“看起来重要,却没有恢复契约”的内存字段。它在正常运行时一切顺利,一旦进程重启就让行为悄悄改变。

因此 Extension API 的 state scope 解决的是隔离与寿命,持久化协议解决的是跨进程语义,两者不能混为一谈。


11.13 一个完整例子:给 Codex 安装“线上故障处理能力”

假设团队想让 Codex 协助处理线上告警。需求包括:

  1. 按团队 runbook 排查;
  2. 查询监控、日志和发布记录;
  3. 执行高风险操作前必须走审批;
  4. 停止前确认已留下事故记录;
  5. IDE 中可以把当前分析结果附到事件面板。

不要把这些需求塞进一个万能 plugin runtime。按职责拆分:

需求 扩展机制 原因
runbook 与排查步骤 skill 它是工作方法和领域知识
查询监控、日志、发布记录 MCP 能力属于外部平台
高风险操作审批 MCP per-tool policy + PermissionRequest hook 这是确定性安全约束
停止前检查事故记录 Stop hook 必须卡在结束边界
附到 IDE 事件面板 dynamic tool 能力由前端程序拥有
一键安装整套能力 plugin 负责组合、元数据和分发
企业强制域名与身份 requirements 用户和 project 都不能放宽

安装后的完整路径如下:

sequenceDiagram
    participant U as 用户
    participant P as Plugin / 配置
    participant H as harness
    participant M as 模型
    participant X as MCP 平台
    participant F as IDE 前端

    U->>P: 安装并启用 incident plugin
    P->>H: 贡献 skill、MCP 与 hooks
    F->>H: thread/start 时声明 dynamic tool
    H->>H: requirements + trust + capability 检查
    U->>H: “调查这次告警”
    H->>M: skill catalog + deferred tool namespaces
    M->>H: 读取 incident-response skill
    M->>H: tool_search("query deployment and logs")
    H->>M: 返回命中的 MCP tool specs
    M->>H: 调用日志与发布查询
    H->>X: 按冻结 MCP binding 执行
    X-->>H: 结构化结果
    H->>M: 结果回灌
    M->>H: 请求回滚发布
    H->>U: 审批请求
    U-->>H: 批准一次
    H->>X: 执行回滚
    M->>H: 提议停止
    H->>H: Stop hook 检查事故记录
    H->>M: 否决停止:“请先生成并关联事故记录”
    M->>H: 调用 IDE attachIncidentReport
    H->>F: 反向请求
    F-->>H: 已附加
    H->>M: 结果回灌
    M->>H: 最终总结

这个例子揭示了可扩展设计的真正目标:不是让每个扩展包办全流程,而是让不同机制在同一组边界上组合。

  • skill 负责“会不会做”;
  • MCP 负责“有没有手”;
  • hook 负责“哪些关口不能忘”;
  • dynamic tool 负责“前端独有动作”;
  • plugin 负责“一起交付”;
  • requirements 负责“无论谁配置都不能越过的线”;
  • LOOP 仍然只做采样、行动和回灌。

11.14 常见失败模式

失败模式 表面现象 根因 更好的做法
把 plugin 当任意代码注入 一个扩展就能拖垮或绕过整个 harness 没有能力分层和信任域 plugin 声明式组合,深层代码只走受信任 Extension API
发现即启用 打开项目就执行 hook 或启动服务 混淆 catalog 与 activation discovery、trust、activation 分离
启用即全量暴露 工具 spec 撑爆上下文,模型选错工具 没有 visibility 层 Direct / Deferred / Hidden 分层,skills progressive disclosure
采样中途热替换工具 参数按旧 spec 生成,却交给新 runtime 没有 step snapshot catalog 更新下一 step 生效,调用绑定 revision
只用 skill 写强制规则 模型偶尔忘记审批或漏做收尾 把建议当成确定性控制 方法写 skill,硬约束写 hook、policy 或 runtime
安装等于授权全部能力 plugin 中任一 hook/MCP 都自动获得最高权限 声明者同时给自己授权 按能力分别启用和信任,requirements 再封顶
把本地沙箱当全局安全边界 MCP 或前端照样产生高风险副作用 忽略执行位置 每个执行域独立鉴权、审批和审计
异步 hook 试图改变过去 工具已经执行,后台检查才返回拒绝 控制结果错过生命周期边界 需要控制就同步等待;async 只做通知和上报
同名扩展静默覆盖 同一 $skill 或工具在不同环境含义不同 把名称当展示文本而非地址 保留 provenance,冲突显式化
扩展状态只放内存 resume 后行为悄悄改变 没有持久化承诺 明确缓存、projection、canonical fact 或外部状态
更新后重放旧副作用 plugin 升级后重复发起历史操作 混淆 replay 与 re-execute 历史只重放结果,新调用按当前能力重新判断
只给最终配置,不给来源 用户无法理解某开关为何无效 丢失 layer 与 disabled reason origin、fingerprint、约束原因一并暴露

这些失败看似分散,根因其实相同:没有把“能力存在、能力获准、模型可见、动作执行、结果留痕”拆成不同阶段。


11.15 更深一层:可扩展性是一种治理能力

当一个 harness 只有内置工具时,开发者既是能力提供者,也是规则制定者。引入 plugin、MCP、skills 和 hooks 后,参与者变多了:

  • 产品团队提供内置运行时;
  • 企业管理员提供 requirements 和 managed hooks;
  • 用户安装 plugin、配置 MCP;
  • project 提供本地 instruction;
  • 前端程序提供 dynamic tools;
  • 外部 server 真正执行副作用;
  • 模型根据当前可见面选择行动。

这已经不是一个“插件加载器”,而是一个小型治理系统。它必须回答:

  • 谁可以声明能力?
  • 谁可以授权能力?
  • 谁决定模型是否看见?
  • 谁执行副作用?
  • 谁保存事实和承担审计责任?
  • 当这些主体意见冲突时,谁有最终否决权?

Codex 的整体答案可以概括为:

低层可以提供默认值,
高层可以表达用户意图,
requirements 可以收紧边界,
step snapshot 冻结本轮事实,
执行域负责最终副作用,
rollout 保存已经发生的结果。

这里最值得注意的是:扩展点不是越多越好,越稳定才越有价值。

每新增一个 hook event 或 contributor,harness 就承诺了一个长期生命周期语义:

  • 它在什么时刻触发;
  • 此时哪些状态已经写入历史;
  • 失败会阻止流程还是只告警;
  • 多个处理器如何排序和归并;
  • 中断、重试、resume 时是否再次触发;
  • 输出是否进入上下文和 rollout。

一个模糊的扩展点看似灵活,实际会把内部实现细节永久冻结;一个职责单一、边界清晰的扩展点能力较窄,却能跨版本稳定。

因此设计新扩展机制时,应该按这个顺序提问:

  1. 现有 skill、MCP、hook、dynamic tool 是否已经能表达?
  2. 能否通过新增一个结构化事件或 tool spec 解决?
  3. 是否真的需要新的 Extension contributor?
  4. 如果必须深入内核,它的输入、输出、顺序、预算、失败和恢复语义是什么?

可扩展性的成熟标志,不是“任何地方都能插代码”,而是大多数需求都能落在少数稳定扩展点上,而且每项权力都能解释、限制和回放。


11.16 小结:可扩展性的七条设计原则

  1. 开放扩展点,不开放任意控制权。 LOOP 保持极简,扩展通过 skill、MCP、hook、dynamic tool 和强类型 contributor 进入固定边界。能表达的权力越具体,影响范围越可推理。

  2. 先区分发现、激活与冻结。 discovery 建 catalog,activation 结合配置、信任和能力决定是否可用,step snapshot 冻结模型本轮真正看到的 spec 与 runtime。动态刷新只影响未来边界,不改写在途采样。

  3. 不同机制承担不同职责。 skill 教方法,MCP 接外部能力,hook 保证生命周期规则,dynamic tool 委托前端执行,plugin 负责组合与分发,Extension API 服务受信任的深层集成。不要用一个万能 plugin 模糊所有边界。

  4. 声明与授权分离,配置与 requirements 正交。 project 或 plugin 可以声明能力,用户和管理策略决定是否信任;配置表达偏好,requirements 给能力封顶。任何低权限来源都不能通过更高优先级配置自行扩大权力。

  5. 执行位置决定安全边界。 本地命令、MCP server、前端 dynamic tool 和同进程 Extension 处在不同信任域。沙箱、审批、鉴权和审计必须覆盖真实执行者,不能假设 harness 的本地沙箱能约束远端世界。

  6. 上下文、名称和状态都要有预算与归属。 skills progressive disclosure、tools deferred exposure、扫描与输出硬上限共同控制 token 和资源;名称保留 provenance,冲突显式处理;scoped state 明确寿命,跨进程状态另行定义持久化承诺。

  7. 扩展也必须可恢复、可兼容、可观测。 replay 旧 item 不重新执行旧能力,连接和 future 在 resume 时重建;更新在稳定边界生效;hook、tool 和 contributor 的触发、耗时、失败、来源与版本都应留下可解释信号。

留给读者思考的几个问题

  • skill 依赖模型遵循 instruction,hook 提供确定性控制。一个规则从“建议”升级为“强制”时,应该如何判断它该从 skill 移到 hook 或 policy?
  • MCP server 与 dynamic tool 都在 harness 外执行。两者的身份、审批、超时、重试和审计协议是否应该完全统一?哪些差异来自网络服务与交互式前端的本质不同?
  • plugin 更新后,已有 thread 应继续使用旧能力快照,还是尽快切到新版本?若要做到真正可复现,是否需要把 plugin 版本和 skill 内容摘要写入 rollout?
  • 多个 hooks 同时修改一个工具调用时,应该按顺序叠加、冲突即拒绝,还是只允许第一个认领?哪种语义最容易测试和向用户解释?
  • Extension Registry 启动后不可变,换来的是确定性;但长时间运行的 app-server 又希望在线升级能力。应该重建进程、迁移 Session,还是引入版本化 Registry?每种方案会破坏哪些不变量?
  • deferred tools 和 progressive disclosure 都依赖“模型知道自己该搜索”。当能力目录越来越大时,发现质量应该由关键词索引、语义检索、规则路由还是另一个 agent 负责?
  • 扩展来源、激活结果、step snapshot、实际调用和结果回灌构成了一条完整因果链。要定位“为什么模型没用某个工具”,可观测系统至少要记录这条链上的哪些节点?

下一章我们进入可观测性:tracing、metrics 与回放如何把一次请求从前端、LOOP、模型流、hook、工具、MCP 一直串到结果 item;当扩展越来越多时,系统如何回答“慢在哪里、谁做了决定、为什么这项能力没有生效”。

分类:Agent Harness标签:#agent #harness #codex