1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::path::Path;
9use std::path::PathBuf;
10use std::time::Duration;
11use uuid::Uuid;
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct ExecutionInput {
16 pub task_description: String,
18 pub success_criteria: String,
20 pub context: Option<String>,
22 pub prior_attempts: Vec<AttemptSummary>,
24 pub verifier_feedback: Option<String>,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct AttemptSummary {
31 pub attempt_number: u32,
33 pub summary: String,
35 pub failure_reason: Option<String>,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct ExecutionReport {
42 pub exit_code: i32,
44 pub duration: Duration,
46 pub stdout: String,
48 pub stderr: String,
50 pub files_created: Vec<PathBuf>,
52 pub files_modified: Vec<PathBuf>,
54 pub files_deleted: Vec<PathBuf>,
56 pub errors: Vec<RuntimeError>,
58}
59
60impl ExecutionReport {
61 pub fn success(duration: Duration, stdout: String, stderr: String) -> Self {
63 Self {
64 exit_code: 0,
65 duration,
66 stdout,
67 stderr,
68 files_created: Vec::new(),
69 files_modified: Vec::new(),
70 files_deleted: Vec::new(),
71 errors: Vec::new(),
72 }
73 }
74
75 pub fn failure(exit_code: i32, duration: Duration, error: RuntimeError) -> Self {
77 Self {
78 exit_code,
79 duration,
80 stdout: String::new(),
81 stderr: String::new(),
82 files_created: Vec::new(),
83 files_modified: Vec::new(),
84 files_deleted: Vec::new(),
85 errors: vec![error],
86 }
87 }
88
89 pub fn is_success(&self) -> bool {
91 self.exit_code == 0 && self.errors.is_empty()
92 }
93
94 #[must_use]
96 pub fn with_file_changes(
97 mut self,
98 created: Vec<PathBuf>,
99 modified: Vec<PathBuf>,
100 deleted: Vec<PathBuf>,
101 ) -> Self {
102 self.files_created = created;
103 self.files_modified = modified;
104 self.files_deleted = deleted;
105 self
106 }
107}
108
109#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
111#[serde(tag = "type", rename_all = "snake_case")]
112pub enum InteractiveAdapterEvent {
113 Output { content: String },
115 Input { content: String },
117 Interrupted,
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct InteractiveExecutionResult {
124 pub report: ExecutionReport,
126 pub terminated_reason: Option<String>,
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct RuntimeError {
133 pub code: String,
135 pub message: String,
137 pub recoverable: bool,
139}
140
141impl RuntimeError {
142 pub fn new(code: impl Into<String>, message: impl Into<String>, recoverable: bool) -> Self {
144 Self {
145 code: code.into(),
146 message: message.into(),
147 recoverable,
148 }
149 }
150
151 pub fn timeout(duration: Duration) -> Self {
153 Self::new(
154 "timeout",
155 format!("Execution timed out after {duration:?}"),
156 true,
157 )
158 }
159
160 pub fn crash(message: impl Into<String>) -> Self {
162 Self::new("crash", message, false)
163 }
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct AdapterConfig {
169 pub name: String,
171 pub binary_path: PathBuf,
173 pub args: Vec<String>,
175 pub env: HashMap<String, String>,
177 pub timeout: Duration,
179 pub working_dir: Option<PathBuf>,
181}
182
183impl AdapterConfig {
184 pub fn new(name: impl Into<String>, binary_path: PathBuf) -> Self {
186 Self {
187 name: name.into(),
188 binary_path,
189 args: Vec::new(),
190 env: HashMap::new(),
191 timeout: Duration::from_secs(300), working_dir: None,
193 }
194 }
195
196 #[must_use]
198 pub fn with_timeout(mut self, timeout: Duration) -> Self {
199 self.timeout = timeout;
200 self
201 }
202
203 #[must_use]
205 pub fn with_arg(mut self, arg: impl Into<String>) -> Self {
206 self.args.push(arg.into());
207 self
208 }
209
210 #[must_use]
212 pub fn with_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
213 self.env.insert(key.into(), value.into());
214 self
215 }
216}
217
218pub trait RuntimeAdapter: Send + Sync {
222 fn name(&self) -> &str;
224
225 fn initialize(&mut self) -> Result<(), RuntimeError>;
227
228 fn prepare(&mut self, task_id: Uuid, worktree: &Path) -> Result<(), RuntimeError>;
230
231 fn execute(&mut self, input: ExecutionInput) -> Result<ExecutionReport, RuntimeError>;
233
234 fn terminate(&mut self) -> Result<(), RuntimeError>;
236
237 fn config(&self) -> &AdapterConfig;
239}
240
241#[derive(Debug)]
243pub struct MockAdapter {
244 config: AdapterConfig,
245 prepared: bool,
246 response: Option<ExecutionReport>,
247}
248
249impl MockAdapter {
250 pub fn new() -> Self {
252 Self {
253 config: AdapterConfig::new("mock", PathBuf::from("/bin/echo")),
254 prepared: false,
255 response: None,
256 }
257 }
258
259 #[must_use]
261 pub fn with_response(mut self, report: ExecutionReport) -> Self {
262 self.response = Some(report);
263 self
264 }
265}
266
267impl Default for MockAdapter {
268 fn default() -> Self {
269 Self::new()
270 }
271}
272
273impl RuntimeAdapter for MockAdapter {
274 fn name(&self) -> &str {
275 &self.config.name
276 }
277
278 fn initialize(&mut self) -> Result<(), RuntimeError> {
279 Ok(())
280 }
281
282 fn prepare(&mut self, _task_id: Uuid, _worktree: &Path) -> Result<(), RuntimeError> {
283 self.prepared = true;
284 Ok(())
285 }
286
287 fn execute(&mut self, _input: ExecutionInput) -> Result<ExecutionReport, RuntimeError> {
288 if !self.prepared {
289 return Err(RuntimeError::new(
290 "not_prepared",
291 "Adapter not prepared",
292 false,
293 ));
294 }
295
296 Ok(self.response.clone().unwrap_or_else(|| {
297 ExecutionReport::success(
298 Duration::from_secs(1),
299 "mock output".to_string(),
300 String::new(),
301 )
302 }))
303 }
304
305 fn terminate(&mut self) -> Result<(), RuntimeError> {
306 self.prepared = false;
307 Ok(())
308 }
309
310 fn config(&self) -> &AdapterConfig {
311 &self.config
312 }
313}
314
315#[cfg(test)]
316mod tests {
317 use super::*;
318
319 #[test]
320 fn execution_input_creation() {
321 let input = ExecutionInput {
322 task_description: "Write a test".to_string(),
323 success_criteria: "Test passes".to_string(),
324 context: None,
325 prior_attempts: Vec::new(),
326 verifier_feedback: None,
327 };
328
329 assert!(input.prior_attempts.is_empty());
330 }
331
332 #[test]
333 fn execution_report_success() {
334 let report =
335 ExecutionReport::success(Duration::from_secs(5), "output".to_string(), String::new());
336
337 assert!(report.is_success());
338 assert_eq!(report.exit_code, 0);
339 }
340
341 #[test]
342 fn execution_report_failure() {
343 let report = ExecutionReport::failure(
344 1,
345 Duration::from_secs(2),
346 RuntimeError::new("test_error", "Test failed", false),
347 );
348
349 assert!(!report.is_success());
350 assert_eq!(report.exit_code, 1);
351 }
352
353 #[test]
354 fn adapter_config_builder() {
355 let config = AdapterConfig::new("test", PathBuf::from("/bin/test"))
356 .with_timeout(Duration::from_secs(60))
357 .with_arg("--verbose")
358 .with_env("DEBUG", "true");
359
360 assert_eq!(config.name, "test");
361 assert_eq!(config.timeout, Duration::from_secs(60));
362 assert_eq!(config.args, vec!["--verbose"]);
363 assert_eq!(config.env.get("DEBUG"), Some(&"true".to_string()));
364 }
365
366 #[test]
367 fn mock_adapter_lifecycle() {
368 let mut adapter = MockAdapter::new();
369
370 assert!(adapter.initialize().is_ok());
371
372 let worktree = PathBuf::from("/tmp/test");
373 let task_id = Uuid::new_v4();
374
375 adapter.prepare(task_id, &worktree).unwrap();
376
377 let input = ExecutionInput {
378 task_description: "Test task".to_string(),
379 success_criteria: "Done".to_string(),
380 context: None,
381 prior_attempts: Vec::new(),
382 verifier_feedback: None,
383 };
384
385 let report = adapter.execute(input).unwrap();
386 assert!(report.is_success());
387
388 adapter.terminate().unwrap();
389 }
390
391 #[test]
392 fn mock_adapter_custom_response() {
393 let custom_report = ExecutionReport::failure(
394 1,
395 Duration::from_secs(3),
396 RuntimeError::new("custom", "Custom error", true),
397 );
398
399 let mut adapter = MockAdapter::new().with_response(custom_report);
400 adapter
401 .prepare(Uuid::new_v4(), &PathBuf::from("/tmp"))
402 .unwrap();
403
404 let input = ExecutionInput {
405 task_description: "Test".to_string(),
406 success_criteria: "Done".to_string(),
407 context: None,
408 prior_attempts: Vec::new(),
409 verifier_feedback: None,
410 };
411
412 let report = adapter.execute(input).unwrap();
413 assert!(!report.is_success());
414 }
415
416 #[test]
417 fn runtime_error_types() {
418 let timeout = RuntimeError::timeout(Duration::from_secs(60));
419 assert_eq!(timeout.code, "timeout");
420 assert!(timeout.recoverable);
421
422 let crash = RuntimeError::crash("Segmentation fault");
423 assert_eq!(crash.code, "crash");
424 assert!(!crash.recoverable);
425 }
426}