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