Skip to main content

cc_sdk/transport/
mod.rs

1//! Transport layer abstractions
2//!
3//! This module defines the Transport trait and its implementations for
4//! communicating with the Claude CLI.
5
6use crate::{
7    errors::Result,
8    types::{ControlRequest, ControlResponse, Message},
9};
10use async_trait::async_trait;
11use futures::stream::Stream;
12use serde_json::Value as JsonValue;
13use std::pin::Pin;
14use tokio::sync::mpsc::Receiver;
15
16pub mod subprocess;
17pub mod mock;
18#[cfg(feature = "websocket")]
19pub mod websocket;
20
21pub use subprocess::SubprocessTransport;
22
23/// Input message structure for sending to Claude
24#[derive(Debug, Clone, serde::Serialize)]
25pub struct InputMessage {
26    /// Message type (always "user")
27    #[serde(rename = "type")]
28    pub r#type: String,
29    /// Message content
30    pub message: serde_json::Value,
31    /// Parent tool use ID (for tool results)
32    pub parent_tool_use_id: Option<String>,
33    /// Session ID
34    pub session_id: String,
35}
36
37impl InputMessage {
38    /// Create a new user message
39    pub fn user(content: String, session_id: String) -> Self {
40        Self {
41            r#type: "user".to_string(),
42            message: serde_json::json!({
43                "role": "user",
44                "content": content
45            }),
46            parent_tool_use_id: None,
47            session_id,
48        }
49    }
50
51    /// Create a tool result message
52    pub fn tool_result(
53        tool_use_id: String,
54        content: String,
55        session_id: String,
56        is_error: bool,
57    ) -> Self {
58        Self {
59            r#type: "user".to_string(),
60            message: serde_json::json!({
61                "role": "user",
62                "content": [{
63                    "type": "tool_result",
64                    "tool_use_id": tool_use_id,
65                    "content": content,
66                    "is_error": is_error
67                }]
68            }),
69            parent_tool_use_id: Some(tool_use_id),
70            session_id,
71        }
72    }
73}
74
75/// Transport trait for communicating with Claude CLI
76#[async_trait]
77pub trait Transport: Send + Sync {
78    /// Get self as Any for downcasting
79    fn as_any_mut(&mut self) -> &mut dyn std::any::Any;
80    
81    /// Connect to the Claude CLI
82    async fn connect(&mut self) -> Result<()>;
83
84    /// Send a message to Claude
85    async fn send_message(&mut self, message: InputMessage) -> Result<()>;
86
87    /// Receive messages from Claude as a stream
88    fn receive_messages(&mut self) -> Pin<Box<dyn Stream<Item = Result<Message>> + Send + 'static>>;
89
90    /// Send a control request (e.g., interrupt)
91    async fn send_control_request(&mut self, request: ControlRequest) -> Result<()>;
92
93    /// Receive control responses
94    async fn receive_control_response(&mut self) -> Result<Option<ControlResponse>>;
95    
96    /// Send an SDK control request (for control protocol)
97    async fn send_sdk_control_request(&mut self, request: JsonValue) -> Result<()>;
98    
99    /// Send an SDK control response
100    async fn send_sdk_control_response(&mut self, response: JsonValue) -> Result<()>;
101
102    /// Take the SDK control receiver, if supported by the transport
103    /// Default implementation returns None for transports that do not
104    /// support inbound control messages.
105    fn take_sdk_control_receiver(&mut self) -> Option<Receiver<JsonValue>> {
106        None
107    }
108
109    /// Check if the transport is connected
110    #[allow(dead_code)]
111    fn is_connected(&self) -> bool;
112
113    /// Disconnect from the Claude CLI
114    async fn disconnect(&mut self) -> Result<()>;
115
116    /// Signal end of input stream (default: no-op)
117    async fn end_input(&mut self) -> Result<()> {
118        Ok(())
119    }
120}
121
122/// Transport state
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124pub enum TransportState {
125    /// Not connected
126    Disconnected,
127    /// Connecting
128    Connecting,
129    /// Connected and ready
130    Connected,
131    /// Disconnecting
132    Disconnecting,
133    /// Error state
134    #[allow(dead_code)]
135    Error,
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn test_input_message_user() {
144        let msg = InputMessage::user("Hello".to_string(), "session-123".to_string());
145        assert_eq!(msg.r#type, "user");
146        assert_eq!(msg.session_id, "session-123");
147        assert!(msg.parent_tool_use_id.is_none());
148
149        let json = serde_json::to_string(&msg).unwrap();
150        assert!(json.contains(r#""type":"user""#));
151        assert!(json.contains(r#""content":"Hello""#));
152    }
153
154    #[test]
155    fn test_input_message_tool_result() {
156        let msg = InputMessage::tool_result(
157            "tool-123".to_string(),
158            "Result".to_string(),
159            "session-456".to_string(),
160            false,
161        );
162        assert_eq!(msg.r#type, "user");
163        assert_eq!(msg.parent_tool_use_id, Some("tool-123".to_string()));
164
165        let json = serde_json::to_string(&msg).unwrap();
166        assert!(json.contains(r#""tool_use_id":"tool-123""#));
167        assert!(json.contains(r#""is_error":false"#));
168    }
169}