Coverage for agentos/models/error.py: 60%
96 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-05 20:52 +0800
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-05 20:52 +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 Enum
14from typing import Any, Dict, List, Optional
16from fastapi import HTTPException
18logger = logging.getLogger(__name__)
21class ErrorCode(str, Enum):
22 """Machine-readable error codes for API responses."""
24 # General
25 INTERNAL_ERROR = "INTERNAL_ERROR"
26 SERVICE_UNAVAILABLE = "SERVICE_UNAVAILABLE"
27 NOT_IMPLEMENTED = "NOT_IMPLEMENTED"
29 # Validation
30 VALIDATION_ERROR = "VALIDATION_ERROR"
31 INVALID_INPUT = "INVALID_INPUT"
32 MISSING_REQUIRED = "MISSING_REQUIRED"
33 TYPE_MISMATCH = "TYPE_MISMATCH"
34 VALUE_OUT_OF_RANGE = "VALUE_OUT_OF_RANGE"
36 # Auth
37 UNAUTHENTICATED = "UNAUTHENTICATED"
38 UNAUTHORIZED = "UNAUTHORIZED"
39 TOKEN_EXPIRED = "TOKEN_EXPIRED"
40 TOKEN_INVALID = "TOKEN_INVALID"
41 INSUFFICIENT_SCOPE = "INSUFFICIENT_SCOPE"
43 # Resource
44 NOT_FOUND = "NOT_FOUND"
45 CONFLICT = "CONFLICT"
46 ALREADY_EXISTS = "ALREADY_EXISTS"
47 GONE = "GONE"
49 # Rate limiting
50 RATE_LIMITED = "RATE_LIMITED"
51 QUOTA_EXCEEDED = "QUOTA_EXCEEDED"
53 # Agent-specific
54 AGENT_NOT_FOUND = "AGENT_NOT_FOUND"
55 AGENT_RUN_FAILED = "AGENT_RUN_FAILED"
56 AGENT_TIMEOUT = "AGENT_TIMEOUT"
57 TOOL_NOT_FOUND = "TOOL_NOT_FOUND"
58 TOOL_EXECUTION_FAILED = "TOOL_EXECUTION_FAILED"
60 # Model / LLM
61 MODEL_NOT_AVAILABLE = "MODEL_NOT_AVAILABLE"
62 MODEL_TIMEOUT = "MODEL_TIMEOUT"
63 CONTEXT_LENGTH_EXCEEDED = "CONTEXT_LENGTH_EXCEEDED"
64 CONTENT_FILTER = "CONTENT_FILTER"
67# ============================================================================
68# Base error
69# ============================================================================
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: Optional[str] = None,
84 headers: Optional[Dict[str, str]] = 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# ============================================================================
130class ValidationError(AgentOSError):
131 """422 — Input validation failed.
133 Supports accumulating multiple field errors via add_error().
134 """
136 def __init__(
137 self,
138 detail: str = "Validation failed",
139 field: Optional[str] = None,
140 errors: Optional[List[Dict[str, str]]] = None,
141 ):
142 super().__init__(
143 status_code=422,
144 code=ErrorCode.VALIDATION_ERROR,
145 detail=detail,
146 field=field,
147 )
148 self.errors: List[Dict[str, str]] = errors or []
149 if field and detail:
150 self.errors.append({"field": field, "message": detail})
152 def add_error(self, field: str, message: str) -> None:
153 """Accumulate an additional field error."""
154 self.errors.append({"field": field, "message": message})
155 if not self.detail:
156 self.detail = f"Validation error on '{field}': {message}"
158 @property
159 def has_errors(self) -> bool:
160 return len(self.errors) > 0
162 def to_dict(self) -> dict:
163 return {
164 **self.to_api_error(),
165 "errors": self.errors,
166 }
169class NotFoundError(AgentOSError):
170 """404 — Resource not found."""
172 def __init__(self, resource_type: str = "Resource", identifier: str = ""):
173 detail = f"{resource_type} not found"
174 if identifier:
175 detail = f"{resource_type} '{identifier}' not found"
176 super().__init__(
177 status_code=404,
178 code=ErrorCode.NOT_FOUND,
179 detail=detail,
180 )
183class AuthenticationError(AgentOSError):
184 """401 — Missing or invalid credentials."""
186 def __init__(self, detail: str = "Authentication required"):
187 super().__init__(
188 status_code=401,
189 code=ErrorCode.UNAUTHENTICATED,
190 detail=detail,
191 )
194class AuthorizationError(AgentOSError):
195 """403 — Insufficient permissions."""
197 def __init__(self, detail: str = "Insufficient permissions"):
198 super().__init__(
199 status_code=403,
200 code=ErrorCode.UNAUTHORIZED,
201 detail=detail,
202 )
205class RateLimitError(AgentOSError):
206 """429 — Too many requests.
208 Includes retry-after header when available.
209 """
211 def __init__(
212 self,
213 detail: str = "Rate limit exceeded",
214 retry_after: Optional[int] = None,
215 ):
216 headers = {}
217 if retry_after is not None:
218 headers["Retry-After"] = str(retry_after)
219 super().__init__(
220 status_code=429,
221 code=ErrorCode.RATE_LIMITED,
222 detail=detail,
223 headers=headers,
224 )
225 self.retry_after = retry_after
228class InternalError(AgentOSError):
229 """500 — Unexpected internal error. User-safe, no stack traces exposed."""
231 def __init__(self, detail: str = "An unexpected error occurred"):
232 super().__init__(
233 status_code=500,
234 code=ErrorCode.INTERNAL_ERROR,
235 detail=detail,
236 )
239class ServiceUnavailableError(AgentOSError):
240 """503 — Service temporarily unavailable (e.g., during maintenance)."""
242 def __init__(
243 self,
244 detail: str = "Service temporarily unavailable",
245 retry_after: Optional[int] = None,
246 ):
247 headers = {}
248 if retry_after is not None:
249 headers["Retry-After"] = str(retry_after)
250 super().__init__(
251 status_code=503,
252 code=ErrorCode.SERVICE_UNAVAILABLE,
253 detail=detail,
254 headers=headers,
255 )