pi-background-tasks 1.0.7 → 2.1.1
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 +8 -8
- package/TESTING.md +3 -3
- package/TEST_PLAN.md +6 -6
- 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 +69 -50
- package/docs/operations/configuration.md +5 -3
- package/docs/read-before-edit.md +3 -0
- package/docs/reference/runtime-contracts.md +87 -82
- 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 +5 -5
- package/docs/subsystems/fusion.md +22 -15
- 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/anthropic-attribution.ts +1930 -0
- package/src/core/fusion/artifacts.ts +168 -21
- package/src/core/fusion/budget.ts +23 -23
- package/src/core/fusion/child-protocol.ts +115 -10
- package/src/core/fusion/claude-cache.ts +21 -0
- package/src/core/fusion/config.ts +10 -2
- package/src/core/fusion/orchestrator.ts +281 -77
- package/src/core/fusion/output-contract.ts +34 -0
- package/src/core/fusion/pi-child.ts +420 -12
- package/src/core/fusion/prompts.ts +11 -1
- package/src/core/fusion/result-package.ts +412 -0
- package/src/core/fusion/types.ts +67 -0
- package/src/core/registry.ts +187 -20
- package/src/delegate-extension.ts +130 -24
- package/src/extension.ts +17 -6
- package/src/fusion-child-extension.ts +117 -2
- package/src/fusion-extension.ts +308 -154
|
@@ -5,6 +5,7 @@ import { getAgentDir } from '@earendil-works/pi-coding-agent';
|
|
|
5
5
|
import type { Api, Model } from '@earendil-works/pi-ai';
|
|
6
6
|
import { isJsonObject, parseJsonText, type JsonObject } from '../common.js';
|
|
7
7
|
import { replaceFileDurable } from '../durable-fs.js';
|
|
8
|
+
import { CLAUDE_CODE_200K_SUBSCRIPTION_CONTEXT_WINDOW } from './anthropic-attribution.js';
|
|
8
9
|
import {
|
|
9
10
|
FUSION_MODEL_CONFIG_SCHEMA_VERSION,
|
|
10
11
|
FusionError,
|
|
@@ -158,6 +159,13 @@ function requireContextWindow(model: Model<Api>, label: string): number {
|
|
|
158
159
|
return Math.floor(value);
|
|
159
160
|
}
|
|
160
161
|
|
|
162
|
+
function transportContextWindow(model: Model<Api>, label: string): number {
|
|
163
|
+
const advertised = requireContextWindow(model, label);
|
|
164
|
+
return model.provider === 'anthropic'
|
|
165
|
+
? Math.min(advertised, CLAUDE_CODE_200K_SUBSCRIPTION_CONTEXT_WINDOW)
|
|
166
|
+
: advertised;
|
|
167
|
+
}
|
|
168
|
+
|
|
161
169
|
function requireMaxOutputTokens(model: Model<Api>, label: string): number {
|
|
162
170
|
const value = model.maxTokens;
|
|
163
171
|
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
|
|
@@ -315,7 +323,7 @@ function resolveSelection(
|
|
|
315
323
|
model: available.id,
|
|
316
324
|
qualifiedId,
|
|
317
325
|
thinkingLevel,
|
|
318
|
-
contextWindow:
|
|
326
|
+
contextWindow: transportContextWindow(available, slotLabel),
|
|
319
327
|
maxOutputTokens: requireMaxOutputTokens(available, slotLabel),
|
|
320
328
|
};
|
|
321
329
|
}
|
|
@@ -334,7 +342,7 @@ function resolveSelection(
|
|
|
334
342
|
model: model.id,
|
|
335
343
|
qualifiedId: selection,
|
|
336
344
|
thinkingLevel,
|
|
337
|
-
contextWindow:
|
|
345
|
+
contextWindow: transportContextWindow(model, slotLabel),
|
|
338
346
|
maxOutputTokens: requireMaxOutputTokens(model, slotLabel),
|
|
339
347
|
};
|
|
340
348
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
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
|
-
import { FUSION_BUDGET_POLICY, FusionBudget
|
|
4
|
+
import { FUSION_BUDGET_POLICY, FusionBudget } from './budget.js';
|
|
5
|
+
import { assertChildOutputWithinContract } from './output-contract.js';
|
|
5
6
|
import {
|
|
6
7
|
FusionArtifactStore,
|
|
7
8
|
type CreateFusionArtifactStoreOptions,
|
|
@@ -39,7 +40,9 @@ import {
|
|
|
39
40
|
FUSION_VALIDATE_CANDIDATE_SCHEMA_VERSION,
|
|
40
41
|
FusionError,
|
|
41
42
|
addFusionUsage,
|
|
43
|
+
cloneFusionUsage,
|
|
42
44
|
createEmptyFusionUsage,
|
|
45
|
+
type FusionArtifactManifest,
|
|
43
46
|
type FusionCalibrationViolation,
|
|
44
47
|
type FusionCapability,
|
|
45
48
|
type FusionCanonicalInputV3,
|
|
@@ -50,6 +53,7 @@ import {
|
|
|
50
53
|
type FusionEvaluationV1,
|
|
51
54
|
type FusionModelConfigV1,
|
|
52
55
|
type FusionProgressEvent,
|
|
56
|
+
type FusionRunProgress,
|
|
53
57
|
type FusionRunResult,
|
|
54
58
|
type FusionSource,
|
|
55
59
|
type FusionStage,
|
|
@@ -63,6 +67,12 @@ export type FusionChildRunner = (options: RunPiChildOptions) => Promise<FusionCh
|
|
|
63
67
|
export type FusionProgressSink = (event: FusionProgressEvent) => void;
|
|
64
68
|
export type FusionRandomBytes = (size: number) => Buffer;
|
|
65
69
|
|
|
70
|
+
export interface FusionRunReady {
|
|
71
|
+
runId: string;
|
|
72
|
+
artifactDir: string;
|
|
73
|
+
artifactDirAbs: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
66
76
|
type CandidateSlot = 1 | 2 | 3;
|
|
67
77
|
|
|
68
78
|
export interface FusionWorkflowInput {
|
|
@@ -79,6 +89,12 @@ export interface FusionWorkflowInput {
|
|
|
79
89
|
profile?: FusionWorkflowProfile | undefined;
|
|
80
90
|
signal?: AbortSignal | undefined;
|
|
81
91
|
onProgress?: FusionProgressSink | undefined;
|
|
92
|
+
/**
|
|
93
|
+
* Optional no-child-yet handoff. The orchestrator pauses here after durable
|
|
94
|
+
* preflight and budget admission, allowing a background registry receipt to
|
|
95
|
+
* become durable before candidate launch.
|
|
96
|
+
*/
|
|
97
|
+
onReady?: ((ready: FusionRunReady) => Promise<void>) | undefined;
|
|
82
98
|
}
|
|
83
99
|
|
|
84
100
|
export interface FusionOrchestratorOptions {
|
|
@@ -120,7 +136,8 @@ function hasOnlyKeys(value: Record<string, unknown>, allowed: readonly string[])
|
|
|
120
136
|
|
|
121
137
|
function isStrictCleanCanonicalInput(value: unknown): boolean {
|
|
122
138
|
if (!isRecord(value)) return false;
|
|
123
|
-
if (!hasOnlyKeys(value, ['schema_version', 'workflow', 'cwd', 'request', 'context']))
|
|
139
|
+
if (!hasOnlyKeys(value, ['schema_version', 'workflow', 'cwd', 'request', 'context']))
|
|
140
|
+
return false;
|
|
124
141
|
const request = value['request'];
|
|
125
142
|
if (!isRecord(request)) return false;
|
|
126
143
|
if (!hasOnlyKeys(request, ['source', 'authority', 'text', 'sha256'])) return false;
|
|
@@ -131,7 +148,8 @@ function isStrictCleanCanonicalInput(value: unknown): boolean {
|
|
|
131
148
|
const declaredSources = context['declared_sources'];
|
|
132
149
|
if (!Array.isArray(declaredSources)) return false;
|
|
133
150
|
for (const source of declaredSources) {
|
|
134
|
-
if (!isRecord(source) || !hasOnlyKeys(source, ['url', 'canonical_url', 'purpose', 'sha256']))
|
|
151
|
+
if (!isRecord(source) || !hasOnlyKeys(source, ['url', 'canonical_url', 'purpose', 'sha256']))
|
|
152
|
+
return false;
|
|
135
153
|
}
|
|
136
154
|
return true;
|
|
137
155
|
}
|
|
@@ -148,6 +166,7 @@ function asFusionError(error: unknown, artifactDir: string, messageOverride?: st
|
|
|
148
166
|
if (error.slot !== undefined) details.slot = error.slot;
|
|
149
167
|
if (error.attempt !== undefined) details.attempt = error.attempt;
|
|
150
168
|
if (error.budget !== undefined) details.budget = error.budget;
|
|
169
|
+
if (error.runProgress !== undefined) details.runProgress = error.runProgress;
|
|
151
170
|
return new FusionError(messageOverride ?? error.message, details);
|
|
152
171
|
}
|
|
153
172
|
return new FusionError(messageOverride ?? errorText(error), {
|
|
@@ -157,6 +176,103 @@ function asFusionError(error: unknown, artifactDir: string, messageOverride?: st
|
|
|
157
176
|
});
|
|
158
177
|
}
|
|
159
178
|
|
|
179
|
+
function fusionStageProgress(
|
|
180
|
+
manifest: FusionArtifactManifest,
|
|
181
|
+
stage: FusionStage,
|
|
182
|
+
): FusionRunProgress['candidates'] {
|
|
183
|
+
const attempts = manifest.attempts.filter((attempt) => attempt.stage === stage);
|
|
184
|
+
const created = attempts.filter((attempt) => attempt.child_created).length;
|
|
185
|
+
const completed = attempts.filter(
|
|
186
|
+
(attempt) => attempt.child_created && attempt.status === 'completed',
|
|
187
|
+
).length;
|
|
188
|
+
const failed = attempts.filter(
|
|
189
|
+
(attempt) => attempt.child_created && attempt.status === 'failed',
|
|
190
|
+
).length;
|
|
191
|
+
const cancelled = attempts.filter(
|
|
192
|
+
(attempt) => attempt.child_created && attempt.status === 'cancelled',
|
|
193
|
+
).length;
|
|
194
|
+
const completedByState =
|
|
195
|
+
stage === 'candidate'
|
|
196
|
+
? completed >= 3
|
|
197
|
+
: stage === 'evaluation'
|
|
198
|
+
? manifest.artifacts['evaluation.json'] !== undefined ||
|
|
199
|
+
manifest.state === 'evaluation_complete' ||
|
|
200
|
+
manifest.state === 'merging' ||
|
|
201
|
+
manifest.state === 'completed'
|
|
202
|
+
: manifest.artifacts['merged.md'] !== undefined || manifest.state === 'completed';
|
|
203
|
+
const progress: FusionRunProgress['candidates'] = {
|
|
204
|
+
status: completedByState ? 'completed' : created === 0 ? 'not_started' : 'incomplete',
|
|
205
|
+
attempts_recorded: attempts.length,
|
|
206
|
+
children_created: created,
|
|
207
|
+
children_completed: completed,
|
|
208
|
+
children_failed: failed,
|
|
209
|
+
children_cancelled: cancelled,
|
|
210
|
+
};
|
|
211
|
+
if (stage === 'candidate') {
|
|
212
|
+
const createdSlots = new Set(
|
|
213
|
+
attempts.flatMap((attempt) =>
|
|
214
|
+
attempt.child_created && attempt.slot !== undefined ? [attempt.slot] : [],
|
|
215
|
+
),
|
|
216
|
+
);
|
|
217
|
+
progress.not_started_slots = 3 - createdSlots.size;
|
|
218
|
+
}
|
|
219
|
+
return progress;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function buildFusionRunProgress(manifest: FusionArtifactManifest): FusionRunProgress {
|
|
223
|
+
return {
|
|
224
|
+
manifest_state: manifest.state,
|
|
225
|
+
candidates: fusionStageProgress(manifest, 'candidate'),
|
|
226
|
+
evaluation: fusionStageProgress(manifest, 'evaluation'),
|
|
227
|
+
merge: fusionStageProgress(manifest, 'merge'),
|
|
228
|
+
usage_so_far: cloneFusionUsage(manifest.usage),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function formatFusionRunStage(name: string, stage: FusionRunProgress['candidates']): string {
|
|
233
|
+
const notStarted =
|
|
234
|
+
stage.not_started_slots === undefined
|
|
235
|
+
? ''
|
|
236
|
+
: `, ${String(stage.not_started_slots)} slot(s) not started`;
|
|
237
|
+
return `${name}=${stage.status} (${String(stage.children_created)} created, ${String(stage.children_completed)} completed, ${String(stage.children_failed)} failed, ${String(stage.children_cancelled)} cancelled${notStarted})`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function formatFusionRunProgress(progress: FusionRunProgress): string {
|
|
241
|
+
const usage = progress.usage_so_far;
|
|
242
|
+
const optionalUsage = [
|
|
243
|
+
usage.cacheWrite1h === undefined ? undefined : `cacheWrite1h=${String(usage.cacheWrite1h)}`,
|
|
244
|
+
usage.reasoning === undefined ? undefined : `reasoning=${String(usage.reasoning)}`,
|
|
245
|
+
].filter((value): value is string => value !== undefined);
|
|
246
|
+
const optionalText = optionalUsage.length === 0 ? '' : `, ${optionalUsage.join(', ')}`;
|
|
247
|
+
return (
|
|
248
|
+
`Run progress from durable attempts: ${formatFusionRunStage('candidates', progress.candidates)}; ` +
|
|
249
|
+
`${formatFusionRunStage('evaluation', progress.evaluation)}; ` +
|
|
250
|
+
`${formatFusionRunStage('merge', progress.merge)}. ` +
|
|
251
|
+
`Usage so far: input=${String(usage.input)}, output=${String(usage.output)}, cacheRead=${String(usage.cacheRead)}, cacheWrite=${String(usage.cacheWrite)}${optionalText}, totalTokens=${String(usage.totalTokens)}, ` +
|
|
252
|
+
`cost.input=${String(usage.cost.input)}, cost.output=${String(usage.cost.output)}, cost.cacheRead=${String(usage.cost.cacheRead)}, cost.cacheWrite=${String(usage.cost.cacheWrite)}, cost.total=${String(usage.cost.total)}.`
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function withRunProgress(
|
|
257
|
+
error: unknown,
|
|
258
|
+
artifactDir: string,
|
|
259
|
+
progress: FusionRunProgress,
|
|
260
|
+
): FusionError {
|
|
261
|
+
const base = asFusionError(error, artifactDir);
|
|
262
|
+
const details: FusionErrorDetails = {
|
|
263
|
+
code: base.code,
|
|
264
|
+
artifactDir,
|
|
265
|
+
transient: base.transient,
|
|
266
|
+
childCreated: base.childCreated,
|
|
267
|
+
runProgress: progress,
|
|
268
|
+
};
|
|
269
|
+
if (base.stage !== undefined) details.stage = base.stage;
|
|
270
|
+
if (base.slot !== undefined) details.slot = base.slot;
|
|
271
|
+
if (base.attempt !== undefined) details.attempt = base.attempt;
|
|
272
|
+
if (base.budget !== undefined) details.budget = base.budget;
|
|
273
|
+
return new FusionError(`${base.message}\n${formatFusionRunProgress(progress)}`, details);
|
|
274
|
+
}
|
|
275
|
+
|
|
160
276
|
function withTerminalArtifactFailure(
|
|
161
277
|
error: unknown,
|
|
162
278
|
artifactDir: string,
|
|
@@ -187,7 +303,9 @@ function recordFailureInput(
|
|
|
187
303
|
error: error.message,
|
|
188
304
|
status: error.code === 'child_cancelled' ? 'cancelled' : 'failed',
|
|
189
305
|
responseKind,
|
|
306
|
+
childCreated: error.childCreated,
|
|
190
307
|
usage: error.usage,
|
|
308
|
+
...(error.outputRecovery === undefined ? {} : { outputRecovery: error.outputRecovery }),
|
|
191
309
|
};
|
|
192
310
|
if (slot !== undefined) base.slot = slot;
|
|
193
311
|
if (error.provider !== undefined) base.provider = error.provider;
|
|
@@ -207,6 +325,7 @@ function recordFailureInput(
|
|
|
207
325
|
status:
|
|
208
326
|
error instanceof FusionError && error.code === 'child_cancelled' ? 'cancelled' : 'failed',
|
|
209
327
|
responseKind,
|
|
328
|
+
childCreated: error instanceof FusionError ? error.childCreated : false,
|
|
210
329
|
};
|
|
211
330
|
if (slot !== undefined) base.slot = slot;
|
|
212
331
|
return base;
|
|
@@ -231,6 +350,7 @@ function childOptions(
|
|
|
231
350
|
slot?: CandidateSlot,
|
|
232
351
|
toolCallLogPath?: string,
|
|
233
352
|
sourcePolicy?: { path: string; sha256: string },
|
|
353
|
+
candidateOutputRecoveryPath?: string,
|
|
234
354
|
): RunPiChildOptions {
|
|
235
355
|
const out: RunPiChildOptions = {
|
|
236
356
|
stage,
|
|
@@ -245,6 +365,8 @@ function childOptions(
|
|
|
245
365
|
if (slot !== undefined) out.slot = slot;
|
|
246
366
|
if (toolCallLogPath !== undefined) out.toolCallLogPath = toolCallLogPath;
|
|
247
367
|
if (sourcePolicy !== undefined) out.sourcePolicy = sourcePolicy;
|
|
368
|
+
if (candidateOutputRecoveryPath !== undefined)
|
|
369
|
+
out.candidateOutputRecoveryPath = candidateOutputRecoveryPath;
|
|
248
370
|
return out;
|
|
249
371
|
}
|
|
250
372
|
|
|
@@ -276,7 +398,10 @@ function parseEvaluationAttempt(
|
|
|
276
398
|
};
|
|
277
399
|
}
|
|
278
400
|
if (expectedValidationFindings !== undefined) {
|
|
279
|
-
const accountingErrors = validateEvaluationAccountsForSourceFindings(
|
|
401
|
+
const accountingErrors = validateEvaluationAccountsForSourceFindings(
|
|
402
|
+
result.value,
|
|
403
|
+
expectedValidationFindings,
|
|
404
|
+
);
|
|
280
405
|
if (accountingErrors.length > 0) return { evaluation: undefined, errors: accountingErrors };
|
|
281
406
|
}
|
|
282
407
|
return { evaluation: result.value, errors: [] };
|
|
@@ -376,7 +501,11 @@ function anonymousCandidates(
|
|
|
376
501
|
}
|
|
377
502
|
|
|
378
503
|
interface ValidationSourceData {
|
|
379
|
-
candidates: readonly [
|
|
504
|
+
candidates: readonly [
|
|
505
|
+
AnonymousFusionCandidate,
|
|
506
|
+
AnonymousFusionCandidate,
|
|
507
|
+
AnonymousFusionCandidate,
|
|
508
|
+
];
|
|
380
509
|
findings: readonly FusionValidationFindingRecord[];
|
|
381
510
|
verified: readonly string[];
|
|
382
511
|
limitations: readonly string[];
|
|
@@ -398,7 +527,11 @@ function boundedContractError(error: unknown): string {
|
|
|
398
527
|
* an explicit limitation; two or more still fail the workflow loudly.
|
|
399
528
|
*/
|
|
400
529
|
async function prepareValidationSourceData(
|
|
401
|
-
candidates: readonly [
|
|
530
|
+
candidates: readonly [
|
|
531
|
+
AnonymousFusionCandidate,
|
|
532
|
+
AnonymousFusionCandidate,
|
|
533
|
+
AnonymousFusionCandidate,
|
|
534
|
+
],
|
|
402
535
|
anonymousMap: Record<FusionCandidateId, CandidateSlot>,
|
|
403
536
|
store: FusionArtifactStore,
|
|
404
537
|
): Promise<ValidationSourceData> {
|
|
@@ -415,7 +548,10 @@ async function prepareValidationSourceData(
|
|
|
415
548
|
|
|
416
549
|
for (const candidate of prepared) {
|
|
417
550
|
try {
|
|
418
|
-
const report = parseFusionValidationCandidateReport(
|
|
551
|
+
const report = parseFusionValidationCandidateReport(
|
|
552
|
+
candidate.response,
|
|
553
|
+
candidate.candidate_id,
|
|
554
|
+
);
|
|
419
555
|
findings.push(...report.findings);
|
|
420
556
|
verified.push(...report.verified);
|
|
421
557
|
limitations.push(...report.limitations);
|
|
@@ -435,7 +571,8 @@ async function prepareValidationSourceData(
|
|
|
435
571
|
normalization: recovered.normalization,
|
|
436
572
|
original_sha256: sha256Text(candidate.response),
|
|
437
573
|
forwarded_sha256: sha256Text(recovered.response),
|
|
438
|
-
warning:
|
|
574
|
+
warning:
|
|
575
|
+
'Candidate output violated the bare-JSON contract; a single complete JSON fence was removed and recorded.',
|
|
439
576
|
},
|
|
440
577
|
});
|
|
441
578
|
candidate.response = recovered.response;
|
|
@@ -506,14 +643,18 @@ function validateEvaluationAccountsForSourceFindings(
|
|
|
506
643
|
}
|
|
507
644
|
const expected = sourceFindings.map((finding) => canonicalJson(finding)).sort();
|
|
508
645
|
const actual = accounting.findings.map((finding) => canonicalJson(finding)).sort();
|
|
509
|
-
if (
|
|
510
|
-
|
|
646
|
+
if (
|
|
647
|
+
expected.length !== actual.length ||
|
|
648
|
+
expected.some((value, index) => value !== actual[index])
|
|
649
|
+
) {
|
|
650
|
+
errors.push(
|
|
651
|
+
'validation evaluator validation_accounting.findings must exactly equal host-assigned source findings',
|
|
652
|
+
);
|
|
511
653
|
}
|
|
512
654
|
errors.push(...validateFusionFindingAccounting(accounting));
|
|
513
655
|
return errors;
|
|
514
656
|
}
|
|
515
657
|
|
|
516
|
-
|
|
517
658
|
function resolveRunProfile(input: FusionWorkflowInput): FusionWorkflowProfile {
|
|
518
659
|
if (input.profile !== undefined) return fusionWorkflowProfile(input.profile.id);
|
|
519
660
|
const workflow = input.canonicalInput.workflow;
|
|
@@ -564,11 +705,17 @@ export class FusionOrchestrator {
|
|
|
564
705
|
{ code: 'orchestration_failed', childCreated: false },
|
|
565
706
|
);
|
|
566
707
|
}
|
|
567
|
-
if (
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
708
|
+
if (
|
|
709
|
+
profile.contextKind === 'clean_task' &&
|
|
710
|
+
!isStrictCleanCanonicalInput(input.canonicalInput)
|
|
711
|
+
) {
|
|
712
|
+
throw new FusionError(
|
|
713
|
+
'clean-task fusion input must not carry parent context fields and must match the strict clean canonical shape',
|
|
714
|
+
{
|
|
715
|
+
code: 'orchestration_failed',
|
|
716
|
+
childCreated: false,
|
|
717
|
+
},
|
|
718
|
+
);
|
|
572
719
|
}
|
|
573
720
|
const candidateCapability = assertWorkflowCapability(profile, input.candidateCapability);
|
|
574
721
|
const storeOptions: CreateFusionArtifactStoreOptions = {
|
|
@@ -589,16 +736,22 @@ export class FusionOrchestrator {
|
|
|
589
736
|
try {
|
|
590
737
|
serializedParsed = parseJsonText(input.canonicalInputSerialized);
|
|
591
738
|
} catch (error) {
|
|
592
|
-
throw new FusionError(
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
739
|
+
throw new FusionError(
|
|
740
|
+
`fusion canonical input artifact is not valid JSON: ${errorText(error)}`,
|
|
741
|
+
{
|
|
742
|
+
code: 'orchestration_failed',
|
|
743
|
+
childCreated: false,
|
|
744
|
+
},
|
|
745
|
+
);
|
|
596
746
|
}
|
|
597
747
|
if (canonicalJson(serializedParsed) !== canonicalJson(input.canonicalInput)) {
|
|
598
|
-
throw new FusionError(
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
748
|
+
throw new FusionError(
|
|
749
|
+
'fusion canonical input serialized bytes do not match canonical input object',
|
|
750
|
+
{
|
|
751
|
+
code: 'orchestration_failed',
|
|
752
|
+
childCreated: false,
|
|
753
|
+
},
|
|
754
|
+
);
|
|
602
755
|
}
|
|
603
756
|
const store = await this.createArtifactStore(storeOptions);
|
|
604
757
|
input.onProgress?.({ type: 'state', state: 'initializing' });
|
|
@@ -608,10 +761,13 @@ export class FusionOrchestrator {
|
|
|
608
761
|
await store.writeCanonicalInput(input.canonicalInputSerialized);
|
|
609
762
|
if (inputContextKind === 'session_projection') {
|
|
610
763
|
if (input.contextLedger === undefined) {
|
|
611
|
-
throw new FusionError(
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
764
|
+
throw new FusionError(
|
|
765
|
+
'session-projection fusion input requires an omission ledger artifact',
|
|
766
|
+
{
|
|
767
|
+
code: 'orchestration_failed',
|
|
768
|
+
childCreated: false,
|
|
769
|
+
},
|
|
770
|
+
);
|
|
615
771
|
}
|
|
616
772
|
await store.writeContextLedger(input.contextLedger);
|
|
617
773
|
} else if (input.contextLedger !== undefined) {
|
|
@@ -649,6 +805,17 @@ export class FusionOrchestrator {
|
|
|
649
805
|
error: 'fusion budget utilization warning',
|
|
650
806
|
});
|
|
651
807
|
}
|
|
808
|
+
await input.onReady?.({
|
|
809
|
+
runId: store.runId,
|
|
810
|
+
artifactDir: store.artifactDir,
|
|
811
|
+
artifactDirAbs: store.artifactDirAbs,
|
|
812
|
+
});
|
|
813
|
+
if (input.signal?.aborted === true) {
|
|
814
|
+
throw new FusionError('fusion run cancelled before launch', {
|
|
815
|
+
code: 'child_cancelled',
|
|
816
|
+
childCreated: false,
|
|
817
|
+
});
|
|
818
|
+
}
|
|
652
819
|
await store.transition('candidates_running');
|
|
653
820
|
input.onProgress?.({ type: 'state', state: 'candidates_running' });
|
|
654
821
|
const candidateResults = await this.runCandidates(
|
|
@@ -667,9 +834,10 @@ export class FusionOrchestrator {
|
|
|
667
834
|
// Persist the blind mapping before workflow-specific contract parsing
|
|
668
835
|
// so a failed validation remains attributable to its durable slot artifact.
|
|
669
836
|
await store.setAnonymousMap(shuffled.map);
|
|
670
|
-
const validationData =
|
|
671
|
-
|
|
672
|
-
|
|
837
|
+
const validationData =
|
|
838
|
+
profile.id === 'validate'
|
|
839
|
+
? await prepareValidationSourceData(shuffled.candidates, shuffled.map, store)
|
|
840
|
+
: undefined;
|
|
673
841
|
const evaluationCandidates = validationData?.candidates ?? shuffled.candidates;
|
|
674
842
|
const blindInput = buildBlindEvaluationInput(
|
|
675
843
|
input.canonicalInput,
|
|
@@ -715,7 +883,12 @@ export class FusionOrchestrator {
|
|
|
715
883
|
'md',
|
|
716
884
|
);
|
|
717
885
|
addFusionUsage(usage, merged.usage);
|
|
718
|
-
await store.recordChildAttempt({
|
|
886
|
+
await store.recordChildAttempt({
|
|
887
|
+
result: merged,
|
|
888
|
+
systemPrompt: profile.mergerSystemPrompt,
|
|
889
|
+
prompt: mergePrompt,
|
|
890
|
+
responseKind: 'md',
|
|
891
|
+
});
|
|
719
892
|
await this.recordCalibrationObservation(
|
|
720
893
|
input,
|
|
721
894
|
store,
|
|
@@ -731,52 +904,67 @@ export class FusionOrchestrator {
|
|
|
731
904
|
if (profile.id === 'validate') {
|
|
732
905
|
const accounting = evaluation.validation_accounting;
|
|
733
906
|
if (accounting === undefined) {
|
|
734
|
-
throw new FusionError(
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
907
|
+
throw new FusionError(
|
|
908
|
+
'fusion_validate evaluation completed without validation accounting',
|
|
909
|
+
{
|
|
910
|
+
code: 'evaluation_invalid',
|
|
911
|
+
stage: 'merge',
|
|
912
|
+
},
|
|
913
|
+
);
|
|
738
914
|
}
|
|
739
915
|
finalMergedText = renderValidatedFusionValidationReport(accounting, validationData);
|
|
740
916
|
}
|
|
741
|
-
if (finalMergedText !== merged.text)
|
|
742
|
-
|
|
917
|
+
if (finalMergedText !== merged.text)
|
|
918
|
+
assertChildOutputWithinContract('merge', finalMergedText);
|
|
919
|
+
const mergedRef = await store.writeMerged(finalMergedText);
|
|
743
920
|
await store.setUsage(usage);
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
921
|
+
const details: FusionRunResult['details'] = {
|
|
922
|
+
schema_version: FUSION_RESULT_SCHEMA_VERSION,
|
|
923
|
+
run_id: store.runId,
|
|
924
|
+
workflow: profile.id,
|
|
925
|
+
source: input.source,
|
|
926
|
+
status: 'completed',
|
|
927
|
+
artifact_dir: store.artifactDir,
|
|
928
|
+
context: {
|
|
929
|
+
kind: inputContextKind,
|
|
930
|
+
policy_id: input.canonicalInput.context?.policy_id ?? 'fusion-session-projection-v1',
|
|
931
|
+
},
|
|
932
|
+
tool_policy: {
|
|
933
|
+
candidate_tools: profile.candidateTools,
|
|
934
|
+
evaluation_tools: [],
|
|
935
|
+
merge_tools: [],
|
|
936
|
+
},
|
|
937
|
+
models: store.snapshot().models,
|
|
938
|
+
evaluator_attempts: store
|
|
939
|
+
.snapshot()
|
|
940
|
+
.attempts.filter((attempt) => attempt.stage === 'evaluation').length,
|
|
941
|
+
usage,
|
|
942
|
+
budget: {
|
|
943
|
+
policy_id: FUSION_BUDGET_POLICY.id,
|
|
944
|
+
calibration_version: budgetPlan.policy.calibration_version,
|
|
945
|
+
route_table: budget.routes,
|
|
946
|
+
rate_sources: budget.resultRateSources,
|
|
947
|
+
unknown_provider_warnings: budget.unknownProviderWarnings,
|
|
948
|
+
calibration_warnings: calibrationWarnings,
|
|
770
949
|
},
|
|
771
950
|
};
|
|
951
|
+
await store.writeCommittedResult(mergedRef, details);
|
|
952
|
+
await store.transition('completed');
|
|
953
|
+
input.onProgress?.({ type: 'completed', runId: store.runId, artifactDir: store.artifactDir });
|
|
954
|
+
return { mergedText: finalMergedText, details };
|
|
772
955
|
} catch (error) {
|
|
773
956
|
const cancelled =
|
|
774
957
|
input.signal?.aborted === true ||
|
|
775
958
|
(error instanceof FusionError && error.code === 'child_cancelled');
|
|
776
|
-
|
|
959
|
+
let terminalError: FusionError;
|
|
777
960
|
try {
|
|
778
961
|
await store.setUsage(usage);
|
|
779
|
-
|
|
962
|
+
terminalError = withRunProgress(
|
|
963
|
+
error,
|
|
964
|
+
store.artifactDir,
|
|
965
|
+
buildFusionRunProgress(store.snapshot()),
|
|
966
|
+
);
|
|
967
|
+
await store.writeError(cancelled ? 'cancelled' : 'failed', terminalError.message);
|
|
780
968
|
} catch (artifactError) {
|
|
781
969
|
throw withTerminalArtifactFailure(error, store.artifactDir, artifactError);
|
|
782
970
|
}
|
|
@@ -785,17 +973,17 @@ export class FusionOrchestrator {
|
|
|
785
973
|
type: 'cancelled',
|
|
786
974
|
runId: store.runId,
|
|
787
975
|
artifactDir: store.artifactDir,
|
|
788
|
-
reason: message,
|
|
976
|
+
reason: terminalError.message,
|
|
789
977
|
});
|
|
790
978
|
} else {
|
|
791
979
|
input.onProgress?.({
|
|
792
980
|
type: 'failed',
|
|
793
981
|
runId: store.runId,
|
|
794
982
|
artifactDir: store.artifactDir,
|
|
795
|
-
error: message,
|
|
983
|
+
error: terminalError.message,
|
|
796
984
|
});
|
|
797
985
|
}
|
|
798
|
-
throw
|
|
986
|
+
throw terminalError;
|
|
799
987
|
}
|
|
800
988
|
}
|
|
801
989
|
|
|
@@ -842,7 +1030,12 @@ export class FusionOrchestrator {
|
|
|
842
1030
|
slot,
|
|
843
1031
|
profile.id === 'validate' ? 'txt' : 'md',
|
|
844
1032
|
).then(async (result) => {
|
|
845
|
-
await store.recordChildAttempt({
|
|
1033
|
+
await store.recordChildAttempt({
|
|
1034
|
+
result,
|
|
1035
|
+
systemPrompt,
|
|
1036
|
+
prompt,
|
|
1037
|
+
responseKind: profile.id === 'validate' ? 'txt' : 'md',
|
|
1038
|
+
});
|
|
846
1039
|
await this.recordCalibrationObservation(
|
|
847
1040
|
input,
|
|
848
1041
|
store,
|
|
@@ -854,12 +1047,12 @@ export class FusionOrchestrator {
|
|
|
854
1047
|
result,
|
|
855
1048
|
slot,
|
|
856
1049
|
);
|
|
857
|
-
// The response
|
|
858
|
-
// answer is preserved
|
|
859
|
-
assertChildOutputWithinContract('candidate', result.text);
|
|
860
|
-
completed += 1;
|
|
1050
|
+
// The response and its consumed usage are durable before the contract
|
|
1051
|
+
// check, so an oversized answer is preserved and accounted rather than lost.
|
|
861
1052
|
addFusionUsage(usage, result.usage);
|
|
862
1053
|
await store.setUsage(usage);
|
|
1054
|
+
assertChildOutputWithinContract('candidate', result.text);
|
|
1055
|
+
completed += 1;
|
|
863
1056
|
input.onProgress?.({ type: 'candidate_completed', slot, completed, total: 3 });
|
|
864
1057
|
return { slot, result };
|
|
865
1058
|
});
|
|
@@ -1054,8 +1247,10 @@ export class FusionOrchestrator {
|
|
|
1054
1247
|
? store.childToolCallLogPath(stage, slot, logicalAttempt)
|
|
1055
1248
|
: undefined;
|
|
1056
1249
|
const sourcePolicy =
|
|
1057
|
-
capability === 'research'
|
|
1058
|
-
|
|
1250
|
+
capability === 'research' ? store.sourcePolicyLaunchReference() : undefined;
|
|
1251
|
+
const candidateOutputRecoveryPath =
|
|
1252
|
+
stage === 'candidate' && slot !== undefined
|
|
1253
|
+
? store.childOutputRecoveryPath(slot, logicalAttempt, responseKind)
|
|
1059
1254
|
: undefined;
|
|
1060
1255
|
try {
|
|
1061
1256
|
return await this.childRunner(
|
|
@@ -1071,13 +1266,22 @@ export class FusionOrchestrator {
|
|
|
1071
1266
|
slot,
|
|
1072
1267
|
toolCallLogPath,
|
|
1073
1268
|
sourcePolicy,
|
|
1269
|
+
candidateOutputRecoveryPath,
|
|
1074
1270
|
),
|
|
1075
1271
|
);
|
|
1076
1272
|
} catch (error) {
|
|
1077
1273
|
if (!signal.aborted && retryableSpawn(error, launchTry) && launchTry === 1) continue;
|
|
1078
1274
|
addFailedChildUsage(usage, error);
|
|
1079
1275
|
await store.recordFailedAttempt(
|
|
1080
|
-
recordFailureInput(
|
|
1276
|
+
recordFailureInput(
|
|
1277
|
+
error,
|
|
1278
|
+
stage,
|
|
1279
|
+
slot,
|
|
1280
|
+
logicalAttempt,
|
|
1281
|
+
systemPrompt,
|
|
1282
|
+
userPrompt,
|
|
1283
|
+
responseKind,
|
|
1284
|
+
),
|
|
1081
1285
|
);
|
|
1082
1286
|
await store.setUsage(usage);
|
|
1083
1287
|
throw error;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { FusionError, type FusionStage } from './types.js';
|
|
2
|
+
|
|
3
|
+
/** Hard output contracts are measured over the JSON rendering embedded downstream. */
|
|
4
|
+
export const FUSION_CANDIDATE_MAX_OUTPUT_BYTES = 48 * 1024;
|
|
5
|
+
export const FUSION_EVALUATION_MAX_OUTPUT_BYTES = 64 * 1024;
|
|
6
|
+
export const FUSION_MERGE_MAX_OUTPUT_BYTES = 64 * 1024;
|
|
7
|
+
export const FUSION_DIAGNOSTICS_MAX_BYTES = 8 * 1024;
|
|
8
|
+
|
|
9
|
+
const FUSION_CANDIDATE_MAX_OUTPUT_BYTES_DISPLAY =
|
|
10
|
+
FUSION_CANDIDATE_MAX_OUTPUT_BYTES.toLocaleString('en-US');
|
|
11
|
+
|
|
12
|
+
export const FUSION_CANDIDATE_OUTPUT_CONTRACT_INSTRUCTION = `Your complete response must be at most ${FUSION_CANDIDATE_MAX_OUTPUT_BYTES_DISPLAY} JSON-rendered UTF-8 bytes. If the requested scope cannot fit, prioritize the most important findings and explicitly state limitations.`;
|
|
13
|
+
|
|
14
|
+
export const FUSION_CANDIDATE_OUTPUT_COMPRESSION_PROMPT = `Compress and restructure only your immediately previous answer so the complete replacement is at most ${FUSION_CANDIDATE_MAX_OUTPUT_BYTES_DISPLAY} JSON-rendered UTF-8 bytes. Do not investigate again, do not use tools, and do not add new evidence. Preserve the most important findings and evidence already present, state material limitations, obey the original output format, and output only the replacement answer.`;
|
|
15
|
+
|
|
16
|
+
export function fusionJsonRenderedTextBytes(text: string): number {
|
|
17
|
+
return Buffer.byteLength(JSON.stringify(text), 'utf8');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function fusionOutputContractBytes(stage: FusionStage): number {
|
|
21
|
+
if (stage === 'candidate') return FUSION_CANDIDATE_MAX_OUTPUT_BYTES;
|
|
22
|
+
if (stage === 'evaluation') return FUSION_EVALUATION_MAX_OUTPUT_BYTES;
|
|
23
|
+
return FUSION_MERGE_MAX_OUTPUT_BYTES;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function assertChildOutputWithinContract(stage: FusionStage, text: string): void {
|
|
27
|
+
const bytes = fusionJsonRenderedTextBytes(text);
|
|
28
|
+
const allowed = fusionOutputContractBytes(stage);
|
|
29
|
+
if (bytes <= allowed) return;
|
|
30
|
+
throw new FusionError(
|
|
31
|
+
`fusion ${stage} response is ${String(bytes)} JSON-rendered bytes, exceeding the ${String(allowed)}-byte output contract for that stage; the response is preserved in the run artifacts and is not forwarded or truncated`,
|
|
32
|
+
{ code: 'child_output_cap', stage, childCreated: true },
|
|
33
|
+
);
|
|
34
|
+
}
|