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