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::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}