Skip to content
Merged
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
13 changes: 7 additions & 6 deletions src/crates/core/src/agentic/agents/prompts/agentic_mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,14 @@ The user will primarily request you perform software engineering tasks. This inc
- Use specialized tools for file reads, edits, searches, and deletions because they preserve workspace context and permissions. Use Bash for commands that genuinely need a shell. Do not use shell commands only to communicate with the user.
- For security-sensitive tasks, support defensive analysis and remediation only. Refuse malicious code, exploit workflows, credential harvesting, or instructions that would facilitate abuse.
- Edit reliability discipline:
- The Edit tool is rejected until the target file has been read in the current session with a non-partial view, and the on-disk content still matches that read (or was refreshed by a prior Edit/Write in this session).
- Base `old_string` on the latest Read result for that file or exact content produced by a successful prior tool call.
- Read output uses cat -n format: spaces, line number, tab, then file content. Copy only the text after the tab into `old_string`.
- Treat Read output as stale after a successful edit to the same file; avoid parallel Edit calls against the same file unless the edits are independent and based on non-overlapping current content.
- Add stable surrounding context from the same block when a snippet may appear multiple times.
- Read a file in this session before Edit. Partial range reads are allowed, but the Read range must include every line you will copy into `old_string`.
- Base `old_string` on the latest Read result for that file (or exact content from a successful prior Edit/Write on the same file).
- Read output uses cat -n format: spaces, line number, tab, then file content. Copy only the text after the tab into `old_string` and `new_string`.
- Do not reformat HTML/CSS/JS when constructing Edit strings; match indentation and blank lines exactly.
- Treat Read output as stale after a successful edit to the same file; re-read before the next Edit unless you are continuing from the updated content in the prior tool result.
- Use 2-4 adjacent lines with stable surrounding context when that is enough to make `old_string` unique.
- Use `replace_all` only when every occurrence should change.
- If an edit fails because the text was not found or matched multiple locations, read the target area again before retrying rather than adjusting the failed string from memory. Use the nearby file snippets in the error when provided.
- If Edit fails because text was not found or matched multiple locations, Read the target lines again and retry with freshly copied text — do not adjust the failed string from memory.
- Subagent delegation: use Explore, FileFinder, or other Task subagents when their specialized focus, separate context, or autonomy is likely to improve coverage. For simple known-path, single-symbol, or one-file questions, direct tools are usually enough.
<example>
user: Give me a high-level map of how authentication flows through this monorepo
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ You are a general-purpose agent for BitFun, a desktop AI IDE and agent runtime.
- Use `Read` when you know the path or have narrowed the candidate set enough that reading is justified.
- Read before you edit. Do not propose or apply changes to code you have not inspected.
- Prefer focused edits to existing files over broad rewrites.
- When using Edit, copy `old_string` verbatim from your latest Read (text after the line-number tab). Do not reformat HTML, CSS, or indentation.
- Do not create new files unless they are clearly necessary for completing the requested task.
- Do not proactively create documentation files such as `README` or `*.md` unless the user explicitly asks for them.

Expand Down
27 changes: 17 additions & 10 deletions src/crates/core/src/agentic/tools/implementations/file_edit_tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,14 @@ Usage:
- You must use your `Read` tool at least once in the conversation before editing. This tool will error if you attempt an edit without reading the file.
- The `file_path` parameter must be a workspace-relative path, an absolute path inside the current workspace, or an exact `bitfun://runtime/...` URI returned by another tool.
- When editing text from Read tool output, ensure you preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix. The line number prefix format is: spaces + line number + tab. Everything after that is the actual file content to match. Never include any part of the line number prefix in the old_string or new_string.
- Copy `old_string` verbatim from your latest Read of this file. Do not reformat HTML/CSS/JS, do not normalize indentation, and do not reconstruct the block from memory.
- Use the smallest `old_string` that is clearly unique — usually 2-4 adjacent lines with stable surrounding context is sufficient.
- If Read output was truncated or used start_line/limit, re-read until the full target block is visible before editing.
- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.
- Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked.
- The edit will FAIL if `old_string` is not unique in the file. Either provide a larger string with more surrounding context to make it unique or use `replace_all` to change every instance of `old_string`.
- Use `replace_all` for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance."#;
- Use `replace_all` for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance.
- If an edit fails because the text was not found, call Read again on the target lines and retry with a freshly copied `old_string`."#;

