@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/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
- splitWirePayload
13
- } from "./chunk-OMAC7LA7.js";
14
-
15
- // src/errors.ts
16
- var ConnectionError = class extends Error {
17
- url;
18
- attempt;
19
- cause;
20
- constructor(url, attempt, cause) {
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 EventAgentLoopStarted = "soothe.cognition.agent_loop.started";
92
- var EventAgentLoopIterated = "soothe.cognition.agent_loop.iterated";
93
- var EventAgentLoopCompleted = "soothe.cognition.agent_loop.completed";
94
- var EventAgentLoopReasoned = "soothe.cognition.agent_loop.reasoned";
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({ type: "daemon_status" }, "daemon_status_response", timeout ?? 5e3);
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-QS23U6WX.js");
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({ type: "daemon_shutdown" }, "shutdown_ack", timeout ?? 1e4);
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({ type: "skills_list" }, "skills_list_response", timeout ?? 15e3);
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({ type: "config_get", section }, "config_get_response", timeout ?? 5e3);
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
- newLoopNewMessage(loopNew),
239
- "loop_new_response",
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("loop_new_response missing loop_id");
277
+ throw new Error("loop_new response missing loop_id");
245
278
  }
246
279
  }
247
- const subResp = await client.requestResponse(
248
- { type: "loop_subscribe", loop_id: loopId, verbosity: cfg.verbosityLevel },
249
- "loop_subscribe_response",
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 === "daemon_ready") {
265
- if (ev.state === "ready") return;
266
- throw new Error(
267
- `daemon not ready: state=${JSON.stringify(ev.state)} message=${JSON.stringify(ev.message ?? "")}`
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 daemon_ready (state=ready)`);
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 errResp = ev;
282
- throw new Error(`daemon error: ${errResp.code}: ${errResp.message}`);
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 status = ev;
286
- const lid = status.loop_id;
316
+ const lid = ev.loop_id;
287
317
  if (lid && lid !== "") {
288
- return status;
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 === "loop_subscribe_response" && ev.success === true) {
302
- if (String(ev.loop_id ?? "") === wantLoopID) return;
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 === "subscription_confirmed") {
305
- const lid = String(ev.loop_id ?? "");
306
- if (lid === wantLoopID) return;
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 subscription_confirmed`);
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(`failed to connect after ${retries} attempts: ${lastErr?.message ?? "unknown 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
- ESSENTIAL_EVENT_TYPES,
331
- EventAgentLoopCompleted,
332
- EventAgentLoopIterated,
333
- EventAgentLoopReasoned,
334
- EventAgentLoopStarted,
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