@wrongstack/core 0.1.4 → 0.1.8
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 +557 -33
- package/dist/defaults/index.js +1421 -244
- package/dist/defaults/index.js.map +1 -1
- package/dist/index.d.ts +31 -13
- package/dist/index.js +1614 -334
- 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-9sOTgmeC.d.ts} +44 -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
|
|
1365
|
+
if (input === null || typeof input !== "object") {
|
|
1366
|
+
return RoughTokenEstimate(String(input));
|
|
1317
1367
|
}
|
|
1318
|
-
const
|
|
1319
|
-
|
|
1320
|
-
if (input !== null && typeof input === "object" && !Array.isArray(input)) {
|
|
1321
|
-
input.__tokenEstimate = estimate;
|
|
1322
|
-
}
|
|
1323
|
-
return estimate;
|
|
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);
|
|
@@ -1336,11 +1383,11 @@ function validateAgainstSchema(value, schema) {
|
|
|
1336
1383
|
walk(value, schema, "", errors);
|
|
1337
1384
|
return { ok: errors.length === 0, errors };
|
|
1338
1385
|
}
|
|
1339
|
-
function walk(value, schema,
|
|
1386
|
+
function walk(value, schema, path17, errors) {
|
|
1340
1387
|
if (schema.enum !== void 0) {
|
|
1341
1388
|
if (!schema.enum.some((e) => deepEqual(e, value))) {
|
|
1342
1389
|
errors.push({
|
|
1343
|
-
path:
|
|
1390
|
+
path: path17 || "<root>",
|
|
1344
1391
|
message: `expected one of ${JSON.stringify(schema.enum)}, got ${JSON.stringify(value)}`
|
|
1345
1392
|
});
|
|
1346
1393
|
return;
|
|
@@ -1349,7 +1396,7 @@ function walk(value, schema, path15, errors) {
|
|
|
1349
1396
|
if (typeof schema.type === "string") {
|
|
1350
1397
|
if (!checkType(value, schema.type)) {
|
|
1351
1398
|
errors.push({
|
|
1352
|
-
path:
|
|
1399
|
+
path: path17 || "<root>",
|
|
1353
1400
|
message: `expected ${schema.type}, got ${describeType(value)}`
|
|
1354
1401
|
});
|
|
1355
1402
|
return;
|
|
@@ -1359,19 +1406,19 @@ function walk(value, schema, path15, errors) {
|
|
|
1359
1406
|
const obj = value;
|
|
1360
1407
|
for (const req of schema.required ?? []) {
|
|
1361
1408
|
if (!(req in obj)) {
|
|
1362
|
-
errors.push({ path: joinPath(
|
|
1409
|
+
errors.push({ path: joinPath(path17, req), message: "required property missing" });
|
|
1363
1410
|
}
|
|
1364
1411
|
}
|
|
1365
1412
|
if (schema.properties) {
|
|
1366
1413
|
for (const [key, subSchema] of Object.entries(schema.properties)) {
|
|
1367
1414
|
if (key in obj) {
|
|
1368
|
-
walk(obj[key], subSchema, joinPath(
|
|
1415
|
+
walk(obj[key], subSchema, joinPath(path17, key), errors);
|
|
1369
1416
|
}
|
|
1370
1417
|
}
|
|
1371
1418
|
}
|
|
1372
1419
|
}
|
|
1373
1420
|
if (schema.type === "array" && Array.isArray(value) && schema.items) {
|
|
1374
|
-
value.forEach((item, i) => walk(item, schema.items, `${
|
|
1421
|
+
value.forEach((item, i) => walk(item, schema.items, `${path17}[${i}]`, errors));
|
|
1375
1422
|
}
|
|
1376
1423
|
}
|
|
1377
1424
|
function checkType(value, type) {
|
|
@@ -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";
|
|
@@ -4427,6 +4710,182 @@ function defaultFormatTaskInput(task) {
|
|
|
4427
4710
|
return task.description ?? "";
|
|
4428
4711
|
}
|
|
4429
4712
|
|
|
4713
|
+
// src/defaults/fleet-bus.ts
|
|
4714
|
+
var FleetBus = class {
|
|
4715
|
+
byId = /* @__PURE__ */ new Map();
|
|
4716
|
+
byType = /* @__PURE__ */ new Map();
|
|
4717
|
+
any = /* @__PURE__ */ new Set();
|
|
4718
|
+
/**
|
|
4719
|
+
* Hook a subagent's EventBus into the fleet. EventBus is strongly
|
|
4720
|
+
* typed and doesn't expose an `onAny` hook, so we subscribe to the
|
|
4721
|
+
* canonical set of event types a subagent emits during a run. New
|
|
4722
|
+
* event types added to the kernel must be added here too — but the
|
|
4723
|
+
* cost is a tiny single line per type, and the explicit list keeps
|
|
4724
|
+
* the wire format clear.
|
|
4725
|
+
*
|
|
4726
|
+
* Returns a disposer that detaches every subscription; call on
|
|
4727
|
+
* subagent teardown so the listeners don't outlive the run.
|
|
4728
|
+
*/
|
|
4729
|
+
attach(subagentId, bus, taskId) {
|
|
4730
|
+
const FORWARDED_TYPES = [
|
|
4731
|
+
"tool.started",
|
|
4732
|
+
"tool.executed",
|
|
4733
|
+
"tool.progress",
|
|
4734
|
+
"tool.confirm_needed",
|
|
4735
|
+
"iteration.started",
|
|
4736
|
+
"iteration.completed",
|
|
4737
|
+
"provider.text_delta",
|
|
4738
|
+
"provider.response",
|
|
4739
|
+
"provider.retry",
|
|
4740
|
+
"provider.error",
|
|
4741
|
+
"session.started",
|
|
4742
|
+
"session.ended",
|
|
4743
|
+
"token.threshold"
|
|
4744
|
+
];
|
|
4745
|
+
const offs = [];
|
|
4746
|
+
for (const t2 of FORWARDED_TYPES) {
|
|
4747
|
+
offs.push(
|
|
4748
|
+
bus.on(t2, (payload) => {
|
|
4749
|
+
this.emit({ subagentId, taskId, ts: Date.now(), type: t2, payload });
|
|
4750
|
+
})
|
|
4751
|
+
);
|
|
4752
|
+
}
|
|
4753
|
+
return () => {
|
|
4754
|
+
for (const off of offs) off();
|
|
4755
|
+
};
|
|
4756
|
+
}
|
|
4757
|
+
/** Subscribe to every event from one subagent. */
|
|
4758
|
+
subscribe(subagentId, handler) {
|
|
4759
|
+
let set = this.byId.get(subagentId);
|
|
4760
|
+
if (!set) {
|
|
4761
|
+
set = /* @__PURE__ */ new Set();
|
|
4762
|
+
this.byId.set(subagentId, set);
|
|
4763
|
+
}
|
|
4764
|
+
set.add(handler);
|
|
4765
|
+
return () => {
|
|
4766
|
+
set.delete(handler);
|
|
4767
|
+
};
|
|
4768
|
+
}
|
|
4769
|
+
/** Subscribe to one event type across all subagents. */
|
|
4770
|
+
filter(type, handler) {
|
|
4771
|
+
let set = this.byType.get(type);
|
|
4772
|
+
if (!set) {
|
|
4773
|
+
set = /* @__PURE__ */ new Set();
|
|
4774
|
+
this.byType.set(type, set);
|
|
4775
|
+
}
|
|
4776
|
+
set.add(handler);
|
|
4777
|
+
return () => {
|
|
4778
|
+
set.delete(handler);
|
|
4779
|
+
};
|
|
4780
|
+
}
|
|
4781
|
+
/** Subscribe to literally everything. The fleet roll-up uses this. */
|
|
4782
|
+
onAny(handler) {
|
|
4783
|
+
this.any.add(handler);
|
|
4784
|
+
return () => {
|
|
4785
|
+
this.any.delete(handler);
|
|
4786
|
+
};
|
|
4787
|
+
}
|
|
4788
|
+
emit(event) {
|
|
4789
|
+
const byId = this.byId.get(event.subagentId);
|
|
4790
|
+
if (byId) for (const h of byId) {
|
|
4791
|
+
try {
|
|
4792
|
+
h(event);
|
|
4793
|
+
} catch {
|
|
4794
|
+
}
|
|
4795
|
+
}
|
|
4796
|
+
const byType = this.byType.get(event.type);
|
|
4797
|
+
if (byType) for (const h of byType) {
|
|
4798
|
+
try {
|
|
4799
|
+
h(event);
|
|
4800
|
+
} catch {
|
|
4801
|
+
}
|
|
4802
|
+
}
|
|
4803
|
+
for (const h of this.any) {
|
|
4804
|
+
try {
|
|
4805
|
+
h(event);
|
|
4806
|
+
} catch {
|
|
4807
|
+
}
|
|
4808
|
+
}
|
|
4809
|
+
}
|
|
4810
|
+
};
|
|
4811
|
+
var FleetUsageAggregator = class {
|
|
4812
|
+
constructor(bus, priceLookup, metaLookup) {
|
|
4813
|
+
this.bus = bus;
|
|
4814
|
+
this.priceLookup = priceLookup;
|
|
4815
|
+
this.metaLookup = metaLookup;
|
|
4816
|
+
bus.filter("provider.response", (e) => this.onProviderResponse(e));
|
|
4817
|
+
bus.filter("tool.executed", (e) => this.onToolExecuted(e));
|
|
4818
|
+
bus.filter("iteration.started", (e) => this.onIterationStarted(e));
|
|
4819
|
+
}
|
|
4820
|
+
bus;
|
|
4821
|
+
priceLookup;
|
|
4822
|
+
metaLookup;
|
|
4823
|
+
perSubagent = /* @__PURE__ */ new Map();
|
|
4824
|
+
total = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
|
|
4825
|
+
/** Live snapshot — safe to call from a tool's execute() body. */
|
|
4826
|
+
snapshot() {
|
|
4827
|
+
return {
|
|
4828
|
+
total: { ...this.total },
|
|
4829
|
+
perSubagent: Object.fromEntries(
|
|
4830
|
+
Array.from(this.perSubagent.entries()).map(([k, v]) => [k, { ...v }])
|
|
4831
|
+
)
|
|
4832
|
+
};
|
|
4833
|
+
}
|
|
4834
|
+
ensure(subagentId) {
|
|
4835
|
+
let snap = this.perSubagent.get(subagentId);
|
|
4836
|
+
if (!snap) {
|
|
4837
|
+
const meta = this.metaLookup?.(subagentId);
|
|
4838
|
+
snap = {
|
|
4839
|
+
subagentId,
|
|
4840
|
+
provider: meta?.provider,
|
|
4841
|
+
model: meta?.model,
|
|
4842
|
+
input: 0,
|
|
4843
|
+
output: 0,
|
|
4844
|
+
cacheRead: 0,
|
|
4845
|
+
cacheWrite: 0,
|
|
4846
|
+
cost: 0,
|
|
4847
|
+
toolCalls: 0,
|
|
4848
|
+
iterations: 0,
|
|
4849
|
+
startedAt: Date.now(),
|
|
4850
|
+
lastEventAt: Date.now()
|
|
4851
|
+
};
|
|
4852
|
+
this.perSubagent.set(subagentId, snap);
|
|
4853
|
+
}
|
|
4854
|
+
return snap;
|
|
4855
|
+
}
|
|
4856
|
+
onProviderResponse(e) {
|
|
4857
|
+
const snap = this.ensure(e.subagentId);
|
|
4858
|
+
const p = e.payload;
|
|
4859
|
+
const usage = p?.usage;
|
|
4860
|
+
if (!usage) return;
|
|
4861
|
+
snap.input += usage.input ?? 0;
|
|
4862
|
+
snap.output += usage.output ?? 0;
|
|
4863
|
+
snap.cacheRead += usage.cacheRead ?? 0;
|
|
4864
|
+
snap.cacheWrite += usage.cacheWrite ?? 0;
|
|
4865
|
+
this.total.input += usage.input ?? 0;
|
|
4866
|
+
this.total.output += usage.output ?? 0;
|
|
4867
|
+
this.total.cacheRead += usage.cacheRead ?? 0;
|
|
4868
|
+
this.total.cacheWrite += usage.cacheWrite ?? 0;
|
|
4869
|
+
const price = this.priceLookup?.(e.subagentId);
|
|
4870
|
+
if (price) {
|
|
4871
|
+
const delta = (usage.input ?? 0) / 1e6 * (price.input ?? 0) + (usage.output ?? 0) / 1e6 * (price.output ?? 0) + (usage.cacheRead ?? 0) / 1e6 * (price.cacheRead ?? 0) + (usage.cacheWrite ?? 0) / 1e6 * (price.cacheWrite ?? 0);
|
|
4872
|
+
snap.cost += delta;
|
|
4873
|
+
this.total.cost += delta;
|
|
4874
|
+
}
|
|
4875
|
+
snap.lastEventAt = e.ts;
|
|
4876
|
+
}
|
|
4877
|
+
onToolExecuted(e) {
|
|
4878
|
+
const snap = this.ensure(e.subagentId);
|
|
4879
|
+
snap.toolCalls += 1;
|
|
4880
|
+
snap.lastEventAt = e.ts;
|
|
4881
|
+
}
|
|
4882
|
+
onIterationStarted(e) {
|
|
4883
|
+
const snap = this.ensure(e.subagentId);
|
|
4884
|
+
snap.iterations += 1;
|
|
4885
|
+
snap.lastEventAt = e.ts;
|
|
4886
|
+
}
|
|
4887
|
+
};
|
|
4888
|
+
|
|
4430
4889
|
// src/defaults/transport/in-memory-transport.ts
|
|
4431
4890
|
var InMemoryBridgeTransport = class {
|
|
4432
4891
|
subs = /* @__PURE__ */ new Map();
|
|
@@ -4513,6 +4972,11 @@ var InMemoryAgentBridge = class {
|
|
|
4513
4972
|
if (this.stopped) throw new Error("Bridge is stopped");
|
|
4514
4973
|
const timeout = timeoutMs ?? this.timeoutMs;
|
|
4515
4974
|
const correlationId = msg.id;
|
|
4975
|
+
if (this.pendingRequests.has(correlationId)) {
|
|
4976
|
+
throw new Error(
|
|
4977
|
+
`Bridge request id "${correlationId}" collides with an in-flight request \u2014 caller is reusing message ids`
|
|
4978
|
+
);
|
|
4979
|
+
}
|
|
4516
4980
|
return new Promise((resolve4, reject) => {
|
|
4517
4981
|
const timer = setTimeout(() => {
|
|
4518
4982
|
this.pendingRequests.delete(correlationId);
|
|
@@ -4549,12 +5013,686 @@ function createMessage(type, from, payload, to) {
|
|
|
4549
5013
|
};
|
|
4550
5014
|
}
|
|
4551
5015
|
|
|
5016
|
+
// src/defaults/director-prompts.ts
|
|
5017
|
+
var DEFAULT_DIRECTOR_PREAMBLE = `You are the Director of a multi-agent fleet. You orchestrate worker
|
|
5018
|
+
subagents by spawning them, assigning tasks, awaiting completions, and
|
|
5019
|
+
rolling up their outputs into your next decision.
|
|
5020
|
+
|
|
5021
|
+
Core fleet tools available to you:
|
|
5022
|
+
- spawn_subagent \u2014 create a worker with a chosen provider / model / role
|
|
5023
|
+
- assign_task \u2014 hand a piece of work to a specific subagent
|
|
5024
|
+
- await_tasks \u2014 block until named task ids complete (parallel-safe)
|
|
5025
|
+
- ask_subagent \u2014 synchronously query a running subagent via the bridge
|
|
5026
|
+
- roll_up \u2014 aggregate finished tasks into a markdown/json summary
|
|
5027
|
+
- terminate_subagent \u2014 abort a stuck worker (use sparingly)
|
|
5028
|
+
- fleet_status \u2014 snapshot of all subagents and pending tasks
|
|
5029
|
+
- fleet_usage \u2014 token + cost breakdown per subagent and total
|
|
5030
|
+
|
|
5031
|
+
Working rules:
|
|
5032
|
+
1. Decompose first. Before spawning, decide which sub-tasks are
|
|
5033
|
+
independent and can run in parallel. Sequential work doesn't need a
|
|
5034
|
+
subagent \u2014 do it yourself.
|
|
5035
|
+
2. Match worker to job. Cheap/fast model for triage, capable model for
|
|
5036
|
+
synthesis. Different providers per sibling is allowed and encouraged.
|
|
5037
|
+
3. Always pair an assign with an await. Don't fire-and-forget; you owe
|
|
5038
|
+
the user a single coherent answer at the end.
|
|
5039
|
+
4. Roll up before deciding. After await_tasks resolves, call roll_up so
|
|
5040
|
+
the results are folded back into your context in a compact form.
|
|
5041
|
+
5. Budget is real. Check fleet_usage periodically. If a subagent is
|
|
5042
|
+
thrashing, terminate it rather than letting cost climb silently.
|
|
5043
|
+
6. Never claim a subagent's work as your own without verifying it. If a
|
|
5044
|
+
result looks wrong, ask_subagent for clarification before passing it
|
|
5045
|
+
to the user.`;
|
|
5046
|
+
var DEFAULT_SUBAGENT_BASELINE = `You are a subagent operating under a Director. You were spawned to handle
|
|
5047
|
+
a specific slice of a larger plan \u2014 do that slice well and report back.
|
|
5048
|
+
|
|
5049
|
+
Bridge contract:
|
|
5050
|
+
- You have a parent (the Director). You may call \`request\` on the
|
|
5051
|
+
parent bridge to ask a clarifying question. Use this sparingly; the
|
|
5052
|
+
parent is also working.
|
|
5053
|
+
- You MAY NOT request the parent's system prompt, tool list, or other
|
|
5054
|
+
subagents' context. Those are not yours to read.
|
|
5055
|
+
- Your final task output is what the Director sees. Be concise,
|
|
5056
|
+
structured, and self-contained \u2014 assume the Director will paste your
|
|
5057
|
+
output into its own context.`;
|
|
5058
|
+
function composeDirectorPrompt(parts = {}) {
|
|
5059
|
+
const sections = [];
|
|
5060
|
+
const preamble = parts.directorPreamble ?? DEFAULT_DIRECTOR_PREAMBLE;
|
|
5061
|
+
if (preamble && preamble.trim().length > 0) sections.push(preamble.trim());
|
|
5062
|
+
if (parts.rosterSummary && parts.rosterSummary.trim().length > 0) {
|
|
5063
|
+
sections.push(`Available roles you can spawn:
|
|
5064
|
+
${parts.rosterSummary.trim()}`);
|
|
5065
|
+
}
|
|
5066
|
+
if (parts.basePrompt && parts.basePrompt.trim().length > 0) {
|
|
5067
|
+
sections.push(parts.basePrompt.trim());
|
|
5068
|
+
}
|
|
5069
|
+
return sections.join("\n\n");
|
|
5070
|
+
}
|
|
5071
|
+
function composeSubagentPrompt(parts = {}) {
|
|
5072
|
+
const sections = [];
|
|
5073
|
+
const baseline = parts.baseline ?? DEFAULT_SUBAGENT_BASELINE;
|
|
5074
|
+
if (baseline && baseline.trim().length > 0) sections.push(baseline.trim());
|
|
5075
|
+
if (parts.role && parts.role.trim().length > 0) {
|
|
5076
|
+
sections.push(`Role:
|
|
5077
|
+
${parts.role.trim()}`);
|
|
5078
|
+
}
|
|
5079
|
+
if (parts.task && parts.task.trim().length > 0) {
|
|
5080
|
+
sections.push(`Task:
|
|
5081
|
+
${parts.task.trim()}`);
|
|
5082
|
+
}
|
|
5083
|
+
if (parts.override && parts.override.trim().length > 0) {
|
|
5084
|
+
sections.push(parts.override.trim());
|
|
5085
|
+
}
|
|
5086
|
+
return sections.join("\n\n");
|
|
5087
|
+
}
|
|
5088
|
+
function rosterSummaryFromConfigs(roster) {
|
|
5089
|
+
const lines = [];
|
|
5090
|
+
for (const [roleId, cfg] of Object.entries(roster)) {
|
|
5091
|
+
const tag = cfg.provider && cfg.model ? ` (${cfg.provider}/${cfg.model})` : "";
|
|
5092
|
+
const headline = cfg.prompt ? (cfg.prompt.split("\n").find((l) => l.trim().length > 0) ?? "").trim().slice(0, 80) : "";
|
|
5093
|
+
const tail = headline ? ` \u2014 ${headline}` : "";
|
|
5094
|
+
lines.push(`- ${roleId}: ${cfg.name}${tag}${tail}`);
|
|
5095
|
+
}
|
|
5096
|
+
return lines.join("\n");
|
|
5097
|
+
}
|
|
5098
|
+
|
|
5099
|
+
// src/defaults/director.ts
|
|
5100
|
+
var Director = class {
|
|
5101
|
+
id;
|
|
5102
|
+
fleet;
|
|
5103
|
+
usage;
|
|
5104
|
+
/**
|
|
5105
|
+
* Director-side bridge endpoint. Subagents are wired to the same
|
|
5106
|
+
* in-memory transport so the director can `ask()` them synchronously
|
|
5107
|
+
* and they can `send()` progress back. Exposed so external code (e.g.
|
|
5108
|
+
* the TUI) can subscribe to inbound messages.
|
|
5109
|
+
*/
|
|
5110
|
+
bridge;
|
|
5111
|
+
transport;
|
|
5112
|
+
coordinator;
|
|
5113
|
+
/** Resolves with the matching `TaskResult` the first time the
|
|
5114
|
+
* coordinator emits `task.completed` for a given task id. Each entry
|
|
5115
|
+
* is created lazily on first poll/await and cleared once consumed. */
|
|
5116
|
+
taskWaiters = /* @__PURE__ */ new Map();
|
|
5117
|
+
/** Cache of completed results in case the consumer asks AFTER the
|
|
5118
|
+
* coordinator already fired the event — `awaitTasks(['t-1'])` after
|
|
5119
|
+
* t-1 finished should resolve immediately, not hang. */
|
|
5120
|
+
completed = /* @__PURE__ */ new Map();
|
|
5121
|
+
/** Per-subagent provider/model metadata, captured at spawn time so the
|
|
5122
|
+
* FleetUsageAggregator's metaLookup can surface readable rows. */
|
|
5123
|
+
subagentMeta = /* @__PURE__ */ new Map();
|
|
5124
|
+
priceLookups = /* @__PURE__ */ new Map();
|
|
5125
|
+
/** Bridge endpoints we created per subagent (so we can `stop()` them
|
|
5126
|
+
* on shutdown and free transport subscriptions). */
|
|
5127
|
+
subagentBridges = /* @__PURE__ */ new Map();
|
|
5128
|
+
/** Tracks per-spawn config + assigned task ids for manifest writing. */
|
|
5129
|
+
manifestEntries = /* @__PURE__ */ new Map();
|
|
5130
|
+
manifestPath;
|
|
5131
|
+
roster;
|
|
5132
|
+
directorPreamble;
|
|
5133
|
+
subagentBaseline;
|
|
5134
|
+
constructor(opts) {
|
|
5135
|
+
this.id = opts.config.coordinatorId || randomUUID();
|
|
5136
|
+
this.manifestPath = opts.manifestPath;
|
|
5137
|
+
this.roster = opts.roster;
|
|
5138
|
+
this.directorPreamble = opts.directorPreamble ?? DEFAULT_DIRECTOR_PREAMBLE;
|
|
5139
|
+
this.subagentBaseline = opts.subagentBaseline ?? DEFAULT_SUBAGENT_BASELINE;
|
|
5140
|
+
this.transport = new InMemoryBridgeTransport();
|
|
5141
|
+
this.bridge = new InMemoryAgentBridge(
|
|
5142
|
+
{ agentId: this.id, coordinatorId: this.id },
|
|
5143
|
+
this.transport
|
|
5144
|
+
);
|
|
5145
|
+
this.fleet = new FleetBus();
|
|
5146
|
+
this.usage = new FleetUsageAggregator(
|
|
5147
|
+
this.fleet,
|
|
5148
|
+
(id) => this.priceLookups.get(id),
|
|
5149
|
+
(id) => this.subagentMeta.get(id)
|
|
5150
|
+
);
|
|
5151
|
+
this.coordinator = new DefaultMultiAgentCoordinator(
|
|
5152
|
+
{ ...opts.config, coordinatorId: this.id },
|
|
5153
|
+
{ runner: opts.runner }
|
|
5154
|
+
);
|
|
5155
|
+
this.coordinator.on("task.completed", (payload) => {
|
|
5156
|
+
const r = payload.result;
|
|
5157
|
+
this.completed.set(r.taskId, r);
|
|
5158
|
+
const waiter = this.taskWaiters.get(r.taskId);
|
|
5159
|
+
if (waiter) {
|
|
5160
|
+
waiter.resolve(r);
|
|
5161
|
+
this.taskWaiters.delete(r.taskId);
|
|
5162
|
+
}
|
|
5163
|
+
});
|
|
5164
|
+
}
|
|
5165
|
+
/**
|
|
5166
|
+
* Spawn a subagent. Identical to the coordinator's `spawn()` but
|
|
5167
|
+
* captures provider/model metadata for the usage aggregator and
|
|
5168
|
+
* lets the FleetBus attach to the runner's EventBus when the task
|
|
5169
|
+
* actually runs (see `attachSubagentBus`).
|
|
5170
|
+
*
|
|
5171
|
+
* Caller-supplied `priceLookup` is optional but recommended — without
|
|
5172
|
+
* it the `cost` column in `usage.snapshot()` stays at 0.
|
|
5173
|
+
*/
|
|
5174
|
+
async spawn(config, priceLookup) {
|
|
5175
|
+
const result = await this.coordinator.spawn(config);
|
|
5176
|
+
this.subagentMeta.set(result.subagentId, {
|
|
5177
|
+
provider: config.provider,
|
|
5178
|
+
model: config.model
|
|
5179
|
+
});
|
|
5180
|
+
if (priceLookup) this.priceLookups.set(result.subagentId, priceLookup);
|
|
5181
|
+
const subagentBridge = new InMemoryAgentBridge(
|
|
5182
|
+
{ agentId: result.subagentId, coordinatorId: this.id },
|
|
5183
|
+
this.transport
|
|
5184
|
+
);
|
|
5185
|
+
this.coordinator.setSubagentBridge(result.subagentId, subagentBridge);
|
|
5186
|
+
this.subagentBridges.set(result.subagentId, subagentBridge);
|
|
5187
|
+
this.manifestEntries.set(result.subagentId, {
|
|
5188
|
+
subagentId: result.subagentId,
|
|
5189
|
+
name: config.name,
|
|
5190
|
+
role: config.role,
|
|
5191
|
+
provider: config.provider,
|
|
5192
|
+
model: config.model,
|
|
5193
|
+
taskIds: []
|
|
5194
|
+
});
|
|
5195
|
+
return result.subagentId;
|
|
5196
|
+
}
|
|
5197
|
+
/**
|
|
5198
|
+
* Synchronously ask a subagent something via the bridge. Sends a
|
|
5199
|
+
* `task` message addressed to the subagent and awaits a matching
|
|
5200
|
+
* reply (matched by message id). Subagent runners that handle these
|
|
5201
|
+
* requests subscribe to `ctx.bridge` and reply with a message whose
|
|
5202
|
+
* `id` equals the incoming request's id (see `InMemoryAgentBridge`'s
|
|
5203
|
+
* `request<T>` implementation).
|
|
5204
|
+
*
|
|
5205
|
+
* Returns the response payload directly (the bridge wrapper is
|
|
5206
|
+
* unwrapped for ergonomics). Times out after `timeoutMs` (default
|
|
5207
|
+
* matches the bridge's own default of 30s) — surface those rejections
|
|
5208
|
+
* to the caller as actionable errors instead of letting tools hang.
|
|
5209
|
+
*/
|
|
5210
|
+
async ask(subagentId, payload, timeoutMs) {
|
|
5211
|
+
if (!this.subagentBridges.has(subagentId)) {
|
|
5212
|
+
throw new Error(
|
|
5213
|
+
`ask: unknown subagent "${subagentId}" (spawn() it first; current fleet: ${Array.from(this.subagentBridges.keys()).join(", ") || "(empty)"})`
|
|
5214
|
+
);
|
|
5215
|
+
}
|
|
5216
|
+
const msg = {
|
|
5217
|
+
id: randomUUID(),
|
|
5218
|
+
type: "task",
|
|
5219
|
+
from: this.id,
|
|
5220
|
+
to: subagentId,
|
|
5221
|
+
payload,
|
|
5222
|
+
timestamp: Date.now(),
|
|
5223
|
+
priority: "normal"
|
|
5224
|
+
};
|
|
5225
|
+
const reply = await this.bridge.request(msg, timeoutMs);
|
|
5226
|
+
return reply.payload;
|
|
5227
|
+
}
|
|
5228
|
+
/**
|
|
5229
|
+
* Read completed task results and format them as a structured text
|
|
5230
|
+
* block the director's LLM can paste into its own context. The
|
|
5231
|
+
* Director keeps every completed `TaskResult` in `completed` so this
|
|
5232
|
+
* is a pure read — no bridge round-trip, cheap to call.
|
|
5233
|
+
*
|
|
5234
|
+
* The returned string is intentionally markdown-flavored: headers per
|
|
5235
|
+
* subagent, a one-line meta row (iter / tools / ms), and the task's
|
|
5236
|
+
* result text. Pass `style: 'json'` for a programmatic shape instead
|
|
5237
|
+
* (useful when the director model is doing structured-output work).
|
|
5238
|
+
*/
|
|
5239
|
+
rollUp(taskIds, style = "markdown") {
|
|
5240
|
+
const rows = taskIds.map((id) => this.completed.get(id)).filter(
|
|
5241
|
+
(r) => !!r
|
|
5242
|
+
);
|
|
5243
|
+
if (style === "json") {
|
|
5244
|
+
return JSON.stringify(
|
|
5245
|
+
rows.map((r) => ({
|
|
5246
|
+
taskId: r.taskId,
|
|
5247
|
+
subagentId: r.subagentId,
|
|
5248
|
+
status: r.status,
|
|
5249
|
+
iterations: r.iterations,
|
|
5250
|
+
toolCalls: r.toolCalls,
|
|
5251
|
+
durationMs: r.durationMs,
|
|
5252
|
+
result: r.result,
|
|
5253
|
+
error: r.error
|
|
5254
|
+
})),
|
|
5255
|
+
null,
|
|
5256
|
+
2
|
|
5257
|
+
);
|
|
5258
|
+
}
|
|
5259
|
+
if (rows.length === 0) {
|
|
5260
|
+
return "_No completed tasks for the requested ids \u2014 try waiting first._";
|
|
5261
|
+
}
|
|
5262
|
+
const lines = [];
|
|
5263
|
+
for (const r of rows) {
|
|
5264
|
+
const meta = this.subagentMeta.get(r.subagentId);
|
|
5265
|
+
const tag = meta?.provider && meta?.model ? ` \xB7 ${meta.provider}/${meta.model}` : "";
|
|
5266
|
+
lines.push(`### ${r.subagentId}${tag}`);
|
|
5267
|
+
lines.push(
|
|
5268
|
+
`_${r.status} \u2014 ${r.iterations} iter \xB7 ${r.toolCalls} tools \xB7 ${r.durationMs}ms_`
|
|
5269
|
+
);
|
|
5270
|
+
lines.push("");
|
|
5271
|
+
if (r.error) lines.push(`**Error:** ${r.error}`);
|
|
5272
|
+
else if (typeof r.result === "string") lines.push(r.result);
|
|
5273
|
+
else if (r.result !== void 0) lines.push("```json\n" + JSON.stringify(r.result, null, 2) + "\n```");
|
|
5274
|
+
else lines.push("_(no output)_");
|
|
5275
|
+
lines.push("");
|
|
5276
|
+
}
|
|
5277
|
+
return lines.join("\n").trimEnd();
|
|
5278
|
+
}
|
|
5279
|
+
/**
|
|
5280
|
+
* Write the fleet manifest to `manifestPath`. Returns the path written
|
|
5281
|
+
* or null when no path was configured. Captures every spawn + its
|
|
5282
|
+
* assigned tasks — paired with per-subagent JSONLs, this is enough to
|
|
5283
|
+
* replay an entire director run.
|
|
5284
|
+
*/
|
|
5285
|
+
async writeManifest() {
|
|
5286
|
+
if (!this.manifestPath) return null;
|
|
5287
|
+
const manifest = {
|
|
5288
|
+
directorRunId: this.id,
|
|
5289
|
+
writtenAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5290
|
+
children: Array.from(this.manifestEntries.values()).map((e) => ({
|
|
5291
|
+
...e,
|
|
5292
|
+
// Surface final status from `completed` when available — manifest
|
|
5293
|
+
// becomes much more useful for replay when it carries the
|
|
5294
|
+
// success/failure state.
|
|
5295
|
+
results: e.taskIds.map((tid) => {
|
|
5296
|
+
const r = this.completed.get(tid);
|
|
5297
|
+
return r ? {
|
|
5298
|
+
taskId: tid,
|
|
5299
|
+
status: r.status,
|
|
5300
|
+
iterations: r.iterations,
|
|
5301
|
+
toolCalls: r.toolCalls,
|
|
5302
|
+
durationMs: r.durationMs
|
|
5303
|
+
} : { taskId: tid, status: "pending" };
|
|
5304
|
+
})
|
|
5305
|
+
})),
|
|
5306
|
+
usage: this.usage.snapshot()
|
|
5307
|
+
};
|
|
5308
|
+
await fsp.mkdir(path2.dirname(this.manifestPath), { recursive: true });
|
|
5309
|
+
await fsp.writeFile(this.manifestPath, JSON.stringify(manifest, null, 2), { mode: 384 });
|
|
5310
|
+
return this.manifestPath;
|
|
5311
|
+
}
|
|
5312
|
+
/**
|
|
5313
|
+
* Tear down the director: stop every subagent, close every bridge
|
|
5314
|
+
* endpoint, and (when configured) write the final manifest. Idempotent
|
|
5315
|
+
* — calling shutdown twice is a no-op on the second invocation.
|
|
5316
|
+
*/
|
|
5317
|
+
async shutdown() {
|
|
5318
|
+
await this.coordinator.stopAll();
|
|
5319
|
+
for (const b of this.subagentBridges.values()) {
|
|
5320
|
+
await b.stop().catch(() => void 0);
|
|
5321
|
+
}
|
|
5322
|
+
this.subagentBridges.clear();
|
|
5323
|
+
await this.bridge.stop().catch(() => void 0);
|
|
5324
|
+
if (this.manifestPath) await this.writeManifest().catch(() => void 0);
|
|
5325
|
+
}
|
|
5326
|
+
/**
|
|
5327
|
+
* Hand a task to the coordinator. Returns the assigned task id so
|
|
5328
|
+
* callers can wait on it via `awaitTasks([id])`. The coordinator's
|
|
5329
|
+
* concurrency limit applies — the task may queue before running.
|
|
5330
|
+
*/
|
|
5331
|
+
async assign(task) {
|
|
5332
|
+
const taskWithId = task.id ? task : { ...task, id: randomUUID() };
|
|
5333
|
+
if (task.subagentId) {
|
|
5334
|
+
const entry = this.manifestEntries.get(task.subagentId);
|
|
5335
|
+
if (entry) entry.taskIds.push(taskWithId.id);
|
|
5336
|
+
}
|
|
5337
|
+
await this.coordinator.assign(taskWithId);
|
|
5338
|
+
return taskWithId.id;
|
|
5339
|
+
}
|
|
5340
|
+
/**
|
|
5341
|
+
* Block until every task id resolves. Returns results in the same
|
|
5342
|
+
* order as the input. If any task hasn't completed by the time this
|
|
5343
|
+
* is called, the promise hangs until it does — pair with a timeout
|
|
5344
|
+
* at the caller if that's a concern. Resolves immediately for ids
|
|
5345
|
+
* whose results were already cached.
|
|
5346
|
+
*/
|
|
5347
|
+
awaitTasks(taskIds) {
|
|
5348
|
+
return Promise.all(taskIds.map((id) => {
|
|
5349
|
+
const cached = this.completed.get(id);
|
|
5350
|
+
if (cached) return cached;
|
|
5351
|
+
const existing = this.taskWaiters.get(id);
|
|
5352
|
+
if (existing) return existing.promise;
|
|
5353
|
+
let resolve4;
|
|
5354
|
+
const promise = new Promise((res) => {
|
|
5355
|
+
resolve4 = res;
|
|
5356
|
+
});
|
|
5357
|
+
this.taskWaiters.set(id, { promise, resolve: resolve4 });
|
|
5358
|
+
return promise;
|
|
5359
|
+
}));
|
|
5360
|
+
}
|
|
5361
|
+
async terminate(subagentId) {
|
|
5362
|
+
await this.coordinator.stop(subagentId);
|
|
5363
|
+
}
|
|
5364
|
+
async terminateAll() {
|
|
5365
|
+
await this.coordinator.stopAll();
|
|
5366
|
+
}
|
|
5367
|
+
status() {
|
|
5368
|
+
return this.coordinator.getStatus();
|
|
5369
|
+
}
|
|
5370
|
+
/**
|
|
5371
|
+
* Subscribe to coordinator events. Currently only `task.completed` is
|
|
5372
|
+
* exposed (the others are internal lifecycle). Returns an unsubscribe
|
|
5373
|
+
* function. External callers (e.g. the CLI's `MultiAgentHost`) use this
|
|
5374
|
+
* to drive their own pending/results tracking without poking the
|
|
5375
|
+
* coordinator directly.
|
|
5376
|
+
*/
|
|
5377
|
+
on(event, handler) {
|
|
5378
|
+
this.coordinator.on(event, handler);
|
|
5379
|
+
return () => {
|
|
5380
|
+
this.coordinator.off(event, handler);
|
|
5381
|
+
};
|
|
5382
|
+
}
|
|
5383
|
+
/**
|
|
5384
|
+
* Snapshot of every task that has resolved (success, failed, timeout,
|
|
5385
|
+
* stopped) since the director started. Returned in completion order
|
|
5386
|
+
* via the internal map's iteration order. Used by `/fleet status` to
|
|
5387
|
+
* paint the completed table without reaching into private state.
|
|
5388
|
+
*/
|
|
5389
|
+
completedResults() {
|
|
5390
|
+
return Array.from(this.completed.values());
|
|
5391
|
+
}
|
|
5392
|
+
snapshot() {
|
|
5393
|
+
return this.usage.snapshot();
|
|
5394
|
+
}
|
|
5395
|
+
/**
|
|
5396
|
+
* Compose the leader/director-agent system prompt: fleet preamble +
|
|
5397
|
+
* (optional) roster summary + user base prompt. Pass the result to your
|
|
5398
|
+
* leader Agent's `ctx.systemPrompt` when constructing it.
|
|
5399
|
+
*
|
|
5400
|
+
* `basePrompt` defaults to `config.leaderSystemPrompt` so callers can
|
|
5401
|
+
* use the no-arg form when the multi-agent config already carries it.
|
|
5402
|
+
*/
|
|
5403
|
+
leaderSystemPrompt(basePrompt) {
|
|
5404
|
+
return composeDirectorPrompt({
|
|
5405
|
+
basePrompt: basePrompt ?? this.coordinator.config.leaderSystemPrompt,
|
|
5406
|
+
directorPreamble: this.directorPreamble,
|
|
5407
|
+
rosterSummary: this.roster ? rosterSummaryFromConfigs(this.roster) : void 0
|
|
5408
|
+
});
|
|
5409
|
+
}
|
|
5410
|
+
/**
|
|
5411
|
+
* Compose a subagent's system prompt for a given `SubagentConfig`:
|
|
5412
|
+
* baseline + role + task + per-spawn override. Returned by value — does
|
|
5413
|
+
* not mutate the config. Factories (the user-supplied `AgentFactory`)
|
|
5414
|
+
* should call this when building each subagent's Agent so the bridge
|
|
5415
|
+
* contract, role context, and override are all surfaced.
|
|
5416
|
+
*
|
|
5417
|
+
* When `taskBrief` is omitted the Task section is dropped. Pass the
|
|
5418
|
+
* actual task description here to reinforce it in the system prompt
|
|
5419
|
+
* (the runner already passes it as user input — duplicating in the
|
|
5420
|
+
* system prompt is optional but improves anchoring on small models).
|
|
5421
|
+
*/
|
|
5422
|
+
subagentSystemPrompt(config, taskBrief) {
|
|
5423
|
+
return composeSubagentPrompt({
|
|
5424
|
+
baseline: this.subagentBaseline,
|
|
5425
|
+
role: config.prompt,
|
|
5426
|
+
task: taskBrief,
|
|
5427
|
+
override: config.systemPromptOverride
|
|
5428
|
+
});
|
|
5429
|
+
}
|
|
5430
|
+
/**
|
|
5431
|
+
* Build the tool set the LLM-driven director uses to orchestrate.
|
|
5432
|
+
* Returns an array of `Tool` definitions; register these on the
|
|
5433
|
+
* director's `Agent` to expose `spawn_subagent`, `assign_task`, etc.
|
|
5434
|
+
* Each tool's `execute()` delegates straight to the matching method
|
|
5435
|
+
* above.
|
|
5436
|
+
*
|
|
5437
|
+
* Tools all carry `permission: 'auto'` — the *user* has already
|
|
5438
|
+
* approved running the director when they kicked off the run, so
|
|
5439
|
+
* gating individual orchestration calls behind a confirm prompt
|
|
5440
|
+
* would just be noise. The actual subagent tools they spawn are
|
|
5441
|
+
* still permission-checked normally.
|
|
5442
|
+
*/
|
|
5443
|
+
tools(roster) {
|
|
5444
|
+
const t2 = [
|
|
5445
|
+
makeSpawnTool(this, roster),
|
|
5446
|
+
makeAssignTool(this),
|
|
5447
|
+
makeAwaitTasksTool(this),
|
|
5448
|
+
makeAskTool(this),
|
|
5449
|
+
makeRollUpTool(this),
|
|
5450
|
+
makeTerminateTool(this),
|
|
5451
|
+
makeFleetStatusTool(this),
|
|
5452
|
+
makeFleetUsageTool(this)
|
|
5453
|
+
];
|
|
5454
|
+
return t2;
|
|
5455
|
+
}
|
|
5456
|
+
};
|
|
5457
|
+
function makeSpawnTool(director, roster) {
|
|
5458
|
+
const inputSchema = {
|
|
5459
|
+
type: "object",
|
|
5460
|
+
properties: {
|
|
5461
|
+
role: { type: "string", description: "Roster role id (preferred). When set, the spawn uses the matching config from the roster and ignores other fields." },
|
|
5462
|
+
name: { type: "string", description: "Display name for the subagent. Required when not using roster." },
|
|
5463
|
+
provider: { type: "string", description: 'Provider id (e.g. "anthropic", "openai"). Defaults to the leader provider when omitted.' },
|
|
5464
|
+
model: { type: "string", description: "Model id within the provider. Defaults to the leader model when omitted." },
|
|
5465
|
+
systemPromptOverride: { type: "string", description: "Extra prompt text appended after the role-base prompt." },
|
|
5466
|
+
maxIterations: { type: "number" },
|
|
5467
|
+
maxToolCalls: { type: "number" },
|
|
5468
|
+
maxCostUsd: { type: "number" }
|
|
5469
|
+
},
|
|
5470
|
+
required: []
|
|
5471
|
+
};
|
|
5472
|
+
return {
|
|
5473
|
+
name: "spawn_subagent",
|
|
5474
|
+
description: "Create a new subagent under this director. Returns the subagent id. Use this when you need a worker with a specific provider, model, or role to handle a piece of the plan.",
|
|
5475
|
+
usageHint: "Either pass `role` (matches the roster) OR pass `name` + optional `provider`/`model`. Returns `{ subagentId }`.",
|
|
5476
|
+
permission: "auto",
|
|
5477
|
+
mutating: false,
|
|
5478
|
+
inputSchema,
|
|
5479
|
+
async execute(input) {
|
|
5480
|
+
const i = input ?? {};
|
|
5481
|
+
const role = typeof i.role === "string" ? i.role : void 0;
|
|
5482
|
+
const base = role && roster ? roster[role] : void 0;
|
|
5483
|
+
if (role && !base) {
|
|
5484
|
+
return { error: `unknown role "${role}". roster has: ${roster ? Object.keys(roster).join(", ") : "(empty)"}` };
|
|
5485
|
+
}
|
|
5486
|
+
const cfg = {
|
|
5487
|
+
...base ?? { name: i.name ?? "subagent" }
|
|
5488
|
+
};
|
|
5489
|
+
if (typeof i.name === "string") cfg.name = i.name;
|
|
5490
|
+
if (typeof i.provider === "string") cfg.provider = i.provider;
|
|
5491
|
+
if (typeof i.model === "string") cfg.model = i.model;
|
|
5492
|
+
if (typeof i.systemPromptOverride === "string") cfg.systemPromptOverride = i.systemPromptOverride;
|
|
5493
|
+
if (typeof i.maxIterations === "number") cfg.maxIterations = i.maxIterations;
|
|
5494
|
+
if (typeof i.maxToolCalls === "number") cfg.maxToolCalls = i.maxToolCalls;
|
|
5495
|
+
if (typeof i.maxCostUsd === "number") cfg.maxCostUsd = i.maxCostUsd;
|
|
5496
|
+
const subagentId = await director.spawn(cfg);
|
|
5497
|
+
return { subagentId, provider: cfg.provider, model: cfg.model, name: cfg.name };
|
|
5498
|
+
}
|
|
5499
|
+
};
|
|
5500
|
+
}
|
|
5501
|
+
function makeAssignTool(director) {
|
|
5502
|
+
const inputSchema = {
|
|
5503
|
+
type: "object",
|
|
5504
|
+
properties: {
|
|
5505
|
+
subagentId: { type: "string", description: "Target subagent id. Required." },
|
|
5506
|
+
description: { type: "string", description: "The task in natural language \u2014 what you want this subagent to do." },
|
|
5507
|
+
maxToolCalls: { type: "number", description: "Optional per-task tool-call budget override." },
|
|
5508
|
+
timeoutMs: { type: "number", description: "Optional per-task timeout in ms." }
|
|
5509
|
+
},
|
|
5510
|
+
required: ["subagentId", "description"]
|
|
5511
|
+
};
|
|
5512
|
+
return {
|
|
5513
|
+
name: "assign_task",
|
|
5514
|
+
description: "Hand a task to a previously spawned subagent. Returns the task id \u2014 pass it to `await_tasks` to block on completion.",
|
|
5515
|
+
permission: "auto",
|
|
5516
|
+
mutating: false,
|
|
5517
|
+
inputSchema,
|
|
5518
|
+
async execute(input) {
|
|
5519
|
+
const i = input;
|
|
5520
|
+
const task = {
|
|
5521
|
+
id: randomUUID(),
|
|
5522
|
+
description: i.description,
|
|
5523
|
+
subagentId: i.subagentId,
|
|
5524
|
+
maxToolCalls: i.maxToolCalls,
|
|
5525
|
+
timeoutMs: i.timeoutMs
|
|
5526
|
+
};
|
|
5527
|
+
const taskId = await director.assign(task);
|
|
5528
|
+
return { taskId, subagentId: i.subagentId };
|
|
5529
|
+
}
|
|
5530
|
+
};
|
|
5531
|
+
}
|
|
5532
|
+
function makeAwaitTasksTool(director) {
|
|
5533
|
+
const inputSchema = {
|
|
5534
|
+
type: "object",
|
|
5535
|
+
properties: {
|
|
5536
|
+
taskIds: {
|
|
5537
|
+
type: "array",
|
|
5538
|
+
items: { type: "string" },
|
|
5539
|
+
description: "One or more task ids returned by `assign_task`. The call blocks until every id resolves."
|
|
5540
|
+
}
|
|
5541
|
+
},
|
|
5542
|
+
required: ["taskIds"]
|
|
5543
|
+
};
|
|
5544
|
+
return {
|
|
5545
|
+
name: "await_tasks",
|
|
5546
|
+
description: "Block until every named task completes. Returns the array of TaskResult \u2014 use this to gather subagent output before deciding the next step.",
|
|
5547
|
+
permission: "auto",
|
|
5548
|
+
mutating: false,
|
|
5549
|
+
inputSchema,
|
|
5550
|
+
async execute(input) {
|
|
5551
|
+
const i = input;
|
|
5552
|
+
const results = await director.awaitTasks(i.taskIds);
|
|
5553
|
+
return { results };
|
|
5554
|
+
}
|
|
5555
|
+
};
|
|
5556
|
+
}
|
|
5557
|
+
function makeAskTool(director) {
|
|
5558
|
+
const inputSchema = {
|
|
5559
|
+
type: "object",
|
|
5560
|
+
properties: {
|
|
5561
|
+
subagentId: { type: "string", description: "Subagent to ask. Must be a previously spawned id." },
|
|
5562
|
+
question: { type: "string", description: "The question or instruction. Sent as the bridge message payload." },
|
|
5563
|
+
timeoutMs: { type: "number", description: "Optional timeout in ms (default 30s)." }
|
|
5564
|
+
},
|
|
5565
|
+
required: ["subagentId", "question"]
|
|
5566
|
+
};
|
|
5567
|
+
return {
|
|
5568
|
+
name: "ask_subagent",
|
|
5569
|
+
description: "Synchronously ask a subagent a question. Blocks until the subagent replies via the bridge (or the timeout fires). Use this when you need a one-shot answer without spawning a fresh task.",
|
|
5570
|
+
permission: "auto",
|
|
5571
|
+
mutating: false,
|
|
5572
|
+
inputSchema,
|
|
5573
|
+
async execute(input) {
|
|
5574
|
+
const i = input;
|
|
5575
|
+
try {
|
|
5576
|
+
const answer = await director.ask(i.subagentId, { question: i.question }, i.timeoutMs);
|
|
5577
|
+
return { ok: true, answer };
|
|
5578
|
+
} catch (err) {
|
|
5579
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
5580
|
+
}
|
|
5581
|
+
}
|
|
5582
|
+
};
|
|
5583
|
+
}
|
|
5584
|
+
function makeRollUpTool(director) {
|
|
5585
|
+
const inputSchema = {
|
|
5586
|
+
type: "object",
|
|
5587
|
+
properties: {
|
|
5588
|
+
taskIds: {
|
|
5589
|
+
type: "array",
|
|
5590
|
+
items: { type: "string" },
|
|
5591
|
+
description: "Completed task ids to aggregate. Pass the ids returned by previous `assign_task` calls."
|
|
5592
|
+
},
|
|
5593
|
+
style: {
|
|
5594
|
+
type: "string",
|
|
5595
|
+
enum: ["markdown", "json"],
|
|
5596
|
+
description: "Output flavor \u2014 markdown (default) for in-prompt summarization, json for structured downstream processing."
|
|
5597
|
+
}
|
|
5598
|
+
},
|
|
5599
|
+
required: ["taskIds"]
|
|
5600
|
+
};
|
|
5601
|
+
return {
|
|
5602
|
+
name: "roll_up",
|
|
5603
|
+
description: "Aggregate completed task results into a single formatted summary. Use this after `await_tasks` to fold subagent outputs back into the director's context before deciding the next step.",
|
|
5604
|
+
permission: "auto",
|
|
5605
|
+
mutating: false,
|
|
5606
|
+
inputSchema,
|
|
5607
|
+
async execute(input) {
|
|
5608
|
+
const i = input;
|
|
5609
|
+
const summary = director.rollUp(i.taskIds, i.style ?? "markdown");
|
|
5610
|
+
return { summary, count: i.taskIds.length };
|
|
5611
|
+
}
|
|
5612
|
+
};
|
|
5613
|
+
}
|
|
5614
|
+
function makeTerminateTool(director) {
|
|
5615
|
+
const inputSchema = {
|
|
5616
|
+
type: "object",
|
|
5617
|
+
properties: {
|
|
5618
|
+
subagentId: { type: "string", description: "Subagent to abort." }
|
|
5619
|
+
},
|
|
5620
|
+
required: ["subagentId"]
|
|
5621
|
+
};
|
|
5622
|
+
return {
|
|
5623
|
+
name: "terminate_subagent",
|
|
5624
|
+
description: 'Forcibly abort a subagent. Use sparingly \u2014 prefer waiting on the natural budget to expire. The current task (if any) ends with status "stopped".',
|
|
5625
|
+
permission: "auto",
|
|
5626
|
+
mutating: true,
|
|
5627
|
+
inputSchema,
|
|
5628
|
+
async execute(input) {
|
|
5629
|
+
const i = input;
|
|
5630
|
+
await director.terminate(i.subagentId);
|
|
5631
|
+
return { ok: true };
|
|
5632
|
+
}
|
|
5633
|
+
};
|
|
5634
|
+
}
|
|
5635
|
+
function makeFleetStatusTool(director) {
|
|
5636
|
+
return {
|
|
5637
|
+
name: "fleet_status",
|
|
5638
|
+
description: "Snapshot of the fleet \u2014 every subagent's current status, pending vs. completed task counts, and the running total iteration count. Cheap; call freely.",
|
|
5639
|
+
permission: "auto",
|
|
5640
|
+
mutating: false,
|
|
5641
|
+
inputSchema: { type: "object", properties: {}, required: [] },
|
|
5642
|
+
async execute() {
|
|
5643
|
+
return director.status();
|
|
5644
|
+
}
|
|
5645
|
+
};
|
|
5646
|
+
}
|
|
5647
|
+
function makeFleetUsageTool(director) {
|
|
5648
|
+
return {
|
|
5649
|
+
name: "fleet_usage",
|
|
5650
|
+
description: "Token + cost breakdown across the fleet, per-subagent and totals. Use this to reason about which workers to assign costly tasks to or when to wrap up to stay within budget.",
|
|
5651
|
+
permission: "auto",
|
|
5652
|
+
mutating: false,
|
|
5653
|
+
inputSchema: { type: "object", properties: {}, required: [] },
|
|
5654
|
+
async execute() {
|
|
5655
|
+
return director.snapshot();
|
|
5656
|
+
}
|
|
5657
|
+
};
|
|
5658
|
+
}
|
|
5659
|
+
function makeDirectorSessionFactory(opts) {
|
|
5660
|
+
const runId = opts.directorRunId ?? `${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-director`;
|
|
5661
|
+
let store;
|
|
5662
|
+
let dir;
|
|
5663
|
+
if (opts.store) {
|
|
5664
|
+
store = opts.store;
|
|
5665
|
+
dir = opts.sessionsRoot ? path2.join(opts.sessionsRoot, runId) : "(caller-managed)";
|
|
5666
|
+
} else if (opts.sessionsRoot) {
|
|
5667
|
+
dir = path2.join(opts.sessionsRoot, runId);
|
|
5668
|
+
store = new DefaultSessionStore({ dir });
|
|
5669
|
+
} else {
|
|
5670
|
+
throw new Error(
|
|
5671
|
+
"makeDirectorSessionFactory requires either `store` or `sessionsRoot`"
|
|
5672
|
+
);
|
|
5673
|
+
}
|
|
5674
|
+
return {
|
|
5675
|
+
dir,
|
|
5676
|
+
directorRunId: runId,
|
|
5677
|
+
async createSubagentSession({ subagentId, provider, model, title }) {
|
|
5678
|
+
return store.create({
|
|
5679
|
+
id: subagentId,
|
|
5680
|
+
title: title ?? subagentId,
|
|
5681
|
+
provider: provider ?? "unknown",
|
|
5682
|
+
model: model ?? "unknown"
|
|
5683
|
+
});
|
|
5684
|
+
}
|
|
5685
|
+
};
|
|
5686
|
+
}
|
|
5687
|
+
|
|
4552
5688
|
// src/defaults/autonomous-runner.ts
|
|
4553
5689
|
var DoneConditionChecker = class {
|
|
4554
5690
|
constructor(condition) {
|
|
4555
5691
|
this.condition = condition;
|
|
5692
|
+
this.compiledRegex = condition.type === "output_match" && condition.pattern ? new RegExp(condition.pattern) : null;
|
|
4556
5693
|
}
|
|
4557
5694
|
condition;
|
|
5695
|
+
compiledRegex;
|
|
4558
5696
|
check(state) {
|
|
4559
5697
|
switch (this.condition.type) {
|
|
4560
5698
|
case "iterations":
|
|
@@ -4568,11 +5706,8 @@ var DoneConditionChecker = class {
|
|
|
4568
5706
|
}
|
|
4569
5707
|
break;
|
|
4570
5708
|
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
|
-
}
|
|
5709
|
+
if (this.compiledRegex && state.lastOutput && this.compiledRegex.test(state.lastOutput)) {
|
|
5710
|
+
return { done: true, reason: `output matched pattern "${this.condition.pattern}"`, ...state };
|
|
4576
5711
|
}
|
|
4577
5712
|
break;
|
|
4578
5713
|
}
|
|
@@ -4591,6 +5726,16 @@ var AutonomousRunner = class {
|
|
|
4591
5726
|
stopped = false;
|
|
4592
5727
|
doneChecker;
|
|
4593
5728
|
async run() {
|
|
5729
|
+
const offToolExecuted = this.opts.agent.events?.on?.("tool.executed", () => {
|
|
5730
|
+
this.toolCalls++;
|
|
5731
|
+
});
|
|
5732
|
+
try {
|
|
5733
|
+
return await this.runLoop();
|
|
5734
|
+
} finally {
|
|
5735
|
+
offToolExecuted?.();
|
|
5736
|
+
}
|
|
5737
|
+
}
|
|
5738
|
+
async runLoop() {
|
|
4594
5739
|
while (!this.stopped) {
|
|
4595
5740
|
const check = this.doneChecker.check({
|
|
4596
5741
|
iterations: this.iterations,
|
|
@@ -4617,7 +5762,6 @@ var AutonomousRunner = class {
|
|
|
4617
5762
|
);
|
|
4618
5763
|
this.iterations++;
|
|
4619
5764
|
this.lastOutput = result.finalText;
|
|
4620
|
-
this.toolCalls++;
|
|
4621
5765
|
if (result.status === "failed" || result.status === "aborted") {
|
|
4622
5766
|
const failedResult = {
|
|
4623
5767
|
status: result.status,
|
|
@@ -4629,7 +5773,8 @@ var AutonomousRunner = class {
|
|
|
4629
5773
|
return failedResult;
|
|
4630
5774
|
}
|
|
4631
5775
|
} catch (e) {
|
|
4632
|
-
|
|
5776
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
5777
|
+
if (msg.includes("timeout")) {
|
|
4633
5778
|
const timeoutResult = {
|
|
4634
5779
|
status: "failed",
|
|
4635
5780
|
error: toWrongStackError(e),
|
|
@@ -4658,14 +5803,11 @@ var AutonomousRunner = class {
|
|
|
4658
5803
|
|
|
4659
5804
|
// src/defaults/spec-parser.ts
|
|
4660
5805
|
var SpecParser = class {
|
|
4661
|
-
constructor(opts = {}) {
|
|
4662
|
-
this.opts = opts;
|
|
4663
|
-
}
|
|
4664
|
-
opts;
|
|
4665
5806
|
parse(content) {
|
|
4666
5807
|
const lines = content.split("\n");
|
|
4667
5808
|
const sections = this.extractSections(lines);
|
|
4668
5809
|
const requirements = this.extractRequirements(lines);
|
|
5810
|
+
const now = Date.now();
|
|
4669
5811
|
return {
|
|
4670
5812
|
id: crypto.randomUUID(),
|
|
4671
5813
|
title: this.extractTitle(lines),
|
|
@@ -4674,8 +5816,8 @@ var SpecParser = class {
|
|
|
4674
5816
|
overview: this.extractOverview(lines),
|
|
4675
5817
|
sections,
|
|
4676
5818
|
requirements,
|
|
4677
|
-
createdAt:
|
|
4678
|
-
updatedAt:
|
|
5819
|
+
createdAt: now,
|
|
5820
|
+
updatedAt: now
|
|
4679
5821
|
};
|
|
4680
5822
|
}
|
|
4681
5823
|
extractTitle(lines) {
|
|
@@ -4767,20 +5909,13 @@ var SpecParser = class {
|
|
|
4767
5909
|
parseRequirementLine(line, id) {
|
|
4768
5910
|
const trimmed = line.trim();
|
|
4769
5911
|
if (!trimmed || trimmed.startsWith("#")) return null;
|
|
4770
|
-
const
|
|
4771
|
-
|
|
4772
|
-
"non-functional": "non-functional",
|
|
4773
|
-
"security": "security",
|
|
4774
|
-
"performance": "performance",
|
|
4775
|
-
"ux": "ux"
|
|
4776
|
-
};
|
|
5912
|
+
const lower = trimmed.toLowerCase();
|
|
5913
|
+
const types = ["functional", "non-functional", "security", "performance", "ux"];
|
|
4777
5914
|
let type = "functional";
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
if (trimmed.toLowerCase().includes(`[${key}]`)) {
|
|
4781
|
-
type = val;
|
|
4782
|
-
}
|
|
5915
|
+
for (const t2 of types) {
|
|
5916
|
+
if (lower.includes(`[${t2}]`)) type = t2;
|
|
4783
5917
|
}
|
|
5918
|
+
let priority = "medium";
|
|
4784
5919
|
if (trimmed.includes("[critical]") || trimmed.includes("[prio:high]")) {
|
|
4785
5920
|
priority = "critical";
|
|
4786
5921
|
} else if (trimmed.includes("[high]")) {
|
|
@@ -4870,9 +6005,10 @@ var SpecParser = class {
|
|
|
4870
6005
|
warnings.push({ path: `requirement.${req.id}`, message: "No acceptance criteria defined" });
|
|
4871
6006
|
}
|
|
4872
6007
|
}
|
|
6008
|
+
const reqIds = new Set(spec.requirements.map((r) => r.id));
|
|
4873
6009
|
const blockedByIds = new Set(spec.requirements.flatMap((r) => r.blockedBy ?? []));
|
|
4874
6010
|
for (const id of blockedByIds) {
|
|
4875
|
-
if (!
|
|
6011
|
+
if (!reqIds.has(id)) {
|
|
4876
6012
|
errors.push({ path: "requirements", message: `BlockedBy references non-existent requirement: ${id}` });
|
|
4877
6013
|
}
|
|
4878
6014
|
}
|
|
@@ -4902,25 +6038,21 @@ var TaskGenerator = class {
|
|
|
4902
6038
|
status: "pending"
|
|
4903
6039
|
});
|
|
4904
6040
|
}
|
|
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);
|
|
6041
|
+
const byPriority = {
|
|
6042
|
+
critical: [],
|
|
6043
|
+
high: [],
|
|
6044
|
+
medium: [],
|
|
6045
|
+
low: []
|
|
6046
|
+
};
|
|
6047
|
+
for (const req of spec.requirements) {
|
|
6048
|
+
const bucket = byPriority[req.priority] ?? byPriority.medium;
|
|
6049
|
+
bucket.push(req);
|
|
4920
6050
|
}
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
6051
|
+
const order = ["critical", "high", "medium", "low"];
|
|
6052
|
+
for (const p of order) {
|
|
6053
|
+
for (const req of byPriority[p]) {
|
|
6054
|
+
this.opts.taskTracker.addNode(this.createTaskFromRequirement(req));
|
|
6055
|
+
}
|
|
4924
6056
|
}
|
|
4925
6057
|
if (spec.apiEndpoints && spec.apiEndpoints.length > 0) {
|
|
4926
6058
|
const apiParent = this.opts.taskTracker.addNode({
|
|
@@ -4954,17 +6086,15 @@ var TaskGenerator = class {
|
|
|
4954
6086
|
});
|
|
4955
6087
|
return graph;
|
|
4956
6088
|
}
|
|
4957
|
-
createTaskFromRequirement(req
|
|
4958
|
-
const type = this.mapRequirementType(req.type);
|
|
4959
|
-
const tags = [req.type, req.priority];
|
|
6089
|
+
createTaskFromRequirement(req) {
|
|
4960
6090
|
return {
|
|
4961
6091
|
title: req.description,
|
|
4962
|
-
description: this.buildDescription(req
|
|
4963
|
-
type,
|
|
4964
|
-
priority:
|
|
6092
|
+
description: this.buildDescription(req),
|
|
6093
|
+
type: this.mapRequirementType(req.type),
|
|
6094
|
+
priority: req.priority,
|
|
4965
6095
|
status: "pending",
|
|
4966
6096
|
specRequirementId: req.id,
|
|
4967
|
-
tags,
|
|
6097
|
+
tags: [req.type, req.priority],
|
|
4968
6098
|
estimateHours: this.estimateHours(req)
|
|
4969
6099
|
};
|
|
4970
6100
|
}
|
|
@@ -4979,7 +6109,7 @@ var TaskGenerator = class {
|
|
|
4979
6109
|
estimateHours: this.estimateForEndpoint(endpoint)
|
|
4980
6110
|
};
|
|
4981
6111
|
}
|
|
4982
|
-
buildDescription(req
|
|
6112
|
+
buildDescription(req) {
|
|
4983
6113
|
const lines = [
|
|
4984
6114
|
req.description,
|
|
4985
6115
|
"",
|
|
@@ -5013,20 +6143,6 @@ var TaskGenerator = class {
|
|
|
5013
6143
|
return "feature";
|
|
5014
6144
|
}
|
|
5015
6145
|
}
|
|
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
6146
|
estimateHours(req) {
|
|
5031
6147
|
switch (req.priority) {
|
|
5032
6148
|
case "critical":
|
|
@@ -5136,40 +6252,40 @@ var TaskTracker = class {
|
|
|
5136
6252
|
this.graph.rootNodes.push(newNode.id);
|
|
5137
6253
|
}
|
|
5138
6254
|
this.graph.updatedAt = now;
|
|
5139
|
-
this.
|
|
6255
|
+
this.persist();
|
|
5140
6256
|
return newNode;
|
|
5141
6257
|
}
|
|
5142
6258
|
addEdge(from, to, type = "depends_on") {
|
|
5143
6259
|
if (!this.graph) throw new Error("No graph loaded");
|
|
5144
|
-
|
|
6260
|
+
this.graph.edges.push({
|
|
5145
6261
|
id: crypto.randomUUID(),
|
|
5146
6262
|
from,
|
|
5147
6263
|
to,
|
|
5148
6264
|
type
|
|
5149
|
-
};
|
|
5150
|
-
this.graph.edges.push(edge);
|
|
6265
|
+
});
|
|
5151
6266
|
this.graph.updatedAt = Date.now();
|
|
5152
|
-
this.
|
|
6267
|
+
this.persist();
|
|
5153
6268
|
}
|
|
5154
6269
|
updateNodeStatus(id, status, reason) {
|
|
5155
6270
|
if (!this.graph) throw new Error("No graph loaded");
|
|
5156
6271
|
const node = this.graph.nodes.get(id);
|
|
5157
6272
|
if (!node) throw new Error(`Node ${id} not found`);
|
|
5158
6273
|
const from = node.status;
|
|
6274
|
+
const now = Date.now();
|
|
5159
6275
|
node.status = status;
|
|
5160
|
-
node.updatedAt =
|
|
6276
|
+
node.updatedAt = now;
|
|
5161
6277
|
if (status === "completed") {
|
|
5162
|
-
node.completedAt =
|
|
6278
|
+
node.completedAt = now;
|
|
5163
6279
|
}
|
|
5164
|
-
this.transitions.push({ from, to: status, timestamp:
|
|
6280
|
+
this.transitions.push({ from, to: status, timestamp: now, reason });
|
|
5165
6281
|
if (status === "completed") {
|
|
5166
6282
|
this.unblockDependents(id);
|
|
5167
6283
|
}
|
|
5168
6284
|
if (status === "in_progress") {
|
|
5169
6285
|
this.checkAndBlockIfNeeded(id);
|
|
5170
6286
|
}
|
|
5171
|
-
this.graph.updatedAt =
|
|
5172
|
-
this.
|
|
6287
|
+
this.graph.updatedAt = now;
|
|
6288
|
+
this.persist();
|
|
5173
6289
|
}
|
|
5174
6290
|
getNode(id) {
|
|
5175
6291
|
return this.graph?.nodes.get(id);
|
|
@@ -5190,9 +6306,7 @@ var TaskTracker = class {
|
|
|
5190
6306
|
}
|
|
5191
6307
|
if (sort) {
|
|
5192
6308
|
nodes.sort((a, b) => {
|
|
5193
|
-
const
|
|
5194
|
-
const bVal = b[sort.field] ?? "";
|
|
5195
|
-
const cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
|
|
6309
|
+
const cmp = compareByField(a, b, sort.field);
|
|
5196
6310
|
return sort.direction === "asc" ? cmp : -cmp;
|
|
5197
6311
|
});
|
|
5198
6312
|
}
|
|
@@ -5271,7 +6385,47 @@ var TaskTracker = class {
|
|
|
5271
6385
|
}
|
|
5272
6386
|
}
|
|
5273
6387
|
}
|
|
6388
|
+
/**
|
|
6389
|
+
* Fire-and-forget persistence with attached error handler.
|
|
6390
|
+
* Synchronous mutators (addNode/addEdge/updateNodeStatus) use this to
|
|
6391
|
+
* avoid forcing an async cascade through every caller; if the store
|
|
6392
|
+
* rejects, the configured `onPersistError` is invoked so failures are
|
|
6393
|
+
* surfaced instead of swallowed by an unhandled promise rejection.
|
|
6394
|
+
*/
|
|
6395
|
+
persist() {
|
|
6396
|
+
if (!this.graph) return;
|
|
6397
|
+
this.opts.store.saveGraph(this.graph).catch((err) => {
|
|
6398
|
+
if (this.opts.onPersistError) this.opts.onPersistError(err);
|
|
6399
|
+
else console.warn("[task-tracker] saveGraph failed:", err instanceof Error ? err.message : String(err));
|
|
6400
|
+
});
|
|
6401
|
+
}
|
|
6402
|
+
};
|
|
6403
|
+
var PRIORITY_RANK = {
|
|
6404
|
+
critical: 0,
|
|
6405
|
+
high: 1,
|
|
6406
|
+
medium: 2,
|
|
6407
|
+
low: 3
|
|
5274
6408
|
};
|
|
6409
|
+
var STATUS_RANK = {
|
|
6410
|
+
in_progress: 0,
|
|
6411
|
+
pending: 1,
|
|
6412
|
+
review: 2,
|
|
6413
|
+
blocked: 3,
|
|
6414
|
+
failed: 4,
|
|
6415
|
+
completed: 5
|
|
6416
|
+
};
|
|
6417
|
+
function compareByField(a, b, field) {
|
|
6418
|
+
switch (field) {
|
|
6419
|
+
case "priority":
|
|
6420
|
+
return PRIORITY_RANK[a.priority] - PRIORITY_RANK[b.priority];
|
|
6421
|
+
case "status":
|
|
6422
|
+
return STATUS_RANK[a.status] - STATUS_RANK[b.status];
|
|
6423
|
+
case "createdAt":
|
|
6424
|
+
return a.createdAt - b.createdAt;
|
|
6425
|
+
case "updatedAt":
|
|
6426
|
+
return a.updatedAt - b.updatedAt;
|
|
6427
|
+
}
|
|
6428
|
+
}
|
|
5275
6429
|
|
|
5276
6430
|
// src/defaults/task-flow.ts
|
|
5277
6431
|
var TaskFlow = class {
|
|
@@ -5323,9 +6477,10 @@ var TaskFlow = class {
|
|
|
5323
6477
|
const task = batch[i];
|
|
5324
6478
|
if (!result || !task) continue;
|
|
5325
6479
|
if (result.status === "rejected") {
|
|
5326
|
-
|
|
5327
|
-
this.
|
|
5328
|
-
|
|
6480
|
+
const reason = result.reason;
|
|
6481
|
+
this.opts.tracker.updateNodeStatus(task.id, "failed", reason?.message);
|
|
6482
|
+
this.emit("task.failed", { taskId: task.id, error: reason?.message ?? "unknown" });
|
|
6483
|
+
ctx.onTaskFail?.(task, reason);
|
|
5329
6484
|
} else {
|
|
5330
6485
|
this.opts.tracker.updateNodeStatus(task.id, "completed");
|
|
5331
6486
|
this.emit("task.completed", { taskId: task.id, result: result.value });
|
|
@@ -5613,7 +6768,7 @@ var ToolExecutor = class {
|
|
|
5613
6768
|
return { result, tool, durationMs: Date.now() - start };
|
|
5614
6769
|
}
|
|
5615
6770
|
} else {
|
|
5616
|
-
const suggestedPattern = this.subjectFor(tool.name, use.input) ?? tool.name;
|
|
6771
|
+
const suggestedPattern = this.subjectFor(tool.name, use.input, tool.subjectKey) ?? tool.name;
|
|
5617
6772
|
const pending = { type: "tool_confirm_pending", toolUseId: use.id, toolName: tool.name, input: use.input, suggestedPattern };
|
|
5618
6773
|
return { result: pending, tool, durationMs: Date.now() - start };
|
|
5619
6774
|
}
|
|
@@ -5645,15 +6800,31 @@ var ToolExecutor = class {
|
|
|
5645
6800
|
span?.end();
|
|
5646
6801
|
}
|
|
5647
6802
|
};
|
|
6803
|
+
const safeRun = async (use) => {
|
|
6804
|
+
try {
|
|
6805
|
+
return await runOne(use);
|
|
6806
|
+
} catch (err) {
|
|
6807
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
6808
|
+
const scrubbed = this.opts.secretScrubber.scrub(msg);
|
|
6809
|
+
const result = {
|
|
6810
|
+
type: "tool_result",
|
|
6811
|
+
tool_use_id: use.id,
|
|
6812
|
+
content: `Tool "${use.name}" execution failed: ${scrubbed}`,
|
|
6813
|
+
is_error: true
|
|
6814
|
+
};
|
|
6815
|
+
budget = this.decrementBudget(result, budget);
|
|
6816
|
+
return { result, tool: this.registry.get(use.name), durationMs: 0 };
|
|
6817
|
+
}
|
|
6818
|
+
};
|
|
5648
6819
|
if (strategy === "sequential") {
|
|
5649
6820
|
const outputs = [];
|
|
5650
6821
|
for (const use of toolUses) {
|
|
5651
|
-
if (use) outputs.push(await
|
|
6822
|
+
if (use) outputs.push(await safeRun(use));
|
|
5652
6823
|
}
|
|
5653
6824
|
return { outputs, remainingBudget: budget };
|
|
5654
6825
|
}
|
|
5655
6826
|
if (strategy === "parallel") {
|
|
5656
|
-
const outputs = await Promise.all(toolUses.map((use) =>
|
|
6827
|
+
const outputs = await Promise.all(toolUses.map((use) => safeRun(use)));
|
|
5657
6828
|
return { outputs, remainingBudget: budget };
|
|
5658
6829
|
}
|
|
5659
6830
|
const nonMutating = [];
|
|
@@ -5664,10 +6835,10 @@ var ToolExecutor = class {
|
|
|
5664
6835
|
if (tool?.mutating) mutating.push(use);
|
|
5665
6836
|
else nonMutating.push(use);
|
|
5666
6837
|
}
|
|
5667
|
-
const firstPass = await Promise.all(nonMutating.map((use) =>
|
|
6838
|
+
const firstPass = await Promise.all(nonMutating.map((use) => safeRun(use)));
|
|
5668
6839
|
const secondPass = [];
|
|
5669
6840
|
for (const use of mutating) {
|
|
5670
|
-
secondPass.push(await
|
|
6841
|
+
secondPass.push(await safeRun(use));
|
|
5671
6842
|
}
|
|
5672
6843
|
return {
|
|
5673
6844
|
outputs: [...firstPass, ...secondPass],
|
|
@@ -5702,7 +6873,8 @@ var ToolExecutor = class {
|
|
|
5702
6873
|
}
|
|
5703
6874
|
async runWithTimeout(tool, input, parentSignal, ctx, toolUseId) {
|
|
5704
6875
|
if (parentSignal.aborted) {
|
|
5705
|
-
|
|
6876
|
+
if (parentSignal.reason instanceof Error) throw parentSignal.reason;
|
|
6877
|
+
throw new Error(typeof parentSignal.reason === "string" ? parentSignal.reason : "aborted");
|
|
5706
6878
|
}
|
|
5707
6879
|
const timeoutMs = tool.timeoutMs ?? this.iterationTimeoutMs;
|
|
5708
6880
|
const ctrl = new AbortController();
|
|
@@ -5771,16 +6943,23 @@ var ToolExecutor = class {
|
|
|
5771
6943
|
* Matches the logic in DefaultPermissionPolicy so the TUI shows the
|
|
5772
6944
|
* same subject that the trust file would use.
|
|
5773
6945
|
*/
|
|
5774
|
-
subjectFor(toolName, input) {
|
|
6946
|
+
subjectFor(toolName, input, subjectKey) {
|
|
5775
6947
|
if (!input || typeof input !== "object") return void 0;
|
|
5776
6948
|
const obj = input;
|
|
5777
6949
|
const globChars = /[*?\[\]]/g;
|
|
5778
6950
|
const escapeGlob = (s) => s.replace(globChars, (c) => `\\${c}`);
|
|
6951
|
+
const normalizePath = (s) => escapeGlob(s.replace(/\\/g, "/"));
|
|
6952
|
+
if (subjectKey) {
|
|
6953
|
+
const v = obj[subjectKey];
|
|
6954
|
+
if (typeof v === "string") {
|
|
6955
|
+
return subjectKey === "path" || subjectKey === "file" || subjectKey === "files" ? normalizePath(v) : escapeGlob(v);
|
|
6956
|
+
}
|
|
6957
|
+
}
|
|
5779
6958
|
if (toolName === "bash" && typeof obj.command === "string") {
|
|
5780
6959
|
return escapeGlob(obj.command);
|
|
5781
6960
|
}
|
|
5782
6961
|
if (typeof obj.path === "string") {
|
|
5783
|
-
return
|
|
6962
|
+
return normalizePath(obj.path);
|
|
5784
6963
|
}
|
|
5785
6964
|
if (typeof obj.url === "string") {
|
|
5786
6965
|
return escapeGlob(obj.url);
|
|
@@ -6204,8 +7383,9 @@ var DefaultHealthRegistry = class {
|
|
|
6204
7383
|
return { status, timestamp: Date.now(), checks: results };
|
|
6205
7384
|
}
|
|
6206
7385
|
async runOne(check) {
|
|
7386
|
+
let timer = null;
|
|
6207
7387
|
const timeout = new Promise((resolve4) => {
|
|
6208
|
-
setTimeout(
|
|
7388
|
+
timer = setTimeout(
|
|
6209
7389
|
() => resolve4({ status: "unhealthy", detail: `timeout after ${this.timeoutMs}ms` }),
|
|
6210
7390
|
this.timeoutMs
|
|
6211
7391
|
);
|
|
@@ -6214,6 +7394,8 @@ var DefaultHealthRegistry = class {
|
|
|
6214
7394
|
return await Promise.race([check.check(), timeout]);
|
|
6215
7395
|
} catch (err) {
|
|
6216
7396
|
return { status: "unhealthy", detail: err instanceof Error ? err.message : String(err) };
|
|
7397
|
+
} finally {
|
|
7398
|
+
if (timer) clearTimeout(timer);
|
|
6217
7399
|
}
|
|
6218
7400
|
}
|
|
6219
7401
|
};
|
|
@@ -6386,7 +7568,7 @@ var PROMETHEUS_CONTENT_TYPE = "text/plain; version=0.0.4; charset=utf-8";
|
|
|
6386
7568
|
async function startMetricsServer(opts) {
|
|
6387
7569
|
const { createServer } = await import('http');
|
|
6388
7570
|
const host = opts.host ?? "127.0.0.1";
|
|
6389
|
-
const
|
|
7571
|
+
const path17 = opts.path ?? "/metrics";
|
|
6390
7572
|
const healthPath = opts.healthPath ?? "/healthz";
|
|
6391
7573
|
const healthRegistry = opts.healthRegistry;
|
|
6392
7574
|
const server = createServer((req, res) => {
|
|
@@ -6396,7 +7578,7 @@ async function startMetricsServer(opts) {
|
|
|
6396
7578
|
return;
|
|
6397
7579
|
}
|
|
6398
7580
|
const url = req.url.split("?")[0];
|
|
6399
|
-
if (url ===
|
|
7581
|
+
if (url === path17) {
|
|
6400
7582
|
let body;
|
|
6401
7583
|
try {
|
|
6402
7584
|
body = renderPrometheus(opts.sink.snapshot());
|
|
@@ -6447,7 +7629,7 @@ async function startMetricsServer(opts) {
|
|
|
6447
7629
|
const boundPort = typeof addr === "object" && addr ? addr.port : opts.port;
|
|
6448
7630
|
return {
|
|
6449
7631
|
port: boundPort,
|
|
6450
|
-
url: `http://${host}:${boundPort}${
|
|
7632
|
+
url: `http://${host}:${boundPort}${path17}`,
|
|
6451
7633
|
close: () => new Promise((resolve4, reject) => {
|
|
6452
7634
|
server.close((err) => err ? reject(err) : resolve4());
|
|
6453
7635
|
})
|
|
@@ -6852,7 +8034,6 @@ function createContextManagerTool(opts = {}) {
|
|
|
6852
8034
|
notes: `Invalid range [${from}, ${to}] for ${messages.length} messages.`
|
|
6853
8035
|
};
|
|
6854
8036
|
}
|
|
6855
|
-
messages.slice(from, to + 1);
|
|
6856
8037
|
const summaryText = input.text ?? '[summary placeholder \u2014 provide "text" to record the summary]';
|
|
6857
8038
|
const summaryMsg = {
|
|
6858
8039
|
role: "system",
|
|
@@ -7114,13 +8295,23 @@ async function streamProviderToResponse(provider, req, signal, ctx, events) {
|
|
|
7114
8295
|
}
|
|
7115
8296
|
} catch (err) {
|
|
7116
8297
|
if (signal.aborted) {
|
|
7117
|
-
state.stopReason = "
|
|
8298
|
+
state.stopReason = "end_turn";
|
|
7118
8299
|
return buildResponse(state);
|
|
7119
8300
|
}
|
|
7120
8301
|
throw err;
|
|
7121
8302
|
} finally {
|
|
7122
8303
|
try {
|
|
7123
|
-
|
|
8304
|
+
let drainTimer = null;
|
|
8305
|
+
try {
|
|
8306
|
+
await Promise.race([
|
|
8307
|
+
Promise.resolve(iter.return?.()),
|
|
8308
|
+
new Promise((resolve4) => {
|
|
8309
|
+
drainTimer = setTimeout(resolve4, 500);
|
|
8310
|
+
})
|
|
8311
|
+
]);
|
|
8312
|
+
} finally {
|
|
8313
|
+
if (drainTimer) clearTimeout(drainTimer);
|
|
8314
|
+
}
|
|
7124
8315
|
} catch {
|
|
7125
8316
|
}
|
|
7126
8317
|
}
|
|
@@ -7177,12 +8368,24 @@ async function runProviderWithRetry(opts) {
|
|
|
7177
8368
|
});
|
|
7178
8369
|
}
|
|
7179
8370
|
await new Promise((resolve4, reject) => {
|
|
7180
|
-
|
|
7181
|
-
const
|
|
8371
|
+
let settled = false;
|
|
8372
|
+
const cleanup = () => {
|
|
8373
|
+
if (settled) return;
|
|
8374
|
+
settled = true;
|
|
7182
8375
|
clearTimeout(t2);
|
|
8376
|
+
};
|
|
8377
|
+
const onAbort = () => {
|
|
8378
|
+
cleanup();
|
|
7183
8379
|
reject(new Error("aborted"));
|
|
7184
8380
|
};
|
|
7185
|
-
|
|
8381
|
+
const t2 = setTimeout(() => {
|
|
8382
|
+
cleanup();
|
|
8383
|
+
resolve4();
|
|
8384
|
+
}, delay);
|
|
8385
|
+
if (signal.aborted) {
|
|
8386
|
+
onAbort();
|
|
8387
|
+
return;
|
|
8388
|
+
}
|
|
7186
8389
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
7187
8390
|
});
|
|
7188
8391
|
attempt++;
|
|
@@ -7306,9 +8509,6 @@ var Agent = class {
|
|
|
7306
8509
|
get scrubber() {
|
|
7307
8510
|
return this.container.resolve(TOKENS.SecretScrubber);
|
|
7308
8511
|
}
|
|
7309
|
-
get compactor() {
|
|
7310
|
-
return this.container.has(TOKENS.Compactor) ? this.container.resolve(TOKENS.Compactor) : void 0;
|
|
7311
|
-
}
|
|
7312
8512
|
get renderer() {
|
|
7313
8513
|
return this.container.has(TOKENS.Renderer) ? this.container.resolve(TOKENS.Renderer) : void 0;
|
|
7314
8514
|
}
|
|
@@ -7354,6 +8554,7 @@ var Agent = class {
|
|
|
7354
8554
|
let iterations = 0;
|
|
7355
8555
|
let effectiveLimit = opts.maxIterations ?? this.maxIterations;
|
|
7356
8556
|
const hasHardLimit = effectiveLimit > 0 && Number.isFinite(effectiveLimit);
|
|
8557
|
+
let recoveryRetries = 0;
|
|
7357
8558
|
for (let i = 0; ; i++) {
|
|
7358
8559
|
iterations = i + 1;
|
|
7359
8560
|
if (controller.signal.aborted) {
|
|
@@ -7383,17 +8584,33 @@ var Agent = class {
|
|
|
7383
8584
|
logger: this.logger,
|
|
7384
8585
|
tracer: this.tracer
|
|
7385
8586
|
});
|
|
8587
|
+
recoveryRetries = 0;
|
|
7386
8588
|
} catch (err) {
|
|
7387
8589
|
if (controller.signal.aborted) {
|
|
7388
8590
|
this.events.emit("error", { err: toError(err), phase: "provider" });
|
|
7389
8591
|
return { status: "aborted", iterations, error: toWrongStackError(err, "AGENT_ABORTED") };
|
|
7390
8592
|
}
|
|
7391
8593
|
const recovered = await this.errorHandler.recover(err, this.ctx);
|
|
7392
|
-
if (!recovered) {
|
|
8594
|
+
if (!recovered || recovered.action === "fail") {
|
|
7393
8595
|
this.events.emit("error", { err: toError(err), phase: "provider" });
|
|
7394
|
-
return {
|
|
8596
|
+
return {
|
|
8597
|
+
status: "failed",
|
|
8598
|
+
iterations,
|
|
8599
|
+
error: toWrongStackError(recovered?.error ?? err)
|
|
8600
|
+
};
|
|
7395
8601
|
}
|
|
7396
|
-
|
|
8602
|
+
if (recovered.action === "retry") {
|
|
8603
|
+
recoveryRetries++;
|
|
8604
|
+
if (recoveryRetries > 2) {
|
|
8605
|
+
this.events.emit("error", { err: toError(err), phase: "provider" });
|
|
8606
|
+
return { status: "failed", iterations, error: toWrongStackError(err) };
|
|
8607
|
+
}
|
|
8608
|
+
if (recovered.model) this.ctx.model = recovered.model;
|
|
8609
|
+
this.logger.info(`Recovered provider error via ${recovered.reason}; retrying turn`);
|
|
8610
|
+
continue;
|
|
8611
|
+
}
|
|
8612
|
+
recoveryRetries = 0;
|
|
8613
|
+
res = recovered.response;
|
|
7397
8614
|
}
|
|
7398
8615
|
const responseResult = await this.processResponse(res, req);
|
|
7399
8616
|
if (responseResult.aborted) {
|
|
@@ -7467,7 +8684,7 @@ var Agent = class {
|
|
|
7467
8684
|
* update session, render text, handle abort.
|
|
7468
8685
|
*/
|
|
7469
8686
|
async processResponse(res, req) {
|
|
7470
|
-
await this.pipelines.response.run(res);
|
|
8687
|
+
res = await this.pipelines.response.run(res);
|
|
7471
8688
|
this.events.emit("provider.response", {
|
|
7472
8689
|
ctx: this.ctx,
|
|
7473
8690
|
usage: res.usage,
|
|
@@ -7538,6 +8755,7 @@ var Agent = class {
|
|
|
7538
8755
|
isError: !!reRunResult.result.is_error
|
|
7539
8756
|
});
|
|
7540
8757
|
this.events.emit("tool.executed", {
|
|
8758
|
+
id: reRunResult.result.tool_use_id,
|
|
7541
8759
|
name: tool.name,
|
|
7542
8760
|
durationMs: reRunResult.durationMs,
|
|
7543
8761
|
ok: !reRunResult.result.is_error,
|
|
@@ -7545,7 +8763,6 @@ var Agent = class {
|
|
|
7545
8763
|
output: truncateForEvent(reRunResult.result.content)
|
|
7546
8764
|
});
|
|
7547
8765
|
}
|
|
7548
|
-
this.ctx.state.appendMessage({ role: "user", content: [reRunResult.result] });
|
|
7549
8766
|
continue;
|
|
7550
8767
|
}
|
|
7551
8768
|
const use = useById.get(result.tool_use_id);
|
|
@@ -7564,6 +8781,7 @@ var Agent = class {
|
|
|
7564
8781
|
isError: !!result.is_error
|
|
7565
8782
|
});
|
|
7566
8783
|
this.events.emit("tool.executed", {
|
|
8784
|
+
id: result.tool_use_id,
|
|
7567
8785
|
name: use.name,
|
|
7568
8786
|
durationMs,
|
|
7569
8787
|
ok: !result.is_error,
|
|
@@ -7604,12 +8822,11 @@ var Agent = class {
|
|
|
7604
8822
|
}
|
|
7605
8823
|
}
|
|
7606
8824
|
/**
|
|
7607
|
-
* Run context window pipeline
|
|
8825
|
+
* Run context window pipeline. The pipeline may be empty, or it may contain
|
|
8826
|
+
* middleware with its own injected dependencies.
|
|
7608
8827
|
*/
|
|
7609
8828
|
async compactContextIfNeeded() {
|
|
7610
|
-
|
|
7611
|
-
await this.pipelines.contextWindow.run(this.ctx);
|
|
7612
|
-
}
|
|
8829
|
+
await this.pipelines.contextWindow.run(this.ctx);
|
|
7613
8830
|
}
|
|
7614
8831
|
};
|
|
7615
8832
|
function toError(err) {
|
|
@@ -7627,7 +8844,6 @@ var ConversationState = class {
|
|
|
7627
8844
|
constructor(ctx) {
|
|
7628
8845
|
this.ctx = ctx;
|
|
7629
8846
|
}
|
|
7630
|
-
// ─── Read API ───────────────────────────────────────────────────────
|
|
7631
8847
|
get messages() {
|
|
7632
8848
|
return this.ctx.messages;
|
|
7633
8849
|
}
|
|
@@ -7642,25 +8858,24 @@ var ConversationState = class {
|
|
|
7642
8858
|
* that need a stable view across an async boundary.
|
|
7643
8859
|
*/
|
|
7644
8860
|
snapshot() {
|
|
7645
|
-
return {
|
|
7646
|
-
messages: [...this.ctx.messages],
|
|
7647
|
-
todos: [...this.ctx.todos],
|
|
7648
|
-
meta: { ...this.ctx.meta }
|
|
7649
|
-
};
|
|
8861
|
+
return Object.freeze({
|
|
8862
|
+
messages: Object.freeze([...this.ctx.messages]),
|
|
8863
|
+
todos: Object.freeze([...this.ctx.todos]),
|
|
8864
|
+
meta: Object.freeze({ ...this.ctx.meta })
|
|
8865
|
+
});
|
|
7650
8866
|
}
|
|
7651
|
-
// ─── Write API (preferred — fires onChange) ─────────────────────────
|
|
7652
8867
|
appendMessage(message) {
|
|
7653
|
-
this.ctx.messages.
|
|
8868
|
+
this.ctx.messages.splice(this.ctx.messages.length, 0, message);
|
|
7654
8869
|
this.emit({ kind: "message_appended", message });
|
|
7655
8870
|
}
|
|
7656
8871
|
replaceMessages(messages) {
|
|
7657
8872
|
this.ctx.messages.length = 0;
|
|
7658
|
-
this.ctx.messages.
|
|
8873
|
+
this.ctx.messages.splice(0, 0, ...messages);
|
|
7659
8874
|
this.emit({ kind: "messages_replaced", messages: [...messages] });
|
|
7660
8875
|
}
|
|
7661
8876
|
replaceTodos(todos) {
|
|
7662
8877
|
this.ctx.todos.length = 0;
|
|
7663
|
-
this.ctx.todos.
|
|
8878
|
+
this.ctx.todos.splice(0, 0, ...todos);
|
|
7664
8879
|
this.emit({ kind: "todos_replaced", todos: [...todos] });
|
|
7665
8880
|
}
|
|
7666
8881
|
setMeta(key, value) {
|
|
@@ -7672,13 +8887,15 @@ var ConversationState = class {
|
|
|
7672
8887
|
delete this.ctx.meta[key];
|
|
7673
8888
|
this.emit({ kind: "meta_deleted", key });
|
|
7674
8889
|
}
|
|
7675
|
-
|
|
8890
|
+
clearMeta() {
|
|
8891
|
+
const keys = Object.keys(this.ctx.meta);
|
|
8892
|
+
if (keys.length === 0) return;
|
|
8893
|
+
for (const key of keys) delete this.ctx.meta[key];
|
|
8894
|
+
this.emit({ kind: "meta_cleared" });
|
|
8895
|
+
}
|
|
7676
8896
|
/**
|
|
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.
|
|
8897
|
+
* Subscribe to mutations that go through this wrapper. Direct mutations of
|
|
8898
|
+
* the compatibility arrays are intentionally not observed.
|
|
7682
8899
|
*/
|
|
7683
8900
|
onChange(listener) {
|
|
7684
8901
|
this.listeners.add(listener);
|
|
@@ -7895,34 +9112,60 @@ You operate inside the user's terminal with direct read and write access to thei
|
|
|
7895
9112
|
|
|
7896
9113
|
## Core principles
|
|
7897
9114
|
|
|
7898
|
-
1. Read before you write
|
|
7899
|
-
|
|
7900
|
-
|
|
9115
|
+
1. **Read before you write.** Always inspect the relevant files before proposing changes. Assumptions about code you haven't read are bugs in waiting.
|
|
9116
|
+
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.
|
|
9117
|
+
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.
|
|
9118
|
+
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.
|
|
9119
|
+
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.
|
|
9120
|
+
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.
|
|
9121
|
+
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.
|
|
9122
|
+
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.
|
|
9123
|
+
9. **Recover explicitly.** When a tool fails, state: (1) what failed, (2) what you tried, (3) what you'll attempt next. Never silently skip.
|
|
7901
9124
|
|
|
7902
|
-
|
|
9125
|
+
## Decision heuristics
|
|
7903
9126
|
|
|
7904
|
-
|
|
9127
|
+
- **Task is ambiguous** (unclear which file, conflicting requirements) \u2192 ask before proceeding
|
|
9128
|
+
- **Task is clear, approach is unknown** \u2192 try one approach, report what happened
|
|
9129
|
+
- **Tool fails** \u2192 retry once with adjusted params, then report failure
|
|
9130
|
+
- **Permission prompt shown** \u2192 wait for user, do not act unilaterally
|
|
9131
|
+
- **Context window filling up** \u2192 use context_manager proactively; don't wait to be told
|
|
7905
9132
|
|
|
7906
|
-
|
|
9133
|
+
## How you work
|
|
7907
9134
|
|
|
7908
|
-
|
|
7909
|
-
|
|
7910
|
-
|
|
7911
|
-
|
|
7912
|
-
|
|
7913
|
-
|
|
7914
|
-
- You do not lecture about software engineering principles unless asked.
|
|
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.`;
|
|
9135
|
+
- **Stay focused.** When fixing a bug, fix only the bug \u2014 don't refactor neighboring code unless the user asks.
|
|
9136
|
+
- **Comment with purpose.** Add comments only when they explain why, not what. The code already says what.
|
|
9137
|
+
- **Own your output.** Never call work "production-ready" or "fully tested" \u2014 the user makes that call.
|
|
9138
|
+
- **Move on from mistakes.** When something fails, report what happened and what you'll do next. No apologies, no hand-wringing.
|
|
9139
|
+
- **Stay in your lane.** Don't lecture about software engineering principles unless explicitly asked \u2014 the user is the expert on their codebase.`;
|
|
7919
9140
|
var DefaultSystemPromptBuilder = class {
|
|
7920
9141
|
constructor(opts = {}) {
|
|
7921
9142
|
this.opts = opts;
|
|
7922
9143
|
}
|
|
7923
9144
|
opts;
|
|
7924
|
-
|
|
9145
|
+
/**
|
|
9146
|
+
* Cached environment block, keyed by projectRoot. A single builder
|
|
9147
|
+
* instance is normally reused across turns of the same agent run, but
|
|
9148
|
+
* tests and library consumers may reuse it across runs with different
|
|
9149
|
+
* roots; keying the cache prevents leaking the first call's project
|
|
9150
|
+
* state into a later call against an unrelated project.
|
|
9151
|
+
*/
|
|
9152
|
+
envCacheByRoot = /* @__PURE__ */ new Map();
|
|
9153
|
+
skillCache;
|
|
7925
9154
|
async build(ctx) {
|
|
9155
|
+
if (this.opts.skillLoader && !this.skillCache) {
|
|
9156
|
+
try {
|
|
9157
|
+
const entries = await this.opts.skillLoader.listEntries();
|
|
9158
|
+
if (entries.length > 0) {
|
|
9159
|
+
const lines = [];
|
|
9160
|
+
for (const e of entries) {
|
|
9161
|
+
const scopeTag = e.scope.length > 0 ? ` \u2014 ${e.scope.slice(0, 4).join(", ")}` : "";
|
|
9162
|
+
lines.push(`- **${e.name}**${scopeTag} (${e.trigger})`);
|
|
9163
|
+
}
|
|
9164
|
+
this.skillCache = lines.join("\n");
|
|
9165
|
+
}
|
|
9166
|
+
} catch {
|
|
9167
|
+
}
|
|
9168
|
+
}
|
|
7926
9169
|
const layer1 = LAYER_1_IDENTITY;
|
|
7927
9170
|
const layer2 = this.buildToolUsage(ctx.tools);
|
|
7928
9171
|
const layer3 = await this.buildEnvironment(ctx);
|
|
@@ -7958,8 +9201,20 @@ var DefaultSystemPromptBuilder = class {
|
|
|
7958
9201
|
### ${t2.name}
|
|
7959
9202
|
${hint.trim()}`);
|
|
7960
9203
|
}
|
|
9204
|
+
lines.push(`
|
|
9205
|
+
## Common patterns
|
|
9206
|
+
|
|
9207
|
+
- **Inspect before edit:** \`read\`/\`glob\`/\`grep\` \u2192 locate target \u2192 \`edit\`
|
|
9208
|
+
- **Search then operate:** \`grep\`/\`glob\` \u2192 identify targets \u2192 \`batch_tool_use\` or iterative \`edit\`
|
|
9209
|
+
- **Verify after mutate:** \`write\`/\`edit\`/\`patch\` \u2192 \`read\` back to confirm \u2192 report outcome
|
|
9210
|
+
- **Explore project:** \`glob\` for structure \u2192 \`read\` key files \u2192 \`grep\` for patterns
|
|
9211
|
+
- **Batch ops:** Use \`replace\` with glob patterns for multi-file surgical changes
|
|
9212
|
+
|
|
9213
|
+
When unsure about a file's current state, read it first rather than assuming.`);
|
|
7961
9214
|
const hasContextManager = tools.some((t2) => t2.name === "context_manager");
|
|
7962
9215
|
if (hasContextManager) {
|
|
9216
|
+
const maxCtx = this.opts.modelCapabilities?.maxContextTokens ?? 128e3;
|
|
9217
|
+
const threshold = maxCtx <= 32e3 ? "50" : "70";
|
|
7963
9218
|
lines.push(`
|
|
7964
9219
|
## Context management
|
|
7965
9220
|
|
|
@@ -7967,7 +9222,7 @@ When the conversation grows long and context window usage exceeds what you can t
|
|
|
7967
9222
|
use the context_manager tool proactively \u2014 do NOT wait to be told:
|
|
7968
9223
|
|
|
7969
9224
|
- Call \`context_manager\` with \`{"action":"check"}\` to see current token budget and message counts.
|
|
7970
|
-
- When the conversation exceeds
|
|
9225
|
+
- When the conversation exceeds ~${threshold}% of your context window, call \`{"action":"summary"}\` or \`{"action":"compact"}\` to reclaim space.
|
|
7971
9226
|
- Use \`{"action":"prune"}\` to surgically remove specific irrelevant message ranges (e.g. old debug output).
|
|
7972
9227
|
- Use \`{"action":"add_note"}\` to inject a summary note at a specific point after a complex operation.
|
|
7973
9228
|
|
|
@@ -7977,14 +9232,17 @@ summarize it, and let the tool result hold only the summary.`);
|
|
|
7977
9232
|
return lines.join("\n");
|
|
7978
9233
|
}
|
|
7979
9234
|
async buildEnvironment(ctx) {
|
|
7980
|
-
|
|
9235
|
+
const cached = this.envCacheByRoot.get(ctx.projectRoot);
|
|
9236
|
+
if (cached) return cached;
|
|
7981
9237
|
const today = this.opts.todayIso ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
7982
9238
|
const platform2 = `${os3.platform()} ${os3.release()}`;
|
|
7983
9239
|
const shell = process.env.SHELL ?? process.env.ComSpec ?? "unknown";
|
|
7984
9240
|
const node = process.version;
|
|
7985
9241
|
const isGit = await this.dirExists(path2.join(ctx.projectRoot, ".git"));
|
|
7986
|
-
const git =
|
|
7987
|
-
|
|
9242
|
+
const [git, langs] = await Promise.all([
|
|
9243
|
+
isGit ? this.gitStatus(ctx.projectRoot) : Promise.resolve("not a git repo"),
|
|
9244
|
+
this.detectLanguages(ctx.projectRoot)
|
|
9245
|
+
]);
|
|
7988
9246
|
const lines = [
|
|
7989
9247
|
"## Environment",
|
|
7990
9248
|
"",
|
|
@@ -8002,8 +9260,17 @@ summarize it, and let the tool result hold only the summary.`);
|
|
|
8002
9260
|
`- Running on: ${ctx.provider ?? "<unknown provider>"}/${ctx.model ?? "<unknown model>"}`
|
|
8003
9261
|
);
|
|
8004
9262
|
}
|
|
9263
|
+
if (this.opts.modeId && this.opts.modeId !== "default") {
|
|
9264
|
+
lines.push(`- Mode: ${this.opts.modeId}`);
|
|
9265
|
+
}
|
|
9266
|
+
if (this.opts.modelCapabilities) {
|
|
9267
|
+
lines.push(`- Context window: ${this.opts.modelCapabilities.maxContextTokens.toLocaleString()} tokens max`);
|
|
9268
|
+
}
|
|
9269
|
+
if (this.skillCache) {
|
|
9270
|
+
lines.push("", "## Skills in scope for this session", this.skillCache);
|
|
9271
|
+
}
|
|
8005
9272
|
const text = lines.join("\n");
|
|
8006
|
-
this.
|
|
9273
|
+
this.envCacheByRoot.set(ctx.projectRoot, text);
|
|
8007
9274
|
return text;
|
|
8008
9275
|
}
|
|
8009
9276
|
async buildMemoryAndSkills() {
|
|
@@ -8017,16 +9284,10 @@ ${mem}`);
|
|
|
8017
9284
|
} catch {
|
|
8018
9285
|
}
|
|
8019
9286
|
}
|
|
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
9287
|
return parts.join("\n\n");
|
|
8028
9288
|
}
|
|
8029
9289
|
async buildMode() {
|
|
9290
|
+
if (this.opts.modePrompt) return this.opts.modePrompt;
|
|
8030
9291
|
if (!this.opts.modeStore) return "";
|
|
8031
9292
|
const mode = await this.opts.modeStore.getActiveMode();
|
|
8032
9293
|
if (!mode?.prompt) return "";
|
|
@@ -8042,8 +9303,19 @@ ${mem}`);
|
|
|
8042
9303
|
}
|
|
8043
9304
|
async gitStatus(root) {
|
|
8044
9305
|
return new Promise((resolve4) => {
|
|
9306
|
+
let settled = false;
|
|
9307
|
+
const finish = (s) => {
|
|
9308
|
+
if (settled) return;
|
|
9309
|
+
settled = true;
|
|
9310
|
+
resolve4(s);
|
|
9311
|
+
};
|
|
9312
|
+
let proc;
|
|
9313
|
+
const timer = setTimeout(() => {
|
|
9314
|
+
proc?.kill("SIGKILL");
|
|
9315
|
+
finish("git timeout");
|
|
9316
|
+
}, 2e3);
|
|
8045
9317
|
try {
|
|
8046
|
-
|
|
9318
|
+
proc = spawn("git", ["status", "--porcelain=v1", "--branch"], {
|
|
8047
9319
|
cwd: root,
|
|
8048
9320
|
stdio: ["ignore", "pipe", "ignore"]
|
|
8049
9321
|
});
|
|
@@ -8051,19 +9323,24 @@ ${mem}`);
|
|
|
8051
9323
|
proc.stdout?.on("data", (c) => {
|
|
8052
9324
|
buf += c.toString();
|
|
8053
9325
|
});
|
|
8054
|
-
proc.on("error", () =>
|
|
9326
|
+
proc.on("error", () => {
|
|
9327
|
+
clearTimeout(timer);
|
|
9328
|
+
finish("git error");
|
|
9329
|
+
});
|
|
8055
9330
|
proc.on("close", () => {
|
|
9331
|
+
clearTimeout(timer);
|
|
8056
9332
|
const lines = buf.split("\n").filter(Boolean);
|
|
8057
9333
|
const branchLine = lines[0] ?? "";
|
|
8058
|
-
const branchMatch = /## ([^\s.]+)
|
|
9334
|
+
const branchMatch = branchLine.match(/## ([^\s.]+)/);
|
|
8059
9335
|
const branch = branchMatch?.[1] ?? "detached";
|
|
8060
9336
|
const dirty = lines.slice(1);
|
|
8061
9337
|
const staged = dirty.filter((l) => /^[MARCD]/.test(l)).length;
|
|
8062
9338
|
const modified = dirty.length - staged;
|
|
8063
|
-
|
|
9339
|
+
finish(`branch=${branch}, ${modified} modified, ${staged} staged`);
|
|
8064
9340
|
});
|
|
8065
9341
|
} catch {
|
|
8066
|
-
|
|
9342
|
+
clearTimeout(timer);
|
|
9343
|
+
finish("git unavailable");
|
|
8067
9344
|
}
|
|
8068
9345
|
});
|
|
8069
9346
|
}
|
|
@@ -8081,14 +9358,17 @@ ${mem}`);
|
|
|
8081
9358
|
["composer.json", "PHP"],
|
|
8082
9359
|
["mix.exs", "Elixir"]
|
|
8083
9360
|
];
|
|
8084
|
-
const
|
|
8085
|
-
|
|
8086
|
-
|
|
8087
|
-
|
|
8088
|
-
|
|
8089
|
-
|
|
8090
|
-
|
|
8091
|
-
|
|
9361
|
+
const hits = await Promise.all(
|
|
9362
|
+
checks.map(async ([marker, lang]) => {
|
|
9363
|
+
try {
|
|
9364
|
+
await fsp.access(path2.join(root, marker));
|
|
9365
|
+
return lang;
|
|
9366
|
+
} catch {
|
|
9367
|
+
return null;
|
|
9368
|
+
}
|
|
9369
|
+
})
|
|
9370
|
+
);
|
|
9371
|
+
const langs = new Set(hits.filter((l) => l !== null));
|
|
8092
9372
|
return langs.size === 0 ? "unknown" : Array.from(langs).join(", ");
|
|
8093
9373
|
}
|
|
8094
9374
|
};
|
|
@@ -8387,7 +9667,7 @@ var noopSlashCommands = {
|
|
|
8387
9667
|
};
|
|
8388
9668
|
|
|
8389
9669
|
// src/plugin/loader.ts
|
|
8390
|
-
var KERNEL_API_VERSION = "0.1.
|
|
9670
|
+
var KERNEL_API_VERSION = "0.1.8";
|
|
8391
9671
|
function parseSemver(v) {
|
|
8392
9672
|
const parts = v.replace(/^[^0-9]*/, "").split(".").map((s) => Number.parseInt(s, 10) || 0);
|
|
8393
9673
|
return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];
|
|
@@ -8619,6 +9899,6 @@ function wrapApiForCapabilityCheck(plugin, api, log) {
|
|
|
8619
9899
|
});
|
|
8620
9900
|
}
|
|
8621
9901
|
|
|
8622
|
-
export { Agent, AgentError, AutoCompactionMiddleware, AutonomousRunner, BudgetExceededError, ConfigError, ConfigMigrationError, Container, Context, ConversationState, DEFAULT_CONFIG_MIGRATIONS, DEFAULT_MAX_ITERATIONS, DEFAULT_MODES, DEFAULT_SPEC_TEMPLATE, DefaultAttachmentStore, DefaultConfigLoader, DefaultConfigStore, DefaultErrorHandler, DefaultHealthRegistry, DefaultLogger, DefaultMemoryStore, DefaultModeStore, DefaultModelsRegistry, DefaultMultiAgentCoordinator, DefaultPathResolver, DefaultPermissionPolicy, DefaultPluginAPI, DefaultRetryPolicy, DefaultSecretScrubber, DefaultSecretVault, DefaultSessionReader, DefaultSessionStore, DefaultSkillLoader, DefaultSystemPromptBuilder, DefaultTaskStore, DefaultTokenCounter, DoneConditionChecker, ENCRYPTED_PREFIX, EventBus, HybridCompactor, InMemoryAgentBridge, InMemoryBridgeTransport, InMemoryMetricsSink, InputBuilder, IntelligentCompactor, KERNEL_API_VERSION, LAYER_1_IDENTITY, LLMSelector, NoopMetricsSink, NoopTracer, OTelTracer, PROMETHEUS_CONTENT_TYPE, Pipeline, PluginError, ProviderError, ProviderRegistry, QueueStore, RecoveryLock, RunController, SelectiveCompactor, SessionError, SlashCommandRegistry, SpecDrivenDev, SpecParser, SubagentBudget, TOKENS, TaskFlow, TaskGenerator, TaskTracker, ToolError, ToolExecutor, ToolRegistry, WrongStackError, allServers, asBlocks, asText, atomicWrite, awsServer, blockServer, braveSearchServer, buildOtlpMetricsRequest, buildOtlpTracesRequest, classifyFamily, color, compileGlob, computeTaskProgress, context7Server, contextManagerTool, createContextManagerTool, createDefaultPipelines, createMessage, createToolOutputSerializer, decryptConfigSecrets, detectNewlineStyle, encryptConfigSecrets, ensureDir, estimateTextTokens, estimateToolInputTokens, estimateToolResultTokens, everArtServer, extractRunEnv, filesystemServer, findCriticalPath, githubServer, googleMapsServer, isAgentError, isConfigError, isImageBlock, isPluginError, isSessionError, isTextBlock, isToolError, isToolResultBlock, isToolUseBlock, isWrongStackError, loadPlugins, loadProjectModes, loadUserModes, makeAgentSubagentRunner, matchAny, matchGlob, migratePlaintextSecrets, normalizeToLf, projectHash, renderPrometheus, resolveWstackPaths, rewriteConfigEncrypted, runConfigMigrations, safeParse, safeStringify, sanitizeJsonString, sentinelServer, slackServer, startMetricsServer, startOtlpMetricsExporter, startOtlpTraceExporter, stripAnsi, toStyle, toWrongStackError, topologicalSort, unifiedDiff, unloadPlugins, validateAgainstSchema, wireMetricsToEvents, wrapAsState };
|
|
9902
|
+
export { Agent, AgentError, AutoCompactionMiddleware, AutonomousRunner, BudgetExceededError, ConfigError, ConfigMigrationError, Container, Context, ConversationState, DEFAULT_CONFIG_MIGRATIONS, DEFAULT_DIRECTOR_PREAMBLE, DEFAULT_MAX_ITERATIONS, DEFAULT_MODES, DEFAULT_SPEC_TEMPLATE, DEFAULT_SUBAGENT_BASELINE, DefaultAttachmentStore, DefaultConfigLoader, DefaultConfigStore, DefaultErrorHandler, DefaultHealthRegistry, DefaultLogger, DefaultMemoryStore, DefaultModeStore, DefaultModelsRegistry, DefaultMultiAgentCoordinator, DefaultPathResolver, DefaultPermissionPolicy, DefaultPluginAPI, DefaultRetryPolicy, DefaultSecretScrubber, DefaultSecretVault, DefaultSessionReader, DefaultSessionStore, DefaultSkillLoader, DefaultSystemPromptBuilder, DefaultTaskStore, DefaultTokenCounter, Director, DoneConditionChecker, ENCRYPTED_PREFIX, EventBus, FleetBus, FleetUsageAggregator, HybridCompactor, InMemoryAgentBridge, InMemoryBridgeTransport, InMemoryMetricsSink, InputBuilder, IntelligentCompactor, KERNEL_API_VERSION, LAYER_1_IDENTITY, LLMSelector, NoopMetricsSink, NoopTracer, OTelTracer, PROMETHEUS_CONTENT_TYPE, Pipeline, PluginError, ProviderError, ProviderRegistry, QueueStore, RecoveryLock, RunController, SelectiveCompactor, SessionError, SlashCommandRegistry, SpecDrivenDev, SpecParser, SubagentBudget, TOKENS, TaskFlow, TaskGenerator, TaskTracker, ToolError, ToolExecutor, ToolRegistry, WrongStackError, allServers, asBlocks, asText, atomicWrite, awsServer, blockServer, braveSearchServer, buildOtlpMetricsRequest, buildOtlpTracesRequest, classifyFamily, color, compileGlob, composeDirectorPrompt, composeSubagentPrompt, computeTaskProgress, context7Server, contextManagerTool, createContextManagerTool, createDefaultPipelines, createMessage, createToolOutputSerializer, decryptConfigSecrets, detectNewlineStyle, encryptConfigSecrets, ensureDir, estimateTextTokens, estimateToolInputTokens, estimateToolResultTokens, everArtServer, extractRunEnv, filesystemServer, findCriticalPath, githubServer, googleMapsServer, isAgentError, isConfigError, isImageBlock, isPluginError, isSessionError, isTextBlock, isToolError, isToolResultBlock, isToolUseBlock, isWrongStackError, loadPlugins, loadProjectModes, loadUserModes, makeAgentSubagentRunner, makeDirectorSessionFactory, matchAny, matchGlob, migratePlaintextSecrets, normalizeToLf, projectHash, renderPrometheus, resolveWstackPaths, rewriteConfigEncrypted, rosterSummaryFromConfigs, runConfigMigrations, safeParse, safeStringify, sanitizeJsonString, sentinelServer, slackServer, startMetricsServer, startOtlpMetricsExporter, startOtlpTraceExporter, stripAnsi, toStyle, toWrongStackError, topologicalSort, unifiedDiff, unloadPlugins, validateAgainstSchema, wireMetricsToEvents, wrapAsState };
|
|
8623
9903
|
//# sourceMappingURL=index.js.map
|
|
8624
9904
|
//# sourceMappingURL=index.js.map
|