1use crate::support::*;
2use lash_core::runtime::{
3 ProcessCommand, ProcessEffectOutcome, RuntimeEffectCommand, RuntimeEffectEnvelope,
4 RuntimeEffectKind, RuntimeEffectLocalExecutor, RuntimeEffectOutcome, RuntimeInvocation,
5 RuntimeScope,
6};
7
8#[derive(Clone)]
9pub struct LashCore {
10 pub(crate) env: RuntimeEnvironment,
11 pub(crate) policy: SessionPolicy,
12 pub(crate) protocol_factory: Option<Arc<dyn PluginFactory>>,
13 pub(crate) store_factory: Option<Arc<dyn SessionStoreFactory>>,
14 pub(crate) plugin_factories: Arc<Vec<Arc<dyn PluginFactory>>>,
15 pub(crate) provider: Option<ProviderHandle>,
16 pub(crate) live_replay_store: Arc<dyn LiveReplayStore>,
17 pub(crate) process_lifecycle_available: bool,
21 pub(crate) work_driver: Arc<InlineWorkDriverSlot>,
24}
25
26pub(crate) enum ProcessWorkDriverSetup {
29 None,
31 LazyDefault {
37 config: Box<DurableProcessWorkerConfig>,
38 },
39 External { driver: ProcessWorkDriver },
41}
42
43#[derive(Clone, Default)]
44pub(crate) enum ProcessWorkSource {
45 #[default]
46 None,
47 Inline {
48 registry: Arc<dyn ProcessRegistry>,
49 hub: Option<lash_core::ProcessChangeHub>,
50 },
51 External(ProcessWorkDriver),
52}
53
54impl ProcessWorkSource {
55 fn process_registry(&self) -> Option<Arc<dyn ProcessRegistry>> {
56 match self {
57 Self::None => None,
58 Self::Inline { registry, .. } => Some(Arc::clone(registry)),
59 Self::External(driver) => Some(driver.process_registry()),
60 }
61 }
62
63 fn has_registry(&self) -> bool {
64 !matches!(self, Self::None)
65 }
66
67 fn watched(self, sink: Option<Arc<dyn lash_core::ProcessEventSink>>) -> Self {
68 match self {
69 Self::Inline {
70 registry,
71 hub: None,
72 } => {
73 let (registry, hub) = lash_core::watch_process_registry_with_sink(registry, sink);
74 Self::Inline {
75 registry,
76 hub: Some(hub),
77 }
78 }
79 other => other,
83 }
84 }
85}
86
87#[derive(Clone, Default)]
88pub(crate) enum QueuedWorkSource {
89 None,
90 #[default]
91 LazyDefault,
92 External(QueuedWorkDriver),
93}
94
95pub(crate) enum QueuedWorkDriverSetup {
96 None,
97 LazyDefault {
98 config: Arc<InlineQueuedWorkRunConfig>,
99 },
100 External {
101 driver: QueuedWorkDriver,
102 },
103}
104
105pub(crate) struct InlineWorkDriverSetup {
106 process: ProcessWorkDriverSetup,
107 queued: QueuedWorkDriverSetup,
108}
109
110#[derive(Clone, Default)]
111pub(crate) struct ResolvedWorkDrivers {
112 pub(crate) process: Option<ProcessWorkDriver>,
113 pub(crate) queued: Option<QueuedWorkDriver>,
114 pub(crate) drive_process_on_open: bool,
115}
116
117pub(crate) struct InlineWorkDriverSlot {
123 setup: InlineWorkDriverSetup,
124 drivers: tokio::sync::OnceCell<ResolvedWorkDrivers>,
125 phase_probe_slot: Option<lash_core::runtime::RuntimeTurnPhaseProbeSlot>,
126}
127
128impl InlineWorkDriverSlot {
129 fn new(setup: InlineWorkDriverSetup) -> Self {
130 let phase_probe_slot = match &setup.process {
131 ProcessWorkDriverSetup::LazyDefault { config } => {
132 Some(config.turn_phase_probe_slot.clone())
133 }
134 ProcessWorkDriverSetup::None | ProcessWorkDriverSetup::External { .. } => None,
135 };
136 Self {
137 setup,
138 drivers: tokio::sync::OnceCell::new(),
139 phase_probe_slot,
140 }
141 }
142
143 pub(crate) async fn drivers(&self) -> ResolvedWorkDrivers {
146 self.drivers
147 .get_or_init(|| async {
148 let queued = match &self.setup.queued {
149 QueuedWorkDriverSetup::None => None,
150 QueuedWorkDriverSetup::External { driver } => Some(driver.clone()),
151 QueuedWorkDriverSetup::LazyDefault { config } => Some(QueuedWorkDriver::new(
152 Arc::new(InlineQueuedWorkRunHandle::new(Arc::clone(config))),
153 )),
154 };
155 let (process, drive_process_on_open) = match &self.setup.process {
156 ProcessWorkDriverSetup::None => (None, false),
157 ProcessWorkDriverSetup::External { driver } => (Some(driver.clone()), false),
158 ProcessWorkDriverSetup::LazyDefault { config } => {
159 let mut config = (**config).clone();
160 if let Some(driver) = queued.clone() {
161 config = config.with_queued_work_driver(driver);
162 }
163 let registry = Arc::clone(&config.process_registry);
164 let hub = config.process_change_hub.clone();
165 let worker = DurableProcessWorker::new(config);
166 let driver = if let Some(hub) = hub {
167 ProcessWorkDriver::from_watched(
168 registry,
169 hub,
170 Arc::new(lash_core::InlineProcessRunHandle::new(worker)),
171 )
172 } else {
173 ProcessWorkDriver::inline(registry, worker)
174 };
175 (Some(driver), true)
176 }
177 };
178 ResolvedWorkDrivers {
179 process,
180 queued,
181 drive_process_on_open,
182 }
183 })
184 .await
185 .clone()
186 }
187
188 pub(crate) fn phase_probe_slot(&self) -> Option<lash_core::runtime::RuntimeTurnPhaseProbeSlot> {
189 self.phase_probe_slot.clone()
190 }
191
192 fn configured_process_work_driver(&self) -> Option<ProcessWorkDriver> {
193 match &self.setup.process {
194 ProcessWorkDriverSetup::External { driver } => Some(driver.clone()),
195 ProcessWorkDriverSetup::None | ProcessWorkDriverSetup::LazyDefault { .. } => None,
196 }
197 }
198
199 fn configured_queued_work_driver(&self) -> Option<QueuedWorkDriver> {
200 match &self.setup.queued {
201 QueuedWorkDriverSetup::External { driver } => Some(driver.clone()),
202 QueuedWorkDriverSetup::None | QueuedWorkDriverSetup::LazyDefault { .. } => None,
203 }
204 }
205}
206
207pub(crate) struct InlineQueuedWorkRunConfig {
208 env: RuntimeEnvironment,
209 policy: SessionPolicy,
210 protocol_factory: Option<Arc<dyn PluginFactory>>,
211 plugin_factories: Arc<Vec<Arc<dyn PluginFactory>>>,
212 store_factory: Arc<dyn SessionStoreFactory>,
213 live_replay_store: Arc<dyn LiveReplayStore>,
214 process_lifecycle_available: bool,
215}
216
217impl InlineQueuedWorkRunConfig {
218 fn new(
219 env: RuntimeEnvironment,
220 policy: SessionPolicy,
221 protocol_factory: Option<Arc<dyn PluginFactory>>,
222 plugin_factories: Arc<Vec<Arc<dyn PluginFactory>>>,
223 store_factory: Arc<dyn SessionStoreFactory>,
224 live_replay_store: Arc<dyn LiveReplayStore>,
225 process_lifecycle_available: bool,
226 ) -> Self {
227 Self {
228 env,
229 policy,
230 protocol_factory,
231 plugin_factories,
232 store_factory,
233 live_replay_store,
234 process_lifecycle_available,
235 }
236 }
237}
238
239struct InlineQueuedWorkRunHandle {
240 config: Arc<InlineQueuedWorkRunConfig>,
241}
242
243impl InlineQueuedWorkRunHandle {
244 fn new(config: Arc<InlineQueuedWorkRunConfig>) -> Self {
245 Self { config }
246 }
247}
248
249#[async_trait]
250impl QueuedWorkRunHandle for InlineQueuedWorkRunHandle {
251 async fn run_queued_work(
252 &self,
253 request: QueuedWorkRunRequest,
254 ) -> std::result::Result<(), lash_core::PluginError> {
255 let Some(session_id) = request.session_id else {
256 return Ok(());
257 };
258 let reason = request.reason;
259 let mut policy = self.config.policy.clone();
260 policy.session_id = Some(session_id.clone());
261 let store = self
262 .config
263 .store_factory
264 .create_store(&SessionStoreCreateRequest {
265 session_id: session_id.clone(),
266 relation: SessionRelation::default(),
267 policy: policy.clone(),
268 })
269 .await
270 .map_err(lash_core::PluginError::Session)?;
271 let state = crate::session::load_state_for_residency(
272 self.config.env.residency,
273 &session_id,
274 &policy,
275 store.as_ref(),
276 )
277 .await
278 .map_err(|err| lash_core::PluginError::Session(err.to_string()))?;
279 let plugin_host = build_plugin_host(
280 self.config.protocol_factory.as_ref(),
281 self.config.plugin_factories.as_ref(),
282 Vec::new(),
283 )
284 .map_err(|err| lash_core::PluginError::Session(err.to_string()))?;
285 let mut env = self.config.env.clone();
286 env.core = plugin_host
287 .install_process_engine_contributions(
288 env.core.clone(),
289 self.config.process_lifecycle_available,
290 )
291 .map_err(|err| lash_core::PluginError::Session(err.to_string()))?;
292 env.plugin_host = Some(Arc::new(plugin_host));
293 let effect_host = Arc::clone(&env.core.control.effect_host);
294 let runtime = LashRuntime::from_environment(&env, policy, state, Some(store))
295 .await
296 .map_err(|err| lash_core::PluginError::Session(err.to_string()))?;
297 let handle = RuntimeHandle::with_live_replay_store(
298 runtime,
299 Arc::clone(&self.config.live_replay_store),
300 );
301 let scope = lash_core::ExecutionScope::queue_drain(session_id, reason);
302 let scoped = effect_host
303 .scoped(scope)
304 .map_err(|err| lash_core::PluginError::Session(err.to_string()))?;
305 crate::turn::stream_next_queued_prepared_turn(
306 &handle,
307 crate::turn::TurnSinks::default(),
308 scoped,
309 CancellationToken::new(),
310 &[],
311 )
312 .await
313 .map_err(|err| lash_core::PluginError::Session(err.to_string()))?;
314 Ok(())
315 }
316}
317
318#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
319pub struct SessionDeleteReport {
320 pub session_id: String,
321 pub process: Option<lash_core::ProcessSessionDeleteReport>,
322}
323
324impl LashCore {
325 pub fn builder() -> LashCoreBuilder {
326 LashCoreBuilder::default()
327 }
328
329 pub fn standard_builder() -> LashCoreBuilder {
332 LashCore::builder()
333 .protocol_plugin(Arc::new(
334 lash_protocol_standard::StandardProtocolPluginFactory::new(),
335 ))
336 .plugins(default_runtime_stack())
337 }
338
339 #[cfg(feature = "rlm")]
347 pub fn rlm_builder(factory: crate::rlm::RlmProtocolPluginFactory) -> LashCoreBuilder {
348 LashCore::builder()
349 .protocol_plugin(Arc::new(factory))
350 .plugins(default_runtime_stack())
351 }
352
353 pub fn session(&self, session_id: impl Into<String>) -> SessionBuilder {
354 SessionBuilder {
355 core: self.clone(),
356 session_id: session_id.into(),
357 spec: SessionSpec::inherit(),
358 parent_session_id: None,
359 session_execution_owner: None,
360 store: None,
361 provider: None,
362 active_plugins: Vec::new(),
363 plugin_factories: Vec::new(),
364 plugin_options: PluginOptions::default(),
365 }
366 }
367
368 pub async fn resume(&self, parked: ParkedSession) -> Result<LashSession> {
381 let plugin_host = build_plugin_host(
386 self.protocol_factory.as_ref(),
387 self.plugin_factories.as_ref(),
388 Vec::new(),
389 )?;
390 let mut env = self.env.clone();
391 env.core = plugin_host.install_process_engine_contributions(
392 env.core.clone(),
393 self.process_lifecycle_available,
394 )?;
395 env.plugin_host = Some(Arc::new(plugin_host));
396 let effect_host = Arc::clone(&env.core.control.effect_host);
397 let drivers = self.work_driver.drivers().await;
398 env.process_work_driver = drivers.process.clone();
399 env.queued_work_driver = drivers.queued.clone();
400 let runtime = LashRuntime::resume(parked.inner, &env).await?;
401 let handle =
402 RuntimeHandle::with_live_replay_store(runtime, Arc::clone(&self.live_replay_store));
403 Ok(LashSession {
404 runtime: handle,
405 effect_host,
406 parent_session_id: None,
407 active_plugins: Vec::new(),
408 process_phase_probe_slot: self.work_driver.phase_probe_slot(),
409 turn_cancels: crate::turn::TurnCancelRegistry::default(),
410 })
411 }
412
413 pub fn flush_trace_sink(&self) -> Result<()> {
424 if let Some(sink) = self.env.core.tracing.trace_sink.as_ref() {
425 sink.flush()?;
426 }
427 Ok(())
428 }
429
430 pub fn triggers(&self) -> crate::admin::CoreTriggerAdmin {
431 crate::admin::CoreTriggerAdmin { core: self.clone() }
432 }
433
434 pub fn processes(&self) -> crate::process_admin::Processes {
435 crate::process_admin::Processes { core: self.clone() }
436 }
437
438 pub fn completions(&self) -> crate::admin::Completions {
439 crate::admin::Completions { core: self.clone() }
440 }
441
442 pub fn effect_host(&self) -> Arc<dyn EffectHost> {
443 Arc::clone(&self.env.core.control.effect_host)
444 }
445
446 pub async fn delete_session(
447 &self,
448 session_id: impl AsRef<str>,
449 scoped_effect_controller: ScopedEffectController<'_>,
450 ) -> Result<SessionDeleteReport> {
451 let session_id = session_id.as_ref().to_string();
452 let Some(store_factory) = self.store_factory.as_ref() else {
453 return Err(EmbedError::MissingSessionStoreFactory);
454 };
455 let process = if let Some(process_registry) = self.env.process_registry.as_ref() {
456 let invocation = RuntimeInvocation::effect(
457 RuntimeScope::new(session_id.clone()),
458 format!("process:delete-session:{session_id}"),
459 RuntimeEffectKind::Process,
460 format!("{session_id}:delete-session"),
461 );
462 let outcome = scoped_effect_controller
463 .controller()
464 .execute_effect(
465 RuntimeEffectEnvelope::new(
466 invocation,
467 RuntimeEffectCommand::process(ProcessCommand::DeleteSession {
468 session_id: session_id.clone(),
469 }),
470 ),
471 RuntimeEffectLocalExecutor::processes(
472 Arc::clone(process_registry),
473 self.env.process_work_driver.clone(),
474 ),
475 )
476 .await
477 .map_err(|err| EmbedError::SessionDeleteProcess {
478 session_id: session_id.clone(),
479 message: err.to_string(),
480 })?;
481 match outcome {
482 RuntimeEffectOutcome::Process {
483 result: ProcessEffectOutcome::DeleteSession { report },
484 } => Some(report),
485 other => {
486 return Err(EmbedError::SessionDeleteProcess {
487 session_id,
488 message: format!(
489 "process delete returned the wrong outcome: {}",
490 other.kind().as_str()
491 ),
492 });
493 }
494 }
495 } else {
496 None
497 };
498 if let Some(trigger_store) = self.env.trigger_store.as_ref() {
499 trigger_store
500 .delete_session_subscriptions(&session_id)
501 .await
502 .map_err(|err| EmbedError::SessionDeleteProcess {
503 session_id: session_id.clone(),
504 message: err.to_string(),
505 })?;
506 }
507 self.env
508 .core
509 .control
510 .effect_host
511 .revoke_await_events_for_session(&session_id)
512 .await
513 .map_err(|err| EmbedError::SessionDeleteProcess {
514 session_id: session_id.clone(),
515 message: err.to_string(),
516 })?;
517 store_factory
518 .delete_session(&session_id)
519 .await
520 .map_err(|message| EmbedError::StoreFactory {
521 session_id: session_id.clone(),
522 message,
523 })?;
524 Ok(SessionDeleteReport {
525 session_id,
526 process,
527 })
528 }
529
530 pub fn process_registry(&self) -> Option<Arc<dyn ProcessRegistry>> {
531 self.env.process_registry.as_ref().cloned()
532 }
533
534 pub fn durable_process_worker_config(&self) -> Result<DurableProcessWorkerConfig> {
535 self.durable_process_worker_config_with_plugins(std::iter::empty::<Arc<dyn PluginFactory>>())
536 }
537
538 pub fn durable_process_worker_config_with_plugins(
539 &self,
540 extra_plugin_factories: impl IntoIterator<Item = Arc<dyn PluginFactory>>,
541 ) -> Result<DurableProcessWorkerConfig> {
542 let Some(process_registry) = self.process_registry() else {
543 return Err(EmbedError::MissingProcessRegistry);
544 };
545 let Some(store_factory) = self.store_factory.as_ref() else {
546 return Err(EmbedError::MissingProcessWorkerStoreFactory);
547 };
548 let plugin_host = build_plugin_host(
549 self.protocol_factory.as_ref(),
550 self.plugin_factories.as_ref(),
551 extra_plugin_factories.into_iter().collect(),
552 )?;
553 let runtime_host = plugin_host.install_process_engine_contributions(
554 self.env.core.clone(),
555 self.process_lifecycle_available,
556 )?;
557 let mut config = DurableProcessWorkerConfig::new(
558 Arc::new(plugin_host),
559 runtime_host,
560 Arc::clone(store_factory),
561 process_registry,
562 )
563 .with_session_policy(self.policy.clone())
564 .with_residency(self.env.residency);
565 if let Some(trigger_store) = self.env.trigger_store.as_ref() {
566 config = config.with_trigger_store(Arc::clone(trigger_store));
567 }
568 if let Some(driver) = self.work_driver.configured_process_work_driver() {
569 config = config
570 .with_change_hub(driver.change_hub())
571 .with_process_work_driver(driver);
572 }
573 if let Some(driver) = self.work_driver.configured_queued_work_driver() {
574 config = config.with_queued_work_driver(driver);
575 }
576 Ok(config)
577 }
578}
579
580fn default_runtime_stack() -> PluginStack {
581 lash_plugin_tool_output_budget::tool_output_budget_stack()
582}
583
584#[derive(Default)]
585pub struct LashCoreBuilder {
586 pub(crate) protocol_factory: Option<Arc<dyn PluginFactory>>,
587 session_spec: SessionSpec,
588 provider: Option<ProviderHandle>,
589 pub(crate) store_factory: Option<Arc<dyn SessionStoreFactory>>,
590 child_store_factory: Option<Arc<dyn SessionStoreFactory>>,
591 effect_host: Option<Arc<dyn EffectHost>>,
595 attachment_store: Option<Arc<dyn AttachmentStore>>,
596 process_env_store: Option<Arc<dyn ProcessExecutionEnvStore>>,
597 trigger_store: Option<Arc<dyn lash_core::TriggerStore>>,
598 prompt: Option<PromptLayer>,
600 trace_sink: Option<Arc<dyn lash_trace::TraceSink>>,
601 trace_level: Option<lash_trace::TraceLevel>,
602 trace_context: Option<lash_trace::TraceContext>,
603 termination: Option<TerminationPolicy>,
604 runtime_host_config: Option<RuntimeHostConfig>,
606 tool_providers: Vec<Arc<dyn ToolProvider>>,
607 plugin_stack: PluginStack,
608 plugin_host: Option<PluginHost>,
609 residency: Option<Residency>,
610 lease_timings: Option<lash_core::LeaseTimings>,
611 process_work_source: ProcessWorkSource,
614 process_event_sink: Option<Arc<dyn lash_core::ProcessEventSink>>,
617 queued_work_source: QueuedWorkSource,
618 live_replay_store: Option<Arc<dyn LiveReplayStore>>,
619}
620
621impl LashCoreBuilder {
622 pub fn protocol_plugin(mut self, plugin: Arc<dyn PluginFactory>) -> Self {
623 self.protocol_factory = Some(plugin);
624 self
625 }
626
627 pub fn provider(mut self, provider: ProviderHandle) -> Self {
628 self.session_spec = self.session_spec.provider_id(provider.kind());
629 self.provider = Some(provider);
630 self
631 }
632
633 pub fn model(mut self, model: lash_core::ModelSpec) -> Self {
634 self.session_spec = self.session_spec.model(model);
635 self
636 }
637
638 pub fn max_turns(mut self, max_turns: usize) -> Self {
639 self.session_spec = self.session_spec.max_turns(max_turns);
640 self
641 }
642
643 pub fn session_spec(mut self, spec: SessionSpec) -> Self {
644 self.session_spec = spec;
645 self
646 }
647
648 pub fn store_factory(mut self, store_factory: Arc<dyn SessionStoreFactory>) -> Self {
656 self.store_factory = Some(store_factory);
657 self
658 }
659
660 pub fn child_store_factory(mut self, store_factory: Arc<dyn SessionStoreFactory>) -> Self {
668 self.child_store_factory = Some(store_factory);
669 self
670 }
671
672 pub fn attachment_store(mut self, attachment_store: Arc<dyn AttachmentStore>) -> Self {
673 self.attachment_store = Some(attachment_store);
674 self
675 }
676
677 pub fn process_env_store(
678 mut self,
679 process_env_store: Arc<dyn ProcessExecutionEnvStore>,
680 ) -> Self {
681 self.process_env_store = Some(process_env_store);
682 self
683 }
684
685 pub fn effect_host(mut self, effect_host: Arc<dyn EffectHost>) -> Self {
690 self.effect_host = Some(effect_host);
691 self
692 }
693
694 pub fn tools(mut self, tools: Arc<dyn ToolProvider>) -> Self {
695 self.tool_providers.push(tools);
696 self
697 }
698
699 pub fn plugin(mut self, plugin: Arc<dyn PluginFactory>) -> Self {
700 self.plugin_stack.push(plugin);
701 self
702 }
703
704 pub fn plugins(mut self, stack: PluginStack) -> Self {
705 self.plugin_stack = stack;
706 self
707 }
708
709 pub fn configure_plugins(mut self, configure: impl FnOnce(&mut PluginStack)) -> Self {
710 configure(&mut self.plugin_stack);
711 self
712 }
713
714 pub fn trace_sink(mut self, trace_sink: Arc<dyn lash_trace::TraceSink>) -> Self {
715 self.trace_sink = Some(trace_sink);
716 self
717 }
718
719 pub fn trace_jsonl_path(mut self, path: impl Into<std::path::PathBuf>) -> Self {
720 self.trace_sink = Some(Arc::new(lash_trace::JsonlTraceSink::new(path.into())));
721 self
722 }
723
724 pub fn trace_level(mut self, trace_level: lash_trace::TraceLevel) -> Self {
725 self.trace_level = Some(trace_level);
726 self
727 }
728
729 pub fn trace_context(mut self, trace_context: lash_trace::TraceContext) -> Self {
730 self.trace_context = Some(trace_context);
731 self
732 }
733
734 pub fn termination(mut self, termination: TerminationPolicy) -> Self {
735 self.termination = Some(termination);
736 self
737 }
738
739 pub fn residency(mut self, residency: Residency) -> Self {
740 self.residency = Some(residency);
741 self
742 }
743
744 pub fn lease_timings(mut self, lease_timings: lash_core::LeaseTimings) -> Self {
757 self.lease_timings = Some(lease_timings);
758 self
759 }
760
761 pub fn live_replay_store(mut self, live_replay_store: Arc<dyn LiveReplayStore>) -> Self {
765 self.live_replay_store = Some(live_replay_store);
766 self
767 }
768
769 fn resolve_runtime_host_config(&mut self) -> Result<RuntimeHostConfig> {
772 if let Some(base) = self.runtime_host_config.take() {
773 return Ok(self.apply_core_overrides(base));
774 }
775 let effect_host = self
776 .effect_host
777 .take()
778 .ok_or(EmbedError::MissingEffectHost)?;
779 let attachment_store = self
780 .attachment_store
781 .take()
782 .ok_or(EmbedError::MissingAttachmentStore)?;
783 let process_env_store = self
784 .process_env_store
785 .take()
786 .ok_or(EmbedError::MissingProcessEnvStore)?;
787 let core = RuntimeHostConfig::new(effect_host, attachment_store, process_env_store);
788 Ok(self.apply_core_overrides(core))
789 }
790
791 fn apply_core_overrides(&mut self, mut core: RuntimeHostConfig) -> RuntimeHostConfig {
793 if let Some(effect_host) = self.effect_host.take() {
794 core.control.effect_host = effect_host;
795 }
796 if let Some(attachment_store) = self.attachment_store.take() {
797 core.durability.attachment_store = Arc::new(
798 lash_core::SessionAttachmentStore::ephemeral(attachment_store),
799 );
800 }
801 if let Some(process_env_store) = self.process_env_store.take() {
802 core.durability.process_env_store = process_env_store;
803 }
804 if let Some(prompt) = self.prompt.take() {
805 core.prompt.prompt = prompt;
806 }
807 if let Some(trace_sink) = self.trace_sink.take() {
808 core.tracing.trace_sink = Some(trace_sink);
809 }
810 if let Some(trace_level) = self.trace_level.take() {
811 core.tracing.trace_level = trace_level;
812 }
813 if let Some(trace_context) = self.trace_context.take() {
814 core.tracing.trace_context = trace_context;
815 }
816 if let Some(termination) = self.termination.take() {
817 core.control.termination = termination;
818 }
819 if let Some(lease_timings) = self.lease_timings.take() {
820 core.control.lease_timings = lease_timings;
821 }
822 core
823 }
824
825 fn effective_session_store_tier(&self) -> Option<DurabilityTier> {
838 self.child_store_factory
839 .as_ref()
840 .or(self.store_factory.as_ref())
841 .map(|factory| factory.durability_tier())
842 }
843
844 fn ensure_store_peer_coherence(
853 session_store_tier: Option<DurabilityTier>,
854 trigger_store_tier: Option<DurabilityTier>,
855 process_registry_tier: Option<DurabilityTier>,
856 core: &RuntimeHostConfig,
857 ) -> Result<()> {
858 let attachment_tier = Some(
859 core.durability
860 .attachment_store
861 .persistence()
862 .durability_tier(),
863 );
864 let process_env_tier = Some(core.durability.process_env_store.durability_tier());
865 let effect_host_tier = Some(core.control.effect_host.durability_tier());
866
867 if session_store_tier == Some(DurabilityTier::Durable) {
868 if attachment_tier == Some(DurabilityTier::Inline) {
869 return Err(EmbedError::DurableStorePeerRequired {
870 facet: "attachment store",
871 });
872 }
873 if process_env_tier == Some(DurabilityTier::Inline) {
874 return Err(EmbedError::DurableStorePeerRequired {
875 facet: "process execution environment store",
876 });
877 }
878 for engine in core.process_engines.engines() {
881 if engine.durability_tier() == DurabilityTier::Inline {
882 return Err(EmbedError::DurableStorePeerRequired {
883 facet: engine.kind(),
884 });
885 }
886 }
887 }
888
889 if process_registry_tier == Some(DurabilityTier::Durable) {
890 if session_store_tier != Some(DurabilityTier::Durable) {
891 return Err(EmbedError::DurableProcessRegistryRequiresStoreFactory);
892 }
893 if trigger_store_tier != Some(DurabilityTier::Durable) {
894 return Err(EmbedError::DurableStorePeerRequired {
895 facet: "trigger store",
896 });
897 }
898 if process_env_tier != Some(DurabilityTier::Durable) {
899 return Err(EmbedError::DurableStorePeerRequired {
900 facet: "process execution environment store",
901 });
902 }
903 }
904
905 if trigger_store_tier == Some(DurabilityTier::Durable) {
906 if session_store_tier != Some(DurabilityTier::Durable) {
907 return Err(EmbedError::DurableStorePeerRequired {
908 facet: "session store factory",
909 });
910 }
911 if process_env_tier != Some(DurabilityTier::Durable) {
912 return Err(EmbedError::DurableStorePeerRequired {
913 facet: "process execution environment store",
914 });
915 }
916 if process_registry_tier == Some(DurabilityTier::Inline) {
917 return Err(EmbedError::DurableStorePeerRequired {
918 facet: "process registry",
919 });
920 }
921 }
922
923 if effect_host_tier == Some(DurabilityTier::Durable) {
924 if attachment_tier != Some(DurabilityTier::Durable) {
925 return Err(EmbedError::DurableStorePeerRequired {
926 facet: "attachment store",
927 });
928 }
929 if process_env_tier != Some(DurabilityTier::Durable) {
930 return Err(EmbedError::DurableStorePeerRequired {
931 facet: "process execution environment store",
932 });
933 }
934 }
935
936 Ok(())
937 }
938
939 pub fn build(mut self) -> Result<LashCore> {
940 let protocol_factory = self.protocol_factory.clone();
941 if protocol_factory.is_none() && self.plugin_host.is_none() {
942 return Err(EmbedError::MissingProtocolPlugin);
943 }
944 let provider_id = self
945 .session_spec
946 .provider_id
947 .clone()
948 .or_else(|| {
949 self.provider
950 .as_ref()
951 .map(|provider| provider.kind().to_string())
952 })
953 .unwrap_or_default();
954 let model = self
955 .session_spec
956 .model
957 .clone()
958 .ok_or(EmbedError::MissingModelSpec)?;
959
960 let base_policy = SessionPolicy {
961 provider_id,
962 model,
963 max_turns: self.session_spec.max_turns.flatten(),
964 ..SessionPolicy::default()
965 };
966 let policy = self.session_spec.resolve_against(&base_policy);
967
968 let session_store_tier = self.effective_session_store_tier();
971 let trigger_store_tier = self
972 .trigger_store
973 .as_ref()
974 .map(|store| store.durability_tier());
975 let process_work_source = self
976 .process_work_source
977 .clone()
978 .watched(self.process_event_sink.clone());
979 let process_registry_tier = process_work_source
980 .process_registry()
981 .map(|registry| registry.durability_tier());
982
983 let mut core = self.resolve_runtime_host_config()?;
984 if let Some(provider) = self.provider.clone() {
985 core.providers.provider_resolver =
986 Arc::new(lash_core::SingleProviderResolver::new(provider));
987 }
988 let plugin_factories = if let Some(plugin_host) = self.plugin_host {
989 plugin_host.factories().to_vec()
990 } else {
991 let mut factories = Vec::new();
992 if !self.tool_providers.is_empty() {
993 let spec = self
994 .tool_providers
995 .into_iter()
996 .fold(PluginSpec::new(), PluginSpec::with_tool_provider);
997 factories.push(Arc::new(StaticPluginFactory::new("embed_tools", spec))
998 as Arc<dyn PluginFactory>);
999 }
1000 factories.extend(self.plugin_stack.into_factories());
1001 factories
1002 };
1003 let default_plugin_host =
1004 build_plugin_host(protocol_factory.as_ref(), &plugin_factories, Vec::new())?;
1005 let process_lifecycle_available = process_work_source.has_registry();
1009 let core_with_engines = default_plugin_host
1017 .install_process_engine_contributions(core.clone(), process_lifecycle_available)?;
1018 Self::ensure_store_peer_coherence(
1021 session_store_tier,
1022 trigger_store_tier,
1023 process_registry_tier,
1024 &core_with_engines,
1025 )?;
1026
1027 let process_registry = process_work_source.process_registry();
1028
1029 let process_work_driver = Self::resolve_process_work_driver(
1035 &process_work_source,
1036 &default_plugin_host,
1037 &core,
1038 process_lifecycle_available,
1039 self.child_store_factory
1042 .as_ref()
1043 .or(self.store_factory.as_ref()),
1044 &policy,
1045 self.residency.unwrap_or_default(),
1046 self.trigger_store.as_ref(),
1047 )?;
1048
1049 let live_replay_clock = Arc::clone(&core.clock);
1050 let mut env_builder = RuntimeEnvironment::builder()
1051 .with_plugin_host(Arc::new(default_plugin_host))
1052 .with_runtime_host_config(core);
1053 if let Some(process_registry) = process_registry.as_ref() {
1054 env_builder = env_builder.with_process_registry(Arc::clone(process_registry));
1055 }
1056 if let Some(residency) = self.residency {
1057 env_builder = env_builder.with_residency(residency);
1058 }
1059 if let Some(child_store_factory) = self
1060 .child_store_factory
1061 .as_ref()
1062 .or(self.store_factory.as_ref())
1063 {
1064 env_builder = env_builder.with_session_store_factory(Arc::clone(child_store_factory));
1065 }
1066 if let Some(trigger_store) = self.trigger_store.as_ref() {
1067 env_builder = env_builder.with_trigger_store(Arc::clone(trigger_store));
1068 }
1069 let live_replay_store = self.live_replay_store.take().unwrap_or_else(|| {
1070 Arc::new(InMemoryLiveReplayStore::with_clock(
1071 lash_core::InMemoryLiveReplayStoreConfig::default(),
1072 live_replay_clock,
1073 ))
1074 });
1075 let env = env_builder.build();
1076 let queued_work_driver = Self::resolve_queued_work_driver(
1077 &self.queued_work_source,
1078 env.clone(),
1079 policy.clone(),
1080 protocol_factory.clone(),
1081 Arc::new(plugin_factories.clone()),
1082 self.child_store_factory
1083 .as_ref()
1084 .or(self.store_factory.as_ref()),
1085 Arc::clone(&live_replay_store),
1086 process_lifecycle_available,
1087 );
1088 let work_driver = InlineWorkDriverSetup {
1089 process: process_work_driver,
1090 queued: queued_work_driver,
1091 };
1092
1093 Ok(LashCore {
1094 env,
1095 policy,
1096 store_factory: self.store_factory,
1097 plugin_factories: Arc::new(plugin_factories),
1098 provider: self.provider,
1099 live_replay_store,
1100 protocol_factory,
1101 process_lifecycle_available,
1102 work_driver: Arc::new(InlineWorkDriverSlot::new(work_driver)),
1103 })
1104 }
1105
1106 #[allow(clippy::too_many_arguments)]
1117 fn resolve_process_work_driver(
1118 process_work_source: &ProcessWorkSource,
1119 worker_plugin_host: &PluginHost,
1120 core: &RuntimeHostConfig,
1121 process_lifecycle_available: bool,
1122 store_factory: Option<&Arc<dyn SessionStoreFactory>>,
1123 policy: &SessionPolicy,
1124 residency: lash_core::Residency,
1125 trigger_store: Option<&Arc<dyn lash_core::TriggerStore>>,
1126 ) -> Result<ProcessWorkDriverSetup> {
1127 let (process_registry, process_change_hub) = match process_work_source {
1128 ProcessWorkSource::None => return Ok(ProcessWorkDriverSetup::None),
1129 ProcessWorkSource::External(driver) => {
1130 return Ok(ProcessWorkDriverSetup::External {
1131 driver: driver.clone(),
1132 });
1133 }
1134 ProcessWorkSource::Inline { registry, hub } => (Arc::clone(registry), hub.clone()),
1135 };
1136 let Some(store_factory) = store_factory else {
1140 return Err(EmbedError::ProcessRegistryRequiresStoreFactory);
1141 };
1142 let runtime_host = worker_plugin_host
1147 .install_process_engine_contributions(core.clone(), process_lifecycle_available)?;
1148 let phase_probe_slot = lash_core::runtime::RuntimeTurnPhaseProbeSlot::default();
1149 let mut config = DurableProcessWorkerConfig::new(
1150 Arc::new(worker_plugin_host.clone()),
1151 runtime_host,
1152 Arc::clone(store_factory),
1153 process_registry,
1154 )
1155 .with_session_policy(policy.clone())
1156 .with_trigger_store(trigger_store.cloned().unwrap_or_else(|| {
1157 Arc::new(lash_core::InMemoryTriggerStore::with_clock(Arc::clone(
1158 &core.clock,
1159 )))
1160 }))
1161 .with_residency(residency)
1162 .with_turn_phase_probe_slot(phase_probe_slot);
1163 if let Some(hub) = process_change_hub {
1164 config = config.with_change_hub(hub);
1165 }
1166 let config = Box::new(config);
1167 Ok(ProcessWorkDriverSetup::LazyDefault { config })
1168 }
1169
1170 #[allow(clippy::too_many_arguments)]
1171 fn resolve_queued_work_driver(
1172 queued_work_source: &QueuedWorkSource,
1173 env: RuntimeEnvironment,
1174 policy: SessionPolicy,
1175 protocol_factory: Option<Arc<dyn PluginFactory>>,
1176 plugin_factories: Arc<Vec<Arc<dyn PluginFactory>>>,
1177 store_factory: Option<&Arc<dyn SessionStoreFactory>>,
1178 live_replay_store: Arc<dyn LiveReplayStore>,
1179 process_lifecycle_available: bool,
1180 ) -> QueuedWorkDriverSetup {
1181 match queued_work_source {
1182 QueuedWorkSource::None => QueuedWorkDriverSetup::None,
1183 QueuedWorkSource::External(driver) => QueuedWorkDriverSetup::External {
1184 driver: driver.clone(),
1185 },
1186 QueuedWorkSource::LazyDefault => match store_factory {
1187 Some(store_factory) => QueuedWorkDriverSetup::LazyDefault {
1188 config: Arc::new(InlineQueuedWorkRunConfig::new(
1189 env,
1190 policy,
1191 protocol_factory,
1192 plugin_factories,
1193 Arc::clone(store_factory),
1194 live_replay_store,
1195 process_lifecycle_available,
1196 )),
1197 },
1198 None => QueuedWorkDriverSetup::None,
1199 },
1200 }
1201 }
1202
1203 pub fn advanced(self) -> AdvancedLashCoreBuilder {
1204 AdvancedLashCoreBuilder { builder: self }
1205 }
1206
1207 pub fn process_registry(mut self, process_registry: Arc<dyn ProcessRegistry>) -> Self {
1208 self.process_work_source = ProcessWorkSource::Inline {
1209 registry: process_registry,
1210 hub: None,
1211 };
1212 self
1213 }
1214
1215 pub fn process_event_sink(mut self, sink: Arc<dyn lash_core::ProcessEventSink>) -> Self {
1230 self.process_event_sink = Some(sink);
1231 self
1232 }
1233
1234 pub fn trigger_store(mut self, store: Arc<dyn lash_core::TriggerStore>) -> Self {
1235 self.trigger_store = Some(store);
1236 self
1237 }
1238
1239 pub fn process_work_driver(mut self, driver: ProcessWorkDriver) -> Self {
1246 self.process_work_source = ProcessWorkSource::External(driver);
1247 self
1248 }
1249
1250 pub fn queued_work_driver(mut self, driver: QueuedWorkDriver) -> Self {
1252 self.queued_work_source = QueuedWorkSource::External(driver);
1253 self
1254 }
1255
1256 pub fn disable_queued_work_driver(mut self) -> Self {
1257 self.queued_work_source = QueuedWorkSource::None;
1258 self
1259 }
1260}
1261
1262pub(crate) fn build_plugin_host(
1263 protocol_factory: Option<&Arc<dyn PluginFactory>>,
1264 common_factories: &[Arc<dyn PluginFactory>],
1265 extra_factories: Vec<Arc<dyn PluginFactory>>,
1266) -> Result<PluginHost> {
1267 let mut factories = Vec::with_capacity(
1268 usize::from(protocol_factory.is_some()) + common_factories.len() + extra_factories.len(),
1269 );
1270 if let Some(protocol_factory) = protocol_factory {
1271 factories.push(Arc::clone(protocol_factory));
1272 }
1273 factories.extend(common_factories.iter().cloned());
1274 factories.extend(extra_factories);
1275 Ok(PluginHost::new(factories))
1276}
1277
1278impl PromptLayerSink for LashCoreBuilder {
1279 fn prompt_layer_mut(&mut self) -> &mut PromptLayer {
1280 self.prompt.get_or_insert_with(PromptLayer::new)
1281 }
1282}
1283
1284pub struct AdvancedLashCoreBuilder {
1285 builder: LashCoreBuilder,
1286}
1287
1288impl AdvancedLashCoreBuilder {
1289 pub fn runtime_host_config(mut self, core: lash_core::RuntimeHostConfig) -> Self {
1290 self.builder.runtime_host_config = Some(core);
1291 self
1292 }
1293
1294 pub fn plugin_host(mut self, plugin_host: PluginHost) -> Self {
1295 self.builder.plugin_host = Some(plugin_host);
1296 self
1297 }
1298
1299 pub fn build(self) -> Result<LashCore> {
1300 self.builder.build()
1301 }
1302}