Code mode · 让模型写 JavaScript 编排工具
Code mode 给模型一个 exec 工具:模型写一段 JavaScript,在独立宿主进程里的 V8 isolate 中执行,脚本通过全局 tools 对象调用 Codex 的其它工具,中间结果留在脚本里,只把最后的文本、图片交还模型。它在 0.158.0 仍是开发中特性,由 code_mode / code_mode_only 开关或模型目录打开;宿主 codex-code-mode-host 默认经 stdin/stdout 与 Codex 通信,也能以 gRPC 服务的形式远程部署。
Code mode · 让模型写 JavaScript 编排工具
前几篇的工具都是“一次调用、一次往返”:模型要先读三个文件、再根据结果决定调哪个 MCP 工具,就得来回采样好几次,每个中间结果都流经上下文。Code mode 换了一种思路:给模型一个能跑 JavaScript 的 exec 工具,让它把编排逻辑写成程序,工具调用在程序里完成。这一篇讲它怎么开、模型看到什么、代码在哪里跑、嵌套的工具调用又怎样回到 Codex。它还在开发中,手册只在 MCP 集成(参数类型的预算)和 SDK 与 App Server(--code-mode-host)两处顺带提到。
怎么开启
Code mode 在 0.158.0 里还是开发中的特性,默认关闭,有两档:
[features] code_mode = true:工具模式变为CodeMode,原有工具照旧直接暴露,另外多出exec与wait,模型可以任选直接调用还是写脚本调用;code_mode_only = true:工具模式变为CodeModeOnly,能在脚本里调用的工具都从直接清单里撤下,模型面前只剩exec、wait和少数只许直接调用的工具(如request_user_input)。打开它会顺带打开code_mode。
更细的设置写在 [features.code_mode] 表里:default_exec_yield_time_ms(exec 默认多久让出一次,缺省 30000 毫秒)、tool_input_schema_max_bytes(渲染参数类型的预算,最少 16000 字节)、excluded_tool_namespaces(不进脚本的命名空间)、direct_only_tool_namespaces(只直接调用的命名空间)。模型目录也能直接指定模式,优先级高于开关:
pub(crate) fn requested_tool_mode(turn_context: &TurnContext, model_info: &ModelInfo) -> ToolMode {
model_info.tool_mode.unwrap_or_else(|| {
if turn_context.config.features.enabled(Feature::CodeModeOnly) {
ToolMode::CodeModeOnly
} else if turn_context.config.features.enabled(Feature::CodeMode) {
ToolMode::CodeMode
} else {
ToolMode::Direct
}
})
}
pub(crate) fn effective_tool_mode(turn_context: &TurnContext, model_info: &ModelInfo) -> ToolMode {
let requested_tool_mode = requested_tool_mode(turn_context, model_info);
if !turn_context.code_mode_available
&& requested_tool_mode == ToolMode::CodeMode
&& !turn_context.config.code_mode.disable_in_process_fallback
{
ToolMode::Direct
} else {
requested_tool_mode
}
}(codex-rs/core/src/tools/mod.rs:74)
code_mode_available 取决于能不能找到宿主程序 codex-code-mode-host(先找安装包的资源目录,再找 codex 可执行文件旁边),宿主开关 code_mode_host 默认开。找不到时,CodeMode 退回普通工具,CodeModeOnly 则“fail closed”,不退回;两种情况都会提示一次 “enable features.code_mode_host and install codex-code-mode-host”。[features.code_mode_host] 里的 disable_in_process_fallback 可以让 CodeMode 也不退回。
模型看到什么
exec 是一个 custom 工具,语法只有两部分:可选的第一行 pragma // @exec: {"yield_time_ms": 10000, "max_output_tokens": 1000},其后是 JavaScript 源码,不许包 JSON、引号或 Markdown 代码块。它的描述模板开宗明义:
const EXEC_DESCRIPTION_TEMPLATE: &str = r#"Run JavaScript code to orchestrate/compose tool calls
- Evaluates the provided JavaScript code in a fresh V8 isolate as an async module.
- All nested tools are available on the global `tools` object, for example `await tools.exec_command(...)`. Tool names are exposed as normalized JavaScript identifiers, for example `await tools.mcp__ologs__get_profile(...)`.
- Nested tool methods take either a string or an object as their input argument.
- Nested tools return either an object or a string, based on the description.
- Runs raw JavaScript -- no Node, no file system, no network access, no console.(codex-rs/code-mode-protocol/src/description.rs:20)
模板后面还列了一组全局函数:text()、image()、audio() 往结果里追加内容,store() / load() 在同一会话的多次 exec 之间存取值,notify() 立刻插入一条额外的工具输出,yield_control() 让出控制、脚本继续跑,setTimeout()、exit()、ALL_TOOLS 等。import 一律报 Unsupported import in exec。
每个可嵌套的工具都被改写了描述,末尾附一段由 JSON Schema 渲染成的 TypeScript 声明,形如 declare const tools: { exec_command(args: ...): Promise<...> },MCP 工具的返回类型写成 CallToolResult<...>。工具在脚本里的名字一般是“命名空间 + __ + 工具名”,再把 JavaScript 标识符里不允许的字符换成 _。exec 让出时返回 Script running with cell ID ...,模型用 wait(参数 cell_id、yield_time_ms、max_tokens、terminate)继续等或终止;跑完则是 Script completed 或带 Script error: 的 Script failed,结果默认截到 10000 token。
代码在哪里跑
执行涉及四个 crate:codex-code-mode-protocol 定义会话接口、宿主消息与 gRPC 协议,codex-code-mode-runtime 用 v8 crate 跑 JavaScript,codex-code-mode-host 是宿主程序,codex-code-mode 是 Codex 这一侧的客户端。内核里每个线程有一个 CodeModeService,第一次用到时才通过 CodeModeSessionProvider 建会话。默认的 ProcessOwnedCodeModeSessionProvider 把宿主作为子进程拉起:Unix 上放进独立的进程组,kill_on_drop,清掉不该继承的环境变量,再经 stdin/stdout 上的分帧协议握手(限时 30 秒),协商协议版本与能力。
宿主里的 InProcessCodeModeSession 为每个 cell 起一条系统线程、新建一个 V8 isolate 与 context。V8 全进程只初始化一次,可以关掉 JIT(--jitless);为绕开所用 V8 版本在数组排序上的一个问题,初始化时还关掉了 Maglev 等优化路径,注释里附了上游修复的链接。脚本调用 tools.x() 时,运行时先造一个 Promise,把调用作为事件发出去,结果回来再兑现:
let Some(resolver) = v8::PromiseResolver::new(scope) else {
throw_type_error(scope, "failed to create tool promise");
return;
};
let promise = resolver.get_promise(scope);
let resolver = v8::Global::new(scope, resolver);
// ...
let id = format!("tool-{}", state.next_tool_call_id);
state.next_tool_call_id = state.next_tool_call_id.saturating_add(1);
let event_tx = state.event_tx.clone();
state.pending_tool_calls.insert(id.clone(), resolver);
let _ = event_tx.send(RuntimeEvent::ToolCall {
id,
name: tool_name,
kind: tool_kind,
input,
});
retval.set(promise.into());(codex-rs/code-mode-runtime/src/runtime/callbacks.rs:40)
嵌套调用回到 Codex
事件经宿主的 RemoteDelegate 变成一条 DelegateRequest::InvokeTool 发回 Codex,落到 CodeModeCellDelegate::invoke_tool,再排进一个分派队列。每次采样开始时,run_sampling_request 会调用 start_turn_worker 为本轮起一个 worker,它有自己的 ToolCallRuntime,从队列里取出调用,交给 submit_nested_tool:
let result = tool_runtime.handle_tool_call_with_source(
step_context,
call,
ToolCallSource::CodeMode {
cell_id: cell_id.to_string(),
runtime_tool_call_id,
},
cancellation_token,
Arc::default(),
);
Ok(async move { Ok(result.await?.code_mode_result()) })(codex-rs/core/src/tools/code_mode/mod.rs:400)
也就是说,脚本里的每次工具调用都走和模型直接调用完全相同的分派链:注册表查找、hook、审批、沙箱一样不少,只是来源标成 ToolCallSource::CodeMode,结果用 code_mode_result() 转成 JSON 而不是写进历史。以 exec_command 为例,脚本拿到的是含 exit_code、session_id、output 等字段的对象。exec 不能在脚本里调用自己。一个 cell 可以跨轮存活:让出之后,它后续的工具调用等到有 worker 在跑时才会被分派。
除了默认的子进程模式,宿主还能以网络服务运行:codex-code-mode-host --listen grpc://IP:PORT 提供 gRPC 服务 CodeModeHost,app-server 可以配置成通过 GrpcCodeModeSessionProvider 连过去(要求开启 code_mode_host)。协议把会话事件、工具调用订阅与每个工具结果放在各自的 HTTP/2 流上:
service CodeModeHost {
// Opens a session lease. The first event is always SessionOpened; dropping
// this stream closes the session and terminates its active cells.
rpc OpenSession(OpenSessionRequest) returns (stream SessionEvent);
rpc CloseSession(CloseSessionRequest) returns (CloseSessionResponse);
// ...
rpc SubscribeToToolCalls(SubscribeToToolCallsRequest)
returns (stream ToolCall);
// ...
rpc CompleteToolCall(CompleteToolCallRequest)
returns (CompleteToolCallResponse);
// ...
rpc Execute(ExecuteRequest) returns (stream ExecuteEvent);
rpc Wait(WaitRequest) returns (WaitResponse);(codex-rs/code-mode-protocol/src/grpc/codex.code_mode.v1.proto:8)
文件头的注释说明了这样拆分的理由:大的工具输入输出不挤在会话事件流里,各个流可以并发推进,一个大结果不会堵住其它工具的完成或会话控制事件。
和同类实现对照
OpenCode 源码解读里的 Code Mode 走的是另一条路:自己用 Acorn 解析、树遍历解释一个为编排裁剪的 JavaScript 子集,不用 eval。Codex 则直接嵌入 V8,把完整的 JavaScript 语义放进独立的宿主进程里跑,用进程边界隔离,并允许宿主部署到别处。两者的共同点是:脚本只能碰到宿主给的 tools,授权仍由每个叶子工具自己的审批与沙箱负责。和《从 LLM 到 Coding Agent》的基本闭环相比,Code mode 把“模型发一个调用、宿主执行、结果回填”的循环挪进了模型写的程序里,减少了采样往返,也减少了流经上下文的中间结果。
上一篇:其它内置工具 · 计划、提问、权限与搜索 · 下一篇:权限模型 · 沙箱策略与审批策略