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