Skip to main content

lash_core/plugin/
runtime_host.rs

1use serde::{Deserialize, Serialize};
2
3use super::*;
4use crate::SessionAppendNode;
5
6#[async_trait::async_trait]
7pub trait SessionStateService: Send + Sync {
8    async fn snapshot_current(&self) -> Result<SessionSnapshot, PluginError> {
9        Err(PluginError::Session(
10            "session snapshots are unavailable in this runtime".to_string(),
11        ))
12    }
13
14    async fn snapshot_session(&self, _session_id: &str) -> Result<SessionSnapshot, PluginError> {
15        Err(PluginError::Session(
16            "session lookup is unavailable in this runtime".to_string(),
17        ))
18    }
19
20    async fn tool_catalog(&self, _session_id: &str) -> Result<Vec<serde_json::Value>, PluginError> {
21        Err(PluginError::Session(
22            "tool catalogs are unavailable in this runtime".to_string(),
23        ))
24    }
25
26    async fn shared_tool_catalog(
27        &self,
28        session_id: &str,
29    ) -> Result<std::sync::Arc<Vec<serde_json::Value>>, PluginError> {
30        Ok(std::sync::Arc::new(self.tool_catalog(session_id).await?))
31    }
32
33    async fn tool_state(&self, _session_id: &str) -> Result<crate::ToolState, PluginError> {
34        Err(PluginError::Session(
35            "tool state is unavailable in this session".to_string(),
36        ))
37    }
38
39    async fn apply_tool_state(
40        &self,
41        _session_id: &str,
42        _snapshot: crate::ToolState,
43    ) -> Result<u64, PluginError> {
44        Err(PluginError::Session(
45            "tool state mutation is unavailable in this session".to_string(),
46        ))
47    }
48
49    /// Toggle Tool Catalog membership for several tools at once. `present` adds
50    /// the tools as members; `!present` removes them (non-membership) while
51    /// keeping their state for later re-add.
52    async fn set_tool_membership(
53        &self,
54        session_id: &str,
55        tool_names: &[String],
56        present: bool,
57    ) -> Result<u64, PluginError> {
58        let mut snapshot = self.tool_state(session_id).await?;
59        for name in tool_names {
60            let id = snapshot
61                .iter()
62                .find(|(_, entry)| entry.manifest().name == *name)
63                .map(|(id, _)| id.clone())
64                .ok_or_else(|| PluginError::Session(format!("unknown tool `{name}`")))?;
65            snapshot
66                .set_membership(&id, present)
67                .map_err(|err| PluginError::Session(err.to_string()))?;
68        }
69        self.apply_tool_state(session_id, snapshot).await
70    }
71}
72
73#[async_trait::async_trait]
74pub trait SessionLifecycleService: Send + Sync {
75    async fn create_session(
76        &self,
77        _request: SessionCreateRequest,
78    ) -> Result<SessionHandle, PluginError> {
79        Err(PluginError::Session(
80            "session creation is unavailable in this runtime".to_string(),
81        ))
82    }
83
84    async fn close_session(&self, _session_id: &str) -> Result<(), PluginError> {
85        Err(PluginError::Session(
86            "session closing is unavailable in this runtime".to_string(),
87        ))
88    }
89
90    async fn start_turn(
91        &self,
92        _request: SessionTurnRequest<'_>,
93    ) -> Result<AssembledTurn, PluginError> {
94        Err(PluginError::Session(
95            "session execution is unavailable in this runtime".to_string(),
96        ))
97    }
98}
99
100#[async_trait::async_trait]
101pub trait SessionGraphService: Send + Sync {
102    async fn append_session_nodes(
103        &self,
104        _session_id: &str,
105        _request: AppendSessionNodesRequest,
106    ) -> Result<AppendSessionNodesResult, PluginError> {
107        Err(PluginError::Session(
108            "session graph mutation is unavailable in this session".to_string(),
109        ))
110    }
111
112    async fn emit_trace_event(
113        &self,
114        _context: lash_trace::TraceContext,
115        _event: lash_trace::TraceEvent,
116    ) -> Result<(), PluginError> {
117        Ok(())
118    }
119}
120
121/// Result of a single-shot direct LLM call.
122#[derive(Clone, Debug, Serialize, Deserialize)]
123pub struct DirectCompletion {
124    pub text: String,
125    pub usage: crate::TokenUsage,
126    pub llm_call: crate::LlmCallRecord,
127}
128
129#[derive(Clone, Debug, Serialize, Deserialize)]
130pub struct DirectLlmCompletion {
131    pub response: crate::LlmResponse,
132    pub usage: crate::TokenUsage,
133    pub llm_call: crate::LlmCallRecord,
134}
135
136#[derive(Clone, Debug, Serialize, Deserialize)]
137pub struct SessionTurnInput {
138    pub session_id: String,
139    pub turn_id: String,
140    pub input: TurnInput,
141}
142
143pub struct SessionTurnRequest<'run> {
144    turn: SessionTurnInput,
145    scoped_effect_controller: crate::ScopedEffectController<'run>,
146}
147
148impl<'run> SessionTurnRequest<'run> {
149    pub fn new(
150        session_id: impl Into<String>,
151        turn_id: impl Into<String>,
152        mut input: TurnInput,
153        scoped_effect_controller: crate::ScopedEffectController<'run>,
154    ) -> Result<Self, PluginError> {
155        let session_id = session_id.into();
156        let turn_id = turn_id.into();
157        if turn_id.trim().is_empty() {
158            return Err(PluginError::Session(
159                "session turns require a non-empty stable turn id".to_string(),
160            ));
161        }
162        if scoped_effect_controller.turn_id() != Some(turn_id.as_str()) {
163            return Err(PluginError::Session(format!(
164                "session turn `{turn_id}` requires an effect turn scope with the same id"
165            )));
166        }
167        if scoped_effect_controller.execution_scope().session_id() != Some(session_id.as_str()) {
168            return Err(PluginError::Session(format!(
169                "session turn `{turn_id}` requires an execution scope for session `{session_id}`"
170            )));
171        }
172        if let Some(input_turn_id) = input.trace_turn_id.as_deref()
173            && input_turn_id != turn_id
174        {
175            return Err(PluginError::Session(format!(
176                "input trace_turn_id `{input_turn_id}` does not match turn id `{turn_id}`"
177            )));
178        }
179        input.trace_turn_id = Some(turn_id.clone());
180        Ok(Self {
181            turn: SessionTurnInput {
182                session_id,
183                turn_id,
184                input,
185            },
186            scoped_effect_controller,
187        })
188    }
189
190    pub fn session_id(&self) -> &str {
191        &self.turn.session_id
192    }
193
194    pub fn turn_id(&self) -> &str {
195        &self.turn.turn_id
196    }
197
198    pub fn input(&self) -> &TurnInput {
199        &self.turn.input
200    }
201
202    pub fn scoped_effect_controller(&self) -> &crate::ScopedEffectController<'run> {
203        &self.scoped_effect_controller
204    }
205
206    pub fn into_parts(self) -> (SessionTurnInput, crate::ScopedEffectController<'run>) {
207        (self.turn, self.scoped_effect_controller)
208    }
209}
210
211#[derive(Clone, Debug, Serialize, Deserialize)]
212pub struct AppendSessionNodesRequest {
213    pub nodes: Vec<SessionAppendNode>,
214    #[serde(default)]
215    pub requires_ancestor_node_id: Option<String>,
216}
217
218#[derive(Clone, Debug, Serialize, Deserialize)]
219#[serde(tag = "status", rename_all = "snake_case")]
220pub enum AppendSessionNodesResult {
221    Appended {
222        node_ids: Vec<String>,
223        leaf_node_id: String,
224    },
225    StaleBranch {
226        current_leaf_node_id: Option<String>,
227    },
228}