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}