core-agent-ide/codex-rs/app-server-protocol/src/experimental_api.rs
jif-oai 3cc9122ee2
feat: experimental flags (#10231)
## Problem being solved
- We need a single, reliable way to mark app-server API surface as
experimental so that:
  1. the runtime can reject experimental usage unless the client opts in
2. generated TS/JSON schemas can exclude experimental methods/fields for
stable clients.

Right now that’s easy to drift or miss when done ad-hoc.

## How to declare experimental methods and fields
- **Experimental method**: add `#[experimental("method/name")]` to the
`ClientRequest` variant in `client_request_definitions!`.
- **Experimental field**: on the params struct, derive `ExperimentalApi`
and annotate the field with `#[experimental("method/name.field")]` + set
`inspect_params: true` for the method variant so
`ClientRequest::experimental_reason()` inspects params for experimental
fields.

## How the macro solves it
- The new derive macro lives in
`codex-rs/codex-experimental-api-macros/src/lib.rs` and is used via
`#[derive(ExperimentalApi)]` plus `#[experimental("reason")]`
attributes.
- **Structs**:
- Generates `ExperimentalApi::experimental_reason(&self)` that checks
only annotated fields.
  - The “presence” check is type-aware:
    - `Option<T>`: `is_some_and(...)` recursively checks inner.
    - `Vec`/`HashMap`/`BTreeMap`: must be non-empty.
    - `bool`: must be `true`.
    - Other types: considered present (returns `true`).
- Registers each experimental field in an `inventory` with `(type_name,
serialized field name, reason)` and exposes `EXPERIMENTAL_FIELDS` for
that type. Field names are converted from `snake_case` to `camelCase`
for schema/TS filtering.
- **Enums**:
- Generates an exhaustive `match` returning `Some(reason)` for annotated
variants and `None` otherwise (no wildcard arm).
- **Wiring**:
- Runtime gating uses `ExperimentalApi::experimental_reason()` in
`codex-rs/app-server/src/message_processor.rs` to reject requests unless
`InitializeParams.capabilities.experimental_api == true`.
- Schema/TS export filters use the inventory list and
`EXPERIMENTAL_CLIENT_METHODS` from `client_request_definitions!` to
strip experimental methods/fields when `experimental_api` is false.
2026-02-02 11:06:50 +00:00

70 lines
2.2 KiB
Rust

/// Marker trait for protocol types that can signal experimental usage.
pub trait ExperimentalApi {
/// Returns a short reason identifier when an experimental method or field is
/// used, or `None` when the value is entirely stable.
fn experimental_reason(&self) -> Option<&'static str>;
}
/// Describes an experimental field on a specific type.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ExperimentalField {
pub type_name: &'static str,
pub field_name: &'static str,
/// Stable identifier returned when this field is used.
/// Convention: `<method>` for method-level gates or `<method>.<field>` for
/// field-level gates.
pub reason: &'static str,
}
inventory::collect!(ExperimentalField);
/// Returns all experimental fields registered across the protocol types.
pub fn experimental_fields() -> Vec<&'static ExperimentalField> {
inventory::iter::<ExperimentalField>.into_iter().collect()
}
/// Constructs a consistent error message for experimental gating.
pub fn experimental_required_message(reason: &str) -> String {
format!("{reason} requires experimentalApi capability")
}
#[cfg(test)]
mod tests {
use super::ExperimentalApi as ExperimentalApiTrait;
use codex_experimental_api_macros::ExperimentalApi;
use pretty_assertions::assert_eq;
#[allow(dead_code)]
#[derive(ExperimentalApi)]
enum EnumVariantShapes {
#[experimental("enum/unit")]
Unit,
#[experimental("enum/tuple")]
Tuple(u8),
#[experimental("enum/named")]
Named {
value: u8,
},
StableTuple(u8),
}
#[test]
fn derive_supports_all_enum_variant_shapes() {
assert_eq!(
ExperimentalApiTrait::experimental_reason(&EnumVariantShapes::Unit),
Some("enum/unit")
);
assert_eq!(
ExperimentalApiTrait::experimental_reason(&EnumVariantShapes::Tuple(1)),
Some("enum/tuple")
);
assert_eq!(
ExperimentalApiTrait::experimental_reason(&EnumVariantShapes::Named { value: 1 }),
Some("enum/named")
);
assert_eq!(
ExperimentalApiTrait::experimental_reason(&EnumVariantShapes::StableTuple(1)),
None
);
}
}