Skip to content

Schema与TypeScript导出

追踪 App Server 类型从 Rust 定义到 JSON Schema、TypeScript 和预计算导出的生成链路。

基于rust-v0.150.0
CodexRustProtocolSchemaTypeScript

Schema与TypeScript导出 ​

本文承接AppServer V2方法注册机制,面向已经理解 typed request/response 和 experimental gate 的读者。本文回答 Rust 协议类型如何生成 TypeScript、JSON Schema 和 stable/experimental artifact,以及 fixture 为什么需要规范化;不把生成文件当作运行时 handler 的替代品。

1. 生成管线 ​

生成过程先从 Rust 类型导出 TS/JSON,再过滤实验方法和字段,最后写入 fixture 与压缩的预计算 exports。公开 schema 是构建产物,源码和 runtime handler 仍是行为来源。

2. TypeScript导出 ​

源码位置:codex-rs/app-server-protocol/src/export.rs :: generate_ts_with_options

rust
ClientRequest::export_all_to(out_dir)?;
export_client_responses(out_dir)?;
ClientNotification::export_all_to(out_dir)?;
ServerRequest::export_all_to(out_dir)?;
export_server_responses(out_dir)?;
ServerNotification::export_all_to(out_dir)?;
ServerNotificationEnvelope::export_all_to(out_dir)?;

if !options.experimental_api {
    filter_experimental_ts(out_dir)?;
}
if options.generate_indices {
    generate_index_ts(out_dir)?;
    generate_index_ts(&v2_out_dir)?;
}

TS 导出覆盖 request、response、notification 和 envelope,并可生成 root/v2 index。stable 模式会删除实验方法 union arm 和实验字段;experimental_api=true 才保留完整类型。

3. JSON Schema聚合 ​

源码位置:codex-rs/app-server-protocol/src/export.rs :: generate_json_with_experimental

rust
let envelope_emitters = vec![
    |d| write_json_schema_with_return::<crate::RequestId>(d, "RequestId"),
    |d| write_json_schema_with_return::<crate::JSONRPCMessage>(d, "JSONRPCMessage"),
    |d| write_json_schema_with_return::<crate::ClientRequest>(d, "ClientRequest"),
    |d| write_json_schema_with_return::<crate::ServerRequest>(d, "ServerRequest"),
];
let mut schemas = Vec::new();
for emit in &envelope_emitters { schemas.push(emit(out_dir)?); }
schemas.extend(export_client_param_schemas(out_dir)?);
schemas.extend(export_client_response_schemas(out_dir)?);
schemas.extend(export_server_param_schemas(out_dir)?);
schemas.extend(export_server_response_schemas(out_dir)?);
let mut bundle = build_schema_bundle(schemas)?;
if !experimental_api { filter_experimental_schema(&mut bundle)?; }
write_pretty_json(out_dir.join("codex_app_server_protocol.schemas.json"), &bundle)?;

JSON bundle 由 envelope、参数、响应和通知 schema 合并而成;还会生成 flat V2 bundle。V1 allowlist 和实验过滤决定哪些类型进入公开文件。

4. 实验过滤 ​

源码位置:codex-rs/app-server-protocol/src/export.rs :: filter_experimental_ts、filter_experimental_schema

rust
filter_request_ts(out_dir, "ClientRequest.ts", EXPERIMENTAL_CLIENT_METHODS)?;
filter_request_ts(out_dir, "ServerRequest.ts", EXPERIMENTAL_SERVER_METHODS)?;
filter_experimental_type_fields_ts(out_dir, &registered_fields)?;
remove_generated_type_files(out_dir, &experimental_method_types, "ts")?;

过滤不仅删除文件,还会删 request union arm 和类型字段;因此 stable TS 与 experimental TS 不是同一文件的不同注释版本。运行时 gate 仍需单独检查。

5. 预计算导出 ​

源码位置:codex-rs/app-server-protocol/src/precomputed_exports.rs :: generate_types、generate_ts_with_options、generate_json_with_experimental、load_exports

rust
pub fn generate_types(out_dir: &Path, prettier: Option<&Path>) -> Result<()> {
    generate_ts(out_dir, prettier)?;
    generate_json(out_dir)
}

pub fn generate_ts_with_options(
    out_dir: &Path,
    prettier: Option<&Path>,
    options: GenerateTsOptions,
) -> Result<()> {
    let export_set = if options.experimental_api {
        ExportSet::Experimental
    } else {
        ExportSet::Stable
    };
    let exports = load_exports(export_set)?;
    let ts_files = write_typescript_exports(out_dir, &exports.typescript, options)?;
    if options.run_prettier && let Some(prettier_bin) = prettier && !ts_files.is_empty() {
        let status = Command::new(prettier_bin)
            .arg("--write")
            .arg("--log-level")
            .arg("warn")
            .args(ts_files.iter().map(PathBuf::as_path))
            .status()?;
        if !status.success() {
            bail!("Prettier failed with status {status}");
        }
    }
    trim_trailing_whitespace_in_ts_files(&ts_files)
}

fn load_exports(export_set: ExportSet) -> Result<PrecomputedExports> {
    let compressed = match export_set {
        ExportSet::Stable => STABLE_EXPORTS,
        ExportSet::Experimental => EXPERIMENTAL_EXPORTS,
    };
    let bytes = zstd::stream::decode_all(Cursor::new(compressed))?;
    serde_json::from_slice(&bytes).context("decode precomputed app-server protocol exports")
}

运行时生成 API 与读取预计算 exports 是两条路径:export.rs 从 Rust 类型现场生成,precomputed_exports.rs 从压缩内嵌 bytes 解压并写盘。两者必须通过 fixture 测试保持一致;否则发布包可能携带旧 TS/JSON,而源码已经生成了新契约。

6. Fixture写入 ​

源码位置:codex-rs/app-server-protocol/src/schema_fixtures.rs :: write_schema_fixtures_with_options

rust
ensure_empty_dir(&typescript_out_dir)?;
generate_ts_with_options(&typescript_out_dir, prettier, GenerateTsOptions::default())?;
generate_json_with_experimental(&json_out_dir, false)?;
generate_internal_json_schema(internal_dir.path())?;
let exports = PrecomputedExports {
    typescript: collect_export_files_recursive(&typescript_out_dir)?,
    json_schema: collect_export_files_recursive(&json_out_dir)?,
    internal_json_schema: collect_export_files_recursive(internal_dir.path())?,
};
write_precomputed_exports(schema_root, "stable", &exports)?;

fixture 写入先清空输出目录,再生成 stable TS/JSON 和 internal schema,最后压缩成预计算 artifact。experimental 模式使用独立临时目录,避免污染 stable fixture。

7. 稳定排序 ​

源码位置:codex-rs/app-server-protocol/src/schema_fixtures.rs :: canonicalize_json、schema_array_item_sort_key

rust
fn canonicalize_json(value: &Value) -> Value {
    match value {
        Value::Array(items) => {
            let items = items.iter().map(canonicalize_json).collect::<Vec<_>>();
            let mut sortable = Vec::with_capacity(items.len());
            for item in &items {
                let Some(key) = schema_array_item_sort_key(item) else {
                    return Value::Array(items);
                };
                let stable = serde_json::to_string(item).unwrap_or_default();
                sortable.push((key, stable));
            }
            let mut items = items.into_iter().zip(sortable).collect::<Vec<_>>();
            items.sort_by(|(_, left), (_, right)| left.cmp(right));
            Value::Array(items.into_iter().map(|(item, _)| item).collect())
        }
        Value::Object(map) => {
            let mut entries: Vec<_> = map.iter().collect();
            entries.sort_by_key(|(key, _)| *key);
            let mut sorted = Map::with_capacity(map.len());
            for (key, child) in entries {
                sorted.insert(key.clone(), canonicalize_json(child));
            }
            Value::Object(sorted)
        }
        _ => value.clone(),
    }
}

规范化只用于 fixture 比较:对象键排序,只有每个数组元素都能得到稳定 key 时才排序数组;无法安全判断顺序是否无关的数组会保留原顺序。它解决的是平台和 HashMap 顺序差异,不会改变生成器或运行时 schema 的语义。

fixture 比较会对 schema 数组做规范化排序,消除 HashMap 顺序和平台差异;这只稳定表示,不改变 runtime 类型语义。无法推导稳定 sort key 的元素会保留序列化字符串作为次级 key。

8. 失败与边界 ​

生成失败可能来自 Rust 类型导出、Prettier、schema 聚合或 fixture 写入;stable 过滤成功也不代表运行时会接受该字段。预计算文件过期时,客户端可能看到旧契约,而 server 已经运行新 handler,必须通过 fixture diff 识别。

9. 源码验证 ​

precomputed_exports_are_written_to_disk 比较 freshly generated TS/JSON 与预计算 exports;schema_fixtures 测试验证 canonicalization 和实验过滤;导出选项测试验证 header、index、Prettier 和 experimental_api 行为。它们证明生成可复现和 artifact 一致,不证明 schema 能覆盖运行时所有条件分支。

源码位置:

  • codex-rs/app-server-protocol/src/precomputed_exports_tests.rs :: precomputed_exports_are_written_to_disk
  • codex-rs/app-server-protocol/src/schema_fixtures.rs :: canonicalize_json
  • codex-rs/app-server-protocol/src/export.rs :: GenerateTsOptions
text
cd codex-rs
cargo test -p codex-app-server-protocol precomputed_exports
cargo test -p codex-app-server-protocol schema_fixtures
cargo test -p codex-app-server-protocol export_options

10. 契约排查 ​

遇到客户端类型缺少方法,检查 stable experimental filter 和生成 index;遇到 schema 与 server 行为不一致,比较 fixture 与当前源码;遇到实验字段被接受或拒绝异常,分别检查 runtime gate 和导出 artifact。生成文件是契约快照,不是行为实现。