agent-relay 3.2.3 → 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/dist/index.cjs +265 -108
- 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/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/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,489 @@
|
|
|
1
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
2
|
+
|
|
3
|
+
import { WebSocket } from 'ws';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
type Message,
|
|
7
|
+
type MessageCallback,
|
|
8
|
+
type RelayConfig,
|
|
9
|
+
RelayAuthError,
|
|
10
|
+
RelayConfigError,
|
|
11
|
+
RelayConnectionError,
|
|
12
|
+
resolveRelayConfig,
|
|
13
|
+
} from './types.js';
|
|
14
|
+
|
|
15
|
+
const HTTP_RETRY_ATTEMPTS = 3;
|
|
16
|
+
const WS_RECONNECT_MAX_DELAY_MS = 30_000;
|
|
17
|
+
|
|
18
|
+
type JsonObject = Record<string, unknown>;
|
|
19
|
+
|
|
20
|
+
export class RelayTransport {
|
|
21
|
+
readonly agentName: string;
|
|
22
|
+
readonly config;
|
|
23
|
+
|
|
24
|
+
agentId?: string;
|
|
25
|
+
token?: string;
|
|
26
|
+
|
|
27
|
+
private ws?: WebSocket;
|
|
28
|
+
private messageCallback?: MessageCallback;
|
|
29
|
+
private closing = false;
|
|
30
|
+
private reconnectDelayMs = 1_000;
|
|
31
|
+
private reconnectTimer?: NodeJS.Timeout;
|
|
32
|
+
private wsConnectPromise?: Promise<void>;
|
|
33
|
+
|
|
34
|
+
constructor(agentName: string, config: RelayConfig = {}) {
|
|
35
|
+
this.agentName = agentName;
|
|
36
|
+
this.config = resolveRelayConfig(config);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async connect(): Promise<void> {
|
|
40
|
+
this.requireConfig({ requireWorkspace: true });
|
|
41
|
+
this.closing = false;
|
|
42
|
+
await this.registerAgent();
|
|
43
|
+
await this.connectWebSocket();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async disconnect(): Promise<void> {
|
|
47
|
+
this.closing = true;
|
|
48
|
+
this.clearReconnectTimer();
|
|
49
|
+
|
|
50
|
+
const socket = this.ws;
|
|
51
|
+
this.ws = undefined;
|
|
52
|
+
this.wsConnectPromise = undefined;
|
|
53
|
+
|
|
54
|
+
if (socket && socket.readyState !== WebSocket.CLOSED) {
|
|
55
|
+
await Promise.race([
|
|
56
|
+
new Promise<void>((resolve) => {
|
|
57
|
+
socket.once('close', () => resolve());
|
|
58
|
+
socket.close();
|
|
59
|
+
}),
|
|
60
|
+
new Promise<void>((resolve) => setTimeout(resolve, 2_000)),
|
|
61
|
+
]);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (this.agentId) {
|
|
65
|
+
try {
|
|
66
|
+
await this.unregisterAgent();
|
|
67
|
+
} catch {
|
|
68
|
+
// Best-effort cleanup.
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
this.closing = false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
onWsMessage(callback: MessageCallback): void {
|
|
76
|
+
this.messageCallback = callback;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async registerAgent(): Promise<string> {
|
|
80
|
+
this.requireConfig({ requireWorkspace: true });
|
|
81
|
+
|
|
82
|
+
if (this.agentId && this.token) {
|
|
83
|
+
return this.agentId;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let payload: JsonObject;
|
|
87
|
+
try {
|
|
88
|
+
payload = await this.sendHttp<JsonObject>(
|
|
89
|
+
'POST',
|
|
90
|
+
'/v1/agents',
|
|
91
|
+
{ name: this.agentName, type: 'agent' },
|
|
92
|
+
);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
if (error instanceof RelayConnectionError && error.statusCode === 409) {
|
|
95
|
+
const agentPayload = await this.sendHttp<JsonObject>(
|
|
96
|
+
'GET',
|
|
97
|
+
`/v1/agents/${encodeURIComponent(this.agentName)}`,
|
|
98
|
+
);
|
|
99
|
+
const agentData = (agentPayload as any).data ?? agentPayload;
|
|
100
|
+
this.agentId = String(agentData.id);
|
|
101
|
+
|
|
102
|
+
const rotatePayload = await this.sendHttp<JsonObject>(
|
|
103
|
+
'POST',
|
|
104
|
+
`/v1/agents/${encodeURIComponent(this.agentName)}/rotate-token`,
|
|
105
|
+
);
|
|
106
|
+
const rotateData = (rotatePayload as any).data ?? rotatePayload;
|
|
107
|
+
this.token = String(rotateData.token);
|
|
108
|
+
return this.agentId;
|
|
109
|
+
}
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const data = (payload as any).data ?? payload;
|
|
114
|
+
this.agentId = String(data.id);
|
|
115
|
+
this.token = String(data.token);
|
|
116
|
+
return this.agentId;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async unregisterAgent(): Promise<void> {
|
|
120
|
+
if (!this.agentId || !this.token) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
this.agentId = undefined;
|
|
125
|
+
const agentToken = this.token;
|
|
126
|
+
this.token = undefined;
|
|
127
|
+
await this.sendHttpAsAgent('POST', '/v1/agents/disconnect', undefined, agentToken);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async sendDm(to: string, text: string): Promise<string> {
|
|
131
|
+
await this.ensureRegistered();
|
|
132
|
+
const payload = await this.sendHttpAsAgent<JsonObject>('POST', '/v1/dm', { to, text });
|
|
133
|
+
const data = (payload as any).data ?? payload;
|
|
134
|
+
return String(data.id ?? data.message_id ?? '');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async postMessage(channel: string, text: string): Promise<string> {
|
|
138
|
+
await this.ensureRegistered();
|
|
139
|
+
const payload = await this.sendHttpAsAgent<JsonObject>(
|
|
140
|
+
'POST',
|
|
141
|
+
`/v1/channels/${encodeURIComponent(channel)}/messages`,
|
|
142
|
+
{ text },
|
|
143
|
+
);
|
|
144
|
+
const data = (payload as any).data ?? payload;
|
|
145
|
+
return String(data.id ?? data.message_id ?? '');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async reply(messageId: string, text: string): Promise<string> {
|
|
149
|
+
await this.ensureRegistered();
|
|
150
|
+
const payload = await this.sendHttpAsAgent<JsonObject>(
|
|
151
|
+
'POST',
|
|
152
|
+
`/v1/messages/${encodeURIComponent(messageId)}/replies`,
|
|
153
|
+
{ text },
|
|
154
|
+
);
|
|
155
|
+
const data = (payload as any).data ?? payload;
|
|
156
|
+
return String(data.id ?? data.message_id ?? '');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async checkInbox(): Promise<Message[]> {
|
|
160
|
+
await this.ensureRegistered();
|
|
161
|
+
const payload = await this.sendHttpAsAgent<JsonObject>('GET', '/v1/inbox');
|
|
162
|
+
const data = (payload as any).data ?? payload;
|
|
163
|
+
const messages: Message[] = [];
|
|
164
|
+
|
|
165
|
+
for (const mention of ((data as any).mentions ?? []) as JsonObject[]) {
|
|
166
|
+
messages.push(this.messageFromPayload(mention));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
for (const dm of ((data as any).unread_dms ?? []) as JsonObject[]) {
|
|
170
|
+
const last = (dm as any).last_message as JsonObject | undefined;
|
|
171
|
+
if (last?.text) {
|
|
172
|
+
messages.push({
|
|
173
|
+
sender: String(dm.from ?? dm.agent_name ?? 'unknown'),
|
|
174
|
+
text: String(last.text),
|
|
175
|
+
channel: undefined,
|
|
176
|
+
threadId: typeof dm.conversation_id === 'string' ? dm.conversation_id : undefined,
|
|
177
|
+
timestamp: typeof last.created_at === 'string' ? undefined : (last.created_at as number | undefined),
|
|
178
|
+
messageId: typeof last.id === 'string' ? last.id : undefined,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return messages;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async listAgents(): Promise<string[]> {
|
|
187
|
+
const payload = await this.sendHttp<JsonObject>('GET', '/v1/agents');
|
|
188
|
+
const data = (payload as any).data ?? (payload as any).agents ?? [];
|
|
189
|
+
if (Array.isArray(data)) {
|
|
190
|
+
return data.map((a: any) => (typeof a === 'string' ? a : String(a.name ?? a)));
|
|
191
|
+
}
|
|
192
|
+
return [];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private async ensureRegistered(): Promise<void> {
|
|
196
|
+
if (this.agentId && this.token) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
await this.registerAgent();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private requireConfig(options: { requireWorkspace?: boolean } = {}): void {
|
|
203
|
+
if (!this.config.apiKey) {
|
|
204
|
+
throw new RelayConfigError(
|
|
205
|
+
'Missing RELAY_API_KEY. Set the environment variable or pass apiKey to RelayConfig.'
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (options.requireWorkspace && !this.config.workspace) {
|
|
210
|
+
throw new RelayConfigError(
|
|
211
|
+
'Missing RELAY_WORKSPACE. Set the environment variable or pass workspace to RelayConfig.'
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private async sendHttpAsAgent<T = unknown>(
|
|
217
|
+
method: string,
|
|
218
|
+
path: string,
|
|
219
|
+
payload?: JsonObject,
|
|
220
|
+
overrideToken?: string,
|
|
221
|
+
): Promise<T> {
|
|
222
|
+
this.requireConfig();
|
|
223
|
+
|
|
224
|
+
const agentToken = overrideToken ?? this.token;
|
|
225
|
+
if (!agentToken) {
|
|
226
|
+
throw new RelayConfigError('Agent not registered; no agent token available.');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const url = `${this.config.baseUrl}${path}`;
|
|
230
|
+
const headers: Record<string, string> = {
|
|
231
|
+
authorization: `Bearer ${agentToken}`,
|
|
232
|
+
};
|
|
233
|
+
if (payload !== undefined) {
|
|
234
|
+
headers['content-type'] = 'application/json';
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
for (let attempt = 1; attempt <= HTTP_RETRY_ATTEMPTS; attempt += 1) {
|
|
238
|
+
let response: Response;
|
|
239
|
+
try {
|
|
240
|
+
response = await fetch(url, {
|
|
241
|
+
method,
|
|
242
|
+
headers,
|
|
243
|
+
body: payload === undefined ? undefined : JSON.stringify(payload),
|
|
244
|
+
});
|
|
245
|
+
} catch (error) {
|
|
246
|
+
if (attempt < HTTP_RETRY_ATTEMPTS) {
|
|
247
|
+
await sleep(Math.min(2 ** (attempt - 1) * 1_000, WS_RECONNECT_MAX_DELAY_MS));
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
throw new RelayConnectionError(0, error instanceof Error ? error.message : String(error));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (response.status === 401) {
|
|
254
|
+
throw new RelayAuthError(await this.errorMessage(response));
|
|
255
|
+
}
|
|
256
|
+
if (response.status >= 500 && response.status <= 599) {
|
|
257
|
+
const message = await this.errorMessage(response);
|
|
258
|
+
if (attempt < HTTP_RETRY_ATTEMPTS) {
|
|
259
|
+
await sleep(Math.min(2 ** (attempt - 1) * 1_000, WS_RECONNECT_MAX_DELAY_MS));
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
throw new RelayConnectionError(response.status, message);
|
|
263
|
+
}
|
|
264
|
+
if (response.status >= 400) {
|
|
265
|
+
throw new RelayConnectionError(response.status, await this.errorMessage(response));
|
|
266
|
+
}
|
|
267
|
+
if (response.status === 204) {
|
|
268
|
+
return undefined as T;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
272
|
+
if (contentType.includes('application/json')) {
|
|
273
|
+
return (await response.json()) as T;
|
|
274
|
+
}
|
|
275
|
+
return (await response.text()) as T;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
throw new RelayConnectionError(500, 'Unexpected transport retry failure');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private async sendHttp<T = unknown>(
|
|
282
|
+
method: string,
|
|
283
|
+
path: string,
|
|
284
|
+
payload?: JsonObject
|
|
285
|
+
): Promise<T> {
|
|
286
|
+
this.requireConfig();
|
|
287
|
+
|
|
288
|
+
const url = `${this.config.baseUrl}${path}`;
|
|
289
|
+
const headers: Record<string, string> = {
|
|
290
|
+
authorization: `Bearer ${this.config.apiKey}`,
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
if (payload !== undefined) {
|
|
294
|
+
headers['content-type'] = 'application/json';
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
for (let attempt = 1; attempt <= HTTP_RETRY_ATTEMPTS; attempt += 1) {
|
|
298
|
+
let response: Response;
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
response = await fetch(url, {
|
|
302
|
+
method,
|
|
303
|
+
headers,
|
|
304
|
+
body: payload === undefined ? undefined : JSON.stringify(payload),
|
|
305
|
+
});
|
|
306
|
+
} catch (error) {
|
|
307
|
+
if (attempt < HTTP_RETRY_ATTEMPTS) {
|
|
308
|
+
await sleep(Math.min(2 ** (attempt - 1) * 1_000, WS_RECONNECT_MAX_DELAY_MS));
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
throw new RelayConnectionError(0, error instanceof Error ? error.message : String(error));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (response.status === 401) {
|
|
315
|
+
throw new RelayAuthError(await this.errorMessage(response));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (response.status >= 500 && response.status <= 599) {
|
|
319
|
+
const message = await this.errorMessage(response);
|
|
320
|
+
if (attempt < HTTP_RETRY_ATTEMPTS) {
|
|
321
|
+
await sleep(Math.min(2 ** (attempt - 1) * 1_000, WS_RECONNECT_MAX_DELAY_MS));
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
throw new RelayConnectionError(response.status, message);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (response.status >= 400) {
|
|
328
|
+
throw new RelayConnectionError(response.status, await this.errorMessage(response));
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (response.status === 204) {
|
|
332
|
+
return undefined as T;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
336
|
+
if (contentType.includes('application/json')) {
|
|
337
|
+
return (await response.json()) as T;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return (await response.text()) as T;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
throw new RelayConnectionError(500, 'Unexpected transport retry failure');
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
private async connectWebSocket(): Promise<void> {
|
|
347
|
+
await this.ensureRegistered();
|
|
348
|
+
|
|
349
|
+
if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (this.wsConnectPromise) {
|
|
354
|
+
return this.wsConnectPromise;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const url = `${this.wsBaseUrl()}/v1/ws?token=${encodeURIComponent(this.token ?? '')}`;
|
|
358
|
+
const socket = new WebSocket(url);
|
|
359
|
+
|
|
360
|
+
this.wsConnectPromise = new Promise<void>((resolve, reject) => {
|
|
361
|
+
let settled = false;
|
|
362
|
+
|
|
363
|
+
socket.once('open', () => {
|
|
364
|
+
settled = true;
|
|
365
|
+
this.ws = socket;
|
|
366
|
+
this.reconnectDelayMs = 1_000;
|
|
367
|
+
resolve();
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
socket.once('error', (error) => {
|
|
371
|
+
if (!settled) {
|
|
372
|
+
settled = true;
|
|
373
|
+
reject(error);
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
}).finally(() => {
|
|
377
|
+
this.wsConnectPromise = undefined;
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
socket.on('message', (raw) => {
|
|
381
|
+
void this.dispatchWsPayload(raw.toString());
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
socket.on('close', () => {
|
|
385
|
+
if (this.ws === socket) {
|
|
386
|
+
this.ws = undefined;
|
|
387
|
+
}
|
|
388
|
+
if (!this.closing) {
|
|
389
|
+
this.scheduleReconnect();
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
socket.on('error', () => {
|
|
394
|
+
// The close handler manages reconnects for established sockets.
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
await this.wsConnectPromise;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
private wsBaseUrl(): string {
|
|
401
|
+
if (this.config.baseUrl.startsWith('https://')) {
|
|
402
|
+
return `wss://${this.config.baseUrl.slice('https://'.length)}`;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (this.config.baseUrl.startsWith('http://')) {
|
|
406
|
+
return `ws://${this.config.baseUrl.slice('http://'.length)}`;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return this.config.baseUrl;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
private scheduleReconnect(): void {
|
|
413
|
+
if (this.reconnectTimer || this.closing) {
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const delayMs = this.reconnectDelayMs;
|
|
418
|
+
this.reconnectDelayMs = Math.min(this.reconnectDelayMs * 2, WS_RECONNECT_MAX_DELAY_MS);
|
|
419
|
+
this.reconnectTimer = setTimeout(() => {
|
|
420
|
+
this.reconnectTimer = undefined;
|
|
421
|
+
void this.connectWebSocket().catch(() => {
|
|
422
|
+
this.scheduleReconnect();
|
|
423
|
+
});
|
|
424
|
+
}, delayMs);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
private clearReconnectTimer(): void {
|
|
428
|
+
if (this.reconnectTimer) {
|
|
429
|
+
clearTimeout(this.reconnectTimer);
|
|
430
|
+
this.reconnectTimer = undefined;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
private async dispatchWsPayload(rawPayload: string): Promise<void> {
|
|
435
|
+
const payload = JSON.parse(rawPayload) as JsonObject;
|
|
436
|
+
if (payload.type === 'ping') {
|
|
437
|
+
this.ws?.send(JSON.stringify({ type: 'pong' }));
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const messageEvents = new Set([
|
|
442
|
+
'message.created', 'dm.received', 'direct_message.received',
|
|
443
|
+
'thread.reply', 'message', 'group_dm.received',
|
|
444
|
+
]);
|
|
445
|
+
if (!messageEvents.has(payload.type as string) || !this.messageCallback) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
await this.messageCallback(this.messageFromPayload(payload));
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
private messageFromPayload(payload: JsonObject): Message {
|
|
453
|
+
const m = (typeof payload.message === 'object' && payload.message !== null)
|
|
454
|
+
? (payload.message as JsonObject)
|
|
455
|
+
: payload;
|
|
456
|
+
|
|
457
|
+
const sender = String(
|
|
458
|
+
m.sender ?? m.agent_name ?? m.from ?? m.agentName
|
|
459
|
+
?? payload.agent_name ?? payload.from ?? 'unknown',
|
|
460
|
+
);
|
|
461
|
+
const text = String(m.text ?? '');
|
|
462
|
+
const channel = String(
|
|
463
|
+
m.channel ?? m.channel_name ?? m.channelName ?? payload.channel ?? payload.channel_name ?? '',
|
|
464
|
+
) || undefined;
|
|
465
|
+
const threadId = String(
|
|
466
|
+
m.thread_id ?? m.threadId ?? m.conversation_id ?? m.conversationId ?? payload.thread_id ?? '',
|
|
467
|
+
) || undefined;
|
|
468
|
+
const rawTs = m.timestamp ?? m.created_at ?? m.createdAt ?? payload.timestamp;
|
|
469
|
+
const timestamp = typeof rawTs === 'number' ? rawTs : undefined;
|
|
470
|
+
const messageId = String(
|
|
471
|
+
m.id ?? m.message_id ?? m.messageId ?? payload.message_id ?? '',
|
|
472
|
+
) || undefined;
|
|
473
|
+
|
|
474
|
+
return { sender, text, channel, threadId, timestamp, messageId };
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
private async errorMessage(response: Response): Promise<string> {
|
|
478
|
+
const text = await response.text().catch(() => '');
|
|
479
|
+
try {
|
|
480
|
+
const payload = JSON.parse(text) as { message?: string; error?: { message?: string } };
|
|
481
|
+
if (typeof payload.error === 'object' && payload.error?.message) {
|
|
482
|
+
return payload.error.message;
|
|
483
|
+
}
|
|
484
|
+
return payload.message ?? response.statusText ?? 'Request failed';
|
|
485
|
+
} catch {
|
|
486
|
+
return text || response.statusText || 'Request failed';
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
export const DEFAULT_RELAY_BASE_URL = 'https://api.relaycast.dev';
|
|
2
|
+
|
|
3
|
+
/** An incoming relay message. All fields except `sender` and `text` are optional. */
|
|
4
|
+
export interface Message {
|
|
5
|
+
readonly sender: string;
|
|
6
|
+
readonly text: string;
|
|
7
|
+
readonly channel?: string;
|
|
8
|
+
readonly threadId?: string;
|
|
9
|
+
readonly timestamp?: number;
|
|
10
|
+
readonly messageId?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Callback invoked when a message is received. */
|
|
14
|
+
export type MessageCallback = (message: Message) => void | Promise<void>;
|
|
15
|
+
|
|
16
|
+
/** User-supplied relay configuration. All fields are optional and fall back to env vars or defaults. */
|
|
17
|
+
export interface RelayConfig {
|
|
18
|
+
workspace?: string;
|
|
19
|
+
apiKey?: string;
|
|
20
|
+
baseUrl?: string;
|
|
21
|
+
channels?: string[];
|
|
22
|
+
pollIntervalMs?: number;
|
|
23
|
+
autoCleanup?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ResolvedRelayConfig {
|
|
27
|
+
workspace?: string;
|
|
28
|
+
apiKey?: string;
|
|
29
|
+
baseUrl: string;
|
|
30
|
+
channels: string[];
|
|
31
|
+
pollIntervalMs: number;
|
|
32
|
+
autoCleanup: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class RelayConnectionError extends Error {
|
|
36
|
+
readonly statusCode: number;
|
|
37
|
+
|
|
38
|
+
constructor(statusCode: number, message: string) {
|
|
39
|
+
super(`${statusCode}: ${message}`);
|
|
40
|
+
this.name = 'RelayConnectionError';
|
|
41
|
+
this.statusCode = statusCode;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class RelayConfigError extends Error {
|
|
46
|
+
constructor(message: string) {
|
|
47
|
+
super(message);
|
|
48
|
+
this.name = 'RelayConfigError';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class RelayAuthError extends RelayConnectionError {
|
|
53
|
+
constructor(message = 'Unauthorized', statusCode = 401) {
|
|
54
|
+
super(statusCode, message);
|
|
55
|
+
this.name = 'RelayAuthError';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Strip trailing slashes without a quantified regex (avoids ReDoS). */
|
|
60
|
+
function trimTrailingSlashes(url: string): string {
|
|
61
|
+
let end = url.length;
|
|
62
|
+
while (end > 0 && url[end - 1] === '/') {
|
|
63
|
+
end -= 1;
|
|
64
|
+
}
|
|
65
|
+
return url.slice(0, end);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Resolve a partial config into a fully-populated config with env-var fallbacks.
|
|
70
|
+
* @param config - Partial user config.
|
|
71
|
+
* @returns Resolved config with all defaults applied.
|
|
72
|
+
*/
|
|
73
|
+
export function resolveRelayConfig(config: RelayConfig = {}): ResolvedRelayConfig {
|
|
74
|
+
return {
|
|
75
|
+
workspace: config.workspace ?? process.env.RELAY_WORKSPACE,
|
|
76
|
+
apiKey: config.apiKey ?? process.env.RELAY_API_KEY,
|
|
77
|
+
baseUrl: trimTrailingSlashes(config.baseUrl ?? process.env.RELAY_BASE_URL ?? DEFAULT_RELAY_BASE_URL),
|
|
78
|
+
channels: [...(config.channels ?? ['general'])],
|
|
79
|
+
pollIntervalMs: config.pollIntervalMs ?? 1_000,
|
|
80
|
+
autoCleanup: config.autoCleanup ?? true,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Format a single message for display in agent instructions.
|
|
86
|
+
* @param message - The relay message to format.
|
|
87
|
+
* @returns Human-readable message string.
|
|
88
|
+
*/
|
|
89
|
+
export function formatRelayMessage(message: Message): string {
|
|
90
|
+
const location = message.channel ? ` [#${message.channel}]` : '';
|
|
91
|
+
const thread = message.threadId ? ` [thread ${message.threadId}]` : '';
|
|
92
|
+
return `Relay message from ${message.sender}${location}${thread}: ${message.text}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Format an array of messages for display in agent instructions.
|
|
97
|
+
* @param messages - The relay messages to format.
|
|
98
|
+
* @returns Human-readable inbox summary.
|
|
99
|
+
*/
|
|
100
|
+
export function formatRelayInbox(messages: Message[]): string {
|
|
101
|
+
if (messages.length === 0) {
|
|
102
|
+
return 'No new relay messages.';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return messages.map((message) => formatRelayMessage(message)).join('\n');
|
|
106
|
+
}
|