agtrace_providers/codex/
tools.rs

1/// Codex provider-specific tool argument types
2///
3/// These structs represent the exact schema that Codex uses, before normalization
4/// to the domain model in agtrace-types.
5use agtrace_types::{ExecuteArgs, FileReadArgs};
6use serde::{Deserialize, Serialize};
7use serde_json::json;
8
9/// Codex apply_patch tool arguments
10///
11/// Raw patch format used by Codex for both file creation and modification.
12///
13/// # Format
14/// ```text
15/// *** Begin Patch
16/// *** Add File: path/to/file.rs
17/// +content line 1
18/// +content line 2
19/// *** End Patch
20/// ```
21///
22/// or
23///
24/// ```text
25/// *** Begin Patch
26/// *** Update File: path/to/file.rs
27/// @@
28///  context line
29/// -old line
30/// +new line
31/// @@
32///  another context
33/// -another old
34/// +another new
35/// *** End Patch
36/// ```
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct ApplyPatchArgs {
39    /// Raw patch content including Begin/End markers, file path header, and diff hunks
40    pub raw: String,
41}
42
43/// Parsed patch structure extracted from ApplyPatchArgs
44///
45/// This represents the structured view of Codex's patch format after parsing.
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct ParsedPatch {
48    /// Operation type (Add or Update)
49    pub operation: PatchOperation,
50    /// Target file path
51    pub file_path: String,
52    /// Original raw patch for preservation
53    pub raw_patch: String,
54}
55
56/// Patch operation type
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum PatchOperation {
59    /// File creation (*** Add File:)
60    Add,
61    /// File modification (*** Update File:)
62    Update,
63}
64
65impl ApplyPatchArgs {
66    /// Parse the raw patch into structured format
67    ///
68    /// # Errors
69    /// Returns error if:
70    /// - No file path header found (neither "Add File:" nor "Update File:")
71    /// - Invalid patch format
72    pub fn parse(&self) -> Result<ParsedPatch, String> {
73        let raw = &self.raw;
74
75        // Find operation and file path
76        for line in raw.lines() {
77            if let Some(path) = line.strip_prefix("*** Add File: ") {
78                return Ok(ParsedPatch {
79                    operation: PatchOperation::Add,
80                    file_path: path.trim().to_string(),
81                    raw_patch: raw.clone(),
82                });
83            }
84            if let Some(path) = line.strip_prefix("*** Update File: ") {
85                return Ok(ParsedPatch {
86                    operation: PatchOperation::Update,
87                    file_path: path.trim().to_string(),
88                    raw_patch: raw.clone(),
89                });
90            }
91        }
92
93        Err("Failed to parse patch: no file path header found".to_string())
94    }
95}
96
97/// Codex shell tool arguments
98///
99/// Codex uses array command format instead of string format.
100///
101/// # Format
102/// ```json
103/// {
104///   "command": ["bash", "-lc", "ls"],
105///   "timeout_ms": 10000,
106///   "workdir": "/path/to/dir"
107/// }
108/// ```
109#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct ShellArgs {
111    /// Command as array of strings (e.g., ["bash", "-lc", "ls"])
112    pub command: Vec<String>,
113    /// Timeout in milliseconds
114    #[serde(default, skip_serializing_if = "Option::is_none")]
115    pub timeout_ms: Option<u64>,
116    /// Working directory
117    #[serde(default, skip_serializing_if = "Option::is_none")]
118    pub workdir: Option<String>,
119}
120
121impl ShellArgs {
122    /// Convert Codex shell args to standard ExecuteArgs
123    ///
124    /// - Joins command array into a single string
125    /// - Converts timeout_ms to timeout
126    /// - Preserves workdir in extra field
127    pub fn to_execute_args(&self) -> ExecuteArgs {
128        let command_str = self.command.join(" ");
129
130        let mut extra = json!({});
131        if let Some(workdir) = &self.workdir {
132            extra["workdir"] = json!(workdir);
133        }
134
135        ExecuteArgs {
136            command: Some(command_str),
137            description: None,
138            timeout: self.timeout_ms,
139            extra,
140        }
141    }
142}
143
144/// Codex read_mcp_resource tool arguments
145///
146/// Codex uses MCP protocol to read resources via a server.
147///
148/// # Format
149/// ```json
150/// {
151///   "server": "local",
152///   "uri": "/path/to/file"
153/// }
154/// ```
155#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct ReadMcpResourceArgs {
157    /// MCP server name (e.g., "local")
158    pub server: String,
159    /// Resource URI (file path for local server)
160    pub uri: String,
161}
162
163impl ReadMcpResourceArgs {
164    /// Convert Codex read_mcp_resource args to standard FileReadArgs
165    ///
166    /// - Maps uri → file_path
167    /// - Preserves server in extra field
168    pub fn to_file_read_args(&self) -> FileReadArgs {
169        let mut extra = json!({});
170        extra["server"] = json!(&self.server);
171
172        FileReadArgs {
173            file_path: Some(self.uri.clone()),
174            path: None,
175            pattern: None,
176            extra,
177        }
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184
185    #[test]
186    fn test_parse_add_file_patch() {
187        let args = ApplyPatchArgs {
188            raw: r#"*** Begin Patch
189*** Add File: docs/test.md
190+# Test Document
191+
192+This is a test.
193*** End Patch"#
194                .to_string(),
195        };
196
197        let parsed = args.parse().unwrap();
198        assert_eq!(parsed.operation, PatchOperation::Add);
199        assert_eq!(parsed.file_path, "docs/test.md");
200    }
201
202    #[test]
203    fn test_parse_update_file_patch() {
204        let args = ApplyPatchArgs {
205            raw: r#"*** Begin Patch
206*** Update File: src/lib.rs
207@@
208 fn example() {
209-    old_code()
210+    new_code()
211 }
212*** End Patch"#
213                .to_string(),
214        };
215
216        let parsed = args.parse().unwrap();
217        assert_eq!(parsed.operation, PatchOperation::Update);
218        assert_eq!(parsed.file_path, "src/lib.rs");
219    }
220
221    #[test]
222    fn test_parse_invalid_patch() {
223        let args = ApplyPatchArgs {
224            raw: "*** Begin Patch\nno header\n*** End Patch".to_string(),
225        };
226
227        assert!(args.parse().is_err());
228    }
229
230    #[test]
231    fn test_shell_args_to_execute_args() {
232        let shell_args = ShellArgs {
233            command: vec!["bash".to_string(), "-lc".to_string(), "ls".to_string()],
234            timeout_ms: Some(10000),
235            workdir: Some("/path/to/dir".to_string()),
236        };
237
238        let execute_args = shell_args.to_execute_args();
239        assert_eq!(execute_args.command, Some("bash -lc ls".to_string()));
240        assert_eq!(execute_args.timeout, Some(10000));
241        assert_eq!(
242            execute_args.extra.get("workdir"),
243            Some(&json!("/path/to/dir"))
244        );
245    }
246
247    #[test]
248    fn test_shell_args_without_optional_fields() {
249        let shell_args = ShellArgs {
250            command: vec!["echo".to_string(), "hello".to_string()],
251            timeout_ms: None,
252            workdir: None,
253        };
254
255        let execute_args = shell_args.to_execute_args();
256        assert_eq!(execute_args.command, Some("echo hello".to_string()));
257        assert_eq!(execute_args.timeout, None);
258        assert_eq!(execute_args.extra, json!({}));
259    }
260
261    #[test]
262    fn test_read_mcp_resource_args_to_file_read_args() {
263        let mcp_args = ReadMcpResourceArgs {
264            server: "local".to_string(),
265            uri: "/foo-bar-hoge-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/AGENTS.md".to_string(),
266        };
267
268        let file_read_args = mcp_args.to_file_read_args();
269        assert_eq!(
270            file_read_args.file_path,
271            Some("/foo-bar-hoge-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/AGENTS.md".to_string())
272        );
273        assert_eq!(file_read_args.path, None);
274        assert_eq!(file_read_args.pattern, None);
275        assert_eq!(file_read_args.extra.get("server"), Some(&json!("local")));
276    }
277}