@love-moon/conductor-sdk 0.1.3 → 0.2.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.
@@ -97,6 +97,28 @@ async function main() {
97
97
  required: ['task_id', 'content'],
98
98
  },
99
99
  },
100
+ {
101
+ name: 'send_runtime_status',
102
+ description: 'Send a runtime status update to a task',
103
+ inputSchema: {
104
+ type: 'object',
105
+ properties: {
106
+ task_id: { type: 'string' },
107
+ state: { type: 'string' },
108
+ phase: { type: 'string' },
109
+ source: { type: 'string' },
110
+ reply_in_progress: { type: 'boolean' },
111
+ status_line: { type: 'string' },
112
+ status_done_line: { type: 'string' },
113
+ reply_preview: { type: 'string' },
114
+ reply_to: { type: 'string' },
115
+ backend: { type: 'string' },
116
+ thread_id: { type: 'string' },
117
+ created_at: { type: 'string' },
118
+ },
119
+ required: ['task_id'],
120
+ },
121
+ },
100
122
  {
101
123
  name: 'receive_messages',
102
124
  description: 'Receive messages from a task',
@@ -24,6 +24,7 @@ export declare class MCPServer {
24
24
  handleRequest(toolName: string, payload: ToolRequest): Promise<ToolResponse>;
25
25
  private toolCreateTaskSession;
26
26
  private toolSendMessage;
27
+ private toolSendRuntimeStatus;
27
28
  private toolReceiveMessages;
28
29
  private toolAckMessages;
29
30
  private toolListProjects;
@@ -16,6 +16,7 @@ export class MCPServer {
16
16
  this.tools = {
17
17
  create_task_session: this.toolCreateTaskSession,
18
18
  send_message: this.toolSendMessage,
19
+ send_runtime_status: this.toolSendRuntimeStatus,
19
20
  receive_messages: this.toolReceiveMessages,
20
21
  ack_messages: this.toolAckMessages,
21
22
  list_projects: this.toolListProjects,
@@ -82,6 +83,30 @@ export class MCPServer {
82
83
  });
83
84
  return { delivered: true };
84
85
  }
86
+ async toolSendRuntimeStatus(payload) {
87
+ const taskId = String(payload.task_id || '');
88
+ if (!taskId) {
89
+ throw new Error('task_id required');
90
+ }
91
+ await this.options.backendSender({
92
+ type: 'task_runtime_status',
93
+ payload: {
94
+ task_id: taskId,
95
+ state: payload.state,
96
+ phase: payload.phase,
97
+ source: payload.source,
98
+ reply_in_progress: payload.reply_in_progress,
99
+ status_line: payload.status_line,
100
+ status_done_line: payload.status_done_line,
101
+ reply_preview: payload.reply_preview,
102
+ reply_to: payload.reply_to,
103
+ backend: payload.backend,
104
+ thread_id: payload.thread_id,
105
+ created_at: payload.created_at,
106
+ },
107
+ });
108
+ return { delivered: true };
109
+ }
85
110
  async toolReceiveMessages(payload) {
86
111
  const taskId = String(payload.task_id || '');
87
112
  if (!taskId) {
@@ -17,6 +17,10 @@ export interface WebSocketClientOptions {
17
17
  extraHeaders?: Record<string, string>;
18
18
  connectImpl?: ConnectImpl;
19
19
  hostName?: string;
20
+ onConnected?: (event: {
21
+ isReconnect: boolean;
22
+ }) => void;
23
+ onDisconnected?: () => void;
20
24
  }
21
25
  export declare class ConductorWebSocketClient {
22
26
  private readonly url;
@@ -24,6 +28,8 @@ export declare class ConductorWebSocketClient {
24
28
  private readonly reconnectDelay;
25
29
  private readonly heartbeatInterval;
26
30
  private readonly connectImpl;
31
+ private readonly onConnected?;
32
+ private readonly onDisconnected?;
27
33
  private readonly handlers;
28
34
  private readonly extraHeaders;
29
35
  private conn;
@@ -32,6 +38,7 @@ export declare class ConductorWebSocketClient {
32
38
  private heartbeatTask;
33
39
  private readonly lock;
34
40
  private waitController;
41
+ private hasConnectedAtLeastOnce;
35
42
  constructor(config: ConductorConfig, options?: WebSocketClientOptions);
36
43
  registerHandler(handler: WebSocketHandler): void;
37
44
  connect(): Promise<void>;
@@ -42,7 +49,10 @@ export declare class ConductorWebSocketClient {
42
49
  private cancelTasks;
43
50
  private listenLoop;
44
51
  private heartbeatLoop;
52
+ private handleConnectionLoss;
45
53
  private dispatch;
54
+ private notifyConnected;
55
+ private notifyDisconnected;
46
56
  private isConnectionClosed;
47
57
  private sendWithReconnect;
48
58
  private isNotOpenError;
package/dist/ws/client.js CHANGED
@@ -14,6 +14,8 @@ export class ConductorWebSocketClient {
14
14
  reconnectDelay;
15
15
  heartbeatInterval;
16
16
  connectImpl;
17
+ onConnected;
18
+ onDisconnected;
17
19
  handlers = [];
18
20
  extraHeaders;
19
21
  conn = null;
@@ -22,16 +24,19 @@ export class ConductorWebSocketClient {
22
24
  heartbeatTask = null;
23
25
  lock = new AsyncLock();
24
26
  waitController = new AbortController();
27
+ hasConnectedAtLeastOnce = false;
25
28
  constructor(config, options = {}) {
26
29
  this.url = config.resolvedWebsocketUrl;
27
30
  this.token = config.agentToken;
28
- this.reconnectDelay = options.reconnectDelay ?? 3000;
31
+ this.reconnectDelay = options.reconnectDelay ?? 10_000;
29
32
  this.heartbeatInterval = options.heartbeatInterval ?? 20_000;
30
33
  this.extraHeaders = {
31
34
  'x-conductor-host': options.hostName ?? defaultHostName(),
32
35
  ...(options.extraHeaders ?? {}),
33
36
  };
34
37
  this.connectImpl = options.connectImpl ?? defaultConnectImpl;
38
+ this.onConnected = options.onConnected;
39
+ this.onDisconnected = options.onDisconnected;
35
40
  }
36
41
  registerHandler(handler) {
37
42
  this.handlers.push(handler);
@@ -77,13 +82,16 @@ export class ConductorWebSocketClient {
77
82
  try {
78
83
  const headers = { Authorization: `Bearer ${this.token}`, ...this.extraHeaders };
79
84
  this.conn = await this.connectImpl(this.url, { headers });
85
+ const isReconnect = this.hasConnectedAtLeastOnce;
86
+ this.hasConnectedAtLeastOnce = true;
87
+ this.notifyConnected(isReconnect);
80
88
  this.listenTask = this.listenLoop(this.conn);
81
89
  this.heartbeatTask = this.heartbeatLoop(this.conn);
82
90
  return;
83
91
  }
84
92
  catch (error) {
85
93
  if (force) {
86
- console.warn(`[WebSocket] Connection failed, retrying in ${this.reconnectDelay}ms... (${error instanceof Error ? error.message : String(error)})`);
94
+ console.warn(`[${formatBeijingTimestamp()}] [WebSocket] Connection failed, retrying in ${this.reconnectDelay}ms... (${error instanceof Error ? error.message : String(error)})`);
87
95
  }
88
96
  await wait(this.reconnectDelay, this.waitController.signal);
89
97
  }
@@ -104,9 +112,7 @@ export class ConductorWebSocketClient {
104
112
  // Ignore errors; reconnection logic handles it.
105
113
  }
106
114
  finally {
107
- if (!this.stop && conn === this.conn) {
108
- await this.openConnection(true);
109
- }
115
+ await this.handleConnectionLoss(conn);
110
116
  }
111
117
  }
112
118
  async heartbeatLoop(conn) {
@@ -122,11 +128,17 @@ export class ConductorWebSocketClient {
122
128
  }
123
129
  }
124
130
  finally {
125
- if (!this.stop && conn === this.conn) {
126
- await this.openConnection(true);
127
- }
131
+ await this.handleConnectionLoss(conn);
128
132
  }
129
133
  }
134
+ async handleConnectionLoss(conn) {
135
+ if (this.stop || conn !== this.conn) {
136
+ return;
137
+ }
138
+ this.conn = null;
139
+ this.notifyDisconnected();
140
+ await this.openConnection(true);
141
+ }
130
142
  async dispatch(message) {
131
143
  let payload;
132
144
  try {
@@ -142,6 +154,28 @@ export class ConductorWebSocketClient {
142
154
  }
143
155
  }
144
156
  }
157
+ notifyConnected(isReconnect) {
158
+ if (!this.onConnected) {
159
+ return;
160
+ }
161
+ try {
162
+ this.onConnected({ isReconnect });
163
+ }
164
+ catch {
165
+ // Swallow callback errors to avoid impacting reconnect behavior.
166
+ }
167
+ }
168
+ notifyDisconnected() {
169
+ if (!this.onDisconnected) {
170
+ return;
171
+ }
172
+ try {
173
+ this.onDisconnected();
174
+ }
175
+ catch {
176
+ // Swallow callback errors to avoid impacting reconnect behavior.
177
+ }
178
+ }
145
179
  isConnectionClosed(conn) {
146
180
  if (!conn) {
147
181
  return true;
@@ -171,6 +205,10 @@ export class ConductorWebSocketClient {
171
205
  throw error instanceof Error ? error : new Error(String(error));
172
206
  }
173
207
  attemptedReconnect = true;
208
+ if (this.conn === conn) {
209
+ this.conn = null;
210
+ this.notifyDisconnected();
211
+ }
174
212
  await this.openConnection(true);
175
213
  }
176
214
  }
@@ -199,6 +237,9 @@ async function wait(ms, signal) {
199
237
  signal.addEventListener('abort', onAbort, { once: true });
200
238
  });
201
239
  }
240
+ function formatBeijingTimestamp(date = new Date()) {
241
+ return date.toLocaleString('sv-SE', { timeZone: 'Asia/Shanghai', hour12: false }).replace(' ', 'T');
242
+ }
202
243
  async function defaultConnectImpl(url, options) {
203
244
  const { default: WebSocket } = await import('ws');
204
245
  return new Promise((resolve, reject) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@love-moon/conductor-sdk",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",