@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/artifacts.ts
DELETED
|
@@ -1,967 +0,0 @@
|
|
|
1
|
-
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
-
import { chmod, mkdir } from 'node:fs/promises';
|
|
3
|
-
import { basename, isAbsolute, join, relative, sep } from 'node:path';
|
|
4
|
-
import { canonicalJson, sha256Buffer } from '../attested-pi-run.js';
|
|
5
|
-
import { sanitizePathSegment } from '../types.js';
|
|
6
|
-
import { replaceFileDurable } from '../durable-fs.js';
|
|
7
|
-
import {
|
|
8
|
-
EMPTY_FUSION_USAGE,
|
|
9
|
-
FUSION_COMMITTED_RESULT_SCHEMA_VERSION,
|
|
10
|
-
FUSION_FAILURE_SUMMARY_SCHEMA_VERSION,
|
|
11
|
-
FUSION_MANIFEST_SCHEMA_VERSION,
|
|
12
|
-
FUSION_VALIDATE_CANDIDATE_CONTRACT_EVENT_SCHEMA_VERSION,
|
|
13
|
-
FusionError,
|
|
14
|
-
cloneFusionUsage,
|
|
15
|
-
type FusionArtifactManifest,
|
|
16
|
-
type FusionArtifactRef,
|
|
17
|
-
type FusionFailureArtifactClassification,
|
|
18
|
-
type FusionFailureAttemptMetadata,
|
|
19
|
-
type FusionFailureEvidenceArtifact,
|
|
20
|
-
type FusionFailureSummaryV1,
|
|
21
|
-
type FusionRunProgress,
|
|
22
|
-
type FusionAttemptArtifactRecord,
|
|
23
|
-
type FusionBudgetPlanV1,
|
|
24
|
-
type FusionCalibrationViolation,
|
|
25
|
-
type FusionCandidateId,
|
|
26
|
-
type FusionCandidateOutputRecovery,
|
|
27
|
-
type FusionCapability,
|
|
28
|
-
type FusionContextOmissionLedgerV2,
|
|
29
|
-
type FusionChildRunResult,
|
|
30
|
-
type FusionCommittedResultV1,
|
|
31
|
-
type FusionResultDetails,
|
|
32
|
-
type FusionModelConfigV1,
|
|
33
|
-
type FusionSource,
|
|
34
|
-
type FusionStage,
|
|
35
|
-
type FusionState,
|
|
36
|
-
type FusionTerminalState,
|
|
37
|
-
type FusionUsage,
|
|
38
|
-
type FusionWorkflowId,
|
|
39
|
-
type ResolvedFusionModels,
|
|
40
|
-
} from './types.js';
|
|
41
|
-
import { fusionWorkflowProfile, type FusionWorkflowProfile } from './workflows.js';
|
|
42
|
-
import {
|
|
43
|
-
FUSION_CANDIDATE_MAX_OUTPUT_BYTES,
|
|
44
|
-
fusionJsonRenderedTextBytes,
|
|
45
|
-
} from './output-contract.js';
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Run ids are prefixed by workflow so an artifact directory is self-describing.
|
|
49
|
-
* The prefix set is closed: an unknown prefix must fail rather than be accepted.
|
|
50
|
-
*/
|
|
51
|
-
const RUN_ID_PATTERN = /^(reason|investigate|research|validate)-[0-9a-f]{32}$/;
|
|
52
|
-
|
|
53
|
-
interface MutableFusionArtifactManifest {
|
|
54
|
-
schema_version: typeof FUSION_MANIFEST_SCHEMA_VERSION;
|
|
55
|
-
run_id: string;
|
|
56
|
-
workflow: FusionWorkflowId;
|
|
57
|
-
source: FusionSource;
|
|
58
|
-
state: FusionState;
|
|
59
|
-
created_at: string;
|
|
60
|
-
updated_at: string;
|
|
61
|
-
cwd: string;
|
|
62
|
-
config: FusionModelConfigV1;
|
|
63
|
-
models: {
|
|
64
|
-
candidates: [string, string, string];
|
|
65
|
-
evaluator: string;
|
|
66
|
-
merger: string;
|
|
67
|
-
thinking_level: string;
|
|
68
|
-
};
|
|
69
|
-
capabilities: {
|
|
70
|
-
candidate: FusionCapability;
|
|
71
|
-
evaluation: FusionCapability;
|
|
72
|
-
merge: FusionCapability;
|
|
73
|
-
};
|
|
74
|
-
context: {
|
|
75
|
-
kind: import('./types.js').FusionContextKind;
|
|
76
|
-
policy_id: string;
|
|
77
|
-
ledger_artifact?: string;
|
|
78
|
-
source_policy_artifact?: string;
|
|
79
|
-
};
|
|
80
|
-
tool_policy: {
|
|
81
|
-
candidate_tools: readonly string[];
|
|
82
|
-
evaluation_tools: readonly [];
|
|
83
|
-
merge_tools: readonly [];
|
|
84
|
-
};
|
|
85
|
-
usage: FusionUsage;
|
|
86
|
-
attempts: FusionAttemptArtifactRecord[];
|
|
87
|
-
artifacts: Record<string, FusionArtifactRef>;
|
|
88
|
-
anonymous_map?: Record<FusionCandidateId, 1 | 2 | 3>;
|
|
89
|
-
error?: string;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export interface CreateFusionArtifactStoreOptions {
|
|
93
|
-
cwd: string;
|
|
94
|
-
sessionId?: string | undefined;
|
|
95
|
-
runId?: string | undefined;
|
|
96
|
-
profile?: FusionWorkflowProfile | undefined;
|
|
97
|
-
source: FusionSource;
|
|
98
|
-
config: FusionModelConfigV1;
|
|
99
|
-
models: ResolvedFusionModels;
|
|
100
|
-
capabilities?: {
|
|
101
|
-
candidate: FusionCapability;
|
|
102
|
-
evaluation: FusionCapability;
|
|
103
|
-
merge: FusionCapability;
|
|
104
|
-
};
|
|
105
|
-
now?: () => Date;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export interface RecordFusionChildAttemptInput {
|
|
109
|
-
result: FusionChildRunResult;
|
|
110
|
-
systemPrompt: string;
|
|
111
|
-
prompt: string;
|
|
112
|
-
responseKind: 'md' | 'txt';
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export type RecordValidationCandidateContractEventInput =
|
|
116
|
-
| {
|
|
117
|
-
candidateId: FusionCandidateId;
|
|
118
|
-
slot: 1 | 2 | 3;
|
|
119
|
-
status: 'normalized';
|
|
120
|
-
detail: {
|
|
121
|
-
normalization: 'markdown_json_fence' | 'prose_then_markdown_json_fence';
|
|
122
|
-
original_sha256: string;
|
|
123
|
-
forwarded_sha256: string;
|
|
124
|
-
warning: string;
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
| {
|
|
128
|
-
candidateId: FusionCandidateId;
|
|
129
|
-
slot: 1 | 2 | 3;
|
|
130
|
-
status: 'dropped';
|
|
131
|
-
detail: {
|
|
132
|
-
response_sha256: string;
|
|
133
|
-
error: string;
|
|
134
|
-
warning: string;
|
|
135
|
-
};
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
export interface RecordFusionFailedAttemptInput {
|
|
139
|
-
stage: FusionStage;
|
|
140
|
-
slot?: 1 | 2 | 3;
|
|
141
|
-
attempt: number;
|
|
142
|
-
systemPrompt: string;
|
|
143
|
-
prompt: string;
|
|
144
|
-
events: Buffer;
|
|
145
|
-
partialResponse: Buffer;
|
|
146
|
-
stderr: Buffer;
|
|
147
|
-
error: string;
|
|
148
|
-
status: 'failed' | 'cancelled';
|
|
149
|
-
childCreated: boolean;
|
|
150
|
-
responseKind: 'md' | 'txt';
|
|
151
|
-
outputRecovery?: FusionCandidateOutputRecovery;
|
|
152
|
-
provider?: string;
|
|
153
|
-
model?: string;
|
|
154
|
-
qualifiedId?: string;
|
|
155
|
-
usage?: FusionUsage;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function makeRunId(profile: FusionWorkflowProfile): string {
|
|
159
|
-
return `${profile.runIdPrefix}${randomBytes(16).toString('hex')}`;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function modelsForManifest(models: ResolvedFusionModels): MutableFusionArtifactManifest['models'] {
|
|
163
|
-
const first = models.candidates[0].qualifiedId;
|
|
164
|
-
const second = models.candidates[1].qualifiedId;
|
|
165
|
-
const third = models.candidates[2].qualifiedId;
|
|
166
|
-
return {
|
|
167
|
-
candidates: [first, second, third],
|
|
168
|
-
evaluator: models.evaluator.qualifiedId,
|
|
169
|
-
merger: models.merger.qualifiedId,
|
|
170
|
-
thinking_level: models.evaluator.thinkingLevel,
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function terminalStates(): ReadonlySet<FusionState> {
|
|
175
|
-
return new Set<FusionState>(['completed', 'failed', 'cancelled']);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const TERMINAL_STATES = terminalStates();
|
|
179
|
-
|
|
180
|
-
const NEXT_STATES: Readonly<Record<FusionState, readonly FusionState[]>> = {
|
|
181
|
-
initializing: ['candidates_running', 'failed', 'cancelled'],
|
|
182
|
-
candidates_running: ['candidates_complete', 'failed', 'cancelled'],
|
|
183
|
-
candidates_complete: ['evaluating', 'failed', 'cancelled'],
|
|
184
|
-
evaluating: ['evaluation_complete', 'failed', 'cancelled'],
|
|
185
|
-
evaluation_complete: ['merging', 'failed', 'cancelled'],
|
|
186
|
-
merging: ['completed', 'failed', 'cancelled'],
|
|
187
|
-
completed: [],
|
|
188
|
-
failed: [],
|
|
189
|
-
cancelled: [],
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
function canTransition(from: FusionState, to: FusionState): boolean {
|
|
193
|
-
return NEXT_STATES[from].includes(to);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function pathInside(parent: string, child: string): boolean {
|
|
197
|
-
const rel = relative(parent, child);
|
|
198
|
-
return (
|
|
199
|
-
rel === '' || (!rel.startsWith('..') && !isAbsolute(rel) && !rel.split(sep).includes('..'))
|
|
200
|
-
);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function errorForArtifact(message: string): FusionError {
|
|
204
|
-
return new FusionError(message, { code: 'artifact_error', childCreated: false });
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
async function writePrivateFile(
|
|
208
|
-
absPath: string,
|
|
209
|
-
data: Buffer | string,
|
|
210
|
-
): Promise<FusionArtifactRef> {
|
|
211
|
-
const bytes = Buffer.isBuffer(data) ? data : Buffer.from(data, 'utf8');
|
|
212
|
-
await replaceFileDurable(absPath, data);
|
|
213
|
-
return { path: basename(absPath), byte_length: bytes.length, sha256: sha256Buffer(bytes) };
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
async function writeJsonAtomic(absPath: string, value: unknown): Promise<FusionArtifactRef> {
|
|
217
|
-
return writePrivateFile(absPath, `${JSON.stringify(value, null, 2)}\n`);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function publicManifest(manifest: MutableFusionArtifactManifest): FusionArtifactManifest {
|
|
221
|
-
const out: FusionArtifactManifest = {
|
|
222
|
-
schema_version: manifest.schema_version,
|
|
223
|
-
run_id: manifest.run_id,
|
|
224
|
-
workflow: manifest.workflow,
|
|
225
|
-
source: manifest.source,
|
|
226
|
-
state: manifest.state,
|
|
227
|
-
created_at: manifest.created_at,
|
|
228
|
-
updated_at: manifest.updated_at,
|
|
229
|
-
cwd: manifest.cwd,
|
|
230
|
-
config: manifest.config,
|
|
231
|
-
models: manifest.models,
|
|
232
|
-
capabilities: manifest.capabilities,
|
|
233
|
-
context: { ...manifest.context },
|
|
234
|
-
tool_policy: {
|
|
235
|
-
candidate_tools: [...manifest.tool_policy.candidate_tools],
|
|
236
|
-
evaluation_tools: [],
|
|
237
|
-
merge_tools: [],
|
|
238
|
-
},
|
|
239
|
-
usage: cloneFusionUsage(manifest.usage),
|
|
240
|
-
attempts: [...manifest.attempts],
|
|
241
|
-
artifacts: { ...manifest.artifacts },
|
|
242
|
-
};
|
|
243
|
-
if (manifest.anonymous_map !== undefined) out.anonymous_map = { ...manifest.anonymous_map };
|
|
244
|
-
if (manifest.error !== undefined) out.error = manifest.error;
|
|
245
|
-
return out;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function attemptPrefix(stage: FusionStage, slot: 1 | 2 | 3 | undefined, attempt: number): string {
|
|
249
|
-
if (stage === 'candidate') {
|
|
250
|
-
if (slot === undefined) throw errorForArtifact('candidate attempt requires slot');
|
|
251
|
-
return `candidate-${String(slot)}.attempt-${String(attempt)}`;
|
|
252
|
-
}
|
|
253
|
-
if (slot !== undefined)
|
|
254
|
-
throw errorForArtifact(`${stage} attempt must not include candidate slot`);
|
|
255
|
-
return `${stage === 'evaluation' ? 'evaluation' : 'merge'}.attempt-${String(attempt)}`;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function responseName(prefix: string, kind: 'md' | 'txt'): string {
|
|
259
|
-
return `${prefix}.response.${kind}`;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
function calibrationViolationName(prefix: string): string {
|
|
263
|
-
return `${prefix}.calibration-violation.json`;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function oversizedResponseName(prefix: string, kind: 'md' | 'txt'): string {
|
|
267
|
-
return `${prefix}.response.oversized.${kind}`;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
function validateOutputRecovery(
|
|
271
|
-
recovery: FusionCandidateOutputRecovery,
|
|
272
|
-
replacementText?: string,
|
|
273
|
-
): void {
|
|
274
|
-
if (recovery.limit_bytes !== FUSION_CANDIDATE_MAX_OUTPUT_BYTES) {
|
|
275
|
-
throw errorForArtifact('fusion output recovery limit mismatches the candidate contract');
|
|
276
|
-
}
|
|
277
|
-
const originalBytes = Buffer.from(recovery.original_text, 'utf8');
|
|
278
|
-
if (createHash('sha256').update(originalBytes).digest('hex') !== recovery.original_text_sha256) {
|
|
279
|
-
throw errorForArtifact('fusion output recovery original text hash mismatch');
|
|
280
|
-
}
|
|
281
|
-
if (
|
|
282
|
-
fusionJsonRenderedTextBytes(recovery.original_text) !== recovery.original_json_rendered_bytes
|
|
283
|
-
) {
|
|
284
|
-
throw errorForArtifact('fusion output recovery original JSON-rendered byte count mismatch');
|
|
285
|
-
}
|
|
286
|
-
if (recovery.original_json_rendered_bytes <= recovery.limit_bytes) {
|
|
287
|
-
throw errorForArtifact('fusion output recovery original did not exceed the candidate contract');
|
|
288
|
-
}
|
|
289
|
-
if (replacementText !== undefined) {
|
|
290
|
-
const replacementBytes = fusionJsonRenderedTextBytes(replacementText);
|
|
291
|
-
if (replacementBytes !== recovery.replacement_json_rendered_bytes) {
|
|
292
|
-
throw errorForArtifact(
|
|
293
|
-
'fusion output recovery replacement JSON-rendered byte count mismatch',
|
|
294
|
-
);
|
|
295
|
-
}
|
|
296
|
-
if (recovery.status === 'completed' && replacementBytes > recovery.limit_bytes) {
|
|
297
|
-
throw errorForArtifact('completed fusion output recovery replacement exceeds the contract');
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
function outputRecoveryRecord(
|
|
303
|
-
recovery: FusionCandidateOutputRecovery,
|
|
304
|
-
path: string,
|
|
305
|
-
): NonNullable<FusionAttemptArtifactRecord['output_recovery']> {
|
|
306
|
-
return {
|
|
307
|
-
kind: recovery.kind,
|
|
308
|
-
status: recovery.status,
|
|
309
|
-
limit_bytes: recovery.limit_bytes,
|
|
310
|
-
original_response_path: path,
|
|
311
|
-
original_record_index: recovery.original_record_index,
|
|
312
|
-
replacement_record_index: recovery.replacement_record_index,
|
|
313
|
-
original_json_rendered_bytes: recovery.original_json_rendered_bytes,
|
|
314
|
-
replacement_json_rendered_bytes: recovery.replacement_json_rendered_bytes,
|
|
315
|
-
original_text_sha256: recovery.original_text_sha256,
|
|
316
|
-
};
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
function artifactRefSha256Hex(value: string): string {
|
|
320
|
-
const hex = value.startsWith('sha256:') ? value.slice('sha256:'.length) : value;
|
|
321
|
-
if (!/^[0-9a-f]{64}$/u.test(hex)) {
|
|
322
|
-
throw errorForArtifact(`fusion artifact sha256 is not a lowercase hex digest: ${value}`);
|
|
323
|
-
}
|
|
324
|
-
return hex;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/** A manifest artifact reference is always one safe basename below its run directory. */
|
|
328
|
-
export function assertFusionArtifactBasename(name: string): string {
|
|
329
|
-
if (
|
|
330
|
-
name.length === 0 ||
|
|
331
|
-
name !== basename(name) ||
|
|
332
|
-
name.includes('/') ||
|
|
333
|
-
name.includes('\\') ||
|
|
334
|
-
name === '.' ||
|
|
335
|
-
name === '..' ||
|
|
336
|
-
Buffer.byteLength(name, 'utf8') > 255
|
|
337
|
-
) {
|
|
338
|
-
throw errorForArtifact(`invalid fusion artifact name: ${name}`);
|
|
339
|
-
}
|
|
340
|
-
return name;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
export const FUSION_FAILURE_SUMMARY_INLINE_MESSAGE_BYTES = 1024;
|
|
344
|
-
export const FUSION_FAILURE_SUMMARY_ATTEMPT_CAP = 12;
|
|
345
|
-
export const FUSION_FAILURE_SUMMARY_EVIDENCE_CAP = 24;
|
|
346
|
-
export const FUSION_FAILURE_SUMMARY_MAX_BYTES = 32 * 1024;
|
|
347
|
-
|
|
348
|
-
function compareArtifactText(left: string, right: string): number {
|
|
349
|
-
return left < right ? -1 : left > right ? 1 : 0;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
interface FusionProgressManifest {
|
|
353
|
-
state: FusionState;
|
|
354
|
-
artifacts: Readonly<Record<string, FusionArtifactRef>>;
|
|
355
|
-
attempts: readonly {
|
|
356
|
-
stage: FusionStage;
|
|
357
|
-
slot?: 1 | 2 | 3 | undefined;
|
|
358
|
-
status: 'completed' | 'failed' | 'cancelled';
|
|
359
|
-
child_created: boolean;
|
|
360
|
-
}[];
|
|
361
|
-
usage: FusionUsage;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
function failureStageProgress(
|
|
365
|
-
manifest: FusionProgressManifest,
|
|
366
|
-
stage: FusionStage,
|
|
367
|
-
): FusionRunProgress['candidates'] {
|
|
368
|
-
const attempts = manifest.attempts.filter((attempt) => attempt.stage === stage);
|
|
369
|
-
const created = attempts.filter((attempt) => attempt.child_created).length;
|
|
370
|
-
const completed = attempts.filter(
|
|
371
|
-
(attempt) => attempt.child_created && attempt.status === 'completed',
|
|
372
|
-
).length;
|
|
373
|
-
const failed = attempts.filter(
|
|
374
|
-
(attempt) => attempt.child_created && attempt.status === 'failed',
|
|
375
|
-
).length;
|
|
376
|
-
const cancelled = attempts.filter(
|
|
377
|
-
(attempt) => attempt.child_created && attempt.status === 'cancelled',
|
|
378
|
-
).length;
|
|
379
|
-
const completedByState =
|
|
380
|
-
stage === 'candidate'
|
|
381
|
-
? completed >= 3
|
|
382
|
-
: stage === 'evaluation'
|
|
383
|
-
? manifest.artifacts['evaluation.json'] !== undefined ||
|
|
384
|
-
manifest.state === 'evaluation_complete' ||
|
|
385
|
-
manifest.state === 'merging' ||
|
|
386
|
-
manifest.state === 'completed'
|
|
387
|
-
: manifest.artifacts['merged.md'] !== undefined || manifest.state === 'completed';
|
|
388
|
-
const progress: FusionRunProgress['candidates'] = {
|
|
389
|
-
status: completedByState ? 'completed' : created === 0 ? 'not_started' : 'incomplete',
|
|
390
|
-
attempts_recorded: attempts.length,
|
|
391
|
-
children_created: created,
|
|
392
|
-
children_completed: completed,
|
|
393
|
-
children_failed: failed,
|
|
394
|
-
children_cancelled: cancelled,
|
|
395
|
-
};
|
|
396
|
-
if (stage === 'candidate') {
|
|
397
|
-
const createdSlots = new Set(
|
|
398
|
-
attempts.flatMap((attempt) =>
|
|
399
|
-
attempt.child_created && attempt.slot !== undefined ? [attempt.slot] : [],
|
|
400
|
-
),
|
|
401
|
-
);
|
|
402
|
-
progress.not_started_slots = 3 - createdSlots.size;
|
|
403
|
-
}
|
|
404
|
-
return progress;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
/** Derive terminal progress solely from the durable manifest. */
|
|
408
|
-
export function buildFusionRunProgress(manifest: FusionProgressManifest): FusionRunProgress {
|
|
409
|
-
return {
|
|
410
|
-
manifest_state: manifest.state,
|
|
411
|
-
candidates: failureStageProgress(manifest, 'candidate'),
|
|
412
|
-
evaluation: failureStageProgress(manifest, 'evaluation'),
|
|
413
|
-
merge: failureStageProgress(manifest, 'merge'),
|
|
414
|
-
usage_so_far: cloneFusionUsage(manifest.usage),
|
|
415
|
-
};
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
function terminalMessageMetadata(message: string): FusionFailureSummaryV1['failure']['message'] {
|
|
419
|
-
const bytes = Buffer.from(message, 'utf8');
|
|
420
|
-
return {
|
|
421
|
-
byte_length: bytes.length,
|
|
422
|
-
sha256: sha256Buffer(bytes),
|
|
423
|
-
...(bytes.length <= FUSION_FAILURE_SUMMARY_INLINE_MESSAGE_BYTES
|
|
424
|
-
? { inline_message: message }
|
|
425
|
-
: { omission_reason: 'exceeds_inline_message_bytes_cap' as const }),
|
|
426
|
-
};
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
function failureArtifactClassification(
|
|
430
|
-
name: string,
|
|
431
|
-
ref: FusionArtifactRef,
|
|
432
|
-
manifest: FusionArtifactManifest,
|
|
433
|
-
): FusionFailureArtifactClassification {
|
|
434
|
-
for (const attempt of manifest.attempts) {
|
|
435
|
-
if (attempt.response_path === name) {
|
|
436
|
-
return ref.byte_length === 0 && attempt.status !== 'completed'
|
|
437
|
-
? 'empty_rejected_output'
|
|
438
|
-
: 'complete_stage_output';
|
|
439
|
-
}
|
|
440
|
-
if (attempt.partial_response_path === name) return 'partial_stage_output';
|
|
441
|
-
if (attempt.output_recovery?.original_response_path === name) return 'oversized_original';
|
|
442
|
-
}
|
|
443
|
-
return 'evidence_only';
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
function failureAttemptMetadata(manifest: FusionArtifactManifest): readonly FusionFailureAttemptMetadata[] {
|
|
447
|
-
return manifest.attempts
|
|
448
|
-
.map((attempt) => ({
|
|
449
|
-
stage: attempt.stage,
|
|
450
|
-
...(attempt.slot === undefined ? {} : { slot: attempt.slot }),
|
|
451
|
-
attempt: attempt.attempt,
|
|
452
|
-
status: attempt.status,
|
|
453
|
-
child_created: attempt.child_created,
|
|
454
|
-
}))
|
|
455
|
-
.sort((left, right) =>
|
|
456
|
-
compareArtifactText(left.stage, right.stage) ||
|
|
457
|
-
(left.slot ?? 0) - (right.slot ?? 0) ||
|
|
458
|
-
left.attempt - right.attempt,
|
|
459
|
-
);
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
export function buildFusionFailureSummary(input: {
|
|
463
|
-
manifest: FusionArtifactManifest;
|
|
464
|
-
terminalError: FusionError;
|
|
465
|
-
progress: FusionRunProgress;
|
|
466
|
-
terminalState: Exclude<FusionTerminalState, 'completed'>;
|
|
467
|
-
createdAt: string;
|
|
468
|
-
}): FusionFailureSummaryV1 {
|
|
469
|
-
if (
|
|
470
|
-
(input.terminalState !== 'failed' && input.terminalState !== 'cancelled') ||
|
|
471
|
-
input.manifest.state !== input.terminalState
|
|
472
|
-
) {
|
|
473
|
-
throw errorForArtifact('failure summary requires a matching failed/cancelled terminal manifest');
|
|
474
|
-
}
|
|
475
|
-
if (input.manifest.error !== input.terminalError.message) {
|
|
476
|
-
throw errorForArtifact('failure summary terminal error does not match the durable manifest');
|
|
477
|
-
}
|
|
478
|
-
if (canonicalJson(input.progress) !== canonicalJson(buildFusionRunProgress(input.manifest))) {
|
|
479
|
-
throw errorForArtifact('failure summary progress does not match the durable terminal manifest');
|
|
480
|
-
}
|
|
481
|
-
if (input.manifest.artifacts['failure-summary.json'] !== undefined) {
|
|
482
|
-
throw errorForArtifact('failure summary already exists in the terminal manifest');
|
|
483
|
-
}
|
|
484
|
-
const attempts = failureAttemptMetadata(input.manifest);
|
|
485
|
-
const evidence: FusionFailureEvidenceArtifact[] = Object.entries(input.manifest.artifacts)
|
|
486
|
-
.map(([name, ref]) => ({
|
|
487
|
-
name: assertFusionArtifactBasename(name),
|
|
488
|
-
classification: failureArtifactClassification(name, ref, input.manifest),
|
|
489
|
-
ref: { ...ref },
|
|
490
|
-
}))
|
|
491
|
-
.sort((left, right) => compareArtifactText(left.name, right.name));
|
|
492
|
-
return {
|
|
493
|
-
schema_version: FUSION_FAILURE_SUMMARY_SCHEMA_VERSION,
|
|
494
|
-
run_id: input.manifest.run_id,
|
|
495
|
-
workflow: input.manifest.workflow,
|
|
496
|
-
source: input.manifest.source,
|
|
497
|
-
terminal_state: input.terminalState,
|
|
498
|
-
created_at: input.createdAt,
|
|
499
|
-
answer: { present: false, reason: 'run_did_not_commit' },
|
|
500
|
-
failure: {
|
|
501
|
-
code: input.terminalError.code,
|
|
502
|
-
...(input.terminalError.stage === undefined ? {} : { stage: input.terminalError.stage }),
|
|
503
|
-
...(input.terminalError.slot === undefined ? {} : { slot: input.terminalError.slot }),
|
|
504
|
-
...(input.terminalError.attempt === undefined
|
|
505
|
-
? {}
|
|
506
|
-
: { attempt: input.terminalError.attempt }),
|
|
507
|
-
child_created: input.terminalError.childCreated,
|
|
508
|
-
message: terminalMessageMetadata(input.terminalError.message),
|
|
509
|
-
},
|
|
510
|
-
progress: input.progress,
|
|
511
|
-
usage_so_far: cloneFusionUsage(input.manifest.usage),
|
|
512
|
-
attempts: {
|
|
513
|
-
listed: attempts.filter((_attempt, index) => index < FUSION_FAILURE_SUMMARY_ATTEMPT_CAP),
|
|
514
|
-
omitted_count:
|
|
515
|
-
attempts.length - Math.min(attempts.length, FUSION_FAILURE_SUMMARY_ATTEMPT_CAP),
|
|
516
|
-
},
|
|
517
|
-
evidence_artifacts: {
|
|
518
|
-
listed: evidence.filter((_artifact, index) => index < FUSION_FAILURE_SUMMARY_EVIDENCE_CAP),
|
|
519
|
-
omitted_count:
|
|
520
|
-
evidence.length - Math.min(evidence.length, FUSION_FAILURE_SUMMARY_EVIDENCE_CAP),
|
|
521
|
-
},
|
|
522
|
-
remediation_ids: [
|
|
523
|
-
'inspect_manifest_bound_evidence',
|
|
524
|
-
'inspect_terminal_error',
|
|
525
|
-
'split_or_reduce_work',
|
|
526
|
-
'retry_same_route_after_operator_review',
|
|
527
|
-
],
|
|
528
|
-
};
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
export class FusionArtifactStore {
|
|
532
|
-
private readonly runDirAbs: string;
|
|
533
|
-
private readonly runDirDisplay: string;
|
|
534
|
-
private readonly now: () => Date;
|
|
535
|
-
private manifest: MutableFusionArtifactManifest;
|
|
536
|
-
private manifestWriteChain: Promise<void> = Promise.resolve();
|
|
537
|
-
|
|
538
|
-
private constructor(
|
|
539
|
-
runDirAbs: string,
|
|
540
|
-
runDirDisplay: string,
|
|
541
|
-
now: () => Date,
|
|
542
|
-
manifest: MutableFusionArtifactManifest,
|
|
543
|
-
) {
|
|
544
|
-
this.runDirAbs = runDirAbs;
|
|
545
|
-
this.runDirDisplay = runDirDisplay;
|
|
546
|
-
this.now = now;
|
|
547
|
-
this.manifest = manifest;
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
static async create(options: CreateFusionArtifactStoreOptions): Promise<FusionArtifactStore> {
|
|
551
|
-
const profile = fusionWorkflowProfile(options.profile?.id ?? 'reason');
|
|
552
|
-
const runId = options.runId ?? makeRunId(profile);
|
|
553
|
-
if (!RUN_ID_PATTERN.test(runId)) throw errorForArtifact(`invalid fusion run id: ${runId}`);
|
|
554
|
-
if (!runId.startsWith(profile.runIdPrefix)) {
|
|
555
|
-
throw errorForArtifact(
|
|
556
|
-
`fusion run id ${runId} does not carry the ${profile.id} workflow prefix ${profile.runIdPrefix}`,
|
|
557
|
-
);
|
|
558
|
-
}
|
|
559
|
-
const sessionSegment = sanitizePathSegment(
|
|
560
|
-
options.sessionId ?? `session-${String(process.pid)}`,
|
|
561
|
-
);
|
|
562
|
-
// USER DECISION (2026-08-21): durable fusion artifacts live under the
|
|
563
|
-
// workspace .unipi/ dir (our convention), not .pi/ and not tmp.
|
|
564
|
-
const sessionDirName = `${sessionSegment}-${String(process.pid)}`;
|
|
565
|
-
const runDirAbs = join(options.cwd, '.unipi', 'fusion', sessionDirName, runId);
|
|
566
|
-
const runDirDisplay = join('.unipi', 'fusion', sessionDirName, runId);
|
|
567
|
-
await mkdir(runDirAbs, { recursive: true, mode: 0o700 });
|
|
568
|
-
await chmod(runDirAbs, 0o700);
|
|
569
|
-
const timestamp = (options.now ?? (() => new Date()))().toISOString();
|
|
570
|
-
const manifest: MutableFusionArtifactManifest = {
|
|
571
|
-
schema_version: FUSION_MANIFEST_SCHEMA_VERSION,
|
|
572
|
-
run_id: runId,
|
|
573
|
-
workflow: profile.id,
|
|
574
|
-
source: options.source,
|
|
575
|
-
state: 'initializing',
|
|
576
|
-
created_at: timestamp,
|
|
577
|
-
updated_at: timestamp,
|
|
578
|
-
cwd: options.cwd,
|
|
579
|
-
config: options.config,
|
|
580
|
-
models: modelsForManifest(options.models),
|
|
581
|
-
capabilities: options.capabilities ?? {
|
|
582
|
-
candidate: 'reason',
|
|
583
|
-
evaluation: 'reason',
|
|
584
|
-
merge: 'reason',
|
|
585
|
-
},
|
|
586
|
-
context: {
|
|
587
|
-
kind: profile.contextKind,
|
|
588
|
-
policy_id:
|
|
589
|
-
profile.contextKind === 'session_projection'
|
|
590
|
-
? 'fusion-session-projection-v1'
|
|
591
|
-
: 'fusion-clean-task-v1',
|
|
592
|
-
},
|
|
593
|
-
tool_policy: {
|
|
594
|
-
candidate_tools: profile.candidateTools,
|
|
595
|
-
evaluation_tools: [],
|
|
596
|
-
merge_tools: [],
|
|
597
|
-
},
|
|
598
|
-
usage: cloneFusionUsage(EMPTY_FUSION_USAGE),
|
|
599
|
-
attempts: [],
|
|
600
|
-
artifacts: {},
|
|
601
|
-
};
|
|
602
|
-
const store = new FusionArtifactStore(
|
|
603
|
-
runDirAbs,
|
|
604
|
-
runDirDisplay,
|
|
605
|
-
options.now ?? (() => new Date()),
|
|
606
|
-
manifest,
|
|
607
|
-
);
|
|
608
|
-
await store.writeManifest();
|
|
609
|
-
return store;
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
get runId(): string {
|
|
613
|
-
return this.manifest.run_id;
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
get artifactDir(): string {
|
|
617
|
-
return this.runDirDisplay;
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
get artifactDirAbs(): string {
|
|
621
|
-
return this.runDirAbs;
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
childToolCallLogPath(stage: FusionStage, slot: 1 | 2 | 3 | undefined, attempt: number): string {
|
|
625
|
-
return this.artifactPath(`${attemptPrefix(stage, slot, attempt)}.tool-calls.jsonl`);
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
childOutputRecoveryPath(slot: 1 | 2 | 3, attempt: number, responseKind: 'md' | 'txt'): string {
|
|
629
|
-
return this.artifactPath(
|
|
630
|
-
oversizedResponseName(attemptPrefix('candidate', slot, attempt), responseKind),
|
|
631
|
-
);
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
snapshot(): FusionArtifactManifest {
|
|
635
|
-
return publicManifest(this.manifest);
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
async transition(to: FusionState): Promise<void> {
|
|
639
|
-
await this.updateManifest((manifest) => {
|
|
640
|
-
if (!canTransition(manifest.state, to)) {
|
|
641
|
-
throw new FusionError(`illegal fusion state transition ${manifest.state} -> ${to}`, {
|
|
642
|
-
code: 'state_transition_invalid',
|
|
643
|
-
childCreated: false,
|
|
644
|
-
});
|
|
645
|
-
}
|
|
646
|
-
if (
|
|
647
|
-
to === 'completed' &&
|
|
648
|
-
(manifest.artifacts['merged.md'] === undefined ||
|
|
649
|
-
manifest.artifacts['result.json'] === undefined)
|
|
650
|
-
) {
|
|
651
|
-
throw new FusionError(
|
|
652
|
-
'fusion cannot complete before merged.md and result.json are durable',
|
|
653
|
-
{
|
|
654
|
-
code: 'state_transition_invalid',
|
|
655
|
-
childCreated: false,
|
|
656
|
-
},
|
|
657
|
-
);
|
|
658
|
-
}
|
|
659
|
-
manifest.state = to;
|
|
660
|
-
});
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
async setAnonymousMap(map: Record<FusionCandidateId, 1 | 2 | 3>): Promise<void> {
|
|
664
|
-
await this.updateManifest((manifest) => {
|
|
665
|
-
manifest.anonymous_map = { ...map };
|
|
666
|
-
});
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
async setUsage(usage: FusionUsage): Promise<void> {
|
|
670
|
-
await this.updateManifest((manifest) => {
|
|
671
|
-
manifest.usage = cloneFusionUsage(usage);
|
|
672
|
-
});
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
async writeCanonicalInput(serialized: string): Promise<void> {
|
|
676
|
-
await this.writeArtifact('canonical-input.json', serialized);
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
/**
|
|
680
|
-
* Complete, source-ordered ledger of every omitted conversation event. Kept in
|
|
681
|
-
* a separate artifact so canonical input carries only compact run receipts
|
|
682
|
-
* while the full omission accounting stays locally auditable.
|
|
683
|
-
*/
|
|
684
|
-
async writeContextLedger(ledger: FusionContextOmissionLedgerV2): Promise<void> {
|
|
685
|
-
const ref = await this.writeArtifact('context-omission-ledger.json', canonicalJson(ledger));
|
|
686
|
-
await this.updateManifest((manifest) => {
|
|
687
|
-
manifest.context.ledger_artifact = ref.path;
|
|
688
|
-
});
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
async writeSourcePolicy(serialized: string): Promise<void> {
|
|
692
|
-
const ref = await this.writeArtifact('source-policy.private.json', serialized);
|
|
693
|
-
await this.updateManifest((manifest) => {
|
|
694
|
-
manifest.context.source_policy_artifact = ref.path;
|
|
695
|
-
});
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
sourcePolicyLaunchReference(): { path: string; sha256: string } {
|
|
699
|
-
const ref = this.manifest.artifacts['source-policy.private.json'];
|
|
700
|
-
if (ref === undefined) throw errorForArtifact('research source policy has not been written');
|
|
701
|
-
return { path: this.artifactPath(ref.path), sha256: artifactRefSha256Hex(ref.sha256) };
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
/** Route capacities and the pre-candidate whole-workflow feasibility decision. */
|
|
705
|
-
async writeBudgetPlan(plan: FusionBudgetPlanV1): Promise<void> {
|
|
706
|
-
await this.writeArtifact('budget-plan.json', canonicalJson(plan));
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
async writeBlindCandidates(serialized: string): Promise<void> {
|
|
710
|
-
await this.writeArtifact('blind-candidates.json', serialized);
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
async writeEvaluationJson(value: unknown): Promise<void> {
|
|
714
|
-
await this.writeArtifact('evaluation.json', canonicalJson(value));
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
async writeMerged(text: string): Promise<FusionArtifactRef> {
|
|
718
|
-
return this.writeArtifact('merged.md', text);
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
async writeCommittedResult(
|
|
722
|
-
merged: FusionArtifactRef,
|
|
723
|
-
details: FusionResultDetails,
|
|
724
|
-
): Promise<FusionArtifactRef> {
|
|
725
|
-
const value: FusionCommittedResultV1 = {
|
|
726
|
-
schema_version: FUSION_COMMITTED_RESULT_SCHEMA_VERSION,
|
|
727
|
-
run_id: this.runId,
|
|
728
|
-
merged,
|
|
729
|
-
details,
|
|
730
|
-
};
|
|
731
|
-
return this.writeArtifact('result.json', `${canonicalJson(value)}\n`);
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
async writeError(state: Exclude<FusionTerminalState, 'completed'>, error: string): Promise<void> {
|
|
735
|
-
await this.writeArtifact('error.json', `${JSON.stringify({ state, error }, null, 2)}\n`);
|
|
736
|
-
await this.updateManifest((manifest) => {
|
|
737
|
-
if (!TERMINAL_STATES.has(state)) throw errorForArtifact(`invalid terminal state ${state}`);
|
|
738
|
-
if (!canTransition(manifest.state, state)) {
|
|
739
|
-
throw new FusionError(`illegal fusion state transition ${manifest.state} -> ${state}`, {
|
|
740
|
-
code: 'state_transition_invalid',
|
|
741
|
-
childCreated: false,
|
|
742
|
-
});
|
|
743
|
-
}
|
|
744
|
-
manifest.state = state;
|
|
745
|
-
manifest.error = error;
|
|
746
|
-
});
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
/**
|
|
750
|
-
* Writes the terminal evidence summary exactly once after writeError has made
|
|
751
|
-
* the manifest terminal. The summary deliberately contains refs only, never
|
|
752
|
-
* stage-output bodies.
|
|
753
|
-
*/
|
|
754
|
-
async writeFailureSummary(summary: FusionFailureSummaryV1): Promise<FusionArtifactRef> {
|
|
755
|
-
if (
|
|
756
|
-
this.manifest.state !== 'failed' &&
|
|
757
|
-
this.manifest.state !== 'cancelled'
|
|
758
|
-
) {
|
|
759
|
-
throw errorForArtifact('failure summary requires a failed/cancelled terminal manifest');
|
|
760
|
-
}
|
|
761
|
-
if (summary.terminal_state !== this.manifest.state) {
|
|
762
|
-
throw errorForArtifact('failure summary terminal state does not match the manifest');
|
|
763
|
-
}
|
|
764
|
-
if (
|
|
765
|
-
summary.run_id !== this.manifest.run_id ||
|
|
766
|
-
summary.workflow !== this.manifest.workflow ||
|
|
767
|
-
summary.source !== this.manifest.source
|
|
768
|
-
) {
|
|
769
|
-
throw errorForArtifact('failure summary identity does not match the terminal manifest');
|
|
770
|
-
}
|
|
771
|
-
if (
|
|
772
|
-
summary.answer?.present !== false ||
|
|
773
|
-
summary.answer.reason !== 'run_did_not_commit'
|
|
774
|
-
) {
|
|
775
|
-
throw errorForArtifact('failure summary must assert that no answer was committed');
|
|
776
|
-
}
|
|
777
|
-
if (this.manifest.error === undefined || this.manifest.artifacts['error.json'] === undefined) {
|
|
778
|
-
throw errorForArtifact('failure summary requires durable terminal error evidence');
|
|
779
|
-
}
|
|
780
|
-
if (
|
|
781
|
-
canonicalJson(summary.failure.message) !==
|
|
782
|
-
canonicalJson(terminalMessageMetadata(this.manifest.error))
|
|
783
|
-
) {
|
|
784
|
-
throw errorForArtifact('failure summary terminal error metadata does not match the manifest');
|
|
785
|
-
}
|
|
786
|
-
if (canonicalJson(summary.progress) !== canonicalJson(buildFusionRunProgress(this.snapshot()))) {
|
|
787
|
-
throw errorForArtifact('failure summary progress does not match the terminal manifest');
|
|
788
|
-
}
|
|
789
|
-
if (canonicalJson(summary.usage_so_far) !== canonicalJson(this.manifest.usage)) {
|
|
790
|
-
throw errorForArtifact('failure summary usage does not match the terminal manifest');
|
|
791
|
-
}
|
|
792
|
-
if (this.manifest.artifacts['failure-summary.json'] !== undefined) {
|
|
793
|
-
throw errorForArtifact('failure summary is already bound in the manifest');
|
|
794
|
-
}
|
|
795
|
-
const bytes = Buffer.from(`${canonicalJson(summary)}\n`, 'utf8');
|
|
796
|
-
if (bytes.length > FUSION_FAILURE_SUMMARY_MAX_BYTES) {
|
|
797
|
-
throw errorForArtifact('failure summary exceeds its bounded diagnostics artifact limit');
|
|
798
|
-
}
|
|
799
|
-
return this.writeArtifact('failure-summary.json', bytes);
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
async recordChildAttempt(input: RecordFusionChildAttemptInput): Promise<void> {
|
|
803
|
-
const prefix = attemptPrefix(input.result.stage, input.result.slot, input.result.attempt);
|
|
804
|
-
await this.writeArtifact(`${prefix}.system-prompt.txt`, input.systemPrompt);
|
|
805
|
-
const promptRef = await this.writeArtifact(`${prefix}.prompt.txt`, input.prompt);
|
|
806
|
-
const eventsRef = await this.writeArtifact(`${prefix}.events.jsonl`, input.result.events);
|
|
807
|
-
const stderrRef = await this.writeArtifact(`${prefix}.stderr.txt`, input.result.stderr);
|
|
808
|
-
const responseRef = await this.writeArtifact(
|
|
809
|
-
responseName(prefix, input.responseKind),
|
|
810
|
-
input.result.text,
|
|
811
|
-
);
|
|
812
|
-
const toolCallsRef =
|
|
813
|
-
input.result.toolCallTrace === undefined
|
|
814
|
-
? undefined
|
|
815
|
-
: await this.writeArtifact(`${prefix}.tool-calls.jsonl`, input.result.toolCallTrace.bytes);
|
|
816
|
-
if (input.result.outputRecovery !== undefined) {
|
|
817
|
-
validateOutputRecovery(input.result.outputRecovery, input.result.text);
|
|
818
|
-
}
|
|
819
|
-
const outputRecoveryRef =
|
|
820
|
-
input.result.outputRecovery === undefined
|
|
821
|
-
? undefined
|
|
822
|
-
: await this.writeArtifact(
|
|
823
|
-
oversizedResponseName(prefix, input.responseKind),
|
|
824
|
-
input.result.outputRecovery.original_text,
|
|
825
|
-
);
|
|
826
|
-
await this.updateManifest((manifest) => {
|
|
827
|
-
const record: FusionAttemptArtifactRecord = {
|
|
828
|
-
stage: input.result.stage,
|
|
829
|
-
attempt: input.result.attempt,
|
|
830
|
-
status: 'completed',
|
|
831
|
-
child_created: true,
|
|
832
|
-
prompt_path: promptRef.path,
|
|
833
|
-
events_path: eventsRef.path,
|
|
834
|
-
stderr_path: stderrRef.path,
|
|
835
|
-
response_path: responseRef.path,
|
|
836
|
-
provider: input.result.provider,
|
|
837
|
-
model: input.result.model,
|
|
838
|
-
qualifiedId: input.result.qualifiedId,
|
|
839
|
-
usage: cloneFusionUsage(input.result.usage),
|
|
840
|
-
};
|
|
841
|
-
if (toolCallsRef !== undefined && input.result.toolCallTrace !== undefined) {
|
|
842
|
-
record.tool_calls_path = toolCallsRef.path;
|
|
843
|
-
record.tool_calls = { ...input.result.toolCallTrace.summary };
|
|
844
|
-
}
|
|
845
|
-
if (outputRecoveryRef !== undefined && input.result.outputRecovery !== undefined) {
|
|
846
|
-
record.output_recovery = outputRecoveryRecord(
|
|
847
|
-
input.result.outputRecovery,
|
|
848
|
-
outputRecoveryRef.path,
|
|
849
|
-
);
|
|
850
|
-
}
|
|
851
|
-
if (input.result.slot !== undefined) record.slot = input.result.slot;
|
|
852
|
-
manifest.attempts.push(record);
|
|
853
|
-
});
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
async recordCalibrationViolation(input: {
|
|
857
|
-
stage: FusionStage;
|
|
858
|
-
slot?: 1 | 2 | 3;
|
|
859
|
-
attempt: number;
|
|
860
|
-
violation: FusionCalibrationViolation;
|
|
861
|
-
}): Promise<FusionArtifactRef> {
|
|
862
|
-
const prefix = attemptPrefix(input.stage, input.slot, input.attempt);
|
|
863
|
-
return this.writeArtifact(
|
|
864
|
-
calibrationViolationName(prefix),
|
|
865
|
-
`${canonicalJson(input.violation)}\n`,
|
|
866
|
-
);
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
async recordValidationCandidateContractEvent(
|
|
870
|
-
input: RecordValidationCandidateContractEventInput,
|
|
871
|
-
): Promise<FusionArtifactRef> {
|
|
872
|
-
const name = `candidate-${String(input.slot)}.output-contract-${input.status}.json`;
|
|
873
|
-
return this.writeArtifact(
|
|
874
|
-
name,
|
|
875
|
-
`${canonicalJson({
|
|
876
|
-
schema_version: FUSION_VALIDATE_CANDIDATE_CONTRACT_EVENT_SCHEMA_VERSION,
|
|
877
|
-
...input.detail,
|
|
878
|
-
candidate_id: input.candidateId,
|
|
879
|
-
slot: input.slot,
|
|
880
|
-
status: input.status,
|
|
881
|
-
})}\n`,
|
|
882
|
-
);
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
async recordFailedAttempt(input: RecordFusionFailedAttemptInput): Promise<void> {
|
|
886
|
-
const prefix = attemptPrefix(input.stage, input.slot, input.attempt);
|
|
887
|
-
await this.writeArtifact(`${prefix}.system-prompt.txt`, input.systemPrompt);
|
|
888
|
-
const promptRef = await this.writeArtifact(`${prefix}.prompt.txt`, input.prompt);
|
|
889
|
-
const eventsRef = await this.writeArtifact(`${prefix}.events.jsonl`, input.events);
|
|
890
|
-
const stderrRef = await this.writeArtifact(`${prefix}.stderr.txt`, input.stderr);
|
|
891
|
-
const responseRef = await this.writeArtifact(responseName(prefix, input.responseKind), '');
|
|
892
|
-
const partialResponseRef =
|
|
893
|
-
input.partialResponse.length === 0
|
|
894
|
-
? undefined
|
|
895
|
-
: await this.writeArtifact(
|
|
896
|
-
`${prefix}.response.partial.${input.responseKind}`,
|
|
897
|
-
input.partialResponse,
|
|
898
|
-
);
|
|
899
|
-
if (input.outputRecovery !== undefined) validateOutputRecovery(input.outputRecovery);
|
|
900
|
-
const outputRecoveryRef =
|
|
901
|
-
input.outputRecovery === undefined
|
|
902
|
-
? undefined
|
|
903
|
-
: await this.writeArtifact(
|
|
904
|
-
oversizedResponseName(prefix, input.responseKind),
|
|
905
|
-
input.outputRecovery.original_text,
|
|
906
|
-
);
|
|
907
|
-
await this.updateManifest((manifest) => {
|
|
908
|
-
const record: FusionAttemptArtifactRecord = {
|
|
909
|
-
stage: input.stage,
|
|
910
|
-
attempt: input.attempt,
|
|
911
|
-
status: input.status,
|
|
912
|
-
child_created: input.childCreated,
|
|
913
|
-
prompt_path: promptRef.path,
|
|
914
|
-
events_path: eventsRef.path,
|
|
915
|
-
stderr_path: stderrRef.path,
|
|
916
|
-
response_path: responseRef.path,
|
|
917
|
-
error: input.error,
|
|
918
|
-
};
|
|
919
|
-
if (partialResponseRef !== undefined) record.partial_response_path = partialResponseRef.path;
|
|
920
|
-
if (outputRecoveryRef !== undefined && input.outputRecovery !== undefined) {
|
|
921
|
-
record.output_recovery = outputRecoveryRecord(input.outputRecovery, outputRecoveryRef.path);
|
|
922
|
-
}
|
|
923
|
-
if (input.provider !== undefined) record.provider = input.provider;
|
|
924
|
-
if (input.model !== undefined) record.model = input.model;
|
|
925
|
-
if (input.qualifiedId !== undefined) record.qualifiedId = input.qualifiedId;
|
|
926
|
-
if (input.usage !== undefined) record.usage = cloneFusionUsage(input.usage);
|
|
927
|
-
if (input.slot !== undefined) record.slot = input.slot;
|
|
928
|
-
manifest.attempts.push(record);
|
|
929
|
-
});
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
/** Writes a durable artifact then binds its exact bytes in the manifest. */
|
|
933
|
-
private async writeArtifact(name: string, data: Buffer | string): Promise<FusionArtifactRef> {
|
|
934
|
-
const absPath = this.artifactPath(name);
|
|
935
|
-
const ref = await writePrivateFile(absPath, data);
|
|
936
|
-
await this.updateManifest((manifest) => {
|
|
937
|
-
manifest.artifacts[name] = ref;
|
|
938
|
-
});
|
|
939
|
-
return ref;
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
private artifactPath(name: string): string {
|
|
943
|
-
assertFusionArtifactBasename(name);
|
|
944
|
-
const absPath = join(this.runDirAbs, name);
|
|
945
|
-
if (!pathInside(this.runDirAbs, absPath)) {
|
|
946
|
-
throw errorForArtifact(`fusion artifact path escapes run directory: ${name}`);
|
|
947
|
-
}
|
|
948
|
-
return absPath;
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
private async writeManifest(): Promise<void> {
|
|
952
|
-
await writeJsonAtomic(join(this.runDirAbs, 'manifest.json'), publicManifest(this.manifest));
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
private async updateManifest(
|
|
956
|
-
mutator: (manifest: MutableFusionArtifactManifest) => void,
|
|
957
|
-
): Promise<void> {
|
|
958
|
-
const write = async () => {
|
|
959
|
-
mutator(this.manifest);
|
|
960
|
-
this.manifest.updated_at = this.now().toISOString();
|
|
961
|
-
await this.writeManifest();
|
|
962
|
-
};
|
|
963
|
-
const next = this.manifestWriteChain.then(write, write);
|
|
964
|
-
this.manifestWriteChain = next.catch(() => undefined);
|
|
965
|
-
await next;
|
|
966
|
-
}
|
|
967
|
-
}
|