Skip to main content

sqlite_graphrag/spawn/
mod.rs

1//! Spawn subsystem abstraction (v1.0.75 — G22 solution)
2//!
3//! Provides `VersionAdapter` trait that detects the version of external CLI
4//! executors (claude code, codex CLI, opencode headless) and adapts flags,
5//! schema and error handling accordingly.
6
7pub mod claude_adapter;
8pub mod codex_adapter;
9pub mod compat_matrix;
10pub mod env_whitelist;
11pub mod error_propagator;
12pub mod executor_version;
13pub mod opencode_adapter;
14
15use crate::errors::AppError;
16use async_trait::async_trait;
17use executor_version::ExecutorVersion;
18use std::collections::BTreeMap;
19use std::process::Stdio;
20
21/// Result of parsing a subprocess output stream.
22#[derive(Debug, Clone)]
23pub struct ParsedOutput {
24    pub items: Vec<serde_json::Value>,
25    pub raw_stdout: String,
26    pub raw_stderr: String,
27    pub exit_code: i32,
28}
29
30/// Detected capability of a given executor version.
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct ExecutorCapabilities {
33    pub supports_mcp_map: bool,
34    pub supports_ask_for_approval_flag: bool,
35    pub supports_strict_schema: bool,
36    pub default_flags: Vec<String>,
37    pub removed_flags: Vec<String>,
38}
39
40impl ExecutorCapabilities {
41    pub fn empty() -> Self {
42        Self {
43            supports_mcp_map: false,
44            supports_ask_for_approval_flag: false,
45            supports_strict_schema: false,
46            default_flags: Vec::new(),
47            removed_flags: Vec::new(),
48        }
49    }
50}
51
52/// Trait for adapting spawn invocations to a particular executor's version.
53#[async_trait]
54pub trait VersionAdapter: Send + Sync {
55    /// Logical name of the executor (e.g. "codex", "claude", "opencode").
56    fn name(&self) -> &'static str;
57
58    /// Detect the version by invoking `<executor> --version` and parsing the output.
59    async fn detect(&self) -> Result<ExecutorVersion, AppError>;
60
61    /// Returns the capability matrix for the given version.
62    fn capabilities_for(&self, version: &ExecutorVersion) -> ExecutorCapabilities;
63
64    /// Build the CLI invocation arguments for a given prompt and capabilities.
65    fn build_args(
66        &self,
67        prompt: &str,
68        caps: &ExecutorCapabilities,
69        compat_mode: CompatMode,
70    ) -> Vec<String>;
71
72    /// Parses the executor output into structured items.
73    fn parse_output(&self, raw_stdout: &str, raw_stderr: &str, exit_code: i32) -> ParsedOutput;
74}
75
76/// Compatibility mode controlling how strict the adapter is with version drift.
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub enum CompatMode {
79    /// Abort on unknown versions
80    Strict,
81    /// Try the invocation anyway
82    Lenient,
83    /// Auto-detect and adapt (default)
84    Auto,
85}
86
87impl CompatMode {
88    pub fn parse(s: &str) -> Self {
89        match s.to_ascii_lowercase().as_str() {
90            "strict" => Self::Strict,
91            "lenient" => Self::Lenient,
92            _ => Self::Auto,
93        }
94    }
95}
96
97/// In-memory cache of `executor -> ExecutorVersion` to avoid re-spawning
98/// `--version` on every command. Resettable via `--executor-version-check`.
99#[derive(Debug, Default)]
100pub struct VersionCache {
101    inner: std::sync::Mutex<BTreeMap<String, ExecutorVersion>>,
102}
103
104impl VersionCache {
105    pub fn new() -> Self {
106        Self::default()
107    }
108
109    pub fn get(&self, name: &str) -> Option<ExecutorVersion> {
110        self.inner.lock().ok().and_then(|m| m.get(name).cloned())
111    }
112
113    pub fn put(&self, name: &str, version: ExecutorVersion) {
114        if let Ok(mut m) = self.inner.lock() {
115            m.insert(name.to_string(), version);
116        }
117    }
118
119    pub fn clear(&self) {
120        if let Ok(mut m) = self.inner.lock() {
121            m.clear();
122        }
123    }
124}
125
126static VERSION_CACHE: std::sync::OnceLock<VersionCache> = std::sync::OnceLock::new();
127
128pub fn global_version_cache() -> &'static VersionCache {
129    VERSION_CACHE.get_or_init(VersionCache::new)
130}
131
132/// Reusable tokio command builder for subprocess invocation.
133pub fn base_command(binary: &str) -> std::process::Command {
134    let mut cmd = std::process::Command::new(binary);
135    cmd.stdin(Stdio::null())
136        .stdout(Stdio::piped())
137        .stderr(Stdio::piped());
138    cmd
139}