scv-protocol 0.3.3

Versioned JSON protocol types for SCV clients and servers
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
//! [`ServerEvent`]: everything the server sends.

use serde::{Deserialize, Serialize};
use serde_json::Value;

use crate::{
    DaemonStatus, ErrorCode, JobChange, PeerInfo, QueueEntry, ToolErrorKind, TurnOrigin, Usage,
};

/// A message from server to client. Serialized as one JSON object per line,
/// tagged by `type` (such as `turn.completed`). Turn events carry the
/// session's consecutive `seq`.
///
/// A `type` this client does not know parses as [`ServerEvent::Unknown`], so
/// a newer server can add events without breaking older clients; a client
/// ignores them.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type")]
pub enum ServerEvent {
    /// The answer to a `daemon.control` status request.
    #[serde(rename = "daemon.status")]
    DaemonStatus {
        /// The client request this answers or belongs to.
        request_id: String,
        /// The daemon's state.
        status: DaemonStatus,
    },
    /// The answer to `initialize`.
    #[serde(rename = "initialized")]
    Initialized {
        /// The client request this answers or belongs to.
        request_id: String,
        /// The [`PROTOCOL_VERSION`](crate::PROTOCOL_VERSION) the server speaks.
        protocol_version: u32,
        /// The server's name and version.
        server: PeerInfo,
    },
    /// The answer to `session.start`, with the limits the client should use.
    #[serde(rename = "session.started")]
    SessionStarted {
        /// The client request this answers or belongs to.
        request_id: String,
        /// The session this concerns.
        session_id: String,
        /// The session's workspace directory.
        cwd: String,
        /// The model answering.
        model: String,
        /// The model's context window.
        context_max_tokens: usize,
        /// Largest event frame the server sends; clients size their reads by it.
        max_server_frame_bytes: usize,
        /// Transcript bytes a client should keep for display.
        max_transcript_bytes: usize,
        /// Transcript items a client should keep for display.
        max_transcript_items: usize,
        /// Prompt-history bytes a client should keep.
        max_prompt_history_bytes: usize,
        /// Prompt-history entries a client should keep.
        max_prompt_history_items: usize,
    },
    /// The whole queue, sent when a session starts or on request.
    #[serde(rename = "queue.snapshot")]
    QueueSnapshot {
        /// The client request this answers or belongs to.
        request_id: Option<String>,
        /// The session this concerns.
        session_id: String,
        /// The session's event sequence number: consecutive, so a gap means events were lost.
        seq: u64,
        /// Queued prompts in the order they will run.
        entries: Vec<QueueEntry>,
        /// Whether queued prompts wait instead of starting.
        paused: bool,
    },
    /// A prompt was queued behind the running turn.
    #[serde(rename = "queue.enqueued")]
    QueueEnqueued {
        /// The client request this answers or belongs to.
        request_id: String,
        /// The session this concerns.
        session_id: String,
        /// The session's event sequence number: consecutive, so a gap means events were lost.
        seq: u64,
        /// The queued prompt.
        entry: QueueEntry,
        /// Its index in the queue.
        position: usize,
    },
    /// A queued prompt's text changed.
    #[serde(rename = "queue.updated")]
    QueueUpdated {
        /// The client request this answers or belongs to.
        request_id: String,
        /// The session this concerns.
        session_id: String,
        /// The session's event sequence number: consecutive, so a gap means events were lost.
        seq: u64,
        /// The queued prompt.
        entry: QueueEntry,
    },
    /// A queued prompt moved.
    #[serde(rename = "queue.moved")]
    QueueMoved {
        /// The client request this answers or belongs to.
        request_id: String,
        /// The session this concerns.
        session_id: String,
        /// The session's event sequence number: consecutive, so a gap means events were lost.
        seq: u64,
        /// The queued prompt.
        queue_id: String,
        /// Its index in the queue.
        position: usize,
        /// The entry's revision after the change.
        revision: u64,
    },
    /// A queued prompt was dropped.
    #[serde(rename = "queue.removed")]
    QueueRemoved {
        /// The client request this answers or belongs to.
        request_id: String,
        /// The session this concerns.
        session_id: String,
        /// The session's event sequence number: consecutive, so a gap means events were lost.
        seq: u64,
        /// The queued prompt.
        queue_id: String,
        /// The entry's revision after the change.
        revision: u64,
    },
    /// A queued prompt left the queue to run as a turn.
    #[serde(rename = "queue.dequeued")]
    QueueDequeued {
        /// The client request this answers or belongs to.
        request_id: String,
        /// The session this concerns.
        session_id: String,
        /// The session's event sequence number: consecutive, so a gap means events were lost.
        seq: u64,
        /// The queued prompt.
        queue_id: String,
        /// The turn this belongs to.
        turn_id: String,
    },
    /// The queue was held or released.
    #[serde(rename = "session.paused")]
    SessionPaused {
        /// The client request this answers or belongs to.
        request_id: String,
        /// The session this concerns.
        session_id: String,
        /// The session's event sequence number: consecutive, so a gap means events were lost.
        seq: u64,
        /// Whether queued prompts wait instead of starting.
        paused: bool,
    },
    /// A turn began.
    #[serde(rename = "turn.started")]
    TurnStarted {
        /// The client request this answers or belongs to.
        request_id: String,
        /// The session this concerns.
        session_id: String,
        /// The turn this belongs to.
        turn_id: String,
        /// The session's event sequence number: consecutive, so a gap means events were lost.
        seq: u64,
        /// Set when the server started this turn itself, such as to report
        /// finished background work; absent for a client's own `turn.start`.
        #[serde(default, skip_serializing_if = "Option::is_none")]
        origin: Option<TurnOrigin>,
    },
    /// Answer text as it streams.
    #[serde(rename = "assistant.delta")]
    AssistantDelta {
        /// The client request this answers or belongs to.
        request_id: String,
        /// The session this concerns.
        session_id: String,
        /// The turn this belongs to.
        turn_id: String,
        /// The session's event sequence number: consecutive, so a gap means events were lost.
        seq: u64,
        /// The text.
        content: String,
    },
    /// The model's complete answer for one step.
    #[serde(rename = "assistant.completed")]
    AssistantCompleted {
        /// The client request this answers or belongs to.
        request_id: String,
        /// The session this concerns.
        session_id: String,
        /// The turn this belongs to.
        turn_id: String,
        /// The session's event sequence number: consecutive, so a gap means events were lost.
        seq: u64,
        /// The text.
        content: String,
    },
    /// The model asked to call a tool.
    #[serde(rename = "tool.proposed")]
    ToolProposed {
        /// The client request this answers or belongs to.
        request_id: String,
        /// The session this concerns.
        session_id: String,
        /// The turn this belongs to.
        turn_id: String,
        /// The session's event sequence number: consecutive, so a gap means events were lost.
        seq: u64,
        /// The tool call, as the model named it.
        call_id: String,
        /// The tool.
        name: String,
        /// The call's arguments as the model sent them.
        arguments: Value,
    },
    /// A tool call waits for a person's decision.
    #[serde(rename = "approval.requested")]
    ApprovalRequested {
        /// The client request this answers or belongs to.
        request_id: String,
        /// The session this concerns.
        session_id: String,
        /// The turn this belongs to.
        turn_id: String,
        /// The session's event sequence number: consecutive, so a gap means events were lost.
        seq: u64,
        /// Answer with `approval.resolve` naming this ID.
        approval_id: String,
        /// The tool call, as the model named it.
        call_id: String,
        /// The tool.
        name: String,
        /// The call's risk, which selected the approval rule.
        risk: String,
        /// The session's workspace directory.
        cwd: String,
        /// What the call will do, for the person deciding.
        summary: String,
    },
    /// An approved tool call began running.
    #[serde(rename = "tool.started")]
    ToolStarted {
        /// The client request this answers or belongs to.
        request_id: String,
        /// The session this concerns.
        session_id: String,
        /// The turn this belongs to.
        turn_id: String,
        /// The session's event sequence number: consecutive, so a gap means events were lost.
        seq: u64,
        /// The tool call, as the model named it.
        call_id: String,
        /// The tool.
        name: String,
    },
    /// Short status lines from a running tool, at most two events a second
    /// per call and 512 bytes each. Display only; not part of the history.
    #[serde(rename = "tool.progress")]
    ToolProgress {
        /// The client request this answers or belongs to.
        request_id: String,
        /// The session this concerns.
        session_id: String,
        /// The turn this belongs to.
        turn_id: String,
        /// The session's event sequence number: consecutive, so a gap means events were lost.
        seq: u64,
        /// The tool call, as the model named it.
        call_id: String,
        /// The status lines.
        text: String,
    },
    /// A tool call finished; its output goes to the model.
    #[serde(rename = "tool.completed")]
    ToolCompleted {
        /// The client request this answers or belongs to.
        request_id: String,
        /// The session this concerns.
        session_id: String,
        /// The turn this belongs to.
        turn_id: String,
        /// The session's event sequence number: consecutive, so a gap means events were lost.
        seq: u64,
        /// The tool call, as the model named it.
        call_id: String,
        /// The tool.
        name: String,
        /// Whether the tool succeeded.
        success: bool,
        /// What the model sees as the result.
        output: String,
        /// Whether `output` was cut to its limit.
        truncated: bool,
        /// Why the call failed; absent when it succeeded, and from servers
        /// before 0.3.0.
        #[serde(default, skip_serializing_if = "Option::is_none")]
        error: Option<ToolErrorKind>,
        /// Background jobs this call started, or whose results it showed the
        /// model; absent when none, and from servers before 0.3.0.
        #[serde(default, skip_serializing_if = "Vec::is_empty")]
        jobs: Vec<JobChange>,
    },
    /// Older history was summarized to fit the model's context window.
    #[serde(rename = "context.compacted")]
    ContextCompacted {
        /// The client request this answers or belongs to.
        request_id: String,
        /// The session this concerns.
        session_id: String,
        /// The turn this belongs to.
        turn_id: String,
        /// The session's event sequence number: consecutive, so a gap means events were lost.
        seq: u64,
        /// Estimated request size before compaction.
        before_tokens: usize,
        /// Estimated request size as sent.
        after_tokens: usize,
        /// Messages left out or removed.
        removed_messages: usize,
    },
    /// Old turns were removed to keep the session within its history limits.
    #[serde(rename = "session.trimmed")]
    SessionTrimmed {
        /// The client request this answers or belongs to.
        request_id: String,
        /// The session this concerns.
        session_id: String,
        /// The session's event sequence number: consecutive, so a gap means events were lost.
        seq: u64,
        /// Messages left out or removed.
        removed_messages: usize,
        /// Size of the session history afterwards.
        history_bytes: usize,
    },
    /// The session's history and queue were cleared.
    #[serde(rename = "session.cleared")]
    SessionCleared {
        /// The client request this answers or belongs to.
        request_id: String,
        /// The session this concerns.
        session_id: String,
        /// The session's event sequence number: consecutive, so a gap means events were lost.
        seq: u64,
    },
    /// A turn finished.
    #[serde(rename = "turn.completed")]
    TurnCompleted {
        /// The client request this answers or belongs to.
        request_id: String,
        /// The session this concerns.
        session_id: String,
        /// The turn this belongs to.
        turn_id: String,
        /// The session's event sequence number: consecutive, so a gap means events were lost.
        seq: u64,
        /// Model requests the turn made.
        steps: usize,
        /// Tokens the turn used, as the provider reported them.
        usage: Usage,
        /// Set when the server started this turn itself, such as to report
        /// finished background work; absent for a client's own `turn.start`.
        #[serde(default, skip_serializing_if = "Option::is_none")]
        origin: Option<TurnOrigin>,
    },
    /// A turn was cancelled; its history changes were rolled back.
    #[serde(rename = "turn.cancelled")]
    TurnCancelled {
        /// The client request this answers or belongs to.
        request_id: String,
        /// The session this concerns.
        session_id: String,
        /// The turn this belongs to.
        turn_id: String,
        /// The session's event sequence number: consecutive, so a gap means events were lost.
        seq: u64,
        /// Set when the server started this turn itself, such as to report
        /// finished background work; absent for a client's own `turn.start`.
        #[serde(default, skip_serializing_if = "Option::is_none")]
        origin: Option<TurnOrigin>,
    },
    /// A turn failed; its history changes were rolled back.
    #[serde(rename = "turn.failed")]
    TurnFailed {
        /// The client request this answers or belongs to.
        request_id: String,
        /// The session this concerns.
        session_id: String,
        /// The turn this belongs to.
        turn_id: String,
        /// The session's event sequence number: consecutive, so a gap means events were lost.
        seq: u64,
        /// Stable machine-readable error code.
        code: ErrorCode,
        /// What went wrong, for people.
        message: String,
        /// Set when the server started this turn itself, such as to report
        /// finished background work; absent for a client's own `turn.start`.
        #[serde(default, skip_serializing_if = "Option::is_none")]
        origin: Option<TurnOrigin>,
    },
    /// A request failed, or a connection-level error.
    #[serde(rename = "error")]
    Error {
        /// The client request this answers or belongs to.
        #[serde(skip_serializing_if = "Option::is_none")]
        request_id: Option<String>,
        /// Stable machine-readable error code.
        code: ErrorCode,
        /// What went wrong, for people.
        message: String,
        /// Whether the connection is unusable after this error.
        fatal: bool,
    },
    /// An event this client does not know, from a newer server. Never sent.
    #[serde(other, rename = "unknown")]
    Unknown,
}

impl ServerEvent {
    /// The submitting request of a turn-scoped event, which identifies the
    /// turn to a client that has several turns' events interleaved.
    pub fn turn_request_id(&self) -> Option<&str> {
        match self {
            Self::QueueDequeued { request_id, .. }
            | Self::TurnStarted { request_id, .. }
            | Self::AssistantDelta { request_id, .. }
            | Self::AssistantCompleted { request_id, .. }
            | Self::ToolProposed { request_id, .. }
            | Self::ApprovalRequested { request_id, .. }
            | Self::ToolStarted { request_id, .. }
            | Self::ToolProgress { request_id, .. }
            | Self::ToolCompleted { request_id, .. }
            | Self::ContextCompacted { request_id, .. }
            | Self::TurnCompleted { request_id, .. }
            | Self::TurnCancelled { request_id, .. }
            | Self::TurnFailed { request_id, .. } => Some(request_id),
            _ => None,
        }
    }
}