Skip to main content

agent_sdk/
types.rs

1//! Core types for the agent SDK.
2//!
3//! This module contains the fundamental types used throughout the SDK:
4//!
5//! - [`ThreadId`]: Unique identifier for conversation threads
6//! - [`AgentConfig`]: Configuration for the agent loop
7//! - [`TokenUsage`]: Token consumption statistics
8//! - [`ToolResult`]: Result returned from tool execution
9//! - [`ToolTier`]: Permission tiers for tools
10//! - [`AgentRunState`]: Outcome of running the agent loop (looping mode)
11//! - [`TurnOutcome`]: Outcome of running a single turn (single-turn mode)
12//! - [`AgentInput`]: Input to start or resume an agent run
13//! - [`AgentContinuation`]: Opaque state for resuming after confirmation
14//! - [`AgentState`]: Checkpointable agent state
15
16use crate::llm::ContentBlock;
17use serde::{Deserialize, Serialize};
18use std::collections::HashMap;
19use time::OffsetDateTime;
20use uuid::Uuid;
21
22/// Unique identifier for a conversation thread
23#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
24pub struct ThreadId(pub String);
25
26impl ThreadId {
27    #[must_use]
28    pub fn new() -> Self {
29        Self(Uuid::new_v4().to_string())
30    }
31
32    #[must_use]
33    pub fn from_string(s: impl Into<String>) -> Self {
34        Self(s.into())
35    }
36}
37
38impl Default for ThreadId {
39    fn default() -> Self {
40        Self::new()
41    }
42}
43
44impl std::fmt::Display for ThreadId {
45    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46        write!(f, "{}", self.0)
47    }
48}
49
50/// Configuration for the agent loop
51#[derive(Clone, Debug)]
52pub struct AgentConfig {
53    /// Maximum number of turns (LLM round-trips) before stopping
54    pub max_turns: Option<usize>,
55    /// Maximum tokens per response.
56    ///
57    /// If `None`, the SDK uses the provider/model-specific default.
58    pub max_tokens: Option<u32>,
59    /// System prompt for the agent
60    pub system_prompt: String,
61    /// Model identifier
62    pub model: String,
63    /// Retry configuration for transient errors
64    pub retry: RetryConfig,
65    /// Enable streaming responses from the LLM.
66    ///
67    /// When `true`, emits `TextDelta` and `ThinkingDelta` events as text arrives
68    /// in real-time. When `false` (default), waits for the complete response
69    /// before emitting `Text` and `Thinking` events.
70    pub streaming: bool,
71}
72
73impl Default for AgentConfig {
74    fn default() -> Self {
75        Self {
76            max_turns: None,
77            max_tokens: None,
78            system_prompt: String::new(),
79            model: String::from("claude-sonnet-4-5-20250929"),
80            retry: RetryConfig::default(),
81            streaming: false,
82        }
83    }
84}
85
86/// Configuration for retry behavior on transient errors.
87#[derive(Clone, Debug)]
88pub struct RetryConfig {
89    /// Maximum number of retry attempts
90    pub max_retries: u32,
91    /// Base delay in milliseconds for exponential backoff
92    pub base_delay_ms: u64,
93    /// Maximum delay cap in milliseconds
94    pub max_delay_ms: u64,
95}
96
97impl Default for RetryConfig {
98    fn default() -> Self {
99        Self {
100            max_retries: 5,
101            base_delay_ms: 1000,
102            max_delay_ms: 120_000,
103        }
104    }
105}
106
107impl RetryConfig {
108    /// Create a retry config with no retries (for testing)
109    #[must_use]
110    pub const fn no_retry() -> Self {
111        Self {
112            max_retries: 0,
113            base_delay_ms: 0,
114            max_delay_ms: 0,
115        }
116    }
117
118    /// Create a retry config with fast retries (for testing)
119    #[must_use]
120    pub const fn fast() -> Self {
121        Self {
122            max_retries: 5,
123            base_delay_ms: 10,
124            max_delay_ms: 100,
125        }
126    }
127}
128
129/// Token usage statistics
130#[derive(Clone, Debug, Default, Serialize, Deserialize)]
131pub struct TokenUsage {
132    pub input_tokens: u32,
133    pub output_tokens: u32,
134}
135
136impl TokenUsage {
137    pub const fn add(&mut self, other: &Self) {
138        self.input_tokens += other.input_tokens;
139        self.output_tokens += other.output_tokens;
140    }
141}
142
143/// Result of a tool execution
144#[derive(Clone, Debug, Serialize, Deserialize)]
145pub struct ToolResult {
146    /// Whether the tool execution succeeded
147    pub success: bool,
148    /// Output content (displayed to user and fed back to LLM)
149    pub output: String,
150    /// Optional structured data
151    pub data: Option<serde_json::Value>,
152    /// Optional documents (PDFs, images) to pass back to the LLM as native content blocks.
153    /// The agent appends these as `ContentBlock::Document` / `ContentBlock::Image` blocks
154    /// in the same user message as the tool result, so the model can read them directly.
155    #[serde(default, skip_serializing_if = "Vec::is_empty")]
156    pub documents: Vec<crate::llm::ContentSource>,
157    /// Duration of the tool execution in milliseconds
158    pub duration_ms: Option<u64>,
159}
160
161impl ToolResult {
162    #[must_use]
163    pub fn success(output: impl Into<String>) -> Self {
164        Self {
165            success: true,
166            output: output.into(),
167            data: None,
168            documents: Vec::new(),
169            duration_ms: None,
170        }
171    }
172
173    #[must_use]
174    pub fn success_with_data(output: impl Into<String>, data: serde_json::Value) -> Self {
175        Self {
176            success: true,
177            output: output.into(),
178            data: Some(data),
179            documents: Vec::new(),
180            duration_ms: None,
181        }
182    }
183
184    #[must_use]
185    pub fn error(message: impl Into<String>) -> Self {
186        Self {
187            success: false,
188            output: message.into(),
189            data: None,
190            documents: Vec::new(),
191            duration_ms: None,
192        }
193    }
194
195    #[must_use]
196    pub const fn with_duration(mut self, duration_ms: u64) -> Self {
197        self.duration_ms = Some(duration_ms);
198        self
199    }
200
201    /// Attach documents (PDFs, images) to be sent back to the LLM as native content blocks.
202    ///
203    /// Use this when a tool produces a binary document that the model should read directly,
204    /// e.g. a decrypted PDF that Anthropic can parse natively via its document API.
205    ///
206    /// # Example
207    /// ```rust,ignore
208    /// use agent_sdk::{ToolResult, ContentSource};
209    ///
210    /// Ok(ToolResult::success("PDF decrypted.").with_documents(vec![
211    ///     ContentSource::new("application/pdf", base64_data),
212    /// ]))
213    /// ```
214    #[must_use]
215    pub fn with_documents(mut self, documents: Vec<crate::llm::ContentSource>) -> Self {
216        self.documents = documents;
217        self
218    }
219}
220
221/// Permission tier for tools
222#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
223pub enum ToolTier {
224    /// Read-only, always allowed (e.g., `get_balance`)
225    Observe,
226    /// Requires confirmation before execution.
227    /// The application determines the confirmation type (normal, PIN, biometric).
228    Confirm,
229}
230
231/// Snapshot of agent state for checkpointing
232#[derive(Clone, Debug, Serialize, Deserialize)]
233pub struct AgentState {
234    pub thread_id: ThreadId,
235    pub turn_count: usize,
236    pub total_usage: TokenUsage,
237    pub metadata: HashMap<String, serde_json::Value>,
238    #[serde(with = "time::serde::rfc3339")]
239    pub created_at: OffsetDateTime,
240}
241
242impl AgentState {
243    #[must_use]
244    pub fn new(thread_id: ThreadId) -> Self {
245        Self {
246            thread_id,
247            turn_count: 0,
248            total_usage: TokenUsage::default(),
249            metadata: HashMap::new(),
250            created_at: OffsetDateTime::now_utc(),
251        }
252    }
253}
254
255/// Error from the agent loop.
256#[derive(Debug, Clone)]
257pub struct AgentError {
258    /// Error message
259    pub message: String,
260    /// Whether the error is potentially recoverable
261    pub recoverable: bool,
262}
263
264impl AgentError {
265    #[must_use]
266    pub fn new(message: impl Into<String>, recoverable: bool) -> Self {
267        Self {
268            message: message.into(),
269            recoverable,
270        }
271    }
272}
273
274impl std::fmt::Display for AgentError {
275    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
276        write!(f, "{}", self.message)
277    }
278}
279
280impl std::error::Error for AgentError {}
281
282/// Outcome of running the agent loop.
283#[derive(Debug)]
284pub enum AgentRunState {
285    /// Agent completed successfully.
286    Done {
287        total_turns: u32,
288        input_tokens: u64,
289        output_tokens: u64,
290    },
291
292    /// Agent was refused by the model (safety/policy).
293    Refusal {
294        total_turns: u32,
295        input_tokens: u64,
296        output_tokens: u64,
297    },
298
299    /// Agent encountered an error.
300    Error(AgentError),
301
302    /// Agent is awaiting confirmation for a tool call.
303    /// The application should present this to the user and call resume.
304    AwaitingConfirmation {
305        /// ID of the pending tool call (from LLM)
306        tool_call_id: String,
307        /// Tool name string (for LLM protocol)
308        tool_name: String,
309        /// Human-readable display name
310        display_name: String,
311        /// Tool input parameters
312        input: serde_json::Value,
313        /// Description of what confirmation is needed
314        description: String,
315        /// Continuation state for resuming (boxed for enum size efficiency)
316        continuation: Box<AgentContinuation>,
317    },
318}
319
320/// Information about a pending tool call that was extracted from the LLM response.
321#[derive(Clone, Debug, Serialize, Deserialize)]
322pub struct PendingToolCallInfo {
323    /// Unique ID for this tool call (from LLM)
324    pub id: String,
325    /// Tool name string (for LLM protocol)
326    pub name: String,
327    /// Human-readable display name
328    pub display_name: String,
329    /// Tool input parameters
330    pub input: serde_json::Value,
331    /// Optional context for tools that prepare asynchronously and execute later.
332    #[serde(default, skip_serializing_if = "Option::is_none")]
333    pub listen_context: Option<ListenExecutionContext>,
334}
335
336/// Context captured for listen/execute tools while awaiting confirmation.
337#[derive(Clone, Debug, Serialize, Deserialize)]
338pub struct ListenExecutionContext {
339    /// Opaque operation identifier used to execute/cancel.
340    pub operation_id: String,
341    /// Revision used for optimistic concurrency checks.
342    pub revision: u64,
343    /// Snapshot shown to the user during confirmation.
344    pub snapshot: serde_json::Value,
345    /// Optional expiration timestamp (RFC3339).
346    #[serde(
347        default,
348        skip_serializing_if = "Option::is_none",
349        with = "time::serde::rfc3339::option"
350    )]
351    pub expires_at: Option<OffsetDateTime>,
352}
353
354/// Continuation state that allows resuming the agent loop.
355///
356/// This contains all the internal state needed to continue execution
357/// after receiving a confirmation decision. Pass this back when resuming.
358#[derive(Clone, Debug, Serialize, Deserialize)]
359pub struct AgentContinuation {
360    /// Thread ID (used for validation on resume)
361    pub thread_id: ThreadId,
362    /// Current turn number
363    pub turn: usize,
364    /// Total token usage so far
365    pub total_usage: TokenUsage,
366    /// Token usage for this specific turn (from the LLM call that generated tool calls)
367    pub turn_usage: TokenUsage,
368    /// All pending tool calls from this turn
369    pub pending_tool_calls: Vec<PendingToolCallInfo>,
370    /// Index of the tool call awaiting confirmation
371    pub awaiting_index: usize,
372    /// Tool results already collected (for tools before the awaiting one)
373    pub completed_results: Vec<(String, ToolResult)>,
374    /// Agent state snapshot
375    pub state: AgentState,
376}
377
378/// Input to start or resume an agent run.
379#[derive(Debug)]
380pub enum AgentInput {
381    /// Start a new conversation with user text.
382    Text(String),
383
384    /// Start a new conversation with rich content (text, images, documents).
385    Message(Vec<ContentBlock>),
386
387    /// Resume after a confirmation decision.
388    Resume {
389        /// The continuation state from `AwaitingConfirmation` (boxed for enum size efficiency).
390        continuation: Box<AgentContinuation>,
391        /// ID of the tool call being confirmed/rejected.
392        tool_call_id: String,
393        /// Whether the user confirmed the action.
394        confirmed: bool,
395        /// Optional reason if rejected.
396        rejection_reason: Option<String>,
397    },
398
399    /// Continue to the next turn (for single-turn mode).
400    ///
401    /// Use this after `TurnOutcome::NeedsMoreTurns` to execute the next turn.
402    /// The message history already contains tool results from the previous turn.
403    Continue,
404}
405
406/// Result of tool execution - may indicate async operation in progress.
407#[derive(Clone, Debug, Serialize, Deserialize)]
408pub enum ToolOutcome {
409    /// Tool completed synchronously with success
410    Success(ToolResult),
411
412    /// Tool completed synchronously with failure
413    Failed(ToolResult),
414
415    /// Tool started an async operation - must stream status to completion
416    InProgress {
417        /// Identifier for the operation (to query status)
418        operation_id: String,
419        /// Initial message for the user
420        message: String,
421    },
422}
423
424impl ToolOutcome {
425    #[must_use]
426    pub fn success(output: impl Into<String>) -> Self {
427        Self::Success(ToolResult::success(output))
428    }
429
430    #[must_use]
431    pub fn failed(message: impl Into<String>) -> Self {
432        Self::Failed(ToolResult::error(message))
433    }
434
435    #[must_use]
436    pub fn in_progress(operation_id: impl Into<String>, message: impl Into<String>) -> Self {
437        Self::InProgress {
438            operation_id: operation_id.into(),
439            message: message.into(),
440        }
441    }
442
443    /// Returns true if operation is still in progress
444    #[must_use]
445    pub const fn is_in_progress(&self) -> bool {
446        matches!(self, Self::InProgress { .. })
447    }
448}
449
450// ============================================================================
451// Tool Execution Idempotency Types
452// ============================================================================
453
454/// Status of a tool execution for idempotency tracking.
455#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
456pub enum ExecutionStatus {
457    /// Execution started but not yet completed
458    InFlight,
459    /// Execution completed (success or failure)
460    Completed,
461}
462
463/// Record of a tool execution for idempotency.
464///
465/// This struct tracks tool executions to prevent duplicate execution when
466/// the agent loop retries after a failure. The write-ahead pattern ensures
467/// that execution intent is recorded BEFORE calling the tool, and updated
468/// with results AFTER completion.
469#[derive(Clone, Debug, Serialize, Deserialize)]
470pub struct ToolExecution {
471    /// The tool call ID from the LLM (unique per invocation)
472    pub tool_call_id: String,
473    /// Thread this execution belongs to
474    pub thread_id: ThreadId,
475    /// Tool name
476    pub tool_name: String,
477    /// Display name
478    pub display_name: String,
479    /// Input parameters (for verification)
480    pub input: serde_json::Value,
481    /// Current status
482    pub status: ExecutionStatus,
483    /// Result if completed
484    pub result: Option<ToolResult>,
485    /// For async tools: the operation ID returned by `execute()`
486    pub operation_id: Option<String>,
487    /// Timestamp when execution started
488    #[serde(with = "time::serde::rfc3339")]
489    pub started_at: OffsetDateTime,
490    /// Timestamp when execution completed
491    #[serde(with = "time::serde::rfc3339::option")]
492    pub completed_at: Option<OffsetDateTime>,
493}
494
495impl ToolExecution {
496    /// Create a new in-flight execution record.
497    #[must_use]
498    pub fn new_in_flight(
499        tool_call_id: impl Into<String>,
500        thread_id: ThreadId,
501        tool_name: impl Into<String>,
502        display_name: impl Into<String>,
503        input: serde_json::Value,
504        started_at: OffsetDateTime,
505    ) -> Self {
506        Self {
507            tool_call_id: tool_call_id.into(),
508            thread_id,
509            tool_name: tool_name.into(),
510            display_name: display_name.into(),
511            input,
512            status: ExecutionStatus::InFlight,
513            result: None,
514            operation_id: None,
515            started_at,
516            completed_at: None,
517        }
518    }
519
520    /// Mark this execution as completed with a result.
521    pub fn complete(&mut self, result: ToolResult) {
522        self.status = ExecutionStatus::Completed;
523        self.result = Some(result);
524        self.completed_at = Some(OffsetDateTime::now_utc());
525    }
526
527    /// Set the operation ID for async tool tracking.
528    pub fn set_operation_id(&mut self, operation_id: impl Into<String>) {
529        self.operation_id = Some(operation_id.into());
530    }
531
532    /// Returns true if this execution is still in flight.
533    #[must_use]
534    pub fn is_in_flight(&self) -> bool {
535        self.status == ExecutionStatus::InFlight
536    }
537
538    /// Returns true if this execution has completed.
539    #[must_use]
540    pub fn is_completed(&self) -> bool {
541        self.status == ExecutionStatus::Completed
542    }
543}
544
545/// Outcome of running a single turn.
546///
547/// This is returned by `run_turn` to indicate what happened and what to do next.
548#[derive(Debug)]
549pub enum TurnOutcome {
550    /// Turn completed successfully, but more turns are needed.
551    ///
552    /// Tools were executed and their results are stored in the message history.
553    /// Call `run_turn` again with `AgentInput::Continue` to proceed.
554    NeedsMoreTurns {
555        /// The turn number that just completed
556        turn: usize,
557        /// Token usage for this turn
558        turn_usage: TokenUsage,
559        /// Cumulative token usage so far
560        total_usage: TokenUsage,
561    },
562
563    /// Agent completed successfully (no more tool calls).
564    Done {
565        /// Total turns executed
566        total_turns: u32,
567        /// Total input tokens consumed
568        input_tokens: u64,
569        /// Total output tokens consumed
570        output_tokens: u64,
571    },
572
573    /// A tool requires user confirmation.
574    ///
575    /// Present this to the user and call `run_turn` with `AgentInput::Resume`
576    /// to continue.
577    AwaitingConfirmation {
578        /// ID of the pending tool call (from LLM)
579        tool_call_id: String,
580        /// Tool name string (for LLM protocol)
581        tool_name: String,
582        /// Human-readable display name
583        display_name: String,
584        /// Tool input parameters
585        input: serde_json::Value,
586        /// Description of what confirmation is needed
587        description: String,
588        /// Continuation state for resuming (boxed for enum size efficiency)
589        continuation: Box<AgentContinuation>,
590    },
591
592    /// Model refused the request (safety/policy).
593    Refusal {
594        /// Total turns executed
595        total_turns: u32,
596        /// Total input tokens consumed
597        input_tokens: u64,
598        /// Total output tokens consumed
599        output_tokens: u64,
600    },
601
602    /// An error occurred.
603    Error(AgentError),
604}