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