Skip to main content

vtcode_desktop_protocol/
lib.rs

1//! Versioned JSON-RPC messages shared by the VT Code desktop app and service.
2//!
3//! The protocol deliberately carries the canonical [`VersionedThreadEvent`]
4//! instead of defining a second runtime event model.
5
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8use vtcode_exec_events::VersionedThreadEvent;
9
10pub mod slash_command;
11
12/// Current protocol version supported by the desktop client and service.
13pub const PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion { major: 1, minor: 24 };
14
15/// Largest permitted JSON-RPC line, excluding its line ending.
16pub const MAX_FRAME_BYTES: usize = 1024 * 1024;
17
18/// Capacity used by host-owned event and command queues.
19pub const DEFAULT_QUEUE_CAPACITY: usize = 128;
20
21/// A comparable protocol version.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
23pub struct ProtocolVersion {
24    /// Incompatible wire-contract changes increment the major version.
25    pub major: u16,
26    /// Compatible additive changes increment the minor version.
27    pub minor: u16,
28}
29
30/// A client's acceptable range for protocol negotiation.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
32#[serde(rename_all = "camelCase")]
33pub struct ProtocolRange {
34    /// Lowest accepted version.
35    pub minimum: ProtocolVersion,
36    /// Highest accepted version.
37    pub maximum: ProtocolVersion,
38}
39
40impl ProtocolRange {
41    /// Select the highest compatible version in this range.
42    pub fn negotiate(self, server: ProtocolVersion) -> Result<ProtocolVersion, ProtocolError> {
43        if self.minimum.major != self.maximum.major || server.major != self.minimum.major {
44            return Err(ProtocolError::IncompatibleVersion { client: self, server });
45        }
46
47        if server < self.minimum || server > self.maximum {
48            return Err(ProtocolError::IncompatibleVersion { client: self, server });
49        }
50
51        Ok(server)
52    }
53}
54
55/// JSON-RPC request id. `null` is intentionally not accepted for requests.
56#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
57#[serde(untagged)]
58pub enum RequestId {
59    /// String request identifier.
60    String(String),
61    /// Integer request identifier.
62    Number(i64),
63}
64
65/// A JSON-RPC request received from the desktop client.
66#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
67pub struct Request {
68    /// JSON-RPC version marker.
69    pub jsonrpc: String,
70    /// Request correlation id.
71    pub id: RequestId,
72    /// Method name.
73    pub method: String,
74    /// Typed by the method handler.
75    #[serde(default)]
76    pub params: Value,
77}
78
79/// A JSON-RPC notification received from or sent to the desktop client.
80#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
81pub struct Notification {
82    /// JSON-RPC version marker.
83    pub jsonrpc: String,
84    /// Method name.
85    pub method: String,
86    /// Typed by the method handler.
87    #[serde(default)]
88    pub params: Value,
89}
90
91/// A successful JSON-RPC response.
92#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
93pub struct Response {
94    /// JSON-RPC version marker.
95    pub jsonrpc: String,
96    /// Request correlation id.
97    pub id: RequestId,
98    /// Result payload.
99    pub result: Value,
100}
101
102/// A failed JSON-RPC response.
103#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
104pub struct ErrorResponse {
105    /// JSON-RPC version marker.
106    pub jsonrpc: String,
107    /// Request correlation id; omitted for malformed requests without an id.
108    pub id: Option<RequestId>,
109    /// Error payload.
110    pub error: RpcError,
111}
112
113/// Standard JSON-RPC error body.
114#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
115pub struct RpcError {
116    /// JSON-RPC error code.
117    pub code: i32,
118    /// Sanitized error message.
119    pub message: String,
120    /// Optional structured data.
121    #[serde(skip_serializing_if = "Option::is_none")]
122    pub data: Option<Value>,
123}
124
125/// Client initialization request.
126#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
127#[serde(rename_all = "camelCase")]
128pub struct InitializeParams {
129    /// Range of protocol versions supported by the client.
130    pub protocol: ProtocolRange,
131    /// Client application name.
132    pub client_name: String,
133    /// Client application version.
134    pub client_version: String,
135}
136
137/// Successful initialization response.
138#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
139#[serde(rename_all = "camelCase")]
140pub struct InitializeResult {
141    /// Negotiated protocol version.
142    pub protocol: ProtocolVersion,
143    /// Canonical selected workspace path.
144    pub workspace: String,
145    /// Server version.
146    pub server_version: String,
147    /// CLI slash commands shown by the native composer.
148    #[serde(default)]
149    pub slash_commands: Vec<SlashCommandInfo>,
150}
151
152/// Display metadata for a slash command exposed by the CLI chat input.
153#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
154#[serde(rename_all = "camelCase")]
155pub struct SlashCommandInfo {
156    /// Command name without the leading slash.
157    pub name: String,
158    /// Short description shown in composer completion.
159    pub description: String,
160}
161
162/// Canonical runtime event notification.
163#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
164#[serde(rename_all = "camelCase")]
165pub struct ThreadEventNotification {
166    /// Session owning the event.
167    pub session_id: String,
168    /// Turn identity when the event is turn scoped.
169    pub turn_id: Option<String>,
170    /// Authoritative, schema-versioned runtime event.
171    pub event: VersionedThreadEvent,
172}
173
174/// Ephemeral active primary-agent display state for one live session.
175#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
176#[serde(rename_all = "camelCase")]
177pub struct PrimaryAgentChanged {
178    /// Session owning the active-agent state.
179    pub session_id: String,
180    /// Sanitized active-agent label, or `None` to use the configured fallback.
181    pub display_name: Option<String>,
182}
183
184pub mod method;
185
186/// Error raised by protocol framing, negotiation, or identity validation.
187#[derive(Debug, Clone, PartialEq, Eq)]
188pub enum ProtocolError {
189    /// A frame exceeds the configured byte limit.
190    FrameTooLarge {
191        /// Maximum accepted frame size in bytes.
192        limit: usize,
193    },
194    /// The client and service do not share a supported protocol version.
195    IncompatibleVersion {
196        /// Client-supported range.
197        client: ProtocolRange,
198        /// Server version.
199        server: ProtocolVersion,
200    },
201    /// Interaction does not exactly match the active session, turn, and id.
202    StaleInteraction,
203    /// Interaction was already resolved or never registered.
204    UnknownInteraction,
205}
206
207impl std::fmt::Display for ProtocolError {
208    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
209        match self {
210            Self::FrameTooLarge { limit } => write!(formatter, "JSON-RPC frame exceeds {limit} bytes"),
211            Self::IncompatibleVersion { client, server } => write!(
212                formatter,
213                "incompatible desktop protocol versions: client {}.{}..={}.{}, server {}.{}",
214                client.minimum.major,
215                client.minimum.minor,
216                client.maximum.major,
217                client.maximum.minor,
218                server.major,
219                server.minor
220            ),
221            Self::StaleInteraction => {
222                formatter.write_str("interaction response does not match the active session and turn")
223            }
224            Self::UnknownInteraction => formatter.write_str("interaction is no longer pending"),
225        }
226    }
227}
228
229impl std::error::Error for ProtocolError {}
230
231pub mod interaction_registry;
232pub use interaction_registry::InteractionRegistry;
233
234pub mod frames;
235pub use frames::{read_frame, write_frame};
236
237/// Build a JSON-RPC success response.
238pub fn success_response(id: RequestId, result: impl Serialize) -> Result<Response, serde_json::Error> {
239    Ok(Response {
240        jsonrpc: "2.0".to_string(),
241        id,
242        result: serde_json::to_value(result)?,
243    })
244}
245
246/// Build a JSON-RPC error response with a sanitized public message.
247pub fn error_response(id: Option<RequestId>, code: i32, message: impl Into<String>) -> ErrorResponse {
248    ErrorResponse {
249        jsonrpc: "2.0".to_string(),
250        id,
251        error: RpcError { code, message: message.into(), data: None },
252    }
253}
254
255pub mod live_events;
256pub use live_events::{LiveEventReceiver, LiveEventSendError, LiveEventSender, live_event_channel};
257
258pub mod secret;
259pub use secret::{SecretStoreInfo, SecretStoreParams, SecretStoreResult};
260
261pub mod storage;
262pub use storage::{SessionDeleteResult, StorageSummary, WorktreeEntry, WorktreeRemoveParams, WorktreeRemoveResult};
263
264pub mod composer;
265pub use composer::{ComposerResource, ComposerResourceKind};
266
267pub mod config;
268pub use config::{
269    ConfigOpenFileParams, ConfigSetting, ConfigSettingsSnapshot, ConfigSettingsWriteParams, ConfigSettingsWriteResult,
270    ConfigTarget, ConfigTargetVersion, ConfigurationSnapshot, ModelCapabilities, ModelInfo, ModelSelectParams,
271    SharedUiPreferences,
272};
273
274pub mod interaction;
275pub use interaction::{InteractionChoice, InteractionKind, InteractionRequest, InteractionResponse, InteractionStep};
276
277pub mod review;
278pub use review::{
279    MAX_MCP_APP_BODY_BYTES, McpAppReadParams, McpAppReadResult, WorkspaceReviewParams, WorkspaceReviewResult,
280};
281
282pub mod session;
283pub use session::{
284    AgentModeInfo, MAX_SESSION_HISTORY_PAGE_MESSAGES, MAX_SESSION_OUTPUT_PAGE_BYTES, PermissionMode,
285    RESTORED_OUTPUT_TRUNCATION_SUFFIX, SessionCreateParams, SessionHistoryMessage, SessionHistoryPage,
286    SessionHistoryParams, SessionIdParams, SessionLoadResult, SessionOptionsSelectParams, SessionOptionsSnapshot,
287    SessionOutputCursor, SessionOutputPage, SessionOutputReadParams, SessionSummary, ThemePreferenceSelectParams,
288};
289
290pub mod turn;
291pub use turn::{
292    MAX_QUEUE_PREVIEW_CHARS, MAX_QUEUE_STATUS_PREVIEWS, SESSION_START_TURN_ID, STARTUP_INTERACTION_PENDING_ERROR,
293    TurnAccepted, TurnAttachment, TurnCancelParams, TurnQueueStatus, TurnQueueTakeLatestParams,
294    TurnQueueTakeLatestResult, TurnSubmitParams, queue_preview_label,
295};
296
297pub mod update;
298pub use update::UpdateStatusSnapshot;
299
300pub mod webmcp;
301pub use webmcp::{WebmcpBridgeStatus, WebmcpPairParams, WebmcpPairResult};
302
303#[cfg(test)]
304mod tests;