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