fabro/crates/coding-agent-loop/src/profiles/mod.rs
Bryan Helmkamp db32f00cd0 Simplify coding-agent-loop: deduplicate mocks, narrow traits, type events
- Extract shared test infrastructure (MockExecutionEnvironment, TestProfile,
  MockLlmProvider) replacing 11 duplicate mock implementations across tests
- Deduplicate tool execution logic between sequential and parallel paths
- Narrow ProviderProfile trait from 14 to 7 required methods via
  ProfileCapabilities struct and default implementations
- Replace stringly-typed HashMap event data with typed EventData enum
- Extract shared assemble_system_prompt helper and register_subagent_tools
  default method, eliminating copy-paste across all 3 profiles
- Replace fragile shell-based glob with glob crate, fix rg detection
- Add delete_file to ExecutionEnvironment, wire git context into env block
- Remove dead code (AgentError::Io, count_turns, trivial derived-trait tests)
- Use match-based lookups in truncation instead of per-call HashMap allocation

Net reduction: -1,401 lines across 20 files. All 180 tests pass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 16:34:29 -04:00

135 lines
4.3 KiB
Rust

pub mod anthropic;
pub mod gemini;
pub mod openai;
pub use anthropic::AnthropicProfile;
pub use gemini::GeminiProfile;
pub use openai::OpenAiProfile;
use crate::execution_env::ExecutionEnvironment;
/// Additional context for building environment blocks
#[derive(Default)]
pub struct EnvContext {
pub git_branch: Option<String>,
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>,
}
/// Assembles a complete system prompt from a core prompt template and standard sections.
///
/// The `core_prompt` should contain `{env_block}` as a placeholder where the environment
/// context block will be inserted. Project docs and user instructions are appended at the end.
#[must_use]
pub fn assemble_system_prompt(
core_prompt: &str,
env: &dyn ExecutionEnvironment,
env_context: &EnvContext,
project_docs: &[String],
user_instructions: Option<&str>,
) -> String {
let env_block = build_env_context_block_with(env, env_context);
let docs_section = if project_docs.is_empty() {
String::new()
} else {
format!("\n\n{}", project_docs.join("\n\n"))
};
let user_section = match user_instructions {
Some(instructions) => format!("\n\n# User Instructions\n{instructions}"),
None => String::new(),
};
let prompt = core_prompt.replace("{env_block}", &env_block);
format!("{prompt}{docs_section}{user_section}")
}
#[must_use]
pub fn build_env_context_block(env: &dyn ExecutionEnvironment) -> String {
build_env_context_block_with(env, &EnvContext::default())
}
#[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!("Is git repository: {}", ctx.is_git_repo),
];
if let Some(ref branch) = ctx.git_branch {
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));
}
if let Some(ref status) = ctx.git_status_short {
lines.push(format!("Git status:\n{status}"));
}
if let Some(ref commits) = ctx.git_recent_commits {
lines.push(format!("Recent commits:\n{commits}"));
}
lines.push("</environment>".to_string());
lines.join("\n")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::MockExecutionEnvironment;
fn linux_env() -> MockExecutionEnvironment {
MockExecutionEnvironment {
working_dir: "/home/test",
platform_str: "linux",
os_version_str: "Linux 6.1.0".into(),
..Default::default()
}
}
#[test]
fn env_context_block_contains_platform() {
let env = linux_env();
let block = build_env_context_block(&env);
assert!(block.contains("<environment>"));
assert!(block.contains("</environment>"));
assert!(block.contains("linux"));
assert!(block.contains("/home/test"));
assert!(block.contains("Linux 6.1.0"));
}
#[test]
fn env_context_block_with_extra_context() {
let env = linux_env();
let ctx = EnvContext {
git_branch: Some("main".into()),
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 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"));
}
}