codex-app-server-sdk 0.5.1

Tokio Rust SDK for Codex App Server
Documentation
use crate::protocol::shared::RequestId;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use thiserror::Error;

#[derive(Debug, Error)]
pub enum ClientError {
    #[error("not initialized: call initialize() before invoking {method}")]
    NotInitialized { method: String },
    #[error("connection not ready: call initialized() before invoking {method}")]
    NotReady { method: String },
    #[error("connection is already initialized")]
    AlreadyInitialized,
    #[error("request timed out after {timeout_ms}ms for method {method}")]
    Timeout { method: String, timeout_ms: u64 },
    #[error("transport send failed: {0}")]
    TransportSend(String),
    #[error("transport closed")]
    TransportClosed,
    #[error("invalid message from server: {0}")]
    InvalidMessage(String),
    #[error("serialization error: {0}")]
    Serialization(#[from] serde_json::Error),
    #[error("io error: {0}")]
    Io(#[from] std::io::Error),
    #[error("rpc error {error:?}")]
    Rpc { error: RpcError },
    #[error("unexpected result shape for method {method}: {source}")]
    UnexpectedResult {
        method: String,
        source: serde_json::Error,
    },
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RpcError {
    pub code: i64,
    pub message: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub data: Option<Value>,
}

#[derive(Debug)]
pub enum IncomingClassified {
    Response {
        id: RequestId,
        result: Result<Value, RpcError>,
    },
    Notification {
        method: String,
        params: Value,
        raw: Value,
    },
    ServerRequest {
        id: RequestId,
        method: String,
        params: Value,
        raw: Value,
    },
}

pub fn classify_incoming(value: Value) -> Result<IncomingClassified, ClientError> {
    let obj = value
        .as_object()
        .ok_or_else(|| ClientError::InvalidMessage("expected JSON object".to_string()))?;

    let id = obj
        .get("id")
        .map(|v| serde_json::from_value::<RequestId>(v.clone()))
        .transpose()
        .map_err(ClientError::Serialization)?;

    let method = obj
        .get("method")
        .and_then(|v| v.as_str())
        .map(|s| s.to_string());

    match (id, method) {
        (Some(id), Some(method)) => {
            let params = obj.get("params").cloned().unwrap_or(Value::Null);
            Ok(IncomingClassified::ServerRequest {
                id,
                method,
                params,
                raw: value,
            })
        }
        (None, Some(method)) => {
            let params = obj.get("params").cloned().unwrap_or(Value::Null);
            Ok(IncomingClassified::Notification {
                method,
                params,
                raw: value,
            })
        }
        (Some(id), None) => {
            if let Some(result) = obj.get("result") {
                return Ok(IncomingClassified::Response {
                    id,
                    result: Ok(result.clone()),
                });
            }
            if let Some(error) = obj.get("error") {
                let parsed = serde_json::from_value::<RpcError>(error.clone())?;
                return Ok(IncomingClassified::Response {
                    id,
                    result: Err(parsed),
                });
            }
            Err(ClientError::InvalidMessage(
                "response missing both result and error".to_string(),
            ))
        }
        (None, None) => Err(ClientError::InvalidMessage(
            "message missing method and id".to_string(),
        )),
    }
}