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