Skip to main content

codewhale_protocol/
event_msg.rs

1//! `EventMsg`-out API in `crates/protocol` (issue #5261).
2//!
3//! Mirrors `crates/tui/src/core/events::Event` but as a serializable
4//! protocol. The TUI's `rx_event` / `Event` channel, the app-server's SSE
5//! stream, and the CLI's `stream-json` output all speak this one type so
6//! headless and TUI observe byte-identical event shapes for the same `Op`.
7
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10
11use crate::ids::{SessionId, ThreadId};
12
13/// One event emitted by the core engine to every consumer (TUI, CLI,
14/// app-server, tests). This is the `EventMsg`-out half of the `Op`-in /
15/// `EventMsg`-out contract. It is a straight projection of the existing
16/// internal `Event` variants (streaming deltas, tool lifecycle, turn
17/// lifecycle, approvals) plus the thread/session ids that `ThreadId` /
18/// `SessionId` now make explicit.
19#[derive(Debug, Clone, Serialize, Deserialize)]
20#[serde(tag = "event", rename_all = "snake_case")]
21pub enum EventMsg {
22    TurnStarted {
23        thread_id: ThreadId,
24        session_id: SessionId,
25        turn_id: String,
26    },
27    ResponseDelta {
28        thread_id: ThreadId,
29        session_id: SessionId,
30        delta: String,
31        #[serde(default)]
32        channel: String,
33    },
34    ToolCallStarted {
35        thread_id: ThreadId,
36        session_id: SessionId,
37        tool_call_id: String,
38        tool_name: String,
39        input: Value,
40    },
41    ToolCallComplete {
42        thread_id: ThreadId,
43        session_id: SessionId,
44        tool_call_id: String,
45        tool_name: String,
46        result: Value,
47    },
48    TurnComplete {
49        thread_id: ThreadId,
50        session_id: SessionId,
51        turn_id: String,
52        status: String,
53        #[serde(skip_serializing_if = "Option::is_none")]
54        error: Option<String>,
55    },
56    TurnUsage {
57        thread_id: ThreadId,
58        session_id: SessionId,
59        input_tokens: u32,
60        output_tokens: u32,
61    },
62    CompactionStarted {
63        thread_id: ThreadId,
64        session_id: SessionId,
65        message: String,
66    },
67    CompactionCompleted {
68        thread_id: ThreadId,
69        session_id: SessionId,
70        message: String,
71    },
72    Error {
73        thread_id: ThreadId,
74        session_id: SessionId,
75        message: String,
76    },
77}
78
79/// Envelope that carries an `EventMsg` over the wire / channel with a
80/// monotonic seq so consumers can detect drops. Mirrors the existing
81/// `RuntimeEventEnvelope` but typed to `EventMsg`.
82#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct EventEnvelope {
84    pub seq: u64,
85    pub thread_id: ThreadId,
86    pub session_id: SessionId,
87    pub turn_id: Option<String>,
88    pub event: EventMsg,
89}
90
91impl EventMsg {
92    #[must_use]
93    pub fn kind_str(&self) -> &'static str {
94        match self {
95            Self::TurnStarted { .. } => "turn_started",
96            Self::ResponseDelta { .. } => "response_delta",
97            Self::ToolCallStarted { .. } => "tool_call_started",
98            Self::ToolCallComplete { .. } => "tool_call_complete",
99            Self::TurnComplete { .. } => "turn_complete",
100            Self::TurnUsage { .. } => "turn_usage",
101            Self::CompactionStarted { .. } => "compaction_started",
102            Self::CompactionCompleted { .. } => "compaction_completed",
103            Self::Error { .. } => "error",
104        }
105    }
106
107    #[must_use]
108    pub fn thread_id(&self) -> &ThreadId {
109        match self {
110            Self::TurnStarted { thread_id, .. }
111            | Self::ResponseDelta { thread_id, .. }
112            | Self::ToolCallStarted { thread_id, .. }
113            | Self::ToolCallComplete { thread_id, .. }
114            | Self::TurnComplete { thread_id, .. }
115            | Self::TurnUsage { thread_id, .. }
116            | Self::CompactionStarted { thread_id, .. }
117            | Self::CompactionCompleted { thread_id, .. }
118            | Self::Error { thread_id, .. } => thread_id,
119        }
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn event_msg_roundtrip() {
129        let msg = EventMsg::TurnComplete {
130            thread_id: ThreadId::new(),
131            session_id: SessionId::new(),
132            turn_id: "turn-1".into(),
133            status: "completed".into(),
134            error: None,
135        };
136        let json = serde_json::to_string(&msg).unwrap();
137        let back: EventMsg = serde_json::from_str(&json).unwrap();
138        assert_eq!(back.kind_str(), "turn_complete");
139    }
140}