1pub(crate) use crate::session::SessionConfigPatch;
2use crate::support::*;
3pub use lash_core::{AcceptedInjectedTurnInput, PluginCommand, PluginQuery, PluginTask};
4
5#[derive(Clone)]
6pub struct Completions {
7 pub(crate) core: LashCore,
8}
9
10impl Completions {
11 pub async fn resolve(
12 &self,
13 key: lash_core::AwaitEventKey,
14 resolution: lash_core::Resolution,
15 ) -> Result<lash_core::ResolveOutcome> {
16 self.core
17 .env
18 .core
19 .control
20 .effect_host
21 .resolve_await_event(&key, resolution)
22 .await
23 .map_err(|err| EmbedError::Plugin(lash_core::PluginError::Session(err.to_string())))
24 }
25}
26
27#[derive(Clone)]
28pub struct CoreTriggerAdmin {
29 pub(crate) core: LashCore,
30}
31
32impl CoreTriggerAdmin {
33 pub async fn emit(
34 &self,
35 request: lash_core::TriggerOccurrenceRequest,
36 scoped_effect_controller: ScopedEffectController<'_>,
37 ) -> Result<lash_core::TriggerEmitReport> {
38 let store = self.core.env.trigger_store.as_ref().ok_or_else(|| {
39 EmbedError::Plugin(lash_core::PluginError::Session(
40 "trigger store is unavailable in this runtime".to_string(),
41 ))
42 })?;
43 let drivers = self.core.work_driver.drivers().await;
44 let router = lash_core::TriggerRouter::new(
45 Arc::clone(store),
46 self.core.env.process_registry.clone(),
47 drivers.process,
48 );
49 router
50 .emit(request, scoped_effect_controller.controller())
51 .await
52 .map_err(Into::into)
53 }
54
55 pub async fn subscriptions(
56 &self,
57 filter: lash_core::TriggerSubscriptionFilter,
58 ) -> Result<Vec<lash_core::TriggerRegistration>> {
59 let store = self.core.env.trigger_store.as_ref().ok_or_else(|| {
60 EmbedError::Plugin(lash_core::PluginError::Session(
61 "trigger store is unavailable in this runtime".to_string(),
62 ))
63 })?;
64 let records = store.list_subscriptions(filter).await?;
65 Ok(records
66 .iter()
67 .map(lash_core::TriggerRegistration::from)
68 .collect())
69 }
70}
71
72#[derive(Clone)]
73pub struct Processes {
74 pub(crate) core: LashCore,
75}
76
77impl Processes {
78 fn registry(&self) -> Result<Arc<dyn lash_core::ProcessRegistry>> {
79 self.core
80 .env
81 .process_registry
82 .as_ref()
83 .cloned()
84 .ok_or_else(|| {
85 EmbedError::Plugin(lash_core::PluginError::Session(
86 "process registry is unavailable in this runtime".to_string(),
87 ))
88 })
89 }
90
91 fn make_observer(&self) -> Result<lash_core::ProcessWorkObserver> {
92 Ok(lash_core::ProcessWorkObserver::new(self.registry()?))
93 }
94
95 fn process_invocation(command: &lash_core::ProcessCommand) -> lash_core::RuntimeInvocation {
96 let effect_id = command.effect_id();
97 lash_core::RuntimeInvocation::effect(
98 lash_core::runtime::RuntimeScope::new("runtime"),
99 effect_id.clone(),
100 lash_core::RuntimeEffectKind::Process,
101 effect_id,
102 )
103 }
104
105 async fn run_command(
106 &self,
107 command: lash_core::ProcessCommand,
108 scoped_effect_controller: ScopedEffectController<'_>,
109 ) -> Result<lash_core::ProcessEffectOutcome> {
110 let registry = self.registry()?;
111 let invocation = Self::process_invocation(&command);
112 let outcome = scoped_effect_controller
113 .controller()
114 .execute_effect(
115 lash_core::RuntimeEffectEnvelope::new(
116 invocation,
117 lash_core::RuntimeEffectCommand::process(command),
118 ),
119 lash_core::RuntimeEffectLocalExecutor::processes(
120 registry,
121 self.core.env.process_work_driver.clone(),
122 ),
123 )
124 .await
125 .map_err(|err| EmbedError::Plugin(lash_core::PluginError::Session(err.to_string())))?;
126 match outcome {
127 lash_core::RuntimeEffectOutcome::Process { result } => Ok(result),
128 _ => Err(EmbedError::Plugin(lash_core::PluginError::Session(
129 "process effect returned non-process outcome".to_string(),
130 ))),
131 }
132 }
133
134 pub async fn start(
135 &self,
136 request: lash_core::ProcessStartRequest,
137 scoped_effect_controller: ScopedEffectController<'_>,
138 ) -> Result<lash_core::ProcessRecord> {
139 let env_ref = match request.env_spec.as_ref() {
140 Some(env_spec) => Some(
141 lash_core::runtime::persist_process_execution_env(
142 self.core.env.core.durability.process_env_store.as_ref(),
143 env_spec,
144 )
145 .await?,
146 ),
147 None => None,
148 };
149 let grant = request.grant.clone();
150 let registration = request.into_registration(env_ref);
151 let command = lash_core::ProcessCommand::Start {
152 registration,
153 grant,
154 execution_context: Box::new(lash_core::ProcessExecutionContext::default()),
155 };
156 let outcome = self
157 .run_command(command, scoped_effect_controller.clone())
158 .await?;
159 let lash_core::ProcessEffectOutcome::Start { record } = outcome else {
160 return Err(EmbedError::Plugin(lash_core::PluginError::Session(
161 "process start returned the wrong outcome".to_string(),
162 )));
163 };
164 if let Some(driver) = self.core.work_driver.drivers().await.process {
165 driver.claim_and_run_pending("admin_process_start").await?;
166 }
167 Ok(record)
168 }
169
170 pub async fn list(
171 &self,
172 filter: &lash_core::ProcessListFilter,
173 ) -> Result<Vec<lash_core::ObservedProcess>> {
174 self.make_observer()?.list(filter).await.map_err(Into::into)
175 }
176
177 pub async fn get(&self, process_id: &str) -> Result<Option<lash_core::ObservedProcess>> {
178 Ok(self.make_observer()?.process(process_id).await)
179 }
180
181 pub async fn events(
182 &self,
183 process_id: &str,
184 after_sequence: u64,
185 ) -> Result<Vec<lash_core::ObservedProcessEvent>> {
186 self.make_observer()?
187 .events_after(process_id, after_sequence)
188 .await
189 .map_err(Into::into)
190 }
191
192 pub async fn await_output(&self, process_id: &str) -> Result<lash_core::ProcessAwaitOutput> {
193 if let Some(driver) = self.core.env.process_work_driver.as_ref() {
194 return driver.await_terminal(process_id).await.map_err(Into::into);
195 }
196 lash_core::ProcessAwaiter::polling(self.registry()?)
197 .await_terminal(process_id)
198 .await
199 .map_err(Into::into)
200 }
201
202 pub async fn cancel(
203 &self,
204 process_id: &str,
205 scoped_effect_controller: ScopedEffectController<'_>,
206 ) -> Result<lash_core::ProcessCancelSummary> {
207 let command = lash_core::ProcessCommand::Cancel {
208 process_id: process_id.to_string(),
209 reason: Some("requested by host".to_string()),
210 };
211 let outcome = self
212 .run_command(command, scoped_effect_controller.clone())
213 .await?;
214 let lash_core::ProcessEffectOutcome::Cancel { record } = outcome else {
215 return Err(EmbedError::Plugin(lash_core::PluginError::Session(
216 "process cancel returned the wrong outcome".to_string(),
217 )));
218 };
219 Ok(lash_core::ProcessCancelSummary::from_record(record))
220 }
221
222 pub async fn signal(
223 &self,
224 process_id: &str,
225 signal_name: impl Into<String>,
226 signal_id: impl Into<String>,
227 request: lash_core::ProcessEventAppendRequest,
228 scoped_effect_controller: ScopedEffectController<'_>,
229 ) -> Result<lash_core::ProcessEvent> {
230 let signal_name = signal_name.into();
231 let event_type = request.event_type.clone();
232 let payload = request.payload.clone();
233 let command = lash_core::ProcessCommand::Signal {
234 process_id: process_id.to_string(),
235 signal_name: signal_name.clone(),
236 signal_id: signal_id.into(),
237 request,
238 };
239 let outcome = self
240 .run_command(command, scoped_effect_controller.clone())
241 .await?;
242 let lash_core::ProcessEffectOutcome::Signal { event } = outcome else {
243 return Err(EmbedError::Plugin(lash_core::PluginError::Session(
244 "process signal returned the wrong outcome".to_string(),
245 )));
246 };
247 let registry = self.registry()?;
248 let waiting_ordinal =
249 registry
250 .get_process(process_id)
251 .await
252 .and_then(|record| match record.wait {
253 Some(lash_core::WaitState {
254 kind:
255 lash_core::WaitKind::Signal {
256 name,
257 event_type: wait_event_type,
258 ordinal,
259 ..
260 },
261 ..
262 }) if name == signal_name && wait_event_type == event_type => Some(ordinal),
263 _ => None,
264 });
265 let ordinal = match waiting_ordinal {
266 Some(ordinal) => ordinal,
267 None => {
268 registry
269 .count_events_through(process_id, &event_type, event.sequence)
270 .await?
271 }
272 };
273 if ordinal > 0 {
274 let key = scoped_effect_controller
275 .controller()
276 .await_event_key(
277 &lash_core::ExecutionScope::process(process_id),
278 lash_core::AwaitEventWaitIdentity::process_signal(
279 process_id,
280 &signal_name,
281 ordinal,
282 ),
283 )
284 .await
285 .map_err(|err| {
286 EmbedError::Plugin(lash_core::PluginError::Session(err.to_string()))
287 })?;
288 let _ = scoped_effect_controller
289 .controller()
290 .resolve_await_event(&key, lash_core::Resolution::Ok(payload))
291 .await
292 .map_err(|err| {
293 EmbedError::Plugin(lash_core::PluginError::Session(err.to_string()))
294 })?;
295 }
296 Ok(event)
297 }
298
299 pub async fn session_snapshot(
300 &self,
301 session_id: impl Into<String>,
302 ) -> Result<lash_core::ProcessWorkSnapshot> {
303 self.make_observer()?
304 .snapshot_for_session(session_id)
305 .await
306 .map_err(Into::into)
307 }
308
309 pub fn observer(&self) -> Result<lash_core::ProcessWorkObserver> {
310 self.make_observer()
311 }
312}
313
314#[derive(Clone)]
315pub struct SessionAdmin {
316 pub(crate) runtime: RuntimeHandle,
317}
318
319impl SessionAdmin {
320 pub fn config(&self) -> SessionConfigAdmin {
321 SessionConfigAdmin {
322 control: self.clone(),
323 }
324 }
325
326 pub fn tools(&self) -> ToolAdmin {
327 ToolAdmin {
328 control: self.clone(),
329 }
330 }
331
332 pub fn commands(&self) -> SessionCommandAdmin {
333 SessionCommandAdmin {
334 control: self.clone(),
335 }
336 }
337
338 pub fn triggers(&self) -> SessionTriggerAdmin {
339 SessionTriggerAdmin {
340 control: self.clone(),
341 }
342 }
343
344 pub fn state(&self) -> SessionStateAdmin {
345 SessionStateAdmin {
346 control: self.clone(),
347 }
348 }
349
350 pub fn children(&self) -> ChildSessionAdmin {
351 ChildSessionAdmin {
352 control: self.clone(),
353 }
354 }
355
356 pub fn injection(&self) -> InjectionAdmin {
357 InjectionAdmin {
358 control: self.clone(),
359 }
360 }
361
362 pub fn protocol(&self) -> ProtocolAdmin {
363 ProtocolAdmin {
364 control: self.clone(),
365 }
366 }
367
368 pub fn processes(&self) -> SessionProcessAdmin {
369 SessionProcessAdmin {
370 control: self.clone(),
371 }
372 }
373
374 async fn with_writer<F, T>(&self, f: F) -> T
379 where
380 F: AsyncFnOnce(&mut LashRuntime) -> T,
381 {
382 let writer = self.runtime.writer();
383 let mut runtime = writer.lock().await;
384 let value = f(&mut runtime).await;
385 self.runtime.publish_from(&runtime);
386 value
387 }
388
389 async fn update_config(&self, patch: SessionConfigPatch) -> Result<()> {
390 self.with_writer(async |runtime: &mut LashRuntime| {
391 runtime
392 .update_session_config(patch.provider, patch.model, patch.prompt)
393 .await;
394 })
395 .await;
396 Ok(())
397 }
398
399 async fn export_state(&self) -> lash_core::SessionSnapshot {
400 self.runtime.observe().read_view.to_snapshot()
401 }
402
403 async fn append_messages(&self, messages: Vec<PluginMessage>) -> Result<()> {
404 self.with_writer(async |runtime: &mut LashRuntime| {
405 runtime
406 .append_session_nodes(lash_core::AppendSessionNodesRequest {
407 nodes: messages
408 .into_iter()
409 .map(lash_core::SessionAppendNode::message)
410 .collect(),
411 requires_ancestor_node_id: None,
412 })
413 .await
414 .map(|_| ())
415 .map_err(Into::into)
416 })
417 .await
418 }
419
420 async fn append_plugin_body(
421 &self,
422 plugin_type: impl Into<String>,
423 body: serde_json::Value,
424 ) -> Result<()> {
425 self.with_writer(async |runtime: &mut LashRuntime| {
426 runtime
427 .append_session_nodes(lash_core::AppendSessionNodesRequest {
428 nodes: vec![lash_core::SessionAppendNode::plugin(plugin_type, body)],
429 requires_ancestor_node_id: None,
430 })
431 .await
432 .map(|_| ())
433 .map_err(Into::into)
434 })
435 .await
436 }
437
438 async fn set_persisted_state(&self, state: RuntimeSessionState) -> Result<()> {
439 self.with_writer(async |runtime: &mut LashRuntime| {
440 runtime.set_persisted_state(state).map_err(Into::into)
441 })
442 .await
443 }
444
445 async fn set_prompt_template(&self, template: PromptTemplate) -> Result<()> {
446 self.with_writer(async |runtime: &mut LashRuntime| {
447 runtime.set_prompt_template(template).await;
448 })
449 .await;
450 Ok(())
451 }
452
453 async fn clear_prompt_template(&self) -> Result<()> {
454 self.with_writer(async |runtime: &mut LashRuntime| {
455 runtime.clear_prompt_template().await;
456 })
457 .await;
458 Ok(())
459 }
460
461 async fn add_prompt_contribution(&self, contribution: PromptContribution) -> Result<()> {
462 self.with_writer(async |runtime: &mut LashRuntime| {
463 runtime.add_prompt_contribution(contribution).await;
464 })
465 .await;
466 Ok(())
467 }
468
469 async fn replace_prompt_slot(
470 &self,
471 slot: PromptSlot,
472 contributions: impl IntoIterator<Item = PromptContribution>,
473 ) -> Result<()> {
474 self.with_writer(async |runtime: &mut LashRuntime| {
475 runtime.replace_prompt_slot(slot, contributions).await;
476 })
477 .await;
478 Ok(())
479 }
480
481 async fn clear_prompt_slot(&self, slot: PromptSlot) -> Result<()> {
482 self.with_writer(async |runtime: &mut LashRuntime| {
483 runtime.clear_prompt_slot(slot).await;
484 })
485 .await;
486 Ok(())
487 }
488
489 async fn apply_protocol_session_extension(
490 &self,
491 extension: lash_core::ProtocolSessionExtensionHandle,
492 ) -> Result<()> {
493 self.with_writer(async |runtime: &mut LashRuntime| {
494 runtime
495 .apply_protocol_session_extension(extension)
496 .await
497 .map_err(Into::into)
498 })
499 .await
500 }
501
502 async fn branch_to_node(
503 &self,
504 target_leaf: Option<String>,
505 ) -> Result<lash_core::SessionSnapshot> {
506 self.with_writer(async |runtime: &mut LashRuntime| {
507 runtime
508 .branch_to_node(target_leaf)
509 .await
510 .map_err(Into::into)
511 })
512 .await
513 }
514
515 async fn await_background_work(&self) -> Result<()> {
516 self.with_writer(async |runtime: &mut LashRuntime| {
517 runtime.await_background_work().await.map_err(Into::into)
518 })
519 .await
520 }
521
522 async fn refresh_tool_catalog(&self) -> Result<()> {
523 self.with_writer(async |runtime: &mut LashRuntime| {
524 runtime
525 .refresh_session_tool_catalog()
526 .await
527 .map_err(Into::into)
528 })
529 .await
530 }
531
532 async fn submit_session_command(
533 &self,
534 command: lash_core::SessionCommand,
535 idempotency_key: impl Into<String>,
536 ) -> Result<lash_core::SessionCommandReceipt> {
537 let idempotency_key = idempotency_key.into();
538 self.with_writer(async |runtime: &mut LashRuntime| {
539 runtime
540 .submit_session_command(command, idempotency_key)
541 .await
542 .map_err(Into::into)
543 })
544 .await
545 }
546
547 async fn list_trigger_registrations(&self) -> Result<Vec<lash_core::TriggerRegistration>> {
548 self.with_writer(async |runtime: &mut LashRuntime| {
549 runtime
550 .list_trigger_registrations()
551 .await
552 .map_err(Into::into)
553 })
554 .await
555 }
556
557 async fn trigger_registrations_by_source_type(
558 &self,
559 source_type: impl Into<lash_core::TriggerEventType>,
560 ) -> Result<Vec<lash_core::TriggerRegistration>> {
561 self.with_writer(async |runtime: &mut LashRuntime| {
562 runtime
563 .trigger_registrations_by_source_type(source_type)
564 .await
565 .map_err(Into::into)
566 })
567 .await
568 }
569
570 async fn query_plugin_raw(
571 &self,
572 name: &str,
573 args: serde_json::Value,
574 ) -> Result<(String, serde_json::Value)> {
575 let observation = self.runtime.observe();
576 let session_id = observation.session_id().to_string();
577 observation
578 .query_plugin(name, args, Some(session_id))
579 .await
580 .map_err(Into::into)
581 }
582
583 async fn run_plugin_command_raw(
584 &self,
585 name: &str,
586 args: serde_json::Value,
587 ) -> Result<lash_core::PluginCommandReceipt<serde_json::Value>> {
588 let session_id = self.runtime.observe().session_id().to_string();
589 let writer = self.runtime.writer();
590 let mut runtime = writer.lock().await;
591 let receipt = runtime
592 .run_plugin_command(name, args, Some(session_id))
593 .await?;
594 self.record_plugin_operation_observations(&receipt.events, &receipt.pending_turn_inputs);
595 self.runtime.publish_from(&runtime);
596 Ok(receipt)
597 }
598
599 async fn run_plugin_task_raw_with_cancel(
600 &self,
601 name: &str,
602 args: serde_json::Value,
603 cancellation_token: CancellationToken,
604 ) -> Result<lash_core::PluginTaskReceipt<serde_json::Value>> {
605 let session_id = self.runtime.observe().session_id().to_string();
606 let writer = self.runtime.writer();
607 let mut runtime = writer.lock().await;
608 let scope_id = format!(
609 "{session_id}:plugin_task:{name}:{}",
610 lash_core::TurnActivityId::fresh().0
611 );
612 let scoped_effect_controller = runtime
613 .effect_host()
614 .scoped_static(lash_core::ExecutionScope::runtime_operation(scope_id))
615 .map_err(EmbedError::Runtime)?
616 .ok_or_else(|| {
617 EmbedError::Plugin(lash_core::PluginError::Session(
618 "plugin task execution requires an effect host that can create a static runtime-operation scope".to_string(),
619 ))
620 })?;
621 let receipt = runtime
622 .run_plugin_task(
623 name,
624 args,
625 Some(session_id),
626 scoped_effect_controller,
627 cancellation_token,
628 )
629 .await?;
630 self.record_plugin_operation_observations(&receipt.events, &receipt.pending_turn_inputs);
631 self.runtime.publish_from(&runtime);
632 Ok(receipt)
633 }
634
635 fn record_plugin_operation_observations(
636 &self,
637 events: &[lash_core::PluginOwned<lash_core::PluginRuntimeEvent>],
638 pending_turn_inputs: &[lash_core::PendingTurnInput],
639 ) {
640 for owned in events {
641 self.runtime
642 .record_turn_activity(lash_core::TurnActivity::independent(
643 lash_core::TurnEvent::PluginRuntime {
644 plugin_id: owned.plugin_id.clone(),
645 event: owned.value.clone(),
646 },
647 ));
648 }
649 if !pending_turn_inputs.is_empty() {
650 self.runtime.record_queue_changed(
651 lash_core::SessionQueueEventKind::Enqueued,
652 pending_turn_inputs
653 .iter()
654 .map(|input| input.input_id.clone())
655 .collect(),
656 );
657 }
658 }
659
660 async fn compact_context(
661 &self,
662 instructions: Option<String>,
663 scoped_effect_controller: ScopedEffectController<'_>,
664 ) -> Result<bool> {
665 self.with_writer(async |runtime: &mut LashRuntime| {
666 runtime
667 .compact_context(instructions, scoped_effect_controller)
668 .await
669 .map_err(Into::into)
670 })
671 .await
672 }
673
674 async fn persist_current_state(&self) -> Result<RuntimeSessionState> {
675 self.with_writer(async |runtime: &mut LashRuntime| {
676 runtime.await_background_work().await?;
677 Ok(runtime.export_persisted_state())
678 })
679 .await
680 }
681
682 async fn list_process_handles(&self) -> Result<Vec<lash_core::ProcessHandleSummary>> {
683 Ok(self.runtime.observe().list_process_handles().await)
684 }
685
686 async fn list_all_process_handles(&self) -> Result<Vec<lash_core::ProcessHandleSummary>> {
687 Ok(self.runtime.observe().list_all_process_handles().await)
688 }
689
690 async fn start_process(
691 &self,
692 request: lash_core::ProcessStartRequest,
693 scoped_effect_controller: ScopedEffectController<'_>,
694 ) -> Result<lash_core::ProcessHandleSummary> {
695 let writer = self.runtime.writer();
696 let runtime = writer.lock().await;
697 let session_id = runtime.session_id().to_string();
698 let processes = runtime.process_service()?;
699 let scope = lash_core::ProcessOpScope::new(scoped_effect_controller);
700 let summary = processes
701 .start_from_request(&session_id, request, scope)
702 .await
703 .map_err(EmbedError::Plugin)?;
704 self.runtime.record_process_changed(
705 SessionProcessEventKind::Started,
706 vec![summary.process_id.clone()],
707 );
708 Ok(summary)
709 }
710
711 async fn session_state_service(&self) -> Result<Arc<dyn SessionStateService>> {
712 self.runtime
713 .writer()
714 .lock()
715 .await
716 .session_state_service()
717 .map_err(Into::into)
718 }
719
720 async fn cancel_process(
721 &self,
722 process_id: &str,
723 scoped_effect_controller: ScopedEffectController<'_>,
724 ) -> Result<lash_core::ProcessCancelSummary> {
725 let writer = self.runtime.writer();
726 let runtime = writer.lock().await;
727 let session_id = runtime.session_id().to_string();
728 let processes = runtime.process_service()?;
729 let cancel_ability = runtime.process_cancel_ability();
730 let scope = lash_core::ProcessOpScope::new(scoped_effect_controller);
731 let summary = cancel_ability
732 .cancel_summary(
733 processes.as_ref(),
734 lash_core::ProcessCancelRequest::new(
735 &session_id,
736 process_id,
737 scope,
738 lash_core::ProcessCancelSource::HostApi,
739 )
740 .with_reason("requested by host API"),
741 )
742 .await
743 .map_err(EmbedError::Plugin)?;
744 self.runtime.record_process_changed(
745 SessionProcessEventKind::Cancelled,
746 vec![summary.process_id.clone()],
747 );
748 Ok(summary)
749 }
750
751 async fn cancel_visible_processes(
752 &self,
753 scoped_effect_controller: ScopedEffectController<'_>,
754 ) -> Result<Vec<lash_core::ProcessCancelSummary>> {
755 let writer = self.runtime.writer();
756 let runtime = writer.lock().await;
757 let session_id = runtime.session_id().to_string();
758 let processes = runtime.process_service()?;
759 let cancel_ability = runtime.process_cancel_ability();
760 let scope = lash_core::ProcessOpScope::new(scoped_effect_controller);
761 let summaries = cancel_ability
762 .cancel_all_visible(
763 processes.as_ref(),
764 lash_core::ProcessCancelAllRequest::new(
765 &session_id,
766 scope,
767 lash_core::ProcessCancelSource::HostApi,
768 )
769 .with_reason("requested by host API"),
770 )
771 .await
772 .map_err(EmbedError::Plugin)?;
773 self.runtime.record_process_changed(
774 SessionProcessEventKind::Cancelled,
775 summaries
776 .iter()
777 .map(|summary| summary.process_id.clone())
778 .collect(),
779 );
780 Ok(summaries)
781 }
782
783 async fn snapshot_execution_state(&self) -> Result<Option<Vec<u8>>> {
784 self.with_writer(async |runtime: &mut LashRuntime| {
785 runtime.snapshot_execution_state().await.map_err(Into::into)
786 })
787 .await
788 }
789
790 async fn restore_execution_state(&self, bytes: &[u8]) -> Result<()> {
791 self.with_writer(async |runtime: &mut LashRuntime| {
792 runtime
793 .restore_execution_state(bytes)
794 .await
795 .map_err(Into::into)
796 })
797 .await
798 }
799
800 async fn tool_state(&self) -> Result<ToolState> {
801 self.runtime.observe().tool_state.clone().ok_or_else(|| {
802 EmbedError::Session(SessionError::Protocol(
803 "runtime session not available".to_string(),
804 ))
805 })
806 }
807
808 async fn apply_tool_state(&self, state: ToolState) -> Result<u64> {
809 self.with_writer(async |runtime: &mut LashRuntime| {
810 runtime
811 .apply_tool_state(state)
812 .await
813 .map_err(EmbedError::from)
814 })
815 .await
816 }
817
818 async fn restore_tool_state(&self, state: ToolState) -> Result<ToolRestoreReport> {
819 self.with_writer(async |runtime: &mut LashRuntime| {
820 runtime
821 .restore_tool_state(state)
822 .await
823 .map_err(EmbedError::from)
824 })
825 .await
826 }
827
828 async fn set_tool_membership(&self, tool_id: lash_core::ToolId, present: bool) -> Result<u64> {
829 self.set_tool_membership_many(&[(tool_id, present)]).await
830 }
831
832 async fn set_tool_membership_many(&self, updates: &[(lash_core::ToolId, bool)]) -> Result<u64> {
833 let mut state = self.tool_state().await?;
834 for (tool_id, present) in updates {
835 state
836 .set_membership(tool_id, *present)
837 .map_err(|err| EmbedError::Session(SessionError::Protocol(err.to_string())))?;
838 }
839 self.apply_tool_state(state).await
840 }
841
842 async fn active_tool_manifests(&self) -> Result<Vec<ToolManifest>> {
843 Ok(self.tool_state().await?.tool_manifests())
844 }
845
846 async fn add_tool_provider(&self, provider: Arc<dyn ToolProvider>) -> Result<ToolSourceHandle> {
847 let tool_registry = self.tool_registry().await?;
848 let handle = tool_registry
849 .add_tool_provider(provider)
850 .map_err(|err| EmbedError::Session(SessionError::Protocol(err.to_string())))?;
851 self.refresh_tool_catalog().await?;
852 Ok(handle)
853 }
854
855 async fn remove_tool_source(&self, handle: &ToolSourceHandle) -> Result<u64> {
856 let tool_registry = self.tool_registry().await?;
857 let generation = tool_registry
858 .remove_source(handle)
859 .map_err(|err| EmbedError::Session(SessionError::Protocol(err.to_string())))?;
860 self.refresh_tool_catalog().await?;
861 Ok(generation)
862 }
863
864 async fn create_child_session(&self, request: SessionCreateRequest) -> Result<SessionHandle> {
865 let writer = self.runtime.writer();
866 let runtime = writer.lock().await;
867 let lifecycle = runtime.session_lifecycle_service()?;
868 lifecycle.create_session(request).await.map_err(Into::into)
869 }
870
871 async fn close_child_session(&self, session_id: &str) -> Result<()> {
872 let writer = self.runtime.writer();
873 let runtime = writer.lock().await;
874 let lifecycle = runtime.session_lifecycle_service()?;
875 lifecycle
876 .close_session(session_id)
877 .await
878 .map_err(Into::into)
879 }
880
881 async fn activate_managed_session(&self, session_id: &str) -> Result<()> {
882 self.with_writer(async |runtime: &mut LashRuntime| {
883 runtime
884 .activate_managed_session(session_id)
885 .await
886 .map_err(Into::into)
887 })
888 .await
889 }
890
891 async fn inject_turn_input(
892 &self,
893 turn_id: &str,
894 id: Option<String>,
895 message: PluginMessage,
896 ) -> Result<()> {
897 self.inject_turn_inputs_for_turn(
898 turn_id,
899 vec![lash_core::InjectedTurnInput { id, message }],
900 )
901 .await
902 }
903
904 async fn inject_turn_inputs_for_turn(
905 &self,
906 turn_id: &str,
907 messages: Vec<lash_core::InjectedTurnInput>,
908 ) -> Result<()> {
909 for input in messages {
910 let source_key = input.id.map(|id| format!("injection:{id}"));
911 let turn_input = turn_input_from_plugin_message(input.message);
912 self.runtime
913 .enqueue_turn_input(
914 turn_input,
915 lash_core::TurnInputIngress::active_turn(
916 turn_id,
917 lash_core::TurnInputCheckpointBoundary::AfterWork,
918 ),
919 source_key,
920 )
921 .await
922 .map(|_| ())
923 .map_err(EmbedError::Runtime)?;
924 }
925 Ok(())
926 }
927
928 async fn tool_registry(&self) -> Result<Arc<lash_core::ToolRegistry>> {
929 self.runtime
930 .writer()
931 .lock()
932 .await
933 .plugin_session()
934 .map(|session| session.tool_registry())
935 .ok_or_else(|| {
936 EmbedError::Session(SessionError::Protocol(
937 "tool registry is unavailable in this runtime session".to_string(),
938 ))
939 })
940 }
941}
942
943fn turn_input_from_plugin_message(message: PluginMessage) -> TurnInput {
944 let mut input = TurnInput::empty();
945 if !message.content.is_empty() {
946 input.items.push(InputItem::Text {
947 text: message.content,
948 });
949 }
950 for (index, bytes) in message.images.into_iter().enumerate() {
951 let id = format!("injected-image-{index}");
952 input.items.push(InputItem::ImageRef { id: id.clone() });
953 input.image_blobs.insert(id, bytes);
954 }
955 input
956}
957
958#[derive(Clone)]
959pub struct SessionConfigAdmin {
960 control: SessionAdmin,
961}
962
963impl SessionConfigAdmin {
964 pub async fn update(&self, patch: SessionConfigPatch) -> Result<()> {
965 self.control.update_config(patch).await
966 }
967
968 pub async fn set_prompt_template(&self, template: PromptTemplate) -> Result<()> {
969 self.control.set_prompt_template(template).await
970 }
971
972 pub async fn clear_prompt_template(&self) -> Result<()> {
973 self.control.clear_prompt_template().await
974 }
975
976 pub async fn add_prompt_contribution(&self, contribution: PromptContribution) -> Result<()> {
977 self.control.add_prompt_contribution(contribution).await
978 }
979
980 pub async fn replace_prompt_slot(
981 &self,
982 slot: PromptSlot,
983 contributions: impl IntoIterator<Item = PromptContribution>,
984 ) -> Result<()> {
985 self.control.replace_prompt_slot(slot, contributions).await
986 }
987
988 pub async fn clear_prompt_slot(&self, slot: PromptSlot) -> Result<()> {
989 self.control.clear_prompt_slot(slot).await
990 }
991}
992
993#[derive(Clone)]
994pub struct ToolAdmin {
995 control: SessionAdmin,
996}
997
998impl ToolAdmin {
999 pub(crate) fn new(control: SessionAdmin) -> Self {
1000 Self { control }
1001 }
1002}
1003
1004impl ToolAdmin {
1005 pub async fn state(&self) -> Result<ToolState> {
1006 self.control.tool_state().await
1007 }
1008
1009 pub fn advanced(&self) -> AdvancedToolAdmin {
1010 AdvancedToolAdmin {
1011 control: self.control.clone(),
1012 }
1013 }
1014
1015 pub async fn set_membership(
1018 &self,
1019 tool_id: impl Into<lash_core::ToolId>,
1020 present: bool,
1021 ) -> Result<u64> {
1022 self.control
1023 .set_tool_membership(tool_id.into(), present)
1024 .await
1025 }
1026
1027 pub async fn set_membership_many(&self, updates: &[(lash_core::ToolId, bool)]) -> Result<u64> {
1028 self.control.set_tool_membership_many(updates).await
1029 }
1030
1031 pub async fn active_manifests(&self) -> Result<Vec<ToolManifest>> {
1032 self.control.active_tool_manifests().await
1033 }
1034
1035 pub async fn add_provider(&self, provider: Arc<dyn ToolProvider>) -> Result<ToolSourceHandle> {
1036 self.control.add_tool_provider(provider).await
1037 }
1038
1039 pub async fn remove_source(&self, handle: &ToolSourceHandle) -> Result<u64> {
1040 self.control.remove_tool_source(handle).await
1041 }
1042}
1043
1044#[derive(Clone)]
1045pub struct AdvancedToolAdmin {
1046 control: SessionAdmin,
1047}
1048
1049impl AdvancedToolAdmin {
1050 pub async fn apply_state(&self, state: ToolState) -> Result<u64> {
1056 self.control.apply_tool_state(state).await
1057 }
1058
1059 pub async fn restore_state(&self, state: ToolState) -> Result<ToolRestoreReport> {
1072 self.control.restore_tool_state(state).await
1073 }
1074}
1075
1076#[derive(Clone)]
1077pub struct SessionCommandAdmin {
1078 control: SessionAdmin,
1079}
1080
1081impl SessionCommandAdmin {
1082 pub async fn refresh_tool_catalog(
1087 &self,
1088 reason: impl Into<String>,
1089 idempotency_key: impl Into<String>,
1090 ) -> Result<lash_core::SessionCommandReceipt> {
1091 self.control
1092 .submit_session_command(
1093 lash_core::SessionCommand::RefreshToolCatalog {
1094 reason: reason.into(),
1095 },
1096 idempotency_key,
1097 )
1098 .await
1099 }
1100
1101 pub async fn reset(
1102 &self,
1103 reason: impl Into<String>,
1104 idempotency_key: impl Into<String>,
1105 ) -> Result<lash_core::SessionCommandReceipt> {
1106 self.control
1107 .submit_session_command(
1108 lash_core::SessionCommand::ResetSession {
1109 reason: reason.into(),
1110 },
1111 idempotency_key,
1112 )
1113 .await
1114 }
1115}
1116
1117#[derive(Clone)]
1119pub struct SessionTriggerAdmin {
1120 control: SessionAdmin,
1121}
1122
1123impl SessionTriggerAdmin {
1124 pub async fn list_all(&self) -> Result<Vec<lash_core::TriggerRegistration>> {
1130 self.control.list_trigger_registrations().await
1131 }
1132
1133 pub async fn by_source_type(
1138 &self,
1139 source_type: impl Into<lash_core::TriggerEventType>,
1140 ) -> Result<Vec<lash_core::TriggerRegistration>> {
1141 self.control
1142 .trigger_registrations_by_source_type(source_type)
1143 .await
1144 }
1145}
1146
1147#[derive(Clone)]
1148pub struct SessionProcessAdmin {
1149 control: SessionAdmin,
1150}
1151
1152impl SessionProcessAdmin {
1153 pub(crate) fn new(control: SessionAdmin) -> Self {
1154 Self { control }
1155 }
1156
1157 pub async fn start(
1158 &self,
1159 request: lash_core::ProcessStartRequest,
1160 scoped_effect_controller: ScopedEffectController<'_>,
1161 ) -> Result<lash_core::ProcessHandleSummary> {
1162 self.control
1163 .start_process(request, scoped_effect_controller)
1164 .await
1165 }
1166
1167 pub async fn list(&self) -> Result<Vec<lash_core::ProcessHandleSummary>> {
1168 self.control.list_process_handles().await
1169 }
1170
1171 pub async fn list_all(&self) -> Result<Vec<lash_core::ProcessHandleSummary>> {
1172 self.control.list_all_process_handles().await
1173 }
1174
1175 pub async fn await_all(&self) -> Result<()> {
1176 self.control.await_background_work().await
1177 }
1178
1179 pub async fn cancel(
1180 &self,
1181 process_id: &str,
1182 scoped_effect_controller: ScopedEffectController<'_>,
1183 ) -> Result<lash_core::ProcessCancelSummary> {
1184 self.control
1185 .cancel_process(process_id, scoped_effect_controller)
1186 .await
1187 }
1188
1189 pub async fn cancel_all(
1190 &self,
1191 scoped_effect_controller: ScopedEffectController<'_>,
1192 ) -> Result<Vec<lash_core::ProcessCancelSummary>> {
1193 self.control
1194 .cancel_visible_processes(scoped_effect_controller)
1195 .await
1196 }
1197}
1198
1199#[derive(Clone)]
1200pub struct SessionStateAdmin {
1201 control: SessionAdmin,
1202}
1203
1204impl SessionStateAdmin {
1205 pub async fn export(&self) -> lash_core::SessionSnapshot {
1206 self.control.export_state().await
1207 }
1208
1209 pub async fn append_messages(&self, messages: Vec<PluginMessage>) -> Result<()> {
1210 self.control.append_messages(messages).await
1211 }
1212
1213 pub async fn append_plugin_body(
1214 &self,
1215 plugin_type: impl Into<String>,
1216 body: serde_json::Value,
1217 ) -> Result<()> {
1218 self.control.append_plugin_body(plugin_type, body).await
1219 }
1220
1221 pub async fn set_persisted(&self, state: RuntimeSessionState) -> Result<()> {
1222 self.control.set_persisted_state(state).await
1223 }
1224
1225 pub async fn branch_to_node(
1226 &self,
1227 target_leaf: Option<String>,
1228 ) -> Result<lash_core::SessionSnapshot> {
1229 self.control.branch_to_node(target_leaf).await
1230 }
1231
1232 pub async fn persist_current(&self) -> Result<RuntimeSessionState> {
1233 self.control.persist_current_state().await
1234 }
1235
1236 pub async fn session_state_service(&self) -> Result<Arc<dyn SessionStateService>> {
1237 self.control.session_state_service().await
1238 }
1239
1240 pub async fn snapshot_execution(&self) -> Result<Option<Vec<u8>>> {
1241 self.control.snapshot_execution_state().await
1242 }
1243
1244 pub async fn restore_execution(&self, bytes: &[u8]) -> Result<()> {
1245 self.control.restore_execution_state(bytes).await
1246 }
1247
1248 pub async fn compact_context(
1249 &self,
1250 instructions: Option<String>,
1251 scoped_effect_controller: ScopedEffectController<'_>,
1252 ) -> Result<bool> {
1253 self.control
1254 .compact_context(instructions, scoped_effect_controller)
1255 .await
1256 }
1257}
1258
1259#[derive(Clone)]
1260pub struct PluginOperations {
1261 pub(crate) control: SessionAdmin,
1262}
1263
1264impl PluginOperations {
1265 pub async fn query<Op: lash_core::PluginQuery>(&self, args: Op::Args) -> Result<Op::Output> {
1266 let (_plugin_id, output) = self
1267 .control
1268 .query_plugin_raw(Op::NAME, encode_plugin_args::<Op>(args)?)
1269 .await?;
1270 decode_plugin_output::<Op>(output)
1271 }
1272
1273 pub async fn query_raw(
1274 &self,
1275 name: &str,
1276 args: serde_json::Value,
1277 ) -> Result<(String, serde_json::Value)> {
1278 self.control.query_plugin_raw(name, args).await
1279 }
1280
1281 pub async fn run_command<Op: lash_core::PluginCommand>(
1282 &self,
1283 args: Op::Args,
1284 ) -> Result<lash_core::PluginCommandReceipt<Op::Output>> {
1285 let receipt = self
1286 .control
1287 .run_plugin_command_raw(Op::NAME, encode_plugin_args::<Op>(args)?)
1288 .await?;
1289 Ok(lash_core::PluginCommandReceipt {
1290 output: decode_plugin_output::<Op>(receipt.output)?,
1291 events: receipt.events,
1292 pending_turn_inputs: receipt.pending_turn_inputs,
1293 })
1294 }
1295
1296 pub async fn run_command_raw(
1297 &self,
1298 name: &str,
1299 args: serde_json::Value,
1300 ) -> Result<lash_core::PluginCommandReceipt<serde_json::Value>> {
1301 self.control.run_plugin_command_raw(name, args).await
1302 }
1303
1304 pub async fn run_task<Op: lash_core::PluginTask>(
1305 &self,
1306 args: Op::Args,
1307 ) -> Result<lash_core::PluginTaskReceipt<Op::Output>> {
1308 self.run_task_with_cancel::<Op>(args, CancellationToken::new())
1309 .await
1310 }
1311
1312 pub async fn run_task_with_cancel<Op: lash_core::PluginTask>(
1313 &self,
1314 args: Op::Args,
1315 cancellation_token: CancellationToken,
1316 ) -> Result<lash_core::PluginTaskReceipt<Op::Output>> {
1317 let receipt = self
1318 .control
1319 .run_plugin_task_raw_with_cancel(
1320 Op::NAME,
1321 encode_plugin_args::<Op>(args)?,
1322 cancellation_token,
1323 )
1324 .await?;
1325 Ok(lash_core::PluginTaskReceipt {
1326 output: decode_plugin_output::<Op>(receipt.output)?,
1327 events: receipt.events,
1328 pending_turn_inputs: receipt.pending_turn_inputs,
1329 })
1330 }
1331
1332 pub async fn run_task_raw(
1333 &self,
1334 name: &str,
1335 args: serde_json::Value,
1336 ) -> Result<lash_core::PluginTaskReceipt<serde_json::Value>> {
1337 self.run_task_raw_with_cancel(name, args, CancellationToken::new())
1338 .await
1339 }
1340
1341 pub async fn run_task_raw_with_cancel(
1342 &self,
1343 name: &str,
1344 args: serde_json::Value,
1345 cancellation_token: CancellationToken,
1346 ) -> Result<lash_core::PluginTaskReceipt<serde_json::Value>> {
1347 self.control
1348 .run_plugin_task_raw_with_cancel(name, args, cancellation_token)
1349 .await
1350 }
1351}
1352
1353fn encode_plugin_args<Op: lash_core::PluginOperation>(args: Op::Args) -> Result<serde_json::Value> {
1354 serde_json::to_value(args).map_err(|err| {
1355 EmbedError::Plugin(lash_core::PluginError::Invoke(format!(
1356 "invalid {} args: {err}",
1357 Op::NAME
1358 )))
1359 })
1360}
1361
1362fn decode_plugin_output<Op: lash_core::PluginOperation>(
1363 output: serde_json::Value,
1364) -> Result<Op::Output> {
1365 serde_json::from_value(output).map_err(|err| {
1366 EmbedError::Plugin(lash_core::PluginError::Invoke(format!(
1367 "invalid {} output: {err}",
1368 Op::NAME
1369 )))
1370 })
1371}
1372
1373#[derive(Clone)]
1374pub struct ChildSessionAdmin {
1375 control: SessionAdmin,
1376}
1377
1378impl ChildSessionAdmin {
1379 pub async fn create_session(&self, request: SessionCreateRequest) -> Result<SessionHandle> {
1380 self.control.create_child_session(request).await
1381 }
1382
1383 pub async fn close_session(&self, session_id: &str) -> Result<()> {
1384 self.control.close_child_session(session_id).await
1385 }
1386
1387 pub async fn activate_managed_session(&self, session_id: &str) -> Result<()> {
1388 self.control.activate_managed_session(session_id).await
1389 }
1390}
1391
1392#[derive(Clone)]
1393pub struct InjectionAdmin {
1394 control: SessionAdmin,
1395}
1396
1397impl InjectionAdmin {
1398 pub async fn inject_turn_input(
1399 &self,
1400 turn_id: &str,
1401 id: Option<String>,
1402 message: PluginMessage,
1403 ) -> Result<()> {
1404 self.control.inject_turn_input(turn_id, id, message).await
1405 }
1406
1407 pub async fn inject_turn_inputs_for_turn(
1408 &self,
1409 turn_id: &str,
1410 messages: Vec<lash_core::InjectedTurnInput>,
1411 ) -> Result<()> {
1412 self.control
1413 .inject_turn_inputs_for_turn(turn_id, messages)
1414 .await
1415 }
1416}
1417
1418#[derive(Clone)]
1419pub struct ProtocolAdmin {
1420 control: SessionAdmin,
1421}
1422
1423impl ProtocolAdmin {
1424 pub async fn apply_session_extension(
1425 &self,
1426 extension: lash_core::ProtocolSessionExtensionHandle,
1427 ) -> Result<()> {
1428 self.control
1429 .apply_protocol_session_extension(extension)
1430 .await
1431 }
1432}