青雲的博客
拆开 Codex 第四部:外部能力怎样进入同一套治理 第 23 章

Code Mode 为什么仍然绕不开同一张工具表

从 InProcessCodeModeSession、V8 JavaScript engine cell 和 runtime callbacks 出发,追踪一次 Code Mode nested tool call 如何经由 CodeModeDispatchBroker 回到当前 turn 持有的 ToolRouter,再把 typed result 投回 JavaScript,最后由 yield 或 wait 交回模型。

源码版本
rust-v0.144.6
验证日期
Commit
5d1fbf26c43abc65a203928b2e31561cb039e06d

第 22 章把执行权反转给宿主:模型拿到一个动态工具规格,真正的 call 由宿主完成。Code Mode 的表面现象相反。模型给 Codex 一段 JavaScript,脚本里可以写 await tools.get_context_remaining({}),看起来像脚本拥有了一组自己的工具。

固定版本 rust-v0.144.6 没有为 Code Mode 再造 executor。它有两个控制入口:exec 负责创建或观察一个 cell,wait 负责观察或终止已经创建的 cell;脚本中的 tools.foo 必须回到当前 turn 的 ToolRouter。这条调用链可以从源码逐段走完:

model custom exec
  -> CodeModeExecuteHandler
  -> CodeModeService / InProcessCodeModeSession
  -> SessionRuntime / CellActor / V8 runtime
  -> tools.foo callback creates a pending Promise
  -> CodeModeDispatchBroker
  -> ToolCallRuntime with the same Arc<ToolRouter>
  -> ToolRegistry and the selected handler
  -> AnyToolResult::code_mode_result()
  -> RuntimeCommand::ToolResponse
  -> JavaScript Promise resolution
  -> text(), yield_control(), or wait()

这章的停止线很明确:Code Mode 负责编排与观察。权限仍由原工具路径治理,ToolSpec 的返回 schema 也不会在这里自动变成 validator。

先把两张“表”分开

模型能看到的规格和宿主能执行的 runtime 在 Codex 里本来就是两个集合。ToolRouter 自己保存 registrymodel_visible_specs 两个字段;前者按完整 ToolName 找 executor,后者只负责发给模型的规格。Code Mode 并没有绕开这个分层。

本节源码依据(1 处)

规划阶段只在 Code Mode 或 Code Mode Only 下构造两个专用 runtime:CodeModeExecuteHandlerCodeModeWaitHandlerbuild_code_mode_executors 遍历已经规划好的 executors,收集它们的 ToolSpec 作为 nested surface,再把这两个 handler 放回同一组 runtimes;这里没有为每个脚本工具复制一份 handler。

本节源码依据(1 处)

collect_code_mode_tool_definitions 先把 function、freeform 和 namespace spec 转成一份中间 ToolDefinition,其中保留 input / output schema,再把这份列表放进 ExecuteRequest.enabled_tools。到了 in-process session 边界,runtime_request 的内部映射会丢弃这些 schema,只复制 name、内部 tool_name、description 和 kind。真正可调用的实现仍在 ToolRegistry

本节源码依据(1 处)

协议层定义因此不等于 cell 最终拿到的 metadata。runtime 会把内部定义压成 EnabledToolMetadata;cell 内的 ALL_TOOLS 最终只暴露 name 和 description,不保留 input / output schema。

本节源码依据(2 处)

为了不把控制面和 nested call 混成一张表,先把四种调用形态并排看一遍。这里的 outer exec 是模型直接调用的 custom tool;nested Functionnested Freeform 才是 cell 内 tools.foo 的两种输入投影;wait 只是继续观察已有 cell。

调用形态payload / 输入投影hook 与结果边界
outer execToolPayload::Custom;default pre/post hook payload: None控制面只创建并观察 cell
nested FunctionToolPayload::FunctionObject → JSONNone → {}其他(非 Object) → error先经过 pre_tool_use_payload,完成后经过 post_tool_use_payload
nested FreeformToolPayload::CustomString onlyCustom 默认 pre/post hook payload 都是 NoneApplyPatch 例外:重建两种 payload,hook 还能改写 command
waitToolPayload::Functionpre_tool_use_payload: Nonepost_tool_use_payload: None
本节源码依据(1 处)

