llm_coding_tools_rig/absolute/
read.rs1use 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#[derive(Debug, Clone, Deserialize, JsonSchema)]
25pub struct ReadArgs {
26 pub file_path: String,
28 #[serde(default = "default_offset")]
30 pub offset: usize,
31 #[serde(default = "default_limit")]
33 pub limit: usize,
34}
35
36#[derive(Debug, Clone, Default)]
38pub struct ReadTool<const LINE_NUMBERS: bool = true>;
39
40impl<const LINE_NUMBERS: bool> ReadTool<LINE_NUMBERS> {
41 #[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}