1use serde::{Deserialize, Serialize};
13
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
18#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export_to = "../../../frontend/types/rpc/"))]
19#[serde(tag = "op", rename_all = "lowercase")]
20pub enum ToolChunk {
21 Chunk {
23 tool_id: String,
24 kind: ToolChunkKind,
25 content: String,
26 #[cfg_attr(feature = "ts", ts(type = "number"))]
27 timestamp: u64,
28 },
29 Terminal {
31 tool_id: String,
32 exit_code: i32,
33 #[cfg_attr(feature = "ts", ts(type = "number"))]
34 timestamp: u64,
35 },
36 Pid {
39 tool_id: String,
40 pid: u32,
41 #[cfg_attr(feature = "ts", ts(type = "number"))]
42 timestamp: u64,
43 },
44 Call {
47 tool_id: String,
48 pid: u32,
49 description: String,
50 #[cfg_attr(feature = "ts", ts(type = "number"))]
51 timestamp: u64,
52 },
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
57#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export_to = "../../../frontend/types/rpc/"))]
58#[serde(rename_all = "lowercase")]
59pub enum ToolChunkKind {
60 Stdout,
61 Stderr,
62 System,
64}
65
66#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
69#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export_to = "../../../frontend/types/rpc/"))]
70#[serde(rename_all = "camelCase")]
71pub struct CompactionStarted {
72 pub trigger: CompactionTrigger,
73 pub session_id: String,
74 pub started_at: String,
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
82#[cfg_attr(feature = "ts", derive(ts_rs::TS), ts(export_to = "../../../frontend/types/rpc/"))]
83#[serde(rename_all = "snake_case")]
84pub enum CompactionTrigger {
85 Auto,
86 Manual,
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92 use serde_json::json;
93
94 #[test]
95 fn tool_chunks_serialize_as_the_wrapper_always_sent_them() {
96 let chunk = ToolChunk::Chunk { tool_id: "toolu_1".into(), kind: ToolChunkKind::Stderr, content: "x\n".into(), timestamp: 5 };
97 assert_eq!(serde_json::to_value(&chunk).unwrap(), json!({"op": "chunk", "tool_id": "toolu_1", "kind": "stderr", "content": "x\n", "timestamp": 5}));
98 let terminal = ToolChunk::Terminal { tool_id: "toolu_1".into(), exit_code: -1, timestamp: 6 };
99 assert_eq!(serde_json::to_value(&terminal).unwrap(), json!({"op": "terminal", "tool_id": "toolu_1", "exit_code": -1, "timestamp": 6}));
100 let pid = ToolChunk::Pid { tool_id: "toolu_1".into(), pid: 42, timestamp: 7 };
101 assert_eq!(serde_json::to_value(&pid).unwrap(), json!({"op": "pid", "tool_id": "toolu_1", "pid": 42, "timestamp": 7}));
102 let call = ToolChunk::Call { tool_id: "toolu_1".into(), pid: 42, description: "Run it".into(), timestamp: 8 };
103 assert_eq!(serde_json::to_value(&call).unwrap(), json!({"op": "call", "tool_id": "toolu_1", "pid": 42, "description": "Run it", "timestamp": 8}));
104 for v in [chunk, terminal, pid, call] {
105 assert_eq!(serde_json::from_value::<ToolChunk>(serde_json::to_value(&v).unwrap()).unwrap(), v);
106 }
107 }
108
109 #[test]
110 fn compaction_started_serializes_camel_case() {
111 let p = CompactionStarted { trigger: CompactionTrigger::Auto, session_id: "s1".into(), started_at: "2026-10-13T00:00:00Z".into() };
112 assert_eq!(serde_json::to_value(&p).unwrap(), json!({"trigger": "auto", "sessionId": "s1", "startedAt": "2026-10-13T00:00:00Z"}));
113 }
114
115 #[test]
116 fn an_unknown_op_or_kind_is_refused() {
117 assert!(serde_json::from_value::<ToolChunk>(json!({"op": "nope", "tool_id": "t"})).is_err());
118 assert!(serde_json::from_value::<ToolChunk>(json!({"op": "chunk", "tool_id": "t", "kind": "tty", "content": "", "timestamp": 0})).is_err());
119 assert!(serde_json::from_value::<CompactionStarted>(json!({"trigger": "sometimes", "sessionId": "", "startedAt": ""})).is_err());
120 }
121}