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::path::Path;
9use std::path::PathBuf;
10use std::time::Duration;
11use uuid::Uuid;
12
13/// Input for runtime execution.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct ExecutionInput {
16    /// Task description/objective.
17    pub task_description: String,
18    /// Success criteria.
19    pub success_criteria: String,
20    /// Additional context.
21    pub context: Option<String>,
22    /// Prior attempt summaries (for retries).
23    pub prior_attempts: Vec<AttemptSummary>,
24    /// Verifier feedback (for retries).
25    pub verifier_feedback: Option<String>,
26}
27
28/// Summary of a prior attempt for context.
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct AttemptSummary {
31    /// Attempt number.
32    pub attempt_number: u32,
33    /// What was tried.
34    pub summary: String,
35    /// Why it failed.
36    pub failure_reason: Option<String>,
37}
38
39/// Report from runtime execution.
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct ExecutionReport {
42    /// Exit code from the runtime.
43    pub exit_code: i32,
44    /// Execution duration.
45    pub duration: Duration,
46    /// Captured stdout.
47    pub stdout: String,
48    /// Captured stderr.
49    pub stderr: String,
50    /// Files created during execution.
51    pub files_created: Vec<PathBuf>,
52    /// Files modified during execution.
53    pub files_modified: Vec<PathBuf>,
54    /// Files deleted during execution.
55    pub files_deleted: Vec<PathBuf>,
56    /// Any errors that occurred.
57    pub errors: Vec<RuntimeError>,
58}
59
60impl ExecutionReport {
61    /// Creates a successful execution report.
62    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    /// Creates a failed execution report.
76    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    /// Returns true if execution succeeded (exit code 0, no errors).
90    pub fn is_success(&self) -> bool {
91        self.exit_code == 0 && self.errors.is_empty()
92    }
93
94    /// Adds file changes to the input.
95    #[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/// Interactive runtime transport events.
110#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
111#[serde(tag = "type", rename_all = "snake_case")]
112pub enum InteractiveAdapterEvent {
113    /// Runtime emitted output content.
114    Output { content: String },
115    /// User input forwarded to runtime.
116    Input { content: String },
117    /// Runtime interrupted (for example by Ctrl+C).
118    Interrupted,
119}
120
121/// Interactive execution result metadata.
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct InteractiveExecutionResult {
124    /// Final execution report.
125    pub report: ExecutionReport,
126    /// Optional termination reason.
127    pub terminated_reason: Option<String>,
128}
129
130/// An error from the runtime.
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct RuntimeError {
133    /// Error code.
134    pub code: String,
135    /// Error message.
136    pub message: String,
137    /// Whether this error is recoverable.
138    pub recoverable: bool,
139}
140
141impl RuntimeError {
142    /// Creates a new runtime error.
143    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    /// Creates a timeout error.
152    pub fn timeout(duration: Duration) -> Self {
153        Self::new(
154            "timeout",
155            format!("Execution timed out after {duration:?}"),
156            true,
157        )
158    }
159
160    /// Creates a crash error.
161    pub fn crash(message: impl Into<String>) -> Self {
162        Self::new("crash", message, false)
163    }
164}
165
166/// Configuration for a runtime adapter.
167#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct AdapterConfig {
169    /// Name of the adapter.
170    pub name: String,
171    /// Path to the runtime binary.
172    pub binary_path: PathBuf,
173    /// Additional arguments to pass.
174    pub args: Vec<String>,
175    /// Environment variables.
176    pub env: HashMap<String, String>,
177    /// Execution timeout.
178    pub timeout: Duration,
179    /// Working directory (if different from worktree).
180    pub working_dir: Option<PathBuf>,
181}
182
183impl AdapterConfig {
184    /// Creates a new adapter config.
185    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), // 5 minutes default
192            working_dir: None,
193        }
194    }
195
196    /// Sets the timeout.
197    #[must_use]
198    pub fn with_timeout(mut self, timeout: Duration) -> Self {
199        self.timeout = timeout;
200        self
201    }
202
203    /// Adds an argument.
204    #[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    /// Sets an environment variable.
211    #[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
218/// Trait for runtime adapters.
219///
220/// All adapters must implement this trait to be usable by Hivemind.
221pub trait RuntimeAdapter: Send + Sync {
222    /// Returns the adapter name.
223    fn name(&self) -> &str;
224
225    /// Initializes the adapter.
226    fn initialize(&mut self) -> Result<(), RuntimeError>;
227
228    /// Prepares the adapter for execution.
229    fn prepare(&mut self, task_id: Uuid, worktree: &Path) -> Result<(), RuntimeError>;
230
231    /// Executes the runtime with the given input.
232    fn execute(&mut self, input: ExecutionInput) -> Result<ExecutionReport, RuntimeError>;
233
234    /// Terminates execution (if running).
235    fn terminate(&mut self) -> Result<(), RuntimeError>;
236
237    /// Returns the adapter configuration.
238    fn config(&self) -> &AdapterConfig;
239}
240
241/// A mock adapter for testing.
242#[derive(Debug)]
243pub struct MockAdapter {
244    config: AdapterConfig,
245    prepared: bool,
246    response: Option<ExecutionReport>,
247}
248
249impl MockAdapter {
250    /// Creates a new mock adapter.
251    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    /// Sets the response to return on execute.
260    #[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}