scv-protocol 0.4.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
//! The agent workbench: what SCV knows about delegated agents, background
//! jobs, and their sessions across the whole instance, as a dashboard reads
//! it ([`WorkbenchSnapshot`]), and the activity a delegated run records for
//! its live view ([`RunEvent`], [`RunLogPage`]).
//!
//! Every type here is additive to protocol version 3: a daemon sends them
//! only in answer to the `workbench*`, `run_log`, and `worktree*`
//! `daemon.control` actions, which only newer clients send.

use serde::{Deserialize, Serialize};

/// What a delegated run, a background job, or a session is doing, as far as
/// SCV can tell. A state a client does not know parses as `unknown`, which
/// also means SCV itself cannot tell.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AgentState {
    /// Launching: starting the process, its handshake, or its session.
    Starting,
    /// A turn runs and the agent reports activity.
    Working,
    /// The agent waits for a person: an approval request it relayed, or the
    /// session's own approval request, is unanswered.
    AwaitingInput,
    /// A live agent (an ACP server or nested SCV) that waits between turns
    /// for its next prompt; or a session with no turn running.
    Idle,
    /// A prompt that waits for its conversation's running turn.
    Queued,
    /// The turn finished and the agent completed its task.
    Completed,
    /// The run or job failed.
    Failed,
    /// The agent's model refused the request.
    Declined,
    /// The run reached its time limit.
    Timeout,
    /// Stopped on request.
    Cancelled,
    /// Its SCV process or session stopped before it finished: a crash, a
    /// restart, or the session's client going away.
    Interrupted,
    /// SCV cannot tell: a turn has reported nothing for a long while, or the
    /// process that owns it cannot be observed. Also any state this client
    /// does not know.
    #[serde(other)]
    Unknown,
}

impl AgentState {
    /// The state as SCV writes it, such as `awaiting_input`.
    pub fn as_str(self) -> &'static str {
        match self {
            Self::Starting => "starting",
            Self::Working => "working",
            Self::AwaitingInput => "awaiting_input",
            Self::Idle => "idle",
            Self::Queued => "queued",
            Self::Completed => "completed",
            Self::Failed => "failed",
            Self::Declined => "declined",
            Self::Timeout => "timeout",
            Self::Cancelled => "cancelled",
            Self::Interrupted => "interrupted",
            Self::Unknown => "unknown",
        }
    }

    /// Whether nothing more happens in this state by itself: the run or job
    /// has ended one way or another.
    pub fn is_final(self) -> bool {
        matches!(
            self,
            Self::Completed
                | Self::Failed
                | Self::Declined
                | Self::Timeout
                | Self::Cancelled
                | Self::Interrupted
        )
    }

    /// Whether a person is needed to go on.
    pub fn needs_attention(self) -> bool {
        matches!(self, Self::AwaitingInput)
    }
}

impl std::fmt::Display for AgentState {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        formatter.write_str(self.as_str())
    }
}

/// Who started a background job.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum JobOrigin {
    /// The session's model, through an `agent` call.
    #[default]
    Model,
    /// The owner, from the workbench (a dashboard or `scv jobs`).
    Owner,
    /// An origin this client does not know.
    #[serde(other)]
    Unknown,
}

/// A git worktree SCV created for one job, so its agent works apart from
/// the repository's own checkout.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WorktreeRef {
    /// The worktree's directory, under `$SCV_HOME/worktrees`.
    pub path: String,
    /// The branch SCV created for it, such as `scv/codex-3f9a2c`.
    pub branch: String,
    /// The repository it was created from.
    pub repo: String,
}

/// A worktree SCV created, as `scv agents worktrees` lists it.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WorktreeInfo {
    /// Where it is, its branch, and the repository it belongs to.
    #[serde(flatten)]
    pub worktree: WorktreeRef,
    /// Whether it has uncommitted changes; absent when git could not tell.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub dirty: Option<bool>,
    /// The jobs recorded as working in it, as `<session>/<job>`.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub jobs: Vec<String>,
}

