1use serde::{Deserialize, Serialize};
11use std::sync::atomic::{AtomicU32, Ordering};
12
13pub const SENTINEL: &str = "@@RZ:";
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct Envelope {
17 pub id: String,
18 pub from: String,
19 #[serde(skip_serializing_if = "Option::is_none")]
20 pub to: Option<String>,
21 #[serde(skip_serializing_if = "Option::is_none")]
22 pub r#ref: Option<String>,
23 pub kind: MessageKind,
24 pub ts: u64,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
28#[serde(tag = "kind", content = "body", rename_all = "snake_case")]
29pub enum MessageKind {
30 Chat { text: String },
31 Ping,
32 Pong,
33 Error { message: String },
34 Timer { label: String },
35 Status { state: String, detail: String },
36 ToolCall { name: String, input: String },
37 ToolResult { name: String, result: String, is_error: bool },
38 Delegate { task: String, to: Option<String> },
39 Hello { name: String },
40}
41
42static COUNTER: AtomicU32 = AtomicU32::new(0);
43
44impl Envelope {
45 pub fn chat(from: impl Into<String>, text: impl Into<String>) -> Self {
46 Self::new(from, MessageKind::Chat { text: text.into() })
47 }
48
49 pub fn new(from: impl Into<String>, kind: MessageKind) -> Self {
50 let seq = COUNTER.fetch_add(1, Ordering::Relaxed);
51 let ts = std::time::SystemTime::now()
52 .duration_since(std::time::UNIX_EPOCH)
53 .unwrap_or_default()
54 .as_millis() as u64;
55 Self {
56 id: format!("{:04x}{:04x}", (ts & 0xFFFF) as u16, seq),
57 to: None,
58 r#ref: None,
59 from: from.into(),
60 kind,
61 ts,
62 }
63 }
64
65 pub fn with_to(mut self, t: impl Into<String>) -> Self {
67 self.to = Some(t.into());
68 self
69 }
70
71 pub fn with_ref(mut self, r: impl Into<String>) -> Self {
73 self.r#ref = Some(r.into());
74 self
75 }
76
77 pub fn maybe_with_ref(mut self, r: Option<String>) -> Self {
79 self.r#ref = r;
80 self
81 }
82
83 pub fn encode(&self) -> eyre::Result<String> {
85 let json = serde_json::to_string(self)?;
86 Ok(format!("{SENTINEL}{json}"))
87 }
88
89 pub fn decode(line: &str) -> eyre::Result<Self> {
91 let payload = line.strip_prefix(SENTINEL).unwrap_or(line);
92 Ok(serde_json::from_str(payload.trim())?)
93 }
94}