agent-sdk 0.8.0

Rust Agent SDK for building LLM agents
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
//! Core types for the agent SDK.
//!
//! This module contains the fundamental types used throughout the SDK:
//!
//! - [`ThreadId`]: Unique identifier for conversation threads
//! - [`AgentConfig`]: Configuration for the agent loop
//! - [`TokenUsage`]: Token consumption statistics
//! - [`ToolResult`]: Result returned from tool execution
//! - [`ToolTier`]: Permission tiers for tools
//! - [`AgentRunState`]: Outcome of running the agent loop (looping mode)
//! - [`TurnOutcome`]: Outcome of running a single turn (single-turn mode)
//! - [`AgentInput`]: Input to start or resume an agent run
//! - [`AgentContinuation`]: Opaque state for resuming after confirmation
//! - [`AgentState`]: Checkpointable agent state

use crate::llm::ContentBlock;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use time::OffsetDateTime;
use uuid::Uuid;

/// Unique identifier for a conversation thread
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ThreadId(pub String);

impl ThreadId {
    #[must_use]
    pub fn new() -> Self {
        Self(Uuid::new_v4().to_string())
    }

    #[must_use]
    pub fn from_string(s: impl Into<String>) -> Self {
        Self(s.into())
    }
}

impl Default for ThreadId {
    fn default() -> Self {
        Self::new()
    }
}

impl std::fmt::Display for ThreadId {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.0)
    }
}

/// Configuration for the agent loop
#[derive(Clone, Debug)]
pub struct AgentConfig {
    /// Maximum number of turns (LLM round-trips) before stopping
    pub max_turns: Option<usize>,
    /// Maximum tokens per response.
    ///
    /// If `None`, the SDK uses the provider/model-specific default.
    pub max_tokens: Option<u32>,
    /// System prompt for the agent
    pub system_prompt: String,
    /// Model identifier
    pub model: String,
    /// Retry configuration for transient errors
    pub retry: RetryConfig,
    /// Enable streaming responses from the LLM.
    ///
    /// When `true`, emits `TextDelta` and `ThinkingDelta` events as text arrives
    /// in real-time. When `false` (default), waits for the complete response
    /// before emitting `Text` and `Thinking` events.
    pub streaming: bool,
}

impl Default for AgentConfig {
    fn default() -> Self {
        Self {
            max_turns: None,
            max_tokens: None,
            system_prompt: String::new(),
            model: String::from("claude-sonnet-4-5-20250929"),
            retry: RetryConfig::default(),
            streaming: false,
        }
    }
}

/// Configuration for retry behavior on transient errors.
#[derive(Clone, Debug)]
pub struct RetryConfig {
    /// Maximum number of retry attempts
    pub max_retries: u32,
    /// Base delay in milliseconds for exponential backoff
    pub base_delay_ms: u64,
    /// Maximum delay cap in milliseconds
    pub max_delay_ms: u64,
}

impl Default for RetryConfig {
    fn default() -> Self {
        Self {
            max_retries: 5,
            base_delay_ms: 1000,
            max_delay_ms: 120_000,
        }
    }
}

impl RetryConfig {
    /// Create a retry config with no retries (for testing)
    #[must_use]
    pub const fn no_retry() -> Self {
        Self {
            max_retries: 0,
            base_delay_ms: 0,
            max_delay_ms: 0,
        }
    }

    /// Create a retry config with fast retries (for testing)
    #[must_use]
    pub const fn fast() -> Self {
        Self {
            max_retries: 5,
            base_delay_ms: 10,
            max_delay_ms: 100,
        }
    }
}

/// Token usage statistics
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct TokenUsage {
    pub input_tokens: u32,
    pub output_tokens: u32,
}

impl TokenUsage {
    pub const fn add(&mut self, other: &Self) {
        self.input_tokens = self.input_tokens.saturating_add(other.input_tokens);
        self.output_tokens = self.output_tokens.saturating_add(other.output_tokens);
    }
}

/// Result of a tool execution
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ToolResult {
    /// Whether the tool execution succeeded
    pub success: bool,
    /// Output content (displayed to user and fed back to LLM)
    pub output: String,
    /// Optional structured data
    pub data: Option<serde_json::Value>,
    /// Optional documents (PDFs, images) to pass back to the LLM as native content blocks.
    /// The agent appends these as `ContentBlock::Document` / `ContentBlock::Image` blocks
    /// in the same user message as the tool result, so the model can read them directly.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub documents: Vec<crate::llm::ContentSource>,
    /// Duration of the tool execution in milliseconds
    pub duration_ms: Option<u64>,
}

