pi-background-tasks 0.7.3 → 0.7.6
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/PUBLISHING.md +10 -7
- package/README.md +98 -6
- package/TESTING.md +13 -3
- package/TEST_PLAN.md +10 -5
- package/package.json +10 -8
- package/src/core/attested-pi-run.ts +19 -35
- package/src/core/common.ts +124 -8
- package/src/core/durable-fs.ts +400 -0
- package/src/core/fusion/artifacts.ts +20 -36
- package/src/core/fusion/budget.ts +656 -0
- package/src/core/fusion/config.ts +4 -36
- package/src/core/fusion/context.ts +507 -47
- package/src/core/fusion/orchestrator.ts +43 -4
- package/src/core/fusion/pi-child.ts +28 -6
- package/src/core/fusion/prompts.ts +21 -7
- package/src/core/fusion/types.ts +284 -4
- package/src/core/pi-launch.ts +225 -0
- package/src/core/registry.ts +507 -56
- package/src/core/windows-taskkill.ts +250 -0
- package/src/fusion-extension.ts +6 -1
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import {
|
|
3
|
+
FUSION_CANDIDATE_SYSTEM_PROMPT,
|
|
4
|
+
FUSION_EVALUATION_REPAIR_SYSTEM_PROMPT,
|
|
5
|
+
FUSION_EVALUATOR_SYSTEM_PROMPT,
|
|
6
|
+
FUSION_MERGER_SYSTEM_PROMPT,
|
|
7
|
+
buildBlindEvaluationInput,
|
|
8
|
+
buildCandidatePrompt,
|
|
9
|
+
buildEvaluationPrompt,
|
|
10
|
+
buildEvaluationRepairPrompt,
|
|
11
|
+
buildMergeInput,
|
|
12
|
+
buildMergePrompt,
|
|
13
|
+
type AnonymousFusionCandidate,
|
|
14
|
+
} from './prompts.js';
|
|
15
|
+
import {
|
|
16
|
+
FUSION_BUDGET_PLAN_SCHEMA_VERSION,
|
|
17
|
+
FUSION_EVALUATION_SCHEMA_VERSION,
|
|
18
|
+
FusionError,
|
|
19
|
+
type FusionBudgetBlocker,
|
|
20
|
+
type FusionBudgetEmptyRequestVerdict,
|
|
21
|
+
type FusionBudgetErrorDetail,
|
|
22
|
+
type FusionBudgetPlanV1,
|
|
23
|
+
type FusionBudgetPolicyDescriptor,
|
|
24
|
+
type FusionBudgetStage,
|
|
25
|
+
type FusionBudgetStageComposition,
|
|
26
|
+
type FusionBudgetWarning,
|
|
27
|
+
type FusionCanonicalInputV3,
|
|
28
|
+
type FusionEvaluationV1,
|
|
29
|
+
type FusionRouteCapacity,
|
|
30
|
+
type FusionStage,
|
|
31
|
+
type FusionStageBudgetPlanEntry,
|
|
32
|
+
type ResolvedFusionModel,
|
|
33
|
+
type ResolvedFusionModels,
|
|
34
|
+
} from './types.js';
|
|
35
|
+
|
|
36
|
+
export const FUSION_BYTES_PER_TOKEN_DIVISOR = 2;
|
|
37
|
+
|
|
38
|
+
export const FUSION_CANDIDATE_MAX_OUTPUT_BYTES = 48 * 1024;
|
|
39
|
+
export const FUSION_EVALUATION_MAX_OUTPUT_BYTES = 64 * 1024;
|
|
40
|
+
export const FUSION_MERGE_MAX_OUTPUT_BYTES = 64 * 1024;
|
|
41
|
+
export const FUSION_DIAGNOSTICS_MAX_BYTES = 8 * 1024;
|
|
42
|
+
|
|
43
|
+
export const FUSION_RESERVED_OUTPUT_TOKENS = Math.ceil(
|
|
44
|
+
FUSION_MERGE_MAX_OUTPUT_BYTES / FUSION_BYTES_PER_TOKEN_DIVISOR,
|
|
45
|
+
);
|
|
46
|
+
export const FUSION_FRAMING_RESERVE_TOKENS = 4_096;
|
|
47
|
+
export const FUSION_SAFETY_RESERVE_TOKENS = 4_096;
|
|
48
|
+
export const FUSION_MIN_CANONICAL_INPUT_TOKENS = 8_192;
|
|
49
|
+
export const FUSION_MIN_CONTEXT_WINDOW_TOKENS =
|
|
50
|
+
FUSION_MIN_CANONICAL_INPUT_TOKENS +
|
|
51
|
+
FUSION_RESERVED_OUTPUT_TOKENS +
|
|
52
|
+
FUSION_FRAMING_RESERVE_TOKENS +
|
|
53
|
+
FUSION_SAFETY_RESERVE_TOKENS;
|
|
54
|
+
export const FUSION_UTILIZATION_WARNING_THRESHOLD = 0.8;
|
|
55
|
+
|
|
56
|
+
export const FUSION_BUDGET_POLICY: FusionBudgetPolicyDescriptor = {
|
|
57
|
+
id: 'fusion-budget-policy-v2',
|
|
58
|
+
bytes_per_token_divisor: FUSION_BYTES_PER_TOKEN_DIVISOR,
|
|
59
|
+
reserved_output_tokens: FUSION_RESERVED_OUTPUT_TOKENS,
|
|
60
|
+
framing_reserve_tokens: FUSION_FRAMING_RESERVE_TOKENS,
|
|
61
|
+
safety_reserve_tokens: FUSION_SAFETY_RESERVE_TOKENS,
|
|
62
|
+
candidate_output_contract_bytes: FUSION_CANDIDATE_MAX_OUTPUT_BYTES,
|
|
63
|
+
evaluation_output_contract_bytes: FUSION_EVALUATION_MAX_OUTPUT_BYTES,
|
|
64
|
+
merge_output_contract_bytes: FUSION_MERGE_MAX_OUTPUT_BYTES,
|
|
65
|
+
diagnostics_contract_bytes: FUSION_DIAGNOSTICS_MAX_BYTES,
|
|
66
|
+
utilization_warning_threshold: FUSION_UTILIZATION_WARNING_THRESHOLD,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const EMPTY_REMEDIATION: readonly string[] = Object.freeze([
|
|
70
|
+
'Start a fresh Pi conversation, or run Fusion earlier in the session.',
|
|
71
|
+
"Raise the route's context window with a larger-context model via /fusion-models.",
|
|
72
|
+
'Restate only the required prior findings as visible conversation text.',
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
const REQUEST_REMEDIATION: readonly string[] = Object.freeze([
|
|
76
|
+
'Provide a shorter, self-contained fusion_brainstorm prompt.',
|
|
77
|
+
'Start a fresh Pi conversation, or run Fusion earlier in the session.',
|
|
78
|
+
"Raise the route's context window with a larger-context model via /fusion-models.",
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
const EMPTY_CANDIDATES: readonly [
|
|
82
|
+
AnonymousFusionCandidate,
|
|
83
|
+
AnonymousFusionCandidate,
|
|
84
|
+
AnonymousFusionCandidate,
|
|
85
|
+
] = Object.freeze([
|
|
86
|
+
Object.freeze({ candidate_id: 'A', response: '' }),
|
|
87
|
+
Object.freeze({ candidate_id: 'B', response: '' }),
|
|
88
|
+
Object.freeze({ candidate_id: 'C', response: '' }),
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
const EMPTY_EVALUATION: FusionEvaluationV1 = Object.freeze({
|
|
92
|
+
schema_version: FUSION_EVALUATION_SCHEMA_VERSION,
|
|
93
|
+
candidate_assessments: Object.freeze([
|
|
94
|
+
Object.freeze({
|
|
95
|
+
candidate_id: 'A',
|
|
96
|
+
summary: '',
|
|
97
|
+
strengths: Object.freeze([]),
|
|
98
|
+
limitations: Object.freeze([]),
|
|
99
|
+
useful_contributions: Object.freeze([]),
|
|
100
|
+
risks: Object.freeze([]),
|
|
101
|
+
}),
|
|
102
|
+
Object.freeze({
|
|
103
|
+
candidate_id: 'B',
|
|
104
|
+
summary: '',
|
|
105
|
+
strengths: Object.freeze([]),
|
|
106
|
+
limitations: Object.freeze([]),
|
|
107
|
+
useful_contributions: Object.freeze([]),
|
|
108
|
+
risks: Object.freeze([]),
|
|
109
|
+
}),
|
|
110
|
+
Object.freeze({
|
|
111
|
+
candidate_id: 'C',
|
|
112
|
+
summary: '',
|
|
113
|
+
strengths: Object.freeze([]),
|
|
114
|
+
limitations: Object.freeze([]),
|
|
115
|
+
useful_contributions: Object.freeze([]),
|
|
116
|
+
risks: Object.freeze([]),
|
|
117
|
+
}),
|
|
118
|
+
]) as readonly [
|
|
119
|
+
FusionEvaluationV1['candidate_assessments'][0],
|
|
120
|
+
FusionEvaluationV1['candidate_assessments'][1],
|
|
121
|
+
FusionEvaluationV1['candidate_assessments'][2],
|
|
122
|
+
],
|
|
123
|
+
agreements: Object.freeze([]),
|
|
124
|
+
conflicts: Object.freeze([]),
|
|
125
|
+
synthesis_plan: Object.freeze({
|
|
126
|
+
must_include: Object.freeze([]),
|
|
127
|
+
must_resolve: Object.freeze([]),
|
|
128
|
+
must_avoid: Object.freeze([]),
|
|
129
|
+
}),
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
interface StageForecastDraft {
|
|
133
|
+
budget_stage: FusionBudgetStage;
|
|
134
|
+
slot?: 1 | 2 | 3;
|
|
135
|
+
route: FusionRouteCapacity;
|
|
136
|
+
conditional: boolean;
|
|
137
|
+
system_prompt: string;
|
|
138
|
+
empty_user_prompt: string;
|
|
139
|
+
upstream_output_contract_bytes: number;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function fusionTokenUpperBound(utf8Bytes: number): number {
|
|
143
|
+
return Math.ceil(utf8Bytes / FUSION_BYTES_PER_TOKEN_DIVISOR);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function fusionOutputContractBytes(stage: FusionStage): number {
|
|
147
|
+
if (stage === 'candidate') return FUSION_CANDIDATE_MAX_OUTPUT_BYTES;
|
|
148
|
+
if (stage === 'evaluation') return FUSION_EVALUATION_MAX_OUTPUT_BYTES;
|
|
149
|
+
return FUSION_MERGE_MAX_OUTPUT_BYTES;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function assertChildOutputWithinContract(stage: FusionStage, text: string): void {
|
|
153
|
+
const bytes = Buffer.byteLength(JSON.stringify(text), 'utf8');
|
|
154
|
+
const allowed = fusionOutputContractBytes(stage);
|
|
155
|
+
if (bytes <= allowed) return;
|
|
156
|
+
throw new FusionError(
|
|
157
|
+
`fusion ${stage} response is ${String(bytes)} JSON-rendered bytes, exceeding the ${String(allowed)}-byte output contract for that stage; the response is preserved in the run artifacts and is not forwarded or truncated`,
|
|
158
|
+
{ code: 'child_output_cap', stage, childCreated: true },
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function utf8Bytes(value: string): number {
|
|
163
|
+
return Buffer.byteLength(value, 'utf8');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function sha256Hex(value: string): string {
|
|
167
|
+
return createHash('sha256').update(value, 'utf8').digest('hex');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function requirePositiveContextWindow(model: ResolvedFusionModel, role: string): number {
|
|
171
|
+
const value = model.contextWindow;
|
|
172
|
+
if (!Number.isFinite(value) || !Number.isInteger(value) || value <= 0) {
|
|
173
|
+
throw new FusionError(
|
|
174
|
+
`fusion ${role} route ${model.qualifiedId} has no usable context window capacity`,
|
|
175
|
+
{ code: 'model_capacity_unknown', childCreated: false },
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
return value;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function routeCapacity(
|
|
182
|
+
model: ResolvedFusionModel,
|
|
183
|
+
role: FusionRouteCapacity['role'],
|
|
184
|
+
): FusionRouteCapacity {
|
|
185
|
+
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;
|
|
191
|
+
if (allowed < FUSION_MIN_CANONICAL_INPUT_TOKENS) {
|
|
192
|
+
throw new FusionError(
|
|
193
|
+
`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
|
+
{ code: 'model_capacity_unknown', childCreated: false },
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
role,
|
|
199
|
+
provider: model.provider,
|
|
200
|
+
model: model.model,
|
|
201
|
+
qualified_id: model.qualifiedId,
|
|
202
|
+
context_window_tokens: contextWindow,
|
|
203
|
+
reserved_output_tokens: FUSION_RESERVED_OUTPUT_TOKENS,
|
|
204
|
+
framing_reserve_tokens: FUSION_FRAMING_RESERVE_TOKENS,
|
|
205
|
+
safety_reserve_tokens: FUSION_SAFETY_RESERVE_TOKENS,
|
|
206
|
+
allowed_input_tokens: allowed,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function fusionRouteCapacities(models: ResolvedFusionModels): readonly FusionRouteCapacity[] {
|
|
211
|
+
return [
|
|
212
|
+
routeCapacity(models.candidates[0], 'candidate-1'),
|
|
213
|
+
routeCapacity(models.candidates[1], 'candidate-2'),
|
|
214
|
+
routeCapacity(models.candidates[2], 'candidate-3'),
|
|
215
|
+
routeCapacity(models.evaluator, 'evaluator'),
|
|
216
|
+
routeCapacity(models.merger, 'merger'),
|
|
217
|
+
];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function fusionLimitingRoute(
|
|
221
|
+
routes: readonly FusionRouteCapacity[],
|
|
222
|
+
): FusionRouteCapacity {
|
|
223
|
+
let limiting: FusionRouteCapacity | undefined;
|
|
224
|
+
for (const route of routes) {
|
|
225
|
+
if (limiting === undefined || route.allowed_input_tokens < limiting.allowed_input_tokens) {
|
|
226
|
+
limiting = route;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (limiting === undefined) {
|
|
230
|
+
throw new FusionError('fusion budget planning received no configured routes', {
|
|
231
|
+
code: 'model_capacity_unknown',
|
|
232
|
+
childCreated: false,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
return limiting;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function routeByRole(
|
|
239
|
+
routes: readonly FusionRouteCapacity[],
|
|
240
|
+
role: FusionRouteCapacity['role'],
|
|
241
|
+
): FusionRouteCapacity {
|
|
242
|
+
const route = routes.find((item) => item.role === role);
|
|
243
|
+
if (route === undefined) {
|
|
244
|
+
throw new FusionError(`fusion budget route ${role} is missing`, {
|
|
245
|
+
code: 'model_capacity_unknown',
|
|
246
|
+
childCreated: false,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
return route;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function candidateRole(slot: 1 | 2 | 3): FusionRouteCapacity['role'] {
|
|
253
|
+
if (slot === 1) return 'candidate-1';
|
|
254
|
+
if (slot === 2) return 'candidate-2';
|
|
255
|
+
return 'candidate-3';
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
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);
|
|
264
|
+
const entry: FusionStageBudgetPlanEntry = {
|
|
265
|
+
budget_stage: draft.budget_stage,
|
|
266
|
+
route: draft.route,
|
|
267
|
+
conditional: draft.conditional,
|
|
268
|
+
forecast_utf8_bytes: forecastUtf8Bytes,
|
|
269
|
+
forecast_input_tokens_upper_bound: tokens,
|
|
270
|
+
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,
|
|
274
|
+
};
|
|
275
|
+
if (draft.slot !== undefined) entry.slot = draft.slot;
|
|
276
|
+
return entry;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function blockerFromEntry(entry: FusionStageBudgetPlanEntry): FusionBudgetBlocker {
|
|
280
|
+
return { ...entry, overage_tokens: Math.max(0, -entry.signed_headroom_tokens) };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function blockerOrder(blocker: FusionBudgetBlocker): number {
|
|
284
|
+
if (blocker.budget_stage === 'candidate') return blocker.slot ?? 1;
|
|
285
|
+
if (blocker.budget_stage === 'evaluation') return 4;
|
|
286
|
+
if (blocker.budget_stage === 'merge') return 5;
|
|
287
|
+
return 6;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function selectBlockers(entries: readonly FusionStageBudgetPlanEntry[]): readonly FusionBudgetBlocker[] {
|
|
291
|
+
return entries.filter((entry) => !entry.fits).map(blockerFromEntry).sort((left, right) => {
|
|
292
|
+
const byOrder = blockerOrder(left) - blockerOrder(right);
|
|
293
|
+
if (byOrder !== 0) return byOrder;
|
|
294
|
+
return left.route.role.localeCompare(right.route.role);
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function selectPrimaryBlocker(
|
|
299
|
+
blockers: readonly FusionBudgetBlocker[],
|
|
300
|
+
): FusionBudgetBlocker | undefined {
|
|
301
|
+
const mandatory = blockers.find((blocker) => !blocker.conditional);
|
|
302
|
+
return mandatory ?? blockers[0];
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function replaceRequestText(input: FusionCanonicalInputV3, text: string): FusionCanonicalInputV3 {
|
|
306
|
+
return {
|
|
307
|
+
...input,
|
|
308
|
+
request: {
|
|
309
|
+
...input.request,
|
|
310
|
+
text,
|
|
311
|
+
sha256: sha256Hex(text),
|
|
312
|
+
},
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function visibleTextBytes(input: FusionCanonicalInputV3): number {
|
|
317
|
+
let total = 0;
|
|
318
|
+
for (const entry of input.conversation_projection.entries) {
|
|
319
|
+
if (entry.kind === 'text') total += utf8Bytes(JSON.stringify(entry.text));
|
|
320
|
+
}
|
|
321
|
+
return total;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function omissionReceiptBytes(input: FusionCanonicalInputV3): number {
|
|
325
|
+
let total = 0;
|
|
326
|
+
for (const entry of input.conversation_projection.entries) {
|
|
327
|
+
if (entry.kind === 'omitted_activity') total += utf8Bytes(JSON.stringify(entry));
|
|
328
|
+
}
|
|
329
|
+
return total;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function warningFromEntry(entry: FusionStageBudgetPlanEntry): FusionBudgetWarning {
|
|
333
|
+
return { ...entry, threshold: FUSION_UTILIZATION_WARNING_THRESHOLD };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function warningsFor(entries: readonly FusionStageBudgetPlanEntry[]): readonly FusionBudgetWarning[] {
|
|
337
|
+
return entries
|
|
338
|
+
.filter((entry) => entry.fits && entry.utilization >= FUSION_UTILIZATION_WARNING_THRESHOLD)
|
|
339
|
+
.map(warningFromEntry);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function entryLabel(entry: FusionStageBudgetPlanEntry): string {
|
|
343
|
+
const slot = entry.slot === undefined ? '' : `-${String(entry.slot)}`;
|
|
344
|
+
const conditional = entry.conditional ? ' (conditional)' : '';
|
|
345
|
+
return `${entry.budget_stage}${slot}${conditional}`;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function formatTable(entries: readonly FusionStageBudgetPlanEntry[]): string {
|
|
349
|
+
const lines = entries.map(
|
|
350
|
+
(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'}`,
|
|
352
|
+
);
|
|
353
|
+
return lines.join('\n');
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function formatComposition(composition: FusionBudgetStageComposition): string {
|
|
357
|
+
return [
|
|
358
|
+
`visible text ${String(composition.visible_text_bytes)} B`,
|
|
359
|
+
`omission receipts ${String(composition.omission_receipt_bytes)} B`,
|
|
360
|
+
`projection metadata ${String(composition.projection_metadata_bytes)} B`,
|
|
361
|
+
`request ${String(composition.request_bytes)} B`,
|
|
362
|
+
`static stage framing ${String(composition.static_stage_framing_bytes)} B`,
|
|
363
|
+
`upstream output contracts ${String(composition.upstream_output_contract_bytes)} B`,
|
|
364
|
+
].join('; ');
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function remediationFor(verdict: FusionBudgetEmptyRequestVerdict): readonly string[] {
|
|
368
|
+
return verdict.still_fails_with_empty_request ? EMPTY_REMEDIATION : REQUEST_REMEDIATION;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function formatEmptyRequestVerdict(verdict: FusionBudgetEmptyRequestVerdict): string {
|
|
372
|
+
if (verdict.still_fails_with_empty_request) {
|
|
373
|
+
return `Empty-request counterfactual: still fails with ${String(verdict.blockers_with_empty_request.length)} blocking stage(s), so shortening the request cannot make this workflow fit.`;
|
|
374
|
+
}
|
|
375
|
+
if (verdict.minimum_request_byte_reduction === 0) {
|
|
376
|
+
return 'Empty-request counterfactual: fits; no request reduction is required by the current plan.';
|
|
377
|
+
}
|
|
378
|
+
return `Empty-request counterfactual: fits, so request size determines feasibility; reduce the request by at least ${String(verdict.minimum_request_byte_reduction)} UTF-8 bytes, leaving at most ${String(verdict.maximum_safe_request_utf8_bytes)} UTF-8 bytes.`;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function stageFromBudgetStage(stage: FusionBudgetStage): FusionStage {
|
|
382
|
+
if (stage === 'merge') return 'merge';
|
|
383
|
+
if (stage === 'candidate') return 'candidate';
|
|
384
|
+
return 'evaluation';
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export class FusionBudget {
|
|
388
|
+
readonly routes: readonly FusionRouteCapacity[];
|
|
389
|
+
readonly limiting: FusionRouteCapacity;
|
|
390
|
+
private readonly contextPolicyId: string;
|
|
391
|
+
|
|
392
|
+
constructor(models: ResolvedFusionModels, contextPolicyId: string) {
|
|
393
|
+
this.routes = fusionRouteCapacities(models);
|
|
394
|
+
this.limiting = fusionLimitingRoute(this.routes);
|
|
395
|
+
this.contextPolicyId = contextPolicyId;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
get allowedInputTokens(): number {
|
|
399
|
+
return this.limiting.allowed_input_tokens;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
private routeForStage(stage: FusionBudgetStage, slot?: 1 | 2 | 3): FusionRouteCapacity {
|
|
403
|
+
if (stage === 'candidate') return routeByRole(this.routes, candidateRole(slot ?? 1));
|
|
404
|
+
if (stage === 'merge') return routeByRole(this.routes, 'merger');
|
|
405
|
+
return routeByRole(this.routes, 'evaluator');
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
private drafts(input: FusionCanonicalInputV3): readonly StageForecastDraft[] {
|
|
409
|
+
const candidatePrompt = buildCandidatePrompt(input);
|
|
410
|
+
const blindInput = buildBlindEvaluationInput(input, EMPTY_CANDIDATES);
|
|
411
|
+
const evaluationPrompt = buildEvaluationPrompt(blindInput);
|
|
412
|
+
const repairPrompt = buildEvaluationRepairPrompt({
|
|
413
|
+
schema_version: 'pi-background-tasks.fusion-evaluation-repair-input.v1',
|
|
414
|
+
original_blind_input: blindInput,
|
|
415
|
+
invalid_output: '',
|
|
416
|
+
validation_errors: [],
|
|
417
|
+
});
|
|
418
|
+
const mergePrompt = buildMergePrompt(buildMergeInput(input, EMPTY_CANDIDATES, EMPTY_EVALUATION));
|
|
419
|
+
return [
|
|
420
|
+
{
|
|
421
|
+
budget_stage: 'candidate',
|
|
422
|
+
slot: 1,
|
|
423
|
+
route: this.routeForStage('candidate', 1),
|
|
424
|
+
conditional: false,
|
|
425
|
+
system_prompt: FUSION_CANDIDATE_SYSTEM_PROMPT,
|
|
426
|
+
empty_user_prompt: candidatePrompt,
|
|
427
|
+
upstream_output_contract_bytes: 0,
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
budget_stage: 'candidate',
|
|
431
|
+
slot: 2,
|
|
432
|
+
route: this.routeForStage('candidate', 2),
|
|
433
|
+
conditional: false,
|
|
434
|
+
system_prompt: FUSION_CANDIDATE_SYSTEM_PROMPT,
|
|
435
|
+
empty_user_prompt: candidatePrompt,
|
|
436
|
+
upstream_output_contract_bytes: 0,
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
budget_stage: 'candidate',
|
|
440
|
+
slot: 3,
|
|
441
|
+
route: this.routeForStage('candidate', 3),
|
|
442
|
+
conditional: false,
|
|
443
|
+
system_prompt: FUSION_CANDIDATE_SYSTEM_PROMPT,
|
|
444
|
+
empty_user_prompt: candidatePrompt,
|
|
445
|
+
upstream_output_contract_bytes: 0,
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
budget_stage: 'evaluation',
|
|
449
|
+
route: this.routeForStage('evaluation'),
|
|
450
|
+
conditional: false,
|
|
451
|
+
system_prompt: FUSION_EVALUATOR_SYSTEM_PROMPT,
|
|
452
|
+
empty_user_prompt: evaluationPrompt,
|
|
453
|
+
upstream_output_contract_bytes: 3 * FUSION_CANDIDATE_MAX_OUTPUT_BYTES,
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
budget_stage: 'merge',
|
|
457
|
+
route: this.routeForStage('merge'),
|
|
458
|
+
conditional: false,
|
|
459
|
+
system_prompt: FUSION_MERGER_SYSTEM_PROMPT,
|
|
460
|
+
empty_user_prompt: mergePrompt,
|
|
461
|
+
upstream_output_contract_bytes:
|
|
462
|
+
3 * FUSION_CANDIDATE_MAX_OUTPUT_BYTES + FUSION_EVALUATION_MAX_OUTPUT_BYTES,
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
budget_stage: 'evaluation_repair',
|
|
466
|
+
route: this.routeForStage('evaluation_repair'),
|
|
467
|
+
conditional: true,
|
|
468
|
+
system_prompt: FUSION_EVALUATION_REPAIR_SYSTEM_PROMPT,
|
|
469
|
+
empty_user_prompt: repairPrompt,
|
|
470
|
+
upstream_output_contract_bytes:
|
|
471
|
+
3 * FUSION_CANDIDATE_MAX_OUTPUT_BYTES +
|
|
472
|
+
FUSION_EVALUATION_MAX_OUTPUT_BYTES +
|
|
473
|
+
FUSION_DIAGNOSTICS_MAX_BYTES,
|
|
474
|
+
},
|
|
475
|
+
];
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
private entries(input: FusionCanonicalInputV3): readonly FusionStageBudgetPlanEntry[] {
|
|
479
|
+
return this.drafts(input).map(forecastEntry);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
private composition(
|
|
483
|
+
input: FusionCanonicalInputV3,
|
|
484
|
+
blocker: FusionBudgetBlocker,
|
|
485
|
+
): FusionBudgetStageComposition {
|
|
486
|
+
const canonicalBytes = utf8Bytes(buildCandidatePrompt(input));
|
|
487
|
+
const emptyRequestCanonicalBytes = utf8Bytes(buildCandidatePrompt(replaceRequestText(input, '')));
|
|
488
|
+
const requestBytes = canonicalBytes - emptyRequestCanonicalBytes;
|
|
489
|
+
const visible = visibleTextBytes(input);
|
|
490
|
+
const omissions = omissionReceiptBytes(input);
|
|
491
|
+
const projectionMetadata = canonicalBytes - requestBytes - visible - omissions;
|
|
492
|
+
const draft = this.drafts(input).find(
|
|
493
|
+
(item) => item.budget_stage === blocker.budget_stage && item.slot === blocker.slot,
|
|
494
|
+
);
|
|
495
|
+
if (draft === undefined) {
|
|
496
|
+
throw new FusionError('primary budget blocker disappeared during composition', {
|
|
497
|
+
code: 'orchestration_failed',
|
|
498
|
+
childCreated: false,
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
return {
|
|
502
|
+
visible_text_bytes: visible,
|
|
503
|
+
omission_receipt_bytes: omissions,
|
|
504
|
+
projection_metadata_bytes: projectionMetadata,
|
|
505
|
+
request_bytes: requestBytes,
|
|
506
|
+
static_stage_framing_bytes:
|
|
507
|
+
utf8Bytes(draft.system_prompt) + utf8Bytes(draft.empty_user_prompt) - canonicalBytes,
|
|
508
|
+
upstream_output_contract_bytes: draft.upstream_output_contract_bytes,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
private emptyRequestVerdict(
|
|
513
|
+
input: FusionCanonicalInputV3,
|
|
514
|
+
entries: readonly FusionStageBudgetPlanEntry[],
|
|
515
|
+
): FusionBudgetEmptyRequestVerdict {
|
|
516
|
+
const emptyEntries = this.entries(replaceRequestText(input, ''));
|
|
517
|
+
const emptyBlockers = selectBlockers(emptyEntries);
|
|
518
|
+
let reduction = 0;
|
|
519
|
+
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);
|
|
522
|
+
}
|
|
523
|
+
reduction = Math.max(0, reduction);
|
|
524
|
+
const requestBytes = utf8Bytes(input.request.text);
|
|
525
|
+
return {
|
|
526
|
+
request_utf8_bytes: requestBytes,
|
|
527
|
+
still_fails_with_empty_request: emptyBlockers.length > 0,
|
|
528
|
+
shortening_request_can_help: emptyBlockers.length === 0,
|
|
529
|
+
minimum_request_byte_reduction: reduction,
|
|
530
|
+
maximum_safe_request_utf8_bytes: Math.max(0, requestBytes - reduction),
|
|
531
|
+
blockers_with_empty_request: emptyBlockers,
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
private failure(
|
|
536
|
+
primary: FusionBudgetBlocker,
|
|
537
|
+
plan: FusionBudgetPlanV1,
|
|
538
|
+
artifactDir: string,
|
|
539
|
+
measurementKind: FusionBudgetErrorDetail['measurement_kind'],
|
|
540
|
+
): FusionError {
|
|
541
|
+
const remediation = remediationFor(plan.empty_request);
|
|
542
|
+
const budget: FusionBudgetErrorDetail = {
|
|
543
|
+
budget_stage: primary.budget_stage,
|
|
544
|
+
measurement_kind: measurementKind,
|
|
545
|
+
measured_utf8_bytes: primary.forecast_utf8_bytes,
|
|
546
|
+
measured_input_tokens_upper_bound: primary.forecast_input_tokens_upper_bound,
|
|
547
|
+
allowed_input_tokens: primary.allowed_input_tokens,
|
|
548
|
+
limiting_model: {
|
|
549
|
+
provider: primary.route.provider,
|
|
550
|
+
model: primary.route.model,
|
|
551
|
+
qualified_id: primary.route.qualified_id,
|
|
552
|
+
context_window_tokens: primary.route.context_window_tokens,
|
|
553
|
+
},
|
|
554
|
+
context_policy_id: this.contextPolicyId,
|
|
555
|
+
remediation,
|
|
556
|
+
blockers: plan.blockers,
|
|
557
|
+
artifact_dir: artifactDir,
|
|
558
|
+
};
|
|
559
|
+
if (primary.slot !== undefined) budget.slot = primary.slot;
|
|
560
|
+
const composition = plan.primary_blocker_composition;
|
|
561
|
+
const compositionText = composition === undefined ? 'unavailable' : formatComposition(composition);
|
|
562
|
+
const additional = plan.blockers
|
|
563
|
+
.filter((blocker) => blocker !== primary)
|
|
564
|
+
.map((blocker) => `${entryLabel(blocker)} route=${blocker.route.qualified_id}`)
|
|
565
|
+
.join('; ');
|
|
566
|
+
const overage = primary.forecast_input_tokens_upper_bound - primary.allowed_input_tokens;
|
|
567
|
+
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. ` +
|
|
570
|
+
`No child was created. Nothing was clipped, dropped, or substituted. Artifact directory: ${artifactDir}.\n` +
|
|
571
|
+
`Per-stage forecast table:\n${formatTable(plan.stages)}\n` +
|
|
572
|
+
`Primary blocker byte composition: ${compositionText}.\n` +
|
|
573
|
+
`Additional blockers: ${additional.length === 0 ? 'none' : additional}.\n` +
|
|
574
|
+
`${formatEmptyRequestVerdict(plan.empty_request)}\n` +
|
|
575
|
+
`Remediation: ${remediation.join(' ')}`;
|
|
576
|
+
const details = {
|
|
577
|
+
code: 'prompt_budget_exceeded' as const,
|
|
578
|
+
childCreated: false,
|
|
579
|
+
budget,
|
|
580
|
+
stage: stageFromBudgetStage(primary.budget_stage),
|
|
581
|
+
};
|
|
582
|
+
if (primary.slot !== undefined) return new FusionError(message, { ...details, slot: primary.slot });
|
|
583
|
+
return new FusionError(message, details);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
plan(input: FusionCanonicalInputV3): FusionBudgetPlanV1 {
|
|
587
|
+
const stages = this.entries(input);
|
|
588
|
+
const blockers = selectBlockers(stages);
|
|
589
|
+
const primary = selectPrimaryBlocker(blockers);
|
|
590
|
+
const emptyRequest = this.emptyRequestVerdict(input, stages);
|
|
591
|
+
const base: FusionBudgetPlanV1 = {
|
|
592
|
+
schema_version: FUSION_BUDGET_PLAN_SCHEMA_VERSION,
|
|
593
|
+
policy: FUSION_BUDGET_POLICY,
|
|
594
|
+
routes: this.routes,
|
|
595
|
+
stages,
|
|
596
|
+
blockers,
|
|
597
|
+
empty_request: emptyRequest,
|
|
598
|
+
warnings: warningsFor(stages),
|
|
599
|
+
};
|
|
600
|
+
if (primary === undefined) return base;
|
|
601
|
+
return {
|
|
602
|
+
...base,
|
|
603
|
+
primary_blocker: primary,
|
|
604
|
+
primary_blocker_composition: this.composition(input, primary),
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
assertPlanFits(plan: FusionBudgetPlanV1, artifactDir: string): void {
|
|
609
|
+
if (plan.primary_blocker !== undefined) {
|
|
610
|
+
throw this.failure(plan.primary_blocker, plan, artifactDir, 'stage_forecast');
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
assertStagePrompt(
|
|
615
|
+
stage: FusionBudgetStage,
|
|
616
|
+
systemPrompt: string,
|
|
617
|
+
userPrompt: string,
|
|
618
|
+
slot?: 1 | 2 | 3,
|
|
619
|
+
): void {
|
|
620
|
+
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;
|
|
624
|
+
const entry: FusionStageBudgetPlanEntry = {
|
|
625
|
+
budget_stage: stage,
|
|
626
|
+
route,
|
|
627
|
+
conditional: stage === 'evaluation_repair',
|
|
628
|
+
forecast_utf8_bytes: forecastUtf8Bytes,
|
|
629
|
+
forecast_input_tokens_upper_bound: tokens,
|
|
630
|
+
allowed_input_tokens: route.allowed_input_tokens,
|
|
631
|
+
signed_headroom_tokens: route.allowed_input_tokens - tokens,
|
|
632
|
+
utilization: tokens / route.allowed_input_tokens,
|
|
633
|
+
fits: false,
|
|
634
|
+
};
|
|
635
|
+
if (slot !== undefined) entry.slot = slot;
|
|
636
|
+
const blocker = blockerFromEntry(entry);
|
|
637
|
+
const plan: FusionBudgetPlanV1 = {
|
|
638
|
+
schema_version: FUSION_BUDGET_PLAN_SCHEMA_VERSION,
|
|
639
|
+
policy: FUSION_BUDGET_POLICY,
|
|
640
|
+
routes: this.routes,
|
|
641
|
+
stages: [entry],
|
|
642
|
+
blockers: [blocker],
|
|
643
|
+
primary_blocker: blocker,
|
|
644
|
+
empty_request: {
|
|
645
|
+
request_utf8_bytes: 0,
|
|
646
|
+
still_fails_with_empty_request: true,
|
|
647
|
+
shortening_request_can_help: false,
|
|
648
|
+
minimum_request_byte_reduction: forecastUtf8Bytes - route.allowed_input_tokens * FUSION_BYTES_PER_TOKEN_DIVISOR,
|
|
649
|
+
maximum_safe_request_utf8_bytes: 0,
|
|
650
|
+
blockers_with_empty_request: [blocker],
|
|
651
|
+
},
|
|
652
|
+
warnings: [],
|
|
653
|
+
};
|
|
654
|
+
throw this.failure(blocker, plan, 'stage prompt re-measurement', 'rendered_prompt');
|
|
655
|
+
}
|
|
656
|
+
}
|