Skip to main content

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}