Skip to main content

noether_engine/executor/
composite.rs

1//! Composite executor: routes stages to the right executor by capability.
2//!
3//! Lookup order:
4//! 1. `NixExecutor`    — synthesized stages with `implementation_code`
5//! 2. `RuntimeExecutor`— LLM + store-aware stdlib stages
6//! 3. `InlineExecutor` — pure stdlib stages (function pointers)
7
8use super::inline::{InlineExecutor, InlineRegistry};
9use super::nix::NixExecutor;
10use super::runtime::RuntimeExecutor;
11use super::{ExecutionError, StageExecutor};
12use noether_core::stage::StageId;
13use noether_store::StageStore;
14use serde_json::Value;
15
16/// Executor that combines all three executor layers.
17pub struct CompositeExecutor {
18    inline: InlineExecutor,
19    nix: Option<NixExecutor>,
20    runtime: RuntimeExecutor,
21}
22
23impl CompositeExecutor {
24    /// Build from a store using only the built-in stdlib implementations.
25    /// `NixExecutor` is included only when `nix` is available in `PATH`.
26    pub fn from_store(store: &dyn StageStore) -> Self {
27        Self::from_store_with_registry(store, InlineRegistry::new())
28    }
29
30    /// Build from a store, augmenting the stdlib with additional inline
31    /// stage implementations from `registry`.
32    ///
33    /// Use this when your project needs Pure Rust stage implementations
34    /// without modifying `noether-core`.  See [`InlineRegistry`] for usage.
35    pub fn from_store_with_registry(store: &dyn StageStore, registry: InlineRegistry) -> Self {
36        let inline = InlineExecutor::from_store_with_registry(store, registry);
37        let nix = NixExecutor::from_store(store);
38        let runtime = RuntimeExecutor::from_store(store);
39
40        if nix.is_some() {
41            eprintln!("Nix executor: active (synthesized stages will run via nix)");
42        }
43
44        Self {
45            inline,
46            nix,
47            runtime,
48        }
49    }
50
51    /// Attach an LLM provider so `llm_complete` / `llm_classify` / `llm_extract`
52    /// stages are actually executed instead of returning a config error.
53    pub fn with_llm(
54        mut self,
55        llm: Box<dyn crate::llm::LlmProvider>,
56        config: crate::llm::LlmConfig,
57    ) -> Self {
58        self.runtime.set_llm(llm, config);
59        self
60    }
61
62    /// Attach an embedding provider so `llm_embed` uses real embeddings and
63    /// `store_search` uses cosine similarity instead of substring matching.
64    pub fn with_embedding(
65        mut self,
66        provider: Box<dyn crate::index::embedding::EmbeddingProvider>,
67    ) -> Self {
68        self.runtime = self.runtime.with_embedding(provider);
69        self
70    }
71
72    /// Register a freshly synthesized stage so it can be executed immediately
73    /// without reloading the store.
74    pub fn register_synthesized(&mut self, stage_id: &StageId, code: &str, language: &str) {
75        if let Some(nix) = &mut self.nix {
76            nix.register(stage_id, code, language);
77        }
78    }
79
80    /// True when `nix` is available and will handle synthesized stages.
81    pub fn nix_available(&self) -> bool {
82        self.nix.is_some()
83    }
84}
85
86impl StageExecutor for CompositeExecutor {
87    fn execute(&self, stage_id: &StageId, input: &Value) -> Result<Value, ExecutionError> {
88        // 1. Synthesized stages (have implementation_code stored) → Nix
89        if let Some(nix) = &self.nix {
90            if nix.has_implementation(stage_id) {
91                return nix.execute(stage_id, input);
92            }
93        }
94        // 2. LLM + store-aware stages → RuntimeExecutor
95        if self.runtime.has_implementation(stage_id) {
96            return self.runtime.execute(stage_id, input);
97        }
98        // 3. Pure stdlib + registered extra stages → InlineExecutor
99        self.inline.execute(stage_id, input)
100    }
101}