pi-background-tasks 2.0.0 → 2.1.2
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 +2 -2
- package/TESTING.md +13 -12
- package/TEST_PLAN.md +7 -7
- package/docs/manifest.json +14 -4
- package/docs/operations/configuration.md +5 -3
- package/docs/read-before-edit.md +2 -0
- package/docs/reference/runtime-contracts.md +64 -62
- package/docs/subsystems/delegation.md +2 -2
- package/docs/subsystems/docs-freshness-gate.md +3 -3
- package/docs/subsystems/fusion.md +10 -7
- package/package.json +6 -6
- package/src/core/delegate/hook-contract.ts +14 -13
- package/src/core/fusion/anthropic-attribution.ts +1930 -0
- package/src/core/fusion/artifacts.ts +96 -1
- 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 +128 -10
- 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 +30 -3
- package/src/core/fusion/types.ts +58 -0
- package/src/delegate-child-extension.ts +12 -13
- package/src/fusion-child-extension.ts +117 -2
|
@@ -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';
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
type FusionBudgetPlanV1,
|
|
18
18
|
type FusionCalibrationViolation,
|
|
19
19
|
type FusionCandidateId,
|
|
20
|
+
type FusionCandidateOutputRecovery,
|
|
20
21
|
type FusionCapability,
|
|
21
22
|
type FusionContextOmissionLedgerV2,
|
|
22
23
|
type FusionChildRunResult,
|
|
@@ -32,6 +33,10 @@ import {
|
|
|
32
33
|
type ResolvedFusionModels,
|
|
33
34
|
} from './types.js';
|
|
34
35
|
import { fusionWorkflowProfile, type FusionWorkflowProfile } from './workflows.js';
|
|
36
|
+
import {
|
|
37
|
+
FUSION_CANDIDATE_MAX_OUTPUT_BYTES,
|
|
38
|
+
fusionJsonRenderedTextBytes,
|
|
39
|
+
} from './output-contract.js';
|
|
35
40
|
|
|
36
41
|
/**
|
|
37
42
|
* Run ids are prefixed by workflow so an artifact directory is self-describing.
|
|
@@ -135,7 +140,9 @@ export interface RecordFusionFailedAttemptInput {
|
|
|
135
140
|
stderr: Buffer;
|
|
136
141
|
error: string;
|
|
137
142
|
status: 'failed' | 'cancelled';
|
|
143
|
+
childCreated: boolean;
|
|
138
144
|
responseKind: 'md' | 'txt';
|
|
145
|
+
outputRecovery?: FusionCandidateOutputRecovery;
|
|
139
146
|
provider?: string;
|
|
140
147
|
model?: string;
|
|
141
148
|
qualifiedId?: string;
|
|
@@ -250,6 +257,59 @@ function calibrationViolationName(prefix: string): string {
|
|
|
250
257
|
return `${prefix}.calibration-violation.json`;
|
|
251
258
|
}
|
|
252
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
|
+
|
|
253
313
|
function artifactRefSha256Hex(value: string): string {
|
|
254
314
|
const hex = value.startsWith('sha256:') ? value.slice('sha256:'.length) : value;
|
|
255
315
|
if (!/^[0-9a-f]{64}$/u.test(hex)) {
|
|
@@ -353,6 +413,12 @@ export class FusionArtifactStore {
|
|
|
353
413
|
return this.artifactPath(`${attemptPrefix(stage, slot, attempt)}.tool-calls.jsonl`);
|
|
354
414
|
}
|
|
355
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
|
+
|
|
356
422
|
snapshot(): FusionArtifactManifest {
|
|
357
423
|
return publicManifest(this.manifest);
|
|
358
424
|
}
|
|
@@ -482,11 +548,22 @@ export class FusionArtifactStore {
|
|
|
482
548
|
input.result.toolCallTrace === undefined
|
|
483
549
|
? undefined
|
|
484
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
|
+
);
|
|
485
561
|
await this.updateManifest((manifest) => {
|
|
486
562
|
const record: FusionAttemptArtifactRecord = {
|
|
487
563
|
stage: input.result.stage,
|
|
488
564
|
attempt: input.result.attempt,
|
|
489
565
|
status: 'completed',
|
|
566
|
+
child_created: true,
|
|
490
567
|
prompt_path: promptRef.path,
|
|
491
568
|
events_path: eventsRef.path,
|
|
492
569
|
stderr_path: stderrRef.path,
|
|
@@ -500,6 +577,12 @@ export class FusionArtifactStore {
|
|
|
500
577
|
record.tool_calls_path = toolCallsRef.path;
|
|
501
578
|
record.tool_calls = { ...input.result.toolCallTrace.summary };
|
|
502
579
|
}
|
|
580
|
+
if (outputRecoveryRef !== undefined && input.result.outputRecovery !== undefined) {
|
|
581
|
+
record.output_recovery = outputRecoveryRecord(
|
|
582
|
+
input.result.outputRecovery,
|
|
583
|
+
outputRecoveryRef.path,
|
|
584
|
+
);
|
|
585
|
+
}
|
|
503
586
|
if (input.result.slot !== undefined) record.slot = input.result.slot;
|
|
504
587
|
manifest.attempts.push(record);
|
|
505
588
|
});
|
|
@@ -548,11 +631,20 @@ export class FusionArtifactStore {
|
|
|
548
631
|
`${prefix}.response.partial.${input.responseKind}`,
|
|
549
632
|
input.partialResponse,
|
|
550
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
|
+
);
|
|
551
642
|
await this.updateManifest((manifest) => {
|
|
552
643
|
const record: FusionAttemptArtifactRecord = {
|
|
553
644
|
stage: input.stage,
|
|
554
645
|
attempt: input.attempt,
|
|
555
646
|
status: input.status,
|
|
647
|
+
child_created: input.childCreated,
|
|
556
648
|
prompt_path: promptRef.path,
|
|
557
649
|
events_path: eventsRef.path,
|
|
558
650
|
stderr_path: stderrRef.path,
|
|
@@ -560,6 +652,9 @@ export class FusionArtifactStore {
|
|
|
560
652
|
error: input.error,
|
|
561
653
|
};
|
|
562
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
|
+
}
|
|
563
658
|
if (input.provider !== undefined) record.provider = input.provider;
|
|
564
659
|
if (input.model !== undefined) record.model = input.model;
|
|
565
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');
|
|
@@ -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
|
}
|