下面的图只画一次 nested call。execwait 是模型可见的控制工具;get_context_remaining 是由脚本发起、最后仍由同一 registry handler 执行的普通 tool。

flowchart TB
  accTitle: Code Mode nested call returns through the same ToolRouter
  accDescr: The outer exec handler starts a cell, JavaScript callbacks emit a pending tool call, the dispatch broker routes it to the turn worker that owns the same ToolRouter, and the typed result resolves the JavaScript promise before yield or wait produces an observable response.
  MODEL["model: custom exec"] --> EXECUTE_HANDLER["CodeModeExecuteHandler"]
  EXECUTE_HANDLER --> SERVICE["CodeModeService"]
  SERVICE --> SESSION["InProcessCodeModeSession"]
  SESSION --> CELL["SessionRuntime / CellActor"]
  CELL --> V8["V8 runtime + tools callbacks"]
  V8 -->|"tools.foo()"| PENDING["pending Promise + RuntimeEvent::ToolCall"]
  PENDING --> BROKER["CodeModeDispatchBroker"]
  BROKER --> WORKER["turn worker / ToolCallRuntime"]
  WORKER --> ROUTER["same Arc<ToolRouter>"]
  ROUTER --> REGISTRY["ToolRegistry -> selected handler"]
  REGISTRY --> TYPED["AnyToolResult::code_mode_result()"]
  TYPED -->|"JSON value"| RESOLVE["RuntimeCommand::ToolResponse"]
  RESOLVE --> V8
  V8 --> TEXT_IMAGE["TEXT_IMAGE"]
  TEXT_IMAGE --> CONTENT_ITEMS["CONTENT_ITEMS"]
  V8 --> NOTIFY["NOTIFY"]
  NOTIFY --> NOTIFICATION_TASKS["NOTIFICATION_TASKS"]
  NOTIFICATION_TASKS --> INJECT_CUSTOM_TOOL_CALL_OUTPUT["INJECT_CUSTOM_TOOL_CALL_OUTPUT"]
  V8 --> YIELD_CONTROL["yield_control"]
  YIELD_CONTROL --> OBSERVED["Yielded observation"]
  V8 --> RESULT["Result"]
  RESULT --> INITIAL_RESPONSE["INITIAL_RESPONSE"]
  EXECUTE_HANDLER --> INITIAL_RESPONSE
  V8 --> TERMINAL["terminal"]
  SUBSEQUENT_WAIT["SUBSEQUENT_WAIT"] --> WAIT_HANDLER["CodeModeWaitHandler"]

图中的 EXECUTE_HANDLER → INITIAL_RESPONSE 表示 execute 处理器产出初始观察响应;后续观察则沿 SUBSEQUENT_WAIT → WAIT_HANDLER 进入 wait 处理器。后者是一个新的观察请求,不是重新跑一遍 source。NOTIFY 走 notification task 和 INJECT_CUSTOM_TOOL_CALL_OUTPUT 旁路,不进入 CONTENT_ITEMS,也不直接改变 wait 路由。

本节源码依据(1 处)

一个 session,许多 cell

CodeModeService 持有一个 OnceCell<Arc<dyn CodeModeSession>>、一个 CodeModeSessionProvider、一个 CodeModeDispatchBroker 和关闭标记。第一次 exec 才通过 provider 建立 session;关闭已经开始时,后来的初始化会被拒绝,正在初始化的 session 也会被立即 shutdown。start_turn_worker 还会检查当前 ToolMode,只有 CodeModeCodeModeOnly 才为这一 turn 启动 broker worker。

本节源码依据(1 处)

固定的 in-process provider 创建的是 InProcessCodeModeSession。这个类型本身很薄:内部只有 SessionRuntime<ProtocolDelegate>execute 把 protocol request 转成 runtime request,选择 YieldAfter 的初始观察模式,再把初始 event 放进 StartedCellwaitterminateshutdown 都继续委托给 runtime。

SessionRuntime 才是 session 级 owner。它持有:

