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,561 @@
|
|
|
1
|
+
"""Tests for A2A transport implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
from aiohttp import web
|
|
11
|
+
from aiohttp.test_utils import AioHTTPTestCase, TestServer, TestClient
|
|
12
|
+
|
|
13
|
+
from agent_relay.communicate.a2a_types import (
|
|
14
|
+
A2AAgentCard,
|
|
15
|
+
A2AConfig,
|
|
16
|
+
A2AMessage,
|
|
17
|
+
A2APart,
|
|
18
|
+
A2ASkill,
|
|
19
|
+
A2ATask,
|
|
20
|
+
A2ATaskStatus,
|
|
21
|
+
make_jsonrpc_request,
|
|
22
|
+
make_jsonrpc_response,
|
|
23
|
+
)
|
|
24
|
+
from agent_relay.communicate.a2a_transport import A2AError, A2ATransport
|
|
25
|
+
from agent_relay.communicate.types import Message
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# === Fixtures ===
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _make_agent_card(name: str = "remote-agent", url: str = "http://localhost:9999") -> dict:
|
|
32
|
+
return A2AAgentCard(
|
|
33
|
+
name=name,
|
|
34
|
+
description=f"Test agent: {name}",
|
|
35
|
+
url=url,
|
|
36
|
+
skills=[A2ASkill(id="s1", name="Echo", description="Echo messages")],
|
|
37
|
+
).to_dict()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _make_remote_a2a_app() -> web.Application:
|
|
41
|
+
"""Create a mock A2A remote agent app for testing client-side calls."""
|
|
42
|
+
app = web.Application()
|
|
43
|
+
# agent_card will be patched after we know the port
|
|
44
|
+
app["agent_card"] = None
|
|
45
|
+
|
|
46
|
+
async def handle_agent_card(request: web.Request) -> web.Response:
|
|
47
|
+
return web.json_response(request.app["agent_card"])
|
|
48
|
+
|
|
49
|
+
async def handle_jsonrpc(request: web.Request) -> web.Response:
|
|
50
|
+
body = await request.json()
|
|
51
|
+
method = body.get("method", "")
|
|
52
|
+
rpc_id = body.get("id")
|
|
53
|
+
params = body.get("params", {})
|
|
54
|
+
|
|
55
|
+
if method == "message/send":
|
|
56
|
+
msg = params.get("message", {})
|
|
57
|
+
text = " ".join(
|
|
58
|
+
p.get("text", "") for p in msg.get("parts", []) if p.get("text")
|
|
59
|
+
)
|
|
60
|
+
task = A2ATask(
|
|
61
|
+
id="task-001",
|
|
62
|
+
contextId="ctx-001",
|
|
63
|
+
status=A2ATaskStatus(state="completed"),
|
|
64
|
+
messages=[
|
|
65
|
+
A2AMessage.from_dict(msg),
|
|
66
|
+
A2AMessage(
|
|
67
|
+
role="agent",
|
|
68
|
+
parts=[A2APart(text=f"Echo: {text}")],
|
|
69
|
+
messageId="resp-1",
|
|
70
|
+
),
|
|
71
|
+
],
|
|
72
|
+
)
|
|
73
|
+
return web.json_response(make_jsonrpc_response(task.to_dict(), rpc_id))
|
|
74
|
+
|
|
75
|
+
return web.json_response({
|
|
76
|
+
"jsonrpc": "2.0",
|
|
77
|
+
"error": {"code": -32601, "message": f"Method not found: {method}"},
|
|
78
|
+
"id": rpc_id,
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
app.router.add_get("/.well-known/agent.json", handle_agent_card)
|
|
82
|
+
app.router.add_post("/", handle_jsonrpc)
|
|
83
|
+
return app
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@pytest.fixture
|
|
87
|
+
async def remote_agent():
|
|
88
|
+
"""Start a mock remote A2A agent server."""
|
|
89
|
+
app = _make_remote_a2a_app()
|
|
90
|
+
runner = web.AppRunner(app)
|
|
91
|
+
await runner.setup()
|
|
92
|
+
site = web.TCPSite(runner, "127.0.0.1", 0)
|
|
93
|
+
await site.start()
|
|
94
|
+
|
|
95
|
+
# Get the actual port and set the agent card with correct URL
|
|
96
|
+
sockets = site._server.sockets
|
|
97
|
+
port = sockets[0].getsockname()[1]
|
|
98
|
+
base_url = f"http://127.0.0.1:{port}"
|
|
99
|
+
app["agent_card"] = _make_agent_card(url=base_url)
|
|
100
|
+
|
|
101
|
+
yield base_url
|
|
102
|
+
|
|
103
|
+
await site.stop()
|
|
104
|
+
await runner.cleanup()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@pytest.fixture
|
|
108
|
+
def a2a_config():
|
|
109
|
+
"""Create a basic A2A config for testing."""
|
|
110
|
+
return A2AConfig(
|
|
111
|
+
server_port=0, # Use 0 for auto-assign
|
|
112
|
+
server_host="127.0.0.1",
|
|
113
|
+
agent_description="Test agent",
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# === A2ATransport registration tests ===
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class TestA2ATransportRegister:
|
|
121
|
+
@pytest.mark.asyncio
|
|
122
|
+
async def test_register_starts_server(self):
|
|
123
|
+
config = A2AConfig(server_port=0, server_host="127.0.0.1")
|
|
124
|
+
transport = A2ATransport(config)
|
|
125
|
+
try:
|
|
126
|
+
result = await transport.register("test-agent")
|
|
127
|
+
assert result["name"] == "test-agent"
|
|
128
|
+
assert result["type"] == "a2a"
|
|
129
|
+
assert transport.agent_name == "test-agent"
|
|
130
|
+
assert transport.agent_card is not None
|
|
131
|
+
assert transport.agent_card.name == "test-agent"
|
|
132
|
+
finally:
|
|
133
|
+
await transport.unregister()
|
|
134
|
+
|
|
135
|
+
@pytest.mark.asyncio
|
|
136
|
+
async def test_unregister_stops_server(self):
|
|
137
|
+
config = A2AConfig(server_port=0, server_host="127.0.0.1")
|
|
138
|
+
transport = A2ATransport(config)
|
|
139
|
+
await transport.register("test-agent")
|
|
140
|
+
await transport.unregister()
|
|
141
|
+
assert transport._site is None
|
|
142
|
+
assert transport._runner is None
|
|
143
|
+
assert transport._app is None
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# === A2ATransport server-side tests (incoming JSON-RPC) ===
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class TestA2ATransportServer:
|
|
150
|
+
@pytest.mark.asyncio
|
|
151
|
+
async def test_agent_card_endpoint(self):
|
|
152
|
+
config = A2AConfig(server_port=0, server_host="127.0.0.1")
|
|
153
|
+
transport = A2ATransport(config)
|
|
154
|
+
try:
|
|
155
|
+
await transport.register("card-test-agent")
|
|
156
|
+
|
|
157
|
+
# Get the server port
|
|
158
|
+
sockets = transport._site._server.sockets
|
|
159
|
+
port = sockets[0].getsockname()[1]
|
|
160
|
+
|
|
161
|
+
import aiohttp
|
|
162
|
+
async with aiohttp.ClientSession() as session:
|
|
163
|
+
async with session.get(f"http://127.0.0.1:{port}/.well-known/agent.json") as resp:
|
|
164
|
+
assert resp.status == 200
|
|
165
|
+
data = await resp.json()
|
|
166
|
+
assert data["name"] == "card-test-agent"
|
|
167
|
+
assert data["version"] == "1.0.0"
|
|
168
|
+
finally:
|
|
169
|
+
await transport.unregister()
|
|
170
|
+
|
|
171
|
+
@pytest.mark.asyncio
|
|
172
|
+
async def test_message_send_jsonrpc(self):
|
|
173
|
+
config = A2AConfig(server_port=0, server_host="127.0.0.1")
|
|
174
|
+
transport = A2ATransport(config)
|
|
175
|
+
received: list[Message] = []
|
|
176
|
+
transport.on_message(lambda msg: received.append(msg))
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
await transport.register("rpc-test-agent")
|
|
180
|
+
|
|
181
|
+
sockets = transport._site._server.sockets
|
|
182
|
+
port = sockets[0].getsockname()[1]
|
|
183
|
+
|
|
184
|
+
rpc_request = make_jsonrpc_request(
|
|
185
|
+
"message/send",
|
|
186
|
+
{
|
|
187
|
+
"message": {
|
|
188
|
+
"role": "user",
|
|
189
|
+
"parts": [{"text": "hello from test"}],
|
|
190
|
+
"messageId": "msg-test-1",
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
id="req-1",
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
import aiohttp
|
|
197
|
+
async with aiohttp.ClientSession() as session:
|
|
198
|
+
async with session.post(
|
|
199
|
+
f"http://127.0.0.1:{port}/",
|
|
200
|
+
json=rpc_request,
|
|
201
|
+
) as resp:
|
|
202
|
+
assert resp.status == 200
|
|
203
|
+
data = await resp.json()
|
|
204
|
+
assert data["jsonrpc"] == "2.0"
|
|
205
|
+
assert data["id"] == "req-1"
|
|
206
|
+
result = data["result"]
|
|
207
|
+
assert result["status"]["state"] == "completed"
|
|
208
|
+
assert len(result["messages"]) == 1
|
|
209
|
+
|
|
210
|
+
# Verify callback was invoked
|
|
211
|
+
assert len(received) == 1
|
|
212
|
+
assert received[0].text == "hello from test"
|
|
213
|
+
finally:
|
|
214
|
+
await transport.unregister()
|
|
215
|
+
|
|
216
|
+
@pytest.mark.asyncio
|
|
217
|
+
async def test_async_message_callback(self):
|
|
218
|
+
config = A2AConfig(server_port=0, server_host="127.0.0.1")
|
|
219
|
+
transport = A2ATransport(config)
|
|
220
|
+
received: list[Message] = []
|
|
221
|
+
|
|
222
|
+
async def async_cb(msg: Message) -> None:
|
|
223
|
+
received.append(msg)
|
|
224
|
+
|
|
225
|
+
transport.on_message(async_cb)
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
await transport.register("async-cb-agent")
|
|
229
|
+
sockets = transport._site._server.sockets
|
|
230
|
+
port = sockets[0].getsockname()[1]
|
|
231
|
+
|
|
232
|
+
rpc_request = make_jsonrpc_request(
|
|
233
|
+
"message/send",
|
|
234
|
+
{"message": {"role": "user", "parts": [{"text": "async test"}], "messageId": "m1"}},
|
|
235
|
+
id="r1",
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
import aiohttp
|
|
239
|
+
async with aiohttp.ClientSession() as session:
|
|
240
|
+
async with session.post(f"http://127.0.0.1:{port}/", json=rpc_request) as resp:
|
|
241
|
+
assert resp.status == 200
|
|
242
|
+
|
|
243
|
+
assert len(received) == 1
|
|
244
|
+
assert received[0].text == "async test"
|
|
245
|
+
finally:
|
|
246
|
+
await transport.unregister()
|
|
247
|
+
|
|
248
|
+
@pytest.mark.asyncio
|
|
249
|
+
async def test_tasks_get(self):
|
|
250
|
+
config = A2AConfig(server_port=0, server_host="127.0.0.1")
|
|
251
|
+
transport = A2ATransport(config)
|
|
252
|
+
|
|
253
|
+
try:
|
|
254
|
+
await transport.register("tasks-get-agent")
|
|
255
|
+
sockets = transport._site._server.sockets
|
|
256
|
+
port = sockets[0].getsockname()[1]
|
|
257
|
+
|
|
258
|
+
import aiohttp
|
|
259
|
+
async with aiohttp.ClientSession() as session:
|
|
260
|
+
# First send a message to create a task
|
|
261
|
+
send_req = make_jsonrpc_request(
|
|
262
|
+
"message/send",
|
|
263
|
+
{"message": {"role": "user", "parts": [{"text": "create task"}], "messageId": "m1"}},
|
|
264
|
+
id="r1",
|
|
265
|
+
)
|
|
266
|
+
async with session.post(f"http://127.0.0.1:{port}/", json=send_req) as resp:
|
|
267
|
+
send_data = await resp.json()
|
|
268
|
+
task_id = send_data["result"]["id"]
|
|
269
|
+
|
|
270
|
+
# Then get the task
|
|
271
|
+
get_req = make_jsonrpc_request("tasks/get", {"id": task_id}, id="r2")
|
|
272
|
+
async with session.post(f"http://127.0.0.1:{port}/", json=get_req) as resp:
|
|
273
|
+
data = await resp.json()
|
|
274
|
+
assert data["result"]["id"] == task_id
|
|
275
|
+
assert data["result"]["status"]["state"] == "completed"
|
|
276
|
+
finally:
|
|
277
|
+
await transport.unregister()
|
|
278
|
+
|
|
279
|
+
@pytest.mark.asyncio
|
|
280
|
+
async def test_tasks_get_not_found(self):
|
|
281
|
+
config = A2AConfig(server_port=0, server_host="127.0.0.1")
|
|
282
|
+
transport = A2ATransport(config)
|
|
283
|
+
|
|
284
|
+
try:
|
|
285
|
+
await transport.register("tasks-404-agent")
|
|
286
|
+
sockets = transport._site._server.sockets
|
|
287
|
+
port = sockets[0].getsockname()[1]
|
|
288
|
+
|
|
289
|
+
import aiohttp
|
|
290
|
+
async with aiohttp.ClientSession() as session:
|
|
291
|
+
req = make_jsonrpc_request("tasks/get", {"id": "nonexistent"}, id="r1")
|
|
292
|
+
async with session.post(f"http://127.0.0.1:{port}/", json=req) as resp:
|
|
293
|
+
data = await resp.json()
|
|
294
|
+
assert "error" in data
|
|
295
|
+
assert data["error"]["code"] == -32001
|
|
296
|
+
|
|
297
|
+
finally:
|
|
298
|
+
await transport.unregister()
|
|
299
|
+
|
|
300
|
+
@pytest.mark.asyncio
|
|
301
|
+
async def test_tasks_cancel(self):
|
|
302
|
+
config = A2AConfig(server_port=0, server_host="127.0.0.1")
|
|
303
|
+
transport = A2ATransport(config)
|
|
304
|
+
|
|
305
|
+
try:
|
|
306
|
+
await transport.register("cancel-agent")
|
|
307
|
+
|
|
308
|
+
# Manually create a task in working state
|
|
309
|
+
task = A2ATask(
|
|
310
|
+
id="cancel-task-1",
|
|
311
|
+
status=A2ATaskStatus(state="working"),
|
|
312
|
+
)
|
|
313
|
+
transport.tasks["cancel-task-1"] = task
|
|
314
|
+
|
|
315
|
+
sockets = transport._site._server.sockets
|
|
316
|
+
port = sockets[0].getsockname()[1]
|
|
317
|
+
|
|
318
|
+
import aiohttp
|
|
319
|
+
async with aiohttp.ClientSession() as session:
|
|
320
|
+
req = make_jsonrpc_request("tasks/cancel", {"id": "cancel-task-1"}, id="r1")
|
|
321
|
+
async with session.post(f"http://127.0.0.1:{port}/", json=req) as resp:
|
|
322
|
+
data = await resp.json()
|
|
323
|
+
assert data["result"]["status"]["state"] == "canceled"
|
|
324
|
+
finally:
|
|
325
|
+
await transport.unregister()
|
|
326
|
+
|
|
327
|
+
@pytest.mark.asyncio
|
|
328
|
+
async def test_method_not_found(self):
|
|
329
|
+
config = A2AConfig(server_port=0, server_host="127.0.0.1")
|
|
330
|
+
transport = A2ATransport(config)
|
|
331
|
+
|
|
332
|
+
try:
|
|
333
|
+
await transport.register("method-404-agent")
|
|
334
|
+
sockets = transport._site._server.sockets
|
|
335
|
+
port = sockets[0].getsockname()[1]
|
|
336
|
+
|
|
337
|
+
import aiohttp
|
|
338
|
+
async with aiohttp.ClientSession() as session:
|
|
339
|
+
req = make_jsonrpc_request("unknown/method", {}, id="r1")
|
|
340
|
+
async with session.post(f"http://127.0.0.1:{port}/", json=req) as resp:
|
|
341
|
+
data = await resp.json()
|
|
342
|
+
assert "error" in data
|
|
343
|
+
assert data["error"]["code"] == -32601
|
|
344
|
+
finally:
|
|
345
|
+
await transport.unregister()
|
|
346
|
+
|
|
347
|
+
@pytest.mark.asyncio
|
|
348
|
+
async def test_parse_error(self):
|
|
349
|
+
config = A2AConfig(server_port=0, server_host="127.0.0.1")
|
|
350
|
+
transport = A2ATransport(config)
|
|
351
|
+
|
|
352
|
+
try:
|
|
353
|
+
await transport.register("parse-err-agent")
|
|
354
|
+
sockets = transport._site._server.sockets
|
|
355
|
+
port = sockets[0].getsockname()[1]
|
|
356
|
+
|
|
357
|
+
import aiohttp
|
|
358
|
+
async with aiohttp.ClientSession() as session:
|
|
359
|
+
async with session.post(
|
|
360
|
+
f"http://127.0.0.1:{port}/",
|
|
361
|
+
data="not valid json",
|
|
362
|
+
headers={"Content-Type": "application/json"},
|
|
363
|
+
) as resp:
|
|
364
|
+
data = await resp.json()
|
|
365
|
+
assert "error" in data
|
|
366
|
+
assert data["error"]["code"] == -32700
|
|
367
|
+
finally:
|
|
368
|
+
await transport.unregister()
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
# === A2ATransport client-side tests ===
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
class TestA2ATransportClient:
|
|
375
|
+
@pytest.mark.asyncio
|
|
376
|
+
async def test_discover_agent(self, remote_agent):
|
|
377
|
+
config = A2AConfig()
|
|
378
|
+
transport = A2ATransport(config)
|
|
379
|
+
try:
|
|
380
|
+
card = await transport._discover_agent(remote_agent)
|
|
381
|
+
assert card.name == "remote-agent"
|
|
382
|
+
assert len(card.skills) == 1
|
|
383
|
+
assert card.skills[0].name == "Echo"
|
|
384
|
+
finally:
|
|
385
|
+
await transport._close_session()
|
|
386
|
+
|
|
387
|
+
@pytest.mark.asyncio
|
|
388
|
+
async def test_discover_agent_caches(self, remote_agent):
|
|
389
|
+
config = A2AConfig()
|
|
390
|
+
transport = A2ATransport(config)
|
|
391
|
+
try:
|
|
392
|
+
card1 = await transport._discover_agent(remote_agent)
|
|
393
|
+
card2 = await transport._discover_agent(remote_agent)
|
|
394
|
+
assert card1 is card2 # Same object from cache
|
|
395
|
+
finally:
|
|
396
|
+
await transport._close_session()
|
|
397
|
+
|
|
398
|
+
@pytest.mark.asyncio
|
|
399
|
+
async def test_send_dm(self, remote_agent):
|
|
400
|
+
config = A2AConfig()
|
|
401
|
+
transport = A2ATransport(config)
|
|
402
|
+
try:
|
|
403
|
+
result = await transport.send_dm(remote_agent, "test message")
|
|
404
|
+
assert result["text"] == "Echo: test message"
|
|
405
|
+
assert result["task_id"] == "task-001"
|
|
406
|
+
assert result["status"] == "completed"
|
|
407
|
+
finally:
|
|
408
|
+
await transport._close_session()
|
|
409
|
+
|
|
410
|
+
@pytest.mark.asyncio
|
|
411
|
+
async def test_list_agents(self, remote_agent):
|
|
412
|
+
config = A2AConfig(registry=[remote_agent])
|
|
413
|
+
transport = A2ATransport(config)
|
|
414
|
+
try:
|
|
415
|
+
agents = await transport.list_agents()
|
|
416
|
+
assert len(agents) == 1
|
|
417
|
+
assert agents[0]["name"] == "remote-agent"
|
|
418
|
+
assert agents[0]["description"] == "Test agent: remote-agent"
|
|
419
|
+
finally:
|
|
420
|
+
await transport._close_session()
|
|
421
|
+
|
|
422
|
+
@pytest.mark.asyncio
|
|
423
|
+
async def test_list_agents_skips_unreachable(self):
|
|
424
|
+
config = A2AConfig(registry=["http://127.0.0.1:1"])
|
|
425
|
+
transport = A2ATransport(config)
|
|
426
|
+
try:
|
|
427
|
+
agents = await transport.list_agents()
|
|
428
|
+
assert agents == []
|
|
429
|
+
finally:
|
|
430
|
+
await transport._close_session()
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
# === Message conversion tests ===
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
class TestMessageConversion:
|
|
437
|
+
def test_relay_msg_to_a2a(self):
|
|
438
|
+
a2a_msg = A2ATransport._relay_msg_to_a2a("hello world", "sender-1")
|
|
439
|
+
assert a2a_msg.role == "user"
|
|
440
|
+
assert len(a2a_msg.parts) == 1
|
|
441
|
+
assert a2a_msg.parts[0].text == "hello world"
|
|
442
|
+
|
|
443
|
+
def test_a2a_to_relay_msg(self):
|
|
444
|
+
a2a_msg = A2AMessage(
|
|
445
|
+
role="agent",
|
|
446
|
+
parts=[A2APart(text="response text")],
|
|
447
|
+
messageId="msg-1",
|
|
448
|
+
contextId="ctx-1",
|
|
449
|
+
)
|
|
450
|
+
relay_msg = A2ATransport._a2a_to_relay_msg(a2a_msg, sender="remote-agent")
|
|
451
|
+
assert relay_msg.sender == "remote-agent"
|
|
452
|
+
assert relay_msg.text == "response text"
|
|
453
|
+
assert relay_msg.thread_id == "ctx-1"
|
|
454
|
+
assert relay_msg.message_id == "msg-1"
|
|
455
|
+
|
|
456
|
+
def test_a2a_to_relay_msg_multi_part(self):
|
|
457
|
+
a2a_msg = A2AMessage(
|
|
458
|
+
role="agent",
|
|
459
|
+
parts=[A2APart(text="hello"), A2APart(text="world")],
|
|
460
|
+
messageId="msg-2",
|
|
461
|
+
)
|
|
462
|
+
relay_msg = A2ATransport._a2a_to_relay_msg(a2a_msg, sender="agent")
|
|
463
|
+
assert relay_msg.text == "hello world"
|
|
464
|
+
|
|
465
|
+
def test_roundtrip_conversion(self):
|
|
466
|
+
original_text = "roundtrip test message"
|
|
467
|
+
a2a_msg = A2ATransport._relay_msg_to_a2a(original_text, "sender")
|
|
468
|
+
relay_msg = A2ATransport._a2a_to_relay_msg(a2a_msg, sender="sender")
|
|
469
|
+
assert relay_msg.text == original_text
|
|
470
|
+
|
|
471
|
+
def test_a2a_result_to_relay(self):
|
|
472
|
+
result = {
|
|
473
|
+
"id": "task-1",
|
|
474
|
+
"status": {"state": "completed"},
|
|
475
|
+
"messages": [
|
|
476
|
+
{"role": "user", "parts": [{"text": "input"}]},
|
|
477
|
+
{"role": "agent", "parts": [{"text": "output"}]},
|
|
478
|
+
],
|
|
479
|
+
}
|
|
480
|
+
relay_result = A2ATransport._a2a_result_to_relay(result, "remote")
|
|
481
|
+
assert relay_result["sender"] == "remote"
|
|
482
|
+
assert relay_result["text"] == "output"
|
|
483
|
+
assert relay_result["task_id"] == "task-1"
|
|
484
|
+
assert relay_result["status"] == "completed"
|
|
485
|
+
|
|
486
|
+
def test_a2a_result_to_relay_empty_messages(self):
|
|
487
|
+
result = {"id": "task-1", "status": {"state": "completed"}, "messages": []}
|
|
488
|
+
relay_result = A2ATransport._a2a_result_to_relay(result, "remote")
|
|
489
|
+
assert relay_result["text"] == ""
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
# === connect_ws is a no-op ===
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
class TestConnectWs:
|
|
496
|
+
@pytest.mark.asyncio
|
|
497
|
+
async def test_connect_ws_is_noop(self):
|
|
498
|
+
config = A2AConfig()
|
|
499
|
+
transport = A2ATransport(config)
|
|
500
|
+
await transport.connect_ws() # Should not raise
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
# === on_message ===
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
class TestOnMessage:
|
|
507
|
+
def test_registers_callback(self):
|
|
508
|
+
config = A2AConfig()
|
|
509
|
+
transport = A2ATransport(config)
|
|
510
|
+
cb = lambda msg: None
|
|
511
|
+
transport.on_message(cb)
|
|
512
|
+
assert cb in transport._message_callbacks
|
|
513
|
+
|
|
514
|
+
def test_multiple_callbacks(self):
|
|
515
|
+
config = A2AConfig()
|
|
516
|
+
transport = A2ATransport(config)
|
|
517
|
+
cb1 = lambda msg: None
|
|
518
|
+
cb2 = lambda msg: None
|
|
519
|
+
transport.on_message(cb1)
|
|
520
|
+
transport.on_message(cb2)
|
|
521
|
+
assert len(transport._message_callbacks) == 2
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
# === Auth headers ===
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
class TestAuthHeaders:
|
|
528
|
+
def test_no_auth(self):
|
|
529
|
+
config = A2AConfig()
|
|
530
|
+
transport = A2ATransport(config)
|
|
531
|
+
headers = transport._auth_headers()
|
|
532
|
+
assert headers == {}
|
|
533
|
+
|
|
534
|
+
def test_bearer_auth(self):
|
|
535
|
+
config = A2AConfig(auth_scheme="bearer", auth_token="my-token")
|
|
536
|
+
transport = A2ATransport(config)
|
|
537
|
+
headers = transport._auth_headers()
|
|
538
|
+
assert headers["Authorization"] == "Bearer my-token"
|
|
539
|
+
|
|
540
|
+
def test_api_key_auth(self):
|
|
541
|
+
config = A2AConfig(auth_scheme="api_key", auth_token="key-123")
|
|
542
|
+
transport = A2ATransport(config)
|
|
543
|
+
headers = transport._auth_headers()
|
|
544
|
+
assert headers["X-API-Key"] == "key-123"
|
|
545
|
+
|
|
546
|
+
def test_default_auth_scheme(self):
|
|
547
|
+
config = A2AConfig(auth_token="fallback-token")
|
|
548
|
+
transport = A2ATransport(config)
|
|
549
|
+
headers = transport._auth_headers()
|
|
550
|
+
assert headers["Authorization"] == "Bearer fallback-token"
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
# === A2AError ===
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
class TestA2AError:
|
|
557
|
+
def test_error_attributes(self):
|
|
558
|
+
err = A2AError(-32001, "Task not found")
|
|
559
|
+
assert err.code == -32001
|
|
560
|
+
assert err.message == "Task not found"
|
|
561
|
+
assert "A2A error -32001" in str(err)
|