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 /// The model repeatedly stopped without any tool call after the
41 /// configured no-tool recovery budget had already been spent.
42 #[error(
43 "empty assistant outcome retry budget exhausted: observed {observed} no-tool assistant stop(s), budget {budget}"
44 )]
45 EmptyOutcomeBudgetExhausted { budget: usize, observed: usize },
46}
47
48#[derive(Debug, Error)]
49pub enum StreamError {
50 /// Transient failure: rate limit, network blip, retryable provider
51 /// error. The transport implementation decides whether to retry
52 /// internally or surface this.
53 #[error("transient stream error: {0}")]
54 Transient(String),
55
56 /// The selected model/provider is temporarily rate-limited. The
57 /// transport exhausted its own retry budget before surfacing this.
58 #[error("provider rate-limited request: {0}")]
59 ProviderRateLimited(String),
60
61 /// Transport failed before the provider produced an actionable
62 /// assistant turn. The request can be replayed as a clean provider
63 /// attempt because there is no runnable assistant turn to preserve.
64 #[error("zero-output transport error: {0}")]
65 ZeroOutputTransport(String),
66
67 /// Permanent failure: invalid request, auth, unsupported model.
68 #[error("fatal stream error: {0}")]
69 Fatal(String),
70
71 /// Provider request history violated the tool-call/result adjacency
72 /// contract after transport-specific projection. This is deterministic
73 /// for the assembled request and must not be retried as a transient or
74 /// collapsed into an empty model outcome.
75 #[error("inconsistent tool history: {0}")]
76 InconsistentToolHistory(String),
77
78 /// Provider returned an empty response after streaming completed.
79 /// The model produced nothing.
80 #[error("empty stream response")]
81 Empty,
82
83 /// Provider rejected the request because the input context exceeds
84 /// the model's window. Distinct from `Fatal` so the loop can apply
85 /// recovery (compact + retry) instead of terminating. Today the
86 /// run still ends — the recovery path lands with the Phase 2
87 /// `OverflowRecovery` plugin chain.
88 #[error("context overflow: {0}")]
89 ContextOverflow(String),
90}
91
92#[derive(Debug, Error)]
93pub enum ToolError {
94 /// Tool execution failed but the agent should keep running. Maps to
95 /// a tool result with the error text and `is_error = true`.
96 #[error("tool execution failed: {0}")]
97 Execution(String),
98
99 /// Tool was cancelled mid-run via the abort signal.
100 #[error("tool aborted")]
101 Aborted,
102
103 /// Tool encountered a fatal error that should end the run. Use
104 /// sparingly — most failures should be `Execution`.
105 #[error("fatal tool error: {0}")]
106 Fatal(String),
107}
108
109#[derive(Debug, Error)]
110pub enum ToolValidationError {
111 /// JSON Schema validation failed for the named field.
112 #[error("invalid arguments for `{tool}`: {reason}")]
113 InvalidArguments { tool: String, reason: String },
114
115 /// Required field is missing for the requested action variant.
116 #[error("missing required field `{field}` for `{tool}.{action}`")]
117 MissingField {
118 tool: String,
119 action: String,
120 field: String,
121 },
122
123 /// Some other validation failure not covered above.
124 #[error("{0}")]
125 Other(String),
126}