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