diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 4d20f024a9..d7462cddb2 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -2671,16 +2671,23 @@ pub struct DynamicToolCallParams { #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] pub struct DynamicToolCallResponse { + #[serde(flatten)] + pub result: DynamicToolCallResult, + pub success: bool, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(untagged, rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub enum DynamicToolCallResult { /// Preferred structured tool output (for example text + images) that is /// forwarded directly to the model as content items. - /// - /// At least one of `content_items` or `output` must be set. - pub content_items: Option>, + ContentItems { + #[serde(rename = "contentItems")] + content_items: Vec, + }, /// Legacy plain-text tool output. - /// - /// At least one of `content_items` or `output` must be set. - pub output: Option, - pub success: bool, + Output { output: String }, } /// App-server-facing dynamic tool output items. @@ -3089,10 +3096,11 @@ mod tests { #[test] fn dynamic_tool_response_serializes_content_items() { let value = serde_json::to_value(DynamicToolCallResponse { - content_items: Some(vec![DynamicToolCallOutputContentItem::InputText { - text: "dynamic-ok".to_string(), - }]), - output: None, + result: DynamicToolCallResult::ContentItems { + content_items: vec![DynamicToolCallOutputContentItem::InputText { + text: "dynamic-ok".to_string(), + }], + }, success: true, }) .unwrap(); @@ -3100,7 +3108,6 @@ mod tests { assert_eq!( value, json!({ - "output": null, "success": true, "contentItems": [ { diff --git a/codex-rs/app-server/src/dynamic_tools.rs b/codex-rs/app-server/src/dynamic_tools.rs index 91a76018b6..c63ec45d34 100644 --- a/codex-rs/app-server/src/dynamic_tools.rs +++ b/codex-rs/app-server/src/dynamic_tools.rs @@ -1,7 +1,7 @@ use codex_app_server_protocol::DynamicToolCallResponse; +use codex_app_server_protocol::DynamicToolCallResult; use codex_core::CodexThread; use codex_protocol::dynamic_tools::DynamicToolResponse as CoreDynamicToolResponse; -use codex_protocol::models::FunctionCallOutputContentItem; use codex_protocol::protocol::Op; use std::sync::Arc; use tokio::sync::oneshot; @@ -36,31 +36,26 @@ pub(crate) async fn on_call_response( } }; - let mut response = - serde_json::from_value::(value).unwrap_or_else(|err| { - error!("failed to deserialize DynamicToolCallResponse: {err}"); - DynamicToolCallResponse { - content_items: None, - output: Some("dynamic tool response was invalid".to_string()), - success: false, - } - }); - - if response.content_items.is_none() && response.output.is_none() { - error!("dynamic tool response must include output or contentItems"); - response.output = Some("dynamic tool response must include output or contentItems".into()); - response.success = false; - } - - let content_items = response.content_items.map(|items| { - items - .into_iter() - .map(Into::into) - .collect::>() + let response = serde_json::from_value::(value).unwrap_or_else(|err| { + error!("failed to deserialize DynamicToolCallResponse: {err}"); + DynamicToolCallResponse { + result: DynamicToolCallResult::Output { + output: "dynamic tool response was invalid".to_string(), + }, + success: false, + } }); + + let (output, content_items) = match response.result { + DynamicToolCallResult::ContentItems { content_items } => ( + None, + Some(content_items.into_iter().map(Into::into).collect()), + ), + DynamicToolCallResult::Output { output } => (Some(output), None), + }; let response = CoreDynamicToolResponse { call_id: call_id.clone(), - output: response.output, + output, success: response.success, content_items, }; diff --git a/codex-rs/app-server/tests/suite/v2/dynamic_tools.rs b/codex-rs/app-server/tests/suite/v2/dynamic_tools.rs index 0fa854f87a..d670c096cc 100644 --- a/codex-rs/app-server/tests/suite/v2/dynamic_tools.rs +++ b/codex-rs/app-server/tests/suite/v2/dynamic_tools.rs @@ -7,6 +7,7 @@ use app_test_support::to_response; use codex_app_server_protocol::DynamicToolCallOutputContentItem; use codex_app_server_protocol::DynamicToolCallParams; use codex_app_server_protocol::DynamicToolCallResponse; +use codex_app_server_protocol::DynamicToolCallResult; use codex_app_server_protocol::DynamicToolSpec; use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::RequestId; @@ -203,9 +204,10 @@ async fn dynamic_tool_call_round_trip_sends_output_to_model() -> Result<()> { // Respond to the tool call so the model receives a function_call_output. let response = DynamicToolCallResponse { - output: Some("dynamic-ok".to_string()), + result: DynamicToolCallResult::Output { + output: "dynamic-ok".to_string(), + }, success: true, - content_items: None, }; mcp.send_response(request_id, serde_json::to_value(response)?) .await?; @@ -331,9 +333,10 @@ async fn dynamic_tool_call_round_trip_sends_content_items_to_model() -> Result<( .map(Into::into) .collect::>(); let response = DynamicToolCallResponse { - output: None, + result: DynamicToolCallResult::ContentItems { + content_items: response_content_items, + }, success: true, - content_items: Some(response_content_items), }; mcp.send_response(request_id, serde_json::to_value(response)?) .await?;