mirror of
https://github.com/openai/codex.git
synced 2026-11-03 00:21:31 +00:00
## Summary Extends rollout tracing across tool dispatch and code-mode runtime boundaries. This records canonical tool-call lifecycle events and links code-mode execution/wait operations back to the model-visible calls that caused them. ## Stack This is PR 3/5 in the rollout trace stack. - [#18876](https://github.com/openai/codex/pull/18876): Add rollout trace crate - [#18877](https://github.com/openai/codex/pull/18877): Record core session rollout traces - [#18878](https://github.com/openai/codex/pull/18878): Trace tool and code-mode boundaries - [#18879](https://github.com/openai/codex/pull/18879): Trace sessions and multi-agent edges - [#18880](https://github.com/openai/codex/pull/18880): Add debug trace reduction command ## Review Notes This PR is about attribution. Reviewers should focus on whether direct tool calls, code-mode-originated tool calls, waits, outputs, and cancellation boundaries are recorded with enough source information for deterministic reduction without coupling the reducer to live runtime internals. The stack remains valid after this layer: tool and code-mode traces reduce through the existing crate model, while the broader session and multi-agent relationships are added in the next PR.
222 lines
7.2 KiB
Rust
222 lines
7.2 KiB
Rust
use std::sync::Arc;
|
|
use std::time::Instant;
|
|
|
|
use crate::function_tool::FunctionCallError;
|
|
use crate::mcp_tool_call::handle_mcp_tool_call;
|
|
use crate::original_image_detail::can_request_original_image_detail;
|
|
use crate::tools::context::McpToolOutput;
|
|
use crate::tools::context::ToolInvocation;
|
|
use crate::tools::context::ToolOutput;
|
|
use crate::tools::context::ToolPayload;
|
|
use crate::tools::hook_names::HookToolName;
|
|
use crate::tools::registry::PostToolUsePayload;
|
|
use crate::tools::registry::PreToolUsePayload;
|
|
use crate::tools::registry::ToolHandler;
|
|
use crate::tools::registry::ToolKind;
|
|
use serde_json::Value;
|
|
|
|
pub struct McpHandler;
|
|
impl ToolHandler for McpHandler {
|
|
type Output = McpToolOutput;
|
|
|
|
fn kind(&self) -> ToolKind {
|
|
ToolKind::Mcp
|
|
}
|
|
|
|
fn pre_tool_use_payload(&self, invocation: &ToolInvocation) -> Option<PreToolUsePayload> {
|
|
let ToolPayload::Mcp { raw_arguments, .. } = &invocation.payload else {
|
|
return None;
|
|
};
|
|
|
|
Some(PreToolUsePayload {
|
|
tool_name: HookToolName::new(invocation.tool_name.display()),
|
|
tool_input: mcp_hook_tool_input(raw_arguments),
|
|
})
|
|
}
|
|
|
|
fn post_tool_use_payload(
|
|
&self,
|
|
invocation: &ToolInvocation,
|
|
result: &Self::Output,
|
|
) -> Option<PostToolUsePayload> {
|
|
let ToolPayload::Mcp { .. } = &invocation.payload else {
|
|
return None;
|
|
};
|
|
|
|
let tool_response =
|
|
result.post_tool_use_response(&invocation.call_id, &invocation.payload)?;
|
|
Some(PostToolUsePayload {
|
|
tool_name: HookToolName::new(invocation.tool_name.display()),
|
|
tool_use_id: invocation.call_id.clone(),
|
|
tool_input: result.tool_input.clone(),
|
|
tool_response,
|
|
})
|
|
}
|
|
|
|
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
|
|
let ToolInvocation {
|
|
session,
|
|
turn,
|
|
call_id,
|
|
tool_name: model_tool_name,
|
|
payload,
|
|
..
|
|
} = invocation;
|
|
|
|
let payload = match payload {
|
|
ToolPayload::Mcp {
|
|
server,
|
|
tool,
|
|
raw_arguments,
|
|
} => (server, tool, raw_arguments),
|
|
_ => {
|
|
return Err(FunctionCallError::RespondToModel(
|
|
"mcp handler received unsupported payload".to_string(),
|
|
));
|
|
}
|
|
};
|
|
|
|
let (server, tool, raw_arguments) = payload;
|
|
let arguments_str = raw_arguments;
|
|
|
|
let started = Instant::now();
|
|
let result = handle_mcp_tool_call(
|
|
Arc::clone(&session),
|
|
&turn,
|
|
call_id.clone(),
|
|
server,
|
|
tool,
|
|
model_tool_name.display(),
|
|
arguments_str,
|
|
)
|
|
.await;
|
|
|
|
Ok(McpToolOutput {
|
|
result: result.result,
|
|
tool_input: result.tool_input,
|
|
wall_time: started.elapsed(),
|
|
original_image_detail_supported: can_request_original_image_detail(&turn.model_info),
|
|
})
|
|
}
|
|
}
|
|
|
|
fn mcp_hook_tool_input(raw_arguments: &str) -> Value {
|
|
if raw_arguments.trim().is_empty() {
|
|
return Value::Object(serde_json::Map::new());
|
|
}
|
|
|
|
serde_json::from_str(raw_arguments).unwrap_or_else(|_| Value::String(raw_arguments.to_string()))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::session::tests::make_session_and_context;
|
|
use crate::tools::context::ToolCallSource;
|
|
use crate::turn_diff_tracker::TurnDiffTracker;
|
|
use pretty_assertions::assert_eq;
|
|
use serde_json::json;
|
|
use std::time::Duration;
|
|
use tokio::sync::Mutex;
|
|
|
|
#[tokio::test]
|
|
async fn mcp_pre_tool_use_payload_uses_model_tool_name_and_raw_args() {
|
|
let payload = ToolPayload::Mcp {
|
|
server: "memory".to_string(),
|
|
tool: "create_entities".to_string(),
|
|
raw_arguments: json!({
|
|
"entities": [{
|
|
"name": "Ada",
|
|
"entityType": "person"
|
|
}]
|
|
})
|
|
.to_string(),
|
|
};
|
|
let (session, turn) = make_session_and_context().await;
|
|
|
|
assert_eq!(
|
|
McpHandler.pre_tool_use_payload(&ToolInvocation {
|
|
session: session.into(),
|
|
turn: turn.into(),
|
|
cancellation_token: tokio_util::sync::CancellationToken::new(),
|
|
tracker: Arc::new(Mutex::new(TurnDiffTracker::new())),
|
|
call_id: "call-mcp-pre".to_string(),
|
|
tool_name: codex_tools::ToolName::namespaced("mcp__memory__", "create_entities"),
|
|
source: ToolCallSource::Direct,
|
|
payload,
|
|
}),
|
|
Some(PreToolUsePayload {
|
|
tool_name: HookToolName::new("mcp__memory__create_entities"),
|
|
tool_input: json!({
|
|
"entities": [{
|
|
"name": "Ada",
|
|
"entityType": "person"
|
|
}]
|
|
}),
|
|
})
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn mcp_post_tool_use_payload_uses_model_tool_name_args_and_result() {
|
|
let payload = ToolPayload::Mcp {
|
|
server: "filesystem".to_string(),
|
|
tool: "read_file".to_string(),
|
|
raw_arguments: json!({ "path": "/tmp/notes.txt" }).to_string(),
|
|
};
|
|
let output = McpToolOutput {
|
|
result: codex_protocol::mcp::CallToolResult {
|
|
content: vec![json!({
|
|
"type": "text",
|
|
"text": "notes"
|
|
})],
|
|
structured_content: Some(json!({ "bytes": 5 })),
|
|
is_error: None,
|
|
meta: None,
|
|
},
|
|
tool_input: json!({
|
|
"path": {
|
|
"file_id": "file_123"
|
|
}
|
|
}),
|
|
wall_time: Duration::from_millis(42),
|
|
original_image_detail_supported: true,
|
|
};
|
|
let (session, turn) = make_session_and_context().await;
|
|
let invocation = ToolInvocation {
|
|
session: session.into(),
|
|
turn: turn.into(),
|
|
cancellation_token: tokio_util::sync::CancellationToken::new(),
|
|
tracker: Arc::new(Mutex::new(TurnDiffTracker::new())),
|
|
call_id: "call-mcp-post".to_string(),
|
|
tool_name: codex_tools::ToolName::namespaced("mcp__filesystem__", "read_file"),
|
|
source: ToolCallSource::Direct,
|
|
payload,
|
|
};
|
|
assert_eq!(
|
|
McpHandler.post_tool_use_payload(&invocation, &output),
|
|
Some(PostToolUsePayload {
|
|
tool_name: HookToolName::new("mcp__filesystem__read_file"),
|
|
tool_use_id: "call-mcp-post".to_string(),
|
|
tool_input: json!({
|
|
"path": {
|
|
"file_id": "file_123"
|
|
}
|
|
}),
|
|
tool_response: json!({
|
|
"content": [{
|
|
"type": "text",
|
|
"text": "notes"
|
|
}],
|
|
"structuredContent": { "bytes": 5 }
|
|
}),
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn mcp_hook_tool_input_defaults_empty_args_to_object() {
|
|
assert_eq!(mcp_hook_tool_input(" "), json!({}));
|
|
}
|
|
}
|