pi-background-tasks 1.0.6 → 2.0.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 (49) hide show
  1. package/README.md +7 -7
  2. package/TESTING.md +3 -3
  3. package/TEST_PLAN.md +2 -2
  4. package/docs/INDEX.md +25 -25
  5. package/docs/choose-a-workflow.md +4 -4
  6. package/docs/commands/bg-clear.md +1 -1
  7. package/docs/commands/bg-update.md +1 -1
  8. package/docs/commands/bg.md +1 -1
  9. package/docs/commands/fusion-models.md +1 -1
  10. package/docs/commands/fusion.md +5 -8
  11. package/docs/commands/jobs.md +1 -1
  12. package/docs/commands/kill.md +1 -1
  13. package/docs/commands/logs.md +1 -1
  14. package/docs/commands/task-manager.md +2 -2
  15. package/docs/concepts/completion-delivery.md +1 -0
  16. package/docs/getting-started.md +1 -1
  17. package/docs/manifest.json +59 -50
  18. package/docs/read-before-edit.md +1 -0
  19. package/docs/reference/runtime-contracts.md +55 -51
  20. package/docs/reference/shortcuts-and-dock.md +2 -2
  21. package/docs/subsystems/background-task-runtime.md +7 -1
  22. package/docs/subsystems/docs-freshness-gate.md +4 -4
  23. package/docs/subsystems/fusion.md +15 -11
  24. package/docs/subsystems/host-ui-and-telemetry.md +1 -1
  25. package/docs/tools/bg_delegate.md +1 -1
  26. package/docs/tools/bg_kill.md +1 -1
  27. package/docs/tools/bg_logs.md +1 -1
  28. package/docs/tools/bg_result.md +14 -10
  29. package/docs/tools/bg_run.md +1 -1
  30. package/docs/tools/bg_run_pi_attested.md +1 -1
  31. package/docs/tools/bg_status.md +1 -1
  32. package/docs/tools/fusion_investigate.md +6 -4
  33. package/docs/tools/fusion_reason.md +5 -5
  34. package/docs/tools/fusion_research.md +6 -2
  35. package/docs/tools/fusion_validate.md +5 -3
  36. package/package.json +1 -1
  37. package/src/core/common.ts +50 -2
  38. package/src/core/fusion/artifacts.ts +106 -13
  39. package/src/core/fusion/budget.ts +12 -4
  40. package/src/core/fusion/evaluation.ts +61 -0
  41. package/src/core/fusion/orchestrator.ts +270 -73
  42. package/src/core/fusion/pi-child.ts +6 -0
  43. package/src/core/fusion/prompts.ts +1 -0
  44. package/src/core/fusion/result-package.ts +385 -0
  45. package/src/core/fusion/types.ts +19 -1
  46. package/src/core/registry.ts +187 -20
  47. package/src/delegate-extension.ts +130 -24
  48. package/src/extension.ts +17 -6
  49. package/src/fusion-extension.ts +308 -154
@@ -1,4 +1,4 @@
1
- import { randomBytes as nodeRandomBytes } from 'node:crypto';
1
+ import { createHash, randomBytes as nodeRandomBytes } from 'node:crypto';
2
2
  import { canonicalJson } from '../attested-pi-run.js';
3
3
  import { parseJsonText } from '../common.js';
4
4
  import { FUSION_BUDGET_POLICY, FusionBudget, assertChildOutputWithinContract } from './budget.js';
