agtrace_providers/codex/
tool_mapping.rs

1use crate::tool_spec::ToolSpec;
2use agtrace_types::{ToolKind, ToolOrigin};
3use serde_json::Value;
4
5/// Registry of Codex tools
6///
7/// # Note on ToolOrigin classification
8/// `read_mcp_resource` is classified as System, not Mcp, because it is a
9/// provider-native tool built into Codex. The tool happens to read MCP resources,
10/// but the invocation itself is NOT via MCP protocol. Only tools invoked via
11/// MCP protocol (typically prefixed with `mcp__`) should use ToolOrigin::Mcp.
12const CODEX_TOOLS: &[ToolSpec] = &[
13    // Write tools
14    ToolSpec::new("apply_patch", ToolOrigin::System, ToolKind::Write),
15    // Read tools
16    // NOTE: read_mcp_resource is System because it's a Codex-native tool,
17    // even though it reads MCP resources. Origin is about invocation, not target.
18    ToolSpec::new("read_mcp_resource", ToolOrigin::System, ToolKind::Read),
19    // Execute tools
20    ToolSpec::new("shell", ToolOrigin::System, ToolKind::Execute),
21    ToolSpec::new("shell_command", ToolOrigin::System, ToolKind::Execute),
22    // Plan tools
23    ToolSpec::new("update_plan", ToolOrigin::System, ToolKind::Plan),
24];
25
26/// Classify Codex tool by origin and semantic kind
27pub fn classify_tool(tool_name: &str) -> Option<(ToolOrigin, ToolKind)> {
28    // Check registry first
29    if let Some(spec) = CODEX_TOOLS.iter().find(|t| t.name == tool_name) {
30        return Some((spec.origin, spec.kind));
31    }
32
33    // Handle MCP protocol tools (invoked via MCP, not just accessing MCP resources)
34    // These tools are prefixed with "mcp__" and are external integrations
35    if tool_name.starts_with("mcp__") {
36        return Some((ToolOrigin::Mcp, ToolKind::Other));
37    }
38
39    None
40}
41
42/// Extract summary from Codex tool arguments
43pub fn extract_summary(tool_name: &str, _kind: ToolKind, arguments: &Value) -> Option<String> {
44    match tool_name {
45        // apply_patch: extract filename from raw patch
46        "apply_patch" => extract_patch_filename(arguments),
47
48        // shell: join array command
49        "shell" => extract_shell_command(arguments),
50
51        // update_plan: extract explanation
52        "update_plan" => arguments
53            .get("explanation")
54            .and_then(|v| v.as_str())
55            .map(|s| s.to_string()),
56
57        // Default: None (use common logic)
58        _ => None,
59    }
60}
61
62fn extract_patch_filename(args: &Value) -> Option<String> {
63    let raw = args.get("raw")?.as_str()?;
64    // Try to extract "*** Update File: path/to/file.rs"
65    let start = raw.find("Update File: ")?;
66    let rest = &raw[start + 13..];
67    let end = rest.find('\n').unwrap_or(rest.len());
68    Some(rest[..end].trim().to_string())
69}
70
71fn extract_shell_command(args: &Value) -> Option<String> {
72    let cmd_array = args.get("command")?.as_array()?;
73    let cmd_str = cmd_array
74        .iter()
75        .filter_map(|s| s.as_str())
76        .collect::<Vec<_>>()
77        .join(" ");
78    Some(cmd_str)
79}