claude_code_agent_sdk/
query.rs

1//! Simple query function for one-shot interactions
2
3use crate::errors::{ClaudeError, Result};
4use crate::internal::client::InternalClient;
5use crate::internal::message_parser::MessageParser;
6use crate::internal::transport::subprocess::QueryPrompt;
7use crate::internal::transport::{SubprocessTransport, Transport};
8use crate::types::config::ClaudeAgentOptions;
9use crate::types::messages::{Message, UserContentBlock};
10use futures::stream::{Stream, StreamExt};
11use std::pin::Pin;
12
13/// Validate options for one-shot queries
14///
15/// One-shot queries don't support bidirectional control protocol features
16/// like `can_use_tool` callbacks or hooks. This function validates that
17/// incompatible options are not set.
18fn validate_oneshot_options(options: &ClaudeAgentOptions) -> Result<()> {
19    if options.can_use_tool.is_some() {
20        return Err(ClaudeError::InvalidConfig(
21            "can_use_tool callback is not supported in one-shot queries. \
22            Use ClaudeClient for bidirectional communication with permission callbacks."
23                .to_string(),
24        ));
25    }
26
27    if options.hooks.is_some() {
28        return Err(ClaudeError::InvalidConfig(
29            "hooks are not supported in one-shot queries. \
30            Use ClaudeClient for bidirectional communication with hook support."
31                .to_string(),
32        ));
33    }
34
35    Ok(())
36}
37
38/// Query Claude Code for one-shot interactions.
39///
40/// This function is ideal for simple, stateless queries where you don't need
41/// bidirectional communication or conversation management.
42///
43/// **Note:** This function does not support `can_use_tool` callbacks or hooks.
44/// For permission handling or hook support, use [`ClaudeClient`] instead.
45///
46/// # Errors
47///
48/// Returns an error if:
49/// - `options.can_use_tool` is set (use ClaudeClient instead)
50/// - `options.hooks` is set (use ClaudeClient instead)
51/// - Claude CLI cannot be found or started
52///
53/// # Examples
54///
55/// ```no_run
56/// use claude_agent_sdk_rs::{query, Message, ContentBlock};
57///
58/// #[tokio::main]
59/// async fn main() -> anyhow::Result<()> {
60///     let messages = query("What is 2 + 2?", None).await?;
61///
62///     for message in messages {
63///         match message {
64///             Message::Assistant(msg) => {
65///                 for block in &msg.message.content {
66///                     if let ContentBlock::Text(text) = block {
67///                         println!("Claude: {}", text.text);
68///                     }
69///                 }
70///             }
71///             _ => {}
72///         }
73///     }
74///
75///     Ok(())
76/// }
77/// ```
78pub async fn query(
79    prompt: impl Into<String>,
80    options: Option<ClaudeAgentOptions>,
81) -> Result<Vec<Message>> {
82    let query_prompt = QueryPrompt::Text(prompt.into());
83    let opts = options.unwrap_or_default();
84
85    // Validate that incompatible options are not set
86    validate_oneshot_options(&opts)?;
87
88    let client = InternalClient::new(query_prompt, opts)?;
89    client.execute().await
90}
91
92/// Query Claude Code with streaming responses for memory-efficient processing.
93///
94/// Unlike `query()` which collects all messages in memory before returning,
95/// this function returns a stream that yields messages as they arrive from Claude.
96/// This is more memory-efficient for large conversations and provides real-time
97/// message processing capabilities.
98///
99/// **Note:** This function does not support `can_use_tool` callbacks or hooks.
100/// For permission handling or hook support, use [`ClaudeClient`] instead.
101///
102/// # Performance Comparison
103///
104/// - **`query()`**: O(n) memory usage, waits for all messages before returning
105/// - **`query_stream()`**: O(1) memory per message, processes messages in real-time
106///
107/// # Errors
108///
109/// Returns an error if:
110/// - `options.can_use_tool` is set (use ClaudeClient instead)
111/// - `options.hooks` is set (use ClaudeClient instead)
112/// - Claude CLI cannot be found or started
113///
114/// # Examples
115///
116/// ```no_run
117/// use claude_agent_sdk_rs::{query_stream, Message, ContentBlock};
118/// use futures::stream::StreamExt;
119///
120/// #[tokio::main]
121/// async fn main() -> anyhow::Result<()> {
122///     let mut stream = query_stream("What is 2 + 2?", None).await?;
123///
124///     while let Some(result) = stream.next().await {
125///         match result? {
126///             Message::Assistant(msg) => {
127///                 for block in &msg.message.content {
128///                     if let ContentBlock::Text(text) = block {
129///                         println!("Claude: {}", text.text);
130///                     }
131///                 }
132///             }
133///             _ => {}
134///         }
135///     }
136///
137///     Ok(())
138/// }
139/// ```
140pub async fn query_stream(
141    prompt: impl Into<String>,
142    options: Option<ClaudeAgentOptions>,
143) -> Result<Pin<Box<dyn Stream<Item = Result<Message>> + Send>>> {
144    let query_prompt = QueryPrompt::Text(prompt.into());
145    let opts = options.unwrap_or_default();
146
147    // Validate that incompatible options are not set
148    validate_oneshot_options(&opts)?;
149
150    let mut transport = SubprocessTransport::new(query_prompt, opts)?;
151    transport.connect().await?;
152
153    // Move transport into the stream to extend its lifetime
154    let stream = async_stream::stream! {
155        let mut message_stream = transport.read_messages();
156        while let Some(json_result) = message_stream.next().await {
157            match json_result {
158                Ok(json) => {
159                    match MessageParser::parse(json) {
160                        Ok(message) => yield Ok(message),
161                        Err(e) => {
162                            yield Err(e);
163                            break;
164                        }
165                    }
166                }
167                Err(e) => {
168                    yield Err(e);
169                    break;
170                }
171            }
172        }
173    };
174
175    Ok(Box::pin(stream))
176}
177
178/// Query Claude Code with structured content blocks (supports images).
179///
180/// This function allows you to send mixed content including text and images
181/// to Claude. Use [`UserContentBlock`] to construct the content array.
182///
183/// **Note:** This function does not support `can_use_tool` callbacks or hooks.
184/// For permission handling or hook support, use [`ClaudeClient`] instead.
185///
186/// # Errors
187///
188/// Returns an error if:
189/// - The content vector is empty (must include at least one text or image block)
190/// - `options.can_use_tool` is set (use ClaudeClient instead)
191/// - `options.hooks` is set (use ClaudeClient instead)
192/// - Claude CLI cannot be found or started
193/// - The query execution fails
194///
195/// # Examples
196///
197/// ```no_run
198/// use claude_agent_sdk_rs::{query_with_content, Message, ContentBlock, UserContentBlock};
199///
200/// #[tokio::main]
201/// async fn main() -> anyhow::Result<()> {
202///     // Create content with text and image
203///     let content = vec![
204///         UserContentBlock::text("What's in this image?"),
205///         UserContentBlock::image_url("https://example.com/image.png"),
206///     ];
207///
208///     let messages = query_with_content(content, None).await?;
209///
210///     for message in messages {
211///         if let Message::Assistant(msg) = message {
212///             for block in &msg.message.content {
213///                 if let ContentBlock::Text(text) = block {
214///                     println!("Claude: {}", text.text);
215///                 }
216///             }
217///         }
218///     }
219///
220///     Ok(())
221/// }
222/// ```
223pub async fn query_with_content(
224    content: impl Into<Vec<UserContentBlock>>,
225    options: Option<ClaudeAgentOptions>,
226) -> Result<Vec<Message>> {
227    // Validate options first (fail fast - cheaper check)
228    let opts = options.unwrap_or_default();
229    validate_oneshot_options(&opts)?;
230
231    // Then validate content
232    let content_blocks = content.into();
233    UserContentBlock::validate_content(&content_blocks)?;
234
235    let query_prompt = QueryPrompt::Content(content_blocks);
236    let client = InternalClient::new(query_prompt, opts)?;
237    client.execute().await
238}
239
240/// Query Claude Code with streaming and structured content blocks.
241///
242/// Combines the benefits of [`query_stream`] (memory efficiency, real-time processing)
243/// with support for structured content blocks including images.
244///
245/// **Note:** This function does not support `can_use_tool` callbacks or hooks.
246/// For permission handling or hook support, use [`ClaudeClient`] instead.
247///
248/// # Errors
249///
250/// Returns an error if:
251/// - The content vector is empty (must include at least one text or image block)
252/// - `options.can_use_tool` is set (use ClaudeClient instead)
253/// - `options.hooks` is set (use ClaudeClient instead)
254/// - Claude CLI cannot be found or started
255/// - The streaming connection fails
256///
257/// # Examples
258///
259/// ```no_run
260/// use claude_agent_sdk_rs::{query_stream_with_content, Message, ContentBlock, UserContentBlock};
261/// use futures::stream::StreamExt;
262///
263/// #[tokio::main]
264/// async fn main() -> anyhow::Result<()> {
265///     // Create content with base64 image
266///     let content = vec![
267///         UserContentBlock::image_base64("image/png", "iVBORw0KGgo...")?,
268///         UserContentBlock::text("Describe this diagram in detail"),
269///     ];
270///
271///     let mut stream = query_stream_with_content(content, None).await?;
272///
273///     while let Some(result) = stream.next().await {
274///         match result? {
275///             Message::Assistant(msg) => {
276///                 for block in &msg.message.content {
277///                     if let ContentBlock::Text(text) = block {
278///                         println!("Claude: {}", text.text);
279///                     }
280///                 }
281///             }
282///             _ => {}
283///         }
284///     }
285///
286///     Ok(())
287/// }
288/// ```
289pub async fn query_stream_with_content(
290    content: impl Into<Vec<UserContentBlock>>,
291    options: Option<ClaudeAgentOptions>,
292) -> Result<Pin<Box<dyn Stream<Item = Result<Message>> + Send>>> {
293    // Validate options first (fail fast - cheaper check)
294    let opts = options.unwrap_or_default();
295    validate_oneshot_options(&opts)?;
296
297    // Then validate content
298    let content_blocks = content.into();
299    UserContentBlock::validate_content(&content_blocks)?;
300
301    let query_prompt = QueryPrompt::Content(content_blocks);
302    let mut transport = SubprocessTransport::new(query_prompt, opts)?;
303    transport.connect().await?;
304
305    let stream = async_stream::stream! {
306        let mut message_stream = transport.read_messages();
307        while let Some(json_result) = message_stream.next().await {
308            match json_result {
309                Ok(json) => {
310                    match MessageParser::parse(json) {
311                        Ok(message) => yield Ok(message),
312                        Err(e) => {
313                            yield Err(e);
314                            break;
315                        }
316                    }
317                }
318                Err(e) => {
319                    yield Err(e);
320                    break;
321                }
322            }
323        }
324    };
325
326    Ok(Box::pin(stream))
327}
328
329#[cfg(test)]
330mod tests {
331    use super::*;
332    use crate::types::hooks::{HookContext, HookInput, HookJsonOutput, Hooks, SyncHookJsonOutput};
333    use crate::types::permissions::{PermissionResult, PermissionResultAllow};
334    use std::sync::Arc;
335
336    #[test]
337    fn test_validate_oneshot_options_accepts_default() {
338        let opts = ClaudeAgentOptions::default();
339        assert!(validate_oneshot_options(&opts).is_ok());
340    }
341
342    #[test]
343    fn test_validate_oneshot_options_accepts_normal_options() {
344        let opts = ClaudeAgentOptions::builder()
345            .model("claude-sonnet-4-20250514")
346            .cwd("/tmp")
347            .build();
348        assert!(validate_oneshot_options(&opts).is_ok());
349    }
350
351    #[test]
352    fn test_validate_oneshot_options_rejects_can_use_tool() {
353        let callback: crate::types::permissions::CanUseToolCallback = Arc::new(
354            |_tool_name, _tool_input, _context| {
355                Box::pin(async move { PermissionResult::Allow(PermissionResultAllow::default()) })
356            },
357        );
358
359        let opts = ClaudeAgentOptions::builder()
360            .can_use_tool(callback)
361            .build();
362
363        let result = validate_oneshot_options(&opts);
364        assert!(result.is_err());
365
366        let err = result.unwrap_err();
367        assert!(matches!(err, ClaudeError::InvalidConfig(_)));
368        assert!(err.to_string().contains("can_use_tool"));
369    }
370
371    #[test]
372    fn test_validate_oneshot_options_rejects_hooks() {
373        async fn test_hook(
374            _input: HookInput,
375            _tool_use_id: Option<String>,
376            _context: HookContext,
377        ) -> HookJsonOutput {
378            HookJsonOutput::Sync(SyncHookJsonOutput::default())
379        }
380
381        let mut hooks = Hooks::new();
382        hooks.add_stop(test_hook);
383        let hooks_map = hooks.build();
384
385        let opts = ClaudeAgentOptions::builder().hooks(hooks_map).build();
386
387        let result = validate_oneshot_options(&opts);
388        assert!(result.is_err());
389
390        let err = result.unwrap_err();
391        assert!(matches!(err, ClaudeError::InvalidConfig(_)));
392        assert!(err.to_string().contains("hooks"));
393    }
394
395    #[test]
396    fn test_validate_oneshot_options_rejects_both() {
397        let callback: crate::types::permissions::CanUseToolCallback = Arc::new(
398            |_tool_name, _tool_input, _context| {
399                Box::pin(async move { PermissionResult::Allow(PermissionResultAllow::default()) })
400            },
401        );
402
403        async fn test_hook(
404            _input: HookInput,
405            _tool_use_id: Option<String>,
406            _context: HookContext,
407        ) -> HookJsonOutput {
408            HookJsonOutput::Sync(SyncHookJsonOutput::default())
409        }
410
411        let mut hooks = Hooks::new();
412        hooks.add_stop(test_hook);
413        let hooks_map = hooks.build();
414
415        let opts = ClaudeAgentOptions::builder()
416            .can_use_tool(callback)
417            .hooks(hooks_map)
418            .build();
419
420        // Should fail on first check (can_use_tool)
421        let result = validate_oneshot_options(&opts);
422        assert!(result.is_err());
423        assert!(result.unwrap_err().to_string().contains("can_use_tool"));
424    }
425}