@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.js
CHANGED
|
@@ -1,7 +1,27 @@
|
|
|
1
1
|
import {
|
|
2
|
+
CLIENT_VERSION,
|
|
2
3
|
Client,
|
|
4
|
+
ConnectionError,
|
|
5
|
+
DEFAULT_CLIENT_CAPABILITIES,
|
|
6
|
+
DEFAULT_DELIVERABLE_PHASES,
|
|
7
|
+
DaemonError,
|
|
8
|
+
DisconnectCause,
|
|
9
|
+
INTENT_HINT_EMBED,
|
|
10
|
+
INTENT_HINT_IMAGE_TO_TEXT,
|
|
11
|
+
INTENT_HINT_OCR,
|
|
12
|
+
INTENT_HINT_TEXT_COMPLETION,
|
|
13
|
+
LOOP_ASSISTANT_OUTPUT_PHASES,
|
|
14
|
+
Multiplexer,
|
|
15
|
+
PROTO_VERSION,
|
|
16
|
+
REMOVED_INTENT_HINTS,
|
|
17
|
+
ReconnectError,
|
|
18
|
+
StaleLoopError,
|
|
19
|
+
TimeoutError,
|
|
20
|
+
connectionInitEnvelope,
|
|
3
21
|
decodeMessage,
|
|
4
22
|
defaultConfig,
|
|
23
|
+
disconnectCauseName,
|
|
24
|
+
disconnectEnvelope,
|
|
5
25
|
encodeMessage,
|
|
6
26
|
extractSootheLoopID,
|
|
7
27
|
loadConfigFromEnv,
|
|
@@ -9,43 +29,15 @@ import {
|
|
|
9
29
|
newLoopNewMessage,
|
|
10
30
|
newLoopSubscribeMessage,
|
|
11
31
|
newRequestID,
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
super(`connection error to ${url} (attempt ${attempt}): ${cause.message}`);
|
|
22
|
-
this.name = "ConnectionError";
|
|
23
|
-
this.url = url;
|
|
24
|
-
this.attempt = attempt;
|
|
25
|
-
this.cause = cause;
|
|
26
|
-
}
|
|
27
|
-
};
|
|
28
|
-
var DaemonError = class extends Error {
|
|
29
|
-
code;
|
|
30
|
-
/** The daemon's error message text. */
|
|
31
|
-
daemonMessage;
|
|
32
|
-
constructor(code, message) {
|
|
33
|
-
super(`daemon error [${code}]: ${message}`);
|
|
34
|
-
this.name = "DaemonError";
|
|
35
|
-
this.code = code;
|
|
36
|
-
this.daemonMessage = message;
|
|
37
|
-
}
|
|
38
|
-
};
|
|
39
|
-
var TimeoutError = class extends Error {
|
|
40
|
-
operation;
|
|
41
|
-
duration;
|
|
42
|
-
constructor(operation, duration) {
|
|
43
|
-
super(`timeout after ${duration} waiting for ${operation}`);
|
|
44
|
-
this.name = "TimeoutError";
|
|
45
|
-
this.operation = operation;
|
|
46
|
-
this.duration = duration;
|
|
47
|
-
}
|
|
48
|
-
};
|
|
32
|
+
notificationEnvelope,
|
|
33
|
+
pingEnvelope,
|
|
34
|
+
pongEnvelope,
|
|
35
|
+
requestEnvelope,
|
|
36
|
+
splitWirePayload,
|
|
37
|
+
subscribeEnvelope,
|
|
38
|
+
unsubscribeEnvelope,
|
|
39
|
+
validateLoopInputIntentHint
|
|
40
|
+
} from "./chunk-AQZACDIC.js";
|
|
49
41
|
|
|
50
42
|
// src/verbosity.ts
|
|
51
43
|
var VerbosityTier = /* @__PURE__ */ ((VerbosityTier2) => {
|
|
@@ -83,18 +75,31 @@ var EventTacitusGatherSummary = "soothe.subagent.tacitus.gather.summary";
|
|
|
83
75
|
var EventTacitusCompleted = "soothe.subagent.tacitus.completed";
|
|
84
76
|
var EventReplayComplete = "replay_complete";
|
|
85
77
|
var EventLoopReattachedWire = "loop_reattached";
|
|
78
|
+
var EventCardReplayBegin = "card.replay_begin";
|
|
79
|
+
var EventCardCreated = "card.created";
|
|
80
|
+
var EventCardReplayEnd = "card.replay_end";
|
|
86
81
|
var EventToolStarted = "soothe.tool.execution.started";
|
|
87
82
|
var EventToolCompleted = "soothe.tool.execution.completed";
|
|
88
83
|
var EventToolError = "soothe.tool.execution.error";
|
|
89
84
|
var EventStreamToolCallUpdate = "soothe.stream.tool_call.update";
|
|
90
85
|
var EventToolCallUpdatesBatch = "tool_call_updates_batch";
|
|
91
|
-
var
|
|
92
|
-
var
|
|
93
|
-
var
|
|
94
|
-
var
|
|
86
|
+
var EventStrangeLoopStarted = "soothe.cognition.strange_loop.started";
|
|
87
|
+
var EventStrangeLoopCompleted = "soothe.cognition.strange_loop.completed";
|
|
88
|
+
var EventStrangeLoopPlanDecision = "soothe.cognition.strange_loop.plan.decision";
|
|
89
|
+
var EventStrangeLoopReasoned = "soothe.cognition.strange_loop.reasoned";
|
|
90
|
+
var EventStrangeLoopStepStarted = "soothe.cognition.strange_loop.step.started";
|
|
91
|
+
var EventStrangeLoopStepQueued = "soothe.cognition.strange_loop.step.queued";
|
|
92
|
+
var EventStrangeLoopStepCompleted = "soothe.cognition.strange_loop.step.completed";
|
|
93
|
+
var EventStrangeLoopContextCompacted = "soothe.cognition.strange_loop.context.compacted";
|
|
95
94
|
var EventMessageReceived = "soothe.protocol.message.received";
|
|
96
95
|
var EventMessageSent = "soothe.protocol.message.sent";
|
|
97
96
|
var EventFinalReport = "soothe.output.autonomous.final_report.reported";
|
|
97
|
+
var EventAutopilotGoalStatus = "soothe.autopilot.goal.status";
|
|
98
|
+
var EventAutopilotGoalProgress = "soothe.autopilot.goal.progress";
|
|
99
|
+
var EventAutopilotGoalCreated = "soothe.autopilot.goal.created";
|
|
100
|
+
var EventAutopilotGoalCompleted = "soothe.autopilot.goal.completed";
|
|
101
|
+
var EventAutopilotWorkerAssigned = "soothe.autopilot.worker.assigned";
|
|
102
|
+
var EventAutopilotWorkerUnassigned = "soothe.autopilot.worker.unassigned";
|
|
98
103
|
var EventGeneralFailed = "soothe.error.general.failed";
|
|
99
104
|
function parseNamespace(ns) {
|
|
100
105
|
const parts = splitNamespace(ns);
|
|
@@ -135,6 +140,8 @@ function classifyByDomainAndComponent(domain, _component, full) {
|
|
|
135
140
|
return 99 /* Internal */;
|
|
136
141
|
case "subagent":
|
|
137
142
|
return classifySubagentEvent(full);
|
|
143
|
+
case "autopilot":
|
|
144
|
+
return 1 /* Normal */;
|
|
138
145
|
case "output":
|
|
139
146
|
case "error":
|
|
140
147
|
return 0 /* Quiet */;
|
|
@@ -172,24 +179,13 @@ function isSubagentProgressEvent(eventType) {
|
|
|
172
179
|
}
|
|
173
180
|
return parsed.action === "started" || parsed.action === "completed";
|
|
174
181
|
}
|
|
175
|
-
var ESSENTIAL_EVENT_TYPES = /* @__PURE__ */ new Set([
|
|
176
|
-
EventAgentLoopStarted,
|
|
177
|
-
EventAgentLoopCompleted,
|
|
178
|
-
EventAgentLoopReasoned,
|
|
179
|
-
EventPlanCreated,
|
|
180
|
-
EventExploreStarted,
|
|
181
|
-
EventExploreCompleted,
|
|
182
|
-
EventTacitusStarted,
|
|
183
|
-
EventTacitusCompleted,
|
|
184
|
-
EventGeneralFailed
|
|
185
|
-
]);
|
|
186
182
|
|
|
187
183
|
// src/helpers.ts
|
|
188
184
|
async function checkDaemonStatus(client, timeout) {
|
|
189
|
-
return client.requestResponse(
|
|
185
|
+
return client.requestResponse("daemon_status", {}, "daemon_status", timeout ?? 5e3);
|
|
190
186
|
}
|
|
191
187
|
async function isDaemonLive(wsURL, timeout) {
|
|
192
|
-
const { Client: Client2 } = await import("./client-
|
|
188
|
+
const { Client: Client2 } = await import("./client-CB6WKQYW.js");
|
|
193
189
|
const t = timeout ?? 5e3;
|
|
194
190
|
const client = new Client2(wsURL, defaultConfig());
|
|
195
191
|
try {
|
|
@@ -207,68 +203,103 @@ async function isDaemonLive(wsURL, timeout) {
|
|
|
207
203
|
}
|
|
208
204
|
}
|
|
209
205
|
async function requestDaemonShutdown(client, timeout) {
|
|
210
|
-
const resp = await client.requestResponse(
|
|
206
|
+
const resp = await client.requestResponse(
|
|
207
|
+
"daemon_shutdown",
|
|
208
|
+
{},
|
|
209
|
+
"daemon_shutdown",
|
|
210
|
+
timeout ?? 1e4
|
|
211
|
+
);
|
|
211
212
|
if (resp.status !== "acknowledged") {
|
|
212
213
|
throw new Error(`shutdown not acknowledged: ${JSON.stringify(resp)}`);
|
|
213
214
|
}
|
|
214
215
|
}
|
|
215
216
|
async function fetchSkillsCatalog(client, timeout) {
|
|
216
|
-
const resp = await client.requestResponse(
|
|
217
|
+
const resp = await client.requestResponse("skills_list", {}, "skills_list", timeout ?? 15e3);
|
|
217
218
|
const skillsRaw = resp.skills;
|
|
218
219
|
if (!skillsRaw || !Array.isArray(skillsRaw)) return [];
|
|
219
220
|
return skillsRaw.filter((s) => typeof s === "object" && s !== null);
|
|
220
221
|
}
|
|
221
222
|
async function fetchConfigSection(client, section, timeout) {
|
|
222
|
-
const resp = await client.requestResponse(
|
|
223
|
+
const resp = await client.requestResponse(
|
|
224
|
+
"config_get",
|
|
225
|
+
{ section },
|
|
226
|
+
"config_get",
|
|
227
|
+
timeout ?? 5e3
|
|
228
|
+
);
|
|
223
229
|
const sec = resp[section];
|
|
224
230
|
if (sec && typeof sec === "object") {
|
|
225
231
|
return sec;
|
|
226
232
|
}
|
|
227
233
|
return resp;
|
|
228
234
|
}
|
|
235
|
+
async function requestDaemonConfigReload(client, timeout) {
|
|
236
|
+
return client.requestResponse("config_reload", {}, "config_reload", timeout ?? 15e3);
|
|
237
|
+
}
|
|
238
|
+
async function fetchLoopHistory(client, loopID, timeout) {
|
|
239
|
+
return client.requestResponse(
|
|
240
|
+
"loop_history_fetch",
|
|
241
|
+
{ loop_id: loopID },
|
|
242
|
+
"loop_history_fetch",
|
|
243
|
+
timeout ?? 15e3
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
async function authenticate(client, accessKey, secretKey, timeout) {
|
|
247
|
+
return client.requestResponse(
|
|
248
|
+
"auth",
|
|
249
|
+
{ access_key: accessKey, secret_key: secretKey },
|
|
250
|
+
"auth",
|
|
251
|
+
timeout ?? 15e3
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
async function refreshAuthToken(client, refreshToken, timeout) {
|
|
255
|
+
return client.requestResponse(
|
|
256
|
+
"auth_refresh",
|
|
257
|
+
{ refresh_token: refreshToken },
|
|
258
|
+
"auth_refresh",
|
|
259
|
+
timeout ?? 15e3
|
|
260
|
+
);
|
|
261
|
+
}
|
|
229
262
|
|
|
230
263
|
// src/session.ts
|
|
231
264
|
async function bootstrapLoopSession(client, resumeLoopId, config, loopNew) {
|
|
232
265
|
const cfg = config ?? defaultConfig();
|
|
233
|
-
await client.sendMessage({ type: "daemon_ready" });
|
|
234
|
-
await waitDaemonReady(client, cfg.daemonReadyTimeout);
|
|
235
266
|
let loopId = (resumeLoopId ?? "").trim();
|
|
236
267
|
if (!loopId) {
|
|
268
|
+
const env = newLoopNewMessage(loopNew);
|
|
237
269
|
const newResp = await client.requestResponse(
|
|
238
|
-
|
|
239
|
-
|
|
270
|
+
env.method,
|
|
271
|
+
env.params ?? {},
|
|
272
|
+
"loop_new",
|
|
240
273
|
cfg.loopStatusTimeout
|
|
241
274
|
);
|
|
242
275
|
loopId = String(newResp.loop_id ?? "").trim();
|
|
243
276
|
if (!loopId) {
|
|
244
|
-
throw new Error("
|
|
277
|
+
throw new Error("loop_new response missing loop_id");
|
|
245
278
|
}
|
|
246
279
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
280
|
+
await client.subscribe(
|
|
281
|
+
"loop_events",
|
|
282
|
+
{ loop_id: loopId, verbosity: cfg.verbosityLevel },
|
|
250
283
|
cfg.subscriptionTimeout
|
|
251
284
|
);
|
|
252
|
-
if (subResp.success === false) {
|
|
253
|
-
throw new Error(String(subResp.message ?? "loop_subscribe failed"));
|
|
254
|
-
}
|
|
255
285
|
return loopId;
|
|
256
286
|
}
|
|
257
287
|
async function waitDaemonReady(client, timeout) {
|
|
288
|
+
if (client.isConnected()) return;
|
|
258
289
|
const deadline = Date.now() + timeout;
|
|
259
290
|
while (Date.now() < deadline) {
|
|
260
291
|
const remaining = deadline - Date.now();
|
|
261
292
|
if (remaining <= 0) break;
|
|
262
293
|
const ev = await client.readEventWithTimeout(remaining);
|
|
263
294
|
if (ev === null) break;
|
|
264
|
-
if (ev.type === "
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
);
|
|
295
|
+
if (ev.type === "connection_ack") {
|
|
296
|
+
const result = ev.result ?? {};
|
|
297
|
+
const state = result.readiness_state;
|
|
298
|
+
if (state === "ready") return;
|
|
299
|
+
throw new Error(`daemon not ready: state=${JSON.stringify(state ?? "unknown")}`);
|
|
269
300
|
}
|
|
270
301
|
}
|
|
271
|
-
throw new Error(`timeout after ${timeout}ms waiting for
|
|
302
|
+
throw new Error(`timeout after ${timeout}ms waiting for connection_ack (ready)`);
|
|
272
303
|
}
|
|
273
304
|
async function waitLoopStatusWithID(client, timeout) {
|
|
274
305
|
const deadline = Date.now() + timeout;
|
|
@@ -278,14 +309,13 @@ async function waitLoopStatusWithID(client, timeout) {
|
|
|
278
309
|
const ev = await client.readEventWithTimeout(remaining);
|
|
279
310
|
if (ev === null) break;
|
|
280
311
|
if (ev.type === "error") {
|
|
281
|
-
const
|
|
282
|
-
throw new
|
|
312
|
+
const errObj = ev.error ?? {};
|
|
313
|
+
throw new DaemonError(errObj.code ?? -32603, errObj.message ?? "daemon error");
|
|
283
314
|
}
|
|
284
315
|
if (ev.type === "status") {
|
|
285
|
-
const
|
|
286
|
-
const lid = status.loop_id;
|
|
316
|
+
const lid = ev.loop_id;
|
|
287
317
|
if (lid && lid !== "") {
|
|
288
|
-
return
|
|
318
|
+
return ev;
|
|
289
319
|
}
|
|
290
320
|
}
|
|
291
321
|
}
|
|
@@ -298,15 +328,18 @@ async function waitSubscriptionConfirmed(client, wantLoopID, _wantVerbosity, tim
|
|
|
298
328
|
if (remaining <= 0) break;
|
|
299
329
|
const ev = await client.readEventWithTimeout(remaining);
|
|
300
330
|
if (ev === null) break;
|
|
301
|
-
if (ev.type === "
|
|
302
|
-
|
|
331
|
+
if (ev.type === "next") {
|
|
332
|
+
const payload = ev.payload ?? {};
|
|
333
|
+
const lid = String(payload.loop_id ?? "");
|
|
334
|
+
if (lid === wantLoopID && payload.success === true) return;
|
|
335
|
+
continue;
|
|
303
336
|
}
|
|
304
|
-
if (ev.type === "
|
|
305
|
-
const
|
|
306
|
-
|
|
337
|
+
if (ev.type === "error") {
|
|
338
|
+
const errObj = ev.error ?? {};
|
|
339
|
+
throw new Error(`daemon error: ${errObj.message ?? "subscription failed"}`);
|
|
307
340
|
}
|
|
308
341
|
}
|
|
309
|
-
throw new Error(`timeout after ${timeout}ms waiting for
|
|
342
|
+
throw new Error(`timeout after ${timeout}ms waiting for subscription confirmation`);
|
|
310
343
|
}
|
|
311
344
|
async function connectWithRetries(client, maxRetries, retryDelay) {
|
|
312
345
|
const retries = maxRetries && maxRetries > 0 ? maxRetries : 40;
|
|
@@ -321,17 +354,1117 @@ async function connectWithRetries(client, maxRetries, retryDelay) {
|
|
|
321
354
|
}
|
|
322
355
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
323
356
|
}
|
|
324
|
-
throw new Error(
|
|
357
|
+
throw new Error(
|
|
358
|
+
`failed to connect after ${retries} attempts: ${lastErr?.message ?? "unknown error"}`
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// src/appkit/broadcaster.ts
|
|
363
|
+
var SUBSCRIBER_QUEUE_CAP = 100;
|
|
364
|
+
var SSEBroadcaster = class {
|
|
365
|
+
subscribers = /* @__PURE__ */ new Map();
|
|
366
|
+
nextSubID = 0;
|
|
367
|
+
/** Creates an empty broadcaster. */
|
|
368
|
+
constructor() {
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Registers a new subscriber channel for a session id. Returns an async
|
|
372
|
+
* iterable the subscriber reads events from. Unsubscribe via
|
|
373
|
+
* `unsubscribe()` or `close()`.
|
|
374
|
+
*/
|
|
375
|
+
subscribe(sessionID) {
|
|
376
|
+
const subID = String(this.nextSubID++);
|
|
377
|
+
const sub = { queue: [], waiters: [], closed: false };
|
|
378
|
+
let subs = this.subscribers.get(sessionID);
|
|
379
|
+
if (!subs) {
|
|
380
|
+
subs = /* @__PURE__ */ new Map();
|
|
381
|
+
this.subscribers.set(sessionID, subs);
|
|
382
|
+
}
|
|
383
|
+
subs.set(subID, sub);
|
|
384
|
+
const iterable = {
|
|
385
|
+
[Symbol.asyncIterator]() {
|
|
386
|
+
return {
|
|
387
|
+
next() {
|
|
388
|
+
if (sub.queue.length > 0) {
|
|
389
|
+
return Promise.resolve({ value: sub.queue.shift(), done: false });
|
|
390
|
+
}
|
|
391
|
+
if (sub.closed) {
|
|
392
|
+
return Promise.resolve({ value: void 0, done: true });
|
|
393
|
+
}
|
|
394
|
+
return new Promise((resolve) => {
|
|
395
|
+
sub.waiters.push((ev) => {
|
|
396
|
+
if (ev === null) {
|
|
397
|
+
resolve({ value: void 0, done: true });
|
|
398
|
+
} else {
|
|
399
|
+
resolve({ value: ev, done: false });
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
return { iterable, id: subID };
|
|
408
|
+
}
|
|
409
|
+
/** Removes a subscriber by id and closes its iterable. Safe if unknown. */
|
|
410
|
+
unsubscribe(sessionID, subID) {
|
|
411
|
+
const subs = this.subscribers.get(sessionID);
|
|
412
|
+
if (!subs) return;
|
|
413
|
+
const sub = subs.get(subID);
|
|
414
|
+
if (!sub) return;
|
|
415
|
+
sub.closed = true;
|
|
416
|
+
for (const w of sub.waiters) w(null);
|
|
417
|
+
sub.waiters = [];
|
|
418
|
+
subs.delete(subID);
|
|
419
|
+
if (subs.size === 0) this.subscribers.delete(sessionID);
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Sends an event to all subscribers for a session id. Non-blocking: a full
|
|
423
|
+
* subscriber queue is skipped (drop-on-full) so one slow consumer cannot
|
|
424
|
+
* block the others.
|
|
425
|
+
*/
|
|
426
|
+
broadcast(sessionID, event) {
|
|
427
|
+
const subs = this.subscribers.get(sessionID);
|
|
428
|
+
if (!subs) return;
|
|
429
|
+
for (const sub of subs.values()) {
|
|
430
|
+
if (sub.closed) continue;
|
|
431
|
+
if (sub.waiters.length > 0) {
|
|
432
|
+
const w = sub.waiters.shift();
|
|
433
|
+
w(event);
|
|
434
|
+
} else if (sub.queue.length < SUBSCRIBER_QUEUE_CAP) {
|
|
435
|
+
sub.queue.push(event);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
/** Closes all subscribers for a session id and removes the entry. */
|
|
440
|
+
close(sessionID) {
|
|
441
|
+
const subs = this.subscribers.get(sessionID);
|
|
442
|
+
if (!subs) return;
|
|
443
|
+
for (const sub of subs.values()) {
|
|
444
|
+
sub.closed = true;
|
|
445
|
+
for (const w of sub.waiters) w(null);
|
|
446
|
+
sub.waiters = [];
|
|
447
|
+
}
|
|
448
|
+
this.subscribers.delete(sessionID);
|
|
449
|
+
}
|
|
450
|
+
/** Closes every subscriber channel across all sessions. */
|
|
451
|
+
closeAll() {
|
|
452
|
+
for (const [sessionID, subs] of this.subscribers) {
|
|
453
|
+
for (const sub of subs.values()) {
|
|
454
|
+
sub.closed = true;
|
|
455
|
+
for (const w of sub.waiters) w(null);
|
|
456
|
+
sub.waiters = [];
|
|
457
|
+
}
|
|
458
|
+
this.subscribers.delete(sessionID);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
// src/appkit/thinking_step.ts
|
|
464
|
+
var MAX_THINKING_STEP_RUNES = 280;
|
|
465
|
+
var DEFAULT_THINKING_STEP_EVENTS = /* @__PURE__ */ new Set([
|
|
466
|
+
"soothe.cognition.plan.step.started",
|
|
467
|
+
"soothe.cognition.plan.step.completed",
|
|
468
|
+
"soothe.cognition.plan.step.failed",
|
|
469
|
+
"soothe.lifecycle.iteration.started",
|
|
470
|
+
"soothe.agent.loop.step.started",
|
|
471
|
+
"soothe.agent.loop.started",
|
|
472
|
+
"soothe.cognition.plan.batch.started",
|
|
473
|
+
"soothe.cognition.plan.created",
|
|
474
|
+
"soothe.cognition.goal.created",
|
|
475
|
+
"soothe.tool.execution.started"
|
|
476
|
+
]);
|
|
477
|
+
function extractThinkingStep(eventType, data, allow) {
|
|
478
|
+
if (!eventType || !data) return ["", false];
|
|
479
|
+
const et = eventType.trim();
|
|
480
|
+
if (!et) return ["", false];
|
|
481
|
+
const allowlist = allow ?? DEFAULT_THINKING_STEP_EVENTS;
|
|
482
|
+
if (!allowlist.has(et)) return ["", false];
|
|
483
|
+
let line = "";
|
|
484
|
+
switch (et) {
|
|
485
|
+
case "soothe.cognition.plan.step.started":
|
|
486
|
+
line = formatPlanStepLine(data, "");
|
|
487
|
+
break;
|
|
488
|
+
case "soothe.cognition.plan.step.completed":
|
|
489
|
+
line = formatPlanStepLine(data, "done");
|
|
490
|
+
break;
|
|
491
|
+
case "soothe.cognition.plan.step.failed": {
|
|
492
|
+
const stepID = strField(data, "step_id");
|
|
493
|
+
const errMsg = strField(data, "error");
|
|
494
|
+
if (stepID && errMsg) line = `Step ${stepID} failed: ${errMsg}`;
|
|
495
|
+
else if (stepID) line = `Step ${stepID} failed`;
|
|
496
|
+
else if (errMsg) line = `Step failed: ${errMsg}`;
|
|
497
|
+
break;
|
|
498
|
+
}
|
|
499
|
+
case "soothe.agent.loop.step.started":
|
|
500
|
+
line = formatAgentStepLine(data, "");
|
|
501
|
+
break;
|
|
502
|
+
case "soothe.cognition.plan.batch.started": {
|
|
503
|
+
const n = data["parallel_count"];
|
|
504
|
+
if (typeof n === "number" && n > 0) line = `Running ${Math.floor(n)} steps in parallel`;
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
case "soothe.cognition.plan.created":
|
|
508
|
+
case "soothe.agent.loop.started": {
|
|
509
|
+
const g = strField(data, "goal");
|
|
510
|
+
if (g) line = "Goal: " + g;
|
|
511
|
+
break;
|
|
512
|
+
}
|
|
513
|
+
case "soothe.cognition.goal.created": {
|
|
514
|
+
const g = strField(data, "friendly_message", "description");
|
|
515
|
+
if (g) line = "Goal: " + g;
|
|
516
|
+
break;
|
|
517
|
+
}
|
|
518
|
+
case "soothe.lifecycle.iteration.started": {
|
|
519
|
+
const g = strField(data, "goal_description");
|
|
520
|
+
if (g) line = "Iteration: " + g;
|
|
521
|
+
break;
|
|
522
|
+
}
|
|
523
|
+
case "soothe.tool.execution.started": {
|
|
524
|
+
const name = strField(data, "tool_name", "name");
|
|
525
|
+
if (name) line = "Tool: " + name;
|
|
526
|
+
break;
|
|
527
|
+
}
|
|
528
|
+
default:
|
|
529
|
+
return ["", false];
|
|
530
|
+
}
|
|
531
|
+
line = line.trim();
|
|
532
|
+
if (!line) return ["", false];
|
|
533
|
+
const runes = [...line];
|
|
534
|
+
if (runes.length > MAX_THINKING_STEP_RUNES) {
|
|
535
|
+
line = runes.slice(0, MAX_THINKING_STEP_RUNES).join("") + "\u2026";
|
|
536
|
+
}
|
|
537
|
+
return [line, true];
|
|
538
|
+
}
|
|
539
|
+
function formatPlanStepLine(data, suffix) {
|
|
540
|
+
const stepID = strField(data, "step_id");
|
|
541
|
+
const desc = strField(data, "description");
|
|
542
|
+
if (stepID && suffix) return `Step ${stepID}: ${suffix}`;
|
|
543
|
+
if (stepID && desc) return `Step ${stepID}: ${desc}`;
|
|
544
|
+
if (stepID) return `Step ${stepID}`;
|
|
545
|
+
if (desc && suffix) return `Step: ${suffix}`;
|
|
546
|
+
if (desc) return `Step: ${desc}`;
|
|
547
|
+
if (suffix) return "Step: " + suffix;
|
|
548
|
+
return "";
|
|
549
|
+
}
|
|
550
|
+
function formatAgentStepLine(data, suffix) {
|
|
551
|
+
const stepID = strField(data, "step_id");
|
|
552
|
+
const desc = strField(data, "description");
|
|
553
|
+
if (stepID && desc) return `Step ${stepID}: ${desc}`;
|
|
554
|
+
if (desc) return suffix ? `Step: ${suffix}` : `Step: ${desc}`;
|
|
555
|
+
if (stepID) return `Step ${stepID}`;
|
|
556
|
+
return "";
|
|
557
|
+
}
|
|
558
|
+
function strField(data, ...keys) {
|
|
559
|
+
for (const key of keys) {
|
|
560
|
+
const v = data[key];
|
|
561
|
+
if (typeof v === "string") {
|
|
562
|
+
const s = v.trim();
|
|
563
|
+
if (s) return s;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return "";
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// src/appkit/classifier.ts
|
|
570
|
+
var ChatEventTerminal = /* @__PURE__ */ ((ChatEventTerminal2) => {
|
|
571
|
+
ChatEventTerminal2[ChatEventTerminal2["Continue"] = 0] = "Continue";
|
|
572
|
+
ChatEventTerminal2[ChatEventTerminal2["DeliverableComplete"] = 1] = "DeliverableComplete";
|
|
573
|
+
ChatEventTerminal2[ChatEventTerminal2["FailedComplete"] = 2] = "FailedComplete";
|
|
574
|
+
return ChatEventTerminal2;
|
|
575
|
+
})(ChatEventTerminal || {});
|
|
576
|
+
var EVENT_LOOP_HISTORY_REPLAYED = "soothe.lifecycle.loop.history.replayed";
|
|
577
|
+
var EventClassifier = class {
|
|
578
|
+
deliverablePhases;
|
|
579
|
+
minDeliverableRunes;
|
|
580
|
+
thinkingStepEvents;
|
|
581
|
+
constructor(cfg) {
|
|
582
|
+
if (!cfg.deliverablePhases) {
|
|
583
|
+
throw new Error("appkit: ClassifierConfig.deliverablePhases must not be nil");
|
|
584
|
+
}
|
|
585
|
+
this.deliverablePhases = cfg.deliverablePhases;
|
|
586
|
+
this.minDeliverableRunes = cfg.minDeliverableRunes && cfg.minDeliverableRunes > 0 ? cfg.minDeliverableRunes : 8;
|
|
587
|
+
this.thinkingStepEvents = cfg.thinkingStepEvents;
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Inspects one decoded event and returns its outcome. `accumulated` is the
|
|
591
|
+
* running assistant text so far, used to pick the final reply when a
|
|
592
|
+
* deliverable event arrives.
|
|
593
|
+
*/
|
|
594
|
+
classify(msg, accumulated) {
|
|
595
|
+
return this.processChatEvent(msg, accumulated);
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Reports whether a persisted completion_event is user-facing. Uses the
|
|
599
|
+
* configured deliverable phase set; recognizes the protocol output namespace
|
|
600
|
+
* and final_report component as deliverable.
|
|
601
|
+
*/
|
|
602
|
+
isDeliverableCompletionEvent(eventType) {
|
|
603
|
+
if (!eventType) return false;
|
|
604
|
+
if (eventType === EventFinalReport) return true;
|
|
605
|
+
if (eventType.startsWith("soothe.protocol.message.")) {
|
|
606
|
+
const phase = eventType.slice("soothe.protocol.message.".length);
|
|
607
|
+
return this.isDeliverableLoopPhase(phase);
|
|
608
|
+
}
|
|
609
|
+
return eventType.includes("soothe.output") && eventType.includes("responded");
|
|
610
|
+
}
|
|
611
|
+
isDeliverableLoopPhase(phase) {
|
|
612
|
+
return this.deliverablePhases.has(phase);
|
|
613
|
+
}
|
|
614
|
+
deliverableResult(content, completionEvent) {
|
|
615
|
+
return { content, terminal: 1 /* DeliverableComplete */, completionEvent };
|
|
616
|
+
}
|
|
617
|
+
continueResult(content) {
|
|
618
|
+
return { content, terminal: 0 /* Continue */ };
|
|
619
|
+
}
|
|
620
|
+
failedResult(err) {
|
|
621
|
+
return { terminal: 2 /* FailedComplete */, err };
|
|
622
|
+
}
|
|
623
|
+
/** Reports whether trimmed assistant text is long enough to persist as final. */
|
|
624
|
+
isSubstantiveAssistantReply(content) {
|
|
625
|
+
return [...content.trim()].length >= this.minDeliverableRunes;
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Picks the user-visible reply for a completed query. Only a deliverable
|
|
629
|
+
* terminal result with a recognized completion event yields a final reply.
|
|
630
|
+
*/
|
|
631
|
+
resolveDeliverableFinalContent(eventResult, _accumulated) {
|
|
632
|
+
if (eventResult.terminal !== 1 /* DeliverableComplete */) return ["", false];
|
|
633
|
+
if (!this.isDeliverableCompletionEvent(eventResult.completionEvent ?? "")) return ["", false];
|
|
634
|
+
const final = (eventResult.content ?? "").trim();
|
|
635
|
+
if (final) return [final, true];
|
|
636
|
+
return ["", false];
|
|
637
|
+
}
|
|
638
|
+
/** The event→outcome mapper, ported from triarch's ProcessChatEvent. */
|
|
639
|
+
processChatEvent(msg, _accumulated) {
|
|
640
|
+
if (!msg || typeof msg !== "object") {
|
|
641
|
+
return { terminal: 0 /* Continue */ };
|
|
642
|
+
}
|
|
643
|
+
const m = msg;
|
|
644
|
+
const typ = m.type;
|
|
645
|
+
if (typ === "next") {
|
|
646
|
+
return this.classifyNextEnvelope(m);
|
|
647
|
+
}
|
|
648
|
+
if (typ === "response" || typ === "complete" || typ === "receipt_response" || typ === "connection_ack" || typ === "status") {
|
|
649
|
+
return { terminal: 0 /* Continue */ };
|
|
650
|
+
}
|
|
651
|
+
if (typ === "error") {
|
|
652
|
+
const errObj = m.error ?? {};
|
|
653
|
+
const code = typeof errObj.code === "number" ? errObj.code : -32603;
|
|
654
|
+
return this.failedResult(
|
|
655
|
+
new DaemonError(code, errObj.message ?? "daemon error", errObj.data)
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
if (typ === "event") {
|
|
659
|
+
return this.classifyEventPayload(
|
|
660
|
+
m.namespace ?? null,
|
|
661
|
+
m.mode ?? "",
|
|
662
|
+
m.data
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
return { terminal: 0 /* Continue */ };
|
|
666
|
+
}
|
|
667
|
+
/** Classifies a `next` envelope by projecting its payload. */
|
|
668
|
+
classifyNextEnvelope(env) {
|
|
669
|
+
const payload = env.payload ?? {};
|
|
670
|
+
const innerData = payload.data;
|
|
671
|
+
if (innerData && typeof innerData === "object") {
|
|
672
|
+
const innerMode = innerData.mode ?? "";
|
|
673
|
+
if (innerMode) {
|
|
674
|
+
return this.classifyEventPayload(
|
|
675
|
+
innerData.namespace ?? payload.namespace ?? null,
|
|
676
|
+
innerMode,
|
|
677
|
+
innerData.data
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
const mode = payload.mode ?? "";
|
|
682
|
+
if (mode) {
|
|
683
|
+
return this.classifyEventPayload(payload.namespace ?? null, mode, payload.data);
|
|
684
|
+
}
|
|
685
|
+
return { terminal: 0 /* Continue */ };
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Classifies an event payload by (namespace, mode, phase). `data` may be a
|
|
689
|
+
* map or an array of messages (mode="messages").
|
|
690
|
+
*/
|
|
691
|
+
classifyEventPayload(namespace, mode, data) {
|
|
692
|
+
const ns = namespaceToString(namespace);
|
|
693
|
+
const dataMap = normalizeEventData(data);
|
|
694
|
+
if (dataMap) {
|
|
695
|
+
let dataType2 = ns;
|
|
696
|
+
const dt2 = dataMap["type"];
|
|
697
|
+
if (typeof dt2 === "string" && dt2) dataType2 = dt2;
|
|
698
|
+
if (dataType2 === EVENT_LOOP_HISTORY_REPLAYED) {
|
|
699
|
+
return { terminal: 0 /* Continue */ };
|
|
700
|
+
}
|
|
701
|
+
const [step, ok] = extractThinkingStep(dataType2, dataMap, this.thinkingStepEvents);
|
|
702
|
+
if (ok) {
|
|
703
|
+
return { thinkingStep: step, terminal: 0 /* Continue */ };
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
if (mode === "messages") {
|
|
707
|
+
const result = this.classifyMessagesMode(data, ns);
|
|
708
|
+
if (result) return result;
|
|
709
|
+
}
|
|
710
|
+
if (!dataMap) {
|
|
711
|
+
return { terminal: 0 /* Continue */ };
|
|
712
|
+
}
|
|
713
|
+
let dataType = ns;
|
|
714
|
+
const dt = dataMap["type"];
|
|
715
|
+
if (typeof dt === "string" && dt) dataType = dt;
|
|
716
|
+
let completionEvent = dataType;
|
|
717
|
+
if (!completionEvent) completionEvent = ns;
|
|
718
|
+
if (isNamespaceMatch(ns, dataType, "soothe.output") || isNamespaceMatch(ns, dataType, "responded")) {
|
|
719
|
+
const [content, ok] = extractContentFromData(dataMap);
|
|
720
|
+
if (ok) {
|
|
721
|
+
if (this.isFinalOutputEvent(dataType, ns)) {
|
|
722
|
+
return this.deliverableResult(content, completionEvent);
|
|
723
|
+
}
|
|
724
|
+
return this.continueResult(content);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
if (isNamespaceMatch(ns, dataType, "agent_loop.completed") || isNamespaceMatch(ns, dataType, "agent_loop.reasoned") || isNamespaceMatch(ns, dataType, "loop.completed")) {
|
|
728
|
+
const [content, ok] = extractContentFromData(dataMap);
|
|
729
|
+
if (ok) return this.continueResult(content);
|
|
730
|
+
}
|
|
731
|
+
if (isNamespaceMatch(ns, dataType, "final_report")) {
|
|
732
|
+
const [content, ok] = extractContentFromData(dataMap);
|
|
733
|
+
if (ok) return this.deliverableResult(content, completionEvent);
|
|
734
|
+
}
|
|
735
|
+
if (dataType.includes("soothe.error.") || ns.includes("soothe.error.")) {
|
|
736
|
+
const errType = dataType || ns;
|
|
737
|
+
const msg = dataMap["message"];
|
|
738
|
+
if (typeof msg === "string" && msg) {
|
|
739
|
+
return this.failedResult(new Error(`${errType}: ${msg}`));
|
|
740
|
+
}
|
|
741
|
+
const [content, ok] = extractContentFromData(dataMap);
|
|
742
|
+
if (ok) return this.failedResult(new Error(`${errType}: ${content}`));
|
|
743
|
+
return this.failedResult(new Error(errType));
|
|
744
|
+
}
|
|
745
|
+
if (isNamespaceMatch(ns, dataType, "stream") || isNamespaceMatch(ns, dataType, "progress") || isNamespaceMatch(ns, dataType, "tool_call_updates_batch") || isNamespaceMatch(ns, dataType, "soothe.stream.tool_call.update")) {
|
|
746
|
+
const delta = dataMap["delta"];
|
|
747
|
+
if (typeof delta === "string") return this.continueResult(delta);
|
|
748
|
+
}
|
|
749
|
+
if (isNamespaceMatch(ns, dataType, "heartbeat") || isNamespaceMatch(ns, dataType, "system.daemon") || isNamespaceMatch(ns, dataType, "agent_loop.started") || isNamespaceMatch(ns, dataType, "intent.classified")) {
|
|
750
|
+
return { terminal: 0 /* Continue */ };
|
|
751
|
+
}
|
|
752
|
+
return { terminal: 0 /* Continue */ };
|
|
753
|
+
}
|
|
754
|
+
/** Classifies a mode="messages" payload (array of message objects). */
|
|
755
|
+
classifyMessagesMode(data, _ns) {
|
|
756
|
+
const items = Array.isArray(data) ? data : null;
|
|
757
|
+
if (!items || items.length === 0) return null;
|
|
758
|
+
const first = items[0];
|
|
759
|
+
if (!first || typeof first !== "object") return null;
|
|
760
|
+
const [msgType, rawContent, phase, hasPayload] = firstMessagePayload(data);
|
|
761
|
+
if (hasPayload && rawContent && isStreamingMessageType(msgType)) {
|
|
762
|
+
return this.continueResult(rawContent);
|
|
763
|
+
}
|
|
764
|
+
const loopMsg = loopAIMessage(data);
|
|
765
|
+
if (loopMsg) {
|
|
766
|
+
const content = loopMsg.content;
|
|
767
|
+
if (content) {
|
|
768
|
+
if (isStreamingMessageType(loopMsg.type)) {
|
|
769
|
+
return this.continueResult(content);
|
|
770
|
+
}
|
|
771
|
+
if (this.isDeliverableLoopPhase(loopMsg.phase) && this.isSubstantiveAssistantReply(content)) {
|
|
772
|
+
return this.deliverableResult(content, "soothe.protocol.message." + loopMsg.phase);
|
|
773
|
+
}
|
|
774
|
+
return this.continueResult(content);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
const [directContent, directOk] = this.messagesModeAssistantContent(data);
|
|
778
|
+
if (directOk && this.isSubstantiveAssistantReply(directContent)) {
|
|
779
|
+
return this.deliverableResult(directContent, "soothe.protocol.message.direct_model");
|
|
780
|
+
}
|
|
781
|
+
if (hasPayload && rawContent) {
|
|
782
|
+
if (isTerminalMessageType(msgType) || msgType === "") {
|
|
783
|
+
if (this.isDeliverableLoopPhase(phase) && this.isSubstantiveAssistantReply(rawContent)) {
|
|
784
|
+
return this.deliverableResult(rawContent, "soothe.protocol.message." + phase);
|
|
785
|
+
}
|
|
786
|
+
return this.continueResult(rawContent);
|
|
787
|
+
}
|
|
788
|
+
return this.continueResult(rawContent);
|
|
789
|
+
}
|
|
790
|
+
return null;
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* Extracts plain assistant text from mode="messages" events that carry a
|
|
794
|
+
* terminal AIMessage without loop-tagged phase metadata (legacy direct_llm turns
|
|
795
|
+
* before phase tagging; prefer deliverablePhases including text_completion).
|
|
796
|
+
*/
|
|
797
|
+
messagesModeAssistantContent(data) {
|
|
798
|
+
if (!Array.isArray(data) || data.length === 0) return ["", false];
|
|
799
|
+
const msgMap = data[0];
|
|
800
|
+
if (!msgMap || typeof msgMap !== "object") return ["", false];
|
|
801
|
+
const phase = typeof msgMap.phase === "string" ? msgMap.phase.trim() : "";
|
|
802
|
+
if (phase) return ["", false];
|
|
803
|
+
const msgType = typeof msgMap.type === "string" ? msgMap.type : "";
|
|
804
|
+
if (msgType && !isTerminalMessageType(msgType)) return ["", false];
|
|
805
|
+
const content = extractContentFromMessage(msgMap).trim();
|
|
806
|
+
if (!content) return ["", false];
|
|
807
|
+
return [content, true];
|
|
808
|
+
}
|
|
809
|
+
/** soothe output/responded events that carry user-facing final text. */
|
|
810
|
+
isFinalOutputEvent(dataType, ns) {
|
|
811
|
+
const combined = dataType + " " + ns;
|
|
812
|
+
if (combined.includes("final_report")) return true;
|
|
813
|
+
for (const phase of this.deliverablePhases) {
|
|
814
|
+
if (combined.includes(phase)) return true;
|
|
815
|
+
}
|
|
816
|
+
return false;
|
|
817
|
+
}
|
|
818
|
+
};
|
|
819
|
+
function isStreamingMessageType(msgType) {
|
|
820
|
+
return msgType === "AIMessageChunk" || msgType === "ai_chunk" || msgType === "message_chunk";
|
|
821
|
+
}
|
|
822
|
+
function isTerminalMessageType(msgType) {
|
|
823
|
+
return msgType === "AIMessage" || msgType === "ai" || msgType === "assistant";
|
|
824
|
+
}
|
|
825
|
+
function firstMessagePayload(data) {
|
|
826
|
+
if (!Array.isArray(data) || data.length === 0) return ["", "", "", false];
|
|
827
|
+
const msgMap = data[0];
|
|
828
|
+
if (!msgMap || typeof msgMap !== "object") return ["", "", "", false];
|
|
829
|
+
const msgType = typeof msgMap.type === "string" ? msgMap.type : "";
|
|
830
|
+
const phase = typeof msgMap.phase === "string" ? msgMap.phase : "";
|
|
831
|
+
const content = extractContentFromMessage(msgMap);
|
|
832
|
+
return [msgType, content, phase, true];
|
|
833
|
+
}
|
|
834
|
+
function loopAIMessage(data) {
|
|
835
|
+
if (!Array.isArray(data) || data.length === 0) return null;
|
|
836
|
+
const msgMap = data[0];
|
|
837
|
+
if (!msgMap || typeof msgMap !== "object") return null;
|
|
838
|
+
const phase = typeof msgMap.phase === "string" ? msgMap.phase.trim() : "";
|
|
839
|
+
if (!phase) return null;
|
|
840
|
+
const type = typeof msgMap.type === "string" ? msgMap.type : "";
|
|
841
|
+
const content = extractContentFromMessage(msgMap);
|
|
842
|
+
return { type, content, phase };
|
|
843
|
+
}
|
|
844
|
+
function extractContentFromMessage(msgMap) {
|
|
845
|
+
const c = msgMap.content;
|
|
846
|
+
if (typeof c === "string" && c) return c;
|
|
847
|
+
if (Array.isArray(c) && c.length > 0) {
|
|
848
|
+
let b = "";
|
|
849
|
+
for (const item of c) {
|
|
850
|
+
if (typeof item === "string") {
|
|
851
|
+
b += item;
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
if (item && typeof item === "object") {
|
|
855
|
+
const blk = item;
|
|
856
|
+
const t = blk.text;
|
|
857
|
+
if (typeof t === "string") b += t;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
return b;
|
|
861
|
+
}
|
|
862
|
+
const blocks = msgMap.content_blocks;
|
|
863
|
+
if (Array.isArray(blocks) && blocks.length > 0) {
|
|
864
|
+
let b = "";
|
|
865
|
+
for (const blk of blocks) {
|
|
866
|
+
if (blk && typeof blk === "object") {
|
|
867
|
+
const m = blk;
|
|
868
|
+
const t = m.text;
|
|
869
|
+
if (typeof t === "string") b += t;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
return b;
|
|
873
|
+
}
|
|
874
|
+
return "";
|
|
875
|
+
}
|
|
876
|
+
function extractContentFromData(data) {
|
|
877
|
+
for (const key of [
|
|
878
|
+
"final_stdout_message",
|
|
879
|
+
"completion_summary",
|
|
880
|
+
"content",
|
|
881
|
+
"text",
|
|
882
|
+
"response",
|
|
883
|
+
"output",
|
|
884
|
+
"message",
|
|
885
|
+
"report"
|
|
886
|
+
]) {
|
|
887
|
+
const val = data[key];
|
|
888
|
+
if (typeof val === "string" && val) return [val, true];
|
|
889
|
+
}
|
|
890
|
+
const nested = data.data;
|
|
891
|
+
if (nested && typeof nested === "object") {
|
|
892
|
+
const nm = nested;
|
|
893
|
+
for (const key of [
|
|
894
|
+
"final_stdout_message",
|
|
895
|
+
"completion_summary",
|
|
896
|
+
"content",
|
|
897
|
+
"text",
|
|
898
|
+
"response",
|
|
899
|
+
"output",
|
|
900
|
+
"message",
|
|
901
|
+
"report"
|
|
902
|
+
]) {
|
|
903
|
+
const val = nm[key];
|
|
904
|
+
if (typeof val === "string" && val) return [val, true];
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
return ["", false];
|
|
908
|
+
}
|
|
909
|
+
function isNamespaceMatch(ns, dataType, pattern) {
|
|
910
|
+
return dataType.includes(pattern) || ns.includes(pattern);
|
|
911
|
+
}
|
|
912
|
+
function namespaceToString(namespace) {
|
|
913
|
+
if (typeof namespace === "string") return namespace;
|
|
914
|
+
if (Array.isArray(namespace)) return namespace.filter((s) => typeof s === "string").join(".");
|
|
915
|
+
return "";
|
|
916
|
+
}
|
|
917
|
+
function normalizeEventData(data) {
|
|
918
|
+
if (data == null) return null;
|
|
919
|
+
if (typeof data === "object" && !Array.isArray(data)) {
|
|
920
|
+
return data;
|
|
921
|
+
}
|
|
922
|
+
if (typeof data === "string") {
|
|
923
|
+
try {
|
|
924
|
+
const m = JSON.parse(data);
|
|
925
|
+
if (m && typeof m === "object" && !Array.isArray(m)) return m;
|
|
926
|
+
} catch {
|
|
927
|
+
return null;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
return null;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// src/appkit/query_gate.ts
|
|
934
|
+
var ErrQueryBusy = class extends Error {
|
|
935
|
+
constructor() {
|
|
936
|
+
super("appkit: query already in progress for session");
|
|
937
|
+
this.name = "ErrQueryBusy";
|
|
938
|
+
}
|
|
939
|
+
};
|
|
940
|
+
var QueryGate = class {
|
|
941
|
+
active = /* @__PURE__ */ new Map();
|
|
942
|
+
/** Constructs an empty gate. */
|
|
943
|
+
constructor() {
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* Reserves sessionID for one agent turn. Returns ErrQueryBusy if a query is
|
|
947
|
+
* already in flight. `abort` is the AbortController for the query's timeout
|
|
948
|
+
* context. `sendCancel` is the daemon-cancel sender; it is invoked from
|
|
949
|
+
* `cancel()` on a detached 10s timeout.
|
|
950
|
+
*/
|
|
951
|
+
acquire(sessionID, abort, sendCancel) {
|
|
952
|
+
if (this.active.has(sessionID)) {
|
|
953
|
+
throw new ErrQueryBusy();
|
|
954
|
+
}
|
|
955
|
+
this.active.set(sessionID, { abort, sendCancel });
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* Cooperatively stops a running query for sessionID. Sends the daemon cancel
|
|
959
|
+
* (on a detached 10s-timeout abort so caller cancellation cannot block the
|
|
960
|
+
* wire send) BEFORE aborting the local context. Returns silently if no query
|
|
961
|
+
* is in flight (intent already satisfied).
|
|
962
|
+
*/
|
|
963
|
+
async cancel(sessionID) {
|
|
964
|
+
const state = this.active.get(sessionID);
|
|
965
|
+
if (!state) return;
|
|
966
|
+
this.active.delete(sessionID);
|
|
967
|
+
if (state.sendCancel) {
|
|
968
|
+
const detached = new AbortController();
|
|
969
|
+
const timer = setTimeout(() => detached.abort(), 1e4);
|
|
970
|
+
try {
|
|
971
|
+
await state.sendCancel(detached.signal);
|
|
972
|
+
} catch {
|
|
973
|
+
} finally {
|
|
974
|
+
clearTimeout(timer);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
state.abort.abort();
|
|
978
|
+
}
|
|
979
|
+
/**
|
|
980
|
+
* Clears the gate for sessionID without sending a daemon cancel. Call when a
|
|
981
|
+
* query completes normally (success or local failure) so the next turn can
|
|
982
|
+
* acquire.
|
|
983
|
+
*/
|
|
984
|
+
release(sessionID) {
|
|
985
|
+
this.active.delete(sessionID);
|
|
986
|
+
}
|
|
987
|
+
/** Reports whether a query is in flight for sessionID. */
|
|
988
|
+
isActive(sessionID) {
|
|
989
|
+
return this.active.has(sessionID);
|
|
990
|
+
}
|
|
991
|
+
};
|
|
992
|
+
|
|
993
|
+
// src/appkit/client.ts
|
|
994
|
+
function defaultClientFactory() {
|
|
995
|
+
return (url, config) => {
|
|
996
|
+
return new Client(url, config ?? defaultConfig());
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
function defaultBootstrapFunc() {
|
|
1000
|
+
return async (client, workspaceID, userID, config) => {
|
|
1001
|
+
const c = client;
|
|
1002
|
+
const opts = {
|
|
1003
|
+
client_workspace: workspaceID,
|
|
1004
|
+
user_id: userID,
|
|
1005
|
+
client_workspace_id: workspaceID
|
|
1006
|
+
};
|
|
1007
|
+
return bootstrapLoopSession(c, "", config, opts);
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// src/appkit/pool.ts
|
|
1012
|
+
var ErrPoolExhausted = class extends Error {
|
|
1013
|
+
constructor() {
|
|
1014
|
+
super("appkit: connection pool exhausted");
|
|
1015
|
+
this.name = "ErrPoolExhausted";
|
|
1016
|
+
}
|
|
1017
|
+
};
|
|
1018
|
+
function defaultPoolConfig() {
|
|
1019
|
+
return {
|
|
1020
|
+
poolSize: 1e3,
|
|
1021
|
+
queryTimeout: 30 * 60 * 1e3,
|
|
1022
|
+
connectionTimeout: 3e4,
|
|
1023
|
+
maxIdleTime: 10 * 60 * 1e3,
|
|
1024
|
+
healthCheckInterval: 3e4
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
var PooledConn = class {
|
|
1028
|
+
slotID;
|
|
1029
|
+
client;
|
|
1030
|
+
eventStream = null;
|
|
1031
|
+
streamController = null;
|
|
1032
|
+
sessionID = "";
|
|
1033
|
+
loopID = "";
|
|
1034
|
+
workspaceID = "";
|
|
1035
|
+
lastUsed = 0;
|
|
1036
|
+
constructor(slotID, client) {
|
|
1037
|
+
this.slotID = slotID;
|
|
1038
|
+
this.client = client;
|
|
1039
|
+
}
|
|
1040
|
+
/** Reports whether the underlying client signalled a drop. */
|
|
1041
|
+
isDisconnected() {
|
|
1042
|
+
return this.client.isDisconnected();
|
|
1043
|
+
}
|
|
1044
|
+
isConnected() {
|
|
1045
|
+
return this.client.isConnected() && !this.isDisconnected();
|
|
1046
|
+
}
|
|
1047
|
+
getLoopID() {
|
|
1048
|
+
return this.loopID;
|
|
1049
|
+
}
|
|
1050
|
+
};
|
|
1051
|
+
var ConnectionPool = class {
|
|
1052
|
+
cfg;
|
|
1053
|
+
scfg;
|
|
1054
|
+
factory;
|
|
1055
|
+
bootstrap;
|
|
1056
|
+
store;
|
|
1057
|
+
pool = [];
|
|
1058
|
+
activeSlots = /* @__PURE__ */ new Map();
|
|
1059
|
+
nextSlotID = 1;
|
|
1060
|
+
url;
|
|
1061
|
+
/**
|
|
1062
|
+
* Constructs a pool. `url` is the daemon WebSocket URL. If cfg is null,
|
|
1063
|
+
* defaultPoolConfig is used; if scfg is null, defaultConfig is used; nil
|
|
1064
|
+
* factory/bootstrap fall back to the defaults.
|
|
1065
|
+
*/
|
|
1066
|
+
constructor(url, store, cfg, scfg, factory) {
|
|
1067
|
+
this.cfg = cfg ?? defaultPoolConfig();
|
|
1068
|
+
this.scfg = scfg ?? defaultConfig();
|
|
1069
|
+
this.factory = factory ?? defaultClientFactory();
|
|
1070
|
+
this.bootstrap = defaultBootstrapFunc();
|
|
1071
|
+
this.store = store;
|
|
1072
|
+
this.url = url;
|
|
1073
|
+
for (let i = 0; i < this.cfg.poolSize; i++) {
|
|
1074
|
+
this.pool.push(new PooledConn(this.nextSlotID++, this.factory(url, this.scfg)));
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
/** Overrides the loop bootstrap function (useful for test fakes). */
|
|
1078
|
+
withBootstrap(f) {
|
|
1079
|
+
if (f) this.bootstrap = f;
|
|
1080
|
+
return this;
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* Returns a live connection for sessionID, reusing an active slot or
|
|
1084
|
+
* bootstrapping/reattaching as needed. The caller must call `release()`
|
|
1085
|
+
* when done with the connection (a turn completes or the session is reset).
|
|
1086
|
+
*/
|
|
1087
|
+
async acquire(sessionID, workspaceID, userID, _signal) {
|
|
1088
|
+
const existing = this.activeSlots.get(sessionID);
|
|
1089
|
+
if (existing) {
|
|
1090
|
+
if (existing.isDisconnected() || !existing.isConnected()) {
|
|
1091
|
+
await this.release(sessionID);
|
|
1092
|
+
} else {
|
|
1093
|
+
existing.lastUsed = Date.now();
|
|
1094
|
+
await this.store.updateLastUsed(sessionID).catch(() => {
|
|
1095
|
+
});
|
|
1096
|
+
return existing;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
const conn = this.pool.pop();
|
|
1100
|
+
if (!conn) throw new ErrPoolExhausted();
|
|
1101
|
+
this.activeSlots.set(sessionID, conn);
|
|
1102
|
+
const { loopID, ok } = await this.store.getLoopIDForSession(sessionID).catch(() => ({ loopID: "", ok: false }));
|
|
1103
|
+
let finalLoopID = "";
|
|
1104
|
+
try {
|
|
1105
|
+
if (!ok || !loopID) {
|
|
1106
|
+
await conn.client.connect();
|
|
1107
|
+
finalLoopID = await this.bootstrapNew(conn, workspaceID, userID);
|
|
1108
|
+
await this.store.createSession(workspaceID, sessionID, finalLoopID, "").catch(() => {
|
|
1109
|
+
});
|
|
1110
|
+
} else {
|
|
1111
|
+
try {
|
|
1112
|
+
await this.resumeAndReattach(conn, loopID);
|
|
1113
|
+
finalLoopID = loopID;
|
|
1114
|
+
} catch {
|
|
1115
|
+
await conn.client.connect();
|
|
1116
|
+
finalLoopID = await this.bootstrapNew(conn, workspaceID, userID);
|
|
1117
|
+
await this.store.createSession(workspaceID, sessionID, finalLoopID, "").catch(() => {
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
} catch (err) {
|
|
1122
|
+
await this.release(sessionID);
|
|
1123
|
+
throw err;
|
|
1124
|
+
}
|
|
1125
|
+
conn.sessionID = sessionID;
|
|
1126
|
+
conn.loopID = finalLoopID;
|
|
1127
|
+
conn.workspaceID = workspaceID;
|
|
1128
|
+
conn.lastUsed = Date.now();
|
|
1129
|
+
await this.store.updateLastUsed(sessionID).catch(() => {
|
|
1130
|
+
});
|
|
1131
|
+
return conn;
|
|
1132
|
+
}
|
|
1133
|
+
/** Tears down the connection for sessionID and returns the slot. */
|
|
1134
|
+
async release(sessionID) {
|
|
1135
|
+
const conn = this.activeSlots.get(sessionID);
|
|
1136
|
+
if (!conn) return;
|
|
1137
|
+
this.activeSlots.delete(sessionID);
|
|
1138
|
+
if (conn.streamController) {
|
|
1139
|
+
conn.streamController.abort();
|
|
1140
|
+
conn.streamController = null;
|
|
1141
|
+
}
|
|
1142
|
+
try {
|
|
1143
|
+
conn.client.close();
|
|
1144
|
+
} catch {
|
|
1145
|
+
}
|
|
1146
|
+
conn.sessionID = "";
|
|
1147
|
+
conn.loopID = "";
|
|
1148
|
+
conn.eventStream = null;
|
|
1149
|
+
this.pool.push(new PooledConn(this.nextSlotID++, this.factory(this.url, this.scfg)));
|
|
1150
|
+
}
|
|
1151
|
+
/**
|
|
1152
|
+
* Tears down the connection for sessionID so the next acquire bootstraps
|
|
1153
|
+
* fresh. The store should archive the loop id so getLoopIDForSession returns
|
|
1154
|
+
* false next time.
|
|
1155
|
+
*/
|
|
1156
|
+
async resetSession(sessionID) {
|
|
1157
|
+
await this.release(sessionID);
|
|
1158
|
+
}
|
|
1159
|
+
/** Gracefully shuts down all active connections. */
|
|
1160
|
+
stop() {
|
|
1161
|
+
for (const [sid, conn] of this.activeSlots) {
|
|
1162
|
+
if (conn.streamController) conn.streamController.abort();
|
|
1163
|
+
try {
|
|
1164
|
+
conn.client.close();
|
|
1165
|
+
} catch {
|
|
1166
|
+
}
|
|
1167
|
+
this.activeSlots.delete(sid);
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
/** Stats snapshot for observability. */
|
|
1171
|
+
stats() {
|
|
1172
|
+
return { active: this.activeSlots.size, idle: this.pool.length };
|
|
1173
|
+
}
|
|
1174
|
+
/** Bootstrap a fresh loop and start the reader. */
|
|
1175
|
+
async bootstrapNew(conn, workspaceID, userID) {
|
|
1176
|
+
const loopID = await this.bootstrap(conn.client, workspaceID, userID, this.scfg);
|
|
1177
|
+
this.startReader(conn);
|
|
1178
|
+
return loopID;
|
|
1179
|
+
}
|
|
1180
|
+
/** Reconnect + reattach an existing loop, then start the reader. */
|
|
1181
|
+
async resumeAndReattach(conn, loopID) {
|
|
1182
|
+
await conn.client.connect();
|
|
1183
|
+
try {
|
|
1184
|
+
await conn.client.reattachAndProbe(loopID);
|
|
1185
|
+
} catch (err) {
|
|
1186
|
+
if (err instanceof StaleLoopError) throw err;
|
|
1187
|
+
throw err;
|
|
1188
|
+
}
|
|
1189
|
+
this.startReader(conn);
|
|
1190
|
+
}
|
|
1191
|
+
/** Starts a receiveMessages generator and stores the stream + controller. */
|
|
1192
|
+
startReader(conn) {
|
|
1193
|
+
const controller = new AbortController();
|
|
1194
|
+
conn.streamController = controller;
|
|
1195
|
+
conn.eventStream = conn.client.receiveMessages(controller.signal);
|
|
1196
|
+
}
|
|
1197
|
+
};
|
|
1198
|
+
|
|
1199
|
+
// src/appkit/turn_runner.ts
|
|
1200
|
+
var ErrQueryTimeout = class extends Error {
|
|
1201
|
+
constructor() {
|
|
1202
|
+
super("appkit: query timeout");
|
|
1203
|
+
this.name = "ErrQueryTimeout";
|
|
1204
|
+
}
|
|
1205
|
+
};
|
|
1206
|
+
function inputMessageForLoop(text, loopID, attachments, opts) {
|
|
1207
|
+
const msg = { type: "loop_input", content: text };
|
|
1208
|
+
if (loopID) msg.loop_id = loopID;
|
|
1209
|
+
if (attachments && attachments.length > 0) msg.attachments = attachments;
|
|
1210
|
+
if (opts) {
|
|
1211
|
+
if (opts.intentHint?.trim()) {
|
|
1212
|
+
const hintError = validateLoopInputIntentHint(opts.intentHint);
|
|
1213
|
+
if (hintError) {
|
|
1214
|
+
throw new Error(hintError);
|
|
1215
|
+
}
|
|
1216
|
+
msg.intent_hint = opts.intentHint.trim();
|
|
1217
|
+
}
|
|
1218
|
+
if (opts.preferredSubagent?.trim()) msg.preferred_subagent = opts.preferredSubagent.trim();
|
|
1219
|
+
if (opts.responseSchema && Object.keys(opts.responseSchema).length > 0) {
|
|
1220
|
+
msg.response_schema = opts.responseSchema;
|
|
1221
|
+
}
|
|
1222
|
+
if (opts.responseSchemaName?.trim()) msg.response_schema_name = opts.responseSchemaName.trim();
|
|
1223
|
+
if (opts.responseSchemaStrict !== void 0)
|
|
1224
|
+
msg.response_schema_strict = opts.responseSchemaStrict;
|
|
1225
|
+
}
|
|
1226
|
+
return msg;
|
|
325
1227
|
}
|
|
1228
|
+
var TurnRunner = class {
|
|
1229
|
+
pool;
|
|
1230
|
+
gate;
|
|
1231
|
+
classifier;
|
|
1232
|
+
store;
|
|
1233
|
+
broadcaster;
|
|
1234
|
+
cfg;
|
|
1235
|
+
buildInput = inputMessageForLoop;
|
|
1236
|
+
onComplete = null;
|
|
1237
|
+
onError = null;
|
|
1238
|
+
/**
|
|
1239
|
+
* Constructs a TurnRunner. pool, gate, classifier, and store are required;
|
|
1240
|
+
* broadcaster may be null.
|
|
1241
|
+
*/
|
|
1242
|
+
constructor(pool, gate, classifier, store, broadcaster, cfg) {
|
|
1243
|
+
this.pool = pool;
|
|
1244
|
+
this.gate = gate;
|
|
1245
|
+
this.classifier = classifier;
|
|
1246
|
+
this.store = store;
|
|
1247
|
+
this.broadcaster = broadcaster;
|
|
1248
|
+
this.cfg = { queryTimeout: cfg.queryTimeout > 0 ? cfg.queryTimeout : 30 * 60 * 1e3 };
|
|
1249
|
+
}
|
|
1250
|
+
/** Overrides the loop_input payload builder. */
|
|
1251
|
+
withInputBuilder(f) {
|
|
1252
|
+
if (f) this.buildInput = f;
|
|
1253
|
+
return this;
|
|
1254
|
+
}
|
|
1255
|
+
/** Sets a completion hook (runs inline on success). */
|
|
1256
|
+
withOnComplete(f) {
|
|
1257
|
+
this.onComplete = f;
|
|
1258
|
+
return this;
|
|
1259
|
+
}
|
|
1260
|
+
/** Sets an error hook (runs inline on failure). */
|
|
1261
|
+
withOnError(f) {
|
|
1262
|
+
this.onError = f;
|
|
1263
|
+
return this;
|
|
1264
|
+
}
|
|
1265
|
+
/**
|
|
1266
|
+
* Runs one query turn. The response is broadcast via the SSE broadcaster and
|
|
1267
|
+
* persisted via the SessionStore; it is not returned to the caller (SSE
|
|
1268
|
+
* subscribers receive it). Resolves on success; rejects on failure
|
|
1269
|
+
* (ErrQueryTimeout, AbortError, or a daemon/processing error).
|
|
1270
|
+
*/
|
|
1271
|
+
async execute(sessionID, message, userID, workspaceID, attachments, opts, signal) {
|
|
1272
|
+
let conn;
|
|
1273
|
+
try {
|
|
1274
|
+
conn = await this.pool.acquire(sessionID, workspaceID, userID, signal);
|
|
1275
|
+
} catch (err) {
|
|
1276
|
+
await this.persistFailed(sessionID, "", err);
|
|
1277
|
+
this.broadcastError(sessionID, err);
|
|
1278
|
+
this.onError?.(sessionID, "", err);
|
|
1279
|
+
throw err;
|
|
1280
|
+
}
|
|
1281
|
+
const loopID = conn.getLoopID();
|
|
1282
|
+
const timeoutController = new AbortController();
|
|
1283
|
+
const timeoutMs = this.cfg.queryTimeout;
|
|
1284
|
+
const timer = setTimeout(() => timeoutController.abort(), timeoutMs);
|
|
1285
|
+
const sendCancel = async (detachedSignal) => {
|
|
1286
|
+
await this.sendLoopCancel(detachedSignal, conn, loopID);
|
|
1287
|
+
};
|
|
1288
|
+
try {
|
|
1289
|
+
this.gate.acquire(sessionID, timeoutController, sendCancel);
|
|
1290
|
+
} catch (err) {
|
|
1291
|
+
clearTimeout(timer);
|
|
1292
|
+
await this.pool.release(sessionID);
|
|
1293
|
+
await this.persistFailed(sessionID, loopID, err);
|
|
1294
|
+
this.broadcastError(sessionID, err);
|
|
1295
|
+
this.onError?.(sessionID, loopID, err);
|
|
1296
|
+
throw err;
|
|
1297
|
+
}
|
|
1298
|
+
try {
|
|
1299
|
+
const inputMsg = this.buildInput(
|
|
1300
|
+
message,
|
|
1301
|
+
loopID,
|
|
1302
|
+
attachments ?? void 0,
|
|
1303
|
+
opts ?? void 0
|
|
1304
|
+
);
|
|
1305
|
+
try {
|
|
1306
|
+
await conn.client.sendMessage(inputMsg);
|
|
1307
|
+
} catch (err) {
|
|
1308
|
+
await this.persistFailed(sessionID, loopID, err);
|
|
1309
|
+
this.broadcastError(sessionID, err);
|
|
1310
|
+
this.onError?.(sessionID, loopID, err);
|
|
1311
|
+
throw err;
|
|
1312
|
+
}
|
|
1313
|
+
const eventStream = conn.eventStream;
|
|
1314
|
+
if (!eventStream) {
|
|
1315
|
+
const err = new Error(`missing event stream for session ${sessionID} (loop ${loopID})`);
|
|
1316
|
+
await this.persistFailed(sessionID, loopID, err);
|
|
1317
|
+
this.broadcastError(sessionID, err);
|
|
1318
|
+
this.onError?.(sessionID, loopID, err);
|
|
1319
|
+
throw err;
|
|
1320
|
+
}
|
|
1321
|
+
let assistantContent = "";
|
|
1322
|
+
const startedAt = Date.now();
|
|
1323
|
+
const abortRace = new Promise((resolve) => {
|
|
1324
|
+
const onTimeout = () => resolve("timeout");
|
|
1325
|
+
timeoutController.signal.addEventListener("abort", onTimeout, { once: true });
|
|
1326
|
+
if (signal) {
|
|
1327
|
+
const onCaller = () => resolve("caller");
|
|
1328
|
+
signal.addEventListener("abort", onCaller, { once: true });
|
|
1329
|
+
}
|
|
1330
|
+
});
|
|
1331
|
+
const iterator = eventStream[Symbol.asyncIterator]();
|
|
1332
|
+
while (true) {
|
|
1333
|
+
const next = iterator.next();
|
|
1334
|
+
const raced = await Promise.race([
|
|
1335
|
+
next.then((res2) => ({ tag: "msg", res: res2 })),
|
|
1336
|
+
abortRace.then((tag) => ({ tag }))
|
|
1337
|
+
]);
|
|
1338
|
+
if ("tag" in raced && raced.tag !== "msg") {
|
|
1339
|
+
if (raced.tag === "caller" || signal?.aborted) {
|
|
1340
|
+
const err = new Error("aborted");
|
|
1341
|
+
await this.persistFailed(sessionID, loopID, err);
|
|
1342
|
+
this.broadcastError(sessionID, err);
|
|
1343
|
+
this.onError?.(sessionID, loopID, err);
|
|
1344
|
+
throw err;
|
|
1345
|
+
}
|
|
1346
|
+
await this.sendLoopCancel(new AbortController().signal, conn, loopID).catch(() => {
|
|
1347
|
+
});
|
|
1348
|
+
await this.persistFailed(sessionID, loopID, new ErrQueryTimeout());
|
|
1349
|
+
this.broadcastError(sessionID, new ErrQueryTimeout());
|
|
1350
|
+
this.onError?.(sessionID, loopID, new ErrQueryTimeout());
|
|
1351
|
+
throw new ErrQueryTimeout();
|
|
1352
|
+
}
|
|
1353
|
+
const res = raced.res;
|
|
1354
|
+
if (res.done) {
|
|
1355
|
+
const err = new Error("event stream closed");
|
|
1356
|
+
await this.persistFailed(sessionID, loopID, err);
|
|
1357
|
+
this.broadcastError(sessionID, err);
|
|
1358
|
+
this.onError?.(sessionID, loopID, err);
|
|
1359
|
+
throw err;
|
|
1360
|
+
}
|
|
1361
|
+
const msg = res.value;
|
|
1362
|
+
const eventResult = this.classifier.classify(msg, assistantContent);
|
|
1363
|
+
if (eventResult.err && eventResult.terminal === 2 /* FailedComplete */) {
|
|
1364
|
+
await this.persistFailed(sessionID, loopID, eventResult.err);
|
|
1365
|
+
this.broadcastError(sessionID, eventResult.err);
|
|
1366
|
+
this.onError?.(sessionID, loopID, eventResult.err);
|
|
1367
|
+
throw eventResult.err;
|
|
1368
|
+
}
|
|
1369
|
+
const step = (eventResult.thinkingStep ?? "").trim();
|
|
1370
|
+
if (step) this.broadcastThinkingStep(sessionID, step);
|
|
1371
|
+
if (eventResult.content) {
|
|
1372
|
+
if (eventResult.content.startsWith(assistantContent)) {
|
|
1373
|
+
assistantContent = eventResult.content;
|
|
1374
|
+
} else {
|
|
1375
|
+
assistantContent += eventResult.content;
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
const [final, deliverable] = this.classifier.resolveDeliverableFinalContent(
|
|
1379
|
+
eventResult,
|
|
1380
|
+
assistantContent
|
|
1381
|
+
);
|
|
1382
|
+
if (deliverable) {
|
|
1383
|
+
const elapsedMs = Date.now() - startedAt;
|
|
1384
|
+
await this.persistResponse(
|
|
1385
|
+
sessionID,
|
|
1386
|
+
loopID,
|
|
1387
|
+
final,
|
|
1388
|
+
startedAt,
|
|
1389
|
+
eventResult.completionEvent ?? ""
|
|
1390
|
+
);
|
|
1391
|
+
this.broadcastComplete(sessionID, final);
|
|
1392
|
+
this.onComplete?.(sessionID, loopID, final, eventResult.completionEvent ?? "", elapsedMs);
|
|
1393
|
+
return;
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
} finally {
|
|
1397
|
+
clearTimeout(timer);
|
|
1398
|
+
this.gate.release(sessionID);
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
/** Asks the daemon to cooperatively stop the loop runner on a detached signal. */
|
|
1402
|
+
async sendLoopCancel(_signal, conn, loopID) {
|
|
1403
|
+
const lid = (loopID ?? "").trim();
|
|
1404
|
+
if (!conn || !lid) return;
|
|
1405
|
+
const cancelMsg = { type: "command_request", command: "cancel", loop_id: lid };
|
|
1406
|
+
await conn.client.sendMessage(cancelMsg);
|
|
1407
|
+
}
|
|
1408
|
+
async persistResponse(sessionID, loopID, content, startedAt, completionEvent) {
|
|
1409
|
+
const msg = {
|
|
1410
|
+
role: "assistant",
|
|
1411
|
+
content,
|
|
1412
|
+
metadata: {
|
|
1413
|
+
started_at: startedAt,
|
|
1414
|
+
completed_at: Date.now(),
|
|
1415
|
+
duration_ms: Date.now() - startedAt,
|
|
1416
|
+
status: "completed",
|
|
1417
|
+
completion_event: completionEvent,
|
|
1418
|
+
deliverable: true
|
|
1419
|
+
}
|
|
1420
|
+
};
|
|
1421
|
+
await this.store.appendMessage(sessionID, msg).catch(() => {
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
async persistFailed(sessionID, _loopID, err) {
|
|
1425
|
+
const msg = {
|
|
1426
|
+
role: "error",
|
|
1427
|
+
content: err.message,
|
|
1428
|
+
metadata: { status: "failed", error_message: err.message }
|
|
1429
|
+
};
|
|
1430
|
+
await this.store.appendMessage(sessionID, msg).catch(() => {
|
|
1431
|
+
});
|
|
1432
|
+
}
|
|
1433
|
+
broadcastThinkingStep(sessionID, step) {
|
|
1434
|
+
if (!this.broadcaster) return;
|
|
1435
|
+
this.broadcaster.broadcast(sessionID, { type: "delta", data: step + "\n" });
|
|
1436
|
+
}
|
|
1437
|
+
broadcastComplete(sessionID, content) {
|
|
1438
|
+
this.broadcaster?.broadcast(sessionID, { type: "complete", data: content });
|
|
1439
|
+
}
|
|
1440
|
+
broadcastError(sessionID, err) {
|
|
1441
|
+
this.broadcaster?.broadcast(sessionID, { type: "query_error", data: err.message });
|
|
1442
|
+
}
|
|
1443
|
+
};
|
|
326
1444
|
export {
|
|
1445
|
+
CLIENT_VERSION,
|
|
1446
|
+
ChatEventTerminal,
|
|
327
1447
|
Client,
|
|
328
1448
|
ConnectionError,
|
|
1449
|
+
ConnectionPool,
|
|
1450
|
+
DEFAULT_CLIENT_CAPABILITIES,
|
|
1451
|
+
DEFAULT_DELIVERABLE_PHASES,
|
|
1452
|
+
DEFAULT_THINKING_STEP_EVENTS,
|
|
329
1453
|
DaemonError,
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
1454
|
+
DisconnectCause,
|
|
1455
|
+
ErrPoolExhausted,
|
|
1456
|
+
ErrQueryBusy,
|
|
1457
|
+
ErrQueryTimeout,
|
|
1458
|
+
EventAutopilotGoalCompleted,
|
|
1459
|
+
EventAutopilotGoalCreated,
|
|
1460
|
+
EventAutopilotGoalProgress,
|
|
1461
|
+
EventAutopilotGoalStatus,
|
|
1462
|
+
EventAutopilotWorkerAssigned,
|
|
1463
|
+
EventAutopilotWorkerUnassigned,
|
|
1464
|
+
EventCardCreated,
|
|
1465
|
+
EventCardReplayBegin,
|
|
1466
|
+
EventCardReplayEnd,
|
|
1467
|
+
EventClassifier,
|
|
335
1468
|
EventExploreCompleted,
|
|
336
1469
|
EventExploreMilestone,
|
|
337
1470
|
EventExploreStarted,
|
|
@@ -343,6 +1476,14 @@ export {
|
|
|
343
1476
|
EventMessageSent,
|
|
344
1477
|
EventPlanCreated,
|
|
345
1478
|
EventReplayComplete,
|
|
1479
|
+
EventStrangeLoopCompleted,
|
|
1480
|
+
EventStrangeLoopContextCompacted,
|
|
1481
|
+
EventStrangeLoopPlanDecision,
|
|
1482
|
+
EventStrangeLoopReasoned,
|
|
1483
|
+
EventStrangeLoopStarted,
|
|
1484
|
+
EventStrangeLoopStepCompleted,
|
|
1485
|
+
EventStrangeLoopStepQueued,
|
|
1486
|
+
EventStrangeLoopStepStarted,
|
|
346
1487
|
EventStreamToolCallUpdate,
|
|
347
1488
|
EventTacitusCompleted,
|
|
348
1489
|
EventTacitusGatherSummary,
|
|
@@ -351,18 +1492,42 @@ export {
|
|
|
351
1492
|
EventToolCompleted,
|
|
352
1493
|
EventToolError,
|
|
353
1494
|
EventToolStarted,
|
|
1495
|
+
INTENT_HINT_EMBED,
|
|
1496
|
+
INTENT_HINT_IMAGE_TO_TEXT,
|
|
1497
|
+
INTENT_HINT_OCR,
|
|
1498
|
+
INTENT_HINT_TEXT_COMPLETION,
|
|
1499
|
+
LOOP_ASSISTANT_OUTPUT_PHASES,
|
|
1500
|
+
Multiplexer,
|
|
1501
|
+
PROTO_VERSION,
|
|
1502
|
+
PooledConn,
|
|
1503
|
+
QueryGate,
|
|
1504
|
+
REMOVED_INTENT_HINTS,
|
|
1505
|
+
ReconnectError,
|
|
1506
|
+
SSEBroadcaster,
|
|
1507
|
+
StaleLoopError,
|
|
354
1508
|
TimeoutError,
|
|
1509
|
+
TurnRunner,
|
|
355
1510
|
VerbosityTier,
|
|
1511
|
+
authenticate,
|
|
356
1512
|
bootstrapLoopSession,
|
|
357
1513
|
checkDaemonStatus,
|
|
358
1514
|
classifyEventVerbosity,
|
|
359
1515
|
connectWithRetries,
|
|
1516
|
+
connectionInitEnvelope,
|
|
360
1517
|
decodeMessage,
|
|
1518
|
+
defaultBootstrapFunc,
|
|
1519
|
+
defaultClientFactory,
|
|
361
1520
|
defaultConfig,
|
|
1521
|
+
defaultPoolConfig,
|
|
1522
|
+
disconnectCauseName,
|
|
1523
|
+
disconnectEnvelope,
|
|
362
1524
|
encodeMessage,
|
|
363
1525
|
extractSootheLoopID,
|
|
1526
|
+
extractThinkingStep,
|
|
364
1527
|
fetchConfigSection,
|
|
1528
|
+
fetchLoopHistory,
|
|
365
1529
|
fetchSkillsCatalog,
|
|
1530
|
+
inputMessageForLoop,
|
|
366
1531
|
isCompletionEvent,
|
|
367
1532
|
isDaemonLive,
|
|
368
1533
|
isSubagentProgressEvent,
|
|
@@ -372,10 +1537,19 @@ export {
|
|
|
372
1537
|
newLoopNewMessage,
|
|
373
1538
|
newLoopSubscribeMessage,
|
|
374
1539
|
newRequestID,
|
|
1540
|
+
notificationEnvelope,
|
|
375
1541
|
parseNamespace,
|
|
1542
|
+
pingEnvelope,
|
|
1543
|
+
pongEnvelope,
|
|
1544
|
+
refreshAuthToken,
|
|
1545
|
+
requestDaemonConfigReload,
|
|
376
1546
|
requestDaemonShutdown,
|
|
1547
|
+
requestEnvelope,
|
|
377
1548
|
shouldShow,
|
|
378
1549
|
splitWirePayload,
|
|
1550
|
+
subscribeEnvelope,
|
|
1551
|
+
unsubscribeEnvelope,
|
|
1552
|
+
validateLoopInputIntentHint,
|
|
379
1553
|
waitDaemonReady,
|
|
380
1554
|
waitLoopStatusWithID,
|
|
381
1555
|
waitSubscriptionConfirmed
|