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