Skip to main content

hivemind/adapters/
codex.rs

1//! Codex adapter - wrapper for Codex CLI runtime.
2
3use super::opencode::{OpenCodeAdapter, OpenCodeConfig};
4use super::runtime::{
5    AdapterConfig, ExecutionInput, ExecutionReport, InteractiveAdapterEvent,
6    InteractiveExecutionResult, RuntimeAdapter, RuntimeError,
7};
8use std::path::PathBuf;
9use std::time::Duration;
10use uuid::Uuid;
11
12/// Codex adapter configuration.
13#[derive(Debug, Clone)]
14pub struct CodexConfig {
15    /// Base adapter config.
16    pub base: AdapterConfig,
17    /// Optional model identifier.
18    pub model: Option<String>,
19}
20
21impl CodexConfig {
22    /// Creates a new codex config.
23    pub fn new(binary_path: PathBuf) -> Self {
24        Self {
25            base: AdapterConfig::new("codex", binary_path).with_timeout(Duration::from_secs(600)),
26            model: None,
27        }
28    }
29
30    /// Sets the model.
31    #[must_use]
32    pub fn with_model(mut self, model: impl Into<String>) -> Self {
33        self.model = Some(model.into());
34        self
35    }
36}
37
38impl Default for CodexConfig {
39    fn default() -> Self {
40        let mut cfg = Self::new(PathBuf::from("codex"));
41        cfg.base.args = vec![
42            "exec".to_string(),
43            "--skip-git-repo-check".to_string(),
44            "-".to_string(),
45        ];
46        cfg
47    }
48}
49
50/// Codex runtime adapter.
51pub struct CodexAdapter {
52    inner: OpenCodeAdapter,
53}
54
55impl CodexAdapter {
56    /// Creates a new adapter.
57    pub fn new(config: CodexConfig) -> Self {
58        let mut inner = OpenCodeConfig::new(config.base.binary_path.clone());
59        inner.base.name = "codex".to_string();
60        inner.base.args = config.base.args;
61        inner.base.env = config.base.env;
62        inner.base.timeout = config.base.timeout;
63        inner.model.clone_from(&config.model);
64
65        // Codex does not use OpenCode model flag semantics; append as explicit args when requested.
66        if let Some(model) = config.model {
67            let has_model_flag = inner
68                .base
69                .args
70                .iter()
71                .any(|a| a == "--model" || a.starts_with("--model="));
72            if !has_model_flag {
73                inner.base.args.extend(["--model".to_string(), model]);
74            }
75        }
76
77        Self {
78            inner: OpenCodeAdapter::new(inner),
79        }
80    }
81
82    /// Creates an adapter with defaults.
83    pub fn with_defaults() -> Self {
84        Self::new(CodexConfig::default())
85    }
86
87    pub fn execute_interactive<F>(
88        &mut self,
89        input: &ExecutionInput,
90        on_event: F,
91    ) -> Result<InteractiveExecutionResult, RuntimeError>
92    where
93        F: FnMut(InteractiveAdapterEvent) -> std::result::Result<(), String>,
94    {
95        self.inner.execute_interactive(input, on_event)
96    }
97}
98
99impl RuntimeAdapter for CodexAdapter {
100    fn name(&self) -> &str {
101        self.inner.name()
102    }
103
104    fn initialize(&mut self) -> Result<(), RuntimeError> {
105        self.inner.initialize()
106    }
107
108    fn prepare(&mut self, task_id: Uuid, worktree: &std::path::Path) -> Result<(), RuntimeError> {
109        self.inner.prepare(task_id, worktree)
110    }
111
112    fn execute(&mut self, input: ExecutionInput) -> Result<ExecutionReport, RuntimeError> {
113        self.inner.execute(input)
114    }
115
116    fn terminate(&mut self) -> Result<(), RuntimeError> {
117        self.inner.terminate()
118    }
119
120    fn config(&self) -> &AdapterConfig {
121        self.inner.config()
122    }
123}