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