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}