cascade-ai 0.71.0 → 0.73.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -64564,7 +64564,7 @@ var init_dist2 = __esm({
64564
64564
  }
64565
64565
  this._maxListeners = this._maxListeners || void 0;
64566
64566
  };
64567
- EventEmitter9.prototype.setMaxListeners = function setMaxListeners2(n8) {
64567
+ EventEmitter9.prototype.setMaxListeners = function setMaxListeners4(n8) {
64568
64568
  if (typeof n8 !== "number" || n8 < 0 || NumberIsNaN(n8)) throw new RangeError('The value of "n" is out of range. It must be a non-negative number. Received ' + n8 + ".");
64569
64569
  this._maxListeners = n8;
64570
64570
  return this;
@@ -291665,6 +291665,15 @@ function findPricing(modelId, provider, opts = {}) {
291665
291665
  }
291666
291666
  return null;
291667
291667
  }
291668
+ function hasContextBands(model) {
291669
+ if (model.isLocal) return false;
291670
+ const fallbackProviders = model.provider === "azure" ? ["openai"] : model.provider === "gemini" ? ["vertex"] : [];
291671
+ const match = findPricing(model.id, model.provider, {
291672
+ baseModelId: model.baseModelId,
291673
+ fallbackProviders
291674
+ });
291675
+ return (match?.entry.tiers?.length ?? 0) > 1;
291676
+ }
291668
291677
  function tierFor(entry, inputTokens = 0) {
291669
291678
  const tiers = entry.tiers;
291670
291679
  if (!tiers?.length) return null;
@@ -291781,7 +291790,7 @@ function reconcilePrice(model, live) {
291781
291790
  }
291782
291791
 
291783
291792
  // src/constants.ts
291784
- var CASCADE_VERSION = "0.71.0";
291793
+ var CASCADE_VERSION = "0.73.0";
291785
291794
  var CASCADE_CONFIG_DIR = ".cascade";
291786
291795
  var CASCADE_MD_FILE = "CASCADE.md";
291787
291796
  var CASCADE_IGNORE_FILE = ".cascadeignore";
@@ -292226,8 +292235,23 @@ var PROVIDER_DISPLAY_NAMES = {
292226
292235
  };
292227
292236
 
292228
292237
  // src/utils/cost.ts
292229
- function resolveModelPricing(model) {
292238
+ function resolveModelPricing(model, opts = {}) {
292230
292239
  if (model.isLocal) return { input: 0, output: 0, unknown: false };
292240
+ if (opts.inputTokens != null && hasContextBands(model)) {
292241
+ const banded = resolvePricing(model, { inputTokens: opts.inputTokens });
292242
+ const base = resolvePricing(model, { inputTokens: 0 });
292243
+ if (!banded.unknown && !base.unknown) {
292244
+ const scale = (stamped, baseRate) => stamped > 0 && baseRate > 0 ? stamped / baseRate : 1;
292245
+ const inScale = scale(model.inputCostPer1kTokens, base.input);
292246
+ const outScale = scale(model.outputCostPer1kTokens, base.output);
292247
+ return {
292248
+ input: banded.input * inScale,
292249
+ output: banded.output * outScale,
292250
+ unknown: false,
292251
+ ...banded.estimatedFromProvider ? { estimatedFromProvider: banded.estimatedFromProvider } : {}
292252
+ };
292253
+ }
292254
+ }
292231
292255
  if (model.inputCostPer1kTokens > 0 || model.outputCostPer1kTokens > 0) {
292232
292256
  return {
292233
292257
  input: model.inputCostPer1kTokens,
@@ -292235,7 +292259,7 @@ function resolveModelPricing(model) {
292235
292259
  unknown: false
292236
292260
  };
292237
292261
  }
292238
- const fromDataset = resolvePricing(model);
292262
+ const fromDataset = resolvePricing(model, { inputTokens: opts.inputTokens });
292239
292263
  if (!fromDataset.unknown) {
292240
292264
  return {
292241
292265
  input: fromDataset.input,
@@ -292257,11 +292281,11 @@ function resolveModelPricing(model) {
292257
292281
  return { input: 0, output: 0, unknown: true };
292258
292282
  }
292259
292283
  function calculateCost(inputTokens, outputTokens, model) {
292260
- const { input, output } = resolveModelPricing(model);
292284
+ const { input, output } = resolveModelPricing(model, { inputTokens });
292261
292285
  return inputTokens / 1e3 * input + outputTokens / 1e3 * output;
292262
292286
  }
292263
292287
  function buildTokenUsage(inputTokens, outputTokens, model) {
292264
- const { input, output, unknown: unknown2 } = resolveModelPricing(model);
292288
+ const { input, output, unknown: unknown2 } = resolveModelPricing(model, { inputTokens });
292265
292289
  return {
292266
292290
  inputTokens,
292267
292291
  outputTokens,
@@ -292340,6 +292364,21 @@ function anthropicThinkingParam(modelId, maxTokens) {
292340
292364
  if (budget < 1024) return {};
292341
292365
  return { thinking: { type: "enabled", budget_tokens: budget } };
292342
292366
  }
292367
+ function toAnthropicTools(tools) {
292368
+ return tools.map((t4) => ({
292369
+ name: t4.name,
292370
+ description: t4.description,
292371
+ input_schema: t4.inputSchema
292372
+ }));
292373
+ }
292374
+ function toAnthropicToolUse(toolCalls) {
292375
+ return toolCalls.map((tc) => ({
292376
+ type: "tool_use",
292377
+ id: tc.id,
292378
+ name: tc.name,
292379
+ input: tc.input
292380
+ }));
292381
+ }
292343
292382
  var AnthropicProvider = class extends BaseProvider {
292344
292383
  client;
292345
292384
  constructor(config2, model) {
@@ -292361,11 +292400,7 @@ var AnthropicProvider = class extends BaseProvider {
292361
292400
  }
292362
292401
  async generateStream(options, onChunk) {
292363
292402
  const messages = this.convertMessages(options.messages);
292364
- const tools = options.tools?.map((t4) => ({
292365
- name: t4.name,
292366
- description: t4.description,
292367
- input_schema: t4.inputSchema
292368
- }));
292403
+ const tools = options.tools && toAnthropicTools(options.tools);
292369
292404
  let fullContent = "";
292370
292405
  let inputTokens = 0;
292371
292406
  let outputTokens = 0;
@@ -292493,14 +292528,7 @@ var AnthropicProvider = class extends BaseProvider {
292493
292528
  const content = [];
292494
292529
  const text = typeof m3.content === "string" ? m3.content : "";
292495
292530
  if (text) content.push({ type: "text", text });
292496
- for (const tc of m3.toolCalls ?? []) {
292497
- content.push({
292498
- type: "tool_use",
292499
- id: tc.id,
292500
- name: tc.name,
292501
- input: tc.input
292502
- });
292503
- }
292531
+ content.push(...toAnthropicToolUse(m3.toolCalls ?? []));
292504
292532
  if (content.length > 0) {
292505
292533
  result.push({ role: "assistant", content });
292506
292534
  }
@@ -300413,6 +300441,26 @@ function isParamShapeError(err) {
300413
300441
  const msg = String(e3?.message ?? e3?.error?.message ?? "").toLowerCase();
300414
300442
  return msg.includes("max_completion_tokens") || msg.includes("max_tokens") && (msg.includes("unsupported") || msg.includes("not supported") || msg.includes("use")) || msg.includes("temperature") && (msg.includes("unsupported") || msg.includes("does not support") || msg.includes("only the default"));
300415
300443
  }
300444
+ function toOpenAITools(tools) {
300445
+ return tools.map((t4) => ({
300446
+ type: "function",
300447
+ function: {
300448
+ name: t4.name,
300449
+ description: t4.description,
300450
+ parameters: t4.inputSchema
300451
+ }
300452
+ }));
300453
+ }
300454
+ function toOpenAIToolCalls(toolCalls) {
300455
+ return toolCalls.map((tc) => ({
300456
+ id: tc.id,
300457
+ type: "function",
300458
+ function: {
300459
+ name: tc.name,
300460
+ arguments: JSON.stringify(tc.input)
300461
+ }
300462
+ }));
300463
+ }
300416
300464
  var OpenAIProvider = class extends BaseProvider {
300417
300465
  client;
300418
300466
  /** Once we learn (from the model id or an API error) that this deployment
@@ -300432,14 +300480,7 @@ var OpenAIProvider = class extends BaseProvider {
300432
300480
  }
300433
300481
  async generateStream(options, onChunk) {
300434
300482
  const messages = this.convertMessages(options.messages, options.systemPrompt);
300435
- const tools = options.tools?.map((t4) => ({
300436
- type: "function",
300437
- function: {
300438
- name: t4.name,
300439
- description: t4.description,
300440
- parameters: t4.inputSchema
300441
- }
300442
- }));
300483
+ const tools = options.tools && toOpenAITools(options.tools);
300443
300484
  let fullContent = "";
300444
300485
  let inputTokens = 0;
300445
300486
  let outputTokens = 0;
@@ -300585,14 +300626,7 @@ var OpenAIProvider = class extends BaseProvider {
300585
300626
  result.push({
300586
300627
  role: "assistant",
300587
300628
  content: m3.content || "",
300588
- tool_calls: m3.toolCalls.map((toolCall) => ({
300589
- id: toolCall.id,
300590
- type: "function",
300591
- function: {
300592
- name: toolCall.name,
300593
- arguments: JSON.stringify(toolCall.input)
300594
- }
300595
- }))
300629
+ tool_calls: toOpenAIToolCalls(m3.toolCalls)
300596
300630
  });
300597
300631
  } else {
300598
300632
  result.push({ role: m3.role, content: m3.content });
@@ -318863,6 +318897,26 @@ function toGeminiParameters(inputSchema) {
318863
318897
  }
318864
318898
 
318865
318899
  // src/providers/gemini.ts
318900
+ function toGeminiTools(tools) {
318901
+ return tools.map((tool) => ({
318902
+ name: tool.name,
318903
+ description: tool.description,
318904
+ // Convert, never cast. Gemini's parameters field is an OpenAPI subset
318905
+ // that 400s on unknown keys, and MCP servers ship schemas carrying their
318906
+ // own extensions (GitHub's adds `x-mcp-header` to every header-bound
318907
+ // property). Casting sent those through verbatim and every request failed
318908
+ // before the model saw it. See providers/gemini-schema.ts.
318909
+ parameters: toGeminiParameters(tool.inputSchema)
318910
+ }));
318911
+ }
318912
+ function toGeminiFunctionCalls(toolCalls) {
318913
+ return toolCalls.map((tc) => ({
318914
+ functionCall: {
318915
+ name: tc.name,
318916
+ args: tc.input
318917
+ }
318918
+ }));
318919
+ }
318866
318920
  var GeminiProvider = class extends BaseProvider {
318867
318921
  client;
318868
318922
  constructor(config2, model) {
@@ -318884,7 +318938,7 @@ var GeminiProvider = class extends BaseProvider {
318884
318938
  { category: HarmCategory.HARM_CATEGORY_HARASSMENT, threshold: HarmBlockThreshold.BLOCK_NONE },
318885
318939
  { category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, threshold: HarmBlockThreshold.BLOCK_NONE }
318886
318940
  ],
318887
- tools: options.tools?.length ? [{ functionDeclarations: options.tools.map(this.convertTool) }] : void 0,
318941
+ tools: options.tools?.length ? [{ functionDeclarations: toGeminiTools(options.tools) }] : void 0,
318888
318942
  abortSignal: options.signal
318889
318943
  }
318890
318944
  });
@@ -319025,14 +319079,7 @@ var GeminiProvider = class extends BaseProvider {
319025
319079
  const parts = [];
319026
319080
  const textContent = typeof m3.content === "string" ? m3.content : "";
319027
319081
  if (textContent) parts.push({ text: textContent });
319028
- for (const tc of m3.toolCalls ?? []) {
319029
- parts.push({
319030
- functionCall: {
319031
- name: tc.name,
319032
- args: tc.input
319033
- }
319034
- });
319035
- }
319082
+ parts.push(...toGeminiFunctionCalls(m3.toolCalls ?? []));
319036
319083
  if (parts.length > 0) {
319037
319084
  contents.push({ role: "model", parts });
319038
319085
  }
@@ -319079,18 +319126,6 @@ var GeminiProvider = class extends BaseProvider {
319079
319126
  }
319080
319127
  return parts;
319081
319128
  }
319082
- convertTool(tool) {
319083
- return {
319084
- name: tool.name,
319085
- description: tool.description,
319086
- // Convert, never cast. Gemini's parameters field is an OpenAPI subset
319087
- // that 400s on unknown keys, and MCP servers ship schemas carrying their
319088
- // own extensions (GitHub's adds `x-mcp-header` to every header-bound
319089
- // property). Casting sent those through verbatim and every request failed
319090
- // before the model saw it. See providers/gemini-schema.ts.
319091
- parameters: toGeminiParameters(tool.inputSchema)
319092
- };
319093
- }
319094
319129
  };
319095
319130
  try {
319096
319131
  dns__default.default.setDefaultResultOrder("ipv4first");
@@ -319183,6 +319218,24 @@ function isToolCapable(modelName) {
319183
319218
  const name = modelName.toLowerCase();
319184
319219
  return TOOL_CAPABLE_FAMILIES.some((family) => name.includes(family));
319185
319220
  }
319221
+ function toOllamaTools(tools) {
319222
+ return tools.map((t4) => ({
319223
+ type: "function",
319224
+ function: {
319225
+ name: t4.name,
319226
+ description: t4.description,
319227
+ parameters: t4.inputSchema
319228
+ }
319229
+ }));
319230
+ }
319231
+ function toOllamaToolCalls(toolCalls) {
319232
+ return toolCalls.map((tc) => ({
319233
+ function: {
319234
+ name: tc.name,
319235
+ arguments: tc.input
319236
+ }
319237
+ }));
319238
+ }
319186
319239
  var OllamaProvider = class extends BaseProvider {
319187
319240
  baseUrl;
319188
319241
  constructor(config2, model) {
@@ -319195,14 +319248,7 @@ var OllamaProvider = class extends BaseProvider {
319195
319248
  }
319196
319249
  async generateStream(options, onChunk) {
319197
319250
  const messages = this.convertMessages(options.messages, options.systemPrompt);
319198
- const ollamaTools = options.tools?.map((t4) => ({
319199
- type: "function",
319200
- function: {
319201
- name: t4.name,
319202
- description: t4.description,
319203
- parameters: t4.inputSchema
319204
- }
319205
- }));
319251
+ const ollamaTools = options.tools && toOllamaTools(options.tools);
319206
319252
  const response = await fetch(`${this.baseUrl}/api/chat`, {
319207
319253
  method: "POST",
319208
319254
  headers: { "Content-Type": "application/json" },
@@ -319382,12 +319428,7 @@ var OllamaProvider = class extends BaseProvider {
319382
319428
  result.push({
319383
319429
  role: "assistant",
319384
319430
  content: typeof m3.content === "string" ? m3.content : "",
319385
- tool_calls: m3.toolCalls.map((tc) => ({
319386
- function: {
319387
- name: tc.name,
319388
- arguments: tc.input
319389
- }
319390
- }))
319431
+ tool_calls: toOllamaToolCalls(m3.toolCalls)
319391
319432
  });
319392
319433
  continue;
319393
319434
  }
@@ -319790,6 +319831,21 @@ var DEFAULT_PROVIDER_TPM = {
319790
319831
  "openai-compatible": 3e4,
319791
319832
  ollama: Number.POSITIVE_INFINITY
319792
319833
  };
319834
+ function abortReason(signal) {
319835
+ return signal.reason instanceof Error ? signal.reason : new Error("Aborted while waiting for rate-limit capacity");
319836
+ }
319837
+ function sleepUnlessAborted(ms2, signal) {
319838
+ if (!signal) return new Promise((resolve2) => setTimeout(resolve2, ms2));
319839
+ return new Promise((resolve2) => {
319840
+ const done = () => {
319841
+ clearTimeout(timer);
319842
+ signal.removeEventListener("abort", done);
319843
+ resolve2();
319844
+ };
319845
+ const timer = setTimeout(done, ms2);
319846
+ signal.addEventListener("abort", done, { once: true });
319847
+ });
319848
+ }
319793
319849
  var TpmLimiter = class {
319794
319850
  buckets = /* @__PURE__ */ new Map();
319795
319851
  constructor(overrides = {}) {
@@ -319808,9 +319864,10 @@ var TpmLimiter = class {
319808
319864
  * are reported back via `refund` when short, or simply settled at the next
319809
319865
  * refill.
319810
319866
  */
319811
- async acquire(provider, estimatedTokens) {
319867
+ async acquire(provider, estimatedTokens, signal) {
319812
319868
  const bucket = this.buckets.get(provider);
319813
319869
  if (!bucket || bucket.tokensPerMinute === Number.POSITIVE_INFINITY) return;
319870
+ if (signal?.aborted) throw abortReason(signal);
319814
319871
  const want = Math.min(estimatedTokens, bucket.tokensPerMinute);
319815
319872
  for (; ; ) {
319816
319873
  this.refill(bucket);
@@ -319820,7 +319877,8 @@ var TpmLimiter = class {
319820
319877
  }
319821
319878
  const deficit = want - bucket.available;
319822
319879
  const waitMs = Math.max(50, Math.ceil(deficit / bucket.tokensPerMinute * 6e4));
319823
- await new Promise((resolve2) => setTimeout(resolve2, waitMs));
319880
+ await sleepUnlessAborted(waitMs, signal);
319881
+ if (signal?.aborted) throw abortReason(signal);
319824
319882
  }
319825
319883
  }
319826
319884
  /**
@@ -319866,6 +319924,9 @@ var TpmLimiter = class {
319866
319924
  };
319867
319925
 
319868
319926
  // src/core/router/local-queue.ts
319927
+ function abortReason2(signal) {
319928
+ return signal.reason instanceof Error ? signal.reason : new Error("Aborted while waiting for a local inference slot");
319929
+ }
319869
319930
  var LocalRequestQueue = class {
319870
319931
  maxConcurrent;
319871
319932
  active = 0;
@@ -319876,9 +319937,16 @@ var LocalRequestQueue = class {
319876
319937
  /**
319877
319938
  * Acquire a queue slot. Returns a `release` function that MUST be called
319878
319939
  * when the inference call is done (even on error). Rejects if the slot
319879
- * cannot be acquired within `timeoutMs`.
319940
+ * cannot be acquired within `timeoutMs`, or as soon as `signal` fires.
319941
+ *
319942
+ * The signal matters as much as the timeout: with concurrency of 1 a caller
319943
+ * can sit here for half the inference timeout — 150 seconds by default —
319944
+ * and until it was wired up, neither cancelling the run nor spending its
319945
+ * budget got it out. It waited out the window for a slot it would drop the
319946
+ * moment it received one.
319880
319947
  */
319881
- async acquire(timeoutMs) {
319948
+ async acquire(timeoutMs, signal) {
319949
+ if (signal?.aborted) throw abortReason2(signal);
319882
319950
  if (this.active < this.maxConcurrent) {
319883
319951
  this.active++;
319884
319952
  return this.makeRelease();
@@ -319886,23 +319954,36 @@ var LocalRequestQueue = class {
319886
319954
  return new Promise((resolve2, reject) => {
319887
319955
  let settled = false;
319888
319956
  let timer;
319957
+ const cleanup = () => {
319958
+ if (timer !== void 0) clearTimeout(timer);
319959
+ signal?.removeEventListener("abort", onAbort);
319960
+ };
319889
319961
  const resolver = (release) => {
319890
319962
  if (settled) return;
319891
319963
  settled = true;
319892
- if (timer !== void 0) clearTimeout(timer);
319964
+ cleanup();
319893
319965
  resolve2(release);
319894
319966
  };
319967
+ const abandon = () => {
319968
+ if (settled) return false;
319969
+ settled = true;
319970
+ cleanup();
319971
+ const idx = this.queue.indexOf(resolver);
319972
+ if (idx !== -1) this.queue.splice(idx, 1);
319973
+ return true;
319974
+ };
319975
+ function onAbort() {
319976
+ if (abandon()) reject(abortReason2(signal));
319977
+ }
319895
319978
  if (timeoutMs !== void 0 && timeoutMs > 0) {
319896
319979
  timer = setTimeout(() => {
319897
- if (settled) return;
319898
- settled = true;
319899
- const idx = this.queue.indexOf(resolver);
319900
- if (idx !== -1) this.queue.splice(idx, 1);
319980
+ if (!abandon()) return;
319901
319981
  reject(new Error(
319902
319982
  `Local model queue: timed out waiting for a free slot after ${timeoutMs}ms. Active: ${this.active}, Queued: ${this.queue.length}. Consider increasing localConcurrency or localInferenceTimeoutMs in your config.`
319903
319983
  ));
319904
319984
  }, timeoutMs);
319905
319985
  }
319986
+ signal?.addEventListener("abort", onAbort, { once: true });
319906
319987
  this.queue.push(resolver);
319907
319988
  });
319908
319989
  }
@@ -320039,6 +320120,116 @@ function fileDeadModelPersistence(filePath) {
320039
320120
  };
320040
320121
  }
320041
320122
 
320123
+ // src/core/context/compaction.ts
320124
+ var CHARS_PER_TOKEN = 4;
320125
+ function estimateTokens(text) {
320126
+ if (!text) return 0;
320127
+ return Math.max(1, Math.ceil(text.length / CHARS_PER_TOKEN));
320128
+ }
320129
+ function contentToText(content) {
320130
+ if (typeof content === "string") return content;
320131
+ return content.map((c4) => c4.type === "text" ? c4.text : c4.type === "tool_result" ? c4.content : `[${c4.type}]`).join(" ");
320132
+ }
320133
+ function messagesTokens(messages) {
320134
+ return messages.reduce((n8, m3) => n8 + estimateTokens(contentToText(m3.content)), 0);
320135
+ }
320136
+ function atomize(text, maxChars) {
320137
+ const seps = ["\n\n", "\n", ". ", " "];
320138
+ let units = [text];
320139
+ for (const sep3 of seps) {
320140
+ if (units.every((u2) => u2.length <= maxChars)) break;
320141
+ const next = [];
320142
+ for (const u2 of units) {
320143
+ if (u2.length <= maxChars) {
320144
+ next.push(u2);
320145
+ continue;
320146
+ }
320147
+ const parts = u2.split(sep3);
320148
+ parts.forEach((p3, i4) => next.push(i4 < parts.length - 1 ? p3 + sep3 : p3));
320149
+ }
320150
+ units = next;
320151
+ }
320152
+ const out = [];
320153
+ for (const u2 of units) {
320154
+ if (u2.length <= maxChars) out.push(u2);
320155
+ else for (let i4 = 0; i4 < u2.length; i4 += maxChars) out.push(u2.slice(i4, i4 + maxChars));
320156
+ }
320157
+ return out;
320158
+ }
320159
+ function chunkText(text, opts) {
320160
+ const maxChars = Math.max(1, Math.floor(opts.maxTokens * 4));
320161
+ const overlap = Math.min(maxChars - 1, Math.max(0, Math.floor(maxChars * (opts.overlapRatio ?? 0.1))));
320162
+ const trimmed2 = text.trim();
320163
+ if (!trimmed2) return [];
320164
+ if (trimmed2.length <= maxChars) return [trimmed2];
320165
+ const units = atomize(trimmed2, maxChars);
320166
+ const chunks = [];
320167
+ let cur = "";
320168
+ for (const u2 of units) {
320169
+ if (cur && cur.length + u2.length > maxChars) {
320170
+ chunks.push(cur.trim());
320171
+ cur = overlap > 0 ? cur.slice(-overlap) : "";
320172
+ }
320173
+ cur += u2;
320174
+ }
320175
+ if (cur.trim()) chunks.push(cur.trim());
320176
+ return chunks;
320177
+ }
320178
+ function needsCompaction(totalTokens, contextWindow, reserve = 0.2) {
320179
+ if (!contextWindow || contextWindow <= 0) return false;
320180
+ return totalTokens > contextWindow * (1 - reserve);
320181
+ }
320182
+ async function mapReduceCompact(text, opts) {
320183
+ const capChars = opts.capTokens * 4;
320184
+ let working = text;
320185
+ let truncated = false;
320186
+ if (working.length > capChars) {
320187
+ working = working.slice(0, capChars);
320188
+ truncated = true;
320189
+ }
320190
+ const instruction = opts.instruction ?? "Summarize this content, preserving key facts, names, numbers, and decisions.";
320191
+ let chunks = chunkText(working, { maxTokens: opts.chunkTokens });
320192
+ if (chunks.length <= 1) return { text: working, chunks: chunks.length, calls: 0, truncated };
320193
+ let calls = 0;
320194
+ let parts = await Promise.all(
320195
+ chunks.map((c4, i4) => {
320196
+ calls++;
320197
+ return opts.summarize(c4, `${instruction} (part ${i4 + 1} of ${chunks.length})`);
320198
+ })
320199
+ );
320200
+ let combined = parts.join("\n\n");
320201
+ const maxPasses = opts.maxReducePasses ?? 3;
320202
+ for (let pass = 0; pass < maxPasses && estimateTokens(combined) > opts.targetTokens; pass++) {
320203
+ const groups = chunkText(combined, { maxTokens: opts.chunkTokens });
320204
+ if (groups.length <= 1) break;
320205
+ parts = await Promise.all(
320206
+ groups.map((g2) => {
320207
+ calls++;
320208
+ return opts.summarize(g2, "Merge these partial summaries into one cohesive summary; keep all distinct facts.");
320209
+ })
320210
+ );
320211
+ combined = parts.join("\n\n");
320212
+ }
320213
+ const note = truncated ? "\n\n[Note: input exceeded the extended-context cap and was truncated.]" : "";
320214
+ return { text: combined + note, chunks: chunks.length, calls, truncated };
320215
+ }
320216
+ async function rollingSummary(messages, opts) {
320217
+ if (messages.length <= opts.keepRecent) return messages;
320218
+ const older = messages.slice(0, messages.length - opts.keepRecent);
320219
+ const recent = messages.slice(messages.length - opts.keepRecent);
320220
+ const transcript = older.map((m3) => `${m3.role}: ${contentToText(m3.content)}`).join("\n");
320221
+ const summary = await opts.summarize(
320222
+ transcript,
320223
+ "Summarize this earlier conversation. Preserve facts, decisions, names, numbers, and any open threads or todos."
320224
+ );
320225
+ const summaryMessage = {
320226
+ role: "system",
320227
+ content: `Summary of earlier conversation (folded to fit the context window):
320228
+ ${summary}`
320229
+ };
320230
+ return [summaryMessage, ...recent];
320231
+ }
320232
+
320042
320233
  // src/utils/retry.ts
320043
320234
  var CascadeCancelledError = class extends Error {
320044
320235
  constructor(reason) {
@@ -320073,6 +320264,158 @@ async function withTimeout(promise, timeoutMs, errorMessage = "Operation timed o
320073
320264
  if (timer !== void 0) clearTimeout(timer);
320074
320265
  }
320075
320266
  }
320267
+ async function withTimeoutAbort(run, timeoutMs, errorMessage = "Operation timed out", outer) {
320268
+ const outers = (Array.isArray(outer) ? outer : [outer]).filter((sig) => sig !== void 0);
320269
+ const controller = new AbortController();
320270
+ let timer;
320271
+ let rejectRace;
320272
+ const settle = (reason) => {
320273
+ rejectRace?.(reason);
320274
+ controller.abort(reason);
320275
+ };
320276
+ const abortOuter = () => {
320277
+ const fired = outers.find((sig) => sig.aborted);
320278
+ settle(fired?.reason instanceof Error ? fired.reason : new CascadeCancelledError("Run cancelled"));
320279
+ };
320280
+ const failPromise = new Promise((_4, reject) => {
320281
+ rejectRace = reject;
320282
+ timer = setTimeout(() => settle(new Error(errorMessage)), timeoutMs);
320283
+ });
320284
+ try {
320285
+ if (outers.some((sig) => sig.aborted)) {
320286
+ abortOuter();
320287
+ return await failPromise;
320288
+ }
320289
+ for (const sig of outers) sig.addEventListener("abort", abortOuter, { once: true });
320290
+ return await Promise.race([run(controller.signal), failPromise]);
320291
+ } finally {
320292
+ if (timer !== void 0) clearTimeout(timer);
320293
+ for (const sig of outers) sig.removeEventListener("abort", abortOuter);
320294
+ }
320295
+ }
320296
+ function anySignal(sources) {
320297
+ const live = sources.filter((s3) => s3 !== void 0);
320298
+ if (live.length === 0) return { release: () => {
320299
+ } };
320300
+ if (live.length === 1) return { signal: live[0], release: () => {
320301
+ } };
320302
+ const controller = new AbortController();
320303
+ function detach() {
320304
+ for (const s3 of live) s3.removeEventListener("abort", onAbort);
320305
+ }
320306
+ function onAbort() {
320307
+ const fired = live.find((s3) => s3.aborted);
320308
+ controller.abort(fired?.reason instanceof Error ? fired.reason : new CascadeCancelledError("Run cancelled"));
320309
+ detach();
320310
+ }
320311
+ if (live.some((s3) => s3.aborted)) {
320312
+ onAbort();
320313
+ return { signal: controller.signal, release: detach };
320314
+ }
320315
+ for (const s3 of live) s3.addEventListener("abort", onAbort, { once: true });
320316
+ return { signal: controller.signal, release: detach };
320317
+ }
320318
+
320319
+ // src/core/router/wire-profile.ts
320320
+ var textAndImageOnly = (block) => block.type === "text" || block.type === "image";
320321
+ var ANTHROPIC = {
320322
+ // convertMessages: `if (m.role === 'system') continue` — history system
320323
+ // messages are skipped outright; only options.systemPrompt goes as system
320324
+ // input. Compaction emits exactly such a message, holding a summary of the
320325
+ // whole conversation, so charging it refused runs over input never sent.
320326
+ dropsMessage: (m3) => m3.role === "system",
320327
+ blockHandling: (m3) => {
320328
+ if (m3.role === "tool") return "stringified";
320329
+ if (m3.role === "assistant") return "dropped";
320330
+ return "blocks";
320331
+ },
320332
+ sendsToolCalls: (m3) => m3.role === "assistant",
320333
+ sendsBlock: textAndImageOnly,
320334
+ sendsToolCallId: true,
320335
+ sizeToolCalls: toAnthropicToolUse,
320336
+ sendsUrlImages: true,
320337
+ readsTopLevelImages: false,
320338
+ sizeTools: toAnthropicTools
320339
+ };
320340
+ var GEMINI = {
320341
+ // buildContents folds a system message into the next user turn, but only
320342
+ // when it has text: array content collapses to '' and the message is
320343
+ // skipped, as is a whitespace-only string.
320344
+ dropsMessage: (m3) => m3.role === "system" && !(typeof m3.content === "string" ? m3.content : "").trim(),
320345
+ blockHandling: (m3) => {
320346
+ if (m3.role === "tool") return "stringified";
320347
+ if (m3.role === "assistant" || m3.role === "system") return "dropped";
320348
+ return "blocks";
320349
+ },
320350
+ sendsToolCalls: (m3) => m3.role === "assistant",
320351
+ sendsBlock: textAndImageOnly,
320352
+ sendsToolCallId: true,
320353
+ sizeToolCalls: toGeminiFunctionCalls,
320354
+ sendsUrlImages: false,
320355
+ readsTopLevelImages: true,
320356
+ sizeTools: toGeminiTools
320357
+ };
320358
+ var OPENAI = {
320359
+ dropsMessage: () => false,
320360
+ blockHandling: (m3) => {
320361
+ if (m3.role === "system" || m3.role === "tool") return "dropped";
320362
+ return "blocks";
320363
+ },
320364
+ // The tool_calls branch is inside `if (typeof m.content === 'string')`; an
320365
+ // assistant turn with array content falls through to the parts branch and
320366
+ // its tool calls are never attached.
320367
+ sendsToolCalls: (m3) => m3.role === "assistant" && typeof m3.content === "string",
320368
+ sendsBlock: textAndImageOnly,
320369
+ sendsToolCallId: true,
320370
+ sizeToolCalls: toOpenAIToolCalls,
320371
+ sendsUrlImages: true,
320372
+ readsTopLevelImages: false,
320373
+ sizeTools: toOpenAITools
320374
+ };
320375
+ var OLLAMA = {
320376
+ dropsMessage: () => false,
320377
+ blockHandling: (m3) => {
320378
+ if (m3.role === "system") return "dropped";
320379
+ if (m3.role === "tool") return "stringified";
320380
+ if (m3.role === "assistant" && m3.toolCalls?.length) return "dropped";
320381
+ return "blocks";
320382
+ },
320383
+ sendsToolCalls: (m3) => m3.role === "assistant",
320384
+ // The image split maps `b.image.data` with no check on `type`, so a URL is
320385
+ // submitted too (as a URL string in the `images` array).
320386
+ sendsBlock: textAndImageOnly,
320387
+ sendsToolCallId: false,
320388
+ sizeToolCalls: toOllamaToolCalls,
320389
+ sendsUrlImages: true,
320390
+ readsTopLevelImages: false,
320391
+ sizeTools: toOllamaTools
320392
+ };
320393
+ var PROFILES = {
320394
+ anthropic: ANTHROPIC,
320395
+ gemini: GEMINI,
320396
+ openai: OPENAI,
320397
+ // AzureOpenAIProvider and OpenAICompatibleProvider both extend OpenAIProvider
320398
+ // and neither overrides convertMessages.
320399
+ azure: OPENAI,
320400
+ "openai-compatible": OPENAI,
320401
+ ollama: OLLAMA
320402
+ };
320403
+ function wireProfile(provider) {
320404
+ return PROFILES[provider] ?? OPENAI;
320405
+ }
320406
+ function geminiImageCopies(messages) {
320407
+ for (const m3 of messages) {
320408
+ if (m3.role === "user") {
320409
+ const laterUserTurn = messages.slice(messages.indexOf(m3) + 1).some((x5) => x5.role === "user");
320410
+ return laterUserTurn ? 2 : 1;
320411
+ }
320412
+ const text = typeof m3.content === "string" ? m3.content : "";
320413
+ if (m3.role === "system" && text.trim()) return 1;
320414
+ if (m3.role === "tool") return 1;
320415
+ if (m3.role === "assistant" && (text || m3.toolCalls?.length)) return 1;
320416
+ }
320417
+ return 1;
320418
+ }
320076
320419
 
320077
320420
  // src/core/router/model-profiler.ts
320078
320421
  var SKIP_PATTERN = /embed|dall-e|whisper|tts|vision|instruct-vision|rerank/i;
@@ -320577,6 +320920,36 @@ function azureModelForTier(tier, ranked) {
320577
320920
  if (tier === "T3") return ranked[ranked.length - 1];
320578
320921
  return ranked[Math.min(1, ranked.length - 1)];
320579
320922
  }
320923
+ var IMAGE_TOKENS_MAX = 2e3;
320924
+ var IMAGE_TOKENS_EACH = IMAGE_TOKENS_MAX;
320925
+ function safeJson(value) {
320926
+ try {
320927
+ return JSON.stringify(value) ?? "";
320928
+ } catch {
320929
+ return "";
320930
+ }
320931
+ }
320932
+ function newRunAbort() {
320933
+ const controller = new AbortController();
320934
+ EventEmitter.setMaxListeners(64, controller.signal);
320935
+ return controller;
320936
+ }
320937
+ var TOKENS_PER_MESSAGE_FRAMING = 4;
320938
+ function guardTokens(text) {
320939
+ if (!text) return 0;
320940
+ let asciiChars = 0;
320941
+ let denseBytes = 0;
320942
+ for (const ch of text) {
320943
+ const cp = ch.codePointAt(0);
320944
+ if (cp <= 127) {
320945
+ asciiChars++;
320946
+ continue;
320947
+ }
320948
+ denseBytes += cp <= 2047 ? 2 : cp <= 65535 ? 3 : 4;
320949
+ }
320950
+ if (denseBytes === 0) return estimateTokens(text);
320951
+ return Math.ceil(asciiChars / CHARS_PER_TOKEN) + denseBytes;
320952
+ }
320580
320953
  var DISCOVERY_TTL_MS = 15 * 60 * 1e3;
320581
320954
  var DISCOVERY_TIMEOUT_MS = 4e3;
320582
320955
  var cloudDiscoveryCache = /* @__PURE__ */ new Map();
@@ -320629,6 +321002,33 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
320629
321002
  // start of every `cascade run`, independent of the session-wide budget.
320630
321003
  runTokens = 0;
320631
321004
  runCostUsd = 0;
321005
+ /**
321006
+ * Budget held for calls that are IN FLIGHT — admitted by the preflight check
321007
+ * but not yet counted by recordStats. Without these, concurrent calls all
321008
+ * measure themselves against the same unspent allowance and are all admitted.
321009
+ * Released when each call settles; reset by beginRun() alongside the totals.
321010
+ */
321011
+ reservedTokens = 0;
321012
+ reservedCostUsd = 0;
321013
+ /**
321014
+ * Fires when this run's budget is spent, cancelling work already in flight.
321015
+ *
321016
+ * The kill switch was previously only a flag, which stops calls that have
321017
+ * not yet submitted. A parallel wave's earlier members are already at the
321018
+ * provider by then, and they carried on generating for a run whose output
321019
+ * would be discarded. Every provider call chains to this, so tripping the
321020
+ * ceiling reaches the requests that are running, not just the ones queued.
321021
+ */
321022
+ runAbort = newRunAbort();
321023
+ /**
321024
+ * Bumped by every beginRun(). A release handle remembers the generation it
321025
+ * was issued in and does nothing if that has moved on: beginRun() zeroes the
321026
+ * counters, so a call still in flight from the previous run would otherwise
321027
+ * subtract its estimate from the NEW run's totals and drive them negative —
321028
+ * leaving later preflights believing there is more allowance than the cap
321029
+ * actually holds, which is worse than no gate at all.
321030
+ */
321031
+ runGeneration = 0;
320632
321032
  runBudgetExceeded = false;
320633
321033
  runBudgetExceededReason;
320634
321034
  /**
@@ -320798,7 +321198,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
320798
321198
  try {
320799
321199
  const cfg = this.config.providers.find((p3) => p3.type === model.provider) ?? { type: model.provider };
320800
321200
  const provider = this.createProvider(cfg, model);
320801
- const result = await withTimeout(provider.generate({
321201
+ const result = await withTimeoutAbort((signal) => provider.generate({
320802
321202
  messages: [{ role: "user", content: "Call the echo tool with text set to 'ping'. Use the tool; do not answer in prose." }],
320803
321203
  tools: [{
320804
321204
  name: "echo",
@@ -320806,7 +321206,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
320806
321206
  inputSchema: { type: "object", properties: { text: { type: "string", description: "Text to echo" } }, required: ["text"] }
320807
321207
  }],
320808
321208
  maxTokens: 80,
320809
- temperature: 0
321209
+ temperature: 0,
321210
+ signal
320810
321211
  }), 3e4, "tool-support probe timed out");
320811
321212
  return (result.toolCalls?.length ?? 0) > 0;
320812
321213
  } catch {
@@ -320927,9 +321328,12 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
320927
321328
  this.runBudgetExceededReason ?? "Per-task budget exceeded."
320928
321329
  );
320929
321330
  }
321331
+ const runGeneration = this.runGeneration;
321332
+ const runAbort = this.runAbort;
321333
+ const runSignal = this.runSignal;
320930
321334
  options = applyTierLimits(options, tier, this.config?.tierLimits);
320931
- if (this.runSignal && !options.signal) {
320932
- options = { ...options, signal: this.runSignal };
321335
+ if (runSignal && !options.signal) {
321336
+ options = { ...options, signal: runSignal };
320933
321337
  }
320934
321338
  if (options.model && !requireVision) {
320935
321339
  this.ensureProvider(options.model, this.config.providers);
@@ -320951,58 +321355,84 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
320951
321355
  if (!model) throw new Error(`No model available for tier ${tier}`);
320952
321356
  const provider = this.getProvider(model);
320953
321357
  if (!provider) throw new Error(`No provider for model ${model.id}`);
321358
+ let releaseReservation = this.enforcePreflightBudget(model, options);
320954
321359
  const requestedTokens = options.maxTokens ?? model.maxOutputTokens ?? 1024;
320955
321360
  const estimatedTokens = Math.min(requestedTokens, model.maxOutputTokens ?? requestedTokens) + 512;
320956
321361
  if (this.tpmLimiter) {
320957
- await this.tpmLimiter.acquire(model.provider, estimatedTokens);
321362
+ const wait = anySignal([options.signal, runAbort.signal]);
321363
+ try {
321364
+ await this.tpmLimiter.acquire(model.provider, estimatedTokens, wait.signal);
321365
+ } catch (err) {
321366
+ releaseReservation?.();
321367
+ throw this.asAbortFailure(err, options.signal, runSignal);
321368
+ } finally {
321369
+ wait.release();
321370
+ }
320958
321371
  }
320959
321372
  const useStream = Boolean(onChunk) && model.supportsStreaming && typeof provider.generateStream === "function";
320960
321373
  let releaseLocalSlot;
320961
321374
  if (model.isLocal) {
320962
321375
  const inferenceTimeoutMs = this.config.localInferenceTimeoutMs ?? 3e5;
320963
321376
  const queueWaitMs = Math.round(inferenceTimeoutMs / 2);
320964
- releaseLocalSlot = await this.localQueue.acquire(queueWaitMs);
321377
+ const wait = anySignal([options.signal, runAbort.signal]);
321378
+ try {
321379
+ releaseLocalSlot = await this.localQueue.acquire(queueWaitMs, wait.signal);
321380
+ } catch (err) {
321381
+ releaseReservation?.();
321382
+ throw this.asAbortFailure(err, options.signal, runSignal);
321383
+ } finally {
321384
+ wait.release();
321385
+ }
320965
321386
  }
320966
321387
  try {
321388
+ if (runGeneration === this.runGeneration && this.runBudgetExceeded) {
321389
+ this.tpmLimiter?.refund(model.provider, estimatedTokens);
321390
+ throw new _CascadeRouter.BudgetExceededError(
321391
+ this.runBudgetExceededReason ?? "Per-task budget exceeded."
321392
+ );
321393
+ }
320967
321394
  let result;
320968
321395
  if (model.isLocal) {
320969
321396
  const inferenceTimeoutMs = this.config.localInferenceTimeoutMs ?? 3e5;
320970
- const inferencePromise = useStream && onChunk ? provider.generateStream(options, (chunk) => {
320971
- const text = typeof chunk?.text === "string" ? chunk.text : "";
320972
- if (text) onChunk({ ...chunk, text });
320973
- }) : provider.generate(options);
320974
- result = await withTimeout(
320975
- inferencePromise,
321397
+ result = await withTimeoutAbort(
321398
+ (signal) => useStream && onChunk ? provider.generateStream({ ...options, signal }, (chunk) => {
321399
+ const text = typeof chunk?.text === "string" ? chunk.text : "";
321400
+ if (text) onChunk({ ...chunk, text });
321401
+ }) : provider.generate({ ...options, signal }),
320976
321402
  inferenceTimeoutMs,
320977
- `Local model ${model.id} inference timed out after ${inferenceTimeoutMs}ms`
321403
+ `Local model ${model.id} inference timed out after ${inferenceTimeoutMs}ms`,
321404
+ [options.signal, runAbort.signal]
320978
321405
  );
320979
321406
  } else if (useStream && onChunk) {
320980
321407
  const cloudTimeoutMs = this.config.cloudInferenceTimeoutMs ?? 12e4;
320981
321408
  try {
320982
- result = await withTimeout(
320983
- provider.generateStream(options, (chunk) => {
321409
+ result = await withTimeoutAbort(
321410
+ (signal) => provider.generateStream({ ...options, signal }, (chunk) => {
320984
321411
  const text = typeof chunk?.text === "string" ? chunk.text : "";
320985
321412
  if (text) onChunk({ ...chunk, text });
320986
321413
  }),
320987
321414
  cloudTimeoutMs,
320988
- `Model ${model.id} stream timed out after ${cloudTimeoutMs}ms`
321415
+ `Model ${model.id} stream timed out after ${cloudTimeoutMs}ms`,
321416
+ [options.signal, runAbort.signal]
320989
321417
  );
320990
321418
  } catch (streamErr) {
320991
- if (streamErr instanceof Error && streamErr.name === "AbortError" || this.runSignal?.aborted || options.signal?.aborted) {
321419
+ if (streamErr instanceof Error && streamErr.name === "AbortError" || runSignal?.aborted || options.signal?.aborted) {
320992
321420
  throw streamErr;
320993
321421
  }
320994
- result = await withTimeout(
320995
- provider.generate(options),
321422
+ result = await withTimeoutAbort(
321423
+ (signal) => provider.generate({ ...options, signal }),
320996
321424
  cloudTimeoutMs,
320997
- `Model ${model.id} inference timed out after ${cloudTimeoutMs}ms`
321425
+ `Model ${model.id} inference timed out after ${cloudTimeoutMs}ms`,
321426
+ [options.signal, runAbort.signal]
320998
321427
  );
320999
321428
  }
321000
321429
  } else {
321001
321430
  const cloudTimeoutMs = this.config.cloudInferenceTimeoutMs ?? 12e4;
321002
- result = await withTimeout(
321003
- provider.generate(options),
321431
+ result = await withTimeoutAbort(
321432
+ (signal) => provider.generate({ ...options, signal }),
321004
321433
  cloudTimeoutMs,
321005
- `Model ${model.id} inference timed out after ${cloudTimeoutMs}ms`
321434
+ `Model ${model.id} inference timed out after ${cloudTimeoutMs}ms`,
321435
+ [options.signal, runAbort.signal]
321006
321436
  );
321007
321437
  }
321008
321438
  const corrected = buildTokenUsage(
@@ -321021,11 +321451,12 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
321021
321451
  if (!result || typeof result.content !== "string" || !result.usage) {
321022
321452
  throw new Error(`Provider ${model.provider}:${model.id} returned an invalid generation result.`);
321023
321453
  }
321024
- this.recordStats(tier, model, result.usage, options.featureTag);
321454
+ this.recordStats(tier, model, result.usage, options.featureTag, runGeneration);
321025
321455
  this.failover.recordSuccess(model.provider);
321026
321456
  return result;
321027
321457
  } catch (err) {
321028
- if (err instanceof Error && err.name === "AbortError" || this.runSignal?.aborted || options.signal?.aborted) {
321458
+ if (err instanceof _CascadeRouter.BudgetExceededError) throw err;
321459
+ if (err instanceof Error && err.name === "AbortError" || runSignal?.aborted || options.signal?.aborted) {
321029
321460
  throw new CascadeCancelledError("Run cancelled");
321030
321461
  }
321031
321462
  const errMsg = err instanceof Error ? err.message : String(err);
@@ -321043,6 +321474,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
321043
321474
  });
321044
321475
  releaseLocalSlot?.();
321045
321476
  releaseLocalSlot = void 0;
321477
+ releaseReservation?.();
321478
+ releaseReservation = void 0;
321046
321479
  const retryOpts = options.model && options.model.id === model.id ? { ...options, model: void 0 } : options;
321047
321480
  return this.generate(tier, retryOpts, onChunk, requireVision);
321048
321481
  }
@@ -321064,6 +321497,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
321064
321497
  });
321065
321498
  releaseLocalSlot?.();
321066
321499
  releaseLocalSlot = void 0;
321500
+ releaseReservation?.();
321501
+ releaseReservation = void 0;
321067
321502
  const base = options.model && options.model.id === model.id ? { ...options, model: void 0 } : options;
321068
321503
  const retryOpts = { ...base, _notFoundDepth: depth };
321069
321504
  return this.generate(tier, retryOpts, onChunk, requireVision);
@@ -321072,6 +321507,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
321072
321507
  throw err;
321073
321508
  } finally {
321074
321509
  releaseLocalSlot?.();
321510
+ releaseReservation?.();
321511
+ releaseReservation = void 0;
321075
321512
  }
321076
321513
  }
321077
321514
  getModelForTier(tier) {
@@ -321442,7 +321879,14 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
321442
321879
  }
321443
321880
  return void 0;
321444
321881
  }
321445
- recordStats(tier, model, usage, featureTag) {
321882
+ /**
321883
+ * `runGeneration` is the generation the call was ADMITTED in — see
321884
+ * `generate()`. Session-wide accounting always applies (the spend is real
321885
+ * whenever it lands), but the per-RUN ceiling is only meaningful for the run
321886
+ * the call belongs to, so a straggler from a finished run is left out of it.
321887
+ * Omitted by the direct callers in tests, which have no run to straddle.
321888
+ */
321889
+ recordStats(tier, model, usage, featureTag, runGeneration) {
321446
321890
  this.stats.totalTokens += usage.totalTokens;
321447
321891
  this.stats.totalCostUsd += usage.estimatedCostUsd;
321448
321892
  this.sessionCostUsd += usage.estimatedCostUsd;
@@ -321467,10 +321911,13 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
321467
321911
  });
321468
321912
  }
321469
321913
  }
321470
- this.runTokens += usage.totalTokens;
321471
- this.runCostUsd += usage.estimatedCostUsd;
321914
+ const currentRun = runGeneration === void 0 || runGeneration === this.runGeneration;
321915
+ if (currentRun) {
321916
+ this.runTokens += usage.totalTokens;
321917
+ this.runCostUsd += usage.estimatedCostUsd;
321918
+ }
321472
321919
  this.updateBudgetState();
321473
- this.enforceRunBudget();
321920
+ if (currentRun) this.enforceRunBudget();
321474
321921
  }
321475
321922
  /**
321476
321923
  * Resets per-run accounting at the start of each `cascade run`. Session
@@ -321480,9 +321927,158 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
321480
321927
  beginRun() {
321481
321928
  this.runTokens = 0;
321482
321929
  this.runCostUsd = 0;
321930
+ this.reservedTokens = 0;
321931
+ this.reservedCostUsd = 0;
321932
+ this.runAbort = newRunAbort();
321933
+ this.runGeneration++;
321483
321934
  this.runBudgetExceeded = false;
321484
321935
  this.runBudgetExceededReason = void 0;
321485
321936
  }
321937
+ /**
321938
+ * Refuse a call whose input alone cannot fit what is left of the run budget.
321939
+ *
321940
+ * `enforceRunBudget()` below runs AFTER a call returns, which makes the caps
321941
+ * a stop rather than a pre-authorisation: the tokens are already bought by
321942
+ * the time it looks. That was tolerable while a prompt could not exceed
321943
+ * 20,000 characters, and stopped being so when that cap was removed in
321944
+ * 0.72.0 — a multi-megabyte prompt is billed in full on the very first call
321945
+ * (the complexity classifier sends the whole thing), and no ceiling
321946
+ * configured downstream can give that money back.
321947
+ *
321948
+ * Deliberately narrow, because a false refusal is worse than a late stop:
321949
+ *
321950
+ * • INPUT ONLY. What a call returns is not knowable in advance, and
321951
+ * charging a worst-case `maxTokens` of output against the budget would
321952
+ * decline runs that would have finished comfortably. Input is the part
321953
+ * that is already determined, and it is the part the removed cap used to
321954
+ * bound.
321955
+ * • Compared against what REMAINS (`cap - spent`), not the whole cap, so a
321956
+ * call is judged on the budget it can actually draw from.
321957
+ * • Skipped when the model has no usable price. An estimate cannot be made,
321958
+ * and refusing on ignorance would break every self-hosted and local model
321959
+ * the moment a cost cap is set. The post-hoc stop still applies there.
321960
+ * • Skipped entirely when no cap is configured — there is nothing to
321961
+ * pre-authorise against.
321962
+ *
321963
+ * The message names the cap AND the estimate, because "too expensive" with
321964
+ * no numbers gives the user nothing to change.
321965
+ */
321966
+ enforcePreflightBudget(model, options) {
321967
+ const budget = this.config?.budget;
321968
+ const maxCost = budget?.maxCostPerRunUsd;
321969
+ const maxTokens = budget?.maxTokensPerRun;
321970
+ if (maxCost == null && maxTokens == null) return void 0;
321971
+ const inputTokens = this.estimateInputTokens(options, model);
321972
+ if (maxTokens != null) {
321973
+ const remaining = maxTokens - this.runTokens - this.reservedTokens;
321974
+ if (inputTokens > remaining) {
321975
+ this.failPreflight(
321976
+ `This request's input is about ${inputTokens.toLocaleString()} tokens, but only ${Math.max(0, remaining).toLocaleString()} of the per-task cap of ${maxTokens.toLocaleString()} remain. Shorten the input, or raise budget.maxTokensPerRun.`
321977
+ );
321978
+ }
321979
+ }
321980
+ let estimatedUsd = 0;
321981
+ if (maxCost != null) {
321982
+ const { input: inputPer1k, unknown: unknown2 } = resolveModelPricing(model, { inputTokens });
321983
+ if (!unknown2 && inputPer1k > 0) {
321984
+ estimatedUsd = inputTokens / 1e3 * inputPer1k;
321985
+ const remaining = maxCost - this.runCostUsd - this.reservedCostUsd;
321986
+ if (estimatedUsd > remaining) {
321987
+ this.failPreflight(
321988
+ `Sending this request to ${model.provider}:${model.id} would cost about $${estimatedUsd.toFixed(4)} in input alone (~${inputTokens.toLocaleString()} tokens), and only $${Math.max(0, remaining).toFixed(4)} of the per-task cap of $${maxCost.toFixed(4)} remains. Shorten the input, choose a cheaper model, or raise budget.maxCostPerRunUsd.`
321989
+ );
321990
+ }
321991
+ }
321992
+ }
321993
+ this.reservedTokens += inputTokens;
321994
+ this.reservedCostUsd += estimatedUsd;
321995
+ const generation = this.runGeneration;
321996
+ let released = false;
321997
+ return () => {
321998
+ if (released || generation !== this.runGeneration) return;
321999
+ released = true;
322000
+ this.reservedTokens -= inputTokens;
322001
+ this.reservedCostUsd -= estimatedUsd;
322002
+ };
322003
+ }
322004
+ /**
322005
+ * What this call will send, in tokens.
322006
+ *
322007
+ * Text is the bulk of it, but not all of it: providers bill the serialized
322008
+ * TOOL DEFINITIONS on every native-tool call (Anthropic sends each name,
322009
+ * description and input schema), and an image is billed by its own size
322010
+ * rather than as the `[image]` placeholder `contentToText` reduces it to. A
322011
+ * tool-heavy or vision request could therefore pass either cap while its real
322012
+ * input already exceeded the allowance.
322013
+ */
322014
+ estimateInputTokens(options, model) {
322015
+ let tokens = guardTokens(options.systemPrompt ?? "");
322016
+ let images = 0;
322017
+ const wire = wireProfile(model.provider);
322018
+ const countsImage = (img) => wire.sendsUrlImages || img?.type === "base64";
322019
+ for (const message of options.messages) {
322020
+ if (wire.dropsMessage(message)) continue;
322021
+ tokens += TOKENS_PER_MESSAGE_FRAMING;
322022
+ if (typeof message.content === "string") {
322023
+ tokens += guardTokens(message.content);
322024
+ } else {
322025
+ switch (wire.blockHandling(message)) {
322026
+ case "blocks":
322027
+ for (const block of message.content) {
322028
+ if (!wire.sendsBlock(block)) continue;
322029
+ if (block.type === "image") {
322030
+ if (countsImage(block.image)) images++;
322031
+ continue;
322032
+ }
322033
+ tokens += guardTokens(contentToText([block]));
322034
+ }
322035
+ break;
322036
+ case "stringified":
322037
+ tokens += guardTokens(safeJson(message.content));
322038
+ break;
322039
+ }
322040
+ }
322041
+ if (message.toolCalls?.length && wire.sendsToolCalls(message)) {
322042
+ tokens += guardTokens(safeJson(wire.sizeToolCalls(message.toolCalls)));
322043
+ }
322044
+ if (message.toolCallId && message.role === "tool" && wire.sendsToolCallId) {
322045
+ tokens += guardTokens(message.toolCallId);
322046
+ }
322047
+ }
322048
+ if (options.tools?.length) {
322049
+ tokens += guardTokens(safeJson(wire.sizeTools(options.tools)));
322050
+ }
322051
+ if (wire.readsTopLevelImages) {
322052
+ const topLevel = (options.images ?? []).filter(countsImage).length;
322053
+ images += topLevel * geminiImageCopies(options.messages);
322054
+ }
322055
+ tokens += images * IMAGE_TOKENS_EACH;
322056
+ return tokens;
322057
+ }
322058
+ /**
322059
+ * Report a wait that ended because something asked it to stop, the same way
322060
+ * the provider call below reports one.
322061
+ *
322062
+ * The queue waits sit OUTSIDE the try/catch that maps aborts, so without
322063
+ * this a cancellation surfaced as whatever the queue happened to throw and
322064
+ * was retried or failed over like an ordinary error. A budget abort keeps
322065
+ * saying it ran out of budget — otherwise the reason the run stopped is lost
322066
+ * on the way up — and anything else that was aborted becomes a cancellation.
322067
+ */
322068
+ asAbortFailure(err, callSignal, runSignal) {
322069
+ if (err instanceof _CascadeRouter.BudgetExceededError) return err;
322070
+ if (err instanceof CascadeCancelledError) return err;
322071
+ if (callSignal?.aborted || runSignal?.aborted) return new CascadeCancelledError("Run cancelled");
322072
+ return err;
322073
+ }
322074
+ /** Trips the same run-level flag a post-hoc overrun does, and reports it the same way. */
322075
+ failPreflight(reason) {
322076
+ this.runBudgetExceeded = true;
322077
+ this.runBudgetExceededReason = reason;
322078
+ this.runAbort.abort(new _CascadeRouter.BudgetExceededError(reason));
322079
+ this.emit("budget:exceeded", { reason, spentUsd: this.sessionCostUsd });
322080
+ throw new _CascadeRouter.BudgetExceededError(reason);
322081
+ }
321486
322082
  /**
321487
322083
  * Enforce the hard per-task ceiling. Once tripped, the flag makes every
321488
322084
  * subsequent (and concurrent) generate() call in this run fail fast.
@@ -321498,6 +322094,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
321498
322094
  const reason = overTokens ? `Per-task token cap of ${maxTokens.toLocaleString()} reached (used ${this.runTokens.toLocaleString()}). Stopping this run to avoid runaway cost \u2014 raise budget.maxTokensPerRun for larger jobs.` : `Per-task cost cap of $${maxCost.toFixed(4)} reached (spent $${this.runCostUsd.toFixed(4)}). Stopping this run to avoid runaway cost.`;
321499
322095
  this.runBudgetExceeded = true;
321500
322096
  this.runBudgetExceededReason = reason;
322097
+ this.runAbort.abort(new _CascadeRouter.BudgetExceededError(reason));
321501
322098
  this.emit("budget:exceeded", { reason, spentUsd: this.sessionCostUsd });
321502
322099
  throw new _CascadeRouter.BudgetExceededError(reason);
321503
322100
  }
@@ -322734,6 +323331,12 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
322734
323331
  maxTokens: 4096,
322735
323332
  ...subtaskModel ? { model: subtaskModel } : {},
322736
323333
  featureTag: this.assignment?.sectionTitle,
323334
+ // The wave signal, so cancel-and-respawn reaches the request that is
323335
+ // RUNNING rather than only stopping the worker at its next checkpoint.
323336
+ // It is a superset of the run signal the router injects when this is
323337
+ // absent (T2 composes it from that signal plus its own per-wave one),
323338
+ // so wiring it here loses no cancellation and adds the wave's.
323339
+ ...this.signal ? { signal: this.signal } : {},
322737
323340
  ...this.localOnlyMatch ? { forceLocal: true } : {}
322738
323341
  };
322739
323342
  const result = await this.router.generate(
@@ -323074,6 +323677,22 @@ ${assignment.expectedOutput}`;
323074
323677
  requiresArtifact() {
323075
323678
  return shouldRequireArtifact(this.assignment, this.tools.map((t4) => t4.name));
323076
323679
  }
323680
+ /**
323681
+ * Where a file a tool wrote actually lives.
323682
+ *
323683
+ * This used to be `process.cwd()`, which is only the workspace in a plain
323684
+ * CLI run. The hosted server runs with cwd at the app directory while the
323685
+ * run's workspace is the tenant's scratch dir, and Electron's cwd is the app
323686
+ * bundle rather than the folder the user picked. So the tool wrote
323687
+ * `<workspace>/report.docx`, verification stat'd `<cwd>/report.docx`, found
323688
+ * nothing, and the worker looped and then threw WorkerStallError — surfacing
323689
+ * as "no file was created" on a file that had in fact been created, followed
323690
+ * by a failed node. Asking the registry is exact: it is the same root every
323691
+ * registered tool was configured with (registry.registerDefaults).
323692
+ */
323693
+ artifactRoot() {
323694
+ return this.toolRegistry.getWorkspaceRoot?.() ?? process.cwd();
323695
+ }
323077
323696
  extractArtifactPaths(assignment) {
323078
323697
  const declared = (assignment.files ?? []).map((f4) => f4.trim()).filter((f4) => f4.includes("."));
323079
323698
  const haystack = `${assignment.description}
@@ -323085,11 +323704,11 @@ ${assignment.expectedOutput}`;
323085
323704
  const artifactPaths = this.extractArtifactPaths(assignment);
323086
323705
  if (!artifactPaths.length) return { ok: true, issues: [] };
323087
323706
  const issues = [];
323088
- const { exec: exec2 } = await import('child_process');
323707
+ const { execFile: execFile4 } = await import('child_process');
323089
323708
  const { promisify: promisify5 } = await import('util');
323090
- const execAsync2 = promisify5(exec2);
323709
+ const execFileAsync3 = promisify5(execFile4);
323091
323710
  for (const artifactPath of artifactPaths) {
323092
- const absolutePath = path28__namespace.default.resolve(process.cwd(), artifactPath);
323711
+ const absolutePath = path28__namespace.default.resolve(this.artifactRoot(), artifactPath);
323093
323712
  try {
323094
323713
  const stat3 = await fs9__namespace.default.stat(absolutePath);
323095
323714
  if (!stat3.isFile()) {
@@ -323125,20 +323744,18 @@ ${assignment.expectedOutput}`;
323125
323744
  continue;
323126
323745
  }
323127
323746
  const ext = path28__namespace.default.extname(absolutePath).toLowerCase();
323128
- try {
323129
- if (ext === ".ts" || ext === ".tsx") {
323130
- await execAsync2(`npx tsc --noEmit ${absolutePath}`, { timeout: 1e4 });
323131
- } else if (ext === ".js" || ext === ".jsx") {
323132
- await execAsync2(`node --check ${absolutePath}`, { timeout: 1e4 });
323133
- } else if (ext === ".py") {
323134
- await execAsync2(`python -m py_compile ${absolutePath}`, { timeout: 1e4 });
323747
+ const check2 = ext === ".ts" || ext === ".tsx" ? { cmd: "npx", args: ["tsc", "--noEmit", absolutePath] } : ext === ".js" || ext === ".jsx" ? { cmd: process.execPath, args: ["--check", absolutePath] } : ext === ".py" ? { cmd: "python", args: ["-m", "py_compile", absolutePath] } : null;
323748
+ if (check2) {
323749
+ try {
323750
+ await execFileAsync3(check2.cmd, check2.args, { timeout: 1e4 });
323751
+ } catch (err) {
323752
+ const code = err.code;
323753
+ if (code === "ENOENT" || code === "EINVAL") continue;
323754
+ const e3 = err;
323755
+ issues.push(`Semantic error in ${artifactPath}:
323756
+ ${e3.stderr || String(err)}
323757
+ ${e3.stdout || ""}`);
323135
323758
  }
323136
- } catch (err) {
323137
- const stderr = err?.stderr || String(err);
323138
- const stdout = err?.stdout || "";
323139
- issues.push(`Semantic error in ${artifactPath}:
323140
- ${stderr}
323141
- ${stdout}`);
323142
323759
  }
323143
323760
  } catch {
323144
323761
  issues.push(`Required artifact was not created: ${artifactPath}`);
@@ -323198,6 +323815,7 @@ ${current}`
323198
323815
  HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
323199
323816
  maxTokens: 4096,
323200
323817
  featureTag: assignment.sectionTitle,
323818
+ ...this.signal ? { signal: this.signal } : {},
323201
323819
  ...this.localOnlyMatch ? { forceLocal: true } : {}
323202
323820
  });
323203
323821
  const next = (improved.content ?? "").trim();
@@ -323217,7 +323835,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
323217
323835
  async checkAcceptance(assignment) {
323218
323836
  const criteria = assignment.acceptance ?? [];
323219
323837
  if (!criteria.length) return [];
323220
- const resolve2 = (target) => path28__namespace.default.resolve(process.cwd(), target);
323838
+ const resolve2 = (target) => path28__namespace.default.resolve(this.artifactRoot(), target);
323221
323839
  return evaluateAcceptance(criteria, assignment.files ?? [], {
323222
323840
  stat: async (target) => {
323223
323841
  try {
@@ -323259,6 +323877,7 @@ Reply with JSON: { "completeness": "pass"|"fail", "correctness": "pass"|"fail",
323259
323877
 
323260
323878
  HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
323261
323879
  featureTag: assignment.sectionTitle,
323880
+ ...this.signal ? { signal: this.signal } : {},
323262
323881
  ...this.localOnlyMatch ? { forceLocal: true } : {}
323263
323882
  });
323264
323883
  try {
@@ -323298,6 +323917,7 @@ ${output.slice(0, 4e3)}`;
323298
323917
  messages: [{ role: "user", content: prompt }],
323299
323918
  maxTokens: 300,
323300
323919
  temperature: 0,
323920
+ ...this.signal ? { signal: this.signal } : {},
323301
323921
  ...this.localOnlyMatch ? { forceLocal: true } : {}
323302
323922
  });
323303
323923
  const match = /\[[\s\S]*\]/.exec(result.content);
@@ -323409,6 +324029,10 @@ Begin execution now.`;
323409
324029
  }
323410
324030
  };
323411
324031
  var PeerBus = class extends EventEmitter__default.default {
324032
+ constructor() {
324033
+ super();
324034
+ this.setMaxListeners(64);
324035
+ }
323412
324036
  outputs = /* @__PURE__ */ new Map();
323413
324037
  waiters = /* @__PURE__ */ new Map();
323414
324038
  members = /* @__PURE__ */ new Set();
@@ -324656,6 +325280,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
324656
325280
  const waveSignal = AbortSignal.any(
324657
325281
  [this.signal, this.waveAbortController.signal].filter(Boolean)
324658
325282
  );
325283
+ EventEmitter.setMaxListeners(64, waveSignal);
324659
325284
  const runOne = async (id) => {
324660
325285
  const assignment = sanitizedAssignments.find((a2) => a2.subtaskId === id);
324661
325286
  const worker = workerMap.get(id);
@@ -355694,6 +356319,18 @@ var ToolRegistry = class extends EventEmitter__default.default {
355694
356319
  this.tools.set(tool.name, tool);
355695
356320
  this.emit("tool:added", tool.name);
355696
356321
  }
356322
+ /**
356323
+ * The directory every registered tool resolves relative paths against.
356324
+ *
356325
+ * Exposed so a caller that has to reason about a file a tool WROTE can look
356326
+ * in the same place the tool wrote it. `process.cwd()` is not that place
356327
+ * outside a plain CLI run: the hosted server's cwd is the app directory
356328
+ * while the run's workspace is the tenant's scratch dir, and Electron's cwd
356329
+ * is the app bundle rather than the user's chosen folder.
356330
+ */
356331
+ getWorkspaceRoot() {
356332
+ return this.workspaceRoot;
356333
+ }
355697
356334
  /**
355698
356335
  * Wait until a named tool is registered, resolving immediately if it already exists.
355699
356336
  * T3 workers can call this after encountering a missing-tool error to resume
@@ -372591,115 +373228,6 @@ var GraphSearchTool = class extends BaseTool {
372591
373228
  }
372592
373229
  };
372593
373230
 
372594
- // src/core/context/compaction.ts
372595
- function estimateTokens(text) {
372596
- if (!text) return 0;
372597
- return Math.max(1, Math.ceil(text.length / 4));
372598
- }
372599
- function contentToText(content) {
372600
- if (typeof content === "string") return content;
372601
- return content.map((c4) => c4.type === "text" ? c4.text : c4.type === "tool_result" ? c4.content : `[${c4.type}]`).join(" ");
372602
- }
372603
- function messagesTokens(messages) {
372604
- return messages.reduce((n8, m3) => n8 + estimateTokens(contentToText(m3.content)), 0);
372605
- }
372606
- function atomize(text, maxChars) {
372607
- const seps = ["\n\n", "\n", ". ", " "];
372608
- let units = [text];
372609
- for (const sep3 of seps) {
372610
- if (units.every((u2) => u2.length <= maxChars)) break;
372611
- const next = [];
372612
- for (const u2 of units) {
372613
- if (u2.length <= maxChars) {
372614
- next.push(u2);
372615
- continue;
372616
- }
372617
- const parts = u2.split(sep3);
372618
- parts.forEach((p3, i4) => next.push(i4 < parts.length - 1 ? p3 + sep3 : p3));
372619
- }
372620
- units = next;
372621
- }
372622
- const out = [];
372623
- for (const u2 of units) {
372624
- if (u2.length <= maxChars) out.push(u2);
372625
- else for (let i4 = 0; i4 < u2.length; i4 += maxChars) out.push(u2.slice(i4, i4 + maxChars));
372626
- }
372627
- return out;
372628
- }
372629
- function chunkText(text, opts) {
372630
- const maxChars = Math.max(1, Math.floor(opts.maxTokens * 4));
372631
- const overlap = Math.min(maxChars - 1, Math.max(0, Math.floor(maxChars * (opts.overlapRatio ?? 0.1))));
372632
- const trimmed2 = text.trim();
372633
- if (!trimmed2) return [];
372634
- if (trimmed2.length <= maxChars) return [trimmed2];
372635
- const units = atomize(trimmed2, maxChars);
372636
- const chunks = [];
372637
- let cur = "";
372638
- for (const u2 of units) {
372639
- if (cur && cur.length + u2.length > maxChars) {
372640
- chunks.push(cur.trim());
372641
- cur = overlap > 0 ? cur.slice(-overlap) : "";
372642
- }
372643
- cur += u2;
372644
- }
372645
- if (cur.trim()) chunks.push(cur.trim());
372646
- return chunks;
372647
- }
372648
- function needsCompaction(totalTokens, contextWindow, reserve = 0.2) {
372649
- if (!contextWindow || contextWindow <= 0) return false;
372650
- return totalTokens > contextWindow * (1 - reserve);
372651
- }
372652
- async function mapReduceCompact(text, opts) {
372653
- const capChars = opts.capTokens * 4;
372654
- let working = text;
372655
- let truncated = false;
372656
- if (working.length > capChars) {
372657
- working = working.slice(0, capChars);
372658
- truncated = true;
372659
- }
372660
- const instruction = opts.instruction ?? "Summarize this content, preserving key facts, names, numbers, and decisions.";
372661
- let chunks = chunkText(working, { maxTokens: opts.chunkTokens });
372662
- if (chunks.length <= 1) return { text: working, chunks: chunks.length, calls: 0, truncated };
372663
- let calls = 0;
372664
- let parts = await Promise.all(
372665
- chunks.map((c4, i4) => {
372666
- calls++;
372667
- return opts.summarize(c4, `${instruction} (part ${i4 + 1} of ${chunks.length})`);
372668
- })
372669
- );
372670
- let combined = parts.join("\n\n");
372671
- const maxPasses = opts.maxReducePasses ?? 3;
372672
- for (let pass = 0; pass < maxPasses && estimateTokens(combined) > opts.targetTokens; pass++) {
372673
- const groups = chunkText(combined, { maxTokens: opts.chunkTokens });
372674
- if (groups.length <= 1) break;
372675
- parts = await Promise.all(
372676
- groups.map((g2) => {
372677
- calls++;
372678
- return opts.summarize(g2, "Merge these partial summaries into one cohesive summary; keep all distinct facts.");
372679
- })
372680
- );
372681
- combined = parts.join("\n\n");
372682
- }
372683
- const note = truncated ? "\n\n[Note: input exceeded the extended-context cap and was truncated.]" : "";
372684
- return { text: combined + note, chunks: chunks.length, calls, truncated };
372685
- }
372686
- async function rollingSummary(messages, opts) {
372687
- if (messages.length <= opts.keepRecent) return messages;
372688
- const older = messages.slice(0, messages.length - opts.keepRecent);
372689
- const recent = messages.slice(messages.length - opts.keepRecent);
372690
- const transcript = older.map((m3) => `${m3.role}: ${contentToText(m3.content)}`).join("\n");
372691
- const summary = await opts.summarize(
372692
- transcript,
372693
- "Summarize this earlier conversation. Preserve facts, decisions, names, numbers, and any open threads or todos."
372694
- );
372695
- const summaryMessage = {
372696
- role: "system",
372697
- content: `Summary of earlier conversation (folded to fit the context window):
372698
- ${summary}`
372699
- };
372700
- return [summaryMessage, ...recent];
372701
- }
372702
-
372703
373231
  // src/core/steering/guidance.ts
372704
373232
  var GuidanceQueue = class {
372705
373233
  entries = [];
@@ -378626,10 +379154,10 @@ ${cur}`;
378626
379154
  }
378627
379155
 
378628
379156
  // src/retrieval/plan.ts
378629
- var CHARS_PER_TOKEN = 4;
379157
+ var CHARS_PER_TOKEN2 = 4;
378630
379158
  function cagCharBudget(contextWindowTokens, opts = {}) {
378631
379159
  const docFraction = opts.docFraction ?? 0.5;
378632
- const charsPerToken = opts.charsPerToken ?? CHARS_PER_TOKEN;
379160
+ const charsPerToken = opts.charsPerToken ?? CHARS_PER_TOKEN2;
378633
379161
  return Math.max(0, Math.floor(contextWindowTokens * docFraction * charsPerToken));
378634
379162
  }
378635
379163
  function planRetrieval(ctx) {
@@ -379633,7 +380161,7 @@ exports.CASCADE_IGNORE_FILE = CASCADE_IGNORE_FILE;
379633
380161
  exports.CASCADE_KEYSTORE_FILE = CASCADE_KEYSTORE_FILE;
379634
380162
  exports.CASCADE_MD_FILE = CASCADE_MD_FILE;
379635
380163
  exports.CASCADE_VERSION = CASCADE_VERSION;
379636
- exports.CHARS_PER_TOKEN = CHARS_PER_TOKEN;
380164
+ exports.CHARS_PER_TOKEN = CHARS_PER_TOKEN2;
379637
380165
  exports.COMPLEXITY_T2_COUNT = COMPLEXITY_T2_COUNT;
379638
380166
  exports.Cascade = Cascade;
379639
380167
  exports.CascadeCancelledError = CascadeCancelledError;