1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::fmt::Write as _;
9use std::path::Path;
10use std::path::PathBuf;
11use std::time::Duration;
12use uuid::Uuid;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct ExecutionInput {
17 pub task_description: String,
19 pub success_criteria: String,
21 pub context: Option<String>,
23 pub prior_attempts: Vec<AttemptSummary>,
25 pub verifier_feedback: Option<String>,
27}
28
29#[must_use]
31pub fn format_execution_prompt(input: &ExecutionInput) -> String {
32 let task_description = &input.task_description;
33 let success_criteria = &input.success_criteria;
34 let mut prompt = format!("Task: {task_description}\n\n");
35 let _ = write!(prompt, "Success Criteria: {success_criteria}\n\n");
36
37 if let Some(ref context) = input.context {
38 let _ = write!(prompt, "Context:\n{context}\n\n");
39 }
40
41 if !input.prior_attempts.is_empty() {
42 prompt.push_str("Prior Attempts:\n");
43 for attempt in &input.prior_attempts {
44 let attempt_number = attempt.attempt_number;
45 let summary = &attempt.summary;
46 let _ = writeln!(prompt, "- Attempt {attempt_number}: {summary}");
47 if let Some(ref reason) = attempt.failure_reason {
48 let _ = writeln!(prompt, " Failure: {reason}");
49 }
50 }
51 prompt.push('\n');
52 }
53
54 if let Some(ref feedback) = input.verifier_feedback {
55 let _ = write!(prompt, "Verifier Feedback:\n{feedback}\n\n");
56 }
57
58 prompt
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct AttemptSummary {
64 pub attempt_number: u32,
66 pub summary: String,
68 pub failure_reason: Option<String>,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct ExecutionReport {
75 pub exit_code: i32,
77 pub duration: Duration,
79 pub stdout: String,
81 pub stderr: String,
83 pub files_created: Vec<PathBuf>,
85 pub files_modified: Vec<PathBuf>,
87 pub files_deleted: Vec<PathBuf>,
89 pub errors: Vec<RuntimeError>,
91}
92
93impl ExecutionReport {
94 pub fn success(duration: Duration, stdout: String, stderr: String) -> Self {
96 Self {
97 exit_code: 0,
98 duration,
99 stdout,
100 stderr,
101 files_created: Vec::new(),
102 files_modified: Vec::new(),
103 files_deleted: Vec::new(),
104 errors: Vec::new(),
105 }
106 }
107
108 pub fn failure(exit_code: i32, duration: Duration, error: RuntimeError) -> Self {
110 Self::failure_with_output(exit_code, duration, error, String::new(), String::new())
111 }
112
113 pub fn failure_with_output(
115 exit_code: i32,
116 duration: Duration,
117 error: RuntimeError,
118 stdout: String,
119 stderr: String,
120 ) -> Self {
121 Self {
122 exit_code,
123 duration,
124 stdout,
125 stderr,
126 files_created: Vec::new(),
127 files_modified: Vec::new(),
128 files_deleted: Vec::new(),
129 errors: vec![error],
130 }
131 }
132
133 pub fn is_success(&self) -> bool {
135 self.exit_code == 0 && self.errors.is_empty()
136 }
137
138 #[must_use]
140 pub fn with_file_changes(
141 mut self,
142 created: Vec<PathBuf>,
143 modified: Vec<PathBuf>,
144 deleted: Vec<PathBuf>,
145 ) -> Self {
146 self.files_created = created;
147 self.files_modified = modified;
148 self.files_deleted = deleted;
149 self
150 }
151}
152
153#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
155#[serde(tag = "type", rename_all = "snake_case")]
156pub enum InteractiveAdapterEvent {
157 Output { content: String },
159 Input { content: String },
161 Interrupted,
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct InteractiveExecutionResult {
168 pub report: ExecutionReport,
170 pub terminated_reason: Option<String>,
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize)]
176pub struct RuntimeError {
177 pub code: String,
179 pub message: String,
181 pub recoverable: bool,
183}
184
185impl RuntimeError {
186 pub fn new(code: impl Into<String>, message: impl Into<String>, recoverable: bool) -> Self {
188 Self {
189 code: code.into(),
190 message: message.into(),
191 recoverable,
192 }
193 }
194
195 pub fn timeout(duration: Duration) -> Self {
197 Self::new(
198 "timeout",
199 format!("Execution timed out after {duration:?}"),
200 true,
201 )
202 }
203
204 pub fn crash(message: impl Into<String>) -> Self {
206 Self::new("crash", message, false)
207 }
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct AdapterConfig {
213 pub name: String,
215 pub binary_path: PathBuf,
217 pub args: Vec<String>,
219 pub env: HashMap<String, String>,
221 pub timeout: Duration,
223 pub working_dir: Option<PathBuf>,
225}
226
227impl AdapterConfig {
228 pub fn new(name: impl Into<String>, binary_path: PathBuf) -> Self {
230 Self {
231 name: name.into(),
232 binary_path,
233 args: Vec::new(),
234 env: HashMap::new(),
235 timeout: Duration::from_secs(300), working_dir: None,
237 }
238 }
239
240 #[must_use]
242 pub fn with_timeout(mut self, timeout: Duration) -> Self {
243 self.timeout = timeout;
244 self
245 }
246
247 #[must_use]
249 pub fn with_arg(mut self, arg: impl Into<String>) -> Self {
250 self.args.push(arg.into());
251 self
252 }
253
254 #[must_use]
256 pub fn with_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
257 self.env.insert(key.into(), value.into());
258 self
259 }
260}
261
262pub trait RuntimeAdapter: Send + Sync {
266 fn name(&self) -> &str;
268
269 fn initialize(&mut self) -> Result<(), RuntimeError>;
271
272 fn prepare(&mut self, task_id: Uuid, worktree: &Path) -> Result<(), RuntimeError>;
274
275 fn execute(&mut self, input: ExecutionInput) -> Result<ExecutionReport, RuntimeError>;
277
278 fn terminate(&mut self) -> Result<(), RuntimeError>;
280
281 fn config(&self) -> &AdapterConfig;
283}
284
285#[derive(Debug)]
287pub struct MockAdapter {
288 config: AdapterConfig,
289 prepared: bool,
290 response: Option<ExecutionReport>,
291}
292
293impl MockAdapter {
294 pub fn new() -> Self {
296 Self {
297 config: AdapterConfig::new("mock", PathBuf::from("/bin/echo")),
298 prepared: false,
299 response: None,
300 }
301 }
302
303 #[must_use]
305 pub fn with_response(mut self, report: ExecutionReport) -> Self {
306 self.response = Some(report);
307 self
308 }
309}
310
311impl Default for MockAdapter {
312 fn default() -> Self {
313 Self::new()
314 }
315}
316
317impl RuntimeAdapter for MockAdapter {
318 fn name(&self) -> &str {
319 &self.config.name
320 }
321
322 fn initialize(&mut self) -> Result<(), RuntimeError> {
323 Ok(())
324 }
325
326 fn prepare(&mut self, _task_id: Uuid, _worktree: &Path) -> Result<(), RuntimeError> {
327 self.prepared = true;
328 Ok(())
329 }
330
331 fn execute(&mut self, _input: ExecutionInput) -> Result<ExecutionReport, RuntimeError> {
332 if !self.prepared {
333 return Err(RuntimeError::new(
334 "not_prepared",
335 "Adapter not prepared",
336 false,
337 ));
338 }
339
340 Ok(self.response.clone().unwrap_or_else(|| {
341 ExecutionReport::success(
342 Duration::from_secs(1),
343 "mock output".to_string(),
344 String::new(),
345 )
346 }))
347 }
348
349 fn terminate(&mut self) -> Result<(), RuntimeError> {
350 self.prepared = false;
351 Ok(())
352 }
353
354 fn config(&self) -> &AdapterConfig {
355 &self.config
356 }
357}
358
359#[cfg(test)]
360mod tests {
361 use super::*;
362
363 #[test]
364 fn execution_input_creation() {
365 let input = ExecutionInput {
366 task_description: "Write a test".to_string(),
367 success_criteria: "Test passes".to_string(),
368 context: None,
369 prior_attempts: Vec::new(),
370 verifier_feedback: None,
371 };
372
373 assert!(input.prior_attempts.is_empty());
374 }
375
376 #[test]
377 fn execution_report_success() {
378 let report =
379 ExecutionReport::success(Duration::from_secs(5), "output".to_string(), String::new());
380
381 assert!(report.is_success());
382 assert_eq!(report.exit_code, 0);
383 }
384
385 #[test]
386 fn execution_report_failure() {
387 let report = ExecutionReport::failure(
388 1,
389 Duration::from_secs(2),
390 RuntimeError::new("test_error", "Test failed", false),
391 );
392
393 assert!(!report.is_success());
394 assert_eq!(report.exit_code, 1);
395 }
396
397 #[test]
398 fn adapter_config_builder() {
399 let config = AdapterConfig::new("test", PathBuf::from("/bin/test"))
400 .with_timeout(Duration::from_secs(60))
401 .with_arg("--verbose")
402 .with_env("DEBUG", "true");
403
404 assert_eq!(config.name, "test");
405 assert_eq!(config.timeout, Duration::from_secs(60));
406 assert_eq!(config.args, vec!["--verbose"]);
407 assert_eq!(config.env.get("DEBUG"), Some(&"true".to_string()));
408 }
409
410 #[test]
411 fn mock_adapter_lifecycle() {
412 let mut adapter = MockAdapter::new();
413
414 assert!(adapter.initialize().is_ok());
415
416 let worktree = PathBuf::from("/tmp/test");
417 let task_id = Uuid::new_v4();
418
419 adapter.prepare(task_id, &worktree).unwrap();
420
421 let input = ExecutionInput {
422 task_description: "Test task".to_string(),
423 success_criteria: "Done".to_string(),
424 context: None,
425 prior_attempts: Vec::new(),
426 verifier_feedback: None,
427 };
428
429 let report = adapter.execute(input).unwrap();
430 assert!(report.is_success());
431
432 adapter.terminate().unwrap();
433 }
434
435 #[test]
436 fn mock_adapter_custom_response() {
437 let custom_report = ExecutionReport::failure(
438 1,
439 Duration::from_secs(3),
440 RuntimeError::new("custom", "Custom error", true),
441 );
442
443 let mut adapter = MockAdapter::new().with_response(custom_report);
444 adapter
445 .prepare(Uuid::new_v4(), &PathBuf::from("/tmp"))
446 .unwrap();
447
448 let input = ExecutionInput {
449 task_description: "Test".to_string(),
450 success_criteria: "Done".to_string(),
451 context: None,
452 prior_attempts: Vec::new(),
453 verifier_feedback: None,
454 };
455
456 let report = adapter.execute(input).unwrap();
457 assert!(!report.is_success());
458 }
459
460 #[test]
461 fn runtime_error_types() {
462 let timeout = RuntimeError::timeout(Duration::from_secs(60));
463 assert_eq!(timeout.code, "timeout");
464 assert!(timeout.recoverable);
465
466 let crash = RuntimeError::crash("Segmentation fault");
467 assert_eq!(crash.code, "crash");
468 assert!(!crash.recoverable);
469 }
470}