claude_code_agent_sdk/
query.rs

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