@wrongstack/core 0.2.0 → 0.3.2

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.
Files changed (57) hide show
  1. package/dist/{agent-bridge-DmBiCipY.d.ts → agent-bridge-C3DUGjSb.d.ts} +1 -1
  2. package/dist/{compactor-DSl2FK7a.d.ts → compactor-DpJBI1YH.d.ts} +8 -2
  3. package/dist/{config-DXrqb41m.d.ts → config-D2qvAxVd.d.ts} +39 -2
  4. package/dist/{context-u0bryklF.d.ts → context-IovtuTf8.d.ts} +2 -0
  5. package/dist/coordination/index.d.ts +11 -11
  6. package/dist/coordination/index.js +307 -245
  7. package/dist/coordination/index.js.map +1 -1
  8. package/dist/defaults/index.d.ts +30 -15
  9. package/dist/defaults/index.js +1077 -479
  10. package/dist/defaults/index.js.map +1 -1
  11. package/dist/{events-B6Q03pTu.d.ts → events-BHIQs4o1.d.ts} +34 -1
  12. package/dist/execution/index.d.ts +17 -14
  13. package/dist/execution/index.js +166 -18
  14. package/dist/execution/index.js.map +1 -1
  15. package/dist/extension/index.d.ts +9 -0
  16. package/dist/extension/index.js +241 -0
  17. package/dist/extension/index.js.map +1 -0
  18. package/dist/{plugin-CoYYZKdn.d.ts → index-hWNybrNZ.d.ts} +368 -11
  19. package/dist/index.d.ts +76 -26
  20. package/dist/index.js +1595 -748
  21. package/dist/index.js.map +1 -1
  22. package/dist/infrastructure/index.d.ts +6 -6
  23. package/dist/infrastructure/index.js +191 -20
  24. package/dist/infrastructure/index.js.map +1 -1
  25. package/dist/kernel/index.d.ts +12 -9
  26. package/dist/kernel/index.js +73 -7
  27. package/dist/kernel/index.js.map +1 -1
  28. package/dist/{mcp-servers-BA1Ofmfj.d.ts → mcp-servers-C2OopXOn.d.ts} +21 -5
  29. package/dist/models/index.d.ts +2 -2
  30. package/dist/models/index.js +24 -1
  31. package/dist/models/index.js.map +1 -1
  32. package/dist/{multi-agent-BDfkxL5C.d.ts → multi-agent-B9a6sflH.d.ts} +2 -2
  33. package/dist/observability/index.d.ts +2 -2
  34. package/dist/{path-resolver-Crkt8wTQ.d.ts → path-resolver--59rCou3.d.ts} +2 -2
  35. package/dist/provider-runner-B39miKRw.d.ts +36 -0
  36. package/dist/sdd/index.d.ts +3 -3
  37. package/dist/{secret-scrubber-3TLUkiCV.d.ts → secret-scrubber-CgG2tV2B.d.ts} +1 -1
  38. package/dist/{secret-scrubber-CwYliRWd.d.ts → secret-scrubber-Cuy5afaQ.d.ts} +1 -1
  39. package/dist/security/index.d.ts +3 -3
  40. package/dist/security/index.js +24 -1
  41. package/dist/security/index.js.map +1 -1
  42. package/dist/{selector-BRqzvugb.d.ts → selector-wT2fv9Fg.d.ts} +1 -1
  43. package/dist/{session-reader-C3x96CDR.d.ts → session-reader-CcPi4BQ8.d.ts} +1 -1
  44. package/dist/{skill-Bx8jxznf.d.ts → skill-C_7znCIC.d.ts} +2 -2
  45. package/dist/storage/index.d.ts +7 -6
  46. package/dist/storage/index.js +204 -14
  47. package/dist/storage/index.js.map +1 -1
  48. package/dist/{renderer-0A2ZEtca.d.ts → system-prompt-Dk1qm8ey.d.ts} +30 -2
  49. package/dist/{tool-executor-CYdZdtno.d.ts → tool-executor-HsBLGRaA.d.ts} +5 -5
  50. package/dist/types/index.d.ts +16 -16
  51. package/dist/types/index.js +230 -10
  52. package/dist/types/index.js.map +1 -1
  53. package/dist/utils/index.d.ts +23 -2
  54. package/dist/utils/index.js +117 -2
  55. package/dist/utils/index.js.map +1 -1
  56. package/package.json +5 -1
  57. package/dist/system-prompt-CG9jU5-5.d.ts +0 -31
