pi-background-tasks 0.7.7 → 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 +234 -385
- package/TESTING.md +15 -9
- package/TEST_PLAN.md +46 -13
- 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 +29 -6
- package/src/core/delegate/budget.ts +1 -1
- package/src/core/delegate/launch.ts +6 -0
- package/src/core/fusion/artifacts.ts +80 -5
- package/src/core/fusion/budget.ts +129 -28
- 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 +33 -6
- package/src/core/fusion/evaluation.ts +392 -15
- package/src/core/fusion/orchestrator.ts +274 -25
- package/src/core/fusion/pi-child.ts +635 -10
- package/src/core/fusion/prompts.ts +167 -6
- package/src/core/fusion/source-policy.ts +257 -0
- package/src/core/fusion/types.ts +232 -5
- package/src/core/fusion/web-fetch.ts +993 -0
- package/src/core/fusion/workflows.ts +184 -0
- package/src/extension.ts +3 -3
- package/src/fusion-child-extension.ts +370 -54
- package/src/fusion-extension.ts +625 -125
- 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,14 +10,13 @@ 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 {
|
|
16
|
-
FUSION_CANDIDATE_SYSTEM_PROMPT,
|
|
17
|
-
FUSION_EVALUATION_REPAIR_SYSTEM_PROMPT,
|
|
18
|
-
FUSION_EVALUATOR_SYSTEM_PROMPT,
|
|
19
|
-
FUSION_MERGER_SYSTEM_PROMPT,
|
|
20
20
|
buildBlindEvaluationInput,
|
|
21
21
|
buildCandidatePrompt,
|
|
22
22
|
buildEvaluationPrompt,
|
|
@@ -26,11 +26,20 @@ import {
|
|
|
26
26
|
type AnonymousFusionCandidate,
|
|
27
27
|
} from './prompts.js';
|
|
28
28
|
import {
|
|
29
|
+
assertWorkflowCapability,
|
|
30
|
+
fusionWorkflowProfile,
|
|
31
|
+
type FusionWorkflowProfile,
|
|
32
|
+
} from './workflows.js';
|
|
33
|
+
import { buildFusionSourcePolicy, sourcePolicyCanonicalBytes } from './source-policy.js';
|
|
34
|
+
import {
|
|
35
|
+
FUSION_INPUT_SCHEMA_VERSION,
|
|
36
|
+
FUSION_NO_TOOLS_CAPABILITY,
|
|
29
37
|
FUSION_RESULT_SCHEMA_VERSION,
|
|
30
38
|
FusionError,
|
|
31
39
|
addFusionUsage,
|
|
32
40
|
createEmptyFusionUsage,
|
|
33
41
|
type FusionCalibrationViolation,
|
|
42
|
+
type FusionCapability,
|
|
34
43
|
type FusionCanonicalInputV3,
|
|
35
44
|
type FusionCandidateId,
|
|
36
45
|
type FusionContextOmissionLedgerV2,
|
|
@@ -43,6 +52,7 @@ import {
|
|
|
43
52
|
type FusionSource,
|
|
44
53
|
type FusionStage,
|
|
45
54
|
type FusionUsage,
|
|
55
|
+
type FusionValidationFindingRecord,
|
|
46
56
|
type ResolvedFusionModel,
|
|
47
57
|
type ResolvedFusionModels,
|
|
48
58
|
} from './types.js';
|
|
@@ -59,9 +69,12 @@ export interface FusionWorkflowInput {
|
|
|
59
69
|
sessionId?: string | undefined;
|
|
60
70
|
canonicalInput: FusionCanonicalInputV3;
|
|
61
71
|
canonicalInputSerialized: string;
|
|
62
|
-
contextLedger
|
|
72
|
+
contextLedger?: FusionContextOmissionLedgerV2 | undefined;
|
|
63
73
|
config: FusionModelConfigV1;
|
|
64
74
|
models: ResolvedFusionModels;
|
|
75
|
+
candidateCapability?: FusionCapability | undefined;
|
|
76
|
+
/** Mandatory v5 workflow profile. */
|
|
77
|
+
profile?: FusionWorkflowProfile | undefined;
|
|
65
78
|
signal?: AbortSignal | undefined;
|
|
66
79
|
onProgress?: FusionProgressSink | undefined;
|
|
67
80
|
}
|
|
@@ -94,6 +107,33 @@ function errorText(error: unknown): string {
|
|
|
94
107
|
return error instanceof Error ? error.message : String(error);
|
|
95
108
|
}
|
|
96
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
|
+
|
|
97
137
|
function asFusionError(error: unknown, artifactDir: string, messageOverride?: string): FusionError {
|
|
98
138
|
if (error instanceof FusionError) {
|
|
99
139
|
const details: FusionErrorDetails = {
|
|
@@ -179,25 +219,34 @@ function childOptions(
|
|
|
179
219
|
model: ResolvedFusionModel,
|
|
180
220
|
stage: FusionStage,
|
|
181
221
|
attempt: number,
|
|
222
|
+
capability: FusionCapability,
|
|
182
223
|
systemPrompt: string,
|
|
183
224
|
userPrompt: string,
|
|
184
225
|
signal: AbortSignal,
|
|
185
226
|
slot?: CandidateSlot,
|
|
227
|
+
toolCallLogPath?: string,
|
|
228
|
+
sourcePolicy?: { path: string; sha256: string },
|
|
186
229
|
): RunPiChildOptions {
|
|
187
230
|
const out: RunPiChildOptions = {
|
|
188
231
|
stage,
|
|
189
232
|
attempt,
|
|
190
233
|
cwd: input.cwd,
|
|
191
234
|
model,
|
|
235
|
+
capability,
|
|
192
236
|
systemPrompt,
|
|
193
237
|
userPrompt,
|
|
194
238
|
signal,
|
|
195
239
|
};
|
|
196
240
|
if (slot !== undefined) out.slot = slot;
|
|
241
|
+
if (toolCallLogPath !== undefined) out.toolCallLogPath = toolCallLogPath;
|
|
242
|
+
if (sourcePolicy !== undefined) out.sourcePolicy = sourcePolicy;
|
|
197
243
|
return out;
|
|
198
244
|
}
|
|
199
245
|
|
|
200
|
-
function parseEvaluationAttempt(
|
|
246
|
+
function parseEvaluationAttempt(
|
|
247
|
+
text: string,
|
|
248
|
+
expectedValidationFindings: readonly FusionValidationFindingRecord[] | undefined,
|
|
249
|
+
): {
|
|
201
250
|
evaluation: FusionEvaluationV1 | undefined;
|
|
202
251
|
errors: readonly string[];
|
|
203
252
|
} {
|
|
@@ -211,8 +260,21 @@ function parseEvaluationAttempt(text: string): {
|
|
|
211
260
|
};
|
|
212
261
|
}
|
|
213
262
|
const result = validateFusionEvaluation(parsed);
|
|
214
|
-
if (result.ok) return { evaluation:
|
|
215
|
-
|
|
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: [] };
|
|
216
278
|
}
|
|
217
279
|
|
|
218
280
|
function randomIndex(limit: number, randomBytes: FusionRandomBytes): number {
|
|
@@ -308,6 +370,63 @@ function anonymousCandidates(
|
|
|
308
370
|
};
|
|
309
371
|
}
|
|
310
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
|
+
|
|
311
430
|
export class FusionOrchestrator {
|
|
312
431
|
private readonly childRunner: FusionChildRunner;
|
|
313
432
|
private readonly randomBytes: FusionRandomBytes;
|
|
@@ -324,26 +443,95 @@ export class FusionOrchestrator {
|
|
|
324
443
|
}
|
|
325
444
|
|
|
326
445
|
async run(input: FusionWorkflowInput): Promise<FusionRunResult> {
|
|
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);
|
|
327
468
|
const storeOptions: CreateFusionArtifactStoreOptions = {
|
|
328
469
|
cwd: input.cwd,
|
|
470
|
+
profile,
|
|
329
471
|
source: input.source,
|
|
330
472
|
config: input.config,
|
|
331
473
|
models: input.models,
|
|
474
|
+
capabilities: {
|
|
475
|
+
candidate: candidateCapability,
|
|
476
|
+
evaluation: FUSION_NO_TOOLS_CAPABILITY,
|
|
477
|
+
merge: FUSION_NO_TOOLS_CAPABILITY,
|
|
478
|
+
},
|
|
332
479
|
};
|
|
333
480
|
if (input.sessionId !== undefined) storeOptions.sessionId = input.sessionId;
|
|
334
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
|
+
}
|
|
335
497
|
const store = await this.createArtifactStore(storeOptions);
|
|
336
498
|
input.onProgress?.({ type: 'state', state: 'initializing' });
|
|
337
499
|
const usage = createEmptyFusionUsage();
|
|
338
500
|
const calibrationWarnings: FusionCalibrationViolation[] = [];
|
|
339
501
|
try {
|
|
340
502
|
await store.writeCanonicalInput(input.canonicalInputSerialized);
|
|
341
|
-
|
|
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
|
+
}
|
|
342
528
|
// Deterministic size accounting for the whole workflow, performed before
|
|
343
529
|
// a single child process exists. A rejection here launches zero children.
|
|
344
530
|
const budget = new FusionBudget(
|
|
345
531
|
input.models,
|
|
346
|
-
input.canonicalInput.
|
|
532
|
+
input.canonicalInput.context?.policy_id ?? 'fusion-session-projection-v1',
|
|
533
|
+
candidateCapability,
|
|
534
|
+
profile,
|
|
347
535
|
);
|
|
348
536
|
const budgetPlan = budget.plan(input.canonicalInput);
|
|
349
537
|
await store.writeBudgetPlan(budgetPlan);
|
|
@@ -363,13 +551,20 @@ export class FusionOrchestrator {
|
|
|
363
551
|
usage,
|
|
364
552
|
budget,
|
|
365
553
|
calibrationWarnings,
|
|
554
|
+
profile,
|
|
555
|
+
candidateCapability,
|
|
366
556
|
);
|
|
367
557
|
await store.transition('candidates_complete');
|
|
368
558
|
input.onProgress?.({ type: 'state', state: 'candidates_complete' });
|
|
369
559
|
|
|
370
560
|
const shuffled = anonymousCandidates(candidateResults, shuffledSlots(this.randomBytes));
|
|
561
|
+
const validationData = profile.id === 'validate' ? validationSourceData(shuffled.candidates) : undefined;
|
|
371
562
|
await store.setAnonymousMap(shuffled.map);
|
|
372
|
-
const blindInput = buildBlindEvaluationInput(
|
|
563
|
+
const blindInput = buildBlindEvaluationInput(
|
|
564
|
+
input.canonicalInput,
|
|
565
|
+
shuffled.candidates,
|
|
566
|
+
validationData?.findings,
|
|
567
|
+
);
|
|
373
568
|
await store.writeBlindCandidates(buildEvaluationPrompt(blindInput));
|
|
374
569
|
|
|
375
570
|
await store.transition('evaluating');
|
|
@@ -381,6 +576,8 @@ export class FusionOrchestrator {
|
|
|
381
576
|
blindInput,
|
|
382
577
|
budget,
|
|
383
578
|
calibrationWarnings,
|
|
579
|
+
profile,
|
|
580
|
+
validationData?.findings,
|
|
384
581
|
);
|
|
385
582
|
await store.writeEvaluationJson(evaluation);
|
|
386
583
|
await store.transition('evaluation_complete');
|
|
@@ -390,7 +587,7 @@ export class FusionOrchestrator {
|
|
|
390
587
|
input.onProgress?.({ type: 'state', state: 'merging' });
|
|
391
588
|
const mergeInput = buildMergeInput(input.canonicalInput, shuffled.candidates, evaluation);
|
|
392
589
|
const mergePrompt = buildMergePrompt(mergeInput);
|
|
393
|
-
budget.assertStagePrompt('merge',
|
|
590
|
+
budget.assertStagePrompt('merge', profile.mergerSystemPrompt, mergePrompt);
|
|
394
591
|
input.onProgress?.({ type: 'merge_started' });
|
|
395
592
|
const merged = await this.runChildWithRetry(
|
|
396
593
|
input,
|
|
@@ -398,9 +595,11 @@ export class FusionOrchestrator {
|
|
|
398
595
|
usage,
|
|
399
596
|
input.models.merger,
|
|
400
597
|
'merge',
|
|
401
|
-
|
|
598
|
+
profile.mergerSystemPrompt,
|
|
402
599
|
mergePrompt,
|
|
403
600
|
input.signal ?? new AbortController().signal,
|
|
601
|
+
// Stage policy, not caller input: evaluator and merger are always reasoning-only.
|
|
602
|
+
FUSION_NO_TOOLS_CAPABILITY,
|
|
404
603
|
undefined,
|
|
405
604
|
'md',
|
|
406
605
|
);
|
|
@@ -412,23 +611,38 @@ export class FusionOrchestrator {
|
|
|
412
611
|
budget,
|
|
413
612
|
calibrationWarnings,
|
|
414
613
|
'merge',
|
|
415
|
-
|
|
614
|
+
profile.mergerSystemPrompt,
|
|
416
615
|
mergePrompt,
|
|
417
616
|
merged,
|
|
418
617
|
);
|
|
419
618
|
assertChildOutputWithinContract('merge', merged.text);
|
|
420
|
-
|
|
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);
|
|
421
632
|
await store.setUsage(usage);
|
|
422
633
|
await store.transition('completed');
|
|
423
634
|
input.onProgress?.({ type: 'completed', runId: store.runId, artifactDir: store.artifactDir });
|
|
424
635
|
return {
|
|
425
|
-
mergedText:
|
|
636
|
+
mergedText: finalMergedText,
|
|
426
637
|
details: {
|
|
427
638
|
schema_version: FUSION_RESULT_SCHEMA_VERSION,
|
|
428
639
|
run_id: store.runId,
|
|
640
|
+
workflow: profile.id,
|
|
429
641
|
source: input.source,
|
|
430
642
|
status: 'completed',
|
|
431
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: [] },
|
|
432
646
|
models: store.snapshot().models,
|
|
433
647
|
evaluator_attempts: store
|
|
434
648
|
.snapshot()
|
|
@@ -480,14 +694,17 @@ export class FusionOrchestrator {
|
|
|
480
694
|
usage: FusionUsage,
|
|
481
695
|
budget: FusionBudget,
|
|
482
696
|
calibrationWarnings: FusionCalibrationViolation[],
|
|
697
|
+
profile: FusionWorkflowProfile,
|
|
698
|
+
candidateCapability: FusionCapability,
|
|
483
699
|
): Promise<readonly CandidateResult[]> {
|
|
484
700
|
const controller = new AbortController();
|
|
485
701
|
const abortListener = () => controller.abort();
|
|
486
702
|
input.signal?.addEventListener('abort', abortListener, { once: true });
|
|
487
703
|
if (input.signal?.aborted) controller.abort();
|
|
704
|
+
const systemPrompt = profile.candidateSystemPrompt(candidateCapability);
|
|
488
705
|
const prompt = buildCandidatePrompt(input.canonicalInput);
|
|
489
706
|
for (const slot of [1, 2, 3] as const) {
|
|
490
|
-
budget.assertStagePrompt('candidate',
|
|
707
|
+
budget.assertStagePrompt('candidate', systemPrompt, prompt, slot);
|
|
491
708
|
}
|
|
492
709
|
let primaryError: unknown;
|
|
493
710
|
let completed = 0;
|
|
@@ -507,9 +724,10 @@ export class FusionOrchestrator {
|
|
|
507
724
|
usage,
|
|
508
725
|
model,
|
|
509
726
|
'candidate',
|
|
510
|
-
|
|
727
|
+
systemPrompt,
|
|
511
728
|
prompt,
|
|
512
729
|
controller.signal,
|
|
730
|
+
candidateCapability,
|
|
513
731
|
slot,
|
|
514
732
|
'md',
|
|
515
733
|
).then(async (result) => {
|
|
@@ -520,7 +738,7 @@ export class FusionOrchestrator {
|
|
|
520
738
|
budget,
|
|
521
739
|
calibrationWarnings,
|
|
522
740
|
'candidate',
|
|
523
|
-
|
|
741
|
+
systemPrompt,
|
|
524
742
|
prompt,
|
|
525
743
|
result,
|
|
526
744
|
slot,
|
|
@@ -562,9 +780,11 @@ export class FusionOrchestrator {
|
|
|
562
780
|
blindInput: Parameters<typeof buildEvaluationPrompt>[0],
|
|
563
781
|
budget: FusionBudget,
|
|
564
782
|
calibrationWarnings: FusionCalibrationViolation[],
|
|
783
|
+
profile: FusionWorkflowProfile,
|
|
784
|
+
expectedValidationFindings: readonly FusionValidationFindingRecord[] | undefined,
|
|
565
785
|
): Promise<FusionEvaluationV1> {
|
|
566
786
|
const firstPrompt = buildEvaluationPrompt(blindInput);
|
|
567
|
-
budget.assertStagePrompt('evaluation',
|
|
787
|
+
budget.assertStagePrompt('evaluation', profile.evaluatorSystemPrompt, firstPrompt);
|
|
568
788
|
const first = await this.runEvaluationAttempt(
|
|
569
789
|
input,
|
|
570
790
|
store,
|
|
@@ -574,6 +794,8 @@ export class FusionOrchestrator {
|
|
|
574
794
|
firstPrompt,
|
|
575
795
|
1,
|
|
576
796
|
false,
|
|
797
|
+
profile,
|
|
798
|
+
expectedValidationFindings,
|
|
577
799
|
);
|
|
578
800
|
if (first.evaluation !== undefined) return first.evaluation;
|
|
579
801
|
const errors = boundedEvaluationErrors(first.errors);
|
|
@@ -586,7 +808,7 @@ export class FusionOrchestrator {
|
|
|
586
808
|
});
|
|
587
809
|
budget.assertStagePrompt(
|
|
588
810
|
'evaluation_repair',
|
|
589
|
-
|
|
811
|
+
profile.evaluationRepairSystemPrompt,
|
|
590
812
|
repairPrompt,
|
|
591
813
|
);
|
|
592
814
|
const second = await this.runEvaluationAttempt(
|
|
@@ -598,6 +820,8 @@ export class FusionOrchestrator {
|
|
|
598
820
|
repairPrompt,
|
|
599
821
|
2,
|
|
600
822
|
true,
|
|
823
|
+
profile,
|
|
824
|
+
expectedValidationFindings,
|
|
601
825
|
);
|
|
602
826
|
if (second.evaluation !== undefined) return second.evaluation;
|
|
603
827
|
throw new FusionError(
|
|
@@ -619,11 +843,13 @@ export class FusionOrchestrator {
|
|
|
619
843
|
prompt: string,
|
|
620
844
|
attempt: 1 | 2,
|
|
621
845
|
repair: boolean,
|
|
846
|
+
profile: FusionWorkflowProfile,
|
|
847
|
+
expectedValidationFindings: readonly FusionValidationFindingRecord[] | undefined,
|
|
622
848
|
): Promise<EvaluationAttemptResult> {
|
|
623
849
|
input.onProgress?.({ type: 'evaluation_started', attempt, repair });
|
|
624
850
|
const systemPrompt = repair
|
|
625
|
-
?
|
|
626
|
-
:
|
|
851
|
+
? profile.evaluationRepairSystemPrompt
|
|
852
|
+
: profile.evaluatorSystemPrompt;
|
|
627
853
|
const result = await this.runChildWithRetry(
|
|
628
854
|
input,
|
|
629
855
|
store,
|
|
@@ -633,6 +859,8 @@ export class FusionOrchestrator {
|
|
|
633
859
|
systemPrompt,
|
|
634
860
|
prompt,
|
|
635
861
|
input.signal ?? new AbortController().signal,
|
|
862
|
+
// Stage policy, not caller input: evaluator and merger are always reasoning-only.
|
|
863
|
+
FUSION_NO_TOOLS_CAPABILITY,
|
|
636
864
|
undefined,
|
|
637
865
|
'txt',
|
|
638
866
|
attempt,
|
|
@@ -652,7 +880,7 @@ export class FusionOrchestrator {
|
|
|
652
880
|
await store.setUsage(usage);
|
|
653
881
|
// Bound the evaluator output before it can be embedded in a repair prompt.
|
|
654
882
|
assertChildOutputWithinContract('evaluation', result.text);
|
|
655
|
-
const parsed = parseEvaluationAttempt(result.text);
|
|
883
|
+
const parsed = parseEvaluationAttempt(result.text, expectedValidationFindings);
|
|
656
884
|
return { result, evaluation: parsed.evaluation, errors: parsed.errors };
|
|
657
885
|
}
|
|
658
886
|
|
|
@@ -700,6 +928,7 @@ export class FusionOrchestrator {
|
|
|
700
928
|
systemPrompt: string,
|
|
701
929
|
userPrompt: string,
|
|
702
930
|
signal: AbortSignal,
|
|
931
|
+
capability: FusionCapability,
|
|
703
932
|
slot: CandidateSlot | undefined,
|
|
704
933
|
responseKind: 'md' | 'txt',
|
|
705
934
|
fixedAttempt?: 1 | 2,
|
|
@@ -709,9 +938,29 @@ export class FusionOrchestrator {
|
|
|
709
938
|
if (stage === 'candidate' && slot !== undefined) {
|
|
710
939
|
input.onProgress?.({ type: 'candidate_started', slot, attempt: logicalAttempt });
|
|
711
940
|
}
|
|
941
|
+
const toolCallLogPath =
|
|
942
|
+
capability !== 'reason'
|
|
943
|
+
? store.childToolCallLogPath(stage, slot, logicalAttempt)
|
|
944
|
+
: undefined;
|
|
945
|
+
const sourcePolicy =
|
|
946
|
+
capability === 'research'
|
|
947
|
+
? store.sourcePolicyLaunchReference()
|
|
948
|
+
: undefined;
|
|
712
949
|
try {
|
|
713
950
|
return await this.childRunner(
|
|
714
|
-
childOptions(
|
|
951
|
+
childOptions(
|
|
952
|
+
input,
|
|
953
|
+
model,
|
|
954
|
+
stage,
|
|
955
|
+
logicalAttempt,
|
|
956
|
+
capability,
|
|
957
|
+
systemPrompt,
|
|
958
|
+
userPrompt,
|
|
959
|
+
signal,
|
|
960
|
+
slot,
|
|
961
|
+
toolCallLogPath,
|
|
962
|
+
sourcePolicy,
|
|
963
|
+
),
|
|
715
964
|
);
|
|
716
965
|
} catch (error) {
|
|
717
966
|
if (!signal.aborted && retryableSpawn(error, launchTry) && launchTry === 1) continue;
|