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,5 @@
|
|
|
1
1
|
import { canonicalJson } from '../attested-pi-run.js';
|
|
2
|
+
import { FUSION_CANDIDATE_OUTPUT_CONTRACT_INSTRUCTION } from './output-contract.js';
|
|
2
3
|
import {
|
|
3
4
|
FUSION_EVALUATION_SCHEMA_VERSION,
|
|
4
5
|
FUSION_VALIDATE_CAPABILITY,
|
|
@@ -33,6 +34,8 @@ ${FUSION_CANONICAL_INPUT_GUIDE}
|
|
|
33
34
|
|
|
34
35
|
Produce the strongest direct answer you can for the request using that context.
|
|
35
36
|
|
|
37
|
+
${FUSION_CANDIDATE_OUTPUT_CONTRACT_INSTRUCTION}
|
|
38
|
+
|
|
36
39
|
Do not invent process metadata. Do not mention provider names, model names, slots, or hidden workflow details. Do not specialize the answer; each child receives the same instruction. Output only the answer text.`;
|
|
37
40
|
|
|
38
41
|
export const FUSION_INSPECT_CANONICAL_INPUT_GUIDE = `The JSON input contains only workflow, cwd, request, and clean-task context. context.kind is "clean_task".
|
|
@@ -47,6 +50,8 @@ ${FUSION_INSPECT_CANONICAL_INPUT_GUIDE}
|
|
|
47
50
|
|
|
48
51
|
Produce the strongest direct answer you can for the request using that context.
|
|
49
52
|
|
|
53
|
+
${FUSION_CANDIDATE_OUTPUT_CONTRACT_INSTRUCTION}
|
|
54
|
+
|
|
50
55
|
Do not invent process metadata. Do not mention provider names, model names, slots, or hidden workflow details. Do not specialize the answer; each child receives the same instruction. Output only the answer text.`;
|
|
51
56
|
|
|
52
57
|
export const FUSION_RESEARCH_CANONICAL_INPUT_GUIDE = `The JSON input contains only workflow, cwd, request, and clean-task context. Research uses context.kind "clean_task" with declared_sources: the only initial public URLs fusion_web_fetch may initiate. Redirects are followed only by the fetcher after public-address checks; fusion_web_fetch is targeted URL fetch, not search.
|
|
@@ -61,6 +66,8 @@ ${FUSION_RESEARCH_CANONICAL_INPUT_GUIDE}
|
|
|
61
66
|
|
|
62
67
|
Produce the strongest direct answer you can for the request using that context.
|
|
63
68
|
|
|
69
|
+
${FUSION_CANDIDATE_OUTPUT_CONTRACT_INSTRUCTION}
|
|
70
|
+
|
|
64
71
|
Do not invent process metadata. Do not mention provider names, model names, slots, or hidden workflow details. Do not specialize the answer; each child receives the same instruction. Output only the answer text.`;
|
|
65
72
|
|
|
66
73
|
export function fusionCandidateSystemPrompt(capability: FusionCapability): string {
|
|
@@ -198,6 +205,8 @@ Stay in scope. Validate what the request names. Do not propose unrelated refacto
|
|
|
198
205
|
|
|
199
206
|
Close with what you verified and what you could not cover.
|
|
200
207
|
|
|
208
|
+
${FUSION_CANDIDATE_OUTPUT_CONTRACT_INSTRUCTION}
|
|
209
|
+
|
|
201
210
|
Do not invent process metadata. Do not mention provider names, model names, slots, or hidden workflow details. Do not specialize the report; each child receives the same instruction. Output only the required JSON.`;
|
|
202
211
|
|
|
203
212
|
/**
|
|
@@ -301,7 +310,8 @@ export function buildBlindEvaluationInput(
|
|
|
301
310
|
canonical_input: canonicalInput,
|
|
302
311
|
candidates,
|
|
303
312
|
};
|
|
304
|
-
if (validationSourceFindings !== undefined)
|
|
313
|
+
if (validationSourceFindings !== undefined)
|
|
314
|
+
input.validation_source_findings = validationSourceFindings;
|
|
305
315
|
return input;
|
|
306
316
|
}
|
|
307
317
|
|
|
@@ -61,7 +61,16 @@ function usage(value: unknown, artifactDir: string): FusionUsage {
|
|
|
61
61
|
if (!isRecord(value)) fail('details.usage must be an object', artifactDir);
|
|
62
62
|
assertOnlyKeys(
|
|
63
63
|
value,
|
|
64
|
-
[
|
|
64
|
+
[
|
|
65
|
+
'input',
|
|
66
|
+
'output',
|
|
67
|
+
'cacheRead',
|
|
68
|
+
'cacheWrite',
|
|
69
|
+
'cacheWrite1h',
|
|
70
|
+
'reasoning',
|
|
71
|
+
'totalTokens',
|
|
72
|
+
'cost',
|
|
73
|
+
],
|
|
65
74
|
'details.usage',
|
|
66
75
|
artifactDir,
|
|
67
76
|
);
|
|
@@ -78,11 +87,29 @@ function usage(value: unknown, artifactDir: string): FusionUsage {
|
|
|
78
87
|
fail(`${label} is invalid`, artifactDir);
|
|
79
88
|
return entry;
|
|
80
89
|
};
|
|
90
|
+
const output = finiteNonnegative(value['output'], 'details.usage.output');
|
|
91
|
+
const cacheWrite = finiteNonnegative(value['cacheWrite'], 'details.usage.cacheWrite');
|
|
92
|
+
const cacheWrite1h =
|
|
93
|
+
value['cacheWrite1h'] === undefined
|
|
94
|
+
? undefined
|
|
95
|
+
: finiteNonnegative(value['cacheWrite1h'], 'details.usage.cacheWrite1h');
|
|
96
|
+
const reasoning =
|
|
97
|
+
value['reasoning'] === undefined
|
|
98
|
+
? undefined
|
|
99
|
+
: finiteNonnegative(value['reasoning'], 'details.usage.reasoning');
|
|
100
|
+
if (cacheWrite1h !== undefined && cacheWrite1h > cacheWrite) {
|
|
101
|
+
fail('details.usage.cacheWrite1h must not exceed cacheWrite', artifactDir);
|
|
102
|
+
}
|
|
103
|
+
if (reasoning !== undefined && reasoning > output) {
|
|
104
|
+
fail('details.usage.reasoning must not exceed output', artifactDir);
|
|
105
|
+
}
|
|
81
106
|
return {
|
|
82
107
|
input: finiteNonnegative(value['input'], 'details.usage.input'),
|
|
83
|
-
output
|
|
108
|
+
output,
|
|
84
109
|
cacheRead: finiteNonnegative(value['cacheRead'], 'details.usage.cacheRead'),
|
|
85
|
-
cacheWrite
|
|
110
|
+
cacheWrite,
|
|
111
|
+
...(cacheWrite1h === undefined ? {} : { cacheWrite1h }),
|
|
112
|
+
...(reasoning === undefined ? {} : { reasoning }),
|
|
86
113
|
totalTokens: finiteNonnegative(value['totalTokens'], 'details.usage.totalTokens'),
|
|
87
114
|
cost: {
|
|
88
115
|
input: finiteNonnegative(cost['input'], 'details.usage.cost.input'),
|
package/src/core/fusion/types.ts
CHANGED
|
@@ -522,6 +522,8 @@ export function cloneFusionUsage(usage: FusionUsage): FusionUsage {
|
|
|
522
522
|
output: usage.output,
|
|
523
523
|
cacheRead: usage.cacheRead,
|
|
524
524
|
cacheWrite: usage.cacheWrite,
|
|
525
|
+
...(usage.cacheWrite1h === undefined ? {} : { cacheWrite1h: usage.cacheWrite1h }),
|
|
526
|
+
...(usage.reasoning === undefined ? {} : { reasoning: usage.reasoning }),
|
|
525
527
|
totalTokens: usage.totalTokens,
|
|
526
528
|
cost: {
|
|
527
529
|
input: usage.cost.input,
|
|
@@ -538,6 +540,12 @@ export function addFusionUsage(target: FusionUsage, delta: FusionUsage): void {
|
|
|
538
540
|
target.output += delta.output;
|
|
539
541
|
target.cacheRead += delta.cacheRead;
|
|
540
542
|
target.cacheWrite += delta.cacheWrite;
|
|
543
|
+
if (delta.cacheWrite1h !== undefined) {
|
|
544
|
+
target.cacheWrite1h = (target.cacheWrite1h ?? 0) + delta.cacheWrite1h;
|
|
545
|
+
}
|
|
546
|
+
if (delta.reasoning !== undefined) {
|
|
547
|
+
target.reasoning = (target.reasoning ?? 0) + delta.reasoning;
|
|
548
|
+
}
|
|
541
549
|
target.totalTokens += delta.totalTokens;
|
|
542
550
|
target.cost.input += delta.cost.input;
|
|
543
551
|
target.cost.output += delta.cost.output;
|
|
@@ -546,6 +554,26 @@ export function addFusionUsage(target: FusionUsage, delta: FusionUsage): void {
|
|
|
546
554
|
target.cost.total += delta.cost.total;
|
|
547
555
|
}
|
|
548
556
|
|
|
557
|
+
export type FusionRunStageProgressStatus = 'not_started' | 'incomplete' | 'completed';
|
|
558
|
+
|
|
559
|
+
export interface FusionRunStageProgress {
|
|
560
|
+
status: FusionRunStageProgressStatus;
|
|
561
|
+
attempts_recorded: number;
|
|
562
|
+
children_created: number;
|
|
563
|
+
children_completed: number;
|
|
564
|
+
children_failed: number;
|
|
565
|
+
children_cancelled: number;
|
|
566
|
+
not_started_slots?: number | undefined;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
export interface FusionRunProgress {
|
|
570
|
+
manifest_state: FusionState;
|
|
571
|
+
candidates: FusionRunStageProgress;
|
|
572
|
+
evaluation: FusionRunStageProgress;
|
|
573
|
+
merge: FusionRunStageProgress;
|
|
574
|
+
usage_so_far: FusionUsage;
|
|
575
|
+
}
|
|
576
|
+
|
|
549
577
|
export interface FusionResultBudgetDetails {
|
|
550
578
|
policy_id: string;
|
|
551
579
|
calibration_version: string;
|
|
@@ -707,6 +735,7 @@ export interface FusionErrorDetails {
|
|
|
707
735
|
transient?: boolean;
|
|
708
736
|
childCreated?: boolean;
|
|
709
737
|
budget?: FusionBudgetErrorDetail;
|
|
738
|
+
runProgress?: FusionRunProgress;
|
|
710
739
|
}
|
|
711
740
|
|
|
712
741
|
export class FusionError extends Error {
|
|
@@ -718,6 +747,7 @@ export class FusionError extends Error {
|
|
|
718
747
|
readonly transient: boolean;
|
|
719
748
|
readonly childCreated: boolean;
|
|
720
749
|
readonly budget: FusionBudgetErrorDetail | undefined;
|
|
750
|
+
readonly runProgress: FusionRunProgress | undefined;
|
|
721
751
|
|
|
722
752
|
constructor(message: string, details: FusionErrorDetails) {
|
|
723
753
|
super(message);
|
|
@@ -730,6 +760,7 @@ export class FusionError extends Error {
|
|
|
730
760
|
this.transient = details.transient ?? false;
|
|
731
761
|
this.childCreated = details.childCreated ?? true;
|
|
732
762
|
this.budget = details.budget;
|
|
763
|
+
this.runProgress = details.runProgress;
|
|
733
764
|
}
|
|
734
765
|
}
|
|
735
766
|
|
|
@@ -772,6 +803,18 @@ export interface FusionToolCallTrace {
|
|
|
772
803
|
summary: FusionToolCallLogSummary;
|
|
773
804
|
}
|
|
774
805
|
|
|
806
|
+
export interface FusionCandidateOutputRecovery {
|
|
807
|
+
kind: 'same_session_compression';
|
|
808
|
+
limit_bytes: number;
|
|
809
|
+
original_record_index: number;
|
|
810
|
+
replacement_record_index: number | null;
|
|
811
|
+
original_json_rendered_bytes: number;
|
|
812
|
+
replacement_json_rendered_bytes: number | null;
|
|
813
|
+
original_text_sha256: string;
|
|
814
|
+
original_text: string;
|
|
815
|
+
status: 'completed' | 'failed';
|
|
816
|
+
}
|
|
817
|
+
|
|
775
818
|
export interface FusionChildRunResult {
|
|
776
819
|
stage: FusionStage;
|
|
777
820
|
slot?: 1 | 2 | 3;
|
|
@@ -786,6 +829,7 @@ export interface FusionChildRunResult {
|
|
|
786
829
|
firstRequestUsage?: FusionUsage;
|
|
787
830
|
/** Number of provider requests represented by aggregate usage. */
|
|
788
831
|
providerRequestCount?: number;
|
|
832
|
+
outputRecovery?: FusionCandidateOutputRecovery;
|
|
789
833
|
events: Buffer;
|
|
790
834
|
stderr: Buffer;
|
|
791
835
|
exitCode: number;
|
|
@@ -793,11 +837,24 @@ export interface FusionChildRunResult {
|
|
|
793
837
|
toolCallTrace?: FusionToolCallTrace;
|
|
794
838
|
}
|
|
795
839
|
|
|
840
|
+
export interface FusionAttemptOutputRecoveryRecord {
|
|
841
|
+
kind: 'same_session_compression';
|
|
842
|
+
status: 'completed' | 'failed';
|
|
843
|
+
limit_bytes: number;
|
|
844
|
+
original_response_path: string;
|
|
845
|
+
original_record_index: number;
|
|
846
|
+
replacement_record_index: number | null;
|
|
847
|
+
original_json_rendered_bytes: number;
|
|
848
|
+
replacement_json_rendered_bytes: number | null;
|
|
849
|
+
original_text_sha256: string;
|
|
850
|
+
}
|
|
851
|
+
|
|
796
852
|
export interface FusionAttemptArtifactRecord {
|
|
797
853
|
stage: FusionStage;
|
|
798
854
|
slot?: 1 | 2 | 3;
|
|
799
855
|
attempt: number;
|
|
800
856
|
status: 'completed' | 'failed' | 'cancelled';
|
|
857
|
+
child_created: boolean;
|
|
801
858
|
prompt_path: string;
|
|
802
859
|
events_path?: string;
|
|
803
860
|
stderr_path?: string;
|
|
@@ -805,6 +862,7 @@ export interface FusionAttemptArtifactRecord {
|
|
|
805
862
|
partial_response_path?: string;
|
|
806
863
|
tool_calls_path?: string;
|
|
807
864
|
tool_calls?: FusionToolCallLogSummary;
|
|
865
|
+
output_recovery?: FusionAttemptOutputRecoveryRecord;
|
|
808
866
|
provider?: string;
|
|
809
867
|
model?: string;
|
|
810
868
|
qualifiedId?: string;
|
|
@@ -36,18 +36,17 @@ import {
|
|
|
36
36
|
* - enforcing turn and tool-call limits;
|
|
37
37
|
* - committing exactly one result package atomically.
|
|
38
38
|
*
|
|
39
|
-
* Measured
|
|
40
|
-
* `tests/scripted-provider/pi-hook-contract.test.ts`):
|
|
39
|
+
* Measured behavior across the supported Pi lines (see
|
|
40
|
+
* `tests/scripted-provider/pi-hook-contract.test.ts` and `scripts/test-compat.ts`):
|
|
41
41
|
*
|
|
42
|
-
* - Throwing from a `context` handler does NOT stop
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* -
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* because the content is no longer there.
|
|
42
|
+
* - Throwing from a `context` handler does NOT stop dispatch. Pi catches the
|
|
43
|
+
* exception and continues, so a throw is never used as a barrier here.
|
|
44
|
+
* - `ctx.abort()` blocks transport in both supported modes: Pi 0.81.1-0.83.0
|
|
45
|
+
* invoke the provider with an already-aborted signal, while Pi 0.84.0 skips
|
|
46
|
+
* the provider entry point during signal-aware auth resolution.
|
|
47
|
+
* - The guard ALSO replaces the offending content in the returned message set.
|
|
48
|
+
* Even a non-conforming provider could not transmit the content because the
|
|
49
|
+
* content is no longer there.
|
|
51
50
|
*/
|
|
52
51
|
|
|
53
52
|
const SPILL_DIRNAME = 'spill';
|
|
@@ -471,8 +470,8 @@ export default function delegateChildExtension(pi: ExtensionAPI): void {
|
|
|
471
470
|
if (verdict.withinBudget) return undefined;
|
|
472
471
|
const message = `delegate child context reached ${String(verdict.measuredTokens)} input tokens against a ${String(verdict.allowedTokens)}-token allowance on route ${seed.route.qualified_id}, over by ${String(verdict.overageTokens)}; estimator family ${verdict.rateSource.family}, source ${verdict.rateSource.source}, backed=${String(verdict.backed)}, dominant_byte_class=${verdict.dominantByteClass}, rate ${String(verdict.rateSource.effective_rate_bytes_per_token_x100)}/100 B/tok + ${String(verdict.rateSource.affine_f_tokens)} tokens`;
|
|
473
472
|
latch('provider_context_budget_exhausted', message);
|
|
474
|
-
// Barrier one: terminate the run.
|
|
475
|
-
// provider
|
|
473
|
+
// Barrier one: terminate the run. Depending on the supported Pi line,
|
|
474
|
+
// this skips provider dispatch or hands it an already-aborted signal.
|
|
476
475
|
ctx.abort();
|
|
477
476
|
// Barrier two: remove the content itself, so the request could not carry it
|
|
478
477
|
// even if a provider ignored the aborted signal. Retaining only the first
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
readFileSync,
|
|
9
9
|
writeSync,
|
|
10
10
|
} from 'node:fs';
|
|
11
|
-
import { dirname } from 'node:path';
|
|
11
|
+
import { dirname, isAbsolute } from 'node:path';
|
|
12
12
|
import { parseJsonText } from './core/common.js';
|
|
13
13
|
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
14
14
|
import { Type, type Static } from 'typebox';
|
|
@@ -32,11 +32,13 @@ import {
|
|
|
32
32
|
parseFusionSourcePolicy,
|
|
33
33
|
} from './core/fusion/source-policy.js';
|
|
34
34
|
import {
|
|
35
|
+
applyFusionClaudePromptCachingScopeHeader,
|
|
35
36
|
nonAnthropicFusionCacheObservation,
|
|
36
37
|
normalizeFusionClaudeCachePayload,
|
|
37
38
|
type FusionClaudeCacheObservation,
|
|
38
39
|
} from './core/fusion/claude-cache.js';
|
|
39
40
|
import {
|
|
41
|
+
FUSION_CANDIDATE_OUTPUT_RECOVERY_PATH_ENV,
|
|
40
42
|
FUSION_CHILD_MAX_PROVIDER_REQUESTS,
|
|
41
43
|
FUSION_CHILD_MAX_TOOL_CALLS,
|
|
42
44
|
FUSION_CHILD_MAX_TOTAL_TOOL_RESULT_BYTES,
|
|
@@ -59,12 +61,19 @@ import {
|
|
|
59
61
|
type FusionRuntimeGuardCode,
|
|
60
62
|
type FusionRuntimeGuardRecord,
|
|
61
63
|
} from './core/fusion/child-protocol.js';
|
|
64
|
+
import {
|
|
65
|
+
FUSION_CANDIDATE_MAX_OUTPUT_BYTES,
|
|
66
|
+
FUSION_CANDIDATE_OUTPUT_COMPRESSION_PROMPT,
|
|
67
|
+
fusionJsonRenderedTextBytes,
|
|
68
|
+
} from './core/fusion/output-contract.js';
|
|
62
69
|
|
|
63
70
|
export {
|
|
64
71
|
FUSION_CLAUDE_CACHE_BREAKPOINT_LIMIT,
|
|
65
72
|
FUSION_CLAUDE_CACHE_DEFAULT_RETENTION,
|
|
66
73
|
FUSION_CLAUDE_CACHE_OBSERVATION_SCHEMA_VERSION,
|
|
67
74
|
FUSION_CLAUDE_CACHE_RETENTION_ENV,
|
|
75
|
+
FUSION_CLAUDE_PROMPT_CACHING_SCOPE_BETA,
|
|
76
|
+
applyFusionClaudePromptCachingScopeHeader,
|
|
68
77
|
nonAnthropicFusionCacheObservation,
|
|
69
78
|
normalizeFusionClaudeCachePayload,
|
|
70
79
|
resolveFusionClaudeCachePolicy,
|
|
@@ -75,6 +84,7 @@ export {
|
|
|
75
84
|
} from './core/fusion/claude-cache.js';
|
|
76
85
|
|
|
77
86
|
export {
|
|
87
|
+
FUSION_CANDIDATE_OUTPUT_RECOVERY_PATH_ENV,
|
|
78
88
|
FUSION_CHILD_MAX_PROVIDER_REQUESTS,
|
|
79
89
|
FUSION_CHILD_MAX_TOOL_CALLS,
|
|
80
90
|
FUSION_CHILD_MAX_TOTAL_TOOL_RESULT_BYTES,
|
|
@@ -103,6 +113,12 @@ export {
|
|
|
103
113
|
type FusionRuntimeGuardRecord,
|
|
104
114
|
} from './core/fusion/child-protocol.js';
|
|
105
115
|
|
|
116
|
+
export {
|
|
117
|
+
FUSION_CANDIDATE_MAX_OUTPUT_BYTES,
|
|
118
|
+
FUSION_CANDIDATE_OUTPUT_COMPRESSION_PROMPT,
|
|
119
|
+
fusionJsonRenderedTextBytes,
|
|
120
|
+
} from './core/fusion/output-contract.js';
|
|
121
|
+
|
|
106
122
|
const FUSION_CHILD_O_NOFOLLOW = typeof constants.O_NOFOLLOW === 'number' ? constants.O_NOFOLLOW : 0;
|
|
107
123
|
|
|
108
124
|
const FusionWebFetchParams = Type.Object(
|
|
@@ -253,6 +269,24 @@ function createToolCallLog(path: string): void {
|
|
|
253
269
|
fsyncParentDirectorySync(path);
|
|
254
270
|
}
|
|
255
271
|
|
|
272
|
+
function createCandidateOutputRecoveryArtifact(path: string, text: string): void {
|
|
273
|
+
if (!isAbsolute(path)) {
|
|
274
|
+
throw new Error(`${FUSION_CANDIDATE_OUTPUT_RECOVERY_PATH_ENV} must be an absolute path`);
|
|
275
|
+
}
|
|
276
|
+
const bytes = Buffer.from(text, 'utf8');
|
|
277
|
+
withRegularFileDescriptorSync(
|
|
278
|
+
path,
|
|
279
|
+
constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | FUSION_CHILD_O_NOFOLLOW,
|
|
280
|
+
0o600,
|
|
281
|
+
'fusion oversized candidate response',
|
|
282
|
+
(fd) => {
|
|
283
|
+
writeAllSync(fd, bytes, 'fusion oversized candidate response');
|
|
284
|
+
fsyncSync(fd);
|
|
285
|
+
},
|
|
286
|
+
);
|
|
287
|
+
fsyncParentDirectorySync(path);
|
|
288
|
+
}
|
|
289
|
+
|
|
256
290
|
function appendToolCallLogLine(path: string, record: FusionToolCallLogRecord): void {
|
|
257
291
|
// The log is an audit trail, not a payload copy: raw tool arguments/results may
|
|
258
292
|
// contain secrets, so only byte counts and SHA-256 digests are persisted.
|
|
@@ -670,6 +704,15 @@ function fusionWebFetchResultText(result: Awaited<ReturnType<typeof fusionWebFet
|
|
|
670
704
|
*/
|
|
671
705
|
export default function fusionChildExtension(pi: ExtensionAPI): void {
|
|
672
706
|
const toolCallLogPath = process.env[FUSION_TOOL_CALL_LOG_PATH_ENV];
|
|
707
|
+
const candidateOutputRecoveryPath = process.env[FUSION_CANDIDATE_OUTPUT_RECOVERY_PATH_ENV];
|
|
708
|
+
if (
|
|
709
|
+
candidateOutputRecoveryPath !== undefined &&
|
|
710
|
+
(candidateOutputRecoveryPath.trim().length === 0 || !isAbsolute(candidateOutputRecoveryPath))
|
|
711
|
+
) {
|
|
712
|
+
throw new Error(
|
|
713
|
+
`${FUSION_CANDIDATE_OUTPUT_RECOVERY_PATH_ENV} must be an absolute non-blank path`,
|
|
714
|
+
);
|
|
715
|
+
}
|
|
673
716
|
const researchEnabled = process.env[FUSION_RESEARCH_ENABLED_ENV];
|
|
674
717
|
if (researchEnabled !== undefined && researchEnabled !== '1') {
|
|
675
718
|
throw new Error(`${FUSION_RESEARCH_ENABLED_ENV} must be unset or exactly 1`);
|
|
@@ -684,10 +727,17 @@ export default function fusionChildExtension(pi: ExtensionAPI): void {
|
|
|
684
727
|
let providerRequestCount = 0;
|
|
685
728
|
let toolCallCount = 0;
|
|
686
729
|
let runtimeGuardFailed = false;
|
|
730
|
+
let outputRecoveryFailed = false;
|
|
731
|
+
let outputRecoveryPhase: 'eligible' | 'queued' | 'finished' = 'eligible';
|
|
687
732
|
let settlementPublished = false;
|
|
688
733
|
const childResultRecords: FusionChildResultMetadata[] = [];
|
|
689
734
|
let pendingCacheObservation: FusionClaudeCacheObservation | undefined;
|
|
690
735
|
|
|
736
|
+
pi.on('before_provider_headers', (event, ctx) => {
|
|
737
|
+
if (ctx.model?.provider !== 'anthropic') return;
|
|
738
|
+
applyFusionClaudePromptCachingScopeHeader(event.headers);
|
|
739
|
+
});
|
|
740
|
+
|
|
691
741
|
pi.on('before_provider_request', async (event, ctx) => {
|
|
692
742
|
providerRequestCount += 1;
|
|
693
743
|
if (runtimeGuardFailed) {
|
|
@@ -820,6 +870,15 @@ export default function fusionChildExtension(pi: ExtensionAPI): void {
|
|
|
820
870
|
pi.on('tool_call', async (event, ctx) => {
|
|
821
871
|
try {
|
|
822
872
|
requireOpen('tool_call');
|
|
873
|
+
if (outputRecoveryPhase === 'queued') {
|
|
874
|
+
outputRecoveryFailed = true;
|
|
875
|
+
latchAuditProcessFailure();
|
|
876
|
+
ctx.abort();
|
|
877
|
+
return {
|
|
878
|
+
block: true,
|
|
879
|
+
reason: 'fusion candidate output compression is a no-tool continuation',
|
|
880
|
+
};
|
|
881
|
+
}
|
|
823
882
|
if (runtimeGuardFailed) {
|
|
824
883
|
ctx.abort();
|
|
825
884
|
return { block: true, reason: 'fusion child runtime guard already refused the run' };
|
|
@@ -978,13 +1037,68 @@ export default function fusionChildExtension(pi: ExtensionAPI): void {
|
|
|
978
1037
|
if (event.message.role !== 'assistant') return;
|
|
979
1038
|
const cacheObservation = pendingCacheObservation;
|
|
980
1039
|
if (cacheObservation === undefined) {
|
|
1040
|
+
// Authentication and other pre-transport failures can produce an assistant
|
|
1041
|
+
// error without ever reaching before_provider_request. Preserve Pi's original
|
|
1042
|
+
// provider diagnostic and let settlement report no_records; emitting a second
|
|
1043
|
+
// cache-policy error would mask the actionable failure. A successful result
|
|
1044
|
+
// without an observation remains a hard invariant violation.
|
|
1045
|
+
if (event.message.stopReason === 'error') return;
|
|
981
1046
|
latchAuditProcessFailure();
|
|
982
1047
|
throw new Error('fusion child assistant result has no matching cache-policy observation');
|
|
983
1048
|
}
|
|
984
1049
|
pendingCacheObservation = undefined;
|
|
985
|
-
const
|
|
1050
|
+
const text = event.message.content
|
|
1051
|
+
.flatMap((part) => (part.type === 'text' ? [part.text] : []))
|
|
1052
|
+
.join('');
|
|
1053
|
+
const renderedBytes = fusionJsonRenderedTextBytes(text);
|
|
1054
|
+
const isReplacement = outputRecoveryPhase === 'queued';
|
|
1055
|
+
const shouldRecover =
|
|
1056
|
+
candidateOutputRecoveryPath !== undefined &&
|
|
1057
|
+
outputRecoveryPhase === 'eligible' &&
|
|
1058
|
+
event.message.stopReason === 'stop' &&
|
|
1059
|
+
renderedBytes > FUSION_CANDIDATE_MAX_OUTPUT_BYTES;
|
|
1060
|
+
const recoveryRole = isReplacement
|
|
1061
|
+
? 'replacement'
|
|
1062
|
+
: shouldRecover
|
|
1063
|
+
? 'oversized_original'
|
|
1064
|
+
: 'none';
|
|
1065
|
+
const record = buildFusionChildResultMetadata(event.message, cacheObservation, {
|
|
1066
|
+
candidateLimitBytes:
|
|
1067
|
+
candidateOutputRecoveryPath === undefined ? null : FUSION_CANDIDATE_MAX_OUTPUT_BYTES,
|
|
1068
|
+
recoveryRole,
|
|
1069
|
+
});
|
|
1070
|
+
if (shouldRecover) {
|
|
1071
|
+
try {
|
|
1072
|
+
createCandidateOutputRecoveryArtifact(candidateOutputRecoveryPath, text);
|
|
1073
|
+
} catch (error) {
|
|
1074
|
+
outputRecoveryFailed = true;
|
|
1075
|
+
latchAuditProcessFailure();
|
|
1076
|
+
throw error;
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
986
1079
|
await writeMetadata(record);
|
|
987
1080
|
childResultRecords.push(record);
|
|
1081
|
+
if (shouldRecover) {
|
|
1082
|
+
outputRecoveryPhase = 'queued';
|
|
1083
|
+
try {
|
|
1084
|
+
pi.setActiveTools([]);
|
|
1085
|
+
pi.sendUserMessage(FUSION_CANDIDATE_OUTPUT_COMPRESSION_PROMPT, {
|
|
1086
|
+
deliverAs: 'followUp',
|
|
1087
|
+
});
|
|
1088
|
+
} catch (error) {
|
|
1089
|
+
outputRecoveryFailed = true;
|
|
1090
|
+
latchAuditProcessFailure();
|
|
1091
|
+
throw error;
|
|
1092
|
+
}
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
if (isReplacement) {
|
|
1096
|
+
outputRecoveryPhase = 'finished';
|
|
1097
|
+
if (renderedBytes > FUSION_CANDIDATE_MAX_OUTPUT_BYTES) {
|
|
1098
|
+
outputRecoveryFailed = true;
|
|
1099
|
+
latchAuditProcessFailure();
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
988
1102
|
});
|
|
989
1103
|
pi.on('agent_settled', async (_event, ctx) => {
|
|
990
1104
|
if (settlementPublished) {
|
|
@@ -1001,6 +1115,7 @@ export default function fusionChildExtension(pi: ExtensionAPI): void {
|
|
|
1001
1115
|
childResultRecords,
|
|
1002
1116
|
runtimeGuardFailed,
|
|
1003
1117
|
cacheObservationFailed,
|
|
1118
|
+
outputRecoveryFailed,
|
|
1004
1119
|
);
|
|
1005
1120
|
if (settlement.status !== 'complete') latchAuditProcessFailure();
|
|
1006
1121
|
await writeSettlement(settlement);
|