agtrace_providers/gemini/
tools.rs

1/// Gemini provider-specific tool argument types
2///
3/// These structs represent the exact schema that Gemini uses, before normalization
4/// to the domain model in agtrace-types.
5use agtrace_types::{ExecuteArgs, FileEditArgs, FileReadArgs, FileWriteArgs, SearchArgs};
6use serde::{Deserialize, Serialize};
7use serde_json::json;
8
9/// Gemini read_file tool arguments
10///
11/// Gemini uses the same schema as domain model for read_file.
12///
13/// # Format
14/// ```json
15/// {
16///   "file_path": "src/main.rs"
17/// }
18/// ```
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct GeminiReadFileArgs {
21    pub file_path: String,
22}
23
24impl GeminiReadFileArgs {
25    /// Convert Gemini read_file args to standard FileReadArgs
26    pub fn to_file_read_args(&self) -> FileReadArgs {
27        FileReadArgs {
28            file_path: Some(self.file_path.clone()),
29            path: None,
30            pattern: None,
31            extra: json!({}),
32        }
33    }
34}
35
36/// Gemini write_file tool arguments
37///
38/// Gemini uses the same schema as domain model for write_file.
39///
40/// # Format
41/// ```json
42/// {
43///   "content": "...",
44///   "file_path": "test.txt"
45/// }
46/// ```
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct GeminiWriteFileArgs {
49    pub content: String,
50    pub file_path: String,
51}
52
53impl GeminiWriteFileArgs {
54    /// Convert Gemini write_file args to standard FileWriteArgs
55    pub fn to_file_write_args(&self) -> FileWriteArgs {
56        FileWriteArgs {
57            file_path: self.file_path.clone(),
58            content: self.content.clone(),
59        }
60    }
61}
62
63/// Gemini replace tool arguments
64///
65/// Gemini includes an `instruction` field that explains the edit,
66/// which is not present in the domain model.
67///
68/// # Format
69/// ```json
70/// {
71///   "file_path": "src/lib.rs",
72///   "instruction": "Update import statement...",
73///   "old_string": "old code",
74///   "new_string": "new code"
75/// }
76/// ```
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct GeminiReplaceArgs {
79    pub file_path: String,
80    /// Gemini-specific: explanation of the edit
81    #[serde(default, skip_serializing_if = "Option::is_none")]
82    pub instruction: Option<String>,
83    pub old_string: String,
84    pub new_string: String,
85}
86
87impl GeminiReplaceArgs {
88    /// Convert Gemini replace args to standard FileEditArgs
89    ///
90    /// Note: FileEditArgs doesn't have an `extra` field, so the Gemini-specific
91    /// `instruction` field is currently lost during normalization.
92    /// TODO: Add `extra` field to FileEditArgs in agtrace-types to preserve this.
93    ///
94    /// - Sets replace_all to false (Gemini doesn't support this option)
95    pub fn to_file_edit_args(&self) -> FileEditArgs {
96        // TODO: Preserve instruction in extra field when FileEditArgs supports it
97        FileEditArgs {
98            file_path: self.file_path.clone(),
99            old_string: self.old_string.clone(),
100            new_string: self.new_string.clone(),
101            replace_all: false, // Gemini doesn't support replace_all
102        }
103    }
104}
105
106/// Gemini run_shell_command tool arguments
107///
108/// Gemini uses the same schema as domain model for run_shell_command.
109///
110/// # Format
111/// ```json
112/// {
113///   "command": "ls -la",
114///   "description": "List files in directory"
115/// }
116/// ```
117#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct GeminiRunShellCommandArgs {
119    pub command: String,
120    #[serde(default, skip_serializing_if = "Option::is_none")]
121    pub description: Option<String>,
122}
123
124impl GeminiRunShellCommandArgs {
125    /// Convert Gemini run_shell_command args to standard ExecuteArgs
126    pub fn to_execute_args(&self) -> ExecuteArgs {
127        ExecuteArgs {
128            command: Some(self.command.clone()),
129            description: self.description.clone(),
130            timeout: None,
131            extra: json!({}),
132        }
133    }
134}
135
136/// Gemini google_web_search tool arguments
137///
138/// # Format
139/// ```json
140/// {
141///   "query": "rust async programming"
142/// }
143/// ```
144#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct GeminiGoogleWebSearchArgs {
146    pub query: String,
147}
148
149impl GeminiGoogleWebSearchArgs {
150    /// Convert Gemini google_web_search args to standard SearchArgs
151    pub fn to_search_args(&self) -> SearchArgs {
152        SearchArgs {
153            pattern: None,
154            query: Some(self.query.clone()),
155            input: None,
156            path: None,
157            extra: json!({}),
158        }
159    }
160}
161
162/// Gemini write_todos tool arguments
163///
164/// Gemini uses `description` instead of `content` for todo items.
165///
166/// # Format
167/// ```json
168/// {
169///   "todos": [
170///     {
171///       "description": "Task description",
172///       "status": "pending"
173///     }
174///   ]
175/// }
176/// ```
177///
178/// # Normalization Note
179/// This tool maps to ToolKind::Plan but currently falls back to Generic variant
180/// since there's no unified Plan variant in ToolCallPayload yet.
181/// The raw JSON is preserved in Generic.arguments.
182#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct GeminiWriteTodosArgs {
184    pub todos: Vec<GeminiTodoItem>,
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize)]
188pub struct GeminiTodoItem {
189    /// Task description (Gemini-specific field name)
190    pub description: String,
191    /// Task status: "pending", "in_progress", "completed", "cancelled"
192    pub status: String,
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    #[test]
200    fn test_read_file_args_conversion() {
201        let args = GeminiReadFileArgs {
202            file_path: "src/main.rs".to_string(),
203        };
204
205        let domain_args = args.to_file_read_args();
206        assert_eq!(domain_args.file_path, Some("src/main.rs".to_string()));
207        assert_eq!(domain_args.path, None);
208        assert_eq!(domain_args.pattern, None);
209        assert_eq!(domain_args.extra, json!({}));
210    }
211
212    #[test]
213    fn test_write_file_args_conversion() {
214        let args = GeminiWriteFileArgs {
215            content: "hello world".to_string(),
216            file_path: "test.txt".to_string(),
217        };
218
219        let domain_args = args.to_file_write_args();
220        assert_eq!(domain_args.file_path, "test.txt");
221        assert_eq!(domain_args.content, "hello world");
222    }
223
224    #[test]
225    fn test_replace_args_with_instruction() {
226        let args = GeminiReplaceArgs {
227            file_path: "src/lib.rs".to_string(),
228            instruction: Some("Update import statement".to_string()),
229            old_string: "old code".to_string(),
230            new_string: "new code".to_string(),
231        };
232
233        let domain_args = args.to_file_edit_args();
234        assert_eq!(domain_args.file_path, "src/lib.rs");
235        assert_eq!(domain_args.old_string, "old code");
236        assert_eq!(domain_args.new_string, "new code");
237        assert_eq!(domain_args.replace_all, false);
238        // Note: instruction is lost due to lack of extra field in FileEditArgs
239    }
240
241    #[test]
242    fn test_replace_args_without_instruction() {
243        let args = GeminiReplaceArgs {
244            file_path: "src/lib.rs".to_string(),
245            instruction: None,
246            old_string: "old".to_string(),
247            new_string: "new".to_string(),
248        };
249
250        let domain_args = args.to_file_edit_args();
251        assert_eq!(domain_args.file_path, "src/lib.rs");
252        assert_eq!(domain_args.old_string, "old");
253        assert_eq!(domain_args.new_string, "new");
254        assert_eq!(domain_args.replace_all, false);
255    }
256
257    #[test]
258    fn test_run_shell_command_args_conversion() {
259        let args = GeminiRunShellCommandArgs {
260            command: "ls -la".to_string(),
261            description: Some("List files".to_string()),
262        };
263
264        let domain_args = args.to_execute_args();
265        assert_eq!(domain_args.command, Some("ls -la".to_string()));
266        assert_eq!(domain_args.description, Some("List files".to_string()));
267        assert_eq!(domain_args.timeout, None);
268        assert_eq!(domain_args.extra, json!({}));
269    }
270
271    #[test]
272    fn test_run_shell_command_args_without_description() {
273        let args = GeminiRunShellCommandArgs {
274            command: "pwd".to_string(),
275            description: None,
276        };
277
278        let domain_args = args.to_execute_args();
279        assert_eq!(domain_args.command, Some("pwd".to_string()));
280        assert_eq!(domain_args.description, None);
281    }
282
283    #[test]
284    fn test_google_web_search_args_conversion() {
285        let args = GeminiGoogleWebSearchArgs {
286            query: "rust async".to_string(),
287        };
288
289        let domain_args = args.to_search_args();
290        assert_eq!(domain_args.query, Some("rust async".to_string()));
291        assert_eq!(domain_args.extra, json!({}));
292    }
293
294    #[test]
295    fn test_write_todos_args_parsing() {
296        let json_value = json!({
297            "todos": [
298                {
299                    "description": "Create cli directory",
300                    "status": "pending"
301                },
302                {
303                    "description": "Move import logic",
304                    "status": "in_progress"
305                }
306            ]
307        });
308
309        let args: GeminiWriteTodosArgs = serde_json::from_value(json_value).unwrap();
310        assert_eq!(args.todos.len(), 2);
311        assert_eq!(args.todos[0].description, "Create cli directory");
312        assert_eq!(args.todos[0].status, "pending");
313        assert_eq!(args.todos[1].description, "Move import logic");
314        assert_eq!(args.todos[1].status, "in_progress");
315    }
316}