diff --git a/codex-rs/core/src/tools/parallel.rs b/codex-rs/core/src/tools/parallel.rs index 384a800b4b..6b7cf0544f 100644 --- a/codex-rs/core/src/tools/parallel.rs +++ b/codex-rs/core/src/tools/parallel.rs @@ -179,6 +179,7 @@ impl ToolCallRuntime { message: Self::abort_message(call, secs), }), post_tool_use_payload: None, + model_visible_override: None, } } diff --git a/codex-rs/core/src/tools/registry.rs b/codex-rs/core/src/tools/registry.rs index 7c04e12c29..1494d0df09 100644 --- a/codex-rs/core/src/tools/registry.rs +++ b/codex-rs/core/src/tools/registry.rs @@ -109,6 +109,7 @@ pub(crate) struct AnyToolResult { pub(crate) payload: ToolPayload, pub(crate) result: Box, pub(crate) post_tool_use_payload: Option, + pub(crate) model_visible_override: Option, } impl AnyToolResult { @@ -117,9 +118,13 @@ impl AnyToolResult { call_id, payload, result, + model_visible_override, .. } = self; - result.to_response_item(&call_id, &payload) + model_visible_override.map_or_else( + || result.to_response_item(&call_id, &payload), + |override_output| override_output.to_response_item(&call_id, &payload), + ) } pub(crate) fn code_mode_result(self) -> serde_json::Value { @@ -207,6 +212,7 @@ where payload, result: Box::new(output), post_tool_use_payload, + model_visible_override: None, }) }) } @@ -481,7 +487,7 @@ impl ToolRegistry { } else if let Some(updated_tool_output) = &outcome.updated_tool_output { let mut guard = response_cell.lock().await; if let Some(result) = guard.as_mut() { - result.result = Box::new(FunctionToolOutput::from_text( + result.model_visible_override = Some(FunctionToolOutput::from_text( post_tool_use_output_to_model_text(updated_tool_output), Some(true), )); @@ -511,6 +517,10 @@ impl ToolRegistry { &result.call_id, &result.payload, result.result.as_ref(), + result + .model_visible_override + .as_ref() + .map(|override_output| override_output as &dyn ToolOutput), ); Ok(result) } diff --git a/codex-rs/core/src/tools/registry_tests.rs b/codex-rs/core/src/tools/registry_tests.rs index d44c3d0f9b..3c500976a5 100644 --- a/codex-rs/core/src/tools/registry_tests.rs +++ b/codex-rs/core/src/tools/registry_tests.rs @@ -1,5 +1,9 @@ use super::*; +use crate::tools::context::McpToolOutput; +use codex_protocol::mcp::CallToolResult; use pretty_assertions::assert_eq; +use serde_json::json; +use std::time::Duration; #[derive(Default)] struct TestHandler; @@ -53,3 +57,55 @@ fn handler_looks_up_namespaced_aliases_explicitly() { .is_some_and(|handler| Arc::ptr_eq(handler, &namespaced_handler)) ); } + +#[test] +fn model_visible_override_does_not_replace_typed_tool_output() { + let result = mcp_result_with_model_visible_override(); + + match result.into_response() { + ResponseInputItem::FunctionCallOutput { call_id, output } => { + assert_eq!(call_id, "mcp-call-1"); + assert_eq!(output.body.to_text().as_deref(), Some("[redacted]")); + } + other => panic!("expected FunctionCallOutput, got {other:?}"), + } + + assert_eq!( + mcp_result_with_model_visible_override().code_mode_result(), + json!({ + "content": [], + "structuredContent": { + "echo": "original", + }, + "isError": false, + }) + ); +} + +fn mcp_result_with_model_visible_override() -> AnyToolResult { + AnyToolResult { + call_id: "mcp-call-1".to_string(), + payload: ToolPayload::Mcp { + server: "memory".to_string(), + tool: "lookup".to_string(), + raw_arguments: "{}".to_string(), + }, + result: Box::new(McpToolOutput { + result: CallToolResult { + content: Vec::new(), + structured_content: Some(json!({ "echo": "original" })), + is_error: Some(false), + meta: None, + }, + tool_input: json!({}), + wall_time: Duration::ZERO, + original_image_detail_supported: false, + truncation_policy: codex_utils_output_truncation::TruncationPolicy::Bytes(1024), + }), + post_tool_use_payload: None, + model_visible_override: Some(FunctionToolOutput::from_text( + "[redacted]".to_string(), + Some(true), + )), + } +} diff --git a/codex-rs/core/src/tools/tool_dispatch_trace.rs b/codex-rs/core/src/tools/tool_dispatch_trace.rs index 344b686348..3ceaa7601c 100644 --- a/codex-rs/core/src/tools/tool_dispatch_trace.rs +++ b/codex-rs/core/src/tools/tool_dispatch_trace.rs @@ -37,12 +37,14 @@ impl ToolDispatchTrace { call_id: &str, payload: &ToolPayload, result: &dyn ToolOutput, + model_visible_override: Option<&dyn ToolOutput>, ) { if !self.context.is_enabled() { return; } - let Some(result_payload) = tool_dispatch_result(invocation, call_id, payload, result) + let Some(result_payload) = + tool_dispatch_result(invocation, call_id, payload, result, model_visible_override) else { return; }; @@ -89,10 +91,13 @@ fn tool_dispatch_result( call_id: &str, payload: &ToolPayload, result: &dyn ToolOutput, + model_visible_override: Option<&dyn ToolOutput>, ) -> Option { match invocation.source { ToolCallSource::Direct => Some(ToolDispatchResult::DirectResponse { - response_item: result.to_response_item(call_id, payload), + response_item: model_visible_override + .unwrap_or(result) + .to_response_item(call_id, payload), }), ToolCallSource::CodeMode { .. } => Some(ToolDispatchResult::CodeModeResponse { value: result.code_mode_result(payload),