Skip to main content

claude_codes/
messages.rs

1//! Message types for the Claude Code protocol
2
3use crate::types::*;
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6
7/// Base message trait for all protocol messages
8pub trait Message: Serialize + for<'de> Deserialize<'de> {
9    fn message_type(&self) -> &str;
10}
11
12/// Request message sent to Claude Code
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct Request {
15    #[serde(rename = "type")]
16    pub message_type: String,
17
18    pub id: Id,
19
20    #[serde(skip_serializing_if = "Option::is_none")]
21    pub session_id: Option<SessionId>,
22
23    pub payload: RequestPayload,
24
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub metadata: Option<Metadata>,
27}
28
29/// Different types of request payloads
30#[derive(Debug, Clone, Serialize, Deserialize)]
31#[serde(tag = "action", rename_all = "snake_case")]
32pub enum RequestPayload {
33    Initialize(InitializeRequest),
34    Execute(ExecuteRequest),
35    Complete(CompleteRequest),
36    Cancel(CancelRequest),
37    GetStatus(GetStatusRequest),
38    Custom(Value),
39}
40
41/// Initialize a new session
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct InitializeRequest {
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub working_directory: Option<String>,
46
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub environment: Option<Vec<EnvironmentVariable>>,
49
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub capabilities: Option<Vec<Capability>>,
52}
53
54/// Environment variable
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct EnvironmentVariable {
57    pub name: String,
58    pub value: String,
59}
60
61/// Execute a command or task
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct ExecuteRequest {
64    pub command: String,
65
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub arguments: Option<Vec<String>>,
68
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub input: Option<String>,
71
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub timeout_ms: Option<u64>,
74
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub working_directory: Option<String>,
77}
78
79/// Request completion suggestions
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct CompleteRequest {
82    pub prompt: String,
83
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub context: Option<CompletionContext>,
86
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub max_suggestions: Option<usize>,
89}
90
91/// Context for completion requests
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct CompletionContext {
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub file_path: Option<String>,
96
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub cursor_position: Option<CursorPosition>,
99
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub surrounding_code: Option<String>,
102}
103
104/// Cursor position in a file
105#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct CursorPosition {
107    pub line: usize,
108    pub column: usize,
109}
110
111/// Cancel a running operation
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct CancelRequest {
114    pub target_id: Id,
115
116    #[serde(skip_serializing_if = "Option::is_none")]
117    pub reason: Option<String>,
118}
119
120/// Get status of an operation
121#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct GetStatusRequest {
123    pub target_id: Id,
124
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub include_details: Option<bool>,
127}
128
129/// Response message from Claude Code
130#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct Response {
132    #[serde(rename = "type")]
133    pub message_type: String,
134
135    pub id: Id,
136
137    #[serde(skip_serializing_if = "Option::is_none")]
138    pub request_id: Option<Id>,
139
140    pub status: Status,
141
142    #[serde(skip_serializing_if = "Option::is_none")]
143    pub payload: Option<ResponsePayload>,
144
145    #[serde(skip_serializing_if = "Option::is_none")]
146    pub error: Option<ErrorDetail>,
147
148    #[serde(skip_serializing_if = "Option::is_none")]
149    pub metadata: Option<Metadata>,
150}
151
152/// Different types of response payloads
153#[derive(Debug, Clone, Serialize, Deserialize)]
154#[serde(tag = "result_type", rename_all = "snake_case")]
155pub enum ResponsePayload {
156    Initialize(InitializeResponse),
157    Execute(ExecuteResponse),
158    Complete(CompleteResponse),
159    Status(StatusResponse),
160    Stream(StreamResponse),
161    Custom(Value),
162}
163
164/// Response to initialization
165#[derive(Debug, Clone, Serialize, Deserialize)]
166pub struct InitializeResponse {
167    pub session_id: SessionId,
168    pub version: String,
169    pub capabilities: Vec<Capability>,
170}
171
172/// Response to execution
173#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct ExecuteResponse {
175    #[serde(skip_serializing_if = "Option::is_none")]
176    pub output: Option<String>,
177
178    #[serde(skip_serializing_if = "Option::is_none")]
179    pub error_output: Option<String>,
180
181    #[serde(skip_serializing_if = "Option::is_none")]
182    pub exit_code: Option<i32>,
183
184    #[serde(skip_serializing_if = "Option::is_none")]
185    pub execution_time_ms: Option<u64>,
186}
187
188/// Response to completion request
189#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct CompleteResponse {
191    pub suggestions: Vec<CompletionSuggestion>,
192}
193
194/// A single completion suggestion
195#[derive(Debug, Clone, Serialize, Deserialize)]
196pub struct CompletionSuggestion {
197    pub text: String,
198
199    #[serde(skip_serializing_if = "Option::is_none")]
200    pub description: Option<String>,
201
202    #[serde(skip_serializing_if = "Option::is_none")]
203    pub score: Option<f32>,
204
205    #[serde(skip_serializing_if = "Option::is_none")]
206    pub metadata: Option<SuggestionMetadata>,
207}
208
209/// Metadata for a completion suggestion.
210///
211/// This struct captures common metadata fields while allowing additional
212/// custom fields through the `extra` field.
213#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
214pub struct SuggestionMetadata {
215    /// The source of the suggestion (e.g., "history", "model", "cache")
216    #[serde(skip_serializing_if = "Option::is_none")]
217    pub source: Option<String>,
218
219    /// Priority level for the suggestion
220    #[serde(skip_serializing_if = "Option::is_none")]
221    pub priority: Option<i32>,
222
223    /// Category of the suggestion
224    #[serde(skip_serializing_if = "Option::is_none")]
225    pub category: Option<String>,
226
227    /// Any additional metadata fields
228    #[serde(flatten)]
229    pub extra: std::collections::HashMap<String, Value>,
230}
231
232/// Status response
233#[derive(Debug, Clone, Serialize, Deserialize)]
234pub struct StatusResponse {
235    pub status: Status,
236
237    #[serde(skip_serializing_if = "Option::is_none")]
238    pub progress: Option<Progress>,
239
240    #[serde(skip_serializing_if = "Option::is_none")]
241    pub details: Option<StatusDetails>,
242}
243
244/// Details for a status response.
245///
246/// This struct captures common status detail fields while allowing additional
247/// custom fields through the `extra` field.
248#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
249pub struct StatusDetails {
250    /// Error message if the status indicates an error
251    #[serde(skip_serializing_if = "Option::is_none")]
252    pub error: Option<String>,
253
254    /// Reason for the current status
255    #[serde(skip_serializing_if = "Option::is_none")]
256    pub reason: Option<String>,
257
258    /// Human-readable description of the status
259    #[serde(skip_serializing_if = "Option::is_none")]
260    pub description: Option<String>,
261
262    /// Any additional detail fields
263    #[serde(flatten)]
264    pub extra: std::collections::HashMap<String, Value>,
265}
266
267/// Progress information
268#[derive(Debug, Clone, Serialize, Deserialize)]
269pub struct Progress {
270    pub current: usize,
271    pub total: Option<usize>,
272
273    #[serde(skip_serializing_if = "Option::is_none")]
274    pub percentage: Option<f32>,
275
276    #[serde(skip_serializing_if = "Option::is_none")]
277    pub message: Option<String>,
278}
279
280/// Streaming response
281#[derive(Debug, Clone, Serialize, Deserialize)]
282pub struct StreamResponse {
283    pub chunk: String,
284
285    #[serde(skip_serializing_if = "Option::is_none")]
286    pub stream_id: Option<String>,
287
288    pub is_final: bool,
289}
290
291/// Event message for notifications
292#[derive(Debug, Clone, Serialize, Deserialize)]
293pub struct Event {
294    #[serde(rename = "type")]
295    pub message_type: String,
296
297    pub event_type: EventType,
298
299    #[serde(skip_serializing_if = "Option::is_none")]
300    pub session_id: Option<SessionId>,
301
302    pub payload: Value,
303
304    #[serde(skip_serializing_if = "Option::is_none")]
305    pub metadata: Option<Metadata>,
306}
307
308/// Types of events
309#[derive(Debug, Clone, Serialize, Deserialize)]
310#[serde(rename_all = "snake_case")]
311pub enum EventType {
312    Log,
313    Progress,
314    StateChange,
315    Error,
316    Warning,
317    Info,
318    Debug,
319    Custom(String),
320}
321
322#[cfg(test)]
323mod tests {
324    use super::*;
325
326    #[test]
327    fn test_suggestion_metadata_parsing() {
328        let json = r#"{
329            "source": "history",
330            "priority": 5,
331            "category": "command",
332            "custom_field": "custom_value"
333        }"#;
334
335        let metadata: SuggestionMetadata = serde_json::from_str(json).unwrap();
336        assert_eq!(metadata.source, Some("history".to_string()));
337        assert_eq!(metadata.priority, Some(5));
338        assert_eq!(metadata.category, Some("command".to_string()));
339        assert_eq!(
340            metadata.extra.get("custom_field").unwrap(),
341            &serde_json::json!("custom_value")
342        );
343    }
344
345    #[test]
346    fn test_suggestion_metadata_minimal() {
347        let json = r#"{}"#;
348
349        let metadata: SuggestionMetadata = serde_json::from_str(json).unwrap();
350        assert_eq!(metadata.source, None);
351        assert_eq!(metadata.priority, None);
352        assert!(metadata.extra.is_empty());
353    }
354
355    #[test]
356    fn test_suggestion_metadata_roundtrip() {
357        let mut extra = std::collections::HashMap::new();
358        extra.insert("key".to_string(), serde_json::json!("value"));
359
360        let metadata = SuggestionMetadata {
361            source: Some("model".to_string()),
362            priority: Some(10),
363            category: None,
364            extra,
365        };
366
367        let json = serde_json::to_string(&metadata).unwrap();
368        let parsed: SuggestionMetadata = serde_json::from_str(&json).unwrap();
369        assert_eq!(parsed, metadata);
370    }
371
372    #[test]
373    fn test_status_details_parsing() {
374        let json = r#"{
375            "error": "Connection failed",
376            "reason": "timeout",
377            "description": "The server did not respond in time",
378            "retry_count": 3
379        }"#;
380
381        let details: StatusDetails = serde_json::from_str(json).unwrap();
382        assert_eq!(details.error, Some("Connection failed".to_string()));
383        assert_eq!(details.reason, Some("timeout".to_string()));
384        assert_eq!(
385            details.description,
386            Some("The server did not respond in time".to_string())
387        );
388        assert_eq!(
389            details.extra.get("retry_count").unwrap(),
390            &serde_json::json!(3)
391        );
392    }
393
394    #[test]
395    fn test_status_details_minimal() {
396        let json = r#"{}"#;
397
398        let details: StatusDetails = serde_json::from_str(json).unwrap();
399        assert_eq!(details.error, None);
400        assert_eq!(details.reason, None);
401        assert!(details.extra.is_empty());
402    }
403
404    #[test]
405    fn test_status_details_roundtrip() {
406        let details = StatusDetails {
407            error: Some("Error message".to_string()),
408            reason: None,
409            description: Some("Description".to_string()),
410            extra: std::collections::HashMap::new(),
411        };
412
413        let json = serde_json::to_string(&details).unwrap();
414        let parsed: StatusDetails = serde_json::from_str(&json).unwrap();
415        assert_eq!(parsed, details);
416    }
417
418    #[test]
419    fn test_completion_suggestion_with_metadata() {
420        let json = r#"{
421            "text": "git status",
422            "description": "Show repository status",
423            "score": 0.95,
424            "metadata": {
425                "source": "history",
426                "priority": 1
427            }
428        }"#;
429
430        let suggestion: CompletionSuggestion = serde_json::from_str(json).unwrap();
431        assert_eq!(suggestion.text, "git status");
432        assert_eq!(
433            suggestion.description,
434            Some("Show repository status".to_string())
435        );
436        assert_eq!(suggestion.score, Some(0.95));
437        assert!(suggestion.metadata.is_some());
438
439        let meta = suggestion.metadata.unwrap();
440        assert_eq!(meta.source, Some("history".to_string()));
441        assert_eq!(meta.priority, Some(1));
442    }
443
444    #[test]
445    fn test_status_response_with_details() {
446        let json = r#"{
447            "status": "in_progress",
448            "progress": {
449                "current": 50,
450                "total": 100,
451                "percentage": 0.5
452            },
453            "details": {
454                "reason": "processing",
455                "description": "Processing request"
456            }
457        }"#;
458
459        let response: StatusResponse = serde_json::from_str(json).unwrap();
460        assert!(matches!(response.status, Status::InProgress));
461        assert!(response.progress.is_some());
462        assert!(response.details.is_some());
463
464        let details = response.details.unwrap();
465        assert_eq!(details.reason, Some("processing".to_string()));
466    }
467}