use serde::{Deserialize, Serialize};
pub const PROTOCOL_VERSION: &str = "1.1";
pub const MIN_PROTOCOL_VERSION: &str = "1.0";
pub const SUPPORTED_VERSIONS: &[&str] = &["1.1", "1.0"];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProtocolCapability {
Streaming,
Tools,
Presence,
Compression,
Attachments,
Priority,
Telemetry,
}
impl ProtocolCapability {
pub fn all_supported() -> Vec<Self> {
vec![
Self::Streaming,
Self::Tools,
Self::Attachments,
Self::Priority,
]
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
#[serde(rename_all = "snake_case")]
#[derive(Default)]
pub enum CommandPriority {
Critical = 0,
High = 1,
#[default]
Normal = 2,
Low = 3,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RetryPolicy {
pub max_attempts: u32,
pub backoff_multiplier: f32,
#[serde(default = "default_initial_delay")]
pub initial_delay_ms: u64,
}
fn default_initial_delay() -> u64 {
100
}
impl Default for RetryPolicy {
fn default() -> Self {
Self {
max_attempts: 3,
backoff_multiplier: 2.0,
initial_delay_ms: 100,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PrioritizedCommand {
pub command: BackendCommand,
#[serde(default)]
pub priority: CommandPriority,
#[serde(skip_serializing_if = "Option::is_none")]
pub deadline_ms: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub retry_policy: Option<RetryPolicy>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProtocolHello {
pub supported_versions: Vec<String>,
pub preferred_version: String,
pub capabilities: Vec<ProtocolCapability>,
}
impl Default for ProtocolHello {
fn default() -> Self {
Self {
supported_versions: SUPPORTED_VERSIONS.iter().map(|s| s.to_string()).collect(),
preferred_version: PROTOCOL_VERSION.to_string(),
capabilities: ProtocolCapability::all_supported(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProtocolAccept {
pub selected_version: String,
pub enabled_capabilities: Vec<ProtocolCapability>,
}
impl Default for ProtocolAccept {
fn default() -> Self {
Self {
selected_version: PROTOCOL_VERSION.to_string(),
enabled_capabilities: vec![ProtocolCapability::Streaming, ProtocolCapability::Tools],
}
}
}
#[derive(Debug, Clone)]
pub struct NegotiatedProtocol {
pub version: String,
pub capabilities: Vec<ProtocolCapability>,
}
impl NegotiatedProtocol {
pub fn has_capability(&self, cap: ProtocolCapability) -> bool {
self.capabilities.contains(&cap)
}
pub fn from_accept(accept: ProtocolAccept) -> Self {
Self {
version: accept.selected_version,
capabilities: accept.enabled_capabilities,
}
}
}
impl Default for NegotiatedProtocol {
fn default() -> Self {
Self {
version: PROTOCOL_VERSION.to_string(),
capabilities: vec![ProtocolCapability::Streaming, ProtocolCapability::Tools],
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum RemoteMessage {
Register {
api_key: String,
hostname: String,
os: String,
version: String,
#[serde(skip_serializing_if = "Option::is_none")]
protocol: Option<ProtocolHello>,
},
Heartbeat {
session_token: String,
agents: Vec<RemoteAgentInfo>,
system_load: f32,
},
CommandResult {
command_id: String,
success: bool,
#[serde(skip_serializing_if = "Option::is_none")]
result: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
error: Option<String>,
},
AgentEvent {
event_type: AgentEventType,
agent_id: String,
data: serde_json::Value,
},
AgentStream {
agent_id: String,
chunk_type: StreamChunkType,
content: String,
},
Pong {
timestamp: i64,
},
AttachmentReceived {
attachment_id: String,
success: bool,
#[serde(skip_serializing_if = "Option::is_none")]
file_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
error: Option<String>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum BackendCommand {
Authenticated {
session_token: String,
user_id: String,
refresh_interval_secs: u32,
#[serde(skip_serializing_if = "Option::is_none")]
protocol: Option<ProtocolAccept>,
},
SendInput {
command_id: String,
agent_id: String,
content: String,
},
SlashCommand {
command_id: String,
agent_id: String,
command: String,
args: Vec<String>,
},
CancelOperation {
command_id: String,
agent_id: String,
},
Subscribe {
agent_id: String,
},
Unsubscribe {
agent_id: String,
},
SpawnAgent {
command_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
model: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
working_directory: Option<String>,
},
RequestSync,
Ping {
timestamp: i64,
},
Disconnect {
reason: String,
},
AuthenticationFailed {
error: String,
},
AttachmentUpload {
command_id: String,
agent_id: String,
attachment_id: String,
filename: String,
mime_type: String,
size: u64,
compressed: bool,
#[serde(skip_serializing_if = "Option::is_none")]
compression_algorithm: Option<CompressionAlgorithm>,
chunks_total: u32,
},
AttachmentChunk {
attachment_id: String,
chunk_index: u32,
data: String,
is_final: bool,
},
AttachmentComplete {
attachment_id: String,
checksum: String,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CompressionAlgorithm {
Zstd,
Gzip,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RemoteAgentInfo {
pub session_id: String,
pub model: String,
pub is_busy: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_id: Option<String>,
pub working_directory: String,
pub message_count: usize,
pub last_activity: i64,
pub status: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AgentEventType {
Spawned,
Exited,
Busy,
Idle,
StateChanged,
ViewerConnected,
ViewerDisconnected,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum StreamChunkType {
Text,
Thinking,
ToolCall,
ToolResult,
Error,
System,
Complete,
History,
UserInput,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_remote_message_serialization() {
let msg = RemoteMessage::Register {
api_key: "bw_prod_test123".to_string(),
hostname: "my-laptop".to_string(),
os: "linux".to_string(),
version: "0.5.0".to_string(),
protocol: None,
};
let json = serde_json::to_string(&msg).unwrap();
assert!(json.contains("\"type\":\"register\""));
assert!(json.contains("\"api_key\":\"bw_prod_test123\""));
}
#[test]
fn test_remote_message_with_protocol() {
let msg = RemoteMessage::Register {
api_key: "bw_prod_test123".to_string(),
hostname: "my-laptop".to_string(),
os: "linux".to_string(),
version: "0.5.0".to_string(),
protocol: Some(ProtocolHello::default()),
};
let json = serde_json::to_string(&msg).unwrap();
assert!(json.contains("\"protocol\""));
assert!(json.contains("\"preferred_version\":\"1.1\""));
assert!(json.contains("\"streaming\""));
}
#[test]
fn test_backend_command_deserialization() {
let json = r#"{"type":"authenticated","session_token":"abc123","user_id":"user-456","refresh_interval_secs":30}"#;
let cmd: BackendCommand = serde_json::from_str(json).unwrap();
match cmd {
BackendCommand::Authenticated {
session_token,
user_id,
refresh_interval_secs,
protocol,
} => {
assert_eq!(session_token, "abc123");
assert_eq!(user_id, "user-456");
assert_eq!(refresh_interval_secs, 30);
assert!(protocol.is_none());
}
_ => panic!("Expected Authenticated command"),
}
}
#[test]
fn test_backend_command_with_protocol() {
let json = r#"{"type":"authenticated","session_token":"abc123","user_id":"user-456","refresh_interval_secs":30,"protocol":{"selected_version":"1.1","enabled_capabilities":["streaming","tools"]}}"#;
let cmd: BackendCommand = serde_json::from_str(json).unwrap();
match cmd {
BackendCommand::Authenticated {
protocol,
..
} => {
let proto = protocol.expect("Expected protocol");
assert_eq!(proto.selected_version, "1.1");
assert!(proto.enabled_capabilities.contains(&ProtocolCapability::Streaming));
assert!(proto.enabled_capabilities.contains(&ProtocolCapability::Tools));
}
_ => panic!("Expected Authenticated command"),
}
}
#[test]
fn test_protocol_capability_serialization() {
let cap = ProtocolCapability::Streaming;
let json = serde_json::to_string(&cap).unwrap();
assert_eq!(json, "\"streaming\"");
let cap: ProtocolCapability = serde_json::from_str("\"attachments\"").unwrap();
assert_eq!(cap, ProtocolCapability::Attachments);
}
#[test]
fn test_negotiated_protocol() {
let accept = ProtocolAccept {
selected_version: "1.1".to_string(),
enabled_capabilities: vec![ProtocolCapability::Streaming, ProtocolCapability::Compression],
};
let negotiated = NegotiatedProtocol::from_accept(accept);
assert!(negotiated.has_capability(ProtocolCapability::Streaming));
assert!(negotiated.has_capability(ProtocolCapability::Compression));
assert!(!negotiated.has_capability(ProtocolCapability::Attachments));
}
#[test]
fn test_remote_agent_info() {
let info = RemoteAgentInfo {
session_id: "agent-123".to_string(),
model: "claude-3-5-sonnet".to_string(),
is_busy: false,
parent_id: None,
working_directory: "/home/user/project".to_string(),
message_count: 5,
last_activity: 1700000000,
status: "idle".to_string(),
name: Some("main-agent".to_string()),
};
let json = serde_json::to_string(&info).unwrap();
assert!(json.contains("\"session_id\":\"agent-123\""));
assert!(json.contains("\"name\":\"main-agent\""));
}
}