agent-relay-sdk 0.1.0
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/bus-client.d.ts +82 -0
- package/dist/bus-client.d.ts.map +1 -0
- package/dist/bus-client.js +277 -0
- package/dist/bus-client.js.map +1 -0
- package/dist/http-client.d.ts +49 -0
- package/dist/http-client.d.ts.map +1 -0
- package/dist/http-client.js +143 -0
- package/dist/http-client.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol.d.ts +128 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +143 -0
- package/dist/protocol.js.map +1 -0
- package/dist/provider-base.d.ts +47 -0
- package/dist/provider-base.d.ts.map +1 -0
- package/dist/provider-base.js +66 -0
- package/dist/provider-base.js.map +1 -0
- package/dist/reconnect.d.ts +17 -0
- package/dist/reconnect.d.ts.map +1 -0
- package/dist/reconnect.js +47 -0
- package/dist/reconnect.js.map +1 -0
- package/dist/types.d.ts +642 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +58 -0
- package/src/bus-client.ts +337 -0
- package/src/http-client.ts +158 -0
- package/src/index.ts +6 -0
- package/src/protocol.ts +286 -0
- package/src/provider-base.ts +101 -0
- package/src/reconnect.ts +62 -0
- package/src/types.ts +740 -0
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agent-relay-sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "TypeScript SDK for Agent Relay — WebSocket bus client, HTTP client, and shared types",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "AGPL-3.0-or-later",
|
|
7
|
+
"author": "Edin Mujkanovic <edin@exelerus.com>",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/edimuj/agent-relay.git",
|
|
11
|
+
"directory": "sdk"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"agent-relay",
|
|
15
|
+
"ai-agents",
|
|
16
|
+
"multi-agent",
|
|
17
|
+
"websocket",
|
|
18
|
+
"message-bus",
|
|
19
|
+
"sdk"
|
|
20
|
+
],
|
|
21
|
+
"main": "./dist/index.js",
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"import": "./dist/index.js"
|
|
27
|
+
},
|
|
28
|
+
"./types": {
|
|
29
|
+
"types": "./dist/types.d.ts",
|
|
30
|
+
"import": "./dist/types.js"
|
|
31
|
+
},
|
|
32
|
+
"./protocol": {
|
|
33
|
+
"types": "./dist/protocol.d.ts",
|
|
34
|
+
"import": "./dist/protocol.js"
|
|
35
|
+
},
|
|
36
|
+
"./http-client": {
|
|
37
|
+
"types": "./dist/http-client.d.ts",
|
|
38
|
+
"import": "./dist/http-client.js"
|
|
39
|
+
},
|
|
40
|
+
"./bus-client": {
|
|
41
|
+
"types": "./dist/bus-client.d.ts",
|
|
42
|
+
"import": "./dist/bus-client.js"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"files": [
|
|
46
|
+
"dist",
|
|
47
|
+
"src",
|
|
48
|
+
"!src/**/*.test.ts"
|
|
49
|
+
],
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "tsc -p tsconfig.json",
|
|
52
|
+
"prepack": "bun run build"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@types/bun": "latest",
|
|
56
|
+
"typescript": "^5"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import {
|
|
3
|
+
type BusAgentStatus,
|
|
4
|
+
type BusRole,
|
|
5
|
+
type ErrorFrame,
|
|
6
|
+
type EventFrame,
|
|
7
|
+
type RegisteredFrame,
|
|
8
|
+
type ServerBusFrame,
|
|
9
|
+
} from "./protocol.js";
|
|
10
|
+
import { ReconnectionManager } from "./reconnect.js";
|
|
11
|
+
|
|
12
|
+
export interface CursorStore {
|
|
13
|
+
load(): Promise<number | null>;
|
|
14
|
+
save(cursor: number): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface BusClientOptions {
|
|
18
|
+
url: string;
|
|
19
|
+
role: BusRole;
|
|
20
|
+
componentId: string;
|
|
21
|
+
agentId?: string;
|
|
22
|
+
instanceId: string;
|
|
23
|
+
token?: string;
|
|
24
|
+
capabilities?: string[];
|
|
25
|
+
tags?: string[];
|
|
26
|
+
machine?: string;
|
|
27
|
+
meta?: Record<string, unknown>;
|
|
28
|
+
reconnect?: {
|
|
29
|
+
initialMs?: number;
|
|
30
|
+
maxMs?: number;
|
|
31
|
+
jitterMs?: number;
|
|
32
|
+
};
|
|
33
|
+
heartbeatIntervalMs?: number;
|
|
34
|
+
cursorStore?: CursorStore;
|
|
35
|
+
reconcile?: () => Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface BusClientEvents {
|
|
39
|
+
connected: () => void;
|
|
40
|
+
disconnected: (reason: string) => void;
|
|
41
|
+
reconnecting: (attempt: number, delayMs: number) => void;
|
|
42
|
+
registered: (epoch: number, cursor: number) => void;
|
|
43
|
+
event: (eventType: string, data: Record<string, unknown>, seq: number) => void;
|
|
44
|
+
command: (commandType: string, params: Record<string, unknown>, commandId: string) => void;
|
|
45
|
+
error: (code: string, message: string) => void;
|
|
46
|
+
"message.new": (message: unknown) => void;
|
|
47
|
+
"agent.status": (data: { id: string; status: string; previousStatus?: string }) => void;
|
|
48
|
+
"task.changed": (task: unknown) => void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface PendingFrame {
|
|
52
|
+
resolve(value: unknown): void;
|
|
53
|
+
reject(error: Error): void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
class MemoryCursorStore implements CursorStore {
|
|
57
|
+
private value: number | null = null;
|
|
58
|
+
async load(): Promise<number | null> {
|
|
59
|
+
return this.value;
|
|
60
|
+
}
|
|
61
|
+
async save(cursor: number): Promise<void> {
|
|
62
|
+
this.value = cursor;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export class RelayBusClient extends EventEmitter {
|
|
67
|
+
private ws: WebSocket | null = null;
|
|
68
|
+
private registeredEpoch = 0;
|
|
69
|
+
private lastCursor = 0;
|
|
70
|
+
private serverCursor = 0;
|
|
71
|
+
private closed = false;
|
|
72
|
+
private connecting: Promise<void> | null = null;
|
|
73
|
+
private heartbeat?: Timer;
|
|
74
|
+
private readonly reconnect: ReconnectionManager;
|
|
75
|
+
private readonly cursorStore: CursorStore;
|
|
76
|
+
private readonly subscriptions = new Set<string>(["message.*", "agent.status", "task.*"]);
|
|
77
|
+
private readonly pending = new Map<string, PendingFrame>();
|
|
78
|
+
|
|
79
|
+
constructor(private readonly options: BusClientOptions) {
|
|
80
|
+
super();
|
|
81
|
+
this.reconnect = new ReconnectionManager({
|
|
82
|
+
initialMs: options.reconnect?.initialMs,
|
|
83
|
+
maxMs: options.reconnect?.maxMs,
|
|
84
|
+
jitterMs: options.reconnect?.jitterMs,
|
|
85
|
+
});
|
|
86
|
+
this.cursorStore = options.cursorStore ?? new MemoryCursorStore();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
get connected(): boolean {
|
|
90
|
+
return this.ws?.readyState === WebSocket.OPEN && this.registeredEpoch > 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
get epoch(): number {
|
|
94
|
+
return this.registeredEpoch;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
get cursor(): number {
|
|
98
|
+
return this.lastCursor;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async connect(): Promise<void> {
|
|
102
|
+
if (this.connecting) return this.connecting;
|
|
103
|
+
this.closed = false;
|
|
104
|
+
this.connecting = this.connectLoop().finally(() => {
|
|
105
|
+
this.connecting = null;
|
|
106
|
+
});
|
|
107
|
+
return this.connecting;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async close(): Promise<void> {
|
|
111
|
+
this.closed = true;
|
|
112
|
+
this.stopHeartbeat();
|
|
113
|
+
this.reconnect.cancel();
|
|
114
|
+
for (const pending of this.pending.values()) pending.reject(new Error("bus client closed"));
|
|
115
|
+
this.pending.clear();
|
|
116
|
+
if (this.ws && this.ws.readyState <= WebSocket.OPEN) this.ws.close();
|
|
117
|
+
this.ws = null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
status(update: { agentStatus: string; ready?: boolean; meta?: Record<string, unknown> }): void {
|
|
121
|
+
this.send({
|
|
122
|
+
type: "status",
|
|
123
|
+
payload: {
|
|
124
|
+
agentStatus: update.agentStatus as BusAgentStatus,
|
|
125
|
+
ready: update.ready,
|
|
126
|
+
meta: update.meta,
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
command(type: string, target: string, params: Record<string, unknown>): Promise<unknown> {
|
|
132
|
+
const id = crypto.randomUUID();
|
|
133
|
+
this.send({ type: "command", id, payload: { commandType: type, target, params } });
|
|
134
|
+
return new Promise((resolve, reject) => {
|
|
135
|
+
this.pending.set(id, { resolve, reject });
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async subscribe(events: string[], scopes?: string[]): Promise<void> {
|
|
140
|
+
for (const event of events) this.subscriptions.add(event);
|
|
141
|
+
const id = crypto.randomUUID();
|
|
142
|
+
this.send({ type: "subscribe", id, payload: { events, scopes } });
|
|
143
|
+
await Promise.resolve();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
override on<K extends keyof BusClientEvents>(event: K, listener: BusClientEvents[K]): this {
|
|
147
|
+
return super.on(event, listener);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
override off<K extends keyof BusClientEvents>(event: K, listener: BusClientEvents[K]): this {
|
|
151
|
+
return super.off(event, listener);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
override once<K extends keyof BusClientEvents>(event: K, listener: BusClientEvents[K]): this {
|
|
155
|
+
return super.once(event, listener);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private async connectLoop(): Promise<void> {
|
|
159
|
+
while (!this.closed) {
|
|
160
|
+
try {
|
|
161
|
+
await this.openOnce();
|
|
162
|
+
this.reconnect.reset();
|
|
163
|
+
return;
|
|
164
|
+
} catch (error) {
|
|
165
|
+
if (this.closed) throw error;
|
|
166
|
+
const delayMs = this.reconnect.nextDelay();
|
|
167
|
+
this.emit("reconnecting", this.reconnect.attempt, delayMs);
|
|
168
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private async openOnce(): Promise<void> {
|
|
174
|
+
const cursor = await this.cursorStore.load();
|
|
175
|
+
if (cursor !== null) this.lastCursor = cursor;
|
|
176
|
+
|
|
177
|
+
await new Promise<void>((resolve, reject) => {
|
|
178
|
+
const ws = new WebSocket(this.wsUrl());
|
|
179
|
+
this.ws = ws;
|
|
180
|
+
let resolved = false;
|
|
181
|
+
|
|
182
|
+
ws.onopen = () => {
|
|
183
|
+
this.emit("connected");
|
|
184
|
+
this.sendRegister();
|
|
185
|
+
};
|
|
186
|
+
ws.onmessage = (message) => {
|
|
187
|
+
void this.handleMessage(message.data, () => {
|
|
188
|
+
if (!resolved) {
|
|
189
|
+
resolved = true;
|
|
190
|
+
resolve();
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
};
|
|
194
|
+
ws.onerror = () => {
|
|
195
|
+
if (!resolved) reject(new Error("bus connection failed"));
|
|
196
|
+
};
|
|
197
|
+
ws.onclose = (event) => {
|
|
198
|
+
this.stopHeartbeat();
|
|
199
|
+
this.registeredEpoch = 0;
|
|
200
|
+
const reason = event.reason || `closed:${event.code}`;
|
|
201
|
+
this.emit("disconnected", reason);
|
|
202
|
+
if (!resolved) {
|
|
203
|
+
resolved = true;
|
|
204
|
+
reject(new Error(reason));
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (!this.closed) void this.connect();
|
|
208
|
+
};
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private sendRegister(): void {
|
|
213
|
+
this.send({
|
|
214
|
+
type: "register",
|
|
215
|
+
id: crypto.randomUUID(),
|
|
216
|
+
payload: {
|
|
217
|
+
role: this.options.role,
|
|
218
|
+
componentId: this.options.componentId,
|
|
219
|
+
agentId: this.options.agentId,
|
|
220
|
+
instanceId: this.options.instanceId,
|
|
221
|
+
capabilities: this.options.capabilities ?? [],
|
|
222
|
+
tags: this.options.tags ?? [],
|
|
223
|
+
machine: this.options.machine ?? "unknown",
|
|
224
|
+
meta: this.options.meta ?? {},
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private async handleMessage(data: unknown, onRegistered: () => void): Promise<void> {
|
|
230
|
+
let frame: ServerBusFrame;
|
|
231
|
+
try {
|
|
232
|
+
frame = JSON.parse(typeof data === "string" ? data : String(data)) as ServerBusFrame;
|
|
233
|
+
} catch {
|
|
234
|
+
this.emit("error", "INVALID_JSON", "server sent invalid JSON");
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (frame.type === "registered") {
|
|
239
|
+
const payload = (frame as RegisteredFrame).payload;
|
|
240
|
+
const resumeFrom = this.lastCursor;
|
|
241
|
+
this.registeredEpoch = payload.epoch;
|
|
242
|
+
this.serverCursor = payload.cursor;
|
|
243
|
+
this.emit("registered", payload.epoch, payload.cursor);
|
|
244
|
+
this.send({ type: "subscribe", id: crypto.randomUUID(), payload: { events: [...this.subscriptions] } });
|
|
245
|
+
if (resumeFrom > 0 && resumeFrom < payload.cursor) {
|
|
246
|
+
this.send({ type: "resume", id: crypto.randomUUID(), payload: { since: resumeFrom } });
|
|
247
|
+
} else {
|
|
248
|
+
this.lastCursor = payload.cursor;
|
|
249
|
+
await this.cursorStore.save(this.lastCursor);
|
|
250
|
+
}
|
|
251
|
+
this.startHeartbeat();
|
|
252
|
+
onRegistered();
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (frame.type === "event") {
|
|
257
|
+
await this.handleEvent((frame as EventFrame).payload);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (frame.type === "resumed") {
|
|
262
|
+
for (const event of frame.payload.events) await this.handleEvent(event);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (frame.type === "command.result") {
|
|
267
|
+
const pending = this.pending.get(frame.payload.commandId);
|
|
268
|
+
if (!pending) return;
|
|
269
|
+
this.pending.delete(frame.payload.commandId);
|
|
270
|
+
if (frame.payload.status === "succeeded") pending.resolve(frame.payload.result ?? null);
|
|
271
|
+
else pending.reject(new Error(frame.payload.error || frame.payload.status));
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (frame.type === "error") {
|
|
276
|
+
const error = frame as ErrorFrame;
|
|
277
|
+
if (error.payload.code === "REPLAY_GAP") {
|
|
278
|
+
await this.options.reconcile?.();
|
|
279
|
+
this.lastCursor = this.serverCursor;
|
|
280
|
+
await this.cursorStore.save(this.lastCursor);
|
|
281
|
+
}
|
|
282
|
+
this.emit("error", error.payload.code, error.payload.message);
|
|
283
|
+
if (error.payload.frameId) {
|
|
284
|
+
const pending = this.pending.get(error.payload.frameId);
|
|
285
|
+
if (pending) {
|
|
286
|
+
this.pending.delete(error.payload.frameId);
|
|
287
|
+
pending.reject(new Error(error.payload.message));
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private async handleEvent(event: EventFrame["payload"]): Promise<void> {
|
|
294
|
+
this.lastCursor = Math.max(this.lastCursor, event.seq);
|
|
295
|
+
await this.cursorStore.save(this.lastCursor);
|
|
296
|
+
this.emit("event", event.eventType, event.data, event.seq);
|
|
297
|
+
if (event.eventType === "command.requested") {
|
|
298
|
+
const command = isRecord(event.data.command) ? event.data.command : undefined;
|
|
299
|
+
if (command) {
|
|
300
|
+
const commandType = typeof command.type === "string" ? command.type : "";
|
|
301
|
+
const commandId = typeof command.id === "string" ? command.id : "";
|
|
302
|
+
const params = isRecord(command.params) ? command.params : {};
|
|
303
|
+
this.emit("command", commandType, params, commandId);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
this.emit(event.eventType as keyof BusClientEvents, event.data as never);
|
|
307
|
+
if (event.eventType.startsWith("task.")) this.emit("task.changed", event.data);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
private startHeartbeat(): void {
|
|
311
|
+
this.stopHeartbeat();
|
|
312
|
+
this.heartbeat = setInterval(() => {
|
|
313
|
+
this.send({ type: "heartbeat", payload: { status: "online" } });
|
|
314
|
+
}, this.options.heartbeatIntervalMs ?? 30_000);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private stopHeartbeat(): void {
|
|
318
|
+
if (this.heartbeat) clearInterval(this.heartbeat);
|
|
319
|
+
this.heartbeat = undefined;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
private send(frame: Record<string, unknown>): void {
|
|
323
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
|
|
324
|
+
this.ws.send(JSON.stringify(frame));
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private wsUrl(): string {
|
|
328
|
+
const url = new URL(this.options.url);
|
|
329
|
+
const token = this.options.token ?? process.env.AGENT_RELAY_TOKEN;
|
|
330
|
+
if (token && !url.searchParams.has("token")) url.searchParams.set("token", token);
|
|
331
|
+
return url.toString();
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
336
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
337
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import type { AgentCard, PollQuery, RegisterAgentInput, SendMessageInput, Message } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export interface HttpClientOptions {
|
|
4
|
+
baseUrl: string;
|
|
5
|
+
token?: string;
|
|
6
|
+
timeout?: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class RelayHttpError extends Error {
|
|
10
|
+
constructor(
|
|
11
|
+
readonly method: string,
|
|
12
|
+
readonly path: string,
|
|
13
|
+
readonly status: number,
|
|
14
|
+
readonly statusText: string,
|
|
15
|
+
readonly body: string,
|
|
16
|
+
) {
|
|
17
|
+
super(`relay ${method} ${path} failed: ${status}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class RelayHttpClient {
|
|
22
|
+
private readonly baseUrl: string;
|
|
23
|
+
private readonly token?: string;
|
|
24
|
+
private readonly timeout: number;
|
|
25
|
+
|
|
26
|
+
constructor(options: HttpClientOptions) {
|
|
27
|
+
this.baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
28
|
+
this.token = options.token;
|
|
29
|
+
this.timeout = options.timeout ?? 10_000;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
registerAgent(input: RegisterAgentInput): Promise<AgentCard> {
|
|
33
|
+
return this.json("POST", "/api/agents", input) as Promise<AgentCard>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async getAgent(id: string): Promise<AgentCard | null> {
|
|
37
|
+
const response = await this.request("GET", `/api/agents/${encodeURIComponent(id)}`);
|
|
38
|
+
if (response.status === 404) return null;
|
|
39
|
+
return this.parseResponse(response, "GET", `/api/agents/${id}`) as Promise<AgentCard>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
listAgents(filter: { tag?: string; machine?: string; status?: string } = {}): Promise<AgentCard[]> {
|
|
43
|
+
const url = this.url("/api/agents");
|
|
44
|
+
for (const [key, value] of Object.entries(filter)) {
|
|
45
|
+
if (value) url.searchParams.set(key, value);
|
|
46
|
+
}
|
|
47
|
+
return this.jsonUrl("GET", url) as Promise<AgentCard[]>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async heartbeat(agentId: string): Promise<void> {
|
|
51
|
+
await this.json("POST", `/api/agents/${encodeURIComponent(agentId)}/heartbeat`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async setStatus(agentId: string, status: string): Promise<void> {
|
|
55
|
+
await this.json("PATCH", `/api/agents/${encodeURIComponent(agentId)}/status`, { status });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async setReady(agentId: string, ready: boolean): Promise<void> {
|
|
59
|
+
await this.json("PATCH", `/api/agents/${encodeURIComponent(agentId)}/ready`, { ready });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async setLabel(agentId: string, label: string | null): Promise<void> {
|
|
63
|
+
await this.json("PATCH", `/api/agents/${encodeURIComponent(agentId)}/label`, { label });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async deleteAgent(agentId: string): Promise<void> {
|
|
67
|
+
await this.json("DELETE", `/api/agents/${encodeURIComponent(agentId)}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
sendMessage(input: SendMessageInput): Promise<Message> {
|
|
71
|
+
return this.json("POST", "/api/messages", input) as Promise<Message>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
pollMessages(query: PollQuery): Promise<Message[]> {
|
|
75
|
+
const url = this.url("/api/messages");
|
|
76
|
+
url.searchParams.set("for", query.for);
|
|
77
|
+
if (query.since !== undefined) url.searchParams.set("since", String(query.since));
|
|
78
|
+
if (query.sinceId !== undefined) url.searchParams.set("sinceId", String(query.sinceId));
|
|
79
|
+
if (query.unread !== undefined) url.searchParams.set("unread", String(query.unread));
|
|
80
|
+
if (query.channel) url.searchParams.set("channel", query.channel);
|
|
81
|
+
if (query.limit !== undefined) url.searchParams.set("limit", String(query.limit));
|
|
82
|
+
return this.jsonUrl("GET", url) as Promise<Message[]>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async getMessage(id: number): Promise<Message | null> {
|
|
86
|
+
const path = `/api/messages/${id}`;
|
|
87
|
+
const response = await this.request("GET", path);
|
|
88
|
+
if (response.status === 404) return null;
|
|
89
|
+
return this.parseResponse(response, "GET", path) as Promise<Message>;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
getThread(messageId: number): Promise<Message[]> {
|
|
93
|
+
return this.json("GET", `/api/messages/${messageId}/thread`) as Promise<Message[]>;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async claimMessage(messageId: number, agentId: string): Promise<boolean> {
|
|
97
|
+
const path = `/api/messages/${messageId}/claim`;
|
|
98
|
+
const response = await this.request("POST", path, { agentId });
|
|
99
|
+
if (response.ok) return true;
|
|
100
|
+
if ([400, 404, 409].includes(response.status)) return false;
|
|
101
|
+
await this.parseResponse(response, "POST", path);
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async markRead(messageId: number, agentId: string): Promise<void> {
|
|
106
|
+
await this.json("PATCH", `/api/messages/${messageId}`, { readBy: agentId });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async deleteMessage(messageId: number): Promise<void> {
|
|
110
|
+
await this.json("DELETE", `/api/messages/${messageId}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
getCursor(): Promise<{ latestId: number }> {
|
|
114
|
+
return this.json("GET", "/api/messages/cursor") as Promise<{ latestId: number }>;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private json(method: string, path: string, body?: unknown): Promise<unknown> {
|
|
118
|
+
return this.jsonUrl(method, this.url(path), body);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private async jsonUrl(method: string, url: URL, body?: unknown): Promise<unknown> {
|
|
122
|
+
const response = await this.request(method, url, body);
|
|
123
|
+
return this.parseResponse(response, method, url.pathname);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private async request(method: string, pathOrUrl: string | URL, body?: unknown): Promise<Response> {
|
|
127
|
+
const controller = new AbortController();
|
|
128
|
+
const timeout = setTimeout(() => controller.abort(), this.timeout);
|
|
129
|
+
try {
|
|
130
|
+
return await fetch(pathOrUrl instanceof URL ? pathOrUrl : this.url(pathOrUrl), {
|
|
131
|
+
method,
|
|
132
|
+
headers: this.headers(body === undefined ? undefined : { "Content-Type": "application/json" }),
|
|
133
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
134
|
+
signal: controller.signal,
|
|
135
|
+
});
|
|
136
|
+
} finally {
|
|
137
|
+
clearTimeout(timeout);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private async parseResponse(response: Response, method: string, path: string): Promise<unknown> {
|
|
142
|
+
if (!response.ok) {
|
|
143
|
+
const text = await response.text();
|
|
144
|
+
throw new RelayHttpError(method, path, response.status, response.statusText, text);
|
|
145
|
+
}
|
|
146
|
+
if (response.status === 204) return null;
|
|
147
|
+
return response.json().catch(() => null);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private headers(base: Record<string, string> = {}): Record<string, string> {
|
|
151
|
+
const token = this.token ?? process.env.AGENT_RELAY_TOKEN;
|
|
152
|
+
return token ? { ...base, "X-Agent-Relay-Token": token } : base;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private url(path: string): URL {
|
|
156
|
+
return new URL(path, this.baseUrl);
|
|
157
|
+
}
|
|
158
|
+
}
|