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.
- 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 +55 -51
- 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 +15 -11
- 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 +106 -13
- package/src/core/fusion/budget.ts +12 -4
- package/src/core/fusion/evaluation.ts +61 -0
- package/src/core/fusion/orchestrator.ts +270 -73
- package/src/core/fusion/pi-child.ts +6 -0
- package/src/core/fusion/prompts.ts +1 -0
- package/src/core/fusion/result-package.ts +385 -0
- package/src/core/fusion/types.ts +19 -1
- 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
|
@@ -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']))
|
|
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']))
|
|
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(
|
|
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
|
|
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
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
-
|
|
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 (
|
|
404
|
-
|
|
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 (
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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(
|
|
487
|
-
|
|
488
|
-
|
|
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(
|
|
493
|
-
|
|
494
|
-
|
|
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(
|
|
506
|
-
|
|
507
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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({
|
|
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(
|
|
624
|
-
|
|
625
|
-
|
|
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)
|
|
631
|
-
|
|
809
|
+
if (finalMergedText !== merged.text)
|
|
810
|
+
assertChildOutputWithinContract('merge', finalMergedText);
|
|
811
|
+
const mergedRef = await store.writeMerged(finalMergedText);
|
|
632
812
|
await store.setUsage(usage);
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
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({
|
|
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(
|
|
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
|
|