@pi-unipi/background-tasks 2.16.1 → 2.17.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.
Files changed (46) hide show
  1. package/README.md +21 -27
  2. package/package.json +3 -4
  3. package/src/cards.ts +76 -0
  4. package/src/child-process.ts +1 -1
  5. package/src/config.ts +0 -42
  6. package/src/context-visible-conversation-v2.ts +1 -1
  7. package/src/delegate/artifacts.ts +1 -1
  8. package/src/delegate/launch.ts +17 -30
  9. package/src/delegate/result-package.ts +1 -1
  10. package/src/delegate/runner.ts +1 -20
  11. package/src/delegate/seed.ts +1 -1
  12. package/src/delegate-extension.ts +16 -168
  13. package/src/index.ts +53 -25
  14. package/src/json-utils.ts +56 -0
  15. package/src/package-assets.ts +51 -0
  16. package/src/registry.ts +8 -459
  17. package/src/task-manager.ts +13 -2
  18. package/src/tools.ts +4 -189
  19. package/src/types.ts +17 -70
  20. package/extensions/anthropic-attribution.ts +0 -1
  21. package/extensions/fusion-child.ts +0 -1
  22. package/src/anthropic-attribution-path.ts +0 -21
  23. package/src/anthropic-attribution.ts +0 -1983
  24. package/src/attested-pi-run.ts +0 -612
  25. package/src/fixtures/fusion-golden-bytes.json +0 -310
  26. package/src/fixtures/fusion-validate-golden-bytes.json +0 -282
  27. package/src/fusion/artifacts.ts +0 -967
  28. package/src/fusion/budget.ts +0 -1162
  29. package/src/fusion/child-protocol.ts +0 -305
  30. package/src/fusion/claude-cache.ts +0 -207
  31. package/src/fusion/clean-context.ts +0 -91
  32. package/src/fusion/config.ts +0 -449
  33. package/src/fusion/context.ts +0 -265
  34. package/src/fusion/evaluation.ts +0 -800
  35. package/src/fusion/orchestrator.ts +0 -1288
  36. package/src/fusion/output-contract.ts +0 -34
  37. package/src/fusion/pi-child.ts +0 -2373
  38. package/src/fusion/prompts.ts +0 -345
  39. package/src/fusion/result-package.ts +0 -959
  40. package/src/fusion/source-policy.ts +0 -257
  41. package/src/fusion/types.ts +0 -1139
  42. package/src/fusion/web-fetch.ts +0 -1060
  43. package/src/fusion/workflows.ts +0 -184
  44. package/src/fusion-child-extension.ts +0 -1052
  45. package/src/fusion-extension.ts +0 -1293
  46. package/src/ui/fusion-model-selector.ts +0 -322
