pi-background-tasks 0.9.0 → 1.0.3
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 +63 -0
- package/PUBLISHING.md +43 -29
- package/README.md +233 -441
- package/TESTING.md +15 -9
- package/TEST_PLAN.md +43 -17
- package/docs/INDEX.md +157 -0
- package/docs/api/eventbus-v1.md +166 -0
- package/docs/assets/architecture.svg +78 -0
- package/docs/assets/footer-dock.svg +47 -0
- package/docs/assets/logo.svg +49 -0
- package/docs/attestations.json +189 -0
- package/docs/choose-a-workflow.md +98 -0
- package/docs/commands/bg-clear.md +70 -0
- package/docs/commands/bg-update.md +82 -0
- package/docs/commands/bg.md +90 -0
- package/docs/commands/fusion-models.md +70 -0
- package/docs/commands/fusion.md +69 -0
- package/docs/commands/jobs.md +74 -0
- package/docs/commands/kill.md +82 -0
- package/docs/commands/logs.md +90 -0
- package/docs/commands/task-manager.md +109 -0
- package/docs/concepts/completion-delivery.md +66 -0
- package/docs/concepts/context-projection-and-budgeting.md +79 -0
- package/docs/getting-started.md +122 -0
- package/docs/manifest.json +1825 -0
- package/docs/operations/configuration.md +110 -0
- package/docs/operations/releasing.md +67 -0
- package/docs/operations/testing.md +101 -0
- package/docs/operations/troubleshooting.md +38 -0
- package/docs/read-before-edit.md +94 -0
- package/docs/reference/runtime-contracts.md +213 -0
- package/docs/reference/shortcuts-and-dock.md +70 -0
- package/docs/subsystems/attested-pi-runs.md +141 -0
- package/docs/subsystems/background-task-runtime.md +85 -0
- package/docs/subsystems/child-launch-durability-and-safety.md +57 -0
- package/docs/subsystems/delegation.md +190 -0
- package/docs/subsystems/docs-freshness-gate.md +26 -0
- package/docs/subsystems/fusion.md +121 -0
- package/docs/subsystems/host-ui-and-telemetry.md +83 -0
- package/docs/tools/bg_delegate.md +193 -0
- package/docs/tools/bg_kill.md +114 -0
- package/docs/tools/bg_logs.md +133 -0
- package/docs/tools/bg_result.md +120 -0
- package/docs/tools/bg_run.md +168 -0
- package/docs/tools/bg_run_pi_attested.md +170 -0
- package/docs/tools/bg_status.md +111 -0
- package/docs/tools/fusion_investigate.md +116 -0
- package/docs/tools/fusion_reason.md +75 -0
- package/docs/tools/fusion_research.md +162 -0
- package/docs/tools/fusion_validate.md +206 -0
- package/logo.png +0 -0
- package/package.json +25 -7
- package/src/core/delegate/budget.ts +1 -1
- package/src/core/delegate/launch.ts +5 -0
- package/src/core/fusion/artifacts.ts +34 -4
- package/src/core/fusion/budget.ts +112 -20
- package/src/core/fusion/child-protocol.ts +82 -0
- package/src/core/fusion/clean-context.ts +91 -0
- package/src/core/fusion/config.ts +124 -35
- package/src/core/fusion/context.ts +29 -7
- package/src/core/fusion/evaluation.ts +392 -15
- package/src/core/fusion/orchestrator.ts +217 -23
- package/src/core/fusion/pi-child.ts +183 -23
- package/src/core/fusion/prompts.ts +39 -26
- package/src/core/fusion/source-policy.ts +257 -0
- package/src/core/fusion/types.ts +156 -11
- package/src/core/fusion/web-fetch.ts +104 -15
- package/src/core/fusion/workflows.ts +119 -65
- package/src/extension.ts +3 -3
- package/src/fusion-child-extension.ts +159 -120
- package/src/fusion-extension.ts +585 -240
- package/src/testing/normalize.ts +0 -22
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomBytes as nodeRandomBytes } from 'node:crypto';
|
|
2
|
+
import { canonicalJson } from '../attested-pi-run.js';
|
|
2
3
|
import { parseJsonText } from '../common.js';
|
|
3
4
|
import { FUSION_BUDGET_POLICY, FusionBudget, assertChildOutputWithinContract } from './budget.js';
|
|
4
5
|
import {
|
|
@@ -9,7 +10,10 @@ import {
|
|
|
9
10
|
import {
|
|
10
11
|
boundedEvaluationErrors,
|
|
11
12
|
formatEvaluationErrors,
|
|
13
|
+
parseFusionValidationCandidateReport,
|
|
14
|
+
renderValidatedFusionValidationReport,
|
|
12
15
|
validateFusionEvaluation,
|
|
16
|
+
validateFusionFindingAccounting,
|
|
13
17
|
} from './evaluation.js';
|
|
14
18
|
import { FusionChildRunError, runPiChild, type RunPiChildOptions } from './pi-child.js';
|
|
15
19
|
import {
|
|
@@ -22,12 +26,14 @@ import {
|
|
|
22
26
|
type AnonymousFusionCandidate,
|
|
23
27
|
} from './prompts.js';
|
|
24
28
|
import {
|
|
25
|
-
|
|
26
|
-
|
|
29
|
+
assertWorkflowCapability,
|
|
30
|
+
fusionWorkflowProfile,
|
|
27
31
|
type FusionWorkflowProfile,
|
|
28
32
|
} from './workflows.js';
|
|
33
|
+
import { buildFusionSourcePolicy, sourcePolicyCanonicalBytes } from './source-policy.js';
|
|
29
34
|
import {
|
|
30
|
-
|
|
35
|
+
FUSION_INPUT_SCHEMA_VERSION,
|
|
36
|
+
FUSION_NO_TOOLS_CAPABILITY,
|
|
31
37
|
FUSION_RESULT_SCHEMA_VERSION,
|
|
32
38
|
FusionError,
|
|
33
39
|
addFusionUsage,
|
|
@@ -46,6 +52,7 @@ import {
|
|
|
46
52
|
type FusionSource,
|
|
47
53
|
type FusionStage,
|
|
48
54
|
type FusionUsage,
|
|
55
|
+
type FusionValidationFindingRecord,
|
|
49
56
|
type ResolvedFusionModel,
|
|
50
57
|
type ResolvedFusionModels,
|
|
51
58
|
} from './types.js';
|
|
@@ -62,11 +69,11 @@ export interface FusionWorkflowInput {
|
|
|
62
69
|
sessionId?: string | undefined;
|
|
63
70
|
canonicalInput: FusionCanonicalInputV3;
|
|
64
71
|
canonicalInputSerialized: string;
|
|
65
|
-
contextLedger
|
|
72
|
+
contextLedger?: FusionContextOmissionLedgerV2 | undefined;
|
|
66
73
|
config: FusionModelConfigV1;
|
|
67
74
|
models: ResolvedFusionModels;
|
|
68
75
|
candidateCapability?: FusionCapability | undefined;
|
|
69
|
-
/**
|
|
76
|
+
/** Mandatory v5 workflow profile. */
|
|
70
77
|
profile?: FusionWorkflowProfile | undefined;
|
|
71
78
|
signal?: AbortSignal | undefined;
|
|
72
79
|
onProgress?: FusionProgressSink | undefined;
|
|
@@ -100,6 +107,33 @@ function errorText(error: unknown): string {
|
|
|
100
107
|
return error instanceof Error ? error.message : String(error);
|
|
101
108
|
}
|
|
102
109
|
|
|
110
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
111
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function hasOnlyKeys(value: Record<string, unknown>, allowed: readonly string[]): boolean {
|
|
115
|
+
const allowedSet = new Set(allowed);
|
|
116
|
+
return Object.keys(value).every((key) => allowedSet.has(key));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function isStrictCleanCanonicalInput(value: unknown): boolean {
|
|
120
|
+
if (!isRecord(value)) return false;
|
|
121
|
+
if (!hasOnlyKeys(value, ['schema_version', 'workflow', 'cwd', 'request', 'context'])) return false;
|
|
122
|
+
const request = value['request'];
|
|
123
|
+
if (!isRecord(request)) return false;
|
|
124
|
+
if (!hasOnlyKeys(request, ['source', 'authority', 'text', 'sha256'])) return false;
|
|
125
|
+
const context = value['context'];
|
|
126
|
+
if (!isRecord(context)) return false;
|
|
127
|
+
if (!hasOnlyKeys(context, ['kind', 'policy_id', 'declared_sources'])) return false;
|
|
128
|
+
if (context['kind'] !== 'clean_task') return false;
|
|
129
|
+
const declaredSources = context['declared_sources'];
|
|
130
|
+
if (!Array.isArray(declaredSources)) return false;
|
|
131
|
+
for (const source of declaredSources) {
|
|
132
|
+
if (!isRecord(source) || !hasOnlyKeys(source, ['url', 'canonical_url', 'purpose', 'sha256'])) return false;
|
|
133
|
+
}
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
|
|
103
137
|
function asFusionError(error: unknown, artifactDir: string, messageOverride?: string): FusionError {
|
|
104
138
|
if (error instanceof FusionError) {
|
|
105
139
|
const details: FusionErrorDetails = {
|
|
@@ -191,6 +225,7 @@ function childOptions(
|
|
|
191
225
|
signal: AbortSignal,
|
|
192
226
|
slot?: CandidateSlot,
|
|
193
227
|
toolCallLogPath?: string,
|
|
228
|
+
sourcePolicy?: { path: string; sha256: string },
|
|
194
229
|
): RunPiChildOptions {
|
|
195
230
|
const out: RunPiChildOptions = {
|
|
196
231
|
stage,
|
|
@@ -204,10 +239,14 @@ function childOptions(
|
|
|
204
239
|
};
|
|
205
240
|
if (slot !== undefined) out.slot = slot;
|
|
206
241
|
if (toolCallLogPath !== undefined) out.toolCallLogPath = toolCallLogPath;
|
|
242
|
+
if (sourcePolicy !== undefined) out.sourcePolicy = sourcePolicy;
|
|
207
243
|
return out;
|
|
208
244
|
}
|
|
209
245
|
|
|
210
|
-
function parseEvaluationAttempt(
|
|
246
|
+
function parseEvaluationAttempt(
|
|
247
|
+
text: string,
|
|
248
|
+
expectedValidationFindings: readonly FusionValidationFindingRecord[] | undefined,
|
|
249
|
+
): {
|
|
211
250
|
evaluation: FusionEvaluationV1 | undefined;
|
|
212
251
|
errors: readonly string[];
|
|
213
252
|
} {
|
|
@@ -221,8 +260,21 @@ function parseEvaluationAttempt(text: string): {
|
|
|
221
260
|
};
|
|
222
261
|
}
|
|
223
262
|
const result = validateFusionEvaluation(parsed);
|
|
224
|
-
if (result.ok) return { evaluation:
|
|
225
|
-
|
|
263
|
+
if (!result.ok) return { evaluation: undefined, errors: result.errors };
|
|
264
|
+
if (
|
|
265
|
+
expectedValidationFindings === undefined &&
|
|
266
|
+
result.value.validation_accounting !== undefined
|
|
267
|
+
) {
|
|
268
|
+
return {
|
|
269
|
+
evaluation: undefined,
|
|
270
|
+
errors: ['evaluation.validation_accounting is permitted only for fusion_validate'],
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
if (expectedValidationFindings !== undefined) {
|
|
274
|
+
const accountingErrors = validateEvaluationAccountsForSourceFindings(result.value, expectedValidationFindings);
|
|
275
|
+
if (accountingErrors.length > 0) return { evaluation: undefined, errors: accountingErrors };
|
|
276
|
+
}
|
|
277
|
+
return { evaluation: result.value, errors: [] };
|
|
226
278
|
}
|
|
227
279
|
|
|
228
280
|
function randomIndex(limit: number, randomBytes: FusionRandomBytes): number {
|
|
@@ -318,6 +370,63 @@ function anonymousCandidates(
|
|
|
318
370
|
};
|
|
319
371
|
}
|
|
320
372
|
|
|
373
|
+
interface ValidationSourceData {
|
|
374
|
+
findings: readonly FusionValidationFindingRecord[];
|
|
375
|
+
verified: readonly string[];
|
|
376
|
+
limitations: readonly string[];
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function validationSourceData(candidates: readonly [AnonymousFusionCandidate, AnonymousFusionCandidate, AnonymousFusionCandidate]): ValidationSourceData {
|
|
380
|
+
const findings: FusionValidationFindingRecord[] = [];
|
|
381
|
+
const verified: string[] = [];
|
|
382
|
+
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);
|
|
387
|
+
limitations.push(...report.limitations);
|
|
388
|
+
}
|
|
389
|
+
return { findings, verified, limitations };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function validateEvaluationAccountsForSourceFindings(
|
|
393
|
+
evaluation: FusionEvaluationV1,
|
|
394
|
+
sourceFindings: readonly FusionValidationFindingRecord[],
|
|
395
|
+
): readonly string[] {
|
|
396
|
+
const errors: string[] = [];
|
|
397
|
+
const accounting = evaluation.validation_accounting;
|
|
398
|
+
if (accounting === undefined) {
|
|
399
|
+
return ['validation evaluator output must include validation_accounting'];
|
|
400
|
+
}
|
|
401
|
+
const expected = sourceFindings.map((finding) => canonicalJson(finding)).sort();
|
|
402
|
+
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');
|
|
405
|
+
}
|
|
406
|
+
errors.push(...validateFusionFindingAccounting(accounting));
|
|
407
|
+
return errors;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
function resolveRunProfile(input: FusionWorkflowInput): FusionWorkflowProfile {
|
|
412
|
+
if (input.profile !== undefined) return fusionWorkflowProfile(input.profile.id);
|
|
413
|
+
const workflow = input.canonicalInput.workflow;
|
|
414
|
+
const contextKind = input.canonicalInput.context?.kind;
|
|
415
|
+
if (workflow !== undefined && workflow !== 'reason') {
|
|
416
|
+
throw new FusionError(`fusion workflow profile is required for ${workflow} runs`, {
|
|
417
|
+
code: 'orchestration_failed',
|
|
418
|
+
childCreated: false,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
if (contextKind === 'clean_task') {
|
|
422
|
+
throw new FusionError('fusion workflow profile is required for clean-task runs', {
|
|
423
|
+
code: 'orchestration_failed',
|
|
424
|
+
childCreated: false,
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
return fusionWorkflowProfile('reason');
|
|
428
|
+
}
|
|
429
|
+
|
|
321
430
|
export class FusionOrchestrator {
|
|
322
431
|
private readonly childRunner: FusionChildRunner;
|
|
323
432
|
private readonly randomBytes: FusionRandomBytes;
|
|
@@ -334,11 +443,28 @@ export class FusionOrchestrator {
|
|
|
334
443
|
}
|
|
335
444
|
|
|
336
445
|
async run(input: FusionWorkflowInput): Promise<FusionRunResult> {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
446
|
+
if (input.canonicalInput.schema_version !== FUSION_INPUT_SCHEMA_VERSION) {
|
|
447
|
+
throw new FusionError('fusion orchestrator accepts only v5 canonical input', {
|
|
448
|
+
code: 'orchestration_failed',
|
|
449
|
+
childCreated: false,
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
const profile = resolveRunProfile(input);
|
|
453
|
+
const inputWorkflow = input.canonicalInput.workflow ?? profile.id;
|
|
454
|
+
const inputContextKind = input.canonicalInput.context?.kind ?? 'session_projection';
|
|
455
|
+
if (inputWorkflow !== profile.id || inputContextKind !== profile.contextKind) {
|
|
456
|
+
throw new FusionError(
|
|
457
|
+
`fusion workflow profile ${profile.id} is incompatible with canonical input workflow=${String(inputWorkflow)} context=${String(inputContextKind)}`,
|
|
458
|
+
{ code: 'orchestration_failed', childCreated: false },
|
|
459
|
+
);
|
|
460
|
+
}
|
|
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
|
+
});
|
|
466
|
+
}
|
|
467
|
+
const candidateCapability = assertWorkflowCapability(profile, input.candidateCapability);
|
|
342
468
|
const storeOptions: CreateFusionArtifactStoreOptions = {
|
|
343
469
|
cwd: input.cwd,
|
|
344
470
|
profile,
|
|
@@ -347,24 +473,63 @@ export class FusionOrchestrator {
|
|
|
347
473
|
models: input.models,
|
|
348
474
|
capabilities: {
|
|
349
475
|
candidate: candidateCapability,
|
|
350
|
-
evaluation:
|
|
351
|
-
merge:
|
|
476
|
+
evaluation: FUSION_NO_TOOLS_CAPABILITY,
|
|
477
|
+
merge: FUSION_NO_TOOLS_CAPABILITY,
|
|
352
478
|
},
|
|
353
479
|
};
|
|
354
480
|
if (input.sessionId !== undefined) storeOptions.sessionId = input.sessionId;
|
|
355
481
|
if (this.now !== undefined) storeOptions.now = this.now;
|
|
482
|
+
let serializedParsed: unknown;
|
|
483
|
+
try {
|
|
484
|
+
serializedParsed = parseJsonText(input.canonicalInputSerialized);
|
|
485
|
+
} catch (error) {
|
|
486
|
+
throw new FusionError(`fusion canonical input artifact is not valid JSON: ${errorText(error)}`, {
|
|
487
|
+
code: 'orchestration_failed',
|
|
488
|
+
childCreated: false,
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
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
|
+
});
|
|
496
|
+
}
|
|
356
497
|
const store = await this.createArtifactStore(storeOptions);
|
|
357
498
|
input.onProgress?.({ type: 'state', state: 'initializing' });
|
|
358
499
|
const usage = createEmptyFusionUsage();
|
|
359
500
|
const calibrationWarnings: FusionCalibrationViolation[] = [];
|
|
360
501
|
try {
|
|
361
502
|
await store.writeCanonicalInput(input.canonicalInputSerialized);
|
|
362
|
-
|
|
503
|
+
if (inputContextKind === 'session_projection') {
|
|
504
|
+
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
|
+
});
|
|
509
|
+
}
|
|
510
|
+
await store.writeContextLedger(input.contextLedger);
|
|
511
|
+
} else if (input.contextLedger !== undefined) {
|
|
512
|
+
throw new FusionError('clean-task fusion input must not carry a parent omission ledger', {
|
|
513
|
+
code: 'orchestration_failed',
|
|
514
|
+
childCreated: false,
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
if (profile.id === 'research') {
|
|
518
|
+
const cleanContext = input.canonicalInput.context;
|
|
519
|
+
if (cleanContext?.kind !== 'clean_task') {
|
|
520
|
+
throw new FusionError('research workflow requires a clean-task canonical input', {
|
|
521
|
+
code: 'orchestration_failed',
|
|
522
|
+
childCreated: false,
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
const policy = buildFusionSourcePolicy(input.cwd, cleanContext.declared_sources);
|
|
526
|
+
await store.writeSourcePolicy(sourcePolicyCanonicalBytes(policy));
|
|
527
|
+
}
|
|
363
528
|
// Deterministic size accounting for the whole workflow, performed before
|
|
364
529
|
// a single child process exists. A rejection here launches zero children.
|
|
365
530
|
const budget = new FusionBudget(
|
|
366
531
|
input.models,
|
|
367
|
-
input.canonicalInput.
|
|
532
|
+
input.canonicalInput.context?.policy_id ?? 'fusion-session-projection-v1',
|
|
368
533
|
candidateCapability,
|
|
369
534
|
profile,
|
|
370
535
|
);
|
|
@@ -393,8 +558,13 @@ export class FusionOrchestrator {
|
|
|
393
558
|
input.onProgress?.({ type: 'state', state: 'candidates_complete' });
|
|
394
559
|
|
|
395
560
|
const shuffled = anonymousCandidates(candidateResults, shuffledSlots(this.randomBytes));
|
|
561
|
+
const validationData = profile.id === 'validate' ? validationSourceData(shuffled.candidates) : undefined;
|
|
396
562
|
await store.setAnonymousMap(shuffled.map);
|
|
397
|
-
const blindInput = buildBlindEvaluationInput(
|
|
563
|
+
const blindInput = buildBlindEvaluationInput(
|
|
564
|
+
input.canonicalInput,
|
|
565
|
+
shuffled.candidates,
|
|
566
|
+
validationData?.findings,
|
|
567
|
+
);
|
|
398
568
|
await store.writeBlindCandidates(buildEvaluationPrompt(blindInput));
|
|
399
569
|
|
|
400
570
|
await store.transition('evaluating');
|
|
@@ -407,6 +577,7 @@ export class FusionOrchestrator {
|
|
|
407
577
|
budget,
|
|
408
578
|
calibrationWarnings,
|
|
409
579
|
profile,
|
|
580
|
+
validationData?.findings,
|
|
410
581
|
);
|
|
411
582
|
await store.writeEvaluationJson(evaluation);
|
|
412
583
|
await store.transition('evaluation_complete');
|
|
@@ -428,7 +599,7 @@ export class FusionOrchestrator {
|
|
|
428
599
|
mergePrompt,
|
|
429
600
|
input.signal ?? new AbortController().signal,
|
|
430
601
|
// Stage policy, not caller input: evaluator and merger are always reasoning-only.
|
|
431
|
-
|
|
602
|
+
FUSION_NO_TOOLS_CAPABILITY,
|
|
432
603
|
undefined,
|
|
433
604
|
'md',
|
|
434
605
|
);
|
|
@@ -445,12 +616,24 @@ export class FusionOrchestrator {
|
|
|
445
616
|
merged,
|
|
446
617
|
);
|
|
447
618
|
assertChildOutputWithinContract('merge', merged.text);
|
|
448
|
-
|
|
619
|
+
let finalMergedText = merged.text;
|
|
620
|
+
if (profile.id === 'validate') {
|
|
621
|
+
const accounting = evaluation.validation_accounting;
|
|
622
|
+
if (accounting === undefined) {
|
|
623
|
+
throw new FusionError('fusion_validate evaluation completed without validation accounting', {
|
|
624
|
+
code: 'evaluation_invalid',
|
|
625
|
+
stage: 'merge',
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
finalMergedText = renderValidatedFusionValidationReport(accounting, validationData);
|
|
629
|
+
}
|
|
630
|
+
if (finalMergedText !== merged.text) assertChildOutputWithinContract('merge', finalMergedText);
|
|
631
|
+
await store.writeMerged(finalMergedText);
|
|
449
632
|
await store.setUsage(usage);
|
|
450
633
|
await store.transition('completed');
|
|
451
634
|
input.onProgress?.({ type: 'completed', runId: store.runId, artifactDir: store.artifactDir });
|
|
452
635
|
return {
|
|
453
|
-
mergedText:
|
|
636
|
+
mergedText: finalMergedText,
|
|
454
637
|
details: {
|
|
455
638
|
schema_version: FUSION_RESULT_SCHEMA_VERSION,
|
|
456
639
|
run_id: store.runId,
|
|
@@ -458,6 +641,8 @@ export class FusionOrchestrator {
|
|
|
458
641
|
source: input.source,
|
|
459
642
|
status: 'completed',
|
|
460
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: [] },
|
|
461
646
|
models: store.snapshot().models,
|
|
462
647
|
evaluator_attempts: store
|
|
463
648
|
.snapshot()
|
|
@@ -596,6 +781,7 @@ export class FusionOrchestrator {
|
|
|
596
781
|
budget: FusionBudget,
|
|
597
782
|
calibrationWarnings: FusionCalibrationViolation[],
|
|
598
783
|
profile: FusionWorkflowProfile,
|
|
784
|
+
expectedValidationFindings: readonly FusionValidationFindingRecord[] | undefined,
|
|
599
785
|
): Promise<FusionEvaluationV1> {
|
|
600
786
|
const firstPrompt = buildEvaluationPrompt(blindInput);
|
|
601
787
|
budget.assertStagePrompt('evaluation', profile.evaluatorSystemPrompt, firstPrompt);
|
|
@@ -609,6 +795,7 @@ export class FusionOrchestrator {
|
|
|
609
795
|
1,
|
|
610
796
|
false,
|
|
611
797
|
profile,
|
|
798
|
+
expectedValidationFindings,
|
|
612
799
|
);
|
|
613
800
|
if (first.evaluation !== undefined) return first.evaluation;
|
|
614
801
|
const errors = boundedEvaluationErrors(first.errors);
|
|
@@ -634,6 +821,7 @@ export class FusionOrchestrator {
|
|
|
634
821
|
2,
|
|
635
822
|
true,
|
|
636
823
|
profile,
|
|
824
|
+
expectedValidationFindings,
|
|
637
825
|
);
|
|
638
826
|
if (second.evaluation !== undefined) return second.evaluation;
|
|
639
827
|
throw new FusionError(
|
|
@@ -656,6 +844,7 @@ export class FusionOrchestrator {
|
|
|
656
844
|
attempt: 1 | 2,
|
|
657
845
|
repair: boolean,
|
|
658
846
|
profile: FusionWorkflowProfile,
|
|
847
|
+
expectedValidationFindings: readonly FusionValidationFindingRecord[] | undefined,
|
|
659
848
|
): Promise<EvaluationAttemptResult> {
|
|
660
849
|
input.onProgress?.({ type: 'evaluation_started', attempt, repair });
|
|
661
850
|
const systemPrompt = repair
|
|
@@ -671,7 +860,7 @@ export class FusionOrchestrator {
|
|
|
671
860
|
prompt,
|
|
672
861
|
input.signal ?? new AbortController().signal,
|
|
673
862
|
// Stage policy, not caller input: evaluator and merger are always reasoning-only.
|
|
674
|
-
|
|
863
|
+
FUSION_NO_TOOLS_CAPABILITY,
|
|
675
864
|
undefined,
|
|
676
865
|
'txt',
|
|
677
866
|
attempt,
|
|
@@ -691,7 +880,7 @@ export class FusionOrchestrator {
|
|
|
691
880
|
await store.setUsage(usage);
|
|
692
881
|
// Bound the evaluator output before it can be embedded in a repair prompt.
|
|
693
882
|
assertChildOutputWithinContract('evaluation', result.text);
|
|
694
|
-
const parsed = parseEvaluationAttempt(result.text);
|
|
883
|
+
const parsed = parseEvaluationAttempt(result.text, expectedValidationFindings);
|
|
695
884
|
return { result, evaluation: parsed.evaluation, errors: parsed.errors };
|
|
696
885
|
}
|
|
697
886
|
|
|
@@ -753,6 +942,10 @@ export class FusionOrchestrator {
|
|
|
753
942
|
capability !== 'reason'
|
|
754
943
|
? store.childToolCallLogPath(stage, slot, logicalAttempt)
|
|
755
944
|
: undefined;
|
|
945
|
+
const sourcePolicy =
|
|
946
|
+
capability === 'research'
|
|
947
|
+
? store.sourcePolicyLaunchReference()
|
|
948
|
+
: undefined;
|
|
756
949
|
try {
|
|
757
950
|
return await this.childRunner(
|
|
758
951
|
childOptions(
|
|
@@ -766,6 +959,7 @@ export class FusionOrchestrator {
|
|
|
766
959
|
signal,
|
|
767
960
|
slot,
|
|
768
961
|
toolCallLogPath,
|
|
962
|
+
sourcePolicy,
|
|
769
963
|
),
|
|
770
964
|
);
|
|
771
965
|
} catch (error) {
|