wactorz_mqtt/
topics.rs

1//! Well-known MQTT topic constants and builder helpers.
2//!
3//! All AgentFlow topics follow one of two patterns:
4//! - `agents/{agent_id}/{event}` — per-actor events
5//! - `system/{event}` — system-wide broadcasts
6//!
7//! Use the builder functions to avoid string formatting errors in call sites.
8
9/// Subscribe to all agent events (wildcard).
10pub const AGENTS_ALL: &str = "agents/#";
11
12/// Subscribe to all node events (wildcard).
13pub const NODES_ALL: &str = "nodes/#";
14
15/// Node heartbeat topic pattern — `nodes/{name}/heartbeat`.
16pub const NODES_HEARTBEAT: &str = "nodes/heartbeat";
17
18/// System-wide health topic.
19pub const SYSTEM_HEALTH: &str = "system/health";
20
21/// System-wide shutdown topic.
22pub const SYSTEM_SHUTDOWN: &str = "system/shutdown";
23
24/// LLM provider error broadcast (published by LlmAgent / MainActor on API failure).
25/// Payload: `{ provider, model, error, consecutiveErrors, timestampMs }`
26pub const SYSTEM_LLM_ERROR: &str = "system/llm/error";
27
28/// LLM provider switch command (published by WIK agent to trigger hot-swap).
29/// Payload: `{ provider, model, apiKey?, baseUrl?, reason }`
30pub const SYSTEM_LLM_SWITCH: &str = "system/llm/switch";
31
32// ── Per-agent topic builders ──────────────────────────────────────────────────
33
34/// `agents/{id}/heartbeat`
35pub fn heartbeat(agent_id: &str) -> String {
36    format!("agents/{agent_id}/heartbeat")
37}
38
39/// `agents/{id}/status`
40pub fn status(agent_id: &str) -> String {
41    format!("agents/{agent_id}/status")
42}
43
44/// `agents/{id}/logs`
45pub fn logs(agent_id: &str) -> String {
46    format!("agents/{agent_id}/logs")
47}
48
49/// `agents/{id}/alert`
50pub fn alert(agent_id: &str) -> String {
51    format!("agents/{agent_id}/alert")
52}
53
54/// `agents/{id}/commands` — topic on which the agent listens for commands.
55pub fn commands(agent_id: &str) -> String {
56    format!("agents/{agent_id}/commands")
57}
58
59/// `agents/{id}/result` — agent publishes task results here.
60pub fn result(agent_id: &str) -> String {
61    format!("agents/{agent_id}/result")
62}
63
64/// `agents/{id}/detections` — ML/monitoring agents publish detections here.
65pub fn detections(agent_id: &str) -> String {
66    format!("agents/{agent_id}/detections")
67}
68
69/// `agents/{id}/chat` — direct chat messages to/from an agent.
70pub fn chat(agent_id: &str) -> String {
71    format!("agents/{agent_id}/chat")
72}
73
74/// `agents/{id}/spawn` — agent announces its presence on startup.
75pub fn spawn(agent_id: &str) -> String {
76    format!("agents/{agent_id}/spawn")
77}
78
79/// `io/chat` — inbound messages from the UI gateway.
80pub const IO_CHAT: &str = "io/chat";
81
82// ── Parsing helpers ───────────────────────────────────────────────────────────
83
84/// Extract `(agent_id, event)` from an `agents/{id}/{event}` topic.
85///
86/// Returns `None` if the topic does not match the expected pattern.
87pub fn parse_agent_topic(topic: &str) -> Option<(&str, &str)> {
88    let parts: Vec<&str> = topic.splitn(3, '/').collect();
89    match parts.as_slice() {
90        ["agents", id, event] => Some((id, event)),
91        _ => None,
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    #[test]
100    fn topic_builders_are_correct() {
101        assert_eq!(heartbeat("abc"), "agents/abc/heartbeat");
102        assert_eq!(commands("xyz"), "agents/xyz/commands");
103    }
104
105    #[test]
106    fn parse_valid_agent_topic() {
107        assert_eq!(
108            parse_agent_topic("agents/abc-123/heartbeat"),
109            Some(("abc-123", "heartbeat"))
110        );
111    }
112
113    #[test]
114    fn parse_invalid_topic_returns_none() {
115        assert_eq!(parse_agent_topic("system/health"), None);
116        assert_eq!(parse_agent_topic("agents/only-two"), None);
117    }
118}