claude_agent_sdk/v2/
types.rs

1//! Type definitions for V2 API
2//!
3//! This module contains the simplified types used by the V2 API.
4
5use serde::{Deserialize, Serialize};
6use typed_builder::TypedBuilder;
7
8/// Simplified session options for V2 API
9///
10/// `SessionOptions` contains only the most commonly used configuration parameters,
11/// making it easier to configure Claude compared to the full `ClaudeAgentOptions`.
12///
13/// For advanced configuration, convert to `ClaudeAgentOptions` using `.into()`.
14///
15/// # Example
16///
17/// ```no_run
18/// use claude_agent_sdk::v2::SessionOptions;
19/// use claude_agent_sdk::v2::PermissionMode;
20///
21/// // Default configuration
22/// let options = SessionOptions::default();
23///
24/// // Custom configuration
25/// let options = SessionOptions::builder()
26///     .model("claude-sonnet-4-20250514".to_string())
27///     .max_turns(10)
28///     .permission_mode(PermissionMode::BypassPermissions)
29///     .build();
30/// ```
31#[derive(Debug, Clone, TypedBuilder, Serialize, Deserialize)]
32pub struct SessionOptions {
33    /// Model to use (None = system default)
34    #[builder(default, setter(strip_option))]
35    pub model: Option<String>,
36
37    /// Permission mode for tool execution
38    #[builder(default, setter(strip_option))]
39    pub permission_mode: Option<PermissionMode>,
40
41    /// Maximum budget in USD (None = no limit)
42    #[builder(default, setter(strip_option))]
43    pub max_budget_usd: Option<f64>,
44
45    /// Maximum number of conversation turns
46    #[builder(default, setter(strip_option))]
47    pub max_turns: Option<u32>,
48
49    /// Maximum thinking tokens for extended thinking
50    #[builder(default, setter(strip_option))]
51    pub max_thinking_tokens: Option<u32>,
52
53    /// Custom system prompt
54    #[builder(default, setter(strip_option))]
55    pub system_prompt: Option<String>,
56
57    /// Whether to include partial messages in stream
58    #[builder(default = false)]
59    pub include_partial_messages: bool,
60}
61
62impl Default for SessionOptions {
63    fn default() -> Self {
64        Self {
65            model: None,
66            permission_mode: None,
67            max_budget_usd: None,
68            max_turns: None,
69            max_thinking_tokens: None,
70            system_prompt: None,
71            include_partial_messages: false,
72        }
73    }
74}
75
76impl From<SessionOptions> for crate::types::config::ClaudeAgentOptions {
77    fn from(options: SessionOptions) -> Self {
78        // Convert permission_mode if present
79        let permission_mode: Option<crate::types::config::PermissionMode> =
80            options.permission_mode.map(|pm| pm.into());
81
82        // Convert system_prompt to SystemPrompt if present
83        let system_prompt: Option<crate::types::config::SystemPrompt> =
84            options.system_prompt.map(|text| crate::types::config::SystemPrompt::Text(text));
85
86        // Build ClaudeAgentOptions using builder with conditional field setting
87        // Since we can't use if-else with builder reassignment due to TypedBuilder's type system,
88        // we use a match to handle the different cases
89        match (options.model, permission_mode, options.max_budget_usd) {
90            (Some(model), Some(pm), Some(max_budget)) => {
91                crate::types::config::ClaudeAgentOptions::builder()
92                    .model(model)
93                    .permission_mode(pm)
94                    .max_budget_usd(max_budget)
95                    .max_turns(options.max_turns.unwrap_or(0))
96                    .max_thinking_tokens(options.max_thinking_tokens.unwrap_or(0))
97                    .system_prompt(system_prompt.unwrap_or(
98                        crate::types::config::SystemPrompt::Text(String::new())
99                    ))
100                    .include_partial_messages(options.include_partial_messages)
101                    .build()
102            }
103            (Some(model), Some(pm), None) => {
104                crate::types::config::ClaudeAgentOptions::builder()
105                    .model(model)
106                    .permission_mode(pm)
107                    .max_turns(options.max_turns.unwrap_or(0))
108                    .max_thinking_tokens(options.max_thinking_tokens.unwrap_or(0))
109                    .system_prompt(system_prompt.unwrap_or(
110                        crate::types::config::SystemPrompt::Text(String::new())
111                    ))
112                    .include_partial_messages(options.include_partial_messages)
113                    .build()
114            }
115            (Some(model), None, Some(max_budget)) => {
116                crate::types::config::ClaudeAgentOptions::builder()
117                    .model(model)
118                    .max_budget_usd(max_budget)
119                    .max_turns(options.max_turns.unwrap_or(0))
120                    .max_thinking_tokens(options.max_thinking_tokens.unwrap_or(0))
121                    .system_prompt(system_prompt.unwrap_or(
122                        crate::types::config::SystemPrompt::Text(String::new())
123                    ))
124                    .include_partial_messages(options.include_partial_messages)
125                    .build()
126            }
127            (Some(model), None, None) => {
128                crate::types::config::ClaudeAgentOptions::builder()
129                    .model(model)
130                    .max_turns(options.max_turns.unwrap_or(0))
131                    .max_thinking_tokens(options.max_thinking_tokens.unwrap_or(0))
132                    .system_prompt(system_prompt.unwrap_or(
133                        crate::types::config::SystemPrompt::Text(String::new())
134                    ))
135                    .include_partial_messages(options.include_partial_messages)
136                    .build()
137            }
138            (None, Some(pm), Some(max_budget)) => {
139                crate::types::config::ClaudeAgentOptions::builder()
140                    .permission_mode(pm)
141                    .max_budget_usd(max_budget)
142                    .max_turns(options.max_turns.unwrap_or(0))
143                    .max_thinking_tokens(options.max_thinking_tokens.unwrap_or(0))
144                    .system_prompt(system_prompt.unwrap_or(
145                        crate::types::config::SystemPrompt::Text(String::new())
146                    ))
147                    .include_partial_messages(options.include_partial_messages)
148                    .build()
149            }
150            (None, Some(pm), None) => {
151                crate::types::config::ClaudeAgentOptions::builder()
152                    .permission_mode(pm)
153                    .max_turns(options.max_turns.unwrap_or(0))
154                    .max_thinking_tokens(options.max_thinking_tokens.unwrap_or(0))
155                    .system_prompt(system_prompt.unwrap_or(
156                        crate::types::config::SystemPrompt::Text(String::new())
157                    ))
158                    .include_partial_messages(options.include_partial_messages)
159                    .build()
160            }
161            (None, None, Some(max_budget)) => {
162                crate::types::config::ClaudeAgentOptions::builder()
163                    .max_budget_usd(max_budget)
164                    .max_turns(options.max_turns.unwrap_or(0))
165                    .max_thinking_tokens(options.max_thinking_tokens.unwrap_or(0))
166                    .system_prompt(system_prompt.unwrap_or(
167                        crate::types::config::SystemPrompt::Text(String::new())
168                    ))
169                    .include_partial_messages(options.include_partial_messages)
170                    .build()
171            }
172            (None, None, None) => {
173                crate::types::config::ClaudeAgentOptions::builder()
174                    .max_turns(options.max_turns.unwrap_or(0))
175                    .max_thinking_tokens(options.max_thinking_tokens.unwrap_or(0))
176                    .system_prompt(system_prompt.unwrap_or(
177                        crate::types::config::SystemPrompt::Text(String::new())
178                    ))
179                    .include_partial_messages(options.include_partial_messages)
180                    .build()
181            }
182        }
183    }
184}
185
186/// Permission mode for tool execution
187///
188/// Controls how Claude requests permission to use tools.
189///
190/// # Variants
191///
192/// * `Default` - Default permission mode
193/// * `AcceptEdits` - Accept edits automatically
194/// * `Plan` - Plan mode
195/// * `BypassPermissions` - Auto-approve all tool usage
196#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
197pub enum PermissionMode {
198    /// Default permission mode
199    Default,
200
201    /// Accept edits automatically
202    AcceptEdits,
203
204    /// Plan mode
205    Plan,
206
207    /// Auto-approve all tool usage
208    BypassPermissions,
209}
210
211impl From<PermissionMode> for crate::types::config::PermissionMode {
212    fn from(mode: PermissionMode) -> Self {
213        match mode {
214            PermissionMode::Default => Self::Default,
215            PermissionMode::AcceptEdits => Self::AcceptEdits,
216            PermissionMode::Plan => Self::Plan,
217            PermissionMode::BypassPermissions => Self::BypassPermissions,
218        }
219    }
220}
221
222/// Result of a one-shot prompt
223///
224/// Contains the response text and metadata from a `prompt()` call.
225///
226/// # Example
227///
228/// ```no_run
229/// # use claude_agent_sdk::v2::PromptResult;
230/// let result = PromptResult {
231///     content: "The answer is 4".to_string(),
232///     input_tokens: 15,
233///     output_tokens: 5,
234///     model: Some("claude-sonnet-4-20250514".to_string()),
235/// };
236///
237/// println!("Response: {}", result.content);
238/// println!("Total tokens: {}", result.total_tokens());
239/// ```
240#[derive(Debug, Clone, Serialize, Deserialize)]
241pub struct PromptResult {
242    /// The text content of Claude's response
243    pub content: String,
244
245    /// Number of tokens in the input
246    pub input_tokens: u64,
247
248    /// Number of tokens in the output
249    pub output_tokens: u64,
250
251    /// Model used for generation (if known)
252    pub model: Option<String>,
253}
254
255impl PromptResult {
256    /// Get the total token usage (input + output)
257    pub fn total_tokens(&self) -> u64 {
258        self.input_tokens + self.output_tokens
259    }
260
261    /// Get the cost in USD (approximate)
262    ///
263    /// This is a rough estimate based on public pricing.
264    /// Actual costs may vary.
265    pub fn estimated_cost_usd(&self) -> f64 {
266        // Rough pricing (subject to change)
267        // Input: $3/M tokens, Output: $15/M tokens
268        let input_cost = (self.input_tokens as f64) / 1_000_000.0 * 3.0;
269        let output_cost = (self.output_tokens as f64) / 1_000_000.0 * 15.0;
270        input_cost + output_cost
271    }
272}
273
274/// Simplified message type for V2 API
275///
276/// This is a simplified version of the full `Message` type,
277/// containing only the most essential information.
278///
279/// # Variants
280///
281/// * `User` - Message from the user
282/// * `Assistant` - Response from Claude
283/// * `ToolResult` - Result from a tool execution
284#[derive(Debug, Clone, Serialize, Deserialize)]
285pub enum Message {
286    /// Message from the user
287    User {
288        /// The text content
289        content: String,
290    },
291
292    /// Response from Claude
293    Assistant {
294        /// The text content
295        content: String,
296    },
297
298    /// Result from a tool execution
299    ToolResult {
300        /// Tool name
301        tool_name: String,
302        /// Tool result
303        result: String,
304    },
305}
306
307impl Message {
308    /// Get the content text as a string (if applicable)
309    pub fn as_text(&self) -> Option<&str> {
310        match self {
311            Message::User { content } => Some(content),
312            Message::Assistant { content } => Some(content),
313            Message::ToolResult { .. } => None,
314        }
315    }
316
317    /// Check if this is a user message
318    pub fn is_user(&self) -> bool {
319        matches!(self, Message::User { .. })
320    }
321
322    /// Check if this is an assistant message
323    pub fn is_assistant(&self) -> bool {
324        matches!(self, Message::Assistant { .. })
325    }
326
327    /// Check if this is a tool result
328    pub fn is_tool_result(&self) -> bool {
329        matches!(self, Message::ToolResult { .. })
330    }
331}
332
333impl From<crate::types::messages::Message> for Message {
334    fn from(msg: crate::types::messages::Message) -> Self {
335        match msg {
336            crate::types::messages::Message::User(user_msg) => {
337                // Extract text from user message
338                // UserMessage has text: Option<String> and content: Option<Vec<ContentBlock>>
339                let content = if let Some(text) = user_msg.text {
340                    text
341                } else if let Some(blocks) = user_msg.content {
342                    blocks
343                        .iter()
344                        .filter_map(|block| match block {
345                            crate::types::messages::ContentBlock::Text(text) => Some(text.text.clone()),
346                            _ => None,
347                        })
348                        .collect::<Vec<_>>()
349                        .join("\n")
350                } else {
351                    String::new()
352                };
353
354                Message::User { content }
355            }
356            crate::types::messages::Message::Assistant(assist_msg) => {
357                // Extract text from assistant message
358                let content = assist_msg
359                    .message
360                    .content
361                    .iter()
362                    .filter_map(|block| match block {
363                        crate::types::messages::ContentBlock::Text(text) => Some(text.text.clone()),
364                        _ => None,
365                    })
366                    .collect::<Vec<_>>()
367                    .join("\n");
368
369                Message::Assistant { content }
370            }
371            _ => Message::Assistant {
372                content: String::new(),
373            },
374        }
375    }
376}
377
378#[cfg(test)]
379mod tests {
380    use super::*;
381
382    #[test]
383    fn test_session_options_builder() {
384        let options = SessionOptions::builder()
385            .model("claude-sonnet-4-20250514".to_string())
386            .max_turns(5)
387            .build();
388
389        assert_eq!(options.model, Some("claude-sonnet-4-20250514".to_string()));
390        assert_eq!(options.max_turns, Some(5));
391    }
392
393    #[test]
394    fn test_permission_mode_conversion() {
395        let mode = PermissionMode::BypassPermissions;
396        let converted: crate::types::config::PermissionMode = mode.into();
397        assert!(matches!(
398            converted,
399            crate::types::config::PermissionMode::BypassPermissions
400        ));
401    }
402
403    #[test]
404    fn test_prompt_result_total_tokens() {
405        let result = PromptResult {
406            content: "Test".to_string(),
407            input_tokens: 100,
408            output_tokens: 50,
409            model: None,
410        };
411
412        assert_eq!(result.total_tokens(), 150);
413    }
414
415    #[test]
416    fn test_message_is_user() {
417        let msg = Message::User {
418            content: "Hello".to_string(),
419        };
420
421        assert!(msg.is_user());
422        assert!(!msg.is_assistant());
423        assert_eq!(msg.as_text(), Some("Hello"));
424    }
425
426    #[test]
427    fn test_message_is_assistant() {
428        let msg = Message::Assistant {
429            content: "Hi there!".to_string(),
430        };
431
432        assert!(!msg.is_user());
433        assert!(msg.is_assistant());
434        assert_eq!(msg.as_text(), Some("Hi there!"));
435    }
436
437    #[test]
438    fn test_message_is_tool_result() {
439        let msg = Message::ToolResult {
440            tool_name: "calculator".to_string(),
441            result: "42".to_string(),
442        };
443
444        assert!(msg.is_tool_result());
445        assert!(!msg.is_user());
446        assert!(!msg.is_assistant());
447        assert_eq!(msg.as_text(), None);
448    }
449
450    #[test]
451    fn test_prompt_result_cost_estimation() {
452        let result = PromptResult {
453            content: "Test".to_string(),
454            input_tokens: 1_000_000, // 1M input tokens
455            output_tokens: 1_000_000, // 1M output tokens
456            model: None,
457        };
458
459        // 1M input * $3/M = $3
460        // 1M output * $15/M = $15
461        // Total = $18
462        let cost = result.estimated_cost_usd();
463        assert!((cost - 18.0).abs() < 0.01); // Allow small floating point error
464    }
465}