@wrongstack/core 0.148.0 → 0.236.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/dist/{agent-bridge-r9y6gdn4.d.ts → agent-bridge-Cimv7bK7.d.ts} +1 -1
  2. package/dist/{agent-subagent-runner-1GeQE_L0.d.ts → agent-subagent-runner-C658wj_c.d.ts} +9 -8
  3. package/dist/{brain-Cp_3GIS2.d.ts → brain-sCZ3lCjq.d.ts} +28 -2
  4. package/dist/{compactor-BueGt7LG.d.ts → compactor-BRfg3QPd.d.ts} +1 -1
  5. package/dist/{config-BaVThgnT.d.ts → config-Koq6f3fs.d.ts} +2 -2
  6. package/dist/{context-C7G_MtLV.d.ts → context-CLz3z_E8.d.ts} +126 -2
  7. package/dist/coordination/index.d.ts +70 -13
  8. package/dist/coordination/index.js +2126 -151
  9. package/dist/coordination/index.js.map +1 -1
  10. package/dist/defaults/index.d.ts +27 -27
  11. package/dist/defaults/index.js +1328 -354
  12. package/dist/defaults/index.js.map +1 -1
  13. package/dist/execution/index.d.ts +45 -16
  14. package/dist/execution/index.js +367 -59
  15. package/dist/execution/index.js.map +1 -1
  16. package/dist/execution/prompt-enhancer.d.ts +86 -0
  17. package/dist/execution/prompt-enhancer.js +125 -0
  18. package/dist/execution/prompt-enhancer.js.map +1 -0
  19. package/dist/extension/index.d.ts +6 -6
  20. package/dist/extension/index.js +3 -1
  21. package/dist/extension/index.js.map +1 -1
  22. package/dist/{goal-preamble-CYJLg0wk.d.ts → goal-preamble-CnbzyVvl.d.ts} +19 -10
  23. package/dist/{index-BZdezm3g.d.ts → index-BlMqh5GO.d.ts} +8 -8
  24. package/dist/{index-CPweVoFM.d.ts → index-C2eSNPsB.d.ts} +7 -5
  25. package/dist/index.d.ts +439 -129
  26. package/dist/index.js +5206 -905
  27. package/dist/index.js.map +1 -1
  28. package/dist/infrastructure/index.d.ts +7 -7
  29. package/dist/infrastructure/index.js +72 -15
  30. package/dist/infrastructure/index.js.map +1 -1
  31. package/dist/kernel/index.d.ts +9 -9
  32. package/dist/kernel/index.js +7 -1
  33. package/dist/kernel/index.js.map +1 -1
  34. package/dist/{llm-selector-CP72f1lC.d.ts → llm-selector-D22R4AFz.d.ts} +2 -2
  35. package/dist/logger-DmmQhf4P.d.ts +65 -0
  36. package/dist/{mcp-servers-Bl5LTvQg.d.ts → mcp-servers-DFbirBv6.d.ts} +11 -4
  37. package/dist/models/index.d.ts +5 -5
  38. package/dist/models/index.js +89 -9
  39. package/dist/models/index.js.map +1 -1
  40. package/dist/{models-registry-D90K9UnM.d.ts → models-registry-CnJRjTXc.d.ts} +1 -1
  41. package/dist/{multi-agent-coordinator-QWEzJDlm.d.ts → multi-agent-coordinator-60weDZoA.d.ts} +8 -8
  42. package/dist/{null-fleet-bus-BUyfqh23.d.ts → null-fleet-bus-1068dEnr.d.ts} +7 -7
  43. package/dist/observability/index.d.ts +2 -2
  44. package/dist/package-outdated-watcher-pzJ5w7y8.d.ts +560 -0
  45. package/dist/{parallel-eternal-engine-C75QuhAI.d.ts → parallel-eternal-engine-DtG1fjc9.d.ts} +13 -9
  46. package/dist/{path-resolver-DRjQBkoO.d.ts → path-resolver-CA1ULU0J.d.ts} +3 -3
  47. package/dist/{permission-B7nKnEvQ.d.ts → permission-DbWPbuoA.d.ts} +1 -1
  48. package/dist/{permission-policy-8-6zBmfA.d.ts → permission-policy-AOk0LVsV.d.ts} +2 -2
  49. package/dist/pipeline-DsmlwTXu.d.ts +493 -0
  50. package/dist/{plan-templates-CkKNPU3I.d.ts → plan-templates-DPABrDvy.d.ts} +19 -8
  51. package/dist/{provider-runner-BNpuIyOL.d.ts → provider-runner-D0HgUqwV.d.ts} +3 -3
  52. package/dist/{retry-policy-rutAfVeR.d.ts → retry-policy-BVnkbMET.d.ts} +1 -1
  53. package/dist/sdd/index.d.ts +8 -8
  54. package/dist/sdd/index.js +358 -85
  55. package/dist/sdd/index.js.map +1 -1
  56. package/dist/{secret-vault-DoISxaKO.d.ts → secret-vault-BJDY28ev.d.ts} +7 -1
  57. package/dist/{secret-vault-BTcC_T5v.d.ts → secret-vault-CeVNiy_f.d.ts} +4 -3
  58. package/dist/security/index.d.ts +6 -5
  59. package/dist/security/index.js +214 -35
  60. package/dist/security/index.js.map +1 -1
  61. package/dist/{selector-4vDFZKt3.d.ts → selector-Cb4_9-hf.d.ts} +1 -1
  62. package/dist/{session-event-bridge-DWlvglC2.d.ts → session-event-bridge-BhtkkFFy.d.ts} +4 -2
  63. package/dist/{session-reader-BAtCxdaw.d.ts → session-reader-CCOssnBS.d.ts} +1 -1
  64. package/dist/skills/index.js +171 -21
  65. package/dist/skills/index.js.map +1 -1
  66. package/dist/storage/index.d.ts +151 -13
  67. package/dist/storage/index.js +1117 -256
  68. package/dist/storage/index.js.map +1 -1
  69. package/dist/types/index.d.ts +68 -21
  70. package/dist/types/index.js +616 -74
  71. package/dist/types/index.js.map +1 -1
  72. package/dist/utils/expect-defined.js +3 -1
  73. package/dist/utils/expect-defined.js.map +1 -1
  74. package/dist/utils/index.d.ts +80 -4
  75. package/dist/utils/index.js +100 -15
  76. package/dist/utils/index.js.map +1 -1
  77. package/dist/{wstack-paths-DD50Omgn.d.ts → wstack-paths-CJjEwPXn.d.ts} +14 -1
  78. package/package.json +7 -3
  79. package/skills/chimera/SKILL.md +105 -0
  80. package/skills/research-web/SKILL.md +342 -0
  81. package/dist/logger-B9J5puGM.d.ts +0 -32
  82. package/dist/pipeline-BG7UgbDc.d.ts +0 -239
@@ -77,7 +77,13 @@ var ERROR_CODES = {
77
77
  FS_MKDIR_FAILED: "FS_MKDIR_FAILED",
78
78
  FS_DELETE_FAILED: "FS_DELETE_FAILED",
79
79
  FS_ATOMIC_WRITE_FAILED: "FS_ATOMIC_WRITE_FAILED",
80
+ // SDD (Spec-Driven Development)
81
+ SDD_VALIDATION_FAILED: "SDD_VALIDATION_FAILED",
82
+ SDD_PARSE_FAILED: "SDD_PARSE_FAILED",
83
+ SDD_INVALID_STATE: "SDD_INVALID_STATE",
84
+ SDD_NOT_READY: "SDD_NOT_READY",
80
85
  // General
86
+ VALIDATION_ERROR: "VALIDATION_ERROR",
81
87
  UNKNOWN: "UNKNOWN"
82
88
  };
