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, 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}