@@ -11,6 +11,7 @@ import {
11
11
  boundedEvaluationErrors,
12
12
  formatEvaluationErrors,
13
13
  parseFusionValidationCandidateReport,
14
+ recoverFencedFusionValidationCandidateReport,
14
15
  renderValidatedFusionValidationReport,
15
16
  validateFusionEvaluation,
16
17
  validateFusionFindingAccounting,
@@ -35,6 +36,7 @@ import {
35
36
  FUSION_INPUT_SCHEMA_VERSION,
36
37
  FUSION_NO_TOOLS_CAPABILITY,
37
38
  FUSION_RESULT_SCHEMA_VERSION,
39
+ FUSION_VALIDATE_CANDIDATE_SCHEMA_VERSION,
38
40
  FusionError,
39
41
  addFusionUsage,
40
42
  createEmptyFusionUsage,
@@ -61,6 +63,12 @@ export type FusionChildRunner = (options: RunPiChildOptions) => Promise<FusionCh
61
63
  export type FusionProgressSink = (event: FusionProgressEvent) => void;
62
64
  export type FusionRandomBytes = (size: number) => Buffer;
63
65
 
66
+ export interface FusionRunReady {
67
+ runId: string;
68
+ artifactDir: string;
69
+ artifactDirAbs: string;
70
+ }
71
+
64
72
  type CandidateSlot = 1 | 2 | 3;
65
73
 
66
74
  export interface FusionWorkflowInput {
@@ -77,6 +85,12 @@ export interface FusionWorkflowInput {
77
85
  profile?: FusionWorkflowProfile | undefined;
78
86
  signal?: AbortSignal | undefined;
79
87
  onProgress?: FusionProgressSink | undefined;
88
+ /**
89
+ * Optional no-child-yet handoff. The orchestrator pauses here after durable
90
+ * preflight and budget admission, allowing a background registry receipt to
91
+ * become durable before candidate launch.
92
+ */
93
+ onReady?: ((ready: FusionRunReady) => Promise<void>) | undefined;
80
94
  }
81
95
 
82
96
  export interface FusionOrchestratorOptions {
@@ -118,7 +132,8 @@ function hasOnlyKeys(value: Record<string, unknown>, allowed: readonly string[])
118
132
 
119
133
  function isStrictCleanCanonicalInput(value: unknown): boolean {
120
134
  if (!isRecord(value)) return false;
121
- if (!hasOnlyKeys(value, ['schema_version', 'workflow', 'cwd', 'request', 'context'])) return false;
135
+ if (!hasOnlyKeys(value, ['schema_version', 'workflow', 'cwd', 'request', 'context']))
136
+ return false;
122
137
  const request = value['request'];
123
138
  if (!isRecord(request)) return false;
124
139
  if (!hasOnlyKeys(request, ['source', 'authority', 'text', 'sha256'])) return false;
@@ -129,7 +144,8 @@ function isStrictCleanCanonicalInput(value: unknown): boolean {
129
144
  const declaredSources = context['declared_sources'];
130
145
  if (!Array.isArray(declaredSources)) return false;
131
146
  for (const source of declaredSources) {
132
- if (!isRecord(source) || !hasOnlyKeys(source, ['url', 'canonical_url', 'purpose', 'sha256'])) return false;
147
+ if (!isRecord(source) || !hasOnlyKeys(source, ['url', 'canonical_url', 'purpose', 'sha256']))
148
+ return false;
133
149
  }
134
150
  return true;
135
151
  }
@@ -169,6 +185,7 @@ function recordFailureInput(
169
185
  stage: FusionStage,
170
186
  slot: CandidateSlot | undefined,
171
187
  attempt: number,
188
+ systemPrompt: string,
172
189
  prompt: string,
173
190
  responseKind: 'md' | 'txt',
174
191
  ): RecordFusionFailedAttemptInput {
@@ -176,6 +193,7 @@ function recordFailureInput(
176
193
  const base: RecordFusionFailedAttemptInput = {
177
194
  stage,
178
195
  attempt,
196
+ systemPrompt,
179
197
  prompt,
180
198
  events: error.events,
181
199
  partialResponse: error.response,
@@ -194,6 +212,7 @@ function recordFailureInput(
194
212
  const base: RecordFusionFailedAttemptInput = {
195
213
  stage,
196
214
  attempt,
215
+ systemPrompt,
197
216
  prompt,
198
217
  events: Buffer.alloc(0),
199
218
  partialResponse: Buffer.alloc(0),
@@ -271,7 +290,10 @@ function parseEvaluationAttempt(
271
290
  };
272
291
  }
273
292
  if (expectedValidationFindings !== undefined) {
274
- const accountingErrors = validateEvaluationAccountsForSourceFindings(result.value, expectedValidationFindings);
293
+ const accountingErrors = validateEvaluationAccountsForSourceFindings(
294
+ result.value,
295
+ expectedValidationFindings,
296
+ );
275
297
  if (accountingErrors.length > 0) return { evaluation: undefined, errors: accountingErrors };
276
298
  }
277
299
  return { evaluation: result.value, errors: [] };
@@ -371,22 +393,135 @@ function anonymousCandidates(
371
393
  }
372
394
 
373
395
  interface ValidationSourceData {
396
+ candidates: readonly [
397
+ AnonymousFusionCandidate,
398
+ AnonymousFusionCandidate,
399
+ AnonymousFusionCandidate,
400
+ ];
374
401
  findings: readonly FusionValidationFindingRecord[];
375
402
  verified: readonly string[];
376
403
  limitations: readonly string[];
377
404
  }
378
405
 
379
- function validationSourceData(candidates: readonly [AnonymousFusionCandidate, AnonymousFusionCandidate, AnonymousFusionCandidate]): ValidationSourceData {
406
+ function sha256Text(value: string): string {
407
+ return createHash('sha256').update(value, 'utf8').digest('hex');
408
+ }
409
+
410
+ function boundedContractError(error: unknown): string {
411
+ const value = errorText(error);
412
+ return value.length <= 1_000 ? value : `${value.slice(0, 999)}…`;
413
+ }
414
+
415
+ /**
416
+ * Enforce the validation-candidate contract without making the shared JSON
417
+ * parser permissive. A single, tightly recognized fenced response is recovered
418
+ * with a durable warning. One irrecoverable minority report is represented as
419
+ * an explicit limitation; two or more still fail the workflow loudly.
420
+ */
421
+ async function prepareValidationSourceData(
422
+ candidates: readonly [
423
+ AnonymousFusionCandidate,
424
+ AnonymousFusionCandidate,
425
+ AnonymousFusionCandidate,
426
+ ],
427
+ anonymousMap: Record<FusionCandidateId, CandidateSlot>,
428
+ store: FusionArtifactStore,
429
+ ): Promise<ValidationSourceData> {
430
+ const prepared = candidates.map((candidate) => ({ ...candidate })) as [
431
+ AnonymousFusionCandidate,
432
+ AnonymousFusionCandidate,
433
+ AnonymousFusionCandidate,
434
+ ];
380
435
  const findings: FusionValidationFindingRecord[] = [];
381
436
  const verified: string[] = [];
382
437
  const limitations: string[] = [];
383
- for (const candidate of candidates) {
384
- const report = parseFusionValidationCandidateReport(candidate.response, candidate.candidate_id);
385
- findings.push(...report.findings);
386
- verified.push(...report.verified);
438
+ let normalizationCount = 0;
439
+ const failures: Array<{ candidate: AnonymousFusionCandidate; error: string }> = [];
440
+
441
+ for (const candidate of prepared) {
442
+ try {
443
+ const report = parseFusionValidationCandidateReport(
444
+ candidate.response,
445
+ candidate.candidate_id,
446
+ );
447
+ findings.push(...report.findings);
448
+ verified.push(...report.verified);
449
+ limitations.push(...report.limitations);
450
+ continue;
451
+ } catch (strictError) {
452
+ try {
453
+ const recovered = recoverFencedFusionValidationCandidateReport(
454
+ candidate.response,
455
+ candidate.candidate_id,
456
+ );
457
+ if (recovered === undefined) throw strictError;
458
+ await store.recordValidationCandidateContractEvent({
459
+ candidateId: candidate.candidate_id,
460
+ slot: anonymousMap[candidate.candidate_id],
461
+ status: 'normalized',
462
+ detail: {
463
+ normalization: recovered.normalization,
464
+ original_sha256: sha256Text(candidate.response),
465
+ forwarded_sha256: sha256Text(recovered.response),
466
+ warning:
467
+ 'Candidate output violated the bare-JSON contract; a single complete JSON fence was removed and recorded.',
468
+ },
469
+ });
470
+ candidate.response = recovered.response;
471
+ findings.push(...recovered.report.findings);
472
+ verified.push(...recovered.report.verified);
473
+ limitations.push(...recovered.report.limitations);
474
+ normalizationCount += 1;
475
+ continue;
476
+ } catch (recoveryError) {
477
+ failures.push({
478
+ candidate,
479
+ error: boundedContractError(recoveryError === strictError ? strictError : recoveryError),
480
+ });
481
+ }
482
+ }
483
+ }
484
+
485
+ if (normalizationCount > 0) {
486
+ limitations.push(
487
+ `${String(normalizationCount)} validation report${normalizationCount === 1 ? '' : 's'} required audited removal of a Markdown JSON wrapper; JSON content was unchanged.`,
488
+ );
489
+ }
490
+
491
+ for (const failure of failures) {
492
+ await store.recordValidationCandidateContractEvent({
493
+ candidateId: failure.candidate.candidate_id,
494
+ slot: anonymousMap[failure.candidate.candidate_id],
495
+ status: 'dropped',
496
+ detail: {
497
+ response_sha256: sha256Text(failure.candidate.response),
498
+ error: failure.error,
499
+ warning: 'Candidate output could not be parsed under the strict or fenced-JSON contract.',
500
+ },
501
+ });
502
+ }
503
+ if (failures.length > 1) {
504
+ throw new FusionError(
505
+ `fusion_validate cannot continue: ${String(failures.length)} of 3 candidate reports violated the structured-output contract`,
506
+ { code: 'evaluation_invalid', stage: 'candidate' },
507
+ );
508
+ }
509
+ const failure = failures[0];
510
+ if (failure !== undefined) {
511
+ const synthetic = canonicalJson({
512
+ schema_version: FUSION_VALIDATE_CANDIDATE_SCHEMA_VERSION,
513
+ findings: [],
514
+ verified: [],
515
+ limitations: [
516
+ 'This validation report could not be parsed after strict contract checks; no findings or verification claims from it were included.',
517
+ ],
518
+ });
519
+ failure.candidate.response = synthetic;
520
+ const report = parseFusionValidationCandidateReport(synthetic, failure.candidate.candidate_id);
387
521
  limitations.push(...report.limitations);
388
522
  }
389
- return { findings, verified, limitations };
523
+
524
+ return { candidates: prepared, findings, verified, limitations };
390
525
  }
391
526
 
392
527
  function validateEvaluationAccountsForSourceFindings(
@@ -400,14 +535,18 @@ function validateEvaluationAccountsForSourceFindings(
400
535
  }
401
536
  const expected = sourceFindings.map((finding) => canonicalJson(finding)).sort();
402
537
  const actual = accounting.findings.map((finding) => canonicalJson(finding)).sort();
403
- if (expected.length !== actual.length || expected.some((value, index) => value !== actual[index])) {
404
- errors.push('validation evaluator validation_accounting.findings must exactly equal host-assigned source findings');
538
+ if (
539
+ expected.length !== actual.length ||
540
+ expected.some((value, index) => value !== actual[index])
541
+ ) {
542
+ errors.push(
543
+ 'validation evaluator validation_accounting.findings must exactly equal host-assigned source findings',
544
+ );
405
545
  }
406
546
  errors.push(...validateFusionFindingAccounting(accounting));
407
547
  return errors;
408
548
  }
409
549
 
410
-
411
550
  function resolveRunProfile(input: FusionWorkflowInput): FusionWorkflowProfile {
412
551
  if (input.profile !== undefined) return fusionWorkflowProfile(input.profile.id);
413
552
  const workflow = input.canonicalInput.workflow;
@@ -458,11 +597,17 @@ export class FusionOrchestrator {
458
597
  { code: 'orchestration_failed', childCreated: false },
459
598
  );
460
599
  }
461
- if (profile.contextKind === 'clean_task' && !isStrictCleanCanonicalInput(input.canonicalInput)) {
462
- throw new FusionError('clean-task fusion input must not carry parent context fields and must match the strict clean canonical shape', {
463
- code: 'orchestration_failed',
464
- childCreated: false,
465
- });
600
+ if (
601
+ profile.contextKind === 'clean_task' &&
602
+ !isStrictCleanCanonicalInput(input.canonicalInput)
603
+ ) {
604
+ throw new FusionError(
605
+ 'clean-task fusion input must not carry parent context fields and must match the strict clean canonical shape',
606
+ {
607
+ code: 'orchestration_failed',
608
+ childCreated: false,
609
+ },
610
+ );
466
611
  }
467
612
  const candidateCapability = assertWorkflowCapability(profile, input.candidateCapability);
468
613
  const storeOptions: CreateFusionArtifactStoreOptions = {
@@ -483,16 +628,22 @@ export class FusionOrchestrator {
483
628
  try {
484
629
  serializedParsed = parseJsonText(input.canonicalInputSerialized);
485
630
  } catch (error) {
486
- throw new FusionError(`fusion canonical input artifact is not valid JSON: ${errorText(error)}`, {
487
- code: 'orchestration_failed',
488
- childCreated: false,
489
- });
631
+ throw new FusionError(
632
+ `fusion canonical input artifact is not valid JSON: ${errorText(error)}`,
633
+ {
634
+ code: 'orchestration_failed',
635
+ childCreated: false,
636
+ },
637
+ );
490
638
  }
491
639
  if (canonicalJson(serializedParsed) !== canonicalJson(input.canonicalInput)) {
492
- throw new FusionError('fusion canonical input serialized bytes do not match canonical input object', {
493
- code: 'orchestration_failed',
494
- childCreated: false,
495
- });
640
+ throw new FusionError(
641
+ 'fusion canonical input serialized bytes do not match canonical input object',
642
+ {
643
+ code: 'orchestration_failed',
644
+ childCreated: false,
645
+ },
646
+ );
496
647
  }
497
648
  const store = await this.createArtifactStore(storeOptions);
498
649
  input.onProgress?.({ type: 'state', state: 'initializing' });
@@ -502,10 +653,13 @@ export class FusionOrchestrator {
502
653
  await store.writeCanonicalInput(input.canonicalInputSerialized);
503
654
  if (inputContextKind === 'session_projection') {
504
655
  if (input.contextLedger === undefined) {
505
- throw new FusionError('session-projection fusion input requires an omission ledger artifact', {
506
- code: 'orchestration_failed',
507
- childCreated: false,
508
- });
656
+ throw new FusionError(
657
+ 'session-projection fusion input requires an omission ledger artifact',
658
+ {
659
+ code: 'orchestration_failed',
660
+ childCreated: false,
661
+ },
662
+ );
509
663
  }
510
664
  await store.writeContextLedger(input.contextLedger);
511
665
  } else if (input.contextLedger !== undefined) {
@@ -543,6 +697,17 @@ export class FusionOrchestrator {
543
697
  error: 'fusion budget utilization warning',
544
698
  });
545
699
  }
700
+ await input.onReady?.({
701
+ runId: store.runId,
702
+ artifactDir: store.artifactDir,
703
+ artifactDirAbs: store.artifactDirAbs,
704
+ });
705
+ if (input.signal?.aborted === true) {
706
+ throw new FusionError('fusion run cancelled before launch', {
707
+ code: 'child_cancelled',
708
+ childCreated: false,
709
+ });
710
+ }
546
711
  await store.transition('candidates_running');
547
712
  input.onProgress?.({ type: 'state', state: 'candidates_running' });
548
713
  const candidateResults = await this.runCandidates(
@@ -558,11 +723,17 @@ export class FusionOrchestrator {
558
723
  input.onProgress?.({ type: 'state', state: 'candidates_complete' });
559
724
 
560
725
  const shuffled = anonymousCandidates(candidateResults, shuffledSlots(this.randomBytes));
561
- const validationData = profile.id === 'validate' ? validationSourceData(shuffled.candidates) : undefined;
726
+ // Persist the blind mapping before workflow-specific contract parsing
727
+ // so a failed validation remains attributable to its durable slot artifact.
562
728
  await store.setAnonymousMap(shuffled.map);
729
+ const validationData =
730
+ profile.id === 'validate'
731
+ ? await prepareValidationSourceData(shuffled.candidates, shuffled.map, store)
732
+ : undefined;
733
+ const evaluationCandidates = validationData?.candidates ?? shuffled.candidates;
563
734
  const blindInput = buildBlindEvaluationInput(
564
735
  input.canonicalInput,
565
- shuffled.candidates,
736
+ evaluationCandidates,
566
737
  validationData?.findings,
567
738
  );
568
739
  await store.writeBlindCandidates(buildEvaluationPrompt(blindInput));
@@ -585,7 +756,7 @@ export class FusionOrchestrator {
585
756
 
586
757
  await store.transition('merging');
587
758
  input.onProgress?.({ type: 'state', state: 'merging' });
588
- const mergeInput = buildMergeInput(input.canonicalInput, shuffled.candidates, evaluation);
759
+ const mergeInput = buildMergeInput(input.canonicalInput, evaluationCandidates, evaluation);
589
760
  const mergePrompt = buildMergePrompt(mergeInput);
590
761
  budget.assertStagePrompt('merge', profile.mergerSystemPrompt, mergePrompt);
591
762
  input.onProgress?.({ type: 'merge_started' });
@@ -604,7 +775,12 @@ export class FusionOrchestrator {
604
775
  'md',
605
776
  );
606
777
  addFusionUsage(usage, merged.usage);
607
- await store.recordChildAttempt({ result: merged, prompt: mergePrompt, responseKind: 'md' });
778
+ await store.recordChildAttempt({
779
+ result: merged,
780
+ systemPrompt: profile.mergerSystemPrompt,
781
+ prompt: mergePrompt,
782
+ responseKind: 'md',
783
+ });
608
784
  await this.recordCalibrationObservation(
609
785
  input,
610
786
  store,
@@ -620,44 +796,54 @@ export class FusionOrchestrator {
620
796
  if (profile.id === 'validate') {
621
797
  const accounting = evaluation.validation_accounting;
622
798
  if (accounting === undefined) {
623
- throw new FusionError('fusion_validate evaluation completed without validation accounting', {
624
- code: 'evaluation_invalid',
625
- stage: 'merge',
626
- });
799
+ throw new FusionError(
800
+ 'fusion_validate evaluation completed without validation accounting',
801
+ {
802
+ code: 'evaluation_invalid',
803
+ stage: 'merge',
804
+ },
805
+ );
627
806
  }
628
807
  finalMergedText = renderValidatedFusionValidationReport(accounting, validationData);
629
808
  }
630
- if (finalMergedText !== merged.text) assertChildOutputWithinContract('merge', finalMergedText);
631
- await store.writeMerged(finalMergedText);
809
+ if (finalMergedText !== merged.text)
810
+ assertChildOutputWithinContract('merge', finalMergedText);
811
+ const mergedRef = await store.writeMerged(finalMergedText);
632
812
  await store.setUsage(usage);
633
- await store.transition('completed');
634
- input.onProgress?.({ type: 'completed', runId: store.runId, artifactDir: store.artifactDir });
635
- return {
636
- mergedText: finalMergedText,
637
- details: {
638
- schema_version: FUSION_RESULT_SCHEMA_VERSION,
639
- run_id: store.runId,
640
- workflow: profile.id,
641
- source: input.source,
642
- status: 'completed',
643
- artifact_dir: store.artifactDir,
644
- context: { kind: inputContextKind, policy_id: input.canonicalInput.context?.policy_id ?? 'fusion-session-projection-v1' },
645
- tool_policy: { candidate_tools: profile.candidateTools, evaluation_tools: [], merge_tools: [] },
646
- models: store.snapshot().models,
647
- evaluator_attempts: store
648
- .snapshot()
649
- .attempts.filter((attempt) => attempt.stage === 'evaluation').length,
650
- usage,
651
- budget: {
652
- policy_id: FUSION_BUDGET_POLICY.id,
653
- calibration_version: budgetPlan.policy.calibration_version,
654
- route_table: budget.routes,
655
- rate_sources: budget.resultRateSources,
656
- unknown_provider_warnings: budget.unknownProviderWarnings,
657
- calibration_warnings: calibrationWarnings,
658
- },
813
+ const details: FusionRunResult['details'] = {
814
+ schema_version: FUSION_RESULT_SCHEMA_VERSION,
815
+ run_id: store.runId,
816
+ workflow: profile.id,
817
+ source: input.source,
818
+ status: 'completed',
819
+ artifact_dir: store.artifactDir,
820
+ context: {
821
+ kind: inputContextKind,
822
+ policy_id: input.canonicalInput.context?.policy_id ?? 'fusion-session-projection-v1',
823
+ },
824
+ tool_policy: {
825
+ candidate_tools: profile.candidateTools,
826
+ evaluation_tools: [],
827
+ merge_tools: [],
828
+ },
829
+ models: store.snapshot().models,
830
+ evaluator_attempts: store
831
+ .snapshot()
832
+ .attempts.filter((attempt) => attempt.stage === 'evaluation').length,
833
+ usage,
834
+ budget: {
835
+ policy_id: FUSION_BUDGET_POLICY.id,
836
+ calibration_version: budgetPlan.policy.calibration_version,
837
+ route_table: budget.routes,
838
+ rate_sources: budget.resultRateSources,
839
+ unknown_provider_warnings: budget.unknownProviderWarnings,
840
+ calibration_warnings: calibrationWarnings,
659
841
  },
660
842
  };
843
+ await store.writeCommittedResult(mergedRef, details);
844
+ await store.transition('completed');
845
+ input.onProgress?.({ type: 'completed', runId: store.runId, artifactDir: store.artifactDir });
846
+ return { mergedText: finalMergedText, details };
661
847
  } catch (error) {
662
848
  const cancelled =
663
849
  input.signal?.aborted === true ||
@@ -729,9 +915,14 @@ export class FusionOrchestrator {
729
915
  controller.signal,
730
916
  candidateCapability,
731
917
  slot,
732
- 'md',
918
+ profile.id === 'validate' ? 'txt' : 'md',
733
919
  ).then(async (result) => {
734
- await store.recordChildAttempt({ result, prompt, responseKind: 'md' });
920
+ await store.recordChildAttempt({
921
+ result,
922
+ systemPrompt,
923
+ prompt,
924
+ responseKind: profile.id === 'validate' ? 'txt' : 'md',
925
+ });
735
926
  await this.recordCalibrationObservation(
736
927
  input,
737
928
  store,
@@ -866,7 +1057,7 @@ export class FusionOrchestrator {
866
1057
  attempt,
867
1058
  );
868
1059
  addFusionUsage(usage, result.usage);
869
- await store.recordChildAttempt({ result, prompt, responseKind: 'txt' });
1060
+ await store.recordChildAttempt({ result, systemPrompt, prompt, responseKind: 'txt' });
870
1061
  await this.recordCalibrationObservation(
871
1062
  input,
872
1063
  store,
@@ -943,9 +1134,7 @@ export class FusionOrchestrator {
943
1134
  ? store.childToolCallLogPath(stage, slot, logicalAttempt)
944
1135
  : undefined;
945
1136
  const sourcePolicy =
946
- capability === 'research'
947
- ? store.sourcePolicyLaunchReference()
948
- : undefined;
1137
+ capability === 'research' ? store.sourcePolicyLaunchReference() : undefined;
949
1138
  try {
950
1139
  return await this.childRunner(
951
1140
  childOptions(
@@ -966,7 +1155,15 @@ export class FusionOrchestrator {
966
1155
  if (!signal.aborted && retryableSpawn(error, launchTry) && launchTry === 1) continue;
967
1156
  addFailedChildUsage(usage, error);
968
1157
  await store.recordFailedAttempt(
969
- recordFailureInput(error, stage, slot, logicalAttempt, userPrompt, responseKind),
1158
+ recordFailureInput(
1159
+ error,
1160
+ stage,
1161
+ slot,
1162
+ logicalAttempt,
1163
+ systemPrompt,
1164
+ userPrompt,
1165
+ responseKind,
1166
+ ),
970
1167
  );
971
1168
  await store.setUsage(usage);
972
1169
  throw error;
@@ -1440,6 +1440,8 @@ export class FusionPiCompactResultParser {
1440
1440
  ): {
1441
1441
  text: string;
1442
1442
  usage: FusionUsage;
1443
+ firstRequestUsage: FusionUsage;
1444
+ providerRequestCount: number;
1443
1445
  provider: string;
1444
1446
  model: string;
1445
1447
  qualifiedId: string;
@@ -1474,6 +1476,8 @@ export class FusionPiCompactResultParser {
1474
1476
  return {
1475
1477
  text: reconstructFinalText(response, final),
1476
1478
  usage: observed.usage,
1479
+ firstRequestUsage: cloneFusionUsage(parsed.records[0]?.usage ?? createEmptyFusionUsage()),
1480
+ providerRequestCount: parsed.records.length,
1477
1481
  provider: final.provider,
1478
1482
  model: final.model,
1479
1483
  qualifiedId: `${final.provider}/${final.model}`,
@@ -2119,6 +2123,8 @@ export async function runPiChild(options: RunPiChildOptions): Promise<FusionChil
2119
2123
  qualifiedId: parsed.qualifiedId,
2120
2124
  text: parsed.text,
2121
2125
  usage: parsed.usage,
2126
+ firstRequestUsage: parsed.firstRequestUsage,
2127
+ providerRequestCount: parsed.providerRequestCount,
2122
2128
  events: parsed.events,
2123
2129
  stderr: parsed.diagnostics,
2124
2130
  exitCode: close.code,
@@ -190,6 +190,7 @@ Return only JSON matching this exact closed schema:
190
190
  "limitations": ["non-blank statement of what you could not cover"]
191
191
  }
192
192
  Use an empty findings array when no issues were found; do not omit verified or limitations.
193
+ Do not wrap the JSON in Markdown fences or prose. Emit exactly one bare JSON object.
193
194
 
194
195
  Do not inflate severity and do not invent issues to appear thorough. If the work is correct, say so plainly in verified/limitations. A report with no findings that names the evidence behind that conclusion is a valid and valuable result; a padded report is not.
195
196