pi-background-tasks 1.0.4 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BACKGROUND-TASKS-INSTRUCTIONS.md +2 -1
- package/README.md +2 -1
- package/TESTING.md +2 -2
- package/TEST_PLAN.md +10 -9
- package/docs/manifest.json +8 -4
- package/docs/operations/configuration.md +10 -0
- package/docs/operations/testing.md +2 -1
- package/docs/operations/troubleshooting.md +3 -1
- package/docs/read-before-edit.md +1 -0
- package/docs/reference/runtime-contracts.md +44 -40
- package/docs/subsystems/docs-freshness-gate.md +5 -5
- package/docs/subsystems/fusion.md +17 -12
- package/package.json +2 -1
- package/src/core/fusion/budget.ts +11 -4
- package/src/core/fusion/child-protocol.ts +137 -12
- package/src/core/fusion/claude-cache.ts +186 -0
- package/src/core/fusion/config.ts +13 -0
- package/src/core/fusion/pi-child.ts +558 -14
- package/src/core/fusion/types.ts +29 -7
- package/src/fusion-child-extension.ts +413 -4
package/src/core/fusion/types.ts
CHANGED
|
@@ -14,14 +14,15 @@ export const FUSION_MODEL_CONFIG_SCHEMA_VERSION = 'pi-background-tasks.fusion-mo
|
|
|
14
14
|
export const FUSION_LEGACY_INPUT_SCHEMA_VERSION = 'pi-background-tasks.fusion-input.v4';
|
|
15
15
|
export const FUSION_INPUT_SCHEMA_VERSION = 'pi-background-tasks.fusion-input.v5';
|
|
16
16
|
export const FUSION_EVALUATION_SCHEMA_VERSION = 'pi-background-tasks.fusion-evaluation.v1';
|
|
17
|
-
export const FUSION_VALIDATE_CANDIDATE_SCHEMA_VERSION =
|
|
17
|
+
export const FUSION_VALIDATE_CANDIDATE_SCHEMA_VERSION =
|
|
18
|
+
'pi-background-tasks.fusion-validation-candidate.v1';
|
|
18
19
|
export const FUSION_LEGACY_RESULT_SCHEMA_VERSION = 'pi-background-tasks.fusion-result.v4';
|
|
19
20
|
export const FUSION_RESULT_SCHEMA_VERSION = 'pi-background-tasks.fusion-result.v5';
|
|
20
21
|
export const FUSION_LEGACY_MANIFEST_SCHEMA_VERSION = 'pi-background-tasks.fusion-manifest.v3';
|
|
21
22
|
export const FUSION_MANIFEST_SCHEMA_VERSION = 'pi-background-tasks.fusion-manifest.v4';
|
|
22
23
|
export const FUSION_CONTEXT_LEDGER_SCHEMA_VERSION = 'pi-background-tasks.fusion-context-ledger.v2';
|
|
23
24
|
export const FUSION_SOURCE_POLICY_SCHEMA_VERSION = 'pi-background-tasks.fusion-source-policy.v1';
|
|
24
|
-
export const FUSION_BUDGET_PLAN_SCHEMA_VERSION = 'pi-background-tasks.fusion-budget-plan.
|
|
25
|
+
export const FUSION_BUDGET_PLAN_SCHEMA_VERSION = 'pi-background-tasks.fusion-budget-plan.v4';
|
|
25
26
|
export const FUSION_CALIBRATION_VIOLATION_SCHEMA_VERSION =
|
|
26
27
|
'pi-background-tasks.fusion-calibration-violation.v1';
|
|
27
28
|
export const FUSION_TOOL_CALL_LOG_SCHEMA_VERSION = 'pi-background-tasks.fusion-tool-call.v1';
|
|
@@ -186,6 +187,7 @@ export interface ResolvedFusionModel {
|
|
|
186
187
|
qualifiedId: string;
|
|
187
188
|
thinkingLevel: FusionThinkingLevel;
|
|
188
189
|
contextWindow: number;
|
|
190
|
+
maxOutputTokens: number;
|
|
189
191
|
}
|
|
190
192
|
|
|
191
193
|
export interface ResolvedFusionModels {
|
|
@@ -556,7 +558,11 @@ export interface FusionResultDetails {
|
|
|
556
558
|
source: FusionSource;
|
|
557
559
|
status: 'completed';
|
|
558
560
|
context: { kind: FusionContextKind; policy_id: string };
|
|
559
|
-
tool_policy: {
|
|
561
|
+
tool_policy: {
|
|
562
|
+
candidate_tools: readonly string[];
|
|
563
|
+
evaluation_tools: readonly [];
|
|
564
|
+
merge_tools: readonly [];
|
|
565
|
+
};
|
|
560
566
|
artifact_dir: string;
|
|
561
567
|
models: {
|
|
562
568
|
candidates: readonly [string, string, string];
|
|
@@ -595,6 +601,8 @@ export type FusionErrorCode =
|
|
|
595
601
|
| 'child_stdin_failed'
|
|
596
602
|
| 'child_event_invalid'
|
|
597
603
|
| 'child_exit_failed'
|
|
604
|
+
| 'child_runtime_budget_exceeded'
|
|
605
|
+
| 'child_cache_policy_invalid'
|
|
598
606
|
| 'child_timeout'
|
|
599
607
|
| 'child_output_cap'
|
|
600
608
|
| 'child_cancelled'
|
|
@@ -815,8 +823,17 @@ export interface FusionArtifactManifest {
|
|
|
815
823
|
evaluation: FusionCapability;
|
|
816
824
|
merge: FusionCapability;
|
|
817
825
|
};
|
|
818
|
-
context: {
|
|
819
|
-
|
|
826
|
+
context: {
|
|
827
|
+
kind: FusionContextKind;
|
|
828
|
+
policy_id: string;
|
|
829
|
+
ledger_artifact?: string;
|
|
830
|
+
source_policy_artifact?: string;
|
|
831
|
+
};
|
|
832
|
+
tool_policy: {
|
|
833
|
+
candidate_tools: readonly string[];
|
|
834
|
+
evaluation_tools: readonly [];
|
|
835
|
+
merge_tools: readonly [];
|
|
836
|
+
};
|
|
820
837
|
usage: FusionUsage;
|
|
821
838
|
attempts: readonly FusionAttemptArtifactRecord[];
|
|
822
839
|
artifacts: Readonly<Record<string, FusionArtifactRef>>;
|
|
@@ -900,7 +917,11 @@ export interface FusionBudgetPlanV1 {
|
|
|
900
917
|
workflow: FusionWorkflowId;
|
|
901
918
|
context: { kind: FusionContextKind; policy_id: string };
|
|
902
919
|
fixed_candidate_policy: { capability: FusionCapability; tools: readonly string[] };
|
|
903
|
-
tool_policy: {
|
|
920
|
+
tool_policy: {
|
|
921
|
+
candidate_tools: readonly string[];
|
|
922
|
+
evaluation_tools: readonly [];
|
|
923
|
+
merge_tools: readonly [];
|
|
924
|
+
};
|
|
904
925
|
policy: FusionBudgetPolicyDescriptor;
|
|
905
926
|
routes: readonly FusionRouteCapacity[];
|
|
906
927
|
stages: readonly FusionStageBudgetPlanEntry[];
|
|
@@ -913,7 +934,8 @@ export interface FusionBudgetPlanV1 {
|
|
|
913
934
|
|
|
914
935
|
/** Documented, versioned budget policy. */
|
|
915
936
|
export interface FusionBudgetPolicyDescriptor {
|
|
916
|
-
id: 'fusion-budget-policy-
|
|
937
|
+
id: 'fusion-budget-policy-v4';
|
|
938
|
+
route_output_reserve_strategy: 'max_fusion_contract_or_model_max';
|
|
917
939
|
calibration_version: string;
|
|
918
940
|
calibration_table: Readonly<Record<TokenBudgetFamily, TokenBudgetFamilyCalibration>>;
|
|
919
941
|
reserved_output_tokens: number;
|
|
@@ -9,8 +9,14 @@ import {
|
|
|
9
9
|
writeSync,
|
|
10
10
|
} from 'node:fs';
|
|
11
11
|
import { dirname } from 'node:path';
|
|
12
|
+
import { parseJsonText } from './core/common.js';
|
|
12
13
|
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
13
14
|
import { Type, type Static } from 'typebox';
|
|
15
|
+
import {
|
|
16
|
+
estimateInputTokens,
|
|
17
|
+
knownJsonSegment,
|
|
18
|
+
resolveTokenBudgetFamily,
|
|
19
|
+
} from './core/context/token-budget.js';
|
|
14
20
|
import {
|
|
15
21
|
FUSION_TOOL_CALL_LOG_SCHEMA_VERSION,
|
|
16
22
|
FUSION_WEB_FETCH_TOOL_NAME,
|
|
@@ -26,32 +32,75 @@ import {
|
|
|
26
32
|
parseFusionSourcePolicy,
|
|
27
33
|
} from './core/fusion/source-policy.js';
|
|
28
34
|
import {
|
|
35
|
+
nonAnthropicFusionCacheObservation,
|
|
36
|
+
normalizeFusionClaudeCachePayload,
|
|
37
|
+
type FusionClaudeCacheObservation,
|
|
38
|
+
} from './core/fusion/claude-cache.js';
|
|
39
|
+
import {
|
|
40
|
+
FUSION_CHILD_MAX_PROVIDER_REQUESTS,
|
|
41
|
+
FUSION_CHILD_MAX_TOOL_CALLS,
|
|
29
42
|
FUSION_CHILD_MAX_TOTAL_TOOL_RESULT_BYTES,
|
|
43
|
+
FUSION_CHILD_MIN_OUTPUT_RESERVE_TOKENS,
|
|
30
44
|
FUSION_CHILD_RESULT_PREFIX,
|
|
45
|
+
FUSION_CHILD_SAFETY_RESERVE_TOKENS,
|
|
46
|
+
FUSION_CHILD_SETTLEMENT_PREFIX,
|
|
31
47
|
FUSION_RESEARCH_ENABLED_ENV,
|
|
48
|
+
FUSION_RUNTIME_GUARD_PREFIX,
|
|
49
|
+
FUSION_RUNTIME_GUARD_SCHEMA_VERSION,
|
|
32
50
|
FUSION_SOURCE_POLICY_PATH_ENV,
|
|
33
51
|
FUSION_SOURCE_POLICY_SHA256_ENV,
|
|
34
52
|
FUSION_TOOL_CALL_LOG_PATH_ENV,
|
|
35
53
|
FUSION_TOOL_CALL_SEAL_SCHEMA_VERSION,
|
|
36
54
|
FUSION_TOOL_CALL_SEAL_SUFFIX,
|
|
37
55
|
buildFusionChildResultMetadata,
|
|
56
|
+
buildFusionChildSettlement,
|
|
38
57
|
type FusionChildResultMetadata,
|
|
58
|
+
type FusionChildSettlementRecord,
|
|
59
|
+
type FusionRuntimeGuardCode,
|
|
60
|
+
type FusionRuntimeGuardRecord,
|
|
39
61
|
} from './core/fusion/child-protocol.js';
|
|
40
62
|
|
|
41
63
|
export {
|
|
64
|
+
FUSION_CLAUDE_CACHE_BREAKPOINT_LIMIT,
|
|
65
|
+
FUSION_CLAUDE_CACHE_DEFAULT_RETENTION,
|
|
66
|
+
FUSION_CLAUDE_CACHE_OBSERVATION_SCHEMA_VERSION,
|
|
67
|
+
FUSION_CLAUDE_CACHE_RETENTION_ENV,
|
|
68
|
+
nonAnthropicFusionCacheObservation,
|
|
69
|
+
normalizeFusionClaudeCachePayload,
|
|
70
|
+
resolveFusionClaudeCachePolicy,
|
|
71
|
+
type FusionClaudeCacheNormalization,
|
|
72
|
+
type FusionClaudeCacheObservation,
|
|
73
|
+
type FusionClaudeCachePolicySource,
|
|
74
|
+
type FusionClaudeCacheRetention,
|
|
75
|
+
} from './core/fusion/claude-cache.js';
|
|
76
|
+
|
|
77
|
+
export {
|
|
78
|
+
FUSION_CHILD_MAX_PROVIDER_REQUESTS,
|
|
79
|
+
FUSION_CHILD_MAX_TOOL_CALLS,
|
|
42
80
|
FUSION_CHILD_MAX_TOTAL_TOOL_RESULT_BYTES,
|
|
81
|
+
FUSION_CHILD_MIN_OUTPUT_RESERVE_TOKENS,
|
|
43
82
|
FUSION_CHILD_RESULT_PREFIX,
|
|
44
83
|
FUSION_CHILD_RESULT_SCHEMA_VERSION,
|
|
84
|
+
FUSION_CHILD_SAFETY_RESERVE_TOKENS,
|
|
85
|
+
FUSION_CHILD_SETTLEMENT_PREFIX,
|
|
86
|
+
FUSION_CHILD_SETTLEMENT_SCHEMA_VERSION,
|
|
45
87
|
FUSION_RESEARCH_ENABLED_ENV,
|
|
88
|
+
FUSION_RUNTIME_GUARD_PREFIX,
|
|
89
|
+
FUSION_RUNTIME_GUARD_SCHEMA_VERSION,
|
|
46
90
|
FUSION_SOURCE_POLICY_PATH_ENV,
|
|
47
91
|
FUSION_SOURCE_POLICY_SHA256_ENV,
|
|
48
92
|
FUSION_TOOL_CALL_LOG_PATH_ENV,
|
|
49
93
|
FUSION_TOOL_CALL_SEAL_SCHEMA_VERSION,
|
|
50
94
|
FUSION_TOOL_CALL_SEAL_SUFFIX,
|
|
51
95
|
buildFusionChildResultMetadata,
|
|
96
|
+
buildFusionChildSettlement,
|
|
52
97
|
type FusionChildResultMetadata,
|
|
53
98
|
type FusionChildResultUsageMetadata,
|
|
99
|
+
type FusionChildSettlementFailureReason,
|
|
100
|
+
type FusionChildSettlementRecord,
|
|
54
101
|
type FusionChildTextBlockMetadata,
|
|
102
|
+
type FusionRuntimeGuardCode,
|
|
103
|
+
type FusionRuntimeGuardRecord,
|
|
55
104
|
} from './core/fusion/child-protocol.js';
|
|
56
105
|
|
|
57
106
|
const FUSION_CHILD_O_NOFOLLOW = typeof constants.O_NOFOLLOW === 'number' ? constants.O_NOFOLLOW : 0;
|
|
@@ -263,6 +312,237 @@ async function writeMetadata(record: FusionChildResultMetadata): Promise<void> {
|
|
|
263
312
|
});
|
|
264
313
|
}
|
|
265
314
|
|
|
315
|
+
async function writeSettlement(record: FusionChildSettlementRecord): Promise<void> {
|
|
316
|
+
const line = `${FUSION_CHILD_SETTLEMENT_PREFIX}${JSON.stringify(record)}\n`;
|
|
317
|
+
await new Promise<void>((resolve, reject) => {
|
|
318
|
+
process.stderr.write(line, (error) => {
|
|
319
|
+
if (error) reject(error);
|
|
320
|
+
else resolve();
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async function writeRuntimeGuard(record: FusionRuntimeGuardRecord): Promise<void> {
|
|
326
|
+
const line = `${FUSION_RUNTIME_GUARD_PREFIX}${JSON.stringify(record)}\n`;
|
|
327
|
+
await new Promise<void>((resolve, reject) => {
|
|
328
|
+
process.stderr.write(line, (error) => {
|
|
329
|
+
if (error) reject(error);
|
|
330
|
+
else resolve();
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export interface FusionRuntimeRequestEvaluationInput {
|
|
336
|
+
payload: unknown;
|
|
337
|
+
provider: string | undefined;
|
|
338
|
+
model: string | undefined;
|
|
339
|
+
contextWindowTokens: number | undefined;
|
|
340
|
+
maxOutputTokens: number | undefined;
|
|
341
|
+
requestOrdinal: number;
|
|
342
|
+
toolCallCount: number;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function invalidFusionRuntimeRequest(
|
|
346
|
+
input: FusionRuntimeRequestEvaluationInput,
|
|
347
|
+
detail: string,
|
|
348
|
+
code: Extract<
|
|
349
|
+
FusionRuntimeGuardCode,
|
|
350
|
+
'provider_payload_invalid' | 'claude_cache_policy'
|
|
351
|
+
> = 'provider_payload_invalid',
|
|
352
|
+
): FusionRuntimeGuardRecord {
|
|
353
|
+
const contextWindowTokens =
|
|
354
|
+
input.contextWindowTokens !== undefined &&
|
|
355
|
+
Number.isSafeInteger(input.contextWindowTokens) &&
|
|
356
|
+
input.contextWindowTokens > 0
|
|
357
|
+
? input.contextWindowTokens
|
|
358
|
+
: 0;
|
|
359
|
+
const modelOutputTokens =
|
|
360
|
+
input.maxOutputTokens !== undefined &&
|
|
361
|
+
Number.isSafeInteger(input.maxOutputTokens) &&
|
|
362
|
+
input.maxOutputTokens > 0
|
|
363
|
+
? input.maxOutputTokens
|
|
364
|
+
: 0;
|
|
365
|
+
const reservedOutputTokens = Math.max(FUSION_CHILD_MIN_OUTPUT_RESERVE_TOKENS, modelOutputTokens);
|
|
366
|
+
const allowedInputTokens = Math.max(
|
|
367
|
+
0,
|
|
368
|
+
contextWindowTokens - reservedOutputTokens - FUSION_CHILD_SAFETY_RESERVE_TOKENS,
|
|
369
|
+
);
|
|
370
|
+
const emptyPayload = Buffer.alloc(0);
|
|
371
|
+
return {
|
|
372
|
+
schema_version: FUSION_RUNTIME_GUARD_SCHEMA_VERSION,
|
|
373
|
+
code,
|
|
374
|
+
provider: input.provider ?? 'unknown',
|
|
375
|
+
model: input.model ?? 'unknown',
|
|
376
|
+
request_ordinal: input.requestOrdinal,
|
|
377
|
+
tool_call_count: input.toolCallCount,
|
|
378
|
+
payload_bytes: 0,
|
|
379
|
+
payload_sha256: sha256(emptyPayload),
|
|
380
|
+
estimated_input_tokens: 0,
|
|
381
|
+
context_window_tokens: contextWindowTokens,
|
|
382
|
+
reserved_output_tokens: reservedOutputTokens,
|
|
383
|
+
safety_reserve_tokens: FUSION_CHILD_SAFETY_RESERVE_TOKENS,
|
|
384
|
+
allowed_input_tokens: allowedInputTokens,
|
|
385
|
+
message: `fusion child could not validate provider request ${String(input.requestOrdinal)}: ${detail}`,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export interface PreparedFusionRuntimeRequest {
|
|
390
|
+
payload: unknown;
|
|
391
|
+
guard: FusionRuntimeGuardRecord | undefined;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export function prepareFusionRuntimeRequest(
|
|
395
|
+
input: FusionRuntimeRequestEvaluationInput,
|
|
396
|
+
): PreparedFusionRuntimeRequest {
|
|
397
|
+
try {
|
|
398
|
+
const serialized: unknown = JSON.stringify(input.payload);
|
|
399
|
+
if (typeof serialized !== 'string') {
|
|
400
|
+
throw new Error('provider payload serialized to a non-string value');
|
|
401
|
+
}
|
|
402
|
+
const payload = parseJsonText(serialized);
|
|
403
|
+
if (typeof payload !== 'object' || payload === null || Array.isArray(payload)) {
|
|
404
|
+
throw new Error('provider payload must serialize to a JSON object');
|
|
405
|
+
}
|
|
406
|
+
if (JSON.stringify(payload) !== serialized) {
|
|
407
|
+
throw new Error('provider payload does not have a stable JSON serialization');
|
|
408
|
+
}
|
|
409
|
+
return { payload, guard: evaluateFusionRuntimeRequest({ ...input, payload }) };
|
|
410
|
+
} catch (error) {
|
|
411
|
+
return {
|
|
412
|
+
payload: input.payload,
|
|
413
|
+
guard: invalidFusionRuntimeRequest(
|
|
414
|
+
input,
|
|
415
|
+
error instanceof Error ? error.message : String(error),
|
|
416
|
+
),
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export function evaluateFusionRuntimeRequest(
|
|
422
|
+
input: FusionRuntimeRequestEvaluationInput,
|
|
423
|
+
): FusionRuntimeGuardRecord | undefined {
|
|
424
|
+
let code: FusionRuntimeGuardCode | undefined;
|
|
425
|
+
let message = '';
|
|
426
|
+
let payloadBytes = Buffer.alloc(0);
|
|
427
|
+
let estimatedInputTokens = 0;
|
|
428
|
+
let allowedInputTokens = 0;
|
|
429
|
+
const provider = input.provider ?? 'unknown';
|
|
430
|
+
const model = input.model ?? 'unknown';
|
|
431
|
+
const contextWindowTokens = input.contextWindowTokens ?? 0;
|
|
432
|
+
const reservedOutputTokens = Math.max(
|
|
433
|
+
FUSION_CHILD_MIN_OUTPUT_RESERVE_TOKENS,
|
|
434
|
+
input.maxOutputTokens ?? 0,
|
|
435
|
+
);
|
|
436
|
+
try {
|
|
437
|
+
if (input.provider === undefined || input.model === undefined) {
|
|
438
|
+
throw new Error('active model is unavailable');
|
|
439
|
+
}
|
|
440
|
+
if (!Number.isSafeInteger(contextWindowTokens) || contextWindowTokens <= 0) {
|
|
441
|
+
throw new Error('active model context window is unavailable');
|
|
442
|
+
}
|
|
443
|
+
if (
|
|
444
|
+
input.maxOutputTokens === undefined ||
|
|
445
|
+
!Number.isSafeInteger(input.maxOutputTokens) ||
|
|
446
|
+
input.maxOutputTokens <= 0
|
|
447
|
+
) {
|
|
448
|
+
throw new Error('active model maximum output tokens are unavailable');
|
|
449
|
+
}
|
|
450
|
+
allowedInputTokens =
|
|
451
|
+
contextWindowTokens - reservedOutputTokens - FUSION_CHILD_SAFETY_RESERVE_TOKENS;
|
|
452
|
+
if (allowedInputTokens <= 0) {
|
|
453
|
+
throw new Error('active model has no safe provider input capacity');
|
|
454
|
+
}
|
|
455
|
+
const payloadText = JSON.stringify(input.payload);
|
|
456
|
+
if (payloadText === undefined) throw new Error('provider payload serialized to undefined');
|
|
457
|
+
payloadBytes = Buffer.from(payloadText, 'utf8');
|
|
458
|
+
const family = resolveTokenBudgetFamily({ provider, model });
|
|
459
|
+
estimatedInputTokens = estimateInputTokens({
|
|
460
|
+
family: family.family,
|
|
461
|
+
calibrationBacked: family.backed,
|
|
462
|
+
familyResolution: family.resolution,
|
|
463
|
+
allowedInputTokens,
|
|
464
|
+
scope: 'fusion',
|
|
465
|
+
segments: [knownJsonSegment(payloadText)],
|
|
466
|
+
}).tokens;
|
|
467
|
+
if (input.requestOrdinal > FUSION_CHILD_MAX_PROVIDER_REQUESTS) {
|
|
468
|
+
code = 'provider_request_limit';
|
|
469
|
+
message = `fusion child reached provider request ${String(input.requestOrdinal)}, exceeding the ${String(FUSION_CHILD_MAX_PROVIDER_REQUESTS)}-request execution limit`;
|
|
470
|
+
} else if (estimatedInputTokens > allowedInputTokens) {
|
|
471
|
+
code = 'provider_request_budget';
|
|
472
|
+
message = `fusion child blocked provider request ${String(input.requestOrdinal)} before transport: exact final payload is ${String(payloadBytes.length)} UTF-8 bytes (estimated <= ${String(estimatedInputTokens)} input tokens), exceeding ${String(allowedInputTokens)} allowed input tokens after reserving ${String(reservedOutputTokens)} model output + ${String(FUSION_CHILD_SAFETY_RESERVE_TOKENS)} safety from the ${String(contextWindowTokens)}-token context window`;
|
|
473
|
+
}
|
|
474
|
+
} catch (error) {
|
|
475
|
+
code = 'provider_payload_invalid';
|
|
476
|
+
message = `fusion child could not validate provider request ${String(input.requestOrdinal)}: ${error instanceof Error ? error.message : String(error)}`;
|
|
477
|
+
}
|
|
478
|
+
if (code === undefined) return undefined;
|
|
479
|
+
return {
|
|
480
|
+
schema_version: FUSION_RUNTIME_GUARD_SCHEMA_VERSION,
|
|
481
|
+
code,
|
|
482
|
+
provider,
|
|
483
|
+
model,
|
|
484
|
+
request_ordinal: input.requestOrdinal,
|
|
485
|
+
tool_call_count: input.toolCallCount,
|
|
486
|
+
payload_bytes: payloadBytes.length,
|
|
487
|
+
payload_sha256: sha256(payloadBytes),
|
|
488
|
+
estimated_input_tokens: estimatedInputTokens,
|
|
489
|
+
context_window_tokens: contextWindowTokens,
|
|
490
|
+
reserved_output_tokens: reservedOutputTokens,
|
|
491
|
+
safety_reserve_tokens: FUSION_CHILD_SAFETY_RESERVE_TOKENS,
|
|
492
|
+
allowed_input_tokens: allowedInputTokens,
|
|
493
|
+
message,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
export interface FusionRuntimeToolLimitEvaluationInput {
|
|
498
|
+
provider: string | undefined;
|
|
499
|
+
model: string | undefined;
|
|
500
|
+
contextWindowTokens: number | undefined;
|
|
501
|
+
maxOutputTokens: number | undefined;
|
|
502
|
+
requestOrdinal: number;
|
|
503
|
+
toolCallCount: number;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
export function evaluateFusionRuntimeToolLimit(
|
|
507
|
+
input: FusionRuntimeToolLimitEvaluationInput,
|
|
508
|
+
): FusionRuntimeGuardRecord | undefined {
|
|
509
|
+
if (input.toolCallCount <= FUSION_CHILD_MAX_TOOL_CALLS) return undefined;
|
|
510
|
+
const contextWindowTokens =
|
|
511
|
+
input.contextWindowTokens !== undefined &&
|
|
512
|
+
Number.isSafeInteger(input.contextWindowTokens) &&
|
|
513
|
+
input.contextWindowTokens > 0
|
|
514
|
+
? input.contextWindowTokens
|
|
515
|
+
: 0;
|
|
516
|
+
const modelOutputTokens =
|
|
517
|
+
input.maxOutputTokens !== undefined &&
|
|
518
|
+
Number.isSafeInteger(input.maxOutputTokens) &&
|
|
519
|
+
input.maxOutputTokens > 0
|
|
520
|
+
? input.maxOutputTokens
|
|
521
|
+
: 0;
|
|
522
|
+
const reservedOutputTokens = Math.max(FUSION_CHILD_MIN_OUTPUT_RESERVE_TOKENS, modelOutputTokens);
|
|
523
|
+
const allowedInputTokens = Math.max(
|
|
524
|
+
0,
|
|
525
|
+
contextWindowTokens - reservedOutputTokens - FUSION_CHILD_SAFETY_RESERVE_TOKENS,
|
|
526
|
+
);
|
|
527
|
+
const emptyPayload = Buffer.alloc(0);
|
|
528
|
+
return {
|
|
529
|
+
schema_version: FUSION_RUNTIME_GUARD_SCHEMA_VERSION,
|
|
530
|
+
code: 'tool_call_limit',
|
|
531
|
+
provider: input.provider ?? 'unknown',
|
|
532
|
+
model: input.model ?? 'unknown',
|
|
533
|
+
request_ordinal: input.requestOrdinal,
|
|
534
|
+
tool_call_count: input.toolCallCount,
|
|
535
|
+
payload_bytes: 0,
|
|
536
|
+
payload_sha256: sha256(emptyPayload),
|
|
537
|
+
estimated_input_tokens: 0,
|
|
538
|
+
context_window_tokens: contextWindowTokens,
|
|
539
|
+
reserved_output_tokens: reservedOutputTokens,
|
|
540
|
+
safety_reserve_tokens: FUSION_CHILD_SAFETY_RESERVE_TOKENS,
|
|
541
|
+
allowed_input_tokens: allowedInputTokens,
|
|
542
|
+
message: `fusion child reached tool call ${String(input.toolCallCount)}, exceeding the ${String(FUSION_CHILD_MAX_TOOL_CALLS)}-call execution limit`,
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
|
|
266
546
|
function strictFusionWebFetchArgs(args: unknown): FusionWebFetchParamsValue {
|
|
267
547
|
if (typeof args !== 'object' || args === null || Array.isArray(args)) {
|
|
268
548
|
throw new Error(`${FUSION_WEB_FETCH_TOOL_NAME} arguments must be an object`);
|
|
@@ -401,6 +681,82 @@ export default function fusionChildExtension(pi: ExtensionAPI): void {
|
|
|
401
681
|
}
|
|
402
682
|
const declaredResearchUrls = researchEnabled === '1' ? loadDeclaredResearchUrls() : undefined;
|
|
403
683
|
const fetchAuditMetadata = new Map<string, FusionWebFetchAuditMetadata>();
|
|
684
|
+
let providerRequestCount = 0;
|
|
685
|
+
let toolCallCount = 0;
|
|
686
|
+
let runtimeGuardFailed = false;
|
|
687
|
+
let settlementPublished = false;
|
|
688
|
+
const childResultRecords: FusionChildResultMetadata[] = [];
|
|
689
|
+
let pendingCacheObservation: FusionClaudeCacheObservation | undefined;
|
|
690
|
+
|
|
691
|
+
pi.on('before_provider_request', async (event, ctx) => {
|
|
692
|
+
providerRequestCount += 1;
|
|
693
|
+
if (runtimeGuardFailed) {
|
|
694
|
+
ctx.abort();
|
|
695
|
+
return event.payload;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const model = ctx.model;
|
|
699
|
+
let cacheNormalizedPayload = event.payload;
|
|
700
|
+
let cacheObservation: FusionClaudeCacheObservation;
|
|
701
|
+
try {
|
|
702
|
+
if (model?.provider === 'anthropic') {
|
|
703
|
+
const supportsLongCacheRetention =
|
|
704
|
+
model.compat !== undefined && 'supportsLongCacheRetention' in model.compat
|
|
705
|
+
? model.compat.supportsLongCacheRetention
|
|
706
|
+
: undefined;
|
|
707
|
+
const normalized = normalizeFusionClaudeCachePayload({
|
|
708
|
+
payload: event.payload,
|
|
709
|
+
requestOrdinal: providerRequestCount,
|
|
710
|
+
supportsLongCacheRetention:
|
|
711
|
+
typeof supportsLongCacheRetention === 'boolean'
|
|
712
|
+
? supportsLongCacheRetention
|
|
713
|
+
: undefined,
|
|
714
|
+
});
|
|
715
|
+
cacheNormalizedPayload = normalized.payload;
|
|
716
|
+
cacheObservation = normalized.observation;
|
|
717
|
+
} else {
|
|
718
|
+
cacheObservation = nonAnthropicFusionCacheObservation(providerRequestCount);
|
|
719
|
+
}
|
|
720
|
+
} catch (error) {
|
|
721
|
+
const guard = invalidFusionRuntimeRequest(
|
|
722
|
+
{
|
|
723
|
+
payload: event.payload,
|
|
724
|
+
provider: model?.provider,
|
|
725
|
+
model: model?.id,
|
|
726
|
+
contextWindowTokens: model?.contextWindow,
|
|
727
|
+
maxOutputTokens: model?.maxTokens,
|
|
728
|
+
requestOrdinal: providerRequestCount,
|
|
729
|
+
toolCallCount,
|
|
730
|
+
},
|
|
731
|
+
`Claude cache policy rejected the final payload: ${error instanceof Error ? error.message : String(error)}`,
|
|
732
|
+
'claude_cache_policy',
|
|
733
|
+
);
|
|
734
|
+
runtimeGuardFailed = true;
|
|
735
|
+
latchAuditProcessFailure();
|
|
736
|
+
ctx.abort();
|
|
737
|
+
await writeRuntimeGuard(guard);
|
|
738
|
+
return event.payload;
|
|
739
|
+
}
|
|
740
|
+
pendingCacheObservation = cacheObservation;
|
|
741
|
+
|
|
742
|
+
const prepared = prepareFusionRuntimeRequest({
|
|
743
|
+
payload: cacheNormalizedPayload,
|
|
744
|
+
provider: model?.provider,
|
|
745
|
+
model: model?.id,
|
|
746
|
+
contextWindowTokens: model?.contextWindow,
|
|
747
|
+
maxOutputTokens: model?.maxTokens,
|
|
748
|
+
requestOrdinal: providerRequestCount,
|
|
749
|
+
toolCallCount,
|
|
750
|
+
});
|
|
751
|
+
const guard = prepared.guard;
|
|
752
|
+
if (guard === undefined) return prepared.payload;
|
|
753
|
+
runtimeGuardFailed = true;
|
|
754
|
+
latchAuditProcessFailure();
|
|
755
|
+
ctx.abort();
|
|
756
|
+
await writeRuntimeGuard(guard);
|
|
757
|
+
return prepared.payload;
|
|
758
|
+
});
|
|
759
|
+
|
|
404
760
|
if (toolCallLogPath !== undefined) {
|
|
405
761
|
// Establish the audit file before tools can run. Exclusive creation makes a reused
|
|
406
762
|
// attempt path or redirected file loud instead of appending to untrusted history.
|
|
@@ -441,7 +797,8 @@ export default function fusionChildExtension(pi: ExtensionAPI): void {
|
|
|
441
797
|
}
|
|
442
798
|
phase = 'finalizing';
|
|
443
799
|
const unmatchedStarts = starts.size;
|
|
444
|
-
const complete =
|
|
800
|
+
const complete =
|
|
801
|
+
normalSettlement && !auditFailed && !runtimeGuardFailed && unmatchedStarts === 0;
|
|
445
802
|
if (!complete) {
|
|
446
803
|
auditFailed = true;
|
|
447
804
|
latchAuditProcessFailure();
|
|
@@ -460,9 +817,30 @@ export default function fusionChildExtension(pi: ExtensionAPI): void {
|
|
|
460
817
|
}
|
|
461
818
|
};
|
|
462
819
|
|
|
463
|
-
pi.on('tool_call', (event) => {
|
|
820
|
+
pi.on('tool_call', async (event, ctx) => {
|
|
464
821
|
try {
|
|
465
822
|
requireOpen('tool_call');
|
|
823
|
+
if (runtimeGuardFailed) {
|
|
824
|
+
ctx.abort();
|
|
825
|
+
return { block: true, reason: 'fusion child runtime guard already refused the run' };
|
|
826
|
+
}
|
|
827
|
+
toolCallCount += 1;
|
|
828
|
+
const model = ctx.model;
|
|
829
|
+
const guard = evaluateFusionRuntimeToolLimit({
|
|
830
|
+
provider: model?.provider,
|
|
831
|
+
model: model?.id,
|
|
832
|
+
contextWindowTokens: model?.contextWindow,
|
|
833
|
+
maxOutputTokens: model?.maxTokens,
|
|
834
|
+
requestOrdinal: providerRequestCount,
|
|
835
|
+
toolCallCount,
|
|
836
|
+
});
|
|
837
|
+
if (guard !== undefined) {
|
|
838
|
+
runtimeGuardFailed = true;
|
|
839
|
+
latchAuditProcessFailure();
|
|
840
|
+
ctx.abort();
|
|
841
|
+
await writeRuntimeGuard(guard);
|
|
842
|
+
return { block: true, reason: guard.message };
|
|
843
|
+
}
|
|
466
844
|
if (starts.has(event.toolCallId)) {
|
|
467
845
|
throw new Error(`fusion tool-call log duplicate start for ${event.toolCallId}`);
|
|
468
846
|
}
|
|
@@ -470,8 +848,9 @@ export default function fusionChildExtension(pi: ExtensionAPI): void {
|
|
|
470
848
|
startedAt: Date.now(),
|
|
471
849
|
toolName: event.toolName,
|
|
472
850
|
});
|
|
851
|
+
return undefined;
|
|
473
852
|
} catch (error) {
|
|
474
|
-
failAudit(error);
|
|
853
|
+
return failAudit(error);
|
|
475
854
|
}
|
|
476
855
|
});
|
|
477
856
|
pi.on('tool_result', (event) => {
|
|
@@ -597,6 +976,36 @@ export default function fusionChildExtension(pi: ExtensionAPI): void {
|
|
|
597
976
|
|
|
598
977
|
pi.on('message_end', async (event) => {
|
|
599
978
|
if (event.message.role !== 'assistant') return;
|
|
600
|
-
|
|
979
|
+
const cacheObservation = pendingCacheObservation;
|
|
980
|
+
if (cacheObservation === undefined) {
|
|
981
|
+
latchAuditProcessFailure();
|
|
982
|
+
throw new Error('fusion child assistant result has no matching cache-policy observation');
|
|
983
|
+
}
|
|
984
|
+
pendingCacheObservation = undefined;
|
|
985
|
+
const record = buildFusionChildResultMetadata(event.message, cacheObservation);
|
|
986
|
+
await writeMetadata(record);
|
|
987
|
+
childResultRecords.push(record);
|
|
988
|
+
});
|
|
989
|
+
pi.on('agent_settled', async (_event, ctx) => {
|
|
990
|
+
if (settlementPublished) {
|
|
991
|
+
latchAuditProcessFailure();
|
|
992
|
+
throw new Error('fusion child received duplicate agent_settled for result settlement');
|
|
993
|
+
}
|
|
994
|
+
if (!ctx.isIdle()) {
|
|
995
|
+
latchAuditProcessFailure();
|
|
996
|
+
throw new Error('fusion child result settlement observed agent_settled while not idle');
|
|
997
|
+
}
|
|
998
|
+
settlementPublished = true;
|
|
999
|
+
const cacheObservationFailed = pendingCacheObservation !== undefined;
|
|
1000
|
+
const settlement = buildFusionChildSettlement(
|
|
1001
|
+
childResultRecords,
|
|
1002
|
+
runtimeGuardFailed,
|
|
1003
|
+
cacheObservationFailed,
|
|
1004
|
+
);
|
|
1005
|
+
if (settlement.status !== 'complete') latchAuditProcessFailure();
|
|
1006
|
+
await writeSettlement(settlement);
|
|
1007
|
+
});
|
|
1008
|
+
pi.on('session_shutdown', () => {
|
|
1009
|
+
if (!settlementPublished) latchAuditProcessFailure();
|
|
601
1010
|
});
|
|
602
1011
|
}
|