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 },
50 External(ProcessWorkDriver),
51}
52
53impl ProcessWorkSource {
54 fn process_registry(&self) -> Option<Arc<dyn ProcessRegistry>> {
55 match self {
56 Self::None => None,
57 Self::Inline { registry } => Some(Arc::clone(registry)),
58 Self::External(driver) => Some(driver.process_registry()),
59 }
60 }
61
62 fn has_registry(&self) -> bool {
63 !matches!(self, Self::None)
64 }
65}
66
67#[derive(Clone, Default)]
68pub(crate) enum QueuedWorkSource {
69 None,
70 #[default]
71 LazyDefault,
72 External(QueuedWorkDriver),
73}
74
75pub(crate) enum QueuedWorkDriverSetup {
76 None,
77 LazyDefault {
78 config: Arc<InlineQueuedWorkRunConfig>,
79 },
80 External {
81 driver: QueuedWorkDriver,
82 },
83}
84
85pub(crate) struct InlineWorkDriverSetup {
86 process: ProcessWorkDriverSetup,
87 queued: QueuedWorkDriverSetup,
88}
89
90#[derive(Clone, Default)]
91pub(crate) struct ResolvedWorkDrivers {
92 pub(crate) process: Option<ProcessWorkDriver>,
93 pub(crate) queued: Option<QueuedWorkDriver>,
94 pub(crate) drive_process_on_open: bool,
95}
96
97pub(crate) struct InlineWorkDriverSlot {
103 setup: InlineWorkDriverSetup,
104 drivers: tokio::sync::OnceCell<ResolvedWorkDrivers>,
105 phase_probe_slot: Option<lash_core::runtime::RuntimeTurnPhaseProbeSlot>,
106}
107
108impl InlineWorkDriverSlot {
109 fn new(setup: InlineWorkDriverSetup) -> Self {
110 let phase_probe_slot = match &setup.process {
111 ProcessWorkDriverSetup::LazyDefault { config } => {
112 Some(config.turn_phase_probe_slot.clone())
113 }
114 ProcessWorkDriverSetup::None | ProcessWorkDriverSetup::External { .. } => None,
115 };
116 Self {
117 setup,
118 drivers: tokio::sync::OnceCell::new(),
119 phase_probe_slot,
120 }
121 }
122
123 pub(crate) async fn drivers(&self) -> ResolvedWorkDrivers {
126 self.drivers
127 .get_or_init(|| async {
128 let queued = match &self.setup.queued {
129 QueuedWorkDriverSetup::None => None,
130 QueuedWorkDriverSetup::External { driver } => Some(driver.clone()),
131 QueuedWorkDriverSetup::LazyDefault { config } => Some(QueuedWorkDriver::new(
132 Arc::new(InlineQueuedWorkRunHandle::new(Arc::clone(config))),
133 )),
134 };
135 let (process, drive_process_on_open) = match &self.setup.process {
136 ProcessWorkDriverSetup::None => (None, false),
137 ProcessWorkDriverSetup::External { driver } => (Some(driver.clone()), false),
138 ProcessWorkDriverSetup::LazyDefault { config } => {
139 let mut config = (**config).clone();
140 if let Some(driver) = queued.clone() {
141 config = config.with_queued_work_driver(driver);
142 }
143 let registry = Arc::clone(&config.process_registry);
144 let worker = DurableProcessWorker::new(config);
145 (Some(ProcessWorkDriver::inline(registry, worker)), true)
146 }
147 };
148 ResolvedWorkDrivers {
149 process,
150 queued,
151 drive_process_on_open,
152 }
153 })
154 .await
155 .clone()
156 }
157
158 pub(crate) fn phase_probe_slot(&self) -> Option<lash_core::runtime::RuntimeTurnPhaseProbeSlot> {
159 self.phase_probe_slot.clone()
160 }
161
162 fn configured_process_work_driver(&self) -> Option<ProcessWorkDriver> {
163 match &self.setup.process {
164 ProcessWorkDriverSetup::External { driver } => Some(driver.clone()),
165 ProcessWorkDriverSetup::None | ProcessWorkDriverSetup::LazyDefault { .. } => None,
166 }
167 }
168
169 fn configured_queued_work_driver(&self) -> Option<QueuedWorkDriver> {
170 match &self.setup.queued {
171 QueuedWorkDriverSetup::External { driver } => Some(driver.clone()),
172 QueuedWorkDriverSetup::None | QueuedWorkDriverSetup::LazyDefault { .. } => None,
173 }
174 }
175}
176
177pub(crate) struct InlineQueuedWorkRunConfig {
178 env: RuntimeEnvironment,
179 policy: SessionPolicy,
180 protocol_factory: Option<Arc<dyn PluginFactory>>,
181 plugin_factories: Arc<Vec<Arc<dyn PluginFactory>>>,
182 store_factory: Arc<dyn SessionStoreFactory>,
183 live_replay_store: Arc<dyn LiveReplayStore>,
184 process_lifecycle_available: bool,
185}
186
187impl InlineQueuedWorkRunConfig {
188 fn new(
189 env: RuntimeEnvironment,
190 policy: SessionPolicy,
191 protocol_factory: Option<Arc<dyn PluginFactory>>,
192 plugin_factories: Arc<Vec<Arc<dyn PluginFactory>>>,
193 store_factory: Arc<dyn SessionStoreFactory>,
194 live_replay_store: Arc<dyn LiveReplayStore>,
195 process_lifecycle_available: bool,
196 ) -> Self {
197 Self {
198 env,
199 policy,
200 protocol_factory,
201 plugin_factories,
202 store_factory,
203 live_replay_store,
204 process_lifecycle_available,
205 }
206 }
207}
208
209struct InlineQueuedWorkRunHandle {
210 config: Arc<InlineQueuedWorkRunConfig>,
211}
212
213impl InlineQueuedWorkRunHandle {
214 fn new(config: Arc<InlineQueuedWorkRunConfig>) -> Self {
215 Self { config }
216 }
217}
218
219#[async_trait]
220impl QueuedWorkRunHandle for InlineQueuedWorkRunHandle {
221 async fn run_queued_work(
222 &self,
223 request: QueuedWorkRunRequest,
224 ) -> std::result::Result<(), lash_core::PluginError> {
225 let Some(session_id) = request.session_id else {
226 return Ok(());
227 };
228 let reason = request.reason;
229 let mut policy = self.config.policy.clone();
230 policy.session_id = Some(session_id.clone());
231 let store = self
232 .config
233 .store_factory
234 .create_store(&SessionStoreCreateRequest {
235 session_id: session_id.clone(),
236 relation: SessionRelation::default(),
237 policy: policy.clone(),
238 })
239 .await
240 .map_err(lash_core::PluginError::Session)?;
241 let state = crate::session::load_state_for_residency(
242 self.config.env.residency,
243 &session_id,
244 &policy,
245 store.as_ref(),
246 )
247 .await
248 .map_err(|err| lash_core::PluginError::Session(err.to_string()))?;
249 let plugin_host = build_plugin_host(
250 self.config.protocol_factory.as_ref(),
251 self.config.plugin_factories.as_ref(),
252 Vec::new(),
253 )
254 .map_err(|err| lash_core::PluginError::Session(err.to_string()))?;
255 let mut env = self.config.env.clone();
256 env.core = plugin_host
257 .install_process_engine_contributions(
258 env.core.clone(),
259 self.config.process_lifecycle_available,
260 )
261 .map_err(|err| lash_core::PluginError::Session(err.to_string()))?;
262 env.plugin_host = Some(Arc::new(plugin_host));
263 let effect_host = Arc::clone(&env.core.control.effect_host);
264 let runtime = LashRuntime::from_environment(&env, policy, state, Some(store))
265 .await
266 .map_err(|err| lash_core::PluginError::Session(err.to_string()))?;
267 let handle = RuntimeHandle::with_live_replay_store(
268 runtime,
269 Arc::clone(&self.config.live_replay_store),
270 );
271 let scope = lash_core::ExecutionScope::queue_drain(session_id, reason);
272 let scoped = effect_host
273 .scoped(scope)
274 .map_err(|err| lash_core::PluginError::Session(err.to_string()))?;
275 crate::turn::stream_next_queued_prepared_turn(
276 &handle,
277 crate::turn::TurnSinks::default(),
278 scoped,
279 CancellationToken::new(),
280 &[],
281 )
282 .await
283 .map_err(|err| lash_core::PluginError::Session(err.to_string()))?;
284 Ok(())
285 }
286}
287
288#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
289pub struct SessionDeleteReport {
290 pub session_id: String,
291 pub process: Option<lash_core::ProcessSessionDeleteReport>,
292}
293
294impl LashCore {
295 pub fn builder() -> LashCoreBuilder {
296 LashCoreBuilder::default()
297 }
298
299 pub fn standard_builder() -> LashCoreBuilder {
302 LashCore::builder()
303 .protocol_plugin(Arc::new(
304 lash_protocol_standard::StandardProtocolPluginFactory::new(),
305 ))
306 .plugins(default_runtime_stack())
307 }
308
309 #[cfg(feature = "rlm")]
317 pub fn rlm_builder(factory: crate::rlm::RlmProtocolPluginFactory) -> LashCoreBuilder {
318 LashCore::builder()
319 .protocol_plugin(Arc::new(factory))
320 .plugins(default_runtime_stack())
321 }
322
323 pub fn session(&self, session_id: impl Into<String>) -> SessionBuilder {
324 SessionBuilder {
325 core: self.clone(),
326 session_id: session_id.into(),
327 spec: SessionSpec::inherit(),
328 parent_session_id: None,
329 session_execution_owner: None,
330 store: None,
331 provider: None,
332 active_plugins: Vec::new(),
333 plugin_factories: Vec::new(),
334 plugin_options: PluginOptions::default(),
335 }
336 }
337
338 pub async fn resume(&self, parked: ParkedSession) -> Result<LashSession> {
351 let plugin_host = build_plugin_host(
356 self.protocol_factory.as_ref(),
357 self.plugin_factories.as_ref(),
358 Vec::new(),
359 )?;
360 let mut env = self.env.clone();
361 env.core = plugin_host.install_process_engine_contributions(
362 env.core.clone(),
363 self.process_lifecycle_available,
364 )?;
365 env.plugin_host = Some(Arc::new(plugin_host));
366 let effect_host = Arc::clone(&env.core.control.effect_host);
367 let drivers = self.work_driver.drivers().await;
368 env.process_work_driver = drivers.process.clone();
369 env.queued_work_driver = drivers.queued.clone();
370 let runtime = LashRuntime::resume(parked.inner, &env).await?;
371 let handle =
372 RuntimeHandle::with_live_replay_store(runtime, Arc::clone(&self.live_replay_store));
373 Ok(LashSession {
374 runtime: handle,
375 effect_host,
376 parent_session_id: None,
377 active_plugins: Vec::new(),
378 process_phase_probe_slot: self.work_driver.phase_probe_slot(),
379 turn_cancels: crate::turn::TurnCancelRegistry::default(),
380 })
381 }
382
383 pub fn flush_trace_sink(&self) -> Result<()> {
394 if let Some(sink) = self.env.core.tracing.trace_sink.as_ref() {
395 sink.flush()?;
396 }
397 Ok(())
398 }
399
400 pub fn triggers(&self) -> crate::admin::CoreTriggerAdmin {
401 crate::admin::CoreTriggerAdmin { core: self.clone() }
402 }
403
404 pub fn processes(&self) -> crate::admin::Processes {
405 crate::admin::Processes { core: self.clone() }
406 }
407
408 pub fn completions(&self) -> crate::admin::Completions {
409 crate::admin::Completions { core: self.clone() }
410 }
411
412 pub fn effect_host(&self) -> Arc<dyn EffectHost> {
413 Arc::clone(&self.env.core.control.effect_host)
414 }
415
416 pub async fn delete_session(
417 &self,
418 session_id: impl AsRef<str>,
419 scoped_effect_controller: ScopedEffectController<'_>,
420 ) -> Result<SessionDeleteReport> {
421 let session_id = session_id.as_ref().to_string();
422 let Some(store_factory) = self.store_factory.as_ref() else {
423 return Err(EmbedError::MissingSessionStoreFactory);
424 };
425 let process = if let Some(process_registry) = self.env.process_registry.as_ref() {
426 let invocation = RuntimeInvocation::effect(
427 RuntimeScope::new(session_id.clone()),
428 format!("process:delete-session:{session_id}"),
429 RuntimeEffectKind::Process,
430 format!("{session_id}:delete-session"),
431 );
432 let outcome = scoped_effect_controller
433 .controller()
434 .execute_effect(
435 RuntimeEffectEnvelope::new(
436 invocation,
437 RuntimeEffectCommand::process(ProcessCommand::DeleteSession {
438 session_id: session_id.clone(),
439 }),
440 ),
441 RuntimeEffectLocalExecutor::processes(Arc::clone(process_registry)),
442 )
443 .await
444 .map_err(|err| EmbedError::SessionDeleteProcess {
445 session_id: session_id.clone(),
446 message: err.to_string(),
447 })?;
448 match outcome {
449 RuntimeEffectOutcome::Process {
450 result: ProcessEffectOutcome::DeleteSession { report },
451 } => Some(report),
452 other => {
453 return Err(EmbedError::SessionDeleteProcess {
454 session_id,
455 message: format!(
456 "process delete returned the wrong outcome: {}",
457 other.kind().as_str()
458 ),
459 });
460 }
461 }
462 } else {
463 None
464 };
465 if let Some(trigger_store) = self.env.trigger_store.as_ref() {
466 trigger_store
467 .delete_session_subscriptions(&session_id)
468 .await
469 .map_err(|err| EmbedError::SessionDeleteProcess {
470 session_id: session_id.clone(),
471 message: err.to_string(),
472 })?;
473 }
474 self.env
475 .core
476 .control
477 .effect_host
478 .revoke_await_events_for_session(&session_id)
479 .await
480 .map_err(|err| EmbedError::SessionDeleteProcess {
481 session_id: session_id.clone(),
482 message: err.to_string(),
483 })?;
484 store_factory
485 .delete_session(&session_id)
486 .await
487 .map_err(|message| EmbedError::StoreFactory {
488 session_id: session_id.clone(),
489 message,
490 })?;
491 Ok(SessionDeleteReport {
492 session_id,
493 process,
494 })
495 }
496
497 pub fn process_registry(&self) -> Option<Arc<dyn ProcessRegistry>> {
498 self.env.process_registry.as_ref().cloned()
499 }
500
501 pub fn durable_process_worker_config(&self) -> Result<DurableProcessWorkerConfig> {
502 self.durable_process_worker_config_with_plugins(std::iter::empty::<Arc<dyn PluginFactory>>())
503 }
504
505 pub fn durable_process_worker_config_with_plugins(
506 &self,
507 extra_plugin_factories: impl IntoIterator<Item = Arc<dyn PluginFactory>>,
508 ) -> Result<DurableProcessWorkerConfig> {
509 let Some(process_registry) = self.process_registry() else {
510 return Err(EmbedError::MissingProcessRegistry);
511 };
512 let Some(store_factory) = self.store_factory.as_ref() else {
513 return Err(EmbedError::MissingProcessWorkerStoreFactory);
514 };
515 let plugin_host = build_plugin_host(
516 self.protocol_factory.as_ref(),
517 self.plugin_factories.as_ref(),
518 extra_plugin_factories.into_iter().collect(),
519 )?;
520 let runtime_host = plugin_host.install_process_engine_contributions(
521 self.env.core.clone(),
522 self.process_lifecycle_available,
523 )?;
524 let mut config = DurableProcessWorkerConfig::new(
525 Arc::new(plugin_host),
526 runtime_host,
527 Arc::clone(store_factory),
528 process_registry,
529 )
530 .with_session_policy(self.policy.clone())
531 .with_residency(self.env.residency);
532 if let Some(trigger_store) = self.env.trigger_store.as_ref() {
533 config = config.with_trigger_store(Arc::clone(trigger_store));
534 }
535 if let Some(driver) = self.work_driver.configured_process_work_driver() {
536 config = config.with_process_work_driver(driver);
537 }
538 if let Some(driver) = self.work_driver.configured_queued_work_driver() {
539 config = config.with_queued_work_driver(driver);
540 }
541 Ok(config)
542 }
543}
544
545fn default_runtime_stack() -> PluginStack {
546 lash_plugin_tool_output_budget::tool_output_budget_stack()
547}
548
549#[derive(Default)]
550pub struct LashCoreBuilder {
551 pub(crate) protocol_factory: Option<Arc<dyn PluginFactory>>,
552 session_spec: SessionSpec,
553 provider: Option<ProviderHandle>,
554 pub(crate) store_factory: Option<Arc<dyn SessionStoreFactory>>,
555 child_store_factory: Option<Arc<dyn SessionStoreFactory>>,
556 effect_host: Option<Arc<dyn EffectHost>>,
560 attachment_store: Option<Arc<dyn AttachmentStore>>,
561 process_env_store: Option<Arc<dyn ProcessExecutionEnvStore>>,
562 trigger_store: Option<Arc<dyn lash_core::TriggerStore>>,
563 prompt: Option<PromptLayer>,
565 trace_sink: Option<Arc<dyn lash_trace::TraceSink>>,
566 trace_level: Option<lash_trace::TraceLevel>,
567 trace_context: Option<lash_trace::TraceContext>,
568 termination: Option<TerminationPolicy>,
569 runtime_host_config: Option<RuntimeHostConfig>,
571 tool_providers: Vec<Arc<dyn ToolProvider>>,
572 plugin_stack: PluginStack,
573 plugin_host: Option<PluginHost>,
574 residency: Option<Residency>,
575 lease_timings: Option<lash_core::LeaseTimings>,
576 process_work_source: ProcessWorkSource,
579 queued_work_source: QueuedWorkSource,
580 live_replay_store: Option<Arc<dyn LiveReplayStore>>,
581}
582
583impl LashCoreBuilder {
584 pub fn protocol_plugin(mut self, plugin: Arc<dyn PluginFactory>) -> Self {
585 self.protocol_factory = Some(plugin);
586 self
587 }
588
589 pub fn provider(mut self, provider: ProviderHandle) -> Self {
590 self.session_spec = self.session_spec.provider_id(provider.kind());
591 self.provider = Some(provider);
592 self
593 }
594
595 pub fn model(mut self, model: lash_core::ModelSpec) -> Self {
596 self.session_spec = self.session_spec.model(model);
597 self
598 }
599
600 pub fn max_turns(mut self, max_turns: usize) -> Self {
601 self.session_spec = self.session_spec.max_turns(max_turns);
602 self
603 }
604
605 pub fn session_spec(mut self, spec: SessionSpec) -> Self {
606 self.session_spec = spec;
607 self
608 }
609
610 pub fn store_factory(mut self, store_factory: Arc<dyn SessionStoreFactory>) -> Self {
618 self.store_factory = Some(store_factory);
619 self
620 }
621
622 pub fn child_store_factory(mut self, store_factory: Arc<dyn SessionStoreFactory>) -> Self {
630 self.child_store_factory = Some(store_factory);
631 self
632 }
633
634 pub fn attachment_store(mut self, attachment_store: Arc<dyn AttachmentStore>) -> Self {
635 self.attachment_store = Some(attachment_store);
636 self
637 }
638
639 pub fn process_env_store(
640 mut self,
641 process_env_store: Arc<dyn ProcessExecutionEnvStore>,
642 ) -> Self {
643 self.process_env_store = Some(process_env_store);
644 self
645 }
646
647 pub fn effect_host(mut self, effect_host: Arc<dyn EffectHost>) -> Self {
652 self.effect_host = Some(effect_host);
653 self
654 }
655
656 pub fn tools(mut self, tools: Arc<dyn ToolProvider>) -> Self {
657 self.tool_providers.push(tools);
658 self
659 }
660
661 pub fn plugin(mut self, plugin: Arc<dyn PluginFactory>) -> Self {
662 self.plugin_stack.push(plugin);
663 self
664 }
665
666 pub fn plugins(mut self, stack: PluginStack) -> Self {
667 self.plugin_stack = stack;
668 self
669 }
670
671 pub fn configure_plugins(mut self, configure: impl FnOnce(&mut PluginStack)) -> Self {
672 configure(&mut self.plugin_stack);
673 self
674 }
675
676 pub fn trace_sink(mut self, trace_sink: Arc<dyn lash_trace::TraceSink>) -> Self {
677 self.trace_sink = Some(trace_sink);
678 self
679 }
680
681 pub fn trace_jsonl_path(mut self, path: impl Into<std::path::PathBuf>) -> Self {
682 self.trace_sink = Some(Arc::new(lash_trace::JsonlTraceSink::new(path.into())));
683 self
684 }
685
686 pub fn trace_level(mut self, trace_level: lash_trace::TraceLevel) -> Self {
687 self.trace_level = Some(trace_level);
688 self
689 }
690
691 pub fn trace_context(mut self, trace_context: lash_trace::TraceContext) -> Self {
692 self.trace_context = Some(trace_context);
693 self
694 }
695
696 pub fn termination(mut self, termination: TerminationPolicy) -> Self {
697 self.termination = Some(termination);
698 self
699 }
700
701 pub fn residency(mut self, residency: Residency) -> Self {
702 self.residency = Some(residency);
703 self
704 }
705
706 pub fn lease_timings(mut self, lease_timings: lash_core::LeaseTimings) -> Self {
719 self.lease_timings = Some(lease_timings);
720 self
721 }
722
723 pub fn live_replay_store(mut self, live_replay_store: Arc<dyn LiveReplayStore>) -> Self {
727 self.live_replay_store = Some(live_replay_store);
728 self
729 }
730
731 fn resolve_runtime_host_config(&mut self) -> Result<RuntimeHostConfig> {
734 if let Some(base) = self.runtime_host_config.take() {
735 return Ok(self.apply_core_overrides(base));
736 }
737 let effect_host = self
738 .effect_host
739 .take()
740 .ok_or(EmbedError::MissingEffectHost)?;
741 let attachment_store = self
742 .attachment_store
743 .take()
744 .ok_or(EmbedError::MissingAttachmentStore)?;
745 let process_env_store = self
746 .process_env_store
747 .take()
748 .ok_or(EmbedError::MissingProcessEnvStore)?;
749 let core = RuntimeHostConfig::new(effect_host, attachment_store, process_env_store);
750 Ok(self.apply_core_overrides(core))
751 }
752
753 fn apply_core_overrides(&mut self, mut core: RuntimeHostConfig) -> RuntimeHostConfig {
755 if let Some(effect_host) = self.effect_host.take() {
756 core.control.effect_host = effect_host;
757 }
758 if let Some(attachment_store) = self.attachment_store.take() {
759 core.durability.attachment_store = attachment_store;
760 }
761 if let Some(process_env_store) = self.process_env_store.take() {
762 core.durability.process_env_store = process_env_store;
763 }
764 if let Some(prompt) = self.prompt.take() {
765 core.prompt.prompt = prompt;
766 }
767 if let Some(trace_sink) = self.trace_sink.take() {
768 core.tracing.trace_sink = Some(trace_sink);
769 }
770 if let Some(trace_level) = self.trace_level.take() {
771 core.tracing.trace_level = trace_level;
772 }
773 if let Some(trace_context) = self.trace_context.take() {
774 core.tracing.trace_context = trace_context;
775 }
776 if let Some(termination) = self.termination.take() {
777 core.control.termination = termination;
778 }
779 if let Some(lease_timings) = self.lease_timings.take() {
780 core.control.lease_timings = lease_timings;
781 }
782 core
783 }
784
785 fn effective_session_store_tier(&self) -> Option<DurabilityTier> {
798 self.child_store_factory
799 .as_ref()
800 .or(self.store_factory.as_ref())
801 .map(|factory| factory.durability_tier())
802 }
803
804 fn ensure_store_peer_coherence(
813 session_store_tier: Option<DurabilityTier>,
814 trigger_store_tier: Option<DurabilityTier>,
815 process_registry_tier: Option<DurabilityTier>,
816 core: &RuntimeHostConfig,
817 ) -> Result<()> {
818 let attachment_tier = Some(
819 core.durability
820 .attachment_store
821 .persistence()
822 .durability_tier(),
823 );
824 let process_env_tier = Some(core.durability.process_env_store.durability_tier());
825 let effect_host_tier = Some(core.control.effect_host.durability_tier());
826
827 if session_store_tier == Some(DurabilityTier::Durable) {
828 if attachment_tier == Some(DurabilityTier::Inline) {
829 return Err(EmbedError::DurableStorePeerRequired {
830 facet: "attachment store",
831 });
832 }
833 if process_env_tier == Some(DurabilityTier::Inline) {
834 return Err(EmbedError::DurableStorePeerRequired {
835 facet: "process execution environment store",
836 });
837 }
838 for engine in core.process_engines.engines() {
841 if engine.durability_tier() == DurabilityTier::Inline {
842 return Err(EmbedError::DurableStorePeerRequired {
843 facet: engine.kind(),
844 });
845 }
846 }
847 }
848
849 if process_registry_tier == Some(DurabilityTier::Durable) {
850 if session_store_tier != Some(DurabilityTier::Durable) {
851 return Err(EmbedError::DurableProcessRegistryRequiresStoreFactory);
852 }
853 if trigger_store_tier != Some(DurabilityTier::Durable) {
854 return Err(EmbedError::DurableStorePeerRequired {
855 facet: "trigger store",
856 });
857 }
858 if process_env_tier != Some(DurabilityTier::Durable) {
859 return Err(EmbedError::DurableStorePeerRequired {
860 facet: "process execution environment store",
861 });
862 }
863 }
864
865 if trigger_store_tier == Some(DurabilityTier::Durable) {
866 if session_store_tier != Some(DurabilityTier::Durable) {
867 return Err(EmbedError::DurableStorePeerRequired {
868 facet: "session store factory",
869 });
870 }
871 if process_env_tier != Some(DurabilityTier::Durable) {
872 return Err(EmbedError::DurableStorePeerRequired {
873 facet: "process execution environment store",
874 });
875 }
876 if process_registry_tier == Some(DurabilityTier::Inline) {
877 return Err(EmbedError::DurableStorePeerRequired {
878 facet: "process registry",
879 });
880 }
881 }
882
883 if effect_host_tier == Some(DurabilityTier::Durable) {
884 if attachment_tier != Some(DurabilityTier::Durable) {
885 return Err(EmbedError::DurableStorePeerRequired {
886 facet: "attachment store",
887 });
888 }
889 if process_env_tier != Some(DurabilityTier::Durable) {
890 return Err(EmbedError::DurableStorePeerRequired {
891 facet: "process execution environment store",
892 });
893 }
894 }
895
896 Ok(())
897 }
898
899 pub fn build(mut self) -> Result<LashCore> {
900 let protocol_factory = self.protocol_factory.clone();
901 if protocol_factory.is_none() && self.plugin_host.is_none() {
902 return Err(EmbedError::MissingProtocolPlugin);
903 }
904 let provider_id = self
905 .session_spec
906 .provider_id
907 .clone()
908 .or_else(|| {
909 self.provider
910 .as_ref()
911 .map(|provider| provider.kind().to_string())
912 })
913 .unwrap_or_default();
914 let model = self
915 .session_spec
916 .model
917 .clone()
918 .ok_or(EmbedError::MissingModelSpec)?;
919
920 let base_policy = SessionPolicy {
921 provider_id,
922 model,
923 max_turns: self.session_spec.max_turns.flatten(),
924 ..SessionPolicy::default()
925 };
926 let policy = self.session_spec.resolve_against(&base_policy);
927
928 let session_store_tier = self.effective_session_store_tier();
931 let trigger_store_tier = self
932 .trigger_store
933 .as_ref()
934 .map(|store| store.durability_tier());
935 let process_registry_tier = self
936 .process_work_source
937 .process_registry()
938 .map(|registry| registry.durability_tier());
939
940 let mut core = self.resolve_runtime_host_config()?;
941 if let Some(provider) = self.provider.clone() {
942 core.providers.provider_resolver =
943 Arc::new(lash_core::SingleProviderResolver::new(provider));
944 }
945 let plugin_factories = if let Some(plugin_host) = self.plugin_host {
946 plugin_host.factories().to_vec()
947 } else {
948 let mut factories = Vec::new();
949 if !self.tool_providers.is_empty() {
950 let spec = self
951 .tool_providers
952 .into_iter()
953 .fold(PluginSpec::new(), PluginSpec::with_tool_provider);
954 factories.push(Arc::new(StaticPluginFactory::new("embed_tools", spec))
955 as Arc<dyn PluginFactory>);
956 }
957 factories.extend(self.plugin_stack.into_factories());
958 factories
959 };
960 let default_plugin_host =
961 build_plugin_host(protocol_factory.as_ref(), &plugin_factories, Vec::new())?;
962 let process_lifecycle_available = self.process_work_source.has_registry();
966 let core_with_engines = default_plugin_host
974 .install_process_engine_contributions(core.clone(), process_lifecycle_available)?;
975 Self::ensure_store_peer_coherence(
978 session_store_tier,
979 trigger_store_tier,
980 process_registry_tier,
981 &core_with_engines,
982 )?;
983
984 let process_registry = self.process_work_source.process_registry();
985
986 let process_work_driver = Self::resolve_process_work_driver(
992 &self.process_work_source,
993 &default_plugin_host,
994 &core,
995 process_lifecycle_available,
996 self.child_store_factory
999 .as_ref()
1000 .or(self.store_factory.as_ref()),
1001 &policy,
1002 self.residency.unwrap_or_default(),
1003 self.trigger_store.as_ref(),
1004 )?;
1005
1006 let live_replay_clock = Arc::clone(&core.clock);
1007 let mut env_builder = RuntimeEnvironment::builder()
1008 .with_plugin_host(Arc::new(default_plugin_host))
1009 .with_runtime_host_config(core);
1010 if let Some(process_registry) = process_registry.as_ref() {
1011 env_builder = env_builder.with_process_registry(Arc::clone(process_registry));
1012 }
1013 if let Some(residency) = self.residency {
1014 env_builder = env_builder.with_residency(residency);
1015 }
1016 if let Some(child_store_factory) = self
1017 .child_store_factory
1018 .as_ref()
1019 .or(self.store_factory.as_ref())
1020 {
1021 env_builder = env_builder.with_session_store_factory(Arc::clone(child_store_factory));
1022 }
1023 if let Some(trigger_store) = self.trigger_store.as_ref() {
1024 env_builder = env_builder.with_trigger_store(Arc::clone(trigger_store));
1025 }
1026 let live_replay_store = self.live_replay_store.take().unwrap_or_else(|| {
1027 Arc::new(InMemoryLiveReplayStore::with_clock(
1028 lash_core::InMemoryLiveReplayStoreConfig::default(),
1029 live_replay_clock,
1030 ))
1031 });
1032 let env = env_builder.build();
1033 let queued_work_driver = Self::resolve_queued_work_driver(
1034 &self.queued_work_source,
1035 env.clone(),
1036 policy.clone(),
1037 protocol_factory.clone(),
1038 Arc::new(plugin_factories.clone()),
1039 self.child_store_factory
1040 .as_ref()
1041 .or(self.store_factory.as_ref()),
1042 Arc::clone(&live_replay_store),
1043 process_lifecycle_available,
1044 );
1045 let work_driver = InlineWorkDriverSetup {
1046 process: process_work_driver,
1047 queued: queued_work_driver,
1048 };
1049
1050 Ok(LashCore {
1051 env,
1052 policy,
1053 store_factory: self.store_factory,
1054 plugin_factories: Arc::new(plugin_factories),
1055 provider: self.provider,
1056 live_replay_store,
1057 protocol_factory,
1058 process_lifecycle_available,
1059 work_driver: Arc::new(InlineWorkDriverSlot::new(work_driver)),
1060 })
1061 }
1062
1063 #[allow(clippy::too_many_arguments)]
1074 fn resolve_process_work_driver(
1075 process_work_source: &ProcessWorkSource,
1076 worker_plugin_host: &PluginHost,
1077 core: &RuntimeHostConfig,
1078 process_lifecycle_available: bool,
1079 store_factory: Option<&Arc<dyn SessionStoreFactory>>,
1080 policy: &SessionPolicy,
1081 residency: lash_core::Residency,
1082 trigger_store: Option<&Arc<dyn lash_core::TriggerStore>>,
1083 ) -> Result<ProcessWorkDriverSetup> {
1084 let process_registry = match process_work_source {
1085 ProcessWorkSource::None => return Ok(ProcessWorkDriverSetup::None),
1086 ProcessWorkSource::External(driver) => {
1087 return Ok(ProcessWorkDriverSetup::External {
1088 driver: driver.clone(),
1089 });
1090 }
1091 ProcessWorkSource::Inline { registry } => Arc::clone(registry),
1092 };
1093 let Some(store_factory) = store_factory else {
1097 return Err(EmbedError::ProcessRegistryRequiresStoreFactory);
1098 };
1099 let runtime_host = worker_plugin_host
1104 .install_process_engine_contributions(core.clone(), process_lifecycle_available)?;
1105 let phase_probe_slot = lash_core::runtime::RuntimeTurnPhaseProbeSlot::default();
1106 let config = Box::new(
1107 DurableProcessWorkerConfig::new(
1108 Arc::new(worker_plugin_host.clone()),
1109 runtime_host,
1110 Arc::clone(store_factory),
1111 process_registry,
1112 )
1113 .with_session_policy(policy.clone())
1114 .with_trigger_store(trigger_store.cloned().unwrap_or_else(|| {
1115 Arc::new(lash_core::InMemoryTriggerStore::with_clock(Arc::clone(
1116 &core.clock,
1117 )))
1118 }))
1119 .with_residency(residency)
1120 .with_turn_phase_probe_slot(phase_probe_slot),
1121 );
1122 Ok(ProcessWorkDriverSetup::LazyDefault { config })
1123 }
1124
1125 #[allow(clippy::too_many_arguments)]
1126 fn resolve_queued_work_driver(
1127 queued_work_source: &QueuedWorkSource,
1128 env: RuntimeEnvironment,
1129 policy: SessionPolicy,
1130 protocol_factory: Option<Arc<dyn PluginFactory>>,
1131 plugin_factories: Arc<Vec<Arc<dyn PluginFactory>>>,
1132 store_factory: Option<&Arc<dyn SessionStoreFactory>>,
1133 live_replay_store: Arc<dyn LiveReplayStore>,
1134 process_lifecycle_available: bool,
1135 ) -> QueuedWorkDriverSetup {
1136 match queued_work_source {
1137 QueuedWorkSource::None => QueuedWorkDriverSetup::None,
1138 QueuedWorkSource::External(driver) => QueuedWorkDriverSetup::External {
1139 driver: driver.clone(),
1140 },
1141 QueuedWorkSource::LazyDefault => match store_factory {
1142 Some(store_factory) => QueuedWorkDriverSetup::LazyDefault {
1143 config: Arc::new(InlineQueuedWorkRunConfig::new(
1144 env,
1145 policy,
1146 protocol_factory,
1147 plugin_factories,
1148 Arc::clone(store_factory),
1149 live_replay_store,
1150 process_lifecycle_available,
1151 )),
1152 },
1153 None => QueuedWorkDriverSetup::None,
1154 },
1155 }
1156 }
1157
1158 pub fn advanced(self) -> AdvancedLashCoreBuilder {
1159 AdvancedLashCoreBuilder { builder: self }
1160 }
1161
1162 pub fn process_registry(mut self, process_registry: Arc<dyn ProcessRegistry>) -> Self {
1163 self.process_work_source = ProcessWorkSource::Inline {
1164 registry: process_registry,
1165 };
1166 self
1167 }
1168
1169 pub fn trigger_store(mut self, store: Arc<dyn lash_core::TriggerStore>) -> Self {
1170 self.trigger_store = Some(store);
1171 self
1172 }
1173
1174 pub fn process_work_driver(mut self, driver: ProcessWorkDriver) -> Self {
1181 self.process_work_source = ProcessWorkSource::External(driver);
1182 self
1183 }
1184
1185 pub fn queued_work_driver(mut self, driver: QueuedWorkDriver) -> Self {
1187 self.queued_work_source = QueuedWorkSource::External(driver);
1188 self
1189 }
1190
1191 pub fn disable_queued_work_driver(mut self) -> Self {
1192 self.queued_work_source = QueuedWorkSource::None;
1193 self
1194 }
1195}
1196
1197pub(crate) fn build_plugin_host(
1198 protocol_factory: Option<&Arc<dyn PluginFactory>>,
1199 common_factories: &[Arc<dyn PluginFactory>],
1200 extra_factories: Vec<Arc<dyn PluginFactory>>,
1201) -> Result<PluginHost> {
1202 let mut factories = Vec::with_capacity(
1203 usize::from(protocol_factory.is_some()) + common_factories.len() + extra_factories.len(),
1204 );
1205 if let Some(protocol_factory) = protocol_factory {
1206 factories.push(Arc::clone(protocol_factory));
1207 }
1208 factories.extend(common_factories.iter().cloned());
1209 factories.extend(extra_factories);
1210 Ok(PluginHost::new(factories))
1211}
1212
1213impl PromptLayerSink for LashCoreBuilder {
1214 fn prompt_layer_mut(&mut self) -> &mut PromptLayer {
1215 self.prompt.get_or_insert_with(PromptLayer::new)
1216 }
1217}
1218
1219pub struct AdvancedLashCoreBuilder {
1220 builder: LashCoreBuilder,
1221}
1222
1223impl AdvancedLashCoreBuilder {
1224 pub fn runtime_host_config(mut self, core: lash_core::RuntimeHostConfig) -> Self {
1225 self.builder.runtime_host_config = Some(core);
1226 self
1227 }
1228
1229 pub fn plugin_host(mut self, plugin_host: PluginHost) -> Self {
1230 self.builder.plugin_host = Some(plugin_host);
1231 self
1232 }
1233
1234 pub fn build(self) -> Result<LashCore> {
1235 self.builder.build()
1236 }
1237}