package/dist/index.js CHANGED
@@ -395,6 +395,7 @@ var Pipeline = class {
395
395
  // src/kernel/events.ts
396
396
  var EventBus = class {
397
397
  listeners = /* @__PURE__ */ new Map();
398
+ wildcards = [];
398
399
  logger;
399
400
  setLogger(logger) {
400
401
  this.logger = logger;
@@ -421,24 +422,73 @@ var EventBus = class {
421
422
  this.off(event, wrapper);
422
423
  };
423
424
  }
425
+ /**
426
+ * Subscribe to all events whose name matches a glob-style prefix.
427
+ * `'tool.*'` matches `tool.started`, `tool.executed`, `tool.progress`, etc.
428
+ * `'*'` matches every event.
429
+ *
430
+ * The handler receives `(eventName, payload)` with the event name as a
431
+ * string and the payload as `unknown`. Use for logging, debugging, or
432
+ * metrics collection across a family of events.
433
+ *
434
+ * Returns an unsubscribe function.
435
+ */
436
+ onPattern(pattern, fn) {
437
+ const match = makePatternMatcher(pattern);
438
+ const entry = { match, fn };
439
+ this.wildcards.push(entry);
440
+ return () => {
441
+ const idx = this.wildcards.indexOf(entry);
442
+ if (idx >= 0) this.wildcards.splice(idx, 1);
443
+ };
444
+ }
445
+ /**
446
+ * Subscribe to all events whose name matches a RegExp.
447
+ * More flexible than `onPattern` — use when you need regex features
448
+ * (alternation, character classes, capture groups).
449
+ *
450
+ * Returns an unsubscribe function.
451
+ */
452
+ onRegex(regex, fn) {
453
+ const entry = { match: (e) => regex.test(e), fn };
454
+ this.wildcards.push(entry);
455
+ return () => {
456
+ const idx = this.wildcards.indexOf(entry);
457
+ if (idx >= 0) this.wildcards.splice(idx, 1);
458
+ };
459
+ }
424
460
  emit(event, payload) {
425
461
  const set = this.listeners.get(event);
426
- if (!set) return;
427
- for (const fn of set) {
428
- try {
429
- fn(payload);
430
- } catch (err) {
431
- this.logger?.error(`EventBus listener for "${event}" threw`, err);
462
+ if (set) {
463
+ for (const fn of set) {
464
+ try {
465
+ fn(payload);
466
+ } catch (err) {
467
+ this.logger?.error(`EventBus listener for "${event}" threw`, err);
468
+ }
469
+ }
470
+ }
471
+ if (this.wildcards.length > 0) {
472
+ const name = event;
473
+ for (const { match, fn } of this.wildcards) {
474
+ if (!match(name)) continue;
475
+ try {
476
+ fn(name, payload);
477
+ } catch (err) {
478
+ this.logger?.error(`EventBus wildcard listener for "${name}" threw`, err);
479
+ }
432
480
  }
433
481
  }
434
482
  }
435
483
  clear() {
436
484
  this.listeners.clear();
485
+ this.wildcards.length = 0;
437
486
  }
438
487
  /**
439
488
  * V2-D: introspection helper. Pass an `event` to count handlers for a
440
489
  * single key, or omit to get the total across every event. Used by the
441
490
  * leak-detection smoke test to flag handler accumulation across runs.
491
+ * Does NOT include wildcard listeners.
442
492
  */
443
493
  listenerCount(event) {
444
494
  if (event !== void 0) return this.listeners.get(event)?.size ?? 0;
@@ -446,7 +496,21 @@ var EventBus = class {
446
496
  for (const set of this.listeners.values()) total += set.size;
447
497
  return total;
448
498
  }
499
+ /**
500
+ * Number of wildcard listeners currently registered.
501
+ */
502
+ wildcardCount() {
503
+ return this.wildcards.length;
504
+ }
449
505
  };
506
+ function makePatternMatcher(pattern) {
507
+ if (pattern === "*") return () => true;
508
+ if (pattern.endsWith(".*")) {
509
+ const prefix = pattern.slice(0, -2);
510
+ return (e) => e.startsWith(`${prefix}.`);
511
+ }
512
+ return (e) => e === pattern;
513
+ }
450
514
 
451
515
  // src/kernel/tokens.ts
452
516
  var t = (name) => Symbol(name);
@@ -468,7 +532,9 @@ var TOKENS = {
468
532
  SystemPromptBuilder: t("SystemPromptBuilder"),
469
533
  SecretScrubber: t("SecretScrubber"),
470
534
  ModelsRegistry: t("ModelsRegistry"),
471
- ModeStore: t("ModeStore")
535
+ ModeStore: t("ModeStore"),
536
+ /** Replaces the entire provider call layer — retry, streaming, tracing. */
537
+ ProviderRunner: t("ProviderRunner")
472
538
  };
473
539
 
474
540
  // src/kernel/run-controller.ts
@@ -1100,6 +1166,98 @@ function estimateTextTokens(text) {
1100
1166
  return RoughTokenEstimate(text);
1101
1167
  }
1102
1168
 
1169
+ // src/utils/message-invariants.ts
1170
+ function repairToolUseAdjacency(messages) {
1171
+ const removedToolUses = [];
1172
+ const removedToolResults = [];
1173
+ let removedMessages = 0;
1174
+ let changed = false;
1175
+ const out = [];
1176
+ for (let i = 0; i < messages.length; i++) {
1177
+ const original = messages[i];
1178
+ let msg = original;
1179
+ if (hasToolUse(msg)) {
1180
+ const nextIds = toolResultIds(messages[i + 1]);
1181
+ const filtered = mapContent(msg, (blocks) => {
1182
+ const next = [];
1183
+ for (const block of blocks) {
1184
+ if (block.type === "tool_use" && !nextIds.has(block.id)) {
1185
+ removedToolUses.push(block.id);
1186
+ changed = true;
1187
+ continue;
1188
+ }
1189
+ next.push(block);
1190
+ }
1191
+ return next;
1192
+ });
1193
+ msg = filtered ?? msg;
1194
+ }
1195
+ if (hasToolResult(msg)) {
1196
+ const allowed = toolUseIds(out[out.length - 1]);
1197
+ const filtered = mapContent(msg, (blocks) => {
1198
+ const next = [];
1199
+ for (const block of blocks) {
1200
+ if (block.type === "tool_result" && !allowed.has(block.tool_use_id)) {
1201
+ removedToolResults.push(block.tool_use_id);
1202
+ changed = true;
1203
+ continue;
1204
+ }
1205
+ next.push(block);
1206
+ }
1207
+ return next;
1208
+ });
1209
+ msg = filtered ?? msg;
1210
+ }
1211
+ if (isEmptyMessage(msg)) {
1212
+ removedMessages++;
1213
+ changed = true;
1214
+ continue;
1215
+ }
1216
+ out.push(msg);
1217
+ }
1218
+ return {
1219
+ messages: changed ? out : messages,
1220
+ report: { changed, removedToolUses, removedToolResults, removedMessages }
1221
+ };
1222
+ }
1223
+ function hasToolUse(msg) {
1224
+ return contentBlocks(msg).some((b) => b.type === "tool_use");
1225
+ }
1226
+ function hasToolResult(msg) {
1227
+ return contentBlocks(msg).some((b) => b.type === "tool_result");
1228
+ }
1229
+ function toolUseIds(msg) {
1230
+ const ids = /* @__PURE__ */ new Set();
1231
+ if (!msg || msg.role !== "assistant") return ids;
1232
+ for (const block of contentBlocks(msg)) {
1233
+ if (block.type === "tool_use") ids.add(block.id);
1234
+ }
1235
+ return ids;
1236
+ }
1237
+ function toolResultIds(msg) {
1238
+ const ids = /* @__PURE__ */ new Set();
1239
+ if (!msg || msg.role !== "user") return ids;
1240
+ for (const block of contentBlocks(msg)) {
1241
+ if (block.type === "tool_result") ids.add(block.tool_use_id);
1242
+ }
1243
+ return ids;
1244
+ }
1245
+ function contentBlocks(msg) {
1246
+ return msg && Array.isArray(msg.content) ? msg.content : [];
1247
+ }
1248
+ function mapContent(msg, fn) {
1249
+ if (!Array.isArray(msg.content)) return msg;
1250
+ const next = fn(msg.content);
1251
+ if (next.length === msg.content.length && next.every((b, idx) => b === msg.content[idx])) {
1252
+ return msg;
1253
+ }
1254
+ return { ...msg, content: next };
1255
+ }
1256
+ function isEmptyMessage(msg) {
1257
+ if (typeof msg.content === "string") return msg.content.trim().length === 0;
1258
+ return msg.content.length === 0;
1259
+ }
1260
+
1103
1261
  // src/execution/compactor.ts
1104
1262
  var HybridCompactor = class {
1105
1263
  preserveK;
@@ -1113,20 +1271,36 @@ var HybridCompactor = class {
1113
1271
  async compact(ctx, opts = {}) {
1114
1272
  const beforeTokens = this.estimateMessages(ctx.messages);
1115
1273
  const reductions = [];
1116
- const phase1Saved = this.eliseOldToolResults(ctx);
1274
+ const policy = readContextWindowPolicy(ctx);
1275
+ const preserveK = policy?.preserveK ?? this.preserveK;
1276
+ const eliseThreshold = policy?.eliseThreshold ?? this.eliseThreshold;
1277
+ const phase1Saved = this.eliseOldToolResults(ctx, preserveK, eliseThreshold);
1117
1278
  if (phase1Saved > 0) reductions.push({ phase: "elision", saved: phase1Saved });
1118
1279
  if (opts.aggressive) {
1119
- const phase2Saved = this.collapseAncientTurns(ctx);
1280
+ const phase2Saved = this.collapseAncientTurns(ctx, preserveK);
1120
1281
  if (phase2Saved > 0) reductions.push({ phase: "summary", saved: phase2Saved });
1121
1282
  }
1283
+ const repaired = repairToolUseAdjacency(ctx.messages);
1284
+ if (repaired.report.changed) {
1285
+ ctx.state.replaceMessages(repaired.messages);
1286
+ }
1122
1287
  const afterTokens = this.estimateMessages(ctx.messages);
1123
- return { before: beforeTokens, after: afterTokens, reductions };
1288
+ return {
1289
+ before: beforeTokens,
1290
+ after: afterTokens,
1291
+ reductions,
1292
+ repaired: repaired.report.changed ? {
1293
+ removedToolUses: repaired.report.removedToolUses,
1294
+ removedToolResults: repaired.report.removedToolResults,
1295
+ removedMessages: repaired.report.removedMessages
1296
+ } : void 0
1297
+ };
1124
1298
  }
1125
- eliseOldToolResults(ctx) {
1299
+ eliseOldToolResults(ctx, preserveK = this.preserveK, eliseThreshold = this.eliseThreshold) {
1126
1300
  const messages = ctx.messages;
1127
1301
  let pairCount = 0;
1128
1302
  let preserveStart = messages.length;
1129
- for (let i = messages.length - 1; i >= 0 && pairCount < this.preserveK; i--) {
1303
+ for (let i = messages.length - 1; i >= 0 && pairCount < preserveK; i--) {
1130
1304
  const m = messages[i];
1131
1305
  if (!m) continue;
1132
1306
  if (m.role === "user" || m.role === "assistant") {
@@ -1150,7 +1324,7 @@ var HybridCompactor = class {
1150
1324
  const newContent = msg.content.map((b) => {
1151
1325
  if (b.type !== "tool_result") return b;
1152
1326
  const tokens = estimateToolResultTokens(b.content);
1153
- if (tokens < this.eliseThreshold) return b;
1327
+ if (tokens < eliseThreshold) return b;
1154
1328
  saved += tokens;
1155
1329
  const elided = {
1156
1330
  type: "tool_result",
@@ -1170,9 +1344,9 @@ var HybridCompactor = class {
1170
1344
  if (changed) ctx.state.replaceMessages(nextMessages);
1171
1345
  return saved;
1172
1346
  }
1173
- collapseAncientTurns(ctx) {
1347
+ collapseAncientTurns(ctx, preserveK = this.preserveK) {
1174
1348
  const messages = ctx.messages;
1175
- const cutTarget = Math.max(0, messages.length - this.preserveK * 2);
1349
+ const cutTarget = Math.max(0, messages.length - preserveK * 2);
1176
1350
  if (cutTarget <= 0) return 0;
1177
1351
  let boundary = -1;
1178
1352
  for (let i = cutTarget; i < messages.length; i++) {
@@ -1213,6 +1387,15 @@ var HybridCompactor = class {
1213
1387
  return total;
1214
1388
  }
1215
1389
  };
1390
+ function readContextWindowPolicy(ctx) {
1391
+ const policy = ctx.meta?.["contextWindowPolicy"];
1392
+ if (!policy || typeof policy !== "object") return null;
1393
+ const candidate = policy;
1394
+ if (typeof candidate.preserveK !== "number" || typeof candidate.eliseThreshold !== "number") {
1395
+ return null;
1396
+ }
1397
+ return candidate;
1398
+ }
1216
1399
  function hasTextContent(m) {
1217
1400
  if (typeof m.content === "string") return m.content.trim().length > 0;
1218
1401
  return m.content.some((b) => b.type === "text" && b.text.trim().length > 0);
@@ -1539,7 +1722,7 @@ async function atomicWrite(targetPath, content, opts = {}) {
1539
1722
  if (mode !== void 0) {
1540
1723
  await fsp2.chmod(tmp, mode);
1541
1724
  }
1542
- await fsp2.rename(tmp, targetPath);
1725
+ await renameWithRetry(tmp, targetPath);
1543
1726
  } catch (err) {
1544
1727
  try {
1545
1728
  await fsp2.unlink(tmp);
@@ -1551,6 +1734,29 @@ async function atomicWrite(targetPath, content, opts = {}) {
1551
1734
  async function ensureDir(dir) {
1552
1735
  await fsp2.mkdir(dir, { recursive: true });
1553
1736
  }
1737
+ var TRANSIENT_RENAME_CODES = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES", "ENOTEMPTY"]);
1738
+ async function renameWithRetry(from, to) {
1739
+ if (process.platform !== "win32") {
1740
+ await fsp2.rename(from, to);
1741
+ return;
1742
+ }
1743
+ const delays = [10, 25, 60, 120, 250];
1744
+ let lastErr;
1745
+ for (let i = 0; i <= delays.length; i++) {
1746
+ try {
1747
+ await fsp2.rename(from, to);
1748
+ return;
1749
+ } catch (err) {
1750
+ lastErr = err;
1751
+ const code = err?.code;
1752
+ if (!code || !TRANSIENT_RENAME_CODES.has(code) || i === delays.length) {
1753
+ throw err;
1754
+ }
1755
+ await new Promise((resolve4) => setTimeout(resolve4, delays[i]));
1756
+ }
1757
+ }
1758
+ throw lastErr;
1759
+ }
1554
1760
 
1555
1761
  // src/models/models-registry.ts
1556
1762
  var DEFAULT_URL = "https://models.dev/api.json";
@@ -1855,6 +2061,85 @@ When refactoring code:
1855
2061
  }
1856
2062
  ];
1857
2063
 
2064
+ // src/types/context-window.ts
2065
+ var DEFAULT_CONTEXT_WINDOW_MODE_ID = "balanced";
2066
+ var CONTEXT_WINDOW_MODES = Object.freeze([
2067
+ {
2068
+ id: "balanced",
2069
+ name: "Balanced",
2070
+ description: "Default rolling compaction: recent work stays verbatim, old tool output is trimmed.",
2071
+ thresholds: { warn: 0.6, soft: 0.75, hard: 0.9 },
2072
+ aggressiveOn: "soft",
2073
+ preserveK: 10,
2074
+ eliseThreshold: 2e3,
2075
+ targetLoad: 0.65
2076
+ },
2077
+ {
2078
+ id: "frugal",
2079
+ name: "Frugal",
2080
+ description: "Token-saver mode: compacts early and keeps a tighter verbatim tail.",
2081
+ thresholds: { warn: 0.45, soft: 0.6, hard: 0.75 },
2082
+ aggressiveOn: "warn",
2083
+ preserveK: 6,
2084
+ eliseThreshold: 700,
2085
+ targetLoad: 0.5
2086
+ },
2087
+ {
2088
+ id: "deep",
2089
+ name: "Deep",
2090
+ description: "Long-reasoning mode: delays compaction and keeps more recent turns intact.",
2091
+ thresholds: { warn: 0.72, soft: 0.86, hard: 0.96 },
2092
+ aggressiveOn: "hard",
2093
+ preserveK: 18,
2094
+ eliseThreshold: 5e3,
2095
+ targetLoad: 0.78
2096
+ },
2097
+ {
2098
+ id: "archival",
2099
+ name: "Archival",
2100
+ description: "Decision-preserving mode: compacts steadily while keeping summaries prominent.",
2101
+ thresholds: { warn: 0.55, soft: 0.7, hard: 0.84 },
2102
+ aggressiveOn: "soft",
2103
+ preserveK: 8,
2104
+ eliseThreshold: 1200,
2105
+ targetLoad: 0.58
2106
+ }
2107
+ ]);
2108
+ function listContextWindowModes() {
2109
+ return CONTEXT_WINDOW_MODES.map((m) => ({ ...m, thresholds: { ...m.thresholds } }));
2110
+ }
2111
+ function getContextWindowMode(id) {
2112
+ if (!id) return null;
2113
+ const mode = CONTEXT_WINDOW_MODES.find((m) => m.id === id);
2114
+ return mode ? { ...mode, thresholds: { ...mode.thresholds } } : null;
2115
+ }
2116
+ function isContextWindowModeId(id) {
2117
+ return CONTEXT_WINDOW_MODES.some((m) => m.id === id);
2118
+ }
2119
+ function resolveContextWindowPolicy(config = {}, overrideMode) {
2120
+ const requested = overrideMode ?? config.mode ?? DEFAULT_CONTEXT_WINDOW_MODE_ID;
2121
+ const mode = getContextWindowMode(requested) ?? getContextWindowMode(DEFAULT_CONTEXT_WINDOW_MODE_ID);
2122
+ if (mode.id !== DEFAULT_CONTEXT_WINDOW_MODE_ID) {
2123
+ return mode;
2124
+ }
2125
+ return {
2126
+ ...mode,
2127
+ thresholds: {
2128
+ warn: config.warnThreshold ?? mode.thresholds.warn,
2129
+ soft: config.softThreshold ?? mode.thresholds.soft,
2130
+ hard: config.hardThreshold ?? mode.thresholds.hard
2131
+ },
2132
+ preserveK: config.preserveK ?? mode.preserveK,
2133
+ eliseThreshold: config.eliseThreshold ?? mode.eliseThreshold
2134
+ };
2135
+ }
2136
+ function formatContextWindowModeList(activeId) {
2137
+ return CONTEXT_WINDOW_MODES.map((m) => {
2138
+ const marker = m.id === activeId ? "*" : " ";
2139
+ return `${marker} ${m.id.padEnd(9)} ${m.name} - ${m.description}`;
2140
+ }).join("\n");
2141
+ }
2142
+
1858
2143
  // src/coordination/in-memory-transport.ts
1859
2144
  var InMemoryBridgeTransport = class {
1860
2145
  subs = /* @__PURE__ */ new Map();
@@ -1974,6 +2259,7 @@ var InMemoryAgentBridge = class {
1974
2259
  this.stopped = true;
1975
2260
  for (const [, p] of this.pendingRequests) {
1976
2261
  clearTimeout(p.timer);
2262
+ p.reject(new Error("Bridge stopped"));
1977
2263
  }
1978
2264
  this.pendingRequests.clear();
1979
2265
  this.inflightGuards.clear();
@@ -3404,11 +3690,17 @@ var DefaultSessionStore = class {
3404
3690
  if (openToolUses.size > 0) {
3405
3691
  this.events?.emit("session.damaged", {
3406
3692
  sessionId,
3407
- detail: `${openToolUses.size} tool_use blocks without matching results \u2014 replay truncated`
3693
+ detail: `${openToolUses.size} tool_use blocks without matching results - replay repaired`
3694
+ });
3695
+ }
3696
+ const repaired = repairToolUseAdjacency(messages);
3697
+ if (repaired.report.changed) {
3698
+ this.events?.emit("session.damaged", {
3699
+ sessionId,
3700
+ detail: `Repaired replay adjacency: removed ${repaired.report.removedToolUses.length} tool_use, ${repaired.report.removedToolResults.length} tool_result, ${repaired.report.removedMessages} empty messages`
3408
3701
  });
3409
- return { messages, usage };
3410
3702
  }
3411
- return { messages, usage };
3703
+ return { messages: repaired.messages, usage };
3412
3704
  }
3413
3705
  };
3414
3706
  var FileSessionWriter = class {
@@ -3434,6 +3726,7 @@ var FileSessionWriter = class {
3434
3726
  startedAt;
3435
3727
  meta;
3436
3728
  closed = false;
3729
+ closing = false;
3437
3730
  manifestFile;
3438
3731
  summary;
3439
3732
  tokenIn = 0;
@@ -3449,9 +3742,7 @@ var FileSessionWriter = class {
3449
3742
  resumed;
3450
3743
  appendFailCount = 0;
3451
3744
  lastAppendWarnAt = 0;
3452
- async writeSessionStart() {
3453
- if (this.initDone || this.closed) return;
3454
- this.initDone = true;
3745
+ async writeSessionStartLazy() {
3455
3746
  const record = `${JSON.stringify({
3456
3747
  type: this.resumed ? "session_resumed" : "session_start",
3457
3748
  ts: this.startedAt,
@@ -3470,7 +3761,8 @@ var FileSessionWriter = class {
3470
3761
  async append(event) {
3471
3762
  if (this.closed) return;
3472
3763
  if (!this.initDone) {
3473
- await this.writeSessionStart();
3764
+ this.initDone = true;
3765
+ await this.writeSessionStartLazy();
3474
3766
  }
3475
3767
  this.observeForSummary(event);
3476
3768
  try {
@@ -3511,7 +3803,8 @@ var FileSessionWriter = class {
3511
3803
  }
3512
3804
  }
3513
3805
  async close() {
3514
- if (this.closed) return;
3806
+ if (this.closing) return;
3807
+ this.closing = true;
3515
3808
  this.closed = true;
3516
3809
  if (this.manifestFile) {
3517
3810
  try {
@@ -3913,9 +4206,52 @@ function deepFreeze(obj) {
3913
4206
  }
3914
4207
  return Object.freeze(obj);
3915
4208
  }
4209
+
4210
+ // src/security/config-secrets.ts
4211
+ function decryptConfigSecrets2(cfg, vault) {
4212
+ return walk3(cfg, vault, (v, key) => {
4213
+ try {
4214
+ return vault.decrypt(v);
4215
+ } catch (err) {
4216
+ console.warn(
4217
+ `[secret-vault] Failed to decrypt "${key}":`,
4218
+ err instanceof Error ? err.message : err
4219
+ );
4220
+ return "";
4221
+ }
4222
+ });
4223
+ }
4224
+ function walk3(node, vault, transform) {
4225
+ if (node === null || node === void 0) return node;
4226
+ if (typeof node !== "object") return node;
4227
+ if (Array.isArray(node)) {
4228
+ return node.map((item) => walk3(item, vault, transform));
4229
+ }
4230
+ const out = {};
4231
+ for (const [k, v] of Object.entries(node)) {
4232
+ if (typeof v === "string" && isSecretField2(k)) {
4233
+ out[k] = transform(v, k);
4234
+ } else if (typeof v === "object" && v !== null) {
4235
+ out[k] = walk3(v, vault, transform);
4236
+ } else {
4237
+ out[k] = v;
4238
+ }
4239
+ }
4240
+ return out;
4241
+ }
4242
+ var SECRET_KEY_PATTERN2 = /(?:apikey|api_key|authtoken|auth_token|bearer|secret|password|passwd|pwd|refreshtoken|refresh_token|sessionkey|session_key|access[_-]?token|private[_-]?key)/i;
4243
+ var NON_SECRET_OVERRIDES2 = /* @__PURE__ */ new Set(["publickey", "public_key"]);
4244
+ function isSecretField2(name) {
4245
+ const lc = name.toLowerCase();
4246
+ if (NON_SECRET_OVERRIDES2.has(lc)) return false;
4247
+ return SECRET_KEY_PATTERN2.test(lc);
4248
+ }
4249
+
4250
+ // src/storage/config-loader.ts
3916
4251
  var BEHAVIOR_DEFAULTS = {
3917
4252
  version: 1,
3918
4253
  context: {
4254
+ mode: DEFAULT_CONTEXT_WINDOW_MODE_ID,
3919
4255
  warnThreshold: 0.6,
3920
4256
  softThreshold: 0.75,
3921
4257
  hardThreshold: 0.9,
@@ -4030,7 +4366,7 @@ var DefaultConfigLoader = class {
4030
4366
  cfg = deepMerge2(cfg, opts.cliFlags);
4031
4367
  }
4032
4368
  if (this.vault) {
4033
- cfg = decryptConfigSecrets(cfg, this.vault);
4369
+ cfg = decryptConfigSecrets2(cfg, this.vault);
4034
4370
  }
4035
4371
  if (cfg.providers) {
4036
4372
  for (const pcfg of Object.values(cfg.providers)) {
@@ -4090,6 +4426,10 @@ var DefaultConfigLoader = class {
4090
4426
  if (c.warnThreshold >= c.softThreshold || c.softThreshold >= c.hardThreshold) {
4091
4427
  throw new Error("Config: context thresholds must satisfy warn < soft < hard");
4092
4428
  }
4429
+ if (c.mode !== void 0 && !isContextWindowModeId(c.mode)) {
4430
+ const known = listContextWindowModes().map((m) => m.id).join(", ");
4431
+ throw new Error(`Config: context.mode must be one of: ${known}`);
4432
+ }
4093
4433
  }
4094
4434
  validateIdentity(cfg) {
4095
4435
  if (!cfg.provider) {
@@ -4410,24 +4750,35 @@ async function saveTodosCheckpoint(filePath, sessionId, todos) {
4410
4750
  function attachTodosCheckpoint(state, filePath, sessionId) {
4411
4751
  let timer = null;
4412
4752
  let pending = null;
4753
+ let writeChain = Promise.resolve();
4754
+ const enqueueWrite = (todos) => {
4755
+ writeChain = writeChain.then(() => saveTodosCheckpoint(filePath, sessionId, todos));
4756
+ return writeChain;
4757
+ };
4413
4758
  const flush = () => {
4414
4759
  timer = null;
4415
4760
  if (pending) {
4416
- void saveTodosCheckpoint(filePath, sessionId, pending);
4761
+ const todos = pending;
4417
4762
  pending = null;
4763
+ return enqueueWrite(todos);
4418
4764
  }
4765
+ return writeChain;
4419
4766
  };
4420
4767
  const unsubscribe = state.onChange((change) => {
4421
4768
  if (change.kind !== "todos_replaced") return;
4422
4769
  pending = change.todos;
4423
4770
  if (timer) clearTimeout(timer);
4424
- timer = setTimeout(flush, 150);
4771
+ timer = setTimeout(() => {
4772
+ void flush();
4773
+ }, 150);
4425
4774
  });
4426
- return () => {
4775
+ return async () => {
4427
4776
  unsubscribe();
4428
4777
  if (timer) {
4429
4778
  clearTimeout(timer);
4430
- flush();
4779
+ await flush();
4780
+ } else {
4781
+ await writeChain;
4431
4782
  }
4432
4783
  };
4433
4784
  }
@@ -4889,112 +5240,419 @@ function parseDescription(raw) {
4889
5240
  return { trigger, scope };
4890
5241
  }
4891
5242
 
4892
- // src/execution/intelligent-compactor.ts
4893
- var IntelligentCompactor = class {
4894
- provider;
4895
- warnThreshold;
4896
- softThreshold;
4897
- hardThreshold;
4898
- maxContext;
4899
- preserveK;
4900
- eliseThreshold;
4901
- summarizerPrompt;
4902
- summarizerModel;
4903
- constructor(opts) {
4904
- this.provider = opts.provider;
4905
- this.warnThreshold = opts.warnThreshold ?? 0.6;
4906
- this.softThreshold = opts.softThreshold ?? 0.75;
4907
- this.hardThreshold = opts.hardThreshold ?? 0.9;
4908
- this.maxContext = opts.maxContext ?? 128e3;
4909
- this.preserveK = opts.preserveK ?? 4;
4910
- this.eliseThreshold = opts.eliseThreshold ?? 500;
4911
- this.summarizerPrompt = opts.summarizerPrompt ?? "You are a context summarizer. Given a list of conversation messages, produce a concise but complete summary that preserves all factual information, decisions made, and any state changes (e.g. file edits, todo updates). Do not add commentary. Output only the summary.";
4912
- this.summarizerModel = opts.summarizerModel;
4913
- }
4914
- async compact(ctx, opts = {}) {
4915
- const beforeTokens = this.estimateTokens(ctx.messages);
4916
- const reductions = [];
4917
- const load = beforeTokens / this.maxContext;
4918
- const aggressive = load >= this.hardThreshold ? true : opts.aggressive ?? load >= this.softThreshold;
4919
- const saved1 = this.eliseOldToolResults(ctx);
4920
- if (saved1 > 0) reductions.push({ phase: "elision", saved: saved1 });
4921
- if (aggressive) {
4922
- const saved2 = await this.summarizeAncientTurns(ctx);
4923
- if (saved2 > 0) reductions.push({ phase: "summary", saved: saved2 });
4924
- } else if (load >= this.warnThreshold) {
4925
- const saved2 = this.lightweightCompact(ctx);
4926
- if (saved2 > 0) reductions.push({ phase: "elision", saved: saved2 });
4927
- }
4928
- const afterTokens = this.estimateTokens(ctx.messages);
4929
- return { before: beforeTokens, after: afterTokens, reductions };
4930
- }
4931
- async summarizeAncientTurns(ctx) {
4932
- const messages = ctx.messages;
4933
- const cutoff = Math.max(0, messages.length - this.preserveK * 2);
4934
- if (cutoff <= 2) return 0;
4935
- const boundary = this.findSafeBoundary(messages, 0, cutoff);
4936
- if (boundary <= 1) return 0;
4937
- const toSummarize = messages.slice(0, boundary);
4938
- const removedTokens = this.estimateTokens(toSummarize);
4939
- let summaryText;
4940
- try {
4941
- summaryText = await this.callSummarizer(toSummarize, ctx);
4942
- } catch {
4943
- summaryText = `[${toSummarize.length} earlier turns omitted \u2014 key decisions and file states preserved in context]`;
4944
- }
4945
- const summaryMsg = {
4946
- role: "system",
4947
- content: `[prior_turns_summary: ${summaryText}]`
4948
- };
4949
- const summaryTokens = this.estimateTokens([summaryMsg]);
4950
- const tail = ctx.messages.slice(boundary);
4951
- ctx.state.replaceMessages([summaryMsg, ...tail]);
4952
- return Math.max(0, removedTokens - summaryTokens);
4953
- }
4954
- findSafeBoundary(messages, from, to) {
4955
- for (let i = to; i >= from; i--) {
4956
- const m = messages[i];
4957
- if (!m) continue;
4958
- if (m.role === "user" && this.hasTextContent(m)) {
4959
- return this.findExchangeStart(messages, i);
5243
+ // src/core/streaming-response-builder.ts
5244
+ function buildResponse(state) {
5245
+ const content = [];
5246
+ for (const b of state.blockOrder) {
5247
+ if (b.kind === "text") {
5248
+ const txt = state.textBuffers[b.idx] ?? "";
5249
+ if (txt) content.push({ type: "text", text: txt });
5250
+ } else if (b.kind === "thinking") {
5251
+ const t2 = state.thinking[b.idx];
5252
+ if (!t2) continue;
5253
+ if (!t2.textBuf && !t2.signature) continue;
5254
+ const block = { type: "thinking", thinking: t2.textBuf };
5255
+ if (t2.signature) block.signature = t2.signature;
5256
+ if (t2.providerMeta && Object.keys(t2.providerMeta).length > 0) {
5257
+ block.providerMeta = t2.providerMeta;
4960
5258
  }
4961
- }
4962
- return -1;
4963
- }
4964
- findExchangeStart(messages, userIndex) {
4965
- for (let i = userIndex - 1; i >= 0; i--) {
4966
- const m = messages[i];
4967
- if (!m) continue;
4968
- if (m.role === "assistant") {
4969
- const hasToolUse = Array.isArray(m.content) ? m.content.some((b) => b.type === "tool_use") : false;
4970
- if (!hasToolUse) {
4971
- return i + 1;
5259
+ content.push(block);
5260
+ } else {
5261
+ const tb = state.tools.get(b.id);
5262
+ if (tb) {
5263
+ const block = {
5264
+ type: "tool_use",
5265
+ id: b.id,
5266
+ name: tb.name,
5267
+ input: tb.input ?? {}
5268
+ };
5269
+ if (tb.providerMeta && Object.keys(tb.providerMeta).length > 0) {
5270
+ block.providerMeta = tb.providerMeta;
4972
5271
  }
4973
- } else if (m.role !== "user") ; else {
4974
- return i;
5272
+ content.push(block);
4975
5273
  }
4976
5274
  }
4977
- return 0;
4978
- }
4979
- async callSummarizer(messages, ctx) {
4980
- const prompt = [
4981
- { type: "text", text: this.summarizerPrompt },
4982
- { type: "text", text: "\n\nConversation to summarize:\n" },
4983
- ...this.messagesToText(messages)
4984
- ];
4985
- const req = {
4986
- model: this.summarizerModel ?? ctx.model,
4987
- system: prompt,
4988
- messages: [],
4989
- maxTokens: 1024
4990
- };
4991
- const ac = ctx.signal ? void 0 : new AbortController();
4992
- const signal = ctx.signal ?? ac.signal;
4993
- const res = await this.provider.complete(req, { signal });
4994
- const textBlocks = res.content.filter(isTextBlock);
4995
- return textBlocks.map((b) => b.text).join("\n").trim() || "(empty summary)";
4996
5275
  }
4997
- messagesToText(messages) {
5276
+ if (content.length === 0) content.push({ type: "text", text: "" });
5277
+ return { content, stopReason: state.stopReason, usage: state.usage, model: state.model };
5278
+ }
5279
+ function createStreamingState(model) {
5280
+ return {
5281
+ model,
5282
+ stopReason: "end_turn",
5283
+ usage: { input: 0, output: 0 },
5284
+ textBuffers: [],
5285
+ currentTextIndex: -1,
5286
+ tools: /* @__PURE__ */ new Map(),
5287
+ thinking: [],
5288
+ currentThinkingIndex: -1,
5289
+ blockOrder: []
5290
+ };
5291
+ }
5292
+ function handleMessageStart(state, model) {
5293
+ state.model = model;
5294
+ }
5295
+ function handleContentBlockStart(state, ev) {
5296
+ const kind = ev.kind ?? "text";
5297
+ if (kind === "text") {
5298
+ state.currentTextIndex = state.textBuffers.length;
5299
+ state.textBuffers.push("");
5300
+ state.blockOrder.push({ kind: "text", idx: state.currentTextIndex });
5301
+ } else if (kind === "tool_use") {
5302
+ const id = ev.id ?? crypto.randomUUID();
5303
+ state.tools.set(id, { name: ev.name ?? "unknown", partial: "" });
5304
+ state.blockOrder.push({ kind: "tool", id });
5305
+ state.currentTextIndex = -1;
5306
+ } else if (kind === "thinking") {
5307
+ state.currentThinkingIndex = state.thinking.length;
5308
+ state.thinking.push({
5309
+ textBuf: "",
5310
+ ...ev.providerMeta ? { providerMeta: ev.providerMeta } : {}
5311
+ });
5312
+ state.blockOrder.push({ kind: "thinking", idx: state.currentThinkingIndex });
5313
+ state.currentTextIndex = -1;
5314
+ }
5315
+ }
5316
+ function handleContentBlockStop(state, ev) {
5317
+ }
5318
+ function handleTextDelta(state, text) {
5319
+ if (state.currentTextIndex === -1) {
5320
+ state.currentTextIndex = state.textBuffers.length;
5321
+ state.textBuffers.push("");
5322
+ state.blockOrder.push({ kind: "text", idx: state.currentTextIndex });
5323
+ }
5324
+ state.textBuffers[state.currentTextIndex] = (state.textBuffers[state.currentTextIndex] ?? "") + text;
5325
+ }
5326
+ function handleToolUseStart(state, ev) {
5327
+ state.currentTextIndex = -1;
5328
+ state.tools.set(ev.id, { name: ev.name, partial: "" });
5329
+ state.blockOrder.push({ kind: "tool", id: ev.id });
5330
+ }
5331
+ function handleToolUseInputDelta(state, ev) {
5332
+ const t2 = state.tools.get(ev.id);
5333
+ if (t2) t2.partial += ev.partial;
5334
+ }
5335
+ function safeJsonOrRaw(s) {
5336
+ if (!s) return {};
5337
+ try {
5338
+ return JSON.parse(s);
5339
+ } catch {
5340
+ return { _raw: s };
5341
+ }
5342
+ }
5343
+ function handleToolUseStop(state, ev) {
5344
+ const t2 = state.tools.get(ev.id);
5345
+ if (t2) {
5346
+ t2.input = ev.input !== void 0 ? ev.input : safeJsonOrRaw(t2.partial);
5347
+ if (ev.providerMeta) t2.providerMeta = ev.providerMeta;
5348
+ }
5349
+ state.currentTextIndex = -1;
5350
+ }
5351
+ function handleThinkingStart(state, ev) {
5352
+ state.currentThinkingIndex = state.thinking.length;
5353
+ state.thinking.push({
5354
+ textBuf: "",
5355
+ ...ev.providerMeta ? { providerMeta: ev.providerMeta } : {}
5356
+ });
5357
+ state.blockOrder.push({ kind: "thinking", idx: state.currentThinkingIndex });
5358
+ state.currentTextIndex = -1;
5359
+ }
5360
+ function handleThinkingDelta(state, text) {
5361
+ if (state.currentThinkingIndex === -1) {
5362
+ handleThinkingStart(state, {});
5363
+ }
5364
+ const t2 = state.thinking[state.currentThinkingIndex];
5365
+ if (t2) t2.textBuf += text;
5366
+ }
5367
+ function handleThinkingSignature(state, signature) {
5368
+ if (state.currentThinkingIndex === -1) {
5369
+ handleThinkingStart(state, {});
5370
+ }
5371
+ const t2 = state.thinking[state.currentThinkingIndex];
5372
+ if (t2) t2.signature = signature;
5373
+ }
5374
+ function handleThinkingStop(state) {
5375
+ state.currentThinkingIndex = -1;
5376
+ }
5377
+ function handleMessageStop(state, ev) {
5378
+ state.stopReason = ev.stopReason ?? "end_turn";
5379
+ state.usage = ev.usage ?? { input: 0, output: 0 };
5380
+ }
5381
+ async function streamProviderToResponse(provider, req, signal, ctx, events) {
5382
+ const state = createStreamingState(req.model);
5383
+ const iter = provider.stream(req, { signal })[Symbol.asyncIterator]();
5384
+ try {
5385
+ for (; ; ) {
5386
+ const next = await iter.next();
5387
+ if (next.done) break;
5388
+ const ev = next.value;
5389
+ switch (ev.type) {
5390
+ case "message_start":
5391
+ handleMessageStart(state, ev.model);
5392
+ break;
5393
+ case "content_block_start":
5394
+ handleContentBlockStart(state, ev);
5395
+ break;
5396
+ case "content_block_stop":
5397
+ handleContentBlockStop(state, ev);
5398
+ break;
5399
+ case "text_delta":
5400
+ handleTextDelta(state, ev.text);
5401
+ events.emit("provider.text_delta", { ctx, text: ev.text });
5402
+ break;
5403
+ case "tool_use_start":
5404
+ handleToolUseStart(state, ev);
5405
+ events.emit("provider.tool_use_start", { ctx, id: ev.id, name: ev.name });
5406
+ break;
5407
+ case "tool_use_input_delta":
5408
+ handleToolUseInputDelta(state, ev);
5409
+ break;
5410
+ case "tool_use_stop":
5411
+ handleToolUseStop(state, ev);
5412
+ events.emit("provider.tool_use_stop", { ctx, id: ev.id });
5413
+ break;
5414
+ case "thinking_start":
5415
+ handleThinkingStart(state, ev);
5416
+ break;
5417
+ case "thinking_delta":
5418
+ handleThinkingDelta(state, ev.text);
5419
+ events.emit("provider.thinking_delta", { ctx, text: ev.text });
5420
+ break;
5421
+ case "thinking_signature":
5422
+ handleThinkingSignature(state, ev.signature);
5423
+ break;
5424
+ case "thinking_stop":
5425
+ handleThinkingStop(state);
5426
+ break;
5427
+ case "message_stop":
5428
+ handleMessageStop(state, ev);
5429
+ break;
5430
+ }
5431
+ }
5432
+ } catch (err) {
5433
+ if (signal.aborted) {
5434
+ state.stopReason = "end_turn";
5435
+ return buildResponse(state);
5436
+ }
5437
+ throw err;
5438
+ } finally {
5439
+ try {
5440
+ let drainTimer = null;
5441
+ try {
5442
+ await Promise.race([
5443
+ Promise.resolve(iter.return?.()),
5444
+ new Promise((resolve4) => {
5445
+ drainTimer = setTimeout(resolve4, 500);
5446
+ })
5447
+ ]);
5448
+ } finally {
5449
+ if (drainTimer) clearTimeout(drainTimer);
5450
+ }
5451
+ } catch {
5452
+ }
5453
+ }
5454
+ return buildResponse(state);
5455
+ }
5456
+
5457
+ // src/core/provider-runner.ts
5458
+ async function runProviderWithRetry(opts) {
5459
+ const { provider, request, signal, ctx, events, retry, logger, tracer } = opts;
5460
+ let attempt = 0;
5461
+ for (; ; ) {
5462
+ const span = tracer?.startSpan("provider.complete", {
5463
+ "provider.id": provider.id,
5464
+ "provider.model": request.model,
5465
+ "provider.streaming": provider.capabilities.streaming,
5466
+ "provider.attempt": attempt
5467
+ });
5468
+ try {
5469
+ const res = provider.capabilities.streaming ? await streamProviderToResponse(provider, request, signal, ctx, events) : await provider.complete(request, { signal });
5470
+ span?.setAttribute("provider.stopReason", res.stopReason);
5471
+ span?.setAttribute("provider.usage_in", res.usage.input);
5472
+ span?.setAttribute("provider.usage_out", res.usage.output);
5473
+ span?.end();
5474
+ return res;
5475
+ } catch (err) {
5476
+ if (err instanceof Error) span?.recordError(err);
5477
+ span?.end();
5478
+ if (signal.aborted) throw err;
5479
+ const isProviderErr = err instanceof ProviderError;
5480
+ const errAsErr = err instanceof Error ? err : new Error(String(err));
5481
+ const canRetry = retry.shouldRetry(isProviderErr ? err : errAsErr, attempt);
5482
+ const description = isProviderErr ? err.describe() : errAsErr.message;
5483
+ if (!canRetry) {
5484
+ if (isProviderErr) {
5485
+ events.emit("provider.error", {
5486
+ providerId: err.providerId,
5487
+ status: err.status,
5488
+ description,
5489
+ retryable: false
5490
+ });
5491
+ }
5492
+ throw err;
5493
+ }
5494
+ const delay = Math.round(retry.delayMs(attempt));
5495
+ const attemptNum = attempt + 1;
5496
+ logger.warn(`Provider retry ${attemptNum} in ${delay}ms \u2014 ${description}`);
5497
+ if (isProviderErr) {
5498
+ events.emit("provider.retry", {
5499
+ providerId: err.providerId,
5500
+ attempt: attemptNum,
5501
+ delayMs: delay,
5502
+ status: err.status,
5503
+ description
5504
+ });
5505
+ }
5506
+ await new Promise((resolve4, reject) => {
5507
+ let settled = false;
5508
+ const onAbort = () => {
5509
+ if (settled) return;
5510
+ settled = true;
5511
+ clearTimeout(t2);
5512
+ reject(new Error("aborted"));
5513
+ };
5514
+ const t2 = setTimeout(() => {
5515
+ if (settled) return;
5516
+ settled = true;
5517
+ clearTimeout(t2);
5518
+ signal.removeEventListener("abort", onAbort);
5519
+ resolve4();
5520
+ }, delay);
5521
+ if (signal.aborted) {
5522
+ onAbort();
5523
+ return;
5524
+ }
5525
+ signal.addEventListener("abort", onAbort, { once: true });
5526
+ });
5527
+ attempt++;
5528
+ }
5529
+ }
5530
+ }
5531
+
5532
+ // src/execution/provider-runner-impl.ts
5533
+ var DefaultProviderRunner = class {
5534
+ async run(opts) {
5535
+ return runProviderWithRetry(opts);
5536
+ }
5537
+ };
5538
+
5539
+ // src/execution/intelligent-compactor.ts
5540
+ var IntelligentCompactor = class {
5541
+ provider;
5542
+ warnThreshold;
5543
+ softThreshold;
5544
+ hardThreshold;
5545
+ maxContext;
5546
+ preserveK;
5547
+ eliseThreshold;
5548
+ summarizerPrompt;
5549
+ summarizerModel;
5550
+ constructor(opts) {
5551
+ this.provider = opts.provider;
5552
+ this.warnThreshold = opts.warnThreshold ?? 0.6;
5553
+ this.softThreshold = opts.softThreshold ?? 0.75;
5554
+ this.hardThreshold = opts.hardThreshold ?? 0.9;
5555
+ this.maxContext = opts.maxContext ?? 128e3;
5556
+ this.preserveK = opts.preserveK ?? 4;
5557
+ this.eliseThreshold = opts.eliseThreshold ?? 500;
5558
+ this.summarizerPrompt = opts.summarizerPrompt ?? "You are a context summarizer. Given a list of conversation messages, produce a concise but complete summary that preserves all factual information, decisions made, and any state changes (e.g. file edits, todo updates). Do not add commentary. Output only the summary.";
5559
+ this.summarizerModel = opts.summarizerModel;
5560
+ }
5561
+ async compact(ctx, opts = {}) {
5562
+ const beforeTokens = this.estimateTokens(ctx.messages);
5563
+ const reductions = [];
5564
+ const load = beforeTokens / this.maxContext;
5565
+ const aggressive = load >= this.hardThreshold ? true : opts.aggressive ?? load >= this.softThreshold;
5566
+ const saved1 = this.eliseOldToolResults(ctx);
5567
+ if (saved1 > 0) reductions.push({ phase: "elision", saved: saved1 });
5568
+ if (aggressive) {
5569
+ const saved2 = await this.summarizeAncientTurns(ctx);
5570
+ if (saved2 > 0) reductions.push({ phase: "summary", saved: saved2 });
5571
+ } else if (load >= this.warnThreshold) {
5572
+ const saved2 = this.lightweightCompact(ctx);
5573
+ if (saved2 > 0) reductions.push({ phase: "elision", saved: saved2 });
5574
+ }
5575
+ const repaired = repairToolUseAdjacency(ctx.messages);
5576
+ if (repaired.report.changed) ctx.state.replaceMessages(repaired.messages);
5577
+ const afterTokens = this.estimateTokens(ctx.messages);
5578
+ return {
5579
+ before: beforeTokens,
5580
+ after: afterTokens,
5581
+ reductions,
5582
+ repaired: repaired.report.changed ? {
5583
+ removedToolUses: repaired.report.removedToolUses,
5584
+ removedToolResults: repaired.report.removedToolResults,
5585
+ removedMessages: repaired.report.removedMessages
5586
+ } : void 0
5587
+ };
5588
+ }
5589
+ async summarizeAncientTurns(ctx) {
5590
+ const messages = ctx.messages;
5591
+ const cutoff = Math.max(0, messages.length - this.preserveK * 2);
5592
+ if (cutoff <= 2) return 0;
5593
+ const boundary = this.findSafeBoundary(messages, 0, cutoff);
5594
+ if (boundary <= 1) return 0;
5595
+ const toSummarize = messages.slice(0, boundary);
5596
+ const removedTokens = this.estimateTokens(toSummarize);
5597
+ let summaryText;
5598
+ try {
5599
+ summaryText = await this.callSummarizer(toSummarize, ctx);
5600
+ } catch {
5601
+ summaryText = `[${toSummarize.length} earlier turns omitted \u2014 key decisions and file states preserved in context]`;
5602
+ }
5603
+ const summaryMsg = {
5604
+ role: "system",
5605
+ content: `[prior_turns_summary: ${summaryText}]`
5606
+ };
5607
+ const summaryTokens = this.estimateTokens([summaryMsg]);
5608
+ const tail = ctx.messages.slice(boundary);
5609
+ ctx.state.replaceMessages([summaryMsg, ...tail]);
5610
+ return Math.max(0, removedTokens - summaryTokens);
5611
+ }
5612
+ findSafeBoundary(messages, from, to) {
5613
+ for (let i = to; i >= from; i--) {
5614
+ const m = messages[i];
5615
+ if (!m) continue;
5616
+ if (m.role === "user" && this.hasTextContent(m)) {
5617
+ return this.findExchangeStart(messages, i);
5618
+ }
5619
+ }
5620
+ return -1;
5621
+ }
5622
+ findExchangeStart(messages, userIndex) {
5623
+ for (let i = userIndex - 1; i >= 0; i--) {
5624
+ const m = messages[i];
5625
+ if (!m) continue;
5626
+ if (m.role === "assistant") {
5627
+ const hasToolUse2 = Array.isArray(m.content) ? m.content.some((b) => b.type === "tool_use") : false;
5628
+ if (!hasToolUse2) {
5629
+ return i + 1;
5630
+ }
5631
+ } else if (m.role !== "user") ; else {
5632
+ return i;
5633
+ }
5634
+ }
5635
+ return 0;
5636
+ }
5637
+ async callSummarizer(messages, ctx) {
5638
+ const prompt = [
5639
+ { type: "text", text: this.summarizerPrompt },
5640
+ { type: "text", text: "\n\nConversation to summarize:\n" },
5641
+ ...this.messagesToText(messages)
5642
+ ];
5643
+ const req = {
5644
+ model: this.summarizerModel ?? ctx.model,
5645
+ system: prompt,
5646
+ messages: [],
5647
+ maxTokens: 1024
5648
+ };
5649
+ const ac = ctx.signal ? void 0 : new AbortController();
5650
+ const signal = ctx.signal ?? ac.signal;
5651
+ const res = await this.provider.complete(req, { signal });
5652
+ const textBlocks = res.content.filter(isTextBlock);
5653
+ return textBlocks.map((b) => b.text).join("\n").trim() || "(empty summary)";
5654
+ }
5655
+ messagesToText(messages) {
4998
5656
  const lines = [];
4999
5657
  for (const m of messages) {
5000
5658
  const role = m.role.padEnd(10, " ");
@@ -5273,8 +5931,9 @@ var SelectiveCompactor = class {
5273
5931
  if (!shouldCompact) {
5274
5932
  const saved = this.eliseOldToolResults(ctx);
5275
5933
  if (saved > 0) reductions.push({ phase: "elision", saved });
5934
+ const repair2 = this.repairProtocolAdjacency(ctx);
5276
5935
  const afterTokens2 = this.estimateTokens(ctx.messages);
5277
- return { before: beforeTokens, after: afterTokens2, reductions };
5936
+ return { before: beforeTokens, after: afterTokens2, reductions, repaired: repair2 };
5278
5937
  }
5279
5938
  const savedElision = this.eliseOldToolResults(ctx);
5280
5939
  if (savedElision > 0) reductions.push({ phase: "elision", saved: savedElision });
@@ -5284,8 +5943,18 @@ var SelectiveCompactor = class {
5284
5943
  const savedSelective = await this.runSelector(ctx, targetBudget);
5285
5944
  if (savedSelective > 0) reductions.push({ phase: "selective", saved: savedSelective });
5286
5945
  }
5946
+ const repair = this.repairProtocolAdjacency(ctx);
5287
5947
  const afterTokens = this.estimateTokens(ctx.messages);
5288
- return { before: beforeTokens, after: afterTokens, reductions };
5948
+ return { before: beforeTokens, after: afterTokens, reductions, repaired: repair };
5949
+ }
5950
+ repairProtocolAdjacency(ctx) {
5951
+ const repaired = repairToolUseAdjacency(ctx.messages);
5952
+ if (repaired.report.changed) ctx.state.replaceMessages(repaired.messages);
5953
+ return repaired.report.changed ? {
5954
+ removedToolUses: repaired.report.removedToolUses,
5955
+ removedToolResults: repaired.report.removedToolResults,
5956
+ removedMessages: repaired.report.removedMessages
5957
+ } : void 0;
5289
5958
  }
5290
5959
  /**
5291
5960
  * Run the LLM selector to decide what to keep vs collapse.
@@ -5473,6 +6142,7 @@ var AutoCompactionMiddleware = class {
5473
6142
  aggressiveOn;
5474
6143
  events;
5475
6144
  failureMode;
6145
+ policyProvider;
5476
6146
  /**
5477
6147
  * @param compactor Compactor to use for compaction.
5478
6148
  * @param maxContext Provider's max context window in tokens.
@@ -5495,17 +6165,25 @@ var AutoCompactionMiddleware = class {
5495
6165
  this.aggressiveOn = opts.aggressiveOn ?? "soft";
5496
6166
  this.events = opts.events;
5497
6167
  this.failureMode = opts.failureMode ?? "throw_on_hard";
6168
+ this.policyProvider = opts.policyProvider;
5498
6169
  }
5499
6170
  handler() {
5500
6171
  return async (ctx, next) => {
5501
6172
  const tokens = this.estimator(ctx);
5502
6173
  const load = tokens / this.maxContext;
5503
- if (load >= this.hardThreshold) {
6174
+ const policy = this.policyProvider?.(ctx);
6175
+ const thresholds = policy?.thresholds ?? {
6176
+ warn: this.warnThreshold,
6177
+ soft: this.softThreshold,
6178
+ hard: this.hardThreshold
6179
+ };
6180
+ const aggressiveOn = policy?.aggressiveOn ?? this.aggressiveOn;
6181
+ if (load >= thresholds.hard) {
5504
6182
  await this.compact(ctx, true, { level: "hard", tokens, load });
5505
- } else if (load >= this.softThreshold) {
5506
- await this.compact(ctx, this.aggressiveOn !== "hard", { level: "soft", tokens, load });
5507
- } else if (load >= this.warnThreshold) {
5508
- await this.compact(ctx, false, { level: "warn", tokens, load });
6183
+ } else if (load >= thresholds.soft) {
6184
+ await this.compact(ctx, aggressiveOn !== "hard", { level: "soft", tokens, load });
6185
+ } else if (load >= thresholds.warn) {
6186
+ await this.compact(ctx, aggressiveOn === "warn", { level: "warn", tokens, load });
5509
6187
  }
5510
6188
  return next(ctx);
5511
6189
  };
@@ -6473,6 +7151,179 @@ function providerErrorToSubagentError(err, message, cause) {
6473
7151
  }
6474
7152
  return { kind: "unknown", message, retryable: err.retryable, cause };
6475
7153
  }
7154
+ function makeSpawnTool(director, roster) {
7155
+ const inputSchema = {
7156
+ type: "object",
7157
+ properties: {
7158
+ role: { type: "string", description: "Roster role id (preferred). When set, the spawn uses the matching config from the roster and ignores other fields." },
7159
+ name: { type: "string", description: "Display name for the subagent. Required when not using roster." },
7160
+ provider: { type: "string", description: 'Provider id (e.g. "anthropic", "openai"). Defaults to the leader provider when omitted.' },
7161
+ model: { type: "string", description: "Model id within the provider. Defaults to the leader model when omitted." },
7162
+ systemPromptOverride: { type: "string", description: "Extra prompt text appended after the role-base prompt." },
7163
+ maxIterations: { type: "number" },
7164
+ maxToolCalls: { type: "number" },
7165
+ maxCostUsd: { type: "number" }
7166
+ },
7167
+ required: []
7168
+ };
7169
+ return {
7170
+ name: "spawn_subagent",
7171
+ description: "Create a new subagent under this director. Returns the subagent id.",
7172
+ usageHint: "Either pass `role` (matches the roster) OR pass `name` + optional `provider`/`model`. Returns `{ subagentId }`.",
7173
+ permission: "auto",
7174
+ mutating: false,
7175
+ inputSchema,
7176
+ async execute(input) {
7177
+ const i = input ?? {};
7178
+ const role = typeof i.role === "string" ? i.role : void 0;
7179
+ const base = role && roster ? roster[role] : void 0;
7180
+ if (role && !base) {
7181
+ return { error: `unknown role "${role}". roster has: ${roster ? Object.keys(roster).join(", ") : "(empty)"}` };
7182
+ }
7183
+ const cfg = { ...base ?? { name: i.name ?? "subagent" } };
7184
+ if (typeof i.name === "string") cfg.name = i.name;
7185
+ if (typeof i.provider === "string") cfg.provider = i.provider;
7186
+ if (typeof i.model === "string") cfg.model = i.model;
7187
+ if (typeof i.systemPromptOverride === "string") cfg.systemPromptOverride = i.systemPromptOverride;
7188
+ if (typeof i.maxIterations === "number") cfg.maxIterations = i.maxIterations;
7189
+ if (typeof i.maxToolCalls === "number") cfg.maxToolCalls = i.maxToolCalls;
7190
+ if (typeof i.maxCostUsd === "number") cfg.maxCostUsd = i.maxCostUsd;
7191
+ try {
7192
+ const subagentId = await director.spawn(cfg);
7193
+ return { subagentId, provider: cfg.provider, model: cfg.model, name: cfg.name };
7194
+ } catch (err) {
7195
+ if (err instanceof DirectorBudgetError) {
7196
+ return { error: err.message, kind: err.kind, limit: err.limit, observed: err.observed };
7197
+ }
7198
+ return { error: err instanceof Error ? err.message : String(err) };
7199
+ }
7200
+ }
7201
+ };
7202
+ }
7203
+ function makeAssignTool(director) {
7204
+ const inputSchema = {
7205
+ type: "object",
7206
+ properties: {
7207
+ subagentId: { type: "string", description: "Target subagent id. Required." },
7208
+ description: { type: "string", description: "The task in natural language \u2014 what you want this subagent to do." },
7209
+ maxToolCalls: { type: "number", description: "Optional per-task tool-call budget override." },
7210
+ timeoutMs: { type: "number", description: "Optional per-task timeout in ms." }
7211
+ },
7212
+ required: ["subagentId", "description"]
7213
+ };
7214
+ return {
7215
+ name: "assign_task",
7216
+ description: "Hand a task to a previously spawned subagent. Returns the task id.",
7217
+ permission: "auto",
7218
+ mutating: false,
7219
+ inputSchema,
7220
+ async execute(input) {
7221
+ const i = input;
7222
+ const task = { id: randomUUID(), description: i.description, subagentId: i.subagentId, maxToolCalls: i.maxToolCalls, timeoutMs: i.timeoutMs };
7223
+ const taskId = await director.assign(task);
7224
+ return { taskId, subagentId: i.subagentId };
7225
+ }
7226
+ };
7227
+ }
7228
+ function makeAwaitTasksTool(director) {
7229
+ return {
7230
+ name: "await_tasks",
7231
+ description: "Block until every named task completes. Returns the array of TaskResult.",
7232
+ permission: "auto",
7233
+ mutating: false,
7234
+ inputSchema: { type: "object", properties: { taskIds: { type: "array", items: { type: "string" }, description: "One or more task ids returned by `assign_task`." } }, required: ["taskIds"] },
7235
+ async execute(input) {
7236
+ const i = input;
7237
+ const results = await director.awaitTasks(i.taskIds);
7238
+ return { results };
7239
+ }
7240
+ };
7241
+ }
7242
+ function makeAskTool(director) {
7243
+ return {
7244
+ name: "ask_subagent",
7245
+ description: "Synchronously ask a subagent a question. Blocks until the subagent replies via the bridge.",
7246
+ permission: "auto",
7247
+ mutating: false,
7248
+ inputSchema: {
7249
+ type: "object",
7250
+ properties: {
7251
+ subagentId: { type: "string", description: "Subagent to ask. Must be a previously spawned id." },
7252
+ question: { type: "string", description: "The question or instruction." },
7253
+ timeoutMs: { type: "number", description: "Optional timeout in ms (default 30s)." }
7254
+ },
7255
+ required: ["subagentId", "question"]
7256
+ },
7257
+ async execute(input) {
7258
+ const i = input;
7259
+ try {
7260
+ const answer = await director.ask(i.subagentId, { question: i.question }, i.timeoutMs);
7261
+ return { ok: true, answer };
7262
+ } catch (err) {
7263
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
7264
+ }
7265
+ }
7266
+ };
7267
+ }
7268
+ function makeRollUpTool(director) {
7269
+ return {
7270
+ name: "roll_up",
7271
+ description: "Aggregate completed task results into a single formatted summary.",
7272
+ permission: "auto",
7273
+ mutating: false,
7274
+ inputSchema: {
7275
+ type: "object",
7276
+ properties: {
7277
+ taskIds: { type: "array", items: { type: "string" }, description: "Completed task ids to aggregate." },
7278
+ style: { type: "string", enum: ["markdown", "json"], description: "Output flavor \u2014 markdown (default) or json." }
7279
+ },
7280
+ required: ["taskIds"]
7281
+ },
7282
+ async execute(input) {
7283
+ const i = input;
7284
+ const summary = director.rollUp(i.taskIds, i.style ?? "markdown");
7285
+ return { summary, count: i.taskIds.length };
7286
+ }
7287
+ };
7288
+ }
7289
+ function makeTerminateTool(director) {
7290
+ return {
7291
+ name: "terminate_subagent",
7292
+ description: "Forcibly abort a subagent.",
7293
+ permission: "auto",
7294
+ mutating: true,
7295
+ inputSchema: { type: "object", properties: { subagentId: { type: "string", description: "Subagent to abort." } }, required: ["subagentId"] },
7296
+ async execute(input) {
7297
+ const i = input;
7298
+ await director.terminate(i.subagentId);
7299
+ return { ok: true };
7300
+ }
7301
+ };
7302
+ }
7303
+ function makeFleetStatusTool(director) {
7304
+ return {
7305
+ name: "fleet_status",
7306
+ description: "Snapshot of the fleet \u2014 every subagent's current status, pending vs. completed task counts.",
7307
+ permission: "auto",
7308
+ mutating: false,
7309
+ inputSchema: { type: "object", properties: {}, required: [] },
7310
+ async execute() {
7311
+ return director.status();
7312
+ }
7313
+ };
7314
+ }
7315
+ function makeFleetUsageTool(director) {
7316
+ return {
7317
+ name: "fleet_usage",
7318
+ description: "Token + cost breakdown across the fleet, per-subagent and totals.",
7319
+ permission: "auto",
7320
+ mutating: false,
7321
+ inputSchema: { type: "object", properties: {}, required: [] },
7322
+ async execute() {
7323
+ return director.snapshot();
7324
+ }
7325
+ };
7326
+ }
6476
7327
 
6477
7328
  // src/coordination/director.ts
6478
7329
  var DirectorBudgetError = class extends Error {
@@ -7031,242 +7882,6 @@ var Director = class {
7031
7882
  return t2;
7032
7883
  }
7033
7884
  };
7034
- function makeSpawnTool(director, roster) {
7035
- const inputSchema = {
7036
- type: "object",
7037
- properties: {
7038
- role: {
7039
- type: "string",
7040
- description: "Roster role id (preferred). When set, the spawn uses the matching config from the roster and ignores other fields."
7041
- },
7042
- name: {
7043
- type: "string",
7044
- description: "Display name for the subagent. Required when not using roster."
7045
- },
7046
- provider: {
7047
- type: "string",
7048
- description: 'Provider id (e.g. "anthropic", "openai"). Defaults to the leader provider when omitted.'
7049
- },
7050
- model: {
7051
- type: "string",
7052
- description: "Model id within the provider. Defaults to the leader model when omitted."
7053
- },
7054
- systemPromptOverride: {
7055
- type: "string",
7056
- description: "Extra prompt text appended after the role-base prompt."
7057
- },
7058
- maxIterations: { type: "number" },
7059
- maxToolCalls: { type: "number" },
7060
- maxCostUsd: { type: "number" }
7061
- },
7062
- required: []
7063
- };
7064
- return {
7065
- name: "spawn_subagent",
7066
- description: "Create a new subagent under this director. Returns the subagent id. Use this when you need a worker with a specific provider, model, or role to handle a piece of the plan.",
7067
- usageHint: "Either pass `role` (matches the roster) OR pass `name` + optional `provider`/`model`. Returns `{ subagentId }`.",
7068
- permission: "auto",
7069
- mutating: false,
7070
- inputSchema,
7071
- async execute(input) {
7072
- const i = input ?? {};
7073
- const role = typeof i.role === "string" ? i.role : void 0;
7074
- const base = role && roster ? roster[role] : void 0;
7075
- if (role && !base) {
7076
- return {
7077
- error: `unknown role "${role}". roster has: ${roster ? Object.keys(roster).join(", ") : "(empty)"}`
7078
- };
7079
- }
7080
- const cfg = {
7081
- ...base ?? { name: i.name ?? "subagent" }
7082
- };
7083
- if (typeof i.name === "string") cfg.name = i.name;
7084
- if (typeof i.provider === "string") cfg.provider = i.provider;
7085
- if (typeof i.model === "string") cfg.model = i.model;
7086
- if (typeof i.systemPromptOverride === "string")
7087
- cfg.systemPromptOverride = i.systemPromptOverride;
7088
- if (typeof i.maxIterations === "number") cfg.maxIterations = i.maxIterations;
7089
- if (typeof i.maxToolCalls === "number") cfg.maxToolCalls = i.maxToolCalls;
7090
- if (typeof i.maxCostUsd === "number") cfg.maxCostUsd = i.maxCostUsd;
7091
- try {
7092
- const subagentId = await director.spawn(cfg);
7093
- return { subagentId, provider: cfg.provider, model: cfg.model, name: cfg.name };
7094
- } catch (err) {
7095
- if (err instanceof DirectorBudgetError) {
7096
- return { error: err.message, kind: err.kind, limit: err.limit, observed: err.observed };
7097
- }
7098
- return { error: err instanceof Error ? err.message : String(err) };
7099
- }
7100
- }
7101
- };
7102
- }
7103
- function makeAssignTool(director) {
7104
- const inputSchema = {
7105
- type: "object",
7106
- properties: {
7107
- subagentId: { type: "string", description: "Target subagent id. Required." },
7108
- description: {
7109
- type: "string",
7110
- description: "The task in natural language \u2014 what you want this subagent to do."
7111
- },
7112
- maxToolCalls: { type: "number", description: "Optional per-task tool-call budget override." },
7113
- timeoutMs: { type: "number", description: "Optional per-task timeout in ms." }
7114
- },
7115
- required: ["subagentId", "description"]
7116
- };
7117
- return {
7118
- name: "assign_task",
7119
- description: "Hand a task to a previously spawned subagent. Returns the task id \u2014 pass it to `await_tasks` to block on completion.",
7120
- permission: "auto",
7121
- mutating: false,
7122
- inputSchema,
7123
- async execute(input) {
7124
- const i = input;
7125
- const task = {
7126
- id: randomUUID(),
7127
- description: i.description,
7128
- subagentId: i.subagentId,
7129
- maxToolCalls: i.maxToolCalls,
7130
- timeoutMs: i.timeoutMs
7131
- };
7132
- const taskId = await director.assign(task);
7133
- return { taskId, subagentId: i.subagentId };
7134
- }
7135
- };
7136
- }
7137
- function makeAwaitTasksTool(director) {
7138
- const inputSchema = {
7139
- type: "object",
7140
- properties: {
7141
- taskIds: {
7142
- type: "array",
7143
- items: { type: "string" },
7144
- description: "One or more task ids returned by `assign_task`. The call blocks until every id resolves."
7145
- }
7146
- },
7147
- required: ["taskIds"]
7148
- };
7149
- return {
7150
- name: "await_tasks",
7151
- description: "Block until every named task completes. Returns the array of TaskResult \u2014 use this to gather subagent output before deciding the next step.",
7152
- permission: "auto",
7153
- mutating: false,
7154
- inputSchema,
7155
- async execute(input) {
7156
- const i = input;
7157
- const results = await director.awaitTasks(i.taskIds);
7158
- return { results };
7159
- }
7160
- };
7161
- }
7162
- function makeAskTool(director) {
7163
- const inputSchema = {
7164
- type: "object",
7165
- properties: {
7166
- subagentId: {
7167
- type: "string",
7168
- description: "Subagent to ask. Must be a previously spawned id."
7169
- },
7170
- question: {
7171
- type: "string",
7172
- description: "The question or instruction. Sent as the bridge message payload."
7173
- },
7174
- timeoutMs: { type: "number", description: "Optional timeout in ms (default 30s)." }
7175
- },
7176
- required: ["subagentId", "question"]
7177
- };
7178
- return {
7179
- name: "ask_subagent",
7180
- description: "Synchronously ask a subagent a question. Blocks until the subagent replies via the bridge (or the timeout fires). Use this when you need a one-shot answer without spawning a fresh task.",
7181
- permission: "auto",
7182
- mutating: false,
7183
- inputSchema,
7184
- async execute(input) {
7185
- const i = input;
7186
- try {
7187
- const answer = await director.ask(i.subagentId, { question: i.question }, i.timeoutMs);
7188
- return { ok: true, answer };
7189
- } catch (err) {
7190
- return { ok: false, error: err instanceof Error ? err.message : String(err) };
7191
- }
7192
- }
7193
- };
7194
- }
7195
- function makeRollUpTool(director) {
7196
- const inputSchema = {
7197
- type: "object",
7198
- properties: {
7199
- taskIds: {
7200
- type: "array",
7201
- items: { type: "string" },
7202
- description: "Completed task ids to aggregate. Pass the ids returned by previous `assign_task` calls."
7203
- },
7204
- style: {
7205
- type: "string",
7206
- enum: ["markdown", "json"],
7207
- description: "Output flavor \u2014 markdown (default) for in-prompt summarization, json for structured downstream processing."
7208
- }
7209
- },
7210
- required: ["taskIds"]
7211
- };
7212
- return {
7213
- name: "roll_up",
7214
- description: "Aggregate completed task results into a single formatted summary. Use this after `await_tasks` to fold subagent outputs back into the director's context before deciding the next step.",
7215
- permission: "auto",
7216
- mutating: false,
7217
- inputSchema,
7218
- async execute(input) {
7219
- const i = input;
7220
- const summary = director.rollUp(i.taskIds, i.style ?? "markdown");
7221
- return { summary, count: i.taskIds.length };
7222
- }
7223
- };
7224
- }
7225
- function makeTerminateTool(director) {
7226
- const inputSchema = {
7227
- type: "object",
7228
- properties: {
7229
- subagentId: { type: "string", description: "Subagent to abort." }
7230
- },
7231
- required: ["subagentId"]
7232
- };
7233
- return {
7234
- name: "terminate_subagent",
7235
- description: 'Forcibly abort a subagent. Use sparingly \u2014 prefer waiting on the natural budget to expire. The current task (if any) ends with status "stopped".',
7236
- permission: "auto",
7237
- mutating: true,
7238
- inputSchema,
7239
- async execute(input) {
7240
- const i = input;
7241
- await director.terminate(i.subagentId);
7242
- return { ok: true };
7243
- }
7244
- };
7245
- }
7246
- function makeFleetStatusTool(director) {
7247
- return {
7248
- name: "fleet_status",
7249
- description: "Snapshot of the fleet \u2014 every subagent's current status, pending vs. completed task counts, and the running total iteration count. Cheap; call freely.",
7250
- permission: "auto",
7251
- mutating: false,
7252
- inputSchema: { type: "object", properties: {}, required: [] },
7253
- async execute() {
7254
- return director.status();
7255
- }
7256
- };
7257
- }
7258
- function makeFleetUsageTool(director) {
7259
- return {
7260
- name: "fleet_usage",
7261
- description: "Token + cost breakdown across the fleet, per-subagent and totals. Use this to reason about which workers to assign costly tasks to or when to wrap up to stay within budget.",
7262
- permission: "auto",
7263
- mutating: false,
7264
- inputSchema: { type: "object", properties: {}, required: [] },
7265
- async execute() {
7266
- return director.snapshot();
7267
- }
7268
- };
7269
- }
7270
7885
  function createDelegateTool(opts) {
7271
7886
  const defaultTimeoutMs = opts.defaultTimeoutMs ?? 4 * 60 * 60 * 1e3;
7272
7887
  const rosterIds = opts.roster ? Object.keys(opts.roster) : [];
@@ -9452,13 +10067,13 @@ function roughEstimate(messages) {
9452
10067
  function createContextManagerTool(opts = {}) {
9453
10068
  return {
9454
10069
  name: CONTEXT_MANAGER_TOOL_NAME,
9455
- description: 'Inspect or reorganize the conversation context window. Use "check" to see token budget. Use "summary" to collapse a message range into a concise note (provide "text" for custom summary). Use "prune" to remove specific messages by index. Use "add_note" to inject a summary note. Use "compact" to run aggressive compaction.',
10070
+ description: 'Inspect or reorganize the conversation context window. Use "check" to see token budget. Use "summary" to collapse a message range into a concise note (provide "text" for custom summary). Use "prune" to remove specific messages by index. Use "add_note" to inject a summary note. Use "compact" to run aggressive compaction. Use "repair" to remove orphan tool_use/tool_result blocks after manual context surgery.',
9456
10071
  inputSchema: {
9457
10072
  type: "object",
9458
10073
  properties: {
9459
10074
  action: {
9460
10075
  type: "string",
9461
- enum: ["check", "summary", "prune", "add_note", "compact"],
10076
+ enum: ["check", "summary", "prune", "add_note", "compact", "repair"],
9462
10077
  description: "The context operation to perform."
9463
10078
  },
9464
10079
  from: {
@@ -9486,12 +10101,15 @@ function createContextManagerTool(opts = {}) {
9486
10101
  const messages = ctx.messages;
9487
10102
  const beforeTokens = roughEstimate(messages);
9488
10103
  const applyMessages = (next) => {
10104
+ const repaired = repairToolUseAdjacency(next);
10105
+ const finalMessages = repaired.messages;
9489
10106
  if (ctx.state) {
9490
- ctx.state.replaceMessages(next);
10107
+ ctx.state.replaceMessages(finalMessages);
9491
10108
  } else {
9492
10109
  messages.length = 0;
9493
- messages.splice(0, 0, ...next);
10110
+ messages.splice(0, 0, ...finalMessages);
9494
10111
  }
10112
+ return repaired.report;
9495
10113
  };
9496
10114
  switch (input.action) {
9497
10115
  case "check": {
@@ -9508,6 +10126,22 @@ function createContextManagerTool(opts = {}) {
9508
10126
  })
9509
10127
  };
9510
10128
  }
10129
+ case "repair": {
10130
+ const repair = applyMessages([...messages]);
10131
+ const afterTokens = roughEstimate(ctx.messages);
10132
+ return {
10133
+ action: "repair",
10134
+ beforeTokens,
10135
+ afterTokens,
10136
+ messageCount: ctx.messages.length,
10137
+ repaired: repair.changed ? {
10138
+ removedToolUses: repair.removedToolUses,
10139
+ removedToolResults: repair.removedToolResults,
10140
+ removedMessages: repair.removedMessages
10141
+ } : void 0,
10142
+ notes: repair.changed ? "Context tool-call adjacency repaired." : "Context tool-call adjacency already valid."
10143
+ };
10144
+ }
9511
10145
  case "compact": {
9512
10146
  if (!opts.compactor) {
9513
10147
  return {
@@ -9518,11 +10152,19 @@ function createContextManagerTool(opts = {}) {
9518
10152
  };
9519
10153
  }
9520
10154
  const report = await opts.compactor.compact(ctx);
10155
+ const repair = applyMessages([...ctx.messages]);
10156
+ const afterTokens = repair.changed ? roughEstimate(ctx.messages) : report.after;
10157
+ const repaired = report.repaired ?? (repair.changed ? repair : void 0);
9521
10158
  return {
9522
10159
  action: "compact",
9523
10160
  beforeTokens,
9524
- afterTokens: report.after,
9525
- messageCount: messages.length
10161
+ afterTokens,
10162
+ messageCount: ctx.messages.length,
10163
+ repaired: repaired ? {
10164
+ removedToolUses: repaired.removedToolUses,
10165
+ removedToolResults: repaired.removedToolResults,
10166
+ removedMessages: repaired.removedMessages
10167
+ } : void 0
9526
10168
  };
9527
10169
  }
9528
10170
  case "prune": {
@@ -9538,14 +10180,19 @@ function createContextManagerTool(opts = {}) {
9538
10180
  }
9539
10181
  const copy = [...messages];
9540
10182
  const removed = copy.splice(from, to - from + 1);
9541
- applyMessages(copy);
9542
- const afterTokens = roughEstimate(copy);
10183
+ const repair = applyMessages(copy);
10184
+ const afterTokens = roughEstimate(ctx.messages);
9543
10185
  return {
9544
10186
  action: "prune",
9545
10187
  beforeTokens,
9546
10188
  afterTokens,
9547
- messageCount: copy.length,
9548
- removedCount: removed.length
10189
+ messageCount: ctx.messages.length,
10190
+ removedCount: removed.length,
10191
+ repaired: repair.changed ? {
10192
+ removedToolUses: repair.removedToolUses,
10193
+ removedToolResults: repair.removedToolResults,
10194
+ removedMessages: repair.removedMessages
10195
+ } : void 0
9549
10196
  };
9550
10197
  }
9551
10198
  case "add_note": {
@@ -9557,14 +10204,19 @@ function createContextManagerTool(opts = {}) {
9557
10204
  };
9558
10205
  const copy = [...messages];
9559
10206
  copy.splice(afterIdx, 0, noteMsg);
9560
- applyMessages(copy);
9561
- const afterTokens = roughEstimate(copy);
10207
+ const repair = applyMessages(copy);
10208
+ const afterTokens = roughEstimate(ctx.messages);
9562
10209
  return {
9563
10210
  action: "add_note",
9564
10211
  beforeTokens,
9565
10212
  afterTokens,
9566
- messageCount: copy.length,
9567
- summary: noteText
10213
+ messageCount: ctx.messages.length,
10214
+ summary: noteText,
10215
+ repaired: repair.changed ? {
10216
+ removedToolUses: repair.removedToolUses,
10217
+ removedToolResults: repair.removedToolResults,
10218
+ removedMessages: repair.removedMessages
10219
+ } : void 0
9568
10220
  };
9569
10221
  }
9570
10222
  case "summary": {
@@ -9585,14 +10237,19 @@ function createContextManagerTool(opts = {}) {
9585
10237
  };
9586
10238
  const copy = [...messages];
9587
10239
  copy.splice(from, to - from + 1, summaryMsg);
9588
- applyMessages(copy);
9589
- const afterTokens = roughEstimate(copy);
10240
+ const repair = applyMessages(copy);
10241
+ const afterTokens = roughEstimate(ctx.messages);
9590
10242
  return {
9591
10243
  action: "summary",
9592
10244
  beforeTokens,
9593
10245
  afterTokens,
9594
- messageCount: copy.length,
9595
- summary: summaryText
10246
+ messageCount: ctx.messages.length,
10247
+ summary: summaryText,
10248
+ repaired: repair.changed ? {
10249
+ removedToolUses: repair.removedToolUses,
10250
+ removedToolResults: repair.removedToolResults,
10251
+ removedMessages: repair.removedMessages
10252
+ } : void 0
9596
10253
  };
9597
10254
  }
9598
10255
  default:
@@ -9696,349 +10353,305 @@ var sentinelServer = () => ({
9696
10353
  permission: "deny"
9697
10354
  // security tool — require explicit confirmation
9698
10355
  });
10356
+ var zaiVisionServer = () => ({
10357
+ name: "zai-vision",
10358
+ description: "Z.AI Vision MCP \u2014 image analysis and screenshot understanding",
10359
+ transport: "stdio",
10360
+ command: "npx",
10361
+ args: ["-y", "@z_ai/mcp-server@latest"],
10362
+ env: {
10363
+ Z_AI_API_KEY: process.env.Z_AI_API_KEY ?? "",
10364
+ Z_AI_MODE: process.env.Z_AI_MODE ?? "ZAI"
10365
+ },
10366
+ allowedTools: [
10367
+ "image_analysis",
10368
+ "extract_text_from_screenshot",
10369
+ "diagnose_error_screenshot",
10370
+ "understand_technical_diagram",
10371
+ "analyze_data_visualization",
10372
+ "ui_diff_check"
10373
+ ],
10374
+ permission: "auto"
10375
+ });
10376
+ var miniMaxVisionServer = () => ({
10377
+ name: "minimax-vision",
10378
+ description: "MiniMax MCP \u2014 image understanding via understand_image",
10379
+ transport: "stdio",
10380
+ command: "uvx",
10381
+ args: ["minimax-coding-plan-mcp", "-y"],
10382
+ env: {
10383
+ MINIMAX_API_KEY: process.env.MINIMAX_API_KEY ?? "",
10384
+ MINIMAX_MCP_BASE_PATH: process.env.MINIMAX_MCP_BASE_PATH ?? "./.wrongstack/minimax-output",
10385
+ MINIMAX_API_HOST: process.env.MINIMAX_API_HOST ?? "https://api.minimax.io",
10386
+ MINIMAX_API_RESOURCE_MODE: process.env.MINIMAX_API_RESOURCE_MODE ?? "url"
10387
+ },
10388
+ allowedTools: ["understand_image"],
10389
+ permission: "auto"
10390
+ });
9699
10391
  var allServers = () => ({
9700
10392
  filesystem: { ...filesystemServer(), enabled: false },
9701
10393
  github: { ...githubServer(), enabled: false },
9702
- context7: { ...context7Server(), enabled: false },
9703
- "brave-search": { ...braveSearchServer(), enabled: false },
9704
- block: { ...blockServer(), enabled: false },
9705
- everart: { ...everArtServer(), enabled: false },
9706
- slack: { ...slackServer(), enabled: false },
9707
- aws: { ...awsServer(), enabled: false },
9708
- "google-maps": { ...googleMapsServer(), enabled: false },
9709
- sentinel: { ...sentinelServer(), enabled: false }
9710
- });
9711
-
9712
- // src/core/iteration-limit.ts
9713
- function requestLimitExtension(opts) {
9714
- const { events, currentIterations, currentLimit, autoExtend, timeoutMs = 3e4 } = opts;
9715
- return new Promise((resolve4) => {
9716
- let resolved = false;
9717
- const timer = setTimeout(() => {
9718
- if (!resolved) {
9719
- resolved = true;
9720
- resolve4(0);
9721
- }
9722
- }, timeoutMs);
9723
- const deny = () => {
9724
- if (!resolved) {
9725
- resolved = true;
9726
- clearTimeout(timer);
9727
- resolve4(0);
9728
- }
10394
+ context7: { ...context7Server(), enabled: false },
10395
+ "brave-search": { ...braveSearchServer(), enabled: false },
10396
+ block: { ...blockServer(), enabled: false },
10397
+ everart: { ...everArtServer(), enabled: false },
10398
+ slack: { ...slackServer(), enabled: false },
10399
+ aws: { ...awsServer(), enabled: false },
10400
+ "google-maps": { ...googleMapsServer(), enabled: false },
10401
+ sentinel: { ...sentinelServer(), enabled: false },
10402
+ "zai-vision": { ...zaiVisionServer(), enabled: false },
10403
+ "minimax-vision": { ...miniMaxVisionServer(), enabled: false }
10404
+ });
10405
+
10406
+ // src/extension/registry.ts
10407
+ var ExtensionRegistry = class {
10408
+ extensions = [];
10409
+ promptContributors = [];
10410
+ log;
10411
+ setLogger(log) {
10412
+ this.log = log;
10413
+ }
10414
+ /**
10415
+ * Register a system prompt contributor. Returns an unregister function.
10416
+ * Contributors are called on every system prompt build in registration
10417
+ * order. Their output blocks are inserted after the core environment
10418
+ * block, before the mode and plan blocks.
10419
+ */
10420
+ registerSystemPromptContributor(c) {
10421
+ this.promptContributors.push(c);
10422
+ return () => {
10423
+ const idx = this.promptContributors.indexOf(c);
10424
+ if (idx >= 0) this.promptContributors.splice(idx, 1);
9729
10425
  };
9730
- const grant = (extra) => {
9731
- if (!resolved) {
9732
- resolved = true;
9733
- clearTimeout(timer);
9734
- resolve4(Math.max(0, extra));
10426
+ }
10427
+ /**
10428
+ * Build all registered system prompt contributions.
10429
+ * Failures are caught and logged — one bad contributor doesn't
10430
+ * break the prompt assembly.
10431
+ */
10432
+ async buildSystemPromptContributions(ctx) {
10433
+ const blocks = [];
10434
+ for (const c of this.promptContributors) {
10435
+ try {
10436
+ const contributed = await c(ctx);
10437
+ blocks.push(...contributed);
10438
+ } catch (err) {
10439
+ this.log?.error("SystemPromptContributor failed", err);
9735
10440
  }
9736
- };
9737
- events.emit("iteration.limit_reached", {
9738
- currentIterations,
9739
- currentLimit,
9740
- grant,
9741
- deny
9742
- });
9743
- if (autoExtend) {
9744
- setImmediate(() => {
9745
- if (!resolved) {
9746
- resolved = true;
9747
- clearTimeout(timer);
9748
- resolve4(100);
9749
- }
9750
- });
9751
10441
  }
9752
- });
9753
- }
9754
-
9755
- // src/core/streaming-response-builder.ts
9756
- function buildResponse(state) {
9757
- const content = [];
9758
- for (const b of state.blockOrder) {
9759
- if (b.kind === "text") {
9760
- const txt = state.textBuffers[b.idx] ?? "";
9761
- if (txt) content.push({ type: "text", text: txt });
9762
- } else if (b.kind === "thinking") {
9763
- const t2 = state.thinking[b.idx];
9764
- if (!t2) continue;
9765
- if (!t2.textBuf && !t2.signature) continue;
9766
- const block = { type: "thinking", thinking: t2.textBuf };
9767
- if (t2.signature) block.signature = t2.signature;
9768
- if (t2.providerMeta && Object.keys(t2.providerMeta).length > 0) {
9769
- block.providerMeta = t2.providerMeta;
9770
- }
9771
- content.push(block);
9772
- } else {
9773
- const tb = state.tools.get(b.id);
9774
- if (tb) {
9775
- const block = {
9776
- type: "tool_use",
9777
- id: b.id,
9778
- name: tb.name,
9779
- input: tb.input ?? {}
9780
- };
9781
- if (tb.providerMeta && Object.keys(tb.providerMeta).length > 0) {
9782
- block.providerMeta = tb.providerMeta;
9783
- }
9784
- content.push(block);
9785
- }
10442
+ return blocks;
10443
+ }
10444
+ /**
10445
+ * Returns the live array of contributors (readonly snapshot for
10446
+ * passing to DefaultSystemPromptBuilder at build time).
10447
+ */
10448
+ listSystemPromptContributors() {
10449
+ return this.promptContributors;
10450
+ }
10451
+ /**
10452
+ * Register an extension. Duplicate names are rejected.
10453
+ * Returns an unregister function.
10454
+ */
10455
+ register(ext) {
10456
+ if (this.extensions.some((e) => e.name === ext.name)) {
10457
+ throw new WrongStackError({
10458
+ message: `Extension "${ext.name}" already registered`,
10459
+ code: "REGISTRY_DUPLICATE",
10460
+ subsystem: "container",
10461
+ context: { extension: ext.name }
10462
+ });
9786
10463
  }
10464
+ this.extensions.push(ext);
10465
+ return () => this.unregister(ext.name);
9787
10466
  }
9788
- if (content.length === 0) content.push({ type: "text", text: "" });
9789
- return { content, stopReason: state.stopReason, usage: state.usage, model: state.model };
9790
- }
9791
- function createStreamingState(model) {
9792
- return {
9793
- model,
9794
- stopReason: "end_turn",
9795
- usage: { input: 0, output: 0 },
9796
- textBuffers: [],
9797
- currentTextIndex: -1,
9798
- tools: /* @__PURE__ */ new Map(),
9799
- thinking: [],
9800
- currentThinkingIndex: -1,
9801
- blockOrder: []
9802
- };
9803
- }
9804
- function handleMessageStart(state, model) {
9805
- state.model = model;
9806
- }
9807
- function handleContentBlockStart(state, ev) {
9808
- const kind = ev.kind ?? "text";
9809
- if (kind === "text") {
9810
- state.currentTextIndex = state.textBuffers.length;
9811
- state.textBuffers.push("");
9812
- state.blockOrder.push({ kind: "text", idx: state.currentTextIndex });
9813
- } else if (kind === "tool_use") {
9814
- const id = ev.id ?? crypto.randomUUID();
9815
- state.tools.set(id, { name: ev.name ?? "unknown", partial: "" });
9816
- state.blockOrder.push({ kind: "tool", id });
9817
- state.currentTextIndex = -1;
9818
- } else if (kind === "thinking") {
9819
- state.currentThinkingIndex = state.thinking.length;
9820
- state.thinking.push({
9821
- textBuf: "",
9822
- ...ev.providerMeta ? { providerMeta: ev.providerMeta } : {}
9823
- });
9824
- state.blockOrder.push({ kind: "thinking", idx: state.currentThinkingIndex });
9825
- state.currentTextIndex = -1;
10467
+ /**
10468
+ * Register an extension, silently replacing any previous registration
10469
+ * with the same name. Use this when overriding a default extension.
10470
+ */
10471
+ registerOrReplace(ext) {
10472
+ const idx = this.extensions.findIndex((e) => e.name === ext.name);
10473
+ if (idx >= 0) this.extensions.splice(idx, 1);
10474
+ return this.register(ext);
9826
10475
  }
9827
- }
9828
- function handleContentBlockStop(state, ev) {
9829
- }
9830
- function handleTextDelta(state, text) {
9831
- if (state.currentTextIndex === -1) {
9832
- state.currentTextIndex = state.textBuffers.length;
9833
- state.textBuffers.push("");
9834
- state.blockOrder.push({ kind: "text", idx: state.currentTextIndex });
10476
+ /**
10477
+ * Unregister an extension by name. Returns true if found.
10478
+ */
10479
+ unregister(name) {
10480
+ const idx = this.extensions.findIndex((e) => e.name === name);
10481
+ if (idx === -1) return false;
10482
+ this.extensions.splice(idx, 1);
10483
+ return true;
9835
10484
  }
9836
- state.textBuffers[state.currentTextIndex] = (state.textBuffers[state.currentTextIndex] ?? "") + text;
9837
- }
9838
- function handleToolUseStart(state, ev) {
9839
- state.currentTextIndex = -1;
9840
- state.tools.set(ev.id, { name: ev.name, partial: "" });
9841
- state.blockOrder.push({ kind: "tool", id: ev.id });
9842
- }
9843
- function handleToolUseInputDelta(state, ev) {
9844
- const t2 = state.tools.get(ev.id);
9845
- if (t2) t2.partial += ev.partial;
9846
- }
9847
- function safeJsonOrRaw(s) {
9848
- if (!s) return {};
9849
- try {
9850
- return JSON.parse(s);
9851
- } catch {
9852
- return { _raw: s };
10485
+ /**
10486
+ * List registered extension names in order.
10487
+ */
10488
+ list() {
10489
+ return this.extensions.map((e) => e.name);
9853
10490
  }
9854
- }
9855
- function handleToolUseStop(state, ev) {
9856
- const t2 = state.tools.get(ev.id);
9857
- if (t2) {
9858
- t2.input = ev.input !== void 0 ? ev.input : safeJsonOrRaw(t2.partial);
9859
- if (ev.providerMeta) t2.providerMeta = ev.providerMeta;
10491
+ /**
10492
+ * Check if an extension with the given name is registered.
10493
+ */
10494
+ has(name) {
10495
+ return this.extensions.some((e) => e.name === name);
9860
10496
  }
9861
- state.currentTextIndex = -1;
9862
- }
9863
- function handleThinkingStart(state, ev) {
9864
- state.currentThinkingIndex = state.thinking.length;
9865
- state.thinking.push({
9866
- textBuf: "",
9867
- ...ev.providerMeta ? { providerMeta: ev.providerMeta } : {}
9868
- });
9869
- state.blockOrder.push({ kind: "thinking", idx: state.currentThinkingIndex });
9870
- state.currentTextIndex = -1;
9871
- }
9872
- function handleThinkingDelta(state, text) {
9873
- if (state.currentThinkingIndex === -1) {
9874
- handleThinkingStart(state, {});
10497
+ /**
10498
+ * Remove all registered extensions and contributors.
10499
+ */
10500
+ clear() {
10501
+ this.extensions.length = 0;
10502
+ this.promptContributors.length = 0;
10503
+ }
10504
+ // ── Hook runners ─────────────────────────────────────────────────
10505
+ async runBeforeRun(...args) {
10506
+ const snapshot = [...this.extensions];
10507
+ for (const ext of snapshot) {
10508
+ if (!ext.beforeRun) continue;
10509
+ try {
10510
+ await ext.beforeRun(...args);
10511
+ } catch (err) {
10512
+ this.log?.error(`Extension "${ext.name}" beforeRun hook failed`, err);
10513
+ }
10514
+ }
9875
10515
  }
9876
- const t2 = state.thinking[state.currentThinkingIndex];
9877
- if (t2) t2.textBuf += text;
9878
- }
9879
- function handleThinkingSignature(state, signature) {
9880
- if (state.currentThinkingIndex === -1) {
9881
- handleThinkingStart(state, {});
10516
+ async runAfterRun(...args) {
10517
+ const snapshot = [...this.extensions];
10518
+ for (const ext of snapshot) {
10519
+ if (!ext.afterRun) continue;
10520
+ try {
10521
+ await ext.afterRun(...args);
10522
+ } catch (err) {
10523
+ this.log?.error(`Extension "${ext.name}" afterRun hook failed`, err);
10524
+ }
10525
+ }
9882
10526
  }
9883
- const t2 = state.thinking[state.currentThinkingIndex];
9884
- if (t2) t2.signature = signature;
9885
- }
9886
- function handleThinkingStop(state) {
9887
- state.currentThinkingIndex = -1;
9888
- }
9889
- function handleMessageStop(state, ev) {
9890
- state.stopReason = ev.stopReason ?? "end_turn";
9891
- state.usage = ev.usage ?? { input: 0, output: 0 };
9892
- }
9893
- async function streamProviderToResponse(provider, req, signal, ctx, events) {
9894
- const state = createStreamingState(req.model);
9895
- const iter = provider.stream(req, { signal })[Symbol.asyncIterator]();
9896
- try {
9897
- for (; ; ) {
9898
- const next = await iter.next();
9899
- if (next.done) break;
9900
- const ev = next.value;
9901
- switch (ev.type) {
9902
- case "message_start":
9903
- handleMessageStart(state, ev.model);
9904
- break;
9905
- case "content_block_start":
9906
- handleContentBlockStart(state, ev);
9907
- break;
9908
- case "content_block_stop":
9909
- handleContentBlockStop(state, ev);
9910
- break;
9911
- case "text_delta":
9912
- handleTextDelta(state, ev.text);
9913
- events.emit("provider.text_delta", { ctx, text: ev.text });
9914
- break;
9915
- case "tool_use_start":
9916
- handleToolUseStart(state, ev);
9917
- events.emit("provider.tool_use_start", { ctx, id: ev.id, name: ev.name });
9918
- break;
9919
- case "tool_use_input_delta":
9920
- handleToolUseInputDelta(state, ev);
9921
- break;
9922
- case "tool_use_stop":
9923
- handleToolUseStop(state, ev);
9924
- events.emit("provider.tool_use_stop", { ctx, id: ev.id });
9925
- break;
9926
- case "thinking_start":
9927
- handleThinkingStart(state, ev);
9928
- break;
9929
- case "thinking_delta":
9930
- handleThinkingDelta(state, ev.text);
9931
- events.emit("provider.thinking_delta", { ctx, text: ev.text });
9932
- break;
9933
- case "thinking_signature":
9934
- handleThinkingSignature(state, ev.signature);
9935
- break;
9936
- case "thinking_stop":
9937
- handleThinkingStop(state);
9938
- break;
9939
- case "message_stop":
9940
- handleMessageStop(state, ev);
9941
- break;
10527
+ async runBeforeIteration(...args) {
10528
+ const snapshot = [...this.extensions];
10529
+ for (const ext of snapshot) {
10530
+ if (!ext.beforeIteration) continue;
10531
+ try {
10532
+ await ext.beforeIteration(...args);
10533
+ } catch (err) {
10534
+ this.log?.error(`Extension "${ext.name}" beforeIteration hook failed`, err);
9942
10535
  }
9943
10536
  }
9944
- } catch (err) {
9945
- if (signal.aborted) {
9946
- state.stopReason = "end_turn";
9947
- return buildResponse(state);
10537
+ }
10538
+ async runAfterIteration(...args) {
10539
+ const snapshot = [...this.extensions];
10540
+ for (const ext of snapshot) {
10541
+ if (!ext.afterIteration) continue;
10542
+ try {
10543
+ await ext.afterIteration(...args);
10544
+ } catch (err) {
10545
+ this.log?.error(`Extension "${ext.name}" afterIteration hook failed`, err);
10546
+ }
9948
10547
  }
9949
- throw err;
9950
- } finally {
9951
- try {
9952
- let drainTimer = null;
10548
+ }
10549
+ /**
10550
+ * Run onError hooks in order. The first hook that returns a non-void
10551
+ * result wins; subsequent hooks are skipped.
10552
+ */
10553
+ async runOnError(...args) {
10554
+ const snapshot = [...this.extensions];
10555
+ for (const ext of snapshot) {
10556
+ if (!ext.onError) continue;
9953
10557
  try {
9954
- await Promise.race([
9955
- Promise.resolve(iter.return?.()),
9956
- new Promise((resolve4) => {
9957
- drainTimer = setTimeout(resolve4, 500);
9958
- })
9959
- ]);
9960
- } finally {
9961
- if (drainTimer) clearTimeout(drainTimer);
10558
+ const result = await ext.onError(...args);
10559
+ if (result) return result;
10560
+ } catch (err) {
10561
+ this.log?.error(`Extension "${ext.name}" onError hook failed`, err);
9962
10562
  }
9963
- } catch {
9964
10563
  }
9965
10564
  }
9966
- return buildResponse(state);
9967
- }
9968
-
9969
- // src/core/provider-runner.ts
9970
- async function runProviderWithRetry(opts) {
9971
- const { provider, request, signal, ctx, events, retry, logger, tracer } = opts;
9972
- let attempt = 0;
9973
- for (; ; ) {
9974
- const span = tracer?.startSpan("provider.complete", {
9975
- "provider.id": provider.id,
9976
- "provider.model": request.model,
9977
- "provider.streaming": provider.capabilities.streaming,
9978
- "provider.attempt": attempt
9979
- });
9980
- try {
9981
- const res = provider.capabilities.streaming ? await streamProviderToResponse(provider, request, signal, ctx, events) : await provider.complete(request, { signal });
9982
- span?.setAttribute("provider.stopReason", res.stopReason);
9983
- span?.setAttribute("provider.usage_in", res.usage.input);
9984
- span?.setAttribute("provider.usage_out", res.usage.output);
9985
- span?.end();
9986
- return res;
9987
- } catch (err) {
9988
- if (err instanceof Error) span?.recordError(err);
9989
- span?.end();
9990
- if (signal.aborted) throw err;
9991
- const isProviderErr = err instanceof ProviderError;
9992
- const errAsErr = err instanceof Error ? err : new Error(String(err));
9993
- const canRetry = retry.shouldRetry(isProviderErr ? err : errAsErr, attempt);
9994
- const description = isProviderErr ? err.describe() : errAsErr.message;
9995
- if (!canRetry) {
9996
- if (isProviderErr) {
9997
- events.emit("provider.error", {
9998
- providerId: err.providerId,
9999
- status: err.status,
10000
- description,
10001
- retryable: false
10002
- });
10565
+ /**
10566
+ * Build a composed provider runner. Extensions with `wrapProviderRunner`
10567
+ * form a middleware-style chain: the innermost extension wraps the
10568
+ * default runner, each subsequent wrapper wraps the previous.
10569
+ */
10570
+ wrapProviderRunner(inner) {
10571
+ const wrappers = this.extensions.filter((e) => e.wrapProviderRunner).map((e) => ({ name: e.name, wrap: e.wrapProviderRunner }));
10572
+ if (wrappers.length === 0) return inner;
10573
+ let composed = inner;
10574
+ for (let i = wrappers.length - 1; i >= 0; i--) {
10575
+ const wrapper = wrappers[i];
10576
+ const next = composed;
10577
+ composed = async (ctx, req) => {
10578
+ try {
10579
+ return await wrapper.wrap(ctx, req, next);
10580
+ } catch (err) {
10581
+ this.log?.error(`Extension "${wrapper.name}" wrapProviderRunner failed`, err);
10582
+ throw err;
10003
10583
  }
10004
- throw err;
10584
+ };
10585
+ }
10586
+ return composed;
10587
+ }
10588
+ async runBeforeToolExecution(...args) {
10589
+ let toolUses = args[1];
10590
+ const snapshot = [...this.extensions];
10591
+ for (const ext of snapshot) {
10592
+ if (!ext.beforeToolExecution) continue;
10593
+ try {
10594
+ toolUses = await ext.beforeToolExecution(args[0], toolUses);
10595
+ } catch (err) {
10596
+ this.log?.error(`Extension "${ext.name}" beforeToolExecution hook failed`, err);
10005
10597
  }
10006
- const delay = Math.round(retry.delayMs(attempt));
10007
- const attemptNum = attempt + 1;
10008
- logger.warn(`Provider retry ${attemptNum} in ${delay}ms \u2014 ${description}`);
10009
- if (isProviderErr) {
10010
- events.emit("provider.retry", {
10011
- providerId: err.providerId,
10012
- attempt: attemptNum,
10013
- delayMs: delay,
10014
- status: err.status,
10015
- description
10016
- });
10598
+ }
10599
+ return toolUses;
10600
+ }
10601
+ async runAfterToolExecution(...args) {
10602
+ const snapshot = [...this.extensions];
10603
+ for (const ext of snapshot) {
10604
+ if (!ext.afterToolExecution) continue;
10605
+ try {
10606
+ await ext.afterToolExecution(...args);
10607
+ } catch (err) {
10608
+ this.log?.error(`Extension "${ext.name}" afterToolExecution hook failed`, err);
10017
10609
  }
10018
- await new Promise((resolve4, reject) => {
10019
- let settled = false;
10020
- const onAbort = () => {
10021
- if (settled) return;
10022
- settled = true;
10023
- clearTimeout(t2);
10024
- reject(new Error("aborted"));
10025
- };
10026
- const t2 = setTimeout(() => {
10027
- if (settled) return;
10028
- settled = true;
10029
- clearTimeout(t2);
10030
- signal.removeEventListener("abort", onAbort);
10031
- resolve4();
10032
- }, delay);
10033
- if (signal.aborted) {
10034
- onAbort();
10035
- return;
10610
+ }
10611
+ }
10612
+ };
10613
+
10614
+ // src/core/iteration-limit.ts
10615
+ function requestLimitExtension(opts) {
10616
+ const { events, currentIterations, currentLimit, autoExtend, timeoutMs = 3e4 } = opts;
10617
+ return new Promise((resolve4) => {
10618
+ let resolved = false;
10619
+ const timer = setTimeout(() => {
10620
+ if (!resolved) {
10621
+ resolved = true;
10622
+ resolve4(0);
10623
+ }
10624
+ }, timeoutMs);
10625
+ const deny = () => {
10626
+ if (!resolved) {
10627
+ resolved = true;
10628
+ clearTimeout(timer);
10629
+ resolve4(0);
10630
+ }
10631
+ };
10632
+ const grant = (extra) => {
10633
+ if (!resolved) {
10634
+ resolved = true;
10635
+ clearTimeout(timer);
10636
+ resolve4(Math.max(0, extra));
10637
+ }
10638
+ };
10639
+ events.emit("iteration.limit_reached", {
10640
+ currentIterations,
10641
+ currentLimit,
10642
+ grant,
10643
+ deny
10644
+ });
10645
+ if (autoExtend) {
10646
+ setImmediate(() => {
10647
+ if (!resolved) {
10648
+ resolved = true;
10649
+ clearTimeout(timer);
10650
+ resolve4(100);
10036
10651
  }
10037
- signal.addEventListener("abort", onAbort, { once: true });
10038
10652
  });
10039
- attempt++;
10040
10653
  }
10041
- }
10654
+ });
10042
10655
  }
10043
10656
 
10044
10657
  // src/core/agent.ts
@@ -10075,6 +10688,7 @@ var Agent = class {
10075
10688
  toolExecutor;
10076
10689
  autoExtendLimit;
10077
10690
  tracer;
10691
+ extensions;
10078
10692
  constructor(init) {
10079
10693
  this.container = init.container;
10080
10694
  this.tools = init.tools;
@@ -10088,6 +10702,8 @@ var Agent = class {
10088
10702
  this.perIterationOutputCapBytes = init.perIterationOutputCapBytes ?? 1e5;
10089
10703
  this.autoExtendLimit = init.autoExtendLimit ?? true;
10090
10704
  this.tracer = init.tracer;
10705
+ this.extensions = init.extensions ?? new ExtensionRegistry();
10706
+ this.extensions.setLogger(this.container.resolve(TOKENS.Logger));
10091
10707
  this.toolExecutor = new ToolExecutor(this.tools, {
10092
10708
  permissionPolicy: init.permissionPolicy ?? this.permission,
10093
10709
  secretScrubber: this.scrubber,
@@ -10133,33 +10749,66 @@ var Agent = class {
10133
10749
  "agent.model": opts.model ?? this.ctx.model,
10134
10750
  "agent.executionStrategy": opts.executionStrategy ?? this.executionStrategy
10135
10751
  });
10752
+ const { blocks, text } = normalizeInput(userInput);
10753
+ const inputPayload = { content: blocks, text, ctx: this.ctx };
10754
+ await this.extensions.runBeforeRun(this.ctx, inputPayload);
10136
10755
  try {
10137
- const result = await this.runInner(userInput, opts, controller);
10756
+ const result = await this.runInner(inputPayload, opts, controller);
10138
10757
  span?.setAttribute("agent.status", result.status);
10139
10758
  span?.setAttribute("agent.iterations", result.iterations);
10759
+ await this.extensions.runAfterRun(this.ctx, result);
10140
10760
  return result;
10141
10761
  } catch (err) {
10142
10762
  const wse = err instanceof AgentError ? err : toWrongStackError(err);
10143
10763
  this.events.emit("error", { err: toError(err), phase: "agent" });
10144
10764
  if (err instanceof Error) span?.recordError(err);
10145
10765
  span?.setAttribute("agent.status", "failed");
10146
- return {
10766
+ const result = {
10147
10767
  status: signal.aborted ? "aborted" : "failed",
10148
10768
  iterations: 0,
10149
10769
  error: wse
10150
10770
  };
10771
+ await this.extensions.runAfterRun(this.ctx, result);
10772
+ return result;
10151
10773
  } finally {
10152
10774
  span?.end();
10153
10775
  await controller.dispose();
10154
10776
  }
10155
10777
  }
10156
- async runInner(userInput, opts, controller) {
10157
- await this.normalizeAndEmitUserInput(userInput);
10778
+ async runInner(inputPayload, opts, controller) {
10779
+ await this.pipelines.userInput.run(inputPayload);
10780
+ this.ctx.state.appendMessage({ role: "user", content: inputPayload.content });
10781
+ await this.ctx.session.append({
10782
+ type: "user_input",
10783
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
10784
+ content: inputPayload.content
10785
+ });
10158
10786
  let finalText = "";
10159
10787
  let iterations = 0;
10160
10788
  let effectiveLimit = opts.maxIterations ?? this.maxIterations;
10161
10789
  const hasHardLimit = effectiveLimit > 0 && Number.isFinite(effectiveLimit);
10162
10790
  let recoveryRetries = 0;
10791
+ const diRunner = this.container.has(TOKENS.ProviderRunner) ? this.container.resolve(TOKENS.ProviderRunner) : null;
10792
+ const baseRunner = diRunner ? (ctx, req) => diRunner.run({
10793
+ provider: ctx.provider,
10794
+ request: req,
10795
+ signal: controller.signal,
10796
+ ctx,
10797
+ events: this.events,
10798
+ retry: this.retry,
10799
+ logger: this.logger,
10800
+ tracer: this.tracer
10801
+ }) : async (ctx, req) => runProviderWithRetry({
10802
+ provider: ctx.provider,
10803
+ request: req,
10804
+ signal: controller.signal,
10805
+ ctx,
10806
+ events: this.events,
10807
+ retry: this.retry,
10808
+ logger: this.logger,
10809
+ tracer: this.tracer
10810
+ });
10811
+ const customRunner = this.extensions.wrapProviderRunner(baseRunner);
10163
10812
  for (let i = 0; ; i++) {
10164
10813
  iterations = i + 1;
10165
10814
  if (controller.signal.aborted) {
@@ -10175,26 +10824,39 @@ var Agent = class {
10175
10824
  if (limitCheck.exit) {
10176
10825
  return { ...limitCheck.exit, finalText };
10177
10826
  }
10827
+ await this.extensions.runBeforeIteration(this.ctx, i);
10178
10828
  this.events.emit("iteration.started", { ctx: this.ctx, index: i });
10179
10829
  const req = await this.buildAndRunRequestPipeline(opts);
10180
10830
  let res;
10181
10831
  try {
10182
- res = await runProviderWithRetry({
10183
- provider: this.ctx.provider,
10184
- request: req,
10185
- signal: controller.signal,
10186
- ctx: this.ctx,
10187
- events: this.events,
10188
- retry: this.retry,
10189
- logger: this.logger,
10190
- tracer: this.tracer
10191
- });
10832
+ res = await customRunner(this.ctx, req);
10192
10833
  recoveryRetries = 0;
10193
10834
  } catch (err) {
10194
10835
  if (controller.signal.aborted) {
10195
10836
  this.events.emit("error", { err: toError(err), phase: "provider" });
10196
10837
  return { status: "aborted", iterations, error: toWrongStackError(err, "AGENT_ABORTED") };
10197
10838
  }
10839
+ const extDecision = await this.extensions.runOnError(this.ctx, err, "provider", i);
10840
+ if (extDecision) {
10841
+ if (extDecision.action === "fail") {
10842
+ this.events.emit("error", { err: toError(err), phase: "provider" });
10843
+ return { status: "failed", iterations, error: toWrongStackError(err) };
10844
+ }
10845
+ if (extDecision.action === "continue") {
10846
+ await this.extensions.runAfterIteration(this.ctx, i);
10847
+ continue;
10848
+ }
10849
+ if (extDecision.action === "retry") {
10850
+ recoveryRetries++;
10851
+ if (recoveryRetries > 2) {
10852
+ this.events.emit("error", { err: toError(err), phase: "provider" });
10853
+ return { status: "failed", iterations, error: toWrongStackError(err) };
10854
+ }
10855
+ if (extDecision.model) this.ctx.model = extDecision.model;
10856
+ this.logger.info("Extension requested retry; retrying turn");
10857
+ continue;
10858
+ }
10859
+ }
10198
10860
  const recovered = await this.errorHandler.recover(err, this.ctx);
10199
10861
  if (!recovered || recovered.action === "fail") {
10200
10862
  this.events.emit("error", { err: toError(err), phase: "provider" });
@@ -10233,21 +10895,9 @@ var Agent = class {
10233
10895
  await this.executeTools(toolUses);
10234
10896
  this.events.emit("iteration.completed", { ctx: this.ctx, index: i });
10235
10897
  await this.compactContextIfNeeded();
10898
+ await this.extensions.runAfterIteration(this.ctx, i);
10236
10899
  }
10237
10900
  }
10238
- /**
10239
- * Normalize user input and emit through userInput pipeline + session append.
10240
- */
10241
- async normalizeAndEmitUserInput(userInput) {
10242
- const { blocks, text } = normalizeInput(userInput);
10243
- await this.pipelines.userInput.run({ content: blocks, text, ctx: this.ctx });
10244
- this.ctx.state.appendMessage({ role: "user", content: blocks });
10245
- await this.ctx.session.append({
10246
- type: "user_input",
10247
- ts: (/* @__PURE__ */ new Date()).toISOString(),
10248
- content: blocks
10249
- });
10250
- }
10251
10901
  /**
10252
10902
  * Check if iteration limit has been reached and request extension if needed.
10253
10903
  * Returns the new effective limit (possibly extended) and a RunResult if
@@ -10275,6 +10925,17 @@ var Agent = class {
10275
10925
  * Build request and run through request pipeline.
10276
10926
  */
10277
10927
  async buildAndRunRequestPipeline(opts) {
10928
+ const repaired = repairToolUseAdjacency(this.ctx.messages);
10929
+ if (repaired.report.changed) {
10930
+ this.ctx.state.replaceMessages(repaired.messages);
10931
+ this.events.emit("context.repaired", {
10932
+ ctx: this.ctx,
10933
+ ...repaired.report
10934
+ });
10935
+ this.logger.warn(
10936
+ `Repaired context tool adjacency: removed ${repaired.report.removedToolUses.length} tool_use block(s), ${repaired.report.removedToolResults.length} tool_result block(s), ${repaired.report.removedMessages} empty message(s)`
10937
+ );
10938
+ }
10278
10939
  const baseReq = {
10279
10940
  model: opts.model ?? this.ctx.model,
10280
10941
  system: this.ctx.systemPrompt,
@@ -10331,12 +10992,13 @@ var Agent = class {
10331
10992
  * single tool.
10332
10993
  */
10333
10994
  async executeTools(toolUses) {
10995
+ const selectedToolUses = await this.extensions.runBeforeToolExecution(this.ctx, toolUses);
10334
10996
  const { outputs } = await this.toolExecutor.executeBatch(
10335
- toolUses,
10997
+ selectedToolUses,
10336
10998
  this.ctx,
10337
10999
  this.executionStrategy
10338
11000
  );
10339
- const useById = new Map(toolUses.map((u) => [u.id, u]));
11001
+ const useById = new Map(selectedToolUses.map((u) => [u.id, u]));
10340
11002
  for (const { result, tool, durationMs } of outputs) {
10341
11003
  if (result.type === "tool_confirm_pending") {
10342
11004
  const decision = await this.waitForConfirm({
@@ -10416,6 +11078,7 @@ var Agent = class {
10416
11078
  role: "user",
10417
11079
  content: outputs.map((o) => o.result)
10418
11080
  });
11081
+ await this.extensions.runAfterToolExecution(this.ctx, outputs);
10419
11082
  }
10420
11083
  waitForConfirm(info) {
10421
11084
  return new Promise((resolve4) => {
@@ -10870,6 +11533,15 @@ var DefaultSystemPromptBuilder = class {
10870
11533
  cache_control: { type: "ephemeral" }
10871
11534
  });
10872
11535
  }
11536
+ if (this.opts.contributors && this.opts.contributors.length > 0) {
11537
+ for (const c of this.opts.contributors) {
11538
+ try {
11539
+ const contributed = await c(ctx);
11540
+ blocks.push(...contributed);
11541
+ } catch {
11542
+ }
11543
+ }
11544
+ }
10873
11545
  return blocks;
10874
11546
  }
10875
11547
  /**
@@ -10911,12 +11583,38 @@ var DefaultSystemPromptBuilder = class {
10911
11583
  }
10912
11584
  buildToolUsage(tools) {
10913
11585
  if (tools.length === 0) return "## Tool usage\n\nNo tools registered.";
10914
- const lines = ["## Tool usage"];
11586
+ const byCat = /* @__PURE__ */ new Map();
11587
+ const uncategorized = [];
10915
11588
  for (const t2 of tools) {
10916
- const hint = t2.usageHint ?? t2.description;
11589
+ if (t2.category) {
11590
+ let group = byCat.get(t2.category);
11591
+ if (!group) {
11592
+ group = [];
11593
+ byCat.set(t2.category, group);
11594
+ }
11595
+ group.push(t2);
11596
+ } else {
11597
+ uncategorized.push(t2);
11598
+ }
11599
+ }
11600
+ const lines = ["## Tool usage"];
11601
+ for (const [cat, catTools] of byCat) {
10917
11602
  lines.push(`
11603
+ ### ${cat}`);
11604
+ for (const t2 of catTools) {
11605
+ const hint = t2.usageHint ?? t2.description;
11606
+ const desc = hint.length > 80 ? `${hint.slice(0, 77)}...` : hint.trim();
11607
+ lines.push(`- **${t2.name}** \u2014 ${desc}`);
11608
+ }
11609
+ }
11610
+ if (uncategorized.length > 0) {
11611
+ if (byCat.size > 0) lines.push("");
11612
+ for (const t2 of uncategorized) {
11613
+ const hint = t2.usageHint ?? t2.description;
11614
+ lines.push(`
10918
11615
  ### ${t2.name}
10919
11616
  ${hint.trim()}`);
11617
+ }
10920
11618
  }
10921
11619
  lines.push(`
10922
11620
  ## Common patterns
@@ -11188,6 +11886,21 @@ var ToolRegistry = class {
11188
11886
  this.tools.set(tool.name, { tool, owner });
11189
11887
  return true;
11190
11888
  }
11889
+ /**
11890
+ * Bulk-register multiple tools at once. Each tool that conflicts with an
11891
+ * existing registration is silently skipped — use `registerAllOrThrow`
11892
+ * if you want it to throw on conflicts.
11893
+ */
11894
+ registerAll(tools, owner = "core") {
11895
+ for (const tool of tools) this.tryRegister(tool, owner);
11896
+ }
11897
+ /**
11898
+ * Bulk-register and throw on the first conflict. Use when you need
11899
+ * strict registration (e.g. at boot time).
11900
+ */
11901
+ registerAllOrThrow(tools, owner = "core") {
11902
+ for (const tool of tools) this.register(tool, owner);
11903
+ }
11191
11904
  /**
11192
11905
  * Register a tool as a default. If the tool name is already registered,
11193
11906
  * this is a no-op — the existing registration (from core or another
@@ -11215,6 +11928,30 @@ var ToolRegistry = class {
11215
11928
  }
11216
11929
  this.tools.set(name, { tool, owner });
11217
11930
  }
11931
+ /**
11932
+ * Wrap (decorate) an existing tool. The wrapper receives the current
11933
+ * tool and must return a new tool — typically the same tool with a
11934
+ * wrapped `execute` or `executeStream`. Throws if the tool is not
11935
+ * registered.
11936
+ *
11937
+ * Multiple wraps stack: each wrapper gets the output of the previous.
11938
+ *
11939
+ * @example
11940
+ * registry.wrap('bash', (t) => ({ ...t, permission: 'confirm' }));
11941
+ */
11942
+ wrap(name, wrapper, owner = "core") {
11943
+ const entry = this.tools.get(name);
11944
+ if (!entry) {
11945
+ throw new WrongStackError({
11946
+ message: `Tool "${name}" not registered; cannot wrap`,
11947
+ code: "REGISTRY_NOT_FOUND",
11948
+ subsystem: "container",
11949
+ context: { tool: name }
11950
+ });
11951
+ }
11952
+ const wrapped = wrapper(entry.tool);
11953
+ this.tools.set(name, { tool: wrapped, owner: `${entry.owner}+${owner}` });
11954
+ }
11218
11955
  get(name) {
11219
11956
  return this.tools.get(name)?.tool;
11220
11957
  }
@@ -11224,6 +11961,24 @@ var ToolRegistry = class {
11224
11961
  list() {
11225
11962
  return Array.from(this.tools.values()).map((e) => e.tool);
11226
11963
  }
11964
+ /**
11965
+ * Group tools by their `category` field. Tools without a category
11966
+ * are placed under the key `""` (empty string). Returns a Map of
11967
+ * category → tools, sorted by registration order within each category.
11968
+ */
11969
+ listByCategory() {
11970
+ const map = /* @__PURE__ */ new Map();
11971
+ for (const { tool } of this.tools.values()) {
11972
+ const cat = tool.category ?? "";
11973
+ let group = map.get(cat);
11974
+ if (!group) {
11975
+ group = [];
11976
+ map.set(cat, group);
11977
+ }
11978
+ group.push(tool);
11979
+ }
11980
+ return map;
11981
+ }
11227
11982
  listWithOwner() {
11228
11983
  return Array.from(this.tools.values());
11229
11984
  }
@@ -11243,6 +11998,12 @@ var ProviderRegistry = class {
11243
11998
  register(f) {
11244
11999
  this.factories.set(f.type, f);
11245
12000
  }
12001
+ /**
12002
+ * Bulk-register multiple provider factories at once.
12003
+ */
12004
+ registerAll(factories) {
12005
+ for (const f of factories) this.register(f);
12006
+ }
11246
12007
  /**
11247
12008
  * Override an existing factory. Throws if no factory is registered
11248
12009
  * for the given type. Use this to safely replace a provider at runtime
@@ -11311,6 +12072,12 @@ var SlashCommandRegistry = class {
11311
12072
  }
11312
12073
  return this.cmds.delete(name);
11313
12074
  }
12075
+ /**
12076
+ * Bulk-register multiple slash commands at once.
12077
+ */
12078
+ registerAll(cmds, owner = "core") {
12079
+ for (const cmd of cmds) this.register(cmd, owner);
12080
+ }
11314
12081
  get(name) {
11315
12082
  return this.cmds.get(name)?.cmd;
11316
12083
  }
@@ -11383,15 +12150,23 @@ var DefaultPluginAPI = class {
11383
12150
  providers;
11384
12151
  mcp;
11385
12152
  slashCommands;
12153
+ extensions;
12154
+ session;
12155
+ metrics;
11386
12156
  config;
11387
12157
  log;
12158
+ configStore;
11388
12159
  pluginCleanupFns = [];
11389
12160
  constructor(init) {
11390
12161
  const owner = init.ownerName;
11391
12162
  this.container = init.container;
11392
12163
  this.events = init.events;
11393
12164
  this.config = init.config;
12165
+ this.configStore = init.configStore;
11394
12166
  this.log = init.log.child({ plugin: owner });
12167
+ this.extensions = init.extensions ?? new ExtensionRegistry();
12168
+ this.session = init.sessionWriter ?? noopSession;
12169
+ this.metrics = init.metricsSink ? scopedMetrics(init.metricsSink, owner) : noopMetrics;
11395
12170
  const pipelines = init.pipelines;
11396
12171
  const readonlyPipelines = {};
11397
12172
  for (const [key, pipeline] of Object.entries(pipelines)) {
@@ -11402,6 +12177,7 @@ var DefaultPluginAPI = class {
11402
12177
  this.tools = {
11403
12178
  register: (t2) => tr.register(t2, owner),
11404
12179
  unregister: (name) => tr.unregister(name),
12180
+ wrap: (name, wrapper) => tr.wrap(name, wrapper, owner),
11405
12181
  get: (name) => tr.get(name),
11406
12182
  list: () => tr.list()
11407
12183
  };
@@ -11421,10 +12197,23 @@ var DefaultPluginAPI = class {
11421
12197
  } : noopSlashCommands;
11422
12198
  }
11423
12199
  onEvent(event, handler) {
11424
- const off = this.events.once(event, handler);
12200
+ const off = this.events.on(event, handler);
11425
12201
  this.pluginCleanupFns.push(off);
11426
12202
  return off;
11427
12203
  }
12204
+ onPattern(pattern, handler) {
12205
+ const off = this.events.onPattern(pattern, handler);
12206
+ this.pluginCleanupFns.push(off);
12207
+ return off;
12208
+ }
12209
+ emitCustom(event, payload) {
12210
+ this.events.emit(event, payload);
12211
+ }
12212
+ onConfigChange(handler) {
12213
+ if (!this.configStore) return () => {
12214
+ };
12215
+ return this.configStore.watch(handler);
12216
+ }
11428
12217
  /** Called by the plugin loader when uninstalling the plugin. */
11429
12218
  drainCleanup() {
11430
12219
  for (const fn of this.pluginCleanupFns.splice(0)) {
@@ -11434,6 +12223,9 @@ var DefaultPluginAPI = class {
11434
12223
  }
11435
12224
  }
11436
12225
  }
12226
+ registerSystemPromptContributor(c) {
12227
+ return this.extensions.registerSystemPromptContributor(c);
12228
+ }
11437
12229
  };
11438
12230
  var noopMcp = {
11439
12231
  start: async () => void 0,
@@ -11454,8 +12246,35 @@ var noopSlashCommands = {
11454
12246
  return [];
11455
12247
  }
11456
12248
  };
12249
+ var noopSession = {
12250
+ append: async () => {
12251
+ }
12252
+ };
12253
+ var noopMetrics = {
12254
+ counter() {
12255
+ },
12256
+ histogram() {
12257
+ },
12258
+ gauge() {
12259
+ }
12260
+ };
12261
+ function scopedMetrics(sink, pluginName) {
12262
+ const prefix = `plugin.${pluginName}.`;
12263
+ return {
12264
+ counter(name, value, labels) {
12265
+ sink.counter(`${prefix}${name}`, value, labels);
12266
+ },
12267
+ histogram(name, value, labels) {
12268
+ sink.histogram(`${prefix}${name}`, value, labels);
12269
+ },
12270
+ gauge(name, value, labels) {
12271
+ sink.gauge(`${prefix}${name}`, value, labels);
12272
+ }
12273
+ };
12274
+ }
11457
12275
 
11458
12276
  // src/plugin/loader.ts
12277
+ var pluginApiMap = /* @__PURE__ */ new WeakMap();
11459
12278
  var KERNEL_API_VERSION = "0.1.10";
11460
12279
  function parseSemver(v) {
11461
12280
  const parts = v.replace(/^[^0-9]*/, "").split(".").map((s) => Number.parseInt(s, 10) || 0);
@@ -11479,6 +12298,16 @@ function satisfies(range, version) {
11479
12298
  function normalizeDep(d) {
11480
12299
  return typeof d === "string" ? { name: d } : d;
11481
12300
  }
12301
+ function shallowMerge(defaults, overrides) {
12302
+ if (overrides === void 0 || overrides === null) return { ...defaults };
12303
+ if (typeof overrides !== "object") return { ...defaults };
12304
+ const ov = overrides;
12305
+ const out = { ...defaults };
12306
+ for (const key of Object.keys(ov)) {
12307
+ out[key] = ov[key];
12308
+ }
12309
+ return out;
12310
+ }
11482
12311
  function topoSort(plugins) {
11483
12312
  const map = /* @__PURE__ */ new Map();
11484
12313
  for (const p of plugins) map.set(p.name, p);
@@ -11574,6 +12403,11 @@ async function loadPlugins(plugins, opts) {
11574
12403
  failed.push({ plugin, err });
11575
12404
  continue;
11576
12405
  }
12406
+ if (plugin.defaultConfig && opts.pluginOptions) {
12407
+ const userOpts = opts.pluginOptions[plugin.name];
12408
+ const merged = shallowMerge(plugin.defaultConfig, userOpts);
12409
+ opts.pluginOptions[plugin.name] = merged;
12410
+ }
11577
12411
  if (plugin.configSchema && opts.pluginOptions) {
11578
12412
  const pluginOpts = opts.pluginOptions[plugin.name];
11579
12413
  if (pluginOpts !== void 0) {
@@ -11595,8 +12429,9 @@ async function loadPlugins(plugins, opts) {
11595
12429
  }
11596
12430
  try {
11597
12431
  const rawApi = opts.apiFactory(plugin);
11598
- const api = plugin.capabilities ? wrapApiForCapabilityCheck(plugin, rawApi, opts.log) : rawApi;
12432
+ const api = plugin.capabilities ? wrapApiForCapabilityCheck(plugin, rawApi, opts.log, opts.enforceCapabilities) : rawApi;
11599
12433
  await plugin.setup(api);
12434
+ pluginApiMap.set(plugin, api);
11600
12435
  loaded.push(plugin);
11601
12436
  opts.log.info(`Plugin "${plugin.name}" loaded`);
11602
12437
  } catch (err) {
@@ -11611,18 +12446,27 @@ async function unloadPlugins(loadedPlugins, opts) {
11611
12446
  for (const plugin of ordered) {
11612
12447
  if (typeof plugin.teardown !== "function") continue;
11613
12448
  try {
11614
- const api = opts.apiFactory(plugin);
12449
+ const api = pluginApiMap.get(plugin) ?? opts.apiFactory(plugin);
11615
12450
  await plugin.teardown(api);
12451
+ pluginApiMap.delete(plugin);
11616
12452
  opts.log.info(`Plugin "${plugin.name}" torn down`);
11617
12453
  } catch (err) {
11618
12454
  opts.log.error(`Plugin "${plugin.name}" teardown failed`, err);
11619
12455
  }
11620
12456
  }
11621
12457
  }
11622
- function wrapApiForCapabilityCheck(plugin, api, log) {
12458
+ function wrapApiForCapabilityCheck(plugin, api, log, enforce = false) {
11623
12459
  const caps = plugin.capabilities ?? {};
11624
- const warn = (subsystem, detail) => {
12460
+ const violate = (subsystem, detail) => {
11625
12461
  const msg = `Plugin "${plugin.name}" used ${subsystem} without declaring capabilities.${subsystem} \u2014 ${detail}`;
12462
+ if (enforce) {
12463
+ throw new PluginError({
12464
+ message: msg,
12465
+ code: "PLUGIN_LOAD_FAILED",
12466
+ pluginName: plugin.name,
12467
+ context: { subsystem, detail }
12468
+ });
12469
+ }
11626
12470
  if (typeof log.warn === "function") log.warn(msg);
11627
12471
  else log.error(msg);
11628
12472
  };
@@ -11630,7 +12474,7 @@ function wrapApiForCapabilityCheck(plugin, api, log) {
11630
12474
  get(target, prop, receiver) {
11631
12475
  if (prop === "register") {
11632
12476
  return (t2) => {
11633
- warn("tools", `register(${t2?.name ?? "<unknown>"})`);
12477
+ violate("tools", `register(${t2?.name ?? "<unknown>"})`);
11634
12478
  return target.register(t2);
11635
12479
  };
11636
12480
  }
@@ -11641,7 +12485,7 @@ function wrapApiForCapabilityCheck(plugin, api, log) {
11641
12485
  get(target, prop, receiver) {
11642
12486
  if (prop === "register") {
11643
12487
  return (f) => {
11644
- warn("providers", `register(${f?.type ?? "<unknown>"})`);
12488
+ violate("providers", `register(${f?.type ?? "<unknown>"})`);
11645
12489
  return target.register(f);
11646
12490
  };
11647
12491
  }
@@ -11652,7 +12496,10 @@ function wrapApiForCapabilityCheck(plugin, api, log) {
11652
12496
  get(target, prop, receiver) {
11653
12497
  if (prop === "register") {
11654
12498
  return (c) => {
11655
- warn("slashCommands", `register(${c?.name ?? "<unknown>"})`);
12499
+ violate(
12500
+ "slashCommands",
12501
+ `register(${c?.name ?? "<unknown>"})`
12502
+ );
11656
12503
  return target.register(c);
11657
12504
  };
11658
12505
  }
@@ -11663,7 +12510,7 @@ function wrapApiForCapabilityCheck(plugin, api, log) {
11663
12510
  get(target, prop, receiver) {
11664
12511
  if (prop === "start") {
11665
12512
  return (cfg) => {
11666
- warn("mcp", `start(${cfg?.name ?? "<unknown>"})`);
12513
+ violate("mcp", `start(${cfg?.name ?? "<unknown>"})`);
11667
12514
  return target.start(cfg);
11668
12515
  };
11669
12516
  }
@@ -11688,6 +12535,6 @@ function wrapApiForCapabilityCheck(plugin, api, log) {
11688
12535
  });
11689
12536
  }
11690
12537
 
11691
- export { ALL_FLEET_AGENTS, AUDIT_LOG_AGENT, Agent, AgentError, AutoApprovePermissionPolicy, AutoCompactionMiddleware, AutonomousRunner, BUG_HUNTER_AGENT, BudgetExceededError, ConfigError, ConfigMigrationError, Container, Context, ConversationState, DEFAULT_CONFIG_MIGRATIONS, DEFAULT_DIRECTOR_PREAMBLE, DEFAULT_MAX_ITERATIONS, DEFAULT_MODES, DEFAULT_RECOVERY_STRATEGIES, DEFAULT_SPEC_TEMPLATE, DEFAULT_SUBAGENT_BASELINE, DefaultAttachmentStore, DefaultConfigLoader, DefaultConfigStore, DefaultErrorHandler, DefaultHealthRegistry, DefaultLogger, DefaultMemoryStore, DefaultModeStore, DefaultModelsRegistry, DefaultMultiAgentCoordinator, DefaultPathResolver, DefaultPermissionPolicy, DefaultPluginAPI, DefaultRetryPolicy, DefaultSecretScrubber, DefaultSecretVault, DefaultSessionReader, DefaultSessionStore, DefaultSkillLoader, DefaultSystemPromptBuilder, DefaultTaskStore, DefaultTokenCounter, Director, DirectorBudgetError, DirectorStateCheckpoint, DoneConditionChecker, EventBus, FLEET_ROSTER, FleetBus, FleetUsageAggregator, HybridCompactor, InMemoryAgentBridge, InMemoryBridgeTransport, InMemoryMetricsSink, InputBuilder, IntelligentCompactor, KERNEL_API_VERSION, LAYER_1_IDENTITY, LLMSelector, NoopMetricsSink, NoopTracer, OTelTracer, PROMETHEUS_CONTENT_TYPE, Pipeline, PluginError, ProviderError, ProviderRegistry, QueueStore, REFACTOR_PLANNER_AGENT, RecoveryLock, RunController, SECURITY_SCANNER_AGENT, SelectiveCompactor, SessionAnalyzer, SessionError, SlashCommandRegistry, SpecDrivenDev, SpecParser, SubagentBudget, TOKENS, TaskFlow, TaskGenerator, TaskTracker, ToolError, ToolExecutor, ToolRegistry, WrongStackError, addPlanItem, allServers, asBlocks, asText, atomicWrite, attachPlanCheckpoint, attachTodosCheckpoint, awsServer, blockServer, braveSearchServer, buildChildEnv, buildOtlpMetricsRequest, buildOtlpTracesRequest, buildRecoveryStrategies, classifyFamily, clearPlan, color, compileGlob, composeDirectorPrompt, composeSubagentPrompt, computeTaskProgress, context7Server, contextManagerTool, createContextManagerTool, createDefaultPipelines, createDelegateTool, createMessage, createToolOutputSerializer, decryptConfigSecrets, detectNewlineStyle, emptyPlan, encryptConfigSecrets, ensureDir, estimateTextTokens, estimateToolInputTokens, estimateToolResultTokens, everArtServer, extractRunEnv, filesystemServer, findCriticalPath, formatPlan, formatTodosList, githubServer, googleMapsServer, isAgentError, isConfigError, isImageBlock, isPluginError, isSessionError, isTextBlock, isThinkingBlock, isToolError, isToolResultBlock, isToolUseBlock, isWrongStackError, loadDirectorState, loadPlan, loadPlugins, loadProjectModes, loadTodosCheckpoint, loadUserModes, makeAgentSubagentRunner, makeDirectorSessionFactory, matchAny, matchGlob, migratePlaintextSecrets, normalizeToLf, projectHash, removePlanItem, renderPrometheus, resolveWstackPaths, rewriteConfigEncrypted, rosterSummaryFromConfigs, runConfigMigrations, safeParse, safeStringify, sanitizeJsonString, savePlan, saveTodosCheckpoint, sentinelServer, setPlanItemStatus, slackServer, startMetricsServer, startOtlpMetricsExporter, startOtlpTraceExporter, stripAnsi, toStyle, toWrongStackError, topologicalSort, unifiedDiff, unloadPlugins, validateAgainstSchema, wireMetricsToEvents, wrapAsState };
12538
+ export { ALL_FLEET_AGENTS, AUDIT_LOG_AGENT, Agent, AgentError, AutoApprovePermissionPolicy, AutoCompactionMiddleware, AutonomousRunner, BUG_HUNTER_AGENT, BudgetExceededError, CONTEXT_WINDOW_MODES, ConfigError, ConfigMigrationError, Container, Context, ConversationState, DEFAULT_CONFIG_MIGRATIONS, DEFAULT_CONTEXT_WINDOW_MODE_ID, DEFAULT_DIRECTOR_PREAMBLE, DEFAULT_MAX_ITERATIONS, DEFAULT_MODES, DEFAULT_RECOVERY_STRATEGIES, DEFAULT_SPEC_TEMPLATE, DEFAULT_SUBAGENT_BASELINE, DefaultAttachmentStore, DefaultConfigLoader, DefaultConfigStore, DefaultErrorHandler, DefaultHealthRegistry, DefaultLogger, DefaultMemoryStore, DefaultModeStore, DefaultModelsRegistry, DefaultMultiAgentCoordinator, DefaultPathResolver, DefaultPermissionPolicy, DefaultPluginAPI, DefaultProviderRunner, DefaultRetryPolicy, DefaultSecretScrubber, DefaultSecretVault, DefaultSessionReader, DefaultSessionStore, DefaultSkillLoader, DefaultSystemPromptBuilder, DefaultTaskStore, DefaultTokenCounter, Director, DirectorBudgetError, DirectorStateCheckpoint, DoneConditionChecker, EventBus, ExtensionRegistry, FLEET_ROSTER, FleetBus, FleetUsageAggregator, HybridCompactor, InMemoryAgentBridge, InMemoryBridgeTransport, InMemoryMetricsSink, InputBuilder, IntelligentCompactor, KERNEL_API_VERSION, LAYER_1_IDENTITY, LLMSelector, NoopMetricsSink, NoopTracer, OTelTracer, PROMETHEUS_CONTENT_TYPE, Pipeline, PluginError, ProviderError, ProviderRegistry, QueueStore, REFACTOR_PLANNER_AGENT, RecoveryLock, RunController, SECURITY_SCANNER_AGENT, SelectiveCompactor, SessionAnalyzer, SessionError, SlashCommandRegistry, SpecDrivenDev, SpecParser, SubagentBudget, TOKENS, TaskFlow, TaskGenerator, TaskTracker, ToolError, ToolExecutor, ToolRegistry, WrongStackError, addPlanItem, allServers, asBlocks, asText, atomicWrite, attachPlanCheckpoint, attachTodosCheckpoint, awsServer, blockServer, braveSearchServer, buildChildEnv, buildOtlpMetricsRequest, buildOtlpTracesRequest, buildRecoveryStrategies, classifyFamily, clearPlan, color, compileGlob, composeDirectorPrompt, composeSubagentPrompt, computeTaskProgress, context7Server, contextManagerTool, createContextManagerTool, createDefaultPipelines, createDelegateTool, createMessage, createToolOutputSerializer, decryptConfigSecrets, detectNewlineStyle, emptyPlan, encryptConfigSecrets, ensureDir, estimateTextTokens, estimateToolInputTokens, estimateToolResultTokens, everArtServer, extractRunEnv, filesystemServer, findCriticalPath, formatContextWindowModeList, formatPlan, formatTodosList, getContextWindowMode, githubServer, googleMapsServer, isAgentError, isConfigError, isContextWindowModeId, isImageBlock, isPluginError, isSessionError, isTextBlock, isThinkingBlock, isToolError, isToolResultBlock, isToolUseBlock, isWrongStackError, listContextWindowModes, loadDirectorState, loadPlan, loadPlugins, loadProjectModes, loadTodosCheckpoint, loadUserModes, makeAgentSubagentRunner, makeDirectorSessionFactory, matchAny, matchGlob, migratePlaintextSecrets, miniMaxVisionServer, normalizeToLf, projectHash, removePlanItem, renderPrometheus, repairToolUseAdjacency, resolveContextWindowPolicy, resolveWstackPaths, rewriteConfigEncrypted, rosterSummaryFromConfigs, runConfigMigrations, safeParse, safeStringify, sanitizeJsonString, savePlan, saveTodosCheckpoint, sentinelServer, setPlanItemStatus, slackServer, startMetricsServer, startOtlpMetricsExporter, startOtlpTraceExporter, stripAnsi, toStyle, toWrongStackError, topologicalSort, unifiedDiff, unloadPlugins, validateAgainstSchema, wireMetricsToEvents, wrapAsState, zaiVisionServer };
11692
12539
  //# sourceMappingURL=index.js.map
11693
12540
  //# sourceMappingURL=index.js.map