Skip to main content

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}