diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index 70064c3ed9..4810275aea 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -96,11 +96,57 @@ macro_rules! client_request_definitions { client_request_definitions! { /// NEW APIs + // Thread lifecycle + #[serde(rename = "thread/start")] + #[ts(rename = "thread/start")] + ThreadStart { + params: v2::ThreadStartParams, + response: v2::ThreadStartResponse, + }, + #[serde(rename = "thread/resume")] + #[ts(rename = "thread/resume")] + ThreadResume { + params: v2::ThreadResumeParams, + response: v2::ThreadResumeResponse, + }, + #[serde(rename = "thread/archive")] + #[ts(rename = "thread/archive")] + ThreadArchive { + params: v2::ThreadArchiveParams, + response: v2::ThreadArchiveResponse, + }, + #[serde(rename = "thread/list")] + #[ts(rename = "thread/list")] + ThreadList { + params: v2::ThreadListParams, + response: v2::ThreadListResponse, + }, + #[serde(rename = "thread/compact")] + #[ts(rename = "thread/compact")] + ThreadCompact { + params: v2::ThreadCompactParams, + response: v2::ThreadCompactResponse, + }, + + // Turn lifecycle + #[serde(rename = "turn/start")] + #[ts(rename = "turn/start")] + TurnStart { + params: v2::TurnStartParams, + response: v2::TurnStartResponse, + }, + #[serde(rename = "turn/interrupt")] + #[ts(rename = "turn/interrupt")] + TurnInterrupt { + params: v2::TurnInterruptParams, + response: v2::TurnInterruptResponse, + }, + #[serde(rename = "model/list")] #[ts(rename = "model/list")] ListModels { - params: v2::ListModelsParams, - response: v2::ListModelsResponse, + params: v2::ModelListParams, + response: v2::ModelListResponse, }, #[serde(rename = "account/login")] @@ -127,8 +173,8 @@ client_request_definitions! { #[serde(rename = "feedback/upload")] #[ts(rename = "feedback/upload")] UploadFeedback { - params: v2::UploadFeedbackParams, - response: v2::UploadFeedbackResponse, + params: v2::FeedbackUploadParams, + response: v2::FeedbackUploadResponse, }, #[serde(rename = "account/read")] @@ -685,7 +731,7 @@ mod tests { fn serialize_list_models() -> Result<()> { let request = ClientRequest::ListModels { request_id: RequestId::Integer(6), - params: v2::ListModelsParams::default(), + params: v2::ModelListParams::default(), }; assert_eq!( json!({ diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 3d776e51cd..297716476c 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -10,6 +10,13 @@ use serde_json::Value as JsonValue; use ts_rs::TS; use uuid::Uuid; +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub enum SortOrder { + Asc, + Desc, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(tag = "type", rename_all = "camelCase")] #[ts(tag = "type")] @@ -79,11 +86,13 @@ pub struct GetAccountResponse { #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] -pub struct ListModelsParams { - /// Optional page size; defaults to a reasonable server-side value. - pub page_size: Option, +pub struct ModelListParams { /// Opaque pagination cursor returned by a previous call. pub cursor: Option, + /// Optional page size; defaults to a reasonable server-side value. + pub limit: Option, + /// Optional sort order; defaults to descending. + pub order: Option, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] @@ -111,8 +120,8 @@ pub struct ReasoningEffortOption { #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] -pub struct ListModelsResponse { - pub items: Vec, +pub struct ModelListResponse { + pub data: Vec, /// Opaque cursor to pass to the next call to continue after the last item. /// if None, there are no more items to return. pub next_cursor: Option, @@ -121,7 +130,7 @@ pub struct ListModelsResponse { #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] -pub struct UploadFeedbackParams { +pub struct FeedbackUploadParams { pub classification: String, pub reason: Option, pub conversation_id: Option, @@ -131,12 +140,86 @@ pub struct UploadFeedbackParams { #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] -pub struct UploadFeedbackResponse { +pub struct FeedbackUploadResponse { pub thread_id: String, } // === Threads, Turns, and Items === +// Thread APIs + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadStartParams {} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadStartResponse { + pub thread_id: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadResumeParams { + pub thread_id: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadResumeResponse { + pub thread: Thread, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadArchiveParams { + pub thread_id: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadArchiveResponse {} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadListParams { + /// Opaque pagination cursor returned by a previous call. + pub cursor: Option, + /// Optional page size; defaults to a reasonable server-side value. + pub limit: Option, + /// Optional sort order; defaults to descending. + pub order: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadListResponse { + pub data: Vec, + /// Opaque cursor to pass to the next call to continue after the last item. + /// if None, there are no more items to return. + pub next_cursor: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadCompactParams { + pub thread_id: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadCompactResponse {} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] @@ -171,6 +254,38 @@ pub enum TurnStatus { InProgress, } +// Turn APIs + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct TurnStartParams { + pub thread_id: String, + pub input: Vec, + pub model: String, + pub effort: ReasoningEffort, + pub summary: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct TurnStartResponse { + pub turn_id: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct TurnInterruptParams { + pub turn_id: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct TurnInterruptResponse {} + // User input types #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(tag = "type", rename_all = "camelCase")] diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 99310e198f..6dd93711f5 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -18,6 +18,8 @@ use codex_app_server_protocol::ExecCommandApprovalParams; use codex_app_server_protocol::ExecCommandApprovalResponse; use codex_app_server_protocol::ExecOneOffCommandParams; use codex_app_server_protocol::ExecOneOffCommandResponse; +use codex_app_server_protocol::FeedbackUploadParams; +use codex_app_server_protocol::FeedbackUploadResponse; use codex_app_server_protocol::FuzzyFileSearchParams; use codex_app_server_protocol::FuzzyFileSearchResponse; use codex_app_server_protocol::GetAccountRateLimitsResponse; @@ -32,12 +34,12 @@ use codex_app_server_protocol::InterruptConversationResponse; use codex_app_server_protocol::JSONRPCErrorError; use codex_app_server_protocol::ListConversationsParams; use codex_app_server_protocol::ListConversationsResponse; -use codex_app_server_protocol::ListModelsParams; -use codex_app_server_protocol::ListModelsResponse; use codex_app_server_protocol::LoginApiKeyParams; use codex_app_server_protocol::LoginApiKeyResponse; use codex_app_server_protocol::LoginChatGptCompleteNotification; use codex_app_server_protocol::LoginChatGptResponse; +use codex_app_server_protocol::ModelListParams; +use codex_app_server_protocol::ModelListResponse; use codex_app_server_protocol::NewConversationParams; use codex_app_server_protocol::NewConversationResponse; use codex_app_server_protocol::RemoveConversationListenerParams; @@ -54,8 +56,6 @@ use codex_app_server_protocol::ServerRequestPayload; use codex_app_server_protocol::SessionConfiguredNotification; use codex_app_server_protocol::SetDefaultModelParams; use codex_app_server_protocol::SetDefaultModelResponse; -use codex_app_server_protocol::UploadFeedbackParams; -use codex_app_server_protocol::UploadFeedbackResponse; use codex_app_server_protocol::UserInfoResponse; use codex_app_server_protocol::UserSavedConfig; use codex_backend_client::Client as BackendClient; @@ -952,13 +952,13 @@ impl CodexMessageProcessor { self.outgoing.send_response(request_id, response).await; } - async fn list_models(&self, request_id: RequestId, params: ListModelsParams) { - let ListModelsParams { page_size, cursor } = params; + async fn list_models(&self, request_id: RequestId, params: ModelListParams) { + let ModelListParams { page_size, cursor } = params; let models = supported_models(); let total = models.len(); if total == 0 { - let response = ListModelsResponse { + let response = ModelListResponse { items: Vec::new(), next_cursor: None, }; @@ -1000,7 +1000,7 @@ impl CodexMessageProcessor { } else { None }; - let response = ListModelsResponse { items, next_cursor }; + let response = ModelListResponse { items, next_cursor }; self.outgoing.send_response(request_id, response).await; } @@ -1590,8 +1590,8 @@ impl CodexMessageProcessor { self.outgoing.send_response(request_id, response).await; } - async fn upload_feedback(&self, request_id: RequestId, params: UploadFeedbackParams) { - let UploadFeedbackParams { + async fn upload_feedback(&self, request_id: RequestId, params: FeedbackUploadParams) { + let FeedbackUploadParams { classification, reason, conversation_id, @@ -1636,7 +1636,7 @@ impl CodexMessageProcessor { match upload_result { Ok(()) => { - let response = UploadFeedbackResponse { thread_id }; + let response = FeedbackUploadResponse { thread_id }; self.outgoing.send_response(request_id, response).await; } Err(err) => { diff --git a/codex-rs/app-server/tests/common/mcp_process.rs b/codex-rs/app-server/tests/common/mcp_process.rs index 90c7645d58..803c69c877 100644 --- a/codex-rs/app-server/tests/common/mcp_process.rs +++ b/codex-rs/app-server/tests/common/mcp_process.rs @@ -17,12 +17,13 @@ use codex_app_server_protocol::ArchiveConversationParams; use codex_app_server_protocol::CancelLoginChatGptParams; use codex_app_server_protocol::ClientInfo; use codex_app_server_protocol::ClientNotification; +use codex_app_server_protocol::FeedbackUploadParams; use codex_app_server_protocol::GetAuthStatusParams; use codex_app_server_protocol::InitializeParams; use codex_app_server_protocol::InterruptConversationParams; use codex_app_server_protocol::ListConversationsParams; -use codex_app_server_protocol::ListModelsParams; use codex_app_server_protocol::LoginApiKeyParams; +use codex_app_server_protocol::ModelListParams; use codex_app_server_protocol::NewConversationParams; use codex_app_server_protocol::RemoveConversationListenerParams; use codex_app_server_protocol::ResumeConversationParams; @@ -30,7 +31,6 @@ use codex_app_server_protocol::SendUserMessageParams; use codex_app_server_protocol::SendUserTurnParams; use codex_app_server_protocol::ServerRequest; use codex_app_server_protocol::SetDefaultModelParams; -use codex_app_server_protocol::UploadFeedbackParams; use codex_app_server_protocol::JSONRPCError; use codex_app_server_protocol::JSONRPCMessage; @@ -246,7 +246,7 @@ impl McpProcess { /// Send a `feedback/upload` JSON-RPC request. pub async fn send_upload_feedback_request( &mut self, - params: UploadFeedbackParams, + params: FeedbackUploadParams, ) -> anyhow::Result { let params = Some(serde_json::to_value(params)?); self.send_request("feedback/upload", params).await @@ -278,7 +278,7 @@ impl McpProcess { /// Send a `model/list` JSON-RPC request. pub async fn send_list_models_request( &mut self, - params: ListModelsParams, + params: ModelListParams, ) -> anyhow::Result { let params = Some(serde_json::to_value(params)?); self.send_request("model/list", params).await diff --git a/codex-rs/app-server/tests/suite/model_list.rs b/codex-rs/app-server/tests/suite/model_list.rs index 3472e0d3cb..7f6a6da78f 100644 --- a/codex-rs/app-server/tests/suite/model_list.rs +++ b/codex-rs/app-server/tests/suite/model_list.rs @@ -6,9 +6,9 @@ use app_test_support::McpProcess; use app_test_support::to_response; use codex_app_server_protocol::JSONRPCError; use codex_app_server_protocol::JSONRPCResponse; -use codex_app_server_protocol::ListModelsParams; -use codex_app_server_protocol::ListModelsResponse; use codex_app_server_protocol::Model; +use codex_app_server_protocol::ModelListParams; +use codex_app_server_protocol::ModelListResponse; use codex_app_server_protocol::ReasoningEffortOption; use codex_app_server_protocol::RequestId; use codex_protocol::config_types::ReasoningEffort; @@ -27,7 +27,7 @@ async fn list_models_returns_all_models_with_large_limit() -> Result<()> { timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp - .send_list_models_request(ListModelsParams { + .send_list_models_request(ModelListParams { page_size: Some(100), cursor: None, }) @@ -39,7 +39,7 @@ async fn list_models_returns_all_models_with_large_limit() -> Result<()> { ) .await??; - let ListModelsResponse { items, next_cursor } = to_response::(response)?; + let ModelListResponse { items, next_cursor } = to_response::(response)?; let expected_models = vec![ Model { @@ -111,7 +111,7 @@ async fn list_models_pagination_works() -> Result<()> { timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let first_request = mcp - .send_list_models_request(ListModelsParams { + .send_list_models_request(ModelListParams { page_size: Some(1), cursor: None, }) @@ -123,17 +123,17 @@ async fn list_models_pagination_works() -> Result<()> { ) .await??; - let ListModelsResponse { + let ModelListResponse { items: first_items, next_cursor: first_cursor, - } = to_response::(first_response)?; + } = to_response::(first_response)?; assert_eq!(first_items.len(), 1); assert_eq!(first_items[0].id, "gpt-5-codex"); let next_cursor = first_cursor.ok_or_else(|| anyhow!("cursor for second page"))?; let second_request = mcp - .send_list_models_request(ListModelsParams { + .send_list_models_request(ModelListParams { page_size: Some(1), cursor: Some(next_cursor.clone()), }) @@ -145,10 +145,10 @@ async fn list_models_pagination_works() -> Result<()> { ) .await??; - let ListModelsResponse { + let ModelListResponse { items: second_items, next_cursor: second_cursor, - } = to_response::(second_response)?; + } = to_response::(second_response)?; assert_eq!(second_items.len(), 1); assert_eq!(second_items[0].id, "gpt-5"); @@ -164,7 +164,7 @@ async fn list_models_rejects_invalid_cursor() -> Result<()> { timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp - .send_list_models_request(ListModelsParams { + .send_list_models_request(ModelListParams { page_size: None, cursor: Some("invalid".to_string()), })