Skip to main content

adk_devtools/tools/
read.rs

1//! `read_file` — return the (optionally sliced) contents of a file.
2
3use 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
12/// Reads a UTF-8 text file from the workspace and returns it with line numbers.
13pub struct ReadFileTool {
14    workspace: Workspace,
15}
16
17impl ReadFileTool {
18    /// Create a `read_file` tool bound to `workspace`.
19    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}