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#[derive(Debug, Clone, Default)]
224pub struct AnthropicBashTool20241022;
225
226impl AnthropicBashTool20241022 {
227 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#[derive(Debug, Clone, Default)]
254pub struct AnthropicBashTool20250124;
255
256impl AnthropicBashTool20250124 {
257 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#[derive(Debug, Clone, Default)]
420pub struct AnthropicTextEditorTool20250124;
421
422impl AnthropicTextEditorTool20250124 {
423 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#[derive(Debug, Clone, Default)]
450pub struct AnthropicTextEditorTool20250429 {
451 max_characters: Option<i32>,
452}
453
454impl AnthropicTextEditorTool20250429 {
455 pub fn new() -> Self {
457 Self::default()
458 }
459
460 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#[derive(Debug, Clone, Default)]
491pub struct AnthropicTextEditorTool20250728 {
492 max_characters: Option<i32>,
493}
494
495impl AnthropicTextEditorTool20250728 {
496 pub fn new() -> Self {
498 Self::default()
499 }
500
501 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}