agent-relay 3.2.2 → 3.2.4
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.
- package/bin/agent-relay-broker-darwin-arm64 +0 -0
- package/bin/agent-relay-broker-darwin-x64 +0 -0
- package/bin/agent-relay-broker-linux-arm64 +0 -0
- package/bin/agent-relay-broker-linux-x64 +0 -0
- package/dist/index.cjs +1358 -941
- package/dist/src/cli/commands/agent-management.d.ts +2 -2
- package/dist/src/cli/commands/agent-management.d.ts.map +1 -1
- package/dist/src/cli/commands/agent-management.js +41 -240
- package/dist/src/cli/commands/agent-management.js.map +1 -1
- package/dist/src/cli/commands/messaging.d.ts +1 -1
- package/dist/src/cli/commands/messaging.d.ts.map +1 -1
- package/dist/src/cli/commands/messaging.js +14 -5
- package/dist/src/cli/commands/messaging.js.map +1 -1
- package/dist/src/cli/lib/agent-management-listing.d.ts +4 -1
- package/dist/src/cli/lib/agent-management-listing.d.ts.map +1 -1
- package/dist/src/cli/lib/agent-management-listing.js +27 -2
- package/dist/src/cli/lib/agent-management-listing.js.map +1 -1
- package/package.json +11 -10
- package/packages/acp-bridge/package.json +2 -2
- package/packages/config/package.json +1 -1
- package/packages/hooks/package.json +4 -4
- package/packages/memory/package.json +2 -2
- package/packages/openclaw/package.json +2 -2
- package/packages/policy/package.json +2 -2
- package/packages/sdk/ADAPTER_REVIEW.md +109 -0
- package/packages/sdk/dist/client.d.ts +66 -0
- package/packages/sdk/dist/client.d.ts.map +1 -1
- package/packages/sdk/dist/client.js +230 -0
- package/packages/sdk/dist/client.js.map +1 -1
- package/packages/sdk/dist/communicate/a2a-bridge.d.ts +25 -0
- package/packages/sdk/dist/communicate/a2a-bridge.d.ts.map +1 -0
- package/packages/sdk/dist/communicate/a2a-bridge.js +89 -0
- package/packages/sdk/dist/communicate/a2a-bridge.js.map +1 -0
- package/packages/sdk/dist/communicate/a2a-server.d.ts +31 -0
- package/packages/sdk/dist/communicate/a2a-server.d.ts.map +1 -0
- package/packages/sdk/dist/communicate/a2a-server.js +220 -0
- package/packages/sdk/dist/communicate/a2a-server.js.map +1 -0
- package/packages/sdk/dist/communicate/a2a-transport.d.ts +48 -0
- package/packages/sdk/dist/communicate/a2a-transport.d.ts.map +1 -0
- package/packages/sdk/dist/communicate/a2a-transport.js +302 -0
- package/packages/sdk/dist/communicate/a2a-transport.js.map +1 -0
- package/packages/sdk/dist/communicate/a2a-types.d.ts +107 -0
- package/packages/sdk/dist/communicate/a2a-types.d.ts.map +1 -0
- package/packages/sdk/dist/communicate/a2a-types.js +209 -0
- package/packages/sdk/dist/communicate/a2a-types.js.map +1 -0
- package/packages/sdk/dist/communicate/adapters/claude-sdk.d.ts +28 -0
- package/packages/sdk/dist/communicate/adapters/claude-sdk.d.ts.map +1 -0
- package/packages/sdk/dist/communicate/adapters/claude-sdk.js +47 -0
- package/packages/sdk/dist/communicate/adapters/claude-sdk.js.map +1 -0
- package/packages/sdk/dist/communicate/adapters/crewai.d.ts +42 -0
- package/packages/sdk/dist/communicate/adapters/crewai.d.ts.map +1 -0
- package/packages/sdk/dist/communicate/adapters/crewai.js +95 -0
- package/packages/sdk/dist/communicate/adapters/crewai.js.map +1 -0
- package/packages/sdk/dist/communicate/adapters/google-adk.d.ts +53 -0
- package/packages/sdk/dist/communicate/adapters/google-adk.d.ts.map +1 -0
- package/packages/sdk/dist/communicate/adapters/google-adk.js +77 -0
- package/packages/sdk/dist/communicate/adapters/google-adk.js.map +1 -0
- package/packages/sdk/dist/communicate/adapters/index.d.ts +7 -0
- package/packages/sdk/dist/communicate/adapters/index.d.ts.map +1 -0
- package/packages/sdk/dist/communicate/adapters/index.js +7 -0
- package/packages/sdk/dist/communicate/adapters/index.js.map +1 -0
- package/packages/sdk/dist/communicate/adapters/langgraph.d.ts +40 -0
- package/packages/sdk/dist/communicate/adapters/langgraph.d.ts.map +1 -0
- package/packages/sdk/dist/communicate/adapters/langgraph.js +77 -0
- package/packages/sdk/dist/communicate/adapters/langgraph.js.map +1 -0
- package/packages/sdk/dist/communicate/adapters/openai-agents.d.ts +25 -0
- package/packages/sdk/dist/communicate/adapters/openai-agents.d.ts.map +1 -0
- package/packages/sdk/dist/communicate/adapters/openai-agents.js +70 -0
- package/packages/sdk/dist/communicate/adapters/openai-agents.js.map +1 -0
- package/packages/sdk/dist/communicate/adapters/pi.d.ts +45 -0
- package/packages/sdk/dist/communicate/adapters/pi.d.ts.map +1 -0
- package/packages/sdk/dist/communicate/adapters/pi.js +59 -0
- package/packages/sdk/dist/communicate/adapters/pi.js.map +1 -0
- package/packages/sdk/dist/communicate/core.d.ts +58 -0
- package/packages/sdk/dist/communicate/core.d.ts.map +1 -0
- package/packages/sdk/dist/communicate/core.js +128 -0
- package/packages/sdk/dist/communicate/core.js.map +1 -0
- package/packages/sdk/dist/communicate/index.d.ts +4 -0
- package/packages/sdk/dist/communicate/index.d.ts.map +1 -0
- package/packages/sdk/dist/communicate/index.js +4 -0
- package/packages/sdk/dist/communicate/index.js.map +1 -0
- package/packages/sdk/dist/communicate/transport.d.ts +36 -0
- package/packages/sdk/dist/communicate/transport.d.ts.map +1 -0
- package/packages/sdk/dist/communicate/transport.js +371 -0
- package/packages/sdk/dist/communicate/transport.js.map +1 -0
- package/packages/sdk/dist/communicate/types.d.ts +58 -0
- package/packages/sdk/dist/communicate/types.d.ts.map +1 -0
- package/packages/sdk/dist/communicate/types.js +66 -0
- package/packages/sdk/dist/communicate/types.js.map +1 -0
- package/packages/sdk/dist/workflows/builder.d.ts +35 -5
- package/packages/sdk/dist/workflows/builder.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/builder.js +81 -7
- package/packages/sdk/dist/workflows/builder.js.map +1 -1
- package/packages/sdk/dist/workflows/cli.js +14 -1
- package/packages/sdk/dist/workflows/cli.js.map +1 -1
- package/packages/sdk/dist/workflows/runner.d.ts +10 -2
- package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/runner.js +95 -1
- package/packages/sdk/dist/workflows/runner.js.map +1 -1
- package/packages/sdk/dist/workflows/types.d.ts +11 -0
- package/packages/sdk/dist/workflows/types.d.ts.map +1 -1
- package/packages/sdk/examples/communicate/claude_sdk_example.ts +5 -0
- package/packages/sdk/examples/communicate/pi_example.ts +8 -0
- package/packages/sdk/package.json +48 -2
- package/packages/sdk/src/__tests__/builder-deterministic.test.ts +132 -0
- package/packages/sdk/src/__tests__/communicate/a2a-bridge.test.ts +211 -0
- package/packages/sdk/src/__tests__/communicate/a2a-server.test.ts +359 -0
- package/packages/sdk/src/__tests__/communicate/a2a-transport.test.ts +537 -0
- package/packages/sdk/src/__tests__/communicate/a2a-types.test.ts +297 -0
- package/packages/sdk/src/__tests__/communicate/adapters/claude-sdk.test.ts +163 -0
- package/packages/sdk/src/__tests__/communicate/adapters/crewai.test.ts +219 -0
- package/packages/sdk/src/__tests__/communicate/adapters/e2e-crewai.test.ts +101 -0
- package/packages/sdk/src/__tests__/communicate/adapters/e2e-google-adk.test.ts +166 -0
- package/packages/sdk/src/__tests__/communicate/adapters/e2e-langgraph.test.ts +181 -0
- package/packages/sdk/src/__tests__/communicate/adapters/e2e-openai-agents.test.ts +137 -0
- package/packages/sdk/src/__tests__/communicate/adapters/e2e-pi.test.ts +140 -0
- package/packages/sdk/src/__tests__/communicate/adapters/google-adk.test.ts +200 -0
- package/packages/sdk/src/__tests__/communicate/adapters/langgraph.test.ts +162 -0
- package/packages/sdk/src/__tests__/communicate/adapters/openai-agents.test.ts +166 -0
- package/packages/sdk/src/__tests__/communicate/adapters/pi.test.ts +140 -0
- package/packages/sdk/src/__tests__/communicate/core.test.ts +574 -0
- package/packages/sdk/src/__tests__/communicate/integration/cross-framework.test.ts +353 -0
- package/packages/sdk/src/__tests__/communicate/transport.test.ts +613 -0
- package/packages/sdk/src/__tests__/start-from.test.ts +346 -0
- package/packages/sdk/src/client.ts +301 -0
- package/packages/sdk/src/communicate/a2a-bridge.ts +111 -0
- package/packages/sdk/src/communicate/a2a-server.ts +277 -0
- package/packages/sdk/src/communicate/a2a-transport.ts +395 -0
- package/packages/sdk/src/communicate/a2a-types.ts +338 -0
- package/packages/sdk/src/communicate/adapters/claude-sdk.ts +85 -0
- package/packages/sdk/src/communicate/adapters/crewai.ts +141 -0
- package/packages/sdk/src/communicate/adapters/google-adk.ts +139 -0
- package/packages/sdk/src/communicate/adapters/index.ts +6 -0
- package/packages/sdk/src/communicate/adapters/langgraph.ts +112 -0
- package/packages/sdk/src/communicate/adapters/openai-agents.ts +113 -0
- package/packages/sdk/src/communicate/adapters/pi.ts +105 -0
- package/packages/sdk/src/communicate/core.ts +157 -0
- package/packages/sdk/src/communicate/index.ts +3 -0
- package/packages/sdk/src/communicate/transport.ts +489 -0
- package/packages/sdk/src/communicate/types.ts +106 -0
- package/packages/sdk/src/examples/workflows/fix-dashboard-user-registration.yaml +182 -0
- package/packages/sdk/src/workflows/builder.ts +97 -9
- package/packages/sdk/src/workflows/cli.ts +16 -1
- package/packages/sdk/src/workflows/runner.ts +110 -1
- package/packages/sdk/src/workflows/types.ts +14 -0
- package/packages/sdk/tsconfig.build.json +1 -7
- package/packages/sdk/tsconfig.json +1 -7
- package/packages/sdk-py/README.md +67 -25
- package/packages/sdk-py/examples/communicate/agno_example.py +8 -0
- package/packages/sdk-py/examples/communicate/claude_sdk_example.py +6 -0
- package/packages/sdk-py/examples/communicate/crewai_example.py +7 -0
- package/packages/sdk-py/examples/communicate/google_adk_example.py +7 -0
- package/packages/sdk-py/examples/communicate/openai_agents_example.py +8 -0
- package/packages/sdk-py/examples/communicate/swarms_example.py +7 -0
- package/packages/sdk-py/pyproject.toml +12 -1
- package/packages/sdk-py/src/agent_relay/__init__.py +8 -0
- package/packages/sdk-py/src/agent_relay/builder.py +65 -26
- package/packages/sdk-py/src/agent_relay/communicate/__init__.py +6 -0
- package/packages/sdk-py/src/agent_relay/communicate/a2a_bridge.py +138 -0
- package/packages/sdk-py/src/agent_relay/communicate/a2a_server.py +242 -0
- package/packages/sdk-py/src/agent_relay/communicate/a2a_transport.py +366 -0
- package/packages/sdk-py/src/agent_relay/communicate/a2a_types.py +294 -0
- package/packages/sdk-py/src/agent_relay/communicate/adapters/__init__.py +10 -0
- package/packages/sdk-py/src/agent_relay/communicate/adapters/agno.py +74 -0
- package/packages/sdk-py/src/agent_relay/communicate/adapters/claude_sdk.py +78 -0
- package/packages/sdk-py/src/agent_relay/communicate/adapters/crewai.py +143 -0
- package/packages/sdk-py/src/agent_relay/communicate/adapters/google_adk.py +69 -0
- package/packages/sdk-py/src/agent_relay/communicate/adapters/openai_agents.py +86 -0
- package/packages/sdk-py/src/agent_relay/communicate/adapters/pi.py +175 -0
- package/packages/sdk-py/src/agent_relay/communicate/adapters/swarms.py +44 -0
- package/packages/sdk-py/src/agent_relay/communicate/core.py +293 -0
- package/packages/sdk-py/src/agent_relay/communicate/transport.py +502 -0
- package/packages/sdk-py/src/agent_relay/communicate/types.py +89 -0
- package/packages/sdk-py/src/agent_relay/types.py +2 -1
- package/packages/sdk-py/tests/communicate/__init__.py +0 -0
- package/packages/sdk-py/tests/communicate/adapters/__init__.py +0 -0
- package/packages/sdk-py/tests/communicate/adapters/e2e_test_agno.py +154 -0
- package/packages/sdk-py/tests/communicate/adapters/e2e_test_claude_sdk.py +428 -0
- package/packages/sdk-py/tests/communicate/adapters/e2e_test_crewai.py +234 -0
- package/packages/sdk-py/tests/communicate/adapters/e2e_test_google_adk.py +182 -0
- package/packages/sdk-py/tests/communicate/adapters/e2e_test_langgraph.py +262 -0
- package/packages/sdk-py/tests/communicate/adapters/e2e_test_openai_agents.py +88 -0
- package/packages/sdk-py/tests/communicate/adapters/e2e_test_pi.py +156 -0
- package/packages/sdk-py/tests/communicate/adapters/e2e_test_swarms.py +239 -0
- package/packages/sdk-py/tests/communicate/adapters/test_agno.py +140 -0
- package/packages/sdk-py/tests/communicate/adapters/test_claude_sdk.py +147 -0
- package/packages/sdk-py/tests/communicate/adapters/test_crewai.py +136 -0
- package/packages/sdk-py/tests/communicate/adapters/test_google_adk.py +125 -0
- package/packages/sdk-py/tests/communicate/adapters/test_openai_agents.py +99 -0
- package/packages/sdk-py/tests/communicate/adapters/test_pi.py +270 -0
- package/packages/sdk-py/tests/communicate/adapters/test_swarms.py +113 -0
- package/packages/sdk-py/tests/communicate/conftest.py +555 -0
- package/packages/sdk-py/tests/communicate/integration/__init__.py +1 -0
- package/packages/sdk-py/tests/communicate/integration/test_cross_framework.py +331 -0
- package/packages/sdk-py/tests/communicate/integration/test_end_to_end.py +151 -0
- package/packages/sdk-py/tests/communicate/test_a2a_bridge.py +363 -0
- package/packages/sdk-py/tests/communicate/test_a2a_server.py +346 -0
- package/packages/sdk-py/tests/communicate/test_a2a_transport.py +561 -0
- package/packages/sdk-py/tests/communicate/test_a2a_types.py +342 -0
- package/packages/sdk-py/tests/communicate/test_auto_detect.py +67 -0
- package/packages/sdk-py/tests/communicate/test_core.py +331 -0
- package/packages/sdk-py/tests/communicate/test_transport.py +373 -0
- package/packages/sdk-py/tests/communicate/test_types.py +285 -0
- package/packages/sdk-py/tests/test_builder_deterministic.py +118 -0
- package/packages/telemetry/package.json +1 -1
- package/packages/trajectory/package.json +2 -2
- package/packages/user-directory/package.json +2 -2
- package/packages/utils/package.json +2 -2
- package/packages/sdk/dist/__tests__/completion-pipeline.test.d.ts +0 -14
- package/packages/sdk/dist/__tests__/completion-pipeline.test.d.ts.map +0 -1
- package/packages/sdk/dist/__tests__/completion-pipeline.test.js +0 -1476
- package/packages/sdk/dist/__tests__/completion-pipeline.test.js.map +0 -1
- package/packages/sdk/dist/__tests__/contract-fixtures.test.d.ts +0 -2
- package/packages/sdk/dist/__tests__/contract-fixtures.test.d.ts.map +0 -1
- package/packages/sdk/dist/__tests__/contract-fixtures.test.js +0 -152
- package/packages/sdk/dist/__tests__/contract-fixtures.test.js.map +0 -1
- package/packages/sdk/dist/__tests__/e2e-owner-review.test.d.ts +0 -16
- package/packages/sdk/dist/__tests__/e2e-owner-review.test.d.ts.map +0 -1
- package/packages/sdk/dist/__tests__/e2e-owner-review.test.js +0 -640
- package/packages/sdk/dist/__tests__/e2e-owner-review.test.js.map +0 -1
- package/packages/sdk/dist/__tests__/facade.test.d.ts +0 -2
- package/packages/sdk/dist/__tests__/facade.test.d.ts.map +0 -1
- package/packages/sdk/dist/__tests__/facade.test.js +0 -305
- package/packages/sdk/dist/__tests__/facade.test.js.map +0 -1
- package/packages/sdk/dist/__tests__/integration.test.d.ts +0 -2
- package/packages/sdk/dist/__tests__/integration.test.d.ts.map +0 -1
- package/packages/sdk/dist/__tests__/integration.test.js +0 -205
- package/packages/sdk/dist/__tests__/integration.test.js.map +0 -1
- package/packages/sdk/dist/__tests__/pty.test.d.ts +0 -2
- package/packages/sdk/dist/__tests__/pty.test.d.ts.map +0 -1
- package/packages/sdk/dist/__tests__/pty.test.js +0 -20
- package/packages/sdk/dist/__tests__/pty.test.js.map +0 -1
- package/packages/sdk/dist/__tests__/quickstart.test.d.ts +0 -2
- package/packages/sdk/dist/__tests__/quickstart.test.d.ts.map +0 -1
- package/packages/sdk/dist/__tests__/quickstart.test.js +0 -176
- package/packages/sdk/dist/__tests__/quickstart.test.js.map +0 -1
- package/packages/sdk/dist/__tests__/spawn-from-env.test.d.ts +0 -2
- package/packages/sdk/dist/__tests__/spawn-from-env.test.d.ts.map +0 -1
- package/packages/sdk/dist/__tests__/spawn-from-env.test.js +0 -222
- package/packages/sdk/dist/__tests__/spawn-from-env.test.js.map +0 -1
- package/packages/sdk/dist/__tests__/unit.test.d.ts +0 -2
- package/packages/sdk/dist/__tests__/unit.test.d.ts.map +0 -1
- package/packages/sdk/dist/__tests__/unit.test.js +0 -357
- package/packages/sdk/dist/__tests__/unit.test.js.map +0 -1
- package/packages/sdk-py/agent_relay/__init__.py +0 -21
- package/packages/sdk-py/agent_relay/models.py +0 -398
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""A2A-compliant HTTP server that exposes a Relay agent as an A2A endpoint."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
from dataclasses import asdict
|
|
7
|
+
from typing import Any, Callable, Awaitable
|
|
8
|
+
|
|
9
|
+
from aiohttp import web
|
|
10
|
+
|
|
11
|
+
from .a2a_types import (
|
|
12
|
+
A2AAgentCard,
|
|
13
|
+
A2AMessage,
|
|
14
|
+
A2APart,
|
|
15
|
+
A2ASkill,
|
|
16
|
+
A2ATask,
|
|
17
|
+
A2ATaskStatus,
|
|
18
|
+
A2AConfig,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class A2AServer:
|
|
23
|
+
"""Lightweight HTTP server that exposes a Relay agent as an A2A endpoint.
|
|
24
|
+
|
|
25
|
+
Routes:
|
|
26
|
+
GET /.well-known/agent.json -> Agent Card
|
|
27
|
+
POST / -> JSON-RPC 2.0 dispatcher
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
agent_name: str,
|
|
33
|
+
port: int = 5000,
|
|
34
|
+
host: str = "0.0.0.0",
|
|
35
|
+
skills: list[A2ASkill] | None = None,
|
|
36
|
+
) -> None:
|
|
37
|
+
self.agent_name = agent_name
|
|
38
|
+
self.port = port
|
|
39
|
+
self.host = host
|
|
40
|
+
self.skills = skills or []
|
|
41
|
+
self.tasks: dict[str, A2ATask] = {}
|
|
42
|
+
self._on_message: Callable[[A2AMessage], Awaitable[A2AMessage | None] | A2AMessage | None] | None = None
|
|
43
|
+
|
|
44
|
+
self._app = web.Application()
|
|
45
|
+
self._app.router.add_get("/.well-known/agent.json", self._handle_agent_card)
|
|
46
|
+
self._app.router.add_post("/", self._handle_jsonrpc)
|
|
47
|
+
|
|
48
|
+
self._runner: web.AppRunner | None = None
|
|
49
|
+
self._site: web.TCPSite | None = None
|
|
50
|
+
self._actual_port: int | None = None
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def url(self) -> str:
|
|
54
|
+
port = self._actual_port or self.port
|
|
55
|
+
return f"http://{self.host}:{port}"
|
|
56
|
+
|
|
57
|
+
def on_message(self, callback: Callable[[A2AMessage], Awaitable[A2AMessage | None] | A2AMessage | None]) -> None:
|
|
58
|
+
"""Register callback for incoming A2A messages."""
|
|
59
|
+
self._on_message = callback
|
|
60
|
+
|
|
61
|
+
def get_agent_card(self) -> A2AAgentCard:
|
|
62
|
+
"""Build Agent Card for this agent."""
|
|
63
|
+
return A2AAgentCard(
|
|
64
|
+
name=self.agent_name,
|
|
65
|
+
description=f"Agent Relay agent: {self.agent_name}",
|
|
66
|
+
url=self.url,
|
|
67
|
+
skills=list(self.skills),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
async def handle_message_send(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
71
|
+
"""Handle JSON-RPC message/send.
|
|
72
|
+
|
|
73
|
+
1. Extract message from params
|
|
74
|
+
2. Create or update Task
|
|
75
|
+
3. Call on_message callback
|
|
76
|
+
4. Return Task response
|
|
77
|
+
"""
|
|
78
|
+
message_data = params.get("message", {})
|
|
79
|
+
parts = [A2APart(text=p.get("text")) for p in message_data.get("parts", [])]
|
|
80
|
+
incoming = A2AMessage(
|
|
81
|
+
role=message_data.get("role", "user"),
|
|
82
|
+
parts=parts,
|
|
83
|
+
messageId=message_data.get("messageId") or str(uuid.uuid4()),
|
|
84
|
+
contextId=message_data.get("contextId"),
|
|
85
|
+
taskId=message_data.get("taskId"),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# Create or find task
|
|
89
|
+
task_id = incoming.taskId or str(uuid.uuid4())
|
|
90
|
+
context_id = incoming.contextId or str(uuid.uuid4())
|
|
91
|
+
|
|
92
|
+
if task_id in self.tasks:
|
|
93
|
+
task = self.tasks[task_id]
|
|
94
|
+
task.messages.append(incoming)
|
|
95
|
+
task.status = A2ATaskStatus(state="working")
|
|
96
|
+
else:
|
|
97
|
+
task = A2ATask(
|
|
98
|
+
id=task_id,
|
|
99
|
+
contextId=context_id,
|
|
100
|
+
status=A2ATaskStatus(state="working"),
|
|
101
|
+
messages=[incoming],
|
|
102
|
+
)
|
|
103
|
+
self.tasks[task_id] = task
|
|
104
|
+
|
|
105
|
+
# Invoke callback
|
|
106
|
+
response_msg: A2AMessage | None = None
|
|
107
|
+
if self._on_message is not None:
|
|
108
|
+
result = self._on_message(incoming)
|
|
109
|
+
if hasattr(result, "__await__"):
|
|
110
|
+
response_msg = await result # type: ignore[union-attr]
|
|
111
|
+
else:
|
|
112
|
+
response_msg = result # type: ignore[assignment]
|
|
113
|
+
|
|
114
|
+
if response_msg is not None:
|
|
115
|
+
task.messages.append(response_msg)
|
|
116
|
+
task.status = A2ATaskStatus(state="completed", message=response_msg)
|
|
117
|
+
else:
|
|
118
|
+
task.status = A2ATaskStatus(state="completed")
|
|
119
|
+
|
|
120
|
+
return self._task_to_dict(task)
|
|
121
|
+
|
|
122
|
+
async def handle_tasks_get(self, task_id: str) -> dict[str, Any]:
|
|
123
|
+
"""JSON-RPC: tasks/get — return task state."""
|
|
124
|
+
task = self.tasks.get(task_id)
|
|
125
|
+
if task is None:
|
|
126
|
+
raise KeyError(f"Task not found: {task_id}")
|
|
127
|
+
return self._task_to_dict(task)
|
|
128
|
+
|
|
129
|
+
async def handle_tasks_cancel(self, task_id: str) -> dict[str, Any]:
|
|
130
|
+
"""JSON-RPC: tasks/cancel — cancel a running task."""
|
|
131
|
+
task = self.tasks.get(task_id)
|
|
132
|
+
if task is None:
|
|
133
|
+
raise KeyError(f"Task not found: {task_id}")
|
|
134
|
+
task.status = A2ATaskStatus(state="canceled")
|
|
135
|
+
return self._task_to_dict(task)
|
|
136
|
+
|
|
137
|
+
async def start(self) -> None:
|
|
138
|
+
"""Start aiohttp server."""
|
|
139
|
+
self._runner = web.AppRunner(self._app)
|
|
140
|
+
await self._runner.setup()
|
|
141
|
+
self._site = web.TCPSite(self._runner, self.host, self.port)
|
|
142
|
+
await self._site.start()
|
|
143
|
+
|
|
144
|
+
# Resolve actual port (useful when port=0)
|
|
145
|
+
server = getattr(self._site, "_server", None)
|
|
146
|
+
if server is not None and server.sockets:
|
|
147
|
+
self._actual_port = server.sockets[0].getsockname()[1]
|
|
148
|
+
|
|
149
|
+
async def stop(self) -> None:
|
|
150
|
+
"""Stop server."""
|
|
151
|
+
if self._runner is not None:
|
|
152
|
+
await self._runner.cleanup()
|
|
153
|
+
self._runner = None
|
|
154
|
+
self._site = None
|
|
155
|
+
|
|
156
|
+
# --- HTTP Handlers ---
|
|
157
|
+
|
|
158
|
+
async def _handle_agent_card(self, request: web.Request) -> web.Response:
|
|
159
|
+
card = self.get_agent_card()
|
|
160
|
+
return web.json_response(self._agent_card_to_dict(card))
|
|
161
|
+
|
|
162
|
+
async def _handle_jsonrpc(self, request: web.Request) -> web.Response:
|
|
163
|
+
try:
|
|
164
|
+
body = await request.json()
|
|
165
|
+
except Exception:
|
|
166
|
+
return web.json_response(
|
|
167
|
+
{"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error"}, "id": None},
|
|
168
|
+
status=400,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
method = body.get("method", "")
|
|
172
|
+
params = body.get("params", {})
|
|
173
|
+
rpc_id = body.get("id")
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
if method == "message/send":
|
|
177
|
+
result = await self.handle_message_send(params)
|
|
178
|
+
elif method == "tasks/get":
|
|
179
|
+
task_id = params.get("id") or params.get("taskId", "")
|
|
180
|
+
result = await self.handle_tasks_get(task_id)
|
|
181
|
+
elif method == "tasks/cancel":
|
|
182
|
+
task_id = params.get("id") or params.get("taskId", "")
|
|
183
|
+
result = await self.handle_tasks_cancel(task_id)
|
|
184
|
+
else:
|
|
185
|
+
return web.json_response(
|
|
186
|
+
{"jsonrpc": "2.0", "error": {"code": -32601, "message": f"Method not found: {method}"}, "id": rpc_id},
|
|
187
|
+
status=400,
|
|
188
|
+
)
|
|
189
|
+
except KeyError as exc:
|
|
190
|
+
return web.json_response(
|
|
191
|
+
{"jsonrpc": "2.0", "error": {"code": -32602, "message": str(exc)}, "id": rpc_id},
|
|
192
|
+
status=404,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
return web.json_response({"jsonrpc": "2.0", "result": result, "id": rpc_id})
|
|
196
|
+
|
|
197
|
+
# --- Serialization helpers ---
|
|
198
|
+
|
|
199
|
+
@staticmethod
|
|
200
|
+
def _task_to_dict(task: A2ATask) -> dict[str, Any]:
|
|
201
|
+
status_dict: dict[str, Any] = {"state": task.status.state}
|
|
202
|
+
if task.status.message is not None:
|
|
203
|
+
status_dict["message"] = {
|
|
204
|
+
"role": task.status.message.role,
|
|
205
|
+
"parts": [{"text": p.text} for p in task.status.message.parts],
|
|
206
|
+
}
|
|
207
|
+
if task.status.message.messageId:
|
|
208
|
+
status_dict["message"]["messageId"] = task.status.message.messageId
|
|
209
|
+
|
|
210
|
+
messages = []
|
|
211
|
+
for m in task.messages:
|
|
212
|
+
md: dict[str, Any] = {
|
|
213
|
+
"role": m.role,
|
|
214
|
+
"parts": [{"text": p.text} for p in m.parts],
|
|
215
|
+
}
|
|
216
|
+
if m.messageId:
|
|
217
|
+
md["messageId"] = m.messageId
|
|
218
|
+
messages.append(md)
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
"id": task.id,
|
|
222
|
+
"contextId": task.contextId,
|
|
223
|
+
"status": status_dict,
|
|
224
|
+
"messages": messages,
|
|
225
|
+
"artifacts": task.artifacts,
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
@staticmethod
|
|
229
|
+
def _agent_card_to_dict(card: A2AAgentCard) -> dict[str, Any]:
|
|
230
|
+
return {
|
|
231
|
+
"name": card.name,
|
|
232
|
+
"description": card.description,
|
|
233
|
+
"url": card.url,
|
|
234
|
+
"version": card.version,
|
|
235
|
+
"capabilities": card.capabilities,
|
|
236
|
+
"skills": [{"id": s.id, "name": s.name, "description": s.description} for s in card.skills],
|
|
237
|
+
"defaultInputModes": card.defaultInputModes,
|
|
238
|
+
"defaultOutputModes": card.defaultOutputModes,
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
__all__ = ["A2AServer"]
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
"""A2A (Agent-to-Agent) protocol transport implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import uuid
|
|
8
|
+
from contextlib import suppress
|
|
9
|
+
from inspect import isawaitable
|
|
10
|
+
from typing import Any, Callable
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
import aiohttp
|
|
14
|
+
from aiohttp import web
|
|
15
|
+
except ImportError:
|
|
16
|
+
raise ImportError(
|
|
17
|
+
"A2A transport requires 'aiohttp'. "
|
|
18
|
+
"Install it with: pip install agent-relay-sdk[communicate]"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from .a2a_types import (
|
|
22
|
+
A2AAgentCard,
|
|
23
|
+
A2AConfig,
|
|
24
|
+
A2AMessage,
|
|
25
|
+
A2APart,
|
|
26
|
+
A2ASkill,
|
|
27
|
+
A2ATask,
|
|
28
|
+
A2ATaskStatus,
|
|
29
|
+
A2A_TASK_NOT_CANCELABLE,
|
|
30
|
+
A2A_TASK_NOT_FOUND,
|
|
31
|
+
JSONRPC_INTERNAL_ERROR,
|
|
32
|
+
JSONRPC_INVALID_PARAMS,
|
|
33
|
+
JSONRPC_METHOD_NOT_FOUND,
|
|
34
|
+
JSONRPC_PARSE_ERROR,
|
|
35
|
+
make_jsonrpc_error,
|
|
36
|
+
make_jsonrpc_request,
|
|
37
|
+
make_jsonrpc_response,
|
|
38
|
+
)
|
|
39
|
+
from .types import Message, MessageCallback
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class A2ATransport:
|
|
43
|
+
"""
|
|
44
|
+
Transport that speaks A2A protocol instead of Relaycast API.
|
|
45
|
+
|
|
46
|
+
Client side: sends JSON-RPC 2.0 to external A2A agent endpoints.
|
|
47
|
+
Server side: runs a local HTTP server accepting A2A JSON-RPC calls.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(self, config: A2AConfig) -> None:
|
|
51
|
+
self.config = config
|
|
52
|
+
self.agent_name: str | None = None
|
|
53
|
+
self.agent_card: A2AAgentCard | None = None
|
|
54
|
+
self.tasks: dict[str, A2ATask] = {}
|
|
55
|
+
self._message_callbacks: list[MessageCallback] = []
|
|
56
|
+
self._session: aiohttp.ClientSession | None = None
|
|
57
|
+
self._app: web.Application | None = None
|
|
58
|
+
self._runner: web.AppRunner | None = None
|
|
59
|
+
self._site: web.TCPSite | None = None
|
|
60
|
+
self._discovered_cards: dict[str, A2AAgentCard] = {}
|
|
61
|
+
self._closing = False
|
|
62
|
+
|
|
63
|
+
# === Transport interface ===
|
|
64
|
+
|
|
65
|
+
async def register(self, name: str) -> dict[str, Any]:
|
|
66
|
+
"""
|
|
67
|
+
Register by starting an HTTP server that serves:
|
|
68
|
+
- GET /.well-known/agent.json -> AgentCard
|
|
69
|
+
- POST / -> JSON-RPC 2.0 endpoint
|
|
70
|
+
"""
|
|
71
|
+
self.agent_name = name
|
|
72
|
+
self.agent_card = A2AAgentCard(
|
|
73
|
+
name=name,
|
|
74
|
+
description=self.config.agent_description or f"Agent Relay agent: {name}",
|
|
75
|
+
url=f"http://{self.config.server_host}:{self.config.server_port}",
|
|
76
|
+
skills=list(self.config.skills),
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
self._app = web.Application()
|
|
80
|
+
self._app.router.add_get("/.well-known/agent.json", self._handle_agent_card)
|
|
81
|
+
self._app.router.add_post("/", self._handle_jsonrpc_http)
|
|
82
|
+
|
|
83
|
+
self._runner = web.AppRunner(self._app)
|
|
84
|
+
await self._runner.setup()
|
|
85
|
+
self._site = web.TCPSite(self._runner, self.config.server_host, self.config.server_port)
|
|
86
|
+
await self._site.start()
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
"name": name,
|
|
90
|
+
"url": self.agent_card.url,
|
|
91
|
+
"type": "a2a",
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async def unregister(self) -> None:
|
|
95
|
+
"""Stop the HTTP server and clean up."""
|
|
96
|
+
self._closing = True
|
|
97
|
+
if self._site is not None:
|
|
98
|
+
await self._site.stop()
|
|
99
|
+
self._site = None
|
|
100
|
+
if self._runner is not None:
|
|
101
|
+
await self._runner.cleanup()
|
|
102
|
+
self._runner = None
|
|
103
|
+
self._app = None
|
|
104
|
+
await self._close_session()
|
|
105
|
+
self._closing = False
|
|
106
|
+
|
|
107
|
+
async def send_dm(self, target: str, text: str) -> dict[str, Any]:
|
|
108
|
+
"""
|
|
109
|
+
Send a message to an external A2A agent.
|
|
110
|
+
|
|
111
|
+
target: URL of the A2A agent endpoint
|
|
112
|
+
text: message text to send
|
|
113
|
+
"""
|
|
114
|
+
card = await self._discover_agent(target)
|
|
115
|
+
|
|
116
|
+
message = A2AMessage(
|
|
117
|
+
role="user",
|
|
118
|
+
parts=[A2APart(text=text)],
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
rpc_request = make_jsonrpc_request(
|
|
122
|
+
"message/send",
|
|
123
|
+
{"message": message.to_dict()},
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
session = await self._ensure_session()
|
|
127
|
+
headers = self._auth_headers()
|
|
128
|
+
headers["Content-Type"] = "application/json"
|
|
129
|
+
|
|
130
|
+
async with session.post(card.url, json=rpc_request, headers=headers) as resp:
|
|
131
|
+
body = await resp.json()
|
|
132
|
+
|
|
133
|
+
if "error" in body:
|
|
134
|
+
err = body["error"]
|
|
135
|
+
raise A2AError(err.get("code", -1), err.get("message", "Unknown error"))
|
|
136
|
+
|
|
137
|
+
result = body.get("result", {})
|
|
138
|
+
return self._a2a_result_to_relay(result, card.name)
|
|
139
|
+
|
|
140
|
+
async def list_agents(self) -> list[dict[str, Any]]:
|
|
141
|
+
"""List known A2A agents from registry."""
|
|
142
|
+
agents: list[dict[str, Any]] = []
|
|
143
|
+
for url in self.config.registry:
|
|
144
|
+
try:
|
|
145
|
+
card = await self._discover_agent(url)
|
|
146
|
+
agents.append({
|
|
147
|
+
"name": card.name,
|
|
148
|
+
"url": card.url,
|
|
149
|
+
"description": card.description,
|
|
150
|
+
"skills": [s.to_dict() for s in card.skills],
|
|
151
|
+
})
|
|
152
|
+
except Exception:
|
|
153
|
+
continue
|
|
154
|
+
return agents
|
|
155
|
+
|
|
156
|
+
def on_message(self, callback: MessageCallback) -> None:
|
|
157
|
+
"""Register callback for incoming A2A messages."""
|
|
158
|
+
self._message_callbacks.append(callback)
|
|
159
|
+
|
|
160
|
+
async def connect_ws(self) -> None:
|
|
161
|
+
"""
|
|
162
|
+
A2A uses HTTP, not WebSocket. This is a no-op.
|
|
163
|
+
The HTTP server started in register() handles incoming calls.
|
|
164
|
+
"""
|
|
165
|
+
pass
|
|
166
|
+
|
|
167
|
+
# === HTTP handlers for incoming A2A requests ===
|
|
168
|
+
|
|
169
|
+
async def _handle_agent_card(self, request: web.Request) -> web.Response:
|
|
170
|
+
"""Serve the Agent Card at /.well-known/agent.json."""
|
|
171
|
+
if self.agent_card is None:
|
|
172
|
+
return web.json_response({"error": "Not registered"}, status=503)
|
|
173
|
+
return web.json_response(self.agent_card.to_dict())
|
|
174
|
+
|
|
175
|
+
async def _handle_jsonrpc_http(self, request: web.Request) -> web.Response:
|
|
176
|
+
"""Handle incoming JSON-RPC 2.0 requests over HTTP."""
|
|
177
|
+
try:
|
|
178
|
+
body = await request.json()
|
|
179
|
+
except (json.JSONDecodeError, Exception):
|
|
180
|
+
error = make_jsonrpc_error(JSONRPC_PARSE_ERROR, "Parse error", None)
|
|
181
|
+
return web.json_response(error)
|
|
182
|
+
|
|
183
|
+
result = await self._dispatch_jsonrpc(body)
|
|
184
|
+
return web.json_response(result)
|
|
185
|
+
|
|
186
|
+
async def _dispatch_jsonrpc(self, request: dict[str, Any]) -> dict[str, Any]:
|
|
187
|
+
"""Dispatch a JSON-RPC request to the appropriate handler."""
|
|
188
|
+
rpc_id = request.get("id")
|
|
189
|
+
method = request.get("method", "")
|
|
190
|
+
params = request.get("params", {})
|
|
191
|
+
|
|
192
|
+
handlers = {
|
|
193
|
+
"message/send": self._handle_message_send,
|
|
194
|
+
"tasks/get": self._handle_tasks_get,
|
|
195
|
+
"tasks/cancel": self._handle_tasks_cancel,
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
handler = handlers.get(method)
|
|
199
|
+
if handler is None:
|
|
200
|
+
return make_jsonrpc_error(JSONRPC_METHOD_NOT_FOUND, f"Method not found: {method}", rpc_id)
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
result = await handler(params)
|
|
204
|
+
return make_jsonrpc_response(result, rpc_id)
|
|
205
|
+
except A2AError as exc:
|
|
206
|
+
return make_jsonrpc_error(exc.code, exc.message, rpc_id)
|
|
207
|
+
except Exception as exc:
|
|
208
|
+
return make_jsonrpc_error(JSONRPC_INTERNAL_ERROR, str(exc), rpc_id)
|
|
209
|
+
|
|
210
|
+
async def _handle_message_send(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
211
|
+
"""Handle message/send JSON-RPC method."""
|
|
212
|
+
msg_data = params.get("message")
|
|
213
|
+
if not msg_data:
|
|
214
|
+
raise A2AError(JSONRPC_INVALID_PARAMS, "Missing 'message' in params")
|
|
215
|
+
|
|
216
|
+
a2a_msg = A2AMessage.from_dict(msg_data)
|
|
217
|
+
|
|
218
|
+
# Create or update task
|
|
219
|
+
task_id = a2a_msg.taskId or str(uuid.uuid4())
|
|
220
|
+
context_id = a2a_msg.contextId or str(uuid.uuid4())
|
|
221
|
+
|
|
222
|
+
if task_id in self.tasks:
|
|
223
|
+
task = self.tasks[task_id]
|
|
224
|
+
task.messages.append(a2a_msg)
|
|
225
|
+
task.status = A2ATaskStatus(state="working")
|
|
226
|
+
else:
|
|
227
|
+
task = A2ATask(
|
|
228
|
+
id=task_id,
|
|
229
|
+
contextId=context_id,
|
|
230
|
+
status=A2ATaskStatus(state="working"),
|
|
231
|
+
messages=[a2a_msg],
|
|
232
|
+
)
|
|
233
|
+
self.tasks[task_id] = task
|
|
234
|
+
|
|
235
|
+
# Convert to Relay message and invoke callbacks
|
|
236
|
+
relay_msg = self._a2a_to_relay_msg(a2a_msg, sender="a2a-client")
|
|
237
|
+
await self._invoke_callbacks(relay_msg)
|
|
238
|
+
|
|
239
|
+
# Mark completed
|
|
240
|
+
task.status = A2ATaskStatus(state="completed")
|
|
241
|
+
|
|
242
|
+
return task.to_dict()
|
|
243
|
+
|
|
244
|
+
async def _handle_tasks_get(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
245
|
+
"""Handle tasks/get JSON-RPC method."""
|
|
246
|
+
task_id = params.get("id")
|
|
247
|
+
if not task_id or task_id not in self.tasks:
|
|
248
|
+
raise A2AError(A2A_TASK_NOT_FOUND, f"Task not found: {task_id}")
|
|
249
|
+
return self.tasks[task_id].to_dict()
|
|
250
|
+
|
|
251
|
+
async def _handle_tasks_cancel(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
252
|
+
"""Handle tasks/cancel JSON-RPC method."""
|
|
253
|
+
task_id = params.get("id")
|
|
254
|
+
if not task_id or task_id not in self.tasks:
|
|
255
|
+
raise A2AError(A2A_TASK_NOT_FOUND, f"Task not found: {task_id}")
|
|
256
|
+
|
|
257
|
+
task = self.tasks[task_id]
|
|
258
|
+
if task.status.state in ("completed", "failed", "canceled"):
|
|
259
|
+
raise A2AError(A2A_TASK_NOT_CANCELABLE, f"Task {task_id} is already {task.status.state}")
|
|
260
|
+
|
|
261
|
+
task.status = A2ATaskStatus(state="canceled")
|
|
262
|
+
return task.to_dict()
|
|
263
|
+
|
|
264
|
+
# === Agent discovery ===
|
|
265
|
+
|
|
266
|
+
async def _discover_agent(self, url: str) -> A2AAgentCard:
|
|
267
|
+
"""Fetch and parse Agent Card from /.well-known/agent.json."""
|
|
268
|
+
url = url.rstrip("/")
|
|
269
|
+
|
|
270
|
+
if url in self._discovered_cards:
|
|
271
|
+
return self._discovered_cards[url]
|
|
272
|
+
|
|
273
|
+
session = await self._ensure_session()
|
|
274
|
+
card_url = f"{url}/.well-known/agent.json"
|
|
275
|
+
|
|
276
|
+
async with session.get(card_url) as resp:
|
|
277
|
+
if resp.status != 200:
|
|
278
|
+
raise A2AError(-1, f"Failed to discover agent at {card_url}: HTTP {resp.status}")
|
|
279
|
+
data = await resp.json()
|
|
280
|
+
|
|
281
|
+
card = A2AAgentCard.from_dict(data)
|
|
282
|
+
self._discovered_cards[url] = card
|
|
283
|
+
return card
|
|
284
|
+
|
|
285
|
+
# === Message conversion ===
|
|
286
|
+
|
|
287
|
+
@staticmethod
|
|
288
|
+
def _relay_msg_to_a2a(text: str, sender: str) -> A2AMessage:
|
|
289
|
+
"""Convert Relay message text to A2A Message."""
|
|
290
|
+
return A2AMessage(
|
|
291
|
+
role="user",
|
|
292
|
+
parts=[A2APart(text=text)],
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
@staticmethod
|
|
296
|
+
def _a2a_to_relay_msg(msg: A2AMessage, sender: str = "unknown") -> Message:
|
|
297
|
+
"""Convert A2A Message to Relay Message format."""
|
|
298
|
+
text = msg.get_text()
|
|
299
|
+
return Message(
|
|
300
|
+
sender=sender,
|
|
301
|
+
text=text,
|
|
302
|
+
channel=None,
|
|
303
|
+
thread_id=msg.contextId,
|
|
304
|
+
message_id=msg.messageId,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
@staticmethod
|
|
308
|
+
def _a2a_result_to_relay(result: dict[str, Any], sender: str) -> dict[str, Any]:
|
|
309
|
+
"""Convert A2A task/message result to Relay-compatible dict."""
|
|
310
|
+
# Result could be a Task dict
|
|
311
|
+
messages = result.get("messages", [])
|
|
312
|
+
text = ""
|
|
313
|
+
if messages:
|
|
314
|
+
last_msg = messages[-1]
|
|
315
|
+
parts = last_msg.get("parts", [])
|
|
316
|
+
text = " ".join(p.get("text", "") for p in parts if p.get("text"))
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
"sender": sender,
|
|
320
|
+
"text": text,
|
|
321
|
+
"task_id": result.get("id"),
|
|
322
|
+
"status": result.get("status", {}).get("state"),
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
# === Internal helpers ===
|
|
326
|
+
|
|
327
|
+
async def _invoke_callbacks(self, msg: Message) -> None:
|
|
328
|
+
"""Invoke all registered message callbacks."""
|
|
329
|
+
for cb in self._message_callbacks:
|
|
330
|
+
result = cb(msg)
|
|
331
|
+
if isawaitable(result):
|
|
332
|
+
await result
|
|
333
|
+
|
|
334
|
+
def _auth_headers(self) -> dict[str, str]:
|
|
335
|
+
"""Build auth headers from config."""
|
|
336
|
+
headers: dict[str, str] = {}
|
|
337
|
+
if self.config.auth_token:
|
|
338
|
+
if self.config.auth_scheme == "bearer":
|
|
339
|
+
headers["Authorization"] = f"Bearer {self.config.auth_token}"
|
|
340
|
+
elif self.config.auth_scheme == "api_key":
|
|
341
|
+
headers["X-API-Key"] = self.config.auth_token
|
|
342
|
+
else:
|
|
343
|
+
headers["Authorization"] = f"Bearer {self.config.auth_token}"
|
|
344
|
+
return headers
|
|
345
|
+
|
|
346
|
+
async def _ensure_session(self) -> aiohttp.ClientSession:
|
|
347
|
+
if self._session is None or self._session.closed:
|
|
348
|
+
self._session = aiohttp.ClientSession()
|
|
349
|
+
return self._session
|
|
350
|
+
|
|
351
|
+
async def _close_session(self) -> None:
|
|
352
|
+
if self._session is not None and not self._session.closed:
|
|
353
|
+
await self._session.close()
|
|
354
|
+
self._session = None
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
class A2AError(Exception):
|
|
358
|
+
"""Error raised during A2A protocol operations."""
|
|
359
|
+
|
|
360
|
+
def __init__(self, code: int, message: str) -> None:
|
|
361
|
+
self.code = code
|
|
362
|
+
self.message = message
|
|
363
|
+
super().__init__(f"A2A error {code}: {message}")
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
__all__ = ["A2AError", "A2ATransport"]
|