字段作用域关键边界
stored_valuessession新 cell 启动时拿到一份 clone,cell 完成时才提交 writes
cellssessionCellId 找到当前 CellHandle,供 execute、observe、terminate 使用
cell_taskssession跟踪所有 actor 和 failure watcher,shutdown 会关闭 admission 并等待
shutdown_tokensession -> cell父 token 派生给每个 cell,取消会终止 runtime 和 callback tasks
start snapshotsession 到 cell-local stored_values新 cell 从 session clone 开始,不持有一根实时共享引用
storecell-local map + write set同一 cell 立即 read-your-writes;完成时再提交到 session
loadcell-local stored_values读取启动快照加本 cell 已 store 的值;缺失 key 返回 undefined

启动 cell 时,runtime 先 clone session 的 stored_values,创建 CellActor,把 handle 插进 cells,再把 actor task 放入 tracker。JavaScript 调用 store 时,callback 同时更新 cell-local stored_valuesstored_value_writes,所以同一个 cell 随后的 load 能读到刚写入的值,也就是 read-your-writes。另一个 cell 仍只看到自己启动时的 clone;必须等前一个 cell completion commit 把 write set 合并回 session,之后新启动的 cell 才能读到它。

完成时,RuntimeCellHost::commit_completion 在 cancellation 允许的情况下把 writes 合并回 session;cell 关闭后才从 registry 移除并通知 delegate。这里的隔离边界是“cell-local 立即可见、跨 cell 在 completion 后可见”,不能缩写成“load 只读启动快照”。

本节源码依据(4 处)

StartedCell 只承诺 cell 已创建:它带着 cell_id 和一个等待初始观察结果的 future,结果可能是 YieldedResultTerminated。协议还把 WaitOutcome::LiveCellWaitOutcome::MissingCell 分开;cell 被清理后,wait 会返回结构化的 missing response,源码不会重新执行。

本节源码依据(1 处)

stored values 是 cell 的状态边界

上一节回答的是 SessionRuntime 持有什么;这里换成数据流视角,只看一次 cell 的 snapshot、读写和 completion commit,不再把 stored_values 当成跨 cell 的全局对象:

状态进入路径所有权与结果
start snapshotstart snapshot -> stored_valuesexec 创建 cell 时捕获;后续 cell 不会看到别的 cell 的实时 map
storestore -> stored_values; store -> writes先写当前 cell 的可读 map,再把序列化值放进待提交 write set
loadload -> stored_values读取当前 cell 的 snapshot + 已写入值;cell 被取消时 write set 不提交到 session

这张表只描述 cell 内的读写视图,不把 stored_values 说成 durable conversation history。真正的 session 写回要等 terminal RuntimeEvent::Result 通过 commit 边界;即使结果带 error_text,只要 cancellation/termination 没有赢得竞态,completion commit 仍可能写回。yield 只交付观察结果,不会提前提交 write set。

本节源码依据(1 处)

V8 callback 只发事件,不执行宿主工具

runtime thread 用 source 评估 main module,并把 enabled tool metadata、stored value snapshot、pending promise map 和 write set 放进 RuntimeState 的 isolate slot。JS 的 tools 全局函数由 callback 读取这些状态。

本节源码依据(2 处)

tools.foo 的 Promise 从哪里来

tool_callback 不会直接调用 Rust handler。它先解析 tool index 和输入,创建 PromiseResolver,分配 cell 内唯一的 tool-<n> id,把 resolver 放进 pending_tool_calls,再发出 RuntimeEvent::ToolCall。因此脚本的 await 会在这里停住,直到后面某个 host callback 发回 RuntimeCommand::ToolResponseToolError

本节源码依据(1 处)

这个 tool-<n> id 只在一个 cell 内需要唯一。后面的 call_nested_tool 会生成一个 UUID,并把它存进新建的 ToolCall.call_idToolCallSource::CodeMode 自身只含 cell_idruntime_tool_call_id,没有那个 UUID。这个 nested ToolCall.call_id 是 Core 内部 dispatch 与 lifecycle 使用的调用 id,不是 model-visible call id。

本节源码依据(1 处)

输出、状态和暂停

