Skip to main content

lash/
control.rs

1pub use crate::session::SessionConfigPatch;
2use crate::support::*;
3pub use lash_core::{AcceptedInjectedTurnInput, PluginAction};
4
5#[derive(Clone)]
6pub struct HostEventsControl {
7    pub(crate) core: LashCore,
8}
9
10impl HostEventsControl {
11    pub async fn emit(
12        &self,
13        request: lash_core::HostEventOccurrenceRequest,
14        scoped_effect_controller: ScopedEffectController<'_>,
15    ) -> Result<lash_core::HostEventEmitReport> {
16        let store = self.core.env.host_event_store.as_ref().ok_or_else(|| {
17            EmbedError::Plugin(lash_core::PluginError::Session(
18                "host event store is unavailable in this runtime".to_string(),
19            ))
20        })?;
21        let process_work_poke = self.core.process_work_runner.poke().await;
22        let router = lash_core::HostEventRouter::new(
23            Arc::clone(store),
24            self.core.env.process_registry.clone(),
25            process_work_poke,
26            self.core.env.core.profile.host_profile_id.clone(),
27        );
28        router
29            .emit(request, scoped_effect_controller.controller())
30            .await
31            .map_err(Into::into)
32    }
33
34    pub async fn subscriptions(
35        &self,
36        filter: lash_core::TriggerSubscriptionFilter,
37    ) -> Result<Vec<lash_core::TriggerRegistration>> {
38        let store = self.core.env.host_event_store.as_ref().ok_or_else(|| {
39            EmbedError::Plugin(lash_core::PluginError::Session(
40                "host event store is unavailable in this runtime".to_string(),
41            ))
42        })?;
43        let records = store.list_subscriptions(filter).await?;
44        Ok(records
45            .iter()
46            .map(lash_core::TriggerRegistration::from)
47            .collect())
48    }
49}
50
51#[derive(Clone)]
52pub struct SessionControl {
53    pub(crate) runtime: RuntimeHandle,
54}
55
56impl SessionControl {
57    pub fn config(&self) -> ConfigControl {
58        ConfigControl {
59            control: self.clone(),
60        }
61    }
62
63    pub fn tools(&self) -> ToolsControl {
64        ToolsControl {
65            control: self.clone(),
66        }
67    }
68
69    pub fn commands(&self) -> SessionCommandsControl {
70        SessionCommandsControl {
71            control: self.clone(),
72        }
73    }
74
75    pub fn triggers(&self) -> TriggersControl {
76        TriggersControl {
77            control: self.clone(),
78        }
79    }
80
81    pub fn state(&self) -> StateControl {
82        StateControl {
83            control: self.clone(),
84        }
85    }
86
87    pub fn children(&self) -> ChildrenControl {
88        ChildrenControl {
89            control: self.clone(),
90        }
91    }
92
93    pub fn injection(&self) -> InjectionControl {
94        InjectionControl {
95            control: self.clone(),
96        }
97    }
98
99    pub fn mode(&self) -> ModeControl {
100        ModeControl {
101            control: self.clone(),
102        }
103    }
104
105    pub fn processes(&self) -> ProcessControl {
106        ProcessControl {
107            control: self.clone(),
108        }
109    }
110
111    /// Run `f` against the locked runtime writer, then publish the resulting
112    /// observation. The body is the canonical `lock → call → publish_from`
113    /// stamp shared by nearly every mutating control method; publish happens
114    /// unconditionally once the closure returns.
115    async fn with_writer<F, T>(&self, f: F) -> T
116    where
117        F: AsyncFnOnce(&mut LashRuntime) -> T,
118    {
119        let writer = self.runtime.writer();
120        let mut runtime = writer.lock().await;
121        let value = f(&mut runtime).await;
122        self.runtime.publish_from(&runtime);
123        value
124    }
125
126    async fn update_config(&self, patch: SessionConfigPatch) -> Result<()> {
127        self.update_session_config(patch.provider, patch.model, patch.prompt)
128            .await?;
129        Ok(())
130    }
131
132    async fn update_session_config(
133        &self,
134        provider: Option<ProviderHandle>,
135        model: Option<lash_core::ModelSpec>,
136        prompt: Option<PromptLayer>,
137    ) -> Result<()> {
138        self.with_writer(async |runtime: &mut LashRuntime| {
139            runtime.update_session_config(provider, model, prompt).await;
140        })
141        .await;
142        Ok(())
143    }
144
145    async fn export_state(&self) -> lash_core::SessionSnapshot {
146        self.runtime.observe().read_view.to_snapshot()
147    }
148
149    async fn append_messages(&self, messages: Vec<PluginMessage>) -> Result<()> {
150        self.with_writer(async |runtime: &mut LashRuntime| {
151            runtime
152                .append_session_nodes(lash_core::AppendSessionNodesRequest {
153                    nodes: messages
154                        .into_iter()
155                        .map(lash_core::SessionAppendNode::message)
156                        .collect(),
157                    requires_ancestor_node_id: None,
158                })
159                .await
160                .map(|_| ())
161                .map_err(Into::into)
162        })
163        .await
164    }
165
166    async fn append_plugin_body(
167        &self,
168        plugin_type: impl Into<String>,
169        body: serde_json::Value,
170    ) -> Result<()> {
171        self.with_writer(async |runtime: &mut LashRuntime| {
172            runtime
173                .append_session_nodes(lash_core::AppendSessionNodesRequest {
174                    nodes: vec![lash_core::SessionAppendNode::plugin(plugin_type, body)],
175                    requires_ancestor_node_id: None,
176                })
177                .await
178                .map(|_| ())
179                .map_err(Into::into)
180        })
181        .await
182    }
183
184    async fn set_persisted_state(&self, state: RuntimeSessionState) -> Result<()> {
185        self.with_writer(async |runtime: &mut LashRuntime| {
186            runtime.set_persisted_state(state).map_err(Into::into)
187        })
188        .await
189    }
190
191    async fn set_prompt_template(&self, template: PromptTemplate) -> Result<()> {
192        self.with_writer(async |runtime: &mut LashRuntime| {
193            runtime.set_prompt_template(template).await;
194        })
195        .await;
196        Ok(())
197    }
198
199    async fn clear_prompt_template(&self) -> Result<()> {
200        self.with_writer(async |runtime: &mut LashRuntime| {
201            runtime.clear_prompt_template().await;
202        })
203        .await;
204        Ok(())
205    }
206
207    async fn add_prompt_contribution(&self, contribution: PromptContribution) -> Result<()> {
208        self.with_writer(async |runtime: &mut LashRuntime| {
209            runtime.add_prompt_contribution(contribution).await;
210        })
211        .await;
212        Ok(())
213    }
214
215    async fn replace_prompt_slot(
216        &self,
217        slot: PromptSlot,
218        contributions: impl IntoIterator<Item = PromptContribution>,
219    ) -> Result<()> {
220        self.with_writer(async |runtime: &mut LashRuntime| {
221            runtime.replace_prompt_slot(slot, contributions).await;
222        })
223        .await;
224        Ok(())
225    }
226
227    async fn clear_prompt_slot(&self, slot: PromptSlot) -> Result<()> {
228        self.with_writer(async |runtime: &mut LashRuntime| {
229            runtime.clear_prompt_slot(slot).await;
230        })
231        .await;
232        Ok(())
233    }
234
235    async fn apply_protocol_session_extension(
236        &self,
237        extension: lash_core::ProtocolSessionExtensionHandle,
238    ) -> Result<()> {
239        self.with_writer(async |runtime: &mut LashRuntime| {
240            runtime
241                .apply_protocol_session_extension(extension)
242                .await
243                .map_err(Into::into)
244        })
245        .await
246    }
247
248    async fn branch_to_node(
249        &self,
250        target_leaf: Option<String>,
251    ) -> Result<lash_core::SessionSnapshot> {
252        self.with_writer(async |runtime: &mut LashRuntime| {
253            runtime
254                .branch_to_node(target_leaf)
255                .await
256                .map_err(Into::into)
257        })
258        .await
259    }
260
261    async fn await_background_work(&self) -> Result<()> {
262        self.with_writer(async |runtime: &mut LashRuntime| {
263            runtime.await_background_work().await.map_err(Into::into)
264        })
265        .await
266    }
267
268    async fn refresh_tool_surface(&self) -> Result<()> {
269        self.with_writer(async |runtime: &mut LashRuntime| {
270            runtime
271                .refresh_session_tool_surface()
272                .await
273                .map_err(Into::into)
274        })
275        .await
276    }
277
278    async fn submit_session_command(
279        &self,
280        command: lash_core::SessionCommand,
281        idempotency_key: impl Into<String>,
282    ) -> Result<lash_core::SessionCommandReceipt> {
283        let idempotency_key = idempotency_key.into();
284        self.with_writer(async |runtime: &mut LashRuntime| {
285            runtime
286                .submit_session_command(command, idempotency_key)
287                .await
288                .map_err(Into::into)
289        })
290        .await
291    }
292
293    async fn list_lashlang_trigger_registrations(
294        &self,
295    ) -> Result<Vec<lash_core::TriggerRegistration>> {
296        self.with_writer(async |runtime: &mut LashRuntime| {
297            runtime
298                .list_lashlang_trigger_registrations()
299                .await
300                .map_err(Into::into)
301        })
302        .await
303    }
304
305    async fn lashlang_trigger_registrations_by_source_type(
306        &self,
307        source_type: impl Into<lash_core::TriggerSourceType>,
308    ) -> Result<Vec<lash_core::TriggerRegistration>> {
309        self.with_writer(async |runtime: &mut LashRuntime| {
310            runtime
311                .lashlang_trigger_registrations_by_source_type(source_type)
312                .await
313                .map_err(Into::into)
314        })
315        .await
316    }
317
318    async fn invoke_plugin_action(
319        &self,
320        name: &str,
321        args: serde_json::Value,
322    ) -> Result<ToolResult> {
323        let session_id = self.runtime.observe().session_id().to_string();
324        let writer = self.runtime.writer();
325        writer
326            .lock()
327            .await
328            .invoke_plugin_action(name, args, Some(session_id))
329            .await
330            .map_err(Into::into)
331    }
332
333    async fn call_plugin_action<Op: lash_core::PluginAction>(
334        &self,
335        args: Op::Args,
336    ) -> Result<Op::Output> {
337        let result = self
338            .invoke_plugin_action(
339                Op::NAME,
340                serde_json::to_value(args).map_err(|err| {
341                    EmbedError::Plugin(lash_core::PluginError::Invoke(format!(
342                        "invalid {} args: {err}",
343                        Op::NAME
344                    )))
345                })?,
346            )
347            .await?;
348        if !result.is_success() {
349            return Err(EmbedError::Plugin(lash_core::PluginError::Invoke(format!(
350                "{} failed: {}",
351                Op::NAME,
352                result.value_for_projection()
353            ))));
354        }
355        serde_json::from_value(result.into_output().value_for_projection()).map_err(|err| {
356            EmbedError::Plugin(lash_core::PluginError::Invoke(format!(
357                "invalid {} output: {err}",
358                Op::NAME
359            )))
360        })
361    }
362
363    async fn compact_context(
364        &self,
365        instructions: Option<String>,
366        scoped_effect_controller: ScopedEffectController<'_>,
367    ) -> Result<bool> {
368        self.with_writer(async |runtime: &mut LashRuntime| {
369            runtime
370                .compact_context(instructions, scoped_effect_controller)
371                .await
372                .map_err(Into::into)
373        })
374        .await
375    }
376
377    async fn persist_current_state(&self) -> Result<RuntimeSessionState> {
378        self.with_writer(async |runtime: &mut LashRuntime| {
379            runtime.await_background_work().await?;
380            Ok(runtime.export_persisted_state())
381        })
382        .await
383    }
384
385    async fn list_process_handles(&self) -> Result<Vec<lash_core::ProcessHandleSummary>> {
386        Ok(self.runtime.observe().list_process_handles().await)
387    }
388
389    async fn list_all_process_handles(&self) -> Result<Vec<lash_core::ProcessHandleSummary>> {
390        Ok(self.runtime.observe().list_all_process_handles().await)
391    }
392
393    async fn start_process(
394        &self,
395        request: lash_core::ProcessStartRequest,
396        scoped_effect_controller: ScopedEffectController<'_>,
397    ) -> Result<lash_core::ProcessHandleSummary> {
398        let writer = self.runtime.writer();
399        let runtime = writer.lock().await;
400        let session_id = runtime.session_id().to_string();
401        let processes = runtime.process_service()?;
402        let scope = lash_core::ProcessOpScope::new(scoped_effect_controller);
403        let summary = processes
404            .start_from_request(&session_id, request, scope)
405            .await
406            .map_err(EmbedError::Plugin)?;
407        self.runtime.record_process_changed(
408            SessionProcessEventKind::Started,
409            vec![summary.process_id.clone()],
410        );
411        Ok(summary)
412    }
413
414    async fn session_state_service(&self) -> Result<Arc<dyn SessionStateService>> {
415        self.runtime
416            .writer()
417            .lock()
418            .await
419            .session_state_service()
420            .map_err(Into::into)
421    }
422
423    async fn cancel_process(
424        &self,
425        process_id: &str,
426        scoped_effect_controller: ScopedEffectController<'_>,
427    ) -> Result<lash_core::ProcessCancelSummary> {
428        let writer = self.runtime.writer();
429        let runtime = writer.lock().await;
430        let session_id = runtime.session_id().to_string();
431        let processes = runtime.process_service()?;
432        let cancel_ability = runtime.process_cancel_ability();
433        let scope = lash_core::ProcessOpScope::new(scoped_effect_controller);
434        let summary = cancel_ability
435            .cancel_summary(
436                processes.as_ref(),
437                lash_core::ProcessCancelRequest::new(
438                    &session_id,
439                    process_id,
440                    scope,
441                    lash_core::ProcessCancelSource::HostApi,
442                )
443                .with_reason("requested by host API"),
444            )
445            .await
446            .map_err(EmbedError::Plugin)?;
447        self.runtime.record_process_changed(
448            SessionProcessEventKind::Cancelled,
449            vec![summary.process_id.clone()],
450        );
451        Ok(summary)
452    }
453
454    async fn cancel_visible_processes(
455        &self,
456        scoped_effect_controller: ScopedEffectController<'_>,
457    ) -> Result<Vec<lash_core::ProcessCancelSummary>> {
458        let writer = self.runtime.writer();
459        let runtime = writer.lock().await;
460        let session_id = runtime.session_id().to_string();
461        let processes = runtime.process_service()?;
462        let cancel_ability = runtime.process_cancel_ability();
463        let scope = lash_core::ProcessOpScope::new(scoped_effect_controller);
464        let summaries = cancel_ability
465            .cancel_all_visible(
466                processes.as_ref(),
467                lash_core::ProcessCancelAllRequest::new(
468                    &session_id,
469                    scope,
470                    lash_core::ProcessCancelSource::HostApi,
471                )
472                .with_reason("requested by host API"),
473            )
474            .await
475            .map_err(EmbedError::Plugin)?;
476        self.runtime.record_process_changed(
477            SessionProcessEventKind::Cancelled,
478            summaries
479                .iter()
480                .map(|summary| summary.process_id.clone())
481                .collect(),
482        );
483        Ok(summaries)
484    }
485
486    async fn snapshot_execution_state(&self) -> Result<Option<Vec<u8>>> {
487        self.with_writer(async |runtime: &mut LashRuntime| {
488            runtime.snapshot_execution_state().await.map_err(Into::into)
489        })
490        .await
491    }
492
493    async fn restore_execution_state(&self, bytes: &[u8]) -> Result<()> {
494        self.with_writer(async |runtime: &mut LashRuntime| {
495            runtime
496                .restore_execution_state(bytes)
497                .await
498                .map_err(Into::into)
499        })
500        .await
501    }
502
503    async fn tool_state(&self) -> Result<ToolState> {
504        self.runtime.observe().tool_state.clone().ok_or_else(|| {
505            EmbedError::Session(SessionError::Protocol(
506                "runtime session not available".to_string(),
507            ))
508        })
509    }
510
511    async fn apply_tool_state(&self, state: ToolState) -> Result<u64> {
512        self.with_writer(async |runtime: &mut LashRuntime| {
513            runtime
514                .apply_tool_state(state)
515                .await
516                .map_err(EmbedError::from)
517        })
518        .await
519    }
520
521    async fn restore_tool_state(&self, state: ToolState) -> Result<ToolRestoreReport> {
522        self.with_writer(async |runtime: &mut LashRuntime| {
523            runtime
524                .restore_tool_state(state)
525                .await
526                .map_err(EmbedError::from)
527        })
528        .await
529    }
530
531    async fn set_tool_availability(
532        &self,
533        name: &str,
534        availability: ToolAvailability,
535    ) -> Result<u64> {
536        self.set_tool_availability_many(&[(name, availability)])
537            .await
538    }
539
540    async fn set_tool_availability_many<N: AsRef<str>>(
541        &self,
542        updates: &[(N, ToolAvailability)],
543    ) -> Result<u64> {
544        let mut state = self.tool_state().await?;
545        for (name, availability) in updates {
546            state
547                .set_availability(name.as_ref(), Some(*availability))
548                .map_err(|err| EmbedError::Session(SessionError::Protocol(err.to_string())))?;
549        }
550        self.apply_tool_state(state).await
551    }
552
553    async fn clear_tool_availability_override(&self, name: &str) -> Result<u64> {
554        let mut state = self.tool_state().await?;
555        state
556            .set_availability(name, None)
557            .map_err(|err| EmbedError::Session(SessionError::Protocol(err.to_string())))?;
558        self.apply_tool_state(state).await
559    }
560
561    async fn active_tool_manifests(&self) -> Result<Vec<ToolManifest>> {
562        Ok(self.tool_state().await?.tool_manifests())
563    }
564
565    async fn add_tool_provider(&self, provider: Arc<dyn ToolProvider>) -> Result<ToolSourceHandle> {
566        let tool_registry = self.tool_registry().await?;
567        let handle = tool_registry
568            .add_tool_provider(provider)
569            .map_err(|err| EmbedError::Session(SessionError::Protocol(err.to_string())))?;
570        self.refresh_tool_surface().await?;
571        Ok(handle)
572    }
573
574    async fn remove_tool_source(&self, handle: &ToolSourceHandle) -> Result<u64> {
575        let tool_registry = self.tool_registry().await?;
576        let generation = tool_registry
577            .remove_source(handle)
578            .map_err(|err| EmbedError::Session(SessionError::Protocol(err.to_string())))?;
579        self.refresh_tool_surface().await?;
580        Ok(generation)
581    }
582
583    async fn create_child_session(&self, request: SessionCreateRequest) -> Result<SessionHandle> {
584        let writer = self.runtime.writer();
585        let runtime = writer.lock().await;
586        let lifecycle = runtime.session_lifecycle_service()?;
587        lifecycle.create_session(request).await.map_err(Into::into)
588    }
589
590    async fn close_child_session(&self, session_id: &str) -> Result<()> {
591        let writer = self.runtime.writer();
592        let runtime = writer.lock().await;
593        let lifecycle = runtime.session_lifecycle_service()?;
594        lifecycle
595            .close_session(session_id)
596            .await
597            .map_err(Into::into)
598    }
599
600    async fn activate_managed_session(&self, session_id: &str) -> Result<()> {
601        self.with_writer(async |runtime: &mut LashRuntime| {
602            runtime
603                .activate_managed_session(session_id)
604                .await
605                .map_err(Into::into)
606        })
607        .await
608    }
609
610    async fn inject_turn_input(&self, id: Option<String>, message: PluginMessage) -> Result<()> {
611        self.inject_turn_inputs(vec![lash_core::InjectedTurnInput { id, message }])
612            .await
613    }
614
615    async fn inject_turn_inputs(&self, messages: Vec<lash_core::InjectedTurnInput>) -> Result<()> {
616        for input in messages {
617            let source_key = input.id.map(|id| format!("injection:{id}"));
618            let turn_input = turn_input_from_plugin_message(input.message);
619            self.runtime
620                .enqueue_turn_input(
621                    turn_input,
622                    lash_core::DeliveryPolicy::EarliestSafeBoundary,
623                    lash_core::SlotPolicy::Join,
624                    source_key,
625                )
626                .await
627                .map(|_| ())
628                .map_err(EmbedError::Runtime)?;
629        }
630        Ok(())
631    }
632
633    async fn tool_registry(&self) -> Result<Arc<lash_core::ToolRegistry>> {
634        self.runtime
635            .writer()
636            .lock()
637            .await
638            .plugin_session()
639            .map(|session| session.tool_registry())
640            .ok_or_else(|| {
641                EmbedError::Session(SessionError::Protocol(
642                    "tool registry is unavailable in this runtime session".to_string(),
643                ))
644            })
645    }
646}
647
648fn turn_input_from_plugin_message(message: PluginMessage) -> TurnInput {
649    let mut input = TurnInput::empty();
650    if !message.content.is_empty() {
651        input.items.push(InputItem::Text {
652            text: message.content,
653        });
654    }
655    for (index, bytes) in message.images.into_iter().enumerate() {
656        let id = format!("injected-image-{index}");
657        input.items.push(InputItem::ImageRef { id: id.clone() });
658        input.image_blobs.insert(id, bytes);
659    }
660    input
661}
662
663#[derive(Clone)]
664pub struct ConfigControl {
665    control: SessionControl,
666}
667
668impl ConfigControl {
669    pub async fn update(&self, patch: SessionConfigPatch) -> Result<()> {
670        self.control.update_config(patch).await
671    }
672
673    pub async fn update_session_config(
674        &self,
675        provider: Option<ProviderHandle>,
676        model: Option<lash_core::ModelSpec>,
677        prompt: Option<PromptLayer>,
678    ) -> Result<()> {
679        self.control
680            .update_session_config(provider, model, prompt)
681            .await
682    }
683
684    pub async fn set_prompt_template(&self, template: PromptTemplate) -> Result<()> {
685        self.control.set_prompt_template(template).await
686    }
687
688    pub async fn clear_prompt_template(&self) -> Result<()> {
689        self.control.clear_prompt_template().await
690    }
691
692    pub async fn add_prompt_contribution(&self, contribution: PromptContribution) -> Result<()> {
693        self.control.add_prompt_contribution(contribution).await
694    }
695
696    pub async fn replace_prompt_slot(
697        &self,
698        slot: PromptSlot,
699        contributions: impl IntoIterator<Item = PromptContribution>,
700    ) -> Result<()> {
701        self.control.replace_prompt_slot(slot, contributions).await
702    }
703
704    pub async fn clear_prompt_slot(&self, slot: PromptSlot) -> Result<()> {
705        self.control.clear_prompt_slot(slot).await
706    }
707}
708
709#[derive(Clone)]
710pub struct ToolsControl {
711    control: SessionControl,
712}
713
714impl ToolsControl {
715    pub(crate) fn new(control: SessionControl) -> Self {
716        Self { control }
717    }
718}
719
720impl ToolsControl {
721    pub async fn state(&self) -> Result<ToolState> {
722        self.control.tool_state().await
723    }
724
725    pub fn advanced(&self) -> AdvancedToolsControl {
726        AdvancedToolsControl {
727            control: self.control.clone(),
728        }
729    }
730
731    pub async fn set_availability(
732        &self,
733        name: impl AsRef<str>,
734        availability: ToolAvailability,
735    ) -> Result<u64> {
736        self.control
737            .set_tool_availability(name.as_ref(), availability)
738            .await
739    }
740
741    pub async fn set_availability_many<N: AsRef<str>>(
742        &self,
743        updates: &[(N, ToolAvailability)],
744    ) -> Result<u64> {
745        self.control.set_tool_availability_many(updates).await
746    }
747
748    pub async fn clear_availability_override(&self, name: impl AsRef<str>) -> Result<u64> {
749        self.control
750            .clear_tool_availability_override(name.as_ref())
751            .await
752    }
753
754    pub async fn active_manifests(&self) -> Result<Vec<ToolManifest>> {
755        self.control.active_tool_manifests().await
756    }
757
758    pub async fn add_provider(&self, provider: Arc<dyn ToolProvider>) -> Result<ToolSourceHandle> {
759        self.control.add_tool_provider(provider).await
760    }
761
762    pub async fn remove_source(&self, handle: &ToolSourceHandle) -> Result<u64> {
763        self.control.remove_tool_source(handle).await
764    }
765}
766
767#[derive(Clone)]
768pub struct AdvancedToolsControl {
769    control: SessionControl,
770}
771
772impl AdvancedToolsControl {
773    /// Replace the entire tool-state snapshot.
774    ///
775    /// This is a generation-checked escape hatch for hosts that intentionally
776    /// edit the full snapshot. Prefer `ToolsControl` availability methods for
777    /// ordinary tool policy changes.
778    pub async fn apply_state(&self, state: ToolState) -> Result<u64> {
779        self.control.apply_tool_state(state).await
780    }
781
782    /// Restore a persisted tool-state snapshot, adopting its generation.
783    ///
784    /// Use this when re-applying a snapshot read from durable storage (session
785    /// resume), not an edited delta: it reconstructs the exact persisted surface
786    /// idempotently rather than requiring the snapshot to match the current
787    /// generation. A cold resume of a session whose surface reached generation
788    /// ≥ 2 needs this — [`apply_state`](Self::apply_state) would reject it.
789    ///
790    /// Persisted tools whose source is not currently registered (e.g. a
791    /// detached MCP server) do not fail the restore: they are kept as orphans,
792    /// forced `Off`, listed in the returned [`ToolRestoreReport`], and rebind
793    /// automatically when a source re-advertises the same tool.
794    pub async fn restore_state(&self, state: ToolState) -> Result<ToolRestoreReport> {
795        self.control.restore_tool_state(state).await
796    }
797}
798
799#[derive(Clone)]
800pub struct SessionCommandsControl {
801    control: SessionControl,
802}
803
804impl SessionCommandsControl {
805    /// Enqueue an unconditional tool-surface refresh. The command drains
806    /// asynchronously and recomputes the surface from live sources, so it
807    /// takes no generation guard — any generation observed at enqueue time
808    /// could legitimately have advanced by drain time.
809    pub async fn refresh_tool_surface(
810        &self,
811        reason: impl Into<String>,
812        idempotency_key: impl Into<String>,
813    ) -> Result<lash_core::SessionCommandReceipt> {
814        self.control
815            .submit_session_command(
816                lash_core::SessionCommand::RefreshToolSurface {
817                    reason: reason.into(),
818                },
819                idempotency_key,
820            )
821            .await
822    }
823
824    pub async fn reset(
825        &self,
826        reason: impl Into<String>,
827        idempotency_key: impl Into<String>,
828    ) -> Result<lash_core::SessionCommandReceipt> {
829        self.control
830            .submit_session_command(
831                lash_core::SessionCommand::ResetSession {
832                    reason: reason.into(),
833                },
834                idempotency_key,
835            )
836            .await
837    }
838}
839
840/// Session-scoped read controls for Lashlang trigger registrations.
841#[derive(Clone)]
842pub struct TriggersControl {
843    control: SessionControl,
844}
845
846impl TriggersControl {
847    /// Return every trigger registration in the session.
848    ///
849    /// This is an admin/introspection view. Source owners should prefer
850    /// [`Self::by_source_type`] so they only inspect registrations for the
851    /// concrete source type they own.
852    pub async fn list_all(&self) -> Result<Vec<lash_core::TriggerRegistration>> {
853        self.control.list_lashlang_trigger_registrations().await
854    }
855
856    /// Return registrations whose source value has the given host value type.
857    ///
858    /// This is the source-owner API: a timer, UI, webhook, or other host-owned
859    /// source uses it to inspect registrations for keys it may schedule and emit.
860    pub async fn by_source_type(
861        &self,
862        source_type: impl Into<lash_core::TriggerSourceType>,
863    ) -> Result<Vec<lash_core::TriggerRegistration>> {
864        self.control
865            .lashlang_trigger_registrations_by_source_type(source_type)
866            .await
867    }
868}
869
870#[derive(Clone)]
871pub struct ProcessControl {
872    control: SessionControl,
873}
874
875impl ProcessControl {
876    pub(crate) fn new(control: SessionControl) -> Self {
877        Self { control }
878    }
879
880    pub async fn start(
881        &self,
882        request: lash_core::ProcessStartRequest,
883        scoped_effect_controller: ScopedEffectController<'_>,
884    ) -> Result<lash_core::ProcessHandleSummary> {
885        self.control
886            .start_process(request, scoped_effect_controller)
887            .await
888    }
889
890    pub async fn list(&self) -> Result<Vec<lash_core::ProcessHandleSummary>> {
891        self.control.list_process_handles().await
892    }
893
894    pub async fn list_all(&self) -> Result<Vec<lash_core::ProcessHandleSummary>> {
895        self.control.list_all_process_handles().await
896    }
897
898    pub async fn await_all(&self) -> Result<()> {
899        self.control.await_background_work().await
900    }
901
902    pub async fn cancel(
903        &self,
904        process_id: &str,
905        scoped_effect_controller: ScopedEffectController<'_>,
906    ) -> Result<lash_core::ProcessCancelSummary> {
907        self.control
908            .cancel_process(process_id, scoped_effect_controller)
909            .await
910    }
911
912    pub async fn cancel_all(
913        &self,
914        scoped_effect_controller: ScopedEffectController<'_>,
915    ) -> Result<Vec<lash_core::ProcessCancelSummary>> {
916        self.control
917            .cancel_visible_processes(scoped_effect_controller)
918            .await
919    }
920}
921
922#[derive(Clone)]
923pub struct StateControl {
924    control: SessionControl,
925}
926
927impl StateControl {
928    pub async fn export(&self) -> lash_core::SessionSnapshot {
929        self.control.export_state().await
930    }
931
932    pub async fn append_messages(&self, messages: Vec<PluginMessage>) -> Result<()> {
933        self.control.append_messages(messages).await
934    }
935
936    pub async fn append_plugin_body(
937        &self,
938        plugin_type: impl Into<String>,
939        body: serde_json::Value,
940    ) -> Result<()> {
941        self.control.append_plugin_body(plugin_type, body).await
942    }
943
944    pub async fn set_persisted(&self, state: RuntimeSessionState) -> Result<()> {
945        self.control.set_persisted_state(state).await
946    }
947
948    pub async fn branch_to_node(
949        &self,
950        target_leaf: Option<String>,
951    ) -> Result<lash_core::SessionSnapshot> {
952        self.control.branch_to_node(target_leaf).await
953    }
954
955    pub async fn persist_current(&self) -> Result<RuntimeSessionState> {
956        self.control.persist_current_state().await
957    }
958
959    pub async fn session_state_service(&self) -> Result<Arc<dyn SessionStateService>> {
960        self.control.session_state_service().await
961    }
962
963    pub async fn snapshot_execution(&self) -> Result<Option<Vec<u8>>> {
964        self.control.snapshot_execution_state().await
965    }
966
967    pub async fn restore_execution(&self, bytes: &[u8]) -> Result<()> {
968        self.control.restore_execution_state(bytes).await
969    }
970
971    pub async fn compact_context(
972        &self,
973        instructions: Option<String>,
974        scoped_effect_controller: ScopedEffectController<'_>,
975    ) -> Result<bool> {
976        self.control
977            .compact_context(instructions, scoped_effect_controller)
978            .await
979    }
980}
981
982#[derive(Clone)]
983pub struct PluginActions {
984    pub(crate) control: SessionControl,
985}
986
987impl PluginActions {
988    pub async fn call<Op: lash_core::PluginAction>(&self, args: Op::Args) -> Result<Op::Output> {
989        self.control.call_plugin_action::<Op>(args).await
990    }
991}
992
993#[derive(Clone)]
994pub struct ChildrenControl {
995    control: SessionControl,
996}
997
998impl ChildrenControl {
999    pub async fn create_session(&self, request: SessionCreateRequest) -> Result<SessionHandle> {
1000        self.control.create_child_session(request).await
1001    }
1002
1003    pub async fn close_session(&self, session_id: &str) -> Result<()> {
1004        self.control.close_child_session(session_id).await
1005    }
1006
1007    pub async fn activate_managed_session(&self, session_id: &str) -> Result<()> {
1008        self.control.activate_managed_session(session_id).await
1009    }
1010}
1011
1012#[derive(Clone)]
1013pub struct InjectionControl {
1014    control: SessionControl,
1015}
1016
1017impl InjectionControl {
1018    pub async fn inject_turn_input(
1019        &self,
1020        id: Option<String>,
1021        message: PluginMessage,
1022    ) -> Result<()> {
1023        self.control.inject_turn_input(id, message).await
1024    }
1025
1026    pub async fn inject_turn_inputs(
1027        &self,
1028        messages: Vec<lash_core::InjectedTurnInput>,
1029    ) -> Result<()> {
1030        self.control.inject_turn_inputs(messages).await
1031    }
1032}
1033
1034#[derive(Clone)]
1035pub struct ModeControl {
1036    control: SessionControl,
1037}
1038
1039impl ModeControl {
1040    pub async fn apply_session_extension(
1041        &self,
1042        extension: lash_core::ProtocolSessionExtensionHandle,
1043    ) -> Result<()> {
1044        self.control
1045            .apply_protocol_session_extension(extension)
1046            .await
1047    }
1048}