Skip to content

CodeMode协议

解析Code Mode的domain session API、V1长度前缀JSON host协议、双WebSocket lane与gRPC lease/stream契约。

基于rust-v0.150.0
CodexRustExecutionCodeModeProtocol

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

rust
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

rust
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 :: CodeModeSessionDelegate
  • codex-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、HostHello
  • codex-rs/code-mode-protocol/src/host/error.rs :: HandshakeRejectReason
  • codex-rs/code-mode-protocol/src/host/types.rs :: SupportedProtocolVersions、CapabilitySet
rust
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

rust
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

rust
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

rust
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

rust
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

rust
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

rust
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

protobuf
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

text
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.rs
  • codex-rs/code-mode-protocol/src/host/host_tests.rs
  • codex-rs/code-mode-protocol/src/session_tests.rs
  • codex-rs/code-mode-protocol/src/json_schema_types_tests.rs
text
cd codex-rs
cargo test -p codex-code-mode-protocol --lib -- --test-threads=1

codex-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_session
  • codex-rs/code-mode/src/remote_session/connection/driver_tests.rs
  • codex-rs/code-mode/src/remote_session_tests.rs
text
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 的资源所有权。