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}