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