1use hashbrown::HashMap;
12use serde::{Deserialize, Serialize};
13use serde_json::Value;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(rename_all = "snake_case")]
18#[derive(Default)]
19pub enum SessionState {
20 #[default]
22 Created,
23 Active,
25 AwaitingInput,
27 Completed,
29 Cancelled,
31 Failed,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct AcpSession {
38 pub session_id: String,
40
41 pub state: SessionState,
43
44 pub created_at: String,
46
47 #[serde(skip_serializing_if = "Option::is_none")]
49 pub last_activity_at: Option<String>,
50
51 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
53 pub metadata: HashMap<String, Value>,
54
55 #[serde(default)]
57 pub turn_count: u32,
58}
59
60impl AcpSession {
61 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 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 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#[derive(Debug, Clone, Serialize, Deserialize, Default)]
92pub struct SessionNewParams {
93 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
95 pub metadata: HashMap<String, Value>,
96
97 #[serde(skip_serializing_if = "Option::is_none")]
99 pub workspace: Option<WorkspaceContext>,
100
101 #[serde(skip_serializing_if = "Option::is_none")]
103 pub model_preferences: Option<ModelPreferences>,
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct SessionNewResult {
109 pub session_id: String,
111
112 #[serde(default)]
114 pub state: SessionState,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct SessionLoadParams {
124 pub session_id: String,
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct SessionLoadResult {
131 pub session: AcpSession,
133
134 #[serde(default, skip_serializing_if = "Vec::is_empty")]
136 pub history: Vec<ConversationTurn>,
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct SessionPromptParams {
146 pub session_id: String,
148
149 pub content: Vec<PromptContent>,
151
152 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
154 pub metadata: HashMap<String, Value>,
155}
156
157#[derive(Debug, Clone, Serialize, Deserialize)]
159#[serde(tag = "type", rename_all = "snake_case")]
160pub enum PromptContent {
161 Text {
163 text: String,
165 },
166
167 Image {
169 data: String,
171 mime_type: String,
173 #[serde(default)]
175 is_url: bool,
176 },
177
178 Context {
180 path: String,
182 content: String,
184 #[serde(skip_serializing_if = "Option::is_none")]
186 language: Option<String>,
187 },
188}
189
190impl PromptContent {
191 pub fn text(text: impl Into<String>) -> Self {
193 Self::Text { text: text.into() }
194 }
195
196 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#[derive(Debug, Clone, Serialize, Deserialize)]
208pub struct SessionPromptResult {
209 pub turn_id: String,
211
212 #[serde(skip_serializing_if = "Option::is_none")]
214 pub response: Option<String>,
215
216 #[serde(default, skip_serializing_if = "Vec::is_empty")]
218 pub tool_calls: Vec<ToolCallRecord>,
219
220 pub status: TurnStatus,
222}
223
224#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
226#[serde(rename_all = "snake_case")]
227pub enum TurnStatus {
228 Completed,
230 Cancelled,
232 Failed,
234 AwaitingInput,
236}
237
238#[derive(Debug, Clone, Serialize, Deserialize)]
244#[serde(rename_all = "camelCase")]
245pub struct RequestPermissionParams {
246 pub session_id: String,
248
249 pub tool_call: ToolCallRecord,
251
252 pub options: Vec<PermissionOption>,
254}
255
256#[derive(Debug, Clone, Serialize, Deserialize)]
258pub struct PermissionOption {
259 pub id: String,
261
262 pub label: String,
264
265 #[serde(skip_serializing_if = "Option::is_none")]
267 pub description: Option<String>,
268}
269
270#[derive(Debug, Clone, Serialize, Deserialize)]
272#[serde(tag = "outcome", rename_all = "snake_case")]
273pub enum RequestPermissionResult {
274 Selected {
276 option_id: String,
278 },
279 Cancelled,
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize)]
289pub struct SessionCancelParams {
290 pub session_id: String,
292
293 #[serde(skip_serializing_if = "Option::is_none")]
295 pub turn_id: Option<String>,
296}
297
298#[derive(Debug, Clone, Serialize, Deserialize)]
304pub struct SessionUpdateNotification {
305 pub session_id: String,
307
308 pub turn_id: String,
310
311 #[serde(flatten)]
313 pub update: SessionUpdate,
314}
315
316#[derive(Debug, Clone, Serialize, Deserialize)]
318#[serde(tag = "update_type", rename_all = "snake_case")]
319pub enum SessionUpdate {
320 MessageDelta {
322 delta: String,
324 },
325
326 ToolCallStart {
328 tool_call: ToolCallRecord,
330 },
331
332 ToolCallEnd {
334 tool_call_id: String,
336 result: Value,
338 },
339
340 TurnComplete {
342 status: TurnStatus,
344 },
345
346 Error {
348 code: String,
350 message: String,
352 },
353
354 ServerRequest {
359 request: ToolExecutionRequest,
361 },
362}
363
364#[derive(Debug, Clone, Serialize, Deserialize)]
370pub struct WorkspaceContext {
371 pub root_path: String,
373
374 #[serde(skip_serializing_if = "Option::is_none")]
376 pub name: Option<String>,
377
378 #[serde(default, skip_serializing_if = "Vec::is_empty")]
380 pub active_files: Vec<String>,
381}
382
383#[derive(Debug, Clone, Serialize, Deserialize)]
385pub struct ModelPreferences {
386 #[serde(skip_serializing_if = "Option::is_none")]
388 pub model_id: Option<String>,
389
390 #[serde(skip_serializing_if = "Option::is_none")]
392 pub temperature: Option<f32>,
393
394 #[serde(skip_serializing_if = "Option::is_none")]
396 pub max_tokens: Option<u32>,
397}
398
399#[derive(Debug, Clone, Serialize, Deserialize)]
401pub struct ToolCallRecord {
402 pub id: String,
404
405 pub name: String,
407
408 pub arguments: Value,
410
411 #[serde(skip_serializing_if = "Option::is_none")]
413 pub result: Option<Value>,
414
415 pub timestamp: String,
417}
418
419#[derive(Debug, Clone, Serialize, Deserialize)]
425pub struct ToolExecutionRequest {
426 pub request_id: String,
428 pub tool_call: ToolCallRecord,
430}
431
432#[derive(Debug, Clone, Serialize, Deserialize)]
434pub struct ToolExecutionResult {
435 pub request_id: String,
437 pub tool_call_id: String,
439 pub output: Value,
441 pub success: bool,
443 #[serde(skip_serializing_if = "Option::is_none")]
445 pub error: Option<String>,
446}
447
448#[derive(Debug, Clone, Serialize, Deserialize)]
450pub struct ServerRequestNotification {
451 pub session_id: String,
453 pub request: ToolExecutionRequest,
455}
456
457#[derive(Debug, Clone, Serialize, Deserialize)]
459pub struct ConversationTurn {
460 pub turn_id: String,
462
463 pub prompt: Vec<PromptContent>,
465
466 #[serde(skip_serializing_if = "Option::is_none")]
468 pub response: Option<String>,
469
470 #[serde(default, skip_serializing_if = "Vec::is_empty")]
472 pub tool_calls: Vec<ToolCallRecord>,
473
474 pub timestamp: String,
476}
477
478#[cfg(test)]
479mod tests {
480 use super::*;
481 use serde_json::json;
482
483 #[test]
484 fn test_session_new_params() {
485 let params = SessionNewParams::default();
486 let json = serde_json::to_value(¶ms).unwrap();
487 assert_eq!(json, json!({}));
488 }
489
490 #[test]
491 fn test_prompt_content_text() {
492 let content = PromptContent::text("Hello, world!");
493 let json = serde_json::to_value(&content).unwrap();
494 assert_eq!(json["type"], "text");
495 assert_eq!(json["text"], "Hello, world!");
496 }
497
498 #[test]
499 fn test_session_update_message_delta() {
500 let update = SessionUpdate::MessageDelta {
501 delta: "Hello".to_string(),
502 };
503 let json = serde_json::to_value(&update).unwrap();
504 assert_eq!(json["update_type"], "message_delta");
505 assert_eq!(json["delta"], "Hello");
506 }
507
508 #[test]
509 fn test_session_state_transitions() {
510 let mut session = AcpSession::new("test-session");
511 assert_eq!(session.state, SessionState::Created);
512
513 session.set_state(SessionState::Active);
514 assert_eq!(session.state, SessionState::Active);
515 assert!(session.last_activity_at.is_some());
516 }
517
518 #[test]
519 fn server_request_update_serializes_correctly() {
520 let tool_call = ToolCallRecord {
521 id: "tc-1".to_string(),
522 name: "unified_search".to_string(),
523 arguments: json!({"query": "fn main"}),
524 result: None,
525 timestamp: "2025-01-01T00:00:00Z".to_string(),
526 };
527 let request = ToolExecutionRequest {
528 request_id: "req-1".to_string(),
529 tool_call,
530 };
531 let update = SessionUpdate::ServerRequest { request };
532 let json = serde_json::to_value(&update).unwrap();
533 assert_eq!(json["update_type"], "server_request");
534 assert_eq!(json["request"]["request_id"], "req-1");
535 }
536
537 #[test]
538 fn tool_execution_result_success_serializes() {
539 let result = ToolExecutionResult {
540 request_id: "req-1".to_string(),
541 tool_call_id: "tc-1".to_string(),
542 output: json!({"matches": []}),
543 success: true,
544 error: None,
545 };
546 let json = serde_json::to_value(&result).unwrap();
547 assert_eq!(json["success"], true);
548 assert!(json.get("error").is_none());
549 }
550
551 #[test]
552 fn tool_execution_result_failure_includes_error() {
553 let result = ToolExecutionResult {
554 request_id: "req-1".to_string(),
555 tool_call_id: "tc-1".to_string(),
556 output: Value::Null,
557 success: false,
558 error: Some("permission denied".to_string()),
559 };
560 let json = serde_json::to_value(&result).unwrap();
561 assert_eq!(json["success"], false);
562 assert_eq!(json["error"], "permission denied");
563 }
564}