@pi-unipi/background-tasks 2.16.1 → 2.17.0
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 +21 -27
- package/package.json +3 -4
- package/src/cards.ts +76 -0
- package/src/child-process.ts +1 -1
- package/src/config.ts +0 -42
- package/src/context-visible-conversation-v2.ts +1 -1
- package/src/delegate/artifacts.ts +1 -1
- package/src/delegate/launch.ts +17 -30
- package/src/delegate/result-package.ts +1 -1
- package/src/delegate/runner.ts +1 -20
- package/src/delegate/seed.ts +1 -1
- package/src/delegate-extension.ts +16 -168
- package/src/index.ts +53 -25
- package/src/json-utils.ts +56 -0
- package/src/package-assets.ts +51 -0
- package/src/registry.ts +8 -459
- package/src/task-manager.ts +13 -2
- package/src/tools.ts +4 -189
- package/src/types.ts +17 -70
- package/extensions/anthropic-attribution.ts +0 -1
- package/extensions/fusion-child.ts +0 -1
- package/src/anthropic-attribution-path.ts +0 -21
- package/src/anthropic-attribution.ts +0 -1983
- package/src/attested-pi-run.ts +0 -612
- package/src/fixtures/fusion-golden-bytes.json +0 -310
- package/src/fixtures/fusion-validate-golden-bytes.json +0 -282
- package/src/fusion/artifacts.ts +0 -967
- package/src/fusion/budget.ts +0 -1162
- package/src/fusion/child-protocol.ts +0 -305
- package/src/fusion/claude-cache.ts +0 -207
- package/src/fusion/clean-context.ts +0 -91
- package/src/fusion/config.ts +0 -449
- package/src/fusion/context.ts +0 -265
- package/src/fusion/evaluation.ts +0 -800
- package/src/fusion/orchestrator.ts +0 -1288
- package/src/fusion/output-contract.ts +0 -34
- package/src/fusion/pi-child.ts +0 -2373
- package/src/fusion/prompts.ts +0 -345
- package/src/fusion/result-package.ts +0 -959
- package/src/fusion/source-policy.ts +0 -257
- package/src/fusion/types.ts +0 -1139
- package/src/fusion/web-fetch.ts +0 -1060
- package/src/fusion/workflows.ts +0 -184
- package/src/fusion-child-extension.ts +0 -1052
- package/src/fusion-extension.ts +0 -1293
- package/src/ui/fusion-model-selector.ts +0 -322
package/src/fusion/pi-child.ts
DELETED
|
@@ -1,2373 +0,0 @@
|
|
|
1
|
-
import { spawn as nodeSpawn, type SpawnOptions } from 'node:child_process';
|
|
2
|
-
import { createHash } from 'node:crypto';
|
|
3
|
-
import { constants, existsSync } from 'node:fs';
|
|
4
|
-
import { open } from 'node:fs/promises';
|
|
5
|
-
import { dirname, resolve } from 'node:path';
|
|
6
|
-
import { fileURLToPath } from 'node:url';
|
|
7
|
-
import {
|
|
8
|
-
FUSION_CANDIDATE_OUTPUT_RECOVERY_PATH_ENV,
|
|
9
|
-
FUSION_CHILD_MAX_PROVIDER_REQUESTS,
|
|
10
|
-
FUSION_CHILD_MAX_TOOL_CALLS,
|
|
11
|
-
FUSION_CHILD_RESULT_PREFIX,
|
|
12
|
-
FUSION_CHILD_RESULT_SCHEMA_VERSION,
|
|
13
|
-
FUSION_CHILD_SETTLEMENT_PREFIX,
|
|
14
|
-
FUSION_CHILD_SETTLEMENT_SCHEMA_VERSION,
|
|
15
|
-
FUSION_RESEARCH_ENABLED_ENV,
|
|
16
|
-
FUSION_RUNTIME_GUARD_PREFIX,
|
|
17
|
-
FUSION_RUNTIME_GUARD_SCHEMA_VERSION,
|
|
18
|
-
FUSION_SOURCE_POLICY_PATH_ENV,
|
|
19
|
-
FUSION_SOURCE_POLICY_SHA256_ENV,
|
|
20
|
-
FUSION_TOOL_CALL_LOG_PATH_ENV,
|
|
21
|
-
FUSION_TOOL_CALL_SEAL_SCHEMA_VERSION,
|
|
22
|
-
FUSION_TOOL_CALL_SEAL_SUFFIX,
|
|
23
|
-
FUSION_CHILD_MAX_TOTAL_TOOL_RESULT_BYTES,
|
|
24
|
-
buildFusionChildSettlement,
|
|
25
|
-
isRecoverableFusionChildErrorRecord,
|
|
26
|
-
serializeFusionChildResultRecords,
|
|
27
|
-
type FusionChildOutputContractMetadata,
|
|
28
|
-
type FusionChildOutputRecoveryRole,
|
|
29
|
-
type FusionChildResultMetadata,
|
|
30
|
-
type FusionChildSettlementFailureReason,
|
|
31
|
-
type FusionChildSettlementRecord,
|
|
32
|
-
type FusionRuntimeGuardCode,
|
|
33
|
-
type FusionRuntimeGuardRecord,
|
|
34
|
-
} from './child-protocol.js';
|
|
35
|
-
import {
|
|
36
|
-
FUSION_CLAUDE_CACHE_BREAKPOINT_LIMIT,
|
|
37
|
-
FUSION_CLAUDE_CACHE_OBSERVATION_SCHEMA_VERSION,
|
|
38
|
-
FUSION_CLAUDE_CACHE_RETENTION_ENV,
|
|
39
|
-
type FusionClaudeCacheObservation,
|
|
40
|
-
type FusionClaudeCacheRetention,
|
|
41
|
-
} from './claude-cache.js';
|
|
42
|
-
import {
|
|
43
|
-
FUSION_FORBIDDEN_TOOLS,
|
|
44
|
-
FUSION_NO_TOOLS_CAPABILITY,
|
|
45
|
-
FUSION_INSPECT_TOOLS,
|
|
46
|
-
FUSION_RESEARCH_TOOLS,
|
|
47
|
-
FUSION_TOOL_CALL_LOG_SCHEMA_VERSION,
|
|
48
|
-
FUSION_WEB_FETCH_TOOL_NAME,
|
|
49
|
-
FusionError,
|
|
50
|
-
addFusionUsage,
|
|
51
|
-
cloneFusionUsage,
|
|
52
|
-
createEmptyFusionUsage,
|
|
53
|
-
type FusionCapability,
|
|
54
|
-
type FusionCandidateOutputRecovery,
|
|
55
|
-
type FusionChildRunResult,
|
|
56
|
-
type FusionErrorDetails,
|
|
57
|
-
type FusionStage,
|
|
58
|
-
type FusionToolCallLogRecord,
|
|
59
|
-
type FusionToolCallTrace,
|
|
60
|
-
type FusionUsage,
|
|
61
|
-
type ResolvedFusionModel,
|
|
62
|
-
} from './types.js';
|
|
63
|
-
import { isJsonObject, parseJsonText } from '../types.js';
|
|
64
|
-
import {
|
|
65
|
-
FUSION_CANDIDATE_MAX_OUTPUT_BYTES,
|
|
66
|
-
fusionJsonRenderedTextBytes,
|
|
67
|
-
} from './output-contract.js';
|
|
68
|
-
import { canonicalizeFusionPublicUrl, readFusionSourcePolicyFile } from './source-policy.js';
|
|
69
|
-
import {
|
|
70
|
-
assertWindowsCommandLineWithinLimit,
|
|
71
|
-
piLaunchArgv,
|
|
72
|
-
resolvePiLaunch,
|
|
73
|
-
type PiLaunchDependencies,
|
|
74
|
-
} from '../pi-launch.js';
|
|
75
|
-
import { resolveAnthropicAttributionExtensionPath } from '../anthropic-attribution-path.js';
|
|
76
|
-
|
|
77
|
-
// The response cap now applies to one final full answer, not cumulative Pi JSON events.
|
|
78
|
-
export const FUSION_CHILD_STDOUT_LIMIT_BYTES = 32 * 1024 * 1024;
|
|
79
|
-
export const FUSION_CHILD_STDERR_LIMIT_BYTES = 16 * 1024 * 1024;
|
|
80
|
-
export const FUSION_CHILD_TIMEOUT_MS = 50 * 60 * 1000;
|
|
81
|
-
/**
|
|
82
|
-
* Stale-action watchdog threshold.
|
|
83
|
-
*
|
|
84
|
-
* Activity is one stdout or stderr byte from the child. The child extension emits its
|
|
85
|
-
* compact metadata frame only at `message_end`, and Pi text mode writes stdout only for
|
|
86
|
-
* the final assistant message, so a single slow model turn is genuinely silent on both
|
|
87
|
-
* streams. The threshold must therefore exceed the longest plausible single turn, not the
|
|
88
|
-
* longest plausible tool call: a value tuned to tool latency would kill healthy children
|
|
89
|
-
* mid-reasoning. Thirty-five minutes leaves a wide margin over observed turn latency
|
|
90
|
-
* while remaining materially below the 50-minute absolute cap, so the stale-action
|
|
91
|
-
* watchdog retains an independent purpose instead of becoming an unreachable duplicate
|
|
92
|
-
* timeout.
|
|
93
|
-
*/
|
|
94
|
-
export const FUSION_CHILD_IDLE_TIMEOUT_MS = 35 * 60 * 1000;
|
|
95
|
-
export const FUSION_CHILD_KILL_GRACE_MS = 3000;
|
|
96
|
-
export const FUSION_CHILD_SIGKILL_WAIT_MS = 5000;
|
|
97
|
-
const FUSION_PI_CHILD_O_NOFOLLOW =
|
|
98
|
-
typeof constants.O_NOFOLLOW === 'number' ? constants.O_NOFOLLOW : 0;
|
|
99
|
-
|
|
100
|
-
export const FUSION_CHILD_REMOVED_ENV_KEYS = [
|
|
101
|
-
'PI_SESSION_ID',
|
|
102
|
-
'PI_SESSION_FILE',
|
|
103
|
-
'PI_PROVIDER',
|
|
104
|
-
'PI_MODEL',
|
|
105
|
-
'PI_REASONING_LEVEL',
|
|
106
|
-
'OPENROUTER_API_KEY',
|
|
107
|
-
'OPENROUTER_BASE_URL',
|
|
108
|
-
'OPENAI_API_KEY',
|
|
109
|
-
'OPENAI_BASE_URL',
|
|
110
|
-
'ANTHROPIC_API_KEY',
|
|
111
|
-
'ANTHROPIC_AUTH_TOKEN',
|
|
112
|
-
'ANTHROPIC_BASE_URL',
|
|
113
|
-
'AZURE_OPENAI_API_KEY',
|
|
114
|
-
'AZURE_OPENAI_BASE_URL',
|
|
115
|
-
'AZURE_OPENAI_ENDPOINT',
|
|
116
|
-
'AZURE_OPENAI_RESOURCE_NAME',
|
|
117
|
-
'AZURE_OPENAI_API_VERSION',
|
|
118
|
-
'AZURE_OPENAI_DEPLOYMENT_NAME_MAP',
|
|
119
|
-
'AZURE_OPENAI_AD_TOKEN',
|
|
120
|
-
'PI_API_KEY',
|
|
121
|
-
'PI_API_BASE_URL',
|
|
122
|
-
'PI_AUTH_FILE',
|
|
123
|
-
FUSION_TOOL_CALL_LOG_PATH_ENV,
|
|
124
|
-
FUSION_CANDIDATE_OUTPUT_RECOVERY_PATH_ENV,
|
|
125
|
-
FUSION_RESEARCH_ENABLED_ENV,
|
|
126
|
-
FUSION_SOURCE_POLICY_PATH_ENV,
|
|
127
|
-
FUSION_SOURCE_POLICY_SHA256_ENV,
|
|
128
|
-
] as const;
|
|
129
|
-
|
|
130
|
-
interface FusionReadableStream {
|
|
131
|
-
on(event: 'data', listener: (data: Buffer | string) => void): unknown;
|
|
132
|
-
off(event: 'data', listener: (data: Buffer | string) => void): unknown;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
interface FusionWritableStream {
|
|
136
|
-
write(data: Buffer, callback: (error?: Error | null) => void): boolean;
|
|
137
|
-
end(callback?: () => void): unknown;
|
|
138
|
-
once(event: 'error', listener: (error: Error) => void): unknown;
|
|
139
|
-
off(event: 'error', listener: (error: Error) => void): unknown;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export interface FusionChildProcess {
|
|
143
|
-
pid?: number | undefined;
|
|
144
|
-
stdin?: FusionWritableStream | null | undefined;
|
|
145
|
-
stdout?: FusionReadableStream | null | undefined;
|
|
146
|
-
stderr?: FusionReadableStream | null | undefined;
|
|
147
|
-
kill(signal?: NodeJS.Signals): boolean;
|
|
148
|
-
once(event: 'error', listener: (error: Error) => void): unknown;
|
|
149
|
-
once(
|
|
150
|
-
event: 'close',
|
|
151
|
-
listener: (code: number | null, signal: NodeJS.Signals | null) => void,
|
|
152
|
-
): unknown;
|
|
153
|
-
off(event: 'error', listener: (error: Error) => void): unknown;
|
|
154
|
-
off(
|
|
155
|
-
event: 'close',
|
|
156
|
-
listener: (code: number | null, signal: NodeJS.Signals | null) => void,
|
|
157
|
-
): unknown;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export type FusionChildSpawn = (
|
|
161
|
-
command: string,
|
|
162
|
-
args: string[],
|
|
163
|
-
options: SpawnOptions,
|
|
164
|
-
) => FusionChildProcess;
|
|
165
|
-
|
|
166
|
-
export type FusionKillProcess = (pid: number, signal?: NodeJS.Signals | number) => boolean;
|
|
167
|
-
|
|
168
|
-
export interface RunPiChildOptions {
|
|
169
|
-
stage: FusionStage;
|
|
170
|
-
slot?: 1 | 2 | 3;
|
|
171
|
-
attempt: number;
|
|
172
|
-
cwd: string;
|
|
173
|
-
model: ResolvedFusionModel;
|
|
174
|
-
capability?: FusionCapability | undefined;
|
|
175
|
-
systemPrompt: string;
|
|
176
|
-
userPrompt: string;
|
|
177
|
-
signal?: AbortSignal | undefined;
|
|
178
|
-
spawn?: FusionChildSpawn | undefined;
|
|
179
|
-
killProcess?: FusionKillProcess | undefined;
|
|
180
|
-
platform?: NodeJS.Platform | undefined;
|
|
181
|
-
env?: NodeJS.ProcessEnv | undefined;
|
|
182
|
-
stdoutLimitBytes?: number | undefined;
|
|
183
|
-
childExtensionPath?: string | undefined;
|
|
184
|
-
stderrLimitBytes?: number | undefined;
|
|
185
|
-
timeoutMs?: number | undefined;
|
|
186
|
-
idleTimeoutMs?: number | undefined;
|
|
187
|
-
killGraceMs?: number | undefined;
|
|
188
|
-
sigkillWaitMs?: number | undefined;
|
|
189
|
-
piLaunchDependencies?: PiLaunchDependencies | undefined;
|
|
190
|
-
toolCallLogPath?: string | undefined;
|
|
191
|
-
sourcePolicy?: { path: string; sha256: string } | undefined;
|
|
192
|
-
candidateOutputRecoveryPath?: string | undefined;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
interface CloseRecord {
|
|
196
|
-
code: number | null;
|
|
197
|
-
signal: NodeJS.Signals | null;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
interface ProcessState {
|
|
201
|
-
primaryError: FusionError | undefined;
|
|
202
|
-
cleanupErrors: string[];
|
|
203
|
-
terminationStarted: boolean;
|
|
204
|
-
termTimer: NodeJS.Timeout | undefined;
|
|
205
|
-
waitTimer: NodeJS.Timeout | undefined;
|
|
206
|
-
timeoutTimer: NodeJS.Timeout | undefined;
|
|
207
|
-
idleTimer: NodeJS.Timeout | undefined;
|
|
208
|
-
settled: boolean;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
interface ObservedChildSnapshot {
|
|
212
|
-
usage: FusionUsage;
|
|
213
|
-
provider?: string;
|
|
214
|
-
model?: string;
|
|
215
|
-
qualifiedId?: string;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
export class FusionChildRunError extends FusionError {
|
|
219
|
-
readonly events: Buffer;
|
|
220
|
-
readonly response: Buffer;
|
|
221
|
-
readonly stderr: Buffer;
|
|
222
|
-
readonly exitCode: number | null;
|
|
223
|
-
readonly signalName: NodeJS.Signals | null;
|
|
224
|
-
readonly usage: FusionUsage;
|
|
225
|
-
readonly provider: string | undefined;
|
|
226
|
-
readonly modelName: string | undefined;
|
|
227
|
-
readonly qualifiedId: string | undefined;
|
|
228
|
-
readonly outputRecovery: FusionCandidateOutputRecovery | undefined;
|
|
229
|
-
|
|
230
|
-
constructor(
|
|
231
|
-
error: FusionError,
|
|
232
|
-
events: Buffer,
|
|
233
|
-
response: Buffer,
|
|
234
|
-
stderr: Buffer,
|
|
235
|
-
close: CloseRecord,
|
|
236
|
-
observed: ObservedChildSnapshot,
|
|
237
|
-
outputRecovery?: FusionCandidateOutputRecovery,
|
|
238
|
-
) {
|
|
239
|
-
const details: FusionErrorDetails = {
|
|
240
|
-
code: error.code,
|
|
241
|
-
transient: error.transient,
|
|
242
|
-
childCreated: error.childCreated,
|
|
243
|
-
};
|
|
244
|
-
if (error.stage !== undefined) details.stage = error.stage;
|
|
245
|
-
if (error.slot !== undefined) details.slot = error.slot;
|
|
246
|
-
if (error.attempt !== undefined) details.attempt = error.attempt;
|
|
247
|
-
if (error.artifactDir !== undefined) details.artifactDir = error.artifactDir;
|
|
248
|
-
super(error.message, details);
|
|
249
|
-
this.name = 'FusionChildRunError';
|
|
250
|
-
this.events = events;
|
|
251
|
-
this.response = response;
|
|
252
|
-
this.stderr = stderr;
|
|
253
|
-
this.exitCode = close.code;
|
|
254
|
-
this.signalName = close.signal;
|
|
255
|
-
this.usage = cloneFusionUsage(observed.usage);
|
|
256
|
-
this.provider = observed.provider;
|
|
257
|
-
this.modelName = observed.model;
|
|
258
|
-
this.qualifiedId = observed.qualifiedId;
|
|
259
|
-
this.outputRecovery = outputRecovery;
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
export function fusionPiChildEnv(
|
|
264
|
-
env: NodeJS.ProcessEnv = process.env,
|
|
265
|
-
provider?: string | undefined,
|
|
266
|
-
): NodeJS.ProcessEnv {
|
|
267
|
-
const out: NodeJS.ProcessEnv = { ...env };
|
|
268
|
-
const removed = new Set<string>(FUSION_CHILD_REMOVED_ENV_KEYS);
|
|
269
|
-
for (const inheritedKey of Object.keys(out)) {
|
|
270
|
-
if (removed.has(inheritedKey.toUpperCase())) Reflect.deleteProperty(out, inheritedKey);
|
|
271
|
-
}
|
|
272
|
-
out['PI_SKIP_VERSION_CHECK'] = '1';
|
|
273
|
-
if (provider === 'anthropic' && out[FUSION_CLAUDE_CACHE_RETENTION_ENV] === undefined) {
|
|
274
|
-
out[FUSION_CLAUDE_CACHE_RETENTION_ENV] = 'long';
|
|
275
|
-
}
|
|
276
|
-
return out;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
export function resolveFusionChildExtensionPath(
|
|
280
|
-
moduleUrl = import.meta.url,
|
|
281
|
-
pathExists: (path: string) => boolean = existsSync,
|
|
282
|
-
): string {
|
|
283
|
-
const modulePath = fileURLToPath(moduleUrl);
|
|
284
|
-
const extension = modulePath.endsWith('.ts') ? 'fusion-child.ts' : 'fusion-child.js';
|
|
285
|
-
const candidate = resolve(dirname(modulePath), '../../extensions', extension);
|
|
286
|
-
if (!pathExists(candidate)) {
|
|
287
|
-
throw new Error(`Fusion child metadata extension is missing: ${candidate}`);
|
|
288
|
-
}
|
|
289
|
-
return candidate;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
/**
|
|
293
|
-
* Provider whose isolated children require the package-owned attribution and
|
|
294
|
-
* exact-match system-prompt sanitization extension.
|
|
295
|
-
*/
|
|
296
|
-
export const FUSION_SANITIZED_PROVIDER = 'anthropic';
|
|
297
|
-
|
|
298
|
-
export function assertFusionToolPolicyDisjoint(
|
|
299
|
-
allowlist: readonly string[] = FUSION_INSPECT_TOOLS,
|
|
300
|
-
denylist: readonly string[] = FUSION_FORBIDDEN_TOOLS,
|
|
301
|
-
): void {
|
|
302
|
-
for (const forbidden of denylist) {
|
|
303
|
-
if (allowlist.includes(forbidden)) {
|
|
304
|
-
throw new FusionError(
|
|
305
|
-
`fusion inspect capability would enable the forbidden tool ${forbidden}`,
|
|
306
|
-
{ code: 'orchestration_failed', childCreated: false },
|
|
307
|
-
);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
function researchToolAllowlist(): readonly string[] {
|
|
313
|
-
return FUSION_RESEARCH_TOOLS;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
function fusionToolArgv(capability: FusionCapability): string[] {
|
|
317
|
-
if (capability === 'reason') return ['--no-tools'];
|
|
318
|
-
if (capability === 'inspect') {
|
|
319
|
-
assertFusionToolPolicyDisjoint(FUSION_INSPECT_TOOLS);
|
|
320
|
-
return [
|
|
321
|
-
'--no-builtin-tools',
|
|
322
|
-
'--tools',
|
|
323
|
-
FUSION_INSPECT_TOOLS.join(','),
|
|
324
|
-
'--exclude-tools',
|
|
325
|
-
FUSION_FORBIDDEN_TOOLS.join(','),
|
|
326
|
-
];
|
|
327
|
-
}
|
|
328
|
-
if (capability === 'research') {
|
|
329
|
-
const allowlist = researchToolAllowlist();
|
|
330
|
-
assertFusionToolPolicyDisjoint(allowlist);
|
|
331
|
-
return [
|
|
332
|
-
'--no-builtin-tools',
|
|
333
|
-
'--tools',
|
|
334
|
-
allowlist.join(','),
|
|
335
|
-
'--exclude-tools',
|
|
336
|
-
FUSION_FORBIDDEN_TOOLS.join(','),
|
|
337
|
-
];
|
|
338
|
-
}
|
|
339
|
-
throw new FusionError(`fusion capability ${String(capability)} is not supported`, {
|
|
340
|
-
code: 'orchestration_failed',
|
|
341
|
-
childCreated: false,
|
|
342
|
-
});
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
/**
|
|
346
|
-
* Extensions explicitly loaded into a child, in deterministic order.
|
|
347
|
-
*
|
|
348
|
-
* `--no-extensions` disables discovery but still honours explicit `--extension`
|
|
349
|
-
* paths, so this list is the complete set a child receives. The metadata
|
|
350
|
-
* extension is always present. For Claude routes the sanitizer loads first so
|
|
351
|
-
* the package-owned attribution provider establishes the Claude Code OAuth
|
|
352
|
-
* request shape first, the sanitizer preserves that shape while removing only
|
|
353
|
-
* rejected prompt lines, and the private runtime governor observes the final
|
|
354
|
-
* payload. Non-Anthropic child argv remains unchanged.
|
|
355
|
-
*/
|
|
356
|
-
export function fusionChildExtensionPaths(
|
|
357
|
-
model: ResolvedFusionModel,
|
|
358
|
-
childExtensionPath: string,
|
|
359
|
-
resolveAttribution: () => string = resolveAnthropicAttributionExtensionPath,
|
|
360
|
-
): readonly string[] {
|
|
361
|
-
if (model.provider !== FUSION_SANITIZED_PROVIDER) return [childExtensionPath];
|
|
362
|
-
return [resolveAttribution(), childExtensionPath];
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
export function buildFusionPiChildArgv(
|
|
366
|
-
model: ResolvedFusionModel,
|
|
367
|
-
systemPrompt: string,
|
|
368
|
-
childExtensionPath = resolveFusionChildExtensionPath(),
|
|
369
|
-
capability: FusionCapability = FUSION_NO_TOOLS_CAPABILITY,
|
|
370
|
-
resolveAttribution: () => string = resolveAnthropicAttributionExtensionPath,
|
|
371
|
-
): string[] {
|
|
372
|
-
const extensionArgs = fusionChildExtensionPaths(
|
|
373
|
-
model,
|
|
374
|
-
childExtensionPath,
|
|
375
|
-
resolveAttribution,
|
|
376
|
-
).flatMap((path) => ['--extension', path]);
|
|
377
|
-
return [
|
|
378
|
-
'--mode',
|
|
379
|
-
'text',
|
|
380
|
-
'--no-session',
|
|
381
|
-
...fusionToolArgv(capability),
|
|
382
|
-
'--no-extensions',
|
|
383
|
-
'--no-skills',
|
|
384
|
-
'--no-prompt-templates',
|
|
385
|
-
'--no-themes',
|
|
386
|
-
'--no-context-files',
|
|
387
|
-
...extensionArgs,
|
|
388
|
-
'--provider',
|
|
389
|
-
model.provider,
|
|
390
|
-
'--model',
|
|
391
|
-
model.model,
|
|
392
|
-
'--thinking',
|
|
393
|
-
model.thinkingLevel,
|
|
394
|
-
'--system-prompt',
|
|
395
|
-
systemPrompt,
|
|
396
|
-
];
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
const SHA256_HEX_PATTERN = /^[0-9a-f]{64}$/;
|
|
400
|
-
const FUSION_CHILD_RESULT_PREFIX_BYTES = Buffer.from(FUSION_CHILD_RESULT_PREFIX, 'utf8');
|
|
401
|
-
const FUSION_CHILD_SETTLEMENT_PREFIX_BYTES = Buffer.from(FUSION_CHILD_SETTLEMENT_PREFIX, 'utf8');
|
|
402
|
-
const FUSION_RUNTIME_GUARD_PREFIX_BYTES = Buffer.from(FUSION_RUNTIME_GUARD_PREFIX, 'utf8');
|
|
403
|
-
const PI_EXTENSION_ERROR_PREFIX_BYTES = Buffer.from('Extension error (', 'utf8');
|
|
404
|
-
|
|
405
|
-
interface ParsedFusionChildStderr {
|
|
406
|
-
records: FusionChildResultMetadata[];
|
|
407
|
-
events: Buffer;
|
|
408
|
-
diagnostics: Buffer;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
function assertClosedRecord(
|
|
412
|
-
value: unknown,
|
|
413
|
-
keys: readonly string[],
|
|
414
|
-
label: string,
|
|
415
|
-
): Record<PropertyKey, unknown> {
|
|
416
|
-
if (!isJsonObject(value) || Array.isArray(value)) throw new Error(`${label} must be an object`);
|
|
417
|
-
const actual = Object.keys(value).sort();
|
|
418
|
-
const expected = [...keys].sort();
|
|
419
|
-
if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) {
|
|
420
|
-
throw new Error(`${label} keys mismatch: expected ${expected.join(', ')}`);
|
|
421
|
-
}
|
|
422
|
-
return value;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
function assertClosedRecordWithOptional(
|
|
426
|
-
value: unknown,
|
|
427
|
-
requiredKeys: readonly string[],
|
|
428
|
-
optionalKeys: readonly string[],
|
|
429
|
-
label: string,
|
|
430
|
-
): Record<PropertyKey, unknown> {
|
|
431
|
-
if (!isJsonObject(value) || Array.isArray(value)) throw new Error(`${label} must be an object`);
|
|
432
|
-
const allowed = new Set([...requiredKeys, ...optionalKeys]);
|
|
433
|
-
const actual = Object.keys(value).sort();
|
|
434
|
-
const missing = requiredKeys.filter((key) => !Object.hasOwn(value, key));
|
|
435
|
-
const unknownKeys = actual.filter((key) => !allowed.has(key));
|
|
436
|
-
if (missing.length > 0 || unknownKeys.length > 0) {
|
|
437
|
-
throw new Error(
|
|
438
|
-
`${label} keys mismatch: required ${[...requiredKeys].sort().join(', ')}; optional ${[
|
|
439
|
-
...optionalKeys,
|
|
440
|
-
]
|
|
441
|
-
.sort()
|
|
442
|
-
.join(', ')}`,
|
|
443
|
-
);
|
|
444
|
-
}
|
|
445
|
-
return value;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
function requireNonBlankString(
|
|
449
|
-
record: Record<PropertyKey, unknown>,
|
|
450
|
-
key: string,
|
|
451
|
-
label: string,
|
|
452
|
-
): string {
|
|
453
|
-
const value = record[key];
|
|
454
|
-
if (typeof value !== 'string' || value.trim().length === 0)
|
|
455
|
-
throw new Error(`${label}.${key} must be a non-blank string`);
|
|
456
|
-
return value;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
function requireSha256(record: Record<PropertyKey, unknown>, key: string, label: string): string {
|
|
460
|
-
const value = record[key];
|
|
461
|
-
if (typeof value !== 'string' || !SHA256_HEX_PATTERN.test(value))
|
|
462
|
-
throw new Error(`${label}.${key} must be a lowercase SHA-256 hex digest`);
|
|
463
|
-
return value;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
function requireUsageInteger(
|
|
467
|
-
record: Record<PropertyKey, unknown>,
|
|
468
|
-
key: string,
|
|
469
|
-
label: string,
|
|
470
|
-
): number {
|
|
471
|
-
const value = record[key];
|
|
472
|
-
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0)
|
|
473
|
-
throw new Error(`${label}.${key} must be a non-negative safe integer`);
|
|
474
|
-
return value;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
function requirePositiveSafeInteger(
|
|
478
|
-
record: Record<PropertyKey, unknown>,
|
|
479
|
-
key: string,
|
|
480
|
-
label: string,
|
|
481
|
-
): number {
|
|
482
|
-
const value = requireUsageInteger(record, key, label);
|
|
483
|
-
if (value === 0) throw new Error(`${label}.${key} must be a positive safe integer`);
|
|
484
|
-
return value;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
function requireCostNumber(
|
|
488
|
-
record: Record<PropertyKey, unknown>,
|
|
489
|
-
key: string,
|
|
490
|
-
label: string,
|
|
491
|
-
): number {
|
|
492
|
-
const value = record[key];
|
|
493
|
-
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0)
|
|
494
|
-
throw new Error(`${label}.${key} must be a non-negative finite number`);
|
|
495
|
-
return value;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
function parseCompactUsage(value: unknown): FusionUsage {
|
|
499
|
-
const record = assertClosedRecordWithOptional(
|
|
500
|
-
value,
|
|
501
|
-
['input', 'output', 'cacheRead', 'cacheWrite', 'totalTokens', 'cost'],
|
|
502
|
-
['cacheWrite1h', 'reasoning'],
|
|
503
|
-
'fusion child usage',
|
|
504
|
-
);
|
|
505
|
-
const cost = assertClosedRecord(
|
|
506
|
-
record['cost'],
|
|
507
|
-
['input', 'output', 'cacheRead', 'cacheWrite', 'total'],
|
|
508
|
-
'fusion child usage.cost',
|
|
509
|
-
);
|
|
510
|
-
const output = requireUsageInteger(record, 'output', 'fusion child usage');
|
|
511
|
-
const cacheWrite = requireUsageInteger(record, 'cacheWrite', 'fusion child usage');
|
|
512
|
-
const cacheWrite1h =
|
|
513
|
-
record['cacheWrite1h'] === undefined
|
|
514
|
-
? undefined
|
|
515
|
-
: requireUsageInteger(record, 'cacheWrite1h', 'fusion child usage');
|
|
516
|
-
const reasoning =
|
|
517
|
-
record['reasoning'] === undefined
|
|
518
|
-
? undefined
|
|
519
|
-
: requireUsageInteger(record, 'reasoning', 'fusion child usage');
|
|
520
|
-
if (cacheWrite1h !== undefined && cacheWrite1h > cacheWrite) {
|
|
521
|
-
throw new Error('fusion child usage.cacheWrite1h must not exceed cacheWrite');
|
|
522
|
-
}
|
|
523
|
-
if (reasoning !== undefined && reasoning > output) {
|
|
524
|
-
throw new Error('fusion child usage.reasoning must not exceed output');
|
|
525
|
-
}
|
|
526
|
-
return {
|
|
527
|
-
input: requireUsageInteger(record, 'input', 'fusion child usage'),
|
|
528
|
-
output,
|
|
529
|
-
cacheRead: requireUsageInteger(record, 'cacheRead', 'fusion child usage'),
|
|
530
|
-
cacheWrite,
|
|
531
|
-
...(cacheWrite1h === undefined ? {} : { cacheWrite1h }),
|
|
532
|
-
...(reasoning === undefined ? {} : { reasoning }),
|
|
533
|
-
totalTokens: requireUsageInteger(record, 'totalTokens', 'fusion child usage'),
|
|
534
|
-
cost: {
|
|
535
|
-
input: requireCostNumber(cost, 'input', 'fusion child usage.cost'),
|
|
536
|
-
output: requireCostNumber(cost, 'output', 'fusion child usage.cost'),
|
|
537
|
-
cacheRead: requireCostNumber(cost, 'cacheRead', 'fusion child usage.cost'),
|
|
538
|
-
cacheWrite: requireCostNumber(cost, 'cacheWrite', 'fusion child usage.cost'),
|
|
539
|
-
total: requireCostNumber(cost, 'total', 'fusion child usage.cost'),
|
|
540
|
-
},
|
|
541
|
-
};
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
const FUSION_CLAUDE_CACHE_RETENTIONS = new Set<FusionClaudeCacheRetention>([
|
|
545
|
-
'none',
|
|
546
|
-
'short',
|
|
547
|
-
'long',
|
|
548
|
-
]);
|
|
549
|
-
|
|
550
|
-
function parseFusionClaudeCacheObservation(
|
|
551
|
-
value: unknown,
|
|
552
|
-
provider: string,
|
|
553
|
-
): FusionClaudeCacheObservation {
|
|
554
|
-
const record = assertClosedRecord(
|
|
555
|
-
value,
|
|
556
|
-
[
|
|
557
|
-
'schema_version',
|
|
558
|
-
'applicability',
|
|
559
|
-
'source',
|
|
560
|
-
'requested_retention',
|
|
561
|
-
'effective_retention',
|
|
562
|
-
'breakpoint_count',
|
|
563
|
-
'request_ordinal',
|
|
564
|
-
],
|
|
565
|
-
'fusion child cache observation',
|
|
566
|
-
);
|
|
567
|
-
if (record['schema_version'] !== FUSION_CLAUDE_CACHE_OBSERVATION_SCHEMA_VERSION) {
|
|
568
|
-
throw new Error('fusion child cache observation schema_version mismatch');
|
|
569
|
-
}
|
|
570
|
-
const applicability = record['applicability'];
|
|
571
|
-
const source = record['source'];
|
|
572
|
-
const requested = record['requested_retention'];
|
|
573
|
-
const effective = record['effective_retention'];
|
|
574
|
-
const breakpointCount = requireUsageInteger(
|
|
575
|
-
record,
|
|
576
|
-
'breakpoint_count',
|
|
577
|
-
'fusion child cache observation',
|
|
578
|
-
);
|
|
579
|
-
const requestOrdinal = requirePositiveSafeInteger(
|
|
580
|
-
record,
|
|
581
|
-
'request_ordinal',
|
|
582
|
-
'fusion child cache observation',
|
|
583
|
-
);
|
|
584
|
-
if (requestOrdinal > FUSION_CHILD_MAX_PROVIDER_REQUESTS) {
|
|
585
|
-
throw new Error('fusion child cache observation exceeds the provider request limit');
|
|
586
|
-
}
|
|
587
|
-
if (breakpointCount > FUSION_CLAUDE_CACHE_BREAKPOINT_LIMIT) {
|
|
588
|
-
throw new Error('fusion child cache observation exceeds the Anthropic breakpoint limit');
|
|
589
|
-
}
|
|
590
|
-
if (provider !== 'anthropic') {
|
|
591
|
-
if (
|
|
592
|
-
applicability !== 'not_applicable' ||
|
|
593
|
-
source !== 'not_applicable' ||
|
|
594
|
-
requested !== null ||
|
|
595
|
-
effective !== null ||
|
|
596
|
-
breakpointCount !== 0
|
|
597
|
-
) {
|
|
598
|
-
throw new Error('non-Anthropic fusion child has contradictory cache observation');
|
|
599
|
-
}
|
|
600
|
-
return {
|
|
601
|
-
schema_version: FUSION_CLAUDE_CACHE_OBSERVATION_SCHEMA_VERSION,
|
|
602
|
-
applicability: 'not_applicable',
|
|
603
|
-
source: 'not_applicable',
|
|
604
|
-
requested_retention: null,
|
|
605
|
-
effective_retention: null,
|
|
606
|
-
breakpoint_count: 0,
|
|
607
|
-
request_ordinal: requestOrdinal,
|
|
608
|
-
};
|
|
609
|
-
}
|
|
610
|
-
if (applicability !== 'anthropic') {
|
|
611
|
-
throw new Error('Anthropic fusion child cache observation is not applicable');
|
|
612
|
-
}
|
|
613
|
-
if (source !== 'default' && source !== FUSION_CLAUDE_CACHE_RETENTION_ENV) {
|
|
614
|
-
throw new Error('Anthropic fusion child cache observation source is invalid');
|
|
615
|
-
}
|
|
616
|
-
if (
|
|
617
|
-
typeof requested !== 'string' ||
|
|
618
|
-
!FUSION_CLAUDE_CACHE_RETENTIONS.has(requested as FusionClaudeCacheRetention) ||
|
|
619
|
-
typeof effective !== 'string' ||
|
|
620
|
-
!FUSION_CLAUDE_CACHE_RETENTIONS.has(effective as FusionClaudeCacheRetention)
|
|
621
|
-
) {
|
|
622
|
-
throw new Error('Anthropic fusion child cache retention is invalid');
|
|
623
|
-
}
|
|
624
|
-
const requestedRetention = requested as FusionClaudeCacheRetention;
|
|
625
|
-
const effectiveRetention = effective as FusionClaudeCacheRetention;
|
|
626
|
-
if (source === 'default' && requestedRetention !== 'long') {
|
|
627
|
-
throw new Error('default Anthropic cache policy did not request long retention');
|
|
628
|
-
}
|
|
629
|
-
if ((effectiveRetention === 'none') !== (breakpointCount === 0)) {
|
|
630
|
-
throw new Error('Anthropic fusion child cache retention/breakpoint evidence mismatch');
|
|
631
|
-
}
|
|
632
|
-
if (requestedRetention === 'none' && effectiveRetention !== 'none') {
|
|
633
|
-
throw new Error('disabled Anthropic cache policy reported active breakpoints');
|
|
634
|
-
}
|
|
635
|
-
if (effectiveRetention === 'long' && requestedRetention !== 'long') {
|
|
636
|
-
throw new Error('Anthropic long cache retention was not requested');
|
|
637
|
-
}
|
|
638
|
-
return {
|
|
639
|
-
schema_version: FUSION_CLAUDE_CACHE_OBSERVATION_SCHEMA_VERSION,
|
|
640
|
-
applicability: 'anthropic',
|
|
641
|
-
source,
|
|
642
|
-
requested_retention: requestedRetention,
|
|
643
|
-
effective_retention: effectiveRetention,
|
|
644
|
-
breakpoint_count: breakpointCount,
|
|
645
|
-
request_ordinal: requestOrdinal,
|
|
646
|
-
};
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
const FUSION_OUTPUT_RECOVERY_ROLES = new Set<FusionChildOutputRecoveryRole>([
|
|
650
|
-
'none',
|
|
651
|
-
'oversized_original',
|
|
652
|
-
'replacement',
|
|
653
|
-
]);
|
|
654
|
-
|
|
655
|
-
function parseChildOutputContract(value: unknown): FusionChildOutputContractMetadata {
|
|
656
|
-
const record = assertClosedRecord(
|
|
657
|
-
value,
|
|
658
|
-
['json_rendered_bytes', 'candidate_limit_bytes', 'recovery_role'],
|
|
659
|
-
'fusion child result.output_contract',
|
|
660
|
-
);
|
|
661
|
-
const candidateLimitValue = record['candidate_limit_bytes'];
|
|
662
|
-
const candidateLimit =
|
|
663
|
-
candidateLimitValue === null
|
|
664
|
-
? null
|
|
665
|
-
: requirePositiveSafeInteger(
|
|
666
|
-
record,
|
|
667
|
-
'candidate_limit_bytes',
|
|
668
|
-
'fusion child result.output_contract',
|
|
669
|
-
);
|
|
670
|
-
if (candidateLimit !== null && candidateLimit !== FUSION_CANDIDATE_MAX_OUTPUT_BYTES) {
|
|
671
|
-
throw new Error('fusion child result candidate output limit mismatches the shared contract');
|
|
672
|
-
}
|
|
673
|
-
const recoveryRole = record['recovery_role'];
|
|
674
|
-
if (
|
|
675
|
-
typeof recoveryRole !== 'string' ||
|
|
676
|
-
!FUSION_OUTPUT_RECOVERY_ROLES.has(recoveryRole as FusionChildOutputRecoveryRole)
|
|
677
|
-
) {
|
|
678
|
-
throw new Error('fusion child result.output_contract.recovery_role is invalid');
|
|
679
|
-
}
|
|
680
|
-
if (recoveryRole !== 'none' && candidateLimit === null) {
|
|
681
|
-
throw new Error('fusion child result output recovery has no candidate contract');
|
|
682
|
-
}
|
|
683
|
-
return {
|
|
684
|
-
json_rendered_bytes: requireUsageInteger(
|
|
685
|
-
record,
|
|
686
|
-
'json_rendered_bytes',
|
|
687
|
-
'fusion child result.output_contract',
|
|
688
|
-
),
|
|
689
|
-
candidate_limit_bytes: candidateLimit,
|
|
690
|
-
recovery_role: recoveryRole as FusionChildOutputRecoveryRole,
|
|
691
|
-
};
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
function parseChildResultMetadata(value: unknown): FusionChildResultMetadata {
|
|
695
|
-
const record = assertClosedRecord(
|
|
696
|
-
value,
|
|
697
|
-
[
|
|
698
|
-
'schema_version',
|
|
699
|
-
'provider',
|
|
700
|
-
'model',
|
|
701
|
-
'stop_reason',
|
|
702
|
-
'text_blocks',
|
|
703
|
-
'text_sha256',
|
|
704
|
-
'usage',
|
|
705
|
-
'cache_observation',
|
|
706
|
-
'output_contract',
|
|
707
|
-
],
|
|
708
|
-
'fusion child result',
|
|
709
|
-
);
|
|
710
|
-
if (record['schema_version'] !== FUSION_CHILD_RESULT_SCHEMA_VERSION)
|
|
711
|
-
throw new Error('fusion child result schema_version mismatch');
|
|
712
|
-
const textBlocksValue = record['text_blocks'];
|
|
713
|
-
if (!Array.isArray(textBlocksValue))
|
|
714
|
-
throw new Error('fusion child result.text_blocks must be an array');
|
|
715
|
-
const textBlocks = textBlocksValue.map((value, index) => {
|
|
716
|
-
const label = `fusion child result.text_blocks[${String(index)}]`;
|
|
717
|
-
const block = assertClosedRecord(value, ['utf8_bytes', 'sha256'], label);
|
|
718
|
-
return {
|
|
719
|
-
utf8_bytes: requireUsageInteger(block, 'utf8_bytes', label),
|
|
720
|
-
sha256: requireSha256(block, 'sha256', label),
|
|
721
|
-
};
|
|
722
|
-
});
|
|
723
|
-
const usage = parseCompactUsage(record['usage']);
|
|
724
|
-
const provider = requireNonBlankString(record, 'provider', 'fusion child result');
|
|
725
|
-
return {
|
|
726
|
-
schema_version: FUSION_CHILD_RESULT_SCHEMA_VERSION,
|
|
727
|
-
provider,
|
|
728
|
-
model: requireNonBlankString(record, 'model', 'fusion child result'),
|
|
729
|
-
stop_reason: requireNonBlankString(record, 'stop_reason', 'fusion child result'),
|
|
730
|
-
text_blocks: textBlocks,
|
|
731
|
-
text_sha256: requireSha256(record, 'text_sha256', 'fusion child result'),
|
|
732
|
-
usage,
|
|
733
|
-
cache_observation: parseFusionClaudeCacheObservation(record['cache_observation'], provider),
|
|
734
|
-
output_contract: parseChildOutputContract(record['output_contract']),
|
|
735
|
-
};
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
const FUSION_RUNTIME_GUARD_CODES = new Set<FusionRuntimeGuardCode>([
|
|
739
|
-
'provider_request_limit',
|
|
740
|
-
'provider_payload_invalid',
|
|
741
|
-
'claude_cache_policy',
|
|
742
|
-
'tool_call_limit',
|
|
743
|
-
]);
|
|
744
|
-
|
|
745
|
-
export function parseFusionRuntimeGuard(stderr: Buffer): FusionRuntimeGuardRecord | undefined {
|
|
746
|
-
const frames: FusionRuntimeGuardRecord[] = [];
|
|
747
|
-
let cursor = 0;
|
|
748
|
-
for (;;) {
|
|
749
|
-
const frameStart = stderr.indexOf(FUSION_RUNTIME_GUARD_PREFIX_BYTES, cursor);
|
|
750
|
-
if (frameStart < 0) break;
|
|
751
|
-
const payloadStart = frameStart + FUSION_RUNTIME_GUARD_PREFIX_BYTES.length;
|
|
752
|
-
const newline = stderr.indexOf(10, payloadStart);
|
|
753
|
-
if (newline < 0) throw new Error('fusion runtime guard frame is not newline-terminated');
|
|
754
|
-
const bytes = stderr.subarray(payloadStart, newline);
|
|
755
|
-
const text = bytes.toString('utf8');
|
|
756
|
-
if (!Buffer.from(text, 'utf8').equals(bytes)) {
|
|
757
|
-
throw new Error('fusion runtime guard frame is not valid UTF-8');
|
|
758
|
-
}
|
|
759
|
-
const record = assertClosedRecord(
|
|
760
|
-
parseJsonText(text),
|
|
761
|
-
[
|
|
762
|
-
'schema_version',
|
|
763
|
-
'code',
|
|
764
|
-
'provider',
|
|
765
|
-
'model',
|
|
766
|
-
'request_ordinal',
|
|
767
|
-
'tool_call_count',
|
|
768
|
-
'payload_bytes',
|
|
769
|
-
'payload_sha256',
|
|
770
|
-
'message',
|
|
771
|
-
],
|
|
772
|
-
'fusion runtime guard',
|
|
773
|
-
);
|
|
774
|
-
if (record['schema_version'] !== FUSION_RUNTIME_GUARD_SCHEMA_VERSION) {
|
|
775
|
-
throw new Error('fusion runtime guard schema_version mismatch');
|
|
776
|
-
}
|
|
777
|
-
const code = requireNonBlankString(record, 'code', 'fusion runtime guard');
|
|
778
|
-
if (!FUSION_RUNTIME_GUARD_CODES.has(code as FusionRuntimeGuardCode)) {
|
|
779
|
-
throw new Error(`fusion runtime guard code is unsupported: ${code}`);
|
|
780
|
-
}
|
|
781
|
-
const frame: FusionRuntimeGuardRecord = {
|
|
782
|
-
schema_version: FUSION_RUNTIME_GUARD_SCHEMA_VERSION,
|
|
783
|
-
code: code as FusionRuntimeGuardCode,
|
|
784
|
-
provider: requireNonBlankString(record, 'provider', 'fusion runtime guard'),
|
|
785
|
-
model: requireNonBlankString(record, 'model', 'fusion runtime guard'),
|
|
786
|
-
request_ordinal: requirePositiveSafeInteger(
|
|
787
|
-
record,
|
|
788
|
-
'request_ordinal',
|
|
789
|
-
'fusion runtime guard',
|
|
790
|
-
),
|
|
791
|
-
tool_call_count: requireUsageInteger(record, 'tool_call_count', 'fusion runtime guard'),
|
|
792
|
-
payload_bytes: requireUsageInteger(record, 'payload_bytes', 'fusion runtime guard'),
|
|
793
|
-
payload_sha256: requireSha256(record, 'payload_sha256', 'fusion runtime guard'),
|
|
794
|
-
message: requireNonBlankString(record, 'message', 'fusion runtime guard'),
|
|
795
|
-
};
|
|
796
|
-
const emptyPayloadHash = createHash('sha256').update(Buffer.alloc(0)).digest('hex');
|
|
797
|
-
if (frame.code === 'claude_cache_policy' || frame.code === 'provider_payload_invalid') {
|
|
798
|
-
if (frame.payload_bytes !== 0 || frame.payload_sha256 !== emptyPayloadHash) {
|
|
799
|
-
throw new Error('fusion runtime guard invalid-payload evidence mismatch');
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
if (
|
|
803
|
-
frame.code === 'provider_request_limit' &&
|
|
804
|
-
frame.request_ordinal <= FUSION_CHILD_MAX_PROVIDER_REQUESTS
|
|
805
|
-
) {
|
|
806
|
-
throw new Error('fusion runtime guard provider request limit was not exceeded');
|
|
807
|
-
}
|
|
808
|
-
if (frame.code === 'tool_call_limit') {
|
|
809
|
-
if (frame.tool_call_count <= FUSION_CHILD_MAX_TOOL_CALLS) {
|
|
810
|
-
throw new Error('fusion runtime guard tool call limit was not exceeded');
|
|
811
|
-
}
|
|
812
|
-
if (frame.payload_bytes !== 0 || frame.payload_sha256 !== emptyPayloadHash) {
|
|
813
|
-
throw new Error('fusion runtime guard tool call limit payload evidence mismatch');
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
frames.push(frame);
|
|
817
|
-
cursor = newline + 1;
|
|
818
|
-
}
|
|
819
|
-
if (frames.length > 1) throw new Error('fusion child emitted multiple runtime guard frames');
|
|
820
|
-
return frames[0];
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
const FUSION_CHILD_SETTLEMENT_FAILURE_REASONS = new Set<FusionChildSettlementFailureReason>([
|
|
824
|
-
'no_records',
|
|
825
|
-
'final_not_stop',
|
|
826
|
-
'invalid_non_final',
|
|
827
|
-
'runtime_guard',
|
|
828
|
-
'cache_observation',
|
|
829
|
-
'output_recovery',
|
|
830
|
-
]);
|
|
831
|
-
|
|
832
|
-
export function parseFusionChildSettlement(
|
|
833
|
-
stderr: Buffer,
|
|
834
|
-
): FusionChildSettlementRecord | undefined {
|
|
835
|
-
const frames: FusionChildSettlementRecord[] = [];
|
|
836
|
-
let cursor = 0;
|
|
837
|
-
for (;;) {
|
|
838
|
-
const frameStart = stderr.indexOf(FUSION_CHILD_SETTLEMENT_PREFIX_BYTES, cursor);
|
|
839
|
-
if (frameStart < 0) break;
|
|
840
|
-
const payloadStart = frameStart + FUSION_CHILD_SETTLEMENT_PREFIX_BYTES.length;
|
|
841
|
-
const newline = stderr.indexOf(10, payloadStart);
|
|
842
|
-
if (newline < 0) throw new Error('fusion child settlement frame is not newline-terminated');
|
|
843
|
-
const bytes = stderr.subarray(payloadStart, newline);
|
|
844
|
-
const text = bytes.toString('utf8');
|
|
845
|
-
if (!Buffer.from(text, 'utf8').equals(bytes)) {
|
|
846
|
-
throw new Error('fusion child settlement frame is not valid UTF-8');
|
|
847
|
-
}
|
|
848
|
-
const record = assertClosedRecord(
|
|
849
|
-
parseJsonText(text),
|
|
850
|
-
[
|
|
851
|
-
'schema_version',
|
|
852
|
-
'status',
|
|
853
|
-
'record_count',
|
|
854
|
-
'records_sha256',
|
|
855
|
-
'final_record_index',
|
|
856
|
-
'final_text_sha256',
|
|
857
|
-
'recovered_error_ordinals',
|
|
858
|
-
'recovered_output_cap_ordinals',
|
|
859
|
-
'failure_reason',
|
|
860
|
-
],
|
|
861
|
-
'fusion child settlement',
|
|
862
|
-
);
|
|
863
|
-
if (record['schema_version'] !== FUSION_CHILD_SETTLEMENT_SCHEMA_VERSION) {
|
|
864
|
-
throw new Error('fusion child settlement schema_version mismatch');
|
|
865
|
-
}
|
|
866
|
-
const status = record['status'];
|
|
867
|
-
if (status !== 'complete' && status !== 'failed') {
|
|
868
|
-
throw new Error('fusion child settlement.status is invalid');
|
|
869
|
-
}
|
|
870
|
-
const recordCount = requireUsageInteger(record, 'record_count', 'fusion child settlement');
|
|
871
|
-
const finalIndexValue = record['final_record_index'];
|
|
872
|
-
const finalRecordIndex =
|
|
873
|
-
finalIndexValue === null
|
|
874
|
-
? null
|
|
875
|
-
: requireUsageInteger(record, 'final_record_index', 'fusion child settlement');
|
|
876
|
-
const finalHashValue = record['final_text_sha256'];
|
|
877
|
-
const finalTextSha256 =
|
|
878
|
-
finalHashValue === null
|
|
879
|
-
? null
|
|
880
|
-
: requireSha256(record, 'final_text_sha256', 'fusion child settlement');
|
|
881
|
-
const recoveredValue = record['recovered_error_ordinals'];
|
|
882
|
-
if (!Array.isArray(recoveredValue)) {
|
|
883
|
-
throw new Error('fusion child settlement.recovered_error_ordinals must be an array');
|
|
884
|
-
}
|
|
885
|
-
const recoveredErrorOrdinals = recoveredValue.map((value, index) => {
|
|
886
|
-
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {
|
|
887
|
-
throw new Error(
|
|
888
|
-
`fusion child settlement.recovered_error_ordinals[${String(index)}] must be a non-negative safe integer`,
|
|
889
|
-
);
|
|
890
|
-
}
|
|
891
|
-
return value;
|
|
892
|
-
});
|
|
893
|
-
for (let index = 0; index < recoveredErrorOrdinals.length; index += 1) {
|
|
894
|
-
const ordinal = recoveredErrorOrdinals[index];
|
|
895
|
-
if (
|
|
896
|
-
ordinal === undefined ||
|
|
897
|
-
ordinal >= recordCount - 1 ||
|
|
898
|
-
(index > 0 && ordinal <= (recoveredErrorOrdinals[index - 1] ?? -1))
|
|
899
|
-
) {
|
|
900
|
-
throw new Error('fusion child settlement recovered-error ordinals are not canonical');
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
const recoveredOutputValue = record['recovered_output_cap_ordinals'];
|
|
904
|
-
if (!Array.isArray(recoveredOutputValue)) {
|
|
905
|
-
throw new Error('fusion child settlement.recovered_output_cap_ordinals must be an array');
|
|
906
|
-
}
|
|
907
|
-
const recoveredOutputCapOrdinals = recoveredOutputValue.map((value, index) => {
|
|
908
|
-
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {
|
|
909
|
-
throw new Error(
|
|
910
|
-
`fusion child settlement.recovered_output_cap_ordinals[${String(index)}] must be a non-negative safe integer`,
|
|
911
|
-
);
|
|
912
|
-
}
|
|
913
|
-
return value;
|
|
914
|
-
});
|
|
915
|
-
for (let index = 0; index < recoveredOutputCapOrdinals.length; index += 1) {
|
|
916
|
-
const ordinal = recoveredOutputCapOrdinals[index];
|
|
917
|
-
if (
|
|
918
|
-
ordinal === undefined ||
|
|
919
|
-
ordinal >= recordCount - 1 ||
|
|
920
|
-
(index > 0 && ordinal <= (recoveredOutputCapOrdinals[index - 1] ?? -1))
|
|
921
|
-
) {
|
|
922
|
-
throw new Error('fusion child settlement recovered-output ordinals are not canonical');
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
const failureValue = record['failure_reason'];
|
|
926
|
-
let failureReason: FusionChildSettlementFailureReason | null;
|
|
927
|
-
if (failureValue === null) failureReason = null;
|
|
928
|
-
else if (
|
|
929
|
-
typeof failureValue === 'string' &&
|
|
930
|
-
FUSION_CHILD_SETTLEMENT_FAILURE_REASONS.has(
|
|
931
|
-
failureValue as FusionChildSettlementFailureReason,
|
|
932
|
-
)
|
|
933
|
-
) {
|
|
934
|
-
failureReason = failureValue as FusionChildSettlementFailureReason;
|
|
935
|
-
} else {
|
|
936
|
-
throw new Error('fusion child settlement.failure_reason is invalid');
|
|
937
|
-
}
|
|
938
|
-
if ((status === 'complete') !== (failureReason === null)) {
|
|
939
|
-
throw new Error('fusion child settlement status/failure_reason mismatch');
|
|
940
|
-
}
|
|
941
|
-
if (recordCount === 0) {
|
|
942
|
-
if (finalRecordIndex !== null || finalTextSha256 !== null) {
|
|
943
|
-
throw new Error('fusion child settlement empty stream has final-record evidence');
|
|
944
|
-
}
|
|
945
|
-
} else if (finalRecordIndex !== recordCount - 1 || finalTextSha256 === null) {
|
|
946
|
-
throw new Error('fusion child settlement final-record evidence mismatch');
|
|
947
|
-
}
|
|
948
|
-
frames.push({
|
|
949
|
-
schema_version: FUSION_CHILD_SETTLEMENT_SCHEMA_VERSION,
|
|
950
|
-
status,
|
|
951
|
-
record_count: recordCount,
|
|
952
|
-
records_sha256: requireSha256(record, 'records_sha256', 'fusion child settlement'),
|
|
953
|
-
final_record_index: finalRecordIndex,
|
|
954
|
-
final_text_sha256: finalTextSha256,
|
|
955
|
-
recovered_error_ordinals: recoveredErrorOrdinals,
|
|
956
|
-
recovered_output_cap_ordinals: recoveredOutputCapOrdinals,
|
|
957
|
-
failure_reason: failureReason,
|
|
958
|
-
});
|
|
959
|
-
cursor = newline + 1;
|
|
960
|
-
}
|
|
961
|
-
if (frames.length > 1) throw new Error('fusion child emitted multiple settlement frames');
|
|
962
|
-
return frames[0];
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
function assertFusionChildSettlementOrdering(stderr: Buffer): void {
|
|
966
|
-
const settlementStart = stderr.indexOf(FUSION_CHILD_SETTLEMENT_PREFIX_BYTES);
|
|
967
|
-
if (settlementStart < 0) return;
|
|
968
|
-
if (stderr.indexOf(FUSION_CHILD_RESULT_PREFIX_BYTES, settlementStart) >= 0) {
|
|
969
|
-
throw new Error('fusion child emitted result metadata after terminal settlement');
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
function stripChildControlFrames(stderr: Buffer): Buffer {
|
|
974
|
-
const prefixes = [FUSION_CHILD_SETTLEMENT_PREFIX_BYTES, FUSION_RUNTIME_GUARD_PREFIX_BYTES];
|
|
975
|
-
const diagnostics: Buffer[] = [];
|
|
976
|
-
let cursor = 0;
|
|
977
|
-
for (;;) {
|
|
978
|
-
let next = -1;
|
|
979
|
-
let prefix: Buffer | undefined;
|
|
980
|
-
for (const candidate of prefixes) {
|
|
981
|
-
const found = stderr.indexOf(candidate, cursor);
|
|
982
|
-
if (found >= 0 && (next < 0 || found < next)) {
|
|
983
|
-
next = found;
|
|
984
|
-
prefix = candidate;
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
if (next < 0 || prefix === undefined) {
|
|
988
|
-
if (cursor < stderr.length) diagnostics.push(stderr.subarray(cursor));
|
|
989
|
-
break;
|
|
990
|
-
}
|
|
991
|
-
if (next > cursor) diagnostics.push(stderr.subarray(cursor, next));
|
|
992
|
-
const newline = stderr.indexOf(10, next + prefix.length);
|
|
993
|
-
if (newline < 0) throw new Error('fusion child control frame is not newline-terminated');
|
|
994
|
-
cursor = newline + 1;
|
|
995
|
-
}
|
|
996
|
-
return Buffer.concat(diagnostics);
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
export function assertFusionRuntimeGuardMatchesModel(
|
|
1000
|
-
guard: FusionRuntimeGuardRecord,
|
|
1001
|
-
model: ResolvedFusionModel,
|
|
1002
|
-
): void {
|
|
1003
|
-
const routeUnknown = guard.provider === 'unknown' && guard.model === 'unknown';
|
|
1004
|
-
if (!routeUnknown && (guard.provider !== model.provider || guard.model !== model.model)) {
|
|
1005
|
-
throw new Error(
|
|
1006
|
-
`fusion runtime guard route mismatch: expected ${model.qualifiedId}, observed ${guard.provider}/${guard.model}`,
|
|
1007
|
-
);
|
|
1008
|
-
}
|
|
1009
|
-
if (routeUnknown && guard.code !== 'provider_payload_invalid') {
|
|
1010
|
-
throw new Error('fusion runtime guard omitted the route for a route-bound refusal');
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
export function parseFusionChildStderr(stderr: Buffer): ParsedFusionChildStderr {
|
|
1015
|
-
// Validate every package-owned side frame even though result metadata is parsed
|
|
1016
|
-
// independently below. Malformed refusal/settlement evidence must never degrade
|
|
1017
|
-
// into opaque diagnostics.
|
|
1018
|
-
parseFusionRuntimeGuard(stderr);
|
|
1019
|
-
const settlement = parseFusionChildSettlement(stderr);
|
|
1020
|
-
assertFusionChildSettlementOrdering(stderr);
|
|
1021
|
-
const records: FusionChildResultMetadata[] = [];
|
|
1022
|
-
const diagnostics: Buffer[] = [];
|
|
1023
|
-
let cursor = 0;
|
|
1024
|
-
for (;;) {
|
|
1025
|
-
const frameStart = stderr.indexOf(FUSION_CHILD_RESULT_PREFIX_BYTES, cursor);
|
|
1026
|
-
if (frameStart < 0) {
|
|
1027
|
-
if (cursor < stderr.length) diagnostics.push(stderr.subarray(cursor));
|
|
1028
|
-
break;
|
|
1029
|
-
}
|
|
1030
|
-
if (frameStart > cursor) diagnostics.push(stderr.subarray(cursor, frameStart));
|
|
1031
|
-
const payloadStart = frameStart + FUSION_CHILD_RESULT_PREFIX_BYTES.length;
|
|
1032
|
-
const newline = stderr.indexOf(10, payloadStart);
|
|
1033
|
-
if (newline < 0) throw new Error('fusion child metadata frame is not newline-terminated');
|
|
1034
|
-
const payloadBytes = stderr.subarray(payloadStart, newline);
|
|
1035
|
-
const payloadText = payloadBytes.toString('utf8');
|
|
1036
|
-
if (!Buffer.from(payloadText, 'utf8').equals(payloadBytes))
|
|
1037
|
-
throw new Error('fusion child metadata frame is not valid UTF-8');
|
|
1038
|
-
let parsed: unknown;
|
|
1039
|
-
try {
|
|
1040
|
-
parsed = parseJsonText(payloadText);
|
|
1041
|
-
} catch (error) {
|
|
1042
|
-
throw new Error(
|
|
1043
|
-
`fusion child metadata frame is invalid JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
1044
|
-
);
|
|
1045
|
-
}
|
|
1046
|
-
records.push(parseChildResultMetadata(parsed));
|
|
1047
|
-
cursor = newline + 1;
|
|
1048
|
-
}
|
|
1049
|
-
const resultEvents = serializeFusionChildResultRecords(records);
|
|
1050
|
-
const events =
|
|
1051
|
-
settlement === undefined
|
|
1052
|
-
? resultEvents
|
|
1053
|
-
: Buffer.concat([resultEvents, Buffer.from(`${JSON.stringify(settlement)}\n`, 'utf8')]);
|
|
1054
|
-
return {
|
|
1055
|
-
records,
|
|
1056
|
-
events,
|
|
1057
|
-
diagnostics: stripChildControlFrames(Buffer.concat(diagnostics)),
|
|
1058
|
-
};
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
function parseToolCallLogRecord(value: unknown, label: string): FusionToolCallLogRecord {
|
|
1062
|
-
const record = assertClosedRecordWithOptional(
|
|
1063
|
-
value,
|
|
1064
|
-
[
|
|
1065
|
-
'schema_version',
|
|
1066
|
-
'ordinal',
|
|
1067
|
-
'tool_name',
|
|
1068
|
-
'arguments_sha256',
|
|
1069
|
-
'arguments_bytes',
|
|
1070
|
-
'result_bytes',
|
|
1071
|
-
'result_sha256',
|
|
1072
|
-
'status',
|
|
1073
|
-
'duration_ms',
|
|
1074
|
-
],
|
|
1075
|
-
['url', 'rejected_url_sha256', 'final_url', 'http_status', 'response_bytes', 'content_sha256'],
|
|
1076
|
-
label,
|
|
1077
|
-
);
|
|
1078
|
-
if (record['schema_version'] !== FUSION_TOOL_CALL_LOG_SCHEMA_VERSION) {
|
|
1079
|
-
throw new Error(`${label}.schema_version mismatch`);
|
|
1080
|
-
}
|
|
1081
|
-
const status = record['status'];
|
|
1082
|
-
if (status !== 'ok' && status !== 'error') throw new Error(`${label}.status is invalid`);
|
|
1083
|
-
const parsedRecord: FusionToolCallLogRecord = {
|
|
1084
|
-
schema_version: FUSION_TOOL_CALL_LOG_SCHEMA_VERSION,
|
|
1085
|
-
ordinal: requireUsageInteger(record, 'ordinal', label),
|
|
1086
|
-
tool_name: requireNonBlankString(record, 'tool_name', label),
|
|
1087
|
-
arguments_sha256: requireSha256(record, 'arguments_sha256', label),
|
|
1088
|
-
arguments_bytes: requireUsageInteger(record, 'arguments_bytes', label),
|
|
1089
|
-
result_bytes: requireUsageInteger(record, 'result_bytes', label),
|
|
1090
|
-
result_sha256: requireSha256(record, 'result_sha256', label),
|
|
1091
|
-
status,
|
|
1092
|
-
duration_ms: requireUsageInteger(record, 'duration_ms', label),
|
|
1093
|
-
};
|
|
1094
|
-
if (record['url'] !== undefined) parsedRecord.url = requireNonBlankString(record, 'url', label);
|
|
1095
|
-
if (record['rejected_url_sha256'] !== undefined)
|
|
1096
|
-
parsedRecord.rejected_url_sha256 = requireSha256(record, 'rejected_url_sha256', label);
|
|
1097
|
-
if (record['final_url'] !== undefined)
|
|
1098
|
-
parsedRecord.final_url = requireNonBlankString(record, 'final_url', label);
|
|
1099
|
-
if (record['http_status'] !== undefined)
|
|
1100
|
-
parsedRecord.http_status = requireUsageInteger(record, 'http_status', label);
|
|
1101
|
-
if (record['response_bytes'] !== undefined)
|
|
1102
|
-
parsedRecord.response_bytes = requireUsageInteger(record, 'response_bytes', label);
|
|
1103
|
-
if (record['content_sha256'] !== undefined)
|
|
1104
|
-
parsedRecord.content_sha256 = requireSha256(record, 'content_sha256', label);
|
|
1105
|
-
return parsedRecord;
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
export function parseFusionToolCallLog(bytes: Buffer): FusionToolCallTrace {
|
|
1109
|
-
if (bytes.length === 0) {
|
|
1110
|
-
return {
|
|
1111
|
-
bytes,
|
|
1112
|
-
records: [],
|
|
1113
|
-
summary: { count: 0, total_result_bytes: 0, trace_complete: true },
|
|
1114
|
-
};
|
|
1115
|
-
}
|
|
1116
|
-
if (bytes.at(-1) !== 10) {
|
|
1117
|
-
throw new Error('fusion tool-call log has trailing partial line');
|
|
1118
|
-
}
|
|
1119
|
-
const text = bytes.toString('utf8');
|
|
1120
|
-
if (!Buffer.from(text, 'utf8').equals(bytes)) {
|
|
1121
|
-
throw new Error('fusion tool-call log is not valid UTF-8');
|
|
1122
|
-
}
|
|
1123
|
-
const lines = text.split('\n');
|
|
1124
|
-
lines.pop();
|
|
1125
|
-
const records = lines.map((line, index) => {
|
|
1126
|
-
let parsed: unknown;
|
|
1127
|
-
try {
|
|
1128
|
-
parsed = parseJsonText(line);
|
|
1129
|
-
} catch (error) {
|
|
1130
|
-
throw new Error(
|
|
1131
|
-
`fusion tool-call log line ${String(index)} is invalid JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
1132
|
-
);
|
|
1133
|
-
}
|
|
1134
|
-
return parseToolCallLogRecord(parsed, `fusion tool-call log line ${String(index)}`);
|
|
1135
|
-
});
|
|
1136
|
-
const seen = new Set<number>();
|
|
1137
|
-
for (const [index, record] of records.entries()) {
|
|
1138
|
-
if (seen.has(record.ordinal)) {
|
|
1139
|
-
throw new Error(`fusion tool-call log duplicate ordinal ${String(record.ordinal)}`);
|
|
1140
|
-
}
|
|
1141
|
-
seen.add(record.ordinal);
|
|
1142
|
-
if (record.ordinal !== index) {
|
|
1143
|
-
throw new Error(
|
|
1144
|
-
`fusion tool-call log ordinal gap: expected ${String(index)}, observed ${String(record.ordinal)}`,
|
|
1145
|
-
);
|
|
1146
|
-
}
|
|
1147
|
-
}
|
|
1148
|
-
return {
|
|
1149
|
-
bytes,
|
|
1150
|
-
records,
|
|
1151
|
-
summary: {
|
|
1152
|
-
count: records.length,
|
|
1153
|
-
total_result_bytes: records.reduce((sum, record) => sum + record.result_bytes, 0),
|
|
1154
|
-
trace_complete: true,
|
|
1155
|
-
},
|
|
1156
|
-
};
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
async function assertCompletedToolPolicy(
|
|
1160
|
-
trace: FusionToolCallTrace,
|
|
1161
|
-
capability: FusionCapability,
|
|
1162
|
-
sourcePolicy: { path: string; sha256: string } | undefined,
|
|
1163
|
-
): Promise<void> {
|
|
1164
|
-
const allowed =
|
|
1165
|
-
capability === 'inspect'
|
|
1166
|
-
? FUSION_INSPECT_TOOLS
|
|
1167
|
-
: capability === 'research'
|
|
1168
|
-
? FUSION_RESEARCH_TOOLS
|
|
1169
|
-
: [];
|
|
1170
|
-
const allowedSet = new Set<string>(allowed);
|
|
1171
|
-
const declared =
|
|
1172
|
-
capability === 'research' && sourcePolicy !== undefined
|
|
1173
|
-
? new Set(
|
|
1174
|
-
(await readFusionSourcePolicyFile(sourcePolicy.path, sourcePolicy.sha256)).sources.map(
|
|
1175
|
-
(source) => source.canonical_url,
|
|
1176
|
-
),
|
|
1177
|
-
)
|
|
1178
|
-
: undefined;
|
|
1179
|
-
for (const record of trace.records) {
|
|
1180
|
-
if (!allowedSet.has(record.tool_name)) {
|
|
1181
|
-
throw new Error(`fusion child used non-allowlisted tool ${record.tool_name}`);
|
|
1182
|
-
}
|
|
1183
|
-
if (capability === 'research' && record.tool_name === FUSION_WEB_FETCH_TOOL_NAME) {
|
|
1184
|
-
if (sourcePolicy === undefined || declared === undefined)
|
|
1185
|
-
throw new Error('fusion research source policy missing during audit');
|
|
1186
|
-
if (record.status === 'ok') {
|
|
1187
|
-
if (record.url === undefined) throw new Error('fusion research fetch audit is missing url');
|
|
1188
|
-
const canonicalUrl = canonicalizeFusionPublicUrl(record.url);
|
|
1189
|
-
if (record.url !== canonicalUrl)
|
|
1190
|
-
throw new Error('fusion research fetch audit URL was not canonical');
|
|
1191
|
-
if (!declared.has(canonicalUrl))
|
|
1192
|
-
throw new Error('fusion research fetch audit URL was not declared');
|
|
1193
|
-
if (record.rejected_url_sha256 !== undefined) {
|
|
1194
|
-
throw new Error(
|
|
1195
|
-
'fusion research successful fetch audit must not include rejected_url_sha256',
|
|
1196
|
-
);
|
|
1197
|
-
}
|
|
1198
|
-
if (record.final_url === undefined)
|
|
1199
|
-
throw new Error('fusion research fetch audit is missing final_url');
|
|
1200
|
-
if (record.http_status === undefined)
|
|
1201
|
-
throw new Error('fusion research fetch audit is missing http_status');
|
|
1202
|
-
if (record.response_bytes === undefined)
|
|
1203
|
-
throw new Error('fusion research fetch audit is missing response_bytes');
|
|
1204
|
-
if (record.content_sha256 === undefined)
|
|
1205
|
-
throw new Error('fusion research fetch audit is missing content_sha256');
|
|
1206
|
-
} else {
|
|
1207
|
-
if (record.url !== undefined || record.final_url !== undefined) {
|
|
1208
|
-
throw new Error('fusion research rejected fetch audit must not persist raw URL');
|
|
1209
|
-
}
|
|
1210
|
-
if (record.rejected_url_sha256 === undefined) {
|
|
1211
|
-
throw new Error('fusion research rejected fetch audit is missing rejected_url_sha256');
|
|
1212
|
-
}
|
|
1213
|
-
}
|
|
1214
|
-
}
|
|
1215
|
-
}
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
|
-
function isNotFound(error: unknown): boolean {
|
|
1219
|
-
return isJsonObject(error) && error['code'] === 'ENOENT';
|
|
1220
|
-
}
|
|
1221
|
-
|
|
1222
|
-
async function readFusionToolCallLog(path: string): Promise<FusionToolCallTrace> {
|
|
1223
|
-
let handle: Awaited<ReturnType<typeof open>>;
|
|
1224
|
-
try {
|
|
1225
|
-
handle = await open(path, constants.O_RDONLY | FUSION_PI_CHILD_O_NOFOLLOW);
|
|
1226
|
-
} catch (error) {
|
|
1227
|
-
// The child extension creates this file before tools can run, so a missing file
|
|
1228
|
-
// means the audit trail was never established - not that zero tools were used. Those
|
|
1229
|
-
// must stay distinguishable: silently accepting absence would let a run whose activity
|
|
1230
|
-
// was never recorded report success, defeating the purpose of the log.
|
|
1231
|
-
if (isNotFound(error)) {
|
|
1232
|
-
throw new Error(
|
|
1233
|
-
`fusion tool-call log is missing at ${path}; the inspect child never initialized its audit trail`,
|
|
1234
|
-
);
|
|
1235
|
-
}
|
|
1236
|
-
if (isJsonObject(error) && error['code'] === 'ELOOP') {
|
|
1237
|
-
throw new Error(
|
|
1238
|
-
`fusion tool-call log at ${path} is a symlink; refusing to trust a redirected audit trail`,
|
|
1239
|
-
);
|
|
1240
|
-
}
|
|
1241
|
-
throw error;
|
|
1242
|
-
}
|
|
1243
|
-
try {
|
|
1244
|
-
// The audit trail must be a real file inside the run directory. A symlink here would let
|
|
1245
|
-
// anything able to pre-create the path redirect the parent's read elsewhere, so the file
|
|
1246
|
-
// is opened with O_NOFOLLOW and then fstat-checked before its bytes are trusted.
|
|
1247
|
-
const stats = await handle.stat();
|
|
1248
|
-
if (!stats.isFile()) {
|
|
1249
|
-
throw new Error(
|
|
1250
|
-
`fusion tool-call log at ${path} is not a regular file; refusing to trust a redirected audit trail`,
|
|
1251
|
-
);
|
|
1252
|
-
}
|
|
1253
|
-
return parseFusionToolCallLog(await handle.readFile());
|
|
1254
|
-
} finally {
|
|
1255
|
-
await handle.close();
|
|
1256
|
-
}
|
|
1257
|
-
}
|
|
1258
|
-
|
|
1259
|
-
async function assertFusionToolCallLogSeal(
|
|
1260
|
-
path: string,
|
|
1261
|
-
trace: FusionToolCallTrace,
|
|
1262
|
-
): Promise<void> {
|
|
1263
|
-
const sealPath = `${path}${FUSION_TOOL_CALL_SEAL_SUFFIX}`;
|
|
1264
|
-
let handle: Awaited<ReturnType<typeof open>>;
|
|
1265
|
-
try {
|
|
1266
|
-
handle = await open(sealPath, constants.O_RDONLY | FUSION_PI_CHILD_O_NOFOLLOW);
|
|
1267
|
-
} catch (error) {
|
|
1268
|
-
if (isNotFound(error)) throw new Error('fusion tool-call audit completion seal is missing');
|
|
1269
|
-
if (isJsonObject(error) && error['code'] === 'ELOOP') {
|
|
1270
|
-
throw new Error('fusion tool-call audit completion seal is a symlink');
|
|
1271
|
-
}
|
|
1272
|
-
throw error;
|
|
1273
|
-
}
|
|
1274
|
-
try {
|
|
1275
|
-
const stats = await handle.stat();
|
|
1276
|
-
if (!stats.isFile())
|
|
1277
|
-
throw new Error('fusion tool-call audit completion seal is not a regular file');
|
|
1278
|
-
if (stats.size > 4096) throw new Error('fusion tool-call audit completion seal is oversized');
|
|
1279
|
-
const bytes = await handle.readFile();
|
|
1280
|
-
if (bytes.at(-1) !== 10) throw new Error('fusion tool-call audit completion seal is partial');
|
|
1281
|
-
const text = bytes.toString('utf8');
|
|
1282
|
-
if (!Buffer.from(text, 'utf8').equals(bytes)) {
|
|
1283
|
-
throw new Error('fusion tool-call audit completion seal is not UTF-8');
|
|
1284
|
-
}
|
|
1285
|
-
const parsed = parseJsonText(text);
|
|
1286
|
-
if (!isJsonObject(parsed) || Array.isArray(parsed)) {
|
|
1287
|
-
throw new Error('fusion tool-call audit completion seal must be an object');
|
|
1288
|
-
}
|
|
1289
|
-
const keys = Object.keys(parsed).sort();
|
|
1290
|
-
const expected = [
|
|
1291
|
-
'log_sha256',
|
|
1292
|
-
'record_count',
|
|
1293
|
-
'schema_version',
|
|
1294
|
-
'status',
|
|
1295
|
-
'total_result_bytes',
|
|
1296
|
-
];
|
|
1297
|
-
if (keys.join('\0') !== expected.join('\0')) {
|
|
1298
|
-
throw new Error('fusion tool-call audit completion seal keys mismatch');
|
|
1299
|
-
}
|
|
1300
|
-
if (parsed['schema_version'] !== FUSION_TOOL_CALL_SEAL_SCHEMA_VERSION) {
|
|
1301
|
-
throw new Error('fusion tool-call audit completion seal schema mismatch');
|
|
1302
|
-
}
|
|
1303
|
-
if (parsed['status'] !== 'complete') {
|
|
1304
|
-
throw new Error('fusion tool-call audit completion seal reports a failed audit');
|
|
1305
|
-
}
|
|
1306
|
-
const recordCount = requireUsageInteger(parsed, 'record_count', 'fusion tool-call audit seal');
|
|
1307
|
-
const totalResultBytes = requireUsageInteger(
|
|
1308
|
-
parsed,
|
|
1309
|
-
'total_result_bytes',
|
|
1310
|
-
'fusion tool-call audit seal',
|
|
1311
|
-
);
|
|
1312
|
-
const logSha256 = requireSha256(parsed, 'log_sha256', 'fusion tool-call audit seal');
|
|
1313
|
-
if (recordCount !== trace.summary.count) {
|
|
1314
|
-
throw new Error('fusion tool-call audit completion seal record count mismatch');
|
|
1315
|
-
}
|
|
1316
|
-
if (recordCount > FUSION_CHILD_MAX_TOOL_CALLS) {
|
|
1317
|
-
throw new Error(
|
|
1318
|
-
`fusion tool-call audit exceeds tool-call limit ${String(FUSION_CHILD_MAX_TOOL_CALLS)}`,
|
|
1319
|
-
);
|
|
1320
|
-
}
|
|
1321
|
-
if (totalResultBytes !== trace.summary.total_result_bytes) {
|
|
1322
|
-
throw new Error('fusion tool-call audit completion seal result-byte total mismatch');
|
|
1323
|
-
}
|
|
1324
|
-
if (logSha256 !== sha256Buffer(trace.bytes)) {
|
|
1325
|
-
throw new Error('fusion tool-call audit completion seal log hash mismatch');
|
|
1326
|
-
}
|
|
1327
|
-
if (totalResultBytes > FUSION_CHILD_MAX_TOTAL_TOOL_RESULT_BYTES) {
|
|
1328
|
-
throw new Error(
|
|
1329
|
-
`fusion tool-call audit exceeds aggregate result-byte limit ${String(FUSION_CHILD_MAX_TOTAL_TOOL_RESULT_BYTES)}`,
|
|
1330
|
-
);
|
|
1331
|
-
}
|
|
1332
|
-
} finally {
|
|
1333
|
-
await handle.close();
|
|
1334
|
-
}
|
|
1335
|
-
}
|
|
1336
|
-
|
|
1337
|
-
function sha256Buffer(bytes: Buffer): string {
|
|
1338
|
-
return createHash('sha256').update(bytes).digest('hex');
|
|
1339
|
-
}
|
|
1340
|
-
|
|
1341
|
-
function reconstructFinalText(response: Buffer, record: FusionChildResultMetadata): string {
|
|
1342
|
-
const blocks: Buffer[] = [];
|
|
1343
|
-
let cursor = 0;
|
|
1344
|
-
for (const [index, block] of record.text_blocks.entries()) {
|
|
1345
|
-
const end = cursor + block.utf8_bytes;
|
|
1346
|
-
if (end > response.length)
|
|
1347
|
-
throw new Error(`Pi final text block ${String(index)} is shorter than its metadata length`);
|
|
1348
|
-
const bytes = response.subarray(cursor, end);
|
|
1349
|
-
if (sha256Buffer(bytes) !== block.sha256)
|
|
1350
|
-
throw new Error(`Pi final text block ${String(index)} hash mismatch`);
|
|
1351
|
-
blocks.push(bytes);
|
|
1352
|
-
if (response.at(end) !== 10)
|
|
1353
|
-
throw new Error(`Pi final text block ${String(index)} lacks its print-mode newline`);
|
|
1354
|
-
cursor = end + 1;
|
|
1355
|
-
}
|
|
1356
|
-
if (cursor !== response.length)
|
|
1357
|
-
throw new Error('Pi final text stdout contains bytes outside declared text blocks');
|
|
1358
|
-
const joined = Buffer.concat(blocks);
|
|
1359
|
-
if (sha256Buffer(joined) !== record.text_sha256)
|
|
1360
|
-
throw new Error('Pi final text aggregate hash mismatch');
|
|
1361
|
-
const text = joined.toString('utf8');
|
|
1362
|
-
if (!Buffer.from(text, 'utf8').equals(joined))
|
|
1363
|
-
throw new Error('Pi final text is not valid UTF-8');
|
|
1364
|
-
if (fusionJsonRenderedTextBytes(text) !== record.output_contract.json_rendered_bytes) {
|
|
1365
|
-
throw new Error('Pi final text JSON-rendered byte count mismatch');
|
|
1366
|
-
}
|
|
1367
|
-
if (text.trim().length === 0) throw new Error('Pi assistant response is empty');
|
|
1368
|
-
return text;
|
|
1369
|
-
}
|
|
1370
|
-
|
|
1371
|
-
type ParsedCandidateOutputRecovery = Omit<FusionCandidateOutputRecovery, 'original_text'>;
|
|
1372
|
-
|
|
1373
|
-
async function readCandidateOutputRecovery(
|
|
1374
|
-
path: string,
|
|
1375
|
-
evidence: ParsedCandidateOutputRecovery,
|
|
1376
|
-
): Promise<FusionCandidateOutputRecovery> {
|
|
1377
|
-
let handle: Awaited<ReturnType<typeof open>>;
|
|
1378
|
-
try {
|
|
1379
|
-
handle = await open(path, constants.O_RDONLY | FUSION_PI_CHILD_O_NOFOLLOW);
|
|
1380
|
-
} catch (error) {
|
|
1381
|
-
if (isNotFound(error))
|
|
1382
|
-
throw new Error('fusion oversized candidate response artifact is missing');
|
|
1383
|
-
if (isJsonObject(error) && error['code'] === 'ELOOP') {
|
|
1384
|
-
throw new Error('fusion oversized candidate response artifact is a symlink');
|
|
1385
|
-
}
|
|
1386
|
-
throw error;
|
|
1387
|
-
}
|
|
1388
|
-
try {
|
|
1389
|
-
const stats = await handle.stat();
|
|
1390
|
-
if (!stats.isFile()) {
|
|
1391
|
-
throw new Error('fusion oversized candidate response artifact is not a regular file');
|
|
1392
|
-
}
|
|
1393
|
-
if (stats.size > FUSION_CHILD_STDOUT_LIMIT_BYTES) {
|
|
1394
|
-
throw new Error(
|
|
1395
|
-
`fusion oversized candidate response artifact exceeds ${String(FUSION_CHILD_STDOUT_LIMIT_BYTES)} bytes`,
|
|
1396
|
-
);
|
|
1397
|
-
}
|
|
1398
|
-
const bytes = await handle.readFile();
|
|
1399
|
-
if (sha256Buffer(bytes) !== evidence.original_text_sha256) {
|
|
1400
|
-
throw new Error('fusion oversized candidate response artifact hash mismatch');
|
|
1401
|
-
}
|
|
1402
|
-
const text = bytes.toString('utf8');
|
|
1403
|
-
if (!Buffer.from(text, 'utf8').equals(bytes)) {
|
|
1404
|
-
throw new Error('fusion oversized candidate response artifact is not valid UTF-8');
|
|
1405
|
-
}
|
|
1406
|
-
if (fusionJsonRenderedTextBytes(text) !== evidence.original_json_rendered_bytes) {
|
|
1407
|
-
throw new Error('fusion oversized candidate response JSON-rendered byte count mismatch');
|
|
1408
|
-
}
|
|
1409
|
-
if (evidence.original_json_rendered_bytes <= evidence.limit_bytes) {
|
|
1410
|
-
throw new Error('fusion output recovery original did not exceed the candidate contract');
|
|
1411
|
-
}
|
|
1412
|
-
return { ...evidence, original_text: text };
|
|
1413
|
-
} finally {
|
|
1414
|
-
await handle.close();
|
|
1415
|
-
}
|
|
1416
|
-
}
|
|
1417
|
-
|
|
1418
|
-
function parsedCandidateOutputRecovery(
|
|
1419
|
-
records: readonly FusionChildResultMetadata[],
|
|
1420
|
-
status: FusionCandidateOutputRecovery['status'],
|
|
1421
|
-
): ParsedCandidateOutputRecovery | undefined {
|
|
1422
|
-
const originalRecordIndex = records.findIndex(
|
|
1423
|
-
(record) => record.output_contract.recovery_role === 'oversized_original',
|
|
1424
|
-
);
|
|
1425
|
-
if (originalRecordIndex < 0) return undefined;
|
|
1426
|
-
const original = records[originalRecordIndex];
|
|
1427
|
-
if (original === undefined) throw new Error('Pi output recovery original record disappeared');
|
|
1428
|
-
const replacementRecordIndex = records.findIndex(
|
|
1429
|
-
(record) => record.output_contract.recovery_role === 'replacement',
|
|
1430
|
-
);
|
|
1431
|
-
const replacement = replacementRecordIndex < 0 ? undefined : records[replacementRecordIndex];
|
|
1432
|
-
return {
|
|
1433
|
-
kind: 'same_session_compression',
|
|
1434
|
-
limit_bytes: FUSION_CANDIDATE_MAX_OUTPUT_BYTES,
|
|
1435
|
-
original_record_index: originalRecordIndex,
|
|
1436
|
-
replacement_record_index: replacementRecordIndex < 0 ? null : replacementRecordIndex,
|
|
1437
|
-
original_json_rendered_bytes: original.output_contract.json_rendered_bytes,
|
|
1438
|
-
replacement_json_rendered_bytes: replacement?.output_contract.json_rendered_bytes ?? null,
|
|
1439
|
-
original_text_sha256: original.text_sha256,
|
|
1440
|
-
status,
|
|
1441
|
-
};
|
|
1442
|
-
}
|
|
1443
|
-
|
|
1444
|
-
export class FusionPiCompactResultParser {
|
|
1445
|
-
private readonly expectedProvider: string;
|
|
1446
|
-
private readonly expectedModel: string;
|
|
1447
|
-
|
|
1448
|
-
constructor(expectedProvider: string, expectedModel: string) {
|
|
1449
|
-
this.expectedProvider = expectedProvider;
|
|
1450
|
-
this.expectedModel = expectedModel;
|
|
1451
|
-
}
|
|
1452
|
-
|
|
1453
|
-
snapshot(stderr: Buffer): ObservedChildSnapshot {
|
|
1454
|
-
try {
|
|
1455
|
-
const parsed = parseFusionChildStderr(stderr);
|
|
1456
|
-
return this.observedFromRecords(parsed.records);
|
|
1457
|
-
} catch {
|
|
1458
|
-
return { usage: createEmptyFusionUsage() };
|
|
1459
|
-
}
|
|
1460
|
-
}
|
|
1461
|
-
|
|
1462
|
-
finishOutputRecoveryFailure(
|
|
1463
|
-
response: Buffer,
|
|
1464
|
-
stderr: Buffer,
|
|
1465
|
-
): {
|
|
1466
|
-
usage: FusionUsage;
|
|
1467
|
-
provider: string;
|
|
1468
|
-
model: string;
|
|
1469
|
-
qualifiedId: string;
|
|
1470
|
-
events: Buffer;
|
|
1471
|
-
diagnostics: Buffer;
|
|
1472
|
-
outputRecovery: ParsedCandidateOutputRecovery;
|
|
1473
|
-
} {
|
|
1474
|
-
const parsed = parseFusionChildStderr(stderr);
|
|
1475
|
-
const settlement = parseFusionChildSettlement(stderr);
|
|
1476
|
-
if (settlement === undefined) throw new Error('Pi child emitted no terminal result settlement');
|
|
1477
|
-
const expectedSettlement = buildFusionChildSettlement(parsed.records);
|
|
1478
|
-
if (JSON.stringify(settlement) !== JSON.stringify(expectedSettlement)) {
|
|
1479
|
-
throw new Error('Pi child terminal result settlement does not match the metadata stream');
|
|
1480
|
-
}
|
|
1481
|
-
if (settlement.status !== 'failed' || settlement.failure_reason !== 'output_recovery') {
|
|
1482
|
-
throw new Error('Pi child did not report a failed candidate output recovery');
|
|
1483
|
-
}
|
|
1484
|
-
if (parsed.diagnostics.includes(PI_EXTENSION_ERROR_PREFIX_BYTES)) {
|
|
1485
|
-
throw new Error('Pi child reported an extension error diagnostic');
|
|
1486
|
-
}
|
|
1487
|
-
const final = parsed.records.at(-1);
|
|
1488
|
-
if (final === undefined) throw new Error('Pi child emitted no compact result metadata');
|
|
1489
|
-
for (const record of parsed.records) this.assertModel(record);
|
|
1490
|
-
this.assertCacheObservationOrdinals(parsed.records);
|
|
1491
|
-
this.assertTranscriptStopReasons(parsed.records);
|
|
1492
|
-
reconstructFinalText(response, final);
|
|
1493
|
-
const outputRecovery = parsedCandidateOutputRecovery(parsed.records, 'failed');
|
|
1494
|
-
if (outputRecovery === undefined) {
|
|
1495
|
-
throw new Error('Pi child output-recovery failure omitted the oversized original');
|
|
1496
|
-
}
|
|
1497
|
-
const observed = this.observedFromRecords(parsed.records);
|
|
1498
|
-
return {
|
|
1499
|
-
usage: observed.usage,
|
|
1500
|
-
provider: final.provider,
|
|
1501
|
-
model: final.model,
|
|
1502
|
-
qualifiedId: `${final.provider}/${final.model}`,
|
|
1503
|
-
events: parsed.events,
|
|
1504
|
-
diagnostics: parsed.diagnostics,
|
|
1505
|
-
outputRecovery,
|
|
1506
|
-
};
|
|
1507
|
-
}
|
|
1508
|
-
|
|
1509
|
-
finish(
|
|
1510
|
-
response: Buffer,
|
|
1511
|
-
stderr: Buffer,
|
|
1512
|
-
): {
|
|
1513
|
-
text: string;
|
|
1514
|
-
usage: FusionUsage;
|
|
1515
|
-
firstRequestUsage: FusionUsage;
|
|
1516
|
-
providerRequestCount: number;
|
|
1517
|
-
provider: string;
|
|
1518
|
-
model: string;
|
|
1519
|
-
qualifiedId: string;
|
|
1520
|
-
events: Buffer;
|
|
1521
|
-
diagnostics: Buffer;
|
|
1522
|
-
outputRecovery: ParsedCandidateOutputRecovery | undefined;
|
|
1523
|
-
} {
|
|
1524
|
-
const parsed = parseFusionChildStderr(stderr);
|
|
1525
|
-
const runtimeGuard = parseFusionRuntimeGuard(stderr);
|
|
1526
|
-
if (runtimeGuard !== undefined) {
|
|
1527
|
-
throw new Error(`Pi child runtime guard refused the run: ${runtimeGuard.message}`);
|
|
1528
|
-
}
|
|
1529
|
-
const settlement = parseFusionChildSettlement(stderr);
|
|
1530
|
-
if (settlement === undefined) throw new Error('Pi child emitted no terminal result settlement');
|
|
1531
|
-
const expectedSettlement = buildFusionChildSettlement(parsed.records);
|
|
1532
|
-
if (JSON.stringify(settlement) !== JSON.stringify(expectedSettlement)) {
|
|
1533
|
-
throw new Error('Pi child terminal result settlement does not match the metadata stream');
|
|
1534
|
-
}
|
|
1535
|
-
if (parsed.diagnostics.includes(PI_EXTENSION_ERROR_PREFIX_BYTES)) {
|
|
1536
|
-
throw new Error('Pi child reported an extension error diagnostic');
|
|
1537
|
-
}
|
|
1538
|
-
const final = parsed.records.at(-1);
|
|
1539
|
-
if (final === undefined) throw new Error('Pi child emitted no compact result metadata');
|
|
1540
|
-
for (const record of parsed.records) this.assertModel(record);
|
|
1541
|
-
this.assertCacheObservationOrdinals(parsed.records);
|
|
1542
|
-
this.assertTranscriptStopReasons(parsed.records);
|
|
1543
|
-
if (settlement.status !== 'complete') {
|
|
1544
|
-
throw new Error(
|
|
1545
|
-
`Pi child terminal result settlement failed: ${settlement.failure_reason ?? 'unknown'}`,
|
|
1546
|
-
);
|
|
1547
|
-
}
|
|
1548
|
-
const observed = this.observedFromRecords(parsed.records);
|
|
1549
|
-
const text = reconstructFinalText(response, final);
|
|
1550
|
-
return {
|
|
1551
|
-
text,
|
|
1552
|
-
usage: observed.usage,
|
|
1553
|
-
firstRequestUsage: cloneFusionUsage(parsed.records[0]?.usage ?? createEmptyFusionUsage()),
|
|
1554
|
-
providerRequestCount: parsed.records.length,
|
|
1555
|
-
provider: final.provider,
|
|
1556
|
-
model: final.model,
|
|
1557
|
-
qualifiedId: `${final.provider}/${final.model}`,
|
|
1558
|
-
events: parsed.events,
|
|
1559
|
-
diagnostics: parsed.diagnostics,
|
|
1560
|
-
outputRecovery: parsedCandidateOutputRecovery(parsed.records, 'completed'),
|
|
1561
|
-
};
|
|
1562
|
-
}
|
|
1563
|
-
|
|
1564
|
-
private assertModel(record: FusionChildResultMetadata): void {
|
|
1565
|
-
if (record.provider !== this.expectedProvider || record.model !== this.expectedModel) {
|
|
1566
|
-
throw new Error(
|
|
1567
|
-
`Pi assistant model mismatch: expected ${this.expectedProvider}/${this.expectedModel}, observed ${record.provider}/${record.model}`,
|
|
1568
|
-
);
|
|
1569
|
-
}
|
|
1570
|
-
}
|
|
1571
|
-
|
|
1572
|
-
private assertCacheObservationOrdinals(records: readonly FusionChildResultMetadata[]): void {
|
|
1573
|
-
let priorOrdinal = 0;
|
|
1574
|
-
for (const [index, record] of records.entries()) {
|
|
1575
|
-
const ordinal = record.cache_observation.request_ordinal;
|
|
1576
|
-
if (ordinal <= priorOrdinal) {
|
|
1577
|
-
throw new Error(
|
|
1578
|
-
`Pi child cache observation ordinal is not increasing at result ${String(index)}: previous ${String(priorOrdinal)}, observed ${String(ordinal)}`,
|
|
1579
|
-
);
|
|
1580
|
-
}
|
|
1581
|
-
priorOrdinal = ordinal;
|
|
1582
|
-
}
|
|
1583
|
-
}
|
|
1584
|
-
|
|
1585
|
-
private assertTranscriptStopReasons(records: readonly FusionChildResultMetadata[]): void {
|
|
1586
|
-
for (const [index, record] of records.entries()) {
|
|
1587
|
-
const isFinal = index === records.length - 1;
|
|
1588
|
-
if (isFinal) {
|
|
1589
|
-
if (record.stop_reason !== 'stop') {
|
|
1590
|
-
throw new Error(this.stopReasonError('final', 'stop', record.stop_reason, true));
|
|
1591
|
-
}
|
|
1592
|
-
} else if (
|
|
1593
|
-
record.stop_reason !== 'toolUse' &&
|
|
1594
|
-
!isRecoverableFusionChildErrorRecord(record) &&
|
|
1595
|
-
record.output_contract.recovery_role !== 'oversized_original'
|
|
1596
|
-
) {
|
|
1597
|
-
throw new Error(
|
|
1598
|
-
this.stopReasonError(
|
|
1599
|
-
`non-final record ${index}`,
|
|
1600
|
-
'toolUse, a settled zero-usage retry marker, or one oversized original',
|
|
1601
|
-
record.stop_reason,
|
|
1602
|
-
true,
|
|
1603
|
-
),
|
|
1604
|
-
);
|
|
1605
|
-
}
|
|
1606
|
-
}
|
|
1607
|
-
}
|
|
1608
|
-
|
|
1609
|
-
private stopReasonError(
|
|
1610
|
-
position: string,
|
|
1611
|
-
expected: string,
|
|
1612
|
-
observed: string,
|
|
1613
|
-
includeStopDetail: boolean,
|
|
1614
|
-
): string {
|
|
1615
|
-
const prefix = `Pi ${position} stop reason is not ${expected}: ${observed}`;
|
|
1616
|
-
if (!includeStopDetail) return prefix;
|
|
1617
|
-
switch (observed) {
|
|
1618
|
-
case 'length':
|
|
1619
|
-
return `${prefix} (model output was truncated)`;
|
|
1620
|
-
case 'error':
|
|
1621
|
-
return `${prefix} (Pi reported an error stop)`;
|
|
1622
|
-
case 'aborted':
|
|
1623
|
-
return `${prefix} (Pi reported an aborted stop)`;
|
|
1624
|
-
case 'pending':
|
|
1625
|
-
return `${prefix} (Pi reported a pending stop)`;
|
|
1626
|
-
default:
|
|
1627
|
-
return prefix;
|
|
1628
|
-
}
|
|
1629
|
-
}
|
|
1630
|
-
|
|
1631
|
-
private observedFromRecords(
|
|
1632
|
-
records: readonly FusionChildResultMetadata[],
|
|
1633
|
-
): ObservedChildSnapshot {
|
|
1634
|
-
const usage = createEmptyFusionUsage();
|
|
1635
|
-
for (const record of records) addFusionUsage(usage, record.usage);
|
|
1636
|
-
const final = records.at(-1);
|
|
1637
|
-
if (final === undefined) return { usage };
|
|
1638
|
-
return {
|
|
1639
|
-
usage,
|
|
1640
|
-
provider: final.provider,
|
|
1641
|
-
model: final.model,
|
|
1642
|
-
qualifiedId: `${final.provider}/${final.model}`,
|
|
1643
|
-
};
|
|
1644
|
-
}
|
|
1645
|
-
}
|
|
1646
|
-
|
|
1647
|
-
function appendCapped(
|
|
1648
|
-
chunks: Buffer[],
|
|
1649
|
-
currentBytes: number,
|
|
1650
|
-
chunk: Buffer,
|
|
1651
|
-
limit: number,
|
|
1652
|
-
): { bytes: number; accepted: Buffer; exceeded: boolean } {
|
|
1653
|
-
if (currentBytes >= limit)
|
|
1654
|
-
return { bytes: currentBytes, accepted: Buffer.alloc(0), exceeded: true };
|
|
1655
|
-
const remaining = limit - currentBytes;
|
|
1656
|
-
if (chunk.length <= remaining) {
|
|
1657
|
-
chunks.push(chunk);
|
|
1658
|
-
return { bytes: currentBytes + chunk.length, accepted: chunk, exceeded: false };
|
|
1659
|
-
}
|
|
1660
|
-
const accepted = chunk.subarray(0, remaining);
|
|
1661
|
-
if (accepted.length > 0) chunks.push(accepted);
|
|
1662
|
-
return { bytes: limit, accepted, exceeded: true };
|
|
1663
|
-
}
|
|
1664
|
-
|
|
1665
|
-
function codeOf(error: unknown): string | undefined {
|
|
1666
|
-
return isJsonObject(error) && typeof error['code'] === 'string' ? error['code'] : undefined;
|
|
1667
|
-
}
|
|
1668
|
-
|
|
1669
|
-
function isTransientSpawnCode(code: string | undefined): boolean {
|
|
1670
|
-
return code === 'EAGAIN' || code === 'EMFILE' || code === 'ENFILE';
|
|
1671
|
-
}
|
|
1672
|
-
|
|
1673
|
-
function childError(
|
|
1674
|
-
message: string,
|
|
1675
|
-
code: FusionError['code'],
|
|
1676
|
-
input: Pick<RunPiChildOptions, 'stage' | 'slot' | 'attempt'>,
|
|
1677
|
-
transient = false,
|
|
1678
|
-
childCreated = true,
|
|
1679
|
-
): FusionError {
|
|
1680
|
-
const details: FusionErrorDetails = {
|
|
1681
|
-
code,
|
|
1682
|
-
stage: input.stage,
|
|
1683
|
-
attempt: input.attempt,
|
|
1684
|
-
transient,
|
|
1685
|
-
childCreated,
|
|
1686
|
-
};
|
|
1687
|
-
if (input.slot !== undefined) details.slot = input.slot;
|
|
1688
|
-
return new FusionError(message, details);
|
|
1689
|
-
}
|
|
1690
|
-
|
|
1691
|
-
function withCleanupErrors(error: FusionError, cleanupErrors: readonly string[]): FusionError {
|
|
1692
|
-
if (cleanupErrors.length === 0) return error;
|
|
1693
|
-
const details: FusionErrorDetails = {
|
|
1694
|
-
code: error.code,
|
|
1695
|
-
transient: error.transient,
|
|
1696
|
-
childCreated: error.childCreated,
|
|
1697
|
-
};
|
|
1698
|
-
if (error.stage !== undefined) details.stage = error.stage;
|
|
1699
|
-
if (error.slot !== undefined) details.slot = error.slot;
|
|
1700
|
-
if (error.attempt !== undefined) details.attempt = error.attempt;
|
|
1701
|
-
if (error.artifactDir !== undefined) details.artifactDir = error.artifactDir;
|
|
1702
|
-
return new FusionError(
|
|
1703
|
-
`${error.message}; process cleanup issues: ${cleanupErrors.join('; ')}`,
|
|
1704
|
-
details,
|
|
1705
|
-
);
|
|
1706
|
-
}
|
|
1707
|
-
|
|
1708
|
-
function defaultSpawn(command: string, args: string[], options: SpawnOptions): FusionChildProcess {
|
|
1709
|
-
return nodeSpawn(command, args, options);
|
|
1710
|
-
}
|
|
1711
|
-
|
|
1712
|
-
/**
|
|
1713
|
-
* Termination timers must keep the event loop alive.
|
|
1714
|
-
*
|
|
1715
|
-
* The SIGTERM grace, SIGKILL wait, overall timeout, and idle timeout timers
|
|
1716
|
-
* are the only things that settle the run promise when a child stops emitting events. An
|
|
1717
|
-
* unref'd timer lets the loop drain first, leaving the promise pending forever
|
|
1718
|
-
* ("Promise resolution is still pending but the event loop has already
|
|
1719
|
-
* resolved"). Every timer stored here is cleared in the `finally` of
|
|
1720
|
-
* `runPiChild` via `cleanupTimers`, so keeping them referenced cannot leak.
|
|
1721
|
-
*/
|
|
1722
|
-
function trackTimer(timer: NodeJS.Timeout): NodeJS.Timeout {
|
|
1723
|
-
return timer;
|
|
1724
|
-
}
|
|
1725
|
-
|
|
1726
|
-
function rememberCleanupErrors(
|
|
1727
|
-
state: ProcessState,
|
|
1728
|
-
signal: NodeJS.Signals,
|
|
1729
|
-
errors: readonly string[],
|
|
1730
|
-
): void {
|
|
1731
|
-
for (const error of errors) state.cleanupErrors.push(`${signal}: ${error}`);
|
|
1732
|
-
}
|
|
1733
|
-
|
|
1734
|
-
function terminateChild(
|
|
1735
|
-
child: FusionChildProcess,
|
|
1736
|
-
state: ProcessState,
|
|
1737
|
-
platform: NodeJS.Platform,
|
|
1738
|
-
killProcess: FusionKillProcess,
|
|
1739
|
-
killGraceMs: number,
|
|
1740
|
-
sigkillWaitMs: number,
|
|
1741
|
-
settleSyntheticClose: (close: CloseRecord) => void,
|
|
1742
|
-
): void {
|
|
1743
|
-
if (state.settled || state.terminationStarted) return;
|
|
1744
|
-
state.terminationStarted = true;
|
|
1745
|
-
const termResult = sendSignal(child, platform, killProcess, 'SIGTERM');
|
|
1746
|
-
rememberCleanupErrors(state, 'SIGTERM', termResult.errors);
|
|
1747
|
-
if (!termResult.sent && state.primaryError === undefined) {
|
|
1748
|
-
state.primaryError = new FusionError(
|
|
1749
|
-
`Pi child SIGTERM failed: ${termResult.errors.join('; ')}`,
|
|
1750
|
-
{
|
|
1751
|
-
code: 'child_exit_failed',
|
|
1752
|
-
childCreated: true,
|
|
1753
|
-
},
|
|
1754
|
-
);
|
|
1755
|
-
}
|
|
1756
|
-
state.termTimer = trackTimer(
|
|
1757
|
-
setTimeout(() => {
|
|
1758
|
-
if (state.settled) return;
|
|
1759
|
-
const killResult = sendSignal(child, platform, killProcess, 'SIGKILL');
|
|
1760
|
-
rememberCleanupErrors(state, 'SIGKILL', killResult.errors);
|
|
1761
|
-
if (!killResult.sent && state.primaryError === undefined) {
|
|
1762
|
-
state.primaryError = new FusionError(
|
|
1763
|
-
`Pi child SIGKILL failed: ${killResult.errors.join('; ')}`,
|
|
1764
|
-
{
|
|
1765
|
-
code: 'child_exit_failed',
|
|
1766
|
-
childCreated: true,
|
|
1767
|
-
},
|
|
1768
|
-
);
|
|
1769
|
-
}
|
|
1770
|
-
}, killGraceMs),
|
|
1771
|
-
);
|
|
1772
|
-
state.waitTimer = trackTimer(
|
|
1773
|
-
setTimeout(() => {
|
|
1774
|
-
if (state.settled) return;
|
|
1775
|
-
const message = 'Pi child did not emit close after SIGKILL wait';
|
|
1776
|
-
state.cleanupErrors.push(message);
|
|
1777
|
-
if (state.primaryError === undefined) {
|
|
1778
|
-
state.primaryError = new FusionError(message, {
|
|
1779
|
-
code: 'child_exit_failed',
|
|
1780
|
-
childCreated: true,
|
|
1781
|
-
});
|
|
1782
|
-
}
|
|
1783
|
-
settleSyntheticClose({ code: null, signal: 'SIGKILL' });
|
|
1784
|
-
}, killGraceMs + sigkillWaitMs),
|
|
1785
|
-
);
|
|
1786
|
-
}
|
|
1787
|
-
|
|
1788
|
-
function sendSignal(
|
|
1789
|
-
child: FusionChildProcess,
|
|
1790
|
-
platform: NodeJS.Platform,
|
|
1791
|
-
killProcess: FusionKillProcess,
|
|
1792
|
-
signal: NodeJS.Signals,
|
|
1793
|
-
): { sent: boolean; errors: readonly string[] } {
|
|
1794
|
-
const errors: string[] = [];
|
|
1795
|
-
const pid = child.pid;
|
|
1796
|
-
if (platform !== 'win32' && pid !== undefined) {
|
|
1797
|
-
try {
|
|
1798
|
-
if (killProcess(-pid, signal)) return { sent: true, errors };
|
|
1799
|
-
errors.push('process group kill returned false');
|
|
1800
|
-
} catch (error) {
|
|
1801
|
-
errors.push(
|
|
1802
|
-
`process group kill failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
1803
|
-
);
|
|
1804
|
-
}
|
|
1805
|
-
}
|
|
1806
|
-
try {
|
|
1807
|
-
if (child.kill(signal)) return { sent: true, errors };
|
|
1808
|
-
errors.push('child kill returned false');
|
|
1809
|
-
} catch (error) {
|
|
1810
|
-
errors.push(`child kill failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1811
|
-
}
|
|
1812
|
-
return { sent: false, errors };
|
|
1813
|
-
}
|
|
1814
|
-
|
|
1815
|
-
function cleanupTimers(state: ProcessState): void {
|
|
1816
|
-
if (state.termTimer !== undefined) clearTimeout(state.termTimer);
|
|
1817
|
-
if (state.waitTimer !== undefined) clearTimeout(state.waitTimer);
|
|
1818
|
-
if (state.timeoutTimer !== undefined) clearTimeout(state.timeoutTimer);
|
|
1819
|
-
if (state.idleTimer !== undefined) clearTimeout(state.idleTimer);
|
|
1820
|
-
state.termTimer = undefined;
|
|
1821
|
-
state.waitTimer = undefined;
|
|
1822
|
-
state.timeoutTimer = undefined;
|
|
1823
|
-
state.idleTimer = undefined;
|
|
1824
|
-
}
|
|
1825
|
-
|
|
1826
|
-
async function writePromptToStdin(child: FusionChildProcess, prompt: string): Promise<void> {
|
|
1827
|
-
const stdin = child.stdin;
|
|
1828
|
-
if (stdin === undefined || stdin === null) throw new Error('Pi child stdin pipe is unavailable');
|
|
1829
|
-
await new Promise<void>((resolve, reject) => {
|
|
1830
|
-
let settled = false;
|
|
1831
|
-
const fail = (error: Error) => {
|
|
1832
|
-
if (settled) return;
|
|
1833
|
-
settled = true;
|
|
1834
|
-
stdin.off('error', fail);
|
|
1835
|
-
reject(error);
|
|
1836
|
-
};
|
|
1837
|
-
const finish = () => {
|
|
1838
|
-
if (settled) return;
|
|
1839
|
-
settled = true;
|
|
1840
|
-
stdin.off('error', fail);
|
|
1841
|
-
resolve();
|
|
1842
|
-
};
|
|
1843
|
-
stdin.once('error', fail);
|
|
1844
|
-
stdin.write(Buffer.from(prompt, 'utf8'), (error?: Error | null) => {
|
|
1845
|
-
if (error !== undefined && error !== null) {
|
|
1846
|
-
fail(error);
|
|
1847
|
-
return;
|
|
1848
|
-
}
|
|
1849
|
-
stdin.end(finish);
|
|
1850
|
-
});
|
|
1851
|
-
});
|
|
1852
|
-
}
|
|
1853
|
-
|
|
1854
|
-
export async function runPiChild(options: RunPiChildOptions): Promise<FusionChildRunResult> {
|
|
1855
|
-
if (options.signal?.aborted) {
|
|
1856
|
-
throw childError(
|
|
1857
|
-
'Pi child launch cancelled before spawn',
|
|
1858
|
-
'child_cancelled',
|
|
1859
|
-
options,
|
|
1860
|
-
false,
|
|
1861
|
-
false,
|
|
1862
|
-
);
|
|
1863
|
-
}
|
|
1864
|
-
const spawnImpl = options.spawn ?? defaultSpawn;
|
|
1865
|
-
const killProcess = options.killProcess ?? process.kill.bind(process);
|
|
1866
|
-
const platform = options.platform ?? process.platform;
|
|
1867
|
-
const capability = options.capability ?? FUSION_NO_TOOLS_CAPABILITY;
|
|
1868
|
-
const env = fusionPiChildEnv(options.env ?? process.env, options.model.provider);
|
|
1869
|
-
if (options.candidateOutputRecoveryPath !== undefined) {
|
|
1870
|
-
if (options.stage !== 'candidate') {
|
|
1871
|
-
throw childError(
|
|
1872
|
-
'candidate output recovery may be enabled only for candidate children',
|
|
1873
|
-
'orchestration_failed',
|
|
1874
|
-
options,
|
|
1875
|
-
false,
|
|
1876
|
-
false,
|
|
1877
|
-
);
|
|
1878
|
-
}
|
|
1879
|
-
env[FUSION_CANDIDATE_OUTPUT_RECOVERY_PATH_ENV] = options.candidateOutputRecoveryPath;
|
|
1880
|
-
}
|
|
1881
|
-
if (capability !== 'reason') {
|
|
1882
|
-
if (options.toolCallLogPath === undefined) {
|
|
1883
|
-
throw childError(
|
|
1884
|
-
`fusion ${capability} child requires a tool-call log path`,
|
|
1885
|
-
'orchestration_failed',
|
|
1886
|
-
options,
|
|
1887
|
-
false,
|
|
1888
|
-
false,
|
|
1889
|
-
);
|
|
1890
|
-
}
|
|
1891
|
-
env[FUSION_TOOL_CALL_LOG_PATH_ENV] = options.toolCallLogPath;
|
|
1892
|
-
if (capability === 'research') {
|
|
1893
|
-
if (options.sourcePolicy === undefined) {
|
|
1894
|
-
throw childError(
|
|
1895
|
-
'fusion research child requires a source-policy path and hash',
|
|
1896
|
-
'orchestration_failed',
|
|
1897
|
-
options,
|
|
1898
|
-
false,
|
|
1899
|
-
false,
|
|
1900
|
-
);
|
|
1901
|
-
}
|
|
1902
|
-
env[FUSION_RESEARCH_ENABLED_ENV] = '1';
|
|
1903
|
-
env[FUSION_SOURCE_POLICY_PATH_ENV] = options.sourcePolicy.path;
|
|
1904
|
-
env[FUSION_SOURCE_POLICY_SHA256_ENV] = options.sourcePolicy.sha256;
|
|
1905
|
-
}
|
|
1906
|
-
}
|
|
1907
|
-
const stdoutLimit = options.stdoutLimitBytes ?? FUSION_CHILD_STDOUT_LIMIT_BYTES;
|
|
1908
|
-
const stderrLimit = options.stderrLimitBytes ?? FUSION_CHILD_STDERR_LIMIT_BYTES;
|
|
1909
|
-
const timeoutMs = options.timeoutMs ?? FUSION_CHILD_TIMEOUT_MS;
|
|
1910
|
-
const idleTimeoutMs = options.idleTimeoutMs ?? FUSION_CHILD_IDLE_TIMEOUT_MS;
|
|
1911
|
-
const killGraceMs = options.killGraceMs ?? FUSION_CHILD_KILL_GRACE_MS;
|
|
1912
|
-
const sigkillWaitMs = options.sigkillWaitMs ?? FUSION_CHILD_SIGKILL_WAIT_MS;
|
|
1913
|
-
const argv = buildFusionPiChildArgv(
|
|
1914
|
-
options.model,
|
|
1915
|
-
options.systemPrompt,
|
|
1916
|
-
options.childExtensionPath ?? resolveFusionChildExtensionPath(),
|
|
1917
|
-
capability,
|
|
1918
|
-
);
|
|
1919
|
-
const parser = new FusionPiCompactResultParser(options.model.provider, options.model.model);
|
|
1920
|
-
const stdoutChunks: Buffer[] = [];
|
|
1921
|
-
const stderrChunks: Buffer[] = [];
|
|
1922
|
-
let stdoutBytes = 0;
|
|
1923
|
-
let stderrBytes = 0;
|
|
1924
|
-
const state: ProcessState = {
|
|
1925
|
-
primaryError: undefined,
|
|
1926
|
-
cleanupErrors: [],
|
|
1927
|
-
terminationStarted: false,
|
|
1928
|
-
termTimer: undefined,
|
|
1929
|
-
waitTimer: undefined,
|
|
1930
|
-
timeoutTimer: undefined,
|
|
1931
|
-
idleTimer: undefined,
|
|
1932
|
-
settled: false,
|
|
1933
|
-
};
|
|
1934
|
-
|
|
1935
|
-
let child: FusionChildProcess;
|
|
1936
|
-
try {
|
|
1937
|
-
const launchDeps =
|
|
1938
|
-
options.piLaunchDependencies === undefined
|
|
1939
|
-
? { platform }
|
|
1940
|
-
: { ...options.piLaunchDependencies, platform };
|
|
1941
|
-
const launch = resolvePiLaunch(launchDeps);
|
|
1942
|
-
assertWindowsCommandLineWithinLimit(launch, argv, platform, `fusion-${options.stage}`);
|
|
1943
|
-
child = spawnImpl(launch.executable, piLaunchArgv(launch, argv), {
|
|
1944
|
-
cwd: options.cwd,
|
|
1945
|
-
detached: platform !== 'win32',
|
|
1946
|
-
shell: false,
|
|
1947
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1948
|
-
env,
|
|
1949
|
-
windowsHide: true,
|
|
1950
|
-
});
|
|
1951
|
-
} catch (error) {
|
|
1952
|
-
const code = codeOf(error);
|
|
1953
|
-
throw childError(
|
|
1954
|
-
`Pi child spawn failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
1955
|
-
'child_spawn_failed',
|
|
1956
|
-
options,
|
|
1957
|
-
isTransientSpawnCode(code),
|
|
1958
|
-
false,
|
|
1959
|
-
);
|
|
1960
|
-
}
|
|
1961
|
-
|
|
1962
|
-
let settleClose: (close: CloseRecord) => void = () => undefined;
|
|
1963
|
-
const closePromise = new Promise<CloseRecord>((resolve) => {
|
|
1964
|
-
settleClose = (close) => {
|
|
1965
|
-
if (state.settled) return;
|
|
1966
|
-
state.settled = true;
|
|
1967
|
-
resolve(close);
|
|
1968
|
-
};
|
|
1969
|
-
});
|
|
1970
|
-
|
|
1971
|
-
const resetIdleTimer = () => {
|
|
1972
|
-
if (state.settled) return;
|
|
1973
|
-
if (state.idleTimer !== undefined) clearTimeout(state.idleTimer);
|
|
1974
|
-
state.idleTimer = trackTimer(
|
|
1975
|
-
setTimeout(() => {
|
|
1976
|
-
if (state.settled) return;
|
|
1977
|
-
if (state.primaryError === undefined) {
|
|
1978
|
-
state.primaryError = childError(
|
|
1979
|
-
`Pi child produced no output for ${String(idleTimeoutMs)}ms (stalled)`,
|
|
1980
|
-
'child_timeout',
|
|
1981
|
-
options,
|
|
1982
|
-
);
|
|
1983
|
-
}
|
|
1984
|
-
terminateChild(
|
|
1985
|
-
child,
|
|
1986
|
-
state,
|
|
1987
|
-
platform,
|
|
1988
|
-
killProcess,
|
|
1989
|
-
killGraceMs,
|
|
1990
|
-
sigkillWaitMs,
|
|
1991
|
-
settleClose,
|
|
1992
|
-
);
|
|
1993
|
-
}, idleTimeoutMs),
|
|
1994
|
-
);
|
|
1995
|
-
};
|
|
1996
|
-
const abortListener = () => {
|
|
1997
|
-
if (state.settled) return;
|
|
1998
|
-
if (state.primaryError === undefined) {
|
|
1999
|
-
state.primaryError = childError('Pi child cancelled', 'child_cancelled', options);
|
|
2000
|
-
}
|
|
2001
|
-
terminateChild(child, state, platform, killProcess, killGraceMs, sigkillWaitMs, settleClose);
|
|
2002
|
-
};
|
|
2003
|
-
const stdoutListener = (data: Buffer | string) => {
|
|
2004
|
-
resetIdleTimer();
|
|
2005
|
-
const chunk = Buffer.isBuffer(data) ? data : Buffer.from(data, 'utf8');
|
|
2006
|
-
const appended = appendCapped(stdoutChunks, stdoutBytes, chunk, stdoutLimit);
|
|
2007
|
-
stdoutBytes = appended.bytes;
|
|
2008
|
-
if (appended.exceeded && state.primaryError === undefined) {
|
|
2009
|
-
state.primaryError = childError(
|
|
2010
|
-
`Pi child final response exceeded ${String(stdoutLimit)} bytes`,
|
|
2011
|
-
'child_output_cap',
|
|
2012
|
-
options,
|
|
2013
|
-
);
|
|
2014
|
-
terminateChild(child, state, platform, killProcess, killGraceMs, sigkillWaitMs, settleClose);
|
|
2015
|
-
}
|
|
2016
|
-
};
|
|
2017
|
-
const stderrListener = (data: Buffer | string) => {
|
|
2018
|
-
resetIdleTimer();
|
|
2019
|
-
const chunk = Buffer.isBuffer(data) ? data : Buffer.from(data, 'utf8');
|
|
2020
|
-
const appended = appendCapped(stderrChunks, stderrBytes, chunk, stderrLimit);
|
|
2021
|
-
stderrBytes = appended.bytes;
|
|
2022
|
-
if (appended.exceeded && state.primaryError === undefined) {
|
|
2023
|
-
state.primaryError = childError(
|
|
2024
|
-
`Pi child stderr exceeded ${String(stderrLimit)} bytes`,
|
|
2025
|
-
'child_output_cap',
|
|
2026
|
-
options,
|
|
2027
|
-
);
|
|
2028
|
-
terminateChild(child, state, platform, killProcess, killGraceMs, sigkillWaitMs, settleClose);
|
|
2029
|
-
}
|
|
2030
|
-
};
|
|
2031
|
-
const errorListener = (error: Error) => {
|
|
2032
|
-
if (state.settled) return;
|
|
2033
|
-
if (state.primaryError === undefined) {
|
|
2034
|
-
const code = codeOf(error);
|
|
2035
|
-
const childCreated = child.pid !== undefined;
|
|
2036
|
-
state.primaryError = childError(
|
|
2037
|
-
`Pi child process error: ${error.message}`,
|
|
2038
|
-
'child_spawn_failed',
|
|
2039
|
-
options,
|
|
2040
|
-
isTransientSpawnCode(code),
|
|
2041
|
-
childCreated,
|
|
2042
|
-
);
|
|
2043
|
-
}
|
|
2044
|
-
if (child.pid === undefined) settleClose({ code: null, signal: null });
|
|
2045
|
-
};
|
|
2046
|
-
const closeListener = (code: number | null, signal: NodeJS.Signals | null) => {
|
|
2047
|
-
settleClose({ code, signal });
|
|
2048
|
-
};
|
|
2049
|
-
|
|
2050
|
-
child.stdout?.on('data', stdoutListener);
|
|
2051
|
-
child.stderr?.on('data', stderrListener);
|
|
2052
|
-
child.once('error', errorListener);
|
|
2053
|
-
child.once('close', closeListener);
|
|
2054
|
-
options.signal?.addEventListener('abort', abortListener, { once: true });
|
|
2055
|
-
if (options.signal?.aborted) abortListener();
|
|
2056
|
-
resetIdleTimer();
|
|
2057
|
-
state.timeoutTimer = trackTimer(
|
|
2058
|
-
setTimeout(() => {
|
|
2059
|
-
if (state.primaryError === undefined) {
|
|
2060
|
-
state.primaryError = childError(
|
|
2061
|
-
`Pi child timed out after ${String(timeoutMs)}ms`,
|
|
2062
|
-
'child_timeout',
|
|
2063
|
-
options,
|
|
2064
|
-
);
|
|
2065
|
-
}
|
|
2066
|
-
terminateChild(child, state, platform, killProcess, killGraceMs, sigkillWaitMs, settleClose);
|
|
2067
|
-
}, timeoutMs),
|
|
2068
|
-
);
|
|
2069
|
-
|
|
2070
|
-
try {
|
|
2071
|
-
try {
|
|
2072
|
-
if (state.primaryError === undefined) await writePromptToStdin(child, options.userPrompt);
|
|
2073
|
-
} catch (error) {
|
|
2074
|
-
if (state.primaryError === undefined) {
|
|
2075
|
-
state.primaryError = childError(
|
|
2076
|
-
`Pi child stdin write failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
2077
|
-
'child_stdin_failed',
|
|
2078
|
-
options,
|
|
2079
|
-
);
|
|
2080
|
-
}
|
|
2081
|
-
terminateChild(child, state, platform, killProcess, killGraceMs, sigkillWaitMs, settleClose);
|
|
2082
|
-
}
|
|
2083
|
-
|
|
2084
|
-
const close = await closePromise;
|
|
2085
|
-
const response = Buffer.concat(stdoutChunks);
|
|
2086
|
-
const rawStderr = Buffer.concat(stderrChunks);
|
|
2087
|
-
const observed = parser.snapshot(rawStderr);
|
|
2088
|
-
let compactEvents: Buffer = Buffer.alloc(0);
|
|
2089
|
-
let diagnostics: Buffer = rawStderr;
|
|
2090
|
-
try {
|
|
2091
|
-
const decoded = parseFusionChildStderr(rawStderr);
|
|
2092
|
-
compactEvents = decoded.events;
|
|
2093
|
-
diagnostics = decoded.diagnostics;
|
|
2094
|
-
} catch {
|
|
2095
|
-
// A primary process/cap error remains authoritative; malformed metadata is
|
|
2096
|
-
// surfaced below when the child otherwise exits successfully.
|
|
2097
|
-
}
|
|
2098
|
-
const readPartialOutputRecovery = async (): Promise<
|
|
2099
|
-
FusionCandidateOutputRecovery | undefined
|
|
2100
|
-
> => {
|
|
2101
|
-
let parsedRecords: readonly FusionChildResultMetadata[];
|
|
2102
|
-
try {
|
|
2103
|
-
parsedRecords = parseFusionChildStderr(rawStderr).records;
|
|
2104
|
-
} catch {
|
|
2105
|
-
return undefined;
|
|
2106
|
-
}
|
|
2107
|
-
const evidence = parsedCandidateOutputRecovery(parsedRecords, 'failed');
|
|
2108
|
-
if (evidence === undefined) return undefined;
|
|
2109
|
-
if (options.candidateOutputRecoveryPath === undefined) {
|
|
2110
|
-
throw new Error('fusion child emitted output-recovery evidence without an artifact path');
|
|
2111
|
-
}
|
|
2112
|
-
return readCandidateOutputRecovery(options.candidateOutputRecoveryPath, evidence);
|
|
2113
|
-
};
|
|
2114
|
-
const primary = state.primaryError;
|
|
2115
|
-
if (primary !== undefined) {
|
|
2116
|
-
let outputRecovery: FusionCandidateOutputRecovery | undefined;
|
|
2117
|
-
try {
|
|
2118
|
-
outputRecovery = await readPartialOutputRecovery();
|
|
2119
|
-
} catch (error) {
|
|
2120
|
-
state.cleanupErrors.push(
|
|
2121
|
-
`output recovery evidence invalid: ${error instanceof Error ? error.message : String(error)}`,
|
|
2122
|
-
);
|
|
2123
|
-
}
|
|
2124
|
-
throw new FusionChildRunError(
|
|
2125
|
-
withCleanupErrors(primary, state.cleanupErrors),
|
|
2126
|
-
compactEvents,
|
|
2127
|
-
response,
|
|
2128
|
-
diagnostics,
|
|
2129
|
-
close,
|
|
2130
|
-
observed,
|
|
2131
|
-
outputRecovery,
|
|
2132
|
-
);
|
|
2133
|
-
}
|
|
2134
|
-
let terminalSettlement: FusionChildSettlementRecord | undefined;
|
|
2135
|
-
try {
|
|
2136
|
-
terminalSettlement = parseFusionChildSettlement(rawStderr);
|
|
2137
|
-
} catch (error) {
|
|
2138
|
-
throw new FusionChildRunError(
|
|
2139
|
-
withCleanupErrors(
|
|
2140
|
-
childError(
|
|
2141
|
-
`Pi child terminal settlement invalid: ${error instanceof Error ? error.message : String(error)}`,
|
|
2142
|
-
'child_event_invalid',
|
|
2143
|
-
options,
|
|
2144
|
-
),
|
|
2145
|
-
state.cleanupErrors,
|
|
2146
|
-
),
|
|
2147
|
-
compactEvents,
|
|
2148
|
-
response,
|
|
2149
|
-
diagnostics,
|
|
2150
|
-
close,
|
|
2151
|
-
observed,
|
|
2152
|
-
);
|
|
2153
|
-
}
|
|
2154
|
-
if (terminalSettlement?.failure_reason === 'output_recovery') {
|
|
2155
|
-
try {
|
|
2156
|
-
const failure = parser.finishOutputRecoveryFailure(response, rawStderr);
|
|
2157
|
-
if (options.candidateOutputRecoveryPath === undefined) {
|
|
2158
|
-
throw new Error(
|
|
2159
|
-
'fusion child output-recovery failure omitted the configured artifact path',
|
|
2160
|
-
);
|
|
2161
|
-
}
|
|
2162
|
-
const outputRecovery = await readCandidateOutputRecovery(
|
|
2163
|
-
options.candidateOutputRecoveryPath,
|
|
2164
|
-
failure.outputRecovery,
|
|
2165
|
-
);
|
|
2166
|
-
const recoveryMessage =
|
|
2167
|
-
outputRecovery.replacement_json_rendered_bytes === null
|
|
2168
|
-
? `Pi child could not complete its one allowed same-session candidate compression continuation; the ${String(outputRecovery.original_json_rendered_bytes)}-byte original is preserved and nothing was truncated`
|
|
2169
|
-
: `Pi child compressed candidate response is still ${String(outputRecovery.replacement_json_rendered_bytes)} JSON-rendered bytes, exceeding the ${String(outputRecovery.limit_bytes)}-byte output contract; both responses are preserved and nothing was truncated`;
|
|
2170
|
-
throw new FusionChildRunError(
|
|
2171
|
-
withCleanupErrors(
|
|
2172
|
-
childError(recoveryMessage, 'child_output_cap', options),
|
|
2173
|
-
state.cleanupErrors,
|
|
2174
|
-
),
|
|
2175
|
-
failure.events,
|
|
2176
|
-
response,
|
|
2177
|
-
failure.diagnostics,
|
|
2178
|
-
close,
|
|
2179
|
-
{
|
|
2180
|
-
usage: failure.usage,
|
|
2181
|
-
provider: failure.provider,
|
|
2182
|
-
model: failure.model,
|
|
2183
|
-
qualifiedId: failure.qualifiedId,
|
|
2184
|
-
},
|
|
2185
|
-
outputRecovery,
|
|
2186
|
-
);
|
|
2187
|
-
} catch (error) {
|
|
2188
|
-
if (error instanceof FusionChildRunError) throw error;
|
|
2189
|
-
throw new FusionChildRunError(
|
|
2190
|
-
withCleanupErrors(
|
|
2191
|
-
childError(
|
|
2192
|
-
`Pi child output-recovery evidence invalid: ${error instanceof Error ? error.message : String(error)}`,
|
|
2193
|
-
'child_event_invalid',
|
|
2194
|
-
options,
|
|
2195
|
-
),
|
|
2196
|
-
state.cleanupErrors,
|
|
2197
|
-
),
|
|
2198
|
-
compactEvents,
|
|
2199
|
-
response,
|
|
2200
|
-
diagnostics,
|
|
2201
|
-
close,
|
|
2202
|
-
observed,
|
|
2203
|
-
);
|
|
2204
|
-
}
|
|
2205
|
-
}
|
|
2206
|
-
if (close.code !== 0 || close.signal !== null) {
|
|
2207
|
-
let runtimeGuard: FusionRuntimeGuardRecord | undefined;
|
|
2208
|
-
try {
|
|
2209
|
-
runtimeGuard = parseFusionRuntimeGuard(rawStderr);
|
|
2210
|
-
if (runtimeGuard !== undefined) {
|
|
2211
|
-
assertFusionRuntimeGuardMatchesModel(runtimeGuard, options.model);
|
|
2212
|
-
}
|
|
2213
|
-
} catch (error) {
|
|
2214
|
-
throw new FusionChildRunError(
|
|
2215
|
-
withCleanupErrors(
|
|
2216
|
-
childError(
|
|
2217
|
-
`Pi child runtime guard evidence invalid: ${error instanceof Error ? error.message : String(error)}`,
|
|
2218
|
-
'child_event_invalid',
|
|
2219
|
-
options,
|
|
2220
|
-
),
|
|
2221
|
-
state.cleanupErrors,
|
|
2222
|
-
),
|
|
2223
|
-
compactEvents,
|
|
2224
|
-
response,
|
|
2225
|
-
diagnostics,
|
|
2226
|
-
close,
|
|
2227
|
-
observed,
|
|
2228
|
-
);
|
|
2229
|
-
}
|
|
2230
|
-
throw new FusionChildRunError(
|
|
2231
|
-
withCleanupErrors(
|
|
2232
|
-
childError(
|
|
2233
|
-
runtimeGuard?.message ??
|
|
2234
|
-
`Pi child exited with code ${close.code === null ? 'null' : String(close.code)}${close.signal === null ? '' : ` (${close.signal})`}`,
|
|
2235
|
-
runtimeGuard === undefined
|
|
2236
|
-
? 'child_exit_failed'
|
|
2237
|
-
: runtimeGuard.code === 'claude_cache_policy'
|
|
2238
|
-
? 'child_cache_policy_invalid'
|
|
2239
|
-
: runtimeGuard.code === 'provider_payload_invalid'
|
|
2240
|
-
? 'child_runtime_payload_invalid'
|
|
2241
|
-
: 'child_runtime_limit_exceeded',
|
|
2242
|
-
options,
|
|
2243
|
-
),
|
|
2244
|
-
state.cleanupErrors,
|
|
2245
|
-
),
|
|
2246
|
-
compactEvents,
|
|
2247
|
-
response,
|
|
2248
|
-
diagnostics,
|
|
2249
|
-
close,
|
|
2250
|
-
observed,
|
|
2251
|
-
);
|
|
2252
|
-
}
|
|
2253
|
-
let parsed: ReturnType<FusionPiCompactResultParser['finish']>;
|
|
2254
|
-
try {
|
|
2255
|
-
parsed = parser.finish(response, rawStderr);
|
|
2256
|
-
} catch (error) {
|
|
2257
|
-
throw new FusionChildRunError(
|
|
2258
|
-
withCleanupErrors(
|
|
2259
|
-
childError(
|
|
2260
|
-
`Pi child compact result invalid: ${error instanceof Error ? error.message : String(error)}`,
|
|
2261
|
-
'child_event_invalid',
|
|
2262
|
-
options,
|
|
2263
|
-
),
|
|
2264
|
-
state.cleanupErrors,
|
|
2265
|
-
),
|
|
2266
|
-
compactEvents,
|
|
2267
|
-
response,
|
|
2268
|
-
diagnostics,
|
|
2269
|
-
close,
|
|
2270
|
-
observed,
|
|
2271
|
-
);
|
|
2272
|
-
}
|
|
2273
|
-
let outputRecovery: FusionCandidateOutputRecovery | undefined;
|
|
2274
|
-
if (parsed.outputRecovery !== undefined) {
|
|
2275
|
-
if (options.candidateOutputRecoveryPath === undefined) {
|
|
2276
|
-
throw new FusionChildRunError(
|
|
2277
|
-
childError(
|
|
2278
|
-
'Pi child output-recovery evidence has no configured artifact path',
|
|
2279
|
-
'child_event_invalid',
|
|
2280
|
-
options,
|
|
2281
|
-
),
|
|
2282
|
-
parsed.events,
|
|
2283
|
-
response,
|
|
2284
|
-
parsed.diagnostics,
|
|
2285
|
-
close,
|
|
2286
|
-
observed,
|
|
2287
|
-
);
|
|
2288
|
-
}
|
|
2289
|
-
try {
|
|
2290
|
-
outputRecovery = await readCandidateOutputRecovery(
|
|
2291
|
-
options.candidateOutputRecoveryPath,
|
|
2292
|
-
parsed.outputRecovery,
|
|
2293
|
-
);
|
|
2294
|
-
} catch (error) {
|
|
2295
|
-
throw new FusionChildRunError(
|
|
2296
|
-
childError(
|
|
2297
|
-
`Pi child output-recovery artifact invalid: ${error instanceof Error ? error.message : String(error)}`,
|
|
2298
|
-
'child_event_invalid',
|
|
2299
|
-
options,
|
|
2300
|
-
),
|
|
2301
|
-
parsed.events,
|
|
2302
|
-
response,
|
|
2303
|
-
parsed.diagnostics,
|
|
2304
|
-
close,
|
|
2305
|
-
observed,
|
|
2306
|
-
);
|
|
2307
|
-
}
|
|
2308
|
-
}
|
|
2309
|
-
let toolCallTrace: FusionToolCallTrace | undefined;
|
|
2310
|
-
if (capability !== 'reason') {
|
|
2311
|
-
// The launch path above refuses to spawn a tool-enabled child without a log path, so
|
|
2312
|
-
// this is unreachable. Assert rather than defaulting: a `?? ''` here would silently
|
|
2313
|
-
// read an empty path if that guard were ever refactored away, turning a missing
|
|
2314
|
-
// audit trail into a successful run.
|
|
2315
|
-
const logPath = options.toolCallLogPath;
|
|
2316
|
-
if (logPath === undefined) {
|
|
2317
|
-
throw childError(
|
|
2318
|
-
`fusion ${capability} child completed without a tool-call log path`,
|
|
2319
|
-
'orchestration_failed',
|
|
2320
|
-
options,
|
|
2321
|
-
);
|
|
2322
|
-
}
|
|
2323
|
-
try {
|
|
2324
|
-
toolCallTrace = await readFusionToolCallLog(logPath);
|
|
2325
|
-
await assertFusionToolCallLogSeal(logPath, toolCallTrace);
|
|
2326
|
-
await assertCompletedToolPolicy(toolCallTrace, capability, options.sourcePolicy);
|
|
2327
|
-
} catch (error) {
|
|
2328
|
-
throw new FusionChildRunError(
|
|
2329
|
-
withCleanupErrors(
|
|
2330
|
-
childError(
|
|
2331
|
-
`Pi child tool-call log invalid: ${error instanceof Error ? error.message : String(error)}`,
|
|
2332
|
-
'child_event_invalid',
|
|
2333
|
-
options,
|
|
2334
|
-
),
|
|
2335
|
-
state.cleanupErrors,
|
|
2336
|
-
),
|
|
2337
|
-
compactEvents,
|
|
2338
|
-
response,
|
|
2339
|
-
diagnostics,
|
|
2340
|
-
close,
|
|
2341
|
-
observed,
|
|
2342
|
-
outputRecovery,
|
|
2343
|
-
);
|
|
2344
|
-
}
|
|
2345
|
-
}
|
|
2346
|
-
const result: FusionChildRunResult = {
|
|
2347
|
-
stage: options.stage,
|
|
2348
|
-
attempt: options.attempt,
|
|
2349
|
-
provider: parsed.provider,
|
|
2350
|
-
model: parsed.model,
|
|
2351
|
-
qualifiedId: parsed.qualifiedId,
|
|
2352
|
-
text: parsed.text,
|
|
2353
|
-
usage: parsed.usage,
|
|
2354
|
-
firstRequestUsage: parsed.firstRequestUsage,
|
|
2355
|
-
providerRequestCount: parsed.providerRequestCount,
|
|
2356
|
-
events: parsed.events,
|
|
2357
|
-
stderr: parsed.diagnostics,
|
|
2358
|
-
exitCode: close.code,
|
|
2359
|
-
signal: close.signal,
|
|
2360
|
-
};
|
|
2361
|
-
if (options.slot !== undefined) result.slot = options.slot;
|
|
2362
|
-
if (outputRecovery !== undefined) result.outputRecovery = outputRecovery;
|
|
2363
|
-
if (toolCallTrace !== undefined) result.toolCallTrace = toolCallTrace;
|
|
2364
|
-
return result;
|
|
2365
|
-
} finally {
|
|
2366
|
-
cleanupTimers(state);
|
|
2367
|
-
options.signal?.removeEventListener('abort', abortListener);
|
|
2368
|
-
child.stdout?.off('data', stdoutListener);
|
|
2369
|
-
child.stderr?.off('data', stderrListener);
|
|
2370
|
-
child.off('error', errorListener);
|
|
2371
|
-
child.off('close', closeListener);
|
|
2372
|
-
}
|
|
2373
|
-
}
|