Preserve full MCP results in rollout traces (#57280)

## Why

MCP completion events can truncate large results and omit content removed for model consumption. Rollout traces need the original response, including content and metadata, as well as results for calls denied before reaching the backend.

## What changed

- Record MCP runtime results before callbacks, content filtering, and UI truncation, and record skipped calls as failures.
- Stop deriving MCP runtime completion records from protocol events to avoid duplicate records.
- Treat MCP runtime payloads as opaque results when reducing agent interactions.
- Buffer JSON trace writes and explicitly flush them.

## Testing

Extend app-server tests to verify that traces retain oversized MCP text, audio, structured content, and metadata while UI results remain truncated, and that denied calls produce exactly one failed runtime result with the denial reason.

GitOrigin-RevId: 57880770545eb5869420d7dc6fa4a15e481cae9d
This commit is contained in:
cassirer-openai
2026-10-26 21:56:23 +00:00
committed by copyberry
parent 6d8f311b00
commit f2df3e3b56
7 changed files with 154 additions and 23 deletions
+18 -4
View File
@@ -489,7 +489,7 @@ async fn handle_approved_mcp_tool_call(
let mut request_error_code = None;
let result = async {
let result = async {
let mut result = prepared_call
let result = prepared_call
.call_with_preparation(/*requested_timeout*/ None, || async {
if let McpToolApprovalApplication::Apply { decision, policy } =
&approval_application
@@ -572,7 +572,14 @@ async fn handle_approved_mcp_tool_call(
.map_err(|error| {
request_error_code = codex_rmcp_client::request_error_code(&error);
format!("tool call error: {error:?}")
})?;
});
// Keep the backend response intact, before callbacks or model/UI limits can change it.
sess.services.rollout_thread_trace.record_mcp_tool_result(
&turn_context.sub_id,
call_id,
&result,
);
let mut result = result?;
crate::tools::record_confirmed_code_mode_send(
sess,
&turn_context.sub_id,
@@ -2588,6 +2595,13 @@ async fn notify_mcp_tool_call_skip(
.await;
}
let result = Err(message);
// Skipped calls never reach the backend-result recorder.
sess.services.rollout_thread_trace.record_mcp_tool_result(
&turn_context.sub_id,
call_id,
&result,
);
notify_mcp_tool_call_completed(
sess,
turn_context,
@@ -2595,10 +2609,10 @@ async fn notify_mcp_tool_call_skip(
invocation,
item_metadata,
Duration::ZERO,
truncate_mcp_tool_result_for_event(&Err(message.clone())),
truncate_mcp_tool_result_for_event(&result),
)
.await;
Err(message)
result
}
#[cfg(test)]