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) process_execution_concurrency: usize,
23 pub(crate) work_driver: Arc<InlineWorkDriverSlot>,
26}
27
28pub(crate) enum ProcessWorkDriverSetup {
31 None,
33 LazyDefault {
39 config: Box<DurableProcessWorkerConfig>,
40 },
41 External { driver: ProcessWorkDriver },
43}
44
45#[derive(Clone, Default)]
46pub(crate) enum ProcessWorkSource {
47 #[default]
48 None,
49 Inline {
50 registry: Arc<dyn ProcessRegistry>,
51 hub: Option<lash_core::ProcessChangeHub>,
52 },
53 External(ProcessWorkDriver),
54}
55
56impl ProcessWorkSource {
57 fn process_registry(&self) -> Option<Arc<dyn ProcessRegistry>> {
58 match self {
59 Self::None => None,
60 Self::Inline { registry, .. } => Some(Arc::clone(registry)),
61 Self::External(driver) => Some(driver.process_registry()),
62 }
63 }
64
65 fn has_registry(&self) -> bool {
66 !matches!(self, Self::None)
67 }
68
69 fn watched(self, sink: Option<Arc<dyn lash_core::ProcessEventSink>>) -> Self {
70 match self {
71 Self::Inline {
72 registry,
73 hub: None,
74 } => {
75 let (registry, hub) = lash_core::watch_process_registry_with_sink(registry, sink);
76 Self::Inline {
77 registry,
78 hub: Some(hub),
79 }
80 }
81 other => other,
85 }
86 }
87}
88
89#[derive(Clone, Default)]
90pub(crate) enum QueuedWorkSource {
91 None,
92 #[default]
93 LazyDefault,
94 External(QueuedWorkDriver),
95}
96
97pub(crate) enum QueuedWorkDriverSetup {
98 None,
99 LazyDefault {
100 config: Arc<InlineQueuedWorkRunConfig>,
101 },
102 External {
103 driver: QueuedWorkDriver,
104 },
105}
106
107pub(crate) struct InlineWorkDriverSetup {
108 process: ProcessWorkDriverSetup,
109 queued: QueuedWorkDriverSetup,
110}
111
112#[derive(Clone, Default)]
113pub(crate) struct ResolvedWorkDrivers {
114 pub(crate) process: Option<ProcessWorkDriver>,
115 pub(crate) queued: Option<QueuedWorkDriver>,
116 pub(crate) drive_process_on_open: bool,
117}
118
119pub(crate) struct InlineWorkDriverSlot {
125 setup: InlineWorkDriverSetup,
126 drivers: tokio::sync::OnceCell<ResolvedWorkDrivers>,
127 phase_probe_slot: Option<lash_core::runtime::RuntimeTurnPhaseProbeSlot>,
128}
129
130impl InlineWorkDriverSlot {
131 fn new(setup: InlineWorkDriverSetup) -> Self {
132 let phase_probe_slot = match &setup.process {
133 ProcessWorkDriverSetup::LazyDefault { config } => {
134 Some(config.turn_phase_probe_slot.clone())
135 }
136 ProcessWorkDriverSetup::None | ProcessWorkDriverSetup::External { .. } => None,
137 };
138 Self {
139 setup,
140 drivers: tokio::sync::OnceCell::new(),
141 phase_probe_slot,
142 }
143 }
144
145 pub(crate) async fn drivers(&self) -> ResolvedWorkDrivers {
148 self.drivers
149 .get_or_init(|| async {
150 let queued = match &self.setup.queued {
151 QueuedWorkDriverSetup::None => None,
152 QueuedWorkDriverSetup::External { driver } => Some(driver.clone()),
153 QueuedWorkDriverSetup::LazyDefault { config } => Some(QueuedWorkDriver::new(
154 Arc::new(InlineQueuedWorkRunHandle::new(Arc::clone(config))),
155 )),
156 };
157 let (process, drive_process_on_open) = match &self.setup.process {
158 ProcessWorkDriverSetup::None => (None, false),
159 ProcessWorkDriverSetup::External { driver } => (Some(driver.clone()), false),
160 ProcessWorkDriverSetup::LazyDefault { config } => {
161 let mut config = (**config).clone();
162 if let Some(driver) = queued.clone() {
163 config = config.with_queued_work_driver(driver);
164 }
165 let registry = Arc::clone(&config.process_registry);
166 let hub = config.process_change_hub.clone();
167 let worker = DurableProcessWorker::new(config);
168 let driver = if let Some(hub) = hub {
169 ProcessWorkDriver::from_watched(
170 registry,
171 hub,
172 Arc::new(lash_core::InlineProcessRunHandle::new(worker)),
173 )
174 } else {
175 ProcessWorkDriver::inline(registry, worker)
176 };
177 (Some(driver), true)
178 }
179 };
180 ResolvedWorkDrivers {
181 process,
182 queued,
183 drive_process_on_open,
184 }
185 })
186 .await
187 .clone()
188 }
189
190 pub(crate) fn phase_probe_slot(&self) -> Option<lash_core::runtime::RuntimeTurnPhaseProbeSlot> {
191 self.phase_probe_slot.clone()
192 }
193
194 fn configured_process_work_driver(&self) -> Option<ProcessWorkDriver> {
195 match &self.setup.process {
196 ProcessWorkDriverSetup::External { driver } => Some(driver.clone()),
197 ProcessWorkDriverSetup::None | ProcessWorkDriverSetup::LazyDefault { .. } => None,
198 }
199 }
200
201 fn configured_queued_work_driver(&self) -> Option<QueuedWorkDriver> {
202 match &self.setup.queued {
203 QueuedWorkDriverSetup::External { driver } => Some(driver.clone()),
204 QueuedWorkDriverSetup::None | QueuedWorkDriverSetup::LazyDefault { .. } => None,
205 }
206 }
207}
208
209pub(crate) struct InlineQueuedWorkRunConfig {
210 env: RuntimeEnvironment,
211 policy: SessionPolicy,
212 protocol_factory: Option<Arc<dyn PluginFactory>>,
213 plugin_factories: Arc<Vec<Arc<dyn PluginFactory>>>,
214 store_factory: Arc<dyn SessionStoreFactory>,
215 live_replay_store: Arc<dyn LiveReplayStore>,
216 process_lifecycle_available: bool,
217}
218
219impl InlineQueuedWorkRunConfig {
220 fn new(
221 env: RuntimeEnvironment,
222 policy: SessionPolicy,
223 protocol_factory: Option<Arc<dyn PluginFactory>>,
224 plugin_factories: Arc<Vec<Arc<dyn PluginFactory>>>,
225 store_factory: Arc<dyn SessionStoreFactory>,
226 live_replay_store: Arc<dyn LiveReplayStore>,
227 process_lifecycle_available: bool,
228 ) -> Self {
229 Self {
230 env,
231 policy,
232 protocol_factory,
233 plugin_factories,
234 store_factory,
235 live_replay_store,
236 process_lifecycle_available,
237 }
238 }
239}
240
241struct InlineQueuedWorkRunHandle {
242 config: Arc<InlineQueuedWorkRunConfig>,
243}
244
245impl InlineQueuedWorkRunHandle {
246 fn new(config: Arc<InlineQueuedWorkRunConfig>) -> Self {
247 Self { config }
248 }
249}
250
251#[async_trait]
252impl QueuedWorkRunHandle for InlineQueuedWorkRunHandle {
253 async fn run_queued_work(
254 &self,
255 request: QueuedWorkRunRequest,
256 ) -> std::result::Result<(), lash_core::PluginError> {
257 let Some(session_id) = request.session_id else {
258 return Ok(());
259 };
260 let reason = request.reason;
261 let mut policy = self.config.policy.clone();
262 policy.session_id = Some(session_id.clone());
263 let store = self
264 .config
265 .store_factory
266 .create_store(&SessionStoreCreateRequest {
267 session_id: session_id.clone(),
268 relation: SessionRelation::default(),
269 policy: policy.clone(),
270 })
271 .await
272 .map_err(lash_core::PluginError::Session)?;
273 let state = crate::session::load_state_for_residency(
274 self.config.env.residency,
275 &session_id,
276 &policy,
277 store.as_ref(),
278 )
279 .await
280 .map_err(|err| lash_core::PluginError::Session(err.to_string()))?;
281 let plugin_host = build_plugin_host(
282 self.config.protocol_factory.as_ref(),
283 self.config.plugin_factories.as_ref(),
284 Vec::new(),
285 )
286 .map_err(|err| lash_core::PluginError::Session(err.to_string()))?;
287 let mut env = self.config.env.clone();
288 env.core = plugin_host
289 .install_process_engine_contributions(
290 env.core.clone(),
291 self.config.process_lifecycle_available,
292 )
293 .map_err(|err| lash_core::PluginError::Session(err.to_string()))?;
294 env.plugin_host = Some(Arc::new(plugin_host));
295 let effect_host = Arc::clone(&env.core.control.effect_host);
296 let runtime = LashRuntime::from_environment(&env, policy, state, Some(store))
297 .await
298 .map_err(|err| lash_core::PluginError::Session(err.to_string()))?;
299 let handle = RuntimeHandle::with_live_replay_store(
300 runtime,
301 Arc::clone(&self.config.live_replay_store),
302 );
303 let scope = lash_core::ExecutionScope::queue_drain(session_id, reason);
304 let scoped = effect_host
305 .scoped(scope)
306 .map_err(|err| lash_core::PluginError::Session(err.to_string()))?;
307 crate::turn::stream_next_queued_prepared_turn(
308 &handle,
309 crate::turn::TurnSinks::default(),
310 scoped,
311 CancellationToken::new(),
312 lash_core::TurnCancelOriginHint::default(),
313 &[],
314 )
315 .await
316 .map_err(|err| lash_core::PluginError::Session(err.to_string()))?;
317 Ok(())
318 }
319}
320
321#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
322pub struct SessionDeleteReport {
323 pub session_id: String,
324 pub process: Option<lash_core::ProcessSessionDeleteReport>,
325}
326
327impl LashCore {
328 pub fn builder() -> LashCoreBuilder {
329 LashCoreBuilder::default()
330 }
331
332 pub fn standard_builder() -> LashCoreBuilder {
335 LashCore::builder()
336 .protocol_plugin(Arc::new(
337 lash_protocol_standard::StandardProtocolPluginFactory::new(),
338 ))
339 .plugins(default_runtime_stack())
340 }
341
342 #[cfg(feature = "rlm")]
350 pub fn rlm_builder(factory: crate::rlm::RlmProtocolPluginFactory) -> LashCoreBuilder {
351 LashCore::builder()
352 .protocol_plugin(Arc::new(factory))
353 .plugins(default_runtime_stack())
354 }
355
356 pub fn session(&self, session_id: impl Into<String>) -> SessionBuilder {
357 SessionBuilder {
358 core: self.clone(),
359 session_id: session_id.into(),
360 spec: SessionSpec::inherit(),
361 parent_session_id: None,
362 session_execution_owner: None,
363 store: None,
364 provider: None,
365 active_plugins: Vec::new(),
366 plugin_factories: Vec::new(),
367 plugin_options: PluginOptions::default(),
368 }
369 }
370
371 pub async fn resume(&self, parked: ParkedSession) -> Result<LashSession> {
384 let plugin_host = build_plugin_host(
389 self.protocol_factory.as_ref(),
390 self.plugin_factories.as_ref(),
391 Vec::new(),
392 )?;
393 let mut env = self.env.clone();
394 env.core = plugin_host.install_process_engine_contributions(
395 env.core.clone(),
396 self.process_lifecycle_available,
397 )?;
398 env.plugin_host = Some(Arc::new(plugin_host));
399 let effect_host = Arc::clone(&env.core.control.effect_host);
400 let drivers = self.work_driver.drivers().await;
401 env.process_work_driver = drivers.process.clone();
402 env.queued_work_driver = drivers.queued.clone();
403 let runtime = LashRuntime::resume(parked.inner, &env).await?;
404 let handle =
405 RuntimeHandle::with_live_replay_store(runtime, Arc::clone(&self.live_replay_store));
406 Ok(LashSession {
407 runtime: handle,
408 effect_host,
409 parent_session_id: None,
410 active_plugins: Vec::new(),
411 process_phase_probe_slot: self.work_driver.phase_probe_slot(),
412 turn_cancels: crate::turn::TurnCancelRegistry::default(),
413 })
414 }
415
416 pub fn flush_trace_sink(&self) -> Result<()> {
427 if let Some(sink) = self.env.core.tracing.trace_sink.as_ref() {
428 sink.flush()?;
429 }
430 Ok(())
431 }
432
433 pub fn triggers(&self) -> crate::admin::CoreTriggerAdmin {
434 crate::admin::CoreTriggerAdmin { core: self.clone() }
435 }
436
437 pub fn processes(&self) -> crate::process_admin::Processes {
438 crate::process_admin::Processes { core: self.clone() }
439 }
440
441 pub fn completions(&self) -> crate::admin::Completions {
442 crate::admin::Completions { core: self.clone() }
443 }
444
445 pub fn effect_host(&self) -> Arc<dyn EffectHost> {
446 Arc::clone(&self.env.core.control.effect_host)
447 }
448
449 pub fn turn_work_driver(&self) -> lash_core::TurnWorkDriver {
455 lash_core::TurnWorkDriver::new(self.effect_host())
456 }
457
458 pub async fn enqueue_turn_input(
465 &self,
466 session_id: impl Into<String>,
467 input: lash_core::TurnInput,
468 ingress: lash_core::TurnInputIngress,
469 id: Option<String>,
470 ) -> Result<lash_core::PendingTurnInput> {
471 lash_core::ensure_durable_effect_input(&input).map_err(EmbedError::Runtime)?;
472 let session_id = session_id.into();
473 let Some(store_factory) = self.store_factory.as_ref() else {
474 return Err(EmbedError::MissingSessionStoreFactory);
475 };
476 let mut policy = self.policy.clone();
477 policy.session_id = Some(session_id.clone());
478 let store = store_factory
479 .create_store(&SessionStoreCreateRequest {
480 session_id: session_id.clone(),
481 relation: SessionRelation::default(),
482 policy,
483 })
484 .await
485 .map_err(|message| EmbedError::StoreFactory {
486 session_id: session_id.clone(),
487 message,
488 })?;
489 let is_next_turn = matches!(ingress, lash_core::TurnInputIngress::NextTurn);
490 let mut draft = lash_core::PendingTurnInputDraft::new(session_id, ingress, input);
491 draft.source_key = id.map(|id| format!("host:{id}"));
492 let enqueued = store
493 .enqueue_pending_turn_input(draft)
494 .await
495 .map_err(|err| {
496 EmbedError::Runtime(lash_core::RuntimeError::new(
497 lash_core::RuntimeErrorCode::StoreCommitFailed,
498 err.to_string(),
499 ))
500 })?;
501 if is_next_turn && let Some(driver) = self.work_driver.drivers().await.queued.as_ref() {
502 driver
503 .claim_and_run_pending(Some(&enqueued.session_id), "queued_turn_input")
504 .await
505 .map_err(|err| {
506 EmbedError::Runtime(lash_core::RuntimeError::new(
507 lash_core::RuntimeErrorCode::Other("queued_work".to_string()),
508 err.to_string(),
509 ))
510 })?;
511 }
512 Ok(enqueued)
513 }
514
515 pub async fn delete_session(
516 &self,
517 session_id: impl AsRef<str>,
518 scoped_effect_controller: ScopedEffectController<'_>,
519 ) -> Result<SessionDeleteReport> {
520 let session_id = session_id.as_ref().to_string();
521 let Some(store_factory) = self.store_factory.as_ref() else {
522 return Err(EmbedError::MissingSessionStoreFactory);
523 };
524 let process = if let Some(process_registry) = self.env.process_registry.as_ref() {
525 let invocation = RuntimeInvocation::effect(
526 RuntimeScope::new(session_id.clone()),
527 format!("process:delete-session:{session_id}"),
528 RuntimeEffectKind::Process,
529 format!("{session_id}:delete-session"),
530 );
531 let outcome = scoped_effect_controller
532 .controller()
533 .execute_effect(
534 RuntimeEffectEnvelope::new(
535 invocation,
536 RuntimeEffectCommand::process(ProcessCommand::DeleteSession {
537 session_id: session_id.clone(),
538 }),
539 ),
540 RuntimeEffectLocalExecutor::processes(
541 Arc::clone(process_registry),
542 self.env.process_work_driver.clone(),
543 ),
544 )
545 .await
546 .map_err(|err| EmbedError::SessionDeleteProcess {
547 session_id: session_id.clone(),
548 message: err.to_string(),
549 })?;
550 match outcome {
551 RuntimeEffectOutcome::Process {
552 result: ProcessEffectOutcome::DeleteSession { report },
553 } => Some(report),
554 other => {
555 return Err(EmbedError::SessionDeleteProcess {
556 session_id,
557 message: format!(
558 "process delete returned the wrong outcome: {}",
559 other.kind().as_str()
560 ),
561 });
562 }
563 }
564 } else {
565 None
566 };
567 if let Some(trigger_store) = self.env.trigger_store.as_ref() {
568 trigger_store
569 .delete_session_subscriptions(&session_id)
570 .await
571 .map_err(|err| EmbedError::SessionDeleteProcess {
572 session_id: session_id.clone(),
573 message: err.to_string(),
574 })?;
575 }
576 self.env
577 .core
578 .control
579 .effect_host
580 .revoke_await_events_for_session(&session_id)
581 .await
582 .map_err(|err| EmbedError::SessionDeleteProcess {
583 session_id: session_id.clone(),
584 message: err.to_string(),
585 })?;
586 store_factory
587 .delete_session(&session_id)
588 .await
589 .map_err(|message| EmbedError::StoreFactory {
590 session_id: session_id.clone(),
591 message,
592 })?;
593 Ok(SessionDeleteReport {
594 session_id,
595 process,
596 })
597 }
598
599 pub fn process_registry(&self) -> Option<Arc<dyn ProcessRegistry>> {
600 self.env.process_registry.as_ref().cloned()
601 }
602
603 pub fn durable_process_worker_config(&self) -> Result<DurableProcessWorkerConfig> {
604 self.durable_process_worker_config_with_plugins(std::iter::empty::<Arc<dyn PluginFactory>>())
605 }
606
607 pub fn durable_process_worker_config_with_plugins(
608 &self,
609 extra_plugin_factories: impl IntoIterator<Item = Arc<dyn PluginFactory>>,
610 ) -> Result<DurableProcessWorkerConfig> {
611 let Some(process_registry) = self.process_registry() else {
612 return Err(EmbedError::MissingProcessRegistry);
613 };
614 let Some(store_factory) = self.store_factory.as_ref() else {
615 return Err(EmbedError::MissingProcessWorkerStoreFactory);
616 };
617 let plugin_host = build_plugin_host(
618 self.protocol_factory.as_ref(),
619 self.plugin_factories.as_ref(),
620 extra_plugin_factories.into_iter().collect(),
621 )?;
622 let runtime_host = plugin_host.install_process_engine_contributions(
623 self.env.core.clone(),
624 self.process_lifecycle_available,
625 )?;
626 let mut config = DurableProcessWorkerConfig::new(
627 Arc::new(plugin_host),
628 runtime_host,
629 Arc::clone(store_factory),
630 process_registry,
631 )
632 .with_session_policy(self.policy.clone())
633 .with_residency(self.env.residency)
634 .with_process_execution_concurrency(self.process_execution_concurrency)?;
635 if let Some(trigger_store) = self.env.trigger_store.as_ref() {
636 config = config.with_trigger_store(Arc::clone(trigger_store));
637 }
638 if let Some(driver) = self.work_driver.configured_process_work_driver() {
639 config = config
640 .with_change_hub(driver.change_hub())
641 .with_process_work_driver(driver);
642 }
643 if let Some(driver) = self.work_driver.configured_queued_work_driver() {
644 config = config.with_queued_work_driver(driver);
645 }
646 Ok(config)
647 }
648}
649
650fn default_runtime_stack() -> PluginStack {
651 lash_plugin_tool_output_budget::tool_output_budget_stack()
652}
653
654#[derive(Default)]
655pub struct LashCoreBuilder {
656 pub(crate) protocol_factory: Option<Arc<dyn PluginFactory>>,
657 session_spec: SessionSpec,
658 provider: Option<ProviderHandle>,
659 pub(crate) store_factory: Option<Arc<dyn SessionStoreFactory>>,
660 child_store_factory: Option<Arc<dyn SessionStoreFactory>>,
661 effect_host: Option<Arc<dyn EffectHost>>,
665 attachment_store: Option<Arc<dyn AttachmentStore>>,
666 process_env_store: Option<Arc<dyn ProcessExecutionEnvStore>>,
667 trigger_store: Option<Arc<dyn lash_core::TriggerStore>>,
668 prompt: Option<PromptLayer>,
670 trace_sink: Option<Arc<dyn lash_trace::TraceSink>>,
671 trace_level: Option<lash_trace::TraceLevel>,
672 trace_context: Option<lash_trace::TraceContext>,
673 termination: Option<TerminationPolicy>,
674 runtime_host_config: Option<RuntimeHostConfig>,
676 tool_providers: Vec<Arc<dyn ToolProvider>>,
677 plugin_stack: PluginStack,
678 plugin_host: Option<PluginHost>,
679 residency: Option<Residency>,
680 lease_timings: Option<lash_core::LeaseTimings>,
681 clock: Option<Arc<dyn lash_core::Clock>>,
682 process_work_source: ProcessWorkSource,
685 process_execution_concurrency: Option<usize>,
687 process_event_sink: Option<Arc<dyn lash_core::ProcessEventSink>>,
690 queued_work_source: QueuedWorkSource,
691 live_replay_store: Option<Arc<dyn LiveReplayStore>>,
692}
693
694impl LashCoreBuilder {
695 pub fn protocol_plugin(mut self, plugin: Arc<dyn PluginFactory>) -> Self {
696 self.protocol_factory = Some(plugin);
697 self
698 }
699
700 pub fn provider(mut self, provider: ProviderHandle) -> Self {
701 self.session_spec = self.session_spec.provider_id(provider.kind());
702 self.provider = Some(provider);
703 self
704 }
705
706 pub fn model(mut self, model: lash_core::ModelSpec) -> Self {
707 self.session_spec = self.session_spec.model(model);
708 self
709 }
710
711 pub fn max_turns(mut self, max_turns: usize) -> Self {
712 self.session_spec = self.session_spec.max_turns(max_turns);
713 self
714 }
715
716 pub fn session_spec(mut self, spec: SessionSpec) -> Self {
717 self.session_spec = spec;
718 self
719 }
720
721 pub fn store_factory(mut self, store_factory: Arc<dyn SessionStoreFactory>) -> Self {
735 self.store_factory = Some(store_factory);
736 self
737 }
738
739 pub fn child_store_factory(mut self, store_factory: Arc<dyn SessionStoreFactory>) -> Self {
749 self.child_store_factory = Some(store_factory);
750 self
751 }
752
753 pub fn attachment_store(mut self, attachment_store: Arc<dyn AttachmentStore>) -> Self {
754 self.attachment_store = Some(attachment_store);
755 self
756 }
757
758 pub fn process_env_store(
759 mut self,
760 process_env_store: Arc<dyn ProcessExecutionEnvStore>,
761 ) -> Self {
762 self.process_env_store = Some(process_env_store);
763 self
764 }
765
766 pub fn process_execution_concurrency(mut self, concurrency: usize) -> Self {
775 self.process_execution_concurrency = Some(concurrency);
776 self
777 }
778
779 pub fn effect_host(mut self, effect_host: Arc<dyn EffectHost>) -> Self {
784 self.effect_host = Some(effect_host);
785 self
786 }
787
788 pub fn tools(mut self, tools: Arc<dyn ToolProvider>) -> Self {
789 self.tool_providers.push(tools);
790 self
791 }
792
793 pub fn plugin(mut self, plugin: Arc<dyn PluginFactory>) -> Self {
794 self.plugin_stack.push(plugin);
795 self
796 }
797
798 pub fn plugins(mut self, stack: PluginStack) -> Self {
799 self.plugin_stack = stack;
800 self
801 }
802
803 pub fn configure_plugins(mut self, configure: impl FnOnce(&mut PluginStack)) -> Self {
804 configure(&mut self.plugin_stack);
805 self
806 }
807
808 pub fn trace_sink(mut self, trace_sink: Arc<dyn lash_trace::TraceSink>) -> Self {
809 self.trace_sink = Some(trace_sink);
810 self
811 }
812
813 pub fn trace_jsonl_path(mut self, path: impl Into<std::path::PathBuf>) -> Self {
814 self.trace_sink = Some(Arc::new(lash_trace::JsonlTraceSink::new(path.into())));
815 self
816 }
817
818 pub fn trace_level(mut self, trace_level: lash_trace::TraceLevel) -> Self {
819 self.trace_level = Some(trace_level);
820 self
821 }
822
823 pub fn trace_context(mut self, trace_context: lash_trace::TraceContext) -> Self {
824 self.trace_context = Some(trace_context);
825 self
826 }
827
828 pub fn termination(mut self, termination: TerminationPolicy) -> Self {
829 self.termination = Some(termination);
830 self
831 }
832
833 pub fn residency(mut self, residency: Residency) -> Self {
834 self.residency = Some(residency);
835 self
836 }
837
838 pub fn lease_timings(mut self, lease_timings: lash_core::LeaseTimings) -> Self {
851 self.lease_timings = Some(lease_timings);
852 self
853 }
854
855 pub fn clock(mut self, clock: Arc<dyn lash_core::Clock>) -> Self {
857 self.clock = Some(clock);
858 self
859 }
860
861 pub fn live_replay_store(mut self, live_replay_store: Arc<dyn LiveReplayStore>) -> Self {
865 self.live_replay_store = Some(live_replay_store);
866 self
867 }
868
869 fn resolve_runtime_host_config(&mut self) -> Result<RuntimeHostConfig> {
872 if let Some(base) = self.runtime_host_config.take() {
873 return Ok(self.apply_core_overrides(base));
874 }
875 let effect_host = self
876 .effect_host
877 .take()
878 .ok_or(EmbedError::MissingEffectHost)?;
879 let attachment_store = self
880 .attachment_store
881 .take()
882 .ok_or(EmbedError::MissingAttachmentStore)?;
883 let process_env_store = self
884 .process_env_store
885 .take()
886 .ok_or(EmbedError::MissingProcessEnvStore)?;
887 let core = RuntimeHostConfig::new(effect_host, attachment_store, process_env_store);
888 Ok(self.apply_core_overrides(core))
889 }
890
891 fn apply_core_overrides(&mut self, mut core: RuntimeHostConfig) -> RuntimeHostConfig {
893 if let Some(effect_host) = self.effect_host.take() {
894 core.control.effect_host = effect_host;
895 }
896 if let Some(attachment_store) = self.attachment_store.take() {
897 core.durability.attachment_store = Arc::new(
898 lash_core::SessionAttachmentStore::ephemeral(attachment_store),
899 );
900 }
901 if let Some(process_env_store) = self.process_env_store.take() {
902 core.durability.process_env_store = process_env_store;
903 }
904 if let Some(prompt) = self.prompt.take() {
905 core.prompt.prompt = prompt;
906 }
907 if let Some(trace_sink) = self.trace_sink.take() {
908 core.tracing.trace_sink = Some(trace_sink);
909 }
910 if let Some(trace_level) = self.trace_level.take() {
911 core.tracing.trace_level = trace_level;
912 }
913 if let Some(trace_context) = self.trace_context.take() {
914 core.tracing.trace_context = trace_context;
915 }
916 if let Some(termination) = self.termination.take() {
917 core.control.termination = termination;
918 }
919 if let Some(lease_timings) = self.lease_timings.take() {
920 core.control.lease_timings = lease_timings;
921 }
922 if let Some(clock) = self.clock.take() {
923 core.clock = clock;
924 }
925 core
926 }
927
928 fn effective_session_store_tier(&self) -> Option<DurabilityTier> {
941 self.child_store_factory
942 .as_ref()
943 .or(self.store_factory.as_ref())
944 .map(|factory| factory.durability_tier())
945 }
946
947 fn ensure_store_peer_coherence(
956 session_store_tier: Option<DurabilityTier>,
957 trigger_store_tier: Option<DurabilityTier>,
958 process_registry_tier: Option<DurabilityTier>,
959 core: &RuntimeHostConfig,
960 ) -> Result<()> {
961 let attachment_tier = Some(
962 core.durability
963 .attachment_store
964 .persistence()
965 .durability_tier(),
966 );
967 let process_env_tier = Some(core.durability.process_env_store.durability_tier());
968 let effect_host_tier = Some(core.control.effect_host.durability_tier());
969
970 if session_store_tier == Some(DurabilityTier::Durable) {
971 if attachment_tier == Some(DurabilityTier::Inline) {
972 return Err(EmbedError::DurableStorePeerRequired {
973 facet: "attachment store",
974 });
975 }
976 if process_env_tier == Some(DurabilityTier::Inline) {
977 return Err(EmbedError::DurableStorePeerRequired {
978 facet: "process execution environment store",
979 });
980 }
981 for engine in core.process_engines.engines() {
984 if engine.durability_tier() == DurabilityTier::Inline {
985 return Err(EmbedError::DurableStorePeerRequired {
986 facet: engine.kind(),
987 });
988 }
989 }
990 }
991
992 if process_registry_tier == Some(DurabilityTier::Durable) {
993 if session_store_tier != Some(DurabilityTier::Durable) {
994 return Err(EmbedError::DurableProcessRegistryRequiresStoreFactory);
995 }
996 if trigger_store_tier != Some(DurabilityTier::Durable) {
997 return Err(EmbedError::DurableStorePeerRequired {
998 facet: "trigger store",
999 });
1000 }
1001 if process_env_tier != Some(DurabilityTier::Durable) {
1002 return Err(EmbedError::DurableStorePeerRequired {
1003 facet: "process execution environment store",
1004 });
1005 }
1006 }
1007
1008 if trigger_store_tier == Some(DurabilityTier::Durable) {
1009 if session_store_tier != Some(DurabilityTier::Durable) {
1010 return Err(EmbedError::DurableStorePeerRequired {
1011 facet: "session store factory",
1012 });
1013 }
1014 if process_env_tier != Some(DurabilityTier::Durable) {
1015 return Err(EmbedError::DurableStorePeerRequired {
1016 facet: "process execution environment store",
1017 });
1018 }
1019 if process_registry_tier == Some(DurabilityTier::Inline) {
1020 return Err(EmbedError::DurableStorePeerRequired {
1021 facet: "process registry",
1022 });
1023 }
1024 }
1025
1026 if effect_host_tier == Some(DurabilityTier::Durable) {
1027 if attachment_tier != Some(DurabilityTier::Durable) {
1028 return Err(EmbedError::DurableStorePeerRequired {
1029 facet: "attachment store",
1030 });
1031 }
1032 if process_env_tier != Some(DurabilityTier::Durable) {
1033 return Err(EmbedError::DurableStorePeerRequired {
1034 facet: "process execution environment store",
1035 });
1036 }
1037 }
1038
1039 Ok(())
1040 }
1041
1042 pub fn build(mut self) -> Result<LashCore> {
1043 let process_execution_concurrency = self
1044 .process_execution_concurrency
1045 .unwrap_or(lash_core::DEFAULT_PROCESS_EXECUTION_CONCURRENCY);
1046 DurableProcessWorkerConfig::validate_process_execution_concurrency(
1047 process_execution_concurrency,
1048 )?;
1049 let protocol_factory = self.protocol_factory.clone();
1050 if protocol_factory.is_none() && self.plugin_host.is_none() {
1051 return Err(EmbedError::MissingProtocolPlugin);
1052 }
1053 let provider_id = self
1054 .session_spec
1055 .provider_id
1056 .clone()
1057 .or_else(|| {
1058 self.provider
1059 .as_ref()
1060 .map(|provider| provider.kind().to_string())
1061 })
1062 .unwrap_or_default();
1063 let model = self
1064 .session_spec
1065 .model
1066 .clone()
1067 .ok_or(EmbedError::MissingModelSpec)?;
1068
1069 let base_policy = SessionPolicy {
1070 provider_id,
1071 model,
1072 max_turns: self.session_spec.max_turns.flatten(),
1073 ..SessionPolicy::default()
1074 };
1075 let policy = self.session_spec.resolve_against(&base_policy);
1076
1077 let session_store_tier = self.effective_session_store_tier();
1080 let trigger_store_tier = self
1081 .trigger_store
1082 .as_ref()
1083 .map(|store| store.durability_tier());
1084 let process_work_source = self
1085 .process_work_source
1086 .clone()
1087 .watched(self.process_event_sink.clone());
1088 let process_registry_tier = process_work_source
1089 .process_registry()
1090 .map(|registry| registry.durability_tier());
1091
1092 let mut core = self.resolve_runtime_host_config()?;
1093 if let Some(provider) = self.provider.clone() {
1094 core.providers.provider_resolver =
1095 Arc::new(lash_core::SingleProviderResolver::new(provider));
1096 }
1097 let plugin_factories = if let Some(plugin_host) = self.plugin_host {
1098 plugin_host.factories().to_vec()
1099 } else {
1100 let mut factories = Vec::new();
1101 if !self.tool_providers.is_empty() {
1102 let spec = self
1103 .tool_providers
1104 .into_iter()
1105 .fold(PluginSpec::new(), PluginSpec::with_tool_provider);
1106 factories.push(Arc::new(StaticPluginFactory::new("embed_tools", spec))
1107 as Arc<dyn PluginFactory>);
1108 }
1109 factories.extend(self.plugin_stack.into_factories());
1110 factories
1111 };
1112 let default_plugin_host =
1113 build_plugin_host(protocol_factory.as_ref(), &plugin_factories, Vec::new())?;
1114 let process_lifecycle_available = process_work_source.has_registry();
1118 let core_with_engines = default_plugin_host
1126 .install_process_engine_contributions(core.clone(), process_lifecycle_available)?;
1127 Self::ensure_store_peer_coherence(
1130 session_store_tier,
1131 trigger_store_tier,
1132 process_registry_tier,
1133 &core_with_engines,
1134 )?;
1135
1136 let process_registry = process_work_source.process_registry();
1137
1138 let process_work_driver = Self::resolve_process_work_driver(
1144 &process_work_source,
1145 &default_plugin_host,
1146 &core,
1147 process_lifecycle_available,
1148 self.child_store_factory
1151 .as_ref()
1152 .or(self.store_factory.as_ref()),
1153 &policy,
1154 self.residency.unwrap_or_default(),
1155 self.trigger_store.as_ref(),
1156 process_execution_concurrency,
1157 )?;
1158
1159 let live_replay_clock = Arc::clone(&core.clock);
1160 let mut env_builder = RuntimeEnvironment::builder()
1161 .with_plugin_host(Arc::new(default_plugin_host))
1162 .with_runtime_host_config(core);
1163 if let Some(process_registry) = process_registry.as_ref() {
1164 env_builder = env_builder.with_process_registry(Arc::clone(process_registry));
1165 }
1166 if let Some(residency) = self.residency {
1167 env_builder = env_builder.with_residency(residency);
1168 }
1169 if let Some(child_store_factory) = self
1170 .child_store_factory
1171 .as_ref()
1172 .or(self.store_factory.as_ref())
1173 {
1174 env_builder = env_builder.with_session_store_factory(Arc::clone(child_store_factory));
1175 }
1176 if let Some(trigger_store) = self.trigger_store.as_ref() {
1177 env_builder = env_builder.with_trigger_store(Arc::clone(trigger_store));
1178 }
1179 let live_replay_store = self.live_replay_store.take().unwrap_or_else(|| {
1180 Arc::new(InMemoryLiveReplayStore::with_clock(
1181 lash_core::InMemoryLiveReplayStoreConfig::default(),
1182 live_replay_clock,
1183 ))
1184 });
1185 let env = env_builder.build();
1186 let queued_work_driver = Self::resolve_queued_work_driver(
1187 &self.queued_work_source,
1188 env.clone(),
1189 policy.clone(),
1190 protocol_factory.clone(),
1191 Arc::new(plugin_factories.clone()),
1192 self.child_store_factory
1193 .as_ref()
1194 .or(self.store_factory.as_ref()),
1195 Arc::clone(&live_replay_store),
1196 process_lifecycle_available,
1197 );
1198 let work_driver = InlineWorkDriverSetup {
1199 process: process_work_driver,
1200 queued: queued_work_driver,
1201 };
1202
1203 Ok(LashCore {
1204 env,
1205 policy,
1206 store_factory: self.store_factory,
1207 plugin_factories: Arc::new(plugin_factories),
1208 provider: self.provider,
1209 live_replay_store,
1210 protocol_factory,
1211 process_lifecycle_available,
1212 process_execution_concurrency,
1213 work_driver: Arc::new(InlineWorkDriverSlot::new(work_driver)),
1214 })
1215 }
1216
1217 #[allow(clippy::too_many_arguments)]
1228 fn resolve_process_work_driver(
1229 process_work_source: &ProcessWorkSource,
1230 worker_plugin_host: &PluginHost,
1231 core: &RuntimeHostConfig,
1232 process_lifecycle_available: bool,
1233 store_factory: Option<&Arc<dyn SessionStoreFactory>>,
1234 policy: &SessionPolicy,
1235 residency: lash_core::Residency,
1236 trigger_store: Option<&Arc<dyn lash_core::TriggerStore>>,
1237 process_execution_concurrency: usize,
1238 ) -> Result<ProcessWorkDriverSetup> {
1239 let (process_registry, process_change_hub) = match process_work_source {
1240 ProcessWorkSource::None => return Ok(ProcessWorkDriverSetup::None),
1241 ProcessWorkSource::External(driver) => {
1242 return Ok(ProcessWorkDriverSetup::External {
1243 driver: driver.clone(),
1244 });
1245 }
1246 ProcessWorkSource::Inline { registry, hub } => (Arc::clone(registry), hub.clone()),
1247 };
1248 let Some(store_factory) = store_factory else {
1252 return Err(EmbedError::ProcessRegistryRequiresStoreFactory);
1253 };
1254 let runtime_host = worker_plugin_host
1259 .install_process_engine_contributions(core.clone(), process_lifecycle_available)?;
1260 let phase_probe_slot = lash_core::runtime::RuntimeTurnPhaseProbeSlot::default();
1261 let mut config = DurableProcessWorkerConfig::new(
1262 Arc::new(worker_plugin_host.clone()),
1263 runtime_host,
1264 Arc::clone(store_factory),
1265 process_registry,
1266 )
1267 .with_session_policy(policy.clone())
1268 .with_trigger_store(trigger_store.cloned().unwrap_or_else(|| {
1269 Arc::new(lash_core::InMemoryTriggerStore::with_clock(Arc::clone(
1270 &core.clock,
1271 )))
1272 }))
1273 .with_residency(residency)
1274 .with_turn_phase_probe_slot(phase_probe_slot)
1275 .with_process_execution_concurrency(process_execution_concurrency)?;
1276 if let Some(hub) = process_change_hub {
1277 config = config.with_change_hub(hub);
1278 }
1279 let config = Box::new(config);
1280 Ok(ProcessWorkDriverSetup::LazyDefault { config })
1281 }
1282
1283 #[allow(clippy::too_many_arguments)]
1284 fn resolve_queued_work_driver(
1285 queued_work_source: &QueuedWorkSource,
1286 env: RuntimeEnvironment,
1287 policy: SessionPolicy,
1288 protocol_factory: Option<Arc<dyn PluginFactory>>,
1289 plugin_factories: Arc<Vec<Arc<dyn PluginFactory>>>,
1290 store_factory: Option<&Arc<dyn SessionStoreFactory>>,
1291 live_replay_store: Arc<dyn LiveReplayStore>,
1292 process_lifecycle_available: bool,
1293 ) -> QueuedWorkDriverSetup {
1294 match queued_work_source {
1295 QueuedWorkSource::None => QueuedWorkDriverSetup::None,
1296 QueuedWorkSource::External(driver) => QueuedWorkDriverSetup::External {
1297 driver: driver.clone(),
1298 },
1299 QueuedWorkSource::LazyDefault => match store_factory {
1300 Some(store_factory) => QueuedWorkDriverSetup::LazyDefault {
1301 config: Arc::new(InlineQueuedWorkRunConfig::new(
1302 env,
1303 policy,
1304 protocol_factory,
1305 plugin_factories,
1306 Arc::clone(store_factory),
1307 live_replay_store,
1308 process_lifecycle_available,
1309 )),
1310 },
1311 None => QueuedWorkDriverSetup::None,
1312 },
1313 }
1314 }
1315
1316 pub fn advanced(self) -> AdvancedLashCoreBuilder {
1317 AdvancedLashCoreBuilder { builder: self }
1318 }
1319
1320 pub fn process_registry(mut self, process_registry: Arc<dyn ProcessRegistry>) -> Self {
1321 self.process_work_source = ProcessWorkSource::Inline {
1322 registry: process_registry,
1323 hub: None,
1324 };
1325 self
1326 }
1327
1328 pub fn process_event_sink(mut self, sink: Arc<dyn lash_core::ProcessEventSink>) -> Self {
1343 self.process_event_sink = Some(sink);
1344 self
1345 }
1346
1347 pub fn trigger_store(mut self, store: Arc<dyn lash_core::TriggerStore>) -> Self {
1348 self.trigger_store = Some(store);
1349 self
1350 }
1351
1352 pub fn process_work_driver(mut self, driver: ProcessWorkDriver) -> Self {
1359 self.process_work_source = ProcessWorkSource::External(driver);
1360 self
1361 }
1362
1363 pub fn queued_work_driver(mut self, driver: QueuedWorkDriver) -> Self {
1365 self.queued_work_source = QueuedWorkSource::External(driver);
1366 self
1367 }
1368
1369 pub fn disable_queued_work_driver(mut self) -> Self {
1370 self.queued_work_source = QueuedWorkSource::None;
1371 self
1372 }
1373}
1374
1375pub(crate) fn build_plugin_host(
1376 protocol_factory: Option<&Arc<dyn PluginFactory>>,
1377 common_factories: &[Arc<dyn PluginFactory>],
1378 extra_factories: Vec<Arc<dyn PluginFactory>>,
1379) -> Result<PluginHost> {
1380 let mut factories = Vec::with_capacity(
1381 usize::from(protocol_factory.is_some()) + common_factories.len() + extra_factories.len(),
1382 );
1383 if let Some(protocol_factory) = protocol_factory {
1384 factories.push(Arc::clone(protocol_factory));
1385 }
1386 factories.extend(common_factories.iter().cloned());
1387 factories.extend(extra_factories);
1388 Ok(PluginHost::new(factories))
1389}
1390
1391impl PromptLayerSink for LashCoreBuilder {
1392 fn prompt_layer_mut(&mut self) -> &mut PromptLayer {
1393 self.prompt.get_or_insert_with(PromptLayer::new)
1394 }
1395}
1396
1397pub struct AdvancedLashCoreBuilder {
1398 builder: LashCoreBuilder,
1399}
1400
1401impl AdvancedLashCoreBuilder {
1402 pub fn runtime_host_config(mut self, core: lash_core::RuntimeHostConfig) -> Self {
1403 self.builder.runtime_host_config = Some(core);
1404 self
1405 }
1406
1407 pub fn plugin_host(mut self, plugin_host: PluginHost) -> Self {
1408 self.builder.plugin_host = Some(plugin_host);
1409 self
1410 }
1411
1412 pub fn build(self) -> Result<LashCore> {
1413 self.builder.build()
1414 }
1415}