scv_protocol/background.rs
1//! Background delegation jobs, as clients see them.
2
3use std::fmt;
4
5use serde::{Deserialize, Serialize};
6
7/// Why the server started a turn on its own.
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
9pub struct TurnOrigin {
10 /// What the turn is for.
11 pub kind: OriginKind,
12 /// The background jobs this turn reports. Once it starts, the model has
13 /// seen their results.
14 #[serde(default, skip_serializing_if = "Vec::is_empty")]
15 pub jobs: Vec<String>,
16}
17
18/// What a server-started turn is for.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
20#[serde(rename_all = "snake_case")]
21#[non_exhaustive]
22pub enum OriginKind {
23 /// Finished background delegations are being reported.
24 Background,
25 /// A kind this client does not know, from a newer server.
26 #[serde(other)]
27 Unknown,
28}
29
30/// Where a background job stands, with the strings the model reads in the
31/// job tools' results.
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
33#[serde(rename_all = "snake_case")]
34#[non_exhaustive]
35pub enum JobStatus {
36 /// Still working.
37 Running,
38 /// The agent finished its task.
39 Completed,
40 /// The agent or its run failed.
41 Failed,
42 /// The agent's model refused the request.
43 Declined,
44 /// The run reached its time limit.
45 Timeout,
46 /// Stopped on request.
47 Cancelled,
48 /// A status this client does not know, from a newer server.
49 #[serde(other)]
50 Unknown,
51}
52
53impl JobStatus {
54 /// The status as the model reads it, such as `completed`.
55 pub fn as_str(self) -> &'static str {
56 match self {
57 Self::Running => "running",
58 Self::Completed => "completed",
59 Self::Failed => "failed",
60 Self::Declined => "declined",
61 Self::Timeout => "timeout",
62 Self::Cancelled => "cancelled",
63 Self::Unknown => "unknown",
64 }
65 }
66}
67
68impl fmt::Display for JobStatus {
69 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
70 formatter.write_str(self.as_str())
71 }
72}
73
74/// A background job a tool call started, or whose result the call showed
75/// the model (`tool.completed.jobs`). A session's clients keep it open while
76/// its jobs run, since closing the session cancels them.
77///
78/// A job appears once as `running`, from the `agent` call that started it,
79/// and once more with how it ended, from the `agent_wait`, `agent_status`, or
80/// `agent_cancel` call through which the model saw that result. A job whose
81/// result the model sees in a report turn is settled by that turn's
82/// [`TurnOrigin::jobs`] instead.
83#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
84pub struct JobChange {
85 /// The job's handle, such as `job-1`.
86 pub job: String,
87 /// The delegating tool: `agent`, or `agent_<name>` from SCV 0.3.0 and
88 /// older, which had one tool per agent.
89 pub tool: String,
90 /// The agent that runs the job, such as `codex`; empty from SCV 0.3.0
91 /// and older. [`JobChange::agent_name`] reads either form.
92 #[serde(default, skip_serializing_if = "String::is_empty")]
93 pub agent: String,
94 /// `running` when the call started it; otherwise how it ended.
95 pub status: JobStatus,
96 /// The first line of the delegated prompt, shortened; empty when unknown.
97 #[serde(default, skip_serializing_if = "String::is_empty")]
98 pub task: String,
99}
100
101impl JobChange {
102 /// Whether the call started the job, rather than settled it.
103 pub fn started(&self) -> bool {
104 self.status == JobStatus::Running
105 }
106
107 /// The agent that runs the job, such as `codex`, from either form.
108 pub fn agent_name(&self) -> &str {
109 job_agent(&self.agent, &self.tool)
110 }
111}
112
113/// The agent a background job runs, such as `codex`: `agent` when it is
114/// set, otherwise what follows `agent_` in the delegating `tool`, as SCV
115/// 0.3.0 and older named it (`agent_codex`), otherwise `tool` itself.
116pub fn job_agent<'a>(agent: &'a str, tool: &'a str) -> &'a str {
117 if agent.is_empty() {
118 tool.strip_prefix("agent_").unwrap_or(tool)
119 } else {
120 agent
121 }
122}