@wrongstack/core 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,17 +1,155 @@
1
1
  import * as fsp from 'fs/promises';
2
2
  import * as path2 from 'path';
3
+ import * as crypto2 from 'crypto';
3
4
  import { randomBytes, createHash, createCipheriv, createDecipheriv, randomUUID } from 'crypto';
4
5
  import * as os3 from 'os';
5
- import * as fs4 from 'fs';
6
+ import * as fs5 from 'fs';
6
7
  import { EventEmitter } from 'events';
7
8
  import { spawn } from 'child_process';
8
9
 
10
+ // src/types/errors.ts
11
+ var WrongStackError = class extends Error {
12
+ code;
13
+ subsystem;
14
+ severity;
15
+ recoverable;
16
+ context;
17
+ constructor(opts) {
18
+ super(opts.message, { cause: opts.cause });
19
+ this.name = "WrongStackError";
20
+ this.code = opts.code;
21
+ this.subsystem = opts.subsystem;
22
+ this.severity = opts.severity ?? "error";
23
+ this.recoverable = opts.recoverable ?? false;
24
+ this.context = opts.context;
25
+ }
26
+ /**
27
+ * Render a one-line user-facing description.
28
+ * Subclasses should override for domain-specific formatting.
29
+ */
30
+ describe() {
31
+ const ctx = this.context ? ` ${formatContext(this.context)}` : "";
32
+ return `${this.code}: ${this.message}${ctx}`;
33
+ }
34
+ };
35
+ function formatContext(ctx) {
36
+ const parts = Object.entries(ctx).filter(([, v]) => v !== void 0).slice(0, 3).map(([k, v]) => `${k}=${String(v)}`);
37
+ return parts.length > 0 ? `[${parts.join(" ")}]` : "";
38
+ }
39
+ var ToolError = class extends WrongStackError {
40
+ toolName;
41
+ constructor(opts) {
42
+ super({
43
+ message: opts.message,
44
+ code: opts.code,
45
+ subsystem: "tool",
46
+ recoverable: opts.recoverable,
47
+ context: { tool: opts.toolName, ...opts.context },
48
+ cause: opts.cause
49
+ });
50
+ this.name = "ToolError";
51
+ this.toolName = opts.toolName;
52
+ }
53
+ };
54
+ var ConfigError = class extends WrongStackError {
55
+ constructor(opts) {
56
+ super({
57
+ message: opts.message,
58
+ code: opts.code,
59
+ subsystem: "config",
60
+ severity: "fatal",
61
+ recoverable: false,
62
+ context: opts.context,
63
+ cause: opts.cause
64
+ });
65
+ this.name = "ConfigError";
66
+ }
67
+ };
68
+ var PluginError = class extends WrongStackError {
69
+ pluginName;
70
+ constructor(opts) {
71
+ super({
72
+ message: opts.message,
73
+ code: opts.code,
74
+ subsystem: "plugin",
75
+ severity: "error",
76
+ recoverable: opts.code === "PLUGIN_MISSING_DEPENDENCY",
77
+ context: { plugin: opts.pluginName, ...opts.context },
78
+ cause: opts.cause
79
+ });
80
+ this.name = "PluginError";
81
+ this.pluginName = opts.pluginName;
82
+ }
83
+ };
84
+ var AgentError = class extends WrongStackError {
85
+ constructor(opts) {
86
+ super({
87
+ message: opts.message,
88
+ code: opts.code,
89
+ subsystem: "agent",
90
+ severity: opts.code === "AGENT_ABORTED" ? "warning" : "error",
91
+ recoverable: opts.recoverable ?? opts.code === "AGENT_ITERATION_LIMIT",
92
+ context: opts.context,
93
+ cause: opts.cause
94
+ });
95
+ this.name = "AgentError";
96
+ }
97
+ };
98
+ function toWrongStackError(err, code = "AGENT_RUN_FAILED") {
99
+ if (err instanceof WrongStackError) return err;
100
+ const message = err instanceof Error ? err.message : String(err);
101
+ return new AgentError({
102
+ message,
103
+ code: code === "UNKNOWN" ? "AGENT_RUN_FAILED" : code,
104
+ cause: err
105
+ });
106
+ }
107
+ var SessionError = class extends WrongStackError {
108
+ sessionId;
109
+ constructor(opts) {
110
+ super({
111
+ message: opts.message,
112
+ code: opts.code,
113
+ subsystem: "session",
114
+ severity: opts.code === "SESSION_WRITE_FAILED" ? "error" : "warning",
115
+ recoverable: opts.code !== "SESSION_CORRUPTED",
116
+ context: { sessionId: opts.sessionId, ...opts.context },
117
+ cause: opts.cause
118
+ });
119
+ this.name = "SessionError";
120
+ this.sessionId = opts.sessionId;
121
+ }
122
+ };
123
+ function isWrongStackError(err) {
124
+ return err instanceof WrongStackError;
125
+ }
126
+ function isToolError(err) {
127
+ return err instanceof ToolError;
128
+ }
129
+ function isConfigError(err) {
130
+ return err instanceof ConfigError;
131
+ }
132
+ function isPluginError(err) {
133
+ return err instanceof PluginError;
134
+ }
135
+ function isSessionError(err) {
136
+ return err instanceof SessionError;
137
+ }
138
+ function isAgentError(err) {
139
+ return err instanceof AgentError;
140
+ }
141
+
9
142
  // src/kernel/container.ts
