claude_code_agent_sdk/client.rs
1//! ClaudeClient for bidirectional streaming interactions with hook support
2
3use futures::stream::Stream;
4use std::pin::Pin;
5use std::sync::Arc;
6use tokio::io::AsyncWriteExt;
7use tokio::sync::Mutex;
8
9use crate::errors::{ClaudeError, Result};
10use crate::internal::message_parser::MessageParser;
11use crate::internal::query_full::QueryFull;
12use crate::internal::transport::subprocess::QueryPrompt;
13use crate::internal::transport::{SubprocessTransport, Transport};
14use crate::types::config::{ClaudeAgentOptions, PermissionMode};
15use crate::types::hooks::HookEvent;
16use crate::types::messages::{Message, UserContentBlock};
17
18/// Client for bidirectional streaming interactions with Claude
19///
20/// This client provides the same functionality as Python's ClaudeSDKClient,
21/// supporting bidirectional communication, streaming responses, and dynamic
22/// control over the Claude session.
23///
24/// # Example
25///
26/// ```no_run
27/// use claude_agent_sdk_rs::{ClaudeClient, ClaudeAgentOptions};
28/// use futures::StreamExt;
29///
30/// #[tokio::main]
31/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
32/// let mut client = ClaudeClient::new(ClaudeAgentOptions::default());
33///
34/// // Connect to Claude
35/// client.connect().await?;
36///
37/// // Send a query
38/// client.query("Hello Claude!").await?;
39///
40/// // Receive response as a stream
41/// {
42/// let mut stream = client.receive_response();
43/// while let Some(message) = stream.next().await {
44/// println!("Received: {:?}", message?);
45/// }
46/// }
47///
48/// // Disconnect
49/// client.disconnect().await?;
50/// Ok(())
51/// }
52/// ```
53pub struct ClaudeClient {
54 options: ClaudeAgentOptions,
55 query: Option<Arc<Mutex<QueryFull>>>,
56 connected: bool,
57}
58
59impl ClaudeClient {
60 /// Create a new ClaudeClient
61 ///
62 /// # Arguments
63 ///
64 /// * `options` - Configuration options for the Claude client
65 ///
66 /// # Example
67 ///
68 /// ```no_run
69 /// use claude_agent_sdk_rs::{ClaudeClient, ClaudeAgentOptions};
70 ///
71 /// let client = ClaudeClient::new(ClaudeAgentOptions::default());
72 /// ```
73 pub fn new(options: ClaudeAgentOptions) -> Self {
74 Self {
75 options,
76 query: None,
77 connected: false,
78 }
79 }
80
81 /// Create a new ClaudeClient with early validation
82 ///
83 /// Unlike `new()`, this validates the configuration eagerly by attempting
84 /// to create the transport. This catches issues like invalid working directory
85 /// or missing CLI before `connect()` is called.
86 ///
87 /// # Arguments
88 ///
89 /// * `options` - Configuration options for the Claude client
90 ///
91 /// # Errors
92 ///
93 /// Returns an error if:
94 /// - The working directory does not exist or is not a directory
95 /// - Claude CLI cannot be found
96 ///
97 /// # Example
98 ///
99 /// ```no_run
100 /// use claude_agent_sdk_rs::{ClaudeClient, ClaudeAgentOptions};
101 ///
102 /// let client = ClaudeClient::try_new(ClaudeAgentOptions::default())?;
103 /// # Ok::<(), claude_agent_sdk_rs::ClaudeError>(())
104 /// ```
105 pub fn try_new(options: ClaudeAgentOptions) -> Result<Self> {
106 // Validate by attempting to create transport (but don't keep it)
107 let prompt = QueryPrompt::Streaming;
108 let _ = SubprocessTransport::new(prompt, options.clone())?;
109
110 Ok(Self {
111 options,
112 query: None,
113 connected: false,
114 })
115 }
116
117 /// Connect to Claude (analogous to Python's __aenter__)
118 ///
119 /// This establishes the connection to the Claude Code CLI and initializes
120 /// the bidirectional communication channel.
121 ///
122 /// # Errors
123 ///
124 /// Returns an error if:
125 /// - Claude CLI cannot be found or started
126 /// - The initialization handshake fails
127 /// - Hook registration fails
128 pub async fn connect(&mut self) -> Result<()> {
129 if self.connected {
130 return Ok(());
131 }
132
133 // Create transport in streaming mode (no initial prompt)
134 let prompt = QueryPrompt::Streaming;
135 let mut transport = SubprocessTransport::new(prompt, self.options.clone())?;
136
137 // Don't send initial prompt - we'll use query() for that
138 transport.connect().await?;
139
140 // Extract stdin for direct access (avoids transport lock deadlock)
141 let stdin = Arc::clone(&transport.stdin);
142
143 // Create Query with hooks
144 let mut query = QueryFull::new(Box::new(transport));
145 query.set_stdin(stdin);
146
147 // Extract SDK MCP servers from options
148 let sdk_mcp_servers =
149 if let crate::types::mcp::McpServers::Dict(servers_dict) = &self.options.mcp_servers {
150 servers_dict
151 .iter()
152 .filter_map(|(name, config)| {
153 if let crate::types::mcp::McpServerConfig::Sdk(sdk_config) = config {
154 Some((name.clone(), sdk_config.clone()))
155 } else {
156 None
157 }
158 })
159 .collect()
160 } else {
161 std::collections::HashMap::new()
162 };
163 query.set_sdk_mcp_servers(sdk_mcp_servers).await;
164
165 // Convert hooks to internal format
166 let hooks = self.options.hooks.as_ref().map(|hooks_map| {
167 hooks_map
168 .iter()
169 .map(|(event, matchers)| {
170 let event_name = match event {
171 HookEvent::PreToolUse => "PreToolUse",
172 HookEvent::PostToolUse => "PostToolUse",
173 HookEvent::UserPromptSubmit => "UserPromptSubmit",
174 HookEvent::Stop => "Stop",
175 HookEvent::SubagentStop => "SubagentStop",
176 HookEvent::PreCompact => "PreCompact",
177 };
178 (event_name.to_string(), matchers.clone())
179 })
180 .collect()
181 });
182
183 // Start reading messages in background FIRST
184 // This must happen before initialize() because initialize()
185 // sends a control request and waits for response
186 query.start().await?;
187
188 // Initialize with hooks (sends control request)
189 query.initialize(hooks).await?;
190
191 self.query = Some(Arc::new(Mutex::new(query)));
192 self.connected = true;
193
194 Ok(())
195 }
196
197 /// Send a query to Claude
198 ///
199 /// This sends a new user prompt to Claude. Claude will remember the context
200 /// of previous queries within the same session.
201 ///
202 /// # Arguments
203 ///
204 /// * `prompt` - The user prompt to send
205 ///
206 /// # Errors
207 ///
208 /// Returns an error if the client is not connected or if sending fails.
209 ///
210 /// # Example
211 ///
212 /// ```no_run
213 /// # use claude_agent_sdk_rs::{ClaudeClient, ClaudeAgentOptions};
214 /// # #[tokio::main]
215 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
216 /// # let mut client = ClaudeClient::new(ClaudeAgentOptions::default());
217 /// # client.connect().await?;
218 /// client.query("What is 2 + 2?").await?;
219 /// # Ok(())
220 /// # }
221 /// ```
222 pub async fn query(&mut self, prompt: impl Into<String>) -> Result<()> {
223 self.query_with_session(prompt, "default").await
224 }
225
226 /// Send a query to Claude with a specific session ID
227 ///
228 /// This sends a new user prompt to Claude. Different session IDs maintain
229 /// separate conversation contexts.
230 ///
231 /// # Arguments
232 ///
233 /// * `prompt` - The user prompt to send
234 /// * `session_id` - Session identifier for the conversation
235 ///
236 /// # Errors
237 ///
238 /// Returns an error if the client is not connected or if sending fails.
239 ///
240 /// # Example
241 ///
242 /// ```no_run
243 /// # use claude_agent_sdk_rs::{ClaudeClient, ClaudeAgentOptions};
244 /// # #[tokio::main]
245 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
246 /// # let mut client = ClaudeClient::new(ClaudeAgentOptions::default());
247 /// # client.connect().await?;
248 /// // Separate conversation contexts
249 /// client.query_with_session("First question", "session-1").await?;
250 /// client.query_with_session("Different question", "session-2").await?;
251 /// # Ok(())
252 /// # }
253 /// ```
254 pub async fn query_with_session(
255 &mut self,
256 prompt: impl Into<String>,
257 session_id: impl Into<String>,
258 ) -> Result<()> {
259 let query = self.query.as_ref().ok_or_else(|| {
260 ClaudeError::InvalidConfig("Client not connected. Call connect() first.".to_string())
261 })?;
262
263 let prompt_str = prompt.into();
264 let session_id_str = session_id.into();
265
266 // Format as JSON message for stream-json input format
267 let user_message = serde_json::json!({
268 "type": "user",
269 "message": {
270 "role": "user",
271 "content": prompt_str
272 },
273 "session_id": session_id_str
274 });
275
276 let message_str = serde_json::to_string(&user_message).map_err(|e| {
277 ClaudeError::Transport(format!("Failed to serialize user message: {}", e))
278 })?;
279
280 // Write directly to stdin (bypasses transport lock)
281 let query_guard = query.lock().await;
282 let stdin = query_guard.stdin.clone();
283 drop(query_guard);
284
285 if let Some(stdin_arc) = stdin {
286 let mut stdin_guard = stdin_arc.lock().await;
287 if let Some(ref mut stdin_stream) = *stdin_guard {
288 stdin_stream
289 .write_all(message_str.as_bytes())
290 .await
291 .map_err(|e| ClaudeError::Transport(format!("Failed to write query: {}", e)))?;
292 stdin_stream.write_all(b"\n").await.map_err(|e| {
293 ClaudeError::Transport(format!("Failed to write newline: {}", e))
294 })?;
295 stdin_stream
296 .flush()
297 .await
298 .map_err(|e| ClaudeError::Transport(format!("Failed to flush: {}", e)))?;
299 } else {
300 return Err(ClaudeError::Transport("stdin not available".to_string()));
301 }
302 } else {
303 return Err(ClaudeError::Transport("stdin not set".to_string()));
304 }
305
306 Ok(())
307 }
308
309 /// Send a query with structured content blocks (supports images)
310 ///
311 /// This method enables multimodal queries in bidirectional streaming mode.
312 /// Use it to send images alongside text for vision-related tasks.
313 ///
314 /// # Arguments
315 ///
316 /// * `content` - A vector of content blocks (text and/or images)
317 ///
318 /// # Errors
319 ///
320 /// Returns an error if:
321 /// - The content vector is empty (must include at least one text or image block)
322 /// - The client is not connected (call `connect()` first)
323 /// - Sending the message fails
324 ///
325 /// # Example
326 ///
327 /// ```no_run
328 /// # use claude_agent_sdk_rs::{ClaudeClient, ClaudeAgentOptions, UserContentBlock};
329 /// # #[tokio::main]
330 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
331 /// # let mut client = ClaudeClient::new(ClaudeAgentOptions::default());
332 /// # client.connect().await?;
333 /// let base64_data = "iVBORw0KGgo..."; // base64 encoded image
334 /// client.query_with_content(vec![
335 /// UserContentBlock::text("What's in this image?"),
336 /// UserContentBlock::image_base64("image/png", base64_data)?,
337 /// ]).await?;
338 /// # Ok(())
339 /// # }
340 /// ```
341 pub async fn query_with_content(
342 &mut self,
343 content: impl Into<Vec<UserContentBlock>>,
344 ) -> Result<()> {
345 self.query_with_content_and_session(content, "default")
346 .await
347 }
348
349 /// Send a query with structured content blocks and a specific session ID
350 ///
351 /// This method enables multimodal queries with session management for
352 /// maintaining separate conversation contexts.
353 ///
354 /// # Arguments
355 ///
356 /// * `content` - A vector of content blocks (text and/or images)
357 /// * `session_id` - Session identifier for the conversation
358 ///
359 /// # Errors
360 ///
361 /// Returns an error if:
362 /// - The content vector is empty (must include at least one text or image block)
363 /// - The client is not connected (call `connect()` first)
364 /// - Sending the message fails
365 ///
366 /// # Example
367 ///
368 /// ```no_run
369 /// # use claude_agent_sdk_rs::{ClaudeClient, ClaudeAgentOptions, UserContentBlock};
370 /// # #[tokio::main]
371 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
372 /// # let mut client = ClaudeClient::new(ClaudeAgentOptions::default());
373 /// # client.connect().await?;
374 /// client.query_with_content_and_session(
375 /// vec![
376 /// UserContentBlock::text("Analyze this chart"),
377 /// UserContentBlock::image_url("https://example.com/chart.png"),
378 /// ],
379 /// "analysis-session",
380 /// ).await?;
381 /// # Ok(())
382 /// # }
383 /// ```
384 pub async fn query_with_content_and_session(
385 &mut self,
386 content: impl Into<Vec<UserContentBlock>>,
387 session_id: impl Into<String>,
388 ) -> Result<()> {
389 let query = self.query.as_ref().ok_or_else(|| {
390 ClaudeError::InvalidConfig("Client not connected. Call connect() first.".to_string())
391 })?;
392
393 let content_blocks: Vec<UserContentBlock> = content.into();
394 UserContentBlock::validate_content(&content_blocks)?;
395
396 let session_id_str = session_id.into();
397
398 // Format as JSON message for stream-json input format
399 // Content is an array of content blocks, not a simple string
400 let user_message = serde_json::json!({
401 "type": "user",
402 "message": {
403 "role": "user",
404 "content": content_blocks
405 },
406 "session_id": session_id_str
407 });
408
409 let message_str = serde_json::to_string(&user_message).map_err(|e| {
410 ClaudeError::Transport(format!("Failed to serialize user message: {}", e))
411 })?;
412
413 // Write directly to stdin (bypasses transport lock)
414 let query_guard = query.lock().await;
415 let stdin = query_guard.stdin.clone();
416 drop(query_guard);
417
418 if let Some(stdin_arc) = stdin {
419 let mut stdin_guard = stdin_arc.lock().await;
420 if let Some(ref mut stdin_stream) = *stdin_guard {
421 stdin_stream
422 .write_all(message_str.as_bytes())
423 .await
424 .map_err(|e| ClaudeError::Transport(format!("Failed to write query: {}", e)))?;
425 stdin_stream.write_all(b"\n").await.map_err(|e| {
426 ClaudeError::Transport(format!("Failed to write newline: {}", e))
427 })?;
428 stdin_stream
429 .flush()
430 .await
431 .map_err(|e| ClaudeError::Transport(format!("Failed to flush: {}", e)))?;
432 } else {
433 return Err(ClaudeError::Transport("stdin not available".to_string()));
434 }
435 } else {
436 return Err(ClaudeError::Transport("stdin not set".to_string()));
437 }
438
439 Ok(())
440 }
441
442 /// Receive all messages as a stream (continuous)
443 ///
444 /// This method returns a stream that yields all messages from Claude
445 /// indefinitely until the stream is closed or an error occurs.
446 ///
447 /// Use this when you want to process all messages, including multiple
448 /// responses and system events.
449 ///
450 /// # Returns
451 ///
452 /// A stream of `Result<Message>` that continues until the connection closes.
453 ///
454 /// # Example
455 ///
456 /// ```no_run
457 /// # use claude_agent_sdk_rs::{ClaudeClient, ClaudeAgentOptions};
458 /// # use futures::StreamExt;
459 /// # #[tokio::main]
460 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
461 /// # let mut client = ClaudeClient::new(ClaudeAgentOptions::default());
462 /// # client.connect().await?;
463 /// # client.query("Hello").await?;
464 /// let mut stream = client.receive_messages();
465 /// while let Some(message) = stream.next().await {
466 /// println!("Received: {:?}", message?);
467 /// }
468 /// # Ok(())
469 /// # }
470 /// ```
471 pub fn receive_messages(&self) -> Pin<Box<dyn Stream<Item = Result<Message>> + Send + '_>> {
472 let query = match &self.query {
473 Some(q) => Arc::clone(q),
474 None => {
475 return Box::pin(futures::stream::once(async {
476 Err(ClaudeError::InvalidConfig(
477 "Client not connected. Call connect() first.".to_string(),
478 ))
479 }));
480 }
481 };
482
483 Box::pin(async_stream::stream! {
484 let rx: Arc<Mutex<tokio::sync::mpsc::UnboundedReceiver<serde_json::Value>>> = {
485 let query_guard = query.lock().await;
486 Arc::clone(&query_guard.message_rx)
487 };
488
489 loop {
490 let message = {
491 let mut rx_guard = rx.lock().await;
492 rx_guard.recv().await
493 };
494
495 match message {
496 Some(json) => {
497 match MessageParser::parse(json) {
498 Ok(msg) => yield Ok(msg),
499 Err(e) => {
500 eprintln!("Failed to parse message: {}", e);
501 yield Err(e);
502 }
503 }
504 }
505 None => break,
506 }
507 }
508 })
509 }
510
511 /// Receive messages until a ResultMessage
512 ///
513 /// This method returns a stream that yields messages until it encounters
514 /// a `ResultMessage`, which signals the completion of a Claude response.
515 ///
516 /// This is the most common pattern for handling Claude responses, as it
517 /// processes one complete "turn" of the conversation.
518 ///
519 /// # Returns
520 ///
521 /// A stream of `Result<Message>` that ends when a ResultMessage is received.
522 ///
523 /// # Example
524 ///
525 /// ```no_run
526 /// # use claude_agent_sdk_rs::{ClaudeClient, ClaudeAgentOptions, Message};
527 /// # use futures::StreamExt;
528 /// # #[tokio::main]
529 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
530 /// # let mut client = ClaudeClient::new(ClaudeAgentOptions::default());
531 /// # client.connect().await?;
532 /// # client.query("Hello").await?;
533 /// let mut stream = client.receive_response();
534 /// while let Some(message) = stream.next().await {
535 /// match message? {
536 /// Message::Assistant(msg) => println!("Assistant: {:?}", msg),
537 /// Message::Result(result) => {
538 /// println!("Done! Cost: ${:?}", result.total_cost_usd);
539 /// break;
540 /// }
541 /// _ => {}
542 /// }
543 /// }
544 /// # Ok(())
545 /// # }
546 /// ```
547 pub fn receive_response(&self) -> Pin<Box<dyn Stream<Item = Result<Message>> + Send + '_>> {
548 let query = match &self.query {
549 Some(q) => Arc::clone(q),
550 None => {
551 return Box::pin(futures::stream::once(async {
552 Err(ClaudeError::InvalidConfig(
553 "Client not connected. Call connect() first.".to_string(),
554 ))
555 }));
556 }
557 };
558
559 Box::pin(async_stream::stream! {
560 let rx: Arc<Mutex<tokio::sync::mpsc::UnboundedReceiver<serde_json::Value>>> = {
561 let query_guard = query.lock().await;
562 Arc::clone(&query_guard.message_rx)
563 };
564
565 loop {
566 let message = {
567 let mut rx_guard = rx.lock().await;
568 rx_guard.recv().await
569 };
570
571 match message {
572 Some(json) => {
573 match MessageParser::parse(json) {
574 Ok(msg) => {
575 let is_result = matches!(msg, Message::Result(_));
576 yield Ok(msg);
577 if is_result {
578 break;
579 }
580 }
581 Err(e) => {
582 eprintln!("Failed to parse message: {}", e);
583 yield Err(e);
584 }
585 }
586 }
587 None => break,
588 }
589 }
590 })
591 }
592
593 /// Send an interrupt signal to stop the current Claude operation
594 ///
595 /// This is analogous to Python's `client.interrupt()`.
596 ///
597 /// # Errors
598 ///
599 /// Returns an error if the client is not connected or if sending fails.
600 pub async fn interrupt(&self) -> Result<()> {
601 let query = self.query.as_ref().ok_or_else(|| {
602 ClaudeError::InvalidConfig("Client not connected. Call connect() first.".to_string())
603 })?;
604
605 let query_guard = query.lock().await;
606 query_guard.interrupt().await
607 }
608
609 /// Change the permission mode dynamically
610 ///
611 /// This is analogous to Python's `client.set_permission_mode()`.
612 ///
613 /// # Arguments
614 ///
615 /// * `mode` - The new permission mode to set
616 ///
617 /// # Errors
618 ///
619 /// Returns an error if the client is not connected or if sending fails.
620 pub async fn set_permission_mode(&self, mode: PermissionMode) -> Result<()> {
621 let query = self.query.as_ref().ok_or_else(|| {
622 ClaudeError::InvalidConfig("Client not connected. Call connect() first.".to_string())
623 })?;
624
625 let query_guard = query.lock().await;
626 query_guard.set_permission_mode(mode).await
627 }
628
629 /// Change the AI model dynamically
630 ///
631 /// This is analogous to Python's `client.set_model()`.
632 ///
633 /// # Arguments
634 ///
635 /// * `model` - The new model name, or None to use default
636 ///
637 /// # Errors
638 ///
639 /// Returns an error if the client is not connected or if sending fails.
640 pub async fn set_model(&self, model: Option<&str>) -> Result<()> {
641 let query = self.query.as_ref().ok_or_else(|| {
642 ClaudeError::InvalidConfig("Client not connected. Call connect() first.".to_string())
643 })?;
644
645 let query_guard = query.lock().await;
646 query_guard.set_model(model).await
647 }
648
649 /// Rewind tracked files to their state at a specific user message.
650 ///
651 /// This is analogous to Python's `client.rewind_files()`.
652 ///
653 /// # Requirements
654 ///
655 /// - `enable_file_checkpointing=true` in options to track file changes
656 /// - `extra_args={"replay-user-messages": None}` to receive UserMessage
657 /// objects with `uuid` in the response stream
658 ///
659 /// # Arguments
660 ///
661 /// * `user_message_id` - UUID of the user message to rewind to. This should be
662 /// the `uuid` field from a `UserMessage` received during the conversation.
663 ///
664 /// # Errors
665 ///
666 /// Returns an error if the client is not connected or if sending fails.
667 ///
668 /// # Example
669 ///
670 /// ```no_run
671 /// # use claude_agent_sdk_rs::{ClaudeClient, ClaudeAgentOptions, Message};
672 /// # use std::collections::HashMap;
673 /// # #[tokio::main]
674 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
675 /// let options = ClaudeAgentOptions::builder()
676 /// .enable_file_checkpointing(true)
677 /// .extra_args(HashMap::from([("replay-user-messages".to_string(), None)]))
678 /// .build();
679 /// let mut client = ClaudeClient::new(options);
680 /// client.connect().await?;
681 ///
682 /// client.query("Make some changes to my files").await?;
683 /// let mut checkpoint_id = None;
684 /// {
685 /// let mut stream = client.receive_response();
686 /// use futures::StreamExt;
687 /// while let Some(Ok(msg)) = stream.next().await {
688 /// if let Message::User(user_msg) = &msg {
689 /// if let Some(uuid) = &user_msg.uuid {
690 /// checkpoint_id = Some(uuid.clone());
691 /// }
692 /// }
693 /// }
694 /// }
695 ///
696 /// // Later, rewind to that point
697 /// if let Some(id) = checkpoint_id {
698 /// client.rewind_files(&id).await?;
699 /// }
700 /// # Ok(())
701 /// # }
702 /// ```
703 pub async fn rewind_files(&self, user_message_id: &str) -> Result<()> {
704 let query = self.query.as_ref().ok_or_else(|| {
705 ClaudeError::InvalidConfig("Client not connected. Call connect() first.".to_string())
706 })?;
707
708 let query_guard = query.lock().await;
709 query_guard.rewind_files(user_message_id).await
710 }
711
712 /// Get server initialization info including available commands and output styles
713 ///
714 /// Returns initialization information from the Claude Code server including:
715 /// - Available commands (slash commands, system commands, etc.)
716 /// - Current and available output styles
717 /// - Server capabilities
718 ///
719 /// This is analogous to Python's `client.get_server_info()`.
720 ///
721 /// # Returns
722 ///
723 /// Dictionary with server info, or None if not connected
724 ///
725 /// # Example
726 ///
727 /// ```no_run
728 /// # use claude_agent_sdk_rs::{ClaudeClient, ClaudeAgentOptions};
729 /// # #[tokio::main]
730 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
731 /// # let mut client = ClaudeClient::new(ClaudeAgentOptions::default());
732 /// # client.connect().await?;
733 /// if let Some(info) = client.get_server_info().await {
734 /// println!("Commands available: {}", info.get("commands").map(|c| c.as_array().map(|a| a.len()).unwrap_or(0)).unwrap_or(0));
735 /// println!("Output style: {:?}", info.get("output_style"));
736 /// }
737 /// # Ok(())
738 /// # }
739 /// ```
740 pub async fn get_server_info(&self) -> Option<serde_json::Value> {
741 let query = self.query.as_ref()?;
742 let query_guard = query.lock().await;
743 query_guard.get_initialization_result().await
744 }
745
746 /// Start a new session by switching to a different session ID
747 ///
748 /// This is a convenience method that creates a new conversation context.
749 /// It's equivalent to calling `query_with_session()` with a new session ID.
750 ///
751 /// To completely clear memory and start fresh, use `ClaudeAgentOptions::builder().fork_session(true).build()`
752 /// when creating a new client.
753 ///
754 /// # Arguments
755 ///
756 /// * `session_id` - The new session ID to use
757 /// * `prompt` - Initial message for the new session
758 ///
759 /// # Errors
760 ///
761 /// Returns an error if the client is not connected or if sending fails.
762 ///
763 /// # Example
764 ///
765 /// ```no_run
766 /// # use claude_agent_sdk_rs::{ClaudeClient, ClaudeAgentOptions};
767 /// # #[tokio::main]
768 /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
769 /// # let mut client = ClaudeClient::new(ClaudeAgentOptions::default());
770 /// # client.connect().await?;
771 /// // First conversation
772 /// client.query("Hello").await?;
773 ///
774 /// // Start new conversation with different context
775 /// client.new_session("session-2", "Tell me about Rust").await?;
776 /// # Ok(())
777 /// # }
778 /// ```
779 pub async fn new_session(
780 &mut self,
781 session_id: impl Into<String>,
782 prompt: impl Into<String>,
783 ) -> Result<()> {
784 self.query_with_session(prompt, session_id).await
785 }
786
787 /// Disconnect from Claude (analogous to Python's __aexit__)
788 ///
789 /// This cleanly shuts down the connection to Claude Code CLI.
790 ///
791 /// # Errors
792 ///
793 /// Returns an error if disconnection fails.
794 pub async fn disconnect(&mut self) -> Result<()> {
795 if !self.connected {
796 return Ok(());
797 }
798
799 if let Some(query) = self.query.take() {
800 // Close stdin first (using direct access) to signal CLI to exit
801 // This will cause the background task to finish and release transport lock
802 let query_guard = query.lock().await;
803 if let Some(ref stdin_arc) = query_guard.stdin {
804 let mut stdin_guard = stdin_arc.lock().await;
805 if let Some(mut stdin_stream) = stdin_guard.take() {
806 let _ = stdin_stream.shutdown().await;
807 }
808 }
809 let transport = Arc::clone(&query_guard.transport);
810 drop(query_guard);
811
812 // Give background task a moment to finish reading and release lock
813 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
814
815 let mut transport_guard = transport.lock().await;
816 transport_guard.close().await?;
817 }
818
819 self.connected = false;
820 Ok(())
821 }
822}
823
824impl Drop for ClaudeClient {
825 fn drop(&mut self) {
826 // Note: We can't run async code in Drop, so we can't guarantee clean shutdown
827 // Users should call disconnect() explicitly
828 if self.connected {
829 eprintln!(
830 "Warning: ClaudeClient dropped without calling disconnect(). Resources may not be cleaned up properly."
831 );
832 }
833 }
834}