@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/README.md +171 -0
- package/dist/client.d.ts +181 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +695 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol/framing.d.ts +80 -0
- package/dist/protocol/framing.d.ts.map +1 -0
- package/dist/protocol/framing.js +206 -0
- package/dist/protocol/framing.js.map +1 -0
- package/dist/protocol/index.d.ts +6 -0
- package/dist/protocol/index.d.ts.map +1 -0
- package/dist/protocol/index.js +6 -0
- package/dist/protocol/index.js.map +1 -0
- package/dist/protocol/types.d.ts +341 -0
- package/dist/protocol/types.d.ts.map +1 -0
- package/dist/protocol/types.js +8 -0
- package/dist/protocol/types.js.map +1 -0
- package/dist/standalone.d.ts +87 -0
- package/dist/standalone.d.ts.map +1 -0
- package/dist/standalone.js +126 -0
- package/dist/standalone.js.map +1 -0
- package/package.json +80 -0
package/dist/client.js
ADDED
|
@@ -0,0 +1,695 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RelayClient - Agent Relay SDK Client
|
|
3
|
+
* @agent-relay/sdk
|
|
4
|
+
*
|
|
5
|
+
* Lightweight client for agent-to-agent communication via Agent Relay daemon.
|
|
6
|
+
*/
|
|
7
|
+
import net from 'node:net';
|
|
8
|
+
import { randomUUID } from 'node:crypto';
|
|
9
|
+
import { PROTOCOL_VERSION, } from './protocol/types.js';
|
|
10
|
+
import { encodeFrameLegacy, FrameParser } from './protocol/framing.js';
|
|
11
|
+
const DEFAULT_SOCKET_PATH = '/tmp/agent-relay.sock';
|
|
12
|
+
const DEFAULT_CLIENT_CONFIG = {
|
|
13
|
+
socketPath: DEFAULT_SOCKET_PATH,
|
|
14
|
+
agentName: 'agent',
|
|
15
|
+
cli: undefined,
|
|
16
|
+
quiet: false,
|
|
17
|
+
reconnect: true,
|
|
18
|
+
maxReconnectAttempts: 10,
|
|
19
|
+
reconnectDelayMs: 1000, // Increased from 100ms to prevent reconnect storms
|
|
20
|
+
reconnectMaxDelayMs: 30000,
|
|
21
|
+
};
|
|
22
|
+
// Simple ID generator
|
|
23
|
+
let idCounter = 0;
|
|
24
|
+
function generateId() {
|
|
25
|
+
return `${Date.now().toString(36)}-${(++idCounter).toString(36)}`;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Circular buffer for O(1) deduplication with bounded memory.
|
|
29
|
+
*/
|
|
30
|
+
class CircularDedupeCache {
|
|
31
|
+
ids = new Set();
|
|
32
|
+
ring;
|
|
33
|
+
head = 0;
|
|
34
|
+
capacity;
|
|
35
|
+
constructor(capacity = 2000) {
|
|
36
|
+
this.capacity = capacity;
|
|
37
|
+
this.ring = new Array(capacity);
|
|
38
|
+
}
|
|
39
|
+
check(id) {
|
|
40
|
+
if (this.ids.has(id))
|
|
41
|
+
return true;
|
|
42
|
+
if (this.ids.size >= this.capacity) {
|
|
43
|
+
const oldest = this.ring[this.head];
|
|
44
|
+
if (oldest)
|
|
45
|
+
this.ids.delete(oldest);
|
|
46
|
+
}
|
|
47
|
+
this.ring[this.head] = id;
|
|
48
|
+
this.ids.add(id);
|
|
49
|
+
this.head = (this.head + 1) % this.capacity;
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
clear() {
|
|
53
|
+
this.ids.clear();
|
|
54
|
+
this.ring = new Array(this.capacity);
|
|
55
|
+
this.head = 0;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* RelayClient for agent-to-agent communication.
|
|
60
|
+
*/
|
|
61
|
+
export class RelayClient {
|
|
62
|
+
config;
|
|
63
|
+
socket;
|
|
64
|
+
parser;
|
|
65
|
+
_state = 'DISCONNECTED';
|
|
66
|
+
sessionId;
|
|
67
|
+
resumeToken;
|
|
68
|
+
reconnectAttempts = 0;
|
|
69
|
+
reconnectDelay;
|
|
70
|
+
reconnectTimer;
|
|
71
|
+
_destroyed = false;
|
|
72
|
+
dedupeCache = new CircularDedupeCache(2000);
|
|
73
|
+
writeQueue = [];
|
|
74
|
+
writeScheduled = false;
|
|
75
|
+
pendingSyncAcks = new Map();
|
|
76
|
+
// Event handlers
|
|
77
|
+
onMessage;
|
|
78
|
+
/**
|
|
79
|
+
* Callback for channel messages.
|
|
80
|
+
*/
|
|
81
|
+
onChannelMessage;
|
|
82
|
+
onStateChange;
|
|
83
|
+
onError;
|
|
84
|
+
constructor(config = {}) {
|
|
85
|
+
this.config = { ...DEFAULT_CLIENT_CONFIG, ...config };
|
|
86
|
+
this.parser = new FrameParser();
|
|
87
|
+
this.parser.setLegacyMode(true);
|
|
88
|
+
this.reconnectDelay = this.config.reconnectDelayMs;
|
|
89
|
+
}
|
|
90
|
+
get state() {
|
|
91
|
+
return this._state;
|
|
92
|
+
}
|
|
93
|
+
get agentName() {
|
|
94
|
+
return this.config.agentName;
|
|
95
|
+
}
|
|
96
|
+
get currentSessionId() {
|
|
97
|
+
return this.sessionId;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Connect to the relay daemon.
|
|
101
|
+
*/
|
|
102
|
+
connect() {
|
|
103
|
+
if (this._state !== 'DISCONNECTED' && this._state !== 'BACKOFF') {
|
|
104
|
+
return Promise.resolve();
|
|
105
|
+
}
|
|
106
|
+
return new Promise((resolve, reject) => {
|
|
107
|
+
let settled = false;
|
|
108
|
+
const settleResolve = () => {
|
|
109
|
+
if (settled)
|
|
110
|
+
return;
|
|
111
|
+
settled = true;
|
|
112
|
+
resolve();
|
|
113
|
+
};
|
|
114
|
+
const settleReject = (err) => {
|
|
115
|
+
if (settled)
|
|
116
|
+
return;
|
|
117
|
+
settled = true;
|
|
118
|
+
reject(err);
|
|
119
|
+
};
|
|
120
|
+
this.setState('CONNECTING');
|
|
121
|
+
this.socket = net.createConnection(this.config.socketPath, () => {
|
|
122
|
+
this.setState('HANDSHAKING');
|
|
123
|
+
this.sendHello();
|
|
124
|
+
});
|
|
125
|
+
this.socket.on('data', (data) => this.handleData(data));
|
|
126
|
+
this.socket.on('close', () => {
|
|
127
|
+
this.handleDisconnect();
|
|
128
|
+
});
|
|
129
|
+
this.socket.on('error', (err) => {
|
|
130
|
+
if (this._state === 'CONNECTING') {
|
|
131
|
+
settleReject(err);
|
|
132
|
+
}
|
|
133
|
+
this.handleError(err);
|
|
134
|
+
});
|
|
135
|
+
const checkReady = setInterval(() => {
|
|
136
|
+
if (this._state === 'READY') {
|
|
137
|
+
clearInterval(checkReady);
|
|
138
|
+
clearTimeout(timeout);
|
|
139
|
+
settleResolve();
|
|
140
|
+
}
|
|
141
|
+
}, 10);
|
|
142
|
+
const timeout = setTimeout(() => {
|
|
143
|
+
if (this._state !== 'READY') {
|
|
144
|
+
clearInterval(checkReady);
|
|
145
|
+
this.socket?.destroy();
|
|
146
|
+
settleReject(new Error('Connection timeout'));
|
|
147
|
+
}
|
|
148
|
+
}, 5000);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Disconnect from the relay daemon.
|
|
153
|
+
*/
|
|
154
|
+
disconnect() {
|
|
155
|
+
if (this.reconnectTimer) {
|
|
156
|
+
clearTimeout(this.reconnectTimer);
|
|
157
|
+
this.reconnectTimer = undefined;
|
|
158
|
+
}
|
|
159
|
+
if (this.socket) {
|
|
160
|
+
this.send({
|
|
161
|
+
v: PROTOCOL_VERSION,
|
|
162
|
+
type: 'BYE',
|
|
163
|
+
id: generateId(),
|
|
164
|
+
ts: Date.now(),
|
|
165
|
+
payload: {},
|
|
166
|
+
});
|
|
167
|
+
this.socket.end();
|
|
168
|
+
this.socket = undefined;
|
|
169
|
+
}
|
|
170
|
+
this.setState('DISCONNECTED');
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Permanently destroy the client.
|
|
174
|
+
*/
|
|
175
|
+
destroy() {
|
|
176
|
+
this._destroyed = true;
|
|
177
|
+
this.disconnect();
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Send a message to another agent.
|
|
181
|
+
*/
|
|
182
|
+
sendMessage(to, body, kind = 'message', data, thread, meta) {
|
|
183
|
+
if (this._state !== 'READY') {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
const envelope = {
|
|
187
|
+
v: PROTOCOL_VERSION,
|
|
188
|
+
type: 'SEND',
|
|
189
|
+
id: generateId(),
|
|
190
|
+
ts: Date.now(),
|
|
191
|
+
to,
|
|
192
|
+
payload: {
|
|
193
|
+
kind,
|
|
194
|
+
body,
|
|
195
|
+
data,
|
|
196
|
+
thread,
|
|
197
|
+
},
|
|
198
|
+
payload_meta: meta,
|
|
199
|
+
};
|
|
200
|
+
return this.send(envelope);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Send an ACK for a delivered message.
|
|
204
|
+
*/
|
|
205
|
+
sendAck(payload) {
|
|
206
|
+
if (this._state !== 'READY') {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
const envelope = {
|
|
210
|
+
v: PROTOCOL_VERSION,
|
|
211
|
+
type: 'ACK',
|
|
212
|
+
id: generateId(),
|
|
213
|
+
ts: Date.now(),
|
|
214
|
+
payload,
|
|
215
|
+
};
|
|
216
|
+
return this.send(envelope);
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Send a message and wait for ACK response.
|
|
220
|
+
*/
|
|
221
|
+
async sendAndWait(to, body, options = {}) {
|
|
222
|
+
if (this._state !== 'READY') {
|
|
223
|
+
throw new Error('Client not ready');
|
|
224
|
+
}
|
|
225
|
+
const correlationId = randomUUID();
|
|
226
|
+
const timeoutMs = options.timeoutMs ?? 30000;
|
|
227
|
+
const kind = options.kind ?? 'message';
|
|
228
|
+
return new Promise((resolve, reject) => {
|
|
229
|
+
const timeoutHandle = setTimeout(() => {
|
|
230
|
+
this.pendingSyncAcks.delete(correlationId);
|
|
231
|
+
reject(new Error(`ACK timeout after ${timeoutMs}ms`));
|
|
232
|
+
}, timeoutMs);
|
|
233
|
+
this.pendingSyncAcks.set(correlationId, { resolve, reject, timeoutHandle });
|
|
234
|
+
const envelope = {
|
|
235
|
+
v: PROTOCOL_VERSION,
|
|
236
|
+
type: 'SEND',
|
|
237
|
+
id: generateId(),
|
|
238
|
+
ts: Date.now(),
|
|
239
|
+
to,
|
|
240
|
+
payload: {
|
|
241
|
+
kind,
|
|
242
|
+
body,
|
|
243
|
+
data: options.data,
|
|
244
|
+
thread: options.thread,
|
|
245
|
+
},
|
|
246
|
+
payload_meta: {
|
|
247
|
+
sync: {
|
|
248
|
+
correlationId,
|
|
249
|
+
timeoutMs,
|
|
250
|
+
blocking: true,
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
const sent = this.send(envelope);
|
|
255
|
+
if (!sent) {
|
|
256
|
+
clearTimeout(timeoutHandle);
|
|
257
|
+
this.pendingSyncAcks.delete(correlationId);
|
|
258
|
+
reject(new Error('Failed to send message'));
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Broadcast a message to all agents.
|
|
264
|
+
*/
|
|
265
|
+
broadcast(body, kind = 'message', data) {
|
|
266
|
+
return this.sendMessage('*', body, kind, data);
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Subscribe to a topic.
|
|
270
|
+
*/
|
|
271
|
+
subscribe(topic) {
|
|
272
|
+
if (this._state !== 'READY')
|
|
273
|
+
return false;
|
|
274
|
+
return this.send({
|
|
275
|
+
v: PROTOCOL_VERSION,
|
|
276
|
+
type: 'SUBSCRIBE',
|
|
277
|
+
id: generateId(),
|
|
278
|
+
ts: Date.now(),
|
|
279
|
+
topic,
|
|
280
|
+
payload: {},
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Unsubscribe from a topic.
|
|
285
|
+
*/
|
|
286
|
+
unsubscribe(topic) {
|
|
287
|
+
if (this._state !== 'READY')
|
|
288
|
+
return false;
|
|
289
|
+
return this.send({
|
|
290
|
+
v: PROTOCOL_VERSION,
|
|
291
|
+
type: 'UNSUBSCRIBE',
|
|
292
|
+
id: generateId(),
|
|
293
|
+
ts: Date.now(),
|
|
294
|
+
topic,
|
|
295
|
+
payload: {},
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Bind as a shadow to a primary agent.
|
|
300
|
+
*/
|
|
301
|
+
bindAsShadow(primaryAgent, options = {}) {
|
|
302
|
+
if (this._state !== 'READY')
|
|
303
|
+
return false;
|
|
304
|
+
return this.send({
|
|
305
|
+
v: PROTOCOL_VERSION,
|
|
306
|
+
type: 'SHADOW_BIND',
|
|
307
|
+
id: generateId(),
|
|
308
|
+
ts: Date.now(),
|
|
309
|
+
payload: {
|
|
310
|
+
primaryAgent,
|
|
311
|
+
speakOn: options.speakOn,
|
|
312
|
+
receiveIncoming: options.receiveIncoming,
|
|
313
|
+
receiveOutgoing: options.receiveOutgoing,
|
|
314
|
+
},
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Unbind from a primary agent.
|
|
319
|
+
*/
|
|
320
|
+
unbindAsShadow(primaryAgent) {
|
|
321
|
+
if (this._state !== 'READY')
|
|
322
|
+
return false;
|
|
323
|
+
return this.send({
|
|
324
|
+
v: PROTOCOL_VERSION,
|
|
325
|
+
type: 'SHADOW_UNBIND',
|
|
326
|
+
id: generateId(),
|
|
327
|
+
ts: Date.now(),
|
|
328
|
+
payload: {
|
|
329
|
+
primaryAgent,
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Send log output to the daemon for dashboard streaming.
|
|
335
|
+
*/
|
|
336
|
+
sendLog(data) {
|
|
337
|
+
if (this._state !== 'READY') {
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
const envelope = {
|
|
341
|
+
v: PROTOCOL_VERSION,
|
|
342
|
+
type: 'LOG',
|
|
343
|
+
id: generateId(),
|
|
344
|
+
ts: Date.now(),
|
|
345
|
+
payload: {
|
|
346
|
+
data,
|
|
347
|
+
timestamp: Date.now(),
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
return this.send(envelope);
|
|
351
|
+
}
|
|
352
|
+
// =============================================================================
|
|
353
|
+
// Channel Operations
|
|
354
|
+
// =============================================================================
|
|
355
|
+
/**
|
|
356
|
+
* Join a channel.
|
|
357
|
+
* @param channel - Channel name (e.g., '#general', 'dm:alice:bob')
|
|
358
|
+
* @param displayName - Optional display name for this member
|
|
359
|
+
*/
|
|
360
|
+
joinChannel(channel, displayName) {
|
|
361
|
+
if (this._state !== 'READY') {
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
const envelope = {
|
|
365
|
+
v: PROTOCOL_VERSION,
|
|
366
|
+
type: 'CHANNEL_JOIN',
|
|
367
|
+
id: generateId(),
|
|
368
|
+
ts: Date.now(),
|
|
369
|
+
payload: {
|
|
370
|
+
channel,
|
|
371
|
+
displayName,
|
|
372
|
+
},
|
|
373
|
+
};
|
|
374
|
+
return this.send(envelope);
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Admin join: Add any member to a channel (does not require member to be connected).
|
|
378
|
+
* @param channel - Channel name
|
|
379
|
+
* @param member - Name of the member to add
|
|
380
|
+
*/
|
|
381
|
+
adminJoinChannel(channel, member) {
|
|
382
|
+
if (this._state !== 'READY') {
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
const envelope = {
|
|
386
|
+
v: PROTOCOL_VERSION,
|
|
387
|
+
type: 'CHANNEL_JOIN',
|
|
388
|
+
id: generateId(),
|
|
389
|
+
ts: Date.now(),
|
|
390
|
+
payload: {
|
|
391
|
+
channel,
|
|
392
|
+
member,
|
|
393
|
+
},
|
|
394
|
+
};
|
|
395
|
+
return this.send(envelope);
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Leave a channel.
|
|
399
|
+
* @param channel - Channel name to leave
|
|
400
|
+
* @param reason - Optional reason for leaving
|
|
401
|
+
*/
|
|
402
|
+
leaveChannel(channel, reason) {
|
|
403
|
+
if (this._state !== 'READY')
|
|
404
|
+
return false;
|
|
405
|
+
const envelope = {
|
|
406
|
+
v: PROTOCOL_VERSION,
|
|
407
|
+
type: 'CHANNEL_LEAVE',
|
|
408
|
+
id: generateId(),
|
|
409
|
+
ts: Date.now(),
|
|
410
|
+
payload: {
|
|
411
|
+
channel,
|
|
412
|
+
reason,
|
|
413
|
+
},
|
|
414
|
+
};
|
|
415
|
+
return this.send(envelope);
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Admin remove: Remove any member from a channel.
|
|
419
|
+
* @param channel - Channel name
|
|
420
|
+
* @param member - Name of the member to remove
|
|
421
|
+
*/
|
|
422
|
+
adminRemoveMember(channel, member) {
|
|
423
|
+
if (this._state !== 'READY') {
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
const envelope = {
|
|
427
|
+
v: PROTOCOL_VERSION,
|
|
428
|
+
type: 'CHANNEL_LEAVE',
|
|
429
|
+
id: generateId(),
|
|
430
|
+
ts: Date.now(),
|
|
431
|
+
payload: {
|
|
432
|
+
channel,
|
|
433
|
+
member,
|
|
434
|
+
},
|
|
435
|
+
};
|
|
436
|
+
return this.send(envelope);
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Send a message to a channel.
|
|
440
|
+
* @param channel - Channel name
|
|
441
|
+
* @param body - Message content
|
|
442
|
+
* @param options - Optional thread, mentions, attachments
|
|
443
|
+
*/
|
|
444
|
+
sendChannelMessage(channel, body, options) {
|
|
445
|
+
if (this._state !== 'READY') {
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
const envelope = {
|
|
449
|
+
v: PROTOCOL_VERSION,
|
|
450
|
+
type: 'CHANNEL_MESSAGE',
|
|
451
|
+
id: generateId(),
|
|
452
|
+
ts: Date.now(),
|
|
453
|
+
payload: {
|
|
454
|
+
channel,
|
|
455
|
+
body,
|
|
456
|
+
thread: options?.thread,
|
|
457
|
+
mentions: options?.mentions,
|
|
458
|
+
attachments: options?.attachments,
|
|
459
|
+
data: options?.data,
|
|
460
|
+
},
|
|
461
|
+
};
|
|
462
|
+
return this.send(envelope);
|
|
463
|
+
}
|
|
464
|
+
// Private methods
|
|
465
|
+
setState(state) {
|
|
466
|
+
this._state = state;
|
|
467
|
+
if (this.onStateChange) {
|
|
468
|
+
this.onStateChange(state);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
sendHello() {
|
|
472
|
+
const hello = {
|
|
473
|
+
v: PROTOCOL_VERSION,
|
|
474
|
+
type: 'HELLO',
|
|
475
|
+
id: generateId(),
|
|
476
|
+
ts: Date.now(),
|
|
477
|
+
payload: {
|
|
478
|
+
agent: this.config.agentName,
|
|
479
|
+
entityType: this.config.entityType,
|
|
480
|
+
cli: this.config.cli,
|
|
481
|
+
program: this.config.program,
|
|
482
|
+
model: this.config.model,
|
|
483
|
+
task: this.config.task,
|
|
484
|
+
workingDirectory: this.config.workingDirectory,
|
|
485
|
+
displayName: this.config.displayName,
|
|
486
|
+
avatarUrl: this.config.avatarUrl,
|
|
487
|
+
capabilities: {
|
|
488
|
+
ack: true,
|
|
489
|
+
resume: true,
|
|
490
|
+
max_inflight: 256,
|
|
491
|
+
supports_topics: true,
|
|
492
|
+
},
|
|
493
|
+
session: this.resumeToken ? { resume_token: this.resumeToken } : undefined,
|
|
494
|
+
},
|
|
495
|
+
};
|
|
496
|
+
this.send(hello);
|
|
497
|
+
}
|
|
498
|
+
send(envelope) {
|
|
499
|
+
if (!this.socket)
|
|
500
|
+
return false;
|
|
501
|
+
try {
|
|
502
|
+
const frame = encodeFrameLegacy(envelope);
|
|
503
|
+
this.writeQueue.push(frame);
|
|
504
|
+
if (!this.writeScheduled) {
|
|
505
|
+
this.writeScheduled = true;
|
|
506
|
+
setImmediate(() => this.flushWrites());
|
|
507
|
+
}
|
|
508
|
+
return true;
|
|
509
|
+
}
|
|
510
|
+
catch (err) {
|
|
511
|
+
this.handleError(err);
|
|
512
|
+
return false;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
flushWrites() {
|
|
516
|
+
this.writeScheduled = false;
|
|
517
|
+
if (this.writeQueue.length === 0 || !this.socket)
|
|
518
|
+
return;
|
|
519
|
+
if (this.writeQueue.length === 1) {
|
|
520
|
+
this.socket.write(this.writeQueue[0]);
|
|
521
|
+
}
|
|
522
|
+
else {
|
|
523
|
+
this.socket.write(Buffer.concat(this.writeQueue));
|
|
524
|
+
}
|
|
525
|
+
this.writeQueue = [];
|
|
526
|
+
}
|
|
527
|
+
handleData(data) {
|
|
528
|
+
try {
|
|
529
|
+
const frames = this.parser.push(data);
|
|
530
|
+
for (const frame of frames) {
|
|
531
|
+
this.processFrame(frame);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
catch (err) {
|
|
535
|
+
this.handleError(err);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
processFrame(envelope) {
|
|
539
|
+
switch (envelope.type) {
|
|
540
|
+
case 'WELCOME':
|
|
541
|
+
this.handleWelcome(envelope);
|
|
542
|
+
break;
|
|
543
|
+
case 'DELIVER':
|
|
544
|
+
this.handleDeliver(envelope);
|
|
545
|
+
break;
|
|
546
|
+
case 'CHANNEL_MESSAGE':
|
|
547
|
+
this.handleChannelMessage(envelope);
|
|
548
|
+
break;
|
|
549
|
+
case 'PING':
|
|
550
|
+
this.handlePing(envelope);
|
|
551
|
+
break;
|
|
552
|
+
case 'ACK':
|
|
553
|
+
this.handleAck(envelope);
|
|
554
|
+
break;
|
|
555
|
+
case 'ERROR':
|
|
556
|
+
this.handleErrorFrame(envelope);
|
|
557
|
+
break;
|
|
558
|
+
case 'BUSY':
|
|
559
|
+
if (!this.config.quiet) {
|
|
560
|
+
console.warn('[sdk] Server busy, backing off');
|
|
561
|
+
}
|
|
562
|
+
break;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
handleWelcome(envelope) {
|
|
566
|
+
this.sessionId = envelope.payload.session_id;
|
|
567
|
+
this.resumeToken = envelope.payload.resume_token;
|
|
568
|
+
this.reconnectAttempts = 0;
|
|
569
|
+
this.reconnectDelay = this.config.reconnectDelayMs;
|
|
570
|
+
this.setState('READY');
|
|
571
|
+
if (!this.config.quiet) {
|
|
572
|
+
console.log(`[sdk] Connected as ${this.config.agentName} (session: ${this.sessionId})`);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
handleDeliver(envelope) {
|
|
576
|
+
// Send ACK
|
|
577
|
+
this.send({
|
|
578
|
+
v: PROTOCOL_VERSION,
|
|
579
|
+
type: 'ACK',
|
|
580
|
+
id: generateId(),
|
|
581
|
+
ts: Date.now(),
|
|
582
|
+
payload: {
|
|
583
|
+
ack_id: envelope.id,
|
|
584
|
+
seq: envelope.delivery.seq,
|
|
585
|
+
},
|
|
586
|
+
});
|
|
587
|
+
const duplicate = this.dedupeCache.check(envelope.id);
|
|
588
|
+
if (duplicate) {
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
if (this.onMessage && envelope.from) {
|
|
592
|
+
this.onMessage(envelope.from, envelope.payload, envelope.id, envelope.payload_meta, envelope.delivery.originalTo);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
handleChannelMessage(envelope) {
|
|
596
|
+
const duplicate = this.dedupeCache.check(envelope.id);
|
|
597
|
+
if (duplicate) {
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
// Notify channel message handler
|
|
601
|
+
if (this.onChannelMessage && envelope.from) {
|
|
602
|
+
this.onChannelMessage(envelope.from, envelope.payload.channel, envelope.payload.body, envelope);
|
|
603
|
+
}
|
|
604
|
+
// Also call onMessage for backwards compatibility
|
|
605
|
+
if (this.onMessage && envelope.from) {
|
|
606
|
+
const sendPayload = {
|
|
607
|
+
kind: 'message',
|
|
608
|
+
body: envelope.payload.body,
|
|
609
|
+
data: {
|
|
610
|
+
_isChannelMessage: true,
|
|
611
|
+
_channel: envelope.payload.channel,
|
|
612
|
+
_mentions: envelope.payload.mentions,
|
|
613
|
+
},
|
|
614
|
+
thread: envelope.payload.thread,
|
|
615
|
+
};
|
|
616
|
+
this.onMessage(envelope.from, sendPayload, envelope.id, undefined, envelope.payload.channel);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
handleAck(envelope) {
|
|
620
|
+
const correlationId = envelope.payload.correlationId;
|
|
621
|
+
if (!correlationId)
|
|
622
|
+
return;
|
|
623
|
+
const pending = this.pendingSyncAcks.get(correlationId);
|
|
624
|
+
if (!pending)
|
|
625
|
+
return;
|
|
626
|
+
clearTimeout(pending.timeoutHandle);
|
|
627
|
+
this.pendingSyncAcks.delete(correlationId);
|
|
628
|
+
pending.resolve(envelope.payload);
|
|
629
|
+
}
|
|
630
|
+
handlePing(envelope) {
|
|
631
|
+
this.send({
|
|
632
|
+
v: PROTOCOL_VERSION,
|
|
633
|
+
type: 'PONG',
|
|
634
|
+
id: generateId(),
|
|
635
|
+
ts: Date.now(),
|
|
636
|
+
payload: envelope.payload ?? {},
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
handleErrorFrame(envelope) {
|
|
640
|
+
if (!this.config.quiet) {
|
|
641
|
+
console.error('[sdk] Server error:', envelope.payload);
|
|
642
|
+
}
|
|
643
|
+
if (envelope.payload.code === 'RESUME_TOO_OLD') {
|
|
644
|
+
this.resumeToken = undefined;
|
|
645
|
+
this.sessionId = undefined;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
handleDisconnect() {
|
|
649
|
+
this.parser.reset();
|
|
650
|
+
this.socket = undefined;
|
|
651
|
+
this.rejectPendingSyncAcks(new Error('Disconnected while awaiting ACK'));
|
|
652
|
+
if (this._destroyed) {
|
|
653
|
+
this.setState('DISCONNECTED');
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
if (this.config.reconnect && this.reconnectAttempts < this.config.maxReconnectAttempts) {
|
|
657
|
+
this.scheduleReconnect();
|
|
658
|
+
}
|
|
659
|
+
else {
|
|
660
|
+
this.setState('DISCONNECTED');
|
|
661
|
+
if (this.reconnectAttempts >= this.config.maxReconnectAttempts && !this.config.quiet) {
|
|
662
|
+
console.error(`[sdk] Max reconnect attempts reached (${this.config.maxReconnectAttempts}), giving up`);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
handleError(error) {
|
|
667
|
+
if (!this.config.quiet) {
|
|
668
|
+
console.error('[sdk] Error:', error.message);
|
|
669
|
+
}
|
|
670
|
+
if (this.onError) {
|
|
671
|
+
this.onError(error);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
rejectPendingSyncAcks(error) {
|
|
675
|
+
for (const [correlationId, pending] of this.pendingSyncAcks.entries()) {
|
|
676
|
+
clearTimeout(pending.timeoutHandle);
|
|
677
|
+
pending.reject(error);
|
|
678
|
+
this.pendingSyncAcks.delete(correlationId);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
scheduleReconnect() {
|
|
682
|
+
this.setState('BACKOFF');
|
|
683
|
+
this.reconnectAttempts++;
|
|
684
|
+
const jitter = Math.random() * 0.3 + 0.85;
|
|
685
|
+
const delay = Math.min(this.reconnectDelay * jitter, this.config.reconnectMaxDelayMs);
|
|
686
|
+
this.reconnectDelay *= 2;
|
|
687
|
+
if (!this.config.quiet) {
|
|
688
|
+
console.log(`[sdk] Reconnecting in ${Math.round(delay)}ms (attempt ${this.reconnectAttempts})`);
|
|
689
|
+
}
|
|
690
|
+
this.reconnectTimer = setTimeout(() => {
|
|
691
|
+
this.connect().catch(() => { });
|
|
692
|
+
}, delay);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
//# sourceMappingURL=client.js.map
|