Skip to main content

clark_agent/
error.rs

1//! Typed error enums.
2//!
3//! `LoopError` is fatal-only: stream transport unrecoverable failure or
4//! caller cancellation. Recoverable tool errors are not loop errors —
5//! they're context events: the tool returns `ToolResult` with the error encoded as text,
6//! the loop appends it to history, and the model decides what to do.
7//! Only explicit tool aborts and fatal tool errors bubble out.
8
9use thiserror::Error;
10
11/// Why the loop terminated abnormally.
12///
13/// A successful run returns `Ok(messages)` with no error. The loop's
14/// natural stop condition (no more tool calls + no follow-up) does not
15/// produce an error.
16#[derive(Debug, Error)]
17pub enum LoopError {
18    /// Stream transport raised an unrecoverable error. The provider
19    /// implementation decides what's recoverable; everything that bubbles
20    /// up through `StreamFn::stream` ends the run.
21    #[error("stream transport error: {0}")]
22    Stream(#[from] StreamError),
23
24    /// Caller cancelled via the abort signal.
25    #[error("aborted")]
26    Aborted,
27
28    /// A tool encountered an unrecoverable failure and requested that
29    /// the loop stop immediately rather than append a recoverable
30    /// context event.
31    #[error("fatal tool `{tool}` error: {reason}")]
32    ToolFatal { tool: String, reason: String },
33
34    /// Cannot continue without a starting message: `run_continue` was
35    /// called on an empty context, or the trailing message is `assistant`
36    /// (which the model would not respond to).
37    #[error("cannot continue: {0}")]
38    InvalidContinuation(String),
39}
40
41#[derive(Debug, Error)]
42pub enum StreamError {
43    /// Transient failure: rate limit, network blip, retryable provider
44    /// error. The loop retries these until caller cancellation.
45    #[error("transient stream error: {0}")]
46    Transient(String),
47
48    /// The selected model/provider is temporarily rate-limited. The
49    /// loop retries until the provider recovers or the caller cancels.
50    #[error("provider rate-limited request: {0}")]
51    ProviderRateLimited(String),
52
53    /// Transport failed before the provider produced an actionable
54    /// assistant turn. The request can be replayed as a clean provider
55    /// attempt because there is no runnable assistant turn to preserve.
56    #[error("zero-output transport error: {0}")]
57    ZeroOutputTransport(String),
58
59    /// Permanent failure: invalid request, auth, unsupported model.
60    #[error("fatal stream error: {0}")]
61    Fatal(String),
62
63    /// Provider request history violated the tool-call/result adjacency
64    /// contract after transport-specific projection. This is deterministic
65    /// for the assembled request and must not be retried as a transient or
66    /// collapsed into an empty model outcome.
67    #[error("inconsistent tool history: {0}")]
68    InconsistentToolHistory(String),
69
70    /// Provider returned an empty response after streaming completed.
71    /// The model produced nothing.
72    #[error("empty stream response")]
73    Empty,
74
75    /// Provider rejected the request because the input context exceeds
76    /// the model's window. Distinct from `Fatal` so the loop can apply
77    /// recovery (compact + retry) instead of terminating. Today the
78    /// run still ends — the recovery path lands with the Phase 2
79    /// `OverflowRecovery` plugin chain.
80    #[error("context overflow: {0}")]
81    ContextOverflow(String),
82}
83
84#[derive(Debug, Error)]
85pub enum ToolError {
86    /// Tool execution failed but the agent should keep running. Maps to
87    /// a tool result with the error text and `is_error = true`.
88    #[error("tool execution failed: {0}")]
89    Execution(String),
90
91    /// Tool was cancelled mid-run via the abort signal.
92    #[error("tool aborted")]
93    Aborted,
94
95    /// Tool encountered a fatal error that should end the run. Use
96    /// sparingly — most failures should be `Execution`.
97    #[error("fatal tool error: {0}")]
98    Fatal(String),
99}
100
101#[derive(Debug, Error)]
102pub enum ToolValidationError {
103    /// JSON Schema validation failed for the named field.
104    #[error("invalid arguments for `{tool}`: {reason}")]
105    InvalidArguments { tool: String, reason: String },
106
107    /// Required field is missing for the requested action variant.
108    #[error("missing required field `{field}` for `{tool}.{action}`")]
109    MissingField {
110        tool: String,
111        action: String,
112        field: String,
113    },
114
115    /// Some other validation failure not covered above.
116    #[error("{0}")]
117    Other(String),
118}