@wrongstack/core 0.1.1 → 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/defaults/index.d.ts +738 -138
- package/dist/defaults/index.js +2507 -975
- package/dist/defaults/index.js.map +1 -1
- package/dist/index.d.ts +28 -9
- package/dist/index.js +3272 -1058
- package/dist/index.js.map +1 -1
- package/dist/kernel/index.d.ts +5 -3
- package/dist/kernel/index.js +89 -11
- package/dist/kernel/index.js.map +1 -1
- package/dist/provider-DovtyuM8.d.ts +813 -0
- package/dist/{secret-scrubber-Dax_Ou_o.d.ts → secret-scrubber-qU3AwEiI.d.ts} +126 -457
- package/dist/{tool-executor-DjnMELMV.d.ts → session-reader-DR4u3bu9.d.ts} +445 -59
- package/dist/{system-prompt-BG3nks8P.d.ts → system-prompt--mzZnenv.d.ts} +1 -1
- package/dist/types/index.d.ts +4 -3
- package/dist/types/index.js +153 -5
- package/dist/types/index.js.map +1 -1
- package/dist/utils/index.d.ts +42 -1
- package/dist/utils/index.js +122 -3
- package/dist/utils/index.js.map +1 -1
- package/package.json +5 -4
package/dist/defaults/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as fs5 from 'fs';
|
|
2
2
|
import * as path2 from 'path';
|
|
3
3
|
import * as fsp from 'fs/promises';
|
|
4
|
+
import * as crypto2 from 'crypto';
|
|
4
5
|
import { randomBytes, createCipheriv, createDecipheriv, randomUUID } from 'crypto';
|
|
5
|
-
import * as os from 'os';
|
|
6
6
|
import { EventEmitter } from 'events';
|
|
7
|
+
import * as os from 'os';
|
|
7
8
|
|
|
8
9
|
// src/defaults/logger.ts
|
|
9
10
|
|
|
@@ -61,7 +62,7 @@ var DefaultLogger = class _DefaultLogger {
|
|
|
61
62
|
this.pretty = opts.pretty ?? true;
|
|
62
63
|
if (this.file) {
|
|
63
64
|
try {
|
|
64
|
-
|
|
65
|
+
fs5.mkdirSync(path2.dirname(this.file), { recursive: true });
|
|
65
66
|
} catch {
|
|
66
67
|
}
|
|
67
68
|
}
|
|
@@ -100,7 +101,7 @@ var DefaultLogger = class _DefaultLogger {
|
|
|
100
101
|
}
|
|
101
102
|
if (this.file) {
|
|
102
103
|
try {
|
|
103
|
-
|
|
104
|
+
fs5.appendFileSync(this.file, `${JSON.stringify(entry)}
|
|
104
105
|
`);
|
|
105
106
|
} catch {
|
|
106
107
|
}
|
|
@@ -148,7 +149,7 @@ var DefaultPathResolver = class {
|
|
|
148
149
|
while (dir !== root) {
|
|
149
150
|
for (const marker of PROJECT_MARKERS) {
|
|
150
151
|
try {
|
|
151
|
-
|
|
152
|
+
fs5.accessSync(path2.join(dir, marker));
|
|
152
153
|
return dir;
|
|
153
154
|
} catch {
|
|
154
155
|
}
|
|
@@ -163,7 +164,7 @@ var DefaultPathResolver = class {
|
|
|
163
164
|
const abs = path2.isAbsolute(input) ? input : path2.resolve(this.cwd, input);
|
|
164
165
|
let real;
|
|
165
166
|
try {
|
|
166
|
-
real =
|
|
167
|
+
real = fs5.realpathSync(abs);
|
|
167
168
|
} catch {
|
|
168
169
|
real = path2.normalize(abs);
|
|
169
170
|
}
|
|
@@ -187,248 +188,6 @@ var DefaultPathResolver = class {
|
|
|
187
188
|
}
|
|
188
189
|
};
|
|
189
190
|
|
|
190
|
-
// src/defaults/secret-scrubber.ts
|
|
191
|
-
var PATTERNS = [
|
|
192
|
-
// Anchored at the start where possible so partial matches inside larger
|
|
193
|
-
// strings don't trigger false positives.
|
|
194
|
-
{ type: "anthropic_key", regex: /(?<![A-Za-z0-9])sk-ant-api\d+-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g },
|
|
195
|
-
{ type: "openai_key", regex: /(?<![A-Za-z0-9])sk-(?:proj-)?[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g },
|
|
196
|
-
{ type: "github_pat", regex: /(?<![A-Za-z0-9])ghp_[A-Za-z0-9]{36,}(?![A-Za-z0-9])/g },
|
|
197
|
-
{ type: "github_pat_v2", regex: /(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{50,}(?![A-Za-z0-9])/g },
|
|
198
|
-
{ type: "aws_access_key", regex: /(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}(?![A-Za-z0-9])/g },
|
|
199
|
-
{ type: "gcp_key", regex: /(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}(?![A-Za-z0-9])/g },
|
|
200
|
-
{ type: "slack_token", regex: /(?<![A-Za-z0-9-])xox[abpos]-[A-Za-z0-9-]{10,}(?![A-Za-z0-9-])/g },
|
|
201
|
-
{ type: "stripe_key", regex: /(?<![A-Za-z0-9])sk_(?:live|test)_[A-Za-z0-9]{24,}(?![A-Za-z0-9])/g },
|
|
202
|
-
{ type: "twilio_sid", regex: /(?<![A-Za-z0-9])AC[a-f0-9]{32}(?![A-Za-z0-9])/g },
|
|
203
|
-
{
|
|
204
|
-
type: "jwt",
|
|
205
|
-
// Anchored: look for literal "eyJ" which is unambiguous for JWT header
|
|
206
|
-
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
|
|
207
|
-
},
|
|
208
|
-
{
|
|
209
|
-
type: "private_key",
|
|
210
|
-
// Anchored: start must be BEGIN, end must be END with no extra dashes after END
|
|
211
|
-
regex: /(?:^|\n)-----BEGIN (?:RSA|EC|OPENSSH|DSA|PGP)? ?PRIVATE KEY-----[\s\S]*?-----END[^-]*-----(?:\n|$)/g
|
|
212
|
-
},
|
|
213
|
-
{ type: "mongodb_uri", regex: /mongodb(?:\+srv)?:\/\/[^\s"'`]+/g },
|
|
214
|
-
{ type: "postgres_uri", regex: /postgres(?:ql)?:\/\/[^\s"'`]+/g },
|
|
215
|
-
{ type: "mysql_uri", regex: /mysql:\/\/[^\s"'`]+/g },
|
|
216
|
-
{ type: "redis_uri", regex: /redis:\/\/[^\s"'`]+/g },
|
|
217
|
-
{ type: "bearer_token", regex: /(?<![A-Za-z0-9_.~+/-])Bearer\s+[A-Za-z0-9._~+/-]{20,}=*(?![A-Za-z0-9_.~+/-])/g },
|
|
218
|
-
{
|
|
219
|
-
type: "high_entropy_env",
|
|
220
|
-
// Value-side word boundary + length gate to avoid matching short random strings
|
|
221
|
-
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
|
|
222
|
-
}
|
|
223
|
-
];
|
|
224
|
-
var DefaultSecretScrubber = class {
|
|
225
|
-
scrub(text) {
|
|
226
|
-
if (!text) return text;
|
|
227
|
-
let out = text;
|
|
228
|
-
for (const p of PATTERNS) {
|
|
229
|
-
out = out.replace(p.regex, (_match, group1, group2) => {
|
|
230
|
-
if (p.type === "high_entropy_env" && group1 && group2) {
|
|
231
|
-
return `${group1}=[REDACTED:${p.type}]`;
|
|
232
|
-
}
|
|
233
|
-
return `[REDACTED:${p.type}]`;
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
return out;
|
|
237
|
-
}
|
|
238
|
-
scrubObject(obj) {
|
|
239
|
-
const seen = /* @__PURE__ */ new WeakSet();
|
|
240
|
-
const visit = (v) => {
|
|
241
|
-
if (typeof v === "string") return this.scrub(v);
|
|
242
|
-
if (v === null || typeof v !== "object") return v;
|
|
243
|
-
if (seen.has(v)) return v;
|
|
244
|
-
seen.add(v);
|
|
245
|
-
if (Array.isArray(v)) return v.map(visit);
|
|
246
|
-
const out = {};
|
|
247
|
-
for (const [k, val] of Object.entries(v)) {
|
|
248
|
-
out[k] = visit(val);
|
|
249
|
-
}
|
|
250
|
-
return out;
|
|
251
|
-
};
|
|
252
|
-
return visit(obj);
|
|
253
|
-
}
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
// src/types/provider.ts
|
|
257
|
-
var ProviderError = class extends Error {
|
|
258
|
-
status;
|
|
259
|
-
retryable;
|
|
260
|
-
providerId;
|
|
261
|
-
body;
|
|
262
|
-
cause;
|
|
263
|
-
constructor(message, status, retryable, providerId, opts = {}) {
|
|
264
|
-
super(message);
|
|
265
|
-
this.name = "ProviderError";
|
|
266
|
-
this.status = status;
|
|
267
|
-
this.retryable = retryable;
|
|
268
|
-
this.providerId = providerId;
|
|
269
|
-
this.body = opts.body;
|
|
270
|
-
this.cause = opts.cause;
|
|
271
|
-
}
|
|
272
|
-
/**
|
|
273
|
-
* Render a one-line, user-facing description. Designed for the CLI/TUI
|
|
274
|
-
* status line and the agent's retry warning. Avoids dumping raw JSON
|
|
275
|
-
* (which is what users see today when a 529 lands and the log message
|
|
276
|
-
* includes the full `{"type":"error",...}` body).
|
|
277
|
-
*
|
|
278
|
-
* Examples:
|
|
279
|
-
* "minimax-coding-plan overloaded (529): High traffic detected. Upgrade for highspeed model. [req 06534785201de9c0…]"
|
|
280
|
-
* "openai rate limited (429): Retry after 12s"
|
|
281
|
-
* "anthropic invalid request (400): messages.0.role must be one of 'user'|'assistant'"
|
|
282
|
-
* "groq HTTP 500 (server error)"
|
|
283
|
-
*/
|
|
284
|
-
describe() {
|
|
285
|
-
const kind = describeStatus(this.status, this.body?.type);
|
|
286
|
-
const head = `${this.providerId} ${kind}`;
|
|
287
|
-
const detail = this.body?.message?.trim();
|
|
288
|
-
const reqId = this.body?.requestId ? ` [req ${this.body.requestId.slice(0, 16)}${this.body.requestId.length > 16 ? "\u2026" : ""}]` : "";
|
|
289
|
-
if (detail && detail.length > 0) {
|
|
290
|
-
return `${head}: ${truncate(detail, 240)}${reqId}`;
|
|
291
|
-
}
|
|
292
|
-
return `${head}${reqId}`;
|
|
293
|
-
}
|
|
294
|
-
};
|
|
295
|
-
function describeStatus(status, type) {
|
|
296
|
-
if (status === 0) return "network error";
|
|
297
|
-
if (type === "overloaded_error" || status === 529) return `overloaded (${status})`;
|
|
298
|
-
if (type === "rate_limit_error" || status === 429) return `rate limited (${status})`;
|
|
299
|
-
if (type === "authentication_error" || status === 401) return `auth failed (${status})`;
|
|
300
|
-
if (type === "permission_error" || status === 403) return `forbidden (${status})`;
|
|
301
|
-
if (type === "not_found_error" || status === 404) return `not found (${status})`;
|
|
302
|
-
if (type === "invalid_request_error" || status === 400) return `invalid request (${status})`;
|
|
303
|
-
if (status === 408) return `timeout (${status})`;
|
|
304
|
-
if (status >= 500 && status < 600) return `HTTP ${status} (server error)`;
|
|
305
|
-
if (type) return `${type} (${status})`;
|
|
306
|
-
return `HTTP ${status}`;
|
|
307
|
-
}
|
|
308
|
-
function truncate(s, n) {
|
|
309
|
-
return s.length <= n ? s : `${s.slice(0, n - 1)}\u2026`;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// src/defaults/retry-policy.ts
|
|
313
|
-
var DefaultRetryPolicy = class {
|
|
314
|
-
shouldRetry(err, attempt) {
|
|
315
|
-
if (err instanceof ProviderError) {
|
|
316
|
-
if (!err.retryable) return false;
|
|
317
|
-
return attempt < this.maxAttempts(err);
|
|
318
|
-
}
|
|
319
|
-
const msg = err.message ?? "";
|
|
320
|
-
const isNetwork = /ECONN|ETIMEDOUT|ETIME|ENOTFOUND|EAI_AGAIN|fetch failed/i.test(msg);
|
|
321
|
-
if (isNetwork) return attempt < 2;
|
|
322
|
-
return false;
|
|
323
|
-
}
|
|
324
|
-
maxAttempts(err) {
|
|
325
|
-
if (err instanceof ProviderError) {
|
|
326
|
-
if (err.status === 429) return 5;
|
|
327
|
-
if (err.status === 529) return 3;
|
|
328
|
-
if (err.status >= 500) return 3;
|
|
329
|
-
return 0;
|
|
330
|
-
}
|
|
331
|
-
return 2;
|
|
332
|
-
}
|
|
333
|
-
delayMs(attempt) {
|
|
334
|
-
const base = 1e3;
|
|
335
|
-
const exp = base * 2 ** attempt;
|
|
336
|
-
const jitter = Math.random() * base;
|
|
337
|
-
return Math.min(3e4, exp + jitter);
|
|
338
|
-
}
|
|
339
|
-
};
|
|
340
|
-
|
|
341
|
-
// src/defaults/error-handler.ts
|
|
342
|
-
function buildRecoveryStrategies(opts) {
|
|
343
|
-
return [
|
|
344
|
-
{
|
|
345
|
-
label: "context_overflow_reduce",
|
|
346
|
-
compactor: opts?.compactor,
|
|
347
|
-
async attempt(err, ctx) {
|
|
348
|
-
if (err instanceof ProviderError && (err.status === 413 || /context|too long|tokens/i.test(err.message))) {
|
|
349
|
-
if (this.compactor) {
|
|
350
|
-
try {
|
|
351
|
-
const report = await this.compactor.compact(ctx, { aggressive: true });
|
|
352
|
-
if (report.after < report.before) {
|
|
353
|
-
return {
|
|
354
|
-
content: [{ type: "text", text: "[context compacted automatically \u2014 please retry]" }],
|
|
355
|
-
stopReason: "end_turn",
|
|
356
|
-
usage: { input: 0, output: 0 },
|
|
357
|
-
model: ctx.model
|
|
358
|
-
};
|
|
359
|
-
}
|
|
360
|
-
} catch {
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
return null;
|
|
364
|
-
}
|
|
365
|
-
return null;
|
|
366
|
-
}
|
|
367
|
-
},
|
|
368
|
-
{
|
|
369
|
-
label: "rate_limit_backoff",
|
|
370
|
-
async attempt(err, ctx) {
|
|
371
|
-
if (err instanceof ProviderError && err.status === 429) {
|
|
372
|
-
const delayMs = err.body?.retryAfterMs ?? 5e3;
|
|
373
|
-
const delay = Math.max(1e3, Math.min(delayMs, 6e4));
|
|
374
|
-
await new Promise((r) => setTimeout(r, delay));
|
|
375
|
-
return {
|
|
376
|
-
content: [{ type: "text", text: "[rate limit backoff applied \u2014 please retry]" }],
|
|
377
|
-
stopReason: "end_turn",
|
|
378
|
-
usage: { input: 0, output: 0 },
|
|
379
|
-
model: ctx.model
|
|
380
|
-
};
|
|
381
|
-
}
|
|
382
|
-
return null;
|
|
383
|
-
}
|
|
384
|
-
},
|
|
385
|
-
{
|
|
386
|
-
label: "downgrade_model",
|
|
387
|
-
async attempt(err, ctx) {
|
|
388
|
-
if (err instanceof ProviderError && (err.status === 429 || err.status === 529 || err.status >= 500)) {
|
|
389
|
-
return null;
|
|
390
|
-
}
|
|
391
|
-
return null;
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
];
|
|
395
|
-
}
|
|
396
|
-
var DEFAULT_RECOVERY_STRATEGIES = buildRecoveryStrategies();
|
|
397
|
-
var DefaultErrorHandler = class {
|
|
398
|
-
strategies;
|
|
399
|
-
constructor(strategies = DEFAULT_RECOVERY_STRATEGIES) {
|
|
400
|
-
this.strategies = strategies;
|
|
401
|
-
}
|
|
402
|
-
classify(err) {
|
|
403
|
-
if (err instanceof DOMException && err.name === "AbortError") {
|
|
404
|
-
return { kind: "abort", retryable: false };
|
|
405
|
-
}
|
|
406
|
-
if (err instanceof Error && err.name === "AbortError") {
|
|
407
|
-
return { kind: "abort", retryable: false };
|
|
408
|
-
}
|
|
409
|
-
if (err instanceof ProviderError) {
|
|
410
|
-
if (err.status === 429) return { kind: "rate_limit", retryable: true };
|
|
411
|
-
if (err.status === 529) return { kind: "overloaded", retryable: true };
|
|
412
|
-
if (err.status >= 500) return { kind: "server", retryable: true };
|
|
413
|
-
if (err.status === 413 || /context|too long|tokens/i.test(err.message)) {
|
|
414
|
-
return { kind: "context_overflow", retryable: false };
|
|
415
|
-
}
|
|
416
|
-
if (err.status >= 400) return { kind: "client", retryable: false };
|
|
417
|
-
}
|
|
418
|
-
if (err instanceof Error && /ECONN|ETIMEDOUT|ETIME|ENOTFOUND|EAI_AGAIN|fetch failed/i.test(err.message)) {
|
|
419
|
-
return { kind: "network", retryable: true };
|
|
420
|
-
}
|
|
421
|
-
return { kind: "unknown", retryable: false };
|
|
422
|
-
}
|
|
423
|
-
async recover(err, ctx) {
|
|
424
|
-
for (const strategy of this.strategies) {
|
|
425
|
-
const result = await strategy.attempt(err, ctx);
|
|
426
|
-
if (result !== null) return result;
|
|
427
|
-
}
|
|
428
|
-
return null;
|
|
429
|
-
}
|
|
430
|
-
};
|
|
431
|
-
|
|
432
191
|
// src/defaults/token-counter.ts
|
|
433
192
|
var DefaultTokenCounter = class {
|
|
434
193
|
input = 0;
|
|
@@ -547,8 +306,11 @@ async function atomicWrite(targetPath, content, opts = {}) {
|
|
|
547
306
|
}
|
|
548
307
|
try {
|
|
549
308
|
const fh = await fsp.open(tmp, "r+");
|
|
550
|
-
|
|
551
|
-
|
|
309
|
+
try {
|
|
310
|
+
await fh.sync();
|
|
311
|
+
} finally {
|
|
312
|
+
await fh.close();
|
|
313
|
+
}
|
|
552
314
|
} catch {
|
|
553
315
|
}
|
|
554
316
|
let mode;
|
|
@@ -863,130 +625,7 @@ function userInputTitle(content) {
|
|
|
863
625
|
const text = content.filter((b) => b.type === "text").map((b) => b.text).join(" ");
|
|
864
626
|
return (text || "(non-text input)").slice(0, 60);
|
|
865
627
|
}
|
|
866
|
-
var
|
|
867
|
-
var DEFAULT_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
868
|
-
var RecoveryLock = class {
|
|
869
|
-
file;
|
|
870
|
-
pid;
|
|
871
|
-
hostname;
|
|
872
|
-
maxAgeMs;
|
|
873
|
-
sessionStore;
|
|
874
|
-
probe;
|
|
875
|
-
constructor(opts) {
|
|
876
|
-
this.file = path2.join(opts.dir, LOCK_FILE);
|
|
877
|
-
this.pid = opts.pid ?? process.pid;
|
|
878
|
-
this.hostname = opts.hostname ?? os.hostname();
|
|
879
|
-
this.maxAgeMs = opts.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
|
|
880
|
-
this.sessionStore = opts.sessionStore;
|
|
881
|
-
this.probe = opts.isPidAlive ?? defaultIsPidAlive;
|
|
882
|
-
}
|
|
883
|
-
/**
|
|
884
|
-
* Examine the lockfile and decide whether it represents an abandoned
|
|
885
|
-
* session. Returns `null` if the file is missing, points to a live
|
|
886
|
-
* instance, references a clean-closed session, is too old, or is
|
|
887
|
-
* malformed. Otherwise returns enough detail to prompt the user.
|
|
888
|
-
*
|
|
889
|
-
* Important: this is a read-only check. We never delete an active
|
|
890
|
-
* lock from here — if another wstack instance is alive, the caller
|
|
891
|
-
* should bail or run with a fresh session instead.
|
|
892
|
-
*/
|
|
893
|
-
async checkAbandoned() {
|
|
894
|
-
const lock = await this.readLock();
|
|
895
|
-
if (!lock) return null;
|
|
896
|
-
const ageMs = Date.now() - new Date(lock.startedAt).getTime();
|
|
897
|
-
if (Number.isNaN(ageMs) || ageMs < 0) {
|
|
898
|
-
return null;
|
|
899
|
-
}
|
|
900
|
-
if (ageMs > this.maxAgeMs) return null;
|
|
901
|
-
if (lock.hostname === this.hostname && this.probe(lock.pid)) {
|
|
902
|
-
return null;
|
|
903
|
-
}
|
|
904
|
-
let messageCount = 0;
|
|
905
|
-
if (this.sessionStore) {
|
|
906
|
-
try {
|
|
907
|
-
const data = await this.sessionStore.load(lock.sessionId);
|
|
908
|
-
const closed = data.events.some((e) => e.type === "session_end");
|
|
909
|
-
if (closed) return null;
|
|
910
|
-
messageCount = data.messages.length;
|
|
911
|
-
} catch {
|
|
912
|
-
return null;
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
return {
|
|
916
|
-
sessionId: lock.sessionId,
|
|
917
|
-
pid: lock.pid,
|
|
918
|
-
startedAt: lock.startedAt,
|
|
919
|
-
ageMs,
|
|
920
|
-
messageCount
|
|
921
|
-
};
|
|
922
|
-
}
|
|
923
|
-
/**
|
|
924
|
-
* Claim the lock for the given session. Overwrites any existing lock
|
|
925
|
-
* — the caller should have already handled abandonment (via
|
|
926
|
-
* `checkAbandoned`) before calling this.
|
|
927
|
-
*/
|
|
928
|
-
async write(sessionId) {
|
|
929
|
-
await ensureDir(path2.dirname(this.file));
|
|
930
|
-
const lock = {
|
|
931
|
-
v: 1,
|
|
932
|
-
sessionId,
|
|
933
|
-
pid: this.pid,
|
|
934
|
-
hostname: this.hostname,
|
|
935
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
936
|
-
};
|
|
937
|
-
const tmp = `${this.file}.tmp`;
|
|
938
|
-
await fsp.writeFile(tmp, JSON.stringify(lock), { mode: 384 });
|
|
939
|
-
await fsp.rename(tmp, this.file);
|
|
940
|
-
}
|
|
941
|
-
/**
|
|
942
|
-
* Release the lock. Idempotent — silently succeeds if the file is
|
|
943
|
-
* already gone (e.g. someone else cleared it, or the directory was
|
|
944
|
-
* wiped).
|
|
945
|
-
*/
|
|
946
|
-
async clear() {
|
|
947
|
-
try {
|
|
948
|
-
await fsp.unlink(this.file);
|
|
949
|
-
} catch (err) {
|
|
950
|
-
const code = err.code;
|
|
951
|
-
if (code === "ENOENT") return;
|
|
952
|
-
throw err;
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
async readLock() {
|
|
956
|
-
let raw;
|
|
957
|
-
try {
|
|
958
|
-
raw = await fsp.readFile(this.file, "utf8");
|
|
959
|
-
} catch (err) {
|
|
960
|
-
const code = err.code;
|
|
961
|
-
if (code === "ENOENT") return null;
|
|
962
|
-
return null;
|
|
963
|
-
}
|
|
964
|
-
try {
|
|
965
|
-
const parsed = JSON.parse(raw);
|
|
966
|
-
if (!isLockFile(parsed)) return null;
|
|
967
|
-
return parsed;
|
|
968
|
-
} catch {
|
|
969
|
-
return null;
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
|
-
};
|
|
973
|
-
function isLockFile(v) {
|
|
974
|
-
if (typeof v !== "object" || v === null) return false;
|
|
975
|
-
const o = v;
|
|
976
|
-
return o["v"] === 1 && typeof o["sessionId"] === "string" && typeof o["pid"] === "number" && typeof o["hostname"] === "string" && typeof o["startedAt"] === "string";
|
|
977
|
-
}
|
|
978
|
-
function defaultIsPidAlive(pid) {
|
|
979
|
-
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
980
|
-
try {
|
|
981
|
-
process.kill(pid, 0);
|
|
982
|
-
return true;
|
|
983
|
-
} catch (err) {
|
|
984
|
-
const code = err.code;
|
|
985
|
-
if (code === "EPERM") return true;
|
|
986
|
-
return false;
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
|
-
var QueueStore = class {
|
|
628
|
+
var QueueStore = class {
|
|
990
629
|
file;
|
|
991
630
|
constructor(opts) {
|
|
992
631
|
this.file = path2.join(opts.dir, "queue.json");
|
|
@@ -1148,92 +787,291 @@ function mergeAdjacentText(blocks) {
|
|
|
1148
787
|
}
|
|
1149
788
|
return out;
|
|
1150
789
|
}
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
// src/defaults/secret-vault.ts
|
|
1156
|
-
var KEY_BYTES = 32;
|
|
1157
|
-
var IV_BYTES = 12;
|
|
1158
|
-
var TAG_BYTES = 16;
|
|
1159
|
-
var ALGO = "aes-256-gcm";
|
|
1160
|
-
var DefaultSecretVault = class {
|
|
1161
|
-
keyFile;
|
|
1162
|
-
key;
|
|
790
|
+
var MAX_BYTES_TOTAL = 32e3;
|
|
791
|
+
var DefaultMemoryStore = class {
|
|
792
|
+
files;
|
|
1163
793
|
constructor(opts) {
|
|
1164
|
-
this.
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
794
|
+
this.files = {
|
|
795
|
+
"project-agents": opts.paths.inProjectAgentsFile,
|
|
796
|
+
"project-memory": opts.paths.projectMemory,
|
|
797
|
+
"user-memory": opts.paths.globalMemory
|
|
798
|
+
};
|
|
1168
799
|
}
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
const
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
800
|
+
async readAll() {
|
|
801
|
+
const parts = [];
|
|
802
|
+
for (const scope of ["project-agents", "project-memory", "user-memory"]) {
|
|
803
|
+
const body = await this.read(scope);
|
|
804
|
+
if (body.trim()) parts.push(`## ${labelOf(scope)}
|
|
805
|
+
|
|
806
|
+
${body.trim()}`);
|
|
807
|
+
}
|
|
808
|
+
return parts.join("\n\n");
|
|
1177
809
|
}
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
throw new Error("SecretVault: malformed encrypted value");
|
|
810
|
+
async read(scope) {
|
|
811
|
+
try {
|
|
812
|
+
return await fsp.readFile(this.files[scope], "utf8");
|
|
813
|
+
} catch {
|
|
814
|
+
return "";
|
|
1184
815
|
}
|
|
1185
|
-
const [ivB64, tagB64, ctB64] = parts;
|
|
1186
|
-
const iv = Buffer.from(ivB64, "base64");
|
|
1187
|
-
const tag = Buffer.from(tagB64, "base64");
|
|
1188
|
-
const ct = Buffer.from(ctB64, "base64");
|
|
1189
|
-
if (iv.length !== IV_BYTES) throw new Error("SecretVault: bad IV length");
|
|
1190
|
-
if (tag.length !== TAG_BYTES) throw new Error("SecretVault: bad tag length");
|
|
1191
|
-
const key = this.loadOrCreateKey();
|
|
1192
|
-
const decipher = createDecipheriv(ALGO, key, iv);
|
|
1193
|
-
decipher.setAuthTag(tag);
|
|
1194
|
-
const pt = Buffer.concat([decipher.update(ct), decipher.final()]);
|
|
1195
|
-
return pt.toString("utf8");
|
|
1196
816
|
}
|
|
1197
|
-
|
|
1198
|
-
|
|
817
|
+
async remember(text, scope = "project-memory") {
|
|
818
|
+
const file = this.files[scope];
|
|
819
|
+
await ensureDir(path2.dirname(file));
|
|
820
|
+
let existing = "";
|
|
1199
821
|
try {
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
throw new Error(`SecretVault: key file ${this.keyFile} has wrong size`);
|
|
1203
|
-
}
|
|
1204
|
-
this.key = buf;
|
|
1205
|
-
return this.key;
|
|
1206
|
-
} catch (err) {
|
|
1207
|
-
if (err.code !== "ENOENT") throw err;
|
|
822
|
+
existing = await fsp.readFile(file, "utf8");
|
|
823
|
+
} catch {
|
|
1208
824
|
}
|
|
1209
|
-
|
|
1210
|
-
const
|
|
825
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
826
|
+
const id = `mem_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
|
827
|
+
const entry = `
|
|
828
|
+
- [${ts}] ${id} ${text.replace(/\n/g, " ")}
|
|
829
|
+
`;
|
|
830
|
+
const next = existing.trim() ? existing.replace(/\n+$/, "") + entry : `# WrongStack Memory
|
|
831
|
+
${entry}`;
|
|
832
|
+
await atomicWrite(file, next);
|
|
833
|
+
const buf = Buffer.byteLength(next, "utf8");
|
|
834
|
+
if (buf > MAX_BYTES_TOTAL) {
|
|
835
|
+
await this.consolidate(scope);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
async forget(query, scope = "project-memory") {
|
|
839
|
+
const file = this.files[scope];
|
|
840
|
+
let existing;
|
|
1211
841
|
try {
|
|
1212
|
-
|
|
1213
|
-
} catch
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
842
|
+
existing = await fsp.readFile(file, "utf8");
|
|
843
|
+
} catch {
|
|
844
|
+
return 0;
|
|
845
|
+
}
|
|
846
|
+
const needle = query.toLowerCase();
|
|
847
|
+
const idMatcher = /mem_\d+_\w+/;
|
|
848
|
+
let removed = 0;
|
|
849
|
+
const lines = existing.split("\n").filter((line) => {
|
|
850
|
+
const trimmed = line.trim();
|
|
851
|
+
if (!trimmed.startsWith("- ")) return true;
|
|
852
|
+
if (idMatcher.test(query)) {
|
|
853
|
+
const afterBracket = trimmed.indexOf("] ");
|
|
854
|
+
if (afterBracket !== -1) {
|
|
855
|
+
const afterTs = trimmed.slice(afterBracket + 2);
|
|
856
|
+
const entryIdMatch = /^mem_\d+_\w+/.exec(afterTs);
|
|
857
|
+
if (entryIdMatch && entryIdMatch[0] === query) {
|
|
858
|
+
removed++;
|
|
859
|
+
return false;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
1218
862
|
}
|
|
1219
|
-
|
|
1220
|
-
|
|
863
|
+
if (trimmed.toLowerCase().includes(needle)) {
|
|
864
|
+
removed++;
|
|
865
|
+
return false;
|
|
866
|
+
}
|
|
867
|
+
return true;
|
|
868
|
+
});
|
|
869
|
+
if (removed > 0) {
|
|
870
|
+
await atomicWrite(file, lines.join("\n"));
|
|
1221
871
|
}
|
|
1222
|
-
|
|
1223
|
-
return key;
|
|
872
|
+
return removed;
|
|
1224
873
|
}
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
874
|
+
async consolidate(scope) {
|
|
875
|
+
const file = this.files[scope];
|
|
876
|
+
let existing;
|
|
877
|
+
try {
|
|
878
|
+
existing = await fsp.readFile(file, "utf8");
|
|
879
|
+
} catch {
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
const seen = /* @__PURE__ */ new Set();
|
|
883
|
+
const lines = existing.split("\n").filter((line) => {
|
|
884
|
+
const trimmed = line.trim();
|
|
885
|
+
if (!trimmed.startsWith("- ")) return true;
|
|
886
|
+
const norm = trimmed.replace(/\[[^\]]+\]/, "").replace(/\bmem_\d+_\w+\s*/, "").trim().toLowerCase();
|
|
887
|
+
if (seen.has(norm)) return false;
|
|
888
|
+
seen.add(norm);
|
|
889
|
+
return true;
|
|
890
|
+
});
|
|
891
|
+
const next = lines.join("\n");
|
|
892
|
+
try {
|
|
893
|
+
await atomicWrite(file, next);
|
|
894
|
+
} catch {
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
const backup = `${file}.bak.${Date.now()}`;
|
|
898
|
+
try {
|
|
899
|
+
await fsp.copyFile(file, backup);
|
|
900
|
+
} catch {
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
async clear(scope) {
|
|
904
|
+
if (scope) {
|
|
905
|
+
await atomicWrite(this.files[scope], "");
|
|
906
|
+
} else {
|
|
907
|
+
for (const s of ["project-agents", "project-memory", "user-memory"]) {
|
|
908
|
+
await atomicWrite(this.files[s], "");
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
};
|
|
913
|
+
function labelOf(scope) {
|
|
914
|
+
switch (scope) {
|
|
915
|
+
case "project-agents":
|
|
916
|
+
return "Project AGENTS.md";
|
|
917
|
+
case "project-memory":
|
|
918
|
+
return "Project memory";
|
|
919
|
+
case "user-memory":
|
|
920
|
+
return "User memory";
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// src/defaults/secret-scrubber.ts
|
|
925
|
+
var PATTERNS = [
|
|
926
|
+
// Anchored at the start where possible so partial matches inside larger
|
|
927
|
+
// strings don't trigger false positives.
|
|
928
|
+
{ type: "anthropic_key", regex: /(?<![A-Za-z0-9])sk-ant-api\d+-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g },
|
|
929
|
+
{ type: "openai_key", regex: /(?<![A-Za-z0-9])sk-(?:proj-)?[A-Za-z0-9_-]{20,}(?![A-Za-z0-9])/g },
|
|
930
|
+
{ type: "github_pat", regex: /(?<![A-Za-z0-9])ghp_[A-Za-z0-9]{36,}(?![A-Za-z0-9])/g },
|
|
931
|
+
{ type: "github_pat_v2", regex: /(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{50,}(?![A-Za-z0-9])/g },
|
|
932
|
+
{ type: "aws_access_key", regex: /(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}(?![A-Za-z0-9])/g },
|
|
933
|
+
{ type: "gcp_key", regex: /(?<![A-Za-z0-9])AIza[0-9A-Za-z_-]{35}(?![A-Za-z0-9])/g },
|
|
934
|
+
{ type: "slack_token", regex: /(?<![A-Za-z0-9-])xox[abpos]-[A-Za-z0-9-]{10,}(?![A-Za-z0-9-])/g },
|
|
935
|
+
{ type: "stripe_key", regex: /(?<![A-Za-z0-9])sk_(?:live|test)_[A-Za-z0-9]{24,}(?![A-Za-z0-9])/g },
|
|
936
|
+
{ type: "twilio_sid", regex: /(?<![A-Za-z0-9])AC[a-f0-9]{32}(?![A-Za-z0-9])/g },
|
|
937
|
+
{
|
|
938
|
+
type: "jwt",
|
|
939
|
+
// Anchored: look for literal "eyJ" which is unambiguous for JWT header
|
|
940
|
+
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
|
|
941
|
+
},
|
|
942
|
+
{
|
|
943
|
+
type: "private_key",
|
|
944
|
+
// Anchored: start must be BEGIN, end must be END with no extra dashes after END
|
|
945
|
+
regex: /(?:^|\n)-----BEGIN (?:RSA|EC|OPENSSH|DSA|PGP)? ?PRIVATE KEY-----[\s\S]*?-----END[^-]*-----(?:\n|$)/g
|
|
946
|
+
},
|
|
947
|
+
{ type: "mongodb_uri", regex: /mongodb(?:\+srv)?:\/\/[^\s"'`]+/g },
|
|
948
|
+
{ type: "postgres_uri", regex: /postgres(?:ql)?:\/\/[^\s"'`]+/g },
|
|
949
|
+
{ type: "mysql_uri", regex: /mysql:\/\/[^\s"'`]+/g },
|
|
950
|
+
{ type: "redis_uri", regex: /redis:\/\/[^\s"'`]+/g },
|
|
951
|
+
{ type: "bearer_token", regex: /(?<![A-Za-z0-9_.~+/-])Bearer\s+[A-Za-z0-9._~+/-]{20,}=*(?![A-Za-z0-9_.~+/-])/g },
|
|
952
|
+
{
|
|
953
|
+
type: "high_entropy_env",
|
|
954
|
+
// Value-side word boundary + length gate to avoid matching short random strings
|
|
955
|
+
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
|
|
956
|
+
}
|
|
957
|
+
];
|
|
958
|
+
var DefaultSecretScrubber = class {
|
|
959
|
+
scrub(text) {
|
|
960
|
+
if (!text) return text;
|
|
961
|
+
let out = text;
|
|
962
|
+
for (const p of PATTERNS) {
|
|
963
|
+
out = out.replace(p.regex, (_match, group1, group2) => {
|
|
964
|
+
if (p.type === "high_entropy_env" && group1 && group2) {
|
|
965
|
+
return `${group1}=[REDACTED:${p.type}]`;
|
|
966
|
+
}
|
|
967
|
+
return `[REDACTED:${p.type}]`;
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
return out;
|
|
971
|
+
}
|
|
972
|
+
scrubObject(obj) {
|
|
973
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
974
|
+
const visit = (v) => {
|
|
975
|
+
if (typeof v === "string") return this.scrub(v);
|
|
976
|
+
if (v === null || typeof v !== "object") return v;
|
|
977
|
+
if (seen.has(v)) return v;
|
|
978
|
+
seen.add(v);
|
|
979
|
+
if (Array.isArray(v)) return v.map(visit);
|
|
980
|
+
const out = {};
|
|
981
|
+
for (const [k, val] of Object.entries(v)) {
|
|
982
|
+
out[k] = visit(val);
|
|
983
|
+
}
|
|
984
|
+
return out;
|
|
985
|
+
};
|
|
986
|
+
return visit(obj);
|
|
987
|
+
}
|
|
988
|
+
};
|
|
989
|
+
|
|
990
|
+
// src/types/secret-vault.ts
|
|
991
|
+
var ENCRYPTED_PREFIX = "enc:v1:";
|
|
992
|
+
|
|
993
|
+
// src/defaults/secret-vault.ts
|
|
994
|
+
var KEY_BYTES = 32;
|
|
995
|
+
var IV_BYTES = 12;
|
|
996
|
+
var TAG_BYTES = 16;
|
|
997
|
+
var ALGO = "aes-256-gcm";
|
|
998
|
+
var DefaultSecretVault = class {
|
|
999
|
+
keyFile;
|
|
1000
|
+
key;
|
|
1001
|
+
constructor(opts) {
|
|
1002
|
+
this.keyFile = opts.keyFile;
|
|
1003
|
+
}
|
|
1004
|
+
isEncrypted(value) {
|
|
1005
|
+
return typeof value === "string" && value.startsWith(ENCRYPTED_PREFIX);
|
|
1006
|
+
}
|
|
1007
|
+
encrypt(plaintext) {
|
|
1008
|
+
if (this.isEncrypted(plaintext)) return plaintext;
|
|
1009
|
+
const key = this.loadOrCreateKey();
|
|
1010
|
+
const iv = randomBytes(IV_BYTES);
|
|
1011
|
+
const cipher = createCipheriv(ALGO, key, iv);
|
|
1012
|
+
const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
1013
|
+
const tag = cipher.getAuthTag();
|
|
1014
|
+
return `${ENCRYPTED_PREFIX}${iv.toString("base64")}:${tag.toString("base64")}:${ct.toString("base64")}`;
|
|
1015
|
+
}
|
|
1016
|
+
decrypt(value) {
|
|
1017
|
+
if (!this.isEncrypted(value)) return value;
|
|
1018
|
+
const rest = value.slice(ENCRYPTED_PREFIX.length);
|
|
1019
|
+
const parts = rest.split(":");
|
|
1020
|
+
if (parts.length !== 3) {
|
|
1021
|
+
throw new Error("SecretVault: malformed encrypted value");
|
|
1022
|
+
}
|
|
1023
|
+
const [ivB64, tagB64, ctB64] = parts;
|
|
1024
|
+
const iv = Buffer.from(ivB64, "base64");
|
|
1025
|
+
const tag = Buffer.from(tagB64, "base64");
|
|
1026
|
+
const ct = Buffer.from(ctB64, "base64");
|
|
1027
|
+
if (iv.length !== IV_BYTES) throw new Error("SecretVault: bad IV length");
|
|
1028
|
+
if (tag.length !== TAG_BYTES) throw new Error("SecretVault: bad tag length");
|
|
1029
|
+
const key = this.loadOrCreateKey();
|
|
1030
|
+
const decipher = createDecipheriv(ALGO, key, iv);
|
|
1031
|
+
decipher.setAuthTag(tag);
|
|
1032
|
+
const pt = Buffer.concat([decipher.update(ct), decipher.final()]);
|
|
1033
|
+
return pt.toString("utf8");
|
|
1034
|
+
}
|
|
1035
|
+
loadOrCreateKey() {
|
|
1036
|
+
if (this.key) return this.key;
|
|
1037
|
+
try {
|
|
1038
|
+
const buf = fs5.readFileSync(this.keyFile);
|
|
1039
|
+
if (buf.length !== KEY_BYTES) {
|
|
1040
|
+
throw new Error(`SecretVault: key file ${this.keyFile} has wrong size`);
|
|
1041
|
+
}
|
|
1042
|
+
this.key = buf;
|
|
1043
|
+
return this.key;
|
|
1044
|
+
} catch (err) {
|
|
1045
|
+
if (err.code !== "ENOENT") throw err;
|
|
1046
|
+
}
|
|
1047
|
+
fs5.mkdirSync(path2.dirname(this.keyFile), { recursive: true });
|
|
1048
|
+
const key = randomBytes(KEY_BYTES);
|
|
1049
|
+
try {
|
|
1050
|
+
fs5.writeFileSync(this.keyFile, key, { mode: 384, flag: "wx" });
|
|
1051
|
+
} catch (err) {
|
|
1052
|
+
if (err.code !== "EEXIST") throw err;
|
|
1053
|
+
const buf = fs5.readFileSync(this.keyFile);
|
|
1054
|
+
if (buf.length !== KEY_BYTES) {
|
|
1055
|
+
throw new Error(`SecretVault: key file ${this.keyFile} has wrong size`);
|
|
1056
|
+
}
|
|
1057
|
+
this.key = buf;
|
|
1058
|
+
return this.key;
|
|
1059
|
+
}
|
|
1060
|
+
this.key = key;
|
|
1061
|
+
return key;
|
|
1062
|
+
}
|
|
1063
|
+
};
|
|
1064
|
+
function decryptConfigSecrets(cfg, vault) {
|
|
1065
|
+
return walk(cfg, vault, (v) => vault.decrypt(v));
|
|
1066
|
+
}
|
|
1067
|
+
function encryptConfigSecrets(cfg, vault) {
|
|
1068
|
+
return walk(cfg, vault, (v) => vault.encrypt(v));
|
|
1069
|
+
}
|
|
1070
|
+
function walk(node, vault, transform) {
|
|
1071
|
+
if (node === null || node === void 0) return node;
|
|
1072
|
+
if (typeof node !== "object") return node;
|
|
1073
|
+
if (Array.isArray(node)) {
|
|
1074
|
+
return node.map((item) => walk(item, vault, transform));
|
|
1237
1075
|
}
|
|
1238
1076
|
const out = {};
|
|
1239
1077
|
for (const [k, v] of Object.entries(node)) {
|
|
@@ -1324,139 +1162,6 @@ function deepMerge(a, b) {
|
|
|
1324
1162
|
}
|
|
1325
1163
|
return out;
|
|
1326
1164
|
}
|
|
1327
|
-
var MAX_BYTES_TOTAL = 32e3;
|
|
1328
|
-
var DefaultMemoryStore = class {
|
|
1329
|
-
files;
|
|
1330
|
-
constructor(opts) {
|
|
1331
|
-
this.files = {
|
|
1332
|
-
"project-agents": opts.paths.inProjectAgentsFile,
|
|
1333
|
-
"project-memory": opts.paths.projectMemory,
|
|
1334
|
-
"user-memory": opts.paths.globalMemory
|
|
1335
|
-
};
|
|
1336
|
-
}
|
|
1337
|
-
async readAll() {
|
|
1338
|
-
const parts = [];
|
|
1339
|
-
for (const scope of ["project-agents", "project-memory", "user-memory"]) {
|
|
1340
|
-
const body = await this.read(scope);
|
|
1341
|
-
if (body.trim()) parts.push(`## ${labelOf(scope)}
|
|
1342
|
-
|
|
1343
|
-
${body.trim()}`);
|
|
1344
|
-
}
|
|
1345
|
-
return parts.join("\n\n");
|
|
1346
|
-
}
|
|
1347
|
-
async read(scope) {
|
|
1348
|
-
try {
|
|
1349
|
-
return await fsp.readFile(this.files[scope], "utf8");
|
|
1350
|
-
} catch {
|
|
1351
|
-
return "";
|
|
1352
|
-
}
|
|
1353
|
-
}
|
|
1354
|
-
async remember(text, scope = "project-memory") {
|
|
1355
|
-
const file = this.files[scope];
|
|
1356
|
-
await ensureDir(path2.dirname(file));
|
|
1357
|
-
let existing = "";
|
|
1358
|
-
try {
|
|
1359
|
-
existing = await fsp.readFile(file, "utf8");
|
|
1360
|
-
} catch {
|
|
1361
|
-
}
|
|
1362
|
-
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
1363
|
-
const id = `mem_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
|
1364
|
-
const entry = `
|
|
1365
|
-
- [${ts}] ${id} ${text.replace(/\n/g, " ")}
|
|
1366
|
-
`;
|
|
1367
|
-
const next = existing.trim() ? existing.replace(/\n+$/, "") + entry : `# WrongStack Memory
|
|
1368
|
-
${entry}`;
|
|
1369
|
-
await atomicWrite(file, next);
|
|
1370
|
-
const buf = Buffer.byteLength(next, "utf8");
|
|
1371
|
-
if (buf > MAX_BYTES_TOTAL) {
|
|
1372
|
-
await this.consolidate(scope);
|
|
1373
|
-
}
|
|
1374
|
-
}
|
|
1375
|
-
async forget(query, scope = "project-memory") {
|
|
1376
|
-
const file = this.files[scope];
|
|
1377
|
-
let existing;
|
|
1378
|
-
try {
|
|
1379
|
-
existing = await fsp.readFile(file, "utf8");
|
|
1380
|
-
} catch {
|
|
1381
|
-
return 0;
|
|
1382
|
-
}
|
|
1383
|
-
const needle = query.toLowerCase();
|
|
1384
|
-
const idMatcher = /mem_\d+_\w+/;
|
|
1385
|
-
let removed = 0;
|
|
1386
|
-
const lines = existing.split("\n").filter((line) => {
|
|
1387
|
-
const trimmed = line.trim();
|
|
1388
|
-
if (!trimmed.startsWith("- ")) return true;
|
|
1389
|
-
if (idMatcher.test(query)) {
|
|
1390
|
-
const afterBracket = trimmed.indexOf("] ");
|
|
1391
|
-
if (afterBracket !== -1) {
|
|
1392
|
-
const afterTs = trimmed.slice(afterBracket + 2);
|
|
1393
|
-
const entryIdMatch = /^mem_\d+_\w+/.exec(afterTs);
|
|
1394
|
-
if (entryIdMatch && entryIdMatch[0] === query) {
|
|
1395
|
-
removed++;
|
|
1396
|
-
return false;
|
|
1397
|
-
}
|
|
1398
|
-
}
|
|
1399
|
-
}
|
|
1400
|
-
if (trimmed.toLowerCase().includes(needle)) {
|
|
1401
|
-
removed++;
|
|
1402
|
-
return false;
|
|
1403
|
-
}
|
|
1404
|
-
return true;
|
|
1405
|
-
});
|
|
1406
|
-
if (removed > 0) {
|
|
1407
|
-
await atomicWrite(file, lines.join("\n"));
|
|
1408
|
-
}
|
|
1409
|
-
return removed;
|
|
1410
|
-
}
|
|
1411
|
-
async consolidate(scope) {
|
|
1412
|
-
const file = this.files[scope];
|
|
1413
|
-
let existing;
|
|
1414
|
-
try {
|
|
1415
|
-
existing = await fsp.readFile(file, "utf8");
|
|
1416
|
-
} catch {
|
|
1417
|
-
return;
|
|
1418
|
-
}
|
|
1419
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1420
|
-
const lines = existing.split("\n").filter((line) => {
|
|
1421
|
-
const trimmed = line.trim();
|
|
1422
|
-
if (!trimmed.startsWith("- ")) return true;
|
|
1423
|
-
const norm = trimmed.replace(/\[[^\]]+\]/, "").replace(/\bmem_\d+_\w+\s*/, "").trim().toLowerCase();
|
|
1424
|
-
if (seen.has(norm)) return false;
|
|
1425
|
-
seen.add(norm);
|
|
1426
|
-
return true;
|
|
1427
|
-
});
|
|
1428
|
-
const next = lines.join("\n");
|
|
1429
|
-
try {
|
|
1430
|
-
await atomicWrite(file, next);
|
|
1431
|
-
} catch {
|
|
1432
|
-
return;
|
|
1433
|
-
}
|
|
1434
|
-
const backup = `${file}.bak.${Date.now()}`;
|
|
1435
|
-
try {
|
|
1436
|
-
await fsp.copyFile(file, backup);
|
|
1437
|
-
} catch {
|
|
1438
|
-
}
|
|
1439
|
-
}
|
|
1440
|
-
async clear(scope) {
|
|
1441
|
-
if (scope) {
|
|
1442
|
-
await atomicWrite(this.files[scope], "");
|
|
1443
|
-
} else {
|
|
1444
|
-
for (const s of ["project-agents", "project-memory", "user-memory"]) {
|
|
1445
|
-
await atomicWrite(this.files[s], "");
|
|
1446
|
-
}
|
|
1447
|
-
}
|
|
1448
|
-
}
|
|
1449
|
-
};
|
|
1450
|
-
function labelOf(scope) {
|
|
1451
|
-
switch (scope) {
|
|
1452
|
-
case "project-agents":
|
|
1453
|
-
return "Project AGENTS.md";
|
|
1454
|
-
case "project-memory":
|
|
1455
|
-
return "Project memory";
|
|
1456
|
-
case "user-memory":
|
|
1457
|
-
return "User memory";
|
|
1458
|
-
}
|
|
1459
|
-
}
|
|
1460
1165
|
|
|
1461
1166
|
// src/utils/glob-match.ts
|
|
1462
1167
|
function escapeRegex(s) {
|
|
@@ -1632,118 +1337,363 @@ var DefaultPermissionPolicy = class {
|
|
|
1632
1337
|
return void 0;
|
|
1633
1338
|
}
|
|
1634
1339
|
};
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1340
|
+
|
|
1341
|
+
// src/types/errors.ts
|
|
1342
|
+
var WrongStackError = class extends Error {
|
|
1343
|
+
code;
|
|
1344
|
+
subsystem;
|
|
1345
|
+
severity;
|
|
1346
|
+
recoverable;
|
|
1347
|
+
context;
|
|
1638
1348
|
constructor(opts) {
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1349
|
+
super(opts.message, { cause: opts.cause });
|
|
1350
|
+
this.name = "WrongStackError";
|
|
1351
|
+
this.code = opts.code;
|
|
1352
|
+
this.subsystem = opts.subsystem;
|
|
1353
|
+
this.severity = opts.severity ?? "error";
|
|
1354
|
+
this.recoverable = opts.recoverable ?? false;
|
|
1355
|
+
this.context = opts.context;
|
|
1646
1356
|
}
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
for (const e of entries) {
|
|
1655
|
-
if (!e.isDirectory()) continue;
|
|
1656
|
-
const skillFile = path2.join(dir, e.name, "SKILL.md");
|
|
1657
|
-
try {
|
|
1658
|
-
const raw = await fsp.readFile(skillFile, "utf8");
|
|
1659
|
-
const meta = parseFrontmatter(raw);
|
|
1660
|
-
if (!meta.name || !meta.description) continue;
|
|
1661
|
-
if (seen.has(meta.name)) continue;
|
|
1662
|
-
seen.add(meta.name);
|
|
1663
|
-
found.push({
|
|
1664
|
-
name: meta.name,
|
|
1665
|
-
description: meta.description,
|
|
1666
|
-
version: meta.version,
|
|
1667
|
-
path: skillFile,
|
|
1668
|
-
source
|
|
1669
|
-
});
|
|
1670
|
-
} catch {
|
|
1671
|
-
}
|
|
1672
|
-
}
|
|
1673
|
-
} catch {
|
|
1674
|
-
}
|
|
1675
|
-
}
|
|
1676
|
-
this.cache = found;
|
|
1677
|
-
return found;
|
|
1357
|
+
/**
|
|
1358
|
+
* Render a one-line user-facing description.
|
|
1359
|
+
* Subclasses should override for domain-specific formatting.
|
|
1360
|
+
*/
|
|
1361
|
+
describe() {
|
|
1362
|
+
const ctx = this.context ? ` ${formatContext(this.context)}` : "";
|
|
1363
|
+
return `${this.code}: ${this.message}${ctx}`;
|
|
1678
1364
|
}
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1365
|
+
};
|
|
1366
|
+
function formatContext(ctx) {
|
|
1367
|
+
const parts = Object.entries(ctx).filter(([, v]) => v !== void 0).slice(0, 3).map(([k, v]) => `${k}=${String(v)}`);
|
|
1368
|
+
return parts.length > 0 ? `[${parts.join(" ")}]` : "";
|
|
1369
|
+
}
|
|
1370
|
+
var AgentError = class extends WrongStackError {
|
|
1371
|
+
constructor(opts) {
|
|
1372
|
+
super({
|
|
1373
|
+
message: opts.message,
|
|
1374
|
+
code: opts.code,
|
|
1375
|
+
subsystem: "agent",
|
|
1376
|
+
severity: opts.code === "AGENT_ABORTED" ? "warning" : "error",
|
|
1377
|
+
recoverable: opts.recoverable ?? opts.code === "AGENT_ITERATION_LIMIT",
|
|
1378
|
+
context: opts.context,
|
|
1379
|
+
cause: opts.cause
|
|
1380
|
+
});
|
|
1381
|
+
this.name = "AgentError";
|
|
1682
1382
|
}
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1383
|
+
};
|
|
1384
|
+
function toWrongStackError(err, code = "AGENT_RUN_FAILED") {
|
|
1385
|
+
if (err instanceof WrongStackError) return err;
|
|
1386
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1387
|
+
return new AgentError({
|
|
1388
|
+
message,
|
|
1389
|
+
code: code === "UNKNOWN" ? "AGENT_RUN_FAILED" : code,
|
|
1390
|
+
cause: err
|
|
1391
|
+
});
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
// src/types/provider.ts
|
|
1395
|
+
var ProviderError = class extends WrongStackError {
|
|
1396
|
+
status;
|
|
1397
|
+
retryable;
|
|
1398
|
+
providerId;
|
|
1399
|
+
body;
|
|
1400
|
+
constructor(message, status, retryable, providerId, opts = {}) {
|
|
1401
|
+
super({
|
|
1402
|
+
message,
|
|
1403
|
+
code: providerStatusToCode(status, opts.body?.type),
|
|
1404
|
+
subsystem: "provider",
|
|
1405
|
+
severity: status >= 500 ? "error" : "warning",
|
|
1406
|
+
recoverable: retryable,
|
|
1407
|
+
context: { providerId, status },
|
|
1408
|
+
cause: opts.cause
|
|
1409
|
+
});
|
|
1410
|
+
this.name = "ProviderError";
|
|
1411
|
+
this.status = status;
|
|
1412
|
+
this.retryable = retryable;
|
|
1413
|
+
this.providerId = providerId;
|
|
1414
|
+
this.body = opts.body;
|
|
1692
1415
|
}
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1416
|
+
/**
|
|
1417
|
+
* Render a one-line, user-facing description. Designed for the CLI/TUI
|
|
1418
|
+
* status line and the agent's retry warning. Avoids dumping raw JSON
|
|
1419
|
+
* (which is what users see today when a 529 lands and the log message
|
|
1420
|
+
* includes the full `{"type":"error",...}` body).
|
|
1421
|
+
*
|
|
1422
|
+
* Examples:
|
|
1423
|
+
* "minimax-coding-plan overloaded (529): High traffic detected. Upgrade for highspeed model. [req 06534785201de9c0…]"
|
|
1424
|
+
* "openai rate limited (429): Retry after 12s"
|
|
1425
|
+
* "anthropic invalid request (400): messages.0.role must be one of 'user'|'assistant'"
|
|
1426
|
+
* "groq HTTP 500 (server error)"
|
|
1427
|
+
*/
|
|
1428
|
+
describe() {
|
|
1429
|
+
const kind = describeStatus(this.status, this.body?.type);
|
|
1430
|
+
const head = `${this.providerId} ${kind}`;
|
|
1431
|
+
const detail = this.body?.message?.trim();
|
|
1432
|
+
const reqId = this.body?.requestId ? ` [req ${this.body.requestId.slice(0, 16)}${this.body.requestId.length > 16 ? "\u2026" : ""}]` : "";
|
|
1433
|
+
if (detail && detail.length > 0) {
|
|
1434
|
+
return `${head}: ${truncate(detail, 240)}${reqId}`;
|
|
1435
|
+
}
|
|
1436
|
+
return `${head}${reqId}`;
|
|
1697
1437
|
}
|
|
1698
1438
|
};
|
|
1699
|
-
function
|
|
1700
|
-
if (
|
|
1701
|
-
|
|
1702
|
-
if (
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1439
|
+
function describeStatus(status, type) {
|
|
1440
|
+
if (status === 0) return "network error";
|
|
1441
|
+
if (type === "overloaded_error" || status === 529) return `overloaded (${status})`;
|
|
1442
|
+
if (type === "rate_limit_error" || status === 429) return `rate limited (${status})`;
|
|
1443
|
+
if (type === "authentication_error" || status === 401) return `auth failed (${status})`;
|
|
1444
|
+
if (type === "permission_error" || status === 403) return `forbidden (${status})`;
|
|
1445
|
+
if (type === "not_found_error" || status === 404) return `not found (${status})`;
|
|
1446
|
+
if (type === "invalid_request_error" || status === 400) return `invalid request (${status})`;
|
|
1447
|
+
if (status === 408) return `timeout (${status})`;
|
|
1448
|
+
if (status >= 500 && status < 600) return `HTTP ${status} (server error)`;
|
|
1449
|
+
if (type) return `${type} (${status})`;
|
|
1450
|
+
return `HTTP ${status}`;
|
|
1451
|
+
}
|
|
1452
|
+
function truncate(s, n) {
|
|
1453
|
+
return s.length <= n ? s : `${s.slice(0, n - 1)}\u2026`;
|
|
1454
|
+
}
|
|
1455
|
+
function providerStatusToCode(status, type) {
|
|
1456
|
+
if (status === 0) return "PROVIDER_NETWORK_ERROR";
|
|
1457
|
+
if (type === "rate_limit_error" || status === 429) return "PROVIDER_RATE_LIMITED";
|
|
1458
|
+
if (type === "authentication_error" || status === 401) return "PROVIDER_AUTH_FAILED";
|
|
1459
|
+
if (type === "overloaded_error" || status === 529) return "PROVIDER_OVERLOADED";
|
|
1460
|
+
if (type === "invalid_request_error" || status === 400) return "PROVIDER_INVALID_REQUEST";
|
|
1461
|
+
if (status === 408) return "PROVIDER_NETWORK_ERROR";
|
|
1462
|
+
if (status >= 500) return "PROVIDER_SERVER_ERROR";
|
|
1463
|
+
return "PROVIDER_INVALID_REQUEST";
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
// src/defaults/retry-policy.ts
|
|
1467
|
+
var DefaultRetryPolicy = class {
|
|
1468
|
+
shouldRetry(err, attempt) {
|
|
1469
|
+
if (err instanceof ProviderError) {
|
|
1470
|
+
if (!err.retryable) return false;
|
|
1471
|
+
return attempt < this.maxAttempts(err);
|
|
1710
1472
|
}
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
value = [];
|
|
1723
|
-
} else if (rest) {
|
|
1724
|
-
value = [rest];
|
|
1725
|
-
} else {
|
|
1726
|
-
value = [];
|
|
1727
|
-
}
|
|
1728
|
-
} else if (key) {
|
|
1729
|
-
value.push(line.replace(/^\s+/, ""));
|
|
1473
|
+
const msg = err.message ?? "";
|
|
1474
|
+
const isNetwork = /ECONN|ETIMEDOUT|ETIME|ENOTFOUND|EAI_AGAIN|fetch failed/i.test(msg);
|
|
1475
|
+
if (isNetwork) return attempt < 2;
|
|
1476
|
+
return false;
|
|
1477
|
+
}
|
|
1478
|
+
maxAttempts(err) {
|
|
1479
|
+
if (err instanceof ProviderError) {
|
|
1480
|
+
if (err.status === 429) return 5;
|
|
1481
|
+
if (err.status === 529) return 3;
|
|
1482
|
+
if (err.status >= 500) return 3;
|
|
1483
|
+
return 0;
|
|
1730
1484
|
}
|
|
1485
|
+
return 2;
|
|
1731
1486
|
}
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1487
|
+
delayMs(attempt) {
|
|
1488
|
+
const base = 1e3;
|
|
1489
|
+
const exp = base * 2 ** attempt;
|
|
1490
|
+
const jitter = Math.random() * base;
|
|
1491
|
+
return Math.min(3e4, exp + jitter);
|
|
1492
|
+
}
|
|
1493
|
+
};
|
|
1494
|
+
|
|
1495
|
+
// src/defaults/error-handler.ts
|
|
1496
|
+
function buildRecoveryStrategies(opts) {
|
|
1497
|
+
return [
|
|
1498
|
+
{
|
|
1499
|
+
label: "context_overflow_reduce",
|
|
1500
|
+
compactor: opts?.compactor,
|
|
1501
|
+
async attempt(err, ctx) {
|
|
1502
|
+
if (err instanceof ProviderError && (err.status === 413 || /context|too long|tokens/i.test(err.message))) {
|
|
1503
|
+
if (this.compactor) {
|
|
1504
|
+
try {
|
|
1505
|
+
const report = await this.compactor.compact(ctx, { aggressive: true });
|
|
1506
|
+
if (report.after < report.before) {
|
|
1507
|
+
return {
|
|
1508
|
+
content: [{ type: "text", text: "[context compacted automatically \u2014 please retry]" }],
|
|
1509
|
+
stopReason: "end_turn",
|
|
1510
|
+
usage: { input: 0, output: 0 },
|
|
1511
|
+
model: ctx.model
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1514
|
+
} catch {
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
return null;
|
|
1518
|
+
}
|
|
1519
|
+
return null;
|
|
1520
|
+
}
|
|
1521
|
+
},
|
|
1522
|
+
{
|
|
1523
|
+
label: "rate_limit_backoff",
|
|
1524
|
+
async attempt(err, ctx) {
|
|
1525
|
+
if (err instanceof ProviderError && err.status === 429) {
|
|
1526
|
+
const delayMs = err.body?.retryAfterMs ?? 5e3;
|
|
1527
|
+
const delay = Math.max(1e3, Math.min(delayMs, 6e4));
|
|
1528
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
1529
|
+
return {
|
|
1530
|
+
content: [{ type: "text", text: "[rate limit backoff applied \u2014 please retry]" }],
|
|
1531
|
+
stopReason: "end_turn",
|
|
1532
|
+
usage: { input: 0, output: 0 },
|
|
1533
|
+
model: ctx.model
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1536
|
+
return null;
|
|
1537
|
+
}
|
|
1538
|
+
},
|
|
1539
|
+
{
|
|
1540
|
+
label: "downgrade_model",
|
|
1541
|
+
async attempt(err, ctx) {
|
|
1542
|
+
if (err instanceof ProviderError && (err.status === 429 || err.status === 529 || err.status >= 500)) {
|
|
1543
|
+
return null;
|
|
1544
|
+
}
|
|
1545
|
+
return null;
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
];
|
|
1549
|
+
}
|
|
1550
|
+
var DEFAULT_RECOVERY_STRATEGIES = buildRecoveryStrategies();
|
|
1551
|
+
var DefaultErrorHandler = class {
|
|
1552
|
+
strategies;
|
|
1553
|
+
constructor(strategies = DEFAULT_RECOVERY_STRATEGIES) {
|
|
1554
|
+
this.strategies = strategies;
|
|
1555
|
+
}
|
|
1556
|
+
classify(err) {
|
|
1557
|
+
if (err instanceof DOMException && err.name === "AbortError") {
|
|
1558
|
+
return { kind: "abort", retryable: false };
|
|
1559
|
+
}
|
|
1560
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
1561
|
+
return { kind: "abort", retryable: false };
|
|
1562
|
+
}
|
|
1563
|
+
if (err instanceof ProviderError) {
|
|
1564
|
+
if (err.status === 429) return { kind: "rate_limit", retryable: true };
|
|
1565
|
+
if (err.status === 529) return { kind: "overloaded", retryable: true };
|
|
1566
|
+
if (err.status >= 500) return { kind: "server", retryable: true };
|
|
1567
|
+
if (err.status === 413 || /context|too long|tokens/i.test(err.message)) {
|
|
1568
|
+
return { kind: "context_overflow", retryable: false };
|
|
1569
|
+
}
|
|
1570
|
+
if (err.status >= 400) return { kind: "client", retryable: false };
|
|
1571
|
+
}
|
|
1572
|
+
if (err instanceof Error && /ECONN|ETIMEDOUT|ETIME|ENOTFOUND|EAI_AGAIN|fetch failed/i.test(err.message)) {
|
|
1573
|
+
return { kind: "network", retryable: true };
|
|
1574
|
+
}
|
|
1575
|
+
return { kind: "unknown", retryable: false };
|
|
1576
|
+
}
|
|
1577
|
+
async recover(err, ctx) {
|
|
1578
|
+
for (const strategy of this.strategies) {
|
|
1579
|
+
const result = await strategy.attempt(err, ctx);
|
|
1580
|
+
if (result !== null) return result;
|
|
1581
|
+
}
|
|
1582
|
+
return null;
|
|
1583
|
+
}
|
|
1584
|
+
};
|
|
1585
|
+
var DefaultSkillLoader = class {
|
|
1586
|
+
dirs;
|
|
1587
|
+
cache;
|
|
1588
|
+
constructor(opts) {
|
|
1589
|
+
this.dirs = [
|
|
1590
|
+
{ dir: opts.paths.inProjectSkills, source: "project" },
|
|
1591
|
+
{ dir: opts.paths.globalSkills, source: "user" }
|
|
1592
|
+
];
|
|
1593
|
+
if (opts.bundledDir) {
|
|
1594
|
+
this.dirs.push({ dir: opts.bundledDir, source: "bundled" });
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
async list() {
|
|
1598
|
+
if (this.cache) return this.cache;
|
|
1599
|
+
const found = [];
|
|
1600
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1601
|
+
for (const { dir, source } of this.dirs) {
|
|
1602
|
+
try {
|
|
1603
|
+
const entries = await fsp.readdir(dir, { withFileTypes: true });
|
|
1604
|
+
for (const e of entries) {
|
|
1605
|
+
if (!e.isDirectory()) continue;
|
|
1606
|
+
const skillFile = path2.join(dir, e.name, "SKILL.md");
|
|
1607
|
+
try {
|
|
1608
|
+
const raw = await fsp.readFile(skillFile, "utf8");
|
|
1609
|
+
const meta = parseFrontmatter(raw);
|
|
1610
|
+
if (!meta.name || !meta.description) continue;
|
|
1611
|
+
if (seen.has(meta.name)) continue;
|
|
1612
|
+
seen.add(meta.name);
|
|
1613
|
+
found.push({
|
|
1614
|
+
name: meta.name,
|
|
1615
|
+
description: meta.description,
|
|
1616
|
+
version: meta.version,
|
|
1617
|
+
path: skillFile,
|
|
1618
|
+
source
|
|
1619
|
+
});
|
|
1620
|
+
} catch {
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
} catch {
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
this.cache = found;
|
|
1627
|
+
return found;
|
|
1628
|
+
}
|
|
1629
|
+
async find(name) {
|
|
1630
|
+
const all = await this.list();
|
|
1631
|
+
return all.find((s) => s.name === name);
|
|
1632
|
+
}
|
|
1633
|
+
async manifestText() {
|
|
1634
|
+
const skills = await this.list();
|
|
1635
|
+
if (skills.length === 0) return "";
|
|
1636
|
+
const lines = ["## Available skills"];
|
|
1637
|
+
for (const s of skills) {
|
|
1638
|
+
lines.push(`- **${s.name}** \u2014 ${s.description.replace(/\n/g, " ").trim()}`);
|
|
1639
|
+
lines.push(` Path: ${s.path}`);
|
|
1640
|
+
}
|
|
1641
|
+
return lines.join("\n");
|
|
1642
|
+
}
|
|
1643
|
+
async readBody(name) {
|
|
1644
|
+
const m = await this.find(name);
|
|
1645
|
+
if (!m) throw new Error(`Skill "${name}" not found`);
|
|
1646
|
+
return fsp.readFile(m.path, "utf8");
|
|
1647
|
+
}
|
|
1648
|
+
};
|
|
1649
|
+
function parseFrontmatter(raw) {
|
|
1650
|
+
if (!raw.startsWith("---")) return {};
|
|
1651
|
+
const end = raw.indexOf("\n---", 4);
|
|
1652
|
+
if (end === -1) return {};
|
|
1653
|
+
const block = raw.slice(4, end);
|
|
1654
|
+
const out = {};
|
|
1655
|
+
let key = null;
|
|
1656
|
+
let value = [];
|
|
1657
|
+
const flush = () => {
|
|
1658
|
+
if (key) {
|
|
1659
|
+
out[key] = value.join("\n").trim();
|
|
1660
|
+
}
|
|
1661
|
+
key = null;
|
|
1662
|
+
value = [];
|
|
1663
|
+
};
|
|
1664
|
+
for (const line of block.split("\n")) {
|
|
1665
|
+
const m = /^([a-zA-Z_]+):\s*(\|?)\s*(.*)$/.exec(line);
|
|
1666
|
+
if (m) {
|
|
1667
|
+
flush();
|
|
1668
|
+
key = m[1] ?? "";
|
|
1669
|
+
const pipe = m[2];
|
|
1670
|
+
const rest = m[3] ?? "";
|
|
1671
|
+
if (pipe === "|") {
|
|
1672
|
+
value = [];
|
|
1673
|
+
} else if (rest) {
|
|
1674
|
+
value = [rest];
|
|
1675
|
+
} else {
|
|
1676
|
+
value = [];
|
|
1677
|
+
}
|
|
1678
|
+
} else if (key) {
|
|
1679
|
+
value.push(line.replace(/^\s+/, ""));
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
flush();
|
|
1683
|
+
return out;
|
|
1684
|
+
}
|
|
1685
|
+
var BEHAVIOR_DEFAULTS = {
|
|
1686
|
+
version: 1,
|
|
1687
|
+
context: {
|
|
1688
|
+
warnThreshold: 0.6,
|
|
1689
|
+
softThreshold: 0.75,
|
|
1690
|
+
hardThreshold: 0.9,
|
|
1691
|
+
autoCompact: true,
|
|
1692
|
+
preserveK: 10,
|
|
1693
|
+
eliseThreshold: 2e3
|
|
1694
|
+
},
|
|
1695
|
+
tools: {
|
|
1696
|
+
defaultExecutionStrategy: "smart",
|
|
1747
1697
|
maxIterations: 100,
|
|
1748
1698
|
iterationTimeoutMs: 3e5,
|
|
1749
1699
|
sessionTimeoutMs: 18e5,
|
|
@@ -1849,6 +1799,20 @@ var DefaultConfigLoader = class {
|
|
|
1849
1799
|
if (this.vault) {
|
|
1850
1800
|
cfg = decryptConfigSecrets(cfg, this.vault);
|
|
1851
1801
|
}
|
|
1802
|
+
if (cfg.providers) {
|
|
1803
|
+
for (const pcfg of Object.values(cfg.providers)) {
|
|
1804
|
+
if (!pcfg || typeof pcfg !== "object") continue;
|
|
1805
|
+
const keys = pcfg.apiKeys;
|
|
1806
|
+
if (!Array.isArray(keys) || keys.length === 0) continue;
|
|
1807
|
+
const existing = pcfg.apiKey;
|
|
1808
|
+
if (existing && existing.length > 0) continue;
|
|
1809
|
+
const activeLabel = pcfg.activeKey;
|
|
1810
|
+
const chosen = activeLabel ? keys.find((k) => k.label === activeLabel) ?? keys[0] : keys[0];
|
|
1811
|
+
if (chosen?.apiKey) {
|
|
1812
|
+
pcfg.apiKey = chosen.apiKey;
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1852
1816
|
this.validateBehavior(cfg);
|
|
1853
1817
|
if (this.strict) this.validateIdentity(cfg);
|
|
1854
1818
|
return Object.freeze(cfg);
|
|
@@ -1885,6 +1849,132 @@ var DefaultConfigLoader = class {
|
|
|
1885
1849
|
}
|
|
1886
1850
|
};
|
|
1887
1851
|
|
|
1852
|
+
// src/defaults/config-store.ts
|
|
1853
|
+
var DefaultConfigStore = class {
|
|
1854
|
+
current;
|
|
1855
|
+
watchers = /* @__PURE__ */ new Set();
|
|
1856
|
+
constructor(initial) {
|
|
1857
|
+
this.current = deepFreeze(structuredClone(initial));
|
|
1858
|
+
}
|
|
1859
|
+
get() {
|
|
1860
|
+
return this.current;
|
|
1861
|
+
}
|
|
1862
|
+
getSection(key) {
|
|
1863
|
+
return this.current[key];
|
|
1864
|
+
}
|
|
1865
|
+
getExtension(pluginName) {
|
|
1866
|
+
const ext = this.current.extensions?.[pluginName];
|
|
1867
|
+
return ext ? ext : FROZEN_EMPTY;
|
|
1868
|
+
}
|
|
1869
|
+
update(partial) {
|
|
1870
|
+
const next = deepFreeze(
|
|
1871
|
+
structuredClone({ ...this.current, ...partial })
|
|
1872
|
+
);
|
|
1873
|
+
if (next.version !== 1) {
|
|
1874
|
+
throw new Error(`ConfigStore.update: version must remain 1, got ${String(next.version)}`);
|
|
1875
|
+
}
|
|
1876
|
+
const prev = this.current;
|
|
1877
|
+
this.current = next;
|
|
1878
|
+
for (const w of this.watchers) {
|
|
1879
|
+
try {
|
|
1880
|
+
w(next, prev);
|
|
1881
|
+
} catch {
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
return next;
|
|
1885
|
+
}
|
|
1886
|
+
watch(cb) {
|
|
1887
|
+
this.watchers.add(cb);
|
|
1888
|
+
return () => this.watchers.delete(cb);
|
|
1889
|
+
}
|
|
1890
|
+
};
|
|
1891
|
+
var FROZEN_EMPTY = Object.freeze({});
|
|
1892
|
+
function deepFreeze(obj) {
|
|
1893
|
+
if (obj === null || typeof obj !== "object") return obj;
|
|
1894
|
+
if (Object.isFrozen(obj)) return obj;
|
|
1895
|
+
for (const key of Object.keys(obj)) {
|
|
1896
|
+
const v = obj[key];
|
|
1897
|
+
if (v !== null && typeof v === "object" && !Object.isFrozen(v)) {
|
|
1898
|
+
deepFreeze(v);
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
return Object.freeze(obj);
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
// src/defaults/config-migration.ts
|
|
1905
|
+
var ConfigMigrationError = class extends Error {
|
|
1906
|
+
fromVersion;
|
|
1907
|
+
targetVersion;
|
|
1908
|
+
missingStep;
|
|
1909
|
+
constructor(opts) {
|
|
1910
|
+
super(opts.message);
|
|
1911
|
+
this.name = "ConfigMigrationError";
|
|
1912
|
+
this.fromVersion = opts.fromVersion;
|
|
1913
|
+
this.targetVersion = opts.targetVersion;
|
|
1914
|
+
this.missingStep = opts.missingStep;
|
|
1915
|
+
}
|
|
1916
|
+
};
|
|
1917
|
+
function runConfigMigrations(input, targetVersion, migrations) {
|
|
1918
|
+
const initial = typeof input["version"] === "number" ? input["version"] : 1;
|
|
1919
|
+
let current = { ...input };
|
|
1920
|
+
let currentVersion = initial;
|
|
1921
|
+
const applied = [];
|
|
1922
|
+
let shouldPersist = false;
|
|
1923
|
+
let guard = 0;
|
|
1924
|
+
while (currentVersion !== targetVersion) {
|
|
1925
|
+
if (++guard > 100) {
|
|
1926
|
+
throw new ConfigMigrationError({
|
|
1927
|
+
message: `Config migration looped past 100 steps (from v${initial} toward v${targetVersion})`,
|
|
1928
|
+
fromVersion: initial,
|
|
1929
|
+
targetVersion,
|
|
1930
|
+
missingStep: currentVersion
|
|
1931
|
+
});
|
|
1932
|
+
}
|
|
1933
|
+
const step = migrations.find((m) => m.from === currentVersion);
|
|
1934
|
+
if (!step) {
|
|
1935
|
+
throw new ConfigMigrationError({
|
|
1936
|
+
message: `No migration registered from config v${currentVersion} (target v${targetVersion}). Update the framework or revert the config file.`,
|
|
1937
|
+
fromVersion: initial,
|
|
1938
|
+
targetVersion,
|
|
1939
|
+
missingStep: currentVersion
|
|
1940
|
+
});
|
|
1941
|
+
}
|
|
1942
|
+
const ctx = { fromVersion: currentVersion, shouldPersist: false };
|
|
1943
|
+
const next = step.migrate(current, ctx);
|
|
1944
|
+
if (typeof next["version"] !== "number" || next["version"] !== step.to) {
|
|
1945
|
+
next["version"] = step.to;
|
|
1946
|
+
}
|
|
1947
|
+
current = next;
|
|
1948
|
+
currentVersion = step.to;
|
|
1949
|
+
applied.push(`v${step.from}\u2192v${step.to}`);
|
|
1950
|
+
shouldPersist = shouldPersist || ctx.shouldPersist || step.from < step.to;
|
|
1951
|
+
}
|
|
1952
|
+
return { config: current, applied, shouldPersist };
|
|
1953
|
+
}
|
|
1954
|
+
var DEFAULT_CONFIG_MIGRATIONS = [];
|
|
1955
|
+
|
|
1956
|
+
// src/utils/token-estimate.ts
|
|
1957
|
+
var RoughTokenEstimate = (text) => Math.max(1, Math.ceil(text.length / 4));
|
|
1958
|
+
function estimateToolInputTokens(input) {
|
|
1959
|
+
if (typeof input === "string") return RoughTokenEstimate(input);
|
|
1960
|
+
if (input !== null && typeof input === "object" && "__tokenEstimate" in input) {
|
|
1961
|
+
return input.__tokenEstimate;
|
|
1962
|
+
}
|
|
1963
|
+
const str = typeof input === "object" ? JSON.stringify(input) : String(input);
|
|
1964
|
+
const estimate = RoughTokenEstimate(str);
|
|
1965
|
+
if (input !== null && typeof input === "object" && !Array.isArray(input)) {
|
|
1966
|
+
input.__tokenEstimate = estimate;
|
|
1967
|
+
}
|
|
1968
|
+
return estimate;
|
|
1969
|
+
}
|
|
1970
|
+
function estimateToolResultTokens(content) {
|
|
1971
|
+
if (typeof content === "string") return RoughTokenEstimate(content);
|
|
1972
|
+
return RoughTokenEstimate(JSON.stringify(content));
|
|
1973
|
+
}
|
|
1974
|
+
function estimateTextTokens(text) {
|
|
1975
|
+
return RoughTokenEstimate(text);
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1888
1978
|
// src/defaults/compactor.ts
|
|
1889
1979
|
var HybridCompactor = class {
|
|
1890
1980
|
preserveK;
|
|
@@ -1893,7 +1983,7 @@ var HybridCompactor = class {
|
|
|
1893
1983
|
constructor(opts = {}) {
|
|
1894
1984
|
this.preserveK = opts.preserveK ?? 10;
|
|
1895
1985
|
this.eliseThreshold = opts.eliseThreshold ?? 2e3;
|
|
1896
|
-
this.estimator = opts.estimator ??
|
|
1986
|
+
this.estimator = opts.estimator ?? estimateTextTokens;
|
|
1897
1987
|
}
|
|
1898
1988
|
async compact(ctx, opts = {}) {
|
|
1899
1989
|
const beforeTokens = this.estimateMessages(ctx.messages);
|
|
@@ -1925,8 +2015,7 @@ var HybridCompactor = class {
|
|
|
1925
2015
|
if (!msg || !Array.isArray(msg.content)) continue;
|
|
1926
2016
|
const newContent = msg.content.map((b) => {
|
|
1927
2017
|
if (b.type !== "tool_result") return b;
|
|
1928
|
-
const
|
|
1929
|
-
const tokens = this.estimator(text);
|
|
2018
|
+
const tokens = estimateToolResultTokens(b.content);
|
|
1930
2019
|
if (tokens < this.eliseThreshold) return b;
|
|
1931
2020
|
saved += tokens;
|
|
1932
2021
|
const elided = {
|
|
@@ -1964,23 +2053,20 @@ var HybridCompactor = class {
|
|
|
1964
2053
|
},
|
|
1965
2054
|
{ role: "assistant", content: "Continuing from compacted context." }
|
|
1966
2055
|
];
|
|
1967
|
-
ctx.messages.
|
|
2056
|
+
const tail = ctx.messages.slice(boundary);
|
|
2057
|
+
ctx.state.replaceMessages([...summary, ...tail]);
|
|
1968
2058
|
return Math.max(0, removedTokens - this.estimateMessages(summary));
|
|
1969
2059
|
}
|
|
1970
2060
|
estimateMessages(messages) {
|
|
1971
2061
|
let total = 0;
|
|
1972
2062
|
for (const m of messages) {
|
|
1973
2063
|
if (typeof m.content === "string") {
|
|
1974
|
-
total +=
|
|
2064
|
+
total += estimateTextTokens(m.content);
|
|
1975
2065
|
} else {
|
|
1976
2066
|
for (const b of m.content) {
|
|
1977
|
-
if (b.type === "text") total +=
|
|
1978
|
-
else if (b.type === "tool_use") total +=
|
|
1979
|
-
else if (b.type === "tool_result")
|
|
1980
|
-
total += this.estimator(
|
|
1981
|
-
typeof b.content === "string" ? b.content : JSON.stringify(b.content)
|
|
1982
|
-
);
|
|
1983
|
-
}
|
|
2067
|
+
if (b.type === "text") total += estimateTextTokens(b.text);
|
|
2068
|
+
else if (b.type === "tool_use") total += estimateToolInputTokens(b.input);
|
|
2069
|
+
else if (b.type === "tool_result") total += estimateToolResultTokens(b.content);
|
|
1984
2070
|
}
|
|
1985
2071
|
}
|
|
1986
2072
|
}
|
|
@@ -1991,9 +2077,6 @@ function hasTextContent(m) {
|
|
|
1991
2077
|
if (typeof m.content === "string") return m.content.trim().length > 0;
|
|
1992
2078
|
return m.content.some((b) => b.type === "text" && b.text.trim().length > 0);
|
|
1993
2079
|
}
|
|
1994
|
-
function roughTokenEstimate(text) {
|
|
1995
|
-
return Math.max(1, Math.ceil(text.length / 4));
|
|
1996
|
-
}
|
|
1997
2080
|
|
|
1998
2081
|
// src/types/blocks.ts
|
|
1999
2082
|
function isTextBlock(b) {
|
|
@@ -2058,7 +2141,8 @@ var IntelligentCompactor = class {
|
|
|
2058
2141
|
content: `[prior_turns_summary: ${summaryText}]`
|
|
2059
2142
|
};
|
|
2060
2143
|
const summaryTokens = this.estimateTokens([summaryMsg]);
|
|
2061
|
-
ctx.messages.
|
|
2144
|
+
const tail = ctx.messages.slice(boundary);
|
|
2145
|
+
ctx.state.replaceMessages([summaryMsg, ...tail]);
|
|
2062
2146
|
return Math.max(0, removedTokens - summaryTokens);
|
|
2063
2147
|
}
|
|
2064
2148
|
findSafeBoundary(messages, from, to) {
|
|
@@ -2139,8 +2223,7 @@ var IntelligentCompactor = class {
|
|
|
2139
2223
|
if (!msg || !Array.isArray(msg.content)) continue;
|
|
2140
2224
|
const newContent = msg.content.map((b) => {
|
|
2141
2225
|
if (b.type !== "tool_result") return b;
|
|
2142
|
-
const
|
|
2143
|
-
const tokens = this.roughTokenEstimate(text);
|
|
2226
|
+
const tokens = estimateToolResultTokens(b.content);
|
|
2144
2227
|
if (tokens < this.eliseThreshold) return b;
|
|
2145
2228
|
saved += tokens;
|
|
2146
2229
|
return {
|
|
@@ -2162,24 +2245,17 @@ var IntelligentCompactor = class {
|
|
|
2162
2245
|
let total = 0;
|
|
2163
2246
|
for (const m of messages) {
|
|
2164
2247
|
if (typeof m.content === "string") {
|
|
2165
|
-
total +=
|
|
2248
|
+
total += estimateTextTokens(m.content);
|
|
2166
2249
|
} else {
|
|
2167
2250
|
for (const b of m.content) {
|
|
2168
|
-
if (b.type === "text") total +=
|
|
2169
|
-
else if (b.type === "tool_use") total +=
|
|
2170
|
-
else if (b.type === "tool_result")
|
|
2171
|
-
total += this.roughTokenEstimate(
|
|
2172
|
-
typeof b.content === "string" ? b.content : JSON.stringify(b.content)
|
|
2173
|
-
);
|
|
2174
|
-
}
|
|
2251
|
+
if (b.type === "text") total += estimateTextTokens(b.text);
|
|
2252
|
+
else if (b.type === "tool_use") total += estimateToolInputTokens(b.input);
|
|
2253
|
+
else if (b.type === "tool_result") total += estimateToolResultTokens(b.content);
|
|
2175
2254
|
}
|
|
2176
2255
|
}
|
|
2177
2256
|
}
|
|
2178
2257
|
return total;
|
|
2179
2258
|
}
|
|
2180
|
-
roughTokenEstimate(text) {
|
|
2181
|
-
return Math.max(1, Math.ceil(text.length / 4));
|
|
2182
|
-
}
|
|
2183
2259
|
};
|
|
2184
2260
|
|
|
2185
2261
|
// src/defaults/llm-selector.ts
|
|
@@ -2402,8 +2478,8 @@ var SelectiveCompactor = class {
|
|
|
2402
2478
|
* insert summaries where the selector provided them.
|
|
2403
2479
|
*/
|
|
2404
2480
|
async executePlan(ctx, plan) {
|
|
2405
|
-
|
|
2406
|
-
|
|
2481
|
+
if (ctx.messages.length === 0) return;
|
|
2482
|
+
const messages = [...ctx.messages];
|
|
2407
2483
|
const sortedCollapsed = [...plan.collapsed].sort((a, b) => b.from - a.from);
|
|
2408
2484
|
for (const range of sortedCollapsed) {
|
|
2409
2485
|
if (range.from < 0 || range.to >= messages.length || range.from > range.to) continue;
|
|
@@ -2418,6 +2494,7 @@ var SelectiveCompactor = class {
|
|
|
2418
2494
|
};
|
|
2419
2495
|
messages.splice(range.from, range.to - range.from + 1, summaryMsg);
|
|
2420
2496
|
}
|
|
2497
|
+
ctx.state.replaceMessages(messages);
|
|
2421
2498
|
}
|
|
2422
2499
|
async summarizeRange(messages, ctx) {
|
|
2423
2500
|
const systemText = `${this.summarizerPrompt}
|
|
@@ -2464,7 +2541,8 @@ Summarize the following message range:`;
|
|
|
2464
2541
|
role: "system",
|
|
2465
2542
|
content: `[${removed.length} earlier turns trimmed \u2014 see session log for details]`
|
|
2466
2543
|
};
|
|
2467
|
-
messages.
|
|
2544
|
+
const tail = messages.slice(boundary);
|
|
2545
|
+
ctx.state.replaceMessages([summaryMsg, ...tail]);
|
|
2468
2546
|
return Math.max(0, removedTokens - this.estimateTokens([summaryMsg]));
|
|
2469
2547
|
}
|
|
2470
2548
|
computeTargetBudget(load, aggressive) {
|
|
@@ -2991,44 +3069,128 @@ async function loadUserModes(modesDir) {
|
|
|
2991
3069
|
}
|
|
2992
3070
|
return modes;
|
|
2993
3071
|
}
|
|
3072
|
+
|
|
3073
|
+
// src/defaults/subagent-budget.ts
|
|
3074
|
+
var BudgetExceededError = class extends Error {
|
|
3075
|
+
kind;
|
|
3076
|
+
limit;
|
|
3077
|
+
observed;
|
|
3078
|
+
constructor(kind, limit, observed) {
|
|
3079
|
+
super(`Budget exceeded: ${kind} (limit=${limit}, observed=${observed})`);
|
|
3080
|
+
this.name = "BudgetExceededError";
|
|
3081
|
+
this.kind = kind;
|
|
3082
|
+
this.limit = limit;
|
|
3083
|
+
this.observed = observed;
|
|
3084
|
+
}
|
|
3085
|
+
};
|
|
3086
|
+
var SubagentBudget = class {
|
|
3087
|
+
limits;
|
|
3088
|
+
iterations = 0;
|
|
3089
|
+
toolCalls = 0;
|
|
3090
|
+
tokenInput = 0;
|
|
3091
|
+
tokenOutput = 0;
|
|
3092
|
+
costUsd = 0;
|
|
3093
|
+
startTime = null;
|
|
3094
|
+
constructor(limits = {}) {
|
|
3095
|
+
this.limits = Object.freeze({ ...limits });
|
|
3096
|
+
}
|
|
3097
|
+
start() {
|
|
3098
|
+
this.startTime = Date.now();
|
|
3099
|
+
}
|
|
3100
|
+
recordIteration() {
|
|
3101
|
+
this.iterations++;
|
|
3102
|
+
if (this.limits.maxIterations !== void 0 && this.iterations > this.limits.maxIterations) {
|
|
3103
|
+
throw new BudgetExceededError("iterations", this.limits.maxIterations, this.iterations);
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
recordToolCall() {
|
|
3107
|
+
this.toolCalls++;
|
|
3108
|
+
if (this.limits.maxToolCalls !== void 0 && this.toolCalls > this.limits.maxToolCalls) {
|
|
3109
|
+
throw new BudgetExceededError("tool_calls", this.limits.maxToolCalls, this.toolCalls);
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
recordUsage(usage, costUsd = 0) {
|
|
3113
|
+
this.tokenInput += usage.input;
|
|
3114
|
+
this.tokenOutput += usage.output;
|
|
3115
|
+
this.costUsd += costUsd;
|
|
3116
|
+
const totalTokens = this.tokenInput + this.tokenOutput;
|
|
3117
|
+
if (this.limits.maxTokens !== void 0 && totalTokens > this.limits.maxTokens) {
|
|
3118
|
+
throw new BudgetExceededError("tokens", this.limits.maxTokens, totalTokens);
|
|
3119
|
+
}
|
|
3120
|
+
if (this.limits.maxCostUsd !== void 0 && this.costUsd > this.limits.maxCostUsd) {
|
|
3121
|
+
throw new BudgetExceededError("cost", this.limits.maxCostUsd, this.costUsd);
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
/**
|
|
3125
|
+
* Throws if the wall-clock budget is exhausted. Call this from the iteration
|
|
3126
|
+
* loop so a hung tool can't keep a subagent running past its deadline.
|
|
3127
|
+
*/
|
|
3128
|
+
checkTimeout() {
|
|
3129
|
+
if (this.startTime === null || this.limits.timeoutMs === void 0) return;
|
|
3130
|
+
const elapsed = Date.now() - this.startTime;
|
|
3131
|
+
if (elapsed > this.limits.timeoutMs) {
|
|
3132
|
+
throw new BudgetExceededError("timeout", this.limits.timeoutMs, elapsed);
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
/** Returns true if a timeout has occurred without throwing. Useful for races. */
|
|
3136
|
+
isTimedOut() {
|
|
3137
|
+
if (this.startTime === null || this.limits.timeoutMs === void 0) return false;
|
|
3138
|
+
return Date.now() - this.startTime > this.limits.timeoutMs;
|
|
3139
|
+
}
|
|
3140
|
+
usage() {
|
|
3141
|
+
return {
|
|
3142
|
+
iterations: this.iterations,
|
|
3143
|
+
toolCalls: this.toolCalls,
|
|
3144
|
+
tokens: {
|
|
3145
|
+
input: this.tokenInput,
|
|
3146
|
+
output: this.tokenOutput,
|
|
3147
|
+
total: this.tokenInput + this.tokenOutput
|
|
3148
|
+
},
|
|
3149
|
+
costUsd: this.costUsd,
|
|
3150
|
+
elapsedMs: this.startTime === null ? 0 : Date.now() - this.startTime
|
|
3151
|
+
};
|
|
3152
|
+
}
|
|
3153
|
+
};
|
|
3154
|
+
|
|
3155
|
+
// src/defaults/multi-agent-coordinator.ts
|
|
2994
3156
|
var DefaultMultiAgentCoordinator = class extends EventEmitter {
|
|
2995
3157
|
coordinatorId;
|
|
2996
3158
|
config;
|
|
3159
|
+
runner;
|
|
2997
3160
|
subagents = /* @__PURE__ */ new Map();
|
|
2998
3161
|
pendingTasks = [];
|
|
2999
3162
|
completedResults = [];
|
|
3000
3163
|
totalIterations = 0;
|
|
3001
|
-
|
|
3164
|
+
inFlight = 0;
|
|
3165
|
+
constructor(config, options = {}) {
|
|
3002
3166
|
super();
|
|
3003
3167
|
this.coordinatorId = config.coordinatorId;
|
|
3004
3168
|
this.config = config;
|
|
3169
|
+
this.runner = options.runner;
|
|
3005
3170
|
}
|
|
3006
3171
|
async spawn(subagent) {
|
|
3007
3172
|
const id = subagent.id || randomUUID();
|
|
3008
3173
|
const context = {
|
|
3009
3174
|
subagentId: id,
|
|
3010
3175
|
tasks: [],
|
|
3176
|
+
// parentBridge: wired by the caller via setSubagentBridge() once the
|
|
3177
|
+
// bidirectional bridge is created. Reads gated by hasParentBridge().
|
|
3011
3178
|
parentBridge: null,
|
|
3012
3179
|
doneCondition: this.config.doneCondition,
|
|
3013
3180
|
maxConcurrent: this.config.maxConcurrent ?? 4
|
|
3014
3181
|
};
|
|
3015
3182
|
this.subagents.set(id, {
|
|
3016
|
-
config: subagent,
|
|
3183
|
+
config: { ...subagent, id },
|
|
3017
3184
|
context,
|
|
3018
|
-
status: "idle"
|
|
3185
|
+
status: "idle",
|
|
3186
|
+
abortController: new AbortController()
|
|
3019
3187
|
});
|
|
3020
3188
|
this.emit("subagent.started", { subagent: { ...subagent, id } });
|
|
3021
|
-
return {
|
|
3022
|
-
subagentId: id,
|
|
3023
|
-
agentId: id
|
|
3024
|
-
};
|
|
3189
|
+
return { subagentId: id, agentId: id };
|
|
3025
3190
|
}
|
|
3026
3191
|
async assign(task) {
|
|
3027
3192
|
this.pendingTasks.push(task);
|
|
3028
|
-
|
|
3029
|
-
if (available) {
|
|
3030
|
-
await this.dispatch(available, task);
|
|
3031
|
-
}
|
|
3193
|
+
this.tryDispatchNext();
|
|
3032
3194
|
}
|
|
3033
3195
|
async delegate(to, msg) {
|
|
3034
3196
|
const subagent = this.subagents.get(to);
|
|
@@ -3039,8 +3201,8 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
|
|
|
3039
3201
|
await subagent.context.parentBridge.send(msg);
|
|
3040
3202
|
}
|
|
3041
3203
|
/**
|
|
3042
|
-
* Wire up the communication bridge for a subagent. Call
|
|
3043
|
-
*
|
|
3204
|
+
* Wire up the communication bridge for a subagent. Call after spawn() once
|
|
3205
|
+
* the caller has created the bidirectional connection.
|
|
3044
3206
|
*/
|
|
3045
3207
|
setSubagentBridge(subagentId, bridge) {
|
|
3046
3208
|
const subagent = this.subagents.get(subagentId);
|
|
@@ -3050,6 +3212,7 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
|
|
|
3050
3212
|
async stop(subagentId) {
|
|
3051
3213
|
const subagent = this.subagents.get(subagentId);
|
|
3052
3214
|
if (!subagent) return;
|
|
3215
|
+
subagent.abortController.abort();
|
|
3053
3216
|
subagent.status = "stopped";
|
|
3054
3217
|
subagent.currentTask = void 0;
|
|
3055
3218
|
subagent.context.parentBridge = null;
|
|
@@ -3075,71 +3238,234 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
|
|
|
3075
3238
|
done: this.isDone()
|
|
3076
3239
|
};
|
|
3077
3240
|
}
|
|
3078
|
-
|
|
3241
|
+
/** Expose snapshot of completed results — useful for callers awaiting all done. */
|
|
3242
|
+
results() {
|
|
3243
|
+
return this.completedResults;
|
|
3244
|
+
}
|
|
3245
|
+
/**
|
|
3246
|
+
* Manual completion — for callers that drive subagents without a runner
|
|
3247
|
+
* (e.g. external orchestrators). When a runner is configured the coordinator
|
|
3248
|
+
* calls this itself.
|
|
3249
|
+
*/
|
|
3250
|
+
completeTask(result) {
|
|
3251
|
+
this.recordCompletion(result);
|
|
3252
|
+
}
|
|
3253
|
+
// --- internal dispatching ---------------------------------------------
|
|
3254
|
+
tryDispatchNext() {
|
|
3255
|
+
while (this.canDispatch()) {
|
|
3256
|
+
const subagentId = this.findIdleSubagent();
|
|
3257
|
+
if (!subagentId) return;
|
|
3258
|
+
const task = this.pendingTasks.shift();
|
|
3259
|
+
if (!task) return;
|
|
3260
|
+
void this.runDispatched(subagentId, task);
|
|
3261
|
+
}
|
|
3262
|
+
}
|
|
3263
|
+
canDispatch() {
|
|
3264
|
+
const max = this.config.maxConcurrent ?? 4;
|
|
3265
|
+
return this.inFlight < max && this.pendingTasks.length > 0;
|
|
3266
|
+
}
|
|
3267
|
+
findIdleSubagent() {
|
|
3079
3268
|
for (const [id, s] of this.subagents) {
|
|
3080
3269
|
if (s.status === "idle") return id;
|
|
3081
3270
|
}
|
|
3082
3271
|
return null;
|
|
3083
3272
|
}
|
|
3084
|
-
async
|
|
3273
|
+
async runDispatched(subagentId, task) {
|
|
3085
3274
|
const subagent = this.subagents.get(subagentId);
|
|
3086
3275
|
if (!subagent) return;
|
|
3087
3276
|
subagent.status = "running";
|
|
3088
3277
|
subagent.currentTask = task.id;
|
|
3089
3278
|
task.subagentId = subagentId;
|
|
3090
3279
|
subagent.context.tasks.push(task);
|
|
3091
|
-
|
|
3092
|
-
|
|
3280
|
+
this.inFlight++;
|
|
3281
|
+
this.emit("task.assigned", { task, subagentId });
|
|
3282
|
+
const budget = new SubagentBudget({
|
|
3283
|
+
maxIterations: subagent.config.maxIterations ?? this.config.defaultBudget?.maxIterations,
|
|
3284
|
+
maxToolCalls: task.maxToolCalls ?? subagent.config.maxToolCalls ?? this.config.defaultBudget?.maxToolCalls,
|
|
3285
|
+
maxTokens: subagent.config.maxTokens ?? this.config.defaultBudget?.maxTokens,
|
|
3286
|
+
maxCostUsd: subagent.config.maxCostUsd ?? this.config.defaultBudget?.maxCostUsd,
|
|
3287
|
+
timeoutMs: task.timeoutMs ?? subagent.config.timeoutMs ?? this.config.defaultBudget?.timeoutMs
|
|
3288
|
+
});
|
|
3289
|
+
subagent.activeBudget = budget;
|
|
3290
|
+
const startTime = Date.now();
|
|
3291
|
+
const runCtx = {
|
|
3292
|
+
subagentId,
|
|
3293
|
+
config: subagent.config,
|
|
3294
|
+
budget,
|
|
3295
|
+
signal: subagent.abortController.signal,
|
|
3296
|
+
bridge: subagent.context.parentBridge || null
|
|
3297
|
+
};
|
|
3298
|
+
let result;
|
|
3299
|
+
if (!this.runner) {
|
|
3093
3300
|
return;
|
|
3094
3301
|
}
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3302
|
+
budget.start();
|
|
3303
|
+
try {
|
|
3304
|
+
const outcome = await this.executeWithTimeout(this.runner, task, runCtx, budget);
|
|
3305
|
+
result = {
|
|
3306
|
+
subagentId,
|
|
3307
|
+
taskId: task.id,
|
|
3308
|
+
status: "success",
|
|
3309
|
+
result: outcome.result,
|
|
3310
|
+
iterations: outcome.iterations,
|
|
3311
|
+
toolCalls: outcome.toolCalls,
|
|
3312
|
+
durationMs: Date.now() - startTime
|
|
3313
|
+
};
|
|
3314
|
+
} catch (err) {
|
|
3315
|
+
const status = err instanceof BudgetExceededError && err.kind === "timeout" ? "timeout" : subagent.abortController.signal.aborted ? "stopped" : "failed";
|
|
3316
|
+
result = {
|
|
3317
|
+
subagentId,
|
|
3318
|
+
taskId: task.id,
|
|
3319
|
+
status,
|
|
3320
|
+
error: err instanceof Error ? err.message : String(err),
|
|
3321
|
+
iterations: budget.usage().iterations,
|
|
3322
|
+
toolCalls: budget.usage().toolCalls,
|
|
3323
|
+
durationMs: Date.now() - startTime
|
|
3324
|
+
};
|
|
3108
3325
|
}
|
|
3109
|
-
|
|
3110
|
-
|
|
3326
|
+
this.recordCompletion(result);
|
|
3327
|
+
}
|
|
3328
|
+
async executeWithTimeout(runner, task, ctx, budget) {
|
|
3329
|
+
const timeoutMs = budget.limits.timeoutMs;
|
|
3330
|
+
if (timeoutMs === void 0) return runner(task, ctx);
|
|
3331
|
+
let timer = null;
|
|
3332
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
3333
|
+
timer = setTimeout(() => {
|
|
3334
|
+
this.subagents.get(ctx.subagentId)?.abortController.abort();
|
|
3335
|
+
reject(new BudgetExceededError("timeout", timeoutMs, Date.now()));
|
|
3336
|
+
}, timeoutMs);
|
|
3337
|
+
});
|
|
3338
|
+
try {
|
|
3339
|
+
return await Promise.race([runner(task, ctx), timeoutPromise]);
|
|
3340
|
+
} finally {
|
|
3341
|
+
if (timer) clearTimeout(timer);
|
|
3111
3342
|
}
|
|
3112
|
-
return false;
|
|
3113
3343
|
}
|
|
3114
|
-
|
|
3344
|
+
recordCompletion(result) {
|
|
3115
3345
|
this.completedResults.push(result);
|
|
3116
3346
|
this.totalIterations += result.iterations;
|
|
3347
|
+
this.inFlight = Math.max(0, this.inFlight - 1);
|
|
3117
3348
|
const subagent = this.subagents.get(result.subagentId);
|
|
3118
|
-
if (subagent) {
|
|
3119
|
-
subagent.status = "idle";
|
|
3349
|
+
if (subagent && subagent.status !== "stopped") {
|
|
3350
|
+
subagent.status = result.status === "failed" || result.status === "timeout" ? "error" : "idle";
|
|
3120
3351
|
subagent.currentTask = void 0;
|
|
3352
|
+
if (subagent.status === "error") {
|
|
3353
|
+
queueMicrotask(() => {
|
|
3354
|
+
if (subagent.status === "error") subagent.status = "idle";
|
|
3355
|
+
this.tryDispatchNext();
|
|
3356
|
+
});
|
|
3357
|
+
}
|
|
3121
3358
|
}
|
|
3122
3359
|
this.emit("task.completed", {
|
|
3123
|
-
task:
|
|
3360
|
+
task: subagent?.context.tasks.find((t) => t.id === result.taskId) ?? { id: result.taskId },
|
|
3124
3361
|
result
|
|
3125
3362
|
});
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
if (available) {
|
|
3129
|
-
const nextTask = this.pendingTasks.shift();
|
|
3130
|
-
this.dispatch(available, nextTask);
|
|
3131
|
-
}
|
|
3132
|
-
} else if (this.isDone()) {
|
|
3363
|
+
this.tryDispatchNext();
|
|
3364
|
+
if (this.isDone()) {
|
|
3133
3365
|
this.emit("done", {
|
|
3134
3366
|
results: this.completedResults,
|
|
3135
3367
|
totalIterations: this.totalIterations
|
|
3136
3368
|
});
|
|
3137
3369
|
}
|
|
3138
3370
|
}
|
|
3371
|
+
isDone() {
|
|
3372
|
+
if (this.config.doneCondition.type === "all_tasks_done") {
|
|
3373
|
+
return this.pendingTasks.length === 0 && this.inFlight === 0;
|
|
3374
|
+
}
|
|
3375
|
+
if (this.config.doneCondition.maxIterations !== void 0 && this.totalIterations >= this.config.doneCondition.maxIterations) {
|
|
3376
|
+
return true;
|
|
3377
|
+
}
|
|
3378
|
+
return false;
|
|
3379
|
+
}
|
|
3139
3380
|
};
|
|
3381
|
+
|
|
3382
|
+
// src/defaults/agent-subagent-runner.ts
|
|
3383
|
+
function makeAgentSubagentRunner(opts) {
|
|
3384
|
+
const format = opts.formatTaskInput ?? defaultFormatTaskInput;
|
|
3385
|
+
return async (task, ctx) => {
|
|
3386
|
+
const { agent, events } = await opts.factory(ctx.config);
|
|
3387
|
+
const aborter = new AbortController();
|
|
3388
|
+
const onBudgetError = (err) => {
|
|
3389
|
+
if (err instanceof BudgetExceededError) {
|
|
3390
|
+
aborter.abort();
|
|
3391
|
+
budgetError = err;
|
|
3392
|
+
} else {
|
|
3393
|
+
throw err;
|
|
3394
|
+
}
|
|
3395
|
+
};
|
|
3396
|
+
let budgetError = null;
|
|
3397
|
+
const unsub = [];
|
|
3398
|
+
unsub.push(
|
|
3399
|
+
events.on("tool.started", () => {
|
|
3400
|
+
try {
|
|
3401
|
+
ctx.budget.recordToolCall();
|
|
3402
|
+
} catch (e) {
|
|
3403
|
+
onBudgetError(e);
|
|
3404
|
+
}
|
|
3405
|
+
}),
|
|
3406
|
+
events.on("provider.response", (e) => {
|
|
3407
|
+
try {
|
|
3408
|
+
ctx.budget.recordUsage(e.usage);
|
|
3409
|
+
} catch (e2) {
|
|
3410
|
+
onBudgetError(e2);
|
|
3411
|
+
}
|
|
3412
|
+
}),
|
|
3413
|
+
events.on("iteration.started", () => {
|
|
3414
|
+
try {
|
|
3415
|
+
ctx.budget.recordIteration();
|
|
3416
|
+
ctx.budget.checkTimeout();
|
|
3417
|
+
} catch (e) {
|
|
3418
|
+
onBudgetError(e);
|
|
3419
|
+
}
|
|
3420
|
+
})
|
|
3421
|
+
);
|
|
3422
|
+
const onParentAbort = () => aborter.abort();
|
|
3423
|
+
ctx.signal.addEventListener("abort", onParentAbort);
|
|
3424
|
+
let result;
|
|
3425
|
+
try {
|
|
3426
|
+
result = await agent.run(format(task, ctx.config), { signal: aborter.signal });
|
|
3427
|
+
} finally {
|
|
3428
|
+
ctx.signal.removeEventListener("abort", onParentAbort);
|
|
3429
|
+
for (const u of unsub) u();
|
|
3430
|
+
}
|
|
3431
|
+
if (budgetError) throw budgetError;
|
|
3432
|
+
if (result.status === "failed") {
|
|
3433
|
+
throw result.error instanceof Error ? result.error : new Error(String(result.error ?? "agent failed"));
|
|
3434
|
+
}
|
|
3435
|
+
if (result.status === "aborted") {
|
|
3436
|
+
throw new Error("agent aborted");
|
|
3437
|
+
}
|
|
3438
|
+
if (result.status === "max_iterations") {
|
|
3439
|
+
throw new Error("agent exhausted iteration limit");
|
|
3440
|
+
}
|
|
3441
|
+
const usage = ctx.budget.usage();
|
|
3442
|
+
return {
|
|
3443
|
+
result: result.finalText,
|
|
3444
|
+
iterations: result.iterations,
|
|
3445
|
+
toolCalls: usage.toolCalls
|
|
3446
|
+
};
|
|
3447
|
+
};
|
|
3448
|
+
}
|
|
3449
|
+
function defaultFormatTaskInput(task) {
|
|
3450
|
+
return task.description ?? "";
|
|
3451
|
+
}
|
|
3452
|
+
|
|
3453
|
+
// src/defaults/transport/in-memory-transport.ts
|
|
3140
3454
|
var InMemoryBridgeTransport = class {
|
|
3141
3455
|
subs = /* @__PURE__ */ new Map();
|
|
3142
3456
|
send(msg, to) {
|
|
3457
|
+
if (to === "*") {
|
|
3458
|
+
for (const [id, handlers2] of this.subs) {
|
|
3459
|
+
if (id === msg.from) continue;
|
|
3460
|
+
for (const h of handlers2) {
|
|
3461
|
+
try {
|
|
3462
|
+
h(msg);
|
|
3463
|
+
} catch {
|
|
3464
|
+
}
|
|
3465
|
+
}
|
|
3466
|
+
}
|
|
3467
|
+
return Promise.resolve();
|
|
3468
|
+
}
|
|
3143
3469
|
const handlers = this.subs.get(to);
|
|
3144
3470
|
if (handlers) {
|
|
3145
3471
|
for (const h of handlers) {
|
|
@@ -3161,6 +3487,8 @@ var InMemoryBridgeTransport = class {
|
|
|
3161
3487
|
return Promise.resolve();
|
|
3162
3488
|
}
|
|
3163
3489
|
};
|
|
3490
|
+
|
|
3491
|
+
// src/defaults/agent-bridge.ts
|
|
3164
3492
|
var InMemoryAgentBridge = class {
|
|
3165
3493
|
agentId;
|
|
3166
3494
|
coordinatorId;
|
|
@@ -3327,7 +3655,7 @@ var AutonomousRunner = class {
|
|
|
3327
3655
|
if (e.message.includes("timeout")) {
|
|
3328
3656
|
const timeoutResult = {
|
|
3329
3657
|
status: "failed",
|
|
3330
|
-
error: e,
|
|
3658
|
+
error: toWrongStackError(e),
|
|
3331
3659
|
iterations: this.iterations,
|
|
3332
3660
|
toolCalls: this.toolCalls,
|
|
3333
3661
|
reason: "iteration timeout"
|
|
@@ -4133,251 +4461,1354 @@ var TaskFlow = class {
|
|
|
4133
4461
|
});
|
|
4134
4462
|
}
|
|
4135
4463
|
};
|
|
4136
|
-
var SpecDrivenDev = class {
|
|
4137
|
-
store;
|
|
4138
|
-
tracker;
|
|
4139
|
-
events;
|
|
4140
|
-
flows = /* @__PURE__ */ new Map();
|
|
4141
|
-
constructor(opts) {
|
|
4142
|
-
this.store = new DefaultTaskStore();
|
|
4143
|
-
this.tracker = new TaskTracker({ store: this.store });
|
|
4144
|
-
this.events = opts.events;
|
|
4464
|
+
var SpecDrivenDev = class {
|
|
4465
|
+
store;
|
|
4466
|
+
tracker;
|
|
4467
|
+
events;
|
|
4468
|
+
flows = /* @__PURE__ */ new Map();
|
|
4469
|
+
constructor(opts) {
|
|
4470
|
+
this.store = new DefaultTaskStore();
|
|
4471
|
+
this.tracker = new TaskTracker({ store: this.store });
|
|
4472
|
+
this.events = opts.events;
|
|
4473
|
+
}
|
|
4474
|
+
async createFlow(specContent, options) {
|
|
4475
|
+
const flow = new TaskFlow({
|
|
4476
|
+
tracker: this.tracker,
|
|
4477
|
+
events: this.events,
|
|
4478
|
+
...options
|
|
4479
|
+
});
|
|
4480
|
+
const graph = await flow.fromSpec(specContent);
|
|
4481
|
+
this.flows.set(graph.id, flow);
|
|
4482
|
+
return flow;
|
|
4483
|
+
}
|
|
4484
|
+
getTracker() {
|
|
4485
|
+
return this.tracker;
|
|
4486
|
+
}
|
|
4487
|
+
getFlow(graphId) {
|
|
4488
|
+
return this.flows.get(graphId);
|
|
4489
|
+
}
|
|
4490
|
+
listFlows() {
|
|
4491
|
+
return Array.from(this.flows.entries()).map(([id, flow]) => ({
|
|
4492
|
+
id,
|
|
4493
|
+
title: flow.getGraph()?.title ?? "Untitled",
|
|
4494
|
+
phase: flow.getPhase()
|
|
4495
|
+
}));
|
|
4496
|
+
}
|
|
4497
|
+
};
|
|
4498
|
+
var LOCK_FILE = "active.json";
|
|
4499
|
+
var DEFAULT_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
4500
|
+
var RecoveryLock = class {
|
|
4501
|
+
file;
|
|
4502
|
+
pid;
|
|
4503
|
+
hostname;
|
|
4504
|
+
maxAgeMs;
|
|
4505
|
+
sessionStore;
|
|
4506
|
+
probe;
|
|
4507
|
+
constructor(opts) {
|
|
4508
|
+
this.file = path2.join(opts.dir, LOCK_FILE);
|
|
4509
|
+
this.pid = opts.pid ?? process.pid;
|
|
4510
|
+
this.hostname = opts.hostname ?? os.hostname();
|
|
4511
|
+
this.maxAgeMs = opts.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
|
|
4512
|
+
this.sessionStore = opts.sessionStore;
|
|
4513
|
+
this.probe = opts.isPidAlive ?? defaultIsPidAlive;
|
|
4514
|
+
}
|
|
4515
|
+
/**
|
|
4516
|
+
* Examine the lockfile and decide whether it represents an abandoned
|
|
4517
|
+
* session. Returns `null` if the file is missing, points to a live
|
|
4518
|
+
* instance, references a clean-closed session, is too old, or is
|
|
4519
|
+
* malformed. Otherwise returns enough detail to prompt the user.
|
|
4520
|
+
*
|
|
4521
|
+
* Important: this is a read-only check. We never delete an active
|
|
4522
|
+
* lock from here — if another wstack instance is alive, the caller
|
|
4523
|
+
* should bail or run with a fresh session instead.
|
|
4524
|
+
*/
|
|
4525
|
+
async checkAbandoned() {
|
|
4526
|
+
const lock = await this.readLock();
|
|
4527
|
+
if (!lock) return null;
|
|
4528
|
+
const ageMs = Date.now() - new Date(lock.startedAt).getTime();
|
|
4529
|
+
if (Number.isNaN(ageMs) || ageMs < 0) {
|
|
4530
|
+
return null;
|
|
4531
|
+
}
|
|
4532
|
+
if (ageMs > this.maxAgeMs) return null;
|
|
4533
|
+
if (lock.hostname === this.hostname && this.probe(lock.pid)) {
|
|
4534
|
+
return null;
|
|
4535
|
+
}
|
|
4536
|
+
let messageCount = 0;
|
|
4537
|
+
if (this.sessionStore) {
|
|
4538
|
+
try {
|
|
4539
|
+
const data = await this.sessionStore.load(lock.sessionId);
|
|
4540
|
+
const closed = data.events.some((e) => e.type === "session_end");
|
|
4541
|
+
if (closed) return null;
|
|
4542
|
+
messageCount = data.messages.length;
|
|
4543
|
+
} catch {
|
|
4544
|
+
return null;
|
|
4545
|
+
}
|
|
4546
|
+
}
|
|
4547
|
+
return {
|
|
4548
|
+
sessionId: lock.sessionId,
|
|
4549
|
+
pid: lock.pid,
|
|
4550
|
+
startedAt: lock.startedAt,
|
|
4551
|
+
ageMs,
|
|
4552
|
+
messageCount
|
|
4553
|
+
};
|
|
4554
|
+
}
|
|
4555
|
+
/**
|
|
4556
|
+
* Claim the lock for the given session. Overwrites any existing lock
|
|
4557
|
+
* — the caller should have already handled abandonment (via
|
|
4558
|
+
* `checkAbandoned`) before calling this.
|
|
4559
|
+
*/
|
|
4560
|
+
async write(sessionId) {
|
|
4561
|
+
await ensureDir(path2.dirname(this.file));
|
|
4562
|
+
const lock = {
|
|
4563
|
+
v: 1,
|
|
4564
|
+
sessionId,
|
|
4565
|
+
pid: this.pid,
|
|
4566
|
+
hostname: this.hostname,
|
|
4567
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4568
|
+
};
|
|
4569
|
+
const tmp = `${this.file}.tmp`;
|
|
4570
|
+
await fsp.writeFile(tmp, JSON.stringify(lock), { mode: 384 });
|
|
4571
|
+
await fsp.rename(tmp, this.file);
|
|
4572
|
+
}
|
|
4573
|
+
/**
|
|
4574
|
+
* Release the lock. Idempotent — silently succeeds if the file is
|
|
4575
|
+
* already gone (e.g. someone else cleared it, or the directory was
|
|
4576
|
+
* wiped).
|
|
4577
|
+
*/
|
|
4578
|
+
async clear() {
|
|
4579
|
+
try {
|
|
4580
|
+
await fsp.unlink(this.file);
|
|
4581
|
+
} catch (err) {
|
|
4582
|
+
const code = err.code;
|
|
4583
|
+
if (code === "ENOENT") return;
|
|
4584
|
+
throw err;
|
|
4585
|
+
}
|
|
4586
|
+
}
|
|
4587
|
+
async readLock() {
|
|
4588
|
+
let raw;
|
|
4589
|
+
try {
|
|
4590
|
+
raw = await fsp.readFile(this.file, "utf8");
|
|
4591
|
+
} catch (err) {
|
|
4592
|
+
const code = err.code;
|
|
4593
|
+
if (code === "ENOENT") return null;
|
|
4594
|
+
return null;
|
|
4595
|
+
}
|
|
4596
|
+
try {
|
|
4597
|
+
const parsed = JSON.parse(raw);
|
|
4598
|
+
if (!isLockFile(parsed)) return null;
|
|
4599
|
+
return parsed;
|
|
4600
|
+
} catch {
|
|
4601
|
+
return null;
|
|
4602
|
+
}
|
|
4603
|
+
}
|
|
4604
|
+
};
|
|
4605
|
+
function isLockFile(v) {
|
|
4606
|
+
if (typeof v !== "object" || v === null) return false;
|
|
4607
|
+
const o = v;
|
|
4608
|
+
return o["v"] === 1 && typeof o["sessionId"] === "string" && typeof o["pid"] === "number" && typeof o["hostname"] === "string" && typeof o["startedAt"] === "string";
|
|
4609
|
+
}
|
|
4610
|
+
function defaultIsPidAlive(pid) {
|
|
4611
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
4612
|
+
try {
|
|
4613
|
+
process.kill(pid, 0);
|
|
4614
|
+
return true;
|
|
4615
|
+
} catch (err) {
|
|
4616
|
+
const code = err.code;
|
|
4617
|
+
if (code === "EPERM") return true;
|
|
4618
|
+
return false;
|
|
4619
|
+
}
|
|
4620
|
+
}
|
|
4621
|
+
|
|
4622
|
+
// src/utils/tool-output-serializer.ts
|
|
4623
|
+
function createToolOutputSerializer(opts = {}) {
|
|
4624
|
+
const capBytes = opts.perIterationOutputCapBytes ?? 1e5;
|
|
4625
|
+
function serialize(value) {
|
|
4626
|
+
if (typeof value === "string") return value;
|
|
4627
|
+
if (value === null || value === void 0) return "";
|
|
4628
|
+
if (typeof value === "object") {
|
|
4629
|
+
if (Array.isArray(value)) return value.map(serialize).join("\n");
|
|
4630
|
+
if ("text" in value) {
|
|
4631
|
+
const t = value.text;
|
|
4632
|
+
return typeof t === "string" ? t : JSON.stringify(value, null, 2);
|
|
4633
|
+
}
|
|
4634
|
+
try {
|
|
4635
|
+
return JSON.stringify(value, null, 2);
|
|
4636
|
+
} catch {
|
|
4637
|
+
return String(value);
|
|
4638
|
+
}
|
|
4639
|
+
}
|
|
4640
|
+
return String(value);
|
|
4641
|
+
}
|
|
4642
|
+
function enforceCap(text, remainingBudget) {
|
|
4643
|
+
if (remainingBudget <= 0) {
|
|
4644
|
+
return { text: "[truncated: iteration output cap exceeded]", newBudget: 0 };
|
|
4645
|
+
}
|
|
4646
|
+
const textBytes = Buffer.byteLength(text, "utf8");
|
|
4647
|
+
if (textBytes <= remainingBudget) {
|
|
4648
|
+
return { text, newBudget: remainingBudget - textBytes };
|
|
4649
|
+
}
|
|
4650
|
+
const marker = `
|
|
4651
|
+
\u2026[truncated ${textBytes - remainingBudget} bytes]\u2026
|
|
4652
|
+
`;
|
|
4653
|
+
const markerBytes = Buffer.byteLength(marker, "utf8");
|
|
4654
|
+
const available = remainingBudget - markerBytes;
|
|
4655
|
+
if (available <= 0) {
|
|
4656
|
+
return { text: "[truncated: iteration output cap exceeded]", newBudget: 0 };
|
|
4657
|
+
}
|
|
4658
|
+
const half = Math.floor(available / 2);
|
|
4659
|
+
const first = text.slice(0, half);
|
|
4660
|
+
Buffer.byteLength(first, "utf8");
|
|
4661
|
+
const second = text.slice(text.length - half);
|
|
4662
|
+
return { text: `${first}${marker}${second}`, newBudget: 0 };
|
|
4663
|
+
}
|
|
4664
|
+
return { serialize, enforceCap, capBytes };
|
|
4665
|
+
}
|
|
4666
|
+
|
|
4667
|
+
// src/defaults/tool-executor.ts
|
|
4668
|
+
var ToolExecutor = class {
|
|
4669
|
+
constructor(registry, opts) {
|
|
4670
|
+
this.registry = registry;
|
|
4671
|
+
this.opts = opts;
|
|
4672
|
+
this.iterationTimeoutMs = opts.iterationTimeoutMs ?? 3e5;
|
|
4673
|
+
this.serializer = createToolOutputSerializer({
|
|
4674
|
+
perIterationOutputCapBytes: opts.perIterationOutputCapBytes ?? 1e5
|
|
4675
|
+
});
|
|
4676
|
+
}
|
|
4677
|
+
registry;
|
|
4678
|
+
opts;
|
|
4679
|
+
serializer;
|
|
4680
|
+
iterationTimeoutMs;
|
|
4681
|
+
/**
|
|
4682
|
+
* Execute a batch of tool uses using the configured strategy.
|
|
4683
|
+
* Returns the execution results and the remaining output budget.
|
|
4684
|
+
*/
|
|
4685
|
+
async executeBatch(toolUses, ctx, strategy) {
|
|
4686
|
+
let budget = this.opts.perIterationOutputCapBytes ?? 1e5;
|
|
4687
|
+
const runOne = async (use) => {
|
|
4688
|
+
const start = Date.now();
|
|
4689
|
+
const tool = this.registry.get(use.name);
|
|
4690
|
+
if (!tool) {
|
|
4691
|
+
const result = this.unknownToolResult(use, () => this.registry.list().map((t) => t.name));
|
|
4692
|
+
budget = this.decrementBudget(result, budget);
|
|
4693
|
+
return { result, tool, durationMs: Date.now() - start };
|
|
4694
|
+
}
|
|
4695
|
+
const decision = await this.opts.permissionPolicy.evaluate(tool, use.input, ctx);
|
|
4696
|
+
if (decision.permission === "deny") {
|
|
4697
|
+
const result = this.deniedResult(use, decision.reason);
|
|
4698
|
+
budget = this.decrementBudget(result, budget);
|
|
4699
|
+
return { result, tool, durationMs: Date.now() - start };
|
|
4700
|
+
}
|
|
4701
|
+
if (decision.permission === "confirm") {
|
|
4702
|
+
if (this.opts.confirmAwaiter) {
|
|
4703
|
+
const choice = await this.opts.confirmAwaiter(tool, use.input, use.id, tool.name);
|
|
4704
|
+
if (choice !== "yes" && choice !== "always") {
|
|
4705
|
+
const result = { type: "tool_result", tool_use_id: use.id, content: `Tool "${tool.name}" denied by user.`, is_error: true };
|
|
4706
|
+
budget = this.decrementBudget(result, budget);
|
|
4707
|
+
return { result, tool, durationMs: Date.now() - start };
|
|
4708
|
+
}
|
|
4709
|
+
} else {
|
|
4710
|
+
const suggestedPattern = this.subjectFor(tool.name, use.input) ?? tool.name;
|
|
4711
|
+
const pending = { type: "tool_confirm_pending", toolUseId: use.id, toolName: tool.name, input: use.input, suggestedPattern };
|
|
4712
|
+
return { result: pending, tool, durationMs: Date.now() - start };
|
|
4713
|
+
}
|
|
4714
|
+
}
|
|
4715
|
+
const span = this.opts.tracer?.startSpan(`tool.${tool.name}`, {
|
|
4716
|
+
"tool.name": tool.name,
|
|
4717
|
+
"tool.mutating": tool.mutating,
|
|
4718
|
+
"tool.permission": tool.permission
|
|
4719
|
+
});
|
|
4720
|
+
try {
|
|
4721
|
+
const result = await this.executeTool(tool, use, ctx, budget);
|
|
4722
|
+
budget = this.decrementBudget(result, budget);
|
|
4723
|
+
span?.setAttribute("tool.is_error", !!result.is_error);
|
|
4724
|
+
span?.setAttribute(
|
|
4725
|
+
"tool.output_bytes",
|
|
4726
|
+
typeof result.content === "string" ? result.content.length : 0
|
|
4727
|
+
);
|
|
4728
|
+
return { result, tool, durationMs: Date.now() - start };
|
|
4729
|
+
} catch (err) {
|
|
4730
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4731
|
+
const scrubbed = this.opts.secretScrubber.scrub(msg);
|
|
4732
|
+
this.opts.renderer?.writeToolResult(tool.name, scrubbed, true);
|
|
4733
|
+
const result = { type: "tool_result", tool_use_id: use.id, content: `Tool "${tool.name}" threw: ${scrubbed}`, is_error: true };
|
|
4734
|
+
budget = this.decrementBudget(result, budget);
|
|
4735
|
+
if (err instanceof Error) span?.recordError(err);
|
|
4736
|
+
span?.setAttribute("tool.is_error", true);
|
|
4737
|
+
return { result, tool, durationMs: Date.now() - start };
|
|
4738
|
+
} finally {
|
|
4739
|
+
span?.end();
|
|
4740
|
+
}
|
|
4741
|
+
};
|
|
4742
|
+
if (strategy === "sequential") {
|
|
4743
|
+
const outputs = [];
|
|
4744
|
+
for (const use of toolUses) {
|
|
4745
|
+
if (use) outputs.push(await runOne(use));
|
|
4746
|
+
}
|
|
4747
|
+
return { outputs, remainingBudget: budget };
|
|
4748
|
+
}
|
|
4749
|
+
if (strategy === "parallel") {
|
|
4750
|
+
const outputs = await Promise.all(toolUses.map((use) => runOne(use)));
|
|
4751
|
+
return { outputs, remainingBudget: budget };
|
|
4752
|
+
}
|
|
4753
|
+
const nonMutating = [];
|
|
4754
|
+
const mutating = [];
|
|
4755
|
+
for (const use of toolUses) {
|
|
4756
|
+
if (!use) continue;
|
|
4757
|
+
const tool = this.registry.get(use.name);
|
|
4758
|
+
if (tool?.mutating) mutating.push(use);
|
|
4759
|
+
else nonMutating.push(use);
|
|
4760
|
+
}
|
|
4761
|
+
const firstPass = await Promise.all(nonMutating.map((use) => runOne(use)));
|
|
4762
|
+
const secondPass = [];
|
|
4763
|
+
for (const use of mutating) {
|
|
4764
|
+
secondPass.push(await runOne(use));
|
|
4765
|
+
}
|
|
4766
|
+
return {
|
|
4767
|
+
outputs: [...firstPass, ...secondPass],
|
|
4768
|
+
remainingBudget: budget
|
|
4769
|
+
};
|
|
4770
|
+
}
|
|
4771
|
+
/**
|
|
4772
|
+
* Execute a single tool with timeout, permission check, and output capping.
|
|
4773
|
+
* Emits `tool.started` via the injected EventBus (if any) right before
|
|
4774
|
+
* invoking the tool — closes the observability gap between "model decided
|
|
4775
|
+
* to call a tool" and "tool.executed".
|
|
4776
|
+
*/
|
|
4777
|
+
async executeTool(tool, use, ctx, budget) {
|
|
4778
|
+
this.opts.events?.emit("tool.started", {
|
|
4779
|
+
name: tool.name,
|
|
4780
|
+
id: use.id,
|
|
4781
|
+
input: use.input
|
|
4782
|
+
});
|
|
4783
|
+
this.opts.renderer?.writeToolCall(tool.name, use.input);
|
|
4784
|
+
const output = await this.runWithTimeout(tool, use.input, ctx.signal, ctx, use.id);
|
|
4785
|
+
const text = this.serializer.serialize(output);
|
|
4786
|
+
const scrubbed = this.opts.secretScrubber.scrub(text);
|
|
4787
|
+
const { text: capped } = this.serializer.enforceCap(scrubbed, budget);
|
|
4788
|
+
this.opts.renderer?.writeToolResult(tool.name, capped, false);
|
|
4789
|
+
return {
|
|
4790
|
+
type: "tool_result",
|
|
4791
|
+
tool_use_id: use.id,
|
|
4792
|
+
name: tool.name,
|
|
4793
|
+
content: capped,
|
|
4794
|
+
is_error: false
|
|
4795
|
+
};
|
|
4796
|
+
}
|
|
4797
|
+
async runWithTimeout(tool, input, parentSignal, ctx, toolUseId) {
|
|
4798
|
+
if (parentSignal.aborted) {
|
|
4799
|
+
throw parentSignal.reason instanceof Error ? parentSignal.reason : new Error(typeof parentSignal.reason === "string" ? parentSignal.reason : "aborted");
|
|
4800
|
+
}
|
|
4801
|
+
const timeoutMs = tool.timeoutMs ?? this.iterationTimeoutMs;
|
|
4802
|
+
const ctrl = new AbortController();
|
|
4803
|
+
const timer = setTimeout(() => ctrl.abort(new Error("tool timeout")), timeoutMs);
|
|
4804
|
+
const combined = AbortSignal.any([parentSignal, ctrl.signal]);
|
|
4805
|
+
try {
|
|
4806
|
+
if (typeof tool.executeStream === "function") {
|
|
4807
|
+
return await this.runStreamedTool(tool, input, ctx, combined, toolUseId);
|
|
4808
|
+
}
|
|
4809
|
+
return await tool.execute(input, ctx, { signal: combined });
|
|
4810
|
+
} catch (err) {
|
|
4811
|
+
if (combined.aborted && typeof tool.cleanup === "function") {
|
|
4812
|
+
try {
|
|
4813
|
+
await tool.cleanup(input, ctx);
|
|
4814
|
+
} catch {
|
|
4815
|
+
}
|
|
4816
|
+
}
|
|
4817
|
+
throw err;
|
|
4818
|
+
} finally {
|
|
4819
|
+
clearTimeout(timer);
|
|
4820
|
+
}
|
|
4821
|
+
}
|
|
4822
|
+
async runStreamedTool(tool, input, ctx, signal, toolUseId) {
|
|
4823
|
+
let finalOutput;
|
|
4824
|
+
let sawFinal = false;
|
|
4825
|
+
const stream = tool.executeStream(input, ctx, { signal });
|
|
4826
|
+
for await (const ev of stream) {
|
|
4827
|
+
if (ev.type === "final") {
|
|
4828
|
+
finalOutput = ev.output;
|
|
4829
|
+
sawFinal = true;
|
|
4830
|
+
break;
|
|
4831
|
+
}
|
|
4832
|
+
this.opts.events?.emit("tool.progress", {
|
|
4833
|
+
name: tool.name,
|
|
4834
|
+
id: toolUseId ?? "<unknown>",
|
|
4835
|
+
event: ev
|
|
4836
|
+
});
|
|
4837
|
+
}
|
|
4838
|
+
if (!sawFinal) {
|
|
4839
|
+
throw new Error(`tool "${tool.name}" executeStream completed without a 'final' event`);
|
|
4840
|
+
}
|
|
4841
|
+
return finalOutput;
|
|
4842
|
+
}
|
|
4843
|
+
unknownToolResult(use, listFns) {
|
|
4844
|
+
return {
|
|
4845
|
+
type: "tool_result",
|
|
4846
|
+
tool_use_id: use.id,
|
|
4847
|
+
content: `Tool "${use.name}" is not registered. Available tools: ${listFns().join(", ")}`,
|
|
4848
|
+
is_error: true
|
|
4849
|
+
};
|
|
4850
|
+
}
|
|
4851
|
+
deniedResult(use, reason) {
|
|
4852
|
+
return {
|
|
4853
|
+
type: "tool_result",
|
|
4854
|
+
tool_use_id: use.id,
|
|
4855
|
+
content: `Tool "${use.name}" denied: ${reason ?? "policy"}`,
|
|
4856
|
+
is_error: true
|
|
4857
|
+
};
|
|
4858
|
+
}
|
|
4859
|
+
decrementBudget(result, budget) {
|
|
4860
|
+
const contentBytes = typeof result.content === "string" ? Buffer.byteLength(result.content, "utf8") : Buffer.byteLength(JSON.stringify(result.content), "utf8");
|
|
4861
|
+
return Math.max(0, budget - contentBytes);
|
|
4862
|
+
}
|
|
4863
|
+
/**
|
|
4864
|
+
* Compute the suggestedPattern string for a tool+input pair.
|
|
4865
|
+
* Matches the logic in DefaultPermissionPolicy so the TUI shows the
|
|
4866
|
+
* same subject that the trust file would use.
|
|
4867
|
+
*/
|
|
4868
|
+
subjectFor(toolName, input) {
|
|
4869
|
+
if (!input || typeof input !== "object") return void 0;
|
|
4870
|
+
const obj = input;
|
|
4871
|
+
const globChars = /[*?\[\]]/g;
|
|
4872
|
+
const escapeGlob = (s) => s.replace(globChars, (c) => `\\${c}`);
|
|
4873
|
+
if (toolName === "bash" && typeof obj.command === "string") {
|
|
4874
|
+
return escapeGlob(obj.command);
|
|
4875
|
+
}
|
|
4876
|
+
if (typeof obj.path === "string") {
|
|
4877
|
+
return escapeGlob(obj.path.replace(/\\/g, "/"));
|
|
4878
|
+
}
|
|
4879
|
+
if (typeof obj.url === "string") {
|
|
4880
|
+
return escapeGlob(obj.url);
|
|
4881
|
+
}
|
|
4882
|
+
if (typeof obj.name === "string") {
|
|
4883
|
+
return escapeGlob(obj.name);
|
|
4884
|
+
}
|
|
4885
|
+
return void 0;
|
|
4886
|
+
}
|
|
4887
|
+
};
|
|
4888
|
+
|
|
4889
|
+
// src/defaults/session-reader.ts
|
|
4890
|
+
var DefaultSessionReader = class {
|
|
4891
|
+
store;
|
|
4892
|
+
constructor(opts) {
|
|
4893
|
+
this.store = opts.store;
|
|
4894
|
+
}
|
|
4895
|
+
async query(q = {}) {
|
|
4896
|
+
const raw = await this.store.list(q.limit ? Math.max(q.limit * 4, 100) : 1e3);
|
|
4897
|
+
const titleNeedle = q.titleContains?.toLowerCase();
|
|
4898
|
+
const filtered = raw.filter((s) => {
|
|
4899
|
+
if (q.since && s.startedAt < q.since) return false;
|
|
4900
|
+
if (q.until && s.startedAt > q.until) return false;
|
|
4901
|
+
if (q.provider && s.provider !== q.provider) return false;
|
|
4902
|
+
if (q.model && s.model !== q.model) return false;
|
|
4903
|
+
if (q.minTokens !== void 0 && s.tokenTotal < q.minTokens) return false;
|
|
4904
|
+
if (titleNeedle && !s.title.toLowerCase().includes(titleNeedle)) return false;
|
|
4905
|
+
return true;
|
|
4906
|
+
});
|
|
4907
|
+
const out = filtered.map((s) => ({
|
|
4908
|
+
id: s.id,
|
|
4909
|
+
title: s.title,
|
|
4910
|
+
startedAt: s.startedAt,
|
|
4911
|
+
provider: s.provider,
|
|
4912
|
+
model: s.model,
|
|
4913
|
+
tokenTotal: s.tokenTotal
|
|
4914
|
+
}));
|
|
4915
|
+
return q.limit ? out.slice(0, q.limit) : out;
|
|
4916
|
+
}
|
|
4917
|
+
async *replay(sessionId) {
|
|
4918
|
+
const data = await this.store.load(sessionId);
|
|
4919
|
+
for (const e of data.events) yield e;
|
|
4920
|
+
}
|
|
4921
|
+
async search(q, sessionId) {
|
|
4922
|
+
const limit = q.limit ?? 100;
|
|
4923
|
+
const matcher = buildMatcher(q);
|
|
4924
|
+
const allowedTypes = q.types ? new Set(q.types) : null;
|
|
4925
|
+
const ids = sessionId ? [sessionId] : (await this.store.list(1e3)).map((s) => s.id);
|
|
4926
|
+
const hits = [];
|
|
4927
|
+
for (const id of ids) {
|
|
4928
|
+
let data;
|
|
4929
|
+
try {
|
|
4930
|
+
data = await this.store.load(id);
|
|
4931
|
+
} catch {
|
|
4932
|
+
continue;
|
|
4933
|
+
}
|
|
4934
|
+
for (let i = 0; i < data.events.length; i++) {
|
|
4935
|
+
const ev = data.events[i];
|
|
4936
|
+
if (allowedTypes && !allowedTypes.has(ev.type)) continue;
|
|
4937
|
+
const text = eventText(ev);
|
|
4938
|
+
if (text === null) continue;
|
|
4939
|
+
const hit = matcher(text);
|
|
4940
|
+
if (!hit) continue;
|
|
4941
|
+
hits.push({
|
|
4942
|
+
sessionId: id,
|
|
4943
|
+
eventIndex: i,
|
|
4944
|
+
ts: ev.ts,
|
|
4945
|
+
type: ev.type,
|
|
4946
|
+
snippet: snippetOf(text, hit.start, hit.end)
|
|
4947
|
+
});
|
|
4948
|
+
if (hits.length >= limit) return hits;
|
|
4949
|
+
}
|
|
4950
|
+
}
|
|
4951
|
+
return hits;
|
|
4952
|
+
}
|
|
4953
|
+
async export(sessionId, opts) {
|
|
4954
|
+
const data = await this.store.load(sessionId);
|
|
4955
|
+
const includeTools = opts.includeTools ?? true;
|
|
4956
|
+
const includeDiagnostics = opts.includeDiagnostics ?? true;
|
|
4957
|
+
const filtered = data.events.filter((e) => {
|
|
4958
|
+
if (!includeTools && (e.type === "tool_use" || e.type === "tool_result" || e.type === "tool_call_start" || e.type === "tool_call_end")) {
|
|
4959
|
+
return false;
|
|
4960
|
+
}
|
|
4961
|
+
if (!includeDiagnostics && (e.type === "error" || e.type === "compaction" || e.type === "message_truncated")) {
|
|
4962
|
+
return false;
|
|
4963
|
+
}
|
|
4964
|
+
return true;
|
|
4965
|
+
});
|
|
4966
|
+
if (opts.format === "json") {
|
|
4967
|
+
return JSON.stringify({ metadata: data.metadata, events: filtered }, null, 2);
|
|
4968
|
+
}
|
|
4969
|
+
if (opts.format === "text") {
|
|
4970
|
+
return renderPlainText(data.metadata, filtered);
|
|
4971
|
+
}
|
|
4972
|
+
return renderMarkdown(data.metadata, filtered);
|
|
4973
|
+
}
|
|
4974
|
+
async metadata(sessionId) {
|
|
4975
|
+
const data = await this.store.load(sessionId);
|
|
4976
|
+
return data.metadata;
|
|
4977
|
+
}
|
|
4978
|
+
};
|
|
4979
|
+
function buildMatcher(q) {
|
|
4980
|
+
const ci = q.caseInsensitive ?? true;
|
|
4981
|
+
if (q.regex) {
|
|
4982
|
+
const flags = ci ? "i" : "";
|
|
4983
|
+
const re = new RegExp(q.query, flags);
|
|
4984
|
+
return (text) => {
|
|
4985
|
+
const m = re.exec(text);
|
|
4986
|
+
return m ? { start: m.index, end: m.index + m[0].length } : null;
|
|
4987
|
+
};
|
|
4988
|
+
}
|
|
4989
|
+
const needle = ci ? q.query.toLowerCase() : q.query;
|
|
4990
|
+
return (text) => {
|
|
4991
|
+
const hay = ci ? text.toLowerCase() : text;
|
|
4992
|
+
const idx = hay.indexOf(needle);
|
|
4993
|
+
return idx === -1 ? null : { start: idx, end: idx + needle.length };
|
|
4994
|
+
};
|
|
4995
|
+
}
|
|
4996
|
+
function eventText(e) {
|
|
4997
|
+
switch (e.type) {
|
|
4998
|
+
case "user_input":
|
|
4999
|
+
return contentToString(e.content);
|
|
5000
|
+
case "llm_response":
|
|
5001
|
+
return contentToString(e.content);
|
|
5002
|
+
case "tool_use":
|
|
5003
|
+
return `${e.name} ${JSON.stringify(e.input)}`;
|
|
5004
|
+
case "tool_result":
|
|
5005
|
+
return typeof e.content === "string" ? e.content : JSON.stringify(e.content);
|
|
5006
|
+
case "error":
|
|
5007
|
+
return `${e.phase}: ${e.message}`;
|
|
5008
|
+
case "session_start":
|
|
5009
|
+
case "session_resumed":
|
|
5010
|
+
return `${e.model}/${e.provider}`;
|
|
5011
|
+
case "task_created":
|
|
5012
|
+
case "task_completed":
|
|
5013
|
+
return e.title;
|
|
5014
|
+
case "task_failed":
|
|
5015
|
+
return `${e.title}: ${e.error}`;
|
|
5016
|
+
case "skill_activated":
|
|
5017
|
+
case "skill_deactivated":
|
|
5018
|
+
return e.skillName;
|
|
5019
|
+
default:
|
|
5020
|
+
return null;
|
|
5021
|
+
}
|
|
5022
|
+
}
|
|
5023
|
+
function contentToString(content) {
|
|
5024
|
+
if (typeof content === "string") return content;
|
|
5025
|
+
return content.map((b) => {
|
|
5026
|
+
switch (b.type) {
|
|
5027
|
+
case "text":
|
|
5028
|
+
return b.text;
|
|
5029
|
+
case "tool_use":
|
|
5030
|
+
return `[tool_use:${b.name} ${JSON.stringify(b.input)}]`;
|
|
5031
|
+
case "tool_result":
|
|
5032
|
+
return typeof b.content === "string" ? b.content : JSON.stringify(b.content);
|
|
5033
|
+
default:
|
|
5034
|
+
return "";
|
|
5035
|
+
}
|
|
5036
|
+
}).join("\n");
|
|
5037
|
+
}
|
|
5038
|
+
var SNIPPET_RADIUS = 60;
|
|
5039
|
+
function snippetOf(text, start, end) {
|
|
5040
|
+
const from = Math.max(0, start - SNIPPET_RADIUS);
|
|
5041
|
+
const to = Math.min(text.length, end + SNIPPET_RADIUS);
|
|
5042
|
+
const prefix = from > 0 ? "\u2026" : "";
|
|
5043
|
+
const suffix = to < text.length ? "\u2026" : "";
|
|
5044
|
+
return prefix + text.slice(from, to).replace(/\s+/g, " ").trim() + suffix;
|
|
5045
|
+
}
|
|
5046
|
+
function renderMarkdown(meta, events) {
|
|
5047
|
+
const lines = [];
|
|
5048
|
+
lines.push(`# Session ${meta.id}`);
|
|
5049
|
+
lines.push("");
|
|
5050
|
+
if (meta.model || meta.provider) {
|
|
5051
|
+
lines.push(`- **Model:** ${meta.provider ?? "?"}/${meta.model ?? "?"}`);
|
|
5052
|
+
}
|
|
5053
|
+
lines.push(`- **Started:** ${meta.startedAt}`);
|
|
5054
|
+
if (meta.endedAt) lines.push(`- **Ended:** ${meta.endedAt}`);
|
|
5055
|
+
lines.push("");
|
|
5056
|
+
lines.push("---");
|
|
5057
|
+
lines.push("");
|
|
5058
|
+
for (const e of events) {
|
|
5059
|
+
switch (e.type) {
|
|
5060
|
+
case "user_input": {
|
|
5061
|
+
lines.push(`## User \u2014 ${e.ts}`);
|
|
5062
|
+
lines.push("");
|
|
5063
|
+
lines.push(contentToString(e.content));
|
|
5064
|
+
lines.push("");
|
|
5065
|
+
break;
|
|
5066
|
+
}
|
|
5067
|
+
case "llm_response": {
|
|
5068
|
+
lines.push(`## Assistant \u2014 ${e.ts}`);
|
|
5069
|
+
lines.push("");
|
|
5070
|
+
lines.push(contentToString(e.content));
|
|
5071
|
+
if (e.stopReason && e.stopReason !== "end_turn") {
|
|
5072
|
+
lines.push("");
|
|
5073
|
+
lines.push(`*stop: ${e.stopReason}*`);
|
|
5074
|
+
}
|
|
5075
|
+
lines.push("");
|
|
5076
|
+
break;
|
|
5077
|
+
}
|
|
5078
|
+
case "tool_use": {
|
|
5079
|
+
lines.push(`### Tool call: \`${e.name}\``);
|
|
5080
|
+
lines.push("");
|
|
5081
|
+
lines.push("```json");
|
|
5082
|
+
lines.push(JSON.stringify(e.input, null, 2));
|
|
5083
|
+
lines.push("```");
|
|
5084
|
+
lines.push("");
|
|
5085
|
+
break;
|
|
5086
|
+
}
|
|
5087
|
+
case "tool_result": {
|
|
5088
|
+
const body = typeof e.content === "string" ? e.content : JSON.stringify(e.content, null, 2);
|
|
5089
|
+
lines.push(`### Tool result${e.isError ? " (error)" : ""}`);
|
|
5090
|
+
lines.push("");
|
|
5091
|
+
lines.push("```");
|
|
5092
|
+
lines.push(body);
|
|
5093
|
+
lines.push("```");
|
|
5094
|
+
lines.push("");
|
|
5095
|
+
break;
|
|
5096
|
+
}
|
|
5097
|
+
case "error": {
|
|
5098
|
+
lines.push(`> **Error** (${e.phase}): ${e.message}`);
|
|
5099
|
+
lines.push("");
|
|
5100
|
+
break;
|
|
5101
|
+
}
|
|
5102
|
+
case "compaction": {
|
|
5103
|
+
lines.push(`> **Compaction**: ${e.before} \u2192 ${e.after} tokens`);
|
|
5104
|
+
lines.push("");
|
|
5105
|
+
break;
|
|
5106
|
+
}
|
|
5107
|
+
}
|
|
5108
|
+
}
|
|
5109
|
+
return lines.join("\n");
|
|
5110
|
+
}
|
|
5111
|
+
function renderPlainText(meta, events) {
|
|
5112
|
+
const lines = [];
|
|
5113
|
+
lines.push(`Session ${meta.id} \u2014 ${meta.provider ?? "?"}/${meta.model ?? "?"} \u2014 started ${meta.startedAt}`);
|
|
5114
|
+
lines.push("".padEnd(72, "-"));
|
|
5115
|
+
for (const e of events) {
|
|
5116
|
+
switch (e.type) {
|
|
5117
|
+
case "user_input":
|
|
5118
|
+
lines.push(`[${e.ts}] USER`);
|
|
5119
|
+
lines.push(contentToString(e.content));
|
|
5120
|
+
lines.push("");
|
|
5121
|
+
break;
|
|
5122
|
+
case "llm_response":
|
|
5123
|
+
lines.push(`[${e.ts}] ASSISTANT`);
|
|
5124
|
+
lines.push(contentToString(e.content));
|
|
5125
|
+
lines.push("");
|
|
5126
|
+
break;
|
|
5127
|
+
case "tool_use":
|
|
5128
|
+
lines.push(`[${e.ts}] TOOL_USE ${e.name} ${JSON.stringify(e.input)}`);
|
|
5129
|
+
break;
|
|
5130
|
+
case "tool_result":
|
|
5131
|
+
lines.push(
|
|
5132
|
+
`[${e.ts}] TOOL_RESULT${e.isError ? " (error)" : ""} ${typeof e.content === "string" ? e.content : JSON.stringify(e.content)}`
|
|
5133
|
+
);
|
|
5134
|
+
break;
|
|
5135
|
+
case "error":
|
|
5136
|
+
lines.push(`[${e.ts}] ERROR (${e.phase}): ${e.message}`);
|
|
5137
|
+
break;
|
|
5138
|
+
}
|
|
5139
|
+
}
|
|
5140
|
+
return lines.join("\n");
|
|
5141
|
+
}
|
|
5142
|
+
|
|
5143
|
+
// src/defaults/observability/metrics.ts
|
|
5144
|
+
var RESERVOIR_SIZE = 1024;
|
|
5145
|
+
function labelKey(labels) {
|
|
5146
|
+
if (!labels) return "";
|
|
5147
|
+
const keys = Object.keys(labels).sort();
|
|
5148
|
+
return keys.map((k) => `${k}=${labels[k]}`).join(",");
|
|
5149
|
+
}
|
|
5150
|
+
function quantile(sorted, q) {
|
|
5151
|
+
if (sorted.length === 0) return 0;
|
|
5152
|
+
const idx = Math.min(sorted.length - 1, Math.floor(q * sorted.length));
|
|
5153
|
+
return sorted[idx] ?? 0;
|
|
5154
|
+
}
|
|
5155
|
+
var InMemoryMetricsSink = class {
|
|
5156
|
+
counters = /* @__PURE__ */ new Map();
|
|
5157
|
+
gauges = /* @__PURE__ */ new Map();
|
|
5158
|
+
histograms = /* @__PURE__ */ new Map();
|
|
5159
|
+
counter(name, value = 1, labels) {
|
|
5160
|
+
const series = this.getOrCreate(this.counters, name);
|
|
5161
|
+
const key = labelKey(labels);
|
|
5162
|
+
const state = series.get(key) ?? { value: 0 };
|
|
5163
|
+
state.value += value;
|
|
5164
|
+
series.set(key, state);
|
|
5165
|
+
}
|
|
5166
|
+
gauge(name, value, labels) {
|
|
5167
|
+
const series = this.getOrCreate(this.gauges, name);
|
|
5168
|
+
series.set(labelKey(labels), { value });
|
|
5169
|
+
}
|
|
5170
|
+
histogram(name, value, labels) {
|
|
5171
|
+
const series = this.getOrCreate(this.histograms, name);
|
|
5172
|
+
const key = labelKey(labels);
|
|
5173
|
+
let state = series.get(key);
|
|
5174
|
+
if (!state) {
|
|
5175
|
+
state = { count: 0, sum: 0, min: value, max: value, samples: [] };
|
|
5176
|
+
series.set(key, state);
|
|
5177
|
+
}
|
|
5178
|
+
state.count++;
|
|
5179
|
+
state.sum += value;
|
|
5180
|
+
if (value < state.min) state.min = value;
|
|
5181
|
+
if (value > state.max) state.max = value;
|
|
5182
|
+
if (state.samples.length < RESERVOIR_SIZE) {
|
|
5183
|
+
state.samples.push(value);
|
|
5184
|
+
} else {
|
|
5185
|
+
const r = Math.floor(Math.random() * state.count);
|
|
5186
|
+
if (r < RESERVOIR_SIZE) state.samples[r] = value;
|
|
5187
|
+
}
|
|
5188
|
+
}
|
|
5189
|
+
snapshot() {
|
|
5190
|
+
const series = [];
|
|
5191
|
+
for (const [name, byLabel] of this.counters) {
|
|
5192
|
+
for (const [key, state] of byLabel) {
|
|
5193
|
+
series.push({
|
|
5194
|
+
name,
|
|
5195
|
+
type: "counter",
|
|
5196
|
+
labels: parseLabelKey(key),
|
|
5197
|
+
values: { value: state.value }
|
|
5198
|
+
});
|
|
5199
|
+
}
|
|
5200
|
+
}
|
|
5201
|
+
for (const [name, byLabel] of this.gauges) {
|
|
5202
|
+
for (const [key, state] of byLabel) {
|
|
5203
|
+
series.push({
|
|
5204
|
+
name,
|
|
5205
|
+
type: "gauge",
|
|
5206
|
+
labels: parseLabelKey(key),
|
|
5207
|
+
values: { value: state.value }
|
|
5208
|
+
});
|
|
5209
|
+
}
|
|
5210
|
+
}
|
|
5211
|
+
for (const [name, byLabel] of this.histograms) {
|
|
5212
|
+
for (const [key, state] of byLabel) {
|
|
5213
|
+
const sorted = [...state.samples].sort((a, b) => a - b);
|
|
5214
|
+
series.push({
|
|
5215
|
+
name,
|
|
5216
|
+
type: "histogram",
|
|
5217
|
+
labels: parseLabelKey(key),
|
|
5218
|
+
values: {
|
|
5219
|
+
count: state.count,
|
|
5220
|
+
sum: state.sum,
|
|
5221
|
+
min: state.min,
|
|
5222
|
+
max: state.max,
|
|
5223
|
+
p50: quantile(sorted, 0.5),
|
|
5224
|
+
p95: quantile(sorted, 0.95),
|
|
5225
|
+
p99: quantile(sorted, 0.99)
|
|
5226
|
+
}
|
|
5227
|
+
});
|
|
5228
|
+
}
|
|
5229
|
+
}
|
|
5230
|
+
return { timestamp: Date.now(), series };
|
|
5231
|
+
}
|
|
5232
|
+
reset() {
|
|
5233
|
+
this.counters.clear();
|
|
5234
|
+
this.gauges.clear();
|
|
5235
|
+
this.histograms.clear();
|
|
5236
|
+
}
|
|
5237
|
+
getOrCreate(bag, name) {
|
|
5238
|
+
let series = bag.get(name);
|
|
5239
|
+
if (!series) {
|
|
5240
|
+
series = /* @__PURE__ */ new Map();
|
|
5241
|
+
bag.set(name, series);
|
|
5242
|
+
}
|
|
5243
|
+
return series;
|
|
5244
|
+
}
|
|
5245
|
+
};
|
|
5246
|
+
function parseLabelKey(key) {
|
|
5247
|
+
if (!key) return {};
|
|
5248
|
+
const labels = {};
|
|
5249
|
+
for (const pair of key.split(",")) {
|
|
5250
|
+
const eq = pair.indexOf("=");
|
|
5251
|
+
if (eq > 0) labels[pair.slice(0, eq)] = pair.slice(eq + 1);
|
|
5252
|
+
}
|
|
5253
|
+
return labels;
|
|
5254
|
+
}
|
|
5255
|
+
var NoopMetricsSink = class {
|
|
5256
|
+
counter() {
|
|
5257
|
+
}
|
|
5258
|
+
gauge() {
|
|
5259
|
+
}
|
|
5260
|
+
histogram() {
|
|
5261
|
+
}
|
|
5262
|
+
snapshot() {
|
|
5263
|
+
return { timestamp: Date.now(), series: [] };
|
|
5264
|
+
}
|
|
5265
|
+
reset() {
|
|
5266
|
+
}
|
|
5267
|
+
};
|
|
5268
|
+
|
|
5269
|
+
// src/defaults/observability/health.ts
|
|
5270
|
+
var SEVERITY = {
|
|
5271
|
+
healthy: 0,
|
|
5272
|
+
degraded: 1,
|
|
5273
|
+
unhealthy: 2
|
|
5274
|
+
};
|
|
5275
|
+
var DefaultHealthRegistry = class {
|
|
5276
|
+
checks = /* @__PURE__ */ new Map();
|
|
5277
|
+
timeoutMs;
|
|
5278
|
+
constructor(opts = {}) {
|
|
5279
|
+
this.timeoutMs = opts.timeoutMs ?? 5e3;
|
|
5280
|
+
}
|
|
5281
|
+
register(check) {
|
|
5282
|
+
this.checks.set(check.name, check);
|
|
5283
|
+
}
|
|
5284
|
+
unregister(name) {
|
|
5285
|
+
this.checks.delete(name);
|
|
5286
|
+
}
|
|
5287
|
+
async run() {
|
|
5288
|
+
const results = await Promise.all(
|
|
5289
|
+
Array.from(this.checks.values()).map(async (c) => {
|
|
5290
|
+
const result = await this.runOne(c);
|
|
5291
|
+
return { name: c.name, ...result };
|
|
5292
|
+
})
|
|
5293
|
+
);
|
|
5294
|
+
let status = "healthy";
|
|
5295
|
+
for (const r of results) {
|
|
5296
|
+
if (SEVERITY[r.status] > SEVERITY[status]) status = r.status;
|
|
5297
|
+
}
|
|
5298
|
+
return { status, timestamp: Date.now(), checks: results };
|
|
5299
|
+
}
|
|
5300
|
+
async runOne(check) {
|
|
5301
|
+
const timeout = new Promise((resolve3) => {
|
|
5302
|
+
setTimeout(
|
|
5303
|
+
() => resolve3({ status: "unhealthy", detail: `timeout after ${this.timeoutMs}ms` }),
|
|
5304
|
+
this.timeoutMs
|
|
5305
|
+
);
|
|
5306
|
+
});
|
|
5307
|
+
try {
|
|
5308
|
+
return await Promise.race([check.check(), timeout]);
|
|
5309
|
+
} catch (err) {
|
|
5310
|
+
return { status: "unhealthy", detail: err instanceof Error ? err.message : String(err) };
|
|
5311
|
+
}
|
|
5312
|
+
}
|
|
5313
|
+
};
|
|
5314
|
+
|
|
5315
|
+
// src/defaults/observability/tracer.ts
|
|
5316
|
+
var NoopTracer = class {
|
|
5317
|
+
startSpan() {
|
|
5318
|
+
return NOOP_SPAN;
|
|
4145
5319
|
}
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
this.flows.set(graph.id, flow);
|
|
4154
|
-
return flow;
|
|
5320
|
+
};
|
|
5321
|
+
var NOOP_SPAN = {
|
|
5322
|
+
setAttribute() {
|
|
5323
|
+
},
|
|
5324
|
+
recordError() {
|
|
5325
|
+
},
|
|
5326
|
+
end() {
|
|
4155
5327
|
}
|
|
4156
|
-
|
|
4157
|
-
|
|
5328
|
+
};
|
|
5329
|
+
|
|
5330
|
+
// src/defaults/observability/otel-tracer.ts
|
|
5331
|
+
var OTEL_STATUS_ERROR = 2;
|
|
5332
|
+
var OTelTracer = class {
|
|
5333
|
+
constructor(upstream) {
|
|
5334
|
+
this.upstream = upstream;
|
|
4158
5335
|
}
|
|
4159
|
-
|
|
4160
|
-
|
|
5336
|
+
upstream;
|
|
5337
|
+
startSpan(name, attrs) {
|
|
5338
|
+
const otelSpan = this.upstream.startSpan(name, attrs ? { attributes: attrs } : void 0);
|
|
5339
|
+
return new OTelSpan(otelSpan);
|
|
4161
5340
|
}
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
5341
|
+
};
|
|
5342
|
+
var OTelSpan = class {
|
|
5343
|
+
constructor(span) {
|
|
5344
|
+
this.span = span;
|
|
5345
|
+
}
|
|
5346
|
+
span;
|
|
5347
|
+
setAttribute(key, value) {
|
|
5348
|
+
this.span.setAttribute(key, value);
|
|
5349
|
+
}
|
|
5350
|
+
recordError(err) {
|
|
5351
|
+
this.span.recordException(err);
|
|
5352
|
+
this.span.setStatus?.({ code: OTEL_STATUS_ERROR, message: err.message });
|
|
5353
|
+
}
|
|
5354
|
+
end() {
|
|
5355
|
+
this.span.end();
|
|
4168
5356
|
}
|
|
4169
5357
|
};
|
|
4170
5358
|
|
|
4171
|
-
// src/
|
|
4172
|
-
function
|
|
4173
|
-
const
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
5359
|
+
// src/defaults/observability/event-bridge.ts
|
|
5360
|
+
function wireMetricsToEvents(events, sink) {
|
|
5361
|
+
const unsubs = [];
|
|
5362
|
+
unsubs.push(
|
|
5363
|
+
events.on("session.started", () => sink.counter("agent.sessions.started")),
|
|
5364
|
+
events.on("session.ended", (e) => {
|
|
5365
|
+
sink.counter("agent.sessions.ended");
|
|
5366
|
+
sink.histogram("agent.session.tokens.input", e.usage.input);
|
|
5367
|
+
sink.histogram("agent.session.tokens.output", e.usage.output);
|
|
5368
|
+
}),
|
|
5369
|
+
events.on("session.damaged", () => sink.counter("agent.sessions.damaged")),
|
|
5370
|
+
events.on("iteration.completed", () => sink.counter("agent.iterations.total")),
|
|
5371
|
+
events.on("iteration.limit_reached", () => sink.counter("agent.iteration_limit.hit")),
|
|
5372
|
+
events.on("provider.response", (e) => {
|
|
5373
|
+
sink.counter("provider.responses.total", 1, { stop_reason: e.stopReason });
|
|
5374
|
+
sink.counter("provider.tokens.input", e.usage.input);
|
|
5375
|
+
sink.counter("provider.tokens.output", e.usage.output);
|
|
5376
|
+
if (e.usage.cacheRead) sink.counter("provider.tokens.cache_read", e.usage.cacheRead);
|
|
5377
|
+
if (e.usage.cacheWrite) sink.counter("provider.tokens.cache_write", e.usage.cacheWrite);
|
|
5378
|
+
}),
|
|
5379
|
+
events.on(
|
|
5380
|
+
"provider.retry",
|
|
5381
|
+
(e) => sink.counter("provider.retries.total", 1, {
|
|
5382
|
+
provider: e.providerId,
|
|
5383
|
+
status: String(e.status)
|
|
5384
|
+
})
|
|
5385
|
+
),
|
|
5386
|
+
events.on(
|
|
5387
|
+
"provider.error",
|
|
5388
|
+
(e) => sink.counter("provider.errors.total", 1, {
|
|
5389
|
+
provider: e.providerId,
|
|
5390
|
+
status: String(e.status),
|
|
5391
|
+
retryable: String(e.retryable)
|
|
5392
|
+
})
|
|
5393
|
+
),
|
|
5394
|
+
events.on("tool.started", (e) => sink.counter("tool.starts.total", 1, { tool: e.name })),
|
|
5395
|
+
events.on("tool.executed", (e) => {
|
|
5396
|
+
sink.counter("tool.executions.total", 1, { tool: e.name, ok: String(e.ok) });
|
|
5397
|
+
sink.histogram("tool.duration_ms", e.durationMs, { tool: e.name });
|
|
5398
|
+
}),
|
|
5399
|
+
events.on("token.threshold", (e) => sink.gauge("agent.tokens.used", e.used)),
|
|
5400
|
+
events.on("compaction.fired", (e) => {
|
|
5401
|
+
sink.counter("compaction.fired.total");
|
|
5402
|
+
sink.histogram("compaction.reduction_tokens", e.before - e.after);
|
|
5403
|
+
}),
|
|
5404
|
+
events.on(
|
|
5405
|
+
"mcp.server.connected",
|
|
5406
|
+
(e) => sink.counter("mcp.connects.total", 1, { server: e.name })
|
|
5407
|
+
),
|
|
5408
|
+
events.on(
|
|
5409
|
+
"mcp.server.reconnected",
|
|
5410
|
+
(e) => sink.counter("mcp.reconnects.total", 1, { server: e.name })
|
|
5411
|
+
),
|
|
5412
|
+
events.on(
|
|
5413
|
+
"mcp.server.disconnected",
|
|
5414
|
+
(e) => sink.counter("mcp.disconnects.total", 1, { server: e.name })
|
|
5415
|
+
),
|
|
5416
|
+
events.on("error", (e) => sink.counter("agent.errors.total", 1, { phase: e.phase }))
|
|
5417
|
+
);
|
|
5418
|
+
return () => {
|
|
5419
|
+
for (const u of unsubs) u();
|
|
5420
|
+
};
|
|
5421
|
+
}
|
|
5422
|
+
|
|
5423
|
+
// src/defaults/observability/prometheus.ts
|
|
5424
|
+
var NUMBER_FORMAT_INFINITY = "NaN";
|
|
5425
|
+
function escapeLabelValue(v) {
|
|
5426
|
+
return v.replace(/\\/g, "\\\\").replace(/\n/g, "\\n").replace(/"/g, '\\"');
|
|
5427
|
+
}
|
|
5428
|
+
function formatLabels(labels) {
|
|
5429
|
+
const keys = Object.keys(labels);
|
|
5430
|
+
if (keys.length === 0) return "";
|
|
5431
|
+
const parts = keys.map((k) => `${k}="${escapeLabelValue(labels[k] ?? "")}"`);
|
|
5432
|
+
return `{${parts.join(",")}}`;
|
|
5433
|
+
}
|
|
5434
|
+
function formatNumber(n) {
|
|
5435
|
+
if (!Number.isFinite(n)) return NUMBER_FORMAT_INFINITY;
|
|
5436
|
+
return Number.isInteger(n) ? n.toString() : n.toString();
|
|
5437
|
+
}
|
|
5438
|
+
function joinLabels(base, extra) {
|
|
5439
|
+
return { ...base, ...extra };
|
|
5440
|
+
}
|
|
5441
|
+
function renderPrometheus(snapshot) {
|
|
5442
|
+
const groups = /* @__PURE__ */ new Map();
|
|
5443
|
+
for (const s of snapshot.series) {
|
|
5444
|
+
let g = groups.get(s.name);
|
|
5445
|
+
if (!g) {
|
|
5446
|
+
g = { type: s.type, rows: [] };
|
|
5447
|
+
groups.set(s.name, g);
|
|
5448
|
+
}
|
|
5449
|
+
g.rows.push({ labels: s.labels, values: s.values });
|
|
5450
|
+
}
|
|
5451
|
+
const lines = [];
|
|
5452
|
+
for (const [name, g] of groups) {
|
|
5453
|
+
const promType = g.type === "histogram" ? "summary" : g.type;
|
|
5454
|
+
lines.push(`# HELP ${name} ${name}`);
|
|
5455
|
+
lines.push(`# TYPE ${name} ${promType}`);
|
|
5456
|
+
if (g.type === "counter" || g.type === "gauge") {
|
|
5457
|
+
for (const row of g.rows) {
|
|
5458
|
+
lines.push(`${name}${formatLabels(row.labels)} ${formatNumber(row.values.value ?? 0)}`);
|
|
4182
5459
|
}
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
5460
|
+
} else {
|
|
5461
|
+
for (const row of g.rows) {
|
|
5462
|
+
const { count = 0, sum = 0, p50 = 0, p95 = 0, p99 = 0 } = row.values;
|
|
5463
|
+
lines.push(
|
|
5464
|
+
`${name}${formatLabels(joinLabels(row.labels, { quantile: "0.5" }))} ${formatNumber(p50)}`
|
|
5465
|
+
);
|
|
5466
|
+
lines.push(
|
|
5467
|
+
`${name}${formatLabels(joinLabels(row.labels, { quantile: "0.95" }))} ${formatNumber(p95)}`
|
|
5468
|
+
);
|
|
5469
|
+
lines.push(
|
|
5470
|
+
`${name}${formatLabels(joinLabels(row.labels, { quantile: "0.99" }))} ${formatNumber(p99)}`
|
|
5471
|
+
);
|
|
5472
|
+
lines.push(`${name}_sum${formatLabels(row.labels)} ${formatNumber(sum)}`);
|
|
5473
|
+
lines.push(`${name}_count${formatLabels(row.labels)} ${formatNumber(count)}`);
|
|
4187
5474
|
}
|
|
4188
5475
|
}
|
|
4189
|
-
return String(value);
|
|
4190
5476
|
}
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
5477
|
+
return lines.join("\n") + "\n";
|
|
5478
|
+
}
|
|
5479
|
+
var PROMETHEUS_CONTENT_TYPE = "text/plain; version=0.0.4; charset=utf-8";
|
|
5480
|
+
async function startMetricsServer(opts) {
|
|
5481
|
+
const { createServer } = await import('http');
|
|
5482
|
+
const host = opts.host ?? "127.0.0.1";
|
|
5483
|
+
const path13 = opts.path ?? "/metrics";
|
|
5484
|
+
const healthPath = opts.healthPath ?? "/healthz";
|
|
5485
|
+
const healthRegistry = opts.healthRegistry;
|
|
5486
|
+
const server = createServer((req, res) => {
|
|
5487
|
+
if (!req.url || req.method !== "GET") {
|
|
5488
|
+
res.statusCode = req.url ? 405 : 400;
|
|
5489
|
+
res.end();
|
|
5490
|
+
return;
|
|
4194
5491
|
}
|
|
4195
|
-
const
|
|
4196
|
-
if (
|
|
4197
|
-
|
|
5492
|
+
const url = req.url.split("?")[0];
|
|
5493
|
+
if (url === path13) {
|
|
5494
|
+
let body;
|
|
5495
|
+
try {
|
|
5496
|
+
body = renderPrometheus(opts.sink.snapshot());
|
|
5497
|
+
} catch (err) {
|
|
5498
|
+
res.statusCode = 500;
|
|
5499
|
+
res.setHeader("content-type", "text/plain; charset=utf-8");
|
|
5500
|
+
res.end(`metrics render failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
5501
|
+
return;
|
|
5502
|
+
}
|
|
5503
|
+
res.statusCode = 200;
|
|
5504
|
+
res.setHeader("content-type", PROMETHEUS_CONTENT_TYPE);
|
|
5505
|
+
res.end(body);
|
|
5506
|
+
return;
|
|
4198
5507
|
}
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
5508
|
+
if (healthRegistry && url === healthPath) {
|
|
5509
|
+
healthRegistry.run().then(
|
|
5510
|
+
(agg) => {
|
|
5511
|
+
res.statusCode = agg.status === "unhealthy" ? 503 : 200;
|
|
5512
|
+
res.setHeader("content-type", "application/json; charset=utf-8");
|
|
5513
|
+
res.end(JSON.stringify(agg, null, 2));
|
|
5514
|
+
},
|
|
5515
|
+
(err) => {
|
|
5516
|
+
res.statusCode = 500;
|
|
5517
|
+
res.setHeader("content-type", "text/plain; charset=utf-8");
|
|
5518
|
+
res.end(`health run failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
5519
|
+
}
|
|
5520
|
+
);
|
|
5521
|
+
return;
|
|
4206
5522
|
}
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
5523
|
+
res.statusCode = 404;
|
|
5524
|
+
res.setHeader("content-type", "text/plain; charset=utf-8");
|
|
5525
|
+
res.end("Not Found");
|
|
5526
|
+
});
|
|
5527
|
+
await new Promise((resolve3, reject) => {
|
|
5528
|
+
const onError = (err) => {
|
|
5529
|
+
server.off("listening", onListening);
|
|
5530
|
+
reject(err);
|
|
5531
|
+
};
|
|
5532
|
+
const onListening = () => {
|
|
5533
|
+
server.off("error", onError);
|
|
5534
|
+
resolve3();
|
|
5535
|
+
};
|
|
5536
|
+
server.once("error", onError);
|
|
5537
|
+
server.once("listening", onListening);
|
|
5538
|
+
server.listen(opts.port, host);
|
|
5539
|
+
});
|
|
5540
|
+
const addr = server.address();
|
|
5541
|
+
const boundPort = typeof addr === "object" && addr ? addr.port : opts.port;
|
|
5542
|
+
return {
|
|
5543
|
+
port: boundPort,
|
|
5544
|
+
url: `http://${host}:${boundPort}${path13}`,
|
|
5545
|
+
close: () => new Promise((resolve3, reject) => {
|
|
5546
|
+
server.close((err) => err ? reject(err) : resolve3());
|
|
5547
|
+
})
|
|
5548
|
+
};
|
|
4214
5549
|
}
|
|
4215
5550
|
|
|
4216
|
-
// src/defaults/
|
|
4217
|
-
var
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
let budget = this.opts.perIterationOutputCapBytes ?? 1e5;
|
|
4236
|
-
const runOne = async (use, index) => {
|
|
4237
|
-
const start = Date.now();
|
|
4238
|
-
const tool = this.registry.get(use.name);
|
|
4239
|
-
let result;
|
|
4240
|
-
if (!tool) {
|
|
4241
|
-
result = this.unknownToolResult(use, () => this.registry.list().map((t) => t.name));
|
|
4242
|
-
} else {
|
|
4243
|
-
const decision = await this.opts.permissionPolicy.evaluate(tool, use.input, ctx);
|
|
4244
|
-
if (decision.permission === "deny") {
|
|
4245
|
-
result = this.deniedResult(use, decision.reason);
|
|
4246
|
-
} else if (decision.permission === "confirm") {
|
|
4247
|
-
result = this.confirmResult(use);
|
|
4248
|
-
} else {
|
|
4249
|
-
try {
|
|
4250
|
-
result = await this.executeTool(tool, use, ctx, budget);
|
|
4251
|
-
} catch (err) {
|
|
4252
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
4253
|
-
const scrubbed = this.opts.secretScrubber.scrub(msg);
|
|
4254
|
-
this.opts.renderer?.writeToolResult(tool.name, scrubbed, true);
|
|
4255
|
-
result = {
|
|
4256
|
-
type: "tool_result",
|
|
4257
|
-
tool_use_id: use.id,
|
|
4258
|
-
content: `Tool "${use.name}" threw: ${scrubbed}`,
|
|
4259
|
-
is_error: true
|
|
4260
|
-
};
|
|
4261
|
-
}
|
|
4262
|
-
}
|
|
4263
|
-
}
|
|
4264
|
-
const contentBytes = typeof result.content === "string" ? Buffer.byteLength(result.content, "utf8") : Buffer.byteLength(JSON.stringify(result.content), "utf8");
|
|
4265
|
-
budget = Math.max(0, budget - contentBytes);
|
|
4266
|
-
return { result, tool, durationMs: Date.now() - start };
|
|
5551
|
+
// src/defaults/observability/otlp-metrics.ts
|
|
5552
|
+
var DEFAULT_INTERVAL_MS = 3e4;
|
|
5553
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
5554
|
+
function joinEndpoint(base) {
|
|
5555
|
+
if (/\/v1\/metrics\/?$/.test(base)) return base;
|
|
5556
|
+
return base.replace(/\/$/, "") + "/v1/metrics";
|
|
5557
|
+
}
|
|
5558
|
+
function attributesFor(labels) {
|
|
5559
|
+
return Object.entries(labels).map(([key, value]) => ({
|
|
5560
|
+
key,
|
|
5561
|
+
value: { stringValue: value }
|
|
5562
|
+
}));
|
|
5563
|
+
}
|
|
5564
|
+
function buildExportBody(opts) {
|
|
5565
|
+
const metrics = [];
|
|
5566
|
+
for (const s of opts.series) {
|
|
5567
|
+
const dp = {
|
|
5568
|
+
attributes: attributesFor(s.labels),
|
|
5569
|
+
timeUnixNano: opts.timeUnixNano
|
|
4267
5570
|
};
|
|
4268
|
-
if (
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
}
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
if (tool?.mutating) mutating.push({ use, index: i });
|
|
4287
|
-
else nonMutating.push({ use, index: i });
|
|
4288
|
-
}
|
|
4289
|
-
const firstPass = await Promise.all(nonMutating.map(({ use, index }) => runOne(use)));
|
|
4290
|
-
const secondPass = [];
|
|
4291
|
-
for (const { use, index } of mutating) {
|
|
4292
|
-
secondPass.push(await runOne(use));
|
|
5571
|
+
if (s.type === "counter") {
|
|
5572
|
+
dp.asDouble = s.values.value ?? 0;
|
|
5573
|
+
metrics.push({
|
|
5574
|
+
name: s.name,
|
|
5575
|
+
sum: { dataPoints: [dp], aggregationTemporality: 2, isMonotonic: true }
|
|
5576
|
+
});
|
|
5577
|
+
} else if (s.type === "gauge") {
|
|
5578
|
+
dp.asDouble = s.values.value ?? 0;
|
|
5579
|
+
metrics.push({ name: s.name, gauge: { dataPoints: [dp] } });
|
|
5580
|
+
} else {
|
|
5581
|
+
dp.count = String(s.values.count ?? 0);
|
|
5582
|
+
dp.sum = s.values.sum ?? 0;
|
|
5583
|
+
dp.quantileValues = [
|
|
5584
|
+
{ quantile: 0.5, value: s.values.p50 ?? 0 },
|
|
5585
|
+
{ quantile: 0.95, value: s.values.p95 ?? 0 },
|
|
5586
|
+
{ quantile: 0.99, value: s.values.p99 ?? 0 }
|
|
5587
|
+
];
|
|
5588
|
+
metrics.push({ name: s.name, summary: { dataPoints: [dp] } });
|
|
4293
5589
|
}
|
|
4294
|
-
return {
|
|
4295
|
-
outputs: [...firstPass, ...secondPass],
|
|
4296
|
-
remainingBudget: budget
|
|
4297
|
-
};
|
|
4298
5590
|
}
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
5591
|
+
return {
|
|
5592
|
+
resourceMetrics: [
|
|
5593
|
+
{
|
|
5594
|
+
resource: { attributes: attributesFor(opts.resourceAttributes) },
|
|
5595
|
+
scopeMetrics: [
|
|
5596
|
+
{
|
|
5597
|
+
scope: { name: opts.scopeName },
|
|
5598
|
+
metrics
|
|
5599
|
+
}
|
|
5600
|
+
]
|
|
5601
|
+
}
|
|
5602
|
+
]
|
|
5603
|
+
};
|
|
5604
|
+
}
|
|
5605
|
+
function buildOtlpMetricsRequest(sink, opts = {}) {
|
|
5606
|
+
return buildExportBody({
|
|
5607
|
+
series: sink.snapshot().series,
|
|
5608
|
+
resourceAttributes: opts.resourceAttributes ?? { "service.name": "wrongstack" },
|
|
5609
|
+
scopeName: opts.scopeName ?? "wrongstack",
|
|
5610
|
+
timeUnixNano: String(BigInt(Date.now()) * 1000000n)
|
|
5611
|
+
});
|
|
5612
|
+
}
|
|
5613
|
+
function startOtlpMetricsExporter(opts) {
|
|
5614
|
+
const url = joinEndpoint(opts.endpoint);
|
|
5615
|
+
const intervalMs = opts.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
5616
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
5617
|
+
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
5618
|
+
const onError = opts.onError ?? (() => {
|
|
5619
|
+
});
|
|
5620
|
+
const resourceAttributes = opts.resourceAttributes ?? { "service.name": "wrongstack" };
|
|
5621
|
+
const scopeName = opts.scopeName ?? "wrongstack";
|
|
5622
|
+
let stopped = false;
|
|
5623
|
+
const headers = {
|
|
5624
|
+
"content-type": "application/json",
|
|
5625
|
+
...opts.headers ?? {}
|
|
5626
|
+
};
|
|
5627
|
+
if (opts.authorization) headers.authorization = opts.authorization;
|
|
5628
|
+
async function pushOnce() {
|
|
5629
|
+
if (stopped) return;
|
|
5630
|
+
const body = buildExportBody({
|
|
5631
|
+
series: opts.sink.snapshot().series,
|
|
5632
|
+
resourceAttributes,
|
|
5633
|
+
scopeName,
|
|
5634
|
+
timeUnixNano: String(BigInt(Date.now()) * 1000000n)
|
|
4310
5635
|
});
|
|
4311
|
-
|
|
4312
|
-
const
|
|
4313
|
-
const text = this.serializer.serialize(output);
|
|
4314
|
-
const scrubbed = this.opts.secretScrubber.scrub(text);
|
|
4315
|
-
const { text: capped } = this.serializer.enforceCap(scrubbed, budget);
|
|
4316
|
-
this.opts.renderer?.writeToolResult(tool.name, capped, false);
|
|
4317
|
-
return {
|
|
4318
|
-
type: "tool_result",
|
|
4319
|
-
tool_use_id: use.id,
|
|
4320
|
-
name: tool.name,
|
|
4321
|
-
content: capped,
|
|
4322
|
-
is_error: false
|
|
4323
|
-
};
|
|
4324
|
-
}
|
|
4325
|
-
async runWithTimeout(tool, input, parentSignal, ctx) {
|
|
4326
|
-
if (parentSignal.aborted) {
|
|
4327
|
-
throw parentSignal.reason instanceof Error ? parentSignal.reason : new Error(typeof parentSignal.reason === "string" ? parentSignal.reason : "aborted");
|
|
4328
|
-
}
|
|
4329
|
-
const timeoutMs = tool.timeoutMs ?? this.iterationTimeoutMs;
|
|
4330
|
-
const ctrl = new AbortController();
|
|
4331
|
-
const timer = setTimeout(() => ctrl.abort(new Error("tool timeout")), timeoutMs);
|
|
4332
|
-
const combined = anySignal([parentSignal, ctrl.signal]);
|
|
5636
|
+
const controller = new AbortController();
|
|
5637
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
4333
5638
|
try {
|
|
4334
|
-
|
|
5639
|
+
const res = await fetchImpl(url, {
|
|
5640
|
+
method: "POST",
|
|
5641
|
+
headers,
|
|
5642
|
+
body: JSON.stringify(body),
|
|
5643
|
+
signal: controller.signal
|
|
5644
|
+
});
|
|
5645
|
+
if (!res.ok) {
|
|
5646
|
+
const text = await res.text().catch(() => "");
|
|
5647
|
+
onError(new Error(`OTLP push failed: ${res.status} ${res.statusText} ${text}`));
|
|
5648
|
+
}
|
|
5649
|
+
} catch (err) {
|
|
5650
|
+
onError(err);
|
|
4335
5651
|
} finally {
|
|
4336
5652
|
clearTimeout(timer);
|
|
4337
5653
|
}
|
|
4338
5654
|
}
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
5655
|
+
const handle = setInterval(() => {
|
|
5656
|
+
void pushOnce();
|
|
5657
|
+
}, intervalMs);
|
|
5658
|
+
handle.unref?.();
|
|
5659
|
+
return {
|
|
5660
|
+
flush: pushOnce,
|
|
5661
|
+
async stop() {
|
|
5662
|
+
stopped = true;
|
|
5663
|
+
clearInterval(handle);
|
|
5664
|
+
await pushOnce().catch(onError);
|
|
5665
|
+
}
|
|
5666
|
+
};
|
|
5667
|
+
}
|
|
5668
|
+
var SPAN_STATUS_CODE_UNSET = 0;
|
|
5669
|
+
var SPAN_STATUS_CODE_OK = 1;
|
|
5670
|
+
var SPAN_STATUS_CODE_ERROR = 2;
|
|
5671
|
+
function hex(bytes) {
|
|
5672
|
+
return crypto2.randomBytes(bytes).toString("hex");
|
|
5673
|
+
}
|
|
5674
|
+
function nowNs() {
|
|
5675
|
+
return BigInt(Date.now()) * 1000000n;
|
|
5676
|
+
}
|
|
5677
|
+
var CapturingSpan = class {
|
|
5678
|
+
constructor(state, onEnd) {
|
|
5679
|
+
this.state = state;
|
|
5680
|
+
this.onEnd = onEnd;
|
|
4346
5681
|
}
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
content: `Tool "${use.name}" denied: ${reason ?? "policy"}`,
|
|
4352
|
-
is_error: true
|
|
4353
|
-
};
|
|
5682
|
+
state;
|
|
5683
|
+
onEnd;
|
|
5684
|
+
setAttribute(key, value) {
|
|
5685
|
+
this.state.attributes[key] = value;
|
|
4354
5686
|
}
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
5687
|
+
recordError(err) {
|
|
5688
|
+
this.state.status = { code: SPAN_STATUS_CODE_ERROR, message: err.message };
|
|
5689
|
+
this.state.attributes["exception.message"] = err.message;
|
|
5690
|
+
if (err.name) this.state.attributes["exception.type"] = err.name;
|
|
5691
|
+
}
|
|
5692
|
+
end() {
|
|
5693
|
+
if (this.state.endTimeUnixNano !== void 0) return;
|
|
5694
|
+
this.state.endTimeUnixNano = nowNs();
|
|
5695
|
+
if (this.state.status.code === SPAN_STATUS_CODE_UNSET) {
|
|
5696
|
+
this.state.status.code = SPAN_STATUS_CODE_OK;
|
|
5697
|
+
}
|
|
5698
|
+
this.onEnd(this.state);
|
|
4362
5699
|
}
|
|
4363
5700
|
};
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
5701
|
+
var DEFAULT_INTERVAL_MS2 = 5e3;
|
|
5702
|
+
var DEFAULT_BUFFER_CAP = 2048;
|
|
5703
|
+
var DEFAULT_TIMEOUT_MS2 = 1e4;
|
|
5704
|
+
function joinEndpoint2(base) {
|
|
5705
|
+
if (/\/v1\/traces\/?$/.test(base)) return base;
|
|
5706
|
+
return base.replace(/\/$/, "") + "/v1/traces";
|
|
5707
|
+
}
|
|
5708
|
+
function encodeAttr(key, value) {
|
|
5709
|
+
if (typeof value === "boolean") return { key, value: { boolValue: value } };
|
|
5710
|
+
if (typeof value === "number") {
|
|
5711
|
+
return Number.isInteger(value) ? { key, value: { intValue: String(value) } } : { key, value: { doubleValue: value } };
|
|
4367
5712
|
}
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
5713
|
+
return { key, value: { stringValue: value } };
|
|
5714
|
+
}
|
|
5715
|
+
function buildOtlpTracesRequest(spans, opts = {}) {
|
|
5716
|
+
const resourceAttributes = opts.resourceAttributes ?? { "service.name": "wrongstack" };
|
|
5717
|
+
const scopeName = opts.scopeName ?? "wrongstack";
|
|
5718
|
+
const otlpSpans = spans.map((s) => ({
|
|
5719
|
+
traceId: s.traceId,
|
|
5720
|
+
spanId: s.spanId,
|
|
5721
|
+
name: s.name,
|
|
5722
|
+
kind: 1,
|
|
5723
|
+
// SPAN_KIND_INTERNAL
|
|
5724
|
+
startTimeUnixNano: s.startTimeUnixNano.toString(),
|
|
5725
|
+
endTimeUnixNano: (s.endTimeUnixNano ?? s.startTimeUnixNano).toString(),
|
|
5726
|
+
attributes: Object.entries(s.attributes).map(([k, v]) => encodeAttr(k, v)),
|
|
5727
|
+
status: s.status
|
|
5728
|
+
}));
|
|
5729
|
+
return {
|
|
5730
|
+
resourceSpans: [
|
|
5731
|
+
{
|
|
5732
|
+
resource: {
|
|
5733
|
+
attributes: Object.entries(resourceAttributes).map(
|
|
5734
|
+
([k, v]) => encodeAttr(k, v)
|
|
5735
|
+
)
|
|
5736
|
+
},
|
|
5737
|
+
scopeSpans: [{ scope: { name: scopeName }, spans: otlpSpans }]
|
|
5738
|
+
}
|
|
5739
|
+
]
|
|
5740
|
+
};
|
|
5741
|
+
}
|
|
5742
|
+
function startOtlpTraceExporter(opts) {
|
|
5743
|
+
const url = joinEndpoint2(opts.endpoint);
|
|
5744
|
+
const intervalMs = opts.intervalMs ?? DEFAULT_INTERVAL_MS2;
|
|
5745
|
+
const maxBuffered = opts.maxBufferedSpans ?? DEFAULT_BUFFER_CAP;
|
|
5746
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
5747
|
+
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
5748
|
+
const onError = opts.onError ?? (() => {
|
|
5749
|
+
});
|
|
5750
|
+
const resourceAttributes = opts.resourceAttributes ?? { "service.name": "wrongstack" };
|
|
5751
|
+
const scopeName = opts.scopeName ?? "wrongstack";
|
|
5752
|
+
let stopped = false;
|
|
5753
|
+
const buffer = [];
|
|
5754
|
+
const headers = {
|
|
5755
|
+
"content-type": "application/json",
|
|
5756
|
+
...opts.headers ?? {}
|
|
5757
|
+
};
|
|
5758
|
+
if (opts.authorization) headers.authorization = opts.authorization;
|
|
5759
|
+
const tracer = {
|
|
5760
|
+
startSpan(name, attrs) {
|
|
5761
|
+
const state = {
|
|
5762
|
+
traceId: hex(16),
|
|
5763
|
+
spanId: hex(8),
|
|
5764
|
+
name,
|
|
5765
|
+
startTimeUnixNano: nowNs(),
|
|
5766
|
+
attributes: { ...attrs ?? {} },
|
|
5767
|
+
status: { code: SPAN_STATUS_CODE_UNSET }
|
|
5768
|
+
};
|
|
5769
|
+
return new CapturingSpan(state, (ended) => {
|
|
5770
|
+
if (buffer.length >= maxBuffered) buffer.shift();
|
|
5771
|
+
buffer.push(ended);
|
|
5772
|
+
});
|
|
5773
|
+
}
|
|
5774
|
+
};
|
|
5775
|
+
async function pushOnce() {
|
|
5776
|
+
if (buffer.length === 0) return;
|
|
5777
|
+
const batch = buffer.splice(0, buffer.length);
|
|
5778
|
+
const body = buildOtlpTracesRequest(batch, { resourceAttributes, scopeName });
|
|
5779
|
+
const controller = new AbortController();
|
|
5780
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
5781
|
+
try {
|
|
5782
|
+
const res = await fetchImpl(url, {
|
|
5783
|
+
method: "POST",
|
|
5784
|
+
headers,
|
|
5785
|
+
body: JSON.stringify(body),
|
|
5786
|
+
signal: controller.signal
|
|
5787
|
+
});
|
|
5788
|
+
if (!res.ok) {
|
|
5789
|
+
const text = await res.text().catch(() => "");
|
|
5790
|
+
onError(new Error(`OTLP traces push failed: ${res.status} ${res.statusText} ${text}`));
|
|
5791
|
+
}
|
|
5792
|
+
} catch (err) {
|
|
5793
|
+
onError(err);
|
|
5794
|
+
} finally {
|
|
5795
|
+
clearTimeout(timer);
|
|
4374
5796
|
}
|
|
4375
|
-
abortSources.push(s);
|
|
4376
|
-
}
|
|
4377
|
-
for (const s of abortSources) {
|
|
4378
|
-
s.addEventListener("abort", () => ctrl.abort(s.reason), { once: true });
|
|
4379
5797
|
}
|
|
4380
|
-
|
|
5798
|
+
const handle = setInterval(() => {
|
|
5799
|
+
if (!stopped) void pushOnce();
|
|
5800
|
+
}, intervalMs);
|
|
5801
|
+
handle.unref?.();
|
|
5802
|
+
return {
|
|
5803
|
+
tracer,
|
|
5804
|
+
flush: pushOnce,
|
|
5805
|
+
async stop() {
|
|
5806
|
+
stopped = true;
|
|
5807
|
+
clearInterval(handle);
|
|
5808
|
+
await pushOnce().catch(onError);
|
|
5809
|
+
},
|
|
5810
|
+
buffered: () => [...buffer]
|
|
5811
|
+
};
|
|
4381
5812
|
}
|
|
4382
5813
|
|
|
4383
5814
|
// src/defaults/context-manager.ts
|
|
@@ -4544,6 +5975,107 @@ function createContextManagerTool(opts = {}) {
|
|
|
4544
5975
|
}
|
|
4545
5976
|
var contextManagerTool = createContextManagerTool();
|
|
4546
5977
|
|
|
4547
|
-
|
|
5978
|
+
// src/defaults/mcp-servers.ts
|
|
5979
|
+
var filesystemServer = () => ({
|
|
5980
|
+
name: "filesystem",
|
|
5981
|
+
description: "Read, write, and navigate the local filesystem (read-heavy tools)",
|
|
5982
|
+
transport: "stdio",
|
|
5983
|
+
command: "npx",
|
|
5984
|
+
args: ["-y", "@modelcontextprotocol/server-filesystem", "."],
|
|
5985
|
+
permission: "confirm"
|
|
5986
|
+
});
|
|
5987
|
+
var githubServer = () => ({
|
|
5988
|
+
name: "github",
|
|
5989
|
+
description: "GitHub API \u2014 issues, PRs, repos, search, file ops (requires GITHUB_PERSONAL_ACCESS_TOKEN)",
|
|
5990
|
+
transport: "stdio",
|
|
5991
|
+
command: "npx",
|
|
5992
|
+
args: ["-y", "@modelcontextprotocol/server-github"],
|
|
5993
|
+
env: { GITHUB_PERSONAL_ACCESS_TOKEN: process.env.GITHUB_PERSONAL_ACCESS_TOKEN ?? "" },
|
|
5994
|
+
permission: "confirm"
|
|
5995
|
+
});
|
|
5996
|
+
var context7Server = () => ({
|
|
5997
|
+
name: "context7",
|
|
5998
|
+
description: "Codebase-aware documentation and Q&A (context7.ai)",
|
|
5999
|
+
transport: "streamable-http",
|
|
6000
|
+
url: "https://server.context7.ai/mcp",
|
|
6001
|
+
permission: "confirm"
|
|
6002
|
+
});
|
|
6003
|
+
var braveSearchServer = () => ({
|
|
6004
|
+
name: "brave-search",
|
|
6005
|
+
description: "Web search (Brave). Requires BRAVE_SEARCH_API_KEY \u2014 free tier 2k queries/month",
|
|
6006
|
+
transport: "stdio",
|
|
6007
|
+
command: "npx",
|
|
6008
|
+
args: ["-y", "@modelcontextprotocol/server-brave-search"],
|
|
6009
|
+
env: { BRAVE_SEARCH_API_KEY: process.env.BRAVE_SEARCH_API_KEY ?? "" },
|
|
6010
|
+
permission: "confirm"
|
|
6011
|
+
});
|
|
6012
|
+
var blockServer = () => ({
|
|
6013
|
+
name: "block",
|
|
6014
|
+
description: "Postgres database access via SQL (Block MCP server)",
|
|
6015
|
+
transport: "stdio",
|
|
6016
|
+
command: "npx",
|
|
6017
|
+
args: ["-y", "@modelcontextprotocol/server-block"],
|
|
6018
|
+
permission: "confirm"
|
|
6019
|
+
});
|
|
6020
|
+
var everArtServer = () => ({
|
|
6021
|
+
name: "everart",
|
|
6022
|
+
description: "AI image generation (EverArt). Requires EVERART_API_KEY",
|
|
6023
|
+
transport: "stdio",
|
|
6024
|
+
command: "npx",
|
|
6025
|
+
args: ["-y", "@modelcontextprotocol/server-everart"],
|
|
6026
|
+
env: { EVERART_API_KEY: process.env.EVERART_API_KEY ?? "" },
|
|
6027
|
+
permission: "confirm"
|
|
6028
|
+
});
|
|
6029
|
+
var slackServer = () => ({
|
|
6030
|
+
name: "slack",
|
|
6031
|
+
description: "Slack \u2014 messaging, channels, search. Requires SLACK_BOT_TOKEN + SLACK_TEAM_ID",
|
|
6032
|
+
transport: "stdio",
|
|
6033
|
+
command: "npx",
|
|
6034
|
+
args: ["-y", "@modelcontextprotocol/server-slack"],
|
|
6035
|
+
env: {
|
|
6036
|
+
SLACK_BOT_TOKEN: process.env.SLACK_BOT_TOKEN ?? "",
|
|
6037
|
+
SLACK_TEAM_ID: process.env.SLACK_TEAM_ID ?? ""
|
|
6038
|
+
},
|
|
6039
|
+
permission: "confirm"
|
|
6040
|
+
});
|
|
6041
|
+
var awsServer = () => ({
|
|
6042
|
+
name: "aws",
|
|
6043
|
+
description: "AWS \u2014 EC2, S3, Lambda, IAM, CloudFormation, costs. Requires AWS credentials",
|
|
6044
|
+
transport: "stdio",
|
|
6045
|
+
command: "npx",
|
|
6046
|
+
args: ["-y", "@modelcontextprotocol/server-aws"],
|
|
6047
|
+
permission: "confirm"
|
|
6048
|
+
});
|
|
6049
|
+
var googleMapsServer = () => ({
|
|
6050
|
+
name: "google-maps",
|
|
6051
|
+
description: "Google Maps \u2014 directions, geocoding, places. Requires GOOGLE_MAPS_API_KEY",
|
|
6052
|
+
transport: "stdio",
|
|
6053
|
+
command: "npx",
|
|
6054
|
+
args: ["-y", "@modelcontextprotocol/server-google-maps"],
|
|
6055
|
+
env: { GOOGLE_MAPS_API_KEY: process.env.GOOGLE_MAPS_API_KEY ?? "" },
|
|
6056
|
+
permission: "confirm"
|
|
6057
|
+
});
|
|
6058
|
+
var sentinelServer = () => ({
|
|
6059
|
+
name: "sentinel",
|
|
6060
|
+
description: "Security vulnerability scanning (Sentinel)",
|
|
6061
|
+
transport: "streamable-http",
|
|
6062
|
+
url: "https://mcp.sentinel.ai",
|
|
6063
|
+
permission: "deny"
|
|
6064
|
+
// security tool — require explicit confirmation
|
|
6065
|
+
});
|
|
6066
|
+
var allServers = () => ({
|
|
6067
|
+
filesystem: { ...filesystemServer(), enabled: false },
|
|
6068
|
+
github: { ...githubServer(), enabled: false },
|
|
6069
|
+
context7: { ...context7Server(), enabled: false },
|
|
6070
|
+
"brave-search": { ...braveSearchServer(), enabled: false },
|
|
6071
|
+
block: { ...blockServer(), enabled: false },
|
|
6072
|
+
everart: { ...everArtServer(), enabled: false },
|
|
6073
|
+
slack: { ...slackServer(), enabled: false },
|
|
6074
|
+
aws: { ...awsServer(), enabled: false },
|
|
6075
|
+
"google-maps": { ...googleMapsServer(), enabled: false },
|
|
6076
|
+
sentinel: { ...sentinelServer(), enabled: false }
|
|
6077
|
+
});
|
|
6078
|
+
|
|
6079
|
+
export { AutoCompactionMiddleware, AutonomousRunner, BudgetExceededError, ConfigMigrationError, DEFAULT_CONFIG_MIGRATIONS, DefaultAttachmentStore, DefaultConfigLoader, DefaultConfigStore, DefaultErrorHandler, DefaultHealthRegistry, DefaultLogger, DefaultMemoryStore, DefaultModeStore, DefaultModelsRegistry, DefaultMultiAgentCoordinator, DefaultPathResolver, DefaultPermissionPolicy, DefaultRetryPolicy, DefaultSecretScrubber, DefaultSecretVault, DefaultSessionReader, DefaultSessionStore, DefaultSkillLoader, DefaultTaskStore, DefaultTokenCounter, DoneConditionChecker, HybridCompactor, InMemoryAgentBridge, InMemoryBridgeTransport, InMemoryMetricsSink, IntelligentCompactor, LLMSelector, NoopMetricsSink, NoopTracer, OTelTracer, PROMETHEUS_CONTENT_TYPE, QueueStore, RecoveryLock, SelectiveCompactor, SpecDrivenDev, SpecParser, SubagentBudget, TaskFlow, TaskGenerator, TaskTracker, ToolExecutor, allServers, awsServer, blockServer, braveSearchServer, buildOtlpMetricsRequest, buildOtlpTracesRequest, classifyFamily, context7Server, contextManagerTool, createContextManagerTool, createMessage, decryptConfigSecrets, encryptConfigSecrets, everArtServer, filesystemServer, githubServer, googleMapsServer, loadProjectModes, loadUserModes, makeAgentSubagentRunner, migratePlaintextSecrets, renderPrometheus, rewriteConfigEncrypted, runConfigMigrations, sentinelServer, slackServer, startMetricsServer, startOtlpMetricsExporter, startOtlpTraceExporter, wireMetricsToEvents };
|
|
4548
6080
|
//# sourceMappingURL=index.js.map
|
|
4549
6081
|
//# sourceMappingURL=index.js.map
|