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}