Include originating item IDs in MCP request metadata (#40866)

## What changed

- Add the originating Responses item ID to MCP tool request metadata as
  `_meta.itemId`.
- Preserve the initiating `exec` item ID across code-mode cells so nested MCP
  calls carry the same origin.

## Testing

- Cover metadata generation for direct and code-mode MCP tool calls.

GitOrigin-RevId: 1161cac64f1cc3a6d4dac4368e63804bd9d2c417
This commit is contained in:
jif
2026-08-26 10:39:05 +00:00
committed by copyberry
parent 74772623db
commit f5420174da
9 changed files with 175 additions and 49 deletions
+44 -7
View File
@@ -7,6 +7,7 @@ use codex_code_mode::CodeModeNestedToolCall;
use codex_code_mode::CodeModeSessionDelegate;
use codex_code_mode::NotificationFuture;
use codex_code_mode::ToolInvocationFuture;
use codex_protocol::ResponseItemId;
use codex_protocol::models::FunctionCallOutputPayload;
use codex_protocol::models::ResponseItem;
use serde_json::Value as JsonValue;
@@ -24,7 +25,13 @@ use crate::tools::parallel::ToolCallRuntime;
pub(super) struct CodeModeDispatchBroker {
dispatch_tx: async_channel::Sender<DispatchMessage>,
dispatch_rx: async_channel::Receiver<DispatchMessage>,
dispatch_gates: Arc<Mutex<HashMap<CellId, watch::Sender<bool>>>>,
dispatch_gates: Arc<Mutex<HashMap<CellId, CellDispatchGate>>>,
}
struct CellDispatchGate {
ready: watch::Sender<bool>,
// Keep the original exec item when later waits resume this cell.
originating_item_id: Option<ResponseItemId>,
}
impl CodeModeDispatchBroker {
@@ -37,8 +44,34 @@ impl CodeModeDispatchBroker {
}
}
pub(super) fn mark_cell_ready_for_dispatch(&self, cell_id: &CellId) {
dispatch_gate(&self.dispatch_gates, cell_id).send_replace(true);
pub(super) fn mark_cell_ready_for_dispatch(
&self,
cell_id: &CellId,
originating_item_id: Option<ResponseItemId>,
) {
let ready = {
let mut dispatch_gates = self
.dispatch_gates
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let gate = dispatch_gates
.entry(cell_id.clone())
.or_insert_with(|| CellDispatchGate {
ready: watch::channel(false).0,
originating_item_id: None,
});
gate.originating_item_id = originating_item_id;
gate.ready.clone()
};
ready.send_replace(true);
}
pub(super) fn cell_originating_item_id(&self, cell_id: &CellId) -> Option<ResponseItemId> {
self.dispatch_gates
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.get(cell_id)
.and_then(|gate| gate.originating_item_id.clone())
}
pub(super) fn close_cell(&self, cell_id: &CellId) {
@@ -135,7 +168,7 @@ impl CodeModeDispatchBroker {
}
fn dispatch_gate(
dispatch_gates: &Mutex<HashMap<CellId, watch::Sender<bool>>>,
dispatch_gates: &Mutex<HashMap<CellId, CellDispatchGate>>,
cell_id: &CellId,
) -> watch::Sender<bool> {
let mut dispatch_gates = match dispatch_gates.lock() {
@@ -144,12 +177,16 @@ fn dispatch_gate(
};
dispatch_gates
.entry(cell_id.clone())
.or_insert_with(|| watch::channel(false).0)
.or_insert_with(|| CellDispatchGate {
ready: watch::channel(false).0,
originating_item_id: None,
})
.ready
.clone()
}
fn remove_dispatch_gate(
dispatch_gates: &Mutex<HashMap<CellId, watch::Sender<bool>>>,
dispatch_gates: &Mutex<HashMap<CellId, CellDispatchGate>>,
cell_id: &CellId,
) {
let mut dispatch_gates = match dispatch_gates.lock() {
@@ -160,7 +197,7 @@ fn remove_dispatch_gate(
}
async fn wait_until_cell_ready_for_dispatch(
dispatch_gates: &Mutex<HashMap<CellId, watch::Sender<bool>>>,
dispatch_gates: &Mutex<HashMap<CellId, CellDispatchGate>>,
cell_id: &CellId,
cancellation_token: &CancellationToken,
) -> bool {
@@ -35,6 +35,7 @@ impl CodeModeExecuteHandler {
session: std::sync::Arc<crate::session::session::Session>,
turn: std::sync::Arc<crate::session::turn_context::TurnContext>,
call_id: String,
originating_item_id: Option<codex_protocol::ResponseItemId>,
code: String,
telemetry: &mut CodeModeToolCallGuard,
) -> Result<FunctionToolOutput, FunctionCallError> {
@@ -103,7 +104,7 @@ impl CodeModeExecuteHandler {
exec.session
.services
.code_mode_service
.mark_cell_ready_for_dispatch(&cell_id);
.mark_cell_ready_for_dispatch(&cell_id, originating_item_id);
let response = started_cell
.initial_response()
.await
@@ -155,6 +156,7 @@ impl CodeModeExecuteHandler {
&self,
invocation: ToolInvocation,
) -> Result<Box<dyn crate::tools::context::ToolOutput>, FunctionCallError> {
let originating_item_id = invocation.originating_item_id().await;
let ToolInvocation {
session,
turn,
@@ -174,7 +176,14 @@ impl CodeModeExecuteHandler {
);
let result = match payload {
ToolPayload::Custom { input } if is_exec_tool_name(&tool_name) => self
.execute(session, turn, call_id, input, &mut telemetry)
.execute(
session,
turn,
call_id,
originating_item_id,
input,
&mut telemetry,
)
.await
.map(boxed_tool_output),
_ => Err(FunctionCallError::RespondToModel(format!(
+14 -2
View File
@@ -176,8 +176,20 @@ impl CodeModeService {
}
}
pub(crate) fn mark_cell_ready_for_dispatch(&self, cell_id: &codex_code_mode::CellId) {
self.dispatch_broker.mark_cell_ready_for_dispatch(cell_id);
pub(crate) fn mark_cell_ready_for_dispatch(
&self,
cell_id: &codex_code_mode::CellId,
originating_item_id: Option<codex_protocol::ResponseItemId>,
) {
self.dispatch_broker
.mark_cell_ready_for_dispatch(cell_id, originating_item_id);
}
pub(crate) fn cell_originating_item_id(
&self,
cell_id: &codex_code_mode::CellId,
) -> Option<codex_protocol::ResponseItemId> {
self.dispatch_broker.cell_originating_item_id(cell_id)
}
pub(crate) fn finish_cell_dispatch(&self, cell_id: &CellId) {
+30
View File
@@ -6,11 +6,13 @@ use crate::session::turn_context::TurnContext;
use crate::turn_diff_tracker::TurnDiffTracker;
use crate::unified_exec::format_output_omission_marker;
use crate::unified_exec::resolve_max_tokens;
use codex_protocol::ResponseItemId;
use codex_protocol::mcp::CallToolResult;
use codex_protocol::models::FunctionCallOutputBody;
use codex_protocol::models::FunctionCallOutputContentItem;
use codex_protocol::models::FunctionCallOutputPayload;
use codex_protocol::models::ResponseInputItem;
use codex_protocol::models::ResponseItem;
use codex_protocol::models::function_call_output_content_items_to_text;
use codex_tools::LoadableToolSpec;
use codex_tools::ToolName;
@@ -66,6 +68,34 @@ pub struct ToolInvocation {
pub payload: ToolPayload,
}
impl ToolInvocation {
/// Returns the Responses item that requested this call or started its code-mode cell.
pub(crate) async fn originating_item_id(&self) -> Option<ResponseItemId> {
if let ToolCallSource::CodeMode { cell_id, .. } = &self.source {
return self
.session
.services
.code_mode_service
.cell_originating_item_id(&codex_code_mode::CellId::new(cell_id.clone()));
}
self.session
.clone_history()
.await
.raw_items()
.rev()
.find_map(|item| match item {
ResponseItem::FunctionCall { id, call_id, .. }
| ResponseItem::CustomToolCall { id, call_id, .. }
if call_id == &self.call_id =>
{
id.clone()
}
_ => None,
})
}
}
#[derive(Clone, Debug)]
pub struct McpToolOutput {
pub result: CallToolResult,
+2
View File
@@ -171,6 +171,7 @@ impl McpHandler {
&self,
invocation: ToolInvocation,
) -> Result<Box<dyn crate::tools::context::ToolOutput>, FunctionCallError> {
let originating_item_id = invocation.originating_item_id().await;
let ToolInvocation {
session,
step_context,
@@ -197,6 +198,7 @@ impl McpHandler {
&step_context,
&cancellation_token,
call_id.clone(),
originating_item_id,
&self.tool_info,
self.hook_tool_name(),
tool_name,