theprotocol-sdk 0.1.0__tar.gz

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 (35) hide show
  1. theprotocol_sdk-0.1.0/.gitignore +31 -0
  2. theprotocol_sdk-0.1.0/PKG-INFO +96 -0
  3. theprotocol_sdk-0.1.0/README.md +61 -0
  4. theprotocol_sdk-0.1.0/examples/simple_agent.py +64 -0
  5. theprotocol_sdk-0.1.0/pyproject.toml +52 -0
  6. theprotocol_sdk-0.1.0/src/theprotocol/__init__.py +10 -0
  7. theprotocol_sdk-0.1.0/src/theprotocol/agent/__init__.py +17 -0
  8. theprotocol_sdk-0.1.0/src/theprotocol/agent/base.py +63 -0
  9. theprotocol_sdk-0.1.0/src/theprotocol/agent/decorators.py +29 -0
  10. theprotocol_sdk-0.1.0/src/theprotocol/agent/router.py +194 -0
  11. theprotocol_sdk-0.1.0/src/theprotocol/agent/task_store.py +202 -0
  12. theprotocol_sdk-0.1.0/src/theprotocol/bridges/__init__.py +9 -0
  13. theprotocol_sdk-0.1.0/src/theprotocol/bridges/acp.py +240 -0
  14. theprotocol_sdk-0.1.0/src/theprotocol/bridges/anp/__init__.py +22 -0
  15. theprotocol_sdk-0.1.0/src/theprotocol/bridges/anp/auth.py +210 -0
  16. theprotocol_sdk-0.1.0/src/theprotocol/bridges/anp/bridge.py +188 -0
  17. theprotocol_sdk-0.1.0/src/theprotocol/bridges/anp/did_wba.py +261 -0
  18. theprotocol_sdk-0.1.0/src/theprotocol/bridges/anp/translation.py +188 -0
  19. theprotocol_sdk-0.1.0/src/theprotocol/bridges/base.py +61 -0
  20. theprotocol_sdk-0.1.0/src/theprotocol/bridges/google_a2a.py +194 -0
  21. theprotocol_sdk-0.1.0/src/theprotocol/bridges/mcp.py +236 -0
  22. theprotocol_sdk-0.1.0/src/theprotocol/client/__init__.py +6 -0
  23. theprotocol_sdk-0.1.0/src/theprotocol/client/a2a_client.py +214 -0
  24. theprotocol_sdk-0.1.0/src/theprotocol/client/credentials.py +186 -0
  25. theprotocol_sdk-0.1.0/src/theprotocol/exceptions.py +76 -0
  26. theprotocol_sdk-0.1.0/src/theprotocol/models/__init__.py +30 -0
  27. theprotocol_sdk-0.1.0/src/theprotocol/models/a2a_protocol.py +128 -0
  28. theprotocol_sdk-0.1.0/src/theprotocol/models/agent_card.py +90 -0
  29. theprotocol_sdk-0.1.0/tests/test_agent.py +92 -0
  30. theprotocol_sdk-0.1.0/tests/test_bridge_acp.py +192 -0
  31. theprotocol_sdk-0.1.0/tests/test_bridge_anp.py +447 -0
  32. theprotocol_sdk-0.1.0/tests/test_bridge_google_a2a.py +175 -0
  33. theprotocol_sdk-0.1.0/tests/test_bridge_mcp.py +188 -0
  34. theprotocol_sdk-0.1.0/tests/test_credentials.py +60 -0
  35. theprotocol_sdk-0.1.0/tests/test_models.py +87 -0
