[codex] preserve MCP result meta in McpToolCallItemResult (#22946)

## Summary

https://openai.slack.com/archives/C0ARA9UAQEA/p1778890981647319?thread_ts=1778888537.934319&cid=C0ARA9UAQEA


- Add `_meta` to exec JSONL MCP tool call result events.
- Copy MCP result metadata through the JSONL event conversion.
- Add a focused test that verifies `_meta` is serialized as `_meta` and
not `meta`.


## Verification

https://www.notion.so/openai/Miaolin-0516-_meta-population-debug-3628e50b62b08074b365e0ce1ffb8f74
This commit is contained in:
Miaolin Min
2026-05-16 13:27:44 -07:00
committed by GitHub
parent b200dd1b6f
commit 6941f5c2c5
5 changed files with 58 additions and 0 deletions

View File

@@ -223,6 +223,7 @@ impl EventProcessorWithJsonOutput {
arguments,
result: result.map(|result| McpToolCallItemResult {
content: result.content,
meta: result.meta,
structured_content: result.structured_content,
}),
error: error.map(|error| McpToolCallItemError {

View File

@@ -1,5 +1,6 @@
use super::*;
use pretty_assertions::assert_eq;
use serde_json::json;
use tempfile::tempdir;
#[test]
@@ -57,3 +58,53 @@ fn failed_turn_does_not_overwrite_output_last_message_file() {
"keep existing contents"
);
}
#[test]
fn mcp_tool_call_result_preserves_meta_in_jsonl_event() {
let mut processor = EventProcessorWithJsonOutput::new(/*last_message_path*/ None);
let collected = processor.collect_thread_events(ServerNotification::ItemCompleted(
codex_app_server_protocol::ItemCompletedNotification {
item: ThreadItem::McpToolCall {
id: "mcp-1".to_string(),
server: "search service".to_string(),
tool: "web_run".to_string(),
status: McpToolCallStatus::Completed,
arguments: json!({"search_query": [{"q": "OpenAI Codex CLI documentation"}]}),
mcp_app_resource_uri: None,
result: Some(Box::new(codex_app_server_protocol::McpToolCallResult {
content: vec![json!({"type": "text", "text": "search result"})],
structured_content: None,
meta: Some(json!({"raw_messages": [{"ref_id": "turn0search0"}]})),
})),
error: None,
duration_ms: Some(42),
},
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
completed_at_ms: 0,
},
));
assert_eq!(collected.status, CodexStatus::Running);
assert_eq!(collected.events.len(), 1);
let ThreadEvent::ItemCompleted(ItemCompletedEvent { item }) = &collected.events[0] else {
panic!("expected item.completed event");
};
let ThreadItemDetails::McpToolCall(item) = &item.details else {
panic!("expected MCP tool call item");
};
let result = item.result.as_ref().expect("expected MCP tool result");
assert_eq!(
result.meta,
Some(json!({"raw_messages": [{"ref_id": "turn0search0"}]}))
);
let serialized = serde_json::to_value(&collected.events[0]).expect("serialize event");
assert_eq!(
serialized["item"]["result"]["_meta"],
json!({"raw_messages": [{"ref_id": "turn0search0"}]})
);
assert!(serialized["item"]["result"].get("meta").is_none());
}

View File

@@ -266,6 +266,9 @@ pub struct McpToolCallItemResult {
// representations). Using `JsonValue` keeps the payload wire-shaped and
// easy to export.
pub content: Vec<JsonValue>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub meta: Option<JsonValue>,
pub structured_content: Option<JsonValue>,
}

View File

@@ -540,6 +540,7 @@ fn mcp_tool_call_begin_and_end_emit_item_events() {
arguments: json!({ "key": "value" }),
result: Some(McpToolCallItemResult {
content: Vec::new(),
meta: None,
structured_content: None,
}),
error: None,
@@ -681,6 +682,7 @@ fn mcp_tool_call_defaults_arguments_and_preserves_structured_content() {
"type": "text",
"text": "done",
})],
meta: None,
structured_content: Some(json!({ "status": "ok" })),
}),
error: None,

View File

@@ -60,6 +60,7 @@ export type McpToolCallItem = {
/** Result payload returned by the MCP server for successful calls. */
result?: {
content: McpContentBlock[];
_meta?: unknown;
structured_content: unknown;
};
/** Error message reported for failed calls. */