use std::path::PathBuf;
use algocline_core::TokenUsage;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "op", rename_all = "snake_case")]
pub enum PoolRequest {
Handshake {
version: String,
},
Run {
code: String,
ctx: Option<serde_json::Value>,
lib_paths: Vec<PathBuf>,
},
Continue {
sid: String,
response: String,
query_id: Option<String>,
usage: Option<TokenUsage>,
},
Status {
#[serde(default)]
include_history: bool,
},
Shutdown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum PoolResponseData {
Handshake {
version: String,
},
Feed {
session_id: String,
feed_result: serde_json::Value,
},
Status {
has_session: bool,
session_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
conversation_history: Option<serde_json::Value>,
},
Shutdown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PoolResponse {
pub ok: bool,
pub data: Option<PoolResponseData>,
pub error: Option<String>,
}
impl PoolResponse {
pub fn success(data: PoolResponseData) -> Self {
Self {
ok: true,
data: Some(data),
error: None,
}
}
pub fn failure(error: impl Into<String>) -> Self {
Self {
ok: false,
data: None,
error: Some(error.into()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn handshake_request_roundtrip() {
let req = PoolRequest::Handshake {
version: "0.31.0".to_string(),
};
let json = serde_json::to_string(&req).expect("serialize");
assert!(!json.contains('\n'), "JSON line must not contain newlines");
assert!(json.contains("\"op\":\"handshake\""), "op field missing");
assert!(
json.contains("\"version\":\"0.31.0\""),
"version field missing"
);
let decoded: PoolRequest = serde_json::from_str(&json).expect("deserialize");
match decoded {
PoolRequest::Handshake { version } => {
assert_eq!(version, "0.31.0");
}
other => panic!("unexpected variant: {other:?}"),
}
}
#[test]
fn response_success_roundtrip() {
let resp = PoolResponse::success(PoolResponseData::Handshake {
version: "0.31.0".to_string(),
});
let json = serde_json::to_string(&resp).expect("serialize");
assert!(json.contains("\"ok\":true"), "ok flag missing");
let decoded: PoolResponse = serde_json::from_str(&json).expect("deserialize");
assert!(decoded.ok);
assert!(decoded.error.is_none());
match decoded.data {
Some(PoolResponseData::Handshake { version }) => {
assert_eq!(version, "0.31.0");
}
other => panic!("unexpected data: {other:?}"),
}
}
#[test]
fn status_request_legacy_wire_default_false() {
let legacy_json = r#"{"op":"status"}"#;
let decoded: PoolRequest = serde_json::from_str(legacy_json).expect("deserialize legacy");
match decoded {
PoolRequest::Status { include_history } => {
assert!(
!include_history,
"legacy {{op:status}} must default include_history=false"
);
}
other => panic!("unexpected variant: {other:?}"),
}
}
#[test]
fn status_request_with_history_roundtrip() {
let req = PoolRequest::Status {
include_history: true,
};
let json = serde_json::to_string(&req).expect("serialize");
assert!(json.contains("\"op\":\"status\""), "op missing");
assert!(
json.contains("\"include_history\":true"),
"include_history missing"
);
let decoded: PoolRequest = serde_json::from_str(&json).expect("deserialize");
match decoded {
PoolRequest::Status { include_history } => {
assert!(include_history, "round-trip must preserve true");
}
other => panic!("unexpected variant: {other:?}"),
}
}
#[test]
fn status_response_history_roundtrip_and_skip() {
let with_history = PoolResponse::success(PoolResponseData::Status {
has_session: true,
session_id: Some("sid-1".to_string()),
conversation_history: Some(serde_json::json!([{"role":"user","content":"hi"}])),
});
let json = serde_json::to_string(&with_history).expect("serialize");
assert!(
json.contains("\"conversation_history\""),
"conversation_history must be present when Some"
);
let decoded: PoolResponse = serde_json::from_str(&json).expect("deserialize");
match decoded.data {
Some(PoolResponseData::Status {
conversation_history: Some(_),
..
}) => {}
other => panic!("expected Status with history, got {other:?}"),
}
let without_history = PoolResponse::success(PoolResponseData::Status {
has_session: false,
session_id: None,
conversation_history: None,
});
let json = serde_json::to_string(&without_history).expect("serialize");
assert!(
!json.contains("\"conversation_history\""),
"conversation_history must be omitted when None"
);
let decoded: PoolResponse = serde_json::from_str(&json).expect("deserialize");
match decoded.data {
Some(PoolResponseData::Status {
conversation_history: None,
..
}) => {}
other => panic!("expected Status with no history, got {other:?}"),
}
}
#[test]
fn response_failure_roundtrip() {
let resp = PoolResponse::failure("version mismatch");
let json = serde_json::to_string(&resp).expect("serialize");
assert!(json.contains("\"ok\":false"), "ok flag missing");
let decoded: PoolResponse = serde_json::from_str(&json).expect("deserialize");
assert!(!decoded.ok);
assert!(decoded.data.is_none());
assert_eq!(decoded.error.as_deref(), Some("version mismatch"));
}
}