Experimental API边界
本文承接SchemaFixture与兼容测试和AppServer V2方法注册机制,面向已经理解方法宏、runtime gate 和 schema export 的读者。本文回答实验 method、variant、field 和 nested value 如何被识别、拒绝或过滤,以及稳定化需要同步改变哪些层;不讨论产品层是否应开放某项实验功能。
1. 四层门控
实验接口同时受类型标记、request runtime gate、outbound notification gate 和 stable artifact filter 约束。只修改其中一层会造成客户端契约与运行行为不一致。
2. Reason模型
源码位置:codex-rs/app-server-protocol/src/experimental_api.rs :: ExperimentalApi、ExperimentalField
pub trait ExperimentalApi {
fn experimental_reason(&self) -> Option<&'static str>;
}
pub struct ExperimentalField {
pub type_name: &'static str,
pub field_name: &'static str,
pub reason: &'static str,
}
pub fn experimental_required_message(reason: &str) -> String {
format!("{reason} requires experimentalApi capability")
}
pub fn experimental_fields() -> Vec<&'static ExperimentalField> {
inventory::iter::<ExperimentalField>.into_iter().collect()
}reason 是稳定的诊断标识,通常为 method 或 method.field。它同时服务 runtime error 和 export 过滤注册。
3. 嵌套传播
源码位置:codex-rs/app-server-protocol/src/experimental_api.rs :: ExperimentalApi for Option/Vec/Map
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)
}
}
impl<K, V: ExperimentalApi, S> ExperimentalApi for HashMap<K, V, S> {
fn experimental_reason(&self) -> Option<&'static str> {
self.values().find_map(ExperimentalApi::experimental_reason)
}
}嵌套 field、collection 和 map 会传播第一个实验 reason。空 collection 没有 reason;带 Some(empty vec) 的直接实验字段仍可被 derive 标记为实验使用。
4. Request拒绝
源码位置:codex-rs/app-server/src/message_processor.rs :: dispatch_initialized_client_request
if let Some(reason) = codex_request.experimental_reason()
&& !session.experimental_api_enabled()
{
return Err(invalid_request(experimental_required_message(reason)));
}request 已成功反序列化,但在业务 handler 前被拒绝,因此不会产生 method 副作用。客户端 capability 是 connection/request context,而不是全局 feature flag。
5. Notification过滤
源码位置:codex-rs/app-server/src/transport.rs :: should_skip_notification_for_connection
fn should_skip_notification_for_connection(
connection_state: &OutboundConnectionState,
message: &OutgoingMessage,
) -> bool {
match message {
OutgoingMessage::AppServerNotification(envelope) => {
envelope.notification.experimental_reason().is_some()
&& !connection_state
.experimental_api_enabled
.load(Ordering::Acquire)
}
_ => false,
}
}实验 notification 在 transport 层按连接能力抑制;server 内部仍可能产生事件。缺少 notification 不代表 core 状态没有变化。
同一层还会对实验审批字段做出站裁剪:filter_outgoing_message_for_connection 在 capability 缺失时调用 strip_experimental_fields,所以 notification 的“整条消息丢弃”和 request 的“保留主体、移除实验字段”是两种不同策略。
6. Stable导出
源码位置:codex-rs/app-server-protocol/src/export.rs :: filter_experimental_ts、filter_experimental_schema
filter_request_ts(out_dir, "ClientRequest.ts", EXPERIMENTAL_CLIENT_METHODS)?;
filter_experimental_type_fields_ts(out_dir, ®istered_fields)?;
remove_generated_type_files(out_dir, &experimental_method_types, "ts")?;
filter_experimental_fields_in_root(bundle, ®istered_fields);
prune_experimental_methods(bundle, EXPERIMENTAL_CLIENT_METHODS);stable artifact 删除 union arm、字段和专属类型文件;experimental artifact 保留全部定义。stable 客户端在类型层根本看不到这些接口。
7. 稳定化路径
一个接口稳定化需要移除 attribute/reason、从实验 method/type 表退出、更新 stable fixture,并确认 runtime 不再要求 capability。仅把 schema 放进 stable exports 而保留 runtime gate,会制造“能编译但调用被拒绝”的接口;这条失败路径必须在稳定化前清理。
8. 源码验证
derive 测试覆盖 enum unit/tuple/named、nested option/vec/map 和 optional collection;common protocol 测试验证 method/field reason;message processor 测试验证 capability 缺失时拒绝;fixture 测试验证 stable/experimental 输出差异。它们证明门控一致性,不证明实验接口本身稳定或适合公开。
源码位置:
codex-rs/app-server-protocol/src/experimental_api.rs :: derive_supports_*codex-rs/app-server-protocol/src/protocol/common.rs :: experimental_reason testscodex-rs/app-server/src/message_processor.rs :: experimentalApi gate testscodex-rs/app-server-protocol/src/schema_fixtures_tests.rs :: experimental_precomputed_exports_match_generated
cd codex-rs
cargo test -p codex-app-server-protocol experimental_api
cargo test -p codex-app-server experimentalApi
cargo test -p codex-app-server-protocol experimental_precomputed_exports源码位置:codex-rs/app-server/src/request_processors/initialize_processor.rs :: initialize
let capabilities = params.capabilities.unwrap_or_default();
let experimental_api_enabled = capabilities.experimental_api;
let opt_out_notification_methods = capabilities
.opt_out_notification_methods
.unwrap_or_default();
session.initialize(InitializedConnectionSessionState {
experimental_api_enabled,
opted_out_notification_methods: opt_out_notification_methods.into_iter().collect(),
app_server_client_name: name.clone(),
client_version: version,
request_attestation,
client_mcp_extensions,
})?;能力是在 initialize 时写入连接 session,而不是每个请求临时猜测。这个状态同时被 request gate 和 outbound transport 使用;因此同一条连接初始化后,实验请求和实验通知会遵循同一个 capability 快照。
9. 门控排查
遇到类型里没有接口,检查 stable export 过滤;请求被拒绝时查看 reason 与 connection capability;状态变化但没 notification 时检查 transport gate;稳定化后仍要求 capability,则逐层检查 marker、method 表、runtime 和 fixture 是否同步更新。
