Skip to content

AppServer V2方法注册机制

通过协议宏、方法表、作用域和 schema 导出,理解 App Server V2 方法如何从一份定义生成完整类型边界。

基于rust-v0.150.0
CodexRustProtocolAppServerJSON-RPC

AppServer V2方法注册机制 ​

本文承接JSON-RPC封装与错误模型。App Server 的 V2 方法不是在每个 handler 中手写一套 request、response、wire method 和 schema;app-server-protocol/src/protocol/common.rs 使用声明宏,把方法定义展开为 typed enum、ID 访问器、method name、参数作用域、反向请求构造和 schema 导出辅助函数。

阅读这套机制时,要区分三种“注册”:宏调用中的方法表注册、运行时 ClientRequest/ServerRequest 枚举注册,以及 schema/experimental API 中的导出注册。它们共享一份声明,但作用时间不同。

1. Client方法表 ​

源码位置:codex-rs/app-server-protocol/src/protocol/common.rs :: client_request_definitions!

rust
macro_rules! client_request_definitions {
    (
        $(
            $(#[experimental($reason:expr)])?
            $(#[doc = $variant_doc:literal])*
            $variant:ident => $wire:literal {
                params: $(#[$params_meta:meta])* $params:ty,
                $(inspect_params: $inspect_params:tt,)?
                serialization: $serialization:ident $( ( $($serialization_args:tt)* ) )?,
                $(manual_payload_conversion: $manual_payload_conversion:ident,)?
                response: $response:ty,
            }
        ),* $(,)?
    ) => {
        /// Request from the client to the server.
        #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
        #[serde(tag = "method", rename_all = "camelCase")]
        pub enum ClientRequest {
            $(
                $(#[doc = $variant_doc])*
                #[serde(rename = $wire)]
                #[ts(rename = $wire)]
                $variant {
                    #[serde(rename = "id")]
                    request_id: RequestId,
                    $(#[$params_meta])*
                    params: $params,
                },
            )*
        }
    };
}

每个条目至少绑定 variant、wire method、params、serialization scope 和 response。inspect_params 允许一个总体稳定的方法根据参数中的实验字段继续判断;manual_payload_conversion 则为少数不能自动生成 payload conversion 的响应保留出口。

2. ClientRequest ​

源码位置:codex-rs/app-server-protocol/src/protocol/common.rs :: client_request_definitions! 展开的 ClientRequest

宏源码使用 $variant、$params 等 metavariable;真正编译出的 enum 会由每个条目替换这些变量。关键点是 wire method 作为 serde tag,request ID 作为字段,具体参数类型仍由 method 条目决定。

源码位置:codex-rs/app-server-protocol/src/protocol/common.rs :: ClientRequest::id、method_name

rust
pub fn id(&self) -> &RequestId {
    match self {
        $(Self::$variant { request_id, .. } => request_id,)*
    }
}

pub const fn method_name(&self) -> &'static str {
    match self {
        $(Self::$variant { .. } => $wire,)*
    }
}

任何新增方法都会自动获得 ID 和 wire name 访问器;handler 不需要自行维护字符串到 variant 的第二张表。

3. JSON到请求 ​

源码位置:codex-rs/app-server-protocol/src/protocol/common.rs :: TryFrom<JSONRPCRequest> for ClientRequest

rust
impl TryFrom<JSONRPCRequest> for ClientRequest {
    type Error = serde_json::Error;

    fn try_from(request: JSONRPCRequest) -> Result<Self, Self::Error> {
        let JSONRPCRequest {
            id: request_id,
            method,
            params,
            trace: _,
        } = request;
        let mut request = serde_json::Map::new();
        request.insert("id".to_string(), serde_json::to_value(request_id)?);
        request.insert("method".to_string(), serde_json::Value::String(method));
        if let Some(params) = params {
            request.insert("params".to_string(), params);
        }
        serde_json::from_value(serde_json::Value::Object(request))
    }
}

这里把通用 envelope 重建成宏生成 enum 所需的 JSON 形状,再由 serde 根据 method 解码具体 params。trace 不进入 enum,是因为它已经在上层 RequestContext 中用于 tracing。

4. 参数作用域 ​

源码位置:codex-rs/app-server-protocol/src/protocol/common.rs :: ClientRequestSerializationScope、serialization_scope_expr!

rust
pub enum ClientRequestSerializationScope {
    Global(&'static str),
    GlobalSharedRead(&'static str),
    Thread { thread_id: String },
    ThreadPath { path: PathBuf },
    CommandExecProcess { process_id: String },
    Process { process_handle: String },
    FuzzyFileSearchSession { session_id: String },
    FsWatch { watch_id: String },
    McpOauth { server_name: String },
}

作用域不是权限判断,而是 App Server 决定请求应发送到哪些连接或由哪个共享状态处理的路由提示。宏把 thread_id(...)、thread_or_path(...)、process_handle(...) 等声明转换成统一的 enum,避免各 method handler 自己解释字段。

源码位置:codex-rs/app-server-protocol/src/protocol/common.rs :: serialization_scope_expr! 的 thread/process 分支

rust
($actual_params:ident, thread_id($params:ident . $field:ident)) => {
    Some(ClientRequestSerializationScope::Thread {
        thread_id: $actual_params.$field.clone(),
    })
};
($actual_params:ident, thread_or_path($params:ident . $thread_field:ident, $params2:ident . $path_field:ident)) => {
    if !$actual_params.$thread_field.is_empty() {
        Some(ClientRequestSerializationScope::Thread {
            thread_id: $actual_params.$thread_field.clone(),
        })
    } else if let Some(path) = $actual_params.$path_field.clone() {
        Some(ClientRequestSerializationScope::ThreadPath { path })
    } else {
        Some(ClientRequestSerializationScope::Thread {
            thread_id: $actual_params.$thread_field.clone(),
        })
    }
};
($actual_params:ident, process_handle($params:ident . $field:ident)) => {
    Some(ClientRequestSerializationScope::Process {
        process_handle: $actual_params.$field.clone(),
    })
};

thread_or_path 的 fallback 保留空 thread ID,而不是静默返回 None;调用方可以据此发现请求既没有有效线程 ID 也没有 rollout path。

5. 实际方法声明 ​

源码位置:codex-rs/app-server-protocol/src/protocol/common.rs :: client_request_definitions! 调用中的 thread methods

rust
client_request_definitions! {
    Initialize => "initialize" {
        params: v1::InitializeParams,
        serialization: None,
        response: v1::InitializeResponse,
    },
    ThreadStart => "thread/start" {
        params: v2::ThreadStartParams,
        inspect_params: true,
        serialization: None,
        response: v2::ThreadStartResponse,
    },
    ThreadResume => "thread/resume" {
        params: v2::ThreadResumeParams,
        inspect_params: true,
        serialization: thread_or_path(params.thread_id, params.path),
        response: v2::ThreadResumeResponse,
    },
    ThreadArchive => "thread/archive" {
        params: v2::ThreadArchiveParams,
        serialization: thread_id(params.thread_id),
        response: v2::ThreadArchiveResponse,
    },
}

这个表同时描述稳定方法和带参数实验字段的方法。ThreadResume 可能由 thread ID 或 rollout path 定位;ThreadArchive 只接受 thread ID。路由差异在声明层可见,运行时 helper 自动保持一致。

6. Response封装 ​

源码位置:codex-rs/app-server-protocol/src/protocol/common.rs :: ClientResponse、ClientResponsePayload

rust
pub enum ClientResponse {
    $(
        $(#[doc = $variant_doc])*
        #[serde(rename = $wire)]
        $variant {
        #[serde(rename = "id")]
        request_id: RequestId,
        response: $response,
        },
    )*
}

impl ClientResponse {
    pub fn id(&self) -> &RequestId {
        match self {
            $(Self::$variant { request_id, .. } => request_id,)*
        }
    }

    pub fn into_jsonrpc_parts(
        self,
    ) -> std::result::Result<(RequestId, crate::Result), serde_json::Error> {
        match self {
            $(Self::$variant { request_id, response } => {
                serde_json::to_value(response).map(|result| (request_id, result))
            }),*
        }
    }
}

Response payload 也由同一份 method table 生成,ID 不会在 handler 返回 typed response 后丢失。ClientResponsePayload::to_jsonrpc_parts 将具体 response 序列化为通用 JSON result,供 transport 层构造 envelope。

7. Server反向请求 ​

源码位置:codex-rs/app-server-protocol/src/protocol/common.rs :: server_request_definitions!

rust
macro_rules! server_request_definitions {
    (
        $(
            $(#[experimental($reason:expr)])?
            $(#[doc = $variant_doc:literal])*
            $variant:ident $(=> $wire:literal)? {
                params: $params:ty,
                response: $response:ty,
            }
        ),* $(,)?
    ) => {
        /// Request initiated from the server and sent to the client.
        #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
        #[allow(clippy::large_enum_variant)]
        #[serde(tag = "method", rename_all = "camelCase")]
        pub enum ServerRequest {
            $(
                $(#[doc = $variant_doc])*
                $(#[serde(rename = $wire)] #[ts(rename = $wire)])?
                $variant {
                    #[serde(rename = "id")]
                    request_id: RequestId,
                    params: $params,
                },
            )*
        }
    };
}

服务器发给客户端的 request 使用另一张 method table,因为有些 client callback 没有对应的 incoming method。当前条目包含 command approval、file change approval、tool input、MCP elicitation、permissions request 等反向请求。

源码位置:codex-rs/app-server-protocol/src/protocol/common.rs :: ServerRequest::response_from_result、ServerRequestPayload::request_with_id

rust
pub fn response_from_result(
    &self,
    result: crate::Result,
) -> serde_json::Result<ServerResponse> {
    match self {
        $(Self::$variant { request_id, .. } => {
            let response = serde_json::from_value::<$response>(result)?;
            Ok(ServerResponse::$variant {
                request_id: request_id.clone(),
                response,
            })
        }),*
    }
}

pub fn request_with_id(self, request_id: RequestId) -> ServerRequest {
    match self {
        $(Self::$variant(params) => ServerRequest::$variant { request_id, params },)*
    }
}

反向请求先由 payload enum 携带 params,分配 ID 后变成 ServerRequest;客户端结果再按照原 request variant 的 response 类型解码。类型错误发生在 callback 完成前,不会被误当成业务成功。

8. 实验能力与schema ​

源码位置:codex-rs/app-server-protocol/src/protocol/common.rs :: experimental_reason_expr!、ExperimentalApi 实现

rust
macro_rules! experimental_reason_expr {
    (variant $variant:ident, #[experimental($reason:expr)] $params:ident $(, $inspect_params:tt)?) => {
        Some($reason)
    };
    (variant $variant:ident, $params:ident, true) => {
        crate::experimental_api::ExperimentalApi::experimental_reason($params)
    };
    (variant $variant:ident, $params:ident $(, $inspect_params:tt)?) => {
        None
    };
}

方法级实验标记直接返回 reason;参数级检查递归调用参数类型的 ExperimentalApi。因此一个总体稳定的方法可以因为某个字段而被标记实验,客户端能力协商不必复制字段判断逻辑。

源码位置:codex-rs/app-server-protocol/src/experimental_api.rs :: ExperimentalApi、experimental_required_message

rust
pub trait ExperimentalApi {
    fn experimental_reason(&self) -> Option<&'static str>;
}

pub fn experimental_required_message(reason: &str) -> String {
    format!("{reason} requires experimentalApi capability")
}

impl<T: ExperimentalApi> ExperimentalApi for Option<T> {
    fn experimental_reason(&self) -> Option<&'static str> {
        self.as_ref().and_then(ExperimentalApi::experimental_reason)
    }
}

impl<T: ExperimentalApi> ExperimentalApi for Vec<T> {
    fn experimental_reason(&self) -> Option<&'static str> {
        self.iter().find_map(ExperimentalApi::experimental_reason)
    }
}

schema 导出函数会遍历宏生成的 request/response types,并按 stable/experimental API 选择输出集合。schema fixture 不是运行时 handler 测试,但它能发现 method、字段和生成文件之间的不一致。

9. 测试与边界 ​

源码位置:codex-rs/app-server-protocol/src/protocol/common.rs :: jsonrpc_request_conversion_preserves_serde_enum_decoding、client_request_serialization_scope_covers_keyed_families

源码位置:codex-rs/app-server-protocol/src/protocol/common_tests.rs :: client_response_payload_serializes_without_an_intermediate_json_value

源码位置:codex-rs/app-server-protocol/src/schema_fixtures_tests.rs :: typescript_schema_fixtures_match_generated、json_schema_fixtures_match_generated、stable_precomputed_exports_match_schema_fixtures

text
cd codex-rs
cargo test -p codex-app-server-protocol jsonrpc_request_conversion_preserves_serde_enum_decoding -- --nocapture --test-threads=1
cargo test -p codex-app-server-protocol client_request_serialization_scope_covers_keyed_families -- --nocapture --test-threads=1
cargo test -p codex-app-server-protocol client_response_payload_serializes_without_an_intermediate_json_value -- --nocapture --test-threads=1
cargo test -p codex-app-server-protocol typescript_schema_fixtures_match_generated -- --nocapture --test-threads=1
cargo test -p codex-app-server-protocol json_schema_fixtures_match_generated -- --nocapture --test-threads=1
cargo test -p codex-app-server-protocol stable_precomputed_exports_match_schema_fixtures -- --nocapture --test-threads=1

这些测试说明 typed request/response 转换、路由作用域和 schema 生成一致性;不能证明每个 method 的业务 handler 成功,也不能证明实验字段在所有客户端都被正确展示。

10. 源码定位练习 ​

遇到“方法名能解析但参数错误”,先看 method table 中的 params type 和 ClientRequest::try_from;遇到“请求发到了错误连接”,检查 serialization scope,而不是修改 handler 内的 thread lookup。

遇到“客户端收到了未知实验字段”,检查 experimental_reason_expr!、initialize capability 和 schema fixture;遇到“反向请求无响应”,沿 ServerRequestPayload::request_with_id、pending callback、response_from_result 和 connection cleanup 逐层定位。

App Server 方法注册的价值在于消除平行协议表:wire method、typed params/response、ID 关联、路由作用域、实验能力和 schema 导出都从同一份声明生成。理解这份声明如何展开,比记住某个具体方法的 JSON 形状更能帮助你阅读后续协议代码。