@wrongstack/core 0.2.0 → 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-DmBiCipY.d.ts → agent-bridge-C3DUGjSb.d.ts} +1 -1
- package/dist/{compactor-DSl2FK7a.d.ts → compactor-BUU6Zm_3.d.ts} +1 -1
- package/dist/{config-DXrqb41m.d.ts → config-CKLYPkCi.d.ts} +1 -1
- package/dist/{context-u0bryklF.d.ts → context-IovtuTf8.d.ts} +2 -0
- package/dist/coordination/index.d.ts +11 -11
- package/dist/coordination/index.js +24 -1
- package/dist/coordination/index.js.map +1 -1
- package/dist/defaults/index.d.ts +30 -15
- package/dist/defaults/index.js +321 -2
- package/dist/defaults/index.js.map +1 -1
- package/dist/{events-B6Q03pTu.d.ts → events-CNB9PALO.d.ts} +27 -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/{plugin-CoYYZKdn.d.ts → index-BDb0cAMP.d.ts} +370 -11
- package/dist/index.d.ts +75 -25
- package/dist/index.js +1864 -1347
- 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-BA1Ofmfj.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-BDfkxL5C.d.ts → multi-agent-B9a6sflH.d.ts} +2 -2
- package/dist/observability/index.d.ts +2 -2
- package/dist/{path-resolver-Crkt8wTQ.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-3TLUkiCV.d.ts → secret-scrubber-CgG2tV2B.d.ts} +1 -1
- package/dist/{secret-scrubber-CwYliRWd.d.ts → secret-scrubber-Cuy5afaQ.d.ts} +1 -1
- package/dist/security/index.d.ts +3 -3
- package/dist/security/index.js +24 -1
- package/dist/security/index.js.map +1 -1
- package/dist/{selector-BRqzvugb.d.ts → selector-wT2fv9Fg.d.ts} +1 -1
- package/dist/{session-reader-C3x96CDR.d.ts → session-reader-CcPi4BQ8.d.ts} +1 -1
- package/dist/{skill-Bx8jxznf.d.ts → skill-C_7znCIC.d.ts} +2 -2
- package/dist/storage/index.d.ts +5 -5
- package/dist/storage/index.js +24 -1
- package/dist/storage/index.js.map +1 -1
- package/dist/{renderer-0A2ZEtca.d.ts → system-prompt-Dk1qm8ey.d.ts} +30 -2
- package/dist/{tool-executor-CYdZdtno.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/system-prompt-CG9jU5-5.d.ts +0 -31
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";
|
|
@@ -4889,501 +4978,424 @@ function parseDescription(raw) {
|
|
|
4889
4978
|
return { trigger, scope };
|
|
4890
4979
|
}
|
|
4891
4980
|
|
|
4892
|
-
// src/
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
this.hardThreshold = opts.hardThreshold ?? 0.9;
|
|
4908
|
-
this.maxContext = opts.maxContext ?? 128e3;
|
|
4909
|
-
this.preserveK = opts.preserveK ?? 4;
|
|
4910
|
-
this.eliseThreshold = opts.eliseThreshold ?? 500;
|
|
4911
|
-
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.";
|
|
4912
|
-
this.summarizerModel = opts.summarizerModel;
|
|
4913
|
-
}
|
|
4914
|
-
async compact(ctx, opts = {}) {
|
|
4915
|
-
const beforeTokens = this.estimateTokens(ctx.messages);
|
|
4916
|
-
const reductions = [];
|
|
4917
|
-
const load = beforeTokens / this.maxContext;
|
|
4918
|
-
const aggressive = load >= this.hardThreshold ? true : opts.aggressive ?? load >= this.softThreshold;
|
|
4919
|
-
const saved1 = this.eliseOldToolResults(ctx);
|
|
4920
|
-
if (saved1 > 0) reductions.push({ phase: "elision", saved: saved1 });
|
|
4921
|
-
if (aggressive) {
|
|
4922
|
-
const saved2 = await this.summarizeAncientTurns(ctx);
|
|
4923
|
-
if (saved2 > 0) reductions.push({ phase: "summary", saved: saved2 });
|
|
4924
|
-
} else if (load >= this.warnThreshold) {
|
|
4925
|
-
const saved2 = this.lightweightCompact(ctx);
|
|
4926
|
-
if (saved2 > 0) reductions.push({ phase: "elision", saved: saved2 });
|
|
4927
|
-
}
|
|
4928
|
-
const afterTokens = this.estimateTokens(ctx.messages);
|
|
4929
|
-
return { before: beforeTokens, after: afterTokens, reductions };
|
|
4930
|
-
}
|
|
4931
|
-
async summarizeAncientTurns(ctx) {
|
|
4932
|
-
const messages = ctx.messages;
|
|
4933
|
-
const cutoff = Math.max(0, messages.length - this.preserveK * 2);
|
|
4934
|
-
if (cutoff <= 2) return 0;
|
|
4935
|
-
const boundary = this.findSafeBoundary(messages, 0, cutoff);
|
|
4936
|
-
if (boundary <= 1) return 0;
|
|
4937
|
-
const toSummarize = messages.slice(0, boundary);
|
|
4938
|
-
const removedTokens = this.estimateTokens(toSummarize);
|
|
4939
|
-
let summaryText;
|
|
4940
|
-
try {
|
|
4941
|
-
summaryText = await this.callSummarizer(toSummarize, ctx);
|
|
4942
|
-
} catch {
|
|
4943
|
-
summaryText = `[${toSummarize.length} earlier turns omitted \u2014 key decisions and file states preserved in context]`;
|
|
4944
|
-
}
|
|
4945
|
-
const summaryMsg = {
|
|
4946
|
-
role: "system",
|
|
4947
|
-
content: `[prior_turns_summary: ${summaryText}]`
|
|
4948
|
-
};
|
|
4949
|
-
const summaryTokens = this.estimateTokens([summaryMsg]);
|
|
4950
|
-
const tail = ctx.messages.slice(boundary);
|
|
4951
|
-
ctx.state.replaceMessages([summaryMsg, ...tail]);
|
|
4952
|
-
return Math.max(0, removedTokens - summaryTokens);
|
|
4953
|
-
}
|
|
4954
|
-
findSafeBoundary(messages, from, to) {
|
|
4955
|
-
for (let i = to; i >= from; i--) {
|
|
4956
|
-
const m = messages[i];
|
|
4957
|
-
if (!m) continue;
|
|
4958
|
-
if (m.role === "user" && this.hasTextContent(m)) {
|
|
4959
|
-
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;
|
|
4960
4996
|
}
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
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;
|
|
4972
5009
|
}
|
|
4973
|
-
|
|
4974
|
-
return i;
|
|
5010
|
+
content.push(block);
|
|
4975
5011
|
}
|
|
4976
5012
|
}
|
|
4977
|
-
return 0;
|
|
4978
5013
|
}
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
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;
|
|
4996
5052
|
}
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
if (textParts.length > 0) {
|
|
5006
|
-
lines.push(`[${role}]: ${textParts.join(" ").slice(0, 500)}`);
|
|
5007
|
-
}
|
|
5008
|
-
}
|
|
5009
|
-
}
|
|
5010
|
-
return [{ type: "text", text: lines.join("\n") }];
|
|
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 });
|
|
5011
5061
|
}
|
|
5012
|
-
|
|
5013
|
-
|
|
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 };
|
|
5014
5079
|
}
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
if (!m) continue;
|
|
5022
|
-
if (m.role === "user" || m.role === "assistant") {
|
|
5023
|
-
pairCount++;
|
|
5024
|
-
preserveStart = i;
|
|
5025
|
-
}
|
|
5026
|
-
}
|
|
5027
|
-
let saved = 0;
|
|
5028
|
-
let changed = false;
|
|
5029
|
-
const nextMessages = new Array(messages.length);
|
|
5030
|
-
for (let i = 0; i < messages.length; i++) {
|
|
5031
|
-
const msg = messages[i];
|
|
5032
|
-
if (i >= preserveStart) {
|
|
5033
|
-
nextMessages[i] = msg;
|
|
5034
|
-
continue;
|
|
5035
|
-
}
|
|
5036
|
-
if (!msg || !Array.isArray(msg.content)) {
|
|
5037
|
-
nextMessages[i] = msg;
|
|
5038
|
-
continue;
|
|
5039
|
-
}
|
|
5040
|
-
const newContent = msg.content.map((b) => {
|
|
5041
|
-
if (b.type !== "tool_result") return b;
|
|
5042
|
-
const tokens = estimateToolResultTokens(b.content);
|
|
5043
|
-
if (tokens < this.eliseThreshold) return b;
|
|
5044
|
-
saved += tokens;
|
|
5045
|
-
return {
|
|
5046
|
-
type: "tool_result",
|
|
5047
|
-
tool_use_id: b.tool_use_id,
|
|
5048
|
-
content: `[elided: ~${tokens} tokens]`,
|
|
5049
|
-
is_error: b.is_error
|
|
5050
|
-
};
|
|
5051
|
-
});
|
|
5052
|
-
if (newContent.length === msg.content.length && newContent.every((b, idx) => b === msg.content[idx])) {
|
|
5053
|
-
nextMessages[i] = msg;
|
|
5054
|
-
} else {
|
|
5055
|
-
nextMessages[i] = { ...msg, content: newContent };
|
|
5056
|
-
changed = true;
|
|
5057
|
-
}
|
|
5058
|
-
}
|
|
5059
|
-
if (changed) ctx.state.replaceMessages(nextMessages);
|
|
5060
|
-
return saved;
|
|
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;
|
|
5061
5086
|
}
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
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, {});
|
|
5065
5101
|
}
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
for (const b of m.content) {
|
|
5073
|
-
if (b.type === "text") total += estimateTextTokens(b.text);
|
|
5074
|
-
else if (b.type === "tool_use") total += estimateToolInputTokens(b.input);
|
|
5075
|
-
else if (b.type === "tool_result") total += estimateToolResultTokens(b.content);
|
|
5076
|
-
}
|
|
5077
|
-
}
|
|
5078
|
-
}
|
|
5079
|
-
return total;
|
|
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, {});
|
|
5080
5108
|
}
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
// src/models/llm-selector.ts
|
|
5084
|
-
var DEFAULT_SYSTEM_PROMPT = `You are a context pruning assistant. Given a conversation history and a token budget, decide which message ranges are worth keeping verbatim and which should be collapsed into summaries.
|
|
5085
|
-
|
|
5086
|
-
Output a JSON object with this structure:
|
|
5087
|
-
{
|
|
5088
|
-
"kept": [{"from": 0, "to": 5, "importance": "critical"}],
|
|
5089
|
-
"collapsed": [{"from": 6, "to": 20, "summary": "optional summary"}],
|
|
5090
|
-
"reasoning": "brief explanation of decisions"
|
|
5109
|
+
const t2 = state.thinking[state.currentThinkingIndex];
|
|
5110
|
+
if (t2) t2.signature = signature;
|
|
5091
5111
|
}
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
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;
|
|
5115
5168
|
}
|
|
5116
5169
|
}
|
|
5117
|
-
}
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
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);
|
|
5135
5188
|
}
|
|
5189
|
+
} catch {
|
|
5136
5190
|
}
|
|
5137
|
-
const line = `[${i}][${role}]: ${text}`;
|
|
5138
|
-
if (used + line.length > maxChars) break;
|
|
5139
|
-
lines.push(line);
|
|
5140
|
-
used += line.length;
|
|
5141
5191
|
}
|
|
5142
|
-
return
|
|
5192
|
+
return buildResponse(state);
|
|
5143
5193
|
}
|
|
5144
|
-
var LLMSelector = class {
|
|
5145
|
-
provider;
|
|
5146
|
-
model;
|
|
5147
|
-
maxContextTokens;
|
|
5148
|
-
systemPrompt;
|
|
5149
|
-
constructor(opts) {
|
|
5150
|
-
this.provider = opts.provider;
|
|
5151
|
-
this.model = opts.model ?? "unknown";
|
|
5152
|
-
this.maxContextTokens = opts.maxContextTokens ?? 4e4;
|
|
5153
|
-
this.systemPrompt = opts.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
5154
|
-
}
|
|
5155
|
-
async select(messages, maxToKeep) {
|
|
5156
|
-
const effectiveBudget = Math.min(maxToKeep, this.maxContextTokens);
|
|
5157
|
-
const historyText = formatMessages(messages);
|
|
5158
|
-
const totalTokens = estimateTokens(messages);
|
|
5159
|
-
const systemText = `${this.systemPrompt}
|
|
5160
5194
|
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
const
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
};
|
|
5172
|
-
let raw;
|
|
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
|
+
});
|
|
5173
5206
|
try {
|
|
5174
|
-
const
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
return
|
|
5180
|
-
}
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
break;
|
|
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;
|
|
5199
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++;
|
|
5200
5266
|
}
|
|
5201
|
-
if (startIdx > 0) {
|
|
5202
|
-
toCollapse.push({ from: 0, to: startIdx - 1 });
|
|
5203
|
-
}
|
|
5204
|
-
toKeep.push({ from: startIdx, to: messages.length - 1, importance: "high" });
|
|
5205
|
-
return {
|
|
5206
|
-
kept: toKeep,
|
|
5207
|
-
collapsed: toCollapse,
|
|
5208
|
-
reasoning: `Fallback: kept last ${messages.length - startIdx} messages within ${budget} token budget`
|
|
5209
|
-
};
|
|
5210
5267
|
}
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
this.maxContextTokens
|
|
5218
|
-
);
|
|
5219
|
-
}
|
|
5220
|
-
let parsed;
|
|
5221
|
-
try {
|
|
5222
|
-
parsed = JSON.parse(raw.slice(jsonStart, jsonEnd + 1));
|
|
5223
|
-
} catch {
|
|
5224
|
-
return this.fallbackSelect(
|
|
5225
|
-
Array.from({ length: messageCount }, () => ({ role: "user", content: "" })),
|
|
5226
|
-
this.maxContextTokens
|
|
5227
|
-
);
|
|
5228
|
-
}
|
|
5229
|
-
const obj = parsed;
|
|
5230
|
-
const kept = obj.kept ?? [];
|
|
5231
|
-
const collapsed = obj.collapsed ?? [];
|
|
5232
|
-
return {
|
|
5233
|
-
kept: kept.map((k) => ({
|
|
5234
|
-
from: k.from,
|
|
5235
|
-
to: k.to,
|
|
5236
|
-
importance: k.importance ?? "medium"
|
|
5237
|
-
})),
|
|
5238
|
-
collapsed: collapsed.map((c) => ({ from: c.from, to: c.to, summary: c.summary })),
|
|
5239
|
-
reasoning: typeof obj.reasoning === "string" ? obj.reasoning : ""
|
|
5240
|
-
};
|
|
5268
|
+
}
|
|
5269
|
+
|
|
5270
|
+
// src/execution/provider-runner-impl.ts
|
|
5271
|
+
var DefaultProviderRunner = class {
|
|
5272
|
+
async run(opts) {
|
|
5273
|
+
return runProviderWithRetry(opts);
|
|
5241
5274
|
}
|
|
5242
5275
|
};
|
|
5243
5276
|
|
|
5244
|
-
// src/execution/
|
|
5245
|
-
var
|
|
5277
|
+
// src/execution/intelligent-compactor.ts
|
|
5278
|
+
var IntelligentCompactor = class {
|
|
5246
5279
|
provider;
|
|
5247
|
-
selector;
|
|
5248
5280
|
warnThreshold;
|
|
5249
5281
|
softThreshold;
|
|
5250
5282
|
hardThreshold;
|
|
5251
5283
|
maxContext;
|
|
5252
5284
|
preserveK;
|
|
5253
5285
|
eliseThreshold;
|
|
5254
|
-
summarizerModel;
|
|
5255
5286
|
summarizerPrompt;
|
|
5287
|
+
summarizerModel;
|
|
5256
5288
|
constructor(opts) {
|
|
5257
5289
|
this.provider = opts.provider;
|
|
5258
|
-
this.selector = opts.selector ?? new LLMSelector({ provider: opts.provider, model: opts.selectorModel });
|
|
5259
5290
|
this.warnThreshold = opts.warnThreshold ?? 0.6;
|
|
5260
5291
|
this.softThreshold = opts.softThreshold ?? 0.75;
|
|
5261
5292
|
this.hardThreshold = opts.hardThreshold ?? 0.9;
|
|
5262
5293
|
this.maxContext = opts.maxContext ?? 128e3;
|
|
5263
5294
|
this.preserveK = opts.preserveK ?? 4;
|
|
5264
5295
|
this.eliseThreshold = opts.eliseThreshold ?? 500;
|
|
5265
|
-
this.
|
|
5266
|
-
this.
|
|
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;
|
|
5267
5298
|
}
|
|
5268
5299
|
async compact(ctx, opts = {}) {
|
|
5269
5300
|
const beforeTokens = this.estimateTokens(ctx.messages);
|
|
5270
5301
|
const reductions = [];
|
|
5271
5302
|
const load = beforeTokens / this.maxContext;
|
|
5272
|
-
const
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5276
|
-
const
|
|
5277
|
-
|
|
5278
|
-
}
|
|
5279
|
-
|
|
5280
|
-
|
|
5281
|
-
const afterPhase1 = this.estimateTokens(ctx.messages);
|
|
5282
|
-
const targetBudget = this.computeTargetBudget(load);
|
|
5283
|
-
if (afterPhase1 > targetBudget) {
|
|
5284
|
-
const savedSelective = await this.runSelector(ctx, targetBudget);
|
|
5285
|
-
if (savedSelective > 0) reductions.push({ phase: "selective", saved: savedSelective });
|
|
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 });
|
|
5286
5312
|
}
|
|
5287
5313
|
const afterTokens = this.estimateTokens(ctx.messages);
|
|
5288
5314
|
return { before: beforeTokens, after: afterTokens, reductions };
|
|
5289
5315
|
}
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
|
|
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;
|
|
5297
5325
|
try {
|
|
5298
|
-
|
|
5326
|
+
summaryText = await this.callSummarizer(toSummarize, ctx);
|
|
5299
5327
|
} catch {
|
|
5300
|
-
|
|
5301
|
-
}
|
|
5302
|
-
await this.executePlan(ctx, result);
|
|
5303
|
-
const after = this.estimateTokens(ctx.messages);
|
|
5304
|
-
return Math.max(0, before - after);
|
|
5305
|
-
}
|
|
5306
|
-
/**
|
|
5307
|
-
* Execute a SelectorResult plan: collapse/remove ranges and
|
|
5308
|
-
* insert summaries where the selector provided them.
|
|
5309
|
-
*/
|
|
5310
|
-
async executePlan(ctx, plan) {
|
|
5311
|
-
if (ctx.messages.length === 0) return;
|
|
5312
|
-
const messages = [...ctx.messages];
|
|
5313
|
-
const sortedCollapsed = [...plan.collapsed].sort((a, b) => b.from - a.from);
|
|
5314
|
-
for (const range of sortedCollapsed) {
|
|
5315
|
-
if (range.from < 0 || range.to >= messages.length || range.from > range.to) continue;
|
|
5316
|
-
let summary = range.summary;
|
|
5317
|
-
if (!summary) {
|
|
5318
|
-
const toSummarize = messages.slice(range.from, range.to + 1);
|
|
5319
|
-
summary = await this.summarizeRange(toSummarize, ctx);
|
|
5320
|
-
}
|
|
5321
|
-
const summaryMsg = {
|
|
5322
|
-
role: "system",
|
|
5323
|
-
content: `[prior_turns_${range.from}-${range.to}: ${summary}]`
|
|
5324
|
-
};
|
|
5325
|
-
messages.splice(range.from, range.to - range.from + 1, summaryMsg);
|
|
5328
|
+
summaryText = `[${toSummarize.length} earlier turns omitted \u2014 key decisions and file states preserved in context]`;
|
|
5326
5329
|
}
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
const systemText = `${this.summarizerPrompt}
|
|
5331
|
-
|
|
5332
|
-
Summarize the following message range:`;
|
|
5333
|
-
const body = messages.map((m, i) => `[${i}] ${m.role}: ${this.messagePreview(m)}`).join("\n");
|
|
5334
|
-
const req = {
|
|
5335
|
-
model: this.summarizerModel,
|
|
5336
|
-
system: [{ type: "text", text: systemText }],
|
|
5337
|
-
messages: [{ role: "user", content: body }],
|
|
5338
|
-
maxTokens: 512
|
|
5330
|
+
const summaryMsg = {
|
|
5331
|
+
role: "system",
|
|
5332
|
+
content: `[prior_turns_summary: ${summaryText}]`
|
|
5339
5333
|
};
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
return res.content.filter(isTextBlock).map((b) => b.text).join("\n").trim() || "(empty)";
|
|
5345
|
-
} catch {
|
|
5346
|
-
return `[${messages.length} earlier turns omitted]`;
|
|
5347
|
-
}
|
|
5348
|
-
}
|
|
5349
|
-
messagePreview(m) {
|
|
5350
|
-
if (typeof m.content === "string") return m.content.slice(0, 300);
|
|
5351
|
-
return m.content.filter(isTextBlock).map((b) => b.text).join(" ").slice(0, 300);
|
|
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);
|
|
5352
5338
|
}
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
* until we hit targetBudget.
|
|
5356
|
-
*/
|
|
5357
|
-
aggressiveRecencyTrim(ctx) {
|
|
5358
|
-
const messages = ctx.messages;
|
|
5359
|
-
const preserveIdx = Math.max(0, messages.length - this.preserveK * 2);
|
|
5360
|
-
if (preserveIdx <= 0) return 0;
|
|
5361
|
-
let boundary = preserveIdx;
|
|
5362
|
-
for (let i = preserveIdx; i < messages.length && i < preserveIdx + 6; i++) {
|
|
5339
|
+
findSafeBoundary(messages, from, to) {
|
|
5340
|
+
for (let i = to; i >= from; i--) {
|
|
5363
5341
|
const m = messages[i];
|
|
5342
|
+
if (!m) continue;
|
|
5364
5343
|
if (m.role === "user" && this.hasTextContent(m)) {
|
|
5365
|
-
|
|
5366
|
-
break;
|
|
5344
|
+
return this.findExchangeStart(messages, i);
|
|
5367
5345
|
}
|
|
5368
5346
|
}
|
|
5369
|
-
|
|
5370
|
-
const removedTokens = this.estimateTokens(removed);
|
|
5371
|
-
const summaryMsg = {
|
|
5372
|
-
role: "system",
|
|
5373
|
-
content: `[${removed.length} earlier turns trimmed \u2014 see session log for details]`
|
|
5374
|
-
};
|
|
5375
|
-
const tail = messages.slice(boundary);
|
|
5376
|
-
ctx.state.replaceMessages([summaryMsg, ...tail]);
|
|
5377
|
-
return Math.max(0, removedTokens - this.estimateTokens([summaryMsg]));
|
|
5347
|
+
return -1;
|
|
5378
5348
|
}
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
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
|
+
}
|
|
5385
5361
|
}
|
|
5386
|
-
return
|
|
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: [],
|
|
5374
|
+
maxTokens: 1024
|
|
5375
|
+
};
|
|
5376
|
+
const ac = ctx.signal ? void 0 : new AbortController();
|
|
5377
|
+
const signal = ctx.signal ?? ac.signal;
|
|
5378
|
+
const res = await this.provider.complete(req, { signal });
|
|
5379
|
+
const textBlocks = res.content.filter(isTextBlock);
|
|
5380
|
+
return textBlocks.map((b) => b.text).join("\n").trim() || "(empty summary)";
|
|
5381
|
+
}
|
|
5382
|
+
messagesToText(messages) {
|
|
5383
|
+
const lines = [];
|
|
5384
|
+
for (const m of messages) {
|
|
5385
|
+
const role = m.role.padEnd(10, " ");
|
|
5386
|
+
if (typeof m.content === "string") {
|
|
5387
|
+
lines.push(`[${role}]: ${m.content.slice(0, 500)}`);
|
|
5388
|
+
} else if (Array.isArray(m.content)) {
|
|
5389
|
+
const textParts = m.content.filter(isTextBlock).map((b) => b.text);
|
|
5390
|
+
if (textParts.length > 0) {
|
|
5391
|
+
lines.push(`[${role}]: ${textParts.join(" ").slice(0, 500)}`);
|
|
5392
|
+
}
|
|
5393
|
+
}
|
|
5394
|
+
}
|
|
5395
|
+
return [{ type: "text", text: lines.join("\n") }];
|
|
5396
|
+
}
|
|
5397
|
+
lightweightCompact(ctx) {
|
|
5398
|
+
return this.eliseOldToolResults(ctx);
|
|
5387
5399
|
}
|
|
5388
5400
|
eliseOldToolResults(ctx) {
|
|
5389
5401
|
const messages = ctx.messages;
|
|
@@ -5412,8 +5424,7 @@ Summarize the following message range:`;
|
|
|
5412
5424
|
}
|
|
5413
5425
|
const newContent = msg.content.map((b) => {
|
|
5414
5426
|
if (b.type !== "tool_result") return b;
|
|
5415
|
-
const
|
|
5416
|
-
const tokens = this.roughTokenEstimate(text);
|
|
5427
|
+
const tokens = estimateToolResultTokens(b.content);
|
|
5417
5428
|
if (tokens < this.eliseThreshold) return b;
|
|
5418
5429
|
saved += tokens;
|
|
5419
5430
|
return {
|
|
@@ -5423,7 +5434,7 @@ Summarize the following message range:`;
|
|
|
5423
5434
|
is_error: b.is_error
|
|
5424
5435
|
};
|
|
5425
5436
|
});
|
|
5426
|
-
if (newContent.every((b, idx) => b === msg.content[idx])) {
|
|
5437
|
+
if (newContent.length === msg.content.length && newContent.every((b, idx) => b === msg.content[idx])) {
|
|
5427
5438
|
nextMessages[i] = msg;
|
|
5428
5439
|
} else {
|
|
5429
5440
|
nextMessages[i] = { ...msg, content: newContent };
|
|
@@ -5441,610 +5452,984 @@ Summarize the following message range:`;
|
|
|
5441
5452
|
let total = 0;
|
|
5442
5453
|
for (const m of messages) {
|
|
5443
5454
|
if (typeof m.content === "string") {
|
|
5444
|
-
total +=
|
|
5455
|
+
total += estimateTextTokens(m.content);
|
|
5445
5456
|
} else {
|
|
5446
5457
|
for (const b of m.content) {
|
|
5447
|
-
if (b.type === "text") total +=
|
|
5448
|
-
else if (b.type === "tool_use") total +=
|
|
5449
|
-
else if (b.type === "tool_result")
|
|
5450
|
-
total += this.roughTokenEstimate(
|
|
5451
|
-
typeof b.content === "string" ? b.content : JSON.stringify(b.content)
|
|
5452
|
-
);
|
|
5453
|
-
}
|
|
5458
|
+
if (b.type === "text") total += estimateTextTokens(b.text);
|
|
5459
|
+
else if (b.type === "tool_use") total += estimateToolInputTokens(b.input);
|
|
5460
|
+
else if (b.type === "tool_result") total += estimateToolResultTokens(b.content);
|
|
5454
5461
|
}
|
|
5455
5462
|
}
|
|
5456
5463
|
}
|
|
5457
5464
|
return total;
|
|
5458
5465
|
}
|
|
5459
|
-
roughTokenEstimate(text) {
|
|
5460
|
-
return Math.max(1, Math.ceil(text.length / 4));
|
|
5461
|
-
}
|
|
5462
5466
|
};
|
|
5463
5467
|
|
|
5464
|
-
// src/
|
|
5465
|
-
var
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
|
|
5472
|
-
|
|
5473
|
-
|
|
5474
|
-
|
|
5475
|
-
|
|
5476
|
-
|
|
5477
|
-
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
|
|
5485
|
-
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
this.events = opts.events;
|
|
5497
|
-
this.failureMode = opts.failureMode ?? "throw_on_hard";
|
|
5498
|
-
}
|
|
5499
|
-
handler() {
|
|
5500
|
-
return async (ctx, next) => {
|
|
5501
|
-
const tokens = this.estimator(ctx);
|
|
5502
|
-
const load = tokens / this.maxContext;
|
|
5503
|
-
if (load >= this.hardThreshold) {
|
|
5504
|
-
await this.compact(ctx, true, { level: "hard", tokens, load });
|
|
5505
|
-
} else if (load >= this.softThreshold) {
|
|
5506
|
-
await this.compact(ctx, this.aggressiveOn !== "hard", { level: "soft", tokens, load });
|
|
5507
|
-
} else if (load >= this.warnThreshold) {
|
|
5508
|
-
await this.compact(ctx, false, { level: "warn", tokens, load });
|
|
5468
|
+
// src/models/llm-selector.ts
|
|
5469
|
+
var DEFAULT_SYSTEM_PROMPT = `You are a context pruning assistant. Given a conversation history and a token budget, decide which message ranges are worth keeping verbatim and which should be collapsed into summaries.
|
|
5470
|
+
|
|
5471
|
+
Output a JSON object with this structure:
|
|
5472
|
+
{
|
|
5473
|
+
"kept": [{"from": 0, "to": 5, "importance": "critical"}],
|
|
5474
|
+
"collapsed": [{"from": 6, "to": 20, "summary": "optional summary"}],
|
|
5475
|
+
"reasoning": "brief explanation of decisions"
|
|
5476
|
+
}
|
|
5477
|
+
|
|
5478
|
+
Importance tiers:
|
|
5479
|
+
- "critical": decisions, file edits, tool results that affect state, final answers
|
|
5480
|
+
- "high": substantive tool use, complex reasoning, non-obvious observations
|
|
5481
|
+
- "medium": routine exchanges, confirmations, straightforward Q&A
|
|
5482
|
+
|
|
5483
|
+
Rules:
|
|
5484
|
+
- Always keep the most recent K pairs (preserve recency)
|
|
5485
|
+
- Never collapse the final 2 user/assistant pairs (working memory)
|
|
5486
|
+
- Preserve tool results that modified files or had external effects
|
|
5487
|
+
- Collapse old, low-information exchanges (greetings, acknowledgements, etc.)
|
|
5488
|
+
- If unsure, keep rather than collapse (errors are more costly than waste)
|
|
5489
|
+
|
|
5490
|
+
Return ONLY the JSON object, no markdown, no explanation outside the JSON.`;
|
|
5491
|
+
function estimateTokens(messages) {
|
|
5492
|
+
let total = 0;
|
|
5493
|
+
for (const m of messages) {
|
|
5494
|
+
if (typeof m.content === "string") {
|
|
5495
|
+
total += Math.ceil(m.content.length / 4);
|
|
5496
|
+
} else if (Array.isArray(m.content)) {
|
|
5497
|
+
for (const b of m.content) {
|
|
5498
|
+
if (b.type === "text") total += Math.ceil(b.text.length / 4);
|
|
5499
|
+
else total += Math.ceil(JSON.stringify(b).length / 4);
|
|
5509
5500
|
}
|
|
5510
|
-
|
|
5511
|
-
};
|
|
5501
|
+
}
|
|
5512
5502
|
}
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
|
|
5519
|
-
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
|
|
5523
|
-
|
|
5524
|
-
|
|
5525
|
-
|
|
5526
|
-
|
|
5527
|
-
|
|
5528
|
-
if (
|
|
5529
|
-
|
|
5530
|
-
message: `Auto-compaction failed at ${pressure.level} threshold`,
|
|
5531
|
-
code: "AGENT_CONTEXT_OVERFLOW",
|
|
5532
|
-
recoverable: true,
|
|
5533
|
-
context: {
|
|
5534
|
-
level: pressure.level,
|
|
5535
|
-
tokens: pressure.tokens,
|
|
5536
|
-
maxContext: this.maxContext
|
|
5537
|
-
},
|
|
5538
|
-
cause: err
|
|
5539
|
-
});
|
|
5503
|
+
return total;
|
|
5504
|
+
}
|
|
5505
|
+
function formatMessages(messages, maxChars = 8e3) {
|
|
5506
|
+
const lines = [];
|
|
5507
|
+
let used = 0;
|
|
5508
|
+
for (let i = 0; i < messages.length; i++) {
|
|
5509
|
+
const m = messages[i];
|
|
5510
|
+
const role = m.role.padEnd(10, " ");
|
|
5511
|
+
let text;
|
|
5512
|
+
if (typeof m.content === "string") {
|
|
5513
|
+
text = m.content.slice(0, 500);
|
|
5514
|
+
} else {
|
|
5515
|
+
const content = m.content;
|
|
5516
|
+
text = content.filter(isTextBlock).map((b) => b.text).join(" ");
|
|
5517
|
+
const toolUses = content.filter((b) => b.type === "tool_use");
|
|
5518
|
+
if (toolUses.length > 0) {
|
|
5519
|
+
text += ` [tools: ${toolUses.map((b) => b.name).join(", ")}]`;
|
|
5540
5520
|
}
|
|
5541
5521
|
}
|
|
5522
|
+
const line = `[${i}][${role}]: ${text}`;
|
|
5523
|
+
if (used + line.length > maxChars) break;
|
|
5524
|
+
lines.push(line);
|
|
5525
|
+
used += line.length;
|
|
5542
5526
|
}
|
|
5543
|
-
|
|
5527
|
+
return lines.join("\n");
|
|
5528
|
+
}
|
|
5529
|
+
var LLMSelector = class {
|
|
5530
|
+
provider;
|
|
5531
|
+
model;
|
|
5532
|
+
maxContextTokens;
|
|
5533
|
+
systemPrompt;
|
|
5534
|
+
constructor(opts) {
|
|
5535
|
+
this.provider = opts.provider;
|
|
5536
|
+
this.model = opts.model ?? "unknown";
|
|
5537
|
+
this.maxContextTokens = opts.maxContextTokens ?? 4e4;
|
|
5538
|
+
this.systemPrompt = opts.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
|
|
5539
|
+
}
|
|
5540
|
+
async select(messages, maxToKeep) {
|
|
5541
|
+
const effectiveBudget = Math.min(maxToKeep, this.maxContextTokens);
|
|
5542
|
+
const historyText = formatMessages(messages);
|
|
5543
|
+
const totalTokens = estimateTokens(messages);
|
|
5544
|
+
const systemText = `${this.systemPrompt}
|
|
5544
5545
|
|
|
5545
|
-
|
|
5546
|
-
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
|
|
5546
|
+
Conversation (${messages.length} messages, ~${totalTokens} tokens, budget: ${effectiveBudget}):
|
|
5547
|
+
`;
|
|
5548
|
+
const budgetInstruction = totalTokens > effectiveBudget ? `
|
|
5549
|
+
|
|
5550
|
+
IMPORTANT: Total conversation (${totalTokens} tokens) exceeds budget (${effectiveBudget}). You MUST collapse enough to fit. Prefer collapsing older/lower-importance ranges.` : "";
|
|
5551
|
+
const req = {
|
|
5552
|
+
model: this.model,
|
|
5553
|
+
system: [{ type: "text", text: systemText + budgetInstruction }],
|
|
5554
|
+
messages: [{ role: "user", content: historyText }],
|
|
5555
|
+
maxTokens: 1024
|
|
5556
|
+
};
|
|
5557
|
+
let raw;
|
|
5558
|
+
try {
|
|
5559
|
+
const ac = new AbortController();
|
|
5560
|
+
const res = await this.provider.complete(req, { signal: ac.signal });
|
|
5561
|
+
const textBlocks = res.content.filter(isTextBlock);
|
|
5562
|
+
raw = textBlocks.map((b) => b.text).join("\n").trim();
|
|
5563
|
+
} catch (err) {
|
|
5564
|
+
return this.fallbackSelect(messages, effectiveBudget);
|
|
5565
|
+
}
|
|
5566
|
+
return this.parseSelectorOutput(raw, messages.length);
|
|
5550
5567
|
}
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
|
|
5558
|
-
|
|
5559
|
-
|
|
5560
|
-
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
return {
|
|
5567
|
-
done: true,
|
|
5568
|
-
reason: `max tool calls (${this.condition.maxToolCalls}) reached`,
|
|
5569
|
-
...state
|
|
5570
|
-
};
|
|
5571
|
-
}
|
|
5572
|
-
break;
|
|
5573
|
-
case "output_match":
|
|
5574
|
-
if (this.compiledRegex && state.lastOutput && this.compiledRegex.test(state.lastOutput)) {
|
|
5575
|
-
return {
|
|
5576
|
-
done: true,
|
|
5577
|
-
reason: `output matched pattern "${this.condition.pattern}"`,
|
|
5578
|
-
...state
|
|
5579
|
-
};
|
|
5580
|
-
}
|
|
5568
|
+
fallbackSelect(messages, budget) {
|
|
5569
|
+
const toKeep = [];
|
|
5570
|
+
const toCollapse = [];
|
|
5571
|
+
let tokenCount = 0;
|
|
5572
|
+
let startIdx = 0;
|
|
5573
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
5574
|
+
const m = messages[i];
|
|
5575
|
+
const cost = typeof m.content === "string" ? Math.ceil(m.content.length / 4) : m.content.reduce(
|
|
5576
|
+
(acc, b) => acc + (b.type === "text" ? Math.ceil(b.text.length / 4) : Math.ceil(JSON.stringify(b).length / 4)),
|
|
5577
|
+
0
|
|
5578
|
+
);
|
|
5579
|
+
if (tokenCount + cost <= budget) {
|
|
5580
|
+
tokenCount += cost;
|
|
5581
|
+
} else {
|
|
5582
|
+
startIdx = i + 1;
|
|
5581
5583
|
break;
|
|
5584
|
+
}
|
|
5582
5585
|
}
|
|
5583
|
-
|
|
5586
|
+
if (startIdx > 0) {
|
|
5587
|
+
toCollapse.push({ from: 0, to: startIdx - 1 });
|
|
5588
|
+
}
|
|
5589
|
+
toKeep.push({ from: startIdx, to: messages.length - 1, importance: "high" });
|
|
5590
|
+
return {
|
|
5591
|
+
kept: toKeep,
|
|
5592
|
+
collapsed: toCollapse,
|
|
5593
|
+
reasoning: `Fallback: kept last ${messages.length - startIdx} messages within ${budget} token budget`
|
|
5594
|
+
};
|
|
5595
|
+
}
|
|
5596
|
+
parseSelectorOutput(raw, messageCount) {
|
|
5597
|
+
const jsonStart = raw.indexOf("{");
|
|
5598
|
+
const jsonEnd = raw.lastIndexOf("}");
|
|
5599
|
+
if (jsonStart === -1 || jsonEnd === -1) {
|
|
5600
|
+
return this.fallbackSelect(
|
|
5601
|
+
Array.from({ length: messageCount }, () => ({ role: "user", content: "" })),
|
|
5602
|
+
this.maxContextTokens
|
|
5603
|
+
);
|
|
5604
|
+
}
|
|
5605
|
+
let parsed;
|
|
5606
|
+
try {
|
|
5607
|
+
parsed = JSON.parse(raw.slice(jsonStart, jsonEnd + 1));
|
|
5608
|
+
} catch {
|
|
5609
|
+
return this.fallbackSelect(
|
|
5610
|
+
Array.from({ length: messageCount }, () => ({ role: "user", content: "" })),
|
|
5611
|
+
this.maxContextTokens
|
|
5612
|
+
);
|
|
5613
|
+
}
|
|
5614
|
+
const obj = parsed;
|
|
5615
|
+
const kept = obj.kept ?? [];
|
|
5616
|
+
const collapsed = obj.collapsed ?? [];
|
|
5617
|
+
return {
|
|
5618
|
+
kept: kept.map((k) => ({
|
|
5619
|
+
from: k.from,
|
|
5620
|
+
to: k.to,
|
|
5621
|
+
importance: k.importance ?? "medium"
|
|
5622
|
+
})),
|
|
5623
|
+
collapsed: collapsed.map((c) => ({ from: c.from, to: c.to, summary: c.summary })),
|
|
5624
|
+
reasoning: typeof obj.reasoning === "string" ? obj.reasoning : ""
|
|
5625
|
+
};
|
|
5584
5626
|
}
|
|
5585
5627
|
};
|
|
5586
|
-
|
|
5628
|
+
|
|
5629
|
+
// src/execution/selective-compactor.ts
|
|
5630
|
+
var SelectiveCompactor = class {
|
|
5631
|
+
provider;
|
|
5632
|
+
selector;
|
|
5633
|
+
warnThreshold;
|
|
5634
|
+
softThreshold;
|
|
5635
|
+
hardThreshold;
|
|
5636
|
+
maxContext;
|
|
5637
|
+
preserveK;
|
|
5638
|
+
eliseThreshold;
|
|
5639
|
+
summarizerModel;
|
|
5640
|
+
summarizerPrompt;
|
|
5587
5641
|
constructor(opts) {
|
|
5588
|
-
this.
|
|
5589
|
-
this.
|
|
5642
|
+
this.provider = opts.provider;
|
|
5643
|
+
this.selector = opts.selector ?? new LLMSelector({ provider: opts.provider, model: opts.selectorModel });
|
|
5644
|
+
this.warnThreshold = opts.warnThreshold ?? 0.6;
|
|
5645
|
+
this.softThreshold = opts.softThreshold ?? 0.75;
|
|
5646
|
+
this.hardThreshold = opts.hardThreshold ?? 0.9;
|
|
5647
|
+
this.maxContext = opts.maxContext ?? 128e3;
|
|
5648
|
+
this.preserveK = opts.preserveK ?? 4;
|
|
5649
|
+
this.eliseThreshold = opts.eliseThreshold ?? 500;
|
|
5650
|
+
this.summarizerModel = opts.summarizerModel ?? opts.selectorModel ?? "unknown";
|
|
5651
|
+
this.summarizerPrompt = opts.summarizerPrompt ?? "You are a context summarizer. Given a list of messages, produce a concise summary that preserves all factual information, decisions, file changes, and state changes. Do not add commentary or opinions.";
|
|
5590
5652
|
}
|
|
5591
|
-
opts
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
this.
|
|
5600
|
-
|
|
5653
|
+
async compact(ctx, opts = {}) {
|
|
5654
|
+
const beforeTokens = this.estimateTokens(ctx.messages);
|
|
5655
|
+
const reductions = [];
|
|
5656
|
+
const load = beforeTokens / this.maxContext;
|
|
5657
|
+
const shouldCompact = load >= this.warnThreshold || opts.aggressive;
|
|
5658
|
+
if (!shouldCompact) {
|
|
5659
|
+
const saved = this.eliseOldToolResults(ctx);
|
|
5660
|
+
if (saved > 0) reductions.push({ phase: "elision", saved });
|
|
5661
|
+
const afterTokens2 = this.estimateTokens(ctx.messages);
|
|
5662
|
+
return { before: beforeTokens, after: afterTokens2, reductions };
|
|
5663
|
+
}
|
|
5664
|
+
const savedElision = this.eliseOldToolResults(ctx);
|
|
5665
|
+
if (savedElision > 0) reductions.push({ phase: "elision", saved: savedElision });
|
|
5666
|
+
const afterPhase1 = this.estimateTokens(ctx.messages);
|
|
5667
|
+
const targetBudget = this.computeTargetBudget(load);
|
|
5668
|
+
if (afterPhase1 > targetBudget) {
|
|
5669
|
+
const savedSelective = await this.runSelector(ctx, targetBudget);
|
|
5670
|
+
if (savedSelective > 0) reductions.push({ phase: "selective", saved: savedSelective });
|
|
5671
|
+
}
|
|
5672
|
+
const afterTokens = this.estimateTokens(ctx.messages);
|
|
5673
|
+
return { before: beforeTokens, after: afterTokens, reductions };
|
|
5674
|
+
}
|
|
5675
|
+
/**
|
|
5676
|
+
* Run the LLM selector to decide what to keep vs collapse.
|
|
5677
|
+
* Returns the token savings achieved.
|
|
5678
|
+
*/
|
|
5679
|
+
async runSelector(ctx, targetBudget) {
|
|
5680
|
+
const before = this.estimateTokens(ctx.messages);
|
|
5681
|
+
let result;
|
|
5601
5682
|
try {
|
|
5602
|
-
|
|
5603
|
-
}
|
|
5604
|
-
|
|
5683
|
+
result = await this.selector.select(ctx.messages, targetBudget);
|
|
5684
|
+
} catch {
|
|
5685
|
+
return this.aggressiveRecencyTrim(ctx);
|
|
5605
5686
|
}
|
|
5687
|
+
await this.executePlan(ctx, result);
|
|
5688
|
+
const after = this.estimateTokens(ctx.messages);
|
|
5689
|
+
return Math.max(0, before - after);
|
|
5606
5690
|
}
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
|
|
5611
|
-
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
|
|
5616
|
-
|
|
5617
|
-
|
|
5618
|
-
|
|
5619
|
-
|
|
5620
|
-
|
|
5621
|
-
this.opts.onDone?.(result);
|
|
5622
|
-
return result;
|
|
5623
|
-
}
|
|
5624
|
-
this.opts.onIteration?.({ iteration: this.iterations, toolCalls: this.toolCalls });
|
|
5625
|
-
const ctrl = new AbortController();
|
|
5626
|
-
const timeout = setTimeout(() => ctrl.abort(), this.opts.iterationTimeoutMs ?? 3e4);
|
|
5627
|
-
try {
|
|
5628
|
-
const result = await this.opts.agent.run("", {
|
|
5629
|
-
signal: ctrl.signal,
|
|
5630
|
-
maxIterations: 1,
|
|
5631
|
-
executionStrategy: "sequential"
|
|
5632
|
-
});
|
|
5633
|
-
this.iterations++;
|
|
5634
|
-
if (result.status === "done") {
|
|
5635
|
-
this.lastOutput = result.finalText;
|
|
5636
|
-
}
|
|
5637
|
-
if (result.status === "failed" || result.status === "aborted") {
|
|
5638
|
-
const failedResult = {
|
|
5639
|
-
status: result.status,
|
|
5640
|
-
error: result.error,
|
|
5641
|
-
iterations: this.iterations,
|
|
5642
|
-
toolCalls: this.toolCalls
|
|
5643
|
-
};
|
|
5644
|
-
this.opts.onDone?.(failedResult);
|
|
5645
|
-
return failedResult;
|
|
5646
|
-
}
|
|
5647
|
-
} catch (e) {
|
|
5648
|
-
const isAbort = e instanceof DOMException && e.name === "AbortError" || e instanceof Error && e.name === "AbortError" || e instanceof Error && e.message.includes("iteration timeout");
|
|
5649
|
-
if (isAbort) {
|
|
5650
|
-
const timeoutResult = {
|
|
5651
|
-
status: "failed",
|
|
5652
|
-
error: toWrongStackError(e),
|
|
5653
|
-
iterations: this.iterations,
|
|
5654
|
-
toolCalls: this.toolCalls,
|
|
5655
|
-
reason: "iteration timeout"
|
|
5656
|
-
};
|
|
5657
|
-
this.opts.onDone?.(timeoutResult);
|
|
5658
|
-
return timeoutResult;
|
|
5659
|
-
}
|
|
5660
|
-
this.stopped = true;
|
|
5661
|
-
const failedResult = {
|
|
5662
|
-
status: "failed",
|
|
5663
|
-
error: toWrongStackError(e),
|
|
5664
|
-
iterations: this.iterations,
|
|
5665
|
-
toolCalls: this.toolCalls,
|
|
5666
|
-
reason: e instanceof Error ? e.message : String(e)
|
|
5667
|
-
};
|
|
5668
|
-
this.opts.onDone?.(failedResult);
|
|
5669
|
-
return failedResult;
|
|
5670
|
-
} finally {
|
|
5671
|
-
clearTimeout(timeout);
|
|
5691
|
+
/**
|
|
5692
|
+
* Execute a SelectorResult plan: collapse/remove ranges and
|
|
5693
|
+
* insert summaries where the selector provided them.
|
|
5694
|
+
*/
|
|
5695
|
+
async executePlan(ctx, plan) {
|
|
5696
|
+
if (ctx.messages.length === 0) return;
|
|
5697
|
+
const messages = [...ctx.messages];
|
|
5698
|
+
const sortedCollapsed = [...plan.collapsed].sort((a, b) => b.from - a.from);
|
|
5699
|
+
for (const range of sortedCollapsed) {
|
|
5700
|
+
if (range.from < 0 || range.to >= messages.length || range.from > range.to) continue;
|
|
5701
|
+
let summary = range.summary;
|
|
5702
|
+
if (!summary) {
|
|
5703
|
+
const toSummarize = messages.slice(range.from, range.to + 1);
|
|
5704
|
+
summary = await this.summarizeRange(toSummarize, ctx);
|
|
5672
5705
|
}
|
|
5706
|
+
const summaryMsg = {
|
|
5707
|
+
role: "system",
|
|
5708
|
+
content: `[prior_turns_${range.from}-${range.to}: ${summary}]`
|
|
5709
|
+
};
|
|
5710
|
+
messages.splice(range.from, range.to - range.from + 1, summaryMsg);
|
|
5673
5711
|
}
|
|
5674
|
-
|
|
5675
|
-
status: "aborted",
|
|
5676
|
-
iterations: this.iterations,
|
|
5677
|
-
toolCalls: this.toolCalls,
|
|
5678
|
-
reason: "stopped externally"
|
|
5679
|
-
};
|
|
5680
|
-
}
|
|
5681
|
-
stop() {
|
|
5682
|
-
this.stopped = true;
|
|
5712
|
+
ctx.state.replaceMessages(messages);
|
|
5683
5713
|
}
|
|
5684
|
-
|
|
5685
|
-
|
|
5686
|
-
// src/coordination/director-prompts.ts
|
|
5687
|
-
var DEFAULT_DIRECTOR_PREAMBLE = `You are the Director of a multi-agent fleet. You orchestrate worker
|
|
5688
|
-
subagents by spawning them, assigning tasks, awaiting completions, and
|
|
5689
|
-
rolling up their outputs into your next decision.
|
|
5714
|
+
async summarizeRange(messages, ctx) {
|
|
5715
|
+
const systemText = `${this.summarizerPrompt}
|
|
5690
5716
|
|
|
5691
|
-
|
|
5692
|
-
|
|
5693
|
-
|
|
5694
|
-
|
|
5695
|
-
|
|
5696
|
-
|
|
5697
|
-
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
|
|
5702
|
-
|
|
5703
|
-
|
|
5704
|
-
|
|
5705
|
-
|
|
5706
|
-
|
|
5707
|
-
3. Always pair an assign with an await. Don't fire-and-forget; you owe
|
|
5708
|
-
the user a single coherent answer at the end.
|
|
5709
|
-
4. Roll up before deciding. After await_tasks resolves, call roll_up so
|
|
5710
|
-
the results are folded back into your context in a compact form.
|
|
5711
|
-
5. Budget is real. Check fleet_usage periodically. If a subagent is
|
|
5712
|
-
thrashing, terminate it rather than letting cost climb silently.
|
|
5713
|
-
6. Never claim a subagent's work as your own without verifying it. If a
|
|
5714
|
-
result looks wrong, ask_subagent for clarification before passing it
|
|
5715
|
-
to the user.`;
|
|
5716
|
-
var DEFAULT_SUBAGENT_BASELINE = `You are a subagent operating under a Director. You were spawned to handle
|
|
5717
|
-
a specific slice of a larger plan \u2014 do that slice well and report back.
|
|
5718
|
-
|
|
5719
|
-
Bridge contract:
|
|
5720
|
-
- You have a parent (the Director). You may call \`request\` on the
|
|
5721
|
-
parent bridge to ask a clarifying question. Use this sparingly; the
|
|
5722
|
-
parent is also working.
|
|
5723
|
-
- You MAY NOT request the parent's system prompt, tool list, or other
|
|
5724
|
-
subagents' context. Those are not yours to read.
|
|
5725
|
-
- Your final task output is what the Director sees. Be concise,
|
|
5726
|
-
structured, and self-contained \u2014 assume the Director will paste your
|
|
5727
|
-
output into its own context.`;
|
|
5728
|
-
function composeDirectorPrompt(parts = {}) {
|
|
5729
|
-
const sections = [];
|
|
5730
|
-
const preamble = parts.directorPreamble ?? DEFAULT_DIRECTOR_PREAMBLE;
|
|
5731
|
-
if (preamble && preamble.trim().length > 0) sections.push(preamble.trim());
|
|
5732
|
-
if (parts.rosterSummary && parts.rosterSummary.trim().length > 0) {
|
|
5733
|
-
sections.push(`Available roles you can spawn:
|
|
5734
|
-
${parts.rosterSummary.trim()}`);
|
|
5735
|
-
}
|
|
5736
|
-
if (parts.basePrompt && parts.basePrompt.trim().length > 0) {
|
|
5737
|
-
sections.push(parts.basePrompt.trim());
|
|
5738
|
-
}
|
|
5739
|
-
return sections.join("\n\n");
|
|
5740
|
-
}
|
|
5741
|
-
function composeSubagentPrompt(parts = {}) {
|
|
5742
|
-
const sections = [];
|
|
5743
|
-
const baseline = parts.baseline ?? DEFAULT_SUBAGENT_BASELINE;
|
|
5744
|
-
if (baseline && baseline.trim().length > 0) sections.push(baseline.trim());
|
|
5745
|
-
if (parts.role && parts.role.trim().length > 0) {
|
|
5746
|
-
sections.push(`Role:
|
|
5747
|
-
${parts.role.trim()}`);
|
|
5748
|
-
}
|
|
5749
|
-
if (parts.task && parts.task.trim().length > 0) {
|
|
5750
|
-
sections.push(`Task:
|
|
5751
|
-
${parts.task.trim()}`);
|
|
5752
|
-
}
|
|
5753
|
-
if (parts.sharedScratchpad && parts.sharedScratchpad.trim().length > 0) {
|
|
5754
|
-
sections.push(
|
|
5755
|
-
`Shared notes:
|
|
5756
|
-
A scratchpad shared with the rest of the fleet is mounted at \`${parts.sharedScratchpad.trim()}\`.
|
|
5757
|
-
- Write your final findings as markdown files there (e.g. \`findings.md\`, \`security.md\`).
|
|
5758
|
-
- Before starting, list the directory and read any sibling files relevant to your task \u2014 they may already contain context you can build on.
|
|
5759
|
-
- Use stable filenames (one file per concern); overwrite instead of appending so the Director sees the latest state.`
|
|
5760
|
-
);
|
|
5761
|
-
}
|
|
5762
|
-
if (parts.override && parts.override.trim().length > 0) {
|
|
5763
|
-
sections.push(parts.override.trim());
|
|
5717
|
+
Summarize the following message range:`;
|
|
5718
|
+
const body = messages.map((m, i) => `[${i}] ${m.role}: ${this.messagePreview(m)}`).join("\n");
|
|
5719
|
+
const req = {
|
|
5720
|
+
model: this.summarizerModel,
|
|
5721
|
+
system: [{ type: "text", text: systemText }],
|
|
5722
|
+
messages: [{ role: "user", content: body }],
|
|
5723
|
+
maxTokens: 512
|
|
5724
|
+
};
|
|
5725
|
+
try {
|
|
5726
|
+
const res = await this.provider.complete(req, {
|
|
5727
|
+
signal: ctx.signal ?? new AbortController().signal
|
|
5728
|
+
});
|
|
5729
|
+
return res.content.filter(isTextBlock).map((b) => b.text).join("\n").trim() || "(empty)";
|
|
5730
|
+
} catch {
|
|
5731
|
+
return `[${messages.length} earlier turns omitted]`;
|
|
5732
|
+
}
|
|
5764
5733
|
}
|
|
5765
|
-
|
|
5766
|
-
|
|
5767
|
-
|
|
5768
|
-
const lines = [];
|
|
5769
|
-
for (const [roleId, cfg] of Object.entries(roster)) {
|
|
5770
|
-
const tag = cfg.provider && cfg.model ? ` (${cfg.provider}/${cfg.model})` : "";
|
|
5771
|
-
const headline = cfg.prompt ? (cfg.prompt.split("\n").find((l) => l.trim().length > 0) ?? "").trim().slice(0, 80) : "";
|
|
5772
|
-
const tail = headline ? ` \u2014 ${headline}` : "";
|
|
5773
|
-
lines.push(`- ${roleId}: ${cfg.name}${tag}${tail}`);
|
|
5734
|
+
messagePreview(m) {
|
|
5735
|
+
if (typeof m.content === "string") return m.content.slice(0, 300);
|
|
5736
|
+
return m.content.filter(isTextBlock).map((b) => b.text).join(" ").slice(0, 300);
|
|
5774
5737
|
}
|
|
5775
|
-
return lines.join("\n");
|
|
5776
|
-
}
|
|
5777
|
-
|
|
5778
|
-
// src/coordination/fleet-bus.ts
|
|
5779
|
-
var FleetBus = class {
|
|
5780
|
-
byId = /* @__PURE__ */ new Map();
|
|
5781
|
-
byType = /* @__PURE__ */ new Map();
|
|
5782
|
-
any = /* @__PURE__ */ new Set();
|
|
5783
5738
|
/**
|
|
5784
|
-
*
|
|
5785
|
-
*
|
|
5786
|
-
* canonical set of event types a subagent emits during a run. New
|
|
5787
|
-
* event types added to the kernel must be added here too — but the
|
|
5788
|
-
* cost is a tiny single line per type, and the explicit list keeps
|
|
5789
|
-
* the wire format clear.
|
|
5790
|
-
*
|
|
5791
|
-
* Returns a disposer that detaches every subscription; call on
|
|
5792
|
-
* subagent teardown so the listeners don't outlive the run.
|
|
5739
|
+
* Fallback when selector fails: aggressively trim from the oldest end
|
|
5740
|
+
* until we hit targetBudget.
|
|
5793
5741
|
*/
|
|
5794
|
-
|
|
5795
|
-
const
|
|
5796
|
-
|
|
5797
|
-
|
|
5798
|
-
|
|
5799
|
-
|
|
5800
|
-
|
|
5801
|
-
"
|
|
5802
|
-
|
|
5803
|
-
|
|
5804
|
-
|
|
5805
|
-
// silent gap between iteration.started and the first text_delta.
|
|
5806
|
-
"provider.thinking_delta",
|
|
5807
|
-
"provider.response",
|
|
5808
|
-
"provider.retry",
|
|
5809
|
-
"provider.error",
|
|
5810
|
-
"session.started",
|
|
5811
|
-
"session.ended",
|
|
5812
|
-
"session.damaged",
|
|
5813
|
-
"compaction.fired",
|
|
5814
|
-
"compaction.failed",
|
|
5815
|
-
"token.threshold"
|
|
5816
|
-
];
|
|
5817
|
-
const offs = [];
|
|
5818
|
-
for (const t2 of FORWARDED_TYPES) {
|
|
5819
|
-
offs.push(
|
|
5820
|
-
bus.on(t2, (payload) => {
|
|
5821
|
-
this.emit({ subagentId, taskId, ts: Date.now(), type: t2, payload });
|
|
5822
|
-
})
|
|
5823
|
-
);
|
|
5742
|
+
aggressiveRecencyTrim(ctx) {
|
|
5743
|
+
const messages = ctx.messages;
|
|
5744
|
+
const preserveIdx = Math.max(0, messages.length - this.preserveK * 2);
|
|
5745
|
+
if (preserveIdx <= 0) return 0;
|
|
5746
|
+
let boundary = preserveIdx;
|
|
5747
|
+
for (let i = preserveIdx; i < messages.length && i < preserveIdx + 6; i++) {
|
|
5748
|
+
const m = messages[i];
|
|
5749
|
+
if (m.role === "user" && this.hasTextContent(m)) {
|
|
5750
|
+
boundary = i;
|
|
5751
|
+
break;
|
|
5752
|
+
}
|
|
5824
5753
|
}
|
|
5825
|
-
|
|
5826
|
-
|
|
5754
|
+
const removed = messages.slice(0, boundary);
|
|
5755
|
+
const removedTokens = this.estimateTokens(removed);
|
|
5756
|
+
const summaryMsg = {
|
|
5757
|
+
role: "system",
|
|
5758
|
+
content: `[${removed.length} earlier turns trimmed \u2014 see session log for details]`
|
|
5827
5759
|
};
|
|
5760
|
+
const tail = messages.slice(boundary);
|
|
5761
|
+
ctx.state.replaceMessages([summaryMsg, ...tail]);
|
|
5762
|
+
return Math.max(0, removedTokens - this.estimateTokens([summaryMsg]));
|
|
5828
5763
|
}
|
|
5829
|
-
|
|
5830
|
-
|
|
5831
|
-
|
|
5832
|
-
if (!set) {
|
|
5833
|
-
set = /* @__PURE__ */ new Set();
|
|
5834
|
-
this.byId.set(subagentId, set);
|
|
5764
|
+
computeTargetBudget(load) {
|
|
5765
|
+
if (load >= this.hardThreshold) {
|
|
5766
|
+
return Math.floor(this.maxContext * 0.5);
|
|
5835
5767
|
}
|
|
5836
|
-
|
|
5837
|
-
|
|
5838
|
-
set.delete(handler);
|
|
5839
|
-
};
|
|
5840
|
-
}
|
|
5841
|
-
/** Subscribe to one event type across all subagents. */
|
|
5842
|
-
filter(type, handler) {
|
|
5843
|
-
let set = this.byType.get(type);
|
|
5844
|
-
if (!set) {
|
|
5845
|
-
set = /* @__PURE__ */ new Set();
|
|
5846
|
-
this.byType.set(type, set);
|
|
5768
|
+
if (load >= this.softThreshold) {
|
|
5769
|
+
return Math.floor(this.maxContext * 0.65);
|
|
5847
5770
|
}
|
|
5848
|
-
|
|
5849
|
-
return () => {
|
|
5850
|
-
set.delete(handler);
|
|
5851
|
-
};
|
|
5852
|
-
}
|
|
5853
|
-
/** Subscribe to literally everything. The fleet roll-up uses this. */
|
|
5854
|
-
onAny(handler) {
|
|
5855
|
-
this.any.add(handler);
|
|
5856
|
-
return () => {
|
|
5857
|
-
this.any.delete(handler);
|
|
5858
|
-
};
|
|
5771
|
+
return Math.floor(this.maxContext * 0.75);
|
|
5859
5772
|
}
|
|
5860
|
-
|
|
5861
|
-
const
|
|
5862
|
-
|
|
5863
|
-
|
|
5864
|
-
|
|
5865
|
-
|
|
5866
|
-
|
|
5867
|
-
|
|
5773
|
+
eliseOldToolResults(ctx) {
|
|
5774
|
+
const messages = ctx.messages;
|
|
5775
|
+
let pairCount = 0;
|
|
5776
|
+
let preserveStart = messages.length;
|
|
5777
|
+
for (let i = messages.length - 1; i >= 0 && pairCount < this.preserveK; i--) {
|
|
5778
|
+
const m = messages[i];
|
|
5779
|
+
if (!m) continue;
|
|
5780
|
+
if (m.role === "user" || m.role === "assistant") {
|
|
5781
|
+
pairCount++;
|
|
5782
|
+
preserveStart = i;
|
|
5868
5783
|
}
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
|
|
5875
|
-
|
|
5784
|
+
}
|
|
5785
|
+
let saved = 0;
|
|
5786
|
+
let changed = false;
|
|
5787
|
+
const nextMessages = new Array(messages.length);
|
|
5788
|
+
for (let i = 0; i < messages.length; i++) {
|
|
5789
|
+
const msg = messages[i];
|
|
5790
|
+
if (i >= preserveStart) {
|
|
5791
|
+
nextMessages[i] = msg;
|
|
5792
|
+
continue;
|
|
5876
5793
|
}
|
|
5877
|
-
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
}
|
|
5794
|
+
if (!msg || !Array.isArray(msg.content)) {
|
|
5795
|
+
nextMessages[i] = msg;
|
|
5796
|
+
continue;
|
|
5797
|
+
}
|
|
5798
|
+
const newContent = msg.content.map((b) => {
|
|
5799
|
+
if (b.type !== "tool_result") return b;
|
|
5800
|
+
const text = typeof b.content === "string" ? b.content : JSON.stringify(b.content);
|
|
5801
|
+
const tokens = this.roughTokenEstimate(text);
|
|
5802
|
+
if (tokens < this.eliseThreshold) return b;
|
|
5803
|
+
saved += tokens;
|
|
5804
|
+
return {
|
|
5805
|
+
type: "tool_result",
|
|
5806
|
+
tool_use_id: b.tool_use_id,
|
|
5807
|
+
content: `[elided: ~${tokens} tokens]`,
|
|
5808
|
+
is_error: b.is_error
|
|
5809
|
+
};
|
|
5810
|
+
});
|
|
5811
|
+
if (newContent.every((b, idx) => b === msg.content[idx])) {
|
|
5812
|
+
nextMessages[i] = msg;
|
|
5813
|
+
} else {
|
|
5814
|
+
nextMessages[i] = { ...msg, content: newContent };
|
|
5815
|
+
changed = true;
|
|
5881
5816
|
}
|
|
5882
5817
|
}
|
|
5818
|
+
if (changed) ctx.state.replaceMessages(nextMessages);
|
|
5819
|
+
return saved;
|
|
5883
5820
|
}
|
|
5884
|
-
|
|
5885
|
-
|
|
5886
|
-
|
|
5887
|
-
this.bus = bus;
|
|
5888
|
-
this.priceLookup = priceLookup;
|
|
5889
|
-
this.metaLookup = metaLookup;
|
|
5890
|
-
bus.filter("provider.response", (e) => this.onProviderResponse(e));
|
|
5891
|
-
bus.filter("tool.executed", (e) => this.onToolExecuted(e));
|
|
5892
|
-
bus.filter("iteration.started", (e) => this.onIterationStarted(e));
|
|
5893
|
-
}
|
|
5894
|
-
bus;
|
|
5895
|
-
priceLookup;
|
|
5896
|
-
metaLookup;
|
|
5897
|
-
perSubagent = /* @__PURE__ */ new Map();
|
|
5898
|
-
total = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
|
|
5899
|
-
/** Live snapshot — safe to call from a tool's execute() body. */
|
|
5900
|
-
snapshot() {
|
|
5901
|
-
return {
|
|
5902
|
-
total: { ...this.total },
|
|
5903
|
-
perSubagent: Object.fromEntries(
|
|
5904
|
-
Array.from(this.perSubagent.entries()).map(([k, v]) => [k, { ...v }])
|
|
5905
|
-
)
|
|
5906
|
-
};
|
|
5907
|
-
}
|
|
5908
|
-
ensure(subagentId) {
|
|
5909
|
-
let snap = this.perSubagent.get(subagentId);
|
|
5910
|
-
if (!snap) {
|
|
5911
|
-
const meta = this.metaLookup?.(subagentId);
|
|
5912
|
-
snap = {
|
|
5913
|
-
subagentId,
|
|
5914
|
-
provider: meta?.provider,
|
|
5915
|
-
model: meta?.model,
|
|
5916
|
-
input: 0,
|
|
5917
|
-
output: 0,
|
|
5918
|
-
cacheRead: 0,
|
|
5919
|
-
cacheWrite: 0,
|
|
5920
|
-
cost: 0,
|
|
5921
|
-
toolCalls: 0,
|
|
5922
|
-
iterations: 0,
|
|
5923
|
-
startedAt: Date.now(),
|
|
5924
|
-
lastEventAt: Date.now()
|
|
5925
|
-
};
|
|
5926
|
-
this.perSubagent.set(subagentId, snap);
|
|
5927
|
-
}
|
|
5928
|
-
return snap;
|
|
5929
|
-
}
|
|
5930
|
-
onProviderResponse(e) {
|
|
5931
|
-
const snap = this.ensure(e.subagentId);
|
|
5932
|
-
const p = e.payload;
|
|
5933
|
-
const usage = p?.usage;
|
|
5934
|
-
if (!usage) return;
|
|
5935
|
-
snap.input += usage.input ?? 0;
|
|
5936
|
-
snap.output += usage.output ?? 0;
|
|
5937
|
-
snap.cacheRead += usage.cacheRead ?? 0;
|
|
5938
|
-
snap.cacheWrite += usage.cacheWrite ?? 0;
|
|
5939
|
-
this.total.input += usage.input ?? 0;
|
|
5940
|
-
this.total.output += usage.output ?? 0;
|
|
5941
|
-
this.total.cacheRead += usage.cacheRead ?? 0;
|
|
5942
|
-
this.total.cacheWrite += usage.cacheWrite ?? 0;
|
|
5943
|
-
const price = this.priceLookup?.(e.subagentId);
|
|
5944
|
-
if (price) {
|
|
5945
|
-
const delta = (usage.input ?? 0) / 1e6 * (price.input ?? 0) + (usage.output ?? 0) / 1e6 * (price.output ?? 0) + (usage.cacheRead ?? 0) / 1e6 * (price.cacheRead ?? 0) + (usage.cacheWrite ?? 0) / 1e6 * (price.cacheWrite ?? 0);
|
|
5946
|
-
snap.cost += delta;
|
|
5947
|
-
this.total.cost += delta;
|
|
5948
|
-
}
|
|
5949
|
-
snap.lastEventAt = e.ts;
|
|
5821
|
+
hasTextContent(m) {
|
|
5822
|
+
if (typeof m.content === "string") return m.content.trim().length > 0;
|
|
5823
|
+
return m.content.some((b) => b.type === "text" && b.text.trim().length > 0);
|
|
5950
5824
|
}
|
|
5951
|
-
|
|
5952
|
-
|
|
5953
|
-
|
|
5954
|
-
|
|
5825
|
+
estimateTokens(messages) {
|
|
5826
|
+
let total = 0;
|
|
5827
|
+
for (const m of messages) {
|
|
5828
|
+
if (typeof m.content === "string") {
|
|
5829
|
+
total += this.roughTokenEstimate(m.content);
|
|
5830
|
+
} else {
|
|
5831
|
+
for (const b of m.content) {
|
|
5832
|
+
if (b.type === "text") total += this.roughTokenEstimate(b.text);
|
|
5833
|
+
else if (b.type === "tool_use") total += this.roughTokenEstimate(JSON.stringify(b.input));
|
|
5834
|
+
else if (b.type === "tool_result") {
|
|
5835
|
+
total += this.roughTokenEstimate(
|
|
5836
|
+
typeof b.content === "string" ? b.content : JSON.stringify(b.content)
|
|
5837
|
+
);
|
|
5838
|
+
}
|
|
5839
|
+
}
|
|
5840
|
+
}
|
|
5841
|
+
}
|
|
5842
|
+
return total;
|
|
5955
5843
|
}
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
snap.iterations += 1;
|
|
5959
|
-
snap.lastEventAt = e.ts;
|
|
5844
|
+
roughTokenEstimate(text) {
|
|
5845
|
+
return Math.max(1, Math.ceil(text.length / 4));
|
|
5960
5846
|
}
|
|
5961
5847
|
};
|
|
5962
5848
|
|
|
5963
|
-
// src/
|
|
5964
|
-
var
|
|
5965
|
-
|
|
5966
|
-
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
};
|
|
5976
|
-
var SubagentBudget = class {
|
|
5977
|
-
limits;
|
|
5978
|
-
iterations = 0;
|
|
5979
|
-
toolCalls = 0;
|
|
5980
|
-
tokenInput = 0;
|
|
5981
|
-
tokenOutput = 0;
|
|
5982
|
-
costUsd = 0;
|
|
5983
|
-
startTime = null;
|
|
5984
|
-
constructor(limits = {}) {
|
|
5985
|
-
this.limits = Object.freeze({ ...limits });
|
|
5986
|
-
}
|
|
5987
|
-
start() {
|
|
5988
|
-
this.startTime = Date.now();
|
|
5989
|
-
}
|
|
5990
|
-
recordIteration() {
|
|
5991
|
-
this.iterations++;
|
|
5992
|
-
if (this.limits.maxIterations !== void 0 && this.iterations > this.limits.maxIterations) {
|
|
5993
|
-
throw new BudgetExceededError("iterations", this.limits.maxIterations, this.iterations);
|
|
5994
|
-
}
|
|
5995
|
-
}
|
|
5996
|
-
recordToolCall() {
|
|
5997
|
-
this.toolCalls++;
|
|
5998
|
-
if (this.limits.maxToolCalls !== void 0 && this.toolCalls > this.limits.maxToolCalls) {
|
|
5999
|
-
throw new BudgetExceededError("tool_calls", this.limits.maxToolCalls, this.toolCalls);
|
|
6000
|
-
}
|
|
6001
|
-
}
|
|
6002
|
-
recordUsage(usage, costUsd = 0) {
|
|
6003
|
-
this.tokenInput += usage.input;
|
|
6004
|
-
this.tokenOutput += usage.output;
|
|
6005
|
-
this.costUsd += costUsd;
|
|
6006
|
-
const totalTokens = this.tokenInput + this.tokenOutput;
|
|
6007
|
-
if (this.limits.maxTokens !== void 0 && totalTokens > this.limits.maxTokens) {
|
|
6008
|
-
throw new BudgetExceededError("tokens", this.limits.maxTokens, totalTokens);
|
|
6009
|
-
}
|
|
6010
|
-
if (this.limits.maxCostUsd !== void 0 && this.costUsd > this.limits.maxCostUsd) {
|
|
6011
|
-
throw new BudgetExceededError("cost", this.limits.maxCostUsd, this.costUsd);
|
|
6012
|
-
}
|
|
6013
|
-
}
|
|
5849
|
+
// src/execution/auto-compaction-middleware.ts
|
|
5850
|
+
var AutoCompactionMiddleware = class {
|
|
5851
|
+
name = "AutoCompaction";
|
|
5852
|
+
compactor;
|
|
5853
|
+
warnThreshold;
|
|
5854
|
+
softThreshold;
|
|
5855
|
+
hardThreshold;
|
|
5856
|
+
maxContext;
|
|
5857
|
+
estimator;
|
|
5858
|
+
aggressiveOn;
|
|
5859
|
+
events;
|
|
5860
|
+
failureMode;
|
|
6014
5861
|
/**
|
|
6015
|
-
*
|
|
6016
|
-
*
|
|
5862
|
+
* @param compactor Compactor to use for compaction.
|
|
5863
|
+
* @param maxContext Provider's max context window in tokens.
|
|
5864
|
+
* @param estimator Token estimation function.
|
|
5865
|
+
* @param thresholds Threshold fractions (0-1) of maxContext.
|
|
5866
|
+
* @param opts Optional behavior. By default, failures at the
|
|
5867
|
+
* hard threshold throw AGENT_CONTEXT_OVERFLOW so
|
|
5868
|
+
* the agent does not continue into a likely
|
|
5869
|
+
* provider context overflow. Warn/soft failures
|
|
5870
|
+
* still emit compaction.failed and continue.
|
|
6017
5871
|
*/
|
|
6018
|
-
|
|
6019
|
-
|
|
6020
|
-
|
|
6021
|
-
|
|
6022
|
-
|
|
6023
|
-
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
|
|
6028
|
-
|
|
5872
|
+
constructor(compactor, maxContext, estimator, thresholds, optsOrAggressiveOn = {}, events) {
|
|
5873
|
+
const opts = typeof optsOrAggressiveOn === "string" ? { aggressiveOn: optsOrAggressiveOn, events } : optsOrAggressiveOn;
|
|
5874
|
+
this.compactor = compactor;
|
|
5875
|
+
this.maxContext = maxContext;
|
|
5876
|
+
this.estimator = estimator;
|
|
5877
|
+
this.warnThreshold = thresholds.warn;
|
|
5878
|
+
this.softThreshold = thresholds.soft;
|
|
5879
|
+
this.hardThreshold = thresholds.hard;
|
|
5880
|
+
this.aggressiveOn = opts.aggressiveOn ?? "soft";
|
|
5881
|
+
this.events = opts.events;
|
|
5882
|
+
this.failureMode = opts.failureMode ?? "throw_on_hard";
|
|
6029
5883
|
}
|
|
6030
|
-
|
|
6031
|
-
return {
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
|
|
6035
|
-
|
|
6036
|
-
|
|
6037
|
-
|
|
6038
|
-
}
|
|
6039
|
-
|
|
6040
|
-
|
|
5884
|
+
handler() {
|
|
5885
|
+
return async (ctx, next) => {
|
|
5886
|
+
const tokens = this.estimator(ctx);
|
|
5887
|
+
const load = tokens / this.maxContext;
|
|
5888
|
+
if (load >= this.hardThreshold) {
|
|
5889
|
+
await this.compact(ctx, true, { level: "hard", tokens, load });
|
|
5890
|
+
} else if (load >= this.softThreshold) {
|
|
5891
|
+
await this.compact(ctx, this.aggressiveOn !== "hard", { level: "soft", tokens, load });
|
|
5892
|
+
} else if (load >= this.warnThreshold) {
|
|
5893
|
+
await this.compact(ctx, false, { level: "warn", tokens, load });
|
|
5894
|
+
}
|
|
5895
|
+
return next(ctx);
|
|
6041
5896
|
};
|
|
6042
5897
|
}
|
|
5898
|
+
async compact(ctx, aggressive, pressure) {
|
|
5899
|
+
try {
|
|
5900
|
+
await this.compactor.compact(ctx, { aggressive });
|
|
5901
|
+
} catch (err) {
|
|
5902
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
5903
|
+
const fatal = this.failureMode === "throw" || this.failureMode === "throw_on_hard" && pressure.level === "hard";
|
|
5904
|
+
this.events?.emit("compaction.failed", {
|
|
5905
|
+
err: error,
|
|
5906
|
+
aggressive,
|
|
5907
|
+
level: pressure.level,
|
|
5908
|
+
tokens: pressure.tokens,
|
|
5909
|
+
maxContext: this.maxContext,
|
|
5910
|
+
load: pressure.load,
|
|
5911
|
+
fatal
|
|
5912
|
+
});
|
|
5913
|
+
if (fatal) {
|
|
5914
|
+
throw new AgentError({
|
|
5915
|
+
message: `Auto-compaction failed at ${pressure.level} threshold`,
|
|
5916
|
+
code: "AGENT_CONTEXT_OVERFLOW",
|
|
5917
|
+
recoverable: true,
|
|
5918
|
+
context: {
|
|
5919
|
+
level: pressure.level,
|
|
5920
|
+
tokens: pressure.tokens,
|
|
5921
|
+
maxContext: this.maxContext
|
|
5922
|
+
},
|
|
5923
|
+
cause: err
|
|
5924
|
+
});
|
|
5925
|
+
}
|
|
5926
|
+
}
|
|
5927
|
+
}
|
|
6043
5928
|
};
|
|
6044
5929
|
|
|
6045
|
-
// src/
|
|
6046
|
-
var
|
|
6047
|
-
|
|
5930
|
+
// src/execution/autonomous-runner.ts
|
|
5931
|
+
var DoneConditionChecker = class {
|
|
5932
|
+
constructor(condition) {
|
|
5933
|
+
this.condition = condition;
|
|
5934
|
+
this.compiledRegex = condition.type === "output_match" && condition.pattern ? new RegExp(condition.pattern) : null;
|
|
5935
|
+
}
|
|
5936
|
+
condition;
|
|
5937
|
+
compiledRegex;
|
|
5938
|
+
check(state) {
|
|
5939
|
+
switch (this.condition.type) {
|
|
5940
|
+
case "iterations":
|
|
5941
|
+
if (this.condition.maxIterations && state.iterations >= this.condition.maxIterations) {
|
|
5942
|
+
return {
|
|
5943
|
+
done: true,
|
|
5944
|
+
reason: `max iterations (${this.condition.maxIterations}) reached`,
|
|
5945
|
+
...state
|
|
5946
|
+
};
|
|
5947
|
+
}
|
|
5948
|
+
break;
|
|
5949
|
+
case "tool_calls":
|
|
5950
|
+
if (this.condition.maxToolCalls && state.toolCalls >= this.condition.maxToolCalls) {
|
|
5951
|
+
return {
|
|
5952
|
+
done: true,
|
|
5953
|
+
reason: `max tool calls (${this.condition.maxToolCalls}) reached`,
|
|
5954
|
+
...state
|
|
5955
|
+
};
|
|
5956
|
+
}
|
|
5957
|
+
break;
|
|
5958
|
+
case "output_match":
|
|
5959
|
+
if (this.compiledRegex && state.lastOutput && this.compiledRegex.test(state.lastOutput)) {
|
|
5960
|
+
return {
|
|
5961
|
+
done: true,
|
|
5962
|
+
reason: `output matched pattern "${this.condition.pattern}"`,
|
|
5963
|
+
...state
|
|
5964
|
+
};
|
|
5965
|
+
}
|
|
5966
|
+
break;
|
|
5967
|
+
}
|
|
5968
|
+
return { done: false, iterations: state.iterations, toolCalls: state.toolCalls };
|
|
5969
|
+
}
|
|
5970
|
+
};
|
|
5971
|
+
var AutonomousRunner = class {
|
|
5972
|
+
constructor(opts) {
|
|
5973
|
+
this.opts = opts;
|
|
5974
|
+
this.doneChecker = new DoneConditionChecker(opts.doneCondition);
|
|
5975
|
+
}
|
|
5976
|
+
opts;
|
|
5977
|
+
iterations = 0;
|
|
5978
|
+
toolCalls = 0;
|
|
5979
|
+
lastOutput;
|
|
5980
|
+
stopped = false;
|
|
5981
|
+
doneChecker;
|
|
5982
|
+
async run() {
|
|
5983
|
+
const offToolExecuted = this.opts.agent.events?.on?.("tool.executed", () => {
|
|
5984
|
+
this.toolCalls++;
|
|
5985
|
+
});
|
|
5986
|
+
try {
|
|
5987
|
+
return await this.runLoop();
|
|
5988
|
+
} finally {
|
|
5989
|
+
offToolExecuted?.();
|
|
5990
|
+
}
|
|
5991
|
+
}
|
|
5992
|
+
async runLoop() {
|
|
5993
|
+
while (!this.stopped) {
|
|
5994
|
+
const check = this.doneChecker.check({
|
|
5995
|
+
iterations: this.iterations,
|
|
5996
|
+
toolCalls: this.toolCalls,
|
|
5997
|
+
lastOutput: this.lastOutput
|
|
5998
|
+
});
|
|
5999
|
+
if (check.done) {
|
|
6000
|
+
const result = {
|
|
6001
|
+
status: "done",
|
|
6002
|
+
iterations: this.iterations,
|
|
6003
|
+
toolCalls: this.toolCalls,
|
|
6004
|
+
reason: check.reason
|
|
6005
|
+
};
|
|
6006
|
+
this.opts.onDone?.(result);
|
|
6007
|
+
return result;
|
|
6008
|
+
}
|
|
6009
|
+
this.opts.onIteration?.({ iteration: this.iterations, toolCalls: this.toolCalls });
|
|
6010
|
+
const ctrl = new AbortController();
|
|
6011
|
+
const timeout = setTimeout(() => ctrl.abort(), this.opts.iterationTimeoutMs ?? 3e4);
|
|
6012
|
+
try {
|
|
6013
|
+
const result = await this.opts.agent.run("", {
|
|
6014
|
+
signal: ctrl.signal,
|
|
6015
|
+
maxIterations: 1,
|
|
6016
|
+
executionStrategy: "sequential"
|
|
6017
|
+
});
|
|
6018
|
+
this.iterations++;
|
|
6019
|
+
if (result.status === "done") {
|
|
6020
|
+
this.lastOutput = result.finalText;
|
|
6021
|
+
}
|
|
6022
|
+
if (result.status === "failed" || result.status === "aborted") {
|
|
6023
|
+
const failedResult = {
|
|
6024
|
+
status: result.status,
|
|
6025
|
+
error: result.error,
|
|
6026
|
+
iterations: this.iterations,
|
|
6027
|
+
toolCalls: this.toolCalls
|
|
6028
|
+
};
|
|
6029
|
+
this.opts.onDone?.(failedResult);
|
|
6030
|
+
return failedResult;
|
|
6031
|
+
}
|
|
6032
|
+
} catch (e) {
|
|
6033
|
+
const isAbort = e instanceof DOMException && e.name === "AbortError" || e instanceof Error && e.name === "AbortError" || e instanceof Error && e.message.includes("iteration timeout");
|
|
6034
|
+
if (isAbort) {
|
|
6035
|
+
const timeoutResult = {
|
|
6036
|
+
status: "failed",
|
|
6037
|
+
error: toWrongStackError(e),
|
|
6038
|
+
iterations: this.iterations,
|
|
6039
|
+
toolCalls: this.toolCalls,
|
|
6040
|
+
reason: "iteration timeout"
|
|
6041
|
+
};
|
|
6042
|
+
this.opts.onDone?.(timeoutResult);
|
|
6043
|
+
return timeoutResult;
|
|
6044
|
+
}
|
|
6045
|
+
this.stopped = true;
|
|
6046
|
+
const failedResult = {
|
|
6047
|
+
status: "failed",
|
|
6048
|
+
error: toWrongStackError(e),
|
|
6049
|
+
iterations: this.iterations,
|
|
6050
|
+
toolCalls: this.toolCalls,
|
|
6051
|
+
reason: e instanceof Error ? e.message : String(e)
|
|
6052
|
+
};
|
|
6053
|
+
this.opts.onDone?.(failedResult);
|
|
6054
|
+
return failedResult;
|
|
6055
|
+
} finally {
|
|
6056
|
+
clearTimeout(timeout);
|
|
6057
|
+
}
|
|
6058
|
+
}
|
|
6059
|
+
return {
|
|
6060
|
+
status: "aborted",
|
|
6061
|
+
iterations: this.iterations,
|
|
6062
|
+
toolCalls: this.toolCalls,
|
|
6063
|
+
reason: "stopped externally"
|
|
6064
|
+
};
|
|
6065
|
+
}
|
|
6066
|
+
stop() {
|
|
6067
|
+
this.stopped = true;
|
|
6068
|
+
}
|
|
6069
|
+
};
|
|
6070
|
+
|
|
6071
|
+
// src/coordination/director-prompts.ts
|
|
6072
|
+
var DEFAULT_DIRECTOR_PREAMBLE = `You are the Director of a multi-agent fleet. You orchestrate worker
|
|
6073
|
+
subagents by spawning them, assigning tasks, awaiting completions, and
|
|
6074
|
+
rolling up their outputs into your next decision.
|
|
6075
|
+
|
|
6076
|
+
Core fleet tools available to you:
|
|
6077
|
+
- spawn_subagent \u2014 create a worker with a chosen provider / model / role
|
|
6078
|
+
- assign_task \u2014 hand a piece of work to a specific subagent
|
|
6079
|
+
- await_tasks \u2014 block until named task ids complete (parallel-safe)
|
|
6080
|
+
- ask_subagent \u2014 synchronously query a running subagent via the bridge
|
|
6081
|
+
- roll_up \u2014 aggregate finished tasks into a markdown/json summary
|
|
6082
|
+
- terminate_subagent \u2014 abort a stuck worker (use sparingly)
|
|
6083
|
+
- fleet_status \u2014 snapshot of all subagents and pending tasks
|
|
6084
|
+
- fleet_usage \u2014 token + cost breakdown per subagent and total
|
|
6085
|
+
|
|
6086
|
+
Working rules:
|
|
6087
|
+
1. Decompose first. Before spawning, decide which sub-tasks are
|
|
6088
|
+
independent and can run in parallel. Sequential work doesn't need a
|
|
6089
|
+
subagent \u2014 do it yourself.
|
|
6090
|
+
2. Match worker to job. Cheap/fast model for triage, capable model for
|
|
6091
|
+
synthesis. Different providers per sibling is allowed and encouraged.
|
|
6092
|
+
3. Always pair an assign with an await. Don't fire-and-forget; you owe
|
|
6093
|
+
the user a single coherent answer at the end.
|
|
6094
|
+
4. Roll up before deciding. After await_tasks resolves, call roll_up so
|
|
6095
|
+
the results are folded back into your context in a compact form.
|
|
6096
|
+
5. Budget is real. Check fleet_usage periodically. If a subagent is
|
|
6097
|
+
thrashing, terminate it rather than letting cost climb silently.
|
|
6098
|
+
6. Never claim a subagent's work as your own without verifying it. If a
|
|
6099
|
+
result looks wrong, ask_subagent for clarification before passing it
|
|
6100
|
+
to the user.`;
|
|
6101
|
+
var DEFAULT_SUBAGENT_BASELINE = `You are a subagent operating under a Director. You were spawned to handle
|
|
6102
|
+
a specific slice of a larger plan \u2014 do that slice well and report back.
|
|
6103
|
+
|
|
6104
|
+
Bridge contract:
|
|
6105
|
+
- You have a parent (the Director). You may call \`request\` on the
|
|
6106
|
+
parent bridge to ask a clarifying question. Use this sparingly; the
|
|
6107
|
+
parent is also working.
|
|
6108
|
+
- You MAY NOT request the parent's system prompt, tool list, or other
|
|
6109
|
+
subagents' context. Those are not yours to read.
|
|
6110
|
+
- Your final task output is what the Director sees. Be concise,
|
|
6111
|
+
structured, and self-contained \u2014 assume the Director will paste your
|
|
6112
|
+
output into its own context.`;
|
|
6113
|
+
function composeDirectorPrompt(parts = {}) {
|
|
6114
|
+
const sections = [];
|
|
6115
|
+
const preamble = parts.directorPreamble ?? DEFAULT_DIRECTOR_PREAMBLE;
|
|
6116
|
+
if (preamble && preamble.trim().length > 0) sections.push(preamble.trim());
|
|
6117
|
+
if (parts.rosterSummary && parts.rosterSummary.trim().length > 0) {
|
|
6118
|
+
sections.push(`Available roles you can spawn:
|
|
6119
|
+
${parts.rosterSummary.trim()}`);
|
|
6120
|
+
}
|
|
6121
|
+
if (parts.basePrompt && parts.basePrompt.trim().length > 0) {
|
|
6122
|
+
sections.push(parts.basePrompt.trim());
|
|
6123
|
+
}
|
|
6124
|
+
return sections.join("\n\n");
|
|
6125
|
+
}
|
|
6126
|
+
function composeSubagentPrompt(parts = {}) {
|
|
6127
|
+
const sections = [];
|
|
6128
|
+
const baseline = parts.baseline ?? DEFAULT_SUBAGENT_BASELINE;
|
|
6129
|
+
if (baseline && baseline.trim().length > 0) sections.push(baseline.trim());
|
|
6130
|
+
if (parts.role && parts.role.trim().length > 0) {
|
|
6131
|
+
sections.push(`Role:
|
|
6132
|
+
${parts.role.trim()}`);
|
|
6133
|
+
}
|
|
6134
|
+
if (parts.task && parts.task.trim().length > 0) {
|
|
6135
|
+
sections.push(`Task:
|
|
6136
|
+
${parts.task.trim()}`);
|
|
6137
|
+
}
|
|
6138
|
+
if (parts.sharedScratchpad && parts.sharedScratchpad.trim().length > 0) {
|
|
6139
|
+
sections.push(
|
|
6140
|
+
`Shared notes:
|
|
6141
|
+
A scratchpad shared with the rest of the fleet is mounted at \`${parts.sharedScratchpad.trim()}\`.
|
|
6142
|
+
- Write your final findings as markdown files there (e.g. \`findings.md\`, \`security.md\`).
|
|
6143
|
+
- Before starting, list the directory and read any sibling files relevant to your task \u2014 they may already contain context you can build on.
|
|
6144
|
+
- Use stable filenames (one file per concern); overwrite instead of appending so the Director sees the latest state.`
|
|
6145
|
+
);
|
|
6146
|
+
}
|
|
6147
|
+
if (parts.override && parts.override.trim().length > 0) {
|
|
6148
|
+
sections.push(parts.override.trim());
|
|
6149
|
+
}
|
|
6150
|
+
return sections.join("\n\n");
|
|
6151
|
+
}
|
|
6152
|
+
function rosterSummaryFromConfigs(roster) {
|
|
6153
|
+
const lines = [];
|
|
6154
|
+
for (const [roleId, cfg] of Object.entries(roster)) {
|
|
6155
|
+
const tag = cfg.provider && cfg.model ? ` (${cfg.provider}/${cfg.model})` : "";
|
|
6156
|
+
const headline = cfg.prompt ? (cfg.prompt.split("\n").find((l) => l.trim().length > 0) ?? "").trim().slice(0, 80) : "";
|
|
6157
|
+
const tail = headline ? ` \u2014 ${headline}` : "";
|
|
6158
|
+
lines.push(`- ${roleId}: ${cfg.name}${tag}${tail}`);
|
|
6159
|
+
}
|
|
6160
|
+
return lines.join("\n");
|
|
6161
|
+
}
|
|
6162
|
+
|
|
6163
|
+
// src/coordination/fleet-bus.ts
|
|
6164
|
+
var FleetBus = class {
|
|
6165
|
+
byId = /* @__PURE__ */ new Map();
|
|
6166
|
+
byType = /* @__PURE__ */ new Map();
|
|
6167
|
+
any = /* @__PURE__ */ new Set();
|
|
6168
|
+
/**
|
|
6169
|
+
* Hook a subagent's EventBus into the fleet. EventBus is strongly
|
|
6170
|
+
* typed and doesn't expose an `onAny` hook, so we subscribe to the
|
|
6171
|
+
* canonical set of event types a subagent emits during a run. New
|
|
6172
|
+
* event types added to the kernel must be added here too — but the
|
|
6173
|
+
* cost is a tiny single line per type, and the explicit list keeps
|
|
6174
|
+
* the wire format clear.
|
|
6175
|
+
*
|
|
6176
|
+
* Returns a disposer that detaches every subscription; call on
|
|
6177
|
+
* subagent teardown so the listeners don't outlive the run.
|
|
6178
|
+
*/
|
|
6179
|
+
attach(subagentId, bus, taskId) {
|
|
6180
|
+
const FORWARDED_TYPES = [
|
|
6181
|
+
"tool.started",
|
|
6182
|
+
"tool.executed",
|
|
6183
|
+
"tool.progress",
|
|
6184
|
+
"tool.confirm_needed",
|
|
6185
|
+
"iteration.started",
|
|
6186
|
+
"iteration.completed",
|
|
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",
|
|
6192
|
+
"provider.response",
|
|
6193
|
+
"provider.retry",
|
|
6194
|
+
"provider.error",
|
|
6195
|
+
"session.started",
|
|
6196
|
+
"session.ended",
|
|
6197
|
+
"session.damaged",
|
|
6198
|
+
"compaction.fired",
|
|
6199
|
+
"compaction.failed",
|
|
6200
|
+
"token.threshold"
|
|
6201
|
+
];
|
|
6202
|
+
const offs = [];
|
|
6203
|
+
for (const t2 of FORWARDED_TYPES) {
|
|
6204
|
+
offs.push(
|
|
6205
|
+
bus.on(t2, (payload) => {
|
|
6206
|
+
this.emit({ subagentId, taskId, ts: Date.now(), type: t2, payload });
|
|
6207
|
+
})
|
|
6208
|
+
);
|
|
6209
|
+
}
|
|
6210
|
+
return () => {
|
|
6211
|
+
for (const off of offs) off();
|
|
6212
|
+
};
|
|
6213
|
+
}
|
|
6214
|
+
/** Subscribe to every event from one subagent. */
|
|
6215
|
+
subscribe(subagentId, handler) {
|
|
6216
|
+
let set = this.byId.get(subagentId);
|
|
6217
|
+
if (!set) {
|
|
6218
|
+
set = /* @__PURE__ */ new Set();
|
|
6219
|
+
this.byId.set(subagentId, set);
|
|
6220
|
+
}
|
|
6221
|
+
set.add(handler);
|
|
6222
|
+
return () => {
|
|
6223
|
+
set.delete(handler);
|
|
6224
|
+
};
|
|
6225
|
+
}
|
|
6226
|
+
/** Subscribe to one event type across all subagents. */
|
|
6227
|
+
filter(type, handler) {
|
|
6228
|
+
let set = this.byType.get(type);
|
|
6229
|
+
if (!set) {
|
|
6230
|
+
set = /* @__PURE__ */ new Set();
|
|
6231
|
+
this.byType.set(type, set);
|
|
6232
|
+
}
|
|
6233
|
+
set.add(handler);
|
|
6234
|
+
return () => {
|
|
6235
|
+
set.delete(handler);
|
|
6236
|
+
};
|
|
6237
|
+
}
|
|
6238
|
+
/** Subscribe to literally everything. The fleet roll-up uses this. */
|
|
6239
|
+
onAny(handler) {
|
|
6240
|
+
this.any.add(handler);
|
|
6241
|
+
return () => {
|
|
6242
|
+
this.any.delete(handler);
|
|
6243
|
+
};
|
|
6244
|
+
}
|
|
6245
|
+
emit(event) {
|
|
6246
|
+
const byId = this.byId.get(event.subagentId);
|
|
6247
|
+
if (byId)
|
|
6248
|
+
for (const h of byId) {
|
|
6249
|
+
try {
|
|
6250
|
+
h(event);
|
|
6251
|
+
} catch {
|
|
6252
|
+
}
|
|
6253
|
+
}
|
|
6254
|
+
const byType = this.byType.get(event.type);
|
|
6255
|
+
if (byType)
|
|
6256
|
+
for (const h of byType) {
|
|
6257
|
+
try {
|
|
6258
|
+
h(event);
|
|
6259
|
+
} catch {
|
|
6260
|
+
}
|
|
6261
|
+
}
|
|
6262
|
+
for (const h of this.any) {
|
|
6263
|
+
try {
|
|
6264
|
+
h(event);
|
|
6265
|
+
} catch {
|
|
6266
|
+
}
|
|
6267
|
+
}
|
|
6268
|
+
}
|
|
6269
|
+
};
|
|
6270
|
+
var FleetUsageAggregator = class {
|
|
6271
|
+
constructor(bus, priceLookup, metaLookup) {
|
|
6272
|
+
this.bus = bus;
|
|
6273
|
+
this.priceLookup = priceLookup;
|
|
6274
|
+
this.metaLookup = metaLookup;
|
|
6275
|
+
bus.filter("provider.response", (e) => this.onProviderResponse(e));
|
|
6276
|
+
bus.filter("tool.executed", (e) => this.onToolExecuted(e));
|
|
6277
|
+
bus.filter("iteration.started", (e) => this.onIterationStarted(e));
|
|
6278
|
+
}
|
|
6279
|
+
bus;
|
|
6280
|
+
priceLookup;
|
|
6281
|
+
metaLookup;
|
|
6282
|
+
perSubagent = /* @__PURE__ */ new Map();
|
|
6283
|
+
total = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
|
|
6284
|
+
/** Live snapshot — safe to call from a tool's execute() body. */
|
|
6285
|
+
snapshot() {
|
|
6286
|
+
return {
|
|
6287
|
+
total: { ...this.total },
|
|
6288
|
+
perSubagent: Object.fromEntries(
|
|
6289
|
+
Array.from(this.perSubagent.entries()).map(([k, v]) => [k, { ...v }])
|
|
6290
|
+
)
|
|
6291
|
+
};
|
|
6292
|
+
}
|
|
6293
|
+
ensure(subagentId) {
|
|
6294
|
+
let snap = this.perSubagent.get(subagentId);
|
|
6295
|
+
if (!snap) {
|
|
6296
|
+
const meta = this.metaLookup?.(subagentId);
|
|
6297
|
+
snap = {
|
|
6298
|
+
subagentId,
|
|
6299
|
+
provider: meta?.provider,
|
|
6300
|
+
model: meta?.model,
|
|
6301
|
+
input: 0,
|
|
6302
|
+
output: 0,
|
|
6303
|
+
cacheRead: 0,
|
|
6304
|
+
cacheWrite: 0,
|
|
6305
|
+
cost: 0,
|
|
6306
|
+
toolCalls: 0,
|
|
6307
|
+
iterations: 0,
|
|
6308
|
+
startedAt: Date.now(),
|
|
6309
|
+
lastEventAt: Date.now()
|
|
6310
|
+
};
|
|
6311
|
+
this.perSubagent.set(subagentId, snap);
|
|
6312
|
+
}
|
|
6313
|
+
return snap;
|
|
6314
|
+
}
|
|
6315
|
+
onProviderResponse(e) {
|
|
6316
|
+
const snap = this.ensure(e.subagentId);
|
|
6317
|
+
const p = e.payload;
|
|
6318
|
+
const usage = p?.usage;
|
|
6319
|
+
if (!usage) return;
|
|
6320
|
+
snap.input += usage.input ?? 0;
|
|
6321
|
+
snap.output += usage.output ?? 0;
|
|
6322
|
+
snap.cacheRead += usage.cacheRead ?? 0;
|
|
6323
|
+
snap.cacheWrite += usage.cacheWrite ?? 0;
|
|
6324
|
+
this.total.input += usage.input ?? 0;
|
|
6325
|
+
this.total.output += usage.output ?? 0;
|
|
6326
|
+
this.total.cacheRead += usage.cacheRead ?? 0;
|
|
6327
|
+
this.total.cacheWrite += usage.cacheWrite ?? 0;
|
|
6328
|
+
const price = this.priceLookup?.(e.subagentId);
|
|
6329
|
+
if (price) {
|
|
6330
|
+
const delta = (usage.input ?? 0) / 1e6 * (price.input ?? 0) + (usage.output ?? 0) / 1e6 * (price.output ?? 0) + (usage.cacheRead ?? 0) / 1e6 * (price.cacheRead ?? 0) + (usage.cacheWrite ?? 0) / 1e6 * (price.cacheWrite ?? 0);
|
|
6331
|
+
snap.cost += delta;
|
|
6332
|
+
this.total.cost += delta;
|
|
6333
|
+
}
|
|
6334
|
+
snap.lastEventAt = e.ts;
|
|
6335
|
+
}
|
|
6336
|
+
onToolExecuted(e) {
|
|
6337
|
+
const snap = this.ensure(e.subagentId);
|
|
6338
|
+
snap.toolCalls += 1;
|
|
6339
|
+
snap.lastEventAt = e.ts;
|
|
6340
|
+
}
|
|
6341
|
+
onIterationStarted(e) {
|
|
6342
|
+
const snap = this.ensure(e.subagentId);
|
|
6343
|
+
snap.iterations += 1;
|
|
6344
|
+
snap.lastEventAt = e.ts;
|
|
6345
|
+
}
|
|
6346
|
+
};
|
|
6347
|
+
|
|
6348
|
+
// src/coordination/subagent-budget.ts
|
|
6349
|
+
var BudgetExceededError = class extends Error {
|
|
6350
|
+
kind;
|
|
6351
|
+
limit;
|
|
6352
|
+
observed;
|
|
6353
|
+
constructor(kind, limit, observed) {
|
|
6354
|
+
super(`Budget exceeded: ${kind} (limit=${limit}, observed=${observed})`);
|
|
6355
|
+
this.name = "BudgetExceededError";
|
|
6356
|
+
this.kind = kind;
|
|
6357
|
+
this.limit = limit;
|
|
6358
|
+
this.observed = observed;
|
|
6359
|
+
}
|
|
6360
|
+
};
|
|
6361
|
+
var SubagentBudget = class {
|
|
6362
|
+
limits;
|
|
6363
|
+
iterations = 0;
|
|
6364
|
+
toolCalls = 0;
|
|
6365
|
+
tokenInput = 0;
|
|
6366
|
+
tokenOutput = 0;
|
|
6367
|
+
costUsd = 0;
|
|
6368
|
+
startTime = null;
|
|
6369
|
+
constructor(limits = {}) {
|
|
6370
|
+
this.limits = Object.freeze({ ...limits });
|
|
6371
|
+
}
|
|
6372
|
+
start() {
|
|
6373
|
+
this.startTime = Date.now();
|
|
6374
|
+
}
|
|
6375
|
+
recordIteration() {
|
|
6376
|
+
this.iterations++;
|
|
6377
|
+
if (this.limits.maxIterations !== void 0 && this.iterations > this.limits.maxIterations) {
|
|
6378
|
+
throw new BudgetExceededError("iterations", this.limits.maxIterations, this.iterations);
|
|
6379
|
+
}
|
|
6380
|
+
}
|
|
6381
|
+
recordToolCall() {
|
|
6382
|
+
this.toolCalls++;
|
|
6383
|
+
if (this.limits.maxToolCalls !== void 0 && this.toolCalls > this.limits.maxToolCalls) {
|
|
6384
|
+
throw new BudgetExceededError("tool_calls", this.limits.maxToolCalls, this.toolCalls);
|
|
6385
|
+
}
|
|
6386
|
+
}
|
|
6387
|
+
recordUsage(usage, costUsd = 0) {
|
|
6388
|
+
this.tokenInput += usage.input;
|
|
6389
|
+
this.tokenOutput += usage.output;
|
|
6390
|
+
this.costUsd += costUsd;
|
|
6391
|
+
const totalTokens = this.tokenInput + this.tokenOutput;
|
|
6392
|
+
if (this.limits.maxTokens !== void 0 && totalTokens > this.limits.maxTokens) {
|
|
6393
|
+
throw new BudgetExceededError("tokens", this.limits.maxTokens, totalTokens);
|
|
6394
|
+
}
|
|
6395
|
+
if (this.limits.maxCostUsd !== void 0 && this.costUsd > this.limits.maxCostUsd) {
|
|
6396
|
+
throw new BudgetExceededError("cost", this.limits.maxCostUsd, this.costUsd);
|
|
6397
|
+
}
|
|
6398
|
+
}
|
|
6399
|
+
/**
|
|
6400
|
+
* Throws if the wall-clock budget is exhausted. Call this from the iteration
|
|
6401
|
+
* loop so a hung tool can't keep a subagent running past its deadline.
|
|
6402
|
+
*/
|
|
6403
|
+
checkTimeout() {
|
|
6404
|
+
if (this.startTime === null || this.limits.timeoutMs === void 0) return;
|
|
6405
|
+
const elapsed = Date.now() - this.startTime;
|
|
6406
|
+
if (elapsed > this.limits.timeoutMs) {
|
|
6407
|
+
throw new BudgetExceededError("timeout", this.limits.timeoutMs, elapsed);
|
|
6408
|
+
}
|
|
6409
|
+
}
|
|
6410
|
+
/** Returns true if a timeout has occurred without throwing. Useful for races. */
|
|
6411
|
+
isTimedOut() {
|
|
6412
|
+
if (this.startTime === null || this.limits.timeoutMs === void 0) return false;
|
|
6413
|
+
return Date.now() - this.startTime > this.limits.timeoutMs;
|
|
6414
|
+
}
|
|
6415
|
+
usage() {
|
|
6416
|
+
return {
|
|
6417
|
+
iterations: this.iterations,
|
|
6418
|
+
toolCalls: this.toolCalls,
|
|
6419
|
+
tokens: {
|
|
6420
|
+
input: this.tokenInput,
|
|
6421
|
+
output: this.tokenOutput,
|
|
6422
|
+
total: this.tokenInput + this.tokenOutput
|
|
6423
|
+
},
|
|
6424
|
+
costUsd: this.costUsd,
|
|
6425
|
+
elapsedMs: this.startTime === null ? 0 : Date.now() - this.startTime
|
|
6426
|
+
};
|
|
6427
|
+
}
|
|
6428
|
+
};
|
|
6429
|
+
|
|
6430
|
+
// src/coordination/multi-agent-coordinator.ts
|
|
6431
|
+
var DefaultMultiAgentCoordinator = class extends EventEmitter {
|
|
6432
|
+
coordinatorId;
|
|
6048
6433
|
config;
|
|
6049
6434
|
runner;
|
|
6050
6435
|
subagents = /* @__PURE__ */ new Map();
|
|
@@ -9697,348 +10082,260 @@ var sentinelServer = () => ({
|
|
|
9697
10082
|
// security tool — require explicit confirmation
|
|
9698
10083
|
});
|
|
9699
10084
|
var allServers = () => ({
|
|
9700
|
-
filesystem: { ...filesystemServer(), enabled: false },
|
|
9701
|
-
github: { ...githubServer(), enabled: false },
|
|
9702
|
-
context7: { ...context7Server(), enabled: false },
|
|
9703
|
-
"brave-search": { ...braveSearchServer(), enabled: false },
|
|
9704
|
-
block: { ...blockServer(), enabled: false },
|
|
9705
|
-
everart: { ...everArtServer(), enabled: false },
|
|
9706
|
-
slack: { ...slackServer(), enabled: false },
|
|
9707
|
-
aws: { ...awsServer(), enabled: false },
|
|
9708
|
-
"google-maps": { ...googleMapsServer(), enabled: false },
|
|
9709
|
-
sentinel: { ...sentinelServer(), enabled: false }
|
|
9710
|
-
});
|
|
9711
|
-
|
|
9712
|
-
// src/
|
|
9713
|
-
|
|
9714
|
-
|
|
9715
|
-
|
|
9716
|
-
|
|
9717
|
-
|
|
9718
|
-
|
|
9719
|
-
|
|
9720
|
-
|
|
9721
|
-
|
|
9722
|
-
|
|
9723
|
-
|
|
9724
|
-
|
|
9725
|
-
|
|
9726
|
-
|
|
9727
|
-
|
|
9728
|
-
|
|
10085
|
+
filesystem: { ...filesystemServer(), enabled: false },
|
|
10086
|
+
github: { ...githubServer(), enabled: false },
|
|
10087
|
+
context7: { ...context7Server(), enabled: false },
|
|
10088
|
+
"brave-search": { ...braveSearchServer(), enabled: false },
|
|
10089
|
+
block: { ...blockServer(), enabled: false },
|
|
10090
|
+
everart: { ...everArtServer(), enabled: false },
|
|
10091
|
+
slack: { ...slackServer(), enabled: false },
|
|
10092
|
+
aws: { ...awsServer(), enabled: false },
|
|
10093
|
+
"google-maps": { ...googleMapsServer(), enabled: false },
|
|
10094
|
+
sentinel: { ...sentinelServer(), enabled: false }
|
|
10095
|
+
});
|
|
10096
|
+
|
|
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);
|
|
9729
10116
|
};
|
|
9730
|
-
|
|
9731
|
-
|
|
9732
|
-
|
|
9733
|
-
|
|
9734
|
-
|
|
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);
|
|
9735
10131
|
}
|
|
9736
|
-
};
|
|
9737
|
-
events.emit("iteration.limit_reached", {
|
|
9738
|
-
currentIterations,
|
|
9739
|
-
currentLimit,
|
|
9740
|
-
grant,
|
|
9741
|
-
deny
|
|
9742
|
-
});
|
|
9743
|
-
if (autoExtend) {
|
|
9744
|
-
setImmediate(() => {
|
|
9745
|
-
if (!resolved) {
|
|
9746
|
-
resolved = true;
|
|
9747
|
-
clearTimeout(timer);
|
|
9748
|
-
resolve4(100);
|
|
9749
|
-
}
|
|
9750
|
-
});
|
|
9751
10132
|
}
|
|
9752
|
-
|
|
9753
|
-
}
|
|
9754
|
-
|
|
9755
|
-
|
|
9756
|
-
|
|
9757
|
-
|
|
9758
|
-
|
|
9759
|
-
|
|
9760
|
-
|
|
9761
|
-
|
|
9762
|
-
|
|
9763
|
-
|
|
9764
|
-
|
|
9765
|
-
|
|
9766
|
-
|
|
9767
|
-
|
|
9768
|
-
|
|
9769
|
-
|
|
9770
|
-
|
|
9771
|
-
|
|
9772
|
-
|
|
9773
|
-
const tb = state.tools.get(b.id);
|
|
9774
|
-
if (tb) {
|
|
9775
|
-
const block = {
|
|
9776
|
-
type: "tool_use",
|
|
9777
|
-
id: b.id,
|
|
9778
|
-
name: tb.name,
|
|
9779
|
-
input: tb.input ?? {}
|
|
9780
|
-
};
|
|
9781
|
-
if (tb.providerMeta && Object.keys(tb.providerMeta).length > 0) {
|
|
9782
|
-
block.providerMeta = tb.providerMeta;
|
|
9783
|
-
}
|
|
9784
|
-
content.push(block);
|
|
9785
|
-
}
|
|
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 }
|
|
10153
|
+
});
|
|
9786
10154
|
}
|
|
10155
|
+
this.extensions.push(ext);
|
|
10156
|
+
return () => this.unregister(ext.name);
|
|
9787
10157
|
}
|
|
9788
|
-
|
|
9789
|
-
|
|
9790
|
-
|
|
9791
|
-
|
|
9792
|
-
|
|
9793
|
-
|
|
9794
|
-
|
|
9795
|
-
|
|
9796
|
-
textBuffers: [],
|
|
9797
|
-
currentTextIndex: -1,
|
|
9798
|
-
tools: /* @__PURE__ */ new Map(),
|
|
9799
|
-
thinking: [],
|
|
9800
|
-
currentThinkingIndex: -1,
|
|
9801
|
-
blockOrder: []
|
|
9802
|
-
};
|
|
9803
|
-
}
|
|
9804
|
-
function handleMessageStart(state, model) {
|
|
9805
|
-
state.model = model;
|
|
9806
|
-
}
|
|
9807
|
-
function handleContentBlockStart(state, ev) {
|
|
9808
|
-
const kind = ev.kind ?? "text";
|
|
9809
|
-
if (kind === "text") {
|
|
9810
|
-
state.currentTextIndex = state.textBuffers.length;
|
|
9811
|
-
state.textBuffers.push("");
|
|
9812
|
-
state.blockOrder.push({ kind: "text", idx: state.currentTextIndex });
|
|
9813
|
-
} else if (kind === "tool_use") {
|
|
9814
|
-
const id = ev.id ?? crypto.randomUUID();
|
|
9815
|
-
state.tools.set(id, { name: ev.name ?? "unknown", partial: "" });
|
|
9816
|
-
state.blockOrder.push({ kind: "tool", id });
|
|
9817
|
-
state.currentTextIndex = -1;
|
|
9818
|
-
} else if (kind === "thinking") {
|
|
9819
|
-
state.currentThinkingIndex = state.thinking.length;
|
|
9820
|
-
state.thinking.push({
|
|
9821
|
-
textBuf: "",
|
|
9822
|
-
...ev.providerMeta ? { providerMeta: ev.providerMeta } : {}
|
|
9823
|
-
});
|
|
9824
|
-
state.blockOrder.push({ kind: "thinking", idx: state.currentThinkingIndex });
|
|
9825
|
-
state.currentTextIndex = -1;
|
|
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);
|
|
9826
10166
|
}
|
|
9827
|
-
|
|
9828
|
-
|
|
9829
|
-
|
|
9830
|
-
|
|
9831
|
-
|
|
9832
|
-
|
|
9833
|
-
|
|
9834
|
-
|
|
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;
|
|
9835
10175
|
}
|
|
9836
|
-
|
|
9837
|
-
|
|
9838
|
-
|
|
9839
|
-
|
|
9840
|
-
|
|
9841
|
-
state.blockOrder.push({ kind: "tool", id: ev.id });
|
|
9842
|
-
}
|
|
9843
|
-
function handleToolUseInputDelta(state, ev) {
|
|
9844
|
-
const t2 = state.tools.get(ev.id);
|
|
9845
|
-
if (t2) t2.partial += ev.partial;
|
|
9846
|
-
}
|
|
9847
|
-
function safeJsonOrRaw(s) {
|
|
9848
|
-
if (!s) return {};
|
|
9849
|
-
try {
|
|
9850
|
-
return JSON.parse(s);
|
|
9851
|
-
} catch {
|
|
9852
|
-
return { _raw: s };
|
|
10176
|
+
/**
|
|
10177
|
+
* List registered extension names in order.
|
|
10178
|
+
*/
|
|
10179
|
+
list() {
|
|
10180
|
+
return this.extensions.map((e) => e.name);
|
|
9853
10181
|
}
|
|
9854
|
-
|
|
9855
|
-
|
|
9856
|
-
|
|
9857
|
-
|
|
9858
|
-
|
|
9859
|
-
if (ev.providerMeta) t2.providerMeta = ev.providerMeta;
|
|
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);
|
|
9860
10187
|
}
|
|
9861
|
-
|
|
9862
|
-
|
|
9863
|
-
|
|
9864
|
-
|
|
9865
|
-
|
|
9866
|
-
|
|
9867
|
-
...ev.providerMeta ? { providerMeta: ev.providerMeta } : {}
|
|
9868
|
-
});
|
|
9869
|
-
state.blockOrder.push({ kind: "thinking", idx: state.currentThinkingIndex });
|
|
9870
|
-
state.currentTextIndex = -1;
|
|
9871
|
-
}
|
|
9872
|
-
function handleThinkingDelta(state, text) {
|
|
9873
|
-
if (state.currentThinkingIndex === -1) {
|
|
9874
|
-
handleThinkingStart(state, {});
|
|
10188
|
+
/**
|
|
10189
|
+
* Remove all registered extensions and contributors.
|
|
10190
|
+
*/
|
|
10191
|
+
clear() {
|
|
10192
|
+
this.extensions.length = 0;
|
|
10193
|
+
this.promptContributors.length = 0;
|
|
9875
10194
|
}
|
|
9876
|
-
|
|
9877
|
-
|
|
9878
|
-
|
|
9879
|
-
|
|
9880
|
-
|
|
9881
|
-
|
|
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);
|
|
10203
|
+
}
|
|
10204
|
+
}
|
|
9882
10205
|
}
|
|
9883
|
-
|
|
9884
|
-
|
|
9885
|
-
|
|
9886
|
-
|
|
9887
|
-
|
|
9888
|
-
}
|
|
9889
|
-
|
|
9890
|
-
state.stopReason = ev.stopReason ?? "end_turn";
|
|
9891
|
-
state.usage = ev.usage ?? { input: 0, output: 0 };
|
|
9892
|
-
}
|
|
9893
|
-
async function streamProviderToResponse(provider, req, signal, ctx, events) {
|
|
9894
|
-
const state = createStreamingState(req.model);
|
|
9895
|
-
const iter = provider.stream(req, { signal })[Symbol.asyncIterator]();
|
|
9896
|
-
try {
|
|
9897
|
-
for (; ; ) {
|
|
9898
|
-
const next = await iter.next();
|
|
9899
|
-
if (next.done) break;
|
|
9900
|
-
const ev = next.value;
|
|
9901
|
-
switch (ev.type) {
|
|
9902
|
-
case "message_start":
|
|
9903
|
-
handleMessageStart(state, ev.model);
|
|
9904
|
-
break;
|
|
9905
|
-
case "content_block_start":
|
|
9906
|
-
handleContentBlockStart(state, ev);
|
|
9907
|
-
break;
|
|
9908
|
-
case "content_block_stop":
|
|
9909
|
-
handleContentBlockStop(state, ev);
|
|
9910
|
-
break;
|
|
9911
|
-
case "text_delta":
|
|
9912
|
-
handleTextDelta(state, ev.text);
|
|
9913
|
-
events.emit("provider.text_delta", { ctx, text: ev.text });
|
|
9914
|
-
break;
|
|
9915
|
-
case "tool_use_start":
|
|
9916
|
-
handleToolUseStart(state, ev);
|
|
9917
|
-
events.emit("provider.tool_use_start", { ctx, id: ev.id, name: ev.name });
|
|
9918
|
-
break;
|
|
9919
|
-
case "tool_use_input_delta":
|
|
9920
|
-
handleToolUseInputDelta(state, ev);
|
|
9921
|
-
break;
|
|
9922
|
-
case "tool_use_stop":
|
|
9923
|
-
handleToolUseStop(state, ev);
|
|
9924
|
-
events.emit("provider.tool_use_stop", { ctx, id: ev.id });
|
|
9925
|
-
break;
|
|
9926
|
-
case "thinking_start":
|
|
9927
|
-
handleThinkingStart(state, ev);
|
|
9928
|
-
break;
|
|
9929
|
-
case "thinking_delta":
|
|
9930
|
-
handleThinkingDelta(state, ev.text);
|
|
9931
|
-
events.emit("provider.thinking_delta", { ctx, text: ev.text });
|
|
9932
|
-
break;
|
|
9933
|
-
case "thinking_signature":
|
|
9934
|
-
handleThinkingSignature(state, ev.signature);
|
|
9935
|
-
break;
|
|
9936
|
-
case "thinking_stop":
|
|
9937
|
-
handleThinkingStop(state);
|
|
9938
|
-
break;
|
|
9939
|
-
case "message_stop":
|
|
9940
|
-
handleMessageStop(state, ev);
|
|
9941
|
-
break;
|
|
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);
|
|
9942
10213
|
}
|
|
9943
10214
|
}
|
|
9944
|
-
}
|
|
9945
|
-
|
|
9946
|
-
|
|
9947
|
-
|
|
10215
|
+
}
|
|
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
|
+
}
|
|
9948
10224
|
}
|
|
9949
|
-
|
|
9950
|
-
|
|
9951
|
-
|
|
9952
|
-
|
|
10225
|
+
}
|
|
10226
|
+
async runAfterIteration(...args) {
|
|
10227
|
+
for (const ext of this.extensions) {
|
|
10228
|
+
if (!ext.afterIteration) continue;
|
|
9953
10229
|
try {
|
|
9954
|
-
await
|
|
9955
|
-
|
|
9956
|
-
|
|
9957
|
-
drainTimer = setTimeout(resolve4, 500);
|
|
9958
|
-
})
|
|
9959
|
-
]);
|
|
9960
|
-
} finally {
|
|
9961
|
-
if (drainTimer) clearTimeout(drainTimer);
|
|
10230
|
+
await ext.afterIteration(...args);
|
|
10231
|
+
} catch (err) {
|
|
10232
|
+
this.log?.error(`Extension "${ext.name}" afterIteration hook failed`, err);
|
|
9962
10233
|
}
|
|
9963
|
-
} catch {
|
|
9964
10234
|
}
|
|
9965
10235
|
}
|
|
9966
|
-
|
|
9967
|
-
|
|
9968
|
-
|
|
9969
|
-
|
|
9970
|
-
async
|
|
9971
|
-
|
|
9972
|
-
|
|
9973
|
-
|
|
9974
|
-
|
|
9975
|
-
|
|
9976
|
-
|
|
9977
|
-
|
|
9978
|
-
|
|
9979
|
-
}
|
|
9980
|
-
|
|
9981
|
-
|
|
9982
|
-
|
|
9983
|
-
|
|
9984
|
-
|
|
9985
|
-
|
|
9986
|
-
|
|
9987
|
-
|
|
9988
|
-
|
|
9989
|
-
|
|
9990
|
-
|
|
9991
|
-
const
|
|
9992
|
-
const
|
|
9993
|
-
|
|
9994
|
-
|
|
9995
|
-
|
|
9996
|
-
|
|
9997
|
-
|
|
9998
|
-
|
|
9999
|
-
status: err.status,
|
|
10000
|
-
description,
|
|
10001
|
-
retryable: false
|
|
10002
|
-
});
|
|
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
|
+
}
|
|
10250
|
+
}
|
|
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;
|
|
10003
10269
|
}
|
|
10004
|
-
|
|
10270
|
+
};
|
|
10271
|
+
}
|
|
10272
|
+
return composed;
|
|
10273
|
+
}
|
|
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);
|
|
10005
10282
|
}
|
|
10006
|
-
|
|
10007
|
-
|
|
10008
|
-
|
|
10009
|
-
|
|
10010
|
-
|
|
10011
|
-
|
|
10012
|
-
|
|
10013
|
-
|
|
10014
|
-
|
|
10015
|
-
|
|
10016
|
-
});
|
|
10283
|
+
}
|
|
10284
|
+
return toolUses;
|
|
10285
|
+
}
|
|
10286
|
+
async runAfterToolExecution(...args) {
|
|
10287
|
+
for (const ext of this.extensions) {
|
|
10288
|
+
if (!ext.afterToolExecution) continue;
|
|
10289
|
+
try {
|
|
10290
|
+
await ext.afterToolExecution(...args);
|
|
10291
|
+
} catch (err) {
|
|
10292
|
+
this.log?.error(`Extension "${ext.name}" afterToolExecution hook failed`, err);
|
|
10017
10293
|
}
|
|
10018
|
-
|
|
10019
|
-
|
|
10020
|
-
|
|
10021
|
-
|
|
10022
|
-
|
|
10023
|
-
|
|
10024
|
-
|
|
10025
|
-
|
|
10026
|
-
|
|
10027
|
-
|
|
10028
|
-
|
|
10029
|
-
|
|
10030
|
-
|
|
10031
|
-
|
|
10032
|
-
|
|
10033
|
-
|
|
10034
|
-
|
|
10035
|
-
|
|
10294
|
+
}
|
|
10295
|
+
}
|
|
10296
|
+
};
|
|
10297
|
+
|
|
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);
|
|
10307
|
+
}
|
|
10308
|
+
}, timeoutMs);
|
|
10309
|
+
const deny = () => {
|
|
10310
|
+
if (!resolved) {
|
|
10311
|
+
resolved = true;
|
|
10312
|
+
clearTimeout(timer);
|
|
10313
|
+
resolve4(0);
|
|
10314
|
+
}
|
|
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);
|
|
10036
10335
|
}
|
|
10037
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
10038
10336
|
});
|
|
10039
|
-
attempt++;
|
|
10040
10337
|
}
|
|
10041
|
-
}
|
|
10338
|
+
});
|
|
10042
10339
|
}
|
|
10043
10340
|
|
|
10044
10341
|
// src/core/agent.ts
|
|
@@ -10075,6 +10372,7 @@ var Agent = class {
|
|
|
10075
10372
|
toolExecutor;
|
|
10076
10373
|
autoExtendLimit;
|
|
10077
10374
|
tracer;
|
|
10375
|
+
extensions;
|
|
10078
10376
|
constructor(init) {
|
|
10079
10377
|
this.container = init.container;
|
|
10080
10378
|
this.tools = init.tools;
|
|
@@ -10088,6 +10386,8 @@ var Agent = class {
|
|
|
10088
10386
|
this.perIterationOutputCapBytes = init.perIterationOutputCapBytes ?? 1e5;
|
|
10089
10387
|
this.autoExtendLimit = init.autoExtendLimit ?? true;
|
|
10090
10388
|
this.tracer = init.tracer;
|
|
10389
|
+
this.extensions = init.extensions ?? new ExtensionRegistry();
|
|
10390
|
+
this.extensions.setLogger(this.container.resolve(TOKENS.Logger));
|
|
10091
10391
|
this.toolExecutor = new ToolExecutor(this.tools, {
|
|
10092
10392
|
permissionPolicy: init.permissionPolicy ?? this.permission,
|
|
10093
10393
|
secretScrubber: this.scrubber,
|
|
@@ -10133,33 +10433,66 @@ var Agent = class {
|
|
|
10133
10433
|
"agent.model": opts.model ?? this.ctx.model,
|
|
10134
10434
|
"agent.executionStrategy": opts.executionStrategy ?? this.executionStrategy
|
|
10135
10435
|
});
|
|
10436
|
+
const { blocks, text } = normalizeInput(userInput);
|
|
10437
|
+
const inputPayload = { content: blocks, text, ctx: this.ctx };
|
|
10438
|
+
await this.extensions.runBeforeRun(this.ctx, inputPayload);
|
|
10136
10439
|
try {
|
|
10137
|
-
const result = await this.runInner(
|
|
10440
|
+
const result = await this.runInner(inputPayload, opts, controller);
|
|
10138
10441
|
span?.setAttribute("agent.status", result.status);
|
|
10139
10442
|
span?.setAttribute("agent.iterations", result.iterations);
|
|
10443
|
+
await this.extensions.runAfterRun(this.ctx, result);
|
|
10140
10444
|
return result;
|
|
10141
10445
|
} catch (err) {
|
|
10142
10446
|
const wse = err instanceof AgentError ? err : toWrongStackError(err);
|
|
10143
10447
|
this.events.emit("error", { err: toError(err), phase: "agent" });
|
|
10144
10448
|
if (err instanceof Error) span?.recordError(err);
|
|
10145
10449
|
span?.setAttribute("agent.status", "failed");
|
|
10146
|
-
|
|
10450
|
+
const result = {
|
|
10147
10451
|
status: signal.aborted ? "aborted" : "failed",
|
|
10148
10452
|
iterations: 0,
|
|
10149
10453
|
error: wse
|
|
10150
10454
|
};
|
|
10455
|
+
await this.extensions.runAfterRun(this.ctx, result);
|
|
10456
|
+
return result;
|
|
10151
10457
|
} finally {
|
|
10152
10458
|
span?.end();
|
|
10153
10459
|
await controller.dispose();
|
|
10154
10460
|
}
|
|
10155
10461
|
}
|
|
10156
|
-
async runInner(
|
|
10157
|
-
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
|
+
});
|
|
10158
10470
|
let finalText = "";
|
|
10159
10471
|
let iterations = 0;
|
|
10160
10472
|
let effectiveLimit = opts.maxIterations ?? this.maxIterations;
|
|
10161
10473
|
const hasHardLimit = effectiveLimit > 0 && Number.isFinite(effectiveLimit);
|
|
10162
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);
|
|
10163
10496
|
for (let i = 0; ; i++) {
|
|
10164
10497
|
iterations = i + 1;
|
|
10165
10498
|
if (controller.signal.aborted) {
|
|
@@ -10175,26 +10508,39 @@ var Agent = class {
|
|
|
10175
10508
|
if (limitCheck.exit) {
|
|
10176
10509
|
return { ...limitCheck.exit, finalText };
|
|
10177
10510
|
}
|
|
10511
|
+
await this.extensions.runBeforeIteration(this.ctx, i);
|
|
10178
10512
|
this.events.emit("iteration.started", { ctx: this.ctx, index: i });
|
|
10179
10513
|
const req = await this.buildAndRunRequestPipeline(opts);
|
|
10180
10514
|
let res;
|
|
10181
10515
|
try {
|
|
10182
|
-
res = await
|
|
10183
|
-
provider: this.ctx.provider,
|
|
10184
|
-
request: req,
|
|
10185
|
-
signal: controller.signal,
|
|
10186
|
-
ctx: this.ctx,
|
|
10187
|
-
events: this.events,
|
|
10188
|
-
retry: this.retry,
|
|
10189
|
-
logger: this.logger,
|
|
10190
|
-
tracer: this.tracer
|
|
10191
|
-
});
|
|
10516
|
+
res = await customRunner(this.ctx, req);
|
|
10192
10517
|
recoveryRetries = 0;
|
|
10193
10518
|
} catch (err) {
|
|
10194
10519
|
if (controller.signal.aborted) {
|
|
10195
10520
|
this.events.emit("error", { err: toError(err), phase: "provider" });
|
|
10196
10521
|
return { status: "aborted", iterations, error: toWrongStackError(err, "AGENT_ABORTED") };
|
|
10197
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
|
+
}
|
|
10198
10544
|
const recovered = await this.errorHandler.recover(err, this.ctx);
|
|
10199
10545
|
if (!recovered || recovered.action === "fail") {
|
|
10200
10546
|
this.events.emit("error", { err: toError(err), phase: "provider" });
|
|
@@ -10233,21 +10579,9 @@ var Agent = class {
|
|
|
10233
10579
|
await this.executeTools(toolUses);
|
|
10234
10580
|
this.events.emit("iteration.completed", { ctx: this.ctx, index: i });
|
|
10235
10581
|
await this.compactContextIfNeeded();
|
|
10582
|
+
await this.extensions.runAfterIteration(this.ctx, i);
|
|
10236
10583
|
}
|
|
10237
10584
|
}
|
|
10238
|
-
/**
|
|
10239
|
-
* Normalize user input and emit through userInput pipeline + session append.
|
|
10240
|
-
*/
|
|
10241
|
-
async normalizeAndEmitUserInput(userInput) {
|
|
10242
|
-
const { blocks, text } = normalizeInput(userInput);
|
|
10243
|
-
await this.pipelines.userInput.run({ content: blocks, text, ctx: this.ctx });
|
|
10244
|
-
this.ctx.state.appendMessage({ role: "user", content: blocks });
|
|
10245
|
-
await this.ctx.session.append({
|
|
10246
|
-
type: "user_input",
|
|
10247
|
-
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10248
|
-
content: blocks
|
|
10249
|
-
});
|
|
10250
|
-
}
|
|
10251
10585
|
/**
|
|
10252
10586
|
* Check if iteration limit has been reached and request extension if needed.
|
|
10253
10587
|
* Returns the new effective limit (possibly extended) and a RunResult if
|
|
@@ -10331,6 +10665,7 @@ var Agent = class {
|
|
|
10331
10665
|
* single tool.
|
|
10332
10666
|
*/
|
|
10333
10667
|
async executeTools(toolUses) {
|
|
10668
|
+
toolUses = await this.extensions.runBeforeToolExecution(this.ctx, toolUses);
|
|
10334
10669
|
const { outputs } = await this.toolExecutor.executeBatch(
|
|
10335
10670
|
toolUses,
|
|
10336
10671
|
this.ctx,
|
|
@@ -10416,6 +10751,7 @@ var Agent = class {
|
|
|
10416
10751
|
role: "user",
|
|
10417
10752
|
content: outputs.map((o) => o.result)
|
|
10418
10753
|
});
|
|
10754
|
+
await this.extensions.runAfterToolExecution(this.ctx, outputs);
|
|
10419
10755
|
}
|
|
10420
10756
|
waitForConfirm(info) {
|
|
10421
10757
|
return new Promise((resolve4) => {
|
|
@@ -10870,6 +11206,15 @@ var DefaultSystemPromptBuilder = class {
|
|
|
10870
11206
|
cache_control: { type: "ephemeral" }
|
|
10871
11207
|
});
|
|
10872
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
|
+
}
|
|
10873
11218
|
return blocks;
|
|
10874
11219
|
}
|
|
10875
11220
|
/**
|
|
@@ -10911,12 +11256,38 @@ var DefaultSystemPromptBuilder = class {
|
|
|
10911
11256
|
}
|
|
10912
11257
|
buildToolUsage(tools) {
|
|
10913
11258
|
if (tools.length === 0) return "## Tool usage\n\nNo tools registered.";
|
|
10914
|
-
const
|
|
11259
|
+
const byCat = /* @__PURE__ */ new Map();
|
|
11260
|
+
const uncategorized = [];
|
|
10915
11261
|
for (const t2 of tools) {
|
|
10916
|
-
|
|
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) {
|
|
10917
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(`
|
|
10918
11288
|
### ${t2.name}
|
|
10919
11289
|
${hint.trim()}`);
|
|
11290
|
+
}
|
|
10920
11291
|
}
|
|
10921
11292
|
lines.push(`
|
|
10922
11293
|
## Common patterns
|
|
@@ -11188,6 +11559,21 @@ var ToolRegistry = class {
|
|
|
11188
11559
|
this.tools.set(tool.name, { tool, owner });
|
|
11189
11560
|
return true;
|
|
11190
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
|
+
}
|
|
11191
11577
|
/**
|
|
11192
11578
|
* Register a tool as a default. If the tool name is already registered,
|
|
11193
11579
|
* this is a no-op — the existing registration (from core or another
|
|
@@ -11215,6 +11601,30 @@ var ToolRegistry = class {
|
|
|
11215
11601
|
}
|
|
11216
11602
|
this.tools.set(name, { tool, owner });
|
|
11217
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
|
+
}
|
|
11218
11628
|
get(name) {
|
|
11219
11629
|
return this.tools.get(name)?.tool;
|
|
11220
11630
|
}
|
|
@@ -11224,6 +11634,24 @@ var ToolRegistry = class {
|
|
|
11224
11634
|
list() {
|
|
11225
11635
|
return Array.from(this.tools.values()).map((e) => e.tool);
|
|
11226
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
|
+
}
|
|
11227
11655
|
listWithOwner() {
|
|
11228
11656
|
return Array.from(this.tools.values());
|
|
11229
11657
|
}
|
|
@@ -11243,6 +11671,12 @@ var ProviderRegistry = class {
|
|
|
11243
11671
|
register(f) {
|
|
11244
11672
|
this.factories.set(f.type, f);
|
|
11245
11673
|
}
|
|
11674
|
+
/**
|
|
11675
|
+
* Bulk-register multiple provider factories at once.
|
|
11676
|
+
*/
|
|
11677
|
+
registerAll(factories) {
|
|
11678
|
+
for (const f of factories) this.register(f);
|
|
11679
|
+
}
|
|
11246
11680
|
/**
|
|
11247
11681
|
* Override an existing factory. Throws if no factory is registered
|
|
11248
11682
|
* for the given type. Use this to safely replace a provider at runtime
|
|
@@ -11311,6 +11745,12 @@ var SlashCommandRegistry = class {
|
|
|
11311
11745
|
}
|
|
11312
11746
|
return this.cmds.delete(name);
|
|
11313
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
|
+
}
|
|
11314
11754
|
get(name) {
|
|
11315
11755
|
return this.cmds.get(name)?.cmd;
|
|
11316
11756
|
}
|
|
@@ -11383,15 +11823,23 @@ var DefaultPluginAPI = class {
|
|
|
11383
11823
|
providers;
|
|
11384
11824
|
mcp;
|
|
11385
11825
|
slashCommands;
|
|
11826
|
+
extensions;
|
|
11827
|
+
session;
|
|
11828
|
+
metrics;
|
|
11386
11829
|
config;
|
|
11387
11830
|
log;
|
|
11831
|
+
configStore;
|
|
11388
11832
|
pluginCleanupFns = [];
|
|
11389
11833
|
constructor(init) {
|
|
11390
11834
|
const owner = init.ownerName;
|
|
11391
11835
|
this.container = init.container;
|
|
11392
11836
|
this.events = init.events;
|
|
11393
11837
|
this.config = init.config;
|
|
11838
|
+
this.configStore = init.configStore;
|
|
11394
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;
|
|
11395
11843
|
const pipelines = init.pipelines;
|
|
11396
11844
|
const readonlyPipelines = {};
|
|
11397
11845
|
for (const [key, pipeline] of Object.entries(pipelines)) {
|
|
@@ -11402,6 +11850,7 @@ var DefaultPluginAPI = class {
|
|
|
11402
11850
|
this.tools = {
|
|
11403
11851
|
register: (t2) => tr.register(t2, owner),
|
|
11404
11852
|
unregister: (name) => tr.unregister(name),
|
|
11853
|
+
wrap: (name, wrapper) => tr.wrap(name, wrapper, owner),
|
|
11405
11854
|
get: (name) => tr.get(name),
|
|
11406
11855
|
list: () => tr.list()
|
|
11407
11856
|
};
|
|
@@ -11425,6 +11874,19 @@ var DefaultPluginAPI = class {
|
|
|
11425
11874
|
this.pluginCleanupFns.push(off);
|
|
11426
11875
|
return off;
|
|
11427
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
|
+
}
|
|
11428
11890
|
/** Called by the plugin loader when uninstalling the plugin. */
|
|
11429
11891
|
drainCleanup() {
|
|
11430
11892
|
for (const fn of this.pluginCleanupFns.splice(0)) {
|
|
@@ -11434,6 +11896,9 @@ var DefaultPluginAPI = class {
|
|
|
11434
11896
|
}
|
|
11435
11897
|
}
|
|
11436
11898
|
}
|
|
11899
|
+
registerSystemPromptContributor(c) {
|
|
11900
|
+
return this.extensions.registerSystemPromptContributor(c);
|
|
11901
|
+
}
|
|
11437
11902
|
};
|
|
11438
11903
|
var noopMcp = {
|
|
11439
11904
|
start: async () => void 0,
|
|
@@ -11454,6 +11919,32 @@ var noopSlashCommands = {
|
|
|
11454
11919
|
return [];
|
|
11455
11920
|
}
|
|
11456
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
|
+
}
|
|
11457
11948
|
|
|
11458
11949
|
// src/plugin/loader.ts
|
|
11459
11950
|
var KERNEL_API_VERSION = "0.1.10";
|
|
@@ -11479,6 +11970,16 @@ function satisfies(range, version) {
|
|
|
11479
11970
|
function normalizeDep(d) {
|
|
11480
11971
|
return typeof d === "string" ? { name: d } : d;
|
|
11481
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
|
+
}
|
|
11482
11983
|
function topoSort(plugins) {
|
|
11483
11984
|
const map = /* @__PURE__ */ new Map();
|
|
11484
11985
|
for (const p of plugins) map.set(p.name, p);
|
|
@@ -11574,6 +12075,11 @@ async function loadPlugins(plugins, opts) {
|
|
|
11574
12075
|
failed.push({ plugin, err });
|
|
11575
12076
|
continue;
|
|
11576
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
|
+
}
|
|
11577
12083
|
if (plugin.configSchema && opts.pluginOptions) {
|
|
11578
12084
|
const pluginOpts = opts.pluginOptions[plugin.name];
|
|
11579
12085
|
if (pluginOpts !== void 0) {
|
|
@@ -11595,7 +12101,7 @@ async function loadPlugins(plugins, opts) {
|
|
|
11595
12101
|
}
|
|
11596
12102
|
try {
|
|
11597
12103
|
const rawApi = opts.apiFactory(plugin);
|
|
11598
|
-
const api = plugin.capabilities ? wrapApiForCapabilityCheck(plugin, rawApi, opts.log) : rawApi;
|
|
12104
|
+
const api = plugin.capabilities ? wrapApiForCapabilityCheck(plugin, rawApi, opts.log, opts.enforceCapabilities) : rawApi;
|
|
11599
12105
|
await plugin.setup(api);
|
|
11600
12106
|
loaded.push(plugin);
|
|
11601
12107
|
opts.log.info(`Plugin "${plugin.name}" loaded`);
|
|
@@ -11619,10 +12125,18 @@ async function unloadPlugins(loadedPlugins, opts) {
|
|
|
11619
12125
|
}
|
|
11620
12126
|
}
|
|
11621
12127
|
}
|
|
11622
|
-
function wrapApiForCapabilityCheck(plugin, api, log) {
|
|
12128
|
+
function wrapApiForCapabilityCheck(plugin, api, log, enforce = false) {
|
|
11623
12129
|
const caps = plugin.capabilities ?? {};
|
|
11624
|
-
const
|
|
12130
|
+
const violate = (subsystem, detail) => {
|
|
11625
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
|
+
}
|
|
11626
12140
|
if (typeof log.warn === "function") log.warn(msg);
|
|
11627
12141
|
else log.error(msg);
|
|
11628
12142
|
};
|
|
@@ -11630,7 +12144,7 @@ function wrapApiForCapabilityCheck(plugin, api, log) {
|
|
|
11630
12144
|
get(target, prop, receiver) {
|
|
11631
12145
|
if (prop === "register") {
|
|
11632
12146
|
return (t2) => {
|
|
11633
|
-
|
|
12147
|
+
violate("tools", `register(${t2?.name ?? "<unknown>"})`);
|
|
11634
12148
|
return target.register(t2);
|
|
11635
12149
|
};
|
|
11636
12150
|
}
|
|
@@ -11641,7 +12155,7 @@ function wrapApiForCapabilityCheck(plugin, api, log) {
|
|
|
11641
12155
|
get(target, prop, receiver) {
|
|
11642
12156
|
if (prop === "register") {
|
|
11643
12157
|
return (f) => {
|
|
11644
|
-
|
|
12158
|
+
violate("providers", `register(${f?.type ?? "<unknown>"})`);
|
|
11645
12159
|
return target.register(f);
|
|
11646
12160
|
};
|
|
11647
12161
|
}
|
|
@@ -11652,7 +12166,10 @@ function wrapApiForCapabilityCheck(plugin, api, log) {
|
|
|
11652
12166
|
get(target, prop, receiver) {
|
|
11653
12167
|
if (prop === "register") {
|
|
11654
12168
|
return (c) => {
|
|
11655
|
-
|
|
12169
|
+
violate(
|
|
12170
|
+
"slashCommands",
|
|
12171
|
+
`register(${c?.name ?? "<unknown>"})`
|
|
12172
|
+
);
|
|
11656
12173
|
return target.register(c);
|
|
11657
12174
|
};
|
|
11658
12175
|
}
|
|
@@ -11663,7 +12180,7 @@ function wrapApiForCapabilityCheck(plugin, api, log) {
|
|
|
11663
12180
|
get(target, prop, receiver) {
|
|
11664
12181
|
if (prop === "start") {
|
|
11665
12182
|
return (cfg) => {
|
|
11666
|
-
|
|
12183
|
+
violate("mcp", `start(${cfg?.name ?? "<unknown>"})`);
|
|
11667
12184
|
return target.start(cfg);
|
|
11668
12185
|
};
|
|
11669
12186
|
}
|
|
@@ -11688,6 +12205,6 @@ function wrapApiForCapabilityCheck(plugin, api, log) {
|
|
|
11688
12205
|
});
|
|
11689
12206
|
}
|
|
11690
12207
|
|
|
11691
|
-
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, DefaultRetryPolicy, DefaultSecretScrubber, DefaultSecretVault, DefaultSessionReader, DefaultSessionStore, DefaultSkillLoader, DefaultSystemPromptBuilder, DefaultTaskStore, DefaultTokenCounter, Director, DirectorBudgetError, DirectorStateCheckpoint, 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, 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 };
|
|
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 };
|
|
11692
12209
|
//# sourceMappingURL=index.js.map
|
|
11693
12210
|
//# sourceMappingURL=index.js.map
|