1use serde::{Deserialize, Serialize};
2
3use crate::error::ErrorCode;
4use crate::types::{CharacterInfo, Message, StreamMetadata};
5
6#[derive(Serialize, Deserialize, Debug, Clone)]
8pub struct ServerHello {
9 pub v: u32,
10 pub server_name: String,
11 #[serde(default)]
12 pub characters: Vec<CharacterInfo>,
13}
14
15#[derive(Serialize, Deserialize, Debug, Clone)]
17pub struct History {
18 #[serde(default, skip_serializing_if = "Option::is_none")]
19 pub rid: Option<String>,
20 pub messages: Vec<Message>,
21 #[serde(default, skip_serializing_if = "is_zero")]
28 pub active_start: usize,
29 #[serde(default)]
30 pub config: serde_json::Value,
31 #[serde(default, skip_serializing_if = "Option::is_none")]
32 pub selected_character: Option<String>,
33 #[serde(default)]
34 pub revision: u64,
35}
36
37#[expect(
38 clippy::trivially_copy_pass_by_ref,
39 reason = "serde skip_serializing_if requires a &T predicate signature"
40)]
41fn is_zero(value: &usize) -> bool {
42 *value == 0
43}
44
45#[derive(Serialize, Deserialize, Debug, Clone)]
47pub struct Shutdown {}
48
49#[derive(Serialize, Deserialize, Debug, Clone)]
51pub struct Ping {}
52
53#[derive(Serialize, Deserialize, Debug, Clone)]
55pub struct CommandOutput {
56 #[serde(default, skip_serializing_if = "Option::is_none")]
57 pub rid: Option<String>,
58 pub name: String,
59 pub data: serde_json::Value,
60}
61
62#[derive(Serialize, Deserialize, Debug, Clone)]
64pub struct Error {
65 #[serde(default, skip_serializing_if = "Option::is_none")]
66 pub rid: Option<String>,
67 pub code: ErrorCode,
68 pub message: String,
69}
70
71#[derive(Serialize, Deserialize, Debug, Clone)]
73pub struct StreamStart {
74 #[serde(default, skip_serializing_if = "Option::is_none")]
75 pub rid: Option<String>,
76 #[serde(default)]
77 pub regen: bool,
78 #[serde(default, skip_serializing_if = "Option::is_none")]
82 pub subagent: Option<String>,
83}
84
85#[derive(Serialize, Deserialize, Debug, Clone)]
87pub struct StreamChunk {
88 #[serde(default, skip_serializing_if = "Option::is_none")]
89 pub rid: Option<String>,
90 pub text: String,
91 #[serde(default = "default_content_type")]
92 pub content_type: String,
93 #[serde(default, skip_serializing_if = "Option::is_none")]
96 pub subagent: Option<String>,
97}
98
99fn default_content_type() -> String {
100 "text".to_owned()
101}
102
103#[derive(Serialize, Deserialize, Debug, Clone)]
113pub struct StreamEnd {
114 #[serde(default, skip_serializing_if = "Option::is_none")]
115 pub rid: Option<String>,
116 #[serde(default, skip_serializing_if = "Option::is_none")]
121 pub msg_id: Option<String>,
122 #[serde(default, skip_serializing_if = "Option::is_none")]
124 pub revision: Option<u64>,
125 pub content: String,
126 pub metadata: StreamMetadata,
127 #[serde(default, skip_serializing_if = "String::is_empty")]
129 pub finish_reason: String,
130 #[serde(default = "default_true")]
135 pub is_final: bool,
136 #[serde(default, skip_serializing_if = "Option::is_none")]
141 pub subagent: Option<String>,
142}
143
144fn default_true() -> bool {
145 true
146}
147
148#[derive(Serialize, Deserialize, Debug, Clone)]
150pub struct Phase {
151 #[serde(default, skip_serializing_if = "Option::is_none")]
152 pub rid: Option<String>,
153 pub phase: String,
154 #[serde(skip_serializing_if = "Option::is_none")]
155 pub model: Option<String>,
156}
157
158pub use crate::types::MessageOrigin;
161
162#[derive(Serialize, Deserialize, Debug, Clone)]
169pub struct NewMessage {
170 #[serde(default)]
171 pub revision: u64,
172 #[serde(default, skip_serializing_if = "Option::is_none")]
173 pub character: Option<String>,
174 #[serde(flatten)]
175 pub message: Message,
176}
177
178#[derive(Serialize, Deserialize, Debug, Clone)]
180pub struct ToolCall {
181 #[serde(default, skip_serializing_if = "Option::is_none")]
182 pub rid: Option<String>,
183 pub tool_id: String,
184 pub tool_name: String,
185 pub input: serde_json::Value,
186 #[serde(default, skip_serializing_if = "Option::is_none")]
189 pub subagent: Option<String>,
190}
191
192#[derive(Serialize, Deserialize, Debug, Clone)]
194pub struct ToolResult {
195 #[serde(default, skip_serializing_if = "Option::is_none")]
196 pub rid: Option<String>,
197 pub tool_id: String,
198 pub tool_name: String,
199 pub output: String,
200 #[serde(default)]
201 pub is_error: bool,
202 #[serde(default, skip_serializing_if = "Option::is_none")]
205 pub subagent: Option<String>,
206}
207
208#[derive(Serialize, Deserialize, Debug, Clone)]
210pub struct SendImage {
211 #[serde(default, skip_serializing_if = "Option::is_none")]
212 pub rid: Option<String>,
213 pub path: String,
214 #[serde(skip_serializing_if = "Option::is_none")]
215 pub caption: Option<String>,
216 #[serde(default, skip_serializing_if = "Option::is_none")]
218 pub data: Option<String>,
219 #[serde(default, skip_serializing_if = "Option::is_none")]
222 pub subagent: Option<String>,
223}
224
225#[derive(Serialize, Deserialize, Debug, Clone)]
227pub struct CacheWarning {
228 pub expected_tokens: u32,
229 pub message: String,
230}
231
232#[derive(Serialize, Deserialize, Debug, Clone)]
241pub struct ProviderFallbackWarning {
242 #[serde(default, skip_serializing_if = "Option::is_none")]
243 pub rid: Option<String>,
244 pub provider: String,
246 pub from_key: String,
248 pub to_key: String,
250 pub kind: String,
253 #[serde(default, skip_serializing_if = "Option::is_none")]
256 pub status: Option<u16>,
257 pub message: String,
259}
260
261#[derive(Serialize, Deserialize, Debug, Clone)]
263pub struct UsageWarning {
264 #[serde(default, skip_serializing_if = "Option::is_none")]
265 pub rid: Option<String>,
266 pub budget: String,
268 pub message: String,
270 pub current_cost: f64,
272 pub cost_limit: f64,
274 pub percent_used: f64,
276 pub crossed_warn_at: Vec<f64>,
278 pub period: String,
280 pub period_start: String,
282 pub reset_at: String,
284 #[serde(default)]
288 pub reset_at_display: String,
289}
290
291#[derive(Serialize, Deserialize, Debug, Clone)]
293#[serde(tag = "type", rename_all = "snake_case")]
294pub enum ServerMessage {
295 Hello(ServerHello),
296 History(History),
297 Shutdown(Shutdown),
298 Ping(Ping),
299 CommandOutput(CommandOutput),
300 Error(Error),
301 StreamStart(StreamStart),
302 StreamChunk(StreamChunk),
303 StreamEnd(StreamEnd),
304 Phase(Phase),
305 NewMessage(NewMessage),
306 ToolCall(ToolCall),
307 ToolResult(ToolResult),
308 SendImage(SendImage),
309 CacheWarning(CacheWarning),
310 ProviderFallbackWarning(ProviderFallbackWarning),
311 UsageWarning(UsageWarning),
312 #[serde(other)]
318 Unknown,
319}
320
321impl ServerMessage {
322 #[must_use]
326 pub fn with_rid(mut self, rid: Option<String>) -> Self {
327 match &mut self {
330 ServerMessage::History(msg) => msg.rid = rid,
331 ServerMessage::CommandOutput(msg) => msg.rid = rid,
332 ServerMessage::Error(msg) => msg.rid = rid,
333 ServerMessage::StreamStart(msg) => msg.rid = rid,
334 ServerMessage::StreamChunk(msg) => msg.rid = rid,
335 ServerMessage::StreamEnd(msg) => msg.rid = rid,
336 ServerMessage::Phase(msg) => msg.rid = rid,
337 ServerMessage::ToolCall(msg) => msg.rid = rid,
338 ServerMessage::ToolResult(msg) => msg.rid = rid,
339 ServerMessage::SendImage(msg) => msg.rid = rid,
340 ServerMessage::ProviderFallbackWarning(msg) => msg.rid = rid,
341 ServerMessage::UsageWarning(msg) => msg.rid = rid,
342 ServerMessage::Hello(_)
343 | ServerMessage::Shutdown(_)
344 | ServerMessage::Ping(_)
345 | ServerMessage::NewMessage(_)
346 | ServerMessage::CacheWarning(_)
347 | ServerMessage::Unknown => {}
348 }
349 self
350 }
351
352 #[must_use]
356 pub fn subagent(&self) -> Option<&str> {
357 match self {
358 ServerMessage::StreamStart(m) => m.subagent.as_deref(),
359 ServerMessage::StreamChunk(m) => m.subagent.as_deref(),
360 ServerMessage::StreamEnd(m) => m.subagent.as_deref(),
361 ServerMessage::ToolCall(m) => m.subagent.as_deref(),
362 ServerMessage::ToolResult(m) => m.subagent.as_deref(),
363 ServerMessage::SendImage(m) => m.subagent.as_deref(),
364 ServerMessage::Hello(_)
365 | ServerMessage::History(_)
366 | ServerMessage::Shutdown(_)
367 | ServerMessage::Ping(_)
368 | ServerMessage::CommandOutput(_)
369 | ServerMessage::Error(_)
370 | ServerMessage::Phase(_)
371 | ServerMessage::NewMessage(_)
372 | ServerMessage::CacheWarning(_)
373 | ServerMessage::ProviderFallbackWarning(_)
374 | ServerMessage::UsageWarning(_)
375 | ServerMessage::Unknown => None,
376 }
377 }
378
379 pub fn set_subagent(&mut self, name: &str) {
385 let tag = || Some(name.to_owned());
386 match self {
387 ServerMessage::StreamStart(msg) => msg.subagent = tag(),
388 ServerMessage::StreamChunk(msg) => msg.subagent = tag(),
389 ServerMessage::StreamEnd(msg) => msg.subagent = tag(),
390 ServerMessage::ToolCall(msg) => msg.subagent = tag(),
391 ServerMessage::ToolResult(msg) => msg.subagent = tag(),
392 ServerMessage::SendImage(msg) => msg.subagent = tag(),
393 ServerMessage::Hello(_)
394 | ServerMessage::History(_)
395 | ServerMessage::Shutdown(_)
396 | ServerMessage::Ping(_)
397 | ServerMessage::CommandOutput(_)
398 | ServerMessage::Error(_)
399 | ServerMessage::Phase(_)
400 | ServerMessage::NewMessage(_)
401 | ServerMessage::CacheWarning(_)
402 | ServerMessage::ProviderFallbackWarning(_)
403 | ServerMessage::UsageWarning(_)
404 | ServerMessage::Unknown => {}
405 }
406 }
407}
408
409#[cfg(test)]
410mod tests {
411 use super::*;
412
413 #[test]
414 fn unknown_type_deserializes_to_unknown_variant() {
415 let json = r#"{"type":"some_future_message","field":42}"#;
419 let msg: ServerMessage = serde_json::from_str(json).expect("must not error");
420 assert!(matches!(msg, ServerMessage::Unknown));
421 }
422
423 #[test]
424 fn known_types_still_deserialize_to_their_variant() {
425 let json = r#"{"type":"ping"}"#;
427 let msg: ServerMessage = serde_json::from_str(json).expect("ping must parse");
428 assert!(matches!(msg, ServerMessage::Ping(_)));
429 }
430
431 #[test]
432 fn set_subagent_tags_stream_and_tool_frames() {
433 let mut chunk = ServerMessage::StreamChunk(StreamChunk {
434 rid: None,
435 text: "hi".into(),
436 content_type: "text".into(),
437 subagent: None,
438 });
439 chunk.set_subagent("research");
440 assert_eq!(chunk.subagent(), Some("research"));
441
442 let mut phase = ServerMessage::Phase(Phase {
444 rid: None,
445 phase: "thinking".into(),
446 model: None,
447 });
448 phase.set_subagent("research");
449 assert_eq!(phase.subagent(), None);
450 }
451
452 #[test]
453 fn subagent_tag_survives_wire_round_trip() {
454 let mut call = ServerMessage::ToolCall(ToolCall {
455 rid: None,
456 tool_id: "t1".into(),
457 tool_name: "search".into(),
458 input: serde_json::json!({}),
459 subagent: None,
460 });
461 call.set_subagent("research");
462 let wire = serde_json::to_string(&call).unwrap();
463 assert!(wire.contains("\"subagent\":\"research\""), "wire: {wire}");
464 let back: ServerMessage = serde_json::from_str(&wire).unwrap();
465 assert_eq!(back.subagent(), Some("research"));
466 }
467
468 #[test]
469 fn untagged_frame_omits_subagent_on_the_wire() {
470 let call = ServerMessage::ToolCall(ToolCall {
473 rid: None,
474 tool_id: "t1".into(),
475 tool_name: "search".into(),
476 input: serde_json::json!({}),
477 subagent: None,
478 });
479 let wire = serde_json::to_string(&call).unwrap();
480 assert!(!wire.contains("subagent"), "wire: {wire}");
481 }
482}