@wrongstack/core 0.1.4 → 0.1.7
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/README.md +155 -0
- package/dist/defaults/index.d.ts +68 -33
- package/dist/defaults/index.js +559 -239
- package/dist/defaults/index.js.map +1 -1
- package/dist/index.d.ts +26 -12
- package/dist/index.js +745 -322
- package/dist/index.js.map +1 -1
- package/dist/kernel/index.d.ts +5 -4
- package/dist/kernel/index.js +2 -1
- package/dist/kernel/index.js.map +1 -1
- package/dist/{secret-scrubber-qU3AwEiI.d.ts → mode-Pjt5vMS6.d.ts} +94 -3
- package/dist/{provider-DovtyuM8.d.ts → provider-txgB0Oq9.d.ts} +27 -30
- package/dist/{session-reader-DR4u3bu9.d.ts → session-reader-7AutWHut.d.ts} +13 -32
- package/dist/system-prompt-vAB0F54-.d.ts +23 -0
- package/dist/types/index.d.ts +4 -4
- package/dist/types/index.js +34 -15
- package/dist/types/index.js.map +1 -1
- package/dist/utils/index.d.ts +16 -11
- package/dist/utils/index.js +40 -13
- package/dist/utils/index.js.map +1 -1
- package/dist/{wstack-paths-D24ynAz1.d.ts → wstack-paths-BGu2INTm.d.ts} +7 -0
- package/package.json +7 -4
- package/dist/system-prompt--mzZnenv.d.ts +0 -16
package/dist/index.js
CHANGED
|
@@ -467,7 +467,8 @@ var TOKENS = {
|
|
|
467
467
|
SkillLoader: t("SkillLoader"),
|
|
468
468
|
SystemPromptBuilder: t("SystemPromptBuilder"),
|
|
469
469
|
SecretScrubber: t("SecretScrubber"),
|
|
470
|
-
ModelsRegistry: t("ModelsRegistry")
|
|
470
|
+
ModelsRegistry: t("ModelsRegistry"),
|
|
471
|
+
ModeStore: t("ModeStore")
|
|
471
472
|
};
|
|
472
473
|
|
|
473
474
|
// src/kernel/run-controller.ts
|
|
@@ -800,16 +801,33 @@ var DEFAULT_SPEC_TEMPLATE = {
|
|
|
800
801
|
|
|
801
802
|
// src/types/task-graph.ts
|
|
802
803
|
function computeTaskProgress(graph) {
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
const
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
804
|
+
let completed = 0, pending = 0, inProgress = 0, blocked = 0, failed = 0, review = 0;
|
|
805
|
+
let estimatedHours = 0, actualHours = 0;
|
|
806
|
+
for (const n of graph.nodes.values()) {
|
|
807
|
+
switch (n.status) {
|
|
808
|
+
case "completed":
|
|
809
|
+
completed++;
|
|
810
|
+
break;
|
|
811
|
+
case "pending":
|
|
812
|
+
pending++;
|
|
813
|
+
break;
|
|
814
|
+
case "in_progress":
|
|
815
|
+
inProgress++;
|
|
816
|
+
break;
|
|
817
|
+
case "blocked":
|
|
818
|
+
blocked++;
|
|
819
|
+
break;
|
|
820
|
+
case "failed":
|
|
821
|
+
failed++;
|
|
822
|
+
break;
|
|
823
|
+
case "review":
|
|
824
|
+
review++;
|
|
825
|
+
break;
|
|
826
|
+
}
|
|
827
|
+
estimatedHours += n.estimateHours ?? 0;
|
|
828
|
+
actualHours += n.actualHours ?? 0;
|
|
829
|
+
}
|
|
830
|
+
const total = graph.nodes.size;
|
|
813
831
|
return {
|
|
814
832
|
total,
|
|
815
833
|
pending,
|
|
@@ -836,16 +854,18 @@ function findCriticalPath(graph) {
|
|
|
836
854
|
}
|
|
837
855
|
function topologicalSort(graph) {
|
|
838
856
|
const visited = /* @__PURE__ */ new Set();
|
|
857
|
+
const inStack = /* @__PURE__ */ new Set();
|
|
839
858
|
const result = [];
|
|
840
859
|
function visit(id) {
|
|
860
|
+
if (inStack.has(id)) return;
|
|
841
861
|
if (visited.has(id)) return;
|
|
862
|
+
if (!graph.nodes.has(id)) return;
|
|
842
863
|
visited.add(id);
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
for (const edge of outgoing) {
|
|
847
|
-
visit(edge.to);
|
|
864
|
+
inStack.add(id);
|
|
865
|
+
for (const edge of graph.edges) {
|
|
866
|
+
if (edge.from === id) visit(edge.to);
|
|
848
867
|
}
|
|
868
|
+
inStack.delete(id);
|
|
849
869
|
result.push(id);
|
|
850
870
|
}
|
|
851
871
|
for (const rootId of graph.rootNodes) {
|
|
@@ -937,7 +957,7 @@ function sanitizeJsonString(s) {
|
|
|
937
957
|
JSON.parse(out);
|
|
938
958
|
return out;
|
|
939
959
|
} catch {
|
|
940
|
-
return
|
|
960
|
+
return null;
|
|
941
961
|
}
|
|
942
962
|
}
|
|
943
963
|
function stripSingleLineComments(s) {
|
|
@@ -1027,7 +1047,22 @@ function stripAnsi(s) {
|
|
|
1027
1047
|
|
|
1028
1048
|
// src/utils/glob-match.ts
|
|
1029
1049
|
function escapeRegex(s) {
|
|
1030
|
-
return s.replace(/[.+^${}()
|
|
1050
|
+
return s.replace(/[.+^${}()|\\]/g, "\\$&");
|
|
1051
|
+
}
|
|
1052
|
+
var COMPILED_GLOB_CACHE = /* @__PURE__ */ new Map();
|
|
1053
|
+
var CACHE_MAX_SIZE = 2e3;
|
|
1054
|
+
function getCachedGlob(pattern) {
|
|
1055
|
+
const cached = COMPILED_GLOB_CACHE.get(pattern);
|
|
1056
|
+
if (cached) return cached;
|
|
1057
|
+
if (COMPILED_GLOB_CACHE.size >= CACHE_MAX_SIZE) {
|
|
1058
|
+
const keys = [...COMPILED_GLOB_CACHE.keys()];
|
|
1059
|
+
for (let i = 0; i < Math.floor(CACHE_MAX_SIZE / 4); i++) {
|
|
1060
|
+
COMPILED_GLOB_CACHE.delete(keys[i]);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
const re = compileGlob(pattern);
|
|
1064
|
+
COMPILED_GLOB_CACHE.set(pattern, re);
|
|
1065
|
+
return re;
|
|
1031
1066
|
}
|
|
1032
1067
|
function compileGlob(pattern) {
|
|
1033
1068
|
let i = 0;
|
|
@@ -1076,7 +1111,7 @@ function compileGlob(pattern) {
|
|
|
1076
1111
|
return new RegExp(re);
|
|
1077
1112
|
}
|
|
1078
1113
|
function matchGlob(pattern, input) {
|
|
1079
|
-
return
|
|
1114
|
+
return getCachedGlob(pattern).test(input);
|
|
1080
1115
|
}
|
|
1081
1116
|
function matchAny(patterns, input) {
|
|
1082
1117
|
return patterns.some((p) => matchGlob(p, input));
|
|
@@ -1243,6 +1278,7 @@ function resolveWstackPaths(opts) {
|
|
|
1243
1278
|
const projectDir = path2.join(globalRoot, "projects", hash);
|
|
1244
1279
|
return {
|
|
1245
1280
|
globalRoot,
|
|
1281
|
+
configDir: globalRoot,
|
|
1246
1282
|
globalConfig: path2.join(globalRoot, "config.json"),
|
|
1247
1283
|
secretsKey: path2.join(globalRoot, ".key"),
|
|
1248
1284
|
globalMemory: path2.join(globalRoot, "memory.md"),
|
|
@@ -1301,7 +1337,6 @@ function createToolOutputSerializer(opts = {}) {
|
|
|
1301
1337
|
}
|
|
1302
1338
|
const half = Math.floor(available / 2);
|
|
1303
1339
|
const first = text.slice(0, half);
|
|
1304
|
-
Buffer.byteLength(first, "utf8");
|
|
1305
1340
|
const second = text.slice(text.length - half);
|
|
1306
1341
|
return { text: `${first}${marker}${second}`, newBudget: 0 };
|
|
1307
1342
|
}
|
|
@@ -1310,21 +1345,33 @@ function createToolOutputSerializer(opts = {}) {
|
|
|
1310
1345
|
|
|
1311
1346
|
// src/utils/token-estimate.ts
|
|
1312
1347
|
var RoughTokenEstimate = (text) => Math.max(1, Math.ceil(text.length / 4));
|
|
1348
|
+
var ESTIMATE_CACHE = /* @__PURE__ */ new Map();
|
|
1349
|
+
var ESTIMATE_CACHE_MAX_SIZE = 1e4;
|
|
1350
|
+
function getCachedEstimate(key, compute) {
|
|
1351
|
+
const existing = ESTIMATE_CACHE.get(key);
|
|
1352
|
+
if (existing !== void 0) return existing;
|
|
1353
|
+
if (ESTIMATE_CACHE.size >= ESTIMATE_CACHE_MAX_SIZE) {
|
|
1354
|
+
const keys = [...ESTIMATE_CACHE.keys()];
|
|
1355
|
+
for (let i = 0; i < Math.floor(ESTIMATE_CACHE_MAX_SIZE / 4); i++) {
|
|
1356
|
+
ESTIMATE_CACHE.delete(keys[i]);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
const estimate = compute();
|
|
1360
|
+
ESTIMATE_CACHE.set(key, estimate);
|
|
1361
|
+
return estimate;
|
|
1362
|
+
}
|
|
1313
1363
|
function estimateToolInputTokens(input) {
|
|
1314
1364
|
if (typeof input === "string") return RoughTokenEstimate(input);
|
|
1315
|
-
if (input
|
|
1316
|
-
return input
|
|
1317
|
-
}
|
|
1318
|
-
const str = typeof input === "object" ? JSON.stringify(input) : String(input);
|
|
1319
|
-
const estimate = RoughTokenEstimate(str);
|
|
1320
|
-
if (input !== null && typeof input === "object" && !Array.isArray(input)) {
|
|
1321
|
-
input.__tokenEstimate = estimate;
|
|
1365
|
+
if (input === null || typeof input !== "object") {
|
|
1366
|
+
return RoughTokenEstimate(String(input));
|
|
1322
1367
|
}
|
|
1323
|
-
|
|
1368
|
+
const key = JSON.stringify(input);
|
|
1369
|
+
return getCachedEstimate(key, () => RoughTokenEstimate(key));
|
|
1324
1370
|
}
|
|
1325
1371
|
function estimateToolResultTokens(content) {
|
|
1326
1372
|
if (typeof content === "string") return RoughTokenEstimate(content);
|
|
1327
|
-
|
|
1373
|
+
const key = JSON.stringify(content);
|
|
1374
|
+
return getCachedEstimate(key, () => RoughTokenEstimate(key));
|
|
1328
1375
|
}
|
|
1329
1376
|
function estimateTextTokens(text) {
|
|
1330
1377
|
return RoughTokenEstimate(text);
|
|
@@ -1567,9 +1614,11 @@ var DefaultPathResolver = class {
|
|
|
1567
1614
|
ensureInsideRoot(absPath) {
|
|
1568
1615
|
const resolved = this.resolve(absPath);
|
|
1569
1616
|
if (!this.isInsideRoot(resolved)) {
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1617
|
+
const display = path2.isAbsolute(absPath) ? path2.basename(absPath) : absPath;
|
|
1618
|
+
const err = new Error(`Path "${display}" resolves outside the project root`);
|
|
1619
|
+
err.fullPath = absPath;
|
|
1620
|
+
err.projectRoot = this.projectRoot;
|
|
1621
|
+
throw err;
|
|
1573
1622
|
}
|
|
1574
1623
|
return resolved;
|
|
1575
1624
|
}
|
|
@@ -1697,7 +1746,9 @@ var DefaultSessionStore = class {
|
|
|
1697
1746
|
try {
|
|
1698
1747
|
handle = await fsp.open(file, "a", 384);
|
|
1699
1748
|
} catch (err) {
|
|
1700
|
-
throw new Error(`Failed to open session file: ${err instanceof Error ? err.message : String(err)}
|
|
1749
|
+
throw new Error(`Failed to open session file: ${err instanceof Error ? err.message : String(err)}`, {
|
|
1750
|
+
cause: err
|
|
1751
|
+
});
|
|
1701
1752
|
}
|
|
1702
1753
|
try {
|
|
1703
1754
|
return new FileSessionWriter(id, handle, startedAt, meta, { dir: this.dir, filePath: file });
|
|
@@ -1715,7 +1766,8 @@ var DefaultSessionStore = class {
|
|
|
1715
1766
|
handle = await fsp.open(file, "a", 384);
|
|
1716
1767
|
} catch (err) {
|
|
1717
1768
|
throw new Error(
|
|
1718
|
-
`Failed to open session "${id}" for append: ${err instanceof Error ? err.message : String(err)}
|
|
1769
|
+
`Failed to open session "${id}" for append: ${err instanceof Error ? err.message : String(err)}`,
|
|
1770
|
+
{ cause: err }
|
|
1719
1771
|
);
|
|
1720
1772
|
}
|
|
1721
1773
|
const writer = new FileSessionWriter(
|
|
@@ -1738,7 +1790,10 @@ var DefaultSessionStore = class {
|
|
|
1738
1790
|
const events = [];
|
|
1739
1791
|
for (const line of lines) {
|
|
1740
1792
|
try {
|
|
1741
|
-
|
|
1793
|
+
const parsed = JSON.parse(line);
|
|
1794
|
+
if (parsed !== null && typeof parsed === "object" && typeof parsed.type === "string" && typeof parsed.ts === "string") {
|
|
1795
|
+
events.push(parsed);
|
|
1796
|
+
}
|
|
1742
1797
|
} catch {
|
|
1743
1798
|
}
|
|
1744
1799
|
}
|
|
@@ -1755,7 +1810,11 @@ var DefaultSessionStore = class {
|
|
|
1755
1810
|
ids.map((id) => this.summaryFor(id).catch(() => null))
|
|
1756
1811
|
);
|
|
1757
1812
|
const out = sessions.filter((s) => s !== null);
|
|
1758
|
-
out.sort((a, b) =>
|
|
1813
|
+
out.sort((a, b) => {
|
|
1814
|
+
if (a.startedAt < b.startedAt) return 1;
|
|
1815
|
+
if (a.startedAt > b.startedAt) return -1;
|
|
1816
|
+
return a.id.localeCompare(b.id);
|
|
1817
|
+
});
|
|
1759
1818
|
return out.slice(0, limit);
|
|
1760
1819
|
} catch {
|
|
1761
1820
|
return [];
|
|
@@ -1901,6 +1960,8 @@ var FileSessionWriter = class {
|
|
|
1901
1960
|
filePath;
|
|
1902
1961
|
initDone = false;
|
|
1903
1962
|
resumed;
|
|
1963
|
+
appendFailCount = 0;
|
|
1964
|
+
lastAppendWarnAt = 0;
|
|
1904
1965
|
async writeSessionStart() {
|
|
1905
1966
|
if (this.initDone || this.closed) return;
|
|
1906
1967
|
this.initDone = true;
|
|
@@ -1929,7 +1990,19 @@ var FileSessionWriter = class {
|
|
|
1929
1990
|
await this.handle.appendFile(`${JSON.stringify(event)}
|
|
1930
1991
|
`, "utf8");
|
|
1931
1992
|
} catch (err) {
|
|
1932
|
-
|
|
1993
|
+
this.appendFailCount++;
|
|
1994
|
+
const now = Date.now();
|
|
1995
|
+
if (now - this.lastAppendWarnAt > 5e3) {
|
|
1996
|
+
const suppressed = this.appendFailCount - 1;
|
|
1997
|
+
const tail = suppressed > 0 ? ` (+${suppressed} suppressed)` : "";
|
|
1998
|
+
console.warn(
|
|
1999
|
+
"[session] append failed:",
|
|
2000
|
+
err instanceof Error ? err.message : String(err),
|
|
2001
|
+
tail
|
|
2002
|
+
);
|
|
2003
|
+
this.lastAppendWarnAt = now;
|
|
2004
|
+
this.appendFailCount = 0;
|
|
2005
|
+
}
|
|
1933
2006
|
}
|
|
1934
2007
|
}
|
|
1935
2008
|
/**
|
|
@@ -2135,6 +2208,15 @@ function mergeAdjacentText(blocks) {
|
|
|
2135
2208
|
var MAX_BYTES_TOTAL = 32e3;
|
|
2136
2209
|
var DefaultMemoryStore = class {
|
|
2137
2210
|
files;
|
|
2211
|
+
/**
|
|
2212
|
+
* Per-scope serialization queue. `remember` / `forget` / `consolidate` /
|
|
2213
|
+
* `clear` are read-modify-write against a single file; without a lock,
|
|
2214
|
+
* two concurrent calls on the same scope can read the same baseline and
|
|
2215
|
+
* the later write silently drops the earlier entry. We chain each
|
|
2216
|
+
* mutation onto the prior promise for the same scope so they run in
|
|
2217
|
+
* issue order. Different scopes still proceed in parallel.
|
|
2218
|
+
*/
|
|
2219
|
+
writeChain = /* @__PURE__ */ new Map();
|
|
2138
2220
|
constructor(opts) {
|
|
2139
2221
|
this.files = {
|
|
2140
2222
|
"project-agents": opts.paths.inProjectAgentsFile,
|
|
@@ -2142,6 +2224,18 @@ var DefaultMemoryStore = class {
|
|
|
2142
2224
|
"user-memory": opts.paths.globalMemory
|
|
2143
2225
|
};
|
|
2144
2226
|
}
|
|
2227
|
+
async runSerialized(scope, work) {
|
|
2228
|
+
const prior = this.writeChain.get(scope) ?? Promise.resolve();
|
|
2229
|
+
const next = prior.catch(() => void 0).then(work);
|
|
2230
|
+
this.writeChain.set(scope, next);
|
|
2231
|
+
try {
|
|
2232
|
+
return await next;
|
|
2233
|
+
} finally {
|
|
2234
|
+
if (this.writeChain.get(scope) === next) {
|
|
2235
|
+
this.writeChain.delete(scope);
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2145
2239
|
async readAll() {
|
|
2146
2240
|
const parts = [];
|
|
2147
2241
|
for (const scope of ["project-agents", "project-memory", "user-memory"]) {
|
|
@@ -2160,27 +2254,32 @@ ${body.trim()}`);
|
|
|
2160
2254
|
}
|
|
2161
2255
|
}
|
|
2162
2256
|
async remember(text, scope = "project-memory") {
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2257
|
+
return this.runSerialized(scope, async () => {
|
|
2258
|
+
const file = this.files[scope];
|
|
2259
|
+
await ensureDir(path2.dirname(file));
|
|
2260
|
+
let existing = "";
|
|
2261
|
+
try {
|
|
2262
|
+
existing = await fsp.readFile(file, "utf8");
|
|
2263
|
+
} catch {
|
|
2264
|
+
}
|
|
2265
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
2266
|
+
const id = `mem_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
|
2267
|
+
const entry = `
|
|
2173
2268
|
- [${ts}] ${id} ${text.replace(/\n/g, " ")}
|
|
2174
2269
|
`;
|
|
2175
|
-
|
|
2270
|
+
const next = existing.trim() ? existing.replace(/\n+$/, "") + entry : `# WrongStack Memory
|
|
2176
2271
|
${entry}`;
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2272
|
+
await atomicWrite(file, next);
|
|
2273
|
+
const buf = Buffer.byteLength(next, "utf8");
|
|
2274
|
+
if (buf > MAX_BYTES_TOTAL) {
|
|
2275
|
+
await this.consolidateUnsafe(scope);
|
|
2276
|
+
}
|
|
2277
|
+
});
|
|
2182
2278
|
}
|
|
2183
2279
|
async forget(query, scope = "project-memory") {
|
|
2280
|
+
return this.runSerialized(scope, async () => this.forgetUnsafe(query, scope));
|
|
2281
|
+
}
|
|
2282
|
+
async forgetUnsafe(query, scope) {
|
|
2184
2283
|
const file = this.files[scope];
|
|
2185
2284
|
let existing;
|
|
2186
2285
|
try {
|
|
@@ -2217,6 +2316,9 @@ ${entry}`;
|
|
|
2217
2316
|
return removed;
|
|
2218
2317
|
}
|
|
2219
2318
|
async consolidate(scope) {
|
|
2319
|
+
return this.runSerialized(scope, async () => this.consolidateUnsafe(scope));
|
|
2320
|
+
}
|
|
2321
|
+
async consolidateUnsafe(scope) {
|
|
2220
2322
|
const file = this.files[scope];
|
|
2221
2323
|
let existing;
|
|
2222
2324
|
try {
|
|
@@ -2247,12 +2349,14 @@ ${entry}`;
|
|
|
2247
2349
|
}
|
|
2248
2350
|
async clear(scope) {
|
|
2249
2351
|
if (scope) {
|
|
2250
|
-
await atomicWrite(this.files[scope], "");
|
|
2251
|
-
|
|
2252
|
-
for (const s of ["project-agents", "project-memory", "user-memory"]) {
|
|
2253
|
-
await atomicWrite(this.files[s], "");
|
|
2254
|
-
}
|
|
2352
|
+
await this.runSerialized(scope, async () => atomicWrite(this.files[scope], ""));
|
|
2353
|
+
return;
|
|
2255
2354
|
}
|
|
2355
|
+
await Promise.all(
|
|
2356
|
+
["project-agents", "project-memory", "user-memory"].map(
|
|
2357
|
+
(s) => this.runSerialized(s, async () => atomicWrite(this.files[s], ""))
|
|
2358
|
+
)
|
|
2359
|
+
);
|
|
2256
2360
|
}
|
|
2257
2361
|
};
|
|
2258
2362
|
function labelOf(scope) {
|
|
@@ -2300,9 +2404,27 @@ var PATTERNS = [
|
|
|
2300
2404
|
regex: /\b([A-Z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD))\s*[:=]\s*['"]?([A-Za-z0-9_/+=-]{20,})['"]?(?!\s*[A-Za-z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD))/g
|
|
2301
2405
|
}
|
|
2302
2406
|
];
|
|
2407
|
+
var SCRUB_CHUNK_BYTES = 64 * 1024;
|
|
2303
2408
|
var DefaultSecretScrubber = class {
|
|
2304
2409
|
scrub(text) {
|
|
2305
2410
|
if (!text) return text;
|
|
2411
|
+
if (text.length <= SCRUB_CHUNK_BYTES) {
|
|
2412
|
+
return this.scrubOne(text);
|
|
2413
|
+
}
|
|
2414
|
+
const out = [];
|
|
2415
|
+
let i = 0;
|
|
2416
|
+
while (i < text.length) {
|
|
2417
|
+
let end = Math.min(i + SCRUB_CHUNK_BYTES, text.length);
|
|
2418
|
+
if (end < text.length) {
|
|
2419
|
+
const nl = text.lastIndexOf("\n", end);
|
|
2420
|
+
if (nl > i + SCRUB_CHUNK_BYTES / 2) end = nl + 1;
|
|
2421
|
+
}
|
|
2422
|
+
out.push(this.scrubOne(text.slice(i, end)));
|
|
2423
|
+
i = end;
|
|
2424
|
+
}
|
|
2425
|
+
return out.join("");
|
|
2426
|
+
}
|
|
2427
|
+
scrubOne(text) {
|
|
2306
2428
|
let out = text;
|
|
2307
2429
|
for (const p of PATTERNS) {
|
|
2308
2430
|
out = out.replace(p.regex, (_match, group1, group2) => {
|
|
@@ -2402,7 +2524,17 @@ var DefaultSecretVault = class {
|
|
|
2402
2524
|
}
|
|
2403
2525
|
};
|
|
2404
2526
|
function decryptConfigSecrets(cfg, vault) {
|
|
2405
|
-
return walk2(cfg, vault, (v) =>
|
|
2527
|
+
return walk2(cfg, vault, (v, key) => {
|
|
2528
|
+
try {
|
|
2529
|
+
return vault.decrypt(v);
|
|
2530
|
+
} catch (err) {
|
|
2531
|
+
console.warn(
|
|
2532
|
+
`[secret-vault] Failed to decrypt "${key}":`,
|
|
2533
|
+
err instanceof Error ? err.message : err
|
|
2534
|
+
);
|
|
2535
|
+
return "";
|
|
2536
|
+
}
|
|
2537
|
+
});
|
|
2406
2538
|
}
|
|
2407
2539
|
function encryptConfigSecrets(cfg, vault) {
|
|
2408
2540
|
return walk2(cfg, vault, (v) => vault.encrypt(v));
|
|
@@ -2416,7 +2548,7 @@ function walk2(node, vault, transform) {
|
|
|
2416
2548
|
const out = {};
|
|
2417
2549
|
for (const [k, v] of Object.entries(node)) {
|
|
2418
2550
|
if (typeof v === "string" && isSecretField(k)) {
|
|
2419
|
-
out[k] = transform(v);
|
|
2551
|
+
out[k] = transform(v, k);
|
|
2420
2552
|
} else if (typeof v === "object" && v !== null) {
|
|
2421
2553
|
out[k] = walk2(v, vault, transform);
|
|
2422
2554
|
} else {
|
|
@@ -2490,9 +2622,11 @@ function walkCount(node, vault, counter) {
|
|
|
2490
2622
|
}
|
|
2491
2623
|
return out;
|
|
2492
2624
|
}
|
|
2625
|
+
var FORBIDDEN_PROTO_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
2493
2626
|
function deepMerge(a, b) {
|
|
2494
2627
|
const out = { ...a };
|
|
2495
2628
|
for (const [k, v] of Object.entries(b)) {
|
|
2629
|
+
if (FORBIDDEN_PROTO_KEYS.has(k)) continue;
|
|
2496
2630
|
const existing = out[k];
|
|
2497
2631
|
if (v !== null && typeof v === "object" && !Array.isArray(v) && existing !== null && typeof existing === "object" && !Array.isArray(existing)) {
|
|
2498
2632
|
out[k] = deepMerge(existing, v);
|
|
@@ -2527,7 +2661,7 @@ var DefaultPermissionPolicy = class {
|
|
|
2527
2661
|
if (!this.loaded) await this.reload();
|
|
2528
2662
|
const namespaceEntry = this.findNamespaceEntry(tool.name);
|
|
2529
2663
|
const entry = this.policy[tool.name] ?? namespaceEntry;
|
|
2530
|
-
const subject = this.subjectFor(tool.name, input);
|
|
2664
|
+
const subject = this.subjectFor(tool.name, input, tool.subjectKey);
|
|
2531
2665
|
if (entry?.deny && subject && matchAny(entry.deny, subject)) {
|
|
2532
2666
|
return { permission: "deny", source: "deny", reason: "matched deny pattern" };
|
|
2533
2667
|
}
|
|
@@ -2575,16 +2709,23 @@ var DefaultPermissionPolicy = class {
|
|
|
2575
2709
|
throw err;
|
|
2576
2710
|
}
|
|
2577
2711
|
}
|
|
2578
|
-
subjectFor(toolName, input) {
|
|
2712
|
+
subjectFor(toolName, input, subjectKey) {
|
|
2579
2713
|
if (!input || typeof input !== "object") return void 0;
|
|
2580
2714
|
const obj = input;
|
|
2581
2715
|
const globChars = /[*?\[\]]/g;
|
|
2582
2716
|
const escapeGlob = (s) => s.replace(globChars, (c) => `\\${c}`);
|
|
2717
|
+
const normalizePath = (s) => escapeGlob(s.replace(/\\/g, "/"));
|
|
2718
|
+
if (subjectKey) {
|
|
2719
|
+
const v = obj[subjectKey];
|
|
2720
|
+
if (typeof v === "string") {
|
|
2721
|
+
return subjectKey === "path" || subjectKey === "file" || subjectKey === "files" ? normalizePath(v) : escapeGlob(v);
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2583
2724
|
if (toolName === "bash" && typeof obj.command === "string") {
|
|
2584
2725
|
return escapeGlob(obj.command);
|
|
2585
2726
|
}
|
|
2586
2727
|
if (typeof obj.path === "string") {
|
|
2587
|
-
return
|
|
2728
|
+
return normalizePath(obj.path);
|
|
2588
2729
|
}
|
|
2589
2730
|
if (typeof obj.url === "string") {
|
|
2590
2731
|
return escapeGlob(obj.url);
|
|
@@ -2605,14 +2746,15 @@ var DefaultPermissionPolicy = class {
|
|
|
2605
2746
|
};
|
|
2606
2747
|
|
|
2607
2748
|
// src/defaults/retry-policy.ts
|
|
2608
|
-
var DefaultRetryPolicy = class {
|
|
2749
|
+
var DefaultRetryPolicy = class _DefaultRetryPolicy {
|
|
2750
|
+
static NETWORK_ERR_RE = /ECONN|ETIMEDOUT|ETIME|ENOTFOUND|EAI_AGAIN|fetch failed/i;
|
|
2609
2751
|
shouldRetry(err, attempt) {
|
|
2610
2752
|
if (err instanceof ProviderError) {
|
|
2611
2753
|
if (!err.retryable) return false;
|
|
2612
2754
|
return attempt < this.maxAttempts(err);
|
|
2613
2755
|
}
|
|
2614
2756
|
const msg = err.message ?? "";
|
|
2615
|
-
const isNetwork =
|
|
2757
|
+
const isNetwork = _DefaultRetryPolicy.NETWORK_ERR_RE.test(msg);
|
|
2616
2758
|
if (isNetwork) return attempt < 2;
|
|
2617
2759
|
return false;
|
|
2618
2760
|
}
|
|
@@ -2634,55 +2776,43 @@ var DefaultRetryPolicy = class {
|
|
|
2634
2776
|
};
|
|
2635
2777
|
|
|
2636
2778
|
// src/defaults/error-handler.ts
|
|
2779
|
+
var CONTEXT_OVERFLOW_RE = /context|too long|tokens/i;
|
|
2780
|
+
var NETWORK_ERR_RE = /ECONN|ETIMEDOUT|ETIME|ENOTFOUND|EAI_AGAIN|fetch failed/i;
|
|
2637
2781
|
function buildRecoveryStrategies(opts) {
|
|
2638
2782
|
return [
|
|
2639
2783
|
{
|
|
2640
2784
|
label: "context_overflow_reduce",
|
|
2641
2785
|
compactor: opts?.compactor,
|
|
2642
2786
|
async attempt(err, ctx) {
|
|
2643
|
-
if (err instanceof ProviderError
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
stopReason: "end_turn",
|
|
2651
|
-
usage: { input: 0, output: 0 },
|
|
2652
|
-
model: ctx.model
|
|
2653
|
-
};
|
|
2654
|
-
}
|
|
2655
|
-
} catch {
|
|
2787
|
+
if (!(err instanceof ProviderError)) return null;
|
|
2788
|
+
if (err.status !== 413 && !CONTEXT_OVERFLOW_RE.test(err.message)) return null;
|
|
2789
|
+
if (this.compactor) {
|
|
2790
|
+
try {
|
|
2791
|
+
const report = await this.compactor.compact(ctx, { aggressive: true });
|
|
2792
|
+
if (report.after < report.before) {
|
|
2793
|
+
return { action: "retry", reason: "context_compacted" };
|
|
2656
2794
|
}
|
|
2795
|
+
} catch {
|
|
2657
2796
|
}
|
|
2658
|
-
return null;
|
|
2659
2797
|
}
|
|
2660
2798
|
return null;
|
|
2661
2799
|
}
|
|
2662
2800
|
},
|
|
2663
2801
|
{
|
|
2664
2802
|
label: "rate_limit_backoff",
|
|
2665
|
-
async attempt(err
|
|
2666
|
-
if (err instanceof ProviderError
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
content: [{ type: "text", text: "[rate limit backoff applied \u2014 please retry]" }],
|
|
2672
|
-
stopReason: "end_turn",
|
|
2673
|
-
usage: { input: 0, output: 0 },
|
|
2674
|
-
model: ctx.model
|
|
2675
|
-
};
|
|
2676
|
-
}
|
|
2677
|
-
return null;
|
|
2803
|
+
async attempt(err) {
|
|
2804
|
+
if (!(err instanceof ProviderError) || err.status !== 429) return null;
|
|
2805
|
+
const delayMs = err.body?.retryAfterMs ?? 5e3;
|
|
2806
|
+
const delay = Math.max(1e3, Math.min(delayMs, 6e4));
|
|
2807
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
2808
|
+
return { action: "retry", reason: "rate_limit_backoff" };
|
|
2678
2809
|
}
|
|
2679
2810
|
},
|
|
2680
2811
|
{
|
|
2681
2812
|
label: "downgrade_model",
|
|
2682
2813
|
async attempt(err, ctx) {
|
|
2683
|
-
if (err instanceof ProviderError
|
|
2684
|
-
|
|
2685
|
-
}
|
|
2814
|
+
if (!(err instanceof ProviderError)) return null;
|
|
2815
|
+
if (err.status !== 429 && err.status !== 529 && err.status < 500) return null;
|
|
2686
2816
|
return null;
|
|
2687
2817
|
}
|
|
2688
2818
|
}
|
|
@@ -2705,12 +2835,12 @@ var DefaultErrorHandler = class {
|
|
|
2705
2835
|
if (err.status === 429) return { kind: "rate_limit", retryable: true };
|
|
2706
2836
|
if (err.status === 529) return { kind: "overloaded", retryable: true };
|
|
2707
2837
|
if (err.status >= 500) return { kind: "server", retryable: true };
|
|
2708
|
-
if (err.status === 413 ||
|
|
2838
|
+
if (err.status === 413 || CONTEXT_OVERFLOW_RE.test(err.message)) {
|
|
2709
2839
|
return { kind: "context_overflow", retryable: false };
|
|
2710
2840
|
}
|
|
2711
2841
|
if (err.status >= 400) return { kind: "client", retryable: false };
|
|
2712
2842
|
}
|
|
2713
|
-
if (err instanceof Error &&
|
|
2843
|
+
if (err instanceof Error && NETWORK_ERR_RE.test(err.message)) {
|
|
2714
2844
|
return { kind: "network", retryable: true };
|
|
2715
2845
|
}
|
|
2716
2846
|
return { kind: "unknown", retryable: false };
|
|
@@ -2774,13 +2904,28 @@ var DefaultSkillLoader = class {
|
|
|
2774
2904
|
async manifestText() {
|
|
2775
2905
|
const skills = await this.list();
|
|
2776
2906
|
if (skills.length === 0) return "";
|
|
2907
|
+
const entries = await this.listEntries();
|
|
2777
2908
|
const lines = ["## Available skills"];
|
|
2778
|
-
for (const
|
|
2779
|
-
|
|
2780
|
-
lines.push(
|
|
2909
|
+
for (const e of entries) {
|
|
2910
|
+
const scopeTag = e.scope.length > 0 ? ` \u2014 ${e.scope.slice(0, 3).join(", ")}` : "";
|
|
2911
|
+
lines.push(`- **${e.name}**${scopeTag}`);
|
|
2912
|
+
lines.push(` Use when: ${e.trigger}`);
|
|
2781
2913
|
}
|
|
2782
2914
|
return lines.join("\n");
|
|
2783
2915
|
}
|
|
2916
|
+
async listEntries() {
|
|
2917
|
+
const skills = await this.list();
|
|
2918
|
+
const entries = [];
|
|
2919
|
+
for (const s of skills) {
|
|
2920
|
+
try {
|
|
2921
|
+
const raw = await fsp.readFile(s.path, "utf8");
|
|
2922
|
+
const { trigger, scope } = parseDescription(raw);
|
|
2923
|
+
entries.push({ name: s.name, trigger, scope, source: s.source, path: s.path });
|
|
2924
|
+
} catch {
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
return entries;
|
|
2928
|
+
}
|
|
2784
2929
|
async readBody(name) {
|
|
2785
2930
|
const m = await this.find(name);
|
|
2786
2931
|
if (!m) throw new Error(`Skill "${name}" not found`);
|
|
@@ -2823,6 +2968,19 @@ function parseFrontmatter(raw) {
|
|
|
2823
2968
|
flush();
|
|
2824
2969
|
return out;
|
|
2825
2970
|
}
|
|
2971
|
+
function parseDescription(raw) {
|
|
2972
|
+
const fm = parseFrontmatter(raw);
|
|
2973
|
+
const desc = fm.description ?? "";
|
|
2974
|
+
const firstSentenceEnd = desc.indexOf(". ");
|
|
2975
|
+
const trigger = firstSentenceEnd !== -1 ? desc.slice(0, firstSentenceEnd + 1).trim() : desc.trim().split("\n")[0] ?? "";
|
|
2976
|
+
const scope = [];
|
|
2977
|
+
const coversMatch = /(?:covers|for|including)\s+([^.]+)/i.exec(desc);
|
|
2978
|
+
if (coversMatch) {
|
|
2979
|
+
const items = coversMatch[1].replace(/[·•]/g, ",").split(",").map((s) => s.trim()).filter(Boolean);
|
|
2980
|
+
scope.push(...items);
|
|
2981
|
+
}
|
|
2982
|
+
return { trigger, scope };
|
|
2983
|
+
}
|
|
2826
2984
|
var BEHAVIOR_DEFAULTS = {
|
|
2827
2985
|
version: 1,
|
|
2828
2986
|
context: {
|
|
@@ -2871,11 +3029,13 @@ var ENV_MAP = {
|
|
|
2871
3029
|
function isPrimitiveArray(a) {
|
|
2872
3030
|
return a.every((v) => v === null || typeof v !== "object");
|
|
2873
3031
|
}
|
|
3032
|
+
var FORBIDDEN_PROTO_KEYS2 = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
2874
3033
|
function deepMerge2(base, patch) {
|
|
2875
3034
|
if (typeof base !== "object" || base === null) return patch ?? base;
|
|
2876
3035
|
if (typeof patch !== "object" || patch === null) return base;
|
|
2877
3036
|
const out = { ...base };
|
|
2878
3037
|
for (const [k, v] of Object.entries(patch)) {
|
|
3038
|
+
if (FORBIDDEN_PROTO_KEYS2.has(k)) continue;
|
|
2879
3039
|
const existing = out[k];
|
|
2880
3040
|
if (Array.isArray(v)) {
|
|
2881
3041
|
if (Array.isArray(existing) && isPrimitiveArray(v) && isPrimitiveArray(existing)) {
|
|
@@ -2943,8 +3103,12 @@ var DefaultConfigLoader = class {
|
|
|
2943
3103
|
if (cfg.providers) {
|
|
2944
3104
|
for (const pcfg of Object.values(cfg.providers)) {
|
|
2945
3105
|
if (!pcfg || typeof pcfg !== "object") continue;
|
|
2946
|
-
const
|
|
2947
|
-
if (!Array.isArray(
|
|
3106
|
+
const rawKeys = pcfg.apiKeys;
|
|
3107
|
+
if (!Array.isArray(rawKeys) || rawKeys.length === 0) continue;
|
|
3108
|
+
const keys = rawKeys.filter(
|
|
3109
|
+
(k) => !!k && typeof k === "object" && typeof k.label === "string" && typeof k.apiKey === "string"
|
|
3110
|
+
);
|
|
3111
|
+
if (keys.length === 0) continue;
|
|
2948
3112
|
const existing = pcfg.apiKey;
|
|
2949
3113
|
if (existing && existing.length > 0) continue;
|
|
2950
3114
|
const activeLabel = pcfg.activeKey;
|
|
@@ -2955,23 +3119,42 @@ var DefaultConfigLoader = class {
|
|
|
2955
3119
|
}
|
|
2956
3120
|
}
|
|
2957
3121
|
this.validateBehavior(cfg);
|
|
2958
|
-
if (this.strict)
|
|
3122
|
+
if (this.strict) {
|
|
3123
|
+
this.validateIdentity(cfg);
|
|
3124
|
+
}
|
|
2959
3125
|
return Object.freeze(cfg);
|
|
2960
3126
|
}
|
|
2961
3127
|
async readJson(file) {
|
|
3128
|
+
let raw;
|
|
2962
3129
|
try {
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
if (
|
|
2966
|
-
|
|
3130
|
+
raw = await fsp.readFile(file, "utf8");
|
|
3131
|
+
} catch (err) {
|
|
3132
|
+
if (err.code !== "ENOENT") {
|
|
3133
|
+
console.warn(`[config] Failed to read "${file}":`, err);
|
|
3134
|
+
}
|
|
3135
|
+
return {};
|
|
3136
|
+
}
|
|
3137
|
+
const parsed = safeParse(raw);
|
|
3138
|
+
if (!parsed.ok || !parsed.value) {
|
|
3139
|
+
console.warn(
|
|
3140
|
+
`[config] Failed to parse "${file}": invalid JSON. Falling back to defaults for this layer.`
|
|
3141
|
+
);
|
|
3142
|
+
return {};
|
|
2967
3143
|
}
|
|
2968
|
-
return
|
|
3144
|
+
return parsed.value;
|
|
2969
3145
|
}
|
|
2970
3146
|
validateBehavior(cfg) {
|
|
2971
3147
|
if (cfg.version === void 0) throw new Error("Config: missing version field");
|
|
2972
3148
|
if (cfg.version !== 1) throw new Error(`Config: unsupported version ${cfg.version}`);
|
|
2973
3149
|
const c = cfg.context;
|
|
2974
3150
|
if (!c) throw new Error("Config: missing context section");
|
|
3151
|
+
const fields = ["warnThreshold", "softThreshold", "hardThreshold"];
|
|
3152
|
+
for (const f of fields) {
|
|
3153
|
+
const v = c[f];
|
|
3154
|
+
if (typeof v !== "number" || !Number.isFinite(v)) {
|
|
3155
|
+
throw new Error(`Config: context.${String(f)} must be a finite number (got ${typeof v})`);
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
2975
3158
|
if (c.warnThreshold >= c.softThreshold || c.softThreshold >= c.hardThreshold) {
|
|
2976
3159
|
throw new Error("Config: context thresholds must satisfy warn < soft < hard");
|
|
2977
3160
|
}
|
|
@@ -3019,7 +3202,8 @@ var DefaultConfigStore = class {
|
|
|
3019
3202
|
for (const w of this.watchers) {
|
|
3020
3203
|
try {
|
|
3021
3204
|
w(next, prev);
|
|
3022
|
-
} catch {
|
|
3205
|
+
} catch (err) {
|
|
3206
|
+
console.error("[config-store] watcher threw:", err);
|
|
3023
3207
|
}
|
|
3024
3208
|
}
|
|
3025
3209
|
return next;
|
|
@@ -3129,9 +3313,18 @@ var HybridCompactor = class {
|
|
|
3129
3313
|
}
|
|
3130
3314
|
}
|
|
3131
3315
|
let saved = 0;
|
|
3132
|
-
|
|
3316
|
+
let changed = false;
|
|
3317
|
+
const nextMessages = new Array(messages.length);
|
|
3318
|
+
for (let i = 0; i < messages.length; i++) {
|
|
3133
3319
|
const msg = messages[i];
|
|
3134
|
-
if (
|
|
3320
|
+
if (i >= preserveStart) {
|
|
3321
|
+
nextMessages[i] = msg;
|
|
3322
|
+
continue;
|
|
3323
|
+
}
|
|
3324
|
+
if (!msg || !Array.isArray(msg.content)) {
|
|
3325
|
+
nextMessages[i] = msg;
|
|
3326
|
+
continue;
|
|
3327
|
+
}
|
|
3135
3328
|
const newContent = msg.content.map((b) => {
|
|
3136
3329
|
if (b.type !== "tool_result") return b;
|
|
3137
3330
|
const tokens = estimateToolResultTokens(b.content);
|
|
@@ -3145,8 +3338,14 @@ var HybridCompactor = class {
|
|
|
3145
3338
|
};
|
|
3146
3339
|
return elided;
|
|
3147
3340
|
});
|
|
3148
|
-
|
|
3341
|
+
if (newContent.length === msg.content.length && newContent.every((b, idx) => b === msg.content[idx])) {
|
|
3342
|
+
nextMessages[i] = msg;
|
|
3343
|
+
} else {
|
|
3344
|
+
nextMessages[i] = { ...msg, content: newContent };
|
|
3345
|
+
changed = true;
|
|
3346
|
+
}
|
|
3149
3347
|
}
|
|
3348
|
+
if (changed) ctx.state.replaceMessages(nextMessages);
|
|
3150
3349
|
return saved;
|
|
3151
3350
|
}
|
|
3152
3351
|
collapseAncientTurns(ctx) {
|
|
@@ -3180,10 +3379,10 @@ var HybridCompactor = class {
|
|
|
3180
3379
|
let total = 0;
|
|
3181
3380
|
for (const m of messages) {
|
|
3182
3381
|
if (typeof m.content === "string") {
|
|
3183
|
-
total +=
|
|
3382
|
+
total += this.estimator(m.content);
|
|
3184
3383
|
} else {
|
|
3185
3384
|
for (const b of m.content) {
|
|
3186
|
-
if (b.type === "text") total +=
|
|
3385
|
+
if (b.type === "text") total += this.estimator(b.text);
|
|
3187
3386
|
else if (b.type === "tool_use") total += estimateToolInputTokens(b.input);
|
|
3188
3387
|
else if (b.type === "tool_result") total += estimateToolResultTokens(b.content);
|
|
3189
3388
|
}
|
|
@@ -3223,7 +3422,7 @@ var IntelligentCompactor = class {
|
|
|
3223
3422
|
const beforeTokens = this.estimateTokens(ctx.messages);
|
|
3224
3423
|
const reductions = [];
|
|
3225
3424
|
const load = beforeTokens / this.maxContext;
|
|
3226
|
-
const aggressive = opts.aggressive ?? load >= this.softThreshold;
|
|
3425
|
+
const aggressive = load >= this.hardThreshold ? true : opts.aggressive ?? load >= this.softThreshold;
|
|
3227
3426
|
const saved1 = this.eliseOldToolResults(ctx);
|
|
3228
3427
|
if (saved1 > 0) reductions.push({ phase: "elision", saved: saved1 });
|
|
3229
3428
|
if (aggressive) {
|
|
@@ -3332,9 +3531,18 @@ var IntelligentCompactor = class {
|
|
|
3332
3531
|
}
|
|
3333
3532
|
}
|
|
3334
3533
|
let saved = 0;
|
|
3335
|
-
|
|
3534
|
+
let changed = false;
|
|
3535
|
+
const nextMessages = new Array(messages.length);
|
|
3536
|
+
for (let i = 0; i < messages.length; i++) {
|
|
3336
3537
|
const msg = messages[i];
|
|
3337
|
-
if (
|
|
3538
|
+
if (i >= preserveStart) {
|
|
3539
|
+
nextMessages[i] = msg;
|
|
3540
|
+
continue;
|
|
3541
|
+
}
|
|
3542
|
+
if (!msg || !Array.isArray(msg.content)) {
|
|
3543
|
+
nextMessages[i] = msg;
|
|
3544
|
+
continue;
|
|
3545
|
+
}
|
|
3338
3546
|
const newContent = msg.content.map((b) => {
|
|
3339
3547
|
if (b.type !== "tool_result") return b;
|
|
3340
3548
|
const tokens = estimateToolResultTokens(b.content);
|
|
@@ -3347,8 +3555,14 @@ var IntelligentCompactor = class {
|
|
|
3347
3555
|
is_error: b.is_error
|
|
3348
3556
|
};
|
|
3349
3557
|
});
|
|
3350
|
-
|
|
3558
|
+
if (newContent.length === msg.content.length && newContent.every((b, idx) => b === msg.content[idx])) {
|
|
3559
|
+
nextMessages[i] = msg;
|
|
3560
|
+
} else {
|
|
3561
|
+
nextMessages[i] = { ...msg, content: newContent };
|
|
3562
|
+
changed = true;
|
|
3563
|
+
}
|
|
3351
3564
|
}
|
|
3565
|
+
if (changed) ctx.state.replaceMessages(nextMessages);
|
|
3352
3566
|
return saved;
|
|
3353
3567
|
}
|
|
3354
3568
|
hasTextContent(m) {
|
|
@@ -3501,7 +3715,7 @@ IMPORTANT: Total conversation (${totalTokens} tokens) exceeds budget (${effectiv
|
|
|
3501
3715
|
const jsonEnd = raw.lastIndexOf("}");
|
|
3502
3716
|
if (jsonStart === -1 || jsonEnd === -1) {
|
|
3503
3717
|
return this.fallbackSelect(
|
|
3504
|
-
Array.from({ length: messageCount }, (
|
|
3718
|
+
Array.from({ length: messageCount }, () => ({ role: "user", content: "" })),
|
|
3505
3719
|
this.maxContextTokens
|
|
3506
3720
|
);
|
|
3507
3721
|
}
|
|
@@ -3510,7 +3724,7 @@ IMPORTANT: Total conversation (${totalTokens} tokens) exceeds budget (${effectiv
|
|
|
3510
3724
|
parsed = JSON.parse(raw.slice(jsonStart, jsonEnd + 1));
|
|
3511
3725
|
} catch {
|
|
3512
3726
|
return this.fallbackSelect(
|
|
3513
|
-
Array.from({ length: messageCount }, (
|
|
3727
|
+
Array.from({ length: messageCount }, () => ({ role: "user", content: "" })),
|
|
3514
3728
|
this.maxContextTokens
|
|
3515
3729
|
);
|
|
3516
3730
|
}
|
|
@@ -3563,7 +3777,7 @@ var SelectiveCompactor = class {
|
|
|
3563
3777
|
const savedElision = this.eliseOldToolResults(ctx);
|
|
3564
3778
|
if (savedElision > 0) reductions.push({ phase: "elision", saved: savedElision });
|
|
3565
3779
|
const afterPhase1 = this.estimateTokens(ctx.messages);
|
|
3566
|
-
const targetBudget = this.computeTargetBudget(load
|
|
3780
|
+
const targetBudget = this.computeTargetBudget(load);
|
|
3567
3781
|
if (afterPhase1 > targetBudget) {
|
|
3568
3782
|
const savedSelective = await this.runSelector(ctx, targetBudget);
|
|
3569
3783
|
if (savedSelective > 0) reductions.push({ phase: "selective", saved: savedSelective });
|
|
@@ -3581,7 +3795,7 @@ var SelectiveCompactor = class {
|
|
|
3581
3795
|
try {
|
|
3582
3796
|
result = await this.selector.select(ctx.messages, targetBudget);
|
|
3583
3797
|
} catch {
|
|
3584
|
-
return this.aggressiveRecencyTrim(ctx
|
|
3798
|
+
return this.aggressiveRecencyTrim(ctx);
|
|
3585
3799
|
}
|
|
3586
3800
|
await this.executePlan(ctx, result);
|
|
3587
3801
|
const after = this.estimateTokens(ctx.messages);
|
|
@@ -3636,9 +3850,8 @@ Summarize the following message range:`;
|
|
|
3636
3850
|
* Fallback when selector fails: aggressively trim from the oldest end
|
|
3637
3851
|
* until we hit targetBudget.
|
|
3638
3852
|
*/
|
|
3639
|
-
aggressiveRecencyTrim(ctx
|
|
3853
|
+
aggressiveRecencyTrim(ctx) {
|
|
3640
3854
|
const messages = ctx.messages;
|
|
3641
|
-
this.estimateTokens(messages);
|
|
3642
3855
|
const preserveIdx = Math.max(0, messages.length - this.preserveK * 2);
|
|
3643
3856
|
if (preserveIdx <= 0) return 0;
|
|
3644
3857
|
let boundary = preserveIdx;
|
|
@@ -3659,7 +3872,7 @@ Summarize the following message range:`;
|
|
|
3659
3872
|
ctx.state.replaceMessages([summaryMsg, ...tail]);
|
|
3660
3873
|
return Math.max(0, removedTokens - this.estimateTokens([summaryMsg]));
|
|
3661
3874
|
}
|
|
3662
|
-
computeTargetBudget(load
|
|
3875
|
+
computeTargetBudget(load) {
|
|
3663
3876
|
if (load >= this.hardThreshold) {
|
|
3664
3877
|
return Math.floor(this.maxContext * 0.5);
|
|
3665
3878
|
}
|
|
@@ -3681,9 +3894,18 @@ Summarize the following message range:`;
|
|
|
3681
3894
|
}
|
|
3682
3895
|
}
|
|
3683
3896
|
let saved = 0;
|
|
3684
|
-
|
|
3897
|
+
let changed = false;
|
|
3898
|
+
const nextMessages = new Array(messages.length);
|
|
3899
|
+
for (let i = 0; i < messages.length; i++) {
|
|
3685
3900
|
const msg = messages[i];
|
|
3686
|
-
if (
|
|
3901
|
+
if (i >= preserveStart) {
|
|
3902
|
+
nextMessages[i] = msg;
|
|
3903
|
+
continue;
|
|
3904
|
+
}
|
|
3905
|
+
if (!msg || !Array.isArray(msg.content)) {
|
|
3906
|
+
nextMessages[i] = msg;
|
|
3907
|
+
continue;
|
|
3908
|
+
}
|
|
3687
3909
|
const newContent = msg.content.map((b) => {
|
|
3688
3910
|
if (b.type !== "tool_result") return b;
|
|
3689
3911
|
const text = typeof b.content === "string" ? b.content : JSON.stringify(b.content);
|
|
@@ -3697,8 +3919,14 @@ Summarize the following message range:`;
|
|
|
3697
3919
|
is_error: b.is_error
|
|
3698
3920
|
};
|
|
3699
3921
|
});
|
|
3700
|
-
|
|
3922
|
+
if (newContent.every((b, idx) => b === msg.content[idx])) {
|
|
3923
|
+
nextMessages[i] = msg;
|
|
3924
|
+
} else {
|
|
3925
|
+
nextMessages[i] = { ...msg, content: newContent };
|
|
3926
|
+
changed = true;
|
|
3927
|
+
}
|
|
3701
3928
|
}
|
|
3929
|
+
if (changed) ctx.state.replaceMessages(nextMessages);
|
|
3702
3930
|
return saved;
|
|
3703
3931
|
}
|
|
3704
3932
|
hasTextContent(m) {
|
|
@@ -3734,46 +3962,78 @@ var AutoCompactionMiddleware = class {
|
|
|
3734
3962
|
name = "AutoCompaction";
|
|
3735
3963
|
compactor;
|
|
3736
3964
|
warnThreshold;
|
|
3737
|
-
// fraction of maxContext (0-1)
|
|
3738
3965
|
softThreshold;
|
|
3739
3966
|
hardThreshold;
|
|
3740
3967
|
maxContext;
|
|
3741
3968
|
estimator;
|
|
3742
3969
|
aggressiveOn;
|
|
3970
|
+
events;
|
|
3971
|
+
failureMode;
|
|
3743
3972
|
/**
|
|
3744
|
-
* @param compactor Compactor to use for compaction
|
|
3745
|
-
* @param maxContext Provider's max context window in tokens
|
|
3746
|
-
* @param estimator Token estimation function
|
|
3747
|
-
* @param thresholds
|
|
3748
|
-
* @param
|
|
3973
|
+
* @param compactor Compactor to use for compaction.
|
|
3974
|
+
* @param maxContext Provider's max context window in tokens.
|
|
3975
|
+
* @param estimator Token estimation function.
|
|
3976
|
+
* @param thresholds Threshold fractions (0-1) of maxContext.
|
|
3977
|
+
* @param opts Optional behavior. By default, failures at the
|
|
3978
|
+
* hard threshold throw AGENT_CONTEXT_OVERFLOW so
|
|
3979
|
+
* the agent does not continue into a likely
|
|
3980
|
+
* provider context overflow. Warn/soft failures
|
|
3981
|
+
* still emit compaction.failed and continue.
|
|
3749
3982
|
*/
|
|
3750
|
-
constructor(compactor, maxContext, estimator, thresholds,
|
|
3983
|
+
constructor(compactor, maxContext, estimator, thresholds, optsOrAggressiveOn = {}, events) {
|
|
3984
|
+
const opts = typeof optsOrAggressiveOn === "string" ? { aggressiveOn: optsOrAggressiveOn, events } : optsOrAggressiveOn;
|
|
3751
3985
|
this.compactor = compactor;
|
|
3752
3986
|
this.maxContext = maxContext;
|
|
3753
3987
|
this.estimator = estimator;
|
|
3754
3988
|
this.warnThreshold = thresholds.warn;
|
|
3755
3989
|
this.softThreshold = thresholds.soft;
|
|
3756
3990
|
this.hardThreshold = thresholds.hard;
|
|
3757
|
-
this.aggressiveOn = aggressiveOn;
|
|
3991
|
+
this.aggressiveOn = opts.aggressiveOn ?? "soft";
|
|
3992
|
+
this.events = opts.events;
|
|
3993
|
+
this.failureMode = opts.failureMode ?? "throw_on_hard";
|
|
3758
3994
|
}
|
|
3759
3995
|
handler() {
|
|
3760
3996
|
return async (ctx, next) => {
|
|
3761
3997
|
const tokens = this.estimator(ctx);
|
|
3762
3998
|
const load = tokens / this.maxContext;
|
|
3763
3999
|
if (load >= this.hardThreshold) {
|
|
3764
|
-
await this.compact(ctx, true);
|
|
4000
|
+
await this.compact(ctx, true, { level: "hard", tokens, load });
|
|
3765
4001
|
} else if (load >= this.softThreshold) {
|
|
3766
|
-
await this.compact(ctx, this.aggressiveOn !== "hard");
|
|
4002
|
+
await this.compact(ctx, this.aggressiveOn !== "hard", { level: "soft", tokens, load });
|
|
3767
4003
|
} else if (load >= this.warnThreshold) {
|
|
3768
|
-
await this.compact(ctx, false);
|
|
4004
|
+
await this.compact(ctx, false, { level: "warn", tokens, load });
|
|
3769
4005
|
}
|
|
3770
4006
|
return next(ctx);
|
|
3771
4007
|
};
|
|
3772
4008
|
}
|
|
3773
|
-
async compact(ctx, aggressive) {
|
|
4009
|
+
async compact(ctx, aggressive, pressure) {
|
|
3774
4010
|
try {
|
|
3775
4011
|
await this.compactor.compact(ctx, { aggressive });
|
|
3776
|
-
} catch {
|
|
4012
|
+
} catch (err) {
|
|
4013
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
4014
|
+
const fatal = this.failureMode === "throw" || this.failureMode === "throw_on_hard" && pressure.level === "hard";
|
|
4015
|
+
this.events?.emit("compaction.failed", {
|
|
4016
|
+
err: error,
|
|
4017
|
+
aggressive,
|
|
4018
|
+
level: pressure.level,
|
|
4019
|
+
tokens: pressure.tokens,
|
|
4020
|
+
maxContext: this.maxContext,
|
|
4021
|
+
load: pressure.load,
|
|
4022
|
+
fatal
|
|
4023
|
+
});
|
|
4024
|
+
if (fatal) {
|
|
4025
|
+
throw new AgentError({
|
|
4026
|
+
message: `Auto-compaction failed at ${pressure.level} threshold`,
|
|
4027
|
+
code: "AGENT_CONTEXT_OVERFLOW",
|
|
4028
|
+
recoverable: true,
|
|
4029
|
+
context: {
|
|
4030
|
+
level: pressure.level,
|
|
4031
|
+
tokens: pressure.tokens,
|
|
4032
|
+
maxContext: this.maxContext
|
|
4033
|
+
},
|
|
4034
|
+
cause: err
|
|
4035
|
+
});
|
|
4036
|
+
}
|
|
3777
4037
|
}
|
|
3778
4038
|
}
|
|
3779
4039
|
};
|
|
@@ -4150,8 +4410,9 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
|
|
|
4150
4410
|
const context = {
|
|
4151
4411
|
subagentId: id,
|
|
4152
4412
|
tasks: [],
|
|
4153
|
-
//
|
|
4154
|
-
// bidirectional bridge is created.
|
|
4413
|
+
// Wired later by the caller via setSubagentBridge() once the
|
|
4414
|
+
// bidirectional bridge is created. Readers must null-check / use
|
|
4415
|
+
// hasParentBridge() — the type now reflects this.
|
|
4155
4416
|
parentBridge: null,
|
|
4156
4417
|
doneCondition: this.config.doneCondition,
|
|
4157
4418
|
maxConcurrent: this.config.maxConcurrent ?? 4
|
|
@@ -4196,9 +4457,7 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
|
|
|
4196
4457
|
this.emit("subagent.stopped", { subagentId, reason: "stopped by coordinator" });
|
|
4197
4458
|
}
|
|
4198
4459
|
async stopAll() {
|
|
4199
|
-
|
|
4200
|
-
await this.stop(id);
|
|
4201
|
-
}
|
|
4460
|
+
await Promise.allSettled([...this.subagents.keys()].map((id) => this.stop(id)));
|
|
4202
4461
|
}
|
|
4203
4462
|
getStatus() {
|
|
4204
4463
|
return {
|
|
@@ -4234,7 +4493,17 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
|
|
|
4234
4493
|
if (!subagentId) return;
|
|
4235
4494
|
const task = this.pendingTasks.shift();
|
|
4236
4495
|
if (!task) return;
|
|
4237
|
-
|
|
4496
|
+
this.runDispatched(subagentId, task).catch((err) => {
|
|
4497
|
+
this.recordCompletion({
|
|
4498
|
+
subagentId,
|
|
4499
|
+
taskId: task.id,
|
|
4500
|
+
status: "failed",
|
|
4501
|
+
error: err instanceof Error ? err.message : String(err),
|
|
4502
|
+
iterations: 0,
|
|
4503
|
+
toolCalls: 0,
|
|
4504
|
+
durationMs: 0
|
|
4505
|
+
});
|
|
4506
|
+
});
|
|
4238
4507
|
}
|
|
4239
4508
|
}
|
|
4240
4509
|
canDispatch() {
|
|
@@ -4254,7 +4523,6 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
|
|
|
4254
4523
|
subagent.currentTask = task.id;
|
|
4255
4524
|
task.subagentId = subagentId;
|
|
4256
4525
|
subagent.context.tasks.push(task);
|
|
4257
|
-
this.inFlight++;
|
|
4258
4526
|
this.emit("task.assigned", { task, subagentId });
|
|
4259
4527
|
const budget = new SubagentBudget({
|
|
4260
4528
|
maxIterations: subagent.config.maxIterations ?? this.config.defaultBudget?.maxIterations,
|
|
@@ -4264,6 +4532,10 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
|
|
|
4264
4532
|
timeoutMs: task.timeoutMs ?? subagent.config.timeoutMs ?? this.config.defaultBudget?.timeoutMs
|
|
4265
4533
|
});
|
|
4266
4534
|
subagent.activeBudget = budget;
|
|
4535
|
+
if (!this.runner) {
|
|
4536
|
+
return;
|
|
4537
|
+
}
|
|
4538
|
+
this.inFlight++;
|
|
4267
4539
|
const startTime = Date.now();
|
|
4268
4540
|
const runCtx = {
|
|
4269
4541
|
subagentId,
|
|
@@ -4273,9 +4545,6 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
|
|
|
4273
4545
|
bridge: subagent.context.parentBridge || null
|
|
4274
4546
|
};
|
|
4275
4547
|
let result;
|
|
4276
|
-
if (!this.runner) {
|
|
4277
|
-
return;
|
|
4278
|
-
}
|
|
4279
4548
|
budget.start();
|
|
4280
4549
|
try {
|
|
4281
4550
|
const outcome = await this.executeWithTimeout(this.runner, task, runCtx, budget);
|
|
@@ -4290,13 +4559,14 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
|
|
|
4290
4559
|
};
|
|
4291
4560
|
} catch (err) {
|
|
4292
4561
|
const status = err instanceof BudgetExceededError && err.kind === "timeout" ? "timeout" : subagent.abortController.signal.aborted ? "stopped" : "failed";
|
|
4562
|
+
const usage = budget.usage();
|
|
4293
4563
|
result = {
|
|
4294
4564
|
subagentId,
|
|
4295
4565
|
taskId: task.id,
|
|
4296
4566
|
status,
|
|
4297
4567
|
error: err instanceof Error ? err.message : String(err),
|
|
4298
|
-
iterations:
|
|
4299
|
-
toolCalls:
|
|
4568
|
+
iterations: usage.iterations,
|
|
4569
|
+
toolCalls: usage.toolCalls,
|
|
4300
4570
|
durationMs: Date.now() - startTime
|
|
4301
4571
|
};
|
|
4302
4572
|
}
|
|
@@ -4321,11 +4591,24 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
|
|
|
4321
4591
|
recordCompletion(result) {
|
|
4322
4592
|
this.completedResults.push(result);
|
|
4323
4593
|
this.totalIterations += result.iterations;
|
|
4324
|
-
|
|
4594
|
+
if (this.inFlight > 0) {
|
|
4595
|
+
this.inFlight--;
|
|
4596
|
+
} else if (this.runner) {
|
|
4597
|
+
this.emit("warning", {
|
|
4598
|
+
type: "inFlight_underflow",
|
|
4599
|
+
taskId: result.taskId,
|
|
4600
|
+
subagentId: result.subagentId
|
|
4601
|
+
});
|
|
4602
|
+
return;
|
|
4603
|
+
}
|
|
4325
4604
|
const subagent = this.subagents.get(result.subagentId);
|
|
4326
4605
|
if (subagent && subagent.status !== "stopped") {
|
|
4327
|
-
|
|
4606
|
+
const failed = result.status === "failed" || result.status === "timeout";
|
|
4607
|
+
subagent.status = failed ? "error" : "idle";
|
|
4328
4608
|
subagent.currentTask = void 0;
|
|
4609
|
+
if (subagent.abortController.signal.aborted) {
|
|
4610
|
+
subagent.abortController = new AbortController();
|
|
4611
|
+
}
|
|
4329
4612
|
if (subagent.status === "error") {
|
|
4330
4613
|
queueMicrotask(() => {
|
|
4331
4614
|
if (subagent.status === "error") subagent.status = "idle";
|
|
@@ -4513,6 +4796,11 @@ var InMemoryAgentBridge = class {
|
|
|
4513
4796
|
if (this.stopped) throw new Error("Bridge is stopped");
|
|
4514
4797
|
const timeout = timeoutMs ?? this.timeoutMs;
|
|
4515
4798
|
const correlationId = msg.id;
|
|
4799
|
+
if (this.pendingRequests.has(correlationId)) {
|
|
4800
|
+
throw new Error(
|
|
4801
|
+
`Bridge request id "${correlationId}" collides with an in-flight request \u2014 caller is reusing message ids`
|
|
4802
|
+
);
|
|
4803
|
+
}
|
|
4516
4804
|
return new Promise((resolve4, reject) => {
|
|
4517
4805
|
const timer = setTimeout(() => {
|
|
4518
4806
|
this.pendingRequests.delete(correlationId);
|
|
@@ -4553,8 +4841,10 @@ function createMessage(type, from, payload, to) {
|
|
|
4553
4841
|
var DoneConditionChecker = class {
|
|
4554
4842
|
constructor(condition) {
|
|
4555
4843
|
this.condition = condition;
|
|
4844
|
+
this.compiledRegex = condition.type === "output_match" && condition.pattern ? new RegExp(condition.pattern) : null;
|
|
4556
4845
|
}
|
|
4557
4846
|
condition;
|
|
4847
|
+
compiledRegex;
|
|
4558
4848
|
check(state) {
|
|
4559
4849
|
switch (this.condition.type) {
|
|
4560
4850
|
case "iterations":
|
|
@@ -4568,11 +4858,8 @@ var DoneConditionChecker = class {
|
|
|
4568
4858
|
}
|
|
4569
4859
|
break;
|
|
4570
4860
|
case "output_match":
|
|
4571
|
-
if (this.
|
|
4572
|
-
|
|
4573
|
-
if (regex.test(state.lastOutput)) {
|
|
4574
|
-
return { done: true, reason: `output matched pattern "${this.condition.pattern}"`, ...state };
|
|
4575
|
-
}
|
|
4861
|
+
if (this.compiledRegex && state.lastOutput && this.compiledRegex.test(state.lastOutput)) {
|
|
4862
|
+
return { done: true, reason: `output matched pattern "${this.condition.pattern}"`, ...state };
|
|
4576
4863
|
}
|
|
4577
4864
|
break;
|
|
4578
4865
|
}
|
|
@@ -4629,7 +4916,8 @@ var AutonomousRunner = class {
|
|
|
4629
4916
|
return failedResult;
|
|
4630
4917
|
}
|
|
4631
4918
|
} catch (e) {
|
|
4632
|
-
|
|
4919
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
4920
|
+
if (msg.includes("timeout")) {
|
|
4633
4921
|
const timeoutResult = {
|
|
4634
4922
|
status: "failed",
|
|
4635
4923
|
error: toWrongStackError(e),
|
|
@@ -4658,14 +4946,11 @@ var AutonomousRunner = class {
|
|
|
4658
4946
|
|
|
4659
4947
|
// src/defaults/spec-parser.ts
|
|
4660
4948
|
var SpecParser = class {
|
|
4661
|
-
constructor(opts = {}) {
|
|
4662
|
-
this.opts = opts;
|
|
4663
|
-
}
|
|
4664
|
-
opts;
|
|
4665
4949
|
parse(content) {
|
|
4666
4950
|
const lines = content.split("\n");
|
|
4667
4951
|
const sections = this.extractSections(lines);
|
|
4668
4952
|
const requirements = this.extractRequirements(lines);
|
|
4953
|
+
const now = Date.now();
|
|
4669
4954
|
return {
|
|
4670
4955
|
id: crypto.randomUUID(),
|
|
4671
4956
|
title: this.extractTitle(lines),
|
|
@@ -4674,8 +4959,8 @@ var SpecParser = class {
|
|
|
4674
4959
|
overview: this.extractOverview(lines),
|
|
4675
4960
|
sections,
|
|
4676
4961
|
requirements,
|
|
4677
|
-
createdAt:
|
|
4678
|
-
updatedAt:
|
|
4962
|
+
createdAt: now,
|
|
4963
|
+
updatedAt: now
|
|
4679
4964
|
};
|
|
4680
4965
|
}
|
|
4681
4966
|
extractTitle(lines) {
|
|
@@ -4767,20 +5052,13 @@ var SpecParser = class {
|
|
|
4767
5052
|
parseRequirementLine(line, id) {
|
|
4768
5053
|
const trimmed = line.trim();
|
|
4769
5054
|
if (!trimmed || trimmed.startsWith("#")) return null;
|
|
4770
|
-
const
|
|
4771
|
-
|
|
4772
|
-
"non-functional": "non-functional",
|
|
4773
|
-
"security": "security",
|
|
4774
|
-
"performance": "performance",
|
|
4775
|
-
"ux": "ux"
|
|
4776
|
-
};
|
|
5055
|
+
const lower = trimmed.toLowerCase();
|
|
5056
|
+
const types = ["functional", "non-functional", "security", "performance", "ux"];
|
|
4777
5057
|
let type = "functional";
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
if (trimmed.toLowerCase().includes(`[${key}]`)) {
|
|
4781
|
-
type = val;
|
|
4782
|
-
}
|
|
5058
|
+
for (const t2 of types) {
|
|
5059
|
+
if (lower.includes(`[${t2}]`)) type = t2;
|
|
4783
5060
|
}
|
|
5061
|
+
let priority = "medium";
|
|
4784
5062
|
if (trimmed.includes("[critical]") || trimmed.includes("[prio:high]")) {
|
|
4785
5063
|
priority = "critical";
|
|
4786
5064
|
} else if (trimmed.includes("[high]")) {
|
|
@@ -4870,9 +5148,10 @@ var SpecParser = class {
|
|
|
4870
5148
|
warnings.push({ path: `requirement.${req.id}`, message: "No acceptance criteria defined" });
|
|
4871
5149
|
}
|
|
4872
5150
|
}
|
|
5151
|
+
const reqIds = new Set(spec.requirements.map((r) => r.id));
|
|
4873
5152
|
const blockedByIds = new Set(spec.requirements.flatMap((r) => r.blockedBy ?? []));
|
|
4874
5153
|
for (const id of blockedByIds) {
|
|
4875
|
-
if (!
|
|
5154
|
+
if (!reqIds.has(id)) {
|
|
4876
5155
|
errors.push({ path: "requirements", message: `BlockedBy references non-existent requirement: ${id}` });
|
|
4877
5156
|
}
|
|
4878
5157
|
}
|
|
@@ -4902,25 +5181,21 @@ var TaskGenerator = class {
|
|
|
4902
5181
|
status: "pending"
|
|
4903
5182
|
});
|
|
4904
5183
|
}
|
|
4905
|
-
const
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
const task = this.createTaskFromRequirement(req, spec.title);
|
|
4915
|
-
this.opts.taskTracker.addNode(task);
|
|
4916
|
-
}
|
|
4917
|
-
for (const req of mediumReqs) {
|
|
4918
|
-
const task = this.createTaskFromRequirement(req, spec.title);
|
|
4919
|
-
this.opts.taskTracker.addNode(task);
|
|
5184
|
+
const byPriority = {
|
|
5185
|
+
critical: [],
|
|
5186
|
+
high: [],
|
|
5187
|
+
medium: [],
|
|
5188
|
+
low: []
|
|
5189
|
+
};
|
|
5190
|
+
for (const req of spec.requirements) {
|
|
5191
|
+
const bucket = byPriority[req.priority] ?? byPriority.medium;
|
|
5192
|
+
bucket.push(req);
|
|
4920
5193
|
}
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
5194
|
+
const order = ["critical", "high", "medium", "low"];
|
|
5195
|
+
for (const p of order) {
|
|
5196
|
+
for (const req of byPriority[p]) {
|
|
5197
|
+
this.opts.taskTracker.addNode(this.createTaskFromRequirement(req));
|
|
5198
|
+
}
|
|
4924
5199
|
}
|
|
4925
5200
|
if (spec.apiEndpoints && spec.apiEndpoints.length > 0) {
|
|
4926
5201
|
const apiParent = this.opts.taskTracker.addNode({
|
|
@@ -4954,17 +5229,15 @@ var TaskGenerator = class {
|
|
|
4954
5229
|
});
|
|
4955
5230
|
return graph;
|
|
4956
5231
|
}
|
|
4957
|
-
createTaskFromRequirement(req
|
|
4958
|
-
const type = this.mapRequirementType(req.type);
|
|
4959
|
-
const tags = [req.type, req.priority];
|
|
5232
|
+
createTaskFromRequirement(req) {
|
|
4960
5233
|
return {
|
|
4961
5234
|
title: req.description,
|
|
4962
|
-
description: this.buildDescription(req
|
|
4963
|
-
type,
|
|
4964
|
-
priority:
|
|
5235
|
+
description: this.buildDescription(req),
|
|
5236
|
+
type: this.mapRequirementType(req.type),
|
|
5237
|
+
priority: req.priority,
|
|
4965
5238
|
status: "pending",
|
|
4966
5239
|
specRequirementId: req.id,
|
|
4967
|
-
tags,
|
|
5240
|
+
tags: [req.type, req.priority],
|
|
4968
5241
|
estimateHours: this.estimateHours(req)
|
|
4969
5242
|
};
|
|
4970
5243
|
}
|
|
@@ -4979,7 +5252,7 @@ var TaskGenerator = class {
|
|
|
4979
5252
|
estimateHours: this.estimateForEndpoint(endpoint)
|
|
4980
5253
|
};
|
|
4981
5254
|
}
|
|
4982
|
-
buildDescription(req
|
|
5255
|
+
buildDescription(req) {
|
|
4983
5256
|
const lines = [
|
|
4984
5257
|
req.description,
|
|
4985
5258
|
"",
|
|
@@ -5013,20 +5286,6 @@ var TaskGenerator = class {
|
|
|
5013
5286
|
return "feature";
|
|
5014
5287
|
}
|
|
5015
5288
|
}
|
|
5016
|
-
mapPriority(priority) {
|
|
5017
|
-
switch (priority) {
|
|
5018
|
-
case "critical":
|
|
5019
|
-
return "critical";
|
|
5020
|
-
case "high":
|
|
5021
|
-
return "high";
|
|
5022
|
-
case "medium":
|
|
5023
|
-
return "medium";
|
|
5024
|
-
case "low":
|
|
5025
|
-
return "low";
|
|
5026
|
-
default:
|
|
5027
|
-
return "medium";
|
|
5028
|
-
}
|
|
5029
|
-
}
|
|
5030
5289
|
estimateHours(req) {
|
|
5031
5290
|
switch (req.priority) {
|
|
5032
5291
|
case "critical":
|
|
@@ -5136,40 +5395,40 @@ var TaskTracker = class {
|
|
|
5136
5395
|
this.graph.rootNodes.push(newNode.id);
|
|
5137
5396
|
}
|
|
5138
5397
|
this.graph.updatedAt = now;
|
|
5139
|
-
this.
|
|
5398
|
+
this.persist();
|
|
5140
5399
|
return newNode;
|
|
5141
5400
|
}
|
|
5142
5401
|
addEdge(from, to, type = "depends_on") {
|
|
5143
5402
|
if (!this.graph) throw new Error("No graph loaded");
|
|
5144
|
-
|
|
5403
|
+
this.graph.edges.push({
|
|
5145
5404
|
id: crypto.randomUUID(),
|
|
5146
5405
|
from,
|
|
5147
5406
|
to,
|
|
5148
5407
|
type
|
|
5149
|
-
};
|
|
5150
|
-
this.graph.edges.push(edge);
|
|
5408
|
+
});
|
|
5151
5409
|
this.graph.updatedAt = Date.now();
|
|
5152
|
-
this.
|
|
5410
|
+
this.persist();
|
|
5153
5411
|
}
|
|
5154
5412
|
updateNodeStatus(id, status, reason) {
|
|
5155
5413
|
if (!this.graph) throw new Error("No graph loaded");
|
|
5156
5414
|
const node = this.graph.nodes.get(id);
|
|
5157
5415
|
if (!node) throw new Error(`Node ${id} not found`);
|
|
5158
5416
|
const from = node.status;
|
|
5417
|
+
const now = Date.now();
|
|
5159
5418
|
node.status = status;
|
|
5160
|
-
node.updatedAt =
|
|
5419
|
+
node.updatedAt = now;
|
|
5161
5420
|
if (status === "completed") {
|
|
5162
|
-
node.completedAt =
|
|
5421
|
+
node.completedAt = now;
|
|
5163
5422
|
}
|
|
5164
|
-
this.transitions.push({ from, to: status, timestamp:
|
|
5423
|
+
this.transitions.push({ from, to: status, timestamp: now, reason });
|
|
5165
5424
|
if (status === "completed") {
|
|
5166
5425
|
this.unblockDependents(id);
|
|
5167
5426
|
}
|
|
5168
5427
|
if (status === "in_progress") {
|
|
5169
5428
|
this.checkAndBlockIfNeeded(id);
|
|
5170
5429
|
}
|
|
5171
|
-
this.graph.updatedAt =
|
|
5172
|
-
this.
|
|
5430
|
+
this.graph.updatedAt = now;
|
|
5431
|
+
this.persist();
|
|
5173
5432
|
}
|
|
5174
5433
|
getNode(id) {
|
|
5175
5434
|
return this.graph?.nodes.get(id);
|
|
@@ -5190,9 +5449,7 @@ var TaskTracker = class {
|
|
|
5190
5449
|
}
|
|
5191
5450
|
if (sort) {
|
|
5192
5451
|
nodes.sort((a, b) => {
|
|
5193
|
-
const
|
|
5194
|
-
const bVal = b[sort.field] ?? "";
|
|
5195
|
-
const cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
|
|
5452
|
+
const cmp = compareByField(a, b, sort.field);
|
|
5196
5453
|
return sort.direction === "asc" ? cmp : -cmp;
|
|
5197
5454
|
});
|
|
5198
5455
|
}
|
|
@@ -5271,7 +5528,47 @@ var TaskTracker = class {
|
|
|
5271
5528
|
}
|
|
5272
5529
|
}
|
|
5273
5530
|
}
|
|
5531
|
+
/**
|
|
5532
|
+
* Fire-and-forget persistence with attached error handler.
|
|
5533
|
+
* Synchronous mutators (addNode/addEdge/updateNodeStatus) use this to
|
|
5534
|
+
* avoid forcing an async cascade through every caller; if the store
|
|
5535
|
+
* rejects, the configured `onPersistError` is invoked so failures are
|
|
5536
|
+
* surfaced instead of swallowed by an unhandled promise rejection.
|
|
5537
|
+
*/
|
|
5538
|
+
persist() {
|
|
5539
|
+
if (!this.graph) return;
|
|
5540
|
+
this.opts.store.saveGraph(this.graph).catch((err) => {
|
|
5541
|
+
if (this.opts.onPersistError) this.opts.onPersistError(err);
|
|
5542
|
+
else console.warn("[task-tracker] saveGraph failed:", err instanceof Error ? err.message : String(err));
|
|
5543
|
+
});
|
|
5544
|
+
}
|
|
5545
|
+
};
|
|
5546
|
+
var PRIORITY_RANK = {
|
|
5547
|
+
critical: 0,
|
|
5548
|
+
high: 1,
|
|
5549
|
+
medium: 2,
|
|
5550
|
+
low: 3
|
|
5274
5551
|
};
|
|
5552
|
+
var STATUS_RANK = {
|
|
5553
|
+
in_progress: 0,
|
|
5554
|
+
pending: 1,
|
|
5555
|
+
review: 2,
|
|
5556
|
+
blocked: 3,
|
|
5557
|
+
failed: 4,
|
|
5558
|
+
completed: 5
|
|
5559
|
+
};
|
|
5560
|
+
function compareByField(a, b, field) {
|
|
5561
|
+
switch (field) {
|
|
5562
|
+
case "priority":
|
|
5563
|
+
return PRIORITY_RANK[a.priority] - PRIORITY_RANK[b.priority];
|
|
5564
|
+
case "status":
|
|
5565
|
+
return STATUS_RANK[a.status] - STATUS_RANK[b.status];
|
|
5566
|
+
case "createdAt":
|
|
5567
|
+
return a.createdAt - b.createdAt;
|
|
5568
|
+
case "updatedAt":
|
|
5569
|
+
return a.updatedAt - b.updatedAt;
|
|
5570
|
+
}
|
|
5571
|
+
}
|
|
5275
5572
|
|
|
5276
5573
|
// src/defaults/task-flow.ts
|
|
5277
5574
|
var TaskFlow = class {
|
|
@@ -5323,9 +5620,10 @@ var TaskFlow = class {
|
|
|
5323
5620
|
const task = batch[i];
|
|
5324
5621
|
if (!result || !task) continue;
|
|
5325
5622
|
if (result.status === "rejected") {
|
|
5326
|
-
|
|
5327
|
-
this.
|
|
5328
|
-
|
|
5623
|
+
const reason = result.reason;
|
|
5624
|
+
this.opts.tracker.updateNodeStatus(task.id, "failed", reason?.message);
|
|
5625
|
+
this.emit("task.failed", { taskId: task.id, error: reason?.message ?? "unknown" });
|
|
5626
|
+
ctx.onTaskFail?.(task, reason);
|
|
5329
5627
|
} else {
|
|
5330
5628
|
this.opts.tracker.updateNodeStatus(task.id, "completed");
|
|
5331
5629
|
this.emit("task.completed", { taskId: task.id, result: result.value });
|
|
@@ -5613,7 +5911,7 @@ var ToolExecutor = class {
|
|
|
5613
5911
|
return { result, tool, durationMs: Date.now() - start };
|
|
5614
5912
|
}
|
|
5615
5913
|
} else {
|
|
5616
|
-
const suggestedPattern = this.subjectFor(tool.name, use.input) ?? tool.name;
|
|
5914
|
+
const suggestedPattern = this.subjectFor(tool.name, use.input, tool.subjectKey) ?? tool.name;
|
|
5617
5915
|
const pending = { type: "tool_confirm_pending", toolUseId: use.id, toolName: tool.name, input: use.input, suggestedPattern };
|
|
5618
5916
|
return { result: pending, tool, durationMs: Date.now() - start };
|
|
5619
5917
|
}
|
|
@@ -5645,15 +5943,31 @@ var ToolExecutor = class {
|
|
|
5645
5943
|
span?.end();
|
|
5646
5944
|
}
|
|
5647
5945
|
};
|
|
5946
|
+
const safeRun = async (use) => {
|
|
5947
|
+
try {
|
|
5948
|
+
return await runOne(use);
|
|
5949
|
+
} catch (err) {
|
|
5950
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5951
|
+
const scrubbed = this.opts.secretScrubber.scrub(msg);
|
|
5952
|
+
const result = {
|
|
5953
|
+
type: "tool_result",
|
|
5954
|
+
tool_use_id: use.id,
|
|
5955
|
+
content: `Tool "${use.name}" execution failed: ${scrubbed}`,
|
|
5956
|
+
is_error: true
|
|
5957
|
+
};
|
|
5958
|
+
budget = this.decrementBudget(result, budget);
|
|
5959
|
+
return { result, tool: this.registry.get(use.name), durationMs: 0 };
|
|
5960
|
+
}
|
|
5961
|
+
};
|
|
5648
5962
|
if (strategy === "sequential") {
|
|
5649
5963
|
const outputs = [];
|
|
5650
5964
|
for (const use of toolUses) {
|
|
5651
|
-
if (use) outputs.push(await
|
|
5965
|
+
if (use) outputs.push(await safeRun(use));
|
|
5652
5966
|
}
|
|
5653
5967
|
return { outputs, remainingBudget: budget };
|
|
5654
5968
|
}
|
|
5655
5969
|
if (strategy === "parallel") {
|
|
5656
|
-
const outputs = await Promise.all(toolUses.map((use) =>
|
|
5970
|
+
const outputs = await Promise.all(toolUses.map((use) => safeRun(use)));
|
|
5657
5971
|
return { outputs, remainingBudget: budget };
|
|
5658
5972
|
}
|
|
5659
5973
|
const nonMutating = [];
|
|
@@ -5664,10 +5978,10 @@ var ToolExecutor = class {
|
|
|
5664
5978
|
if (tool?.mutating) mutating.push(use);
|
|
5665
5979
|
else nonMutating.push(use);
|
|
5666
5980
|
}
|
|
5667
|
-
const firstPass = await Promise.all(nonMutating.map((use) =>
|
|
5981
|
+
const firstPass = await Promise.all(nonMutating.map((use) => safeRun(use)));
|
|
5668
5982
|
const secondPass = [];
|
|
5669
5983
|
for (const use of mutating) {
|
|
5670
|
-
secondPass.push(await
|
|
5984
|
+
secondPass.push(await safeRun(use));
|
|
5671
5985
|
}
|
|
5672
5986
|
return {
|
|
5673
5987
|
outputs: [...firstPass, ...secondPass],
|
|
@@ -5702,7 +6016,8 @@ var ToolExecutor = class {
|
|
|
5702
6016
|
}
|
|
5703
6017
|
async runWithTimeout(tool, input, parentSignal, ctx, toolUseId) {
|
|
5704
6018
|
if (parentSignal.aborted) {
|
|
5705
|
-
|
|
6019
|
+
if (parentSignal.reason instanceof Error) throw parentSignal.reason;
|
|
6020
|
+
throw new Error(typeof parentSignal.reason === "string" ? parentSignal.reason : "aborted");
|
|
5706
6021
|
}
|
|
5707
6022
|
const timeoutMs = tool.timeoutMs ?? this.iterationTimeoutMs;
|
|
5708
6023
|
const ctrl = new AbortController();
|
|
@@ -5771,16 +6086,23 @@ var ToolExecutor = class {
|
|
|
5771
6086
|
* Matches the logic in DefaultPermissionPolicy so the TUI shows the
|
|
5772
6087
|
* same subject that the trust file would use.
|
|
5773
6088
|
*/
|
|
5774
|
-
subjectFor(toolName, input) {
|
|
6089
|
+
subjectFor(toolName, input, subjectKey) {
|
|
5775
6090
|
if (!input || typeof input !== "object") return void 0;
|
|
5776
6091
|
const obj = input;
|
|
5777
6092
|
const globChars = /[*?\[\]]/g;
|
|
5778
6093
|
const escapeGlob = (s) => s.replace(globChars, (c) => `\\${c}`);
|
|
6094
|
+
const normalizePath = (s) => escapeGlob(s.replace(/\\/g, "/"));
|
|
6095
|
+
if (subjectKey) {
|
|
6096
|
+
const v = obj[subjectKey];
|
|
6097
|
+
if (typeof v === "string") {
|
|
6098
|
+
return subjectKey === "path" || subjectKey === "file" || subjectKey === "files" ? normalizePath(v) : escapeGlob(v);
|
|
6099
|
+
}
|
|
6100
|
+
}
|
|
5779
6101
|
if (toolName === "bash" && typeof obj.command === "string") {
|
|
5780
6102
|
return escapeGlob(obj.command);
|
|
5781
6103
|
}
|
|
5782
6104
|
if (typeof obj.path === "string") {
|
|
5783
|
-
return
|
|
6105
|
+
return normalizePath(obj.path);
|
|
5784
6106
|
}
|
|
5785
6107
|
if (typeof obj.url === "string") {
|
|
5786
6108
|
return escapeGlob(obj.url);
|
|
@@ -6204,8 +6526,9 @@ var DefaultHealthRegistry = class {
|
|
|
6204
6526
|
return { status, timestamp: Date.now(), checks: results };
|
|
6205
6527
|
}
|
|
6206
6528
|
async runOne(check) {
|
|
6529
|
+
let timer = null;
|
|
6207
6530
|
const timeout = new Promise((resolve4) => {
|
|
6208
|
-
setTimeout(
|
|
6531
|
+
timer = setTimeout(
|
|
6209
6532
|
() => resolve4({ status: "unhealthy", detail: `timeout after ${this.timeoutMs}ms` }),
|
|
6210
6533
|
this.timeoutMs
|
|
6211
6534
|
);
|
|
@@ -6214,6 +6537,8 @@ var DefaultHealthRegistry = class {
|
|
|
6214
6537
|
return await Promise.race([check.check(), timeout]);
|
|
6215
6538
|
} catch (err) {
|
|
6216
6539
|
return { status: "unhealthy", detail: err instanceof Error ? err.message : String(err) };
|
|
6540
|
+
} finally {
|
|
6541
|
+
if (timer) clearTimeout(timer);
|
|
6217
6542
|
}
|
|
6218
6543
|
}
|
|
6219
6544
|
};
|
|
@@ -6852,7 +7177,6 @@ function createContextManagerTool(opts = {}) {
|
|
|
6852
7177
|
notes: `Invalid range [${from}, ${to}] for ${messages.length} messages.`
|
|
6853
7178
|
};
|
|
6854
7179
|
}
|
|
6855
|
-
messages.slice(from, to + 1);
|
|
6856
7180
|
const summaryText = input.text ?? '[summary placeholder \u2014 provide "text" to record the summary]';
|
|
6857
7181
|
const summaryMsg = {
|
|
6858
7182
|
role: "system",
|
|
@@ -7114,13 +7438,23 @@ async function streamProviderToResponse(provider, req, signal, ctx, events) {
|
|
|
7114
7438
|
}
|
|
7115
7439
|
} catch (err) {
|
|
7116
7440
|
if (signal.aborted) {
|
|
7117
|
-
state.stopReason = "
|
|
7441
|
+
state.stopReason = "end_turn";
|
|
7118
7442
|
return buildResponse(state);
|
|
7119
7443
|
}
|
|
7120
7444
|
throw err;
|
|
7121
7445
|
} finally {
|
|
7122
7446
|
try {
|
|
7123
|
-
|
|
7447
|
+
let drainTimer = null;
|
|
7448
|
+
try {
|
|
7449
|
+
await Promise.race([
|
|
7450
|
+
Promise.resolve(iter.return?.()),
|
|
7451
|
+
new Promise((resolve4) => {
|
|
7452
|
+
drainTimer = setTimeout(resolve4, 500);
|
|
7453
|
+
})
|
|
7454
|
+
]);
|
|
7455
|
+
} finally {
|
|
7456
|
+
if (drainTimer) clearTimeout(drainTimer);
|
|
7457
|
+
}
|
|
7124
7458
|
} catch {
|
|
7125
7459
|
}
|
|
7126
7460
|
}
|
|
@@ -7177,12 +7511,24 @@ async function runProviderWithRetry(opts) {
|
|
|
7177
7511
|
});
|
|
7178
7512
|
}
|
|
7179
7513
|
await new Promise((resolve4, reject) => {
|
|
7180
|
-
|
|
7181
|
-
const
|
|
7514
|
+
let settled = false;
|
|
7515
|
+
const cleanup = () => {
|
|
7516
|
+
if (settled) return;
|
|
7517
|
+
settled = true;
|
|
7182
7518
|
clearTimeout(t2);
|
|
7519
|
+
};
|
|
7520
|
+
const onAbort = () => {
|
|
7521
|
+
cleanup();
|
|
7183
7522
|
reject(new Error("aborted"));
|
|
7184
7523
|
};
|
|
7185
|
-
|
|
7524
|
+
const t2 = setTimeout(() => {
|
|
7525
|
+
cleanup();
|
|
7526
|
+
resolve4();
|
|
7527
|
+
}, delay);
|
|
7528
|
+
if (signal.aborted) {
|
|
7529
|
+
onAbort();
|
|
7530
|
+
return;
|
|
7531
|
+
}
|
|
7186
7532
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
7187
7533
|
});
|
|
7188
7534
|
attempt++;
|
|
@@ -7306,9 +7652,6 @@ var Agent = class {
|
|
|
7306
7652
|
get scrubber() {
|
|
7307
7653
|
return this.container.resolve(TOKENS.SecretScrubber);
|
|
7308
7654
|
}
|
|
7309
|
-
get compactor() {
|
|
7310
|
-
return this.container.has(TOKENS.Compactor) ? this.container.resolve(TOKENS.Compactor) : void 0;
|
|
7311
|
-
}
|
|
7312
7655
|
get renderer() {
|
|
7313
7656
|
return this.container.has(TOKENS.Renderer) ? this.container.resolve(TOKENS.Renderer) : void 0;
|
|
7314
7657
|
}
|
|
@@ -7354,6 +7697,7 @@ var Agent = class {
|
|
|
7354
7697
|
let iterations = 0;
|
|
7355
7698
|
let effectiveLimit = opts.maxIterations ?? this.maxIterations;
|
|
7356
7699
|
const hasHardLimit = effectiveLimit > 0 && Number.isFinite(effectiveLimit);
|
|
7700
|
+
let recoveryRetries = 0;
|
|
7357
7701
|
for (let i = 0; ; i++) {
|
|
7358
7702
|
iterations = i + 1;
|
|
7359
7703
|
if (controller.signal.aborted) {
|
|
@@ -7383,17 +7727,33 @@ var Agent = class {
|
|
|
7383
7727
|
logger: this.logger,
|
|
7384
7728
|
tracer: this.tracer
|
|
7385
7729
|
});
|
|
7730
|
+
recoveryRetries = 0;
|
|
7386
7731
|
} catch (err) {
|
|
7387
7732
|
if (controller.signal.aborted) {
|
|
7388
7733
|
this.events.emit("error", { err: toError(err), phase: "provider" });
|
|
7389
7734
|
return { status: "aborted", iterations, error: toWrongStackError(err, "AGENT_ABORTED") };
|
|
7390
7735
|
}
|
|
7391
7736
|
const recovered = await this.errorHandler.recover(err, this.ctx);
|
|
7392
|
-
if (!recovered) {
|
|
7737
|
+
if (!recovered || recovered.action === "fail") {
|
|
7393
7738
|
this.events.emit("error", { err: toError(err), phase: "provider" });
|
|
7394
|
-
return {
|
|
7739
|
+
return {
|
|
7740
|
+
status: "failed",
|
|
7741
|
+
iterations,
|
|
7742
|
+
error: toWrongStackError(recovered?.error ?? err)
|
|
7743
|
+
};
|
|
7744
|
+
}
|
|
7745
|
+
if (recovered.action === "retry") {
|
|
7746
|
+
recoveryRetries++;
|
|
7747
|
+
if (recoveryRetries > 2) {
|
|
7748
|
+
this.events.emit("error", { err: toError(err), phase: "provider" });
|
|
7749
|
+
return { status: "failed", iterations, error: toWrongStackError(err) };
|
|
7750
|
+
}
|
|
7751
|
+
if (recovered.model) this.ctx.model = recovered.model;
|
|
7752
|
+
this.logger.info(`Recovered provider error via ${recovered.reason}; retrying turn`);
|
|
7753
|
+
continue;
|
|
7395
7754
|
}
|
|
7396
|
-
|
|
7755
|
+
recoveryRetries = 0;
|
|
7756
|
+
res = recovered.response;
|
|
7397
7757
|
}
|
|
7398
7758
|
const responseResult = await this.processResponse(res, req);
|
|
7399
7759
|
if (responseResult.aborted) {
|
|
@@ -7467,7 +7827,7 @@ var Agent = class {
|
|
|
7467
7827
|
* update session, render text, handle abort.
|
|
7468
7828
|
*/
|
|
7469
7829
|
async processResponse(res, req) {
|
|
7470
|
-
await this.pipelines.response.run(res);
|
|
7830
|
+
res = await this.pipelines.response.run(res);
|
|
7471
7831
|
this.events.emit("provider.response", {
|
|
7472
7832
|
ctx: this.ctx,
|
|
7473
7833
|
usage: res.usage,
|
|
@@ -7538,6 +7898,7 @@ var Agent = class {
|
|
|
7538
7898
|
isError: !!reRunResult.result.is_error
|
|
7539
7899
|
});
|
|
7540
7900
|
this.events.emit("tool.executed", {
|
|
7901
|
+
id: reRunResult.result.tool_use_id,
|
|
7541
7902
|
name: tool.name,
|
|
7542
7903
|
durationMs: reRunResult.durationMs,
|
|
7543
7904
|
ok: !reRunResult.result.is_error,
|
|
@@ -7545,7 +7906,6 @@ var Agent = class {
|
|
|
7545
7906
|
output: truncateForEvent(reRunResult.result.content)
|
|
7546
7907
|
});
|
|
7547
7908
|
}
|
|
7548
|
-
this.ctx.state.appendMessage({ role: "user", content: [reRunResult.result] });
|
|
7549
7909
|
continue;
|
|
7550
7910
|
}
|
|
7551
7911
|
const use = useById.get(result.tool_use_id);
|
|
@@ -7564,6 +7924,7 @@ var Agent = class {
|
|
|
7564
7924
|
isError: !!result.is_error
|
|
7565
7925
|
});
|
|
7566
7926
|
this.events.emit("tool.executed", {
|
|
7927
|
+
id: result.tool_use_id,
|
|
7567
7928
|
name: use.name,
|
|
7568
7929
|
durationMs,
|
|
7569
7930
|
ok: !result.is_error,
|
|
@@ -7604,12 +7965,11 @@ var Agent = class {
|
|
|
7604
7965
|
}
|
|
7605
7966
|
}
|
|
7606
7967
|
/**
|
|
7607
|
-
* Run context window pipeline
|
|
7968
|
+
* Run context window pipeline. The pipeline may be empty, or it may contain
|
|
7969
|
+
* middleware with its own injected dependencies.
|
|
7608
7970
|
*/
|
|
7609
7971
|
async compactContextIfNeeded() {
|
|
7610
|
-
|
|
7611
|
-
await this.pipelines.contextWindow.run(this.ctx);
|
|
7612
|
-
}
|
|
7972
|
+
await this.pipelines.contextWindow.run(this.ctx);
|
|
7613
7973
|
}
|
|
7614
7974
|
};
|
|
7615
7975
|
function toError(err) {
|
|
@@ -7627,7 +7987,6 @@ var ConversationState = class {
|
|
|
7627
7987
|
constructor(ctx) {
|
|
7628
7988
|
this.ctx = ctx;
|
|
7629
7989
|
}
|
|
7630
|
-
// ─── Read API ───────────────────────────────────────────────────────
|
|
7631
7990
|
get messages() {
|
|
7632
7991
|
return this.ctx.messages;
|
|
7633
7992
|
}
|
|
@@ -7642,25 +8001,24 @@ var ConversationState = class {
|
|
|
7642
8001
|
* that need a stable view across an async boundary.
|
|
7643
8002
|
*/
|
|
7644
8003
|
snapshot() {
|
|
7645
|
-
return {
|
|
7646
|
-
messages: [...this.ctx.messages],
|
|
7647
|
-
todos: [...this.ctx.todos],
|
|
7648
|
-
meta: { ...this.ctx.meta }
|
|
7649
|
-
};
|
|
8004
|
+
return Object.freeze({
|
|
8005
|
+
messages: Object.freeze([...this.ctx.messages]),
|
|
8006
|
+
todos: Object.freeze([...this.ctx.todos]),
|
|
8007
|
+
meta: Object.freeze({ ...this.ctx.meta })
|
|
8008
|
+
});
|
|
7650
8009
|
}
|
|
7651
|
-
// ─── Write API (preferred — fires onChange) ─────────────────────────
|
|
7652
8010
|
appendMessage(message) {
|
|
7653
|
-
this.ctx.messages.
|
|
8011
|
+
this.ctx.messages.splice(this.ctx.messages.length, 0, message);
|
|
7654
8012
|
this.emit({ kind: "message_appended", message });
|
|
7655
8013
|
}
|
|
7656
8014
|
replaceMessages(messages) {
|
|
7657
8015
|
this.ctx.messages.length = 0;
|
|
7658
|
-
this.ctx.messages.
|
|
8016
|
+
this.ctx.messages.splice(0, 0, ...messages);
|
|
7659
8017
|
this.emit({ kind: "messages_replaced", messages: [...messages] });
|
|
7660
8018
|
}
|
|
7661
8019
|
replaceTodos(todos) {
|
|
7662
8020
|
this.ctx.todos.length = 0;
|
|
7663
|
-
this.ctx.todos.
|
|
8021
|
+
this.ctx.todos.splice(0, 0, ...todos);
|
|
7664
8022
|
this.emit({ kind: "todos_replaced", todos: [...todos] });
|
|
7665
8023
|
}
|
|
7666
8024
|
setMeta(key, value) {
|
|
@@ -7672,13 +8030,15 @@ var ConversationState = class {
|
|
|
7672
8030
|
delete this.ctx.meta[key];
|
|
7673
8031
|
this.emit({ kind: "meta_deleted", key });
|
|
7674
8032
|
}
|
|
7675
|
-
|
|
8033
|
+
clearMeta() {
|
|
8034
|
+
const keys = Object.keys(this.ctx.meta);
|
|
8035
|
+
if (keys.length === 0) return;
|
|
8036
|
+
for (const key of keys) delete this.ctx.meta[key];
|
|
8037
|
+
this.emit({ kind: "meta_cleared" });
|
|
8038
|
+
}
|
|
7676
8039
|
/**
|
|
7677
|
-
* Subscribe to mutations that go through this wrapper.
|
|
7678
|
-
*
|
|
7679
|
-
* NOT observed — by design during migration, since we don't want to
|
|
7680
|
-
* monkey-patch arrays. Migrating call sites to use this API is the
|
|
7681
|
-
* dev-plan #1 work.
|
|
8040
|
+
* Subscribe to mutations that go through this wrapper. Direct mutations of
|
|
8041
|
+
* the compatibility arrays are intentionally not observed.
|
|
7682
8042
|
*/
|
|
7683
8043
|
onChange(listener) {
|
|
7684
8044
|
this.listeners.add(listener);
|
|
@@ -7895,34 +8255,60 @@ You operate inside the user's terminal with direct read and write access to thei
|
|
|
7895
8255
|
|
|
7896
8256
|
## Core principles
|
|
7897
8257
|
|
|
7898
|
-
1. Read before you write
|
|
7899
|
-
|
|
7900
|
-
|
|
7901
|
-
|
|
7902
|
-
|
|
8258
|
+
1. **Read before you write.** Always inspect the relevant files before proposing changes. Assumptions about code you haven't read are bugs in waiting.
|
|
8259
|
+
2. **Prefer surgical edits over rewrites.** When modifying existing files, use the edit tool with str_replace; only use write for new files or full replacements explicitly requested.
|
|
8260
|
+
3. **Show your work.** Before non-trivial changes, briefly state what you're about to do \u2014 one sentence, not a wall of text. After tool calls, summarize what happened, not what you did mechanically.
|
|
8261
|
+
4. **Be honest about limits.** If you don't know, say so. If something failed, say what failed and what you'll try next. Never fabricate file contents, API responses, or test results.
|
|
8262
|
+
5. **Be concise.** The user is a developer in a terminal. No marketing language, no "great question!", no bullet-point lists when prose works. If a one-liner answers, a one-liner is the answer.
|
|
8263
|
+
6. **Ask when blocked, proceed when not.** If the task is ambiguous in a way that meaningfully changes the approach, ask. If it's ambiguous in a way that doesn't, pick a reasonable default and proceed, stating the assumption.
|
|
8264
|
+
7. **Trust the tools.** If a permission prompt is shown, the user will answer. Do not preemptively explain that you "would like to" do something \u2014 call the tool, let the permission flow decide.
|
|
8265
|
+
8. **Format for scanability.** Use code blocks for code, backticks for file paths, bold for key terms. One-liners stay one line. Paragraphs max 3 sentences.
|
|
8266
|
+
9. **Recover explicitly.** When a tool fails, state: (1) what failed, (2) what you tried, (3) what you'll attempt next. Never silently skip.
|
|
7903
8267
|
|
|
7904
|
-
|
|
8268
|
+
## Decision heuristics
|
|
7905
8269
|
|
|
7906
|
-
|
|
8270
|
+
- **Task is ambiguous** (unclear which file, conflicting requirements) \u2192 ask before proceeding
|
|
8271
|
+
- **Task is clear, approach is unknown** \u2192 try one approach, report what happened
|
|
8272
|
+
- **Tool fails** \u2192 retry once with adjusted params, then report failure
|
|
8273
|
+
- **Permission prompt shown** \u2192 wait for user, do not act unilaterally
|
|
8274
|
+
- **Context window filling up** \u2192 use context_manager proactively; don't wait to be told
|
|
7907
8275
|
|
|
7908
|
-
|
|
8276
|
+
## How you work
|
|
7909
8277
|
|
|
7910
|
-
|
|
7911
|
-
|
|
7912
|
-
|
|
7913
|
-
|
|
7914
|
-
-
|
|
7915
|
-
- You do not add comments to code unless they materially help or were requested.
|
|
7916
|
-
- You do not refactor adjacent code while fixing a bug, unless asked.
|
|
7917
|
-
- You do not claim work is "production-ready" or "fully tested" \u2014 the user decides that.
|
|
7918
|
-
- You do not apologize for failures. You report them and proceed.`;
|
|
8278
|
+
- **Stay focused.** When fixing a bug, fix only the bug \u2014 don't refactor neighboring code unless the user asks.
|
|
8279
|
+
- **Comment with purpose.** Add comments only when they explain why, not what. The code already says what.
|
|
8280
|
+
- **Own your output.** Never call work "production-ready" or "fully tested" \u2014 the user makes that call.
|
|
8281
|
+
- **Move on from mistakes.** When something fails, report what happened and what you'll do next. No apologies, no hand-wringing.
|
|
8282
|
+
- **Stay in your lane.** Don't lecture about software engineering principles unless explicitly asked \u2014 the user is the expert on their codebase.`;
|
|
7919
8283
|
var DefaultSystemPromptBuilder = class {
|
|
7920
8284
|
constructor(opts = {}) {
|
|
7921
8285
|
this.opts = opts;
|
|
7922
8286
|
}
|
|
7923
8287
|
opts;
|
|
7924
|
-
|
|
8288
|
+
/**
|
|
8289
|
+
* Cached environment block, keyed by projectRoot. A single builder
|
|
8290
|
+
* instance is normally reused across turns of the same agent run, but
|
|
8291
|
+
* tests and library consumers may reuse it across runs with different
|
|
8292
|
+
* roots; keying the cache prevents leaking the first call's project
|
|
8293
|
+
* state into a later call against an unrelated project.
|
|
8294
|
+
*/
|
|
8295
|
+
envCacheByRoot = /* @__PURE__ */ new Map();
|
|
8296
|
+
skillCache;
|
|
7925
8297
|
async build(ctx) {
|
|
8298
|
+
if (this.opts.skillLoader && !this.skillCache) {
|
|
8299
|
+
try {
|
|
8300
|
+
const entries = await this.opts.skillLoader.listEntries();
|
|
8301
|
+
if (entries.length > 0) {
|
|
8302
|
+
const lines = [];
|
|
8303
|
+
for (const e of entries) {
|
|
8304
|
+
const scopeTag = e.scope.length > 0 ? ` \u2014 ${e.scope.slice(0, 4).join(", ")}` : "";
|
|
8305
|
+
lines.push(`- **${e.name}**${scopeTag} (${e.trigger})`);
|
|
8306
|
+
}
|
|
8307
|
+
this.skillCache = lines.join("\n");
|
|
8308
|
+
}
|
|
8309
|
+
} catch {
|
|
8310
|
+
}
|
|
8311
|
+
}
|
|
7926
8312
|
const layer1 = LAYER_1_IDENTITY;
|
|
7927
8313
|
const layer2 = this.buildToolUsage(ctx.tools);
|
|
7928
8314
|
const layer3 = await this.buildEnvironment(ctx);
|
|
@@ -7958,8 +8344,20 @@ var DefaultSystemPromptBuilder = class {
|
|
|
7958
8344
|
### ${t2.name}
|
|
7959
8345
|
${hint.trim()}`);
|
|
7960
8346
|
}
|
|
8347
|
+
lines.push(`
|
|
8348
|
+
## Common patterns
|
|
8349
|
+
|
|
8350
|
+
- **Inspect before edit:** \`read\`/\`glob\`/\`grep\` \u2192 locate target \u2192 \`edit\`
|
|
8351
|
+
- **Search then operate:** \`grep\`/\`glob\` \u2192 identify targets \u2192 \`batch_tool_use\` or iterative \`edit\`
|
|
8352
|
+
- **Verify after mutate:** \`write\`/\`edit\`/\`patch\` \u2192 \`read\` back to confirm \u2192 report outcome
|
|
8353
|
+
- **Explore project:** \`glob\` for structure \u2192 \`read\` key files \u2192 \`grep\` for patterns
|
|
8354
|
+
- **Batch ops:** Use \`replace\` with glob patterns for multi-file surgical changes
|
|
8355
|
+
|
|
8356
|
+
When unsure about a file's current state, read it first rather than assuming.`);
|
|
7961
8357
|
const hasContextManager = tools.some((t2) => t2.name === "context_manager");
|
|
7962
8358
|
if (hasContextManager) {
|
|
8359
|
+
const maxCtx = this.opts.modelCapabilities?.maxContextTokens ?? 128e3;
|
|
8360
|
+
const threshold = maxCtx <= 32e3 ? "50" : "70";
|
|
7963
8361
|
lines.push(`
|
|
7964
8362
|
## Context management
|
|
7965
8363
|
|
|
@@ -7967,7 +8365,7 @@ When the conversation grows long and context window usage exceeds what you can t
|
|
|
7967
8365
|
use the context_manager tool proactively \u2014 do NOT wait to be told:
|
|
7968
8366
|
|
|
7969
8367
|
- Call \`context_manager\` with \`{"action":"check"}\` to see current token budget and message counts.
|
|
7970
|
-
- When the conversation exceeds
|
|
8368
|
+
- When the conversation exceeds ~${threshold}% of your context window, call \`{"action":"summary"}\` or \`{"action":"compact"}\` to reclaim space.
|
|
7971
8369
|
- Use \`{"action":"prune"}\` to surgically remove specific irrelevant message ranges (e.g. old debug output).
|
|
7972
8370
|
- Use \`{"action":"add_note"}\` to inject a summary note at a specific point after a complex operation.
|
|
7973
8371
|
|
|
@@ -7977,14 +8375,17 @@ summarize it, and let the tool result hold only the summary.`);
|
|
|
7977
8375
|
return lines.join("\n");
|
|
7978
8376
|
}
|
|
7979
8377
|
async buildEnvironment(ctx) {
|
|
7980
|
-
|
|
8378
|
+
const cached = this.envCacheByRoot.get(ctx.projectRoot);
|
|
8379
|
+
if (cached) return cached;
|
|
7981
8380
|
const today = this.opts.todayIso ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
7982
8381
|
const platform2 = `${os3.platform()} ${os3.release()}`;
|
|
7983
8382
|
const shell = process.env.SHELL ?? process.env.ComSpec ?? "unknown";
|
|
7984
8383
|
const node = process.version;
|
|
7985
8384
|
const isGit = await this.dirExists(path2.join(ctx.projectRoot, ".git"));
|
|
7986
|
-
const git =
|
|
7987
|
-
|
|
8385
|
+
const [git, langs] = await Promise.all([
|
|
8386
|
+
isGit ? this.gitStatus(ctx.projectRoot) : Promise.resolve("not a git repo"),
|
|
8387
|
+
this.detectLanguages(ctx.projectRoot)
|
|
8388
|
+
]);
|
|
7988
8389
|
const lines = [
|
|
7989
8390
|
"## Environment",
|
|
7990
8391
|
"",
|
|
@@ -8002,8 +8403,17 @@ summarize it, and let the tool result hold only the summary.`);
|
|
|
8002
8403
|
`- Running on: ${ctx.provider ?? "<unknown provider>"}/${ctx.model ?? "<unknown model>"}`
|
|
8003
8404
|
);
|
|
8004
8405
|
}
|
|
8406
|
+
if (this.opts.modeId && this.opts.modeId !== "default") {
|
|
8407
|
+
lines.push(`- Mode: ${this.opts.modeId}`);
|
|
8408
|
+
}
|
|
8409
|
+
if (this.opts.modelCapabilities) {
|
|
8410
|
+
lines.push(`- Context window: ${this.opts.modelCapabilities.maxContextTokens.toLocaleString()} tokens max`);
|
|
8411
|
+
}
|
|
8412
|
+
if (this.skillCache) {
|
|
8413
|
+
lines.push("", "## Skills in scope for this session", this.skillCache);
|
|
8414
|
+
}
|
|
8005
8415
|
const text = lines.join("\n");
|
|
8006
|
-
this.
|
|
8416
|
+
this.envCacheByRoot.set(ctx.projectRoot, text);
|
|
8007
8417
|
return text;
|
|
8008
8418
|
}
|
|
8009
8419
|
async buildMemoryAndSkills() {
|
|
@@ -8017,16 +8427,10 @@ ${mem}`);
|
|
|
8017
8427
|
} catch {
|
|
8018
8428
|
}
|
|
8019
8429
|
}
|
|
8020
|
-
if (this.opts.skillLoader) {
|
|
8021
|
-
try {
|
|
8022
|
-
const manifest = await this.opts.skillLoader.manifestText();
|
|
8023
|
-
if (manifest.trim()) parts.push(manifest);
|
|
8024
|
-
} catch {
|
|
8025
|
-
}
|
|
8026
|
-
}
|
|
8027
8430
|
return parts.join("\n\n");
|
|
8028
8431
|
}
|
|
8029
8432
|
async buildMode() {
|
|
8433
|
+
if (this.opts.modePrompt) return this.opts.modePrompt;
|
|
8030
8434
|
if (!this.opts.modeStore) return "";
|
|
8031
8435
|
const mode = await this.opts.modeStore.getActiveMode();
|
|
8032
8436
|
if (!mode?.prompt) return "";
|
|
@@ -8042,8 +8446,19 @@ ${mem}`);
|
|
|
8042
8446
|
}
|
|
8043
8447
|
async gitStatus(root) {
|
|
8044
8448
|
return new Promise((resolve4) => {
|
|
8449
|
+
let settled = false;
|
|
8450
|
+
const finish = (s) => {
|
|
8451
|
+
if (settled) return;
|
|
8452
|
+
settled = true;
|
|
8453
|
+
resolve4(s);
|
|
8454
|
+
};
|
|
8455
|
+
let proc;
|
|
8456
|
+
const timer = setTimeout(() => {
|
|
8457
|
+
proc?.kill("SIGKILL");
|
|
8458
|
+
finish("git timeout");
|
|
8459
|
+
}, 2e3);
|
|
8045
8460
|
try {
|
|
8046
|
-
|
|
8461
|
+
proc = spawn("git", ["status", "--porcelain=v1", "--branch"], {
|
|
8047
8462
|
cwd: root,
|
|
8048
8463
|
stdio: ["ignore", "pipe", "ignore"]
|
|
8049
8464
|
});
|
|
@@ -8051,19 +8466,24 @@ ${mem}`);
|
|
|
8051
8466
|
proc.stdout?.on("data", (c) => {
|
|
8052
8467
|
buf += c.toString();
|
|
8053
8468
|
});
|
|
8054
|
-
proc.on("error", () =>
|
|
8469
|
+
proc.on("error", () => {
|
|
8470
|
+
clearTimeout(timer);
|
|
8471
|
+
finish("git error");
|
|
8472
|
+
});
|
|
8055
8473
|
proc.on("close", () => {
|
|
8474
|
+
clearTimeout(timer);
|
|
8056
8475
|
const lines = buf.split("\n").filter(Boolean);
|
|
8057
8476
|
const branchLine = lines[0] ?? "";
|
|
8058
|
-
const branchMatch = /## ([^\s.]+)
|
|
8477
|
+
const branchMatch = branchLine.match(/## ([^\s.]+)/);
|
|
8059
8478
|
const branch = branchMatch?.[1] ?? "detached";
|
|
8060
8479
|
const dirty = lines.slice(1);
|
|
8061
8480
|
const staged = dirty.filter((l) => /^[MARCD]/.test(l)).length;
|
|
8062
8481
|
const modified = dirty.length - staged;
|
|
8063
|
-
|
|
8482
|
+
finish(`branch=${branch}, ${modified} modified, ${staged} staged`);
|
|
8064
8483
|
});
|
|
8065
8484
|
} catch {
|
|
8066
|
-
|
|
8485
|
+
clearTimeout(timer);
|
|
8486
|
+
finish("git unavailable");
|
|
8067
8487
|
}
|
|
8068
8488
|
});
|
|
8069
8489
|
}
|
|
@@ -8081,14 +8501,17 @@ ${mem}`);
|
|
|
8081
8501
|
["composer.json", "PHP"],
|
|
8082
8502
|
["mix.exs", "Elixir"]
|
|
8083
8503
|
];
|
|
8084
|
-
const
|
|
8085
|
-
|
|
8086
|
-
|
|
8087
|
-
|
|
8088
|
-
|
|
8089
|
-
|
|
8090
|
-
|
|
8091
|
-
|
|
8504
|
+
const hits = await Promise.all(
|
|
8505
|
+
checks.map(async ([marker, lang]) => {
|
|
8506
|
+
try {
|
|
8507
|
+
await fsp.access(path2.join(root, marker));
|
|
8508
|
+
return lang;
|
|
8509
|
+
} catch {
|
|
8510
|
+
return null;
|
|
8511
|
+
}
|
|
8512
|
+
})
|
|
8513
|
+
);
|
|
8514
|
+
const langs = new Set(hits.filter((l) => l !== null));
|
|
8092
8515
|
return langs.size === 0 ? "unknown" : Array.from(langs).join(", ");
|
|
8093
8516
|
}
|
|
8094
8517
|
};
|