/// One SCV session the workbench knows: a live daemon session, the
/// workbench's own host for jobs the owner starts, or one that ended.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WorkbenchSession {
    /// The session ID.
    pub id: String,
    /// Who it serves, such as `TUI`, `WeChat`, `scv exec`, or `workbench`.
    pub label: String,
    /// Its workspace directory.
    pub workspace: String,
    /// Whether it still runs in a process of this instance.
    pub live: bool,
    /// What it is doing, when it is live.
    pub state: AgentState,
    /// SCV's words for that state, such as which approval it waits for.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub detail: Option<String>,
}

/// One background job, from the instance's job ledger and, while it runs,
/// its live state.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WorkbenchJob {
    /// The session that owns it.
    pub session: String,
    /// Its handle in that session, such as `job-3`.
    pub job: String,
    /// The agent that runs it, such as `codex`.
    pub agent: String,
    /// The first line of its prompt, shortened.
    pub task: String,
    /// The session's workspace.
    pub workspace: String,
    /// Where its agent runs: a directory in the workspace, or its worktree.
    pub cwd: String,
    /// What it is doing.
    pub state: AgentState,
    /// SCV's words for that state.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub detail: Option<String>,
    /// Who started it.
    pub started_by: JobOrigin,
    /// The conversation it runs a turn of, such as `codex-2`, once known.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub conversation: Option<String>,
    /// The delegated run serving it now, or that served it last.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub run: Option<String>,
    /// The worktree SCV created for it.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub worktree: Option<WorktreeRef>,
    /// A reviewed job (`review`), whose journal holds its rounds.
    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
    pub reviewed: bool,
    /// The owner can resume it from the workbench.
    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
    pub resumable: bool,
    /// The job it was resumed as, `<session>/<job>`, once it was.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub resumed_as: Option<String>,
    /// When it was started.
    pub created_unix_seconds: u64,
    /// When its record last changed.
    pub updated_unix_seconds: u64,
}

/// One delegated run (an agent process) and what it is doing.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WorkbenchRun {
    /// Its delegation handle, such as `codex-3f9a2c`; its run log's name.
    pub handle: String,
    /// The agent, such as `codex`.
    pub agent: String,
    /// The session that started it.
    pub session: String,
    /// How SCV reaches it: `cli` (one process per turn), `acp`, or `scv`.
    pub transport: String,
    /// The conversation it serves, such as `codex-2`.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub conversation: Option<String>,
    /// The turn of that conversation it serves, or served last.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub turn: Option<u32>,
    /// The background job it serves, when it serves one.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub job: Option<String>,
    /// Every background job its turns served, oldest first: a live
    /// conversation's run serves several.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub jobs: Vec<String>,
    /// Its working directory.
    pub cwd: String,
    /// What it is doing.
    pub state: AgentState,
    /// SCV's words for that state, such as the approval it waits for or
    /// how long it has been quiet.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub detail: Option<String>,
    /// When it started.
    pub started_unix_seconds: u64,
    /// When it last reported anything.
    pub last_activity_unix_seconds: u64,
    /// Its process still runs.
    pub live: bool,
    /// A running turn of it can take a further prompt (ACP steering).
    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
    pub steerable: bool,
}

/// An approval request a background job relayed while the owner watches the
/// workbench, waiting for the owner's answer.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WorkbenchApproval {
    /// Answer with `workbench_approve` naming this ID.
    pub id: String,
    /// The session whose job asks.
    pub session: String,
    /// The job that asks.
    pub job: String,
    /// The tool the request is for, as the nested agent named it.
    pub name: String,
    /// Its risk, such as `process`.
    pub risk: String,
    /// The working directory it concerns.
    pub cwd: String,
    /// What it will do.
    pub summary: String,
    /// When it was asked.
    pub since_unix_seconds: u64,
    /// When it gets the answer the session gives unattended (a denial).
    pub expires_unix_seconds: u64,
}

/// A session whose queued prompts SCV kept when its process stopped (a
/// restart or crash), which a new session can restore.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RestorableSession {
    /// The session that stopped.
    pub id: String,
    /// Who it served.
    pub label: String,
    /// Its workspace.
    pub workspace: String,
    /// Prompts that never started.
    pub queued: u32,
    /// The start of the prompt whose turn ran when it stopped, which is
    /// never sent again by itself.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub interrupted: Option<String>,
    /// When it was last saved.
    pub saved_unix_seconds: u64,
}

