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 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}