Feedback: dynamic tool result enum

This commit is contained in:
Aaron Levine
2026-02-02 15:58:30 -08:00
parent 35d08c0fdc
commit 86dc7d0ccb
3 changed files with 44 additions and 39 deletions

View File

@@ -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<Vec<DynamicToolCallOutputContentItem>>,
ContentItems {
#[serde(rename = "contentItems")]
content_items: Vec<DynamicToolCallOutputContentItem>,
},
/// Legacy plain-text tool output.
///
/// At least one of `content_items` or `output` must be set.
pub output: Option<String>,
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": [
{

View File

@@ -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::<DynamicToolCallResponse>(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::<Vec<FunctionCallOutputContentItem>>()
let response = serde_json::from_value::<DynamicToolCallResponse>(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,
};

View File

@@ -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::<Vec<FunctionCallOutputContentItem>>();
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?;