@love-moon/conductor-sdk 0.2.10 → 0.2.12
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 +14 -0
- package/dist/client.js +103 -20
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/limits/index.d.ts +1 -0
- package/dist/limits/index.js +1 -0
- package/dist/limits/plan_limits.d.ts +3 -0
- package/dist/limits/plan_limits.js +80 -0
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ export interface ConductorClientConnectOptions {
|
|
|
21
21
|
isReconnect: boolean;
|
|
22
22
|
}) => void;
|
|
23
23
|
onDisconnected?: () => void;
|
|
24
|
+
onStopTask?: (event: StopTaskEvent) => Promise<void> | void;
|
|
24
25
|
}
|
|
25
26
|
interface ConductorClientInit {
|
|
26
27
|
config: ConductorConfig;
|
|
@@ -32,6 +33,12 @@ interface ConductorClientInit {
|
|
|
32
33
|
sessionStore: SessionDiskStore;
|
|
33
34
|
messageRouter: MessageRouter;
|
|
34
35
|
agentHost: string;
|
|
36
|
+
onStopTask?: (event: StopTaskEvent) => Promise<void> | void;
|
|
37
|
+
}
|
|
38
|
+
export interface StopTaskEvent {
|
|
39
|
+
taskId: string;
|
|
40
|
+
requestId?: string;
|
|
41
|
+
reason?: string;
|
|
35
42
|
}
|
|
36
43
|
export declare class ConductorClient {
|
|
37
44
|
private readonly config;
|
|
@@ -43,6 +50,7 @@ export declare class ConductorClient {
|
|
|
43
50
|
private readonly sessionStore;
|
|
44
51
|
private readonly messageRouter;
|
|
45
52
|
private readonly agentHost;
|
|
53
|
+
private readonly onStopTask?;
|
|
46
54
|
private closed;
|
|
47
55
|
constructor(init: ConductorClientInit);
|
|
48
56
|
static connect(options?: ConductorClientConnectOptions): Promise<ConductorClient>;
|
|
@@ -62,6 +70,11 @@ export declare class ConductorClient {
|
|
|
62
70
|
event_type?: string;
|
|
63
71
|
accepted?: boolean;
|
|
64
72
|
}): Promise<Record<string, any>>;
|
|
73
|
+
sendTaskStopAck(payload: {
|
|
74
|
+
task_id: string;
|
|
75
|
+
request_id: string;
|
|
76
|
+
accepted?: boolean;
|
|
77
|
+
}): Promise<Record<string, any>>;
|
|
65
78
|
receiveMessages(taskId: string, limit?: number): Promise<Record<string, any>>;
|
|
66
79
|
ackMessages(taskId: string, ackToken?: string | null): Promise<Record<string, any> | undefined>;
|
|
67
80
|
listProjects(): Promise<Record<string, any>>;
|
|
@@ -73,6 +86,7 @@ export declare class ConductorClient {
|
|
|
73
86
|
private readonly handleBackendEvent;
|
|
74
87
|
private sendEnvelope;
|
|
75
88
|
private maybeAckInboundCommand;
|
|
89
|
+
private handleStopTaskEvent;
|
|
76
90
|
private resolveHostname;
|
|
77
91
|
private waitForTaskCreation;
|
|
78
92
|
private readIntEnv;
|
package/dist/client.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
2
|
import { BackendApiClient } from './backend/index.js';
|
|
3
3
|
import { loadConfig } from './config/index.js';
|
|
4
|
+
import { getPlanLimitMessageFromError } from './limits/index.js';
|
|
4
5
|
import { MessageRouter } from './message/index.js';
|
|
5
6
|
import { SessionDiskStore, SessionManager, currentHostname, currentSessionId } from './session/index.js';
|
|
6
7
|
import { ConductorWebSocketClient } from './ws/index.js';
|
|
@@ -15,6 +16,7 @@ export class ConductorClient {
|
|
|
15
16
|
sessionStore;
|
|
16
17
|
messageRouter;
|
|
17
18
|
agentHost;
|
|
19
|
+
onStopTask;
|
|
18
20
|
closed = false;
|
|
19
21
|
constructor(init) {
|
|
20
22
|
this.config = init.config;
|
|
@@ -26,6 +28,7 @@ export class ConductorClient {
|
|
|
26
28
|
this.sessionStore = init.sessionStore;
|
|
27
29
|
this.messageRouter = init.messageRouter;
|
|
28
30
|
this.agentHost = init.agentHost;
|
|
31
|
+
this.onStopTask = init.onStopTask;
|
|
29
32
|
this.wsClient.registerHandler(this.handleBackendEvent);
|
|
30
33
|
}
|
|
31
34
|
static async connect(options = {}) {
|
|
@@ -53,6 +56,7 @@ export class ConductorClient {
|
|
|
53
56
|
sessionStore,
|
|
54
57
|
messageRouter,
|
|
55
58
|
agentHost,
|
|
59
|
+
onStopTask: options.onStopTask,
|
|
56
60
|
});
|
|
57
61
|
await client.wsClient.connect();
|
|
58
62
|
return client;
|
|
@@ -73,22 +77,31 @@ export class ConductorClient {
|
|
|
73
77
|
const taskId = String(payload.task_id || safeRandomUuid());
|
|
74
78
|
const sessionId = String(payload.session_id || taskId);
|
|
75
79
|
await this.sessions.addSession(taskId, sessionId, projectId);
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
80
|
+
try {
|
|
81
|
+
await this.backendApi.createTask({
|
|
82
|
+
id: taskId,
|
|
83
|
+
projectId,
|
|
84
|
+
title,
|
|
85
|
+
backendType: typeof payload.backend_type === 'string'
|
|
86
|
+
? payload.backend_type
|
|
87
|
+
: typeof payload.backendType === 'string'
|
|
88
|
+
? payload.backendType
|
|
89
|
+
: undefined,
|
|
90
|
+
initialContent: typeof payload.prefill === 'string' ? payload.prefill : undefined,
|
|
91
|
+
agentHost: typeof payload.agent_host === 'string'
|
|
92
|
+
? payload.agent_host
|
|
93
|
+
: typeof payload.agentHost === 'string'
|
|
94
|
+
? payload.agentHost
|
|
95
|
+
: this.agentHost,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
const limitMessage = getPlanLimitMessageFromError(error);
|
|
100
|
+
if (limitMessage) {
|
|
101
|
+
throw new Error(limitMessage, error instanceof Error ? { cause: error } : undefined);
|
|
102
|
+
}
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
92
105
|
await this.waitForTaskCreation(projectId, taskId);
|
|
93
106
|
const projectPath = typeof payload.project_path === 'string' && payload.project_path
|
|
94
107
|
? payload.project_path
|
|
@@ -177,6 +190,25 @@ export class ConductorClient {
|
|
|
177
190
|
});
|
|
178
191
|
return { delivered: true };
|
|
179
192
|
}
|
|
193
|
+
async sendTaskStopAck(payload) {
|
|
194
|
+
const taskId = String(payload.task_id || '').trim();
|
|
195
|
+
const requestId = String(payload.request_id || '').trim();
|
|
196
|
+
if (!taskId) {
|
|
197
|
+
throw new Error('task_id is required');
|
|
198
|
+
}
|
|
199
|
+
if (!requestId) {
|
|
200
|
+
throw new Error('request_id is required');
|
|
201
|
+
}
|
|
202
|
+
await this.sendEnvelope({
|
|
203
|
+
type: 'task_stop_ack',
|
|
204
|
+
payload: {
|
|
205
|
+
task_id: taskId,
|
|
206
|
+
request_id: requestId,
|
|
207
|
+
accepted: payload.accepted !== false,
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
return { delivered: true };
|
|
211
|
+
}
|
|
180
212
|
async receiveMessages(taskId, limit = 20) {
|
|
181
213
|
const messages = await this.sessions.popMessages(taskId, limit);
|
|
182
214
|
return formatMessagesResponse(messages);
|
|
@@ -278,15 +310,20 @@ export class ConductorClient {
|
|
|
278
310
|
};
|
|
279
311
|
}
|
|
280
312
|
handleBackendEvent = async (payload) => {
|
|
313
|
+
const stopCommandAccepted = await this.handleStopTaskEvent(payload);
|
|
281
314
|
await this.messageRouter.handleBackendEvent(payload);
|
|
282
|
-
await this.maybeAckInboundCommand(payload
|
|
315
|
+
await this.maybeAckInboundCommand(payload, {
|
|
316
|
+
accepted: typeof stopCommandAccepted === 'boolean'
|
|
317
|
+
? stopCommandAccepted
|
|
318
|
+
: undefined,
|
|
319
|
+
});
|
|
283
320
|
};
|
|
284
321
|
async sendEnvelope(envelope) {
|
|
285
322
|
await this.wsClient.sendJson(envelope);
|
|
286
323
|
}
|
|
287
|
-
async maybeAckInboundCommand(payload) {
|
|
324
|
+
async maybeAckInboundCommand(payload, options = {}) {
|
|
288
325
|
const eventType = typeof payload?.type === 'string' ? payload.type : '';
|
|
289
|
-
if (eventType !== 'task_user_message' && eventType !== 'task_action') {
|
|
326
|
+
if (eventType !== 'task_user_message' && eventType !== 'task_action' && eventType !== 'stop_task') {
|
|
290
327
|
return;
|
|
291
328
|
}
|
|
292
329
|
const data = payload?.payload && typeof payload.payload === 'object'
|
|
@@ -304,7 +341,7 @@ export class ConductorClient {
|
|
|
304
341
|
request_id: requestId,
|
|
305
342
|
task_id: typeof data.task_id === 'string' ? data.task_id : undefined,
|
|
306
343
|
event_type: eventType,
|
|
307
|
-
accepted: true,
|
|
344
|
+
accepted: typeof options.accepted === 'boolean' ? options.accepted : true,
|
|
308
345
|
});
|
|
309
346
|
}
|
|
310
347
|
catch (error) {
|
|
@@ -312,6 +349,52 @@ export class ConductorClient {
|
|
|
312
349
|
console.warn(`[sdk] failed to ack inbound command ${requestId}: ${message}`);
|
|
313
350
|
}
|
|
314
351
|
}
|
|
352
|
+
async handleStopTaskEvent(payload) {
|
|
353
|
+
if (typeof payload?.type !== 'string' || payload.type !== 'stop_task') {
|
|
354
|
+
return undefined;
|
|
355
|
+
}
|
|
356
|
+
const data = payload?.payload && typeof payload.payload === 'object'
|
|
357
|
+
? payload.payload
|
|
358
|
+
: null;
|
|
359
|
+
if (!data) {
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
const taskId = typeof data.task_id === 'string' ? data.task_id.trim() : '';
|
|
363
|
+
const requestId = typeof data.request_id === 'string' ? data.request_id.trim() : '';
|
|
364
|
+
const reason = typeof data.reason === 'string' ? data.reason.trim() : '';
|
|
365
|
+
if (!taskId) {
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
let accepted = false;
|
|
369
|
+
if (this.onStopTask) {
|
|
370
|
+
try {
|
|
371
|
+
await this.onStopTask({
|
|
372
|
+
taskId,
|
|
373
|
+
requestId: requestId || undefined,
|
|
374
|
+
reason: reason || undefined,
|
|
375
|
+
});
|
|
376
|
+
accepted = true;
|
|
377
|
+
}
|
|
378
|
+
catch (error) {
|
|
379
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
380
|
+
console.warn(`[sdk] stop_task callback failed for task ${taskId}: ${message}`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (requestId) {
|
|
384
|
+
try {
|
|
385
|
+
await this.sendTaskStopAck({
|
|
386
|
+
task_id: taskId,
|
|
387
|
+
request_id: requestId,
|
|
388
|
+
accepted,
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
catch (error) {
|
|
392
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
393
|
+
console.warn(`[sdk] failed to send task_stop_ack for task ${taskId}: ${message}`);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return accepted;
|
|
397
|
+
}
|
|
315
398
|
resolveHostname() {
|
|
316
399
|
const records = this.sessionStore.load();
|
|
317
400
|
for (const record of records) {
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './plan_limits.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './plan_limits.js';
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const PLAN_LIMIT_MESSAGES = {
|
|
2
|
+
manual_fire_active_task: 'Free plan limit reached: only 1 active fire task is allowed.',
|
|
3
|
+
app_active_task: 'Free plan limit reached: only 1 active app task is allowed.',
|
|
4
|
+
daemon_active_connection: 'Free plan limit reached: only 1 active daemon connection is allowed.',
|
|
5
|
+
};
|
|
6
|
+
function normalizeText(value) {
|
|
7
|
+
if (typeof value !== 'string') {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
const text = value.trim();
|
|
11
|
+
return text ? text : null;
|
|
12
|
+
}
|
|
13
|
+
function getMessageByLimitType(limitType) {
|
|
14
|
+
const normalized = limitType.trim().toLowerCase();
|
|
15
|
+
if (normalized in PLAN_LIMIT_MESSAGES) {
|
|
16
|
+
return PLAN_LIMIT_MESSAGES[normalized];
|
|
17
|
+
}
|
|
18
|
+
if (normalized === 'free_daemon_connection' || normalized === 'free-daemon-limit') {
|
|
19
|
+
return PLAN_LIMIT_MESSAGES.daemon_active_connection;
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
function inferFromText(raw) {
|
|
24
|
+
const text = normalizeText(raw);
|
|
25
|
+
if (!text) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const lower = text.toLowerCase();
|
|
29
|
+
if (lower.includes('one active manual fire task')) {
|
|
30
|
+
return PLAN_LIMIT_MESSAGES.manual_fire_active_task;
|
|
31
|
+
}
|
|
32
|
+
if (lower.includes('one active app task')) {
|
|
33
|
+
return PLAN_LIMIT_MESSAGES.app_active_task;
|
|
34
|
+
}
|
|
35
|
+
if (lower.includes('one active daemon connection')) {
|
|
36
|
+
return PLAN_LIMIT_MESSAGES.daemon_active_connection;
|
|
37
|
+
}
|
|
38
|
+
if (lower.includes('free plan task limit reached')) {
|
|
39
|
+
return 'Free plan task limit reached: only 1 active fire task and 1 active app task are allowed.';
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
export function getPlanLimitMessageFromDetails(details) {
|
|
44
|
+
if (!details) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
if (typeof details === 'string') {
|
|
48
|
+
return inferFromText(details);
|
|
49
|
+
}
|
|
50
|
+
if (typeof details !== 'object') {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
const payload = details;
|
|
54
|
+
const byType = normalizeText(payload.limit_type);
|
|
55
|
+
if (byType) {
|
|
56
|
+
const mapped = getMessageByLimitType(byType);
|
|
57
|
+
if (mapped) {
|
|
58
|
+
return mapped;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return inferFromText(payload.message) ?? inferFromText(payload.error);
|
|
62
|
+
}
|
|
63
|
+
export function getPlanLimitMessageFromError(error) {
|
|
64
|
+
if (!error || typeof error !== 'object') {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
const payload = error;
|
|
68
|
+
return (getPlanLimitMessageFromDetails(payload.details) ??
|
|
69
|
+
inferFromText(payload.message));
|
|
70
|
+
}
|
|
71
|
+
export function getPlanLimitMessageFromRealtimeEvent(event) {
|
|
72
|
+
if (!event || typeof event !== 'object') {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const payload = event;
|
|
76
|
+
if (typeof payload.type === 'string' && payload.type !== 'error') {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
return getPlanLimitMessageFromDetails(payload.payload);
|
|
80
|
+
}
|