impl ToolResult {
    #[must_use]
    pub fn success(output: impl Into<String>) -> Self {
        Self {
            success: true,
            output: output.into(),
            data: None,
            documents: Vec::new(),
            duration_ms: None,
        }
    }

    #[must_use]
    pub fn success_with_data(output: impl Into<String>, data: serde_json::Value) -> Self {
        Self {
            success: true,
            output: output.into(),
            data: Some(data),
            documents: Vec::new(),
            duration_ms: None,
        }
    }

    #[must_use]
    pub fn error(message: impl Into<String>) -> Self {
        Self {
            success: false,
            output: message.into(),
            data: None,
            documents: Vec::new(),
            duration_ms: None,
        }
    }

    #[must_use]
    pub const fn with_duration(mut self, duration_ms: u64) -> Self {
        self.duration_ms = Some(duration_ms);
        self
    }

    /// Attach documents (PDFs, images) to be sent back to the LLM as native content blocks.
    ///
    /// Use this when a tool produces a binary document that the model should read directly,
    /// e.g. a decrypted PDF that Anthropic can parse natively via its document API.
    ///
    /// # Example
    /// ```rust,ignore
    /// use agent_sdk::{ToolResult, ContentSource};
    ///
    /// Ok(ToolResult::success("PDF decrypted.").with_documents(vec![
    ///     ContentSource::new("application/pdf", base64_data),
    /// ]))
    /// ```
    #[must_use]
    pub fn with_documents(mut self, documents: Vec<crate::llm::ContentSource>) -> Self {
        self.documents = documents;
        self
    }
}

/// Permission tier for tools
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum ToolTier {
    /// Read-only, always allowed (e.g., `get_balance`)
    Observe,
    /// Requires confirmation before execution.
    /// The application determines the confirmation type (normal, PIN, biometric).
    Confirm,
}

/// Snapshot of agent state for checkpointing
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AgentState {
    pub thread_id: ThreadId,
    pub turn_count: usize,
    pub total_usage: TokenUsage,
    pub metadata: HashMap<String, serde_json::Value>,
    #[serde(with = "time::serde::rfc3339")]
    pub created_at: OffsetDateTime,
}

impl AgentState {
    #[must_use]
    pub fn new(thread_id: ThreadId) -> Self {
        Self {
            thread_id,
            turn_count: 0,
            total_usage: TokenUsage::default(),
            metadata: HashMap::new(),
            created_at: OffsetDateTime::now_utc(),
        }
    }
}

/// Error from the agent loop.
#[derive(Debug, Clone)]
pub struct AgentError {
    /// Error message
    pub message: String,
    /// Whether the error is potentially recoverable
    pub recoverable: bool,
}

impl AgentError {
    #[must_use]
    pub fn new(message: impl Into<String>, recoverable: bool) -> Self {
        Self {
            message: message.into(),
            recoverable,
        }
    }
}

impl std::fmt::Display for AgentError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.message)
    }
}

impl std::error::Error for AgentError {}

/// Outcome of running the agent loop.
#[derive(Debug)]
pub enum AgentRunState {
    /// Agent completed successfully.
    Done {
        total_turns: u32,
        input_tokens: u64,
        output_tokens: u64,
    },

    /// Agent was refused by the model (safety/policy).
    Refusal {
        total_turns: u32,
        input_tokens: u64,
        output_tokens: u64,
    },

    /// Agent encountered an error.
    Error(AgentError),

    /// Agent is awaiting confirmation for a tool call.
    /// The application should present this to the user and call resume.
    AwaitingConfirmation {
        /// ID of the pending tool call (from LLM)
        tool_call_id: String,
        /// Tool name string (for LLM protocol)
        tool_name: String,
        /// Human-readable display name
        display_name: String,
        /// Tool input parameters
        input: serde_json::Value,
        /// Description of what confirmation is needed
        description: String,
        /// Continuation state for resuming (boxed for enum size efficiency)
        continuation: Box<AgentContinuation>,
    },

    /// Agent run was cancelled via a [`CancellationToken`].
    Cancelled {
        total_turns: u32,
        input_tokens: u64,
        output_tokens: u64,
    },
}

