Coverage for agentos/models/error.py: 60%
95 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-08 01:44 +0800
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-08 01:44 +0800
1"""AgentOS Error Models — typed HTTP exceptions for the API layer.
3Provides a hierarchy of FastAPI-compatible HTTP exceptions with:
4- Machine-readable error codes
5- RFC 9457 problem detail fields
6- Structured validation error accumulation
7- Built-in logging integration
8"""
10from __future__ import annotations
12import logging
13from enum import StrEnum
15from fastapi import HTTPException
17logger = logging.getLogger(__name__)
20class ErrorCode(StrEnum):
21 """Machine-readable error codes for API responses."""
23 # General
24 INTERNAL_ERROR = "INTERNAL_ERROR"
25 SERVICE_UNAVAILABLE = "SERVICE_UNAVAILABLE"
26 NOT_IMPLEMENTED = "NOT_IMPLEMENTED"
28 # Validation
29 VALIDATION_ERROR = "VALIDATION_ERROR"
30 INVALID_INPUT = "INVALID_INPUT"
31 MISSING_REQUIRED = "MISSING_REQUIRED"
32 TYPE_MISMATCH = "TYPE_MISMATCH"
33 VALUE_OUT_OF_RANGE = "VALUE_OUT_OF_RANGE"
35 # Auth
36 UNAUTHENTICATED = "UNAUTHENTICATED"
37 UNAUTHORIZED = "UNAUTHORIZED"
38 TOKEN_EXPIRED = "TOKEN_EXPIRED"
39 TOKEN_INVALID = "TOKEN_INVALID"
40 INSUFFICIENT_SCOPE = "INSUFFICIENT_SCOPE"
42 # Resource
43 NOT_FOUND = "NOT_FOUND"
44 CONFLICT = "CONFLICT"
45 ALREADY_EXISTS = "ALREADY_EXISTS"
46 GONE = "GONE"
48 # Rate limiting
49 RATE_LIMITED = "RATE_LIMITED"
50 QUOTA_EXCEEDED = "QUOTA_EXCEEDED"
52 # Agent-specific
53 AGENT_NOT_FOUND = "AGENT_NOT_FOUND"
54 AGENT_RUN_FAILED = "AGENT_RUN_FAILED"
55 AGENT_TIMEOUT = "AGENT_TIMEOUT"
56 TOOL_NOT_FOUND = "TOOL_NOT_FOUND"
57 TOOL_EXECUTION_FAILED = "TOOL_EXECUTION_FAILED"
59 # Model / LLM
60 MODEL_NOT_AVAILABLE = "MODEL_NOT_AVAILABLE"
61 MODEL_TIMEOUT = "MODEL_TIMEOUT"
62 CONTEXT_LENGTH_EXCEEDED = "CONTEXT_LENGTH_EXCEEDED"
63 CONTENT_FILTER = "CONTENT_FILTER"
66# ============================================================================
67# Base error
68# ============================================================================
71class AgentOSError(HTTPException):
72 """Base exception for all AgentOS API errors.
74 Extends FastAPI's HTTPException with structured error codes
75 and optional field-level details.
76 """
78 def __init__(
79 self,
80 status_code: int,
81 code: ErrorCode,
82 detail: str = "",
83 field: str | None = None,
84 headers: dict[str, str] | None = None,
85 ):
86 super().__init__(status_code=status_code, detail=detail, headers=headers)
87 self.code = code
88 self.field = field
89 self._log()
91 def _log(self):
92 """Log the error at appropriate level."""
93 log_msg = f"[{self.code.value}] {self.detail}"
94 if self.field:
95 log_msg += f" (field: {self.field})"
96 if self.status_code >= 500:
97 logger.error(log_msg)
98 else:
99 logger.warning(log_msg)
101 def to_api_error(self) -> dict:
102 """Convert to APIErrorDetail-compatible dict."""
103 return {
104 "type": f"https://errors.agentos.dev/{self.code.value.lower()}",
105 "title": self._title(),
106 "status": self.status_code,
107 "detail": self.detail,
108 "code": self.code.value,
109 "field": self.field,
110 }
112 def _title(self) -> str:
113 titles = {
114 400: "Bad Request",
115 401: "Unauthorized",
116 403: "Forbidden",
117 404: "Not Found",
118 409: "Conflict",
119 429: "Too Many Requests",
120 500: "Internal Server Error",
121 503: "Service Unavailable",
122 }
123 return titles.get(self.status_code, "Error")
126# ============================================================================
127# Concrete error types
128# ============================================================================
131class ValidationError(AgentOSError):
132 """422 — Input validation failed.
134 Supports accumulating multiple field errors via add_error().
135 """
137 def __init__(
138 self,
139 detail: str = "Validation failed",
140 field: str | None = None,
141 errors: list[dict[str, str]] | None = None,
142 ):
143 super().__init__(
144 status_code=422,
145 code=ErrorCode.VALIDATION_ERROR,
146 detail=detail,
147 field=field,
148 )
149 self.errors: list[dict[str, str]] = errors or []
150 if field and detail:
151 self.errors.append({"field": field, "message": detail})
153 def add_error(self, field: str, message: str) -> None:
154 """Accumulate an additional field error."""
155 self.errors.append({"field": field, "message": message})
156 if not self.detail:
157 self.detail = f"Validation error on '{field}': {message}"
159 @property
160 def has_errors(self) -> bool:
161 return len(self.errors) > 0
163 def to_dict(self) -> dict:
164 return {
165 **self.to_api_error(),
166 "errors": self.errors,
167 }
170class NotFoundError(AgentOSError):
171 """404 — Resource not found."""
173 def __init__(self, resource_type: str = "Resource", identifier: str = ""):
174 detail = f"{resource_type} not found"
175 if identifier:
176 detail = f"{resource_type} '{identifier}' not found"
177 super().__init__(
178 status_code=404,
179 code=ErrorCode.NOT_FOUND,
180 detail=detail,
181 )
184class AuthenticationError(AgentOSError):
185 """401 — Missing or invalid credentials."""
187 def __init__(self, detail: str = "Authentication required"):
188 super().__init__(
189 status_code=401,
190 code=ErrorCode.UNAUTHENTICATED,
191 detail=detail,
192 )
195class AuthorizationError(AgentOSError):
196 """403 — Insufficient permissions."""
198 def __init__(self, detail: str = "Insufficient permissions"):
199 super().__init__(
200 status_code=403,
201 code=ErrorCode.UNAUTHORIZED,
202 detail=detail,
203 )
206class RateLimitError(AgentOSError):
207 """429 — Too many requests.
209 Includes retry-after header when available.
210 """
212 def __init__(
213 self,
214 detail: str = "Rate limit exceeded",
215 retry_after: int | None = None,
216 ):
217 headers = {}
218 if retry_after is not None:
219 headers["Retry-After"] = str(retry_after)
220 super().__init__(
221 status_code=429,
222 code=ErrorCode.RATE_LIMITED,
223 detail=detail,
224 headers=headers,
225 )
226 self.retry_after = retry_after
229class InternalError(AgentOSError):
230 """500 — Unexpected internal error. User-safe, no stack traces exposed."""
232 def __init__(self, detail: str = "An unexpected error occurred"):
233 super().__init__(
234 status_code=500,
235 code=ErrorCode.INTERNAL_ERROR,
236 detail=detail,
237 )
240class ServiceUnavailableError(AgentOSError):
241 """503 — Service temporarily unavailable (e.g., during maintenance)."""
243 def __init__(
244 self,
245 detail: str = "Service temporarily unavailable",
246 retry_after: int | None = None,
247 ):
248 headers = {}
249 if retry_after is not None:
250 headers["Retry-After"] = str(retry_after)
251 super().__init__(
252 status_code=503,
253 code=ErrorCode.SERVICE_UNAVAILABLE,
254 detail=detail,
255 headers=headers,
256 )