pi-background-tasks 1.0.4 → 1.0.7
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/BACKGROUND-TASKS-INSTRUCTIONS.md +2 -1
- package/README.md +2 -1
- package/TESTING.md +2 -2
- package/TEST_PLAN.md +10 -9
- package/docs/manifest.json +8 -4
- package/docs/operations/configuration.md +10 -0
- package/docs/operations/testing.md +2 -1
- package/docs/operations/troubleshooting.md +3 -1
- package/docs/read-before-edit.md +1 -0
- package/docs/reference/runtime-contracts.md +66 -61
- package/docs/subsystems/docs-freshness-gate.md +5 -5
- package/docs/subsystems/fusion.md +19 -14
- package/package.json +2 -1
- package/src/core/fusion/artifacts.ts +41 -0
- package/src/core/fusion/budget.ts +23 -8
- package/src/core/fusion/child-protocol.ts +137 -12
- package/src/core/fusion/claude-cache.ts +186 -0
- package/src/core/fusion/config.ts +13 -0
- package/src/core/fusion/evaluation.ts +61 -0
- package/src/core/fusion/orchestrator.ts +126 -15
- package/src/core/fusion/pi-child.ts +564 -14
- package/src/core/fusion/prompts.ts +1 -0
- package/src/core/fusion/types.ts +39 -8
- package/src/fusion-child-extension.ts +413 -4
|
@@ -158,6 +158,17 @@ function requireContextWindow(model: Model<Api>, label: string): number {
|
|
|
158
158
|
return Math.floor(value);
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
function requireMaxOutputTokens(model: Model<Api>, label: string): number {
|
|
162
|
+
const value = model.maxTokens;
|
|
163
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
|
|
164
|
+
throw new FusionError(`${label} has no positive maximum output token capacity`, {
|
|
165
|
+
code: 'model_unavailable',
|
|
166
|
+
childCreated: false,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
return Math.floor(value);
|
|
170
|
+
}
|
|
171
|
+
|
|
161
172
|
function modelIndex(models: readonly Model<Api>[]): Map<string, Model<Api>> {
|
|
162
173
|
const out = new Map<string, Model<Api>>();
|
|
163
174
|
for (const model of models) out.set(qualifiedModelKey(model), model);
|
|
@@ -305,6 +316,7 @@ function resolveSelection(
|
|
|
305
316
|
qualifiedId,
|
|
306
317
|
thinkingLevel,
|
|
307
318
|
contextWindow: requireContextWindow(available, slotLabel),
|
|
319
|
+
maxOutputTokens: requireMaxOutputTokens(available, slotLabel),
|
|
308
320
|
};
|
|
309
321
|
}
|
|
310
322
|
const model = availableByKey.get(selection);
|
|
@@ -323,6 +335,7 @@ function resolveSelection(
|
|
|
323
335
|
qualifiedId: selection,
|
|
324
336
|
thinkingLevel,
|
|
325
337
|
contextWindow: requireContextWindow(model, slotLabel),
|
|
338
|
+
maxOutputTokens: requireMaxOutputTokens(model, slotLabel),
|
|
326
339
|
};
|
|
327
340
|
}
|
|
328
341
|
|
|
@@ -525,6 +525,67 @@ export interface ParsedFusionValidationCandidateReport {
|
|
|
525
525
|
limitations: readonly string[];
|
|
526
526
|
}
|
|
527
527
|
|
|
528
|
+
export type FusionValidationCandidateNormalization =
|
|
529
|
+
| 'markdown_json_fence'
|
|
530
|
+
| 'prose_then_markdown_json_fence';
|
|
531
|
+
|
|
532
|
+
export interface RecoveredFusionValidationCandidateReport {
|
|
533
|
+
report: ParsedFusionValidationCandidateReport;
|
|
534
|
+
/** Bare JSON forwarded to the evaluator after explicit, audited recovery. */
|
|
535
|
+
response: string;
|
|
536
|
+
normalization: FusionValidationCandidateNormalization;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Recognize exactly one complete Markdown JSON fence, optionally preceded by a
|
|
541
|
+
* short prose preamble. This is deliberately narrower than generic substring
|
|
542
|
+
* extraction: trailing prose, nested fences, unlabelled fences, and oversized
|
|
543
|
+
* preambles remain contract failures.
|
|
544
|
+
*/
|
|
545
|
+
function fencedValidationCandidateJson(text: string): {
|
|
546
|
+
payload: string;
|
|
547
|
+
normalization: FusionValidationCandidateNormalization;
|
|
548
|
+
} | undefined {
|
|
549
|
+
const trimmed = text.trim();
|
|
550
|
+
const openingPattern = /```json[ \t]*\r?\n/giu;
|
|
551
|
+
const openings = [...trimmed.matchAll(openingPattern)];
|
|
552
|
+
if (openings.length !== 1) return undefined;
|
|
553
|
+
const opening = openings[0];
|
|
554
|
+
if (opening === undefined) return undefined;
|
|
555
|
+
const headerEnd = opening.index + opening[0].length;
|
|
556
|
+
const closing = trimmed.indexOf('```', headerEnd);
|
|
557
|
+
if (closing < 0 || trimmed.slice(closing + 3).includes('```')) return undefined;
|
|
558
|
+
if (trimmed.slice(closing + 3).trim().length > 0) return undefined;
|
|
559
|
+
const preamble = trimmed.slice(0, opening.index).trim();
|
|
560
|
+
if (Buffer.byteLength(preamble, 'utf8') > 2_000 || preamble.includes('```')) return undefined;
|
|
561
|
+
const payload = trimmed.slice(headerEnd, closing).trim();
|
|
562
|
+
if (payload.length === 0 || payload.includes('```')) return undefined;
|
|
563
|
+
return {
|
|
564
|
+
payload,
|
|
565
|
+
normalization: preamble.length === 0
|
|
566
|
+
? 'markdown_json_fence'
|
|
567
|
+
: 'prose_then_markdown_json_fence',
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Defensive recovery for the one observed contract violation shape. Callers
|
|
573
|
+
* must persist/surface the returned normalization; this function intentionally
|
|
574
|
+
* does not make the strict parser permissive.
|
|
575
|
+
*/
|
|
576
|
+
export function recoverFencedFusionValidationCandidateReport(
|
|
577
|
+
text: string,
|
|
578
|
+
candidateId: FusionCandidateId,
|
|
579
|
+
): RecoveredFusionValidationCandidateReport | undefined {
|
|
580
|
+
const recovered = fencedValidationCandidateJson(text);
|
|
581
|
+
if (recovered === undefined) return undefined;
|
|
582
|
+
return {
|
|
583
|
+
report: parseFusionValidationCandidateReport(recovered.payload, candidateId),
|
|
584
|
+
response: recovered.payload,
|
|
585
|
+
normalization: recovered.normalization,
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
|
|
528
589
|
export function parseFusionValidationCandidateReport(text: string, candidateId: FusionCandidateId): ParsedFusionValidationCandidateReport {
|
|
529
590
|
let parsed: unknown;
|
|
530
591
|
try {
|
|
@@ -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,
|
|
@@ -169,6 +171,7 @@ function recordFailureInput(
|
|
|
169
171
|
stage: FusionStage,
|
|
170
172
|
slot: CandidateSlot | undefined,
|
|
171
173
|
attempt: number,
|
|
174
|
+
systemPrompt: string,
|
|
172
175
|
prompt: string,
|
|
173
176
|
responseKind: 'md' | 'txt',
|
|
174
177
|
): RecordFusionFailedAttemptInput {
|
|
@@ -176,6 +179,7 @@ function recordFailureInput(
|
|
|
176
179
|
const base: RecordFusionFailedAttemptInput = {
|
|
177
180
|
stage,
|
|
178
181
|
attempt,
|
|
182
|
+
systemPrompt,
|
|
179
183
|
prompt,
|
|
180
184
|
events: error.events,
|
|
181
185
|
partialResponse: error.response,
|
|
@@ -194,6 +198,7 @@ function recordFailureInput(
|
|
|
194
198
|
const base: RecordFusionFailedAttemptInput = {
|
|
195
199
|
stage,
|
|
196
200
|
attempt,
|
|
201
|
+
systemPrompt,
|
|
197
202
|
prompt,
|
|
198
203
|
events: Buffer.alloc(0),
|
|
199
204
|
partialResponse: Buffer.alloc(0),
|
|
@@ -371,22 +376,123 @@ function anonymousCandidates(
|
|
|
371
376
|
}
|
|
372
377
|
|
|
373
378
|
interface ValidationSourceData {
|
|
379
|
+
candidates: readonly [AnonymousFusionCandidate, AnonymousFusionCandidate, AnonymousFusionCandidate];
|
|
374
380
|
findings: readonly FusionValidationFindingRecord[];
|
|
375
381
|
verified: readonly string[];
|
|
376
382
|
limitations: readonly string[];
|
|
377
383
|
}
|
|
378
384
|
|
|
379
|
-
function
|
|
385
|
+
function sha256Text(value: string): string {
|
|
386
|
+
return createHash('sha256').update(value, 'utf8').digest('hex');
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function boundedContractError(error: unknown): string {
|
|
390
|
+
const value = errorText(error);
|
|
391
|
+
return value.length <= 1_000 ? value : `${value.slice(0, 999)}…`;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Enforce the validation-candidate contract without making the shared JSON
|
|
396
|
+
* parser permissive. A single, tightly recognized fenced response is recovered
|
|
397
|
+
* with a durable warning. One irrecoverable minority report is represented as
|
|
398
|
+
* an explicit limitation; two or more still fail the workflow loudly.
|
|
399
|
+
*/
|
|
400
|
+
async function prepareValidationSourceData(
|
|
401
|
+
candidates: readonly [AnonymousFusionCandidate, AnonymousFusionCandidate, AnonymousFusionCandidate],
|
|
402
|
+
anonymousMap: Record<FusionCandidateId, CandidateSlot>,
|
|
403
|
+
store: FusionArtifactStore,
|
|
404
|
+
): Promise<ValidationSourceData> {
|
|
405
|
+
const prepared = candidates.map((candidate) => ({ ...candidate })) as [
|
|
406
|
+
AnonymousFusionCandidate,
|
|
407
|
+
AnonymousFusionCandidate,
|
|
408
|
+
AnonymousFusionCandidate,
|
|
409
|
+
];
|
|
380
410
|
const findings: FusionValidationFindingRecord[] = [];
|
|
381
411
|
const verified: string[] = [];
|
|
382
412
|
const limitations: string[] = [];
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
413
|
+
let normalizationCount = 0;
|
|
414
|
+
const failures: Array<{ candidate: AnonymousFusionCandidate; error: string }> = [];
|
|
415
|
+
|
|
416
|
+
for (const candidate of prepared) {
|
|
417
|
+
try {
|
|
418
|
+
const report = parseFusionValidationCandidateReport(candidate.response, candidate.candidate_id);
|
|
419
|
+
findings.push(...report.findings);
|
|
420
|
+
verified.push(...report.verified);
|
|
421
|
+
limitations.push(...report.limitations);
|
|
422
|
+
continue;
|
|
423
|
+
} catch (strictError) {
|
|
424
|
+
try {
|
|
425
|
+
const recovered = recoverFencedFusionValidationCandidateReport(
|
|
426
|
+
candidate.response,
|
|
427
|
+
candidate.candidate_id,
|
|
428
|
+
);
|
|
429
|
+
if (recovered === undefined) throw strictError;
|
|
430
|
+
await store.recordValidationCandidateContractEvent({
|
|
431
|
+
candidateId: candidate.candidate_id,
|
|
432
|
+
slot: anonymousMap[candidate.candidate_id],
|
|
433
|
+
status: 'normalized',
|
|
434
|
+
detail: {
|
|
435
|
+
normalization: recovered.normalization,
|
|
436
|
+
original_sha256: sha256Text(candidate.response),
|
|
437
|
+
forwarded_sha256: sha256Text(recovered.response),
|
|
438
|
+
warning: 'Candidate output violated the bare-JSON contract; a single complete JSON fence was removed and recorded.',
|
|
439
|
+
},
|
|
440
|
+
});
|
|
441
|
+
candidate.response = recovered.response;
|
|
442
|
+
findings.push(...recovered.report.findings);
|
|
443
|
+
verified.push(...recovered.report.verified);
|
|
444
|
+
limitations.push(...recovered.report.limitations);
|
|
445
|
+
normalizationCount += 1;
|
|
446
|
+
continue;
|
|
447
|
+
} catch (recoveryError) {
|
|
448
|
+
failures.push({
|
|
449
|
+
candidate,
|
|
450
|
+
error: boundedContractError(recoveryError === strictError ? strictError : recoveryError),
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (normalizationCount > 0) {
|
|
457
|
+
limitations.push(
|
|
458
|
+
`${String(normalizationCount)} validation report${normalizationCount === 1 ? '' : 's'} required audited removal of a Markdown JSON wrapper; JSON content was unchanged.`,
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
for (const failure of failures) {
|
|
463
|
+
await store.recordValidationCandidateContractEvent({
|
|
464
|
+
candidateId: failure.candidate.candidate_id,
|
|
465
|
+
slot: anonymousMap[failure.candidate.candidate_id],
|
|
466
|
+
status: 'dropped',
|
|
467
|
+
detail: {
|
|
468
|
+
response_sha256: sha256Text(failure.candidate.response),
|
|
469
|
+
error: failure.error,
|
|
470
|
+
warning: 'Candidate output could not be parsed under the strict or fenced-JSON contract.',
|
|
471
|
+
},
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
if (failures.length > 1) {
|
|
475
|
+
throw new FusionError(
|
|
476
|
+
`fusion_validate cannot continue: ${String(failures.length)} of 3 candidate reports violated the structured-output contract`,
|
|
477
|
+
{ code: 'evaluation_invalid', stage: 'candidate' },
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
const failure = failures[0];
|
|
481
|
+
if (failure !== undefined) {
|
|
482
|
+
const synthetic = canonicalJson({
|
|
483
|
+
schema_version: FUSION_VALIDATE_CANDIDATE_SCHEMA_VERSION,
|
|
484
|
+
findings: [],
|
|
485
|
+
verified: [],
|
|
486
|
+
limitations: [
|
|
487
|
+
'This validation report could not be parsed after strict contract checks; no findings or verification claims from it were included.',
|
|
488
|
+
],
|
|
489
|
+
});
|
|
490
|
+
failure.candidate.response = synthetic;
|
|
491
|
+
const report = parseFusionValidationCandidateReport(synthetic, failure.candidate.candidate_id);
|
|
387
492
|
limitations.push(...report.limitations);
|
|
388
493
|
}
|
|
389
|
-
|
|
494
|
+
|
|
495
|
+
return { candidates: prepared, findings, verified, limitations };
|
|
390
496
|
}
|
|
391
497
|
|
|
392
498
|
function validateEvaluationAccountsForSourceFindings(
|
|
@@ -558,11 +664,16 @@ export class FusionOrchestrator {
|
|
|
558
664
|
input.onProgress?.({ type: 'state', state: 'candidates_complete' });
|
|
559
665
|
|
|
560
666
|
const shuffled = anonymousCandidates(candidateResults, shuffledSlots(this.randomBytes));
|
|
561
|
-
|
|
667
|
+
// Persist the blind mapping before workflow-specific contract parsing
|
|
668
|
+
// so a failed validation remains attributable to its durable slot artifact.
|
|
562
669
|
await store.setAnonymousMap(shuffled.map);
|
|
670
|
+
const validationData = profile.id === 'validate'
|
|
671
|
+
? await prepareValidationSourceData(shuffled.candidates, shuffled.map, store)
|
|
672
|
+
: undefined;
|
|
673
|
+
const evaluationCandidates = validationData?.candidates ?? shuffled.candidates;
|
|
563
674
|
const blindInput = buildBlindEvaluationInput(
|
|
564
675
|
input.canonicalInput,
|
|
565
|
-
|
|
676
|
+
evaluationCandidates,
|
|
566
677
|
validationData?.findings,
|
|
567
678
|
);
|
|
568
679
|
await store.writeBlindCandidates(buildEvaluationPrompt(blindInput));
|
|
@@ -585,7 +696,7 @@ export class FusionOrchestrator {
|
|
|
585
696
|
|
|
586
697
|
await store.transition('merging');
|
|
587
698
|
input.onProgress?.({ type: 'state', state: 'merging' });
|
|
588
|
-
const mergeInput = buildMergeInput(input.canonicalInput,
|
|
699
|
+
const mergeInput = buildMergeInput(input.canonicalInput, evaluationCandidates, evaluation);
|
|
589
700
|
const mergePrompt = buildMergePrompt(mergeInput);
|
|
590
701
|
budget.assertStagePrompt('merge', profile.mergerSystemPrompt, mergePrompt);
|
|
591
702
|
input.onProgress?.({ type: 'merge_started' });
|
|
@@ -604,7 +715,7 @@ export class FusionOrchestrator {
|
|
|
604
715
|
'md',
|
|
605
716
|
);
|
|
606
717
|
addFusionUsage(usage, merged.usage);
|
|
607
|
-
await store.recordChildAttempt({ result: merged, prompt: mergePrompt, responseKind: 'md' });
|
|
718
|
+
await store.recordChildAttempt({ result: merged, systemPrompt: profile.mergerSystemPrompt, prompt: mergePrompt, responseKind: 'md' });
|
|
608
719
|
await this.recordCalibrationObservation(
|
|
609
720
|
input,
|
|
610
721
|
store,
|
|
@@ -729,9 +840,9 @@ export class FusionOrchestrator {
|
|
|
729
840
|
controller.signal,
|
|
730
841
|
candidateCapability,
|
|
731
842
|
slot,
|
|
732
|
-
'md',
|
|
843
|
+
profile.id === 'validate' ? 'txt' : 'md',
|
|
733
844
|
).then(async (result) => {
|
|
734
|
-
await store.recordChildAttempt({ result, prompt, responseKind: 'md' });
|
|
845
|
+
await store.recordChildAttempt({ result, systemPrompt, prompt, responseKind: profile.id === 'validate' ? 'txt' : 'md' });
|
|
735
846
|
await this.recordCalibrationObservation(
|
|
736
847
|
input,
|
|
737
848
|
store,
|
|
@@ -866,7 +977,7 @@ export class FusionOrchestrator {
|
|
|
866
977
|
attempt,
|
|
867
978
|
);
|
|
868
979
|
addFusionUsage(usage, result.usage);
|
|
869
|
-
await store.recordChildAttempt({ result, prompt, responseKind: 'txt' });
|
|
980
|
+
await store.recordChildAttempt({ result, systemPrompt, prompt, responseKind: 'txt' });
|
|
870
981
|
await this.recordCalibrationObservation(
|
|
871
982
|
input,
|
|
872
983
|
store,
|
|
@@ -966,7 +1077,7 @@ export class FusionOrchestrator {
|
|
|
966
1077
|
if (!signal.aborted && retryableSpawn(error, launchTry) && launchTry === 1) continue;
|
|
967
1078
|
addFailedChildUsage(usage, error);
|
|
968
1079
|
await store.recordFailedAttempt(
|
|
969
|
-
recordFailureInput(error, stage, slot, logicalAttempt, userPrompt, responseKind),
|
|
1080
|
+
recordFailureInput(error, stage, slot, logicalAttempt, systemPrompt, userPrompt, responseKind),
|
|
970
1081
|
);
|
|
971
1082
|
await store.setUsage(usage);
|
|
972
1083
|
throw error;
|