CodeMode协议
Code Mode 不是只有一套“发 source、收 result”的 wire 格式。当前代码先定义 transport-neutral 的 domain API:CodeModeSession、ExecuteRequest、StartedCell、RuntimeResponse 和 delegate。其下有两套远程 承载:process/WebSocket 使用 Protocol V1 的长度前缀 JSON message,可协商 control/bulk 双 socket;gRPC 则用 session lease、独立 Execute stream、tool subscription 和 unary completion 分离大 payload。两套 transport 最终都必须还原成同一 domain 行为。
本文承接CodeMode架构总览和ExecServer消息模型。 范围是 session、cell、execute/wait、delegate、取消、ID 和 transport lane;V8 内部执行不在本文范围。 读完后,你应能区分 domain ID 与 wire ID,解释 execute 为什么有 admission 与 initial outcome 两个时刻, 并判断一个 tool callback、notification、wait cancel 或 cell close 走哪条消息通道。
1. Domain契约
1.1 Session接口
CodeModeSession 是 Core 依赖的稳定接口。execute 返回 StartedCell,其中 cell ID 已经确定,但 initial response 仍是 future;wait 和 terminate 返回 WaitOutcome,missing cell 被建模为 outcome 而不一定 是 transport error。shutdown 关闭整个逻辑 session。
源码位置:codex-rs/code-mode-protocol/src/session.rs :: CodeModeSession、StartedCell
pub trait CodeModeSession: Send + Sync {
fn execute<'a>(
&'a self,
request: ExecuteRequest,
) -> CodeModeSessionResultFuture<'a, StartedCell>;
fn wait<'a>(
&'a self,
request: WaitRequest,
) -> CodeModeSessionResultFuture<'a, WaitOutcome>;
fn terminate<'a>(
&'a self,
cell_id: CellId,
) -> CodeModeSessionResultFuture<'a, WaitOutcome>;
fn shutdown<'a>(&'a self) -> CodeModeSessionResultFuture<'a, ()>;
}
pub struct StartedCell {
pub cell_id: CellId,
initial_response: CodeModeSessionResultFuture<'static, RuntimeResponse>,
}StartedCell 的两阶段设计允许 host 在 cell admission 后立即返回 ID,同时让 JavaScript 继续运行到 yield/result/termination frontier。调用方取消 initial future 时,transport 实现必须清理已经 admitted 的 remote cell,不能只丢弃 receiver。
1.2 Execute与结果
ExecuteRequest 携带外层 tool call ID、enabled tool definitions、source、yield 时间和输出预算。 RuntimeResponse 分为 Yielded、Terminated、Result;Result 的 error_text 表达脚本错误,transport error 则通过外层 Result<_, String> 返回。
源码位置:codex-rs/code-mode-protocol/src/runtime.rs :: ExecuteRequest、RuntimeResponse、CodeModeNestedToolCall
pub struct ExecuteRequest {
pub tool_call_id: String,
pub enabled_tools: Vec<ToolDefinition>,
pub source: String,
pub yield_time_ms: Option<u64>,
pub max_output_tokens: Option<usize>,
}
pub enum RuntimeResponse {
Yielded {
cell_id: CellId,
content_items: Vec<FunctionCallOutputContentItem>,
},
Terminated {
cell_id: CellId,
content_items: Vec<FunctionCallOutputContentItem>,
},
Result {
cell_id: CellId,
content_items: Vec<FunctionCallOutputContentItem>,
error_text: Option<String>,
},
}1.3 Delegate接口
host callback 使用 CodeModeSessionDelegate:nested tool 返回 JSON,notification 只返回 delivered/error, cell_closed 释放 client 侧该 cell 的状态。CodeModeNestedToolCall 同时携带 domain CellId、runtime tool call ID、逻辑 ToolName namespace、function/freeform kind 和可选 JSON input。
源码位置:
codex-rs/code-mode-protocol/src/session.rs::CodeModeSessionDelegatecodex-rs/code-mode-protocol/src/runtime.rs::CodeModeNestedToolCall
2. Host V1握手
process-owned 与 WebSocket provider 使用 host 模块的 Protocol V1。client hello 提供非空版本集合、 required capabilities 和 optional capabilities;同一 capability 不能同时出现在 required/optional。host 选择版本并返回实际 capabilities,或用结构化 reason 拒绝连接。
源码位置:
codex-rs/code-mode-protocol/src/host/message.rs::ClientHello、HostHellocodex-rs/code-mode-protocol/src/host/error.rs::HandshakeRejectReasoncodex-rs/code-mode-protocol/src/host/types.rs::SupportedProtocolVersions、CapabilitySet
pub fn new(
supported_versions: SupportedProtocolVersions,
required_capabilities: CapabilitySet,
optional_capabilities: CapabilitySet,
) -> Result<Self, ClientHelloError> {
if let Some(capability) = required_capabilities
.iter()
.find(|capability| optional_capabilities.contains(capability))
{
return Err(ClientHelloError::OverlappingCapability(capability.clone()));
}
Ok(Self {
supported_versions,
required_capabilities,
optional_capabilities,
})
}当前已命名 capability 包括 dual-websocket-v1 和 session-cell-execution-resource-limits。前者允许 WebSocket peers 建立第二条 bulk connection;后者允许 session/open 携带 max yield/heap limits。required capability 缺失时必须拒绝,而 optional capability 可被忽略。
3. JSON消息族
3.1 外层Envelope
V1 的 client/host message 都是 serde tagged enum,并对未知字段使用 deny_unknown_fields。ClientToHost 包含 hello、operation request/cancel、delegate response;HostToClient 包含 ready/rejected、operation response、execute initial response、delegate request/cancel 和 cell closed。
源码位置:codex-rs/code-mode-protocol/src/host/message.rs :: ClientToHost、HostToClient
pub enum ClientToHost {
ClientHello(ClientHello),
Request { id: RequestId, request: HostRequest },
CancelRequest { id: RequestId },
DelegateResponse {
id: DelegateRequestId,
result: WireResult<DelegateResponse>,
},
}
pub enum HostToClient {
HostHello(HostHello),
HandshakeRejected { reason: HandshakeRejectReason },
Response { id: RequestId, result: WireResult<HostResponse> },
InitialResponse { id: RequestId, result: WireResult<WireRuntimeResponse> },
DelegateRequest {
id: DelegateRequestId,
session_id: SessionId,
request: DelegateRequest,
},
CancelDelegateRequest { id: DelegateRequestId },
CellClosed { session_id: SessionId, cell_id: WireCellId },
}RequestId 只关联 client 发起的 host operation;DelegateRequestId 只关联 host 发起的 callback,二者 不能混用。SessionId 和 capability 名由 NonEmptyString 保证非空;ProtocolVersion 使用 NonZeroU32,版本 0 无法构造。
3.2 Session操作
HostRequest 包含 open、execute、wait、terminate、shutdown。Execute 的 operation response 只是 ExecutionStarted { cell_id };当 runtime 到达 frontier 时,host 再用相同 RequestId 发送 execute/initialResponse。Wait 则以 WaitCompleted 一次返回 live/missing outcome。
源码位置:codex-rs/code-mode-protocol/src/host/message.rs :: HostRequest、HostResponse
pub enum HostRequest {
OpenSession {
session_id: SessionId,
cell_execution_limits: Option<WireSessionCellExecutionLimits>,
},
Execute { session_id: SessionId, request: WireExecuteRequest },
Wait { session_id: SessionId, request: WireWaitRequest },
Terminate { session_id: SessionId, cell_id: WireCellId },
ShutdownSession { session_id: SessionId },
}
pub enum HostResponse {
SessionReady { session_id: SessionId },
ExecutionStarted { cell_id: WireCellId },
WaitCompleted { outcome: WireWaitOutcome },
SessionClosed { session_id: SessionId },
}3.3 Wire转换
domain 与 V1 wire 类型分离。比如 domain max_output_tokens: Option<usize> 在 V1 变成 Option<i32>, heap limit 在 wire 为 u64;转换使用 TryFrom,越界不是截断。每个 tool definition 和 content item 也 使用 deny_unknown_fields 的专用 wire 类型。
源码位置:codex-rs/code-mode-protocol/src/host/payload.rs :: WireExecuteRequest、WireSessionCellExecutionLimits
impl TryFrom<ExecuteRequest> for WireExecuteRequest {
type Error = TryFromIntError;
fn try_from(value: ExecuteRequest) -> Result<Self, Self::Error> {
Ok(Self {
tool_call_id: value.tool_call_id,
enabled_tools: value.enabled_tools.into_iter().map(Into::into).collect(),
source: value.source,
yield_time_ms: value.yield_time_ms,
max_output_tokens: value.max_output_tokens.map(i32::try_from).transpose()?,
})
}
}4. Framing与Lane
stdio/process transport 使用四字节 little-endian payload 长度加 JSON,单 frame 最大 64 MiB。EOF 只有 发生在 frame 边界才是正常结束;截断 prefix、声明长度与 payload 不符、超限或 malformed JSON 都是 connection error。
源码位置:codex-rs/code-mode-protocol/src/host/codec.rs :: EncodedFrame、FramedReader、FramedWriter
pub const MAX_FRAME_BYTES: usize = 64 * 1024 * 1024;
pub fn into_framed_bytes(self) -> Vec<u8> {
let mut bytes = Vec::with_capacity(size_of::<u32>() + self.payload.len());
bytes.extend_from_slice(&(self.payload.len() as u32).to_le_bytes());
bytes.extend_from_slice(&self.payload);
bytes
}dual WebSocket 按 message family 固定 lane。host 发出的 nested tool invoke 和 delegate cancel 属于 bulk; notification、operation response、initial response、cell close 属于 control。client 返回 tool result 走 bulk,notification delivered acknowledgment 走 control。lane 不由 payload 大小临时选择,错误 socket 上的 消息必须拒绝。
源码位置:codex-rs/code-mode-protocol/src/host/message.rs :: ClientToHost::transport_lane、HostToClient::transport_lane
match self {
HostToClient::DelegateRequest {
request: DelegateRequest::InvokeTool { .. },
..
}
| HostToClient::CancelDelegateRequest { .. } => TransportLane::Bulk,
HostToClient::DelegateRequest {
request: DelegateRequest::Notify { .. },
..
}
| HostToClient::HostHello(_)
| HostToClient::HandshakeRejected { .. }
| HostToClient::Response { .. }
| HostToClient::InitialResponse { .. }
| HostToClient::CellClosed { .. } => TransportLane::Control,
}host connection 最多允许 1024 个未完成 delegate callback,避免 JavaScript 无限制造未解决 Promise。
5. Delegate与取消
DelegateRequest::InvokeTool 携带 WireNestedToolCall,Notify 携带外层 call ID、cell ID 和 text。 DelegateResponse 要么是 JSON tool result,要么是 notification delivered。WireResult 将 transport 成功 与业务错误显式区分,不依赖空对象或异常断连。
源码位置:codex-rs/code-mode-protocol/src/host/message.rs :: DelegateRequest、DelegateResponse、WireResult
pub enum DelegateRequest {
InvokeTool { invocation: WireNestedToolCall },
Notify { call_id: String, cell_id: WireCellId, text: String },
}
pub enum DelegateResponse {
ToolResult { result: JsonValue },
NotificationDelivered,
}
pub enum WireResult<T> {
Ok { value: T },
Err { message: String },
}取消方向必须分清:client 的 operation/cancel 取消 execute/wait 等 host operation;host 的 delegate/cancel 撤销 nested callback;cell/closed 让 client 释放 cell 状态。取消 message 表示请求 撤销意图,真正 V8 isolate、callback task 和 store commit 的终止规则由 runtime/host 实现。
6. gRPC契约
gRPC 不复用 ClientHello/HostHello JSON envelope,而由 HTTP/2 endpoint 和 protobuf service 建立连接。 OpenSession 返回 session lease stream:第一个 event 必须是 SessionOpened,lease 被 drop 时 host 关闭 session 和 active cells。tool calls 使用独立 subscription stream,大结果通过独立 CompleteToolCall unary RPC 返回,避免阻塞 session control stream。
源码位置:codex-rs/code-mode-protocol/src/grpc/codex.code_mode.v1.proto :: service CodeModeHost
service CodeModeHost {
rpc OpenSession(OpenSessionRequest) returns (stream SessionEvent);
rpc CloseSession(CloseSessionRequest) returns (CloseSessionResponse);
rpc SubscribeToToolCalls(SubscribeToToolCallsRequest) returns (stream ToolCall);
rpc CompleteToolCall(CompleteToolCallRequest) returns (CompleteToolCallResponse);
rpc AcknowledgeNotification(AcknowledgeNotificationRequest)
returns (AcknowledgeNotificationResponse);
rpc Execute(ExecuteRequest) returns (stream ExecuteEvent);
rpc Wait(WaitRequest) returns (WaitResponse);
rpc CancelWait(CancelWaitRequest) returns (CancelWaitResponse);
rpc Terminate(TerminateRequest) returns (WaitResponse);
}Execute stream 与 V1 两阶段语义对应:先 ExecutionStarted,再一个 ExecutionOutcome。gRPC 增加 client 选择的 execution_id,使 tool callback 可以在 started event 到达前关联 execute;每个 execution 的 tool call sequence 从 1 单调增长。CellClosed 带 final_tool_call_sequence,client 可拒绝 closure 后才到达的 迟到 callback。
Wait 使用独立 wait_id。取消 wait 后,client 调 CancelWait 并等待 host 确认 observer 已退休,之后才能 安全启动下一次 wait;这避免被取消的旧 observer 抢走新 wait 的 outcome。
7. Generation映射
gRPC cached session 可以在 host 重启后创建新 binding。client 为第二代及以后公开 cell ID 添加 generation 前缀,并在发请求时剥离为当前 wire cell ID;旧 generation ID 在 reconnect 后必须拒绝。delegate callback 也绑定 generation,避免旧 host 的迟到消息进入新 session。
源码位置:codex-rs/code-mode/src/grpc_session/generation.rs :: generation ID mapping
first generation: opaque-host-cell-id
later generation: g2:opaque-host-cell-id这层映射属于 gRPC client state,不改变 domain CellId 的字符串封装,也不出现在 V1 JSON host 协议。
8. 源码验证
codex-code-mode-protocol 的 37 项纯协议测试覆盖:exec source pragma、schema bounds、frame round-trip 与 截断、V1 message 固定形状、未知字段拒绝、lane、capability、limits 和 StartedCell error preservation。
源码位置:
codex-rs/code-mode-protocol/src/host/codec_tests.rscodex-rs/code-mode-protocol/src/host/host_tests.rscodex-rs/code-mode-protocol/src/session_tests.rscodex-rs/code-mode-protocol/src/json_schema_types_tests.rs
cd codex-rs
cargo test -p codex-code-mode-protocol --lib -- --test-threads=1codex-code-mode 的 70 项测试覆盖 gRPC conversion/deadline/generation/state,以及 V1 process/WebSocket driver 的 request tracking、delegate limits、wrong response、wait cancellation、connection death 和 shared host reuse。这些测试不需要启动 V8 runtime。
源码位置:
codex-rs/code-mode/src/grpc_sessioncodex-rs/code-mode/src/remote_session/connection/driver_tests.rscodex-rs/code-mode/src/remote_session_tests.rs
cargo test -p codex-code-mode --lib -- --test-threads=1这 107 项测试验证协议和 client state,但不证明 V8 实际执行结果或 host 内 runtime 行为。后者依赖 codex-code-mode-runtime/codex-code-mode-host 测试;在缺少对应 V8 构建产物时应保持这个边界,而 不能用 codec round-trip 代替执行验证。
下一篇CodeModeHost生命周期将追踪 host 进程、connection driver、session generation、重连与 shutdown 的资源所有权。
