pi-background-tasks 1.0.7 → 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.
- package/README.md +7 -7
- package/TESTING.md +3 -3
- package/TEST_PLAN.md +2 -2
- package/docs/INDEX.md +25 -25
- package/docs/choose-a-workflow.md +4 -4
- package/docs/commands/bg-clear.md +1 -1
- package/docs/commands/bg-update.md +1 -1
- package/docs/commands/bg.md +1 -1
- package/docs/commands/fusion-models.md +1 -1
- package/docs/commands/fusion.md +5 -8
- package/docs/commands/jobs.md +1 -1
- package/docs/commands/kill.md +1 -1
- package/docs/commands/logs.md +1 -1
- package/docs/commands/task-manager.md +2 -2
- package/docs/concepts/completion-delivery.md +1 -0
- package/docs/getting-started.md +1 -1
- package/docs/manifest.json +59 -50
- package/docs/read-before-edit.md +1 -0
- package/docs/reference/runtime-contracts.md +48 -45
- package/docs/reference/shortcuts-and-dock.md +2 -2
- package/docs/subsystems/background-task-runtime.md +7 -1
- package/docs/subsystems/docs-freshness-gate.md +4 -4
- package/docs/subsystems/fusion.md +13 -9
- package/docs/subsystems/host-ui-and-telemetry.md +1 -1
- package/docs/tools/bg_delegate.md +1 -1
- package/docs/tools/bg_kill.md +1 -1
- package/docs/tools/bg_logs.md +1 -1
- package/docs/tools/bg_result.md +14 -10
- package/docs/tools/bg_run.md +1 -1
- package/docs/tools/bg_run_pi_attested.md +1 -1
- package/docs/tools/bg_status.md +1 -1
- package/docs/tools/fusion_investigate.md +6 -4
- package/docs/tools/fusion_reason.md +5 -5
- package/docs/tools/fusion_research.md +6 -2
- package/docs/tools/fusion_validate.md +5 -3
- package/package.json +1 -1
- package/src/core/common.ts +50 -2
- package/src/core/fusion/artifacts.ts +72 -20
- package/src/core/fusion/orchestrator.ts +154 -68
- package/src/core/fusion/result-package.ts +385 -0
- package/src/core/fusion/types.ts +9 -0
- package/src/core/registry.ts +187 -20
- package/src/delegate-extension.ts +130 -24
- package/src/extension.ts +17 -6
- package/src/fusion-extension.ts +308 -154
|
@@ -63,6 +63,12 @@ export type FusionChildRunner = (options: RunPiChildOptions) => Promise<FusionCh
|
|
|
63
63
|
export type FusionProgressSink = (event: FusionProgressEvent) => void;
|
|
64
64
|
export type FusionRandomBytes = (size: number) => Buffer;
|
|
65
65
|
|
|
66
|
+
export interface FusionRunReady {
|
|
67
|
+
runId: string;
|
|
68
|
+
artifactDir: string;
|
|
69
|
+
artifactDirAbs: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
66
72
|
type CandidateSlot = 1 | 2 | 3;
|
|
67
73
|
|
|
68
74
|
export interface FusionWorkflowInput {
|
|
@@ -79,6 +85,12 @@ export interface FusionWorkflowInput {
|
|
|
79
85
|
profile?: FusionWorkflowProfile | undefined;
|
|
80
86
|
signal?: AbortSignal | undefined;
|
|
81
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;
|
|
82
94
|
}
|
|
83
95
|
|
|
84
96
|
export interface FusionOrchestratorOptions {
|
|
@@ -120,7 +132,8 @@ function hasOnlyKeys(value: Record<string, unknown>, allowed: readonly string[])
|
|
|
120
132
|
|
|
121
133
|
function isStrictCleanCanonicalInput(value: unknown): boolean {
|
|
122
134
|
if (!isRecord(value)) return false;
|
|
123
|
-
if (!hasOnlyKeys(value, ['schema_version', 'workflow', 'cwd', 'request', 'context']))
|
|
135
|
+
if (!hasOnlyKeys(value, ['schema_version', 'workflow', 'cwd', 'request', 'context']))
|
|
136
|
+
return false;
|
|
124
137
|
const request = value['request'];
|
|
125
138
|
if (!isRecord(request)) return false;
|
|
126
139
|
if (!hasOnlyKeys(request, ['source', 'authority', 'text', 'sha256'])) return false;
|
|
@@ -131,7 +144,8 @@ function isStrictCleanCanonicalInput(value: unknown): boolean {
|
|
|
131
144
|
const declaredSources = context['declared_sources'];
|
|
132
145
|
if (!Array.isArray(declaredSources)) return false;
|
|
133
146
|
for (const source of declaredSources) {
|
|
134
|
-
if (!isRecord(source) || !hasOnlyKeys(source, ['url', 'canonical_url', 'purpose', 'sha256']))
|
|
147
|
+
if (!isRecord(source) || !hasOnlyKeys(source, ['url', 'canonical_url', 'purpose', 'sha256']))
|
|
148
|
+
return false;
|
|
135
149
|
}
|
|
136
150
|
return true;
|
|
137
151
|
}
|
|
@@ -276,7 +290,10 @@ function parseEvaluationAttempt(
|
|
|
276
290
|
};
|
|
277
291
|
}
|
|
278
292
|
if (expectedValidationFindings !== undefined) {
|
|
279
|
-
const accountingErrors = validateEvaluationAccountsForSourceFindings(
|
|
293
|
+
const accountingErrors = validateEvaluationAccountsForSourceFindings(
|
|
294
|
+
result.value,
|
|
295
|
+
expectedValidationFindings,
|
|
296
|
+
);
|
|
280
297
|
if (accountingErrors.length > 0) return { evaluation: undefined, errors: accountingErrors };
|
|
281
298
|
}
|
|
282
299
|
return { evaluation: result.value, errors: [] };
|
|
@@ -376,7 +393,11 @@ function anonymousCandidates(
|
|
|
376
393
|
}
|
|
377
394
|
|
|
378
395
|
interface ValidationSourceData {
|
|
379
|
-
candidates: readonly [
|
|
396
|
+
candidates: readonly [
|
|
397
|
+
AnonymousFusionCandidate,
|
|
398
|
+
AnonymousFusionCandidate,
|
|
399
|
+
AnonymousFusionCandidate,
|
|
400
|
+
];
|
|
380
401
|
findings: readonly FusionValidationFindingRecord[];
|
|
381
402
|
verified: readonly string[];
|
|
382
403
|
limitations: readonly string[];
|
|
@@ -398,7 +419,11 @@ function boundedContractError(error: unknown): string {
|
|
|
398
419
|
* an explicit limitation; two or more still fail the workflow loudly.
|
|
399
420
|
*/
|
|
400
421
|
async function prepareValidationSourceData(
|
|
401
|
-
candidates: readonly [
|
|
422
|
+
candidates: readonly [
|
|
423
|
+
AnonymousFusionCandidate,
|
|
424
|
+
AnonymousFusionCandidate,
|
|
425
|
+
AnonymousFusionCandidate,
|
|
426
|
+
],
|
|
402
427
|
anonymousMap: Record<FusionCandidateId, CandidateSlot>,
|
|
403
428
|
store: FusionArtifactStore,
|
|
404
429
|
): Promise<ValidationSourceData> {
|
|
@@ -415,7 +440,10 @@ async function prepareValidationSourceData(
|
|
|
415
440
|
|
|
416
441
|
for (const candidate of prepared) {
|
|
417
442
|
try {
|
|
418
|
-
const report = parseFusionValidationCandidateReport(
|
|
443
|
+
const report = parseFusionValidationCandidateReport(
|
|
444
|
+
candidate.response,
|
|
445
|
+
candidate.candidate_id,
|
|
446
|
+
);
|
|
419
447
|
findings.push(...report.findings);
|
|
420
448
|
verified.push(...report.verified);
|
|
421
449
|
limitations.push(...report.limitations);
|
|
@@ -435,7 +463,8 @@ async function prepareValidationSourceData(
|
|
|
435
463
|
normalization: recovered.normalization,
|
|
436
464
|
original_sha256: sha256Text(candidate.response),
|
|
437
465
|
forwarded_sha256: sha256Text(recovered.response),
|
|
438
|
-
warning:
|
|
466
|
+
warning:
|
|
467
|
+
'Candidate output violated the bare-JSON contract; a single complete JSON fence was removed and recorded.',
|
|
439
468
|
},
|
|
440
469
|
});
|
|
441
470
|
candidate.response = recovered.response;
|
|
@@ -506,14 +535,18 @@ function validateEvaluationAccountsForSourceFindings(
|
|
|
506
535
|
}
|
|
507
536
|
const expected = sourceFindings.map((finding) => canonicalJson(finding)).sort();
|
|
508
537
|
const actual = accounting.findings.map((finding) => canonicalJson(finding)).sort();
|
|
509
|
-
if (
|
|
510
|
-
|
|
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
|
+
);
|
|
511
545
|
}
|
|
512
546
|
errors.push(...validateFusionFindingAccounting(accounting));
|
|
513
547
|
return errors;
|
|
514
548
|
}
|
|
515
549
|
|
|
516
|
-
|
|
517
550
|
function resolveRunProfile(input: FusionWorkflowInput): FusionWorkflowProfile {
|
|
518
551
|
if (input.profile !== undefined) return fusionWorkflowProfile(input.profile.id);
|
|
519
552
|
const workflow = input.canonicalInput.workflow;
|
|
@@ -564,11 +597,17 @@ export class FusionOrchestrator {
|
|
|
564
597
|
{ code: 'orchestration_failed', childCreated: false },
|
|
565
598
|
);
|
|
566
599
|
}
|
|
567
|
-
if (
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
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
|
+
);
|
|
572
611
|
}
|
|
573
612
|
const candidateCapability = assertWorkflowCapability(profile, input.candidateCapability);
|
|
574
613
|
const storeOptions: CreateFusionArtifactStoreOptions = {
|
|
@@ -589,16 +628,22 @@ export class FusionOrchestrator {
|
|
|
589
628
|
try {
|
|
590
629
|
serializedParsed = parseJsonText(input.canonicalInputSerialized);
|
|
591
630
|
} catch (error) {
|
|
592
|
-
throw new FusionError(
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
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
|
+
);
|
|
596
638
|
}
|
|
597
639
|
if (canonicalJson(serializedParsed) !== canonicalJson(input.canonicalInput)) {
|
|
598
|
-
throw new FusionError(
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
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
|
+
);
|
|
602
647
|
}
|
|
603
648
|
const store = await this.createArtifactStore(storeOptions);
|
|
604
649
|
input.onProgress?.({ type: 'state', state: 'initializing' });
|
|
@@ -608,10 +653,13 @@ export class FusionOrchestrator {
|
|
|
608
653
|
await store.writeCanonicalInput(input.canonicalInputSerialized);
|
|
609
654
|
if (inputContextKind === 'session_projection') {
|
|
610
655
|
if (input.contextLedger === undefined) {
|
|
611
|
-
throw new FusionError(
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
656
|
+
throw new FusionError(
|
|
657
|
+
'session-projection fusion input requires an omission ledger artifact',
|
|
658
|
+
{
|
|
659
|
+
code: 'orchestration_failed',
|
|
660
|
+
childCreated: false,
|
|
661
|
+
},
|
|
662
|
+
);
|
|
615
663
|
}
|
|
616
664
|
await store.writeContextLedger(input.contextLedger);
|
|
617
665
|
} else if (input.contextLedger !== undefined) {
|
|
@@ -649,6 +697,17 @@ export class FusionOrchestrator {
|
|
|
649
697
|
error: 'fusion budget utilization warning',
|
|
650
698
|
});
|
|
651
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
|
+
}
|
|
652
711
|
await store.transition('candidates_running');
|
|
653
712
|
input.onProgress?.({ type: 'state', state: 'candidates_running' });
|
|
654
713
|
const candidateResults = await this.runCandidates(
|
|
@@ -667,9 +726,10 @@ export class FusionOrchestrator {
|
|
|
667
726
|
// Persist the blind mapping before workflow-specific contract parsing
|
|
668
727
|
// so a failed validation remains attributable to its durable slot artifact.
|
|
669
728
|
await store.setAnonymousMap(shuffled.map);
|
|
670
|
-
const validationData =
|
|
671
|
-
|
|
672
|
-
|
|
729
|
+
const validationData =
|
|
730
|
+
profile.id === 'validate'
|
|
731
|
+
? await prepareValidationSourceData(shuffled.candidates, shuffled.map, store)
|
|
732
|
+
: undefined;
|
|
673
733
|
const evaluationCandidates = validationData?.candidates ?? shuffled.candidates;
|
|
674
734
|
const blindInput = buildBlindEvaluationInput(
|
|
675
735
|
input.canonicalInput,
|
|
@@ -715,7 +775,12 @@ export class FusionOrchestrator {
|
|
|
715
775
|
'md',
|
|
716
776
|
);
|
|
717
777
|
addFusionUsage(usage, merged.usage);
|
|
718
|
-
await store.recordChildAttempt({
|
|
778
|
+
await store.recordChildAttempt({
|
|
779
|
+
result: merged,
|
|
780
|
+
systemPrompt: profile.mergerSystemPrompt,
|
|
781
|
+
prompt: mergePrompt,
|
|
782
|
+
responseKind: 'md',
|
|
783
|
+
});
|
|
719
784
|
await this.recordCalibrationObservation(
|
|
720
785
|
input,
|
|
721
786
|
store,
|
|
@@ -731,44 +796,54 @@ export class FusionOrchestrator {
|
|
|
731
796
|
if (profile.id === 'validate') {
|
|
732
797
|
const accounting = evaluation.validation_accounting;
|
|
733
798
|
if (accounting === undefined) {
|
|
734
|
-
throw new FusionError(
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
799
|
+
throw new FusionError(
|
|
800
|
+
'fusion_validate evaluation completed without validation accounting',
|
|
801
|
+
{
|
|
802
|
+
code: 'evaluation_invalid',
|
|
803
|
+
stage: 'merge',
|
|
804
|
+
},
|
|
805
|
+
);
|
|
738
806
|
}
|
|
739
807
|
finalMergedText = renderValidatedFusionValidationReport(accounting, validationData);
|
|
740
808
|
}
|
|
741
|
-
if (finalMergedText !== merged.text)
|
|
742
|
-
|
|
809
|
+
if (finalMergedText !== merged.text)
|
|
810
|
+
assertChildOutputWithinContract('merge', finalMergedText);
|
|
811
|
+
const mergedRef = await store.writeMerged(finalMergedText);
|
|
743
812
|
await store.setUsage(usage);
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
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,
|
|
770
841
|
},
|
|
771
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 };
|
|
772
847
|
} catch (error) {
|
|
773
848
|
const cancelled =
|
|
774
849
|
input.signal?.aborted === true ||
|
|
@@ -842,7 +917,12 @@ export class FusionOrchestrator {
|
|
|
842
917
|
slot,
|
|
843
918
|
profile.id === 'validate' ? 'txt' : 'md',
|
|
844
919
|
).then(async (result) => {
|
|
845
|
-
await store.recordChildAttempt({
|
|
920
|
+
await store.recordChildAttempt({
|
|
921
|
+
result,
|
|
922
|
+
systemPrompt,
|
|
923
|
+
prompt,
|
|
924
|
+
responseKind: profile.id === 'validate' ? 'txt' : 'md',
|
|
925
|
+
});
|
|
846
926
|
await this.recordCalibrationObservation(
|
|
847
927
|
input,
|
|
848
928
|
store,
|
|
@@ -1054,9 +1134,7 @@ export class FusionOrchestrator {
|
|
|
1054
1134
|
? store.childToolCallLogPath(stage, slot, logicalAttempt)
|
|
1055
1135
|
: undefined;
|
|
1056
1136
|
const sourcePolicy =
|
|
1057
|
-
capability === 'research'
|
|
1058
|
-
? store.sourcePolicyLaunchReference()
|
|
1059
|
-
: undefined;
|
|
1137
|
+
capability === 'research' ? store.sourcePolicyLaunchReference() : undefined;
|
|
1060
1138
|
try {
|
|
1061
1139
|
return await this.childRunner(
|
|
1062
1140
|
childOptions(
|
|
@@ -1077,7 +1155,15 @@ export class FusionOrchestrator {
|
|
|
1077
1155
|
if (!signal.aborted && retryableSpawn(error, launchTry) && launchTry === 1) continue;
|
|
1078
1156
|
addFailedChildUsage(usage, error);
|
|
1079
1157
|
await store.recordFailedAttempt(
|
|
1080
|
-
recordFailureInput(
|
|
1158
|
+
recordFailureInput(
|
|
1159
|
+
error,
|
|
1160
|
+
stage,
|
|
1161
|
+
slot,
|
|
1162
|
+
logicalAttempt,
|
|
1163
|
+
systemPrompt,
|
|
1164
|
+
userPrompt,
|
|
1165
|
+
responseKind,
|
|
1166
|
+
),
|
|
1081
1167
|
);
|
|
1082
1168
|
await store.setUsage(usage);
|
|
1083
1169
|
throw error;
|