@love-moon/conductor-sdk 0.2.14 → 0.2.16
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/client.d.ts +21 -0
- package/dist/client.js +145 -19
- package/dist/ws/client.d.ts +3 -0
- package/dist/ws/client.js +16 -0
- package/package.json +3 -2
package/dist/client.d.ts
CHANGED
|
@@ -52,6 +52,8 @@ export declare class ConductorClient {
|
|
|
52
52
|
private readonly agentHost;
|
|
53
53
|
private readonly onStopTask?;
|
|
54
54
|
private closed;
|
|
55
|
+
private pendingOutbound;
|
|
56
|
+
private readonly ACK_TIMEOUT_MS;
|
|
55
57
|
constructor(init: ConductorClientInit);
|
|
56
58
|
static connect(options?: ConductorClientConnectOptions): Promise<ConductorClient>;
|
|
57
59
|
close(): Promise<void>;
|
|
@@ -86,6 +88,25 @@ export declare class ConductorClient {
|
|
|
86
88
|
matchProjectByPath(payload?: Record<string, any>): Promise<Record<string, any>>;
|
|
87
89
|
bindProjectPath(projectId: string, payload?: Record<string, any>): Promise<Record<string, any>>;
|
|
88
90
|
private readonly handleBackendEvent;
|
|
91
|
+
/**
|
|
92
|
+
* Handle acknowledgment messages from backend for confirmable outbound messages
|
|
93
|
+
*/
|
|
94
|
+
private handleAcknowledgment;
|
|
95
|
+
/**
|
|
96
|
+
* Send a confirmable message and wait for acknowledgment from backend.
|
|
97
|
+
* Implements reliable delivery with idempotency.
|
|
98
|
+
*/
|
|
99
|
+
private sendConfirmable;
|
|
100
|
+
/**
|
|
101
|
+
* Actually send a confirmable message over websocket.
|
|
102
|
+
* Called initially and on reconnection.
|
|
103
|
+
*/
|
|
104
|
+
private doSendConfirmable;
|
|
105
|
+
/**
|
|
106
|
+
* Flush all pending confirmable messages.
|
|
107
|
+
* Called after websocket reconnection.
|
|
108
|
+
*/
|
|
109
|
+
private flushPendingOutbound;
|
|
89
110
|
private sendEnvelope;
|
|
90
111
|
private maybeAckInboundCommand;
|
|
91
112
|
private handleStopTaskEvent;
|
package/dist/client.js
CHANGED
|
@@ -18,6 +18,8 @@ export class ConductorClient {
|
|
|
18
18
|
agentHost;
|
|
19
19
|
onStopTask;
|
|
20
20
|
closed = false;
|
|
21
|
+
pendingOutbound = new Map();
|
|
22
|
+
ACK_TIMEOUT_MS = 30000; // 30 seconds timeout for acknowledgment
|
|
21
23
|
constructor(init) {
|
|
22
24
|
this.config = init.config;
|
|
23
25
|
this.env = init.env;
|
|
@@ -45,6 +47,10 @@ export class ConductorClient {
|
|
|
45
47
|
hostName: agentHost,
|
|
46
48
|
onConnected: options.onConnected,
|
|
47
49
|
onDisconnected: options.onDisconnected,
|
|
50
|
+
onReconnected: () => {
|
|
51
|
+
// Resend pending confirmable messages after reconnection
|
|
52
|
+
void client.flushPendingOutbound();
|
|
53
|
+
},
|
|
48
54
|
});
|
|
49
55
|
const client = new ConductorClient({
|
|
50
56
|
config,
|
|
@@ -145,15 +151,15 @@ export class ConductorClient {
|
|
|
145
151
|
};
|
|
146
152
|
}
|
|
147
153
|
async sendMessage(taskId, content, metadata) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
return { delivered: true };
|
|
154
|
+
const messageId = safeRandomUuid();
|
|
155
|
+
const payload = {
|
|
156
|
+
task_id: taskId,
|
|
157
|
+
content,
|
|
158
|
+
metadata,
|
|
159
|
+
message_id: messageId,
|
|
160
|
+
};
|
|
161
|
+
await this.sendConfirmable('sdk_message', payload, messageId);
|
|
162
|
+
return { delivered: true, message_id: messageId };
|
|
157
163
|
}
|
|
158
164
|
async sendTaskStatus(taskId, payload) {
|
|
159
165
|
await this.sendEnvelope({
|
|
@@ -210,16 +216,14 @@ export class ConductorClient {
|
|
|
210
216
|
if (!requestId) {
|
|
211
217
|
throw new Error('request_id is required');
|
|
212
218
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
});
|
|
222
|
-
return { delivered: true };
|
|
219
|
+
const envelopePayload = {
|
|
220
|
+
request_id: requestId,
|
|
221
|
+
task_id: payload.task_id,
|
|
222
|
+
event_type: payload.event_type,
|
|
223
|
+
accepted: payload.accepted !== false,
|
|
224
|
+
};
|
|
225
|
+
await this.sendConfirmable('agent_command_ack', envelopePayload, requestId);
|
|
226
|
+
return { delivered: true, request_id: requestId };
|
|
223
227
|
}
|
|
224
228
|
async sendTaskStopAck(payload) {
|
|
225
229
|
const taskId = String(payload.task_id || '').trim();
|
|
@@ -418,6 +422,8 @@ export class ConductorClient {
|
|
|
418
422
|
};
|
|
419
423
|
}
|
|
420
424
|
handleBackendEvent = async (payload) => {
|
|
425
|
+
// First handle acknowledgments for confirmable outbound messages
|
|
426
|
+
this.handleAcknowledgment(payload);
|
|
421
427
|
const stopCommandAccepted = await this.handleStopTaskEvent(payload);
|
|
422
428
|
await this.messageRouter.handleBackendEvent(payload);
|
|
423
429
|
await this.maybeAckInboundCommand(payload, {
|
|
@@ -426,6 +432,126 @@ export class ConductorClient {
|
|
|
426
432
|
: undefined,
|
|
427
433
|
});
|
|
428
434
|
};
|
|
435
|
+
/**
|
|
436
|
+
* Handle acknowledgment messages from backend for confirmable outbound messages
|
|
437
|
+
*/
|
|
438
|
+
handleAcknowledgment(payload) {
|
|
439
|
+
const eventType = typeof payload?.type === 'string' ? payload.type : '';
|
|
440
|
+
const data = payload?.payload && typeof payload.payload === 'object'
|
|
441
|
+
? payload.payload
|
|
442
|
+
: null;
|
|
443
|
+
if (!data)
|
|
444
|
+
return;
|
|
445
|
+
if (eventType === 'message_recorded') {
|
|
446
|
+
// Acknowledgment for sdk_message
|
|
447
|
+
const messageId = typeof data.message_id === 'string' ? data.message_id : null;
|
|
448
|
+
if (messageId && this.pendingOutbound.has(messageId)) {
|
|
449
|
+
const pending = this.pendingOutbound.get(messageId);
|
|
450
|
+
if (pending.type === 'sdk_message') {
|
|
451
|
+
this.pendingOutbound.delete(messageId);
|
|
452
|
+
pending.resolve();
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
else if (eventType === 'agent_command_ack_recorded') {
|
|
457
|
+
// Acknowledgment for agent_command_ack
|
|
458
|
+
const requestId = typeof data.request_id === 'string' ? data.request_id : null;
|
|
459
|
+
if (requestId && this.pendingOutbound.has(requestId)) {
|
|
460
|
+
const pending = this.pendingOutbound.get(requestId);
|
|
461
|
+
if (pending.type === 'agent_command_ack') {
|
|
462
|
+
this.pendingOutbound.delete(requestId);
|
|
463
|
+
pending.resolve();
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Send a confirmable message and wait for acknowledgment from backend.
|
|
470
|
+
* Implements reliable delivery with idempotency.
|
|
471
|
+
*/
|
|
472
|
+
async sendConfirmable(type, payload, stableId) {
|
|
473
|
+
// Check if there's already a pending message with this stableId
|
|
474
|
+
const existingPending = this.pendingOutbound.get(stableId);
|
|
475
|
+
if (existingPending) {
|
|
476
|
+
// Return the existing promise to deduplicate in-flight requests
|
|
477
|
+
return new Promise((resolve, reject) => {
|
|
478
|
+
const checkResolved = () => {
|
|
479
|
+
if (!this.pendingOutbound.has(stableId)) {
|
|
480
|
+
resolve();
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
const pending = this.pendingOutbound.get(stableId);
|
|
484
|
+
if (pending.retryCount > 100) { // Safety limit
|
|
485
|
+
reject(new Error(`Message ${stableId} exceeded max retry attempts`));
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
setTimeout(checkResolved, 100);
|
|
489
|
+
};
|
|
490
|
+
checkResolved();
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
return new Promise((resolve, reject) => {
|
|
494
|
+
const now = Date.now();
|
|
495
|
+
const pendingMessage = {
|
|
496
|
+
type,
|
|
497
|
+
payload,
|
|
498
|
+
stableId,
|
|
499
|
+
firstSentAt: now,
|
|
500
|
+
lastSentAt: now,
|
|
501
|
+
retryCount: 0,
|
|
502
|
+
resolve,
|
|
503
|
+
reject,
|
|
504
|
+
};
|
|
505
|
+
this.pendingOutbound.set(stableId, pendingMessage);
|
|
506
|
+
// Send the message
|
|
507
|
+
void this.doSendConfirmable(stableId);
|
|
508
|
+
// Set timeout for acknowledgment
|
|
509
|
+
setTimeout(() => {
|
|
510
|
+
if (this.pendingOutbound.has(stableId)) {
|
|
511
|
+
this.pendingOutbound.delete(stableId);
|
|
512
|
+
reject(new Error(`Timeout waiting for acknowledgment of ${type} (${stableId})`));
|
|
513
|
+
}
|
|
514
|
+
}, this.ACK_TIMEOUT_MS);
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Actually send a confirmable message over websocket.
|
|
519
|
+
* Called initially and on reconnection.
|
|
520
|
+
*/
|
|
521
|
+
async doSendConfirmable(stableId) {
|
|
522
|
+
const pending = this.pendingOutbound.get(stableId);
|
|
523
|
+
if (!pending)
|
|
524
|
+
return; // Already acknowledged or timed out
|
|
525
|
+
try {
|
|
526
|
+
await this.sendEnvelope({
|
|
527
|
+
type: pending.type,
|
|
528
|
+
payload: pending.payload,
|
|
529
|
+
});
|
|
530
|
+
pending.lastSentAt = Date.now();
|
|
531
|
+
pending.retryCount++;
|
|
532
|
+
}
|
|
533
|
+
catch (error) {
|
|
534
|
+
// Send failed - message remains in pending queue for retry on reconnection
|
|
535
|
+
console.warn(`[sdk] Failed to send ${pending.type} (${stableId}): ${error instanceof Error ? error.message : String(error)}`);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Flush all pending confirmable messages.
|
|
540
|
+
* Called after websocket reconnection.
|
|
541
|
+
*/
|
|
542
|
+
async flushPendingOutbound() {
|
|
543
|
+
if (this.pendingOutbound.size === 0)
|
|
544
|
+
return;
|
|
545
|
+
console.log(`[sdk] Flushing ${this.pendingOutbound.size} pending confirmable messages after reconnection`);
|
|
546
|
+
const promises = [];
|
|
547
|
+
for (const [stableId, pending] of this.pendingOutbound) {
|
|
548
|
+
promises.push(this.doSendConfirmable(stableId));
|
|
549
|
+
}
|
|
550
|
+
// Wait for all sends to complete (but not for acknowledgments)
|
|
551
|
+
await Promise.all(promises).catch(() => {
|
|
552
|
+
// Errors are logged in doSendConfirmable, messages remain pending
|
|
553
|
+
});
|
|
554
|
+
}
|
|
429
555
|
async sendEnvelope(envelope) {
|
|
430
556
|
await this.wsClient.sendJson(envelope);
|
|
431
557
|
}
|
package/dist/ws/client.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ export interface WebSocketClientOptions {
|
|
|
21
21
|
isReconnect: boolean;
|
|
22
22
|
}) => void;
|
|
23
23
|
onDisconnected?: () => void;
|
|
24
|
+
onReconnected?: () => void;
|
|
24
25
|
}
|
|
25
26
|
export declare class ConductorWebSocketClient {
|
|
26
27
|
private readonly url;
|
|
@@ -30,6 +31,7 @@ export declare class ConductorWebSocketClient {
|
|
|
30
31
|
private readonly connectImpl;
|
|
31
32
|
private readonly onConnected?;
|
|
32
33
|
private readonly onDisconnected?;
|
|
34
|
+
private readonly onReconnected?;
|
|
33
35
|
private readonly handlers;
|
|
34
36
|
private readonly extraHeaders;
|
|
35
37
|
private conn;
|
|
@@ -52,6 +54,7 @@ export declare class ConductorWebSocketClient {
|
|
|
52
54
|
private handleConnectionLoss;
|
|
53
55
|
private dispatch;
|
|
54
56
|
private notifyConnected;
|
|
57
|
+
private notifyReconnected;
|
|
55
58
|
private notifyDisconnected;
|
|
56
59
|
private isConnectionClosed;
|
|
57
60
|
private sendWithReconnect;
|
package/dist/ws/client.js
CHANGED
|
@@ -16,6 +16,7 @@ export class ConductorWebSocketClient {
|
|
|
16
16
|
connectImpl;
|
|
17
17
|
onConnected;
|
|
18
18
|
onDisconnected;
|
|
19
|
+
onReconnected;
|
|
19
20
|
handlers = [];
|
|
20
21
|
extraHeaders;
|
|
21
22
|
conn = null;
|
|
@@ -37,6 +38,7 @@ export class ConductorWebSocketClient {
|
|
|
37
38
|
this.connectImpl = options.connectImpl ?? defaultConnectImpl;
|
|
38
39
|
this.onConnected = options.onConnected;
|
|
39
40
|
this.onDisconnected = options.onDisconnected;
|
|
41
|
+
this.onReconnected = options.onReconnected;
|
|
40
42
|
}
|
|
41
43
|
registerHandler(handler) {
|
|
42
44
|
this.handlers.push(handler);
|
|
@@ -85,6 +87,9 @@ export class ConductorWebSocketClient {
|
|
|
85
87
|
const isReconnect = this.hasConnectedAtLeastOnce;
|
|
86
88
|
this.hasConnectedAtLeastOnce = true;
|
|
87
89
|
this.notifyConnected(isReconnect);
|
|
90
|
+
if (isReconnect) {
|
|
91
|
+
this.notifyReconnected();
|
|
92
|
+
}
|
|
88
93
|
this.listenTask = this.listenLoop(this.conn);
|
|
89
94
|
this.heartbeatTask = this.heartbeatLoop(this.conn);
|
|
90
95
|
return;
|
|
@@ -165,6 +170,17 @@ export class ConductorWebSocketClient {
|
|
|
165
170
|
// Swallow callback errors to avoid impacting reconnect behavior.
|
|
166
171
|
}
|
|
167
172
|
}
|
|
173
|
+
notifyReconnected() {
|
|
174
|
+
if (!this.onReconnected) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
this.onReconnected();
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
// Swallow callback errors to avoid impacting reconnect behavior.
|
|
182
|
+
}
|
|
183
|
+
}
|
|
168
184
|
notifyDisconnected() {
|
|
169
185
|
if (!this.onDisconnected) {
|
|
170
186
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@love-moon/conductor-sdk",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.16",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -26,5 +26,6 @@
|
|
|
26
26
|
"@types/ws": "^8.5.12",
|
|
27
27
|
"typescript": "^5.6.3",
|
|
28
28
|
"vitest": "^2.1.4"
|
|
29
|
-
}
|
|
29
|
+
},
|
|
30
|
+
"gitCommitId": "13b4d6c"
|
|
30
31
|
}
|