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.
Files changed (66) hide show
  1. a2a_server/__init__.py +29 -0
  2. a2a_server/a2a_agent_card.py +365 -0
  3. a2a_server/a2a_errors.py +1133 -0
  4. a2a_server/a2a_executor.py +926 -0
  5. a2a_server/a2a_router.py +1033 -0
  6. a2a_server/a2a_types.py +344 -0
  7. a2a_server/agent_card.py +408 -0
  8. a2a_server/agents_server.py +271 -0
  9. a2a_server/auth_api.py +349 -0
  10. a2a_server/billing_api.py +638 -0
  11. a2a_server/billing_service.py +712 -0
  12. a2a_server/billing_webhooks.py +501 -0
  13. a2a_server/config.py +96 -0
  14. a2a_server/database.py +2165 -0
  15. a2a_server/email_inbound.py +398 -0
  16. a2a_server/email_notifications.py +486 -0
  17. a2a_server/enhanced_agents.py +919 -0
  18. a2a_server/enhanced_server.py +160 -0
  19. a2a_server/hosted_worker.py +1049 -0
  20. a2a_server/integrated_agents_server.py +347 -0
  21. a2a_server/keycloak_auth.py +750 -0
  22. a2a_server/livekit_bridge.py +439 -0
  23. a2a_server/marketing_tools.py +1364 -0
  24. a2a_server/mcp_client.py +196 -0
  25. a2a_server/mcp_http_server.py +2256 -0
  26. a2a_server/mcp_server.py +191 -0
  27. a2a_server/message_broker.py +725 -0
  28. a2a_server/mock_mcp.py +273 -0
  29. a2a_server/models.py +494 -0
  30. a2a_server/monitor_api.py +5904 -0
  31. a2a_server/opencode_bridge.py +1594 -0
  32. a2a_server/redis_task_manager.py +518 -0
  33. a2a_server/server.py +726 -0
  34. a2a_server/task_manager.py +668 -0
  35. a2a_server/task_queue.py +742 -0
  36. a2a_server/tenant_api.py +333 -0
  37. a2a_server/tenant_middleware.py +219 -0
  38. a2a_server/tenant_service.py +760 -0
  39. a2a_server/user_auth.py +721 -0
  40. a2a_server/vault_client.py +576 -0
  41. a2a_server/worker_sse.py +873 -0
  42. agent_worker/__init__.py +8 -0
  43. agent_worker/worker.py +4877 -0
  44. codetether/__init__.py +10 -0
  45. codetether/__main__.py +4 -0
  46. codetether/cli.py +112 -0
  47. codetether/worker_cli.py +57 -0
  48. codetether-1.2.2.dist-info/METADATA +570 -0
  49. codetether-1.2.2.dist-info/RECORD +66 -0
  50. codetether-1.2.2.dist-info/WHEEL +5 -0
  51. codetether-1.2.2.dist-info/entry_points.txt +4 -0
  52. codetether-1.2.2.dist-info/licenses/LICENSE +202 -0
  53. codetether-1.2.2.dist-info/top_level.txt +5 -0
  54. codetether_voice_agent/__init__.py +6 -0
  55. codetether_voice_agent/agent.py +445 -0
  56. codetether_voice_agent/codetether_mcp.py +345 -0
  57. codetether_voice_agent/config.py +16 -0
  58. codetether_voice_agent/functiongemma_caller.py +380 -0
  59. codetether_voice_agent/session_playback.py +247 -0
  60. codetether_voice_agent/tools/__init__.py +21 -0
  61. codetether_voice_agent/tools/definitions.py +135 -0
  62. codetether_voice_agent/tools/handlers.py +380 -0
  63. run_server.py +314 -0
  64. ui/monitor-tailwind.html +1790 -0
  65. ui/monitor.html +1775 -0
  66. ui/monitor.js +2662 -0
@@ -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
+ ]