@mirasoth/soothe-client 0.1.0 → 0.2.1
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/chunk-AQZACDIC.js +1399 -0
- package/dist/chunk-AQZACDIC.js.map +1 -0
- package/dist/client-CB6WKQYW.js +7 -0
- package/dist/index.cjs +2383 -419
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1110 -280
- package/dist/index.d.ts +1110 -280
- package/dist/index.js +1265 -91
- package/dist/index.js.map +1 -1
- package/package.json +34 -8
- package/dist/chunk-OMAC7LA7.js +0 -647
- package/dist/chunk-OMAC7LA7.js.map +0 -1
- package/dist/client-QS23U6WX.js +0 -7
- /package/dist/{client-QS23U6WX.js.map → client-CB6WKQYW.js.map} +0 -0
package/dist/index.cjs
CHANGED
|
@@ -30,6 +30,82 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
30
30
|
));
|
|
31
31
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
32
32
|
|
|
33
|
+
// src/errors.ts
|
|
34
|
+
function disconnectCauseName(cause) {
|
|
35
|
+
return cause === 1 /* Clean */ ? "clean" : "unclean";
|
|
36
|
+
}
|
|
37
|
+
var ConnectionError, DaemonError, TimeoutError, DisconnectCause, ReconnectError, StaleLoopError;
|
|
38
|
+
var init_errors = __esm({
|
|
39
|
+
"src/errors.ts"() {
|
|
40
|
+
"use strict";
|
|
41
|
+
ConnectionError = class extends Error {
|
|
42
|
+
url;
|
|
43
|
+
attempt;
|
|
44
|
+
cause;
|
|
45
|
+
constructor(url, attempt, cause) {
|
|
46
|
+
super(`connection error to ${url} (attempt ${attempt}): ${cause.message}`);
|
|
47
|
+
this.name = "ConnectionError";
|
|
48
|
+
this.url = url;
|
|
49
|
+
this.attempt = attempt;
|
|
50
|
+
this.cause = cause;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
DaemonError = class extends Error {
|
|
54
|
+
/** Numeric error code from the RFC-450 §7.3 registry. */
|
|
55
|
+
code;
|
|
56
|
+
/** The daemon's error message text. */
|
|
57
|
+
daemonMessage;
|
|
58
|
+
/** Optional machine-parseable error details. */
|
|
59
|
+
data;
|
|
60
|
+
constructor(code, message, data) {
|
|
61
|
+
super(`daemon error [${code}]: ${message}`);
|
|
62
|
+
this.name = "DaemonError";
|
|
63
|
+
this.code = code;
|
|
64
|
+
this.daemonMessage = message;
|
|
65
|
+
this.data = data;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
TimeoutError = class extends Error {
|
|
69
|
+
operation;
|
|
70
|
+
duration;
|
|
71
|
+
constructor(operation, duration) {
|
|
72
|
+
super(`timeout after ${duration} waiting for ${operation}`);
|
|
73
|
+
this.name = "TimeoutError";
|
|
74
|
+
this.operation = operation;
|
|
75
|
+
this.duration = duration;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
DisconnectCause = /* @__PURE__ */ ((DisconnectCause2) => {
|
|
79
|
+
DisconnectCause2[DisconnectCause2["Unclean"] = 0] = "Unclean";
|
|
80
|
+
DisconnectCause2[DisconnectCause2["Clean"] = 1] = "Clean";
|
|
81
|
+
return DisconnectCause2;
|
|
82
|
+
})(DisconnectCause || {});
|
|
83
|
+
ReconnectError = class extends Error {
|
|
84
|
+
url;
|
|
85
|
+
attempts;
|
|
86
|
+
cause;
|
|
87
|
+
constructor(url, attempts, cause) {
|
|
88
|
+
super(`reconnect to ${url} failed after ${attempts} attempts: ${cause.message}`);
|
|
89
|
+
this.name = "ReconnectError";
|
|
90
|
+
this.url = url;
|
|
91
|
+
this.attempts = attempts;
|
|
92
|
+
this.cause = cause;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
StaleLoopError = class extends Error {
|
|
96
|
+
loopID;
|
|
97
|
+
cause;
|
|
98
|
+
constructor(loopID, cause) {
|
|
99
|
+
const detail = cause ? `: ${cause.message}` : "";
|
|
100
|
+
super(`stale loop ${loopID}: reattach accepted but liveness probe failed${detail}`);
|
|
101
|
+
this.name = "StaleLoopError";
|
|
102
|
+
this.loopID = loopID;
|
|
103
|
+
this.cause = cause;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
33
109
|
// src/config.ts
|
|
34
110
|
function defaultConfig() {
|
|
35
111
|
return {
|
|
@@ -40,7 +116,11 @@ function defaultConfig() {
|
|
|
40
116
|
heartbeatInterval: 3e4,
|
|
41
117
|
daemonReadyTimeout: 2e4,
|
|
42
118
|
loopStatusTimeout: 6e4,
|
|
43
|
-
subscriptionTimeout: 1e4
|
|
119
|
+
subscriptionTimeout: 1e4,
|
|
120
|
+
reconnectMaxAttempts: 10,
|
|
121
|
+
reconnectInitialDelay: 500,
|
|
122
|
+
reconnectMaxDelay: 1e4,
|
|
123
|
+
reattachProbeTimeout: 5e3
|
|
44
124
|
};
|
|
45
125
|
}
|
|
46
126
|
function loadConfigFromEnv() {
|
|
@@ -90,102 +170,89 @@ function decodeMessage(data) {
|
|
|
90
170
|
} catch {
|
|
91
171
|
throw new Error(`invalid JSON: ${data}`);
|
|
92
172
|
}
|
|
173
|
+
if (!parsed || typeof parsed !== "object") return parsed;
|
|
93
174
|
const type = parsed.type;
|
|
94
175
|
if (!type) return parsed;
|
|
95
176
|
switch (type) {
|
|
96
|
-
|
|
97
|
-
case "loop_input":
|
|
98
|
-
return { ...parsed };
|
|
99
|
-
case "command":
|
|
100
|
-
return { ...parsed };
|
|
101
|
-
case "daemon_status":
|
|
102
|
-
return { ...parsed };
|
|
103
|
-
case "daemon_shutdown":
|
|
104
|
-
return { ...parsed };
|
|
105
|
-
case "config_get":
|
|
106
|
-
return { ...parsed };
|
|
107
|
-
case "loop_new":
|
|
108
|
-
return { ...parsed };
|
|
109
|
-
case "loop_subscribe":
|
|
110
|
-
return { ...parsed };
|
|
111
|
-
case "loop_detach":
|
|
112
|
-
return { ...parsed };
|
|
113
|
-
case "loop_list":
|
|
114
|
-
return { ...parsed };
|
|
115
|
-
case "loop_get":
|
|
116
|
-
return { ...parsed };
|
|
117
|
-
case "loop_tree":
|
|
118
|
-
return { ...parsed };
|
|
119
|
-
case "loop_prune":
|
|
120
|
-
return { ...parsed };
|
|
121
|
-
case "loop_delete":
|
|
177
|
+
case "connection_init":
|
|
122
178
|
return { ...parsed };
|
|
123
|
-
case "
|
|
179
|
+
case "connection_ack":
|
|
124
180
|
return { ...parsed };
|
|
125
|
-
case "
|
|
181
|
+
case "request":
|
|
126
182
|
return { ...parsed };
|
|
127
|
-
case "
|
|
183
|
+
case "response":
|
|
128
184
|
return { ...parsed };
|
|
129
|
-
case "
|
|
185
|
+
case "notification":
|
|
130
186
|
return { ...parsed };
|
|
131
|
-
case "
|
|
187
|
+
case "subscribe":
|
|
132
188
|
return { ...parsed };
|
|
133
|
-
|
|
134
|
-
case "event":
|
|
135
|
-
return { ...parsed };
|
|
136
|
-
case "status": {
|
|
137
|
-
const msg = { ...parsed };
|
|
138
|
-
if (!msg.loop_id && parsed.loopId && typeof parsed.loopId === "string") {
|
|
139
|
-
msg.loop_id = parsed.loopId;
|
|
140
|
-
}
|
|
141
|
-
return msg;
|
|
142
|
-
}
|
|
143
|
-
case "subscription_confirmed":
|
|
189
|
+
case "next":
|
|
144
190
|
return { ...parsed };
|
|
145
191
|
case "error":
|
|
146
192
|
return { ...parsed };
|
|
147
|
-
case "
|
|
148
|
-
return { ...parsed };
|
|
149
|
-
case "daemon_status_response":
|
|
150
|
-
return { ...parsed };
|
|
151
|
-
case "shutdown_ack":
|
|
152
|
-
return { ...parsed };
|
|
153
|
-
case "loop_new_response":
|
|
154
|
-
return { ...parsed };
|
|
155
|
-
case "loop_subscribe_response":
|
|
156
|
-
return { ...parsed };
|
|
157
|
-
case "loop_detach_response":
|
|
193
|
+
case "complete":
|
|
158
194
|
return { ...parsed };
|
|
159
|
-
case "
|
|
195
|
+
case "unsubscribe":
|
|
160
196
|
return { ...parsed };
|
|
161
|
-
case "
|
|
197
|
+
case "ping":
|
|
162
198
|
return { ...parsed };
|
|
163
|
-
case "
|
|
199
|
+
case "pong":
|
|
164
200
|
return { ...parsed };
|
|
165
|
-
case "
|
|
201
|
+
case "receipt_response":
|
|
166
202
|
return { ...parsed };
|
|
167
|
-
case "
|
|
203
|
+
case "disconnect":
|
|
168
204
|
return { ...parsed };
|
|
169
|
-
case "
|
|
170
|
-
return { ...parsed };
|
|
171
|
-
case "history_replay":
|
|
172
|
-
return { ...parsed };
|
|
173
|
-
case "history_replay_complete":
|
|
174
|
-
case "replay_complete":
|
|
175
|
-
return { ...parsed };
|
|
176
|
-
case "loop_reattached":
|
|
177
|
-
return { ...parsed };
|
|
178
|
-
case "config_get_response":
|
|
179
|
-
case "invoke_skill_response":
|
|
180
|
-
return parsed;
|
|
181
|
-
case "skills_list_response":
|
|
182
|
-
return { ...parsed };
|
|
183
|
-
case "models_list_response":
|
|
205
|
+
case "status":
|
|
184
206
|
return { ...parsed };
|
|
185
207
|
default:
|
|
186
208
|
return parsed;
|
|
187
209
|
}
|
|
188
210
|
}
|
|
211
|
+
function requestEnvelope(method, params, id) {
|
|
212
|
+
return {
|
|
213
|
+
proto: PROTO_VERSION,
|
|
214
|
+
type: "request",
|
|
215
|
+
method,
|
|
216
|
+
params,
|
|
217
|
+
id: id ?? newRequestID()
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function notificationEnvelope(method, params) {
|
|
221
|
+
return { proto: PROTO_VERSION, type: "notification", method, params };
|
|
222
|
+
}
|
|
223
|
+
function subscribeEnvelope(method, params, id) {
|
|
224
|
+
return {
|
|
225
|
+
proto: PROTO_VERSION,
|
|
226
|
+
type: "subscribe",
|
|
227
|
+
method,
|
|
228
|
+
params,
|
|
229
|
+
id: id ?? newRequestID()
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
function unsubscribeEnvelope(id) {
|
|
233
|
+
return { proto: PROTO_VERSION, type: "unsubscribe", id };
|
|
234
|
+
}
|
|
235
|
+
function connectionInitEnvelope(opts) {
|
|
236
|
+
return {
|
|
237
|
+
proto: PROTO_VERSION,
|
|
238
|
+
type: "connection_init",
|
|
239
|
+
params: {
|
|
240
|
+
client_version: opts?.client_version ?? CLIENT_VERSION,
|
|
241
|
+
client_name: opts?.client_name ?? "soothe-client-ts",
|
|
242
|
+
accept_proto: opts?.accept_proto ?? [PROTO_VERSION],
|
|
243
|
+
capabilities: opts?.capabilities ?? DEFAULT_CLIENT_CAPABILITIES
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
function pingEnvelope() {
|
|
248
|
+
return { proto: PROTO_VERSION, type: "ping" };
|
|
249
|
+
}
|
|
250
|
+
function pongEnvelope() {
|
|
251
|
+
return { proto: PROTO_VERSION, type: "pong" };
|
|
252
|
+
}
|
|
253
|
+
function disconnectEnvelope() {
|
|
254
|
+
return { proto: PROTO_VERSION, type: "disconnect" };
|
|
255
|
+
}
|
|
189
256
|
function splitWirePayload(data) {
|
|
190
257
|
const trimmed = data.trim();
|
|
191
258
|
if (trimmed === "") return [];
|
|
@@ -195,21 +262,25 @@ function splitWirePayload(data) {
|
|
|
195
262
|
function extractSootheLoopID(msg) {
|
|
196
263
|
if (!msg || typeof msg !== "object") return ["", false];
|
|
197
264
|
const m = msg;
|
|
265
|
+
if (m.type === "next") {
|
|
266
|
+
const payload = m.payload;
|
|
267
|
+
if (payload && typeof payload === "object") {
|
|
268
|
+
const data = payload.data;
|
|
269
|
+
if (data && typeof data === "object") {
|
|
270
|
+
const id = data.loop_id;
|
|
271
|
+
if (id && id !== "") return [id, true];
|
|
272
|
+
}
|
|
273
|
+
const pid = payload.loop_id;
|
|
274
|
+
if (pid && pid !== "") return [pid, true];
|
|
275
|
+
}
|
|
276
|
+
return ["", false];
|
|
277
|
+
}
|
|
198
278
|
if (m.type === "status") {
|
|
199
279
|
const id = m.loop_id;
|
|
200
280
|
if (id && id !== "") return [id, true];
|
|
201
281
|
return ["", false];
|
|
202
282
|
}
|
|
203
|
-
|
|
204
|
-
const top = m.loop_id;
|
|
205
|
-
if (top && top !== "") return [top, true];
|
|
206
|
-
const data = m.data;
|
|
207
|
-
if (data && typeof data === "object") {
|
|
208
|
-
const dataId = data["loop_id"] ?? data["loopId"];
|
|
209
|
-
if (dataId && dataId !== "") return [dataId, true];
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
const generic = m["loop_id"] ?? m["loopId"];
|
|
283
|
+
const generic = m.loop_id;
|
|
213
284
|
if (generic && generic !== "") return [generic, true];
|
|
214
285
|
return ["", false];
|
|
215
286
|
}
|
|
@@ -217,52 +288,232 @@ function newRequestID() {
|
|
|
217
288
|
return (0, import_node_crypto.randomUUID)();
|
|
218
289
|
}
|
|
219
290
|
function newLoopInputMessage(loopID, content) {
|
|
220
|
-
return {
|
|
221
|
-
request_id: newRequestID(),
|
|
222
|
-
type: "loop_input",
|
|
291
|
+
return notificationEnvelope("loop_input", {
|
|
223
292
|
loop_id: loopID,
|
|
224
293
|
content,
|
|
225
294
|
autonomous: false
|
|
226
|
-
};
|
|
295
|
+
});
|
|
227
296
|
}
|
|
228
297
|
function newLoopNewMessage(opts) {
|
|
229
298
|
const options = typeof opts === "string" ? { client_workspace: opts } : opts ?? {};
|
|
230
299
|
const clientWorkspace = options.client_workspace ?? options.workspace;
|
|
231
|
-
const
|
|
232
|
-
request_id: newRequestID(),
|
|
233
|
-
type: "loop_new"
|
|
234
|
-
};
|
|
300
|
+
const params = {};
|
|
235
301
|
if (clientWorkspace?.trim()) {
|
|
236
|
-
|
|
302
|
+
params.client_workspace = clientWorkspace.trim();
|
|
237
303
|
}
|
|
238
304
|
if (options.user_id?.trim()) {
|
|
239
|
-
|
|
305
|
+
params.user_id = options.user_id.trim();
|
|
240
306
|
}
|
|
241
307
|
if (options.client_workspace_id?.trim()) {
|
|
242
|
-
|
|
308
|
+
params.client_workspace_id = options.client_workspace_id.trim();
|
|
243
309
|
}
|
|
244
310
|
if (options.is_ephemeral) {
|
|
245
|
-
|
|
311
|
+
params.is_ephemeral = true;
|
|
246
312
|
}
|
|
247
|
-
return
|
|
313
|
+
return requestEnvelope("loop_new", params);
|
|
248
314
|
}
|
|
249
315
|
function newLoopSubscribeMessage(loopID, verbosity, streamDelivery) {
|
|
250
|
-
const
|
|
251
|
-
request_id: newRequestID(),
|
|
252
|
-
type: "loop_subscribe",
|
|
253
|
-
loop_id: loopID,
|
|
254
|
-
verbosity
|
|
255
|
-
};
|
|
316
|
+
const params = { loop_id: loopID, verbosity };
|
|
256
317
|
if (streamDelivery) {
|
|
257
|
-
|
|
318
|
+
params.stream_delivery = streamDelivery;
|
|
258
319
|
}
|
|
259
|
-
return
|
|
320
|
+
return subscribeEnvelope("loop_events", params);
|
|
260
321
|
}
|
|
261
|
-
var import_node_crypto;
|
|
322
|
+
var import_node_crypto, PROTO_VERSION, DEFAULT_CLIENT_CAPABILITIES, CLIENT_VERSION;
|
|
262
323
|
var init_protocol = __esm({
|
|
263
324
|
"src/protocol.ts"() {
|
|
264
325
|
"use strict";
|
|
265
326
|
import_node_crypto = require("crypto");
|
|
327
|
+
PROTO_VERSION = "1";
|
|
328
|
+
DEFAULT_CLIENT_CAPABILITIES = ["streaming", "batch", "heartbeat", "receipts"];
|
|
329
|
+
CLIENT_VERSION = "0.1.0";
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// src/intent_hints.ts
|
|
334
|
+
function validateLoopInputIntentHint(hint) {
|
|
335
|
+
const key = hint.trim().toLowerCase();
|
|
336
|
+
if (key === "direct_llm" || key === "quiz") {
|
|
337
|
+
return REMOVED_INTENT_HINT_MESSAGES[key];
|
|
338
|
+
}
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
var INTENT_HINT_TEXT_COMPLETION, INTENT_HINT_IMAGE_TO_TEXT, INTENT_HINT_OCR, INTENT_HINT_EMBED, REMOVED_INTENT_HINTS, REMOVED_INTENT_HINT_MESSAGES, LOOP_ASSISTANT_OUTPUT_PHASES, DEFAULT_DELIVERABLE_PHASES;
|
|
342
|
+
var init_intent_hints = __esm({
|
|
343
|
+
"src/intent_hints.ts"() {
|
|
344
|
+
"use strict";
|
|
345
|
+
INTENT_HINT_TEXT_COMPLETION = "text_completion";
|
|
346
|
+
INTENT_HINT_IMAGE_TO_TEXT = "image_to_text";
|
|
347
|
+
INTENT_HINT_OCR = "ocr";
|
|
348
|
+
INTENT_HINT_EMBED = "embed";
|
|
349
|
+
REMOVED_INTENT_HINTS = ["direct_llm", "quiz"];
|
|
350
|
+
REMOVED_INTENT_HINT_MESSAGES = {
|
|
351
|
+
direct_llm: "intent_hint direct_llm is removed; use text_completion (text-only) or image_to_text (with attachments)",
|
|
352
|
+
quiz: "intent_hint quiz is removed; omit intent_hint and let intake classify the turn"
|
|
353
|
+
};
|
|
354
|
+
LOOP_ASSISTANT_OUTPUT_PHASES = [
|
|
355
|
+
"goal_completion",
|
|
356
|
+
"quiz",
|
|
357
|
+
"autonomous_goal",
|
|
358
|
+
"direct_model",
|
|
359
|
+
"text_completion",
|
|
360
|
+
"image_to_text",
|
|
361
|
+
"ocr",
|
|
362
|
+
"embed",
|
|
363
|
+
"plan_direct"
|
|
364
|
+
];
|
|
365
|
+
DEFAULT_DELIVERABLE_PHASES = /* @__PURE__ */ new Set([
|
|
366
|
+
"quiz",
|
|
367
|
+
"goal_completion",
|
|
368
|
+
"direct_model",
|
|
369
|
+
"text_completion",
|
|
370
|
+
"image_to_text",
|
|
371
|
+
"ocr",
|
|
372
|
+
"embed"
|
|
373
|
+
]);
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// src/multiplexer.ts
|
|
378
|
+
var Multiplexer;
|
|
379
|
+
var init_multiplexer = __esm({
|
|
380
|
+
"src/multiplexer.ts"() {
|
|
381
|
+
"use strict";
|
|
382
|
+
init_errors();
|
|
383
|
+
Multiplexer = class {
|
|
384
|
+
rpcs = /* @__PURE__ */ new Map();
|
|
385
|
+
subs = /* @__PURE__ */ new Map();
|
|
386
|
+
receipts = /* @__PURE__ */ new Map();
|
|
387
|
+
/**
|
|
388
|
+
* Installs a pending RPC wait keyed by `id`. Returns the pending call and an
|
|
389
|
+
* unregister function that MUST be called when the wait ends (success,
|
|
390
|
+
* timeout, or cancel) to avoid leaks. If a late response arrives after the
|
|
391
|
+
* caller has unregistered, it is dropped (log-and-drop) — no leak.
|
|
392
|
+
*/
|
|
393
|
+
registerRPC(id) {
|
|
394
|
+
let callResolve;
|
|
395
|
+
let callReject;
|
|
396
|
+
const call = new Promise((resolve, reject) => {
|
|
397
|
+
callResolve = resolve;
|
|
398
|
+
callReject = reject;
|
|
399
|
+
});
|
|
400
|
+
const pending = { resolve: callResolve, reject: callReject };
|
|
401
|
+
this.rpcs.set(id, pending);
|
|
402
|
+
const unregister = () => {
|
|
403
|
+
if (this.rpcs.get(id) === pending) {
|
|
404
|
+
this.rpcs.delete(id);
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
return { call, unregister };
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Installs a pending subscription stream keyed by `id`. Returns the stream
|
|
411
|
+
* channel (an async-iterable-like push sink), a `done` signal, and an
|
|
412
|
+
* unregister function. The Client pushes `next`/`complete` frames via
|
|
413
|
+
* `push`; the application reads from the channel.
|
|
414
|
+
*/
|
|
415
|
+
registerSubscription(id) {
|
|
416
|
+
let resolveDone;
|
|
417
|
+
const done = new Promise((resolve) => {
|
|
418
|
+
resolveDone = resolve;
|
|
419
|
+
});
|
|
420
|
+
const pending = {
|
|
421
|
+
push: () => {
|
|
422
|
+
},
|
|
423
|
+
done,
|
|
424
|
+
resolveDone,
|
|
425
|
+
settled: false
|
|
426
|
+
};
|
|
427
|
+
const push = (frame) => {
|
|
428
|
+
if (pending.settled) return;
|
|
429
|
+
pending.push(frame);
|
|
430
|
+
};
|
|
431
|
+
pending.push = () => {
|
|
432
|
+
};
|
|
433
|
+
this.subs.set(id, pending);
|
|
434
|
+
const unregister = () => {
|
|
435
|
+
if (this.subs.get(id) === pending) {
|
|
436
|
+
pending.settled = true;
|
|
437
|
+
this.subs.delete(id);
|
|
438
|
+
resolveDone();
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
return { push, done, unregister };
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Installs a pending receipt wait keyed by `receipt`. Returns an unregister
|
|
445
|
+
* function.
|
|
446
|
+
*/
|
|
447
|
+
registerReceipt(receipt) {
|
|
448
|
+
let resolveWait;
|
|
449
|
+
const wait = new Promise((resolve) => {
|
|
450
|
+
resolveWait = resolve;
|
|
451
|
+
});
|
|
452
|
+
this.receipts.set(receipt, resolveWait);
|
|
453
|
+
const unregister = () => {
|
|
454
|
+
this.receipts.delete(receipt);
|
|
455
|
+
};
|
|
456
|
+
return { wait, unregister };
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Wires a real sink for a registered subscription's `push`. Called by the
|
|
460
|
+
* Client right after `registerSubscription` to install the channel/queue the
|
|
461
|
+
* application reads from.
|
|
462
|
+
*/
|
|
463
|
+
setSubscriptionSink(id, sink) {
|
|
464
|
+
const sub = this.subs.get(id);
|
|
465
|
+
if (sub) sub.push = sink;
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Inspects one decoded frame, delivers it to a matching waiter if one
|
|
469
|
+
* exists, and returns `true` (consumed). Returns `false` for frames with no
|
|
470
|
+
* matching waiter — these flow on to the resolver queue / event stream.
|
|
471
|
+
* Safe to call from the message handler.
|
|
472
|
+
*/
|
|
473
|
+
route(frame) {
|
|
474
|
+
if (!frame || typeof frame !== "object") return false;
|
|
475
|
+
const typ = frame.type;
|
|
476
|
+
const id = frame.id;
|
|
477
|
+
if (typ === "response" || typ === "error") {
|
|
478
|
+
if (!id) return false;
|
|
479
|
+
const pc = this.rpcs.get(id);
|
|
480
|
+
if (!pc) return false;
|
|
481
|
+
if (typ === "error") {
|
|
482
|
+
const errObj = frame.error ?? {};
|
|
483
|
+
const code = typeof errObj.code === "number" ? errObj.code : -32603;
|
|
484
|
+
const message = errObj.message ?? "daemon error";
|
|
485
|
+
pc.reject(new DaemonError(code, message, errObj.data));
|
|
486
|
+
} else {
|
|
487
|
+
const result = frame.result ?? frame;
|
|
488
|
+
pc.resolve(result);
|
|
489
|
+
}
|
|
490
|
+
this.rpcs.delete(id);
|
|
491
|
+
return true;
|
|
492
|
+
}
|
|
493
|
+
if (typ === "next" || typ === "complete") {
|
|
494
|
+
if (!id) return false;
|
|
495
|
+
const ps = this.subs.get(id);
|
|
496
|
+
if (!ps) return false;
|
|
497
|
+
if (ps.settled) return true;
|
|
498
|
+
ps.push(frame);
|
|
499
|
+
return true;
|
|
500
|
+
}
|
|
501
|
+
if (typ === "receipt_response") {
|
|
502
|
+
const rid = frame.receipt;
|
|
503
|
+
if (!rid) return false;
|
|
504
|
+
const ch = this.receipts.get(rid);
|
|
505
|
+
if (!ch) return false;
|
|
506
|
+
ch(frame);
|
|
507
|
+
this.receipts.delete(rid);
|
|
508
|
+
return true;
|
|
509
|
+
}
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
/** Reports whether an RPC waiter is registered for `id`. */
|
|
513
|
+
hasRPCWaiter(id) {
|
|
514
|
+
return this.rpcs.has(id);
|
|
515
|
+
}
|
|
516
|
+
};
|
|
266
517
|
}
|
|
267
518
|
});
|
|
268
519
|
|
|
@@ -278,6 +529,9 @@ var init_client = __esm({
|
|
|
278
529
|
import_node_events = require("events");
|
|
279
530
|
import_ws = __toESM(require("ws"), 1);
|
|
280
531
|
init_config();
|
|
532
|
+
init_errors();
|
|
533
|
+
init_multiplexer();
|
|
534
|
+
init_intent_hints();
|
|
281
535
|
init_protocol();
|
|
282
536
|
Client = class extends import_node_events.EventEmitter {
|
|
283
537
|
url;
|
|
@@ -285,6 +539,22 @@ var init_client = __esm({
|
|
|
285
539
|
ws = null;
|
|
286
540
|
messageBuffer = [];
|
|
287
541
|
resolvers = [];
|
|
542
|
+
// Protocol-1 handshake state (RFC-450 §8.2)
|
|
543
|
+
handshakeComplete = false;
|
|
544
|
+
negotiatedCapabilities = /* @__PURE__ */ new Set();
|
|
545
|
+
protocolVersion = null;
|
|
546
|
+
readinessState = null;
|
|
547
|
+
heartbeatIntervalMs = 0;
|
|
548
|
+
heartbeatTimer = null;
|
|
549
|
+
lastPongMonotonic = 0;
|
|
550
|
+
// Mid-session drop signal (RFC-450 §8.3). The 'disconnected' event is
|
|
551
|
+
// emitted exactly once when the connection drops, carrying a DisconnectCause
|
|
552
|
+
// that distinguishes clean (peer `disconnect`) from unclean (read/write
|
|
553
|
+
// error or missed pong). `disconnFired` guards the once-only delivery.
|
|
554
|
+
disconnFired = false;
|
|
555
|
+
// Pending-request/subscription multiplexer (RFC-629 constraint #1). Routes
|
|
556
|
+
// inbound frames by (type, id) instead of discarding non-matching events.
|
|
557
|
+
mux = new Multiplexer();
|
|
288
558
|
constructor(url, config) {
|
|
289
559
|
super();
|
|
290
560
|
this.url = url;
|
|
@@ -293,7 +563,10 @@ var init_client = __esm({
|
|
|
293
563
|
// ---------------------------------------------------------------------------
|
|
294
564
|
// Connection lifecycle
|
|
295
565
|
// ---------------------------------------------------------------------------
|
|
296
|
-
/**
|
|
566
|
+
/**
|
|
567
|
+
* Dials the Soothe daemon WebSocket and completes the protocol-1 handshake
|
|
568
|
+
* (connection_init → connection_ack with readiness_state "ready").
|
|
569
|
+
*/
|
|
297
570
|
connect() {
|
|
298
571
|
return new Promise((resolve, reject) => {
|
|
299
572
|
const ws = new import_ws.default(this.url, {
|
|
@@ -301,9 +574,27 @@ var init_client = __esm({
|
|
|
301
574
|
});
|
|
302
575
|
ws.on("open", () => {
|
|
303
576
|
this.ws = ws;
|
|
304
|
-
|
|
577
|
+
this.disconnFired = false;
|
|
578
|
+
this._lastCause = null;
|
|
579
|
+
this.mux = new Multiplexer();
|
|
580
|
+
this._performHandshake().then((ack) => {
|
|
581
|
+
this.handshakeComplete = true;
|
|
582
|
+
this.readinessState = ack.result?.readiness_state ?? "ready";
|
|
583
|
+
this._startHeartbeat();
|
|
584
|
+
resolve();
|
|
585
|
+
}).catch((err) => {
|
|
586
|
+
this._stopHeartbeat();
|
|
587
|
+
this.ws = null;
|
|
588
|
+
this.handshakeComplete = false;
|
|
589
|
+
try {
|
|
590
|
+
ws.close(1011, "handshake failed");
|
|
591
|
+
} catch {
|
|
592
|
+
}
|
|
593
|
+
reject(err);
|
|
594
|
+
});
|
|
305
595
|
});
|
|
306
596
|
ws.on("error", (err) => {
|
|
597
|
+
this._signalDisconnect(0 /* Unclean */);
|
|
307
598
|
if (!this.ws) {
|
|
308
599
|
reject(new Error(`soothe dial: ${err.message}`));
|
|
309
600
|
}
|
|
@@ -311,20 +602,42 @@ var init_client = __esm({
|
|
|
311
602
|
ws.on("message", (data) => {
|
|
312
603
|
const text = data.toString();
|
|
313
604
|
for (const frame of splitWirePayload(text)) {
|
|
605
|
+
let msg;
|
|
314
606
|
try {
|
|
315
|
-
|
|
316
|
-
if (msg !== null) {
|
|
317
|
-
this.messageBuffer.push(msg);
|
|
318
|
-
this.emit("message", msg);
|
|
319
|
-
const resolver = this.resolvers.shift();
|
|
320
|
-
if (resolver) resolver(msg);
|
|
321
|
-
}
|
|
607
|
+
msg = decodeMessage(frame);
|
|
322
608
|
} catch {
|
|
609
|
+
continue;
|
|
323
610
|
}
|
|
611
|
+
if (msg === null) continue;
|
|
612
|
+
const m = msg;
|
|
613
|
+
if (m.type === "ping") {
|
|
614
|
+
this._sendRaw(pongEnvelope());
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
if (m.type === "pong") {
|
|
618
|
+
this.lastPongMonotonic = Date.now();
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
if (m.type === "disconnect") {
|
|
622
|
+
this._signalDisconnect(1 /* Clean */);
|
|
623
|
+
}
|
|
624
|
+
if (this.mux.route(m)) {
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
const resolver = this.resolvers.shift();
|
|
628
|
+
if (resolver) {
|
|
629
|
+
resolver(msg);
|
|
630
|
+
} else {
|
|
631
|
+
this.messageBuffer.push(msg);
|
|
632
|
+
}
|
|
633
|
+
this.emit("message", msg);
|
|
324
634
|
}
|
|
325
635
|
});
|
|
326
636
|
ws.on("close", () => {
|
|
327
637
|
this.ws = null;
|
|
638
|
+
this._stopHeartbeat();
|
|
639
|
+
this.handshakeComplete = false;
|
|
640
|
+
this._signalDisconnect(0 /* Unclean */);
|
|
328
641
|
this.emit("close");
|
|
329
642
|
for (const resolver of this.resolvers) {
|
|
330
643
|
resolver(null);
|
|
@@ -333,18 +646,219 @@ var init_client = __esm({
|
|
|
333
646
|
});
|
|
334
647
|
});
|
|
335
648
|
}
|
|
336
|
-
/**
|
|
649
|
+
/** Sends a `disconnect` notification and closes the WebSocket. */
|
|
337
650
|
close() {
|
|
651
|
+
this._stopHeartbeat();
|
|
338
652
|
if (!this.ws) return;
|
|
339
653
|
try {
|
|
654
|
+
if (this.ws.readyState === import_ws.default.OPEN) {
|
|
655
|
+
this.ws.send(JSON.stringify(disconnectEnvelope()));
|
|
656
|
+
}
|
|
340
657
|
this.ws.close(1e3, "");
|
|
341
658
|
} catch {
|
|
342
659
|
}
|
|
343
660
|
this.ws = null;
|
|
661
|
+
this.handshakeComplete = false;
|
|
344
662
|
}
|
|
345
|
-
/** Returns whether the client has an active WebSocket connection. */
|
|
663
|
+
/** Returns whether the client has an active, handshaked WebSocket connection. */
|
|
346
664
|
isConnected() {
|
|
347
|
-
return this.ws !== null && this.ws.readyState === import_ws.default.OPEN;
|
|
665
|
+
return this.ws !== null && this.ws.readyState === import_ws.default.OPEN && this.handshakeComplete;
|
|
666
|
+
}
|
|
667
|
+
// ---------------------------------------------------------------------------
|
|
668
|
+
// Mid-session drop signal + reconnect/reattach (RFC-450 §8.3, RFC-629 L0)
|
|
669
|
+
// ---------------------------------------------------------------------------
|
|
670
|
+
/**
|
|
671
|
+
* Returns whether the connection has dropped (the `'disconnected'` event has
|
|
672
|
+
* fired). Pair with the `'disconnected'` event for the signal. Use
|
|
673
|
+
* `disconnectCause()` to read the cause.
|
|
674
|
+
*/
|
|
675
|
+
isDisconnected() {
|
|
676
|
+
return this.disconnFired;
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Returns the cause of the most recent drop, or `null` if the connection has
|
|
680
|
+
* not dropped. Clean follows a `disconnect` notification (loops keep running
|
|
681
|
+
* server-side); unclean is a read/write error or missed pong.
|
|
682
|
+
*/
|
|
683
|
+
disconnectCause() {
|
|
684
|
+
if (!this.disconnFired) return null;
|
|
685
|
+
return this._lastCause ?? 0 /* Unclean */;
|
|
686
|
+
}
|
|
687
|
+
_lastCause = null;
|
|
688
|
+
/**
|
|
689
|
+
* Delivers the disconnect cause exactly once via the `'disconnected'` event.
|
|
690
|
+
* Safe to call from any path; subsequent calls are no-ops. Listeners receive
|
|
691
|
+
* the cause as the event argument.
|
|
692
|
+
*/
|
|
693
|
+
_signalDisconnect(cause) {
|
|
694
|
+
if (this.disconnFired) return;
|
|
695
|
+
this.disconnFired = true;
|
|
696
|
+
this._lastCause = cause;
|
|
697
|
+
try {
|
|
698
|
+
this.emit("disconnected", cause);
|
|
699
|
+
} catch {
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Re-dials the daemon and re-handshakes after a connection drop (RFC-450
|
|
704
|
+
* §8.3). Does not re-establish loop subscriptions; follow with
|
|
705
|
+
* `reattachAndProbe()` to resume a loop session. The caller should invoke
|
|
706
|
+
* this after the `'disconnected'` event fires. Reuses the same Client,
|
|
707
|
+
* resetting the drop signal and multiplexer.
|
|
708
|
+
*
|
|
709
|
+
* Performs bounded-retry backoff using the configured reconnect knobs.
|
|
710
|
+
*/
|
|
711
|
+
async reconnect() {
|
|
712
|
+
const maxAttempts = this.config.reconnectMaxAttempts || 10;
|
|
713
|
+
const initialDelay = this.config.reconnectInitialDelay || 500;
|
|
714
|
+
const maxDelay = this.config.reconnectMaxDelay || 1e4;
|
|
715
|
+
let lastErr = null;
|
|
716
|
+
let delay = initialDelay;
|
|
717
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
718
|
+
try {
|
|
719
|
+
await this.connect();
|
|
720
|
+
return;
|
|
721
|
+
} catch (err) {
|
|
722
|
+
lastErr = err;
|
|
723
|
+
}
|
|
724
|
+
if (attempt < maxAttempts) {
|
|
725
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
726
|
+
delay = Math.min(delay * 2, maxDelay);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
throw new ReconnectError(this.url, maxAttempts, lastErr ?? new Error("unknown error"));
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Resumes an existing loop after a reconnect: issues `loop_reattach`,
|
|
733
|
+
* re-subscribes to `loop_events`, then runs a `loop_get` liveness probe to
|
|
734
|
+
* detect stale loops that accept the handshake but silently drop input.
|
|
735
|
+
* Returns a `StaleLoopError` when the probe fails; callers should fall back
|
|
736
|
+
* to a fresh `loop_new` bootstrap.
|
|
737
|
+
*
|
|
738
|
+
* Per RFC-629: connection-level readiness is the handshake's readiness_state
|
|
739
|
+
* (+ daemon_status); loop_get is a loop-scoped probe only, not a readiness
|
|
740
|
+
* probe.
|
|
741
|
+
*/
|
|
742
|
+
async reattachAndProbe(loopID) {
|
|
743
|
+
if (!loopID || !loopID.trim()) {
|
|
744
|
+
throw new Error("soothe: reattachAndProbe requires a loop id");
|
|
745
|
+
}
|
|
746
|
+
const lid = loopID.trim();
|
|
747
|
+
const reattachTimeout = this.config.loopStatusTimeout || 15e3;
|
|
748
|
+
try {
|
|
749
|
+
await this.requestResponse(
|
|
750
|
+
"loop_reattach",
|
|
751
|
+
{ loop_id: lid },
|
|
752
|
+
"loop_reattach",
|
|
753
|
+
reattachTimeout
|
|
754
|
+
);
|
|
755
|
+
} catch (err) {
|
|
756
|
+
throw new Error(`loop_reattach: ${err.message}`);
|
|
757
|
+
}
|
|
758
|
+
const subTimeout = this.config.subscriptionTimeout || 1e4;
|
|
759
|
+
try {
|
|
760
|
+
await this.subscribe(
|
|
761
|
+
"loop_events",
|
|
762
|
+
{ loop_id: lid, verbosity: this.config.verbosityLevel },
|
|
763
|
+
subTimeout
|
|
764
|
+
);
|
|
765
|
+
} catch (err) {
|
|
766
|
+
throw new Error(`loop events subscription failed: ${err.message}`);
|
|
767
|
+
}
|
|
768
|
+
const probeTimeout = this.config.reattachProbeTimeout || 5e3;
|
|
769
|
+
try {
|
|
770
|
+
await this.getLoop(lid, probeTimeout);
|
|
771
|
+
} catch (err) {
|
|
772
|
+
if (err instanceof DaemonError && err.code === -32200) {
|
|
773
|
+
throw new StaleLoopError(lid, err);
|
|
774
|
+
}
|
|
775
|
+
throw new StaleLoopError(lid, err);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
// ---------------------------------------------------------------------------
|
|
779
|
+
// Protocol-1 handshake (RFC-450 §8.2)
|
|
780
|
+
// ---------------------------------------------------------------------------
|
|
781
|
+
/** Send connection_init and wait for connection_ack with readiness "ready". */
|
|
782
|
+
async _performHandshake() {
|
|
783
|
+
const init = connectionInitEnvelope({
|
|
784
|
+
client_version: CLIENT_VERSION,
|
|
785
|
+
client_name: "soothe-client-ts",
|
|
786
|
+
accept_proto: [PROTO_VERSION],
|
|
787
|
+
capabilities: DEFAULT_CLIENT_CAPABILITIES
|
|
788
|
+
});
|
|
789
|
+
await this.sendMessage(init);
|
|
790
|
+
const deadline = Date.now() + this.config.daemonReadyTimeout;
|
|
791
|
+
while (Date.now() < deadline) {
|
|
792
|
+
const remaining = deadline - Date.now();
|
|
793
|
+
if (remaining <= 0) break;
|
|
794
|
+
const ev = await this.readEventWithTimeout(remaining);
|
|
795
|
+
if (ev === null) {
|
|
796
|
+
throw new Error("connection closed during handshake");
|
|
797
|
+
}
|
|
798
|
+
if (ev.type === "status") {
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
if (ev.type !== "connection_ack") {
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
const ack = ev;
|
|
805
|
+
const result = ack.result ?? {};
|
|
806
|
+
const state = result.readiness_state ?? "ready";
|
|
807
|
+
this.protocolVersion = result.protocol_version ?? PROTO_VERSION;
|
|
808
|
+
this.negotiatedCapabilities = new Set(result.capabilities ?? []);
|
|
809
|
+
this.heartbeatIntervalMs = result.heartbeat_interval_ms ?? 0;
|
|
810
|
+
if (state === "incompatible") {
|
|
811
|
+
throw new Error(`protocol version incompatible: daemon returned ${this.protocolVersion}`);
|
|
812
|
+
}
|
|
813
|
+
if (state === "ready") {
|
|
814
|
+
return ack;
|
|
815
|
+
}
|
|
816
|
+
if (state === "error") {
|
|
817
|
+
throw new Error("daemon startup failed");
|
|
818
|
+
}
|
|
819
|
+
if (state === "degraded") {
|
|
820
|
+
throw new Error("daemon is degraded");
|
|
821
|
+
}
|
|
822
|
+
await this._sleep(50);
|
|
823
|
+
await this.sendMessage(init);
|
|
824
|
+
}
|
|
825
|
+
throw new Error(`timeout after ${this.config.daemonReadyTimeout}ms waiting for connection_ack`);
|
|
826
|
+
}
|
|
827
|
+
// ---------------------------------------------------------------------------
|
|
828
|
+
// Heartbeat (RFC-450 §8.3)
|
|
829
|
+
// ---------------------------------------------------------------------------
|
|
830
|
+
_startHeartbeat() {
|
|
831
|
+
if (!this.negotiatedCapabilities.has("heartbeat")) return;
|
|
832
|
+
const interval = this.heartbeatIntervalMs;
|
|
833
|
+
if (interval <= 0) return;
|
|
834
|
+
this.lastPongMonotonic = Date.now();
|
|
835
|
+
this.heartbeatTimer = setInterval(() => this._heartbeatTick(interval), interval);
|
|
836
|
+
}
|
|
837
|
+
_stopHeartbeat() {
|
|
838
|
+
if (this.heartbeatTimer) {
|
|
839
|
+
clearInterval(this.heartbeatTimer);
|
|
840
|
+
this.heartbeatTimer = null;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
_heartbeatTick(intervalMs) {
|
|
844
|
+
if (!this.ws || this.ws.readyState !== import_ws.default.OPEN) return;
|
|
845
|
+
const timeoutMs = Math.max(1e4, intervalMs * 2);
|
|
846
|
+
const now = Date.now();
|
|
847
|
+
if (now - (this.lastPongMonotonic || now) > intervalMs + timeoutMs) {
|
|
848
|
+
this._signalDisconnect(0 /* Unclean */);
|
|
849
|
+
try {
|
|
850
|
+
this.ws.close(1001, "heartbeat timeout");
|
|
851
|
+
} catch {
|
|
852
|
+
}
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
try {
|
|
856
|
+
this.ws.send(JSON.stringify(pingEnvelope()));
|
|
857
|
+
} catch {
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
_sleep(ms) {
|
|
861
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
348
862
|
}
|
|
349
863
|
// ---------------------------------------------------------------------------
|
|
350
864
|
// Core messaging
|
|
@@ -352,17 +866,29 @@ var init_client = __esm({
|
|
|
352
866
|
/** Serializes msg as JSON and sends it as a WebSocket text frame. */
|
|
353
867
|
sendMessage(msg) {
|
|
354
868
|
return new Promise((resolve, reject) => {
|
|
355
|
-
if (!this.ws) {
|
|
869
|
+
if (!this.ws || this.ws.readyState !== import_ws.default.OPEN) {
|
|
356
870
|
reject(new Error("soothe: not connected"));
|
|
357
871
|
return;
|
|
358
872
|
}
|
|
359
873
|
const payload = JSON.stringify(msg);
|
|
360
874
|
this.ws.send(payload, (err) => {
|
|
361
|
-
if (err)
|
|
362
|
-
|
|
875
|
+
if (err) {
|
|
876
|
+
this._signalDisconnect(0 /* Unclean */);
|
|
877
|
+
reject(err);
|
|
878
|
+
} else {
|
|
879
|
+
resolve();
|
|
880
|
+
}
|
|
363
881
|
});
|
|
364
882
|
});
|
|
365
883
|
}
|
|
884
|
+
/** Low-level send that does not reject on a missing connection (best-effort). */
|
|
885
|
+
_sendRaw(msg) {
|
|
886
|
+
if (!this.ws || this.ws.readyState !== import_ws.default.OPEN) return;
|
|
887
|
+
try {
|
|
888
|
+
this.ws.send(JSON.stringify(msg));
|
|
889
|
+
} catch {
|
|
890
|
+
}
|
|
891
|
+
}
|
|
366
892
|
/** Returns an async iterable of decoded messages. Ends when connection closes. */
|
|
367
893
|
async *receiveMessages(signal) {
|
|
368
894
|
while (true) {
|
|
@@ -395,7 +921,7 @@ var init_client = __esm({
|
|
|
395
921
|
if (msg === null) return null;
|
|
396
922
|
return msg;
|
|
397
923
|
}
|
|
398
|
-
/** Reads a single event with a timeout. Returns null on timeout or
|
|
924
|
+
/** Reads a single event with a timeout. Returns null on timeout or close. */
|
|
399
925
|
readEventWithTimeout(timeout) {
|
|
400
926
|
if (this.messageBuffer.length > 0) {
|
|
401
927
|
const msg = this.messageBuffer.shift();
|
|
@@ -416,269 +942,490 @@ var init_client = __esm({
|
|
|
416
942
|
});
|
|
417
943
|
}
|
|
418
944
|
// ---------------------------------------------------------------------------
|
|
945
|
+
// Protocol-1 RPC primitives (RFC-450 §5/§9)
|
|
946
|
+
// ---------------------------------------------------------------------------
|
|
947
|
+
/**
|
|
948
|
+
* Reads the next frame directly from the live socket (via a resolver),
|
|
949
|
+
* bypassing `messageBuffer`. Used by RPC waits so that stream events
|
|
950
|
+
* previously buffered for `readEvent()`/`receiveMessages()` consumers are
|
|
951
|
+
* not re-cycled through the RPC wait loop (which would stall behind a
|
|
952
|
+
* continuous subscription stream). Non-RPC frames read here are pushed to
|
|
953
|
+
* `messageBuffer` for the stream readers.
|
|
954
|
+
*/
|
|
955
|
+
readLiveEventWithTimeout(timeout) {
|
|
956
|
+
if (!this.ws) return Promise.resolve(null);
|
|
957
|
+
return new Promise((resolve) => {
|
|
958
|
+
const timer = setTimeout(() => {
|
|
959
|
+
const idx = this.resolvers.indexOf(resolver);
|
|
960
|
+
if (idx >= 0) this.resolvers.splice(idx, 1);
|
|
961
|
+
resolve(null);
|
|
962
|
+
}, timeout);
|
|
963
|
+
const resolver = (val) => {
|
|
964
|
+
clearTimeout(timer);
|
|
965
|
+
resolve(val);
|
|
966
|
+
};
|
|
967
|
+
this.resolvers.push(resolver);
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* Sends a `request` envelope and waits for the matching `response` (or
|
|
972
|
+
* `error`) correlated by `id` (RFC-450 §5/§9). Returns the `result` object.
|
|
973
|
+
*
|
|
974
|
+
* Multiplexer-aware (RFC-629 constraint #1): registers a pending RPC wait
|
|
975
|
+
* keyed by the request id so that, even when a `receiveMessages()` reader
|
|
976
|
+
* is concurrently active, the matching `response`/`error` is routed to
|
|
977
|
+
* this caller instead of being discarded or buffered behind a stream.
|
|
978
|
+
* Non-matching frames are routed to their own waiters by the multiplexer
|
|
979
|
+
* or flow on to the resolver queue for stream readers.
|
|
980
|
+
*/
|
|
981
|
+
async requestResponse(method, params, responseType, timeout = 15e3) {
|
|
982
|
+
const req = requestEnvelope(method, params);
|
|
983
|
+
const rid = req.id;
|
|
984
|
+
const { call, unregister } = this.mux.registerRPC(rid);
|
|
985
|
+
const label = responseType ?? method;
|
|
986
|
+
try {
|
|
987
|
+
await this.sendMessage(req);
|
|
988
|
+
const result = await this._raceRPC(call, timeout, label);
|
|
989
|
+
return result;
|
|
990
|
+
} finally {
|
|
991
|
+
unregister();
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
/**
|
|
995
|
+
* Races the multiplexer's RPC promise against a timeout and the connection
|
|
996
|
+
* drop signal. Resolves with the `result` on `response`; rejects with a
|
|
997
|
+
* `DaemonError` on `error`; rejects with a timeout/close error otherwise.
|
|
998
|
+
* The disconnect listener is always removed to avoid accumulating handlers.
|
|
999
|
+
*/
|
|
1000
|
+
async _raceRPC(call, timeout, label) {
|
|
1001
|
+
let timer;
|
|
1002
|
+
let cleanupDisconnect = () => {
|
|
1003
|
+
};
|
|
1004
|
+
const timeoutP = new Promise((_, reject) => {
|
|
1005
|
+
timer = setTimeout(
|
|
1006
|
+
() => reject(new Error(`timeout after ${timeout}ms waiting for ${label}`)),
|
|
1007
|
+
timeout
|
|
1008
|
+
);
|
|
1009
|
+
});
|
|
1010
|
+
const closedP = new Promise((_, reject) => {
|
|
1011
|
+
if (this.disconnFired) {
|
|
1012
|
+
reject(new Error(`connection closed waiting for ${label}`));
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
const onDisconnect = () => {
|
|
1016
|
+
reject(new Error(`connection closed waiting for ${label}`));
|
|
1017
|
+
};
|
|
1018
|
+
this.once("disconnected", onDisconnect);
|
|
1019
|
+
cleanupDisconnect = () => this.removeListener("disconnected", onDisconnect);
|
|
1020
|
+
});
|
|
1021
|
+
try {
|
|
1022
|
+
return await Promise.race([call, timeoutP, closedP]);
|
|
1023
|
+
} finally {
|
|
1024
|
+
if (timer) clearTimeout(timer);
|
|
1025
|
+
cleanupDisconnect();
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* Sends a pre-built envelope (e.g. `unsubscribe`) that carries an `id` and
|
|
1030
|
+
* waits for the matching `response`/`error`. Used for envelope types that
|
|
1031
|
+
* are not `request` (e.g. `unsubscribe` → `autopilot_unsubscribe`) but still
|
|
1032
|
+
* expect a correlated response from the daemon.
|
|
1033
|
+
*/
|
|
1034
|
+
async _requestResponseForEnvelope(env, label, timeout) {
|
|
1035
|
+
const rid = env.id;
|
|
1036
|
+
const { call, unregister } = this.mux.registerRPC(rid);
|
|
1037
|
+
try {
|
|
1038
|
+
await this.sendMessage(env);
|
|
1039
|
+
return await this._raceRPC(call, timeout, label);
|
|
1040
|
+
} finally {
|
|
1041
|
+
unregister();
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
/** Sends a fire-and-forget `notification` envelope (no response expected). */
|
|
1045
|
+
notify(method, params) {
|
|
1046
|
+
return this.sendMessage(notificationEnvelope(method, params));
|
|
1047
|
+
}
|
|
1048
|
+
/**
|
|
1049
|
+
* Starts a subscription stream. Returns the subscription `id` for later
|
|
1050
|
+
* correlation and `unsubscribe()`. Stream events arrive as `next` frames
|
|
1051
|
+
* carrying the same `id`.
|
|
1052
|
+
*/
|
|
1053
|
+
async subscribe(method, params, timeout = 5e3) {
|
|
1054
|
+
const req = subscribeEnvelope(method, params);
|
|
1055
|
+
const subId = req.id;
|
|
1056
|
+
await this.sendMessage(req);
|
|
1057
|
+
const deadline = Date.now() + timeout;
|
|
1058
|
+
while (Date.now() < deadline) {
|
|
1059
|
+
const remaining = deadline - Date.now();
|
|
1060
|
+
if (remaining <= 0) break;
|
|
1061
|
+
const ev = await this.readLiveEventWithTimeout(remaining);
|
|
1062
|
+
if (ev === null) break;
|
|
1063
|
+
const evId = ev.id;
|
|
1064
|
+
if (evId !== subId) {
|
|
1065
|
+
this.messageBuffer.push(ev);
|
|
1066
|
+
continue;
|
|
1067
|
+
}
|
|
1068
|
+
const typ = ev.type;
|
|
1069
|
+
if (typ === "error") {
|
|
1070
|
+
const errObj = ev.error ?? {};
|
|
1071
|
+
throw new DaemonError(
|
|
1072
|
+
errObj.code ?? -32603,
|
|
1073
|
+
errObj.message ?? "subscription rejected",
|
|
1074
|
+
errObj.data
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
1077
|
+
if (typ === "next" || typ === "complete") {
|
|
1078
|
+
this.messageBuffer.unshift(ev);
|
|
1079
|
+
break;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
return subId;
|
|
1083
|
+
}
|
|
1084
|
+
/** Cancels an active subscription by id. */
|
|
1085
|
+
unsubscribe(subscriptionId) {
|
|
1086
|
+
return this.sendMessage(unsubscribeEnvelope(subscriptionId));
|
|
1087
|
+
}
|
|
1088
|
+
/**
|
|
1089
|
+
* Reads the next stream event from a subscription. For `next` frames the
|
|
1090
|
+
* `payload` is returned; for `complete`/`error` the full envelope is
|
|
1091
|
+
* returned so the caller can inspect termination.
|
|
1092
|
+
*/
|
|
1093
|
+
async next() {
|
|
1094
|
+
const ev = await this.readEvent();
|
|
1095
|
+
if (ev === null) return null;
|
|
1096
|
+
if (ev.type === "next") {
|
|
1097
|
+
return ev.payload ?? {};
|
|
1098
|
+
}
|
|
1099
|
+
return ev;
|
|
1100
|
+
}
|
|
1101
|
+
// ---------------------------------------------------------------------------
|
|
419
1102
|
// High-level API methods (Loop-first, RFC-503)
|
|
420
1103
|
// ---------------------------------------------------------------------------
|
|
421
|
-
/** Sends user input to the daemon (loop_input; requires loopID). */
|
|
1104
|
+
/** Sends user input to the daemon (loop_input notification; requires loopID). */
|
|
422
1105
|
sendInput(text, options) {
|
|
423
1106
|
const loopId = (options?.loopID ?? "").trim();
|
|
424
1107
|
if (!loopId) {
|
|
425
1108
|
return Promise.reject(new Error("sendInput requires options.loopID"));
|
|
426
1109
|
}
|
|
427
|
-
const
|
|
428
|
-
type: "loop_input",
|
|
1110
|
+
const params = {
|
|
429
1111
|
loop_id: loopId,
|
|
430
1112
|
content: text,
|
|
431
1113
|
autonomous: options?.autonomous ?? false
|
|
432
1114
|
};
|
|
433
|
-
if (options?.maxIterations !== void 0)
|
|
434
|
-
if (options?.subagent)
|
|
435
|
-
if (options?.
|
|
436
|
-
if (options?.
|
|
437
|
-
if (options?.
|
|
438
|
-
if (options?.
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
1115
|
+
if (options?.maxIterations !== void 0) params.max_iterations = options.maxIterations;
|
|
1116
|
+
if (options?.subagent) params.preferred_subagent = options.subagent;
|
|
1117
|
+
if (options?.model) params.model = options.model;
|
|
1118
|
+
if (options?.modelParams) params.model_params = options.modelParams;
|
|
1119
|
+
if (options?.attachments) params.attachments = options.attachments;
|
|
1120
|
+
if (options?.intentHint) {
|
|
1121
|
+
const hintError = validateLoopInputIntentHint(options.intentHint);
|
|
1122
|
+
if (hintError) {
|
|
1123
|
+
return Promise.reject(new Error(hintError));
|
|
1124
|
+
}
|
|
1125
|
+
params.intent_hint = options.intentHint;
|
|
1126
|
+
}
|
|
1127
|
+
if (options?.responseSchema) params.response_schema = options.responseSchema;
|
|
1128
|
+
if (options?.responseSchemaName) params.response_schema_name = options.responseSchemaName;
|
|
1129
|
+
if (options?.responseSchemaStrict !== void 0)
|
|
1130
|
+
params.response_schema_strict = options.responseSchemaStrict;
|
|
1131
|
+
if (options?.clarificationMode) params.clarification_mode = options.clarificationMode;
|
|
1132
|
+
if (options?.clarificationAnswer) params.clarification_answer = true;
|
|
1133
|
+
if (options?.clarificationAnswers) params.clarification_answers = options.clarificationAnswers;
|
|
1134
|
+
return this.notify("loop_input", params);
|
|
1135
|
+
}
|
|
1136
|
+
/** Sends a slash command to the daemon (slash_command notification). */
|
|
442
1137
|
sendCommand(cmd) {
|
|
443
|
-
return this.
|
|
1138
|
+
return this.notify("slash_command", { cmd });
|
|
444
1139
|
}
|
|
445
1140
|
// ---------------------------------------------------------------------------
|
|
446
1141
|
// Loop lifecycle methods (RFC-503)
|
|
447
1142
|
// ---------------------------------------------------------------------------
|
|
448
|
-
/** Requests the daemon to create a new
|
|
1143
|
+
/** Requests the daemon to create a new StrangeLoop and waits for the response. */
|
|
449
1144
|
sendLoopNew(opts) {
|
|
450
1145
|
return this.sendMessage(newLoopNewMessage(opts));
|
|
451
1146
|
}
|
|
452
|
-
/** Subscribes to events for a loop. */
|
|
453
|
-
sendLoopSubscribe(loopID, verbosity, streamDelivery) {
|
|
454
|
-
|
|
455
|
-
if (streamDelivery) {
|
|
456
|
-
msg.stream_delivery = streamDelivery;
|
|
457
|
-
}
|
|
458
|
-
return this.sendMessage(msg);
|
|
459
|
-
}
|
|
460
|
-
/** Detaches from a loop (keeps loop running). */
|
|
461
|
-
sendLoopDetach(loopID, requestID) {
|
|
462
|
-
return this.sendMessage({
|
|
463
|
-
type: "loop_detach",
|
|
1147
|
+
/** Subscribes to events for a loop (subscribe → loop_events). */
|
|
1148
|
+
async sendLoopSubscribe(loopID, verbosity, streamDelivery) {
|
|
1149
|
+
await this.subscribe("loop_events", {
|
|
464
1150
|
loop_id: loopID,
|
|
465
|
-
|
|
1151
|
+
verbosity,
|
|
1152
|
+
stream_delivery: streamDelivery
|
|
466
1153
|
});
|
|
467
1154
|
}
|
|
468
|
-
/**
|
|
469
|
-
|
|
470
|
-
return this.sendMessage(
|
|
1155
|
+
/** Detaches from a loop (unsubscribe by subscription id). */
|
|
1156
|
+
sendLoopDetach(loopID) {
|
|
1157
|
+
return this.sendMessage(unsubscribeEnvelope(loopID));
|
|
471
1158
|
}
|
|
472
|
-
/**
|
|
473
|
-
|
|
474
|
-
return this.sendMessage(
|
|
1159
|
+
/** Notifies the daemon that this client is leaving (disconnect notification). */
|
|
1160
|
+
sendDetach() {
|
|
1161
|
+
return this.sendMessage(disconnectEnvelope());
|
|
475
1162
|
}
|
|
476
1163
|
/** Requests daemon status check. */
|
|
477
|
-
sendDaemonStatus(
|
|
478
|
-
return this.sendMessage({
|
|
479
|
-
type: "daemon_status",
|
|
480
|
-
request_id: requestID ?? newRequestID()
|
|
481
|
-
});
|
|
1164
|
+
sendDaemonStatus() {
|
|
1165
|
+
return this.sendMessage(requestEnvelope("daemon_status", {}));
|
|
482
1166
|
}
|
|
483
1167
|
/** Requests daemon shutdown. */
|
|
484
|
-
sendDaemonShutdown(
|
|
485
|
-
return this.sendMessage({
|
|
486
|
-
type: "daemon_shutdown",
|
|
487
|
-
request_id: requestID ?? newRequestID()
|
|
488
|
-
});
|
|
1168
|
+
sendDaemonShutdown() {
|
|
1169
|
+
return this.sendMessage(requestEnvelope("daemon_shutdown", {}));
|
|
489
1170
|
}
|
|
490
1171
|
/** Requests a config section from the daemon. */
|
|
491
|
-
sendConfigGet(section
|
|
492
|
-
return this.sendMessage({
|
|
493
|
-
type: "config_get",
|
|
494
|
-
section,
|
|
495
|
-
request_id: requestID ?? newRequestID()
|
|
496
|
-
});
|
|
1172
|
+
sendConfigGet(section) {
|
|
1173
|
+
return this.sendMessage(requestEnvelope("config_get", { section }));
|
|
497
1174
|
}
|
|
498
1175
|
// ---------------------------------------------------------------------------
|
|
499
|
-
//
|
|
1176
|
+
// Convenience RPC methods (blocking request/response)
|
|
500
1177
|
// ---------------------------------------------------------------------------
|
|
501
|
-
/** Requests the
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
type: "loop_list",
|
|
505
|
-
request_id: requestID ?? newRequestID()
|
|
506
|
-
};
|
|
507
|
-
if (filter) msg.filter = filter;
|
|
508
|
-
if (limit !== void 0) msg.limit = limit;
|
|
509
|
-
return this.sendMessage(msg);
|
|
510
|
-
}
|
|
511
|
-
/** Requests detailed loop metadata. */
|
|
512
|
-
sendLoopGet(loopID, verbose, requestID) {
|
|
513
|
-
const msg = {
|
|
514
|
-
type: "loop_get",
|
|
515
|
-
loop_id: loopID,
|
|
516
|
-
request_id: requestID ?? newRequestID()
|
|
517
|
-
};
|
|
518
|
-
if (verbose) msg.verbose = verbose;
|
|
519
|
-
return this.sendMessage(msg);
|
|
520
|
-
}
|
|
521
|
-
/** Requests loop tree visualization. */
|
|
522
|
-
sendLoopTree(loopID, format, requestID) {
|
|
523
|
-
const msg = {
|
|
524
|
-
type: "loop_tree",
|
|
525
|
-
loop_id: loopID,
|
|
526
|
-
request_id: requestID ?? newRequestID()
|
|
527
|
-
};
|
|
528
|
-
if (format) msg.format = format;
|
|
529
|
-
return this.sendMessage(msg);
|
|
1178
|
+
/** Requests the skills catalog and waits for the response. */
|
|
1179
|
+
listSkills(timeout) {
|
|
1180
|
+
return this.requestResponse("skills_list", {}, "skills_list", timeout ?? 15e3);
|
|
530
1181
|
}
|
|
531
|
-
/** Requests
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
type: "loop_prune",
|
|
535
|
-
loop_id: loopID,
|
|
536
|
-
request_id: requestID ?? newRequestID()
|
|
537
|
-
};
|
|
538
|
-
if (retentionDays !== void 0) msg.retention_days = retentionDays;
|
|
539
|
-
if (dryRun !== void 0) msg.dry_run = dryRun;
|
|
540
|
-
return this.sendMessage(msg);
|
|
541
|
-
}
|
|
542
|
-
/** Requests loop deletion. */
|
|
543
|
-
sendLoopDelete(loopID, requestID) {
|
|
544
|
-
return this.sendMessage({
|
|
545
|
-
type: "loop_delete",
|
|
546
|
-
loop_id: loopID,
|
|
547
|
-
request_id: requestID ?? newRequestID()
|
|
548
|
-
});
|
|
1182
|
+
/** Requests the models catalog and waits for the response. */
|
|
1183
|
+
listModels(timeout) {
|
|
1184
|
+
return this.requestResponse("models_list", {}, "models_list", timeout ?? 15e3);
|
|
549
1185
|
}
|
|
550
|
-
/**
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
loop_id: loopID,
|
|
555
|
-
request_id: requestID ?? newRequestID()
|
|
556
|
-
});
|
|
1186
|
+
/** Invokes a skill on the daemon host and receives echo. */
|
|
1187
|
+
invokeSkill(skill, args, timeout) {
|
|
1188
|
+
const params = { skill, args: args ?? "" };
|
|
1189
|
+
return this.requestResponse("invoke_skill", params, "invoke_skill", timeout ?? 12e4);
|
|
557
1190
|
}
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
return this.sendMessage({
|
|
564
|
-
type: "skills_list",
|
|
565
|
-
request_id: requestID ?? newRequestID()
|
|
566
|
-
});
|
|
1191
|
+
/** Requests loop list and waits for response. */
|
|
1192
|
+
listLoops(timeout, workspace) {
|
|
1193
|
+
const params = {};
|
|
1194
|
+
if (workspace) params.filter = { workspace };
|
|
1195
|
+
return this.requestResponse("loop_list", params, "loop_list", timeout ?? 15e3);
|
|
567
1196
|
}
|
|
568
|
-
/** Requests
|
|
569
|
-
|
|
570
|
-
return this.
|
|
571
|
-
type: "models_list",
|
|
572
|
-
request_id: requestID ?? newRequestID()
|
|
573
|
-
});
|
|
1197
|
+
/** Requests loop details and waits for response. */
|
|
1198
|
+
getLoop(loopID, timeout) {
|
|
1199
|
+
return this.requestResponse("loop_get", { loop_id: loopID }, "loop_get", timeout ?? 15e3);
|
|
574
1200
|
}
|
|
575
|
-
/**
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
type: "invoke_skill",
|
|
579
|
-
skill,
|
|
580
|
-
request_id: requestID ?? newRequestID()
|
|
581
|
-
};
|
|
582
|
-
if (args) msg.args = args;
|
|
583
|
-
return this.sendMessage(msg);
|
|
1201
|
+
/** Requests loop tree and waits for response. */
|
|
1202
|
+
getLoopTree(loopID, timeout) {
|
|
1203
|
+
return this.requestResponse("loop_tree", { loop_id: loopID }, "loop_tree", timeout ?? 15e3);
|
|
584
1204
|
}
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
1205
|
+
/** Requests loop deletion and waits for response. */
|
|
1206
|
+
deleteLoop(loopID, timeout) {
|
|
1207
|
+
return this.requestResponse(
|
|
1208
|
+
"loop_delete",
|
|
1209
|
+
{ loop_id: loopID },
|
|
1210
|
+
"loop_delete",
|
|
1211
|
+
timeout ?? 15e3
|
|
1212
|
+
);
|
|
1213
|
+
}
|
|
1214
|
+
/** Requests persisted conversation/activity rows. */
|
|
1215
|
+
sendLoopMessages(loopID, limit, offset, includeEvents) {
|
|
1216
|
+
const params = { loop_id: loopID };
|
|
1217
|
+
if (limit !== void 0) params.limit = limit;
|
|
1218
|
+
if (offset !== void 0) params.offset = offset;
|
|
1219
|
+
if (includeEvents) params.include_events = true;
|
|
1220
|
+
return this.sendMessage(requestEnvelope("loop_messages", params));
|
|
1221
|
+
}
|
|
1222
|
+
/** Requests LangGraph checkpoint channel values. */
|
|
1223
|
+
sendLoopStateGet(loopID) {
|
|
1224
|
+
return this.sendMessage(requestEnvelope("loop_state_get", { loop_id: loopID }));
|
|
1225
|
+
}
|
|
1226
|
+
/** Applies partial checkpoint values. */
|
|
1227
|
+
sendLoopStateUpdate(loopID, values, asNode) {
|
|
1228
|
+
const params = { loop_id: loopID, values };
|
|
1229
|
+
if (asNode) params.as_node = asNode;
|
|
1230
|
+
return this.sendMessage(requestEnvelope("loop_state_update", params));
|
|
1231
|
+
}
|
|
1232
|
+
/** Requests display card ledger snapshot. */
|
|
1233
|
+
sendLoopCardsFetch(loopID) {
|
|
1234
|
+
return this.sendMessage(requestEnvelope("loop_cards_fetch", { loop_id: loopID }));
|
|
1235
|
+
}
|
|
1236
|
+
/** Requests the full loop history (RFC-631). */
|
|
1237
|
+
sendLoopHistoryFetch(loopID) {
|
|
1238
|
+
return this.sendMessage(requestEnvelope("loop_history_fetch", { loop_id: loopID }));
|
|
1239
|
+
}
|
|
1240
|
+
/** Requests MCP server status. */
|
|
1241
|
+
sendMCPStatus() {
|
|
1242
|
+
return this.sendMessage(requestEnvelope("mcp_status", {}));
|
|
1243
|
+
}
|
|
1244
|
+
/** Requests daemon config reload. */
|
|
1245
|
+
sendConfigReload() {
|
|
1246
|
+
return this.sendMessage(requestEnvelope("config_reload", {}));
|
|
1247
|
+
}
|
|
1248
|
+
/** Submits credentials for daemon-side authentication. */
|
|
1249
|
+
sendAuth(accessKey, secretKey) {
|
|
1250
|
+
return this.sendMessage(
|
|
1251
|
+
requestEnvelope("auth", { access_key: accessKey, secret_key: secretKey })
|
|
1252
|
+
);
|
|
1253
|
+
}
|
|
1254
|
+
/** Refreshes the daemon-side auth token. */
|
|
1255
|
+
sendAuthRefresh(refreshToken) {
|
|
1256
|
+
return this.sendMessage(requestEnvelope("auth_refresh", { refresh_token: refreshToken }));
|
|
1257
|
+
}
|
|
1258
|
+
/** Requests persisted messages and waits for response. */
|
|
1259
|
+
getLoopMessages(loopID, limit, offset, includeEvents, timeout) {
|
|
1260
|
+
const params = { loop_id: loopID };
|
|
1261
|
+
if (limit !== void 0) params.limit = limit;
|
|
1262
|
+
if (offset !== void 0) params.offset = offset;
|
|
1263
|
+
if (includeEvents) params.include_events = true;
|
|
1264
|
+
return this.requestResponse("loop_messages", params, "loop_messages", timeout ?? 15e3);
|
|
1265
|
+
}
|
|
1266
|
+
/** Requests loop state and waits for response. */
|
|
1267
|
+
getLoopState(loopID, timeout) {
|
|
1268
|
+
return this.requestResponse(
|
|
1269
|
+
"loop_state_get",
|
|
1270
|
+
{ loop_id: loopID },
|
|
1271
|
+
"loop_state_get",
|
|
1272
|
+
timeout ?? 15e3
|
|
1273
|
+
);
|
|
1274
|
+
}
|
|
1275
|
+
/** Updates loop state and waits for response. */
|
|
1276
|
+
updateLoopState(loopID, values, asNode, timeout) {
|
|
1277
|
+
const params = { loop_id: loopID, values };
|
|
1278
|
+
if (asNode) params.as_node = asNode;
|
|
1279
|
+
return this.requestResponse(
|
|
1280
|
+
"loop_state_update",
|
|
1281
|
+
params,
|
|
1282
|
+
"loop_state_update",
|
|
1283
|
+
timeout ?? 15e3
|
|
1284
|
+
);
|
|
1285
|
+
}
|
|
1286
|
+
/** Requests display cards and waits for response. */
|
|
1287
|
+
fetchLoopCards(loopID, timeout) {
|
|
1288
|
+
return this.requestResponse(
|
|
1289
|
+
"loop_cards_fetch",
|
|
1290
|
+
{ loop_id: loopID },
|
|
1291
|
+
"loop_cards_fetch",
|
|
1292
|
+
timeout ?? 15e3
|
|
1293
|
+
);
|
|
1294
|
+
}
|
|
1295
|
+
/** Requests MCP status and waits for response. */
|
|
1296
|
+
getMCPStatus(timeout) {
|
|
1297
|
+
return this.requestResponse("mcp_status", {}, "mcp_status", timeout ?? 15e3);
|
|
1298
|
+
}
|
|
1299
|
+
/** Requests loop history and waits for response. */
|
|
1300
|
+
fetchLoopHistory(loopID, timeout) {
|
|
1301
|
+
return this.requestResponse(
|
|
1302
|
+
"loop_history_fetch",
|
|
1303
|
+
{ loop_id: loopID },
|
|
1304
|
+
"loop_history_fetch",
|
|
1305
|
+
timeout ?? 15e3
|
|
1306
|
+
);
|
|
1307
|
+
}
|
|
1308
|
+
/** Requests daemon config reload and waits for response. */
|
|
1309
|
+
reloadConfig(timeout) {
|
|
1310
|
+
return this.requestResponse("config_reload", {}, "config_reload", timeout ?? 15e3);
|
|
1311
|
+
}
|
|
1312
|
+
/** Submits credentials for daemon-side authentication and waits for response. */
|
|
1313
|
+
authenticate(accessKey, secretKey, timeout) {
|
|
1314
|
+
return this.requestResponse(
|
|
1315
|
+
"auth",
|
|
1316
|
+
{ access_key: accessKey, secret_key: secretKey },
|
|
1317
|
+
"auth",
|
|
1318
|
+
timeout ?? 15e3
|
|
1319
|
+
);
|
|
1320
|
+
}
|
|
1321
|
+
/** Refreshes the daemon-side auth token and waits for response. */
|
|
1322
|
+
refreshAuthToken(refreshToken, timeout) {
|
|
1323
|
+
return this.requestResponse(
|
|
1324
|
+
"auth_refresh",
|
|
1325
|
+
{ refresh_token: refreshToken },
|
|
1326
|
+
"auth_refresh",
|
|
1327
|
+
timeout ?? 15e3
|
|
1328
|
+
);
|
|
613
1329
|
}
|
|
614
1330
|
// ---------------------------------------------------------------------------
|
|
615
|
-
//
|
|
1331
|
+
// RFC-228 Job IPC methods
|
|
616
1332
|
// ---------------------------------------------------------------------------
|
|
617
|
-
/**
|
|
618
|
-
|
|
619
|
-
|
|
1333
|
+
/** Creates an autopilot job and waits for the response. */
|
|
1334
|
+
createJob(goal, verificationRules, workspace, timeout) {
|
|
1335
|
+
const params = { goal };
|
|
1336
|
+
if (verificationRules) params.verification_rules = verificationRules;
|
|
1337
|
+
if (workspace) params.workspace = workspace;
|
|
1338
|
+
return this.requestResponse("job_create", params, "job_create", timeout ?? 15e3);
|
|
620
1339
|
}
|
|
621
|
-
/**
|
|
622
|
-
|
|
623
|
-
return this.requestResponse({
|
|
1340
|
+
/** Queries job status and waits for the response. */
|
|
1341
|
+
getJobStatus(jobId, timeout) {
|
|
1342
|
+
return this.requestResponse("job_status", { job_id: jobId }, "job_status", timeout ?? 15e3);
|
|
624
1343
|
}
|
|
625
|
-
/**
|
|
626
|
-
|
|
627
|
-
return this.requestResponse(
|
|
1344
|
+
/** Pauses a running job. */
|
|
1345
|
+
pauseJob(jobId, timeout) {
|
|
1346
|
+
return this.requestResponse("job_pause", { job_id: jobId }, "job_pause", timeout ?? 15e3);
|
|
628
1347
|
}
|
|
629
|
-
/**
|
|
630
|
-
|
|
631
|
-
return this.requestResponse({
|
|
1348
|
+
/** Resumes a paused job. */
|
|
1349
|
+
resumeJob(jobId, timeout) {
|
|
1350
|
+
return this.requestResponse("job_resume", { job_id: jobId }, "job_resume", timeout ?? 15e3);
|
|
632
1351
|
}
|
|
633
|
-
/**
|
|
634
|
-
|
|
635
|
-
return this.requestResponse(
|
|
1352
|
+
/** Cancels a job. */
|
|
1353
|
+
cancelJob(jobId, timeout) {
|
|
1354
|
+
return this.requestResponse("job_cancel", { job_id: jobId }, "job_cancel", timeout ?? 15e3);
|
|
636
1355
|
}
|
|
637
|
-
/** Requests
|
|
638
|
-
|
|
639
|
-
return this.requestResponse(
|
|
1356
|
+
/** Requests the DAG visualization for a job. */
|
|
1357
|
+
getJobDag(jobId, timeout) {
|
|
1358
|
+
return this.requestResponse("job_dag", { job_id: jobId }, "job_dag", timeout ?? 15e3);
|
|
640
1359
|
}
|
|
641
|
-
/**
|
|
642
|
-
|
|
643
|
-
|
|
1360
|
+
/** Sends guidance to a job or specific goal. */
|
|
1361
|
+
sendJobGuidance(jobId, text, goalId, timeout) {
|
|
1362
|
+
const params = { job_id: jobId, content: text };
|
|
1363
|
+
if (goalId) params.goal_id = goalId;
|
|
1364
|
+
return this.requestResponse("job_guidance", params, "job_guidance", timeout ?? 3e4);
|
|
1365
|
+
}
|
|
1366
|
+
/** Subscribes to autopilot worker events. */
|
|
1367
|
+
autopilotSubscribe(timeout) {
|
|
1368
|
+
return this.subscribe("autopilot_events", {}, timeout ?? 15e3);
|
|
1369
|
+
}
|
|
1370
|
+
/** Unsubscribes from autopilot worker events. */
|
|
1371
|
+
autopilotUnsubscribe(timeout) {
|
|
1372
|
+
const req = unsubscribeEnvelope(newRequestID());
|
|
1373
|
+
return this._requestResponseForEnvelope(req, "autopilot_unsubscribe", timeout ?? 15e3);
|
|
644
1374
|
}
|
|
645
1375
|
// ---------------------------------------------------------------------------
|
|
646
|
-
//
|
|
1376
|
+
// RFC-229 Cron IPC methods
|
|
1377
|
+
// ---------------------------------------------------------------------------
|
|
1378
|
+
/** Creates a scheduled job from natural language. */
|
|
1379
|
+
cronAdd(text, priority, timeout) {
|
|
1380
|
+
const params = { text };
|
|
1381
|
+
if (priority !== void 0) params.priority = priority;
|
|
1382
|
+
return this.requestResponse(
|
|
1383
|
+
"cron_add",
|
|
1384
|
+
params,
|
|
1385
|
+
"cron_add",
|
|
1386
|
+
timeout ?? 3e4
|
|
1387
|
+
// Longer timeout for NL extraction
|
|
1388
|
+
);
|
|
1389
|
+
}
|
|
1390
|
+
/** Lists scheduled jobs. */
|
|
1391
|
+
cronList(status, timeout) {
|
|
1392
|
+
const params = {};
|
|
1393
|
+
if (status !== void 0) params.status = status;
|
|
1394
|
+
return this.requestResponse("cron_list", params, "cron_list", timeout ?? 15e3);
|
|
1395
|
+
}
|
|
1396
|
+
/** Shows a specific scheduled job. */
|
|
1397
|
+
cronShow(jobId, timeout) {
|
|
1398
|
+
return this.requestResponse("cron_show", { job_id: jobId }, "cron_show", timeout ?? 15e3);
|
|
1399
|
+
}
|
|
1400
|
+
/** Cancels a scheduled job. */
|
|
1401
|
+
cronCancel(jobId, timeout) {
|
|
1402
|
+
return this.requestResponse("cron_cancel", { job_id: jobId }, "cron_cancel", timeout ?? 15e3);
|
|
1403
|
+
}
|
|
647
1404
|
// ---------------------------------------------------------------------------
|
|
648
|
-
|
|
1405
|
+
// Wait helpers
|
|
1406
|
+
/**
|
|
1407
|
+
* Waits for the connection_ack to report readiness (already done in
|
|
1408
|
+
* connect(); kept for callers that reconnect manually). Resolves
|
|
1409
|
+
* immediately if the handshake is already complete.
|
|
1410
|
+
*/
|
|
649
1411
|
async waitForDaemonReady(timeout) {
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
while (Date.now() < deadline) {
|
|
653
|
-
const remaining = deadline - Date.now();
|
|
654
|
-
if (remaining <= 0) break;
|
|
655
|
-
const ev = await this.readEventWithTimeout(remaining);
|
|
656
|
-
if (ev === null) break;
|
|
657
|
-
if (ev.type !== "daemon_ready") continue;
|
|
658
|
-
if (ev.state === "ready") return ev;
|
|
659
|
-
const msg = ev.message ?? `daemon state is ${ev.state}`;
|
|
660
|
-
throw new Error(`daemon not ready: ${msg}`);
|
|
1412
|
+
if (this.handshakeComplete) {
|
|
1413
|
+
return { readiness_state: this.readinessState ?? "ready" };
|
|
661
1414
|
}
|
|
662
|
-
|
|
663
|
-
}
|
|
664
|
-
/** Waits for subscription confirmation matching loop id. */
|
|
665
|
-
async waitForSubscriptionConfirmed(loopID, _verbosity, timeout) {
|
|
666
|
-
const t = timeout ?? 5e3;
|
|
1415
|
+
const t = timeout ?? 1e4;
|
|
667
1416
|
const deadline = Date.now() + t;
|
|
668
1417
|
while (Date.now() < deadline) {
|
|
669
1418
|
const remaining = deadline - Date.now();
|
|
670
1419
|
if (remaining <= 0) break;
|
|
671
1420
|
const ev = await this.readEventWithTimeout(remaining);
|
|
672
1421
|
if (ev === null) break;
|
|
673
|
-
if (ev.type
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
const lid = String(ev.loop_id ?? "");
|
|
679
|
-
if (lid === loopID) return;
|
|
1422
|
+
if (ev.type !== "connection_ack") continue;
|
|
1423
|
+
const result = ev.result ?? {};
|
|
1424
|
+
const state = result.readiness_state;
|
|
1425
|
+
if (state === "ready") return ev;
|
|
1426
|
+
throw new Error(`daemon not ready: state=${state ?? "unknown"}`);
|
|
680
1427
|
}
|
|
681
|
-
throw new Error(`timeout after ${t}ms waiting for
|
|
1428
|
+
throw new Error(`timeout after ${t}ms waiting for connection_ack`);
|
|
682
1429
|
}
|
|
683
1430
|
};
|
|
684
1431
|
}
|
|
@@ -687,14 +1434,29 @@ var init_client = __esm({
|
|
|
687
1434
|
// src/index.ts
|
|
688
1435
|
var index_exports = {};
|
|
689
1436
|
__export(index_exports, {
|
|
1437
|
+
CLIENT_VERSION: () => CLIENT_VERSION,
|
|
1438
|
+
ChatEventTerminal: () => ChatEventTerminal,
|
|
690
1439
|
Client: () => Client,
|
|
691
1440
|
ConnectionError: () => ConnectionError,
|
|
1441
|
+
ConnectionPool: () => ConnectionPool,
|
|
1442
|
+
DEFAULT_CLIENT_CAPABILITIES: () => DEFAULT_CLIENT_CAPABILITIES,
|
|
1443
|
+
DEFAULT_DELIVERABLE_PHASES: () => DEFAULT_DELIVERABLE_PHASES,
|
|
1444
|
+
DEFAULT_THINKING_STEP_EVENTS: () => DEFAULT_THINKING_STEP_EVENTS,
|
|
692
1445
|
DaemonError: () => DaemonError,
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
1446
|
+
DisconnectCause: () => DisconnectCause,
|
|
1447
|
+
ErrPoolExhausted: () => ErrPoolExhausted,
|
|
1448
|
+
ErrQueryBusy: () => ErrQueryBusy,
|
|
1449
|
+
ErrQueryTimeout: () => ErrQueryTimeout,
|
|
1450
|
+
EventAutopilotGoalCompleted: () => EventAutopilotGoalCompleted,
|
|
1451
|
+
EventAutopilotGoalCreated: () => EventAutopilotGoalCreated,
|
|
1452
|
+
EventAutopilotGoalProgress: () => EventAutopilotGoalProgress,
|
|
1453
|
+
EventAutopilotGoalStatus: () => EventAutopilotGoalStatus,
|
|
1454
|
+
EventAutopilotWorkerAssigned: () => EventAutopilotWorkerAssigned,
|
|
1455
|
+
EventAutopilotWorkerUnassigned: () => EventAutopilotWorkerUnassigned,
|
|
1456
|
+
EventCardCreated: () => EventCardCreated,
|
|
1457
|
+
EventCardReplayBegin: () => EventCardReplayBegin,
|
|
1458
|
+
EventCardReplayEnd: () => EventCardReplayEnd,
|
|
1459
|
+
EventClassifier: () => EventClassifier,
|
|
698
1460
|
EventExploreCompleted: () => EventExploreCompleted,
|
|
699
1461
|
EventExploreMilestone: () => EventExploreMilestone,
|
|
700
1462
|
EventExploreStarted: () => EventExploreStarted,
|
|
@@ -706,6 +1468,14 @@ __export(index_exports, {
|
|
|
706
1468
|
EventMessageSent: () => EventMessageSent,
|
|
707
1469
|
EventPlanCreated: () => EventPlanCreated,
|
|
708
1470
|
EventReplayComplete: () => EventReplayComplete,
|
|
1471
|
+
EventStrangeLoopCompleted: () => EventStrangeLoopCompleted,
|
|
1472
|
+
EventStrangeLoopContextCompacted: () => EventStrangeLoopContextCompacted,
|
|
1473
|
+
EventStrangeLoopPlanDecision: () => EventStrangeLoopPlanDecision,
|
|
1474
|
+
EventStrangeLoopReasoned: () => EventStrangeLoopReasoned,
|
|
1475
|
+
EventStrangeLoopStarted: () => EventStrangeLoopStarted,
|
|
1476
|
+
EventStrangeLoopStepCompleted: () => EventStrangeLoopStepCompleted,
|
|
1477
|
+
EventStrangeLoopStepQueued: () => EventStrangeLoopStepQueued,
|
|
1478
|
+
EventStrangeLoopStepStarted: () => EventStrangeLoopStepStarted,
|
|
709
1479
|
EventStreamToolCallUpdate: () => EventStreamToolCallUpdate,
|
|
710
1480
|
EventTacitusCompleted: () => EventTacitusCompleted,
|
|
711
1481
|
EventTacitusGatherSummary: () => EventTacitusGatherSummary,
|
|
@@ -714,18 +1484,42 @@ __export(index_exports, {
|
|
|
714
1484
|
EventToolCompleted: () => EventToolCompleted,
|
|
715
1485
|
EventToolError: () => EventToolError,
|
|
716
1486
|
EventToolStarted: () => EventToolStarted,
|
|
1487
|
+
INTENT_HINT_EMBED: () => INTENT_HINT_EMBED,
|
|
1488
|
+
INTENT_HINT_IMAGE_TO_TEXT: () => INTENT_HINT_IMAGE_TO_TEXT,
|
|
1489
|
+
INTENT_HINT_OCR: () => INTENT_HINT_OCR,
|
|
1490
|
+
INTENT_HINT_TEXT_COMPLETION: () => INTENT_HINT_TEXT_COMPLETION,
|
|
1491
|
+
LOOP_ASSISTANT_OUTPUT_PHASES: () => LOOP_ASSISTANT_OUTPUT_PHASES,
|
|
1492
|
+
Multiplexer: () => Multiplexer,
|
|
1493
|
+
PROTO_VERSION: () => PROTO_VERSION,
|
|
1494
|
+
PooledConn: () => PooledConn,
|
|
1495
|
+
QueryGate: () => QueryGate,
|
|
1496
|
+
REMOVED_INTENT_HINTS: () => REMOVED_INTENT_HINTS,
|
|
1497
|
+
ReconnectError: () => ReconnectError,
|
|
1498
|
+
SSEBroadcaster: () => SSEBroadcaster,
|
|
1499
|
+
StaleLoopError: () => StaleLoopError,
|
|
717
1500
|
TimeoutError: () => TimeoutError,
|
|
1501
|
+
TurnRunner: () => TurnRunner,
|
|
718
1502
|
VerbosityTier: () => VerbosityTier,
|
|
1503
|
+
authenticate: () => authenticate,
|
|
719
1504
|
bootstrapLoopSession: () => bootstrapLoopSession,
|
|
720
1505
|
checkDaemonStatus: () => checkDaemonStatus,
|
|
721
1506
|
classifyEventVerbosity: () => classifyEventVerbosity,
|
|
722
1507
|
connectWithRetries: () => connectWithRetries,
|
|
1508
|
+
connectionInitEnvelope: () => connectionInitEnvelope,
|
|
723
1509
|
decodeMessage: () => decodeMessage,
|
|
1510
|
+
defaultBootstrapFunc: () => defaultBootstrapFunc,
|
|
1511
|
+
defaultClientFactory: () => defaultClientFactory,
|
|
724
1512
|
defaultConfig: () => defaultConfig,
|
|
1513
|
+
defaultPoolConfig: () => defaultPoolConfig,
|
|
1514
|
+
disconnectCauseName: () => disconnectCauseName,
|
|
1515
|
+
disconnectEnvelope: () => disconnectEnvelope,
|
|
725
1516
|
encodeMessage: () => encodeMessage,
|
|
726
1517
|
extractSootheLoopID: () => extractSootheLoopID,
|
|
1518
|
+
extractThinkingStep: () => extractThinkingStep,
|
|
727
1519
|
fetchConfigSection: () => fetchConfigSection,
|
|
1520
|
+
fetchLoopHistory: () => fetchLoopHistory,
|
|
728
1521
|
fetchSkillsCatalog: () => fetchSkillsCatalog,
|
|
1522
|
+
inputMessageForLoop: () => inputMessageForLoop,
|
|
729
1523
|
isCompletionEvent: () => isCompletionEvent,
|
|
730
1524
|
isDaemonLive: () => isDaemonLive,
|
|
731
1525
|
isSubagentProgressEvent: () => isSubagentProgressEvent,
|
|
@@ -735,50 +1529,25 @@ __export(index_exports, {
|
|
|
735
1529
|
newLoopNewMessage: () => newLoopNewMessage,
|
|
736
1530
|
newLoopSubscribeMessage: () => newLoopSubscribeMessage,
|
|
737
1531
|
newRequestID: () => newRequestID,
|
|
1532
|
+
notificationEnvelope: () => notificationEnvelope,
|
|
738
1533
|
parseNamespace: () => parseNamespace,
|
|
1534
|
+
pingEnvelope: () => pingEnvelope,
|
|
1535
|
+
pongEnvelope: () => pongEnvelope,
|
|
1536
|
+
refreshAuthToken: () => refreshAuthToken,
|
|
1537
|
+
requestDaemonConfigReload: () => requestDaemonConfigReload,
|
|
739
1538
|
requestDaemonShutdown: () => requestDaemonShutdown,
|
|
1539
|
+
requestEnvelope: () => requestEnvelope,
|
|
740
1540
|
shouldShow: () => shouldShow,
|
|
741
1541
|
splitWirePayload: () => splitWirePayload,
|
|
1542
|
+
subscribeEnvelope: () => subscribeEnvelope,
|
|
1543
|
+
unsubscribeEnvelope: () => unsubscribeEnvelope,
|
|
1544
|
+
validateLoopInputIntentHint: () => validateLoopInputIntentHint,
|
|
742
1545
|
waitDaemonReady: () => waitDaemonReady,
|
|
743
1546
|
waitLoopStatusWithID: () => waitLoopStatusWithID,
|
|
744
1547
|
waitSubscriptionConfirmed: () => waitSubscriptionConfirmed
|
|
745
1548
|
});
|
|
746
1549
|
module.exports = __toCommonJS(index_exports);
|
|
747
|
-
|
|
748
|
-
// src/errors.ts
|
|
749
|
-
var ConnectionError = class extends Error {
|
|
750
|
-
url;
|
|
751
|
-
attempt;
|
|
752
|
-
cause;
|
|
753
|
-
constructor(url, attempt, cause) {
|
|
754
|
-
super(`connection error to ${url} (attempt ${attempt}): ${cause.message}`);
|
|
755
|
-
this.name = "ConnectionError";
|
|
756
|
-
this.url = url;
|
|
757
|
-
this.attempt = attempt;
|
|
758
|
-
this.cause = cause;
|
|
759
|
-
}
|
|
760
|
-
};
|
|
761
|
-
var DaemonError = class extends Error {
|
|
762
|
-
code;
|
|
763
|
-
/** The daemon's error message text. */
|
|
764
|
-
daemonMessage;
|
|
765
|
-
constructor(code, message) {
|
|
766
|
-
super(`daemon error [${code}]: ${message}`);
|
|
767
|
-
this.name = "DaemonError";
|
|
768
|
-
this.code = code;
|
|
769
|
-
this.daemonMessage = message;
|
|
770
|
-
}
|
|
771
|
-
};
|
|
772
|
-
var TimeoutError = class extends Error {
|
|
773
|
-
operation;
|
|
774
|
-
duration;
|
|
775
|
-
constructor(operation, duration) {
|
|
776
|
-
super(`timeout after ${duration} waiting for ${operation}`);
|
|
777
|
-
this.name = "TimeoutError";
|
|
778
|
-
this.operation = operation;
|
|
779
|
-
this.duration = duration;
|
|
780
|
-
}
|
|
781
|
-
};
|
|
1550
|
+
init_errors();
|
|
782
1551
|
|
|
783
1552
|
// src/verbosity.ts
|
|
784
1553
|
var VerbosityTier = /* @__PURE__ */ ((VerbosityTier2) => {
|
|
@@ -808,6 +1577,7 @@ function isValidVerbosityLevel(s) {
|
|
|
808
1577
|
// src/index.ts
|
|
809
1578
|
init_config();
|
|
810
1579
|
init_protocol();
|
|
1580
|
+
init_intent_hints();
|
|
811
1581
|
|
|
812
1582
|
// src/events.ts
|
|
813
1583
|
var EventPlanCreated = "soothe.cognition.plan.created";
|
|
@@ -820,18 +1590,31 @@ var EventTacitusGatherSummary = "soothe.subagent.tacitus.gather.summary";
|
|
|
820
1590
|
var EventTacitusCompleted = "soothe.subagent.tacitus.completed";
|
|
821
1591
|
var EventReplayComplete = "replay_complete";
|
|
822
1592
|
var EventLoopReattachedWire = "loop_reattached";
|
|
1593
|
+
var EventCardReplayBegin = "card.replay_begin";
|
|
1594
|
+
var EventCardCreated = "card.created";
|
|
1595
|
+
var EventCardReplayEnd = "card.replay_end";
|
|
823
1596
|
var EventToolStarted = "soothe.tool.execution.started";
|
|
824
1597
|
var EventToolCompleted = "soothe.tool.execution.completed";
|
|
825
1598
|
var EventToolError = "soothe.tool.execution.error";
|
|
826
1599
|
var EventStreamToolCallUpdate = "soothe.stream.tool_call.update";
|
|
827
1600
|
var EventToolCallUpdatesBatch = "tool_call_updates_batch";
|
|
828
|
-
var
|
|
829
|
-
var
|
|
830
|
-
var
|
|
831
|
-
var
|
|
1601
|
+
var EventStrangeLoopStarted = "soothe.cognition.strange_loop.started";
|
|
1602
|
+
var EventStrangeLoopCompleted = "soothe.cognition.strange_loop.completed";
|
|
1603
|
+
var EventStrangeLoopPlanDecision = "soothe.cognition.strange_loop.plan.decision";
|
|
1604
|
+
var EventStrangeLoopReasoned = "soothe.cognition.strange_loop.reasoned";
|
|
1605
|
+
var EventStrangeLoopStepStarted = "soothe.cognition.strange_loop.step.started";
|
|
1606
|
+
var EventStrangeLoopStepQueued = "soothe.cognition.strange_loop.step.queued";
|
|
1607
|
+
var EventStrangeLoopStepCompleted = "soothe.cognition.strange_loop.step.completed";
|
|
1608
|
+
var EventStrangeLoopContextCompacted = "soothe.cognition.strange_loop.context.compacted";
|
|
832
1609
|
var EventMessageReceived = "soothe.protocol.message.received";
|
|
833
1610
|
var EventMessageSent = "soothe.protocol.message.sent";
|
|
834
1611
|
var EventFinalReport = "soothe.output.autonomous.final_report.reported";
|
|
1612
|
+
var EventAutopilotGoalStatus = "soothe.autopilot.goal.status";
|
|
1613
|
+
var EventAutopilotGoalProgress = "soothe.autopilot.goal.progress";
|
|
1614
|
+
var EventAutopilotGoalCreated = "soothe.autopilot.goal.created";
|
|
1615
|
+
var EventAutopilotGoalCompleted = "soothe.autopilot.goal.completed";
|
|
1616
|
+
var EventAutopilotWorkerAssigned = "soothe.autopilot.worker.assigned";
|
|
1617
|
+
var EventAutopilotWorkerUnassigned = "soothe.autopilot.worker.unassigned";
|
|
835
1618
|
var EventGeneralFailed = "soothe.error.general.failed";
|
|
836
1619
|
function parseNamespace(ns) {
|
|
837
1620
|
const parts = splitNamespace(ns);
|
|
@@ -872,6 +1655,8 @@ function classifyByDomainAndComponent(domain, _component, full) {
|
|
|
872
1655
|
return 99 /* Internal */;
|
|
873
1656
|
case "subagent":
|
|
874
1657
|
return classifySubagentEvent(full);
|
|
1658
|
+
case "autopilot":
|
|
1659
|
+
return 1 /* Normal */;
|
|
875
1660
|
case "output":
|
|
876
1661
|
case "error":
|
|
877
1662
|
return 0 /* Quiet */;
|
|
@@ -909,25 +1694,15 @@ function isSubagentProgressEvent(eventType) {
|
|
|
909
1694
|
}
|
|
910
1695
|
return parsed.action === "started" || parsed.action === "completed";
|
|
911
1696
|
}
|
|
912
|
-
var ESSENTIAL_EVENT_TYPES = /* @__PURE__ */ new Set([
|
|
913
|
-
EventAgentLoopStarted,
|
|
914
|
-
EventAgentLoopCompleted,
|
|
915
|
-
EventAgentLoopReasoned,
|
|
916
|
-
EventPlanCreated,
|
|
917
|
-
EventExploreStarted,
|
|
918
|
-
EventExploreCompleted,
|
|
919
|
-
EventTacitusStarted,
|
|
920
|
-
EventTacitusCompleted,
|
|
921
|
-
EventGeneralFailed
|
|
922
|
-
]);
|
|
923
1697
|
|
|
924
1698
|
// src/index.ts
|
|
925
1699
|
init_client();
|
|
1700
|
+
init_multiplexer();
|
|
926
1701
|
|
|
927
1702
|
// src/helpers.ts
|
|
928
1703
|
init_config();
|
|
929
1704
|
async function checkDaemonStatus(client, timeout) {
|
|
930
|
-
return client.requestResponse(
|
|
1705
|
+
return client.requestResponse("daemon_status", {}, "daemon_status", timeout ?? 5e3);
|
|
931
1706
|
}
|
|
932
1707
|
async function isDaemonLive(wsURL, timeout) {
|
|
933
1708
|
const { Client: Client2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
@@ -948,70 +1723,106 @@ async function isDaemonLive(wsURL, timeout) {
|
|
|
948
1723
|
}
|
|
949
1724
|
}
|
|
950
1725
|
async function requestDaemonShutdown(client, timeout) {
|
|
951
|
-
const resp = await client.requestResponse(
|
|
1726
|
+
const resp = await client.requestResponse(
|
|
1727
|
+
"daemon_shutdown",
|
|
1728
|
+
{},
|
|
1729
|
+
"daemon_shutdown",
|
|
1730
|
+
timeout ?? 1e4
|
|
1731
|
+
);
|
|
952
1732
|
if (resp.status !== "acknowledged") {
|
|
953
1733
|
throw new Error(`shutdown not acknowledged: ${JSON.stringify(resp)}`);
|
|
954
1734
|
}
|
|
955
1735
|
}
|
|
956
1736
|
async function fetchSkillsCatalog(client, timeout) {
|
|
957
|
-
const resp = await client.requestResponse(
|
|
1737
|
+
const resp = await client.requestResponse("skills_list", {}, "skills_list", timeout ?? 15e3);
|
|
958
1738
|
const skillsRaw = resp.skills;
|
|
959
1739
|
if (!skillsRaw || !Array.isArray(skillsRaw)) return [];
|
|
960
1740
|
return skillsRaw.filter((s) => typeof s === "object" && s !== null);
|
|
961
1741
|
}
|
|
962
1742
|
async function fetchConfigSection(client, section, timeout) {
|
|
963
|
-
const resp = await client.requestResponse(
|
|
1743
|
+
const resp = await client.requestResponse(
|
|
1744
|
+
"config_get",
|
|
1745
|
+
{ section },
|
|
1746
|
+
"config_get",
|
|
1747
|
+
timeout ?? 5e3
|
|
1748
|
+
);
|
|
964
1749
|
const sec = resp[section];
|
|
965
1750
|
if (sec && typeof sec === "object") {
|
|
966
1751
|
return sec;
|
|
967
1752
|
}
|
|
968
1753
|
return resp;
|
|
969
1754
|
}
|
|
1755
|
+
async function requestDaemonConfigReload(client, timeout) {
|
|
1756
|
+
return client.requestResponse("config_reload", {}, "config_reload", timeout ?? 15e3);
|
|
1757
|
+
}
|
|
1758
|
+
async function fetchLoopHistory(client, loopID, timeout) {
|
|
1759
|
+
return client.requestResponse(
|
|
1760
|
+
"loop_history_fetch",
|
|
1761
|
+
{ loop_id: loopID },
|
|
1762
|
+
"loop_history_fetch",
|
|
1763
|
+
timeout ?? 15e3
|
|
1764
|
+
);
|
|
1765
|
+
}
|
|
1766
|
+
async function authenticate(client, accessKey, secretKey, timeout) {
|
|
1767
|
+
return client.requestResponse(
|
|
1768
|
+
"auth",
|
|
1769
|
+
{ access_key: accessKey, secret_key: secretKey },
|
|
1770
|
+
"auth",
|
|
1771
|
+
timeout ?? 15e3
|
|
1772
|
+
);
|
|
1773
|
+
}
|
|
1774
|
+
async function refreshAuthToken(client, refreshToken, timeout) {
|
|
1775
|
+
return client.requestResponse(
|
|
1776
|
+
"auth_refresh",
|
|
1777
|
+
{ refresh_token: refreshToken },
|
|
1778
|
+
"auth_refresh",
|
|
1779
|
+
timeout ?? 15e3
|
|
1780
|
+
);
|
|
1781
|
+
}
|
|
970
1782
|
|
|
971
1783
|
// src/session.ts
|
|
972
1784
|
init_config();
|
|
973
1785
|
init_protocol();
|
|
1786
|
+
init_errors();
|
|
974
1787
|
async function bootstrapLoopSession(client, resumeLoopId, config, loopNew) {
|
|
975
1788
|
const cfg = config ?? defaultConfig();
|
|
976
|
-
await client.sendMessage({ type: "daemon_ready" });
|
|
977
|
-
await waitDaemonReady(client, cfg.daemonReadyTimeout);
|
|
978
1789
|
let loopId = (resumeLoopId ?? "").trim();
|
|
979
1790
|
if (!loopId) {
|
|
1791
|
+
const env = newLoopNewMessage(loopNew);
|
|
980
1792
|
const newResp = await client.requestResponse(
|
|
981
|
-
|
|
982
|
-
|
|
1793
|
+
env.method,
|
|
1794
|
+
env.params ?? {},
|
|
1795
|
+
"loop_new",
|
|
983
1796
|
cfg.loopStatusTimeout
|
|
984
1797
|
);
|
|
985
1798
|
loopId = String(newResp.loop_id ?? "").trim();
|
|
986
1799
|
if (!loopId) {
|
|
987
|
-
throw new Error("
|
|
1800
|
+
throw new Error("loop_new response missing loop_id");
|
|
988
1801
|
}
|
|
989
1802
|
}
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
1803
|
+
await client.subscribe(
|
|
1804
|
+
"loop_events",
|
|
1805
|
+
{ loop_id: loopId, verbosity: cfg.verbosityLevel },
|
|
993
1806
|
cfg.subscriptionTimeout
|
|
994
1807
|
);
|
|
995
|
-
if (subResp.success === false) {
|
|
996
|
-
throw new Error(String(subResp.message ?? "loop_subscribe failed"));
|
|
997
|
-
}
|
|
998
1808
|
return loopId;
|
|
999
1809
|
}
|
|
1000
1810
|
async function waitDaemonReady(client, timeout) {
|
|
1811
|
+
if (client.isConnected()) return;
|
|
1001
1812
|
const deadline = Date.now() + timeout;
|
|
1002
1813
|
while (Date.now() < deadline) {
|
|
1003
1814
|
const remaining = deadline - Date.now();
|
|
1004
1815
|
if (remaining <= 0) break;
|
|
1005
1816
|
const ev = await client.readEventWithTimeout(remaining);
|
|
1006
1817
|
if (ev === null) break;
|
|
1007
|
-
if (ev.type === "
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
);
|
|
1818
|
+
if (ev.type === "connection_ack") {
|
|
1819
|
+
const result = ev.result ?? {};
|
|
1820
|
+
const state = result.readiness_state;
|
|
1821
|
+
if (state === "ready") return;
|
|
1822
|
+
throw new Error(`daemon not ready: state=${JSON.stringify(state ?? "unknown")}`);
|
|
1012
1823
|
}
|
|
1013
1824
|
}
|
|
1014
|
-
throw new Error(`timeout after ${timeout}ms waiting for
|
|
1825
|
+
throw new Error(`timeout after ${timeout}ms waiting for connection_ack (ready)`);
|
|
1015
1826
|
}
|
|
1016
1827
|
async function waitLoopStatusWithID(client, timeout) {
|
|
1017
1828
|
const deadline = Date.now() + timeout;
|
|
@@ -1021,14 +1832,13 @@ async function waitLoopStatusWithID(client, timeout) {
|
|
|
1021
1832
|
const ev = await client.readEventWithTimeout(remaining);
|
|
1022
1833
|
if (ev === null) break;
|
|
1023
1834
|
if (ev.type === "error") {
|
|
1024
|
-
const
|
|
1025
|
-
throw new
|
|
1835
|
+
const errObj = ev.error ?? {};
|
|
1836
|
+
throw new DaemonError(errObj.code ?? -32603, errObj.message ?? "daemon error");
|
|
1026
1837
|
}
|
|
1027
1838
|
if (ev.type === "status") {
|
|
1028
|
-
const
|
|
1029
|
-
const lid = status.loop_id;
|
|
1839
|
+
const lid = ev.loop_id;
|
|
1030
1840
|
if (lid && lid !== "") {
|
|
1031
|
-
return
|
|
1841
|
+
return ev;
|
|
1032
1842
|
}
|
|
1033
1843
|
}
|
|
1034
1844
|
}
|
|
@@ -1041,15 +1851,18 @@ async function waitSubscriptionConfirmed(client, wantLoopID, _wantVerbosity, tim
|
|
|
1041
1851
|
if (remaining <= 0) break;
|
|
1042
1852
|
const ev = await client.readEventWithTimeout(remaining);
|
|
1043
1853
|
if (ev === null) break;
|
|
1044
|
-
if (ev.type === "
|
|
1045
|
-
|
|
1854
|
+
if (ev.type === "next") {
|
|
1855
|
+
const payload = ev.payload ?? {};
|
|
1856
|
+
const lid = String(payload.loop_id ?? "");
|
|
1857
|
+
if (lid === wantLoopID && payload.success === true) return;
|
|
1858
|
+
continue;
|
|
1046
1859
|
}
|
|
1047
|
-
if (ev.type === "
|
|
1048
|
-
const
|
|
1049
|
-
|
|
1860
|
+
if (ev.type === "error") {
|
|
1861
|
+
const errObj = ev.error ?? {};
|
|
1862
|
+
throw new Error(`daemon error: ${errObj.message ?? "subscription failed"}`);
|
|
1050
1863
|
}
|
|
1051
1864
|
}
|
|
1052
|
-
throw new Error(`timeout after ${timeout}ms waiting for
|
|
1865
|
+
throw new Error(`timeout after ${timeout}ms waiting for subscription confirmation`);
|
|
1053
1866
|
}
|
|
1054
1867
|
async function connectWithRetries(client, maxRetries, retryDelay) {
|
|
1055
1868
|
const retries = maxRetries && maxRetries > 0 ? maxRetries : 40;
|
|
@@ -1064,18 +1877,1128 @@ async function connectWithRetries(client, maxRetries, retryDelay) {
|
|
|
1064
1877
|
}
|
|
1065
1878
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1066
1879
|
}
|
|
1067
|
-
throw new Error(
|
|
1880
|
+
throw new Error(
|
|
1881
|
+
`failed to connect after ${retries} attempts: ${lastErr?.message ?? "unknown error"}`
|
|
1882
|
+
);
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
// src/appkit/broadcaster.ts
|
|
1886
|
+
var SUBSCRIBER_QUEUE_CAP = 100;
|
|
1887
|
+
var SSEBroadcaster = class {
|
|
1888
|
+
subscribers = /* @__PURE__ */ new Map();
|
|
1889
|
+
nextSubID = 0;
|
|
1890
|
+
/** Creates an empty broadcaster. */
|
|
1891
|
+
constructor() {
|
|
1892
|
+
}
|
|
1893
|
+
/**
|
|
1894
|
+
* Registers a new subscriber channel for a session id. Returns an async
|
|
1895
|
+
* iterable the subscriber reads events from. Unsubscribe via
|
|
1896
|
+
* `unsubscribe()` or `close()`.
|
|
1897
|
+
*/
|
|
1898
|
+
subscribe(sessionID) {
|
|
1899
|
+
const subID = String(this.nextSubID++);
|
|
1900
|
+
const sub = { queue: [], waiters: [], closed: false };
|
|
1901
|
+
let subs = this.subscribers.get(sessionID);
|
|
1902
|
+
if (!subs) {
|
|
1903
|
+
subs = /* @__PURE__ */ new Map();
|
|
1904
|
+
this.subscribers.set(sessionID, subs);
|
|
1905
|
+
}
|
|
1906
|
+
subs.set(subID, sub);
|
|
1907
|
+
const iterable = {
|
|
1908
|
+
[Symbol.asyncIterator]() {
|
|
1909
|
+
return {
|
|
1910
|
+
next() {
|
|
1911
|
+
if (sub.queue.length > 0) {
|
|
1912
|
+
return Promise.resolve({ value: sub.queue.shift(), done: false });
|
|
1913
|
+
}
|
|
1914
|
+
if (sub.closed) {
|
|
1915
|
+
return Promise.resolve({ value: void 0, done: true });
|
|
1916
|
+
}
|
|
1917
|
+
return new Promise((resolve) => {
|
|
1918
|
+
sub.waiters.push((ev) => {
|
|
1919
|
+
if (ev === null) {
|
|
1920
|
+
resolve({ value: void 0, done: true });
|
|
1921
|
+
} else {
|
|
1922
|
+
resolve({ value: ev, done: false });
|
|
1923
|
+
}
|
|
1924
|
+
});
|
|
1925
|
+
});
|
|
1926
|
+
}
|
|
1927
|
+
};
|
|
1928
|
+
}
|
|
1929
|
+
};
|
|
1930
|
+
return { iterable, id: subID };
|
|
1931
|
+
}
|
|
1932
|
+
/** Removes a subscriber by id and closes its iterable. Safe if unknown. */
|
|
1933
|
+
unsubscribe(sessionID, subID) {
|
|
1934
|
+
const subs = this.subscribers.get(sessionID);
|
|
1935
|
+
if (!subs) return;
|
|
1936
|
+
const sub = subs.get(subID);
|
|
1937
|
+
if (!sub) return;
|
|
1938
|
+
sub.closed = true;
|
|
1939
|
+
for (const w of sub.waiters) w(null);
|
|
1940
|
+
sub.waiters = [];
|
|
1941
|
+
subs.delete(subID);
|
|
1942
|
+
if (subs.size === 0) this.subscribers.delete(sessionID);
|
|
1943
|
+
}
|
|
1944
|
+
/**
|
|
1945
|
+
* Sends an event to all subscribers for a session id. Non-blocking: a full
|
|
1946
|
+
* subscriber queue is skipped (drop-on-full) so one slow consumer cannot
|
|
1947
|
+
* block the others.
|
|
1948
|
+
*/
|
|
1949
|
+
broadcast(sessionID, event) {
|
|
1950
|
+
const subs = this.subscribers.get(sessionID);
|
|
1951
|
+
if (!subs) return;
|
|
1952
|
+
for (const sub of subs.values()) {
|
|
1953
|
+
if (sub.closed) continue;
|
|
1954
|
+
if (sub.waiters.length > 0) {
|
|
1955
|
+
const w = sub.waiters.shift();
|
|
1956
|
+
w(event);
|
|
1957
|
+
} else if (sub.queue.length < SUBSCRIBER_QUEUE_CAP) {
|
|
1958
|
+
sub.queue.push(event);
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
/** Closes all subscribers for a session id and removes the entry. */
|
|
1963
|
+
close(sessionID) {
|
|
1964
|
+
const subs = this.subscribers.get(sessionID);
|
|
1965
|
+
if (!subs) return;
|
|
1966
|
+
for (const sub of subs.values()) {
|
|
1967
|
+
sub.closed = true;
|
|
1968
|
+
for (const w of sub.waiters) w(null);
|
|
1969
|
+
sub.waiters = [];
|
|
1970
|
+
}
|
|
1971
|
+
this.subscribers.delete(sessionID);
|
|
1972
|
+
}
|
|
1973
|
+
/** Closes every subscriber channel across all sessions. */
|
|
1974
|
+
closeAll() {
|
|
1975
|
+
for (const [sessionID, subs] of this.subscribers) {
|
|
1976
|
+
for (const sub of subs.values()) {
|
|
1977
|
+
sub.closed = true;
|
|
1978
|
+
for (const w of sub.waiters) w(null);
|
|
1979
|
+
sub.waiters = [];
|
|
1980
|
+
}
|
|
1981
|
+
this.subscribers.delete(sessionID);
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
};
|
|
1985
|
+
|
|
1986
|
+
// src/appkit/classifier.ts
|
|
1987
|
+
init_errors();
|
|
1988
|
+
|
|
1989
|
+
// src/appkit/thinking_step.ts
|
|
1990
|
+
var MAX_THINKING_STEP_RUNES = 280;
|
|
1991
|
+
var DEFAULT_THINKING_STEP_EVENTS = /* @__PURE__ */ new Set([
|
|
1992
|
+
"soothe.cognition.plan.step.started",
|
|
1993
|
+
"soothe.cognition.plan.step.completed",
|
|
1994
|
+
"soothe.cognition.plan.step.failed",
|
|
1995
|
+
"soothe.lifecycle.iteration.started",
|
|
1996
|
+
"soothe.agent.loop.step.started",
|
|
1997
|
+
"soothe.agent.loop.started",
|
|
1998
|
+
"soothe.cognition.plan.batch.started",
|
|
1999
|
+
"soothe.cognition.plan.created",
|
|
2000
|
+
"soothe.cognition.goal.created",
|
|
2001
|
+
"soothe.tool.execution.started"
|
|
2002
|
+
]);
|
|
2003
|
+
function extractThinkingStep(eventType, data, allow) {
|
|
2004
|
+
if (!eventType || !data) return ["", false];
|
|
2005
|
+
const et = eventType.trim();
|
|
2006
|
+
if (!et) return ["", false];
|
|
2007
|
+
const allowlist = allow ?? DEFAULT_THINKING_STEP_EVENTS;
|
|
2008
|
+
if (!allowlist.has(et)) return ["", false];
|
|
2009
|
+
let line = "";
|
|
2010
|
+
switch (et) {
|
|
2011
|
+
case "soothe.cognition.plan.step.started":
|
|
2012
|
+
line = formatPlanStepLine(data, "");
|
|
2013
|
+
break;
|
|
2014
|
+
case "soothe.cognition.plan.step.completed":
|
|
2015
|
+
line = formatPlanStepLine(data, "done");
|
|
2016
|
+
break;
|
|
2017
|
+
case "soothe.cognition.plan.step.failed": {
|
|
2018
|
+
const stepID = strField(data, "step_id");
|
|
2019
|
+
const errMsg = strField(data, "error");
|
|
2020
|
+
if (stepID && errMsg) line = `Step ${stepID} failed: ${errMsg}`;
|
|
2021
|
+
else if (stepID) line = `Step ${stepID} failed`;
|
|
2022
|
+
else if (errMsg) line = `Step failed: ${errMsg}`;
|
|
2023
|
+
break;
|
|
2024
|
+
}
|
|
2025
|
+
case "soothe.agent.loop.step.started":
|
|
2026
|
+
line = formatAgentStepLine(data, "");
|
|
2027
|
+
break;
|
|
2028
|
+
case "soothe.cognition.plan.batch.started": {
|
|
2029
|
+
const n = data["parallel_count"];
|
|
2030
|
+
if (typeof n === "number" && n > 0) line = `Running ${Math.floor(n)} steps in parallel`;
|
|
2031
|
+
break;
|
|
2032
|
+
}
|
|
2033
|
+
case "soothe.cognition.plan.created":
|
|
2034
|
+
case "soothe.agent.loop.started": {
|
|
2035
|
+
const g = strField(data, "goal");
|
|
2036
|
+
if (g) line = "Goal: " + g;
|
|
2037
|
+
break;
|
|
2038
|
+
}
|
|
2039
|
+
case "soothe.cognition.goal.created": {
|
|
2040
|
+
const g = strField(data, "friendly_message", "description");
|
|
2041
|
+
if (g) line = "Goal: " + g;
|
|
2042
|
+
break;
|
|
2043
|
+
}
|
|
2044
|
+
case "soothe.lifecycle.iteration.started": {
|
|
2045
|
+
const g = strField(data, "goal_description");
|
|
2046
|
+
if (g) line = "Iteration: " + g;
|
|
2047
|
+
break;
|
|
2048
|
+
}
|
|
2049
|
+
case "soothe.tool.execution.started": {
|
|
2050
|
+
const name = strField(data, "tool_name", "name");
|
|
2051
|
+
if (name) line = "Tool: " + name;
|
|
2052
|
+
break;
|
|
2053
|
+
}
|
|
2054
|
+
default:
|
|
2055
|
+
return ["", false];
|
|
2056
|
+
}
|
|
2057
|
+
line = line.trim();
|
|
2058
|
+
if (!line) return ["", false];
|
|
2059
|
+
const runes = [...line];
|
|
2060
|
+
if (runes.length > MAX_THINKING_STEP_RUNES) {
|
|
2061
|
+
line = runes.slice(0, MAX_THINKING_STEP_RUNES).join("") + "\u2026";
|
|
2062
|
+
}
|
|
2063
|
+
return [line, true];
|
|
1068
2064
|
}
|
|
2065
|
+
function formatPlanStepLine(data, suffix) {
|
|
2066
|
+
const stepID = strField(data, "step_id");
|
|
2067
|
+
const desc = strField(data, "description");
|
|
2068
|
+
if (stepID && suffix) return `Step ${stepID}: ${suffix}`;
|
|
2069
|
+
if (stepID && desc) return `Step ${stepID}: ${desc}`;
|
|
2070
|
+
if (stepID) return `Step ${stepID}`;
|
|
2071
|
+
if (desc && suffix) return `Step: ${suffix}`;
|
|
2072
|
+
if (desc) return `Step: ${desc}`;
|
|
2073
|
+
if (suffix) return "Step: " + suffix;
|
|
2074
|
+
return "";
|
|
2075
|
+
}
|
|
2076
|
+
function formatAgentStepLine(data, suffix) {
|
|
2077
|
+
const stepID = strField(data, "step_id");
|
|
2078
|
+
const desc = strField(data, "description");
|
|
2079
|
+
if (stepID && desc) return `Step ${stepID}: ${desc}`;
|
|
2080
|
+
if (desc) return suffix ? `Step: ${suffix}` : `Step: ${desc}`;
|
|
2081
|
+
if (stepID) return `Step ${stepID}`;
|
|
2082
|
+
return "";
|
|
2083
|
+
}
|
|
2084
|
+
function strField(data, ...keys) {
|
|
2085
|
+
for (const key of keys) {
|
|
2086
|
+
const v = data[key];
|
|
2087
|
+
if (typeof v === "string") {
|
|
2088
|
+
const s = v.trim();
|
|
2089
|
+
if (s) return s;
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
return "";
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
// src/appkit/classifier.ts
|
|
2096
|
+
var ChatEventTerminal = /* @__PURE__ */ ((ChatEventTerminal2) => {
|
|
2097
|
+
ChatEventTerminal2[ChatEventTerminal2["Continue"] = 0] = "Continue";
|
|
2098
|
+
ChatEventTerminal2[ChatEventTerminal2["DeliverableComplete"] = 1] = "DeliverableComplete";
|
|
2099
|
+
ChatEventTerminal2[ChatEventTerminal2["FailedComplete"] = 2] = "FailedComplete";
|
|
2100
|
+
return ChatEventTerminal2;
|
|
2101
|
+
})(ChatEventTerminal || {});
|
|
2102
|
+
var EVENT_LOOP_HISTORY_REPLAYED = "soothe.lifecycle.loop.history.replayed";
|
|
2103
|
+
var EventClassifier = class {
|
|
2104
|
+
deliverablePhases;
|
|
2105
|
+
minDeliverableRunes;
|
|
2106
|
+
thinkingStepEvents;
|
|
2107
|
+
constructor(cfg) {
|
|
2108
|
+
if (!cfg.deliverablePhases) {
|
|
2109
|
+
throw new Error("appkit: ClassifierConfig.deliverablePhases must not be nil");
|
|
2110
|
+
}
|
|
2111
|
+
this.deliverablePhases = cfg.deliverablePhases;
|
|
2112
|
+
this.minDeliverableRunes = cfg.minDeliverableRunes && cfg.minDeliverableRunes > 0 ? cfg.minDeliverableRunes : 8;
|
|
2113
|
+
this.thinkingStepEvents = cfg.thinkingStepEvents;
|
|
2114
|
+
}
|
|
2115
|
+
/**
|
|
2116
|
+
* Inspects one decoded event and returns its outcome. `accumulated` is the
|
|
2117
|
+
* running assistant text so far, used to pick the final reply when a
|
|
2118
|
+
* deliverable event arrives.
|
|
2119
|
+
*/
|
|
2120
|
+
classify(msg, accumulated) {
|
|
2121
|
+
return this.processChatEvent(msg, accumulated);
|
|
2122
|
+
}
|
|
2123
|
+
/**
|
|
2124
|
+
* Reports whether a persisted completion_event is user-facing. Uses the
|
|
2125
|
+
* configured deliverable phase set; recognizes the protocol output namespace
|
|
2126
|
+
* and final_report component as deliverable.
|
|
2127
|
+
*/
|
|
2128
|
+
isDeliverableCompletionEvent(eventType) {
|
|
2129
|
+
if (!eventType) return false;
|
|
2130
|
+
if (eventType === EventFinalReport) return true;
|
|
2131
|
+
if (eventType.startsWith("soothe.protocol.message.")) {
|
|
2132
|
+
const phase = eventType.slice("soothe.protocol.message.".length);
|
|
2133
|
+
return this.isDeliverableLoopPhase(phase);
|
|
2134
|
+
}
|
|
2135
|
+
return eventType.includes("soothe.output") && eventType.includes("responded");
|
|
2136
|
+
}
|
|
2137
|
+
isDeliverableLoopPhase(phase) {
|
|
2138
|
+
return this.deliverablePhases.has(phase);
|
|
2139
|
+
}
|
|
2140
|
+
deliverableResult(content, completionEvent) {
|
|
2141
|
+
return { content, terminal: 1 /* DeliverableComplete */, completionEvent };
|
|
2142
|
+
}
|
|
2143
|
+
continueResult(content) {
|
|
2144
|
+
return { content, terminal: 0 /* Continue */ };
|
|
2145
|
+
}
|
|
2146
|
+
failedResult(err) {
|
|
2147
|
+
return { terminal: 2 /* FailedComplete */, err };
|
|
2148
|
+
}
|
|
2149
|
+
/** Reports whether trimmed assistant text is long enough to persist as final. */
|
|
2150
|
+
isSubstantiveAssistantReply(content) {
|
|
2151
|
+
return [...content.trim()].length >= this.minDeliverableRunes;
|
|
2152
|
+
}
|
|
2153
|
+
/**
|
|
2154
|
+
* Picks the user-visible reply for a completed query. Only a deliverable
|
|
2155
|
+
* terminal result with a recognized completion event yields a final reply.
|
|
2156
|
+
*/
|
|
2157
|
+
resolveDeliverableFinalContent(eventResult, _accumulated) {
|
|
2158
|
+
if (eventResult.terminal !== 1 /* DeliverableComplete */) return ["", false];
|
|
2159
|
+
if (!this.isDeliverableCompletionEvent(eventResult.completionEvent ?? "")) return ["", false];
|
|
2160
|
+
const final = (eventResult.content ?? "").trim();
|
|
2161
|
+
if (final) return [final, true];
|
|
2162
|
+
return ["", false];
|
|
2163
|
+
}
|
|
2164
|
+
/** The event→outcome mapper, ported from triarch's ProcessChatEvent. */
|
|
2165
|
+
processChatEvent(msg, _accumulated) {
|
|
2166
|
+
if (!msg || typeof msg !== "object") {
|
|
2167
|
+
return { terminal: 0 /* Continue */ };
|
|
2168
|
+
}
|
|
2169
|
+
const m = msg;
|
|
2170
|
+
const typ = m.type;
|
|
2171
|
+
if (typ === "next") {
|
|
2172
|
+
return this.classifyNextEnvelope(m);
|
|
2173
|
+
}
|
|
2174
|
+
if (typ === "response" || typ === "complete" || typ === "receipt_response" || typ === "connection_ack" || typ === "status") {
|
|
2175
|
+
return { terminal: 0 /* Continue */ };
|
|
2176
|
+
}
|
|
2177
|
+
if (typ === "error") {
|
|
2178
|
+
const errObj = m.error ?? {};
|
|
2179
|
+
const code = typeof errObj.code === "number" ? errObj.code : -32603;
|
|
2180
|
+
return this.failedResult(
|
|
2181
|
+
new DaemonError(code, errObj.message ?? "daemon error", errObj.data)
|
|
2182
|
+
);
|
|
2183
|
+
}
|
|
2184
|
+
if (typ === "event") {
|
|
2185
|
+
return this.classifyEventPayload(
|
|
2186
|
+
m.namespace ?? null,
|
|
2187
|
+
m.mode ?? "",
|
|
2188
|
+
m.data
|
|
2189
|
+
);
|
|
2190
|
+
}
|
|
2191
|
+
return { terminal: 0 /* Continue */ };
|
|
2192
|
+
}
|
|
2193
|
+
/** Classifies a `next` envelope by projecting its payload. */
|
|
2194
|
+
classifyNextEnvelope(env) {
|
|
2195
|
+
const payload = env.payload ?? {};
|
|
2196
|
+
const innerData = payload.data;
|
|
2197
|
+
if (innerData && typeof innerData === "object") {
|
|
2198
|
+
const innerMode = innerData.mode ?? "";
|
|
2199
|
+
if (innerMode) {
|
|
2200
|
+
return this.classifyEventPayload(
|
|
2201
|
+
innerData.namespace ?? payload.namespace ?? null,
|
|
2202
|
+
innerMode,
|
|
2203
|
+
innerData.data
|
|
2204
|
+
);
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
const mode = payload.mode ?? "";
|
|
2208
|
+
if (mode) {
|
|
2209
|
+
return this.classifyEventPayload(payload.namespace ?? null, mode, payload.data);
|
|
2210
|
+
}
|
|
2211
|
+
return { terminal: 0 /* Continue */ };
|
|
2212
|
+
}
|
|
2213
|
+
/**
|
|
2214
|
+
* Classifies an event payload by (namespace, mode, phase). `data` may be a
|
|
2215
|
+
* map or an array of messages (mode="messages").
|
|
2216
|
+
*/
|
|
2217
|
+
classifyEventPayload(namespace, mode, data) {
|
|
2218
|
+
const ns = namespaceToString(namespace);
|
|
2219
|
+
const dataMap = normalizeEventData(data);
|
|
2220
|
+
if (dataMap) {
|
|
2221
|
+
let dataType2 = ns;
|
|
2222
|
+
const dt2 = dataMap["type"];
|
|
2223
|
+
if (typeof dt2 === "string" && dt2) dataType2 = dt2;
|
|
2224
|
+
if (dataType2 === EVENT_LOOP_HISTORY_REPLAYED) {
|
|
2225
|
+
return { terminal: 0 /* Continue */ };
|
|
2226
|
+
}
|
|
2227
|
+
const [step, ok] = extractThinkingStep(dataType2, dataMap, this.thinkingStepEvents);
|
|
2228
|
+
if (ok) {
|
|
2229
|
+
return { thinkingStep: step, terminal: 0 /* Continue */ };
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
if (mode === "messages") {
|
|
2233
|
+
const result = this.classifyMessagesMode(data, ns);
|
|
2234
|
+
if (result) return result;
|
|
2235
|
+
}
|
|
2236
|
+
if (!dataMap) {
|
|
2237
|
+
return { terminal: 0 /* Continue */ };
|
|
2238
|
+
}
|
|
2239
|
+
let dataType = ns;
|
|
2240
|
+
const dt = dataMap["type"];
|
|
2241
|
+
if (typeof dt === "string" && dt) dataType = dt;
|
|
2242
|
+
let completionEvent = dataType;
|
|
2243
|
+
if (!completionEvent) completionEvent = ns;
|
|
2244
|
+
if (isNamespaceMatch(ns, dataType, "soothe.output") || isNamespaceMatch(ns, dataType, "responded")) {
|
|
2245
|
+
const [content, ok] = extractContentFromData(dataMap);
|
|
2246
|
+
if (ok) {
|
|
2247
|
+
if (this.isFinalOutputEvent(dataType, ns)) {
|
|
2248
|
+
return this.deliverableResult(content, completionEvent);
|
|
2249
|
+
}
|
|
2250
|
+
return this.continueResult(content);
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
if (isNamespaceMatch(ns, dataType, "agent_loop.completed") || isNamespaceMatch(ns, dataType, "agent_loop.reasoned") || isNamespaceMatch(ns, dataType, "loop.completed")) {
|
|
2254
|
+
const [content, ok] = extractContentFromData(dataMap);
|
|
2255
|
+
if (ok) return this.continueResult(content);
|
|
2256
|
+
}
|
|
2257
|
+
if (isNamespaceMatch(ns, dataType, "final_report")) {
|
|
2258
|
+
const [content, ok] = extractContentFromData(dataMap);
|
|
2259
|
+
if (ok) return this.deliverableResult(content, completionEvent);
|
|
2260
|
+
}
|
|
2261
|
+
if (dataType.includes("soothe.error.") || ns.includes("soothe.error.")) {
|
|
2262
|
+
const errType = dataType || ns;
|
|
2263
|
+
const msg = dataMap["message"];
|
|
2264
|
+
if (typeof msg === "string" && msg) {
|
|
2265
|
+
return this.failedResult(new Error(`${errType}: ${msg}`));
|
|
2266
|
+
}
|
|
2267
|
+
const [content, ok] = extractContentFromData(dataMap);
|
|
2268
|
+
if (ok) return this.failedResult(new Error(`${errType}: ${content}`));
|
|
2269
|
+
return this.failedResult(new Error(errType));
|
|
2270
|
+
}
|
|
2271
|
+
if (isNamespaceMatch(ns, dataType, "stream") || isNamespaceMatch(ns, dataType, "progress") || isNamespaceMatch(ns, dataType, "tool_call_updates_batch") || isNamespaceMatch(ns, dataType, "soothe.stream.tool_call.update")) {
|
|
2272
|
+
const delta = dataMap["delta"];
|
|
2273
|
+
if (typeof delta === "string") return this.continueResult(delta);
|
|
2274
|
+
}
|
|
2275
|
+
if (isNamespaceMatch(ns, dataType, "heartbeat") || isNamespaceMatch(ns, dataType, "system.daemon") || isNamespaceMatch(ns, dataType, "agent_loop.started") || isNamespaceMatch(ns, dataType, "intent.classified")) {
|
|
2276
|
+
return { terminal: 0 /* Continue */ };
|
|
2277
|
+
}
|
|
2278
|
+
return { terminal: 0 /* Continue */ };
|
|
2279
|
+
}
|
|
2280
|
+
/** Classifies a mode="messages" payload (array of message objects). */
|
|
2281
|
+
classifyMessagesMode(data, _ns) {
|
|
2282
|
+
const items = Array.isArray(data) ? data : null;
|
|
2283
|
+
if (!items || items.length === 0) return null;
|
|
2284
|
+
const first = items[0];
|
|
2285
|
+
if (!first || typeof first !== "object") return null;
|
|
2286
|
+
const [msgType, rawContent, phase, hasPayload] = firstMessagePayload(data);
|
|
2287
|
+
if (hasPayload && rawContent && isStreamingMessageType(msgType)) {
|
|
2288
|
+
return this.continueResult(rawContent);
|
|
2289
|
+
}
|
|
2290
|
+
const loopMsg = loopAIMessage(data);
|
|
2291
|
+
if (loopMsg) {
|
|
2292
|
+
const content = loopMsg.content;
|
|
2293
|
+
if (content) {
|
|
2294
|
+
if (isStreamingMessageType(loopMsg.type)) {
|
|
2295
|
+
return this.continueResult(content);
|
|
2296
|
+
}
|
|
2297
|
+
if (this.isDeliverableLoopPhase(loopMsg.phase) && this.isSubstantiveAssistantReply(content)) {
|
|
2298
|
+
return this.deliverableResult(content, "soothe.protocol.message." + loopMsg.phase);
|
|
2299
|
+
}
|
|
2300
|
+
return this.continueResult(content);
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
const [directContent, directOk] = this.messagesModeAssistantContent(data);
|
|
2304
|
+
if (directOk && this.isSubstantiveAssistantReply(directContent)) {
|
|
2305
|
+
return this.deliverableResult(directContent, "soothe.protocol.message.direct_model");
|
|
2306
|
+
}
|
|
2307
|
+
if (hasPayload && rawContent) {
|
|
2308
|
+
if (isTerminalMessageType(msgType) || msgType === "") {
|
|
2309
|
+
if (this.isDeliverableLoopPhase(phase) && this.isSubstantiveAssistantReply(rawContent)) {
|
|
2310
|
+
return this.deliverableResult(rawContent, "soothe.protocol.message." + phase);
|
|
2311
|
+
}
|
|
2312
|
+
return this.continueResult(rawContent);
|
|
2313
|
+
}
|
|
2314
|
+
return this.continueResult(rawContent);
|
|
2315
|
+
}
|
|
2316
|
+
return null;
|
|
2317
|
+
}
|
|
2318
|
+
/**
|
|
2319
|
+
* Extracts plain assistant text from mode="messages" events that carry a
|
|
2320
|
+
* terminal AIMessage without loop-tagged phase metadata (legacy direct_llm turns
|
|
2321
|
+
* before phase tagging; prefer deliverablePhases including text_completion).
|
|
2322
|
+
*/
|
|
2323
|
+
messagesModeAssistantContent(data) {
|
|
2324
|
+
if (!Array.isArray(data) || data.length === 0) return ["", false];
|
|
2325
|
+
const msgMap = data[0];
|
|
2326
|
+
if (!msgMap || typeof msgMap !== "object") return ["", false];
|
|
2327
|
+
const phase = typeof msgMap.phase === "string" ? msgMap.phase.trim() : "";
|
|
2328
|
+
if (phase) return ["", false];
|
|
2329
|
+
const msgType = typeof msgMap.type === "string" ? msgMap.type : "";
|
|
2330
|
+
if (msgType && !isTerminalMessageType(msgType)) return ["", false];
|
|
2331
|
+
const content = extractContentFromMessage(msgMap).trim();
|
|
2332
|
+
if (!content) return ["", false];
|
|
2333
|
+
return [content, true];
|
|
2334
|
+
}
|
|
2335
|
+
/** soothe output/responded events that carry user-facing final text. */
|
|
2336
|
+
isFinalOutputEvent(dataType, ns) {
|
|
2337
|
+
const combined = dataType + " " + ns;
|
|
2338
|
+
if (combined.includes("final_report")) return true;
|
|
2339
|
+
for (const phase of this.deliverablePhases) {
|
|
2340
|
+
if (combined.includes(phase)) return true;
|
|
2341
|
+
}
|
|
2342
|
+
return false;
|
|
2343
|
+
}
|
|
2344
|
+
};
|
|
2345
|
+
function isStreamingMessageType(msgType) {
|
|
2346
|
+
return msgType === "AIMessageChunk" || msgType === "ai_chunk" || msgType === "message_chunk";
|
|
2347
|
+
}
|
|
2348
|
+
function isTerminalMessageType(msgType) {
|
|
2349
|
+
return msgType === "AIMessage" || msgType === "ai" || msgType === "assistant";
|
|
2350
|
+
}
|
|
2351
|
+
function firstMessagePayload(data) {
|
|
2352
|
+
if (!Array.isArray(data) || data.length === 0) return ["", "", "", false];
|
|
2353
|
+
const msgMap = data[0];
|
|
2354
|
+
if (!msgMap || typeof msgMap !== "object") return ["", "", "", false];
|
|
2355
|
+
const msgType = typeof msgMap.type === "string" ? msgMap.type : "";
|
|
2356
|
+
const phase = typeof msgMap.phase === "string" ? msgMap.phase : "";
|
|
2357
|
+
const content = extractContentFromMessage(msgMap);
|
|
2358
|
+
return [msgType, content, phase, true];
|
|
2359
|
+
}
|
|
2360
|
+
function loopAIMessage(data) {
|
|
2361
|
+
if (!Array.isArray(data) || data.length === 0) return null;
|
|
2362
|
+
const msgMap = data[0];
|
|
2363
|
+
if (!msgMap || typeof msgMap !== "object") return null;
|
|
2364
|
+
const phase = typeof msgMap.phase === "string" ? msgMap.phase.trim() : "";
|
|
2365
|
+
if (!phase) return null;
|
|
2366
|
+
const type = typeof msgMap.type === "string" ? msgMap.type : "";
|
|
2367
|
+
const content = extractContentFromMessage(msgMap);
|
|
2368
|
+
return { type, content, phase };
|
|
2369
|
+
}
|
|
2370
|
+
function extractContentFromMessage(msgMap) {
|
|
2371
|
+
const c = msgMap.content;
|
|
2372
|
+
if (typeof c === "string" && c) return c;
|
|
2373
|
+
if (Array.isArray(c) && c.length > 0) {
|
|
2374
|
+
let b = "";
|
|
2375
|
+
for (const item of c) {
|
|
2376
|
+
if (typeof item === "string") {
|
|
2377
|
+
b += item;
|
|
2378
|
+
continue;
|
|
2379
|
+
}
|
|
2380
|
+
if (item && typeof item === "object") {
|
|
2381
|
+
const blk = item;
|
|
2382
|
+
const t = blk.text;
|
|
2383
|
+
if (typeof t === "string") b += t;
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
return b;
|
|
2387
|
+
}
|
|
2388
|
+
const blocks = msgMap.content_blocks;
|
|
2389
|
+
if (Array.isArray(blocks) && blocks.length > 0) {
|
|
2390
|
+
let b = "";
|
|
2391
|
+
for (const blk of blocks) {
|
|
2392
|
+
if (blk && typeof blk === "object") {
|
|
2393
|
+
const m = blk;
|
|
2394
|
+
const t = m.text;
|
|
2395
|
+
if (typeof t === "string") b += t;
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
return b;
|
|
2399
|
+
}
|
|
2400
|
+
return "";
|
|
2401
|
+
}
|
|
2402
|
+
function extractContentFromData(data) {
|
|
2403
|
+
for (const key of [
|
|
2404
|
+
"final_stdout_message",
|
|
2405
|
+
"completion_summary",
|
|
2406
|
+
"content",
|
|
2407
|
+
"text",
|
|
2408
|
+
"response",
|
|
2409
|
+
"output",
|
|
2410
|
+
"message",
|
|
2411
|
+
"report"
|
|
2412
|
+
]) {
|
|
2413
|
+
const val = data[key];
|
|
2414
|
+
if (typeof val === "string" && val) return [val, true];
|
|
2415
|
+
}
|
|
2416
|
+
const nested = data.data;
|
|
2417
|
+
if (nested && typeof nested === "object") {
|
|
2418
|
+
const nm = nested;
|
|
2419
|
+
for (const key of [
|
|
2420
|
+
"final_stdout_message",
|
|
2421
|
+
"completion_summary",
|
|
2422
|
+
"content",
|
|
2423
|
+
"text",
|
|
2424
|
+
"response",
|
|
2425
|
+
"output",
|
|
2426
|
+
"message",
|
|
2427
|
+
"report"
|
|
2428
|
+
]) {
|
|
2429
|
+
const val = nm[key];
|
|
2430
|
+
if (typeof val === "string" && val) return [val, true];
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
return ["", false];
|
|
2434
|
+
}
|
|
2435
|
+
function isNamespaceMatch(ns, dataType, pattern) {
|
|
2436
|
+
return dataType.includes(pattern) || ns.includes(pattern);
|
|
2437
|
+
}
|
|
2438
|
+
function namespaceToString(namespace) {
|
|
2439
|
+
if (typeof namespace === "string") return namespace;
|
|
2440
|
+
if (Array.isArray(namespace)) return namespace.filter((s) => typeof s === "string").join(".");
|
|
2441
|
+
return "";
|
|
2442
|
+
}
|
|
2443
|
+
function normalizeEventData(data) {
|
|
2444
|
+
if (data == null) return null;
|
|
2445
|
+
if (typeof data === "object" && !Array.isArray(data)) {
|
|
2446
|
+
return data;
|
|
2447
|
+
}
|
|
2448
|
+
if (typeof data === "string") {
|
|
2449
|
+
try {
|
|
2450
|
+
const m = JSON.parse(data);
|
|
2451
|
+
if (m && typeof m === "object" && !Array.isArray(m)) return m;
|
|
2452
|
+
} catch {
|
|
2453
|
+
return null;
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
return null;
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
// src/appkit/query_gate.ts
|
|
2460
|
+
var ErrQueryBusy = class extends Error {
|
|
2461
|
+
constructor() {
|
|
2462
|
+
super("appkit: query already in progress for session");
|
|
2463
|
+
this.name = "ErrQueryBusy";
|
|
2464
|
+
}
|
|
2465
|
+
};
|
|
2466
|
+
var QueryGate = class {
|
|
2467
|
+
active = /* @__PURE__ */ new Map();
|
|
2468
|
+
/** Constructs an empty gate. */
|
|
2469
|
+
constructor() {
|
|
2470
|
+
}
|
|
2471
|
+
/**
|
|
2472
|
+
* Reserves sessionID for one agent turn. Returns ErrQueryBusy if a query is
|
|
2473
|
+
* already in flight. `abort` is the AbortController for the query's timeout
|
|
2474
|
+
* context. `sendCancel` is the daemon-cancel sender; it is invoked from
|
|
2475
|
+
* `cancel()` on a detached 10s timeout.
|
|
2476
|
+
*/
|
|
2477
|
+
acquire(sessionID, abort, sendCancel) {
|
|
2478
|
+
if (this.active.has(sessionID)) {
|
|
2479
|
+
throw new ErrQueryBusy();
|
|
2480
|
+
}
|
|
2481
|
+
this.active.set(sessionID, { abort, sendCancel });
|
|
2482
|
+
}
|
|
2483
|
+
/**
|
|
2484
|
+
* Cooperatively stops a running query for sessionID. Sends the daemon cancel
|
|
2485
|
+
* (on a detached 10s-timeout abort so caller cancellation cannot block the
|
|
2486
|
+
* wire send) BEFORE aborting the local context. Returns silently if no query
|
|
2487
|
+
* is in flight (intent already satisfied).
|
|
2488
|
+
*/
|
|
2489
|
+
async cancel(sessionID) {
|
|
2490
|
+
const state = this.active.get(sessionID);
|
|
2491
|
+
if (!state) return;
|
|
2492
|
+
this.active.delete(sessionID);
|
|
2493
|
+
if (state.sendCancel) {
|
|
2494
|
+
const detached = new AbortController();
|
|
2495
|
+
const timer = setTimeout(() => detached.abort(), 1e4);
|
|
2496
|
+
try {
|
|
2497
|
+
await state.sendCancel(detached.signal);
|
|
2498
|
+
} catch {
|
|
2499
|
+
} finally {
|
|
2500
|
+
clearTimeout(timer);
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
state.abort.abort();
|
|
2504
|
+
}
|
|
2505
|
+
/**
|
|
2506
|
+
* Clears the gate for sessionID without sending a daemon cancel. Call when a
|
|
2507
|
+
* query completes normally (success or local failure) so the next turn can
|
|
2508
|
+
* acquire.
|
|
2509
|
+
*/
|
|
2510
|
+
release(sessionID) {
|
|
2511
|
+
this.active.delete(sessionID);
|
|
2512
|
+
}
|
|
2513
|
+
/** Reports whether a query is in flight for sessionID. */
|
|
2514
|
+
isActive(sessionID) {
|
|
2515
|
+
return this.active.has(sessionID);
|
|
2516
|
+
}
|
|
2517
|
+
};
|
|
2518
|
+
|
|
2519
|
+
// src/appkit/pool.ts
|
|
2520
|
+
init_errors();
|
|
2521
|
+
init_config();
|
|
2522
|
+
|
|
2523
|
+
// src/appkit/client.ts
|
|
2524
|
+
init_config();
|
|
2525
|
+
init_client();
|
|
2526
|
+
function defaultClientFactory() {
|
|
2527
|
+
return (url, config) => {
|
|
2528
|
+
return new Client(url, config ?? defaultConfig());
|
|
2529
|
+
};
|
|
2530
|
+
}
|
|
2531
|
+
function defaultBootstrapFunc() {
|
|
2532
|
+
return async (client, workspaceID, userID, config) => {
|
|
2533
|
+
const c = client;
|
|
2534
|
+
const opts = {
|
|
2535
|
+
client_workspace: workspaceID,
|
|
2536
|
+
user_id: userID,
|
|
2537
|
+
client_workspace_id: workspaceID
|
|
2538
|
+
};
|
|
2539
|
+
return bootstrapLoopSession(c, "", config, opts);
|
|
2540
|
+
};
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2543
|
+
// src/appkit/pool.ts
|
|
2544
|
+
var ErrPoolExhausted = class extends Error {
|
|
2545
|
+
constructor() {
|
|
2546
|
+
super("appkit: connection pool exhausted");
|
|
2547
|
+
this.name = "ErrPoolExhausted";
|
|
2548
|
+
}
|
|
2549
|
+
};
|
|
2550
|
+
function defaultPoolConfig() {
|
|
2551
|
+
return {
|
|
2552
|
+
poolSize: 1e3,
|
|
2553
|
+
queryTimeout: 30 * 60 * 1e3,
|
|
2554
|
+
connectionTimeout: 3e4,
|
|
2555
|
+
maxIdleTime: 10 * 60 * 1e3,
|
|
2556
|
+
healthCheckInterval: 3e4
|
|
2557
|
+
};
|
|
2558
|
+
}
|
|
2559
|
+
var PooledConn = class {
|
|
2560
|
+
slotID;
|
|
2561
|
+
client;
|
|
2562
|
+
eventStream = null;
|
|
2563
|
+
streamController = null;
|
|
2564
|
+
sessionID = "";
|
|
2565
|
+
loopID = "";
|
|
2566
|
+
workspaceID = "";
|
|
2567
|
+
lastUsed = 0;
|
|
2568
|
+
constructor(slotID, client) {
|
|
2569
|
+
this.slotID = slotID;
|
|
2570
|
+
this.client = client;
|
|
2571
|
+
}
|
|
2572
|
+
/** Reports whether the underlying client signalled a drop. */
|
|
2573
|
+
isDisconnected() {
|
|
2574
|
+
return this.client.isDisconnected();
|
|
2575
|
+
}
|
|
2576
|
+
isConnected() {
|
|
2577
|
+
return this.client.isConnected() && !this.isDisconnected();
|
|
2578
|
+
}
|
|
2579
|
+
getLoopID() {
|
|
2580
|
+
return this.loopID;
|
|
2581
|
+
}
|
|
2582
|
+
};
|
|
2583
|
+
var ConnectionPool = class {
|
|
2584
|
+
cfg;
|
|
2585
|
+
scfg;
|
|
2586
|
+
factory;
|
|
2587
|
+
bootstrap;
|
|
2588
|
+
store;
|
|
2589
|
+
pool = [];
|
|
2590
|
+
activeSlots = /* @__PURE__ */ new Map();
|
|
2591
|
+
nextSlotID = 1;
|
|
2592
|
+
url;
|
|
2593
|
+
/**
|
|
2594
|
+
* Constructs a pool. `url` is the daemon WebSocket URL. If cfg is null,
|
|
2595
|
+
* defaultPoolConfig is used; if scfg is null, defaultConfig is used; nil
|
|
2596
|
+
* factory/bootstrap fall back to the defaults.
|
|
2597
|
+
*/
|
|
2598
|
+
constructor(url, store, cfg, scfg, factory) {
|
|
2599
|
+
this.cfg = cfg ?? defaultPoolConfig();
|
|
2600
|
+
this.scfg = scfg ?? defaultConfig();
|
|
2601
|
+
this.factory = factory ?? defaultClientFactory();
|
|
2602
|
+
this.bootstrap = defaultBootstrapFunc();
|
|
2603
|
+
this.store = store;
|
|
2604
|
+
this.url = url;
|
|
2605
|
+
for (let i = 0; i < this.cfg.poolSize; i++) {
|
|
2606
|
+
this.pool.push(new PooledConn(this.nextSlotID++, this.factory(url, this.scfg)));
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
/** Overrides the loop bootstrap function (useful for test fakes). */
|
|
2610
|
+
withBootstrap(f) {
|
|
2611
|
+
if (f) this.bootstrap = f;
|
|
2612
|
+
return this;
|
|
2613
|
+
}
|
|
2614
|
+
/**
|
|
2615
|
+
* Returns a live connection for sessionID, reusing an active slot or
|
|
2616
|
+
* bootstrapping/reattaching as needed. The caller must call `release()`
|
|
2617
|
+
* when done with the connection (a turn completes or the session is reset).
|
|
2618
|
+
*/
|
|
2619
|
+
async acquire(sessionID, workspaceID, userID, _signal) {
|
|
2620
|
+
const existing = this.activeSlots.get(sessionID);
|
|
2621
|
+
if (existing) {
|
|
2622
|
+
if (existing.isDisconnected() || !existing.isConnected()) {
|
|
2623
|
+
await this.release(sessionID);
|
|
2624
|
+
} else {
|
|
2625
|
+
existing.lastUsed = Date.now();
|
|
2626
|
+
await this.store.updateLastUsed(sessionID).catch(() => {
|
|
2627
|
+
});
|
|
2628
|
+
return existing;
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
const conn = this.pool.pop();
|
|
2632
|
+
if (!conn) throw new ErrPoolExhausted();
|
|
2633
|
+
this.activeSlots.set(sessionID, conn);
|
|
2634
|
+
const { loopID, ok } = await this.store.getLoopIDForSession(sessionID).catch(() => ({ loopID: "", ok: false }));
|
|
2635
|
+
let finalLoopID = "";
|
|
2636
|
+
try {
|
|
2637
|
+
if (!ok || !loopID) {
|
|
2638
|
+
await conn.client.connect();
|
|
2639
|
+
finalLoopID = await this.bootstrapNew(conn, workspaceID, userID);
|
|
2640
|
+
await this.store.createSession(workspaceID, sessionID, finalLoopID, "").catch(() => {
|
|
2641
|
+
});
|
|
2642
|
+
} else {
|
|
2643
|
+
try {
|
|
2644
|
+
await this.resumeAndReattach(conn, loopID);
|
|
2645
|
+
finalLoopID = loopID;
|
|
2646
|
+
} catch {
|
|
2647
|
+
await conn.client.connect();
|
|
2648
|
+
finalLoopID = await this.bootstrapNew(conn, workspaceID, userID);
|
|
2649
|
+
await this.store.createSession(workspaceID, sessionID, finalLoopID, "").catch(() => {
|
|
2650
|
+
});
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
} catch (err) {
|
|
2654
|
+
await this.release(sessionID);
|
|
2655
|
+
throw err;
|
|
2656
|
+
}
|
|
2657
|
+
conn.sessionID = sessionID;
|
|
2658
|
+
conn.loopID = finalLoopID;
|
|
2659
|
+
conn.workspaceID = workspaceID;
|
|
2660
|
+
conn.lastUsed = Date.now();
|
|
2661
|
+
await this.store.updateLastUsed(sessionID).catch(() => {
|
|
2662
|
+
});
|
|
2663
|
+
return conn;
|
|
2664
|
+
}
|
|
2665
|
+
/** Tears down the connection for sessionID and returns the slot. */
|
|
2666
|
+
async release(sessionID) {
|
|
2667
|
+
const conn = this.activeSlots.get(sessionID);
|
|
2668
|
+
if (!conn) return;
|
|
2669
|
+
this.activeSlots.delete(sessionID);
|
|
2670
|
+
if (conn.streamController) {
|
|
2671
|
+
conn.streamController.abort();
|
|
2672
|
+
conn.streamController = null;
|
|
2673
|
+
}
|
|
2674
|
+
try {
|
|
2675
|
+
conn.client.close();
|
|
2676
|
+
} catch {
|
|
2677
|
+
}
|
|
2678
|
+
conn.sessionID = "";
|
|
2679
|
+
conn.loopID = "";
|
|
2680
|
+
conn.eventStream = null;
|
|
2681
|
+
this.pool.push(new PooledConn(this.nextSlotID++, this.factory(this.url, this.scfg)));
|
|
2682
|
+
}
|
|
2683
|
+
/**
|
|
2684
|
+
* Tears down the connection for sessionID so the next acquire bootstraps
|
|
2685
|
+
* fresh. The store should archive the loop id so getLoopIDForSession returns
|
|
2686
|
+
* false next time.
|
|
2687
|
+
*/
|
|
2688
|
+
async resetSession(sessionID) {
|
|
2689
|
+
await this.release(sessionID);
|
|
2690
|
+
}
|
|
2691
|
+
/** Gracefully shuts down all active connections. */
|
|
2692
|
+
stop() {
|
|
2693
|
+
for (const [sid, conn] of this.activeSlots) {
|
|
2694
|
+
if (conn.streamController) conn.streamController.abort();
|
|
2695
|
+
try {
|
|
2696
|
+
conn.client.close();
|
|
2697
|
+
} catch {
|
|
2698
|
+
}
|
|
2699
|
+
this.activeSlots.delete(sid);
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
/** Stats snapshot for observability. */
|
|
2703
|
+
stats() {
|
|
2704
|
+
return { active: this.activeSlots.size, idle: this.pool.length };
|
|
2705
|
+
}
|
|
2706
|
+
/** Bootstrap a fresh loop and start the reader. */
|
|
2707
|
+
async bootstrapNew(conn, workspaceID, userID) {
|
|
2708
|
+
const loopID = await this.bootstrap(conn.client, workspaceID, userID, this.scfg);
|
|
2709
|
+
this.startReader(conn);
|
|
2710
|
+
return loopID;
|
|
2711
|
+
}
|
|
2712
|
+
/** Reconnect + reattach an existing loop, then start the reader. */
|
|
2713
|
+
async resumeAndReattach(conn, loopID) {
|
|
2714
|
+
await conn.client.connect();
|
|
2715
|
+
try {
|
|
2716
|
+
await conn.client.reattachAndProbe(loopID);
|
|
2717
|
+
} catch (err) {
|
|
2718
|
+
if (err instanceof StaleLoopError) throw err;
|
|
2719
|
+
throw err;
|
|
2720
|
+
}
|
|
2721
|
+
this.startReader(conn);
|
|
2722
|
+
}
|
|
2723
|
+
/** Starts a receiveMessages generator and stores the stream + controller. */
|
|
2724
|
+
startReader(conn) {
|
|
2725
|
+
const controller = new AbortController();
|
|
2726
|
+
conn.streamController = controller;
|
|
2727
|
+
conn.eventStream = conn.client.receiveMessages(controller.signal);
|
|
2728
|
+
}
|
|
2729
|
+
};
|
|
2730
|
+
|
|
2731
|
+
// src/appkit/turn_runner.ts
|
|
2732
|
+
init_intent_hints();
|
|
2733
|
+
var ErrQueryTimeout = class extends Error {
|
|
2734
|
+
constructor() {
|
|
2735
|
+
super("appkit: query timeout");
|
|
2736
|
+
this.name = "ErrQueryTimeout";
|
|
2737
|
+
}
|
|
2738
|
+
};
|
|
2739
|
+
function inputMessageForLoop(text, loopID, attachments, opts) {
|
|
2740
|
+
const msg = { type: "loop_input", content: text };
|
|
2741
|
+
if (loopID) msg.loop_id = loopID;
|
|
2742
|
+
if (attachments && attachments.length > 0) msg.attachments = attachments;
|
|
2743
|
+
if (opts) {
|
|
2744
|
+
if (opts.intentHint?.trim()) {
|
|
2745
|
+
const hintError = validateLoopInputIntentHint(opts.intentHint);
|
|
2746
|
+
if (hintError) {
|
|
2747
|
+
throw new Error(hintError);
|
|
2748
|
+
}
|
|
2749
|
+
msg.intent_hint = opts.intentHint.trim();
|
|
2750
|
+
}
|
|
2751
|
+
if (opts.preferredSubagent?.trim()) msg.preferred_subagent = opts.preferredSubagent.trim();
|
|
2752
|
+
if (opts.responseSchema && Object.keys(opts.responseSchema).length > 0) {
|
|
2753
|
+
msg.response_schema = opts.responseSchema;
|
|
2754
|
+
}
|
|
2755
|
+
if (opts.responseSchemaName?.trim()) msg.response_schema_name = opts.responseSchemaName.trim();
|
|
2756
|
+
if (opts.responseSchemaStrict !== void 0)
|
|
2757
|
+
msg.response_schema_strict = opts.responseSchemaStrict;
|
|
2758
|
+
}
|
|
2759
|
+
return msg;
|
|
2760
|
+
}
|
|
2761
|
+
var TurnRunner = class {
|
|
2762
|
+
pool;
|
|
2763
|
+
gate;
|
|
2764
|
+
classifier;
|
|
2765
|
+
store;
|
|
2766
|
+
broadcaster;
|
|
2767
|
+
cfg;
|
|
2768
|
+
buildInput = inputMessageForLoop;
|
|
2769
|
+
onComplete = null;
|
|
2770
|
+
onError = null;
|
|
2771
|
+
/**
|
|
2772
|
+
* Constructs a TurnRunner. pool, gate, classifier, and store are required;
|
|
2773
|
+
* broadcaster may be null.
|
|
2774
|
+
*/
|
|
2775
|
+
constructor(pool, gate, classifier, store, broadcaster, cfg) {
|
|
2776
|
+
this.pool = pool;
|
|
2777
|
+
this.gate = gate;
|
|
2778
|
+
this.classifier = classifier;
|
|
2779
|
+
this.store = store;
|
|
2780
|
+
this.broadcaster = broadcaster;
|
|
2781
|
+
this.cfg = { queryTimeout: cfg.queryTimeout > 0 ? cfg.queryTimeout : 30 * 60 * 1e3 };
|
|
2782
|
+
}
|
|
2783
|
+
/** Overrides the loop_input payload builder. */
|
|
2784
|
+
withInputBuilder(f) {
|
|
2785
|
+
if (f) this.buildInput = f;
|
|
2786
|
+
return this;
|
|
2787
|
+
}
|
|
2788
|
+
/** Sets a completion hook (runs inline on success). */
|
|
2789
|
+
withOnComplete(f) {
|
|
2790
|
+
this.onComplete = f;
|
|
2791
|
+
return this;
|
|
2792
|
+
}
|
|
2793
|
+
/** Sets an error hook (runs inline on failure). */
|
|
2794
|
+
withOnError(f) {
|
|
2795
|
+
this.onError = f;
|
|
2796
|
+
return this;
|
|
2797
|
+
}
|
|
2798
|
+
/**
|
|
2799
|
+
* Runs one query turn. The response is broadcast via the SSE broadcaster and
|
|
2800
|
+
* persisted via the SessionStore; it is not returned to the caller (SSE
|
|
2801
|
+
* subscribers receive it). Resolves on success; rejects on failure
|
|
2802
|
+
* (ErrQueryTimeout, AbortError, or a daemon/processing error).
|
|
2803
|
+
*/
|
|
2804
|
+
async execute(sessionID, message, userID, workspaceID, attachments, opts, signal) {
|
|
2805
|
+
let conn;
|
|
2806
|
+
try {
|
|
2807
|
+
conn = await this.pool.acquire(sessionID, workspaceID, userID, signal);
|
|
2808
|
+
} catch (err) {
|
|
2809
|
+
await this.persistFailed(sessionID, "", err);
|
|
2810
|
+
this.broadcastError(sessionID, err);
|
|
2811
|
+
this.onError?.(sessionID, "", err);
|
|
2812
|
+
throw err;
|
|
2813
|
+
}
|
|
2814
|
+
const loopID = conn.getLoopID();
|
|
2815
|
+
const timeoutController = new AbortController();
|
|
2816
|
+
const timeoutMs = this.cfg.queryTimeout;
|
|
2817
|
+
const timer = setTimeout(() => timeoutController.abort(), timeoutMs);
|
|
2818
|
+
const sendCancel = async (detachedSignal) => {
|
|
2819
|
+
await this.sendLoopCancel(detachedSignal, conn, loopID);
|
|
2820
|
+
};
|
|
2821
|
+
try {
|
|
2822
|
+
this.gate.acquire(sessionID, timeoutController, sendCancel);
|
|
2823
|
+
} catch (err) {
|
|
2824
|
+
clearTimeout(timer);
|
|
2825
|
+
await this.pool.release(sessionID);
|
|
2826
|
+
await this.persistFailed(sessionID, loopID, err);
|
|
2827
|
+
this.broadcastError(sessionID, err);
|
|
2828
|
+
this.onError?.(sessionID, loopID, err);
|
|
2829
|
+
throw err;
|
|
2830
|
+
}
|
|
2831
|
+
try {
|
|
2832
|
+
const inputMsg = this.buildInput(
|
|
2833
|
+
message,
|
|
2834
|
+
loopID,
|
|
2835
|
+
attachments ?? void 0,
|
|
2836
|
+
opts ?? void 0
|
|
2837
|
+
);
|
|
2838
|
+
try {
|
|
2839
|
+
await conn.client.sendMessage(inputMsg);
|
|
2840
|
+
} catch (err) {
|
|
2841
|
+
await this.persistFailed(sessionID, loopID, err);
|
|
2842
|
+
this.broadcastError(sessionID, err);
|
|
2843
|
+
this.onError?.(sessionID, loopID, err);
|
|
2844
|
+
throw err;
|
|
2845
|
+
}
|
|
2846
|
+
const eventStream = conn.eventStream;
|
|
2847
|
+
if (!eventStream) {
|
|
2848
|
+
const err = new Error(`missing event stream for session ${sessionID} (loop ${loopID})`);
|
|
2849
|
+
await this.persistFailed(sessionID, loopID, err);
|
|
2850
|
+
this.broadcastError(sessionID, err);
|
|
2851
|
+
this.onError?.(sessionID, loopID, err);
|
|
2852
|
+
throw err;
|
|
2853
|
+
}
|
|
2854
|
+
let assistantContent = "";
|
|
2855
|
+
const startedAt = Date.now();
|
|
2856
|
+
const abortRace = new Promise((resolve) => {
|
|
2857
|
+
const onTimeout = () => resolve("timeout");
|
|
2858
|
+
timeoutController.signal.addEventListener("abort", onTimeout, { once: true });
|
|
2859
|
+
if (signal) {
|
|
2860
|
+
const onCaller = () => resolve("caller");
|
|
2861
|
+
signal.addEventListener("abort", onCaller, { once: true });
|
|
2862
|
+
}
|
|
2863
|
+
});
|
|
2864
|
+
const iterator = eventStream[Symbol.asyncIterator]();
|
|
2865
|
+
while (true) {
|
|
2866
|
+
const next = iterator.next();
|
|
2867
|
+
const raced = await Promise.race([
|
|
2868
|
+
next.then((res2) => ({ tag: "msg", res: res2 })),
|
|
2869
|
+
abortRace.then((tag) => ({ tag }))
|
|
2870
|
+
]);
|
|
2871
|
+
if ("tag" in raced && raced.tag !== "msg") {
|
|
2872
|
+
if (raced.tag === "caller" || signal?.aborted) {
|
|
2873
|
+
const err = new Error("aborted");
|
|
2874
|
+
await this.persistFailed(sessionID, loopID, err);
|
|
2875
|
+
this.broadcastError(sessionID, err);
|
|
2876
|
+
this.onError?.(sessionID, loopID, err);
|
|
2877
|
+
throw err;
|
|
2878
|
+
}
|
|
2879
|
+
await this.sendLoopCancel(new AbortController().signal, conn, loopID).catch(() => {
|
|
2880
|
+
});
|
|
2881
|
+
await this.persistFailed(sessionID, loopID, new ErrQueryTimeout());
|
|
2882
|
+
this.broadcastError(sessionID, new ErrQueryTimeout());
|
|
2883
|
+
this.onError?.(sessionID, loopID, new ErrQueryTimeout());
|
|
2884
|
+
throw new ErrQueryTimeout();
|
|
2885
|
+
}
|
|
2886
|
+
const res = raced.res;
|
|
2887
|
+
if (res.done) {
|
|
2888
|
+
const err = new Error("event stream closed");
|
|
2889
|
+
await this.persistFailed(sessionID, loopID, err);
|
|
2890
|
+
this.broadcastError(sessionID, err);
|
|
2891
|
+
this.onError?.(sessionID, loopID, err);
|
|
2892
|
+
throw err;
|
|
2893
|
+
}
|
|
2894
|
+
const msg = res.value;
|
|
2895
|
+
const eventResult = this.classifier.classify(msg, assistantContent);
|
|
2896
|
+
if (eventResult.err && eventResult.terminal === 2 /* FailedComplete */) {
|
|
2897
|
+
await this.persistFailed(sessionID, loopID, eventResult.err);
|
|
2898
|
+
this.broadcastError(sessionID, eventResult.err);
|
|
2899
|
+
this.onError?.(sessionID, loopID, eventResult.err);
|
|
2900
|
+
throw eventResult.err;
|
|
2901
|
+
}
|
|
2902
|
+
const step = (eventResult.thinkingStep ?? "").trim();
|
|
2903
|
+
if (step) this.broadcastThinkingStep(sessionID, step);
|
|
2904
|
+
if (eventResult.content) {
|
|
2905
|
+
if (eventResult.content.startsWith(assistantContent)) {
|
|
2906
|
+
assistantContent = eventResult.content;
|
|
2907
|
+
} else {
|
|
2908
|
+
assistantContent += eventResult.content;
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
const [final, deliverable] = this.classifier.resolveDeliverableFinalContent(
|
|
2912
|
+
eventResult,
|
|
2913
|
+
assistantContent
|
|
2914
|
+
);
|
|
2915
|
+
if (deliverable) {
|
|
2916
|
+
const elapsedMs = Date.now() - startedAt;
|
|
2917
|
+
await this.persistResponse(
|
|
2918
|
+
sessionID,
|
|
2919
|
+
loopID,
|
|
2920
|
+
final,
|
|
2921
|
+
startedAt,
|
|
2922
|
+
eventResult.completionEvent ?? ""
|
|
2923
|
+
);
|
|
2924
|
+
this.broadcastComplete(sessionID, final);
|
|
2925
|
+
this.onComplete?.(sessionID, loopID, final, eventResult.completionEvent ?? "", elapsedMs);
|
|
2926
|
+
return;
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2929
|
+
} finally {
|
|
2930
|
+
clearTimeout(timer);
|
|
2931
|
+
this.gate.release(sessionID);
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
/** Asks the daemon to cooperatively stop the loop runner on a detached signal. */
|
|
2935
|
+
async sendLoopCancel(_signal, conn, loopID) {
|
|
2936
|
+
const lid = (loopID ?? "").trim();
|
|
2937
|
+
if (!conn || !lid) return;
|
|
2938
|
+
const cancelMsg = { type: "command_request", command: "cancel", loop_id: lid };
|
|
2939
|
+
await conn.client.sendMessage(cancelMsg);
|
|
2940
|
+
}
|
|
2941
|
+
async persistResponse(sessionID, loopID, content, startedAt, completionEvent) {
|
|
2942
|
+
const msg = {
|
|
2943
|
+
role: "assistant",
|
|
2944
|
+
content,
|
|
2945
|
+
metadata: {
|
|
2946
|
+
started_at: startedAt,
|
|
2947
|
+
completed_at: Date.now(),
|
|
2948
|
+
duration_ms: Date.now() - startedAt,
|
|
2949
|
+
status: "completed",
|
|
2950
|
+
completion_event: completionEvent,
|
|
2951
|
+
deliverable: true
|
|
2952
|
+
}
|
|
2953
|
+
};
|
|
2954
|
+
await this.store.appendMessage(sessionID, msg).catch(() => {
|
|
2955
|
+
});
|
|
2956
|
+
}
|
|
2957
|
+
async persistFailed(sessionID, _loopID, err) {
|
|
2958
|
+
const msg = {
|
|
2959
|
+
role: "error",
|
|
2960
|
+
content: err.message,
|
|
2961
|
+
metadata: { status: "failed", error_message: err.message }
|
|
2962
|
+
};
|
|
2963
|
+
await this.store.appendMessage(sessionID, msg).catch(() => {
|
|
2964
|
+
});
|
|
2965
|
+
}
|
|
2966
|
+
broadcastThinkingStep(sessionID, step) {
|
|
2967
|
+
if (!this.broadcaster) return;
|
|
2968
|
+
this.broadcaster.broadcast(sessionID, { type: "delta", data: step + "\n" });
|
|
2969
|
+
}
|
|
2970
|
+
broadcastComplete(sessionID, content) {
|
|
2971
|
+
this.broadcaster?.broadcast(sessionID, { type: "complete", data: content });
|
|
2972
|
+
}
|
|
2973
|
+
broadcastError(sessionID, err) {
|
|
2974
|
+
this.broadcaster?.broadcast(sessionID, { type: "query_error", data: err.message });
|
|
2975
|
+
}
|
|
2976
|
+
};
|
|
1069
2977
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1070
2978
|
0 && (module.exports = {
|
|
2979
|
+
CLIENT_VERSION,
|
|
2980
|
+
ChatEventTerminal,
|
|
1071
2981
|
Client,
|
|
1072
2982
|
ConnectionError,
|
|
2983
|
+
ConnectionPool,
|
|
2984
|
+
DEFAULT_CLIENT_CAPABILITIES,
|
|
2985
|
+
DEFAULT_DELIVERABLE_PHASES,
|
|
2986
|
+
DEFAULT_THINKING_STEP_EVENTS,
|
|
1073
2987
|
DaemonError,
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
2988
|
+
DisconnectCause,
|
|
2989
|
+
ErrPoolExhausted,
|
|
2990
|
+
ErrQueryBusy,
|
|
2991
|
+
ErrQueryTimeout,
|
|
2992
|
+
EventAutopilotGoalCompleted,
|
|
2993
|
+
EventAutopilotGoalCreated,
|
|
2994
|
+
EventAutopilotGoalProgress,
|
|
2995
|
+
EventAutopilotGoalStatus,
|
|
2996
|
+
EventAutopilotWorkerAssigned,
|
|
2997
|
+
EventAutopilotWorkerUnassigned,
|
|
2998
|
+
EventCardCreated,
|
|
2999
|
+
EventCardReplayBegin,
|
|
3000
|
+
EventCardReplayEnd,
|
|
3001
|
+
EventClassifier,
|
|
1079
3002
|
EventExploreCompleted,
|
|
1080
3003
|
EventExploreMilestone,
|
|
1081
3004
|
EventExploreStarted,
|
|
@@ -1087,6 +3010,14 @@ async function connectWithRetries(client, maxRetries, retryDelay) {
|
|
|
1087
3010
|
EventMessageSent,
|
|
1088
3011
|
EventPlanCreated,
|
|
1089
3012
|
EventReplayComplete,
|
|
3013
|
+
EventStrangeLoopCompleted,
|
|
3014
|
+
EventStrangeLoopContextCompacted,
|
|
3015
|
+
EventStrangeLoopPlanDecision,
|
|
3016
|
+
EventStrangeLoopReasoned,
|
|
3017
|
+
EventStrangeLoopStarted,
|
|
3018
|
+
EventStrangeLoopStepCompleted,
|
|
3019
|
+
EventStrangeLoopStepQueued,
|
|
3020
|
+
EventStrangeLoopStepStarted,
|
|
1090
3021
|
EventStreamToolCallUpdate,
|
|
1091
3022
|
EventTacitusCompleted,
|
|
1092
3023
|
EventTacitusGatherSummary,
|
|
@@ -1095,18 +3026,42 @@ async function connectWithRetries(client, maxRetries, retryDelay) {
|
|
|
1095
3026
|
EventToolCompleted,
|
|
1096
3027
|
EventToolError,
|
|
1097
3028
|
EventToolStarted,
|
|
3029
|
+
INTENT_HINT_EMBED,
|
|
3030
|
+
INTENT_HINT_IMAGE_TO_TEXT,
|
|
3031
|
+
INTENT_HINT_OCR,
|
|
3032
|
+
INTENT_HINT_TEXT_COMPLETION,
|
|
3033
|
+
LOOP_ASSISTANT_OUTPUT_PHASES,
|
|
3034
|
+
Multiplexer,
|
|
3035
|
+
PROTO_VERSION,
|
|
3036
|
+
PooledConn,
|
|
3037
|
+
QueryGate,
|
|
3038
|
+
REMOVED_INTENT_HINTS,
|
|
3039
|
+
ReconnectError,
|
|
3040
|
+
SSEBroadcaster,
|
|
3041
|
+
StaleLoopError,
|
|
1098
3042
|
TimeoutError,
|
|
3043
|
+
TurnRunner,
|
|
1099
3044
|
VerbosityTier,
|
|
3045
|
+
authenticate,
|
|
1100
3046
|
bootstrapLoopSession,
|
|
1101
3047
|
checkDaemonStatus,
|
|
1102
3048
|
classifyEventVerbosity,
|
|
1103
3049
|
connectWithRetries,
|
|
3050
|
+
connectionInitEnvelope,
|
|
1104
3051
|
decodeMessage,
|
|
3052
|
+
defaultBootstrapFunc,
|
|
3053
|
+
defaultClientFactory,
|
|
1105
3054
|
defaultConfig,
|
|
3055
|
+
defaultPoolConfig,
|
|
3056
|
+
disconnectCauseName,
|
|
3057
|
+
disconnectEnvelope,
|
|
1106
3058
|
encodeMessage,
|
|
1107
3059
|
extractSootheLoopID,
|
|
3060
|
+
extractThinkingStep,
|
|
1108
3061
|
fetchConfigSection,
|
|
3062
|
+
fetchLoopHistory,
|
|
1109
3063
|
fetchSkillsCatalog,
|
|
3064
|
+
inputMessageForLoop,
|
|
1110
3065
|
isCompletionEvent,
|
|
1111
3066
|
isDaemonLive,
|
|
1112
3067
|
isSubagentProgressEvent,
|
|
@@ -1116,10 +3071,19 @@ async function connectWithRetries(client, maxRetries, retryDelay) {
|
|
|
1116
3071
|
newLoopNewMessage,
|
|
1117
3072
|
newLoopSubscribeMessage,
|
|
1118
3073
|
newRequestID,
|
|
3074
|
+
notificationEnvelope,
|
|
1119
3075
|
parseNamespace,
|
|
3076
|
+
pingEnvelope,
|
|
3077
|
+
pongEnvelope,
|
|
3078
|
+
refreshAuthToken,
|
|
3079
|
+
requestDaemonConfigReload,
|
|
1120
3080
|
requestDaemonShutdown,
|
|
3081
|
+
requestEnvelope,
|
|
1121
3082
|
shouldShow,
|
|
1122
3083
|
splitWirePayload,
|
|
3084
|
+
subscribeEnvelope,
|
|
3085
|
+
unsubscribeEnvelope,
|
|
3086
|
+
validateLoopInputIntentHint,
|
|
1123
3087
|
waitDaemonReady,
|
|
1124
3088
|
waitLoopStatusWithID,
|
|
1125
3089
|
waitSubscriptionConfirmed
|