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 /// Replace the isolation backend on the embedded NixExecutor.
52 /// No-op when Nix isn't installed (synthesized stages can't run
53 /// anyway, so the sandbox question doesn't arise).
54 pub fn with_isolation(mut self, backend: super::isolation::IsolationBackend) -> Self {
55 if let Some(nix) = self.nix.take() {
56 use super::nix::{NixConfig, NixExecutor};
57 // Rebuild NixExecutor with the new isolation setting.
58 // NixExecutor doesn't expose a public with_isolation setter
59 // today because the config field was just added; use the
60 // builder pattern via `NixConfig::with_isolation` at
61 // construction time. Reconstruct by re-reading the existing
62 // config and swapping the backend.
63 let old_config = nix.config_snapshot();
64 let new_config = NixConfig {
65 isolation: backend,
66 ..old_config
67 };
68 self.nix = NixExecutor::rebuild_with_config(nix, new_config);
69 }
70 self
71 }
72
73 /// Attach an LLM provider so `llm_complete` / `llm_classify` / `llm_extract`
74 /// stages are actually executed instead of returning a config error.
75 pub fn with_llm(
76 mut self,
77 llm: Box<dyn crate::llm::LlmProvider>,
78 config: crate::llm::LlmConfig,
79 ) -> Self {
80 self.runtime.set_llm(llm, config);
81 self
82 }
83
84 /// Attach an embedding provider so `llm_embed` uses real embeddings and
85 /// `store_search` uses cosine similarity instead of substring matching.
86 pub fn with_embedding(
87 mut self,
88 provider: Box<dyn crate::index::embedding::EmbeddingProvider>,
89 ) -> Self {
90 self.runtime = self.runtime.with_embedding(provider);
91 self
92 }
93
94 /// Register a freshly synthesized stage so it can be executed
95 /// immediately without reloading the store. The caller **must**
96 /// supply the stage's declared effects — the isolation policy is
97 /// derived from them, and silently defaulting to
98 /// [`EffectSet::pure`](noether_core::effects::EffectSet::pure)
99 /// would put a Network-effect stage into a no-network sandbox and
100 /// surface as an opaque DNS failure at runtime.
101 pub fn register_synthesized(
102 &mut self,
103 stage_id: &StageId,
104 code: &str,
105 language: &str,
106 effects: noether_core::effects::EffectSet,
107 ) {
108 if let Some(nix) = &mut self.nix {
109 nix.register_with_effects(stage_id, code, language, effects);
110 }
111 }
112
113 /// True when `nix` is available and will handle synthesized stages.
114 pub fn nix_available(&self) -> bool {
115 self.nix.is_some()
116 }
117}
118
119impl StageExecutor for CompositeExecutor {
120 fn execute(&self, stage_id: &StageId, input: &Value) -> Result<Value, ExecutionError> {
121 // 1. Synthesized stages (have implementation_code stored) → Nix
122 if let Some(nix) = &self.nix {
123 if nix.has_implementation(stage_id) {
124 return nix.execute(stage_id, input);
125 }
126 }
127 // 2. LLM + store-aware stages → RuntimeExecutor
128 if self.runtime.has_implementation(stage_id) {
129 return self.runtime.execute(stage_id, input);
130 }
131 // 3. Pure stdlib + registered extra stages → InlineExecutor
132 self.inline.execute(stage_id, input)
133 }
134}