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
7pub(crate) fn normalize_codex_tool_call(
11 tool_name: String,
12 arguments: serde_json::Value,
13 provider_call_id: Option<String>,
14) -> ToolCallPayload {
15 match tool_name.as_str() {
17 "apply_patch" => {
18 if let Ok(patch_args) = serde_json::from_value::<ApplyPatchArgs>(arguments.clone()) {
20 match patch_args.parse() {
22 Ok(parsed) => {
23 match parsed.operation {
25 PatchOperation::Add => {
26 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 return ToolCallPayload::FileEdit {
41 name: tool_name,
42 arguments: FileEditArgs {
43 file_path: parsed.file_path,
44 old_string: String::new(), new_string: parsed.raw_patch.clone(),
46 replace_all: false,
47 },
48 provider_call_id,
49 };
50 }
51 }
52 }
53 Err(_) => {
54 }
56 }
57 }
58 }
59 "shell" => {
60 if let Ok(shell_args) = serde_json::from_value::<ShellArgs>(arguments.clone()) {
62 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 if let Ok(mcp_args) = serde_json::from_value::<ReadMcpResourceArgs>(arguments.clone()) {
74 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 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 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 }
106 }
107
108 ToolCallPayload::Generic {
110 name: tool_name,
111 arguments,
112 provider_call_id,
113 }
114}
115
116pub 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}