codetether 1.2.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- a2a_server/__init__.py +29 -0
- a2a_server/a2a_agent_card.py +365 -0
- a2a_server/a2a_errors.py +1133 -0
- a2a_server/a2a_executor.py +926 -0
- a2a_server/a2a_router.py +1033 -0
- a2a_server/a2a_types.py +344 -0
- a2a_server/agent_card.py +408 -0
- a2a_server/agents_server.py +271 -0
- a2a_server/auth_api.py +349 -0
- a2a_server/billing_api.py +638 -0
- a2a_server/billing_service.py +712 -0
- a2a_server/billing_webhooks.py +501 -0
- a2a_server/config.py +96 -0
- a2a_server/database.py +2165 -0
- a2a_server/email_inbound.py +398 -0
- a2a_server/email_notifications.py +486 -0
- a2a_server/enhanced_agents.py +919 -0
- a2a_server/enhanced_server.py +160 -0
- a2a_server/hosted_worker.py +1049 -0
- a2a_server/integrated_agents_server.py +347 -0
- a2a_server/keycloak_auth.py +750 -0
- a2a_server/livekit_bridge.py +439 -0
- a2a_server/marketing_tools.py +1364 -0
- a2a_server/mcp_client.py +196 -0
- a2a_server/mcp_http_server.py +2256 -0
- a2a_server/mcp_server.py +191 -0
- a2a_server/message_broker.py +725 -0
- a2a_server/mock_mcp.py +273 -0
- a2a_server/models.py +494 -0
- a2a_server/monitor_api.py +5904 -0
- a2a_server/opencode_bridge.py +1594 -0
- a2a_server/redis_task_manager.py +518 -0
- a2a_server/server.py +726 -0
- a2a_server/task_manager.py +668 -0
- a2a_server/task_queue.py +742 -0
- a2a_server/tenant_api.py +333 -0
- a2a_server/tenant_middleware.py +219 -0
- a2a_server/tenant_service.py +760 -0
- a2a_server/user_auth.py +721 -0
- a2a_server/vault_client.py +576 -0
- a2a_server/worker_sse.py +873 -0
- agent_worker/__init__.py +8 -0
- agent_worker/worker.py +4877 -0
- codetether/__init__.py +10 -0
- codetether/__main__.py +4 -0
- codetether/cli.py +112 -0
- codetether/worker_cli.py +57 -0
- codetether-1.2.2.dist-info/METADATA +570 -0
- codetether-1.2.2.dist-info/RECORD +66 -0
- codetether-1.2.2.dist-info/WHEEL +5 -0
- codetether-1.2.2.dist-info/entry_points.txt +4 -0
- codetether-1.2.2.dist-info/licenses/LICENSE +202 -0
- codetether-1.2.2.dist-info/top_level.txt +5 -0
- codetether_voice_agent/__init__.py +6 -0
- codetether_voice_agent/agent.py +445 -0
- codetether_voice_agent/codetether_mcp.py +345 -0
- codetether_voice_agent/config.py +16 -0
- codetether_voice_agent/functiongemma_caller.py +380 -0
- codetether_voice_agent/session_playback.py +247 -0
- codetether_voice_agent/tools/__init__.py +21 -0
- codetether_voice_agent/tools/definitions.py +135 -0
- codetether_voice_agent/tools/handlers.py +380 -0
- run_server.py +314 -0
- ui/monitor-tailwind.html +1790 -0
- ui/monitor.html +1775 -0
- ui/monitor.js +2662 -0
a2a_server/a2a_errors.py
ADDED
|
@@ -0,0 +1,1133 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A2A-compliant error handling for CodeTether.
|
|
3
|
+
|
|
4
|
+
This module provides error classes and utilities that comply with the A2A protocol
|
|
5
|
+
specification for error handling, including:
|
|
6
|
+
|
|
7
|
+
1. A2A-specific error codes (-32001 to -32009)
|
|
8
|
+
2. Standard JSON-RPC 2.0 error codes (-32700 to -32603)
|
|
9
|
+
3. JSON-RPC error response formatting
|
|
10
|
+
4. RFC 9457 Problem Details format for REST binding
|
|
11
|
+
5. Exception conversion utilities and decorators
|
|
12
|
+
|
|
13
|
+
A2A Error Codes:
|
|
14
|
+
- -32001: TaskNotFoundError (HTTP 404)
|
|
15
|
+
- -32002: TaskNotCancelableError (HTTP 409)
|
|
16
|
+
- -32003: PushNotificationNotSupportedError (HTTP 400)
|
|
17
|
+
- -32004: UnsupportedOperationError (HTTP 400)
|
|
18
|
+
- -32005: ContentTypeNotSupportedError (HTTP 415)
|
|
19
|
+
- -32006: InvalidAgentResponseError (HTTP 502)
|
|
20
|
+
- -32007: ExtendedAgentCardNotConfiguredError (HTTP 400)
|
|
21
|
+
- -32008: ExtensionSupportRequiredError (HTTP 400)
|
|
22
|
+
- -32009: VersionNotSupportedError (HTTP 400)
|
|
23
|
+
|
|
24
|
+
JSON-RPC Error Codes:
|
|
25
|
+
- -32700: ParseError (HTTP 400)
|
|
26
|
+
- -32600: InvalidRequest (HTTP 400)
|
|
27
|
+
- -32601: MethodNotFound (HTTP 404)
|
|
28
|
+
- -32602: InvalidParams (HTTP 400)
|
|
29
|
+
- -32603: InternalError (HTTP 500)
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import functools
|
|
35
|
+
import json
|
|
36
|
+
import logging
|
|
37
|
+
import traceback
|
|
38
|
+
from datetime import datetime, timezone
|
|
39
|
+
from typing import Any, Callable, Dict, List, Optional, Type, TypeVar, Union
|
|
40
|
+
|
|
41
|
+
from fastapi import HTTPException, Request, Response
|
|
42
|
+
from fastapi.responses import JSONResponse
|
|
43
|
+
from pydantic import BaseModel, Field
|
|
44
|
+
|
|
45
|
+
logger = logging.getLogger(__name__)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# =============================================================================
|
|
49
|
+
# Base A2A Error Classes
|
|
50
|
+
# =============================================================================
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class A2AError(Exception):
|
|
54
|
+
"""
|
|
55
|
+
Base exception class for all A2A protocol errors.
|
|
56
|
+
|
|
57
|
+
Subclasses should define:
|
|
58
|
+
- code: JSON-RPC error code (negative integer)
|
|
59
|
+
- http_status: HTTP status code for REST binding
|
|
60
|
+
- message: Default error message
|
|
61
|
+
|
|
62
|
+
Attributes:
|
|
63
|
+
code: JSON-RPC error code
|
|
64
|
+
http_status: HTTP status code
|
|
65
|
+
message: Human-readable error message
|
|
66
|
+
data: Additional error data (optional)
|
|
67
|
+
task_id: Related task ID (optional)
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
code: int = -32000 # Generic server error
|
|
71
|
+
http_status: int = 500
|
|
72
|
+
message: str = 'A2A protocol error'
|
|
73
|
+
|
|
74
|
+
def __init__(
|
|
75
|
+
self,
|
|
76
|
+
message: Optional[str] = None,
|
|
77
|
+
data: Optional[Any] = None,
|
|
78
|
+
task_id: Optional[str] = None,
|
|
79
|
+
):
|
|
80
|
+
self._message = message or self.__class__.message
|
|
81
|
+
self.data = data
|
|
82
|
+
self.task_id = task_id
|
|
83
|
+
super().__init__(self._message)
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def error_message(self) -> str:
|
|
87
|
+
"""Get the error message."""
|
|
88
|
+
return self._message
|
|
89
|
+
|
|
90
|
+
def to_jsonrpc_error(
|
|
91
|
+
self, request_id: Optional[Union[str, int]] = None
|
|
92
|
+
) -> Dict[str, Any]:
|
|
93
|
+
"""
|
|
94
|
+
Convert to JSON-RPC 2.0 error response format.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
request_id: The JSON-RPC request ID (if any)
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
JSON-RPC 2.0 error response dict
|
|
101
|
+
"""
|
|
102
|
+
error: Dict[str, Any] = {
|
|
103
|
+
'code': self.code,
|
|
104
|
+
'message': self._message,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if self.data is not None:
|
|
108
|
+
error['data'] = self.data
|
|
109
|
+
elif self.task_id:
|
|
110
|
+
error['data'] = {'task_id': self.task_id}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
'jsonrpc': '2.0',
|
|
114
|
+
'id': request_id,
|
|
115
|
+
'error': error,
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
def to_problem_details(
|
|
119
|
+
self,
|
|
120
|
+
request: Optional[Request] = None,
|
|
121
|
+
instance: Optional[str] = None,
|
|
122
|
+
) -> Dict[str, Any]:
|
|
123
|
+
"""
|
|
124
|
+
Convert to RFC 9457 Problem Details format for REST binding.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
request: FastAPI request object (for instance URL)
|
|
128
|
+
instance: Override for the instance URI
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
RFC 9457 Problem Details dict
|
|
132
|
+
"""
|
|
133
|
+
problem = {
|
|
134
|
+
'type': f'urn:a2a:error:{self.__class__.__name__}',
|
|
135
|
+
'title': self.__class__.__name__.replace('Error', ' Error'),
|
|
136
|
+
'status': self.http_status,
|
|
137
|
+
'detail': self._message,
|
|
138
|
+
'a2a_code': self.code,
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
# Add instance URI
|
|
142
|
+
if instance:
|
|
143
|
+
problem['instance'] = instance
|
|
144
|
+
elif request:
|
|
145
|
+
problem['instance'] = str(request.url)
|
|
146
|
+
|
|
147
|
+
# Add task_id if present
|
|
148
|
+
if self.task_id:
|
|
149
|
+
problem['task_id'] = self.task_id
|
|
150
|
+
|
|
151
|
+
# Add additional data
|
|
152
|
+
if self.data:
|
|
153
|
+
problem['additional_data'] = self.data
|
|
154
|
+
|
|
155
|
+
# Add timestamp
|
|
156
|
+
problem['timestamp'] = datetime.now(timezone.utc).isoformat()
|
|
157
|
+
|
|
158
|
+
return problem
|
|
159
|
+
|
|
160
|
+
def to_http_response(
|
|
161
|
+
self,
|
|
162
|
+
request: Optional[Request] = None,
|
|
163
|
+
use_problem_details: bool = True,
|
|
164
|
+
) -> JSONResponse:
|
|
165
|
+
"""
|
|
166
|
+
Convert to FastAPI JSONResponse.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
request: FastAPI request object
|
|
170
|
+
use_problem_details: Use RFC 9457 format (default True)
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
FastAPI JSONResponse
|
|
174
|
+
"""
|
|
175
|
+
if use_problem_details:
|
|
176
|
+
content = self.to_problem_details(request)
|
|
177
|
+
media_type = 'application/problem+json'
|
|
178
|
+
else:
|
|
179
|
+
content = {
|
|
180
|
+
'error': {
|
|
181
|
+
'code': self.code,
|
|
182
|
+
'message': self._message,
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if self.data:
|
|
186
|
+
content['error']['data'] = self.data
|
|
187
|
+
media_type = 'application/json'
|
|
188
|
+
|
|
189
|
+
return JSONResponse(
|
|
190
|
+
status_code=self.http_status,
|
|
191
|
+
content=content,
|
|
192
|
+
media_type=media_type,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# =============================================================================
|
|
197
|
+
# A2A-Specific Error Classes (-32001 to -32009)
|
|
198
|
+
# =============================================================================
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class TaskNotFoundError(A2AError):
|
|
202
|
+
"""
|
|
203
|
+
Error code: -32001
|
|
204
|
+
HTTP status: 404
|
|
205
|
+
|
|
206
|
+
Raised when a requested task cannot be found.
|
|
207
|
+
"""
|
|
208
|
+
|
|
209
|
+
code = -32001
|
|
210
|
+
http_status = 404
|
|
211
|
+
message = 'Task not found'
|
|
212
|
+
|
|
213
|
+
def __init__(
|
|
214
|
+
self,
|
|
215
|
+
task_id: str,
|
|
216
|
+
message: Optional[str] = None,
|
|
217
|
+
data: Optional[Any] = None,
|
|
218
|
+
):
|
|
219
|
+
super().__init__(
|
|
220
|
+
message=message or f"Task '{task_id}' not found",
|
|
221
|
+
data=data,
|
|
222
|
+
task_id=task_id,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class TaskNotCancelableError(A2AError):
|
|
227
|
+
"""
|
|
228
|
+
Error code: -32002
|
|
229
|
+
HTTP status: 409
|
|
230
|
+
|
|
231
|
+
Raised when a task cannot be cancelled (e.g., already completed).
|
|
232
|
+
"""
|
|
233
|
+
|
|
234
|
+
code = -32002
|
|
235
|
+
http_status = 409
|
|
236
|
+
message = 'Task cannot be cancelled'
|
|
237
|
+
|
|
238
|
+
def __init__(
|
|
239
|
+
self,
|
|
240
|
+
task_id: str,
|
|
241
|
+
reason: Optional[str] = None,
|
|
242
|
+
message: Optional[str] = None,
|
|
243
|
+
data: Optional[Any] = None,
|
|
244
|
+
):
|
|
245
|
+
reason_text = reason or 'task is in a non-cancelable state'
|
|
246
|
+
super().__init__(
|
|
247
|
+
message=message
|
|
248
|
+
or f"Task '{task_id}' cannot be cancelled: {reason_text}",
|
|
249
|
+
data=data or {'reason': reason_text},
|
|
250
|
+
task_id=task_id,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class PushNotificationNotSupportedError(A2AError):
|
|
255
|
+
"""
|
|
256
|
+
Error code: -32003
|
|
257
|
+
HTTP status: 400
|
|
258
|
+
|
|
259
|
+
Raised when push notifications are requested but not supported.
|
|
260
|
+
"""
|
|
261
|
+
|
|
262
|
+
code = -32003
|
|
263
|
+
http_status = 400
|
|
264
|
+
message = 'Push notifications are not supported'
|
|
265
|
+
|
|
266
|
+
def __init__(
|
|
267
|
+
self,
|
|
268
|
+
message: Optional[str] = None,
|
|
269
|
+
data: Optional[Any] = None,
|
|
270
|
+
):
|
|
271
|
+
super().__init__(
|
|
272
|
+
message=message or 'This agent does not support push notifications',
|
|
273
|
+
data=data,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
class UnsupportedOperationError(A2AError):
|
|
278
|
+
"""
|
|
279
|
+
Error code: -32004
|
|
280
|
+
HTTP status: 400
|
|
281
|
+
|
|
282
|
+
Raised when an operation is not supported by the agent.
|
|
283
|
+
"""
|
|
284
|
+
|
|
285
|
+
code = -32004
|
|
286
|
+
http_status = 400
|
|
287
|
+
message = 'Operation not supported'
|
|
288
|
+
|
|
289
|
+
def __init__(
|
|
290
|
+
self,
|
|
291
|
+
operation: str,
|
|
292
|
+
message: Optional[str] = None,
|
|
293
|
+
data: Optional[Any] = None,
|
|
294
|
+
):
|
|
295
|
+
super().__init__(
|
|
296
|
+
message=message
|
|
297
|
+
or f"Operation '{operation}' is not supported by this agent",
|
|
298
|
+
data=data or {'operation': operation},
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class ContentTypeNotSupportedError(A2AError):
|
|
303
|
+
"""
|
|
304
|
+
Error code: -32005
|
|
305
|
+
HTTP status: 415
|
|
306
|
+
|
|
307
|
+
Raised when the content type is not supported.
|
|
308
|
+
"""
|
|
309
|
+
|
|
310
|
+
code = -32005
|
|
311
|
+
http_status = 415
|
|
312
|
+
message = 'Content type not supported'
|
|
313
|
+
|
|
314
|
+
def __init__(
|
|
315
|
+
self,
|
|
316
|
+
content_type: str,
|
|
317
|
+
supported_types: Optional[List[str]] = None,
|
|
318
|
+
message: Optional[str] = None,
|
|
319
|
+
data: Optional[Any] = None,
|
|
320
|
+
):
|
|
321
|
+
supported = supported_types or ['application/json']
|
|
322
|
+
super().__init__(
|
|
323
|
+
message=message
|
|
324
|
+
or f"Content type '{content_type}' is not supported. Supported: {', '.join(supported)}",
|
|
325
|
+
data=data
|
|
326
|
+
or {
|
|
327
|
+
'content_type': content_type,
|
|
328
|
+
'supported_types': supported,
|
|
329
|
+
},
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
class InvalidAgentResponseError(A2AError):
|
|
334
|
+
"""
|
|
335
|
+
Error code: -32006
|
|
336
|
+
HTTP status: 502
|
|
337
|
+
|
|
338
|
+
Raised when an agent returns an invalid response.
|
|
339
|
+
"""
|
|
340
|
+
|
|
341
|
+
code = -32006
|
|
342
|
+
http_status = 502
|
|
343
|
+
message = 'Invalid agent response'
|
|
344
|
+
|
|
345
|
+
def __init__(
|
|
346
|
+
self,
|
|
347
|
+
agent_name: Optional[str] = None,
|
|
348
|
+
reason: Optional[str] = None,
|
|
349
|
+
message: Optional[str] = None,
|
|
350
|
+
data: Optional[Any] = None,
|
|
351
|
+
):
|
|
352
|
+
detail = reason or 'response did not match expected format'
|
|
353
|
+
agent_info = f" from agent '{agent_name}'" if agent_name else ''
|
|
354
|
+
super().__init__(
|
|
355
|
+
message=message or f'Invalid response{agent_info}: {detail}',
|
|
356
|
+
data=data
|
|
357
|
+
or {
|
|
358
|
+
'agent_name': agent_name,
|
|
359
|
+
'reason': reason,
|
|
360
|
+
},
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
class ExtendedAgentCardNotConfiguredError(A2AError):
|
|
365
|
+
"""
|
|
366
|
+
Error code: -32007
|
|
367
|
+
HTTP status: 400
|
|
368
|
+
|
|
369
|
+
Raised when extended agent card features are required but not configured.
|
|
370
|
+
"""
|
|
371
|
+
|
|
372
|
+
code = -32007
|
|
373
|
+
http_status = 400
|
|
374
|
+
message = 'Extended agent card not configured'
|
|
375
|
+
|
|
376
|
+
def __init__(
|
|
377
|
+
self,
|
|
378
|
+
required_feature: Optional[str] = None,
|
|
379
|
+
message: Optional[str] = None,
|
|
380
|
+
data: Optional[Any] = None,
|
|
381
|
+
):
|
|
382
|
+
feature_info = (
|
|
383
|
+
f' (required: {required_feature})' if required_feature else ''
|
|
384
|
+
)
|
|
385
|
+
super().__init__(
|
|
386
|
+
message=message
|
|
387
|
+
or f'Extended agent card is not configured{feature_info}',
|
|
388
|
+
data=data or {'required_feature': required_feature},
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
class ExtensionSupportRequiredError(A2AError):
|
|
393
|
+
"""
|
|
394
|
+
Error code: -32008
|
|
395
|
+
HTTP status: 400
|
|
396
|
+
|
|
397
|
+
Raised when a required protocol extension is not supported.
|
|
398
|
+
"""
|
|
399
|
+
|
|
400
|
+
code = -32008
|
|
401
|
+
http_status = 400
|
|
402
|
+
message = 'Extension support required'
|
|
403
|
+
|
|
404
|
+
def __init__(
|
|
405
|
+
self,
|
|
406
|
+
extension_uri: str,
|
|
407
|
+
message: Optional[str] = None,
|
|
408
|
+
data: Optional[Any] = None,
|
|
409
|
+
):
|
|
410
|
+
super().__init__(
|
|
411
|
+
message=message
|
|
412
|
+
or f'Protocol extension required but not supported: {extension_uri}',
|
|
413
|
+
data=data or {'extension_uri': extension_uri},
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
class VersionNotSupportedError(A2AError):
|
|
418
|
+
"""
|
|
419
|
+
Error code: -32009
|
|
420
|
+
HTTP status: 400
|
|
421
|
+
|
|
422
|
+
Raised when the requested protocol version is not supported.
|
|
423
|
+
"""
|
|
424
|
+
|
|
425
|
+
code = -32009
|
|
426
|
+
http_status = 400
|
|
427
|
+
message = 'Version not supported'
|
|
428
|
+
|
|
429
|
+
def __init__(
|
|
430
|
+
self,
|
|
431
|
+
requested_version: str,
|
|
432
|
+
supported_versions: Optional[List[str]] = None,
|
|
433
|
+
message: Optional[str] = None,
|
|
434
|
+
data: Optional[Any] = None,
|
|
435
|
+
):
|
|
436
|
+
supported = supported_versions or ['1.0']
|
|
437
|
+
super().__init__(
|
|
438
|
+
message=message
|
|
439
|
+
or f"Protocol version '{requested_version}' is not supported. Supported: {', '.join(supported)}",
|
|
440
|
+
data=data
|
|
441
|
+
or {
|
|
442
|
+
'requested_version': requested_version,
|
|
443
|
+
'supported_versions': supported,
|
|
444
|
+
},
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
# =============================================================================
|
|
449
|
+
# JSON-RPC Standard Error Classes (-32700 to -32603)
|
|
450
|
+
# =============================================================================
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
class ParseError(A2AError):
|
|
454
|
+
"""
|
|
455
|
+
Error code: -32700
|
|
456
|
+
HTTP status: 400
|
|
457
|
+
|
|
458
|
+
Invalid JSON was received by the server.
|
|
459
|
+
"""
|
|
460
|
+
|
|
461
|
+
code = -32700
|
|
462
|
+
http_status = 400
|
|
463
|
+
message = 'Parse error'
|
|
464
|
+
|
|
465
|
+
def __init__(
|
|
466
|
+
self,
|
|
467
|
+
detail: Optional[str] = None,
|
|
468
|
+
message: Optional[str] = None,
|
|
469
|
+
data: Optional[Any] = None,
|
|
470
|
+
):
|
|
471
|
+
super().__init__(
|
|
472
|
+
message=message or f'Invalid JSON: {detail}'
|
|
473
|
+
if detail
|
|
474
|
+
else 'Invalid JSON was received',
|
|
475
|
+
data=data or {'detail': detail},
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
class InvalidRequest(A2AError):
|
|
480
|
+
"""
|
|
481
|
+
Error code: -32600
|
|
482
|
+
HTTP status: 400
|
|
483
|
+
|
|
484
|
+
The JSON sent is not a valid Request object.
|
|
485
|
+
"""
|
|
486
|
+
|
|
487
|
+
code = -32600
|
|
488
|
+
http_status = 400
|
|
489
|
+
message = 'Invalid Request'
|
|
490
|
+
|
|
491
|
+
def __init__(
|
|
492
|
+
self,
|
|
493
|
+
reason: Optional[str] = None,
|
|
494
|
+
message: Optional[str] = None,
|
|
495
|
+
data: Optional[Any] = None,
|
|
496
|
+
):
|
|
497
|
+
super().__init__(
|
|
498
|
+
message=message or f'Invalid request: {reason}'
|
|
499
|
+
if reason
|
|
500
|
+
else 'Invalid request object',
|
|
501
|
+
data=data or {'reason': reason},
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
class MethodNotFound(A2AError):
|
|
506
|
+
"""
|
|
507
|
+
Error code: -32601
|
|
508
|
+
HTTP status: 404
|
|
509
|
+
|
|
510
|
+
The method does not exist or is not available.
|
|
511
|
+
"""
|
|
512
|
+
|
|
513
|
+
code = -32601
|
|
514
|
+
http_status = 404
|
|
515
|
+
message = 'Method not found'
|
|
516
|
+
|
|
517
|
+
def __init__(
|
|
518
|
+
self,
|
|
519
|
+
method: str,
|
|
520
|
+
available_methods: Optional[List[str]] = None,
|
|
521
|
+
message: Optional[str] = None,
|
|
522
|
+
data: Optional[Any] = None,
|
|
523
|
+
):
|
|
524
|
+
super().__init__(
|
|
525
|
+
message=message or f"Method '{method}' not found",
|
|
526
|
+
data=data
|
|
527
|
+
or {
|
|
528
|
+
'method': method,
|
|
529
|
+
'available_methods': available_methods,
|
|
530
|
+
},
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
class InvalidParams(A2AError):
|
|
535
|
+
"""
|
|
536
|
+
Error code: -32602
|
|
537
|
+
HTTP status: 400
|
|
538
|
+
|
|
539
|
+
Invalid method parameters.
|
|
540
|
+
"""
|
|
541
|
+
|
|
542
|
+
code = -32602
|
|
543
|
+
http_status = 400
|
|
544
|
+
message = 'Invalid params'
|
|
545
|
+
|
|
546
|
+
def __init__(
|
|
547
|
+
self,
|
|
548
|
+
param_errors: Optional[List[Dict[str, Any]]] = None,
|
|
549
|
+
message: Optional[str] = None,
|
|
550
|
+
data: Optional[Any] = None,
|
|
551
|
+
):
|
|
552
|
+
if param_errors:
|
|
553
|
+
error_details = '; '.join(
|
|
554
|
+
f'{e.get("param", "unknown")}: {e.get("error", "invalid")}'
|
|
555
|
+
for e in param_errors
|
|
556
|
+
)
|
|
557
|
+
msg = message or f'Invalid parameters: {error_details}'
|
|
558
|
+
else:
|
|
559
|
+
msg = message or 'Invalid method parameters'
|
|
560
|
+
|
|
561
|
+
super().__init__(
|
|
562
|
+
message=msg,
|
|
563
|
+
data=data or {'param_errors': param_errors},
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
class InternalError(A2AError):
|
|
568
|
+
"""
|
|
569
|
+
Error code: -32603
|
|
570
|
+
HTTP status: 500
|
|
571
|
+
|
|
572
|
+
Internal JSON-RPC error.
|
|
573
|
+
"""
|
|
574
|
+
|
|
575
|
+
code = -32603
|
|
576
|
+
http_status = 500
|
|
577
|
+
message = 'Internal error'
|
|
578
|
+
|
|
579
|
+
def __init__(
|
|
580
|
+
self,
|
|
581
|
+
detail: Optional[str] = None,
|
|
582
|
+
message: Optional[str] = None,
|
|
583
|
+
data: Optional[Any] = None,
|
|
584
|
+
include_traceback: bool = False,
|
|
585
|
+
):
|
|
586
|
+
if include_traceback:
|
|
587
|
+
tb = traceback.format_exc()
|
|
588
|
+
data = data or {}
|
|
589
|
+
if isinstance(data, dict):
|
|
590
|
+
data['traceback'] = tb
|
|
591
|
+
|
|
592
|
+
super().__init__(
|
|
593
|
+
message=message or f'Internal error: {detail}'
|
|
594
|
+
if detail
|
|
595
|
+
else 'Internal server error',
|
|
596
|
+
data=data,
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
# =============================================================================
|
|
601
|
+
# Error Registry and Conversion
|
|
602
|
+
# =============================================================================
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
# Map of error codes to error classes
|
|
606
|
+
ERROR_CODE_MAP: Dict[int, Type[A2AError]] = {
|
|
607
|
+
# A2A errors
|
|
608
|
+
-32001: TaskNotFoundError,
|
|
609
|
+
-32002: TaskNotCancelableError,
|
|
610
|
+
-32003: PushNotificationNotSupportedError,
|
|
611
|
+
-32004: UnsupportedOperationError,
|
|
612
|
+
-32005: ContentTypeNotSupportedError,
|
|
613
|
+
-32006: InvalidAgentResponseError,
|
|
614
|
+
-32007: ExtendedAgentCardNotConfiguredError,
|
|
615
|
+
-32008: ExtensionSupportRequiredError,
|
|
616
|
+
-32009: VersionNotSupportedError,
|
|
617
|
+
# JSON-RPC errors
|
|
618
|
+
-32700: ParseError,
|
|
619
|
+
-32600: InvalidRequest,
|
|
620
|
+
-32601: MethodNotFound,
|
|
621
|
+
-32602: InvalidParams,
|
|
622
|
+
-32603: InternalError,
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
# Map of HTTP status codes to default error classes
|
|
627
|
+
HTTP_STATUS_MAP: Dict[int, Type[A2AError]] = {
|
|
628
|
+
400: InvalidRequest,
|
|
629
|
+
404: TaskNotFoundError,
|
|
630
|
+
409: TaskNotCancelableError,
|
|
631
|
+
415: ContentTypeNotSupportedError,
|
|
632
|
+
500: InternalError,
|
|
633
|
+
502: InvalidAgentResponseError,
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def error_from_code(
|
|
638
|
+
code: int,
|
|
639
|
+
message: Optional[str] = None,
|
|
640
|
+
data: Optional[Any] = None,
|
|
641
|
+
) -> A2AError:
|
|
642
|
+
"""
|
|
643
|
+
Create an A2A error from an error code.
|
|
644
|
+
|
|
645
|
+
Args:
|
|
646
|
+
code: JSON-RPC error code
|
|
647
|
+
message: Optional custom message
|
|
648
|
+
data: Optional additional data
|
|
649
|
+
|
|
650
|
+
Returns:
|
|
651
|
+
Appropriate A2AError subclass instance
|
|
652
|
+
"""
|
|
653
|
+
error_class = ERROR_CODE_MAP.get(code, A2AError)
|
|
654
|
+
return error_class(message=message, data=data)
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def error_from_http_status(
|
|
658
|
+
status: int,
|
|
659
|
+
message: Optional[str] = None,
|
|
660
|
+
data: Optional[Any] = None,
|
|
661
|
+
) -> A2AError:
|
|
662
|
+
"""
|
|
663
|
+
Create an A2A error from an HTTP status code.
|
|
664
|
+
|
|
665
|
+
Args:
|
|
666
|
+
status: HTTP status code
|
|
667
|
+
message: Optional custom message
|
|
668
|
+
data: Optional additional data
|
|
669
|
+
|
|
670
|
+
Returns:
|
|
671
|
+
Appropriate A2AError subclass instance
|
|
672
|
+
"""
|
|
673
|
+
error_class = HTTP_STATUS_MAP.get(status, InternalError)
|
|
674
|
+
return error_class(message=message, data=data)
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
# =============================================================================
|
|
678
|
+
# Exception Conversion Utilities
|
|
679
|
+
# =============================================================================
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
def convert_exception(
|
|
683
|
+
exc: Exception, task_id: Optional[str] = None
|
|
684
|
+
) -> A2AError:
|
|
685
|
+
"""
|
|
686
|
+
Convert any exception to an A2A error.
|
|
687
|
+
|
|
688
|
+
This handles conversion from:
|
|
689
|
+
- A2AError (returned as-is)
|
|
690
|
+
- FastAPI HTTPException
|
|
691
|
+
- ValueError/TypeError (InvalidParams)
|
|
692
|
+
- KeyError (TaskNotFoundError)
|
|
693
|
+
- NotImplementedError (UnsupportedOperationError)
|
|
694
|
+
- TaskLimitExceeded (custom error)
|
|
695
|
+
- Any other exception (InternalError)
|
|
696
|
+
|
|
697
|
+
Args:
|
|
698
|
+
exc: The exception to convert
|
|
699
|
+
task_id: Optional task ID to include in error
|
|
700
|
+
|
|
701
|
+
Returns:
|
|
702
|
+
A2AError instance
|
|
703
|
+
"""
|
|
704
|
+
# Already an A2A error
|
|
705
|
+
if isinstance(exc, A2AError):
|
|
706
|
+
if task_id and not exc.task_id:
|
|
707
|
+
exc.task_id = task_id
|
|
708
|
+
return exc
|
|
709
|
+
|
|
710
|
+
# FastAPI HTTPException
|
|
711
|
+
if isinstance(exc, HTTPException):
|
|
712
|
+
return error_from_http_status(
|
|
713
|
+
exc.status_code,
|
|
714
|
+
message=str(exc.detail),
|
|
715
|
+
data={'task_id': task_id} if task_id else None,
|
|
716
|
+
)
|
|
717
|
+
|
|
718
|
+
# ValueError/TypeError -> InvalidParams
|
|
719
|
+
if isinstance(exc, (ValueError, TypeError)):
|
|
720
|
+
return InvalidParams(
|
|
721
|
+
message=str(exc),
|
|
722
|
+
data={'task_id': task_id} if task_id else None,
|
|
723
|
+
)
|
|
724
|
+
|
|
725
|
+
# KeyError -> TaskNotFoundError (common pattern)
|
|
726
|
+
if isinstance(exc, KeyError):
|
|
727
|
+
key = str(exc.args[0]) if exc.args else 'unknown'
|
|
728
|
+
if task_id:
|
|
729
|
+
return TaskNotFoundError(task_id=task_id)
|
|
730
|
+
return TaskNotFoundError(
|
|
731
|
+
task_id=key,
|
|
732
|
+
message=f'Resource not found: {key}',
|
|
733
|
+
)
|
|
734
|
+
|
|
735
|
+
# NotImplementedError -> UnsupportedOperationError
|
|
736
|
+
if isinstance(exc, NotImplementedError):
|
|
737
|
+
return UnsupportedOperationError(
|
|
738
|
+
operation=str(exc) or 'unknown',
|
|
739
|
+
data={'task_id': task_id} if task_id else None,
|
|
740
|
+
)
|
|
741
|
+
|
|
742
|
+
# json.JSONDecodeError -> ParseError
|
|
743
|
+
if isinstance(exc, json.JSONDecodeError):
|
|
744
|
+
return ParseError(
|
|
745
|
+
detail=f'at position {exc.pos}: {exc.msg}',
|
|
746
|
+
data={'task_id': task_id} if task_id else None,
|
|
747
|
+
)
|
|
748
|
+
|
|
749
|
+
# TaskLimitExceeded (from task_queue.py)
|
|
750
|
+
# Import locally to avoid circular imports
|
|
751
|
+
try:
|
|
752
|
+
from a2a_server.task_queue import TaskLimitExceeded
|
|
753
|
+
|
|
754
|
+
if isinstance(exc, TaskLimitExceeded):
|
|
755
|
+
return InvalidRequest(
|
|
756
|
+
reason='task_limit_exceeded',
|
|
757
|
+
message=str(exc),
|
|
758
|
+
data={
|
|
759
|
+
'task_id': task_id,
|
|
760
|
+
'tasks_used': exc.tasks_used,
|
|
761
|
+
'tasks_limit': exc.tasks_limit,
|
|
762
|
+
'running_count': exc.running_count,
|
|
763
|
+
'concurrency_limit': exc.concurrency_limit,
|
|
764
|
+
},
|
|
765
|
+
)
|
|
766
|
+
except ImportError:
|
|
767
|
+
pass
|
|
768
|
+
|
|
769
|
+
# Default to InternalError
|
|
770
|
+
return InternalError(
|
|
771
|
+
detail=str(exc),
|
|
772
|
+
data={
|
|
773
|
+
'task_id': task_id,
|
|
774
|
+
'exception_type': type(exc).__name__,
|
|
775
|
+
},
|
|
776
|
+
)
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
# =============================================================================
|
|
780
|
+
# Decorators and Middleware
|
|
781
|
+
# =============================================================================
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
T = TypeVar('T')
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
def a2a_error_handler(
|
|
788
|
+
request_id_param: Optional[str] = None,
|
|
789
|
+
task_id_param: Optional[str] = None,
|
|
790
|
+
use_jsonrpc: bool = True,
|
|
791
|
+
log_errors: bool = True,
|
|
792
|
+
):
|
|
793
|
+
"""
|
|
794
|
+
Decorator to convert exceptions to A2A error responses.
|
|
795
|
+
|
|
796
|
+
Can be used for both JSON-RPC and REST endpoints.
|
|
797
|
+
|
|
798
|
+
Args:
|
|
799
|
+
request_id_param: Name of the request ID parameter (for JSON-RPC)
|
|
800
|
+
task_id_param: Name of the task ID parameter
|
|
801
|
+
use_jsonrpc: Return JSON-RPC format (True) or Problem Details (False)
|
|
802
|
+
log_errors: Log errors to the logger
|
|
803
|
+
|
|
804
|
+
Example:
|
|
805
|
+
@a2a_error_handler(request_id_param="id", task_id_param="task_id")
|
|
806
|
+
async def handle_get_task(id: str, task_id: str) -> dict:
|
|
807
|
+
...
|
|
808
|
+
"""
|
|
809
|
+
|
|
810
|
+
def decorator(func: Callable[..., T]) -> Callable[..., T]:
|
|
811
|
+
@functools.wraps(func)
|
|
812
|
+
async def wrapper(*args, **kwargs) -> T:
|
|
813
|
+
request_id = (
|
|
814
|
+
kwargs.get(request_id_param) if request_id_param else None
|
|
815
|
+
)
|
|
816
|
+
task_id = kwargs.get(task_id_param) if task_id_param else None
|
|
817
|
+
|
|
818
|
+
try:
|
|
819
|
+
return await func(*args, **kwargs)
|
|
820
|
+
except A2AError as e:
|
|
821
|
+
if task_id and not e.task_id:
|
|
822
|
+
e.task_id = task_id
|
|
823
|
+
|
|
824
|
+
if log_errors:
|
|
825
|
+
logger.warning(
|
|
826
|
+
f'A2A error in {func.__name__}: [{e.code}] {e.error_message}'
|
|
827
|
+
)
|
|
828
|
+
|
|
829
|
+
if use_jsonrpc:
|
|
830
|
+
return e.to_jsonrpc_error(request_id)
|
|
831
|
+
return e.to_problem_details()
|
|
832
|
+
|
|
833
|
+
except Exception as exc:
|
|
834
|
+
a2a_error = convert_exception(exc, task_id)
|
|
835
|
+
|
|
836
|
+
if log_errors:
|
|
837
|
+
logger.error(
|
|
838
|
+
f'Exception in {func.__name__}: {type(exc).__name__}: {exc}',
|
|
839
|
+
exc_info=True,
|
|
840
|
+
)
|
|
841
|
+
|
|
842
|
+
if use_jsonrpc:
|
|
843
|
+
return a2a_error.to_jsonrpc_error(request_id)
|
|
844
|
+
return a2a_error.to_problem_details()
|
|
845
|
+
|
|
846
|
+
return wrapper
|
|
847
|
+
|
|
848
|
+
return decorator
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
async def a2a_exception_middleware(request: Request, call_next):
|
|
852
|
+
"""
|
|
853
|
+
FastAPI middleware to catch exceptions and return A2A error responses.
|
|
854
|
+
|
|
855
|
+
Usage:
|
|
856
|
+
app.middleware("http")(a2a_exception_middleware)
|
|
857
|
+
|
|
858
|
+
This middleware:
|
|
859
|
+
1. Catches any unhandled exceptions
|
|
860
|
+
2. Converts them to A2A errors
|
|
861
|
+
3. Returns appropriate JSON-RPC or Problem Details response
|
|
862
|
+
"""
|
|
863
|
+
try:
|
|
864
|
+
response = await call_next(request)
|
|
865
|
+
return response
|
|
866
|
+
except A2AError as e:
|
|
867
|
+
logger.warning(f'A2A error: [{e.code}] {e.error_message}')
|
|
868
|
+
return e.to_http_response(request)
|
|
869
|
+
except HTTPException as e:
|
|
870
|
+
a2a_error = error_from_http_status(e.status_code, str(e.detail))
|
|
871
|
+
return a2a_error.to_http_response(request)
|
|
872
|
+
except Exception as exc:
|
|
873
|
+
logger.error(
|
|
874
|
+
f'Unhandled exception: {type(exc).__name__}: {exc}', exc_info=True
|
|
875
|
+
)
|
|
876
|
+
a2a_error = convert_exception(exc)
|
|
877
|
+
return a2a_error.to_http_response(request)
|
|
878
|
+
|
|
879
|
+
|
|
880
|
+
def create_exception_handlers() -> Dict[Type[Exception], Callable]:
|
|
881
|
+
"""
|
|
882
|
+
Create FastAPI exception handlers for A2A errors.
|
|
883
|
+
|
|
884
|
+
Usage:
|
|
885
|
+
handlers = create_exception_handlers()
|
|
886
|
+
for exc_class, handler in handlers.items():
|
|
887
|
+
app.add_exception_handler(exc_class, handler)
|
|
888
|
+
"""
|
|
889
|
+
|
|
890
|
+
async def handle_a2a_error(request: Request, exc: A2AError) -> Response:
|
|
891
|
+
logger.warning(f'A2A error: [{exc.code}] {exc.error_message}')
|
|
892
|
+
return exc.to_http_response(request)
|
|
893
|
+
|
|
894
|
+
async def handle_http_exception(
|
|
895
|
+
request: Request, exc: HTTPException
|
|
896
|
+
) -> Response:
|
|
897
|
+
a2a_error = error_from_http_status(exc.status_code, str(exc.detail))
|
|
898
|
+
return a2a_error.to_http_response(request)
|
|
899
|
+
|
|
900
|
+
async def handle_value_error(request: Request, exc: ValueError) -> Response:
|
|
901
|
+
a2a_error = InvalidParams(message=str(exc))
|
|
902
|
+
return a2a_error.to_http_response(request)
|
|
903
|
+
|
|
904
|
+
async def handle_json_error(
|
|
905
|
+
request: Request, exc: json.JSONDecodeError
|
|
906
|
+
) -> Response:
|
|
907
|
+
a2a_error = ParseError(detail=f'at position {exc.pos}: {exc.msg}')
|
|
908
|
+
return a2a_error.to_http_response(request)
|
|
909
|
+
|
|
910
|
+
async def handle_generic_exception(
|
|
911
|
+
request: Request, exc: Exception
|
|
912
|
+
) -> Response:
|
|
913
|
+
logger.error(
|
|
914
|
+
f'Unhandled exception: {type(exc).__name__}: {exc}', exc_info=True
|
|
915
|
+
)
|
|
916
|
+
a2a_error = InternalError(detail=str(exc))
|
|
917
|
+
return a2a_error.to_http_response(request)
|
|
918
|
+
|
|
919
|
+
return {
|
|
920
|
+
A2AError: handle_a2a_error,
|
|
921
|
+
HTTPException: handle_http_exception,
|
|
922
|
+
ValueError: handle_value_error,
|
|
923
|
+
json.JSONDecodeError: handle_json_error,
|
|
924
|
+
Exception: handle_generic_exception,
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
|
|
928
|
+
# =============================================================================
|
|
929
|
+
# JSON-RPC Response Builders
|
|
930
|
+
# =============================================================================
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
def jsonrpc_success(
|
|
934
|
+
result: Any,
|
|
935
|
+
request_id: Optional[Union[str, int]] = None,
|
|
936
|
+
) -> Dict[str, Any]:
|
|
937
|
+
"""
|
|
938
|
+
Build a JSON-RPC 2.0 success response.
|
|
939
|
+
|
|
940
|
+
Args:
|
|
941
|
+
result: The result to return
|
|
942
|
+
request_id: The request ID
|
|
943
|
+
|
|
944
|
+
Returns:
|
|
945
|
+
JSON-RPC 2.0 response dict
|
|
946
|
+
"""
|
|
947
|
+
return {
|
|
948
|
+
'jsonrpc': '2.0',
|
|
949
|
+
'id': request_id,
|
|
950
|
+
'result': result,
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
|
|
954
|
+
def jsonrpc_error(
|
|
955
|
+
code: int,
|
|
956
|
+
message: str,
|
|
957
|
+
data: Optional[Any] = None,
|
|
958
|
+
request_id: Optional[Union[str, int]] = None,
|
|
959
|
+
) -> Dict[str, Any]:
|
|
960
|
+
"""
|
|
961
|
+
Build a JSON-RPC 2.0 error response.
|
|
962
|
+
|
|
963
|
+
Args:
|
|
964
|
+
code: Error code
|
|
965
|
+
message: Error message
|
|
966
|
+
data: Additional error data
|
|
967
|
+
request_id: The request ID
|
|
968
|
+
|
|
969
|
+
Returns:
|
|
970
|
+
JSON-RPC 2.0 error response dict
|
|
971
|
+
"""
|
|
972
|
+
error: Dict[str, Any] = {
|
|
973
|
+
'code': code,
|
|
974
|
+
'message': message,
|
|
975
|
+
}
|
|
976
|
+
if data is not None:
|
|
977
|
+
error['data'] = data
|
|
978
|
+
|
|
979
|
+
return {
|
|
980
|
+
'jsonrpc': '2.0',
|
|
981
|
+
'id': request_id,
|
|
982
|
+
'error': error,
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
|
|
986
|
+
def jsonrpc_error_from_exception(
|
|
987
|
+
exc: Exception,
|
|
988
|
+
request_id: Optional[Union[str, int]] = None,
|
|
989
|
+
task_id: Optional[str] = None,
|
|
990
|
+
) -> Dict[str, Any]:
|
|
991
|
+
"""
|
|
992
|
+
Build a JSON-RPC 2.0 error response from an exception.
|
|
993
|
+
|
|
994
|
+
Args:
|
|
995
|
+
exc: The exception
|
|
996
|
+
request_id: The request ID
|
|
997
|
+
task_id: Optional task ID
|
|
998
|
+
|
|
999
|
+
Returns:
|
|
1000
|
+
JSON-RPC 2.0 error response dict
|
|
1001
|
+
"""
|
|
1002
|
+
a2a_error = convert_exception(exc, task_id)
|
|
1003
|
+
return a2a_error.to_jsonrpc_error(request_id)
|
|
1004
|
+
|
|
1005
|
+
|
|
1006
|
+
# =============================================================================
|
|
1007
|
+
# Pydantic Models for Error Responses
|
|
1008
|
+
# =============================================================================
|
|
1009
|
+
|
|
1010
|
+
|
|
1011
|
+
class JSONRPCErrorData(BaseModel):
|
|
1012
|
+
"""Pydantic model for JSON-RPC error data."""
|
|
1013
|
+
|
|
1014
|
+
code: int = Field(..., description='Error code')
|
|
1015
|
+
message: str = Field(..., description='Error message')
|
|
1016
|
+
data: Optional[Any] = Field(None, description='Additional error data')
|
|
1017
|
+
|
|
1018
|
+
|
|
1019
|
+
class JSONRPCErrorResponse(BaseModel):
|
|
1020
|
+
"""Pydantic model for JSON-RPC error response."""
|
|
1021
|
+
|
|
1022
|
+
jsonrpc: str = Field('2.0', description='JSON-RPC version')
|
|
1023
|
+
id: Optional[Union[str, int]] = Field(None, description='Request ID')
|
|
1024
|
+
error: JSONRPCErrorData = Field(..., description='Error data')
|
|
1025
|
+
|
|
1026
|
+
|
|
1027
|
+
class ProblemDetails(BaseModel):
|
|
1028
|
+
"""
|
|
1029
|
+
Pydantic model for RFC 9457 Problem Details.
|
|
1030
|
+
|
|
1031
|
+
Used for REST binding error responses.
|
|
1032
|
+
"""
|
|
1033
|
+
|
|
1034
|
+
type: str = Field(
|
|
1035
|
+
..., description='URI reference identifying the problem type'
|
|
1036
|
+
)
|
|
1037
|
+
title: str = Field(..., description='Short summary of the problem type')
|
|
1038
|
+
status: int = Field(..., description='HTTP status code')
|
|
1039
|
+
detail: str = Field(
|
|
1040
|
+
..., description='Explanation specific to this occurrence'
|
|
1041
|
+
)
|
|
1042
|
+
instance: Optional[str] = Field(
|
|
1043
|
+
None, description='URI reference for this occurrence'
|
|
1044
|
+
)
|
|
1045
|
+
a2a_code: Optional[int] = Field(None, description='A2A error code')
|
|
1046
|
+
task_id: Optional[str] = Field(None, description='Related task ID')
|
|
1047
|
+
timestamp: Optional[str] = Field(None, description='ISO 8601 timestamp')
|
|
1048
|
+
additional_data: Optional[Dict[str, Any]] = Field(
|
|
1049
|
+
None, description='Additional error data'
|
|
1050
|
+
)
|
|
1051
|
+
|
|
1052
|
+
|
|
1053
|
+
# =============================================================================
|
|
1054
|
+
# Utility Functions
|
|
1055
|
+
# =============================================================================
|
|
1056
|
+
|
|
1057
|
+
|
|
1058
|
+
def is_a2a_error_code(code: int) -> bool:
|
|
1059
|
+
"""Check if an error code is an A2A-specific error."""
|
|
1060
|
+
return -32009 <= code <= -32001
|
|
1061
|
+
|
|
1062
|
+
|
|
1063
|
+
def is_jsonrpc_error_code(code: int) -> bool:
|
|
1064
|
+
"""Check if an error code is a standard JSON-RPC error."""
|
|
1065
|
+
return -32700 <= code <= -32600 or code == -32603
|
|
1066
|
+
|
|
1067
|
+
|
|
1068
|
+
def get_error_description(code: int) -> str:
|
|
1069
|
+
"""Get a human-readable description for an error code."""
|
|
1070
|
+
descriptions = {
|
|
1071
|
+
# A2A errors
|
|
1072
|
+
-32001: 'Task not found',
|
|
1073
|
+
-32002: 'Task cannot be cancelled',
|
|
1074
|
+
-32003: 'Push notifications not supported',
|
|
1075
|
+
-32004: 'Operation not supported',
|
|
1076
|
+
-32005: 'Content type not supported',
|
|
1077
|
+
-32006: 'Invalid agent response',
|
|
1078
|
+
-32007: 'Extended agent card not configured',
|
|
1079
|
+
-32008: 'Extension support required',
|
|
1080
|
+
-32009: 'Version not supported',
|
|
1081
|
+
# JSON-RPC errors
|
|
1082
|
+
-32700: 'Parse error - Invalid JSON',
|
|
1083
|
+
-32600: 'Invalid request',
|
|
1084
|
+
-32601: 'Method not found',
|
|
1085
|
+
-32602: 'Invalid params',
|
|
1086
|
+
-32603: 'Internal error',
|
|
1087
|
+
}
|
|
1088
|
+
return descriptions.get(code, f'Unknown error (code: {code})')
|
|
1089
|
+
|
|
1090
|
+
|
|
1091
|
+
__all__ = [
|
|
1092
|
+
# Base class
|
|
1093
|
+
'A2AError',
|
|
1094
|
+
# A2A errors
|
|
1095
|
+
'TaskNotFoundError',
|
|
1096
|
+
'TaskNotCancelableError',
|
|
1097
|
+
'PushNotificationNotSupportedError',
|
|
1098
|
+
'UnsupportedOperationError',
|
|
1099
|
+
'ContentTypeNotSupportedError',
|
|
1100
|
+
'InvalidAgentResponseError',
|
|
1101
|
+
'ExtendedAgentCardNotConfiguredError',
|
|
1102
|
+
'ExtensionSupportRequiredError',
|
|
1103
|
+
'VersionNotSupportedError',
|
|
1104
|
+
# JSON-RPC errors
|
|
1105
|
+
'ParseError',
|
|
1106
|
+
'InvalidRequest',
|
|
1107
|
+
'MethodNotFound',
|
|
1108
|
+
'InvalidParams',
|
|
1109
|
+
'InternalError',
|
|
1110
|
+
# Conversion utilities
|
|
1111
|
+
'error_from_code',
|
|
1112
|
+
'error_from_http_status',
|
|
1113
|
+
'convert_exception',
|
|
1114
|
+
# Decorators and middleware
|
|
1115
|
+
'a2a_error_handler',
|
|
1116
|
+
'a2a_exception_middleware',
|
|
1117
|
+
'create_exception_handlers',
|
|
1118
|
+
# JSON-RPC builders
|
|
1119
|
+
'jsonrpc_success',
|
|
1120
|
+
'jsonrpc_error',
|
|
1121
|
+
'jsonrpc_error_from_exception',
|
|
1122
|
+
# Pydantic models
|
|
1123
|
+
'JSONRPCErrorData',
|
|
1124
|
+
'JSONRPCErrorResponse',
|
|
1125
|
+
'ProblemDetails',
|
|
1126
|
+
# Maps
|
|
1127
|
+
'ERROR_CODE_MAP',
|
|
1128
|
+
'HTTP_STATUS_MAP',
|
|
1129
|
+
# Utilities
|
|
1130
|
+
'is_a2a_error_code',
|
|
1131
|
+
'is_jsonrpc_error_code',
|
|
1132
|
+
'get_error_description',
|
|
1133
|
+
]
|