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.
package/dist/index.cjs CHANGED
@@ -1792,6 +1792,15 @@ function findPricing(modelId, provider, opts = {}) {
1792
1792
  }
1793
1793
  return null;
1794
1794
  }
1795
+ function hasContextBands(model) {
1796
+ if (model.isLocal) return false;
1797
+ const fallbackProviders = model.provider === "azure" ? ["openai"] : model.provider === "gemini" ? ["vertex"] : [];
1798
+ const match = findPricing(model.id, model.provider, {
1799
+ baseModelId: model.baseModelId,
1800
+ fallbackProviders
1801
+ });
1802
+ return (match?.entry.tiers?.length ?? 0) > 1;
1803
+ }
1795
1804
  function tierFor(entry, inputTokens = 0) {
1796
1805
  const tiers = entry.tiers;
1797
1806
  if (!tiers?.length) return null;
@@ -1908,7 +1917,7 @@ function reconcilePrice(model, live) {
1908
1917
  }
1909
1918
 
1910
1919
  // src/constants.ts
1911
- var CASCADE_VERSION = "0.71.0";
1920
+ var CASCADE_VERSION = "0.73.0";
1912
1921
  var CASCADE_CONFIG_DIR = ".cascade";
1913
1922
  var CASCADE_MD_FILE = "CASCADE.md";
1914
1923
  var CASCADE_IGNORE_FILE = ".cascadeignore";
@@ -2353,8 +2362,23 @@ var PROVIDER_DISPLAY_NAMES = {
2353
2362
  };
2354
2363
 
2355
2364
  // src/utils/cost.ts
2356
- function resolveModelPricing(model) {
2365
+ function resolveModelPricing(model, opts = {}) {
2357
2366
  if (model.isLocal) return { input: 0, output: 0, unknown: false };
2367
+ if (opts.inputTokens != null && hasContextBands(model)) {
2368
+ const banded = resolvePricing(model, { inputTokens: opts.inputTokens });
2369
+ const base = resolvePricing(model, { inputTokens: 0 });
2370
+ if (!banded.unknown && !base.unknown) {
2371
+ const scale = (stamped, baseRate) => stamped > 0 && baseRate > 0 ? stamped / baseRate : 1;
2372
+ const inScale = scale(model.inputCostPer1kTokens, base.input);
2373
+ const outScale = scale(model.outputCostPer1kTokens, base.output);
2374
+ return {
2375
+ input: banded.input * inScale,
2376
+ output: banded.output * outScale,
2377
+ unknown: false,
2378
+ ...banded.estimatedFromProvider ? { estimatedFromProvider: banded.estimatedFromProvider } : {}
2379
+ };
2380
+ }
2381
+ }
2358
2382
  if (model.inputCostPer1kTokens > 0 || model.outputCostPer1kTokens > 0) {
2359
2383
  return {
2360
2384
  input: model.inputCostPer1kTokens,
@@ -2362,7 +2386,7 @@ function resolveModelPricing(model) {
2362
2386
  unknown: false
2363
2387
  };
2364
2388
  }
2365
- const fromDataset = resolvePricing(model);
2389
+ const fromDataset = resolvePricing(model, { inputTokens: opts.inputTokens });
2366
2390
  if (!fromDataset.unknown) {
2367
2391
  return {
2368
2392
  input: fromDataset.input,
@@ -2384,11 +2408,11 @@ function resolveModelPricing(model) {
2384
2408
  return { input: 0, output: 0, unknown: true };
2385
2409
  }
2386
2410
  function calculateCost(inputTokens, outputTokens, model) {
2387
- const { input, output } = resolveModelPricing(model);
2411
+ const { input, output } = resolveModelPricing(model, { inputTokens });
2388
2412
  return inputTokens / 1e3 * input + outputTokens / 1e3 * output;
2389
2413
  }
2390
2414
  function buildTokenUsage(inputTokens, outputTokens, model) {
2391
- const { input, output, unknown } = resolveModelPricing(model);
2415
+ const { input, output, unknown } = resolveModelPricing(model, { inputTokens });
2392
2416
  return {
2393
2417
  inputTokens,
2394
2418
  outputTokens,
@@ -2467,6 +2491,21 @@ function anthropicThinkingParam(modelId, maxTokens) {
2467
2491
  if (budget < 1024) return {};
2468
2492
  return { thinking: { type: "enabled", budget_tokens: budget } };
2469
2493
  }
2494
+ function toAnthropicTools(tools) {
2495
+ return tools.map((t) => ({
2496
+ name: t.name,
2497
+ description: t.description,
2498
+ input_schema: t.inputSchema
2499
+ }));
2500
+ }
2501
+ function toAnthropicToolUse(toolCalls) {
2502
+ return toolCalls.map((tc) => ({
2503
+ type: "tool_use",
2504
+ id: tc.id,
2505
+ name: tc.name,
2506
+ input: tc.input
2507
+ }));
2508
+ }
2470
2509
  var AnthropicProvider = class extends BaseProvider {
2471
2510
  client;
2472
2511
  constructor(config, model) {
@@ -2488,11 +2527,7 @@ var AnthropicProvider = class extends BaseProvider {
2488
2527
  }
2489
2528
  async generateStream(options, onChunk) {
2490
2529
  const messages = this.convertMessages(options.messages);
2491
- const tools = options.tools?.map((t) => ({
2492
- name: t.name,
2493
- description: t.description,
2494
- input_schema: t.inputSchema
2495
- }));
2530
+ const tools = options.tools && toAnthropicTools(options.tools);
2496
2531
  let fullContent = "";
2497
2532
  let inputTokens = 0;
2498
2533
  let outputTokens = 0;
@@ -2620,14 +2655,7 @@ var AnthropicProvider = class extends BaseProvider {
2620
2655
  const content = [];
2621
2656
  const text = typeof m.content === "string" ? m.content : "";
2622
2657
  if (text) content.push({ type: "text", text });
2623
- for (const tc of m.toolCalls ?? []) {
2624
- content.push({
2625
- type: "tool_use",
2626
- id: tc.id,
2627
- name: tc.name,
2628
- input: tc.input
2629
- });
2630
- }
2658
+ content.push(...toAnthropicToolUse(m.toolCalls ?? []));
2631
2659
  if (content.length > 0) {
2632
2660
  result.push({ role: "assistant", content });
2633
2661
  }
@@ -2667,6 +2695,26 @@ function isParamShapeError(err) {
2667
2695
  const msg = String(e?.message ?? e?.error?.message ?? "").toLowerCase();
2668
2696
  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"));
2669
2697
  }
2698
+ function toOpenAITools(tools) {
2699
+ return tools.map((t) => ({
2700
+ type: "function",
2701
+ function: {
2702
+ name: t.name,
2703
+ description: t.description,
2704
+ parameters: t.inputSchema
2705
+ }
2706
+ }));
2707
+ }
2708
+ function toOpenAIToolCalls(toolCalls) {
2709
+ return toolCalls.map((tc) => ({
2710
+ id: tc.id,
2711
+ type: "function",
2712
+ function: {
2713
+ name: tc.name,
2714
+ arguments: JSON.stringify(tc.input)
2715
+ }
2716
+ }));
2717
+ }
2670
2718
  var OpenAIProvider = class extends BaseProvider {
2671
2719
  client;
2672
2720
  /** Once we learn (from the model id or an API error) that this deployment
@@ -2686,14 +2734,7 @@ var OpenAIProvider = class extends BaseProvider {
2686
2734
  }
2687
2735
  async generateStream(options, onChunk) {
2688
2736
  const messages = this.convertMessages(options.messages, options.systemPrompt);
2689
- const tools = options.tools?.map((t) => ({
2690
- type: "function",
2691
- function: {
2692
- name: t.name,
2693
- description: t.description,
2694
- parameters: t.inputSchema
2695
- }
2696
- }));
2737
+ const tools = options.tools && toOpenAITools(options.tools);
2697
2738
  let fullContent = "";
2698
2739
  let inputTokens = 0;
2699
2740
  let outputTokens = 0;
@@ -2839,14 +2880,7 @@ var OpenAIProvider = class extends BaseProvider {
2839
2880
  result.push({
2840
2881
  role: "assistant",
2841
2882
  content: m.content || "",
2842
- tool_calls: m.toolCalls.map((toolCall) => ({
2843
- id: toolCall.id,
2844
- type: "function",
2845
- function: {
2846
- name: toolCall.name,
2847
- arguments: JSON.stringify(toolCall.input)
2848
- }
2849
- }))
2883
+ tool_calls: toOpenAIToolCalls(m.toolCalls)
2850
2884
  });
2851
2885
  } else {
2852
2886
  result.push({ role: m.role, content: m.content });
@@ -3089,6 +3123,26 @@ function toGeminiParameters(inputSchema) {
3089
3123
  }
3090
3124
 
3091
3125
  // src/providers/gemini.ts
3126
+ function toGeminiTools(tools) {
3127
+ return tools.map((tool) => ({
3128
+ name: tool.name,
3129
+ description: tool.description,
3130
+ // Convert, never cast. Gemini's parameters field is an OpenAPI subset
3131
+ // that 400s on unknown keys, and MCP servers ship schemas carrying their
3132
+ // own extensions (GitHub's adds `x-mcp-header` to every header-bound
3133
+ // property). Casting sent those through verbatim and every request failed
3134
+ // before the model saw it. See providers/gemini-schema.ts.
3135
+ parameters: toGeminiParameters(tool.inputSchema)
3136
+ }));
3137
+ }
3138
+ function toGeminiFunctionCalls(toolCalls) {
3139
+ return toolCalls.map((tc) => ({
3140
+ functionCall: {
3141
+ name: tc.name,
3142
+ args: tc.input
3143
+ }
3144
+ }));
3145
+ }
3092
3146
  var GeminiProvider = class extends BaseProvider {
3093
3147
  client;
3094
3148
  constructor(config, model) {
@@ -3110,7 +3164,7 @@ var GeminiProvider = class extends BaseProvider {
3110
3164
  { category: genai.HarmCategory.HARM_CATEGORY_HARASSMENT, threshold: genai.HarmBlockThreshold.BLOCK_NONE },
3111
3165
  { category: genai.HarmCategory.HARM_CATEGORY_HATE_SPEECH, threshold: genai.HarmBlockThreshold.BLOCK_NONE }
3112
3166
  ],
3113
- tools: options.tools?.length ? [{ functionDeclarations: options.tools.map(this.convertTool) }] : void 0,
3167
+ tools: options.tools?.length ? [{ functionDeclarations: toGeminiTools(options.tools) }] : void 0,
3114
3168
  abortSignal: options.signal
3115
3169
  }
3116
3170
  });
@@ -3251,14 +3305,7 @@ var GeminiProvider = class extends BaseProvider {
3251
3305
  const parts = [];
3252
3306
  const textContent = typeof m.content === "string" ? m.content : "";
3253
3307
  if (textContent) parts.push({ text: textContent });
3254
- for (const tc of m.toolCalls ?? []) {
3255
- parts.push({
3256
- functionCall: {
3257
- name: tc.name,
3258
- args: tc.input
3259
- }
3260
- });
3261
- }
3308
+ parts.push(...toGeminiFunctionCalls(m.toolCalls ?? []));
3262
3309
  if (parts.length > 0) {
3263
3310
  contents.push({ role: "model", parts });
3264
3311
  }
@@ -3305,18 +3352,6 @@ var GeminiProvider = class extends BaseProvider {
3305
3352
  }
3306
3353
  return parts;
3307
3354
  }
3308
- convertTool(tool) {
3309
- return {
3310
- name: tool.name,
3311
- description: tool.description,
3312
- // Convert, never cast. Gemini's parameters field is an OpenAPI subset
3313
- // that 400s on unknown keys, and MCP servers ship schemas carrying their
3314
- // own extensions (GitHub's adds `x-mcp-header` to every header-bound
3315
- // property). Casting sent those through verbatim and every request failed
3316
- // before the model saw it. See providers/gemini-schema.ts.
3317
- parameters: toGeminiParameters(tool.inputSchema)
3318
- };
3319
- }
3320
3355
  };
3321
3356
  try {
3322
3357
  dns__default.default.setDefaultResultOrder("ipv4first");
@@ -3409,6 +3444,24 @@ function isToolCapable(modelName) {
3409
3444
  const name = modelName.toLowerCase();
3410
3445
  return TOOL_CAPABLE_FAMILIES.some((family) => name.includes(family));
3411
3446
  }
3447
+ function toOllamaTools(tools) {
3448
+ return tools.map((t) => ({
3449
+ type: "function",
3450
+ function: {
3451
+ name: t.name,
3452
+ description: t.description,
3453
+ parameters: t.inputSchema
3454
+ }
3455
+ }));
3456
+ }
3457
+ function toOllamaToolCalls(toolCalls) {
3458
+ return toolCalls.map((tc) => ({
3459
+ function: {
3460
+ name: tc.name,
3461
+ arguments: tc.input
3462
+ }
3463
+ }));
3464
+ }
3412
3465
  var OllamaProvider = class extends BaseProvider {
3413
3466
  baseUrl;
3414
3467
  constructor(config, model) {
@@ -3421,14 +3474,7 @@ var OllamaProvider = class extends BaseProvider {
3421
3474
  }
3422
3475
  async generateStream(options, onChunk) {
3423
3476
  const messages = this.convertMessages(options.messages, options.systemPrompt);
3424
- const ollamaTools = options.tools?.map((t) => ({
3425
- type: "function",
3426
- function: {
3427
- name: t.name,
3428
- description: t.description,
3429
- parameters: t.inputSchema
3430
- }
3431
- }));
3477
+ const ollamaTools = options.tools && toOllamaTools(options.tools);
3432
3478
  const response = await fetch(`${this.baseUrl}/api/chat`, {
3433
3479
  method: "POST",
3434
3480
  headers: { "Content-Type": "application/json" },
@@ -3608,12 +3654,7 @@ var OllamaProvider = class extends BaseProvider {
3608
3654
  result.push({
3609
3655
  role: "assistant",
3610
3656
  content: typeof m.content === "string" ? m.content : "",
3611
- tool_calls: m.toolCalls.map((tc) => ({
3612
- function: {
3613
- name: tc.name,
3614
- arguments: tc.input
3615
- }
3616
- }))
3657
+ tool_calls: toOllamaToolCalls(m.toolCalls)
3617
3658
  });
3618
3659
  continue;
3619
3660
  }
@@ -4014,6 +4055,21 @@ var DEFAULT_PROVIDER_TPM = {
4014
4055
  "openai-compatible": 3e4,
4015
4056
  ollama: Number.POSITIVE_INFINITY
4016
4057
  };
4058
+ function abortReason(signal) {
4059
+ return signal.reason instanceof Error ? signal.reason : new Error("Aborted while waiting for rate-limit capacity");
4060
+ }
4061
+ function sleepUnlessAborted(ms, signal) {
4062
+ if (!signal) return new Promise((resolve2) => setTimeout(resolve2, ms));
4063
+ return new Promise((resolve2) => {
4064
+ const done = () => {
4065
+ clearTimeout(timer);
4066
+ signal.removeEventListener("abort", done);
4067
+ resolve2();
4068
+ };
4069
+ const timer = setTimeout(done, ms);
4070
+ signal.addEventListener("abort", done, { once: true });
4071
+ });
4072
+ }
4017
4073
  var TpmLimiter = class {
4018
4074
  buckets = /* @__PURE__ */ new Map();
4019
4075
  constructor(overrides = {}) {
@@ -4032,9 +4088,10 @@ var TpmLimiter = class {
4032
4088
  * are reported back via `refund` when short, or simply settled at the next
4033
4089
  * refill.
4034
4090
  */
4035
- async acquire(provider, estimatedTokens) {
4091
+ async acquire(provider, estimatedTokens, signal) {
4036
4092
  const bucket = this.buckets.get(provider);
4037
4093
  if (!bucket || bucket.tokensPerMinute === Number.POSITIVE_INFINITY) return;
4094
+ if (signal?.aborted) throw abortReason(signal);
4038
4095
  const want = Math.min(estimatedTokens, bucket.tokensPerMinute);
4039
4096
  for (; ; ) {
4040
4097
  this.refill(bucket);
@@ -4044,7 +4101,8 @@ var TpmLimiter = class {
4044
4101
  }
4045
4102
  const deficit = want - bucket.available;
4046
4103
  const waitMs = Math.max(50, Math.ceil(deficit / bucket.tokensPerMinute * 6e4));
4047
- await new Promise((resolve2) => setTimeout(resolve2, waitMs));
4104
+ await sleepUnlessAborted(waitMs, signal);
4105
+ if (signal?.aborted) throw abortReason(signal);
4048
4106
  }
4049
4107
  }
4050
4108
  /**
@@ -4090,6 +4148,9 @@ var TpmLimiter = class {
4090
4148
  };
4091
4149
 
4092
4150
  // src/core/router/local-queue.ts
4151
+ function abortReason2(signal) {
4152
+ return signal.reason instanceof Error ? signal.reason : new Error("Aborted while waiting for a local inference slot");
4153
+ }
4093
4154
  var LocalRequestQueue = class {
4094
4155
  maxConcurrent;
4095
4156
  active = 0;
@@ -4100,9 +4161,16 @@ var LocalRequestQueue = class {
4100
4161
  /**
4101
4162
  * Acquire a queue slot. Returns a `release` function that MUST be called
4102
4163
  * when the inference call is done (even on error). Rejects if the slot
4103
- * cannot be acquired within `timeoutMs`.
4164
+ * cannot be acquired within `timeoutMs`, or as soon as `signal` fires.
4165
+ *
4166
+ * The signal matters as much as the timeout: with concurrency of 1 a caller
4167
+ * can sit here for half the inference timeout — 150 seconds by default —
4168
+ * and until it was wired up, neither cancelling the run nor spending its
4169
+ * budget got it out. It waited out the window for a slot it would drop the
4170
+ * moment it received one.
4104
4171
  */
4105
- async acquire(timeoutMs) {
4172
+ async acquire(timeoutMs, signal) {
4173
+ if (signal?.aborted) throw abortReason2(signal);
4106
4174
  if (this.active < this.maxConcurrent) {
4107
4175
  this.active++;
4108
4176
  return this.makeRelease();
@@ -4110,23 +4178,36 @@ var LocalRequestQueue = class {
4110
4178
  return new Promise((resolve2, reject) => {
4111
4179
  let settled = false;
4112
4180
  let timer;
4181
+ const cleanup = () => {
4182
+ if (timer !== void 0) clearTimeout(timer);
4183
+ signal?.removeEventListener("abort", onAbort);
4184
+ };
4113
4185
  const resolver = (release) => {
4114
4186
  if (settled) return;
4115
4187
  settled = true;
4116
- if (timer !== void 0) clearTimeout(timer);
4188
+ cleanup();
4117
4189
  resolve2(release);
4118
4190
  };
4191
+ const abandon = () => {
4192
+ if (settled) return false;
4193
+ settled = true;
4194
+ cleanup();
4195
+ const idx = this.queue.indexOf(resolver);
4196
+ if (idx !== -1) this.queue.splice(idx, 1);
4197
+ return true;
4198
+ };
4199
+ function onAbort() {
4200
+ if (abandon()) reject(abortReason2(signal));
4201
+ }
4119
4202
  if (timeoutMs !== void 0 && timeoutMs > 0) {
4120
4203
  timer = setTimeout(() => {
4121
- if (settled) return;
4122
- settled = true;
4123
- const idx = this.queue.indexOf(resolver);
4124
- if (idx !== -1) this.queue.splice(idx, 1);
4204
+ if (!abandon()) return;
4125
4205
  reject(new Error(
4126
4206
  `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.`
4127
4207
  ));
4128
4208
  }, timeoutMs);
4129
4209
  }
4210
+ signal?.addEventListener("abort", onAbort, { once: true });
4130
4211
  this.queue.push(resolver);
4131
4212
  });
4132
4213
  }
@@ -4263,6 +4344,116 @@ function fileDeadModelPersistence(filePath) {
4263
4344
  };
4264
4345
  }
4265
4346
 
4347
+ // src/core/context/compaction.ts
4348
+ var CHARS_PER_TOKEN = 4;
4349
+ function estimateTokens(text) {
4350
+ if (!text) return 0;
4351
+ return Math.max(1, Math.ceil(text.length / CHARS_PER_TOKEN));
4352
+ }
4353
+ function contentToText(content) {
4354
+ if (typeof content === "string") return content;
4355
+ return content.map((c) => c.type === "text" ? c.text : c.type === "tool_result" ? c.content : `[${c.type}]`).join(" ");
4356
+ }
4357
+ function messagesTokens(messages) {
4358
+ return messages.reduce((n, m) => n + estimateTokens(contentToText(m.content)), 0);
4359
+ }
4360
+ function atomize(text, maxChars) {
4361
+ const seps = ["\n\n", "\n", ". ", " "];
4362
+ let units = [text];
4363
+ for (const sep of seps) {
4364
+ if (units.every((u) => u.length <= maxChars)) break;
4365
+ const next = [];
4366
+ for (const u of units) {
4367
+ if (u.length <= maxChars) {
4368
+ next.push(u);
4369
+ continue;
4370
+ }
4371
+ const parts = u.split(sep);
4372
+ parts.forEach((p, i) => next.push(i < parts.length - 1 ? p + sep : p));
4373
+ }
4374
+ units = next;
4375
+ }
4376
+ const out = [];
4377
+ for (const u of units) {
4378
+ if (u.length <= maxChars) out.push(u);
4379
+ else for (let i = 0; i < u.length; i += maxChars) out.push(u.slice(i, i + maxChars));
4380
+ }
4381
+ return out;
4382
+ }
4383
+ function chunkText(text, opts) {
4384
+ const maxChars = Math.max(1, Math.floor(opts.maxTokens * 4));
4385
+ const overlap = Math.min(maxChars - 1, Math.max(0, Math.floor(maxChars * (opts.overlapRatio ?? 0.1))));
4386
+ const trimmed = text.trim();
4387
+ if (!trimmed) return [];
4388
+ if (trimmed.length <= maxChars) return [trimmed];
4389
+ const units = atomize(trimmed, maxChars);
4390
+ const chunks = [];
4391
+ let cur = "";
4392
+ for (const u of units) {
4393
+ if (cur && cur.length + u.length > maxChars) {
4394
+ chunks.push(cur.trim());
4395
+ cur = overlap > 0 ? cur.slice(-overlap) : "";
4396
+ }
4397
+ cur += u;
4398
+ }
4399
+ if (cur.trim()) chunks.push(cur.trim());
4400
+ return chunks;
4401
+ }
4402
+ function needsCompaction(totalTokens, contextWindow, reserve = 0.2) {
4403
+ if (!contextWindow || contextWindow <= 0) return false;
4404
+ return totalTokens > contextWindow * (1 - reserve);
4405
+ }
4406
+ async function mapReduceCompact(text, opts) {
4407
+ const capChars = opts.capTokens * 4;
4408
+ let working = text;
4409
+ let truncated = false;
4410
+ if (working.length > capChars) {
4411
+ working = working.slice(0, capChars);
4412
+ truncated = true;
4413
+ }
4414
+ const instruction = opts.instruction ?? "Summarize this content, preserving key facts, names, numbers, and decisions.";
4415
+ let chunks = chunkText(working, { maxTokens: opts.chunkTokens });
4416
+ if (chunks.length <= 1) return { text: working, chunks: chunks.length, calls: 0, truncated };
4417
+ let calls = 0;
4418
+ let parts = await Promise.all(
4419
+ chunks.map((c, i) => {
4420
+ calls++;
4421
+ return opts.summarize(c, `${instruction} (part ${i + 1} of ${chunks.length})`);
4422
+ })
4423
+ );
4424
+ let combined = parts.join("\n\n");
4425
+ const maxPasses = opts.maxReducePasses ?? 3;
4426
+ for (let pass = 0; pass < maxPasses && estimateTokens(combined) > opts.targetTokens; pass++) {
4427
+ const groups = chunkText(combined, { maxTokens: opts.chunkTokens });
4428
+ if (groups.length <= 1) break;
4429
+ parts = await Promise.all(
4430
+ groups.map((g) => {
4431
+ calls++;
4432
+ return opts.summarize(g, "Merge these partial summaries into one cohesive summary; keep all distinct facts.");
4433
+ })
4434
+ );
4435
+ combined = parts.join("\n\n");
4436
+ }
4437
+ const note = truncated ? "\n\n[Note: input exceeded the extended-context cap and was truncated.]" : "";
4438
+ return { text: combined + note, chunks: chunks.length, calls, truncated };
4439
+ }
4440
+ async function rollingSummary(messages, opts) {
4441
+ if (messages.length <= opts.keepRecent) return messages;
4442
+ const older = messages.slice(0, messages.length - opts.keepRecent);
4443
+ const recent = messages.slice(messages.length - opts.keepRecent);
4444
+ const transcript = older.map((m) => `${m.role}: ${contentToText(m.content)}`).join("\n");
4445
+ const summary = await opts.summarize(
4446
+ transcript,
4447
+ "Summarize this earlier conversation. Preserve facts, decisions, names, numbers, and any open threads or todos."
4448
+ );
4449
+ const summaryMessage = {
4450
+ role: "system",
4451
+ content: `Summary of earlier conversation (folded to fit the context window):
4452
+ ${summary}`
4453
+ };
4454
+ return [summaryMessage, ...recent];
4455
+ }
4456
+
4266
4457
  // src/utils/retry.ts
4267
4458
  var CascadeCancelledError = class extends Error {
4268
4459
  constructor(reason) {
@@ -4297,6 +4488,158 @@ async function withTimeout(promise, timeoutMs, errorMessage = "Operation timed o
4297
4488
  if (timer !== void 0) clearTimeout(timer);
4298
4489
  }
4299
4490
  }
4491
+ async function withTimeoutAbort(run, timeoutMs, errorMessage = "Operation timed out", outer) {
4492
+ const outers = (Array.isArray(outer) ? outer : [outer]).filter((sig) => sig !== void 0);
4493
+ const controller = new AbortController();
4494
+ let timer;
4495
+ let rejectRace;
4496
+ const settle = (reason) => {
4497
+ rejectRace?.(reason);
4498
+ controller.abort(reason);
4499
+ };
4500
+ const abortOuter = () => {
4501
+ const fired = outers.find((sig) => sig.aborted);
4502
+ settle(fired?.reason instanceof Error ? fired.reason : new CascadeCancelledError("Run cancelled"));
4503
+ };
4504
+ const failPromise = new Promise((_, reject) => {
4505
+ rejectRace = reject;
4506
+ timer = setTimeout(() => settle(new Error(errorMessage)), timeoutMs);
4507
+ });
4508
+ try {
4509
+ if (outers.some((sig) => sig.aborted)) {
4510
+ abortOuter();
4511
+ return await failPromise;
4512
+ }
4513
+ for (const sig of outers) sig.addEventListener("abort", abortOuter, { once: true });
4514
+ return await Promise.race([run(controller.signal), failPromise]);
4515
+ } finally {
4516
+ if (timer !== void 0) clearTimeout(timer);
4517
+ for (const sig of outers) sig.removeEventListener("abort", abortOuter);
4518
+ }
4519
+ }
4520
+ function anySignal(sources) {
4521
+ const live = sources.filter((s) => s !== void 0);
4522
+ if (live.length === 0) return { release: () => {
4523
+ } };
4524
+ if (live.length === 1) return { signal: live[0], release: () => {
4525
+ } };
4526
+ const controller = new AbortController();
4527
+ function detach() {
4528
+ for (const s of live) s.removeEventListener("abort", onAbort);
4529
+ }
4530
+ function onAbort() {
4531
+ const fired = live.find((s) => s.aborted);
4532
+ controller.abort(fired?.reason instanceof Error ? fired.reason : new CascadeCancelledError("Run cancelled"));
4533
+ detach();
4534
+ }
4535
+ if (live.some((s) => s.aborted)) {
4536
+ onAbort();
4537
+ return { signal: controller.signal, release: detach };
4538
+ }
4539
+ for (const s of live) s.addEventListener("abort", onAbort, { once: true });
4540
+ return { signal: controller.signal, release: detach };
4541
+ }
4542
+
4543
+ // src/core/router/wire-profile.ts
4544
+ var textAndImageOnly = (block) => block.type === "text" || block.type === "image";
4545
+ var ANTHROPIC = {
4546
+ // convertMessages: `if (m.role === 'system') continue` — history system
4547
+ // messages are skipped outright; only options.systemPrompt goes as system
4548
+ // input. Compaction emits exactly such a message, holding a summary of the
4549
+ // whole conversation, so charging it refused runs over input never sent.
4550
+ dropsMessage: (m) => m.role === "system",
4551
+ blockHandling: (m) => {
4552
+ if (m.role === "tool") return "stringified";
4553
+ if (m.role === "assistant") return "dropped";
4554
+ return "blocks";
4555
+ },
4556
+ sendsToolCalls: (m) => m.role === "assistant",
4557
+ sendsBlock: textAndImageOnly,
4558
+ sendsToolCallId: true,
4559
+ sizeToolCalls: toAnthropicToolUse,
4560
+ sendsUrlImages: true,
4561
+ readsTopLevelImages: false,
4562
+ sizeTools: toAnthropicTools
4563
+ };
4564
+ var GEMINI = {
4565
+ // buildContents folds a system message into the next user turn, but only
4566
+ // when it has text: array content collapses to '' and the message is
4567
+ // skipped, as is a whitespace-only string.
4568
+ dropsMessage: (m) => m.role === "system" && !(typeof m.content === "string" ? m.content : "").trim(),
4569
+ blockHandling: (m) => {
4570
+ if (m.role === "tool") return "stringified";
4571
+ if (m.role === "assistant" || m.role === "system") return "dropped";
4572
+ return "blocks";
4573
+ },
4574
+ sendsToolCalls: (m) => m.role === "assistant",
4575
+ sendsBlock: textAndImageOnly,
4576
+ sendsToolCallId: true,
4577
+ sizeToolCalls: toGeminiFunctionCalls,
4578
+ sendsUrlImages: false,
4579
+ readsTopLevelImages: true,
4580
+ sizeTools: toGeminiTools
4581
+ };
4582
+ var OPENAI = {
4583
+ dropsMessage: () => false,
4584
+ blockHandling: (m) => {
4585
+ if (m.role === "system" || m.role === "tool") return "dropped";
4586
+ return "blocks";
4587
+ },
4588
+ // The tool_calls branch is inside `if (typeof m.content === 'string')`; an
4589
+ // assistant turn with array content falls through to the parts branch and
4590
+ // its tool calls are never attached.
4591
+ sendsToolCalls: (m) => m.role === "assistant" && typeof m.content === "string",
4592
+ sendsBlock: textAndImageOnly,
4593
+ sendsToolCallId: true,
4594
+ sizeToolCalls: toOpenAIToolCalls,
4595
+ sendsUrlImages: true,
4596
+ readsTopLevelImages: false,
4597
+ sizeTools: toOpenAITools
4598
+ };
4599
+ var OLLAMA = {
4600
+ dropsMessage: () => false,
4601
+ blockHandling: (m) => {
4602
+ if (m.role === "system") return "dropped";
4603
+ if (m.role === "tool") return "stringified";
4604
+ if (m.role === "assistant" && m.toolCalls?.length) return "dropped";
4605
+ return "blocks";
4606
+ },
4607
+ sendsToolCalls: (m) => m.role === "assistant",
4608
+ // The image split maps `b.image.data` with no check on `type`, so a URL is
4609
+ // submitted too (as a URL string in the `images` array).
4610
+ sendsBlock: textAndImageOnly,
4611
+ sendsToolCallId: false,
4612
+ sizeToolCalls: toOllamaToolCalls,
4613
+ sendsUrlImages: true,
4614
+ readsTopLevelImages: false,
4615
+ sizeTools: toOllamaTools
4616
+ };
4617
+ var PROFILES = {
4618
+ anthropic: ANTHROPIC,
4619
+ gemini: GEMINI,
4620
+ openai: OPENAI,
4621
+ // AzureOpenAIProvider and OpenAICompatibleProvider both extend OpenAIProvider
4622
+ // and neither overrides convertMessages.
4623
+ azure: OPENAI,
4624
+ "openai-compatible": OPENAI,
4625
+ ollama: OLLAMA
4626
+ };
4627
+ function wireProfile(provider) {
4628
+ return PROFILES[provider] ?? OPENAI;
4629
+ }
4630
+ function geminiImageCopies(messages) {
4631
+ for (const m of messages) {
4632
+ if (m.role === "user") {
4633
+ const laterUserTurn = messages.slice(messages.indexOf(m) + 1).some((x) => x.role === "user");
4634
+ return laterUserTurn ? 2 : 1;
4635
+ }
4636
+ const text = typeof m.content === "string" ? m.content : "";
4637
+ if (m.role === "system" && text.trim()) return 1;
4638
+ if (m.role === "tool") return 1;
4639
+ if (m.role === "assistant" && (text || m.toolCalls?.length)) return 1;
4640
+ }
4641
+ return 1;
4642
+ }
4300
4643
 
4301
4644
  // src/core/router/model-profiler.ts
4302
4645
  var SKIP_PATTERN = /embed|dall-e|whisper|tts|vision|instruct-vision|rerank/i;
@@ -4801,6 +5144,36 @@ function azureModelForTier(tier, ranked) {
4801
5144
  if (tier === "T3") return ranked[ranked.length - 1];
4802
5145
  return ranked[Math.min(1, ranked.length - 1)];
4803
5146
  }
5147
+ var IMAGE_TOKENS_MAX = 2e3;
5148
+ var IMAGE_TOKENS_EACH = IMAGE_TOKENS_MAX;
5149
+ function safeJson(value) {
5150
+ try {
5151
+ return JSON.stringify(value) ?? "";
5152
+ } catch {
5153
+ return "";
5154
+ }
5155
+ }
5156
+ function newRunAbort() {
5157
+ const controller = new AbortController();
5158
+ EventEmitter.setMaxListeners(64, controller.signal);
5159
+ return controller;
5160
+ }
5161
+ var TOKENS_PER_MESSAGE_FRAMING = 4;
5162
+ function guardTokens(text) {
5163
+ if (!text) return 0;
5164
+ let asciiChars = 0;
5165
+ let denseBytes = 0;
5166
+ for (const ch of text) {
5167
+ const cp = ch.codePointAt(0);
5168
+ if (cp <= 127) {
5169
+ asciiChars++;
5170
+ continue;
5171
+ }
5172
+ denseBytes += cp <= 2047 ? 2 : cp <= 65535 ? 3 : 4;
5173
+ }
5174
+ if (denseBytes === 0) return estimateTokens(text);
5175
+ return Math.ceil(asciiChars / CHARS_PER_TOKEN) + denseBytes;
5176
+ }
4804
5177
  var DISCOVERY_TTL_MS = 15 * 60 * 1e3;
4805
5178
  var DISCOVERY_TIMEOUT_MS = 4e3;
4806
5179
  var cloudDiscoveryCache = /* @__PURE__ */ new Map();
@@ -4853,6 +5226,33 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
4853
5226
  // start of every `cascade run`, independent of the session-wide budget.
4854
5227
  runTokens = 0;
4855
5228
  runCostUsd = 0;
5229
+ /**
5230
+ * Budget held for calls that are IN FLIGHT — admitted by the preflight check
5231
+ * but not yet counted by recordStats. Without these, concurrent calls all
5232
+ * measure themselves against the same unspent allowance and are all admitted.
5233
+ * Released when each call settles; reset by beginRun() alongside the totals.
5234
+ */
5235
+ reservedTokens = 0;
5236
+ reservedCostUsd = 0;
5237
+ /**
5238
+ * Fires when this run's budget is spent, cancelling work already in flight.
5239
+ *
5240
+ * The kill switch was previously only a flag, which stops calls that have
5241
+ * not yet submitted. A parallel wave's earlier members are already at the
5242
+ * provider by then, and they carried on generating for a run whose output
5243
+ * would be discarded. Every provider call chains to this, so tripping the
5244
+ * ceiling reaches the requests that are running, not just the ones queued.
5245
+ */
5246
+ runAbort = newRunAbort();
5247
+ /**
5248
+ * Bumped by every beginRun(). A release handle remembers the generation it
5249
+ * was issued in and does nothing if that has moved on: beginRun() zeroes the
5250
+ * counters, so a call still in flight from the previous run would otherwise
5251
+ * subtract its estimate from the NEW run's totals and drive them negative —
5252
+ * leaving later preflights believing there is more allowance than the cap
5253
+ * actually holds, which is worse than no gate at all.
5254
+ */
5255
+ runGeneration = 0;
4856
5256
  runBudgetExceeded = false;
4857
5257
  runBudgetExceededReason;
4858
5258
  /**
@@ -5022,7 +5422,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
5022
5422
  try {
5023
5423
  const cfg = this.config.providers.find((p) => p.type === model.provider) ?? { type: model.provider };
5024
5424
  const provider = this.createProvider(cfg, model);
5025
- const result = await withTimeout(provider.generate({
5425
+ const result = await withTimeoutAbort((signal) => provider.generate({
5026
5426
  messages: [{ role: "user", content: "Call the echo tool with text set to 'ping'. Use the tool; do not answer in prose." }],
5027
5427
  tools: [{
5028
5428
  name: "echo",
@@ -5030,7 +5430,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
5030
5430
  inputSchema: { type: "object", properties: { text: { type: "string", description: "Text to echo" } }, required: ["text"] }
5031
5431
  }],
5032
5432
  maxTokens: 80,
5033
- temperature: 0
5433
+ temperature: 0,
5434
+ signal
5034
5435
  }), 3e4, "tool-support probe timed out");
5035
5436
  return (result.toolCalls?.length ?? 0) > 0;
5036
5437
  } catch {
@@ -5151,9 +5552,12 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
5151
5552
  this.runBudgetExceededReason ?? "Per-task budget exceeded."
5152
5553
  );
5153
5554
  }
5555
+ const runGeneration = this.runGeneration;
5556
+ const runAbort = this.runAbort;
5557
+ const runSignal = this.runSignal;
5154
5558
  options = applyTierLimits(options, tier, this.config?.tierLimits);
5155
- if (this.runSignal && !options.signal) {
5156
- options = { ...options, signal: this.runSignal };
5559
+ if (runSignal && !options.signal) {
5560
+ options = { ...options, signal: runSignal };
5157
5561
  }
5158
5562
  if (options.model && !requireVision) {
5159
5563
  this.ensureProvider(options.model, this.config.providers);
@@ -5175,58 +5579,84 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
5175
5579
  if (!model) throw new Error(`No model available for tier ${tier}`);
5176
5580
  const provider = this.getProvider(model);
5177
5581
  if (!provider) throw new Error(`No provider for model ${model.id}`);
5582
+ let releaseReservation = this.enforcePreflightBudget(model, options);
5178
5583
  const requestedTokens = options.maxTokens ?? model.maxOutputTokens ?? 1024;
5179
5584
  const estimatedTokens = Math.min(requestedTokens, model.maxOutputTokens ?? requestedTokens) + 512;
5180
5585
  if (this.tpmLimiter) {
5181
- await this.tpmLimiter.acquire(model.provider, estimatedTokens);
5586
+ const wait = anySignal([options.signal, runAbort.signal]);
5587
+ try {
5588
+ await this.tpmLimiter.acquire(model.provider, estimatedTokens, wait.signal);
5589
+ } catch (err) {
5590
+ releaseReservation?.();
5591
+ throw this.asAbortFailure(err, options.signal, runSignal);
5592
+ } finally {
5593
+ wait.release();
5594
+ }
5182
5595
  }
5183
5596
  const useStream = Boolean(onChunk) && model.supportsStreaming && typeof provider.generateStream === "function";
5184
5597
  let releaseLocalSlot;
5185
5598
  if (model.isLocal) {
5186
5599
  const inferenceTimeoutMs = this.config.localInferenceTimeoutMs ?? 3e5;
5187
5600
  const queueWaitMs = Math.round(inferenceTimeoutMs / 2);
5188
- releaseLocalSlot = await this.localQueue.acquire(queueWaitMs);
5601
+ const wait = anySignal([options.signal, runAbort.signal]);
5602
+ try {
5603
+ releaseLocalSlot = await this.localQueue.acquire(queueWaitMs, wait.signal);
5604
+ } catch (err) {
5605
+ releaseReservation?.();
5606
+ throw this.asAbortFailure(err, options.signal, runSignal);
5607
+ } finally {
5608
+ wait.release();
5609
+ }
5189
5610
  }
5190
5611
  try {
5612
+ if (runGeneration === this.runGeneration && this.runBudgetExceeded) {
5613
+ this.tpmLimiter?.refund(model.provider, estimatedTokens);
5614
+ throw new _CascadeRouter.BudgetExceededError(
5615
+ this.runBudgetExceededReason ?? "Per-task budget exceeded."
5616
+ );
5617
+ }
5191
5618
  let result;
5192
5619
  if (model.isLocal) {
5193
5620
  const inferenceTimeoutMs = this.config.localInferenceTimeoutMs ?? 3e5;
5194
- const inferencePromise = useStream && onChunk ? provider.generateStream(options, (chunk) => {
5195
- const text = typeof chunk?.text === "string" ? chunk.text : "";
5196
- if (text) onChunk({ ...chunk, text });
5197
- }) : provider.generate(options);
5198
- result = await withTimeout(
5199
- inferencePromise,
5621
+ result = await withTimeoutAbort(
5622
+ (signal) => useStream && onChunk ? provider.generateStream({ ...options, signal }, (chunk) => {
5623
+ const text = typeof chunk?.text === "string" ? chunk.text : "";
5624
+ if (text) onChunk({ ...chunk, text });
5625
+ }) : provider.generate({ ...options, signal }),
5200
5626
  inferenceTimeoutMs,
5201
- `Local model ${model.id} inference timed out after ${inferenceTimeoutMs}ms`
5627
+ `Local model ${model.id} inference timed out after ${inferenceTimeoutMs}ms`,
5628
+ [options.signal, runAbort.signal]
5202
5629
  );
5203
5630
  } else if (useStream && onChunk) {
5204
5631
  const cloudTimeoutMs = this.config.cloudInferenceTimeoutMs ?? 12e4;
5205
5632
  try {
5206
- result = await withTimeout(
5207
- provider.generateStream(options, (chunk) => {
5633
+ result = await withTimeoutAbort(
5634
+ (signal) => provider.generateStream({ ...options, signal }, (chunk) => {
5208
5635
  const text = typeof chunk?.text === "string" ? chunk.text : "";
5209
5636
  if (text) onChunk({ ...chunk, text });
5210
5637
  }),
5211
5638
  cloudTimeoutMs,
5212
- `Model ${model.id} stream timed out after ${cloudTimeoutMs}ms`
5639
+ `Model ${model.id} stream timed out after ${cloudTimeoutMs}ms`,
5640
+ [options.signal, runAbort.signal]
5213
5641
  );
5214
5642
  } catch (streamErr) {
5215
- if (streamErr instanceof Error && streamErr.name === "AbortError" || this.runSignal?.aborted || options.signal?.aborted) {
5643
+ if (streamErr instanceof Error && streamErr.name === "AbortError" || runSignal?.aborted || options.signal?.aborted) {
5216
5644
  throw streamErr;
5217
5645
  }
5218
- result = await withTimeout(
5219
- provider.generate(options),
5646
+ result = await withTimeoutAbort(
5647
+ (signal) => provider.generate({ ...options, signal }),
5220
5648
  cloudTimeoutMs,
5221
- `Model ${model.id} inference timed out after ${cloudTimeoutMs}ms`
5649
+ `Model ${model.id} inference timed out after ${cloudTimeoutMs}ms`,
5650
+ [options.signal, runAbort.signal]
5222
5651
  );
5223
5652
  }
5224
5653
  } else {
5225
5654
  const cloudTimeoutMs = this.config.cloudInferenceTimeoutMs ?? 12e4;
5226
- result = await withTimeout(
5227
- provider.generate(options),
5655
+ result = await withTimeoutAbort(
5656
+ (signal) => provider.generate({ ...options, signal }),
5228
5657
  cloudTimeoutMs,
5229
- `Model ${model.id} inference timed out after ${cloudTimeoutMs}ms`
5658
+ `Model ${model.id} inference timed out after ${cloudTimeoutMs}ms`,
5659
+ [options.signal, runAbort.signal]
5230
5660
  );
5231
5661
  }
5232
5662
  const corrected = buildTokenUsage(
@@ -5245,11 +5675,12 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
5245
5675
  if (!result || typeof result.content !== "string" || !result.usage) {
5246
5676
  throw new Error(`Provider ${model.provider}:${model.id} returned an invalid generation result.`);
5247
5677
  }
5248
- this.recordStats(tier, model, result.usage, options.featureTag);
5678
+ this.recordStats(tier, model, result.usage, options.featureTag, runGeneration);
5249
5679
  this.failover.recordSuccess(model.provider);
5250
5680
  return result;
5251
5681
  } catch (err) {
5252
- if (err instanceof Error && err.name === "AbortError" || this.runSignal?.aborted || options.signal?.aborted) {
5682
+ if (err instanceof _CascadeRouter.BudgetExceededError) throw err;
5683
+ if (err instanceof Error && err.name === "AbortError" || runSignal?.aborted || options.signal?.aborted) {
5253
5684
  throw new CascadeCancelledError("Run cancelled");
5254
5685
  }
5255
5686
  const errMsg = err instanceof Error ? err.message : String(err);
@@ -5267,6 +5698,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
5267
5698
  });
5268
5699
  releaseLocalSlot?.();
5269
5700
  releaseLocalSlot = void 0;
5701
+ releaseReservation?.();
5702
+ releaseReservation = void 0;
5270
5703
  const retryOpts = options.model && options.model.id === model.id ? { ...options, model: void 0 } : options;
5271
5704
  return this.generate(tier, retryOpts, onChunk, requireVision);
5272
5705
  }
@@ -5288,6 +5721,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
5288
5721
  });
5289
5722
  releaseLocalSlot?.();
5290
5723
  releaseLocalSlot = void 0;
5724
+ releaseReservation?.();
5725
+ releaseReservation = void 0;
5291
5726
  const base = options.model && options.model.id === model.id ? { ...options, model: void 0 } : options;
5292
5727
  const retryOpts = { ...base, _notFoundDepth: depth };
5293
5728
  return this.generate(tier, retryOpts, onChunk, requireVision);
@@ -5296,6 +5731,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
5296
5731
  throw err;
5297
5732
  } finally {
5298
5733
  releaseLocalSlot?.();
5734
+ releaseReservation?.();
5735
+ releaseReservation = void 0;
5299
5736
  }
5300
5737
  }
5301
5738
  getModelForTier(tier) {
@@ -5666,7 +6103,14 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
5666
6103
  }
5667
6104
  return void 0;
5668
6105
  }
5669
- recordStats(tier, model, usage, featureTag) {
6106
+ /**
6107
+ * `runGeneration` is the generation the call was ADMITTED in — see
6108
+ * `generate()`. Session-wide accounting always applies (the spend is real
6109
+ * whenever it lands), but the per-RUN ceiling is only meaningful for the run
6110
+ * the call belongs to, so a straggler from a finished run is left out of it.
6111
+ * Omitted by the direct callers in tests, which have no run to straddle.
6112
+ */
6113
+ recordStats(tier, model, usage, featureTag, runGeneration) {
5670
6114
  this.stats.totalTokens += usage.totalTokens;
5671
6115
  this.stats.totalCostUsd += usage.estimatedCostUsd;
5672
6116
  this.sessionCostUsd += usage.estimatedCostUsd;
@@ -5691,10 +6135,13 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
5691
6135
  });
5692
6136
  }
5693
6137
  }
5694
- this.runTokens += usage.totalTokens;
5695
- this.runCostUsd += usage.estimatedCostUsd;
6138
+ const currentRun = runGeneration === void 0 || runGeneration === this.runGeneration;
6139
+ if (currentRun) {
6140
+ this.runTokens += usage.totalTokens;
6141
+ this.runCostUsd += usage.estimatedCostUsd;
6142
+ }
5696
6143
  this.updateBudgetState();
5697
- this.enforceRunBudget();
6144
+ if (currentRun) this.enforceRunBudget();
5698
6145
  }
5699
6146
  /**
5700
6147
  * Resets per-run accounting at the start of each `cascade run`. Session
@@ -5704,9 +6151,158 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
5704
6151
  beginRun() {
5705
6152
  this.runTokens = 0;
5706
6153
  this.runCostUsd = 0;
6154
+ this.reservedTokens = 0;
6155
+ this.reservedCostUsd = 0;
6156
+ this.runAbort = newRunAbort();
6157
+ this.runGeneration++;
5707
6158
  this.runBudgetExceeded = false;
5708
6159
  this.runBudgetExceededReason = void 0;
5709
6160
  }
6161
+ /**
6162
+ * Refuse a call whose input alone cannot fit what is left of the run budget.
6163
+ *
6164
+ * `enforceRunBudget()` below runs AFTER a call returns, which makes the caps
6165
+ * a stop rather than a pre-authorisation: the tokens are already bought by
6166
+ * the time it looks. That was tolerable while a prompt could not exceed
6167
+ * 20,000 characters, and stopped being so when that cap was removed in
6168
+ * 0.72.0 — a multi-megabyte prompt is billed in full on the very first call
6169
+ * (the complexity classifier sends the whole thing), and no ceiling
6170
+ * configured downstream can give that money back.
6171
+ *
6172
+ * Deliberately narrow, because a false refusal is worse than a late stop:
6173
+ *
6174
+ * • INPUT ONLY. What a call returns is not knowable in advance, and
6175
+ * charging a worst-case `maxTokens` of output against the budget would
6176
+ * decline runs that would have finished comfortably. Input is the part
6177
+ * that is already determined, and it is the part the removed cap used to
6178
+ * bound.
6179
+ * • Compared against what REMAINS (`cap - spent`), not the whole cap, so a
6180
+ * call is judged on the budget it can actually draw from.
6181
+ * • Skipped when the model has no usable price. An estimate cannot be made,
6182
+ * and refusing on ignorance would break every self-hosted and local model
6183
+ * the moment a cost cap is set. The post-hoc stop still applies there.
6184
+ * • Skipped entirely when no cap is configured — there is nothing to
6185
+ * pre-authorise against.
6186
+ *
6187
+ * The message names the cap AND the estimate, because "too expensive" with
6188
+ * no numbers gives the user nothing to change.
6189
+ */
6190
+ enforcePreflightBudget(model, options) {
6191
+ const budget = this.config?.budget;
6192
+ const maxCost = budget?.maxCostPerRunUsd;
6193
+ const maxTokens = budget?.maxTokensPerRun;
6194
+ if (maxCost == null && maxTokens == null) return void 0;
6195
+ const inputTokens = this.estimateInputTokens(options, model);
6196
+ if (maxTokens != null) {
6197
+ const remaining = maxTokens - this.runTokens - this.reservedTokens;
6198
+ if (inputTokens > remaining) {
6199
+ this.failPreflight(
6200
+ `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.`
6201
+ );
6202
+ }
6203
+ }
6204
+ let estimatedUsd = 0;
6205
+ if (maxCost != null) {
6206
+ const { input: inputPer1k, unknown } = resolveModelPricing(model, { inputTokens });
6207
+ if (!unknown && inputPer1k > 0) {
6208
+ estimatedUsd = inputTokens / 1e3 * inputPer1k;
6209
+ const remaining = maxCost - this.runCostUsd - this.reservedCostUsd;
6210
+ if (estimatedUsd > remaining) {
6211
+ this.failPreflight(
6212
+ `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.`
6213
+ );
6214
+ }
6215
+ }
6216
+ }
6217
+ this.reservedTokens += inputTokens;
6218
+ this.reservedCostUsd += estimatedUsd;
6219
+ const generation = this.runGeneration;
6220
+ let released = false;
6221
+ return () => {
6222
+ if (released || generation !== this.runGeneration) return;
6223
+ released = true;
6224
+ this.reservedTokens -= inputTokens;
6225
+ this.reservedCostUsd -= estimatedUsd;
6226
+ };
6227
+ }
6228
+ /**
6229
+ * What this call will send, in tokens.
6230
+ *
6231
+ * Text is the bulk of it, but not all of it: providers bill the serialized
6232
+ * TOOL DEFINITIONS on every native-tool call (Anthropic sends each name,
6233
+ * description and input schema), and an image is billed by its own size
6234
+ * rather than as the `[image]` placeholder `contentToText` reduces it to. A
6235
+ * tool-heavy or vision request could therefore pass either cap while its real
6236
+ * input already exceeded the allowance.
6237
+ */
6238
+ estimateInputTokens(options, model) {
6239
+ let tokens = guardTokens(options.systemPrompt ?? "");
6240
+ let images = 0;
6241
+ const wire = wireProfile(model.provider);
6242
+ const countsImage = (img) => wire.sendsUrlImages || img?.type === "base64";
6243
+ for (const message of options.messages) {
6244
+ if (wire.dropsMessage(message)) continue;
6245
+ tokens += TOKENS_PER_MESSAGE_FRAMING;
6246
+ if (typeof message.content === "string") {
6247
+ tokens += guardTokens(message.content);
6248
+ } else {
6249
+ switch (wire.blockHandling(message)) {
6250
+ case "blocks":
6251
+ for (const block of message.content) {
6252
+ if (!wire.sendsBlock(block)) continue;
6253
+ if (block.type === "image") {
6254
+ if (countsImage(block.image)) images++;
6255
+ continue;
6256
+ }
6257
+ tokens += guardTokens(contentToText([block]));
6258
+ }
6259
+ break;
6260
+ case "stringified":
6261
+ tokens += guardTokens(safeJson(message.content));
6262
+ break;
6263
+ }
6264
+ }
6265
+ if (message.toolCalls?.length && wire.sendsToolCalls(message)) {
6266
+ tokens += guardTokens(safeJson(wire.sizeToolCalls(message.toolCalls)));
6267
+ }
6268
+ if (message.toolCallId && message.role === "tool" && wire.sendsToolCallId) {
6269
+ tokens += guardTokens(message.toolCallId);
6270
+ }
6271
+ }
6272
+ if (options.tools?.length) {
6273
+ tokens += guardTokens(safeJson(wire.sizeTools(options.tools)));
6274
+ }
6275
+ if (wire.readsTopLevelImages) {
6276
+ const topLevel = (options.images ?? []).filter(countsImage).length;
6277
+ images += topLevel * geminiImageCopies(options.messages);
6278
+ }
6279
+ tokens += images * IMAGE_TOKENS_EACH;
6280
+ return tokens;
6281
+ }
6282
+ /**
6283
+ * Report a wait that ended because something asked it to stop, the same way
6284
+ * the provider call below reports one.
6285
+ *
6286
+ * The queue waits sit OUTSIDE the try/catch that maps aborts, so without
6287
+ * this a cancellation surfaced as whatever the queue happened to throw and
6288
+ * was retried or failed over like an ordinary error. A budget abort keeps
6289
+ * saying it ran out of budget — otherwise the reason the run stopped is lost
6290
+ * on the way up — and anything else that was aborted becomes a cancellation.
6291
+ */
6292
+ asAbortFailure(err, callSignal, runSignal) {
6293
+ if (err instanceof _CascadeRouter.BudgetExceededError) return err;
6294
+ if (err instanceof CascadeCancelledError) return err;
6295
+ if (callSignal?.aborted || runSignal?.aborted) return new CascadeCancelledError("Run cancelled");
6296
+ return err;
6297
+ }
6298
+ /** Trips the same run-level flag a post-hoc overrun does, and reports it the same way. */
6299
+ failPreflight(reason) {
6300
+ this.runBudgetExceeded = true;
6301
+ this.runBudgetExceededReason = reason;
6302
+ this.runAbort.abort(new _CascadeRouter.BudgetExceededError(reason));
6303
+ this.emit("budget:exceeded", { reason, spentUsd: this.sessionCostUsd });
6304
+ throw new _CascadeRouter.BudgetExceededError(reason);
6305
+ }
5710
6306
  /**
5711
6307
  * Enforce the hard per-task ceiling. Once tripped, the flag makes every
5712
6308
  * subsequent (and concurrent) generate() call in this run fail fast.
@@ -5722,6 +6318,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
5722
6318
  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.`;
5723
6319
  this.runBudgetExceeded = true;
5724
6320
  this.runBudgetExceededReason = reason;
6321
+ this.runAbort.abort(new _CascadeRouter.BudgetExceededError(reason));
5725
6322
  this.emit("budget:exceeded", { reason, spentUsd: this.sessionCostUsd });
5726
6323
  throw new _CascadeRouter.BudgetExceededError(reason);
5727
6324
  }
@@ -6958,6 +7555,12 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
6958
7555
  maxTokens: 4096,
6959
7556
  ...subtaskModel ? { model: subtaskModel } : {},
6960
7557
  featureTag: this.assignment?.sectionTitle,
7558
+ // The wave signal, so cancel-and-respawn reaches the request that is
7559
+ // RUNNING rather than only stopping the worker at its next checkpoint.
7560
+ // It is a superset of the run signal the router injects when this is
7561
+ // absent (T2 composes it from that signal plus its own per-wave one),
7562
+ // so wiring it here loses no cancellation and adds the wave's.
7563
+ ...this.signal ? { signal: this.signal } : {},
6961
7564
  ...this.localOnlyMatch ? { forceLocal: true } : {}
6962
7565
  };
6963
7566
  const result = await this.router.generate(
@@ -7298,6 +7901,22 @@ ${assignment.expectedOutput}`;
7298
7901
  requiresArtifact() {
7299
7902
  return shouldRequireArtifact(this.assignment, this.tools.map((t) => t.name));
7300
7903
  }
7904
+ /**
7905
+ * Where a file a tool wrote actually lives.
7906
+ *
7907
+ * This used to be `process.cwd()`, which is only the workspace in a plain
7908
+ * CLI run. The hosted server runs with cwd at the app directory while the
7909
+ * run's workspace is the tenant's scratch dir, and Electron's cwd is the app
7910
+ * bundle rather than the folder the user picked. So the tool wrote
7911
+ * `<workspace>/report.docx`, verification stat'd `<cwd>/report.docx`, found
7912
+ * nothing, and the worker looped and then threw WorkerStallError — surfacing
7913
+ * as "no file was created" on a file that had in fact been created, followed
7914
+ * by a failed node. Asking the registry is exact: it is the same root every
7915
+ * registered tool was configured with (registry.registerDefaults).
7916
+ */
7917
+ artifactRoot() {
7918
+ return this.toolRegistry.getWorkspaceRoot?.() ?? process.cwd();
7919
+ }
7301
7920
  extractArtifactPaths(assignment) {
7302
7921
  const declared = (assignment.files ?? []).map((f) => f.trim()).filter((f) => f.includes("."));
7303
7922
  const haystack = `${assignment.description}
@@ -7309,11 +7928,11 @@ ${assignment.expectedOutput}`;
7309
7928
  const artifactPaths = this.extractArtifactPaths(assignment);
7310
7929
  if (!artifactPaths.length) return { ok: true, issues: [] };
7311
7930
  const issues = [];
7312
- const { exec: exec2 } = await import('child_process');
7931
+ const { execFile: execFile4 } = await import('child_process');
7313
7932
  const { promisify: promisify4 } = await import('util');
7314
- const execAsync2 = promisify4(exec2);
7933
+ const execFileAsync3 = promisify4(execFile4);
7315
7934
  for (const artifactPath of artifactPaths) {
7316
- const absolutePath = path26__default.default.resolve(process.cwd(), artifactPath);
7935
+ const absolutePath = path26__default.default.resolve(this.artifactRoot(), artifactPath);
7317
7936
  try {
7318
7937
  const stat = await fs5__default.default.stat(absolutePath);
7319
7938
  if (!stat.isFile()) {
@@ -7349,20 +7968,18 @@ ${assignment.expectedOutput}`;
7349
7968
  continue;
7350
7969
  }
7351
7970
  const ext = path26__default.default.extname(absolutePath).toLowerCase();
7352
- try {
7353
- if (ext === ".ts" || ext === ".tsx") {
7354
- await execAsync2(`npx tsc --noEmit ${absolutePath}`, { timeout: 1e4 });
7355
- } else if (ext === ".js" || ext === ".jsx") {
7356
- await execAsync2(`node --check ${absolutePath}`, { timeout: 1e4 });
7357
- } else if (ext === ".py") {
7358
- await execAsync2(`python -m py_compile ${absolutePath}`, { timeout: 1e4 });
7971
+ const check = 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;
7972
+ if (check) {
7973
+ try {
7974
+ await execFileAsync3(check.cmd, check.args, { timeout: 1e4 });
7975
+ } catch (err) {
7976
+ const code = err.code;
7977
+ if (code === "ENOENT" || code === "EINVAL") continue;
7978
+ const e = err;
7979
+ issues.push(`Semantic error in ${artifactPath}:
7980
+ ${e.stderr || String(err)}
7981
+ ${e.stdout || ""}`);
7359
7982
  }
7360
- } catch (err) {
7361
- const stderr = err?.stderr || String(err);
7362
- const stdout = err?.stdout || "";
7363
- issues.push(`Semantic error in ${artifactPath}:
7364
- ${stderr}
7365
- ${stdout}`);
7366
7983
  }
7367
7984
  } catch {
7368
7985
  issues.push(`Required artifact was not created: ${artifactPath}`);
@@ -7422,6 +8039,7 @@ ${current}`
7422
8039
  HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
7423
8040
  maxTokens: 4096,
7424
8041
  featureTag: assignment.sectionTitle,
8042
+ ...this.signal ? { signal: this.signal } : {},
7425
8043
  ...this.localOnlyMatch ? { forceLocal: true } : {}
7426
8044
  });
7427
8045
  const next = (improved.content ?? "").trim();
@@ -7441,7 +8059,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
7441
8059
  async checkAcceptance(assignment) {
7442
8060
  const criteria = assignment.acceptance ?? [];
7443
8061
  if (!criteria.length) return [];
7444
- const resolve2 = (target) => path26__default.default.resolve(process.cwd(), target);
8062
+ const resolve2 = (target) => path26__default.default.resolve(this.artifactRoot(), target);
7445
8063
  return evaluateAcceptance(criteria, assignment.files ?? [], {
7446
8064
  stat: async (target) => {
7447
8065
  try {
@@ -7483,6 +8101,7 @@ Reply with JSON: { "completeness": "pass"|"fail", "correctness": "pass"|"fail",
7483
8101
 
7484
8102
  HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
7485
8103
  featureTag: assignment.sectionTitle,
8104
+ ...this.signal ? { signal: this.signal } : {},
7486
8105
  ...this.localOnlyMatch ? { forceLocal: true } : {}
7487
8106
  });
7488
8107
  try {
@@ -7522,6 +8141,7 @@ ${output.slice(0, 4e3)}`;
7522
8141
  messages: [{ role: "user", content: prompt }],
7523
8142
  maxTokens: 300,
7524
8143
  temperature: 0,
8144
+ ...this.signal ? { signal: this.signal } : {},
7525
8145
  ...this.localOnlyMatch ? { forceLocal: true } : {}
7526
8146
  });
7527
8147
  const match = /\[[\s\S]*\]/.exec(result.content);
@@ -7633,6 +8253,10 @@ Begin execution now.`;
7633
8253
  }
7634
8254
  };
7635
8255
  var PeerBus = class extends EventEmitter__default.default {
8256
+ constructor() {
8257
+ super();
8258
+ this.setMaxListeners(64);
8259
+ }
7636
8260
  outputs = /* @__PURE__ */ new Map();
7637
8261
  waiters = /* @__PURE__ */ new Map();
7638
8262
  members = /* @__PURE__ */ new Set();
@@ -8880,6 +9504,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
8880
9504
  const waveSignal = AbortSignal.any(
8881
9505
  [this.signal, this.waveAbortController.signal].filter(Boolean)
8882
9506
  );
9507
+ EventEmitter.setMaxListeners(64, waveSignal);
8883
9508
  const runOne = async (id) => {
8884
9509
  const assignment = sanitizedAssignments.find((a) => a.subtaskId === id);
8885
9510
  const worker = workerMap.get(id);
@@ -13029,6 +13654,18 @@ var ToolRegistry = class extends EventEmitter__default.default {
13029
13654
  this.tools.set(tool.name, tool);
13030
13655
  this.emit("tool:added", tool.name);
13031
13656
  }
13657
+ /**
13658
+ * The directory every registered tool resolves relative paths against.
13659
+ *
13660
+ * Exposed so a caller that has to reason about a file a tool WROTE can look
13661
+ * in the same place the tool wrote it. `process.cwd()` is not that place
13662
+ * outside a plain CLI run: the hosted server's cwd is the app directory
13663
+ * while the run's workspace is the tenant's scratch dir, and Electron's cwd
13664
+ * is the app bundle rather than the user's chosen folder.
13665
+ */
13666
+ getWorkspaceRoot() {
13667
+ return this.workspaceRoot;
13668
+ }
13032
13669
  /**
13033
13670
  * Wait until a named tool is registered, resolving immediately if it already exists.
13034
13671
  * T3 workers can call this after encountering a missing-tool error to resume
@@ -16511,115 +17148,6 @@ var GraphSearchTool = class extends BaseTool {
16511
17148
  }
16512
17149
  };
16513
17150
 
16514
- // src/core/context/compaction.ts
16515
- function estimateTokens(text) {
16516
- if (!text) return 0;
16517
- return Math.max(1, Math.ceil(text.length / 4));
16518
- }
16519
- function contentToText(content) {
16520
- if (typeof content === "string") return content;
16521
- return content.map((c) => c.type === "text" ? c.text : c.type === "tool_result" ? c.content : `[${c.type}]`).join(" ");
16522
- }
16523
- function messagesTokens(messages) {
16524
- return messages.reduce((n, m) => n + estimateTokens(contentToText(m.content)), 0);
16525
- }
16526
- function atomize(text, maxChars) {
16527
- const seps = ["\n\n", "\n", ". ", " "];
16528
- let units = [text];
16529
- for (const sep of seps) {
16530
- if (units.every((u) => u.length <= maxChars)) break;
16531
- const next = [];
16532
- for (const u of units) {
16533
- if (u.length <= maxChars) {
16534
- next.push(u);
16535
- continue;
16536
- }
16537
- const parts = u.split(sep);
16538
- parts.forEach((p, i) => next.push(i < parts.length - 1 ? p + sep : p));
16539
- }
16540
- units = next;
16541
- }
16542
- const out = [];
16543
- for (const u of units) {
16544
- if (u.length <= maxChars) out.push(u);
16545
- else for (let i = 0; i < u.length; i += maxChars) out.push(u.slice(i, i + maxChars));
16546
- }
16547
- return out;
16548
- }
16549
- function chunkText(text, opts) {
16550
- const maxChars = Math.max(1, Math.floor(opts.maxTokens * 4));
16551
- const overlap = Math.min(maxChars - 1, Math.max(0, Math.floor(maxChars * (opts.overlapRatio ?? 0.1))));
16552
- const trimmed = text.trim();
16553
- if (!trimmed) return [];
16554
- if (trimmed.length <= maxChars) return [trimmed];
16555
- const units = atomize(trimmed, maxChars);
16556
- const chunks = [];
16557
- let cur = "";
16558
- for (const u of units) {
16559
- if (cur && cur.length + u.length > maxChars) {
16560
- chunks.push(cur.trim());
16561
- cur = overlap > 0 ? cur.slice(-overlap) : "";
16562
- }
16563
- cur += u;
16564
- }
16565
- if (cur.trim()) chunks.push(cur.trim());
16566
- return chunks;
16567
- }
16568
- function needsCompaction(totalTokens, contextWindow, reserve = 0.2) {
16569
- if (!contextWindow || contextWindow <= 0) return false;
16570
- return totalTokens > contextWindow * (1 - reserve);
16571
- }
16572
- async function mapReduceCompact(text, opts) {
16573
- const capChars = opts.capTokens * 4;
16574
- let working = text;
16575
- let truncated = false;
16576
- if (working.length > capChars) {
16577
- working = working.slice(0, capChars);
16578
- truncated = true;
16579
- }
16580
- const instruction = opts.instruction ?? "Summarize this content, preserving key facts, names, numbers, and decisions.";
16581
- let chunks = chunkText(working, { maxTokens: opts.chunkTokens });
16582
- if (chunks.length <= 1) return { text: working, chunks: chunks.length, calls: 0, truncated };
16583
- let calls = 0;
16584
- let parts = await Promise.all(
16585
- chunks.map((c, i) => {
16586
- calls++;
16587
- return opts.summarize(c, `${instruction} (part ${i + 1} of ${chunks.length})`);
16588
- })
16589
- );
16590
- let combined = parts.join("\n\n");
16591
- const maxPasses = opts.maxReducePasses ?? 3;
16592
- for (let pass = 0; pass < maxPasses && estimateTokens(combined) > opts.targetTokens; pass++) {
16593
- const groups = chunkText(combined, { maxTokens: opts.chunkTokens });
16594
- if (groups.length <= 1) break;
16595
- parts = await Promise.all(
16596
- groups.map((g) => {
16597
- calls++;
16598
- return opts.summarize(g, "Merge these partial summaries into one cohesive summary; keep all distinct facts.");
16599
- })
16600
- );
16601
- combined = parts.join("\n\n");
16602
- }
16603
- const note = truncated ? "\n\n[Note: input exceeded the extended-context cap and was truncated.]" : "";
16604
- return { text: combined + note, chunks: chunks.length, calls, truncated };
16605
- }
16606
- async function rollingSummary(messages, opts) {
16607
- if (messages.length <= opts.keepRecent) return messages;
16608
- const older = messages.slice(0, messages.length - opts.keepRecent);
16609
- const recent = messages.slice(messages.length - opts.keepRecent);
16610
- const transcript = older.map((m) => `${m.role}: ${contentToText(m.content)}`).join("\n");
16611
- const summary = await opts.summarize(
16612
- transcript,
16613
- "Summarize this earlier conversation. Preserve facts, decisions, names, numbers, and any open threads or todos."
16614
- );
16615
- const summaryMessage = {
16616
- role: "system",
16617
- content: `Summary of earlier conversation (folded to fit the context window):
16618
- ${summary}`
16619
- };
16620
- return [summaryMessage, ...recent];
16621
- }
16622
-
16623
17151
  // src/core/steering/guidance.ts
16624
17152
  var GuidanceQueue = class {
16625
17153
  entries = [];
@@ -21762,10 +22290,10 @@ ${cur}`;
21762
22290
  }
21763
22291
 
21764
22292
  // src/retrieval/plan.ts
21765
- var CHARS_PER_TOKEN = 4;
22293
+ var CHARS_PER_TOKEN2 = 4;
21766
22294
  function cagCharBudget(contextWindowTokens, opts = {}) {
21767
22295
  const docFraction = opts.docFraction ?? 0.5;
21768
- const charsPerToken = opts.charsPerToken ?? CHARS_PER_TOKEN;
22296
+ const charsPerToken = opts.charsPerToken ?? CHARS_PER_TOKEN2;
21769
22297
  return Math.max(0, Math.floor(contextWindowTokens * docFraction * charsPerToken));
21770
22298
  }
21771
22299
  function planRetrieval(ctx) {
@@ -22344,7 +22872,7 @@ exports.CASCADE_IGNORE_FILE = CASCADE_IGNORE_FILE;
22344
22872
  exports.CASCADE_KEYSTORE_FILE = CASCADE_KEYSTORE_FILE;
22345
22873
  exports.CASCADE_MD_FILE = CASCADE_MD_FILE;
22346
22874
  exports.CASCADE_VERSION = CASCADE_VERSION;
22347
- exports.CHARS_PER_TOKEN = CHARS_PER_TOKEN;
22875
+ exports.CHARS_PER_TOKEN = CHARS_PER_TOKEN2;
22348
22876
  exports.COMPLEXITY_T2_COUNT = COMPLEXITY_T2_COUNT;
22349
22877
  exports.Cascade = Cascade;
22350
22878
  exports.CascadeCancelledError = CascadeCancelledError;