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::QueuedWorkRunError> {
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(|error| {
273 lash_core::QueuedWorkRunError::terminal(lash_core::PluginError::Session(error))
274 })?;
275 let state = crate::session::load_state_for_residency(
276 self.config.env.residency,
277 &session_id,
278 &policy,
279 store.as_ref(),
280 )
281 .await
282 .map_err(|error| {
283 lash_core::QueuedWorkRunError::terminal(lash_core::PluginError::Session(
284 error.to_string(),
285 ))
286 })?;
287 let plugin_host = build_plugin_host(
288 self.config.protocol_factory.as_ref(),
289 self.config.plugin_factories.as_ref(),
290 Vec::new(),
291 )
292 .map_err(|error| {
293 lash_core::QueuedWorkRunError::terminal(lash_core::PluginError::Session(
294 error.to_string(),
295 ))
296 })?;
297 let mut env = self.config.env.clone();
298 env.core = plugin_host
299 .install_process_engine_contributions(
300 env.core.clone(),
301 self.config.process_lifecycle_available,
302 )
303 .map_err(|error| {
304 lash_core::QueuedWorkRunError::terminal(lash_core::PluginError::Session(
305 error.to_string(),
306 ))
307 })?;
308 env.plugin_host = Some(Arc::new(plugin_host));
309 let effect_host = Arc::clone(&env.core.control.effect_host);
310 let runtime = LashRuntime::from_environment(&env, policy, state, Some(store))
311 .await
312 .map_err(|error| {
313 lash_core::QueuedWorkRunError::terminal(lash_core::PluginError::Session(
314 error.to_string(),
315 ))
316 })?;
317 let handle = RuntimeHandle::with_live_replay_store(
318 runtime,
319 Arc::clone(&self.config.live_replay_store),
320 );
321 let scope = lash_core::ExecutionScope::queue_drain(session_id, reason);
322 let scoped = effect_host.scoped(scope).map_err(|error| {
323 lash_core::QueuedWorkRunError::terminal(lash_core::PluginError::Session(
324 error.to_string(),
325 ))
326 })?;
327 crate::turn::stream_next_queued_prepared_turn(
328 &handle,
329 crate::turn::TurnSinks::default(),
330 scoped,
331 CancellationToken::new(),
332 lash_core::TurnCancelOriginHint::default(),
333 &[],
334 )
335 .await
336 .map_err(|error| {
337 let plugin_error = lash_core::PluginError::Session(error.to_string());
338 if error.is_retryable() {
339 lash_core::QueuedWorkRunError::transient(plugin_error)
340 } else {
341 lash_core::QueuedWorkRunError::terminal(plugin_error)
342 }
343 })?;
344 Ok(())
345 }
346}
347
348#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
349pub struct SessionDeleteReport {
350 pub session_id: String,
351 pub process: Option<lash_core::ProcessSessionDeleteReport>,
352}
353
354impl LashCore {
355 pub fn builder() -> LashCoreBuilder {
356 LashCoreBuilder::default()
357 }
358
359 pub fn standard_builder() -> LashCoreBuilder {
362 LashCore::builder()
363 .protocol_plugin(Arc::new(
364 lash_protocol_standard::StandardProtocolPluginFactory::new(),
365 ))
366 .plugins(default_runtime_stack())
367 }
368
369 #[cfg(feature = "rlm")]
377 pub fn rlm_builder(factory: crate::rlm::RlmProtocolPluginFactory) -> LashCoreBuilder {
378 LashCore::builder()
379 .protocol_plugin(Arc::new(factory))
380 .plugins(default_runtime_stack())
381 }
382
383 pub fn session(&self, session_id: impl Into<String>) -> SessionBuilder {
384 SessionBuilder {
385 core: self.clone(),
386 session_id: session_id.into(),
387 spec: SessionSpec::inherit(),
388 parent_session_id: None,
389 session_execution_owner: None,
390 store: None,
391 provider: None,
392 active_plugins: Vec::new(),
393 plugin_factories: Vec::new(),
394 plugin_options: PluginOptions::default(),
395 }
396 }
397
398 pub async fn resume(&self, parked: ParkedSession) -> Result<LashSession> {
411 let plugin_host = build_plugin_host(
416 self.protocol_factory.as_ref(),
417 self.plugin_factories.as_ref(),
418 Vec::new(),
419 )?;
420 let mut env = self.env.clone();
421 env.core = plugin_host.install_process_engine_contributions(
422 env.core.clone(),
423 self.process_lifecycle_available,
424 )?;
425 env.plugin_host = Some(Arc::new(plugin_host));
426 let effect_host = Arc::clone(&env.core.control.effect_host);
427 let drivers = self.work_driver.drivers().await;
428 env.process_work_driver = drivers.process.clone();
429 env.queued_work_driver = drivers.queued.clone();
430 let runtime = LashRuntime::resume(parked.inner, &env).await?;
431 let handle =
432 RuntimeHandle::with_live_replay_store(runtime, Arc::clone(&self.live_replay_store));
433 Ok(LashSession {
434 runtime: handle,
435 effect_host,
436 parent_session_id: None,
437 active_plugins: Vec::new(),
438 process_phase_probe_slot: self.work_driver.phase_probe_slot(),
439 turn_cancels: crate::turn::TurnCancelRegistry::default(),
440 })
441 }
442
443 pub fn flush_trace_sink(&self) -> Result<()> {
454 if let Some(sink) = self.env.core.tracing.trace_sink.as_ref() {
455 sink.flush()?;
456 }
457 Ok(())
458 }
459
460 pub fn triggers(&self) -> crate::admin::CoreTriggerAdmin {
461 crate::admin::CoreTriggerAdmin { core: self.clone() }
462 }
463
464 pub fn processes(&self) -> crate::process_admin::Processes {
465 crate::process_admin::Processes { core: self.clone() }
466 }
467
468 pub fn completions(&self) -> crate::admin::Completions {
469 crate::admin::Completions { core: self.clone() }
470 }
471
472 pub fn effect_host(&self) -> Arc<dyn EffectHost> {
473 Arc::clone(&self.env.core.control.effect_host)
474 }
475
476 pub fn turn_work_driver(&self) -> lash_core::TurnWorkDriver {
482 lash_core::TurnWorkDriver::new(self.effect_host())
483 }
484
485 pub async fn enqueue_turn_input(
494 &self,
495 session_id: impl Into<String>,
496 input: lash_core::TurnInput,
497 ingress: lash_core::TurnInputIngress,
498 id: Option<String>,
499 ) -> Result<lash_core::TurnInputAcceptanceReceipt> {
500 lash_core::ensure_durable_effect_input(&input).map_err(EmbedError::Runtime)?;
501 let session_id = session_id.into();
502 let Some(store_factory) = self.store_factory.as_ref() else {
503 return Err(EmbedError::MissingSessionStoreFactory);
504 };
505 let mut policy = self.policy.clone();
506 policy.session_id = Some(session_id.clone());
507 let store = store_factory
508 .create_store(&SessionStoreCreateRequest {
509 session_id: session_id.clone(),
510 relation: SessionRelation::default(),
511 policy,
512 })
513 .await
514 .map_err(|message| EmbedError::StoreFactory {
515 session_id: session_id.clone(),
516 message,
517 })?;
518 let is_next_turn = matches!(ingress, lash_core::TurnInputIngress::NextTurn);
519 let mut draft = lash_core::PendingTurnInputDraft::new(session_id, ingress, input);
520 draft.source_key = id.map(|id| format!("host:{id}"));
521 let enqueued = store
522 .enqueue_pending_turn_input(draft)
523 .await
524 .map_err(|err| {
525 EmbedError::Runtime(lash_core::RuntimeError::new(
526 lash_core::RuntimeErrorCode::StoreCommitFailed,
527 err.to_string(),
528 ))
529 })?;
530 if is_next_turn && let Some(driver) = self.work_driver.drivers().await.queued.as_ref() {
531 drop(driver.wake_pending(Some(&enqueued.session_id), "queued_turn_input"));
532 }
533 Ok(lash_core::TurnInputAcceptanceReceipt::from(&enqueued))
534 }
535
536 pub async fn delete_session(
537 &self,
538 session_id: impl AsRef<str>,
539 scoped_effect_controller: ScopedEffectController<'_>,
540 ) -> Result<SessionDeleteReport> {
541 let session_id = session_id.as_ref().to_string();
542 let Some(store_factory) = self.store_factory.as_ref() else {
543 return Err(EmbedError::MissingSessionStoreFactory);
544 };
545 let process = if let Some(process_registry) = self.env.process_registry.as_ref() {
546 let invocation = RuntimeInvocation::effect(
547 RuntimeScope::new(session_id.clone()),
548 format!("process:delete-session:{session_id}"),
549 RuntimeEffectKind::Process,
550 format!("{session_id}:delete-session"),
551 );
552 let outcome = scoped_effect_controller
553 .controller()
554 .execute_effect(
555 RuntimeEffectEnvelope::new(
556 invocation,
557 RuntimeEffectCommand::process(ProcessCommand::DeleteSession {
558 session_id: session_id.clone(),
559 }),
560 ),
561 RuntimeEffectLocalExecutor::processes(
562 Arc::clone(process_registry),
563 self.env.process_work_driver.clone(),
564 ),
565 )
566 .await
567 .map_err(|err| EmbedError::SessionDeleteProcess {
568 session_id: session_id.clone(),
569 message: err.to_string(),
570 })?;
571 match outcome {
572 RuntimeEffectOutcome::Process {
573 result: ProcessEffectOutcome::DeleteSession { report },
574 } => Some(report),
575 other => {
576 return Err(EmbedError::SessionDeleteProcess {
577 session_id,
578 message: format!(
579 "process delete returned the wrong outcome: {}",
580 other.kind().as_str()
581 ),
582 });
583 }
584 }
585 } else {
586 None
587 };
588 if let Some(trigger_store) = self.env.trigger_store.as_ref() {
589 trigger_store
590 .delete_session_subscriptions(&session_id)
591 .await
592 .map_err(|err| EmbedError::SessionDeleteProcess {
593 session_id: session_id.clone(),
594 message: err.to_string(),
595 })?;
596 }
597 self.env
598 .core
599 .control
600 .effect_host
601 .revoke_await_events_for_session(&session_id)
602 .await
603 .map_err(|err| EmbedError::SessionDeleteProcess {
604 session_id: session_id.clone(),
605 message: err.to_string(),
606 })?;
607 store_factory
608 .delete_session(&session_id)
609 .await
610 .map_err(|message| EmbedError::StoreFactory {
611 session_id: session_id.clone(),
612 message,
613 })?;
614 Ok(SessionDeleteReport {
615 session_id,
616 process,
617 })
618 }
619
620 pub fn process_registry(&self) -> Option<Arc<dyn ProcessRegistry>> {
621 self.env.process_registry.as_ref().cloned()
622 }
623
624 pub fn durable_process_worker_config(&self) -> Result<DurableProcessWorkerConfig> {
625 self.durable_process_worker_config_with_plugins(std::iter::empty::<Arc<dyn PluginFactory>>())
626 }
627
628 pub fn durable_process_worker_config_with_plugins(
629 &self,
630 extra_plugin_factories: impl IntoIterator<Item = Arc<dyn PluginFactory>>,
631 ) -> Result<DurableProcessWorkerConfig> {
632 let Some(process_registry) = self.process_registry() else {
633 return Err(EmbedError::MissingProcessRegistry);
634 };
635 let Some(store_factory) = self.store_factory.as_ref() else {
636 return Err(EmbedError::MissingProcessWorkerStoreFactory);
637 };
638 let plugin_host = build_plugin_host(
639 self.protocol_factory.as_ref(),
640 self.plugin_factories.as_ref(),
641 extra_plugin_factories.into_iter().collect(),
642 )?;
643 let runtime_host = plugin_host.install_process_engine_contributions(
644 self.env.core.clone(),
645 self.process_lifecycle_available,
646 )?;
647 let mut config = DurableProcessWorkerConfig::new(
648 Arc::new(plugin_host),
649 runtime_host,
650 Arc::clone(store_factory),
651 process_registry,
652 )
653 .with_session_policy(self.policy.clone())
654 .with_residency(self.env.residency)
655 .with_process_execution_concurrency(self.process_execution_concurrency)?;
656 if let Some(trigger_store) = self.env.trigger_store.as_ref() {
657 config = config.with_trigger_store(Arc::clone(trigger_store));
658 }
659 if let Some(driver) = self.work_driver.configured_process_work_driver() {
660 config = config
661 .with_change_hub(driver.change_hub())
662 .with_process_work_driver(driver);
663 }
664 if let Some(driver) = self.work_driver.configured_queued_work_driver() {
665 config = config.with_queued_work_driver(driver);
666 }
667 Ok(config)
668 }
669}
670
671fn default_runtime_stack() -> PluginStack {
672 lash_plugin_tool_output_budget::tool_output_budget_stack()
673}
674
675#[derive(Default)]
676pub struct LashCoreBuilder {
677 pub(crate) protocol_factory: Option<Arc<dyn PluginFactory>>,
678 session_spec: SessionSpec,
679 provider: Option<ProviderHandle>,
680 pub(crate) store_factory: Option<Arc<dyn SessionStoreFactory>>,
681 child_store_factory: Option<Arc<dyn SessionStoreFactory>>,
682 effect_host: Option<Arc<dyn EffectHost>>,
686 attachment_store: Option<Arc<dyn AttachmentStore>>,
687 process_env_store: Option<Arc<dyn ProcessExecutionEnvStore>>,
688 trigger_store: Option<Arc<dyn lash_core::TriggerStore>>,
689 prompt: Option<PromptLayer>,
691 trace_sink: Option<Arc<dyn lash_trace::TraceSink>>,
692 trace_level: Option<lash_trace::TraceLevel>,
693 trace_context: Option<lash_trace::TraceContext>,
694 termination: Option<TerminationPolicy>,
695 runtime_host_config: Option<RuntimeHostConfig>,
697 tool_providers: Vec<Arc<dyn ToolProvider>>,
698 plugin_stack: PluginStack,
699 plugin_host: Option<PluginHost>,
700 residency: Option<Residency>,
701 lease_timings: Option<lash_core::LeaseTimings>,
702 clock: Option<Arc<dyn lash_core::Clock>>,
703 process_work_source: ProcessWorkSource,
706 process_execution_concurrency: Option<usize>,
708 process_event_sink: Option<Arc<dyn lash_core::ProcessEventSink>>,
711 queued_work_source: QueuedWorkSource,
712 live_replay_store: Option<Arc<dyn LiveReplayStore>>,
713}
714
715impl LashCoreBuilder {
716 pub fn protocol_plugin(mut self, plugin: Arc<dyn PluginFactory>) -> Self {
717 self.protocol_factory = Some(plugin);
718 self
719 }
720
721 pub fn provider(mut self, provider: ProviderHandle) -> Self {
722 self.session_spec = self.session_spec.provider_id(provider.kind());
723 self.provider = Some(provider);
724 self
725 }
726
727 pub fn model(mut self, model: lash_core::ModelSpec) -> Self {
728 self.session_spec = self.session_spec.model(model);
729 self
730 }
731
732 pub fn max_turns(mut self, max_turns: usize) -> Self {
733 self.session_spec = self.session_spec.max_turns(max_turns);
734 self
735 }
736
737 pub fn session_spec(mut self, spec: SessionSpec) -> Self {
738 self.session_spec = spec;
739 self
740 }
741
742 pub fn store_factory(mut self, store_factory: Arc<dyn SessionStoreFactory>) -> Self {
756 self.store_factory = Some(store_factory);
757 self
758 }
759
760 pub fn child_store_factory(mut self, store_factory: Arc<dyn SessionStoreFactory>) -> Self {
770 self.child_store_factory = Some(store_factory);
771 self
772 }
773
774 pub fn attachment_store(mut self, attachment_store: Arc<dyn AttachmentStore>) -> Self {
775 self.attachment_store = Some(attachment_store);
776 self
777 }
778
779 pub fn process_env_store(
780 mut self,
781 process_env_store: Arc<dyn ProcessExecutionEnvStore>,
782 ) -> Self {
783 self.process_env_store = Some(process_env_store);
784 self
785 }
786
787 pub fn process_execution_concurrency(mut self, concurrency: usize) -> Self {
796 self.process_execution_concurrency = Some(concurrency);
797 self
798 }
799
800 pub fn effect_host(mut self, effect_host: Arc<dyn EffectHost>) -> Self {
805 self.effect_host = Some(effect_host);
806 self
807 }
808
809 pub fn tools(mut self, tools: Arc<dyn ToolProvider>) -> Self {
810 self.tool_providers.push(tools);
811 self
812 }
813
814 pub fn plugin(mut self, plugin: Arc<dyn PluginFactory>) -> Self {
815 self.plugin_stack.push(plugin);
816 self
817 }
818
819 pub fn plugins(mut self, stack: PluginStack) -> Self {
820 self.plugin_stack = stack;
821 self
822 }
823
824 pub fn configure_plugins(mut self, configure: impl FnOnce(&mut PluginStack)) -> Self {
825 configure(&mut self.plugin_stack);
826 self
827 }
828
829 pub fn trace_sink(mut self, trace_sink: Arc<dyn lash_trace::TraceSink>) -> Self {
830 self.trace_sink = Some(trace_sink);
831 self
832 }
833
834 pub fn trace_jsonl_path(mut self, path: impl Into<std::path::PathBuf>) -> Self {
835 self.trace_sink = Some(Arc::new(lash_trace::JsonlTraceSink::new(path.into())));
836 self
837 }
838
839 pub fn trace_level(mut self, trace_level: lash_trace::TraceLevel) -> Self {
840 self.trace_level = Some(trace_level);
841 self
842 }
843
844 pub fn trace_context(mut self, trace_context: lash_trace::TraceContext) -> Self {
845 self.trace_context = Some(trace_context);
846 self
847 }
848
849 pub fn termination(mut self, termination: TerminationPolicy) -> Self {
850 self.termination = Some(termination);
851 self
852 }
853
854 pub fn residency(mut self, residency: Residency) -> Self {
855 self.residency = Some(residency);
856 self
857 }
858
859 pub fn lease_timings(mut self, lease_timings: lash_core::LeaseTimings) -> Self {
872 self.lease_timings = Some(lease_timings);
873 self
874 }
875
876 pub fn clock(mut self, clock: Arc<dyn lash_core::Clock>) -> Self {
878 self.clock = Some(clock);
879 self
880 }
881
882 pub fn live_replay_store(mut self, live_replay_store: Arc<dyn LiveReplayStore>) -> Self {
886 self.live_replay_store = Some(live_replay_store);
887 self
888 }
889
890 fn resolve_runtime_host_config(&mut self) -> Result<RuntimeHostConfig> {
893 if let Some(base) = self.runtime_host_config.take() {
894 return Ok(self.apply_core_overrides(base));
895 }
896 let effect_host = self
897 .effect_host
898 .take()
899 .ok_or(EmbedError::MissingEffectHost)?;
900 let attachment_store = self
901 .attachment_store
902 .take()
903 .ok_or(EmbedError::MissingAttachmentStore)?;
904 let process_env_store = self
905 .process_env_store
906 .take()
907 .ok_or(EmbedError::MissingProcessEnvStore)?;
908 let core = RuntimeHostConfig::new(effect_host, attachment_store, process_env_store);
909 Ok(self.apply_core_overrides(core))
910 }
911
912 fn apply_core_overrides(&mut self, mut core: RuntimeHostConfig) -> RuntimeHostConfig {
914 if let Some(effect_host) = self.effect_host.take() {
915 core.control.effect_host = effect_host;
916 }
917 if let Some(attachment_store) = self.attachment_store.take() {
918 core.durability.attachment_store = Arc::new(
919 lash_core::SessionAttachmentStore::ephemeral(attachment_store),
920 );
921 }
922 if let Some(process_env_store) = self.process_env_store.take() {
923 core.durability.process_env_store = process_env_store;
924 }
925 if let Some(prompt) = self.prompt.take() {
926 core.prompt.prompt = prompt;
927 }
928 if let Some(trace_sink) = self.trace_sink.take() {
929 core.tracing.trace_sink = Some(trace_sink);
930 }
931 if let Some(trace_level) = self.trace_level.take() {
932 core.tracing.trace_level = trace_level;
933 }
934 if let Some(trace_context) = self.trace_context.take() {
935 core.tracing.trace_context = trace_context;
936 }
937 if let Some(termination) = self.termination.take() {
938 core.control.termination = termination;
939 }
940 if let Some(lease_timings) = self.lease_timings.take() {
941 core.control.lease_timings = lease_timings;
942 }
943 if let Some(clock) = self.clock.take() {
944 core.clock = clock;
945 }
946 core
947 }
948
949 fn effective_session_store_tier(&self) -> Option<DurabilityTier> {
962 self.child_store_factory
963 .as_ref()
964 .or(self.store_factory.as_ref())
965 .map(|factory| factory.durability_tier())
966 }
967
968 fn ensure_store_peer_coherence(
977 session_store_tier: Option<DurabilityTier>,
978 trigger_store_tier: Option<DurabilityTier>,
979 process_registry_tier: Option<DurabilityTier>,
980 core: &RuntimeHostConfig,
981 ) -> Result<()> {
982 let attachment_tier = Some(
983 core.durability
984 .attachment_store
985 .persistence()
986 .durability_tier(),
987 );
988 let process_env_tier = Some(core.durability.process_env_store.durability_tier());
989 let effect_host_tier = Some(core.control.effect_host.durability_tier());
990
991 if session_store_tier == Some(DurabilityTier::Durable) {
992 if attachment_tier == Some(DurabilityTier::Inline) {
993 return Err(EmbedError::DurableStorePeerRequired {
994 facet: "attachment store",
995 });
996 }
997 if process_env_tier == Some(DurabilityTier::Inline) {
998 return Err(EmbedError::DurableStorePeerRequired {
999 facet: "process execution environment store",
1000 });
1001 }
1002 for engine in core.process_engines.engines() {
1005 if engine.durability_tier() == DurabilityTier::Inline {
1006 return Err(EmbedError::DurableStorePeerRequired {
1007 facet: engine.kind(),
1008 });
1009 }
1010 }
1011 }
1012
1013 if process_registry_tier == Some(DurabilityTier::Durable) {
1014 if session_store_tier != Some(DurabilityTier::Durable) {
1015 return Err(EmbedError::DurableProcessRegistryRequiresStoreFactory);
1016 }
1017 if trigger_store_tier != Some(DurabilityTier::Durable) {
1018 return Err(EmbedError::DurableStorePeerRequired {
1019 facet: "trigger store",
1020 });
1021 }
1022 if process_env_tier != Some(DurabilityTier::Durable) {
1023 return Err(EmbedError::DurableStorePeerRequired {
1024 facet: "process execution environment store",
1025 });
1026 }
1027 }
1028
1029 if trigger_store_tier == Some(DurabilityTier::Durable) {
1030 if session_store_tier != Some(DurabilityTier::Durable) {
1031 return Err(EmbedError::DurableStorePeerRequired {
1032 facet: "session store factory",
1033 });
1034 }
1035 if process_env_tier != Some(DurabilityTier::Durable) {
1036 return Err(EmbedError::DurableStorePeerRequired {
1037 facet: "process execution environment store",
1038 });
1039 }
1040 if process_registry_tier == Some(DurabilityTier::Inline) {
1041 return Err(EmbedError::DurableStorePeerRequired {
1042 facet: "process registry",
1043 });
1044 }
1045 }
1046
1047 if effect_host_tier == Some(DurabilityTier::Durable) {
1048 if attachment_tier != Some(DurabilityTier::Durable) {
1049 return Err(EmbedError::DurableStorePeerRequired {
1050 facet: "attachment store",
1051 });
1052 }
1053 if process_env_tier != Some(DurabilityTier::Durable) {
1054 return Err(EmbedError::DurableStorePeerRequired {
1055 facet: "process execution environment store",
1056 });
1057 }
1058 }
1059
1060 Ok(())
1061 }
1062
1063 pub fn build(mut self) -> Result<LashCore> {
1064 let process_execution_concurrency = self
1065 .process_execution_concurrency
1066 .unwrap_or(lash_core::DEFAULT_PROCESS_EXECUTION_CONCURRENCY);
1067 DurableProcessWorkerConfig::validate_process_execution_concurrency(
1068 process_execution_concurrency,
1069 )?;
1070 let protocol_factory = self.protocol_factory.clone();
1071 if protocol_factory.is_none() && self.plugin_host.is_none() {
1072 return Err(EmbedError::MissingProtocolPlugin);
1073 }
1074 let provider_id = self
1075 .session_spec
1076 .provider_id
1077 .clone()
1078 .or_else(|| {
1079 self.provider
1080 .as_ref()
1081 .map(|provider| provider.kind().to_string())
1082 })
1083 .unwrap_or_default();
1084 let model = self
1085 .session_spec
1086 .model
1087 .clone()
1088 .ok_or(EmbedError::MissingModelSpec)?;
1089
1090 let base_policy = SessionPolicy {
1091 provider_id,
1092 model,
1093 max_turns: self.session_spec.max_turns.flatten(),
1094 ..SessionPolicy::default()
1095 };
1096 let policy = self.session_spec.resolve_against(&base_policy);
1097
1098 let session_store_tier = self.effective_session_store_tier();
1101 let trigger_store_tier = self
1102 .trigger_store
1103 .as_ref()
1104 .map(|store| store.durability_tier());
1105 let process_work_source = self
1106 .process_work_source
1107 .clone()
1108 .watched(self.process_event_sink.clone());
1109 let process_registry_tier = process_work_source
1110 .process_registry()
1111 .map(|registry| registry.durability_tier());
1112
1113 let mut core = self.resolve_runtime_host_config()?;
1114 if let Some(provider) = self.provider.clone() {
1115 core.providers.provider_resolver =
1116 Arc::new(lash_core::SingleProviderResolver::new(provider));
1117 }
1118 let plugin_factories = if let Some(plugin_host) = self.plugin_host {
1119 plugin_host.factories().to_vec()
1120 } else {
1121 let mut factories = Vec::new();
1122 if !self.tool_providers.is_empty() {
1123 let spec = self
1124 .tool_providers
1125 .into_iter()
1126 .fold(PluginSpec::new(), PluginSpec::with_tool_provider);
1127 factories.push(Arc::new(StaticPluginFactory::new("embed_tools", spec))
1128 as Arc<dyn PluginFactory>);
1129 }
1130 factories.extend(self.plugin_stack.into_factories());
1131 factories
1132 };
1133 let default_plugin_host =
1134 build_plugin_host(protocol_factory.as_ref(), &plugin_factories, Vec::new())?;
1135 let process_lifecycle_available = process_work_source.has_registry();
1139 let core_with_engines = default_plugin_host
1147 .install_process_engine_contributions(core.clone(), process_lifecycle_available)?;
1148 Self::ensure_store_peer_coherence(
1151 session_store_tier,
1152 trigger_store_tier,
1153 process_registry_tier,
1154 &core_with_engines,
1155 )?;
1156
1157 let process_registry = process_work_source.process_registry();
1158
1159 let process_work_driver = Self::resolve_process_work_driver(
1165 &process_work_source,
1166 &default_plugin_host,
1167 &core,
1168 process_lifecycle_available,
1169 self.child_store_factory
1172 .as_ref()
1173 .or(self.store_factory.as_ref()),
1174 &policy,
1175 self.residency.unwrap_or_default(),
1176 self.trigger_store.as_ref(),
1177 process_execution_concurrency,
1178 )?;
1179
1180 let live_replay_clock = Arc::clone(&core.clock);
1181 let mut env_builder = RuntimeEnvironment::builder()
1182 .with_plugin_host(Arc::new(default_plugin_host))
1183 .with_runtime_host_config(core);
1184 if let Some(process_registry) = process_registry.as_ref() {
1185 env_builder = env_builder.with_process_registry(Arc::clone(process_registry));
1186 }
1187 if let Some(residency) = self.residency {
1188 env_builder = env_builder.with_residency(residency);
1189 }
1190 if let Some(child_store_factory) = self
1191 .child_store_factory
1192 .as_ref()
1193 .or(self.store_factory.as_ref())
1194 {
1195 env_builder = env_builder.with_session_store_factory(Arc::clone(child_store_factory));
1196 }
1197 if let Some(trigger_store) = self.trigger_store.as_ref() {
1198 env_builder = env_builder.with_trigger_store(Arc::clone(trigger_store));
1199 }
1200 let live_replay_store = self.live_replay_store.take().unwrap_or_else(|| {
1201 Arc::new(InMemoryLiveReplayStore::with_clock(
1202 lash_core::InMemoryLiveReplayStoreConfig::default(),
1203 live_replay_clock,
1204 ))
1205 });
1206 let env = env_builder.build();
1207 let queued_work_driver = Self::resolve_queued_work_driver(
1208 &self.queued_work_source,
1209 env.clone(),
1210 policy.clone(),
1211 protocol_factory.clone(),
1212 Arc::new(plugin_factories.clone()),
1213 self.child_store_factory
1214 .as_ref()
1215 .or(self.store_factory.as_ref()),
1216 Arc::clone(&live_replay_store),
1217 process_lifecycle_available,
1218 );
1219 let work_driver = InlineWorkDriverSetup {
1220 process: process_work_driver,
1221 queued: queued_work_driver,
1222 };
1223
1224 Ok(LashCore {
1225 env,
1226 policy,
1227 store_factory: self.store_factory,
1228 plugin_factories: Arc::new(plugin_factories),
1229 provider: self.provider,
1230 live_replay_store,
1231 protocol_factory,
1232 process_lifecycle_available,
1233 process_execution_concurrency,
1234 work_driver: Arc::new(InlineWorkDriverSlot::new(work_driver)),
1235 })
1236 }
1237
1238 #[allow(clippy::too_many_arguments)]
1249 fn resolve_process_work_driver(
1250 process_work_source: &ProcessWorkSource,
1251 worker_plugin_host: &PluginHost,
1252 core: &RuntimeHostConfig,
1253 process_lifecycle_available: bool,
1254 store_factory: Option<&Arc<dyn SessionStoreFactory>>,
1255 policy: &SessionPolicy,
1256 residency: lash_core::Residency,
1257 trigger_store: Option<&Arc<dyn lash_core::TriggerStore>>,
1258 process_execution_concurrency: usize,
1259 ) -> Result<ProcessWorkDriverSetup> {
1260 let (process_registry, process_change_hub) = match process_work_source {
1261 ProcessWorkSource::None => return Ok(ProcessWorkDriverSetup::None),
1262 ProcessWorkSource::External(driver) => {
1263 return Ok(ProcessWorkDriverSetup::External {
1264 driver: driver.clone(),
1265 });
1266 }
1267 ProcessWorkSource::Inline { registry, hub } => (Arc::clone(registry), hub.clone()),
1268 };
1269 let Some(store_factory) = store_factory else {
1273 return Err(EmbedError::ProcessRegistryRequiresStoreFactory);
1274 };
1275 let runtime_host = worker_plugin_host
1280 .install_process_engine_contributions(core.clone(), process_lifecycle_available)?;
1281 let phase_probe_slot = lash_core::runtime::RuntimeTurnPhaseProbeSlot::default();
1282 let mut config = DurableProcessWorkerConfig::new(
1283 Arc::new(worker_plugin_host.clone()),
1284 runtime_host,
1285 Arc::clone(store_factory),
1286 process_registry,
1287 )
1288 .with_session_policy(policy.clone())
1289 .with_trigger_store(trigger_store.cloned().unwrap_or_else(|| {
1290 Arc::new(lash_core::InMemoryTriggerStore::with_clock(Arc::clone(
1291 &core.clock,
1292 )))
1293 }))
1294 .with_residency(residency)
1295 .with_turn_phase_probe_slot(phase_probe_slot)
1296 .with_process_execution_concurrency(process_execution_concurrency)?;
1297 if let Some(hub) = process_change_hub {
1298 config = config.with_change_hub(hub);
1299 }
1300 let config = Box::new(config);
1301 Ok(ProcessWorkDriverSetup::LazyDefault { config })
1302 }
1303
1304 #[allow(clippy::too_many_arguments)]
1305 fn resolve_queued_work_driver(
1306 queued_work_source: &QueuedWorkSource,
1307 env: RuntimeEnvironment,
1308 policy: SessionPolicy,
1309 protocol_factory: Option<Arc<dyn PluginFactory>>,
1310 plugin_factories: Arc<Vec<Arc<dyn PluginFactory>>>,
1311 store_factory: Option<&Arc<dyn SessionStoreFactory>>,
1312 live_replay_store: Arc<dyn LiveReplayStore>,
1313 process_lifecycle_available: bool,
1314 ) -> QueuedWorkDriverSetup {
1315 match queued_work_source {
1316 QueuedWorkSource::None => QueuedWorkDriverSetup::None,
1317 QueuedWorkSource::External(driver) => QueuedWorkDriverSetup::External {
1318 driver: driver.clone(),
1319 },
1320 QueuedWorkSource::LazyDefault => match store_factory {
1321 Some(store_factory) => QueuedWorkDriverSetup::LazyDefault {
1322 config: Arc::new(InlineQueuedWorkRunConfig::new(
1323 env,
1324 policy,
1325 protocol_factory,
1326 plugin_factories,
1327 Arc::clone(store_factory),
1328 live_replay_store,
1329 process_lifecycle_available,
1330 )),
1331 },
1332 None => QueuedWorkDriverSetup::None,
1333 },
1334 }
1335 }
1336
1337 pub fn advanced(self) -> AdvancedLashCoreBuilder {
1338 AdvancedLashCoreBuilder { builder: self }
1339 }
1340
1341 pub fn process_registry(mut self, process_registry: Arc<dyn ProcessRegistry>) -> Self {
1342 self.process_work_source = ProcessWorkSource::Inline {
1343 registry: process_registry,
1344 hub: None,
1345 };
1346 self
1347 }
1348
1349 pub fn process_event_sink(mut self, sink: Arc<dyn lash_core::ProcessEventSink>) -> Self {
1364 self.process_event_sink = Some(sink);
1365 self
1366 }
1367
1368 pub fn trigger_store(mut self, store: Arc<dyn lash_core::TriggerStore>) -> Self {
1369 self.trigger_store = Some(store);
1370 self
1371 }
1372
1373 pub fn process_work_driver(mut self, driver: ProcessWorkDriver) -> Self {
1380 self.process_work_source = ProcessWorkSource::External(driver);
1381 self
1382 }
1383
1384 pub fn queued_work_driver(mut self, driver: QueuedWorkDriver) -> Self {
1386 self.queued_work_source = QueuedWorkSource::External(driver);
1387 self
1388 }
1389
1390 pub fn disable_queued_work_driver(mut self) -> Self {
1391 self.queued_work_source = QueuedWorkSource::None;
1392 self
1393 }
1394}
1395
1396pub(crate) fn build_plugin_host(
1397 protocol_factory: Option<&Arc<dyn PluginFactory>>,
1398 common_factories: &[Arc<dyn PluginFactory>],
1399 extra_factories: Vec<Arc<dyn PluginFactory>>,
1400) -> Result<PluginHost> {
1401 let mut factories = Vec::with_capacity(
1402 usize::from(protocol_factory.is_some()) + common_factories.len() + extra_factories.len(),
1403 );
1404 if let Some(protocol_factory) = protocol_factory {
1405 factories.push(Arc::clone(protocol_factory));
1406 }
1407 factories.extend(common_factories.iter().cloned());
1408 factories.extend(extra_factories);
1409 Ok(PluginHost::new(factories))
1410}
1411
1412impl PromptLayerSink for LashCoreBuilder {
1413 fn prompt_layer_mut(&mut self) -> &mut PromptLayer {
1414 self.prompt.get_or_insert_with(PromptLayer::new)
1415 }
1416}
1417
1418pub struct AdvancedLashCoreBuilder {
1419 builder: LashCoreBuilder,
1420}
1421
1422impl AdvancedLashCoreBuilder {
1423 pub fn runtime_host_config(mut self, core: lash_core::RuntimeHostConfig) -> Self {
1424 self.builder.runtime_host_config = Some(core);
1425 self
1426 }
1427
1428 pub fn plugin_host(mut self, plugin_host: PluginHost) -> Self {
1429 self.builder.plugin_host = Some(plugin_host);
1430 self
1431 }
1432
1433 pub fn build(self) -> Result<LashCore> {
1434 self.builder.build()
1435 }
1436}