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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { randomBytes } from 'node:crypto';
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
2
2
|
import { chmod, mkdir } from 'node:fs/promises';
|
|
3
3
|
import { basename, isAbsolute, join, relative, sep } from 'node:path';
|
|
4
4
|
import { canonicalJson, sha256Buffer } from '../attested-pi-run.js';
|
|
@@ -6,6 +6,7 @@ import { sanitizePathSegment } from '../common.js';
|
|
|
6
6
|
import { replaceFileDurable } from '../durable-fs.js';
|
|
7
7
|
import {
|
|
8
8
|
EMPTY_FUSION_USAGE,
|
|
9
|
+
FUSION_COMMITTED_RESULT_SCHEMA_VERSION,
|
|
9
10
|
FUSION_MANIFEST_SCHEMA_VERSION,
|
|
10
11
|
FUSION_VALIDATE_CANDIDATE_CONTRACT_EVENT_SCHEMA_VERSION,
|
|
11
12
|
FusionError,
|
|
@@ -16,9 +17,12 @@ import {
|
|
|
16
17
|
type FusionBudgetPlanV1,
|
|
17
18
|
type FusionCalibrationViolation,
|
|
18
19
|
type FusionCandidateId,
|
|
20
|
+
type FusionCandidateOutputRecovery,
|
|
19
21
|
type FusionCapability,
|
|
20
22
|
type FusionContextOmissionLedgerV2,
|
|
21
23
|
type FusionChildRunResult,
|
|
24
|
+
type FusionCommittedResultV1,
|
|
25
|
+
type FusionResultDetails,
|
|
22
26
|
type FusionModelConfigV1,
|
|
23
27
|
type FusionSource,
|
|
24
28
|
type FusionStage,
|
|
@@ -29,6 +33,10 @@ import {
|
|
|
29
33
|
type ResolvedFusionModels,
|
|
30
34
|
} from './types.js';
|
|
31
35
|
import { fusionWorkflowProfile, type FusionWorkflowProfile } from './workflows.js';
|
|
36
|
+
import {
|
|
37
|
+
FUSION_CANDIDATE_MAX_OUTPUT_BYTES,
|
|
38
|
+
fusionJsonRenderedTextBytes,
|
|
39
|
+
} from './output-contract.js';
|
|
32
40
|
|
|
33
41
|
/**
|
|
34
42
|
* Run ids are prefixed by workflow so an artifact directory is self-describing.
|
|
@@ -57,8 +65,17 @@ interface MutableFusionArtifactManifest {
|
|
|
57
65
|
evaluation: FusionCapability;
|
|
58
66
|
merge: FusionCapability;
|
|
59
67
|
};
|
|
60
|
-
context: {
|
|
61
|
-
|
|
68
|
+
context: {
|
|
69
|
+
kind: import('./types.js').FusionContextKind;
|
|
70
|
+
policy_id: string;
|
|
71
|
+
ledger_artifact?: string;
|
|
72
|
+
source_policy_artifact?: string;
|
|
73
|
+
};
|
|
74
|
+
tool_policy: {
|
|
75
|
+
candidate_tools: readonly string[];
|
|
76
|
+
evaluation_tools: readonly [];
|
|
77
|
+
merge_tools: readonly [];
|
|
78
|
+
};
|
|
62
79
|
usage: FusionUsage;
|
|
63
80
|
attempts: FusionAttemptArtifactRecord[];
|
|
64
81
|
artifacts: Record<string, FusionArtifactRef>;
|
|
@@ -123,7 +140,9 @@ export interface RecordFusionFailedAttemptInput {
|
|
|
123
140
|
stderr: Buffer;
|
|
124
141
|
error: string;
|
|
125
142
|
status: 'failed' | 'cancelled';
|
|
143
|
+
childCreated: boolean;
|
|
126
144
|
responseKind: 'md' | 'txt';
|
|
145
|
+
outputRecovery?: FusionCandidateOutputRecovery;
|
|
127
146
|
provider?: string;
|
|
128
147
|
model?: string;
|
|
129
148
|
qualifiedId?: string;
|
|
@@ -206,7 +225,11 @@ function publicManifest(manifest: MutableFusionArtifactManifest): FusionArtifact
|
|
|
206
225
|
models: manifest.models,
|
|
207
226
|
capabilities: manifest.capabilities,
|
|
208
227
|
context: { ...manifest.context },
|
|
209
|
-
tool_policy: {
|
|
228
|
+
tool_policy: {
|
|
229
|
+
candidate_tools: [...manifest.tool_policy.candidate_tools],
|
|
230
|
+
evaluation_tools: [],
|
|
231
|
+
merge_tools: [],
|
|
232
|
+
},
|
|
210
233
|
usage: cloneFusionUsage(manifest.usage),
|
|
211
234
|
attempts: [...manifest.attempts],
|
|
212
235
|
artifacts: { ...manifest.artifacts },
|
|
@@ -234,6 +257,59 @@ function calibrationViolationName(prefix: string): string {
|
|
|
234
257
|
return `${prefix}.calibration-violation.json`;
|
|
235
258
|
}
|
|
236
259
|
|
|
260
|
+
function oversizedResponseName(prefix: string, kind: 'md' | 'txt'): string {
|
|
261
|
+
return `${prefix}.response.oversized.${kind}`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function validateOutputRecovery(
|
|
265
|
+
recovery: FusionCandidateOutputRecovery,
|
|
266
|
+
replacementText?: string,
|
|
267
|
+
): void {
|
|
268
|
+
if (recovery.limit_bytes !== FUSION_CANDIDATE_MAX_OUTPUT_BYTES) {
|
|
269
|
+
throw errorForArtifact('fusion output recovery limit mismatches the candidate contract');
|
|
270
|
+
}
|
|
271
|
+
const originalBytes = Buffer.from(recovery.original_text, 'utf8');
|
|
272
|
+
if (createHash('sha256').update(originalBytes).digest('hex') !== recovery.original_text_sha256) {
|
|
273
|
+
throw errorForArtifact('fusion output recovery original text hash mismatch');
|
|
274
|
+
}
|
|
275
|
+
if (
|
|
276
|
+
fusionJsonRenderedTextBytes(recovery.original_text) !== recovery.original_json_rendered_bytes
|
|
277
|
+
) {
|
|
278
|
+
throw errorForArtifact('fusion output recovery original JSON-rendered byte count mismatch');
|
|
279
|
+
}
|
|
280
|
+
if (recovery.original_json_rendered_bytes <= recovery.limit_bytes) {
|
|
281
|
+
throw errorForArtifact('fusion output recovery original did not exceed the candidate contract');
|
|
282
|
+
}
|
|
283
|
+
if (replacementText !== undefined) {
|
|
284
|
+
const replacementBytes = fusionJsonRenderedTextBytes(replacementText);
|
|
285
|
+
if (replacementBytes !== recovery.replacement_json_rendered_bytes) {
|
|
286
|
+
throw errorForArtifact(
|
|
287
|
+
'fusion output recovery replacement JSON-rendered byte count mismatch',
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
if (recovery.status === 'completed' && replacementBytes > recovery.limit_bytes) {
|
|
291
|
+
throw errorForArtifact('completed fusion output recovery replacement exceeds the contract');
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function outputRecoveryRecord(
|
|
297
|
+
recovery: FusionCandidateOutputRecovery,
|
|
298
|
+
path: string,
|
|
299
|
+
): NonNullable<FusionAttemptArtifactRecord['output_recovery']> {
|
|
300
|
+
return {
|
|
301
|
+
kind: recovery.kind,
|
|
302
|
+
status: recovery.status,
|
|
303
|
+
limit_bytes: recovery.limit_bytes,
|
|
304
|
+
original_response_path: path,
|
|
305
|
+
original_record_index: recovery.original_record_index,
|
|
306
|
+
replacement_record_index: recovery.replacement_record_index,
|
|
307
|
+
original_json_rendered_bytes: recovery.original_json_rendered_bytes,
|
|
308
|
+
replacement_json_rendered_bytes: recovery.replacement_json_rendered_bytes,
|
|
309
|
+
original_text_sha256: recovery.original_text_sha256,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
237
313
|
function artifactRefSha256Hex(value: string): string {
|
|
238
314
|
const hex = value.startsWith('sha256:') ? value.slice('sha256:'.length) : value;
|
|
239
315
|
if (!/^[0-9a-f]{64}$/u.test(hex)) {
|
|
@@ -295,8 +371,18 @@ export class FusionArtifactStore {
|
|
|
295
371
|
evaluation: 'reason',
|
|
296
372
|
merge: 'reason',
|
|
297
373
|
},
|
|
298
|
-
context: {
|
|
299
|
-
|
|
374
|
+
context: {
|
|
375
|
+
kind: profile.contextKind,
|
|
376
|
+
policy_id:
|
|
377
|
+
profile.contextKind === 'session_projection'
|
|
378
|
+
? 'fusion-session-projection-v1'
|
|
379
|
+
: 'fusion-clean-task-v1',
|
|
380
|
+
},
|
|
381
|
+
tool_policy: {
|
|
382
|
+
candidate_tools: profile.candidateTools,
|
|
383
|
+
evaluation_tools: [],
|
|
384
|
+
merge_tools: [],
|
|
385
|
+
},
|
|
300
386
|
usage: cloneFusionUsage(EMPTY_FUSION_USAGE),
|
|
301
387
|
attempts: [],
|
|
302
388
|
artifacts: {},
|
|
@@ -327,6 +413,12 @@ export class FusionArtifactStore {
|
|
|
327
413
|
return this.artifactPath(`${attemptPrefix(stage, slot, attempt)}.tool-calls.jsonl`);
|
|
328
414
|
}
|
|
329
415
|
|
|
416
|
+
childOutputRecoveryPath(slot: 1 | 2 | 3, attempt: number, responseKind: 'md' | 'txt'): string {
|
|
417
|
+
return this.artifactPath(
|
|
418
|
+
oversizedResponseName(attemptPrefix('candidate', slot, attempt), responseKind),
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
|
|
330
422
|
snapshot(): FusionArtifactManifest {
|
|
331
423
|
return publicManifest(this.manifest);
|
|
332
424
|
}
|
|
@@ -339,11 +431,18 @@ export class FusionArtifactStore {
|
|
|
339
431
|
childCreated: false,
|
|
340
432
|
});
|
|
341
433
|
}
|
|
342
|
-
if (
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
434
|
+
if (
|
|
435
|
+
to === 'completed' &&
|
|
436
|
+
(manifest.artifacts['merged.md'] === undefined ||
|
|
437
|
+
manifest.artifacts['result.json'] === undefined)
|
|
438
|
+
) {
|
|
439
|
+
throw new FusionError(
|
|
440
|
+
'fusion cannot complete before merged.md and result.json are durable',
|
|
441
|
+
{
|
|
442
|
+
code: 'state_transition_invalid',
|
|
443
|
+
childCreated: false,
|
|
444
|
+
},
|
|
445
|
+
);
|
|
347
446
|
}
|
|
348
447
|
manifest.state = to;
|
|
349
448
|
});
|
|
@@ -403,8 +502,21 @@ export class FusionArtifactStore {
|
|
|
403
502
|
await this.writeArtifact('evaluation.json', canonicalJson(value));
|
|
404
503
|
}
|
|
405
504
|
|
|
406
|
-
async writeMerged(text: string): Promise<
|
|
407
|
-
|
|
505
|
+
async writeMerged(text: string): Promise<FusionArtifactRef> {
|
|
506
|
+
return this.writeArtifact('merged.md', text);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
async writeCommittedResult(
|
|
510
|
+
merged: FusionArtifactRef,
|
|
511
|
+
details: FusionResultDetails,
|
|
512
|
+
): Promise<FusionArtifactRef> {
|
|
513
|
+
const value: FusionCommittedResultV1 = {
|
|
514
|
+
schema_version: FUSION_COMMITTED_RESULT_SCHEMA_VERSION,
|
|
515
|
+
run_id: this.runId,
|
|
516
|
+
merged,
|
|
517
|
+
details,
|
|
518
|
+
};
|
|
519
|
+
return this.writeArtifact('result.json', `${canonicalJson(value)}\n`);
|
|
408
520
|
}
|
|
409
521
|
|
|
410
522
|
async writeError(state: Exclude<FusionTerminalState, 'completed'>, error: string): Promise<void> {
|
|
@@ -436,11 +548,22 @@ export class FusionArtifactStore {
|
|
|
436
548
|
input.result.toolCallTrace === undefined
|
|
437
549
|
? undefined
|
|
438
550
|
: await this.writeArtifact(`${prefix}.tool-calls.jsonl`, input.result.toolCallTrace.bytes);
|
|
551
|
+
if (input.result.outputRecovery !== undefined) {
|
|
552
|
+
validateOutputRecovery(input.result.outputRecovery, input.result.text);
|
|
553
|
+
}
|
|
554
|
+
const outputRecoveryRef =
|
|
555
|
+
input.result.outputRecovery === undefined
|
|
556
|
+
? undefined
|
|
557
|
+
: await this.writeArtifact(
|
|
558
|
+
oversizedResponseName(prefix, input.responseKind),
|
|
559
|
+
input.result.outputRecovery.original_text,
|
|
560
|
+
);
|
|
439
561
|
await this.updateManifest((manifest) => {
|
|
440
562
|
const record: FusionAttemptArtifactRecord = {
|
|
441
563
|
stage: input.result.stage,
|
|
442
564
|
attempt: input.result.attempt,
|
|
443
565
|
status: 'completed',
|
|
566
|
+
child_created: true,
|
|
444
567
|
prompt_path: promptRef.path,
|
|
445
568
|
events_path: eventsRef.path,
|
|
446
569
|
stderr_path: stderrRef.path,
|
|
@@ -454,6 +577,12 @@ export class FusionArtifactStore {
|
|
|
454
577
|
record.tool_calls_path = toolCallsRef.path;
|
|
455
578
|
record.tool_calls = { ...input.result.toolCallTrace.summary };
|
|
456
579
|
}
|
|
580
|
+
if (outputRecoveryRef !== undefined && input.result.outputRecovery !== undefined) {
|
|
581
|
+
record.output_recovery = outputRecoveryRecord(
|
|
582
|
+
input.result.outputRecovery,
|
|
583
|
+
outputRecoveryRef.path,
|
|
584
|
+
);
|
|
585
|
+
}
|
|
457
586
|
if (input.result.slot !== undefined) record.slot = input.result.slot;
|
|
458
587
|
manifest.attempts.push(record);
|
|
459
588
|
});
|
|
@@ -466,20 +595,26 @@ export class FusionArtifactStore {
|
|
|
466
595
|
violation: FusionCalibrationViolation;
|
|
467
596
|
}): Promise<FusionArtifactRef> {
|
|
468
597
|
const prefix = attemptPrefix(input.stage, input.slot, input.attempt);
|
|
469
|
-
return this.writeArtifact(
|
|
598
|
+
return this.writeArtifact(
|
|
599
|
+
calibrationViolationName(prefix),
|
|
600
|
+
`${canonicalJson(input.violation)}\n`,
|
|
601
|
+
);
|
|
470
602
|
}
|
|
471
603
|
|
|
472
604
|
async recordValidationCandidateContractEvent(
|
|
473
605
|
input: RecordValidationCandidateContractEventInput,
|
|
474
606
|
): Promise<FusionArtifactRef> {
|
|
475
607
|
const name = `candidate-${String(input.slot)}.output-contract-${input.status}.json`;
|
|
476
|
-
return this.writeArtifact(
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
608
|
+
return this.writeArtifact(
|
|
609
|
+
name,
|
|
610
|
+
`${canonicalJson({
|
|
611
|
+
schema_version: FUSION_VALIDATE_CANDIDATE_CONTRACT_EVENT_SCHEMA_VERSION,
|
|
612
|
+
...input.detail,
|
|
613
|
+
candidate_id: input.candidateId,
|
|
614
|
+
slot: input.slot,
|
|
615
|
+
status: input.status,
|
|
616
|
+
})}\n`,
|
|
617
|
+
);
|
|
483
618
|
}
|
|
484
619
|
|
|
485
620
|
async recordFailedAttempt(input: RecordFusionFailedAttemptInput): Promise<void> {
|
|
@@ -496,11 +631,20 @@ export class FusionArtifactStore {
|
|
|
496
631
|
`${prefix}.response.partial.${input.responseKind}`,
|
|
497
632
|
input.partialResponse,
|
|
498
633
|
);
|
|
634
|
+
if (input.outputRecovery !== undefined) validateOutputRecovery(input.outputRecovery);
|
|
635
|
+
const outputRecoveryRef =
|
|
636
|
+
input.outputRecovery === undefined
|
|
637
|
+
? undefined
|
|
638
|
+
: await this.writeArtifact(
|
|
639
|
+
oversizedResponseName(prefix, input.responseKind),
|
|
640
|
+
input.outputRecovery.original_text,
|
|
641
|
+
);
|
|
499
642
|
await this.updateManifest((manifest) => {
|
|
500
643
|
const record: FusionAttemptArtifactRecord = {
|
|
501
644
|
stage: input.stage,
|
|
502
645
|
attempt: input.attempt,
|
|
503
646
|
status: input.status,
|
|
647
|
+
child_created: input.childCreated,
|
|
504
648
|
prompt_path: promptRef.path,
|
|
505
649
|
events_path: eventsRef.path,
|
|
506
650
|
stderr_path: stderrRef.path,
|
|
@@ -508,6 +652,9 @@ export class FusionArtifactStore {
|
|
|
508
652
|
error: input.error,
|
|
509
653
|
};
|
|
510
654
|
if (partialResponseRef !== undefined) record.partial_response_path = partialResponseRef.path;
|
|
655
|
+
if (outputRecoveryRef !== undefined && input.outputRecovery !== undefined) {
|
|
656
|
+
record.output_recovery = outputRecoveryRecord(input.outputRecovery, outputRecoveryRef.path);
|
|
657
|
+
}
|
|
511
658
|
if (input.provider !== undefined) record.provider = input.provider;
|
|
512
659
|
if (input.model !== undefined) record.model = input.model;
|
|
513
660
|
if (input.qualifiedId !== undefined) record.qualifiedId = input.qualifiedId;
|
|
@@ -23,6 +23,20 @@ import {
|
|
|
23
23
|
type AnonymousFusionCandidate,
|
|
24
24
|
} from './prompts.js';
|
|
25
25
|
import { FUSION_REASON_WORKFLOW, type FusionWorkflowProfile } from './workflows.js';
|
|
26
|
+
import {
|
|
27
|
+
FUSION_CANDIDATE_MAX_OUTPUT_BYTES,
|
|
28
|
+
FUSION_DIAGNOSTICS_MAX_BYTES,
|
|
29
|
+
FUSION_EVALUATION_MAX_OUTPUT_BYTES,
|
|
30
|
+
FUSION_MERGE_MAX_OUTPUT_BYTES,
|
|
31
|
+
} from './output-contract.js';
|
|
32
|
+
export {
|
|
33
|
+
FUSION_CANDIDATE_MAX_OUTPUT_BYTES,
|
|
34
|
+
FUSION_DIAGNOSTICS_MAX_BYTES,
|
|
35
|
+
FUSION_EVALUATION_MAX_OUTPUT_BYTES,
|
|
36
|
+
FUSION_MERGE_MAX_OUTPUT_BYTES,
|
|
37
|
+
assertChildOutputWithinContract,
|
|
38
|
+
fusionOutputContractBytes,
|
|
39
|
+
} from './output-contract.js';
|
|
26
40
|
import {
|
|
27
41
|
FUSION_BUDGET_PLAN_SCHEMA_VERSION,
|
|
28
42
|
FUSION_CALIBRATION_VIOLATION_SCHEMA_VERSION,
|
|
@@ -54,11 +68,6 @@ import {
|
|
|
54
68
|
|
|
55
69
|
export const FUSION_CALIBRATED_BYTES_PER_TOKEN = TOKEN_BUDGET_FAMILY_CALIBRATIONS;
|
|
56
70
|
|
|
57
|
-
export const FUSION_CANDIDATE_MAX_OUTPUT_BYTES = 48 * 1024;
|
|
58
|
-
export const FUSION_EVALUATION_MAX_OUTPUT_BYTES = 64 * 1024;
|
|
59
|
-
export const FUSION_MERGE_MAX_OUTPUT_BYTES = 64 * 1024;
|
|
60
|
-
export const FUSION_DIAGNOSTICS_MAX_BYTES = 8 * 1024;
|
|
61
|
-
|
|
62
71
|
const FUSION_OUTPUT_RESERVE_RATE_X100 = 200;
|
|
63
72
|
export const FUSION_UTILIZATION_WARNING_THRESHOLD_BASIS_POINTS = 8000;
|
|
64
73
|
const BASIS_POINTS_DENOMINATOR = 10_000;
|
|
@@ -243,22 +252,6 @@ export function fusionTokenUpperBound(utf8Bytes: number): number {
|
|
|
243
252
|
}).tokens;
|
|
244
253
|
}
|
|
245
254
|
|
|
246
|
-
export function fusionOutputContractBytes(stage: FusionStage): number {
|
|
247
|
-
if (stage === 'candidate') return FUSION_CANDIDATE_MAX_OUTPUT_BYTES;
|
|
248
|
-
if (stage === 'evaluation') return FUSION_EVALUATION_MAX_OUTPUT_BYTES;
|
|
249
|
-
return FUSION_MERGE_MAX_OUTPUT_BYTES;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
export function assertChildOutputWithinContract(stage: FusionStage, text: string): void {
|
|
253
|
-
const bytes = Buffer.byteLength(JSON.stringify(text), 'utf8');
|
|
254
|
-
const allowed = fusionOutputContractBytes(stage);
|
|
255
|
-
if (bytes <= allowed) return;
|
|
256
|
-
throw new FusionError(
|
|
257
|
-
`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`,
|
|
258
|
-
{ code: 'child_output_cap', stage, childCreated: true },
|
|
259
|
-
);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
255
|
function utf8Bytes(value: string): number {
|
|
263
256
|
return Buffer.byteLength(value, 'utf8');
|
|
264
257
|
}
|
|
@@ -563,6 +556,13 @@ function entryLabel(entry: FusionStageBudgetPlanEntry): string {
|
|
|
563
556
|
return `${entry.budget_stage}${slot}${conditional}`;
|
|
564
557
|
}
|
|
565
558
|
|
|
559
|
+
function blockingChildLabel(entry: FusionStageBudgetPlanEntry): string {
|
|
560
|
+
if (entry.budget_stage === 'candidate') return `candidate-${String(entry.slot ?? 1)}`;
|
|
561
|
+
if (entry.budget_stage === 'evaluation_repair') return 'evaluator-repair';
|
|
562
|
+
if (entry.budget_stage === 'evaluation') return 'evaluator';
|
|
563
|
+
return 'merger';
|
|
564
|
+
}
|
|
565
|
+
|
|
566
566
|
function formatTable(entries: readonly FusionStageBudgetPlanEntry[]): string {
|
|
567
567
|
const lines = entries.map(
|
|
568
568
|
(entry) =>
|
|
@@ -983,10 +983,10 @@ export class FusionBudget {
|
|
|
983
983
|
? ' Dominant byte class is dense ASCII/low-whitespace content; the whitespace gate is a heuristic token-density proxy, not a bound.'
|
|
984
984
|
: ` Dominant byte class is ${dominantByteClass}.`;
|
|
985
985
|
const message =
|
|
986
|
-
`Fusion prompt budget exceeded by ${checkText} before child creation. Primary blocking stage: ${entryLabel(primary)} on route ${primary.route.qualified_id}. ` +
|
|
986
|
+
`Fusion prompt budget exceeded by ${checkText} before ${blockingChildLabel(primary)} child creation. Primary blocking stage: ${entryLabel(primary)} on route ${primary.route.qualified_id}. ` +
|
|
987
987
|
`Forecast ${String(primary.input_utf8_bytes)} UTF-8 bytes (<= ${String(primary.input_only_input_tokens_upper_bound)} input tokens) against ${String(primary.allowed_input_tokens)} allowed input tokens, over by ${String(Math.max(0, tokensOver))} tokens. ` +
|
|
988
988
|
`Estimator: ${rateText}.${routeWarning}${dominantText} ` +
|
|
989
|
-
`
|
|
989
|
+
`The ${blockingChildLabel(primary)} child was not created. Nothing was clipped, dropped, or substituted. Artifact directory: ${artifactDir}.\n` +
|
|
990
990
|
`Per-stage forecast table:\n${formatTable(plan.stages)}\n` +
|
|
991
991
|
`Primary blocker byte composition: ${compositionText}.\n` +
|
|
992
992
|
`Additional blockers: ${additional.length === 0 ? 'none' : additional}.\n` +
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import type { Usage } from '@earendil-works/pi-ai';
|
|
3
3
|
import type { FusionClaudeCacheObservation } from './claude-cache.js';
|
|
4
|
+
import {
|
|
5
|
+
FUSION_CANDIDATE_MAX_OUTPUT_BYTES,
|
|
6
|
+
fusionJsonRenderedTextBytes,
|
|
7
|
+
} from './output-contract.js';
|
|
4
8
|
|
|
5
9
|
export const FUSION_CHILD_RESULT_SCHEMA_VERSION =
|
|
6
|
-
'pi-background-tasks.fusion-child-result.
|
|
10
|
+
'pi-background-tasks.fusion-child-result.v4' as const;
|
|
7
11
|
export const FUSION_CHILD_RESULT_PREFIX = '\u001ePI_FUSION_CHILD_RESULT ';
|
|
8
12
|
export const FUSION_CHILD_SETTLEMENT_SCHEMA_VERSION =
|
|
9
|
-
'pi-background-tasks.fusion-child-settlement.
|
|
13
|
+
'pi-background-tasks.fusion-child-settlement.v3' as const;
|
|
10
14
|
export const FUSION_CHILD_SETTLEMENT_PREFIX = '\u001ePI_FUSION_CHILD_SETTLEMENT ';
|
|
11
15
|
export const FUSION_TOOL_CALL_LOG_PATH_ENV = 'PI_FUSION_TOOL_CALL_LOG_PATH';
|
|
16
|
+
export const FUSION_CANDIDATE_OUTPUT_RECOVERY_PATH_ENV = 'PI_FUSION_CANDIDATE_OUTPUT_RECOVERY_PATH';
|
|
12
17
|
export const FUSION_RESEARCH_ENABLED_ENV = 'PI_FUSION_RESEARCH_ENABLED';
|
|
13
18
|
export const FUSION_SOURCE_POLICY_PATH_ENV = 'PI_FUSION_SOURCE_POLICY_PATH';
|
|
14
19
|
export const FUSION_SOURCE_POLICY_SHA256_ENV = 'PI_FUSION_SOURCE_POLICY_SHA256';
|
|
@@ -63,6 +68,14 @@ export interface FusionChildTextBlockMetadata {
|
|
|
63
68
|
|
|
64
69
|
export type FusionChildResultUsageMetadata = Usage;
|
|
65
70
|
|
|
71
|
+
export type FusionChildOutputRecoveryRole = 'none' | 'oversized_original' | 'replacement';
|
|
72
|
+
|
|
73
|
+
export interface FusionChildOutputContractMetadata {
|
|
74
|
+
json_rendered_bytes: number;
|
|
75
|
+
candidate_limit_bytes: number | null;
|
|
76
|
+
recovery_role: FusionChildOutputRecoveryRole;
|
|
77
|
+
}
|
|
78
|
+
|
|
66
79
|
export interface FusionChildResultMetadata {
|
|
67
80
|
schema_version: typeof FUSION_CHILD_RESULT_SCHEMA_VERSION;
|
|
68
81
|
provider: string;
|
|
@@ -72,6 +85,7 @@ export interface FusionChildResultMetadata {
|
|
|
72
85
|
text_sha256: string;
|
|
73
86
|
usage: FusionChildResultUsageMetadata;
|
|
74
87
|
cache_observation: FusionClaudeCacheObservation;
|
|
88
|
+
output_contract: FusionChildOutputContractMetadata;
|
|
75
89
|
}
|
|
76
90
|
|
|
77
91
|
export type FusionChildSettlementFailureReason =
|
|
@@ -79,7 +93,8 @@ export type FusionChildSettlementFailureReason =
|
|
|
79
93
|
| 'final_not_stop'
|
|
80
94
|
| 'invalid_non_final'
|
|
81
95
|
| 'runtime_guard'
|
|
82
|
-
| 'cache_observation'
|
|
96
|
+
| 'cache_observation'
|
|
97
|
+
| 'output_recovery';
|
|
83
98
|
|
|
84
99
|
export interface FusionChildSettlementRecord {
|
|
85
100
|
schema_version: typeof FUSION_CHILD_SETTLEMENT_SCHEMA_VERSION;
|
|
@@ -89,6 +104,7 @@ export interface FusionChildSettlementRecord {
|
|
|
89
104
|
final_record_index: number | null;
|
|
90
105
|
final_text_sha256: string | null;
|
|
91
106
|
recovered_error_ordinals: number[];
|
|
107
|
+
recovered_output_cap_ordinals: number[];
|
|
92
108
|
failure_reason: FusionChildSettlementFailureReason | null;
|
|
93
109
|
}
|
|
94
110
|
|
|
@@ -126,7 +142,60 @@ export function isRecoverableFusionChildErrorRecord(record: FusionChildResultMet
|
|
|
126
142
|
record.stop_reason === 'error' &&
|
|
127
143
|
record.text_blocks.length === 0 &&
|
|
128
144
|
record.text_sha256 === protocolSha256(Buffer.alloc(0)) &&
|
|
129
|
-
hasZeroUsage(record)
|
|
145
|
+
hasZeroUsage(record) &&
|
|
146
|
+
record.output_contract.recovery_role === 'none'
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function isOversizedOriginal(record: FusionChildResultMetadata): boolean {
|
|
151
|
+
const output = record.output_contract;
|
|
152
|
+
return (
|
|
153
|
+
output.recovery_role === 'oversized_original' &&
|
|
154
|
+
output.candidate_limit_bytes === FUSION_CANDIDATE_MAX_OUTPUT_BYTES &&
|
|
155
|
+
output.json_rendered_bytes > FUSION_CANDIDATE_MAX_OUTPUT_BYTES &&
|
|
156
|
+
record.stop_reason === 'stop'
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function outputRecoveryProtocolInvalid(records: readonly FusionChildResultMetadata[]): boolean {
|
|
161
|
+
const originals = records.flatMap((record, ordinal) =>
|
|
162
|
+
record.output_contract.recovery_role === 'oversized_original' ? [ordinal] : [],
|
|
163
|
+
);
|
|
164
|
+
const replacements = records.flatMap((record, ordinal) =>
|
|
165
|
+
record.output_contract.recovery_role === 'replacement' ? [ordinal] : [],
|
|
166
|
+
);
|
|
167
|
+
for (const record of records) {
|
|
168
|
+
const output = record.output_contract;
|
|
169
|
+
if (
|
|
170
|
+
output.candidate_limit_bytes !== null &&
|
|
171
|
+
output.candidate_limit_bytes !== FUSION_CANDIDATE_MAX_OUTPUT_BYTES
|
|
172
|
+
) {
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
if (output.recovery_role !== 'none' && output.candidate_limit_bytes === null) return true;
|
|
176
|
+
if (output.recovery_role === 'oversized_original' && !isOversizedOriginal(record)) return true;
|
|
177
|
+
}
|
|
178
|
+
if (originals.length === 0 && replacements.length === 0) return false;
|
|
179
|
+
if (originals.length !== 1 || replacements.length !== 1) return true;
|
|
180
|
+
const original = originals[0];
|
|
181
|
+
const replacement = replacements[0];
|
|
182
|
+
return (
|
|
183
|
+
original === undefined ||
|
|
184
|
+
replacement === undefined ||
|
|
185
|
+
original !== records.length - 2 ||
|
|
186
|
+
replacement !== records.length - 1
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function finalCandidateOutputExceedsContract(
|
|
191
|
+
records: readonly FusionChildResultMetadata[],
|
|
192
|
+
): boolean {
|
|
193
|
+
const final = records.at(-1);
|
|
194
|
+
if (final === undefined) return false;
|
|
195
|
+
const output = final.output_contract;
|
|
196
|
+
return (
|
|
197
|
+
output.candidate_limit_bytes === FUSION_CANDIDATE_MAX_OUTPUT_BYTES &&
|
|
198
|
+
output.json_rendered_bytes > FUSION_CANDIDATE_MAX_OUTPUT_BYTES
|
|
130
199
|
);
|
|
131
200
|
}
|
|
132
201
|
|
|
@@ -134,24 +203,36 @@ export function buildFusionChildSettlement(
|
|
|
134
203
|
records: readonly FusionChildResultMetadata[],
|
|
135
204
|
runtimeGuardFailed = false,
|
|
136
205
|
cacheObservationFailed = false,
|
|
206
|
+
outputRecoveryFailed = false,
|
|
137
207
|
): FusionChildSettlementRecord {
|
|
138
208
|
const finalRecordIndex = records.length === 0 ? null : records.length - 1;
|
|
139
209
|
const final = records.at(-1);
|
|
140
210
|
const recoveredErrorOrdinals = records.flatMap((record, ordinal) =>
|
|
141
211
|
ordinal < records.length - 1 && isRecoverableFusionChildErrorRecord(record) ? [ordinal] : [],
|
|
142
212
|
);
|
|
213
|
+
const recoveredOutputCapOrdinals = records.flatMap((record, ordinal) =>
|
|
214
|
+
ordinal < records.length - 1 && isOversizedOriginal(record) ? [ordinal] : [],
|
|
215
|
+
);
|
|
216
|
+
const invalidRecovery = outputRecoveryProtocolInvalid(records);
|
|
143
217
|
const invalidNonFinal = records.some(
|
|
144
218
|
(record, ordinal) =>
|
|
145
219
|
ordinal < records.length - 1 &&
|
|
146
220
|
record.stop_reason !== 'toolUse' &&
|
|
147
|
-
!isRecoverableFusionChildErrorRecord(record)
|
|
221
|
+
!isRecoverableFusionChildErrorRecord(record) &&
|
|
222
|
+
!isOversizedOriginal(record),
|
|
148
223
|
);
|
|
149
224
|
let failureReason: FusionChildSettlementFailureReason | null = null;
|
|
150
225
|
if (runtimeGuardFailed) failureReason = 'runtime_guard';
|
|
151
226
|
else if (cacheObservationFailed) failureReason = 'cache_observation';
|
|
152
227
|
else if (final === undefined) failureReason = 'no_records';
|
|
153
228
|
else if (final.stop_reason !== 'stop') failureReason = 'final_not_stop';
|
|
154
|
-
else if (
|
|
229
|
+
else if (
|
|
230
|
+
outputRecoveryFailed ||
|
|
231
|
+
invalidRecovery ||
|
|
232
|
+
finalCandidateOutputExceedsContract(records)
|
|
233
|
+
) {
|
|
234
|
+
failureReason = 'output_recovery';
|
|
235
|
+
} else if (invalidNonFinal) failureReason = 'invalid_non_final';
|
|
155
236
|
return {
|
|
156
237
|
schema_version: FUSION_CHILD_SETTLEMENT_SCHEMA_VERSION,
|
|
157
238
|
status: failureReason === null ? 'complete' : 'failed',
|
|
@@ -160,6 +241,7 @@ export function buildFusionChildSettlement(
|
|
|
160
241
|
final_record_index: finalRecordIndex,
|
|
161
242
|
final_text_sha256: final?.text_sha256 ?? null,
|
|
162
243
|
recovered_error_ordinals: recoveredErrorOrdinals,
|
|
244
|
+
recovered_output_cap_ordinals: recoveredOutputCapOrdinals,
|
|
163
245
|
failure_reason: failureReason,
|
|
164
246
|
};
|
|
165
247
|
}
|
|
@@ -173,15 +255,33 @@ export function buildFusionChildResultMetadata(
|
|
|
173
255
|
usage: Usage;
|
|
174
256
|
},
|
|
175
257
|
cacheObservation: FusionClaudeCacheObservation,
|
|
258
|
+
outputContract: {
|
|
259
|
+
candidateLimitBytes: number | null;
|
|
260
|
+
recoveryRole: FusionChildOutputRecoveryRole;
|
|
261
|
+
} = { candidateLimitBytes: null, recoveryRole: 'none' },
|
|
176
262
|
): FusionChildResultMetadata {
|
|
263
|
+
if (
|
|
264
|
+
outputContract.candidateLimitBytes !== null &&
|
|
265
|
+
outputContract.candidateLimitBytes !== FUSION_CANDIDATE_MAX_OUTPUT_BYTES
|
|
266
|
+
) {
|
|
267
|
+
throw new Error('fusion child candidate output limit does not match the shared contract');
|
|
268
|
+
}
|
|
269
|
+
if (outputContract.recoveryRole !== 'none' && outputContract.candidateLimitBytes === null) {
|
|
270
|
+
throw new Error('fusion child output recovery role requires the candidate output contract');
|
|
271
|
+
}
|
|
177
272
|
const textBlocks = message.content.flatMap((part) =>
|
|
178
273
|
part.type === 'text' && typeof part.text === 'string' ? [part.text] : [],
|
|
179
274
|
);
|
|
275
|
+
const text = textBlocks.join('');
|
|
180
276
|
const usage: FusionChildResultUsageMetadata = {
|
|
181
277
|
input: message.usage.input,
|
|
182
278
|
output: message.usage.output,
|
|
183
279
|
cacheRead: message.usage.cacheRead,
|
|
184
280
|
cacheWrite: message.usage.cacheWrite,
|
|
281
|
+
...(message.usage.cacheWrite1h === undefined
|
|
282
|
+
? {}
|
|
283
|
+
: { cacheWrite1h: message.usage.cacheWrite1h }),
|
|
284
|
+
...(message.usage.reasoning === undefined ? {} : { reasoning: message.usage.reasoning }),
|
|
185
285
|
totalTokens: message.usage.totalTokens,
|
|
186
286
|
cost: {
|
|
187
287
|
input: message.usage.cost.input,
|
|
@@ -196,12 +296,17 @@ export function buildFusionChildResultMetadata(
|
|
|
196
296
|
provider: message.provider,
|
|
197
297
|
model: message.model,
|
|
198
298
|
stop_reason: message.stopReason,
|
|
199
|
-
text_blocks: textBlocks.map((
|
|
200
|
-
utf8_bytes: Buffer.byteLength(
|
|
201
|
-
sha256: protocolSha256(
|
|
299
|
+
text_blocks: textBlocks.map((blockText) => ({
|
|
300
|
+
utf8_bytes: Buffer.byteLength(blockText, 'utf8'),
|
|
301
|
+
sha256: protocolSha256(blockText),
|
|
202
302
|
})),
|
|
203
|
-
text_sha256: protocolSha256(
|
|
303
|
+
text_sha256: protocolSha256(text),
|
|
204
304
|
usage,
|
|
205
305
|
cache_observation: cacheObservation,
|
|
306
|
+
output_contract: {
|
|
307
|
+
json_rendered_bytes: fusionJsonRenderedTextBytes(text),
|
|
308
|
+
candidate_limit_bytes: outputContract.candidateLimitBytes,
|
|
309
|
+
recovery_role: outputContract.recoveryRole,
|
|
310
|
+
},
|
|
206
311
|
};
|
|
207
312
|
}
|
|
@@ -5,6 +5,7 @@ export const FUSION_CLAUDE_CACHE_OBSERVATION_SCHEMA_VERSION =
|
|
|
5
5
|
export const FUSION_CLAUDE_CACHE_RETENTION_ENV = 'PI_CACHE_RETENTION';
|
|
6
6
|
export const FUSION_CLAUDE_CACHE_DEFAULT_RETENTION = 'long' as const;
|
|
7
7
|
export const FUSION_CLAUDE_CACHE_BREAKPOINT_LIMIT = 4;
|
|
8
|
+
export const FUSION_CLAUDE_PROMPT_CACHING_SCOPE_BETA = 'prompt-caching-scope-2026-01-05' as const;
|
|
8
9
|
|
|
9
10
|
export type FusionClaudeCacheRetention = 'none' | 'short' | 'long';
|
|
10
11
|
export type FusionClaudeCachePolicySource =
|
|
@@ -63,6 +64,26 @@ export function resolveFusionClaudeCachePolicy(env: Readonly<NodeJS.ProcessEnv>
|
|
|
63
64
|
};
|
|
64
65
|
}
|
|
65
66
|
|
|
67
|
+
export function applyFusionClaudePromptCachingScopeHeader(
|
|
68
|
+
headers: Record<string, string | null>,
|
|
69
|
+
): boolean {
|
|
70
|
+
const matchingKey = Object.keys(headers).find((key) => key.toLowerCase() === 'anthropic-beta');
|
|
71
|
+
const existing = matchingKey === undefined ? undefined : headers[matchingKey];
|
|
72
|
+
const values =
|
|
73
|
+
typeof existing === 'string'
|
|
74
|
+
? existing
|
|
75
|
+
.split(',')
|
|
76
|
+
.map((value) => value.trim())
|
|
77
|
+
.filter((value) => value.length > 0)
|
|
78
|
+
: [];
|
|
79
|
+
if (!values.includes(FUSION_CLAUDE_PROMPT_CACHING_SCOPE_BETA)) {
|
|
80
|
+
values.push(FUSION_CLAUDE_PROMPT_CACHING_SCOPE_BETA);
|
|
81
|
+
}
|
|
82
|
+
const targetKey = matchingKey ?? 'anthropic-beta';
|
|
83
|
+
headers[targetKey] = values.join(',');
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
|
|
66
87
|
function validateCacheControl(value: unknown): JsonObject {
|
|
67
88
|
if (!isRecord(value)) {
|
|
68
89
|
throw new Error('Fusion Claude cache_control must be an object');
|