llm_coding_tools_rig/absolute/
read.rs

1//! Read file tool using [`AbsolutePathResolver`].
2
3use llm_coding_tools_core::operations::read_file;
4use llm_coding_tools_core::path::AbsolutePathResolver;
5use llm_coding_tools_core::tool_names;
6use llm_coding_tools_core::{ToolContext, ToolError, ToolOutput};
7use rig::completion::ToolDefinition;
8use rig::tool::Tool;
9use schemars::{schema_for, JsonSchema};
10use serde::Deserialize;
11
12const DEFAULT_OFFSET: usize = 1;
13const DEFAULT_LIMIT: usize = 2000;
14
15fn default_offset() -> usize {
16    DEFAULT_OFFSET
17}
18
19fn default_limit() -> usize {
20    DEFAULT_LIMIT
21}
22
23/// Arguments for the read file tool.
24#[derive(Debug, Clone, Deserialize, JsonSchema)]
25pub struct ReadArgs {
26    /// Absolute path to the file to read.
27    pub file_path: String,
28    /// 1-indexed line number to start reading from (default: 1).
29    #[serde(default = "default_offset")]
30    pub offset: usize,
31    /// Maximum number of lines to return (default: 2000).
32    #[serde(default = "default_limit")]
33    pub limit: usize,
34}
35
36/// Tool for reading file contents with optional line numbers.
37#[derive(Debug, Clone, Default)]
38pub struct ReadTool<const LINE_NUMBERS: bool = true>;
39
40impl<const LINE_NUMBERS: bool> ReadTool<LINE_NUMBERS> {
41    /// Creates a new read tool instance.
42    #[inline]
43    pub fn new() -> Self {
44        Self
45    }
46}
47
48impl<const LINE_NUMBERS: bool> Tool for ReadTool<LINE_NUMBERS> {
49    const NAME: &'static str = tool_names::READ;
50
51    type Error = ToolError;
52    type Args = ReadArgs;
53    type Output = ToolOutput;
54
55    async fn definition(&self, _prompt: String) -> ToolDefinition {
56        let description = if LINE_NUMBERS {
57            "Read file contents with line numbers. Returns lines prefixed with L{number}: format."
58        } else {
59            "Read file contents. Returns raw file content without line number prefixes."
60        };
61        ToolDefinition {
62            name: <Self as Tool>::NAME.to_string(),
63            description: description.to_string(),
64            parameters: serde_json::to_value(schema_for!(ReadArgs))
65                .expect("schema serialization should never fail"),
66        }
67    }
68
69    async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
70        let resolver = AbsolutePathResolver;
71        read_file::<_, LINE_NUMBERS>(&resolver, &args.file_path, args.offset, args.limit).await
72    }
73}
74
75impl<const LINE_NUMBERS: bool> ToolContext for ReadTool<LINE_NUMBERS> {
76    const NAME: &'static str = tool_names::READ;
77
78    fn context(&self) -> &'static str {
79        llm_coding_tools_core::context::READ_ABSOLUTE
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86    use std::io::Write as _;
87    use tempfile::NamedTempFile;
88
89    #[tokio::test]
90    async fn reads_file_with_line_numbers() {
91        let mut temp = NamedTempFile::new().unwrap();
92        temp.write_all(b"hello\nworld\n").unwrap();
93        let tool: ReadTool<true> = ReadTool::new();
94        let args = ReadArgs {
95            file_path: temp.path().to_string_lossy().to_string(),
96            offset: 1,
97            limit: 2000,
98        };
99        let result = tool.call(args).await.unwrap();
100        assert_eq!(result.content, "L1: hello\nL2: world");
101    }
102
103    #[tokio::test]
104    async fn rejects_relative_path() {
105        let tool: ReadTool = ReadTool::new();
106        let args = ReadArgs {
107            file_path: "relative/path.txt".to_string(),
108            offset: 1,
109            limit: 100,
110        };
111        let result = tool.call(args).await;
112        assert!(matches!(result, Err(ToolError::InvalidPath(_))));
113    }
114}