@@ -0,0 +1,31 @@
1
+ # TheProtocol — Root .gitignore
2
+ # Created: 2026-03-09 (§4.1 Credentials Rotation)
3
+
4
+ # !! NEVER COMMIT PRODUCTION SECRETS !!
5
+ .env.production
6
+ .env.production.*
7
+
8
+ # Session logs (operational, not source)
9
+ sessionlogs2026/
10
+
11
+ # Debug/runtime artifacts
12
+ logs/
13
+ *.log
14
+ APEX_PREDATOR_JOBS.log
15
+
16
+ # Node
17
+ node_modules/
18
+
19
+ # Python
20
+ __pycache__/
21
+ *.pyc
22
+ *.pyo
23
+ *.pyd
24
+ .venv/
25
+ *.egg-info/
26
+ dist/
27
+ build/
28
+
29
+ # OS
30
+ .DS_Store
31
+ Thumbs.db
@@ -0,0 +1,96 @@
1
+ Metadata-Version: 2.4
2
+ Name: theprotocol-sdk
3
+ Version: 0.1.0
4
+ Summary: TheProtocol SDK — Build and call A2A agents. Protocol bridges for ACP, ADK, and more.
5
+ Project-URL: Homepage, https://theprotocol.cloud
6
+ Project-URL: Documentation, https://api.theprotocol.cloud/docs
7
+ Project-URL: Repository, https://github.com/theprotocol/theprotocol-sdk
8
+ Author-email: TheProtocol <sdk@theprotocol.cloud>
9
+ License: Apache-2.0
10
+ Keywords: a2a,agents,ai,mcp,protocol,theprotocol
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: Apache Software License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Software Development :: Libraries
19
+ Requires-Python: >=3.10
20
+ Requires-Dist: httpx[http2]>=0.27
21
+ Requires-Dist: pydantic<3.0,>=2.0
22
+ Requires-Dist: python-dotenv>=1.0
23
+ Provides-Extra: all
24
+ Requires-Dist: fastapi>=0.111; extra == 'all'
25
+ Requires-Dist: keyring>=24; extra == 'all'
26
+ Provides-Extra: keyring
27
+ Requires-Dist: keyring>=24; extra == 'keyring'
28
+ Provides-Extra: server
29
+ Requires-Dist: fastapi>=0.111; extra == 'server'
30
+ Provides-Extra: test
31
+ Requires-Dist: pytest-asyncio>=0.21; extra == 'test'
32
+ Requires-Dist: pytest>=7.0; extra == 'test'
33
+ Requires-Dist: respx>=0.20; extra == 'test'
34
+ Description-Content-Type: text/markdown
35
+
36
+ # theprotocol-sdk
37
+
38
+ Build and call A2A (Agent-to-Agent) agents on [TheProtocol](https://theprotocol.cloud).
39
+
40
+ ## Install
41
+
42
+ ```bash
43
+ pip install theprotocol-sdk # Client only (call agents)
44
+ pip install theprotocol-sdk[server] # + FastAPI router (build agents)
45
+ pip install theprotocol-sdk[all] # Everything
46
+ ```
47
+
48
+ ## Build an Agent (10 lines)
49
+
50
+ ```python
51
+ from theprotocol.agent import BaseA2AAgent, create_a2a_router
52
+ from theprotocol.models import Message, TextPart
53
+ from fastapi import FastAPI
54
+
55
+ class MyAgent(BaseA2AAgent):
56
+ async def handle_task_send(self, task_id, message):
57
+ return "task-1" # Return task ID
58
+ async def handle_task_get(self, task_id):
59
+ ... # Return Task object
60
+ async def handle_task_cancel(self, task_id):
61
+ return True
62
+ async def handle_subscribe_request(self, task_id):
63
+ yield # SSE events
64
+
65
+ app = FastAPI()
66
+ app.include_router(create_a2a_router(MyAgent()))
67
+ ```
68
+
69
+ ## Call a Remote Agent
70
+
71
+ ```python
72
+ from theprotocol.client import A2AClient, KeyManager
73
+ from theprotocol.models import Message, TextPart
74
+
75
+ async with A2AClient() as client:
76
+ task_id = await client.initiate_task(agent_card, message, key_manager)
77
+ task = await client.get_task_status(agent_card, task_id, key_manager)
78
+ print(task.state) # COMPLETED
79
+ ```
80
+
81
+ ## Protocol Bridges (coming soon)
82
+
83
+ Translate between A2A and other agent protocols:
84
+
85
+ ```python
86
+ from theprotocol.bridges.acp import ACPBridge # ACP ↔ A2A
87
+ from theprotocol.bridges.adk import ADKBridge # Google ADK ↔ A2A
88
+ ```
89
+
90
+ ## Registry Operations
91
+
92
+ For governance, staking, transfers, and discovery — use [MCP tools](https://api.theprotocol.cloud/docs) instead of SDK functions. 19 tools available via Claude Desktop or any MCP client.
93
+
94
+ ## License
95
+
96
+ Apache-2.0
@@ -0,0 +1,61 @@
1
+ # theprotocol-sdk
2
+
3
+ Build and call A2A (Agent-to-Agent) agents on [TheProtocol](https://theprotocol.cloud).
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install theprotocol-sdk # Client only (call agents)
9
+ pip install theprotocol-sdk[server] # + FastAPI router (build agents)
10
+ pip install theprotocol-sdk[all] # Everything
11
+ ```
12
+
13
+ ## Build an Agent (10 lines)
14
+
15
+ ```python
16
+ from theprotocol.agent import BaseA2AAgent, create_a2a_router
17
+ from theprotocol.models import Message, TextPart
18
+ from fastapi import FastAPI
19
+
20
+ class MyAgent(BaseA2AAgent):
21
+ async def handle_task_send(self, task_id, message):
22
+ return "task-1" # Return task ID
23
+ async def handle_task_get(self, task_id):
24
+ ... # Return Task object
25
+ async def handle_task_cancel(self, task_id):
26
+ return True
27
+ async def handle_subscribe_request(self, task_id):
28
+ yield # SSE events
29
+
30
+ app = FastAPI()
31
+ app.include_router(create_a2a_router(MyAgent()))
32
+ ```
33
+
34
+ ## Call a Remote Agent
35
+
36
+ ```python
37
+ from theprotocol.client import A2AClient, KeyManager
38
+ from theprotocol.models import Message, TextPart
39
+
40
+ async with A2AClient() as client:
41
+ task_id = await client.initiate_task(agent_card, message, key_manager)
42
+ task = await client.get_task_status(agent_card, task_id, key_manager)
43
+ print(task.state) # COMPLETED
44
+ ```
45
+
46
+ ## Protocol Bridges (coming soon)
47
+
48
+ Translate between A2A and other agent protocols:
49
+
50
+ ```python
51
+ from theprotocol.bridges.acp import ACPBridge # ACP ↔ A2A
52
+ from theprotocol.bridges.adk import ADKBridge # Google ADK ↔ A2A
53
+ ```
54
+
55
+ ## Registry Operations
56
+
57
+ For governance, staking, transfers, and discovery — use [MCP tools](https://api.theprotocol.cloud/docs) instead of SDK functions. 19 tools available via Claude Desktop or any MCP client.
58
+
59
+ ## License
60
+
61
+ Apache-2.0
@@ -0,0 +1,64 @@
1
+ """
2
+ Minimal A2A agent example.
3
+
4
+ Run: uvicorn simple_agent:app --port 8080
5
+ Then register on TheProtocol via MCP: createAgent(name="My Agent", ...)
6
+ """
7
+
8
+ import uuid
9
+ import datetime
10
+ from fastapi import FastAPI
11
+
12
+ from theprotocol.agent import BaseA2AAgent, create_a2a_router
13
+ from theprotocol.models import Message, Task, TaskState, TextPart
14
+
15
+
16
+ class EchoAgent(BaseA2AAgent):
17
+ """Simple agent that echoes back whatever you send it."""
18
+
19
+ def __init__(self):
20
+ super().__init__(agent_metadata={"name": "Echo Agent"})
21
+ self.tasks = {}
22
+
23
+ async def handle_task_send(self, task_id, message):
24
+ tid = task_id or f"task-{uuid.uuid4().hex[:8]}"
25
+ # Echo the first text part
26
+ text = ""
27
+ for part in message.parts:
28
+ if hasattr(part, 'content') and isinstance(part.content, str):
29
+ text = part.content
30
+ break
31
+ self.tasks[tid] = {
32
+ "state": TaskState.COMPLETED,
33
+ "response": f"Echo: {text}",
34
+ "message": message,
35
+ }
36
+ return tid
37
+
38
+ async def handle_task_get(self, task_id):
39
+ t = self.tasks.get(task_id)
40
+ if not t:
41
+ raise ValueError(f"Task {task_id} not found")
42
+ now = datetime.datetime.now(datetime.timezone.utc)
43
+ return Task(
44
+ id=task_id,
45
+ state=t["state"],
46
+ createdAt=now,
47
+ updatedAt=now,
48
+ messages=[t["message"]],
49
+ )
50
+
51
+ async def handle_task_cancel(self, task_id):
52
+ if task_id in self.tasks:
53
+ self.tasks[task_id]["state"] = TaskState.CANCELED
54
+ return True
55
+ return False
56
+
57
+ async def handle_subscribe_request(self, task_id):
58
+ # No streaming for this simple agent
59
+ return
60
+ yield # Make it a generator
61
+
62
+
63
+ app = FastAPI(title="Echo Agent")
64
+ app.include_router(create_a2a_router(EchoAgent()), prefix="/a2a")
@@ -0,0 +1,52 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "theprotocol-sdk"
7
+ version = "0.1.0"
8
+ description = "TheProtocol SDK — Build and call A2A agents. Protocol bridges for ACP, ADK, and more."
9
+ readme = "README.md"
10
+ license = {text = "Apache-2.0"}
11
+ requires-python = ">=3.10"
12
+ authors = [
13
+ {name = "TheProtocol", email = "sdk@theprotocol.cloud"},
14
+ ]
15
+ keywords = ["agents", "a2a", "protocol", "ai", "mcp", "theprotocol"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: Apache Software License",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Topic :: Software Development :: Libraries",
25
+ ]
26
+ dependencies = [
27
+ "pydantic>=2.0,<3.0",
28
+ "httpx[http2]>=0.27",
29
+ "python-dotenv>=1.0",
30
+ ]
31
+
32
+ [project.optional-dependencies]
33
+ server = ["fastapi>=0.111"]
34
+ keyring = ["keyring>=24"]
35
+ all = ["theprotocol-sdk[server,keyring]"]
36
+ test = [
37
+ "pytest>=7.0",
38
+ "pytest-asyncio>=0.21",
39
+ "respx>=0.20",
40
+ ]
41
+
42
+ [project.urls]
43
+ Homepage = "https://theprotocol.cloud"
44
+ Documentation = "https://api.theprotocol.cloud/docs"
45
+ Repository = "https://github.com/theprotocol/theprotocol-sdk"
46
+
47
+ [tool.hatch.build.targets.wheel]
48
+ packages = ["src/theprotocol"]
49
+
50
+ [tool.pytest.ini_options]
51
+ asyncio_mode = "auto"
52
+ testpaths = ["tests"]
@@ -0,0 +1,10 @@
1
+ """
2
+ TheProtocol SDK — Build and call A2A agents.
3
+
4
+ Usage:
5
+ from theprotocol.models import Message, Task, AgentCard
6
+ from theprotocol.agent import BaseA2AAgent, create_a2a_router
7
+ from theprotocol.client import A2AClient, KeyManager
8
+ """
9
+
10
+ __version__ = "0.1.0"
@@ -0,0 +1,17 @@
1
+ """Build A2A-compliant agents."""
2
+
3
+ from .base import BaseA2AAgent
4
+ from .task_store import BaseTaskStore, InMemoryTaskStore, TaskContext
5
+ from .decorators import a2a_method
6
+
7
+ __all__ = [
8
+ "BaseA2AAgent", "BaseTaskStore", "InMemoryTaskStore", "TaskContext",
9
+ "a2a_method",
10
+ ]
11
+
12
+ # Import router factory only if fastapi is available
13
+ try:
14
+ from .router import create_a2a_router
15
+ __all__.append("create_a2a_router")
16
+ except ImportError:
17
+ pass
@@ -0,0 +1,63 @@
1
+ """
2
+ BaseA2AAgent — Abstract base class for TheProtocol A2A agents.
3
+
4
+ Subclass this and implement the handle_* methods to create an agent.
5
+ """
6
+
7
+ import logging
8
+ from typing import Optional, Dict, Any, AsyncGenerator
9
+
10
+ from ..models import Message, Task, A2AEvent
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class BaseA2AAgent:
16
+ """
17
+ Abstract base class for A2A agents.
18
+
19
+ Implement the four handle methods to define your agent's behavior:
20
+ - handle_task_send: Process incoming messages
21
+ - handle_task_get: Return current task state
22
+ - handle_task_cancel: Cancel a running task
23
+ - handle_subscribe_request: Stream SSE events for a task
24
+
25
+ Example::
26
+
27
+ class MyAgent(BaseA2AAgent):
28
+ async def handle_task_send(self, task_id, message):
29
+ # Process the message and return task ID
30
+ return "task-123"
31
+
32
+ async def handle_task_get(self, task_id):
33
+ # Return the current task state
34
+ return Task(id=task_id, state=TaskState.COMPLETED, ...)
35
+
36
+ async def handle_task_cancel(self, task_id):
37
+ return True
38
+
39
+ async def handle_subscribe_request(self, task_id):
40
+ yield TaskStatusUpdateEvent(...)
41
+ """
42
+
43
+ def __init__(self, agent_metadata: Optional[Dict[str, Any]] = None):
44
+ self.agent_metadata = agent_metadata or {}
45
+ logger.info(f"Initialized {self.__class__.__name__}")
46
+
47
+ async def handle_task_send(self, task_id: Optional[str], message: Message) -> str:
48
+ """Handle incoming message. Return task ID."""
49
+ raise NotImplementedError("Implement handle_task_send")
50
+
51
+ async def handle_task_get(self, task_id: str) -> Task:
52
+ """Return current task state."""
53
+ raise NotImplementedError("Implement handle_task_get")
54
+
55
+ async def handle_task_cancel(self, task_id: str) -> bool:
56
+ """Cancel a task. Return True if accepted."""
57
+ raise NotImplementedError("Implement handle_task_cancel")
58
+
59
+ async def handle_subscribe_request(self, task_id: str) -> AsyncGenerator[A2AEvent, None]:
60
+ """Yield SSE events for a task."""
61
+ raise NotImplementedError("Implement handle_subscribe_request")
62
+ if False: # pragma: no cover
63
+ yield # Make it a generator
@@ -0,0 +1,29 @@
1
+ """Decorator for custom A2A JSON-RPC methods."""
2
+
3
+ import asyncio
4
+ from typing import Any, Callable, TypeVar
5
+
6
+ F = TypeVar('F', bound=Callable[..., Any])
7
+
8
+
9
+ def a2a_method(method_name: str) -> Callable[[F], F]:
10
+ """
11
+ Mark an agent method as a handler for a custom A2A JSON-RPC method.
12
+
13
+ Example::
14
+
15
+ class MyAgent(BaseA2AAgent):
16
+ @a2a_method("custom/analyze")
17
+ async def analyze(self, data: dict) -> dict:
18
+ return {"result": "analyzed"}
19
+ """
20
+ if not isinstance(method_name, str) or not method_name:
21
+ raise ValueError("a2a_method requires a non-empty method name string.")
22
+
23
+ def _decorator(func: F) -> F:
24
+ if not asyncio.iscoroutinefunction(func):
25
+ raise TypeError(f"A2A method handler '{func.__name__}' must be async.")
26
+ setattr(func, '_a2a_method_name', method_name)
27
+ return func
28
+
29
+ return _decorator
@@ -0,0 +1,194 @@
1
+ """FastAPI router factory for A2A agents."""
2
+
3
+ import json
4
+ import inspect
5
+ import logging
6
+ from typing import Any, Dict, Optional, Union, List, Callable, AsyncGenerator
7
+
8
+ import pydantic
9
+ from pydantic_core import ValidationError
10
+ from fastapi import APIRouter, Depends, Request, Response, status
11
+ from fastapi.responses import StreamingResponse, JSONResponse
12
+
13
+ from .base import BaseA2AAgent
14
+ from .task_store import BaseTaskStore, InMemoryTaskStore, TaskContext
15
+ from ..models import (
16
+ Message, Task, TaskState, A2AEvent,
17
+ TaskSendParams, TaskSendResult, TaskGetParams, GetTaskResult,
18
+ TaskCancelParams, TaskCancelResult,
19
+ TaskStatusUpdateEvent, TaskMessageEvent, TaskArtifactUpdateEvent,
20
+ )
21
+ from ..exceptions import AgentServerError, TaskNotFoundError
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+ # JSON-RPC error codes
26
+ JSONRPC_PARSE_ERROR = -32700
27
+ JSONRPC_INVALID_REQUEST = -32600
28
+ JSONRPC_METHOD_NOT_FOUND = -32601
29
+ JSONRPC_INVALID_PARAMS = -32602
30
+ JSONRPC_INTERNAL_ERROR = -32603
31
+ JSONRPC_APP_ERROR = -32000
32
+ JSONRPC_TASK_NOT_FOUND = -32001
33
+
34
+
35
+ def _jsonrpc_error(req_id: Union[str, int, None], code: int, message: str, data: Optional[Any] = None) -> Dict:
36
+ err: Dict[str, Any] = {"code": code, "message": message}
37
+ if data is not None:
38
+ err["data"] = data
39
+ return {"jsonrpc": "2.0", "error": err, "id": req_id}
40
+
41
+
42
+ def _jsonrpc_success(req_id: Union[str, int, None], result: Any) -> Dict:
43
+ return {"jsonrpc": "2.0", "result": result, "id": req_id}
44
+
45
+
46
+ def _format_sse_bytes(event: A2AEvent) -> Optional[bytes]:
47
+ event_type = None
48
+ if isinstance(event, TaskStatusUpdateEvent):
49
+ event_type = "task_status"
50
+ elif isinstance(event, TaskMessageEvent):
51
+ event_type = "task_message"
52
+ elif isinstance(event, TaskArtifactUpdateEvent):
53
+ event_type = "task_artifact"
54
+ if event_type is None:
55
+ return None
56
+ try:
57
+ json_data = event.model_dump_json(by_alias=True)
58
+ return f"event: {event_type}\ndata: {json_data}\n\n".encode("utf-8")
59
+ except Exception:
60
+ return None
61
+
62
+
63
+ async def _sse_wrapper(task_id: str, gen: AsyncGenerator[A2AEvent, None]) -> AsyncGenerator[bytes, None]:
64
+ try:
65
+ async for event in gen:
66
+ data = _format_sse_bytes(event)
67
+ if data:
68
+ yield data
69
+ except TaskNotFoundError as e:
70
+ yield f'event: error\ndata: {json.dumps({"error": "task_not_found", "message": str(e)})}\n\n'.encode()
71
+ except Exception as e:
72
+ yield f'event: error\ndata: {json.dumps({"error": "stream_error", "message": str(e)})}\n\n'.encode()
73
+
74
+
75
+ def create_a2a_router(
76
+ agent: BaseA2AAgent,
77
+ prefix: str = "",
78
+ tags: Optional[List[str]] = None,
79
+ task_store: Optional[BaseTaskStore] = None,
80
+ dependencies: Optional[list] = None,
81
+ ) -> APIRouter:
82
+ """
83
+ Create a FastAPI router that exposes an A2A agent via JSON-RPC 2.0.
84
+
85
+ Example::
86
+
87
+ from theprotocol.agent import BaseA2AAgent, create_a2a_router
88
+ from fastapi import FastAPI
89
+
90
+ class MyAgent(BaseA2AAgent):
91
+ async def handle_task_send(self, task_id, message):
92
+ return "task-1"
93
+ ...
94
+
95
+ app = FastAPI()
96
+ app.include_router(create_a2a_router(MyAgent()))
97
+ """
98
+ if tags is None:
99
+ tags = ["A2A Protocol"]
100
+ if task_store is None:
101
+ task_store = InMemoryTaskStore()
102
+ store = task_store
103
+
104
+ router = APIRouter(prefix=prefix, tags=tags, dependencies=dependencies or [])
105
+
106
+ # Discover @a2a_method decorated handlers
107
+ decorated: Dict[str, Callable] = {}
108
+ for name, method in inspect.getmembers(agent, predicate=inspect.iscoroutinefunction):
109
+ if hasattr(method, '_a2a_method_name'):
110
+ decorated[getattr(method, '_a2a_method_name')] = method
111
+
112
+ @router.post("/", summary="A2A JSON-RPC Endpoint")
113
+ async def handle_request(request: Request) -> Response:
114
+ req_id = None
115
+ try:
116
+ payload = await request.json()
117
+ except json.JSONDecodeError:
118
+ return JSONResponse(_jsonrpc_error(None, JSONRPC_PARSE_ERROR, "Parse error"))
119
+
120
+ if not isinstance(payload, dict):
121
+ return JSONResponse(_jsonrpc_error(None, JSONRPC_INVALID_REQUEST, "Payload must be JSON object"))
122
+
123
+ req_id = payload.get("id")
124
+ method = payload.get("method", "")
125
+ params = payload.get("params") or {}
126
+ if isinstance(params, list):
127
+ params = {}
128
+
129
+ if payload.get("jsonrpc") != "2.0":
130
+ return JSONResponse(_jsonrpc_error(req_id, JSONRPC_INVALID_REQUEST, "'jsonrpc' must be '2.0'"))
131
+ if not isinstance(method, str) or not method:
132
+ return JSONResponse(_jsonrpc_error(req_id, JSONRPC_INVALID_REQUEST, "'method' is required"))
133
+
134
+ try:
135
+ # Check decorated methods first
136
+ if method in decorated:
137
+ handler = decorated[method]
138
+ sig = inspect.signature(handler)
139
+ param_fields: Dict[str, Any] = {}
140
+ for pname, p in sig.parameters.items():
141
+ if pname in ('self', 'cls'):
142
+ continue
143
+ ann = Any if p.annotation is inspect.Parameter.empty else p.annotation
144
+ default = ... if p.default is inspect.Parameter.empty else p.default
145
+ param_fields[pname] = (ann, default)
146
+
147
+ mapped = dict(params)
148
+ if "task_id" in [p for p in sig.parameters if p not in ('self', 'cls')] and "id" in params:
149
+ mapped['task_id'] = params['id']
150
+
151
+ ParamsModel = pydantic.create_model(f'{handler.__name__}Params', **param_fields)
152
+ validated = ParamsModel.model_validate(mapped).model_dump()
153
+ result = await handler(**validated)
154
+ return JSONResponse(_jsonrpc_success(req_id, result))
155
+
156
+ elif method == "tasks/send":
157
+ vp = TaskSendParams.model_validate(params)
158
+ task_id = await agent.handle_task_send(task_id=vp.id, message=vp.message)
159
+ return JSONResponse(_jsonrpc_success(req_id, TaskSendResult(id=task_id).model_dump(mode='json')))
160
+
161
+ elif method == "tasks/get":
162
+ vp = TaskGetParams.model_validate(params)
163
+ task = await agent.handle_task_get(task_id=vp.id)
164
+ return JSONResponse(_jsonrpc_success(req_id, task.model_dump(mode='json', by_alias=True)))
165
+
166
+ elif method == "tasks/cancel":
167
+ vp = TaskCancelParams.model_validate(params)
168
+ ok = await agent.handle_task_cancel(task_id=vp.id)
169
+ return JSONResponse(_jsonrpc_success(req_id, TaskCancelResult(success=ok).model_dump(mode='json')))
170
+
171
+ elif method == "tasks/sendSubscribe":
172
+ task_id = params.get("id", "")
173
+ if not task_id:
174
+ return JSONResponse(_jsonrpc_error(req_id, JSONRPC_INVALID_PARAMS, "'id' is required"))
175
+ gen = agent.handle_subscribe_request(task_id=task_id)
176
+ return StreamingResponse(content=_sse_wrapper(task_id, gen), media_type="text/event-stream")
177
+
178
+ else:
179
+ return JSONResponse(_jsonrpc_error(req_id, JSONRPC_METHOD_NOT_FOUND, f"Method not found: {method}"))
180
+
181
+ except TaskNotFoundError as e:
182
+ return JSONResponse(_jsonrpc_error(req_id, JSONRPC_TASK_NOT_FOUND, str(e)))
183
+ except (ValueError, TypeError, pydantic.ValidationError, ValidationError) as e:
184
+ return JSONResponse(_jsonrpc_error(req_id, JSONRPC_INVALID_PARAMS, f"Invalid parameters: {e}"))
185
+ except AgentServerError as e:
186
+ return JSONResponse(_jsonrpc_error(req_id, JSONRPC_APP_ERROR, f"Agent error: {e}"))
187
+ except Exception as e:
188
+ logger.exception(f"Unhandled error in A2A handler: {e}")
189
+ return JSONResponse(
190
+ _jsonrpc_error(req_id, JSONRPC_INTERNAL_ERROR, f"Internal error: {type(e).__name__}"),
191
+ status_code=500,
192
+ )
193
+
194
+ return router