/// Information about a pending tool call that was extracted from the LLM response.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PendingToolCallInfo {
    /// Unique ID for this tool call (from LLM)
    pub id: String,
    /// Tool name string (for LLM protocol)
    pub name: String,
    /// Human-readable display name
    pub display_name: String,
    /// Tool input parameters
    pub input: serde_json::Value,
    /// Optional context for tools that prepare asynchronously and execute later.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub listen_context: Option<ListenExecutionContext>,
}

/// Context captured for listen/execute tools while awaiting confirmation.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ListenExecutionContext {
    /// Opaque operation identifier used to execute/cancel.
    pub operation_id: String,
    /// Revision used for optimistic concurrency checks.
    pub revision: u64,
    /// Snapshot shown to the user during confirmation.
    pub snapshot: serde_json::Value,
    /// Optional expiration timestamp (RFC3339).
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        with = "time::serde::rfc3339::option"
    )]
    pub expires_at: Option<OffsetDateTime>,
}

/// Continuation state that allows resuming the agent loop.
///
/// This contains all the internal state needed to continue execution
/// after receiving a confirmation decision. Pass this back when resuming.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AgentContinuation {
    /// Thread ID (used for validation on resume)
    pub thread_id: ThreadId,
    /// Current turn number
    pub turn: usize,
    /// Total token usage so far
    pub total_usage: TokenUsage,
    /// Token usage for this specific turn (from the LLM call that generated tool calls)
    pub turn_usage: TokenUsage,
    /// All pending tool calls from this turn
    pub pending_tool_calls: Vec<PendingToolCallInfo>,
    /// Index of the tool call awaiting confirmation
    pub awaiting_index: usize,
    /// Tool results already collected (for tools before the awaiting one)
    pub completed_results: Vec<(String, ToolResult)>,
    /// Agent state snapshot
    pub state: AgentState,
}

/// Input to start or resume an agent run.
#[derive(Debug)]
pub enum AgentInput {
    /// Start a new conversation with user text.
    Text(String),

    /// Start a new conversation with rich content (text, images, documents).
    Message(Vec<ContentBlock>),

    /// Resume after a confirmation decision.
    Resume {
        /// The continuation state from `AwaitingConfirmation` (boxed for enum size efficiency).
        continuation: Box<AgentContinuation>,
        /// ID of the tool call being confirmed/rejected.
        tool_call_id: String,
        /// Whether the user confirmed the action.
        confirmed: bool,
        /// Optional reason if rejected.
        rejection_reason: Option<String>,
    },

    /// Continue to the next turn (for single-turn mode).
    ///
    /// Use this after `TurnOutcome::NeedsMoreTurns` to execute the next turn.
    /// The message history already contains tool results from the previous turn.
    Continue,
}

/// Result of tool execution - may indicate async operation in progress.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum ToolOutcome {
    /// Tool completed synchronously with success
    Success(ToolResult),

    /// Tool completed synchronously with failure
    Failed(ToolResult),

    /// Tool started an async operation - must stream status to completion
    InProgress {
        /// Identifier for the operation (to query status)
        operation_id: String,
        /// Initial message for the user
        message: String,
    },
}

impl ToolOutcome {
    #[must_use]
    pub fn success(output: impl Into<String>) -> Self {
        Self::Success(ToolResult::success(output))
    }

    #[must_use]
    pub fn failed(message: impl Into<String>) -> Self {
        Self::Failed(ToolResult::error(message))
    }

    #[must_use]
    pub fn in_progress(operation_id: impl Into<String>, message: impl Into<String>) -> Self {
        Self::InProgress {
            operation_id: operation_id.into(),
            message: message.into(),
        }
    }

    /// Returns true if operation is still in progress
    #[must_use]
    pub const fn is_in_progress(&self) -> bool {
        matches!(self, Self::InProgress { .. })
    }
}

// ============================================================================
// Tool Execution Idempotency Types
// ============================================================================

/// Status of a tool execution for idempotency tracking.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum ExecutionStatus {
    /// Execution started but not yet completed
    InFlight,
    /// Execution completed (success or failure)
    Completed,
}

