greentic_aw_runtime/
error.rs1use serde::{Deserialize, Serialize};
8use thiserror::Error;
9
10use crate::config::AgentConfig;
11
12#[derive(Debug, Error)]
13pub enum AgentError {
14 #[error("agent state load failed: {0}")]
15 StateLoad(#[from] StateError),
16
17 #[error("llm provider unavailable")]
18 LlmProviderUnavailable,
19
20 #[error("llm error: {0}")]
21 Llm(#[from] LlmError),
22
23 #[error("config error: {0}")]
24 Config(#[from] ConfigError),
25
26 #[error("tool dispatch error: {0}")]
27 ToolDispatch(String),
28
29 #[error("daily token budget exceeded")]
30 TokenBudgetExceeded,
31
32 #[error("credit budget exceeded")]
33 CreditBudgetExceeded,
34
35 #[error("session lock could not be acquired within wait window")]
36 LockTimeout,
37
38 #[error("loop exceeded max iterations")]
39 MaxIterations,
40
41 #[error("step timed out")]
42 Timeout,
43
44 #[error("internal: {0}")]
45 Internal(String),
46
47 #[error("guardrail denied ({direction}): {message}")]
48 GuardrailDenied {
49 direction: crate::guardrail::GuardrailDirection,
50 code: String,
51 message: String,
52 details: Option<String>,
53 },
54}
55
56impl AgentError {
57 pub fn user_facing_message(&self, config: &AgentConfig) -> String {
61 match self {
62 Self::LlmProviderUnavailable | Self::Llm(_) => config
63 .limits
64 .provider_failure_message
65 .clone()
66 .unwrap_or_else(|| {
67 "I'm having trouble reaching my reasoning system. \
68 Please try again in a moment."
69 .into()
70 }),
71 Self::TokenBudgetExceeded => "Daily usage limit reached. \
72 Please try again tomorrow or contact your administrator."
73 .to_string(),
74 Self::CreditBudgetExceeded => "Your account has no remaining credits. \
75 Please top up your balance or contact your administrator."
76 .to_string(),
77 Self::Timeout => {
78 "I'm taking longer than expected — please try a simpler request.".to_string()
79 }
80 Self::MaxIterations => "I wasn't able to finish reasoning about that. \
81 Could you rephrase or break it into smaller steps?"
82 .to_string(),
83 _ => "Something went wrong. Please try again.".to_string(),
84 }
85 }
86}
87
88#[derive(Debug, Error)]
89pub enum StateError {
90 #[error("redis error: {0}")]
91 Redis(String),
92 #[error("schema version {found} not supported (max supported: {supported})")]
93 SchemaIncompatible { found: u32, supported: u32 },
94 #[error("decode error: {0}")]
95 Decode(String),
96 #[error("lock acquisition timed out after {0:?}")]
97 LockTimeout(std::time::Duration),
98}
99
100#[derive(Debug, Error)]
101pub enum LlmError {
102 #[error("provider returned 5xx after retries")]
103 ServiceUnavailable,
104 #[error("provider returned 4xx: {0}")]
105 BadRequest(String),
106 #[error("transport: {0}")]
107 Transport(String),
108 #[error("decode: {0}")]
109 Decode(String),
110}
111
112#[derive(Debug, Error)]
113pub enum MemoryError {
114 #[error("memory provider unavailable: {0}")]
115 Backend(String),
116 #[error("memory provider not configured")]
117 NotConfigured,
118}
119
120#[derive(Debug, Error)]
121pub enum ConfigError {
122 #[error("agent_id {0} not found for tenant")]
123 AgentNotFound(String),
124 #[error("provider misconfigured: {0}")]
125 Misconfigured(String),
126 #[error("internal: {0}")]
127 Internal(String),
128}
129
130#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
134#[serde(rename_all = "snake_case")]
135pub enum TerminationReason {
136 FinalReply,
137 MaxIterations,
138 Timeout,
139 Error,
140 TokenBudgetExceeded,
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146 use crate::config::{AgentConfig, AgentLimits, LlmProviderRef};
147
148 fn config_with(message: Option<&str>) -> AgentConfig {
149 AgentConfig {
150 agent_id: "a".into(),
151 system_prompt: "".into(),
152 tools: vec![],
153 guardrails: vec![],
154 llm: LlmProviderRef {
155 provider: "openai".into(),
156 model: "gpt-4".into(),
157 credential_ref: None,
158 },
159 limits: AgentLimits {
160 provider_failure_message: message.map(str::to_string),
161 ..AgentLimits::default()
162 },
163 memory: None,
164 knowledge: None,
165 }
166 }
167
168 #[test]
169 fn user_facing_message_defaults_for_provider_unavailable() {
170 let cfg = config_with(None);
171 let msg = AgentError::LlmProviderUnavailable.user_facing_message(&cfg);
172 assert!(msg.contains("reasoning system"));
173 assert!(msg.contains("try again"));
174 }
175
176 #[test]
177 fn user_facing_message_uses_tenant_override_when_set() {
178 let cfg = config_with(Some("Please retry in 5 minutes."));
179 let msg = AgentError::LlmProviderUnavailable.user_facing_message(&cfg);
180 assert_eq!(msg, "Please retry in 5 minutes.");
181 }
182
183 #[test]
184 fn user_facing_message_never_leaks_internal_detail() {
185 let cfg = config_with(None);
186 let leaky = AgentError::Internal("DATABASE_HOST=192.168.1.5".into());
187 let msg = leaky.user_facing_message(&cfg);
188 assert!(!msg.contains("DATABASE_HOST"));
189 assert!(!msg.contains("192.168"));
190 }
191
192 #[test]
193 fn user_facing_message_budget_distinct_from_default() {
194 let cfg = config_with(None);
195 let budget = AgentError::TokenBudgetExceeded.user_facing_message(&cfg);
196 assert!(budget.contains("limit"));
197 assert_ne!(
198 budget,
199 AgentError::Internal("x".into()).user_facing_message(&cfg)
200 );
201 }
202}