scv_protocol/server.rs
1//! [`ServerEvent`]: everything the server sends.
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6use crate::{
7 DaemonStatus, ErrorCode, JobChange, PeerInfo, QueueEntry, ToolErrorKind, TurnOrigin, Usage,
8};
9
10/// A message from server to client. Serialized as one JSON object per line,
11/// tagged by `type` (such as `turn.completed`). Turn events carry the
12/// session's consecutive `seq`.
13///
14/// A `type` this client does not know parses as [`ServerEvent::Unknown`], so
15/// a newer server can add events without breaking older clients; a client
16/// ignores them.
17#[allow(
18 clippy::large_enum_variant,
19 reason = "preserve the established wire event shape without boxing every status frame"
20)]
21#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
22#[serde(tag = "type")]
23pub enum ServerEvent {
24 /// The answer to a `daemon.control` status request.
25 #[serde(rename = "daemon.status")]
26 DaemonStatus {
27 /// The client request this answers or belongs to.
28 request_id: String,
29 /// The daemon's state.
30 status: DaemonStatus,
31 },
32 /// The answer to `initialize`.
33 #[serde(rename = "initialized")]
34 Initialized {
35 /// The client request this answers or belongs to.
36 request_id: String,
37 /// The [`PROTOCOL_VERSION`](crate::PROTOCOL_VERSION) the server speaks.
38 protocol_version: u32,
39 /// The server's name and version.
40 server: PeerInfo,
41 },
42 /// The answer to `session.start`, with the limits the client should use.
43 #[serde(rename = "session.started")]
44 SessionStarted {
45 /// The client request this answers or belongs to.
46 request_id: String,
47 /// The session this concerns.
48 session_id: String,
49 /// The session's workspace directory.
50 cwd: String,
51 /// The model answering.
52 model: String,
53 /// The model's context window.
54 context_max_tokens: usize,
55 /// Largest event frame the server sends; clients size their reads by it.
56 max_server_frame_bytes: usize,
57 /// Transcript bytes a client should keep for display.
58 max_transcript_bytes: usize,
59 /// Transcript items a client should keep for display.
60 max_transcript_items: usize,
61 /// Prompt-history bytes a client should keep.
62 max_prompt_history_bytes: usize,
63 /// Prompt-history entries a client should keep.
64 max_prompt_history_items: usize,
65 },
66 /// The whole queue, sent when a session starts or on request.
67 #[serde(rename = "queue.snapshot")]
68 QueueSnapshot {
69 /// The client request this answers or belongs to.
70 request_id: Option<String>,
71 /// The session this concerns.
72 session_id: String,
73 /// The session's event sequence number: consecutive, so a gap means events were lost.
74 seq: u64,
75 /// Queued prompts in the order they will run.
76 entries: Vec<QueueEntry>,
77 /// Whether queued prompts wait instead of starting.
78 paused: bool,
79 },
80 /// A prompt was queued behind the running turn.
81 #[serde(rename = "queue.enqueued")]
82 QueueEnqueued {
83 /// The client request this answers or belongs to.
84 request_id: String,
85 /// The session this concerns.
86 session_id: String,
87 /// The session's event sequence number: consecutive, so a gap means events were lost.
88 seq: u64,
89 /// The queued prompt.
90 entry: QueueEntry,
91 /// Its index in the queue.
92 position: usize,
93 },
94 /// A queued prompt's text changed.
95 #[serde(rename = "queue.updated")]
96 QueueUpdated {
97 /// The client request this answers or belongs to.
98 request_id: String,
99 /// The session this concerns.
100 session_id: String,
101 /// The session's event sequence number: consecutive, so a gap means events were lost.
102 seq: u64,
103 /// The queued prompt.
104 entry: QueueEntry,
105 },
106 /// A queued prompt moved.
107 #[serde(rename = "queue.moved")]
108 QueueMoved {
109 /// The client request this answers or belongs to.
110 request_id: String,
111 /// The session this concerns.
112 session_id: String,
113 /// The session's event sequence number: consecutive, so a gap means events were lost.
114 seq: u64,
115 /// The queued prompt.
116 queue_id: String,
117 /// Its index in the queue.
118 position: usize,
119 /// The entry's revision after the change.
120 revision: u64,
121 },
122 /// A queued prompt was dropped.
123 #[serde(rename = "queue.removed")]
124 QueueRemoved {
125 /// The client request this answers or belongs to.
126 request_id: String,
127 /// The session this concerns.
128 session_id: String,
129 /// The session's event sequence number: consecutive, so a gap means events were lost.
130 seq: u64,
131 /// The queued prompt.
132 queue_id: String,
133 /// The entry's revision after the change.
134 revision: u64,
135 },
136 /// A queued prompt left the queue to run as a turn.
137 #[serde(rename = "queue.dequeued")]
138 QueueDequeued {
139 /// The client request this answers or belongs to.
140 request_id: String,
141 /// The session this concerns.
142 session_id: String,
143 /// The session's event sequence number: consecutive, so a gap means events were lost.
144 seq: u64,
145 /// The queued prompt.
146 queue_id: String,
147 /// The turn this belongs to.
148 turn_id: String,
149 },
150 /// The queue was held or released.
151 #[serde(rename = "session.paused")]
152 SessionPaused {
153 /// The client request this answers or belongs to.
154 request_id: String,
155 /// The session this concerns.
156 session_id: String,
157 /// The session's event sequence number: consecutive, so a gap means events were lost.
158 seq: u64,
159 /// Whether queued prompts wait instead of starting.
160 paused: bool,
161 },
162 /// A turn began.
163 #[serde(rename = "turn.started")]
164 TurnStarted {
165 /// The client request this answers or belongs to.
166 request_id: String,
167 /// The session this concerns.
168 session_id: String,
169 /// The turn this belongs to.
170 turn_id: String,
171 /// The session's event sequence number: consecutive, so a gap means events were lost.
172 seq: u64,
173 /// Set when the server started this turn itself, such as to report
174 /// finished background work; absent for a client's own `turn.start`.
175 #[serde(default, skip_serializing_if = "Option::is_none")]
176 origin: Option<TurnOrigin>,
177 },
178 /// Answer text as it streams.
179 #[serde(rename = "assistant.delta")]
180 AssistantDelta {
181 /// The client request this answers or belongs to.
182 request_id: String,
183 /// The session this concerns.
184 session_id: String,
185 /// The turn this belongs to.
186 turn_id: String,
187 /// The session's event sequence number: consecutive, so a gap means events were lost.
188 seq: u64,
189 /// The text.
190 content: String,
191 },
192 /// The model's complete answer for one step.
193 #[serde(rename = "assistant.completed")]
194 AssistantCompleted {
195 /// The client request this answers or belongs to.
196 request_id: String,
197 /// The session this concerns.
198 session_id: String,
199 /// The turn this belongs to.
200 turn_id: String,
201 /// The session's event sequence number: consecutive, so a gap means events were lost.
202 seq: u64,
203 /// The text.
204 content: String,
205 },
206 /// The model asked to call a tool.
207 #[serde(rename = "tool.proposed")]
208 ToolProposed {
209 /// The client request this answers or belongs to.
210 request_id: String,
211 /// The session this concerns.
212 session_id: String,
213 /// The turn this belongs to.
214 turn_id: String,
215 /// The session's event sequence number: consecutive, so a gap means events were lost.
216 seq: u64,
217 /// The tool call, as the model named it.
218 call_id: String,
219 /// The tool.
220 name: String,
221 /// The call's arguments as the model sent them.
222 arguments: Value,
223 },
224 /// A tool call waits for a person's decision.
225 #[serde(rename = "approval.requested")]
226 ApprovalRequested {
227 /// The client request this answers or belongs to.
228 request_id: String,
229 /// The session this concerns.
230 session_id: String,
231 /// The turn this belongs to.
232 turn_id: String,
233 /// The session's event sequence number: consecutive, so a gap means events were lost.
234 seq: u64,
235 /// Answer with `approval.resolve` naming this ID.
236 approval_id: String,
237 /// The tool call, as the model named it.
238 call_id: String,
239 /// The tool.
240 name: String,
241 /// The call's risk, which selected the approval rule.
242 risk: String,
243 /// The session's workspace directory.
244 cwd: String,
245 /// What the call will do, for the person deciding.
246 summary: String,
247 },
248 /// An approved tool call began running.
249 #[serde(rename = "tool.started")]
250 ToolStarted {
251 /// The client request this answers or belongs to.
252 request_id: String,
253 /// The session this concerns.
254 session_id: String,
255 /// The turn this belongs to.
256 turn_id: String,
257 /// The session's event sequence number: consecutive, so a gap means events were lost.
258 seq: u64,
259 /// The tool call, as the model named it.
260 call_id: String,
261 /// The tool.
262 name: String,
263 },
264 /// Short status lines from a running tool, at most two events a second
265 /// per call and 512 bytes each. Display only; not part of the history.
266 #[serde(rename = "tool.progress")]
267 ToolProgress {
268 /// The client request this answers or belongs to.
269 request_id: String,
270 /// The session this concerns.
271 session_id: String,
272 /// The turn this belongs to.
273 turn_id: String,
274 /// The session's event sequence number: consecutive, so a gap means events were lost.
275 seq: u64,
276 /// The tool call, as the model named it.
277 call_id: String,
278 /// The status lines.
279 text: String,
280 },
281 /// A tool call finished; its output goes to the model.
282 #[serde(rename = "tool.completed")]
283 ToolCompleted {
284 /// The client request this answers or belongs to.
285 request_id: String,
286 /// The session this concerns.
287 session_id: String,
288 /// The turn this belongs to.
289 turn_id: String,
290 /// The session's event sequence number: consecutive, so a gap means events were lost.
291 seq: u64,
292 /// The tool call, as the model named it.
293 call_id: String,
294 /// The tool.
295 name: String,
296 /// Whether the tool succeeded.
297 success: bool,
298 /// What the model sees as the result.
299 output: String,
300 /// Whether `output` was cut to its limit.
301 truncated: bool,
302 /// Why the call failed; absent when it succeeded, and from servers
303 /// before 0.3.0.
304 #[serde(default, skip_serializing_if = "Option::is_none")]
305 error: Option<ToolErrorKind>,
306 /// Background jobs this call started, or whose results it showed the
307 /// model; absent when none, and from servers before 0.3.0.
308 #[serde(default, skip_serializing_if = "Vec::is_empty")]
309 jobs: Vec<JobChange>,
310 },
311 /// Older history was summarized to fit the model's context window.
312 #[serde(rename = "context.compacted")]
313 ContextCompacted {
314 /// The client request this answers or belongs to.
315 request_id: String,
316 /// The session this concerns.
317 session_id: String,
318 /// The turn this belongs to.
319 turn_id: String,
320 /// The session's event sequence number: consecutive, so a gap means events were lost.
321 seq: u64,
322 /// Estimated request size before compaction.
323 before_tokens: usize,
324 /// Estimated request size as sent.
325 after_tokens: usize,
326 /// Messages left out or removed.
327 removed_messages: usize,
328 },
329 /// Old turns were removed to keep the session within its history limits.
330 #[serde(rename = "session.trimmed")]
331 SessionTrimmed {
332 /// The client request this answers or belongs to.
333 request_id: String,
334 /// The session this concerns.
335 session_id: String,
336 /// The session's event sequence number: consecutive, so a gap means events were lost.
337 seq: u64,
338 /// Messages left out or removed.
339 removed_messages: usize,
340 /// Size of the session history afterwards.
341 history_bytes: usize,
342 },
343 /// The session's history and queue were cleared.
344 #[serde(rename = "session.cleared")]
345 SessionCleared {
346 /// The client request this answers or belongs to.
347 request_id: String,
348 /// The session this concerns.
349 session_id: String,
350 /// The session's event sequence number: consecutive, so a gap means events were lost.
351 seq: u64,
352 },
353 /// A turn finished.
354 #[serde(rename = "turn.completed")]
355 TurnCompleted {
356 /// The client request this answers or belongs to.
357 request_id: String,
358 /// The session this concerns.
359 session_id: String,
360 /// The turn this belongs to.
361 turn_id: String,
362 /// The session's event sequence number: consecutive, so a gap means events were lost.
363 seq: u64,
364 /// Model requests the turn made.
365 steps: usize,
366 /// Tokens the turn used, as the provider reported them.
367 usage: Usage,
368 /// Set when the server started this turn itself, such as to report
369 /// finished background work; absent for a client's own `turn.start`.
370 #[serde(default, skip_serializing_if = "Option::is_none")]
371 origin: Option<TurnOrigin>,
372 },
373 /// A turn was cancelled; its history changes were rolled back.
374 #[serde(rename = "turn.cancelled")]
375 TurnCancelled {
376 /// The client request this answers or belongs to.
377 request_id: String,
378 /// The session this concerns.
379 session_id: String,
380 /// The turn this belongs to.
381 turn_id: String,
382 /// The session's event sequence number: consecutive, so a gap means events were lost.
383 seq: u64,
384 /// Set when the server started this turn itself, such as to report
385 /// finished background work; absent for a client's own `turn.start`.
386 #[serde(default, skip_serializing_if = "Option::is_none")]
387 origin: Option<TurnOrigin>,
388 },
389 /// A turn failed; its history changes were rolled back.
390 #[serde(rename = "turn.failed")]
391 TurnFailed {
392 /// The client request this answers or belongs to.
393 request_id: String,
394 /// The session this concerns.
395 session_id: String,
396 /// The turn this belongs to.
397 turn_id: String,
398 /// The session's event sequence number: consecutive, so a gap means events were lost.
399 seq: u64,
400 /// Stable machine-readable error code.
401 code: ErrorCode,
402 /// What went wrong, for people.
403 message: String,
404 /// Set when the server started this turn itself, such as to report
405 /// finished background work; absent for a client's own `turn.start`.
406 #[serde(default, skip_serializing_if = "Option::is_none")]
407 origin: Option<TurnOrigin>,
408 },
409 /// A request failed, or a connection-level error.
410 #[serde(rename = "error")]
411 Error {
412 /// The client request this answers or belongs to.
413 #[serde(skip_serializing_if = "Option::is_none")]
414 request_id: Option<String>,
415 /// Stable machine-readable error code.
416 code: ErrorCode,
417 /// What went wrong, for people.
418 message: String,
419 /// Whether the connection is unusable after this error.
420 fatal: bool,
421 },
422 /// An event this client does not know, from a newer server. Never sent.
423 #[serde(other, rename = "unknown")]
424 Unknown,
425}
426
427impl ServerEvent {
428 /// The submitting request of a turn-scoped event, which identifies the
429 /// turn to a client that has several turns' events interleaved.
430 pub fn turn_request_id(&self) -> Option<&str> {
431 match self {
432 Self::QueueDequeued { request_id, .. }
433 | Self::TurnStarted { request_id, .. }
434 | Self::AssistantDelta { request_id, .. }
435 | Self::AssistantCompleted { request_id, .. }
436 | Self::ToolProposed { request_id, .. }
437 | Self::ApprovalRequested { request_id, .. }
438 | Self::ToolStarted { request_id, .. }
439 | Self::ToolProgress { request_id, .. }
440 | Self::ToolCompleted { request_id, .. }
441 | Self::ContextCompacted { request_id, .. }
442 | Self::TurnCompleted { request_id, .. }
443 | Self::TurnCancelled { request_id, .. }
444 | Self::TurnFailed { request_id, .. } => Some(request_id),
445 _ => None,
446 }
447 }
448}