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 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 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 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}