adk_devtools/tools/
read.rs1use std::sync::Arc;
4
5use adk_core::{Result, Tool, ToolContext};
6use async_trait::async_trait;
7use serde_json::{Value, json};
8
9use crate::error::DevToolError;
10use crate::workspace::Workspace;
11
12pub struct ReadFileTool {
14 workspace: Workspace,
15}
16
17impl ReadFileTool {
18 pub fn new(workspace: Workspace) -> Self {
20 Self { workspace }
21 }
22}
23
24#[async_trait]
25impl Tool for ReadFileTool {
26 fn name(&self) -> &str {
27 "read_file"
28 }
29
30 fn description(&self) -> &str {
31 "Read a text file from the workspace and return its contents with line numbers. \
32 Supports optional `offset` (1-based start line) and `limit` (number of lines)."
33 }
34
35 fn is_read_only(&self) -> bool {
36 true
37 }
38
39 fn is_concurrency_safe(&self) -> bool {
40 true
41 }
42
43 fn parameters_schema(&self) -> Option<Value> {
44 Some(json!({
45 "type": "object",
46 "properties": {
47 "path": { "type": "string", "description": "Path relative to the workspace root." },
48 "offset": { "type": "integer", "description": "1-based first line to return (optional)." },
49 "limit": { "type": "integer", "description": "Maximum number of lines to return (optional)." }
50 },
51 "required": ["path"]
52 }))
53 }
54
55 async fn execute(&self, _ctx: Arc<dyn ToolContext>, args: Value) -> Result<Value> {
56 let path = require_str(&args, "path")?;
57 let resolved = self.workspace.resolve(&path)?;
58
59 let contents = tokio::fs::read_to_string(&resolved).await.map_err(DevToolError::from)?;
60 self.workspace.mark_read(&resolved);
61
62 let offset = args.get("offset").and_then(Value::as_u64).unwrap_or(1).max(1) as usize;
63 let limit = args.get("limit").and_then(Value::as_u64).map(|v| v as usize);
64
65 let mut lines = String::new();
66 let mut total = 0usize;
67 let mut shown = 0usize;
68 for (idx, line) in contents.lines().enumerate() {
69 total += 1;
70 let line_no = idx + 1;
71 if line_no < offset {
72 continue;
73 }
74 if let Some(limit) = limit
75 && shown >= limit
76 {
77 break;
78 }
79 lines.push_str(&format!("{line_no:>6}\t{line}\n"));
80 shown += 1;
81 }
82
83 Ok(json!({
84 "path": self.workspace.display(&resolved),
85 "content": lines,
86 "total_lines": total,
87 "returned_lines": shown,
88 }))
89 }
90}
91
92pub(crate) fn require_str(args: &Value, key: &str) -> Result<String> {
93 args.get(key).and_then(Value::as_str).map(str::to_string).ok_or_else(|| {
94 adk_core::AdkError::tool(format!("missing required string argument '{key}'"))
95 })
96}