use std::path::PathBuf;
use std::sync::Arc;
use crate::agent::events::OnEvent;
use crate::agent::goal::GoalCondition;
use crate::agent::guardrail::Guardrail;
use crate::agent::{AgentRunner, AgentRunnerBuilder};
use crate::error::Error;
use crate::llm::{BoxedProvider, LlmProvider};
use crate::tool::Tool;
use crate::tool::builtins::{BuiltinToolsConfig, builtin_tools};
use super::verify::VerifyCommandTool;
const DEFAULT_CODE_MAX_TURNS: usize = 40;
const CODE_BUILTIN_NAMES: &[&str] = &[
"read",
"write",
"edit",
"patch",
"grep",
"glob",
"list",
"bash",
"todoread",
"todowrite",
"skill",
];
pub const CODE_SYSTEM_PROMPT: &str = "You are a senior software engineer working inside a fixed workspace. Your job is to \
implement the requested change and PROVE it works by running the project's verification \
command. Follow this loop:\n\
\n\
1. UNDERSTAND: before editing, read the relevant files (read/grep/glob/list). Never edit \
code you have not looked at. Build a concrete picture of the current state.\n\
2. PLAN: keep an explicit, ordered list of subgoals (use the todo tools). Work one step \
at a time.\n\
3. EDIT: make the smallest change that achieves the goal. Prefer `edit`/`patch` over \
rewriting whole files. Read a file before you write to it.\n\
4. VERIFY: after a coherent change, call the `verify` tool. It runs the project's \
configured build/test command and reports `VERIFY_RESULT: PASS` or `VERIFY_RESULT: FAIL` \
with the exit code. This — not your own judgement — is the source of truth for whether \
the code works.\n\
5. REPAIR: if `verify` reports FAIL, read the captured output, identify the specific \
cause, fix it, and run `verify` again. Do not repeat the same failing change; if a \
command keeps failing identically, change your approach.\n\
6. FINISH: you are done ONLY immediately after `verify` reports `VERIFY_RESULT: PASS`. \
Then summarise what you changed. Never claim success without a passing `verify`.\n\
\n\
EFFICIENCY: keep edits and tool calls minimal; do not re-read unchanged files.\n\
SAFETY: stay within the workspace. The `verify` command is fixed by the harness — you \
cannot choose what \"passing\" means, only make the code pass it.";
const CODE_GOAL_OBJECTIVE: &str = "The requested code change is complete and proven to work. The objective is met ONLY \
when the most recent `verify` tool result in the transcript shows `VERIFY_RESULT: PASS` \
(exit code 0) — i.e. the project's configured verification command actually passed. A \
claim of completion WITHOUT a `VERIFY_RESULT: PASS` line from the verify tool, or \
superseded by a later `VERIFY_RESULT: FAIL`, does NOT satisfy the objective.";
pub fn code_tools(
workspace: impl Into<PathBuf>,
dangerous: bool,
verify_commands: Vec<String>,
) -> Vec<Arc<dyn Tool>> {
let ws: PathBuf = workspace.into();
let raw = builtin_tools(BuiltinToolsConfig {
workspace: Some(ws.clone()),
dangerous_tools: dangerous,
..Default::default()
});
let mut tools: Vec<Arc<dyn Tool>> = raw
.into_iter()
.filter(|t| CODE_BUILTIN_NAMES.contains(&t.definition().name.as_str()))
.collect();
tools.push(Arc::new(VerifyCommandTool::new(ws, verify_commands)));
tools
}
pub fn code_goal(judge: Arc<BoxedProvider>) -> GoalCondition {
GoalCondition::new(CODE_GOAL_OBJECTIVE, judge).with_max_continuations(4)
}
pub struct CodeAgentBuilder<P: LlmProvider> {
provider: Arc<P>,
workspace: PathBuf,
verify_commands: Vec<String>,
system_prompt: Option<String>,
name: Option<String>,
max_turns: Option<usize>,
max_identical_tool_calls: Option<u32>,
on_event: Option<Arc<OnEvent>>,
extra_guardrails: Vec<Arc<dyn Guardrail>>,
tool_allow: Vec<String>,
goal: Option<GoalCondition>,
dangerous_tools: bool,
}
impl<P: LlmProvider> CodeAgentBuilder<P> {
pub fn new(provider: Arc<P>, workspace: impl Into<PathBuf>) -> Self {
Self {
provider,
workspace: workspace.into(),
verify_commands: Vec::new(),
system_prompt: None,
name: None,
max_turns: None,
max_identical_tool_calls: None,
on_event: None,
extra_guardrails: Vec::new(),
tool_allow: Vec::new(),
goal: None,
dangerous_tools: true,
}
}
pub fn verify_command(mut self, cmd: impl Into<String>) -> Self {
self.verify_commands.push(cmd.into());
self
}
pub fn verify_commands(mut self, cmds: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.verify_commands = cmds.into_iter().map(Into::into).collect();
self
}
pub fn system_prompt(mut self, prompt: impl Into<String>) -> Self {
self.system_prompt = Some(prompt.into());
self
}
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn max_turns(mut self, max_turns: usize) -> Self {
self.max_turns = Some(max_turns);
self
}
pub fn max_identical_tool_calls(mut self, n: u32) -> Self {
self.max_identical_tool_calls = Some(n);
self
}
pub fn on_event(mut self, callback: Arc<OnEvent>) -> Self {
self.on_event = Some(callback);
self
}
pub fn guardrail(mut self, guard: Arc<dyn Guardrail>) -> Self {
self.extra_guardrails.push(guard);
self
}
pub fn tools_allow(mut self, names: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.tool_allow = names.into_iter().map(Into::into).collect();
self
}
pub fn goal(mut self, goal: GoalCondition) -> Self {
self.goal = Some(goal);
self
}
pub fn dangerous_tools(mut self, enabled: bool) -> Self {
self.dangerous_tools = enabled;
self
}
pub fn build(self) -> Result<AgentRunner<P>, Error> {
let mut tools = code_tools(
self.workspace.clone(),
self.dangerous_tools,
self.verify_commands.clone(),
);
if !self.tool_allow.is_empty() {
tools.retain(|t| {
let n = t.definition().name;
n == "verify" || self.tool_allow.iter().any(|a| a == &n)
});
}
let prompt = self
.system_prompt
.unwrap_or_else(|| CODE_SYSTEM_PROMPT.to_string());
let mut b: AgentRunnerBuilder<P> = AgentRunner::builder(self.provider)
.tools(tools)
.system_prompt(prompt)
.workspace(self.workspace)
.max_turns(self.max_turns.unwrap_or(DEFAULT_CODE_MAX_TURNS));
if let Some(name) = self.name {
b = b.name(name);
}
if let Some(n) = self.max_identical_tool_calls {
b = b.max_identical_tool_calls(n);
}
if let Some(ev) = self.on_event {
b = b.on_event(ev);
}
if !self.extra_guardrails.is_empty() {
b = b.guardrails(self.extra_guardrails);
}
if let Some(goal) = self.goal {
b = b.goal(goal);
}
b.build()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::agent::test_helpers::MockProvider;
#[test]
fn system_prompt_encodes_loop_invariants() {
for needle in [
"UNDERSTAND",
"PLAN",
"EDIT",
"VERIFY",
"REPAIR",
"FINISH",
"verify",
"VERIFY_RESULT",
] {
assert!(
CODE_SYSTEM_PROMPT.contains(needle),
"system prompt missing loop invariant `{needle}`"
);
}
}
#[test]
fn goal_objective_keys_on_verify_sentinel() {
let judge = Arc::new(BoxedProvider::new(MockProvider::new(vec![])));
let goal = code_goal(judge);
assert!(
goal.objective().contains("VERIFY_RESULT: PASS"),
"goal must gate on the deterministic verify sentinel, got: {}",
goal.objective()
);
}
#[test]
fn code_tools_include_verify_and_editing_surface() {
let dir = tempfile::tempdir().unwrap();
let tools = code_tools(dir.path(), true, vec!["true".into()]);
let names: Vec<String> = tools.iter().map(|t| t.definition().name).collect();
for expected in [
"verify", "read", "write", "edit", "patch", "grep", "glob", "list", "bash",
] {
assert!(
names.iter().any(|n| n == expected),
"code_tools missing `{expected}`; got {names:?}"
);
}
}
#[test]
fn code_tools_omit_bash_when_not_dangerous() {
let dir = tempfile::tempdir().unwrap();
let tools = code_tools(dir.path(), false, vec!["true".into()]);
let names: Vec<String> = tools.iter().map(|t| t.definition().name).collect();
assert!(
!names.iter().any(|n| n == "bash"),
"bash must be gated: {names:?}"
);
assert!(names.iter().any(|n| n == "verify"));
assert!(names.iter().any(|n| n == "edit"));
}
#[test]
fn build_wires_a_runner() {
let dir = tempfile::tempdir().unwrap();
let provider = Arc::new(MockProvider::new(vec![]));
let runner = CodeAgentBuilder::new(provider, dir.path())
.name("coder")
.verify_command("cargo test")
.build();
assert!(
runner.is_ok(),
"build should wire a runner: {:?}",
runner.err()
);
}
#[test]
fn build_keeps_verify_even_under_tools_allow() {
let dir = tempfile::tempdir().unwrap();
let provider = Arc::new(MockProvider::new(vec![]));
let tools = code_tools(dir.path(), true, vec!["true".into()]);
let _ = CodeAgentBuilder::new(provider, dir.path())
.verify_command("true")
.tools_allow(["read"])
.build()
.expect("build ok");
assert!(tools.iter().any(|t| t.definition().name == "verify"));
}
}