10
143
  var Container = class {
11
144
  entries = /* @__PURE__ */ new Map();
12
145
  bind(token, factory, opts = {}) {
13
146
  if (this.entries.has(token)) {
14
- throw new Error(`Container: token "${token.description ?? "unknown"}" already bound`);
147
+ throw new WrongStackError({
148
+ message: `Container: token "${token.description ?? "unknown"}" already bound`,
149
+ code: "CONTAINER_TOKEN_ALREADY_BOUND",
150
+ subsystem: "container",
151
+ context: { token: token.description }
152
+ });
15
153
  }
16
154
  this.entries.set(token, {
17
155
  factory,
@@ -23,9 +161,12 @@ var Container = class {
23
161
  override(token, factory, opts = {}) {
24
162
  const existing = this.entries.get(token);
25
163
  if (!existing) {
26
- throw new Error(
27
- `Container: cannot override "${token.description ?? "unknown"}" \u2014 not bound`
28
- );
164
+ throw new WrongStackError({
165
+ message: `Container: cannot override "${token.description ?? "unknown"}" \u2014 not bound`,
166
+ code: "CONTAINER_TOKEN_NOT_BOUND",
167
+ subsystem: "container",
168
+ context: { token: token.description }
169
+ });
29
170
  }
30
171
  this.entries.set(token, {
31
172
  factory,
@@ -37,9 +178,12 @@ var Container = class {
37
178
  decorate(token, decorator, owner = "core") {
38
179
  const existing = this.entries.get(token);
39
180
  if (!existing) {
40
- throw new Error(
41
- `Container: cannot decorate "${token.description ?? "unknown"}" \u2014 not bound`
42
- );
181
+ throw new WrongStackError({
182
+ message: `Container: cannot decorate "${token.description ?? "unknown"}" \u2014 not bound`,
183
+ code: "CONTAINER_TOKEN_NOT_BOUND",
184
+ subsystem: "container",
185
+ context: { token: token.description }
186
+ });
43
187
  }
44
188
  existing.decorators.push(decorator);
45
189
  existing.cache = void 0;
@@ -48,9 +192,12 @@ var Container = class {
48
192
  resolve(token) {
49
193
  const entry = this.entries.get(token);
50
194
  if (!entry) {
51
- throw new Error(
52
- `Container: token "${token.description ?? "unknown"}" not bound`
53
- );
195
+ throw new WrongStackError({
196
+ message: `Container: token "${token.description ?? "unknown"}" not bound`,
197
+ code: "CONTAINER_TOKEN_NOT_BOUND",
198
+ subsystem: "container",
199
+ context: { token: token.description }
200
+ });
54
201
  }
55
202
  if (entry.singleton && entry.cache !== void 0) {
56
203
  return entry.cache;
@@ -114,6 +261,20 @@ var Container = class {
114
261
  // src/kernel/pipeline.ts
115
262
  var Pipeline = class {
116
263
  chain = [];
264
+ errorHandler;
265
+ /**
266
+ * Install an error boundary. When a middleware throws or rejects, the
267
+ * handler is called and decides whether to swallow (continue with the
268
+ * pre-handler value) or rethrow. Without a handler, errors propagate.
269
+ *
270
+ * Wire one per pipeline at boot — the host CLI typically installs a
271
+ * single boundary that logs to the operational log and emits a
272
+ * `pipeline.error` event for /diag.
273
+ */
274
+ setErrorHandler(handler) {
275
+ this.errorHandler = handler;
276
+ return this;
277
+ }
117
278
  use(mw) {
118
279
  this.ensureUnique(mw.name);
119
280
  this.chain.push(mw);
@@ -198,6 +359,7 @@ var Pipeline = class {
198
359
  async run(input) {
199
360
  let index = -1;
200
361
  const chain = this.chain;
362
+ const errorHandler = this.errorHandler;
201
363
  const dispatch = async (i, value) => {
202
364
  if (i <= index) {
203
365
  throw new Error(`Pipeline: next() called multiple times in "${chain[index]?.name}"`);
@@ -205,7 +367,14 @@ var Pipeline = class {
205
367
  index = i;
206
368
  const mw = chain[i];
207
369
  if (!mw) return value;
208
- return mw.handler(value, (v) => dispatch(i + 1, v));
370
+ try {
371
+ return await mw.handler(value, (v) => dispatch(i + 1, v));
372
+ } catch (err) {
373
+ if (!errorHandler) throw err;
374
+ const policy = await errorHandler({ middleware: mw.name, owner: mw.owner, err });
375
+ if (policy === "rethrow") throw err;
376
+ return value;
377
+ }
209
378
  };
210
379
  return dispatch(0, input);
211
380
  }
@@ -266,6 +435,17 @@ var EventBus = class {
266
435
  clear() {
267
436
  this.listeners.clear();
268
437
  }
438
+ /**
439
+ * V2-D: introspection helper. Pass an `event` to count handlers for a
440
+ * single key, or omit to get the total across every event. Used by the
441
+ * leak-detection smoke test to flag handler accumulation across runs.
442
+ */
443
+ listenerCount(event) {
444
+ if (event !== void 0) return this.listeners.get(event)?.size ?? 0;
445
+ let total = 0;
446
+ for (const set of this.listeners.values()) total += set.size;
447
+ return total;
448
+ }
269
449
  };
270
450
 
271
451
  // src/kernel/tokens.ts
@@ -279,6 +459,7 @@ var TOKENS = {
279
459
  Compactor: t("Compactor"),
280
460
  PathResolver: t("PathResolver"),
281
461
  ConfigLoader: t("ConfigLoader"),
462
+ ConfigStore: t("ConfigStore"),
282
463
  Renderer: t("Renderer"),
283
464
  InputReader: t("InputReader"),
284
465
  ErrorHandler: t("ErrorHandler"),
@@ -388,20 +569,26 @@ function asText(content) {
388
569
  }
389
570
 
390
571
  // src/types/provider.ts
391
- var ProviderError = class extends Error {
572
+ var ProviderError = class extends WrongStackError {
392
573
  status;
393
574
  retryable;
394
575
  providerId;
395
576
  body;
396
- cause;
397
577
  constructor(message, status, retryable, providerId, opts = {}) {
398
- super(message);
578
+ super({
579
+ message,
580
+ code: providerStatusToCode(status, opts.body?.type),
581
+ subsystem: "provider",
582
+ severity: status >= 500 ? "error" : "warning",
583
+ recoverable: retryable,
584
+ context: { providerId, status },
585
+ cause: opts.cause
586
+ });
399
587
  this.name = "ProviderError";
400
588
  this.status = status;
401
589
  this.retryable = retryable;
402
590
  this.providerId = providerId;
403
591
  this.body = opts.body;
404
- this.cause = opts.cause;
405
592
  }
406
593
  /**
407
594
  * Render a one-line, user-facing description. Designed for the CLI/TUI
@@ -442,6 +629,16 @@ function describeStatus(status, type) {
442
629
  function truncate(s, n) {
443
630
  return s.length <= n ? s : `${s.slice(0, n - 1)}\u2026`;
444
631
  }
632
+ function providerStatusToCode(status, type) {
633
+ if (status === 0) return "PROVIDER_NETWORK_ERROR";
634
+ if (type === "rate_limit_error" || status === 429) return "PROVIDER_RATE_LIMITED";
635
+ if (type === "authentication_error" || status === 401) return "PROVIDER_AUTH_FAILED";
636
+ if (type === "overloaded_error" || status === 529) return "PROVIDER_OVERLOADED";
637
+ if (type === "invalid_request_error" || status === 400) return "PROVIDER_INVALID_REQUEST";
638
+ if (status === 408) return "PROVIDER_NETWORK_ERROR";
639
+ if (status >= 500) return "PROVIDER_SERVER_ERROR";
640
+ return "PROVIDER_INVALID_REQUEST";
641
+ }
445
642
 
446
643
  // src/types/secret-vault.ts
447
644
  var ENCRYPTED_PREFIX = "enc:v1:";
@@ -668,8 +865,11 @@ async function atomicWrite(targetPath, content, opts = {}) {
668
865
  }
669
866
  try {
670
867
  const fh = await fsp.open(tmp, "r+");
671
- await fh.sync();
672
- await fh.close();
868
+ try {
869
+ await fh.sync();
870
+ } finally {
871
+ await fh.close();
872
+ }
673
873
  } catch {
674
874
  }
675
875
  let mode;
@@ -1107,6 +1307,122 @@ function createToolOutputSerializer(opts = {}) {
1107
1307
  }
1108
1308
  return { serialize, enforceCap, capBytes };
1109
1309
  }
1310
+
1311
+ // src/utils/token-estimate.ts
1312
+ var RoughTokenEstimate = (text) => Math.max(1, Math.ceil(text.length / 4));
1313
+ function estimateToolInputTokens(input) {
1314
+ if (typeof input === "string") return RoughTokenEstimate(input);
1315
+ if (input !== null && typeof input === "object" && "__tokenEstimate" in input) {
1316
+ return input.__tokenEstimate;
1317
+ }
1318
+ const str = typeof input === "object" ? JSON.stringify(input) : String(input);
1319
+ const estimate = RoughTokenEstimate(str);
1320
+ if (input !== null && typeof input === "object" && !Array.isArray(input)) {
1321
+ input.__tokenEstimate = estimate;
1322
+ }
1323
+ return estimate;
1324
+ }
1325
+ function estimateToolResultTokens(content) {
1326
+ if (typeof content === "string") return RoughTokenEstimate(content);
1327
+ return RoughTokenEstimate(JSON.stringify(content));
1328
+ }
1329
+ function estimateTextTokens(text) {
1330
+ return RoughTokenEstimate(text);
1331
+ }
1332
+
1333
+ // src/utils/json-schema-validate.ts
1334
+ function validateAgainstSchema(value, schema) {
1335
+ const errors = [];
1336
+ walk(value, schema, "", errors);
1337
+ return { ok: errors.length === 0, errors };
1338
+ }
1339
+ function walk(value, schema, path15, errors) {
1340
+ if (schema.enum !== void 0) {
1341
+ if (!schema.enum.some((e) => deepEqual(e, value))) {
1342
+ errors.push({
1343
+ path: path15 || "<root>",
1344
+ message: `expected one of ${JSON.stringify(schema.enum)}, got ${JSON.stringify(value)}`
1345
+ });
1346
+ return;
1347
+ }
1348
+ }
1349
+ if (typeof schema.type === "string") {
1350
+ if (!checkType(value, schema.type)) {
1351
+ errors.push({
1352
+ path: path15 || "<root>",
1353
+ message: `expected ${schema.type}, got ${describeType(value)}`
1354
+ });
1355
+ return;
1356
+ }
1357
+ }
1358
+ if (schema.type === "object" && isPlainObject(value)) {
1359
+ const obj = value;
1360
+ for (const req of schema.required ?? []) {
1361
+ if (!(req in obj)) {
1362
+ errors.push({ path: joinPath(path15, req), message: "required property missing" });
1363
+ }
1364
+ }
1365
+ if (schema.properties) {
1366
+ for (const [key, subSchema] of Object.entries(schema.properties)) {
1367
+ if (key in obj) {
1368
+ walk(obj[key], subSchema, joinPath(path15, key), errors);
1369
+ }
1370
+ }
1371
+ }
1372
+ }
1373
+ if (schema.type === "array" && Array.isArray(value) && schema.items) {
1374
+ value.forEach((item, i) => walk(item, schema.items, `${path15}[${i}]`, errors));
1375
+ }
1376
+ }
1377
+ function checkType(value, type) {
1378
+ switch (type) {
1379
+ case "string":
1380
+ return typeof value === "string";
1381
+ case "number":
1382
+ return typeof value === "number" && !Number.isNaN(value);
1383
+ case "integer":
1384
+ return typeof value === "number" && Number.isInteger(value);
1385
+ case "boolean":
1386
+ return typeof value === "boolean";
1387
+ case "null":
1388
+ return value === null;
1389
+ case "array":
1390
+ return Array.isArray(value);
1391
+ case "object":
1392
+ return isPlainObject(value);
1393
+ default:
1394
+ return true;
1395
+ }
1396
+ }
1397
+ function isPlainObject(v) {
1398
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1399
+ }
1400
+ function describeType(v) {
1401
+ if (v === null) return "null";
1402
+ if (Array.isArray(v)) return "array";
1403
+ return typeof v;
1404
+ }
1405
+ function joinPath(parent, key) {
1406
+ if (!parent) return key;
1407
+ return `${parent}.${key}`;
1408
+ }
1409
+ function deepEqual(a, b) {
1410
+ if (a === b) return true;
1411
+ if (typeof a !== typeof b) return false;
1412
+ if (a === null || b === null) return a === b;
1413
+ if (Array.isArray(a) && Array.isArray(b)) {
1414
+ return a.length === b.length && a.every((v, i) => deepEqual(v, b[i]));
1415
+ }
1416
+ if (typeof a === "object" && typeof b === "object") {
1417
+ const ak = Object.keys(a);
1418
+ const bk = Object.keys(b);
1419
+ if (ak.length !== bk.length) return false;
1420
+ return ak.every(
1421
+ (k) => deepEqual(a[k], b[k])
1422
+ );
1423
+ }
1424
+ return false;
1425
+ }
1110
1426
  var LEVEL_RANK = {
1111
1427
  error: 0,
1112
1428
  warn: 1,
@@ -1133,7 +1449,7 @@ var DefaultLogger = class _DefaultLogger {
1133
1449
  this.pretty = opts.pretty ?? true;
1134
1450
  if (this.file) {
1135
1451
  try {
1136
- fs4.mkdirSync(path2.dirname(this.file), { recursive: true });
1452
+ fs5.mkdirSync(path2.dirname(this.file), { recursive: true });
1137
1453
  } catch {
1138
1454
  }
1139
1455
  }
@@ -1172,7 +1488,7 @@ var DefaultLogger = class _DefaultLogger {
1172
1488
  }
1173
1489
  if (this.file) {
1174
1490
  try {
1175
- fs4.appendFileSync(this.file, `${JSON.stringify(entry)}
1491
+ fs5.appendFileSync(this.file, `${JSON.stringify(entry)}
1176
1492
  `);
1177
1493
  } catch {
1178
1494
  }
@@ -1220,7 +1536,7 @@ var DefaultPathResolver = class {
1220
1536
  while (dir !== root) {
1221
1537
  for (const marker of PROJECT_MARKERS) {
1222
1538
  try {
1223
- fs4.accessSync(path2.join(dir, marker));
1539
+ fs5.accessSync(path2.join(dir, marker));
1224
1540
  return dir;
1225
1541
  } catch {
1226
1542
  }
@@ -1235,7 +1551,7 @@ var DefaultPathResolver = class {
1235
1551
  const abs = path2.isAbsolute(input) ? input : path2.resolve(this.cwd, input);
1236
1552
  let real;
1237
1553
  try {
1238
- real = fs4.realpathSync(abs);
1554
+ real = fs5.realpathSync(abs);
1239
1555
  } catch {
1240
1556
  real = path2.normalize(abs);
1241
1557
  }
@@ -1259,267 +1575,81 @@ var DefaultPathResolver = class {
1259
1575
  }
1260
1576
  };
1261
1577
 
1262
- // src/defaults/secret-scrubber.ts
1263
- var PATTERNS = [
1264
- // Anchored at the start where possible so partial matches inside larger
1265
- // strings don't trigger false positives.
1266
- { type: "anthropic_key", regex: /(?<![A-Za-z0-9])sk-ant-api\d+-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g },
1267
- { type: "openai_key", regex: /(?<![A-Za-z0-9])sk-(?:proj-)?[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g },
1268
- { type: "github_pat", regex: /(?<![A-Za-z0-9])ghp_[A-Za-z0-9]{36,}(?![A-Za-z0-9])/g },
1269
- { type: "github_pat_v2", regex: /(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{50,}(?![A-Za-z0-9])/g },
1270
- { type: "aws_access_key", regex: /(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}(?![A-Za-z0-9])/g },
1271
- { type: "gcp_key", regex: /(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}(?![A-Za-z0-9])/g },
1272
- { type: "slack_token", regex: /(?<![A-Za-z0-9-])xox[abpos]-[A-Za-z0-9-]{10,}(?![A-Za-z0-9-])/g },
1273
- { type: "stripe_key", regex: /(?<![A-Za-z0-9])sk_(?:live|test)_[A-Za-z0-9]{24,}(?![A-Za-z0-9])/g },
1274
- { type: "twilio_sid", regex: /(?<![A-Za-z0-9])AC[a-f0-9]{32}(?![A-Za-z0-9])/g },
1275
- {
1276
- type: "jwt",
1277
- // Anchored: look for literal "eyJ" which is unambiguous for JWT header
1278
- regex: /(?<![A-Za-z0-9/+=])eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}(?![A-Za-z0-9/+=])/g
1279
- },
1280
- {
1281
- type: "private_key",
1282
- // Anchored: start must be BEGIN, end must be END with no extra dashes after END
1283
- regex: /(?:^|\n)-----BEGIN (?:RSA|EC|OPENSSH|DSA|PGP)? ?PRIVATE KEY-----[\s\S]*?-----END[^-]*-----(?:\n|$)/g
1284
- },
1285
- { type: "mongodb_uri", regex: /mongodb(?:\+srv)?:\/\/[^\s"'`]+/g },
1286
- { type: "postgres_uri", regex: /postgres(?:ql)?:\/\/[^\s"'`]+/g },
1287
- { type: "mysql_uri", regex: /mysql:\/\/[^\s"'`]+/g },
1288
- { type: "redis_uri", regex: /redis:\/\/[^\s"'`]+/g },
1289
- { type: "bearer_token", regex: /(?<![A-Za-z0-9_.~+/-])Bearer\s+[A-Za-z0-9._~+/-]{20,}=*(?![A-Za-z0-9_.~+/-])/g },
1290
- {
1291
- type: "high_entropy_env",
1292
- // Value-side word boundary + length gate to avoid matching short random strings
1293
- regex: /\b([A-Z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD))\s*[:=]\s*['"]?([A-Za-z0-9_/+=-]{20,})['"]?(?!\s*[A-Za-z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD))/g
1578
+ // src/defaults/token-counter.ts
1579
+ var DefaultTokenCounter = class {
1580
+ input = 0;
1581
+ output = 0;
1582
+ cacheRead = 0;
1583
+ cacheWrite = 0;
1584
+ costInput = 0;
1585
+ costOutput = 0;
1586
+ registry;
1587
+ providerId;
1588
+ events;
1589
+ priceCache = /* @__PURE__ */ new Map();
1590
+ constructor(opts = {}) {
1591
+ this.registry = opts.registry;
1592
+ this.providerId = opts.providerId;
1593
+ this.events = opts.events;
1294
1594
  }
1295
- ];
1296
- var DefaultSecretScrubber = class {
1297
- scrub(text) {
1298
- if (!text) return text;
1299
- let out = text;
1300
- for (const p of PATTERNS) {
1301
- out = out.replace(p.regex, (_match, group1, group2) => {
1302
- if (p.type === "high_entropy_env" && group1 && group2) {
1303
- return `${group1}=[REDACTED:${p.type}]`;
1595
+ account(usage, model) {
1596
+ this.input += usage.input;
1597
+ this.output += usage.output;
1598
+ this.cacheRead += usage.cacheRead ?? 0;
1599
+ this.cacheWrite += usage.cacheWrite ?? 0;
1600
+ const price = model ? this.priceCache.get(model) : void 0;
1601
+ if (price) {
1602
+ this.applyPrice(usage, price);
1603
+ } else if (this.registry && this.providerId && model) {
1604
+ void this.registry.getModel(this.providerId, model).then((m) => {
1605
+ if (m) {
1606
+ const p = priceFromModel(m);
1607
+ this.priceCache.set(model, p);
1608
+ this.applyPrice(usage, p);
1304
1609
  }
1305
- return `[REDACTED:${p.type}]`;
1610
+ }).catch(() => {
1611
+ this.events?.emit("token.cost_estimate_unavailable", { model: model ?? "<unknown>" });
1612
+ return void 0;
1306
1613
  });
1307
1614
  }
1308
- return out;
1309
1615
  }
1310
- scrubObject(obj) {
1311
- const seen = /* @__PURE__ */ new WeakSet();
1312
- const visit = (v) => {
1313
- if (typeof v === "string") return this.scrub(v);
1314
- if (v === null || typeof v !== "object") return v;
1315
- if (seen.has(v)) return v;
1316
- seen.add(v);
1317
- if (Array.isArray(v)) return v.map(visit);
1318
- const out = {};
1319
- for (const [k, val] of Object.entries(v)) {
1320
- out[k] = visit(val);
1321
- }
1322
- return out;
1616
+ /** Synchronous variant for code paths that have already resolved the model. */
1617
+ accountWithModel(usage, resolved) {
1618
+ this.input += usage.input;
1619
+ this.output += usage.output;
1620
+ this.cacheRead += usage.cacheRead ?? 0;
1621
+ this.cacheWrite += usage.cacheWrite ?? 0;
1622
+ const price = priceFromModel(resolved);
1623
+ this.priceCache.set(resolved.modelId, price);
1624
+ this.applyPrice(usage, price);
1625
+ }
1626
+ total() {
1627
+ return {
1628
+ input: this.input,
1629
+ output: this.output,
1630
+ cacheRead: this.cacheRead,
1631
+ cacheWrite: this.cacheWrite
1323
1632
  };
1324
- return visit(obj);
1325
1633
  }
1326
- };
1327
-
1328
- // src/defaults/retry-policy.ts
1329
- var DefaultRetryPolicy = class {
1330
- shouldRetry(err, attempt) {
1331
- if (err instanceof ProviderError) {
1332
- if (!err.retryable) return false;
1333
- return attempt < this.maxAttempts(err);
1334
- }
1335
- const msg = err.message ?? "";
1336
- const isNetwork = /ECONN|ETIMEDOUT|ETIME|ENOTFOUND|EAI_AGAIN|fetch failed/i.test(msg);
1337
- if (isNetwork) return attempt < 2;
1338
- return false;
1634
+ estimateCost() {
1635
+ return {
1636
+ input: round4(this.costInput),
1637
+ output: round4(this.costOutput),
1638
+ total: round4(this.costInput + this.costOutput),
1639
+ currency: "USD"
1640
+ };
1339
1641
  }
1340
- maxAttempts(err) {
1341
- if (err instanceof ProviderError) {
1342
- if (err.status === 429) return 5;
1343
- if (err.status === 529) return 3;
1344
- if (err.status >= 500) return 3;
1345
- return 0;
1346
- }
1347
- return 2;
1642
+ cacheStats() {
1643
+ const denom = this.cacheRead + this.input;
1644
+ return {
1645
+ readTokens: this.cacheRead,
1646
+ writeTokens: this.cacheWrite,
1647
+ hitRatio: denom === 0 ? 0 : this.cacheRead / denom
1648
+ };
1348
1649
  }
1349
- delayMs(attempt) {
1350
- const base = 1e3;
1351
- const exp = base * 2 ** attempt;
1352
- const jitter = Math.random() * base;
1353
- return Math.min(3e4, exp + jitter);
1354
- }
1355
- };
1356
-
1357
- // src/defaults/error-handler.ts
1358
- function buildRecoveryStrategies(opts) {
1359
- return [
1360
- {
1361
- label: "context_overflow_reduce",
1362
- compactor: opts?.compactor,
1363
- async attempt(err, ctx) {
1364
- if (err instanceof ProviderError && (err.status === 413 || /context|too long|tokens/i.test(err.message))) {
1365
- if (this.compactor) {
1366
- try {
1367
- const report = await this.compactor.compact(ctx, { aggressive: true });
1368
- if (report.after < report.before) {
1369
- return {
1370
- content: [{ type: "text", text: "[context compacted automatically \u2014 please retry]" }],
1371
- stopReason: "end_turn",
1372
- usage: { input: 0, output: 0 },
1373
- model: ctx.model
1374
- };
1375
- }
1376
- } catch {
1377
- }
1378
- }
1379
- return null;
1380
- }
1381
- return null;
1382
- }
1383
- },
1384
- {
1385
- label: "rate_limit_backoff",
1386
- async attempt(err, ctx) {
1387
- if (err instanceof ProviderError && err.status === 429) {
1388
- const delayMs = err.body?.retryAfterMs ?? 5e3;
1389
- const delay = Math.max(1e3, Math.min(delayMs, 6e4));
1390
- await new Promise((r) => setTimeout(r, delay));
1391
- return {
1392
- content: [{ type: "text", text: "[rate limit backoff applied \u2014 please retry]" }],
1393
- stopReason: "end_turn",
1394
- usage: { input: 0, output: 0 },
1395
- model: ctx.model
1396
- };
1397
- }
1398
- return null;
1399
- }
1400
- },
1401
- {
1402
- label: "downgrade_model",
1403
- async attempt(err, ctx) {
1404
- if (err instanceof ProviderError && (err.status === 429 || err.status === 529 || err.status >= 500)) {
1405
- return null;
1406
- }
1407
- return null;
1408
- }
1409
- }
1410
- ];
1411
- }
1412
- var DEFAULT_RECOVERY_STRATEGIES = buildRecoveryStrategies();
1413
- var DefaultErrorHandler = class {
1414
- strategies;
1415
- constructor(strategies = DEFAULT_RECOVERY_STRATEGIES) {
1416
- this.strategies = strategies;
1417
- }
1418
- classify(err) {
1419
- if (err instanceof DOMException && err.name === "AbortError") {
1420
- return { kind: "abort", retryable: false };
1421
- }
1422
- if (err instanceof Error && err.name === "AbortError") {
1423
- return { kind: "abort", retryable: false };
1424
- }
1425
- if (err instanceof ProviderError) {
1426
- if (err.status === 429) return { kind: "rate_limit", retryable: true };
1427
- if (err.status === 529) return { kind: "overloaded", retryable: true };
1428
- if (err.status >= 500) return { kind: "server", retryable: true };
1429
- if (err.status === 413 || /context|too long|tokens/i.test(err.message)) {
1430
- return { kind: "context_overflow", retryable: false };
1431
- }
1432
- if (err.status >= 400) return { kind: "client", retryable: false };
1433
- }
1434
- if (err instanceof Error && /ECONN|ETIMEDOUT|ETIME|ENOTFOUND|EAI_AGAIN|fetch failed/i.test(err.message)) {
1435
- return { kind: "network", retryable: true };
1436
- }
1437
- return { kind: "unknown", retryable: false };
1438
- }
1439
- async recover(err, ctx) {
1440
- for (const strategy of this.strategies) {
1441
- const result = await strategy.attempt(err, ctx);
1442
- if (result !== null) return result;
1443
- }
1444
- return null;
1445
- }
1446
- };
1447
-
1448
- // src/defaults/token-counter.ts
1449
- var DefaultTokenCounter = class {
1450
- input = 0;
1451
- output = 0;
1452
- cacheRead = 0;
1453
- cacheWrite = 0;
1454
- costInput = 0;
1455
- costOutput = 0;
1456
- registry;
1457
- providerId;
1458
- events;
1459
- priceCache = /* @__PURE__ */ new Map();
1460
- constructor(opts = {}) {
1461
- this.registry = opts.registry;
1462
- this.providerId = opts.providerId;
1463
- this.events = opts.events;
1464
- }
1465
- account(usage, model) {
1466
- this.input += usage.input;
1467
- this.output += usage.output;
1468
- this.cacheRead += usage.cacheRead ?? 0;
1469
- this.cacheWrite += usage.cacheWrite ?? 0;
1470
- const price = model ? this.priceCache.get(model) : void 0;
1471
- if (price) {
1472
- this.applyPrice(usage, price);
1473
- } else if (this.registry && this.providerId && model) {
1474
- void this.registry.getModel(this.providerId, model).then((m) => {
1475
- if (m) {
1476
- const p = priceFromModel(m);
1477
- this.priceCache.set(model, p);
1478
- this.applyPrice(usage, p);
1479
- }
1480
- }).catch(() => {
1481
- this.events?.emit("token.cost_estimate_unavailable", { model: model ?? "<unknown>" });
1482
- return void 0;
1483
- });
1484
- }
1485
- }
1486
- /** Synchronous variant for code paths that have already resolved the model. */
1487
- accountWithModel(usage, resolved) {
1488
- this.input += usage.input;
1489
- this.output += usage.output;
1490
- this.cacheRead += usage.cacheRead ?? 0;
1491
- this.cacheWrite += usage.cacheWrite ?? 0;
1492
- const price = priceFromModel(resolved);
1493
- this.priceCache.set(resolved.modelId, price);
1494
- this.applyPrice(usage, price);
1495
- }
1496
- total() {
1497
- return {
1498
- input: this.input,
1499
- output: this.output,
1500
- cacheRead: this.cacheRead,
1501
- cacheWrite: this.cacheWrite
1502
- };
1503
- }
1504
- estimateCost() {
1505
- return {
1506
- input: round4(this.costInput),
1507
- output: round4(this.costOutput),
1508
- total: round4(this.costInput + this.costOutput),
1509
- currency: "USD"
1510
- };
1511
- }
1512
- cacheStats() {
1513
- const denom = this.cacheRead + this.input;
1514
- return {
1515
- readTokens: this.cacheRead,
1516
- writeTokens: this.cacheWrite,
1517
- hitRatio: denom === 0 ? 0 : this.cacheRead / denom
1518
- };
1519
- }
1520
- /** Invalidate cached prices so the next account() call fetches fresh data. */
1521
- invalidateCache() {
1522
- this.priceCache.clear();
1650
+ /** Invalidate cached prices so the next account() call fetches fresh data. */
1651
+ invalidateCache() {
1652
+ this.priceCache.clear();
1523
1653
  }
1524
1654
  reset() {
1525
1655
  this.input = 0;
@@ -1840,151 +1970,28 @@ function userInputTitle(content) {
1840
1970
  const text = content.filter((b) => b.type === "text").map((b) => b.text).join(" ");
1841
1971
  return (text || "(non-text input)").slice(0, 60);
1842
1972
  }
1843
- var LOCK_FILE = "active.json";
1844
- var DEFAULT_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
1845
- var RecoveryLock = class {
1973
+ var QueueStore = class {
1846
1974
  file;
1847
- pid;
1848
- hostname;
1849
- maxAgeMs;
1850
- sessionStore;
1851
- probe;
1852
1975
  constructor(opts) {
1853
- this.file = path2.join(opts.dir, LOCK_FILE);
1854
- this.pid = opts.pid ?? process.pid;
1855
- this.hostname = opts.hostname ?? os3.hostname();
1856
- this.maxAgeMs = opts.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
1857
- this.sessionStore = opts.sessionStore;
1858
- this.probe = opts.isPidAlive ?? defaultIsPidAlive;
1859
- }
1860
- /**
1861
- * Examine the lockfile and decide whether it represents an abandoned
1862
- * session. Returns `null` if the file is missing, points to a live
1863
- * instance, references a clean-closed session, is too old, or is
1864
- * malformed. Otherwise returns enough detail to prompt the user.
1865
- *
1866
- * Important: this is a read-only check. We never delete an active
1867
- * lock from here — if another wstack instance is alive, the caller
1868
- * should bail or run with a fresh session instead.
1869
- */
1870
- async checkAbandoned() {
1871
- const lock = await this.readLock();
1872
- if (!lock) return null;
1873
- const ageMs = Date.now() - new Date(lock.startedAt).getTime();
1874
- if (Number.isNaN(ageMs) || ageMs < 0) {
1875
- return null;
1876
- }
1877
- if (ageMs > this.maxAgeMs) return null;
1878
- if (lock.hostname === this.hostname && this.probe(lock.pid)) {
1879
- return null;
1880
- }
1881
- let messageCount = 0;
1882
- if (this.sessionStore) {
1883
- try {
1884
- const data = await this.sessionStore.load(lock.sessionId);
1885
- const closed = data.events.some((e) => e.type === "session_end");
1886
- if (closed) return null;
1887
- messageCount = data.messages.length;
1888
- } catch {
1889
- return null;
1890
- }
1891
- }
1892
- return {
1893
- sessionId: lock.sessionId,
1894
- pid: lock.pid,
1895
- startedAt: lock.startedAt,
1896
- ageMs,
1897
- messageCount
1898
- };
1899
- }
1900
- /**
1901
- * Claim the lock for the given session. Overwrites any existing lock
1902
- * — the caller should have already handled abandonment (via
1903
- * `checkAbandoned`) before calling this.
1904
- */
1905
- async write(sessionId) {
1906
- await ensureDir(path2.dirname(this.file));
1907
- const lock = {
1908
- v: 1,
1909
- sessionId,
1910
- pid: this.pid,
1911
- hostname: this.hostname,
1912
- startedAt: (/* @__PURE__ */ new Date()).toISOString()
1913
- };
1914
- const tmp = `${this.file}.tmp`;
1915
- await fsp.writeFile(tmp, JSON.stringify(lock), { mode: 384 });
1916
- await fsp.rename(tmp, this.file);
1976
+ this.file = path2.join(opts.dir, "queue.json");
1917
1977
  }
1918
- /**
1919
- * Release the lock. Idempotent silently succeeds if the file is
1920
- * already gone (e.g. someone else cleared it, or the directory was
1921
- * wiped).
1922
- */
1923
- async clear() {
1924
- try {
1925
- await fsp.unlink(this.file);
1926
- } catch (err) {
1927
- const code = err.code;
1928
- if (code === "ENOENT") return;
1929
- throw err;
1978
+ async write(items) {
1979
+ if (items.length === 0) {
1980
+ await this.clear();
1981
+ return;
1930
1982
  }
1983
+ await atomicWrite(this.file, JSON.stringify(items), { mode: 384 });
1931
1984
  }
1932
- async readLock() {
1985
+ async read() {
1933
1986
  let raw;
1934
1987
  try {
1935
1988
  raw = await fsp.readFile(this.file, "utf8");
1936
1989
  } catch (err) {
1937
1990
  const code = err.code;
1938
- if (code === "ENOENT") return null;
1939
- return null;
1991
+ if (code === "ENOENT") return [];
1992
+ return [];
1940
1993
  }
1941
- try {
1942
- const parsed = JSON.parse(raw);
1943
- if (!isLockFile(parsed)) return null;
1944
- return parsed;
1945
- } catch {
1946
- return null;
1947
- }
1948
- }
1949
- };
1950
- function isLockFile(v) {
1951
- if (typeof v !== "object" || v === null) return false;
1952
- const o = v;
1953
- return o["v"] === 1 && typeof o["sessionId"] === "string" && typeof o["pid"] === "number" && typeof o["hostname"] === "string" && typeof o["startedAt"] === "string";
1954
- }
1955
- function defaultIsPidAlive(pid) {
1956
- if (!Number.isInteger(pid) || pid <= 0) return false;
1957
- try {
1958
- process.kill(pid, 0);
1959
- return true;
1960
- } catch (err) {
1961
- const code = err.code;
1962
- if (code === "EPERM") return true;
1963
- return false;
1964
- }
1965
- }
1966
- var QueueStore = class {
1967
- file;
1968
- constructor(opts) {
1969
- this.file = path2.join(opts.dir, "queue.json");
1970
- }
1971
- async write(items) {
1972
- if (items.length === 0) {
1973
- await this.clear();
1974
- return;
1975
- }
1976
- await atomicWrite(this.file, JSON.stringify(items), { mode: 384 });
1977
- }
1978
- async read() {
1979
- let raw;
1980
- try {
1981
- raw = await fsp.readFile(this.file, "utf8");
1982
- } catch (err) {
1983
- const code = err.code;
1984
- if (code === "ENOENT") return [];
1985
- return [];
1986
- }
1987
- let parsed;
1994
+ let parsed;
1988
1995
  try {
1989
1996
  parsed = JSON.parse(raw);
1990
1997
  } catch {
@@ -2125,6 +2132,205 @@ function mergeAdjacentText(blocks) {
2125
2132
  }
2126
2133
  return out;
2127
2134
  }
2135
+ var MAX_BYTES_TOTAL = 32e3;
2136
+ var DefaultMemoryStore = class {
2137
+ files;
2138
+ constructor(opts) {
2139
+ this.files = {
2140
+ "project-agents": opts.paths.inProjectAgentsFile,
2141
+ "project-memory": opts.paths.projectMemory,
2142
+ "user-memory": opts.paths.globalMemory
2143
+ };
2144
+ }
2145
+ async readAll() {
2146
+ const parts = [];
2147
+ for (const scope of ["project-agents", "project-memory", "user-memory"]) {
2148
+ const body = await this.read(scope);
2149
+ if (body.trim()) parts.push(`## ${labelOf(scope)}
2150
+
2151
+ ${body.trim()}`);
2152
+ }
2153
+ return parts.join("\n\n");
2154
+ }
2155
+ async read(scope) {
2156
+ try {
2157
+ return await fsp.readFile(this.files[scope], "utf8");
2158
+ } catch {
2159
+ return "";
2160
+ }
2161
+ }
2162
+ async remember(text, scope = "project-memory") {
2163
+ const file = this.files[scope];
2164
+ await ensureDir(path2.dirname(file));
2165
+ let existing = "";
2166
+ try {
2167
+ existing = await fsp.readFile(file, "utf8");
2168
+ } catch {
2169
+ }
2170
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
2171
+ const id = `mem_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
2172
+ const entry = `
2173
+ - [${ts}] ${id} ${text.replace(/\n/g, " ")}
2174
+ `;
2175
+ const next = existing.trim() ? existing.replace(/\n+$/, "") + entry : `# WrongStack Memory
2176
+ ${entry}`;
2177
+ await atomicWrite(file, next);
2178
+ const buf = Buffer.byteLength(next, "utf8");
2179
+ if (buf > MAX_BYTES_TOTAL) {
2180
+ await this.consolidate(scope);
2181
+ }
2182
+ }
2183
+ async forget(query, scope = "project-memory") {
2184
+ const file = this.files[scope];
2185
+ let existing;
2186
+ try {
2187
+ existing = await fsp.readFile(file, "utf8");
2188
+ } catch {
2189
+ return 0;
2190
+ }
2191
+ const needle = query.toLowerCase();
2192
+ const idMatcher = /mem_\d+_\w+/;
2193
+ let removed = 0;
2194
+ const lines = existing.split("\n").filter((line) => {
2195
+ const trimmed = line.trim();
2196
+ if (!trimmed.startsWith("- ")) return true;
2197
+ if (idMatcher.test(query)) {
2198
+ const afterBracket = trimmed.indexOf("] ");
2199
+ if (afterBracket !== -1) {
2200
+ const afterTs = trimmed.slice(afterBracket + 2);
2201
+ const entryIdMatch = /^mem_\d+_\w+/.exec(afterTs);
2202
+ if (entryIdMatch && entryIdMatch[0] === query) {
2203
+ removed++;
2204
+ return false;
2205
+ }
2206
+ }
2207
+ }
2208
+ if (trimmed.toLowerCase().includes(needle)) {
2209
+ removed++;
2210
+ return false;
2211
+ }
2212
+ return true;
2213
+ });
2214
+ if (removed > 0) {
2215
+ await atomicWrite(file, lines.join("\n"));
2216
+ }
2217
+ return removed;
2218
+ }
2219
+ async consolidate(scope) {
2220
+ const file = this.files[scope];
2221
+ let existing;
2222
+ try {
2223
+ existing = await fsp.readFile(file, "utf8");
2224
+ } catch {
2225
+ return;
2226
+ }
2227
+ const seen = /* @__PURE__ */ new Set();
2228
+ const lines = existing.split("\n").filter((line) => {
2229
+ const trimmed = line.trim();
2230
+ if (!trimmed.startsWith("- ")) return true;
2231
+ const norm = trimmed.replace(/\[[^\]]+\]/, "").replace(/\bmem_\d+_\w+\s*/, "").trim().toLowerCase();
2232
+ if (seen.has(norm)) return false;
2233
+ seen.add(norm);
2234
+ return true;
2235
+ });
2236
+ const next = lines.join("\n");
2237
+ try {
2238
+ await atomicWrite(file, next);
2239
+ } catch {
2240
+ return;
2241
+ }
2242
+ const backup = `${file}.bak.${Date.now()}`;
2243
+ try {
2244
+ await fsp.copyFile(file, backup);
2245
+ } catch {
2246
+ }
2247
+ }
2248
+ async clear(scope) {
2249
+ if (scope) {
2250
+ await atomicWrite(this.files[scope], "");
2251
+ } else {
2252
+ for (const s of ["project-agents", "project-memory", "user-memory"]) {
2253
+ await atomicWrite(this.files[s], "");
2254
+ }
2255
+ }
2256
+ }
2257
+ };
2258
+ function labelOf(scope) {
2259
+ switch (scope) {
2260
+ case "project-agents":
2261
+ return "Project AGENTS.md";
2262
+ case "project-memory":
2263
+ return "Project memory";
2264
+ case "user-memory":
2265
+ return "User memory";
2266
+ }
2267
+ }
2268
+
2269
+ // src/defaults/secret-scrubber.ts
2270
+ var PATTERNS = [
2271
+ // Anchored at the start where possible so partial matches inside larger
2272
+ // strings don't trigger false positives.
2273
+ { type: "anthropic_key", regex: /(?<![A-Za-z0-9])sk-ant-api\d+-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g },
2274
+ { type: "openai_key", regex: /(?<![A-Za-z0-9])sk-(?:proj-)?[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g },
2275
+ { type: "github_pat", regex: /(?<![A-Za-z0-9])ghp_[A-Za-z0-9]{36,}(?![A-Za-z0-9])/g },
2276
+ { type: "github_pat_v2", regex: /(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{50,}(?![A-Za-z0-9])/g },
2277
+ { type: "aws_access_key", regex: /(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}(?![A-Za-z0-9])/g },
2278
+ { type: "gcp_key", regex: /(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}(?![A-Za-z0-9])/g },
2279
+ { type: "slack_token", regex: /(?<![A-Za-z0-9-])xox[abpos]-[A-Za-z0-9-]{10,}(?![A-Za-z0-9-])/g },
2280
+ { type: "stripe_key", regex: /(?<![A-Za-z0-9])sk_(?:live|test)_[A-Za-z0-9]{24,}(?![A-Za-z0-9])/g },
2281
+ { type: "twilio_sid", regex: /(?<![A-Za-z0-9])AC[a-f0-9]{32}(?![A-Za-z0-9])/g },
2282
+ {
2283
+ type: "jwt",
2284
+ // Anchored: look for literal "eyJ" which is unambiguous for JWT header
2285
+ regex: /(?<![A-Za-z0-9/+=])eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}(?![A-Za-z0-9/+=])/g
2286
+ },
2287
+ {
2288
+ type: "private_key",
2289
+ // Anchored: start must be BEGIN, end must be END with no extra dashes after END
2290
+ regex: /(?:^|\n)-----BEGIN (?:RSA|EC|OPENSSH|DSA|PGP)? ?PRIVATE KEY-----[\s\S]*?-----END[^-]*-----(?:\n|$)/g
2291
+ },
2292
+ { type: "mongodb_uri", regex: /mongodb(?:\+srv)?:\/\/[^\s"'`]+/g },
2293
+ { type: "postgres_uri", regex: /postgres(?:ql)?:\/\/[^\s"'`]+/g },
2294
+ { type: "mysql_uri", regex: /mysql:\/\/[^\s"'`]+/g },
2295
+ { type: "redis_uri", regex: /redis:\/\/[^\s"'`]+/g },
2296
+ { type: "bearer_token", regex: /(?<![A-Za-z0-9_.~+/-])Bearer\s+[A-Za-z0-9._~+/-]{20,}=*(?![A-Za-z0-9_.~+/-])/g },
2297
+ {
2298
+ type: "high_entropy_env",
2299
+ // Value-side word boundary + length gate to avoid matching short random strings
2300
+ regex: /\b([A-Z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD))\s*[:=]\s*['"]?([A-Za-z0-9_/+=-]{20,})['"]?(?!\s*[A-Za-z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD))/g
2301
+ }
2302
+ ];
2303
+ var DefaultSecretScrubber = class {
2304
+ scrub(text) {
2305
+ if (!text) return text;
2306
+ let out = text;
2307
+ for (const p of PATTERNS) {
2308
+ out = out.replace(p.regex, (_match, group1, group2) => {
2309
+ if (p.type === "high_entropy_env" && group1 && group2) {
2310
+ return `${group1}=[REDACTED:${p.type}]`;
2311
+ }
2312
+ return `[REDACTED:${p.type}]`;
2313
+ });
2314
+ }
2315
+ return out;
2316
+ }
2317
+ scrubObject(obj) {
2318
+ const seen = /* @__PURE__ */ new WeakSet();
2319
+ const visit = (v) => {
2320
+ if (typeof v === "string") return this.scrub(v);
2321
+ if (v === null || typeof v !== "object") return v;
2322
+ if (seen.has(v)) return v;
2323
+ seen.add(v);
2324
+ if (Array.isArray(v)) return v.map(visit);
2325
+ const out = {};
2326
+ for (const [k, val] of Object.entries(v)) {
2327
+ out[k] = visit(val);
2328
+ }
2329
+ return out;
2330
+ };
2331
+ return visit(obj);
2332
+ }
2333
+ };
2128
2334
  var KEY_BYTES = 32;
2129
2335
  var IV_BYTES = 12;
2130
2336
  var TAG_BYTES = 16;
@@ -2169,7 +2375,7 @@ var DefaultSecretVault = class {
2169
2375
  loadOrCreateKey() {
2170
2376
  if (this.key) return this.key;
2171
2377
  try {
2172
- const buf = fs4.readFileSync(this.keyFile);
2378
+ const buf = fs5.readFileSync(this.keyFile);
2173
2379
  if (buf.length !== KEY_BYTES) {
2174
2380
  throw new Error(`SecretVault: key file ${this.keyFile} has wrong size`);
2175
2381
  }
@@ -2178,13 +2384,13 @@ var DefaultSecretVault = class {
2178
2384
  } catch (err) {
2179
2385
  if (err.code !== "ENOENT") throw err;
2180
2386
  }
2181
- fs4.mkdirSync(path2.dirname(this.keyFile), { recursive: true });
2387
+ fs5.mkdirSync(path2.dirname(this.keyFile), { recursive: true });
2182
2388
  const key = randomBytes(KEY_BYTES);
2183
2389
  try {
2184
- fs4.writeFileSync(this.keyFile, key, { mode: 384, flag: "wx" });
2390
+ fs5.writeFileSync(this.keyFile, key, { mode: 384, flag: "wx" });
2185
2391
  } catch (err) {
2186
2392
  if (err.code !== "EEXIST") throw err;
2187
- const buf = fs4.readFileSync(this.keyFile);
2393
+ const buf = fs5.readFileSync(this.keyFile);
2188
2394
  if (buf.length !== KEY_BYTES) {
2189
2395
  throw new Error(`SecretVault: key file ${this.keyFile} has wrong size`);
2190
2396
  }
@@ -2196,23 +2402,23 @@ var DefaultSecretVault = class {
2196
2402
  }
2197
2403
  };
2198
2404
  function decryptConfigSecrets(cfg, vault) {
2199
- return walk(cfg, vault, (v) => vault.decrypt(v));
2405
+ return walk2(cfg, vault, (v) => vault.decrypt(v));
2200
2406
  }
2201
2407
  function encryptConfigSecrets(cfg, vault) {
2202
- return walk(cfg, vault, (v) => vault.encrypt(v));
2408
+ return walk2(cfg, vault, (v) => vault.encrypt(v));
2203
2409
  }
2204
- function walk(node, vault, transform) {
2410
+ function walk2(node, vault, transform) {
2205
2411
  if (node === null || node === void 0) return node;
2206
2412
  if (typeof node !== "object") return node;
2207
2413
  if (Array.isArray(node)) {
2208
- return node.map((item) => walk(item, vault, transform));
2414
+ return node.map((item) => walk2(item, vault, transform));
2209
2415
  }
2210
2416
  const out = {};
2211
2417
  for (const [k, v] of Object.entries(node)) {
2212
2418
  if (typeof v === "string" && isSecretField(k)) {
2213
2419
  out[k] = transform(v);
2214
2420
  } else if (typeof v === "object" && v !== null) {
2215
- out[k] = walk(v, vault, transform);
2421
+ out[k] = walk2(v, vault, transform);
2216
2422
  } else {
2217
2423
  out[k] = v;
2218
2424
  }
@@ -2296,145 +2502,12 @@ function deepMerge(a, b) {
2296
2502
  }
2297
2503
  return out;
2298
2504
  }
2299
- var MAX_BYTES_TOTAL = 32e3;
2300
- var DefaultMemoryStore = class {
2301
- files;
2302
- constructor(opts) {
2303
- this.files = {
2304
- "project-agents": opts.paths.inProjectAgentsFile,
2305
- "project-memory": opts.paths.projectMemory,
2306
- "user-memory": opts.paths.globalMemory
2307
- };
2308
- }
2309
- async readAll() {
2310
- const parts = [];
2311
- for (const scope of ["project-agents", "project-memory", "user-memory"]) {
2312
- const body = await this.read(scope);
2313
- if (body.trim()) parts.push(`## ${labelOf(scope)}
2314
-
2315
- ${body.trim()}`);
2316
- }
2317
- return parts.join("\n\n");
2318
- }
2319
- async read(scope) {
2320
- try {
2321
- return await fsp.readFile(this.files[scope], "utf8");
2322
- } catch {
2323
- return "";
2324
- }
2325
- }
2326
- async remember(text, scope = "project-memory") {
2327
- const file = this.files[scope];
2328
- await ensureDir(path2.dirname(file));
2329
- let existing = "";
2330
- try {
2331
- existing = await fsp.readFile(file, "utf8");
2332
- } catch {
2333
- }
2334
- const ts = (/* @__PURE__ */ new Date()).toISOString();
2335
- const id = `mem_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
2336
- const entry = `
2337
- - [${ts}] ${id} ${text.replace(/\n/g, " ")}
2338
- `;
2339
- const next = existing.trim() ? existing.replace(/\n+$/, "") + entry : `# WrongStack Memory
2340
- ${entry}`;
2341
- await atomicWrite(file, next);
2342
- const buf = Buffer.byteLength(next, "utf8");
2343
- if (buf > MAX_BYTES_TOTAL) {
2344
- await this.consolidate(scope);
2345
- }
2346
- }
2347
- async forget(query, scope = "project-memory") {
2348
- const file = this.files[scope];
2349
- let existing;
2350
- try {
2351
- existing = await fsp.readFile(file, "utf8");
2352
- } catch {
2353
- return 0;
2354
- }
2355
- const needle = query.toLowerCase();
2356
- const idMatcher = /mem_\d+_\w+/;
2357
- let removed = 0;
2358
- const lines = existing.split("\n").filter((line) => {
2359
- const trimmed = line.trim();
2360
- if (!trimmed.startsWith("- ")) return true;
2361
- if (idMatcher.test(query)) {
2362
- const afterBracket = trimmed.indexOf("] ");
2363
- if (afterBracket !== -1) {
2364
- const afterTs = trimmed.slice(afterBracket + 2);
2365
- const entryIdMatch = /^mem_\d+_\w+/.exec(afterTs);
2366
- if (entryIdMatch && entryIdMatch[0] === query) {
2367
- removed++;
2368
- return false;
2369
- }
2370
- }
2371
- }
2372
- if (trimmed.toLowerCase().includes(needle)) {
2373
- removed++;
2374
- return false;
2375
- }
2376
- return true;
2377
- });
2378
- if (removed > 0) {
2379
- await atomicWrite(file, lines.join("\n"));
2380
- }
2381
- return removed;
2382
- }
2383
- async consolidate(scope) {
2384
- const file = this.files[scope];
2385
- let existing;
2386
- try {
2387
- existing = await fsp.readFile(file, "utf8");
2388
- } catch {
2389
- return;
2390
- }
2391
- const seen = /* @__PURE__ */ new Set();
2392
- const lines = existing.split("\n").filter((line) => {
2393
- const trimmed = line.trim();
2394
- if (!trimmed.startsWith("- ")) return true;
2395
- const norm = trimmed.replace(/\[[^\]]+\]/, "").replace(/\bmem_\d+_\w+\s*/, "").trim().toLowerCase();
2396
- if (seen.has(norm)) return false;
2397
- seen.add(norm);
2398
- return true;
2399
- });
2400
- const next = lines.join("\n");
2401
- try {
2402
- await atomicWrite(file, next);
2403
- } catch {
2404
- return;
2405
- }
2406
- const backup = `${file}.bak.${Date.now()}`;
2407
- try {
2408
- await fsp.copyFile(file, backup);
2409
- } catch {
2410
- }
2411
- }
2412
- async clear(scope) {
2413
- if (scope) {
2414
- await atomicWrite(this.files[scope], "");
2415
- } else {
2416
- for (const s of ["project-agents", "project-memory", "user-memory"]) {
2417
- await atomicWrite(this.files[s], "");
2418
- }
2419
- }
2420
- }
2421
- };
2422
- function labelOf(scope) {
2423
- switch (scope) {
2424
- case "project-agents":
2425
- return "Project AGENTS.md";
2426
- case "project-memory":
2427
- return "Project memory";
2428
- case "user-memory":
2429
- return "User memory";
2430
- }
2431
- }
2432
- var DefaultPermissionPolicy = class {
2433
- policy = {};
2434
- loaded = false;
2435
- trustFile;
2436
- yolo;
2437
- promptDelegate;
2505
+ var DefaultPermissionPolicy = class {
2506
+ policy = {};
2507
+ loaded = false;
2508
+ trustFile;
2509
+ yolo;
2510
+ promptDelegate;
2438
2511
  constructor(opts) {
2439
2512
  this.trustFile = opts.trustFile;
2440
2513
  this.yolo = opts.yolo ?? false;
@@ -2530,6 +2603,126 @@ var DefaultPermissionPolicy = class {
2530
2603
  return void 0;
2531
2604
  }
2532
2605
  };
2606
+
2607
+ // src/defaults/retry-policy.ts
2608
+ var DefaultRetryPolicy = class {
2609
+ shouldRetry(err, attempt) {
2610
+ if (err instanceof ProviderError) {
2611
+ if (!err.retryable) return false;
2612
+ return attempt < this.maxAttempts(err);
2613
+ }
2614
+ const msg = err.message ?? "";
2615
+ const isNetwork = /ECONN|ETIMEDOUT|ETIME|ENOTFOUND|EAI_AGAIN|fetch failed/i.test(msg);
2616
+ if (isNetwork) return attempt < 2;
2617
+ return false;
2618
+ }
2619
+ maxAttempts(err) {
2620
+ if (err instanceof ProviderError) {
2621
+ if (err.status === 429) return 5;
2622
+ if (err.status === 529) return 3;
2623
+ if (err.status >= 500) return 3;
2624
+ return 0;
2625
+ }
2626
+ return 2;
2627
+ }
2628
+ delayMs(attempt) {
2629
+ const base = 1e3;
2630
+ const exp = base * 2 ** attempt;
2631
+ const jitter = Math.random() * base;
2632
+ return Math.min(3e4, exp + jitter);
2633
+ }
2634
+ };
2635
+
2636
+ // src/defaults/error-handler.ts
2637
+ function buildRecoveryStrategies(opts) {
2638
+ return [
2639
+ {
2640
+ label: "context_overflow_reduce",
2641
+ compactor: opts?.compactor,
2642
+ async attempt(err, ctx) {
2643
+ if (err instanceof ProviderError && (err.status === 413 || /context|too long|tokens/i.test(err.message))) {
2644
+ if (this.compactor) {
2645
+ try {
2646
+ const report = await this.compactor.compact(ctx, { aggressive: true });
2647
+ if (report.after < report.before) {
2648
+ return {
2649
+ content: [{ type: "text", text: "[context compacted automatically \u2014 please retry]" }],
2650
+ stopReason: "end_turn",
2651
+ usage: { input: 0, output: 0 },
2652
+ model: ctx.model
2653
+ };
2654
+ }
2655
+ } catch {
2656
+ }
2657
+ }
2658
+ return null;
2659
+ }
2660
+ return null;
2661
+ }
2662
+ },
2663
+ {
2664
+ label: "rate_limit_backoff",
2665
+ async attempt(err, ctx) {
2666
+ if (err instanceof ProviderError && err.status === 429) {
2667
+ const delayMs = err.body?.retryAfterMs ?? 5e3;
2668
+ const delay = Math.max(1e3, Math.min(delayMs, 6e4));
2669
+ await new Promise((r) => setTimeout(r, delay));
2670
+ return {
2671
+ content: [{ type: "text", text: "[rate limit backoff applied \u2014 please retry]" }],
2672
+ stopReason: "end_turn",
2673
+ usage: { input: 0, output: 0 },
2674
+ model: ctx.model
2675
+ };
2676
+ }
2677
+ return null;
2678
+ }
2679
+ },
2680
+ {
2681
+ label: "downgrade_model",
2682
+ async attempt(err, ctx) {
2683
+ if (err instanceof ProviderError && (err.status === 429 || err.status === 529 || err.status >= 500)) {
2684
+ return null;
2685
+ }
2686
+ return null;
2687
+ }
2688
+ }
2689
+ ];
2690
+ }
2691
+ var DEFAULT_RECOVERY_STRATEGIES = buildRecoveryStrategies();
2692
+ var DefaultErrorHandler = class {
2693
+ strategies;
2694
+ constructor(strategies = DEFAULT_RECOVERY_STRATEGIES) {
2695
+ this.strategies = strategies;
2696
+ }
2697
+ classify(err) {
2698
+ if (err instanceof DOMException && err.name === "AbortError") {
2699
+ return { kind: "abort", retryable: false };
2700
+ }
2701
+ if (err instanceof Error && err.name === "AbortError") {
2702
+ return { kind: "abort", retryable: false };
2703
+ }
2704
+ if (err instanceof ProviderError) {
2705
+ if (err.status === 429) return { kind: "rate_limit", retryable: true };
2706
+ if (err.status === 529) return { kind: "overloaded", retryable: true };
2707
+ if (err.status >= 500) return { kind: "server", retryable: true };
2708
+ if (err.status === 413 || /context|too long|tokens/i.test(err.message)) {
2709
+ return { kind: "context_overflow", retryable: false };
2710
+ }
2711
+ if (err.status >= 400) return { kind: "client", retryable: false };
2712
+ }
2713
+ if (err instanceof Error && /ECONN|ETIMEDOUT|ETIME|ENOTFOUND|EAI_AGAIN|fetch failed/i.test(err.message)) {
2714
+ return { kind: "network", retryable: true };
2715
+ }
2716
+ return { kind: "unknown", retryable: false };
2717
+ }
2718
+ async recover(err, ctx) {
2719
+ for (const strategy of this.strategies) {
2720
+ const result = await strategy.attempt(err, ctx);
2721
+ if (result !== null) return result;
2722
+ }
2723
+ return null;
2724
+ }
2725
+ };
2533
2726
  var DefaultSkillLoader = class {
2534
2727
  dirs;
2535
2728
  cache;
@@ -2747,8 +2940,22 @@ var DefaultConfigLoader = class {
2747
2940
  if (this.vault) {
2748
2941
  cfg = decryptConfigSecrets(cfg, this.vault);
2749
2942
  }
2750
- this.validateBehavior(cfg);
2751
- if (this.strict) this.validateIdentity(cfg);
2943
+ if (cfg.providers) {
2944
+ for (const pcfg of Object.values(cfg.providers)) {
2945
+ if (!pcfg || typeof pcfg !== "object") continue;
2946
+ const keys = pcfg.apiKeys;
2947
+ if (!Array.isArray(keys) || keys.length === 0) continue;
2948
+ const existing = pcfg.apiKey;
2949
+ if (existing && existing.length > 0) continue;
2950
+ const activeLabel = pcfg.activeKey;
2951
+ const chosen = activeLabel ? keys.find((k) => k.label === activeLabel) ?? keys[0] : keys[0];
2952
+ if (chosen?.apiKey) {
2953
+ pcfg.apiKey = chosen.apiKey;
2954
+ }
2955
+ }
2956
+ }
2957
+ this.validateBehavior(cfg);
2958
+ if (this.strict) this.validateIdentity(cfg);
2752
2959
  return Object.freeze(cfg);
2753
2960
  }
2754
2961
  async readJson(file) {
@@ -2783,6 +2990,110 @@ var DefaultConfigLoader = class {
2783
2990
  }
2784
2991
  };
2785
2992
 
2993
+ // src/defaults/config-store.ts
2994
+ var DefaultConfigStore = class {
2995
+ current;
2996
+ watchers = /* @__PURE__ */ new Set();
2997
+ constructor(initial) {
2998
+ this.current = deepFreeze(structuredClone(initial));
2999
+ }
3000
+ get() {
3001
+ return this.current;
3002
+ }
3003
+ getSection(key) {
3004
+ return this.current[key];
3005
+ }
3006
+ getExtension(pluginName) {
3007
+ const ext = this.current.extensions?.[pluginName];
3008
+ return ext ? ext : FROZEN_EMPTY;
3009
+ }
3010
+ update(partial) {
3011
+ const next = deepFreeze(
3012
+ structuredClone({ ...this.current, ...partial })
3013
+ );
3014
+ if (next.version !== 1) {
3015
+ throw new Error(`ConfigStore.update: version must remain 1, got ${String(next.version)}`);
3016
+ }
3017
+ const prev = this.current;
3018
+ this.current = next;
3019
+ for (const w of this.watchers) {
3020
+ try {
3021
+ w(next, prev);
3022
+ } catch {
3023
+ }
3024
+ }
3025
+ return next;
3026
+ }
3027
+ watch(cb) {
3028
+ this.watchers.add(cb);
3029
+ return () => this.watchers.delete(cb);
3030
+ }
3031
+ };
3032
+ var FROZEN_EMPTY = Object.freeze({});
3033
+ function deepFreeze(obj) {
3034
+ if (obj === null || typeof obj !== "object") return obj;
3035
+ if (Object.isFrozen(obj)) return obj;
3036
+ for (const key of Object.keys(obj)) {
3037
+ const v = obj[key];
3038
+ if (v !== null && typeof v === "object" && !Object.isFrozen(v)) {
3039
+ deepFreeze(v);
3040
+ }
3041
+ }
3042
+ return Object.freeze(obj);
3043
+ }
3044
+
3045
+ // src/defaults/config-migration.ts
3046
+ var ConfigMigrationError = class extends Error {
3047
+ fromVersion;
3048
+ targetVersion;
3049
+ missingStep;
3050
+ constructor(opts) {
3051
+ super(opts.message);
3052
+ this.name = "ConfigMigrationError";
3053
+ this.fromVersion = opts.fromVersion;
3054
+ this.targetVersion = opts.targetVersion;
3055
+ this.missingStep = opts.missingStep;
3056
+ }
3057
+ };
3058
+ function runConfigMigrations(input, targetVersion, migrations) {
3059
+ const initial = typeof input["version"] === "number" ? input["version"] : 1;
3060
+ let current = { ...input };
3061
+ let currentVersion = initial;
3062
+ const applied = [];
3063
+ let shouldPersist = false;
3064
+ let guard = 0;
3065
+ while (currentVersion !== targetVersion) {
3066
+ if (++guard > 100) {
3067
+ throw new ConfigMigrationError({
3068
+ message: `Config migration looped past 100 steps (from v${initial} toward v${targetVersion})`,
3069
+ fromVersion: initial,
3070
+ targetVersion,
3071
+ missingStep: currentVersion
3072
+ });
3073
+ }
3074
+ const step = migrations.find((m) => m.from === currentVersion);
3075
+ if (!step) {
3076
+ throw new ConfigMigrationError({
3077
+ message: `No migration registered from config v${currentVersion} (target v${targetVersion}). Update the framework or revert the config file.`,
3078
+ fromVersion: initial,
3079
+ targetVersion,
3080
+ missingStep: currentVersion
3081
+ });
3082
+ }
3083
+ const ctx = { fromVersion: currentVersion, shouldPersist: false };
3084
+ const next = step.migrate(current, ctx);
3085
+ if (typeof next["version"] !== "number" || next["version"] !== step.to) {
3086
+ next["version"] = step.to;
3087
+ }
3088
+ current = next;
3089
+ currentVersion = step.to;
3090
+ applied.push(`v${step.from}\u2192v${step.to}`);
3091
+ shouldPersist = shouldPersist || ctx.shouldPersist || step.from < step.to;
3092
+ }
3093
+ return { config: current, applied, shouldPersist };
3094
+ }
3095
+ var DEFAULT_CONFIG_MIGRATIONS = [];
3096
+
2786
3097
  // src/defaults/compactor.ts
2787
3098
  var HybridCompactor = class {
2788
3099
  preserveK;
@@ -2791,7 +3102,7 @@ var HybridCompactor = class {
2791
3102
  constructor(opts = {}) {
2792
3103
  this.preserveK = opts.preserveK ?? 10;
2793
3104
  this.eliseThreshold = opts.eliseThreshold ?? 2e3;
2794
- this.estimator = opts.estimator ?? roughTokenEstimate;
3105
+ this.estimator = opts.estimator ?? estimateTextTokens;
2795
3106
  }
2796
3107
  async compact(ctx, opts = {}) {
2797
3108
  const beforeTokens = this.estimateMessages(ctx.messages);
@@ -2823,8 +3134,7 @@ var HybridCompactor = class {
2823
3134
  if (!msg || !Array.isArray(msg.content)) continue;
2824
3135
  const newContent = msg.content.map((b) => {
2825
3136
  if (b.type !== "tool_result") return b;
2826
- const text = typeof b.content === "string" ? b.content : JSON.stringify(b.content);
2827
- const tokens = this.estimator(text);
3137
+ const tokens = estimateToolResultTokens(b.content);
2828
3138
  if (tokens < this.eliseThreshold) return b;
2829
3139
  saved += tokens;
2830
3140
  const elided = {
@@ -2862,23 +3172,20 @@ var HybridCompactor = class {
2862
3172
  },
2863
3173
  { role: "assistant", content: "Continuing from compacted context." }
2864
3174
  ];
2865
- ctx.messages.splice(0, boundary, ...summary);
3175
+ const tail = ctx.messages.slice(boundary);
3176
+ ctx.state.replaceMessages([...summary, ...tail]);
2866
3177
  return Math.max(0, removedTokens - this.estimateMessages(summary));
2867
3178
  }
2868
3179
  estimateMessages(messages) {
2869
3180
  let total = 0;
2870
3181
  for (const m of messages) {
2871
3182
  if (typeof m.content === "string") {
2872
- total += this.estimator(m.content);
3183
+ total += estimateTextTokens(m.content);
2873
3184
  } else {
2874
3185
  for (const b of m.content) {
2875
- if (b.type === "text") total += this.estimator(b.text);
2876
- else if (b.type === "tool_use") total += this.estimator(JSON.stringify(b.input));
2877
- else if (b.type === "tool_result") {
2878
- total += this.estimator(
2879
- typeof b.content === "string" ? b.content : JSON.stringify(b.content)
2880
- );
2881
- }
3186
+ if (b.type === "text") total += estimateTextTokens(b.text);
3187
+ else if (b.type === "tool_use") total += estimateToolInputTokens(b.input);
3188
+ else if (b.type === "tool_result") total += estimateToolResultTokens(b.content);
2882
3189
  }
2883
3190
  }
2884
3191
  }
@@ -2889,9 +3196,6 @@ function hasTextContent(m) {
2889
3196
  if (typeof m.content === "string") return m.content.trim().length > 0;
2890
3197
  return m.content.some((b) => b.type === "text" && b.text.trim().length > 0);
2891
3198
  }
2892
- function roughTokenEstimate(text) {
2893
- return Math.max(1, Math.ceil(text.length / 4));
2894
- }
2895
3199
 
2896
3200
  // src/defaults/intelligent-compactor.ts
2897
3201
  var IntelligentCompactor = class {
@@ -2951,7 +3255,8 @@ var IntelligentCompactor = class {
2951
3255
  content: `[prior_turns_summary: ${summaryText}]`
2952
3256
  };
2953
3257
  const summaryTokens = this.estimateTokens([summaryMsg]);
2954
- ctx.messages.splice(0, boundary, summaryMsg);
3258
+ const tail = ctx.messages.slice(boundary);
3259
+ ctx.state.replaceMessages([summaryMsg, ...tail]);
2955
3260
  return Math.max(0, removedTokens - summaryTokens);
2956
3261
  }
2957
3262
  findSafeBoundary(messages, from, to) {
@@ -3032,8 +3337,7 @@ var IntelligentCompactor = class {
3032
3337
  if (!msg || !Array.isArray(msg.content)) continue;
3033
3338
  const newContent = msg.content.map((b) => {
3034
3339
  if (b.type !== "tool_result") return b;
3035
- const text = typeof b.content === "string" ? b.content : JSON.stringify(b.content);
3036
- const tokens = this.roughTokenEstimate(text);
3340
+ const tokens = estimateToolResultTokens(b.content);
3037
3341
  if (tokens < this.eliseThreshold) return b;
3038
3342
  saved += tokens;
3039
3343
  return {
@@ -3055,24 +3359,17 @@ var IntelligentCompactor = class {
3055
3359
  let total = 0;
3056
3360
  for (const m of messages) {
3057
3361
  if (typeof m.content === "string") {
3058
- total += this.roughTokenEstimate(m.content);
3362
+ total += estimateTextTokens(m.content);
3059
3363
  } else {
3060
3364
  for (const b of m.content) {
3061
- if (b.type === "text") total += this.roughTokenEstimate(b.text);
3062
- else if (b.type === "tool_use") total += this.roughTokenEstimate(JSON.stringify(b.input));
3063
- else if (b.type === "tool_result") {
3064
- total += this.roughTokenEstimate(
3065
- typeof b.content === "string" ? b.content : JSON.stringify(b.content)
3066
- );
3067
- }
3365
+ if (b.type === "text") total += estimateTextTokens(b.text);
3366
+ else if (b.type === "tool_use") total += estimateToolInputTokens(b.input);
3367
+ else if (b.type === "tool_result") total += estimateToolResultTokens(b.content);
3068
3368
  }
3069
3369
  }
3070
3370
  }
3071
3371
  return total;
3072
3372
  }
3073
- roughTokenEstimate(text) {
3074
- return Math.max(1, Math.ceil(text.length / 4));
3075
- }
3076
3373
  };
3077
3374
 
3078
3375
  // src/defaults/llm-selector.ts
@@ -3295,8 +3592,8 @@ var SelectiveCompactor = class {
3295
3592
  * insert summaries where the selector provided them.
3296
3593
  */
3297
3594
  async executePlan(ctx, plan) {
3298
- const messages = ctx.messages;
3299
- if (messages.length === 0) return;
3595
+ if (ctx.messages.length === 0) return;
3596
+ const messages = [...ctx.messages];
3300
3597
  const sortedCollapsed = [...plan.collapsed].sort((a, b) => b.from - a.from);
3301
3598
  for (const range of sortedCollapsed) {
3302
3599
  if (range.from < 0 || range.to >= messages.length || range.from > range.to) continue;
@@ -3311,6 +3608,7 @@ var SelectiveCompactor = class {
3311
3608
  };
3312
3609
  messages.splice(range.from, range.to - range.from + 1, summaryMsg);
3313
3610
  }
3611
+ ctx.state.replaceMessages(messages);
3314
3612
  }
3315
3613
  async summarizeRange(messages, ctx) {
3316
3614
  const systemText = `${this.summarizerPrompt}
@@ -3357,7 +3655,8 @@ Summarize the following message range:`;
3357
3655
  role: "system",
3358
3656
  content: `[${removed.length} earlier turns trimmed \u2014 see session log for details]`
3359
3657
  };
3360
- messages.splice(0, boundary, summaryMsg);
3658
+ const tail = messages.slice(boundary);
3659
+ ctx.state.replaceMessages([summaryMsg, ...tail]);
3361
3660
  return Math.max(0, removedTokens - this.estimateTokens([summaryMsg]));
3362
3661
  }
3363
3662
  computeTargetBudget(load, aggressive) {
@@ -3747,44 +4046,128 @@ async function loadUserModes(modesDir) {
3747
4046
  }
3748
4047
  return modes;
3749
4048
  }
4049
+
4050
+ // src/defaults/subagent-budget.ts
4051
+ var BudgetExceededError = class extends Error {
4052
+ kind;
4053
+ limit;
4054
+ observed;
4055
+ constructor(kind, limit, observed) {
4056
+ super(`Budget exceeded: ${kind} (limit=${limit}, observed=${observed})`);
4057
+ this.name = "BudgetExceededError";
4058
+ this.kind = kind;
4059
+ this.limit = limit;
4060
+ this.observed = observed;
4061
+ }
4062
+ };
4063
+ var SubagentBudget = class {
4064
+ limits;
4065
+ iterations = 0;
4066
+ toolCalls = 0;
4067
+ tokenInput = 0;
4068
+ tokenOutput = 0;
4069
+ costUsd = 0;
4070
+ startTime = null;
4071
+ constructor(limits = {}) {
4072
+ this.limits = Object.freeze({ ...limits });
4073
+ }
4074
+ start() {
4075
+ this.startTime = Date.now();
4076
+ }
4077
+ recordIteration() {
4078
+ this.iterations++;
4079
+ if (this.limits.maxIterations !== void 0 && this.iterations > this.limits.maxIterations) {
4080
+ throw new BudgetExceededError("iterations", this.limits.maxIterations, this.iterations);
4081
+ }
4082
+ }
4083
+ recordToolCall() {
4084
+ this.toolCalls++;
4085
+ if (this.limits.maxToolCalls !== void 0 && this.toolCalls > this.limits.maxToolCalls) {
4086
+ throw new BudgetExceededError("tool_calls", this.limits.maxToolCalls, this.toolCalls);
4087
+ }
4088
+ }
4089
+ recordUsage(usage, costUsd = 0) {
4090
+ this.tokenInput += usage.input;
4091
+ this.tokenOutput += usage.output;
4092
+ this.costUsd += costUsd;
4093
+ const totalTokens = this.tokenInput + this.tokenOutput;
4094
+ if (this.limits.maxTokens !== void 0 && totalTokens > this.limits.maxTokens) {
4095
+ throw new BudgetExceededError("tokens", this.limits.maxTokens, totalTokens);
4096
+ }
4097
+ if (this.limits.maxCostUsd !== void 0 && this.costUsd > this.limits.maxCostUsd) {
4098
+ throw new BudgetExceededError("cost", this.limits.maxCostUsd, this.costUsd);
4099
+ }
4100
+ }
4101
+ /**
4102
+ * Throws if the wall-clock budget is exhausted. Call this from the iteration
4103
+ * loop so a hung tool can't keep a subagent running past its deadline.
4104
+ */
4105
+ checkTimeout() {
4106
+ if (this.startTime === null || this.limits.timeoutMs === void 0) return;
4107
+ const elapsed = Date.now() - this.startTime;
4108
+ if (elapsed > this.limits.timeoutMs) {
4109
+ throw new BudgetExceededError("timeout", this.limits.timeoutMs, elapsed);
4110
+ }
4111
+ }
4112
+ /** Returns true if a timeout has occurred without throwing. Useful for races. */
4113
+ isTimedOut() {
4114
+ if (this.startTime === null || this.limits.timeoutMs === void 0) return false;
4115
+ return Date.now() - this.startTime > this.limits.timeoutMs;
4116
+ }
4117
+ usage() {
4118
+ return {
4119
+ iterations: this.iterations,
4120
+ toolCalls: this.toolCalls,
4121
+ tokens: {
4122
+ input: this.tokenInput,
4123
+ output: this.tokenOutput,
4124
+ total: this.tokenInput + this.tokenOutput
4125
+ },
4126
+ costUsd: this.costUsd,
4127
+ elapsedMs: this.startTime === null ? 0 : Date.now() - this.startTime
4128
+ };
4129
+ }
4130
+ };
4131
+
4132
+ // src/defaults/multi-agent-coordinator.ts
3750
4133
  var DefaultMultiAgentCoordinator = class extends EventEmitter {
3751
4134
  coordinatorId;
3752
4135
  config;
4136
+ runner;
3753
4137
  subagents = /* @__PURE__ */ new Map();
3754
4138
  pendingTasks = [];
3755
4139
  completedResults = [];
3756
4140
  totalIterations = 0;
3757
- constructor(config) {
4141
+ inFlight = 0;
4142
+ constructor(config, options = {}) {
3758
4143
  super();
3759
4144
  this.coordinatorId = config.coordinatorId;
3760
4145
  this.config = config;
4146
+ this.runner = options.runner;
3761
4147
  }
3762
4148
  async spawn(subagent) {
3763
4149
  const id = subagent.id || randomUUID();
3764
4150
  const context = {
3765
4151
  subagentId: id,
3766
4152
  tasks: [],
4153
+ // parentBridge: wired by the caller via setSubagentBridge() once the
4154
+ // bidirectional bridge is created. Reads gated by hasParentBridge().
3767
4155
  parentBridge: null,
3768
4156
  doneCondition: this.config.doneCondition,
3769
4157
  maxConcurrent: this.config.maxConcurrent ?? 4
3770
4158
  };
3771
4159
  this.subagents.set(id, {
3772
- config: subagent,
4160
+ config: { ...subagent, id },
3773
4161
  context,
3774
- status: "idle"
4162
+ status: "idle",
4163
+ abortController: new AbortController()
3775
4164
  });
3776
4165
  this.emit("subagent.started", { subagent: { ...subagent, id } });
3777
- return {
3778
- subagentId: id,
3779
- agentId: id
3780
- };
4166
+ return { subagentId: id, agentId: id };
3781
4167
  }
3782
4168
  async assign(task) {
3783
4169
  this.pendingTasks.push(task);
3784
- const available = this.getAvailableSubagent();
3785
- if (available) {
3786
- await this.dispatch(available, task);
3787
- }
4170
+ this.tryDispatchNext();
3788
4171
  }
3789
4172
  async delegate(to, msg) {
3790
4173
  const subagent = this.subagents.get(to);
@@ -3795,8 +4178,8 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
3795
4178
  await subagent.context.parentBridge.send(msg);
3796
4179
  }
3797
4180
  /**
3798
- * Wire up the communication bridge for a subagent. Call this after `spawn()`
3799
- * once the caller has created the bidirectional bridge connection.
4181
+ * Wire up the communication bridge for a subagent. Call after spawn() once
4182
+ * the caller has created the bidirectional connection.
3800
4183
  */
3801
4184
  setSubagentBridge(subagentId, bridge) {
3802
4185
  const subagent = this.subagents.get(subagentId);
@@ -3806,6 +4189,7 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
3806
4189
  async stop(subagentId) {
3807
4190
  const subagent = this.subagents.get(subagentId);
3808
4191
  if (!subagent) return;
4192
+ subagent.abortController.abort();
3809
4193
  subagent.status = "stopped";
3810
4194
  subagent.currentTask = void 0;
3811
4195
  subagent.context.parentBridge = null;
@@ -3831,71 +4215,234 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
3831
4215
  done: this.isDone()
3832
4216
  };
3833
4217
  }
3834
- getAvailableSubagent() {
4218
+ /** Expose snapshot of completed results — useful for callers awaiting all done. */
4219
+ results() {
4220
+ return this.completedResults;
4221
+ }
4222
+ /**
4223
+ * Manual completion — for callers that drive subagents without a runner
4224
+ * (e.g. external orchestrators). When a runner is configured the coordinator
4225
+ * calls this itself.
4226
+ */
4227
+ completeTask(result) {
4228
+ this.recordCompletion(result);
4229
+ }
4230
+ // --- internal dispatching ---------------------------------------------
4231
+ tryDispatchNext() {
4232
+ while (this.canDispatch()) {
4233
+ const subagentId = this.findIdleSubagent();
4234
+ if (!subagentId) return;
4235
+ const task = this.pendingTasks.shift();
4236
+ if (!task) return;
4237
+ void this.runDispatched(subagentId, task);
4238
+ }
4239
+ }
4240
+ canDispatch() {
4241
+ const max = this.config.maxConcurrent ?? 4;
4242
+ return this.inFlight < max && this.pendingTasks.length > 0;
4243
+ }
4244
+ findIdleSubagent() {
3835
4245
  for (const [id, s] of this.subagents) {
3836
4246
  if (s.status === "idle") return id;
3837
4247
  }
3838
4248
  return null;
3839
4249
  }
3840
- async dispatch(subagentId, task) {
4250
+ async runDispatched(subagentId, task) {
3841
4251
  const subagent = this.subagents.get(subagentId);
3842
4252
  if (!subagent) return;
3843
4253
  subagent.status = "running";
3844
4254
  subagent.currentTask = task.id;
3845
4255
  task.subagentId = subagentId;
3846
4256
  subagent.context.tasks.push(task);
3847
- if (!subagent.context.parentBridge) {
3848
- this.emit("task.assigned", { task, subagentId });
4257
+ this.inFlight++;
4258
+ this.emit("task.assigned", { task, subagentId });
4259
+ const budget = new SubagentBudget({
4260
+ maxIterations: subagent.config.maxIterations ?? this.config.defaultBudget?.maxIterations,
4261
+ maxToolCalls: task.maxToolCalls ?? subagent.config.maxToolCalls ?? this.config.defaultBudget?.maxToolCalls,
4262
+ maxTokens: subagent.config.maxTokens ?? this.config.defaultBudget?.maxTokens,
4263
+ maxCostUsd: subagent.config.maxCostUsd ?? this.config.defaultBudget?.maxCostUsd,
4264
+ timeoutMs: task.timeoutMs ?? subagent.config.timeoutMs ?? this.config.defaultBudget?.timeoutMs
4265
+ });
4266
+ subagent.activeBudget = budget;
4267
+ const startTime = Date.now();
4268
+ const runCtx = {
4269
+ subagentId,
4270
+ config: subagent.config,
4271
+ budget,
4272
+ signal: subagent.abortController.signal,
4273
+ bridge: subagent.context.parentBridge || null
4274
+ };
4275
+ let result;
4276
+ if (!this.runner) {
3849
4277
  return;
3850
4278
  }
3851
- await subagent.context.parentBridge.send({
3852
- id: randomUUID(),
3853
- type: "task",
3854
- from: this.coordinatorId,
3855
- to: subagentId,
3856
- payload: task,
3857
- timestamp: Date.now()
3858
- });
3859
- this.emit("task.assigned", { task, subagentId });
3860
- }
3861
- isDone() {
3862
- if (this.config.doneCondition.type === "all_tasks_done") {
3863
- return this.pendingTasks.length === 0 && this.completedResults.every((r) => r.status === "success");
4279
+ budget.start();
4280
+ try {
4281
+ const outcome = await this.executeWithTimeout(this.runner, task, runCtx, budget);
4282
+ result = {
4283
+ subagentId,
4284
+ taskId: task.id,
4285
+ status: "success",
4286
+ result: outcome.result,
4287
+ iterations: outcome.iterations,
4288
+ toolCalls: outcome.toolCalls,
4289
+ durationMs: Date.now() - startTime
4290
+ };
4291
+ } catch (err) {
4292
+ const status = err instanceof BudgetExceededError && err.kind === "timeout" ? "timeout" : subagent.abortController.signal.aborted ? "stopped" : "failed";
4293
+ result = {
4294
+ subagentId,
4295
+ taskId: task.id,
4296
+ status,
4297
+ error: err instanceof Error ? err.message : String(err),
4298
+ iterations: budget.usage().iterations,
4299
+ toolCalls: budget.usage().toolCalls,
4300
+ durationMs: Date.now() - startTime
4301
+ };
3864
4302
  }
3865
- if (this.config.doneCondition.maxIterations && this.totalIterations >= this.config.doneCondition.maxIterations) {
3866
- return true;
4303
+ this.recordCompletion(result);
4304
+ }
4305
+ async executeWithTimeout(runner, task, ctx, budget) {
4306
+ const timeoutMs = budget.limits.timeoutMs;
4307
+ if (timeoutMs === void 0) return runner(task, ctx);
4308
+ let timer = null;
4309
+ const timeoutPromise = new Promise((_, reject) => {
4310
+ timer = setTimeout(() => {
4311
+ this.subagents.get(ctx.subagentId)?.abortController.abort();
4312
+ reject(new BudgetExceededError("timeout", timeoutMs, Date.now()));
4313
+ }, timeoutMs);
4314
+ });
4315
+ try {
4316
+ return await Promise.race([runner(task, ctx), timeoutPromise]);
4317
+ } finally {
4318
+ if (timer) clearTimeout(timer);
3867
4319
  }
3868
- return false;
3869
4320
  }
3870
- completeTask(result) {
4321
+ recordCompletion(result) {
3871
4322
  this.completedResults.push(result);
3872
4323
  this.totalIterations += result.iterations;
4324
+ this.inFlight = Math.max(0, this.inFlight - 1);
3873
4325
  const subagent = this.subagents.get(result.subagentId);
3874
- if (subagent) {
3875
- subagent.status = "idle";
4326
+ if (subagent && subagent.status !== "stopped") {
4327
+ subagent.status = result.status === "failed" || result.status === "timeout" ? "error" : "idle";
3876
4328
  subagent.currentTask = void 0;
4329
+ if (subagent.status === "error") {
4330
+ queueMicrotask(() => {
4331
+ if (subagent.status === "error") subagent.status = "idle";
4332
+ this.tryDispatchNext();
4333
+ });
4334
+ }
3877
4335
  }
3878
4336
  this.emit("task.completed", {
3879
- task: this.pendingTasks.shift(),
4337
+ task: subagent?.context.tasks.find((t2) => t2.id === result.taskId) ?? { id: result.taskId },
3880
4338
  result
3881
4339
  });
3882
- if (this.pendingTasks.length > 0) {
3883
- const available = this.getAvailableSubagent();
3884
- if (available) {
3885
- const nextTask = this.pendingTasks.shift();
3886
- this.dispatch(available, nextTask);
3887
- }
3888
- } else if (this.isDone()) {
4340
+ this.tryDispatchNext();
4341
+ if (this.isDone()) {
3889
4342
  this.emit("done", {
3890
4343
  results: this.completedResults,
3891
4344
  totalIterations: this.totalIterations
3892
4345
  });
3893
4346
  }
3894
4347
  }
4348
+ isDone() {
4349
+ if (this.config.doneCondition.type === "all_tasks_done") {
4350
+ return this.pendingTasks.length === 0 && this.inFlight === 0;
4351
+ }
4352
+ if (this.config.doneCondition.maxIterations !== void 0 && this.totalIterations >= this.config.doneCondition.maxIterations) {
4353
+ return true;
4354
+ }
4355
+ return false;
4356
+ }
3895
4357
  };
4358
+
4359
+ // src/defaults/agent-subagent-runner.ts
4360
+ function makeAgentSubagentRunner(opts) {
4361
+ const format = opts.formatTaskInput ?? defaultFormatTaskInput;
4362
+ return async (task, ctx) => {
4363
+ const { agent, events } = await opts.factory(ctx.config);
4364
+ const aborter = new AbortController();
4365
+ const onBudgetError = (err) => {
4366
+ if (err instanceof BudgetExceededError) {
4367
+ aborter.abort();
4368
+ budgetError = err;
4369
+ } else {
4370
+ throw err;
4371
+ }
4372
+ };
4373
+ let budgetError = null;
4374
+ const unsub = [];
4375
+ unsub.push(
4376
+ events.on("tool.started", () => {
4377
+ try {
4378
+ ctx.budget.recordToolCall();
4379
+ } catch (e) {
4380
+ onBudgetError(e);
4381
+ }
4382
+ }),
4383
+ events.on("provider.response", (e) => {
4384
+ try {
4385
+ ctx.budget.recordUsage(e.usage);
4386
+ } catch (e2) {
4387
+ onBudgetError(e2);
4388
+ }
4389
+ }),
4390
+ events.on("iteration.started", () => {
4391
+ try {
4392
+ ctx.budget.recordIteration();
4393
+ ctx.budget.checkTimeout();
4394
+ } catch (e) {
4395
+ onBudgetError(e);
4396
+ }
4397
+ })
4398
+ );
4399
+ const onParentAbort = () => aborter.abort();
4400
+ ctx.signal.addEventListener("abort", onParentAbort);
4401
+ let result;
4402
+ try {
4403
+ result = await agent.run(format(task, ctx.config), { signal: aborter.signal });
4404
+ } finally {
4405
+ ctx.signal.removeEventListener("abort", onParentAbort);
4406
+ for (const u of unsub) u();
4407
+ }
4408
+ if (budgetError) throw budgetError;
4409
+ if (result.status === "failed") {
4410
+ throw result.error instanceof Error ? result.error : new Error(String(result.error ?? "agent failed"));
4411
+ }
4412
+ if (result.status === "aborted") {
4413
+ throw new Error("agent aborted");
4414
+ }
4415
+ if (result.status === "max_iterations") {
4416
+ throw new Error("agent exhausted iteration limit");
4417
+ }
4418
+ const usage = ctx.budget.usage();
4419
+ return {
4420
+ result: result.finalText,
4421
+ iterations: result.iterations,
4422
+ toolCalls: usage.toolCalls
4423
+ };
4424
+ };
4425
+ }
4426
+ function defaultFormatTaskInput(task) {
4427
+ return task.description ?? "";
4428
+ }
4429
+
4430
+ // src/defaults/transport/in-memory-transport.ts
3896
4431
  var InMemoryBridgeTransport = class {
3897
4432
  subs = /* @__PURE__ */ new Map();
3898
4433
  send(msg, to) {
4434
+ if (to === "*") {
4435
+ for (const [id, handlers2] of this.subs) {
4436
+ if (id === msg.from) continue;
4437
+ for (const h of handlers2) {
4438
+ try {
4439
+ h(msg);
4440
+ } catch {
4441
+ }
4442
+ }
4443
+ }
4444
+ return Promise.resolve();
4445
+ }
3899
4446
  const handlers = this.subs.get(to);
3900
4447
  if (handlers) {
3901
4448
  for (const h of handlers) {
@@ -3917,6 +4464,8 @@ var InMemoryBridgeTransport = class {
3917
4464
  return Promise.resolve();
3918
4465
  }
3919
4466
  };
4467
+
4468
+ // src/defaults/agent-bridge.ts
3920
4469
  var InMemoryAgentBridge = class {
3921
4470
  agentId;
3922
4471
  coordinatorId;
@@ -4083,7 +4632,7 @@ var AutonomousRunner = class {
4083
4632
  if (e.message.includes("timeout")) {
4084
4633
  const timeoutResult = {
4085
4634
  status: "failed",
4086
- error: e,
4635
+ error: toWrongStackError(e),
4087
4636
  iterations: this.iterations,
4088
4637
  toolCalls: this.toolCalls,
4089
4638
  reason: "iteration timeout"
@@ -4897,95 +5446,239 @@ var SpecDrivenDev = class {
4897
5446
  }));
4898
5447
  }
4899
5448
  };
4900
-
4901
- // src/defaults/tool-executor.ts
4902
- var ToolExecutor = class {
4903
- constructor(registry, opts) {
4904
- this.registry = registry;
4905
- this.opts = opts;
4906
- this.iterationTimeoutMs = opts.iterationTimeoutMs ?? 3e5;
4907
- this.serializer = createToolOutputSerializer({
4908
- perIterationOutputCapBytes: opts.perIterationOutputCapBytes ?? 1e5
4909
- });
5449
+ var LOCK_FILE = "active.json";
5450
+ var DEFAULT_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
5451
+ var RecoveryLock = class {
5452
+ file;
5453
+ pid;
5454
+ hostname;
5455
+ maxAgeMs;
5456
+ sessionStore;
5457
+ probe;
5458
+ constructor(opts) {
5459
+ this.file = path2.join(opts.dir, LOCK_FILE);
5460
+ this.pid = opts.pid ?? process.pid;
5461
+ this.hostname = opts.hostname ?? os3.hostname();
5462
+ this.maxAgeMs = opts.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
5463
+ this.sessionStore = opts.sessionStore;
5464
+ this.probe = opts.isPidAlive ?? defaultIsPidAlive;
4910
5465
  }
4911
- registry;
4912
- opts;
4913
- serializer;
4914
- iterationTimeoutMs;
4915
5466
  /**
4916
- * Execute a batch of tool uses using the configured strategy.
4917
- * Returns the execution results and the remaining output budget.
5467
+ * Examine the lockfile and decide whether it represents an abandoned
5468
+ * session. Returns `null` if the file is missing, points to a live
5469
+ * instance, references a clean-closed session, is too old, or is
5470
+ * malformed. Otherwise returns enough detail to prompt the user.
5471
+ *
5472
+ * Important: this is a read-only check. We never delete an active
5473
+ * lock from here — if another wstack instance is alive, the caller
5474
+ * should bail or run with a fresh session instead.
4918
5475
  */
4919
- async executeBatch(toolUses, ctx, strategy) {
4920
- let budget = this.opts.perIterationOutputCapBytes ?? 1e5;
4921
- const runOne = async (use, index) => {
4922
- const start = Date.now();
4923
- const tool = this.registry.get(use.name);
4924
- let result;
4925
- if (!tool) {
4926
- result = this.unknownToolResult(use, () => this.registry.list().map((t2) => t2.name));
4927
- } else {
4928
- const decision = await this.opts.permissionPolicy.evaluate(tool, use.input, ctx);
4929
- if (decision.permission === "deny") {
4930
- result = this.deniedResult(use, decision.reason);
4931
- } else if (decision.permission === "confirm") {
4932
- result = this.confirmResult(use);
4933
- } else {
4934
- try {
4935
- result = await this.executeTool(tool, use, ctx, budget);
4936
- } catch (err) {
4937
- const msg = err instanceof Error ? err.message : String(err);
4938
- const scrubbed = this.opts.secretScrubber.scrub(msg);
4939
- this.opts.renderer?.writeToolResult(tool.name, scrubbed, true);
4940
- result = {
4941
- type: "tool_result",
4942
- tool_use_id: use.id,
4943
- content: `Tool "${use.name}" threw: ${scrubbed}`,
4944
- is_error: true
4945
- };
4946
- }
4947
- }
4948
- }
4949
- const contentBytes = typeof result.content === "string" ? Buffer.byteLength(result.content, "utf8") : Buffer.byteLength(JSON.stringify(result.content), "utf8");
4950
- budget = Math.max(0, budget - contentBytes);
4951
- return { result, tool, durationMs: Date.now() - start };
4952
- };
4953
- if (strategy === "sequential") {
4954
- const outputs = [];
4955
- for (let i = 0; i < toolUses.length; i++) {
4956
- const use = toolUses[i];
4957
- if (use) outputs.push(await runOne(use));
4958
- }
4959
- return { outputs, remainingBudget: budget };
4960
- }
4961
- if (strategy === "parallel") {
4962
- const outputs = await Promise.all(toolUses.map((use, i) => runOne(use)));
4963
- return { outputs, remainingBudget: budget };
5476
+ async checkAbandoned() {
5477
+ const lock = await this.readLock();
5478
+ if (!lock) return null;
5479
+ const ageMs = Date.now() - new Date(lock.startedAt).getTime();
5480
+ if (Number.isNaN(ageMs) || ageMs < 0) {
5481
+ return null;
4964
5482
  }
4965
- const nonMutating = [];
4966
- const mutating = [];
4967
- for (let i = 0; i < toolUses.length; i++) {
4968
- const use = toolUses[i];
4969
- if (!use) continue;
4970
- const tool = this.registry.get(use.name);
4971
- if (tool?.mutating) mutating.push({ use, index: i });
4972
- else nonMutating.push({ use, index: i });
5483
+ if (ageMs > this.maxAgeMs) return null;
5484
+ if (lock.hostname === this.hostname && this.probe(lock.pid)) {
5485
+ return null;
4973
5486
  }
4974
- const firstPass = await Promise.all(nonMutating.map(({ use, index }) => runOne(use)));
4975
- const secondPass = [];
4976
- for (const { use, index } of mutating) {
4977
- secondPass.push(await runOne(use));
5487
+ let messageCount = 0;
5488
+ if (this.sessionStore) {
5489
+ try {
5490
+ const data = await this.sessionStore.load(lock.sessionId);
5491
+ const closed = data.events.some((e) => e.type === "session_end");
5492
+ if (closed) return null;
5493
+ messageCount = data.messages.length;
5494
+ } catch {
5495
+ return null;
5496
+ }
4978
5497
  }
4979
5498
  return {
4980
- outputs: [...firstPass, ...secondPass],
4981
- remainingBudget: budget
5499
+ sessionId: lock.sessionId,
5500
+ pid: lock.pid,
5501
+ startedAt: lock.startedAt,
5502
+ ageMs,
5503
+ messageCount
4982
5504
  };
4983
5505
  }
4984
5506
  /**
4985
- * Execute a single tool with timeout, permission check, and output capping.
4986
- * Emits `tool.started` via the injected EventBus (if any) right before
4987
- * invoking the tool — closes the observability gap between "model decided
4988
- * to call a tool" and "tool.executed".
5507
+ * Claim the lock for the given session. Overwrites any existing lock
5508
+ * the caller should have already handled abandonment (via
5509
+ * `checkAbandoned`) before calling this.
5510
+ */
5511
+ async write(sessionId) {
5512
+ await ensureDir(path2.dirname(this.file));
5513
+ const lock = {
5514
+ v: 1,
5515
+ sessionId,
5516
+ pid: this.pid,
5517
+ hostname: this.hostname,
5518
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
5519
+ };
5520
+ const tmp = `${this.file}.tmp`;
5521
+ await fsp.writeFile(tmp, JSON.stringify(lock), { mode: 384 });
5522
+ await fsp.rename(tmp, this.file);
5523
+ }
5524
+ /**
5525
+ * Release the lock. Idempotent — silently succeeds if the file is
5526
+ * already gone (e.g. someone else cleared it, or the directory was
5527
+ * wiped).
5528
+ */
5529
+ async clear() {
5530
+ try {
5531
+ await fsp.unlink(this.file);
5532
+ } catch (err) {
5533
+ const code = err.code;
5534
+ if (code === "ENOENT") return;
5535
+ throw err;
5536
+ }
5537
+ }
5538
+ async readLock() {
5539
+ let raw;
5540
+ try {
5541
+ raw = await fsp.readFile(this.file, "utf8");
5542
+ } catch (err) {
5543
+ const code = err.code;
5544
+ if (code === "ENOENT") return null;
5545
+ return null;
5546
+ }
5547
+ try {
5548
+ const parsed = JSON.parse(raw);
5549
+ if (!isLockFile(parsed)) return null;
5550
+ return parsed;
5551
+ } catch {
5552
+ return null;
5553
+ }
5554
+ }
5555
+ };
5556
+ function isLockFile(v) {
5557
+ if (typeof v !== "object" || v === null) return false;
5558
+ const o = v;
5559
+ return o["v"] === 1 && typeof o["sessionId"] === "string" && typeof o["pid"] === "number" && typeof o["hostname"] === "string" && typeof o["startedAt"] === "string";
5560
+ }
5561
+ function defaultIsPidAlive(pid) {
5562
+ if (!Number.isInteger(pid) || pid <= 0) return false;
5563
+ try {
5564
+ process.kill(pid, 0);
5565
+ return true;
5566
+ } catch (err) {
5567
+ const code = err.code;
5568
+ if (code === "EPERM") return true;
5569
+ return false;
5570
+ }
5571
+ }
5572
+
5573
+ // src/defaults/tool-executor.ts
5574
+ var ToolExecutor = class {
5575
+ constructor(registry, opts) {
5576
+ this.registry = registry;
5577
+ this.opts = opts;
5578
+ this.iterationTimeoutMs = opts.iterationTimeoutMs ?? 3e5;
5579
+ this.serializer = createToolOutputSerializer({
5580
+ perIterationOutputCapBytes: opts.perIterationOutputCapBytes ?? 1e5
5581
+ });
5582
+ }
5583
+ registry;
5584
+ opts;
5585
+ serializer;
5586
+ iterationTimeoutMs;
5587
+ /**
5588
+ * Execute a batch of tool uses using the configured strategy.
5589
+ * Returns the execution results and the remaining output budget.
5590
+ */
5591
+ async executeBatch(toolUses, ctx, strategy) {
5592
+ let budget = this.opts.perIterationOutputCapBytes ?? 1e5;
5593
+ const runOne = async (use) => {
5594
+ const start = Date.now();
5595
+ const tool = this.registry.get(use.name);
5596
+ if (!tool) {
5597
+ const result = this.unknownToolResult(use, () => this.registry.list().map((t2) => t2.name));
5598
+ budget = this.decrementBudget(result, budget);
5599
+ return { result, tool, durationMs: Date.now() - start };
5600
+ }
5601
+ const decision = await this.opts.permissionPolicy.evaluate(tool, use.input, ctx);
5602
+ if (decision.permission === "deny") {
5603
+ const result = this.deniedResult(use, decision.reason);
5604
+ budget = this.decrementBudget(result, budget);
5605
+ return { result, tool, durationMs: Date.now() - start };
5606
+ }
5607
+ if (decision.permission === "confirm") {
5608
+ if (this.opts.confirmAwaiter) {
5609
+ const choice = await this.opts.confirmAwaiter(tool, use.input, use.id, tool.name);
5610
+ if (choice !== "yes" && choice !== "always") {
5611
+ const result = { type: "tool_result", tool_use_id: use.id, content: `Tool "${tool.name}" denied by user.`, is_error: true };
5612
+ budget = this.decrementBudget(result, budget);
5613
+ return { result, tool, durationMs: Date.now() - start };
5614
+ }
5615
+ } else {
5616
+ const suggestedPattern = this.subjectFor(tool.name, use.input) ?? tool.name;
5617
+ const pending = { type: "tool_confirm_pending", toolUseId: use.id, toolName: tool.name, input: use.input, suggestedPattern };
5618
+ return { result: pending, tool, durationMs: Date.now() - start };
5619
+ }
5620
+ }
5621
+ const span = this.opts.tracer?.startSpan(`tool.${tool.name}`, {
5622
+ "tool.name": tool.name,
5623
+ "tool.mutating": tool.mutating,
5624
+ "tool.permission": tool.permission
5625
+ });
5626
+ try {
5627
+ const result = await this.executeTool(tool, use, ctx, budget);
5628
+ budget = this.decrementBudget(result, budget);
5629
+ span?.setAttribute("tool.is_error", !!result.is_error);
5630
+ span?.setAttribute(
5631
+ "tool.output_bytes",
5632
+ typeof result.content === "string" ? result.content.length : 0
5633
+ );
5634
+ return { result, tool, durationMs: Date.now() - start };
5635
+ } catch (err) {
5636
+ const msg = err instanceof Error ? err.message : String(err);
5637
+ const scrubbed = this.opts.secretScrubber.scrub(msg);
5638
+ this.opts.renderer?.writeToolResult(tool.name, scrubbed, true);
5639
+ const result = { type: "tool_result", tool_use_id: use.id, content: `Tool "${tool.name}" threw: ${scrubbed}`, is_error: true };
5640
+ budget = this.decrementBudget(result, budget);
5641
+ if (err instanceof Error) span?.recordError(err);
5642
+ span?.setAttribute("tool.is_error", true);
5643
+ return { result, tool, durationMs: Date.now() - start };
5644
+ } finally {
5645
+ span?.end();
5646
+ }
5647
+ };
5648
+ if (strategy === "sequential") {
5649
+ const outputs = [];
5650
+ for (const use of toolUses) {
5651
+ if (use) outputs.push(await runOne(use));
5652
+ }
5653
+ return { outputs, remainingBudget: budget };
5654
+ }
5655
+ if (strategy === "parallel") {
5656
+ const outputs = await Promise.all(toolUses.map((use) => runOne(use)));
5657
+ return { outputs, remainingBudget: budget };
5658
+ }
5659
+ const nonMutating = [];
5660
+ const mutating = [];
5661
+ for (const use of toolUses) {
5662
+ if (!use) continue;
5663
+ const tool = this.registry.get(use.name);
5664
+ if (tool?.mutating) mutating.push(use);
5665
+ else nonMutating.push(use);
5666
+ }
5667
+ const firstPass = await Promise.all(nonMutating.map((use) => runOne(use)));
5668
+ const secondPass = [];
5669
+ for (const use of mutating) {
5670
+ secondPass.push(await runOne(use));
5671
+ }
5672
+ return {
5673
+ outputs: [...firstPass, ...secondPass],
5674
+ remainingBudget: budget
5675
+ };
5676
+ }
5677
+ /**
5678
+ * Execute a single tool with timeout, permission check, and output capping.
5679
+ * Emits `tool.started` via the injected EventBus (if any) right before
5680
+ * invoking the tool — closes the observability gap between "model decided
5681
+ * to call a tool" and "tool.executed".
4989
5682
  */
4990
5683
  async executeTool(tool, use, ctx, budget) {
4991
5684
  this.opts.events?.emit("tool.started", {
@@ -4994,7 +5687,7 @@ var ToolExecutor = class {
4994
5687
  input: use.input
4995
5688
  });
4996
5689
  this.opts.renderer?.writeToolCall(tool.name, use.input);
4997
- const output = await this.runWithTimeout(tool, use.input, ctx.signal, ctx);
5690
+ const output = await this.runWithTimeout(tool, use.input, ctx.signal, ctx, use.id);
4998
5691
  const text = this.serializer.serialize(output);
4999
5692
  const scrubbed = this.opts.secretScrubber.scrub(text);
5000
5693
  const { text: capped } = this.serializer.enforceCap(scrubbed, budget);
@@ -5007,20 +5700,52 @@ var ToolExecutor = class {
5007
5700
  is_error: false
5008
5701
  };
5009
5702
  }
5010
- async runWithTimeout(tool, input, parentSignal, ctx) {
5703
+ async runWithTimeout(tool, input, parentSignal, ctx, toolUseId) {
5011
5704
  if (parentSignal.aborted) {
5012
5705
  throw parentSignal.reason instanceof Error ? parentSignal.reason : new Error(typeof parentSignal.reason === "string" ? parentSignal.reason : "aborted");
5013
5706
  }
5014
5707
  const timeoutMs = tool.timeoutMs ?? this.iterationTimeoutMs;
5015
5708
  const ctrl = new AbortController();
5016
5709
  const timer = setTimeout(() => ctrl.abort(new Error("tool timeout")), timeoutMs);
5017
- const combined = anySignal([parentSignal, ctrl.signal]);
5710
+ const combined = AbortSignal.any([parentSignal, ctrl.signal]);
5018
5711
  try {
5712
+ if (typeof tool.executeStream === "function") {
5713
+ return await this.runStreamedTool(tool, input, ctx, combined, toolUseId);
5714
+ }
5019
5715
  return await tool.execute(input, ctx, { signal: combined });
5716
+ } catch (err) {
5717
+ if (combined.aborted && typeof tool.cleanup === "function") {
5718
+ try {
5719
+ await tool.cleanup(input, ctx);
5720
+ } catch {
5721
+ }
5722
+ }
5723
+ throw err;
5020
5724
  } finally {
5021
5725
  clearTimeout(timer);
5022
5726
  }
5023
5727
  }
5728
+ async runStreamedTool(tool, input, ctx, signal, toolUseId) {
5729
+ let finalOutput;
5730
+ let sawFinal = false;
5731
+ const stream = tool.executeStream(input, ctx, { signal });
5732
+ for await (const ev of stream) {
5733
+ if (ev.type === "final") {
5734
+ finalOutput = ev.output;
5735
+ sawFinal = true;
5736
+ break;
5737
+ }
5738
+ this.opts.events?.emit("tool.progress", {
5739
+ name: tool.name,
5740
+ id: toolUseId ?? "<unknown>",
5741
+ event: ev
5742
+ });
5743
+ }
5744
+ if (!sawFinal) {
5745
+ throw new Error(`tool "${tool.name}" executeStream completed without a 'final' event`);
5746
+ }
5747
+ return finalOutput;
5748
+ }
5024
5749
  unknownToolResult(use, listFns) {
5025
5750
  return {
5026
5751
  type: "tool_result",
@@ -5037,32 +5762,959 @@ var ToolExecutor = class {
5037
5762
  is_error: true
5038
5763
  };
5039
5764
  }
5040
- confirmResult(use) {
5041
- return {
5042
- type: "tool_result",
5043
- tool_use_id: use.id,
5044
- content: `Tool "${use.name}" requires user confirmation but no prompt handler was available.`,
5045
- is_error: true
5765
+ decrementBudget(result, budget) {
5766
+ const contentBytes = typeof result.content === "string" ? Buffer.byteLength(result.content, "utf8") : Buffer.byteLength(JSON.stringify(result.content), "utf8");
5767
+ return Math.max(0, budget - contentBytes);
5768
+ }
5769
+ /**
5770
+ * Compute the suggestedPattern string for a tool+input pair.
5771
+ * Matches the logic in DefaultPermissionPolicy so the TUI shows the
5772
+ * same subject that the trust file would use.
5773
+ */
5774
+ subjectFor(toolName, input) {
5775
+ if (!input || typeof input !== "object") return void 0;
5776
+ const obj = input;
5777
+ const globChars = /[*?\[\]]/g;
5778
+ const escapeGlob = (s) => s.replace(globChars, (c) => `\\${c}`);
5779
+ if (toolName === "bash" && typeof obj.command === "string") {
5780
+ return escapeGlob(obj.command);
5781
+ }
5782
+ if (typeof obj.path === "string") {
5783
+ return escapeGlob(obj.path.replace(/\\/g, "/"));
5784
+ }
5785
+ if (typeof obj.url === "string") {
5786
+ return escapeGlob(obj.url);
5787
+ }
5788
+ if (typeof obj.name === "string") {
5789
+ return escapeGlob(obj.name);
5790
+ }
5791
+ return void 0;
5792
+ }
5793
+ };
5794
+
5795
+ // src/defaults/session-reader.ts
5796
+ var DefaultSessionReader = class {
5797
+ store;
5798
+ constructor(opts) {
5799
+ this.store = opts.store;
5800
+ }
5801
+ async query(q = {}) {
5802
+ const raw = await this.store.list(q.limit ? Math.max(q.limit * 4, 100) : 1e3);
5803
+ const titleNeedle = q.titleContains?.toLowerCase();
5804
+ const filtered = raw.filter((s) => {
5805
+ if (q.since && s.startedAt < q.since) return false;
5806
+ if (q.until && s.startedAt > q.until) return false;
5807
+ if (q.provider && s.provider !== q.provider) return false;
5808
+ if (q.model && s.model !== q.model) return false;
5809
+ if (q.minTokens !== void 0 && s.tokenTotal < q.minTokens) return false;
5810
+ if (titleNeedle && !s.title.toLowerCase().includes(titleNeedle)) return false;
5811
+ return true;
5812
+ });
5813
+ const out = filtered.map((s) => ({
5814
+ id: s.id,
5815
+ title: s.title,
5816
+ startedAt: s.startedAt,
5817
+ provider: s.provider,
5818
+ model: s.model,
5819
+ tokenTotal: s.tokenTotal
5820
+ }));
5821
+ return q.limit ? out.slice(0, q.limit) : out;
5822
+ }
5823
+ async *replay(sessionId) {
5824
+ const data = await this.store.load(sessionId);
5825
+ for (const e of data.events) yield e;
5826
+ }
5827
+ async search(q, sessionId) {
5828
+ const limit = q.limit ?? 100;
5829
+ const matcher = buildMatcher(q);
5830
+ const allowedTypes = q.types ? new Set(q.types) : null;
5831
+ const ids = sessionId ? [sessionId] : (await this.store.list(1e3)).map((s) => s.id);
5832
+ const hits = [];
5833
+ for (const id of ids) {
5834
+ let data;
5835
+ try {
5836
+ data = await this.store.load(id);
5837
+ } catch {
5838
+ continue;
5839
+ }
5840
+ for (let i = 0; i < data.events.length; i++) {
5841
+ const ev = data.events[i];
5842
+ if (allowedTypes && !allowedTypes.has(ev.type)) continue;
5843
+ const text = eventText(ev);
5844
+ if (text === null) continue;
5845
+ const hit = matcher(text);
5846
+ if (!hit) continue;
5847
+ hits.push({
5848
+ sessionId: id,
5849
+ eventIndex: i,
5850
+ ts: ev.ts,
5851
+ type: ev.type,
5852
+ snippet: snippetOf(text, hit.start, hit.end)
5853
+ });
5854
+ if (hits.length >= limit) return hits;
5855
+ }
5856
+ }
5857
+ return hits;
5858
+ }
5859
+ async export(sessionId, opts) {
5860
+ const data = await this.store.load(sessionId);
5861
+ const includeTools = opts.includeTools ?? true;
5862
+ const includeDiagnostics = opts.includeDiagnostics ?? true;
5863
+ const filtered = data.events.filter((e) => {
5864
+ if (!includeTools && (e.type === "tool_use" || e.type === "tool_result" || e.type === "tool_call_start" || e.type === "tool_call_end")) {
5865
+ return false;
5866
+ }
5867
+ if (!includeDiagnostics && (e.type === "error" || e.type === "compaction" || e.type === "message_truncated")) {
5868
+ return false;
5869
+ }
5870
+ return true;
5871
+ });
5872
+ if (opts.format === "json") {
5873
+ return JSON.stringify({ metadata: data.metadata, events: filtered }, null, 2);
5874
+ }
5875
+ if (opts.format === "text") {
5876
+ return renderPlainText(data.metadata, filtered);
5877
+ }
5878
+ return renderMarkdown(data.metadata, filtered);
5879
+ }
5880
+ async metadata(sessionId) {
5881
+ const data = await this.store.load(sessionId);
5882
+ return data.metadata;
5883
+ }
5884
+ };
5885
+ function buildMatcher(q) {
5886
+ const ci = q.caseInsensitive ?? true;
5887
+ if (q.regex) {
5888
+ const flags = ci ? "i" : "";
5889
+ const re = new RegExp(q.query, flags);
5890
+ return (text) => {
5891
+ const m = re.exec(text);
5892
+ return m ? { start: m.index, end: m.index + m[0].length } : null;
5046
5893
  };
5047
5894
  }
5895
+ const needle = ci ? q.query.toLowerCase() : q.query;
5896
+ return (text) => {
5897
+ const hay = ci ? text.toLowerCase() : text;
5898
+ const idx = hay.indexOf(needle);
5899
+ return idx === -1 ? null : { start: idx, end: idx + needle.length };
5900
+ };
5901
+ }
5902
+ function eventText(e) {
5903
+ switch (e.type) {
5904
+ case "user_input":
5905
+ return contentToString(e.content);
5906
+ case "llm_response":
5907
+ return contentToString(e.content);
5908
+ case "tool_use":
5909
+ return `${e.name} ${JSON.stringify(e.input)}`;
5910
+ case "tool_result":
5911
+ return typeof e.content === "string" ? e.content : JSON.stringify(e.content);
5912
+ case "error":
5913
+ return `${e.phase}: ${e.message}`;
5914
+ case "session_start":
5915
+ case "session_resumed":
5916
+ return `${e.model}/${e.provider}`;
5917
+ case "task_created":
5918
+ case "task_completed":
5919
+ return e.title;
5920
+ case "task_failed":
5921
+ return `${e.title}: ${e.error}`;
5922
+ case "skill_activated":
5923
+ case "skill_deactivated":
5924
+ return e.skillName;
5925
+ default:
5926
+ return null;
5927
+ }
5928
+ }
5929
+ function contentToString(content) {
5930
+ if (typeof content === "string") return content;
5931
+ return content.map((b) => {
5932
+ switch (b.type) {
5933
+ case "text":
5934
+ return b.text;
5935
+ case "tool_use":
5936
+ return `[tool_use:${b.name} ${JSON.stringify(b.input)}]`;
5937
+ case "tool_result":
5938
+ return typeof b.content === "string" ? b.content : JSON.stringify(b.content);
5939
+ default:
5940
+ return "";
5941
+ }
5942
+ }).join("\n");
5943
+ }
5944
+ var SNIPPET_RADIUS = 60;
5945
+ function snippetOf(text, start, end) {
5946
+ const from = Math.max(0, start - SNIPPET_RADIUS);
5947
+ const to = Math.min(text.length, end + SNIPPET_RADIUS);
5948
+ const prefix = from > 0 ? "\u2026" : "";
5949
+ const suffix = to < text.length ? "\u2026" : "";
5950
+ return prefix + text.slice(from, to).replace(/\s+/g, " ").trim() + suffix;
5951
+ }
5952
+ function renderMarkdown(meta, events) {
5953
+ const lines = [];
5954
+ lines.push(`# Session ${meta.id}`);
5955
+ lines.push("");
5956
+ if (meta.model || meta.provider) {
5957
+ lines.push(`- **Model:** ${meta.provider ?? "?"}/${meta.model ?? "?"}`);
5958
+ }
5959
+ lines.push(`- **Started:** ${meta.startedAt}`);
5960
+ if (meta.endedAt) lines.push(`- **Ended:** ${meta.endedAt}`);
5961
+ lines.push("");
5962
+ lines.push("---");
5963
+ lines.push("");
5964
+ for (const e of events) {
5965
+ switch (e.type) {
5966
+ case "user_input": {
5967
+ lines.push(`## User \u2014 ${e.ts}`);
5968
+ lines.push("");
5969
+ lines.push(contentToString(e.content));
5970
+ lines.push("");
5971
+ break;
5972
+ }
5973
+ case "llm_response": {
5974
+ lines.push(`## Assistant \u2014 ${e.ts}`);
5975
+ lines.push("");
5976
+ lines.push(contentToString(e.content));
5977
+ if (e.stopReason && e.stopReason !== "end_turn") {
5978
+ lines.push("");
5979
+ lines.push(`*stop: ${e.stopReason}*`);
5980
+ }
5981
+ lines.push("");
5982
+ break;
5983
+ }
5984
+ case "tool_use": {
5985
+ lines.push(`### Tool call: \`${e.name}\``);
5986
+ lines.push("");
5987
+ lines.push("```json");
5988
+ lines.push(JSON.stringify(e.input, null, 2));
5989
+ lines.push("```");
5990
+ lines.push("");
5991
+ break;
5992
+ }
5993
+ case "tool_result": {
5994
+ const body = typeof e.content === "string" ? e.content : JSON.stringify(e.content, null, 2);
5995
+ lines.push(`### Tool result${e.isError ? " (error)" : ""}`);
5996
+ lines.push("");
5997
+ lines.push("```");
5998
+ lines.push(body);
5999
+ lines.push("```");
6000
+ lines.push("");
6001
+ break;
6002
+ }
6003
+ case "error": {
6004
+ lines.push(`> **Error** (${e.phase}): ${e.message}`);
6005
+ lines.push("");
6006
+ break;
6007
+ }
6008
+ case "compaction": {
6009
+ lines.push(`> **Compaction**: ${e.before} \u2192 ${e.after} tokens`);
6010
+ lines.push("");
6011
+ break;
6012
+ }
6013
+ }
6014
+ }
6015
+ return lines.join("\n");
6016
+ }
6017
+ function renderPlainText(meta, events) {
6018
+ const lines = [];
6019
+ lines.push(`Session ${meta.id} \u2014 ${meta.provider ?? "?"}/${meta.model ?? "?"} \u2014 started ${meta.startedAt}`);
6020
+ lines.push("".padEnd(72, "-"));
6021
+ for (const e of events) {
6022
+ switch (e.type) {
6023
+ case "user_input":
6024
+ lines.push(`[${e.ts}] USER`);
6025
+ lines.push(contentToString(e.content));
6026
+ lines.push("");
6027
+ break;
6028
+ case "llm_response":
6029
+ lines.push(`[${e.ts}] ASSISTANT`);
6030
+ lines.push(contentToString(e.content));
6031
+ lines.push("");
6032
+ break;
6033
+ case "tool_use":
6034
+ lines.push(`[${e.ts}] TOOL_USE ${e.name} ${JSON.stringify(e.input)}`);
6035
+ break;
6036
+ case "tool_result":
6037
+ lines.push(
6038
+ `[${e.ts}] TOOL_RESULT${e.isError ? " (error)" : ""} ${typeof e.content === "string" ? e.content : JSON.stringify(e.content)}`
6039
+ );
6040
+ break;
6041
+ case "error":
6042
+ lines.push(`[${e.ts}] ERROR (${e.phase}): ${e.message}`);
6043
+ break;
6044
+ }
6045
+ }
6046
+ return lines.join("\n");
6047
+ }
6048
+
6049
+ // src/defaults/observability/metrics.ts
6050
+ var RESERVOIR_SIZE = 1024;
6051
+ function labelKey(labels) {
6052
+ if (!labels) return "";
6053
+ const keys = Object.keys(labels).sort();
6054
+ return keys.map((k) => `${k}=${labels[k]}`).join(",");
6055
+ }
6056
+ function quantile(sorted, q) {
6057
+ if (sorted.length === 0) return 0;
6058
+ const idx = Math.min(sorted.length - 1, Math.floor(q * sorted.length));
6059
+ return sorted[idx] ?? 0;
6060
+ }
6061
+ var InMemoryMetricsSink = class {
6062
+ counters = /* @__PURE__ */ new Map();
6063
+ gauges = /* @__PURE__ */ new Map();
6064
+ histograms = /* @__PURE__ */ new Map();
6065
+ counter(name, value = 1, labels) {
6066
+ const series = this.getOrCreate(this.counters, name);
6067
+ const key = labelKey(labels);
6068
+ const state = series.get(key) ?? { value: 0 };
6069
+ state.value += value;
6070
+ series.set(key, state);
6071
+ }
6072
+ gauge(name, value, labels) {
6073
+ const series = this.getOrCreate(this.gauges, name);
6074
+ series.set(labelKey(labels), { value });
6075
+ }
6076
+ histogram(name, value, labels) {
6077
+ const series = this.getOrCreate(this.histograms, name);
6078
+ const key = labelKey(labels);
6079
+ let state = series.get(key);
6080
+ if (!state) {
6081
+ state = { count: 0, sum: 0, min: value, max: value, samples: [] };
6082
+ series.set(key, state);
6083
+ }
6084
+ state.count++;
6085
+ state.sum += value;
6086
+ if (value < state.min) state.min = value;
6087
+ if (value > state.max) state.max = value;
6088
+ if (state.samples.length < RESERVOIR_SIZE) {
6089
+ state.samples.push(value);
6090
+ } else {
6091
+ const r = Math.floor(Math.random() * state.count);
6092
+ if (r < RESERVOIR_SIZE) state.samples[r] = value;
6093
+ }
6094
+ }
6095
+ snapshot() {
6096
+ const series = [];
6097
+ for (const [name, byLabel] of this.counters) {
6098
+ for (const [key, state] of byLabel) {
6099
+ series.push({
6100
+ name,
6101
+ type: "counter",
6102
+ labels: parseLabelKey(key),
6103
+ values: { value: state.value }
6104
+ });
6105
+ }
6106
+ }
6107
+ for (const [name, byLabel] of this.gauges) {
6108
+ for (const [key, state] of byLabel) {
6109
+ series.push({
6110
+ name,
6111
+ type: "gauge",
6112
+ labels: parseLabelKey(key),
6113
+ values: { value: state.value }
6114
+ });
6115
+ }
6116
+ }
6117
+ for (const [name, byLabel] of this.histograms) {
6118
+ for (const [key, state] of byLabel) {
6119
+ const sorted = [...state.samples].sort((a, b) => a - b);
6120
+ series.push({
6121
+ name,
6122
+ type: "histogram",
6123
+ labels: parseLabelKey(key),
6124
+ values: {
6125
+ count: state.count,
6126
+ sum: state.sum,
6127
+ min: state.min,
6128
+ max: state.max,
6129
+ p50: quantile(sorted, 0.5),
6130
+ p95: quantile(sorted, 0.95),
6131
+ p99: quantile(sorted, 0.99)
6132
+ }
6133
+ });
6134
+ }
6135
+ }
6136
+ return { timestamp: Date.now(), series };
6137
+ }
6138
+ reset() {
6139
+ this.counters.clear();
6140
+ this.gauges.clear();
6141
+ this.histograms.clear();
6142
+ }
6143
+ getOrCreate(bag, name) {
6144
+ let series = bag.get(name);
6145
+ if (!series) {
6146
+ series = /* @__PURE__ */ new Map();
6147
+ bag.set(name, series);
6148
+ }
6149
+ return series;
6150
+ }
5048
6151
  };
5049
- function anySignal(signals) {
5050
- if (typeof AbortSignal.any === "function") {
5051
- return AbortSignal.any(signals);
6152
+ function parseLabelKey(key) {
6153
+ if (!key) return {};
6154
+ const labels = {};
6155
+ for (const pair of key.split(",")) {
6156
+ const eq = pair.indexOf("=");
6157
+ if (eq > 0) labels[pair.slice(0, eq)] = pair.slice(eq + 1);
6158
+ }
6159
+ return labels;
6160
+ }
6161
+ var NoopMetricsSink = class {
6162
+ counter() {
6163
+ }
6164
+ gauge() {
6165
+ }
6166
+ histogram() {
6167
+ }
6168
+ snapshot() {
6169
+ return { timestamp: Date.now(), series: [] };
6170
+ }
6171
+ reset() {
6172
+ }
6173
+ };
6174
+
6175
+ // src/defaults/observability/health.ts
6176
+ var SEVERITY = {
6177
+ healthy: 0,
6178
+ degraded: 1,
6179
+ unhealthy: 2
6180
+ };
6181
+ var DefaultHealthRegistry = class {
6182
+ checks = /* @__PURE__ */ new Map();
6183
+ timeoutMs;
6184
+ constructor(opts = {}) {
6185
+ this.timeoutMs = opts.timeoutMs ?? 5e3;
6186
+ }
6187
+ register(check) {
6188
+ this.checks.set(check.name, check);
6189
+ }
6190
+ unregister(name) {
6191
+ this.checks.delete(name);
6192
+ }
6193
+ async run() {
6194
+ const results = await Promise.all(
6195
+ Array.from(this.checks.values()).map(async (c) => {
6196
+ const result = await this.runOne(c);
6197
+ return { name: c.name, ...result };
6198
+ })
6199
+ );
6200
+ let status = "healthy";
6201
+ for (const r of results) {
6202
+ if (SEVERITY[r.status] > SEVERITY[status]) status = r.status;
6203
+ }
6204
+ return { status, timestamp: Date.now(), checks: results };
6205
+ }
6206
+ async runOne(check) {
6207
+ const timeout = new Promise((resolve4) => {
6208
+ setTimeout(
6209
+ () => resolve4({ status: "unhealthy", detail: `timeout after ${this.timeoutMs}ms` }),
6210
+ this.timeoutMs
6211
+ );
6212
+ });
6213
+ try {
6214
+ return await Promise.race([check.check(), timeout]);
6215
+ } catch (err) {
6216
+ return { status: "unhealthy", detail: err instanceof Error ? err.message : String(err) };
6217
+ }
6218
+ }
6219
+ };
6220
+
6221
+ // src/defaults/observability/tracer.ts
6222
+ var NoopTracer = class {
6223
+ startSpan() {
6224
+ return NOOP_SPAN;
6225
+ }
6226
+ };
6227
+ var NOOP_SPAN = {
6228
+ setAttribute() {
6229
+ },
6230
+ recordError() {
6231
+ },
6232
+ end() {
6233
+ }
6234
+ };
6235
+
6236
+ // src/defaults/observability/otel-tracer.ts
6237
+ var OTEL_STATUS_ERROR = 2;
6238
+ var OTelTracer = class {
6239
+ constructor(upstream) {
6240
+ this.upstream = upstream;
6241
+ }
6242
+ upstream;
6243
+ startSpan(name, attrs) {
6244
+ const otelSpan = this.upstream.startSpan(name, attrs ? { attributes: attrs } : void 0);
6245
+ return new OTelSpan(otelSpan);
6246
+ }
6247
+ };
6248
+ var OTelSpan = class {
6249
+ constructor(span) {
6250
+ this.span = span;
6251
+ }
6252
+ span;
6253
+ setAttribute(key, value) {
6254
+ this.span.setAttribute(key, value);
6255
+ }
6256
+ recordError(err) {
6257
+ this.span.recordException(err);
6258
+ this.span.setStatus?.({ code: OTEL_STATUS_ERROR, message: err.message });
6259
+ }
6260
+ end() {
6261
+ this.span.end();
6262
+ }
6263
+ };
6264
+
6265
+ // src/defaults/observability/event-bridge.ts
6266
+ function wireMetricsToEvents(events, sink) {
6267
+ const unsubs = [];
6268
+ unsubs.push(
6269
+ events.on("session.started", () => sink.counter("agent.sessions.started")),
6270
+ events.on("session.ended", (e) => {
6271
+ sink.counter("agent.sessions.ended");
6272
+ sink.histogram("agent.session.tokens.input", e.usage.input);
6273
+ sink.histogram("agent.session.tokens.output", e.usage.output);
6274
+ }),
6275
+ events.on("session.damaged", () => sink.counter("agent.sessions.damaged")),
6276
+ events.on("iteration.completed", () => sink.counter("agent.iterations.total")),
6277
+ events.on("iteration.limit_reached", () => sink.counter("agent.iteration_limit.hit")),
6278
+ events.on("provider.response", (e) => {
6279
+ sink.counter("provider.responses.total", 1, { stop_reason: e.stopReason });
6280
+ sink.counter("provider.tokens.input", e.usage.input);
6281
+ sink.counter("provider.tokens.output", e.usage.output);
6282
+ if (e.usage.cacheRead) sink.counter("provider.tokens.cache_read", e.usage.cacheRead);
6283
+ if (e.usage.cacheWrite) sink.counter("provider.tokens.cache_write", e.usage.cacheWrite);
6284
+ }),
6285
+ events.on(
6286
+ "provider.retry",
6287
+ (e) => sink.counter("provider.retries.total", 1, {
6288
+ provider: e.providerId,
6289
+ status: String(e.status)
6290
+ })
6291
+ ),
6292
+ events.on(
6293
+ "provider.error",
6294
+ (e) => sink.counter("provider.errors.total", 1, {
6295
+ provider: e.providerId,
6296
+ status: String(e.status),
6297
+ retryable: String(e.retryable)
6298
+ })
6299
+ ),
6300
+ events.on("tool.started", (e) => sink.counter("tool.starts.total", 1, { tool: e.name })),
6301
+ events.on("tool.executed", (e) => {
6302
+ sink.counter("tool.executions.total", 1, { tool: e.name, ok: String(e.ok) });
6303
+ sink.histogram("tool.duration_ms", e.durationMs, { tool: e.name });
6304
+ }),
6305
+ events.on("token.threshold", (e) => sink.gauge("agent.tokens.used", e.used)),
6306
+ events.on("compaction.fired", (e) => {
6307
+ sink.counter("compaction.fired.total");
6308
+ sink.histogram("compaction.reduction_tokens", e.before - e.after);
6309
+ }),
6310
+ events.on(
6311
+ "mcp.server.connected",
6312
+ (e) => sink.counter("mcp.connects.total", 1, { server: e.name })
6313
+ ),
6314
+ events.on(
6315
+ "mcp.server.reconnected",
6316
+ (e) => sink.counter("mcp.reconnects.total", 1, { server: e.name })
6317
+ ),
6318
+ events.on(
6319
+ "mcp.server.disconnected",
6320
+ (e) => sink.counter("mcp.disconnects.total", 1, { server: e.name })
6321
+ ),
6322
+ events.on("error", (e) => sink.counter("agent.errors.total", 1, { phase: e.phase }))
6323
+ );
6324
+ return () => {
6325
+ for (const u of unsubs) u();
6326
+ };
6327
+ }
6328
+
6329
+ // src/defaults/observability/prometheus.ts
6330
+ var NUMBER_FORMAT_INFINITY = "NaN";
6331
+ function escapeLabelValue(v) {
6332
+ return v.replace(/\\/g, "\\\\").replace(/\n/g, "\\n").replace(/"/g, '\\"');
6333
+ }
6334
+ function formatLabels(labels) {
6335
+ const keys = Object.keys(labels);
6336
+ if (keys.length === 0) return "";
6337
+ const parts = keys.map((k) => `${k}="${escapeLabelValue(labels[k] ?? "")}"`);
6338
+ return `{${parts.join(",")}}`;
6339
+ }
6340
+ function formatNumber(n) {
6341
+ if (!Number.isFinite(n)) return NUMBER_FORMAT_INFINITY;
6342
+ return Number.isInteger(n) ? n.toString() : n.toString();
6343
+ }
6344
+ function joinLabels(base, extra) {
6345
+ return { ...base, ...extra };
6346
+ }
6347
+ function renderPrometheus(snapshot) {
6348
+ const groups = /* @__PURE__ */ new Map();
6349
+ for (const s of snapshot.series) {
6350
+ let g = groups.get(s.name);
6351
+ if (!g) {
6352
+ g = { type: s.type, rows: [] };
6353
+ groups.set(s.name, g);
6354
+ }
6355
+ g.rows.push({ labels: s.labels, values: s.values });
6356
+ }
6357
+ const lines = [];
6358
+ for (const [name, g] of groups) {
6359
+ const promType = g.type === "histogram" ? "summary" : g.type;
6360
+ lines.push(`# HELP ${name} ${name}`);
6361
+ lines.push(`# TYPE ${name} ${promType}`);
6362
+ if (g.type === "counter" || g.type === "gauge") {
6363
+ for (const row of g.rows) {
6364
+ lines.push(`${name}${formatLabels(row.labels)} ${formatNumber(row.values.value ?? 0)}`);
6365
+ }
6366
+ } else {
6367
+ for (const row of g.rows) {
6368
+ const { count = 0, sum = 0, p50 = 0, p95 = 0, p99 = 0 } = row.values;
6369
+ lines.push(
6370
+ `${name}${formatLabels(joinLabels(row.labels, { quantile: "0.5" }))} ${formatNumber(p50)}`
6371
+ );
6372
+ lines.push(
6373
+ `${name}${formatLabels(joinLabels(row.labels, { quantile: "0.95" }))} ${formatNumber(p95)}`
6374
+ );
6375
+ lines.push(
6376
+ `${name}${formatLabels(joinLabels(row.labels, { quantile: "0.99" }))} ${formatNumber(p99)}`
6377
+ );
6378
+ lines.push(`${name}_sum${formatLabels(row.labels)} ${formatNumber(sum)}`);
6379
+ lines.push(`${name}_count${formatLabels(row.labels)} ${formatNumber(count)}`);
6380
+ }
6381
+ }
6382
+ }
6383
+ return lines.join("\n") + "\n";
6384
+ }
6385
+ var PROMETHEUS_CONTENT_TYPE = "text/plain; version=0.0.4; charset=utf-8";
6386
+ async function startMetricsServer(opts) {
6387
+ const { createServer } = await import('http');
6388
+ const host = opts.host ?? "127.0.0.1";
6389
+ const path15 = opts.path ?? "/metrics";
6390
+ const healthPath = opts.healthPath ?? "/healthz";
6391
+ const healthRegistry = opts.healthRegistry;
6392
+ const server = createServer((req, res) => {
6393
+ if (!req.url || req.method !== "GET") {
6394
+ res.statusCode = req.url ? 405 : 400;
6395
+ res.end();
6396
+ return;
6397
+ }
6398
+ const url = req.url.split("?")[0];
6399
+ if (url === path15) {
6400
+ let body;
6401
+ try {
6402
+ body = renderPrometheus(opts.sink.snapshot());
6403
+ } catch (err) {
6404
+ res.statusCode = 500;
6405
+ res.setHeader("content-type", "text/plain; charset=utf-8");
6406
+ res.end(`metrics render failed: ${err instanceof Error ? err.message : String(err)}`);
6407
+ return;
6408
+ }
6409
+ res.statusCode = 200;
6410
+ res.setHeader("content-type", PROMETHEUS_CONTENT_TYPE);
6411
+ res.end(body);
6412
+ return;
6413
+ }
6414
+ if (healthRegistry && url === healthPath) {
6415
+ healthRegistry.run().then(
6416
+ (agg) => {
6417
+ res.statusCode = agg.status === "unhealthy" ? 503 : 200;
6418
+ res.setHeader("content-type", "application/json; charset=utf-8");
6419
+ res.end(JSON.stringify(agg, null, 2));
6420
+ },
6421
+ (err) => {
6422
+ res.statusCode = 500;
6423
+ res.setHeader("content-type", "text/plain; charset=utf-8");
6424
+ res.end(`health run failed: ${err instanceof Error ? err.message : String(err)}`);
6425
+ }
6426
+ );
6427
+ return;
6428
+ }
6429
+ res.statusCode = 404;
6430
+ res.setHeader("content-type", "text/plain; charset=utf-8");
6431
+ res.end("Not Found");
6432
+ });
6433
+ await new Promise((resolve4, reject) => {
6434
+ const onError = (err) => {
6435
+ server.off("listening", onListening);
6436
+ reject(err);
6437
+ };
6438
+ const onListening = () => {
6439
+ server.off("error", onError);
6440
+ resolve4();
6441
+ };
6442
+ server.once("error", onError);
6443
+ server.once("listening", onListening);
6444
+ server.listen(opts.port, host);
6445
+ });
6446
+ const addr = server.address();
6447
+ const boundPort = typeof addr === "object" && addr ? addr.port : opts.port;
6448
+ return {
6449
+ port: boundPort,
6450
+ url: `http://${host}:${boundPort}${path15}`,
6451
+ close: () => new Promise((resolve4, reject) => {
6452
+ server.close((err) => err ? reject(err) : resolve4());
6453
+ })
6454
+ };
6455
+ }
6456
+
6457
+ // src/defaults/observability/otlp-metrics.ts
6458
+ var DEFAULT_INTERVAL_MS = 3e4;
6459
+ var DEFAULT_TIMEOUT_MS = 1e4;
6460
+ function joinEndpoint(base) {
6461
+ if (/\/v1\/metrics\/?$/.test(base)) return base;
6462
+ return base.replace(/\/$/, "") + "/v1/metrics";
6463
+ }
6464
+ function attributesFor(labels) {
6465
+ return Object.entries(labels).map(([key, value]) => ({
6466
+ key,
6467
+ value: { stringValue: value }
6468
+ }));
6469
+ }
6470
+ function buildExportBody(opts) {
6471
+ const metrics = [];
6472
+ for (const s of opts.series) {
6473
+ const dp = {
6474
+ attributes: attributesFor(s.labels),
6475
+ timeUnixNano: opts.timeUnixNano
6476
+ };
6477
+ if (s.type === "counter") {
6478
+ dp.asDouble = s.values.value ?? 0;
6479
+ metrics.push({
6480
+ name: s.name,
6481
+ sum: { dataPoints: [dp], aggregationTemporality: 2, isMonotonic: true }
6482
+ });
6483
+ } else if (s.type === "gauge") {
6484
+ dp.asDouble = s.values.value ?? 0;
6485
+ metrics.push({ name: s.name, gauge: { dataPoints: [dp] } });
6486
+ } else {
6487
+ dp.count = String(s.values.count ?? 0);
6488
+ dp.sum = s.values.sum ?? 0;
6489
+ dp.quantileValues = [
6490
+ { quantile: 0.5, value: s.values.p50 ?? 0 },
6491
+ { quantile: 0.95, value: s.values.p95 ?? 0 },
6492
+ { quantile: 0.99, value: s.values.p99 ?? 0 }
6493
+ ];
6494
+ metrics.push({ name: s.name, summary: { dataPoints: [dp] } });
6495
+ }
5052
6496
  }
5053
- const ctrl = new AbortController();
5054
- const abortSources = [];
5055
- for (const s of signals) {
5056
- if (s.aborted) {
5057
- ctrl.abort(s.reason);
5058
- return ctrl.signal;
6497
+ return {
6498
+ resourceMetrics: [
6499
+ {
6500
+ resource: { attributes: attributesFor(opts.resourceAttributes) },
6501
+ scopeMetrics: [
6502
+ {
6503
+ scope: { name: opts.scopeName },
6504
+ metrics
6505
+ }
6506
+ ]
6507
+ }
6508
+ ]
6509
+ };
6510
+ }
6511
+ function buildOtlpMetricsRequest(sink, opts = {}) {
6512
+ return buildExportBody({
6513
+ series: sink.snapshot().series,
6514
+ resourceAttributes: opts.resourceAttributes ?? { "service.name": "wrongstack" },
6515
+ scopeName: opts.scopeName ?? "wrongstack",
6516
+ timeUnixNano: String(BigInt(Date.now()) * 1000000n)
6517
+ });
6518
+ }
6519
+ function startOtlpMetricsExporter(opts) {
6520
+ const url = joinEndpoint(opts.endpoint);
6521
+ const intervalMs = opts.intervalMs ?? DEFAULT_INTERVAL_MS;
6522
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
6523
+ const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
6524
+ const onError = opts.onError ?? (() => {
6525
+ });
6526
+ const resourceAttributes = opts.resourceAttributes ?? { "service.name": "wrongstack" };
6527
+ const scopeName = opts.scopeName ?? "wrongstack";
6528
+ let stopped = false;
6529
+ const headers = {
6530
+ "content-type": "application/json",
6531
+ ...opts.headers ?? {}
6532
+ };
6533
+ if (opts.authorization) headers.authorization = opts.authorization;
6534
+ async function pushOnce() {
6535
+ if (stopped) return;
6536
+ const body = buildExportBody({
6537
+ series: opts.sink.snapshot().series,
6538
+ resourceAttributes,
6539
+ scopeName,
6540
+ timeUnixNano: String(BigInt(Date.now()) * 1000000n)
6541
+ });
6542
+ const controller = new AbortController();
6543
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
6544
+ try {
6545
+ const res = await fetchImpl(url, {
6546
+ method: "POST",
6547
+ headers,
6548
+ body: JSON.stringify(body),
6549
+ signal: controller.signal
6550
+ });
6551
+ if (!res.ok) {
6552
+ const text = await res.text().catch(() => "");
6553
+ onError(new Error(`OTLP push failed: ${res.status} ${res.statusText} ${text}`));
6554
+ }
6555
+ } catch (err) {
6556
+ onError(err);
6557
+ } finally {
6558
+ clearTimeout(timer);
6559
+ }
6560
+ }
6561
+ const handle = setInterval(() => {
6562
+ void pushOnce();
6563
+ }, intervalMs);
6564
+ handle.unref?.();
6565
+ return {
6566
+ flush: pushOnce,
6567
+ async stop() {
6568
+ stopped = true;
6569
+ clearInterval(handle);
6570
+ await pushOnce().catch(onError);
6571
+ }
6572
+ };
6573
+ }
6574
+ var SPAN_STATUS_CODE_UNSET = 0;
6575
+ var SPAN_STATUS_CODE_OK = 1;
6576
+ var SPAN_STATUS_CODE_ERROR = 2;
6577
+ function hex(bytes) {
6578
+ return crypto2.randomBytes(bytes).toString("hex");
6579
+ }
6580
+ function nowNs() {
6581
+ return BigInt(Date.now()) * 1000000n;
6582
+ }
6583
+ var CapturingSpan = class {
6584
+ constructor(state, onEnd) {
6585
+ this.state = state;
6586
+ this.onEnd = onEnd;
6587
+ }
6588
+ state;
6589
+ onEnd;
6590
+ setAttribute(key, value) {
6591
+ this.state.attributes[key] = value;
6592
+ }
6593
+ recordError(err) {
6594
+ this.state.status = { code: SPAN_STATUS_CODE_ERROR, message: err.message };
6595
+ this.state.attributes["exception.message"] = err.message;
6596
+ if (err.name) this.state.attributes["exception.type"] = err.name;
6597
+ }
6598
+ end() {
6599
+ if (this.state.endTimeUnixNano !== void 0) return;
6600
+ this.state.endTimeUnixNano = nowNs();
6601
+ if (this.state.status.code === SPAN_STATUS_CODE_UNSET) {
6602
+ this.state.status.code = SPAN_STATUS_CODE_OK;
5059
6603
  }
5060
- abortSources.push(s);
6604
+ this.onEnd(this.state);
5061
6605
  }
5062
- for (const s of abortSources) {
5063
- s.addEventListener("abort", () => ctrl.abort(s.reason), { once: true });
6606
+ };
6607
+ var DEFAULT_INTERVAL_MS2 = 5e3;
6608
+ var DEFAULT_BUFFER_CAP = 2048;
6609
+ var DEFAULT_TIMEOUT_MS2 = 1e4;
6610
+ function joinEndpoint2(base) {
6611
+ if (/\/v1\/traces\/?$/.test(base)) return base;
6612
+ return base.replace(/\/$/, "") + "/v1/traces";
6613
+ }
6614
+ function encodeAttr(key, value) {
6615
+ if (typeof value === "boolean") return { key, value: { boolValue: value } };
6616
+ if (typeof value === "number") {
6617
+ return Number.isInteger(value) ? { key, value: { intValue: String(value) } } : { key, value: { doubleValue: value } };
5064
6618
  }
5065
- return ctrl.signal;
6619
+ return { key, value: { stringValue: value } };
6620
+ }
6621
+ function buildOtlpTracesRequest(spans, opts = {}) {
6622
+ const resourceAttributes = opts.resourceAttributes ?? { "service.name": "wrongstack" };
6623
+ const scopeName = opts.scopeName ?? "wrongstack";
6624
+ const otlpSpans = spans.map((s) => ({
6625
+ traceId: s.traceId,
6626
+ spanId: s.spanId,
6627
+ name: s.name,
6628
+ kind: 1,
6629
+ // SPAN_KIND_INTERNAL
6630
+ startTimeUnixNano: s.startTimeUnixNano.toString(),
6631
+ endTimeUnixNano: (s.endTimeUnixNano ?? s.startTimeUnixNano).toString(),
6632
+ attributes: Object.entries(s.attributes).map(([k, v]) => encodeAttr(k, v)),
6633
+ status: s.status
6634
+ }));
6635
+ return {
6636
+ resourceSpans: [
6637
+ {
6638
+ resource: {
6639
+ attributes: Object.entries(resourceAttributes).map(
6640
+ ([k, v]) => encodeAttr(k, v)
6641
+ )
6642
+ },
6643
+ scopeSpans: [{ scope: { name: scopeName }, spans: otlpSpans }]
6644
+ }
6645
+ ]
6646
+ };
6647
+ }
6648
+ function startOtlpTraceExporter(opts) {
6649
+ const url = joinEndpoint2(opts.endpoint);
6650
+ const intervalMs = opts.intervalMs ?? DEFAULT_INTERVAL_MS2;
6651
+ const maxBuffered = opts.maxBufferedSpans ?? DEFAULT_BUFFER_CAP;
6652
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
6653
+ const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
6654
+ const onError = opts.onError ?? (() => {
6655
+ });
6656
+ const resourceAttributes = opts.resourceAttributes ?? { "service.name": "wrongstack" };
6657
+ const scopeName = opts.scopeName ?? "wrongstack";
6658
+ let stopped = false;
6659
+ const buffer = [];
6660
+ const headers = {
6661
+ "content-type": "application/json",
6662
+ ...opts.headers ?? {}
6663
+ };
6664
+ if (opts.authorization) headers.authorization = opts.authorization;
6665
+ const tracer = {
6666
+ startSpan(name, attrs) {
6667
+ const state = {
6668
+ traceId: hex(16),
6669
+ spanId: hex(8),
6670
+ name,
6671
+ startTimeUnixNano: nowNs(),
6672
+ attributes: { ...attrs ?? {} },
6673
+ status: { code: SPAN_STATUS_CODE_UNSET }
6674
+ };
6675
+ return new CapturingSpan(state, (ended) => {
6676
+ if (buffer.length >= maxBuffered) buffer.shift();
6677
+ buffer.push(ended);
6678
+ });
6679
+ }
6680
+ };
6681
+ async function pushOnce() {
6682
+ if (buffer.length === 0) return;
6683
+ const batch = buffer.splice(0, buffer.length);
6684
+ const body = buildOtlpTracesRequest(batch, { resourceAttributes, scopeName });
6685
+ const controller = new AbortController();
6686
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
6687
+ try {
6688
+ const res = await fetchImpl(url, {
6689
+ method: "POST",
6690
+ headers,
6691
+ body: JSON.stringify(body),
6692
+ signal: controller.signal
6693
+ });
6694
+ if (!res.ok) {
6695
+ const text = await res.text().catch(() => "");
6696
+ onError(new Error(`OTLP traces push failed: ${res.status} ${res.statusText} ${text}`));
6697
+ }
6698
+ } catch (err) {
6699
+ onError(err);
6700
+ } finally {
6701
+ clearTimeout(timer);
6702
+ }
6703
+ }
6704
+ const handle = setInterval(() => {
6705
+ if (!stopped) void pushOnce();
6706
+ }, intervalMs);
6707
+ handle.unref?.();
6708
+ return {
6709
+ tracer,
6710
+ flush: pushOnce,
6711
+ async stop() {
6712
+ stopped = true;
6713
+ clearInterval(handle);
6714
+ await pushOnce().catch(onError);
6715
+ },
6716
+ buffered: () => [...buffer]
6717
+ };
5066
6718
  }
5067
6719
 
5068
6720
  // src/defaults/context-manager.ts
@@ -5216,18 +6868,370 @@ function createContextManagerTool(opts = {}) {
5216
6868
  summary: summaryText
5217
6869
  };
5218
6870
  }
5219
- default:
5220
- return {
5221
- action: input.action,
5222
- beforeTokens,
5223
- messageCount: messages.length,
5224
- notes: `Unknown action: ${input.action}`
5225
- };
6871
+ default:
6872
+ return {
6873
+ action: input.action,
6874
+ beforeTokens,
6875
+ messageCount: messages.length,
6876
+ notes: `Unknown action: ${input.action}`
6877
+ };
6878
+ }
6879
+ }
6880
+ };
6881
+ }
6882
+ var contextManagerTool = createContextManagerTool();
6883
+
6884
+ // src/defaults/mcp-servers.ts
6885
+ var filesystemServer = () => ({
6886
+ name: "filesystem",
6887
+ description: "Read, write, and navigate the local filesystem (read-heavy tools)",
6888
+ transport: "stdio",
6889
+ command: "npx",
6890
+ args: ["-y", "@modelcontextprotocol/server-filesystem", "."],
6891
+ permission: "confirm"
6892
+ });
6893
+ var githubServer = () => ({
6894
+ name: "github",
6895
+ description: "GitHub API \u2014 issues, PRs, repos, search, file ops (requires GITHUB_PERSONAL_ACCESS_TOKEN)",
6896
+ transport: "stdio",
6897
+ command: "npx",
6898
+ args: ["-y", "@modelcontextprotocol/server-github"],
6899
+ env: { GITHUB_PERSONAL_ACCESS_TOKEN: process.env.GITHUB_PERSONAL_ACCESS_TOKEN ?? "" },
6900
+ permission: "confirm"
6901
+ });
6902
+ var context7Server = () => ({
6903
+ name: "context7",
6904
+ description: "Codebase-aware documentation and Q&A (context7.ai)",
6905
+ transport: "streamable-http",
6906
+ url: "https://server.context7.ai/mcp",
6907
+ permission: "confirm"
6908
+ });
6909
+ var braveSearchServer = () => ({
6910
+ name: "brave-search",
6911
+ description: "Web search (Brave). Requires BRAVE_SEARCH_API_KEY \u2014 free tier 2k queries/month",
6912
+ transport: "stdio",
6913
+ command: "npx",
6914
+ args: ["-y", "@modelcontextprotocol/server-brave-search"],
6915
+ env: { BRAVE_SEARCH_API_KEY: process.env.BRAVE_SEARCH_API_KEY ?? "" },
6916
+ permission: "confirm"
6917
+ });
6918
+ var blockServer = () => ({
6919
+ name: "block",
6920
+ description: "Postgres database access via SQL (Block MCP server)",
6921
+ transport: "stdio",
6922
+ command: "npx",
6923
+ args: ["-y", "@modelcontextprotocol/server-block"],
6924
+ permission: "confirm"
6925
+ });
6926
+ var everArtServer = () => ({
6927
+ name: "everart",
6928
+ description: "AI image generation (EverArt). Requires EVERART_API_KEY",
6929
+ transport: "stdio",
6930
+ command: "npx",
6931
+ args: ["-y", "@modelcontextprotocol/server-everart"],
6932
+ env: { EVERART_API_KEY: process.env.EVERART_API_KEY ?? "" },
6933
+ permission: "confirm"
6934
+ });
6935
+ var slackServer = () => ({
6936
+ name: "slack",
6937
+ description: "Slack \u2014 messaging, channels, search. Requires SLACK_BOT_TOKEN + SLACK_TEAM_ID",
6938
+ transport: "stdio",
6939
+ command: "npx",
6940
+ args: ["-y", "@modelcontextprotocol/server-slack"],
6941
+ env: {
6942
+ SLACK_BOT_TOKEN: process.env.SLACK_BOT_TOKEN ?? "",
6943
+ SLACK_TEAM_ID: process.env.SLACK_TEAM_ID ?? ""
6944
+ },
6945
+ permission: "confirm"
6946
+ });
6947
+ var awsServer = () => ({
6948
+ name: "aws",
6949
+ description: "AWS \u2014 EC2, S3, Lambda, IAM, CloudFormation, costs. Requires AWS credentials",
6950
+ transport: "stdio",
6951
+ command: "npx",
6952
+ args: ["-y", "@modelcontextprotocol/server-aws"],
6953
+ permission: "confirm"
6954
+ });
6955
+ var googleMapsServer = () => ({
6956
+ name: "google-maps",
6957
+ description: "Google Maps \u2014 directions, geocoding, places. Requires GOOGLE_MAPS_API_KEY",
6958
+ transport: "stdio",
6959
+ command: "npx",
6960
+ args: ["-y", "@modelcontextprotocol/server-google-maps"],
6961
+ env: { GOOGLE_MAPS_API_KEY: process.env.GOOGLE_MAPS_API_KEY ?? "" },
6962
+ permission: "confirm"
6963
+ });
6964
+ var sentinelServer = () => ({
6965
+ name: "sentinel",
6966
+ description: "Security vulnerability scanning (Sentinel)",
6967
+ transport: "streamable-http",
6968
+ url: "https://mcp.sentinel.ai",
6969
+ permission: "deny"
6970
+ // security tool — require explicit confirmation
6971
+ });
6972
+ var allServers = () => ({
6973
+ filesystem: { ...filesystemServer(), enabled: false },
6974
+ github: { ...githubServer(), enabled: false },
6975
+ context7: { ...context7Server(), enabled: false },
6976
+ "brave-search": { ...braveSearchServer(), enabled: false },
6977
+ block: { ...blockServer(), enabled: false },
6978
+ everart: { ...everArtServer(), enabled: false },
6979
+ slack: { ...slackServer(), enabled: false },
6980
+ aws: { ...awsServer(), enabled: false },
6981
+ "google-maps": { ...googleMapsServer(), enabled: false },
6982
+ sentinel: { ...sentinelServer(), enabled: false }
6983
+ });
6984
+
6985
+ // src/core/streaming-response-builder.ts
6986
+ function buildResponse(state) {
6987
+ const content = [];
6988
+ for (const b of state.blockOrder) {
6989
+ if (b.kind === "text") {
6990
+ const txt = state.textBuffers[b.idx] ?? "";
6991
+ if (txt) content.push({ type: "text", text: txt });
6992
+ } else {
6993
+ const tb = state.tools.get(b.id);
6994
+ if (tb) {
6995
+ const block = {
6996
+ type: "tool_use",
6997
+ id: b.id,
6998
+ name: tb.name,
6999
+ input: tb.input ?? {}
7000
+ };
7001
+ if (tb.providerMeta && Object.keys(tb.providerMeta).length > 0) {
7002
+ block.providerMeta = tb.providerMeta;
7003
+ }
7004
+ content.push(block);
7005
+ }
7006
+ }
7007
+ }
7008
+ if (content.length === 0) content.push({ type: "text", text: "" });
7009
+ return { content, stopReason: state.stopReason, usage: state.usage, model: state.model };
7010
+ }
7011
+ function createStreamingState(model) {
7012
+ return {
7013
+ model,
7014
+ stopReason: "end_turn",
7015
+ usage: { input: 0, output: 0 },
7016
+ textBuffers: [],
7017
+ currentTextIndex: -1,
7018
+ tools: /* @__PURE__ */ new Map(),
7019
+ blockOrder: []
7020
+ };
7021
+ }
7022
+ function handleMessageStart(state, model) {
7023
+ state.model = model;
7024
+ }
7025
+ function handleContentBlockStart(state, ev) {
7026
+ const kind = ev.kind ?? "text";
7027
+ if (kind === "text") {
7028
+ state.currentTextIndex = state.textBuffers.length;
7029
+ state.textBuffers.push("");
7030
+ state.blockOrder.push({ kind: "text", idx: state.currentTextIndex });
7031
+ } else if (kind === "tool_use") {
7032
+ const id = ev.id ?? crypto.randomUUID();
7033
+ state.tools.set(id, { name: ev.name ?? "unknown", partial: "" });
7034
+ state.blockOrder.push({ kind: "tool", id });
7035
+ state.currentTextIndex = -1;
7036
+ }
7037
+ }
7038
+ function handleContentBlockStop(state, ev) {
7039
+ }
7040
+ function handleTextDelta(state, text) {
7041
+ if (state.currentTextIndex === -1) {
7042
+ state.currentTextIndex = state.textBuffers.length;
7043
+ state.textBuffers.push("");
7044
+ state.blockOrder.push({ kind: "text", idx: state.currentTextIndex });
7045
+ }
7046
+ state.textBuffers[state.currentTextIndex] = (state.textBuffers[state.currentTextIndex] ?? "") + text;
7047
+ }
7048
+ function handleToolUseStart(state, ev) {
7049
+ state.currentTextIndex = -1;
7050
+ state.tools.set(ev.id, { name: ev.name, partial: "" });
7051
+ state.blockOrder.push({ kind: "tool", id: ev.id });
7052
+ }
7053
+ function handleToolUseInputDelta(state, ev) {
7054
+ const t2 = state.tools.get(ev.id);
7055
+ if (t2) t2.partial += ev.partial;
7056
+ }
7057
+ function safeJsonOrRaw(s) {
7058
+ if (!s) return {};
7059
+ try {
7060
+ return JSON.parse(s);
7061
+ } catch {
7062
+ return { _raw: s };
7063
+ }
7064
+ }
7065
+ function handleToolUseStop(state, ev) {
7066
+ const t2 = state.tools.get(ev.id);
7067
+ if (t2) {
7068
+ t2.input = ev.input !== void 0 ? ev.input : safeJsonOrRaw(t2.partial);
7069
+ if (ev.providerMeta) t2.providerMeta = ev.providerMeta;
7070
+ }
7071
+ state.currentTextIndex = -1;
7072
+ }
7073
+ function handleMessageStop(state, ev) {
7074
+ state.stopReason = ev.stopReason ?? "end_turn";
7075
+ state.usage = ev.usage ?? { input: 0, output: 0 };
7076
+ }
7077
+ async function streamProviderToResponse(provider, req, signal, ctx, events) {
7078
+ const state = createStreamingState(req.model);
7079
+ const iter = provider.stream(req, { signal })[Symbol.asyncIterator]();
7080
+ try {
7081
+ for (; ; ) {
7082
+ const next = await iter.next();
7083
+ if (next.done) break;
7084
+ const ev = next.value;
7085
+ switch (ev.type) {
7086
+ case "message_start":
7087
+ handleMessageStart(state, ev.model);
7088
+ break;
7089
+ case "content_block_start":
7090
+ handleContentBlockStart(state, ev);
7091
+ break;
7092
+ case "content_block_stop":
7093
+ handleContentBlockStop(state, ev);
7094
+ break;
7095
+ case "text_delta":
7096
+ handleTextDelta(state, ev.text);
7097
+ events.emit("provider.text_delta", { ctx, text: ev.text });
7098
+ break;
7099
+ case "tool_use_start":
7100
+ handleToolUseStart(state, ev);
7101
+ events.emit("provider.tool_use_start", { ctx, id: ev.id, name: ev.name });
7102
+ break;
7103
+ case "tool_use_input_delta":
7104
+ handleToolUseInputDelta(state, ev);
7105
+ break;
7106
+ case "tool_use_stop":
7107
+ handleToolUseStop(state, ev);
7108
+ events.emit("provider.tool_use_stop", { ctx, id: ev.id });
7109
+ break;
7110
+ case "message_stop":
7111
+ handleMessageStop(state, ev);
7112
+ break;
7113
+ }
7114
+ }
7115
+ } catch (err) {
7116
+ if (signal.aborted) {
7117
+ state.stopReason = "max_tokens";
7118
+ return buildResponse(state);
7119
+ }
7120
+ throw err;
7121
+ } finally {
7122
+ try {
7123
+ await iter.return?.();
7124
+ } catch {
7125
+ }
7126
+ }
7127
+ return buildResponse(state);
7128
+ }
7129
+
7130
+ // src/core/provider-runner.ts
7131
+ async function runProviderWithRetry(opts) {
7132
+ const { provider, request, signal, ctx, events, retry, logger, tracer } = opts;
7133
+ let attempt = 0;
7134
+ for (; ; ) {
7135
+ const span = tracer?.startSpan("provider.complete", {
7136
+ "provider.id": provider.id,
7137
+ "provider.model": request.model,
7138
+ "provider.streaming": provider.capabilities.streaming,
7139
+ "provider.attempt": attempt
7140
+ });
7141
+ try {
7142
+ const res = provider.capabilities.streaming ? await streamProviderToResponse(provider, request, signal, ctx, events) : await provider.complete(request, { signal });
7143
+ span?.setAttribute("provider.stopReason", res.stopReason);
7144
+ span?.setAttribute("provider.usage_in", res.usage.input);
7145
+ span?.setAttribute("provider.usage_out", res.usage.output);
7146
+ span?.end();
7147
+ return res;
7148
+ } catch (err) {
7149
+ if (err instanceof Error) span?.recordError(err);
7150
+ span?.end();
7151
+ if (signal.aborted) throw err;
7152
+ const isProviderErr = err instanceof ProviderError;
7153
+ const errAsErr = err instanceof Error ? err : new Error(String(err));
7154
+ const canRetry = retry.shouldRetry(isProviderErr ? err : errAsErr, attempt);
7155
+ const description = isProviderErr ? err.describe() : errAsErr.message;
7156
+ if (!canRetry) {
7157
+ if (isProviderErr) {
7158
+ events.emit("provider.error", {
7159
+ providerId: err.providerId,
7160
+ status: err.status,
7161
+ description,
7162
+ retryable: false
7163
+ });
7164
+ }
7165
+ throw err;
7166
+ }
7167
+ const delay = Math.round(retry.delayMs(attempt));
7168
+ const attemptNum = attempt + 1;
7169
+ logger.warn(`Provider retry ${attemptNum} in ${delay}ms \u2014 ${description}`);
7170
+ if (isProviderErr) {
7171
+ events.emit("provider.retry", {
7172
+ providerId: err.providerId,
7173
+ attempt: attemptNum,
7174
+ delayMs: delay,
7175
+ status: err.status,
7176
+ description
7177
+ });
5226
7178
  }
7179
+ await new Promise((resolve4, reject) => {
7180
+ const t2 = setTimeout(resolve4, delay);
7181
+ const onAbort = () => {
7182
+ clearTimeout(t2);
7183
+ reject(new Error("aborted"));
7184
+ };
7185
+ if (signal.aborted) onAbort();
7186
+ signal.addEventListener("abort", onAbort, { once: true });
7187
+ });
7188
+ attempt++;
5227
7189
  }
5228
- };
7190
+ }
7191
+ }
7192
+
7193
+ // src/core/iteration-limit.ts
7194
+ function requestLimitExtension(opts) {
7195
+ const { events, currentIterations, currentLimit, autoExtend, timeoutMs = 3e4 } = opts;
7196
+ return new Promise((resolve4) => {
7197
+ let resolved = false;
7198
+ const timer = setTimeout(() => {
7199
+ if (!resolved) {
7200
+ resolved = true;
7201
+ resolve4(0);
7202
+ }
7203
+ }, timeoutMs);
7204
+ const deny = () => {
7205
+ if (!resolved) {
7206
+ resolved = true;
7207
+ clearTimeout(timer);
7208
+ resolve4(0);
7209
+ }
7210
+ };
7211
+ const grant = (extra) => {
7212
+ if (!resolved) {
7213
+ resolved = true;
7214
+ clearTimeout(timer);
7215
+ resolve4(Math.max(0, extra));
7216
+ }
7217
+ };
7218
+ events.emit("iteration.limit_reached", {
7219
+ currentIterations,
7220
+ currentLimit,
7221
+ grant,
7222
+ deny
7223
+ });
7224
+ if (autoExtend) {
7225
+ setImmediate(() => {
7226
+ if (!resolved) {
7227
+ resolved = true;
7228
+ clearTimeout(timer);
7229
+ resolve4(100);
7230
+ }
7231
+ });
7232
+ }
7233
+ });
5229
7234
  }
5230
- var contextManagerTool = createContextManagerTool();
5231
7235
 
5232
7236
  // src/core/agent.ts
5233
7237
  var DEFAULT_MAX_ITERATIONS = 100;
@@ -5262,6 +7266,7 @@ var Agent = class {
5262
7266
  plugins = [];
5263
7267
  toolExecutor;
5264
7268
  autoExtendLimit;
7269
+ tracer;
5265
7270
  constructor(init) {
5266
7271
  this.container = init.container;
5267
7272
  this.tools = init.tools;
@@ -5274,13 +7279,16 @@ var Agent = class {
5274
7279
  this.executionStrategy = init.executionStrategy ?? "smart";
5275
7280
  this.perIterationOutputCapBytes = init.perIterationOutputCapBytes ?? 1e5;
5276
7281
  this.autoExtendLimit = init.autoExtendLimit ?? true;
7282
+ this.tracer = init.tracer;
5277
7283
  this.toolExecutor = new ToolExecutor(this.tools, {
5278
7284
  permissionPolicy: this.permission,
5279
7285
  secretScrubber: this.scrubber,
5280
7286
  renderer: this.renderer,
5281
7287
  events: this.events,
7288
+ confirmAwaiter: init.confirmAwaiter,
5282
7289
  iterationTimeoutMs: this.iterationTimeoutMs,
5283
- perIterationOutputCapBytes: this.perIterationOutputCapBytes
7290
+ perIterationOutputCapBytes: this.perIterationOutputCapBytes,
7291
+ tracer: this.tracer
5284
7292
  });
5285
7293
  }
5286
7294
  get logger() {
@@ -5316,334 +7324,190 @@ var Agent = class {
5316
7324
  const signal = controller.signal;
5317
7325
  this.ctx.signal = signal;
5318
7326
  controller.onAbort(() => this.ctx.drainAbortHooks());
7327
+ const span = this.tracer?.startSpan("agent.run", {
7328
+ "agent.model": opts.model ?? this.ctx.model,
7329
+ "agent.executionStrategy": opts.executionStrategy ?? this.executionStrategy
7330
+ });
5319
7331
  try {
5320
- return await this.runInner(userInput, opts, controller);
7332
+ const result = await this.runInner(userInput, opts, controller);
7333
+ span?.setAttribute("agent.status", result.status);
7334
+ span?.setAttribute("agent.iterations", result.iterations);
7335
+ return result;
7336
+ } catch (err) {
7337
+ const wse = err instanceof AgentError ? err : toWrongStackError(err);
7338
+ this.events.emit("error", { err: toError(err), phase: "agent" });
7339
+ if (err instanceof Error) span?.recordError(err);
7340
+ span?.setAttribute("agent.status", "failed");
7341
+ return {
7342
+ status: signal.aborted ? "aborted" : "failed",
7343
+ iterations: 0,
7344
+ error: wse
7345
+ };
5321
7346
  } finally {
7347
+ span?.end();
5322
7348
  await controller.dispose();
5323
7349
  }
5324
7350
  }
5325
7351
  async runInner(userInput, opts, controller) {
5326
- const signal = controller.signal;
5327
- const { blocks, text } = normalizeInput(userInput);
5328
- await this.pipelines.userInput.run({ content: blocks, text, ctx: this.ctx });
5329
- this.ctx.messages.push({ role: "user", content: blocks });
5330
- await this.ctx.session.append({
5331
- type: "user_input",
5332
- ts: (/* @__PURE__ */ new Date()).toISOString(),
5333
- content: blocks
5334
- });
7352
+ await this.normalizeAndEmitUserInput(userInput);
5335
7353
  let finalText = "";
5336
7354
  let iterations = 0;
5337
- let maxIter = opts.maxIterations ?? this.maxIterations;
5338
- const hasHardLimit = maxIter > 0 && Number.isFinite(maxIter);
7355
+ let effectiveLimit = opts.maxIterations ?? this.maxIterations;
7356
+ const hasHardLimit = effectiveLimit > 0 && Number.isFinite(effectiveLimit);
5339
7357
  for (let i = 0; ; i++) {
5340
7358
  iterations = i + 1;
5341
- if (signal.aborted) {
7359
+ if (controller.signal.aborted) {
5342
7360
  return { status: "aborted", iterations };
5343
7361
  }
5344
- if (hasHardLimit && i >= maxIter) {
5345
- const extendBy = await this.requestLimitExtension(iterations);
5346
- if (extendBy > 0) {
5347
- maxIter += extendBy;
5348
- this.logger.info(`Iteration limit extended by ${extendBy} (new limit: ${maxIter})`);
5349
- } else {
5350
- return { status: "max_iterations", iterations, finalText };
5351
- }
7362
+ const limitCheck = await this.checkIterationLimit(
7363
+ i,
7364
+ effectiveLimit,
7365
+ hasHardLimit,
7366
+ iterations
7367
+ );
7368
+ effectiveLimit = limitCheck.limit;
7369
+ if (limitCheck.exit) {
7370
+ return { ...limitCheck.exit, finalText };
5352
7371
  }
5353
7372
  this.events.emit("iteration.started", { ctx: this.ctx, index: i });
5354
- const baseReq = {
5355
- model: opts.model ?? this.ctx.model,
5356
- system: this.ctx.systemPrompt,
5357
- messages: this.ctx.messages,
5358
- tools: this.tools.list(),
5359
- maxTokens: 8192
5360
- };
5361
- const req = await this.pipelines.request.run(baseReq);
7373
+ const req = await this.buildAndRunRequestPipeline(opts);
5362
7374
  let res;
5363
7375
  try {
5364
- res = await this.callProviderWithRetry(this.ctx.provider, req, signal);
7376
+ res = await runProviderWithRetry({
7377
+ provider: this.ctx.provider,
7378
+ request: req,
7379
+ signal: controller.signal,
7380
+ ctx: this.ctx,
7381
+ events: this.events,
7382
+ retry: this.retry,
7383
+ logger: this.logger,
7384
+ tracer: this.tracer
7385
+ });
5365
7386
  } catch (err) {
5366
- if (signal.aborted) {
7387
+ if (controller.signal.aborted) {
5367
7388
  this.events.emit("error", { err: toError(err), phase: "provider" });
5368
- return { status: "aborted", iterations, error: err };
7389
+ return { status: "aborted", iterations, error: toWrongStackError(err, "AGENT_ABORTED") };
5369
7390
  }
5370
7391
  const recovered = await this.errorHandler.recover(err, this.ctx);
5371
7392
  if (!recovered) {
5372
7393
  this.events.emit("error", { err: toError(err), phase: "provider" });
5373
- return { status: "failed", iterations, error: err };
7394
+ return { status: "failed", iterations, error: toWrongStackError(err) };
5374
7395
  }
5375
7396
  res = recovered;
5376
7397
  }
5377
- res = await this.pipelines.response.run(res);
5378
- this.events.emit("provider.response", {
5379
- ctx: this.ctx,
5380
- usage: res.usage,
5381
- stopReason: res.stopReason
5382
- });
5383
- this.ctx.tokenCounter.account(res.usage, req.model);
5384
- this.ctx.messages.push({ role: "assistant", content: res.content });
5385
- await this.ctx.session.append({
5386
- type: "llm_response",
5387
- ts: (/* @__PURE__ */ new Date()).toISOString(),
5388
- content: res.content,
5389
- stopReason: res.stopReason,
5390
- usage: res.usage
5391
- });
5392
- if (signal.aborted) {
5393
- for (const block of res.content) {
5394
- if (isTextBlock(block)) finalText += block.text;
5395
- }
5396
- this.events.emit("iteration.completed", { ctx: this.ctx, index: i });
5397
- return { status: "aborted", iterations, finalText };
7398
+ const responseResult = await this.processResponse(res, req);
7399
+ if (responseResult.aborted) {
7400
+ return { status: "aborted", iterations, finalText: responseResult.finalText };
5398
7401
  }
5399
- const streamed = this.ctx.provider.capabilities.streaming;
5400
- for (const block of res.content) {
5401
- if (isTextBlock(block)) {
5402
- const rendered = await this.pipelines.assistantOutput.run(block);
5403
- finalText += rendered.text;
5404
- if (!streamed) this.renderer?.write(rendered);
5405
- }
7402
+ if (responseResult.done) {
7403
+ return { status: "done", iterations, finalText: responseResult.finalText };
5406
7404
  }
7405
+ finalText = responseResult.finalText;
5407
7406
  const toolUses = res.content.filter(isToolUseBlock);
5408
- if (toolUses.length === 0 || res.stopReason === "end_turn") {
7407
+ if (toolUses.length === 0) {
5409
7408
  this.events.emit("iteration.completed", { ctx: this.ctx, index: i });
5410
7409
  return { status: "done", iterations, finalText };
5411
7410
  }
5412
- const results = await this.executeTools(toolUses, signal);
5413
- this.ctx.messages.push({ role: "user", content: results });
7411
+ await this.executeTools(toolUses);
5414
7412
  this.events.emit("iteration.completed", { ctx: this.ctx, index: i });
5415
- if (this.compactor) {
5416
- await this.pipelines.contextWindow.run(this.ctx);
5417
- }
7413
+ await this.compactContextIfNeeded();
5418
7414
  }
5419
- return { status: "max_iterations", iterations, finalText };
5420
7415
  }
5421
7416
  /**
5422
- * Emit an event asking listeners (CLI/TUI) whether to extend the iteration
5423
- * limit. Returns the number of additional iterations granted. If no listener
5424
- * responds or the user declines, returns 0.
7417
+ * Normalize user input and emit through userInput pipeline + session append.
5425
7418
  */
5426
- async requestLimitExtension(currentIterations) {
5427
- return new Promise((resolve4) => {
5428
- let resolved = false;
5429
- const timer = setTimeout(() => {
5430
- if (!resolved) {
5431
- resolved = true;
5432
- resolve4(0);
5433
- }
5434
- }, 3e4);
5435
- const wrappedDeny = () => {
5436
- if (!resolved) {
5437
- resolved = true;
5438
- clearTimeout(timer);
5439
- resolve4(0);
5440
- }
5441
- };
5442
- const wrappedGrant = (extra) => {
5443
- if (!resolved) {
5444
- resolved = true;
5445
- clearTimeout(timer);
5446
- resolve4(Math.max(0, extra));
5447
- }
5448
- };
5449
- if (this.autoExtendLimit) {
5450
- this.events.emit("iteration.limit_reached", {
5451
- currentIterations,
5452
- currentLimit: this.maxIterations,
5453
- grant: wrappedGrant,
5454
- deny: wrappedDeny
5455
- });
5456
- setImmediate(() => {
5457
- if (!resolved) {
5458
- resolved = true;
5459
- clearTimeout(timer);
5460
- resolve4(100);
5461
- }
5462
- });
5463
- } else {
5464
- this.events.emit("iteration.limit_reached", {
5465
- currentIterations,
5466
- currentLimit: this.maxIterations,
5467
- grant: wrappedGrant,
5468
- deny: wrappedDeny
5469
- });
5470
- }
7419
+ async normalizeAndEmitUserInput(userInput) {
7420
+ const { blocks, text } = normalizeInput(userInput);
7421
+ await this.pipelines.userInput.run({ content: blocks, text, ctx: this.ctx });
7422
+ this.ctx.state.appendMessage({ role: "user", content: blocks });
7423
+ await this.ctx.session.append({
7424
+ type: "user_input",
7425
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
7426
+ content: blocks
5471
7427
  });
5472
7428
  }
5473
7429
  /**
5474
- * Consume a Provider.stream() into a Response, emitting text_delta and
5475
- * tool_use lifecycle events to the EventBus as they arrive. This is the
5476
- * canonical path when the provider declares `capabilities.streaming`;
5477
- * complete() is only used as a fallback for legacy providers.
7430
+ * Check if iteration limit has been reached and request extension if needed.
7431
+ * Returns the new effective limit (possibly extended) and a RunResult if
7432
+ * the loop should exit. Returns `{ limit }` with no result when the
7433
+ * iteration may proceed.
5478
7434
  */
5479
- async streamProviderToResponse(provider, req, signal) {
5480
- let model = req.model;
5481
- let stopReason = "end_turn";
5482
- let usage = { input: 0, output: 0 };
5483
- const textBuffers = [];
5484
- let currentTextIndex = -1;
5485
- const tools = /* @__PURE__ */ new Map();
5486
- const blockOrder = [];
5487
- const openContentBlocks = /* @__PURE__ */ new Map();
5488
- const buildResponse = () => {
5489
- const content = [];
5490
- for (const b of blockOrder) {
5491
- if (b.kind === "text") {
5492
- const txt = textBuffers[b.idx] ?? "";
5493
- if (txt) content.push({ type: "text", text: txt });
5494
- } else {
5495
- const tb = tools.get(b.id);
5496
- if (tb) {
5497
- content.push({
5498
- type: "tool_use",
5499
- id: b.id,
5500
- name: tb.name,
5501
- input: tb.input ?? {}
5502
- });
5503
- }
5504
- }
7435
+ async checkIterationLimit(iterationIndex, limit, hasHardLimit, currentIterations) {
7436
+ if (hasHardLimit && iterationIndex >= limit) {
7437
+ const extendBy = await requestLimitExtension({
7438
+ events: this.events,
7439
+ currentIterations,
7440
+ currentLimit: this.maxIterations,
7441
+ autoExtend: this.autoExtendLimit
7442
+ });
7443
+ if (extendBy > 0) {
7444
+ const newLimit = limit + extendBy;
7445
+ this.logger.info(`Iteration limit extended by ${extendBy} (new limit: ${newLimit})`);
7446
+ return { limit: newLimit };
5505
7447
  }
5506
- if (content.length === 0) content.push({ type: "text", text: "" });
5507
- return { content, stopReason, usage, model };
7448
+ return { limit, exit: { status: "max_iterations", iterations: currentIterations } };
7449
+ }
7450
+ return { limit };
7451
+ }
7452
+ /**
7453
+ * Build request and run through request pipeline.
7454
+ */
7455
+ async buildAndRunRequestPipeline(opts) {
7456
+ const baseReq = {
7457
+ model: opts.model ?? this.ctx.model,
7458
+ system: this.ctx.systemPrompt,
7459
+ messages: this.ctx.messages,
7460
+ tools: this.tools.list(),
7461
+ maxTokens: 8192
5508
7462
  };
5509
- const iter = provider.stream(req, { signal })[Symbol.asyncIterator]();
5510
- try {
5511
- for (; ; ) {
5512
- const next = await iter.next();
5513
- if (next.done) break;
5514
- const ev = next.value;
5515
- switch (ev.type) {
5516
- case "message_start":
5517
- model = ev.model;
5518
- break;
5519
- case "content_block_start": {
5520
- const kind = ev.kind ?? "text";
5521
- if (kind === "text") {
5522
- currentTextIndex = textBuffers.length;
5523
- textBuffers.push("");
5524
- blockOrder.push({ kind: "text", idx: currentTextIndex });
5525
- openContentBlocks.set(String(currentTextIndex), "text");
5526
- } else if (kind === "tool_use") {
5527
- const id = ev.id ?? crypto.randomUUID();
5528
- tools.set(id, { name: ev.name ?? "unknown", partial: "" });
5529
- blockOrder.push({ kind: "tool", id });
5530
- openContentBlocks.set(String(blockOrder.length - 1), "tool");
5531
- }
5532
- break;
5533
- }
5534
- case "content_block_stop": {
5535
- const blockIndex = ev.index;
5536
- if (blockIndex !== void 0) {
5537
- openContentBlocks.delete(String(blockIndex));
5538
- }
5539
- break;
5540
- }
5541
- case "text_delta":
5542
- if (currentTextIndex === -1) {
5543
- currentTextIndex = textBuffers.length;
5544
- textBuffers.push("");
5545
- blockOrder.push({ kind: "text", idx: currentTextIndex });
5546
- }
5547
- textBuffers[currentTextIndex] = (textBuffers[currentTextIndex] ?? "") + ev.text;
5548
- this.events.emit("provider.text_delta", { ctx: this.ctx, text: ev.text });
5549
- break;
5550
- case "tool_use_start":
5551
- currentTextIndex = -1;
5552
- tools.set(ev.id, { name: ev.name, partial: "" });
5553
- blockOrder.push({ kind: "tool", id: ev.id });
5554
- this.events.emit("provider.tool_use_start", {
5555
- ctx: this.ctx,
5556
- id: ev.id,
5557
- name: ev.name
5558
- });
5559
- break;
5560
- case "tool_use_input_delta": {
5561
- const t2 = tools.get(ev.id);
5562
- if (t2) t2.partial += ev.partial;
5563
- break;
5564
- }
5565
- case "tool_use_stop": {
5566
- const t2 = tools.get(ev.id);
5567
- if (t2) {
5568
- t2.input = ev.input !== void 0 ? ev.input : safeJsonOrRaw(t2.partial);
5569
- }
5570
- currentTextIndex = -1;
5571
- this.events.emit("provider.tool_use_stop", { ctx: this.ctx, id: ev.id });
5572
- break;
5573
- }
5574
- case "message_stop":
5575
- stopReason = ev.stopReason;
5576
- usage = ev.usage;
5577
- break;
5578
- }
5579
- }
5580
- } catch (err) {
5581
- if (signal.aborted) {
5582
- stopReason = "max_tokens";
5583
- return buildResponse();
5584
- }
5585
- throw err;
5586
- } finally {
5587
- try {
5588
- await iter.return?.();
5589
- } catch {
7463
+ return this.pipelines.request.run(baseReq);
7464
+ }
7465
+ /**
7466
+ * Process the provider response: run response pipeline, emit events,
7467
+ * update session, render text, handle abort.
7468
+ */
7469
+ async processResponse(res, req) {
7470
+ await this.pipelines.response.run(res);
7471
+ this.events.emit("provider.response", {
7472
+ ctx: this.ctx,
7473
+ usage: res.usage,
7474
+ stopReason: res.stopReason
7475
+ });
7476
+ this.ctx.tokenCounter.account(res.usage, req.model);
7477
+ this.ctx.state.appendMessage({ role: "assistant", content: res.content });
7478
+ await this.ctx.session.append({
7479
+ type: "llm_response",
7480
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
7481
+ content: res.content,
7482
+ stopReason: res.stopReason,
7483
+ usage: res.usage
7484
+ });
7485
+ if (this.ctx.signal.aborted) {
7486
+ let finalText2 = "";
7487
+ for (const block of res.content) {
7488
+ if (isTextBlock(block)) finalText2 += block.text;
5590
7489
  }
7490
+ return { finalText: finalText2, aborted: true, done: false };
5591
7491
  }
5592
- return buildResponse();
5593
- }
5594
- async callProviderWithRetry(provider, req, signal) {
5595
- let attempt = 0;
5596
- for (; ; ) {
5597
- try {
5598
- if (provider.capabilities.streaming) {
5599
- return await this.streamProviderToResponse(provider, req, signal);
5600
- }
5601
- return await provider.complete(req, { signal });
5602
- } catch (err) {
5603
- if (signal.aborted) throw err;
5604
- const isProviderErr = err instanceof ProviderError;
5605
- const errAsErr = err instanceof Error ? err : new Error(String(err));
5606
- const canRetry = this.retry.shouldRetry(isProviderErr ? err : errAsErr, attempt);
5607
- const description = isProviderErr ? err.describe() : errAsErr.message;
5608
- if (!canRetry) {
5609
- if (isProviderErr) {
5610
- this.events.emit("provider.error", {
5611
- providerId: err.providerId,
5612
- status: err.status,
5613
- description,
5614
- retryable: false
5615
- });
5616
- }
5617
- throw err;
5618
- }
5619
- const delay = Math.round(this.retry.delayMs(attempt));
5620
- const attemptNum = attempt + 1;
5621
- this.logger.warn(
5622
- `Provider retry ${attemptNum} in ${delay}ms \u2014 ${description}`
5623
- );
5624
- if (isProviderErr) {
5625
- this.events.emit("provider.retry", {
5626
- providerId: err.providerId,
5627
- attempt: attemptNum,
5628
- delayMs: delay,
5629
- status: err.status,
5630
- description
5631
- });
5632
- }
5633
- await new Promise((resolve4, reject) => {
5634
- const t2 = setTimeout(resolve4, delay);
5635
- const onAbort = () => {
5636
- clearTimeout(t2);
5637
- reject(new Error("aborted"));
5638
- };
5639
- if (signal.aborted) onAbort();
5640
- signal.addEventListener("abort", onAbort, { once: true });
5641
- });
5642
- attempt++;
7492
+ let finalText = "";
7493
+ const streamed = this.ctx.provider.capabilities.streaming;
7494
+ for (const block of res.content) {
7495
+ if (isTextBlock(block)) {
7496
+ const rendered = await this.pipelines.assistantOutput.run(block);
7497
+ finalText += rendered.text;
7498
+ if (!streamed) this.renderer?.write(rendered);
5643
7499
  }
5644
7500
  }
7501
+ return { finalText, aborted: false, done: false };
5645
7502
  }
5646
- async executeTools(toolUses, signal) {
7503
+ /**
7504
+ * Execute tools and append tool results to context.
7505
+ * When a tool returns `tool_confirm_pending` (no confirmAwaiter set),
7506
+ * we pause and emit `tool.confirm_needed`. The run is blocked until
7507
+ * the event listener resolves the confirmation, then we re-run the
7508
+ * single tool.
7509
+ */
7510
+ async executeTools(toolUses) {
5647
7511
  const { outputs } = await this.toolExecutor.executeBatch(
5648
7512
  toolUses,
5649
7513
  this.ctx,
@@ -5651,6 +7515,39 @@ var Agent = class {
5651
7515
  );
5652
7516
  const useById = new Map(toolUses.map((u) => [u.id, u]));
5653
7517
  for (const { result, tool, durationMs } of outputs) {
7518
+ if (result.type === "tool_confirm_pending") {
7519
+ const decision = await this.waitForConfirm({
7520
+ tool,
7521
+ input: result.input,
7522
+ toolUseId: result.toolUseId,
7523
+ suggestedPattern: result.suggestedPattern
7524
+ });
7525
+ const reRunResult = await this.executeSingleWithDecision(
7526
+ tool,
7527
+ { id: result.toolUseId, name: tool.name, input: result.input },
7528
+ decision
7529
+ );
7530
+ const use2 = useById.get(reRunResult.result.tool_use_id);
7531
+ if (use2) {
7532
+ await this.pipelines.toolCall.run({ toolUse: use2, result: reRunResult.result, ctx: this.ctx, tool });
7533
+ await this.ctx.session.append({
7534
+ type: "tool_result",
7535
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
7536
+ id: reRunResult.result.tool_use_id,
7537
+ content: reRunResult.result.content,
7538
+ isError: !!reRunResult.result.is_error
7539
+ });
7540
+ this.events.emit("tool.executed", {
7541
+ name: tool.name,
7542
+ durationMs: reRunResult.durationMs,
7543
+ ok: !reRunResult.result.is_error,
7544
+ input: result.input,
7545
+ output: truncateForEvent(reRunResult.result.content)
7546
+ });
7547
+ }
7548
+ this.ctx.state.appendMessage({ role: "user", content: [reRunResult.result] });
7549
+ continue;
7550
+ }
5654
7551
  const use = useById.get(result.tool_use_id);
5655
7552
  if (!use) continue;
5656
7553
  await this.pipelines.toolCall.run({
@@ -5674,25 +7571,132 @@ var Agent = class {
5674
7571
  output: truncateForEvent(result.content)
5675
7572
  });
5676
7573
  }
5677
- return outputs.map((o) => o.result);
7574
+ this.ctx.state.appendMessage({ role: "user", content: outputs.map((o) => o.result) });
7575
+ }
7576
+ waitForConfirm(info) {
7577
+ return new Promise((resolve4) => {
7578
+ this.events.emit("tool.confirm_needed", {
7579
+ tool: info.tool,
7580
+ input: info.input,
7581
+ toolUseId: info.toolUseId,
7582
+ suggestedPattern: info.suggestedPattern,
7583
+ resolve: resolve4
7584
+ });
7585
+ });
7586
+ }
7587
+ async executeSingleWithDecision(tool, use, decision) {
7588
+ const start = Date.now();
7589
+ if (decision === "no" || decision === "deny") {
7590
+ return {
7591
+ result: { type: "tool_result", tool_use_id: use.id, content: `Tool "${tool.name}" denied by user.`, is_error: true },
7592
+ durationMs: Date.now() - start
7593
+ };
7594
+ }
7595
+ try {
7596
+ const result = await this.toolExecutor.executeTool(tool, use, this.ctx, this.perIterationOutputCapBytes);
7597
+ return { result, durationMs: Date.now() - start };
7598
+ } catch (err) {
7599
+ const msg = err instanceof Error ? err.message : String(err);
7600
+ return {
7601
+ result: { type: "tool_result", tool_use_id: use.id, content: `Tool "${tool.name}" threw: ${msg}`, is_error: true },
7602
+ durationMs: Date.now() - start
7603
+ };
7604
+ }
7605
+ }
7606
+ /**
7607
+ * Run context window pipeline if compactor is present.
7608
+ */
7609
+ async compactContextIfNeeded() {
7610
+ if (this.compactor) {
7611
+ await this.pipelines.contextWindow.run(this.ctx);
7612
+ }
5678
7613
  }
5679
7614
  };
5680
7615
  function toError(err) {
5681
7616
  return err instanceof Error ? err : new Error(String(err));
5682
7617
  }
5683
- function safeJsonOrRaw(s) {
5684
- if (!s) return {};
5685
- try {
5686
- return JSON.parse(s);
5687
- } catch {
5688
- return { _raw: s };
5689
- }
5690
- }
5691
7618
  function truncateForEvent(content, max = 400) {
5692
7619
  if (!content) return "";
5693
7620
  return content.length <= max ? content : `${content.slice(0, max - 1)}\u2026`;
5694
7621
  }
5695
7622
 
7623
+ // src/core/conversation-state.ts
7624
+ var ConversationState = class {
7625
+ ctx;
7626
+ listeners = /* @__PURE__ */ new Set();
7627
+ constructor(ctx) {
7628
+ this.ctx = ctx;
7629
+ }
7630
+ // ─── Read API ───────────────────────────────────────────────────────
7631
+ get messages() {
7632
+ return this.ctx.messages;
7633
+ }
7634
+ get todos() {
7635
+ return this.ctx.todos;
7636
+ }
7637
+ get meta() {
7638
+ return this.ctx.meta;
7639
+ }
7640
+ /**
7641
+ * Cheap immutable snapshot. Useful for tests and for compaction passes
7642
+ * that need a stable view across an async boundary.
7643
+ */
7644
+ snapshot() {
7645
+ return {
7646
+ messages: [...this.ctx.messages],
7647
+ todos: [...this.ctx.todos],
7648
+ meta: { ...this.ctx.meta }
7649
+ };
7650
+ }
7651
+ // ─── Write API (preferred — fires onChange) ─────────────────────────
7652
+ appendMessage(message) {
7653
+ this.ctx.messages.push(message);
7654
+ this.emit({ kind: "message_appended", message });
7655
+ }
7656
+ replaceMessages(messages) {
7657
+ this.ctx.messages.length = 0;
7658
+ this.ctx.messages.push(...messages);
7659
+ this.emit({ kind: "messages_replaced", messages: [...messages] });
7660
+ }
7661
+ replaceTodos(todos) {
7662
+ this.ctx.todos.length = 0;
7663
+ this.ctx.todos.push(...todos);
7664
+ this.emit({ kind: "todos_replaced", todos: [...todos] });
7665
+ }
7666
+ setMeta(key, value) {
7667
+ this.ctx.meta[key] = value;
7668
+ this.emit({ kind: "meta_set", key, value });
7669
+ }
7670
+ deleteMeta(key) {
7671
+ if (!(key in this.ctx.meta)) return;
7672
+ delete this.ctx.meta[key];
7673
+ this.emit({ kind: "meta_deleted", key });
7674
+ }
7675
+ // ─── Subscription ───────────────────────────────────────────────────
7676
+ /**
7677
+ * Subscribe to mutations that go through this wrapper. Note: mutations
7678
+ * that bypass the wrapper (e.g. `ctx.messages.push(...)` directly) are
7679
+ * NOT observed — by design during migration, since we don't want to
7680
+ * monkey-patch arrays. Migrating call sites to use this API is the
7681
+ * dev-plan #1 work.
7682
+ */
7683
+ onChange(listener) {
7684
+ this.listeners.add(listener);
7685
+ return () => this.listeners.delete(listener);
7686
+ }
7687
+ emit(change) {
7688
+ for (const h of this.listeners) {
7689
+ try {
7690
+ h(change, this);
7691
+ } catch {
7692
+ }
7693
+ }
7694
+ }
7695
+ };
7696
+ function wrapAsState(ctx) {
7697
+ return new ConversationState(ctx);
7698
+ }
7699
+
5696
7700
  // src/core/context.ts
5697
7701
  var Context = class {
5698
7702
  messages = [];
@@ -5720,11 +7724,29 @@ var Context = class {
5720
7724
  this.model = init.model;
5721
7725
  this.tools = init.tools ?? [];
5722
7726
  }
7727
+ /**
7728
+ * Observable wrapper over the mutable conversation state. Lazy so
7729
+ * subsystems that don't subscribe pay nothing. Mutations made directly
7730
+ * on `ctx.messages` / `ctx.todos` are still visible through this
7731
+ * wrapper's read API (it holds a reference, not a copy) but only
7732
+ * mutations that go through `state.appendMessage()` etc. fire
7733
+ * `onChange`. New code should prefer the wrapper API.
7734
+ */
7735
+ _state = null;
7736
+ get state() {
7737
+ if (!this._state) this._state = new ConversationState(this);
7738
+ return this._state;
7739
+ }
5723
7740
  /**
5724
7741
  * Register a teardown hook tied to the current run's abort signal. The
5725
7742
  * hook fires when the run aborts OR ends normally — Agent.run wires
5726
7743
  * this through a RunController. When no run is active the hook fires
5727
7744
  * immediately so callers don't leak resources.
7745
+ *
7746
+ * **Scope:** these hooks fire on the **whole agent run's** abort, not on
7747
+ * an individual tool call. For per-tool teardown of resources owned by
7748
+ * the tool author (child processes, handles), prefer `Tool.cleanup` —
7749
+ * see its JSDoc for the full rule.
5728
7750
  */
5729
7751
  abortHooks = /* @__PURE__ */ new Set();
5730
7752
  registerAbortHook(fn) {
@@ -5756,6 +7778,21 @@ var Context = class {
5756
7778
  }
5757
7779
  };
5758
7780
 
7781
+ // src/core/run-env.ts
7782
+ function extractRunEnv(ctx) {
7783
+ return Object.freeze({
7784
+ provider: ctx.provider,
7785
+ session: ctx.session,
7786
+ signal: ctx.signal,
7787
+ tokenCounter: ctx.tokenCounter,
7788
+ cwd: ctx.cwd,
7789
+ projectRoot: ctx.projectRoot,
7790
+ model: ctx.model,
7791
+ systemPrompt: ctx.systemPrompt,
7792
+ tools: ctx.tools
7793
+ });
7794
+ }
7795
+
5759
7796
  // src/core/input-builder.ts
5760
7797
  var InputBuilder = class {
5761
7798
  store;
@@ -6061,10 +8098,25 @@ var ToolRegistry = class {
6061
8098
  tools = /* @__PURE__ */ new Map();
6062
8099
  register(tool, owner = "core") {
6063
8100
  if (this.tools.has(tool.name)) {
6064
- throw new Error(`Tool "${tool.name}" already registered`);
8101
+ throw new WrongStackError({
8102
+ message: `Tool "${tool.name}" already registered`,
8103
+ code: "REGISTRY_DUPLICATE",
8104
+ subsystem: "container",
8105
+ context: { tool: tool.name }
8106
+ });
6065
8107
  }
6066
8108
  this.tools.set(tool.name, { tool, owner });
6067
8109
  }
8110
+ /**
8111
+ * Attempt to register a tool. Returns true if successful, false if a tool
8112
+ * with the same name is already registered. Useful in multi-agent or plugin
8113
+ * scenarios where duplicate registration may be intentional.
8114
+ */
8115
+ tryRegister(tool, owner = "core") {
8116
+ if (this.tools.has(tool.name)) return false;
8117
+ this.tools.set(tool.name, { tool, owner });
8118
+ return true;
8119
+ }
6068
8120
  /**
6069
8121
  * Register a tool as a default. If the tool name is already registered,
6070
8122
  * this is a no-op — the existing registration (from core or another
@@ -6083,7 +8135,12 @@ var ToolRegistry = class {
6083
8135
  */
6084
8136
  override(name, tool, owner = "core") {
6085
8137
  if (!this.tools.has(name)) {
6086
- throw new Error(`Tool "${name}" not registered; cannot override`);
8138
+ throw new WrongStackError({
8139
+ message: `Tool "${name}" not registered; cannot override`,
8140
+ code: "REGISTRY_NOT_FOUND",
8141
+ subsystem: "container",
8142
+ context: { tool: name }
8143
+ });
6087
8144
  }
6088
8145
  this.tools.set(name, { tool, owner });
6089
8146
  }
@@ -6107,9 +8164,25 @@ var ToolRegistry = class {
6107
8164
  // src/registry/provider-registry.ts
6108
8165
  var ProviderRegistry = class {
6109
8166
  factories = /* @__PURE__ */ new Map();
8167
+ /**
8168
+ * Register a provider factory. If a factory with the same type already
8169
+ * exists, it is replaced. Use this for both initial registration and
8170
+ * runtime overrides (e.g. from plugins or CLI flags).
8171
+ */
6110
8172
  register(f) {
6111
8173
  this.factories.set(f.type, f);
6112
8174
  }
8175
+ /**
8176
+ * Override an existing factory. Throws if no factory is registered
8177
+ * for the given type. Use this to safely replace a provider at runtime
8178
+ * (e.g. in tests or when a plugin provides a custom implementation).
8179
+ */
8180
+ override(type, f) {
8181
+ if (!this.factories.has(type)) {
8182
+ throw new Error(`Provider type "${type}" not registered; cannot override`);
8183
+ }
8184
+ this.factories.set(type, f);
8185
+ }
6113
8186
  has(type) {
6114
8187
  return this.factories.has(type);
6115
8188
  }
@@ -6319,20 +8392,23 @@ function parseSemver(v) {
6319
8392
  const parts = v.replace(/^[^0-9]*/, "").split(".").map((s) => Number.parseInt(s, 10) || 0);
6320
8393
  return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];
6321
8394
  }
6322
- function satisfies(range, kernelVersion) {
6323
- const [kMaj, kMin, kPatch] = parseSemver(kernelVersion);
8395
+ function satisfies(range, version) {
8396
+ const [vMaj, vMin, vPatch] = parseSemver(version);
6324
8397
  const trimmed = range.trim();
6325
8398
  const op = trimmed.startsWith("^") ? "^" : trimmed.startsWith("~") ? "~" : "=";
6326
8399
  const ver = trimmed.replace(/^[\^~=]/, "");
6327
8400
  const [rMaj, rMin, rPatch] = parseSemver(ver);
6328
8401
  if (op === "^") {
6329
- if (rMaj === 0) return kMaj === 0 && kMin === rMin && kPatch >= rPatch;
6330
- return kMaj === rMaj && (kMin > rMin || kMin === rMin && kPatch >= rPatch);
8402
+ if (rMaj === 0) return vMaj === 0 && vMin === rMin && vPatch >= rPatch;
8403
+ return vMaj === rMaj && (vMin > rMin || vMin === rMin && vPatch >= rPatch);
6331
8404
  }
6332
8405
  if (op === "~") {
6333
- return kMaj === rMaj && kMin === rMin && kPatch >= rPatch;
8406
+ return vMaj === rMaj && vMin === rMin && vPatch >= rPatch;
6334
8407
  }
6335
- return kMaj === rMaj && kMin === rMin && kPatch === rPatch;
8408
+ return vMaj === rMaj && vMin === rMin && vPatch === rPatch;
8409
+ }
8410
+ function normalizeDep(d) {
8411
+ return typeof d === "string" ? { name: d } : d;
6336
8412
  }
6337
8413
  function topoSort(plugins) {
6338
8414
  const map = /* @__PURE__ */ new Map();
@@ -6343,19 +8419,48 @@ function topoSort(plugins) {
6343
8419
  const visit = (p, stack) => {
6344
8420
  if (visited.has(p.name)) return;
6345
8421
  if (visiting.has(p.name)) {
6346
- throw new Error(`Plugin dependency cycle: ${[...stack, p.name].join(" -> ")}`);
8422
+ throw new PluginError({
8423
+ message: `Plugin dependency cycle: ${[...stack, p.name].join(" -> ")}`,
8424
+ code: "PLUGIN_LOAD_FAILED",
8425
+ pluginName: p.name
8426
+ });
6347
8427
  }
6348
8428
  visiting.add(p.name);
6349
- for (const dep of p.dependsOn ?? []) {
6350
- const d = map.get(dep);
8429
+ for (const raw of p.dependsOn ?? []) {
8430
+ const dep = normalizeDep(raw);
8431
+ const d = map.get(dep.name);
6351
8432
  if (!d) {
6352
- throw new Error(`Plugin "${p.name}" depends on missing plugin "${dep}"`);
8433
+ throw new PluginError({
8434
+ message: `Plugin "${p.name}" depends on missing plugin "${dep.name}"`,
8435
+ code: "PLUGIN_MISSING_DEPENDENCY",
8436
+ pluginName: p.name,
8437
+ context: { dependency: dep.name }
8438
+ });
8439
+ }
8440
+ if (dep.version && d.version && !satisfies(dep.version, d.version)) {
8441
+ throw new PluginError({
8442
+ message: `Plugin "${p.name}" requires "${dep.name}@${dep.version}", found ${d.version}`,
8443
+ code: "PLUGIN_LOAD_FAILED",
8444
+ pluginName: p.name,
8445
+ context: { dependency: dep.name, required: dep.version, found: d.version }
8446
+ });
6353
8447
  }
6354
8448
  visit(d, [...stack, p.name]);
6355
8449
  }
6356
- for (const dep of p.optionalDeps ?? []) {
6357
- const d = map.get(dep);
6358
- if (d) visit(d, [...stack, p.name]);
8450
+ for (const raw of p.optionalDeps ?? []) {
8451
+ const dep = normalizeDep(raw);
8452
+ const d = map.get(dep.name);
8453
+ if (d) {
8454
+ if (dep.version && d.version && !satisfies(dep.version, d.version)) {
8455
+ throw new PluginError({
8456
+ message: `Plugin "${p.name}" optional dep "${dep.name}@${dep.version}" found ${d.version}`,
8457
+ code: "PLUGIN_LOAD_FAILED",
8458
+ pluginName: p.name,
8459
+ context: { dependency: dep.name, required: dep.version, found: d.version }
8460
+ });
8461
+ }
8462
+ visit(d, [...stack, p.name]);
8463
+ }
6359
8464
  }
6360
8465
  visiting.delete(p.name);
6361
8466
  visited.add(p.name);
@@ -6372,7 +8477,12 @@ async function loadPlugins(plugins, opts) {
6372
8477
  for (const p of plugins) {
6373
8478
  for (const c of p.conflictsWith ?? []) {
6374
8479
  if (names.has(c)) {
6375
- throw new Error(`Plugin "${p.name}" conflicts with loaded plugin "${c}"`);
8480
+ throw new PluginError({
8481
+ message: `Plugin "${p.name}" conflicts with loaded plugin "${c}"`,
8482
+ code: "PLUGIN_LOAD_FAILED",
8483
+ pluginName: p.name,
8484
+ context: { conflictsWith: c }
8485
+ });
6376
8486
  }
6377
8487
  }
6378
8488
  }
@@ -6385,15 +8495,38 @@ async function loadPlugins(plugins, opts) {
6385
8495
  }
6386
8496
  for (const plugin of sorted) {
6387
8497
  if (!satisfies(plugin.apiVersion, kernelVersion)) {
6388
- const err = new Error(
6389
- `Plugin "${plugin.name}" requires apiVersion ${plugin.apiVersion}; kernel is ${kernelVersion}`
6390
- );
8498
+ const err = new PluginError({
8499
+ message: `Plugin "${plugin.name}" requires apiVersion ${plugin.apiVersion}; kernel is ${kernelVersion}`,
8500
+ code: "PLUGIN_API_MISMATCH",
8501
+ pluginName: plugin.name,
8502
+ context: { required: plugin.apiVersion, kernel: kernelVersion }
8503
+ });
6391
8504
  opts.log.error(err.message);
6392
8505
  failed.push({ plugin, err });
6393
8506
  continue;
6394
8507
  }
8508
+ if (plugin.configSchema && opts.pluginOptions) {
8509
+ const pluginOpts = opts.pluginOptions[plugin.name];
8510
+ if (pluginOpts !== void 0) {
8511
+ const result = validateAgainstSchema(pluginOpts, plugin.configSchema);
8512
+ if (!result.ok) {
8513
+ const firstErr = result.errors[0];
8514
+ const detail = firstErr ? `${firstErr.path}: ${firstErr.message}` : "config invalid";
8515
+ const err = new PluginError({
8516
+ message: `Plugin "${plugin.name}" config invalid \u2014 ${detail}`,
8517
+ code: "PLUGIN_LOAD_FAILED",
8518
+ pluginName: plugin.name,
8519
+ context: { errors: result.errors }
8520
+ });
8521
+ opts.log.error(err.message);
8522
+ failed.push({ plugin, err });
8523
+ continue;
8524
+ }
8525
+ }
8526
+ }
6395
8527
  try {
6396
- const api = opts.apiFactory(plugin);
8528
+ const rawApi = opts.apiFactory(plugin);
8529
+ const api = plugin.capabilities ? wrapApiForCapabilityCheck(plugin, rawApi, opts.log) : rawApi;
6397
8530
  await plugin.setup(api);
6398
8531
  loaded.push(plugin);
6399
8532
  opts.log.info(`Plugin "${plugin.name}" loaded`);
@@ -6404,7 +8537,88 @@ async function loadPlugins(plugins, opts) {
6404
8537
  }
6405
8538
  return { loaded, failed };
6406
8539
  }
8540
+ async function unloadPlugins(loadedPlugins, opts) {
8541
+ const ordered = [...loadedPlugins].reverse();
8542
+ for (const plugin of ordered) {
8543
+ if (typeof plugin.teardown !== "function") continue;
8544
+ try {
8545
+ const api = opts.apiFactory(plugin);
8546
+ await plugin.teardown(api);
8547
+ opts.log.info(`Plugin "${plugin.name}" torn down`);
8548
+ } catch (err) {
8549
+ opts.log.error(`Plugin "${plugin.name}" teardown failed`, err);
8550
+ }
8551
+ }
8552
+ }
8553
+ function wrapApiForCapabilityCheck(plugin, api, log) {
8554
+ const caps = plugin.capabilities ?? {};
8555
+ const warn = (subsystem, detail) => {
8556
+ const msg = `Plugin "${plugin.name}" used ${subsystem} without declaring capabilities.${subsystem} \u2014 ${detail}`;
8557
+ if (typeof log.warn === "function") log.warn(msg);
8558
+ else log.error(msg);
8559
+ };
8560
+ const wrappedTools = caps.tools !== false ? api.tools : new Proxy(api.tools, {
8561
+ get(target, prop, receiver) {
8562
+ if (prop === "register") {
8563
+ return (t2) => {
8564
+ warn("tools", `register(${t2?.name ?? "<unknown>"})`);
8565
+ return target.register(t2);
8566
+ };
8567
+ }
8568
+ return Reflect.get(target, prop, receiver);
8569
+ }
8570
+ });
8571
+ const wrappedProviders = caps.providers !== false ? api.providers : new Proxy(api.providers, {
8572
+ get(target, prop, receiver) {
8573
+ if (prop === "register") {
8574
+ return (f) => {
8575
+ warn("providers", `register(${f?.type ?? "<unknown>"})`);
8576
+ return target.register(f);
8577
+ };
8578
+ }
8579
+ return Reflect.get(target, prop, receiver);
8580
+ }
8581
+ });
8582
+ const wrappedSlash = caps.slashCommands !== false ? api.slashCommands : new Proxy(api.slashCommands, {
8583
+ get(target, prop, receiver) {
8584
+ if (prop === "register") {
8585
+ return (c) => {
8586
+ warn("slashCommands", `register(${c?.name ?? "<unknown>"})`);
8587
+ return target.register(c);
8588
+ };
8589
+ }
8590
+ return Reflect.get(target, prop, receiver);
8591
+ }
8592
+ });
8593
+ const wrappedMcp = caps.mcp !== false ? api.mcp : new Proxy(api.mcp, {
8594
+ get(target, prop, receiver) {
8595
+ if (prop === "start") {
8596
+ return (cfg) => {
8597
+ warn("mcp", `start(${cfg?.name ?? "<unknown>"})`);
8598
+ return target.start(cfg);
8599
+ };
8600
+ }
8601
+ return Reflect.get(target, prop, receiver);
8602
+ }
8603
+ });
8604
+ return new Proxy(api, {
8605
+ get(target, prop, receiver) {
8606
+ switch (prop) {
8607
+ case "tools":
8608
+ return wrappedTools;
8609
+ case "providers":
8610
+ return wrappedProviders;
8611
+ case "slashCommands":
8612
+ return wrappedSlash;
8613
+ case "mcp":
8614
+ return wrappedMcp;
8615
+ default:
8616
+ return Reflect.get(target, prop, receiver);
8617
+ }
8618
+ }
8619
+ });
8620
+ }
6407
8621
 
6408
- export { Agent, AutoCompactionMiddleware, AutonomousRunner, Container, Context, DEFAULT_MAX_ITERATIONS, DEFAULT_MODES, DEFAULT_SPEC_TEMPLATE, DefaultAttachmentStore, DefaultConfigLoader, DefaultErrorHandler, DefaultLogger, DefaultMemoryStore, DefaultModeStore, DefaultModelsRegistry, DefaultMultiAgentCoordinator, DefaultPathResolver, DefaultPermissionPolicy, DefaultPluginAPI, DefaultRetryPolicy, DefaultSecretScrubber, DefaultSecretVault, DefaultSessionStore, DefaultSkillLoader, DefaultSystemPromptBuilder, DefaultTaskStore, DefaultTokenCounter, DoneConditionChecker, ENCRYPTED_PREFIX, EventBus, HybridCompactor, InMemoryAgentBridge, InMemoryBridgeTransport, InputBuilder, IntelligentCompactor, KERNEL_API_VERSION, LAYER_1_IDENTITY, LLMSelector, Pipeline, ProviderError, ProviderRegistry, QueueStore, RecoveryLock, RunController, SelectiveCompactor, SlashCommandRegistry, SpecDrivenDev, SpecParser, TOKENS, TaskFlow, TaskGenerator, TaskTracker, ToolExecutor, ToolRegistry, asBlocks, asText, atomicWrite, classifyFamily, color, compileGlob, computeTaskProgress, contextManagerTool, createContextManagerTool, createDefaultPipelines, createMessage, createToolOutputSerializer, decryptConfigSecrets, detectNewlineStyle, encryptConfigSecrets, ensureDir, findCriticalPath, isImageBlock, isTextBlock, isToolResultBlock, isToolUseBlock, loadPlugins, loadProjectModes, loadUserModes, matchAny, matchGlob, migratePlaintextSecrets, normalizeToLf, projectHash, resolveWstackPaths, rewriteConfigEncrypted, safeParse, safeStringify, sanitizeJsonString, stripAnsi, toStyle, topologicalSort, unifiedDiff };
8622
+ export { Agent, AgentError, AutoCompactionMiddleware, AutonomousRunner, BudgetExceededError, ConfigError, ConfigMigrationError, Container, Context, ConversationState, DEFAULT_CONFIG_MIGRATIONS, DEFAULT_MAX_ITERATIONS, DEFAULT_MODES, DEFAULT_SPEC_TEMPLATE, DefaultAttachmentStore, DefaultConfigLoader, DefaultConfigStore, DefaultErrorHandler, DefaultHealthRegistry, DefaultLogger, DefaultMemoryStore, DefaultModeStore, DefaultModelsRegistry, DefaultMultiAgentCoordinator, DefaultPathResolver, DefaultPermissionPolicy, DefaultPluginAPI, DefaultRetryPolicy, DefaultSecretScrubber, DefaultSecretVault, DefaultSessionReader, DefaultSessionStore, DefaultSkillLoader, DefaultSystemPromptBuilder, DefaultTaskStore, DefaultTokenCounter, DoneConditionChecker, ENCRYPTED_PREFIX, EventBus, HybridCompactor, InMemoryAgentBridge, InMemoryBridgeTransport, InMemoryMetricsSink, InputBuilder, IntelligentCompactor, KERNEL_API_VERSION, LAYER_1_IDENTITY, LLMSelector, NoopMetricsSink, NoopTracer, OTelTracer, PROMETHEUS_CONTENT_TYPE, Pipeline, PluginError, ProviderError, ProviderRegistry, QueueStore, RecoveryLock, RunController, SelectiveCompactor, SessionError, SlashCommandRegistry, SpecDrivenDev, SpecParser, SubagentBudget, TOKENS, TaskFlow, TaskGenerator, TaskTracker, ToolError, ToolExecutor, ToolRegistry, WrongStackError, allServers, asBlocks, asText, atomicWrite, awsServer, blockServer, braveSearchServer, buildOtlpMetricsRequest, buildOtlpTracesRequest, classifyFamily, color, compileGlob, computeTaskProgress, context7Server, contextManagerTool, createContextManagerTool, createDefaultPipelines, createMessage, createToolOutputSerializer, decryptConfigSecrets, detectNewlineStyle, encryptConfigSecrets, ensureDir, estimateTextTokens, estimateToolInputTokens, estimateToolResultTokens, everArtServer, extractRunEnv, filesystemServer, findCriticalPath, githubServer, googleMapsServer, isAgentError, isConfigError, isImageBlock, isPluginError, isSessionError, isTextBlock, isToolError, isToolResultBlock, isToolUseBlock, isWrongStackError, loadPlugins, loadProjectModes, loadUserModes, makeAgentSubagentRunner, matchAny, matchGlob, migratePlaintextSecrets, normalizeToLf, projectHash, renderPrometheus, resolveWstackPaths, rewriteConfigEncrypted, runConfigMigrations, safeParse, safeStringify, sanitizeJsonString, sentinelServer, slackServer, startMetricsServer, startOtlpMetricsExporter, startOtlpTraceExporter, stripAnsi, toStyle, toWrongStackError, topologicalSort, unifiedDiff, unloadPlugins, validateAgainstSchema, wireMetricsToEvents, wrapAsState };
6409
8623
  //# sourceMappingURL=index.js.map
6410
8624
  //# sourceMappingURL=index.js.map