textimage callback 只向 runtime event channel 追加 content item;store 把可序列化值同时写入 cell-local map 和 write set,load 读的是启动快照加本 cell 已写入的当前 map;notify 发给宿主一条旁路通知;yield_control 发出 RuntimeEvent::YieldRequested。这些 callback 都没有直接触碰 ToolRegistry

本节源码依据(1 处)

runtime thread 收到宿主回包后,在 V8 isolate 中 resolve pending promise,再跑 microtask checkpoint;只有 promise 完成、脚本抛错或收到 terminate,才会发 RuntimeEvent::Result 或关闭事件。CellActor 维护 observer、pending tool ids、content items 和 callback task sets;收到 ToolCall 时启动 callback task,收到 ToolResponse 后由 runtime 继续脚本。

本节源码依据(2 处)

broker 把 nested call 送回当前 turn

cell 没有隐藏 registry。它的 host delegate 被绑定到当前 turn worker:CodeModeDispatchBroker::start_turn_worker 收到 Arc<ToolRouter>StepContext 和 diff tracker,构造 ToolCallRuntime 后启动消息循环。每个 cell 还有一道 readiness gate;exec 完成 trace 初始化并调用 mark_cell_ready_for_dispatch 后,broker 才允许 nested invocation 继续。

本节源码依据(1 处)

CodeModeExecuteHandler 在拿到 StartedCell 后才标记 readiness,然后等待 initial response。这样 nested call 不会在 trace 和 turn worker 尚未登记前抢跑。ready gate 只确认当前 cell 已接入本轮 dispatch worker,权限检查发生在后面的原工具路径。

本节源码依据(2 处)

broker 的 invoke_tool 通过 channel 发送 DispatchMessage::InvokeTool,等待 oneshot;取消 token 在发送前、等待 ready gate 时和等待回包时都会检查。worker 里的 CoreTurnHost 最终调用 call_nested_tool,而不是另写一个 handler lookup。

本节源码依据(1 处)

call_nested_tool 的三步

  1. 如果 nested name 又是无 namespace 的 exec,立即返回错误,防止 Code Mode 自己递归调用自己。
  2. CodeModeToolKind 把输入投影成 ToolPayload::FunctionToolPayload::Custom;function 只接受 object,freeform 只接受 string。
  3. 构造一个新的 ToolCall,调用 ToolCallRuntime.handle_tool_call_with_source,并用 ToolCallSource::CodeMode { cell_id, runtime_tool_call_id } 标记来源;最后把 AnyToolResultcode_mode_result() 作为 JSON 返回。
本节源码依据(2 处)

这里的 ToolCallRuntime 保存 worker 收到的 router、session、step context 和 tracker。它会先按工具是否支持并行拿读写锁,再把同一个 cancellation token 传给 ToolRouter。nested source 只改变追踪与结果投影,registry 地址没有变化。

本节源码依据(2 处)

同一张 registry,才有同一条治理链

ToolRouterToolCall 展开成 ToolInvocation,随后交给 ToolRegistry::dispatch_any_with_terminal_outcome。这个 invocation 仍带着当前 SessionTurnContextStepContext、cancellation token、diff tracker、call id、tool name、source 和 payload。registry 先查 executor、核对 payload kind、发 tool-start,再运行 pre-tool hook,调用 handler,按成功结果运行 post-tool hook,最后才形成 AnyToolResult

本节源码依据(3 处)

nested tool 仍然受到同一组边界约束:

边界Code Mode 里实际发生什么不能推出什么
approval / sandboxnested handler 继续读取同一个 TurnContext 的 approval、permission profile、network 和 sandbox 状态;例如 Unified Exec 仍在 handler 内选择 sandbox 并校验额外权限exec 外面包了一层 JavaScript,就自动绕过命令审批或沙箱
cancellationcell token 经过 broker、ToolCallRuntimeToolInvocation 到 handler;取消时 registry/parallel runtime 决定 abort 还是等待 teardownyield 会替代 cancellation,或 wait 能复活已经取消的 cell
hooks / lifecyclenested invocation 仍进入 registry 的 pre/post hook 和 tool lifecycle;只有 wait 自己是 cell 控制面,显式不生成普通 pre/post hook payloadCode Mode 的控制工具和脚本中的普通工具具有完全相同的 hook 语义
output budgetwait 的外部参数名是 max_tokensexec 的 pragma 参数名是 max_output_tokens;两者都在各自边界交给 handle_runtime_response 截断本次 cell 输出ToolSpec.output_schema 会替它做 token 截断或结果验证

