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::ThinkingConfig;
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: usize,
55 /// Maximum tokens per response
56 pub max_tokens: u32,
57 /// System prompt for the agent
58 pub system_prompt: String,
59 /// Model identifier
60 pub model: String,
61 /// Retry configuration for transient errors
62 pub retry: RetryConfig,
63 /// Optional extended thinking configuration
64 pub thinking: Option<ThinkingConfig>,
65}
66
67impl Default for AgentConfig {
68 fn default() -> Self {
69 Self {
70 max_turns: 10,
71 max_tokens: 4096,
72 system_prompt: String::new(),
73 model: String::from("claude-sonnet-4-20250514"),
74 retry: RetryConfig::default(),
75 thinking: None,
76 }
77 }
78}
79
80/// Configuration for retry behavior on transient errors.
81#[derive(Clone, Debug)]
82pub struct RetryConfig {
83 /// Maximum number of retry attempts
84 pub max_retries: u32,
85 /// Base delay in milliseconds for exponential backoff
86 pub base_delay_ms: u64,
87 /// Maximum delay cap in milliseconds
88 pub max_delay_ms: u64,
89}
90
91impl Default for RetryConfig {
92 fn default() -> Self {
93 Self {
94 max_retries: 5,
95 base_delay_ms: 1000,
96 max_delay_ms: 120_000,
97 }
98 }
99}
100
101impl RetryConfig {
102 /// Create a retry config with no retries (for testing)
103 #[must_use]
104 pub const fn no_retry() -> Self {
105 Self {
106 max_retries: 0,
107 base_delay_ms: 0,
108 max_delay_ms: 0,
109 }
110 }
111
112 /// Create a retry config with fast retries (for testing)
113 #[must_use]
114 pub const fn fast() -> Self {
115 Self {
116 max_retries: 5,
117 base_delay_ms: 10,
118 max_delay_ms: 100,
119 }
120 }
121}
122
123/// Token usage statistics
124#[derive(Clone, Debug, Default, Serialize, Deserialize)]
125pub struct TokenUsage {
126 pub input_tokens: u32,
127 pub output_tokens: u32,
128}
129
130impl TokenUsage {
131 pub const fn add(&mut self, other: &Self) {
132 self.input_tokens += other.input_tokens;
133 self.output_tokens += other.output_tokens;
134 }
135}
136
137/// Result of a tool execution
138#[derive(Clone, Debug, Serialize, Deserialize)]
139pub struct ToolResult {
140 /// Whether the tool execution succeeded
141 pub success: bool,
142 /// Output content (displayed to user and fed back to LLM)
143 pub output: String,
144 /// Optional structured data
145 pub data: Option<serde_json::Value>,
146 /// Duration of the tool execution in milliseconds
147 pub duration_ms: Option<u64>,
148}
149
150impl ToolResult {
151 #[must_use]
152 pub fn success(output: impl Into<String>) -> Self {
153 Self {
154 success: true,
155 output: output.into(),
156 data: None,
157 duration_ms: None,
158 }
159 }
160
161 #[must_use]
162 pub fn success_with_data(output: impl Into<String>, data: serde_json::Value) -> Self {
163 Self {
164 success: true,
165 output: output.into(),
166 data: Some(data),
167 duration_ms: None,
168 }
169 }
170
171 #[must_use]
172 pub fn error(message: impl Into<String>) -> Self {
173 Self {
174 success: false,
175 output: message.into(),
176 data: None,
177 duration_ms: None,
178 }
179 }
180
181 #[must_use]
182 pub const fn with_duration(mut self, duration_ms: u64) -> Self {
183 self.duration_ms = Some(duration_ms);
184 self
185 }
186}
187
188/// Permission tier for tools
189#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
190pub enum ToolTier {
191 /// Read-only, always allowed (e.g., `get_balance`)
192 Observe,
193 /// Requires confirmation before execution.
194 /// The application determines the confirmation type (normal, PIN, biometric).
195 Confirm,
196}
197
198/// Snapshot of agent state for checkpointing
199#[derive(Clone, Debug, Serialize, Deserialize)]
200pub struct AgentState {
201 pub thread_id: ThreadId,
202 pub turn_count: usize,
203 pub total_usage: TokenUsage,
204 pub metadata: HashMap<String, serde_json::Value>,
205 #[serde(with = "time::serde::rfc3339")]
206 pub created_at: OffsetDateTime,
207}
208
209impl AgentState {
210 #[must_use]
211 pub fn new(thread_id: ThreadId) -> Self {
212 Self {
213 thread_id,
214 turn_count: 0,
215 total_usage: TokenUsage::default(),
216 metadata: HashMap::new(),
217 created_at: OffsetDateTime::now_utc(),
218 }
219 }
220}
221
222/// Error from the agent loop.
223#[derive(Debug, Clone)]
224pub struct AgentError {
225 /// Error message
226 pub message: String,
227 /// Whether the error is potentially recoverable
228 pub recoverable: bool,
229}
230
231impl AgentError {
232 #[must_use]
233 pub fn new(message: impl Into<String>, recoverable: bool) -> Self {
234 Self {
235 message: message.into(),
236 recoverable,
237 }
238 }
239}
240
241impl std::fmt::Display for AgentError {
242 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
243 write!(f, "{}", self.message)
244 }
245}
246
247impl std::error::Error for AgentError {}
248
249/// Outcome of running the agent loop.
250#[derive(Debug)]
251pub enum AgentRunState {
252 /// Agent completed successfully.
253 Done {
254 total_turns: u32,
255 input_tokens: u64,
256 output_tokens: u64,
257 },
258
259 /// Agent encountered an error.
260 Error(AgentError),
261
262 /// Agent is awaiting confirmation for a tool call.
263 /// The application should present this to the user and call resume.
264 AwaitingConfirmation {
265 /// ID of the pending tool call (from LLM)
266 tool_call_id: String,
267 /// Tool name string (for LLM protocol)
268 tool_name: String,
269 /// Human-readable display name
270 display_name: String,
271 /// Tool input parameters
272 input: serde_json::Value,
273 /// Description of what confirmation is needed
274 description: String,
275 /// Continuation state for resuming (boxed for enum size efficiency)
276 continuation: Box<AgentContinuation>,
277 },
278}
279
280/// Information about a pending tool call that was extracted from the LLM response.
281#[derive(Clone, Debug, Serialize, Deserialize)]
282pub struct PendingToolCallInfo {
283 /// Unique ID for this tool call (from LLM)
284 pub id: String,
285 /// Tool name string (for LLM protocol)
286 pub name: String,
287 /// Human-readable display name
288 pub display_name: String,
289 /// Tool input parameters
290 pub input: serde_json::Value,
291}
292
293/// Continuation state that allows resuming the agent loop.
294///
295/// This contains all the internal state needed to continue execution
296/// after receiving a confirmation decision. Pass this back when resuming.
297#[derive(Clone, Debug, Serialize, Deserialize)]
298pub struct AgentContinuation {
299 /// Thread ID (used for validation on resume)
300 pub thread_id: ThreadId,
301 /// Current turn number
302 pub turn: usize,
303 /// Total token usage so far
304 pub total_usage: TokenUsage,
305 /// Token usage for this specific turn (from the LLM call that generated tool calls)
306 pub turn_usage: TokenUsage,
307 /// All pending tool calls from this turn
308 pub pending_tool_calls: Vec<PendingToolCallInfo>,
309 /// Index of the tool call awaiting confirmation
310 pub awaiting_index: usize,
311 /// Tool results already collected (for tools before the awaiting one)
312 pub completed_results: Vec<(String, ToolResult)>,
313 /// Agent state snapshot
314 pub state: AgentState,
315}
316
317/// Input to start or resume an agent run.
318#[derive(Debug)]
319pub enum AgentInput {
320 /// Start a new conversation with user text.
321 Text(String),
322
323 /// Resume after a confirmation decision.
324 Resume {
325 /// The continuation state from `AwaitingConfirmation` (boxed for enum size efficiency).
326 continuation: Box<AgentContinuation>,
327 /// ID of the tool call being confirmed/rejected.
328 tool_call_id: String,
329 /// Whether the user confirmed the action.
330 confirmed: bool,
331 /// Optional reason if rejected.
332 rejection_reason: Option<String>,
333 },
334
335 /// Continue to the next turn (for single-turn mode).
336 ///
337 /// Use this after `TurnOutcome::NeedsMoreTurns` to execute the next turn.
338 /// The message history already contains tool results from the previous turn.
339 Continue,
340}
341
342/// Result of tool execution - may indicate async operation in progress.
343#[derive(Clone, Debug, Serialize, Deserialize)]
344pub enum ToolOutcome {
345 /// Tool completed synchronously with success
346 Success(ToolResult),
347
348 /// Tool completed synchronously with failure
349 Failed(ToolResult),
350
351 /// Tool started an async operation - must stream status to completion
352 InProgress {
353 /// Identifier for the operation (to query status)
354 operation_id: String,
355 /// Initial message for the user
356 message: String,
357 },
358}
359
360impl ToolOutcome {
361 #[must_use]
362 pub fn success(output: impl Into<String>) -> Self {
363 Self::Success(ToolResult::success(output))
364 }
365
366 #[must_use]
367 pub fn failed(message: impl Into<String>) -> Self {
368 Self::Failed(ToolResult::error(message))
369 }
370
371 #[must_use]
372 pub fn in_progress(operation_id: impl Into<String>, message: impl Into<String>) -> Self {
373 Self::InProgress {
374 operation_id: operation_id.into(),
375 message: message.into(),
376 }
377 }
378
379 /// Returns true if operation is still in progress
380 #[must_use]
381 pub const fn is_in_progress(&self) -> bool {
382 matches!(self, Self::InProgress { .. })
383 }
384}
385
386// ============================================================================
387// Tool Execution Idempotency Types
388// ============================================================================
389
390/// Status of a tool execution for idempotency tracking.
391#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
392pub enum ExecutionStatus {
393 /// Execution started but not yet completed
394 InFlight,
395 /// Execution completed (success or failure)
396 Completed,
397}
398
399/// Record of a tool execution for idempotency.
400///
401/// This struct tracks tool executions to prevent duplicate execution when
402/// the agent loop retries after a failure. The write-ahead pattern ensures
403/// that execution intent is recorded BEFORE calling the tool, and updated
404/// with results AFTER completion.
405#[derive(Clone, Debug, Serialize, Deserialize)]
406pub struct ToolExecution {
407 /// The tool call ID from the LLM (unique per invocation)
408 pub tool_call_id: String,
409 /// Thread this execution belongs to
410 pub thread_id: ThreadId,
411 /// Tool name
412 pub tool_name: String,
413 /// Display name
414 pub display_name: String,
415 /// Input parameters (for verification)
416 pub input: serde_json::Value,
417 /// Current status
418 pub status: ExecutionStatus,
419 /// Result if completed
420 pub result: Option<ToolResult>,
421 /// For async tools: the operation ID returned by `execute()`
422 pub operation_id: Option<String>,
423 /// Timestamp when execution started
424 #[serde(with = "time::serde::rfc3339")]
425 pub started_at: OffsetDateTime,
426 /// Timestamp when execution completed
427 #[serde(with = "time::serde::rfc3339::option")]
428 pub completed_at: Option<OffsetDateTime>,
429}
430
431impl ToolExecution {
432 /// Create a new in-flight execution record.
433 #[must_use]
434 pub fn new_in_flight(
435 tool_call_id: impl Into<String>,
436 thread_id: ThreadId,
437 tool_name: impl Into<String>,
438 display_name: impl Into<String>,
439 input: serde_json::Value,
440 started_at: OffsetDateTime,
441 ) -> Self {
442 Self {
443 tool_call_id: tool_call_id.into(),
444 thread_id,
445 tool_name: tool_name.into(),
446 display_name: display_name.into(),
447 input,
448 status: ExecutionStatus::InFlight,
449 result: None,
450 operation_id: None,
451 started_at,
452 completed_at: None,
453 }
454 }
455
456 /// Mark this execution as completed with a result.
457 pub fn complete(&mut self, result: ToolResult) {
458 self.status = ExecutionStatus::Completed;
459 self.result = Some(result);
460 self.completed_at = Some(OffsetDateTime::now_utc());
461 }
462
463 /// Set the operation ID for async tool tracking.
464 pub fn set_operation_id(&mut self, operation_id: impl Into<String>) {
465 self.operation_id = Some(operation_id.into());
466 }
467
468 /// Returns true if this execution is still in flight.
469 #[must_use]
470 pub fn is_in_flight(&self) -> bool {
471 self.status == ExecutionStatus::InFlight
472 }
473
474 /// Returns true if this execution has completed.
475 #[must_use]
476 pub fn is_completed(&self) -> bool {
477 self.status == ExecutionStatus::Completed
478 }
479}
480
481/// Outcome of running a single turn.
482///
483/// This is returned by `run_turn` to indicate what happened and what to do next.
484#[derive(Debug)]
485pub enum TurnOutcome {
486 /// Turn completed successfully, but more turns are needed.
487 ///
488 /// Tools were executed and their results are stored in the message history.
489 /// Call `run_turn` again with `AgentInput::Continue` to proceed.
490 NeedsMoreTurns {
491 /// The turn number that just completed
492 turn: usize,
493 /// Token usage for this turn
494 turn_usage: TokenUsage,
495 /// Cumulative token usage so far
496 total_usage: TokenUsage,
497 },
498
499 /// Agent completed successfully (no more tool calls).
500 Done {
501 /// Total turns executed
502 total_turns: u32,
503 /// Total input tokens consumed
504 input_tokens: u64,
505 /// Total output tokens consumed
506 output_tokens: u64,
507 },
508
509 /// A tool requires user confirmation.
510 ///
511 /// Present this to the user and call `run_turn` with `AgentInput::Resume`
512 /// to continue.
513 AwaitingConfirmation {
514 /// ID of the pending tool call (from LLM)
515 tool_call_id: String,
516 /// Tool name string (for LLM protocol)
517 tool_name: String,
518 /// Human-readable display name
519 display_name: String,
520 /// Tool input parameters
521 input: serde_json::Value,
522 /// Description of what confirmation is needed
523 description: String,
524 /// Continuation state for resuming (boxed for enum size efficiency)
525 continuation: Box<AgentContinuation>,
526 },
527
528 /// An error occurred.
529 Error(AgentError),
530}