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,294 @@
|
|
|
1
|
+
"""A2A (Agent-to-Agent) protocol data model types."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
from dataclasses import asdict, dataclass, field
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class A2APart:
|
|
13
|
+
"""A single part of an A2A message (text, file, or structured data)."""
|
|
14
|
+
|
|
15
|
+
text: str | None = None
|
|
16
|
+
file: dict[str, Any] | None = None # FileContent — phase 2
|
|
17
|
+
data: dict[str, Any] | None = None # Structured data — phase 2
|
|
18
|
+
|
|
19
|
+
def to_dict(self) -> dict[str, Any]:
|
|
20
|
+
d: dict[str, Any] = {}
|
|
21
|
+
if self.text is not None:
|
|
22
|
+
d["text"] = self.text
|
|
23
|
+
if self.file is not None:
|
|
24
|
+
d["file"] = self.file
|
|
25
|
+
if self.data is not None:
|
|
26
|
+
d["data"] = self.data
|
|
27
|
+
return d
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def from_dict(cls, d: dict[str, Any]) -> A2APart:
|
|
31
|
+
return cls(
|
|
32
|
+
text=d.get("text"),
|
|
33
|
+
file=d.get("file"),
|
|
34
|
+
data=d.get("data"),
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class A2AMessage:
|
|
40
|
+
"""An A2A protocol message."""
|
|
41
|
+
|
|
42
|
+
role: str # "user" | "agent"
|
|
43
|
+
parts: list[A2APart]
|
|
44
|
+
messageId: str | None = None
|
|
45
|
+
contextId: str | None = None
|
|
46
|
+
taskId: str | None = None
|
|
47
|
+
|
|
48
|
+
def __post_init__(self) -> None:
|
|
49
|
+
if self.messageId is None:
|
|
50
|
+
self.messageId = str(uuid.uuid4())
|
|
51
|
+
|
|
52
|
+
def to_dict(self) -> dict[str, Any]:
|
|
53
|
+
d: dict[str, Any] = {
|
|
54
|
+
"role": self.role,
|
|
55
|
+
"parts": [p.to_dict() for p in self.parts],
|
|
56
|
+
}
|
|
57
|
+
if self.messageId is not None:
|
|
58
|
+
d["messageId"] = self.messageId
|
|
59
|
+
if self.contextId is not None:
|
|
60
|
+
d["contextId"] = self.contextId
|
|
61
|
+
if self.taskId is not None:
|
|
62
|
+
d["taskId"] = self.taskId
|
|
63
|
+
return d
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def from_dict(cls, d: dict[str, Any]) -> A2AMessage:
|
|
67
|
+
parts = [A2APart.from_dict(p) for p in d.get("parts", [])]
|
|
68
|
+
return cls(
|
|
69
|
+
role=d["role"],
|
|
70
|
+
parts=parts,
|
|
71
|
+
messageId=d.get("messageId"),
|
|
72
|
+
contextId=d.get("contextId"),
|
|
73
|
+
taskId=d.get("taskId"),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def get_text(self) -> str:
|
|
77
|
+
"""Extract concatenated text from all text parts."""
|
|
78
|
+
return " ".join(p.text for p in self.parts if p.text)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass
|
|
82
|
+
class A2ATaskStatus:
|
|
83
|
+
"""Status of an A2A task."""
|
|
84
|
+
|
|
85
|
+
state: str # "submitted" | "working" | "completed" | "failed" | "canceled"
|
|
86
|
+
message: A2AMessage | None = None
|
|
87
|
+
timestamp: str | None = None
|
|
88
|
+
|
|
89
|
+
def __post_init__(self) -> None:
|
|
90
|
+
if self.timestamp is None:
|
|
91
|
+
self.timestamp = datetime.now(timezone.utc).isoformat()
|
|
92
|
+
|
|
93
|
+
def to_dict(self) -> dict[str, Any]:
|
|
94
|
+
d: dict[str, Any] = {"state": self.state}
|
|
95
|
+
if self.message is not None:
|
|
96
|
+
d["message"] = self.message.to_dict()
|
|
97
|
+
if self.timestamp is not None:
|
|
98
|
+
d["timestamp"] = self.timestamp
|
|
99
|
+
return d
|
|
100
|
+
|
|
101
|
+
@classmethod
|
|
102
|
+
def from_dict(cls, d: dict[str, Any]) -> A2ATaskStatus:
|
|
103
|
+
msg = None
|
|
104
|
+
if "message" in d and d["message"] is not None:
|
|
105
|
+
msg = A2AMessage.from_dict(d["message"])
|
|
106
|
+
return cls(
|
|
107
|
+
state=d["state"],
|
|
108
|
+
message=msg,
|
|
109
|
+
timestamp=d.get("timestamp"),
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
VALID_TASK_STATES = {"submitted", "working", "completed", "failed", "canceled"}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass
|
|
117
|
+
class A2ATask:
|
|
118
|
+
"""An A2A protocol task."""
|
|
119
|
+
|
|
120
|
+
id: str
|
|
121
|
+
contextId: str | None = None
|
|
122
|
+
status: A2ATaskStatus = field(
|
|
123
|
+
default_factory=lambda: A2ATaskStatus(state="submitted")
|
|
124
|
+
)
|
|
125
|
+
messages: list[A2AMessage] = field(default_factory=list)
|
|
126
|
+
artifacts: list[dict[str, Any]] = field(default_factory=list)
|
|
127
|
+
|
|
128
|
+
def to_dict(self) -> dict[str, Any]:
|
|
129
|
+
return {
|
|
130
|
+
"id": self.id,
|
|
131
|
+
"contextId": self.contextId,
|
|
132
|
+
"status": self.status.to_dict(),
|
|
133
|
+
"messages": [m.to_dict() for m in self.messages],
|
|
134
|
+
"artifacts": self.artifacts,
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
@classmethod
|
|
138
|
+
def from_dict(cls, d: dict[str, Any]) -> A2ATask:
|
|
139
|
+
status = A2ATaskStatus.from_dict(d["status"]) if "status" in d else A2ATaskStatus(state="submitted")
|
|
140
|
+
messages = [A2AMessage.from_dict(m) for m in d.get("messages", [])]
|
|
141
|
+
return cls(
|
|
142
|
+
id=d["id"],
|
|
143
|
+
contextId=d.get("contextId"),
|
|
144
|
+
status=status,
|
|
145
|
+
messages=messages,
|
|
146
|
+
artifacts=d.get("artifacts", []),
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@dataclass
|
|
151
|
+
class A2ASkill:
|
|
152
|
+
"""A skill advertised by an A2A agent."""
|
|
153
|
+
|
|
154
|
+
id: str
|
|
155
|
+
name: str
|
|
156
|
+
description: str
|
|
157
|
+
|
|
158
|
+
def to_dict(self) -> dict[str, Any]:
|
|
159
|
+
return {"id": self.id, "name": self.name, "description": self.description}
|
|
160
|
+
|
|
161
|
+
@classmethod
|
|
162
|
+
def from_dict(cls, d: dict[str, Any]) -> A2ASkill:
|
|
163
|
+
return cls(id=d["id"], name=d["name"], description=d["description"])
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@dataclass
|
|
167
|
+
class A2AAgentCard:
|
|
168
|
+
"""An A2A Agent Card describing an agent's capabilities and endpoint."""
|
|
169
|
+
|
|
170
|
+
name: str
|
|
171
|
+
description: str
|
|
172
|
+
url: str
|
|
173
|
+
version: str = "1.0.0"
|
|
174
|
+
capabilities: dict[str, Any] = field(
|
|
175
|
+
default_factory=lambda: {"streaming": True, "pushNotifications": False}
|
|
176
|
+
)
|
|
177
|
+
skills: list[A2ASkill] = field(default_factory=list)
|
|
178
|
+
defaultInputModes: list[str] = field(default_factory=lambda: ["text"])
|
|
179
|
+
defaultOutputModes: list[str] = field(default_factory=lambda: ["text"])
|
|
180
|
+
|
|
181
|
+
def to_dict(self) -> dict[str, Any]:
|
|
182
|
+
return {
|
|
183
|
+
"name": self.name,
|
|
184
|
+
"description": self.description,
|
|
185
|
+
"url": self.url,
|
|
186
|
+
"version": self.version,
|
|
187
|
+
"capabilities": self.capabilities,
|
|
188
|
+
"skills": [s.to_dict() for s in self.skills],
|
|
189
|
+
"defaultInputModes": self.defaultInputModes,
|
|
190
|
+
"defaultOutputModes": self.defaultOutputModes,
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
@classmethod
|
|
194
|
+
def from_dict(cls, d: dict[str, Any]) -> A2AAgentCard:
|
|
195
|
+
skills = [A2ASkill.from_dict(s) for s in d.get("skills", [])]
|
|
196
|
+
return cls(
|
|
197
|
+
name=d["name"],
|
|
198
|
+
description=d["description"],
|
|
199
|
+
url=d["url"],
|
|
200
|
+
version=d.get("version", "1.0.0"),
|
|
201
|
+
capabilities=d.get("capabilities", {"streaming": True, "pushNotifications": False}),
|
|
202
|
+
skills=skills,
|
|
203
|
+
defaultInputModes=d.get("defaultInputModes", ["text"]),
|
|
204
|
+
defaultOutputModes=d.get("defaultOutputModes", ["text"]),
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@dataclass
|
|
209
|
+
class A2AConfig:
|
|
210
|
+
"""Configuration for A2A transport."""
|
|
211
|
+
|
|
212
|
+
# Server mode
|
|
213
|
+
server_port: int = 5000
|
|
214
|
+
server_host: str = "0.0.0.0"
|
|
215
|
+
|
|
216
|
+
# Client mode
|
|
217
|
+
target_url: str | None = None
|
|
218
|
+
|
|
219
|
+
# Agent Card registry (known A2A agent URLs)
|
|
220
|
+
registry: list[str] = field(default_factory=list)
|
|
221
|
+
|
|
222
|
+
# Auth
|
|
223
|
+
auth_scheme: str | None = None # "bearer", "api_key", etc.
|
|
224
|
+
auth_token: str | None = None
|
|
225
|
+
|
|
226
|
+
# Agent metadata
|
|
227
|
+
agent_description: str | None = None
|
|
228
|
+
skills: list[A2ASkill] = field(default_factory=list)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def make_jsonrpc_request(method: str, params: dict[str, Any], id: str | int | None = None) -> dict[str, Any]:
|
|
232
|
+
"""Build a JSON-RPC 2.0 request envelope."""
|
|
233
|
+
req: dict[str, Any] = {
|
|
234
|
+
"jsonrpc": "2.0",
|
|
235
|
+
"method": method,
|
|
236
|
+
"params": params,
|
|
237
|
+
}
|
|
238
|
+
if id is not None:
|
|
239
|
+
req["id"] = id
|
|
240
|
+
else:
|
|
241
|
+
req["id"] = str(uuid.uuid4())
|
|
242
|
+
return req
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def make_jsonrpc_response(result: Any, id: str | int) -> dict[str, Any]:
|
|
246
|
+
"""Build a JSON-RPC 2.0 success response."""
|
|
247
|
+
return {
|
|
248
|
+
"jsonrpc": "2.0",
|
|
249
|
+
"result": result,
|
|
250
|
+
"id": id,
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def make_jsonrpc_error(code: int, message: str, id: str | int | None) -> dict[str, Any]:
|
|
255
|
+
"""Build a JSON-RPC 2.0 error response."""
|
|
256
|
+
return {
|
|
257
|
+
"jsonrpc": "2.0",
|
|
258
|
+
"error": {"code": code, "message": message},
|
|
259
|
+
"id": id,
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
# Standard JSON-RPC error codes
|
|
264
|
+
JSONRPC_PARSE_ERROR = -32700
|
|
265
|
+
JSONRPC_INVALID_REQUEST = -32600
|
|
266
|
+
JSONRPC_METHOD_NOT_FOUND = -32601
|
|
267
|
+
JSONRPC_INVALID_PARAMS = -32602
|
|
268
|
+
JSONRPC_INTERNAL_ERROR = -32603
|
|
269
|
+
|
|
270
|
+
# A2A-specific error codes
|
|
271
|
+
A2A_TASK_NOT_FOUND = -32001
|
|
272
|
+
A2A_TASK_NOT_CANCELABLE = -32002
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
__all__ = [
|
|
276
|
+
"A2AAgentCard",
|
|
277
|
+
"A2AConfig",
|
|
278
|
+
"A2AMessage",
|
|
279
|
+
"A2APart",
|
|
280
|
+
"A2ASkill",
|
|
281
|
+
"A2ATask",
|
|
282
|
+
"A2ATaskStatus",
|
|
283
|
+
"A2A_TASK_NOT_CANCELABLE",
|
|
284
|
+
"A2A_TASK_NOT_FOUND",
|
|
285
|
+
"JSONRPC_INTERNAL_ERROR",
|
|
286
|
+
"JSONRPC_INVALID_PARAMS",
|
|
287
|
+
"JSONRPC_INVALID_REQUEST",
|
|
288
|
+
"JSONRPC_METHOD_NOT_FOUND",
|
|
289
|
+
"JSONRPC_PARSE_ERROR",
|
|
290
|
+
"VALID_TASK_STATES",
|
|
291
|
+
"make_jsonrpc_error",
|
|
292
|
+
"make_jsonrpc_request",
|
|
293
|
+
"make_jsonrpc_response",
|
|
294
|
+
]
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Agno adapter for on_relay()."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import inspect
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from ..core import Relay
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _format_instructions_with_inbox(messages: list[Any], base_instructions: str) -> str:
|
|
12
|
+
content = "\n\nNew messages from other agents:\n"
|
|
13
|
+
for message in messages:
|
|
14
|
+
content += f" {message.sender}: {message.text}\n"
|
|
15
|
+
return f"{content}\n{base_instructions}" if base_instructions else content
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def on_relay(agent: Any, relay: "Relay | None" = None) -> Any:
|
|
19
|
+
"""Wrap Agno Agent to connect it to the relay."""
|
|
20
|
+
if relay is None:
|
|
21
|
+
from ..core import Relay
|
|
22
|
+
relay = Relay(getattr(agent, "name", "Agent"))
|
|
23
|
+
|
|
24
|
+
# 1. Add tools
|
|
25
|
+
async def relay_send(to: str, text: str) -> str:
|
|
26
|
+
"""Send a private message to another agent."""
|
|
27
|
+
await relay.send(to, text)
|
|
28
|
+
return "Message sent"
|
|
29
|
+
|
|
30
|
+
async def relay_inbox() -> str:
|
|
31
|
+
"""Check for new messages in the inbox."""
|
|
32
|
+
messages = await relay.inbox()
|
|
33
|
+
if not messages: return "No new messages"
|
|
34
|
+
return "\n".join([f"From {m.sender}: {m.text}" for m in messages])
|
|
35
|
+
|
|
36
|
+
async def relay_post(channel: str, text: str) -> str:
|
|
37
|
+
"""Post a message to a shared channel."""
|
|
38
|
+
await relay.post(channel, text)
|
|
39
|
+
return "Message posted"
|
|
40
|
+
|
|
41
|
+
async def relay_agents() -> str:
|
|
42
|
+
"""List all agents currently on the relay."""
|
|
43
|
+
agents = await relay.agents()
|
|
44
|
+
return ", ".join(agents)
|
|
45
|
+
|
|
46
|
+
agent.tools.extend([relay_send, relay_inbox, relay_post, relay_agents])
|
|
47
|
+
|
|
48
|
+
# 2. Wrap instructions with a local buffer so we don't starve relay_inbox tool
|
|
49
|
+
orig_instructions = agent.instructions
|
|
50
|
+
pending_messages: list[Any] = []
|
|
51
|
+
|
|
52
|
+
relay.on_message(lambda msg: pending_messages.append(msg))
|
|
53
|
+
|
|
54
|
+
async def instructions_wrapper(*args: Any, **kwargs: Any) -> str:
|
|
55
|
+
if callable(orig_instructions):
|
|
56
|
+
if inspect.iscoroutinefunction(orig_instructions):
|
|
57
|
+
base = await orig_instructions(*args, **kwargs)
|
|
58
|
+
else:
|
|
59
|
+
base = orig_instructions(*args, **kwargs)
|
|
60
|
+
if inspect.isawaitable(base):
|
|
61
|
+
base = await base
|
|
62
|
+
else:
|
|
63
|
+
base = orig_instructions
|
|
64
|
+
|
|
65
|
+
base = base or ""
|
|
66
|
+
if not pending_messages:
|
|
67
|
+
return base
|
|
68
|
+
|
|
69
|
+
messages = list(pending_messages)
|
|
70
|
+
pending_messages.clear()
|
|
71
|
+
return _format_instructions_with_inbox(messages, base)
|
|
72
|
+
|
|
73
|
+
agent.instructions = instructions_wrapper
|
|
74
|
+
return agent
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Claude Agent SDK adapter for on_relay()."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import inspect
|
|
5
|
+
from types import SimpleNamespace
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from ..core import Relay
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def on_relay(name: str, options: Any, relay: "Relay | None" = None) -> Any:
|
|
13
|
+
"""Wrap Claude Agent SDK query options to connect them to the relay."""
|
|
14
|
+
try:
|
|
15
|
+
from claude_agent_sdk.types import HookResult
|
|
16
|
+
except ImportError as exc:
|
|
17
|
+
raise ImportError(
|
|
18
|
+
"on_relay() for Claude Agent SDK requires the 'claude-agent-sdk' package. "
|
|
19
|
+
"Install it with: pip install claude-agent-sdk"
|
|
20
|
+
) from exc
|
|
21
|
+
|
|
22
|
+
agent_name = name or getattr(options, "name", "Agent")
|
|
23
|
+
if relay is None:
|
|
24
|
+
from ..core import Relay
|
|
25
|
+
relay = Relay(agent_name)
|
|
26
|
+
|
|
27
|
+
hooks = getattr(options, "hooks", None)
|
|
28
|
+
if hooks is None:
|
|
29
|
+
hooks = SimpleNamespace(post_tool_use=None, stop=None)
|
|
30
|
+
options.hooks = hooks
|
|
31
|
+
|
|
32
|
+
# 1. Inject Relaycast MCP server
|
|
33
|
+
mcp_config = {"name": "relaycast", "command": "agent-relay", "args": ["mcp"]}
|
|
34
|
+
if hasattr(options, "mcp_servers"):
|
|
35
|
+
options.mcp_servers.append(mcp_config)
|
|
36
|
+
else:
|
|
37
|
+
options.mcp_servers = [mcp_config]
|
|
38
|
+
|
|
39
|
+
# 2. Helper to format inbox messages
|
|
40
|
+
async def _drain_to_system_message() -> str | None:
|
|
41
|
+
messages = await relay.inbox()
|
|
42
|
+
if not messages: return None
|
|
43
|
+
content = "\n\nNew messages from other agents:\n"
|
|
44
|
+
for m in messages:
|
|
45
|
+
content += f" Relay message from {m.sender}: {m.text}\n"
|
|
46
|
+
return content
|
|
47
|
+
|
|
48
|
+
# 3. Hook wrappers
|
|
49
|
+
orig_post = getattr(hooks, "post_tool_use", None)
|
|
50
|
+
orig_stop = getattr(hooks, "stop", None)
|
|
51
|
+
|
|
52
|
+
async def post_tool_use_hook(*args, **kwargs):
|
|
53
|
+
res = None
|
|
54
|
+
if orig_post:
|
|
55
|
+
res = orig_post(*args, **kwargs)
|
|
56
|
+
if inspect.isawaitable(res):
|
|
57
|
+
res = await res
|
|
58
|
+
msg = await _drain_to_system_message()
|
|
59
|
+
if not msg:
|
|
60
|
+
return res
|
|
61
|
+
combined = (res.system_message + msg) if (res and res.system_message) else msg
|
|
62
|
+
return HookResult(system_message=combined)
|
|
63
|
+
|
|
64
|
+
async def stop_hook(*args, **kwargs):
|
|
65
|
+
res = None
|
|
66
|
+
if orig_stop:
|
|
67
|
+
res = orig_stop(*args, **kwargs)
|
|
68
|
+
if inspect.isawaitable(res):
|
|
69
|
+
res = await res
|
|
70
|
+
msg = await _drain_to_system_message()
|
|
71
|
+
if not msg:
|
|
72
|
+
return res
|
|
73
|
+
combined = (res.system_message + msg) if (res and res.system_message) else msg
|
|
74
|
+
return HookResult(system_message=combined, should_continue=True)
|
|
75
|
+
|
|
76
|
+
options.hooks.post_tool_use = post_tool_use_hook
|
|
77
|
+
options.hooks.stop = stop_hook
|
|
78
|
+
return options
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""CrewAI adapter for on_relay()."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import asyncio
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from ..core import Relay
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _message_key(message: Any) -> tuple[str | None, str, str]:
|
|
12
|
+
return (getattr(message, "message_id", None), message.sender, message.text)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _format_backstory(messages: list[Any], base_backstory: str) -> str:
|
|
16
|
+
if not messages:
|
|
17
|
+
return base_backstory
|
|
18
|
+
|
|
19
|
+
content = "\n\nNew messages from other agents:\n"
|
|
20
|
+
for message in messages:
|
|
21
|
+
content += f" {message.sender}: {message.text}\n"
|
|
22
|
+
return f"{content}\n{base_backstory}" if base_backstory else content
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class _RelayBackstory:
|
|
26
|
+
def __init__(self, relay: "Relay", base_backstory: str, buffer: list[Any]) -> None:
|
|
27
|
+
self._relay = relay
|
|
28
|
+
self._base_backstory = base_backstory
|
|
29
|
+
self._buffer = buffer
|
|
30
|
+
|
|
31
|
+
def _drain_buffer(self) -> list[Any]:
|
|
32
|
+
messages = list(self._buffer)
|
|
33
|
+
self._buffer.clear()
|
|
34
|
+
return messages
|
|
35
|
+
|
|
36
|
+
def _dedupe(self, messages: list[Any]) -> list[Any]:
|
|
37
|
+
seen: set[tuple[str | None, str, str]] = set()
|
|
38
|
+
unique: list[Any] = []
|
|
39
|
+
for message in messages:
|
|
40
|
+
key = _message_key(message)
|
|
41
|
+
if key in seen:
|
|
42
|
+
continue
|
|
43
|
+
seen.add(key)
|
|
44
|
+
unique.append(message)
|
|
45
|
+
return unique
|
|
46
|
+
|
|
47
|
+
def _resolve_sync(self) -> str:
|
|
48
|
+
messages = self._drain_buffer()
|
|
49
|
+
try:
|
|
50
|
+
loop = asyncio.get_running_loop()
|
|
51
|
+
except RuntimeError:
|
|
52
|
+
loop = None
|
|
53
|
+
|
|
54
|
+
if loop is None:
|
|
55
|
+
messages.extend(self._relay.inbox_sync())
|
|
56
|
+
else:
|
|
57
|
+
# Running inside an event loop — use a thread to avoid blocking
|
|
58
|
+
import concurrent.futures
|
|
59
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
|
|
60
|
+
polled = pool.submit(asyncio.run, self._relay.inbox()).result()
|
|
61
|
+
messages.extend(polled)
|
|
62
|
+
return _format_backstory(self._dedupe(messages), self._base_backstory)
|
|
63
|
+
|
|
64
|
+
async def _resolve_async(self) -> str:
|
|
65
|
+
messages = self._drain_buffer()
|
|
66
|
+
messages.extend(await self._relay.inbox())
|
|
67
|
+
return _format_backstory(self._dedupe(messages), self._base_backstory)
|
|
68
|
+
|
|
69
|
+
def __call__(self) -> str:
|
|
70
|
+
return self._resolve_sync()
|
|
71
|
+
|
|
72
|
+
def __await__(self) -> Any:
|
|
73
|
+
return self._resolve_async().__await__()
|
|
74
|
+
|
|
75
|
+
def __contains__(self, item: str) -> bool:
|
|
76
|
+
return item in self._resolve_sync()
|
|
77
|
+
|
|
78
|
+
def __eq__(self, other: object) -> bool:
|
|
79
|
+
if isinstance(other, str):
|
|
80
|
+
return self._resolve_sync() == other
|
|
81
|
+
return NotImplemented
|
|
82
|
+
|
|
83
|
+
def __getattr__(self, name: str) -> Any:
|
|
84
|
+
return getattr(self._resolve_sync(), name)
|
|
85
|
+
|
|
86
|
+
def __repr__(self) -> str:
|
|
87
|
+
return repr(self._resolve_sync())
|
|
88
|
+
|
|
89
|
+
def __str__(self) -> str:
|
|
90
|
+
return self._resolve_sync()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def on_relay(agent: Any, relay: "Relay | None" = None) -> Any:
|
|
94
|
+
"""Wrap CrewAI Agent to connect it to the relay."""
|
|
95
|
+
if relay is None:
|
|
96
|
+
from ..core import Relay
|
|
97
|
+
relay = Relay(getattr(agent, "name", "Agent"))
|
|
98
|
+
try:
|
|
99
|
+
from crewai.tools import tool
|
|
100
|
+
except ImportError:
|
|
101
|
+
raise ImportError(
|
|
102
|
+
"on_relay() for CrewAI requires the 'crewai' package. "
|
|
103
|
+
"Install it with: pip install crewai"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
@tool
|
|
107
|
+
async def relay_send(to: str, text: str) -> str:
|
|
108
|
+
"""Send a private message to another agent."""
|
|
109
|
+
await relay.send(to, text)
|
|
110
|
+
return "Message sent"
|
|
111
|
+
|
|
112
|
+
@tool
|
|
113
|
+
async def relay_inbox() -> str:
|
|
114
|
+
"""Check for new messages in the inbox."""
|
|
115
|
+
messages = await relay.inbox()
|
|
116
|
+
if not messages:
|
|
117
|
+
return "No new messages"
|
|
118
|
+
return "\n".join([f"From {m.sender}: {m.text}" for m in messages])
|
|
119
|
+
|
|
120
|
+
@tool
|
|
121
|
+
async def relay_post(channel: str, text: str) -> str:
|
|
122
|
+
"""Post a message to a shared channel."""
|
|
123
|
+
await relay.post(channel, text)
|
|
124
|
+
return "Message posted"
|
|
125
|
+
|
|
126
|
+
@tool
|
|
127
|
+
async def relay_agents() -> str:
|
|
128
|
+
"""List all agents currently on the relay."""
|
|
129
|
+
agents = await relay.agents()
|
|
130
|
+
return ", ".join(agents)
|
|
131
|
+
|
|
132
|
+
agent.tools.extend([relay_send, relay_inbox, relay_post, relay_agents])
|
|
133
|
+
|
|
134
|
+
backstory_buffer: list[Any] = []
|
|
135
|
+
|
|
136
|
+
def _buffer_message(message: Any) -> None:
|
|
137
|
+
backstory_buffer.append(message)
|
|
138
|
+
|
|
139
|
+
unsubscribe = relay.on_message(_buffer_message)
|
|
140
|
+
agent.backstory = _RelayBackstory(relay, agent.backstory or "", backstory_buffer)
|
|
141
|
+
agent._relay_unsubscribe = unsubscribe # type: ignore[attr-defined]
|
|
142
|
+
|
|
143
|
+
return agent
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Google ADK adapter for on_relay()."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import inspect
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from ..core import Relay
|
|
9
|
+
|
|
10
|
+
def on_relay(agent: Any, relay: "Relay | None" = None) -> Any:
|
|
11
|
+
"""Wrap Google ADK Agent to connect it to the relay."""
|
|
12
|
+
if relay is None:
|
|
13
|
+
from ..core import Relay
|
|
14
|
+
relay = Relay(getattr(agent, "name", "Agent"))
|
|
15
|
+
|
|
16
|
+
# 1. Add tools for sending
|
|
17
|
+
async def relay_send(to: str, text: str) -> str:
|
|
18
|
+
"""Send a private message to another agent."""
|
|
19
|
+
await relay.send(to, text)
|
|
20
|
+
return "Message sent"
|
|
21
|
+
|
|
22
|
+
async def relay_inbox() -> str:
|
|
23
|
+
"""Check for new messages in the inbox."""
|
|
24
|
+
messages = await relay.inbox()
|
|
25
|
+
if not messages: return "No new messages"
|
|
26
|
+
return "\n".join([f"From {m.sender}: {m.text}" for m in messages])
|
|
27
|
+
|
|
28
|
+
async def relay_post(channel: str, text: str) -> str:
|
|
29
|
+
"""Post a message to a shared channel."""
|
|
30
|
+
await relay.post(channel, text)
|
|
31
|
+
return "Message posted"
|
|
32
|
+
|
|
33
|
+
async def relay_agents() -> str:
|
|
34
|
+
"""List all agents currently on the relay."""
|
|
35
|
+
agents = await relay.agents()
|
|
36
|
+
return ", ".join(agents)
|
|
37
|
+
|
|
38
|
+
agent.tools.extend([relay_send, relay_inbox, relay_post, relay_agents])
|
|
39
|
+
|
|
40
|
+
# 2. Inject before_model_callback for receiving
|
|
41
|
+
orig_callback = getattr(agent, "before_model_callback", None)
|
|
42
|
+
|
|
43
|
+
async def relay_callback(llm_request: Any) -> Any:
|
|
44
|
+
orig_result = None
|
|
45
|
+
if orig_callback:
|
|
46
|
+
orig_result = orig_callback(llm_request)
|
|
47
|
+
if inspect.isawaitable(orig_result):
|
|
48
|
+
orig_result = await orig_result
|
|
49
|
+
|
|
50
|
+
# Always inject relay messages, even if the original callback returned a result
|
|
51
|
+
messages = await relay.inbox()
|
|
52
|
+
if messages:
|
|
53
|
+
from google.genai.types import Content, Part
|
|
54
|
+
|
|
55
|
+
if getattr(llm_request, "contents", None) is None:
|
|
56
|
+
llm_request.contents = []
|
|
57
|
+
|
|
58
|
+
for message in messages:
|
|
59
|
+
llm_request.contents.append(
|
|
60
|
+
Content(
|
|
61
|
+
role="user",
|
|
62
|
+
parts=[Part(text=f"[Relay] {message.sender}: {message.text}")],
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
return orig_result
|
|
67
|
+
|
|
68
|
+
agent.before_model_callback = relay_callback
|
|
69
|
+
return agent
|