@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 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
- await this.backendApi.createTask({
77
- id: taskId,
78
- projectId,
79
- title,
80
- backendType: typeof payload.backend_type === 'string'
81
- ? payload.backend_type
82
- : typeof payload.backendType === 'string'
83
- ? payload.backendType
84
- : undefined,
85
- initialContent: typeof payload.prefill === 'string' ? payload.prefill : undefined,
86
- agentHost: typeof payload.agent_host === 'string'
87
- ? payload.agent_host
88
- : typeof payload.agentHost === 'string'
89
- ? payload.agentHost
90
- : this.agentHost,
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
@@ -5,3 +5,4 @@ export * from './session/index.js';
5
5
  export * from './message/index.js';
6
6
  export * from './client.js';
7
7
  export * from './context/index.js';
8
+ export * from './limits/index.js';
package/dist/index.js CHANGED
@@ -5,3 +5,4 @@ export * from './session/index.js';
5
5
  export * from './message/index.js';
6
6
  export * from './client.js';
7
7
  export * from './context/index.js';
8
+ export * from './limits/index.js';
@@ -0,0 +1 @@
1
+ export * from './plan_limits.js';
@@ -0,0 +1 @@
1
+ export * from './plan_limits.js';
@@ -0,0 +1,3 @@
1
+ export declare function getPlanLimitMessageFromDetails(details: unknown): string | null;
2
+ export declare function getPlanLimitMessageFromError(error: unknown): string | null;
3
+ export declare function getPlanLimitMessageFromRealtimeEvent(event: unknown): string | null;
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@love-moon/conductor-sdk",
3
- "version": "0.2.10",
3
+ "version": "0.2.12",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",