1use std::path::PathBuf;
10use std::sync::Arc;
11
12use crate::agent::events::OnEvent;
13use crate::agent::goal::GoalCondition;
14use crate::agent::guardrail::Guardrail;
15use crate::agent::{AgentRunner, AgentRunnerBuilder};
16use crate::error::Error;
17use crate::llm::{BoxedProvider, LlmProvider};
18use crate::tool::Tool;
19use crate::tool::builtins::{BuiltinToolsConfig, builtin_tools};
20
21use super::verify::VerifyCommandTool;
22
23const DEFAULT_CODE_MAX_TURNS: usize = 40;
26
27const CODE_BUILTIN_NAMES: &[&str] = &[
31 "read",
32 "write",
33 "edit",
34 "patch",
35 "grep",
36 "glob",
37 "list",
38 "bash",
39 "todoread",
40 "todowrite",
41 "skill",
42];
43
44pub const CODE_SYSTEM_PROMPT: &str = "You are a senior software engineer working inside a fixed workspace. Your job is to \
47implement the requested change and PROVE it works by running the project's verification \
48command. Follow this loop:\n\
49\n\
501. UNDERSTAND: before editing, read the relevant files (read/grep/glob/list). Never edit \
51code you have not looked at. Build a concrete picture of the current state.\n\
522. PLAN: keep an explicit, ordered list of subgoals (use the todo tools). Work one step \
53at a time.\n\
543. EDIT: make the smallest change that achieves the goal. Prefer `edit`/`patch` over \
55rewriting whole files. Read a file before you write to it.\n\
564. VERIFY: after a coherent change, call the `verify` tool. It runs the project's \
57configured build/test command and reports `VERIFY_RESULT: PASS` or `VERIFY_RESULT: FAIL` \
58with the exit code. This — not your own judgement — is the source of truth for whether \
59the code works.\n\
605. REPAIR: if `verify` reports FAIL, read the captured output, identify the specific \
61cause, fix it, and run `verify` again. Do not repeat the same failing change; if a \
62command keeps failing identically, change your approach.\n\
636. FINISH: you are done ONLY immediately after `verify` reports `VERIFY_RESULT: PASS`. \
64Then summarise what you changed. Never claim success without a passing `verify`.\n\
65\n\
66EFFICIENCY: keep edits and tool calls minimal; do not re-read unchanged files.\n\
67SAFETY: stay within the workspace. The `verify` command is fixed by the harness — you \
68cannot choose what \"passing\" means, only make the code pass it.";
69
70const CODE_GOAL_OBJECTIVE: &str = "The requested code change is complete and proven to work. The objective is met ONLY \
72 when the most recent `verify` tool result in the transcript shows `VERIFY_RESULT: PASS` \
73 (exit code 0) — i.e. the project's configured verification command actually passed. A \
74 claim of completion WITHOUT a `VERIFY_RESULT: PASS` line from the verify tool, or \
75 superseded by a later `VERIFY_RESULT: FAIL`, does NOT satisfy the objective.";
76
77pub fn code_tools(
80 workspace: impl Into<PathBuf>,
81 dangerous: bool,
82 verify_commands: Vec<String>,
83) -> Vec<Arc<dyn Tool>> {
84 let ws: PathBuf = workspace.into();
85 let raw = builtin_tools(BuiltinToolsConfig {
86 workspace: Some(ws.clone()),
87 dangerous_tools: dangerous,
88 ..Default::default()
89 });
90 let mut tools: Vec<Arc<dyn Tool>> = raw
91 .into_iter()
92 .filter(|t| CODE_BUILTIN_NAMES.contains(&t.definition().name.as_str()))
93 .collect();
94 tools.push(Arc::new(VerifyCommandTool::new(ws, verify_commands)));
95 tools
96}
97
98pub fn code_goal(judge: Arc<BoxedProvider>) -> GoalCondition {
101 GoalCondition::new(CODE_GOAL_OBJECTIVE, judge).with_max_continuations(4)
102}
103
104pub struct CodeAgentBuilder<P: LlmProvider> {
107 provider: Arc<P>,
108 workspace: PathBuf,
109 verify_commands: Vec<String>,
110 system_prompt: Option<String>,
111 name: Option<String>,
112 max_turns: Option<usize>,
113 max_identical_tool_calls: Option<u32>,
114 on_event: Option<Arc<OnEvent>>,
115 extra_guardrails: Vec<Arc<dyn Guardrail>>,
116 tool_allow: Vec<String>,
117 goal: Option<GoalCondition>,
118 dangerous_tools: bool,
119}
120
121impl<P: LlmProvider> CodeAgentBuilder<P> {
122 pub fn new(provider: Arc<P>, workspace: impl Into<PathBuf>) -> Self {
124 Self {
125 provider,
126 workspace: workspace.into(),
127 verify_commands: Vec::new(),
128 system_prompt: None,
129 name: None,
130 max_turns: None,
131 max_identical_tool_calls: None,
132 on_event: None,
133 extra_guardrails: Vec::new(),
134 tool_allow: Vec::new(),
135 goal: None,
136 dangerous_tools: true,
137 }
138 }
139
140 pub fn verify_command(mut self, cmd: impl Into<String>) -> Self {
142 self.verify_commands.push(cmd.into());
143 self
144 }
145
146 pub fn verify_commands(mut self, cmds: impl IntoIterator<Item = impl Into<String>>) -> Self {
148 self.verify_commands = cmds.into_iter().map(Into::into).collect();
149 self
150 }
151
152 pub fn system_prompt(mut self, prompt: impl Into<String>) -> Self {
154 self.system_prompt = Some(prompt.into());
155 self
156 }
157
158 pub fn name(mut self, name: impl Into<String>) -> Self {
160 self.name = Some(name.into());
161 self
162 }
163
164 pub fn max_turns(mut self, max_turns: usize) -> Self {
166 self.max_turns = Some(max_turns);
167 self
168 }
169
170 pub fn max_identical_tool_calls(mut self, n: u32) -> Self {
172 self.max_identical_tool_calls = Some(n);
173 self
174 }
175
176 pub fn on_event(mut self, callback: Arc<OnEvent>) -> Self {
178 self.on_event = Some(callback);
179 self
180 }
181
182 pub fn guardrail(mut self, guard: Arc<dyn Guardrail>) -> Self {
184 self.extra_guardrails.push(guard);
185 self
186 }
187
188 pub fn tools_allow(mut self, names: impl IntoIterator<Item = impl Into<String>>) -> Self {
191 self.tool_allow = names.into_iter().map(Into::into).collect();
192 self
193 }
194
195 pub fn goal(mut self, goal: GoalCondition) -> Self {
197 self.goal = Some(goal);
198 self
199 }
200
201 pub fn dangerous_tools(mut self, enabled: bool) -> Self {
203 self.dangerous_tools = enabled;
204 self
205 }
206
207 pub fn build(self) -> Result<AgentRunner<P>, Error> {
209 let mut tools = code_tools(
210 self.workspace.clone(),
211 self.dangerous_tools,
212 self.verify_commands.clone(),
213 );
214 if !self.tool_allow.is_empty() {
215 tools.retain(|t| {
216 let n = t.definition().name;
217 n == "verify" || self.tool_allow.iter().any(|a| a == &n)
219 });
220 }
221
222 let prompt = self
223 .system_prompt
224 .unwrap_or_else(|| CODE_SYSTEM_PROMPT.to_string());
225
226 let mut b: AgentRunnerBuilder<P> = AgentRunner::builder(self.provider)
227 .tools(tools)
228 .system_prompt(prompt)
229 .workspace(self.workspace)
230 .max_turns(self.max_turns.unwrap_or(DEFAULT_CODE_MAX_TURNS));
231 if let Some(name) = self.name {
232 b = b.name(name);
233 }
234 if let Some(n) = self.max_identical_tool_calls {
235 b = b.max_identical_tool_calls(n);
236 }
237 if let Some(ev) = self.on_event {
238 b = b.on_event(ev);
239 }
240 if !self.extra_guardrails.is_empty() {
241 b = b.guardrails(self.extra_guardrails);
242 }
243 if let Some(goal) = self.goal {
244 b = b.goal(goal);
245 }
246 b.build()
247 }
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253 use crate::agent::test_helpers::MockProvider;
254
255 #[test]
256 fn system_prompt_encodes_loop_invariants() {
257 for needle in [
258 "UNDERSTAND",
259 "PLAN",
260 "EDIT",
261 "VERIFY",
262 "REPAIR",
263 "FINISH",
264 "verify",
265 "VERIFY_RESULT",
266 ] {
267 assert!(
268 CODE_SYSTEM_PROMPT.contains(needle),
269 "system prompt missing loop invariant `{needle}`"
270 );
271 }
272 }
273
274 #[test]
275 fn goal_objective_keys_on_verify_sentinel() {
276 let judge = Arc::new(BoxedProvider::new(MockProvider::new(vec![])));
277 let goal = code_goal(judge);
278 assert!(
279 goal.objective().contains("VERIFY_RESULT: PASS"),
280 "goal must gate on the deterministic verify sentinel, got: {}",
281 goal.objective()
282 );
283 }
284
285 #[test]
286 fn code_tools_include_verify_and_editing_surface() {
287 let dir = tempfile::tempdir().unwrap();
288 let tools = code_tools(dir.path(), true, vec!["true".into()]);
289 let names: Vec<String> = tools.iter().map(|t| t.definition().name).collect();
290 for expected in [
291 "verify", "read", "write", "edit", "patch", "grep", "glob", "list", "bash",
292 ] {
293 assert!(
294 names.iter().any(|n| n == expected),
295 "code_tools missing `{expected}`; got {names:?}"
296 );
297 }
298 }
299
300 #[test]
301 fn code_tools_omit_bash_when_not_dangerous() {
302 let dir = tempfile::tempdir().unwrap();
303 let tools = code_tools(dir.path(), false, vec!["true".into()]);
304 let names: Vec<String> = tools.iter().map(|t| t.definition().name).collect();
305 assert!(
306 !names.iter().any(|n| n == "bash"),
307 "bash must be gated: {names:?}"
308 );
309 assert!(names.iter().any(|n| n == "verify"));
311 assert!(names.iter().any(|n| n == "edit"));
312 }
313
314 #[test]
315 fn build_wires_a_runner() {
316 let dir = tempfile::tempdir().unwrap();
317 let provider = Arc::new(MockProvider::new(vec![]));
318 let runner = CodeAgentBuilder::new(provider, dir.path())
319 .name("coder")
320 .verify_command("cargo test")
321 .build();
322 assert!(
323 runner.is_ok(),
324 "build should wire a runner: {:?}",
325 runner.err()
326 );
327 }
328
329 #[test]
330 fn build_keeps_verify_even_under_tools_allow() {
331 let dir = tempfile::tempdir().unwrap();
332 let provider = Arc::new(MockProvider::new(vec![]));
333 let tools = code_tools(dir.path(), true, vec!["true".into()]);
335 let _ = CodeAgentBuilder::new(provider, dir.path())
336 .verify_command("true")
337 .tools_allow(["read"])
338 .build()
339 .expect("build ok");
340 assert!(tools.iter().any(|t| t.definition().name == "verify"));
342 }
343}