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    /// Agent run was cancelled via a [`CancellationToken`].
320    Cancelled {
321        total_turns: u32,
322        input_tokens: u64,
323        output_tokens: u64,
324    },
325}
326
327/// Information about a pending tool call that was extracted from the LLM response.
328#[derive(Clone, Debug, Serialize, Deserialize)]
329pub struct PendingToolCallInfo {
330    /// Unique ID for this tool call (from LLM)
331    pub id: String,
332    /// Tool name string (for LLM protocol)
333    pub name: String,
334    /// Human-readable display name
335    pub display_name: String,
336    /// Tool input parameters
337    pub input: serde_json::Value,
338    /// Optional context for tools that prepare asynchronously and execute later.
339    #[serde(default, skip_serializing_if = "Option::is_none")]
340    pub listen_context: Option<ListenExecutionContext>,
341}
342
343/// Context captured for listen/execute tools while awaiting confirmation.
344#[derive(Clone, Debug, Serialize, Deserialize)]
345pub struct ListenExecutionContext {
346    /// Opaque operation identifier used to execute/cancel.
347    pub operation_id: String,
348    /// Revision used for optimistic concurrency checks.
349    pub revision: u64,
350    /// Snapshot shown to the user during confirmation.
351    pub snapshot: serde_json::Value,
352    /// Optional expiration timestamp (RFC3339).
353    #[serde(
354        default,
355        skip_serializing_if = "Option::is_none",
356        with = "time::serde::rfc3339::option"
357    )]
358    pub expires_at: Option<OffsetDateTime>,
359}
360
361/// Continuation state that allows resuming the agent loop.
362///
363/// This contains all the internal state needed to continue execution
364/// after receiving a confirmation decision. Pass this back when resuming.
365#[derive(Clone, Debug, Serialize, Deserialize)]
366pub struct AgentContinuation {
367    /// Thread ID (used for validation on resume)
368    pub thread_id: ThreadId,
369    /// Current turn number
370    pub turn: usize,
371    /// Total token usage so far
372    pub total_usage: TokenUsage,
373    /// Token usage for this specific turn (from the LLM call that generated tool calls)
374    pub turn_usage: TokenUsage,
375    /// All pending tool calls from this turn
376    pub pending_tool_calls: Vec<PendingToolCallInfo>,
377    /// Index of the tool call awaiting confirmation
378    pub awaiting_index: usize,
379    /// Tool results already collected (for tools before the awaiting one)
380    pub completed_results: Vec<(String, ToolResult)>,
381    /// Agent state snapshot
382    pub state: AgentState,
383}
384
385/// Input to start or resume an agent run.
386#[derive(Debug)]
387pub enum AgentInput {
388    /// Start a new conversation with user text.
389    Text(String),
390
391    /// Start a new conversation with rich content (text, images, documents).
392    Message(Vec<ContentBlock>),
393
394    /// Resume after a confirmation decision.
395    Resume {
396        /// The continuation state from `AwaitingConfirmation` (boxed for enum size efficiency).
397        continuation: Box<AgentContinuation>,
398        /// ID of the tool call being confirmed/rejected.
399        tool_call_id: String,
400        /// Whether the user confirmed the action.
401        confirmed: bool,
402        /// Optional reason if rejected.
403        rejection_reason: Option<String>,
404    },
405
406    /// Continue to the next turn (for single-turn mode).
407    ///
408    /// Use this after `TurnOutcome::NeedsMoreTurns` to execute the next turn.
409    /// The message history already contains tool results from the previous turn.
410    Continue,
411}
412
413/// Result of tool execution - may indicate async operation in progress.
414#[derive(Clone, Debug, Serialize, Deserialize)]
415pub enum ToolOutcome {
416    /// Tool completed synchronously with success
417    Success(ToolResult),
418
419    /// Tool completed synchronously with failure
420    Failed(ToolResult),
421
422    /// Tool started an async operation - must stream status to completion
423    InProgress {
424        /// Identifier for the operation (to query status)
425        operation_id: String,
426        /// Initial message for the user
427        message: String,
428    },
429}
430
431impl ToolOutcome {
432    #[must_use]
433    pub fn success(output: impl Into<String>) -> Self {
434        Self::Success(ToolResult::success(output))
435    }
436
437    #[must_use]
438    pub fn failed(message: impl Into<String>) -> Self {
439        Self::Failed(ToolResult::error(message))
440    }
441
442    #[must_use]
443    pub fn in_progress(operation_id: impl Into<String>, message: impl Into<String>) -> Self {
444        Self::InProgress {
445            operation_id: operation_id.into(),
446            message: message.into(),
447        }
448    }
449
450    /// Returns true if operation is still in progress
451    #[must_use]
452    pub const fn is_in_progress(&self) -> bool {
453        matches!(self, Self::InProgress { .. })
454    }
455}
456
457// ============================================================================
458// Tool Execution Idempotency Types
459// ============================================================================
460
461/// Status of a tool execution for idempotency tracking.
462#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
463pub enum ExecutionStatus {
464    /// Execution started but not yet completed
465    InFlight,
466    /// Execution completed (success or failure)
467    Completed,
468}
469
470/// Record of a tool execution for idempotency.
471///
472/// This struct tracks tool executions to prevent duplicate execution when
473/// the agent loop retries after a failure. The write-ahead pattern ensures
474/// that execution intent is recorded BEFORE calling the tool, and updated
475/// with results AFTER completion.
476#[derive(Clone, Debug, Serialize, Deserialize)]
477pub struct ToolExecution {
478    /// The tool call ID from the LLM (unique per invocation)
479    pub tool_call_id: String,
480    /// Thread this execution belongs to
481    pub thread_id: ThreadId,
482    /// Tool name
483    pub tool_name: String,
484    /// Display name
485    pub display_name: String,
486    /// Input parameters (for verification)
487    pub input: serde_json::Value,
488    /// Current status
489    pub status: ExecutionStatus,
490    /// Result if completed
491    pub result: Option<ToolResult>,
492    /// For async tools: the operation ID returned by `execute()`
493    pub operation_id: Option<String>,
494    /// Timestamp when execution started
495    #[serde(with = "time::serde::rfc3339")]
496    pub started_at: OffsetDateTime,
497    /// Timestamp when execution completed
498    #[serde(with = "time::serde::rfc3339::option")]
499    pub completed_at: Option<OffsetDateTime>,
500}
501
502impl ToolExecution {
503    /// Create a new in-flight execution record.
504    #[must_use]
505    pub fn new_in_flight(
506        tool_call_id: impl Into<String>,
507        thread_id: ThreadId,
508        tool_name: impl Into<String>,
509        display_name: impl Into<String>,
510        input: serde_json::Value,
511        started_at: OffsetDateTime,
512    ) -> Self {
513        Self {
514            tool_call_id: tool_call_id.into(),
515            thread_id,
516            tool_name: tool_name.into(),
517            display_name: display_name.into(),
518            input,
519            status: ExecutionStatus::InFlight,
520            result: None,
521            operation_id: None,
522            started_at,
523            completed_at: None,
524        }
525    }
526
527    /// Mark this execution as completed with a result.
528    pub fn complete(&mut self, result: ToolResult) {
529        self.status = ExecutionStatus::Completed;
530        self.result = Some(result);
531        self.completed_at = Some(OffsetDateTime::now_utc());
532    }
533
534    /// Set the operation ID for async tool tracking.
535    pub fn set_operation_id(&mut self, operation_id: impl Into<String>) {
536        self.operation_id = Some(operation_id.into());
537    }
538
539    /// Returns true if this execution is still in flight.
540    #[must_use]
541    pub fn is_in_flight(&self) -> bool {
542        self.status == ExecutionStatus::InFlight
543    }
544
545    /// Returns true if this execution has completed.
546    #[must_use]
547    pub fn is_completed(&self) -> bool {
548        self.status == ExecutionStatus::Completed
549    }
550}
551
552/// Outcome of running a single turn.
553///
554/// This is returned by `run_turn` to indicate what happened and what to do next.
555#[derive(Debug)]
556pub enum TurnOutcome {
557    /// Turn completed successfully, but more turns are needed.
558    ///
559    /// Tools were executed and their results are stored in the message history.
560    /// Call `run_turn` again with `AgentInput::Continue` to proceed.
561    NeedsMoreTurns {
562        /// The turn number that just completed
563        turn: usize,
564        /// Token usage for this turn
565        turn_usage: TokenUsage,
566        /// Cumulative token usage so far
567        total_usage: TokenUsage,
568    },
569
570    /// Agent completed successfully (no more tool calls).
571    Done {
572        /// Total turns executed
573        total_turns: u32,
574        /// Total input tokens consumed
575        input_tokens: u64,
576        /// Total output tokens consumed
577        output_tokens: u64,
578    },
579
580    /// A tool requires user confirmation.
581    ///
582    /// Present this to the user and call `run_turn` with `AgentInput::Resume`
583    /// to continue.
584    AwaitingConfirmation {
585        /// ID of the pending tool call (from LLM)
586        tool_call_id: String,
587        /// Tool name string (for LLM protocol)
588        tool_name: String,
589        /// Human-readable display name
590        display_name: String,
591        /// Tool input parameters
592        input: serde_json::Value,
593        /// Description of what confirmation is needed
594        description: String,
595        /// Continuation state for resuming (boxed for enum size efficiency)
596        continuation: Box<AgentContinuation>,
597    },
598
599    /// Model refused the request (safety/policy).
600    Refusal {
601        /// Total turns executed
602        total_turns: u32,
603        /// Total input tokens consumed
604        input_tokens: u64,
605        /// Total output tokens consumed
606        output_tokens: u64,
607    },
608
609    /// The turn was cancelled via a [`CancellationToken`].
610    Cancelled {
611        /// Total turns executed before cancellation
612        total_turns: u32,
613        /// Total input tokens consumed
614        input_tokens: u64,
615        /// Total output tokens consumed
616        output_tokens: u64,
617    },
618
619    /// An error occurred.
620    Error(AgentError),
621}