/// Record of a tool execution for idempotency.
///
/// This struct tracks tool executions to prevent duplicate execution when
/// the agent loop retries after a failure. The write-ahead pattern ensures
/// that execution intent is recorded BEFORE calling the tool, and updated
/// with results AFTER completion.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ToolExecution {
    /// The tool call ID from the LLM (unique per invocation)
    pub tool_call_id: String,
    /// Thread this execution belongs to
    pub thread_id: ThreadId,
    /// Tool name
    pub tool_name: String,
    /// Display name
    pub display_name: String,
    /// Input parameters (for verification)
    pub input: serde_json::Value,
    /// Current status
    pub status: ExecutionStatus,
    /// Result if completed
    pub result: Option<ToolResult>,
    /// For async tools: the operation ID returned by `execute()`
    pub operation_id: Option<String>,
    /// Timestamp when execution started
    #[serde(with = "time::serde::rfc3339")]
    pub started_at: OffsetDateTime,
    /// Timestamp when execution completed
    #[serde(with = "time::serde::rfc3339::option")]
    pub completed_at: Option<OffsetDateTime>,
}

impl ToolExecution {
    /// Create a new in-flight execution record.
    #[must_use]
    pub fn new_in_flight(
        tool_call_id: impl Into<String>,
        thread_id: ThreadId,
        tool_name: impl Into<String>,
        display_name: impl Into<String>,
        input: serde_json::Value,
        started_at: OffsetDateTime,
    ) -> Self {
        Self {
            tool_call_id: tool_call_id.into(),
            thread_id,
            tool_name: tool_name.into(),
            display_name: display_name.into(),
            input,
            status: ExecutionStatus::InFlight,
            result: None,
            operation_id: None,
            started_at,
            completed_at: None,
        }
    }

    /// Mark this execution as completed with a result.
    pub fn complete(&mut self, result: ToolResult) {
        self.status = ExecutionStatus::Completed;
        self.result = Some(result);
        self.completed_at = Some(OffsetDateTime::now_utc());
    }

    /// Set the operation ID for async tool tracking.
    pub fn set_operation_id(&mut self, operation_id: impl Into<String>) {
        self.operation_id = Some(operation_id.into());
    }

    /// Returns true if this execution is still in flight.
    #[must_use]
    pub fn is_in_flight(&self) -> bool {
        self.status == ExecutionStatus::InFlight
    }

    /// Returns true if this execution has completed.
    #[must_use]
    pub fn is_completed(&self) -> bool {
        self.status == ExecutionStatus::Completed
    }
}

/// Outcome of running a single turn.
///
/// This is returned by `run_turn` to indicate what happened and what to do next.
#[derive(Debug)]
pub enum TurnOutcome {
    /// Turn completed successfully, but more turns are needed.
    ///
    /// Tools were executed and their results are stored in the message history.
    /// Call `run_turn` again with `AgentInput::Continue` to proceed.
    NeedsMoreTurns {
        /// The turn number that just completed
        turn: usize,
        /// Token usage for this turn
        turn_usage: TokenUsage,
        /// Cumulative token usage so far
        total_usage: TokenUsage,
    },

    /// Agent completed successfully (no more tool calls).
    Done {
        /// Total turns executed
        total_turns: u32,
        /// Total input tokens consumed
        input_tokens: u64,
        /// Total output tokens consumed
        output_tokens: u64,
    },

    /// A tool requires user confirmation.
    ///
    /// Present this to the user and call `run_turn` with `AgentInput::Resume`
    /// to continue.
    AwaitingConfirmation {
        /// ID of the pending tool call (from LLM)
        tool_call_id: String,
        /// Tool name string (for LLM protocol)
        tool_name: String,
        /// Human-readable display name
        display_name: String,
        /// Tool input parameters
        input: serde_json::Value,
        /// Description of what confirmation is needed
        description: String,
        /// Continuation state for resuming (boxed for enum size efficiency)
        continuation: Box<AgentContinuation>,
    },

    /// Model refused the request (safety/policy).
    Refusal {
        /// Total turns executed
        total_turns: u32,
        /// Total input tokens consumed
        input_tokens: u64,
        /// Total output tokens consumed
        output_tokens: u64,
    },

    /// The turn was cancelled via a [`CancellationToken`].
    Cancelled {
        /// Total turns executed before cancellation
        total_turns: u32,
        /// Total input tokens consumed
        input_tokens: u64,
        /// Total output tokens consumed
        output_tokens: u64,
    },

    /// An error occurred.
    Error(AgentError),
}