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