Skip to main content

scv_protocol/
client.rs

1//! [`ClientMessage`]: everything a client sends.
2
3use serde::{Deserialize, Serialize};
4
5use crate::{Attachment, DaemonCommand, PeerInfo};
6#[cfg(doc)]
7use crate::{CHAT_ATTACH_TOOL, MAX_CHANNEL_NAME_BYTES, MAX_TURN_ATTACHMENTS};
8
9/// A message from client to server. Serialized as one JSON object per line,
10/// tagged by `type` (such as `turn.start`).
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
12#[serde(tag = "type")]
13pub enum ClientMessage {
14    /// Control the daemon itself (status, reload, channels, delegations,
15    /// restarts). Only the daemon socket accepts it.
16    #[serde(rename = "daemon.control")]
17    DaemonControl {
18        /// Chosen by the client; the events that answer this message carry it.
19        request_id: String,
20        /// What to do.
21        command: DaemonCommand,
22    },
23    /// The first message on every connection.
24    #[serde(rename = "initialize")]
25    Initialize {
26        /// Chosen by the client; the events that answer this message carry it.
27        request_id: String,
28        /// The [`PROTOCOL_VERSION`](crate::PROTOCOL_VERSION) the client speaks.
29        protocol_version: u32,
30        /// Who is connecting.
31        client: PeerInfo,
32    },
33    /// Start a session in a workspace. Each connection has at most one.
34    #[serde(rename = "session.start")]
35    SessionStart {
36        /// Chosen by the client; the events that answer this message carry it.
37        request_id: String,
38        /// The workspace directory.
39        cwd: String,
40        /// Provider profile to use instead of the configured default.
41        #[serde(default, skip_serializing_if = "Option::is_none")]
42        provider: Option<String>,
43        /// Model to use instead of the configured default.
44        #[serde(default, skip_serializing_if = "Option::is_none")]
45        model: Option<String>,
46        /// Provider endpoint to use instead of the configured one.
47        #[serde(default, skip_serializing_if = "Option::is_none")]
48        base_url: Option<String>,
49        /// Start the session without tools.
50        #[serde(default, skip_serializing_if = "Option::is_none")]
51        no_tools: Option<bool>,
52        /// Delegation depth of the client, when it is itself a delegated
53        /// agent (such as a nested SCV). Tools started from the session count
54        /// from it, so the depth limit holds across processes.
55        #[serde(default, skip_serializing_if = "Option::is_none")]
56        delegation_depth: Option<u32>,
57        /// The chat channel this session answers on, as its users name it
58        /// (such as `WeChat` or `Feishu`). The user reads short plain-text
59        /// replies there and never sees tool calls, so the server tells the
60        /// model; a chat client also delivers files the model attaches to
61        /// its reply, so a session with tools offers [`CHAT_ATTACH_TOOL`].
62        /// At most [`MAX_CHANNEL_NAME_BYTES`], without control characters.
63        #[serde(default, skip_serializing_if = "Option::is_none")]
64        channel: Option<String>,
65        /// The client approves every approval request of this session
66        /// without asking anyone. Background jobs, which outlive the turn
67        /// that could carry their requests, then get the same answer;
68        /// otherwise they get only what the approval policy grants unasked.
69        #[serde(default, skip_serializing_if = "Option::is_none")]
70        auto_approve: Option<bool>,
71    },
72    /// Attach to an existing session. Not supported: sessions belong to the
73    /// connection that started them.
74    #[serde(rename = "session.attach")]
75    SessionAttach {
76        /// Chosen by the client; the events that answer this message carry it.
77        request_id: String,
78        /// The session, as `session.started` named it.
79        session_id: String,
80        /// The workspace directory.
81        cwd: String,
82    },
83    /// Send a prompt. It starts a turn at once, or queues behind the running
84    /// one.
85    #[serde(rename = "turn.start")]
86    TurnStart {
87        /// Chosen by the client; the events that answer this message carry it.
88        request_id: String,
89        /// The session, as `session.started` named it.
90        session_id: String,
91        /// The user's text.
92        prompt: String,
93        /// Files that come with the prompt, at most [`MAX_TURN_ATTACHMENTS`].
94        /// The server lists them for the model and shows it images directly
95        /// when the model accepts image input.
96        #[serde(default, skip_serializing_if = "Vec::is_empty")]
97        attachments: Vec<Attachment>,
98    },
99    /// Replace the text of a queued prompt.
100    #[serde(rename = "queue.update")]
101    QueueUpdate {
102        /// Chosen by the client; the events that answer this message carry it.
103        request_id: String,
104        /// The session, as `session.started` named it.
105        session_id: String,
106        /// The queued prompt.
107        queue_id: String,
108        /// The entry's current revision; a stale one is refused.
109        revision: u64,
110        /// The user's text.
111        prompt: String,
112    },
113    /// Reorder a queued prompt.
114    #[serde(rename = "queue.move")]
115    QueueMove {
116        /// Chosen by the client; the events that answer this message carry it.
117        request_id: String,
118        /// The session, as `session.started` named it.
119        session_id: String,
120        /// The queued prompt.
121        queue_id: String,
122        /// The entry's current revision; a stale one is refused.
123        revision: u64,
124        /// Move before this entry; `None` moves it to the end.
125        before_queue_id: Option<String>,
126    },
127    /// Drop a queued prompt.
128    #[serde(rename = "queue.remove")]
129    QueueRemove {
130        /// Chosen by the client; the events that answer this message carry it.
131        request_id: String,
132        /// The session, as `session.started` named it.
133        session_id: String,
134        /// The queued prompt.
135        queue_id: String,
136        /// The entry's current revision; a stale one is refused.
137        revision: u64,
138    },
139    /// Hold or release the queue; a running turn is not affected.
140    #[serde(rename = "session.pause")]
141    SessionPause {
142        /// Chosen by the client; the events that answer this message carry it.
143        request_id: String,
144        /// The session, as `session.started` named it.
145        session_id: String,
146        /// Whether queued prompts wait instead of starting.
147        paused: bool,
148    },
149    /// Stop the running turn.
150    #[serde(rename = "turn.cancel")]
151    TurnCancel {
152        /// Chosen by the client; the events that answer this message carry it.
153        request_id: String,
154        /// The session, as `session.started` named it.
155        session_id: String,
156        /// The turn to cancel.
157        turn_id: String,
158    },
159    /// Answer an `approval.requested` event.
160    #[serde(rename = "approval.resolve")]
161    ApprovalResolve {
162        /// Chosen by the client; the events that answer this message carry it.
163        request_id: String,
164        /// The session, as `session.started` named it.
165        session_id: String,
166        /// The approval request being answered.
167        approval_id: String,
168        /// Whether the call may run.
169        approved: bool,
170    },
171    /// Forget the session's history and queue.
172    #[serde(rename = "session.clear")]
173    SessionClear {
174        /// Chosen by the client; the events that answer this message carry it.
175        request_id: String,
176        /// The session, as `session.started` named it.
177        session_id: String,
178    },
179}
180
181impl ClientMessage {
182    /// The `initialize` request every client sends first, declaring this
183    /// release's [`PROTOCOL_VERSION`](crate::PROTOCOL_VERSION).
184    pub fn initialize(request_id: impl Into<String>, client_name: impl Into<String>) -> Self {
185        Self::Initialize {
186            request_id: request_id.into(),
187            protocol_version: crate::PROTOCOL_VERSION,
188            client: PeerInfo {
189                name: client_name.into(),
190                version: env!("CARGO_PKG_VERSION").into(),
191            },
192        }
193    }
194
195    /// The client-chosen ID that answering events carry.
196    pub fn request_id(&self) -> &str {
197        match self {
198            Self::Initialize { request_id, .. }
199            | Self::DaemonControl { request_id, .. }
200            | Self::SessionStart { request_id, .. }
201            | Self::SessionAttach { request_id, .. }
202            | Self::TurnStart { request_id, .. }
203            | Self::QueueUpdate { request_id, .. }
204            | Self::QueueMove { request_id, .. }
205            | Self::QueueRemove { request_id, .. }
206            | Self::SessionPause { request_id, .. }
207            | Self::TurnCancel { request_id, .. }
208            | Self::ApprovalResolve { request_id, .. }
209            | Self::SessionClear { request_id, .. } => request_id,
210        }
211    }
212}