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    async fn set_tools_availability(
49        &self,
50        session_id: &str,
51        tool_names: &[String],
52        availability: Option<crate::ToolAvailability>,
53    ) -> Result<u64, PluginError> {
54        let mut snapshot = self.tool_state(session_id).await?;
55        for name in tool_names {
56            snapshot
57                .set_availability(name, availability)
58                .map_err(|err| PluginError::Session(err.to_string()))?;
59        }
60        self.apply_tool_state(session_id, snapshot).await
61    }
62
63    async fn set_tool_availability(
64        &self,
65        session_id: &str,
66        tool_name: &str,
67        availability: Option<ToolAvailability>,
68    ) -> Result<u64, PluginError> {
69        let mut snapshot = self.tool_state(session_id).await?;
70        snapshot
71            .set_availability(tool_name, availability)
72            .map_err(|err| PluginError::Session(err.to_string()))?;
73        self.apply_tool_state(session_id, snapshot).await
74    }
75}
76
77#[async_trait::async_trait]
78pub trait SessionLifecycleService: Send + Sync {
79    async fn create_session(
80        &self,
81        _request: SessionCreateRequest,
82    ) -> Result<SessionHandle, PluginError> {
83        Err(PluginError::Session(
84            "session creation is unavailable in this runtime".to_string(),
85        ))
86    }
87
88    async fn close_session(&self, _session_id: &str) -> Result<(), PluginError> {
89        Err(PluginError::Session(
90            "session closing is unavailable in this runtime".to_string(),
91        ))
92    }
93
94    async fn start_turn(
95        &self,
96        _request: SessionTurnRequest<'_>,
97    ) -> Result<AssembledTurn, PluginError> {
98        Err(PluginError::Session(
99            "session execution is unavailable in this runtime".to_string(),
100        ))
101    }
102}
103
104#[async_trait::async_trait]
105pub trait SessionGraphService: Send + Sync {
106    async fn append_session_nodes(
107        &self,
108        _session_id: &str,
109        _request: AppendSessionNodesRequest,
110    ) -> Result<AppendSessionNodesResult, PluginError> {
111        Err(PluginError::Session(
112            "session graph mutation is unavailable in this session".to_string(),
113        ))
114    }
115
116    async fn emit_trace_event(
117        &self,
118        _context: lash_trace::TraceContext,
119        _event: lash_trace::TraceEvent,
120    ) -> Result<(), PluginError> {
121        Ok(())
122    }
123}
124
125/// Result of a single-shot direct LLM call.
126#[derive(Clone, Debug, Serialize, Deserialize)]
127pub struct DirectCompletion {
128    pub text: String,
129    pub usage: crate::TokenUsage,
130}
131
132#[derive(Clone, Debug, Serialize, Deserialize)]
133pub struct DirectLlmCompletion {
134    pub response: crate::LlmResponse,
135    pub usage: crate::TokenUsage,
136}
137
138#[derive(Clone, Debug, Serialize, Deserialize)]
139pub struct SessionTurnInput {
140    pub session_id: String,
141    pub turn_id: String,
142    pub input: TurnInput,
143}
144
145pub struct SessionTurnRequest<'run> {
146    turn: SessionTurnInput,
147    scoped_effect_controller: crate::ScopedEffectController<'run>,
148}
149
150impl<'run> SessionTurnRequest<'run> {
151    pub fn new(
152        session_id: impl Into<String>,
153        turn_id: impl Into<String>,
154        mut input: TurnInput,
155        scoped_effect_controller: crate::ScopedEffectController<'run>,
156    ) -> Result<Self, PluginError> {
157        let session_id = session_id.into();
158        let turn_id = turn_id.into();
159        if turn_id.trim().is_empty() {
160            return Err(PluginError::Session(
161                "session turns require a non-empty stable turn id".to_string(),
162            ));
163        }
164        if scoped_effect_controller.turn_id() != Some(turn_id.as_str()) {
165            return Err(PluginError::Session(format!(
166                "session turn `{turn_id}` requires an effect turn scope with the same id"
167            )));
168        }
169        if scoped_effect_controller.effect_scope().session_id() != Some(session_id.as_str()) {
170            return Err(PluginError::Session(format!(
171                "session turn `{turn_id}` requires an effect scope for session `{session_id}`"
172            )));
173        }
174        if let Some(input_turn_id) = input.trace_turn_id.as_deref() {
175            if input_turn_id != turn_id {
176                return Err(PluginError::Session(format!(
177                    "input trace_turn_id `{input_turn_id}` does not match turn id `{turn_id}`"
178                )));
179            }
180        }
181        input.trace_turn_id = Some(turn_id.clone());
182        Ok(Self {
183            turn: SessionTurnInput {
184                session_id,
185                turn_id,
186                input,
187            },
188            scoped_effect_controller,
189        })
190    }
191
192    pub fn session_id(&self) -> &str {
193        &self.turn.session_id
194    }
195
196    pub fn turn_id(&self) -> &str {
197        &self.turn.turn_id
198    }
199
200    pub fn input(&self) -> &TurnInput {
201        &self.turn.input
202    }
203
204    pub fn scoped_effect_controller(&self) -> &crate::ScopedEffectController<'run> {
205        &self.scoped_effect_controller
206    }
207
208    pub fn into_parts(self) -> (SessionTurnInput, crate::ScopedEffectController<'run>) {
209        (self.turn, self.scoped_effect_controller)
210    }
211}
212
213#[derive(Clone, Debug, Serialize, Deserialize)]
214pub struct AppendSessionNodesRequest {
215    pub nodes: Vec<SessionAppendNode>,
216    #[serde(default)]
217    pub requires_ancestor_node_id: Option<String>,
218}
219
220#[derive(Clone, Debug, Serialize, Deserialize)]
221#[serde(tag = "status", rename_all = "snake_case")]
222pub enum AppendSessionNodesResult {
223    Appended {
224        node_ids: Vec<String>,
225        leaf_node_id: String,
226    },
227    StaleBranch {
228        current_leaf_node_id: Option<String>,
229    },
230}