Unified Exec 的 handler 是一个容易验证的例子:它仍从 turn 读取 file-system/network sandbox policy,按 approval policy 校验 sandbox override,并把额外权限传进 ExecCommandRequest。这些行为来自原 handler;Code Mode 没有另写一遍审批。

本节源码依据(2 处)

wait 是一个范围很窄的例外:它只控制已有 cell,不触发独立的用户副作用。源码明确让 CodeModeWaitHandlerpre_tool_use_payloadpost_tool_use_payload 返回 None;脚本中发起的 tools.foo 仍经过普通 registry hook。

本节源码依据(1 处)

typed result 不等于 model wire output

这条边界用 get_context_remaining 最清楚。它的 ToolSpec 声明了一个 object-shaped output_schema,要求 tokens_left 是 integer 或 null;但 handler 自己实现了两种输出投影:

消费者调用的方法固定版本的结果
direct model tool callToolOutput::to_response_itemFunctionToolOutput 的文本 fragment,例如“当前 context 还剩 N tokens”
Code Mode nested callToolOutput::code_mode_resultjson!({ "tokens_left": self.tokens_left }),脚本收到一个 JSON object
本节源码依据(2 处)

AnyToolResult::into_responseAnyToolResult::code_mode_result 也把这两个出口写成了两个显式方法。Code Mode source 由 dispatch trace 映射到 CodeModeResponse { value: result.code_mode_result(...) };Direct source 才映射到 DirectResponse { response_item: result.to_response_item(...) }

本节源码依据(2 处)

这条 core dispatch 路径没有把返回的 JsonValue 再交给 output schema validator。schema 随 ToolDefinition/ToolSpec 被描述和传递,最终 JSON 则由 ToolOutput::code_mode_result 构造。要判断某个工具是否校验结果,还得继续检查它自己的 handler 或结果投影。

真实实验:9,000 来自结构化结果

固定测试把 model context window 设为 10_000,打开 TokenBudget,然后在脚本中执行:

const result = await tools.get_context_remaining({})
text(JSON.stringify(result))

断言直接读取 Code Mode custom-tool output 中的结构化 JSON:{"tokens_left":9000}。这说明 nested call 确实走到了 handler 的 code_mode_result 分支;重新包装 direct 文本得不到这个 object。

本节源码依据(2 处)

相同 handler 的 direct 测试则检查 context fragment 被注入下一次 Responses input;它不是同一份 wire body。这个对照也是为什么本章把 typed result 和 model wire output 分开写。

本节源码依据(1 处)

yield 是观察点,wait 是再次观察

ExecuteRequest 同时携带 yield_time_msmax_output_tokensInProcessCodeModeSession::execute 将前者变成 ObserveMode::YieldAfter;cell actor 用 timer 或显式 yield_control() 事件把当前 content items 交给 observer。RuntimeResponse::Yielded 只表示“本次观察已经有输出或到达时间边界”,不表示 runtime 已完成。

本节源码依据(1 处)

CellActor 的 timer 到点,或者收到 YieldRequested 时,会把当前 observer 响应成 CellEvent::Yielded,但不会把 cell 从 SessionRuntime.cells 删除;runtime 仍可能等待 pending tool promise。后续 wait 调用 SessionRuntime::begin_observe 找到同一个 CellHandle,将新的 observer 接到同一个 actor 上。

本节源码依据(1 处)

把“脚本何时停下来”和“cell 是否已经结束”分开,观察触发可以列成四种稳定情况:

观察触发观察结果交给谁
yield_controlyield_control → Yielded;允许空输出当时的 observer;可能是 execute 的 initial observe,也可能是 wait
timertimer → Yielded;到达 yield_time_ms 只结束本次观察设置这次 YieldAfter 的 observer;execute 和 wait 都能设置
Resultruntime Result;脚本已经 settle有 observer 就交付;没有则缓存在 completed cell,等下次 observe
terminateTerminated;cell 进入终止路径显式 terminate 的调用者;后续 observe 只能看到 closed/missing