/// Everything the workbench shows, at one moment.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct WorkbenchSnapshot {
    /// When it was taken.
    pub generated_unix_seconds: u64,
    /// Sessions with jobs, runs, or live state.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub sessions: Vec<WorkbenchSession>,
    /// Background jobs, newest first.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub jobs: Vec<WorkbenchJob>,
    /// Delegated runs, newest first.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub runs: Vec<WorkbenchRun>,
    /// Approval requests waiting for the owner.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub approvals: Vec<WorkbenchApproval>,
    /// Sessions whose queued prompts can be restored.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub restorable: Vec<RestorableSession>,
}

/// What one line of a run's activity log records.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RunEventKind {
    /// The run began: its agent, transport, and working directory.
    RunStarted,
    /// A turn began with a prompt.
    TurnStarted,
    /// A line of the agent's own message.
    Text,
    /// A tool call or command the agent started, as a short redacted line.
    Tool,
    /// A tool call that failed.
    ToolFailed,
    /// The agent's current plan step.
    Plan,
    /// The agent asked for approval.
    ApprovalRequested,
    /// That request was answered.
    ApprovalResolved,
    /// A prompt was added to the running turn.
    Steered,
    /// A turn ended.
    TurnEnded,
    /// The run's process ended.
    RunEnded,
    /// SCV's own note, such as why the run stopped.
    Note,
    /// The log reached its size limit; only turns, approvals, and the end
    /// are recorded after this.
    Truncated,
    /// A kind this client does not know.
    #[serde(other)]
    Unknown,
}

/// One entry of a run's activity log. Text is display-only: agent messages
/// and short tool lines with credential-like values redacted, never tool
/// output.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RunEvent {
    /// Its position in the log, from 1.
    pub seq: u64,
    /// When it happened, in Unix milliseconds.
    pub ts_ms: u64,
    /// What it records.
    pub kind: RunEventKind,
    /// Its text: the message line, tool line, prompt, summary, or note.
    #[serde(default, skip_serializing_if = "String::is_empty")]
    pub text: String,
    /// The conversation turn it belongs to.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub turn: Option<u32>,
    /// How a turn or run ended, or an approval was answered (`approved`,
    /// `denied`).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub status: Option<String>,
    /// The background job a turn serves.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub job: Option<String>,
    /// An approval request's risk.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub risk: Option<String>,
    /// Reserved for who sent a turn's prompt or answered an approval:
    /// `model`, `owner`, or `policy`. Logs omit it until a writer records
    /// that actor.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub by: Option<String>,
}

/// Part of a run's activity log, after a position.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RunLogPage {
    /// The run, by delegation handle.
    pub handle: String,
    /// Its events after the requested position, oldest first.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub events: Vec<RunEvent>,
    /// Pass as `after` to read on: the last position returned, or the
    /// requested one when nothing was new.
    pub next: u64,
    /// The run has ended, so nothing more will be logged.
    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
    pub ended: bool,
    /// More events wait beyond this page.
    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
    pub more: bool,
}

/// How `workbench_send` hands the owner's prompt to a conversation that is
/// running a turn.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SendMode {
    /// Steer the running turn when its agent can take the prompt, else
    /// queue it.
    #[default]
    Auto,
    /// Queue it behind the running turn.
    Queue,
    /// Steer the running turn, or fail when it cannot be steered.
    Steer,
}

/// What `session.start` restored from a session that stopped.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RestoredQueue {
    /// The session whose queue was restored.
    pub from: String,
    /// Queued prompts restored; the new session's queue starts paused.
    pub queued: u32,
    /// The start of the prompt that was running when the old session
    /// stopped. It is not sent again: its outcome is unknown.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub interrupted: Option<String>,
    /// Queued prompts that could not be restored, because an attachment
    /// was gone or the prompt was too long. Older servers omit it, and it
    /// is left off the wire when none were.
    #[serde(default, skip_serializing_if = "zero")]
    pub dropped: u32,
}

#[allow(
    clippy::trivially_copy_pass_by_ref,
    reason = "serde skip_serializing_if passes the field by reference"
)]
fn zero(value: &u32) -> bool {
    *value == 0
}

#[cfg(test)]
mod tests;