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, JobOutcome, JobReport, PeerInfo, QueueEntry, ToolErrorKind,
8 TurnOrigin, 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 /// The user's key bindings for the terminal UI (`[tui.keys]`): action
67 /// names to key specifications such as `alt+]`. Absent when none are
68 /// configured, and from older servers.
69 #[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
70 keys: std::collections::BTreeMap<String, String>,
71 /// What the session restored from one that stopped (`restore`).
72 #[serde(default, skip_serializing_if = "Option::is_none")]
73 restored: Option<crate::RestoredQueue>,
74 },
75 /// The whole queue, sent when a session starts or on request.
76 #[serde(rename = "queue.snapshot")]
77 QueueSnapshot {
78 /// The client request this answers or belongs to.
79 request_id: Option<String>,
80 /// The session this concerns.
81 session_id: String,
82 /// The session's event sequence number: consecutive, so a gap means events were lost.
83 seq: u64,
84 /// Queued prompts in the order they will run.
85 entries: Vec<QueueEntry>,
86 /// Whether queued prompts wait instead of starting.
87 paused: bool,
88 },
89 /// A prompt was queued behind the running turn.
90 #[serde(rename = "queue.enqueued")]
91 QueueEnqueued {
92 /// The client request this answers or belongs to.
93 request_id: String,
94 /// The session this concerns.
95 session_id: String,
96 /// The session's event sequence number: consecutive, so a gap means events were lost.
97 seq: u64,
98 /// The queued prompt.
99 entry: QueueEntry,
100 /// Its index in the queue.
101 position: usize,
102 },
103 /// A queued prompt's text changed.
104 #[serde(rename = "queue.updated")]
105 QueueUpdated {
106 /// The client request this answers or belongs to.
107 request_id: String,
108 /// The session this concerns.
109 session_id: String,
110 /// The session's event sequence number: consecutive, so a gap means events were lost.
111 seq: u64,
112 /// The queued prompt.
113 entry: QueueEntry,
114 },
115 /// A queued prompt moved.
116 #[serde(rename = "queue.moved")]
117 QueueMoved {
118 /// The client request this answers or belongs to.
119 request_id: String,
120 /// The session this concerns.
121 session_id: String,
122 /// The session's event sequence number: consecutive, so a gap means events were lost.
123 seq: u64,
124 /// The queued prompt.
125 queue_id: String,
126 /// Its index in the queue.
127 position: usize,
128 /// The entry's revision after the change.
129 revision: u64,
130 },
131 /// A queued prompt was dropped.
132 #[serde(rename = "queue.removed")]
133 QueueRemoved {
134 /// The client request this answers or belongs to.
135 request_id: String,
136 /// The session this concerns.
137 session_id: String,
138 /// The session's event sequence number: consecutive, so a gap means events were lost.
139 seq: u64,
140 /// The queued prompt.
141 queue_id: String,
142 /// The entry's revision after the change.
143 revision: u64,
144 },
145 /// A queued prompt left the queue to run as a turn.
146 #[serde(rename = "queue.dequeued")]
147 QueueDequeued {
148 /// The client request this answers or belongs to.
149 request_id: String,
150 /// The session this concerns.
151 session_id: String,
152 /// The session's event sequence number: consecutive, so a gap means events were lost.
153 seq: u64,
154 /// The queued prompt.
155 queue_id: String,
156 /// The turn this belongs to.
157 turn_id: String,
158 },
159 /// The queue was held or released.
160 #[serde(rename = "session.paused")]
161 SessionPaused {
162 /// The client request this answers or belongs to.
163 request_id: String,
164 /// The session this concerns.
165 session_id: String,
166 /// The session's event sequence number: consecutive, so a gap means events were lost.
167 seq: u64,
168 /// Whether queued prompts wait instead of starting.
169 paused: bool,
170 },
171 /// A turn began.
172 #[serde(rename = "turn.started")]
173 TurnStarted {
174 /// The client request this answers or belongs to.
175 request_id: String,
176 /// The session this concerns.
177 session_id: String,
178 /// The turn this belongs to.
179 turn_id: String,
180 /// The session's event sequence number: consecutive, so a gap means events were lost.
181 seq: u64,
182 /// Set when the server started this turn itself, such as to report
183 /// finished background work; absent for a client's own `turn.start`.
184 #[serde(default, skip_serializing_if = "Option::is_none")]
185 origin: Option<TurnOrigin>,
186 },
187 /// Answer text as it streams.
188 #[serde(rename = "assistant.delta")]
189 AssistantDelta {
190 /// The client request this answers or belongs to.
191 request_id: String,
192 /// The session this concerns.
193 session_id: String,
194 /// The turn this belongs to.
195 turn_id: String,
196 /// The session's event sequence number: consecutive, so a gap means events were lost.
197 seq: u64,
198 /// The text.
199 content: String,
200 },
201 /// The model's complete answer for one step.
202 #[serde(rename = "assistant.completed")]
203 AssistantCompleted {
204 /// The client request this answers or belongs to.
205 request_id: String,
206 /// The session this concerns.
207 session_id: String,
208 /// The turn this belongs to.
209 turn_id: String,
210 /// The session's event sequence number: consecutive, so a gap means events were lost.
211 seq: u64,
212 /// The text.
213 content: String,
214 },
215 /// The model asked to call a tool.
216 #[serde(rename = "tool.proposed")]
217 ToolProposed {
218 /// The client request this answers or belongs to.
219 request_id: String,
220 /// The session this concerns.
221 session_id: String,
222 /// The turn this belongs to.
223 turn_id: String,
224 /// The session's event sequence number: consecutive, so a gap means events were lost.
225 seq: u64,
226 /// The tool call, as the model named it.
227 call_id: String,
228 /// The tool.
229 name: String,
230 /// The call's arguments as the model sent them.
231 arguments: Value,
232 },
233 /// A tool call waits for a person's decision.
234 #[serde(rename = "approval.requested")]
235 ApprovalRequested {
236 /// The client request this answers or belongs to.
237 request_id: String,
238 /// The session this concerns.
239 session_id: String,
240 /// The turn this belongs to.
241 turn_id: String,
242 /// The session's event sequence number: consecutive, so a gap means events were lost.
243 seq: u64,
244 /// Answer with `approval.resolve` naming this ID.
245 approval_id: String,
246 /// The tool call, as the model named it.
247 call_id: String,
248 /// The tool.
249 name: String,
250 /// The call's risk, which selected the approval rule.
251 risk: String,
252 /// The session's workspace directory.
253 cwd: String,
254 /// What the call will do, for the person deciding.
255 summary: String,
256 },
257 /// An approved tool call began running.
258 #[serde(rename = "tool.started")]
259 ToolStarted {
260 /// The client request this answers or belongs to.
261 request_id: String,
262 /// The session this concerns.
263 session_id: String,
264 /// The turn this belongs to.
265 turn_id: String,
266 /// The session's event sequence number: consecutive, so a gap means events were lost.
267 seq: u64,
268 /// The tool call, as the model named it.
269 call_id: String,
270 /// The tool.
271 name: String,
272 },
273 /// Short status lines from a running tool, at most two events a second
274 /// per call and 512 bytes each. Display only; not part of the history.
275 #[serde(rename = "tool.progress")]
276 ToolProgress {
277 /// The client request this answers or belongs to.
278 request_id: String,
279 /// The session this concerns.
280 session_id: String,
281 /// The turn this belongs to.
282 turn_id: String,
283 /// The session's event sequence number: consecutive, so a gap means events were lost.
284 seq: u64,
285 /// The tool call, as the model named it.
286 call_id: String,
287 /// The status lines.
288 text: String,
289 },
290 /// A tool call finished; its output goes to the model.
291 #[serde(rename = "tool.completed")]
292 ToolCompleted {
293 /// The client request this answers or belongs to.
294 request_id: String,
295 /// The session this concerns.
296 session_id: String,
297 /// The turn this belongs to.
298 turn_id: String,
299 /// The session's event sequence number: consecutive, so a gap means events were lost.
300 seq: u64,
301 /// The tool call, as the model named it.
302 call_id: String,
303 /// The tool.
304 name: String,
305 /// Whether the tool succeeded.
306 success: bool,
307 /// What the model sees as the result.
308 output: String,
309 /// Whether `output` was cut to its limit.
310 truncated: bool,
311 /// Why the call failed; absent when it succeeded, and from servers
312 /// before 0.3.0.
313 #[serde(default, skip_serializing_if = "Option::is_none")]
314 error: Option<ToolErrorKind>,
315 /// Background jobs this call started, or whose results it showed the
316 /// model; absent when none, and from servers before 0.3.0.
317 #[serde(default, skip_serializing_if = "Vec::is_empty")]
318 jobs: Vec<JobChange>,
319 },
320 /// Older history was summarized to fit the model's context window.
321 #[serde(rename = "context.compacted")]
322 ContextCompacted {
323 /// The client request this answers or belongs to.
324 request_id: String,
325 /// The session this concerns.
326 session_id: String,
327 /// The turn this belongs to.
328 turn_id: String,
329 /// The session's event sequence number: consecutive, so a gap means events were lost.
330 seq: u64,
331 /// Estimated request size before compaction.
332 before_tokens: usize,
333 /// Estimated request size as sent.
334 after_tokens: usize,
335 /// Messages left out or removed.
336 removed_messages: usize,
337 },
338 /// Old turns were removed to keep the session within its history limits.
339 #[serde(rename = "session.trimmed")]
340 SessionTrimmed {
341 /// The client request this answers or belongs to.
342 request_id: String,
343 /// The session this concerns.
344 session_id: String,
345 /// The session's event sequence number: consecutive, so a gap means events were lost.
346 seq: u64,
347 /// Messages left out or removed.
348 removed_messages: usize,
349 /// Size of the session history afterwards.
350 history_bytes: usize,
351 },
352 /// The session's history and queue were cleared.
353 #[serde(rename = "session.cleared")]
354 SessionCleared {
355 /// The client request this answers or belongs to.
356 request_id: String,
357 /// The session this concerns.
358 session_id: String,
359 /// The session's event sequence number: consecutive, so a gap means events were lost.
360 seq: u64,
361 },
362 /// A turn finished.
363 #[serde(rename = "turn.completed")]
364 TurnCompleted {
365 /// The client request this answers or belongs to.
366 request_id: String,
367 /// The session this concerns.
368 session_id: String,
369 /// The turn this belongs to.
370 turn_id: String,
371 /// The session's event sequence number: consecutive, so a gap means events were lost.
372 seq: u64,
373 /// Model requests the turn made.
374 steps: usize,
375 /// Tokens the turn used, as the provider reported them.
376 usage: Usage,
377 /// Set when the server started this turn itself, such as to report
378 /// finished background work; absent for a client's own `turn.start`.
379 #[serde(default, skip_serializing_if = "Option::is_none")]
380 origin: Option<TurnOrigin>,
381 },
382 /// A turn was cancelled; its history changes were rolled back.
383 #[serde(rename = "turn.cancelled")]
384 TurnCancelled {
385 /// The client request this answers or belongs to.
386 request_id: String,
387 /// The session this concerns.
388 session_id: String,
389 /// The turn this belongs to.
390 turn_id: String,
391 /// The session's event sequence number: consecutive, so a gap means events were lost.
392 seq: u64,
393 /// Set when the server started this turn itself, such as to report
394 /// finished background work; absent for a client's own `turn.start`.
395 #[serde(default, skip_serializing_if = "Option::is_none")]
396 origin: Option<TurnOrigin>,
397 },
398 /// A turn failed; its history changes were rolled back.
399 #[serde(rename = "turn.failed")]
400 TurnFailed {
401 /// The client request this answers or belongs to.
402 request_id: String,
403 /// The session this concerns.
404 session_id: String,
405 /// The turn this belongs to.
406 turn_id: String,
407 /// The session's event sequence number: consecutive, so a gap means events were lost.
408 seq: u64,
409 /// Stable machine-readable error code.
410 code: ErrorCode,
411 /// What went wrong, for people.
412 message: String,
413 /// Set when the server started this turn itself, such as to report
414 /// finished background work; absent for a client's own `turn.start`.
415 #[serde(default, skip_serializing_if = "Option::is_none")]
416 origin: Option<TurnOrigin>,
417 },
418 /// Finished background jobs the server reports to the client itself,
419 /// because the model could not: their report turn failed and will not be
420 /// tried again. Sent just before that turn's `turn.failed`; it settles the
421 /// jobs it names.
422 #[serde(rename = "background.reported")]
423 BackgroundReported {
424 /// The session this concerns.
425 session_id: String,
426 /// The session's event sequence number: consecutive, so a gap means events were lost.
427 seq: u64,
428 /// Why the model could not report them: the last report turn's
429 /// failure, as its `turn.failed` gives it.
430 code: ErrorCode,
431 /// What went wrong, for people.
432 message: String,
433 /// Report turns that failed for these jobs, the last included.
434 attempts: u32,
435 /// The jobs' results.
436 reports: Vec<JobReport>,
437 },
438 /// The final outcome of reviewed jobs whose outcome the client was
439 /// already given while their journal was still open
440 /// ([`ReviewSummary::journal_pending`](crate::ReviewSummary::journal_pending)):
441 /// a job `agent_cancel` stopped that had not stopped by the end of the
442 /// cancel's wait. Its decision is unchanged; the update says whether the
443 /// journal then ended complete. It names no request and belongs to no
444 /// turn, and it settles the jobs it names.
445 #[serde(rename = "background.updated")]
446 BackgroundUpdated {
447 /// The session this concerns.
448 session_id: String,
449 /// The session's event sequence number: consecutive, so a gap means events were lost.
450 seq: u64,
451 /// Each job's final outcome.
452 outcomes: Vec<JobOutcome>,
453 },
454 /// A request failed, or a connection-level error.
455 #[serde(rename = "error")]
456 Error {
457 /// The client request this answers or belongs to.
458 #[serde(skip_serializing_if = "Option::is_none")]
459 request_id: Option<String>,
460 /// Stable machine-readable error code.
461 code: ErrorCode,
462 /// What went wrong, for people.
463 message: String,
464 /// Whether the connection is unusable after this error.
465 fatal: bool,
466 },
467 /// An event this client does not know, from a newer server. Never sent.
468 #[serde(other, rename = "unknown")]
469 Unknown,
470}
471
472impl ServerEvent {
473 /// The submitting request of a turn-scoped event, which identifies the
474 /// turn to a client that has several turns' events interleaved.
475 pub fn turn_request_id(&self) -> Option<&str> {
476 match self {
477 Self::QueueDequeued { request_id, .. }
478 | Self::TurnStarted { request_id, .. }
479 | Self::AssistantDelta { request_id, .. }
480 | Self::AssistantCompleted { request_id, .. }
481 | Self::ToolProposed { request_id, .. }
482 | Self::ApprovalRequested { request_id, .. }
483 | Self::ToolStarted { request_id, .. }
484 | Self::ToolProgress { request_id, .. }
485 | Self::ToolCompleted { request_id, .. }
486 | Self::ContextCompacted { request_id, .. }
487 | Self::TurnCompleted { request_id, .. }
488 | Self::TurnCancelled { request_id, .. }
489 | Self::TurnFailed { request_id, .. } => Some(request_id),
490 _ => None,
491 }
492 }
493}