llm_coding_tools_serdesai/absolute/
read.rs1use async_trait::async_trait;
4use llm_coding_tools_core::ToolContext;
5use llm_coding_tools_core::operations::read_file;
6use llm_coding_tools_core::path::AbsolutePathResolver;
7use llm_coding_tools_core::tool_names;
8use serde::Deserialize;
9use serdes_ai::tools::{RunContext, SchemaBuilder, Tool, ToolDefinition, ToolError, ToolResult};
10
11use crate::convert::to_serdes_result;
12
13const DEFAULT_OFFSET: usize = 1;
14const DEFAULT_LIMIT: usize = 2000;
15
16fn default_offset() -> usize {
17 DEFAULT_OFFSET
18}
19
20fn default_limit() -> usize {
21 DEFAULT_LIMIT
22}
23
24#[derive(Debug, Deserialize)]
26struct ReadArgs {
27 file_path: String,
29 #[serde(default = "default_offset")]
31 offset: usize,
32 #[serde(default = "default_limit")]
34 limit: usize,
35}
36
37#[derive(Debug, Clone, Default)]
43pub struct ReadTool<const LINE_NUMBERS: bool = true>;
44
45impl<const LINE_NUMBERS: bool> ReadTool<LINE_NUMBERS> {
46 #[inline]
48 pub fn new() -> Self {
49 Self
50 }
51}
52
53#[async_trait]
54impl<Deps: Send + Sync, const LINE_NUMBERS: bool> Tool<Deps> for ReadTool<LINE_NUMBERS> {
55 fn definition(&self) -> ToolDefinition {
56 let description = if LINE_NUMBERS {
58 "Read file contents with line numbers. Returns lines prefixed with L{number}: format."
59 } else {
60 "Read file contents. Returns raw file content without line number prefixes."
61 };
62 let schema = SchemaBuilder::new()
63 .string("file_path", "Absolute path to the file", true)
64 .integer_constrained(
65 "offset",
66 "Line offset to start reading from (1-based). Defaults to 1.",
67 false,
68 Some(1),
69 None,
70 )
71 .integer_constrained(
72 "limit",
73 "Maximum number of lines to return. Defaults to 2000.",
74 false,
75 Some(1),
76 None,
77 )
78 .build()
79 .expect("schema build should not fail");
80
81 ToolDefinition::new(tool_names::READ, description).with_parameters(schema)
82 }
83
84 async fn call(&self, _ctx: &RunContext<Deps>, args: serde_json::Value) -> ToolResult {
85 let args: ReadArgs = serde_json::from_value(args)
86 .map_err(|e| ToolError::validation_error(tool_names::READ, None, e.to_string()))?;
87
88 let resolver = AbsolutePathResolver;
89 let result =
91 read_file::<_, LINE_NUMBERS>(&resolver, &args.file_path, args.offset, args.limit).await;
92 to_serdes_result(tool_names::READ, result)
93 }
94}
95
96impl<const LINE_NUMBERS: bool> ToolContext for ReadTool<LINE_NUMBERS> {
97 const NAME: &'static str = tool_names::READ;
98
99 fn context(&self) -> &'static str {
100 llm_coding_tools_core::context::READ_ABSOLUTE
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107 use serde_json::json;
108 use serdes_ai::tools::RunContext;
109 use std::io::Write as _;
110 use tempfile::NamedTempFile;
111
112 fn mock_ctx() -> RunContext<()> {
113 RunContext::new((), "test-model")
114 }
115
116 #[tokio::test]
117 async fn reads_file_with_offset_and_limit() {
118 let mut temp = NamedTempFile::new().unwrap();
119 temp.write_all(b"line1\nline2\nline3\nline4\n").unwrap();
120 let tool: ReadTool<true> = ReadTool::new();
121
122 let result = tool
123 .call(
124 &mock_ctx(),
125 json!({
126 "file_path": temp.path().to_string_lossy(),
127 "offset": 2,
128 "limit": 2
129 }),
130 )
131 .await
132 .unwrap();
133
134 let text = result.as_text().unwrap();
135 assert!(text.contains("L2: line2"));
136 assert!(text.contains("L3: line3"));
137 assert!(!text.contains("L1:"));
138 assert!(!text.contains("L4:"));
139 }
140}