pi-background-tasks 0.7.6 → 0.9.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.
@@ -1,9 +1,19 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import {
3
- FUSION_CANDIDATE_SYSTEM_PROMPT,
4
- FUSION_EVALUATION_REPAIR_SYSTEM_PROMPT,
5
- FUSION_EVALUATOR_SYSTEM_PROMPT,
6
- FUSION_MERGER_SYSTEM_PROMPT,
3
+ TOKEN_BUDGET_AFFINE_F_TOKENS,
4
+ TOKEN_BUDGET_CALIBRATION_VERSION,
5
+ TOKEN_BUDGET_FAMILY_CALIBRATIONS,
6
+ TOKEN_BUDGET_LARGE_PROMPT_MIN_BYTES,
7
+ TOKEN_BUDGET_RATE_SCALE,
8
+ estimateInputTokens,
9
+ knownTextSegment,
10
+ maxKnownTextBytesForTokens,
11
+ resolveTokenBudgetFamily,
12
+ unknownOutputContractSegment,
13
+ allowedInputTokens,
14
+ isUsableContextWindow,
15
+ } from '../context/token-budget.js';
16
+ import {
7
17
  buildBlindEvaluationInput,
8
18
  buildCandidatePrompt,
9
19
  buildEvaluationPrompt,
@@ -12,38 +22,64 @@ import {
12
22
  buildMergePrompt,
13
23
  type AnonymousFusionCandidate,
14
24
  } from './prompts.js';
25
+ import { FUSION_BRAINSTORM_WORKFLOW, type FusionWorkflowProfile } from './workflows.js';
15
26
  import {
16
27
  FUSION_BUDGET_PLAN_SCHEMA_VERSION,
28
+ FUSION_CALIBRATION_VIOLATION_SCHEMA_VERSION,
17
29
  FUSION_EVALUATION_SCHEMA_VERSION,
30
+ FUSION_DEFAULT_CAPABILITY,
18
31
  FusionError,
19
32
  type FusionBudgetBlocker,
33
+ type FusionBudgetCheckKind,
34
+ type FusionBudgetComponentBreakdown,
35
+ type FusionBudgetCounterfactuals,
20
36
  type FusionBudgetEmptyRequestVerdict,
21
37
  type FusionBudgetErrorDetail,
22
38
  type FusionBudgetPlanV1,
23
39
  type FusionBudgetPolicyDescriptor,
40
+ type FusionBudgetRouteTableEntry,
24
41
  type FusionBudgetStage,
25
42
  type FusionBudgetStageComposition,
26
43
  type FusionBudgetWarning,
44
+ type FusionCalibrationViolation,
27
45
  type FusionCanonicalInputV3,
46
+ type FusionCapability,
28
47
  type FusionEvaluationV1,
29
48
  type FusionRouteCapacity,
30
49
  type FusionStage,
31
50
  type FusionStageBudgetPlanEntry,
51
+ type FusionChildRunResult,
32
52
  type ResolvedFusionModel,
33
53
  type ResolvedFusionModels,
34
54
  } from './types.js';
35
55
 
36
- export const FUSION_BYTES_PER_TOKEN_DIVISOR = 2;
56
+ export const FUSION_CALIBRATED_BYTES_PER_TOKEN = TOKEN_BUDGET_FAMILY_CALIBRATIONS;
37
57
 
38
58
  export const FUSION_CANDIDATE_MAX_OUTPUT_BYTES = 48 * 1024;
39
59
  export const FUSION_EVALUATION_MAX_OUTPUT_BYTES = 64 * 1024;
40
60
  export const FUSION_MERGE_MAX_OUTPUT_BYTES = 64 * 1024;
41
61
  export const FUSION_DIAGNOSTICS_MAX_BYTES = 8 * 1024;
42
62
 
43
- export const FUSION_RESERVED_OUTPUT_TOKENS = Math.ceil(
44
- FUSION_MERGE_MAX_OUTPUT_BYTES / FUSION_BYTES_PER_TOKEN_DIVISOR,
63
+ const FUSION_OUTPUT_RESERVE_RATE_X100 = 200;
64
+ export const FUSION_UTILIZATION_WARNING_THRESHOLD_BASIS_POINTS = 8000;
65
+ const BASIS_POINTS_DENOMINATOR = 10_000;
66
+
67
+ function ceilDiv(numerator: number, denominator: number): number {
68
+ if (!Number.isSafeInteger(numerator) || numerator < 0) {
69
+ throw new TypeError('ceilDiv numerator must be a non-negative safe integer');
70
+ }
71
+ if (!Number.isSafeInteger(denominator) || denominator <= 0) {
72
+ throw new TypeError('ceilDiv denominator must be a positive safe integer');
73
+ }
74
+ if (numerator === 0) return 0;
75
+ return Math.floor((numerator - 1) / denominator) + 1;
76
+ }
77
+
78
+ export const FUSION_RESERVED_OUTPUT_TOKENS = ceilDiv(
79
+ FUSION_MERGE_MAX_OUTPUT_BYTES * TOKEN_BUDGET_RATE_SCALE,
80
+ FUSION_OUTPUT_RESERVE_RATE_X100,
45
81
  );
46
- export const FUSION_FRAMING_RESERVE_TOKENS = 4_096;
82
+ export const FUSION_FRAMING_RESERVE_TOKENS = 0;
47
83
  export const FUSION_SAFETY_RESERVE_TOKENS = 4_096;
48
84
  export const FUSION_MIN_CANONICAL_INPUT_TOKENS = 8_192;
49
85
  export const FUSION_MIN_CONTEXT_WINDOW_TOKENS =
@@ -51,11 +87,11 @@ export const FUSION_MIN_CONTEXT_WINDOW_TOKENS =
51
87
  FUSION_RESERVED_OUTPUT_TOKENS +
52
88
  FUSION_FRAMING_RESERVE_TOKENS +
53
89
  FUSION_SAFETY_RESERVE_TOKENS;
54
- export const FUSION_UTILIZATION_WARNING_THRESHOLD = 0.8;
55
90
 
56
91
  export const FUSION_BUDGET_POLICY: FusionBudgetPolicyDescriptor = {
57
- id: 'fusion-budget-policy-v2',
58
- bytes_per_token_divisor: FUSION_BYTES_PER_TOKEN_DIVISOR,
92
+ id: 'fusion-budget-policy-v3',
93
+ calibration_version: TOKEN_BUDGET_CALIBRATION_VERSION,
94
+ calibration_table: FUSION_CALIBRATED_BYTES_PER_TOKEN,
59
95
  reserved_output_tokens: FUSION_RESERVED_OUTPUT_TOKENS,
60
96
  framing_reserve_tokens: FUSION_FRAMING_RESERVE_TOKENS,
61
97
  safety_reserve_tokens: FUSION_SAFETY_RESERVE_TOKENS,
@@ -63,7 +99,7 @@ export const FUSION_BUDGET_POLICY: FusionBudgetPolicyDescriptor = {
63
99
  evaluation_output_contract_bytes: FUSION_EVALUATION_MAX_OUTPUT_BYTES,
64
100
  merge_output_contract_bytes: FUSION_MERGE_MAX_OUTPUT_BYTES,
65
101
  diagnostics_contract_bytes: FUSION_DIAGNOSTICS_MAX_BYTES,
66
- utilization_warning_threshold: FUSION_UTILIZATION_WARNING_THRESHOLD,
102
+ utilization_warning_threshold_basis_points: FUSION_UTILIZATION_WARNING_THRESHOLD_BASIS_POINTS,
67
103
  };
68
104
 
69
105
  const EMPTY_REMEDIATION: readonly string[] = Object.freeze([
@@ -78,6 +114,23 @@ const REQUEST_REMEDIATION: readonly string[] = Object.freeze([
78
114
  "Raise the route's context window with a larger-context model via /fusion-models.",
79
115
  ]);
80
116
 
117
+ const RESERVATION_REMEDIATION: readonly string[] = Object.freeze([
118
+ 'Route the blocking stage to a model with larger byte capacity.',
119
+ 'Keep producer output contracts intact; do not shrink or truncate child answers.',
120
+ 'Inspect budget-plan.json for the advisory reservation component before retrying.',
121
+ ]);
122
+
123
+ const DENSE_REMEDIATION: readonly string[] = Object.freeze([
124
+ 'Remove or externalize low-whitespace dense ASCII payloads (base64, minified code, PEM/hex blocks), then retry.',
125
+ 'The prompt is preserved; no content was clipped to fit.',
126
+ ]);
127
+
128
+ const MULTIBYTE_REMEDIATION: readonly string[] = Object.freeze([
129
+ 'This rejection is dominated by multibyte UTF-8 content; use a larger-context route or split the non-Latin/CJK-heavy task.',
130
+ 'The budget plan includes the stricter multibyte advisory ceiling separately from the fatal estimate.',
131
+ 'The prompt is preserved; no content was clipped to fit.',
132
+ ]);
133
+
81
134
  const EMPTY_CANDIDATES: readonly [
82
135
  AnonymousFusionCandidate,
83
136
  AnonymousFusionCandidate,
@@ -140,7 +193,11 @@ interface StageForecastDraft {
140
193
  }
141
194
 
142
195
  export function fusionTokenUpperBound(utf8Bytes: number): number {
143
- return Math.ceil(utf8Bytes / FUSION_BYTES_PER_TOKEN_DIVISOR);
196
+ return estimateInputTokens({
197
+ family: 'unknown',
198
+ scope: 'conservative',
199
+ segments: [{ kind: 'known_text', bytes: utf8Bytes, multibyteBytes: 0, denseBytes: 0 }],
200
+ }).tokens;
144
201
  }
145
202
 
146
203
  export function fusionOutputContractBytes(stage: FusionStage): number {
@@ -169,7 +226,7 @@ function sha256Hex(value: string): string {
169
226
 
170
227
  function requirePositiveContextWindow(model: ResolvedFusionModel, role: string): number {
171
228
  const value = model.contextWindow;
172
- if (!Number.isFinite(value) || !Number.isInteger(value) || value <= 0) {
229
+ if (!isUsableContextWindow(value)) {
173
230
  throw new FusionError(
174
231
  `fusion ${role} route ${model.qualifiedId} has no usable context window capacity`,
175
232
  { code: 'model_capacity_unknown', childCreated: false },
@@ -178,22 +235,45 @@ function requirePositiveContextWindow(model: ResolvedFusionModel, role: string):
178
235
  return value;
179
236
  }
180
237
 
238
+ function formatRateX100(value: number): string {
239
+ const whole = Math.floor(value / TOKEN_BUDGET_RATE_SCALE);
240
+ const frac = String(value % TOKEN_BUDGET_RATE_SCALE).padStart(2, '0');
241
+ return `${String(whole)}.${frac}`;
242
+ }
243
+
181
244
  function routeCapacity(
182
245
  model: ResolvedFusionModel,
183
246
  role: FusionRouteCapacity['role'],
184
247
  ): FusionRouteCapacity {
185
248
  const contextWindow = requirePositiveContextWindow(model, role);
186
- const allowed =
187
- contextWindow -
188
- FUSION_RESERVED_OUTPUT_TOKENS -
189
- FUSION_FRAMING_RESERVE_TOKENS -
190
- FUSION_SAFETY_RESERVE_TOKENS;
249
+ const allowed = allowedInputTokens(contextWindow, {
250
+ reservedOutputTokens: FUSION_RESERVED_OUTPUT_TOKENS,
251
+ framingReserveTokens: FUSION_FRAMING_RESERVE_TOKENS,
252
+ safetyReserveTokens: FUSION_SAFETY_RESERVE_TOKENS,
253
+ });
191
254
  if (allowed < FUSION_MIN_CANONICAL_INPUT_TOKENS) {
192
255
  throw new FusionError(
193
256
  `fusion ${role} route ${model.qualifiedId} has a ${String(contextWindow)}-token context window, but Fusion requires at least ${String(FUSION_MIN_CONTEXT_WINDOW_TOKENS)} tokens per configured route: ${String(FUSION_RESERVED_OUTPUT_TOKENS)} output + ${String(FUSION_FRAMING_RESERVE_TOKENS)} framing + ${String(FUSION_SAFETY_RESERVE_TOKENS)} safety + ${String(FUSION_MIN_CANONICAL_INPUT_TOKENS)} usable input. Choose a larger-context model for this slot with /fusion-models.`,
194
257
  { code: 'model_capacity_unknown', childCreated: false },
195
258
  );
196
259
  }
260
+ const family = resolveTokenBudgetFamily({ provider: model.provider, model: model.model });
261
+ const rateSource = estimateInputTokens({
262
+ family: family.family,
263
+ calibrationBacked: family.backed,
264
+ familyResolution: family.resolution,
265
+ allowedInputTokens: allowed,
266
+ scope: 'fusion',
267
+ segments: [
268
+ {
269
+ kind: 'known_text',
270
+ bytes: TOKEN_BUDGET_LARGE_PROMPT_MIN_BYTES,
271
+ multibyteBytes: 0,
272
+ denseBytes: 0,
273
+ asciiWhitespaceBytes: TOKEN_BUDGET_LARGE_PROMPT_MIN_BYTES,
274
+ },
275
+ ],
276
+ }).rateSource;
197
277
  return {
198
278
  role,
199
279
  provider: model.provider,
@@ -204,6 +284,11 @@ function routeCapacity(
204
284
  framing_reserve_tokens: FUSION_FRAMING_RESERVE_TOKENS,
205
285
  safety_reserve_tokens: FUSION_SAFETY_RESERVE_TOKENS,
206
286
  allowed_input_tokens: allowed,
287
+ family: family.family,
288
+ rate_source: rateSource,
289
+ byte_capacity_utf8_bytes: Math.floor(
290
+ (allowed * rateSource.effective_rate_bytes_per_token_x100) / TOKEN_BUDGET_RATE_SCALE,
291
+ ),
207
292
  };
208
293
  }
209
294
 
@@ -222,7 +307,12 @@ export function fusionLimitingRoute(
222
307
  ): FusionRouteCapacity {
223
308
  let limiting: FusionRouteCapacity | undefined;
224
309
  for (const route of routes) {
225
- if (limiting === undefined || route.allowed_input_tokens < limiting.allowed_input_tokens) {
310
+ if (
311
+ limiting === undefined ||
312
+ route.byte_capacity_utf8_bytes < limiting.byte_capacity_utf8_bytes ||
313
+ (route.byte_capacity_utf8_bytes === limiting.byte_capacity_utf8_bytes &&
314
+ route.role.localeCompare(limiting.role) < 0)
315
+ ) {
226
316
  limiting = route;
227
317
  }
228
318
  }
@@ -255,29 +345,80 @@ function candidateRole(slot: 1 | 2 | 3): FusionRouteCapacity['role'] {
255
345
  return 'candidate-3';
256
346
  }
257
347
 
348
+ function estimateRouteInput(
349
+ route: FusionRouteCapacity,
350
+ segments: Parameters<typeof estimateInputTokens>[0]['segments'],
351
+ ) {
352
+ return estimateInputTokens({
353
+ family: route.family,
354
+ calibrationBacked: route.rate_source.backed,
355
+ familyResolution: route.rate_source.model_resolution,
356
+ allowedInputTokens: route.allowed_input_tokens,
357
+ scope: 'fusion',
358
+ segments,
359
+ });
360
+ }
361
+
362
+ function utilizationBasisPoints(tokens: number, allowed: number): number {
363
+ return ceilDiv(tokens * BASIS_POINTS_DENOMINATOR, allowed);
364
+ }
365
+
258
366
  function forecastEntry(draft: StageForecastDraft): FusionStageBudgetPlanEntry {
259
- const forecastUtf8Bytes =
260
- utf8Bytes(draft.system_prompt) +
261
- utf8Bytes(draft.empty_user_prompt) +
262
- draft.upstream_output_contract_bytes;
263
- const tokens = fusionTokenUpperBound(forecastUtf8Bytes);
367
+ const inputSegments = [knownTextSegment(draft.system_prompt), knownTextSegment(draft.empty_user_prompt)];
368
+ const inputBytes = inputSegments.reduce((sum, segment) => sum + segment.bytes, 0);
369
+ const inputOnly = estimateRouteInput(draft.route, inputSegments);
370
+ const reservationSegments =
371
+ draft.upstream_output_contract_bytes === 0
372
+ ? inputSegments
373
+ : [...inputSegments, unknownOutputContractSegment(draft.upstream_output_contract_bytes)];
374
+ const reservation = estimateRouteInput(draft.route, reservationSegments);
375
+ const forecastUtf8Bytes = inputBytes + draft.upstream_output_contract_bytes;
264
376
  const entry: FusionStageBudgetPlanEntry = {
265
377
  budget_stage: draft.budget_stage,
266
378
  route: draft.route,
267
379
  conditional: draft.conditional,
380
+ check_kind: 'input_only_preflight',
381
+ input_utf8_bytes: inputBytes,
382
+ upstream_output_contract_bytes: draft.upstream_output_contract_bytes,
268
383
  forecast_utf8_bytes: forecastUtf8Bytes,
269
- forecast_input_tokens_upper_bound: tokens,
384
+ input_only_input_tokens_upper_bound: inputOnly.tokens,
385
+ forecast_input_tokens_upper_bound: reservation.tokens,
270
386
  allowed_input_tokens: draft.route.allowed_input_tokens,
271
- signed_headroom_tokens: draft.route.allowed_input_tokens - tokens,
272
- utilization: tokens / draft.route.allowed_input_tokens,
273
- fits: tokens <= draft.route.allowed_input_tokens,
387
+ input_only_signed_headroom_tokens: draft.route.allowed_input_tokens - inputOnly.tokens,
388
+ signed_headroom_tokens: draft.route.allowed_input_tokens - reservation.tokens,
389
+ input_only_utilization_basis_points: utilizationBasisPoints(
390
+ inputOnly.tokens,
391
+ draft.route.allowed_input_tokens,
392
+ ),
393
+ utilization_basis_points: utilizationBasisPoints(
394
+ reservation.tokens,
395
+ draft.route.allowed_input_tokens,
396
+ ),
397
+ input_only_estimate: inputOnly,
398
+ reservation_estimate: reservation,
399
+ fits: inputOnly.tokens <= draft.route.allowed_input_tokens,
400
+ reservation_fits: reservation.tokens <= draft.route.allowed_input_tokens,
274
401
  };
275
402
  if (draft.slot !== undefined) entry.slot = draft.slot;
276
403
  return entry;
277
404
  }
278
405
 
406
+ function maxKnownTextBytes(route: FusionRouteCapacity): number {
407
+ return maxKnownTextBytesForTokens({
408
+ family: route.family,
409
+ calibrationBacked: route.rate_source.backed,
410
+ familyResolution: route.rate_source.model_resolution,
411
+ allowedInputTokens: route.allowed_input_tokens,
412
+ scope: 'fusion',
413
+ });
414
+ }
415
+
279
416
  function blockerFromEntry(entry: FusionStageBudgetPlanEntry): FusionBudgetBlocker {
280
- return { ...entry, overage_tokens: Math.max(0, -entry.signed_headroom_tokens) };
417
+ return {
418
+ ...entry,
419
+ overage_tokens: Math.max(0, entry.input_only_input_tokens_upper_bound - entry.allowed_input_tokens),
420
+ bytes_over: Math.max(0, entry.input_utf8_bytes - maxKnownTextBytes(entry.route)),
421
+ };
281
422
  }
282
423
 
283
424
  function blockerOrder(blocker: FusionBudgetBlocker): number {
@@ -316,7 +457,7 @@ function replaceRequestText(input: FusionCanonicalInputV3, text: string): Fusion
316
457
  function visibleTextBytes(input: FusionCanonicalInputV3): number {
317
458
  let total = 0;
318
459
  for (const entry of input.conversation_projection.entries) {
319
- if (entry.kind === 'text') total += utf8Bytes(JSON.stringify(entry.text));
460
+ if (entry[0] === 't') total += utf8Bytes(JSON.stringify(entry[4]));
320
461
  }
321
462
  return total;
322
463
  }
@@ -324,19 +465,35 @@ function visibleTextBytes(input: FusionCanonicalInputV3): number {
324
465
  function omissionReceiptBytes(input: FusionCanonicalInputV3): number {
325
466
  let total = 0;
326
467
  for (const entry of input.conversation_projection.entries) {
327
- if (entry.kind === 'omitted_activity') total += utf8Bytes(JSON.stringify(entry));
468
+ if (entry[0] === 'o') total += utf8Bytes(JSON.stringify(entry));
328
469
  }
329
470
  return total;
330
471
  }
331
472
 
332
- function warningFromEntry(entry: FusionStageBudgetPlanEntry): FusionBudgetWarning {
333
- return { ...entry, threshold: FUSION_UTILIZATION_WARNING_THRESHOLD };
473
+ function warningFromEntry(entry: FusionStageBudgetPlanEntry): FusionBudgetWarning | undefined {
474
+ if (!entry.fits) return undefined;
475
+ if (!entry.reservation_fits) {
476
+ return {
477
+ ...entry,
478
+ warning_kind: 'worst_case_reservation',
479
+ threshold_basis_points: FUSION_UTILIZATION_WARNING_THRESHOLD_BASIS_POINTS,
480
+ };
481
+ }
482
+ if (entry.utilization_basis_points >= FUSION_UTILIZATION_WARNING_THRESHOLD_BASIS_POINTS) {
483
+ return {
484
+ ...entry,
485
+ warning_kind: 'input_utilization',
486
+ threshold_basis_points: FUSION_UTILIZATION_WARNING_THRESHOLD_BASIS_POINTS,
487
+ };
488
+ }
489
+ return undefined;
334
490
  }
335
491
 
336
492
  function warningsFor(entries: readonly FusionStageBudgetPlanEntry[]): readonly FusionBudgetWarning[] {
337
- return entries
338
- .filter((entry) => entry.fits && entry.utilization >= FUSION_UTILIZATION_WARNING_THRESHOLD)
339
- .map(warningFromEntry);
493
+ return entries.flatMap((entry) => {
494
+ const warning = warningFromEntry(entry);
495
+ return warning === undefined ? [] : [warning];
496
+ });
340
497
  }
341
498
 
342
499
  function entryLabel(entry: FusionStageBudgetPlanEntry): string {
@@ -348,7 +505,7 @@ function entryLabel(entry: FusionStageBudgetPlanEntry): string {
348
505
  function formatTable(entries: readonly FusionStageBudgetPlanEntry[]): string {
349
506
  const lines = entries.map(
350
507
  (entry) =>
351
- `${entryLabel(entry)} | route=${entry.route.qualified_id} | forecast=${String(entry.forecast_input_tokens_upper_bound)} | allowed=${String(entry.allowed_input_tokens)} | headroom=${String(entry.signed_headroom_tokens)} | ${entry.fits ? 'fits' : 'over'}`,
508
+ `${entryLabel(entry)} | route=${entry.route.qualified_id} | input=${String(entry.input_only_input_tokens_upper_bound)} | reserved=${String(entry.forecast_input_tokens_upper_bound)} | allowed=${String(entry.allowed_input_tokens)} | input_headroom=${String(entry.input_only_signed_headroom_tokens)} | reservation_headroom=${String(entry.signed_headroom_tokens)} | ${entry.fits ? 'input-fits' : 'input-over'} | ${entry.reservation_fits ? 'reservation-fits' : 'reservation-warn'}`,
352
509
  );
353
510
  return lines.join('\n');
354
511
  }
@@ -364,6 +521,27 @@ function formatComposition(composition: FusionBudgetStageComposition): string {
364
521
  ].join('; ');
365
522
  }
366
523
 
524
+ function dominantRemediation(
525
+ verdict: FusionBudgetEmptyRequestVerdict,
526
+ composition: FusionBudgetStageComposition | undefined,
527
+ dominantByteClass: string,
528
+ ): readonly string[] {
529
+ if (dominantByteClass === 'dense_ascii') return DENSE_REMEDIATION;
530
+ if (dominantByteClass === 'multibyte') return MULTIBYTE_REMEDIATION;
531
+ if (composition === undefined) return remediationFor(verdict);
532
+ const entries = [
533
+ { name: 'visible', bytes: composition.visible_text_bytes },
534
+ { name: 'request', bytes: composition.request_bytes },
535
+ { name: 'reservation', bytes: composition.upstream_output_contract_bytes },
536
+ ].sort((left, right) => right.bytes - left.bytes);
537
+ const dominant = entries[0];
538
+ if (dominant?.name === 'reservation') return RESERVATION_REMEDIATION;
539
+ if (dominant?.name === 'request' && !verdict.still_fails_with_empty_request) {
540
+ return REQUEST_REMEDIATION;
541
+ }
542
+ return EMPTY_REMEDIATION;
543
+ }
544
+
367
545
  function remediationFor(verdict: FusionBudgetEmptyRequestVerdict): readonly string[] {
368
546
  return verdict.still_fails_with_empty_request ? EMPTY_REMEDIATION : REQUEST_REMEDIATION;
369
547
  }
@@ -384,21 +562,153 @@ function stageFromBudgetStage(stage: FusionBudgetStage): FusionStage {
384
562
  return 'evaluation';
385
563
  }
386
564
 
565
+ function routeTable(routes: readonly FusionRouteCapacity[]): readonly FusionBudgetRouteTableEntry[] {
566
+ return [...routes]
567
+ .sort((left, right) => {
568
+ const byCapacity = left.byte_capacity_utf8_bytes - right.byte_capacity_utf8_bytes;
569
+ if (byCapacity !== 0) return byCapacity;
570
+ return left.role.localeCompare(right.role);
571
+ })
572
+ .map((route) => ({
573
+ role: route.role,
574
+ qualified_id: route.qualified_id,
575
+ allowed_input_tokens: route.allowed_input_tokens,
576
+ family: route.family,
577
+ effective_rate_bytes_per_token_x100: route.rate_source.effective_rate_bytes_per_token_x100,
578
+ byte_capacity_utf8_bytes: route.byte_capacity_utf8_bytes,
579
+ backed: route.rate_source.backed,
580
+ }));
581
+ }
582
+
583
+ function segmentTokensForBytes(route: FusionRouteCapacity, bytes: number): number {
584
+ if (bytes <= 0) return 0;
585
+ const estimate = estimateRouteInput(route, [
586
+ { kind: 'known_text', bytes, multibyteBytes: 0, denseBytes: 0 },
587
+ ]);
588
+ const segment = estimate.perSegment[0];
589
+ return segment === undefined ? 0 : segment.tokens;
590
+ }
591
+
592
+ function contractTokens(bytes: number): number {
593
+ return bytes;
594
+ }
595
+
596
+ function componentBreakdown(
597
+ composition: FusionBudgetStageComposition | undefined,
598
+ route: FusionRouteCapacity,
599
+ ): FusionBudgetComponentBreakdown {
600
+ const empty = {
601
+ visible_text_bytes: 0,
602
+ omission_receipt_bytes: 0,
603
+ projection_metadata_bytes: 0,
604
+ request_bytes: 0,
605
+ static_stage_framing_bytes: 0,
606
+ upstream_output_contract_bytes: 0,
607
+ } satisfies FusionBudgetStageComposition;
608
+ const item = composition ?? empty;
609
+ return {
610
+ visible_text: {
611
+ bytes: item.visible_text_bytes,
612
+ tokens: segmentTokensForBytes(route, item.visible_text_bytes),
613
+ },
614
+ omission_receipts: {
615
+ bytes: item.omission_receipt_bytes,
616
+ tokens: segmentTokensForBytes(route, item.omission_receipt_bytes),
617
+ },
618
+ projection_metadata: {
619
+ bytes: item.projection_metadata_bytes,
620
+ tokens: segmentTokensForBytes(route, item.projection_metadata_bytes),
621
+ },
622
+ request: {
623
+ bytes: item.request_bytes,
624
+ tokens: segmentTokensForBytes(route, item.request_bytes),
625
+ },
626
+ static_stage_framing: {
627
+ bytes: item.static_stage_framing_bytes,
628
+ tokens: segmentTokensForBytes(route, item.static_stage_framing_bytes),
629
+ },
630
+ upstream_output_contracts: {
631
+ bytes: item.upstream_output_contract_bytes,
632
+ tokens: contractTokens(item.upstream_output_contract_bytes),
633
+ },
634
+ };
635
+ }
636
+
637
+ function medianCounterfactual(primary: FusionBudgetBlocker): FusionBudgetCounterfactuals['at_median_rate'] {
638
+ if (!primary.route.rate_source.backed) {
639
+ return { forecast_input_tokens_upper_bound: null, signed_headroom_tokens: null, fits: null };
640
+ }
641
+ const median = primary.route.rate_source.provenance.median_bpt_x1000;
642
+ if (median === null) return { forecast_input_tokens_upper_bound: null, signed_headroom_tokens: null, fits: null };
643
+ const variableTokens = ceilDiv(primary.input_utf8_bytes * 1000, median);
644
+ const tokens = variableTokens + TOKEN_BUDGET_AFFINE_F_TOKENS;
645
+ return {
646
+ forecast_input_tokens_upper_bound: tokens,
647
+ signed_headroom_tokens: primary.allowed_input_tokens - tokens,
648
+ fits: tokens <= primary.allowed_input_tokens,
649
+ };
650
+ }
651
+
652
+ function inputCounterfactuals(
653
+ primary: FusionBudgetBlocker,
654
+ plan: FusionBudgetPlanV1,
655
+ ): FusionBudgetCounterfactuals {
656
+ return {
657
+ empty_request: plan.empty_request,
658
+ without_reservation: {
659
+ forecast_input_tokens_upper_bound: primary.input_only_input_tokens_upper_bound,
660
+ signed_headroom_tokens: primary.input_only_signed_headroom_tokens,
661
+ fits: primary.fits,
662
+ },
663
+ at_median_rate: medianCounterfactual(primary),
664
+ };
665
+ }
666
+
667
+ function budgetErrorCode(
668
+ checkKind: FusionBudgetCheckKind,
669
+ ): 'prompt_budget_exceeded_forecast' | 'prompt_budget_exceeded_measured' {
670
+ return checkKind === 'rendered_prompt'
671
+ ? 'prompt_budget_exceeded_measured'
672
+ : 'prompt_budget_exceeded_forecast';
673
+ }
674
+
387
675
  export class FusionBudget {
388
676
  readonly routes: readonly FusionRouteCapacity[];
389
677
  readonly limiting: FusionRouteCapacity;
390
678
  private readonly contextPolicyId: string;
679
+ private readonly candidateCapability: FusionCapability;
680
+ private readonly profile: FusionWorkflowProfile;
391
681
 
392
- constructor(models: ResolvedFusionModels, contextPolicyId: string) {
682
+ constructor(
683
+ models: ResolvedFusionModels,
684
+ contextPolicyId: string,
685
+ candidateCapability: FusionCapability = FUSION_DEFAULT_CAPABILITY,
686
+ profile: FusionWorkflowProfile = FUSION_BRAINSTORM_WORKFLOW,
687
+ ) {
393
688
  this.routes = fusionRouteCapacities(models);
394
689
  this.limiting = fusionLimitingRoute(this.routes);
395
690
  this.contextPolicyId = contextPolicyId;
691
+ this.candidateCapability = candidateCapability;
692
+ this.profile = profile;
396
693
  }
397
694
 
398
695
  get allowedInputTokens(): number {
399
696
  return this.limiting.allowed_input_tokens;
400
697
  }
401
698
 
699
+ get resultRateSources() {
700
+ return this.routes.map((route) => route.rate_source);
701
+ }
702
+
703
+ get unknownProviderWarnings(): readonly string[] {
704
+ return this.routes.flatMap((route) => {
705
+ const warning = route.rate_source.warning;
706
+ return route.rate_source.backed || warning === null
707
+ ? []
708
+ : [`${route.qualified_id}: ${warning}`];
709
+ });
710
+ }
711
+
402
712
  private routeForStage(stage: FusionBudgetStage, slot?: 1 | 2 | 3): FusionRouteCapacity {
403
713
  if (stage === 'candidate') return routeByRole(this.routes, candidateRole(slot ?? 1));
404
714
  if (stage === 'merge') return routeByRole(this.routes, 'merger');
@@ -406,6 +716,7 @@ export class FusionBudget {
406
716
  }
407
717
 
408
718
  private drafts(input: FusionCanonicalInputV3): readonly StageForecastDraft[] {
719
+ const candidateSystemPrompt = this.profile.candidateSystemPrompt(this.candidateCapability);
409
720
  const candidatePrompt = buildCandidatePrompt(input);
410
721
  const blindInput = buildBlindEvaluationInput(input, EMPTY_CANDIDATES);
411
722
  const evaluationPrompt = buildEvaluationPrompt(blindInput);
@@ -422,7 +733,7 @@ export class FusionBudget {
422
733
  slot: 1,
423
734
  route: this.routeForStage('candidate', 1),
424
735
  conditional: false,
425
- system_prompt: FUSION_CANDIDATE_SYSTEM_PROMPT,
736
+ system_prompt: candidateSystemPrompt,
426
737
  empty_user_prompt: candidatePrompt,
427
738
  upstream_output_contract_bytes: 0,
428
739
  },
@@ -431,7 +742,7 @@ export class FusionBudget {
431
742
  slot: 2,
432
743
  route: this.routeForStage('candidate', 2),
433
744
  conditional: false,
434
- system_prompt: FUSION_CANDIDATE_SYSTEM_PROMPT,
745
+ system_prompt: candidateSystemPrompt,
435
746
  empty_user_prompt: candidatePrompt,
436
747
  upstream_output_contract_bytes: 0,
437
748
  },
@@ -440,7 +751,7 @@ export class FusionBudget {
440
751
  slot: 3,
441
752
  route: this.routeForStage('candidate', 3),
442
753
  conditional: false,
443
- system_prompt: FUSION_CANDIDATE_SYSTEM_PROMPT,
754
+ system_prompt: candidateSystemPrompt,
444
755
  empty_user_prompt: candidatePrompt,
445
756
  upstream_output_contract_bytes: 0,
446
757
  },
@@ -448,7 +759,7 @@ export class FusionBudget {
448
759
  budget_stage: 'evaluation',
449
760
  route: this.routeForStage('evaluation'),
450
761
  conditional: false,
451
- system_prompt: FUSION_EVALUATOR_SYSTEM_PROMPT,
762
+ system_prompt: this.profile.evaluatorSystemPrompt,
452
763
  empty_user_prompt: evaluationPrompt,
453
764
  upstream_output_contract_bytes: 3 * FUSION_CANDIDATE_MAX_OUTPUT_BYTES,
454
765
  },
@@ -456,7 +767,7 @@ export class FusionBudget {
456
767
  budget_stage: 'merge',
457
768
  route: this.routeForStage('merge'),
458
769
  conditional: false,
459
- system_prompt: FUSION_MERGER_SYSTEM_PROMPT,
770
+ system_prompt: this.profile.mergerSystemPrompt,
460
771
  empty_user_prompt: mergePrompt,
461
772
  upstream_output_contract_bytes:
462
773
  3 * FUSION_CANDIDATE_MAX_OUTPUT_BYTES + FUSION_EVALUATION_MAX_OUTPUT_BYTES,
@@ -465,7 +776,7 @@ export class FusionBudget {
465
776
  budget_stage: 'evaluation_repair',
466
777
  route: this.routeForStage('evaluation_repair'),
467
778
  conditional: true,
468
- system_prompt: FUSION_EVALUATION_REPAIR_SYSTEM_PROMPT,
779
+ system_prompt: this.profile.evaluationRepairSystemPrompt,
469
780
  empty_user_prompt: repairPrompt,
470
781
  upstream_output_contract_bytes:
471
782
  3 * FUSION_CANDIDATE_MAX_OUTPUT_BYTES +
@@ -517,8 +828,7 @@ export class FusionBudget {
517
828
  const emptyBlockers = selectBlockers(emptyEntries);
518
829
  let reduction = 0;
519
830
  for (const entry of entries) {
520
- const byteLimit = entry.allowed_input_tokens * FUSION_BYTES_PER_TOKEN_DIVISOR;
521
- reduction = Math.max(reduction, entry.forecast_utf8_bytes - byteLimit);
831
+ reduction = Math.max(reduction, entry.input_utf8_bytes - maxKnownTextBytes(entry.route));
522
832
  }
523
833
  reduction = Math.max(0, reduction);
524
834
  const requestBytes = utf8Bytes(input.request.text);
@@ -537,13 +847,18 @@ export class FusionBudget {
537
847
  plan: FusionBudgetPlanV1,
538
848
  artifactDir: string,
539
849
  measurementKind: FusionBudgetErrorDetail['measurement_kind'],
850
+ checkKind: FusionBudgetCheckKind,
540
851
  ): FusionError {
541
- const remediation = remediationFor(plan.empty_request);
852
+ const composition = plan.primary_blocker_composition;
853
+ const dominantByteClass = primary.input_only_estimate.rateSource.dominant_byte_class;
854
+ const remediation = dominantRemediation(plan.empty_request, composition, dominantByteClass);
855
+ const tokensOver = primary.input_only_input_tokens_upper_bound - primary.allowed_input_tokens;
542
856
  const budget: FusionBudgetErrorDetail = {
543
857
  budget_stage: primary.budget_stage,
544
858
  measurement_kind: measurementKind,
545
- measured_utf8_bytes: primary.forecast_utf8_bytes,
546
- measured_input_tokens_upper_bound: primary.forecast_input_tokens_upper_bound,
859
+ check_kind: checkKind,
860
+ measured_utf8_bytes: primary.input_utf8_bytes,
861
+ measured_input_tokens_upper_bound: primary.input_only_input_tokens_upper_bound,
547
862
  allowed_input_tokens: primary.allowed_input_tokens,
548
863
  limiting_model: {
549
864
  provider: primary.route.provider,
@@ -551,30 +866,57 @@ export class FusionBudget {
551
866
  qualified_id: primary.route.qualified_id,
552
867
  context_window_tokens: primary.route.context_window_tokens,
553
868
  },
869
+ rate_source: primary.input_only_estimate.rateSource,
870
+ backed: primary.input_only_estimate.rateSource.backed,
871
+ dominant_byte_class: dominantByteClass,
872
+ component_breakdown: componentBreakdown(composition, primary.route),
873
+ byte_class_breakdown: primary.input_only_estimate.byte_class_breakdown,
874
+ dense_regions: [],
875
+ bytes_over: primary.bytes_over,
876
+ tokens_over: Math.max(0, tokensOver),
877
+ required_allowed_tokens: primary.input_only_input_tokens_upper_bound,
878
+ route_table: routeTable(this.routes),
879
+ counterfactuals: inputCounterfactuals(primary, plan),
880
+ stage_upstream_actuals: [],
881
+ policy_id: FUSION_BUDGET_POLICY.id,
882
+ calibration_version: TOKEN_BUDGET_CALIBRATION_VERSION,
554
883
  context_policy_id: this.contextPolicyId,
555
884
  remediation,
556
885
  blockers: plan.blockers,
557
886
  artifact_dir: artifactDir,
558
887
  };
559
888
  if (primary.slot !== undefined) budget.slot = primary.slot;
560
- const composition = plan.primary_blocker_composition;
561
889
  const compositionText = composition === undefined ? 'unavailable' : formatComposition(composition);
562
890
  const additional = plan.blockers
563
891
  .filter((blocker) => blocker !== primary)
564
892
  .map((blocker) => `${entryLabel(blocker)} route=${blocker.route.qualified_id}`)
565
893
  .join('; ');
566
- const overage = primary.forecast_input_tokens_upper_bound - primary.allowed_input_tokens;
894
+ const rateText = `${primary.route.family} ${formatRateX100(primary.input_only_estimate.rateSource.effective_rate_bytes_per_token_x100)} B/tok + ${String(primary.input_only_estimate.rateSource.affine_f_tokens)} tokens (${primary.input_only_estimate.rateSource.source}, backed=${String(primary.input_only_estimate.rateSource.backed)}, dominant=${dominantByteClass})`;
895
+ const sourceWarning = primary.input_only_estimate.rateSource.warning;
896
+ const routeWarning = sourceWarning === null ? '' : ` Rate warning: ${sourceWarning}.`;
897
+ const checkText =
898
+ checkKind === 'input_only_preflight'
899
+ ? 'input-only preflight forecast'
900
+ : 'exact rendered prompt measurement';
901
+ const dominantText =
902
+ dominantByteClass === 'multibyte'
903
+ ? ' Dominant byte class is multibyte UTF-8; the fatal gate uses the conservative multibyte rate and the plan separately records the provable 1.00 B/tok advisory ceiling.'
904
+ : dominantByteClass === 'dense_ascii'
905
+ ? ' Dominant byte class is dense ASCII/low-whitespace content; the whitespace gate is a heuristic token-density proxy, not a bound.'
906
+ : ` Dominant byte class is ${dominantByteClass}.`;
567
907
  const message =
568
- `Fusion prompt budget exceeded before child creation. Primary blocking stage: ${entryLabel(primary)} on route ${primary.route.qualified_id}. ` +
569
- `Forecast ${String(primary.forecast_utf8_bytes)} UTF-8 bytes (<= ${String(primary.forecast_input_tokens_upper_bound)} input tokens) against ${String(primary.allowed_input_tokens)} allowed input tokens, over by ${String(overage)} tokens. ` +
908
+ `Fusion prompt budget exceeded by ${checkText} before child creation. Primary blocking stage: ${entryLabel(primary)} on route ${primary.route.qualified_id}. ` +
909
+ `Forecast ${String(primary.input_utf8_bytes)} UTF-8 bytes (<= ${String(primary.input_only_input_tokens_upper_bound)} input tokens) against ${String(primary.allowed_input_tokens)} allowed input tokens, over by ${String(Math.max(0, tokensOver))} tokens. ` +
910
+ `Estimator: ${rateText}.${routeWarning}${dominantText} ` +
570
911
  `No child was created. Nothing was clipped, dropped, or substituted. Artifact directory: ${artifactDir}.\n` +
571
912
  `Per-stage forecast table:\n${formatTable(plan.stages)}\n` +
572
913
  `Primary blocker byte composition: ${compositionText}.\n` +
573
914
  `Additional blockers: ${additional.length === 0 ? 'none' : additional}.\n` +
574
915
  `${formatEmptyRequestVerdict(plan.empty_request)}\n` +
916
+ `Route byte-capacity order: ${routeTable(this.routes).map((route) => `${route.qualified_id}=${String(route.byte_capacity_utf8_bytes)}B`).join(', ')}.\n` +
575
917
  `Remediation: ${remediation.join(' ')}`;
576
918
  const details = {
577
- code: 'prompt_budget_exceeded' as const,
919
+ code: budgetErrorCode(checkKind),
578
920
  childCreated: false,
579
921
  budget,
580
922
  stage: stageFromBudgetStage(primary.budget_stage),
@@ -607,7 +949,13 @@ export class FusionBudget {
607
949
 
608
950
  assertPlanFits(plan: FusionBudgetPlanV1, artifactDir: string): void {
609
951
  if (plan.primary_blocker !== undefined) {
610
- throw this.failure(plan.primary_blocker, plan, artifactDir, 'stage_forecast');
952
+ throw this.failure(
953
+ plan.primary_blocker,
954
+ plan,
955
+ artifactDir,
956
+ 'stage_forecast',
957
+ 'input_only_preflight',
958
+ );
611
959
  }
612
960
  }
613
961
 
@@ -618,19 +966,29 @@ export class FusionBudget {
618
966
  slot?: 1 | 2 | 3,
619
967
  ): void {
620
968
  const route = this.routeForStage(stage, slot);
621
- const forecastUtf8Bytes = utf8Bytes(systemPrompt) + utf8Bytes(userPrompt);
622
- const tokens = fusionTokenUpperBound(forecastUtf8Bytes);
623
- if (tokens <= route.allowed_input_tokens) return;
969
+ const inputSegments = [knownTextSegment(systemPrompt), knownTextSegment(userPrompt)];
970
+ const inputBytes = inputSegments.reduce((sum, segment) => sum + segment.bytes, 0);
971
+ const estimate = estimateRouteInput(route, inputSegments);
972
+ if (estimate.tokens <= route.allowed_input_tokens) return;
624
973
  const entry: FusionStageBudgetPlanEntry = {
625
974
  budget_stage: stage,
626
975
  route,
627
976
  conditional: stage === 'evaluation_repair',
628
- forecast_utf8_bytes: forecastUtf8Bytes,
629
- forecast_input_tokens_upper_bound: tokens,
977
+ check_kind: 'input_only_preflight',
978
+ input_utf8_bytes: inputBytes,
979
+ upstream_output_contract_bytes: 0,
980
+ forecast_utf8_bytes: inputBytes,
981
+ input_only_input_tokens_upper_bound: estimate.tokens,
982
+ forecast_input_tokens_upper_bound: estimate.tokens,
630
983
  allowed_input_tokens: route.allowed_input_tokens,
631
- signed_headroom_tokens: route.allowed_input_tokens - tokens,
632
- utilization: tokens / route.allowed_input_tokens,
984
+ input_only_signed_headroom_tokens: route.allowed_input_tokens - estimate.tokens,
985
+ signed_headroom_tokens: route.allowed_input_tokens - estimate.tokens,
986
+ input_only_utilization_basis_points: utilizationBasisPoints(estimate.tokens, route.allowed_input_tokens),
987
+ utilization_basis_points: utilizationBasisPoints(estimate.tokens, route.allowed_input_tokens),
988
+ input_only_estimate: estimate,
989
+ reservation_estimate: estimate,
633
990
  fits: false,
991
+ reservation_fits: false,
634
992
  };
635
993
  if (slot !== undefined) entry.slot = slot;
636
994
  const blocker = blockerFromEntry(entry);
@@ -645,12 +1003,53 @@ export class FusionBudget {
645
1003
  request_utf8_bytes: 0,
646
1004
  still_fails_with_empty_request: true,
647
1005
  shortening_request_can_help: false,
648
- minimum_request_byte_reduction: forecastUtf8Bytes - route.allowed_input_tokens * FUSION_BYTES_PER_TOKEN_DIVISOR,
1006
+ minimum_request_byte_reduction: blocker.bytes_over,
649
1007
  maximum_safe_request_utf8_bytes: 0,
650
1008
  blockers_with_empty_request: [blocker],
651
1009
  },
652
1010
  warnings: [],
653
1011
  };
654
- throw this.failure(blocker, plan, 'stage prompt re-measurement', 'rendered_prompt');
1012
+ throw this.failure(blocker, plan, 'stage prompt re-measurement', 'rendered_prompt', 'rendered_prompt');
1013
+ }
1014
+
1015
+ calibrationViolationForCompletedChild(
1016
+ stage: FusionStage,
1017
+ systemPrompt: string,
1018
+ userPrompt: string,
1019
+ result: FusionChildRunResult,
1020
+ slot?: 1 | 2 | 3,
1021
+ ): FusionCalibrationViolation | undefined {
1022
+ const route = this.routeForStage(stage, slot);
1023
+ const inputSegments = [knownTextSegment(systemPrompt), knownTextSegment(userPrompt)];
1024
+ const promptUtf8Bytes = inputSegments.reduce((sum, segment) => sum + segment.bytes, 0);
1025
+ const estimate = estimateRouteInput(route, inputSegments);
1026
+ const billedInput = result.usage.input + result.usage.cacheRead + result.usage.cacheWrite;
1027
+ if (billedInput <= estimate.tokens) return undefined;
1028
+ const violation: FusionCalibrationViolation = {
1029
+ schema_version: FUSION_CALIBRATION_VIOLATION_SCHEMA_VERSION,
1030
+ stage,
1031
+ attempt: result.attempt,
1032
+ route: {
1033
+ provider: result.provider,
1034
+ model: result.model,
1035
+ qualified_id: result.qualifiedId,
1036
+ },
1037
+ family: route.family,
1038
+ rate_source: estimate.rateSource,
1039
+ prompt_utf8_bytes: promptUtf8Bytes,
1040
+ prompt_sha256: sha256Hex(`${systemPrompt}\u0000${userPrompt}`),
1041
+ forecast_input_tokens: estimate.tokens,
1042
+ billed_input_tokens: billedInput,
1043
+ billed_input_breakdown: {
1044
+ input: result.usage.input,
1045
+ cache_read: result.usage.cacheRead,
1046
+ cache_write: result.usage.cacheWrite,
1047
+ },
1048
+ under_forecast_tokens: billedInput - estimate.tokens,
1049
+ byte_class_breakdown: estimate.byte_class_breakdown,
1050
+ dominant_byte_class: estimate.rateSource.dominant_byte_class,
1051
+ };
1052
+ if (slot !== undefined) violation.slot = slot;
1053
+ return violation;
655
1054
  }
656
1055
  }