mirror of
https://github.com/openai/codex.git
synced 2026-11-10 21:00:47 +00:00
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:
@@ -26,6 +26,7 @@ use crate::thread_sample::generate_sample;
|
||||
use codex_app_server_protocol::SelectedCapabilityRoot;
|
||||
use codex_app_server_protocol::ThreadHistoryMode as ApiThreadHistoryMode;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadItemOutputDeferral;
|
||||
use codex_app_server_protocol::ThreadItemPayload;
|
||||
use codex_app_server_protocol::ThreadItemsListAnchor;
|
||||
use codex_app_server_protocol::ThreadItemsListCursor;
|
||||
@@ -46,6 +47,7 @@ use codex_protocol::SanitizedGitUrl;
|
||||
use codex_protocol::config_types::MultiAgentMode;
|
||||
use codex_protocol::error::CodexErrorDetails;
|
||||
use codex_protocol::mcp::ClientMcpExtensions;
|
||||
use codex_protocol::mcp::plain_output_text;
|
||||
use codex_protocol::protocol::ThreadHistoryMode;
|
||||
use codex_thread_store::LocalHistoryAccount;
|
||||
use codex_thread_store::PersistContext;
|
||||
@@ -3733,8 +3735,7 @@ impl ThreadRequestProcessor {
|
||||
sort_direction,
|
||||
defer_output,
|
||||
} = params;
|
||||
let defer_command_output =
|
||||
turn_id.is_some() && defer_output.is_some_and(|options| options.command_execution);
|
||||
let defer_output = defer_output.filter(|_| turn_id.is_some());
|
||||
let position = cursor.map(|cursor| match cursor {
|
||||
ThreadItemsListCursor::Opaque(cursor) => StoreListItemsPosition::Cursor(cursor),
|
||||
ThreadItemsListCursor::Anchor(ThreadItemsListAnchor::Item { item_id }) => {
|
||||
@@ -3787,20 +3788,10 @@ impl ThreadRequestProcessor {
|
||||
let mut data = Vec::with_capacity(page.items.len());
|
||||
for stored_item in page.items {
|
||||
let mut entry = stored_thread_item_to_entry(stored_item)?;
|
||||
if defer_command_output
|
||||
if let Some(options) = &defer_output
|
||||
&& entry.completed_at_ms.is_some()
|
||||
&& let ThreadItemPayload::CommandExecution {
|
||||
status: codex_app_server_protocol::CommandExecutionStatus::Completed,
|
||||
exit_code: Some(0),
|
||||
aggregated_output,
|
||||
..
|
||||
} = &mut entry.item.payload
|
||||
&& aggregated_output
|
||||
.as_ref()
|
||||
.is_some_and(|output| !output.is_empty())
|
||||
{
|
||||
*aggregated_output = None;
|
||||
entry.output_deferred = true;
|
||||
entry.output_deferred = defer_item_output(&mut entry.item.payload, options);
|
||||
}
|
||||
data.push(entry);
|
||||
}
|
||||
@@ -6492,6 +6483,57 @@ fn stored_thread_item_to_api_item(
|
||||
})
|
||||
}
|
||||
|
||||
fn defer_item_output(item: &mut ThreadItemPayload, options: &ThreadItemOutputDeferral) -> bool {
|
||||
match item {
|
||||
ThreadItemPayload::CommandExecution {
|
||||
status: codex_app_server_protocol::CommandExecutionStatus::Completed,
|
||||
exit_code: Some(0),
|
||||
aggregated_output,
|
||||
..
|
||||
} if options.command_execution
|
||||
&& aggregated_output
|
||||
.as_ref()
|
||||
.is_some_and(|output| !output.is_empty()) =>
|
||||
{
|
||||
*aggregated_output = None;
|
||||
}
|
||||
ThreadItemPayload::McpToolCall {
|
||||
server,
|
||||
status: codex_app_server_protocol::McpToolCallStatus::Completed,
|
||||
result: Some(result),
|
||||
error: None,
|
||||
app_context,
|
||||
mcp_app_resource_uri: None,
|
||||
mcp_app_ui: None,
|
||||
..
|
||||
} if options.mcp_tool_call
|
||||
&& server != "codex_app"
|
||||
&& app_context
|
||||
.as_ref()
|
||||
.is_none_or(|ctx| ctx.resource_uri.is_none())
|
||||
&& is_deferrable_mcp_result(result) =>
|
||||
{
|
||||
result.content.clear();
|
||||
}
|
||||
_ => return false,
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn is_deferrable_mcp_result(result: &codex_app_server_protocol::McpToolCallResult) -> bool {
|
||||
if result.structured_content.is_some() || result.meta.is_some() {
|
||||
return false;
|
||||
}
|
||||
let mut has_text = false;
|
||||
for block in &result.content {
|
||||
let Some(text) = plain_output_text(block) else {
|
||||
return false;
|
||||
};
|
||||
has_text |= !text.is_empty();
|
||||
}
|
||||
has_text
|
||||
}
|
||||
|
||||
fn stored_thread_item_to_entry(
|
||||
stored_item: codex_thread_store::StoredThreadItem,
|
||||
) -> Result<ThreadItemEntry, JSONRPCErrorError> {
|
||||
|
||||
Reference in New Issue
Block a user