Skip to main content

lash_core/runtime/
environment.rs

1//! Shared process-level infrastructure for lash embedders.
2//!
3//! `RuntimeEnvironment` is the type an embedder constructs ONCE at
4//! startup and reuses across every `LashRuntime` instance it spawns.
5//! Fields are all `Arc`-wrapped or cheap-to-clone so building a runtime
6//! from an environment never rebuilds expensive state (plugin host,
7//! prompt layer, …).
8//!
9//! Three embedder patterns this enables:
10//!
11//! * **CLI interactive (single runtime, default):** `RuntimeEnvironment::default()`.
12//!   Behaviour byte-identical to the pre-environment world.
13//! * **Long autonomous agent:** set `residency` to `ActivePathOnly`,
14//!   then have the host periodically call
15//!   `runtime.orphaned_node_ids()` + `store.tombstone_nodes(...)` +
16//!   `store.vacuum()` on its own schedule. lash owns RAM; the host owns
17//!   disk lifecycle.
18//! * **Webserver multi-tenant:** one `RuntimeEnvironment` per process,
19//!   `residency: ActivePathOnly`, and `park()` / `resume()` per
20//!   request. HTTP connection pooling is a provider concern —
21//!   provider crates accept an optional shared HTTP client in
22//!   their constructors, so the host can share one pool across every
23//!   materialized provider.
24
25use std::path::PathBuf;
26use std::sync::Arc;
27
28use lash_trace::{JsonlTraceSink, TraceContext, TraceLevel, TraceSink};
29
30use super::TerminationPolicy;
31use super::host::BackgroundTaskHost;
32
33/// Where session nodes live at runtime.
34///
35/// lash owns RAM; the host owns disk lifecycle. Under `ActivePathOnly`
36/// the runtime trims orphans from memory on load, but disk-side
37/// retention (tombstoning + vacuum) is the host's decision — call
38/// `LashRuntime::orphaned_node_ids` when you want the current orphan
39/// set and feed it into `store.tombstone_nodes` / `store.vacuum` on
40/// whatever schedule fits your deployment.
41#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
42pub enum Residency {
43    /// Every node resident in RAM. Default. Best for interactive /
44    /// branching UX where the user may rewind.
45    #[default]
46    KeepAll,
47    /// Only nodes reachable from `leaf_node_id` are resident. Orphans
48    /// live on disk and are loaded on demand via
49    /// `LashRuntime::get_historic_node`. Best for webserver embedders
50    /// with many concurrent idle sessions, and for autonomous agents
51    /// (combined with host-scheduled `tombstone_nodes` + `vacuum`).
52    ActivePathOnly,
53}
54
55/// Shared runtime infrastructure an embedder builds once and reuses
56/// across every `LashRuntime` it constructs.
57///
58/// Cloning is cheap — every field is either `Arc`-wrapped or small.
59/// Default values preserve legacy behaviour so existing embedders can
60/// adopt incrementally.
61#[derive(Clone)]
62pub struct RuntimeEnvironment {
63    // Shared plugin infrastructure. Created once; every session's
64    // `PluginSession` is built from it via `PluginHost::build_session`.
65    pub plugin_host: Option<Arc<crate::PluginHost>>,
66
67    // RAM footprint policy for the session graph. Default `KeepAll`
68    // matches legacy behaviour. Webserver and autonomous-agent
69    // embedders set `ActivePathOnly`; disk lifecycle is then the
70    // host's responsibility via `orphaned_node_ids` + `tombstone_nodes`
71    // + `vacuum`.
72    pub residency: Residency,
73
74    // Host-owned background task lifecycle and local execution support.
75    pub background_task_host: Option<Arc<dyn BackgroundTaskHost>>,
76
77    // Store factory used by managed child sessions created from runtimes
78    // built with this environment.
79    pub session_store_factory: Option<Arc<dyn crate::SessionStoreFactory>>,
80
81    // All fields below mirror `RuntimeCoreConfig` and carry the same
82    // semantics. They live on `RuntimeEnvironment` directly so
83    // embedders don't have to build a separate core config.
84    pub attachment_store: Arc<dyn crate::AttachmentStore>,
85    pub prompt: crate::PromptLayer,
86    pub trace_sink: Option<Arc<dyn TraceSink>>,
87    pub trace_level: TraceLevel,
88    pub trace_context: TraceContext,
89    pub termination: TerminationPolicy,
90}
91
92impl Default for RuntimeEnvironment {
93    fn default() -> Self {
94        Self {
95            plugin_host: None,
96            residency: Residency::default(),
97            background_task_host: None,
98            session_store_factory: None,
99            attachment_store: Arc::new(crate::InMemoryAttachmentStore::new()),
100            prompt: crate::PromptLayer::new(),
101            trace_sink: None,
102            trace_level: TraceLevel::Standard,
103            trace_context: TraceContext::default(),
104            termination: TerminationPolicy::default(),
105        }
106    }
107}
108
109impl RuntimeEnvironment {
110    pub fn builder() -> RuntimeEnvironmentBuilder {
111        RuntimeEnvironmentBuilder::default()
112    }
113}
114
115/// Lightweight handle returned by `LashRuntime::park`. Holds no graph
116/// nodes, no plugin session, no HTTP client — just enough to
117/// `LashRuntime::resume` later. Cheap to cache per-session on a
118/// webserver; bounded memory cost regardless of session history size.
119pub struct ParkedSession {
120    pub(crate) session_id: String,
121    pub(crate) store: Arc<dyn crate::store::RuntimePersistence>,
122    pub(crate) policy: crate::SessionPolicy,
123}
124
125impl ParkedSession {
126    pub fn session_id(&self) -> &str {
127        &self.session_id
128    }
129}
130
131/// Fluent builder for `RuntimeEnvironment`.
132#[derive(Default)]
133pub struct RuntimeEnvironmentBuilder {
134    env: RuntimeEnvironment,
135}
136
137impl RuntimeEnvironmentBuilder {
138    pub fn with_plugin_host(mut self, host: Arc<crate::PluginHost>) -> Self {
139        self.env.plugin_host = Some(if self.env.background_task_host.is_some() {
140            Arc::new(host.as_ref().clone().with_background_tasks())
141        } else {
142            host
143        });
144        self
145    }
146
147    pub fn with_residency(mut self, residency: Residency) -> Self {
148        self.env.residency = residency;
149        self
150    }
151
152    pub fn with_background_task_host(
153        mut self,
154        background_task_host: Arc<dyn BackgroundTaskHost>,
155    ) -> Self {
156        self.env.background_task_host = Some(background_task_host);
157        if let Some(host) = self.env.plugin_host.take() {
158            self.env.plugin_host = Some(Arc::new(host.as_ref().clone().with_background_tasks()));
159        }
160        self
161    }
162
163    pub fn with_session_store_factory(
164        mut self,
165        factory: Arc<dyn crate::SessionStoreFactory>,
166    ) -> Self {
167        self.env.session_store_factory = Some(factory);
168        self
169    }
170
171    pub fn with_attachment_store(mut self, store: Arc<dyn crate::AttachmentStore>) -> Self {
172        self.env.attachment_store = store;
173        self
174    }
175
176    pub fn with_prompt_template(mut self, template: crate::PromptTemplate) -> Self {
177        self.env.prompt.template = Some(template);
178        self
179    }
180
181    pub fn with_prompt_contribution(mut self, contribution: crate::PromptContribution) -> Self {
182        self.env.prompt.add_contribution(contribution);
183        self
184    }
185
186    pub fn with_replaced_prompt_slot(
187        mut self,
188        slot: crate::PromptSlot,
189        contributions: impl IntoIterator<Item = crate::PromptContribution>,
190    ) -> Self {
191        self.env.prompt.replace_slot(slot, contributions);
192        self
193    }
194
195    pub fn with_cleared_prompt_slot(mut self, slot: crate::PromptSlot) -> Self {
196        self.env.prompt.clear_slot(slot);
197        self
198    }
199
200    pub fn with_prompt_layer(mut self, prompt: crate::PromptLayer) -> Self {
201        self.env.prompt = prompt;
202        self
203    }
204
205    pub fn with_trace_jsonl_path(mut self, path: Option<PathBuf>) -> Self {
206        self.env.trace_sink = path.map(|p| Arc::new(JsonlTraceSink::new(p)) as Arc<dyn TraceSink>);
207        self
208    }
209
210    pub fn with_trace_sink(mut self, sink: Option<Arc<dyn TraceSink>>) -> Self {
211        self.env.trace_sink = sink;
212        self
213    }
214
215    pub fn with_trace_level(mut self, level: TraceLevel) -> Self {
216        self.env.trace_level = level;
217        self
218    }
219
220    pub fn with_trace_context(mut self, context: TraceContext) -> Self {
221        self.env.trace_context = context;
222        self
223    }
224
225    pub fn with_termination(mut self, termination: TerminationPolicy) -> Self {
226        self.env.termination = termination;
227        self
228    }
229
230    pub fn build(self) -> RuntimeEnvironment {
231        self.env
232    }
233}