apollo/tools/
build_runner.rs1use std::path::PathBuf;
9use std::sync::Arc;
10
11use async_trait::async_trait;
12use serde::Deserialize;
13
14use crate::agent::build_runner::{BuildRunner, BuildRunnerConfig};
15use crate::policy::ExecutionPolicy;
16
17use super::traits::*;
18
19pub struct BuildRunnerTool {
20 workspace: PathBuf,
21 config: BuildRunnerConfig,
22 policy: Arc<ExecutionPolicy>,
23}
24
25impl BuildRunnerTool {
26 pub fn new(
27 workspace: PathBuf,
28 config: BuildRunnerConfig,
29 policy: Arc<ExecutionPolicy>,
30 ) -> Self {
31 Self {
32 workspace,
33 config,
34 policy,
35 }
36 }
37
38 pub fn with_default_config(workspace: PathBuf, policy: Arc<ExecutionPolicy>) -> Self {
39 Self::new(workspace, BuildRunnerConfig::default(), policy)
40 }
41}
42
43#[derive(Deserialize)]
44struct BuildRunnerArgs {
45 #[serde(default)]
47 action: String,
48 #[serde(default)]
50 package: String,
51 #[serde(default)]
53 timeout: Option<u64>,
54 #[serde(default)]
56 max_retries: Option<usize>,
57}
58
59#[async_trait]
60impl Tool for BuildRunnerTool {
61 fn name(&self) -> &str {
62 "build_runner"
63 }
64
65 fn spec(&self) -> ToolSpec {
66 ToolSpec {
67 name: "build_runner".to_string(),
68 description: "Run cargo build/test/cycle with structured error parsing and retry. \
69 Returns a summary of compiler errors (file:line:col + message) the model \
70 can act on directly, plus the raw output. \
71 'build' = cargo build, 'test' = cargo test, 'cycle' = build then test."
72 .to_string(),
73 parameters: serde_json::json!({
74 "type": "object",
75 "properties": {
76 "action": {
77 "type": "string",
78 "enum": ["build", "test", "cycle"],
79 "description": "build = cargo build, test = cargo test, cycle = build then test (default build)"
80 },
81 "package": {
82 "type": "string",
83 "description": "Package filter (cargo -p <name>). Empty = whole workspace."
84 },
85 "timeout": {
86 "type": "integer",
87 "description": "Per-invocation timeout in seconds"
88 },
89 "max_retries": {
90 "type": "integer",
91 "description": "Maximum retries on failure"
92 }
93 }
94 }),
95 }
96 }
97
98 async fn execute(&self, arguments: &str) -> anyhow::Result<ToolResult> {
99 if !self.policy.allow_shell {
100 return ExecutionPolicy::deny("Build execution is disabled by policy");
101 }
102
103 let args: BuildRunnerArgs = serde_json::from_str(arguments).unwrap_or(BuildRunnerArgs {
104 action: String::new(),
105 package: String::new(),
106 timeout: None,
107 max_retries: None,
108 });
109
110 let mut config = self.config.clone();
111 if !args.package.is_empty() {
112 config.package = args.package;
113 }
114 if let Some(t) = args.timeout {
115 config.timeout_secs = t;
116 }
117 if let Some(r) = args.max_retries {
118 config.max_retries = r;
119 }
120
121 let runner = BuildRunner::new(self.workspace.clone(), config);
122 let action = if args.action.is_empty() {
123 "build"
124 } else {
125 args.action.as_str()
126 };
127
128 let output = match action {
129 "build" => {
130 let r = runner.run_build().await;
131 if r.success {
132 ToolResult::success(r.summary())
133 } else {
134 ToolResult::error(format!(
135 "{}\n\n--- raw output ---\n{}",
136 r.summary(),
137 r.raw_output
138 ))
139 }
140 }
141 "test" => {
142 let r = runner.run_test().await;
143 if r.success {
144 ToolResult::success(r.summary())
145 } else {
146 ToolResult::error(format!(
147 "{}\n\n--- raw output ---\n{}",
148 r.summary(),
149 r.raw_output
150 ))
151 }
152 }
153 "cycle" => {
154 let (build, test) = runner.run_cycle().await;
155 if !build.success {
156 ToolResult::error(format!(
157 "{}\n\n--- raw output ---\n{}",
158 build.summary(),
159 build.raw_output
160 ))
161 } else if let Some(t) = test {
162 if t.success {
163 ToolResult::success(format!("{}\n{}", build.summary(), t.summary()))
164 } else {
165 ToolResult::error(format!(
166 "{}\n\n--- raw output ---\n{}",
167 t.summary(),
168 t.raw_output
169 ))
170 }
171 } else {
172 ToolResult::success(build.summary())
173 }
174 }
175 other => ToolResult::error(format!(
176 "Unknown action: {} (use build, test, or cycle)",
177 other
178 )),
179 };
180
181 Ok(output)
182 }
183}