Skip to main content

lash/
admin.rs

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    /// Run `f` against the locked runtime writer, then publish the resulting
375    /// observation. The body is the canonical `lock → call → publish_from`
376    /// stamp shared by nearly every mutating control method; publish happens
377    /// unconditionally once the closure returns.
378    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    /// Toggle Tool Catalog membership for a tool. `present` adds it as a
1016    /// member; `!present` removes it. Membership is the execution gate.
1017    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    /// Replace the entire tool-state snapshot.
1051    ///
1052    /// This is a generation-checked escape hatch for hosts that intentionally
1053    /// edit the full snapshot. Prefer `ToolAdmin` membership methods for
1054    /// ordinary tool policy changes.
1055    pub async fn apply_state(&self, state: ToolState) -> Result<u64> {
1056        self.control.apply_tool_state(state).await
1057    }
1058
1059    /// Restore a persisted tool-state snapshot, adopting its generation.
1060    ///
1061    /// Use this when re-applying a snapshot read from durable storage (session
1062    /// resume), not an edited delta: it reconstructs the exact persisted surface
1063    /// idempotently rather than requiring the snapshot to match the current
1064    /// generation. A cold resume of a session whose surface reached generation
1065    /// ≥ 2 needs this — [`apply_state`](Self::apply_state) would reject it.
1066    ///
1067    /// Persisted tools whose source is not currently registered (e.g. a
1068    /// detached MCP server) do not fail the restore: they are kept as orphaned
1069    /// non-members, listed in the returned [`ToolRestoreReport`], and rebind
1070    /// automatically when a source re-advertises the same tool.
1071    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    /// Enqueue an unconditional tool-catalog refresh. The command drains
1083    /// asynchronously and recomputes the surface from live sources, so it
1084    /// takes no generation guard — any generation observed at enqueue time
1085    /// could legitimately have advanced by drain time.
1086    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/// Session-scoped read controls for Lashlang trigger registrations.
1118#[derive(Clone)]
1119pub struct SessionTriggerAdmin {
1120    control: SessionAdmin,
1121}
1122
1123impl SessionTriggerAdmin {
1124    /// Return every trigger registration in the session.
1125    ///
1126    /// This is an admin/introspection view. Source owners should prefer
1127    /// [`Self::by_source_type`] so they only inspect registrations for the
1128    /// concrete source type they own.
1129    pub async fn list_all(&self) -> Result<Vec<lash_core::TriggerRegistration>> {
1130        self.control.list_trigger_registrations().await
1131    }
1132
1133    /// Return registrations whose source value has the given host descriptor type.
1134    ///
1135    /// This is the source-owner API: a timer, UI, webhook, or other host-owned
1136    /// source uses it to inspect registrations for keys it may schedule and emit.
1137    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}