@love-moon/conductor-sdk 0.2.11 → 0.2.13

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
@@ -143,6 +156,12 @@ export class ConductorClient {
143
156
  reply_to: payload?.reply_to,
144
157
  backend: payload?.backend,
145
158
  thread_id: payload?.thread_id,
159
+ daemon: payload?.daemon,
160
+ pid: payload?.pid,
161
+ session_id: payload?.session_id,
162
+ session_file_path: payload?.session_file_path,
163
+ token_usage_percent: payload?.token_usage_percent,
164
+ context_usage_percent: payload?.context_usage_percent,
146
165
  created_at: payload?.created_at,
147
166
  },
148
167
  });
@@ -177,6 +196,25 @@ export class ConductorClient {
177
196
  });
178
197
  return { delivered: true };
179
198
  }
199
+ async sendTaskStopAck(payload) {
200
+ const taskId = String(payload.task_id || '').trim();
201
+ const requestId = String(payload.request_id || '').trim();
202
+ if (!taskId) {
203
+ throw new Error('task_id is required');
204
+ }
205
+ if (!requestId) {
206
+ throw new Error('request_id is required');
207
+ }
208
+ await this.sendEnvelope({
209
+ type: 'task_stop_ack',
210
+ payload: {
211
+ task_id: taskId,
212
+ request_id: requestId,
213
+ accepted: payload.accepted !== false,
214
+ },
215
+ });
216
+ return { delivered: true };
217
+ }
180
218
  async receiveMessages(taskId, limit = 20) {
181
219
  const messages = await this.sessions.popMessages(taskId, limit);
182
220
  return formatMessagesResponse(messages);
@@ -278,15 +316,20 @@ export class ConductorClient {
278
316
  };
279
317
  }
280
318
  handleBackendEvent = async (payload) => {
319
+ const stopCommandAccepted = await this.handleStopTaskEvent(payload);
281
320
  await this.messageRouter.handleBackendEvent(payload);
282
- await this.maybeAckInboundCommand(payload);
321
+ await this.maybeAckInboundCommand(payload, {
322
+ accepted: typeof stopCommandAccepted === 'boolean'
323
+ ? stopCommandAccepted
324
+ : undefined,
325
+ });
283
326
  };
284
327
  async sendEnvelope(envelope) {
285
328
  await this.wsClient.sendJson(envelope);
286
329
  }
287
- async maybeAckInboundCommand(payload) {
330
+ async maybeAckInboundCommand(payload, options = {}) {
288
331
  const eventType = typeof payload?.type === 'string' ? payload.type : '';
289
- if (eventType !== 'task_user_message' && eventType !== 'task_action') {
332
+ if (eventType !== 'task_user_message' && eventType !== 'task_action' && eventType !== 'stop_task') {
290
333
  return;
291
334
  }
292
335
  const data = payload?.payload && typeof payload.payload === 'object'
@@ -304,7 +347,7 @@ export class ConductorClient {
304
347
  request_id: requestId,
305
348
  task_id: typeof data.task_id === 'string' ? data.task_id : undefined,
306
349
  event_type: eventType,
307
- accepted: true,
350
+ accepted: typeof options.accepted === 'boolean' ? options.accepted : true,
308
351
  });
309
352
  }
310
353
  catch (error) {
@@ -312,6 +355,52 @@ export class ConductorClient {
312
355
  console.warn(`[sdk] failed to ack inbound command ${requestId}: ${message}`);
313
356
  }
314
357
  }
358
+ async handleStopTaskEvent(payload) {
359
+ if (typeof payload?.type !== 'string' || payload.type !== 'stop_task') {
360
+ return undefined;
361
+ }
362
+ const data = payload?.payload && typeof payload.payload === 'object'
363
+ ? payload.payload
364
+ : null;
365
+ if (!data) {
366
+ return false;
367
+ }
368
+ const taskId = typeof data.task_id === 'string' ? data.task_id.trim() : '';
369
+ const requestId = typeof data.request_id === 'string' ? data.request_id.trim() : '';
370
+ const reason = typeof data.reason === 'string' ? data.reason.trim() : '';
371
+ if (!taskId) {
372
+ return false;
373
+ }
374
+ let accepted = false;
375
+ if (this.onStopTask) {
376
+ try {
377
+ await this.onStopTask({
378
+ taskId,
379
+ requestId: requestId || undefined,
380
+ reason: reason || undefined,
381
+ });
382
+ accepted = true;
383
+ }
384
+ catch (error) {
385
+ const message = error instanceof Error ? error.message : String(error);
386
+ console.warn(`[sdk] stop_task callback failed for task ${taskId}: ${message}`);
387
+ }
388
+ }
389
+ if (requestId) {
390
+ try {
391
+ await this.sendTaskStopAck({
392
+ task_id: taskId,
393
+ request_id: requestId,
394
+ accepted,
395
+ });
396
+ }
397
+ catch (error) {
398
+ const message = error instanceof Error ? error.message : String(error);
399
+ console.warn(`[sdk] failed to send task_stop_ack for task ${taskId}: ${message}`);
400
+ }
401
+ }
402
+ return accepted;
403
+ }
315
404
  resolveHostname() {
316
405
  const records = this.sessionStore.load();
317
406
  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.11",
3
+ "version": "0.2.13",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",