1use agtrace_types::{ToolCallPayload, ToolKind, ToolOrigin};
2use serde_json::Value;
3
4use crate::codex::tools::{ApplyPatchArgs, PatchOperation, ReadMcpResourceArgs, ShellArgs};
5use agtrace_types::{ExecuteArgs, FileEditArgs, FileReadArgs, FileWriteArgs, SearchArgs};
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
65 if let Some(command) = &execute_args.command {
67 match super::execute_intent::classify_execute_command(command) {
68 Some(ToolKind::Search) => {
69 let pattern = super::execute_intent::extract_search_pattern(command);
71
72 return ToolCallPayload::Search {
73 name: tool_name,
74 arguments: SearchArgs {
75 pattern,
76 query: None,
77 input: None,
78 path: None,
79 extra: serde_json::json!({"command": command}),
80 },
81 provider_call_id,
82 };
83 }
84 Some(ToolKind::Read) => {
85 let file_path = super::execute_intent::extract_file_path(command);
87
88 return ToolCallPayload::FileRead {
89 name: tool_name,
90 arguments: FileReadArgs {
91 file_path,
92 path: None,
93 pattern: None,
94 extra: serde_json::json!({"command": command}),
95 },
96 provider_call_id,
97 };
98 }
99 _ => {}
100 }
101 }
102
103 return ToolCallPayload::Execute {
104 name: tool_name,
105 arguments: execute_args,
106 provider_call_id,
107 };
108 }
109 }
110 "read_mcp_resource" => {
111 if let Ok(mcp_args) = serde_json::from_value::<ReadMcpResourceArgs>(arguments.clone()) {
113 let file_read_args = mcp_args.to_file_read_args();
115 return ToolCallPayload::FileRead {
116 name: tool_name,
117 arguments: file_read_args,
118 provider_call_id,
119 };
120 }
121 }
122 "shell_command" => {
123 if let Ok(args) = serde_json::from_value::<ExecuteArgs>(arguments.clone()) {
125 if let Some(command) = &args.command {
127 match super::execute_intent::classify_execute_command(command) {
128 Some(ToolKind::Search) => {
129 let pattern = super::execute_intent::extract_search_pattern(command);
131
132 return ToolCallPayload::Search {
133 name: tool_name,
134 arguments: SearchArgs {
135 pattern,
136 query: None,
137 input: None,
138 path: None,
139 extra: serde_json::json!({"command": command}),
140 },
141 provider_call_id,
142 };
143 }
144 Some(ToolKind::Read) => {
145 let file_path = super::execute_intent::extract_file_path(command);
147
148 return ToolCallPayload::FileRead {
149 name: tool_name,
150 arguments: FileReadArgs {
151 file_path,
152 path: None,
153 pattern: None,
154 extra: serde_json::json!({"command": command}),
155 },
156 provider_call_id,
157 };
158 }
159 _ => {}
160 }
161 }
162
163 return ToolCallPayload::Execute {
164 name: tool_name,
165 arguments: args,
166 provider_call_id,
167 };
168 }
169 }
170 _ if tool_name.starts_with("mcp__") => {
171 let (server, tool) = super::tool_mapping::parse_mcp_name(&tool_name)
173 .map(|(s, t)| (Some(s), Some(t)))
174 .unwrap_or((None, None));
175
176 if let Ok(mut args) =
177 serde_json::from_value::<agtrace_types::McpArgs>(arguments.clone())
178 {
179 args.server = server;
180 args.tool = tool;
181
182 return ToolCallPayload::Mcp {
183 name: tool_name,
184 arguments: args,
185 provider_call_id,
186 };
187 }
188 }
189 _ => {
190 }
192 }
193
194 ToolCallPayload::Generic {
196 name: tool_name,
197 arguments,
198 provider_call_id,
199 }
200}
201
202pub struct CodexToolMapper;
204
205impl crate::traits::ToolMapper for CodexToolMapper {
206 fn classify(&self, tool_name: &str) -> (ToolOrigin, ToolKind) {
207 super::tool_mapping::classify_tool(tool_name)
208 .unwrap_or_else(|| crate::tool_analyzer::classify_common(tool_name))
209 }
210
211 fn normalize_call(&self, name: &str, args: Value, call_id: Option<String>) -> ToolCallPayload {
212 normalize_codex_tool_call(name.to_string(), args, call_id)
213 }
214
215 fn summarize(&self, kind: ToolKind, args: &Value) -> String {
216 crate::tool_analyzer::extract_common_summary(kind, args)
217 }
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223
224 #[test]
225 fn test_normalize_apply_patch_update_file() {
226 let raw_patch = r#"*** Begin Patch
227*** Update File: test.rs
228@@
229-old line
230+new line
231@@
232*** End Patch"#;
233
234 let arguments = serde_json::json!({ "raw": raw_patch });
235 let payload = normalize_codex_tool_call(
236 "apply_patch".to_string(),
237 arguments,
238 Some("call_456".to_string()),
239 );
240
241 match payload {
242 ToolCallPayload::FileEdit {
243 name,
244 arguments,
245 provider_call_id,
246 } => {
247 assert_eq!(name, "apply_patch");
248 assert_eq!(arguments.file_path, "test.rs");
249 assert!(arguments.new_string.contains("*** Begin Patch"));
250 assert_eq!(provider_call_id, Some("call_456".to_string()));
251 }
252 _ => panic!("Expected FileEdit variant"),
253 }
254 }
255
256 #[test]
257 fn test_normalize_apply_patch_add_file() {
258 let raw_patch = r#"*** Begin Patch
259*** Add File: newfile.txt
260@@
261+new content
262@@
263*** End Patch"#;
264
265 let arguments = serde_json::json!({ "raw": raw_patch });
266 let payload = normalize_codex_tool_call(
267 "apply_patch".to_string(),
268 arguments,
269 Some("call_789".to_string()),
270 );
271
272 match payload {
273 ToolCallPayload::FileWrite {
274 name,
275 arguments,
276 provider_call_id,
277 } => {
278 assert_eq!(name, "apply_patch");
279 assert_eq!(arguments.file_path, "newfile.txt");
280 assert!(arguments.content.contains("*** Begin Patch"));
281 assert_eq!(provider_call_id, Some("call_789".to_string()));
282 }
283 _ => panic!("Expected FileWrite variant"),
284 }
285 }
286
287 #[test]
288 fn test_normalize_shell_command() {
289 let arguments = serde_json::json!({
291 "command": ["ls", "-la"],
292 "cwd": "/home/user",
293 "description": "List files"
294 });
295
296 let payload =
297 normalize_codex_tool_call("shell".to_string(), arguments, Some("call_123".to_string()));
298
299 match payload {
300 ToolCallPayload::FileRead {
301 name,
302 provider_call_id,
303 ..
304 } => {
305 assert_eq!(name, "shell");
306 assert_eq!(provider_call_id, Some("call_123".to_string()));
307 }
308 _ => panic!(
309 "Expected FileRead variant for ls command, got: {:?}",
310 payload.kind()
311 ),
312 }
313 }
314
315 #[test]
316 fn test_normalize_shell_minimal() {
317 let arguments = serde_json::json!({
318 "command": ["echo", "hello"]
319 });
320
321 let payload = normalize_codex_tool_call("shell".to_string(), arguments, None);
322
323 match payload {
324 ToolCallPayload::Execute {
325 name, arguments, ..
326 } => {
327 assert_eq!(name, "shell");
328 assert_eq!(arguments.command, Some("echo hello".to_string()));
329 }
330 _ => panic!("Expected Execute variant"),
331 }
332 }
333
334 #[test]
335 fn test_normalize_shell_with_all_fields() {
336 let arguments = serde_json::json!({
337 "command": ["python", "script.py"],
338 "cwd": "/workspace",
339 "description": "Run Python script",
340 "timeout_ms": 5000
341 });
342
343 let payload = normalize_codex_tool_call("shell".to_string(), arguments, None);
344
345 match payload {
346 ToolCallPayload::Execute {
347 name, arguments, ..
348 } => {
349 assert_eq!(name, "shell");
350 assert_eq!(arguments.command, Some("python script.py".to_string()));
351 }
352 _ => panic!("Expected Execute variant"),
353 }
354 }
355
356 #[test]
357 fn test_normalize_read_mcp_resource() {
358 let arguments = serde_json::json!({
359 "server": "local",
360 "uri": "file:///path/to/file.txt"
361 });
362
363 let payload = normalize_codex_tool_call(
364 "read_mcp_resource".to_string(),
365 arguments,
366 Some("call_999".to_string()),
367 );
368
369 match payload {
370 ToolCallPayload::FileRead {
371 name,
372 arguments,
373 provider_call_id,
374 } => {
375 assert_eq!(name, "read_mcp_resource");
376 assert_eq!(
377 arguments.file_path,
378 Some("file:///path/to/file.txt".to_string())
379 );
380 assert_eq!(provider_call_id, Some("call_999".to_string()));
381 }
382 _ => panic!("Expected FileRead variant"),
383 }
384 }
385
386 #[test]
387 fn test_normalize_mcp_tool_parses_server_and_tool() {
388 let payload = normalize_codex_tool_call(
389 "mcp__filesystem__read".to_string(),
390 serde_json::json!({"path": "/tmp/file.txt"}),
391 Some("call_mcp".to_string()),
392 );
393
394 match payload {
395 ToolCallPayload::Mcp {
396 name,
397 arguments,
398 provider_call_id,
399 } => {
400 assert_eq!(name, "mcp__filesystem__read");
401 assert_eq!(arguments.server, Some("filesystem".to_string()));
402 assert_eq!(arguments.tool, Some("read".to_string()));
403 assert_eq!(
404 arguments.inner.get("path").and_then(|v| v.as_str()),
405 Some("/tmp/file.txt")
406 );
407 assert_eq!(provider_call_id, Some("call_mcp".to_string()));
408 }
409 _ => panic!("Expected Mcp variant"),
410 }
411 }
412
413 #[test]
414 fn test_normalize_mcp_tool_handles_malformed_name() {
415 let payload = normalize_codex_tool_call(
416 "mcp__noserver".to_string(),
417 serde_json::json!({"data": "test"}),
418 None,
419 );
420
421 match payload {
422 ToolCallPayload::Mcp {
423 name,
424 arguments,
425 provider_call_id,
426 } => {
427 assert_eq!(name, "mcp__noserver");
428 assert_eq!(arguments.server, None);
430 assert_eq!(arguments.tool, None);
431 assert_eq!(provider_call_id, None);
432 }
433 _ => panic!("Expected Mcp variant"),
434 }
435 }
436
437 #[test]
438 fn test_normalize_shell_read_command_cat() {
439 let payload = normalize_codex_tool_call(
440 "shell".to_string(),
441 serde_json::json!({
442 "command": ["cat", "file.txt"]
443 }),
444 Some("call_read".to_string()),
445 );
446
447 match payload {
448 ToolCallPayload::FileRead {
449 name,
450 arguments,
451 provider_call_id,
452 } => {
453 assert_eq!(name, "shell");
454 assert_eq!(arguments.file_path, Some("file.txt".to_string()));
455 assert_eq!(provider_call_id, Some("call_read".to_string()));
456 }
457 _ => panic!(
458 "Expected FileRead variant for cat command, got: {:?}",
459 payload.kind()
460 ),
461 }
462 }
463
464 #[test]
465 fn test_normalize_shell_read_command_sed() {
466 let payload = normalize_codex_tool_call(
467 "shell".to_string(),
468 serde_json::json!({
469 "command": ["sed", "-n", "1,200p", "packages/extension-inspector/src/App.tsx"]
470 }),
471 None,
472 );
473
474 match payload {
475 ToolCallPayload::FileRead {
476 name, arguments, ..
477 } => {
478 assert_eq!(name, "shell");
479 assert_eq!(
480 arguments.file_path,
481 Some("packages/extension-inspector/src/App.tsx".to_string())
482 );
483 }
484 _ => panic!(
485 "Expected FileRead variant for sed -n command, got: {:?}",
486 payload.kind()
487 ),
488 }
489 }
490
491 #[test]
492 fn test_normalize_shell_read_command_ls() {
493 let payload = normalize_codex_tool_call(
494 "shell".to_string(),
495 serde_json::json!({
496 "command": ["ls", "-la"]
497 }),
498 None,
499 );
500
501 match payload {
502 ToolCallPayload::FileRead { name, .. } => {
503 assert_eq!(name, "shell");
504 }
506 _ => panic!(
507 "Expected FileRead variant for ls command, got: {:?}",
508 payload.kind()
509 ),
510 }
511 }
512
513 #[test]
514 fn test_normalize_shell_write_command_mkdir() {
515 let payload = normalize_codex_tool_call(
516 "shell".to_string(),
517 serde_json::json!({
518 "command": ["mkdir", "-p", "mydir"]
519 }),
520 None,
521 );
522
523 match payload {
524 ToolCallPayload::Execute { name, .. } => {
525 assert_eq!(name, "shell");
526 }
528 _ => panic!(
529 "Expected Execute variant for mkdir command, got: {:?}",
530 payload.kind()
531 ),
532 }
533 }
534
535 #[test]
536 fn test_normalize_shell_command_read() {
537 let payload = normalize_codex_tool_call(
539 "shell_command".to_string(),
540 serde_json::json!({
541 "command": "cat file.txt"
542 }),
543 None,
544 );
545
546 match payload {
547 ToolCallPayload::FileRead {
548 name, arguments, ..
549 } => {
550 assert_eq!(name, "shell_command");
551 assert_eq!(arguments.file_path, Some("file.txt".to_string()));
552 }
553 _ => panic!(
554 "Expected FileRead variant for cat command, got: {:?}",
555 payload.kind()
556 ),
557 }
558 }
559
560 #[test]
561 fn test_normalize_shell_bash_wrapped_read() {
562 let payload = normalize_codex_tool_call(
563 "shell".to_string(),
564 serde_json::json!({
565 "command": ["bash", "-lc", "cat", "Cargo.toml"]
566 }),
567 None,
568 );
569
570 match payload {
571 ToolCallPayload::FileRead {
572 name, arguments, ..
573 } => {
574 assert_eq!(name, "shell");
575 assert_eq!(arguments.file_path, Some("Cargo.toml".to_string()));
576 }
577 _ => panic!(
578 "Expected FileRead variant for bash-wrapped cat, got: {:?}",
579 payload.kind()
580 ),
581 }
582 }
583
584 #[test]
585 fn test_normalize_shell_search_rg() {
586 let payload = normalize_codex_tool_call(
587 "shell_command".to_string(),
588 serde_json::json!({
589 "command": "rg -n \"context window\" docs -S"
590 }),
591 None,
592 );
593
594 match payload {
595 ToolCallPayload::Search {
596 name, arguments, ..
597 } => {
598 assert_eq!(name, "shell_command");
599 assert_eq!(arguments.pattern, Some("context window".to_string()));
600 }
601 _ => panic!(
602 "Expected Search variant for rg command, got: {:?}",
603 payload.kind()
604 ),
605 }
606 }
607
608 #[test]
609 fn test_normalize_shell_read_rg_files() {
610 let payload = normalize_codex_tool_call(
611 "shell_command".to_string(),
612 serde_json::json!({
613 "command": "rg --files docs"
614 }),
615 None,
616 );
617
618 match payload {
619 ToolCallPayload::FileRead { name, .. } => {
620 assert_eq!(name, "shell_command");
621 }
623 _ => panic!(
624 "Expected FileRead variant for rg --files, got: {:?}",
625 payload.kind()
626 ),
627 }
628 }
629
630 #[test]
631 fn test_normalize_shell_search_grep() {
632 let payload = normalize_codex_tool_call(
633 "shell".to_string(),
634 serde_json::json!({
635 "command": ["grep", "-n", "TODO", "src/main.rs"]
636 }),
637 None,
638 );
639
640 match payload {
641 ToolCallPayload::Search {
642 name, arguments, ..
643 } => {
644 assert_eq!(name, "shell");
645 assert_eq!(arguments.pattern, Some("TODO".to_string()));
646 }
647 _ => panic!(
648 "Expected Search variant for grep command, got: {:?}",
649 payload.kind()
650 ),
651 }
652 }
653}