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#[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}