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,363 @@
|
|
|
1
|
+
"""Tests for A2ABridge — bidirectional message forwarding between A2A and Relay."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import uuid
|
|
8
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
|
9
|
+
|
|
10
|
+
import aiohttp
|
|
11
|
+
import pytest
|
|
12
|
+
import pytest_asyncio
|
|
13
|
+
from aiohttp import web
|
|
14
|
+
|
|
15
|
+
from agent_relay.communicate.a2a_bridge import A2ABridge
|
|
16
|
+
from agent_relay.communicate.a2a_server import A2AServer
|
|
17
|
+
from agent_relay.communicate.a2a_types import A2AMessage, A2APart, A2ASkill
|
|
18
|
+
from agent_relay.communicate.types import Message, RelayConfig
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MockA2AAgent:
|
|
22
|
+
"""A minimal mock A2A agent server for testing the bridge."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, name: str = "external-agent", response_text: str = "A2A response") -> None:
|
|
25
|
+
self.name = name
|
|
26
|
+
self.response_text = response_text
|
|
27
|
+
self.received_messages: list[dict] = []
|
|
28
|
+
|
|
29
|
+
self._app = web.Application()
|
|
30
|
+
self._app.router.add_get("/.well-known/agent.json", self._handle_card)
|
|
31
|
+
self._app.router.add_post("/", self._handle_jsonrpc)
|
|
32
|
+
self._runner: web.AppRunner | None = None
|
|
33
|
+
self._site: web.TCPSite | None = None
|
|
34
|
+
self.url = ""
|
|
35
|
+
|
|
36
|
+
async def _handle_card(self, request: web.Request) -> web.Response:
|
|
37
|
+
return web.json_response({
|
|
38
|
+
"name": self.name,
|
|
39
|
+
"description": f"Mock A2A agent: {self.name}",
|
|
40
|
+
"url": self.url,
|
|
41
|
+
"version": "1.0.0",
|
|
42
|
+
"capabilities": {"streaming": False, "pushNotifications": False},
|
|
43
|
+
"skills": [],
|
|
44
|
+
"defaultInputModes": ["text"],
|
|
45
|
+
"defaultOutputModes": ["text"],
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
async def _handle_jsonrpc(self, request: web.Request) -> web.Response:
|
|
49
|
+
body = await request.json()
|
|
50
|
+
self.received_messages.append(body)
|
|
51
|
+
|
|
52
|
+
method = body.get("method", "")
|
|
53
|
+
rpc_id = body.get("id")
|
|
54
|
+
|
|
55
|
+
if method == "message/send":
|
|
56
|
+
task_id = str(uuid.uuid4())
|
|
57
|
+
response_msg = {
|
|
58
|
+
"role": "agent",
|
|
59
|
+
"parts": [{"text": self.response_text}],
|
|
60
|
+
"messageId": str(uuid.uuid4()),
|
|
61
|
+
}
|
|
62
|
+
result = {
|
|
63
|
+
"id": task_id,
|
|
64
|
+
"contextId": str(uuid.uuid4()),
|
|
65
|
+
"status": {"state": "completed", "message": response_msg},
|
|
66
|
+
"messages": [
|
|
67
|
+
body.get("params", {}).get("message", {}),
|
|
68
|
+
response_msg,
|
|
69
|
+
],
|
|
70
|
+
"artifacts": [],
|
|
71
|
+
}
|
|
72
|
+
return web.json_response({"jsonrpc": "2.0", "result": result, "id": rpc_id})
|
|
73
|
+
|
|
74
|
+
return web.json_response(
|
|
75
|
+
{"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found"}, "id": rpc_id}
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
async def start(self) -> None:
|
|
79
|
+
self._runner = web.AppRunner(self._app)
|
|
80
|
+
await self._runner.setup()
|
|
81
|
+
self._site = web.TCPSite(self._runner, "127.0.0.1", 0)
|
|
82
|
+
await self._site.start()
|
|
83
|
+
server = getattr(self._site, "_server", None)
|
|
84
|
+
if server and server.sockets:
|
|
85
|
+
port = server.sockets[0].getsockname()[1]
|
|
86
|
+
self.url = f"http://127.0.0.1:{port}"
|
|
87
|
+
|
|
88
|
+
async def stop(self) -> None:
|
|
89
|
+
if self._runner is not None:
|
|
90
|
+
await self._runner.cleanup()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class MockA2AAgentEmpty:
|
|
94
|
+
"""A mock A2A agent that returns no response text."""
|
|
95
|
+
|
|
96
|
+
def __init__(self) -> None:
|
|
97
|
+
self._app = web.Application()
|
|
98
|
+
self._app.router.add_post("/", self._handle_jsonrpc)
|
|
99
|
+
self._runner: web.AppRunner | None = None
|
|
100
|
+
self._site: web.TCPSite | None = None
|
|
101
|
+
self.url = ""
|
|
102
|
+
|
|
103
|
+
async def _handle_jsonrpc(self, request: web.Request) -> web.Response:
|
|
104
|
+
body = await request.json()
|
|
105
|
+
rpc_id = body.get("id")
|
|
106
|
+
result = {
|
|
107
|
+
"id": str(uuid.uuid4()),
|
|
108
|
+
"status": {"state": "completed"},
|
|
109
|
+
"messages": [],
|
|
110
|
+
"artifacts": [],
|
|
111
|
+
}
|
|
112
|
+
return web.json_response({"jsonrpc": "2.0", "result": result, "id": rpc_id})
|
|
113
|
+
|
|
114
|
+
async def start(self) -> None:
|
|
115
|
+
self._runner = web.AppRunner(self._app)
|
|
116
|
+
await self._runner.setup()
|
|
117
|
+
self._site = web.TCPSite(self._runner, "127.0.0.1", 0)
|
|
118
|
+
await self._site.start()
|
|
119
|
+
server = getattr(self._site, "_server", None)
|
|
120
|
+
if server and server.sockets:
|
|
121
|
+
port = server.sockets[0].getsockname()[1]
|
|
122
|
+
self.url = f"http://127.0.0.1:{port}"
|
|
123
|
+
|
|
124
|
+
async def stop(self) -> None:
|
|
125
|
+
if self._runner is not None:
|
|
126
|
+
await self._runner.cleanup()
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@pytest_asyncio.fixture
|
|
130
|
+
async def mock_a2a_agent():
|
|
131
|
+
agent = MockA2AAgent(response_text="Hello from external A2A!")
|
|
132
|
+
await agent.start()
|
|
133
|
+
try:
|
|
134
|
+
yield agent
|
|
135
|
+
finally:
|
|
136
|
+
await agent.stop()
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# --- Unit tests with mocked Relay ---
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class TestA2ABridgeConstruction:
|
|
143
|
+
def test_bridge_creation(self):
|
|
144
|
+
config = RelayConfig(workspace="test-ws", api_key="test-key", base_url="http://localhost:9999")
|
|
145
|
+
bridge = A2ABridge(
|
|
146
|
+
relay_config=config,
|
|
147
|
+
a2a_agent_url="http://localhost:8000",
|
|
148
|
+
proxy_name="proxy-agent",
|
|
149
|
+
)
|
|
150
|
+
assert bridge.proxy_name == "proxy-agent"
|
|
151
|
+
assert bridge.a2a_agent_url == "http://localhost:8000"
|
|
152
|
+
assert bridge.relay.agent_name == "proxy-agent"
|
|
153
|
+
assert bridge._started is False
|
|
154
|
+
|
|
155
|
+
def test_url_trailing_slash_stripped(self):
|
|
156
|
+
config = RelayConfig(workspace="test-ws", api_key="test-key", base_url="http://localhost:9999")
|
|
157
|
+
bridge = A2ABridge(
|
|
158
|
+
relay_config=config,
|
|
159
|
+
a2a_agent_url="http://localhost:8000/",
|
|
160
|
+
proxy_name="proxy",
|
|
161
|
+
)
|
|
162
|
+
assert bridge.a2a_agent_url == "http://localhost:8000"
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class TestA2ABridgeDiscovery:
|
|
166
|
+
async def test_discover_agent(self, mock_a2a_agent: MockA2AAgent):
|
|
167
|
+
config = RelayConfig(workspace="test-ws", api_key="test-key", base_url="http://localhost:9999")
|
|
168
|
+
bridge = A2ABridge(
|
|
169
|
+
relay_config=config,
|
|
170
|
+
a2a_agent_url=mock_a2a_agent.url,
|
|
171
|
+
proxy_name="proxy",
|
|
172
|
+
)
|
|
173
|
+
try:
|
|
174
|
+
card = await bridge.discover_agent()
|
|
175
|
+
assert card.name == "external-agent"
|
|
176
|
+
assert card.url == mock_a2a_agent.url
|
|
177
|
+
assert card.version == "1.0.0"
|
|
178
|
+
finally:
|
|
179
|
+
if bridge._session and not bridge._session.closed:
|
|
180
|
+
await bridge._session.close()
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class TestA2ABridgeSendMessage:
|
|
184
|
+
async def test_send_a2a_message(self, mock_a2a_agent: MockA2AAgent):
|
|
185
|
+
config = RelayConfig(workspace="test-ws", api_key="test-key", base_url="http://localhost:9999")
|
|
186
|
+
bridge = A2ABridge(
|
|
187
|
+
relay_config=config,
|
|
188
|
+
a2a_agent_url=mock_a2a_agent.url,
|
|
189
|
+
proxy_name="proxy",
|
|
190
|
+
)
|
|
191
|
+
try:
|
|
192
|
+
response_text = await bridge.send_a2a_message("Hello, A2A agent!")
|
|
193
|
+
assert response_text == "Hello from external A2A!"
|
|
194
|
+
|
|
195
|
+
# Verify the mock received the JSON-RPC request
|
|
196
|
+
assert len(mock_a2a_agent.received_messages) == 1
|
|
197
|
+
req = mock_a2a_agent.received_messages[0]
|
|
198
|
+
assert req["method"] == "message/send"
|
|
199
|
+
assert req["params"]["message"]["role"] == "user"
|
|
200
|
+
assert req["params"]["message"]["parts"][0]["text"] == "Hello, A2A agent!"
|
|
201
|
+
finally:
|
|
202
|
+
if bridge._session and not bridge._session.closed:
|
|
203
|
+
await bridge._session.close()
|
|
204
|
+
|
|
205
|
+
async def test_send_uses_discovered_card_url(self, mock_a2a_agent: MockA2AAgent):
|
|
206
|
+
config = RelayConfig(workspace="test-ws", api_key="test-key", base_url="http://localhost:9999")
|
|
207
|
+
bridge = A2ABridge(
|
|
208
|
+
relay_config=config,
|
|
209
|
+
a2a_agent_url=mock_a2a_agent.url,
|
|
210
|
+
proxy_name="proxy",
|
|
211
|
+
)
|
|
212
|
+
try:
|
|
213
|
+
# Discover first, then send
|
|
214
|
+
await bridge.discover_agent()
|
|
215
|
+
response = await bridge.send_a2a_message("After discovery")
|
|
216
|
+
assert response == "Hello from external A2A!"
|
|
217
|
+
finally:
|
|
218
|
+
if bridge._session and not bridge._session.closed:
|
|
219
|
+
await bridge._session.close()
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class TestA2ABridgeRelayForwarding:
|
|
223
|
+
async def test_handle_relay_message_forwards_to_a2a(self, mock_a2a_agent: MockA2AAgent):
|
|
224
|
+
"""Test that _handle_relay_message forwards to A2A and sends response back."""
|
|
225
|
+
config = RelayConfig(workspace="test-ws", api_key="test-key", base_url="http://localhost:9999")
|
|
226
|
+
bridge = A2ABridge(
|
|
227
|
+
relay_config=config,
|
|
228
|
+
a2a_agent_url=mock_a2a_agent.url,
|
|
229
|
+
proxy_name="proxy",
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
# Mock relay.send so we can verify it's called with the A2A response
|
|
233
|
+
bridge.relay.send = AsyncMock()
|
|
234
|
+
bridge.relay._connected = True
|
|
235
|
+
|
|
236
|
+
try:
|
|
237
|
+
relay_msg = Message(sender="alice", text="Please process this")
|
|
238
|
+
await bridge._handle_relay_message(relay_msg)
|
|
239
|
+
|
|
240
|
+
# Verify A2A agent received the message
|
|
241
|
+
assert len(mock_a2a_agent.received_messages) == 1
|
|
242
|
+
assert mock_a2a_agent.received_messages[0]["params"]["message"]["parts"][0]["text"] == "Please process this"
|
|
243
|
+
|
|
244
|
+
# Verify bridge forwarded response back via Relay
|
|
245
|
+
bridge.relay.send.assert_called_once_with("alice", "Hello from external A2A!")
|
|
246
|
+
finally:
|
|
247
|
+
if bridge._session and not bridge._session.closed:
|
|
248
|
+
await bridge._session.close()
|
|
249
|
+
|
|
250
|
+
async def test_handle_relay_message_no_response(self):
|
|
251
|
+
"""Test behavior when A2A agent returns no text in response."""
|
|
252
|
+
config = RelayConfig(workspace="test-ws", api_key="test-key", base_url="http://localhost:9999")
|
|
253
|
+
|
|
254
|
+
# Create a mock A2A agent that returns empty response (no parts)
|
|
255
|
+
empty_agent = MockA2AAgentEmpty()
|
|
256
|
+
await empty_agent.start()
|
|
257
|
+
bridge = None
|
|
258
|
+
try:
|
|
259
|
+
bridge = A2ABridge(
|
|
260
|
+
relay_config=config,
|
|
261
|
+
a2a_agent_url=empty_agent.url,
|
|
262
|
+
proxy_name="proxy",
|
|
263
|
+
)
|
|
264
|
+
bridge.relay.send = AsyncMock()
|
|
265
|
+
bridge.relay._connected = True
|
|
266
|
+
|
|
267
|
+
relay_msg = Message(sender="bob", text="Hello")
|
|
268
|
+
await bridge._handle_relay_message(relay_msg)
|
|
269
|
+
|
|
270
|
+
# Should not call relay.send when no response text
|
|
271
|
+
bridge.relay.send.assert_not_called()
|
|
272
|
+
finally:
|
|
273
|
+
await empty_agent.stop()
|
|
274
|
+
if bridge and bridge._session and not bridge._session.closed:
|
|
275
|
+
await bridge._session.close()
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class TestA2ABridgeContextManager:
|
|
279
|
+
async def test_context_manager(self):
|
|
280
|
+
config = RelayConfig(workspace="test-ws", api_key="test-key", base_url="http://localhost:9999")
|
|
281
|
+
bridge = A2ABridge(
|
|
282
|
+
relay_config=config,
|
|
283
|
+
a2a_agent_url="http://localhost:8000",
|
|
284
|
+
proxy_name="proxy",
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
# Mock the relay to avoid actual connections
|
|
288
|
+
bridge.relay.__aenter__ = AsyncMock(return_value=bridge.relay)
|
|
289
|
+
bridge.relay.__aexit__ = AsyncMock(return_value=None)
|
|
290
|
+
bridge.relay.on_message = MagicMock()
|
|
291
|
+
|
|
292
|
+
async with bridge:
|
|
293
|
+
assert bridge._started is True
|
|
294
|
+
|
|
295
|
+
assert bridge._started is False
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
# --- Integration: A2AServer <-> A2ABridge ---
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
class TestA2AServerBridgeIntegration:
|
|
302
|
+
async def test_bridge_sends_to_a2a_server(self):
|
|
303
|
+
"""A2ABridge can send messages to an A2AServer."""
|
|
304
|
+
# Set up an A2A server with an echo handler
|
|
305
|
+
server = A2AServer(agent_name="echo-server", port=0, host="127.0.0.1")
|
|
306
|
+
|
|
307
|
+
async def echo(msg: A2AMessage) -> A2AMessage:
|
|
308
|
+
text = msg.parts[0].text if msg.parts else ""
|
|
309
|
+
return A2AMessage(role="agent", parts=[A2APart(text=f"echo: {text}")])
|
|
310
|
+
|
|
311
|
+
server.on_message(echo)
|
|
312
|
+
await server.start()
|
|
313
|
+
|
|
314
|
+
try:
|
|
315
|
+
config = RelayConfig(workspace="test-ws", api_key="test-key", base_url="http://localhost:9999")
|
|
316
|
+
bridge = A2ABridge(
|
|
317
|
+
relay_config=config,
|
|
318
|
+
a2a_agent_url=server.url,
|
|
319
|
+
proxy_name="proxy",
|
|
320
|
+
)
|
|
321
|
+
try:
|
|
322
|
+
# Discover and send
|
|
323
|
+
card = await bridge.discover_agent()
|
|
324
|
+
assert card.name == "echo-server"
|
|
325
|
+
|
|
326
|
+
response = await bridge.send_a2a_message("Hello server!")
|
|
327
|
+
assert response == "echo: Hello server!"
|
|
328
|
+
finally:
|
|
329
|
+
if bridge._session and not bridge._session.closed:
|
|
330
|
+
await bridge._session.close()
|
|
331
|
+
finally:
|
|
332
|
+
await server.stop()
|
|
333
|
+
|
|
334
|
+
async def test_bridge_full_relay_forwarding_to_a2a_server(self):
|
|
335
|
+
"""Full integration: Relay message -> Bridge -> A2AServer -> response back."""
|
|
336
|
+
server = A2AServer(agent_name="responder", port=0, host="127.0.0.1")
|
|
337
|
+
|
|
338
|
+
async def respond(msg: A2AMessage) -> A2AMessage:
|
|
339
|
+
return A2AMessage(role="agent", parts=[A2APart(text="processed")])
|
|
340
|
+
|
|
341
|
+
server.on_message(respond)
|
|
342
|
+
await server.start()
|
|
343
|
+
|
|
344
|
+
try:
|
|
345
|
+
config = RelayConfig(workspace="test-ws", api_key="test-key", base_url="http://localhost:9999")
|
|
346
|
+
bridge = A2ABridge(
|
|
347
|
+
relay_config=config,
|
|
348
|
+
a2a_agent_url=server.url,
|
|
349
|
+
proxy_name="proxy",
|
|
350
|
+
)
|
|
351
|
+
bridge.relay.send = AsyncMock()
|
|
352
|
+
bridge.relay._connected = True
|
|
353
|
+
|
|
354
|
+
try:
|
|
355
|
+
relay_msg = Message(sender="user-1", text="do something")
|
|
356
|
+
await bridge._handle_relay_message(relay_msg)
|
|
357
|
+
|
|
358
|
+
bridge.relay.send.assert_called_once_with("user-1", "processed")
|
|
359
|
+
finally:
|
|
360
|
+
if bridge._session and not bridge._session.closed:
|
|
361
|
+
await bridge._session.close()
|
|
362
|
+
finally:
|
|
363
|
+
await server.stop()
|