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