preserve typed outputs for post-tool rewrites

This commit is contained in:
Abhinav Vedmala
2026-05-01 17:06:26 -07:00
parent a041fd668b
commit 365bf45526
4 changed files with 76 additions and 4 deletions

View File

@@ -179,6 +179,7 @@ impl ToolCallRuntime {
message: Self::abort_message(call, secs),
}),
post_tool_use_payload: None,
model_visible_override: None,
}
}

View File

@@ -109,6 +109,7 @@ pub(crate) struct AnyToolResult {
pub(crate) payload: ToolPayload,
pub(crate) result: Box<dyn ToolOutput>,
pub(crate) post_tool_use_payload: Option<PostToolUsePayload>,
pub(crate) model_visible_override: Option<FunctionToolOutput>,
}
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)
}

View File

@@ -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),
)),
}
}

View File

@@ -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<ToolDispatchResult> {
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),