Skip to main content

hivemind/adapters/
runtime.rs

1//! Runtime adapter interface for execution backends.
2//!
3//! Runtime adapters are the only components that interact directly with
4//! execution runtimes (e.g. Claude Code, `OpenCode`, Codex CLI).
5
6use 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/// Input for runtime execution.
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct ExecutionInput {
17    /// Task description/objective.
18    pub task_description: String,
19    /// Success criteria.
20    pub success_criteria: String,
21    /// Additional context.
22    pub context: Option<String>,
23    /// Prior attempt summaries (for retries).
24    pub prior_attempts: Vec<AttemptSummary>,
25    /// Verifier feedback (for retries).
26    pub verifier_feedback: Option<String>,
27}
28
29/// Formats an execution input into the runtime prompt payload.
30#[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/// Summary of a prior attempt for context.
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct AttemptSummary {
64    /// Attempt number.
65    pub attempt_number: u32,
66    /// What was tried.
67    pub summary: String,
68    /// Why it failed.
69    pub failure_reason: Option<String>,
70}
71
72/// Report from runtime execution.
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct ExecutionReport {
75    /// Exit code from the runtime.
76    pub exit_code: i32,
77    /// Execution duration.
78    pub duration: Duration,
79    /// Captured stdout.
80    pub stdout: String,
81    /// Captured stderr.
82    pub stderr: String,
83    /// Files created during execution.
84    pub files_created: Vec<PathBuf>,
85    /// Files modified during execution.
86    pub files_modified: Vec<PathBuf>,
87    /// Files deleted during execution.
88    pub files_deleted: Vec<PathBuf>,
89    /// Any errors that occurred.
90    pub errors: Vec<RuntimeError>,
91}
92
93impl ExecutionReport {
94    /// Creates a successful execution report.
95    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    /// Creates a failed execution report.
109    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    /// Creates a failed execution report preserving runtime output.
114    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    /// Returns true if execution succeeded (exit code 0, no errors).
134    pub fn is_success(&self) -> bool {
135        self.exit_code == 0 && self.errors.is_empty()
136    }
137
138    /// Adds file changes to the input.
139    #[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/// Interactive runtime transport events.
154#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
155#[serde(tag = "type", rename_all = "snake_case")]
156pub enum InteractiveAdapterEvent {
157    /// Runtime emitted output content.
158    Output { content: String },
159    /// User input forwarded to runtime.
160    Input { content: String },
161    /// Runtime interrupted (for example by Ctrl+C).
162    Interrupted,
163}
164
165/// Interactive execution result metadata.
166#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct InteractiveExecutionResult {
168    /// Final execution report.
169    pub report: ExecutionReport,
170    /// Optional termination reason.
171    pub terminated_reason: Option<String>,
172}
173
174/// An error from the runtime.
175#[derive(Debug, Clone, Serialize, Deserialize)]
176pub struct RuntimeError {
177    /// Error code.
178    pub code: String,
179    /// Error message.
180    pub message: String,
181    /// Whether this error is recoverable.
182    pub recoverable: bool,
183}
184
185impl RuntimeError {
186    /// Creates a new runtime error.
187    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    /// Creates a timeout error.
196    pub fn timeout(duration: Duration) -> Self {
197        Self::new(
198            "timeout",
199            format!("Execution timed out after {duration:?}"),
200            true,
201        )
202    }
203
204    /// Creates a crash error.
205    pub fn crash(message: impl Into<String>) -> Self {
206        Self::new("crash", message, false)
207    }
208}
209
210/// Configuration for a runtime adapter.
211#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct AdapterConfig {
213    /// Name of the adapter.
214    pub name: String,
215    /// Path to the runtime binary.
216    pub binary_path: PathBuf,
217    /// Additional arguments to pass.
218    pub args: Vec<String>,
219    /// Environment variables.
220    pub env: HashMap<String, String>,
221    /// Execution timeout.
222    pub timeout: Duration,
223    /// Working directory (if different from worktree).
224    pub working_dir: Option<PathBuf>,
225}
226
227impl AdapterConfig {
228    /// Creates a new adapter config.
229    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), // 5 minutes default
236            working_dir: None,
237        }
238    }
239
240    /// Sets the timeout.
241    #[must_use]
242    pub fn with_timeout(mut self, timeout: Duration) -> Self {
243        self.timeout = timeout;
244        self
245    }
246
247    /// Adds an argument.
248    #[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    /// Sets an environment variable.
255    #[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
262/// Trait for runtime adapters.
263///
264/// All adapters must implement this trait to be usable by Hivemind.
265pub trait RuntimeAdapter: Send + Sync {
266    /// Returns the adapter name.
267    fn name(&self) -> &str;
268
269    /// Initializes the adapter.
270    fn initialize(&mut self) -> Result<(), RuntimeError>;
271
272    /// Prepares the adapter for execution.
273    fn prepare(&mut self, task_id: Uuid, worktree: &Path) -> Result<(), RuntimeError>;
274
275    /// Executes the runtime with the given input.
276    fn execute(&mut self, input: ExecutionInput) -> Result<ExecutionReport, RuntimeError>;
277
278    /// Terminates execution (if running).
279    fn terminate(&mut self) -> Result<(), RuntimeError>;
280
281    /// Returns the adapter configuration.
282    fn config(&self) -> &AdapterConfig;
283}
284
285/// A mock adapter for testing.
286#[derive(Debug)]
287pub struct MockAdapter {
288    config: AdapterConfig,
289    prepared: bool,
290    response: Option<ExecutionReport>,
291}
292
293impl MockAdapter {
294    /// Creates a new mock adapter.
295    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    /// Sets the response to return on execute.
304    #[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}