use serde::{Deserialize, Serialize};
use serde_json::Value;
use brainwires_core::ToolMode;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ViewerMessage {
UserInput {
content: String,
#[serde(default)]
context_files: Vec<String>,
},
Cancel,
SyncRequest,
Detach {
exit_when_done: bool,
},
Exit,
SlashCommand {
command: String,
args: Vec<String>,
},
SetToolMode {
mode: ToolMode,
},
QueueMessage {
content: String,
},
AcquireLock {
resource_type: ResourceLockType,
scope: String,
description: String,
},
ReleaseLock {
resource_type: ResourceLockType,
scope: String,
},
QueryLocks {
scope: Option<String>,
},
UpdateLockStatus {
resource_type: ResourceLockType,
scope: String,
status: String,
},
ListAgents,
SpawnAgent {
model: Option<String>,
reason: Option<String>,
working_directory: Option<String>,
},
NotifyChildren {
action: ChildNotifyAction,
},
ParentSignal {
signal: ParentSignalType,
parent_session_id: String,
},
Disconnect,
EnterPlanMode {
focus: Option<String>,
},
ExitPlanMode,
PlanModeUserInput {
content: String,
#[serde(default)]
context_files: Vec<String>,
},
PlanModeSyncRequest,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum AgentMessage {
StreamChunk {
text: String,
},
StreamEnd {
finish_reason: Option<String>,
},
ToolCallStart {
id: String,
name: String,
#[serde(default)]
server: Option<String>,
input: Value,
},
ToolProgress {
name: String,
message: String,
progress: Option<f64>,
},
ToolResult {
id: String,
name: String,
output: Option<String>,
error: Option<String>,
},
ConversationSync {
session_id: String,
model: String,
messages: Vec<DisplayMessage>,
status: String,
is_busy: bool,
tool_mode: ToolMode,
mcp_servers: Vec<String>,
},
MessageAdded {
message: DisplayMessage,
},
StatusUpdate {
status: String,
},
TaskUpdate {
task_tree: String,
task_count: usize,
completed_count: usize,
},
Error {
message: String,
fatal: bool,
},
Exiting {
reason: String,
},
Ack {
command: String,
},
SlashCommandResult {
command: String,
success: bool,
#[serde(skip_serializing_if = "Option::is_none")]
output: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
action_taken: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
error: Option<String>,
#[serde(default)]
blocked: bool,
},
Toast {
message: String,
duration_ms: u64,
},
SealStatus {
enabled: bool,
entity_count: usize,
last_resolution: Option<String>,
quality_score: f32,
},
LockResult {
success: bool,
resource_type: ResourceLockType,
scope: String,
error: Option<String>,
blocking_agent: Option<String>,
},
LockReleased {
resource_type: ResourceLockType,
scope: String,
},
LockStatus {
locks: Vec<LockInfo>,
},
LockChanged {
change: LockChangeType,
lock: LockInfo,
},
AgentSpawned {
new_session_id: String,
parent_session_id: String,
spawn_reason: String,
model: String,
},
AgentList {
agents: Vec<AgentMetadata>,
},
AgentExiting {
session_id: String,
reason: String,
children_notified: Vec<String>,
},
ParentSignalReceived {
signal: ParentSignalType,
parent_session_id: String,
},
PlanModeEntered {
plan_session_id: String,
messages: Vec<DisplayMessage>,
status: String,
},
PlanModeExited {
summary: Option<String>,
},
PlanModeSync {
plan_session_id: String,
main_session_id: String,
messages: Vec<DisplayMessage>,
status: String,
is_busy: bool,
},
PlanModeMessageAdded {
message: DisplayMessage,
},
PlanModeStreamChunk {
text: String,
},
PlanModeStreamEnd {
finish_reason: Option<String>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ResourceLockType {
Build,
Test,
BuildTest,
GitIndex,
GitCommit,
GitRemoteWrite,
GitRemoteMerge,
GitBranch,
GitDestructive,
}
impl ResourceLockType {
pub fn as_lock_type_str(&self) -> &'static str {
match self {
ResourceLockType::Build => "build",
ResourceLockType::Test => "test",
ResourceLockType::BuildTest => "build_test",
ResourceLockType::GitIndex => "git_index",
ResourceLockType::GitCommit => "git_commit",
ResourceLockType::GitRemoteWrite => "git_remote_write",
ResourceLockType::GitRemoteMerge => "git_remote_merge",
ResourceLockType::GitBranch => "git_branch",
ResourceLockType::GitDestructive => "git_destructive",
}
}
pub fn from_lock_type_str(s: &str) -> Option<Self> {
match s {
"build" => Some(ResourceLockType::Build),
"test" => Some(ResourceLockType::Test),
"build_test" => Some(ResourceLockType::BuildTest),
"git_index" => Some(ResourceLockType::GitIndex),
"git_commit" => Some(ResourceLockType::GitCommit),
"git_remote_write" => Some(ResourceLockType::GitRemoteWrite),
"git_remote_merge" => Some(ResourceLockType::GitRemoteMerge),
"git_branch" => Some(ResourceLockType::GitBranch),
"git_destructive" => Some(ResourceLockType::GitDestructive),
_ => None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LockInfo {
pub agent_id: String,
pub resource_type: ResourceLockType,
pub scope: String,
pub description: String,
pub status: String,
pub held_for_secs: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum LockChangeType {
Acquired,
Released,
Stale,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DisplayMessage {
pub role: String,
pub content: String,
pub created_at: i64,
}
impl DisplayMessage {
pub fn new(role: impl Into<String>, content: impl Into<String>, created_at: i64) -> Self {
Self {
role: role.into(),
content: content.into(),
created_at,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentConfig {
pub session_id: String,
pub model: String,
pub mdap_enabled: bool,
pub seal_enabled: bool,
pub working_directory: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Handshake {
pub version: u32,
pub is_reattach: bool,
pub session_id: Option<String>,
#[serde(default)]
pub session_token: Option<String>,
}
impl Handshake {
pub const PROTOCOL_VERSION: u32 = 2;
pub fn new_session() -> Self {
Self {
version: Self::PROTOCOL_VERSION,
is_reattach: false,
session_id: None,
session_token: None,
}
}
pub fn reattach(session_id: String, session_token: String) -> Self {
Self {
version: Self::PROTOCOL_VERSION,
is_reattach: true,
session_id: Some(session_id),
session_token: Some(session_token),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HandshakeResponse {
pub accepted: bool,
pub session_id: String,
#[serde(default)]
pub session_token: Option<String>,
pub error: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentMetadata {
pub session_id: String,
pub parent_agent_id: Option<String>,
pub spawn_reason: Option<String>,
pub model: String,
pub created_at: i64,
pub last_activity: i64,
pub working_directory: String,
pub is_busy: bool,
#[serde(default)]
pub pid: Option<u32>,
}
impl AgentMetadata {
pub fn new(
session_id: String,
model: String,
working_directory: String,
) -> Self {
let now = chrono::Utc::now().timestamp();
Self {
session_id,
parent_agent_id: None,
spawn_reason: None,
model,
created_at: now,
last_activity: now,
working_directory,
is_busy: false,
pid: None,
}
}
pub fn with_parent(mut self, parent_id: String, reason: Option<String>) -> Self {
self.parent_agent_id = Some(parent_id);
self.spawn_reason = reason;
self
}
pub fn with_pid(mut self, pid: u32) -> Self {
self.pid = Some(pid);
self
}
pub fn touch(&mut self) {
self.last_activity = chrono::Utc::now().timestamp();
}
pub fn set_busy(&mut self, busy: bool) {
self.is_busy = busy;
self.touch();
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ChildNotifyAction {
ShutdownIfIdle,
ForceShutdown,
Detach,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ParentSignalType {
ParentExiting,
Shutdown,
Detached,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_viewer_message_serialization() {
let msg = ViewerMessage::UserInput {
content: "Hello".to_string(),
context_files: vec!["main.rs".to_string()],
};
let json = serde_json::to_string(&msg).unwrap();
assert!(json.contains("user_input"));
assert!(json.contains("Hello"));
}
#[test]
fn test_agent_message_serialization() {
let msg = AgentMessage::StreamChunk {
text: "World".to_string(),
};
let json = serde_json::to_string(&msg).unwrap();
assert!(json.contains("stream_chunk"));
assert!(json.contains("World"));
}
#[test]
fn test_handshake_new_session() {
let hs = Handshake::new_session();
assert!(!hs.is_reattach);
assert!(hs.session_id.is_none());
assert!(hs.session_token.is_none());
assert_eq!(hs.version, Handshake::PROTOCOL_VERSION);
}
#[test]
fn test_handshake_reattach() {
let token = "abc123def456".to_string();
let hs = Handshake::reattach("session-123".to_string(), token.clone());
assert!(hs.is_reattach);
assert_eq!(hs.session_id, Some("session-123".to_string()));
assert_eq!(hs.session_token, Some(token));
}
#[test]
fn test_viewer_message_cancel() {
let msg = ViewerMessage::Cancel;
let json = serde_json::to_string(&msg).unwrap();
let parsed: ViewerMessage = serde_json::from_str(&json).unwrap();
assert!(matches!(parsed, ViewerMessage::Cancel));
}
#[test]
fn test_agent_message_tool_result() {
let msg = AgentMessage::ToolResult {
id: "tool-1".to_string(),
name: "read_file".to_string(),
output: Some("content".to_string()),
error: None,
};
let json = serde_json::to_string(&msg).unwrap();
let parsed: AgentMessage = serde_json::from_str(&json).unwrap();
match parsed {
AgentMessage::ToolResult { id, name, output, error } => {
assert_eq!(id, "tool-1");
assert_eq!(name, "read_file");
assert_eq!(output, Some("content".to_string()));
assert!(error.is_none());
}
_ => panic!("Expected ToolResult"),
}
}
}