algocline_app/pool/
protocol.rs1use std::path::PathBuf;
2
3use algocline_core::TokenUsage;
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
20#[serde(tag = "op", rename_all = "snake_case")]
21pub enum PoolRequest {
22 Handshake {
24 version: String,
26 },
27
28 Run {
30 code: String,
32 ctx: Option<serde_json::Value>,
34 lib_paths: Vec<PathBuf>,
36 },
37
38 Continue {
40 sid: String,
42 response: String,
44 query_id: Option<String>,
46 usage: Option<TokenUsage>,
48 },
49
50 Status {
58 #[serde(default)]
59 include_history: bool,
60 },
61
62 Shutdown,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
75#[serde(tag = "kind", rename_all = "snake_case")]
76pub enum PoolResponseData {
77 Handshake {
79 version: String,
81 },
82
83 Feed {
85 session_id: String,
87 feed_result: serde_json::Value,
89 },
90
91 Status {
93 has_session: bool,
95 session_id: Option<String>,
97 #[serde(default, skip_serializing_if = "Option::is_none")]
101 conversation_history: Option<serde_json::Value>,
102 },
103
104 Shutdown,
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct PoolResponse {
118 pub ok: bool,
120 pub data: Option<PoolResponseData>,
122 pub error: Option<String>,
124}
125
126impl PoolResponse {
127 pub fn success(data: PoolResponseData) -> Self {
129 Self {
130 ok: true,
131 data: Some(data),
132 error: None,
133 }
134 }
135
136 pub fn failure(error: impl Into<String>) -> Self {
138 Self {
139 ok: false,
140 data: None,
141 error: Some(error.into()),
142 }
143 }
144}
145
146#[cfg(test)]
149mod tests {
150 use super::*;
151
152 #[test]
157 fn handshake_request_roundtrip() {
158 let req = PoolRequest::Handshake {
159 version: "0.31.0".to_string(),
160 };
161
162 let json = serde_json::to_string(&req).expect("serialize");
163 assert!(!json.contains('\n'), "JSON line must not contain newlines");
165 assert!(json.contains("\"op\":\"handshake\""), "op field missing");
167 assert!(
169 json.contains("\"version\":\"0.31.0\""),
170 "version field missing"
171 );
172
173 let decoded: PoolRequest = serde_json::from_str(&json).expect("deserialize");
174 match decoded {
175 PoolRequest::Handshake { version } => {
176 assert_eq!(version, "0.31.0");
177 }
178 other => panic!("unexpected variant: {other:?}"),
179 }
180 }
181
182 #[test]
184 fn response_success_roundtrip() {
185 let resp = PoolResponse::success(PoolResponseData::Handshake {
186 version: "0.31.0".to_string(),
187 });
188
189 let json = serde_json::to_string(&resp).expect("serialize");
190 assert!(json.contains("\"ok\":true"), "ok flag missing");
191
192 let decoded: PoolResponse = serde_json::from_str(&json).expect("deserialize");
193 assert!(decoded.ok);
194 assert!(decoded.error.is_none());
195 match decoded.data {
196 Some(PoolResponseData::Handshake { version }) => {
197 assert_eq!(version, "0.31.0");
198 }
199 other => panic!("unexpected data: {other:?}"),
200 }
201 }
202
203 #[test]
207 fn status_request_legacy_wire_default_false() {
208 let legacy_json = r#"{"op":"status"}"#;
209 let decoded: PoolRequest = serde_json::from_str(legacy_json).expect("deserialize legacy");
210 match decoded {
211 PoolRequest::Status { include_history } => {
212 assert!(
213 !include_history,
214 "legacy {{op:status}} must default include_history=false"
215 );
216 }
217 other => panic!("unexpected variant: {other:?}"),
218 }
219 }
220
221 #[test]
223 fn status_request_with_history_roundtrip() {
224 let req = PoolRequest::Status {
225 include_history: true,
226 };
227 let json = serde_json::to_string(&req).expect("serialize");
228 assert!(json.contains("\"op\":\"status\""), "op missing");
229 assert!(
230 json.contains("\"include_history\":true"),
231 "include_history missing"
232 );
233 let decoded: PoolRequest = serde_json::from_str(&json).expect("deserialize");
234 match decoded {
235 PoolRequest::Status { include_history } => {
236 assert!(include_history, "round-trip must preserve true");
237 }
238 other => panic!("unexpected variant: {other:?}"),
239 }
240 }
241
242 #[test]
245 fn status_response_history_roundtrip_and_skip() {
246 let with_history = PoolResponse::success(PoolResponseData::Status {
248 has_session: true,
249 session_id: Some("sid-1".to_string()),
250 conversation_history: Some(serde_json::json!([{"role":"user","content":"hi"}])),
251 });
252 let json = serde_json::to_string(&with_history).expect("serialize");
253 assert!(
254 json.contains("\"conversation_history\""),
255 "conversation_history must be present when Some"
256 );
257 let decoded: PoolResponse = serde_json::from_str(&json).expect("deserialize");
258 match decoded.data {
259 Some(PoolResponseData::Status {
260 conversation_history: Some(_),
261 ..
262 }) => {}
263 other => panic!("expected Status with history, got {other:?}"),
264 }
265
266 let without_history = PoolResponse::success(PoolResponseData::Status {
268 has_session: false,
269 session_id: None,
270 conversation_history: None,
271 });
272 let json = serde_json::to_string(&without_history).expect("serialize");
273 assert!(
274 !json.contains("\"conversation_history\""),
275 "conversation_history must be omitted when None"
276 );
277 let decoded: PoolResponse = serde_json::from_str(&json).expect("deserialize");
278 match decoded.data {
279 Some(PoolResponseData::Status {
280 conversation_history: None,
281 ..
282 }) => {}
283 other => panic!("expected Status with no history, got {other:?}"),
284 }
285 }
286
287 #[test]
289 fn response_failure_roundtrip() {
290 let resp = PoolResponse::failure("version mismatch");
291
292 let json = serde_json::to_string(&resp).expect("serialize");
293 assert!(json.contains("\"ok\":false"), "ok flag missing");
294
295 let decoded: PoolResponse = serde_json::from_str(&json).expect("deserialize");
296 assert!(!decoded.ok);
297 assert!(decoded.data.is_none());
298 assert_eq!(decoded.error.as_deref(), Some("version mismatch"));
299 }
300}