@wrongstack/core 0.1.10 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{agent-bridge-6KPqsFx6.d.ts → agent-bridge-C3DUGjSb.d.ts} +1 -1
- package/dist/{compactor-B4mQZXf2.d.ts → compactor-BUU6Zm_3.d.ts} +1 -1
- package/dist/{config-BU9f_5yH.d.ts → config-CKLYPkCi.d.ts} +1 -1
- package/dist/{context-BmM2xGUZ.d.ts → context-IovtuTf8.d.ts} +10 -0
- package/dist/coordination/index.d.ts +211 -13
- package/dist/coordination/index.js +964 -67
- package/dist/coordination/index.js.map +1 -1
- package/dist/defaults/index.d.ts +33 -18
- package/dist/defaults/index.js +1273 -42
- package/dist/defaults/index.js.map +1 -1
- package/dist/{events-BMNaEFZl.d.ts → events-CNB9PALO.d.ts} +99 -1
- package/dist/execution/index.d.ts +12 -12
- package/dist/extension/index.d.ts +9 -0
- package/dist/extension/index.js +234 -0
- package/dist/extension/index.js.map +1 -0
- package/dist/index-BDb0cAMP.d.ts +806 -0
- package/dist/index.d.ts +112 -29
- package/dist/index.js +2036 -490
- package/dist/index.js.map +1 -1
- package/dist/infrastructure/index.d.ts +6 -6
- package/dist/kernel/index.d.ts +12 -9
- package/dist/kernel/index.js +73 -7
- package/dist/kernel/index.js.map +1 -1
- package/dist/{mcp-servers-Dzgg4x1w.d.ts → mcp-servers-DR35ojJZ.d.ts} +3 -3
- package/dist/models/index.d.ts +2 -2
- package/dist/models/index.js +24 -1
- package/dist/models/index.js.map +1 -1
- package/dist/{multi-agent-fmkRHtof.d.ts → multi-agent-B9a6sflH.d.ts} +71 -3
- package/dist/observability/index.d.ts +2 -2
- package/dist/{path-resolver-DBjaoXFq.d.ts → path-resolver-Cl_q0u-R.d.ts} +2 -2
- package/dist/provider-runner-BXuADQqQ.d.ts +36 -0
- package/dist/sdd/index.d.ts +3 -3
- package/dist/{secret-scrubber-CicHLN4G.d.ts → secret-scrubber-CgG2tV2B.d.ts} +1 -1
- package/dist/{secret-scrubber-DF88luOe.d.ts → secret-scrubber-Cuy5afaQ.d.ts} +1 -1
- package/dist/security/index.d.ts +20 -4
- package/dist/security/index.js +37 -2
- package/dist/security/index.js.map +1 -1
- package/dist/{selector-BbJqiEP4.d.ts → selector-wT2fv9Fg.d.ts} +1 -1
- package/dist/{session-reader-Drq8RvJu.d.ts → session-reader-CcPi4BQ8.d.ts} +1 -1
- package/dist/{skill-DhfSizKv.d.ts → skill-C_7znCIC.d.ts} +2 -2
- package/dist/storage/index.d.ts +164 -6
- package/dist/storage/index.js +297 -2
- package/dist/storage/index.js.map +1 -1
- package/dist/{renderer-rk_1Swwc.d.ts → system-prompt-Dk1qm8ey.d.ts} +30 -2
- package/dist/{tool-executor-CpuJPYm9.d.ts → tool-executor-DKu4A6nB.d.ts} +5 -5
- package/dist/types/index.d.ts +16 -16
- package/dist/types/index.js +24 -1
- package/dist/types/index.js.map +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +24 -1
- package/dist/utils/index.js.map +1 -1
- package/package.json +5 -1
- package/dist/plugin-DJk6LL8B.d.ts +0 -434
- package/dist/system-prompt-BC_8ypCG.d.ts +0 -23
package/dist/index.js
CHANGED
|
@@ -395,6 +395,7 @@ var Pipeline = class {
|
|
|
395
395
|
// src/kernel/events.ts
|
|
396
396
|
var EventBus = class {
|
|
397
397
|
listeners = /* @__PURE__ */ new Map();
|
|
398
|
+
wildcards = [];
|
|
398
399
|
logger;
|
|
399
400
|
setLogger(logger) {
|
|
400
401
|
this.logger = logger;
|
|
@@ -421,24 +422,73 @@ var EventBus = class {
|
|
|
421
422
|
this.off(event, wrapper);
|
|
422
423
|
};
|
|
423
424
|
}
|
|
425
|
+
/**
|
|
426
|
+
* Subscribe to all events whose name matches a glob-style prefix.
|
|
427
|
+
* `'tool.*'` matches `tool.started`, `tool.executed`, `tool.progress`, etc.
|
|
428
|
+
* `'*'` matches every event.
|
|
429
|
+
*
|
|
430
|
+
* The handler receives `(eventName, payload)` with the event name as a
|
|
431
|
+
* string and the payload as `unknown`. Use for logging, debugging, or
|
|
432
|
+
* metrics collection across a family of events.
|
|
433
|
+
*
|
|
434
|
+
* Returns an unsubscribe function.
|
|
435
|
+
*/
|
|
436
|
+
onPattern(pattern, fn) {
|
|
437
|
+
const match = makePatternMatcher(pattern);
|
|
438
|
+
const entry = { match, fn };
|
|
439
|
+
this.wildcards.push(entry);
|
|
440
|
+
return () => {
|
|
441
|
+
const idx = this.wildcards.indexOf(entry);
|
|
442
|
+
if (idx >= 0) this.wildcards.splice(idx, 1);
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Subscribe to all events whose name matches a RegExp.
|
|
447
|
+
* More flexible than `onPattern` — use when you need regex features
|
|
448
|
+
* (alternation, character classes, capture groups).
|
|
449
|
+
*
|
|
450
|
+
* Returns an unsubscribe function.
|
|
451
|
+
*/
|
|
452
|
+
onRegex(regex, fn) {
|
|
453
|
+
const entry = { match: (e) => regex.test(e), fn };
|
|
454
|
+
this.wildcards.push(entry);
|
|
455
|
+
return () => {
|
|
456
|
+
const idx = this.wildcards.indexOf(entry);
|
|
457
|
+
if (idx >= 0) this.wildcards.splice(idx, 1);
|
|
458
|
+
};
|
|
459
|
+
}
|
|
424
460
|
emit(event, payload) {
|
|
425
461
|
const set = this.listeners.get(event);
|
|
426
|
-
if (
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
462
|
+
if (set) {
|
|
463
|
+
for (const fn of set) {
|
|
464
|
+
try {
|
|
465
|
+
fn(payload);
|
|
466
|
+
} catch (err) {
|
|
467
|
+
this.logger?.error(`EventBus listener for "${event}" threw`, err);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (this.wildcards.length > 0) {
|
|
472
|
+
const name = event;
|
|
473
|
+
for (const { match, fn } of this.wildcards) {
|
|
474
|
+
if (!match(name)) continue;
|
|
475
|
+
try {
|
|
476
|
+
fn(name, payload);
|
|
477
|
+
} catch (err) {
|
|
478
|
+
this.logger?.error(`EventBus wildcard listener for "${name}" threw`, err);
|
|
479
|
+
}
|
|
432
480
|
}
|
|
433
481
|
}
|
|
434
482
|
}
|
|
435
483
|
clear() {
|
|
436
484
|
this.listeners.clear();
|
|
485
|
+
this.wildcards.length = 0;
|
|
437
486
|
}
|
|
438
487
|
/**
|
|
439
488
|
* V2-D: introspection helper. Pass an `event` to count handlers for a
|
|
440
489
|
* single key, or omit to get the total across every event. Used by the
|
|
441
490
|
* leak-detection smoke test to flag handler accumulation across runs.
|
|
491
|
+
* Does NOT include wildcard listeners.
|
|
442
492
|
*/
|
|
443
493
|
listenerCount(event) {
|
|
444
494
|
if (event !== void 0) return this.listeners.get(event)?.size ?? 0;
|
|
@@ -446,7 +496,21 @@ var EventBus = class {
|
|
|
446
496
|
for (const set of this.listeners.values()) total += set.size;
|
|
447
497
|
return total;
|
|
448
498
|
}
|
|
499
|
+
/**
|
|
500
|
+
* Number of wildcard listeners currently registered.
|
|
501
|
+
*/
|
|
502
|
+
wildcardCount() {
|
|
503
|
+
return this.wildcards.length;
|
|
504
|
+
}
|
|
449
505
|
};
|
|
506
|
+
function makePatternMatcher(pattern) {
|
|
507
|
+
if (pattern === "*") return () => true;
|
|
508
|
+
if (pattern.endsWith(".*")) {
|
|
509
|
+
const prefix = pattern.slice(0, -2);
|
|
510
|
+
return (e) => e.startsWith(`${prefix}.`);
|
|
511
|
+
}
|
|
512
|
+
return (e) => e === pattern;
|
|
513
|
+
}
|
|
450
514
|
|
|
451
515
|
// src/kernel/tokens.ts
|
|
452
516
|
var t = (name) => Symbol(name);
|
|
@@ -468,7 +532,9 @@ var TOKENS = {
|
|
|
468
532
|
SystemPromptBuilder: t("SystemPromptBuilder"),
|
|
469
533
|
SecretScrubber: t("SecretScrubber"),
|
|
470
534
|
ModelsRegistry: t("ModelsRegistry"),
|
|
471
|
-
ModeStore: t("ModeStore")
|
|
535
|
+
ModeStore: t("ModeStore"),
|
|
536
|
+
/** Replaces the entire provider call layer — retry, streaming, tracing. */
|
|
537
|
+
ProviderRunner: t("ProviderRunner")
|
|
472
538
|
};
|
|
473
539
|
|
|
474
540
|
// src/kernel/run-controller.ts
|
|
@@ -1539,7 +1605,7 @@ async function atomicWrite(targetPath, content, opts = {}) {
|
|
|
1539
1605
|
if (mode !== void 0) {
|
|
1540
1606
|
await fsp2.chmod(tmp, mode);
|
|
1541
1607
|
}
|
|
1542
|
-
await
|
|
1608
|
+
await renameWithRetry(tmp, targetPath);
|
|
1543
1609
|
} catch (err) {
|
|
1544
1610
|
try {
|
|
1545
1611
|
await fsp2.unlink(tmp);
|
|
@@ -1551,6 +1617,29 @@ async function atomicWrite(targetPath, content, opts = {}) {
|
|
|
1551
1617
|
async function ensureDir(dir) {
|
|
1552
1618
|
await fsp2.mkdir(dir, { recursive: true });
|
|
1553
1619
|
}
|
|
1620
|
+
var TRANSIENT_RENAME_CODES = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES", "ENOTEMPTY"]);
|
|
1621
|
+
async function renameWithRetry(from, to) {
|
|
1622
|
+
if (process.platform !== "win32") {
|
|
1623
|
+
await fsp2.rename(from, to);
|
|
1624
|
+
return;
|
|
1625
|
+
}
|
|
1626
|
+
const delays = [10, 25, 60, 120, 250];
|
|
1627
|
+
let lastErr;
|
|
1628
|
+
for (let i = 0; i <= delays.length; i++) {
|
|
1629
|
+
try {
|
|
1630
|
+
await fsp2.rename(from, to);
|
|
1631
|
+
return;
|
|
1632
|
+
} catch (err) {
|
|
1633
|
+
lastErr = err;
|
|
1634
|
+
const code = err?.code;
|
|
1635
|
+
if (!code || !TRANSIENT_RENAME_CODES.has(code) || i === delays.length) {
|
|
1636
|
+
throw err;
|
|
1637
|
+
}
|
|
1638
|
+
await new Promise((resolve4) => setTimeout(resolve4, delays[i]));
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
throw lastErr;
|
|
1642
|
+
}
|
|
1554
1643
|
|
|
1555
1644
|
// src/models/models-registry.ts
|
|
1556
1645
|
var DEFAULT_URL = "https://models.dev/api.json";
|
|
@@ -3116,11 +3205,11 @@ function validateAgainstSchema(value, schema) {
|
|
|
3116
3205
|
walk2(value, schema, "", errors);
|
|
3117
3206
|
return { ok: errors.length === 0, errors };
|
|
3118
3207
|
}
|
|
3119
|
-
function walk2(value, schema,
|
|
3208
|
+
function walk2(value, schema, path18, errors) {
|
|
3120
3209
|
if (schema.enum !== void 0) {
|
|
3121
3210
|
if (!schema.enum.some((e) => deepEqual(e, value))) {
|
|
3122
3211
|
errors.push({
|
|
3123
|
-
path:
|
|
3212
|
+
path: path18 || "<root>",
|
|
3124
3213
|
message: `expected one of ${JSON.stringify(schema.enum)}, got ${JSON.stringify(value)}`
|
|
3125
3214
|
});
|
|
3126
3215
|
return;
|
|
@@ -3129,7 +3218,7 @@ function walk2(value, schema, path17, errors) {
|
|
|
3129
3218
|
if (typeof schema.type === "string") {
|
|
3130
3219
|
if (!checkType(value, schema.type)) {
|
|
3131
3220
|
errors.push({
|
|
3132
|
-
path:
|
|
3221
|
+
path: path18 || "<root>",
|
|
3133
3222
|
message: `expected ${schema.type}, got ${describeType(value)}`
|
|
3134
3223
|
});
|
|
3135
3224
|
return;
|
|
@@ -3139,19 +3228,19 @@ function walk2(value, schema, path17, errors) {
|
|
|
3139
3228
|
const obj = value;
|
|
3140
3229
|
for (const req of schema.required ?? []) {
|
|
3141
3230
|
if (!(req in obj)) {
|
|
3142
|
-
errors.push({ path: joinPath(
|
|
3231
|
+
errors.push({ path: joinPath(path18, req), message: "required property missing" });
|
|
3143
3232
|
}
|
|
3144
3233
|
}
|
|
3145
3234
|
if (schema.properties) {
|
|
3146
3235
|
for (const [key, subSchema] of Object.entries(schema.properties)) {
|
|
3147
3236
|
if (key in obj) {
|
|
3148
|
-
walk2(obj[key], subSchema, joinPath(
|
|
3237
|
+
walk2(obj[key], subSchema, joinPath(path18, key), errors);
|
|
3149
3238
|
}
|
|
3150
3239
|
}
|
|
3151
3240
|
}
|
|
3152
3241
|
}
|
|
3153
3242
|
if (schema.type === "array" && Array.isArray(value) && schema.items) {
|
|
3154
|
-
value.forEach((item, i) => walk2(item, schema.items, `${
|
|
3243
|
+
value.forEach((item, i) => walk2(item, schema.items, `${path18}[${i}]`, errors));
|
|
3155
3244
|
}
|
|
3156
3245
|
}
|
|
3157
3246
|
function checkType(value, type) {
|
|
@@ -3439,6 +3528,12 @@ var FileSessionWriter = class {
|
|
|
3439
3528
|
tokenIn = 0;
|
|
3440
3529
|
tokenOut = 0;
|
|
3441
3530
|
filePath;
|
|
3531
|
+
/** Public accessor for the JSONL path — required by SessionWriter so
|
|
3532
|
+
* observability surfaces (`/fleet log`, FleetPanel) can locate the
|
|
3533
|
+
* transcript without recomputing the path from session metadata. */
|
|
3534
|
+
get transcriptPath() {
|
|
3535
|
+
return this.filePath || void 0;
|
|
3536
|
+
}
|
|
3442
3537
|
initDone = false;
|
|
3443
3538
|
resumed;
|
|
3444
3539
|
appendFailCount = 0;
|
|
@@ -4368,6 +4463,272 @@ var SessionAnalyzer = class {
|
|
|
4368
4463
|
return last - first;
|
|
4369
4464
|
}
|
|
4370
4465
|
};
|
|
4466
|
+
async function loadTodosCheckpoint(filePath) {
|
|
4467
|
+
let raw;
|
|
4468
|
+
try {
|
|
4469
|
+
raw = await fsp2.readFile(filePath, "utf8");
|
|
4470
|
+
} catch {
|
|
4471
|
+
return null;
|
|
4472
|
+
}
|
|
4473
|
+
try {
|
|
4474
|
+
const parsed = JSON.parse(raw);
|
|
4475
|
+
if (parsed?.version !== 1 || !Array.isArray(parsed.todos)) return null;
|
|
4476
|
+
return parsed.todos.filter(
|
|
4477
|
+
(t2) => !!t2 && typeof t2.id === "string" && typeof t2.content === "string" && typeof t2.status === "string"
|
|
4478
|
+
);
|
|
4479
|
+
} catch {
|
|
4480
|
+
return null;
|
|
4481
|
+
}
|
|
4482
|
+
}
|
|
4483
|
+
async function saveTodosCheckpoint(filePath, sessionId, todos) {
|
|
4484
|
+
const payload = {
|
|
4485
|
+
version: 1,
|
|
4486
|
+
sessionId,
|
|
4487
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4488
|
+
todos: [...todos]
|
|
4489
|
+
};
|
|
4490
|
+
try {
|
|
4491
|
+
await atomicWrite(filePath, JSON.stringify(payload, null, 2), { mode: 384 });
|
|
4492
|
+
} catch (err) {
|
|
4493
|
+
console.warn(
|
|
4494
|
+
"[todos-checkpoint] save failed:",
|
|
4495
|
+
err instanceof Error ? err.message : String(err)
|
|
4496
|
+
);
|
|
4497
|
+
}
|
|
4498
|
+
}
|
|
4499
|
+
function attachTodosCheckpoint(state, filePath, sessionId) {
|
|
4500
|
+
let timer = null;
|
|
4501
|
+
let pending = null;
|
|
4502
|
+
const flush = () => {
|
|
4503
|
+
timer = null;
|
|
4504
|
+
if (pending) {
|
|
4505
|
+
void saveTodosCheckpoint(filePath, sessionId, pending);
|
|
4506
|
+
pending = null;
|
|
4507
|
+
}
|
|
4508
|
+
};
|
|
4509
|
+
const unsubscribe = state.onChange((change) => {
|
|
4510
|
+
if (change.kind !== "todos_replaced") return;
|
|
4511
|
+
pending = change.todos;
|
|
4512
|
+
if (timer) clearTimeout(timer);
|
|
4513
|
+
timer = setTimeout(flush, 150);
|
|
4514
|
+
});
|
|
4515
|
+
return () => {
|
|
4516
|
+
unsubscribe();
|
|
4517
|
+
if (timer) {
|
|
4518
|
+
clearTimeout(timer);
|
|
4519
|
+
flush();
|
|
4520
|
+
}
|
|
4521
|
+
};
|
|
4522
|
+
}
|
|
4523
|
+
async function loadPlan(filePath) {
|
|
4524
|
+
let raw;
|
|
4525
|
+
try {
|
|
4526
|
+
raw = await fsp2.readFile(filePath, "utf8");
|
|
4527
|
+
} catch {
|
|
4528
|
+
return null;
|
|
4529
|
+
}
|
|
4530
|
+
try {
|
|
4531
|
+
const parsed = JSON.parse(raw);
|
|
4532
|
+
if (parsed?.version !== 1 || !Array.isArray(parsed.items)) return null;
|
|
4533
|
+
return parsed;
|
|
4534
|
+
} catch {
|
|
4535
|
+
return null;
|
|
4536
|
+
}
|
|
4537
|
+
}
|
|
4538
|
+
async function savePlan(filePath, plan) {
|
|
4539
|
+
try {
|
|
4540
|
+
await atomicWrite(filePath, JSON.stringify(plan, null, 2), { mode: 384 });
|
|
4541
|
+
} catch (err) {
|
|
4542
|
+
console.warn(
|
|
4543
|
+
"[plan-store] save failed:",
|
|
4544
|
+
err instanceof Error ? err.message : String(err)
|
|
4545
|
+
);
|
|
4546
|
+
}
|
|
4547
|
+
}
|
|
4548
|
+
function emptyPlan(sessionId, title) {
|
|
4549
|
+
return {
|
|
4550
|
+
version: 1,
|
|
4551
|
+
sessionId,
|
|
4552
|
+
title,
|
|
4553
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4554
|
+
items: []
|
|
4555
|
+
};
|
|
4556
|
+
}
|
|
4557
|
+
function addPlanItem(plan, title, details) {
|
|
4558
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4559
|
+
const item = {
|
|
4560
|
+
id: `plan_${Date.now()}_${randomUUID().slice(0, 6)}`,
|
|
4561
|
+
title,
|
|
4562
|
+
details,
|
|
4563
|
+
status: "open",
|
|
4564
|
+
createdAt: now,
|
|
4565
|
+
updatedAt: now
|
|
4566
|
+
};
|
|
4567
|
+
return {
|
|
4568
|
+
plan: { ...plan, items: [...plan.items, item], updatedAt: now },
|
|
4569
|
+
item
|
|
4570
|
+
};
|
|
4571
|
+
}
|
|
4572
|
+
function removePlanItem(plan, idOrIndex) {
|
|
4573
|
+
const idx = matchIndex(plan, idOrIndex);
|
|
4574
|
+
if (idx === -1) return plan;
|
|
4575
|
+
return {
|
|
4576
|
+
...plan,
|
|
4577
|
+
items: plan.items.filter((_, i) => i !== idx),
|
|
4578
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4579
|
+
};
|
|
4580
|
+
}
|
|
4581
|
+
function setPlanItemStatus(plan, idOrIndex, status) {
|
|
4582
|
+
const idx = matchIndex(plan, idOrIndex);
|
|
4583
|
+
if (idx === -1) return plan;
|
|
4584
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4585
|
+
const items = plan.items.map(
|
|
4586
|
+
(it, i) => i === idx ? { ...it, status, updatedAt: now } : it
|
|
4587
|
+
);
|
|
4588
|
+
return { ...plan, items, updatedAt: now };
|
|
4589
|
+
}
|
|
4590
|
+
function clearPlan(plan) {
|
|
4591
|
+
return { ...plan, items: [], updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
4592
|
+
}
|
|
4593
|
+
function formatPlan(plan) {
|
|
4594
|
+
if (plan.items.length === 0) return "Plan is empty.";
|
|
4595
|
+
const lines = [];
|
|
4596
|
+
if (plan.title) lines.push(`# ${plan.title}`);
|
|
4597
|
+
plan.items.forEach((it, i) => {
|
|
4598
|
+
const mark = it.status === "done" ? "[x]" : it.status === "in_progress" ? "[~]" : "[ ]";
|
|
4599
|
+
lines.push(`${i + 1}. ${mark} ${it.title}`);
|
|
4600
|
+
if (it.details) {
|
|
4601
|
+
for (const line of it.details.split("\n")) lines.push(` ${line}`);
|
|
4602
|
+
}
|
|
4603
|
+
});
|
|
4604
|
+
return lines.join("\n");
|
|
4605
|
+
}
|
|
4606
|
+
function matchIndex(plan, idOrIndex) {
|
|
4607
|
+
const asNum = Number.parseInt(idOrIndex, 10);
|
|
4608
|
+
if (!Number.isNaN(asNum) && asNum >= 1 && asNum <= plan.items.length) return asNum - 1;
|
|
4609
|
+
const byId = plan.items.findIndex((it) => it.id === idOrIndex);
|
|
4610
|
+
if (byId !== -1) return byId;
|
|
4611
|
+
const lower = idOrIndex.toLowerCase();
|
|
4612
|
+
return plan.items.findIndex((it) => it.title.toLowerCase().includes(lower));
|
|
4613
|
+
}
|
|
4614
|
+
function attachPlanCheckpoint(_state, _filePath, _sessionId) {
|
|
4615
|
+
return () => void 0;
|
|
4616
|
+
}
|
|
4617
|
+
async function loadDirectorState(filePath) {
|
|
4618
|
+
let raw;
|
|
4619
|
+
try {
|
|
4620
|
+
raw = await fsp2.readFile(filePath, "utf8");
|
|
4621
|
+
} catch {
|
|
4622
|
+
return null;
|
|
4623
|
+
}
|
|
4624
|
+
try {
|
|
4625
|
+
const parsed = JSON.parse(raw);
|
|
4626
|
+
if (parsed?.version !== 1) return null;
|
|
4627
|
+
return parsed;
|
|
4628
|
+
} catch {
|
|
4629
|
+
return null;
|
|
4630
|
+
}
|
|
4631
|
+
}
|
|
4632
|
+
var DirectorStateCheckpoint = class {
|
|
4633
|
+
snapshot;
|
|
4634
|
+
filePath;
|
|
4635
|
+
timer = null;
|
|
4636
|
+
debounceMs;
|
|
4637
|
+
writing = false;
|
|
4638
|
+
rewriteRequested = false;
|
|
4639
|
+
constructor(filePath, init, debounceMs = 250) {
|
|
4640
|
+
this.filePath = filePath;
|
|
4641
|
+
this.debounceMs = debounceMs;
|
|
4642
|
+
this.snapshot = {
|
|
4643
|
+
version: 1,
|
|
4644
|
+
directorRunId: init.directorRunId,
|
|
4645
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4646
|
+
spawnCount: 0,
|
|
4647
|
+
maxSpawns: init.maxSpawns,
|
|
4648
|
+
spawnDepth: init.spawnDepth,
|
|
4649
|
+
maxSpawnDepth: init.maxSpawnDepth,
|
|
4650
|
+
subagents: [],
|
|
4651
|
+
tasks: []
|
|
4652
|
+
};
|
|
4653
|
+
}
|
|
4654
|
+
current() {
|
|
4655
|
+
return this.snapshot;
|
|
4656
|
+
}
|
|
4657
|
+
recordSpawn(sub, spawnCount) {
|
|
4658
|
+
this.snapshot = {
|
|
4659
|
+
...this.snapshot,
|
|
4660
|
+
spawnCount,
|
|
4661
|
+
subagents: [...this.snapshot.subagents.filter((s) => s.id !== sub.id), sub]
|
|
4662
|
+
};
|
|
4663
|
+
this.bumpUpdatedAt();
|
|
4664
|
+
this.schedule();
|
|
4665
|
+
}
|
|
4666
|
+
recordTaskAssigned(task) {
|
|
4667
|
+
const exists = this.snapshot.tasks.some((t2) => t2.taskId === task.taskId);
|
|
4668
|
+
this.snapshot = {
|
|
4669
|
+
...this.snapshot,
|
|
4670
|
+
tasks: exists ? this.snapshot.tasks.map((t2) => t2.taskId === task.taskId ? { ...t2, ...task } : t2) : [...this.snapshot.tasks, task]
|
|
4671
|
+
};
|
|
4672
|
+
this.bumpUpdatedAt();
|
|
4673
|
+
this.schedule();
|
|
4674
|
+
}
|
|
4675
|
+
recordTaskStatus(taskId, patch) {
|
|
4676
|
+
this.snapshot = {
|
|
4677
|
+
...this.snapshot,
|
|
4678
|
+
tasks: this.snapshot.tasks.map(
|
|
4679
|
+
(t2) => t2.taskId === taskId ? { ...t2, ...patch } : t2
|
|
4680
|
+
)
|
|
4681
|
+
};
|
|
4682
|
+
this.bumpUpdatedAt();
|
|
4683
|
+
this.schedule();
|
|
4684
|
+
}
|
|
4685
|
+
setUsage(usage) {
|
|
4686
|
+
this.snapshot = { ...this.snapshot, usage };
|
|
4687
|
+
this.bumpUpdatedAt();
|
|
4688
|
+
this.schedule();
|
|
4689
|
+
}
|
|
4690
|
+
/** Force a synchronous flush — used by Director.shutdown(). */
|
|
4691
|
+
async flush() {
|
|
4692
|
+
if (this.timer) {
|
|
4693
|
+
clearTimeout(this.timer);
|
|
4694
|
+
this.timer = null;
|
|
4695
|
+
}
|
|
4696
|
+
await this.persist();
|
|
4697
|
+
}
|
|
4698
|
+
bumpUpdatedAt() {
|
|
4699
|
+
this.snapshot = { ...this.snapshot, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
4700
|
+
}
|
|
4701
|
+
schedule() {
|
|
4702
|
+
if (this.timer) return;
|
|
4703
|
+
this.timer = setTimeout(() => {
|
|
4704
|
+
this.timer = null;
|
|
4705
|
+
void this.persist();
|
|
4706
|
+
}, this.debounceMs);
|
|
4707
|
+
}
|
|
4708
|
+
async persist() {
|
|
4709
|
+
if (this.writing) {
|
|
4710
|
+
this.rewriteRequested = true;
|
|
4711
|
+
return;
|
|
4712
|
+
}
|
|
4713
|
+
this.writing = true;
|
|
4714
|
+
try {
|
|
4715
|
+
await atomicWrite(this.filePath, JSON.stringify(this.snapshot, null, 2), {
|
|
4716
|
+
mode: 384
|
|
4717
|
+
});
|
|
4718
|
+
} catch (err) {
|
|
4719
|
+
console.warn(
|
|
4720
|
+
"[director-state] checkpoint write failed:",
|
|
4721
|
+
err instanceof Error ? err.message : String(err)
|
|
4722
|
+
);
|
|
4723
|
+
} finally {
|
|
4724
|
+
this.writing = false;
|
|
4725
|
+
if (this.rewriteRequested) {
|
|
4726
|
+
this.rewriteRequested = false;
|
|
4727
|
+
this.schedule();
|
|
4728
|
+
}
|
|
4729
|
+
}
|
|
4730
|
+
}
|
|
4731
|
+
};
|
|
4371
4732
|
var DefaultPermissionPolicy = class {
|
|
4372
4733
|
policy = {};
|
|
4373
4734
|
loaded = false;
|
|
@@ -4476,6 +4837,18 @@ var DefaultPermissionPolicy = class {
|
|
|
4476
4837
|
return void 0;
|
|
4477
4838
|
}
|
|
4478
4839
|
};
|
|
4840
|
+
var AutoApprovePermissionPolicy = class {
|
|
4841
|
+
async evaluate(tool) {
|
|
4842
|
+
if (tool.permission === "deny") {
|
|
4843
|
+
return { permission: "deny", source: "default", reason: "tool default deny" };
|
|
4844
|
+
}
|
|
4845
|
+
return { permission: "auto", source: "yolo" };
|
|
4846
|
+
}
|
|
4847
|
+
async trust() {
|
|
4848
|
+
}
|
|
4849
|
+
async reload() {
|
|
4850
|
+
}
|
|
4851
|
+
};
|
|
4479
4852
|
var DefaultSkillLoader = class {
|
|
4480
4853
|
dirs;
|
|
4481
4854
|
cache;
|
|
@@ -4605,103 +4978,399 @@ function parseDescription(raw) {
|
|
|
4605
4978
|
return { trigger, scope };
|
|
4606
4979
|
}
|
|
4607
4980
|
|
|
4608
|
-
// src/
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
this.hardThreshold = opts.hardThreshold ?? 0.9;
|
|
4624
|
-
this.maxContext = opts.maxContext ?? 128e3;
|
|
4625
|
-
this.preserveK = opts.preserveK ?? 4;
|
|
4626
|
-
this.eliseThreshold = opts.eliseThreshold ?? 500;
|
|
4627
|
-
this.summarizerPrompt = opts.summarizerPrompt ?? "You are a context summarizer. Given a list of conversation messages, produce a concise but complete summary that preserves all factual information, decisions made, and any state changes (e.g. file edits, todo updates). Do not add commentary. Output only the summary.";
|
|
4628
|
-
this.summarizerModel = opts.summarizerModel;
|
|
4629
|
-
}
|
|
4630
|
-
async compact(ctx, opts = {}) {
|
|
4631
|
-
const beforeTokens = this.estimateTokens(ctx.messages);
|
|
4632
|
-
const reductions = [];
|
|
4633
|
-
const load = beforeTokens / this.maxContext;
|
|
4634
|
-
const aggressive = load >= this.hardThreshold ? true : opts.aggressive ?? load >= this.softThreshold;
|
|
4635
|
-
const saved1 = this.eliseOldToolResults(ctx);
|
|
4636
|
-
if (saved1 > 0) reductions.push({ phase: "elision", saved: saved1 });
|
|
4637
|
-
if (aggressive) {
|
|
4638
|
-
const saved2 = await this.summarizeAncientTurns(ctx);
|
|
4639
|
-
if (saved2 > 0) reductions.push({ phase: "summary", saved: saved2 });
|
|
4640
|
-
} else if (load >= this.warnThreshold) {
|
|
4641
|
-
const saved2 = this.lightweightCompact(ctx);
|
|
4642
|
-
if (saved2 > 0) reductions.push({ phase: "elision", saved: saved2 });
|
|
4643
|
-
}
|
|
4644
|
-
const afterTokens = this.estimateTokens(ctx.messages);
|
|
4645
|
-
return { before: beforeTokens, after: afterTokens, reductions };
|
|
4646
|
-
}
|
|
4647
|
-
async summarizeAncientTurns(ctx) {
|
|
4648
|
-
const messages = ctx.messages;
|
|
4649
|
-
const cutoff = Math.max(0, messages.length - this.preserveK * 2);
|
|
4650
|
-
if (cutoff <= 2) return 0;
|
|
4651
|
-
const boundary = this.findSafeBoundary(messages, 0, cutoff);
|
|
4652
|
-
if (boundary <= 1) return 0;
|
|
4653
|
-
const toSummarize = messages.slice(0, boundary);
|
|
4654
|
-
const removedTokens = this.estimateTokens(toSummarize);
|
|
4655
|
-
let summaryText;
|
|
4656
|
-
try {
|
|
4657
|
-
summaryText = await this.callSummarizer(toSummarize, ctx);
|
|
4658
|
-
} catch {
|
|
4659
|
-
summaryText = `[${toSummarize.length} earlier turns omitted \u2014 key decisions and file states preserved in context]`;
|
|
4660
|
-
}
|
|
4661
|
-
const summaryMsg = {
|
|
4662
|
-
role: "system",
|
|
4663
|
-
content: `[prior_turns_summary: ${summaryText}]`
|
|
4664
|
-
};
|
|
4665
|
-
const summaryTokens = this.estimateTokens([summaryMsg]);
|
|
4666
|
-
const tail = ctx.messages.slice(boundary);
|
|
4667
|
-
ctx.state.replaceMessages([summaryMsg, ...tail]);
|
|
4668
|
-
return Math.max(0, removedTokens - summaryTokens);
|
|
4669
|
-
}
|
|
4670
|
-
findSafeBoundary(messages, from, to) {
|
|
4671
|
-
for (let i = to; i >= from; i--) {
|
|
4672
|
-
const m = messages[i];
|
|
4673
|
-
if (!m) continue;
|
|
4674
|
-
if (m.role === "user" && this.hasTextContent(m)) {
|
|
4675
|
-
return this.findExchangeStart(messages, i);
|
|
4981
|
+
// src/core/streaming-response-builder.ts
|
|
4982
|
+
function buildResponse(state) {
|
|
4983
|
+
const content = [];
|
|
4984
|
+
for (const b of state.blockOrder) {
|
|
4985
|
+
if (b.kind === "text") {
|
|
4986
|
+
const txt = state.textBuffers[b.idx] ?? "";
|
|
4987
|
+
if (txt) content.push({ type: "text", text: txt });
|
|
4988
|
+
} else if (b.kind === "thinking") {
|
|
4989
|
+
const t2 = state.thinking[b.idx];
|
|
4990
|
+
if (!t2) continue;
|
|
4991
|
+
if (!t2.textBuf && !t2.signature) continue;
|
|
4992
|
+
const block = { type: "thinking", thinking: t2.textBuf };
|
|
4993
|
+
if (t2.signature) block.signature = t2.signature;
|
|
4994
|
+
if (t2.providerMeta && Object.keys(t2.providerMeta).length > 0) {
|
|
4995
|
+
block.providerMeta = t2.providerMeta;
|
|
4676
4996
|
}
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4997
|
+
content.push(block);
|
|
4998
|
+
} else {
|
|
4999
|
+
const tb = state.tools.get(b.id);
|
|
5000
|
+
if (tb) {
|
|
5001
|
+
const block = {
|
|
5002
|
+
type: "tool_use",
|
|
5003
|
+
id: b.id,
|
|
5004
|
+
name: tb.name,
|
|
5005
|
+
input: tb.input ?? {}
|
|
5006
|
+
};
|
|
5007
|
+
if (tb.providerMeta && Object.keys(tb.providerMeta).length > 0) {
|
|
5008
|
+
block.providerMeta = tb.providerMeta;
|
|
4688
5009
|
}
|
|
4689
|
-
|
|
4690
|
-
return i;
|
|
5010
|
+
content.push(block);
|
|
4691
5011
|
}
|
|
4692
5012
|
}
|
|
4693
|
-
return 0;
|
|
4694
5013
|
}
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
5014
|
+
if (content.length === 0) content.push({ type: "text", text: "" });
|
|
5015
|
+
return { content, stopReason: state.stopReason, usage: state.usage, model: state.model };
|
|
5016
|
+
}
|
|
5017
|
+
function createStreamingState(model) {
|
|
5018
|
+
return {
|
|
5019
|
+
model,
|
|
5020
|
+
stopReason: "end_turn",
|
|
5021
|
+
usage: { input: 0, output: 0 },
|
|
5022
|
+
textBuffers: [],
|
|
5023
|
+
currentTextIndex: -1,
|
|
5024
|
+
tools: /* @__PURE__ */ new Map(),
|
|
5025
|
+
thinking: [],
|
|
5026
|
+
currentThinkingIndex: -1,
|
|
5027
|
+
blockOrder: []
|
|
5028
|
+
};
|
|
5029
|
+
}
|
|
5030
|
+
function handleMessageStart(state, model) {
|
|
5031
|
+
state.model = model;
|
|
5032
|
+
}
|
|
5033
|
+
function handleContentBlockStart(state, ev) {
|
|
5034
|
+
const kind = ev.kind ?? "text";
|
|
5035
|
+
if (kind === "text") {
|
|
5036
|
+
state.currentTextIndex = state.textBuffers.length;
|
|
5037
|
+
state.textBuffers.push("");
|
|
5038
|
+
state.blockOrder.push({ kind: "text", idx: state.currentTextIndex });
|
|
5039
|
+
} else if (kind === "tool_use") {
|
|
5040
|
+
const id = ev.id ?? crypto.randomUUID();
|
|
5041
|
+
state.tools.set(id, { name: ev.name ?? "unknown", partial: "" });
|
|
5042
|
+
state.blockOrder.push({ kind: "tool", id });
|
|
5043
|
+
state.currentTextIndex = -1;
|
|
5044
|
+
} else if (kind === "thinking") {
|
|
5045
|
+
state.currentThinkingIndex = state.thinking.length;
|
|
5046
|
+
state.thinking.push({
|
|
5047
|
+
textBuf: "",
|
|
5048
|
+
...ev.providerMeta ? { providerMeta: ev.providerMeta } : {}
|
|
5049
|
+
});
|
|
5050
|
+
state.blockOrder.push({ kind: "thinking", idx: state.currentThinkingIndex });
|
|
5051
|
+
state.currentTextIndex = -1;
|
|
5052
|
+
}
|
|
5053
|
+
}
|
|
5054
|
+
function handleContentBlockStop(state, ev) {
|
|
5055
|
+
}
|
|
5056
|
+
function handleTextDelta(state, text) {
|
|
5057
|
+
if (state.currentTextIndex === -1) {
|
|
5058
|
+
state.currentTextIndex = state.textBuffers.length;
|
|
5059
|
+
state.textBuffers.push("");
|
|
5060
|
+
state.blockOrder.push({ kind: "text", idx: state.currentTextIndex });
|
|
5061
|
+
}
|
|
5062
|
+
state.textBuffers[state.currentTextIndex] = (state.textBuffers[state.currentTextIndex] ?? "") + text;
|
|
5063
|
+
}
|
|
5064
|
+
function handleToolUseStart(state, ev) {
|
|
5065
|
+
state.currentTextIndex = -1;
|
|
5066
|
+
state.tools.set(ev.id, { name: ev.name, partial: "" });
|
|
5067
|
+
state.blockOrder.push({ kind: "tool", id: ev.id });
|
|
5068
|
+
}
|
|
5069
|
+
function handleToolUseInputDelta(state, ev) {
|
|
5070
|
+
const t2 = state.tools.get(ev.id);
|
|
5071
|
+
if (t2) t2.partial += ev.partial;
|
|
5072
|
+
}
|
|
5073
|
+
function safeJsonOrRaw(s) {
|
|
5074
|
+
if (!s) return {};
|
|
5075
|
+
try {
|
|
5076
|
+
return JSON.parse(s);
|
|
5077
|
+
} catch {
|
|
5078
|
+
return { _raw: s };
|
|
5079
|
+
}
|
|
5080
|
+
}
|
|
5081
|
+
function handleToolUseStop(state, ev) {
|
|
5082
|
+
const t2 = state.tools.get(ev.id);
|
|
5083
|
+
if (t2) {
|
|
5084
|
+
t2.input = ev.input !== void 0 ? ev.input : safeJsonOrRaw(t2.partial);
|
|
5085
|
+
if (ev.providerMeta) t2.providerMeta = ev.providerMeta;
|
|
5086
|
+
}
|
|
5087
|
+
state.currentTextIndex = -1;
|
|
5088
|
+
}
|
|
5089
|
+
function handleThinkingStart(state, ev) {
|
|
5090
|
+
state.currentThinkingIndex = state.thinking.length;
|
|
5091
|
+
state.thinking.push({
|
|
5092
|
+
textBuf: "",
|
|
5093
|
+
...ev.providerMeta ? { providerMeta: ev.providerMeta } : {}
|
|
5094
|
+
});
|
|
5095
|
+
state.blockOrder.push({ kind: "thinking", idx: state.currentThinkingIndex });
|
|
5096
|
+
state.currentTextIndex = -1;
|
|
5097
|
+
}
|
|
5098
|
+
function handleThinkingDelta(state, text) {
|
|
5099
|
+
if (state.currentThinkingIndex === -1) {
|
|
5100
|
+
handleThinkingStart(state, {});
|
|
5101
|
+
}
|
|
5102
|
+
const t2 = state.thinking[state.currentThinkingIndex];
|
|
5103
|
+
if (t2) t2.textBuf += text;
|
|
5104
|
+
}
|
|
5105
|
+
function handleThinkingSignature(state, signature) {
|
|
5106
|
+
if (state.currentThinkingIndex === -1) {
|
|
5107
|
+
handleThinkingStart(state, {});
|
|
5108
|
+
}
|
|
5109
|
+
const t2 = state.thinking[state.currentThinkingIndex];
|
|
5110
|
+
if (t2) t2.signature = signature;
|
|
5111
|
+
}
|
|
5112
|
+
function handleThinkingStop(state) {
|
|
5113
|
+
state.currentThinkingIndex = -1;
|
|
5114
|
+
}
|
|
5115
|
+
function handleMessageStop(state, ev) {
|
|
5116
|
+
state.stopReason = ev.stopReason ?? "end_turn";
|
|
5117
|
+
state.usage = ev.usage ?? { input: 0, output: 0 };
|
|
5118
|
+
}
|
|
5119
|
+
async function streamProviderToResponse(provider, req, signal, ctx, events) {
|
|
5120
|
+
const state = createStreamingState(req.model);
|
|
5121
|
+
const iter = provider.stream(req, { signal })[Symbol.asyncIterator]();
|
|
5122
|
+
try {
|
|
5123
|
+
for (; ; ) {
|
|
5124
|
+
const next = await iter.next();
|
|
5125
|
+
if (next.done) break;
|
|
5126
|
+
const ev = next.value;
|
|
5127
|
+
switch (ev.type) {
|
|
5128
|
+
case "message_start":
|
|
5129
|
+
handleMessageStart(state, ev.model);
|
|
5130
|
+
break;
|
|
5131
|
+
case "content_block_start":
|
|
5132
|
+
handleContentBlockStart(state, ev);
|
|
5133
|
+
break;
|
|
5134
|
+
case "content_block_stop":
|
|
5135
|
+
handleContentBlockStop(state, ev);
|
|
5136
|
+
break;
|
|
5137
|
+
case "text_delta":
|
|
5138
|
+
handleTextDelta(state, ev.text);
|
|
5139
|
+
events.emit("provider.text_delta", { ctx, text: ev.text });
|
|
5140
|
+
break;
|
|
5141
|
+
case "tool_use_start":
|
|
5142
|
+
handleToolUseStart(state, ev);
|
|
5143
|
+
events.emit("provider.tool_use_start", { ctx, id: ev.id, name: ev.name });
|
|
5144
|
+
break;
|
|
5145
|
+
case "tool_use_input_delta":
|
|
5146
|
+
handleToolUseInputDelta(state, ev);
|
|
5147
|
+
break;
|
|
5148
|
+
case "tool_use_stop":
|
|
5149
|
+
handleToolUseStop(state, ev);
|
|
5150
|
+
events.emit("provider.tool_use_stop", { ctx, id: ev.id });
|
|
5151
|
+
break;
|
|
5152
|
+
case "thinking_start":
|
|
5153
|
+
handleThinkingStart(state, ev);
|
|
5154
|
+
break;
|
|
5155
|
+
case "thinking_delta":
|
|
5156
|
+
handleThinkingDelta(state, ev.text);
|
|
5157
|
+
events.emit("provider.thinking_delta", { ctx, text: ev.text });
|
|
5158
|
+
break;
|
|
5159
|
+
case "thinking_signature":
|
|
5160
|
+
handleThinkingSignature(state, ev.signature);
|
|
5161
|
+
break;
|
|
5162
|
+
case "thinking_stop":
|
|
5163
|
+
handleThinkingStop(state);
|
|
5164
|
+
break;
|
|
5165
|
+
case "message_stop":
|
|
5166
|
+
handleMessageStop(state, ev);
|
|
5167
|
+
break;
|
|
5168
|
+
}
|
|
5169
|
+
}
|
|
5170
|
+
} catch (err) {
|
|
5171
|
+
if (signal.aborted) {
|
|
5172
|
+
state.stopReason = "end_turn";
|
|
5173
|
+
return buildResponse(state);
|
|
5174
|
+
}
|
|
5175
|
+
throw err;
|
|
5176
|
+
} finally {
|
|
5177
|
+
try {
|
|
5178
|
+
let drainTimer = null;
|
|
5179
|
+
try {
|
|
5180
|
+
await Promise.race([
|
|
5181
|
+
Promise.resolve(iter.return?.()),
|
|
5182
|
+
new Promise((resolve4) => {
|
|
5183
|
+
drainTimer = setTimeout(resolve4, 500);
|
|
5184
|
+
})
|
|
5185
|
+
]);
|
|
5186
|
+
} finally {
|
|
5187
|
+
if (drainTimer) clearTimeout(drainTimer);
|
|
5188
|
+
}
|
|
5189
|
+
} catch {
|
|
5190
|
+
}
|
|
5191
|
+
}
|
|
5192
|
+
return buildResponse(state);
|
|
5193
|
+
}
|
|
5194
|
+
|
|
5195
|
+
// src/core/provider-runner.ts
|
|
5196
|
+
async function runProviderWithRetry(opts) {
|
|
5197
|
+
const { provider, request, signal, ctx, events, retry, logger, tracer } = opts;
|
|
5198
|
+
let attempt = 0;
|
|
5199
|
+
for (; ; ) {
|
|
5200
|
+
const span = tracer?.startSpan("provider.complete", {
|
|
5201
|
+
"provider.id": provider.id,
|
|
5202
|
+
"provider.model": request.model,
|
|
5203
|
+
"provider.streaming": provider.capabilities.streaming,
|
|
5204
|
+
"provider.attempt": attempt
|
|
5205
|
+
});
|
|
5206
|
+
try {
|
|
5207
|
+
const res = provider.capabilities.streaming ? await streamProviderToResponse(provider, request, signal, ctx, events) : await provider.complete(request, { signal });
|
|
5208
|
+
span?.setAttribute("provider.stopReason", res.stopReason);
|
|
5209
|
+
span?.setAttribute("provider.usage_in", res.usage.input);
|
|
5210
|
+
span?.setAttribute("provider.usage_out", res.usage.output);
|
|
5211
|
+
span?.end();
|
|
5212
|
+
return res;
|
|
5213
|
+
} catch (err) {
|
|
5214
|
+
if (err instanceof Error) span?.recordError(err);
|
|
5215
|
+
span?.end();
|
|
5216
|
+
if (signal.aborted) throw err;
|
|
5217
|
+
const isProviderErr = err instanceof ProviderError;
|
|
5218
|
+
const errAsErr = err instanceof Error ? err : new Error(String(err));
|
|
5219
|
+
const canRetry = retry.shouldRetry(isProviderErr ? err : errAsErr, attempt);
|
|
5220
|
+
const description = isProviderErr ? err.describe() : errAsErr.message;
|
|
5221
|
+
if (!canRetry) {
|
|
5222
|
+
if (isProviderErr) {
|
|
5223
|
+
events.emit("provider.error", {
|
|
5224
|
+
providerId: err.providerId,
|
|
5225
|
+
status: err.status,
|
|
5226
|
+
description,
|
|
5227
|
+
retryable: false
|
|
5228
|
+
});
|
|
5229
|
+
}
|
|
5230
|
+
throw err;
|
|
5231
|
+
}
|
|
5232
|
+
const delay = Math.round(retry.delayMs(attempt));
|
|
5233
|
+
const attemptNum = attempt + 1;
|
|
5234
|
+
logger.warn(`Provider retry ${attemptNum} in ${delay}ms \u2014 ${description}`);
|
|
5235
|
+
if (isProviderErr) {
|
|
5236
|
+
events.emit("provider.retry", {
|
|
5237
|
+
providerId: err.providerId,
|
|
5238
|
+
attempt: attemptNum,
|
|
5239
|
+
delayMs: delay,
|
|
5240
|
+
status: err.status,
|
|
5241
|
+
description
|
|
5242
|
+
});
|
|
5243
|
+
}
|
|
5244
|
+
await new Promise((resolve4, reject) => {
|
|
5245
|
+
let settled = false;
|
|
5246
|
+
const onAbort = () => {
|
|
5247
|
+
if (settled) return;
|
|
5248
|
+
settled = true;
|
|
5249
|
+
clearTimeout(t2);
|
|
5250
|
+
reject(new Error("aborted"));
|
|
5251
|
+
};
|
|
5252
|
+
const t2 = setTimeout(() => {
|
|
5253
|
+
if (settled) return;
|
|
5254
|
+
settled = true;
|
|
5255
|
+
clearTimeout(t2);
|
|
5256
|
+
signal.removeEventListener("abort", onAbort);
|
|
5257
|
+
resolve4();
|
|
5258
|
+
}, delay);
|
|
5259
|
+
if (signal.aborted) {
|
|
5260
|
+
onAbort();
|
|
5261
|
+
return;
|
|
5262
|
+
}
|
|
5263
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
5264
|
+
});
|
|
5265
|
+
attempt++;
|
|
5266
|
+
}
|
|
5267
|
+
}
|
|
5268
|
+
}
|
|
5269
|
+
|
|
5270
|
+
// src/execution/provider-runner-impl.ts
|
|
5271
|
+
var DefaultProviderRunner = class {
|
|
5272
|
+
async run(opts) {
|
|
5273
|
+
return runProviderWithRetry(opts);
|
|
5274
|
+
}
|
|
5275
|
+
};
|
|
5276
|
+
|
|
5277
|
+
// src/execution/intelligent-compactor.ts
|
|
5278
|
+
var IntelligentCompactor = class {
|
|
5279
|
+
provider;
|
|
5280
|
+
warnThreshold;
|
|
5281
|
+
softThreshold;
|
|
5282
|
+
hardThreshold;
|
|
5283
|
+
maxContext;
|
|
5284
|
+
preserveK;
|
|
5285
|
+
eliseThreshold;
|
|
5286
|
+
summarizerPrompt;
|
|
5287
|
+
summarizerModel;
|
|
5288
|
+
constructor(opts) {
|
|
5289
|
+
this.provider = opts.provider;
|
|
5290
|
+
this.warnThreshold = opts.warnThreshold ?? 0.6;
|
|
5291
|
+
this.softThreshold = opts.softThreshold ?? 0.75;
|
|
5292
|
+
this.hardThreshold = opts.hardThreshold ?? 0.9;
|
|
5293
|
+
this.maxContext = opts.maxContext ?? 128e3;
|
|
5294
|
+
this.preserveK = opts.preserveK ?? 4;
|
|
5295
|
+
this.eliseThreshold = opts.eliseThreshold ?? 500;
|
|
5296
|
+
this.summarizerPrompt = opts.summarizerPrompt ?? "You are a context summarizer. Given a list of conversation messages, produce a concise but complete summary that preserves all factual information, decisions made, and any state changes (e.g. file edits, todo updates). Do not add commentary. Output only the summary.";
|
|
5297
|
+
this.summarizerModel = opts.summarizerModel;
|
|
5298
|
+
}
|
|
5299
|
+
async compact(ctx, opts = {}) {
|
|
5300
|
+
const beforeTokens = this.estimateTokens(ctx.messages);
|
|
5301
|
+
const reductions = [];
|
|
5302
|
+
const load = beforeTokens / this.maxContext;
|
|
5303
|
+
const aggressive = load >= this.hardThreshold ? true : opts.aggressive ?? load >= this.softThreshold;
|
|
5304
|
+
const saved1 = this.eliseOldToolResults(ctx);
|
|
5305
|
+
if (saved1 > 0) reductions.push({ phase: "elision", saved: saved1 });
|
|
5306
|
+
if (aggressive) {
|
|
5307
|
+
const saved2 = await this.summarizeAncientTurns(ctx);
|
|
5308
|
+
if (saved2 > 0) reductions.push({ phase: "summary", saved: saved2 });
|
|
5309
|
+
} else if (load >= this.warnThreshold) {
|
|
5310
|
+
const saved2 = this.lightweightCompact(ctx);
|
|
5311
|
+
if (saved2 > 0) reductions.push({ phase: "elision", saved: saved2 });
|
|
5312
|
+
}
|
|
5313
|
+
const afterTokens = this.estimateTokens(ctx.messages);
|
|
5314
|
+
return { before: beforeTokens, after: afterTokens, reductions };
|
|
5315
|
+
}
|
|
5316
|
+
async summarizeAncientTurns(ctx) {
|
|
5317
|
+
const messages = ctx.messages;
|
|
5318
|
+
const cutoff = Math.max(0, messages.length - this.preserveK * 2);
|
|
5319
|
+
if (cutoff <= 2) return 0;
|
|
5320
|
+
const boundary = this.findSafeBoundary(messages, 0, cutoff);
|
|
5321
|
+
if (boundary <= 1) return 0;
|
|
5322
|
+
const toSummarize = messages.slice(0, boundary);
|
|
5323
|
+
const removedTokens = this.estimateTokens(toSummarize);
|
|
5324
|
+
let summaryText;
|
|
5325
|
+
try {
|
|
5326
|
+
summaryText = await this.callSummarizer(toSummarize, ctx);
|
|
5327
|
+
} catch {
|
|
5328
|
+
summaryText = `[${toSummarize.length} earlier turns omitted \u2014 key decisions and file states preserved in context]`;
|
|
5329
|
+
}
|
|
5330
|
+
const summaryMsg = {
|
|
5331
|
+
role: "system",
|
|
5332
|
+
content: `[prior_turns_summary: ${summaryText}]`
|
|
5333
|
+
};
|
|
5334
|
+
const summaryTokens = this.estimateTokens([summaryMsg]);
|
|
5335
|
+
const tail = ctx.messages.slice(boundary);
|
|
5336
|
+
ctx.state.replaceMessages([summaryMsg, ...tail]);
|
|
5337
|
+
return Math.max(0, removedTokens - summaryTokens);
|
|
5338
|
+
}
|
|
5339
|
+
findSafeBoundary(messages, from, to) {
|
|
5340
|
+
for (let i = to; i >= from; i--) {
|
|
5341
|
+
const m = messages[i];
|
|
5342
|
+
if (!m) continue;
|
|
5343
|
+
if (m.role === "user" && this.hasTextContent(m)) {
|
|
5344
|
+
return this.findExchangeStart(messages, i);
|
|
5345
|
+
}
|
|
5346
|
+
}
|
|
5347
|
+
return -1;
|
|
5348
|
+
}
|
|
5349
|
+
findExchangeStart(messages, userIndex) {
|
|
5350
|
+
for (let i = userIndex - 1; i >= 0; i--) {
|
|
5351
|
+
const m = messages[i];
|
|
5352
|
+
if (!m) continue;
|
|
5353
|
+
if (m.role === "assistant") {
|
|
5354
|
+
const hasToolUse = Array.isArray(m.content) ? m.content.some((b) => b.type === "tool_use") : false;
|
|
5355
|
+
if (!hasToolUse) {
|
|
5356
|
+
return i + 1;
|
|
5357
|
+
}
|
|
5358
|
+
} else if (m.role !== "user") ; else {
|
|
5359
|
+
return i;
|
|
5360
|
+
}
|
|
5361
|
+
}
|
|
5362
|
+
return 0;
|
|
5363
|
+
}
|
|
5364
|
+
async callSummarizer(messages, ctx) {
|
|
5365
|
+
const prompt = [
|
|
5366
|
+
{ type: "text", text: this.summarizerPrompt },
|
|
5367
|
+
{ type: "text", text: "\n\nConversation to summarize:\n" },
|
|
5368
|
+
...this.messagesToText(messages)
|
|
5369
|
+
];
|
|
5370
|
+
const req = {
|
|
5371
|
+
model: this.summarizerModel ?? ctx.model,
|
|
5372
|
+
system: prompt,
|
|
5373
|
+
messages: [],
|
|
4705
5374
|
maxTokens: 1024
|
|
4706
5375
|
};
|
|
4707
5376
|
const ac = ctx.signal ? void 0 : new AbortController();
|
|
@@ -5516,6 +6185,10 @@ var FleetBus = class {
|
|
|
5516
6185
|
"iteration.started",
|
|
5517
6186
|
"iteration.completed",
|
|
5518
6187
|
"provider.text_delta",
|
|
6188
|
+
// Subagent extended-thinking output. Forwarded so the FleetPanel /
|
|
6189
|
+
// /fleet log can surface "the planner is thinking…" instead of a
|
|
6190
|
+
// silent gap between iteration.started and the first text_delta.
|
|
6191
|
+
"provider.thinking_delta",
|
|
5519
6192
|
"provider.response",
|
|
5520
6193
|
"provider.retry",
|
|
5521
6194
|
"provider.error",
|
|
@@ -5764,14 +6437,34 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
|
|
|
5764
6437
|
completedResults = [];
|
|
5765
6438
|
totalIterations = 0;
|
|
5766
6439
|
inFlight = 0;
|
|
6440
|
+
/**
|
|
6441
|
+
* Subagents currently being stopped. Set on entry to `stop()`, cleared
|
|
6442
|
+
* once `recordCompletion` lands the terminal TaskResult. Used by
|
|
6443
|
+
* `runDispatched` and `findIdleSubagent` to refuse mid-flight dispatch
|
|
6444
|
+
* to a subagent the caller has already asked to terminate — closes the
|
|
6445
|
+
* assign+terminate race where a fresh task could land on a worker that
|
|
6446
|
+
* was about to be killed.
|
|
6447
|
+
*/
|
|
6448
|
+
terminating = /* @__PURE__ */ new Set();
|
|
5767
6449
|
constructor(config, options = {}) {
|
|
5768
6450
|
super();
|
|
5769
6451
|
this.coordinatorId = config.coordinatorId;
|
|
5770
6452
|
this.config = config;
|
|
5771
6453
|
this.runner = options.runner;
|
|
5772
6454
|
}
|
|
6455
|
+
/**
|
|
6456
|
+
* Replace the runner after construction. Used when the runner depends
|
|
6457
|
+
* on infrastructure (e.g. FleetBus) that isn't available until after
|
|
6458
|
+
* the coordinator's owning Director is built.
|
|
6459
|
+
*/
|
|
6460
|
+
setRunner(runner) {
|
|
6461
|
+
this.runner = runner;
|
|
6462
|
+
}
|
|
5773
6463
|
async spawn(subagent) {
|
|
5774
6464
|
const id = subagent.id || randomUUID();
|
|
6465
|
+
if (this.subagents.has(id)) {
|
|
6466
|
+
throw new Error(`Subagent id "${id}" already exists \u2014 refusing to overwrite`);
|
|
6467
|
+
}
|
|
5775
6468
|
const context = {
|
|
5776
6469
|
subagentId: id,
|
|
5777
6470
|
tasks: [],
|
|
@@ -5815,6 +6508,7 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
|
|
|
5815
6508
|
async stop(subagentId) {
|
|
5816
6509
|
const subagent = this.subagents.get(subagentId);
|
|
5817
6510
|
if (!subagent) return;
|
|
6511
|
+
this.terminating.add(subagentId);
|
|
5818
6512
|
subagent.abortController.abort();
|
|
5819
6513
|
subagent.status = "stopped";
|
|
5820
6514
|
subagent.currentTask = void 0;
|
|
@@ -5822,6 +6516,7 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
|
|
|
5822
6516
|
this.emit("subagent.stopped", { subagentId, reason: "stopped by coordinator" });
|
|
5823
6517
|
}
|
|
5824
6518
|
async stopAll() {
|
|
6519
|
+
this.drainPendingAsAborted("Coordinator stopAll() drained the pending queue");
|
|
5825
6520
|
await Promise.allSettled([...this.subagents.keys()].map((id) => this.stop(id)));
|
|
5826
6521
|
}
|
|
5827
6522
|
getStatus() {
|
|
@@ -5855,7 +6550,14 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
|
|
|
5855
6550
|
tryDispatchNext() {
|
|
5856
6551
|
while (this.canDispatch()) {
|
|
5857
6552
|
const subagentId = this.findIdleSubagent();
|
|
5858
|
-
if (!subagentId)
|
|
6553
|
+
if (!subagentId) {
|
|
6554
|
+
if (this.pendingTasks.length > 0 && !this.hasLiveSubagent()) {
|
|
6555
|
+
this.drainPendingAsAborted(
|
|
6556
|
+
"No live subagent available \u2014 all stopped or mid-termination"
|
|
6557
|
+
);
|
|
6558
|
+
}
|
|
6559
|
+
return;
|
|
6560
|
+
}
|
|
5859
6561
|
const task = this.pendingTasks.shift();
|
|
5860
6562
|
if (!task) return;
|
|
5861
6563
|
this.runDispatched(subagentId, task).catch((err) => {
|
|
@@ -5863,7 +6565,7 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
|
|
|
5863
6565
|
subagentId,
|
|
5864
6566
|
taskId: task.id,
|
|
5865
6567
|
status: "failed",
|
|
5866
|
-
error:
|
|
6568
|
+
error: classifySubagentError(err),
|
|
5867
6569
|
iterations: 0,
|
|
5868
6570
|
toolCalls: 0,
|
|
5869
6571
|
durationMs: 0
|
|
@@ -5877,13 +6579,76 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
|
|
|
5877
6579
|
}
|
|
5878
6580
|
findIdleSubagent() {
|
|
5879
6581
|
for (const [id, s] of this.subagents) {
|
|
5880
|
-
if (s.status === "idle") return id;
|
|
6582
|
+
if (s.status === "idle" && !this.terminating.has(id)) return id;
|
|
5881
6583
|
}
|
|
5882
6584
|
return null;
|
|
5883
6585
|
}
|
|
6586
|
+
/**
|
|
6587
|
+
* Returns true iff at least one spawned subagent could still
|
|
6588
|
+
* process a task. A "live" subagent is one that is not stopped
|
|
6589
|
+
* AND not mid-termination — `running` workers count because they
|
|
6590
|
+
* will eventually finish and become idle.
|
|
6591
|
+
*
|
|
6592
|
+
* When no subagent has ever been spawned, returns `true` so a
|
|
6593
|
+
* pre-spawn `assign()` simply queues (legacy behaviour). The
|
|
6594
|
+
* dead-end detection only fires after `stop()` has retired every
|
|
6595
|
+
* spawned worker.
|
|
6596
|
+
*
|
|
6597
|
+
* Used by `tryDispatchNext` to detect a dead-end pending queue.
|
|
6598
|
+
*/
|
|
6599
|
+
hasLiveSubagent() {
|
|
6600
|
+
if (this.subagents.size === 0) return true;
|
|
6601
|
+
for (const [id, s] of this.subagents) {
|
|
6602
|
+
if (s.status !== "stopped" && !this.terminating.has(id)) return true;
|
|
6603
|
+
}
|
|
6604
|
+
return false;
|
|
6605
|
+
}
|
|
6606
|
+
/**
|
|
6607
|
+
* Drain every pending task with a synthetic `aborted_by_parent`
|
|
6608
|
+
* completion event. Same shape as the `stopAll()` drain — we go
|
|
6609
|
+
* around `recordCompletion` because pending tasks were never
|
|
6610
|
+
* counted in `inFlight` and routing them through would trip the
|
|
6611
|
+
* underflow guard on every task after the first.
|
|
6612
|
+
*/
|
|
6613
|
+
drainPendingAsAborted(message) {
|
|
6614
|
+
const dropped = this.pendingTasks.splice(0, this.pendingTasks.length);
|
|
6615
|
+
for (const t2 of dropped) {
|
|
6616
|
+
const synthetic = {
|
|
6617
|
+
subagentId: t2.subagentId ?? "unassigned",
|
|
6618
|
+
taskId: t2.id,
|
|
6619
|
+
status: "stopped",
|
|
6620
|
+
error: {
|
|
6621
|
+
kind: "aborted_by_parent",
|
|
6622
|
+
message,
|
|
6623
|
+
retryable: false
|
|
6624
|
+
},
|
|
6625
|
+
iterations: 0,
|
|
6626
|
+
toolCalls: 0,
|
|
6627
|
+
durationMs: 0
|
|
6628
|
+
};
|
|
6629
|
+
this.completedResults.push(synthetic);
|
|
6630
|
+
this.emit("task.completed", { task: t2, result: synthetic });
|
|
6631
|
+
}
|
|
6632
|
+
}
|
|
5884
6633
|
async runDispatched(subagentId, task) {
|
|
5885
6634
|
const subagent = this.subagents.get(subagentId);
|
|
5886
6635
|
if (!subagent) return;
|
|
6636
|
+
if (this.terminating.has(subagentId) || subagent.status === "stopped") {
|
|
6637
|
+
this.recordCompletion({
|
|
6638
|
+
subagentId,
|
|
6639
|
+
taskId: task.id,
|
|
6640
|
+
status: "stopped",
|
|
6641
|
+
error: {
|
|
6642
|
+
kind: "aborted_by_parent",
|
|
6643
|
+
message: "Subagent was terminated before task could start",
|
|
6644
|
+
retryable: false
|
|
6645
|
+
},
|
|
6646
|
+
iterations: 0,
|
|
6647
|
+
toolCalls: 0,
|
|
6648
|
+
durationMs: 0
|
|
6649
|
+
});
|
|
6650
|
+
return;
|
|
6651
|
+
}
|
|
5887
6652
|
subagent.status = "running";
|
|
5888
6653
|
subagent.currentTask = task.id;
|
|
5889
6654
|
task.subagentId = subagentId;
|
|
@@ -5929,7 +6694,9 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
|
|
|
5929
6694
|
subagentId,
|
|
5930
6695
|
taskId: task.id,
|
|
5931
6696
|
status,
|
|
5932
|
-
error:
|
|
6697
|
+
error: classifySubagentError(err, {
|
|
6698
|
+
parentAborted: subagent.abortController.signal.aborted
|
|
6699
|
+
}),
|
|
5933
6700
|
iterations: usage.iterations,
|
|
5934
6701
|
toolCalls: usage.toolCalls,
|
|
5935
6702
|
durationMs: Date.now() - startTime
|
|
@@ -5968,19 +6735,14 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
|
|
|
5968
6735
|
}
|
|
5969
6736
|
const subagent = this.subagents.get(result.subagentId);
|
|
5970
6737
|
if (subagent && subagent.status !== "stopped") {
|
|
5971
|
-
|
|
5972
|
-
subagent.status =
|
|
6738
|
+
result.status === "failed" || result.status === "timeout";
|
|
6739
|
+
subagent.status = "idle";
|
|
5973
6740
|
subagent.currentTask = void 0;
|
|
5974
6741
|
if (subagent.abortController.signal.aborted) {
|
|
5975
6742
|
subagent.abortController = new AbortController();
|
|
5976
6743
|
}
|
|
5977
|
-
if (subagent.status === "error") {
|
|
5978
|
-
queueMicrotask(() => {
|
|
5979
|
-
if (subagent.status === "error") subagent.status = "idle";
|
|
5980
|
-
this.tryDispatchNext();
|
|
5981
|
-
});
|
|
5982
|
-
}
|
|
5983
6744
|
}
|
|
6745
|
+
this.terminating.delete(result.subagentId);
|
|
5984
6746
|
this.emit("task.completed", {
|
|
5985
6747
|
task: subagent?.context.tasks.find((t2) => t2.id === result.taskId) ?? { id: result.taskId },
|
|
5986
6748
|
result
|
|
@@ -6003,6 +6765,99 @@ var DefaultMultiAgentCoordinator = class extends EventEmitter {
|
|
|
6003
6765
|
return false;
|
|
6004
6766
|
}
|
|
6005
6767
|
};
|
|
6768
|
+
function classifySubagentError(err, hints = {}) {
|
|
6769
|
+
const cause = err instanceof Error ? { name: err.name, message: err.message, stack: err.stack } : void 0;
|
|
6770
|
+
const baseMessage = err instanceof Error ? err.message : String(err);
|
|
6771
|
+
if (err instanceof ProviderError) {
|
|
6772
|
+
return providerErrorToSubagentError(err, baseMessage, cause);
|
|
6773
|
+
}
|
|
6774
|
+
if (err instanceof BudgetExceededError) {
|
|
6775
|
+
const map = {
|
|
6776
|
+
iterations: "budget_iterations",
|
|
6777
|
+
tool_calls: "budget_tool_calls",
|
|
6778
|
+
tokens: "budget_tokens",
|
|
6779
|
+
cost: "budget_cost",
|
|
6780
|
+
timeout: "budget_timeout"
|
|
6781
|
+
};
|
|
6782
|
+
return {
|
|
6783
|
+
kind: map[err.kind],
|
|
6784
|
+
message: baseMessage,
|
|
6785
|
+
// Budgets are user-configured ceilings, not transient failures —
|
|
6786
|
+
// retrying with the same budget will hit the same ceiling. The
|
|
6787
|
+
// orchestrator must raise the budget or narrow the task first.
|
|
6788
|
+
retryable: false,
|
|
6789
|
+
cause
|
|
6790
|
+
};
|
|
6791
|
+
}
|
|
6792
|
+
if (hints.parentAborted) {
|
|
6793
|
+
return {
|
|
6794
|
+
kind: "aborted_by_parent",
|
|
6795
|
+
message: baseMessage,
|
|
6796
|
+
retryable: false,
|
|
6797
|
+
cause
|
|
6798
|
+
};
|
|
6799
|
+
}
|
|
6800
|
+
const lower = baseMessage.toLowerCase();
|
|
6801
|
+
if (/agent aborted$/i.test(baseMessage)) {
|
|
6802
|
+
return {
|
|
6803
|
+
kind: "aborted_by_parent",
|
|
6804
|
+
message: baseMessage,
|
|
6805
|
+
retryable: false,
|
|
6806
|
+
cause
|
|
6807
|
+
};
|
|
6808
|
+
}
|
|
6809
|
+
if (/agent exhausted iteration limit$/i.test(baseMessage)) {
|
|
6810
|
+
return { kind: "budget_iterations", message: baseMessage, retryable: false, cause };
|
|
6811
|
+
}
|
|
6812
|
+
if (/empty response$/i.test(baseMessage)) {
|
|
6813
|
+
return { kind: "empty_response", message: baseMessage, retryable: false, cause };
|
|
6814
|
+
}
|
|
6815
|
+
if (/^tool failed: /i.test(baseMessage)) {
|
|
6816
|
+
return { kind: "tool_failed", message: baseMessage, retryable: false, cause };
|
|
6817
|
+
}
|
|
6818
|
+
if (lower.includes("bridge transport") || /bridge.*(closed|disconnect)/i.test(baseMessage)) {
|
|
6819
|
+
return { kind: "bridge_failed", message: baseMessage, retryable: false, cause };
|
|
6820
|
+
}
|
|
6821
|
+
if (/context length|max.*tokens?.*exceeded|prompt is too long/i.test(baseMessage)) {
|
|
6822
|
+
return { kind: "context_overflow", message: baseMessage, retryable: false, cause };
|
|
6823
|
+
}
|
|
6824
|
+
return {
|
|
6825
|
+
kind: "unknown",
|
|
6826
|
+
message: baseMessage,
|
|
6827
|
+
retryable: false,
|
|
6828
|
+
cause
|
|
6829
|
+
};
|
|
6830
|
+
}
|
|
6831
|
+
function providerErrorToSubagentError(err, message, cause) {
|
|
6832
|
+
const status = err.status;
|
|
6833
|
+
if (status === 429 || err.body?.type === "rate_limit_error") {
|
|
6834
|
+
return {
|
|
6835
|
+
kind: "provider_rate_limit",
|
|
6836
|
+
message,
|
|
6837
|
+
retryable: true,
|
|
6838
|
+
// Conservative default: 5s. Provider-specific code can override
|
|
6839
|
+
// by emitting an error whose body carries an explicit hint.
|
|
6840
|
+
backoffMs: 5e3,
|
|
6841
|
+
cause
|
|
6842
|
+
};
|
|
6843
|
+
}
|
|
6844
|
+
if (status === 401 || status === 403 || err.body?.type === "authentication_error") {
|
|
6845
|
+
return { kind: "provider_auth", message, retryable: false, cause };
|
|
6846
|
+
}
|
|
6847
|
+
if (status === 408 || status === 0) {
|
|
6848
|
+
return { kind: "provider_timeout", message, retryable: true, cause };
|
|
6849
|
+
}
|
|
6850
|
+
if (status >= 500 && status < 600) {
|
|
6851
|
+
return {
|
|
6852
|
+
kind: "provider_5xx",
|
|
6853
|
+
message,
|
|
6854
|
+
retryable: true,
|
|
6855
|
+
backoffMs: 3e3,
|
|
6856
|
+
cause
|
|
6857
|
+
};
|
|
6858
|
+
}
|
|
6859
|
+
return { kind: "unknown", message, retryable: err.retryable, cause };
|
|
6860
|
+
}
|
|
6006
6861
|
|
|
6007
6862
|
// src/coordination/director.ts
|
|
6008
6863
|
var DirectorBudgetError = class extends Error {
|
|
@@ -6066,6 +6921,27 @@ var Director = class {
|
|
|
6066
6921
|
spawnDepth;
|
|
6067
6922
|
/** Live spawn counter for `maxSpawns` enforcement. */
|
|
6068
6923
|
spawnCount = 0;
|
|
6924
|
+
/** Optional checkpoint mirror — writes the live task graph + roster to disk. */
|
|
6925
|
+
stateCheckpoint;
|
|
6926
|
+
/** Optional session writer for emitting task_* / agent_* lifecycle events. */
|
|
6927
|
+
sessionWriter;
|
|
6928
|
+
/** Debounce timer for periodic manifest writes. */
|
|
6929
|
+
manifestTimer = null;
|
|
6930
|
+
manifestDebounceMs;
|
|
6931
|
+
/** Resolves task descriptions back from `assign()` so completion events
|
|
6932
|
+
* can also carry a human-readable title. */
|
|
6933
|
+
taskDescriptions = /* @__PURE__ */ new Map();
|
|
6934
|
+
/** Snapshot of which subagent owns each task — drives state-checkpoint
|
|
6935
|
+
* status updates without re-walking the manifest. */
|
|
6936
|
+
taskOwners = /* @__PURE__ */ new Map();
|
|
6937
|
+
/**
|
|
6938
|
+
* Handle to the coordinator-side `task.completed` listener so we can
|
|
6939
|
+
* unsubscribe in `shutdown()`. Without this, repeated Director
|
|
6940
|
+
* construction (e.g. tests, hot reloads) accumulates listeners on a
|
|
6941
|
+
* cached coordinator and slowly drifts the EventEmitter past its
|
|
6942
|
+
* default cap.
|
|
6943
|
+
*/
|
|
6944
|
+
taskCompletedListener = null;
|
|
6069
6945
|
constructor(opts) {
|
|
6070
6946
|
this.id = opts.config.coordinatorId || randomUUID();
|
|
6071
6947
|
this.manifestPath = opts.manifestPath;
|
|
@@ -6076,6 +6952,14 @@ var Director = class {
|
|
|
6076
6952
|
this.maxSpawns = opts.maxSpawns ?? Number.POSITIVE_INFINITY;
|
|
6077
6953
|
this.maxSpawnDepth = opts.maxSpawnDepth ?? 2;
|
|
6078
6954
|
this.spawnDepth = opts.spawnDepth ?? 0;
|
|
6955
|
+
this.sessionWriter = opts.sessionWriter ?? null;
|
|
6956
|
+
this.manifestDebounceMs = opts.manifestDebounceMs ?? 2e3;
|
|
6957
|
+
this.stateCheckpoint = opts.stateCheckpointPath ? new DirectorStateCheckpoint(opts.stateCheckpointPath, {
|
|
6958
|
+
directorRunId: this.id,
|
|
6959
|
+
maxSpawns: opts.maxSpawns,
|
|
6960
|
+
spawnDepth: this.spawnDepth,
|
|
6961
|
+
maxSpawnDepth: this.maxSpawnDepth
|
|
6962
|
+
}) : null;
|
|
6079
6963
|
if (this.sharedScratchpadPath) {
|
|
6080
6964
|
void fsp2.mkdir(this.sharedScratchpadPath, { recursive: true }).catch(() => void 0);
|
|
6081
6965
|
}
|
|
@@ -6094,7 +6978,7 @@ var Director = class {
|
|
|
6094
6978
|
{ ...opts.config, coordinatorId: this.id },
|
|
6095
6979
|
{ runner: opts.runner }
|
|
6096
6980
|
);
|
|
6097
|
-
this.
|
|
6981
|
+
this.taskCompletedListener = (payload) => {
|
|
6098
6982
|
const r = payload.result;
|
|
6099
6983
|
this.completed.set(r.taskId, r);
|
|
6100
6984
|
const waiter = this.taskWaiters.get(r.taskId);
|
|
@@ -6102,7 +6986,54 @@ var Director = class {
|
|
|
6102
6986
|
waiter.resolve(r);
|
|
6103
6987
|
this.taskWaiters.delete(r.taskId);
|
|
6104
6988
|
}
|
|
6105
|
-
|
|
6989
|
+
const title = this.taskDescriptions.get(r.taskId) ?? payload.task.description ?? r.taskId;
|
|
6990
|
+
const failed = r.status !== "success";
|
|
6991
|
+
const errorString = r.error ? `${r.error.kind}: ${r.error.message}` : void 0;
|
|
6992
|
+
this.stateCheckpoint?.recordTaskStatus(r.taskId, {
|
|
6993
|
+
status: failed ? r.status : "completed",
|
|
6994
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6995
|
+
iterations: r.iterations,
|
|
6996
|
+
toolCalls: r.toolCalls,
|
|
6997
|
+
durationMs: r.durationMs,
|
|
6998
|
+
error: errorString
|
|
6999
|
+
});
|
|
7000
|
+
this.stateCheckpoint?.setUsage(this.usage.snapshot());
|
|
7001
|
+
void this.appendSessionEvent(
|
|
7002
|
+
failed ? {
|
|
7003
|
+
type: "task_failed",
|
|
7004
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7005
|
+
taskId: r.taskId,
|
|
7006
|
+
title,
|
|
7007
|
+
error: errorString ?? r.status
|
|
7008
|
+
} : {
|
|
7009
|
+
type: "task_completed",
|
|
7010
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7011
|
+
taskId: r.taskId,
|
|
7012
|
+
title
|
|
7013
|
+
}
|
|
7014
|
+
);
|
|
7015
|
+
this.scheduleManifest();
|
|
7016
|
+
};
|
|
7017
|
+
this.coordinator.on("task.completed", this.taskCompletedListener);
|
|
7018
|
+
}
|
|
7019
|
+
/** Best-effort session-writer append. Swallows failures — the director
|
|
7020
|
+
* must not break a fleet run because the session JSONL handle closed. */
|
|
7021
|
+
async appendSessionEvent(event) {
|
|
7022
|
+
if (!this.sessionWriter) return;
|
|
7023
|
+
try {
|
|
7024
|
+
await this.sessionWriter.append(event);
|
|
7025
|
+
} catch {
|
|
7026
|
+
}
|
|
7027
|
+
}
|
|
7028
|
+
/** Debounced manifest writer. A burst of spawn/assign/complete events
|
|
7029
|
+
* collapses into one write. Set `manifestDebounceMs` to 0 to disable. */
|
|
7030
|
+
scheduleManifest() {
|
|
7031
|
+
if (!this.manifestPath || this.manifestDebounceMs <= 0) return;
|
|
7032
|
+
if (this.manifestTimer) return;
|
|
7033
|
+
this.manifestTimer = setTimeout(() => {
|
|
7034
|
+
this.manifestTimer = null;
|
|
7035
|
+
void this.writeManifest().catch(() => void 0);
|
|
7036
|
+
}, this.manifestDebounceMs);
|
|
6106
7037
|
}
|
|
6107
7038
|
/**
|
|
6108
7039
|
* Spawn a subagent. Identical to the coordinator's `spawn()` but
|
|
@@ -6141,6 +7072,25 @@ var Director = class {
|
|
|
6141
7072
|
model: config.model,
|
|
6142
7073
|
taskIds: []
|
|
6143
7074
|
});
|
|
7075
|
+
const spawnedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
7076
|
+
this.stateCheckpoint?.recordSpawn(
|
|
7077
|
+
{
|
|
7078
|
+
id: result.subagentId,
|
|
7079
|
+
name: config.name,
|
|
7080
|
+
role: config.role,
|
|
7081
|
+
provider: config.provider,
|
|
7082
|
+
model: config.model,
|
|
7083
|
+
spawnedAt
|
|
7084
|
+
},
|
|
7085
|
+
this.spawnCount
|
|
7086
|
+
);
|
|
7087
|
+
void this.appendSessionEvent({
|
|
7088
|
+
type: "agent_spawned",
|
|
7089
|
+
ts: spawnedAt,
|
|
7090
|
+
agentId: result.subagentId,
|
|
7091
|
+
role: config.role ?? config.name
|
|
7092
|
+
});
|
|
7093
|
+
this.scheduleManifest();
|
|
6144
7094
|
return result.subagentId;
|
|
6145
7095
|
}
|
|
6146
7096
|
/**
|
|
@@ -6261,13 +7211,42 @@ var Director = class {
|
|
|
6261
7211
|
* — calling shutdown twice is a no-op on the second invocation.
|
|
6262
7212
|
*/
|
|
6263
7213
|
async shutdown() {
|
|
7214
|
+
if (this.manifestTimer) {
|
|
7215
|
+
clearTimeout(this.manifestTimer);
|
|
7216
|
+
this.manifestTimer = null;
|
|
7217
|
+
}
|
|
7218
|
+
if (this.taskCompletedListener) {
|
|
7219
|
+
this.coordinator.off("task.completed", this.taskCompletedListener);
|
|
7220
|
+
this.taskCompletedListener = null;
|
|
7221
|
+
}
|
|
6264
7222
|
await this.coordinator.stopAll();
|
|
6265
7223
|
for (const b of this.subagentBridges.values()) {
|
|
6266
|
-
await b.stop().catch(() =>
|
|
7224
|
+
await b.stop().catch((err) => this.logShutdownError("subagent_bridge_stop", err));
|
|
6267
7225
|
}
|
|
6268
7226
|
this.subagentBridges.clear();
|
|
6269
|
-
await this.bridge.stop().catch(() =>
|
|
6270
|
-
if (this.manifestPath)
|
|
7227
|
+
await this.bridge.stop().catch((err) => this.logShutdownError("director_bridge_stop", err));
|
|
7228
|
+
if (this.manifestPath)
|
|
7229
|
+
await this.writeManifest().catch((err) => this.logShutdownError("manifest_write", err));
|
|
7230
|
+
if (this.stateCheckpoint) {
|
|
7231
|
+
this.stateCheckpoint.setUsage(this.usage.snapshot());
|
|
7232
|
+
await this.stateCheckpoint.flush().catch((err) => this.logShutdownError("state_checkpoint_flush", err));
|
|
7233
|
+
}
|
|
7234
|
+
}
|
|
7235
|
+
/**
|
|
7236
|
+
* Funnel for shutdown-phase errors. We can't throw — `shutdown()` is
|
|
7237
|
+
* called from process-exit paths where an uncaught throw would lose
|
|
7238
|
+
* the manifest write that comes after. But we MUST NOT silently
|
|
7239
|
+
* swallow either — a persistent bridge-close failure would otherwise
|
|
7240
|
+
* mask a real bug. `process.emitWarning` is the right tier:
|
|
7241
|
+
* surfaces on stderr by default, lets the host plug a warning
|
|
7242
|
+
* listener for structured collection, and never affects exit code.
|
|
7243
|
+
*/
|
|
7244
|
+
logShutdownError(phase, err) {
|
|
7245
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
7246
|
+
process.emitWarning(
|
|
7247
|
+
`Director shutdown phase "${phase}" failed: ${detail}`,
|
|
7248
|
+
"DirectorShutdownWarning"
|
|
7249
|
+
);
|
|
6271
7250
|
}
|
|
6272
7251
|
/**
|
|
6273
7252
|
* Hand a task to the coordinator. Returns the assigned task id so
|
|
@@ -6281,6 +7260,23 @@ var Director = class {
|
|
|
6281
7260
|
if (entry) entry.taskIds.push(taskWithId.id);
|
|
6282
7261
|
}
|
|
6283
7262
|
await this.coordinator.assign(taskWithId);
|
|
7263
|
+
this.taskDescriptions.set(taskWithId.id, taskWithId.description);
|
|
7264
|
+
if (taskWithId.subagentId) this.taskOwners.set(taskWithId.id, taskWithId.subagentId);
|
|
7265
|
+
const assignedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
7266
|
+
this.stateCheckpoint?.recordTaskAssigned({
|
|
7267
|
+
taskId: taskWithId.id,
|
|
7268
|
+
subagentId: taskWithId.subagentId,
|
|
7269
|
+
description: taskWithId.description,
|
|
7270
|
+
status: "running",
|
|
7271
|
+
assignedAt
|
|
7272
|
+
});
|
|
7273
|
+
void this.appendSessionEvent({
|
|
7274
|
+
type: "task_created",
|
|
7275
|
+
ts: assignedAt,
|
|
7276
|
+
taskId: taskWithId.id,
|
|
7277
|
+
title: taskWithId.description
|
|
7278
|
+
});
|
|
7279
|
+
this.scheduleManifest();
|
|
6284
7280
|
return taskWithId.id;
|
|
6285
7281
|
}
|
|
6286
7282
|
/**
|
|
@@ -6340,6 +7336,23 @@ var Director = class {
|
|
|
6340
7336
|
snapshot() {
|
|
6341
7337
|
return this.usage.snapshot();
|
|
6342
7338
|
}
|
|
7339
|
+
/**
|
|
7340
|
+
* Look up provider/model metadata for a spawned subagent. Returns
|
|
7341
|
+
* undefined when the subagent id is unknown (not yet spawned, or
|
|
7342
|
+
* already torn down). Callers — notably the TUI fleet panel — use
|
|
7343
|
+
* this to render human-readable provider/model tags next to each
|
|
7344
|
+
* subagent row without reaching into private state.
|
|
7345
|
+
*/
|
|
7346
|
+
getSubagentMeta(id) {
|
|
7347
|
+
const usage = this.subagentMeta.get(id);
|
|
7348
|
+
const manifest = this.manifestEntries.get(id);
|
|
7349
|
+
if (!usage && !manifest) return void 0;
|
|
7350
|
+
return {
|
|
7351
|
+
provider: usage?.provider ?? manifest?.provider,
|
|
7352
|
+
model: usage?.model ?? manifest?.model,
|
|
7353
|
+
name: manifest?.name
|
|
7354
|
+
};
|
|
7355
|
+
}
|
|
6343
7356
|
/**
|
|
6344
7357
|
* Compose the leader/director-agent system prompt: fleet preamble +
|
|
6345
7358
|
* (optional) roster summary + user base prompt. Pass the result to your
|
|
@@ -6639,12 +7652,260 @@ function makeFleetUsageTool(director) {
|
|
|
6639
7652
|
}
|
|
6640
7653
|
};
|
|
6641
7654
|
}
|
|
7655
|
+
function createDelegateTool(opts) {
|
|
7656
|
+
const defaultTimeoutMs = opts.defaultTimeoutMs ?? 4 * 60 * 60 * 1e3;
|
|
7657
|
+
const rosterIds = opts.roster ? Object.keys(opts.roster) : [];
|
|
7658
|
+
const inputSchema = {
|
|
7659
|
+
type: "object",
|
|
7660
|
+
properties: {
|
|
7661
|
+
task: {
|
|
7662
|
+
type: "string",
|
|
7663
|
+
description: "What the subagent should do \u2014 natural language, complete sentence(s). The subagent has its own tool slice, its own LLM call, and returns when its task is done."
|
|
7664
|
+
},
|
|
7665
|
+
role: {
|
|
7666
|
+
type: "string",
|
|
7667
|
+
description: rosterIds.length > 0 ? `Roster role (preferred). One of: ${rosterIds.join(", ")}. Picks a pre-tuned config (prompt, budgets, tools) for that role.` : "No roster is configured \u2014 pass `name` instead.",
|
|
7668
|
+
enum: rosterIds.length > 0 ? rosterIds : void 0
|
|
7669
|
+
},
|
|
7670
|
+
name: {
|
|
7671
|
+
type: "string",
|
|
7672
|
+
description: "Display name for the subagent when not using a roster role. Required when `role` is omitted."
|
|
7673
|
+
},
|
|
7674
|
+
provider: {
|
|
7675
|
+
type: "string",
|
|
7676
|
+
description: 'Provider id (e.g. "anthropic", "openai"). Defaults to the host provider when omitted.'
|
|
7677
|
+
},
|
|
7678
|
+
model: {
|
|
7679
|
+
type: "string",
|
|
7680
|
+
description: "Model id within the provider. Defaults to the host model when omitted."
|
|
7681
|
+
},
|
|
7682
|
+
systemPromptOverride: {
|
|
7683
|
+
type: "string",
|
|
7684
|
+
description: "Optional extra prompt text appended to the role baseline."
|
|
7685
|
+
},
|
|
7686
|
+
timeoutMs: {
|
|
7687
|
+
type: "number",
|
|
7688
|
+
description: `Wall-clock budget for this delegate in milliseconds. No hard cap \u2014 set as high as the task realistically needs (a monorepo audit can take hours, a single-file lint takes seconds). Default ${Math.round(defaultTimeoutMs / 1e3 / 60)} minutes.`
|
|
7689
|
+
},
|
|
7690
|
+
maxIterations: {
|
|
7691
|
+
type: "number",
|
|
7692
|
+
description: "Maximum LLM iterations the subagent may take. Unset = use the role/coordinator default. Raise this for tasks with many tool-think-tool cycles (deep code analysis, multi-file refactors)."
|
|
7693
|
+
},
|
|
7694
|
+
maxToolCalls: {
|
|
7695
|
+
type: "number",
|
|
7696
|
+
description: "Maximum number of tool invocations the subagent may make. Unset = use the role/coordinator default. Raise this for tasks that touch many files (large grep + read + report)."
|
|
7697
|
+
}
|
|
7698
|
+
},
|
|
7699
|
+
required: ["task"]
|
|
7700
|
+
};
|
|
7701
|
+
return {
|
|
7702
|
+
name: "delegate",
|
|
7703
|
+
description: "Hand a discrete piece of work to a dedicated subagent and wait for its result. The subagent has its own context, its own LLM call, and its own budget \u2014 use this when a task is self-contained, would otherwise blow up your context, or benefits from a specialized role (bug-hunter, security-scanner, refactor-planner, audit-log). YOU decide how big the budget is: pass `timeoutMs`, `maxIterations`, and `maxToolCalls` sized to the actual work. There is no hidden cap forcing a 3-minute / 80-iteration limit \u2014 if a monorepo audit needs 2 hours and 500 tool calls, ask for that. Call multiple delegates in parallel through the provider's parallel-tool-call surface to fan work out across roles.",
|
|
7704
|
+
usageHint: "Set `task` to a complete instruction. Either pick `role` from the roster or pass `name` + `provider` + `model`. For non-trivial work, also pass `timeoutMs` (the wall-clock budget you actually need), `maxIterations`, and `maxToolCalls` \u2014 defaults are intentionally generous (4 hours) but the right values depend on scope. Returns the subagent's `TaskResult` \u2014 including the textual `result`, iteration count, tool count, and duration. Auto-promotes the host into director mode on first call.",
|
|
7705
|
+
permission: "auto",
|
|
7706
|
+
mutating: false,
|
|
7707
|
+
inputSchema,
|
|
7708
|
+
async execute(input) {
|
|
7709
|
+
const i = input ?? {};
|
|
7710
|
+
if (typeof i.task !== "string" || !i.task.trim()) {
|
|
7711
|
+
return { ok: false, error: "`task` is required." };
|
|
7712
|
+
}
|
|
7713
|
+
let director = await opts.host.ensureDirector();
|
|
7714
|
+
if (!director) {
|
|
7715
|
+
director = await opts.host.promoteToDirector();
|
|
7716
|
+
}
|
|
7717
|
+
if (!director) {
|
|
7718
|
+
const reason = opts.host.getPromotionBlockReason?.();
|
|
7719
|
+
return {
|
|
7720
|
+
ok: false,
|
|
7721
|
+
error: reason ?? "Director could not be activated \u2014 multi-agent host already running in legacy non-director mode. Restart with `--director` for fleet support."
|
|
7722
|
+
};
|
|
7723
|
+
}
|
|
7724
|
+
const timeoutMs = i.timeoutMs ?? defaultTimeoutMs;
|
|
7725
|
+
let cfg;
|
|
7726
|
+
if (i.role) {
|
|
7727
|
+
const base = opts.roster?.[i.role];
|
|
7728
|
+
if (!base) {
|
|
7729
|
+
return {
|
|
7730
|
+
ok: false,
|
|
7731
|
+
error: `Unknown role "${i.role}". Available: ${rosterIds.join(", ") || "(no roster configured)"}.`
|
|
7732
|
+
};
|
|
7733
|
+
}
|
|
7734
|
+
cfg = { ...base };
|
|
7735
|
+
if (i.systemPromptOverride) cfg.systemPromptOverride = i.systemPromptOverride;
|
|
7736
|
+
if (i.provider) cfg.provider = i.provider;
|
|
7737
|
+
if (i.model) cfg.model = i.model;
|
|
7738
|
+
} else {
|
|
7739
|
+
if (!i.name) {
|
|
7740
|
+
return {
|
|
7741
|
+
ok: false,
|
|
7742
|
+
error: "Either `role` (from the roster) or `name` is required."
|
|
7743
|
+
};
|
|
7744
|
+
}
|
|
7745
|
+
cfg = {
|
|
7746
|
+
name: i.name,
|
|
7747
|
+
provider: i.provider,
|
|
7748
|
+
model: i.model,
|
|
7749
|
+
systemPromptOverride: i.systemPromptOverride
|
|
7750
|
+
};
|
|
7751
|
+
}
|
|
7752
|
+
if (typeof i.maxIterations === "number" && i.maxIterations > 0) {
|
|
7753
|
+
cfg.maxIterations = i.maxIterations;
|
|
7754
|
+
}
|
|
7755
|
+
if (typeof i.maxToolCalls === "number" && i.maxToolCalls > 0) {
|
|
7756
|
+
cfg.maxToolCalls = i.maxToolCalls;
|
|
7757
|
+
}
|
|
7758
|
+
const SUBAGENT_TIMEOUT_BUFFER_MS = 3e4;
|
|
7759
|
+
const desiredSubTimeout = Math.max(3e4, timeoutMs - SUBAGENT_TIMEOUT_BUFFER_MS);
|
|
7760
|
+
if (!cfg.timeoutMs || cfg.timeoutMs > desiredSubTimeout) {
|
|
7761
|
+
cfg.timeoutMs = desiredSubTimeout;
|
|
7762
|
+
}
|
|
7763
|
+
try {
|
|
7764
|
+
const subagentId = await director.spawn(cfg);
|
|
7765
|
+
const taskId = await director.assign({
|
|
7766
|
+
id: "",
|
|
7767
|
+
description: i.task,
|
|
7768
|
+
subagentId
|
|
7769
|
+
});
|
|
7770
|
+
const result = await Promise.race([
|
|
7771
|
+
director.awaitTasks([taskId]).then((r) => r[0]),
|
|
7772
|
+
new Promise(
|
|
7773
|
+
(resolve4) => setTimeout(() => resolve4({ __timeout: true }), timeoutMs)
|
|
7774
|
+
)
|
|
7775
|
+
]);
|
|
7776
|
+
if ("__timeout" in result) {
|
|
7777
|
+
const partial2 = await readSubagentPartial(opts, subagentId);
|
|
7778
|
+
return {
|
|
7779
|
+
ok: false,
|
|
7780
|
+
stopReason: "host_timeout",
|
|
7781
|
+
error: `Subagent did not finish within ${timeoutMs}ms.`,
|
|
7782
|
+
hint: "Reduce scope of the next delegate, raise timeoutMs, or use spawn_subagent + await_tasks for long-running work.",
|
|
7783
|
+
subagentId,
|
|
7784
|
+
taskId,
|
|
7785
|
+
partial: partial2
|
|
7786
|
+
};
|
|
7787
|
+
}
|
|
7788
|
+
const baseStopReason = result.status === "success" ? "end_turn" : result.status === "timeout" ? "subagent_timeout" : result.status === "stopped" ? "aborted" : "budget_exhausted";
|
|
7789
|
+
const partial = result.status === "success" ? void 0 : await readSubagentPartial(opts, subagentId);
|
|
7790
|
+
const errorKind = result.error?.kind;
|
|
7791
|
+
const retryable = result.error?.retryable;
|
|
7792
|
+
const backoffMs = result.error?.backoffMs;
|
|
7793
|
+
return {
|
|
7794
|
+
ok: result.status === "success",
|
|
7795
|
+
status: result.status,
|
|
7796
|
+
stopReason: baseStopReason,
|
|
7797
|
+
errorKind,
|
|
7798
|
+
retryable,
|
|
7799
|
+
backoffMs,
|
|
7800
|
+
subagentId: result.subagentId,
|
|
7801
|
+
taskId: result.taskId,
|
|
7802
|
+
result: result.result,
|
|
7803
|
+
error: result.error,
|
|
7804
|
+
iterations: result.iterations,
|
|
7805
|
+
toolCalls: result.toolCalls,
|
|
7806
|
+
durationMs: result.durationMs,
|
|
7807
|
+
...partial ? { partial } : {},
|
|
7808
|
+
...hintForKind(errorKind, retryable, backoffMs) ? { hint: hintForKind(errorKind, retryable, backoffMs) } : {}
|
|
7809
|
+
};
|
|
7810
|
+
} catch (err) {
|
|
7811
|
+
return {
|
|
7812
|
+
ok: false,
|
|
7813
|
+
stopReason: "error",
|
|
7814
|
+
error: err instanceof Error ? err.message : String(err)
|
|
7815
|
+
};
|
|
7816
|
+
}
|
|
7817
|
+
}
|
|
7818
|
+
};
|
|
7819
|
+
}
|
|
7820
|
+
function hintForKind(kind, retryable, backoffMs) {
|
|
7821
|
+
if (!kind) return void 0;
|
|
7822
|
+
switch (kind) {
|
|
7823
|
+
case "provider_rate_limit":
|
|
7824
|
+
return `Provider rate-limited. Retry safe after ${backoffMs ?? 5e3}ms backoff. Consider a smaller model or fewer parallel delegates.`;
|
|
7825
|
+
case "provider_5xx":
|
|
7826
|
+
return `Provider server error. Retry safe after ${backoffMs ?? 3e3}ms backoff \u2014 usually transient.`;
|
|
7827
|
+
case "provider_timeout":
|
|
7828
|
+
return "Provider network timeout. Retry safe; reduce input size if it persists.";
|
|
7829
|
+
case "provider_auth":
|
|
7830
|
+
return "Provider rejected credentials. Cannot retry \u2014 fix the API key / config and re-invoke.";
|
|
7831
|
+
case "context_overflow":
|
|
7832
|
+
return "Subagent context exceeded the model limit. Narrow the task, use a larger-context model, or split into multiple delegates.";
|
|
7833
|
+
case "budget_iterations":
|
|
7834
|
+
case "budget_tool_calls":
|
|
7835
|
+
case "budget_tokens":
|
|
7836
|
+
case "budget_cost":
|
|
7837
|
+
return "Subagent exhausted its budget. Raise the matching `max*` field on the next delegate or narrow task scope.";
|
|
7838
|
+
case "budget_timeout":
|
|
7839
|
+
return "Subagent hit its wall-clock budget. Raise `timeoutMs` on the next delegate or split the task.";
|
|
7840
|
+
case "aborted_by_parent":
|
|
7841
|
+
return "Subagent was aborted (user Ctrl+C, parent unwound, or sibling failure cascade). Not retryable until the abort condition is resolved.";
|
|
7842
|
+
case "empty_response":
|
|
7843
|
+
return "Subagent ended its turn with no text and no tool calls. Almost always a prompt / config issue \u2014 clarify the task or check the model.";
|
|
7844
|
+
case "tool_failed":
|
|
7845
|
+
return "A tool inside the subagent returned ok:false. Inspect `partial.lastAssistantText` for the agent reasoning, then retry with corrected inputs.";
|
|
7846
|
+
case "bridge_failed":
|
|
7847
|
+
return "Parent-child bridge transport failed. This is rare \u2014 restart the session and retry.";
|
|
7848
|
+
default:
|
|
7849
|
+
return retryable ? "Failure classified as retryable. Try again with the same input." : void 0;
|
|
7850
|
+
}
|
|
7851
|
+
}
|
|
7852
|
+
async function readSubagentPartial(opts, subagentId) {
|
|
7853
|
+
if (!opts.sessionsRoot) return void 0;
|
|
7854
|
+
const candidates = [];
|
|
7855
|
+
if (opts.directorRunId) {
|
|
7856
|
+
candidates.push(path6.join(opts.sessionsRoot, opts.directorRunId, `${subagentId}.jsonl`));
|
|
7857
|
+
} else {
|
|
7858
|
+
try {
|
|
7859
|
+
const runDirs = await fsp2.readdir(opts.sessionsRoot);
|
|
7860
|
+
for (const r of runDirs) {
|
|
7861
|
+
candidates.push(path6.join(opts.sessionsRoot, r, `${subagentId}.jsonl`));
|
|
7862
|
+
}
|
|
7863
|
+
} catch {
|
|
7864
|
+
return void 0;
|
|
7865
|
+
}
|
|
7866
|
+
}
|
|
7867
|
+
for (const file of candidates) {
|
|
7868
|
+
let raw;
|
|
7869
|
+
try {
|
|
7870
|
+
raw = await fsp2.readFile(file, "utf8");
|
|
7871
|
+
} catch {
|
|
7872
|
+
continue;
|
|
7873
|
+
}
|
|
7874
|
+
const lines = raw.split("\n").filter((l) => l.trim());
|
|
7875
|
+
let lastAssistantText;
|
|
7876
|
+
let lastStopReason;
|
|
7877
|
+
let toolUses = 0;
|
|
7878
|
+
for (const line of lines) {
|
|
7879
|
+
try {
|
|
7880
|
+
const ev = JSON.parse(line);
|
|
7881
|
+
if (ev.type === "tool_use") toolUses += 1;
|
|
7882
|
+
if (ev.type === "llm_response") {
|
|
7883
|
+
if (typeof ev.stopReason === "string") lastStopReason = ev.stopReason;
|
|
7884
|
+
if (Array.isArray(ev.content)) {
|
|
7885
|
+
const txt = ev.content.filter((b) => b.type === "text").map((b) => b.text ?? "").join("\n").trim();
|
|
7886
|
+
if (txt) lastAssistantText = txt;
|
|
7887
|
+
}
|
|
7888
|
+
}
|
|
7889
|
+
} catch {
|
|
7890
|
+
}
|
|
7891
|
+
}
|
|
7892
|
+
return {
|
|
7893
|
+
lastAssistantText,
|
|
7894
|
+
lastStopReason,
|
|
7895
|
+
toolUsesObserved: toolUses,
|
|
7896
|
+
events: lines.length
|
|
7897
|
+
};
|
|
7898
|
+
}
|
|
7899
|
+
return void 0;
|
|
7900
|
+
}
|
|
6642
7901
|
|
|
6643
7902
|
// src/coordination/agent-subagent-runner.ts
|
|
6644
7903
|
function makeAgentSubagentRunner(opts) {
|
|
6645
7904
|
const format = opts.formatTaskInput ?? defaultFormatTaskInput;
|
|
6646
7905
|
return async (task, ctx) => {
|
|
6647
|
-
const
|
|
7906
|
+
const factoryResult = await opts.factory(ctx.config);
|
|
7907
|
+
const { agent, events } = factoryResult;
|
|
7908
|
+
const detachFleet = opts.fleetBus?.attach(ctx.subagentId, events, task.id);
|
|
6648
7909
|
const aborter = new AbortController();
|
|
6649
7910
|
let budgetError = null;
|
|
6650
7911
|
const onBudgetError = (err) => {
|
|
@@ -6658,13 +7919,19 @@ function makeAgentSubagentRunner(opts) {
|
|
|
6658
7919
|
budgetError.message += ` (caused by: ${err.message})`;
|
|
6659
7920
|
}
|
|
6660
7921
|
};
|
|
7922
|
+
let lastToolFailed = null;
|
|
6661
7923
|
const unsub = [];
|
|
6662
7924
|
unsub.push(
|
|
6663
|
-
events.on("tool.
|
|
7925
|
+
events.on("tool.executed", (e) => {
|
|
6664
7926
|
try {
|
|
6665
7927
|
ctx.budget.recordToolCall();
|
|
6666
|
-
} catch (
|
|
6667
|
-
onBudgetError(
|
|
7928
|
+
} catch (eb) {
|
|
7929
|
+
onBudgetError(eb);
|
|
7930
|
+
}
|
|
7931
|
+
if (e.ok === false) {
|
|
7932
|
+
lastToolFailed = e.name;
|
|
7933
|
+
} else if (e.ok === true) {
|
|
7934
|
+
lastToolFailed = null;
|
|
6668
7935
|
}
|
|
6669
7936
|
}),
|
|
6670
7937
|
events.on("provider.response", (e) => {
|
|
@@ -6681,6 +7948,26 @@ function makeAgentSubagentRunner(opts) {
|
|
|
6681
7948
|
} catch (e) {
|
|
6682
7949
|
onBudgetError(e);
|
|
6683
7950
|
}
|
|
7951
|
+
}),
|
|
7952
|
+
// D3: cooperative timeout enforcement DURING a long tool call.
|
|
7953
|
+
// The iteration-loop checkTimeout() only fires between agent
|
|
7954
|
+
// iterations — a single `bash sleep 3600` call would otherwise
|
|
7955
|
+
// park inside one tool execution while the timeout silently
|
|
7956
|
+
// passes, relying solely on the coordinator's hard Promise.race
|
|
7957
|
+
// to interrupt. Tools that emit `tool.progress` (bash chunks,
|
|
7958
|
+
// fetch byte progress, spawn-stream stdout) give us a heartbeat
|
|
7959
|
+
// we can hang the check on. When the budget trips here:
|
|
7960
|
+
// 1. onBudgetError sets budgetError + aborter.abort()
|
|
7961
|
+
// 2. aborter signal propagates to agent.run → tool executor
|
|
7962
|
+
// 3. tool's own signal listener kills the child process
|
|
7963
|
+
// Cheap: O(1) per progress event, and the budget short-circuits
|
|
7964
|
+
// when timeoutMs is unset (most subagents have one set anyway).
|
|
7965
|
+
events.on("tool.progress", () => {
|
|
7966
|
+
try {
|
|
7967
|
+
ctx.budget.checkTimeout();
|
|
7968
|
+
} catch (e) {
|
|
7969
|
+
onBudgetError(e);
|
|
7970
|
+
}
|
|
6684
7971
|
})
|
|
6685
7972
|
);
|
|
6686
7973
|
const onParentAbort = () => aborter.abort();
|
|
@@ -6689,8 +7976,15 @@ function makeAgentSubagentRunner(opts) {
|
|
|
6689
7976
|
try {
|
|
6690
7977
|
result = await agent.run(format(task, ctx.config), { signal: aborter.signal });
|
|
6691
7978
|
} finally {
|
|
7979
|
+
detachFleet?.();
|
|
6692
7980
|
ctx.signal.removeEventListener("abort", onParentAbort);
|
|
6693
7981
|
for (const u of unsub) u();
|
|
7982
|
+
if (factoryResult.dispose) {
|
|
7983
|
+
try {
|
|
7984
|
+
await factoryResult.dispose();
|
|
7985
|
+
} catch {
|
|
7986
|
+
}
|
|
7987
|
+
}
|
|
6694
7988
|
}
|
|
6695
7989
|
if (budgetError) throw budgetError;
|
|
6696
7990
|
if (result.status === "failed") {
|
|
@@ -6703,6 +7997,13 @@ function makeAgentSubagentRunner(opts) {
|
|
|
6703
7997
|
throw new Error("agent exhausted iteration limit");
|
|
6704
7998
|
}
|
|
6705
7999
|
const usage = ctx.budget.usage();
|
|
8000
|
+
const finalText = (result.finalText ?? "").trim();
|
|
8001
|
+
if (finalText.length === 0 && usage.toolCalls === 0) {
|
|
8002
|
+
throw new Error("empty response");
|
|
8003
|
+
}
|
|
8004
|
+
if (finalText.length === 0 && lastToolFailed !== null) {
|
|
8005
|
+
throw new Error(`tool failed: ${lastToolFailed}`);
|
|
8006
|
+
}
|
|
6706
8007
|
return {
|
|
6707
8008
|
result: result.finalText,
|
|
6708
8009
|
iterations: result.iterations,
|
|
@@ -6768,10 +8069,12 @@ Working rules:
|
|
|
6768
8069
|
- Never fabricate numbers \u2014 read the actual logs first
|
|
6769
8070
|
- Always include file:line references for errors
|
|
6770
8071
|
- If sessionPath is missing, ask the director to provide it
|
|
6771
|
-
- Report confidence level: high (>90% accuracy), medium, low
|
|
6772
|
-
|
|
6773
|
-
|
|
6774
|
-
|
|
8072
|
+
- Report confidence level: high (>90% accuracy), medium, low`
|
|
8073
|
+
// No hardcoded budgets — the orchestrator (delegate tool or
|
|
8074
|
+
// spawn_subagent) decides per-task how much room a subagent gets.
|
|
8075
|
+
// A monorepo audit needs hours; a single-file lint check needs
|
|
8076
|
+
// seconds. Pinning a number here forces the orchestrator to fight
|
|
8077
|
+
// the role's default instead of just asking for what it needs.
|
|
6775
8078
|
};
|
|
6776
8079
|
var BUG_HUNTER_AGENT = {
|
|
6777
8080
|
id: "bug-hunter",
|
|
@@ -6811,10 +8114,8 @@ Working rules:
|
|
|
6811
8114
|
- Never scan node_modules \u2014 it's noise
|
|
6812
8115
|
- Always include file:line for every finding
|
|
6813
8116
|
- If >30% of findings are false positives, note the confidence level
|
|
6814
|
-
- Ask director for clarification if paths are ambiguous
|
|
6815
|
-
|
|
6816
|
-
maxToolCalls: 300,
|
|
6817
|
-
timeoutMs: 18e4
|
|
8117
|
+
- Ask director for clarification if paths are ambiguous`
|
|
8118
|
+
// Budgets are set by the orchestrator per task — see fleet.ts header.
|
|
6818
8119
|
};
|
|
6819
8120
|
var REFACTOR_PLANNER_AGENT = {
|
|
6820
8121
|
id: "refactor-planner",
|
|
@@ -6854,10 +8155,8 @@ Working rules:
|
|
|
6854
8155
|
- Always include rollback strategy \u2014 every refactor can fail
|
|
6855
8156
|
- Merge tasks that take <1h into a single phase
|
|
6856
8157
|
- Respect team constraints (reviewer availability, parallelization)
|
|
6857
|
-
- Never plan without analyzing the actual code first
|
|
6858
|
-
|
|
6859
|
-
maxToolCalls: 250,
|
|
6860
|
-
timeoutMs: 15e4
|
|
8158
|
+
- Never plan without analyzing the actual code first`
|
|
8159
|
+
// Budgets are set by the orchestrator per task — see fleet.ts header.
|
|
6861
8160
|
};
|
|
6862
8161
|
var SECURITY_SCANNER_AGENT = {
|
|
6863
8162
|
id: "security-scanner",
|
|
@@ -6905,10 +8204,8 @@ Working rules:
|
|
|
6905
8204
|
- Never scan node_modules \u2014 use npm audit instead
|
|
6906
8205
|
- Always provide remediation steps, not just findings
|
|
6907
8206
|
- Verify regex-based secrets before flagging (false positive risk)
|
|
6908
|
-
- When in doubt, flag as medium rather than ignoring potential issues
|
|
6909
|
-
|
|
6910
|
-
maxToolCalls: 280,
|
|
6911
|
-
timeoutMs: 16e4
|
|
8207
|
+
- When in doubt, flag as medium rather than ignoring potential issues`
|
|
8208
|
+
// Budgets are set by the orchestrator per task — see fleet.ts header.
|
|
6912
8209
|
};
|
|
6913
8210
|
var FLEET_ROSTER = {
|
|
6914
8211
|
"audit-log": AUDIT_LOG_AGENT,
|
|
@@ -8177,7 +9474,7 @@ async function startMetricsServer(opts) {
|
|
|
8177
9474
|
const tls = opts.tls;
|
|
8178
9475
|
const useHttps = !!(tls?.cert && tls?.key);
|
|
8179
9476
|
const host = opts.host ?? "127.0.0.1";
|
|
8180
|
-
const
|
|
9477
|
+
const path18 = opts.path ?? "/metrics";
|
|
8181
9478
|
const healthPath = opts.healthPath ?? "/healthz";
|
|
8182
9479
|
const healthRegistry = opts.healthRegistry;
|
|
8183
9480
|
const listener = (req, res) => {
|
|
@@ -8187,7 +9484,7 @@ async function startMetricsServer(opts) {
|
|
|
8187
9484
|
return;
|
|
8188
9485
|
}
|
|
8189
9486
|
const url = req.url.split("?")[0];
|
|
8190
|
-
if (url ===
|
|
9487
|
+
if (url === path18) {
|
|
8191
9488
|
let body;
|
|
8192
9489
|
try {
|
|
8193
9490
|
body = renderPrometheus(opts.sink.snapshot());
|
|
@@ -8251,7 +9548,7 @@ async function startMetricsServer(opts) {
|
|
|
8251
9548
|
const protocol = useHttps ? "https" : "http";
|
|
8252
9549
|
return {
|
|
8253
9550
|
port: boundPort,
|
|
8254
|
-
url: `${protocol}://${host}:${boundPort}${
|
|
9551
|
+
url: `${protocol}://${host}:${boundPort}${path18}`,
|
|
8255
9552
|
close: () => new Promise((resolve4, reject) => {
|
|
8256
9553
|
server.close((err) => err ? reject(err) : resolve4());
|
|
8257
9554
|
})
|
|
@@ -8797,336 +10094,248 @@ var allServers = () => ({
|
|
|
8797
10094
|
sentinel: { ...sentinelServer(), enabled: false }
|
|
8798
10095
|
});
|
|
8799
10096
|
|
|
8800
|
-
// src/
|
|
8801
|
-
|
|
8802
|
-
|
|
8803
|
-
|
|
8804
|
-
|
|
8805
|
-
|
|
8806
|
-
|
|
8807
|
-
|
|
8808
|
-
|
|
8809
|
-
|
|
8810
|
-
|
|
8811
|
-
|
|
8812
|
-
|
|
8813
|
-
|
|
8814
|
-
|
|
8815
|
-
|
|
8816
|
-
|
|
10097
|
+
// src/extension/registry.ts
|
|
10098
|
+
var ExtensionRegistry = class {
|
|
10099
|
+
extensions = [];
|
|
10100
|
+
promptContributors = [];
|
|
10101
|
+
log;
|
|
10102
|
+
setLogger(log) {
|
|
10103
|
+
this.log = log;
|
|
10104
|
+
}
|
|
10105
|
+
/**
|
|
10106
|
+
* Register a system prompt contributor. Returns an unregister function.
|
|
10107
|
+
* Contributors are called on every system prompt build in registration
|
|
10108
|
+
* order. Their output blocks are inserted after the core environment
|
|
10109
|
+
* block, before the mode and plan blocks.
|
|
10110
|
+
*/
|
|
10111
|
+
registerSystemPromptContributor(c) {
|
|
10112
|
+
this.promptContributors.push(c);
|
|
10113
|
+
return () => {
|
|
10114
|
+
const idx = this.promptContributors.indexOf(c);
|
|
10115
|
+
if (idx >= 0) this.promptContributors.splice(idx, 1);
|
|
8817
10116
|
};
|
|
8818
|
-
|
|
8819
|
-
|
|
8820
|
-
|
|
8821
|
-
|
|
8822
|
-
|
|
10117
|
+
}
|
|
10118
|
+
/**
|
|
10119
|
+
* Build all registered system prompt contributions.
|
|
10120
|
+
* Failures are caught and logged — one bad contributor doesn't
|
|
10121
|
+
* break the prompt assembly.
|
|
10122
|
+
*/
|
|
10123
|
+
async buildSystemPromptContributions(ctx) {
|
|
10124
|
+
const blocks = [];
|
|
10125
|
+
for (const c of this.promptContributors) {
|
|
10126
|
+
try {
|
|
10127
|
+
const contributed = await c(ctx);
|
|
10128
|
+
blocks.push(...contributed);
|
|
10129
|
+
} catch (err) {
|
|
10130
|
+
this.log?.error("SystemPromptContributor failed", err);
|
|
8823
10131
|
}
|
|
8824
|
-
}
|
|
8825
|
-
|
|
8826
|
-
|
|
8827
|
-
|
|
8828
|
-
|
|
8829
|
-
|
|
8830
|
-
|
|
8831
|
-
|
|
8832
|
-
|
|
8833
|
-
|
|
8834
|
-
|
|
8835
|
-
|
|
8836
|
-
|
|
8837
|
-
|
|
10132
|
+
}
|
|
10133
|
+
return blocks;
|
|
10134
|
+
}
|
|
10135
|
+
/**
|
|
10136
|
+
* Returns the live array of contributors (readonly snapshot for
|
|
10137
|
+
* passing to DefaultSystemPromptBuilder at build time).
|
|
10138
|
+
*/
|
|
10139
|
+
listSystemPromptContributors() {
|
|
10140
|
+
return this.promptContributors;
|
|
10141
|
+
}
|
|
10142
|
+
/**
|
|
10143
|
+
* Register an extension. Duplicate names are rejected.
|
|
10144
|
+
* Returns an unregister function.
|
|
10145
|
+
*/
|
|
10146
|
+
register(ext) {
|
|
10147
|
+
if (this.extensions.some((e) => e.name === ext.name)) {
|
|
10148
|
+
throw new WrongStackError({
|
|
10149
|
+
message: `Extension "${ext.name}" already registered`,
|
|
10150
|
+
code: "REGISTRY_DUPLICATE",
|
|
10151
|
+
subsystem: "container",
|
|
10152
|
+
context: { extension: ext.name }
|
|
8838
10153
|
});
|
|
8839
10154
|
}
|
|
8840
|
-
|
|
8841
|
-
|
|
8842
|
-
|
|
8843
|
-
|
|
8844
|
-
|
|
8845
|
-
|
|
8846
|
-
|
|
8847
|
-
|
|
8848
|
-
|
|
8849
|
-
|
|
8850
|
-
|
|
8851
|
-
|
|
8852
|
-
|
|
8853
|
-
|
|
8854
|
-
|
|
8855
|
-
|
|
8856
|
-
|
|
8857
|
-
|
|
8858
|
-
|
|
8859
|
-
|
|
8860
|
-
|
|
8861
|
-
|
|
8862
|
-
|
|
8863
|
-
|
|
8864
|
-
|
|
8865
|
-
|
|
8866
|
-
|
|
8867
|
-
|
|
8868
|
-
|
|
8869
|
-
|
|
8870
|
-
|
|
8871
|
-
|
|
8872
|
-
|
|
10155
|
+
this.extensions.push(ext);
|
|
10156
|
+
return () => this.unregister(ext.name);
|
|
10157
|
+
}
|
|
10158
|
+
/**
|
|
10159
|
+
* Register an extension, silently replacing any previous registration
|
|
10160
|
+
* with the same name. Use this when overriding a default extension.
|
|
10161
|
+
*/
|
|
10162
|
+
registerOrReplace(ext) {
|
|
10163
|
+
const idx = this.extensions.findIndex((e) => e.name === ext.name);
|
|
10164
|
+
if (idx >= 0) this.extensions.splice(idx, 1);
|
|
10165
|
+
return this.register(ext);
|
|
10166
|
+
}
|
|
10167
|
+
/**
|
|
10168
|
+
* Unregister an extension by name. Returns true if found.
|
|
10169
|
+
*/
|
|
10170
|
+
unregister(name) {
|
|
10171
|
+
const idx = this.extensions.findIndex((e) => e.name === name);
|
|
10172
|
+
if (idx === -1) return false;
|
|
10173
|
+
this.extensions.splice(idx, 1);
|
|
10174
|
+
return true;
|
|
10175
|
+
}
|
|
10176
|
+
/**
|
|
10177
|
+
* List registered extension names in order.
|
|
10178
|
+
*/
|
|
10179
|
+
list() {
|
|
10180
|
+
return this.extensions.map((e) => e.name);
|
|
10181
|
+
}
|
|
10182
|
+
/**
|
|
10183
|
+
* Check if an extension with the given name is registered.
|
|
10184
|
+
*/
|
|
10185
|
+
has(name) {
|
|
10186
|
+
return this.extensions.some((e) => e.name === name);
|
|
10187
|
+
}
|
|
10188
|
+
/**
|
|
10189
|
+
* Remove all registered extensions and contributors.
|
|
10190
|
+
*/
|
|
10191
|
+
clear() {
|
|
10192
|
+
this.extensions.length = 0;
|
|
10193
|
+
this.promptContributors.length = 0;
|
|
10194
|
+
}
|
|
10195
|
+
// ── Hook runners ─────────────────────────────────────────────────
|
|
10196
|
+
async runBeforeRun(...args) {
|
|
10197
|
+
for (const ext of this.extensions) {
|
|
10198
|
+
if (!ext.beforeRun) continue;
|
|
10199
|
+
try {
|
|
10200
|
+
await ext.beforeRun(...args);
|
|
10201
|
+
} catch (err) {
|
|
10202
|
+
this.log?.error(`Extension "${ext.name}" beforeRun hook failed`, err);
|
|
8873
10203
|
}
|
|
8874
10204
|
}
|
|
8875
10205
|
}
|
|
8876
|
-
|
|
8877
|
-
|
|
8878
|
-
|
|
8879
|
-
|
|
8880
|
-
|
|
8881
|
-
|
|
8882
|
-
|
|
8883
|
-
|
|
8884
|
-
|
|
8885
|
-
currentTextIndex: -1,
|
|
8886
|
-
tools: /* @__PURE__ */ new Map(),
|
|
8887
|
-
thinking: [],
|
|
8888
|
-
currentThinkingIndex: -1,
|
|
8889
|
-
blockOrder: []
|
|
8890
|
-
};
|
|
8891
|
-
}
|
|
8892
|
-
function handleMessageStart(state, model) {
|
|
8893
|
-
state.model = model;
|
|
8894
|
-
}
|
|
8895
|
-
function handleContentBlockStart(state, ev) {
|
|
8896
|
-
const kind = ev.kind ?? "text";
|
|
8897
|
-
if (kind === "text") {
|
|
8898
|
-
state.currentTextIndex = state.textBuffers.length;
|
|
8899
|
-
state.textBuffers.push("");
|
|
8900
|
-
state.blockOrder.push({ kind: "text", idx: state.currentTextIndex });
|
|
8901
|
-
} else if (kind === "tool_use") {
|
|
8902
|
-
const id = ev.id ?? crypto.randomUUID();
|
|
8903
|
-
state.tools.set(id, { name: ev.name ?? "unknown", partial: "" });
|
|
8904
|
-
state.blockOrder.push({ kind: "tool", id });
|
|
8905
|
-
state.currentTextIndex = -1;
|
|
8906
|
-
} else if (kind === "thinking") {
|
|
8907
|
-
state.currentThinkingIndex = state.thinking.length;
|
|
8908
|
-
state.thinking.push({
|
|
8909
|
-
textBuf: "",
|
|
8910
|
-
...ev.providerMeta ? { providerMeta: ev.providerMeta } : {}
|
|
8911
|
-
});
|
|
8912
|
-
state.blockOrder.push({ kind: "thinking", idx: state.currentThinkingIndex });
|
|
8913
|
-
state.currentTextIndex = -1;
|
|
8914
|
-
}
|
|
8915
|
-
}
|
|
8916
|
-
function handleContentBlockStop(state, ev) {
|
|
8917
|
-
}
|
|
8918
|
-
function handleTextDelta(state, text) {
|
|
8919
|
-
if (state.currentTextIndex === -1) {
|
|
8920
|
-
state.currentTextIndex = state.textBuffers.length;
|
|
8921
|
-
state.textBuffers.push("");
|
|
8922
|
-
state.blockOrder.push({ kind: "text", idx: state.currentTextIndex });
|
|
10206
|
+
async runAfterRun(...args) {
|
|
10207
|
+
for (const ext of this.extensions) {
|
|
10208
|
+
if (!ext.afterRun) continue;
|
|
10209
|
+
try {
|
|
10210
|
+
await ext.afterRun(...args);
|
|
10211
|
+
} catch (err) {
|
|
10212
|
+
this.log?.error(`Extension "${ext.name}" afterRun hook failed`, err);
|
|
10213
|
+
}
|
|
10214
|
+
}
|
|
8923
10215
|
}
|
|
8924
|
-
|
|
8925
|
-
|
|
8926
|
-
|
|
8927
|
-
|
|
8928
|
-
|
|
8929
|
-
|
|
8930
|
-
}
|
|
8931
|
-
|
|
8932
|
-
|
|
8933
|
-
if (t2) t2.partial += ev.partial;
|
|
8934
|
-
}
|
|
8935
|
-
function safeJsonOrRaw(s) {
|
|
8936
|
-
if (!s) return {};
|
|
8937
|
-
try {
|
|
8938
|
-
return JSON.parse(s);
|
|
8939
|
-
} catch {
|
|
8940
|
-
return { _raw: s };
|
|
10216
|
+
async runBeforeIteration(...args) {
|
|
10217
|
+
for (const ext of this.extensions) {
|
|
10218
|
+
if (!ext.beforeIteration) continue;
|
|
10219
|
+
try {
|
|
10220
|
+
await ext.beforeIteration(...args);
|
|
10221
|
+
} catch (err) {
|
|
10222
|
+
this.log?.error(`Extension "${ext.name}" beforeIteration hook failed`, err);
|
|
10223
|
+
}
|
|
10224
|
+
}
|
|
8941
10225
|
}
|
|
8942
|
-
|
|
8943
|
-
|
|
8944
|
-
|
|
8945
|
-
|
|
8946
|
-
|
|
8947
|
-
|
|
10226
|
+
async runAfterIteration(...args) {
|
|
10227
|
+
for (const ext of this.extensions) {
|
|
10228
|
+
if (!ext.afterIteration) continue;
|
|
10229
|
+
try {
|
|
10230
|
+
await ext.afterIteration(...args);
|
|
10231
|
+
} catch (err) {
|
|
10232
|
+
this.log?.error(`Extension "${ext.name}" afterIteration hook failed`, err);
|
|
10233
|
+
}
|
|
10234
|
+
}
|
|
8948
10235
|
}
|
|
8949
|
-
|
|
8950
|
-
|
|
8951
|
-
|
|
8952
|
-
|
|
8953
|
-
|
|
8954
|
-
|
|
8955
|
-
|
|
8956
|
-
|
|
8957
|
-
|
|
8958
|
-
|
|
8959
|
-
}
|
|
8960
|
-
|
|
8961
|
-
|
|
8962
|
-
|
|
10236
|
+
/**
|
|
10237
|
+
* Run onError hooks in order. The first hook that returns a non-void
|
|
10238
|
+
* result wins; subsequent hooks are skipped.
|
|
10239
|
+
*/
|
|
10240
|
+
async runOnError(...args) {
|
|
10241
|
+
for (const ext of this.extensions) {
|
|
10242
|
+
if (!ext.onError) continue;
|
|
10243
|
+
try {
|
|
10244
|
+
const result = await ext.onError(...args);
|
|
10245
|
+
if (result) return result;
|
|
10246
|
+
} catch (err) {
|
|
10247
|
+
this.log?.error(`Extension "${ext.name}" onError hook failed`, err);
|
|
10248
|
+
}
|
|
10249
|
+
}
|
|
8963
10250
|
}
|
|
8964
|
-
|
|
8965
|
-
|
|
8966
|
-
|
|
8967
|
-
|
|
8968
|
-
|
|
8969
|
-
|
|
10251
|
+
/**
|
|
10252
|
+
* Build a composed provider runner. Extensions with `wrapProviderRunner`
|
|
10253
|
+
* form a middleware-style chain: the innermost extension wraps the
|
|
10254
|
+
* default runner, each subsequent wrapper wraps the previous.
|
|
10255
|
+
*/
|
|
10256
|
+
wrapProviderRunner(inner) {
|
|
10257
|
+
const wrappers = this.extensions.filter((e) => e.wrapProviderRunner).map((e) => ({ name: e.name, wrap: e.wrapProviderRunner }));
|
|
10258
|
+
if (wrappers.length === 0) return inner;
|
|
10259
|
+
let composed = inner;
|
|
10260
|
+
for (let i = wrappers.length - 1; i >= 0; i--) {
|
|
10261
|
+
const wrapper = wrappers[i];
|
|
10262
|
+
const next = composed;
|
|
10263
|
+
composed = async (ctx, req) => {
|
|
10264
|
+
try {
|
|
10265
|
+
return await wrapper.wrap(ctx, req, next);
|
|
10266
|
+
} catch (err) {
|
|
10267
|
+
this.log?.error(`Extension "${wrapper.name}" wrapProviderRunner failed`, err);
|
|
10268
|
+
throw err;
|
|
10269
|
+
}
|
|
10270
|
+
};
|
|
10271
|
+
}
|
|
10272
|
+
return composed;
|
|
8970
10273
|
}
|
|
8971
|
-
|
|
8972
|
-
|
|
8973
|
-
|
|
8974
|
-
|
|
8975
|
-
|
|
8976
|
-
|
|
8977
|
-
|
|
8978
|
-
|
|
8979
|
-
state.usage = ev.usage ?? { input: 0, output: 0 };
|
|
8980
|
-
}
|
|
8981
|
-
async function streamProviderToResponse(provider, req, signal, ctx, events) {
|
|
8982
|
-
const state = createStreamingState(req.model);
|
|
8983
|
-
const iter = provider.stream(req, { signal })[Symbol.asyncIterator]();
|
|
8984
|
-
try {
|
|
8985
|
-
for (; ; ) {
|
|
8986
|
-
const next = await iter.next();
|
|
8987
|
-
if (next.done) break;
|
|
8988
|
-
const ev = next.value;
|
|
8989
|
-
switch (ev.type) {
|
|
8990
|
-
case "message_start":
|
|
8991
|
-
handleMessageStart(state, ev.model);
|
|
8992
|
-
break;
|
|
8993
|
-
case "content_block_start":
|
|
8994
|
-
handleContentBlockStart(state, ev);
|
|
8995
|
-
break;
|
|
8996
|
-
case "content_block_stop":
|
|
8997
|
-
handleContentBlockStop(state, ev);
|
|
8998
|
-
break;
|
|
8999
|
-
case "text_delta":
|
|
9000
|
-
handleTextDelta(state, ev.text);
|
|
9001
|
-
events.emit("provider.text_delta", { ctx, text: ev.text });
|
|
9002
|
-
break;
|
|
9003
|
-
case "tool_use_start":
|
|
9004
|
-
handleToolUseStart(state, ev);
|
|
9005
|
-
events.emit("provider.tool_use_start", { ctx, id: ev.id, name: ev.name });
|
|
9006
|
-
break;
|
|
9007
|
-
case "tool_use_input_delta":
|
|
9008
|
-
handleToolUseInputDelta(state, ev);
|
|
9009
|
-
break;
|
|
9010
|
-
case "tool_use_stop":
|
|
9011
|
-
handleToolUseStop(state, ev);
|
|
9012
|
-
events.emit("provider.tool_use_stop", { ctx, id: ev.id });
|
|
9013
|
-
break;
|
|
9014
|
-
case "thinking_start":
|
|
9015
|
-
handleThinkingStart(state, ev);
|
|
9016
|
-
break;
|
|
9017
|
-
case "thinking_delta":
|
|
9018
|
-
handleThinkingDelta(state, ev.text);
|
|
9019
|
-
events.emit("provider.thinking_delta", { ctx, text: ev.text });
|
|
9020
|
-
break;
|
|
9021
|
-
case "thinking_signature":
|
|
9022
|
-
handleThinkingSignature(state, ev.signature);
|
|
9023
|
-
break;
|
|
9024
|
-
case "thinking_stop":
|
|
9025
|
-
handleThinkingStop(state);
|
|
9026
|
-
break;
|
|
9027
|
-
case "message_stop":
|
|
9028
|
-
handleMessageStop(state, ev);
|
|
9029
|
-
break;
|
|
10274
|
+
async runBeforeToolExecution(...args) {
|
|
10275
|
+
let toolUses = args[1];
|
|
10276
|
+
for (const ext of this.extensions) {
|
|
10277
|
+
if (!ext.beforeToolExecution) continue;
|
|
10278
|
+
try {
|
|
10279
|
+
toolUses = await ext.beforeToolExecution(args[0], toolUses);
|
|
10280
|
+
} catch (err) {
|
|
10281
|
+
this.log?.error(`Extension "${ext.name}" beforeToolExecution hook failed`, err);
|
|
9030
10282
|
}
|
|
9031
10283
|
}
|
|
9032
|
-
|
|
9033
|
-
|
|
9034
|
-
|
|
9035
|
-
|
|
9036
|
-
|
|
9037
|
-
throw err;
|
|
9038
|
-
} finally {
|
|
9039
|
-
try {
|
|
9040
|
-
let drainTimer = null;
|
|
10284
|
+
return toolUses;
|
|
10285
|
+
}
|
|
10286
|
+
async runAfterToolExecution(...args) {
|
|
10287
|
+
for (const ext of this.extensions) {
|
|
10288
|
+
if (!ext.afterToolExecution) continue;
|
|
9041
10289
|
try {
|
|
9042
|
-
await
|
|
9043
|
-
|
|
9044
|
-
|
|
9045
|
-
drainTimer = setTimeout(resolve4, 500);
|
|
9046
|
-
})
|
|
9047
|
-
]);
|
|
9048
|
-
} finally {
|
|
9049
|
-
if (drainTimer) clearTimeout(drainTimer);
|
|
10290
|
+
await ext.afterToolExecution(...args);
|
|
10291
|
+
} catch (err) {
|
|
10292
|
+
this.log?.error(`Extension "${ext.name}" afterToolExecution hook failed`, err);
|
|
9050
10293
|
}
|
|
9051
|
-
} catch {
|
|
9052
10294
|
}
|
|
9053
10295
|
}
|
|
9054
|
-
|
|
9055
|
-
}
|
|
10296
|
+
};
|
|
9056
10297
|
|
|
9057
|
-
// src/core/
|
|
9058
|
-
|
|
9059
|
-
const {
|
|
9060
|
-
|
|
9061
|
-
|
|
9062
|
-
const
|
|
9063
|
-
|
|
9064
|
-
|
|
9065
|
-
|
|
9066
|
-
"provider.attempt": attempt
|
|
9067
|
-
});
|
|
9068
|
-
try {
|
|
9069
|
-
const res = provider.capabilities.streaming ? await streamProviderToResponse(provider, request, signal, ctx, events) : await provider.complete(request, { signal });
|
|
9070
|
-
span?.setAttribute("provider.stopReason", res.stopReason);
|
|
9071
|
-
span?.setAttribute("provider.usage_in", res.usage.input);
|
|
9072
|
-
span?.setAttribute("provider.usage_out", res.usage.output);
|
|
9073
|
-
span?.end();
|
|
9074
|
-
return res;
|
|
9075
|
-
} catch (err) {
|
|
9076
|
-
if (err instanceof Error) span?.recordError(err);
|
|
9077
|
-
span?.end();
|
|
9078
|
-
if (signal.aborted) throw err;
|
|
9079
|
-
const isProviderErr = err instanceof ProviderError;
|
|
9080
|
-
const errAsErr = err instanceof Error ? err : new Error(String(err));
|
|
9081
|
-
const canRetry = retry.shouldRetry(isProviderErr ? err : errAsErr, attempt);
|
|
9082
|
-
const description = isProviderErr ? err.describe() : errAsErr.message;
|
|
9083
|
-
if (!canRetry) {
|
|
9084
|
-
if (isProviderErr) {
|
|
9085
|
-
events.emit("provider.error", {
|
|
9086
|
-
providerId: err.providerId,
|
|
9087
|
-
status: err.status,
|
|
9088
|
-
description,
|
|
9089
|
-
retryable: false
|
|
9090
|
-
});
|
|
9091
|
-
}
|
|
9092
|
-
throw err;
|
|
10298
|
+
// src/core/iteration-limit.ts
|
|
10299
|
+
function requestLimitExtension(opts) {
|
|
10300
|
+
const { events, currentIterations, currentLimit, autoExtend, timeoutMs = 3e4 } = opts;
|
|
10301
|
+
return new Promise((resolve4) => {
|
|
10302
|
+
let resolved = false;
|
|
10303
|
+
const timer = setTimeout(() => {
|
|
10304
|
+
if (!resolved) {
|
|
10305
|
+
resolved = true;
|
|
10306
|
+
resolve4(0);
|
|
9093
10307
|
}
|
|
9094
|
-
|
|
9095
|
-
|
|
9096
|
-
|
|
9097
|
-
|
|
9098
|
-
|
|
9099
|
-
|
|
9100
|
-
attempt: attemptNum,
|
|
9101
|
-
delayMs: delay,
|
|
9102
|
-
status: err.status,
|
|
9103
|
-
description
|
|
9104
|
-
});
|
|
10308
|
+
}, timeoutMs);
|
|
10309
|
+
const deny = () => {
|
|
10310
|
+
if (!resolved) {
|
|
10311
|
+
resolved = true;
|
|
10312
|
+
clearTimeout(timer);
|
|
10313
|
+
resolve4(0);
|
|
9105
10314
|
}
|
|
9106
|
-
|
|
9107
|
-
|
|
9108
|
-
|
|
9109
|
-
|
|
9110
|
-
|
|
9111
|
-
|
|
9112
|
-
|
|
9113
|
-
|
|
9114
|
-
|
|
9115
|
-
|
|
9116
|
-
|
|
9117
|
-
|
|
9118
|
-
|
|
9119
|
-
|
|
9120
|
-
|
|
9121
|
-
|
|
9122
|
-
|
|
9123
|
-
|
|
10315
|
+
};
|
|
10316
|
+
const grant = (extra) => {
|
|
10317
|
+
if (!resolved) {
|
|
10318
|
+
resolved = true;
|
|
10319
|
+
clearTimeout(timer);
|
|
10320
|
+
resolve4(Math.max(0, extra));
|
|
10321
|
+
}
|
|
10322
|
+
};
|
|
10323
|
+
events.emit("iteration.limit_reached", {
|
|
10324
|
+
currentIterations,
|
|
10325
|
+
currentLimit,
|
|
10326
|
+
grant,
|
|
10327
|
+
deny
|
|
10328
|
+
});
|
|
10329
|
+
if (autoExtend) {
|
|
10330
|
+
setImmediate(() => {
|
|
10331
|
+
if (!resolved) {
|
|
10332
|
+
resolved = true;
|
|
10333
|
+
clearTimeout(timer);
|
|
10334
|
+
resolve4(100);
|
|
9124
10335
|
}
|
|
9125
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
9126
10336
|
});
|
|
9127
|
-
attempt++;
|
|
9128
10337
|
}
|
|
9129
|
-
}
|
|
10338
|
+
});
|
|
9130
10339
|
}
|
|
9131
10340
|
|
|
9132
10341
|
// src/core/agent.ts
|
|
@@ -9163,6 +10372,7 @@ var Agent = class {
|
|
|
9163
10372
|
toolExecutor;
|
|
9164
10373
|
autoExtendLimit;
|
|
9165
10374
|
tracer;
|
|
10375
|
+
extensions;
|
|
9166
10376
|
constructor(init) {
|
|
9167
10377
|
this.container = init.container;
|
|
9168
10378
|
this.tools = init.tools;
|
|
@@ -9176,8 +10386,10 @@ var Agent = class {
|
|
|
9176
10386
|
this.perIterationOutputCapBytes = init.perIterationOutputCapBytes ?? 1e5;
|
|
9177
10387
|
this.autoExtendLimit = init.autoExtendLimit ?? true;
|
|
9178
10388
|
this.tracer = init.tracer;
|
|
10389
|
+
this.extensions = init.extensions ?? new ExtensionRegistry();
|
|
10390
|
+
this.extensions.setLogger(this.container.resolve(TOKENS.Logger));
|
|
9179
10391
|
this.toolExecutor = new ToolExecutor(this.tools, {
|
|
9180
|
-
permissionPolicy: this.permission,
|
|
10392
|
+
permissionPolicy: init.permissionPolicy ?? this.permission,
|
|
9181
10393
|
secretScrubber: this.scrubber,
|
|
9182
10394
|
renderer: this.renderer,
|
|
9183
10395
|
events: this.events,
|
|
@@ -9221,33 +10433,66 @@ var Agent = class {
|
|
|
9221
10433
|
"agent.model": opts.model ?? this.ctx.model,
|
|
9222
10434
|
"agent.executionStrategy": opts.executionStrategy ?? this.executionStrategy
|
|
9223
10435
|
});
|
|
10436
|
+
const { blocks, text } = normalizeInput(userInput);
|
|
10437
|
+
const inputPayload = { content: blocks, text, ctx: this.ctx };
|
|
10438
|
+
await this.extensions.runBeforeRun(this.ctx, inputPayload);
|
|
9224
10439
|
try {
|
|
9225
|
-
const result = await this.runInner(
|
|
10440
|
+
const result = await this.runInner(inputPayload, opts, controller);
|
|
9226
10441
|
span?.setAttribute("agent.status", result.status);
|
|
9227
10442
|
span?.setAttribute("agent.iterations", result.iterations);
|
|
10443
|
+
await this.extensions.runAfterRun(this.ctx, result);
|
|
9228
10444
|
return result;
|
|
9229
10445
|
} catch (err) {
|
|
9230
10446
|
const wse = err instanceof AgentError ? err : toWrongStackError(err);
|
|
9231
10447
|
this.events.emit("error", { err: toError(err), phase: "agent" });
|
|
9232
10448
|
if (err instanceof Error) span?.recordError(err);
|
|
9233
10449
|
span?.setAttribute("agent.status", "failed");
|
|
9234
|
-
|
|
10450
|
+
const result = {
|
|
9235
10451
|
status: signal.aborted ? "aborted" : "failed",
|
|
9236
10452
|
iterations: 0,
|
|
9237
10453
|
error: wse
|
|
9238
10454
|
};
|
|
10455
|
+
await this.extensions.runAfterRun(this.ctx, result);
|
|
10456
|
+
return result;
|
|
9239
10457
|
} finally {
|
|
9240
10458
|
span?.end();
|
|
9241
10459
|
await controller.dispose();
|
|
9242
10460
|
}
|
|
9243
10461
|
}
|
|
9244
|
-
async runInner(
|
|
9245
|
-
await this.
|
|
10462
|
+
async runInner(inputPayload, opts, controller) {
|
|
10463
|
+
await this.pipelines.userInput.run(inputPayload);
|
|
10464
|
+
this.ctx.state.appendMessage({ role: "user", content: inputPayload.content });
|
|
10465
|
+
await this.ctx.session.append({
|
|
10466
|
+
type: "user_input",
|
|
10467
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10468
|
+
content: inputPayload.content
|
|
10469
|
+
});
|
|
9246
10470
|
let finalText = "";
|
|
9247
10471
|
let iterations = 0;
|
|
9248
10472
|
let effectiveLimit = opts.maxIterations ?? this.maxIterations;
|
|
9249
10473
|
const hasHardLimit = effectiveLimit > 0 && Number.isFinite(effectiveLimit);
|
|
9250
10474
|
let recoveryRetries = 0;
|
|
10475
|
+
const diRunner = this.container.has(TOKENS.ProviderRunner) ? this.container.resolve(TOKENS.ProviderRunner) : null;
|
|
10476
|
+
const baseRunner = diRunner ? (ctx, req) => diRunner.run({
|
|
10477
|
+
provider: ctx.provider,
|
|
10478
|
+
request: req,
|
|
10479
|
+
signal: controller.signal,
|
|
10480
|
+
ctx,
|
|
10481
|
+
events: this.events,
|
|
10482
|
+
retry: this.retry,
|
|
10483
|
+
logger: this.logger,
|
|
10484
|
+
tracer: this.tracer
|
|
10485
|
+
}) : async (ctx, req) => runProviderWithRetry({
|
|
10486
|
+
provider: ctx.provider,
|
|
10487
|
+
request: req,
|
|
10488
|
+
signal: controller.signal,
|
|
10489
|
+
ctx,
|
|
10490
|
+
events: this.events,
|
|
10491
|
+
retry: this.retry,
|
|
10492
|
+
logger: this.logger,
|
|
10493
|
+
tracer: this.tracer
|
|
10494
|
+
});
|
|
10495
|
+
const customRunner = this.extensions.wrapProviderRunner(baseRunner);
|
|
9251
10496
|
for (let i = 0; ; i++) {
|
|
9252
10497
|
iterations = i + 1;
|
|
9253
10498
|
if (controller.signal.aborted) {
|
|
@@ -9263,26 +10508,39 @@ var Agent = class {
|
|
|
9263
10508
|
if (limitCheck.exit) {
|
|
9264
10509
|
return { ...limitCheck.exit, finalText };
|
|
9265
10510
|
}
|
|
10511
|
+
await this.extensions.runBeforeIteration(this.ctx, i);
|
|
9266
10512
|
this.events.emit("iteration.started", { ctx: this.ctx, index: i });
|
|
9267
10513
|
const req = await this.buildAndRunRequestPipeline(opts);
|
|
9268
10514
|
let res;
|
|
9269
10515
|
try {
|
|
9270
|
-
res = await
|
|
9271
|
-
provider: this.ctx.provider,
|
|
9272
|
-
request: req,
|
|
9273
|
-
signal: controller.signal,
|
|
9274
|
-
ctx: this.ctx,
|
|
9275
|
-
events: this.events,
|
|
9276
|
-
retry: this.retry,
|
|
9277
|
-
logger: this.logger,
|
|
9278
|
-
tracer: this.tracer
|
|
9279
|
-
});
|
|
10516
|
+
res = await customRunner(this.ctx, req);
|
|
9280
10517
|
recoveryRetries = 0;
|
|
9281
10518
|
} catch (err) {
|
|
9282
10519
|
if (controller.signal.aborted) {
|
|
9283
10520
|
this.events.emit("error", { err: toError(err), phase: "provider" });
|
|
9284
10521
|
return { status: "aborted", iterations, error: toWrongStackError(err, "AGENT_ABORTED") };
|
|
9285
10522
|
}
|
|
10523
|
+
const extDecision = await this.extensions.runOnError(this.ctx, err, "provider", i);
|
|
10524
|
+
if (extDecision) {
|
|
10525
|
+
if (extDecision.action === "fail") {
|
|
10526
|
+
this.events.emit("error", { err: toError(err), phase: "provider" });
|
|
10527
|
+
return { status: "failed", iterations, error: toWrongStackError(err) };
|
|
10528
|
+
}
|
|
10529
|
+
if (extDecision.action === "continue") {
|
|
10530
|
+
await this.extensions.runAfterIteration(this.ctx, i);
|
|
10531
|
+
continue;
|
|
10532
|
+
}
|
|
10533
|
+
if (extDecision.action === "retry") {
|
|
10534
|
+
recoveryRetries++;
|
|
10535
|
+
if (recoveryRetries > 2) {
|
|
10536
|
+
this.events.emit("error", { err: toError(err), phase: "provider" });
|
|
10537
|
+
return { status: "failed", iterations, error: toWrongStackError(err) };
|
|
10538
|
+
}
|
|
10539
|
+
if (extDecision.model) this.ctx.model = extDecision.model;
|
|
10540
|
+
this.logger.info("Extension requested retry; retrying turn");
|
|
10541
|
+
continue;
|
|
10542
|
+
}
|
|
10543
|
+
}
|
|
9286
10544
|
const recovered = await this.errorHandler.recover(err, this.ctx);
|
|
9287
10545
|
if (!recovered || recovered.action === "fail") {
|
|
9288
10546
|
this.events.emit("error", { err: toError(err), phase: "provider" });
|
|
@@ -9321,21 +10579,9 @@ var Agent = class {
|
|
|
9321
10579
|
await this.executeTools(toolUses);
|
|
9322
10580
|
this.events.emit("iteration.completed", { ctx: this.ctx, index: i });
|
|
9323
10581
|
await this.compactContextIfNeeded();
|
|
10582
|
+
await this.extensions.runAfterIteration(this.ctx, i);
|
|
9324
10583
|
}
|
|
9325
10584
|
}
|
|
9326
|
-
/**
|
|
9327
|
-
* Normalize user input and emit through userInput pipeline + session append.
|
|
9328
|
-
*/
|
|
9329
|
-
async normalizeAndEmitUserInput(userInput) {
|
|
9330
|
-
const { blocks, text } = normalizeInput(userInput);
|
|
9331
|
-
await this.pipelines.userInput.run({ content: blocks, text, ctx: this.ctx });
|
|
9332
|
-
this.ctx.state.appendMessage({ role: "user", content: blocks });
|
|
9333
|
-
await this.ctx.session.append({
|
|
9334
|
-
type: "user_input",
|
|
9335
|
-
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9336
|
-
content: blocks
|
|
9337
|
-
});
|
|
9338
|
-
}
|
|
9339
10585
|
/**
|
|
9340
10586
|
* Check if iteration limit has been reached and request extension if needed.
|
|
9341
10587
|
* Returns the new effective limit (possibly extended) and a RunResult if
|
|
@@ -9419,6 +10665,7 @@ var Agent = class {
|
|
|
9419
10665
|
* single tool.
|
|
9420
10666
|
*/
|
|
9421
10667
|
async executeTools(toolUses) {
|
|
10668
|
+
toolUses = await this.extensions.runBeforeToolExecution(this.ctx, toolUses);
|
|
9422
10669
|
const { outputs } = await this.toolExecutor.executeBatch(
|
|
9423
10670
|
toolUses,
|
|
9424
10671
|
this.ctx,
|
|
@@ -9504,6 +10751,7 @@ var Agent = class {
|
|
|
9504
10751
|
role: "user",
|
|
9505
10752
|
content: outputs.map((o) => o.result)
|
|
9506
10753
|
});
|
|
10754
|
+
await this.extensions.runAfterToolExecution(this.ctx, outputs);
|
|
9507
10755
|
}
|
|
9508
10756
|
waitForConfirm(info) {
|
|
9509
10757
|
return new Promise((resolve4) => {
|
|
@@ -9931,6 +11179,7 @@ var DefaultSystemPromptBuilder = class {
|
|
|
9931
11179
|
const layer3 = await this.buildEnvironment(ctx);
|
|
9932
11180
|
const layer4 = await this.buildMemoryAndSkills();
|
|
9933
11181
|
const layer5 = await this.buildMode();
|
|
11182
|
+
const layer6 = ctx.subagent ? "" : await this.buildActivePlan();
|
|
9934
11183
|
const blocks = [
|
|
9935
11184
|
{ type: "text", text: layer1 },
|
|
9936
11185
|
{ type: "text", text: layer2 },
|
|
@@ -9950,16 +11199,95 @@ var DefaultSystemPromptBuilder = class {
|
|
|
9950
11199
|
cache_control: { type: "ephemeral" }
|
|
9951
11200
|
});
|
|
9952
11201
|
}
|
|
11202
|
+
if (layer6.trim()) {
|
|
11203
|
+
blocks.push({
|
|
11204
|
+
type: "text",
|
|
11205
|
+
text: layer6,
|
|
11206
|
+
cache_control: { type: "ephemeral" }
|
|
11207
|
+
});
|
|
11208
|
+
}
|
|
11209
|
+
if (this.opts.contributors && this.opts.contributors.length > 0) {
|
|
11210
|
+
for (const c of this.opts.contributors) {
|
|
11211
|
+
try {
|
|
11212
|
+
const contributed = await c(ctx);
|
|
11213
|
+
blocks.push(...contributed);
|
|
11214
|
+
} catch {
|
|
11215
|
+
}
|
|
11216
|
+
}
|
|
11217
|
+
}
|
|
9953
11218
|
return blocks;
|
|
9954
11219
|
}
|
|
11220
|
+
/**
|
|
11221
|
+
* Reads `<sessionId>.plan.json` (when configured) and produces a short
|
|
11222
|
+
* "Active plan" block listing open items so the model is anchored to
|
|
11223
|
+
* the strategic roadmap every turn. Reads on every `build()` so a
|
|
11224
|
+
* plan edit (via `/plan` or the `plan` tool) reflects on the next
|
|
11225
|
+
* turn without restarting the session.
|
|
11226
|
+
*/
|
|
11227
|
+
async buildActivePlan() {
|
|
11228
|
+
const planPath = typeof this.opts.planPath === "function" ? this.opts.planPath() : this.opts.planPath;
|
|
11229
|
+
if (!planPath) return "";
|
|
11230
|
+
let raw;
|
|
11231
|
+
try {
|
|
11232
|
+
raw = await fsp2.readFile(planPath, "utf8");
|
|
11233
|
+
} catch {
|
|
11234
|
+
return "";
|
|
11235
|
+
}
|
|
11236
|
+
let parsed;
|
|
11237
|
+
try {
|
|
11238
|
+
parsed = JSON.parse(raw);
|
|
11239
|
+
} catch {
|
|
11240
|
+
return "";
|
|
11241
|
+
}
|
|
11242
|
+
if (!Array.isArray(parsed.items) || parsed.items.length === 0) return "";
|
|
11243
|
+
const open3 = parsed.items.filter((i) => i?.status !== "done");
|
|
11244
|
+
if (open3.length === 0) return "";
|
|
11245
|
+
const lines = ["## Active plan"];
|
|
11246
|
+
if (parsed.title) lines.push(`*${parsed.title}*`, "");
|
|
11247
|
+
parsed.items.forEach((it, idx) => {
|
|
11248
|
+
const mark = it?.status === "done" ? "[x]" : it?.status === "in_progress" ? "[~]" : "[ ]";
|
|
11249
|
+
lines.push(`${idx + 1}. ${mark} ${it?.title ?? "(untitled)"}`);
|
|
11250
|
+
});
|
|
11251
|
+
lines.push(
|
|
11252
|
+
"",
|
|
11253
|
+
"Use `/plan` (user) or the `plan` tool to update status as you progress. The roadmap survives session resume."
|
|
11254
|
+
);
|
|
11255
|
+
return lines.join("\n");
|
|
11256
|
+
}
|
|
9955
11257
|
buildToolUsage(tools) {
|
|
9956
11258
|
if (tools.length === 0) return "## Tool usage\n\nNo tools registered.";
|
|
9957
|
-
const
|
|
11259
|
+
const byCat = /* @__PURE__ */ new Map();
|
|
11260
|
+
const uncategorized = [];
|
|
9958
11261
|
for (const t2 of tools) {
|
|
9959
|
-
|
|
11262
|
+
if (t2.category) {
|
|
11263
|
+
let group = byCat.get(t2.category);
|
|
11264
|
+
if (!group) {
|
|
11265
|
+
group = [];
|
|
11266
|
+
byCat.set(t2.category, group);
|
|
11267
|
+
}
|
|
11268
|
+
group.push(t2);
|
|
11269
|
+
} else {
|
|
11270
|
+
uncategorized.push(t2);
|
|
11271
|
+
}
|
|
11272
|
+
}
|
|
11273
|
+
const lines = ["## Tool usage"];
|
|
11274
|
+
for (const [cat, catTools] of byCat) {
|
|
9960
11275
|
lines.push(`
|
|
11276
|
+
### ${cat}`);
|
|
11277
|
+
for (const t2 of catTools) {
|
|
11278
|
+
const hint = t2.usageHint ?? t2.description;
|
|
11279
|
+
const desc = hint.length > 80 ? `${hint.slice(0, 77)}...` : hint.trim();
|
|
11280
|
+
lines.push(`- **${t2.name}** \u2014 ${desc}`);
|
|
11281
|
+
}
|
|
11282
|
+
}
|
|
11283
|
+
if (uncategorized.length > 0) {
|
|
11284
|
+
if (byCat.size > 0) lines.push("");
|
|
11285
|
+
for (const t2 of uncategorized) {
|
|
11286
|
+
const hint = t2.usageHint ?? t2.description;
|
|
11287
|
+
lines.push(`
|
|
9961
11288
|
### ${t2.name}
|
|
9962
11289
|
${hint.trim()}`);
|
|
11290
|
+
}
|
|
9963
11291
|
}
|
|
9964
11292
|
lines.push(`
|
|
9965
11293
|
## Common patterns
|
|
@@ -9971,6 +11299,78 @@ ${hint.trim()}`);
|
|
|
9971
11299
|
- **Batch ops:** Use \`replace\` with glob patterns for multi-file surgical changes
|
|
9972
11300
|
|
|
9973
11301
|
When unsure about a file's current state, read it first rather than assuming.`);
|
|
11302
|
+
const hasDelegate = tools.some((t2) => t2.name === "delegate");
|
|
11303
|
+
if (hasDelegate) {
|
|
11304
|
+
const delegateTool = tools.find((t2) => t2.name === "delegate");
|
|
11305
|
+
const enumValues = (() => {
|
|
11306
|
+
const role = delegateTool?.inputSchema?.properties?.role?.enum;
|
|
11307
|
+
return Array.isArray(role) ? role.filter((r) => typeof r === "string") : [];
|
|
11308
|
+
})();
|
|
11309
|
+
const roleList = enumValues.length > 0 ? enumValues.join(", ") : "(no roster configured)";
|
|
11310
|
+
lines.push(`
|
|
11311
|
+
## Delegation
|
|
11312
|
+
|
|
11313
|
+
You have a \`delegate\` tool that hands a discrete piece of work to a
|
|
11314
|
+
dedicated subagent (its own context, its own LLM call, its own budget
|
|
11315
|
+
cap) and waits for the result. Use it proactively when:
|
|
11316
|
+
|
|
11317
|
+
- **The task fans out naturally** \u2014 e.g. "audit these 5 files for
|
|
11318
|
+
security issues" splits cleanly into 5 parallel \`delegate\` calls,
|
|
11319
|
+
one per file or per role. Fire them through the provider's
|
|
11320
|
+
parallel-tool-call surface in the same turn.
|
|
11321
|
+
- **A specialized role exists** \u2014 the roster has tuned prompts and
|
|
11322
|
+
budgets for: ${roleList}. Reach for a role when the description
|
|
11323
|
+
matches your subtask; otherwise pass \`name\` + \`provider\` + \`model\`.
|
|
11324
|
+
- **A subtask would blow up your context** \u2014 long log analyses, large
|
|
11325
|
+
diff reviews, multi-file refactor plans. The subagent absorbs the
|
|
11326
|
+
reading cost and hands back a summary.
|
|
11327
|
+
- **You'd otherwise switch hats mid-turn** \u2014 instead of stopping a code
|
|
11328
|
+
fix to do a security pass, delegate the security pass.
|
|
11329
|
+
|
|
11330
|
+
### Scope it tight \u2014 narrow tasks succeed, broad tasks time out
|
|
11331
|
+
|
|
11332
|
+
A subagent has a finite iteration / tool-call budget (typically 50\u201380
|
|
11333
|
+
iterations, 200\u2013300 tool calls). Tasks that mention "ALL files" or "the
|
|
11334
|
+
entire codebase" reliably exhaust that budget without producing a clean
|
|
11335
|
+
answer \u2014 the delegate returns with \`stopReason: budget_exhausted\` and
|
|
11336
|
+
no useful output.
|
|
11337
|
+
|
|
11338
|
+
- \u274C BAD: \`"Analyze ALL .ts files in src/ for bugs"\`
|
|
11339
|
+
- \u274C BAD: \`"Audit the codebase for security issues"\`
|
|
11340
|
+
- \u274C BAD: \`"Plan a refactor of the whole project"\`
|
|
11341
|
+
- \u2705 GOOD: \`"Audit src/auth/session.ts for null-deref bugs in the login flow"\`
|
|
11342
|
+
- \u2705 GOOD: \`"Check packages/core/src/storage/*.ts for unhandled promise rejections (~6 files)"\`
|
|
11343
|
+
- \u2705 GOOD: \`"Plan a phased refactor of the InMemoryBridge transport (3 files in coordination/)"\`
|
|
11344
|
+
|
|
11345
|
+
If you need fleet-wide coverage, **fan out**: list the target files
|
|
11346
|
+
yourself first (one quick \`glob\` call), then fire one \`delegate\` per
|
|
11347
|
+
chunk of \u22645\u201310 files in parallel.
|
|
11348
|
+
|
|
11349
|
+
### Reading the result
|
|
11350
|
+
|
|
11351
|
+
\`delegate\` returns a structured object. Look at \`stopReason\`:
|
|
11352
|
+
|
|
11353
|
+
- \`end_turn\` \u2014 subagent finished cleanly, \`result\` has the answer.
|
|
11354
|
+
- \`budget_exhausted\` \u2014 task was too broad; \`partial.lastAssistantText\`
|
|
11355
|
+
has whatever it managed. Narrow the next try.
|
|
11356
|
+
- \`subagent_timeout\` / \`host_timeout\` \u2014 likewise partial; raise
|
|
11357
|
+
\`timeoutMs\` only if you have a reason to believe more time would help.
|
|
11358
|
+
- \`aborted\` \u2014 the user or another tool stopped this worker; don't retry
|
|
11359
|
+
silently.
|
|
11360
|
+
- \`error\` \u2014 infrastructure problem; surface it.
|
|
11361
|
+
|
|
11362
|
+
Stay in-process (no \`delegate\`) when:
|
|
11363
|
+
- The task is trivial or atomic.
|
|
11364
|
+
- The information needed is already in your context.
|
|
11365
|
+
- The user is mid-conversation and expects an immediate reply from you,
|
|
11366
|
+
not a research detour through a subagent.
|
|
11367
|
+
|
|
11368
|
+
\`delegate\` auto-promotes the host into director mode the first time
|
|
11369
|
+
it's called \u2014 you do not need to call any setup tool. For fine-grained
|
|
11370
|
+
control over a long-running fleet (spawn N workers, hand them tasks
|
|
11371
|
+
one by one, roll up results), use \`spawn_subagent\` + \`assign_task\` +
|
|
11372
|
+
\`await_tasks\` directly; \`delegate\` is the one-call shortcut.`);
|
|
11373
|
+
}
|
|
9974
11374
|
const hasContextManager = tools.some((t2) => t2.name === "context_manager");
|
|
9975
11375
|
if (hasContextManager) {
|
|
9976
11376
|
const maxCtx = this.opts.modelCapabilities?.maxContextTokens ?? 128e3;
|
|
@@ -10159,6 +11559,21 @@ var ToolRegistry = class {
|
|
|
10159
11559
|
this.tools.set(tool.name, { tool, owner });
|
|
10160
11560
|
return true;
|
|
10161
11561
|
}
|
|
11562
|
+
/**
|
|
11563
|
+
* Bulk-register multiple tools at once. Each tool that conflicts with an
|
|
11564
|
+
* existing registration is silently skipped — use `registerAllOrThrow`
|
|
11565
|
+
* if you want it to throw on conflicts.
|
|
11566
|
+
*/
|
|
11567
|
+
registerAll(tools, owner = "core") {
|
|
11568
|
+
for (const tool of tools) this.tryRegister(tool, owner);
|
|
11569
|
+
}
|
|
11570
|
+
/**
|
|
11571
|
+
* Bulk-register and throw on the first conflict. Use when you need
|
|
11572
|
+
* strict registration (e.g. at boot time).
|
|
11573
|
+
*/
|
|
11574
|
+
registerAllOrThrow(tools, owner = "core") {
|
|
11575
|
+
for (const tool of tools) this.register(tool, owner);
|
|
11576
|
+
}
|
|
10162
11577
|
/**
|
|
10163
11578
|
* Register a tool as a default. If the tool name is already registered,
|
|
10164
11579
|
* this is a no-op — the existing registration (from core or another
|
|
@@ -10186,6 +11601,30 @@ var ToolRegistry = class {
|
|
|
10186
11601
|
}
|
|
10187
11602
|
this.tools.set(name, { tool, owner });
|
|
10188
11603
|
}
|
|
11604
|
+
/**
|
|
11605
|
+
* Wrap (decorate) an existing tool. The wrapper receives the current
|
|
11606
|
+
* tool and must return a new tool — typically the same tool with a
|
|
11607
|
+
* wrapped `execute` or `executeStream`. Throws if the tool is not
|
|
11608
|
+
* registered.
|
|
11609
|
+
*
|
|
11610
|
+
* Multiple wraps stack: each wrapper gets the output of the previous.
|
|
11611
|
+
*
|
|
11612
|
+
* @example
|
|
11613
|
+
* registry.wrap('bash', (t) => ({ ...t, permission: 'confirm' }));
|
|
11614
|
+
*/
|
|
11615
|
+
wrap(name, wrapper, owner = "core") {
|
|
11616
|
+
const entry = this.tools.get(name);
|
|
11617
|
+
if (!entry) {
|
|
11618
|
+
throw new WrongStackError({
|
|
11619
|
+
message: `Tool "${name}" not registered; cannot wrap`,
|
|
11620
|
+
code: "REGISTRY_NOT_FOUND",
|
|
11621
|
+
subsystem: "container",
|
|
11622
|
+
context: { tool: name }
|
|
11623
|
+
});
|
|
11624
|
+
}
|
|
11625
|
+
const wrapped = wrapper(entry.tool);
|
|
11626
|
+
this.tools.set(name, { tool: wrapped, owner: `${entry.owner}+${owner}` });
|
|
11627
|
+
}
|
|
10189
11628
|
get(name) {
|
|
10190
11629
|
return this.tools.get(name)?.tool;
|
|
10191
11630
|
}
|
|
@@ -10195,6 +11634,24 @@ var ToolRegistry = class {
|
|
|
10195
11634
|
list() {
|
|
10196
11635
|
return Array.from(this.tools.values()).map((e) => e.tool);
|
|
10197
11636
|
}
|
|
11637
|
+
/**
|
|
11638
|
+
* Group tools by their `category` field. Tools without a category
|
|
11639
|
+
* are placed under the key `""` (empty string). Returns a Map of
|
|
11640
|
+
* category → tools, sorted by registration order within each category.
|
|
11641
|
+
*/
|
|
11642
|
+
listByCategory() {
|
|
11643
|
+
const map = /* @__PURE__ */ new Map();
|
|
11644
|
+
for (const { tool } of this.tools.values()) {
|
|
11645
|
+
const cat = tool.category ?? "";
|
|
11646
|
+
let group = map.get(cat);
|
|
11647
|
+
if (!group) {
|
|
11648
|
+
group = [];
|
|
11649
|
+
map.set(cat, group);
|
|
11650
|
+
}
|
|
11651
|
+
group.push(tool);
|
|
11652
|
+
}
|
|
11653
|
+
return map;
|
|
11654
|
+
}
|
|
10198
11655
|
listWithOwner() {
|
|
10199
11656
|
return Array.from(this.tools.values());
|
|
10200
11657
|
}
|
|
@@ -10214,6 +11671,12 @@ var ProviderRegistry = class {
|
|
|
10214
11671
|
register(f) {
|
|
10215
11672
|
this.factories.set(f.type, f);
|
|
10216
11673
|
}
|
|
11674
|
+
/**
|
|
11675
|
+
* Bulk-register multiple provider factories at once.
|
|
11676
|
+
*/
|
|
11677
|
+
registerAll(factories) {
|
|
11678
|
+
for (const f of factories) this.register(f);
|
|
11679
|
+
}
|
|
10217
11680
|
/**
|
|
10218
11681
|
* Override an existing factory. Throws if no factory is registered
|
|
10219
11682
|
* for the given type. Use this to safely replace a provider at runtime
|
|
@@ -10282,6 +11745,12 @@ var SlashCommandRegistry = class {
|
|
|
10282
11745
|
}
|
|
10283
11746
|
return this.cmds.delete(name);
|
|
10284
11747
|
}
|
|
11748
|
+
/**
|
|
11749
|
+
* Bulk-register multiple slash commands at once.
|
|
11750
|
+
*/
|
|
11751
|
+
registerAll(cmds, owner = "core") {
|
|
11752
|
+
for (const cmd of cmds) this.register(cmd, owner);
|
|
11753
|
+
}
|
|
10285
11754
|
get(name) {
|
|
10286
11755
|
return this.cmds.get(name)?.cmd;
|
|
10287
11756
|
}
|
|
@@ -10354,15 +11823,23 @@ var DefaultPluginAPI = class {
|
|
|
10354
11823
|
providers;
|
|
10355
11824
|
mcp;
|
|
10356
11825
|
slashCommands;
|
|
11826
|
+
extensions;
|
|
11827
|
+
session;
|
|
11828
|
+
metrics;
|
|
10357
11829
|
config;
|
|
10358
11830
|
log;
|
|
11831
|
+
configStore;
|
|
10359
11832
|
pluginCleanupFns = [];
|
|
10360
11833
|
constructor(init) {
|
|
10361
11834
|
const owner = init.ownerName;
|
|
10362
11835
|
this.container = init.container;
|
|
10363
11836
|
this.events = init.events;
|
|
10364
11837
|
this.config = init.config;
|
|
11838
|
+
this.configStore = init.configStore;
|
|
10365
11839
|
this.log = init.log.child({ plugin: owner });
|
|
11840
|
+
this.extensions = init.extensions ?? new ExtensionRegistry();
|
|
11841
|
+
this.session = init.sessionWriter ?? noopSession;
|
|
11842
|
+
this.metrics = init.metricsSink ? scopedMetrics(init.metricsSink, owner) : noopMetrics;
|
|
10366
11843
|
const pipelines = init.pipelines;
|
|
10367
11844
|
const readonlyPipelines = {};
|
|
10368
11845
|
for (const [key, pipeline] of Object.entries(pipelines)) {
|
|
@@ -10373,6 +11850,7 @@ var DefaultPluginAPI = class {
|
|
|
10373
11850
|
this.tools = {
|
|
10374
11851
|
register: (t2) => tr.register(t2, owner),
|
|
10375
11852
|
unregister: (name) => tr.unregister(name),
|
|
11853
|
+
wrap: (name, wrapper) => tr.wrap(name, wrapper, owner),
|
|
10376
11854
|
get: (name) => tr.get(name),
|
|
10377
11855
|
list: () => tr.list()
|
|
10378
11856
|
};
|
|
@@ -10396,6 +11874,19 @@ var DefaultPluginAPI = class {
|
|
|
10396
11874
|
this.pluginCleanupFns.push(off);
|
|
10397
11875
|
return off;
|
|
10398
11876
|
}
|
|
11877
|
+
onPattern(pattern, handler) {
|
|
11878
|
+
const off = this.events.onPattern(pattern, handler);
|
|
11879
|
+
this.pluginCleanupFns.push(off);
|
|
11880
|
+
return off;
|
|
11881
|
+
}
|
|
11882
|
+
emitCustom(event, payload) {
|
|
11883
|
+
this.events.emit(event, payload);
|
|
11884
|
+
}
|
|
11885
|
+
onConfigChange(handler) {
|
|
11886
|
+
if (!this.configStore) return () => {
|
|
11887
|
+
};
|
|
11888
|
+
return this.configStore.watch(handler);
|
|
11889
|
+
}
|
|
10399
11890
|
/** Called by the plugin loader when uninstalling the plugin. */
|
|
10400
11891
|
drainCleanup() {
|
|
10401
11892
|
for (const fn of this.pluginCleanupFns.splice(0)) {
|
|
@@ -10405,6 +11896,9 @@ var DefaultPluginAPI = class {
|
|
|
10405
11896
|
}
|
|
10406
11897
|
}
|
|
10407
11898
|
}
|
|
11899
|
+
registerSystemPromptContributor(c) {
|
|
11900
|
+
return this.extensions.registerSystemPromptContributor(c);
|
|
11901
|
+
}
|
|
10408
11902
|
};
|
|
10409
11903
|
var noopMcp = {
|
|
10410
11904
|
start: async () => void 0,
|
|
@@ -10425,6 +11919,32 @@ var noopSlashCommands = {
|
|
|
10425
11919
|
return [];
|
|
10426
11920
|
}
|
|
10427
11921
|
};
|
|
11922
|
+
var noopSession = {
|
|
11923
|
+
append: async () => {
|
|
11924
|
+
}
|
|
11925
|
+
};
|
|
11926
|
+
var noopMetrics = {
|
|
11927
|
+
counter() {
|
|
11928
|
+
},
|
|
11929
|
+
histogram() {
|
|
11930
|
+
},
|
|
11931
|
+
gauge() {
|
|
11932
|
+
}
|
|
11933
|
+
};
|
|
11934
|
+
function scopedMetrics(sink, pluginName) {
|
|
11935
|
+
const prefix = `plugin.${pluginName}.`;
|
|
11936
|
+
return {
|
|
11937
|
+
counter(name, value, labels) {
|
|
11938
|
+
sink.counter(`${prefix}${name}`, value, labels);
|
|
11939
|
+
},
|
|
11940
|
+
histogram(name, value, labels) {
|
|
11941
|
+
sink.histogram(`${prefix}${name}`, value, labels);
|
|
11942
|
+
},
|
|
11943
|
+
gauge(name, value, labels) {
|
|
11944
|
+
sink.gauge(`${prefix}${name}`, value, labels);
|
|
11945
|
+
}
|
|
11946
|
+
};
|
|
11947
|
+
}
|
|
10428
11948
|
|
|
10429
11949
|
// src/plugin/loader.ts
|
|
10430
11950
|
var KERNEL_API_VERSION = "0.1.10";
|
|
@@ -10450,6 +11970,16 @@ function satisfies(range, version) {
|
|
|
10450
11970
|
function normalizeDep(d) {
|
|
10451
11971
|
return typeof d === "string" ? { name: d } : d;
|
|
10452
11972
|
}
|
|
11973
|
+
function shallowMerge(defaults, overrides) {
|
|
11974
|
+
if (overrides === void 0 || overrides === null) return { ...defaults };
|
|
11975
|
+
if (typeof overrides !== "object") return { ...defaults };
|
|
11976
|
+
const ov = overrides;
|
|
11977
|
+
const out = { ...defaults };
|
|
11978
|
+
for (const key of Object.keys(ov)) {
|
|
11979
|
+
out[key] = ov[key];
|
|
11980
|
+
}
|
|
11981
|
+
return out;
|
|
11982
|
+
}
|
|
10453
11983
|
function topoSort(plugins) {
|
|
10454
11984
|
const map = /* @__PURE__ */ new Map();
|
|
10455
11985
|
for (const p of plugins) map.set(p.name, p);
|
|
@@ -10545,6 +12075,11 @@ async function loadPlugins(plugins, opts) {
|
|
|
10545
12075
|
failed.push({ plugin, err });
|
|
10546
12076
|
continue;
|
|
10547
12077
|
}
|
|
12078
|
+
if (plugin.defaultConfig && opts.pluginOptions) {
|
|
12079
|
+
const userOpts = opts.pluginOptions[plugin.name];
|
|
12080
|
+
const merged = shallowMerge(plugin.defaultConfig, userOpts);
|
|
12081
|
+
opts.pluginOptions[plugin.name] = merged;
|
|
12082
|
+
}
|
|
10548
12083
|
if (plugin.configSchema && opts.pluginOptions) {
|
|
10549
12084
|
const pluginOpts = opts.pluginOptions[plugin.name];
|
|
10550
12085
|
if (pluginOpts !== void 0) {
|
|
@@ -10566,7 +12101,7 @@ async function loadPlugins(plugins, opts) {
|
|
|
10566
12101
|
}
|
|
10567
12102
|
try {
|
|
10568
12103
|
const rawApi = opts.apiFactory(plugin);
|
|
10569
|
-
const api = plugin.capabilities ? wrapApiForCapabilityCheck(plugin, rawApi, opts.log) : rawApi;
|
|
12104
|
+
const api = plugin.capabilities ? wrapApiForCapabilityCheck(plugin, rawApi, opts.log, opts.enforceCapabilities) : rawApi;
|
|
10570
12105
|
await plugin.setup(api);
|
|
10571
12106
|
loaded.push(plugin);
|
|
10572
12107
|
opts.log.info(`Plugin "${plugin.name}" loaded`);
|
|
@@ -10590,10 +12125,18 @@ async function unloadPlugins(loadedPlugins, opts) {
|
|
|
10590
12125
|
}
|
|
10591
12126
|
}
|
|
10592
12127
|
}
|
|
10593
|
-
function wrapApiForCapabilityCheck(plugin, api, log) {
|
|
12128
|
+
function wrapApiForCapabilityCheck(plugin, api, log, enforce = false) {
|
|
10594
12129
|
const caps = plugin.capabilities ?? {};
|
|
10595
|
-
const
|
|
12130
|
+
const violate = (subsystem, detail) => {
|
|
10596
12131
|
const msg = `Plugin "${plugin.name}" used ${subsystem} without declaring capabilities.${subsystem} \u2014 ${detail}`;
|
|
12132
|
+
if (enforce) {
|
|
12133
|
+
throw new PluginError({
|
|
12134
|
+
message: msg,
|
|
12135
|
+
code: "PLUGIN_LOAD_FAILED",
|
|
12136
|
+
pluginName: plugin.name,
|
|
12137
|
+
context: { subsystem, detail }
|
|
12138
|
+
});
|
|
12139
|
+
}
|
|
10597
12140
|
if (typeof log.warn === "function") log.warn(msg);
|
|
10598
12141
|
else log.error(msg);
|
|
10599
12142
|
};
|
|
@@ -10601,7 +12144,7 @@ function wrapApiForCapabilityCheck(plugin, api, log) {
|
|
|
10601
12144
|
get(target, prop, receiver) {
|
|
10602
12145
|
if (prop === "register") {
|
|
10603
12146
|
return (t2) => {
|
|
10604
|
-
|
|
12147
|
+
violate("tools", `register(${t2?.name ?? "<unknown>"})`);
|
|
10605
12148
|
return target.register(t2);
|
|
10606
12149
|
};
|
|
10607
12150
|
}
|
|
@@ -10612,7 +12155,7 @@ function wrapApiForCapabilityCheck(plugin, api, log) {
|
|
|
10612
12155
|
get(target, prop, receiver) {
|
|
10613
12156
|
if (prop === "register") {
|
|
10614
12157
|
return (f) => {
|
|
10615
|
-
|
|
12158
|
+
violate("providers", `register(${f?.type ?? "<unknown>"})`);
|
|
10616
12159
|
return target.register(f);
|
|
10617
12160
|
};
|
|
10618
12161
|
}
|
|
@@ -10623,7 +12166,10 @@ function wrapApiForCapabilityCheck(plugin, api, log) {
|
|
|
10623
12166
|
get(target, prop, receiver) {
|
|
10624
12167
|
if (prop === "register") {
|
|
10625
12168
|
return (c) => {
|
|
10626
|
-
|
|
12169
|
+
violate(
|
|
12170
|
+
"slashCommands",
|
|
12171
|
+
`register(${c?.name ?? "<unknown>"})`
|
|
12172
|
+
);
|
|
10627
12173
|
return target.register(c);
|
|
10628
12174
|
};
|
|
10629
12175
|
}
|
|
@@ -10634,7 +12180,7 @@ function wrapApiForCapabilityCheck(plugin, api, log) {
|
|
|
10634
12180
|
get(target, prop, receiver) {
|
|
10635
12181
|
if (prop === "start") {
|
|
10636
12182
|
return (cfg) => {
|
|
10637
|
-
|
|
12183
|
+
violate("mcp", `start(${cfg?.name ?? "<unknown>"})`);
|
|
10638
12184
|
return target.start(cfg);
|
|
10639
12185
|
};
|
|
10640
12186
|
}
|
|
@@ -10659,6 +12205,6 @@ function wrapApiForCapabilityCheck(plugin, api, log) {
|
|
|
10659
12205
|
});
|
|
10660
12206
|
}
|
|
10661
12207
|
|
|
10662
|
-
export { ALL_FLEET_AGENTS, AUDIT_LOG_AGENT, Agent, AgentError, AutoCompactionMiddleware, AutonomousRunner, BUG_HUNTER_AGENT, BudgetExceededError, ConfigError, ConfigMigrationError, Container, Context, ConversationState, DEFAULT_CONFIG_MIGRATIONS, DEFAULT_DIRECTOR_PREAMBLE, DEFAULT_MAX_ITERATIONS, DEFAULT_MODES, DEFAULT_RECOVERY_STRATEGIES, 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, DirectorBudgetError, DoneConditionChecker, EventBus, FLEET_ROSTER, 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, REFACTOR_PLANNER_AGENT, RecoveryLock, RunController, SECURITY_SCANNER_AGENT, SelectiveCompactor, SessionAnalyzer, SessionError, SlashCommandRegistry, SpecDrivenDev, SpecParser, SubagentBudget, TOKENS, TaskFlow, TaskGenerator, TaskTracker, ToolError, ToolExecutor, ToolRegistry, WrongStackError, allServers, asBlocks, asText, atomicWrite, awsServer, blockServer, braveSearchServer, buildChildEnv, buildOtlpMetricsRequest, buildOtlpTracesRequest, buildRecoveryStrategies, classifyFamily, color, compileGlob, composeDirectorPrompt, composeSubagentPrompt, computeTaskProgress, context7Server, contextManagerTool, createContextManagerTool, createDefaultPipelines, createMessage, createToolOutputSerializer, decryptConfigSecrets, detectNewlineStyle, encryptConfigSecrets, ensureDir, estimateTextTokens, estimateToolInputTokens, estimateToolResultTokens, everArtServer, extractRunEnv, filesystemServer, findCriticalPath, formatTodosList, githubServer, googleMapsServer, isAgentError, isConfigError, isImageBlock, isPluginError, isSessionError, isTextBlock, isThinkingBlock, 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 };
|
|
12208
|
+
export { ALL_FLEET_AGENTS, AUDIT_LOG_AGENT, Agent, AgentError, AutoApprovePermissionPolicy, AutoCompactionMiddleware, AutonomousRunner, BUG_HUNTER_AGENT, BudgetExceededError, ConfigError, ConfigMigrationError, Container, Context, ConversationState, DEFAULT_CONFIG_MIGRATIONS, DEFAULT_DIRECTOR_PREAMBLE, DEFAULT_MAX_ITERATIONS, DEFAULT_MODES, DEFAULT_RECOVERY_STRATEGIES, DEFAULT_SPEC_TEMPLATE, DEFAULT_SUBAGENT_BASELINE, DefaultAttachmentStore, DefaultConfigLoader, DefaultConfigStore, DefaultErrorHandler, DefaultHealthRegistry, DefaultLogger, DefaultMemoryStore, DefaultModeStore, DefaultModelsRegistry, DefaultMultiAgentCoordinator, DefaultPathResolver, DefaultPermissionPolicy, DefaultPluginAPI, DefaultProviderRunner, DefaultRetryPolicy, DefaultSecretScrubber, DefaultSecretVault, DefaultSessionReader, DefaultSessionStore, DefaultSkillLoader, DefaultSystemPromptBuilder, DefaultTaskStore, DefaultTokenCounter, Director, DirectorBudgetError, DirectorStateCheckpoint, DoneConditionChecker, EventBus, ExtensionRegistry, FLEET_ROSTER, 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, REFACTOR_PLANNER_AGENT, RecoveryLock, RunController, SECURITY_SCANNER_AGENT, SelectiveCompactor, SessionAnalyzer, SessionError, SlashCommandRegistry, SpecDrivenDev, SpecParser, SubagentBudget, TOKENS, TaskFlow, TaskGenerator, TaskTracker, ToolError, ToolExecutor, ToolRegistry, WrongStackError, addPlanItem, allServers, asBlocks, asText, atomicWrite, attachPlanCheckpoint, attachTodosCheckpoint, awsServer, blockServer, braveSearchServer, buildChildEnv, buildOtlpMetricsRequest, buildOtlpTracesRequest, buildRecoveryStrategies, classifyFamily, clearPlan, color, compileGlob, composeDirectorPrompt, composeSubagentPrompt, computeTaskProgress, context7Server, contextManagerTool, createContextManagerTool, createDefaultPipelines, createDelegateTool, createMessage, createToolOutputSerializer, decryptConfigSecrets, detectNewlineStyle, emptyPlan, encryptConfigSecrets, ensureDir, estimateTextTokens, estimateToolInputTokens, estimateToolResultTokens, everArtServer, extractRunEnv, filesystemServer, findCriticalPath, formatPlan, formatTodosList, githubServer, googleMapsServer, isAgentError, isConfigError, isImageBlock, isPluginError, isSessionError, isTextBlock, isThinkingBlock, isToolError, isToolResultBlock, isToolUseBlock, isWrongStackError, loadDirectorState, loadPlan, loadPlugins, loadProjectModes, loadTodosCheckpoint, loadUserModes, makeAgentSubagentRunner, makeDirectorSessionFactory, matchAny, matchGlob, migratePlaintextSecrets, normalizeToLf, projectHash, removePlanItem, renderPrometheus, resolveWstackPaths, rewriteConfigEncrypted, rosterSummaryFromConfigs, runConfigMigrations, safeParse, safeStringify, sanitizeJsonString, savePlan, saveTodosCheckpoint, sentinelServer, setPlanItemStatus, slackServer, startMetricsServer, startOtlpMetricsExporter, startOtlpTraceExporter, stripAnsi, toStyle, toWrongStackError, topologicalSort, unifiedDiff, unloadPlugins, validateAgainstSchema, wireMetricsToEvents, wrapAsState };
|
|
10663
12209
|
//# sourceMappingURL=index.js.map
|
|
10664
12210
|
//# sourceMappingURL=index.js.map
|