1use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6pub const PROTOCOL_VERSION: u32 = 1;
7
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
9pub struct PeerInfo {
10 pub name: String,
11 pub version: String,
12}
13
14#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
15pub struct Usage {
16 #[serde(skip_serializing_if = "Option::is_none")]
17 pub input_tokens: Option<u64>,
18 #[serde(skip_serializing_if = "Option::is_none")]
19 pub output_tokens: Option<u64>,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
23#[serde(tag = "type")]
24pub enum ClientMessage {
25 #[serde(rename = "initialize")]
26 Initialize {
27 request_id: String,
28 protocol_version: u32,
29 client: PeerInfo,
30 },
31 #[serde(rename = "session.start")]
32 SessionStart { request_id: String, cwd: String },
33 #[serde(rename = "turn.start")]
34 TurnStart {
35 request_id: String,
36 session_id: String,
37 prompt: String,
38 },
39 #[serde(rename = "turn.cancel")]
40 TurnCancel {
41 request_id: String,
42 session_id: String,
43 turn_id: String,
44 },
45 #[serde(rename = "approval.resolve")]
46 ApprovalResolve {
47 request_id: String,
48 session_id: String,
49 approval_id: String,
50 approved: bool,
51 },
52 #[serde(rename = "session.clear")]
53 SessionClear {
54 request_id: String,
55 session_id: String,
56 },
57}
58
59impl ClientMessage {
60 pub fn request_id(&self) -> &str {
61 match self {
62 Self::Initialize { request_id, .. }
63 | Self::SessionStart { request_id, .. }
64 | Self::TurnStart { request_id, .. }
65 | Self::TurnCancel { request_id, .. }
66 | Self::ApprovalResolve { request_id, .. }
67 | Self::SessionClear { request_id, .. } => request_id,
68 }
69 }
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
73#[serde(tag = "type")]
74pub enum ServerEvent {
75 #[serde(rename = "initialized")]
76 Initialized {
77 request_id: String,
78 protocol_version: u32,
79 server: PeerInfo,
80 },
81 #[serde(rename = "session.started")]
82 SessionStarted {
83 request_id: String,
84 session_id: String,
85 cwd: String,
86 model: String,
87 context_max_tokens: usize,
88 max_server_frame_bytes: usize,
89 max_transcript_bytes: usize,
90 max_transcript_items: usize,
91 max_prompt_history_bytes: usize,
92 max_prompt_history_items: usize,
93 },
94 #[serde(rename = "turn.started")]
95 TurnStarted {
96 request_id: String,
97 session_id: String,
98 turn_id: String,
99 seq: u64,
100 },
101 #[serde(rename = "assistant.delta")]
102 AssistantDelta {
103 request_id: String,
104 session_id: String,
105 turn_id: String,
106 seq: u64,
107 content: String,
108 },
109 #[serde(rename = "assistant.completed")]
110 AssistantCompleted {
111 request_id: String,
112 session_id: String,
113 turn_id: String,
114 seq: u64,
115 content: String,
116 },
117 #[serde(rename = "tool.proposed")]
118 ToolProposed {
119 request_id: String,
120 session_id: String,
121 turn_id: String,
122 seq: u64,
123 call_id: String,
124 name: String,
125 arguments: Value,
126 },
127 #[serde(rename = "approval.requested")]
128 ApprovalRequested {
129 request_id: String,
130 session_id: String,
131 turn_id: String,
132 seq: u64,
133 approval_id: String,
134 call_id: String,
135 name: String,
136 risk: String,
137 cwd: String,
138 summary: String,
139 },
140 #[serde(rename = "tool.started")]
141 ToolStarted {
142 request_id: String,
143 session_id: String,
144 turn_id: String,
145 seq: u64,
146 call_id: String,
147 name: String,
148 },
149 #[serde(rename = "tool.completed")]
150 ToolCompleted {
151 request_id: String,
152 session_id: String,
153 turn_id: String,
154 seq: u64,
155 call_id: String,
156 name: String,
157 success: bool,
158 output: String,
159 truncated: bool,
160 },
161 #[serde(rename = "context.compacted")]
162 ContextCompacted {
163 request_id: String,
164 session_id: String,
165 turn_id: String,
166 seq: u64,
167 before_tokens: usize,
168 after_tokens: usize,
169 removed_messages: usize,
170 },
171 #[serde(rename = "session.trimmed")]
172 SessionTrimmed {
173 request_id: String,
174 session_id: String,
175 seq: u64,
176 removed_messages: usize,
177 history_bytes: usize,
178 },
179 #[serde(rename = "session.cleared")]
180 SessionCleared {
181 request_id: String,
182 session_id: String,
183 seq: u64,
184 },
185 #[serde(rename = "turn.completed")]
186 TurnCompleted {
187 request_id: String,
188 session_id: String,
189 turn_id: String,
190 seq: u64,
191 steps: usize,
192 usage: Usage,
193 },
194 #[serde(rename = "turn.cancelled")]
195 TurnCancelled {
196 request_id: String,
197 session_id: String,
198 turn_id: String,
199 seq: u64,
200 },
201 #[serde(rename = "turn.failed")]
202 TurnFailed {
203 request_id: String,
204 session_id: String,
205 turn_id: String,
206 seq: u64,
207 code: String,
208 message: String,
209 },
210 #[serde(rename = "error")]
211 Error {
212 #[serde(skip_serializing_if = "Option::is_none")]
213 request_id: Option<String>,
214 code: String,
215 message: String,
216 fatal: bool,
217 },
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223
224 #[test]
225 fn client_message_round_trip() {
226 let message = ClientMessage::TurnStart {
227 request_id: "3".into(),
228 session_id: "session".into(),
229 prompt: "hello".into(),
230 };
231 let json = serde_json::to_string(&message).unwrap();
232 assert!(json.contains("\"type\":\"turn.start\""));
233 assert_eq!(
234 serde_json::from_str::<ClientMessage>(&json).unwrap(),
235 message
236 );
237 }
238
239 #[test]
240 fn additive_fields_are_ignored() {
241 let json = r#"{"type":"session.clear","request_id":"1","session_id":"s","future":true}"#;
242 assert!(matches!(
243 serde_json::from_str::<ClientMessage>(json).unwrap(),
244 ClientMessage::SessionClear { .. }
245 ));
246 }
247
248 #[test]
249 fn event_round_trip() {
250 let event = ServerEvent::AssistantDelta {
251 request_id: "1".into(),
252 session_id: "s".into(),
253 turn_id: "t".into(),
254 seq: 4,
255 content: "hello".into(),
256 };
257 let encoded = serde_json::to_string(&event).unwrap();
258 assert_eq!(
259 serde_json::from_str::<ServerEvent>(&encoded).unwrap(),
260 event
261 );
262 }
263}