Close spec compliance gaps in coding-agent-loop

Fix all gaps identified by spec review against docs/specs/coding-agent-loop-spec.md:

- Replace placeholder system prompts with substantial provider-aligned prompts
  for OpenAI (codex-rs style), Anthropic (Claude Code style), and Gemini
  (gemini-cli style) covering identity, tool usage, and coding best practices
- Fix ExecutionEnvironment trait: add offset/limit to read_file, depth to
  list_directory, path to glob, remove separate args from exec_command
- LocalEnv: use /bin/bash -c, spawn process groups with setsid, SIGTERM to
  -pid, use ripgrep with grep fallback, add env var safelist
- Add environment context block with <environment> XML tags including git
  branch, date, model, and knowledge cutoff fields
- Capture git context snapshot (branch, status, recent commits) on init
- Add user_instructions to SessionConfig, appended as final prompt layer
- Emit AssistantTextStart before LLM calls, SESSION_END on abort path
- Fix truncation messages to match spec wording exactly
- Implement provider_options() for all profiles (reasoning, beta headers,
  safety settings)
- Add Gemini-specific tools: read_many_files, list_dir, web_search, web_fetch
- Wire spawn_agent max_turns parameter
- Fall back to working_dir for project doc discovery outside git repos

All 188 tests pass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-02-20 15:22:10 -04:00
parent 99db2753ca
commit 8ff36fa900
13 changed files with 778 additions and 361 deletions

View file