83
89
  var WrongStackError = class extends Error {
@@ -192,6 +198,20 @@ var SessionError = class extends WrongStackError {
192
198
  this.sessionId = opts.sessionId;
193
199
  }
194
200
  };
201
+ var SddError = class extends WrongStackError {
202
+ constructor(opts) {
203
+ super({
204
+ message: opts.message,
205
+ code: opts.code,
206
+ subsystem: "sdd",
207
+ severity: opts.code === ERROR_CODES.SDD_PARSE_FAILED ? "warning" : "error",
208
+ recoverable: opts.code === ERROR_CODES.SDD_NOT_READY,
209
+ context: opts.context,
210
+ cause: opts.cause
211
+ });
212
+ this.name = "SddError";
213
+ }
214
+ };
195
215
  var FsError = class extends WrongStackError {
196
216
  path;
197
217
  constructor(opts) {
@@ -229,6 +249,9 @@ function isAgentError(err) {
229
249
  function isFsError(err) {
230
250
  return err instanceof FsError;
231
251
  }
252
+ function isSddError(err) {
253
+ return err instanceof SddError;
254
+ }
232
255
 
233
256
  // src/types/provider.ts
234
257
  var ProviderError = class extends WrongStackError {
@@ -419,17 +442,96 @@ async function renameWithRetry(from, to) {
419
442
  if (!code || !TRANSIENT_RENAME_CODES.has(code) || i === delays.length) {
420
443
  throw err;
421
444
  }
422
- await new Promise((resolve3) => setTimeout(resolve3, delays[i]));
445
+ await new Promise((resolve4) => setTimeout(resolve4, delays[i]));
423
446
  }
424
447
  }
425
448
  throw lastErr;
426
449
  }
427
450
 
451
+ // src/utils/deep-merge.ts
452
+ var FORBIDDEN_PROTO_KEYS = /* @__PURE__ */ new Set([
453
+ "__proto__",
454
+ "constructor",
455
+ "prototype",
456
+ "__defineGetter__",
457
+ "__defineSetter__",
458
+ "__lookupGetter__",
459
+ "__lookupSetter__"
460
+ ]);
461
+ function isPrimitiveArray(a) {
462
+ return a.every((v) => v === null || typeof v !== "object" && typeof v !== "function");
463
+ }
464
+ function deepMerge(base, patch, options = {}) {
465
+ const {
466
+ conflictResolution = "prefer-patch",
467
+ arrayMode = "replace",
468
+ protectProto = true,
469
+ onNonPrimitiveArrayReplace
470
+ } = options;
471
+ if (typeof base !== "object" || base === null) {
472
+ return conflictResolution === "prefer-patch" ? patch : base;
473
+ }
474
+ if (typeof patch !== "object" || patch === null) {
475
+ return conflictResolution === "prefer-patch" ? patch : base;
476
+ }
477
+ if (Array.isArray(base) && Array.isArray(patch)) {
478
+ if (arrayMode === "concat-primitives" && isPrimitiveArray(base) && isPrimitiveArray(patch)) {
479
+ return [.../* @__PURE__ */ new Set([...base, ...patch])];
480
+ }
481
+ return conflictResolution === "prefer-patch" ? patch : base;
482
+ }
483
+ if (Array.isArray(base) || Array.isArray(patch)) {
484
+ return conflictResolution === "prefer-patch" ? patch : base;
485
+ }
486
+ const baseObj = base;
487
+ const patchObj = patch;
488
+ const out = { ...baseObj };
489
+ for (const [k, v] of Object.entries(patchObj)) {
490
+ if (protectProto && FORBIDDEN_PROTO_KEYS.has(k)) continue;
491
+ const existing = out[k];
492
+ if (v !== null && typeof v === "object" && !Array.isArray(v) && existing !== null && typeof existing === "object" && !Array.isArray(existing)) {
493
+ out[k] = deepMerge(existing, v, options);
494
+ } else if (Array.isArray(v) && Array.isArray(existing)) {
495
+ if (onNonPrimitiveArrayReplace && !isPrimitiveArray(v)) {
496
+ onNonPrimitiveArrayReplace(k, existing.length, v.length);
497
+ }
498
+ out[k] = deepMerge(existing, v, options);
499
+ } else if (v !== void 0) {
500
+ if (onNonPrimitiveArrayReplace && Array.isArray(v) && !isPrimitiveArray(v)) {
501
+ const existingLen = Array.isArray(existing) ? existing.length : 0;
502
+ onNonPrimitiveArrayReplace(k, existingLen, v.length);
503
+ }
504
+ out[k] = v;
505
+ }
506
+ }
507
+ return out;
508
+ }
509
+
428
510
  // src/security/secret-vault.ts
429
511
  var KEY_BYTES = 32;
430
512
  var IV_BYTES = 12;
431
513
  var TAG_BYTES = 16;
432
514
  var ALGO = "aes-256-gcm";
515
+ var KEY_FILE_MODE = 384;
516
+ function checkKeyFilePermissions(keyFile) {
517
+ if (process.platform === "win32") return;
518
+ try {
519
+ const stat2 = fs2.statSync(keyFile);
520
+ const actualMode = stat2.mode & 511;
521
+ if (actualMode !== KEY_FILE_MODE) {
522
+ console.warn(JSON.stringify({
523
+ level: "warn",
524
+ event: "vault.key_file_wrong_permissions",
525
+ message: `Key file ${keyFile} has mode ${actualMode.toString(8)} \u2014 expected ${KEY_FILE_MODE.toString(8)}. Run: chmod ${KEY_FILE_MODE.toString(8)} ${keyFile}`,
526
+ keyFile,
527
+ expectedMode: KEY_FILE_MODE,
528
+ actualMode,
529
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
530
+ }));
531
+ }
532
+ } catch {
533
+ }
534
+ }
433
535
  var DefaultSecretVault = class {
434
536
  keyFile;
435
537
  key;
@@ -453,14 +555,26 @@ var DefaultSecretVault = class {
453
555
  const rest = value.slice(ENCRYPTED_PREFIX.length);
454
556
  const parts = rest.split(":");
455
557
  if (parts.length !== 3) {
456
- throw new Error("SecretVault: malformed encrypted value");
558
+ throw new ConfigError({
559
+ message: "SecretVault: malformed encrypted value",
560
+ code: ERROR_CODES.CONFIG_PARSE_FAILED,
561
+ context: { field: "encrypted_value" }
562
+ });
457
563
  }
458
564
  const [ivB64, tagB64, ctB64] = parts;
459
565
  const iv = Buffer.from(ivB64, "base64");
460
566
  const tag = Buffer.from(tagB64, "base64");
461
567
  const ct = Buffer.from(ctB64, "base64");
462
- if (iv.length !== IV_BYTES) throw new Error("SecretVault: bad IV length");
463
- if (tag.length !== TAG_BYTES) throw new Error("SecretVault: bad tag length");
568
+ if (iv.length !== IV_BYTES) throw new ConfigError({
569
+ message: "SecretVault: bad IV length",
570
+ code: ERROR_CODES.CONFIG_PARSE_FAILED,
571
+ context: { expected: IV_BYTES, actual: iv.length }
572
+ });
573
+ if (tag.length !== TAG_BYTES) throw new ConfigError({
574
+ message: "SecretVault: bad tag length",
575
+ code: ERROR_CODES.CONFIG_PARSE_FAILED,
576
+ context: { expected: TAG_BYTES, actual: tag.length }
577
+ });
464
578
  const key = this.loadOrCreateKey();
465
579
  const decipher = createDecipheriv(ALGO, key, iv);
466
580
  decipher.setAuthTag(tag);
@@ -472,11 +586,14 @@ var DefaultSecretVault = class {
472
586
  try {
473
587
  const buf = fs2.readFileSync(this.keyFile);
474
588
  if (buf.length !== KEY_BYTES) {
475
- throw new Error(
476
- `SecretVault: key file ${this.keyFile} is ${buf.length} bytes (expected ${KEY_BYTES}). Remove it manually to generate a new key.`
477
- );
589
+ throw new ConfigError({
590
+ message: `SecretVault: key file ${this.keyFile} is ${buf.length} bytes (expected ${KEY_BYTES}). Remove it manually to generate a new key.`,
591
+ code: ERROR_CODES.CONFIG_INVALID,
592
+ context: { keyFile: this.keyFile, expectedBytes: KEY_BYTES, actualBytes: buf.length }
593
+ });
478
594
  }
479
595
  this.key = buf;
596
+ checkKeyFilePermissions(this.keyFile);
480
597
  return this.key;
481
598
  } catch (err) {
482
599
  if (err.code !== "ENOENT") throw err;
@@ -489,11 +606,14 @@ var DefaultSecretVault = class {
489
606
  if (err.code !== "EEXIST") throw err;
490
607
  const buf = fs2.readFileSync(this.keyFile);
491
608
  if (buf.length !== KEY_BYTES) {
492
- throw new Error(
493
- `SecretVault: key file ${this.keyFile} is ${buf.length} bytes (expected ${KEY_BYTES}). Remove it manually to generate a new key.`
494
- );
609
+ throw new ConfigError({
610
+ message: `SecretVault: key file ${this.keyFile} is ${buf.length} bytes (expected ${KEY_BYTES}). Remove it manually to generate a new key.`,
611
+ code: ERROR_CODES.CONFIG_INVALID,
612
+ context: { keyFile: this.keyFile, expectedBytes: KEY_BYTES, actualBytes: buf.length }
613
+ });
495
614
  }
496
615
  this.key = buf;
616
+ checkKeyFilePermissions(this.keyFile);
497
617
  return this.key;
498
618
  }
499
619
  this.key = key;
@@ -554,7 +674,7 @@ async function rewriteConfigEncrypted(configPath, vault, patch) {
554
674
  await atomicWrite(configPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
555
675
  await restrictFilePermissions(configPath);
556
676
  }
557
- async function migratePlaintextSecrets(configPath, vault) {
677
+ async function migratePlaintextSecrets(configPath, vault, logger) {
558
678
  let raw;
559
679
  try {
560
680
  raw = await fs.readFile(configPath, "utf8");
@@ -571,11 +691,14 @@ async function migratePlaintextSecrets(configPath, vault) {
571
691
  const migrated = walkCount(parsed, vault, counter);
572
692
  if (counter.n === 0) return { migrated: 0, file: configPath };
573
693
  await atomicWrite(configPath, JSON.stringify(migrated, null, 2), { mode: 384 });
574
- await restrictFilePermissions(configPath);
694
+ await restrictFilePermissions(
695
+ configPath,
696
+ logger ? { warn: (msg) => logger.warn(msg) } : void 0
697
+ );
575
698
  return { migrated: counter.n, file: configPath };
576
699
  }
577
700
  async function restrictFilePermissions(filePath, opts) {
578
- const warn = ((msg) => console.warn(msg));
701
+ const warn = opts?.warn ?? ((msg) => console.warn(msg));
579
702
  if (process.platform === "win32") {
580
703
  try {
581
704
  const { execFile } = await import('child_process');
@@ -627,28 +750,6 @@ function walkCount(node, vault, counter) {
627
750
  }
628
751
  return out;
629
752
  }
630
- var FORBIDDEN_PROTO_KEYS = /* @__PURE__ */ new Set([
631
- "__proto__",
632
- "constructor",
633
- "prototype",
634
- "__defineGetter__",
635
- "__defineSetter__",
636
- "__lookupGetter__",
637
- "__lookupSetter__"
638
- ]);
639
- function deepMerge(a, b) {
640
- const out = { ...a };
641
- for (const [k, v] of Object.entries(b)) {
642
- if (FORBIDDEN_PROTO_KEYS.has(k)) continue;
643
- const existing = out[k];
644
- if (v !== null && typeof v === "object" && !Array.isArray(v) && existing !== null && typeof existing === "object" && !Array.isArray(existing)) {
645
- out[k] = deepMerge(existing, v);
646
- } else {
647
- out[k] = v;
648
- }
649
- }
650
- return out;
651
- }
652
753
 
653
754
  // src/utils/term.ts
654
755
  var hasStdout = () => typeof process !== "undefined" && !!process.stdout;
@@ -714,18 +815,24 @@ var COLORS = {
714
815
  trace: color.dim
715
816
  };
716
817
  var LOG_LEVELS = /* @__PURE__ */ new Set(["error", "warn", "info", "debug", "trace"]);
818
+ var LOG_FORMATS = /* @__PURE__ */ new Set(["pretty", "json"]);
717
819
  var DefaultLogger = class _DefaultLogger {
820
+ /** How many file writes between rotation size checks (statSync is not free). */
821
+ static ROTATE_CHECK_EVERY = 100;
718
822
  level;
719
823
  file;
720
824
  bindings;
721
- pretty;
825
+ format;
722
826
  stderr;
827
+ maxFileBytes;
828
+ writesSinceRotateCheck = 0;
723
829
  constructor(opts = {}) {
724
830
  this.level = opts.level ?? parseLogLevel(process.env.WRONGSTACK_LOG_LEVEL);
725
831
  this.file = opts.file;
726
832
  this.bindings = opts.bindings ?? {};
727
- this.pretty = opts.pretty ?? true;
833
+ this.format = opts.format ?? parseLogFormat(process.env.WRONGSTACK_LOG_FORMAT);
728
834
  this.stderr = opts.stderr !== false;
835
+ this.maxFileBytes = opts.maxFileBytes ?? 10 * 1024 * 1024;
729
836
  if (this.file) {
730
837
  try {
731
838
  fs2.mkdirSync(path4.dirname(this.file), { recursive: true });
@@ -752,11 +859,30 @@ var DefaultLogger = class _DefaultLogger {
752
859
  return new _DefaultLogger({
753
860
  level: this.level,
754
861
  file: this.file,
755
- pretty: this.pretty,
862
+ format: this.format,
756
863
  stderr: this.stderr,
864
+ maxFileBytes: this.maxFileBytes,
757
865
  bindings: { ...this.bindings, ...bindings }
758
866
  });
759
867
  }
868
+ /**
869
+ * Size-based rotation: when the file outgrows `maxFileBytes`, rename it to
870
+ * `<file>.1` (dropping the previous `.1`) so the live file restarts empty.
871
+ * Checked on the first write and every ROTATE_CHECK_EVERY writes after.
872
+ * Best-effort: a rename can fail on Windows while another process holds
873
+ * the file — the next check retries. Multiple processes appending to the
874
+ * same log all run this check; whoever crosses the threshold first wins.
875
+ */
876
+ maybeRotate(file) {
877
+ if (this.writesSinceRotateCheck++ % _DefaultLogger.ROTATE_CHECK_EVERY !== 0) return;
878
+ try {
879
+ const st = fs2.statSync(file);
880
+ if (st.size < this.maxFileBytes) return;
881
+ fs2.rmSync(`${file}.1`, { force: true });
882
+ fs2.renameSync(file, `${file}.1`);
883
+ } catch {
884
+ }
885
+ }
760
886
  log(level, msg, ctx) {
761
887
  const r = LEVEL_RANK[level];
762
888
  const allowed = LEVEL_RANK[this.level];
@@ -768,13 +894,17 @@ var DefaultLogger = class _DefaultLogger {
768
894
  }
769
895
  if (this.file) {
770
896
  try {
897
+ this.maybeRotate(this.file);
771
898
  fs2.appendFileSync(this.file, `${JSON.stringify(entry)}
772
899
  `);
773
900
  } catch {
774
901
  }
775
902
  }
776
903
  if (!this.stderr) return;
777
- if (r <= LEVEL_RANK.warn || this.level === "debug" || this.level === "trace") {
904
+ if (this.format === "json") {
905
+ writeErr(`${JSON.stringify(entry)}
906
+ `);
907
+ } else {
778
908
  const head = `${color.dim(ts)} ${COLORS[level](level.toUpperCase().padEnd(5))} ${msg}`;
779
909
  if (ctx !== void 0) {
780
910
  writeErr(`${head} ${formatCtx(ctx)}
@@ -789,6 +919,9 @@ var DefaultLogger = class _DefaultLogger {
789
919
  function parseLogLevel(raw) {
790
920
  return raw && LOG_LEVELS.has(raw) ? raw : "info";
791
921
  }
922
+ function parseLogFormat(raw) {
923
+ return raw && LOG_FORMATS.has(raw) ? raw : "pretty";
924
+ }
792
925
  function formatCtx(ctx) {
793
926
  if (ctx instanceof Error) return color.dim(ctx.message);
794
927
  if (typeof ctx === "string") return color.dim(ctx);
@@ -798,6 +931,22 @@ function formatCtx(ctx) {
798
931
  return color.dim(String(ctx));
799
932
  }
800
933
  }
934
+ var noOpLogger = {
935
+ // 'error' is the quietest level the Logger contract offers; the methods
936
+ // discard everything regardless, this only matters to level checks.
937
+ level: "error",
938
+ error: () => {
939
+ },
940
+ warn: () => {
941
+ },
942
+ info: () => {
943
+ },
944
+ debug: () => {
945
+ },
946
+ trace: () => {
947
+ },
948
+ child: () => noOpLogger
949
+ };
801
950
 
802
951
  // src/infrastructure/token-counter.ts
803
952
  var PRICE_CACHE_MAX_SIZE = 100;
@@ -938,7 +1087,9 @@ var MEMORY_TYPE_LABELS = {
938
1087
  // src/utils/expect-defined.ts
939
1088
  function expectDefined(value, label) {
940
1089
  if (value === null || value === void 0) {
941
- throw new Error("Expected value to be defined");
1090
+ const err = new Error("Expected value to be defined");
1091
+ err.name = "ExpectDefinedError";
1092
+ throw err;
942
1093
  }
943
1094
  return value;
944
1095
  }
@@ -986,22 +1137,31 @@ function estimateToolResultTokens(content) {
986
1137
  function estimateTextTokens(text) {
987
1138
  return RoughTokenEstimate(text);
988
1139
  }
1140
+ function computeMessageTokens(msg) {
1141
+ if (typeof msg.content === "string") return estimateTextTokens(msg.content);
1142
+ let total = 0;
1143
+ for (const b of msg.content) {
1144
+ if (b.type === "text") total += estimateTextTokens(b.text);
1145
+ else if (b.type === "tool_use") total += estimateToolInputTokens(b.input);
1146
+ else if (b.type === "tool_result") total += estimateToolResultTokens(b.content);
1147
+ else total += RoughTokenEstimate(JSON.stringify(b));
1148
+ }
1149
+ return total;
1150
+ }
989
1151
  function estimateMessageTokens(messages) {
990
1152
  let total = 0;
991
1153
  for (const m of messages) {
992
- if (typeof m.content === "string") {
993
- total += estimateTextTokens(m.content);
994
- } else {
995
- for (const b of m.content) {
996
- if (b.type === "text") total += estimateTextTokens(b.text);
997
- else if (b.type === "tool_use") total += estimateToolInputTokens(b.input);
998
- else if (b.type === "tool_result") total += estimateToolResultTokens(b.content);
999
- }
1154
+ if (typeof m._estTokens === "number" && m._estTokens > 0) {
1155
+ total += m._estTokens;
1156
+ continue;
1000
1157
  }
1158
+ total += computeMessageTokens(m);
1001
1159
  }
1002
1160
  return total;
1003
1161
  }
1004
1162
  function estimateToolDefTokens(tool) {
1163
+ const cached = tool._estDefTokens;
1164
+ if (typeof cached === "number" && cached > 0) return cached;
1005
1165
  return RoughTokenEstimate(tool.name) + RoughTokenEstimate(tool.description ?? "") + RoughTokenEstimate(JSON.stringify(tool.inputSchema));
1006
1166
  }
1007
1167
  function estimateRequestTokens(messages, systemPrompt, tools, calibrationKey = CALIBRATION_GLOBAL_KEY) {
@@ -1011,6 +1171,11 @@ function estimateRequestTokens(messages, systemPrompt, tools, calibrationKey = C
1011
1171
  } else if (Array.isArray(messages)) {
1012
1172
  for (const m of messages) {
1013
1173
  if (typeof m === "object" && m !== null && "content" in m) {
1174
+ const cached = m._estTokens;
1175
+ if (typeof cached === "number" && cached > 0) {
1176
+ messagesTokens += cached;
1177
+ continue;
1178
+ }
1014
1179
  const content = m.content;
1015
1180
  if (typeof content === "string") {
1016
1181
  messagesTokens += RoughTokenEstimate(content);
@@ -1176,6 +1341,18 @@ function findPreserveStart(messages, preserveK) {
1176
1341
  }
1177
1342
  function eliseOldToolResults(messages, opts) {
1178
1343
  const preserveStart = findPreserveStart(messages, opts.preserveK);
1344
+ let hasOversized = false;
1345
+ for (let i = 0; i < preserveStart && !hasOversized; i++) {
1346
+ const msg = messages[i];
1347
+ if (!msg || !Array.isArray(msg.content)) continue;
1348
+ for (const b of msg.content) {
1349
+ if (b.type === "tool_result" && estimateToolResultTokens(b.content) >= opts.eliseThreshold) {
1350
+ hasOversized = true;
1351
+ break;
1352
+ }
1353
+ }
1354
+ }
1355
+ if (!hasOversized) return { messages, saved: 0, changed: false };
1179
1356
  let saved = 0;
1180
1357
  let changed = false;
1181
1358
  const next = new Array(messages.length);
@@ -2356,6 +2533,77 @@ Remember: your job is to make the user a better developer, not just to complete
2356
2533
  tags: ["teaching", "mentor", "learning"],
2357
2534
  toolPreferences: ["read", "edit", "explain"],
2358
2535
  suggestedSkills: ["prompt-engineering", "skill-creator", "node-modern", "typescript-strict"]
2536
+ },
2537
+ {
2538
+ id: "research-web",
2539
+ name: "Research Web",
2540
+ description: "Current-data research \u2014 search web, verify, inject findings into context",
2541
+ prompt: `## Research Web Mode
2542
+
2543
+ You are in research mode. Your role: find, verify, and incorporate
2544
+ current web data. Your training data is stale \u2014 every factual claim
2545
+ about version numbers, API surfaces, package status, or ecosystem
2546
+ changes must be verified against live sources.
2547
+
2548
+ ### When to research
2549
+ - The user asks "is this still the case?", "what's current?", "latest version?"
2550
+ - You're about to claim a version number, deprecation, or API change
2551
+ - You're comparing tools, packages, or approaches released in the last 12 months
2552
+ - You realize your knowledge may be >6 months old on a fast-moving topic
2553
+
2554
+ ### Research methodology
2555
+ 1. **Search first, fetch selectively.** Use web_search with 5-8 results for
2556
+ broad queries. Then web_fetch the 1-2 most authoritative results for detail.
2557
+ Don't fetch every result \u2014 you'll burn tokens on noise.
2558
+ 2. **Cross-reference.** One source is a data point. Two sources that agree
2559
+ is a signal. Three is confirmation. Flag single-source claims as tentative.
2560
+ 3. **Cite sources.** Every factual claim from web data must include where it
2561
+ came from: domain name, and date if visible on the page.
2562
+ 4. **Know when to stop.** 2-3 searches + 1-2 fetches is usually sufficient.
2563
+ If you're on your 5th search without a clear answer, pause and tell the user
2564
+ what you've found and what's still unclear \u2014 let them decide to dig deeper.
2565
+ 5. **Inject findings for reuse.** After gathering current data, use
2566
+ context_manager with add_note to inject a structured "Research Findings"
2567
+ block into the conversation. Future turns see this and don't re-search.
2568
+
2569
+ ### Self-injection pattern
2570
+ When you discover current data mid-research, inject it so subsequent turns
2571
+ benefit without re-searching:
2572
+
2573
+ web_search("Next.js middleware breaking changes 2025")
2574
+ \u2192 Surfaced: Next.js 15.2 changed middleware runtime from edge to node
2575
+ web_fetch("https://nextjs.org/docs/messages/middleware-upgrade-guide")
2576
+ \u2192 Confirmed: middleware now runs on Node.js runtime by default
2577
+ context_manager: add_note(
2578
+ "## Research: Next.js middleware
2579
+ - Next.js 15.2: middleware defaults to Node.js runtime (was edge)
2580
+ - Breaking: edge-only APIs (crypto.subtle, WebSocket) no longer available
2581
+ - Migration: use node:* equivalents or set runtime: 'edge' explicitly
2582
+ - Source: nextjs.org/docs/messages/middleware-upgrade-guide"
2583
+ )
2584
+
2585
+ The add_note persists in conversation \u2014 you won't re-search on the next turn.
2586
+
2587
+ ### Anti-patterns
2588
+ - Don't research things already in the conversation context (including
2589
+ earlier add_note blocks you injected)
2590
+ - Don't treat a single web search result as ground truth \u2014 cross-reference
2591
+ - Don't inject raw JSON or search result dumps via add_note \u2014 summarize
2592
+ - Don't research while the user is waiting for a quick code edit \u2014 toggle
2593
+ research-web mode only during analysis/discussion phases
2594
+ - Don't research-loop: 5+ searches on one topic \u2192 stop and ask the user
2595
+
2596
+ ### Exiting research mode
2597
+ When the user no longer needs current-data research, suggest switching back
2598
+ to the previous mode. You stay in research mode until explicitly told to
2599
+ switch \u2014 but don't force web searches on every turn. The methodology rules
2600
+ above already gate when to actually search.
2601
+
2602
+ When you're done with research: suggest the user run \`/mode default\` or
2603
+ their previous mode.`,
2604
+ tags: ["research", "web", "current-data", "up-to-date"],
2605
+ toolPreferences: ["web_search", "web_fetch", "search", "fetch", "context_manager"],
2606
+ suggestedSkills: ["research-web", "tech-stack", "node-modern", "security-scanner", "react-modern"]
2359
2607
  }
2360
2608
  ];
2361
2609
 
@@ -2526,28 +2774,40 @@ var InMemoryAgentBridge = class {
2526
2774
  return () => this.subscriptions.delete(handler);
2527
2775
  }
2528
2776
  async request(msg, timeoutMs) {
2529
- if (this.stopped) throw new Error("Bridge is stopped");
2777
+ if (this.stopped) throw new AgentError({
2778
+ message: "Bridge is stopped",
2779
+ code: ERROR_CODES.AGENT_ABORTED
2780
+ });
2530
2781
  const timeout = timeoutMs ?? this.timeoutMs;
2531
2782
  const correlationId = msg.id;
2532
2783
  if (this.inflightGuards.has(correlationId)) {
2533
- throw new Error(
2534
- `Bridge request id "${correlationId}" collides with an in-flight request \u2014 caller is reusing message ids`
2535
- );
2784
+ throw new AgentError({
2785
+ message: `Bridge request id "${correlationId}" collides with an in-flight request \u2014 caller is reusing message ids`,
2786
+ code: ERROR_CODES.AGENT_RUN_FAILED,
2787
+ context: { correlationId }
2788
+ });
2536
2789
  }
2537
2790
  this.inflightGuards.add(correlationId);
2538
- return new Promise((resolve3, reject) => {
2791
+ return new Promise((resolve4, reject) => {
2539
2792
  const timer = setTimeout(() => {
2540
2793
  this.inflightGuards.delete(correlationId);
2541
2794
  this.pendingRequests.delete(correlationId);
2542
- reject(new Error(`Request ${correlationId} timed out after ${timeout}ms`));
2795
+ reject(new AgentError({
2796
+ message: `Request ${correlationId} timed out after ${timeout}ms`,
2797
+ code: ERROR_CODES.AGENT_RUN_FAILED,
2798
+ context: { correlationId, timeoutMs: timeout }
2799
+ }));
2543
2800
  }, timeout);
2544
2801
  if (!this.inflightGuards.has(correlationId)) {
2545
2802
  clearTimeout(timer);
2546
- reject(new Error("Bridge stopped"));
2803
+ reject(new AgentError({
2804
+ message: "Bridge stopped",
2805
+ code: ERROR_CODES.AGENT_ABORTED
2806
+ }));
2547
2807
  return;
2548
2808
  }
2549
2809
  this.pendingRequests.set(correlationId, {
2550
- resolve: resolve3,
2810
+ resolve: resolve4,
2551
2811
  reject,
2552
2812
  timer
2553
2813
  });
@@ -2564,7 +2824,10 @@ var InMemoryAgentBridge = class {
2564
2824
  this.stopped = true;
2565
2825
  for (const [, p] of this.pendingRequests) {
2566
2826
  clearTimeout(p.timer);
2567
- p.reject(new Error("Bridge stopped"));
2827
+ p.reject(new AgentError({
2828
+ message: "Bridge stopped",
2829
+ code: ERROR_CODES.AGENT_ABORTED
2830
+ }));
2568
2831
  }
2569
2832
  this.pendingRequests.clear();
2570
2833
  this.inflightGuards.clear();
@@ -2743,11 +3006,11 @@ function validateAgainstSchema(value, schema) {
2743
3006
  walk2(value, schema, "", errors);
2744
3007
  return { ok: errors.length === 0, errors };
2745
3008
  }
2746
- function walk2(value, schema, path6, errors) {
3009
+ function walk2(value, schema, path7, errors) {
2747
3010
  if (schema.enum !== void 0) {
2748
3011
  if (!schema.enum.some((e) => deepEqual(e, value))) {
2749
3012
  errors.push({
2750
- path: path6 || "<root>",
3013
+ path: path7 || "<root>",
2751
3014
  message: `expected one of ${JSON.stringify(schema.enum)}, got ${JSON.stringify(value)}`
2752
3015
  });
2753
3016
  return;
@@ -2756,7 +3019,7 @@ function walk2(value, schema, path6, errors) {
2756
3019
  if (typeof schema.type === "string") {
2757
3020
  if (!checkType(value, schema.type)) {
2758
3021
  errors.push({
2759
- path: path6 || "<root>",
3022
+ path: path7 || "<root>",
2760
3023
  message: `expected ${schema.type}, got ${describeType(value)}`
2761
3024
  });
2762
3025
  return;
@@ -2766,19 +3029,19 @@ function walk2(value, schema, path6, errors) {
2766
3029
  const obj = value;
2767
3030
  for (const req of schema.required ?? []) {
2768
3031
  if (!(req in obj)) {
2769
- errors.push({ path: joinPath(path6, req), message: "required property missing" });
3032
+ errors.push({ path: joinPath(path7, req), message: "required property missing" });
2770
3033
  }
2771
3034
  }
2772
3035
  if (schema.properties) {
2773
3036
  for (const [key, subSchema] of Object.entries(schema.properties)) {
2774
3037
  if (key in obj) {
2775
- walk2(obj[key], subSchema, joinPath(path6, key), errors);
3038
+ walk2(obj[key], subSchema, joinPath(path7, key), errors);
2776
3039
  }
2777
3040
  }
2778
3041
  }
2779
3042
  }
2780
3043
  if (schema.type === "array" && Array.isArray(value) && schema.items) {
2781
- value.forEach((item, i) => walk2(item, schema.items, `${path6}[${i}]`, errors));
3044
+ value.forEach((item, i) => walk2(item, schema.items, `${path7}[${i}]`, errors));
2782
3045
  }
2783
3046
  }
2784
3047
  function checkType(value, type) {
@@ -2876,7 +3139,7 @@ function createToolOutputSerializer(opts = {}) {
2876
3139
  }
2877
3140
 
2878
3141
  // src/execution/tool-executor.ts
2879
- var ToolExecutor = class {
3142
+ var ToolExecutor = class _ToolExecutor {
2880
3143
  constructor(registry, opts) {
2881
3144
  this.registry = registry;
2882
3145
  this.opts = opts;
@@ -2888,6 +3151,10 @@ var ToolExecutor = class {
2888
3151
  }
2889
3152
  registry;
2890
3153
  opts;
3154
+ /** Minimum gap between coalesced `partial_output` tool.progress emits. */
3155
+ static PROGRESS_EMIT_INTERVAL_MS = 100;
3156
+ /** Max chars of accumulated stream text carried per coalesced emit. */
3157
+ static PROGRESS_TAIL_CHARS = 16384;
2891
3158
  serializer;
2892
3159
  iterationTimeoutMs;
2893
3160
  maxToolTimeoutMs;
@@ -2933,9 +3200,6 @@ Please call the tool again with arguments that match its inputSchema. You can us
2933
3200
  return { result, tool, durationMs: Date.now() - start };
2934
3201
  }
2935
3202
  const toolDangerousCaps = getDangerousCapabilities(tool);
2936
- if (toolDangerousCaps.length > 0) {
2937
- if (this.opts.events) ;
2938
- }
2939
3203
  if (hasMalformedArguments(use.input)) {
2940
3204
  const result = this.malformedInputResult(use, extractMalformedRaw(use.input));
2941
3205
  budget = this.decrementBudget(result, budget);
@@ -3173,17 +3437,48 @@ ${post.additionalContext}` };
3173
3437
  throw new Error(`Tool "${tool.name}" does not support streaming execution`);
3174
3438
  }
3175
3439
  const stream = tool.executeStream(input, ctx, { signal });
3176
- for await (const ev of stream) {
3177
- if (ev.type === "final") {
3178
- finalOutput = ev.output;
3179
- sawFinal = true;
3180
- break;
3181
- }
3440
+ const iter = stream[Symbol.asyncIterator]();
3441
+ let progressTail = "";
3442
+ let lastProgressEmitAt = 0;
3443
+ const emitProgress = (ev) => {
3182
3444
  this.opts.events?.emit("tool.progress", {
3183
3445
  name: tool.name,
3184
3446
  id: toolUseId ?? "<unknown>",
3185
3447
  event: ev
3186
3448
  });
3449
+ };
3450
+ const flushProgressTail = (force) => {
3451
+ if (progressTail.length === 0) return;
3452
+ const now = Date.now();
3453
+ if (!force && now - lastProgressEmitAt < _ToolExecutor.PROGRESS_EMIT_INTERVAL_MS) return;
3454
+ const text = progressTail;
3455
+ progressTail = "";
3456
+ lastProgressEmitAt = now;
3457
+ emitProgress({ type: "partial_output", text });
3458
+ };
3459
+ try {
3460
+ while (true) {
3461
+ const { done, value: ev } = await iter.next();
3462
+ if (done) break;
3463
+ if (ev.type === "final") {
3464
+ finalOutput = ev.output;
3465
+ sawFinal = true;
3466
+ break;
3467
+ }
3468
+ if (ev.type === "partial_output" && typeof ev.text === "string") {
3469
+ progressTail += ev.text;
3470
+ if (progressTail.length > _ToolExecutor.PROGRESS_TAIL_CHARS) {
3471
+ progressTail = progressTail.slice(-_ToolExecutor.PROGRESS_TAIL_CHARS);
3472
+ }
3473
+ flushProgressTail(false);
3474
+ continue;
3475
+ }
3476
+ flushProgressTail(true);
3477
+ emitProgress(ev);
3478
+ }
3479
+ flushProgressTail(true);
3480
+ } finally {
3481
+ await iter.return?.(void 0);
3187
3482
  }
3188
3483
  if (!sawFinal) {
3189
3484
  throw new Error(`tool "${tool.name}" executeStream completed without a 'final' event`);
@@ -3292,6 +3587,253 @@ function extractMalformedRaw(input) {
3292
3587
  }
3293
3588
  }
3294
3589
 
3590
+ // src/core/conversation-state.ts
3591
+ var ConversationState = class {
3592
+ ctx;
3593
+ listeners = /* @__PURE__ */ new Set();
3594
+ constructor(ctx) {
3595
+ this.ctx = ctx;
3596
+ }
3597
+ get messages() {
3598
+ return this.ctx.messages;
3599
+ }
3600
+ get todos() {
3601
+ return this.ctx.todos;
3602
+ }
3603
+ get meta() {
3604
+ return this.ctx.meta;
3605
+ }
3606
+ /**
3607
+ * Cheap immutable snapshot. Useful for tests and for compaction passes
3608
+ * that need a stable view across an async boundary.
3609
+ *
3610
+ * Uses shallow-freeze instead of deep-freeze: only the wrapper object
3611
+ * and the three content arrays are frozen. Individual message/todo
3612
+ * objects are NOT recursively frozen — they are reconstructed via
3613
+ * spread copies and are immutable by convention. This cuts the freeze
3614
+ * count from O(n·m·d) (n=messages, m=content blocks, d=depth) to O(1).
3615
+ */
3616
+ snapshot() {
3617
+ const snap = {
3618
+ messages: [...this.ctx.messages],
3619
+ todos: [...this.ctx.todos],
3620
+ meta: { ...this.ctx.meta }
3621
+ };
3622
+ Object.freeze(snap.messages);
3623
+ Object.freeze(snap.todos);
3624
+ Object.freeze(snap.meta);
3625
+ return Object.freeze(snap);
3626
+ }
3627
+ appendMessage(message) {
3628
+ if (message._estTokens === void 0) {
3629
+ message._estTokens = computeMessageTokens(message);
3630
+ }
3631
+ this.ctx.messages.splice(this.ctx.messages.length, 0, message);
3632
+ this.emit({ kind: "message_appended", message });
3633
+ }
3634
+ replaceMessages(messages) {
3635
+ for (const m of messages) {
3636
+ if (m._estTokens === void 0) {
3637
+ m._estTokens = computeMessageTokens(m);
3638
+ }
3639
+ }
3640
+ const arr = this.ctx.messages;
3641
+ if (messages.length < arr.length) {
3642
+ arr.length = messages.length;
3643
+ }
3644
+ for (let i = 0; i < messages.length; i++) {
3645
+ arr[i] = messages[i];
3646
+ }
3647
+ if (messages.some(
3648
+ (m) => Array.isArray(m.content) && m.content.some((b) => b.type === "tool_use" || b.type === "tool_result")
3649
+ )) {
3650
+ this.ctx.toolAdjacencyDirty = true;
3651
+ }
3652
+ this.emit({ kind: "messages_replaced", messages: [...messages] });
3653
+ }
3654
+ replaceTodos(todos) {
3655
+ const allDone = todos.length > 0 && todos.every((t) => t.status === "completed");
3656
+ const effective = allDone ? [] : todos;
3657
+ this.ctx.todos.length = 0;
3658
+ this.ctx.todos.splice(0, 0, ...effective);
3659
+ this.emit({ kind: "todos_replaced", todos: [...effective] });
3660
+ }
3661
+ setMeta(key, value) {
3662
+ this.ctx.meta[key] = value;
3663
+ this.emit({ kind: "meta_set", key, value });
3664
+ }
3665
+ deleteMeta(key) {
3666
+ if (!(key in this.ctx.meta)) return;
3667
+ delete this.ctx.meta[key];
3668
+ this.emit({ kind: "meta_deleted", key });
3669
+ }
3670
+ clearMeta() {
3671
+ const keys = Object.keys(this.ctx.meta);
3672
+ if (keys.length === 0) return;
3673
+ for (const key of keys) delete this.ctx.meta[key];
3674
+ this.emit({ kind: "meta_cleared" });
3675
+ }
3676
+ /**
3677
+ * Subscribe to mutations that go through this wrapper. Direct mutations of
3678
+ * the compatibility arrays are intentionally not observed.
3679
+ */
3680
+ onChange(listener) {
3681
+ this.listeners.add(listener);
3682
+ return () => this.listeners.delete(listener);
3683
+ }
3684
+ emit(change) {
3685
+ for (const h of this.listeners) {
3686
+ try {
3687
+ h(change, this);
3688
+ } catch {
3689
+ }
3690
+ }
3691
+ }
3692
+ };
3693
+
3694
+ // src/core/context.ts
3695
+ var Context = class {
3696
+ messages = [];
3697
+ todos = [];
3698
+ readFiles = /* @__PURE__ */ new Set();
3699
+ fileMtimes = /* @__PURE__ */ new Map();
3700
+ systemPrompt;
3701
+ provider;
3702
+ session;
3703
+ signal;
3704
+ tokenCounter;
3705
+ cwd;
3706
+ projectRoot;
3707
+ /** Mutable working directory — starts as `cwd`. Change via `setWorkingDir()`. */
3708
+ workingDir;
3709
+ model;
3710
+ tools = [];
3711
+ meta = {};
3712
+ /** Agent id performing this run (e.g. 'leader', 'executor'). */
3713
+ agentId;
3714
+ /** Human-readable agent name. */
3715
+ agentName;
3716
+ /** Callbacks fired when `setWorkingDir()` changes the working directory. */
3717
+ _onWorkingDirChanged = [];
3718
+ /**
3719
+ * Set to true when the conversation gains new tool_use or tool_result
3720
+ * blocks — the only time repairToolUseAdjacency() can find new issues.
3721
+ * buildAndRunRequestPipeline() checks this flag to skip an O(n) scan
3722
+ * on iterations where no tool content was added (pure text responses).
3723
+ */
3724
+ toolAdjacencyDirty = false;
3725
+ constructor(init) {
3726
+ this.systemPrompt = init.systemPrompt;
3727
+ this.provider = init.provider;
3728
+ this.session = init.session;
3729
+ this.signal = init.signal;
3730
+ this.tokenCounter = init.tokenCounter;
3731
+ this.cwd = init.cwd;
3732
+ this.projectRoot = init.projectRoot;
3733
+ this.workingDir = init.workingDir ?? init.cwd;
3734
+ this.model = init.model;
3735
+ this.tools = init.tools ?? [];
3736
+ this.agentId = init.agentId ?? "unknown";
3737
+ this.agentName = init.agentName ?? "Unknown Agent";
3738
+ }
3739
+ /**
3740
+ * Observable wrapper over the mutable conversation state. Lazy so
3741
+ * subsystems that don't subscribe pay nothing. Mutations made directly
3742
+ * on `ctx.messages` / `ctx.todos` are still visible through this
3743
+ * wrapper's read API (it holds a reference, not a copy) but only
3744
+ * mutations that go through `state.appendMessage()` etc. fire
3745
+ * `onChange`. New code should prefer the wrapper API.
3746
+ */
3747
+ _state = null;
3748
+ get state() {
3749
+ if (!this._state) this._state = new ConversationState(this);
3750
+ return this._state;
3751
+ }
3752
+ /**
3753
+ * Register a teardown hook tied to the current run's abort signal.
3754
+ * Hooks registered before a run starts are stored and fired when the
3755
+ * next run ends; there is no immediate fire when no run is active.
3756
+ *
3757
+ * **Scope:** these hooks fire on the **whole agent run's** abort, not on
3758
+ * an individual tool call. For per-tool teardown of resources owned by
3759
+ * the tool author (child processes, handles), prefer `Tool.cleanup` —
3760
+ * see its JSDoc for the full rule.
3761
+ */
3762
+ abortHooks = /* @__PURE__ */ new Set();
3763
+ registerAbortHook(fn) {
3764
+ this.abortHooks.add(fn);
3765
+ return () => this.abortHooks.delete(fn);
3766
+ }
3767
+ async drainAbortHooks() {
3768
+ const snapshot = [...this.abortHooks].reverse();
3769
+ this.abortHooks.clear();
3770
+ for (const fn of snapshot) {
3771
+ try {
3772
+ await fn();
3773
+ } catch {
3774
+ }
3775
+ }
3776
+ }
3777
+ recordRead(absPath, mtimeMs) {
3778
+ this.readFiles.add(absPath);
3779
+ this.fileMtimes.set(absPath, mtimeMs);
3780
+ }
3781
+ /** Clear accumulated file-read metadata after compaction or at boundaries
3782
+ * where stale read history could cause tools to skip legitimate re-reads.
3783
+ * The agent re-populates this naturally on the next file access. */
3784
+ clearFileTracking() {
3785
+ this.readFiles.clear();
3786
+ this.fileMtimes.clear();
3787
+ }
3788
+ hasRead(absPath) {
3789
+ return this.readFiles.has(absPath);
3790
+ }
3791
+ lastReadMtime(absPath) {
3792
+ return this.fileMtimes.get(absPath);
3793
+ }
3794
+ /**
3795
+ * Change the working directory for path resolution. Resolves relative paths
3796
+ * against `projectRoot` and validates the result is within the project root.
3797
+ * Fires all registered `onWorkingDirChanged` callbacks.
3798
+ * Returns the resolved absolute path.
3799
+ */
3800
+ setWorkingDir(dir) {
3801
+ const resolved = path4.isAbsolute(dir) ? path4.resolve(dir) : path4.resolve(this.projectRoot, dir);
3802
+ const root = path4.resolve(this.projectRoot);
3803
+ const rel = path4.relative(root, resolved);
3804
+ if (rel.startsWith("..") || path4.isAbsolute(rel)) {
3805
+ throw new Error(
3806
+ `Working directory "${resolved}" is outside project root "${root}"`
3807
+ );
3808
+ }
3809
+ const old = this.workingDir;
3810
+ this.workingDir = resolved;
3811
+ for (const cb of this._onWorkingDirChanged) {
3812
+ try {
3813
+ cb(resolved, old);
3814
+ } catch {
3815
+ }
3816
+ }
3817
+ return resolved;
3818
+ }
3819
+ /**
3820
+ * Register a callback that fires when the working directory changes.
3821
+ * Returns an unsubscribe function. Callbacks are fired synchronously
3822
+ * inside `setWorkingDir()` — errors in callbacks are swallowed so one
3823
+ * bad listener doesn't prevent others from executing.
3824
+ */
3825
+ onWorkingDirChanged(cb) {
3826
+ this._onWorkingDirChanged.push(cb);
3827
+ return () => {
3828
+ const idx = this._onWorkingDirChanged.indexOf(cb);
3829
+ if (idx >= 0) this._onWorkingDirChanged.splice(idx, 1);
3830
+ };
3831
+ }
3832
+ usage() {
3833
+ return this.tokenCounter.total();
3834
+ }
3835
+ };
3836
+
3295
3837
  // src/utils/regex-guard.ts
3296
3838
  var MAX_PATTERN_LEN = 512;
3297
3839
  var DANGEROUS_PATTERNS = [
@@ -3604,6 +4146,6 @@ function renderPlainText(meta, events) {
3604
4146
  return lines.join("\n");
3605
4147
  }
3606
4148
 
3607
- export { AgentError, CONTEXT_WINDOW_MODES, ConfigError, DEFAULT_AUTONOMY_CONFIG, DEFAULT_CONTEXT_CONFIG, DEFAULT_CONTEXT_WINDOW_MODE_ID, DEFAULT_MODES, DEFAULT_RECOVERY_STRATEGIES, DEFAULT_SESSION_LOGGING_CONFIG, DEFAULT_SESSION_PRUNE_DAYS, DEFAULT_SPEC_TEMPLATE, DEFAULT_TOOLS_CONFIG, DefaultErrorHandler, DefaultLogger, DefaultModelsRegistry, DefaultPathResolver, DefaultRetryPolicy, DefaultSecretScrubber, DefaultSecretVault, DefaultSessionReader, DefaultTokenCounter, ERROR_CODES, FsError, HybridCompactor, InMemoryAgentBridge, InMemoryBridgeTransport, MEMORY_TYPE_LABELS, PluginError, ProviderError, SessionError, StreamHangError, ToolError, ToolExecutor, WrongStackError, asBlocks, asText, buildRecoveryStrategies, classifyFamily, computeTaskProgress, createMessage, decryptConfigSecrets, encryptConfigSecrets, findCriticalPath, formatContextWindowModeList, getContextWindowMode, isAgentError, isConfigError, isContextWindowModeId, isFsError, isImageBlock, isPluginError, isSecretField, isSessionError, isTextBlock, isThinkingBlock, isToolError, isToolResultBlock, isToolUseBlock, isWrongStackError, listContextWindowModes, migratePlaintextSecrets, resolveContextWindowPolicy, rewriteConfigEncrypted, toWrongStackError, topologicalSort };
4149
+ export { AgentError, CONTEXT_WINDOW_MODES, ConfigError, Context, DEFAULT_AUTONOMY_CONFIG, DEFAULT_CONTEXT_CONFIG, DEFAULT_CONTEXT_WINDOW_MODE_ID, DEFAULT_MODES, DEFAULT_RECOVERY_STRATEGIES, DEFAULT_SESSION_LOGGING_CONFIG, DEFAULT_SESSION_PRUNE_DAYS, DEFAULT_SPEC_TEMPLATE, DEFAULT_TOOLS_CONFIG, DefaultErrorHandler, DefaultLogger, DefaultModelsRegistry, DefaultPathResolver, DefaultRetryPolicy, DefaultSecretScrubber, DefaultSecretVault, DefaultSessionReader, DefaultTokenCounter, ERROR_CODES, FsError, HybridCompactor, InMemoryAgentBridge, InMemoryBridgeTransport, MEMORY_TYPE_LABELS, PluginError, ProviderError, SddError, SessionError, StreamHangError, ToolError, ToolExecutor, WrongStackError, asBlocks, asText, buildRecoveryStrategies, classifyFamily, computeTaskProgress, createMessage, decryptConfigSecrets, encryptConfigSecrets, findCriticalPath, formatContextWindowModeList, getContextWindowMode, isAgentError, isConfigError, isContextWindowModeId, isFsError, isImageBlock, isPluginError, isSddError, isSecretField, isSessionError, isTextBlock, isThinkingBlock, isToolError, isToolResultBlock, isToolUseBlock, isWrongStackError, listContextWindowModes, migratePlaintextSecrets, noOpLogger, resolveContextWindowPolicy, rewriteConfigEncrypted, toWrongStackError, topologicalSort };
3608
4150
  //# sourceMappingURL=index.js.map
3609
4151
  //# sourceMappingURL=index.js.map