agtrace_providers/codex/
mapper.rs

1use agtrace_types::{ToolCallPayload, ToolKind, ToolOrigin};
2use serde_json::Value;
3
4use crate::codex::tools::{ApplyPatchArgs, PatchOperation, ReadMcpResourceArgs, ShellArgs};
5use agtrace_types::{ExecuteArgs, FileEditArgs, FileWriteArgs};
6
7/// Normalize Codex-specific tool calls
8///
9/// Handles provider-specific tools like apply_patch before falling back to generic normalization.
10pub(crate) fn normalize_codex_tool_call(
11    tool_name: String,
12    arguments: serde_json::Value,
13    provider_call_id: Option<String>,
14) -> ToolCallPayload {
15    // Handle Codex-specific tools
16    match tool_name.as_str() {
17        "apply_patch" => {
18            // Try to parse as ApplyPatchArgs
19            if let Ok(patch_args) = serde_json::from_value::<ApplyPatchArgs>(arguments.clone()) {
20                // Parse the patch structure
21                match patch_args.parse() {
22                    Ok(parsed) => {
23                        // Map to FileWrite (Add) or FileEdit (Update) based on operation
24                        match parsed.operation {
25                            PatchOperation::Add => {
26                                // New file creation → FileWrite
27                                return ToolCallPayload::FileWrite {
28                                    name: tool_name,
29                                    arguments: FileWriteArgs {
30                                        file_path: parsed.file_path,
31                                        content: parsed.raw_patch,
32                                    },
33                                    provider_call_id,
34                                };
35                            }
36                            PatchOperation::Update => {
37                                // File modification → FileEdit
38                                // Note: For patches, we store the raw patch in old_string/new_string
39                                // as a placeholder. The actual diff is in the raw patch.
40                                return ToolCallPayload::FileEdit {
41                                    name: tool_name,
42                                    arguments: FileEditArgs {
43                                        file_path: parsed.file_path,
44                                        old_string: String::new(), // Placeholder: actual diff in raw patch
45                                        new_string: parsed.raw_patch.clone(),
46                                        replace_all: false,
47                                    },
48                                    provider_call_id,
49                                };
50                            }
51                        }
52                    }
53                    Err(_) => {
54                        // Parsing failed, fall back to generic
55                    }
56                }
57            }
58        }
59        "shell" => {
60            // Try to parse as ShellArgs
61            if let Ok(shell_args) = serde_json::from_value::<ShellArgs>(arguments.clone()) {
62                // Convert to standard ExecuteArgs
63                let execute_args = shell_args.to_execute_args();
64                return ToolCallPayload::Execute {
65                    name: tool_name,
66                    arguments: execute_args,
67                    provider_call_id,
68                };
69            }
70        }
71        "read_mcp_resource" => {
72            // Try to parse as ReadMcpResourceArgs
73            if let Ok(mcp_args) = serde_json::from_value::<ReadMcpResourceArgs>(arguments.clone()) {
74                // Convert to standard FileReadArgs
75                let file_read_args = mcp_args.to_file_read_args();
76                return ToolCallPayload::FileRead {
77                    name: tool_name,
78                    arguments: file_read_args,
79                    provider_call_id,
80                };
81            }
82        }
83        "shell_command" => {
84            // shell_command → Execute (already uses string command format)
85            if let Ok(args) = serde_json::from_value::<ExecuteArgs>(arguments.clone()) {
86                return ToolCallPayload::Execute {
87                    name: tool_name,
88                    arguments: args,
89                    provider_call_id,
90                };
91            }
92        }
93        _ if tool_name.starts_with("mcp__") => {
94            // MCP tools
95            if let Ok(args) = serde_json::from_value(arguments.clone()) {
96                return ToolCallPayload::Mcp {
97                    name: tool_name,
98                    arguments: args,
99                    provider_call_id,
100                };
101            }
102        }
103        _ => {
104            // Unknown Codex tool, fall through to Generic
105        }
106    }
107
108    // Fallback to generic
109    ToolCallPayload::Generic {
110        name: tool_name,
111        arguments,
112        provider_call_id,
113    }
114}
115
116/// Codex tool mapper implementation
117pub struct CodexToolMapper;
118
119impl crate::traits::ToolMapper for CodexToolMapper {
120    fn classify(&self, tool_name: &str) -> (ToolOrigin, ToolKind) {
121        super::tool_mapping::classify_tool(tool_name)
122            .unwrap_or_else(|| crate::tool_analyzer::classify_common(tool_name))
123    }
124
125    fn normalize_call(&self, name: &str, args: Value, call_id: Option<String>) -> ToolCallPayload {
126        normalize_codex_tool_call(name.to_string(), args, call_id)
127    }
128
129    fn summarize(&self, kind: ToolKind, args: &Value) -> String {
130        crate::tool_analyzer::extract_common_summary(kind, args)
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    #[test]
139    fn test_normalize_apply_patch_update_file() {
140        let raw_patch = r#"*** Begin Patch
141*** Update File: test.rs
142@@
143-old line
144+new line
145@@
146*** End Patch"#;
147
148        let arguments = serde_json::json!({ "raw": raw_patch });
149        let payload = normalize_codex_tool_call(
150            "apply_patch".to_string(),
151            arguments,
152            Some("call_456".to_string()),
153        );
154
155        match payload {
156            ToolCallPayload::FileEdit {
157                name,
158                arguments,
159                provider_call_id,
160            } => {
161                assert_eq!(name, "apply_patch");
162                assert_eq!(arguments.file_path, "test.rs");
163                assert!(arguments.new_string.contains("*** Begin Patch"));
164                assert_eq!(provider_call_id, Some("call_456".to_string()));
165            }
166            _ => panic!("Expected FileEdit variant"),
167        }
168    }
169
170    #[test]
171    fn test_normalize_apply_patch_add_file() {
172        let raw_patch = r#"*** Begin Patch
173*** Add File: newfile.txt
174@@
175+new content
176@@
177*** End Patch"#;
178
179        let arguments = serde_json::json!({ "raw": raw_patch });
180        let payload = normalize_codex_tool_call(
181            "apply_patch".to_string(),
182            arguments,
183            Some("call_789".to_string()),
184        );
185
186        match payload {
187            ToolCallPayload::FileWrite {
188                name,
189                arguments,
190                provider_call_id,
191            } => {
192                assert_eq!(name, "apply_patch");
193                assert_eq!(arguments.file_path, "newfile.txt");
194                assert!(arguments.content.contains("*** Begin Patch"));
195                assert_eq!(provider_call_id, Some("call_789".to_string()));
196            }
197            _ => panic!("Expected FileWrite variant"),
198        }
199    }
200
201    #[test]
202    fn test_normalize_shell_command() {
203        let arguments = serde_json::json!({
204            "command": ["ls", "-la"],
205            "cwd": "/home/user",
206            "description": "List files"
207        });
208
209        let payload =
210            normalize_codex_tool_call("shell".to_string(), arguments, Some("call_123".to_string()));
211
212        match payload {
213            ToolCallPayload::Execute {
214                name,
215                arguments,
216                provider_call_id,
217            } => {
218                assert_eq!(name, "shell");
219                assert_eq!(arguments.command, Some("ls -la".to_string()));
220                assert_eq!(provider_call_id, Some("call_123".to_string()));
221            }
222            _ => panic!("Expected Execute variant"),
223        }
224    }
225
226    #[test]
227    fn test_normalize_shell_minimal() {
228        let arguments = serde_json::json!({
229            "command": ["echo", "hello"]
230        });
231
232        let payload = normalize_codex_tool_call("shell".to_string(), arguments, None);
233
234        match payload {
235            ToolCallPayload::Execute {
236                name, arguments, ..
237            } => {
238                assert_eq!(name, "shell");
239                assert_eq!(arguments.command, Some("echo hello".to_string()));
240            }
241            _ => panic!("Expected Execute variant"),
242        }
243    }
244
245    #[test]
246    fn test_normalize_shell_with_all_fields() {
247        let arguments = serde_json::json!({
248            "command": ["python", "script.py"],
249            "cwd": "/workspace",
250            "description": "Run Python script",
251            "timeout_ms": 5000
252        });
253
254        let payload = normalize_codex_tool_call("shell".to_string(), arguments, None);
255
256        match payload {
257            ToolCallPayload::Execute {
258                name, arguments, ..
259            } => {
260                assert_eq!(name, "shell");
261                assert_eq!(arguments.command, Some("python script.py".to_string()));
262            }
263            _ => panic!("Expected Execute variant"),
264        }
265    }
266
267    #[test]
268    fn test_normalize_read_mcp_resource() {
269        let arguments = serde_json::json!({
270            "server": "local",
271            "uri": "file:///path/to/file.txt"
272        });
273
274        let payload = normalize_codex_tool_call(
275            "read_mcp_resource".to_string(),
276            arguments,
277            Some("call_999".to_string()),
278        );
279
280        match payload {
281            ToolCallPayload::FileRead {
282                name,
283                arguments,
284                provider_call_id,
285            } => {
286                assert_eq!(name, "read_mcp_resource");
287                assert_eq!(
288                    arguments.file_path,
289                    Some("file:///path/to/file.txt".to_string())
290                );
291                assert_eq!(provider_call_id, Some("call_999".to_string()));
292            }
293            _ => panic!("Expected FileRead variant"),
294        }
295    }
296}