@wrongstack/core 0.264.0 → 0.267.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{agent-bridge-D8sa1vtv.d.ts → agent-bridge-STJ3JwwK.d.ts} +1 -1
- package/dist/{agent-subagent-runner-c9DLkaas.d.ts → agent-subagent-runner-CzPGP3jA.d.ts} +131 -11
- package/dist/{brain-O1IdKPaK.d.ts → brain-Cdg77tVN.d.ts} +103 -2
- package/dist/{compactor-BBy0rCtB.d.ts → compactor-iMZ84CXq.d.ts} +19 -1
- package/dist/{config-Dz2F3H2K.d.ts → config-Du3pYYln.d.ts} +132 -13
- package/dist/{context-BGSpZNSE.d.ts → context-dT5Ueund.d.ts} +90 -12
- package/dist/coordination/index.d.ts +78 -22
- package/dist/coordination/index.js +695 -273
- package/dist/coordination/index.js.map +1 -1
- package/dist/{default-config-CXsDvOmP.d.ts → default-config-B0cj-Hry.d.ts} +11 -1
- package/dist/defaults/index.d.ts +28 -28
- package/dist/defaults/index.js +2327 -965
- package/dist/defaults/index.js.map +1 -1
- package/dist/execution/index.d.ts +16 -16
- package/dist/execution/index.js +1500 -371
- package/dist/execution/index.js.map +1 -1
- package/dist/execution/prompt-enhancer.d.ts +2 -2
- package/dist/execution/prompt-enhancer.js +1 -1
- package/dist/execution/prompt-enhancer.js.map +1 -1
- package/dist/extension/index.d.ts +6 -6
- package/dist/{goal-preamble-DzjFuN3p.d.ts → goal-preamble-SulMTowG.d.ts} +33 -12
- package/dist/{goal-store-CxWmCGbH.d.ts → goal-store-CABDwdFE.d.ts} +1 -1
- package/dist/{index-CbLSI66_.d.ts → index-Bms0m4oy.d.ts} +5 -5
- package/dist/{index-CYIQrXVF.d.ts → index-DtCVWel4.d.ts} +8 -8
- package/dist/index-IEuxQd-E.d.ts +82 -0
- package/dist/index.d.ts +261 -57
- package/dist/index.js +4799 -2212
- package/dist/index.js.map +1 -1
- package/dist/infrastructure/index.d.ts +6 -6
- package/dist/infrastructure/index.js +84 -9
- package/dist/infrastructure/index.js.map +1 -1
- package/dist/kernel/index.d.ts +9 -9
- package/dist/kernel/index.js +1 -1
- package/dist/kernel/index.js.map +1 -1
- package/dist/{mcp-servers-DC4QRPUI.d.ts → mcp-servers-C2cBTxUR.d.ts} +3 -3
- package/dist/models/index.d.ts +5 -5
- package/dist/models/index.js +104 -31
- package/dist/models/index.js.map +1 -1
- package/dist/{models-registry-B_siPxqN.d.ts → models-registry-BqGZNJQ-.d.ts} +1 -1
- package/dist/{multi-agent-coordinator-CK5Jdj9K.d.ts → multi-agent-coordinator-B8R43uPz.d.ts} +1 -1
- package/dist/{null-fleet-bus-DgvD4SCO.d.ts → null-fleet-bus-CnXa5oTH.d.ts} +14 -9
- package/dist/observability/index.d.ts +2 -2
- package/dist/{parallel-eternal-engine-bK0JQBR_.d.ts → parallel-eternal-engine-DdNnw9BQ.d.ts} +11 -9
- package/dist/{path-resolver-BPEDlN38.d.ts → path-resolver-COIMLCQL.d.ts} +3 -3
- package/dist/{permission-4yvGmMRB.d.ts → permission-B75JAi3-.d.ts} +1 -1
- package/dist/{permission-policy-C6XpsBOy.d.ts → permission-policy-DlR9eJAM.d.ts} +2 -2
- package/dist/{pipeline-CXCeMz8J.d.ts → pipeline-BfD2k1rT.d.ts} +3 -3
- package/dist/{plan-templates-BvzRBkJc.d.ts → plan-templates-DSIKCXZN.d.ts} +32 -8
- package/dist/provider-model-resolve-BNRsNuJx.d.ts +107 -0
- package/dist/{provider-runner-C5aQpDWE.d.ts → provider-runner-CX7iIvox.d.ts} +3 -3
- package/dist/{retry-policy-CFhdtRzz.d.ts → retry-policy-BilV1ujH.d.ts} +1 -1
- package/dist/sdd/index.d.ts +8 -8
- package/dist/sdd/index.js +286 -105
- package/dist/sdd/index.js.map +1 -1
- package/dist/secret-vault-BAKpgFw_.d.ts +57 -0
- package/dist/{secret-vault-CxiVLbt1.d.ts → secret-vault-gkvEZZfE.d.ts} +43 -4
- package/dist/security/index.d.ts +6 -68
- package/dist/security/index.js +296 -95
- package/dist/security/index.js.map +1 -1
- package/dist/{selector-gIuhRTkN.d.ts → selector-Bc7eWtT3.d.ts} +1 -1
- package/dist/{session-event-bridge-DkvvrpDt.d.ts → session-event-bridge-D-araDEz.d.ts} +1 -1
- package/dist/{session-reader-KdfVwkKP.d.ts → session-reader-D7Dapswh.d.ts} +1 -1
- package/dist/storage/index.d.ts +112 -15
- package/dist/storage/index.js +491 -156
- package/dist/storage/index.js.map +1 -1
- package/dist/tools/index.d.ts +4 -2
- package/dist/tools/index.js.map +1 -1
- package/dist/types/index.d.ts +21 -21
- package/dist/types/index.js +1523 -450
- package/dist/types/index.js.map +1 -1
- package/dist/utils/index.d.ts +455 -407
- package/dist/utils/index.js +2191 -1203
- package/dist/utils/index.js.map +1 -1
- package/dist/{wstack-paths-CJjEwPXn.d.ts → wstack-paths-hOpNLmvf.d.ts} +2 -0
- package/package.json +1 -1
- package/skills/api-design/SKILL.md +1 -1
- package/skills/audit-log/SKILL.md +6 -6
- package/skills/bug-hunter/SKILL.md +5 -5
- package/skills/chimera/SKILL.md +4 -4
- package/skills/docker-deploy/SKILL.md +1 -1
- package/skills/git-flow/SKILL.md +3 -3
- package/skills/multi-agent/SKILL.md +3 -3
- package/skills/node-modern/SKILL.md +1 -0
- package/skills/observability/SKILL.md +2 -2
- package/skills/output-standards/SKILL.md +51 -28
- package/skills/refactor-planner/SKILL.md +3 -3
- package/skills/security-scanner/SKILL.md +4 -3
- package/skills/tech-stack/SKILL.md +1 -2
- package/dist/llm-selector-DzxuZnNz.d.ts +0 -58
- package/dist/secret-vault-BJDY28ev.d.ts +0 -25
package/dist/execution/index.js
CHANGED
|
@@ -1,11 +1,81 @@
|
|
|
1
|
+
import * as path3 from 'path';
|
|
1
2
|
import { randomUUID, randomBytes, createHash } from 'crypto';
|
|
2
3
|
import * as fs from 'fs/promises';
|
|
3
|
-
import * as path2 from 'path';
|
|
4
4
|
import * as os from 'os';
|
|
5
5
|
import { execFile } from 'child_process';
|
|
6
6
|
import { promisify } from 'util';
|
|
7
7
|
import { EventEmitter } from 'events';
|
|
8
8
|
|
|
9
|
+
// src/utils/tool-wire-compact.ts
|
|
10
|
+
var TOOL_DESCRIPTION_MAX_CHARS = 640;
|
|
11
|
+
var SCHEMA_DESCRIPTION_MAX_CHARS = 180;
|
|
12
|
+
var compactCache = /* @__PURE__ */ new WeakMap();
|
|
13
|
+
function compactToolDefinitionForWire(tool, opts = {}) {
|
|
14
|
+
const useDefaultOptions = opts.descriptionMaxChars === void 0 && opts.schemaDescriptionMaxChars === void 0;
|
|
15
|
+
if (useDefaultOptions && typeof tool === "object" && tool !== null) {
|
|
16
|
+
const cached = compactCache.get(tool);
|
|
17
|
+
if (cached) return cached;
|
|
18
|
+
}
|
|
19
|
+
const compact = {
|
|
20
|
+
name: tool.name,
|
|
21
|
+
description: compactDescription(
|
|
22
|
+
tool.description ?? "",
|
|
23
|
+
opts.descriptionMaxChars ?? TOOL_DESCRIPTION_MAX_CHARS
|
|
24
|
+
),
|
|
25
|
+
inputSchema: compactSchemaDescriptions(
|
|
26
|
+
tool.inputSchema,
|
|
27
|
+
opts.schemaDescriptionMaxChars ?? SCHEMA_DESCRIPTION_MAX_CHARS
|
|
28
|
+
)
|
|
29
|
+
};
|
|
30
|
+
if (useDefaultOptions && typeof tool === "object" && tool !== null) {
|
|
31
|
+
compactCache.set(tool, compact);
|
|
32
|
+
}
|
|
33
|
+
return compact;
|
|
34
|
+
}
|
|
35
|
+
function compactSchemaDescriptions(schema, maxDescriptionChars = SCHEMA_DESCRIPTION_MAX_CHARS) {
|
|
36
|
+
const compact = compactSchemaNode(schema, maxDescriptionChars);
|
|
37
|
+
return isRecord(compact) ? compact : { type: "object", properties: {} };
|
|
38
|
+
}
|
|
39
|
+
function compactSchemaNode(node, maxDescriptionChars) {
|
|
40
|
+
if (Array.isArray(node)) {
|
|
41
|
+
return node.map((item) => compactSchemaNode(item, maxDescriptionChars));
|
|
42
|
+
}
|
|
43
|
+
if (!isRecord(node)) return node;
|
|
44
|
+
const out = {};
|
|
45
|
+
for (const [key, value] of Object.entries(node)) {
|
|
46
|
+
if (key === "description" && typeof value === "string") {
|
|
47
|
+
out[key] = compactDescription(value, maxDescriptionChars);
|
|
48
|
+
} else {
|
|
49
|
+
out[key] = compactSchemaNode(value, maxDescriptionChars);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
function compactDescription(text, maxChars) {
|
|
55
|
+
const normalized = text.replace(/\s+/g, " ").trim();
|
|
56
|
+
if (normalized.length <= maxChars) return normalized;
|
|
57
|
+
if (maxChars <= 20) return normalized.slice(0, maxChars);
|
|
58
|
+
const hardLimit = maxChars - 12;
|
|
59
|
+
const boundary = findSemanticBoundary(normalized, hardLimit);
|
|
60
|
+
const head = normalized.slice(0, boundary > 0 ? boundary : hardLimit).trimEnd();
|
|
61
|
+
return `${head} ...`;
|
|
62
|
+
}
|
|
63
|
+
function findSemanticBoundary(text, limit) {
|
|
64
|
+
const punctuation = Math.max(
|
|
65
|
+
text.lastIndexOf(". ", limit),
|
|
66
|
+
text.lastIndexOf("; ", limit),
|
|
67
|
+
text.lastIndexOf(": ", limit)
|
|
68
|
+
);
|
|
69
|
+
if (punctuation >= Math.floor(limit * 0.45)) return punctuation + 1;
|
|
70
|
+
const comma = text.lastIndexOf(", ", limit);
|
|
71
|
+
if (comma >= Math.floor(limit * 0.6)) return comma + 1;
|
|
72
|
+
const space = text.lastIndexOf(" ", limit);
|
|
73
|
+
return space >= Math.floor(limit * 0.6) ? space : limit;
|
|
74
|
+
}
|
|
75
|
+
function isRecord(value) {
|
|
76
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
77
|
+
}
|
|
78
|
+
|
|
9
79
|
// src/utils/token-estimate.ts
|
|
10
80
|
var RoughTokenEstimate = (text, charsPerToken = 3.5) => Math.max(1, Math.ceil(text.length / charsPerToken));
|
|
11
81
|
var CALIBRATION_GLOBAL_KEY = "__global__";
|
|
@@ -25,12 +95,9 @@ function getCachedEstimate(key, compute) {
|
|
|
25
95
|
const existing = ESTIMATE_CACHE.get(key);
|
|
26
96
|
if (existing !== void 0) return existing;
|
|
27
97
|
if (ESTIMATE_CACHE.size >= ESTIMATE_CACHE_MAX_SIZE) {
|
|
28
|
-
let evicted = 0;
|
|
29
|
-
const maxEvict = Math.floor(ESTIMATE_CACHE_MAX_SIZE / 4);
|
|
30
98
|
for (const k of ESTIMATE_CACHE.keys()) {
|
|
31
|
-
if (
|
|
99
|
+
if (ESTIMATE_CACHE.size <= Math.floor(ESTIMATE_CACHE_MAX_SIZE / 2)) break;
|
|
32
100
|
ESTIMATE_CACHE.delete(k);
|
|
33
|
-
evicted++;
|
|
34
101
|
}
|
|
35
102
|
}
|
|
36
103
|
const estimate = compute(key);
|
|
@@ -76,7 +143,8 @@ function estimateMessageTokens(messages) {
|
|
|
76
143
|
function estimateToolDefTokens(tool) {
|
|
77
144
|
const cached = tool._estDefTokens;
|
|
78
145
|
if (typeof cached === "number" && cached > 0) return cached;
|
|
79
|
-
|
|
146
|
+
const compact = compactToolDefinitionForWire(tool);
|
|
147
|
+
return RoughTokenEstimate(tool.name) + RoughTokenEstimate(compact.description) + RoughTokenEstimate(JSON.stringify(compact.inputSchema));
|
|
80
148
|
}
|
|
81
149
|
function estimateRequestTokens(messages, systemPrompt, tools, calibrationKey = CALIBRATION_GLOBAL_KEY) {
|
|
82
150
|
let messagesTokens = 0;
|
|
@@ -254,6 +322,79 @@ function isEmptyMessage(msg) {
|
|
|
254
322
|
if (typeof msg.content === "string") return msg.content.trim().length === 0;
|
|
255
323
|
return msg.content.length === 0;
|
|
256
324
|
}
|
|
325
|
+
var MAX_DIGEST_CHARS = 4e3;
|
|
326
|
+
function createContextEvidenceState() {
|
|
327
|
+
return {
|
|
328
|
+
sessionGoals: [],
|
|
329
|
+
implicitFacts: [],
|
|
330
|
+
activeErrors: [],
|
|
331
|
+
toolCalls: [],
|
|
332
|
+
fileGraph: {},
|
|
333
|
+
repeatedReads: [],
|
|
334
|
+
updatedAt: Date.now()
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
function buildContextEvidenceDigest(ctx) {
|
|
338
|
+
const state = ensureEvidence(ctx);
|
|
339
|
+
const lines = [];
|
|
340
|
+
if (state.currentIntent?.text) {
|
|
341
|
+
lines.push(`intent: ${state.currentIntent.text}`);
|
|
342
|
+
}
|
|
343
|
+
const goals = state.sessionGoals.slice(-3);
|
|
344
|
+
if (goals.length > 0) {
|
|
345
|
+
lines.push("session_goals:");
|
|
346
|
+
for (const goal of goals) lines.push(`- ${goal}`);
|
|
347
|
+
}
|
|
348
|
+
const activeErrors = state.activeErrors.slice(-5);
|
|
349
|
+
if (activeErrors.length > 0) {
|
|
350
|
+
lines.push("active_errors:");
|
|
351
|
+
for (const err of activeErrors) lines.push(`- ${err}`);
|
|
352
|
+
}
|
|
353
|
+
const files = Object.values(state.fileGraph).sort((a, b) => b.writes - a.writes || b.reads - a.reads || a.path.localeCompare(b.path)).slice(0, 12);
|
|
354
|
+
if (files.length > 0) {
|
|
355
|
+
lines.push("dependency_graph:");
|
|
356
|
+
for (const file of files) {
|
|
357
|
+
const actions = [
|
|
358
|
+
file.reads > 0 ? `read ${file.reads}x` : "",
|
|
359
|
+
file.writes > 0 ? `write ${file.writes}x` : ""
|
|
360
|
+
].filter(Boolean).join(", ");
|
|
361
|
+
const refs = file.referenced ? "; referenced by assistant" : "";
|
|
362
|
+
const via = file.lastToolUseId ? `; last via ${file.lastToolUseId}` : "";
|
|
363
|
+
lines.push(`- ${file.path} (${actions || "seen"}${refs}${via})`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
const referenced = state.toolCalls.filter((tool) => tool.status === "referenced").slice(-10);
|
|
367
|
+
const recentSeen = state.toolCalls.filter((tool) => tool.status === "seen").slice(-5);
|
|
368
|
+
const trail = [...referenced, ...recentSeen];
|
|
369
|
+
if (trail.length > 0) {
|
|
370
|
+
lines.push("tool_trail:");
|
|
371
|
+
for (const tool of trail) {
|
|
372
|
+
const size = tool.outputTokens ? `; ~${tool.outputTokens} tokens` : "";
|
|
373
|
+
const filesText = tool.files.length > 0 ? `; files=${tool.files.slice(0, 4).join(", ")}` : "";
|
|
374
|
+
const symbolsText = tool.symbols.length > 0 ? `; symbols=${tool.symbols.slice(0, 4).join(", ")}` : "";
|
|
375
|
+
lines.push(
|
|
376
|
+
`- ${tool.toolUseId} ${tool.toolName} ${tool.status}: ${tool.summary}${filesText}${symbolsText}${size}`
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
const facts = state.implicitFacts.slice(-8);
|
|
381
|
+
if (facts.length > 0) {
|
|
382
|
+
lines.push("implicit_facts:");
|
|
383
|
+
for (const fact of facts) lines.push(`- ${fact}`);
|
|
384
|
+
}
|
|
385
|
+
const digest = lines.join("\n");
|
|
386
|
+
if (digest.length <= MAX_DIGEST_CHARS) return digest;
|
|
387
|
+
return `${digest.slice(0, MAX_DIGEST_CHARS)}... [+${digest.length - MAX_DIGEST_CHARS} chars]`;
|
|
388
|
+
}
|
|
389
|
+
function repeatedReadPressure(ctx) {
|
|
390
|
+
return ensureEvidence(ctx).repeatedReads.reduce((max, item) => Math.max(max, item.count), 0);
|
|
391
|
+
}
|
|
392
|
+
function ensureEvidence(ctx) {
|
|
393
|
+
if (!ctx.contextEvidence) {
|
|
394
|
+
ctx.contextEvidence = createContextEvidenceState();
|
|
395
|
+
}
|
|
396
|
+
return ctx.contextEvidence;
|
|
397
|
+
}
|
|
257
398
|
|
|
258
399
|
// src/types/blocks.ts
|
|
259
400
|
function isTextBlock(b) {
|
|
@@ -261,7 +402,11 @@ function isTextBlock(b) {
|
|
|
261
402
|
}
|
|
262
403
|
|
|
263
404
|
// src/execution/compaction-core.ts
|
|
405
|
+
function compactionDebugEnabled() {
|
|
406
|
+
return process.env["NODE_ENV"] === "development" || process.env["WRONGSTACK_DEBUG"] === "1";
|
|
407
|
+
}
|
|
264
408
|
function emitCompactionMetrics(event, metrics) {
|
|
409
|
+
if (!compactionDebugEnabled()) return;
|
|
265
410
|
console.log(
|
|
266
411
|
JSON.stringify({
|
|
267
412
|
level: "debug",
|
|
@@ -296,38 +441,41 @@ function findPreserveStart(messages, preserveK) {
|
|
|
296
441
|
preserveStart = i;
|
|
297
442
|
}
|
|
298
443
|
}
|
|
299
|
-
let
|
|
300
|
-
let
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
444
|
+
let pairRepairIterations = 0;
|
|
445
|
+
let pairRepairInnerIterations = 0;
|
|
446
|
+
while (preserveStart > 0) {
|
|
447
|
+
pairRepairIterations++;
|
|
448
|
+
const first = messages[preserveStart];
|
|
449
|
+
const prev = messages[preserveStart - 1];
|
|
450
|
+
if (!first || !prev || first.role !== "user" || prev.role !== "assistant") break;
|
|
451
|
+
if (typeof first.content === "string" || typeof prev.content === "string") break;
|
|
452
|
+
const resultIds = /* @__PURE__ */ new Set();
|
|
453
|
+
for (const block of first.content) {
|
|
454
|
+
pairRepairInnerIterations++;
|
|
455
|
+
if (block.type === "tool_result") resultIds.add(block.tool_use_id);
|
|
456
|
+
}
|
|
457
|
+
if (resultIds.size === 0) break;
|
|
458
|
+
const hasMatchingUse = prev.content.some((block) => {
|
|
459
|
+
pairRepairInnerIterations++;
|
|
460
|
+
return block.type === "tool_use" && resultIds.has(block.id);
|
|
308
461
|
});
|
|
309
|
-
if (
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
462
|
+
if (!hasMatchingUse) break;
|
|
463
|
+
preserveStart--;
|
|
464
|
+
}
|
|
465
|
+
if (compactionDebugEnabled()) {
|
|
466
|
+
console.log(
|
|
467
|
+
JSON.stringify({
|
|
468
|
+
level: "debug",
|
|
469
|
+
event: "compaction.find_preserve_start.ended",
|
|
470
|
+
messageCount: messages.length,
|
|
471
|
+
preserveK,
|
|
472
|
+
preserveStart,
|
|
473
|
+
pairRepairIterations,
|
|
474
|
+
pairRepairInnerIterations,
|
|
475
|
+
pairRepairInnerPerOuter: pairRepairIterations > 0 ? pairRepairInnerIterations / pairRepairIterations : 0
|
|
476
|
+
})
|
|
477
|
+
);
|
|
318
478
|
}
|
|
319
|
-
console.log(
|
|
320
|
-
JSON.stringify({
|
|
321
|
-
level: "debug",
|
|
322
|
-
event: "compaction.find_preserve_start.ended",
|
|
323
|
-
messageCount: messages.length,
|
|
324
|
-
preserveK,
|
|
325
|
-
preserveStart,
|
|
326
|
-
forwardWalkIterations,
|
|
327
|
-
forwardWalkInnerIterations,
|
|
328
|
-
forwardWalkInnerPerOuter: forwardWalkIterations > 0 ? forwardWalkInnerIterations / forwardWalkIterations : 0
|
|
329
|
-
})
|
|
330
|
-
);
|
|
331
479
|
return preserveStart;
|
|
332
480
|
}
|
|
333
481
|
function eliseOldToolResults(messages, opts) {
|
|
@@ -341,7 +489,8 @@ function eliseOldToolResults(messages, opts) {
|
|
|
341
489
|
if (!msg || !Array.isArray(msg.content)) continue;
|
|
342
490
|
for (const b of msg.content) {
|
|
343
491
|
fastPathInnerIterations++;
|
|
344
|
-
|
|
492
|
+
const oversized = b.type === "tool_result" && estimateToolResultTokens(b.content) >= opts.eliseThreshold || b.type === "tool_use" && estimateToolInputTokens(b.input) >= opts.eliseThreshold;
|
|
493
|
+
if (oversized) {
|
|
345
494
|
hasOversized = true;
|
|
346
495
|
break;
|
|
347
496
|
}
|
|
@@ -375,6 +524,13 @@ function eliseOldToolResults(messages, opts) {
|
|
|
375
524
|
}
|
|
376
525
|
const original = msg.content;
|
|
377
526
|
const newContent = original.map((b) => {
|
|
527
|
+
if (b.type === "tool_use") {
|
|
528
|
+
const tokens2 = estimateToolInputTokens(b.input);
|
|
529
|
+
if (tokens2 < opts.eliseThreshold) return b;
|
|
530
|
+
const elidedInput = summarizeToolUseInputElision(b, tokens2);
|
|
531
|
+
saved += Math.max(0, tokens2 - estimateToolInputTokens(elidedInput));
|
|
532
|
+
return { ...b, input: elidedInput };
|
|
533
|
+
}
|
|
378
534
|
if (b.type !== "tool_result") return b;
|
|
379
535
|
const tokens = estimateToolResultTokens(b.content);
|
|
380
536
|
if (tokens < opts.eliseThreshold) return b;
|
|
@@ -382,7 +538,7 @@ function eliseOldToolResults(messages, opts) {
|
|
|
382
538
|
const elided = {
|
|
383
539
|
type: "tool_result",
|
|
384
540
|
tool_use_id: b.tool_use_id,
|
|
385
|
-
content:
|
|
541
|
+
content: summarizeToolResultElision(b, tokens),
|
|
386
542
|
is_error: b.is_error
|
|
387
543
|
};
|
|
388
544
|
return elided;
|
|
@@ -394,7 +550,7 @@ function eliseOldToolResults(messages, opts) {
|
|
|
394
550
|
changed = true;
|
|
395
551
|
}
|
|
396
552
|
fullPassInnerIterations += original.length;
|
|
397
|
-
if (
|
|
553
|
+
if (compactionDebugEnabled()) {
|
|
398
554
|
const ratio = fullPassInnerIterations / fullPassIterations;
|
|
399
555
|
if (ratio > 10) {
|
|
400
556
|
console.error(
|
|
@@ -422,6 +578,65 @@ function eliseOldToolResults(messages, opts) {
|
|
|
422
578
|
});
|
|
423
579
|
return { messages: changed ? next : messages, saved, changed };
|
|
424
580
|
}
|
|
581
|
+
function summarizeToolUseInputElision(block, tokens) {
|
|
582
|
+
const fields = {};
|
|
583
|
+
for (const [key, value] of Object.entries(block.input ?? {})) {
|
|
584
|
+
fields[key] = summarizeToolUseInputValue(value);
|
|
585
|
+
}
|
|
586
|
+
return {
|
|
587
|
+
__elided_tool_input: `~${tokens} tokens; original arguments are in the session log`,
|
|
588
|
+
tool: block.name,
|
|
589
|
+
fields
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
function summarizeToolUseInputValue(value) {
|
|
593
|
+
if (value === null || value === void 0) return value;
|
|
594
|
+
if (typeof value === "number" || typeof value === "boolean") return value;
|
|
595
|
+
if (typeof value === "string") {
|
|
596
|
+
const oneLine = value.replace(/\s+/g, " ").trim();
|
|
597
|
+
return oneLine.length <= 160 ? oneLine : `${oneLine.slice(0, 120)}...(${oneLine.length} chars)`;
|
|
598
|
+
}
|
|
599
|
+
if (Array.isArray(value)) {
|
|
600
|
+
return `[array:${value.length}]`;
|
|
601
|
+
}
|
|
602
|
+
if (typeof value === "object") {
|
|
603
|
+
const keys = Object.keys(value);
|
|
604
|
+
return `[object:${keys.slice(0, 8).join(",")}${keys.length > 8 ? ",..." : ""}]`;
|
|
605
|
+
}
|
|
606
|
+
return String(value);
|
|
607
|
+
}
|
|
608
|
+
function summarizeToolResultElision(block, tokens) {
|
|
609
|
+
const parts = [`elided: ~${tokens} tokens`];
|
|
610
|
+
if (block.name) parts.push(`tool=${block.name}`);
|
|
611
|
+
const files = extractPathHints(block.content).slice(0, 5);
|
|
612
|
+
if (files.length > 0) parts.push(`files=${files.join(", ")}`);
|
|
613
|
+
const error = firstErrorLine(block.content);
|
|
614
|
+
if (error) parts.push(`error=${error}`);
|
|
615
|
+
return `[${parts.join("; ")}]`;
|
|
616
|
+
}
|
|
617
|
+
function extractPathHints(content) {
|
|
618
|
+
const text = typeof content === "string" ? content : JSON.stringify(content);
|
|
619
|
+
const out = /* @__PURE__ */ new Set();
|
|
620
|
+
const re = /(?:(?:[A-Za-z]:)?[./\\]?[\w@.-]+(?:[\\/][\w@(). -]+)+\.[A-Za-z0-9]{1,12})/g;
|
|
621
|
+
for (const match of text.matchAll(re)) {
|
|
622
|
+
const clean = match[0]?.replace(/\\/g, "/").replace(/^["'`]+|["'`),;:]+$/g, "");
|
|
623
|
+
if (clean && clean.length <= 220) out.add(clean);
|
|
624
|
+
if (out.size >= 5) break;
|
|
625
|
+
}
|
|
626
|
+
return [...out];
|
|
627
|
+
}
|
|
628
|
+
function firstErrorLine(content) {
|
|
629
|
+
const text = typeof content === "string" ? content : JSON.stringify(content);
|
|
630
|
+
for (const line of text.split(/\r?\n/)) {
|
|
631
|
+
if (!/\b(error|exception|failed|failure|fatal|panic|timeout|denied|enoent|eacces|eperm)\b/i.test(
|
|
632
|
+
line
|
|
633
|
+
))
|
|
634
|
+
continue;
|
|
635
|
+
const trimmed = line.replace(/\s+/g, " ").trim();
|
|
636
|
+
if (trimmed) return trimmed.slice(0, 180);
|
|
637
|
+
}
|
|
638
|
+
return void 0;
|
|
639
|
+
}
|
|
425
640
|
function buildLosslessDigest(messages) {
|
|
426
641
|
const lines = [];
|
|
427
642
|
for (const m of messages) {
|
|
@@ -532,15 +747,15 @@ function buildSmartDigest(messages) {
|
|
|
532
747
|
lines.push(`[${m.role}]: ${display}${marker}`);
|
|
533
748
|
}
|
|
534
749
|
if (noiseCount > 0) {
|
|
535
|
-
lines.push(
|
|
750
|
+
lines.push(
|
|
751
|
+
`[system]: ${noiseCount} low-importance turn(s) collapsed (repeated failures / pure tool I/O)`
|
|
752
|
+
);
|
|
536
753
|
}
|
|
537
754
|
return lines.join("\n");
|
|
538
755
|
}
|
|
539
756
|
function countToolBlocks(m) {
|
|
540
757
|
if (typeof m.content === "string") return 0;
|
|
541
|
-
return m.content.filter(
|
|
542
|
-
(b) => b.type === "tool_use" || b.type === "tool_result"
|
|
543
|
-
).length;
|
|
758
|
+
return m.content.filter((b) => b.type === "tool_use" || b.type === "tool_result").length;
|
|
544
759
|
}
|
|
545
760
|
function firstSentence(text) {
|
|
546
761
|
const trimmed = text.trim();
|
|
@@ -609,10 +824,12 @@ var HybridCompactor = class {
|
|
|
609
824
|
if (elide.changed) ctx.state.replaceMessages(elide.messages);
|
|
610
825
|
if (elide.saved > 0) reductions.push({ phase: "elision", saved: elide.saved });
|
|
611
826
|
let collapsedDigest;
|
|
827
|
+
let evidenceDigest;
|
|
612
828
|
if (opts.aggressive) {
|
|
613
829
|
const phase2 = this.collapseAncientTurns(ctx, preserveK);
|
|
614
830
|
if (phase2.saved > 0) reductions.push({ phase: "summary", saved: phase2.saved });
|
|
615
831
|
collapsedDigest = phase2.digest;
|
|
832
|
+
evidenceDigest = phase2.evidenceDigest;
|
|
616
833
|
}
|
|
617
834
|
const repaired = repairToolUseAdjacency(ctx.messages);
|
|
618
835
|
if (repaired.report.changed) {
|
|
@@ -620,6 +837,11 @@ var HybridCompactor = class {
|
|
|
620
837
|
}
|
|
621
838
|
const afterTokens = estimateMessages(ctx.messages);
|
|
622
839
|
const afterFull = this.estimateFullRequest(ctx);
|
|
840
|
+
const quality = checkCompactionQuality(ctx, {
|
|
841
|
+
collapsedDigest,
|
|
842
|
+
evidenceDigest,
|
|
843
|
+
reduced: beforeTokens > afterTokens || beforeFull > afterFull
|
|
844
|
+
});
|
|
623
845
|
return {
|
|
624
846
|
before: beforeTokens,
|
|
625
847
|
after: afterTokens,
|
|
@@ -627,6 +849,8 @@ var HybridCompactor = class {
|
|
|
627
849
|
fullRequestTokensAfter: afterFull,
|
|
628
850
|
reductions,
|
|
629
851
|
collapsedDigest,
|
|
852
|
+
evidenceDigest,
|
|
853
|
+
quality,
|
|
630
854
|
repaired: repaired.report.changed ? {
|
|
631
855
|
removedToolUses: repaired.report.removedToolUses,
|
|
632
856
|
removedToolResults: repaired.report.removedToolResults,
|
|
@@ -668,7 +892,13 @@ var HybridCompactor = class {
|
|
|
668
892
|
if (boundary <= 0) return { saved: 0 };
|
|
669
893
|
const removed = messages.slice(0, boundary);
|
|
670
894
|
const removedTokens = estimateMessages(removed);
|
|
671
|
-
const
|
|
895
|
+
const historyDigest = this.smart ? buildSmartDigest(removed) || `${removed.length} earlier turns (no textual content; tool I/O omitted; see session log)` : buildLosslessDigest(removed) || `${removed.length} earlier turns (no textual content; tool I/O omitted; see session log)`;
|
|
896
|
+
const evidenceDigest = buildContextEvidenceDigest(ctx);
|
|
897
|
+
const digest = evidenceDigest ? `[context_state]
|
|
898
|
+
${evidenceDigest}
|
|
899
|
+
|
|
900
|
+
[prior_history]
|
|
901
|
+
${historyDigest}` : historyDigest;
|
|
672
902
|
const summaryMsg = {
|
|
673
903
|
role: "system",
|
|
674
904
|
content: `[prior_turns_digest: ${digest}]`
|
|
@@ -677,10 +907,29 @@ var HybridCompactor = class {
|
|
|
677
907
|
ctx.state.replaceMessages([summaryMsg, ...tail]);
|
|
678
908
|
return {
|
|
679
909
|
saved: Math.max(0, removedTokens - estimateMessages([summaryMsg])),
|
|
680
|
-
digest
|
|
910
|
+
digest,
|
|
911
|
+
evidenceDigest: evidenceDigest || void 0
|
|
681
912
|
};
|
|
682
913
|
}
|
|
683
914
|
};
|
|
915
|
+
function checkCompactionQuality(ctx, opts) {
|
|
916
|
+
const evidence = ctx.contextEvidence;
|
|
917
|
+
const digest = `${opts.collapsedDigest ?? ""}
|
|
918
|
+
${opts.evidenceDigest ?? ""}`;
|
|
919
|
+
const hasIntent = Boolean(evidence?.currentIntent?.text || /\b(intent|goal|session_goals)\b/i.test(digest));
|
|
920
|
+
const hasPathTrail = Boolean(
|
|
921
|
+
Object.keys(evidence?.fileGraph ?? {}).length > 0 || (evidence?.toolCalls.length ?? 0) > 0 || /\b(dependency_graph|tool_trail|files=)\b/i.test(digest)
|
|
922
|
+
);
|
|
923
|
+
const issues = [];
|
|
924
|
+
if (opts.reduced && !hasIntent) issues.push("missing intent anchor");
|
|
925
|
+
if (opts.reduced && !hasPathTrail) issues.push("missing tool/path trail");
|
|
926
|
+
return {
|
|
927
|
+
ok: issues.length === 0,
|
|
928
|
+
hasIntent,
|
|
929
|
+
hasPathTrail,
|
|
930
|
+
issues
|
|
931
|
+
};
|
|
932
|
+
}
|
|
684
933
|
function readContextWindowPolicy(ctx) {
|
|
685
934
|
const policy = ctx.meta?.["contextWindowPolicy"];
|
|
686
935
|
if (!policy || typeof policy !== "object") return null;
|
|
@@ -801,7 +1050,12 @@ var IntelligentCompactor = class {
|
|
|
801
1050
|
};
|
|
802
1051
|
const ac = ctx.signal ? void 0 : new AbortController();
|
|
803
1052
|
const signal = ctx.signal ?? ac?.signal;
|
|
804
|
-
|
|
1053
|
+
let res;
|
|
1054
|
+
try {
|
|
1055
|
+
res = await this.provider.complete(req, { signal });
|
|
1056
|
+
} finally {
|
|
1057
|
+
ac?.abort();
|
|
1058
|
+
}
|
|
805
1059
|
const textBlocks = res.content.filter(isTextBlock);
|
|
806
1060
|
return textBlocks.map((b) => b.text).join("\n").trim() || "(empty summary)";
|
|
807
1061
|
}
|
|
@@ -845,9 +1099,9 @@ Rules:
|
|
|
845
1099
|
- If unsure, keep rather than collapse (errors are more costly than waste)
|
|
846
1100
|
|
|
847
1101
|
Return ONLY the JSON object, no markdown, no explanation outside the JSON.`;
|
|
848
|
-
function formatMessages(messages,
|
|
1102
|
+
function formatMessages(messages, maxTokens = 2048) {
|
|
849
1103
|
const lines = [];
|
|
850
|
-
let
|
|
1104
|
+
let usedTokens = 0;
|
|
851
1105
|
for (let i = 0; i < messages.length; i++) {
|
|
852
1106
|
const m = expectDefined(messages[i]);
|
|
853
1107
|
const role = m.role.padEnd(10, " ");
|
|
@@ -859,13 +1113,14 @@ function formatMessages(messages, maxChars = 8e3) {
|
|
|
859
1113
|
text = content.filter(isTextBlock).map((b) => b.text).join(" ");
|
|
860
1114
|
const toolUses = content.filter((b) => b.type === "tool_use");
|
|
861
1115
|
if (toolUses.length > 0) {
|
|
862
|
-
text += ` [tools: ${toolUses.map((b) => b.name).join(", ")}]`;
|
|
1116
|
+
text += ` [tools: ${toolUses.map((b) => b.name).filter(Boolean).join(", ")}]`;
|
|
863
1117
|
}
|
|
864
1118
|
}
|
|
865
1119
|
const line = `[${i}][${role}]: ${text}`;
|
|
866
|
-
|
|
1120
|
+
const lineTokens = estimateTextTokens(line);
|
|
1121
|
+
if (usedTokens + lineTokens > maxTokens) break;
|
|
867
1122
|
lines.push(line);
|
|
868
|
-
|
|
1123
|
+
usedTokens += lineTokens;
|
|
869
1124
|
}
|
|
870
1125
|
return lines.join("\n");
|
|
871
1126
|
}
|
|
@@ -874,20 +1129,29 @@ var LLMSelector = class {
|
|
|
874
1129
|
model;
|
|
875
1130
|
maxContextTokens;
|
|
876
1131
|
systemPrompt;
|
|
1132
|
+
maxOutputTokens;
|
|
877
1133
|
constructor(opts) {
|
|
878
1134
|
this.provider = opts.provider;
|
|
879
1135
|
this.model = opts.model ?? "unknown";
|
|
1136
|
+
if (this.model === "unknown" && (process.env["NODE_ENV"] === "development" || process.env["WRONGSTACK_DEBUG"] === "1")) {
|
|
1137
|
+
console.warn(
|
|
1138
|
+
"[LLMSelector] model not set \u2014 selector will use the provider default. Set `model` explicitly in LLMSelectorOptions to silence this warning."
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
880
1141
|
this.maxContextTokens = opts.maxContextTokens ?? 4e4;
|
|
881
1142
|
this.systemPrompt = opts.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
1143
|
+
this.maxOutputTokens = opts.maxOutputTokens ?? 1024;
|
|
882
1144
|
}
|
|
883
1145
|
async select(messages, maxToKeep) {
|
|
884
1146
|
const effectiveBudget = Math.min(maxToKeep, this.maxContextTokens);
|
|
885
|
-
const historyText = formatMessages(messages);
|
|
886
1147
|
const totalTokens = estimateMessageTokens(messages);
|
|
887
1148
|
const systemText = `${this.systemPrompt}
|
|
888
1149
|
|
|
889
1150
|
Conversation (${messages.length} messages, ~${totalTokens} tokens, budget: ${effectiveBudget}):
|
|
890
1151
|
`;
|
|
1152
|
+
const systemTokens = estimateTextTokens(systemText);
|
|
1153
|
+
const historyBudget = Math.max(512, effectiveBudget - systemTokens - this.maxOutputTokens);
|
|
1154
|
+
const historyText = formatMessages(messages, historyBudget);
|
|
891
1155
|
const budgetInstruction = totalTokens > effectiveBudget ? `
|
|
892
1156
|
|
|
893
1157
|
IMPORTANT: Total conversation (${totalTokens} tokens) exceeds budget (${effectiveBudget}). You MUST collapse enough to fit. Prefer collapsing older/lower-importance ranges.` : "";
|
|
@@ -895,18 +1159,26 @@ IMPORTANT: Total conversation (${totalTokens} tokens) exceeds budget (${effectiv
|
|
|
895
1159
|
model: this.model,
|
|
896
1160
|
system: [{ type: "text", text: systemText + budgetInstruction }],
|
|
897
1161
|
messages: [{ role: "user", content: historyText }],
|
|
898
|
-
maxTokens:
|
|
1162
|
+
maxTokens: this.maxOutputTokens
|
|
899
1163
|
};
|
|
900
1164
|
let raw;
|
|
1165
|
+
const ac = new AbortController();
|
|
901
1166
|
try {
|
|
902
|
-
const
|
|
903
|
-
const res = await this.provider.complete(req, {
|
|
1167
|
+
const timeoutSignal = AbortSignal.timeout(3e4);
|
|
1168
|
+
const res = await this.provider.complete(req, {
|
|
1169
|
+
signal: AbortSignal.any([ac.signal, timeoutSignal])
|
|
1170
|
+
});
|
|
904
1171
|
const textBlocks = res.content.filter(isTextBlock);
|
|
905
1172
|
raw = textBlocks.map((b) => b.text).join("\n").trim();
|
|
906
|
-
} catch (
|
|
1173
|
+
} catch (err) {
|
|
1174
|
+
if (err instanceof Error) {
|
|
1175
|
+
console.warn("[LLMSelector] selector call failed, using recency fallback:", err.message);
|
|
1176
|
+
}
|
|
907
1177
|
return this.fallbackSelect(messages, effectiveBudget);
|
|
1178
|
+
} finally {
|
|
1179
|
+
ac.abort();
|
|
908
1180
|
}
|
|
909
|
-
return this.parseSelectorOutput(raw, messages
|
|
1181
|
+
return this.parseSelectorOutput(raw, messages);
|
|
910
1182
|
}
|
|
911
1183
|
fallbackSelect(messages, budget) {
|
|
912
1184
|
const toKeep = [];
|
|
@@ -933,34 +1205,63 @@ IMPORTANT: Total conversation (${totalTokens} tokens) exceeds budget (${effectiv
|
|
|
933
1205
|
reasoning: `Fallback: kept last ${messages.length - startIdx} messages within ${budget} token budget`
|
|
934
1206
|
};
|
|
935
1207
|
}
|
|
936
|
-
|
|
1208
|
+
/**
|
|
1209
|
+
* Parse and validate the raw LLM output into a SelectorResult.
|
|
1210
|
+
* Falls back to recency-based selection if the LLM output is malformed,
|
|
1211
|
+
* out-of-bounds, or internally inconsistent.
|
|
1212
|
+
*/
|
|
1213
|
+
parseSelectorOutput(raw, messages) {
|
|
1214
|
+
const messageCount = messages.length;
|
|
1215
|
+
if (messageCount === 0) {
|
|
1216
|
+
return { kept: [], collapsed: [], reasoning: "empty session" };
|
|
1217
|
+
}
|
|
937
1218
|
const jsonStart = raw.indexOf("{");
|
|
938
1219
|
const jsonEnd = raw.lastIndexOf("}");
|
|
939
1220
|
if (jsonStart === -1 || jsonEnd === -1) {
|
|
940
|
-
return this.fallbackSelect(
|
|
941
|
-
Array.from({ length: messageCount }, () => ({ role: "user", content: "" })),
|
|
942
|
-
this.maxContextTokens
|
|
943
|
-
);
|
|
1221
|
+
return this.fallbackSelect(messages, this.maxContextTokens);
|
|
944
1222
|
}
|
|
945
1223
|
let parsed;
|
|
946
1224
|
try {
|
|
947
1225
|
parsed = JSON.parse(raw.slice(jsonStart, jsonEnd + 1));
|
|
948
1226
|
} catch {
|
|
949
|
-
return this.fallbackSelect(
|
|
950
|
-
Array.from({ length: messageCount }, () => ({ role: "user", content: "" })),
|
|
951
|
-
this.maxContextTokens
|
|
952
|
-
);
|
|
1227
|
+
return this.fallbackSelect(messages, this.maxContextTokens);
|
|
953
1228
|
}
|
|
954
1229
|
const obj = parsed;
|
|
955
|
-
const
|
|
956
|
-
const
|
|
957
|
-
|
|
958
|
-
|
|
1230
|
+
const keptRaw = obj.kept ?? [];
|
|
1231
|
+
const collapsedRaw = obj.collapsed ?? [];
|
|
1232
|
+
const kept = [];
|
|
1233
|
+
for (const k of keptRaw) {
|
|
1234
|
+
if (typeof k.from !== "number" || typeof k.to !== "number" || k.from < 0 || k.to >= messageCount || k.from > k.to) {
|
|
1235
|
+
return this.fallbackSelect(messages, this.maxContextTokens);
|
|
1236
|
+
}
|
|
1237
|
+
kept.push({
|
|
959
1238
|
from: k.from,
|
|
960
1239
|
to: k.to,
|
|
961
1240
|
importance: k.importance ?? "medium"
|
|
962
|
-
})
|
|
963
|
-
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
const collapsed = [];
|
|
1244
|
+
for (const c of collapsedRaw) {
|
|
1245
|
+
if (typeof c.from !== "number" || typeof c.to !== "number" || c.from < 0 || c.to >= messageCount || c.from > c.to) {
|
|
1246
|
+
return this.fallbackSelect(messages, this.maxContextTokens);
|
|
1247
|
+
}
|
|
1248
|
+
collapsed.push({ from: c.from, to: c.to, summary: c.summary });
|
|
1249
|
+
}
|
|
1250
|
+
const allRanges = [...kept, ...collapsed];
|
|
1251
|
+
for (let i = 0; i < allRanges.length; i++) {
|
|
1252
|
+
const a = allRanges[i];
|
|
1253
|
+
if (!a) continue;
|
|
1254
|
+
for (let j = i + 1; j < allRanges.length; j++) {
|
|
1255
|
+
const b = allRanges[j];
|
|
1256
|
+
if (!b) continue;
|
|
1257
|
+
if (a.from <= b.to && a.to >= b.from) {
|
|
1258
|
+
return this.fallbackSelect(messages, this.maxContextTokens);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
return {
|
|
1263
|
+
kept,
|
|
1264
|
+
collapsed,
|
|
964
1265
|
reasoning: typeof obj.reasoning === "string" ? obj.reasoning : ""
|
|
965
1266
|
};
|
|
966
1267
|
}
|
|
@@ -980,14 +1281,19 @@ var SelectiveCompactor = class {
|
|
|
980
1281
|
summarizerPrompt;
|
|
981
1282
|
constructor(opts) {
|
|
982
1283
|
this.provider = opts.provider;
|
|
983
|
-
this.selector = opts.selector ?? new LLMSelector({ provider: opts.provider, model: opts.selectorModel });
|
|
1284
|
+
this.selector = opts.selector ?? new LLMSelector({ provider: opts.provider, model: opts.selectorModel, maxOutputTokens: opts.selectorMaxOutputTokens });
|
|
984
1285
|
this.warnThreshold = opts.warnThreshold ?? 0.6;
|
|
985
1286
|
this.softThreshold = opts.softThreshold ?? 0.75;
|
|
986
1287
|
this.hardThreshold = opts.hardThreshold ?? 0.9;
|
|
987
1288
|
this.maxContext = opts.maxContext ?? 128e3;
|
|
988
1289
|
this.preserveK = opts.preserveK ?? 4;
|
|
989
1290
|
this.eliseThreshold = opts.eliseThreshold ?? 500;
|
|
990
|
-
this.summarizerModel = opts.summarizerModel ?? opts.selectorModel
|
|
1291
|
+
this.summarizerModel = opts.summarizerModel ?? opts.selectorModel;
|
|
1292
|
+
if (this.summarizerModel === void 0 && (process.env["NODE_ENV"] === "development" || process.env["WRONGSTACK_DEBUG"] === "1")) {
|
|
1293
|
+
console.warn(
|
|
1294
|
+
"[SelectiveCompactor] summarizerModel not set \u2014 will fall back to ctx.model at summarize time. Set `summarizerModel` explicitly to silence this warning."
|
|
1295
|
+
);
|
|
1296
|
+
}
|
|
991
1297
|
this.summarizerPrompt = opts.summarizerPrompt ?? "You are a context summarizer. Given a list of messages, produce a concise summary that preserves all factual information, decisions, file changes, and state changes. Do not add commentary or opinions.";
|
|
992
1298
|
}
|
|
993
1299
|
async compact(ctx, opts = {}) {
|
|
@@ -1093,14 +1399,15 @@ var SelectiveCompactor = class {
|
|
|
1093
1399
|
Summarize the following message range:`;
|
|
1094
1400
|
const body = messages.map((m, i) => `[${i}] ${m.role}: ${this.messagePreview(m)}`).join("\n");
|
|
1095
1401
|
const req = {
|
|
1096
|
-
model: this.summarizerModel,
|
|
1402
|
+
model: this.summarizerModel ?? ctx.model,
|
|
1097
1403
|
system: [{ type: "text", text: systemText }],
|
|
1098
1404
|
messages: [{ role: "user", content: body }],
|
|
1099
1405
|
maxTokens: 512
|
|
1100
1406
|
};
|
|
1101
1407
|
try {
|
|
1408
|
+
const timeoutSignal = AbortSignal.timeout(3e4);
|
|
1102
1409
|
const res = await this.provider.complete(req, {
|
|
1103
|
-
signal: ctx.signal
|
|
1410
|
+
signal: AbortSignal.any([ctx.signal, timeoutSignal])
|
|
1104
1411
|
});
|
|
1105
1412
|
return res.content.filter(isTextBlock).map((b) => b.text).join("\n").trim() || "(empty)";
|
|
1106
1413
|
} catch {
|
|
@@ -1159,27 +1466,16 @@ Summarize the following message range:`;
|
|
|
1159
1466
|
if (typeof m.content === "string") return m.content.trim().length > 0;
|
|
1160
1467
|
return m.content.some((b) => b.type === "text" && b.text.trim().length > 0);
|
|
1161
1468
|
}
|
|
1469
|
+
/**
|
|
1470
|
+
* Estimate message-array tokens via the shared `estimateMessages` primitive
|
|
1471
|
+
* so SelectiveCompactor's before/after/load figures agree with the
|
|
1472
|
+
* middleware threshold math and the other compactors. Previously this used a
|
|
1473
|
+
* private `ceil(len/3.5)` walk that diverged from the calibrated shared
|
|
1474
|
+
* estimator, causing the selective `load`/`targetBudget` comparison to mix
|
|
1475
|
+
* two incompatible token scales.
|
|
1476
|
+
*/
|
|
1162
1477
|
estimateTokens(messages) {
|
|
1163
|
-
|
|
1164
|
-
for (const m of messages) {
|
|
1165
|
-
if (typeof m.content === "string") {
|
|
1166
|
-
total += this.roughTokenEstimate(m.content);
|
|
1167
|
-
} else {
|
|
1168
|
-
for (const b of m.content) {
|
|
1169
|
-
if (b.type === "text") total += this.roughTokenEstimate(b.text);
|
|
1170
|
-
else if (b.type === "tool_use") total += this.roughTokenEstimate(JSON.stringify(b.input));
|
|
1171
|
-
else if (b.type === "tool_result") {
|
|
1172
|
-
total += this.roughTokenEstimate(
|
|
1173
|
-
typeof b.content === "string" ? b.content : JSON.stringify(b.content)
|
|
1174
|
-
);
|
|
1175
|
-
}
|
|
1176
|
-
}
|
|
1177
|
-
}
|
|
1178
|
-
}
|
|
1179
|
-
return total;
|
|
1180
|
-
}
|
|
1181
|
-
roughTokenEstimate(text) {
|
|
1182
|
-
return Math.max(1, Math.ceil(text.length / 3.5));
|
|
1478
|
+
return estimateMessages(messages);
|
|
1183
1479
|
}
|
|
1184
1480
|
};
|
|
1185
1481
|
|
|
@@ -1234,6 +1530,7 @@ var ProviderBackedCompactor = class {
|
|
|
1234
1530
|
return new SelectiveCompactor({
|
|
1235
1531
|
...common,
|
|
1236
1532
|
selectorModel: this.opts.summarizerModel,
|
|
1533
|
+
selectorMaxOutputTokens: this.opts.selectorMaxOutputTokens,
|
|
1237
1534
|
summarizerModel: this.opts.summarizerModel
|
|
1238
1535
|
});
|
|
1239
1536
|
}
|
|
@@ -1250,10 +1547,19 @@ function readPolicy(ctx) {
|
|
|
1250
1547
|
if (typeof candidate.preserveK !== "number" || !candidate.thresholds) return null;
|
|
1251
1548
|
return candidate;
|
|
1252
1549
|
}
|
|
1550
|
+
|
|
1551
|
+
// src/utils/assert-never.ts
|
|
1552
|
+
function assertNever(x, message) {
|
|
1553
|
+
const err = new Error(
|
|
1554
|
+
`Unhandled case: ${JSON.stringify(x)}`
|
|
1555
|
+
);
|
|
1556
|
+
err.name = "AssertNeverError";
|
|
1557
|
+
throw err;
|
|
1558
|
+
}
|
|
1253
1559
|
async function atomicWrite(targetPath, content, opts = {}) {
|
|
1254
|
-
const dir =
|
|
1560
|
+
const dir = path3.dirname(targetPath);
|
|
1255
1561
|
await fs.mkdir(dir, { recursive: true });
|
|
1256
|
-
const tmp =
|
|
1562
|
+
const tmp = path3.join(dir, `.${path3.basename(targetPath)}.${randomBytes(6).toString("hex")}.tmp`);
|
|
1257
1563
|
try {
|
|
1258
1564
|
if (typeof content === "string") {
|
|
1259
1565
|
await fs.writeFile(tmp, content, { flag: "wx", encoding: opts.encoding ?? "utf8" });
|
|
@@ -1306,7 +1612,7 @@ async function renameWithRetry(from, to) {
|
|
|
1306
1612
|
if (!code || !TRANSIENT_RENAME_CODES.has(code) || i === delays.length) {
|
|
1307
1613
|
throw err;
|
|
1308
1614
|
}
|
|
1309
|
-
await new Promise((
|
|
1615
|
+
await new Promise((resolve3) => setTimeout(resolve3, delays[i]));
|
|
1310
1616
|
}
|
|
1311
1617
|
}
|
|
1312
1618
|
throw lastErr;
|
|
@@ -1317,136 +1623,17 @@ function toErrorMessage(err) {
|
|
|
1317
1623
|
return err instanceof Error ? err.message : String(err);
|
|
1318
1624
|
}
|
|
1319
1625
|
|
|
1320
|
-
// src/utils/string.ts
|
|
1321
|
-
function truncate(s, max) {
|
|
1322
|
-
return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
|
|
1323
|
-
}
|
|
1324
|
-
function projectHash(absRoot) {
|
|
1325
|
-
return createHash("sha256").update(path2.resolve(absRoot)).digest("hex").slice(0, 12);
|
|
1326
|
-
}
|
|
1327
|
-
function projectSlug(absRoot) {
|
|
1328
|
-
const base = slugify(path2.basename(absRoot));
|
|
1329
|
-
const hash = createHash("sha256").update(path2.resolve(absRoot)).digest("hex").slice(0, 6);
|
|
1330
|
-
return `${base}-${hash}`;
|
|
1331
|
-
}
|
|
1332
|
-
function slugify(name) {
|
|
1333
|
-
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "project";
|
|
1334
|
-
}
|
|
1335
|
-
function wstackGlobalRoot() {
|
|
1336
|
-
const fromEnv = process.env["WRONGSTACK_HOME"];
|
|
1337
|
-
if (fromEnv && fromEnv.trim().length > 0) return path2.resolve(fromEnv);
|
|
1338
|
-
return path2.join(os.homedir(), ".wrongstack");
|
|
1339
|
-
}
|
|
1340
|
-
function resolveWstackPaths(opts) {
|
|
1341
|
-
const globalRoot = opts.globalRoot ?? (opts.userHome ? path2.join(opts.userHome, ".wrongstack") : wstackGlobalRoot());
|
|
1342
|
-
const hash = projectHash(opts.projectRoot);
|
|
1343
|
-
const slug = projectSlug(opts.projectRoot);
|
|
1344
|
-
const projectDir = path2.join(globalRoot, "projects", slug);
|
|
1345
|
-
return {
|
|
1346
|
-
globalRoot,
|
|
1347
|
-
configDir: globalRoot,
|
|
1348
|
-
globalConfig: path2.join(globalRoot, "config.json"),
|
|
1349
|
-
secretsKey: path2.join(globalRoot, ".key"),
|
|
1350
|
-
globalMemory: path2.join(globalRoot, "memory.md"),
|
|
1351
|
-
globalSkills: path2.join(globalRoot, "skills"),
|
|
1352
|
-
globalPrompts: path2.join(globalRoot, "prompts"),
|
|
1353
|
-
cacheDir: path2.join(globalRoot, "cache"),
|
|
1354
|
-
modelsCache: path2.join(globalRoot, "cache", "models.dev.json"),
|
|
1355
|
-
modelsOverlayCache: path2.join(globalRoot, "cache", "models-overlay.json"),
|
|
1356
|
-
historyFile: path2.join(globalRoot, "history"),
|
|
1357
|
-
logFile: path2.join(globalRoot, "logs", "wrongstack.log"),
|
|
1358
|
-
projectDir,
|
|
1359
|
-
projectCodebaseIndex: path2.join(projectDir, "codebase-index"),
|
|
1360
|
-
projectMemory: path2.join(projectDir, "memory.md"),
|
|
1361
|
-
projectSessions: path2.join(projectDir, "sessions"),
|
|
1362
|
-
projectTrust: path2.join(projectDir, "trust.json"),
|
|
1363
|
-
projectMeta: path2.join(projectDir, "meta.json"),
|
|
1364
|
-
projectLocalConfig: path2.join(projectDir, "config.local.json"),
|
|
1365
|
-
inProjectConfig: path2.join(opts.projectRoot, ".wrongstack", "config.json"),
|
|
1366
|
-
inProjectAgentsFile: path2.join(opts.projectRoot, ".wrongstack", "AGENTS.md"),
|
|
1367
|
-
inProjectSkills: path2.join(opts.projectRoot, ".wrongstack", "skills"),
|
|
1368
|
-
inProjectWorktrees: path2.join(opts.projectRoot, ".wrongstack", "worktrees"),
|
|
1369
|
-
projectHash: hash,
|
|
1370
|
-
projectSlug: slug,
|
|
1371
|
-
projectGoal: path2.join(projectDir, "goal.json"),
|
|
1372
|
-
projectSpecs: path2.join(projectDir, "specs"),
|
|
1373
|
-
projectTaskGraphs: path2.join(projectDir, "task-graphs"),
|
|
1374
|
-
projectSddSession: path2.join(projectDir, "sdd-session.json"),
|
|
1375
|
-
projectPlan: path2.join(projectDir, "plan.json"),
|
|
1376
|
-
projectAutophase: path2.join(projectDir, "autophase"),
|
|
1377
|
-
syncConfig: path2.join(globalRoot, "sync.json")
|
|
1378
|
-
};
|
|
1379
|
-
}
|
|
1380
|
-
|
|
1381
|
-
// src/utils/sleep.ts
|
|
1382
|
-
function sleep(ms) {
|
|
1383
|
-
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
// src/utils/assert-never.ts
|
|
1387
|
-
function assertNever(x, message) {
|
|
1388
|
-
const err = new Error(
|
|
1389
|
-
`Unhandled case: ${JSON.stringify(x)}`
|
|
1390
|
-
);
|
|
1391
|
-
err.name = "AssertNeverError";
|
|
1392
|
-
throw err;
|
|
1393
|
-
}
|
|
1394
|
-
|
|
1395
|
-
// src/utils/tool-output-serializer.ts
|
|
1396
|
-
function createToolOutputSerializer(opts = {}) {
|
|
1397
|
-
const capBytes = opts.perIterationOutputCapBytes ?? 1e5;
|
|
1398
|
-
function serialize(value) {
|
|
1399
|
-
if (typeof value === "string") return value;
|
|
1400
|
-
if (value === null || value === void 0) return "";
|
|
1401
|
-
if (typeof value === "object") {
|
|
1402
|
-
if (Array.isArray(value)) return value.map(serialize).join("\n");
|
|
1403
|
-
if ("text" in value) {
|
|
1404
|
-
const t = value.text;
|
|
1405
|
-
return typeof t === "string" ? t : JSON.stringify(value, null, 2);
|
|
1406
|
-
}
|
|
1407
|
-
try {
|
|
1408
|
-
return JSON.stringify(value, null, 2);
|
|
1409
|
-
} catch {
|
|
1410
|
-
return String(value);
|
|
1411
|
-
}
|
|
1412
|
-
}
|
|
1413
|
-
return String(value);
|
|
1414
|
-
}
|
|
1415
|
-
function enforceCap(text, remainingBudget) {
|
|
1416
|
-
if (remainingBudget <= 0) {
|
|
1417
|
-
return { text: "[truncated: iteration output cap exceeded]", newBudget: 0 };
|
|
1418
|
-
}
|
|
1419
|
-
const textBytes = Buffer.byteLength(text, "utf8");
|
|
1420
|
-
if (textBytes <= remainingBudget) {
|
|
1421
|
-
return { text, newBudget: remainingBudget - textBytes };
|
|
1422
|
-
}
|
|
1423
|
-
const marker = `
|
|
1424
|
-
\u2026[truncated ${textBytes - remainingBudget} bytes]\u2026
|
|
1425
|
-
`;
|
|
1426
|
-
const markerBytes = Buffer.byteLength(marker, "utf8");
|
|
1427
|
-
const available = remainingBudget - markerBytes;
|
|
1428
|
-
if (available <= 0) {
|
|
1429
|
-
return { text: "[truncated: iteration output cap exceeded]", newBudget: 0 };
|
|
1430
|
-
}
|
|
1431
|
-
const half = Math.floor(available / 2);
|
|
1432
|
-
const first = text.slice(0, half);
|
|
1433
|
-
const second = text.slice(text.length - half);
|
|
1434
|
-
return { text: `${first}${marker}${second}`, newBudget: 0 };
|
|
1435
|
-
}
|
|
1436
|
-
return { serialize, enforceCap, capBytes };
|
|
1437
|
-
}
|
|
1438
|
-
|
|
1439
1626
|
// src/utils/json-schema-validate.ts
|
|
1440
1627
|
function validateAgainstSchema(value, schema) {
|
|
1441
1628
|
const errors = [];
|
|
1442
1629
|
walk(value, schema, "", errors);
|
|
1443
1630
|
return { ok: errors.length === 0, errors };
|
|
1444
1631
|
}
|
|
1445
|
-
function walk(value, schema,
|
|
1632
|
+
function walk(value, schema, path6, errors) {
|
|
1446
1633
|
if (schema.enum !== void 0) {
|
|
1447
1634
|
if (!schema.enum.some((e) => deepEqual(e, value))) {
|
|
1448
1635
|
errors.push({
|
|
1449
|
-
path:
|
|
1636
|
+
path: path6 || "<root>",
|
|
1450
1637
|
message: `expected one of ${JSON.stringify(schema.enum)}, got ${JSON.stringify(value)}`
|
|
1451
1638
|
});
|
|
1452
1639
|
return;
|
|
@@ -1455,7 +1642,7 @@ function walk(value, schema, path4, errors) {
|
|
|
1455
1642
|
if (typeof schema.type === "string") {
|
|
1456
1643
|
if (!checkType(value, schema.type)) {
|
|
1457
1644
|
errors.push({
|
|
1458
|
-
path:
|
|
1645
|
+
path: path6 || "<root>",
|
|
1459
1646
|
message: `expected ${schema.type}, got ${describeType(value)}`
|
|
1460
1647
|
});
|
|
1461
1648
|
return;
|
|
@@ -1465,20 +1652,20 @@ function walk(value, schema, path4, errors) {
|
|
|
1465
1652
|
const obj = value;
|
|
1466
1653
|
for (const req of schema.required ?? []) {
|
|
1467
1654
|
if (!(req in obj)) {
|
|
1468
|
-
errors.push({ path: joinPath(
|
|
1655
|
+
errors.push({ path: joinPath(path6, req), message: "required property missing" });
|
|
1469
1656
|
}
|
|
1470
1657
|
}
|
|
1471
1658
|
if (schema.properties) {
|
|
1472
1659
|
for (const [key, subSchema] of Object.entries(schema.properties)) {
|
|
1473
1660
|
if (key in obj) {
|
|
1474
|
-
walk(obj[key], subSchema, joinPath(
|
|
1661
|
+
walk(obj[key], subSchema, joinPath(path6, key), errors);
|
|
1475
1662
|
}
|
|
1476
1663
|
}
|
|
1477
1664
|
}
|
|
1478
1665
|
}
|
|
1479
1666
|
if (schema.type === "array" && Array.isArray(value) && schema.items) {
|
|
1480
1667
|
for (let i = 0; i < value.length; i++) {
|
|
1481
|
-
walk(value[i], schema.items, `${
|
|
1668
|
+
walk(value[i], schema.items, `${path6}[${i}]`, errors);
|
|
1482
1669
|
}
|
|
1483
1670
|
}
|
|
1484
1671
|
}
|
|
@@ -1568,6 +1755,714 @@ function compileUserRegex(pattern, flags) {
|
|
|
1568
1755
|
}
|
|
1569
1756
|
}
|
|
1570
1757
|
|
|
1758
|
+
// src/utils/sleep.ts
|
|
1759
|
+
function sleep(ms) {
|
|
1760
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
// src/utils/string.ts
|
|
1764
|
+
function truncate(s, max) {
|
|
1765
|
+
return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
// src/utils/tool-output-serializer.ts
|
|
1769
|
+
var DEFAULT_LIST_LIMIT = 500;
|
|
1770
|
+
var LOG_ENTRY_LIMIT = 200;
|
|
1771
|
+
var INLINE_LIMIT = 240;
|
|
1772
|
+
var GREP_FILE_LIMIT = 80;
|
|
1773
|
+
var GREP_MATCHES_PER_FILE = 3;
|
|
1774
|
+
var DIFF_INLINE_LINE_LIMIT = 260;
|
|
1775
|
+
var DIFF_HUNK_LIMIT = 8;
|
|
1776
|
+
var DIFF_HUNK_CONTEXT = 14;
|
|
1777
|
+
function createToolOutputSerializer(opts = {}) {
|
|
1778
|
+
const capBytes = opts.perIterationOutputCapBytes ?? 1e5;
|
|
1779
|
+
function serialize(value, context = {}) {
|
|
1780
|
+
if (typeof value === "string") return value;
|
|
1781
|
+
if (value === null || value === void 0) return "";
|
|
1782
|
+
if (typeof value === "object") {
|
|
1783
|
+
if (Array.isArray(value)) return value.map((item) => serialize(item)).join("\n");
|
|
1784
|
+
if (context.toolName) {
|
|
1785
|
+
const compact = renderToolObject(context.toolName, value, context.input);
|
|
1786
|
+
if (compact !== void 0) return compact;
|
|
1787
|
+
return renderGenericToolObject(context.toolName, value);
|
|
1788
|
+
}
|
|
1789
|
+
if ("text" in value) {
|
|
1790
|
+
const t = value.text;
|
|
1791
|
+
return typeof t === "string" ? t : JSON.stringify(value, null, 2);
|
|
1792
|
+
}
|
|
1793
|
+
try {
|
|
1794
|
+
return JSON.stringify(value, null, 2);
|
|
1795
|
+
} catch {
|
|
1796
|
+
return String(value);
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
return String(value);
|
|
1800
|
+
}
|
|
1801
|
+
function enforceCap(text, remainingBudget) {
|
|
1802
|
+
if (remainingBudget <= 0) {
|
|
1803
|
+
return { text: "[truncated: iteration output cap exceeded]", newBudget: 0 };
|
|
1804
|
+
}
|
|
1805
|
+
const textBytes = Buffer.byteLength(text, "utf8");
|
|
1806
|
+
if (textBytes <= remainingBudget) {
|
|
1807
|
+
return { text, newBudget: remainingBudget - textBytes };
|
|
1808
|
+
}
|
|
1809
|
+
const marker = `
|
|
1810
|
+
\u2026[truncated ${textBytes - remainingBudget} bytes]\u2026
|
|
1811
|
+
`;
|
|
1812
|
+
const markerBytes = Buffer.byteLength(marker, "utf8");
|
|
1813
|
+
const available = remainingBudget - markerBytes;
|
|
1814
|
+
if (available <= 0) {
|
|
1815
|
+
return { text: "[truncated: iteration output cap exceeded]", newBudget: 0 };
|
|
1816
|
+
}
|
|
1817
|
+
const half = Math.floor(available / 2);
|
|
1818
|
+
const first = text.slice(0, half);
|
|
1819
|
+
const second = text.slice(text.length - half);
|
|
1820
|
+
return { text: `${first}${marker}${second}`, newBudget: 0 };
|
|
1821
|
+
}
|
|
1822
|
+
return { serialize, enforceCap, capBytes };
|
|
1823
|
+
}
|
|
1824
|
+
function renderToolObject(toolName, obj, input) {
|
|
1825
|
+
if (toolName === "read" && typeof obj["text"] === "string") {
|
|
1826
|
+
return joinSections([
|
|
1827
|
+
renderHeader(
|
|
1828
|
+
`read: ${stringFromInput(input, "path") ?? stringField(obj, "path") ?? "<unknown>"}`,
|
|
1829
|
+
{
|
|
1830
|
+
offset: numberFromInput(input, "offset"),
|
|
1831
|
+
limit: numberFromInput(input, "limit"),
|
|
1832
|
+
total_lines: obj["total_lines"],
|
|
1833
|
+
encoding: obj["encoding"],
|
|
1834
|
+
truncated: obj["truncated"],
|
|
1835
|
+
cached: obj["cached"],
|
|
1836
|
+
note: obj["note"]
|
|
1837
|
+
}
|
|
1838
|
+
),
|
|
1839
|
+
obj["text"]
|
|
1840
|
+
]);
|
|
1841
|
+
}
|
|
1842
|
+
if (toolName === "grep" && Array.isArray(obj["matches"])) {
|
|
1843
|
+
const matches = stringArrayField(obj, "matches");
|
|
1844
|
+
return joinSections([
|
|
1845
|
+
renderHeader(`grep: ${stringFromInput(input, "pattern") ?? "<pattern>"}`, {
|
|
1846
|
+
path: stringFromInput(input, "path"),
|
|
1847
|
+
glob: stringFromInput(input, "glob"),
|
|
1848
|
+
mode: stringFromInput(input, "output_mode"),
|
|
1849
|
+
count: obj["count"],
|
|
1850
|
+
shown: matches.length,
|
|
1851
|
+
truncated: obj["truncated"],
|
|
1852
|
+
used: obj["used"]
|
|
1853
|
+
}),
|
|
1854
|
+
renderGrepMatches(matches, stringFromInput(input, "output_mode"))
|
|
1855
|
+
]);
|
|
1856
|
+
}
|
|
1857
|
+
if (toolName === "patch" && Array.isArray(obj["files"])) {
|
|
1858
|
+
const files = stringArrayField(obj, "files");
|
|
1859
|
+
return joinSections([
|
|
1860
|
+
renderHeader("patch", {
|
|
1861
|
+
applied: obj["applied"],
|
|
1862
|
+
rejected: obj["rejected"],
|
|
1863
|
+
files: files.length,
|
|
1864
|
+
dry_run: obj["dry_run"]
|
|
1865
|
+
}),
|
|
1866
|
+
typeof obj["message"] === "string" ? `message:
|
|
1867
|
+
${obj["message"]}` : void 0,
|
|
1868
|
+
files.length > 0 ? `files:
|
|
1869
|
+
${renderStringList(files)}` : void 0
|
|
1870
|
+
]);
|
|
1871
|
+
}
|
|
1872
|
+
if (toolName === "glob" && Array.isArray(obj["files"])) {
|
|
1873
|
+
const files = stringArrayField(obj, "files");
|
|
1874
|
+
return joinSections([
|
|
1875
|
+
renderHeader(
|
|
1876
|
+
`${toolName}: ${stringFromInput(input, "pattern") ?? stringFromInput(input, "files") ?? stringFromInput(input, "path") ?? ""}`.trim(),
|
|
1877
|
+
{
|
|
1878
|
+
path: stringFromInput(input, "path"),
|
|
1879
|
+
files: files.length,
|
|
1880
|
+
truncated: obj["truncated"]
|
|
1881
|
+
}
|
|
1882
|
+
),
|
|
1883
|
+
renderStringList(files, "(no files)")
|
|
1884
|
+
]);
|
|
1885
|
+
}
|
|
1886
|
+
if (toolName === "tree" && typeof obj["tree"] === "string") {
|
|
1887
|
+
return joinSections([
|
|
1888
|
+
renderHeader(
|
|
1889
|
+
`tree: ${stringField(obj, "path") ?? stringFromInput(input, "path") ?? "<cwd>"}`,
|
|
1890
|
+
{
|
|
1891
|
+
total_files: obj["total_files"],
|
|
1892
|
+
total_dirs: obj["total_dirs"],
|
|
1893
|
+
truncated: obj["truncated"]
|
|
1894
|
+
}
|
|
1895
|
+
),
|
|
1896
|
+
obj["tree"]
|
|
1897
|
+
]);
|
|
1898
|
+
}
|
|
1899
|
+
if (toolName === "fetch" && typeof obj["content"] === "string") {
|
|
1900
|
+
return joinSections([
|
|
1901
|
+
renderHeader(
|
|
1902
|
+
`fetch: ${stringField(obj, "url") ?? stringFromInput(input, "url") ?? "<url>"}`,
|
|
1903
|
+
{
|
|
1904
|
+
status: obj["status"],
|
|
1905
|
+
content_type: obj["content_type"]
|
|
1906
|
+
}
|
|
1907
|
+
),
|
|
1908
|
+
obj["content"]
|
|
1909
|
+
]);
|
|
1910
|
+
}
|
|
1911
|
+
if (toolName === "replace" && Array.isArray(obj["results"])) {
|
|
1912
|
+
const results = obj["results"].filter(isRecord2);
|
|
1913
|
+
const sections = [
|
|
1914
|
+
renderHeader("replace", {
|
|
1915
|
+
files_modified: obj["files_modified"],
|
|
1916
|
+
total_replacements: obj["total_replacements"],
|
|
1917
|
+
dry_run: obj["dry_run"]
|
|
1918
|
+
})
|
|
1919
|
+
];
|
|
1920
|
+
for (const r of results.slice(0, DEFAULT_LIST_LIMIT)) {
|
|
1921
|
+
sections.push(
|
|
1922
|
+
joinSections([
|
|
1923
|
+
renderHeader(`file: ${stringField(r, "path") ?? "<unknown>"}`, {
|
|
1924
|
+
replacements: r["replacements"]
|
|
1925
|
+
}),
|
|
1926
|
+
typeof r["diff"] === "string" ? r["diff"] : void 0
|
|
1927
|
+
])
|
|
1928
|
+
);
|
|
1929
|
+
}
|
|
1930
|
+
if (results.length > DEFAULT_LIST_LIMIT) {
|
|
1931
|
+
sections.push(`[serializer omitted ${results.length - DEFAULT_LIST_LIMIT} result item(s)]`);
|
|
1932
|
+
}
|
|
1933
|
+
return joinSections(sections);
|
|
1934
|
+
}
|
|
1935
|
+
if (typeof obj["diff"] === "string") {
|
|
1936
|
+
const diff = obj["diff"];
|
|
1937
|
+
return joinSections([
|
|
1938
|
+
renderHeader(toolName, {
|
|
1939
|
+
path: obj["path"],
|
|
1940
|
+
replacements: obj["replacements"],
|
|
1941
|
+
bytes_written: obj["bytes_written"],
|
|
1942
|
+
created: obj["created"],
|
|
1943
|
+
note: obj["note"],
|
|
1944
|
+
files: Array.isArray(obj["files"]) ? obj["files"].length : void 0,
|
|
1945
|
+
truncated: obj["truncated"],
|
|
1946
|
+
mode: obj["mode"]
|
|
1947
|
+
}),
|
|
1948
|
+
compactDiff(diff)
|
|
1949
|
+
]);
|
|
1950
|
+
}
|
|
1951
|
+
if (toolName === "test" && typeof obj["output"] === "string") {
|
|
1952
|
+
return renderTestOutput(obj, input);
|
|
1953
|
+
}
|
|
1954
|
+
if ((toolName === "typecheck" || toolName === "lint" || toolName === "format") && typeof obj["output"] === "string") {
|
|
1955
|
+
return renderVerifierOutput(toolName, obj, input);
|
|
1956
|
+
}
|
|
1957
|
+
if (hasCommandOutputShape(obj)) {
|
|
1958
|
+
return renderCommandOutput(toolName, obj, input);
|
|
1959
|
+
}
|
|
1960
|
+
if (toolName === "json" && typeof obj["formatted"] === "string") {
|
|
1961
|
+
return joinSections([
|
|
1962
|
+
renderHeader("json", {
|
|
1963
|
+
type: obj["type"],
|
|
1964
|
+
keys: Array.isArray(obj["keys"]) ? obj["keys"].length : void 0,
|
|
1965
|
+
query: stringFromInput(input, "query"),
|
|
1966
|
+
error: obj["error"]
|
|
1967
|
+
}),
|
|
1968
|
+
obj["formatted"]
|
|
1969
|
+
]);
|
|
1970
|
+
}
|
|
1971
|
+
if (toolName === "logs" && Array.isArray(obj["entries"])) {
|
|
1972
|
+
const entries = obj["entries"].filter(isRecord2);
|
|
1973
|
+
const lines = entries.slice(0, LOG_ENTRY_LIMIT).map((entry) => {
|
|
1974
|
+
const ts = stringField(entry, "timestamp") ?? "";
|
|
1975
|
+
const level = stringField(entry, "level") ?? "info";
|
|
1976
|
+
const message = stringField(entry, "message") ?? "";
|
|
1977
|
+
const source = stringField(entry, "source");
|
|
1978
|
+
return [ts, level, source, message].filter(Boolean).join(" ");
|
|
1979
|
+
});
|
|
1980
|
+
if (entries.length > LOG_ENTRY_LIMIT) {
|
|
1981
|
+
lines.push(`[serializer omitted ${entries.length - LOG_ENTRY_LIMIT} log entry item(s)]`);
|
|
1982
|
+
}
|
|
1983
|
+
return joinSections([
|
|
1984
|
+
renderHeader(`logs: ${stringField(obj, "source") ?? "<source>"}`, {
|
|
1985
|
+
total: obj["total"],
|
|
1986
|
+
shown: Math.min(entries.length, LOG_ENTRY_LIMIT),
|
|
1987
|
+
truncated: obj["truncated"],
|
|
1988
|
+
stream_mode: obj["stream_mode"]
|
|
1989
|
+
}),
|
|
1990
|
+
lines.length > 0 ? lines.join("\n") : "(no log entries)"
|
|
1991
|
+
]);
|
|
1992
|
+
}
|
|
1993
|
+
if (toolName === "audit" && Array.isArray(obj["vulnerabilities"])) {
|
|
1994
|
+
const vulns = obj["vulnerabilities"].filter(isRecord2);
|
|
1995
|
+
const lines = vulns.slice(0, DEFAULT_LIST_LIMIT).map((v) => {
|
|
1996
|
+
const severity = stringField(v, "severity") ?? "unknown";
|
|
1997
|
+
const pkg = stringField(v, "package") ?? "<package>";
|
|
1998
|
+
const title = stringField(v, "title") ?? "";
|
|
1999
|
+
const url = stringField(v, "url");
|
|
2000
|
+
return [severity, pkg, title, url].filter(Boolean).join(" | ");
|
|
2001
|
+
});
|
|
2002
|
+
if (vulns.length > DEFAULT_LIST_LIMIT) {
|
|
2003
|
+
lines.push(`[serializer omitted ${vulns.length - DEFAULT_LIST_LIMIT} vulnerability item(s)]`);
|
|
2004
|
+
}
|
|
2005
|
+
return joinSections([
|
|
2006
|
+
renderHeader("audit", {
|
|
2007
|
+
exit_code: obj["exit_code"],
|
|
2008
|
+
total: obj["total"],
|
|
2009
|
+
summary: obj["summary"],
|
|
2010
|
+
truncated: obj["truncated"]
|
|
2011
|
+
}),
|
|
2012
|
+
lines.length > 0 ? lines.join("\n") : stringField(obj, "output")
|
|
2013
|
+
]);
|
|
2014
|
+
}
|
|
2015
|
+
if (toolName === "outdated" && Array.isArray(obj["packages"])) {
|
|
2016
|
+
const packages = obj["packages"].filter(isRecord2);
|
|
2017
|
+
const lines = packages.slice(0, DEFAULT_LIST_LIMIT).map(
|
|
2018
|
+
(p) => [
|
|
2019
|
+
stringField(p, "name") ?? "<package>",
|
|
2020
|
+
`current=${stringField(p, "current") ?? "unknown"}`,
|
|
2021
|
+
`wanted=${stringField(p, "wanted") ?? "unknown"}`,
|
|
2022
|
+
`latest=${stringField(p, "latest") ?? "unknown"}`,
|
|
2023
|
+
stringField(p, "type")
|
|
2024
|
+
].filter(Boolean).join(" | ")
|
|
2025
|
+
);
|
|
2026
|
+
if (packages.length > DEFAULT_LIST_LIMIT) {
|
|
2027
|
+
lines.push(`[serializer omitted ${packages.length - DEFAULT_LIST_LIMIT} package item(s)]`);
|
|
2028
|
+
}
|
|
2029
|
+
return joinSections([
|
|
2030
|
+
renderHeader("outdated", {
|
|
2031
|
+
exit_code: obj["exit_code"],
|
|
2032
|
+
total: obj["total"],
|
|
2033
|
+
truncated: obj["truncated"]
|
|
2034
|
+
}),
|
|
2035
|
+
lines.length > 0 ? lines.join("\n") : stringField(obj, "output")
|
|
2036
|
+
]);
|
|
2037
|
+
}
|
|
2038
|
+
return void 0;
|
|
2039
|
+
}
|
|
2040
|
+
function renderTestOutput(obj, input) {
|
|
2041
|
+
const exitCode = numberField(obj, "exit_code") ?? 0;
|
|
2042
|
+
const failed = numberField(obj, "failed") ?? 0;
|
|
2043
|
+
const output = stringField(obj, "output") ?? "";
|
|
2044
|
+
const header = renderHeader(`test: ${stringField(obj, "runner") ?? "runner"}`, {
|
|
2045
|
+
exit_code: obj["exit_code"],
|
|
2046
|
+
tests_run: obj["tests_run"],
|
|
2047
|
+
passed: obj["passed"],
|
|
2048
|
+
failed: obj["failed"],
|
|
2049
|
+
duration_ms: obj["duration_ms"],
|
|
2050
|
+
truncated: obj["truncated"],
|
|
2051
|
+
files: inputListSummary(input, "files"),
|
|
2052
|
+
grep: stringFromInput(input, "grep")
|
|
2053
|
+
});
|
|
2054
|
+
if (exitCode === 0 && failed === 0) {
|
|
2055
|
+
return joinSections([
|
|
2056
|
+
header,
|
|
2057
|
+
joinSections([
|
|
2058
|
+
"report:",
|
|
2059
|
+
`status=passed`,
|
|
2060
|
+
`tests_run=${obj["tests_run"] ?? 0}`,
|
|
2061
|
+
`passed=${obj["passed"] ?? 0}`,
|
|
2062
|
+
`failed=${obj["failed"] ?? 0}`,
|
|
2063
|
+
`duration_ms=${obj["duration_ms"] ?? 0}`,
|
|
2064
|
+
extractSpoolNote(output)
|
|
2065
|
+
])
|
|
2066
|
+
]);
|
|
2067
|
+
}
|
|
2068
|
+
return joinSections([
|
|
2069
|
+
header,
|
|
2070
|
+
`error_context:
|
|
2071
|
+
${compactFailureOutput(output || "(no runner output)")}`
|
|
2072
|
+
]);
|
|
2073
|
+
}
|
|
2074
|
+
function renderVerifierOutput(toolName, obj, input) {
|
|
2075
|
+
const exitCode = numberField(obj, "exit_code") ?? 0;
|
|
2076
|
+
const errors = numberField(obj, "errors") ?? 0;
|
|
2077
|
+
const warnings = numberField(obj, "warnings") ?? 0;
|
|
2078
|
+
const output = stringField(obj, "output") ?? "";
|
|
2079
|
+
const changed = numberField(obj, "files_changed") ?? 0;
|
|
2080
|
+
const header = renderHeader(toolName, {
|
|
2081
|
+
exit_code: obj["exit_code"],
|
|
2082
|
+
errors: obj["errors"],
|
|
2083
|
+
warnings: obj["warnings"],
|
|
2084
|
+
files_checked: obj["files_checked"],
|
|
2085
|
+
files_changed: obj["files_changed"],
|
|
2086
|
+
fix_applied: obj["fix_applied"],
|
|
2087
|
+
fixer: obj["fixer"],
|
|
2088
|
+
linter: obj["linter"],
|
|
2089
|
+
project: obj["project"],
|
|
2090
|
+
truncated: obj["truncated"],
|
|
2091
|
+
files: inputListSummary(input, "files"),
|
|
2092
|
+
cwd: stringFromInput(input, "cwd")
|
|
2093
|
+
});
|
|
2094
|
+
if (exitCode === 0 && errors === 0 && (toolName !== "format" || changed === 0)) {
|
|
2095
|
+
return joinSections([
|
|
2096
|
+
header,
|
|
2097
|
+
joinSections([
|
|
2098
|
+
"report:",
|
|
2099
|
+
"status=passed",
|
|
2100
|
+
`errors=${errors}`,
|
|
2101
|
+
`warnings=${warnings}`,
|
|
2102
|
+
toolName === "format" ? `files_changed=${changed}` : void 0,
|
|
2103
|
+
extractSpoolNote(output)
|
|
2104
|
+
])
|
|
2105
|
+
]);
|
|
2106
|
+
}
|
|
2107
|
+
if (exitCode === 0 && toolName === "format") {
|
|
2108
|
+
return joinSections([
|
|
2109
|
+
header,
|
|
2110
|
+
joinSections([
|
|
2111
|
+
"report:",
|
|
2112
|
+
"status=changed",
|
|
2113
|
+
`files_changed=${changed}`,
|
|
2114
|
+
extractSpoolNote(output)
|
|
2115
|
+
])
|
|
2116
|
+
]);
|
|
2117
|
+
}
|
|
2118
|
+
return joinSections([
|
|
2119
|
+
header,
|
|
2120
|
+
`error_context:
|
|
2121
|
+
${compactFailureOutput(output || "(no verifier output)")}`
|
|
2122
|
+
]);
|
|
2123
|
+
}
|
|
2124
|
+
function renderGrepMatches(matches, mode) {
|
|
2125
|
+
if (matches.length === 0) return "(no matches)";
|
|
2126
|
+
if (mode === "files_with_matches") return renderStringList(matches, "(no files)");
|
|
2127
|
+
if (mode === "count") return renderStringList(matches, "(no counts)");
|
|
2128
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2129
|
+
const passthrough = [];
|
|
2130
|
+
for (const match of matches) {
|
|
2131
|
+
const parsed = parseGrepContentLine(match);
|
|
2132
|
+
if (!parsed) {
|
|
2133
|
+
passthrough.push(match);
|
|
2134
|
+
continue;
|
|
2135
|
+
}
|
|
2136
|
+
const list = groups.get(parsed.file) ?? [];
|
|
2137
|
+
list.push(`${parsed.line}:${parsed.text}`);
|
|
2138
|
+
groups.set(parsed.file, list);
|
|
2139
|
+
}
|
|
2140
|
+
if (groups.size === 0) return renderStringList(matches, "(no matches)");
|
|
2141
|
+
const sections = [];
|
|
2142
|
+
let fileIndex = 0;
|
|
2143
|
+
for (const [file, lines] of groups) {
|
|
2144
|
+
fileIndex++;
|
|
2145
|
+
if (fileIndex > GREP_FILE_LIMIT) break;
|
|
2146
|
+
const shown = lines.slice(0, GREP_MATCHES_PER_FILE);
|
|
2147
|
+
sections.push(
|
|
2148
|
+
`${file} (${lines.length} match(es), showing ${shown.length})
|
|
2149
|
+
${shown.join("\n")}`
|
|
2150
|
+
);
|
|
2151
|
+
}
|
|
2152
|
+
if (groups.size > GREP_FILE_LIMIT) {
|
|
2153
|
+
sections.push(`[serializer omitted ${groups.size - GREP_FILE_LIMIT} file group(s)]`);
|
|
2154
|
+
}
|
|
2155
|
+
if (passthrough.length > 0) {
|
|
2156
|
+
sections.push(`ungrouped:
|
|
2157
|
+
${renderStringList(passthrough, "", 50)}`);
|
|
2158
|
+
}
|
|
2159
|
+
return sections.join("\n");
|
|
2160
|
+
}
|
|
2161
|
+
function parseGrepContentLine(line) {
|
|
2162
|
+
const match = /^(.+?):(\d+):(.*)$/.exec(line);
|
|
2163
|
+
if (!match?.[1] || !match[2]) return void 0;
|
|
2164
|
+
return { file: match[1], line: match[2], text: match[3] ?? "" };
|
|
2165
|
+
}
|
|
2166
|
+
function compactDiff(diff) {
|
|
2167
|
+
const lines = diff.split(/\r?\n/);
|
|
2168
|
+
if (lines.length <= DIFF_INLINE_LINE_LIMIT) return diff;
|
|
2169
|
+
const fileCount = Math.max(
|
|
2170
|
+
new Set(
|
|
2171
|
+
lines.map(
|
|
2172
|
+
(line) => /^diff --git\s+a\/(.+?)\s+b\//.exec(line)?.[1] ?? /^---\s+(.+)/.exec(line)?.[1]
|
|
2173
|
+
).filter(Boolean)
|
|
2174
|
+
).size,
|
|
2175
|
+
0
|
|
2176
|
+
);
|
|
2177
|
+
const hunks = lines.filter((line) => line.startsWith("@@")).length;
|
|
2178
|
+
const added = lines.filter((line) => line.startsWith("+") && !line.startsWith("+++")).length;
|
|
2179
|
+
const removed = lines.filter((line) => line.startsWith("-") && !line.startsWith("---")).length;
|
|
2180
|
+
const selected = /* @__PURE__ */ new Set();
|
|
2181
|
+
let hunkCount = 0;
|
|
2182
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2183
|
+
const line = lines[i] ?? "";
|
|
2184
|
+
if (line.startsWith("diff --git") || line.startsWith("--- ") || line.startsWith("+++ ")) {
|
|
2185
|
+
selected.add(i);
|
|
2186
|
+
continue;
|
|
2187
|
+
}
|
|
2188
|
+
if (!line.startsWith("@@")) continue;
|
|
2189
|
+
if (hunkCount >= DIFF_HUNK_LIMIT) continue;
|
|
2190
|
+
hunkCount++;
|
|
2191
|
+
for (let j = i; j <= Math.min(lines.length - 1, i + DIFF_HUNK_CONTEXT); j++) {
|
|
2192
|
+
selected.add(j);
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
if (selected.size === 0) {
|
|
2196
|
+
return joinSections([
|
|
2197
|
+
renderHeader("diff_summary", {
|
|
2198
|
+
files: fileCount,
|
|
2199
|
+
hunks,
|
|
2200
|
+
added,
|
|
2201
|
+
removed,
|
|
2202
|
+
lines: lines.length
|
|
2203
|
+
}),
|
|
2204
|
+
lines.slice(0, DIFF_INLINE_LINE_LIMIT).join("\n"),
|
|
2205
|
+
`[serializer omitted ${Math.max(0, lines.length - DIFF_INLINE_LINE_LIMIT)} diff line(s)]`
|
|
2206
|
+
]);
|
|
2207
|
+
}
|
|
2208
|
+
const excerpt = [];
|
|
2209
|
+
let previous = -1;
|
|
2210
|
+
for (const index of [...selected].sort((a, b) => a - b)) {
|
|
2211
|
+
if (index > previous + 1) {
|
|
2212
|
+
const omitted = previous === -1 ? index : index - previous - 1;
|
|
2213
|
+
excerpt.push(`[serializer omitted ${omitted} diff line(s)]`);
|
|
2214
|
+
}
|
|
2215
|
+
excerpt.push(lines[index] ?? "");
|
|
2216
|
+
previous = index;
|
|
2217
|
+
}
|
|
2218
|
+
const trailing = lines.length - previous - 1;
|
|
2219
|
+
if (trailing > 0) excerpt.push(`[serializer omitted ${trailing} trailing diff line(s)]`);
|
|
2220
|
+
return joinSections([
|
|
2221
|
+
renderHeader("diff_summary", {
|
|
2222
|
+
files: fileCount,
|
|
2223
|
+
hunks,
|
|
2224
|
+
shown_hunks: Math.min(hunks, DIFF_HUNK_LIMIT),
|
|
2225
|
+
added,
|
|
2226
|
+
removed,
|
|
2227
|
+
lines: lines.length
|
|
2228
|
+
}),
|
|
2229
|
+
excerpt.join("\n")
|
|
2230
|
+
]);
|
|
2231
|
+
}
|
|
2232
|
+
function compactFailureOutput(output) {
|
|
2233
|
+
const lines = output.split(/\r?\n/);
|
|
2234
|
+
if (lines.length <= 260) return output.trimEnd();
|
|
2235
|
+
const selected = /* @__PURE__ */ new Set();
|
|
2236
|
+
const marker = /\b(fail|failed|failure|error|exception|assertionerror|expected|received|actual|timeout|stack)\b/i;
|
|
2237
|
+
let markerHits = 0;
|
|
2238
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2239
|
+
if (!marker.test(lines[i] ?? "")) continue;
|
|
2240
|
+
markerHits++;
|
|
2241
|
+
for (let j = Math.max(0, i - 4); j <= Math.min(lines.length - 1, i + 10); j++) {
|
|
2242
|
+
selected.add(j);
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
if (markerHits === 0) {
|
|
2246
|
+
return lines.slice(-220).join("\n").trimEnd();
|
|
2247
|
+
}
|
|
2248
|
+
const ordered = [...selected].sort((a, b) => a - b);
|
|
2249
|
+
const out = [];
|
|
2250
|
+
let previous = -1;
|
|
2251
|
+
for (const index of ordered) {
|
|
2252
|
+
if (index > previous + 1) {
|
|
2253
|
+
const omitted = previous === -1 ? index : index - previous - 1;
|
|
2254
|
+
out.push(`[serializer omitted ${omitted} line(s)]`);
|
|
2255
|
+
}
|
|
2256
|
+
out.push(lines[index] ?? "");
|
|
2257
|
+
previous = index;
|
|
2258
|
+
}
|
|
2259
|
+
return out.join("\n").trimEnd();
|
|
2260
|
+
}
|
|
2261
|
+
function extractSpoolNote(output) {
|
|
2262
|
+
return output.split(/\r?\n/).find((line) => line.startsWith("[output truncated") && line.includes("full"));
|
|
2263
|
+
}
|
|
2264
|
+
function hasCommandOutputShape(obj) {
|
|
2265
|
+
return typeof obj["stdout"] === "string" || typeof obj["stderr"] === "string" || typeof obj["output"] === "string" || typeof obj["exitCode"] === "number" || typeof obj["exit_code"] === "number";
|
|
2266
|
+
}
|
|
2267
|
+
function renderCommandOutput(toolName, obj, input) {
|
|
2268
|
+
const command = stringField(obj, "command") ?? stringFromInput(input, "command");
|
|
2269
|
+
const args = stringArrayField(obj, "args");
|
|
2270
|
+
const commandLine = command ? [command, ...args].join(" ") : void 0;
|
|
2271
|
+
const output = stringField(obj, "output");
|
|
2272
|
+
const stdout = stringField(obj, "stdout");
|
|
2273
|
+
const stderr = stringField(obj, "stderr");
|
|
2274
|
+
return joinSections([
|
|
2275
|
+
renderHeader(commandLine ? `${toolName}: ${commandLine}` : toolName, {
|
|
2276
|
+
exit_code: obj["exit_code"] ?? obj["exitCode"],
|
|
2277
|
+
timed_out: obj["timed_out"],
|
|
2278
|
+
pid: obj["pid"],
|
|
2279
|
+
allowed: obj["allowed"],
|
|
2280
|
+
truncated: obj["truncated"],
|
|
2281
|
+
runner: obj["runner"],
|
|
2282
|
+
linter: obj["linter"],
|
|
2283
|
+
fixer: obj["fixer"],
|
|
2284
|
+
project: obj["project"],
|
|
2285
|
+
tests_run: obj["tests_run"],
|
|
2286
|
+
passed: obj["passed"],
|
|
2287
|
+
failed: obj["failed"],
|
|
2288
|
+
duration_ms: obj["duration_ms"],
|
|
2289
|
+
errors: obj["errors"],
|
|
2290
|
+
warnings: obj["warnings"],
|
|
2291
|
+
files_checked: obj["files_checked"],
|
|
2292
|
+
files_changed: obj["files_changed"],
|
|
2293
|
+
fix_applied: obj["fix_applied"]
|
|
2294
|
+
}),
|
|
2295
|
+
stringField(obj, "error") ? `error:
|
|
2296
|
+
${stringField(obj, "error")}` : void 0,
|
|
2297
|
+
output ? `output:
|
|
2298
|
+
${output}` : void 0,
|
|
2299
|
+
stdout ? `stdout:
|
|
2300
|
+
${stdout}` : void 0,
|
|
2301
|
+
stderr ? `stderr:
|
|
2302
|
+
${stderr}` : void 0
|
|
2303
|
+
]);
|
|
2304
|
+
}
|
|
2305
|
+
function renderGenericToolObject(toolName, obj) {
|
|
2306
|
+
const scalars = {};
|
|
2307
|
+
const blocks = [];
|
|
2308
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
2309
|
+
if (value === void 0) continue;
|
|
2310
|
+
if (isScalar(value)) {
|
|
2311
|
+
const inline = String(value);
|
|
2312
|
+
if (inline.length <= INLINE_LIMIT && !inline.includes("\n")) {
|
|
2313
|
+
scalars[key] = value;
|
|
2314
|
+
} else {
|
|
2315
|
+
blocks.push(`${key}:
|
|
2316
|
+
${inline}`);
|
|
2317
|
+
}
|
|
2318
|
+
continue;
|
|
2319
|
+
}
|
|
2320
|
+
if (Array.isArray(value)) {
|
|
2321
|
+
if (value.every((item) => typeof item === "string")) {
|
|
2322
|
+
blocks.push(`${key}:
|
|
2323
|
+
${renderStringList(value)}`);
|
|
2324
|
+
} else {
|
|
2325
|
+
blocks.push(`${key}:
|
|
2326
|
+
${renderUnknownList(value)}`);
|
|
2327
|
+
}
|
|
2328
|
+
continue;
|
|
2329
|
+
}
|
|
2330
|
+
blocks.push(`${key}: ${clipInline(oneLineJson(value))}`);
|
|
2331
|
+
}
|
|
2332
|
+
return joinSections([renderHeader(toolName, scalars), ...blocks]);
|
|
2333
|
+
}
|
|
2334
|
+
function renderHeader(label, fields) {
|
|
2335
|
+
const parts = Object.entries(fields).filter(([, value]) => value !== void 0 && value !== null && value !== "").map(([key, value]) => `${key}=${clipInline(formatInlineValue(value))}`);
|
|
2336
|
+
return parts.length > 0 ? `${label} (${parts.join(" ")})` : label;
|
|
2337
|
+
}
|
|
2338
|
+
function renderStringList(items, empty = "", limit = DEFAULT_LIST_LIMIT) {
|
|
2339
|
+
if (items.length === 0) return empty;
|
|
2340
|
+
const shown = items.slice(0, limit);
|
|
2341
|
+
const omitted = items.length - shown.length;
|
|
2342
|
+
return [
|
|
2343
|
+
...shown,
|
|
2344
|
+
...omitted > 0 ? [`[serializer omitted ${omitted} item(s); narrow the request for more]`] : []
|
|
2345
|
+
].join("\n");
|
|
2346
|
+
}
|
|
2347
|
+
function renderUnknownList(items, limit = DEFAULT_LIST_LIMIT) {
|
|
2348
|
+
const shown = items.slice(0, limit).map((item) => clipInline(oneLineJson(item), 1e3));
|
|
2349
|
+
const omitted = items.length - shown.length;
|
|
2350
|
+
if (omitted > 0)
|
|
2351
|
+
shown.push(`[serializer omitted ${omitted} item(s); narrow the request for more]`);
|
|
2352
|
+
return shown.join("\n");
|
|
2353
|
+
}
|
|
2354
|
+
function joinSections(sections) {
|
|
2355
|
+
return sections.map((section) => typeof section === "string" ? section.trimEnd() : void 0).filter((section) => !!section).join("\n");
|
|
2356
|
+
}
|
|
2357
|
+
function formatInlineValue(value) {
|
|
2358
|
+
if (Array.isArray(value)) return `[${value.map(formatInlineValue).join(",")}]`;
|
|
2359
|
+
if (isScalar(value)) return String(value);
|
|
2360
|
+
return oneLineJson(value);
|
|
2361
|
+
}
|
|
2362
|
+
function clipInline(value, max = INLINE_LIMIT) {
|
|
2363
|
+
const compact = value.replace(/\s+/g, " ").trim();
|
|
2364
|
+
return compact.length <= max ? compact : `${compact.slice(0, max - 15)}...(${compact.length} chars)`;
|
|
2365
|
+
}
|
|
2366
|
+
function oneLineJson(value) {
|
|
2367
|
+
try {
|
|
2368
|
+
return JSON.stringify(value);
|
|
2369
|
+
} catch {
|
|
2370
|
+
return String(value);
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
function stringField(obj, key) {
|
|
2374
|
+
const value = obj[key];
|
|
2375
|
+
return typeof value === "string" ? value : void 0;
|
|
2376
|
+
}
|
|
2377
|
+
function numberField(obj, key) {
|
|
2378
|
+
const value = obj[key];
|
|
2379
|
+
return typeof value === "number" ? value : void 0;
|
|
2380
|
+
}
|
|
2381
|
+
function stringArrayField(obj, key) {
|
|
2382
|
+
const value = obj[key];
|
|
2383
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
2384
|
+
}
|
|
2385
|
+
function stringFromInput(input, key) {
|
|
2386
|
+
if (!isRecord2(input)) return void 0;
|
|
2387
|
+
const value = input[key];
|
|
2388
|
+
return typeof value === "string" ? value : void 0;
|
|
2389
|
+
}
|
|
2390
|
+
function numberFromInput(input, key) {
|
|
2391
|
+
if (!isRecord2(input)) return void 0;
|
|
2392
|
+
const value = input[key];
|
|
2393
|
+
return typeof value === "number" ? value : void 0;
|
|
2394
|
+
}
|
|
2395
|
+
function inputListSummary(input, key) {
|
|
2396
|
+
if (!isRecord2(input)) return void 0;
|
|
2397
|
+
const value = input[key];
|
|
2398
|
+
if (typeof value === "string") return value;
|
|
2399
|
+
if (Array.isArray(value)) return value.filter((item) => typeof item === "string").join(",");
|
|
2400
|
+
return void 0;
|
|
2401
|
+
}
|
|
2402
|
+
function isRecord2(value) {
|
|
2403
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
2404
|
+
}
|
|
2405
|
+
function isScalar(value) {
|
|
2406
|
+
return value === null || ["string", "number", "boolean"].includes(typeof value);
|
|
2407
|
+
}
|
|
2408
|
+
function projectHash(absRoot) {
|
|
2409
|
+
return createHash("sha256").update(path3.resolve(absRoot)).digest("hex").slice(0, 12);
|
|
2410
|
+
}
|
|
2411
|
+
function projectSlug(absRoot) {
|
|
2412
|
+
const base = slugify(path3.basename(absRoot));
|
|
2413
|
+
const hash = createHash("sha256").update(path3.resolve(absRoot)).digest("hex").slice(0, 6);
|
|
2414
|
+
return `${base}-${hash}`;
|
|
2415
|
+
}
|
|
2416
|
+
function slugify(name) {
|
|
2417
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "project";
|
|
2418
|
+
}
|
|
2419
|
+
function wstackGlobalRoot() {
|
|
2420
|
+
const fromEnv = process.env["WRONGSTACK_HOME"];
|
|
2421
|
+
if (fromEnv && fromEnv.trim().length > 0) return path3.resolve(fromEnv);
|
|
2422
|
+
return path3.join(os.homedir(), ".wrongstack");
|
|
2423
|
+
}
|
|
2424
|
+
function resolveWstackPaths(opts) {
|
|
2425
|
+
const globalRoot = opts.globalRoot ?? (opts.userHome ? path3.join(opts.userHome, ".wrongstack") : wstackGlobalRoot());
|
|
2426
|
+
const hash = projectHash(opts.projectRoot);
|
|
2427
|
+
const slug = projectSlug(opts.projectRoot);
|
|
2428
|
+
const projectDir = path3.join(globalRoot, "projects", slug);
|
|
2429
|
+
return {
|
|
2430
|
+
globalRoot,
|
|
2431
|
+
configDir: globalRoot,
|
|
2432
|
+
globalConfig: path3.join(globalRoot, "config.json"),
|
|
2433
|
+
secretsKey: path3.join(globalRoot, ".key"),
|
|
2434
|
+
globalMemory: path3.join(globalRoot, "memory.md"),
|
|
2435
|
+
globalSkills: path3.join(globalRoot, "skills"),
|
|
2436
|
+
globalPrompts: path3.join(globalRoot, "prompts"),
|
|
2437
|
+
cacheDir: path3.join(globalRoot, "cache"),
|
|
2438
|
+
modelsCache: path3.join(globalRoot, "cache", "models.dev.json"),
|
|
2439
|
+
modelsOverlayCache: path3.join(globalRoot, "cache", "models-overlay.json"),
|
|
2440
|
+
historyFile: path3.join(globalRoot, "history"),
|
|
2441
|
+
logFile: path3.join(globalRoot, "logs", "wrongstack.log"),
|
|
2442
|
+
projectDir,
|
|
2443
|
+
projectCodebaseIndex: path3.join(projectDir, "codebase-index"),
|
|
2444
|
+
projectMemory: path3.join(projectDir, "memory.md"),
|
|
2445
|
+
projectSessions: path3.join(projectDir, "sessions"),
|
|
2446
|
+
projectTrust: path3.join(projectDir, "trust.json"),
|
|
2447
|
+
projectMeta: path3.join(projectDir, "meta.json"),
|
|
2448
|
+
projectLocalConfig: path3.join(projectDir, "config.local.json"),
|
|
2449
|
+
inProjectConfig: path3.join(opts.projectRoot, ".wrongstack", "config.json"),
|
|
2450
|
+
inProjectAgentsFile: path3.join(opts.projectRoot, ".wrongstack", "AGENTS.md"),
|
|
2451
|
+
inProjectSkills: path3.join(opts.projectRoot, ".wrongstack", "skills"),
|
|
2452
|
+
inProjectWorktrees: path3.join(opts.projectRoot, ".wrongstack", "worktrees"),
|
|
2453
|
+
projectHash: hash,
|
|
2454
|
+
projectSlug: slug,
|
|
2455
|
+
projectGoal: path3.join(projectDir, "goal.json"),
|
|
2456
|
+
projectSpecs: path3.join(projectDir, "specs"),
|
|
2457
|
+
projectTaskGraphs: path3.join(projectDir, "task-graphs"),
|
|
2458
|
+
projectSddSession: path3.join(projectDir, "sdd-session.json"),
|
|
2459
|
+
projectPlan: path3.join(projectDir, "plan.json"),
|
|
2460
|
+
projectAutophase: path3.join(projectDir, "autophase"),
|
|
2461
|
+
syncConfig: path3.join(globalRoot, "sync.json"),
|
|
2462
|
+
projectStatus: (projectHash2) => path3.join(globalRoot, "projects", projectHash2, "status.json")
|
|
2463
|
+
};
|
|
2464
|
+
}
|
|
2465
|
+
|
|
1571
2466
|
// src/types/errors.ts
|
|
1572
2467
|
var ERROR_CODES = {
|
|
1573
2468
|
// Provider
|
|
@@ -1771,15 +2666,20 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
|
|
|
1771
2666
|
this._cachedMsgCount = msgCount;
|
|
1772
2667
|
this._cachedToolCount = toolCount;
|
|
1773
2668
|
}
|
|
1774
|
-
const
|
|
2669
|
+
const budget = computeContextWindowBudget(ctx, tokens, this._maxContext);
|
|
2670
|
+
const load = budget.load;
|
|
1775
2671
|
const policy = this.policyProvider?.(ctx);
|
|
1776
2672
|
const thresholds = policy?.thresholds ?? {
|
|
1777
2673
|
warn: this.warnThreshold,
|
|
1778
2674
|
soft: this.softThreshold,
|
|
1779
2675
|
hard: this.hardThreshold
|
|
1780
2676
|
};
|
|
2677
|
+
const repetition = repeatedReadPressure(ctx);
|
|
2678
|
+
const adaptiveThresholds = adaptThresholdsForSignals(thresholds, {
|
|
2679
|
+
repeatedReadCount: repetition
|
|
2680
|
+
});
|
|
1781
2681
|
const aggressiveOn = policy?.aggressiveOn ?? this.aggressiveOn;
|
|
1782
|
-
const level = load >=
|
|
2682
|
+
const level = load >= adaptiveThresholds.hard ? "hard" : load >= adaptiveThresholds.soft ? "soft" : load >= adaptiveThresholds.warn ? "warn" : null;
|
|
1783
2683
|
if (!level) {
|
|
1784
2684
|
this.lastNoopAttempt = null;
|
|
1785
2685
|
return next(ctx);
|
|
@@ -1788,7 +2688,13 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
|
|
|
1788
2688
|
return next(ctx);
|
|
1789
2689
|
}
|
|
1790
2690
|
const aggressive = level === "hard" ? true : level === "soft" ? aggressiveOn !== "hard" : aggressiveOn === "warn";
|
|
1791
|
-
await this.compact(ctx, aggressive, {
|
|
2691
|
+
await this.compact(ctx, aggressive, {
|
|
2692
|
+
level,
|
|
2693
|
+
tokens,
|
|
2694
|
+
load,
|
|
2695
|
+
budget,
|
|
2696
|
+
signals: { repeatedReadCount: repetition }
|
|
2697
|
+
});
|
|
1792
2698
|
return next(ctx);
|
|
1793
2699
|
};
|
|
1794
2700
|
}
|
|
@@ -1841,6 +2747,8 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
|
|
|
1841
2747
|
tokens: pressure.tokens,
|
|
1842
2748
|
load: pressure.load,
|
|
1843
2749
|
maxContext: this._maxContext,
|
|
2750
|
+
budget: pressure.budget,
|
|
2751
|
+
signals: pressure.signals,
|
|
1844
2752
|
report,
|
|
1845
2753
|
aggressive
|
|
1846
2754
|
});
|
|
@@ -1852,6 +2760,8 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
|
|
|
1852
2760
|
level: pressure.level,
|
|
1853
2761
|
aggressive,
|
|
1854
2762
|
reductions: report.reductions?.map((r) => ({ phase: r.phase, saved: r.saved })),
|
|
2763
|
+
budget: pressure.budget,
|
|
2764
|
+
signals: pressure.signals,
|
|
1855
2765
|
// Record what was collapsed so the audit trail shows the preserved
|
|
1856
2766
|
// content, not just token counts. Bounded to keep the log line small;
|
|
1857
2767
|
// the full original turns are already in the session JSONL.
|
|
@@ -1867,6 +2777,8 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
|
|
|
1867
2777
|
level: pressure.level,
|
|
1868
2778
|
tokens: pressure.tokens,
|
|
1869
2779
|
maxContext: this._maxContext,
|
|
2780
|
+
budget: pressure.budget,
|
|
2781
|
+
signals: pressure.signals,
|
|
1870
2782
|
load: pressure.load,
|
|
1871
2783
|
fatal
|
|
1872
2784
|
});
|
|
@@ -1886,6 +2798,37 @@ var AutoCompactionMiddleware = class _AutoCompactionMiddleware {
|
|
|
1886
2798
|
}
|
|
1887
2799
|
}
|
|
1888
2800
|
};
|
|
2801
|
+
function computeContextWindowBudget(ctx, inputTokens, maxContext) {
|
|
2802
|
+
const reservedOutputTokens = readPositiveMetaNumber(ctx, "contextOutputReserveTokens") ?? Math.floor(Math.min(8192, maxContext * 0.08));
|
|
2803
|
+
const reservedSafetyTokens = readPositiveMetaNumber(ctx, "contextSafetyBufferTokens") ?? Math.floor(Math.min(4096, maxContext * 0.02));
|
|
2804
|
+
const availableInputTokens = Math.max(
|
|
2805
|
+
1,
|
|
2806
|
+
maxContext - reservedOutputTokens - reservedSafetyTokens
|
|
2807
|
+
);
|
|
2808
|
+
const remainingInputTokens = availableInputTokens - inputTokens;
|
|
2809
|
+
return {
|
|
2810
|
+
maxContext,
|
|
2811
|
+
inputTokens,
|
|
2812
|
+
availableInputTokens,
|
|
2813
|
+
remainingInputTokens,
|
|
2814
|
+
reservedOutputTokens,
|
|
2815
|
+
reservedSafetyTokens,
|
|
2816
|
+
load: inputTokens / availableInputTokens,
|
|
2817
|
+
overflowTokens: Math.max(0, -remainingInputTokens)
|
|
2818
|
+
};
|
|
2819
|
+
}
|
|
2820
|
+
function readPositiveMetaNumber(ctx, key) {
|
|
2821
|
+
const value = ctx.meta?.[key];
|
|
2822
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : void 0;
|
|
2823
|
+
}
|
|
2824
|
+
function adaptThresholdsForSignals(thresholds, signals) {
|
|
2825
|
+
if (signals.repeatedReadCount < 3) return thresholds;
|
|
2826
|
+
return {
|
|
2827
|
+
warn: Math.max(0.25, thresholds.warn - 0.08),
|
|
2828
|
+
soft: Math.max(0.35, thresholds.soft - 0.04),
|
|
2829
|
+
hard: thresholds.hard
|
|
2830
|
+
};
|
|
2831
|
+
}
|
|
1889
2832
|
|
|
1890
2833
|
// src/security/capabilities.ts
|
|
1891
2834
|
var ToolCapabilities = {
|
|
@@ -2028,7 +2971,8 @@ ${errorDetails}`,
|
|
|
2028
2971
|
let effectivePermission = decision.permission;
|
|
2029
2972
|
const policy = this.opts.permissionPolicy;
|
|
2030
2973
|
const yolo = policy.getYolo?.() === true || policy.getYoloDestructive?.() === true;
|
|
2031
|
-
|
|
2974
|
+
const authoritativeAuto = decision.source === "yolo";
|
|
2975
|
+
if (toolDangerousCaps.length > 0 && effectivePermission === "auto" && !yolo && !authoritativeAuto) {
|
|
2032
2976
|
effectivePermission = "confirm";
|
|
2033
2977
|
}
|
|
2034
2978
|
if (effectivePermission === "deny") {
|
|
@@ -2170,9 +3114,10 @@ ${post.additionalContext}`;
|
|
|
2170
3114
|
});
|
|
2171
3115
|
this.opts.renderer?.writeToolCall(tool.name, use.input);
|
|
2172
3116
|
const output = await this.runWithTimeout(tool, use.input, ctx.signal, ctx, use.id);
|
|
2173
|
-
const text = this.serializer.serialize(output);
|
|
3117
|
+
const text = this.serializer.serialize(output, { toolName: tool.name, input: use.input });
|
|
2174
3118
|
const scrubbed = this.opts.secretScrubber.scrub(text);
|
|
2175
|
-
const
|
|
3119
|
+
const withArtifact = await maybePersistLargeToolOutput(tool.name, scrubbed, budget);
|
|
3120
|
+
const { text: capped, newBudget } = this.serializer.enforceCap(withArtifact, budget);
|
|
2176
3121
|
this.opts.renderer?.writeToolResult(tool.name, capped, false);
|
|
2177
3122
|
return {
|
|
2178
3123
|
block: {
|
|
@@ -2197,38 +3142,27 @@ ${post.additionalContext}`;
|
|
|
2197
3142
|
tool.timeoutMs ?? this.iterationTimeoutMs,
|
|
2198
3143
|
this.maxToolTimeoutMs
|
|
2199
3144
|
);
|
|
2200
|
-
const
|
|
2201
|
-
const
|
|
2202
|
-
|
|
2203
|
-
let cleanupCalled = false;
|
|
2204
|
-
let caught = false;
|
|
3145
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
3146
|
+
const combined = AbortSignal.any([parentSignal, timeoutSignal]);
|
|
3147
|
+
let output;
|
|
2205
3148
|
try {
|
|
2206
|
-
|
|
2207
|
-
return await this.runStreamedTool(tool, input, ctx, combined, toolUseId);
|
|
2208
|
-
}
|
|
2209
|
-
return await tool.execute(input, ctx, { signal: combined });
|
|
3149
|
+
output = typeof tool.executeStream === "function" ? await this.runStreamedTool(tool, input, ctx, combined, toolUseId) : await tool.execute(input, ctx, { signal: combined });
|
|
2210
3150
|
} catch (err) {
|
|
2211
|
-
|
|
2212
|
-
if (combined.aborted && typeof tool.cleanup === "function") {
|
|
2213
|
-
cleanupCalled = true;
|
|
2214
|
-
try {
|
|
2215
|
-
await tool.cleanup(input, ctx);
|
|
2216
|
-
} catch {
|
|
2217
|
-
}
|
|
2218
|
-
}
|
|
3151
|
+
if (combined.aborted) await this.runToolCleanup(tool, input, ctx);
|
|
2219
3152
|
throw err;
|
|
2220
|
-
}
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
3153
|
+
}
|
|
3154
|
+
if (combined.aborted) {
|
|
3155
|
+
await this.runToolCleanup(tool, input, ctx);
|
|
3156
|
+
throw combined.reason instanceof Error ? combined.reason : new Error(typeof combined.reason === "string" ? combined.reason : "tool timeout");
|
|
3157
|
+
}
|
|
3158
|
+
return output;
|
|
3159
|
+
}
|
|
3160
|
+
/** Best-effort tool cleanup; never let it mask the original error. */
|
|
3161
|
+
async runToolCleanup(tool, input, ctx) {
|
|
3162
|
+
if (typeof tool.cleanup !== "function") return;
|
|
3163
|
+
try {
|
|
3164
|
+
await tool.cleanup(input, ctx);
|
|
3165
|
+
} catch {
|
|
2232
3166
|
}
|
|
2233
3167
|
}
|
|
2234
3168
|
async runStreamedTool(tool, input, ctx, signal, toolUseId) {
|
|
@@ -2397,6 +3331,25 @@ function extractMalformedRaw(input) {
|
|
|
2397
3331
|
return String(value);
|
|
2398
3332
|
}
|
|
2399
3333
|
}
|
|
3334
|
+
var TOOL_OUTPUT_ARTIFACT_THRESHOLD_BYTES = 64 * 1024;
|
|
3335
|
+
async function maybePersistLargeToolOutput(toolName, content, budget) {
|
|
3336
|
+
const bytes = Buffer.byteLength(content, "utf8");
|
|
3337
|
+
if (bytes <= Math.min(TOOL_OUTPUT_ARTIFACT_THRESHOLD_BYTES, Math.max(0, budget))) {
|
|
3338
|
+
return content;
|
|
3339
|
+
}
|
|
3340
|
+
try {
|
|
3341
|
+
const dir = path3.join(wstackGlobalRoot(), "tool-output");
|
|
3342
|
+
await fs.mkdir(dir, { recursive: true });
|
|
3343
|
+
const safeTool = toolName.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 40) || "tool";
|
|
3344
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
3345
|
+
const filePath = path3.join(dir, `${stamp}-${safeTool}-${randomUUID()}.log`);
|
|
3346
|
+
await fs.writeFile(filePath, content, "utf8");
|
|
3347
|
+
return content + `
|
|
3348
|
+
[full tool output: ${bytes} bytes at ${filePath}; read/grep that file selectively instead of re-running or requesting more output]`;
|
|
3349
|
+
} catch {
|
|
3350
|
+
return content;
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
2400
3353
|
|
|
2401
3354
|
// src/execution/autonomous-runner.ts
|
|
2402
3355
|
var DoneConditionChecker = class {
|
|
@@ -3592,6 +4545,7 @@ ${recentJournal}` : ""
|
|
|
3592
4545
|
|
|
3593
4546
|
// src/coordination/subagent-budget.ts
|
|
3594
4547
|
var TIMEOUT_PREEMPT_FRACTION = 0.85;
|
|
4548
|
+
var DECISION_TIMEOUT_MS = 6e4;
|
|
3595
4549
|
var BudgetExceededError = class extends Error {
|
|
3596
4550
|
kind;
|
|
3597
4551
|
limit;
|
|
@@ -3621,6 +4575,31 @@ var BudgetThresholdSignal = class extends Error {
|
|
|
3621
4575
|
};
|
|
3622
4576
|
var SubagentBudget = class _SubagentBudget {
|
|
3623
4577
|
limits;
|
|
4578
|
+
/** Patch one or more budget limits in-place after construction.
|
|
4579
|
+
* Used by the coordinator watchdog when granting an extension.
|
|
4580
|
+
* All fields are optional — only provided fields are updated.
|
|
4581
|
+
* This is the single write path for limit mutations so that future
|
|
4582
|
+
* validation or side-effects live in one place (M1). */
|
|
4583
|
+
patchLimits(ext) {
|
|
4584
|
+
if (ext.maxIterations !== void 0) {
|
|
4585
|
+
this.limits.maxIterations = ext.maxIterations;
|
|
4586
|
+
}
|
|
4587
|
+
if (ext.maxToolCalls !== void 0) {
|
|
4588
|
+
this.limits.maxToolCalls = ext.maxToolCalls;
|
|
4589
|
+
}
|
|
4590
|
+
if (ext.maxTokens !== void 0) {
|
|
4591
|
+
this.limits.maxTokens = ext.maxTokens;
|
|
4592
|
+
}
|
|
4593
|
+
if (ext.maxCostUsd !== void 0) {
|
|
4594
|
+
this.limits.maxCostUsd = ext.maxCostUsd;
|
|
4595
|
+
}
|
|
4596
|
+
if (ext.timeoutMs !== void 0) {
|
|
4597
|
+
this.limits.timeoutMs = ext.timeoutMs;
|
|
4598
|
+
}
|
|
4599
|
+
if (ext.idleTimeoutMs !== void 0) {
|
|
4600
|
+
this.limits.idleTimeoutMs = ext.idleTimeoutMs;
|
|
4601
|
+
}
|
|
4602
|
+
}
|
|
3624
4603
|
iterations = 0;
|
|
3625
4604
|
toolCalls = 0;
|
|
3626
4605
|
tokenInput = 0;
|
|
@@ -3641,12 +4620,44 @@ var SubagentBudget = class _SubagentBudget {
|
|
|
3641
4620
|
* or hung listener (Director not built / event filter detached mid-run)
|
|
3642
4621
|
* leaves the budget over-limit and never enforces anything.
|
|
3643
4622
|
*/
|
|
3644
|
-
static DECISION_TIMEOUT_MS =
|
|
4623
|
+
static DECISION_TIMEOUT_MS = DECISION_TIMEOUT_MS;
|
|
3645
4624
|
/**
|
|
3646
4625
|
* Injected by the runner when wiring the budget to its EventBus.
|
|
3647
4626
|
* Used to emit `budget.threshold_reached` events in `'auto'` mode.
|
|
3648
4627
|
*/
|
|
3649
4628
|
_events;
|
|
4629
|
+
/**
|
|
4630
|
+
* Guard against dual-path races between the coordinator watchdog
|
|
4631
|
+
* (`executeWithTimeout`) and the budget's own `checkTimeout()`.
|
|
4632
|
+
* Both paths detect `elapsed >= timeoutMs` and can emit
|
|
4633
|
+
* `budget.threshold_reached` for kind `'timeout'` simultaneously.
|
|
4634
|
+
* Set to the current `timeoutMs` ceiling by the coordinator BEFORE
|
|
4635
|
+
* calling `onThreshold`, and cleared after the negotiation resolves.
|
|
4636
|
+
* `checkTimeout()` skips its wall-clock check while this is set so
|
|
4637
|
+
* the coordinator's watchdog is the sole source of wall-clock timeout
|
|
4638
|
+
* events — `checkTimeout()` focuses exclusively on `idle_timeout`.
|
|
4639
|
+
*/
|
|
4640
|
+
_watchdogActive;
|
|
4641
|
+
/** Returns the timeout ceiling currently being negotiated by the watchdog,
|
|
4642
|
+
* or `undefined` when no wall-clock negotiation is in flight.
|
|
4643
|
+
* Used by `executeWithTimeout` to detect a stale lock (M3). */
|
|
4644
|
+
get watchdogActive() {
|
|
4645
|
+
return this._watchdogActive;
|
|
4646
|
+
}
|
|
4647
|
+
/** Called by the coordinator watchdog BEFORE calling `onThreshold` so that
|
|
4648
|
+
* `checkTimeout()` skips its wall-clock check for this ceiling. Prevents
|
|
4649
|
+
* the budget's own `checkTimeout()` from emitting a second
|
|
4650
|
+
* `budget.threshold_reached` event while the watchdog is already
|
|
4651
|
+
* negotiating the same wall-clock deadline (C1). */
|
|
4652
|
+
setWatchdogNegotiation(timeoutMs) {
|
|
4653
|
+
this._watchdogActive = timeoutMs;
|
|
4654
|
+
}
|
|
4655
|
+
/** Clears the watchdog guard after negotiation resolves. Called in the
|
|
4656
|
+
* `finally` block of both the pre-empt and deadline branches so it fires
|
|
4657
|
+
* on every exit path: grant, deny, throw, or error. */
|
|
4658
|
+
clearWatchdogNegotiation() {
|
|
4659
|
+
this._watchdogActive = void 0;
|
|
4660
|
+
}
|
|
3650
4661
|
/**
|
|
3651
4662
|
* Negotiation mode — controls whether a threshold hit tries to emit
|
|
3652
4663
|
* `budget.threshold_reached` and wait for a coordinator decision, or
|
|
@@ -3747,7 +4758,8 @@ var SubagentBudget = class _SubagentBudget {
|
|
|
3747
4758
|
if (this.limits.idleTimeoutMs !== void 0 && idle > this.limits.idleTimeoutMs) {
|
|
3748
4759
|
exceeded.push({ kind: "idle_timeout", used: idle, limit: this.limits.idleTimeoutMs });
|
|
3749
4760
|
}
|
|
3750
|
-
|
|
4761
|
+
const wallOwnedByWatchdog = this._onThreshold !== void 0 && this._watchdogActive === this.limits.timeoutMs;
|
|
4762
|
+
if (this.limits.timeoutMs !== void 0 && elapsedMs > this.limits.timeoutMs && !wallOwnedByWatchdog) {
|
|
3751
4763
|
exceeded.push({ kind: "timeout", used: elapsedMs, limit: this.limits.timeoutMs });
|
|
3752
4764
|
}
|
|
3753
4765
|
}
|
|
@@ -3761,19 +4773,99 @@ var SubagentBudget = class _SubagentBudget {
|
|
|
3761
4773
|
throw new BudgetExceededError(first2.kind, first2.limit, first2.used);
|
|
3762
4774
|
}
|
|
3763
4775
|
const bus = this._events;
|
|
3764
|
-
if (!bus
|
|
4776
|
+
if (!bus) {
|
|
3765
4777
|
const first2 = exceeded[0] ?? { kind: "iterations", limit: 0, used: 0 };
|
|
3766
4778
|
throw new BudgetExceededError(first2.kind, first2.limit, first2.used);
|
|
3767
4779
|
}
|
|
4780
|
+
const first = exceeded[0] ?? { kind: "iterations", limit: 0, used: 0 };
|
|
4781
|
+
if (bus.hasListenerFor("budget.threshold_reached")) {
|
|
4782
|
+
for (const entry of exceeded) {
|
|
4783
|
+
if (this._pendingNegotiations.has(entry.kind)) continue;
|
|
4784
|
+
this._pendingNegotiations.set(entry.kind, this._negotiateExtension(entry));
|
|
4785
|
+
}
|
|
4786
|
+
const decision = this._pendingNegotiations.get(first.kind);
|
|
4787
|
+
if (!decision) throw new Error(`No pending negotiation for ${first.kind}`);
|
|
4788
|
+
throw new BudgetThresholdSignal(first.kind, first.limit, first.used, decision);
|
|
4789
|
+
}
|
|
4790
|
+
let hardStop = null;
|
|
3768
4791
|
for (const entry of exceeded) {
|
|
3769
4792
|
if (this._pendingNegotiations.has(entry.kind)) continue;
|
|
3770
|
-
const
|
|
3771
|
-
this._pendingNegotiations.set(entry.kind,
|
|
4793
|
+
const marker = Promise.resolve("stop");
|
|
4794
|
+
this._pendingNegotiations.set(entry.kind, marker);
|
|
4795
|
+
void marker.finally(() => this._pendingNegotiations.delete(entry.kind));
|
|
4796
|
+
const sync = this._invokeHandlerSync(entry);
|
|
4797
|
+
if (!sync) hardStop ??= new BudgetExceededError(entry.kind, entry.limit, entry.used);
|
|
3772
4798
|
}
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
4799
|
+
if (hardStop) throw hardStop;
|
|
4800
|
+
return exceeded;
|
|
4801
|
+
}
|
|
4802
|
+
/**
|
|
4803
|
+
* Invoke `onThreshold` once for `entry` on the NO-LISTENER path and report
|
|
4804
|
+
* whether it decided synchronously. Returns `true` when the handler returned
|
|
4805
|
+
* a synchronous decision (already honored — an `extend` patched the limits),
|
|
4806
|
+
* or `false` when it returned a Promise (async; the caller hard-stops, since
|
|
4807
|
+
* there is no listener to resolve the negotiation). The handler is given the
|
|
4808
|
+
* full info shape (`requestDecision` plus direct `extend`/`deny`) so both
|
|
4809
|
+
* recording handlers and policy handlers work without a wired listener.
|
|
4810
|
+
*/
|
|
4811
|
+
_invokeHandlerSync(entry) {
|
|
4812
|
+
const handler = this._onThreshold;
|
|
4813
|
+
if (!handler) return false;
|
|
4814
|
+
let extendArg;
|
|
4815
|
+
const result = handler({
|
|
4816
|
+
kind: entry.kind,
|
|
4817
|
+
used: entry.used,
|
|
4818
|
+
limit: entry.limit,
|
|
4819
|
+
requestDecision: () => this._busRequestDecision(entry),
|
|
4820
|
+
// Direct hooks for synchronous policy/recording handlers.
|
|
4821
|
+
extend: (extra) => {
|
|
4822
|
+
extendArg = extra;
|
|
4823
|
+
},
|
|
4824
|
+
deny: () => {
|
|
4825
|
+
}
|
|
4826
|
+
});
|
|
4827
|
+
if (result && typeof result.then === "function") return false;
|
|
4828
|
+
if (result === "throw") return false;
|
|
4829
|
+
if (result && typeof result === "object" && "extend" in result) {
|
|
4830
|
+
extendArg = result.extend;
|
|
4831
|
+
}
|
|
4832
|
+
if (extendArg) this.patchLimits(extendArg);
|
|
4833
|
+
return true;
|
|
4834
|
+
}
|
|
4835
|
+
/**
|
|
4836
|
+
* Emit `budget.threshold_reached` and resolve to the listener's verdict.
|
|
4837
|
+
* Resolves to `'stop'` immediately when there is no listener (or no bus) so
|
|
4838
|
+
* no negotiation can hang and no fallback timer leaks. Mirrors the
|
|
4839
|
+
* coordinator watchdog's own request path so both agree on the no-listener
|
|
4840
|
+
* default.
|
|
4841
|
+
*/
|
|
4842
|
+
_busRequestDecision(entry) {
|
|
4843
|
+
const bus = this._events;
|
|
4844
|
+
if (!bus || !bus.hasListenerFor("budget.threshold_reached")) {
|
|
4845
|
+
return Promise.resolve("stop");
|
|
4846
|
+
}
|
|
4847
|
+
return new Promise((resolve3) => {
|
|
4848
|
+
let resolved = false;
|
|
4849
|
+
const respond = (d) => {
|
|
4850
|
+
if (resolved) return;
|
|
4851
|
+
resolved = true;
|
|
4852
|
+
clearTimeout(fallback);
|
|
4853
|
+
resolve3(d);
|
|
4854
|
+
};
|
|
4855
|
+
const fallback = setTimeout(() => respond("stop"), _SubagentBudget.DECISION_TIMEOUT_MS);
|
|
4856
|
+
bus.emit("budget.threshold_reached", {
|
|
4857
|
+
kind: entry.kind,
|
|
4858
|
+
used: entry.used,
|
|
4859
|
+
limit: entry.limit,
|
|
4860
|
+
timeoutMs: _SubagentBudget.DECISION_TIMEOUT_MS,
|
|
4861
|
+
// deny() wins over a same-dispatch extend(): a listener that both grants
|
|
4862
|
+
// and denies (or two listeners disagreeing) is resolved as a stop. The
|
|
4863
|
+
// grant is deferred a microtask so a synchronous deny in the same emit
|
|
4864
|
+
// pre-empts it; async grants still resolve normally.
|
|
4865
|
+
extend: (extra) => queueMicrotask(() => respond({ extend: extra })),
|
|
4866
|
+
deny: () => respond("stop")
|
|
4867
|
+
});
|
|
4868
|
+
});
|
|
3777
4869
|
}
|
|
3778
4870
|
/**
|
|
3779
4871
|
* Per-kind in-flight negotiation Promises. Each budget kind can have its
|
|
@@ -3793,77 +4885,33 @@ var SubagentBudget = class _SubagentBudget {
|
|
|
3793
4885
|
* `{ extend: {} }` — keep going without patching; next overrun fires
|
|
3794
4886
|
* a fresh signal.
|
|
3795
4887
|
*/
|
|
3796
|
-
async _negotiateExtension(
|
|
4888
|
+
async _negotiateExtension(entry) {
|
|
3797
4889
|
if (!this._onThreshold) {
|
|
3798
4890
|
return "stop";
|
|
3799
4891
|
}
|
|
3800
4892
|
try {
|
|
3801
|
-
const first = exceeded[0] ?? { kind: "iterations", limit: 0, used: 0 };
|
|
3802
4893
|
const result = this._onThreshold({
|
|
3803
|
-
kind:
|
|
3804
|
-
used:
|
|
3805
|
-
limit:
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
if (resolved) return;
|
|
3815
|
-
resolved = true;
|
|
3816
|
-
resolve2(d);
|
|
3817
|
-
};
|
|
3818
|
-
const fallback = setTimeout(
|
|
3819
|
-
() => respond("stop"),
|
|
3820
|
-
_SubagentBudget.DECISION_TIMEOUT_MS
|
|
3821
|
-
);
|
|
3822
|
-
for (const { kind: kind2, used, limit } of exceeded) {
|
|
3823
|
-
bus.emit("budget.threshold_reached", {
|
|
3824
|
-
kind: kind2,
|
|
3825
|
-
used,
|
|
3826
|
-
limit,
|
|
3827
|
-
timeoutMs: _SubagentBudget.DECISION_TIMEOUT_MS,
|
|
3828
|
-
extend: (extra) => {
|
|
3829
|
-
clearTimeout(fallback);
|
|
3830
|
-
respond({ extend: extra });
|
|
3831
|
-
},
|
|
3832
|
-
deny: () => {
|
|
3833
|
-
clearTimeout(fallback);
|
|
3834
|
-
respond("stop");
|
|
3835
|
-
}
|
|
3836
|
-
});
|
|
3837
|
-
}
|
|
3838
|
-
});
|
|
4894
|
+
kind: entry.kind,
|
|
4895
|
+
used: entry.used,
|
|
4896
|
+
limit: entry.limit,
|
|
4897
|
+
// One event for THIS kind only — each exceeded kind has its own
|
|
4898
|
+
// negotiation (and its own resolve), so there is no cross-kind
|
|
4899
|
+
// first-wins drop and no O(N^2) re-emission.
|
|
4900
|
+
requestDecision: () => this._busRequestDecision(entry),
|
|
4901
|
+
extend: (extra) => {
|
|
4902
|
+
this.patchLimits(extra);
|
|
4903
|
+
},
|
|
4904
|
+
deny: () => {
|
|
3839
4905
|
}
|
|
3840
4906
|
});
|
|
3841
4907
|
if (result === "throw") return "stop";
|
|
3842
4908
|
if (result === "continue") return { extend: {} };
|
|
3843
4909
|
const decision = await result;
|
|
3844
4910
|
if (decision === "stop") return "stop";
|
|
3845
|
-
|
|
3846
|
-
if (ext.maxIterations !== void 0) {
|
|
3847
|
-
this.limits.maxIterations = ext.maxIterations;
|
|
3848
|
-
}
|
|
3849
|
-
if (ext.maxToolCalls !== void 0) {
|
|
3850
|
-
this.limits.maxToolCalls = ext.maxToolCalls;
|
|
3851
|
-
}
|
|
3852
|
-
if (ext.maxTokens !== void 0) {
|
|
3853
|
-
this.limits.maxTokens = ext.maxTokens;
|
|
3854
|
-
}
|
|
3855
|
-
if (ext.maxCostUsd !== void 0) {
|
|
3856
|
-
this.limits.maxCostUsd = ext.maxCostUsd;
|
|
3857
|
-
}
|
|
3858
|
-
if (ext.timeoutMs !== void 0) {
|
|
3859
|
-
this.limits.timeoutMs = ext.timeoutMs;
|
|
3860
|
-
}
|
|
3861
|
-
if (ext.idleTimeoutMs !== void 0) {
|
|
3862
|
-
this.limits.idleTimeoutMs = ext.idleTimeoutMs;
|
|
3863
|
-
}
|
|
4911
|
+
this.patchLimits(decision.extend);
|
|
3864
4912
|
return decision;
|
|
3865
4913
|
} finally {
|
|
3866
|
-
this._pendingNegotiations.delete(kind);
|
|
4914
|
+
this._pendingNegotiations.delete(entry.kind);
|
|
3867
4915
|
}
|
|
3868
4916
|
}
|
|
3869
4917
|
recordIteration() {
|
|
@@ -3906,7 +4954,8 @@ var SubagentBudget = class _SubagentBudget {
|
|
|
3906
4954
|
const { timeoutMs, idleTimeoutMs } = this.limits;
|
|
3907
4955
|
if (timeoutMs === void 0 && idleTimeoutMs === void 0) return;
|
|
3908
4956
|
const elapsed = Date.now() - this.startTime;
|
|
3909
|
-
const
|
|
4957
|
+
const wallSkipped = this._onThreshold !== void 0 && this._watchdogActive !== void 0 && timeoutMs !== void 0 && this._watchdogActive === timeoutMs;
|
|
4958
|
+
const wallTripped = wallSkipped ? false : timeoutMs !== void 0 && elapsed > timeoutMs;
|
|
3910
4959
|
const idleTripped = idleTimeoutMs !== void 0 && this.idleMs() > idleTimeoutMs;
|
|
3911
4960
|
if (!wallTripped && !idleTripped) return;
|
|
3912
4961
|
void this.checkLimits(elapsed);
|
|
@@ -7225,6 +8274,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
|
|
|
7225
8274
|
terminating = /* @__PURE__ */ new Set();
|
|
7226
8275
|
constructor(config, options = {}) {
|
|
7227
8276
|
super();
|
|
8277
|
+
this.setMaxListeners(0);
|
|
7228
8278
|
this.coordinatorId = config.coordinatorId;
|
|
7229
8279
|
this.config = config;
|
|
7230
8280
|
this.runner = options.runner;
|
|
@@ -7422,7 +8472,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
|
|
|
7422
8472
|
taskIds.map((id) => {
|
|
7423
8473
|
const cached = this.completedResults.find((r) => r.taskId === id);
|
|
7424
8474
|
if (cached) return cached;
|
|
7425
|
-
return new Promise((
|
|
8475
|
+
return new Promise((resolve3, reject) => {
|
|
7426
8476
|
const timeout = setTimeout(() => {
|
|
7427
8477
|
this.off("task.completed", handler);
|
|
7428
8478
|
reject(new Error(`awaitTasks timed out waiting for task "${id}"`));
|
|
@@ -7431,7 +8481,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
|
|
|
7431
8481
|
if (result.taskId === id) {
|
|
7432
8482
|
clearTimeout(timeout);
|
|
7433
8483
|
this.off("task.completed", handler);
|
|
7434
|
-
|
|
8484
|
+
resolve3(result);
|
|
7435
8485
|
}
|
|
7436
8486
|
};
|
|
7437
8487
|
this.on("task.completed", handler);
|
|
@@ -7619,7 +8669,13 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
|
|
|
7619
8669
|
let result;
|
|
7620
8670
|
budget.start();
|
|
7621
8671
|
try {
|
|
7622
|
-
const outcome = await this.executeWithTimeout(
|
|
8672
|
+
const outcome = await this.executeWithTimeout(
|
|
8673
|
+
this.runner,
|
|
8674
|
+
task,
|
|
8675
|
+
runCtx,
|
|
8676
|
+
budget,
|
|
8677
|
+
subagent.config.preemptFraction
|
|
8678
|
+
);
|
|
7623
8679
|
result = {
|
|
7624
8680
|
subagentId,
|
|
7625
8681
|
taskId: task.id,
|
|
@@ -7646,7 +8702,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
|
|
|
7646
8702
|
}
|
|
7647
8703
|
this.recordCompletion(result);
|
|
7648
8704
|
}
|
|
7649
|
-
async executeWithTimeout(runner, task, ctx, budget) {
|
|
8705
|
+
async executeWithTimeout(runner, task, ctx, budget, preemptFraction = TIMEOUT_PREEMPT_FRACTION) {
|
|
7650
8706
|
const initialTimeoutMs = budget.limits.timeoutMs;
|
|
7651
8707
|
const idleLimitMs = budget.limits.idleTimeoutMs;
|
|
7652
8708
|
if (initialTimeoutMs === void 0 && idleLimitMs === void 0) {
|
|
@@ -7654,8 +8710,21 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
|
|
|
7654
8710
|
}
|
|
7655
8711
|
const start = Date.now();
|
|
7656
8712
|
let timer = null;
|
|
7657
|
-
let
|
|
8713
|
+
let PreemptState;
|
|
8714
|
+
((PreemptState2) => {
|
|
8715
|
+
PreemptState2["ACTIVE"] = "active";
|
|
8716
|
+
PreemptState2["LOCKED"] = "locked";
|
|
8717
|
+
})(PreemptState || (PreemptState = {}));
|
|
8718
|
+
let preemptedCeiling = null;
|
|
8719
|
+
let preemptState = "active" /* ACTIVE */;
|
|
8720
|
+
let lastGrantActivityTs = -1;
|
|
7658
8721
|
const timeoutPromise = new Promise((_, reject) => {
|
|
8722
|
+
const terminate = (kind, limit, used) => {
|
|
8723
|
+
this.subagents.get(ctx.subagentId)?.abortController.abort();
|
|
8724
|
+
reject(
|
|
8725
|
+
budget._events?.hasListenerFor("budget.threshold_reached") ? new Error(`subagent stopped: budget ${kind} (limit=${limit}, used=${used})`) : new BudgetExceededError(kind, limit, used)
|
|
8726
|
+
);
|
|
8727
|
+
};
|
|
7659
8728
|
const armFor = (ms) => {
|
|
7660
8729
|
if (timer) clearTimeout(timer);
|
|
7661
8730
|
timer = setTimeout(onTick, Math.max(0, ms));
|
|
@@ -7664,7 +8733,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
|
|
|
7664
8733
|
const wallLimit = budget.limits.timeoutMs ?? initialTimeoutMs;
|
|
7665
8734
|
const wallRemaining = initialTimeoutMs === void 0 ? Number.POSITIVE_INFINITY : wallLimit - (Date.now() - start);
|
|
7666
8735
|
const idleRemaining = idleLimitMs === void 0 ? Number.POSITIVE_INFINITY : (budget.limits.idleTimeoutMs ?? idleLimitMs) - budget.idleMs();
|
|
7667
|
-
const preemptRemaining = initialTimeoutMs === void 0 ||
|
|
8736
|
+
const preemptRemaining = initialTimeoutMs === void 0 || preemptedCeiling === wallLimit ? Number.POSITIVE_INFINITY : wallLimit * preemptFraction - (Date.now() - start);
|
|
7668
8737
|
armFor(Math.max(25, Math.min(wallRemaining, idleRemaining, preemptRemaining)));
|
|
7669
8738
|
};
|
|
7670
8739
|
const negotiateTimeout = async (used, limit) => {
|
|
@@ -7674,16 +8743,42 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
|
|
|
7674
8743
|
kind: "timeout",
|
|
7675
8744
|
used,
|
|
7676
8745
|
limit,
|
|
7677
|
-
requestDecision: () =>
|
|
7678
|
-
budget._events?.
|
|
7679
|
-
|
|
7680
|
-
|
|
7681
|
-
|
|
7682
|
-
|
|
7683
|
-
|
|
7684
|
-
|
|
8746
|
+
requestDecision: () => {
|
|
8747
|
+
if (!budget._events?.hasListenerFor("budget.threshold_reached")) {
|
|
8748
|
+
return Promise.resolve("stop");
|
|
8749
|
+
}
|
|
8750
|
+
return new Promise((resolveDecision) => {
|
|
8751
|
+
let settled = false;
|
|
8752
|
+
const resolve3 = (d) => {
|
|
8753
|
+
if (settled) return;
|
|
8754
|
+
settled = true;
|
|
8755
|
+
resolveDecision(d);
|
|
8756
|
+
};
|
|
8757
|
+
const fallback = setTimeout(() => resolve3("stop"), DECISION_TIMEOUT_MS);
|
|
8758
|
+
budget._events?.emit("budget.threshold_reached", {
|
|
8759
|
+
kind: "timeout",
|
|
8760
|
+
used,
|
|
8761
|
+
limit,
|
|
8762
|
+
// Informational: the budget's own decision deadline. Listeners may use
|
|
8763
|
+
// this to display a countdown. The coordinator does NOT enforce it —
|
|
8764
|
+
// it is the budget's own `setTimeout(fallback)` that races against
|
|
8765
|
+
// the listener's `extend()`/`deny()` call to guarantee progress.
|
|
8766
|
+
timeoutMs: DECISION_TIMEOUT_MS,
|
|
8767
|
+
// deny() wins over a same-dispatch extend(): defer the grant a
|
|
8768
|
+
// microtask so a synchronous deny in the same emit pre-empts it
|
|
8769
|
+
// (a listener that both grants and denies, or two listeners
|
|
8770
|
+
// disagreeing, resolves as a stop). Async grants still resolve.
|
|
8771
|
+
extend: (extra) => {
|
|
8772
|
+
clearTimeout(fallback);
|
|
8773
|
+
queueMicrotask(() => resolve3({ extend: extra }));
|
|
8774
|
+
},
|
|
8775
|
+
deny: () => {
|
|
8776
|
+
clearTimeout(fallback);
|
|
8777
|
+
resolve3("stop");
|
|
8778
|
+
}
|
|
8779
|
+
});
|
|
7685
8780
|
});
|
|
7686
|
-
}
|
|
8781
|
+
}
|
|
7687
8782
|
});
|
|
7688
8783
|
return typeof result === "string" ? result : await result;
|
|
7689
8784
|
};
|
|
@@ -7694,21 +8789,45 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
|
|
|
7694
8789
|
const wallExceeded = wallLimit !== void 0 && elapsed >= wallLimit;
|
|
7695
8790
|
const idleExceeded = idleLimit !== void 0 && budget.idleMs() >= idleLimit;
|
|
7696
8791
|
if (idleExceeded && !wallExceeded) {
|
|
8792
|
+
budget._events?.emit("budget.threshold_reached", {
|
|
8793
|
+
kind: "idle_timeout",
|
|
8794
|
+
used: budget.idleMs(),
|
|
8795
|
+
limit: idleLimit ?? 0,
|
|
8796
|
+
timeoutMs: DECISION_TIMEOUT_MS,
|
|
8797
|
+
extend: () => {
|
|
8798
|
+
},
|
|
8799
|
+
deny: () => {
|
|
8800
|
+
}
|
|
8801
|
+
});
|
|
7697
8802
|
this.subagents.get(ctx.subagentId)?.abortController.abort();
|
|
7698
|
-
reject(new BudgetExceededError("
|
|
8803
|
+
reject(new BudgetExceededError("idle_timeout", idleLimit ?? 0, budget.idleMs()));
|
|
7699
8804
|
return;
|
|
7700
8805
|
}
|
|
7701
|
-
if (wallLimit !== void 0 && !wallExceeded && budget.onThreshold &&
|
|
8806
|
+
if (wallLimit !== void 0 && !wallExceeded && budget.onThreshold && preemptState === "active" /* ACTIVE */ && elapsed >= wallLimit * preemptFraction) {
|
|
8807
|
+
const activityTs = Date.now() - budget.idleMs();
|
|
8808
|
+
if (activityTs <= lastGrantActivityTs) {
|
|
8809
|
+
preemptState = "locked" /* LOCKED */;
|
|
8810
|
+
preemptedCeiling = wallLimit;
|
|
8811
|
+
scheduleNext();
|
|
8812
|
+
return;
|
|
8813
|
+
}
|
|
8814
|
+
budget.setWatchdogNegotiation(wallLimit);
|
|
7702
8815
|
try {
|
|
7703
8816
|
const decision = await negotiateTimeout(elapsed, wallLimit);
|
|
7704
8817
|
if (typeof decision !== "string" && decision.extend.timeoutMs !== void 0) {
|
|
7705
|
-
budget.
|
|
7706
|
-
|
|
8818
|
+
budget.patchLimits({ timeoutMs: decision.extend.timeoutMs });
|
|
8819
|
+
lastGrantActivityTs = Date.now() - budget.idleMs();
|
|
8820
|
+
preemptState = "active" /* ACTIVE */;
|
|
8821
|
+
preemptedCeiling = null;
|
|
7707
8822
|
} else {
|
|
7708
|
-
|
|
8823
|
+
preemptState = "locked" /* LOCKED */;
|
|
8824
|
+
preemptedCeiling = wallLimit;
|
|
7709
8825
|
}
|
|
7710
8826
|
} catch {
|
|
7711
|
-
|
|
8827
|
+
preemptState = "locked" /* LOCKED */;
|
|
8828
|
+
preemptedCeiling = wallLimit;
|
|
8829
|
+
} finally {
|
|
8830
|
+
budget.clearWatchdogNegotiation();
|
|
7712
8831
|
}
|
|
7713
8832
|
scheduleNext();
|
|
7714
8833
|
return;
|
|
@@ -7723,26 +8842,41 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
|
|
|
7723
8842
|
reject(new BudgetExceededError("timeout", limit, elapsed));
|
|
7724
8843
|
return;
|
|
7725
8844
|
}
|
|
8845
|
+
budget.setWatchdogNegotiation(limit);
|
|
7726
8846
|
try {
|
|
7727
8847
|
const decision = await negotiateTimeout(elapsed, limit);
|
|
7728
|
-
if (decision === "
|
|
7729
|
-
|
|
8848
|
+
if (decision === "throw") {
|
|
8849
|
+
terminate("timeout", limit, elapsed);
|
|
8850
|
+
return;
|
|
8851
|
+
}
|
|
8852
|
+
if (decision === "continue") {
|
|
8853
|
+
preemptState = "locked" /* LOCKED */;
|
|
8854
|
+
preemptedCeiling = wallLimit;
|
|
7730
8855
|
armFor(Math.max(1e3, limit));
|
|
7731
8856
|
return;
|
|
7732
8857
|
}
|
|
8858
|
+
if (decision === "stop") {
|
|
8859
|
+
terminate("timeout", limit, elapsed);
|
|
8860
|
+
return;
|
|
8861
|
+
}
|
|
7733
8862
|
if (decision.extend.timeoutMs !== void 0) {
|
|
7734
|
-
budget.
|
|
7735
|
-
|
|
8863
|
+
budget.patchLimits({ timeoutMs: decision.extend.timeoutMs });
|
|
8864
|
+
lastGrantActivityTs = Date.now() - budget.idleMs();
|
|
8865
|
+
preemptState = "active" /* ACTIVE */;
|
|
8866
|
+
preemptedCeiling = null;
|
|
7736
8867
|
scheduleNext();
|
|
7737
8868
|
return;
|
|
7738
8869
|
}
|
|
7739
|
-
|
|
7740
|
-
|
|
8870
|
+
terminate("timeout", limit, elapsed);
|
|
8871
|
+
return;
|
|
7741
8872
|
} catch (err) {
|
|
7742
8873
|
this.subagents.get(ctx.subagentId)?.abortController.abort();
|
|
7743
8874
|
reject(
|
|
7744
8875
|
err instanceof BudgetExceededError ? err : new BudgetExceededError("timeout", limit, elapsed)
|
|
7745
8876
|
);
|
|
8877
|
+
return;
|
|
8878
|
+
} finally {
|
|
8879
|
+
budget.clearWatchdogNegotiation();
|
|
7746
8880
|
}
|
|
7747
8881
|
};
|
|
7748
8882
|
scheduleNext();
|
|
@@ -8595,7 +9729,7 @@ var DefaultSkillLoader = class {
|
|
|
8595
9729
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
8596
9730
|
for (const e of entries) {
|
|
8597
9731
|
if (!e.isDirectory()) continue;
|
|
8598
|
-
const skillFile =
|
|
9732
|
+
const skillFile = path3.join(dir, e.name, "SKILL.md");
|
|
8599
9733
|
try {
|
|
8600
9734
|
const raw = await fs.readFile(skillFile, "utf8");
|
|
8601
9735
|
const meta = parseFrontmatter(raw);
|
|
@@ -8620,12 +9754,12 @@ var DefaultSkillLoader = class {
|
|
|
8620
9754
|
}
|
|
8621
9755
|
async find(name) {
|
|
8622
9756
|
const all = await this.list();
|
|
8623
|
-
|
|
9757
|
+
const lower = name.toLowerCase();
|
|
9758
|
+
return all.find((s) => s.name.toLowerCase() === lower);
|
|
8624
9759
|
}
|
|
8625
9760
|
async manifestText() {
|
|
8626
|
-
const skills = await this.list();
|
|
8627
|
-
if (skills.length === 0) return "";
|
|
8628
9761
|
const entries = await this.listEntries();
|
|
9762
|
+
if (entries.length === 0) return "";
|
|
8629
9763
|
const lines = ["## Available skills"];
|
|
8630
9764
|
for (const e of entries) {
|
|
8631
9765
|
const scopeTag = e.scope.length > 0 ? ` \u2014 ${e.scope.slice(0, 3).join(", ")}` : "";
|
|
@@ -8639,12 +9773,8 @@ var DefaultSkillLoader = class {
|
|
|
8639
9773
|
const skills = await this.list();
|
|
8640
9774
|
const entries = [];
|
|
8641
9775
|
for (const s of skills) {
|
|
8642
|
-
|
|
8643
|
-
|
|
8644
|
-
const { trigger, scope } = parseDescription(raw);
|
|
8645
|
-
entries.push({ name: s.name, trigger, scope, source: s.source, path: s.path });
|
|
8646
|
-
} catch {
|
|
8647
|
-
}
|
|
9776
|
+
const { trigger, scope } = parseDescriptionFromText(s.description ?? "");
|
|
9777
|
+
entries.push({ name: s.name, trigger, scope, source: s.source, path: s.path });
|
|
8648
9778
|
}
|
|
8649
9779
|
this.entriesCache = entries;
|
|
8650
9780
|
return entries;
|
|
@@ -8655,21 +9785,22 @@ var DefaultSkillLoader = class {
|
|
|
8655
9785
|
this.bodyCache.clear();
|
|
8656
9786
|
}
|
|
8657
9787
|
async readBody(name) {
|
|
8658
|
-
const
|
|
9788
|
+
const key = name.toLowerCase();
|
|
9789
|
+
const cached = this.bodyCache.get(key);
|
|
8659
9790
|
if (cached !== void 0) return cached;
|
|
8660
9791
|
const m = await this.find(name);
|
|
8661
9792
|
if (!m) throw new Error(`Skill "${name}" not found`);
|
|
8662
9793
|
const body = await fs.readFile(m.path, "utf8");
|
|
8663
|
-
this.bodyCache.set(
|
|
9794
|
+
this.bodyCache.set(key, body);
|
|
8664
9795
|
return body;
|
|
8665
9796
|
}
|
|
8666
9797
|
async readSaveBody(name) {
|
|
8667
|
-
const key = `save:${name}`;
|
|
9798
|
+
const key = `save:${name.toLowerCase()}`;
|
|
8668
9799
|
const cached = this.bodyCache.get(key);
|
|
8669
9800
|
if (cached !== void 0) return cached;
|
|
8670
9801
|
const m = await this.find(name);
|
|
8671
9802
|
if (!m) throw new Error(`Skill "${name}" not found`);
|
|
8672
|
-
const savePath =
|
|
9803
|
+
const savePath = path3.join(path3.dirname(m.path), "SKILL.save.md");
|
|
8673
9804
|
let result;
|
|
8674
9805
|
try {
|
|
8675
9806
|
result = await fs.readFile(savePath, "utf8");
|
|
@@ -8725,9 +9856,7 @@ function parseFrontmatter(raw) {
|
|
|
8725
9856
|
flush();
|
|
8726
9857
|
return out;
|
|
8727
9858
|
}
|
|
8728
|
-
function
|
|
8729
|
-
const fm = parseFrontmatter(raw);
|
|
8730
|
-
const desc = fm.description ?? "";
|
|
9859
|
+
function parseDescriptionFromText(desc) {
|
|
8731
9860
|
const firstSentenceEnd = desc.indexOf(". ");
|
|
8732
9861
|
const trigger = firstSentenceEnd !== -1 ? desc.slice(0, firstSentenceEnd + 1).trim() : desc.trim().split("\n")[0] ?? "";
|
|
8733
9862
|
const scope = [];
|