@@ -1,1288 +0,0 @@
1
- import { createHash, randomBytes as nodeRandomBytes } from 'node:crypto';
2
- import { canonicalJson } from '../attested-pi-run.js';
3
- import { parseJsonText } from '../types.js';
4
- import { FUSION_BUDGET_POLICY, FusionBudget } from './budget.js';
5
- import { assertChildOutputWithinContract } from './output-contract.js';
6
- import {
7
- FusionArtifactStore,
8
- buildFusionFailureSummary,
9
- buildFusionRunProgress as deriveFusionRunProgress,
10
- type CreateFusionArtifactStoreOptions,
11
- type RecordFusionFailedAttemptInput,
12
- } from './artifacts.js';
13
- import {
14
- boundedEvaluationErrors,
15
- formatEvaluationErrors,
16
- parseFusionValidationCandidateReport,
17
- recoverFencedFusionValidationCandidateReport,
18
- renderValidatedFusionValidationReport,
19
- validateFusionEvaluation,
20
- validateFusionFindingAccounting,
21
- } from './evaluation.js';
22
- import { FusionChildRunError, runPiChild, type RunPiChildOptions } from './pi-child.js';
23
- import {
24
- buildBlindEvaluationInput,
25
- buildCandidatePrompt,
26
- buildEvaluationPrompt,
27
- buildEvaluationRepairPrompt,
28
- buildMergeInput,
29
- buildMergePrompt,
30
- type AnonymousFusionCandidate,
31
- } from './prompts.js';
32
- import {
33
- assertWorkflowCapability,
34
- fusionWorkflowProfile,
35
- type FusionWorkflowProfile,
36
- } from './workflows.js';
37
- import { buildFusionSourcePolicy, sourcePolicyCanonicalBytes } from './source-policy.js';
38
- import {
39
- FUSION_INPUT_SCHEMA_VERSION,
40
- FUSION_NO_TOOLS_CAPABILITY,
41
- FUSION_RESULT_SCHEMA_VERSION,
42
- FUSION_VALIDATE_CANDIDATE_SCHEMA_VERSION,
43
- FusionError,
44
- addFusionUsage,
45
- createEmptyFusionUsage,
46
- type FusionCalibrationViolation,
47
- type FusionCapability,
48
- type FusionCanonicalInputV3,
49
- type FusionCandidateId,
50
- type FusionContextOmissionLedgerV2,
51
- type FusionChildRunResult,
52
- type FusionErrorDetails,
53
- type FusionEvaluationV1,
54
- type FusionModelConfigV1,
55
- type FusionProgressEvent,
56
- type FusionRunProgress,
57
- type FusionRunResult,
58
- type FusionSource,
59
- type FusionStage,
60
- type FusionUsage,
61
- type FusionValidationFindingRecord,
62
- type ResolvedFusionModel,
63
- type ResolvedFusionModels,
64
- } from './types.js';
65
-
66
- export type FusionChildRunner = (options: RunPiChildOptions) => Promise<FusionChildRunResult>;
67
- export type FusionProgressSink = (event: FusionProgressEvent) => void;
68
- export type FusionRandomBytes = (size: number) => Buffer;
69
-
70
- export interface FusionRunReady {
71
- runId: string;
72
- artifactDir: string;
73
- artifactDirAbs: string;
74
- }
75
-
76
- type CandidateSlot = 1 | 2 | 3;
77
-
78
- export interface FusionWorkflowInput {
79
- source: FusionSource;
80
- cwd: string;
81
- sessionId?: string | undefined;
82
- canonicalInput: FusionCanonicalInputV3;
83
- canonicalInputSerialized: string;
84
- contextLedger?: FusionContextOmissionLedgerV2 | undefined;
85
- config: FusionModelConfigV1;
86
- models: ResolvedFusionModels;
87
- candidateCapability?: FusionCapability | undefined;
88
- /** Mandatory v5 workflow profile. */
89
- profile?: FusionWorkflowProfile | undefined;
90
- signal?: AbortSignal | undefined;
91
- onProgress?: FusionProgressSink | undefined;
92
- /**
93
- * Optional no-child-yet handoff. The orchestrator pauses here after durable
94
- * preflight and budget admission, allowing a background registry receipt to
95
- * become durable before candidate launch.
96
- */
97
- onReady?: ((ready: FusionRunReady) => Promise<void>) | undefined;
98
- }
99
-
100
- export interface FusionOrchestratorOptions {
101
- childRunner?: FusionChildRunner | undefined;
102
- randomBytes?: FusionRandomBytes | undefined;
103
- now?: () => Date;
104
- createArtifactStore?:
105
- | ((options: CreateFusionArtifactStoreOptions) => Promise<FusionArtifactStore>)
106
- | undefined;
107
- }
108
-
109
- interface CandidateResult {
110
- slot: CandidateSlot;
111
- result: FusionChildRunResult;
112
- }
113
-
114
- interface EvaluationAttemptResult {
115
- result: FusionChildRunResult;
116
- evaluation: FusionEvaluationV1 | undefined;
117
- errors: readonly string[];
118
- }
119
-
120
- function addFailedChildUsage(target: FusionUsage, error: unknown): void {
121
- if (error instanceof FusionChildRunError) addFusionUsage(target, error.usage);
122
- }
123
-
124
- function errorText(error: unknown): string {
125
- return error instanceof Error ? error.message : String(error);
126
- }
127
-
128
- function isRecord(value: unknown): value is Record<string, unknown> {
129
- return typeof value === 'object' && value !== null && !Array.isArray(value);
130
- }
131
-
132
- function hasOnlyKeys(value: Record<string, unknown>, allowed: readonly string[]): boolean {
133
- const allowedSet = new Set(allowed);
134
- return Object.keys(value).every((key) => allowedSet.has(key));
135
- }
136
-
137
- function isStrictCleanCanonicalInput(value: unknown): boolean {
138
- if (!isRecord(value)) return false;
139
- if (!hasOnlyKeys(value, ['schema_version', 'workflow', 'cwd', 'request', 'context']))
140
- return false;
141
- const request = value['request'];
142
- if (!isRecord(request)) return false;
143
- if (!hasOnlyKeys(request, ['source', 'authority', 'text', 'sha256'])) return false;
144
- const context = value['context'];
145
- if (!isRecord(context)) return false;
146
- if (!hasOnlyKeys(context, ['kind', 'policy_id', 'declared_sources'])) return false;
147
- if (context['kind'] !== 'clean_task') return false;
148
- const declaredSources = context['declared_sources'];
149
- if (!Array.isArray(declaredSources)) return false;
150
- for (const source of declaredSources) {
151
- if (!isRecord(source) || !hasOnlyKeys(source, ['url', 'canonical_url', 'purpose', 'sha256']))
152
- return false;
153
- }
154
- return true;
155
- }
156
-
157
- function asFusionError(error: unknown, artifactDir: string, messageOverride?: string): FusionError {
158
- if (error instanceof FusionError) {
159
- const details: FusionErrorDetails = {
160
- code: error.code,
161
- artifactDir,
162
- transient: error.transient,
163
- childCreated: error.childCreated,
164
- };
165
- if (error.stage !== undefined) details.stage = error.stage;
166
- if (error.slot !== undefined) details.slot = error.slot;
167
- if (error.attempt !== undefined) details.attempt = error.attempt;
168
- if (error.budget !== undefined) details.budget = error.budget;
169
- if (error.runProgress !== undefined) details.runProgress = error.runProgress;
170
- return new FusionError(messageOverride ?? error.message, details);
171
- }
172
- return new FusionError(messageOverride ?? errorText(error), {
173
- code: 'orchestration_failed',
174
- artifactDir,
175
- childCreated: false,
176
- });
177
- }
178
-
179
- export { buildFusionRunProgress } from './artifacts.js';
180
-
181
- function formatFusionRunStage(name: string, stage: FusionRunProgress['candidates']): string {
182
- const notStarted =
183
- stage.not_started_slots === undefined
184
- ? ''
185
- : `, ${String(stage.not_started_slots)} slot(s) not started`;
186
- return `${name}=${stage.status} (${String(stage.children_created)} created, ${String(stage.children_completed)} completed, ${String(stage.children_failed)} failed, ${String(stage.children_cancelled)} cancelled${notStarted})`;
187
- }
188
-
189
- export function summaryUnavailableNote(error: unknown): string {
190
- const detail = errorText(error);
191
- const detailBytes = Buffer.from(detail, 'utf8');
192
- if (detailBytes.length > 1024) {
193
- return 'failure-summary.json unavailable after terminal publication; write failure detail omitted because it exceeds the 1024-byte diagnostic cap.';
194
- }
195
- return `failure-summary.json unavailable after terminal publication: ${detail}`;
196
- }
197
-
198
- function withSummaryUnavailableNote(error: FusionError, summaryError: unknown): FusionError {
199
- const details: FusionErrorDetails = {
200
- code: error.code,
201
- transient: error.transient,
202
- childCreated: error.childCreated,
203
- };
204
- if (error.artifactDir !== undefined) details.artifactDir = error.artifactDir;
205
- if (error.stage !== undefined) details.stage = error.stage;
206
- if (error.slot !== undefined) details.slot = error.slot;
207
- if (error.attempt !== undefined) details.attempt = error.attempt;
208
- if (error.budget !== undefined) details.budget = error.budget;
209
- if (error.runProgress !== undefined) details.runProgress = error.runProgress;
210
- return new FusionError(`${error.message}\n${summaryUnavailableNote(summaryError)}`, details);
211
- }
212
-
213
- function formatFusionRunProgress(progress: FusionRunProgress): string {
214
- const usage = progress.usage_so_far;
215
- const optionalUsage = [
216
- usage.cacheWrite1h === undefined ? undefined : `cacheWrite1h=${String(usage.cacheWrite1h)}`,
217
- usage.reasoning === undefined ? undefined : `reasoning=${String(usage.reasoning)}`,
218
- ].filter((value): value is string => value !== undefined);
219
- const optionalText = optionalUsage.length === 0 ? '' : `, ${optionalUsage.join(', ')}`;
220
- return (
221
- `Run progress from durable attempts: ${formatFusionRunStage('candidates', progress.candidates)}; ` +
222
- `${formatFusionRunStage('evaluation', progress.evaluation)}; ` +
223
- `${formatFusionRunStage('merge', progress.merge)}. ` +
224
- `Usage so far: input=${String(usage.input)}, output=${String(usage.output)}, cacheRead=${String(usage.cacheRead)}, cacheWrite=${String(usage.cacheWrite)}${optionalText}, totalTokens=${String(usage.totalTokens)}, ` +
225
- `cost.input=${String(usage.cost.input)}, cost.output=${String(usage.cost.output)}, cost.cacheRead=${String(usage.cost.cacheRead)}, cost.cacheWrite=${String(usage.cost.cacheWrite)}, cost.total=${String(usage.cost.total)}.`
226
- );
227
- }
228
-
229
- function withRunProgress(
230
- error: unknown,
231
- artifactDir: string,
232
- progress: FusionRunProgress,
233
- ): FusionError {
234
- const base = asFusionError(error, artifactDir);
235
- const details: FusionErrorDetails = {
236
- code: base.code,
237
- artifactDir,
238
- transient: base.transient,
239
- childCreated: base.childCreated,
240
- runProgress: progress,
241
- };
242
- if (base.stage !== undefined) details.stage = base.stage;
243
- if (base.slot !== undefined) details.slot = base.slot;
244
- if (base.attempt !== undefined) details.attempt = base.attempt;
245
- if (base.budget !== undefined) details.budget = base.budget;
246
- return new FusionError(`${base.message}\n${formatFusionRunProgress(progress)}`, details);
247
- }
248
-
249
- function withTerminalArtifactFailure(
250
- error: unknown,
251
- artifactDir: string,
252
- artifactError: unknown,
253
- ): FusionError {
254
- const message = `${errorText(error)}; additionally failed to write terminal fusion artifacts: ${errorText(artifactError)}`;
255
- return asFusionError(error, artifactDir, message);
256
- }
257
-
258
- function recordFailureInput(
259
- error: unknown,
260
- stage: FusionStage,
261
- slot: CandidateSlot | undefined,
262
- attempt: number,
263
- systemPrompt: string,
264
- prompt: string,
265
- responseKind: 'md' | 'txt',
266
- ): RecordFusionFailedAttemptInput {
267
- if (error instanceof FusionChildRunError) {
268
- const base: RecordFusionFailedAttemptInput = {
269
- stage,
270
- attempt,
271
- systemPrompt,
272
- prompt,
273
- events: error.events,
274
- partialResponse: error.response,
275
- stderr: error.stderr,
276
- error: error.message,
277
- status: error.code === 'child_cancelled' ? 'cancelled' : 'failed',
278
- responseKind,
279
- childCreated: error.childCreated,
280
- usage: error.usage,
281
- ...(error.outputRecovery === undefined ? {} : { outputRecovery: error.outputRecovery }),
282
- };
283
- if (slot !== undefined) base.slot = slot;
284
- if (error.provider !== undefined) base.provider = error.provider;
285
- if (error.modelName !== undefined) base.model = error.modelName;
286
- if (error.qualifiedId !== undefined) base.qualifiedId = error.qualifiedId;
287
- return base;
288
- }
289
- const base: RecordFusionFailedAttemptInput = {
290
- stage,
291
- attempt,
292
- systemPrompt,
293
- prompt,
294
- events: Buffer.alloc(0),
295
- partialResponse: Buffer.alloc(0),
296
- stderr: Buffer.alloc(0),
297
- error: errorText(error),
298
- status:
299
- error instanceof FusionError && error.code === 'child_cancelled' ? 'cancelled' : 'failed',
300
- responseKind,
301
- childCreated: error instanceof FusionError ? error.childCreated : false,
302
- };
303
- if (slot !== undefined) base.slot = slot;
304
- return base;
305
- }
306
-
307
- function retryableSpawn(error: unknown, attempt: number): boolean {
308
- if (!(error instanceof FusionError)) return false;
309
- return (
310
- attempt === 1 && error.code === 'child_spawn_failed' && error.transient && !error.childCreated
311
- );
312
- }
313
-
314
- function childOptions(
315
- input: FusionWorkflowInput,
316
- model: ResolvedFusionModel,
317
- stage: FusionStage,
318
- attempt: number,
319
- capability: FusionCapability,
320
- systemPrompt: string,
321
- userPrompt: string,
322
- signal: AbortSignal,
323
- slot?: CandidateSlot,
324
- toolCallLogPath?: string,
325
- sourcePolicy?: { path: string; sha256: string },
326
- candidateOutputRecoveryPath?: string,
327
- ): RunPiChildOptions {
328
- const out: RunPiChildOptions = {
329
- stage,
330
- attempt,
331
- cwd: input.cwd,
332
- model,
333
- capability,
334
- systemPrompt,
335
- userPrompt,
336
- signal,
337
- };
338
- if (slot !== undefined) out.slot = slot;
339
- if (toolCallLogPath !== undefined) out.toolCallLogPath = toolCallLogPath;
340
- if (sourcePolicy !== undefined) out.sourcePolicy = sourcePolicy;
341
- if (candidateOutputRecoveryPath !== undefined)
342
- out.candidateOutputRecoveryPath = candidateOutputRecoveryPath;
343
- return out;
344
- }
345
-
346
- function parseEvaluationAttempt(
347
- text: string,
348
- expectedValidationFindings: readonly FusionValidationFindingRecord[] | undefined,
349
- ): {
350
- evaluation: FusionEvaluationV1 | undefined;
351
- errors: readonly string[];
352
- } {
353
- let parsed: unknown;
354
- try {
355
- parsed = parseJsonText(text);
356
- } catch (error) {
357
- return {
358
- evaluation: undefined,
359
- errors: [`evaluation output must be JSON only: ${errorText(error)}`],
360
- };
361
- }
362
- const result = validateFusionEvaluation(parsed);
363
- if (!result.ok) return { evaluation: undefined, errors: result.errors };
364
- if (
365
- expectedValidationFindings === undefined &&
366
- result.value.validation_accounting !== undefined
367
- ) {
368
- return {
369
- evaluation: undefined,
370
- errors: ['evaluation.validation_accounting is permitted only for fusion_validate'],
371
- };
372
- }
373
- if (expectedValidationFindings !== undefined) {
374
- const accountingErrors = validateEvaluationAccountsForSourceFindings(
375
- result.value,
376
- expectedValidationFindings,
377
- );
378
- if (accountingErrors.length > 0) return { evaluation: undefined, errors: accountingErrors };
379
- }
380
- return { evaluation: result.value, errors: [] };
381
- }
382
-
383
- function randomIndex(limit: number, randomBytes: FusionRandomBytes): number {
384
- if (!Number.isInteger(limit) || limit <= 0 || limit > 0xffffffff) {
385
- throw new FusionError(`invalid random limit ${String(limit)}`, {
386
- code: 'orchestration_failed',
387
- childCreated: false,
388
- });
389
- }
390
- const range = 0x100000000;
391
- const ceiling = range - (range % limit);
392
- for (;;) {
393
- const bytes = randomBytes(4);
394
- if (bytes.length < 4) {
395
- throw new FusionError('random byte source returned too few bytes', {
396
- code: 'orchestration_failed',
397
- childCreated: false,
398
- });
399
- }
400
- const value = bytes.readUInt32BE(0);
401
- if (value < ceiling) return value % limit;
402
- }
403
- }
404
-
405
- function shuffledSlots(randomBytes: FusionRandomBytes): CandidateSlot[] {
406
- const slots: CandidateSlot[] = [1, 2, 3];
407
- for (let i = slots.length - 1; i > 0; i--) {
408
- const j = randomIndex(i + 1, randomBytes);
409
- const left = slots[i];
410
- const right = slots[j];
411
- if (left === undefined || right === undefined) {
412
- throw new FusionError('random slot shuffle failed', {
413
- code: 'orchestration_failed',
414
- childCreated: false,
415
- });
416
- }
417
- slots[i] = right;
418
- slots[j] = left;
419
- }
420
- return slots;
421
- }
422
-
423
- function candidateBySlot(
424
- results: readonly CandidateResult[],
425
- slot: CandidateSlot,
426
- ): FusionChildRunResult {
427
- const found = results.find((candidate) => candidate.slot === slot);
428
- if (found === undefined) {
429
- throw new FusionError(`candidate slot ${String(slot)} is missing`, {
430
- code: 'orchestration_failed',
431
- childCreated: false,
432
- });
433
- }
434
- return found.result;
435
- }
436
-
437
- function candidateModel(models: ResolvedFusionModels, slot: CandidateSlot): ResolvedFusionModel {
438
- if (slot === 1) return models.candidates[0];
439
- if (slot === 2) return models.candidates[1];
440
- return models.candidates[2];
441
- }
442
-
443
- function anonymousCandidates(
444
- results: readonly CandidateResult[],
445
- slots: readonly CandidateSlot[],
446
- ): {
447
- map: Record<FusionCandidateId, CandidateSlot>;
448
- candidates: readonly [
449
- AnonymousFusionCandidate,
450
- AnonymousFusionCandidate,
451
- AnonymousFusionCandidate,
452
- ];
453
- } {
454
- const firstSlot = slots[0];
455
- const secondSlot = slots[1];
456
- const thirdSlot = slots[2];
457
- if (firstSlot === undefined || secondSlot === undefined || thirdSlot === undefined) {
458
- throw new FusionError('anonymous candidate shuffle produced too few slots', {
459
- code: 'orchestration_failed',
460
- childCreated: false,
461
- });
462
- }
463
- const first = candidateBySlot(results, firstSlot);
464
- const second = candidateBySlot(results, secondSlot);
465
- const third = candidateBySlot(results, thirdSlot);
466
- return {
467
- map: { A: firstSlot, B: secondSlot, C: thirdSlot },
468
- candidates: [
469
- { candidate_id: 'A', response: first.text },
470
- { candidate_id: 'B', response: second.text },
471
- { candidate_id: 'C', response: third.text },
472
- ],
473
- };
474
- }
475
-
476
- interface ValidationSourceData {
477
- candidates: readonly [
478
- AnonymousFusionCandidate,
479
- AnonymousFusionCandidate,
480
- AnonymousFusionCandidate,
481
- ];
482
- findings: readonly FusionValidationFindingRecord[];
483
- verified: readonly string[];
484
- limitations: readonly string[];
485
- }
486
-
487
- function sha256Text(value: string): string {
488
- return createHash('sha256').update(value, 'utf8').digest('hex');
489
- }
490
-
491
- function boundedContractError(error: unknown): string {
492
- const value = errorText(error);
493
- return value.length <= 1_000 ? value : `${value.slice(0, 999)}…`;
494
- }
495
-
496
- /**
497
- * Enforce the validation-candidate contract without making the shared JSON
498
- * parser permissive. A single, tightly recognized fenced response is recovered
499
- * with a durable warning. One irrecoverable minority report is represented as
500
- * an explicit limitation; two or more still fail the workflow loudly.
501
- */
502
- async function prepareValidationSourceData(
503
- candidates: readonly [
504
- AnonymousFusionCandidate,
505
- AnonymousFusionCandidate,
506
- AnonymousFusionCandidate,
507
- ],
508
- anonymousMap: Record<FusionCandidateId, CandidateSlot>,
509
- store: FusionArtifactStore,
510
- ): Promise<ValidationSourceData> {
511
- const prepared = candidates.map((candidate) => ({ ...candidate })) as [
512
- AnonymousFusionCandidate,
513
- AnonymousFusionCandidate,
514
- AnonymousFusionCandidate,
515
- ];
516
- const findings: FusionValidationFindingRecord[] = [];
517
- const verified: string[] = [];
518
- const limitations: string[] = [];
519
- let normalizationCount = 0;
520
- const failures: Array<{ candidate: AnonymousFusionCandidate; error: string }> = [];
521
-
522
- for (const candidate of prepared) {
523
- try {
524
- const report = parseFusionValidationCandidateReport(
525
- candidate.response,
526
- candidate.candidate_id,
527
- );
528
- findings.push(...report.findings);
529
- verified.push(...report.verified);
530
- limitations.push(...report.limitations);
531
- continue;
532
- } catch (strictError) {
533
- try {
534
- const recovered = recoverFencedFusionValidationCandidateReport(
535
- candidate.response,
536
- candidate.candidate_id,
537
- );
538
- if (recovered === undefined) throw strictError;
539
- await store.recordValidationCandidateContractEvent({
540
- candidateId: candidate.candidate_id,
541
- slot: anonymousMap[candidate.candidate_id],
542
- status: 'normalized',
543
- detail: {
544
- normalization: recovered.normalization,
545
- original_sha256: sha256Text(candidate.response),
546
- forwarded_sha256: sha256Text(recovered.response),
547
- warning:
548
- 'Candidate output violated the bare-JSON contract; a single complete JSON fence was removed and recorded.',
549
- },
550
- });
551
- candidate.response = recovered.response;
552
- findings.push(...recovered.report.findings);
553
- verified.push(...recovered.report.verified);
554
- limitations.push(...recovered.report.limitations);
555
- normalizationCount += 1;
556
- continue;
557
- } catch (recoveryError) {
558
- failures.push({
559
- candidate,
560
- error: boundedContractError(recoveryError === strictError ? strictError : recoveryError),
561
- });
562
- }
563
- }
564
- }
565
-
566
- if (normalizationCount > 0) {
567
- limitations.push(
568
- `${String(normalizationCount)} validation report${normalizationCount === 1 ? '' : 's'} required audited removal of a Markdown JSON wrapper; JSON content was unchanged.`,
569
- );
570
- }
571
-
572
- for (const failure of failures) {
573
- await store.recordValidationCandidateContractEvent({
574
- candidateId: failure.candidate.candidate_id,
575
- slot: anonymousMap[failure.candidate.candidate_id],
576
- status: 'dropped',
577
- detail: {
578
- response_sha256: sha256Text(failure.candidate.response),
579
- error: failure.error,
580
- warning: 'Candidate output could not be parsed under the strict or fenced-JSON contract.',
581
- },
582
- });
583
- }
584
- if (failures.length > 1) {
585
- throw new FusionError(
586
- `fusion_validate cannot continue: ${String(failures.length)} of 3 candidate reports violated the structured-output contract`,
587
- { code: 'evaluation_invalid', stage: 'candidate' },
588
- );
589
- }
590
- const failure = failures[0];
591
- if (failure !== undefined) {
592
- const synthetic = canonicalJson({
593
- schema_version: FUSION_VALIDATE_CANDIDATE_SCHEMA_VERSION,
594
- findings: [],
595
- verified: [],
596
- limitations: [
597
- 'This validation report could not be parsed after strict contract checks; no findings or verification claims from it were included.',
598
- ],
599
- });
600
- failure.candidate.response = synthetic;
601
- const report = parseFusionValidationCandidateReport(synthetic, failure.candidate.candidate_id);
602
- limitations.push(...report.limitations);
603
- }
604
-
605
- return { candidates: prepared, findings, verified, limitations };
606
- }
607
-
608
- function validateEvaluationAccountsForSourceFindings(
609
- evaluation: FusionEvaluationV1,
610
- sourceFindings: readonly FusionValidationFindingRecord[],
611
- ): readonly string[] {
612
- const errors: string[] = [];
613
- const accounting = evaluation.validation_accounting;
614
- if (accounting === undefined) {
615
- return ['validation evaluator output must include validation_accounting'];
616
- }
617
- const expected = sourceFindings.map((finding) => canonicalJson(finding)).sort();
618
- const actual = accounting.findings.map((finding) => canonicalJson(finding)).sort();
619
- if (
620
- expected.length !== actual.length ||
621
- expected.some((value, index) => value !== actual[index])
622
- ) {
623
- errors.push(
624
- 'validation evaluator validation_accounting.findings must exactly equal host-assigned source findings',
625
- );
626
- }
627
- errors.push(...validateFusionFindingAccounting(accounting));
628
- return errors;
629
- }
630
-
631
- function resolveRunProfile(input: FusionWorkflowInput): FusionWorkflowProfile {
632
- if (input.profile !== undefined) return fusionWorkflowProfile(input.profile.id);
633
- const workflow = input.canonicalInput.workflow;
634
- const contextKind = input.canonicalInput.context?.kind;
635
- if (workflow !== undefined && workflow !== 'reason') {
636
- throw new FusionError(`fusion workflow profile is required for ${workflow} runs`, {
637
- code: 'orchestration_failed',
638
- childCreated: false,
639
- });
640
- }
641
- if (contextKind === 'clean_task') {
642
- throw new FusionError('fusion workflow profile is required for clean-task runs', {
643
- code: 'orchestration_failed',
644
- childCreated: false,
645
- });
646
- }
647
- return fusionWorkflowProfile('reason');
648
- }
649
-
650
- export class FusionOrchestrator {
651
- private readonly childRunner: FusionChildRunner;
652
- private readonly randomBytes: FusionRandomBytes;
653
- private readonly now: (() => Date) | undefined;
654
- private readonly createArtifactStore: (
655
- options: CreateFusionArtifactStoreOptions,
656
- ) => Promise<FusionArtifactStore>;
657
-
658
- constructor(options: FusionOrchestratorOptions = {}) {
659
- this.childRunner = options.childRunner ?? runPiChild;
660
- this.randomBytes = options.randomBytes ?? nodeRandomBytes;
661
- this.now = options.now;
662
- this.createArtifactStore = options.createArtifactStore ?? FusionArtifactStore.create;
663
- }
664
-
665
- async run(input: FusionWorkflowInput): Promise<FusionRunResult> {
666
- if (input.canonicalInput.schema_version !== FUSION_INPUT_SCHEMA_VERSION) {
667
- throw new FusionError('fusion orchestrator accepts only v5 canonical input', {
668
- code: 'orchestration_failed',
669
- childCreated: false,
670
- });
671
- }
672
- const profile = resolveRunProfile(input);
673
- const inputWorkflow = input.canonicalInput.workflow ?? profile.id;
674
- const inputContextKind = input.canonicalInput.context?.kind ?? 'session_projection';
675
- if (inputWorkflow !== profile.id || inputContextKind !== profile.contextKind) {
676
- throw new FusionError(
677
- `fusion workflow profile ${profile.id} is incompatible with canonical input workflow=${String(inputWorkflow)} context=${String(inputContextKind)}`,
678
- { code: 'orchestration_failed', childCreated: false },
679
- );
680
- }
681
- if (
682
- profile.contextKind === 'clean_task' &&
683
- !isStrictCleanCanonicalInput(input.canonicalInput)
684
- ) {
685
- throw new FusionError(
686
- 'clean-task fusion input must not carry parent context fields and must match the strict clean canonical shape',
687
- {
688
- code: 'orchestration_failed',
689
- childCreated: false,
690
- },
691
- );
692
- }
693
- const candidateCapability = assertWorkflowCapability(profile, input.candidateCapability);
694
- const storeOptions: CreateFusionArtifactStoreOptions = {
695
- cwd: input.cwd,
696
- profile,
697
- source: input.source,
698
- config: input.config,
699
- models: input.models,
700
- capabilities: {
701
- candidate: candidateCapability,
702
- evaluation: FUSION_NO_TOOLS_CAPABILITY,
703
- merge: FUSION_NO_TOOLS_CAPABILITY,
704
- },
705
- };
706
- if (input.sessionId !== undefined) storeOptions.sessionId = input.sessionId;
707
- if (this.now !== undefined) storeOptions.now = this.now;
708
- let serializedParsed: unknown;
709
- try {
710
- serializedParsed = parseJsonText(input.canonicalInputSerialized);
711
- } catch (error) {
712
- throw new FusionError(
713
- `fusion canonical input artifact is not valid JSON: ${errorText(error)}`,
714
- {
715
- code: 'orchestration_failed',
716
- childCreated: false,
717
- },
718
- );
719
- }
720
- if (canonicalJson(serializedParsed) !== canonicalJson(input.canonicalInput)) {
721
- throw new FusionError(
722
- 'fusion canonical input serialized bytes do not match canonical input object',
723
- {
724
- code: 'orchestration_failed',
725
- childCreated: false,
726
- },
727
- );
728
- }
729
- const store = await this.createArtifactStore(storeOptions);
730
- input.onProgress?.({ type: 'state', state: 'initializing' });
731
- const usage = createEmptyFusionUsage();
732
- const calibrationWarnings: FusionCalibrationViolation[] = [];
733
- try {
734
- await store.writeCanonicalInput(input.canonicalInputSerialized);
735
- if (inputContextKind === 'session_projection') {
736
- if (input.contextLedger === undefined) {
737
- throw new FusionError(
738
- 'session-projection fusion input requires an omission ledger artifact',
739
- {
740
- code: 'orchestration_failed',
741
- childCreated: false,
742
- },
743
- );
744
- }
745
- await store.writeContextLedger(input.contextLedger);
746
- } else if (input.contextLedger !== undefined) {
747
- throw new FusionError('clean-task fusion input must not carry a parent omission ledger', {
748
- code: 'orchestration_failed',
749
- childCreated: false,
750
- });
751
- }
752
- if (profile.id === 'research') {
753
- const cleanContext = input.canonicalInput.context;
754
- if (cleanContext?.kind !== 'clean_task') {
755
- throw new FusionError('research workflow requires a clean-task canonical input', {
756
- code: 'orchestration_failed',
757
- childCreated: false,
758
- });
759
- }
760
- const policy = buildFusionSourcePolicy(input.cwd, cleanContext.declared_sources);
761
- await store.writeSourcePolicy(sourcePolicyCanonicalBytes(policy));
762
- }
763
- // Deterministic size accounting for the whole workflow, performed before
764
- // a single child process exists. A rejection here launches zero children.
765
- const budget = new FusionBudget(
766
- input.models,
767
- input.canonicalInput.context?.policy_id ?? 'fusion-session-projection-v1',
768
- candidateCapability,
769
- profile,
770
- );
771
- const budgetPlan = budget.plan(input.canonicalInput);
772
- await store.writeBudgetPlan(budgetPlan);
773
- budget.assertPlanFits(budgetPlan, store.artifactDir);
774
- if (budgetPlan.warnings.length > 0) {
775
- input.onProgress?.({
776
- type: 'budget_warning',
777
- warnings: budgetPlan.warnings,
778
- error: 'fusion budget utilization warning',
779
- });
780
- }
781
- await input.onReady?.({
782
- runId: store.runId,
783
- artifactDir: store.artifactDir,
784
- artifactDirAbs: store.artifactDirAbs,
785
- });
786
- if (input.signal?.aborted === true) {
787
- throw new FusionError('fusion run cancelled before launch', {
788
- code: 'child_cancelled',
789
- childCreated: false,
790
- });
791
- }
792
- await store.transition('candidates_running');
793
- input.onProgress?.({ type: 'state', state: 'candidates_running' });
794
- const candidateResults = await this.runCandidates(
795
- input,
796
- store,
797
- usage,
798
- budget,
799
- calibrationWarnings,
800
- profile,
801
- candidateCapability,
802
- );
803
- await store.transition('candidates_complete');
804
- input.onProgress?.({ type: 'state', state: 'candidates_complete' });
805
-
806
- const shuffled = anonymousCandidates(candidateResults, shuffledSlots(this.randomBytes));
807
- // Persist the blind mapping before workflow-specific contract parsing
808
- // so a failed validation remains attributable to its durable slot artifact.
809
- await store.setAnonymousMap(shuffled.map);
810
- const validationData =
811
- profile.id === 'validate'
812
- ? await prepareValidationSourceData(shuffled.candidates, shuffled.map, store)
813
- : undefined;
814
- const evaluationCandidates = validationData?.candidates ?? shuffled.candidates;
815
- const blindInput = buildBlindEvaluationInput(
816
- input.canonicalInput,
817
- evaluationCandidates,
818
- validationData?.findings,
819
- );
820
- await store.writeBlindCandidates(buildEvaluationPrompt(blindInput));
821
-
822
- await store.transition('evaluating');
823
- input.onProgress?.({ type: 'state', state: 'evaluating' });
824
- const evaluation = await this.runEvaluation(
825
- input,
826
- store,
827
- usage,
828
- blindInput,
829
- budget,
830
- calibrationWarnings,
831
- profile,
832
- validationData?.findings,
833
- );
834
- await store.writeEvaluationJson(evaluation);
835
- await store.transition('evaluation_complete');
836
- input.onProgress?.({ type: 'state', state: 'evaluation_complete' });
837
-
838
- await store.transition('merging');
839
- input.onProgress?.({ type: 'state', state: 'merging' });
840
- const mergeInput = buildMergeInput(input.canonicalInput, evaluationCandidates, evaluation);
841
- const mergePrompt = buildMergePrompt(mergeInput);
842
- budget.assertStagePrompt('merge', profile.mergerSystemPrompt, mergePrompt);
843
- input.onProgress?.({ type: 'merge_started' });
844
- const merged = await this.runChildWithRetry(
845
- input,
846
- store,
847
- usage,
848
- input.models.merger,
849
- 'merge',
850
- profile.mergerSystemPrompt,
851
- mergePrompt,
852
- input.signal ?? new AbortController().signal,
853
- // Stage policy, not caller input: evaluator and merger are always reasoning-only.
854
- FUSION_NO_TOOLS_CAPABILITY,
855
- undefined,
856
- 'md',
857
- );
858
- addFusionUsage(usage, merged.usage);
859
- await store.recordChildAttempt({
860
- result: merged,
861
- systemPrompt: profile.mergerSystemPrompt,
862
- prompt: mergePrompt,
863
- responseKind: 'md',
864
- });
865
- await this.recordCalibrationObservation(
866
- input,
867
- store,
868
- budget,
869
- calibrationWarnings,
870
- 'merge',
871
- profile.mergerSystemPrompt,
872
- mergePrompt,
873
- merged,
874
- );
875
- assertChildOutputWithinContract('merge', merged.text);
876
- let finalMergedText = merged.text;
877
- if (profile.id === 'validate') {
878
- const accounting = evaluation.validation_accounting;
879
- if (accounting === undefined) {
880
- throw new FusionError(
881
- 'fusion_validate evaluation completed without validation accounting',
882
- {
883
- code: 'evaluation_invalid',
884
- stage: 'merge',
885
- },
886
- );
887
- }
888
- finalMergedText = renderValidatedFusionValidationReport(accounting, validationData);
889
- }
890
- if (finalMergedText !== merged.text)
891
- assertChildOutputWithinContract('merge', finalMergedText);
892
- const mergedRef = await store.writeMerged(finalMergedText);
893
- await store.setUsage(usage);
894
- const details: FusionRunResult['details'] = {
895
- schema_version: FUSION_RESULT_SCHEMA_VERSION,
896
- run_id: store.runId,
897
- workflow: profile.id,
898
- source: input.source,
899
- status: 'completed',
900
- artifact_dir: store.artifactDir,
901
- context: {
902
- kind: inputContextKind,
903
- policy_id: input.canonicalInput.context?.policy_id ?? 'fusion-session-projection-v1',
904
- },
905
- tool_policy: {
906
- candidate_tools: profile.candidateTools,
907
- evaluation_tools: [],
908
- merge_tools: [],
909
- },
910
- models: store.snapshot().models,
911
- evaluator_attempts: store
912
- .snapshot()
913
- .attempts.filter((attempt) => attempt.stage === 'evaluation').length,
914
- usage,
915
- budget: {
916
- policy_id: FUSION_BUDGET_POLICY.id,
917
- calibration_version: budgetPlan.policy.calibration_version,
918
- route_table: budget.routes,
919
- rate_sources: budget.resultRateSources,
920
- unknown_provider_warnings: budget.unknownProviderWarnings,
921
- calibration_warnings: calibrationWarnings,
922
- },
923
- };
924
- await store.writeCommittedResult(mergedRef, details);
925
- await store.transition('completed');
926
- input.onProgress?.({ type: 'completed', runId: store.runId, artifactDir: store.artifactDir });
927
- return { mergedText: finalMergedText, details };
928
- } catch (error) {
929
- const cancelled =
930
- input.signal?.aborted === true ||
931
- (error instanceof FusionError && error.code === 'child_cancelled');
932
- let terminalError: FusionError;
933
- try {
934
- await store.setUsage(usage);
935
- terminalError = withRunProgress(
936
- error,
937
- store.artifactDir,
938
- deriveFusionRunProgress(store.snapshot()),
939
- );
940
- const terminalState = cancelled ? 'cancelled' : 'failed';
941
- await store.writeError(terminalState, terminalError.message);
942
- // The terminal manifest/error are authoritative. Summary persistence is
943
- // subordinate and intentionally attempted once from that fresh snapshot.
944
- try {
945
- const terminalManifest = store.snapshot();
946
- await store.writeFailureSummary(
947
- buildFusionFailureSummary({
948
- manifest: terminalManifest,
949
- terminalError,
950
- progress: deriveFusionRunProgress(terminalManifest),
951
- terminalState,
952
- createdAt: terminalManifest.updated_at,
953
- }),
954
- );
955
- } catch (summaryError) {
956
- terminalError = withSummaryUnavailableNote(terminalError, summaryError);
957
- }
958
- } catch (artifactError) {
959
- throw withTerminalArtifactFailure(error, store.artifactDir, artifactError);
960
- }
961
- if (cancelled) {
962
- input.onProgress?.({
963
- type: 'cancelled',
964
- runId: store.runId,
965
- artifactDir: store.artifactDir,
966
- reason: terminalError.message,
967
- });
968
- } else {
969
- input.onProgress?.({
970
- type: 'failed',
971
- runId: store.runId,
972
- artifactDir: store.artifactDir,
973
- error: terminalError.message,
974
- });
975
- }
976
- throw terminalError;
977
- }
978
- }
979
-
980
- private async runCandidates(
981
- input: FusionWorkflowInput,
982
- store: FusionArtifactStore,
983
- usage: FusionUsage,
984
- budget: FusionBudget,
985
- calibrationWarnings: FusionCalibrationViolation[],
986
- profile: FusionWorkflowProfile,
987
- candidateCapability: FusionCapability,
988
- ): Promise<readonly CandidateResult[]> {
989
- const controller = new AbortController();
990
- const abortListener = () => controller.abort();
991
- input.signal?.addEventListener('abort', abortListener, { once: true });
992
- if (input.signal?.aborted) controller.abort();
993
- const systemPrompt = profile.candidateSystemPrompt(candidateCapability);
994
- const prompt = buildCandidatePrompt(input.canonicalInput);
995
- for (const slot of [1, 2, 3] as const) {
996
- budget.assertStagePrompt('candidate', systemPrompt, prompt, slot);
997
- }
998
- let primaryError: unknown;
999
- let completed = 0;
1000
- try {
1001
- if (controller.signal.aborted) {
1002
- throw new FusionError('fusion candidate wave cancelled before launch', {
1003
- code: 'child_cancelled',
1004
- stage: 'candidate',
1005
- childCreated: false,
1006
- });
1007
- }
1008
- const tasks: Array<Promise<CandidateResult>> = ([1, 2, 3] as const).map((slot) => {
1009
- const model = candidateModel(input.models, slot);
1010
- const task = this.runChildWithRetry(
1011
- input,
1012
- store,
1013
- usage,
1014
- model,
1015
- 'candidate',
1016
- systemPrompt,
1017
- prompt,
1018
- controller.signal,
1019
- candidateCapability,
1020
- slot,
1021
- profile.id === 'validate' ? 'txt' : 'md',
1022
- ).then(async (result) => {
1023
- await store.recordChildAttempt({
1024
- result,
1025
- systemPrompt,
1026
- prompt,
1027
- responseKind: profile.id === 'validate' ? 'txt' : 'md',
1028
- });
1029
- await this.recordCalibrationObservation(
1030
- input,
1031
- store,
1032
- budget,
1033
- calibrationWarnings,
1034
- 'candidate',
1035
- systemPrompt,
1036
- prompt,
1037
- result,
1038
- slot,
1039
- );
1040
- // The response and its consumed usage are durable before the contract
1041
- // check, so an oversized answer is preserved and accounted rather than lost.
1042
- addFusionUsage(usage, result.usage);
1043
- await store.setUsage(usage);
1044
- assertChildOutputWithinContract('candidate', result.text);
1045
- completed += 1;
1046
- input.onProgress?.({ type: 'candidate_completed', slot, completed, total: 3 });
1047
- return { slot, result };
1048
- });
1049
- return task.catch((error: unknown) => {
1050
- if (primaryError === undefined) {
1051
- primaryError = error;
1052
- controller.abort();
1053
- }
1054
- throw error;
1055
- });
1056
- });
1057
- const settled = await Promise.allSettled(tasks);
1058
- if (primaryError !== undefined) throw primaryError;
1059
- const results: CandidateResult[] = [];
1060
- for (const item of settled) {
1061
- if (item.status === 'fulfilled') results.push(item.value);
1062
- else throw item.reason;
1063
- }
1064
- return results.sort((left, right) => left.slot - right.slot);
1065
- } finally {
1066
- input.signal?.removeEventListener('abort', abortListener);
1067
- }
1068
- }
1069
-
1070
- private async runEvaluation(
1071
- input: FusionWorkflowInput,
1072
- store: FusionArtifactStore,
1073
- usage: FusionUsage,
1074
- blindInput: Parameters<typeof buildEvaluationPrompt>[0],
1075
- budget: FusionBudget,
1076
- calibrationWarnings: FusionCalibrationViolation[],
1077
- profile: FusionWorkflowProfile,
1078
- expectedValidationFindings: readonly FusionValidationFindingRecord[] | undefined,
1079
- ): Promise<FusionEvaluationV1> {
1080
- const firstPrompt = buildEvaluationPrompt(blindInput);
1081
- budget.assertStagePrompt('evaluation', profile.evaluatorSystemPrompt, firstPrompt);
1082
- const first = await this.runEvaluationAttempt(
1083
- input,
1084
- store,
1085
- usage,
1086
- budget,
1087
- calibrationWarnings,
1088
- firstPrompt,
1089
- 1,
1090
- false,
1091
- profile,
1092
- expectedValidationFindings,
1093
- );
1094
- if (first.evaluation !== undefined) return first.evaluation;
1095
- const errors = boundedEvaluationErrors(first.errors);
1096
- input.onProgress?.({ type: 'evaluation_retry', errors });
1097
- const repairPrompt = buildEvaluationRepairPrompt({
1098
- schema_version: 'pi-background-tasks.fusion-evaluation-repair-input.v1',
1099
- original_blind_input: blindInput,
1100
- invalid_output: first.result.text,
1101
- validation_errors: errors,
1102
- });
1103
- budget.assertStagePrompt(
1104
- 'evaluation_repair',
1105
- profile.evaluationRepairSystemPrompt,
1106
- repairPrompt,
1107
- );
1108
- const second = await this.runEvaluationAttempt(
1109
- input,
1110
- store,
1111
- usage,
1112
- budget,
1113
- calibrationWarnings,
1114
- repairPrompt,
1115
- 2,
1116
- true,
1117
- profile,
1118
- expectedValidationFindings,
1119
- );
1120
- if (second.evaluation !== undefined) return second.evaluation;
1121
- throw new FusionError(
1122
- `evaluation schema repair failed: ${formatEvaluationErrors(second.errors)}`,
1123
- {
1124
- code: 'evaluation_invalid',
1125
- stage: 'evaluation',
1126
- attempt: 2,
1127
- },
1128
- );
1129
- }
1130
-
1131
- private async runEvaluationAttempt(
1132
- input: FusionWorkflowInput,
1133
- store: FusionArtifactStore,
1134
- usage: FusionUsage,
1135
- budget: FusionBudget,
1136
- calibrationWarnings: FusionCalibrationViolation[],
1137
- prompt: string,
1138
- attempt: 1 | 2,
1139
- repair: boolean,
1140
- profile: FusionWorkflowProfile,
1141
- expectedValidationFindings: readonly FusionValidationFindingRecord[] | undefined,
1142
- ): Promise<EvaluationAttemptResult> {
1143
- input.onProgress?.({ type: 'evaluation_started', attempt, repair });
1144
- const systemPrompt = repair
1145
- ? profile.evaluationRepairSystemPrompt
1146
- : profile.evaluatorSystemPrompt;
1147
- const result = await this.runChildWithRetry(
1148
- input,
1149
- store,
1150
- usage,
1151
- input.models.evaluator,
1152
- 'evaluation',
1153
- systemPrompt,
1154
- prompt,
1155
- input.signal ?? new AbortController().signal,
1156
- // Stage policy, not caller input: evaluator and merger are always reasoning-only.
1157
- FUSION_NO_TOOLS_CAPABILITY,
1158
- undefined,
1159
- 'txt',
1160
- attempt,
1161
- );
1162
- addFusionUsage(usage, result.usage);
1163
- await store.recordChildAttempt({ result, systemPrompt, prompt, responseKind: 'txt' });
1164
- await this.recordCalibrationObservation(
1165
- input,
1166
- store,
1167
- budget,
1168
- calibrationWarnings,
1169
- 'evaluation',
1170
- systemPrompt,
1171
- prompt,
1172
- result,
1173
- );
1174
- await store.setUsage(usage);
1175
- // Bound the evaluator output before it can be embedded in a repair prompt.
1176
- assertChildOutputWithinContract('evaluation', result.text);
1177
- const parsed = parseEvaluationAttempt(result.text, expectedValidationFindings);
1178
- return { result, evaluation: parsed.evaluation, errors: parsed.errors };
1179
- }
1180
-
1181
- private async recordCalibrationObservation(
1182
- input: FusionWorkflowInput,
1183
- store: FusionArtifactStore,
1184
- budget: FusionBudget,
1185
- calibrationWarnings: FusionCalibrationViolation[],
1186
- stage: FusionStage,
1187
- systemPrompt: string,
1188
- userPrompt: string,
1189
- result: FusionChildRunResult,
1190
- slot?: CandidateSlot,
1191
- ): Promise<void> {
1192
- const violation = budget.calibrationViolationForCompletedChild(
1193
- stage,
1194
- systemPrompt,
1195
- userPrompt,
1196
- result,
1197
- slot,
1198
- );
1199
- if (violation === undefined) return;
1200
- calibrationWarnings.push(violation);
1201
- let artifact = 'calibration-violation artifact was not written';
1202
- try {
1203
- const ref = await store.recordCalibrationViolation({
1204
- stage,
1205
- attempt: result.attempt,
1206
- violation,
1207
- ...(slot === undefined ? {} : { slot }),
1208
- });
1209
- artifact = ref.path;
1210
- } catch (error) {
1211
- artifact = `calibration-violation artifact write failed: ${errorText(error)}`;
1212
- }
1213
- input.onProgress?.({ type: 'calibration_warning', warning: violation, artifact });
1214
- }
1215
-
1216
- private async runChildWithRetry(
1217
- input: FusionWorkflowInput,
1218
- store: FusionArtifactStore,
1219
- usage: FusionUsage,
1220
- model: ResolvedFusionModel,
1221
- stage: FusionStage,
1222
- systemPrompt: string,
1223
- userPrompt: string,
1224
- signal: AbortSignal,
1225
- capability: FusionCapability,
1226
- slot: CandidateSlot | undefined,
1227
- responseKind: 'md' | 'txt',
1228
- fixedAttempt?: 1 | 2,
1229
- ): Promise<FusionChildRunResult> {
1230
- const logicalAttempt = fixedAttempt ?? 1;
1231
- for (let launchTry = 1; launchTry <= 2; launchTry++) {
1232
- if (stage === 'candidate' && slot !== undefined) {
1233
- input.onProgress?.({ type: 'candidate_started', slot, attempt: logicalAttempt });
1234
- }
1235
- const toolCallLogPath =
1236
- capability !== 'reason'
1237
- ? store.childToolCallLogPath(stage, slot, logicalAttempt)
1238
- : undefined;
1239
- const sourcePolicy =
1240
- capability === 'research' ? store.sourcePolicyLaunchReference() : undefined;
1241
- const candidateOutputRecoveryPath =
1242
- stage === 'candidate' && slot !== undefined
1243
- ? store.childOutputRecoveryPath(slot, logicalAttempt, responseKind)
1244
- : undefined;
1245
- try {
1246
- return await this.childRunner(
1247
- childOptions(
1248
- input,
1249
- model,
1250
- stage,
1251
- logicalAttempt,
1252
- capability,
1253
- systemPrompt,
1254
- userPrompt,
1255
- signal,
1256
- slot,
1257
- toolCallLogPath,
1258
- sourcePolicy,
1259
- candidateOutputRecoveryPath,
1260
- ),
1261
- );
1262
- } catch (error) {
1263
- if (!signal.aborted && retryableSpawn(error, launchTry) && launchTry === 1) continue;
1264
- addFailedChildUsage(usage, error);
1265
- await store.recordFailedAttempt(
1266
- recordFailureInput(
1267
- error,
1268
- stage,
1269
- slot,
1270
- logicalAttempt,
1271
- systemPrompt,
1272
- userPrompt,
1273
- responseKind,
1274
- ),
1275
- );
1276
- await store.setUsage(usage);
1277
- throw error;
1278
- }
1279
- }
1280
- const details: FusionErrorDetails = {
1281
- code: 'orchestration_failed',
1282
- stage,
1283
- childCreated: false,
1284
- };
1285
- if (slot !== undefined) details.slot = slot;
1286
- throw new FusionError(`${stage} child did not produce a result`, details);
1287
- }
1288
- }