Skip to main content

lash_core/runtime/
builder.rs

1use std::sync::Arc;
2
3use crate::plugin::{PluginFactory, PluginHost, PluginSession};
4use crate::{
5    EffectHost, EmbeddedRuntimeHost, LashRuntime, PluginStack, ProcessRegistry, Residency,
6    RuntimeHostConfig, RuntimePersistence, RuntimeSessionState, SessionError, SessionPolicy,
7    SessionStoreFactory, TerminationPolicy,
8};
9
10enum PluginSource {
11    Host(PluginHost),
12    Session(Arc<PluginSession>),
13}
14
15pub(super) fn lashlang_abilities_for_process_registry(
16    mut abilities: lashlang::LashlangAbilities,
17    process_registry_available: bool,
18) -> lashlang::LashlangAbilities {
19    abilities = abilities.with_sleep();
20    if process_registry_available {
21        abilities.with_processes().with_process_signals()
22    } else {
23        abilities.processes = false;
24        abilities.process_signals = false;
25        abilities
26    }
27}
28
29pub struct EmbeddedRuntimeBuilder {
30    session_id: Option<String>,
31    policy: Option<SessionPolicy>,
32    plugin_options: crate::PluginOptions,
33    initial_state: Option<RuntimeSessionState>,
34    plugin_source: PluginSource,
35    core: RuntimeHostConfig,
36    session_store_factory: Option<Arc<dyn SessionStoreFactory>>,
37    host_event_store: Option<Arc<dyn crate::HostEventStore>>,
38    store: Option<Arc<dyn RuntimePersistence>>,
39    process_registry: Option<Arc<dyn ProcessRegistry>>,
40    residency: Residency,
41}
42
43impl Default for EmbeddedRuntimeBuilder {
44    fn default() -> Self {
45        Self {
46            session_id: None,
47            policy: None,
48            plugin_options: crate::PluginOptions::default(),
49            initial_state: None,
50            plugin_source: PluginSource::Host(PluginHost::empty()),
51            // `RuntimeHostConfig` has no `Default`; start from an explicitly
52            // named in-memory core. Callers that need durable stores override
53            // it with `with_runtime_host`.
54            core: RuntimeHostConfig::in_memory(),
55            session_store_factory: None,
56            host_event_store: Some(Arc::new(crate::InMemoryHostEventStore::default())),
57            store: None,
58            process_registry: None,
59            residency: Residency::default(),
60        }
61    }
62}
63
64impl EmbeddedRuntimeBuilder {
65    pub fn new() -> Self {
66        Self::default()
67    }
68
69    pub fn session_id(&self) -> Option<&str> {
70        self.session_id.as_deref()
71    }
72
73    pub fn policy(&self) -> Option<&SessionPolicy> {
74        self.policy.as_ref()
75    }
76
77    pub fn with_session_id(mut self, session_id: impl Into<String>) -> Self {
78        self.session_id = Some(session_id.into());
79        self
80    }
81
82    pub fn with_policy(mut self, policy: SessionPolicy) -> Self {
83        self.policy = Some(policy);
84        self
85    }
86
87    pub fn with_plugin_options(mut self, plugin_options: crate::PluginOptions) -> Self {
88        self.plugin_options = plugin_options;
89        self
90    }
91
92    pub fn with_initial_state(mut self, state: RuntimeSessionState) -> Self {
93        self.initial_state = Some(state);
94        self
95    }
96
97    pub fn with_plugin_host(mut self, plugin_host: PluginHost) -> Self {
98        self.plugin_source = PluginSource::Host(plugin_host);
99        self
100    }
101
102    pub fn with_plugin_session(mut self, plugin_session: Arc<PluginSession>) -> Self {
103        self.plugin_source = PluginSource::Session(plugin_session);
104        self
105    }
106
107    pub fn with_plugin_factories(mut self, factories: Vec<Arc<dyn PluginFactory>>) -> Self {
108        let host = PluginHost::new(factories);
109        self.plugin_source = PluginSource::Host(host);
110        self
111    }
112
113    pub fn with_plugin_stack(self, stack: PluginStack) -> Self {
114        self.with_plugin_factories(stack.into_factories())
115    }
116
117    pub fn with_runtime_host(mut self, core: RuntimeHostConfig) -> Self {
118        self.core = core;
119        self
120    }
121
122    pub fn with_attachment_store(
123        mut self,
124        attachment_store: Arc<dyn crate::AttachmentStore>,
125    ) -> Self {
126        self.core.durability.attachment_store = attachment_store;
127        self
128    }
129
130    pub fn with_prompt_template(mut self, prompt_template: crate::PromptTemplate) -> Self {
131        self.core.prompt.prompt.template = Some(prompt_template);
132        self
133    }
134
135    pub fn with_prompt_contribution(mut self, contribution: crate::PromptContribution) -> Self {
136        self.core.prompt.prompt.add_contribution(contribution);
137        self
138    }
139
140    pub fn with_replaced_prompt_slot(
141        mut self,
142        slot: crate::PromptSlot,
143        contributions: impl IntoIterator<Item = crate::PromptContribution>,
144    ) -> Self {
145        self.core.prompt.prompt.replace_slot(slot, contributions);
146        self
147    }
148
149    pub fn with_cleared_prompt_slot(mut self, slot: crate::PromptSlot) -> Self {
150        self.core.prompt.prompt.clear_slot(slot);
151        self
152    }
153
154    pub fn with_prompt_layer(mut self, prompt: crate::PromptLayer) -> Self {
155        self.core.prompt.prompt = prompt;
156        self
157    }
158
159    pub fn with_trace_sink(mut self, sink: Option<Arc<dyn lash_trace::TraceSink>>) -> Self {
160        self.core.tracing.trace_sink = sink;
161        self
162    }
163
164    pub fn with_lashlang_execution_sink(
165        mut self,
166        sink: Option<Arc<dyn lash_trace::TraceSink>>,
167    ) -> Self {
168        self.core.tracing.lashlang_execution_sink = sink;
169        self
170    }
171
172    pub fn with_lashlang_execution_jsonl_path(mut self, path: Option<std::path::PathBuf>) -> Self {
173        self.core.tracing.lashlang_execution_sink = path.map(|path| {
174            Arc::new(lash_trace::JsonlTraceSink::new(path)) as Arc<dyn lash_trace::TraceSink>
175        });
176        self
177    }
178
179    pub fn with_trace_level(mut self, level: lash_trace::TraceLevel) -> Self {
180        self.core.tracing.trace_level = level;
181        self
182    }
183
184    pub fn with_trace_context(mut self, context: lash_trace::TraceContext) -> Self {
185        self.core.tracing.trace_context = context;
186        self
187    }
188
189    pub fn with_termination(mut self, termination: TerminationPolicy) -> Self {
190        self.core.control.termination = termination;
191        self
192    }
193
194    pub fn with_effect_host(mut self, effect_host: Arc<dyn EffectHost>) -> Self {
195        self.core.control.effect_host = effect_host;
196        self
197    }
198
199    pub fn with_provider_resolver(
200        mut self,
201        provider_resolver: Arc<dyn crate::RuntimeProviderResolver>,
202    ) -> Self {
203        self.core.providers.provider_resolver = provider_resolver;
204        self
205    }
206
207    pub fn with_session_store_factory(
208        mut self,
209        session_store_factory: Arc<dyn SessionStoreFactory>,
210    ) -> Self {
211        self.session_store_factory = Some(session_store_factory);
212        self
213    }
214
215    pub fn with_host_event_store(mut self, store: Arc<dyn crate::HostEventStore>) -> Self {
216        self.host_event_store = Some(store);
217        self
218    }
219
220    pub fn with_store(mut self, store: Arc<dyn RuntimePersistence>) -> Self {
221        self.store = Some(store);
222        self
223    }
224
225    pub fn with_process_registry(mut self, process_registry: Arc<dyn ProcessRegistry>) -> Self {
226        self.process_registry = Some(process_registry);
227        if let PluginSource::Host(host) = &mut self.plugin_source {
228            let abilities =
229                lashlang_abilities_for_process_registry(host.lashlang_abilities(), true);
230            *host = host.clone().with_lashlang_abilities(abilities);
231        }
232        self
233    }
234
235    /// Trim a rebuilt session's resident graph to match the host's residency.
236    ///
237    /// Defaults to [`Residency::KeepAll`]. Setting [`Residency::ActivePathOnly`]
238    /// makes a rebuilt runtime (e.g. a durable worker reconstructing a session to
239    /// run a background process) keep only the active path resident, matching the
240    /// live runtime's behavior instead of silently retaining the full graph.
241    pub fn with_residency(mut self, residency: Residency) -> Self {
242        self.residency = residency;
243        self
244    }
245
246    fn resolve_state_from_defaults(&self) -> RuntimeSessionState {
247        let mut state = self.initial_state.clone().unwrap_or_default();
248        if let Some(session_id) = &self.session_id {
249            state.session_id = session_id.clone();
250        }
251        if let Some(policy) = &self.policy {
252            state.policy = policy.clone();
253        }
254        state
255    }
256
257    async fn resolve_state(&self) -> Result<RuntimeSessionState, SessionError> {
258        if let Some(state) = &self.initial_state {
259            return Ok({
260                let mut state = state.clone();
261                if let Some(session_id) = &self.session_id {
262                    state.session_id = session_id.clone();
263                }
264                if let Some(policy) = &self.policy {
265                    let recorded_provider_id = state.policy.recorded_provider_id().to_string();
266                    state.policy.provider_id = recorded_provider_id;
267                    state.policy.session_id = policy.session_id.clone();
268                    if state.policy.model.id.trim().is_empty() {
269                        state.policy.model = policy.model.clone();
270                    }
271                }
272                state
273            });
274        }
275        if let Some(store) = &self.store {
276            if let Some(mut state) = crate::store::load_persisted_session_state(store.as_ref())
277                .await
278                .map_err(|err| SessionError::Protocol(format!("failed to load store: {err}")))?
279            {
280                if let Some(session_id) = &self.session_id
281                    && &state.session_id != session_id
282                {
283                    return Err(SessionError::Protocol(format!(
284                        "store is bound to session `{}` but builder requested `{session_id}`",
285                        state.session_id
286                    )));
287                }
288                if let Some(policy) = &self.policy {
289                    let recorded_provider_id = state.policy.recorded_provider_id().to_string();
290                    state.policy.provider_id = recorded_provider_id;
291                    state.policy.session_id = policy.session_id.clone();
292                    if state.policy.model.id.trim().is_empty() {
293                        state.policy.model = policy.model.clone();
294                    }
295                }
296                return Ok(state);
297            }
298            let mut state = self.resolve_state_from_defaults();
299            if let Some(policy) = &self.policy {
300                state.policy = policy.clone();
301            }
302            return Ok(state);
303        }
304        Ok(self.resolve_state_from_defaults())
305    }
306
307    fn resolve_plugins(
308        &self,
309        state: &RuntimeSessionState,
310    ) -> Result<Arc<PluginSession>, SessionError> {
311        match &self.plugin_source {
312            PluginSource::Session(session) => Ok(Arc::clone(session)),
313            PluginSource::Host(host) => host
314                .clone()
315                .with_lashlang_abilities(lashlang_abilities_for_process_registry(
316                    host.lashlang_abilities(),
317                    self.process_registry.is_some(),
318                ))
319                .isolated_registry()
320                .build_session_with_parent(
321                    state.session_id.clone(),
322                    None,
323                    None,
324                    crate::plugin::SessionAuthorityContext {
325                        plugin_options: self.plugin_options.clone(),
326                        ..crate::plugin::SessionAuthorityContext::default()
327                    },
328                )
329                .map_err(|err| SessionError::Protocol(err.to_string())),
330        }
331    }
332
333    pub async fn build(self) -> Result<LashRuntime, SessionError> {
334        let state = self.resolve_state().await?;
335        let plugins = self.resolve_plugins(&state)?;
336        let embedded_host = EmbeddedRuntimeHost::new(self.core)
337            .with_session_store_factory_option(self.session_store_factory.clone())
338            .with_host_event_store_option(self.host_event_store.clone());
339        // `assemble_runtime` owns the (store, registry) wiring + residency so the
340        // worker rebuild cannot drift from the live open path.
341        LashRuntime::assemble_runtime(
342            state.policy.clone(),
343            embedded_host,
344            plugins,
345            self.store,
346            self.process_registry,
347            state,
348            self.residency,
349        )
350        .await
351    }
352
353    pub async fn build_ephemeral(mut self) -> Result<LashRuntime, SessionError> {
354        self.store = None;
355        self.build().await
356    }
357
358    pub async fn build_persistent(
359        mut self,
360        store: Arc<dyn RuntimePersistence>,
361    ) -> Result<LashRuntime, SessionError> {
362        self.store = Some(store);
363        self.build().await
364    }
365
366    pub async fn build_background_persistent(
367        mut self,
368        store: Arc<dyn RuntimePersistence>,
369        process_registry: Arc<dyn ProcessRegistry>,
370    ) -> Result<LashRuntime, SessionError> {
371        self.store = Some(store);
372        self = self.with_process_registry(process_registry);
373        self.build().await
374    }
375}
376
377impl LashRuntime {
378    pub fn builder() -> EmbeddedRuntimeBuilder {
379        EmbeddedRuntimeBuilder::new()
380    }
381}
382
383trait EmbeddedRuntimeHostExt {
384    fn with_session_store_factory_option(
385        self,
386        session_store_factory: Option<Arc<dyn SessionStoreFactory>>,
387    ) -> Self;
388
389    fn with_host_event_store_option(
390        self,
391        host_event_store: Option<Arc<dyn crate::HostEventStore>>,
392    ) -> Self;
393}
394
395impl EmbeddedRuntimeHostExt for EmbeddedRuntimeHost {
396    fn with_session_store_factory_option(
397        mut self,
398        session_store_factory: Option<Arc<dyn SessionStoreFactory>>,
399    ) -> Self {
400        self.session_store_factory = session_store_factory;
401        self
402    }
403
404    fn with_host_event_store_option(
405        mut self,
406        host_event_store: Option<Arc<dyn crate::HostEventStore>>,
407    ) -> Self {
408        self.host_event_store = host_event_store;
409        self
410    }
411}