Skip to main content

lash_core/plugin/
runtime_host.rs

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