execute handler 先注册 initial observer,wait handler 后续用同一个 CellId 注册新的 observer;runtime 把 Yielded 或 terminal event 送给当时持有的 response channel。completion 到来时若没有 observer,CellActorState 会保留 completed event,等下一次 observation 再交付。因此 terminal response 到底由哪个 handler 收到,取决于哪个 observer 接管了 cell;response enum 本身不选择 handler。

CodeModeWaitHandler 根据 terminate 选择 waitterminate。它解析的外部预算字段叫 max_tokensCodeModeExecuteHandler 从 pragma 解析的是 max_output_tokens。两者分别在自己的 handler 边界把值传给 handle_runtime_response。如果 response 表示 live cell 已到 terminal state,wait handler 还会记录 code-cell trace 并关闭 dispatch gate;随后同一个出口完成 image detail sanitization、status header 和 output truncation。

本节源码依据(2 处)

handle_runtime_responseYieldedTerminatedResult 使用同一个 max_output_tokens 截断策略;Result 还会把 runtime error text 追加到输出并把 success 设为 false。这个预算只约束交回模型的内容,与 JS 内部对象的 schema 检查无关。

本节源码依据(1 处)
macOS Code Mode yield wait resume 定向测试结果

这是一张历史截图:当时在 macOS 和固定 commit 5d1fbf26c43abc65a203928b2e31561cb039e06d 上运行的命令为:

just test -p codex-core -E 'test(code_mode_can_yield_and_resume_with_wait)'

实际结果为 1 test run: 1 passed, 2968 skipped。当前复现不要直接照抄旧命令:先按第四部导读创建 disposable archive,再在 $ARCHIVE_CODEX_RS 中运行:

just --set rust_min_stack 16777216 test --locked -p codex-core --test all code_mode_can_yield_and_resume_with_wait

截图只证明该 fixture、环境与版本下能够 yield、wait、resume,不能覆盖所有 nested tool、取消、超时、跨 session 调度或宿主实现。

取消、失败和所有权的窄边界

Code Mode 的故障不能用一句“脚本失败”概括。沿这条链至少有以下几个停止点:

位置固定版本的终态owner 还保留什么
recursive execcall_nested_tool 直接返回 model-facing errorcell 仍由 actor 管理,除非脚本随后结束
unknown tool / wrong payloadregistry lookup 或 kind check 失败没有 handler side effect
broker readinesscell 尚未 ready 时等待 gate;cancellation 或 channel close 才收束为 errordispatch gate 会在 cell close 时删除
not readynot ready → watch::Receiver::changed() waitreadiness gate 保留调用,等待 cell 宣布 ready
readyready → dispatch当前 turn worker 已接管 nested invocation
channel closed/cancellationchannel closed/cancellation → erroroneshot 以 terminal error 收束
active nested handlercancellation 根据 tool 的 waits_for_runtime_cancellation 选择等待 teardown 或 abortToolCallRuntime 负责 terminal outcome 去重
V8 runtime closed / panickedactor 形成带 error text 的 CompletedTerminated,并报告 failureSessionRuntime 仍清理 cell/task registry
completion commitcancellation 在写回前到达时,stored writes 被拒绝session 不会收到半个 write set
wait after close返回 MissingCell 的结构化 response不会重新执行 source

SessionRuntimeDelegate 的契约要求 callback 尊重 cancellation,且 cell_closed 只在 runtime 停止路由后调用。CellActor 结束主循环后会 tombstone state、取消并 drain notification/tool tasks,再调用 host.closed();这就是为什么一个已经 yielded 的 cell 仍有清理阶段。

本节源码依据(1 处)

nested tool 的 cancellation 还会在 ToolCallRuntime 的 dispatch task 层被观察。若工具声明需要等待 runtime cancellation,外层会等待 teardown;否则会 abort dispatch 并生成 aborted result。两种情况都不会把一个已取消的 Promise 伪装成成功的 typed result。

指定实验:先证明测试真的跑了

