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,502 @@
|
|
|
1
|
+
"""HTTP and WebSocket transport for communicate mode."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
from contextlib import suppress
|
|
8
|
+
from inspect import isawaitable
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
import aiohttp
|
|
13
|
+
from aiohttp import WSMsgType
|
|
14
|
+
except ImportError:
|
|
15
|
+
raise ImportError(
|
|
16
|
+
"Communicate mode requires 'aiohttp'. "
|
|
17
|
+
"Install it with: pip install agent-relay-sdk[communicate]"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
from .types import (
|
|
21
|
+
DEFAULT_RELAY_BASE_URL,
|
|
22
|
+
Message,
|
|
23
|
+
MessageCallback,
|
|
24
|
+
RelayAuthError,
|
|
25
|
+
RelayConfig,
|
|
26
|
+
RelayConfigError,
|
|
27
|
+
RelayConnectionError,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
HTTP_RETRY_ATTEMPTS = 3
|
|
31
|
+
WS_RECONNECT_MAX_DELAY = 30
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class RelayTransport:
|
|
35
|
+
"""Minimal Relaycast transport backed by aiohttp."""
|
|
36
|
+
|
|
37
|
+
def __init__(self, agent_name: str, config: RelayConfig) -> None:
|
|
38
|
+
self.agent_name = agent_name
|
|
39
|
+
self.config = config
|
|
40
|
+
self.agent_id: str | None = None
|
|
41
|
+
self.token: str | None = None
|
|
42
|
+
|
|
43
|
+
self._session: aiohttp.ClientSession | None = None
|
|
44
|
+
self._ws: aiohttp.ClientWebSocketResponse | None = None
|
|
45
|
+
self._ws_task: asyncio.Task[None] | None = None
|
|
46
|
+
self._message_callback: MessageCallback | None = None
|
|
47
|
+
self._closing = False
|
|
48
|
+
|
|
49
|
+
async def connect(self) -> None:
|
|
50
|
+
self._require_config(require_workspace=True)
|
|
51
|
+
self._closing = False
|
|
52
|
+
|
|
53
|
+
await self.register_agent()
|
|
54
|
+
await self._connect_websocket()
|
|
55
|
+
|
|
56
|
+
if self._ws_task is None or self._ws_task.done():
|
|
57
|
+
self._ws_task = asyncio.create_task(self._ws_loop())
|
|
58
|
+
|
|
59
|
+
async def disconnect(self) -> None:
|
|
60
|
+
self._closing = True
|
|
61
|
+
|
|
62
|
+
ws_task = self._ws_task
|
|
63
|
+
self._ws_task = None
|
|
64
|
+
|
|
65
|
+
ws = self._ws
|
|
66
|
+
self._ws = None
|
|
67
|
+
if ws is not None and not ws.closed:
|
|
68
|
+
with suppress(Exception):
|
|
69
|
+
try:
|
|
70
|
+
await asyncio.wait_for(ws.close(), timeout=2)
|
|
71
|
+
except (asyncio.TimeoutError, asyncio.CancelledError):
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
if ws_task is not None and not ws_task.done():
|
|
75
|
+
ws_task.cancel()
|
|
76
|
+
with suppress(asyncio.CancelledError):
|
|
77
|
+
await ws_task
|
|
78
|
+
|
|
79
|
+
with suppress(Exception):
|
|
80
|
+
await self.unregister_agent()
|
|
81
|
+
|
|
82
|
+
await self._close_session()
|
|
83
|
+
self._closing = False
|
|
84
|
+
|
|
85
|
+
async def send_http(
|
|
86
|
+
self,
|
|
87
|
+
method: str,
|
|
88
|
+
path: str,
|
|
89
|
+
*,
|
|
90
|
+
payload: dict[str, Any] | None = None,
|
|
91
|
+
) -> Any:
|
|
92
|
+
self._require_config()
|
|
93
|
+
session = await self._ensure_session()
|
|
94
|
+
url = f"{self._base_url()}{path}"
|
|
95
|
+
headers = {"Authorization": f"Bearer {self.config.api_key}"}
|
|
96
|
+
|
|
97
|
+
for attempt in range(1, HTTP_RETRY_ATTEMPTS + 1):
|
|
98
|
+
try:
|
|
99
|
+
async with session.request(method, url, json=payload, headers=headers) as response:
|
|
100
|
+
if response.status == 401:
|
|
101
|
+
raise RelayAuthError(await self._error_message(response))
|
|
102
|
+
|
|
103
|
+
if 500 <= response.status <= 599:
|
|
104
|
+
message = await self._error_message(response)
|
|
105
|
+
if attempt < HTTP_RETRY_ATTEMPTS:
|
|
106
|
+
await asyncio.sleep(min(2 ** (attempt - 1), WS_RECONNECT_MAX_DELAY))
|
|
107
|
+
continue
|
|
108
|
+
raise RelayConnectionError(response.status, message)
|
|
109
|
+
|
|
110
|
+
if response.status >= 400:
|
|
111
|
+
raise RelayConnectionError(
|
|
112
|
+
response.status,
|
|
113
|
+
await self._error_message(response),
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
if response.status == 204:
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
if response.content_type == "application/json":
|
|
120
|
+
return await response.json()
|
|
121
|
+
|
|
122
|
+
return await response.text()
|
|
123
|
+
except RelayAuthError:
|
|
124
|
+
raise
|
|
125
|
+
except RelayConnectionError:
|
|
126
|
+
raise
|
|
127
|
+
except aiohttp.ClientError as exc:
|
|
128
|
+
if attempt < HTTP_RETRY_ATTEMPTS:
|
|
129
|
+
await asyncio.sleep(min(2 ** (attempt - 1), WS_RECONNECT_MAX_DELAY))
|
|
130
|
+
continue
|
|
131
|
+
raise RelayConnectionError(0, str(exc)) from exc
|
|
132
|
+
|
|
133
|
+
raise RelayConnectionError(500, "Unexpected transport retry failure")
|
|
134
|
+
|
|
135
|
+
def on_ws_message(self, callback: MessageCallback) -> None:
|
|
136
|
+
self._message_callback = callback
|
|
137
|
+
|
|
138
|
+
async def register_agent(self) -> str:
|
|
139
|
+
"""Register agent, or rotate token if it already exists (registerOrRotate pattern)."""
|
|
140
|
+
self._require_config(require_workspace=True)
|
|
141
|
+
|
|
142
|
+
if self.agent_id is not None and self.token is not None:
|
|
143
|
+
return self.agent_id
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
payload = await self.send_http(
|
|
147
|
+
"POST",
|
|
148
|
+
"/v1/agents",
|
|
149
|
+
payload={"name": self.agent_name, "type": "agent"},
|
|
150
|
+
)
|
|
151
|
+
except RelayConnectionError as exc:
|
|
152
|
+
if exc.status_code == 409:
|
|
153
|
+
# Agent already exists — get its info and rotate the token
|
|
154
|
+
from urllib.parse import quote
|
|
155
|
+
agent_payload = await self.send_http(
|
|
156
|
+
"GET",
|
|
157
|
+
f"/v1/agents/{quote(self.agent_name, safe='')}",
|
|
158
|
+
)
|
|
159
|
+
agent_data = agent_payload.get("data", agent_payload)
|
|
160
|
+
self.agent_id = agent_data["id"]
|
|
161
|
+
|
|
162
|
+
rotate_payload = await self.send_http(
|
|
163
|
+
"POST",
|
|
164
|
+
f"/v1/agents/{quote(self.agent_name, safe='')}/rotate-token",
|
|
165
|
+
)
|
|
166
|
+
rotate_data = rotate_payload.get("data", rotate_payload)
|
|
167
|
+
self.token = rotate_data["token"]
|
|
168
|
+
return self.agent_id
|
|
169
|
+
raise
|
|
170
|
+
# Relaycast API wraps in {ok, data: {...}}
|
|
171
|
+
data = payload.get("data", payload)
|
|
172
|
+
self.agent_id = data["id"]
|
|
173
|
+
self.token = data["token"]
|
|
174
|
+
return self.agent_id
|
|
175
|
+
|
|
176
|
+
async def unregister_agent(self) -> None:
|
|
177
|
+
if self.agent_id is None or self.token is None:
|
|
178
|
+
await self._close_session_if_idle()
|
|
179
|
+
return
|
|
180
|
+
|
|
181
|
+
await self._send_http_as_agent("POST", "/v1/agents/disconnect")
|
|
182
|
+
self.agent_id = None
|
|
183
|
+
self.token = None
|
|
184
|
+
await self._close_session_if_idle()
|
|
185
|
+
|
|
186
|
+
async def send_dm(self, recipient: str, text: str) -> str:
|
|
187
|
+
await self._ensure_registered()
|
|
188
|
+
payload = await self._send_http_as_agent(
|
|
189
|
+
"POST",
|
|
190
|
+
"/v1/dm",
|
|
191
|
+
payload={"to": recipient, "text": text},
|
|
192
|
+
)
|
|
193
|
+
if payload is None:
|
|
194
|
+
return ""
|
|
195
|
+
data = payload.get("data", payload)
|
|
196
|
+
return data.get("id", data.get("message_id", ""))
|
|
197
|
+
|
|
198
|
+
async def post_message(self, channel: str, text: str) -> str:
|
|
199
|
+
await self._ensure_registered()
|
|
200
|
+
from urllib.parse import quote
|
|
201
|
+
|
|
202
|
+
payload = await self._send_http_as_agent(
|
|
203
|
+
"POST",
|
|
204
|
+
f"/v1/channels/{quote(channel, safe='')}/messages",
|
|
205
|
+
payload={"text": text},
|
|
206
|
+
)
|
|
207
|
+
if payload is None:
|
|
208
|
+
return ""
|
|
209
|
+
data = payload.get("data", payload)
|
|
210
|
+
return data.get("id", data.get("message_id", ""))
|
|
211
|
+
|
|
212
|
+
async def reply(self, message_id: str, text: str) -> str:
|
|
213
|
+
await self._ensure_registered()
|
|
214
|
+
from urllib.parse import quote
|
|
215
|
+
|
|
216
|
+
payload = await self._send_http_as_agent(
|
|
217
|
+
"POST",
|
|
218
|
+
f"/v1/messages/{quote(message_id, safe='')}/replies",
|
|
219
|
+
payload={"text": text},
|
|
220
|
+
)
|
|
221
|
+
if payload is None:
|
|
222
|
+
return ""
|
|
223
|
+
data = payload.get("data", payload)
|
|
224
|
+
return data.get("id", data.get("message_id", ""))
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
async def join_channel(self, channel: str) -> None:
|
|
228
|
+
await self._ensure_registered()
|
|
229
|
+
from urllib.parse import quote
|
|
230
|
+
await self._send_http_as_agent(
|
|
231
|
+
'POST',
|
|
232
|
+
f'/v1/channels/{quote(channel, safe="")}/join',
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
async def check_inbox(self) -> list[Message]:
|
|
236
|
+
await self._ensure_registered()
|
|
237
|
+
from urllib.parse import quote
|
|
238
|
+
|
|
239
|
+
payload = await self._send_http_as_agent("GET", "/v1/inbox")
|
|
240
|
+
data = payload.get("data", payload)
|
|
241
|
+
messages: list[Message] = []
|
|
242
|
+
|
|
243
|
+
# Fetch unread DM conversations
|
|
244
|
+
for dm in data.get("unread_dms", []):
|
|
245
|
+
conv_id = dm.get("conversation_id", "")
|
|
246
|
+
sender = dm.get("from", "unknown")
|
|
247
|
+
# Fetch actual messages from the conversation
|
|
248
|
+
try:
|
|
249
|
+
conv_payload = await self._send_http_as_agent(
|
|
250
|
+
"GET", f"/v1/dm/{quote(conv_id, safe='')}/messages"
|
|
251
|
+
)
|
|
252
|
+
conv_data = conv_payload.get("data", conv_payload)
|
|
253
|
+
items = conv_data if isinstance(conv_data, list) else []
|
|
254
|
+
for item in items:
|
|
255
|
+
messages.append(Message(
|
|
256
|
+
sender=item.get("agent_name", sender),
|
|
257
|
+
text=item.get("text", ""),
|
|
258
|
+
channel=None,
|
|
259
|
+
thread_id=conv_id,
|
|
260
|
+
timestamp=item.get("created_at"),
|
|
261
|
+
message_id=item.get("id"),
|
|
262
|
+
))
|
|
263
|
+
except Exception:
|
|
264
|
+
# Fall back to the summary last_message
|
|
265
|
+
last = dm.get("last_message", {})
|
|
266
|
+
if last.get("text"):
|
|
267
|
+
messages.append(Message(
|
|
268
|
+
sender=sender,
|
|
269
|
+
text=last["text"],
|
|
270
|
+
channel=None,
|
|
271
|
+
thread_id=conv_id,
|
|
272
|
+
timestamp=last.get("created_at"),
|
|
273
|
+
message_id=last.get("id"),
|
|
274
|
+
))
|
|
275
|
+
|
|
276
|
+
# Also include unread channel mentions
|
|
277
|
+
for mention in data.get("mentions", []):
|
|
278
|
+
messages.append(Message(
|
|
279
|
+
sender=mention.get("from", mention.get("agent_name", "unknown")),
|
|
280
|
+
text=mention.get("text", ""),
|
|
281
|
+
channel=mention.get("channel_name"),
|
|
282
|
+
thread_id=mention.get("thread_id"),
|
|
283
|
+
timestamp=mention.get("created_at"),
|
|
284
|
+
message_id=mention.get("id"),
|
|
285
|
+
))
|
|
286
|
+
|
|
287
|
+
return messages
|
|
288
|
+
|
|
289
|
+
async def list_agents(self) -> list[str]:
|
|
290
|
+
payload = await self.send_http("GET", "/v1/agents")
|
|
291
|
+
data = payload.get("data", payload)
|
|
292
|
+
if isinstance(data, list):
|
|
293
|
+
return [a.get("name", a) if isinstance(a, dict) else a for a in data]
|
|
294
|
+
return list(data.get("agents", []))
|
|
295
|
+
|
|
296
|
+
async def _send_http_as_agent(
|
|
297
|
+
self,
|
|
298
|
+
method: str,
|
|
299
|
+
path: str,
|
|
300
|
+
*,
|
|
301
|
+
payload: dict[str, Any] | None = None,
|
|
302
|
+
) -> Any:
|
|
303
|
+
"""Like send_http but authenticates with the per-agent token."""
|
|
304
|
+
await self._ensure_registered()
|
|
305
|
+
session = await self._ensure_session()
|
|
306
|
+
url = f"{self._base_url()}{path}"
|
|
307
|
+
headers = {"Authorization": f"Bearer {self.token}"}
|
|
308
|
+
|
|
309
|
+
for attempt in range(1, HTTP_RETRY_ATTEMPTS + 1):
|
|
310
|
+
try:
|
|
311
|
+
async with session.request(method, url, json=payload, headers=headers) as response:
|
|
312
|
+
if response.status == 401:
|
|
313
|
+
raise RelayAuthError(await self._error_message(response))
|
|
314
|
+
|
|
315
|
+
if 500 <= response.status <= 599:
|
|
316
|
+
message = await self._error_message(response)
|
|
317
|
+
if attempt < HTTP_RETRY_ATTEMPTS:
|
|
318
|
+
await asyncio.sleep(min(2 ** (attempt - 1), WS_RECONNECT_MAX_DELAY))
|
|
319
|
+
continue
|
|
320
|
+
raise RelayConnectionError(response.status, message)
|
|
321
|
+
|
|
322
|
+
if response.status >= 400:
|
|
323
|
+
raise RelayConnectionError(
|
|
324
|
+
response.status,
|
|
325
|
+
await self._error_message(response),
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
if response.status == 204:
|
|
329
|
+
return None
|
|
330
|
+
|
|
331
|
+
if response.content_type == "application/json":
|
|
332
|
+
return await response.json()
|
|
333
|
+
|
|
334
|
+
return await response.text()
|
|
335
|
+
except (RelayAuthError, RelayConnectionError):
|
|
336
|
+
raise
|
|
337
|
+
except aiohttp.ClientError as exc:
|
|
338
|
+
if attempt < HTTP_RETRY_ATTEMPTS:
|
|
339
|
+
await asyncio.sleep(min(2 ** (attempt - 1), WS_RECONNECT_MAX_DELAY))
|
|
340
|
+
continue
|
|
341
|
+
raise RelayConnectionError(0, str(exc)) from exc
|
|
342
|
+
|
|
343
|
+
raise RelayConnectionError(500, "Unexpected transport retry failure")
|
|
344
|
+
|
|
345
|
+
async def _ensure_registered(self) -> None:
|
|
346
|
+
if self.agent_id is None or self.token is None:
|
|
347
|
+
await self.register_agent()
|
|
348
|
+
|
|
349
|
+
def _require_config(self, *, require_workspace: bool = False) -> None:
|
|
350
|
+
if not self.config.api_key:
|
|
351
|
+
raise RelayConfigError(
|
|
352
|
+
"Missing RELAY_API_KEY. Set the environment variable or pass api_key= to RelayConfig."
|
|
353
|
+
)
|
|
354
|
+
if require_workspace and not self.config.workspace:
|
|
355
|
+
raise RelayConfigError(
|
|
356
|
+
"Missing RELAY_WORKSPACE. Set the environment variable or pass workspace= to RelayConfig."
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
def _base_url(self) -> str:
|
|
360
|
+
return (self.config.base_url or DEFAULT_RELAY_BASE_URL).rstrip("/")
|
|
361
|
+
|
|
362
|
+
async def _ensure_session(self) -> aiohttp.ClientSession:
|
|
363
|
+
if self._session is None or self._session.closed:
|
|
364
|
+
self._session = aiohttp.ClientSession()
|
|
365
|
+
return self._session
|
|
366
|
+
|
|
367
|
+
async def _close_session(self) -> None:
|
|
368
|
+
if self._session is not None and not self._session.closed:
|
|
369
|
+
await self._session.close()
|
|
370
|
+
self._session = None
|
|
371
|
+
|
|
372
|
+
async def _close_session_if_idle(self) -> None:
|
|
373
|
+
if self._ws_task is None and (self._ws is None or self._ws.closed):
|
|
374
|
+
await self._close_session()
|
|
375
|
+
|
|
376
|
+
async def _connect_websocket(self) -> None:
|
|
377
|
+
await self._ensure_registered()
|
|
378
|
+
|
|
379
|
+
if self._ws is not None and not self._ws.closed:
|
|
380
|
+
return
|
|
381
|
+
|
|
382
|
+
from urllib.parse import quote
|
|
383
|
+
|
|
384
|
+
session = await self._ensure_session()
|
|
385
|
+
ws_url = f"{self._ws_base_url()}/v1/ws?token={quote(self.token, safe='')}"
|
|
386
|
+
self._ws = await session.ws_connect(ws_url)
|
|
387
|
+
|
|
388
|
+
def _ws_base_url(self) -> str:
|
|
389
|
+
base_url = self._base_url()
|
|
390
|
+
if base_url.startswith("https://"):
|
|
391
|
+
return "wss://" + base_url[len("https://") :]
|
|
392
|
+
if base_url.startswith("http://"):
|
|
393
|
+
return "ws://" + base_url[len("http://") :]
|
|
394
|
+
return base_url
|
|
395
|
+
|
|
396
|
+
async def _ws_loop(self) -> None:
|
|
397
|
+
delay = 1
|
|
398
|
+
|
|
399
|
+
while not self._closing:
|
|
400
|
+
try:
|
|
401
|
+
if self._ws is None:
|
|
402
|
+
await self._connect_websocket()
|
|
403
|
+
|
|
404
|
+
assert self._ws is not None
|
|
405
|
+
async for raw_message in self._ws:
|
|
406
|
+
if raw_message.type is WSMsgType.TEXT:
|
|
407
|
+
await self._dispatch_ws_payload(raw_message.data)
|
|
408
|
+
elif raw_message.type in {WSMsgType.CLOSE, WSMsgType.CLOSED, WSMsgType.ERROR}:
|
|
409
|
+
break
|
|
410
|
+
except asyncio.CancelledError:
|
|
411
|
+
raise
|
|
412
|
+
except Exception:
|
|
413
|
+
pass
|
|
414
|
+
finally:
|
|
415
|
+
if self._ws is not None and self._ws.closed:
|
|
416
|
+
self._ws = None
|
|
417
|
+
|
|
418
|
+
if self._closing:
|
|
419
|
+
break
|
|
420
|
+
|
|
421
|
+
await asyncio.sleep(delay)
|
|
422
|
+
delay = min(delay * 2, WS_RECONNECT_MAX_DELAY)
|
|
423
|
+
|
|
424
|
+
try:
|
|
425
|
+
await self._connect_websocket()
|
|
426
|
+
delay = 1
|
|
427
|
+
except asyncio.CancelledError:
|
|
428
|
+
raise
|
|
429
|
+
except Exception:
|
|
430
|
+
continue
|
|
431
|
+
|
|
432
|
+
async def _dispatch_ws_payload(self, raw_payload: str) -> None:
|
|
433
|
+
payload = json.loads(raw_payload)
|
|
434
|
+
event_type = payload.get("type", "")
|
|
435
|
+
|
|
436
|
+
if event_type == "ping":
|
|
437
|
+
if self._ws is not None and not self._ws.closed:
|
|
438
|
+
await self._ws.send_json({"type": "pong"})
|
|
439
|
+
return
|
|
440
|
+
|
|
441
|
+
# Accept message.created, dm.received, direct_message.received, thread.reply, and legacy "message"
|
|
442
|
+
message_events = {"message.created", "dm.received", "direct_message.received",
|
|
443
|
+
"thread.reply", "message", "group_dm.received"}
|
|
444
|
+
if event_type not in message_events:
|
|
445
|
+
return
|
|
446
|
+
|
|
447
|
+
callback = self._message_callback
|
|
448
|
+
if callback is None:
|
|
449
|
+
return
|
|
450
|
+
|
|
451
|
+
try:
|
|
452
|
+
msg = self._message_from_payload(payload)
|
|
453
|
+
except (KeyError, TypeError):
|
|
454
|
+
return
|
|
455
|
+
|
|
456
|
+
result = callback(msg)
|
|
457
|
+
if isawaitable(result):
|
|
458
|
+
await result
|
|
459
|
+
|
|
460
|
+
@staticmethod
|
|
461
|
+
def _message_from_payload(payload: dict[str, Any]) -> Message:
|
|
462
|
+
# Support both flat and nested message structures
|
|
463
|
+
m = payload.get("message") if isinstance(payload.get("message"), dict) else payload
|
|
464
|
+
sender = (
|
|
465
|
+
m.get("sender")
|
|
466
|
+
or m.get("agent_name")
|
|
467
|
+
or m.get("from")
|
|
468
|
+
or m.get("agentName")
|
|
469
|
+
or payload.get("agent_name")
|
|
470
|
+
or payload.get("from")
|
|
471
|
+
or "unknown"
|
|
472
|
+
)
|
|
473
|
+
text = m.get("text", "")
|
|
474
|
+
channel = m.get("channel") or m.get("channel_name") or m.get("channelName") or payload.get("channel") or payload.get("channel_name")
|
|
475
|
+
thread_id = m.get("thread_id") or m.get("threadId") or m.get("conversation_id") or m.get("conversationId") or payload.get("thread_id")
|
|
476
|
+
timestamp = m.get("timestamp") or m.get("created_at") or m.get("createdAt") or payload.get("timestamp")
|
|
477
|
+
message_id = m.get("id") or m.get("message_id") or m.get("messageId") or payload.get("message_id")
|
|
478
|
+
|
|
479
|
+
return Message(
|
|
480
|
+
sender=sender,
|
|
481
|
+
text=text,
|
|
482
|
+
channel=channel,
|
|
483
|
+
thread_id=thread_id,
|
|
484
|
+
timestamp=timestamp,
|
|
485
|
+
message_id=message_id,
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
@staticmethod
|
|
489
|
+
async def _error_message(response: aiohttp.ClientResponse) -> str:
|
|
490
|
+
try:
|
|
491
|
+
payload = await response.json()
|
|
492
|
+
except Exception:
|
|
493
|
+
text = await response.text()
|
|
494
|
+
return text or response.reason or "Request failed"
|
|
495
|
+
# Relaycast wraps errors as {ok: false, error: {code, message}}
|
|
496
|
+
error = payload.get("error")
|
|
497
|
+
if isinstance(error, dict) and error.get("message"):
|
|
498
|
+
return str(error["message"])
|
|
499
|
+
return str(payload.get("message") or response.reason or "Request failed")
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
__all__ = ["RelayTransport"]
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Types shared by the communicate-mode relay client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Awaitable, Callable, TypeAlias
|
|
8
|
+
|
|
9
|
+
DEFAULT_RELAY_BASE_URL = "https://api.relaycast.dev"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class Message:
|
|
14
|
+
sender: str
|
|
15
|
+
text: str
|
|
16
|
+
channel: str | None = None
|
|
17
|
+
thread_id: str | None = None
|
|
18
|
+
timestamp: str | float | None = None
|
|
19
|
+
message_id: str | None = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
MessageCallback: TypeAlias = Callable[[Message], None] | Callable[[Message], Awaitable[None]]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class RelayConfig:
|
|
27
|
+
workspace: str | None = None
|
|
28
|
+
api_key: str | None = None
|
|
29
|
+
base_url: str | None = None
|
|
30
|
+
channels: list[str] = field(default_factory=lambda: ["general"])
|
|
31
|
+
poll_interval_ms: int = 1000
|
|
32
|
+
auto_cleanup: bool = True
|
|
33
|
+
|
|
34
|
+
def __post_init__(self) -> None:
|
|
35
|
+
if self.workspace is None:
|
|
36
|
+
self.workspace = os.getenv("RELAY_WORKSPACE")
|
|
37
|
+
if self.api_key is None:
|
|
38
|
+
self.api_key = os.getenv("RELAY_API_KEY")
|
|
39
|
+
if self.base_url is None:
|
|
40
|
+
self.base_url = os.getenv("RELAY_BASE_URL")
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def resolve(
|
|
44
|
+
cls,
|
|
45
|
+
workspace: str | None = None,
|
|
46
|
+
api_key: str | None = None,
|
|
47
|
+
base_url: str | None = None,
|
|
48
|
+
channels: list[str] | None = None,
|
|
49
|
+
poll_interval_ms: int = 1000,
|
|
50
|
+
auto_cleanup: bool = True,
|
|
51
|
+
) -> "RelayConfig":
|
|
52
|
+
resolved_workspace = workspace if workspace is not None else os.getenv("RELAY_WORKSPACE")
|
|
53
|
+
resolved_api_key = api_key if api_key is not None else os.getenv("RELAY_API_KEY")
|
|
54
|
+
resolved_base_url = base_url if base_url is not None else os.getenv("RELAY_BASE_URL") or DEFAULT_RELAY_BASE_URL
|
|
55
|
+
return cls(
|
|
56
|
+
workspace=resolved_workspace,
|
|
57
|
+
api_key=resolved_api_key,
|
|
58
|
+
base_url=resolved_base_url,
|
|
59
|
+
channels=list(channels) if channels is not None else ["general"],
|
|
60
|
+
poll_interval_ms=poll_interval_ms,
|
|
61
|
+
auto_cleanup=auto_cleanup,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class RelayConnectionError(Exception):
|
|
66
|
+
def __init__(self, status_code: int, message: str) -> None:
|
|
67
|
+
self.status_code = status_code
|
|
68
|
+
self.message = message
|
|
69
|
+
super().__init__(f"{status_code}: {message}")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class RelayConfigError(Exception):
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class RelayAuthError(RelayConnectionError):
|
|
77
|
+
def __init__(self, message: str = "Unauthorized", status_code: int = 401) -> None:
|
|
78
|
+
super().__init__(status_code=status_code, message=message)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
__all__ = [
|
|
82
|
+
"DEFAULT_RELAY_BASE_URL",
|
|
83
|
+
"Message",
|
|
84
|
+
"MessageCallback",
|
|
85
|
+
"RelayAuthError",
|
|
86
|
+
"RelayConfig",
|
|
87
|
+
"RelayConfigError",
|
|
88
|
+
"RelayConnectionError",
|
|
89
|
+
]
|
|
@@ -29,9 +29,10 @@ SwarmPattern = Literal[
|
|
|
29
29
|
"blackboard",
|
|
30
30
|
"swarm",
|
|
31
31
|
"competitive",
|
|
32
|
+
"review-loop",
|
|
32
33
|
]
|
|
33
34
|
|
|
34
|
-
AgentCli = Literal["claude", "codex", "gemini", "aider", "goose", "opencode", "droid"]
|
|
35
|
+
AgentCli = Literal["claude", "codex", "gemini", "aider", "goose", "opencode", "droid", "cursor", "cursor-agent", "agent"]
|
|
35
36
|
AgentStatus = Literal["healthy", "restarting", "dead", "released"]
|
|
36
37
|
CrashCategory = Literal["oom", "segfault", "error", "signal", "unknown"]
|
|
37
38
|
WorkflowOnError = Literal["fail", "skip", "retry"]
|
|
File without changes
|
|
File without changes
|