use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum BrainEvent {
SignalReceived {
id: Uuid,
signal: SignalSummary,
ts: DateTime<Utc>,
},
IntentClassified {
id: Uuid,
intent: IntentSummary,
confidence: f32,
ts: DateTime<Utc>,
},
ToolRouteResolved {
id: Uuid,
route: ToolRouteSummary,
ts: DateTime<Utc>,
},
ConfirmationRequested {
id: Uuid,
nonce: String,
reason: String,
ts: DateTime<Utc>,
},
ConfirmationResolved {
id: Uuid,
nonce: String,
decision: String,
ts: DateTime<Utc>,
},
ToolCallStarted {
id: Uuid,
tool_id: String,
args_redacted: serde_json::Value,
ts: DateTime<Utc>,
},
ToolCallFinished {
id: Uuid,
tool_id: String,
outcome: OutcomeSummary,
duration_ms: u64,
ts: DateTime<Utc>,
},
ReflexFired {
id: Uuid,
trigger_id: String,
payload: serde_json::Value,
ts: DateTime<Utc>,
},
AuditAppended {
id: Uuid,
audit_entry_id: String,
principal: Option<PrincipalSummary>,
ts: DateTime<Utc>,
},
BudgetCrossed {
id: Uuid,
watermark: f32,
window: String,
ts: DateTime<Utc>,
},
ResourcePressure {
id: Uuid,
gauge: String,
value: f64,
threshold: f64,
severity: String,
ts: DateTime<Utc>,
},
BreakerStateChange {
id: Uuid,
tool_id: String,
from: String,
to: String,
ts: DateTime<Utc>,
},
Error {
id: Uuid,
source: String,
message: String,
ts: DateTime<Utc>,
},
TerminalSessionOpened {
id: Uuid,
session_id: String,
program: String,
args: Vec<String>,
cwd: Option<String>,
principal: Option<PrincipalSummary>,
ts: DateTime<Utc>,
},
TerminalSessionClosed {
id: Uuid,
session_id: String,
exit_code: i32,
was_killed: bool,
principal: Option<PrincipalSummary>,
ts: DateTime<Utc>,
},
TaskStateChange {
id: Uuid,
task_id: String,
from: String,
to: String,
ts: DateTime<Utc>,
},
ServiceHealthChanged {
id: Uuid,
service: String,
target: String,
healthy: bool,
detail: String,
ts: DateTime<Utc>,
},
}
impl BrainEvent {
pub fn kind(&self) -> &'static str {
match self {
BrainEvent::SignalReceived { .. } => "signal_received",
BrainEvent::IntentClassified { .. } => "intent_classified",
BrainEvent::ToolRouteResolved { .. } => "tool_route_resolved",
BrainEvent::ConfirmationRequested { .. } => "confirmation_requested",
BrainEvent::ConfirmationResolved { .. } => "confirmation_resolved",
BrainEvent::ToolCallStarted { .. } => "tool_call_started",
BrainEvent::ToolCallFinished { .. } => "tool_call_finished",
BrainEvent::ReflexFired { .. } => "reflex_fired",
BrainEvent::AuditAppended { .. } => "audit_appended",
BrainEvent::BudgetCrossed { .. } => "budget_crossed",
BrainEvent::ResourcePressure { .. } => "resource_pressure",
BrainEvent::BreakerStateChange { .. } => "breaker_state_change",
BrainEvent::Error { .. } => "error",
BrainEvent::TerminalSessionOpened { .. } => "terminal_session_opened",
BrainEvent::TerminalSessionClosed { .. } => "terminal_session_closed",
BrainEvent::TaskStateChange { .. } => "task_state_change",
BrainEvent::ServiceHealthChanged { .. } => "service_health_changed",
}
}
pub fn id(&self) -> Uuid {
match self {
BrainEvent::SignalReceived { id, .. }
| BrainEvent::IntentClassified { id, .. }
| BrainEvent::ToolRouteResolved { id, .. }
| BrainEvent::ConfirmationRequested { id, .. }
| BrainEvent::ConfirmationResolved { id, .. }
| BrainEvent::ToolCallStarted { id, .. }
| BrainEvent::ToolCallFinished { id, .. }
| BrainEvent::ReflexFired { id, .. }
| BrainEvent::AuditAppended { id, .. }
| BrainEvent::BudgetCrossed { id, .. }
| BrainEvent::ResourcePressure { id, .. }
| BrainEvent::BreakerStateChange { id, .. }
| BrainEvent::Error { id, .. }
| BrainEvent::TerminalSessionOpened { id, .. }
| BrainEvent::TerminalSessionClosed { id, .. }
| BrainEvent::TaskStateChange { id, .. }
| BrainEvent::ServiceHealthChanged { id, .. } => *id,
}
}
pub fn tool_id(&self) -> Option<&str> {
match self {
BrainEvent::ToolCallStarted { tool_id, .. }
| BrainEvent::ToolCallFinished { tool_id, .. }
| BrainEvent::BreakerStateChange { tool_id, .. } => Some(tool_id.as_str()),
_ => None,
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SignalSummary {
pub source: String,
pub channel: String,
pub sender: String,
pub namespace: String,
pub content_preview: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct IntentSummary {
pub kind: String,
pub args_redacted: serde_json::Value,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ToolRouteSummary {
pub tool_id: String,
pub source: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct OutcomeSummary {
pub status: String,
pub error: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct PrincipalSummary {
pub user_id: String,
pub agent_id: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn kind_strings_are_snake_case() {
let ev = BrainEvent::Error {
id: Uuid::nil(),
source: "test".into(),
message: "m".into(),
ts: Utc::now(),
};
assert_eq!(ev.kind(), "error");
}
#[test]
fn roundtrip_tool_call_started_through_json() {
let id = Uuid::new_v4();
let ts = Utc::now();
let original = BrainEvent::ToolCallStarted {
id,
tool_id: "mcp:fs:read".into(),
args_redacted: serde_json::json!({"path": "/tmp/x"}),
ts,
};
let json = serde_json::to_string(&original).unwrap();
let decoded: BrainEvent = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.kind(), "tool_call_started");
assert_eq!(decoded.id(), id);
assert_eq!(decoded.tool_id(), Some("mcp:fs:read"));
}
#[test]
fn id_accessor_returns_per_variant_id() {
let id = Uuid::new_v4();
let ts = Utc::now();
let ev = BrainEvent::BudgetCrossed {
id,
watermark: 0.75,
window: "daily".into(),
ts,
};
assert_eq!(ev.id(), id);
assert_eq!(ev.tool_id(), None);
}
#[test]
fn roundtrip_resource_pressure_through_json() {
let id = Uuid::new_v4();
let ts = Utc::now();
let original = BrainEvent::ResourcePressure {
id,
gauge: "rss".into(),
value: 2304.0,
threshold: 2048.0,
severity: "warn".into(),
ts,
};
let json = serde_json::to_string(&original).unwrap();
let decoded: BrainEvent = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.kind(), "resource_pressure");
assert_eq!(decoded.id(), id);
assert_eq!(decoded.tool_id(), None);
match decoded {
BrainEvent::ResourcePressure {
gauge,
value,
threshold,
severity,
..
} => {
assert_eq!(gauge, "rss");
assert_eq!(value, 2304.0);
assert_eq!(threshold, 2048.0);
assert_eq!(severity, "warn");
}
other => panic!("decoded to the wrong variant: {other:?}"),
}
}
#[test]
fn roundtrip_service_health_changed_through_json() {
let id = Uuid::new_v4();
let ts = Utc::now();
let original = BrainEvent::ServiceHealthChanged {
id,
service: "ollama".into(),
target: "http://localhost:11434/api/tags".into(),
healthy: false,
detail: "connection refused".into(),
ts,
};
let json = serde_json::to_string(&original).unwrap();
let decoded: BrainEvent = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.kind(), "service_health_changed");
assert_eq!(decoded.id(), id);
assert_eq!(decoded.tool_id(), None);
match decoded {
BrainEvent::ServiceHealthChanged {
service,
target,
healthy,
detail,
..
} => {
assert_eq!(service, "ollama");
assert_eq!(target, "http://localhost:11434/api/tags");
assert!(!healthy);
assert_eq!(detail, "connection refused");
}
other => panic!("decoded to the wrong variant: {other:?}"),
}
}
}