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