1use std::io::{self, Write};
2
3use serde::Serialize;
4use serde_json::Value;
5
6#[derive(Debug, Serialize, Clone, PartialEq, Eq)]
7#[serde(tag = "type")]
8pub enum ProtocolEvent {
9 #[serde(rename = "session")]
10 Session { session_id: String, resumed: bool },
11 #[serde(rename = "assistant_delta")]
12 AssistantDelta { text: String },
13 #[serde(rename = "tool_call")]
14 ToolCall {
15 id: String,
16 name: String,
17 arguments: String,
18 },
19 #[serde(rename = "tool_result")]
20 ToolResult {
21 id: String,
22 name: String,
23 result: Value,
24 },
25 #[serde(rename = "background_result_pending")]
26 BackgroundResultPending {
27 completion_id: String,
28 task_id: String,
29 child_session_id: String,
30 status: String,
31 result: Value,
32 completed_at: u64,
33 },
34 #[serde(rename = "background_result_delivered")]
35 BackgroundResultDelivered {
36 completion_id: String,
37 task_id: String,
38 logical_turn_id: String,
39 delivery: String,
40 },
41 #[serde(rename = "turn_end")]
42 TurnEnd,
43 #[serde(rename = "turn_interrupted")]
44 TurnInterrupted { reason: String, phase: String },
45 #[serde(rename = "error")]
46 Error { message: String },
47}
48
49pub trait EventSink {
51 fn emit_event(&mut self, event: &ProtocolEvent) -> io::Result<()>;
52
53 fn reasoning_started(&mut self) -> io::Result<()> {
56 Ok(())
57 }
58
59 fn reasoning_completed(&mut self) -> io::Result<()> {
62 Ok(())
63 }
64
65 fn skill_instruction_attached(&mut self, _name: &str) -> io::Result<()> {
69 Ok(())
70 }
71
72 fn context_usage(&mut self, _tokens: usize) -> io::Result<()> {
75 Ok(())
76 }
77
78 fn compaction_started(&mut self) -> io::Result<()> {
81 Ok(())
82 }
83
84 fn compaction_finished(
87 &mut self,
88 _tokens_before: usize,
89 _tokens_after: usize,
90 ) -> io::Result<()> {
91 Ok(())
92 }
93}
94
95pub struct ProtocolWriter<W> {
96 writer: W,
97}
98
99impl<W: Write> ProtocolWriter<W> {
100 pub fn new(writer: W) -> Self {
101 Self { writer }
102 }
103
104 pub fn emit(&mut self, event: &ProtocolEvent) -> io::Result<()> {
105 self.emit_serializable(event)
106 }
107
108 pub fn emit_serializable<T: Serialize>(&mut self, record: &T) -> io::Result<()> {
109 serde_json::to_writer(&mut self.writer, record)
110 .map_err(|error| io::Error::other(format!("encode protocol event: {error}")))?;
111 self.writer.write_all(b"\n")?;
112 self.writer.flush()
113 }
114
115 pub fn session(&mut self, session_id: &str, resumed: bool) -> io::Result<()> {
116 self.emit(&ProtocolEvent::Session {
117 session_id: session_id.to_owned(),
118 resumed,
119 })
120 }
121
122 pub fn assistant_delta(&mut self, text: &str) -> io::Result<()> {
123 if text.is_empty() {
124 return Ok(());
125 }
126 self.emit(&ProtocolEvent::AssistantDelta {
127 text: text.to_owned(),
128 })
129 }
130
131 pub fn tool_call(&mut self, id: &str, name: &str, arguments: &str) -> io::Result<()> {
132 self.emit(&ProtocolEvent::ToolCall {
133 id: id.to_owned(),
134 name: name.to_owned(),
135 arguments: arguments.to_owned(),
136 })
137 }
138
139 pub fn tool_result(&mut self, id: &str, name: &str, result: Value) -> io::Result<()> {
140 self.emit(&ProtocolEvent::ToolResult {
141 id: id.to_owned(),
142 name: name.to_owned(),
143 result,
144 })
145 }
146
147 pub fn turn_end(&mut self) -> io::Result<()> {
148 self.emit(&ProtocolEvent::TurnEnd)
149 }
150
151 pub fn turn_interrupted(&mut self, reason: &str, phase: &str) -> io::Result<()> {
152 self.emit(&ProtocolEvent::TurnInterrupted {
153 reason: reason.to_owned(),
154 phase: phase.to_owned(),
155 })
156 }
157
158 pub fn error(&mut self, message: &str) -> io::Result<()> {
159 self.emit(&ProtocolEvent::Error {
160 message: message.to_owned(),
161 })
162 }
163}
164
165impl<W: Write> EventSink for ProtocolWriter<W> {
166 fn emit_event(&mut self, event: &ProtocolEvent) -> io::Result<()> {
167 self.emit(event)
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 #[test]
176 fn protocol_writer_emits_only_single_line_json_records() {
177 let mut output = Vec::new();
178 {
179 let mut writer = ProtocolWriter::new(&mut output);
180 writer.assistant_delta("line one\nline two").expect("event");
181 writer
182 .tool_result(
183 "call-1",
184 "cmd",
185 serde_json::json!({"stdout":"provider-shape-is-not-forwarded"}),
186 )
187 .expect("result");
188 }
189 let text = String::from_utf8(output).expect("UTF-8");
190 assert_eq!(text.lines().count(), 2);
191 for line in text.lines() {
192 serde_json::from_str::<Value>(line).expect("JSONL record");
193 }
194 assert!(!text.contains("choices"));
195 }
196
197 #[test]
198 fn compaction_frontend_state_is_not_emitted_to_jsonl() {
199 let mut output = Vec::new();
200 let mut writer = ProtocolWriter::new(&mut output);
201 writer.context_usage(100).expect("context usage");
202 writer.compaction_started().expect("compaction start");
203 writer
204 .compaction_finished(100, 20)
205 .expect("compaction finish");
206
207 assert!(output.is_empty());
208 }
209
210 #[test]
211 fn skill_attachment_state_is_not_emitted_to_jsonl() {
212 let mut output = Vec::new();
213 let mut writer = ProtocolWriter::new(&mut output);
214 writer
215 .skill_instruction_attached("release-notes")
216 .expect("non-public TUI state");
217 assert!(output.is_empty());
218 }
219
220 #[test]
221 fn interruption_event_is_a_normalized_json_record() {
222 let event = ProtocolEvent::TurnInterrupted {
223 reason: "user_cancelled".to_owned(),
224 phase: "provider_stream".to_owned(),
225 };
226 let value = serde_json::to_value(event).expect("event JSON");
227 assert_eq!(value["type"], "turn_interrupted");
228 assert_eq!(value["reason"], "user_cancelled");
229 assert_eq!(value["phase"], "provider_stream");
230 assert!(!serde_json::to_string(&value)
231 .expect("serialized event")
232 .contains("choices"));
233 }
234}