Skip to main content

adk_tool/builtin/
anthropic.rs

1use adk_core::{AdkError, Result, Tool, ToolContext};
2use async_trait::async_trait;
3use serde::Deserialize;
4use serde_json::{Value, json};
5use std::io::{ErrorKind, Write};
6use std::path::{Path, PathBuf};
7use std::process::Stdio;
8use std::sync::Arc;
9
10fn invalid_input(message: impl Into<String>) -> AdkError {
11    AdkError::tool(message.into())
12}
13
14fn resolve_workspace_path(path: &str) -> std::result::Result<PathBuf, String> {
15    let raw = Path::new(path);
16    let resolved = if raw.is_absolute() {
17        raw.to_path_buf()
18    } else {
19        std::env::current_dir()
20            .map_err(|error| format!("failed to resolve current directory: {error}"))?
21            .join(raw)
22    };
23    Ok(resolved)
24}
25
26async fn render_view(path: &str, view_range: Option<(u32, u32)>) -> Result<String> {
27    if let Some((start, end)) = view_range
28        && (start == 0 || end == 0 || start > end)
29    {
30        return Err(invalid_input("view_range must use positive 1-based line numbers"));
31    }
32
33    let resolved = resolve_workspace_path(path).map_err(AdkError::tool)?;
34    let metadata = tokio::fs::metadata(&resolved)
35        .await
36        .map_err(|error| AdkError::tool(format!("failed to inspect '{path}': {error}")))?;
37
38    if metadata.is_dir() {
39        let mut entries = tokio::fs::read_dir(&resolved).await.map_err(|error| {
40            AdkError::tool(format!("failed to read directory '{path}': {error}"))
41        })?;
42        let mut listing = String::new();
43        while let Some(entry) = entries.next_entry().await.map_err(|error| {
44            AdkError::tool(format!("failed to read directory '{path}': {error}"))
45        })? {
46            let name = entry.file_name();
47            listing.push_str(&name.to_string_lossy());
48            listing.push('\n');
49        }
50        return Ok(listing);
51    }
52
53    let content = tokio::fs::read_to_string(&resolved)
54        .await
55        .map_err(|error| AdkError::tool(format!("failed to read '{path}': {error}")))?;
56
57    let lines: Vec<&str> = content.split_terminator('\n').collect();
58    let visible = lines
59        .iter()
60        .enumerate()
61        .filter_map(|(index, line)| {
62            let line_no = index as u32 + 1;
63            view_range
64                .map(|(start, end)| (start..=end).contains(&line_no))
65                .unwrap_or(true)
66                .then_some(*line)
67        })
68        .collect::<Vec<_>>()
69        .join("\n");
70
71    Ok(format!("{visible}\n"))
72}
73
74async fn create_file(path: &str, file_text: &str) -> Result<String> {
75    let resolved = resolve_workspace_path(path).map_err(AdkError::tool)?;
76    if let Some(parent) = resolved.parent() {
77        tokio::fs::create_dir_all(parent).await.map_err(|error| {
78            AdkError::tool(format!("failed to create parent directories for '{path}': {error}"))
79        })?;
80    }
81
82    let mut file =
83        std::fs::OpenOptions::new().create_new(true).write(true).open(&resolved).map_err(
84            |error| match error.kind() {
85                ErrorKind::AlreadyExists => AdkError::tool(format!("file '{path}' already exists")),
86                _ => AdkError::tool(format!("failed to create '{path}': {error}")),
87            },
88        )?;
89    file.write_all(file_text.as_bytes())
90        .map_err(|error| AdkError::tool(format!("failed to write '{path}': {error}")))?;
91    Ok("success".to_string())
92}
93
94async fn str_replace(path: &str, old_str: &str, new_str: &str) -> Result<String> {
95    let resolved = resolve_workspace_path(path).map_err(AdkError::tool)?;
96    let content = tokio::fs::read_to_string(&resolved)
97        .await
98        .map_err(|error| AdkError::tool(format!("failed to read '{path}': {error}")))?;
99
100    let matches = content.matches(old_str).count();
101    match matches {
102        0 => Err(invalid_input(format!("old_str not found in '{path}'"))),
103        1 => {
104            let updated = content.replacen(old_str, new_str, 1);
105            tokio::fs::write(&resolved, updated)
106                .await
107                .map_err(|error| AdkError::tool(format!("failed to update '{path}': {error}")))?;
108            Ok("success".to_string())
109        }
110        _ => Err(invalid_input(format!(
111            "old_str appears multiple times in '{path}'; use a more specific match"
112        ))),
113    }
114}
115
116async fn insert_text(path: &str, insert_line: u32, insert_text: &str) -> Result<String> {
117    if insert_line == 0 {
118        return Err(invalid_input("insert_line must be >= 1"));
119    }
120
121    let resolved = resolve_workspace_path(path).map_err(AdkError::tool)?;
122    let content = tokio::fs::read_to_string(&resolved)
123        .await
124        .map_err(|error| AdkError::tool(format!("failed to read '{path}': {error}")))?;
125    let mut lines = content.split_terminator('\n').map(str::to_string).collect::<Vec<_>>();
126
127    let insert_index = insert_line as usize - 1;
128    if insert_index > lines.len() {
129        return Err(invalid_input(format!(
130            "insert_line {insert_line} is out of range for '{path}'"
131        )));
132    }
133
134    lines.insert(insert_index, insert_text.to_string());
135    let mut updated = lines.join("\n");
136    updated.push('\n');
137    tokio::fs::write(&resolved, updated)
138        .await
139        .map_err(|error| AdkError::tool(format!("failed to update '{path}': {error}")))?;
140    Ok("success".to_string())
141}
142
143#[derive(Debug, Clone, Copy, PartialEq, Eq)]
144enum BashVersion {
145    V20241022,
146    V20250124,
147}
148
149impl BashVersion {
150    fn type_name(self) -> &'static str {
151        match self {
152            Self::V20241022 => "bash_20241022",
153            Self::V20250124 => "bash_20250124",
154        }
155    }
156}
157
158#[derive(Debug, Clone)]
159struct AnthropicBashTool {
160    version: BashVersion,
161}
162
163impl AnthropicBashTool {
164    const fn new(version: BashVersion) -> Self {
165        Self { version }
166    }
167
168    fn declaration_json(&self) -> Value {
169        json!({
170            "type": self.version.type_name(),
171            "name": "bash",
172        })
173    }
174}
175
176#[derive(Debug, Deserialize)]
177struct BashArgs {
178    command: String,
179    #[serde(default)]
180    restart: bool,
181}
182
183#[async_trait]
184impl Tool for AnthropicBashTool {
185    fn name(&self) -> &str {
186        "bash"
187    }
188
189    fn description(&self) -> &str {
190        "Executes shell commands for Anthropic's native bash tool."
191    }
192
193    fn declaration(&self) -> Value {
194        json!({
195            "name": self.name(),
196            "description": self.description(),
197            "x-adk-anthropic-tool": self.declaration_json(),
198        })
199    }
200
201    async fn execute(&self, _ctx: Arc<dyn ToolContext>, args: Value) -> Result<Value> {
202        let args: BashArgs = serde_json::from_value(args)
203            .map_err(|error| AdkError::tool(format!("invalid bash arguments: {error}")))?;
204        let output = tokio::process::Command::new("sh")
205            .arg("-lc")
206            .arg(&args.command)
207            .stdin(Stdio::null())
208            .output()
209            .await
210            .map_err(|error| AdkError::tool(format!("failed to execute bash command: {error}")))?;
211
212        let stdout = String::from_utf8_lossy(&output.stdout);
213        let stderr = String::from_utf8_lossy(&output.stderr);
214        let exit_code = output.status.code().unwrap_or(-1);
215        let restart_note =
216            if args.restart { "bash session restart requested before execution\n" } else { "" };
217
218        Ok(Value::String(format!("{restart_note}{stdout}{stderr}\nexit_code: {exit_code}\n")))
219    }
220}
221
222/// Anthropic native bash tool declaration for the `bash_20241022` version.
223#[derive(Debug, Clone, Default)]
224pub struct AnthropicBashTool20241022;
225
226impl AnthropicBashTool20241022 {
227    /// Create a new `bash_20241022` tool.
228    pub fn new() -> Self {
229        Self
230    }
231}
232
233#[async_trait]
234impl Tool for AnthropicBashTool20241022 {
235    fn name(&self) -> &str {
236        "bash"
237    }
238
239    fn description(&self) -> &str {
240        "Executes shell commands for Anthropic's native bash tool."
241    }
242
243    fn declaration(&self) -> Value {
244        AnthropicBashTool::new(BashVersion::V20241022).declaration()
245    }
246
247    async fn execute(&self, ctx: Arc<dyn ToolContext>, args: Value) -> Result<Value> {
248        AnthropicBashTool::new(BashVersion::V20241022).execute(ctx, args).await
249    }
250}
251
252/// Anthropic native bash tool declaration for the `bash_20250124` version.
253#[derive(Debug, Clone, Default)]
254pub struct AnthropicBashTool20250124;
255
256impl AnthropicBashTool20250124 {
257    /// Create a new `bash_20250124` tool.
258    pub fn new() -> Self {
259        Self
260    }
261}
262
263#[async_trait]
264impl Tool for AnthropicBashTool20250124 {
265    fn name(&self) -> &str {
266        "bash"
267    }
268
269    fn description(&self) -> &str {
270        "Executes shell commands for Anthropic's native bash tool."
271    }
272
273    fn declaration(&self) -> Value {
274        AnthropicBashTool::new(BashVersion::V20250124).declaration()
275    }
276
277    async fn execute(&self, ctx: Arc<dyn ToolContext>, args: Value) -> Result<Value> {
278        AnthropicBashTool::new(BashVersion::V20250124).execute(ctx, args).await
279    }
280}
281
282#[derive(Debug, Clone, Copy, PartialEq, Eq)]
283enum TextEditorVersion {
284    V20250124,
285    V20250429,
286    V20250728,
287}
288
289impl TextEditorVersion {
290    fn type_name(self) -> &'static str {
291        match self {
292            Self::V20250124 => "text_editor_20250124",
293            Self::V20250429 => "text_editor_20250429",
294            Self::V20250728 => "text_editor_20250728",
295        }
296    }
297
298    fn tool_name(self) -> &'static str {
299        match self {
300            Self::V20250124 => "str_replace_editor",
301            Self::V20250429 | Self::V20250728 => "str_replace_based_edit_tool",
302        }
303    }
304}
305
306#[derive(Debug, Clone)]
307struct AnthropicTextEditorTool {
308    version: TextEditorVersion,
309    max_characters: Option<i32>,
310}
311
312impl AnthropicTextEditorTool {
313    const fn new(version: TextEditorVersion, max_characters: Option<i32>) -> Self {
314        Self { version, max_characters }
315    }
316
317    fn declaration_json(&self) -> Value {
318        json!({
319            "type": self.version.type_name(),
320            "name": self.version.tool_name(),
321            "max_characters": self.max_characters,
322        })
323    }
324}
325
326#[derive(Debug, Deserialize)]
327struct ViewArgs {
328    path: String,
329    view_range: Option<(u32, u32)>,
330}
331
332#[derive(Debug, Deserialize)]
333struct StrReplaceArgs {
334    path: String,
335    old_str: String,
336    new_str: Option<String>,
337}
338
339#[derive(Debug, Deserialize)]
340struct InsertArgs {
341    path: String,
342    insert_line: u32,
343    insert_text: Option<String>,
344    new_str: Option<String>,
345}
346
347#[derive(Debug, Deserialize)]
348struct CreateArgs {
349    path: String,
350    file_text: String,
351}
352
353#[derive(Debug, Deserialize)]
354struct TextEditorCommand {
355    command: String,
356}
357
358#[async_trait]
359impl Tool for AnthropicTextEditorTool {
360    fn name(&self) -> &str {
361        self.version.tool_name()
362    }
363
364    fn description(&self) -> &str {
365        "Executes Anthropic's native text editor commands against local files."
366    }
367
368    fn declaration(&self) -> Value {
369        json!({
370            "name": self.name(),
371            "description": self.description(),
372            "x-adk-anthropic-tool": self.declaration_json(),
373        })
374    }
375
376    async fn execute(&self, _ctx: Arc<dyn ToolContext>, args: Value) -> Result<Value> {
377        let command: TextEditorCommand = serde_json::from_value(args.clone())
378            .map_err(|error| AdkError::tool(format!("invalid text editor arguments: {error}")))?;
379
380        let rendered = match command.command.as_str() {
381            "view" => {
382                let args: ViewArgs = serde_json::from_value(args).map_err(|error| {
383                    AdkError::tool(format!("invalid text editor view arguments: {error}"))
384                })?;
385                render_view(&args.path, args.view_range).await?
386            }
387            "str_replace" => {
388                let args: StrReplaceArgs = serde_json::from_value(args).map_err(|error| {
389                    AdkError::tool(format!("invalid text editor replace arguments: {error}"))
390                })?;
391                str_replace(&args.path, &args.old_str, args.new_str.as_deref().unwrap_or(""))
392                    .await?
393            }
394            "insert" => {
395                let args: InsertArgs = serde_json::from_value(args).map_err(|error| {
396                    AdkError::tool(format!("invalid text editor insert arguments: {error}"))
397                })?;
398                let payload = args.insert_text.or(args.new_str).ok_or_else(|| {
399                    invalid_input("text editor insert requires insert_text or new_str")
400                })?;
401                insert_text(&args.path, args.insert_line, &payload).await?
402            }
403            "create" => {
404                let args: CreateArgs = serde_json::from_value(args).map_err(|error| {
405                    AdkError::tool(format!("invalid text editor create arguments: {error}"))
406                })?;
407                create_file(&args.path, &args.file_text).await?
408            }
409            other => {
410                return Err(invalid_input(format!("unsupported text editor command '{other}'")));
411            }
412        };
413
414        Ok(Value::String(rendered))
415    }
416}
417
418/// Anthropic native text editor declaration for `text_editor_20250124`.
419#[derive(Debug, Clone, Default)]
420pub struct AnthropicTextEditorTool20250124;
421
422impl AnthropicTextEditorTool20250124 {
423    /// Create a new `text_editor_20250124` tool.
424    pub fn new() -> Self {
425        Self
426    }
427}
428
429#[async_trait]
430impl Tool for AnthropicTextEditorTool20250124 {
431    fn name(&self) -> &str {
432        "str_replace_editor"
433    }
434
435    fn description(&self) -> &str {
436        "Executes Anthropic's native text editor commands against local files."
437    }
438
439    fn declaration(&self) -> Value {
440        AnthropicTextEditorTool::new(TextEditorVersion::V20250124, None).declaration()
441    }
442
443    async fn execute(&self, ctx: Arc<dyn ToolContext>, args: Value) -> Result<Value> {
444        AnthropicTextEditorTool::new(TextEditorVersion::V20250124, None).execute(ctx, args).await
445    }
446}
447
448/// Anthropic native text editor declaration for `text_editor_20250429`.
449#[derive(Debug, Clone, Default)]
450pub struct AnthropicTextEditorTool20250429 {
451    max_characters: Option<i32>,
452}
453
454impl AnthropicTextEditorTool20250429 {
455    /// Create a new `text_editor_20250429` tool.
456    pub fn new() -> Self {
457        Self::default()
458    }
459
460    /// Limit the number of characters returned when viewing a file.
461    pub fn with_max_characters(mut self, max_characters: i32) -> Self {
462        self.max_characters = Some(max_characters);
463        self
464    }
465}
466
467#[async_trait]
468impl Tool for AnthropicTextEditorTool20250429 {
469    fn name(&self) -> &str {
470        "str_replace_based_edit_tool"
471    }
472
473    fn description(&self) -> &str {
474        "Executes Anthropic's native text editor commands against local files."
475    }
476
477    fn declaration(&self) -> Value {
478        AnthropicTextEditorTool::new(TextEditorVersion::V20250429, self.max_characters)
479            .declaration()
480    }
481
482    async fn execute(&self, ctx: Arc<dyn ToolContext>, args: Value) -> Result<Value> {
483        AnthropicTextEditorTool::new(TextEditorVersion::V20250429, self.max_characters)
484            .execute(ctx, args)
485            .await
486    }
487}
488
489/// Anthropic native text editor declaration for `text_editor_20250728`.
490#[derive(Debug, Clone, Default)]
491pub struct AnthropicTextEditorTool20250728 {
492    max_characters: Option<i32>,
493}
494
495impl AnthropicTextEditorTool20250728 {
496    /// Create a new `text_editor_20250728` tool.
497    pub fn new() -> Self {
498        Self::default()
499    }
500
501    /// Limit the number of characters returned when viewing a file.
502    pub fn with_max_characters(mut self, max_characters: i32) -> Self {
503        self.max_characters = Some(max_characters);
504        self
505    }
506}
507
508#[async_trait]
509impl Tool for AnthropicTextEditorTool20250728 {
510    fn name(&self) -> &str {
511        "str_replace_based_edit_tool"
512    }
513
514    fn description(&self) -> &str {
515        "Executes Anthropic's native text editor commands against local files."
516    }
517
518    fn declaration(&self) -> Value {
519        AnthropicTextEditorTool::new(TextEditorVersion::V20250728, self.max_characters)
520            .declaration()
521    }
522
523    async fn execute(&self, ctx: Arc<dyn ToolContext>, args: Value) -> Result<Value> {
524        AnthropicTextEditorTool::new(TextEditorVersion::V20250728, self.max_characters)
525            .execute(ctx, args)
526            .await
527    }
528}