impl Default for FileEditTool {
fn default() -> Self {
Expand Down Expand Up @@ -131,11 +135,11 @@ impl Tool for FileEditTool {
},
"old_string": {
"type": "string",
"description": "The text to replace"
"description": "Exact text to replace, copied verbatim from your latest Read of this file (content after the line-number tab only). Preserve indentation; do not reformat."
},
"new_string": {
"type": "string",
"description": "The text to replace it with (must be different from old_string)"
"description": "Replacement text with the same indentation style as old_string (must be different from old_string)"
},
"replace_all": {
"type": "boolean",
Expand Down Expand Up @@ -438,12 +442,13 @@ mod tests {
assert_eq!(description, EDIT_TOOL_PROMPT);
assert!(description.contains("You must use your `Read` tool"));
assert!(description.contains("spaces + line number + tab"));
assert!(description.contains("verbatim from your latest Read"));
assert!(description.contains("NEVER write new files unless explicitly required"));
assert!(!description.contains("auto-strip"));
}

#[test]
fn edit_tool_schema_uses_minimal_parameter_descriptions() {
fn edit_tool_schema_describes_exact_copy_from_read() {
let schema = FileEditTool::new().input_schema();
let properties = schema
.get("properties")
Expand All @@ -457,19 +462,21 @@ mod tests {
.and_then(Value::as_str),
Some("The path to the file to modify")
);
assert_eq!(
assert!(
properties
.get("old_string")
.and_then(|value| value.get("description"))
.and_then(Value::as_str),
Some("The text to replace")
.and_then(Value::as_str)
.unwrap_or_default()
.contains("latest Read")
);
assert_eq!(
assert!(
properties
.get("new_string")
.and_then(|value| value.get("description"))
.and_then(Value::as_str),
Some("The text to replace it with (must be different from old_string)")
.and_then(Value::as_str)
.unwrap_or_default()
.contains("indentation")
);
assert_eq!(
properties
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ pub struct FileReadTool {
max_total_chars: usize,
}

/// Default cap on characters returned by a single Read call (excluding wrapper text).
pub const DEFAULT_READ_MAX_TOTAL_CHARS: usize = 64_000;

impl Default for FileReadTool {
fn default() -> Self {
Self::new()
Expand All @@ -31,7 +34,7 @@ impl FileReadTool {
Self {
default_max_lines_to_read: 2000,
max_line_chars: 2000,
max_total_chars: 16_000,
max_total_chars: DEFAULT_READ_MAX_TOTAL_CHARS,
}
}

Expand Down Expand Up @@ -211,14 +214,14 @@ impl Tool for FileReadTool {
Usage:
- The file_path parameter must be workspace-relative, an absolute path inside the current workspace, or an exact `bitfun://runtime/...` URI returned by another tool.
- Do not read host roots or placeholder paths such as `/workspace`.
- By default, it reads up to {} lines starting from the beginning of the file.
- You can optionally specify a start_line and limit. For large files, prefer reading targeted ranges instead of starting over from the beginning every time.
- By default, it reads up to {} lines starting from the beginning of the file. When you plan to Edit a file, prefer this default full read so you see the exact bytes you will need to match.
- You can optionally specify a start_line and limit. Use a range only when you already know the target lines; the range must include every line you will copy into Edit `old_string`.
- Any lines longer than {} characters will be truncated.
- Total output is capped at {} characters. If that limit is hit, narrow the range with start_line and limit.
- Total output is capped at {} characters. If that limit is hit, continue with start_line/limit until the target lines are fully visible, then Edit using only text from those Read results.
- Results are returned using cat -n format, with line numbers starting at 1.
- This tool can only read files, not directories. To read a directory, use an ls command via the Bash tool.
- You can call multiple tools in a single response. It is always better to speculatively read multiple potentially useful files in parallel.
- Avoid tiny repeated slices (e.g. 30-100 line chunks). If you need more context, read a larger window.
- Avoid tiny repeated slices (e.g. 30-100 line chunks). If you need more context, read a larger window that covers the whole block you will edit.
"#,
self.default_max_lines_to_read, self.max_line_chars, self.max_total_chars
))
Expand Down
3 changes: 2 additions & 1 deletion src/crates/core/src/agentic/tools/tool_result_storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ use std::collections::HashSet;
use std::path::Path;

pub(crate) const DEFAULT_MAX_TOOL_RESULT_CHARS: usize = 50_000;
pub(crate) const READ_MAX_TOOL_RESULT_CHARS: usize = 16_000;
/// Keep in sync with `FileReadTool::DEFAULT_READ_MAX_TOTAL_CHARS` plus wrapper overhead.
pub(crate) const READ_MAX_TOOL_RESULT_CHARS: usize = 72_000;
pub(crate) const MAX_TOOL_RESULTS_PER_ROUND_CHARS: usize = 200_000;
pub(crate) const TOOL_RESULT_PREVIEW_CHARS: usize = 2_000;
pub(crate) const PERSISTED_OUTPUT_TAG: &str = "<persisted-output>";
Expand Down
18 changes: 8 additions & 10 deletions src/crates/tool-runtime/src/fs/edit_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use std::fs;
const MAX_MATCH_CONTEXTS: usize = 5;
const CONTEXT_LINES_BEFORE: usize = 2;
const CONTEXT_LINES_AFTER: usize = 2;
const NOT_FOUND_DIAGNOSTIC_SNIPPETS: usize = 3;
const NOT_FOUND_DIAGNOSTIC_SNIPPETS: usize = 1;
const NOT_FOUND_MIN_SUBSTRING_LEN: usize = 8;

/// Edit result, contains line number range information
Expand Down Expand Up @@ -180,17 +180,19 @@ fn snippet_context(lines: &[&str], line_idx: usize) -> String {
}

fn build_not_found_diagnostics(content: &str, old_string: &str) -> String {
let mut hints = Vec::new();
let mut hints = vec![
"Re-read the target lines with Read (use start_line/limit if needed), then copy the exact text after the tab on each line into old_string without reformatting indentation.".to_string(),
];

if contains_read_tool_line_prefixes(old_string) {
hints.push(
"Detected Read-tool line-number prefixes inside `old_string`. Copy only the text after the tab on each line, or rely on the tool to strip those prefixes automatically on retry.".to_string(),
"Detected Read-tool line-number prefixes inside `old_string`. Copy only the text after the tab on each line.".to_string(),
);
}

if contains_read_truncation_marker(old_string) {
hints.push(
"Detected a Read-tool `[truncated]` marker inside `old_string`. Re-read the file with a narrower start_line/limit so the target lines are complete.".to_string(),
"Detected a Read-tool `[truncated]` marker inside `old_string`. Re-read with start_line/limit so the target lines are complete.".to_string(),
);
}

Expand Down Expand Up @@ -239,17 +241,13 @@ fn build_not_found_diagnostics(content: &str, old_string: &str) -> String {

if !snippets.is_empty() {
hints.push(format!(
"Current file snippets that may be closest to your `old_string`:\n{}",
"Closest current file snippet:\n{}",
snippets.join("\n---\n")
));
}
}

if hints.is_empty() {
"No close match was found in the current file contents. Re-read the target area and copy the exact current text.".to_string()
} else {
hints.join("\n\n")
}
hints.join("\n\n")
}

fn apply_match_and_replace(
Expand Down
Loading