use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Deserialize)]
pub struct SendMessageRequest {
pub content: String,
pub thread_id: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct SendMessageResponse {
pub message_id: Uuid,
pub status: &'static str,
}
#[derive(Debug, Serialize)]
pub struct ThreadInfo {
pub id: Uuid,
pub state: String,
pub turn_count: usize,
pub created_at: String,
pub updated_at: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thread_type: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct ThreadListResponse {
pub assistant_thread: Option<ThreadInfo>,
pub threads: Vec<ThreadInfo>,
pub active_thread: Option<Uuid>,
}
#[derive(Debug, Serialize)]
pub struct TurnInfo {
pub turn_number: usize,
pub user_input: String,
pub response: Option<String>,
pub state: String,
pub started_at: String,
pub completed_at: Option<String>,
pub tool_calls: Vec<ToolCallInfo>,
}
#[derive(Debug, Serialize)]
pub struct ToolCallInfo {
pub name: String,
pub has_result: bool,
pub has_error: bool,
}
#[derive(Debug, Serialize)]
pub struct HistoryResponse {
pub thread_id: Uuid,
pub turns: Vec<TurnInfo>,
#[serde(default)]
pub has_more: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub oldest_timestamp: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct ApprovalRequest {
pub request_id: String,
pub action: String,
pub thread_id: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "type")]
pub enum SseEvent {
#[serde(rename = "response")]
Response { content: String, thread_id: String },
#[serde(rename = "thinking")]
Thinking {
message: String,
#[serde(skip_serializing_if = "Option::is_none")]
thread_id: Option<String>,
},
#[serde(rename = "tool_started")]
ToolStarted {
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
thread_id: Option<String>,
},
#[serde(rename = "tool_completed")]
ToolCompleted {
name: String,
success: bool,
#[serde(skip_serializing_if = "Option::is_none")]
thread_id: Option<String>,
},
#[serde(rename = "tool_result")]
ToolResult {
name: String,
preview: String,
#[serde(skip_serializing_if = "Option::is_none")]
thread_id: Option<String>,
},
#[serde(rename = "stream_chunk")]
StreamChunk {
content: String,
#[serde(skip_serializing_if = "Option::is_none")]
thread_id: Option<String>,
},
#[serde(rename = "status")]
Status {
message: String,
#[serde(skip_serializing_if = "Option::is_none")]
thread_id: Option<String>,
},
#[serde(rename = "job_started")]
JobStarted {
job_id: String,
title: String,
browse_url: String,
},
#[serde(rename = "approval_needed")]
ApprovalNeeded {
request_id: String,
tool_name: String,
description: String,
parameters: String,
},
#[serde(rename = "auth_required")]
AuthRequired {
extension_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
instructions: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
auth_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
setup_url: Option<String>,
},
#[serde(rename = "auth_completed")]
AuthCompleted {
extension_name: String,
success: bool,
message: String,
},
#[serde(rename = "error")]
Error {
message: String,
#[serde(skip_serializing_if = "Option::is_none")]
thread_id: Option<String>,
},
#[serde(rename = "heartbeat")]
Heartbeat,
#[serde(rename = "job_message")]
JobMessage {
job_id: String,
role: String,
content: String,
},
#[serde(rename = "job_tool_use")]
JobToolUse {
job_id: String,
tool_name: String,
input: serde_json::Value,
},
#[serde(rename = "job_tool_result")]
JobToolResult {
job_id: String,
tool_name: String,
output: String,
},
#[serde(rename = "job_status")]
JobStatus { job_id: String, message: String },
#[serde(rename = "job_result")]
JobResult {
job_id: String,
status: String,
#[serde(skip_serializing_if = "Option::is_none")]
session_id: Option<String>,
},
}
#[derive(Debug, Serialize)]
pub struct MemoryTreeResponse {
pub entries: Vec<TreeEntry>,
}
#[derive(Debug, Serialize)]
pub struct TreeEntry {
pub path: String,
pub is_dir: bool,
}
#[derive(Debug, Serialize)]
pub struct MemoryListResponse {
pub path: String,
pub entries: Vec<ListEntry>,
}
#[derive(Debug, Serialize)]
pub struct ListEntry {
pub name: String,
pub path: String,
pub is_dir: bool,
pub updated_at: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct MemoryReadResponse {
pub path: String,
pub content: String,
pub updated_at: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct MemoryWriteRequest {
pub path: String,
pub content: String,
}
#[derive(Debug, Serialize)]
pub struct MemoryWriteResponse {
pub path: String,
pub status: &'static str,
}
#[derive(Debug, Deserialize)]
pub struct MemorySearchRequest {
pub query: String,
pub limit: Option<usize>,
}
#[derive(Debug, Serialize)]
pub struct MemorySearchResponse {
pub results: Vec<SearchHit>,
}
#[derive(Debug, Serialize)]
pub struct SearchHit {
pub path: String,
pub content: String,
pub score: f64,
}
#[derive(Debug, Serialize)]
pub struct JobInfo {
pub id: Uuid,
pub title: String,
pub state: String,
pub user_id: String,
pub created_at: String,
pub started_at: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct JobListResponse {
pub jobs: Vec<JobInfo>,
}
#[derive(Debug, Serialize)]
pub struct JobSummaryResponse {
pub total: usize,
pub pending: usize,
pub in_progress: usize,
pub completed: usize,
pub failed: usize,
pub stuck: usize,
}
#[derive(Debug, Serialize)]
pub struct JobDetailResponse {
pub id: Uuid,
pub title: String,
pub description: String,
pub state: String,
pub user_id: String,
pub created_at: String,
pub started_at: Option<String>,
pub completed_at: Option<String>,
pub elapsed_secs: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub project_dir: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub browse_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub job_mode: Option<String>,
pub transitions: Vec<TransitionInfo>,
}
#[derive(Debug, Serialize)]
pub struct ProjectFileEntry {
pub name: String,
pub path: String,
pub is_dir: bool,
}
#[derive(Debug, Serialize)]
pub struct ProjectFilesResponse {
pub entries: Vec<ProjectFileEntry>,
}
#[derive(Debug, Serialize)]
pub struct ProjectFileReadResponse {
pub path: String,
pub content: String,
}
#[derive(Debug, Serialize)]
pub struct TransitionInfo {
pub from: String,
pub to: String,
pub timestamp: String,
pub reason: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct ExtensionInfo {
pub name: String,
pub kind: String,
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
pub authenticated: bool,
pub active: bool,
pub tools: Vec<String>,
}
#[derive(Debug, Serialize)]
pub struct ExtensionListResponse {
pub extensions: Vec<ExtensionInfo>,
}
#[derive(Debug, Serialize)]
pub struct ToolInfo {
pub name: String,
pub description: String,
}
#[derive(Debug, Serialize)]
pub struct ToolListResponse {
pub tools: Vec<ToolInfo>,
}
#[derive(Debug, Deserialize)]
pub struct InstallExtensionRequest {
pub name: String,
pub url: Option<String>,
pub kind: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct ActionResponse {
pub success: bool,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub auth_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub awaiting_token: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub instructions: Option<String>,
}
impl ActionResponse {
pub fn ok(message: impl Into<String>) -> Self {
Self {
success: true,
message: message.into(),
auth_url: None,
awaiting_token: None,
instructions: None,
}
}
pub fn fail(message: impl Into<String>) -> Self {
Self {
success: false,
message: message.into(),
auth_url: None,
awaiting_token: None,
instructions: None,
}
}
}
#[derive(Debug, Deserialize)]
pub struct AuthTokenRequest {
pub extension_name: String,
pub token: String,
}
#[derive(Debug, Deserialize)]
pub struct AuthCancelRequest {
pub extension_name: String,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(tag = "type")]
pub enum WsClientMessage {
#[serde(rename = "message")]
Message {
content: String,
thread_id: Option<String>,
},
#[serde(rename = "approval")]
Approval {
request_id: String,
action: String,
thread_id: Option<String>,
},
#[serde(rename = "auth_token")]
AuthToken {
extension_name: String,
token: String,
},
#[serde(rename = "auth_cancel")]
AuthCancel { extension_name: String },
#[serde(rename = "ping")]
Ping,
}
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "type")]
pub enum WsServerMessage {
#[serde(rename = "event")]
Event {
event_type: String,
data: serde_json::Value,
},
#[serde(rename = "pong")]
Pong,
#[serde(rename = "error")]
Error { message: String },
}
impl WsServerMessage {
pub fn from_sse_event(event: &SseEvent) -> Self {
let event_type = match event {
SseEvent::Response { .. } => "response",
SseEvent::Thinking { .. } => "thinking",
SseEvent::ToolStarted { .. } => "tool_started",
SseEvent::ToolCompleted { .. } => "tool_completed",
SseEvent::ToolResult { .. } => "tool_result",
SseEvent::StreamChunk { .. } => "stream_chunk",
SseEvent::Status { .. } => "status",
SseEvent::JobStarted { .. } => "job_started",
SseEvent::ApprovalNeeded { .. } => "approval_needed",
SseEvent::AuthRequired { .. } => "auth_required",
SseEvent::AuthCompleted { .. } => "auth_completed",
SseEvent::Error { .. } => "error",
SseEvent::Heartbeat => "heartbeat",
SseEvent::JobMessage { .. } => "job_message",
SseEvent::JobToolUse { .. } => "job_tool_use",
SseEvent::JobToolResult { .. } => "job_tool_result",
SseEvent::JobStatus { .. } => "job_status",
SseEvent::JobResult { .. } => "job_result",
};
let data = serde_json::to_value(event).unwrap_or(serde_json::Value::Null);
WsServerMessage::Event {
event_type: event_type.to_string(),
data,
}
}
}
#[derive(Debug, Serialize)]
pub struct RoutineInfo {
pub id: Uuid,
pub name: String,
pub description: String,
pub enabled: bool,
pub trigger_type: String,
pub trigger_summary: String,
pub action_type: String,
pub last_run_at: Option<String>,
pub next_fire_at: Option<String>,
pub run_count: u64,
pub consecutive_failures: u32,
pub status: String,
}
#[derive(Debug, Serialize)]
pub struct RoutineListResponse {
pub routines: Vec<RoutineInfo>,
}
#[derive(Debug, Serialize)]
pub struct RoutineSummaryResponse {
pub total: u64,
pub enabled: u64,
pub disabled: u64,
pub failing: u64,
pub runs_today: u64,
}
#[derive(Debug, Serialize)]
pub struct RoutineDetailResponse {
pub id: Uuid,
pub name: String,
pub description: String,
pub enabled: bool,
pub trigger: serde_json::Value,
pub action: serde_json::Value,
pub guardrails: serde_json::Value,
pub notify: serde_json::Value,
pub last_run_at: Option<String>,
pub next_fire_at: Option<String>,
pub run_count: u64,
pub consecutive_failures: u32,
pub created_at: String,
pub recent_runs: Vec<RoutineRunInfo>,
}
#[derive(Debug, Serialize)]
pub struct RoutineRunInfo {
pub id: Uuid,
pub trigger_type: String,
pub started_at: String,
pub completed_at: Option<String>,
pub status: String,
pub result_summary: Option<String>,
pub tokens_used: Option<i32>,
}
#[derive(Debug, Serialize)]
pub struct SettingResponse {
pub key: String,
pub value: serde_json::Value,
pub updated_at: String,
}
#[derive(Debug, Serialize)]
pub struct SettingsListResponse {
pub settings: Vec<SettingResponse>,
}
#[derive(Debug, Deserialize)]
pub struct SettingWriteRequest {
pub value: serde_json::Value,
}
#[derive(Debug, Deserialize)]
pub struct SettingsImportRequest {
pub settings: std::collections::HashMap<String, serde_json::Value>,
}
#[derive(Debug, Serialize)]
pub struct SettingsExportResponse {
pub settings: std::collections::HashMap<String, serde_json::Value>,
}
#[derive(Debug, Serialize)]
pub struct HealthResponse {
pub status: &'static str,
pub channel: &'static str,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ws_client_message_parse() {
let json = r#"{"type":"message","content":"hello","thread_id":"t1"}"#;
let msg: WsClientMessage = serde_json::from_str(json).unwrap();
match msg {
WsClientMessage::Message { content, thread_id } => {
assert_eq!(content, "hello");
assert_eq!(thread_id.as_deref(), Some("t1"));
}
_ => panic!("Expected Message variant"),
}
}
#[test]
fn test_ws_client_message_no_thread() {
let json = r#"{"type":"message","content":"hi"}"#;
let msg: WsClientMessage = serde_json::from_str(json).unwrap();
match msg {
WsClientMessage::Message { content, thread_id } => {
assert_eq!(content, "hi");
assert!(thread_id.is_none());
}
_ => panic!("Expected Message variant"),
}
}
#[test]
fn test_ws_client_approval_parse() {
let json =
r#"{"type":"approval","request_id":"abc-123","action":"approve","thread_id":"t1"}"#;
let msg: WsClientMessage = serde_json::from_str(json).unwrap();
match msg {
WsClientMessage::Approval {
request_id,
action,
thread_id,
} => {
assert_eq!(request_id, "abc-123");
assert_eq!(action, "approve");
assert_eq!(thread_id.as_deref(), Some("t1"));
}
_ => panic!("Expected Approval variant"),
}
}
#[test]
fn test_ws_client_approval_parse_no_thread() {
let json = r#"{"type":"approval","request_id":"abc-123","action":"deny"}"#;
let msg: WsClientMessage = serde_json::from_str(json).unwrap();
match msg {
WsClientMessage::Approval {
request_id,
action,
thread_id,
} => {
assert_eq!(request_id, "abc-123");
assert_eq!(action, "deny");
assert!(thread_id.is_none());
}
_ => panic!("Expected Approval variant"),
}
}
#[test]
fn test_ws_client_ping_parse() {
let json = r#"{"type":"ping"}"#;
let msg: WsClientMessage = serde_json::from_str(json).unwrap();
assert!(matches!(msg, WsClientMessage::Ping));
}
#[test]
fn test_ws_client_unknown_type_fails() {
let json = r#"{"type":"unknown"}"#;
let result: Result<WsClientMessage, _> = serde_json::from_str(json);
assert!(result.is_err());
}
#[test]
fn test_ws_server_pong_serialize() {
let msg = WsServerMessage::Pong;
let json = serde_json::to_string(&msg).unwrap();
assert_eq!(json, r#"{"type":"pong"}"#);
}
#[test]
fn test_ws_server_error_serialize() {
let msg = WsServerMessage::Error {
message: "bad request".to_string(),
};
let json = serde_json::to_string(&msg).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["type"], "error");
assert_eq!(parsed["message"], "bad request");
}
#[test]
fn test_ws_server_from_sse_response() {
let sse = SseEvent::Response {
content: "hello".to_string(),
thread_id: "t1".to_string(),
};
let ws = WsServerMessage::from_sse_event(&sse);
match ws {
WsServerMessage::Event { event_type, data } => {
assert_eq!(event_type, "response");
assert_eq!(data["content"], "hello");
assert_eq!(data["thread_id"], "t1");
}
_ => panic!("Expected Event variant"),
}
}
#[test]
fn test_ws_server_from_sse_thinking() {
let sse = SseEvent::Thinking {
message: "reasoning...".to_string(),
thread_id: None,
};
let ws = WsServerMessage::from_sse_event(&sse);
match ws {
WsServerMessage::Event { event_type, data } => {
assert_eq!(event_type, "thinking");
assert_eq!(data["message"], "reasoning...");
}
_ => panic!("Expected Event variant"),
}
}
#[test]
fn test_ws_server_from_sse_approval_needed() {
let sse = SseEvent::ApprovalNeeded {
request_id: "r1".to_string(),
tool_name: "shell".to_string(),
description: "Run ls".to_string(),
parameters: "{}".to_string(),
};
let ws = WsServerMessage::from_sse_event(&sse);
match ws {
WsServerMessage::Event { event_type, data } => {
assert_eq!(event_type, "approval_needed");
assert_eq!(data["tool_name"], "shell");
}
_ => panic!("Expected Event variant"),
}
}
#[test]
fn test_ws_server_from_sse_heartbeat() {
let sse = SseEvent::Heartbeat;
let ws = WsServerMessage::from_sse_event(&sse);
match ws {
WsServerMessage::Event { event_type, .. } => {
assert_eq!(event_type, "heartbeat");
}
_ => panic!("Expected Event variant"),
}
}
#[test]
fn test_ws_client_auth_token_parse() {
let json = r#"{"type":"auth_token","extension_name":"notion","token":"sk-123"}"#;
let msg: WsClientMessage = serde_json::from_str(json).unwrap();
match msg {
WsClientMessage::AuthToken {
extension_name,
token,
} => {
assert_eq!(extension_name, "notion");
assert_eq!(token, "sk-123");
}
_ => panic!("Expected AuthToken variant"),
}
}
#[test]
fn test_ws_client_auth_cancel_parse() {
let json = r#"{"type":"auth_cancel","extension_name":"notion"}"#;
let msg: WsClientMessage = serde_json::from_str(json).unwrap();
match msg {
WsClientMessage::AuthCancel { extension_name } => {
assert_eq!(extension_name, "notion");
}
_ => panic!("Expected AuthCancel variant"),
}
}
#[test]
fn test_sse_auth_required_serialize() {
let event = SseEvent::AuthRequired {
extension_name: "notion".to_string(),
instructions: Some("Get your token from...".to_string()),
auth_url: None,
setup_url: Some("https://notion.so/integrations".to_string()),
};
let json = serde_json::to_string(&event).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["type"], "auth_required");
assert_eq!(parsed["extension_name"], "notion");
assert_eq!(parsed["instructions"], "Get your token from...");
assert!(parsed.get("auth_url").is_none());
assert_eq!(parsed["setup_url"], "https://notion.so/integrations");
}
#[test]
fn test_sse_auth_completed_serialize() {
let event = SseEvent::AuthCompleted {
extension_name: "notion".to_string(),
success: true,
message: "notion authenticated (3 tools loaded)".to_string(),
};
let json = serde_json::to_string(&event).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["type"], "auth_completed");
assert_eq!(parsed["extension_name"], "notion");
assert_eq!(parsed["success"], true);
}
#[test]
fn test_ws_server_from_sse_auth_required() {
let sse = SseEvent::AuthRequired {
extension_name: "openai".to_string(),
instructions: Some("Enter API key".to_string()),
auth_url: None,
setup_url: None,
};
let ws = WsServerMessage::from_sse_event(&sse);
match ws {
WsServerMessage::Event { event_type, data } => {
assert_eq!(event_type, "auth_required");
assert_eq!(data["extension_name"], "openai");
}
_ => panic!("Expected Event variant"),
}
}
#[test]
fn test_ws_server_from_sse_auth_completed() {
let sse = SseEvent::AuthCompleted {
extension_name: "slack".to_string(),
success: false,
message: "Invalid token".to_string(),
};
let ws = WsServerMessage::from_sse_event(&sse);
match ws {
WsServerMessage::Event { event_type, data } => {
assert_eq!(event_type, "auth_completed");
assert_eq!(data["success"], false);
}
_ => panic!("Expected Event variant"),
}
}
#[test]
fn test_auth_token_request_deserialize() {
let json = r#"{"extension_name":"telegram","token":"bot12345"}"#;
let req: AuthTokenRequest = serde_json::from_str(json).unwrap();
assert_eq!(req.extension_name, "telegram");
assert_eq!(req.token, "bot12345");
}
#[test]
fn test_auth_cancel_request_deserialize() {
let json = r#"{"extension_name":"telegram"}"#;
let req: AuthCancelRequest = serde_json::from_str(json).unwrap();
assert_eq!(req.extension_name, "telegram");
}
}