@ -25,14 +25,13 @@ pub struct GrepOptions {
#[async_trait]
pub trait ExecutionEnvironment: Send + Sync {
async fn read_file(&self, path: &str) -> Result<String, String>;
async fn read_file(&self, path: &str, offset: Option<usize>, limit: Option<usize>) -> Result<String, String>;
async fn write_file(&self, path: &str, content: &str) -> Result<(), String>;
async fn file_exists(&self, path: &str) -> Result<bool, String>;
async fn list_directory(&self, path: &str) -> Result<Vec<DirEntry>, String>;
async fn list_directory(&self, path: &str, depth: Option<usize>) -> Result<Vec<DirEntry>, String>;
async fn exec_command(
&self,
command: &str,
args: &[String],
timeout_ms: u64,
working_dir: Option<&str>,
env_vars: Option<&std::collections::HashMap<String, String>>,
@ -43,7 +42,7 @@ pub trait ExecutionEnvironment: Send + Sync {
path: &str,
options: &GrepOptions,
) -> Result<Vec<String>, String>;
async fn glob(&self, pattern: &str) -> Result<Vec<String>, String>;
async fn glob(&self, pattern: &str, path: Option<&str>) -> Result<Vec<String>, String>;
async fn initialize(&self) -> Result<(), String>;
async fn cleanup(&self) -> Result<(), String>;
fn working_directory(&self) -> &str;
@ -60,7 +59,7 @@ mod tests {
#[async_trait]
impl ExecutionEnvironment for MockEnv {
async fn read_file(&self, _path: &str) -> Result<String, String> {
async fn read_file(&self, _path: &str, _offset: Option<usize>, _limit: Option<usize>) -> Result<String, String> {
Ok("hello".into())
}
async fn write_file(&self, _path: &str, _content: &str) -> Result<(), String> {
@ -69,7 +68,7 @@ mod tests {
async fn file_exists(&self, _path: &str) -> Result<bool, String> {
Ok(true)
}
async fn list_directory(&self, _path: &str) -> Result<Vec<DirEntry>, String> {
async fn list_directory(&self, _path: &str, _depth: Option<usize>) -> Result<Vec<DirEntry>, String> {
Ok(vec![DirEntry {
name: "test.rs".into(),
is_dir: false,
@ -79,7 +78,6 @@ mod tests {
async fn exec_command(
&self,
_command: &str,
_args: &[String],
_timeout_ms: u64,
_working_dir: Option<&str>,
_env_vars: Option<&std::collections::HashMap<String, String>>,
@ -100,7 +98,7 @@ mod tests {
) -> Result<Vec<String>, String> {
Ok(vec!["match".into()])
}
async fn glob(&self, _pattern: &str) -> Result<Vec<String>, String> {
async fn glob(&self, _pattern: &str, _path: Option<&str>) -> Result<Vec<String>, String> {
Ok(vec!["file.rs".into()])
}
async fn initialize(&self) -> Result<(), String> {
@ -123,14 +121,14 @@ mod tests {
#[tokio::test]
async fn mock_env_read_file() {
let env: Arc<dyn ExecutionEnvironment> = Arc::new(MockEnv);
let result = env.read_file("test.rs").await.unwrap();
let result = env.read_file("test.rs", None, None).await.unwrap();
assert_eq!(result, "hello");
}
#[tokio::test]
async fn mock_env_exec_command() {
let env: Arc<dyn ExecutionEnvironment> = Arc::new(MockEnv);
let result = env.exec_command("echo", &[], 5000, None, None).await.unwrap();
let result = env.exec_command("echo", 5000, None, None).await.unwrap();
assert_eq!(result.exit_code, 0);
assert!(!result.timed_out);
}
@ -138,7 +136,7 @@ mod tests {
#[tokio::test]
async fn mock_env_list_directory() {
let env: Arc<dyn ExecutionEnvironment> = Arc::new(MockEnv);
let entries = env.list_directory("/tmp").await.unwrap();
let entries = env.list_directory("/tmp", None).await.unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].name, "test.rs");
assert!(!entries[0].is_dir);

View file

@ -15,20 +15,15 @@ impl LocalExecutionEnvironment {
Self { working_directory }
}
fn format_line_numbered(content: &str) -> String {
use std::fmt::Write;
let lines: Vec<&str> = content.lines().collect();
let width = lines.len().to_string().len().max(1);
let mut result = String::new();
let mut line_num = 1;
for line in &lines {
let _ = writeln!(result, "{line_num:>width$} | {line}");
line_num += 1;
}
result
}
const ENV_SAFELIST: &'static [&'static str] = &[
"PATH", "HOME", "USER", "SHELL", "LANG", "TERM", "TMPDIR",
"GOPATH", "CARGO_HOME", "NVM_DIR",
];
fn should_filter_env_var(key: &str) -> bool {
if Self::ENV_SAFELIST.contains(&key) {
return false;
}
let lower = key.to_lowercase();
lower.ends_with("_api_key")
|| lower.ends_with("_secret")
@ -49,12 +44,25 @@ impl LocalExecutionEnvironment {
#[async_trait]
impl ExecutionEnvironment for LocalExecutionEnvironment {
async fn read_file(&self, path: &str) -> Result<String, String> {
async fn read_file(&self, path: &str, offset: Option<usize>, limit: Option<usize>) -> Result<String, String> {
let full_path = self.resolve_path(path);
let content = tokio::fs::read_to_string(&full_path)
.await
.map_err(|e| format!("Failed to read {}: {e}", full_path.display()))?;
Ok(Self::format_line_numbered(&content))
let all_lines: Vec<&str> = content.lines().collect();
let skip = offset.unwrap_or(0);
let take = limit.unwrap_or(all_lines.len());
let selected: Vec<&str> = all_lines.into_iter().skip(skip).take(take).collect();
use std::fmt::Write;
let width = (skip + selected.len()).to_string().len().max(1);
let mut result = String::new();
for (i, line) in selected.iter().enumerate() {
let line_num = skip + i + 1;
let _ = writeln!(result, "{line_num:>width$} | {line}");
}
Ok(result)
}
async fn write_file(&self, path: &str, content: &str) -> Result<(), String> {
@ -74,40 +82,57 @@ impl ExecutionEnvironment for LocalExecutionEnvironment {
Ok(full_path.exists())
}
async fn list_directory(&self, path: &str) -> Result<Vec<DirEntry>, String> {
async fn list_directory(&self, path: &str, depth: Option<usize>) -> Result<Vec<DirEntry>, String> {
let full_path = self.resolve_path(path);
let mut entries = Vec::new();
let mut read_dir = tokio::fs::read_dir(&full_path)
.await
.map_err(|e| format!("Failed to read directory {}: {e}", full_path.display()))?;
let max_depth = depth.unwrap_or(1);
while let Some(entry) = read_dir
.next_entry()
.await
.map_err(|e| format!("Failed to read entry: {e}"))?
{
let metadata = entry
.metadata()
.await
.map_err(|e| format!("Failed to read metadata: {e}"))?;
entries.push(DirEntry {
name: entry.file_name().to_string_lossy().into_owned(),
is_dir: metadata.is_dir(),
size: if metadata.is_file() {
Some(metadata.len())
fn list_recursive(
base: &std::path::Path,
prefix: &str,
current_depth: usize,
max_depth: usize,
entries: &mut Vec<DirEntry>,
) -> Result<(), String> {
let mut dir_entries: Vec<std::fs::DirEntry> = std::fs::read_dir(base)
.map_err(|e| format!("Failed to read directory {}: {e}", base.display()))?
.filter_map(|e| e.ok())
.collect();
dir_entries.sort_by_key(|e| e.file_name());
for entry in dir_entries {
let metadata = entry
.metadata()
.map_err(|e| format!("Failed to read metadata: {e}"))?;
let name = if prefix.is_empty() {
entry.file_name().to_string_lossy().into_owned()
} else {
None
},
});
format!("{prefix}/{}", entry.file_name().to_string_lossy())
};
let is_dir = metadata.is_dir();
entries.push(DirEntry {
name: name.clone(),
is_dir,
size: if metadata.is_file() {
Some(metadata.len())
} else {
None
},
});
if is_dir && current_depth + 1 < max_depth {
list_recursive(&entry.path(), &name, current_depth + 1, max_depth, entries)?;
}
}
Ok(())
}
entries.sort_by(|a, b| a.name.cmp(&b.name));
let mut entries = Vec::new();
list_recursive(&full_path, "", 0, max_depth, &mut entries)?;
Ok(entries)
}
async fn exec_command(
&self,
command: &str,
args: &[String],
timeout_ms: u64,
working_dir: Option<&str>,
env_vars: Option<&std::collections::HashMap<String, String>>,
@ -129,17 +154,26 @@ impl ExecutionEnvironment for LocalExecutionEnvironment {
std::path::PathBuf::from,
);
let mut cmd = Command::new(command);
cmd.args(args)
let mut cmd = Command::new("/bin/bash");
cmd.arg("-c")
.arg(command)
.current_dir(&effective_dir)
.env_clear()
.envs(filtered_env)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped());
#[cfg(unix)]
unsafe {
cmd.pre_exec(|| {
libc::setpgid(0, 0);
Ok(())
});
}
let mut child = cmd
.spawn()
.map_err(|e| format!("Failed to spawn {command}: {e}"))?;
.map_err(|e| format!("Failed to spawn command: {e}"))?;
let timeout_duration = std::time::Duration::from_millis(timeout_ms);
@ -149,12 +183,13 @@ impl ExecutionEnvironment for LocalExecutionEnvironment {
status_result.map_err(|e| format!("Failed to wait for process: {e}"))?;
(false, status.code().unwrap_or(-1))
} else {
// SIGTERM first, then SIGKILL after 2 seconds
// SIGTERM the process group first, then SIGKILL after 2 seconds
#[cfg(unix)]
if let Some(pid) = child.id() {
#[allow(clippy::cast_possible_wrap)]
unsafe {
libc::kill(pid as i32, libc::SIGTERM);
// Negative pid sends signal to the entire process group
libc::kill(-(pid as i32), libc::SIGTERM);
}
// Wait 2 seconds for graceful shutdown
if tokio::time::timeout(
@ -207,37 +242,71 @@ impl ExecutionEnvironment for LocalExecutionEnvironment {
) -> Result<Vec<String>, String> {
let full_path = self.resolve_path(path);
let mut args = vec!["-rn".to_string()];
if options.case_insensitive {
args.push("-i".into());
}
if let Some(ref glob_filter) = options.glob_filter {
args.push("--include".into());
args.push(glob_filter.clone());
}
if let Some(max) = options.max_results {
args.push("-m".into());
args.push(max.to_string());
}
args.push(pattern.into());
args.push(full_path.to_string_lossy().into_owned());
// Try rg (ripgrep) first, fall back to grep
let use_rg = std::process::Command::new("rg")
.arg("--version")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.is_ok();
let output = std::process::Command::new("grep")
.args(&args)
.output()
.map_err(|e| format!("Failed to run grep: {e}"))?;
let output = if use_rg {
let mut args = vec!["-n".to_string()];
if options.case_insensitive {
args.push("-i".into());
}
if let Some(ref glob_filter) = options.glob_filter {
args.push("--glob".into());
args.push(glob_filter.clone());
}
if let Some(max) = options.max_results {
args.push("-m".into());
args.push(max.to_string());
}
args.push(pattern.into());
args.push(full_path.to_string_lossy().into_owned());
std::process::Command::new("rg")
.args(&args)
.output()
.map_err(|e| format!("Failed to run rg: {e}"))?
} else {
let mut args = vec!["-rn".to_string()];
if options.case_insensitive {
args.push("-i".into());
}
if let Some(ref glob_filter) = options.glob_filter {
args.push("--include".into());
args.push(glob_filter.clone());
}
if let Some(max) = options.max_results {
args.push("-m".into());
args.push(max.to_string());
}
args.push(pattern.into());
args.push(full_path.to_string_lossy().into_owned());
std::process::Command::new("grep")
.args(&args)
.output()
.map_err(|e| format!("Failed to run grep: {e}"))?
};
let stdout = String::from_utf8_lossy(&output.stdout);
let results: Vec<String> = stdout.lines().map(String::from).filter(|l| !l.is_empty()).collect();
Ok(results)
}
async fn glob(&self, pattern: &str) -> Result<Vec<String>, String> {
// Use find + fnmatch-style pattern via shell glob expansion
async fn glob(&self, pattern: &str, path: Option<&str>) -> Result<Vec<String>, String> {
let base_dir = path.map_or_else(
|| self.working_directory.clone(),
std::path::PathBuf::from,
);
let full_pattern = if Path::new(pattern).is_absolute() {
pattern.to_string()
} else {
format!("{}/{pattern}", self.working_directory.display())
format!("{}/{pattern}", base_dir.display())
};
// Use shell globbing via ls
@ -327,7 +396,7 @@ mod tests {
std::fs::write(dir.join("test.txt"), "hello\nworld\nfoo").unwrap();
let env = LocalExecutionEnvironment::new(dir.clone());
let result = env.read_file("test.txt").await.unwrap();
let result = env.read_file("test.txt", None, None).await.unwrap();
assert_eq!(result, "1 | hello\n2 | world\n3 | foo\n");
std::fs::remove_dir_all(&dir).unwrap();
@ -340,7 +409,7 @@ mod tests {
std::fs::write(dir.join("padded.txt"), content.trim_end()).unwrap();
let env = LocalExecutionEnvironment::new(dir.clone());
let result = env.read_file("padded.txt").await.unwrap();
let result = env.read_file("padded.txt", None, None).await.unwrap();
assert!(result.starts_with(" 1 | line 1\n"));
assert!(result.contains("12 | line 12\n"));
@ -351,7 +420,7 @@ mod tests {
async fn read_file_not_found() {
let dir = temp_dir();
let env = LocalExecutionEnvironment::new(dir.clone());
let result = env.read_file("nonexistent.txt").await;
let result = env.read_file("nonexistent.txt", None, None).await;
assert!(result.is_err());
std::fs::remove_dir_all(&dir).unwrap();
}
@ -393,7 +462,7 @@ mod tests {
std::fs::create_dir(dir.join("c_dir")).unwrap();
let env = LocalExecutionEnvironment::new(dir.clone());
let entries = env.list_directory(".").await.unwrap();
let entries = env.list_directory(".", None).await.unwrap();
assert_eq!(entries.len(), 3);
assert_eq!(entries[0].name, "a.txt");
@ -411,7 +480,7 @@ mod tests {
let dir = temp_dir();
let env = LocalExecutionEnvironment::new(dir.clone());
let result = env
.exec_command("echo", &["hello".into()], 5000, None, None)
.exec_command("echo hello", 5000, None, None)
.await
.unwrap();
@ -427,7 +496,7 @@ mod tests {
let dir = temp_dir();
let env = LocalExecutionEnvironment::new(dir.clone());
let result = env
.exec_command("sh", &["-c".into(), "exit 42".into()], 5000, None, None)
.exec_command("exit 42", 5000, None, None)
.await
.unwrap();
@ -441,7 +510,7 @@ mod tests {
let dir = temp_dir();
let env = LocalExecutionEnvironment::new(dir.clone());
let result = env
.exec_command("sleep", &["10".into()], 200, None, None)
.exec_command("sleep 10", 200, None, None)
.await
.unwrap();
@ -455,7 +524,7 @@ mod tests {
let dir = temp_dir();
let env = LocalExecutionEnvironment::new(dir.clone());
let result = env
.exec_command("sh", &["-c".into(), "echo err >&2".into()], 5000, None, None)
.exec_command("echo err >&2", 5000, None, None)
.await
.unwrap();
@ -602,21 +671,10 @@ mod tests {
std::fs::write(dir.join("c.txt"), "").unwrap();
let env = LocalExecutionEnvironment::new(dir.clone());
let results = env.glob("*.rs").await.unwrap();
let results = env.glob("*.rs", None).await.unwrap();
assert_eq!(results.len(), 2);
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn format_line_numbered_empty() {
let result = LocalExecutionEnvironment::format_line_numbered("");
assert_eq!(result, "");
}
#[test]
fn format_line_numbered_single_line() {
let result = LocalExecutionEnvironment::format_line_numbered("hello");
assert_eq!(result, "1 | hello\n");
}
}

View file

@ -96,31 +96,76 @@ impl ProviderProfile for AnthropicProfile {
};
format!(
"You are Claude, an AI coding assistant by Anthropic. \
You help users with software engineering tasks including solving bugs, \
adding new functionality, refactoring code, and explaining code.\n\n\
{env_block}\n\n\
# Tools\n\
Use the provided tools to interact with the codebase and environment.\n\n\
## read_file\n\
Read files before editing them. Use offset/limit for large files.\n\n\
## edit_file\n\
The old_string must be an exact match of existing text and must be unique in the file. \
If old_string matches multiple locations, provide more surrounding context to make it unique. \
Prefer editing existing files over creating new ones.\n\n\
## write_file\n\
Use write_file only when creating new files. Prefer edit_file for modifying existing files.\n\n\
## shell\n\
Use for running commands, tests, and builds. Default timeout is 120 seconds.\n\n\
## grep\n\
Search file contents with regex patterns. Supports output modes: content, files_with_matches, count.\n\n\
## glob\n\
Find files by name pattern. Results sorted by modification time (newest first).\n\n\
# Coding Best Practices\n\
Write clean, maintainable code. Handle errors appropriately. \
Follow existing code conventions in the project.\
{docs_section}\
{user_section}"
"\
You are Claude, an AI coding assistant made by Anthropic. You help users with software \
engineering tasks including solving bugs, adding new functionality, refactoring code, \
explaining code, and more.
You are an interactive agent that helps users with software engineering tasks. Use the \
instructions below and the tools available to you to assist the user.
{env_block}
# Doing Tasks
- The user will primarily request you to perform software engineering tasks. These may include \
solving bugs, adding new functionality, refactoring code, explaining code, and more.
- In general, do not propose changes to code you have not read. If a user asks about or wants \
you to modify a file, read it first. Understand existing code before suggesting modifications.
- Do not create files unless they are absolutely necessary for achieving your goal. Generally \
prefer editing an existing file to creating a new one, as this prevents file bloat and builds \
on existing work more effectively.
- If your approach is blocked, do not attempt to brute force your way to the outcome. Consider \
alternative approaches or other ways you might unblock yourself.
- Avoid over-engineering. Only make changes that are directly requested or clearly necessary. \
Keep solutions simple and focused.
- Do not add features, refactor code, or make improvements beyond what was asked.
- Do not add error handling, fallbacks, or validation for scenarios that cannot happen. Trust \
internal code and framework guarantees. Only validate at system boundaries (user input, external APIs).
- Avoid backwards-compatibility hacks. If you are certain something is unused, delete it completely.
# Tools
Use the provided tools to interact with the codebase and environment. Do NOT use the shell \
tool to run commands when a relevant dedicated tool is provided:
- To read files use read_file instead of cat, head, tail, or sed.
- To edit files use edit_file instead of sed or awk.
- To create files use write_file instead of cat with heredoc or echo redirection.
- To search for files use glob instead of find or ls.
- To search the content of files use grep instead of grep or rg.
## read_file
Read files before editing them. Always read a file before attempting to edit it. Use \
offset/limit for large files. Reading a file you have not read before is always appropriate.
## edit_file
Performs exact string replacements in files. The old_string must be an exact match of \
existing text and must be unique in the file. If old_string matches multiple locations, provide \
more surrounding context to make it unique. Prefer editing existing files over creating new ones. \
When editing text, ensure you preserve the exact indentation as it appears in the file.
## write_file
Use write_file only when creating new files. Prefer edit_file for modifying existing files. \
Always prefer editing existing files in the codebase over creating new ones.
## shell
Use for running commands, tests, and builds. Default timeout is 120 seconds. Use timeout_ms \
parameter for longer-running commands.
## grep
Search file contents with regex patterns. Supports output modes: content, files_with_matches, count. \
Use this for searching the content of files rather than using shell grep or rg.
## glob
Find files by name pattern. Results sorted by modification time (newest first). Use this for \
finding files rather than using shell find or ls commands.
# Coding Best Practices
Write clean, maintainable code. Handle errors appropriately. Follow existing code conventions \
in the project. Keep changes minimal and focused on the task.\
{docs_section}\
{user_section}"
)
}
@ -131,7 +176,7 @@ impl ProviderProfile for AnthropicProfile {
fn provider_options(&self) -> Option<serde_json::Value> {
Some(serde_json::json!({
"anthropic": {
"beta_headers": ["interleaved-thinking-2025-05-14"]
"beta_headers": ["interleaved-thinking-2025-05-14", "extended-thinking-2025-04-14", "max-tokens-3-5-sonnet-2025-04-14"]
}
}))
}
@ -163,7 +208,7 @@ mod tests {
#[async_trait]
impl ExecutionEnvironment for TestEnv {
async fn read_file(&self, _: &str) -> Result<String, String> {
async fn read_file(&self, _: &str, _: Option<usize>, _: Option<usize>) -> Result<String, String> {
Ok(String::new())
}
async fn write_file(&self, _: &str, _: &str) -> Result<(), String> {
@ -172,13 +217,12 @@ mod tests {
async fn file_exists(&self, _: &str) -> Result<bool, String> {
Ok(false)
}
async fn list_directory(&self, _: &str) -> Result<Vec<DirEntry>, String> {
async fn list_directory(&self, _: &str, _: Option<usize>) -> Result<Vec<DirEntry>, String> {
Ok(vec![])
}
async fn exec_command(
&self,
_: &str,
_: &[String],
_: u64,
_: Option<&str>,
_: Option<&std::collections::HashMap<String, String>>,
@ -199,7 +243,7 @@ mod tests {
) -> Result<Vec<String>, String> {
Ok(vec![])
}
async fn glob(&self, _: &str) -> Result<Vec<String>, String> {
async fn glob(&self, _: &str, _: Option<&str>) -> Result<Vec<String>, String> {
Ok(vec![])
}
async fn initialize(&self) -> Result<(), String> {
@ -240,8 +284,8 @@ mod tests {
let profile = AnthropicProfile::new("claude-sonnet-4-20250514");
let env = TestEnv;
let prompt = profile.build_system_prompt(&env, &EnvContext::default(), &[], None);
assert!(prompt.contains("You are Claude, an AI coding assistant by Anthropic"));
assert!(prompt.contains("# Environment"));
assert!(prompt.contains("You are Claude, an AI coding assistant made by Anthropic"));
assert!(prompt.contains("<environment>"));
assert!(prompt.contains("linux"));
assert!(prompt.contains("/home/test"));
assert!(prompt.contains("# Tools"));
@ -287,12 +331,16 @@ mod tests {
is_git_repo: true,
date: "2026-02-20".into(),
model_name: "claude-opus-4-6".into(),
knowledge_cutoff: "May 2025".into(),
git_status_short: None,
git_recent_commits: None,
};
let prompt = profile.build_system_prompt(&env, &ctx, &[], None);
assert!(prompt.contains("Git branch: feature-branch"));
assert!(prompt.contains("Is a git repository: true"));
assert!(prompt.contains("Date: 2026-02-20"));
assert!(prompt.contains("Is git repository: true"));
assert!(prompt.contains("Today's date: 2026-02-20"));
assert!(prompt.contains("Model: claude-opus-4-6"));
assert!(prompt.contains("Knowledge cutoff: May 2025"));
}
#[test]
@ -336,6 +384,14 @@ mod tests {
headers.contains(&"interleaved-thinking-2025-05-14"),
"beta_headers should contain interleaved-thinking header"
);
assert!(
headers.contains(&"extended-thinking-2025-04-14"),
"beta_headers should contain extended-thinking header"
);
assert!(
headers.contains(&"max-tokens-3-5-sonnet-2025-04-14"),
"beta_headers should contain max-tokens header"
);
}
#[test]

View file

@ -4,53 +4,17 @@ use crate::subagent::{
make_close_agent_tool, make_send_input_tool, make_spawn_agent_tool, make_wait_tool,
SessionFactory, SubAgentManager,
};
use crate::tool_registry::{RegisteredTool, ToolRegistry};
use crate::tool_registry::ToolRegistry;
use crate::tools::{
make_edit_file_tool, make_glob_tool, make_grep_tool, make_read_file_tool, make_shell_tool,
make_write_file_tool,
make_edit_file_tool, make_glob_tool, make_grep_tool, make_list_dir_tool,
make_read_file_tool, make_read_many_files_tool, make_shell_tool, make_web_fetch_tool,
make_web_search_tool, make_write_file_tool,
};
use std::sync::Arc;
use unified_llm::types::ToolDefinition;
use super::{build_env_context_block_with, EnvContext};
fn make_list_dir_tool() -> RegisteredTool {
RegisteredTool {
definition: ToolDefinition {
name: "list_dir".into(),
description: "List directory contents with depth control".into(),
parameters: serde_json::json!({
"type": "object",
"properties": {
"path": {"type": "string", "description": "Directory path to list"},
"depth": {"type": "integer", "description": "Depth of listing (default 1)"}
},
"required": ["path"]
}),
},
executor: Arc::new(|args, env| {
Box::pin(async move {
let path = args["path"]
.as_str()
.ok_or_else(|| "path is required".to_string())?;
let entries = env.list_directory(path).await?;
let lines: Vec<String> = entries
.iter()
.map(|e| {
if e.is_dir {
format!("{}/", e.name)
} else {
e.name.clone()
}
})
.collect();
Ok(lines.join("\n"))
})
}),
}
}
pub struct GeminiProfile {
model: String,
registry: ToolRegistry,
@ -62,12 +26,15 @@ impl GeminiProfile {
let mut registry = ToolRegistry::new();
registry.register(make_read_file_tool());
registry.register(make_read_many_files_tool());
registry.register(make_write_file_tool());
registry.register(make_edit_file_tool());
registry.register(make_shell_tool());
registry.register(make_grep_tool());
registry.register(make_glob_tool());
registry.register(make_list_dir_tool());
registry.register(make_web_search_tool());
registry.register(make_web_fetch_tool());
Self {
model: model.into(),
@ -129,22 +96,121 @@ impl ProviderProfile for GeminiProfile {
};
format!(
"You are a coding assistant powered by Gemini. You help users with software engineering tasks \
including solving bugs, adding new functionality, refactoring code, and explaining code.\n\n\
{env_block}\n\n\
# Tools\n\
Use the provided tools to interact with the codebase and environment.\n\n\
- read_file: Read files to understand code before modifying. Use offset/limit for large files.\n\
- edit_file: Use search-and-replace editing. The old_string must exactly match existing text. Prefer editing existing files over creating new ones.\n\
- write_file: Use for creating new files or completely rewriting files.\n\
- shell: Execute shell commands. Default timeout is 10 seconds.\n\
- grep: Search file contents with regex patterns.\n\
- glob: Find files by name pattern. Results sorted by modification time.\n\
- list_dir: List directory contents with depth control.\n\n\
# Project Docs\n\
Look for GEMINI.md and AGENTS.md files in the project for project-specific instructions.\n\n\
# Coding Best Practices\n\
Write clean, maintainable code. Handle errors appropriately. Follow existing code conventions in the project.\
"\
You are Gemini CLI, an interactive CLI agent specializing in software engineering tasks \
including solving bugs, adding new functionality, refactoring code, and explaining code. \
Your primary goal is to help users safely and effectively.
# Core Mandates
## Security and System Integrity
- Never log, print, or commit secrets, API keys, or sensitive credentials. Rigorously protect \
`.env` files, `.git`, and system configuration folders.
- Do not stage or commit changes unless specifically requested by the user.
## Engineering Standards
- Instructions found in GEMINI.md and AGENTS.md files are foundational mandates. They take \
absolute precedence over the general workflows and tool defaults described in this system prompt.
- Rigorously adhere to existing workspace conventions, architectural patterns, and style. \
Analyze surrounding files, tests, and configuration to ensure your changes are seamless, \
idiomatic, and consistent with the local context.
- NEVER assume a library/framework is available. Verify its established usage within the \
project before employing it.
- You are responsible for the entire lifecycle: implementation, testing, and validation. \
A task is only complete when the behavioral correctness of the change has been verified.
- ALWAYS search for and update related tests after making a code change.
## Context Efficiency
Be strategic in your use of the available tools to minimize unnecessary context usage while \
still providing the best answer you can.
- Combine turns whenever possible by utilizing parallel searching and reading.
- Prefer using tools like `grep` to identify points of interest instead of reading lots of \
files individually.
- If you need to read multiple ranges in a file, do so in parallel.
{env_block}
# Development Lifecycle
Operate using a Research -> Strategy -> Execution lifecycle.
1. **Research:** Systematically map the codebase and validate assumptions. Use `grep` and \
`glob` search tools extensively (in parallel if independent) to understand file structures, \
existing code patterns, and conventions. Use `read_file` to validate all assumptions. \
Prioritize empirical reproduction of reported issues.
2. **Strategy:** Formulate a grounded plan based on your research.
3. **Execution:** For each sub-task:
- **Plan:** Define the specific implementation approach and the testing strategy.
- **Act:** Apply targeted, surgical changes. Use the available tools (edit_file, \
write_file, shell). Include necessary automated tests.
- **Validate:** Run tests and workspace standards to confirm success and ensure no \
regressions were introduced.
Validation is the only path to finality. Never assume success or settle for unverified changes.
# Tools
Use the provided tools to interact with the codebase and environment.
## read_file
Read files to understand code before modifying. Use offset/limit for large files. Minimize \
unnecessarily large file reads when doing so does not result in extra turns.
## read_many_files
Read multiple files at once by providing an array of paths. Useful for reading small files in \
their entirety or gathering context from multiple locations efficiently.
## edit_file
Use search-and-replace editing. The old_string must exactly match existing text and be unique \
in the file. Prefer editing existing files over creating new ones. Before making manual code \
changes, check if an ecosystem tool (like `eslint --fix`, `prettier --write`, `cargo fmt`) is \
available in the project.
## write_file
Use for creating new files or completely rewriting files.
## shell
Execute shell commands. Default timeout is 10 seconds. Use timeout_ms parameter for \
longer-running commands. Always prefer non-interactive commands (e.g., using CI flags for \
test runners to avoid persistent watch modes or `git --no-pager`).
## grep
Search file contents with regex patterns. Use conservative result counts and narrow scope \
(include/exclude parameters). Use context/before/after to request enough context to avoid \
needing to read the file before editing matches.
## glob
Find files by name pattern. Results sorted by modification time.
## list_dir
List directory contents with depth control.
## web_search
Search the web for information.
## web_fetch
Fetch content from a URL.
# Project Docs
Look for GEMINI.md and AGENTS.md files in the project for project-specific instructions. \
These are foundational mandates that take precedence over defaults in this prompt.
# Operational Guidelines
## Tone and Style
- Act as a senior software engineer and collaborative peer programmer.
- Be concise and direct. Adopt a professional tone suitable for a CLI environment.
- Use tools for actions, text output only for communication.
## Tool Usage
- Execute multiple independent tool calls in parallel when feasible.
- Use the shell tool for running commands, remembering to explain modifying commands first.
# Coding Best Practices
Write clean, maintainable code. Handle errors appropriately. Follow existing code conventions \
in the project.\
{docs_section}\
{user_section}"
)
@ -157,12 +223,10 @@ Write clean, maintainable code. Handle errors appropriately. Follow existing cod
fn provider_options(&self) -> Option<serde_json::Value> {
Some(serde_json::json!({
"gemini": {
"safety_settings": [
{
"category": "HARM_CATEGORY_DANGEROUS_CONTENT",
"threshold": "BLOCK_NONE"
}
]
"safety_settings": {
"category": "HARM_CATEGORY_DANGEROUS_CONTENT",
"threshold": "BLOCK_ONLY_HIGH"
}
}
}))
}
@ -195,7 +259,7 @@ mod tests {
#[async_trait]
impl ExecutionEnvironment for TestEnv {
async fn read_file(&self, _: &str) -> Result<String, String> {
async fn read_file(&self, _: &str, _: Option<usize>, _: Option<usize>) -> Result<String, String> {
Ok(String::new())
}
async fn write_file(&self, _: &str, _: &str) -> Result<(), String> {
@ -204,13 +268,12 @@ mod tests {
async fn file_exists(&self, _: &str) -> Result<bool, String> {
Ok(false)
}
async fn list_directory(&self, _: &str) -> Result<Vec<DirEntry>, String> {
async fn list_directory(&self, _: &str, _: Option<usize>) -> Result<Vec<DirEntry>, String> {
Ok(vec![])
}
async fn exec_command(
&self,
_: &str,
_: &[String],
_: u64,
_: Option<&str>,
_: Option<&std::collections::HashMap<String, String>>,
@ -231,7 +294,7 @@ mod tests {
) -> Result<Vec<String>, String> {
Ok(vec![])
}
async fn glob(&self, _: &str) -> Result<Vec<String>, String> {
async fn glob(&self, _: &str, _: Option<&str>) -> Result<Vec<String>, String> {
Ok(vec![])
}
async fn initialize(&self) -> Result<(), String> {
@ -272,7 +335,7 @@ mod tests {
let profile = GeminiProfile::new("gemini-2.0-flash");
let env = TestEnv;
let prompt = profile.build_system_prompt(&env, &EnvContext::default(), &[], None);
assert!(prompt.contains("You are a coding assistant powered by Gemini"));
assert!(prompt.contains("You are Gemini CLI"));
assert!(prompt.contains("solving bugs"));
assert!(prompt.contains("adding new functionality"));
assert!(prompt.contains("refactoring code"));
@ -285,12 +348,15 @@ mod tests {
let env = TestEnv;
let prompt = profile.build_system_prompt(&env, &EnvContext::default(), &[], None);
assert!(prompt.contains("read_file"));
assert!(prompt.contains("read_many_files"));
assert!(prompt.contains("edit_file"));
assert!(prompt.contains("write_file"));
assert!(prompt.contains("shell"));
assert!(prompt.contains("grep"));
assert!(prompt.contains("glob"));
assert!(prompt.contains("list_dir"));
assert!(prompt.contains("web_search"));
assert!(prompt.contains("web_fetch"));
assert!(prompt.contains("Default timeout is 10 seconds"));
}
@ -318,7 +384,7 @@ mod tests {
let profile = GeminiProfile::new("gemini-2.0-flash");
let env = TestEnv;
let prompt = profile.build_system_prompt(&env, &EnvContext::default(), &[], None);
assert!(prompt.contains("# Environment"));
assert!(prompt.contains("<environment>"));
assert!(prompt.contains("linux"));
}
@ -329,25 +395,26 @@ mod tests {
assert!(options.is_some());
let options = options.unwrap();
let safety = &options["gemini"]["safety_settings"];
assert!(safety.is_array());
let settings = safety.as_array().unwrap();
assert_eq!(settings.len(), 1);
assert_eq!(settings[0]["category"], "HARM_CATEGORY_DANGEROUS_CONTENT");
assert_eq!(settings[0]["threshold"], "BLOCK_NONE");
assert!(safety.is_object());
assert_eq!(safety["category"], "HARM_CATEGORY_DANGEROUS_CONTENT");
assert_eq!(safety["threshold"], "BLOCK_ONLY_HIGH");
}
#[test]
fn gemini_tools_registered() {
let profile = GeminiProfile::new("gemini-2.0-flash");
let names = profile.tool_registry().names();
assert_eq!(names.len(), 7);
assert_eq!(names.len(), 10);
assert!(names.contains(&"read_file".to_string()));
assert!(names.contains(&"read_many_files".to_string()));
assert!(names.contains(&"write_file".to_string()));
assert!(names.contains(&"edit_file".to_string()));
assert!(names.contains(&"shell".to_string()));
assert!(names.contains(&"grep".to_string()));
assert!(names.contains(&"glob".to_string()));
assert!(names.contains(&"list_dir".to_string()));
assert!(names.contains(&"web_search".to_string()));
assert!(names.contains(&"web_fetch".to_string()));
}
#[test]
@ -361,7 +428,7 @@ mod tests {
});
profile.register_subagent_tools(manager, factory, 0);
let names = profile.tool_registry().names();
assert_eq!(names.len(), 11);
assert_eq!(names.len(), 14);
assert!(names.contains(&"spawn_agent".to_string()));
assert!(names.contains(&"send_input".to_string()));
assert!(names.contains(&"wait".to_string()));

View file

@ -15,6 +15,9 @@ pub struct EnvContext {
pub is_git_repo: bool,
pub date: String,
pub model_name: String,
pub knowledge_cutoff: String,
pub git_status_short: Option<String>,
pub git_recent_commits: Option<String>,
}
#[must_use]
@ -25,25 +28,29 @@ pub fn build_env_context_block(env: &dyn ExecutionEnvironment) -> String {
#[must_use]
pub fn build_env_context_block_with(env: &dyn ExecutionEnvironment, ctx: &EnvContext) -> String {
let mut lines = vec![
"# Environment".to_string(),
format!("- Working directory: {}", env.working_directory()),
format!("- Platform: {}", env.platform()),
format!("- OS: {}", env.os_version()),
"<environment>".to_string(),
format!("Working directory: {}", env.working_directory()),
format!("Is git repository: {}", ctx.is_git_repo),
];
if ctx.is_git_repo {
lines.push(format!("- Is a git repository: {}", ctx.is_git_repo));
}
if let Some(ref branch) = ctx.git_branch {
lines.push(format!("- Git branch: {branch}"));
}
if !ctx.date.is_empty() {
lines.push(format!("- Date: {}", ctx.date));
}
if !ctx.model_name.is_empty() {
lines.push(format!("- Model: {}", ctx.model_name));
lines.push(format!("Git branch: {branch}"));
}
lines.push(format!("Platform: {}", env.platform()));
lines.push(format!("OS version: {}", env.os_version()));
if !ctx.date.is_empty() {
lines.push(format!("Today's date: {}", ctx.date));
}
if !ctx.model_name.is_empty() {
lines.push(format!("Model: {}", ctx.model_name));
}
if !ctx.knowledge_cutoff.is_empty() {
lines.push(format!("Knowledge cutoff: {}", ctx.knowledge_cutoff));
}
lines.push("</environment>".to_string());
lines.join("\n")
}
@ -57,7 +64,7 @@ mod tests {
#[async_trait]
impl ExecutionEnvironment for TestEnv {
async fn read_file(&self, _: &str) -> Result<String, String> {
async fn read_file(&self, _: &str, _: Option<usize>, _: Option<usize>) -> Result<String, String> {
Ok(String::new())
}
async fn write_file(&self, _: &str, _: &str) -> Result<(), String> {
@ -66,13 +73,12 @@ mod tests {
async fn file_exists(&self, _: &str) -> Result<bool, String> {
Ok(false)
}
async fn list_directory(&self, _: &str) -> Result<Vec<DirEntry>, String> {
async fn list_directory(&self, _: &str, _: Option<usize>) -> Result<Vec<DirEntry>, String> {
Ok(vec![])
}
async fn exec_command(
&self,
_: &str,
_: &[String],
_: u64,
_: Option<&str>,
_: Option<&std::collections::HashMap<String, String>>,
@ -93,7 +99,7 @@ mod tests {
) -> Result<Vec<String>, String> {
Ok(vec![])
}
async fn glob(&self, _: &str) -> Result<Vec<String>, String> {
async fn glob(&self, _: &str, _: Option<&str>) -> Result<Vec<String>, String> {
Ok(vec![])
}
async fn initialize(&self) -> Result<(), String> {
@ -117,7 +123,8 @@ mod tests {
fn env_context_block_contains_platform() {
let env = TestEnv;
let block = build_env_context_block(&env);
assert!(block.contains("# Environment"));
assert!(block.contains("<environment>"));
assert!(block.contains("</environment>"));
assert!(block.contains("linux"));
assert!(block.contains("/home/test"));
assert!(block.contains("Linux 6.1.0"));
@ -131,11 +138,15 @@ mod tests {
is_git_repo: true,
date: "2026-02-20".into(),
model_name: "claude-opus-4-6".into(),
knowledge_cutoff: "May 2025".into(),
git_status_short: None,
git_recent_commits: None,
};
let block = build_env_context_block_with(&env, &ctx);
assert!(block.contains("Git branch: main"));
assert!(block.contains("Is a git repository: true"));
assert!(block.contains("Date: 2026-02-20"));
assert!(block.contains("Is git repository: true"));
assert!(block.contains("Today's date: 2026-02-20"));
assert!(block.contains("Model: claude-opus-4-6"));
assert!(block.contains("Knowledge cutoff: May 2025"));
}
}

View file

@ -96,25 +96,87 @@ impl ProviderProfile for OpenAiProfile {
};
format!(
"You are a coding assistant powered by OpenAI. You help users with software engineering tasks \
including solving bugs, adding new functionality, refactoring code, and explaining code.\n\n\
{env_block}\n\n\
# Tools\n\
Use the provided tools to interact with the codebase and environment.\n\n\
- read_file: Read files to understand code before modifying. Use offset/limit for large files.\n\
- apply_patch: Use the v4a patch format for all file modifications. The format uses \
`*** Begin Patch` / `*** End Patch` delimiters with `*** Add File:`, `*** Delete File:`, \
`*** Update File:` operations. Update operations use `@@` context hints and +/- prefixes \
for changes. Show 3 lines of context around each change.\n\
- write_file: Use for creating new files. For modifications, prefer apply_patch.\n\
- shell: Execute shell commands. Default timeout is 10 seconds. Use timeout_ms parameter \
for longer-running commands.\n\
- grep: Search file contents with regex. Use glob_filter to narrow results.\n\
- glob: Find files by name pattern.\n\n\
# Coding Best Practices\n\
Write clean, maintainable code. Handle errors appropriately. Follow existing code conventions in the project.\
{docs_section}\
{user_section}"
"\
You are a coding agent powered by OpenAI, running in a terminal-based agentic coding assistant. \
You are expected to be precise, safe, and helpful.
You can receive user prompts and context such as files in the workspace, communicate with the \
user by streaming thinking and responses, and emit function calls to run terminal commands and \
apply patches.
# Personality
Be concise, direct, and friendly. Communicate efficiently, keeping the user clearly informed \
about ongoing actions without unnecessary detail. Prioritize actionable guidance, clearly \
stating assumptions, environment prerequisites, and next steps.
{env_block}
# AGENTS.md
Repos may contain AGENTS.md files with instructions for the agent. These files can appear \
anywhere in the repository. Instructions in AGENTS.md files whose scope includes a file you \
touch must be obeyed. More-deeply-nested AGENTS.md files take precedence in case of conflict. \
Direct system/developer/user instructions take precedence over AGENTS.md instructions.
# Task Execution
Keep going until the task is completely resolved before ending your turn. Autonomously resolve \
the query to the best of your ability using the tools available. Do NOT guess or make up an answer.
Working on repos in the current environment is allowed, even if they are proprietary.
If completing the task requires writing or modifying files:
- Fix the problem at the root cause rather than applying surface-level patches, when possible.
- Avoid unneeded complexity in your solution.
- Do not attempt to fix unrelated bugs or broken tests.
- Keep changes consistent with the style of the existing codebase. Changes should be minimal \
and focused on the task.
- Use `git log` and `git blame` to search the history of the codebase if additional context is needed.
- NEVER add copyright or license headers unless specifically requested.
- Do not waste tokens re-reading files after calling apply_patch on them. The tool call will \
fail if it did not work.
- Do not `git commit` your changes or create new git branches unless explicitly requested.
# Validating Your Work
If the codebase has tests or the ability to build or run, consider using them to verify your \
work. Start as specific as possible to the code you changed to catch issues efficiently, then \
make your way to broader tests as you build confidence.
# Tools
Use the provided tools to interact with the codebase and environment.
## read_file
Read files to understand code before modifying. Use offset/limit for large files.
## apply_patch
Use the v4a patch format for all file modifications. The format uses `*** Begin Patch` / \
`*** End Patch` delimiters with `*** Add File:`, `*** Delete File:`, `*** Update File:` \
operations. Update operations use `@@` context hints and +/- prefixes for changes. Show 3 \
lines of context around each change. NEVER use `applypatch` or `apply-patch`, only `apply_patch`.
## write_file
Use for creating new files. For modifications, prefer apply_patch.
## shell
Execute shell commands. Default timeout is 10 seconds. Use timeout_ms parameter for \
longer-running commands. When searching for text or files, prefer `rg` (ripgrep) because \
it is much faster than alternatives like `grep`.
## grep
Search file contents with regex. Use glob_filter to narrow results.
## glob
Find files by name pattern.
# Coding Best Practices
Write clean, maintainable code. Handle errors appropriately. Follow existing code conventions \
in the project.\
{docs_section}\
{user_section}"
)
}
@ -295,7 +357,7 @@ pub async fn apply_patch_operations(
results.push(format!("Deleted file: {path}"));
}
PatchOperation::Update { path, hunks } => {
let original = env.read_file(path).await?;
let original = env.read_file(path, None, None).await?;
let updated = apply_hunks(&original, hunks)?;
env.write_file(path, &updated).await?;
results.push(format!("Updated file: {path}"));
@ -403,7 +465,7 @@ mod tests {
#[async_trait]
impl ExecutionEnvironment for TestEnv {
async fn read_file(&self, _: &str) -> Result<String, String> {
async fn read_file(&self, _: &str, _: Option<usize>, _: Option<usize>) -> Result<String, String> {
Ok(String::new())
}
async fn write_file(&self, _: &str, _: &str) -> Result<(), String> {
@ -412,13 +474,12 @@ mod tests {
async fn file_exists(&self, _: &str) -> Result<bool, String> {
Ok(false)
}
async fn list_directory(&self, _: &str) -> Result<Vec<DirEntry>, String> {
async fn list_directory(&self, _: &str, _: Option<usize>) -> Result<Vec<DirEntry>, String> {
Ok(vec![])
}
async fn exec_command(
&self,
_: &str,
_: &[String],
_: u64,
_: Option<&str>,
_: Option<&std::collections::HashMap<String, String>>,
@ -439,7 +500,7 @@ mod tests {
) -> Result<Vec<String>, String> {
Ok(vec![])
}
async fn glob(&self, _: &str) -> Result<Vec<String>, String> {
async fn glob(&self, _: &str, _: Option<&str>) -> Result<Vec<String>, String> {
Ok(vec![])
}
async fn initialize(&self) -> Result<(), String> {
@ -473,7 +534,7 @@ mod tests {
#[async_trait]
impl ExecutionEnvironment for MockFileEnv {
async fn read_file(&self, path: &str) -> Result<String, String> {
async fn read_file(&self, path: &str, _: Option<usize>, _: Option<usize>) -> Result<String, String> {
self.files
.lock()
.unwrap()
@ -491,13 +552,12 @@ mod tests {
async fn file_exists(&self, path: &str) -> Result<bool, String> {
Ok(self.files.lock().unwrap().contains_key(path))
}
async fn list_directory(&self, _: &str) -> Result<Vec<DirEntry>, String> {
async fn list_directory(&self, _: &str, _: Option<usize>) -> Result<Vec<DirEntry>, String> {
Ok(vec![])
}
async fn exec_command(
&self,
_: &str,
_: &[String],
_: u64,
_: Option<&str>,
_: Option<&std::collections::HashMap<String, String>>,
@ -518,7 +578,7 @@ mod tests {
) -> Result<Vec<String>, String> {
Ok(vec![])
}
async fn glob(&self, _: &str) -> Result<Vec<String>, String> {
async fn glob(&self, _: &str, _: Option<&str>) -> Result<Vec<String>, String> {
Ok(vec![])
}
async fn initialize(&self) -> Result<(), String> {
@ -559,8 +619,8 @@ mod tests {
let profile = OpenAiProfile::new("o3-mini");
let env = TestEnv;
let prompt = profile.build_system_prompt(&env, &EnvContext::default(), &[], None);
assert!(prompt.contains("You are a coding assistant powered by OpenAI"));
assert!(prompt.contains("# Environment"));
assert!(prompt.contains("You are a coding agent powered by OpenAI"));
assert!(prompt.contains("<environment>"));
assert!(prompt.contains("linux"));
assert!(prompt.contains("v4a patch format"));
assert!(prompt.contains("*** Begin Patch"));
@ -769,7 +829,7 @@ mod tests {
let result = apply_patch_operations(&ops, &env).await.unwrap();
assert!(result.contains("Added file: src/new.rs"));
let content = env.read_file("src/new.rs").await.unwrap();
let content = env.read_file("src/new.rs", None, None).await.unwrap();
assert_eq!(content, "fn new() {}");
}
@ -796,7 +856,7 @@ mod tests {
let result = apply_patch_operations(&ops, &env).await.unwrap();
assert!(result.contains("Updated file: src/lib.rs"));
let content = env.read_file("src/lib.rs").await.unwrap();
let content = env.read_file("src/lib.rs", None, None).await.unwrap();
assert!(content.contains("println!(\"new\")"));
assert!(!content.contains("println!(\"old\")"));
}

View file

@ -23,7 +23,7 @@ pub async fn discover_project_docs(
for dir in &directories {
for filename in &candidate_filenames {
let path = format!("{dir}/{filename}");
if let Ok(content) = env.read_file(&path).await {
if let Ok(content) = env.read_file(&path, None, None).await {
if content.is_empty() {
continue;
}
@ -97,7 +97,7 @@ mod tests {
#[async_trait]
impl ExecutionEnvironment for DocEnv {
async fn read_file(&self, path: &str) -> Result<String, String> {
async fn read_file(&self, path: &str, _: Option<usize>, _: Option<usize>) -> Result<String, String> {
self.files
.get(path)
.cloned()
@ -109,10 +109,10 @@ mod tests {
async fn file_exists(&self, path: &str) -> Result<bool, String> {
Ok(self.files.contains_key(path))
}
async fn list_directory(&self, _: &str) -> Result<Vec<DirEntry>, String> {
async fn list_directory(&self, _: &str, _: Option<usize>) -> Result<Vec<DirEntry>, String> {
Ok(vec![])
}
async fn exec_command(&self, _: &str, _: &[String], _: u64, _: Option<&str>, _: Option<&std::collections::HashMap<String, String>>) -> Result<ExecResult, String> {
async fn exec_command(&self, _: &str, _: u64, _: Option<&str>, _: Option<&std::collections::HashMap<String, String>>) -> Result<ExecResult, String> {
Ok(ExecResult {
stdout: String::new(),
stderr: String::new(),
@ -124,7 +124,7 @@ mod tests {
async fn grep(&self, _: &str, _: &str, _: &GrepOptions) -> Result<Vec<String>, String> {
Ok(vec![])
}
async fn glob(&self, _: &str) -> Result<Vec<String>, String> {
async fn glob(&self, _: &str, _: Option<&str>) -> Result<Vec<String>, String> {
Ok(vec![])
}
async fn initialize(&self) -> Result<(), String> {

View file

@ -33,7 +33,7 @@ mod tests {
#[async_trait]
impl ExecutionEnvironment for TestEnv {
async fn read_file(&self, _: &str) -> Result<String, String> {
async fn read_file(&self, _: &str, _: Option<usize>, _: Option<usize>) -> Result<String, String> {
Ok(String::new())
}
async fn write_file(&self, _: &str, _: &str) -> Result<(), String> {
@ -42,13 +42,12 @@ mod tests {
async fn file_exists(&self, _: &str) -> Result<bool, String> {
Ok(false)
}
async fn list_directory(&self, _: &str) -> Result<Vec<DirEntry>, String> {
async fn list_directory(&self, _: &str, _: Option<usize>) -> Result<Vec<DirEntry>, String> {
Ok(vec![])
}
async fn exec_command(
&self,
_: &str,
_: &[String],
_: u64,
_: Option<&str>,
_: Option<&std::collections::HashMap<String, String>>,
@ -69,7 +68,7 @@ mod tests {
) -> Result<Vec<String>, String> {
Ok(vec![])
}
async fn glob(&self, _: &str) -> Result<Vec<String>, String> {
async fn glob(&self, _: &str, _: Option<&str>) -> Result<Vec<String>, String> {
Ok(vec![])
}
async fn initialize(&self) -> Result<(), String> {

View file

@ -62,15 +62,18 @@ impl Session {
/// Initialize session by discovering project docs and capturing environment context.
/// Call before `process_input`.
pub async fn initialize(&mut self) {
if let Some(ref git_root) = self.config.git_root {
self.project_docs = discover_project_docs(
self.execution_env.as_ref(),
git_root,
self.execution_env.working_directory(),
&self.provider_profile.id(),
)
.await;
}
let doc_root = self
.config
.git_root
.clone()
.unwrap_or_else(|| self.execution_env.working_directory().to_string());
self.project_docs = discover_project_docs(
self.execution_env.as_ref(),
&doc_root,
self.execution_env.working_directory(),
&self.provider_profile.id(),
)
.await;
// Populate environment context
self.env_context = self.build_env_context().await;
@ -83,7 +86,7 @@ impl Session {
// Detect git info via execution environment
let git_branch = self
.execution_env
.exec_command("git", &["rev-parse".into(), "--abbrev-ref".into(), "HEAD".into()], 5000, None, None)
.exec_command("git rev-parse --abbrev-ref HEAD", 5000, None, None)
.await
.ok()
.filter(|r| r.exit_code == 0)
@ -91,11 +94,38 @@ impl Session {
let is_git_repo = git_branch.is_some();
let git_status_short = if is_git_repo {
self.execution_env
.exec_command("git status --short", 5000, None, None)
.await
.ok()
.filter(|r| r.exit_code == 0)
.map(|r| r.stdout.trim().to_string())
.filter(|s| !s.is_empty())
} else {
None
};
let git_recent_commits = if is_git_repo {
self.execution_env
.exec_command("git log --oneline -10", 5000, None, None)
.await
.ok()
.filter(|r| r.exit_code == 0)
.map(|r| r.stdout.trim().to_string())
.filter(|s| !s.is_empty())
} else {
None
};
EnvContext {
git_branch,
is_git_repo,
date: today,
model_name,
knowledge_cutoff: String::new(),
git_status_short,
git_recent_commits,
}
}
@ -141,6 +171,10 @@ impl Session {
self.config.reasoning_effort = effort;
}
pub fn set_max_turns(&mut self, max_turns: usize) {
self.config.max_turns = max_turns;
}
pub fn history(&self) -> &History {
&self.history
}
@ -233,12 +267,24 @@ impl Session {
// Check abort flag
if self.abort_flag.load(Ordering::SeqCst) {
self.state = SessionState::Closed;
self.event_emitter.emit(
EventKind::SessionEnd,
self.id.clone(),
HashMap::new(),
);
return Err(AgentError::Aborted);
}
// Build request
let request = self.build_request();
// Emit AssistantTextStart before LLM call
self.event_emitter.emit(
EventKind::AssistantTextStart,
self.id.clone(),
HashMap::new(),
);
// Call LLM
let response = match self.llm_client.complete(&request).await {
Ok(resp) => resp,
@ -818,7 +864,7 @@ mod tests {
#[async_trait]
impl ExecutionEnvironment for MemoryExecutionEnvironment {
async fn read_file(&self, path: &str) -> Result<String, String> {
async fn read_file(&self, path: &str, _offset: Option<usize>, _limit: Option<usize>) -> Result<String, String> {
self.files
.get(path)
.cloned()
@ -833,14 +879,13 @@ mod tests {
Ok(self.files.contains_key(path))
}
async fn list_directory(&self, _path: &str) -> Result<Vec<DirEntry>, String> {
async fn list_directory(&self, _path: &str, _depth: Option<usize>) -> Result<Vec<DirEntry>, String> {
Ok(vec![])
}
async fn exec_command(
&self,
_command: &str,
_args: &[String],
_timeout_ms: u64,
_working_dir: Option<&str>,
_env_vars: Option<&std::collections::HashMap<String, String>>,
@ -863,7 +908,7 @@ mod tests {
Ok(vec![])
}
async fn glob(&self, _pattern: &str) -> Result<Vec<String>, String> {
async fn glob(&self, _pattern: &str, _path: Option<&str>) -> Result<Vec<String>, String> {
Ok(vec![])
}

View file

@ -191,7 +191,18 @@ pub fn make_spawn_agent_tool(
.and_then(|v| v.as_str())
.ok_or_else(|| "Missing required parameter: task".to_string())?;
let session = session_factory();
// Extract optional max_turns parameter
#[allow(clippy::cast_possible_truncation)]
let max_turns = args
.get("max_turns")
.and_then(|v| v.as_u64())
.map(|v| v as usize);
// Note: working_dir and model require session factory changes to wire through
let mut session = session_factory();
if let Some(turns) = max_turns {
session.set_max_turns(turns);
}
let mut mgr = manager.lock().await;
mgr.spawn(session, task.to_string(), current_depth)
})
@ -376,7 +387,7 @@ mod tests {
#[async_trait]
impl ExecutionEnvironment for MemoryExecutionEnvironment {
async fn read_file(&self, _path: &str) -> Result<String, String> {
async fn read_file(&self, _path: &str, _offset: Option<usize>, _limit: Option<usize>) -> Result<String, String> {
Ok(String::new())
}
async fn write_file(&self, _path: &str, _content: &str) -> Result<(), String> {
@ -385,13 +396,12 @@ mod tests {
async fn file_exists(&self, _path: &str) -> Result<bool, String> {
Ok(false)
}
async fn list_directory(&self, _path: &str) -> Result<Vec<DirEntry>, String> {
async fn list_directory(&self, _path: &str, _depth: Option<usize>) -> Result<Vec<DirEntry>, String> {
Ok(vec![])
}
async fn exec_command(
&self,
_command: &str,
_args: &[String],
_timeout_ms: u64,
_working_dir: Option<&str>,
_env_vars: Option<&std::collections::HashMap<String, String>>,
@ -412,7 +422,7 @@ mod tests {
) -> Result<Vec<String>, String> {
Ok(vec![])
}
async fn glob(&self, _pattern: &str) -> Result<Vec<String>, String> {
async fn glob(&self, _pattern: &str, _path: Option<&str>) -> Result<Vec<String>, String> {
Ok(vec![])
}
async fn initialize(&self) -> Result<(), String> {

View file

@ -170,7 +170,7 @@ mod tests {
#[async_trait]
impl ExecutionEnvironment for DummyEnv {
async fn read_file(&self, _: &str) -> Result<String, String> {
async fn read_file(&self, _: &str, _: Option<usize>, _: Option<usize>) -> Result<String, String> {
Ok(String::new())
}
async fn write_file(&self, _: &str, _: &str) -> Result<(), String> {
@ -179,13 +179,12 @@ mod tests {
async fn file_exists(&self, _: &str) -> Result<bool, String> {
Ok(false)
}
async fn list_directory(&self, _: &str) -> Result<Vec<DirEntry>, String> {
async fn list_directory(&self, _: &str, _: Option<usize>) -> Result<Vec<DirEntry>, String> {
Ok(vec![])
}
async fn exec_command(
&self,
_: &str,
_: &[String],
_: u64,
_: Option<&str>,
_: Option<&std::collections::HashMap<String, String>>,
@ -206,7 +205,7 @@ mod tests {
) -> Result<Vec<String>, String> {
Ok(vec![])
}
async fn glob(&self, _: &str) -> Result<Vec<String>, String> {
async fn glob(&self, _: &str, _: Option<&str>) -> Result<Vec<String>, String> {
Ok(vec![])
}
async fn initialize(&self) -> Result<(), String> {

View file

@ -29,22 +29,13 @@ pub fn make_read_file_tool() -> RegisteredTool {
let offset = args.get("offset").and_then(serde_json::Value::as_u64);
let limit = args.get("limit").and_then(serde_json::Value::as_u64);
let content = env.read_file(file_path).await?;
// Default limit of 2000 lines when no limit param provided
let effective_limit = limit.unwrap_or(2000);
#[allow(clippy::cast_possible_truncation)]
let offset_val = offset.unwrap_or(1) as usize;
let lines: Vec<&str> = content.lines().collect();
let start = if offset_val > 0 { offset_val - 1 } else { 0 };
let offset_usize = offset.map(|v| v as usize);
#[allow(clippy::cast_possible_truncation)]
let selected: Vec<&str> = lines
.into_iter()
.skip(start)
.take(effective_limit as usize)
.collect();
Ok(selected.join("\n"))
let limit_usize = limit.map(|v| v as usize);
let content = env.read_file(file_path, offset_usize, limit_usize).await?;
Ok(content)
})
}),
}
@ -114,7 +105,7 @@ pub fn make_edit_file_tool() -> RegisteredTool {
.and_then(serde_json::Value::as_bool)
.unwrap_or(false);
let numbered_content = env.read_file(file_path).await?;
let numbered_content = env.read_file(file_path, None, None).await?;
// Strip line numbers: each line looks like " 1 | content" or " 10 | content"
let raw_lines: Vec<&str> = numbered_content
@ -184,13 +175,7 @@ pub fn make_shell_tool_with_config(config: &SessionConfig) -> RegisteredTool {
.min(max_timeout);
let result = env
.exec_command(
"/bin/bash",
&["-c".into(), command.into()],
timeout_ms,
None,
None,
)
.exec_command(command, timeout_ms, None, None)
.await?;
let mut output = String::new();
@ -283,18 +268,145 @@ pub fn make_glob_tool() -> RegisteredTool {
.get("path")
.and_then(serde_json::Value::as_str);
let full_pattern = match path {
Some(dir) => format!("{dir}/{pattern}"),
None => pattern.to_string(),
};
let results = env.glob(&full_pattern).await?;
let results = env.glob(pattern, path).await?;
Ok(results.join("\n"))
})
}),
}
}
#[must_use]
pub fn make_read_many_files_tool() -> RegisteredTool {
RegisteredTool {
definition: ToolDefinition {
name: "read_many_files".into(),
description: "Read multiple files at once".into(),
parameters: serde_json::json!({
"type": "object",
"properties": {
"paths": {
"type": "array",
"items": {"type": "string"},
"description": "Array of absolute file paths to read"
}
},
"required": ["paths"]
}),
},
executor: Arc::new(|args, env| {
Box::pin(async move {
let paths = args["paths"]
.as_array()
.ok_or_else(|| "paths must be an array".to_string())?;
let mut output = String::new();
for path_val in paths {
let path = path_val
.as_str()
.ok_or_else(|| "each path must be a string".to_string())?;
match env.read_file(path, None, None).await {
Ok(content) => {
let _ = write!(output, "=== {path} ===\n{content}\n\n");
}
Err(err) => {
let _ = write!(output, "=== {path} ===\nError: {err}\n\n");
}
}
}
Ok(output)
})
}),
}
}
#[must_use]
pub fn make_list_dir_tool() -> RegisteredTool {
RegisteredTool {
definition: ToolDefinition {
name: "list_dir".into(),
description: "List directory contents with depth control".into(),
parameters: serde_json::json!({
"type": "object",
"properties": {
"path": {"type": "string", "description": "Directory path to list"},
"depth": {"type": "integer", "description": "Depth of listing (default 1)"}
},
"required": ["path"]
}),
},
executor: Arc::new(|args, env| {
Box::pin(async move {
let path = args["path"]
.as_str()
.ok_or_else(|| "path is required".to_string())?;
#[allow(clippy::cast_possible_truncation)]
let depth = args
.get("depth")
.and_then(serde_json::Value::as_u64)
.map(|v| v as usize);
let entries = env.list_directory(path, depth).await?;
let lines: Vec<String> = entries
.iter()
.map(|e| {
if e.is_dir {
format!("{}/", e.name)
} else {
e.name.clone()
}
})
.collect();
Ok(lines.join("\n"))
})
}),
}
}
#[must_use]
pub fn make_web_search_tool() -> RegisteredTool {
RegisteredTool {
definition: ToolDefinition {
name: "web_search".into(),
description: "Search the web".into(),
parameters: serde_json::json!({
"type": "object",
"properties": {
"query": {"type": "string", "description": "Search query"},
"max_results": {"type": "integer", "description": "Maximum number of results"}
},
"required": ["query"]
}),
},
executor: Arc::new(|_args, _env| {
Box::pin(async move {
Ok("Web search is not configured. This is a placeholder tool.".to_string())
})
}),
}
}
#[must_use]
pub fn make_web_fetch_tool() -> RegisteredTool {
RegisteredTool {
definition: ToolDefinition {
name: "web_fetch".into(),
description: "Fetch content from a URL".into(),
parameters: serde_json::json!({
"type": "object",
"properties": {
"url": {"type": "string", "description": "URL to fetch"}
},
"required": ["url"]
}),
},
executor: Arc::new(|_args, _env| {
Box::pin(async move {
Ok("Web fetch is not configured. This is a placeholder tool.".to_string())
})
}),
}
}
#[cfg(test)]
mod tests {
use super::*;
@ -308,8 +420,12 @@ mod tests {
#[async_trait]
impl ExecutionEnvironment for ReadFileEnv {
async fn read_file(&self, _path: &str) -> Result<String, String> {
Ok(self.content.clone())
async fn read_file(&self, _path: &str, offset: Option<usize>, limit: Option<usize>) -> Result<String, String> {
let lines: Vec<&str> = self.content.lines().collect();
let start = offset.unwrap_or(1).saturating_sub(1);
let count = limit.unwrap_or(2000);
let selected: Vec<&str> = lines.into_iter().skip(start).take(count).collect();
Ok(selected.join("\n"))
}
async fn write_file(&self, _: &str, _: &str) -> Result<(), String> {
Ok(())
@ -317,10 +433,10 @@ mod tests {
async fn file_exists(&self, _: &str) -> Result<bool, String> {
Ok(false)
}
async fn list_directory(&self, _: &str) -> Result<Vec<DirEntry>, String> {
async fn list_directory(&self, _: &str, _depth: Option<usize>) -> Result<Vec<DirEntry>, String> {
Ok(vec![])
}
async fn exec_command(&self, _: &str, _: &[String], _: u64, _: Option<&str>, _: Option<&std::collections::HashMap<String, String>>) -> Result<ExecResult, String> {
async fn exec_command(&self, _: &str, _: u64, _: Option<&str>, _: Option<&std::collections::HashMap<String, String>>) -> Result<ExecResult, String> {
Ok(ExecResult {
stdout: String::new(),
stderr: String::new(),
@ -332,7 +448,7 @@ mod tests {
async fn grep(&self, _: &str, _: &str, _: &GrepOptions) -> Result<Vec<String>, String> {
Ok(vec![])
}
async fn glob(&self, _: &str) -> Result<Vec<String>, String> {
async fn glob(&self, _: &str, _path: Option<&str>) -> Result<Vec<String>, String> {
Ok(vec![])
}
async fn initialize(&self) -> Result<(), String> {
@ -358,7 +474,7 @@ mod tests {
#[async_trait]
impl ExecutionEnvironment for WriteFileEnv {
async fn read_file(&self, _path: &str) -> Result<String, String> {
async fn read_file(&self, _path: &str, _offset: Option<usize>, _limit: Option<usize>) -> Result<String, String> {
Ok(String::new())
}
async fn write_file(&self, path: &str, content: &str) -> Result<(), String> {
@ -368,10 +484,10 @@ mod tests {
async fn file_exists(&self, _: &str) -> Result<bool, String> {
Ok(false)
}
async fn list_directory(&self, _: &str) -> Result<Vec<DirEntry>, String> {
async fn list_directory(&self, _: &str, _depth: Option<usize>) -> Result<Vec<DirEntry>, String> {
Ok(vec![])
}
async fn exec_command(&self, _: &str, _: &[String], _: u64, _: Option<&str>, _: Option<&std::collections::HashMap<String, String>>) -> Result<ExecResult, String> {
async fn exec_command(&self, _: &str, _: u64, _: Option<&str>, _: Option<&std::collections::HashMap<String, String>>) -> Result<ExecResult, String> {
Ok(ExecResult {
stdout: String::new(),
stderr: String::new(),
@ -383,7 +499,7 @@ mod tests {
async fn grep(&self, _: &str, _: &str, _: &GrepOptions) -> Result<Vec<String>, String> {
Ok(vec![])
}
async fn glob(&self, _: &str) -> Result<Vec<String>, String> {
async fn glob(&self, _: &str, _path: Option<&str>) -> Result<Vec<String>, String> {
Ok(vec![])
}
async fn initialize(&self) -> Result<(), String> {
@ -410,7 +526,7 @@ mod tests {
#[async_trait]
impl ExecutionEnvironment for EditFileEnv {
async fn read_file(&self, _path: &str) -> Result<String, String> {
async fn read_file(&self, _path: &str, _offset: Option<usize>, _limit: Option<usize>) -> Result<String, String> {
Ok(self.content.clone())
}
async fn write_file(&self, _path: &str, content: &str) -> Result<(), String> {
@ -420,10 +536,10 @@ mod tests {
async fn file_exists(&self, _: &str) -> Result<bool, String> {
Ok(false)
}
async fn list_directory(&self, _: &str) -> Result<Vec<DirEntry>, String> {
async fn list_directory(&self, _: &str, _depth: Option<usize>) -> Result<Vec<DirEntry>, String> {
Ok(vec![])
}
async fn exec_command(&self, _: &str, _: &[String], _: u64, _: Option<&str>, _: Option<&std::collections::HashMap<String, String>>) -> Result<ExecResult, String> {
async fn exec_command(&self, _: &str, _: u64, _: Option<&str>, _: Option<&std::collections::HashMap<String, String>>) -> Result<ExecResult, String> {
Ok(ExecResult {
stdout: String::new(),
stderr: String::new(),
@ -435,7 +551,7 @@ mod tests {
async fn grep(&self, _: &str, _: &str, _: &GrepOptions) -> Result<Vec<String>, String> {
Ok(vec![])
}
async fn glob(&self, _: &str) -> Result<Vec<String>, String> {
async fn glob(&self, _: &str, _path: Option<&str>) -> Result<Vec<String>, String> {
Ok(vec![])
}
async fn initialize(&self) -> Result<(), String> {
@ -461,7 +577,7 @@ mod tests {
#[async_trait]
impl ExecutionEnvironment for ShellEnv {
async fn read_file(&self, _: &str) -> Result<String, String> {
async fn read_file(&self, _: &str, _offset: Option<usize>, _limit: Option<usize>) -> Result<String, String> {
Ok(String::new())
}
async fn write_file(&self, _: &str, _: &str) -> Result<(), String> {
@ -470,16 +586,16 @@ mod tests {
async fn file_exists(&self, _: &str) -> Result<bool, String> {
Ok(false)
}
async fn list_directory(&self, _: &str) -> Result<Vec<DirEntry>, String> {
async fn list_directory(&self, _: &str, _depth: Option<usize>) -> Result<Vec<DirEntry>, String> {
Ok(vec![])
}
async fn exec_command(&self, _: &str, _: &[String], _: u64, _: Option<&str>, _: Option<&std::collections::HashMap<String, String>>) -> Result<ExecResult, String> {
async fn exec_command(&self, _: &str, _: u64, _: Option<&str>, _: Option<&std::collections::HashMap<String, String>>) -> Result<ExecResult, String> {
Ok(self.result.clone())
}
async fn grep(&self, _: &str, _: &str, _: &GrepOptions) -> Result<Vec<String>, String> {
Ok(vec![])
}
async fn glob(&self, _: &str) -> Result<Vec<String>, String> {
async fn glob(&self, _: &str, _path: Option<&str>) -> Result<Vec<String>, String> {
Ok(vec![])
}
async fn initialize(&self) -> Result<(), String> {
@ -505,7 +621,7 @@ mod tests {
#[async_trait]
impl ExecutionEnvironment for ShellCapturingEnv {
async fn read_file(&self, _: &str) -> Result<String, String> {
async fn read_file(&self, _: &str, _offset: Option<usize>, _limit: Option<usize>) -> Result<String, String> {
Ok(String::new())
}
async fn write_file(&self, _: &str, _: &str) -> Result<(), String> {
@ -514,13 +630,12 @@ mod tests {
async fn file_exists(&self, _: &str) -> Result<bool, String> {
Ok(false)
}
async fn list_directory(&self, _: &str) -> Result<Vec<DirEntry>, String> {
async fn list_directory(&self, _: &str, _depth: Option<usize>) -> Result<Vec<DirEntry>, String> {
Ok(vec![])
}
async fn exec_command(
&self,
_: &str,
_: &[String],
timeout_ms: u64,
_: Option<&str>,
_: Option<&std::collections::HashMap<String, String>>,
@ -537,7 +652,7 @@ mod tests {
async fn grep(&self, _: &str, _: &str, _: &GrepOptions) -> Result<Vec<String>, String> {
Ok(vec![])
}
async fn glob(&self, _: &str) -> Result<Vec<String>, String> {
async fn glob(&self, _: &str, _path: Option<&str>) -> Result<Vec<String>, String> {
Ok(vec![])
}
async fn initialize(&self) -> Result<(), String> {
@ -563,7 +678,7 @@ mod tests {
#[async_trait]
impl ExecutionEnvironment for GrepEnv {
async fn read_file(&self, _: &str) -> Result<String, String> {
async fn read_file(&self, _: &str, _offset: Option<usize>, _limit: Option<usize>) -> Result<String, String> {
Ok(String::new())
}
async fn write_file(&self, _: &str, _: &str) -> Result<(), String> {
@ -572,10 +687,10 @@ mod tests {
async fn file_exists(&self, _: &str) -> Result<bool, String> {
Ok(false)
}
async fn list_directory(&self, _: &str) -> Result<Vec<DirEntry>, String> {
async fn list_directory(&self, _: &str, _depth: Option<usize>) -> Result<Vec<DirEntry>, String> {
Ok(vec![])
}
async fn exec_command(&self, _: &str, _: &[String], _: u64, _: Option<&str>, _: Option<&std::collections::HashMap<String, String>>) -> Result<ExecResult, String> {
async fn exec_command(&self, _: &str, _: u64, _: Option<&str>, _: Option<&std::collections::HashMap<String, String>>) -> Result<ExecResult, String> {
Ok(ExecResult {
stdout: String::new(),
stderr: String::new(),
@ -587,7 +702,7 @@ mod tests {
async fn grep(&self, _: &str, _: &str, _: &GrepOptions) -> Result<Vec<String>, String> {
Ok(self.results.clone())
}
async fn glob(&self, _: &str) -> Result<Vec<String>, String> {
async fn glob(&self, _: &str, _path: Option<&str>) -> Result<Vec<String>, String> {
Ok(vec![])
}
async fn initialize(&self) -> Result<(), String> {
@ -613,7 +728,7 @@ mod tests {
#[async_trait]
impl ExecutionEnvironment for GlobEnv {
async fn read_file(&self, _: &str) -> Result<String, String> {
async fn read_file(&self, _: &str, _offset: Option<usize>, _limit: Option<usize>) -> Result<String, String> {
Ok(String::new())
}
async fn write_file(&self, _: &str, _: &str) -> Result<(), String> {
@ -622,10 +737,10 @@ mod tests {
async fn file_exists(&self, _: &str) -> Result<bool, String> {
Ok(false)
}
async fn list_directory(&self, _: &str) -> Result<Vec<DirEntry>, String> {
async fn list_directory(&self, _: &str, _depth: Option<usize>) -> Result<Vec<DirEntry>, String> {
Ok(vec![])
}
async fn exec_command(&self, _: &str, _: &[String], _: u64, _: Option<&str>, _: Option<&std::collections::HashMap<String, String>>) -> Result<ExecResult, String> {
async fn exec_command(&self, _: &str, _: u64, _: Option<&str>, _: Option<&std::collections::HashMap<String, String>>) -> Result<ExecResult, String> {
Ok(ExecResult {
stdout: String::new(),
stderr: String::new(),
@ -637,7 +752,7 @@ mod tests {
async fn grep(&self, _: &str, _: &str, _: &GrepOptions) -> Result<Vec<String>, String> {
Ok(vec![])
}
async fn glob(&self, _: &str) -> Result<Vec<String>, String> {
async fn glob(&self, _: &str, _path: Option<&str>) -> Result<Vec<String>, String> {
Ok(self.results.clone())
}
async fn initialize(&self) -> Result<(), String> {

View file

@ -51,16 +51,16 @@ pub fn truncate_output(output: &str, max_chars: usize, mode: TruncationMode) ->
let head = &output[..half];
let tail = &output[output.len() - half..];
format!(
"{head}\n\n[WARNING: Output truncated. {removed} characters removed. \
Full output available in event stream. Retry with smaller scope if needed.]\n\n{tail}"
"{head}\n\n[WARNING: Tool output was truncated. {removed} characters were removed from the middle. \
The full output is available in the event stream. \
If you need to see specific parts, re-run the tool with more targeted parameters.]\n\n{tail}"
)
}
TruncationMode::Tail => {
let tail = &output[output.len() - max_chars..];
format!(
"\n\n[WARNING: Output truncated. {removed} characters removed. \
Showing last portion only. Full output available in event stream. \
Retry with smaller scope if needed.]\n\n{tail}"
"[WARNING: Tool output was truncated. First {removed} characters were removed. \
The full output is available in the event stream.]\n\n{tail}"
)
}
}
@ -78,8 +78,7 @@ pub fn truncate_lines(output: &str, max_lines: usize) -> String {
let omitted = lines.len() - max_lines;
format!(
"{}\n\n[WARNING: Output truncated by line count. {omitted} lines omitted. \
Showing first and last lines.]\n\n{}",
"{}\n\n[... {omitted} lines omitted ...]\n\n{}",
head.join("\n"),
tail.join("\n")
)
@ -144,16 +143,16 @@ mod tests {
let output = "a".repeat(100);
let result = truncate_output(&output, 40, TruncationMode::HeadTail);
assert!(result.contains(&"a".repeat(20)));
assert!(result.contains("Output truncated"));
assert!(result.contains("60 characters removed"));
assert!(result.contains("Tool output was truncated"));
assert!(result.contains("60 characters were removed from the middle"));
}
#[test]
fn tail_mode() {
let output = format!("{}BBB", "A".repeat(100));
let result = truncate_output(&output, 10, TruncationMode::Tail);
assert!(result.contains("Output truncated"));
assert!(result.contains("Showing last portion only"));
assert!(result.contains("Tool output was truncated"));
assert!(result.contains("First 93 characters were removed"));
assert!(result.ends_with("AAAAAAABBB"));
}
@ -166,7 +165,7 @@ mod tests {
assert!(result.contains("line 3"));
assert!(result.contains("line 18"));
assert!(result.contains("line 20"));
assert!(result.contains("14 lines omitted"));
assert!(result.contains("... 14 lines omitted ..."));
}
#[test]
@ -182,14 +181,14 @@ mod tests {
#[test]
fn config_override_char_limit() {
let output = "x".repeat(200);
let output = "x".repeat(5000);
let mut config = SessionConfig::default();
config
.tool_output_limits
.insert("my_tool".into(), 50);
.insert("my_tool".into(), 100);
let result = truncate_tool_output(&output, "my_tool", &config);
assert!(result.len() < output.len());
assert!(result.contains("Output truncated"));
assert!(result.contains("Tool output was truncated"));
}
#[test]