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