vtcode_acp_client/
session.rs

1//! ACP session types and lifecycle management
2//!
3//! This module implements the session lifecycle as defined by ACP:
4//! - Session creation (session/new)
5//! - Session loading (session/load)
6//! - Prompt handling (session/prompt)
7//! - Session updates (session/update notifications)
8//!
9//! Reference: https://agentclientprotocol.com/llms.txt
10
11use serde::{Deserialize, Serialize};
12use serde_json::Value;
13use std::collections::HashMap;
14
15/// Session state enumeration
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(rename_all = "snake_case")]
18#[derive(Default)]
19pub enum SessionState {
20    /// Session created but not yet active
21    #[default]
22    Created,
23    /// Session is active and processing
24    Active,
25    /// Session is waiting for user input
26    AwaitingInput,
27    /// Session completed successfully
28    Completed,
29    /// Session was cancelled
30    Cancelled,
31    /// Session failed with error
32    Failed,
33}
34
35/// ACP Session representation
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct AcpSession {
38    /// Unique session identifier
39    pub session_id: String,
40
41    /// Current session state
42    pub state: SessionState,
43
44    /// Session creation timestamp (ISO 8601)
45    pub created_at: String,
46
47    /// Last activity timestamp (ISO 8601)
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub last_activity_at: Option<String>,
50
51    /// Session metadata
52    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
53    pub metadata: HashMap<String, Value>,
54
55    /// Turn counter for prompt/response cycles
56    #[serde(default)]
57    pub turn_count: u32,
58}
59
60impl AcpSession {
61    /// Create a new session with the given ID
62    pub fn new(session_id: impl Into<String>) -> Self {
63        Self {
64            session_id: session_id.into(),
65            state: SessionState::Created,
66            created_at: chrono::Utc::now().to_rfc3339(),
67            last_activity_at: None,
68            metadata: HashMap::new(),
69            turn_count: 0,
70        }
71    }
72
73    /// Update session state
74    pub fn set_state(&mut self, state: SessionState) {
75        self.state = state;
76        self.last_activity_at = Some(chrono::Utc::now().to_rfc3339());
77    }
78
79    /// Increment turn counter
80    pub fn increment_turn(&mut self) {
81        self.turn_count += 1;
82        self.last_activity_at = Some(chrono::Utc::now().to_rfc3339());
83    }
84}
85
86// ============================================================================
87// Session/New Request/Response
88// ============================================================================
89
90/// Parameters for session/new method
91#[derive(Debug, Clone, Serialize, Deserialize, Default)]
92pub struct SessionNewParams {
93    /// Optional session metadata
94    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
95    pub metadata: HashMap<String, Value>,
96
97    /// Optional workspace context
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub workspace: Option<WorkspaceContext>,
100
101    /// Optional model preferences
102    #[serde(skip_serializing_if = "Option::is_none")]
103    pub model_preferences: Option<ModelPreferences>,
104}
105
106/// Result of session/new method
107#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct SessionNewResult {
109    /// The created session ID
110    pub session_id: String,
111
112    /// Initial session state
113    #[serde(default)]
114    pub state: SessionState,
115}
116
117// ============================================================================
118// Session/Load Request/Response
119// ============================================================================
120
121/// Parameters for session/load method
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct SessionLoadParams {
124    /// Session ID to load
125    pub session_id: String,
126}
127
128/// Result of session/load method
129#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct SessionLoadResult {
131    /// The loaded session
132    pub session: AcpSession,
133
134    /// Conversation history (if available)
135    #[serde(default, skip_serializing_if = "Vec::is_empty")]
136    pub history: Vec<ConversationTurn>,
137}
138
139// ============================================================================
140// Session/Prompt Request/Response
141// ============================================================================
142
143/// Parameters for session/prompt method
144#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct SessionPromptParams {
146    /// Session ID
147    pub session_id: String,
148
149    /// Prompt content (can be text, images, etc.)
150    pub content: Vec<PromptContent>,
151
152    /// Optional turn-specific metadata
153    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
154    pub metadata: HashMap<String, Value>,
155}
156
157/// Prompt content types
158#[derive(Debug, Clone, Serialize, Deserialize)]
159#[serde(tag = "type", rename_all = "snake_case")]
160pub enum PromptContent {
161    /// Plain text content
162    Text {
163        /// The text content
164        text: String,
165    },
166
167    /// Image content (base64 or URL)
168    Image {
169        /// Image data (base64) or URL
170        data: String,
171        /// MIME type (e.g., "image/png")
172        mime_type: String,
173        /// Whether data is a URL (false = base64)
174        #[serde(default)]
175        is_url: bool,
176    },
177
178    /// Embedded context (file contents, etc.)
179    Context {
180        /// Context identifier/path
181        path: String,
182        /// Context content
183        content: String,
184        /// Language hint for syntax highlighting
185        #[serde(skip_serializing_if = "Option::is_none")]
186        language: Option<String>,
187    },
188}
189
190impl PromptContent {
191    /// Create text content
192    pub fn text(text: impl Into<String>) -> Self {
193        Self::Text { text: text.into() }
194    }
195
196    /// Create context content
197    pub fn context(path: impl Into<String>, content: impl Into<String>) -> Self {
198        Self::Context {
199            path: path.into(),
200            content: content.into(),
201            language: None,
202        }
203    }
204}
205
206/// Result of session/prompt method
207#[derive(Debug, Clone, Serialize, Deserialize)]
208pub struct SessionPromptResult {
209    /// Turn ID for this prompt/response cycle
210    pub turn_id: String,
211
212    /// Final response content (may be streamed via notifications first)
213    #[serde(skip_serializing_if = "Option::is_none")]
214    pub response: Option<String>,
215
216    /// Tool calls made during this turn
217    #[serde(default, skip_serializing_if = "Vec::is_empty")]
218    pub tool_calls: Vec<ToolCallRecord>,
219
220    /// Turn completion status
221    pub status: TurnStatus,
222}
223
224/// Turn completion status
225#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
226#[serde(rename_all = "snake_case")]
227pub enum TurnStatus {
228    /// Turn completed successfully
229    Completed,
230    /// Turn was cancelled
231    Cancelled,
232    /// Turn failed with error
233    Failed,
234    /// Turn requires user input (e.g., permission approval)
235    AwaitingInput,
236}
237
238// ============================================================================
239// Session/RequestPermission (Client Method)
240// ============================================================================
241
242/// Parameters for session/request_permission method (client callable by agent)
243#[derive(Debug, Clone, Serialize, Deserialize)]
244#[serde(rename_all = "camelCase")]
245pub struct RequestPermissionParams {
246    /// Session ID
247    pub session_id: String,
248
249    /// Tool call requiring permission
250    pub tool_call: ToolCallRecord,
251
252    /// Available permission options
253    pub options: Vec<PermissionOption>,
254}
255
256/// A permission option presented to the user
257#[derive(Debug, Clone, Serialize, Deserialize)]
258pub struct PermissionOption {
259    /// Option ID
260    pub id: String,
261
262    /// Display label
263    pub label: String,
264
265    /// Detailed description
266    #[serde(skip_serializing_if = "Option::is_none")]
267    pub description: Option<String>,
268}
269
270/// Result of session/request_permission
271#[derive(Debug, Clone, Serialize, Deserialize)]
272#[serde(tag = "outcome", rename_all = "snake_case")]
273pub enum RequestPermissionResult {
274    /// User selected an option
275    Selected {
276        /// The selected option ID
277        option_id: String,
278    },
279    /// User cancelled the request
280    Cancelled,
281}
282
283// ============================================================================
284// Session/Cancel Request
285// ============================================================================
286
287/// Parameters for session/cancel method
288#[derive(Debug, Clone, Serialize, Deserialize)]
289pub struct SessionCancelParams {
290    /// Session ID
291    pub session_id: String,
292
293    /// Optional turn ID to cancel (if not provided, cancels current turn)
294    #[serde(skip_serializing_if = "Option::is_none")]
295    pub turn_id: Option<String>,
296}
297
298// ============================================================================
299// Session/Update Notification (Streaming)
300// ============================================================================
301
302/// Session update notification payload
303#[derive(Debug, Clone, Serialize, Deserialize)]
304pub struct SessionUpdateNotification {
305    /// Session ID
306    pub session_id: String,
307
308    /// Turn ID this update belongs to
309    pub turn_id: String,
310
311    /// Update type
312    #[serde(flatten)]
313    pub update: SessionUpdate,
314}
315
316/// Session update types
317#[derive(Debug, Clone, Serialize, Deserialize)]
318#[serde(tag = "update_type", rename_all = "snake_case")]
319pub enum SessionUpdate {
320    /// Text delta (streaming response)
321    MessageDelta {
322        /// Incremental text content
323        delta: String,
324    },
325
326    /// Tool call started
327    ToolCallStart {
328        /// Tool call details
329        tool_call: ToolCallRecord,
330    },
331
332    /// Tool call completed
333    ToolCallEnd {
334        /// Tool call ID
335        tool_call_id: String,
336        /// Tool result
337        result: Value,
338    },
339
340    /// Turn completed
341    TurnComplete {
342        /// Final status
343        status: TurnStatus,
344    },
345
346    /// Error occurred
347    Error {
348        /// Error code
349        code: String,
350        /// Error message
351        message: String,
352    },
353}
354
355// ============================================================================
356// Supporting Types
357// ============================================================================
358
359/// Workspace context for session initialization
360#[derive(Debug, Clone, Serialize, Deserialize)]
361pub struct WorkspaceContext {
362    /// Workspace root path
363    pub root_path: String,
364
365    /// Workspace name
366    #[serde(skip_serializing_if = "Option::is_none")]
367    pub name: Option<String>,
368
369    /// Active file paths
370    #[serde(default, skip_serializing_if = "Vec::is_empty")]
371    pub active_files: Vec<String>,
372}
373
374/// Model preferences for session
375#[derive(Debug, Clone, Serialize, Deserialize)]
376pub struct ModelPreferences {
377    /// Preferred model ID
378    #[serde(skip_serializing_if = "Option::is_none")]
379    pub model_id: Option<String>,
380
381    /// Temperature setting
382    #[serde(skip_serializing_if = "Option::is_none")]
383    pub temperature: Option<f32>,
384
385    /// Max tokens
386    #[serde(skip_serializing_if = "Option::is_none")]
387    pub max_tokens: Option<u32>,
388}
389
390/// Record of a tool call
391#[derive(Debug, Clone, Serialize, Deserialize)]
392pub struct ToolCallRecord {
393    /// Unique tool call ID
394    pub id: String,
395
396    /// Tool name
397    pub name: String,
398
399    /// Tool arguments
400    pub arguments: Value,
401
402    /// Tool result (if completed)
403    #[serde(skip_serializing_if = "Option::is_none")]
404    pub result: Option<Value>,
405
406    /// Timestamp
407    pub timestamp: String,
408}
409
410/// A single turn in the conversation
411#[derive(Debug, Clone, Serialize, Deserialize)]
412pub struct ConversationTurn {
413    /// Turn ID
414    pub turn_id: String,
415
416    /// User prompt
417    pub prompt: Vec<PromptContent>,
418
419    /// Agent response
420    #[serde(skip_serializing_if = "Option::is_none")]
421    pub response: Option<String>,
422
423    /// Tool calls made during this turn
424    #[serde(default, skip_serializing_if = "Vec::is_empty")]
425    pub tool_calls: Vec<ToolCallRecord>,
426
427    /// Turn timestamp
428    pub timestamp: String,
429}
430
431#[cfg(test)]
432mod tests {
433    use super::*;
434    use serde_json::json;
435
436    #[test]
437    fn test_session_new_params() {
438        let params = SessionNewParams::default();
439        let json = serde_json::to_value(&params).unwrap();
440        assert_eq!(json, json!({}));
441    }
442
443    #[test]
444    fn test_prompt_content_text() {
445        let content = PromptContent::text("Hello, world!");
446        let json = serde_json::to_value(&content).unwrap();
447        assert_eq!(json["type"], "text");
448        assert_eq!(json["text"], "Hello, world!");
449    }
450
451    #[test]
452    fn test_session_update_message_delta() {
453        let update = SessionUpdate::MessageDelta {
454            delta: "Hello".to_string(),
455        };
456        let json = serde_json::to_value(&update).unwrap();
457        assert_eq!(json["update_type"], "message_delta");
458        assert_eq!(json["delta"], "Hello");
459    }
460
461    #[test]
462    fn test_session_state_transitions() {
463        let mut session = AcpSession::new("test-session");
464        assert_eq!(session.state, SessionState::Created);
465
466        session.set_state(SessionState::Active);
467        assert_eq!(session.state, SessionState::Active);
468        assert!(session.last_activity_at.is_some());
469    }
470}