Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions crates/forge_app/src/fmt/fmt_input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ impl FormatContent for ToolCatalog {
match self {
ToolCatalog::Read(input) => {
let display_path = display_path_for(&input.file_path);
let is_explicit_range = input.start_line.is_some() || input.end_line.is_some();
let is_explicit_range = input.range.is_some();
let mut subtitle = display_path;
if is_explicit_range {
match (&input.start_line, &input.end_line) {
if is_explicit_range && let Some(range) = &input.range {
match (range.start_line, range.end_line) {
(Some(start), Some(end)) => {
subtitle.push_str(&format!(":{start}-{end}"));
}
Expand Down
6 changes: 2 additions & 4 deletions crates/forge_app/src/fmt/fmt_output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,7 @@ mod tests {
let fixture = ToolOperation::FsRead {
input: forge_domain::FSRead {
file_path: "/home/user/test.txt".to_string(),
start_line: None,
end_line: None,
range: None,
show_line_numbers: true,
},
output: ReadOutput {
Expand All @@ -111,8 +110,7 @@ mod tests {
let fixture = ToolOperation::FsRead {
input: forge_domain::FSRead {
file_path: "/home/user/test.txt".to_string(),
start_line: Some(2),
end_line: Some(4),
range: Some(forge_domain::FSReadRange { start_line: Some(2), end_line: Some(4) }),
show_line_numbers: true,
},
output: ReadOutput {
Expand Down
17 changes: 6 additions & 11 deletions crates/forge_app/src/operation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -741,7 +741,7 @@ mod tests {
use std::fmt::Write;
use std::path::PathBuf;

use forge_domain::{FSRead, FileInfo, ToolValue};
use forge_domain::{FSRead, FSReadRange, FileInfo, ToolValue};

use super::*;
use crate::{Content, Match, MatchResult};
Expand Down Expand Up @@ -861,8 +861,7 @@ mod tests {
let fixture = ToolOperation::FsRead {
input: FSRead {
file_path: "/home/user/test.txt".to_string(),
start_line: None,
end_line: None,
range: None,
show_line_numbers: true,
},
output: ReadOutput {
Expand Down Expand Up @@ -892,8 +891,7 @@ mod tests {
let fixture = ToolOperation::FsRead {
input: FSRead {
file_path: "/home/user/test.txt".to_string(),
start_line: None,
end_line: None,
range: None,
show_line_numbers: true,
},
output: ReadOutput {
Expand Down Expand Up @@ -922,8 +920,7 @@ mod tests {
let fixture = ToolOperation::FsRead {
input: FSRead {
file_path: "/home/user/test.txt".to_string(),
start_line: Some(2),
end_line: Some(3),
range: Some(FSReadRange { start_line: Some(2), end_line: Some(3) }),
show_line_numbers: true,
},
output: ReadOutput {
Expand Down Expand Up @@ -953,8 +950,7 @@ mod tests {
let fixture = ToolOperation::FsRead {
input: FSRead {
file_path: "/home/user/large_file.txt".to_string(),
start_line: None,
end_line: None,
range: None,
show_line_numbers: true,
},
output: ReadOutput {
Expand Down Expand Up @@ -2621,8 +2617,7 @@ mod tests {
let fixture = ToolOperation::FsRead {
input: FSRead {
file_path: "/home/user/test.png".to_string(),
start_line: None,
end_line: None,
range: None,
show_line_numbers: true,
},
output: ReadOutput {
Expand Down
12 changes: 10 additions & 2 deletions crates/forge_app/src/tool_executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,16 @@ impl<
.services
.read(
normalized_path,
input.start_line.map(|i| i as u64),
input.end_line.map(|i| i as u64),
input
.range
.as_ref()
.and_then(|r| r.start_line)
.map(|i| i as u64),
input
.range
.as_ref()
.and_then(|r| r.end_line)
.map(|i| i as u64),
)
.await?;

Expand Down
44 changes: 26 additions & 18 deletions crates/forge_domain/src/tools/catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,27 +188,35 @@ impl Todo {
}
}

/// Optional line range for partial file reads.
#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
#[schemars(deny_unknown_fields)]
pub struct FSReadRange {
/// 1-based first line.
#[serde(skip_serializing_if = "Option::is_none")]
pub start_line: Option<i32>,

/// Inclusive 1-based last line.
#[serde(skip_serializing_if = "Option::is_none")]
pub end_line: Option<i32>,
}

#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, ToolDescription, PartialEq)]
#[tool_description_file = "crates/forge_domain/src/tools/descriptions/fs_read.md"]
#[schemars(deny_unknown_fields)]
pub struct FSRead {
/// The absolute path to the file to read
/// Absolute path to the file to read.
#[serde(alias = "path")]
pub file_path: String,

/// The line number to start reading from starting from 1 not 0. Only
/// provide if the file is too large to read at once
/// Optional line range for partial reads.
#[serde(skip_serializing_if = "Option::is_none")]
pub start_line: Option<i32>,
pub range: Option<FSReadRange>,

/// If true, prefixes each line with its line index (starting at 1).
/// Defaults to true.
#[serde(default = "default_true")]
pub show_line_numbers: bool,

/// The line number to stop reading at (inclusive). Only provide if the file
/// is too large to read at once
#[serde(skip_serializing_if = "Option::is_none")]
pub end_line: Option<i32>,
}

#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, ToolDescription, PartialEq)]
Expand Down Expand Up @@ -1261,7 +1269,7 @@ mod tests {
name: ToolName::new("read"),
call_id: None,
arguments: ToolCallArguments::from_json(
r#"{"path": "/test/path.rs", "start_line": "10", "end_line": "20"}"#,
r#"{"path": "/test/path.rs", "range": {"start_line": 10, "end_line": 20}}"#,
),
thought_signature: None,
};
Expand All @@ -1276,8 +1284,8 @@ mod tests {

if let Ok(ToolCatalog::Read(fs_read)) = actual {
assert_eq!(fs_read.file_path, "/test/path.rs");
assert_eq!(fs_read.start_line, Some(10));
assert_eq!(fs_read.end_line, Some(20));
assert_eq!(fs_read.range.as_ref().and_then(|r| r.start_line), Some(10));
assert_eq!(fs_read.range.as_ref().and_then(|r| r.end_line), Some(20));
} else {
panic!("Expected FSRead variant");
}
Expand All @@ -1292,7 +1300,7 @@ mod tests {
name: ToolName::new("read"),
call_id: None,
arguments: ToolCallArguments::from_json(
r#"{"path": "/test/path.rs", "start_line": 10, "end_line": 20}"#,
r#"{"path": "/test/path.rs", "range": {"start_line": 10, "end_line": 20}}"#,
),
thought_signature: None,
};
Expand All @@ -1306,8 +1314,8 @@ mod tests {

if let Ok(ToolCatalog::Read(fs_read)) = actual {
assert_eq!(fs_read.file_path, "/test/path.rs");
assert_eq!(fs_read.start_line, Some(10));
assert_eq!(fs_read.end_line, Some(20));
assert_eq!(fs_read.range.as_ref().and_then(|r| r.start_line), Some(10));
assert_eq!(fs_read.range.as_ref().and_then(|r| r.end_line), Some(20));
} else {
panic!("Expected FSRead variant");
}
Expand All @@ -1322,7 +1330,7 @@ mod tests {
name: ToolName::new("Read"),
call_id: None,
arguments: ToolCallArguments::from_json(
r#"{"path": "/test/path.rs", "start_line": 10, "end_line": 20}"#,
r#"{"path": "/test/path.rs", "range": {"start_line": 10, "end_line": 20}}"#,
),
thought_signature: None,
};
Expand All @@ -1336,8 +1344,8 @@ mod tests {

if let Ok(ToolCatalog::Read(fs_read)) = actual {
assert_eq!(fs_read.file_path, "/test/path.rs");
assert_eq!(fs_read.start_line, Some(10));
assert_eq!(fs_read.end_line, Some(20));
assert_eq!(fs_read.range.as_ref().and_then(|r| r.start_line), Some(10));
assert_eq!(fs_read.range.as_ref().and_then(|r| r.end_line), Some(20));
} else {
panic!("Expected FSRead variant");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
source: crates/forge_domain/src/tools/definition/usage.rs
expression: prompt
---
<tool>{"name":"read","description":"Reads a file from the local filesystem. You can access any file directly by using this tool. Assume this tool is able to read all files on the machine. If the User provides a path to a file assume that path is valid. It is okay to read a file that does not exist; an error will be returned.\n\nUsage:\n- The file_path parameter must be an absolute path, not a relative path\n- By default, it reads up to {{config.maxReadSize}} lines starting from the beginning of the file\n- You can optionally specify a line start_line and end_line (especially handy for long files), but it's recommended to read the whole file by not providing these parameters\n- Any lines longer than {{config.maxLineLength}} characters will be truncated\n- Results are returned using rg \"\" -n format, with line numbers starting at 1\n{{#if (contains model.input_modalities \"image\")}}\n- This tool allows Forge Code to read images (eg PNG, JPG, etc). When reading an image file the contents are presented visually.\n- PDFs, Automatically encoded as base64 and sent as visual content for LLM to analyze pages. Any PDFs larger than {{config.maxImageSize}} bytes will return error\n{{/if}}\n- Jupyter notebooks (.ipynb files) are read as plain JSON text - you can parse the cell structure, outputs, and embedded content directly from the JSON\n- This tool can only read files, not directories. To read a directory, use an ls command via the `{{tool_names.shell}}` tool.\n- You can call multiple tools in a single response. It is always better to speculatively read multiple potentially useful files in parallel.","arguments":{"end_line":{"description":"The line number to stop reading at (inclusive). Only provide if the file\nis too large to read at once","type":"integer","is_required":false},"file_path":{"description":"The absolute path to the file to read","type":"string","is_required":true},"show_line_numbers":{"description":"If true, prefixes each line with its line index (starting at 1).\nDefaults to true.","type":"boolean","is_required":false},"start_line":{"description":"The line number to start reading from starting from 1 not 0. Only\nprovide if the file is too large to read at once","type":"integer","is_required":false}}}</tool>
<tool>{"name":"read","description":"Reads a file from the local filesystem. You can access any file directly by using this tool. Assume this tool is able to read all files on the machine. If the User provides a path to a file assume that path is valid. It is okay to read a file that does not exist; an error will be returned.\n\nUsage:\n- The file_path parameter must be an absolute path, not a relative path\n- By default, it reads up to {{config.maxReadSize}} lines starting from the beginning of the file\n- You can optionally specify a line start_line and end_line (especially handy for long files), but it's recommended to read the whole file by not providing these parameters\n- Any lines longer than {{config.maxLineLength}} characters will be truncated\n- Results are returned using rg \"\" -n format, with line numbers starting at 1\n{{#if (contains model.input_modalities \"image\")}}\n- This tool allows Forge Code to read images (eg PNG, JPG, etc). When reading an image file the contents are presented visually.\n- PDFs, Automatically encoded as base64 and sent as visual content for LLM to analyze pages. Any PDFs larger than {{config.maxImageSize}} bytes will return error\n{{/if}}\n- Jupyter notebooks (.ipynb files) are read as plain JSON text - you can parse the cell structure, outputs, and embedded content directly from the JSON\n- This tool can only read files, not directories. To read a directory, use an ls command via the `{{tool_names.shell}}` tool.\n- You can call multiple tools in a single response. It is always better to speculatively read multiple potentially useful files in parallel.","arguments":{"file_path":{"description":"Absolute path to the file to read.","type":"string","is_required":true},"range":{"description":"Optional line range for partial reads.","type":"object","is_required":false},"show_line_numbers":{"description":"If true, prefixes each line with its line index (starting at 1).\nDefaults to true.","type":"boolean","is_required":false}}}</tool>
<tool>{"name":"write","description":"Writes a file to the local filesystem.\n\nUsage:\n- This tool will overwrite the existing file if there is one at the provided path.\n- If this is an existing file, you MUST use the {{tool_names.read}} tool first to read the file's contents and use this tool with 'overwrite' as true . This tool will fail if you did not read the file first or don't set overwrite parameter to true.\n- ALWAYS prefer {{tool_names.patch}} on existing files in the codebase. NEVER write new files unless explicitly required.\n- NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.\n- Only use emojis if the user explicitly requests it. Avoid writing emojis to files unless asked.","arguments":{"content":{"description":"The content to write to the file","type":"string","is_required":true},"file_path":{"description":"The absolute path to the file to write (must be absolute, not relative)","type":"string","is_required":true},"overwrite":{"description":"If set to true, existing files will be overwritten. If not set and the\nfile exists, an error will be returned with the content of the\nexisting file.","type":"boolean","is_required":false}}}</tool>
<tool>{"name":"fs_search","description":"A powerful search tool built on ripgrep\n\nUsage:\n- ALWAYS use `{{tool_names.fs_search}}` for search tasks. NEVER invoke `grep` or `rg` as a Bash command. The `{{tool_names.fs_search}}` tool has been optimized for correct permissions and access.\n- Supports full regex syntax (e.g., \"log.*Error\", \"function\\\\s+\\\\w+\")\n- Filter files with glob parameter (e.g., \"*.js\", \"**/*.tsx\") or type parameter (e.g., \"js\", \"py\", \"rust\")\n- Output modes: \"content\" shows matching lines, \"files_with_matches\" shows only file paths (default), \"count\" shows match counts\n- Use Task tool for open-ended searches requiring multiple rounds\n- Pattern syntax: Uses ripgrep (not grep) - literal braces need escaping (use `interface\\\\{\\\\}` to find `interface{}` in Go code)\n- Multiline matching: By default patterns match within single lines only. For cross-line patterns like `struct \\\\{[\\\\s\\\\S]*?field`, use `multiline: true`","arguments":{"-A":{"description":"Number of lines to show after each match (rg -A). Requires output_mode:\n\"content\", ignored otherwise.","type":"integer","is_required":false},"-B":{"description":"Number of lines to show before each match (rg -B). Requires output_mode:\n\"content\", ignored otherwise.","type":"integer","is_required":false},"-C":{"description":"Number of lines to show before and after each match (rg -C). Requires\noutput_mode: \"content\", ignored otherwise.","type":"integer","is_required":false},"-i":{"description":"Case insensitive search (rg -i)","type":"boolean","is_required":false},"-n":{"description":"Show line numbers in output (rg -n). Requires output_mode: \"content\",\nignored otherwise.","type":"boolean","is_required":false},"glob":{"description":"Glob pattern to filter files (e.g. \"*.js\", \"*.{ts,tsx}\") - maps to rg\n--glob","type":"string","is_required":false},"head_limit":{"description":"Limit output to first N lines/entries, equivalent to \"| head -N\". Works\nacross all output modes: content (limits output lines),\nfiles_with_matches (limits file paths), count (limits count entries).\nWhen unspecified, shows all results from ripgrep.","type":"integer","is_required":false},"multiline":{"description":"Enable multiline mode where . matches newlines and patterns can span\nlines (rg -U --multiline-dotall). Default: false.","type":"boolean","is_required":false},"offset":{"description":"Skip first N lines/entries before applying head_limit","type":"integer","is_required":false},"output_mode":{"description":"Output mode: \"content\" shows matching lines (supports -A/-B/-C context,\n-n line numbers, head_limit), \"files_with_matches\" shows file paths\n(supports head_limit), \"count\" shows match counts (supports head_limit).\nDefaults to \"files_with_matches\".","type":"string","is_required":false},"path":{"description":"File or directory to search in (rg PATH). Defaults to current working\ndirectory.","type":"string","is_required":false},"pattern":{"description":"The regular expression pattern to search for in file contents.","type":"string","is_required":true},"type":{"description":"File type to search (rg --type). Common types: js, py, rust, go, java,\netc. More efficient than include for standard file types.","type":"string","is_required":false}}}</tool>
<tool>{"name":"sem_search","description":"AI-powered semantic code search. YOUR DEFAULT TOOL for code discovery and exploration when searching within {{env.cwd}}. Use this when you need to find code locations, understand implementations, discover patterns, or explore unfamiliar code - it works with natural language about behavior and concepts, not just keyword matching.\n\n**WHEN TO USE sem_search:**\n- Finding implementation of specific features or algorithms\n- Understanding how a system works across multiple files\n- Discovering architectural patterns and design approaches\n- Locating test examples or fixtures\n- Finding where specific technologies/libraries are used\n- Exploring unfamiliar codebases to learn structure\n- Finding documentation files (README, guides, API docs)\n\n**WHEN NOT TO USE (use {{tool_names.fs_search}} instead):**\n- Searching for exact strings, TODOs, or specific function names\n- Finding all occurrences of a variable or identifier\n- Searching in specific file paths or with regex patterns\n- When you know the exact text to search for\n\nIMPORTANT: Only searches within {{env.cwd}} and subdirectories. For paths outside this scope, use {{tool_names.fs_search}} with path parameter.\n\n**TIPS FOR SUCCESS:**\n- Use 2-3 varied queries to capture different aspects (e.g., \"OAuth token refresh\", \"JWT expiry handling\", \"authentication middleware\")\n- Balance specificity (focused results) with generality (don't miss relevant code)\n- Avoid overly broad queries like \"authentication\" or \"tools\" - be specific about what aspect you need\n- Keep queries targeted - too many broad queries can cause timeouts\n- **Match your intent**: If seeking documentation, use doc-focused keywords (\"setup guide\", \"configuration README\"); if seeking code, use implementation terms (\"token refresh logic\", \"error handling implementation\")\n\nReturns the topK most relevant file:line locations with code context. Each query is ranked independently, then reranked by relevance to your stated intent.","arguments":{"queries":{"description":"List of search queries to execute in parallel. Using multiple queries\n(2-3) with varied phrasings significantly improves results - each query\ncaptures different aspects of what you're looking for. Each query pairs\na search term with a use_case for reranking. Example: for\nauthentication, try \"user login verification\", \"token generation\",\n\"OAuth flow\".","type":"array","is_required":true}}}</tool>
Expand Down
Loading
Loading