Skip to main content

lash_core/runtime/
session_ops.rs

1//! `LashRuntime` session-graph and execution-state operations.
2//!
3//! Extracted from `runtime/mod.rs`. This file re-opens `impl LashRuntime`;
4//! no types live here and no public API is changed.
5
6use std::sync::Arc;
7
8use crate::{PluginOperationInvokeError, SessionError};
9
10use super::LashRuntime;
11use super::state::{
12    RuntimeSessionState, append_session_nodes_to_state_with_clock, normalize_session_graph,
13};
14
15impl LashRuntime {
16    /// Replace the host-owned state envelope.
17    pub fn set_persisted_state(&mut self, state: RuntimeSessionState) -> Result<(), SessionError> {
18        let mut state = state;
19        normalize_session_graph(&mut state);
20        if let Some(session) = self.session.as_ref() {
21            session.invalidate_runtime_caches();
22            // Restore the persisted tool catalog so the live registry matches the
23            // state being installed (mirrors `from_host_state`). Without this the
24            // registry keeps its prior generation/tools and silently diverges from
25            // `state`. `restore_state` accepts the snapshot's generation, so a
26            // surface that reached generation >= 2 restores cleanly; live
27            // changes bump once so the next commit captures them.
28            if let Some(tool_state) = state.tool_state_snapshot.clone() {
29                let report = session
30                    .plugins()
31                    .tool_registry()
32                    .restore_state(tool_state)
33                    .map_err(|err| SessionError::Protocol(err.to_string()))?;
34                if !report.orphaned.is_empty() {
35                    tracing::warn!(
36                        session_id = %state.session_id,
37                        orphaned = ?report.orphaned,
38                        "persisted state installed with orphaned tools: no registered \
39                         source resolves them; they remain non-members until their source returns"
40                    );
41                }
42            }
43            let snapshot = state.plugin_snapshot.clone().unwrap_or_default();
44            session
45                .plugins()
46                .restore(&snapshot)
47                .map_err(|err| SessionError::Protocol(err.to_string()))?;
48            state.plugin_snapshot_revision =
49                Some(session.plugins().snapshot_revision_fingerprint());
50        }
51        self.policy = state.policy.clone();
52        self.protocol_turn_options = state.protocol_turn_options.clone();
53        self.state = state;
54        Ok(())
55    }
56
57    pub async fn append_session_nodes(
58        &mut self,
59        request: crate::AppendSessionNodesRequest,
60    ) -> Result<crate::AppendSessionNodesResult, SessionError> {
61        self.refresh_session_graph_from_store().await?;
62        if let Some(required) = request.requires_ancestor_node_id.as_deref()
63            && !self.state.session_graph.active_path_contains(required)
64        {
65            return Ok(crate::AppendSessionNodesResult::StaleBranch {
66                current_leaf_node_id: self.state.session_graph.leaf_node_id.clone(),
67            });
68        }
69        let node_ids = append_session_nodes_to_state_with_clock(
70            &mut self.state,
71            &request.nodes,
72            self.host.core.clock.as_ref(),
73        );
74        if let Some(session) = self.session.as_mut() {
75            let protocol_session = Arc::clone(session.plugins().protocol_session());
76            let session_id = self.state.session_id.clone();
77            protocol_session
78                .append_session_nodes(
79                    crate::plugin::ProtocolSessionContext::new(session, &session_id),
80                    &request.nodes,
81                )
82                .await?;
83        }
84        self.stamp_live_plugin_state();
85        if let Some(store) = self
86            .session
87            .as_ref()
88            .and_then(|session| session.history_store())
89        {
90            let graph = crate::store::GraphCommitDelta::Append {
91                nodes: node_ids
92                    .iter()
93                    .filter_map(|id| self.state.session_graph.find_node(id).cloned())
94                    .collect(),
95                leaf_node_id: self.state.session_graph.leaf_node_id.clone(),
96            };
97            let commit = crate::store::RuntimeCommit::persisted_state_with_graph_commit(
98                &self.state,
99                graph,
100                &[],
101            );
102            match super::commit_runtime_state_with_fresh_session_execution_lease(
103                store,
104                commit,
105                &self.runtime_lease_owner,
106                self.host.core.control.lease_timings,
107                Arc::clone(&self.host.core.clock),
108            )
109            .await
110            {
111                Ok(result) => self.state.apply_persisted_commit_result(result),
112                Err(err) => tracing::warn!("failed to persist runtime state: {err}"),
113            }
114        }
115        Ok(crate::AppendSessionNodesResult::Appended {
116            node_ids,
117            leaf_node_id: self
118                .state
119                .session_graph
120                .leaf_node_id
121                .clone()
122                .unwrap_or_default(),
123        })
124    }
125
126    pub async fn apply_protocol_session_extension(
127        &mut self,
128        extension: crate::ProtocolSessionExtensionHandle,
129    ) -> Result<(), SessionError> {
130        let Some(session) = self.session.as_ref() else {
131            return Err(SessionError::Protocol(
132                "runtime session is not available".to_string(),
133            ));
134        };
135        let protocol_session = Arc::clone(session.plugins().protocol_session());
136        protocol_session.apply_session_extension(extension).await
137    }
138
139    pub async fn validate_protocol_turn_extension(
140        &mut self,
141        extension: &crate::ProtocolTurnExtensionHandle,
142    ) -> Result<(), SessionError> {
143        let Some(session) = self.session.as_ref() else {
144            return Err(SessionError::Protocol(
145                "runtime session is not available".to_string(),
146            ));
147        };
148        let protocol_session = Arc::clone(session.plugins().protocol_session());
149        protocol_session.validate_turn_extension(extension).await
150    }
151
152    pub async fn branch_to_node(
153        &mut self,
154        node_id: Option<String>,
155    ) -> Result<crate::SessionSnapshot, SessionError> {
156        let mut state = self.export_state();
157        state.session_graph.branch_to(node_id);
158        let mut persisted_state = RuntimeSessionState::from_snapshot(state);
159        normalize_session_graph(&mut persisted_state);
160
161        let policy = persisted_state.policy.clone();
162        let host = self.host.clone();
163        let services = self.services.clone();
164        let managed_sessions = Arc::clone(&self.managed_sessions);
165        let managed_turns = Arc::clone(&self.managed_turns);
166        let process_sync_needed = Arc::clone(&self.process_sync_needed);
167        let runtime_scope_id = Arc::clone(&self.runtime_scope_id);
168        let runtime_lease_owner = self.runtime_lease_owner.clone();
169        let turn_phase_probe = self.turn_phase_probe.clone();
170
171        let mut rebuilt = Self::from_host_state(policy, host, services, persisted_state).await?;
172        rebuilt.managed_sessions = managed_sessions;
173        rebuilt.managed_turns = managed_turns;
174        rebuilt.process_sync_needed = process_sync_needed;
175        rebuilt.runtime_scope_id = runtime_scope_id;
176        rebuilt.runtime_lease_owner = runtime_lease_owner;
177        rebuilt.turn_phase_probe = turn_phase_probe;
178
179        let exported = rebuilt.export_state();
180        *self = rebuilt;
181        Ok(exported)
182    }
183
184    /// Promote a managed child session into the foreground runtime.
185    ///
186    /// Child sessions created through `SessionLifecycleService::create_session` are real
187    /// runtimes, not serialized placeholders. Foreground activation must therefore
188    /// claim that runtime instead of reconstructing a new empty state in the UI.
189    pub async fn activate_managed_session(&mut self, session_id: &str) -> Result<(), SessionError> {
190        let child = {
191            let mut registry = self.managed_sessions.lock().await;
192            registry.remove(session_id).ok_or_else(|| {
193                SessionError::Protocol(format!("unknown managed session `{session_id}`"))
194            })?
195        };
196        let child = child.try_into_runtime().map_err(|_| {
197            SessionError::Protocol(format!("managed session `{session_id}` is still in use"))
198        })?;
199        *self = child;
200        Ok(())
201    }
202
203    /// Explicitly snapshot protocol-local execution state, if any.
204    pub async fn snapshot_execution_state(&mut self) -> Result<Option<Vec<u8>>, SessionError> {
205        let Some(session) = self.session.as_mut() else {
206            return Err(SessionError::Protocol(
207                "runtime session not available".to_string(),
208            ));
209        };
210        let code_executor = session
211            .plugins()
212            .code_executor()
213            .ok_or(SessionError::CodeExecutionUnavailable)?;
214        let session_id = self.state.session_id.clone();
215        let blob = code_executor
216            .snapshot_execution_state(crate::plugin::ProtocolSessionContext::new(
217                session,
218                &session_id,
219            ))
220            .await?;
221        self.state.execution_state_snapshot = blob.clone();
222        Ok(blob)
223    }
224
225    /// Explicitly restore protocol-local execution state from an opaque snapshot blob.
226    pub async fn restore_execution_state(&mut self, snapshot: &[u8]) -> Result<(), SessionError> {
227        let Some(session) = self.session.as_mut() else {
228            return Err(SessionError::Protocol(
229                "runtime session not available".to_string(),
230            ));
231        };
232        let code_executor = session
233            .plugins()
234            .code_executor()
235            .ok_or(SessionError::CodeExecutionUnavailable)?;
236        let session_id = self.state.session_id.clone();
237        code_executor
238            .restore_execution_state(
239                crate::plugin::ProtocolSessionContext::new(session, &session_id),
240                snapshot,
241            )
242            .await?;
243        self.state.execution_state_snapshot = Some(snapshot.to_vec());
244        Ok(())
245    }
246
247    pub async fn list_trigger_registrations(
248        &self,
249    ) -> Result<Vec<crate::TriggerRegistration>, SessionError> {
250        let store = self.host.trigger_store.as_ref().ok_or_else(|| {
251            SessionError::Protocol("trigger store is unavailable in this runtime".to_string())
252        })?;
253        let records = store
254            .list_subscriptions(crate::TriggerSubscriptionFilter::for_session(
255                self.state.session_id.clone(),
256            ))
257            .await
258            .map_err(|err| SessionError::Protocol(err.to_string()))?;
259        Ok(records
260            .iter()
261            .map(crate::TriggerRegistration::from)
262            .collect())
263    }
264
265    pub async fn trigger_registrations_by_source_type(
266        &self,
267        source_type: impl Into<crate::TriggerEventType>,
268    ) -> Result<Vec<crate::TriggerRegistration>, SessionError> {
269        let store = self.host.trigger_store.as_ref().ok_or_else(|| {
270            SessionError::Protocol("trigger store is unavailable in this runtime".to_string())
271        })?;
272        let mut filter =
273            crate::TriggerSubscriptionFilter::for_session(self.state.session_id.clone());
274        filter.source_type = Some(source_type.into().to_string());
275        let records = store
276            .list_subscriptions(filter)
277            .await
278            .map_err(|err| SessionError::Protocol(err.to_string()))?;
279        Ok(records
280            .iter()
281            .map(crate::TriggerRegistration::from)
282            .collect())
283    }
284
285    pub async fn query_plugin(
286        &self,
287        name: &str,
288        args: serde_json::Value,
289        session_id: Option<String>,
290    ) -> Result<(String, serde_json::Value), PluginOperationInvokeError> {
291        let manager = self.runtime_session_services()?;
292        let Some(session) = self.session.as_ref() else {
293            return Err(PluginOperationInvokeError::Unknown(
294                "runtime session not available".to_string(),
295            ));
296        };
297        session
298            .plugins()
299            .query_plugin(
300                name,
301                args,
302                session_id,
303                true,
304                manager.read_service(),
305                manager.process_read_service(),
306            )
307            .await
308    }
309
310    pub async fn run_plugin_command(
311        &mut self,
312        name: &str,
313        args: serde_json::Value,
314        session_id: Option<String>,
315    ) -> Result<crate::PluginCommandReceipt<serde_json::Value>, PluginOperationInvokeError> {
316        let manager = self.runtime_session_services()?;
317        let Some(session) = self.session.as_ref() else {
318            return Err(PluginOperationInvokeError::Unknown(
319                "runtime session not available".to_string(),
320            ));
321        };
322        let (plugin_id, outcome) = session
323            .plugins()
324            .run_plugin_command(
325                name,
326                args,
327                session_id,
328                true,
329                manager.state_service(),
330                manager.lifecycle_service(),
331                manager.graph_service(),
332                manager.process_service(),
333            )
334            .await?;
335        let (events, pending_turn_inputs) = self
336            .apply_plugin_operation_effects(&plugin_id, outcome.events, outcome.directives)
337            .await?;
338        Ok(crate::PluginCommandReceipt {
339            output: outcome.output,
340            events,
341            pending_turn_inputs,
342        })
343    }
344
345    pub async fn run_plugin_task(
346        &mut self,
347        name: &str,
348        args: serde_json::Value,
349        session_id: Option<String>,
350        scoped_effect_controller: crate::ScopedEffectController<'static>,
351        cancellation_token: tokio_util::sync::CancellationToken,
352    ) -> Result<crate::PluginTaskReceipt<serde_json::Value>, PluginOperationInvokeError> {
353        let manager = self.runtime_session_services()?;
354        let Some(session) = self.session.as_ref() else {
355            return Err(PluginOperationInvokeError::Unknown(
356                "runtime session not available".to_string(),
357            ));
358        };
359        let (plugin_id, outcome) = session
360            .plugins()
361            .run_plugin_task(
362                name,
363                args,
364                session_id,
365                true,
366                manager.state_service(),
367                manager.lifecycle_service(),
368                manager.graph_service(),
369                manager.process_service(),
370                scoped_effect_controller,
371                cancellation_token,
372            )
373            .await?;
374        let (events, pending_turn_inputs) = self
375            .apply_plugin_operation_effects(&plugin_id, outcome.events, outcome.directives)
376            .await?;
377        Ok(crate::PluginTaskReceipt {
378            output: outcome.output,
379            events,
380            pending_turn_inputs,
381        })
382    }
383
384    async fn apply_plugin_operation_effects(
385        &mut self,
386        plugin_id: &str,
387        events: Vec<crate::PluginRuntimeEvent>,
388        directives: Vec<crate::PluginRuntimeDirective>,
389    ) -> Result<
390        (
391            Vec<crate::PluginOwned<crate::PluginRuntimeEvent>>,
392            Vec<crate::PendingTurnInput>,
393        ),
394        PluginOperationInvokeError,
395    > {
396        let owned_events = events
397            .into_iter()
398            .map(|event| crate::PluginOwned {
399                plugin_id: plugin_id.to_string(),
400                value: event,
401            })
402            .collect::<Vec<_>>();
403        if !owned_events.is_empty() {
404            let nodes = owned_events
405                .iter()
406                .map(|owned| {
407                    crate::plugin_runtime_protocol_event(&owned.plugin_id, owned.value.clone())
408                        .map(crate::SessionAppendNode::protocol_event)
409                        .map_err(|err| {
410                            PluginOperationInvokeError::Failed(format!(
411                                "failed to encode plugin runtime event: {err}"
412                            ))
413                        })
414                })
415                .collect::<Result<Vec<_>, _>>()?;
416            self.append_plugin_runtime_event_nodes(&nodes).await?;
417        }
418        self.stamp_live_plugin_state();
419        self.persist_plugin_operation_state_if_needed().await?;
420
421        let mut pending_turn_inputs = Vec::new();
422        for directive in directives {
423            match directive {
424                crate::PluginRuntimeDirective::QueueTurn { input, source_key } => {
425                    let pending = self
426                        .enqueue_turn_input(input, crate::TurnInputIngress::NextTurn, source_key)
427                        .await
428                        .map_err(|err| {
429                            PluginOperationInvokeError::Failed(format!(
430                                "failed to queue plugin turn request: {err}"
431                            ))
432                        })?;
433                    pending_turn_inputs.push(pending);
434                }
435            }
436        }
437
438        Ok((owned_events, pending_turn_inputs))
439    }
440
441    async fn append_plugin_runtime_event_nodes(
442        &mut self,
443        nodes: &[crate::SessionAppendNode],
444    ) -> Result<(), PluginOperationInvokeError> {
445        let node_ids = append_session_nodes_to_state_with_clock(
446            &mut self.state,
447            nodes,
448            self.host.core.clock.as_ref(),
449        );
450        if let Some(store) = self
451            .session
452            .as_ref()
453            .and_then(|session| session.history_store())
454        {
455            let graph = crate::store::GraphCommitDelta::Append {
456                nodes: node_ids
457                    .iter()
458                    .filter_map(|id| self.state.session_graph.find_node(id).cloned())
459                    .collect(),
460                leaf_node_id: self.state.session_graph.leaf_node_id.clone(),
461            };
462            let commit = crate::store::RuntimeCommit::persisted_state_with_graph_commit(
463                &self.state,
464                graph,
465                &[],
466            );
467            let result = super::commit_runtime_state_with_fresh_session_execution_lease(
468                store,
469                commit,
470                &self.runtime_lease_owner,
471                self.host.core.control.lease_timings,
472                Arc::clone(&self.host.core.clock),
473            )
474            .await
475            .map_err(|err| {
476                PluginOperationInvokeError::Failed(format!(
477                    "failed to persist plugin runtime events: {err}"
478                ))
479            })?;
480            self.state.apply_persisted_commit_result(result);
481        }
482        Ok(())
483    }
484
485    async fn persist_plugin_operation_state_if_needed(
486        &mut self,
487    ) -> Result<(), PluginOperationInvokeError> {
488        let Some(store) = self
489            .session
490            .as_ref()
491            .and_then(|session| session.history_store())
492        else {
493            return Ok(());
494        };
495        let commit = crate::store::RuntimeCommit::persisted_state(&self.state, &[]);
496        let result = super::commit_runtime_state_with_fresh_session_execution_lease(
497            store,
498            commit,
499            &self.runtime_lease_owner,
500            self.host.core.control.lease_timings,
501            Arc::clone(&self.host.core.clock),
502        )
503        .await
504        .map_err(|err| {
505            PluginOperationInvokeError::Failed(format!(
506                "failed to persist plugin operation state: {err}"
507            ))
508        })?;
509        self.state.apply_persisted_commit_result(result);
510        Ok(())
511    }
512}