pi-background-tasks 1.0.3 → 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 +3 -3
- package/TEST_PLAN.md +10 -9
- package/docs/attestations.json +5 -5
- package/docs/manifest.json +9 -5
- 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 +6 -6
- package/docs/subsystems/fusion.md +18 -11
- package/package.json +4 -3
- 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 +617 -30
- package/src/core/fusion/types.ts +29 -7
- package/src/fusion-child-extension.ts +659 -55
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
closeSync,
|
|
4
|
+
constants,
|
|
5
|
+
fstatSync,
|
|
6
|
+
fsyncSync,
|
|
7
|
+
openSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
writeSync,
|
|
10
|
+
} from 'node:fs';
|
|
11
|
+
import { dirname } from 'node:path';
|
|
12
|
+
import { parseJsonText } from './core/common.js';
|
|
3
13
|
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
4
14
|
import { Type, type Static } from 'typebox';
|
|
15
|
+
import {
|
|
16
|
+
estimateInputTokens,
|
|
17
|
+
knownJsonSegment,
|
|
18
|
+
resolveTokenBudgetFamily,
|
|
19
|
+
} from './core/context/token-budget.js';
|
|
5
20
|
import {
|
|
6
21
|
FUSION_TOOL_CALL_LOG_SCHEMA_VERSION,
|
|
7
22
|
FUSION_WEB_FETCH_TOOL_NAME,
|
|
@@ -17,32 +32,75 @@ import {
|
|
|
17
32
|
parseFusionSourcePolicy,
|
|
18
33
|
} from './core/fusion/source-policy.js';
|
|
19
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,
|
|
20
42
|
FUSION_CHILD_MAX_TOTAL_TOOL_RESULT_BYTES,
|
|
43
|
+
FUSION_CHILD_MIN_OUTPUT_RESERVE_TOKENS,
|
|
21
44
|
FUSION_CHILD_RESULT_PREFIX,
|
|
45
|
+
FUSION_CHILD_SAFETY_RESERVE_TOKENS,
|
|
46
|
+
FUSION_CHILD_SETTLEMENT_PREFIX,
|
|
22
47
|
FUSION_RESEARCH_ENABLED_ENV,
|
|
48
|
+
FUSION_RUNTIME_GUARD_PREFIX,
|
|
49
|
+
FUSION_RUNTIME_GUARD_SCHEMA_VERSION,
|
|
23
50
|
FUSION_SOURCE_POLICY_PATH_ENV,
|
|
24
51
|
FUSION_SOURCE_POLICY_SHA256_ENV,
|
|
25
52
|
FUSION_TOOL_CALL_LOG_PATH_ENV,
|
|
26
53
|
FUSION_TOOL_CALL_SEAL_SCHEMA_VERSION,
|
|
27
54
|
FUSION_TOOL_CALL_SEAL_SUFFIX,
|
|
28
55
|
buildFusionChildResultMetadata,
|
|
56
|
+
buildFusionChildSettlement,
|
|
29
57
|
type FusionChildResultMetadata,
|
|
58
|
+
type FusionChildSettlementRecord,
|
|
59
|
+
type FusionRuntimeGuardCode,
|
|
60
|
+
type FusionRuntimeGuardRecord,
|
|
30
61
|
} from './core/fusion/child-protocol.js';
|
|
31
62
|
|
|
32
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,
|
|
33
80
|
FUSION_CHILD_MAX_TOTAL_TOOL_RESULT_BYTES,
|
|
81
|
+
FUSION_CHILD_MIN_OUTPUT_RESERVE_TOKENS,
|
|
34
82
|
FUSION_CHILD_RESULT_PREFIX,
|
|
35
83
|
FUSION_CHILD_RESULT_SCHEMA_VERSION,
|
|
84
|
+
FUSION_CHILD_SAFETY_RESERVE_TOKENS,
|
|
85
|
+
FUSION_CHILD_SETTLEMENT_PREFIX,
|
|
86
|
+
FUSION_CHILD_SETTLEMENT_SCHEMA_VERSION,
|
|
36
87
|
FUSION_RESEARCH_ENABLED_ENV,
|
|
88
|
+
FUSION_RUNTIME_GUARD_PREFIX,
|
|
89
|
+
FUSION_RUNTIME_GUARD_SCHEMA_VERSION,
|
|
37
90
|
FUSION_SOURCE_POLICY_PATH_ENV,
|
|
38
91
|
FUSION_SOURCE_POLICY_SHA256_ENV,
|
|
39
92
|
FUSION_TOOL_CALL_LOG_PATH_ENV,
|
|
40
93
|
FUSION_TOOL_CALL_SEAL_SCHEMA_VERSION,
|
|
41
94
|
FUSION_TOOL_CALL_SEAL_SUFFIX,
|
|
42
95
|
buildFusionChildResultMetadata,
|
|
96
|
+
buildFusionChildSettlement,
|
|
43
97
|
type FusionChildResultMetadata,
|
|
44
98
|
type FusionChildResultUsageMetadata,
|
|
99
|
+
type FusionChildSettlementFailureReason,
|
|
100
|
+
type FusionChildSettlementRecord,
|
|
45
101
|
type FusionChildTextBlockMetadata,
|
|
102
|
+
type FusionRuntimeGuardCode,
|
|
103
|
+
type FusionRuntimeGuardRecord,
|
|
46
104
|
} from './core/fusion/child-protocol.js';
|
|
47
105
|
|
|
48
106
|
const FUSION_CHILD_O_NOFOLLOW = typeof constants.O_NOFOLLOW === 'number' ? constants.O_NOFOLLOW : 0;
|
|
@@ -100,24 +158,115 @@ function utf8JsonBytes(value: unknown, label: string): Buffer {
|
|
|
100
158
|
return Buffer.from(text, 'utf8');
|
|
101
159
|
}
|
|
102
160
|
|
|
103
|
-
function
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
161
|
+
function throwableError(value: unknown): Error {
|
|
162
|
+
return value instanceof Error ? value : new Error(String(value));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function writeAllSync(fd: number, bytes: Buffer, label: string): void {
|
|
166
|
+
let offset = 0;
|
|
167
|
+
while (offset < bytes.length) {
|
|
168
|
+
const written = writeSync(fd, bytes, offset, bytes.length - offset, null);
|
|
169
|
+
if (written <= 0) {
|
|
170
|
+
throw new Error(`${label} made no write progress at byte ${String(offset)}`);
|
|
171
|
+
}
|
|
172
|
+
offset += written;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function withRegularFileDescriptorSync(
|
|
177
|
+
path: string,
|
|
178
|
+
flags: number,
|
|
179
|
+
mode: number | undefined,
|
|
180
|
+
label: string,
|
|
181
|
+
operation: (fd: number) => void,
|
|
182
|
+
): void {
|
|
108
183
|
let fd: number | undefined;
|
|
184
|
+
let primaryFailure: unknown;
|
|
185
|
+
let closeFailure: unknown;
|
|
109
186
|
try {
|
|
110
|
-
fd = openSync(path,
|
|
111
|
-
const
|
|
112
|
-
if (
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
187
|
+
fd = mode === undefined ? openSync(path, flags) : openSync(path, flags, mode);
|
|
188
|
+
const stats = fstatSync(fd);
|
|
189
|
+
if (!stats.isFile()) throw new Error(`${label} at ${path} is not a regular file`);
|
|
190
|
+
operation(fd);
|
|
191
|
+
} catch (error) {
|
|
192
|
+
primaryFailure = error;
|
|
193
|
+
}
|
|
194
|
+
if (fd !== undefined) {
|
|
195
|
+
try {
|
|
196
|
+
closeSync(fd);
|
|
197
|
+
} catch (error) {
|
|
198
|
+
closeFailure = error;
|
|
116
199
|
}
|
|
200
|
+
}
|
|
201
|
+
if (primaryFailure !== undefined && closeFailure !== undefined) {
|
|
202
|
+
throw new AggregateError(
|
|
203
|
+
[primaryFailure, closeFailure],
|
|
204
|
+
`${label} operation and descriptor close both failed`,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
if (primaryFailure !== undefined) throw throwableError(primaryFailure);
|
|
208
|
+
if (closeFailure !== undefined) throw throwableError(closeFailure);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function fsyncParentDirectorySync(path: string): void {
|
|
212
|
+
if (process.platform === 'win32') return;
|
|
213
|
+
const parent = dirname(path);
|
|
214
|
+
let fd: number | undefined;
|
|
215
|
+
let primaryFailure: unknown;
|
|
216
|
+
let closeFailure: unknown;
|
|
217
|
+
try {
|
|
218
|
+
fd = openSync(parent, constants.O_RDONLY | FUSION_CHILD_O_NOFOLLOW);
|
|
219
|
+
const stats = fstatSync(fd);
|
|
220
|
+
if (!stats.isDirectory())
|
|
221
|
+
throw new Error(`fusion audit parent at ${parent} is not a directory`);
|
|
117
222
|
fsyncSync(fd);
|
|
118
|
-
}
|
|
119
|
-
|
|
223
|
+
} catch (error) {
|
|
224
|
+
primaryFailure = error;
|
|
120
225
|
}
|
|
226
|
+
if (fd !== undefined) {
|
|
227
|
+
try {
|
|
228
|
+
closeSync(fd);
|
|
229
|
+
} catch (error) {
|
|
230
|
+
closeFailure = error;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (primaryFailure !== undefined && closeFailure !== undefined) {
|
|
234
|
+
throw new AggregateError(
|
|
235
|
+
[primaryFailure, closeFailure],
|
|
236
|
+
'fusion audit directory sync and descriptor close both failed',
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
if (primaryFailure !== undefined) throw throwableError(primaryFailure);
|
|
240
|
+
if (closeFailure !== undefined) throw throwableError(closeFailure);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function createToolCallLog(path: string): void {
|
|
244
|
+
withRegularFileDescriptorSync(
|
|
245
|
+
path,
|
|
246
|
+
constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | FUSION_CHILD_O_NOFOLLOW,
|
|
247
|
+
0o600,
|
|
248
|
+
'fusion tool-call log',
|
|
249
|
+
(fd) => {
|
|
250
|
+
fsyncSync(fd);
|
|
251
|
+
},
|
|
252
|
+
);
|
|
253
|
+
fsyncParentDirectorySync(path);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function appendToolCallLogLine(path: string, record: FusionToolCallLogRecord): void {
|
|
257
|
+
// The log is an audit trail, not a payload copy: raw tool arguments/results may
|
|
258
|
+
// contain secrets, so only byte counts and SHA-256 digests are persisted.
|
|
259
|
+
const bytes = Buffer.from(`${JSON.stringify(record)}\n`, 'utf8');
|
|
260
|
+
withRegularFileDescriptorSync(
|
|
261
|
+
path,
|
|
262
|
+
constants.O_WRONLY | constants.O_APPEND | FUSION_CHILD_O_NOFOLLOW,
|
|
263
|
+
undefined,
|
|
264
|
+
'fusion tool-call log',
|
|
265
|
+
(fd) => {
|
|
266
|
+
writeAllSync(fd, bytes, 'fusion tool-call log append');
|
|
267
|
+
fsyncSync(fd);
|
|
268
|
+
},
|
|
269
|
+
);
|
|
121
270
|
}
|
|
122
271
|
|
|
123
272
|
function writeToolCallLogSeal(
|
|
@@ -126,7 +275,7 @@ function writeToolCallLogSeal(
|
|
|
126
275
|
totalResultBytes: number,
|
|
127
276
|
complete: boolean,
|
|
128
277
|
): void {
|
|
129
|
-
const logBytes =
|
|
278
|
+
const logBytes = readRegularFileNoSymlinkSync(path, 'fusion tool-call log');
|
|
130
279
|
const seal = {
|
|
131
280
|
schema_version: FUSION_TOOL_CALL_SEAL_SCHEMA_VERSION,
|
|
132
281
|
status: complete ? 'complete' : 'failed',
|
|
@@ -135,17 +284,22 @@ function writeToolCallLogSeal(
|
|
|
135
284
|
log_sha256: sha256(logBytes),
|
|
136
285
|
} as const;
|
|
137
286
|
const bytes = Buffer.from(`${JSON.stringify(seal)}\n`, 'utf8');
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
287
|
+
const sealPath = `${path}${FUSION_TOOL_CALL_SEAL_SUFFIX}`;
|
|
288
|
+
withRegularFileDescriptorSync(
|
|
289
|
+
sealPath,
|
|
290
|
+
constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | FUSION_CHILD_O_NOFOLLOW,
|
|
291
|
+
0o600,
|
|
292
|
+
'fusion tool-call audit completion seal',
|
|
293
|
+
(fd) => {
|
|
294
|
+
writeAllSync(fd, bytes, 'fusion tool-call audit completion seal');
|
|
295
|
+
fsyncSync(fd);
|
|
296
|
+
},
|
|
297
|
+
);
|
|
298
|
+
fsyncParentDirectorySync(sealPath);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function latchAuditProcessFailure(): void {
|
|
302
|
+
if (process.exitCode === undefined || process.exitCode === 0) process.exitCode = 1;
|
|
149
303
|
}
|
|
150
304
|
|
|
151
305
|
async function writeMetadata(record: FusionChildResultMetadata): Promise<void> {
|
|
@@ -158,6 +312,237 @@ async function writeMetadata(record: FusionChildResultMetadata): Promise<void> {
|
|
|
158
312
|
});
|
|
159
313
|
}
|
|
160
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
|
+
|
|
161
546
|
function strictFusionWebFetchArgs(args: unknown): FusionWebFetchParamsValue {
|
|
162
547
|
if (typeof args !== 'object' || args === null || Array.isArray(args)) {
|
|
163
548
|
throw new Error(`${FUSION_WEB_FETCH_TOOL_NAME} arguments must be an object`);
|
|
@@ -165,7 +550,9 @@ function strictFusionWebFetchArgs(args: unknown): FusionWebFetchParamsValue {
|
|
|
165
550
|
const keys = Object.keys(args);
|
|
166
551
|
const unknownKeys = keys.filter((key) => key !== 'url' && key !== 'extract');
|
|
167
552
|
if (unknownKeys.length > 0 || !keys.includes('url')) {
|
|
168
|
-
throw new Error(
|
|
553
|
+
throw new Error(
|
|
554
|
+
`${FUSION_WEB_FETCH_TOOL_NAME} arguments must contain url and optional extract only`,
|
|
555
|
+
);
|
|
169
556
|
}
|
|
170
557
|
const url = Reflect.get(args, 'url');
|
|
171
558
|
if (typeof url !== 'string' || url.trim().length === 0) {
|
|
@@ -189,7 +576,10 @@ function stringField(value: object, key: string): string | undefined {
|
|
|
189
576
|
return typeof field === 'string' && field.length > 0 ? field : undefined;
|
|
190
577
|
}
|
|
191
578
|
|
|
192
|
-
function fetchAuditMetadataFromObject(
|
|
579
|
+
function fetchAuditMetadataFromObject(
|
|
580
|
+
value: object,
|
|
581
|
+
fallbackUrl: string,
|
|
582
|
+
): FusionWebFetchAuditMetadata {
|
|
193
583
|
const metadata: FusionWebFetchAuditMetadata = {
|
|
194
584
|
url: stringField(value, 'url') ?? canonicalizeFusionPublicUrl(fallbackUrl),
|
|
195
585
|
};
|
|
@@ -204,7 +594,10 @@ function fetchAuditMetadataFromObject(value: object, fallbackUrl: string): Fusio
|
|
|
204
594
|
return metadata;
|
|
205
595
|
}
|
|
206
596
|
|
|
207
|
-
function fetchAuditMetadataFromError(
|
|
597
|
+
function fetchAuditMetadataFromError(
|
|
598
|
+
error: unknown,
|
|
599
|
+
attemptedUrl: string,
|
|
600
|
+
): FusionWebFetchAuditMetadata {
|
|
208
601
|
const metadata: FusionWebFetchAuditMetadata = { rejected_url_sha256: sha256(attemptedUrl) };
|
|
209
602
|
if (error instanceof FusionWebFetchError && typeof error === 'object' && error !== null) {
|
|
210
603
|
const status = numberField(error, 'status');
|
|
@@ -213,7 +606,6 @@ function fetchAuditMetadataFromError(error: unknown, attemptedUrl: string): Fusi
|
|
|
213
606
|
return metadata;
|
|
214
607
|
}
|
|
215
608
|
|
|
216
|
-
|
|
217
609
|
function readRegularFileNoSymlinkSync(path: string, label: string): Buffer {
|
|
218
610
|
let fd: number | undefined;
|
|
219
611
|
try {
|
|
@@ -237,13 +629,17 @@ function loadDeclaredResearchUrls(): ReadonlySet<string> {
|
|
|
237
629
|
const policyPath = process.env[FUSION_SOURCE_POLICY_PATH_ENV];
|
|
238
630
|
const expectedHash = process.env[FUSION_SOURCE_POLICY_SHA256_ENV];
|
|
239
631
|
if (policyPath === undefined || expectedHash === undefined) {
|
|
240
|
-
throw new Error(
|
|
632
|
+
throw new Error(
|
|
633
|
+
`${FUSION_WEB_FETCH_TOOL_NAME} research mode requires source policy path and sha256`,
|
|
634
|
+
);
|
|
241
635
|
}
|
|
242
|
-
if (!/^[0-9a-f]{64}$/.test(expectedHash))
|
|
636
|
+
if (!/^[0-9a-f]{64}$/.test(expectedHash))
|
|
637
|
+
throw new Error('fusion source policy hash is malformed');
|
|
243
638
|
const bytes = readRegularFileNoSymlinkSync(policyPath, 'fusion source policy');
|
|
244
639
|
if (sha256(bytes) !== expectedHash) throw new Error('fusion source policy hash mismatch');
|
|
245
640
|
const text = bytes.toString('utf8');
|
|
246
|
-
if (!Buffer.from(text, 'utf8').equals(bytes))
|
|
641
|
+
if (!Buffer.from(text, 'utf8').equals(bytes))
|
|
642
|
+
throw new Error('fusion source policy is not UTF-8');
|
|
247
643
|
const parsed = parseFusionSourcePolicy(JSON.parse(text));
|
|
248
644
|
return new Set(parsed.sources.map((source) => source.canonical_url));
|
|
249
645
|
}
|
|
@@ -279,30 +675,196 @@ export default function fusionChildExtension(pi: ExtensionAPI): void {
|
|
|
279
675
|
throw new Error(`${FUSION_RESEARCH_ENABLED_ENV} must be unset or exactly 1`);
|
|
280
676
|
}
|
|
281
677
|
if (researchEnabled === '1' && toolCallLogPath === undefined) {
|
|
282
|
-
throw new Error(
|
|
678
|
+
throw new Error(
|
|
679
|
+
`${FUSION_WEB_FETCH_TOOL_NAME} research mode requires ${FUSION_TOOL_CALL_LOG_PATH_ENV}`,
|
|
680
|
+
);
|
|
283
681
|
}
|
|
284
682
|
const declaredResearchUrls = researchEnabled === '1' ? loadDeclaredResearchUrls() : undefined;
|
|
285
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
|
+
|
|
286
760
|
if (toolCallLogPath !== undefined) {
|
|
287
|
-
//
|
|
288
|
-
//
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
761
|
+
// Establish the audit file before tools can run. Exclusive creation makes a reused
|
|
762
|
+
// attempt path or redirected file loud instead of appending to untrusted history.
|
|
763
|
+
try {
|
|
764
|
+
createToolCallLog(toolCallLogPath);
|
|
765
|
+
} catch (error) {
|
|
766
|
+
latchAuditProcessFailure();
|
|
767
|
+
throw error;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
type AuditPhase = 'open' | 'finalizing' | 'sealed-complete' | 'sealed-failed';
|
|
771
|
+
interface ToolStart {
|
|
772
|
+
startedAt: number;
|
|
773
|
+
toolName: string;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
let phase: AuditPhase = 'open';
|
|
292
777
|
let ordinal = 0;
|
|
293
778
|
let totalToolResultBytes = 0;
|
|
294
779
|
let auditFailed = false;
|
|
295
|
-
const starts = new Map<string,
|
|
296
|
-
|
|
297
|
-
|
|
780
|
+
const starts = new Map<string, ToolStart>();
|
|
781
|
+
|
|
782
|
+
const failAudit = (error: unknown): never => {
|
|
783
|
+
auditFailed = true;
|
|
784
|
+
latchAuditProcessFailure();
|
|
785
|
+
throw error instanceof Error ? error : new Error(String(error));
|
|
786
|
+
};
|
|
787
|
+
const requireOpen = (eventName: string): void => {
|
|
788
|
+
if (phase !== 'open') {
|
|
789
|
+
failAudit(`fusion tool-call audit received ${eventName} while ${phase}`);
|
|
790
|
+
}
|
|
791
|
+
};
|
|
792
|
+
const finalizeAudit = (normalSettlement: boolean, trigger: string): void => {
|
|
793
|
+
if (phase !== 'open') {
|
|
794
|
+
failAudit(
|
|
795
|
+
`fusion tool-call audit received duplicate finalization from ${trigger} while ${phase}`,
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
phase = 'finalizing';
|
|
799
|
+
const unmatchedStarts = starts.size;
|
|
800
|
+
const complete =
|
|
801
|
+
normalSettlement && !auditFailed && !runtimeGuardFailed && unmatchedStarts === 0;
|
|
802
|
+
if (!complete) {
|
|
803
|
+
auditFailed = true;
|
|
804
|
+
latchAuditProcessFailure();
|
|
805
|
+
}
|
|
806
|
+
try {
|
|
807
|
+
writeToolCallLogSeal(toolCallLogPath, ordinal, totalToolResultBytes, complete);
|
|
808
|
+
} catch (error) {
|
|
809
|
+
phase = 'sealed-failed';
|
|
810
|
+
failAudit(error);
|
|
811
|
+
}
|
|
812
|
+
phase = complete ? 'sealed-complete' : 'sealed-failed';
|
|
813
|
+
if (!complete) {
|
|
814
|
+
failAudit(
|
|
815
|
+
`fusion tool-call audit finalized as failed from ${trigger}: ${String(unmatchedStarts)} unmatched tool start(s)`,
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
};
|
|
819
|
+
|
|
820
|
+
pi.on('tool_call', async (event, ctx) => {
|
|
821
|
+
try {
|
|
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
|
+
}
|
|
844
|
+
if (starts.has(event.toolCallId)) {
|
|
845
|
+
throw new Error(`fusion tool-call log duplicate start for ${event.toolCallId}`);
|
|
846
|
+
}
|
|
847
|
+
starts.set(event.toolCallId, {
|
|
848
|
+
startedAt: Date.now(),
|
|
849
|
+
toolName: event.toolName,
|
|
850
|
+
});
|
|
851
|
+
return undefined;
|
|
852
|
+
} catch (error) {
|
|
853
|
+
return failAudit(error);
|
|
854
|
+
}
|
|
298
855
|
});
|
|
299
856
|
pi.on('tool_result', (event) => {
|
|
300
857
|
try {
|
|
858
|
+
requireOpen('tool_result');
|
|
301
859
|
const start = starts.get(event.toolCallId);
|
|
302
860
|
if (start === undefined) {
|
|
303
861
|
throw new Error(`fusion tool-call log missing start for ${event.toolCallId}`);
|
|
304
862
|
}
|
|
305
|
-
|
|
863
|
+
if (start.toolName !== event.toolName) {
|
|
864
|
+
throw new Error(
|
|
865
|
+
`fusion tool-call log tool mismatch for ${event.toolCallId}: started ${start.toolName}, completed ${event.toolName}`,
|
|
866
|
+
);
|
|
867
|
+
}
|
|
306
868
|
const argumentsBytes = utf8JsonBytes(event.input, 'arguments');
|
|
307
869
|
const resultBytes = utf8JsonBytes(
|
|
308
870
|
{
|
|
@@ -314,7 +876,7 @@ export default function fusionChildExtension(pi: ExtensionAPI): void {
|
|
|
314
876
|
'result',
|
|
315
877
|
);
|
|
316
878
|
const fetchMetadata = fetchAuditMetadata.get(event.toolCallId);
|
|
317
|
-
|
|
879
|
+
const nextTotalToolResultBytes = totalToolResultBytes + resultBytes.length;
|
|
318
880
|
const record: FusionToolCallLogRecord = {
|
|
319
881
|
schema_version: FUSION_TOOL_CALL_LOG_SCHEMA_VERSION,
|
|
320
882
|
ordinal,
|
|
@@ -324,25 +886,33 @@ export default function fusionChildExtension(pi: ExtensionAPI): void {
|
|
|
324
886
|
result_bytes: resultBytes.length,
|
|
325
887
|
result_sha256: sha256(resultBytes),
|
|
326
888
|
status: event.isError === true ? 'error' : 'ok',
|
|
327
|
-
duration_ms: Math.max(0, Date.now() - start),
|
|
889
|
+
duration_ms: Math.max(0, Date.now() - start.startedAt),
|
|
328
890
|
...(fetchMetadata === undefined ? {} : fetchMetadata),
|
|
329
891
|
};
|
|
330
|
-
ordinal += 1;
|
|
331
892
|
appendToolCallLogLine(toolCallLogPath, record);
|
|
332
|
-
|
|
893
|
+
starts.delete(event.toolCallId);
|
|
894
|
+
fetchAuditMetadata.delete(event.toolCallId);
|
|
895
|
+
ordinal += 1;
|
|
896
|
+
totalToolResultBytes = nextTotalToolResultBytes;
|
|
333
897
|
if (totalToolResultBytes > FUSION_CHILD_MAX_TOTAL_TOOL_RESULT_BYTES) {
|
|
334
898
|
throw new Error(
|
|
335
899
|
`fusion candidate exceeded the aggregate tool-output budget: ${String(totalToolResultBytes)} bytes across ${String(ordinal)} calls exceeds ${String(FUSION_CHILD_MAX_TOTAL_TOOL_RESULT_BYTES)}`,
|
|
336
900
|
);
|
|
337
901
|
}
|
|
338
902
|
} catch (error) {
|
|
339
|
-
|
|
340
|
-
throw error;
|
|
903
|
+
failAudit(error);
|
|
341
904
|
}
|
|
342
905
|
});
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
906
|
+
// agent_end is only the end of one low-level run. Pi may still retry, compact and
|
|
907
|
+
// retry, or consume queued continuations. Sealing there created stale prefix seals.
|
|
908
|
+
pi.on('agent_settled', (_event, ctx) => {
|
|
909
|
+
if (!ctx.isIdle()) {
|
|
910
|
+
failAudit('fusion child emitted agent_settled while the agent was not idle');
|
|
911
|
+
}
|
|
912
|
+
finalizeAudit(true, 'agent_settled');
|
|
913
|
+
});
|
|
914
|
+
pi.on('session_shutdown', () => {
|
|
915
|
+
if (phase === 'open') finalizeAudit(false, 'session_shutdown before agent_settled');
|
|
346
916
|
});
|
|
347
917
|
}
|
|
348
918
|
|
|
@@ -366,10 +936,14 @@ export default function fusionChildExtension(pi: ExtensionAPI): void {
|
|
|
366
936
|
try {
|
|
367
937
|
const canonicalUrl = canonicalizeFusionPublicUrl(params.url);
|
|
368
938
|
if (params.url !== canonicalUrl) {
|
|
369
|
-
throw new Error(
|
|
939
|
+
throw new Error(
|
|
940
|
+
`${FUSION_WEB_FETCH_TOOL_NAME} URL must exactly match its declared canonical URL`,
|
|
941
|
+
);
|
|
370
942
|
}
|
|
371
943
|
if (declaredResearchUrls === undefined || !declaredResearchUrls.has(canonicalUrl)) {
|
|
372
|
-
throw new Error(
|
|
944
|
+
throw new Error(
|
|
945
|
+
`${FUSION_WEB_FETCH_TOOL_NAME} URL was not declared in the research source policy`,
|
|
946
|
+
);
|
|
373
947
|
}
|
|
374
948
|
const result = await fusionWebFetch(
|
|
375
949
|
params.extract === undefined
|
|
@@ -402,6 +976,36 @@ export default function fusionChildExtension(pi: ExtensionAPI): void {
|
|
|
402
976
|
|
|
403
977
|
pi.on('message_end', async (event) => {
|
|
404
978
|
if (event.message.role !== 'assistant') return;
|
|
405
|
-
|
|
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();
|
|
406
1010
|
});
|
|
407
1011
|
}
|