以下命令只在第四部“源码工作台”完成校准的 disposable archive 副本中运行,不在源码 checkout 上直接跑测试。命令就是:

: "${ARCHIVE_CODEX_RS:?先执行第四部导读的 archive 准备脚本}"
cd "$ARCHIVE_CODEX_RS"
just test --locked -p codex-core --test all code_mode_get_context_remaining_returns_structured_result

第一次运行已经命中目标测试,随后 Tokio worker 默认栈在 V8/runtime 场景中溢出:

running 1 test

thread 'tokio-rt-worker' (...) has overflowed its stack
fatal runtime error: stack overflow, aborting
error: test failed, to rerun `-p codex-core --test all`

这一步很重要:running 1 test 说明 filter 命中了目标;SIGABRT 是运行时环境失败,不能被写成通过,也不能被误判成 zero-test。

按固定版本的可复现校准,把 Tokio worker 最小栈调大后重跑:

: "${ARCHIVE_CODEX_RS:?先执行第四部导读的 archive 准备脚本}"
cd "$ARCHIVE_CODEX_RS"
just --set rust_min_stack 16777216 test --locked -p codex-core --test all code_mode_get_context_remaining_returns_structured_result

真实结果为:

PASS ... codex-core::all suite::code_mode::code_mode_get_context_remaining_returns_structured_result
Summary ... 1 test run: 1 passed

本章的实验门禁不是只看 shell exit code,而是同时检查四个信号:

  1. 输出包含完整测试名 suite::code_mode::code_mode_get_context_remaining_returns_structured_result
  2. 出现 nextest 的 PASS,不能是 zero-match。
  3. summary 包含 1 test run: 1 passed,并且没有 runtime skip。
  4. 其余被 filter 的测试不属于这次 evidence;不能把 skipped 数量当成目标 case 已执行的证明。

如果某个环境没有命中目标测试后返回 0,或者测试被 runtime skip,这次实验应标为未验证;只有上面增大栈后的真实运行才支持“Code Mode nested call 得到 structured result”这个结论。

owner / executor / governor

把这条链压成三列,后面读 Plugin 时最有用:

ownerexecutorgovernor
session / cellCodeModeServiceSessionRuntime;session 共享 stored values,cell registry 保存 live handlesInProcessCodeModeSessionCellActor、V8 runtime threadshutdown token、cell state、observer busy/missing/closed 状态
nested tool当前 turn 的 ToolRouter / ToolRegistryStepContext原有 CoreToolRuntime handler(shell、MCP、patch、memory 等按注册项决定)registry hooks、tool-specific approval/sandbox、parallel gate、cancellation
result projection每个 ToolOutput 实现to_response_itemcode_mode_result,再由 response adapter 转换source 选择、image detail policy、max_output_tokens 截断
observationCellActor 的 observer 和 SessionRuntime.cellsexec 初始观察、wait 后续观察、terminate 终止yield timer、pending frontier、cell lifecycle terminalization

这张表也解释了标题:Code Mode 可以改变“谁发起 call”(从 model-visible function call 变成 cell 内 Promise),却没有改变“谁执行 call”。执行 owner 仍是当前 turn 的 registry;Code Mode 只在两端增加了一个 cell runtime 和一个结果投影。

交给第 24 章的接口

第 24 章只接手两个接口问题:Plugin manifest 声明了哪些资源,以及宿主怎样把其中一部分交给 capability owner。这里先停在边界上。native executor 的规格由 ToolExecutor::spec() 提供,但它仍不是 Plugin manifest 的字段:

  1. native ToolContributor 直接交付 ToolExecutor;Code Mode 的 nested call 仍回到当前 turn 的 ToolRouter
  2. Plugin manifest 没有 native executor trait。它声明 Skills、MCP servers、apps 与 hooks;宿主注册的 McpServerContributor 可以消费 executor-selected roots,形成后续 MCP/package projection,但 package root 本身不会变成 ToolExecutor

因此,第 24 章会解释 ResolvedPluginLoadedPluginPluginLoadOutcome 的差别,并决定哪些资源有资格进入 planner 或其他 owner;本章不预先定义安装、启用和加载语义。

本节源码依据(4 处)