mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-09 22:33:37 +00:00
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:
parent
99db2753ca
commit
8ff36fa900
13 changed files with 778 additions and 361 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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()));
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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\")"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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![])
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue