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}