From fe58f3eeca9a88ba0d85f3c1aa87179bdc44a9fe Mon Sep 17 00:00:00 2001 From: Sewer56 Date: Sat, 4 Apr 2026 02:16:45 +0100 Subject: [PATCH] Changed: Remove const generics from ReadTool and GrepTool Replaced `const LINE_NUMBERS: bool` const generics with runtime `line_numbers: bool` fields in ReadTool and GrepTool structs. For most use cases (i.e., with agents), having generics would mean 2 code paths, and the binary size increase (~35% larger for the hot functions) wasn't worth it in practice. Runtime bool is actually faster due to better instruction cache utilization from eliminating monomorphization bloat. - GrepOutput::format() now takes `line_numbers: bool` parameter - read_file() and process_line() now use runtime bool - Both absolute and allowed variants updated - Simplified agent_runtime/build.rs tool construction - Updated all tests and examples --- src/llm-coding-tools-core/src/tools/grep.rs | 17 +++---- src/llm-coding-tools-core/src/tools/read.rs | 37 +++++++++------ src/llm-coding-tools-serdesai/README.md | 8 ++-- .../examples/serdesai-basic.rs | 4 +- .../examples/serdesai-sandboxed.rs | 4 +- .../src/absolute/grep.rs | 45 ++++++++++--------- .../src/absolute/read.rs | 35 ++++++++------- .../src/agent_ext.rs | 4 +- .../src/agent_runtime/build.rs | 32 +++++-------- .../src/allowed/grep.rs | 41 ++++++++++------- .../src/allowed/read.rs | 31 ++++++++----- .../src/common/grep.rs | 7 +-- 12 files changed, 142 insertions(+), 123 deletions(-) diff --git a/src/llm-coding-tools-core/src/tools/grep.rs b/src/llm-coding-tools-core/src/tools/grep.rs index a080e22b..321a2727 100644 --- a/src/llm-coding-tools-core/src/tools/grep.rs +++ b/src/llm-coding-tools-core/src/tools/grep.rs @@ -59,15 +59,12 @@ pub struct GrepOutput { impl GrepOutput { /// Formats grep results as human-readable text. /// - /// # Type Parameters - /// - /// * `LINE_NUMBERS` - When `true`, prefixes each match with `L{num}: ` - /// /// # Arguments /// + /// * `line_numbers` - When `true`, prefixes each match with `L{num}: ` /// * `limit` - The original match limit (used in truncation message) /// * `max_line_len` - Truncate lines exceeding this character length and append `...` - pub fn format(&self, limit: usize, max_line_len: usize) -> String { + pub fn format(&self, line_numbers: bool, limit: usize, max_line_len: usize) -> String { let estimated_capacity = self.match_count * ESTIMATED_CHARS_PER_MATCH; let mut output = String::with_capacity(estimated_capacity); @@ -79,7 +76,7 @@ impl GrepOutput { let (display_text, was_truncated) = truncate_line_with_ellipsis(&m.line_text, max_line_len); - if LINE_NUMBERS { + if line_numbers { let _ = write!(&mut output, " L{}: {}", m.line_num, display_text); } else { let _ = write!(&mut output, " {}", display_text); @@ -364,7 +361,7 @@ mod tests { errors, }; - let formatted = output.format::(10, DEFAULT_MAX_LINE_LENGTH); + let formatted = output.format(true, 10, DEFAULT_MAX_LINE_LENGTH); assert_eq!(formatted.contains("Partial results"), expect_partial_msg); assert_eq!( @@ -453,11 +450,7 @@ mod tests { errors: Vec::new(), }; - let formatted = if with_line_numbers { - output.format::(10, max_len) - } else { - output.format::(10, max_len) - }; + let formatted = output.format(with_line_numbers, 10, max_len); assert!( formatted.contains(expected), diff --git a/src/llm-coding-tools-core/src/tools/read.rs b/src/llm-coding-tools-core/src/tools/read.rs index 0640b56f..36feeca2 100644 --- a/src/llm-coding-tools-core/src/tools/read.rs +++ b/src/llm-coding-tools-core/src/tools/read.rs @@ -19,12 +19,13 @@ fn strip_cr(line: &[u8]) -> &[u8] { /// Processes a single line, appending it to output with optional line numbers. #[inline] -fn process_line( +fn process_line( line_bytes: &[u8], line_number: usize, output: &mut String, lines_output: &mut usize, max_line_length: usize, + line_numbers: bool, ) { let line_bytes = strip_cr(line_bytes); let content: Cow<'_, str> = String::from_utf8_lossy(line_bytes); @@ -34,7 +35,7 @@ fn process_line( output.push('\n'); } - if LINE_NUMBERS { + if line_numbers { let _ = write!(output, "L{}: {}", line_number, display_content); } else { output.push_str(display_content); @@ -49,15 +50,16 @@ fn process_line( /// Reads a file and returns formatted content, optionally with line numbers. /// -/// When `LINE_NUMBERS` is `true`, each line is prefixed with `L{number}: `. +/// When `line_numbers` is `true`, each line is prefixed with `L{number}: `. /// When `false`, raw content is returned without prefixes. #[maybe_async::maybe_async] -pub async fn read_file( +pub async fn read_file( resolver: &R, file_path: &str, offset: usize, limit: usize, max_line_length: usize, + line_numbers: bool, ) -> ToolResult { // Conditional trait import for consume() method #[cfg(feature = "blocking")] @@ -99,12 +101,13 @@ pub async fn read_file( if !overflow.is_empty() { line_number += 1; if line_number >= offset && lines_output < limit { - process_line::( + process_line( &overflow, line_number, &mut output, &mut lines_output, max_line_length, + line_numbers, ); } } @@ -122,22 +125,24 @@ pub async fn read_file( if line_number >= offset && lines_output < limit { if overflow.is_empty() { // Fast path: line is fully in this buffer. - process_line::( + process_line( &buf[pos..newline_pos], line_number, &mut output, &mut lines_output, max_line_length, + line_numbers, ); } else { // Slow path: prepend buffered fragment. overflow.extend_from_slice(&buf[pos..newline_pos]); - process_line::( + process_line( &overflow, line_number, &mut output, &mut lines_output, max_line_length, + line_numbers, ); overflow.clear(); } @@ -181,27 +186,29 @@ mod tests { use tempfile::NamedTempFile; #[maybe_async::maybe_async] - async fn read_temp_file( + async fn read_temp_file( content: &[u8], offset: usize, limit: usize, + line_numbers: bool, ) -> ToolResult { let mut temp = NamedTempFile::new().unwrap(); temp.write_all(content).unwrap(); let resolver = AbsolutePathResolver; - read_file::<_, LINE_NUMBERS>( + read_file::<_>( &resolver, temp.path().to_str().unwrap(), offset, limit, 2000, // max_line_length + line_numbers, ) .await } #[maybe_async::test(feature = "blocking", async(feature = "tokio", tokio::test))] async fn reads_basic_file_with_line_numbers() { - let result = read_temp_file::(b"hello\nworld\n", 1, 2000) + let result = read_temp_file(b"hello\nworld\n", 1, 2000, true) .await .unwrap(); assert_eq!(result.content, "L1: hello\nL2: world"); @@ -209,7 +216,7 @@ mod tests { #[maybe_async::test(feature = "blocking", async(feature = "tokio", tokio::test))] async fn reads_basic_file_without_line_numbers() { - let result = read_temp_file::(b"hello\nworld\n", 1, 2000) + let result = read_temp_file(b"hello\nworld\n", 1, 2000, false) .await .unwrap(); assert_eq!(result.content, "hello\nworld"); @@ -217,7 +224,7 @@ mod tests { #[maybe_async::test(feature = "blocking", async(feature = "tokio", tokio::test))] async fn errors_on_offset_zero() { - let err = read_temp_file::(b"test\n", 0, 10).await.unwrap_err(); + let err = read_temp_file(b"test\n", 0, 10, true).await.unwrap_err(); assert!(matches!(err, ToolError::OutOfBounds(_))); } @@ -227,12 +234,13 @@ mod tests { temp.write_all(b"test\n").unwrap(); let resolver = AbsolutePathResolver; - let err = read_file::<_, true>( + let err = read_file::<_>( &resolver, temp.path().to_str().unwrap(), 1, 10, 3, // below MIN_LINE_LENGTH of 4 + true, ) .await .unwrap_err(); @@ -247,12 +255,13 @@ mod tests { temp.write_all(b"abcdefghij\n").unwrap(); let resolver = AbsolutePathResolver; - let result = read_file::<_, false>( + let result = read_file::<_>( &resolver, temp.path().to_str().unwrap(), 1, 10, 6, // keep 3 chars + "..." + false, ) .await .unwrap(); diff --git a/src/llm-coding-tools-serdesai/README.md b/src/llm-coding-tools-serdesai/README.md index 065af375..5838eb39 100644 --- a/src/llm-coding-tools-serdesai/README.md +++ b/src/llm-coding-tools-serdesai/README.md @@ -30,9 +30,9 @@ let mut pb = SystemPromptBuilder::new(); // Build agent with tools - call .system_prompt() last let agent = AgentBuilder::<(), String>::from_model("openai:gpt-4o")? - .tool(pb.track(ReadTool::::new())) + .tool(pb.track(ReadTool::new())) .tool(pb.track(GlobTool::new())) - .tool(pb.track(GrepTool::::new())) + .tool(pb.track(GrepTool::new())) .tool(pb.track(EditTool::new())) .tool(pb.track(BashTool::host())) .tool(pb.track(WebFetchTool::new())) @@ -69,12 +69,12 @@ use llm_coding_tools_serdesai::AllowedPathResolver; use std::path::PathBuf; // Unrestricted access -let read = ReadTool::::new(); +let read = ReadTool::new(); // Sandboxed access let allowed_paths = vec![PathBuf::from("/home/user/project"), PathBuf::from("/tmp")]; let resolver = AllowedPathResolver::new(allowed_paths).unwrap(); -let sandboxed_read: AllowedReadTool = AllowedReadTool::new(resolver.clone()); +let sandboxed_read: AllowedReadTool = AllowedReadTool::new(resolver.clone()); let sandboxed_edit = AllowedEditTool::new(resolver.clone()); let sandboxed_write = AllowedWriteTool::new(resolver); ``` diff --git a/src/llm-coding-tools-serdesai/examples/serdesai-basic.rs b/src/llm-coding-tools-serdesai/examples/serdesai-basic.rs index 972299b6..cee49c4f 100644 --- a/src/llm-coding-tools-serdesai/examples/serdesai-basic.rs +++ b/src/llm-coding-tools-serdesai/examples/serdesai-basic.rs @@ -44,9 +44,9 @@ async fn main() -> std::result::Result<(), Box> { let agent = AgentBuilder::<(), String>::new(model) .instructions("Use tools to answer; call at least one tool before responding.") // File operations - .tool(pb.track(ReadTool::::new())) + .tool(pb.track(ReadTool::new())) .tool(pb.track(GlobTool::new())) - .tool(pb.track(GrepTool::::new())) + .tool(pb.track(GrepTool::new())) // Shell execution .tool(pb.track(BashTool::host())) // Web content fetching diff --git a/src/llm-coding-tools-serdesai/examples/serdesai-sandboxed.rs b/src/llm-coding-tools-serdesai/examples/serdesai-sandboxed.rs index e62916a7..18985e35 100644 --- a/src/llm-coding-tools-serdesai/examples/serdesai-sandboxed.rs +++ b/src/llm-coding-tools-serdesai/examples/serdesai-sandboxed.rs @@ -45,11 +45,11 @@ async fn main() -> std::result::Result<(), Box> { // More efficient and ensures consistency. let resolver = AllowedPathResolver::new(allowed_paths)?; - let read: ReadTool = ReadTool::new(resolver.clone()); + let read: ReadTool = ReadTool::new(resolver.clone()); let write = WriteTool::new(resolver.clone()); let edit = EditTool::new(resolver.clone()); let glob = GlobTool::new(resolver.clone()); - let grep: GrepTool = GrepTool::new(resolver.clone()); + let grep = GrepTool::new(resolver.clone()); // === Build agent with sandboxed tools === // diff --git a/src/llm-coding-tools-serdesai/src/absolute/grep.rs b/src/llm-coding-tools-serdesai/src/absolute/grep.rs index a94d8d30..53af8fc4 100644 --- a/src/llm-coding-tools-serdesai/src/absolute/grep.rs +++ b/src/llm-coding-tools-serdesai/src/absolute/grep.rs @@ -29,29 +29,31 @@ struct GrepArgs { /// Tool for searching file contents using regex patterns. /// -/// The `LINE_NUMBERS` const generic controls output format: +/// The `line_numbers` field controls output format: /// - `true` (default): Lines prefixed with `L{number}: ` /// - `false`: Raw matching lines #[derive(Debug, Clone)] -pub struct GrepTool { +pub struct GrepTool { definition: ToolDefinition, max_line_length: usize, limit: usize, + line_numbers: bool, } -impl Default for GrepTool { +impl Default for GrepTool { fn default() -> Self { Self::new() } } -impl GrepTool { +impl GrepTool { /// Creates a new grep tool instance with default settings. /// - /// Uses `max_line_length` of 2000 characters and `limit` of 100 matches. + /// Uses `max_line_length` of 2000 characters, `limit` of 100 matches, + /// and enables line numbers. #[inline] pub fn new() -> Self { - Self::with_settings(DEFAULT_MAX_LINE_LENGTH, grep_meta::DEFAULT_LIMIT) + Self::with_settings(DEFAULT_MAX_LINE_LENGTH, grep_meta::DEFAULT_LIMIT, true) } /// Creates a new grep tool instance with custom settings. @@ -61,17 +63,19 @@ impl GrepTool { /// * `max_line_length` - Maximum characters per matching line before truncation. /// Longer lines will be truncated with "..." appended. /// * `limit` - Maximum number of matches to return when not specified in args. - pub fn with_settings(max_line_length: usize, limit: usize) -> Self { + /// * `line_numbers` - Whether to prefix lines with line numbers. + pub fn with_settings(max_line_length: usize, limit: usize, line_numbers: bool) -> Self { Self { - definition: build_definition::(), + definition: build_definition(line_numbers), max_line_length, limit, + line_numbers, } } } #[async_trait] -impl Tool for GrepTool { +impl Tool for GrepTool { fn definition(&self) -> ToolDefinition { self.definition.clone() } @@ -112,8 +116,9 @@ impl Tool for GrepTool to_serdes_result(grep_meta::NAME, Err(e)), - Ok(grep_output) => Ok(grep_output_to_return::( + Ok(grep_output) => Ok(grep_output_to_return( grep_output, + self.line_numbers, limit, self.max_line_length, )), @@ -121,18 +126,18 @@ impl Tool for GrepTool ToolContext for GrepTool { +impl ToolContext for GrepTool { const NAME: &'static str = grep_meta::NAME; fn context(&self) -> ToolPrompt { ToolPrompt::Grep { path_mode: PathMode::Absolute, - line_numbers: LINE_NUMBERS, + line_numbers: self.line_numbers, } } } -fn build_definition() -> ToolDefinition { +fn build_definition(line_numbers: bool) -> ToolDefinition { let schema = SchemaBuilder::new() .string( grep_meta::param::PATTERN.name, @@ -161,7 +166,7 @@ fn build_definition() -> ToolDefinition { ToolDefinition { name: grep_meta::NAME.to_owned(), - description: grep_meta::description::absolute(LINE_NUMBERS).to_owned(), + description: grep_meta::description::absolute(line_numbers).to_owned(), parameters_json_schema: schema, strict: None, outer_typed_dict_key: None, @@ -184,7 +189,7 @@ mod tests { let dir = TempDir::new().unwrap(); std::fs::write(dir.path().join("test.txt"), "hello world\nfoo bar").unwrap(); - let tool: GrepTool = GrepTool::new(); + let tool = GrepTool::new(); let result = tool .call( &mock_ctx(), @@ -206,7 +211,7 @@ mod tests { let dir = TempDir::new().unwrap(); std::fs::write(dir.path().join("test.txt"), "hello").unwrap(); - let tool: GrepTool = GrepTool::new(); + let tool = GrepTool::new(); let result = tool .call( &mock_ctx(), @@ -235,7 +240,7 @@ mod tests { let dir = TempDir::new().unwrap(); std::fs::write(dir.path().join("test.txt"), "hello world").unwrap(); - let tool: GrepTool = GrepTool::new(); + let tool = GrepTool::new(); let result = tool .call( &mock_ctx(), @@ -258,7 +263,7 @@ mod tests { std::fs::write(dir.path().join("code.py"), "def hello(): pass").unwrap(); std::fs::write(dir.path().join("readme.txt"), "hello world").unwrap(); - let tool: GrepTool = GrepTool::new(); + let tool = GrepTool::new(); // Search only .rs files let result = tool @@ -284,7 +289,7 @@ mod tests { async fn returns_partial_json_when_search_has_errors() { let dir = TempDir::new().unwrap(); let missing_path = dir.path().join("missing-root"); - let tool: GrepTool = GrepTool::new(); + let tool = GrepTool::new(); let result = tool .call( @@ -315,7 +320,7 @@ mod tests { let long_line = format!("prefix_{}_suffix", "x".repeat(2500)); std::fs::write(dir.path().join("long.txt"), &long_line).unwrap(); - let tool: GrepTool = GrepTool::new(); + let tool = GrepTool::new(); let result = tool .call( &mock_ctx(), diff --git a/src/llm-coding-tools-serdesai/src/absolute/read.rs b/src/llm-coding-tools-serdesai/src/absolute/read.rs index a603292a..38e9072c 100644 --- a/src/llm-coding-tools-serdesai/src/absolute/read.rs +++ b/src/llm-coding-tools-serdesai/src/absolute/read.rs @@ -26,29 +26,31 @@ struct ReadArgs { /// Tool for reading file contents with optional line numbers. /// -/// The `LINE_NUMBERS` const generic controls output format: +/// The `line_numbers` field controls output format: /// - `true` (default): Lines prefixed with `L{number}: ` /// - `false`: Raw file content #[derive(Debug, Clone)] -pub struct ReadTool { +pub struct ReadTool { definition: ToolDefinition, limit: usize, max_line_length: usize, + line_numbers: bool, } -impl Default for ReadTool { +impl Default for ReadTool { fn default() -> Self { Self::new() } } -impl ReadTool { +impl ReadTool { /// Creates a new read tool instance with default settings. /// - /// Uses `limit` of 2000 lines and `max_line_length` of 2000 characters. + /// Uses `limit` of 2000 lines, `max_line_length` of 2000 characters, + /// and enables line numbers. #[inline] pub fn new() -> Self { - Self::with_settings(read_meta::DEFAULT_LIMIT, read_meta::MAX_LINE_LENGTH) + Self::with_settings(read_meta::DEFAULT_LIMIT, read_meta::MAX_LINE_LENGTH, true) } /// Creates a new read tool instance with custom settings. @@ -59,17 +61,19 @@ impl ReadTool { /// This is the default used when the LLM doesn't specify a limit. /// * `max_line_length` - Maximum characters per line before truncation. /// Longer lines will be truncated with "..." appended. - pub fn with_settings(limit: usize, max_line_length: usize) -> Self { + /// * `line_numbers` - Whether to prefix lines with line numbers. + pub fn with_settings(limit: usize, max_line_length: usize, line_numbers: bool) -> Self { Self { - definition: build_definition::(), + definition: build_definition(line_numbers), limit, max_line_length, + line_numbers, } } } #[async_trait] -impl Tool for ReadTool { +impl Tool for ReadTool { fn definition(&self) -> ToolDefinition { self.definition.clone() } @@ -82,30 +86,31 @@ impl Tool for ReadTool( + let result = read_file::<_>( &resolver, &args.file_path, args.offset, effective_limit, self.max_line_length, + self.line_numbers, ) .await; to_serdes_result(read_meta::NAME, result) } } -impl ToolContext for ReadTool { +impl ToolContext for ReadTool { const NAME: &'static str = read_meta::NAME; fn context(&self) -> ToolPrompt { ToolPrompt::Read { path_mode: PathMode::Absolute, - line_numbers: LINE_NUMBERS, + line_numbers: self.line_numbers, } } } -fn build_definition() -> ToolDefinition { +fn build_definition(line_numbers: bool) -> ToolDefinition { let schema = SchemaBuilder::new() .string( read_meta::param::FILE_PATH_ABSOLUTE.name, @@ -131,7 +136,7 @@ fn build_definition() -> ToolDefinition { ToolDefinition { name: read_meta::NAME.to_owned(), - description: read_meta::description::absolute(LINE_NUMBERS).to_owned(), + description: read_meta::description::absolute(line_numbers).to_owned(), parameters_json_schema: schema, strict: None, outer_typed_dict_key: None, @@ -154,7 +159,7 @@ mod tests { async fn reads_file_with_offset_and_limit() { let mut temp = NamedTempFile::new().unwrap(); temp.write_all(b"line1\nline2\nline3\nline4\n").unwrap(); - let tool: ReadTool = ReadTool::new(); + let tool: ReadTool = ReadTool::new(); let result = tool .call( diff --git a/src/llm-coding-tools-serdesai/src/agent_ext.rs b/src/llm-coding-tools-serdesai/src/agent_ext.rs index f270ceea..d2d3e81d 100644 --- a/src/llm-coding-tools-serdesai/src/agent_ext.rs +++ b/src/llm-coding-tools-serdesai/src/agent_ext.rs @@ -12,7 +12,7 @@ //! //! # fn main() -> std::result::Result<(), Box> { //! let agent = AgentBuilder::<(), String>::from_model("openai:gpt-4o")? -//! .tool(ReadTool::::new()) +//! .tool(ReadTool::new()) //! .tool(GlobTool::new()) //! .system_prompt("You are helpful.") //! .build(); @@ -65,7 +65,7 @@ pub trait AgentBuilderExt { /// /// # fn main() -> std::result::Result<(), Box> { /// let agent = AgentBuilder::<(), String>::from_model("openai:gpt-4o")? - /// .tool(ReadTool::::new()) + /// .tool(ReadTool::new()) /// .tool(GlobTool::new()) /// .build(); /// # Ok(()) diff --git a/src/llm-coding-tools-serdesai/src/agent_runtime/build.rs b/src/llm-coding-tools-serdesai/src/agent_runtime/build.rs index 7b4c6826..e7a23dcc 100644 --- a/src/llm-coding-tools-serdesai/src/agent_runtime/build.rs +++ b/src/llm-coding-tools-serdesai/src/agent_runtime/build.rs @@ -122,17 +122,11 @@ where match entry.kind { ToolCatalogKind::Read => { let settings = &prepared.tool_settings.read; - if settings.line_numbers { - builder = builder.tool(prompt_builder.track(ReadTool::::with_settings( - settings.limit, - settings.max_line_length, - ))); - } else { - builder = builder.tool(prompt_builder.track(ReadTool::::with_settings( - settings.limit, - settings.max_line_length, - ))); - } + builder = builder.tool(prompt_builder.track(ReadTool::with_settings( + settings.limit, + settings.max_line_length, + settings.line_numbers, + ))); } ToolCatalogKind::Write => { builder = builder.tool(prompt_builder.track(WriteTool::new())) @@ -145,17 +139,11 @@ where } ToolCatalogKind::Grep => { let settings = &prepared.tool_settings.grep; - if settings.line_numbers { - builder = builder.tool(prompt_builder.track(GrepTool::::with_settings( - settings.max_line_length, - settings.limit, - ))); - } else { - builder = builder.tool(prompt_builder.track(GrepTool::::with_settings( - settings.max_line_length, - settings.limit, - ))); - } + builder = builder.tool(prompt_builder.track(GrepTool::with_settings( + settings.max_line_length, + settings.limit, + settings.line_numbers, + ))); } ToolCatalogKind::Bash => { let settings = &prepared.tool_settings.bash; diff --git a/src/llm-coding-tools-serdesai/src/allowed/grep.rs b/src/llm-coding-tools-serdesai/src/allowed/grep.rs index 7b36e975..b556c94b 100644 --- a/src/llm-coding-tools-serdesai/src/allowed/grep.rs +++ b/src/llm-coding-tools-serdesai/src/allowed/grep.rs @@ -29,23 +29,30 @@ struct GrepArgs { /// Tool for searching file contents within allowed directories. #[derive(Debug, Clone)] -pub struct GrepTool { +pub struct GrepTool { definition: ToolDefinition, resolver: AllowedPathResolver, max_line_length: usize, limit: usize, + line_numbers: bool, } -impl GrepTool { +impl GrepTool { /// Creates a new grep tool with a shared resolver and default settings. /// - /// Uses `max_line_length` of 2000 characters and `limit` of 100 matches. + /// Uses `max_line_length` of 2000 characters, `limit` of 100 matches, + /// and enables line numbers. /// /// See [`ReadTool::new`] for usage example. /// /// [`ReadTool::new`]: super::ReadTool::new pub fn new(resolver: AllowedPathResolver) -> Self { - Self::with_settings(resolver, DEFAULT_MAX_LINE_LENGTH, grep_meta::DEFAULT_LIMIT) + Self::with_settings( + resolver, + DEFAULT_MAX_LINE_LENGTH, + grep_meta::DEFAULT_LIMIT, + true, + ) } /// Creates a new grep tool with custom settings. @@ -56,22 +63,25 @@ impl GrepTool { /// * `max_line_length` - Maximum characters per matching line before truncation. /// Longer lines will be truncated with "..." appended. /// * `limit` - Maximum number of matches to return when not specified in args. + /// * `line_numbers` - Whether to prefix lines with line numbers. pub fn with_settings( resolver: AllowedPathResolver, max_line_length: usize, limit: usize, + line_numbers: bool, ) -> Self { Self { - definition: build_definition::(), + definition: build_definition(line_numbers), resolver, max_line_length, limit, + line_numbers, } } } #[async_trait] -impl Tool for GrepTool { +impl Tool for GrepTool { fn definition(&self) -> ToolDefinition { self.definition.clone() } @@ -111,8 +121,9 @@ impl Tool for GrepTool to_serdes_result(grep_meta::NAME, Err(e)), - Ok(grep_output) => Ok(grep_output_to_return::( + Ok(grep_output) => Ok(grep_output_to_return( grep_output, + self.line_numbers, limit, self.max_line_length, )), @@ -120,18 +131,18 @@ impl Tool for GrepTool ToolContext for GrepTool { +impl ToolContext for GrepTool { const NAME: &'static str = grep_meta::NAME; fn context(&self) -> ToolPrompt { ToolPrompt::Grep { path_mode: PathMode::Allowed, - line_numbers: LINE_NUMBERS, + line_numbers: self.line_numbers, } } } -fn build_definition() -> ToolDefinition { +fn build_definition(line_numbers: bool) -> ToolDefinition { let schema = SchemaBuilder::new() .string( grep_meta::param::PATTERN.name, @@ -160,7 +171,7 @@ fn build_definition() -> ToolDefinition { ToolDefinition { name: grep_meta::NAME.to_owned(), - description: grep_meta::description::allowed(LINE_NUMBERS).to_owned(), + description: grep_meta::description::allowed(line_numbers).to_owned(), parameters_json_schema: schema, strict: None, outer_typed_dict_key: None, @@ -184,7 +195,7 @@ mod tests { std::fs::write(dir.path().join("test.txt"), "hello world").unwrap(); let resolver = AllowedPathResolver::new([dir.path()]).unwrap(); - let tool: GrepTool = GrepTool::new(resolver); + let tool = GrepTool::new(resolver); let result = tool .call( &mock_ctx(), @@ -205,7 +216,7 @@ mod tests { async fn rejects_path_traversal() { let dir = TempDir::new().unwrap(); let resolver = AllowedPathResolver::new([dir.path()]).unwrap(); - let tool: GrepTool = GrepTool::new(resolver); + let tool = GrepTool::new(resolver); let result = tool .call( &mock_ctx(), @@ -223,7 +234,7 @@ mod tests { async fn rejects_empty_pattern() { let dir = TempDir::new().unwrap(); let resolver = AllowedPathResolver::new([dir.path()]).unwrap(); - let tool: GrepTool = GrepTool::new(resolver); + let tool = GrepTool::new(resolver); let result = tool .call( &mock_ctx(), @@ -241,7 +252,7 @@ mod tests { async fn returns_partial_json_when_search_has_errors() { let dir = TempDir::new().unwrap(); let resolver = AllowedPathResolver::new([dir.path()]).unwrap(); - let tool: GrepTool = GrepTool::new(resolver); + let tool = GrepTool::new(resolver); let result = tool .call( &mock_ctx(), diff --git a/src/llm-coding-tools-serdesai/src/allowed/read.rs b/src/llm-coding-tools-serdesai/src/allowed/read.rs index 3e474128..ece77fbf 100644 --- a/src/llm-coding-tools-serdesai/src/allowed/read.rs +++ b/src/llm-coding-tools-serdesai/src/allowed/read.rs @@ -28,17 +28,19 @@ struct ReadArgs { /// /// Restricts access to configured allowed directories. #[derive(Debug, Clone)] -pub struct ReadTool { +pub struct ReadTool { definition: ToolDefinition, resolver: AllowedPathResolver, limit: usize, max_line_length: usize, + line_numbers: bool, } -impl ReadTool { +impl ReadTool { /// Creates a new read tool with a shared resolver and default settings. /// - /// Uses `limit` of 2000 lines and `max_line_length` of 2000 characters. + /// Uses `limit` of 2000 lines, `max_line_length` of 2000 characters, + /// and enables line numbers. /// /// See [`ReadTool::new`] for usage example. /// @@ -48,6 +50,7 @@ impl ReadTool { resolver, read_meta::DEFAULT_LIMIT, read_meta::MAX_LINE_LENGTH, + true, ) } @@ -60,22 +63,25 @@ impl ReadTool { /// This is the default used when the LLM doesn't specify a limit. /// * `max_line_length` - Maximum characters per line before truncation. /// Longer lines will be truncated with "..." appended. + /// * `line_numbers` - Whether to prefix lines with line numbers. pub fn with_settings( resolver: AllowedPathResolver, limit: usize, max_line_length: usize, + line_numbers: bool, ) -> Self { Self { - definition: build_definition::(), + definition: build_definition(line_numbers), resolver, limit, max_line_length, + line_numbers, } } } #[async_trait] -impl Tool for ReadTool { +impl Tool for ReadTool { fn definition(&self) -> ToolDefinition { self.definition.clone() } @@ -86,30 +92,31 @@ impl Tool for ReadTool( + let result = read_file::<_>( &self.resolver, &args.file_path, args.offset, effective_limit, self.max_line_length, + self.line_numbers, ) .await; to_serdes_result(read_meta::NAME, result) } } -impl ToolContext for ReadTool { +impl ToolContext for ReadTool { const NAME: &'static str = read_meta::NAME; fn context(&self) -> ToolPrompt { ToolPrompt::Read { path_mode: PathMode::Allowed, - line_numbers: LINE_NUMBERS, + line_numbers: self.line_numbers, } } } -fn build_definition() -> ToolDefinition { +fn build_definition(line_numbers: bool) -> ToolDefinition { let schema = SchemaBuilder::new() .string( read_meta::param::FILE_PATH_ALLOWED.name, @@ -135,7 +142,7 @@ fn build_definition() -> ToolDefinition { ToolDefinition { name: read_meta::NAME.to_owned(), - description: read_meta::description::allowed(LINE_NUMBERS).to_owned(), + description: read_meta::description::allowed(line_numbers).to_owned(), parameters_json_schema: schema, strict: None, outer_typed_dict_key: None, @@ -159,7 +166,7 @@ mod tests { std::fs::write(dir.path().join("test.txt"), "hello\nworld\n").unwrap(); let resolver = AllowedPathResolver::new([dir.path()]).unwrap(); - let tool: ReadTool = ReadTool::new(resolver); + let tool: ReadTool = ReadTool::new(resolver); let result = tool .call( &mock_ctx(), @@ -181,7 +188,7 @@ mod tests { async fn rejects_path_traversal() { let dir = TempDir::new().unwrap(); let resolver = AllowedPathResolver::new([dir.path()]).unwrap(); - let tool: ReadTool = ReadTool::new(resolver); + let tool: ReadTool = ReadTool::new(resolver); let result = tool .call( &mock_ctx(), diff --git a/src/llm-coding-tools-serdesai/src/common/grep.rs b/src/llm-coding-tools-serdesai/src/common/grep.rs index 224887e7..949ffa33 100644 --- a/src/llm-coding-tools-serdesai/src/common/grep.rs +++ b/src/llm-coding-tools-serdesai/src/common/grep.rs @@ -7,13 +7,14 @@ use serdes_ai::tools::ToolReturn; const NO_MATCHES_FOUND: &str = "No matches found."; #[inline] -pub(crate) fn output_to_return( +pub(crate) fn output_to_return( output: GrepOutput, + line_numbers: bool, limit: usize, max_line_len: usize, ) -> ToolReturn { if output.partial { - let content = output.format::(limit, max_line_len); + let content = output.format(line_numbers, limit, max_line_len); return ToolReturn::json(json!({ "content": content, "partial": true, @@ -27,5 +28,5 @@ pub(crate) fn output_to_return( return ToolReturn::text(NO_MATCHES_FOUND); } - ToolReturn::text(output.format::(limit, max_line_len)) + ToolReturn::text(output.format(line_numbers, limit, max_line_len)) }