Allow deferring plain-text MCP output in thread item listings (#60390)

## What changed

Add experimental `deferOutput.mcpToolCall` support to `thread/items/list` requests scoped to a `turnId`. Successful, completed MCP calls with nonempty, unannotated plain-text results can return empty `result.content` with `outputDeferred: true`. Clients can retrieve the saved output through `thread/items/read`; connector identity remains on the listed item.

Keep rich results, JSON or fenced text, app UI results, and built-in `codex_app` results inline. Preserve plain-text output when creating bounded previews of oversized MCP results so those previews remain eligible for deferral. Reads return the saved preview when output exceeds the 64 KiB history limit.

## Testing

Extend thread history integration coverage for independent and combined command/MCP deferral, experimental capability gating, excluded result types, connector identity, oversized previews, and retrieval of deferred items.

GitOrigin-RevId: 5c160b30a4eac2a9b70e3fa65b6dbd83e935338b
This commit is contained in:
Brent Traut
2026-11-09 12:54:12 +00:00
committed by copyberry
parent e787af9624
commit d2b4d63e95
7 changed files with 307 additions and 64 deletions
+22 -1
View File
@@ -3,6 +3,7 @@
use std::borrow::Cow;
use codex_protocol::mcp::CallToolResult;
use codex_protocol::mcp::plain_output_text;
use codex_protocol::models::FunctionCallOutputBody;
use codex_protocol::models::FunctionCallOutputContentItem;
use codex_protocol::models::FunctionCallOutputPayload;
@@ -72,7 +73,7 @@ pub fn truncate_mcp_tool_result(
result: &CallToolResult,
max_bytes: usize,
) -> Cow<'_, CallToolResult> {
let Ok(serialized) = serde_json::to_string(result) else {
let Ok(mut serialized) = serde_json::to_string(result) else {
return Cow::Borrowed(result);
};
if serialized.len() <= max_bytes {
@@ -81,6 +82,26 @@ pub fn truncate_mcp_tool_result(
let meta = None;
// Keep plain output recognizable after truncation. Rich results retain their
// serialized preview so history can distinguish them from plain output.
if result.is_error != Some(true)
&& result.structured_content.is_none()
&& result.meta.is_none()
&& let Some(blocks) = result
.content
.iter()
.map(plain_output_text)
.collect::<Option<Vec<_>>>()
&& blocks.iter().any(|text| !text.is_empty())
{
serialized.clear();
serialized.push_str("Truncated tool output:");
for text in blocks {
serialized.push('\n');
serialized.push_str(text);
}
}
let mut preview_budget = max_bytes;
loop {
let preview = if preview_budget == 0 {