pi-background-tasks 1.0.7 → 2.1.1
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 +8 -8
- package/TESTING.md +3 -3
- package/TEST_PLAN.md +6 -6
- package/docs/INDEX.md +25 -25
- package/docs/choose-a-workflow.md +4 -4
- package/docs/commands/bg-clear.md +1 -1
- package/docs/commands/bg-update.md +1 -1
- package/docs/commands/bg.md +1 -1
- package/docs/commands/fusion-models.md +1 -1
- package/docs/commands/fusion.md +5 -8
- package/docs/commands/jobs.md +1 -1
- package/docs/commands/kill.md +1 -1
- package/docs/commands/logs.md +1 -1
- package/docs/commands/task-manager.md +2 -2
- package/docs/concepts/completion-delivery.md +1 -0
- package/docs/getting-started.md +1 -1
- package/docs/manifest.json +69 -50
- package/docs/operations/configuration.md +5 -3
- package/docs/read-before-edit.md +3 -0
- package/docs/reference/runtime-contracts.md +87 -82
- package/docs/reference/shortcuts-and-dock.md +2 -2
- package/docs/subsystems/background-task-runtime.md +7 -1
- package/docs/subsystems/docs-freshness-gate.md +5 -5
- package/docs/subsystems/fusion.md +22 -15
- package/docs/subsystems/host-ui-and-telemetry.md +1 -1
- package/docs/tools/bg_delegate.md +1 -1
- package/docs/tools/bg_kill.md +1 -1
- package/docs/tools/bg_logs.md +1 -1
- package/docs/tools/bg_result.md +14 -10
- package/docs/tools/bg_run.md +1 -1
- package/docs/tools/bg_run_pi_attested.md +1 -1
- package/docs/tools/bg_status.md +1 -1
- package/docs/tools/fusion_investigate.md +6 -4
- package/docs/tools/fusion_reason.md +5 -5
- package/docs/tools/fusion_research.md +6 -2
- package/docs/tools/fusion_validate.md +5 -3
- package/package.json +1 -1
- package/src/core/common.ts +50 -2
- package/src/core/fusion/anthropic-attribution.ts +1930 -0
- package/src/core/fusion/artifacts.ts +168 -21
- package/src/core/fusion/budget.ts +23 -23
- package/src/core/fusion/child-protocol.ts +115 -10
- package/src/core/fusion/claude-cache.ts +21 -0
- package/src/core/fusion/config.ts +10 -2
- package/src/core/fusion/orchestrator.ts +281 -77
- package/src/core/fusion/output-contract.ts +34 -0
- package/src/core/fusion/pi-child.ts +420 -12
- package/src/core/fusion/prompts.ts +11 -1
- package/src/core/fusion/result-package.ts +412 -0
- package/src/core/fusion/types.ts +67 -0
- package/src/core/registry.ts +187 -20
- package/src/delegate-extension.ts +130 -24
- package/src/extension.ts +17 -6
- package/src/fusion-child-extension.ts +117 -2
- package/src/fusion-extension.ts +308 -154
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { canonicalJson } from '../attested-pi-run.js';
|
|
2
|
+
import { FUSION_CANDIDATE_OUTPUT_CONTRACT_INSTRUCTION } from './output-contract.js';
|
|
2
3
|
import {
|
|
3
4
|
FUSION_EVALUATION_SCHEMA_VERSION,
|
|
4
5
|
FUSION_VALIDATE_CAPABILITY,
|
|
@@ -33,6 +34,8 @@ ${FUSION_CANONICAL_INPUT_GUIDE}
|
|
|
33
34
|
|
|
34
35
|
Produce the strongest direct answer you can for the request using that context.
|
|
35
36
|
|
|
37
|
+
${FUSION_CANDIDATE_OUTPUT_CONTRACT_INSTRUCTION}
|
|
38
|
+
|
|
36
39
|
Do not invent process metadata. Do not mention provider names, model names, slots, or hidden workflow details. Do not specialize the answer; each child receives the same instruction. Output only the answer text.`;
|
|
37
40
|
|
|
38
41
|
export const FUSION_INSPECT_CANONICAL_INPUT_GUIDE = `The JSON input contains only workflow, cwd, request, and clean-task context. context.kind is "clean_task".
|
|
@@ -47,6 +50,8 @@ ${FUSION_INSPECT_CANONICAL_INPUT_GUIDE}
|
|
|
47
50
|
|
|
48
51
|
Produce the strongest direct answer you can for the request using that context.
|
|
49
52
|
|
|
53
|
+
${FUSION_CANDIDATE_OUTPUT_CONTRACT_INSTRUCTION}
|
|
54
|
+
|
|
50
55
|
Do not invent process metadata. Do not mention provider names, model names, slots, or hidden workflow details. Do not specialize the answer; each child receives the same instruction. Output only the answer text.`;
|
|
51
56
|
|
|
52
57
|
export const FUSION_RESEARCH_CANONICAL_INPUT_GUIDE = `The JSON input contains only workflow, cwd, request, and clean-task context. Research uses context.kind "clean_task" with declared_sources: the only initial public URLs fusion_web_fetch may initiate. Redirects are followed only by the fetcher after public-address checks; fusion_web_fetch is targeted URL fetch, not search.
|
|
@@ -61,6 +66,8 @@ ${FUSION_RESEARCH_CANONICAL_INPUT_GUIDE}
|
|
|
61
66
|
|
|
62
67
|
Produce the strongest direct answer you can for the request using that context.
|
|
63
68
|
|
|
69
|
+
${FUSION_CANDIDATE_OUTPUT_CONTRACT_INSTRUCTION}
|
|
70
|
+
|
|
64
71
|
Do not invent process metadata. Do not mention provider names, model names, slots, or hidden workflow details. Do not specialize the answer; each child receives the same instruction. Output only the answer text.`;
|
|
65
72
|
|
|
66
73
|
export function fusionCandidateSystemPrompt(capability: FusionCapability): string {
|
|
@@ -198,6 +205,8 @@ Stay in scope. Validate what the request names. Do not propose unrelated refacto
|
|
|
198
205
|
|
|
199
206
|
Close with what you verified and what you could not cover.
|
|
200
207
|
|
|
208
|
+
${FUSION_CANDIDATE_OUTPUT_CONTRACT_INSTRUCTION}
|
|
209
|
+
|
|
201
210
|
Do not invent process metadata. Do not mention provider names, model names, slots, or hidden workflow details. Do not specialize the report; each child receives the same instruction. Output only the required JSON.`;
|
|
202
211
|
|
|
203
212
|
/**
|
|
@@ -301,7 +310,8 @@ export function buildBlindEvaluationInput(
|
|
|
301
310
|
canonical_input: canonicalInput,
|
|
302
311
|
candidates,
|
|
303
312
|
};
|
|
304
|
-
if (validationSourceFindings !== undefined)
|
|
313
|
+
if (validationSourceFindings !== undefined)
|
|
314
|
+
input.validation_source_findings = validationSourceFindings;
|
|
305
315
|
return input;
|
|
306
316
|
}
|
|
307
317
|
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { sha256Buffer } from '../attested-pi-run.js';
|
|
4
|
+
import { parseJsonText, type JsonObject } from '../common.js';
|
|
5
|
+
import {
|
|
6
|
+
FUSION_COMMITTED_RESULT_SCHEMA_VERSION,
|
|
7
|
+
FUSION_MANIFEST_SCHEMA_VERSION,
|
|
8
|
+
FUSION_RESULT_SCHEMA_VERSION,
|
|
9
|
+
FusionError,
|
|
10
|
+
type FusionArtifactRef,
|
|
11
|
+
type FusionResultDetails,
|
|
12
|
+
type FusionRunResult,
|
|
13
|
+
type FusionUsage,
|
|
14
|
+
type FusionWorkflowId,
|
|
15
|
+
} from './types.js';
|
|
16
|
+
|
|
17
|
+
const SHA256_PATTERN = /^sha256:[0-9a-f]{64}$/u;
|
|
18
|
+
|
|
19
|
+
function isRecord(value: unknown): value is JsonObject {
|
|
20
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function fail(message: string, artifactDir: string): never {
|
|
24
|
+
throw new FusionError(`fusion committed result invalid: ${message}`, {
|
|
25
|
+
code: 'artifact_error',
|
|
26
|
+
childCreated: true,
|
|
27
|
+
artifactDir,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function assertOnlyKeys(
|
|
32
|
+
value: JsonObject,
|
|
33
|
+
allowed: readonly string[],
|
|
34
|
+
label: string,
|
|
35
|
+
artifactDir: string,
|
|
36
|
+
): void {
|
|
37
|
+
const unexpected = Object.keys(value).filter((key) => !allowed.includes(key));
|
|
38
|
+
if (unexpected.length > 0)
|
|
39
|
+
fail(`${label} contains unexpected keys: ${unexpected.join(', ')}`, artifactDir);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function artifactRef(value: unknown, label: string, artifactDir: string): FusionArtifactRef {
|
|
43
|
+
if (!isRecord(value)) fail(`${label} must be an object`, artifactDir);
|
|
44
|
+
assertOnlyKeys(value, ['path', 'byte_length', 'sha256'], label, artifactDir);
|
|
45
|
+
const path = value['path'];
|
|
46
|
+
const byteLength = value['byte_length'];
|
|
47
|
+
const sha256 = value['sha256'];
|
|
48
|
+
if (typeof path !== 'string' || path.length === 0 || path.includes('/') || path.includes('\\')) {
|
|
49
|
+
fail(`${label}.path is invalid`, artifactDir);
|
|
50
|
+
}
|
|
51
|
+
if (!Number.isSafeInteger(byteLength) || Number(byteLength) < 0) {
|
|
52
|
+
fail(`${label}.byte_length is invalid`, artifactDir);
|
|
53
|
+
}
|
|
54
|
+
if (typeof sha256 !== 'string' || !SHA256_PATTERN.test(sha256)) {
|
|
55
|
+
fail(`${label}.sha256 is invalid`, artifactDir);
|
|
56
|
+
}
|
|
57
|
+
return { path, byte_length: Number(byteLength), sha256 };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function usage(value: unknown, artifactDir: string): FusionUsage {
|
|
61
|
+
if (!isRecord(value)) fail('details.usage must be an object', artifactDir);
|
|
62
|
+
assertOnlyKeys(
|
|
63
|
+
value,
|
|
64
|
+
[
|
|
65
|
+
'input',
|
|
66
|
+
'output',
|
|
67
|
+
'cacheRead',
|
|
68
|
+
'cacheWrite',
|
|
69
|
+
'cacheWrite1h',
|
|
70
|
+
'reasoning',
|
|
71
|
+
'totalTokens',
|
|
72
|
+
'cost',
|
|
73
|
+
],
|
|
74
|
+
'details.usage',
|
|
75
|
+
artifactDir,
|
|
76
|
+
);
|
|
77
|
+
const cost = value['cost'];
|
|
78
|
+
if (!isRecord(cost)) fail('details.usage.cost must be an object', artifactDir);
|
|
79
|
+
assertOnlyKeys(
|
|
80
|
+
cost,
|
|
81
|
+
['input', 'output', 'cacheRead', 'cacheWrite', 'total'],
|
|
82
|
+
'details.usage.cost',
|
|
83
|
+
artifactDir,
|
|
84
|
+
);
|
|
85
|
+
const finiteNonnegative = (entry: unknown, label: string): number => {
|
|
86
|
+
if (typeof entry !== 'number' || !Number.isFinite(entry) || entry < 0)
|
|
87
|
+
fail(`${label} is invalid`, artifactDir);
|
|
88
|
+
return entry;
|
|
89
|
+
};
|
|
90
|
+
const output = finiteNonnegative(value['output'], 'details.usage.output');
|
|
91
|
+
const cacheWrite = finiteNonnegative(value['cacheWrite'], 'details.usage.cacheWrite');
|
|
92
|
+
const cacheWrite1h =
|
|
93
|
+
value['cacheWrite1h'] === undefined
|
|
94
|
+
? undefined
|
|
95
|
+
: finiteNonnegative(value['cacheWrite1h'], 'details.usage.cacheWrite1h');
|
|
96
|
+
const reasoning =
|
|
97
|
+
value['reasoning'] === undefined
|
|
98
|
+
? undefined
|
|
99
|
+
: finiteNonnegative(value['reasoning'], 'details.usage.reasoning');
|
|
100
|
+
if (cacheWrite1h !== undefined && cacheWrite1h > cacheWrite) {
|
|
101
|
+
fail('details.usage.cacheWrite1h must not exceed cacheWrite', artifactDir);
|
|
102
|
+
}
|
|
103
|
+
if (reasoning !== undefined && reasoning > output) {
|
|
104
|
+
fail('details.usage.reasoning must not exceed output', artifactDir);
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
input: finiteNonnegative(value['input'], 'details.usage.input'),
|
|
108
|
+
output,
|
|
109
|
+
cacheRead: finiteNonnegative(value['cacheRead'], 'details.usage.cacheRead'),
|
|
110
|
+
cacheWrite,
|
|
111
|
+
...(cacheWrite1h === undefined ? {} : { cacheWrite1h }),
|
|
112
|
+
...(reasoning === undefined ? {} : { reasoning }),
|
|
113
|
+
totalTokens: finiteNonnegative(value['totalTokens'], 'details.usage.totalTokens'),
|
|
114
|
+
cost: {
|
|
115
|
+
input: finiteNonnegative(cost['input'], 'details.usage.cost.input'),
|
|
116
|
+
output: finiteNonnegative(cost['output'], 'details.usage.cost.output'),
|
|
117
|
+
cacheRead: finiteNonnegative(cost['cacheRead'], 'details.usage.cost.cacheRead'),
|
|
118
|
+
cacheWrite: finiteNonnegative(cost['cacheWrite'], 'details.usage.cost.cacheWrite'),
|
|
119
|
+
total: finiteNonnegative(cost['total'], 'details.usage.cost.total'),
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function resultDetails(
|
|
125
|
+
value: unknown,
|
|
126
|
+
expected: { runId: string; workflow: FusionWorkflowId; artifactDir: string },
|
|
127
|
+
): FusionResultDetails {
|
|
128
|
+
if (!isRecord(value)) fail('details must be an object', expected.artifactDir);
|
|
129
|
+
assertOnlyKeys(
|
|
130
|
+
value,
|
|
131
|
+
[
|
|
132
|
+
'schema_version',
|
|
133
|
+
'run_id',
|
|
134
|
+
'workflow',
|
|
135
|
+
'source',
|
|
136
|
+
'status',
|
|
137
|
+
'context',
|
|
138
|
+
'tool_policy',
|
|
139
|
+
'artifact_dir',
|
|
140
|
+
'models',
|
|
141
|
+
'evaluator_attempts',
|
|
142
|
+
'usage',
|
|
143
|
+
'budget',
|
|
144
|
+
],
|
|
145
|
+
'details',
|
|
146
|
+
expected.artifactDir,
|
|
147
|
+
);
|
|
148
|
+
if (value['schema_version'] !== FUSION_RESULT_SCHEMA_VERSION)
|
|
149
|
+
fail('details schema version mismatch', expected.artifactDir);
|
|
150
|
+
if (value['run_id'] !== expected.runId) fail('details run id mismatch', expected.artifactDir);
|
|
151
|
+
if (value['workflow'] !== expected.workflow)
|
|
152
|
+
fail('details workflow mismatch', expected.artifactDir);
|
|
153
|
+
if (value['source'] !== 'command' && value['source'] !== 'tool')
|
|
154
|
+
fail('details source is invalid', expected.artifactDir);
|
|
155
|
+
if (value['status'] !== 'completed')
|
|
156
|
+
fail('details status is not completed', expected.artifactDir);
|
|
157
|
+
if (value['artifact_dir'] !== expected.artifactDir)
|
|
158
|
+
fail('details artifact directory mismatch', expected.artifactDir);
|
|
159
|
+
const context = value['context'];
|
|
160
|
+
const toolPolicy = value['tool_policy'];
|
|
161
|
+
const models = value['models'];
|
|
162
|
+
const budget = value['budget'];
|
|
163
|
+
if (!isRecord(context) || !isRecord(toolPolicy) || !isRecord(models) || !isRecord(budget)) {
|
|
164
|
+
fail('details nested contract is malformed', expected.artifactDir);
|
|
165
|
+
}
|
|
166
|
+
assertOnlyKeys(context, ['kind', 'policy_id'], 'details.context', expected.artifactDir);
|
|
167
|
+
if (
|
|
168
|
+
(context['kind'] !== 'session_projection' && context['kind'] !== 'clean_task') ||
|
|
169
|
+
typeof context['policy_id'] !== 'string'
|
|
170
|
+
) {
|
|
171
|
+
fail('details.context is invalid', expected.artifactDir);
|
|
172
|
+
}
|
|
173
|
+
assertOnlyKeys(
|
|
174
|
+
toolPolicy,
|
|
175
|
+
['candidate_tools', 'evaluation_tools', 'merge_tools'],
|
|
176
|
+
'details.tool_policy',
|
|
177
|
+
expected.artifactDir,
|
|
178
|
+
);
|
|
179
|
+
const stringArray = (entry: unknown): entry is string[] =>
|
|
180
|
+
Array.isArray(entry) && entry.every((item) => typeof item === 'string');
|
|
181
|
+
if (
|
|
182
|
+
!stringArray(toolPolicy['candidate_tools']) ||
|
|
183
|
+
!Array.isArray(toolPolicy['evaluation_tools']) ||
|
|
184
|
+
toolPolicy['evaluation_tools'].length !== 0 ||
|
|
185
|
+
!Array.isArray(toolPolicy['merge_tools']) ||
|
|
186
|
+
toolPolicy['merge_tools'].length !== 0
|
|
187
|
+
) {
|
|
188
|
+
fail('details.tool_policy is invalid', expected.artifactDir);
|
|
189
|
+
}
|
|
190
|
+
assertOnlyKeys(
|
|
191
|
+
models,
|
|
192
|
+
['candidates', 'evaluator', 'merger', 'thinking_level'],
|
|
193
|
+
'details.models',
|
|
194
|
+
expected.artifactDir,
|
|
195
|
+
);
|
|
196
|
+
if (
|
|
197
|
+
!stringArray(models['candidates']) ||
|
|
198
|
+
models['candidates'].length !== 3 ||
|
|
199
|
+
typeof models['evaluator'] !== 'string' ||
|
|
200
|
+
typeof models['merger'] !== 'string' ||
|
|
201
|
+
typeof models['thinking_level'] !== 'string'
|
|
202
|
+
) {
|
|
203
|
+
fail('details.models is invalid', expected.artifactDir);
|
|
204
|
+
}
|
|
205
|
+
assertOnlyKeys(
|
|
206
|
+
budget,
|
|
207
|
+
[
|
|
208
|
+
'policy_id',
|
|
209
|
+
'calibration_version',
|
|
210
|
+
'route_table',
|
|
211
|
+
'rate_sources',
|
|
212
|
+
'unknown_provider_warnings',
|
|
213
|
+
'calibration_warnings',
|
|
214
|
+
],
|
|
215
|
+
'details.budget',
|
|
216
|
+
expected.artifactDir,
|
|
217
|
+
);
|
|
218
|
+
if (
|
|
219
|
+
typeof budget['policy_id'] !== 'string' ||
|
|
220
|
+
typeof budget['calibration_version'] !== 'string' ||
|
|
221
|
+
!Array.isArray(budget['route_table']) ||
|
|
222
|
+
!Array.isArray(budget['rate_sources']) ||
|
|
223
|
+
!stringArray(budget['unknown_provider_warnings']) ||
|
|
224
|
+
!Array.isArray(budget['calibration_warnings'])
|
|
225
|
+
) {
|
|
226
|
+
fail('details.budget is invalid', expected.artifactDir);
|
|
227
|
+
}
|
|
228
|
+
if (
|
|
229
|
+
!Number.isSafeInteger(value['evaluator_attempts']) ||
|
|
230
|
+
![1, 2].includes(Number(value['evaluator_attempts']))
|
|
231
|
+
) {
|
|
232
|
+
fail('details evaluator_attempts is invalid', expected.artifactDir);
|
|
233
|
+
}
|
|
234
|
+
const checkedUsage = usage(value['usage'], expected.artifactDir);
|
|
235
|
+
const candidates = models['candidates'];
|
|
236
|
+
if (!stringArray(candidates) || candidates.length !== 3)
|
|
237
|
+
fail('details.models candidates are invalid', expected.artifactDir);
|
|
238
|
+
const candidate1 = candidates[0];
|
|
239
|
+
const candidate2 = candidates[1];
|
|
240
|
+
const candidate3 = candidates[2];
|
|
241
|
+
if (candidate1 === undefined || candidate2 === undefined || candidate3 === undefined) {
|
|
242
|
+
fail('details.models candidates are incomplete', expected.artifactDir);
|
|
243
|
+
}
|
|
244
|
+
const source = value['source'];
|
|
245
|
+
const contextKind = context['kind'];
|
|
246
|
+
return {
|
|
247
|
+
schema_version: FUSION_RESULT_SCHEMA_VERSION,
|
|
248
|
+
run_id: expected.runId,
|
|
249
|
+
workflow: expected.workflow,
|
|
250
|
+
source,
|
|
251
|
+
status: 'completed',
|
|
252
|
+
context: { kind: contextKind, policy_id: context['policy_id'] },
|
|
253
|
+
tool_policy: {
|
|
254
|
+
candidate_tools: [...toolPolicy['candidate_tools']],
|
|
255
|
+
evaluation_tools: [],
|
|
256
|
+
merge_tools: [],
|
|
257
|
+
},
|
|
258
|
+
artifact_dir: expected.artifactDir,
|
|
259
|
+
models: {
|
|
260
|
+
candidates: [candidate1, candidate2, candidate3],
|
|
261
|
+
evaluator: models['evaluator'],
|
|
262
|
+
merger: models['merger'],
|
|
263
|
+
thinking_level: models['thinking_level'],
|
|
264
|
+
},
|
|
265
|
+
evaluator_attempts: Number(value['evaluator_attempts']),
|
|
266
|
+
usage: checkedUsage,
|
|
267
|
+
budget: {
|
|
268
|
+
policy_id: budget['policy_id'],
|
|
269
|
+
calibration_version: budget['calibration_version'],
|
|
270
|
+
route_table: budget['route_table'] as FusionResultDetails['budget']['route_table'],
|
|
271
|
+
rate_sources: budget['rate_sources'] as FusionResultDetails['budget']['rate_sources'],
|
|
272
|
+
unknown_provider_warnings: [...budget['unknown_provider_warnings']],
|
|
273
|
+
calibration_warnings: budget[
|
|
274
|
+
'calibration_warnings'
|
|
275
|
+
] as FusionResultDetails['budget']['calibration_warnings'],
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function sameRef(left: FusionArtifactRef, right: FusionArtifactRef): boolean {
|
|
281
|
+
return (
|
|
282
|
+
left.path === right.path &&
|
|
283
|
+
left.byte_length === right.byte_length &&
|
|
284
|
+
left.sha256 === right.sha256
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async function readUtf8(
|
|
289
|
+
path: string,
|
|
290
|
+
label: string,
|
|
291
|
+
artifactDir: string,
|
|
292
|
+
): Promise<{ bytes: Buffer; text: string }> {
|
|
293
|
+
let bytes: Buffer;
|
|
294
|
+
try {
|
|
295
|
+
bytes = await readFile(path);
|
|
296
|
+
} catch (error) {
|
|
297
|
+
fail(
|
|
298
|
+
`${label} is unreadable: ${error instanceof Error ? error.message : String(error)}`,
|
|
299
|
+
artifactDir,
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
let text: string;
|
|
303
|
+
try {
|
|
304
|
+
text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
305
|
+
} catch {
|
|
306
|
+
fail(`${label} is not well-formed UTF-8`, artifactDir);
|
|
307
|
+
}
|
|
308
|
+
return { bytes, text };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export interface ReadFusionCommittedResultOptions {
|
|
312
|
+
artifactDirAbs: string;
|
|
313
|
+
artifactDir: string;
|
|
314
|
+
runId: string;
|
|
315
|
+
workflow: FusionWorkflowId;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** Verify the manifest-bound Fusion commit before returning merged bytes. */
|
|
319
|
+
export async function readFusionCommittedResult(
|
|
320
|
+
options: ReadFusionCommittedResultOptions,
|
|
321
|
+
): Promise<FusionRunResult> {
|
|
322
|
+
const manifestFile = await readUtf8(
|
|
323
|
+
join(options.artifactDirAbs, 'manifest.json'),
|
|
324
|
+
'manifest.json',
|
|
325
|
+
options.artifactDir,
|
|
326
|
+
);
|
|
327
|
+
let manifestValue: unknown;
|
|
328
|
+
try {
|
|
329
|
+
manifestValue = parseJsonText(manifestFile.text);
|
|
330
|
+
} catch (error) {
|
|
331
|
+
fail(
|
|
332
|
+
`manifest.json is invalid JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
333
|
+
options.artifactDir,
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
if (!isRecord(manifestValue)) fail('manifest.json must be an object', options.artifactDir);
|
|
337
|
+
if (manifestValue['schema_version'] !== FUSION_MANIFEST_SCHEMA_VERSION)
|
|
338
|
+
fail('manifest schema version mismatch', options.artifactDir);
|
|
339
|
+
if (manifestValue['run_id'] !== options.runId || manifestValue['workflow'] !== options.workflow)
|
|
340
|
+
fail('manifest identity mismatch', options.artifactDir);
|
|
341
|
+
if (manifestValue['state'] !== 'completed')
|
|
342
|
+
fail('manifest is not committed', options.artifactDir);
|
|
343
|
+
const artifacts = manifestValue['artifacts'];
|
|
344
|
+
if (!isRecord(artifacts)) fail('manifest artifacts map is invalid', options.artifactDir);
|
|
345
|
+
const manifestMerged = artifactRef(
|
|
346
|
+
artifacts['merged.md'],
|
|
347
|
+
'manifest artifacts merged.md',
|
|
348
|
+
options.artifactDir,
|
|
349
|
+
);
|
|
350
|
+
const manifestResult = artifactRef(
|
|
351
|
+
artifacts['result.json'],
|
|
352
|
+
'manifest artifacts result.json',
|
|
353
|
+
options.artifactDir,
|
|
354
|
+
);
|
|
355
|
+
if (manifestMerged.path !== 'merged.md' || manifestResult.path !== 'result.json')
|
|
356
|
+
fail('manifest fixed artifact paths are invalid', options.artifactDir);
|
|
357
|
+
|
|
358
|
+
const resultFile = await readUtf8(
|
|
359
|
+
join(options.artifactDirAbs, 'result.json'),
|
|
360
|
+
'result.json',
|
|
361
|
+
options.artifactDir,
|
|
362
|
+
);
|
|
363
|
+
if (
|
|
364
|
+
resultFile.bytes.length !== manifestResult.byte_length ||
|
|
365
|
+
sha256Buffer(resultFile.bytes) !== manifestResult.sha256
|
|
366
|
+
) {
|
|
367
|
+
fail('result.json does not match its manifest hash and length', options.artifactDir);
|
|
368
|
+
}
|
|
369
|
+
let resultValue: unknown;
|
|
370
|
+
try {
|
|
371
|
+
resultValue = parseJsonText(resultFile.text);
|
|
372
|
+
} catch (error) {
|
|
373
|
+
fail(
|
|
374
|
+
`result.json is invalid JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
375
|
+
options.artifactDir,
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
if (!isRecord(resultValue)) fail('result.json must be an object', options.artifactDir);
|
|
379
|
+
assertOnlyKeys(
|
|
380
|
+
resultValue,
|
|
381
|
+
['schema_version', 'run_id', 'merged', 'details'],
|
|
382
|
+
'result.json',
|
|
383
|
+
options.artifactDir,
|
|
384
|
+
);
|
|
385
|
+
if (
|
|
386
|
+
resultValue['schema_version'] !== FUSION_COMMITTED_RESULT_SCHEMA_VERSION ||
|
|
387
|
+
resultValue['run_id'] !== options.runId
|
|
388
|
+
) {
|
|
389
|
+
fail('result.json identity mismatch', options.artifactDir);
|
|
390
|
+
}
|
|
391
|
+
const committedMerged = artifactRef(
|
|
392
|
+
resultValue['merged'],
|
|
393
|
+
'result.json merged',
|
|
394
|
+
options.artifactDir,
|
|
395
|
+
);
|
|
396
|
+
if (!sameRef(committedMerged, manifestMerged))
|
|
397
|
+
fail('result.json merged reference does not match manifest', options.artifactDir);
|
|
398
|
+
const details = resultDetails(resultValue['details'], options);
|
|
399
|
+
|
|
400
|
+
const mergedFile = await readUtf8(
|
|
401
|
+
join(options.artifactDirAbs, 'merged.md'),
|
|
402
|
+
'merged.md',
|
|
403
|
+
options.artifactDir,
|
|
404
|
+
);
|
|
405
|
+
if (
|
|
406
|
+
mergedFile.bytes.length !== committedMerged.byte_length ||
|
|
407
|
+
sha256Buffer(mergedFile.bytes) !== committedMerged.sha256
|
|
408
|
+
) {
|
|
409
|
+
fail('merged.md does not match its committed hash and length', options.artifactDir);
|
|
410
|
+
}
|
|
411
|
+
return { mergedText: mergedFile.text, details };
|
|
412
|
+
}
|
package/src/core/fusion/types.ts
CHANGED
|
@@ -18,6 +18,8 @@ export const FUSION_VALIDATE_CANDIDATE_SCHEMA_VERSION =
|
|
|
18
18
|
'pi-background-tasks.fusion-validation-candidate.v1';
|
|
19
19
|
export const FUSION_LEGACY_RESULT_SCHEMA_VERSION = 'pi-background-tasks.fusion-result.v4';
|
|
20
20
|
export const FUSION_RESULT_SCHEMA_VERSION = 'pi-background-tasks.fusion-result.v5';
|
|
21
|
+
export const FUSION_COMMITTED_RESULT_SCHEMA_VERSION =
|
|
22
|
+
'pi-background-tasks.fusion-committed-result.v1';
|
|
21
23
|
export const FUSION_LEGACY_MANIFEST_SCHEMA_VERSION = 'pi-background-tasks.fusion-manifest.v3';
|
|
22
24
|
export const FUSION_MANIFEST_SCHEMA_VERSION = 'pi-background-tasks.fusion-manifest.v4';
|
|
23
25
|
export const FUSION_CONTEXT_LEDGER_SCHEMA_VERSION = 'pi-background-tasks.fusion-context-ledger.v2';
|
|
@@ -520,6 +522,8 @@ export function cloneFusionUsage(usage: FusionUsage): FusionUsage {
|
|
|
520
522
|
output: usage.output,
|
|
521
523
|
cacheRead: usage.cacheRead,
|
|
522
524
|
cacheWrite: usage.cacheWrite,
|
|
525
|
+
...(usage.cacheWrite1h === undefined ? {} : { cacheWrite1h: usage.cacheWrite1h }),
|
|
526
|
+
...(usage.reasoning === undefined ? {} : { reasoning: usage.reasoning }),
|
|
523
527
|
totalTokens: usage.totalTokens,
|
|
524
528
|
cost: {
|
|
525
529
|
input: usage.cost.input,
|
|
@@ -536,6 +540,12 @@ export function addFusionUsage(target: FusionUsage, delta: FusionUsage): void {
|
|
|
536
540
|
target.output += delta.output;
|
|
537
541
|
target.cacheRead += delta.cacheRead;
|
|
538
542
|
target.cacheWrite += delta.cacheWrite;
|
|
543
|
+
if (delta.cacheWrite1h !== undefined) {
|
|
544
|
+
target.cacheWrite1h = (target.cacheWrite1h ?? 0) + delta.cacheWrite1h;
|
|
545
|
+
}
|
|
546
|
+
if (delta.reasoning !== undefined) {
|
|
547
|
+
target.reasoning = (target.reasoning ?? 0) + delta.reasoning;
|
|
548
|
+
}
|
|
539
549
|
target.totalTokens += delta.totalTokens;
|
|
540
550
|
target.cost.input += delta.cost.input;
|
|
541
551
|
target.cost.output += delta.cost.output;
|
|
@@ -544,6 +554,26 @@ export function addFusionUsage(target: FusionUsage, delta: FusionUsage): void {
|
|
|
544
554
|
target.cost.total += delta.cost.total;
|
|
545
555
|
}
|
|
546
556
|
|
|
557
|
+
export type FusionRunStageProgressStatus = 'not_started' | 'incomplete' | 'completed';
|
|
558
|
+
|
|
559
|
+
export interface FusionRunStageProgress {
|
|
560
|
+
status: FusionRunStageProgressStatus;
|
|
561
|
+
attempts_recorded: number;
|
|
562
|
+
children_created: number;
|
|
563
|
+
children_completed: number;
|
|
564
|
+
children_failed: number;
|
|
565
|
+
children_cancelled: number;
|
|
566
|
+
not_started_slots?: number | undefined;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
export interface FusionRunProgress {
|
|
570
|
+
manifest_state: FusionState;
|
|
571
|
+
candidates: FusionRunStageProgress;
|
|
572
|
+
evaluation: FusionRunStageProgress;
|
|
573
|
+
merge: FusionRunStageProgress;
|
|
574
|
+
usage_so_far: FusionUsage;
|
|
575
|
+
}
|
|
576
|
+
|
|
547
577
|
export interface FusionResultBudgetDetails {
|
|
548
578
|
policy_id: string;
|
|
549
579
|
calibration_version: string;
|
|
@@ -553,6 +583,13 @@ export interface FusionResultBudgetDetails {
|
|
|
553
583
|
calibration_warnings: readonly FusionCalibrationViolation[];
|
|
554
584
|
}
|
|
555
585
|
|
|
586
|
+
export interface FusionCommittedResultV1 {
|
|
587
|
+
schema_version: typeof FUSION_COMMITTED_RESULT_SCHEMA_VERSION;
|
|
588
|
+
run_id: string;
|
|
589
|
+
merged: FusionArtifactRef;
|
|
590
|
+
details: FusionResultDetails;
|
|
591
|
+
}
|
|
592
|
+
|
|
556
593
|
export interface FusionResultDetails {
|
|
557
594
|
schema_version: typeof FUSION_RESULT_SCHEMA_VERSION;
|
|
558
595
|
run_id: string;
|
|
@@ -698,6 +735,7 @@ export interface FusionErrorDetails {
|
|
|
698
735
|
transient?: boolean;
|
|
699
736
|
childCreated?: boolean;
|
|
700
737
|
budget?: FusionBudgetErrorDetail;
|
|
738
|
+
runProgress?: FusionRunProgress;
|
|
701
739
|
}
|
|
702
740
|
|
|
703
741
|
export class FusionError extends Error {
|
|
@@ -709,6 +747,7 @@ export class FusionError extends Error {
|
|
|
709
747
|
readonly transient: boolean;
|
|
710
748
|
readonly childCreated: boolean;
|
|
711
749
|
readonly budget: FusionBudgetErrorDetail | undefined;
|
|
750
|
+
readonly runProgress: FusionRunProgress | undefined;
|
|
712
751
|
|
|
713
752
|
constructor(message: string, details: FusionErrorDetails) {
|
|
714
753
|
super(message);
|
|
@@ -721,6 +760,7 @@ export class FusionError extends Error {
|
|
|
721
760
|
this.transient = details.transient ?? false;
|
|
722
761
|
this.childCreated = details.childCreated ?? true;
|
|
723
762
|
this.budget = details.budget;
|
|
763
|
+
this.runProgress = details.runProgress;
|
|
724
764
|
}
|
|
725
765
|
}
|
|
726
766
|
|
|
@@ -763,6 +803,18 @@ export interface FusionToolCallTrace {
|
|
|
763
803
|
summary: FusionToolCallLogSummary;
|
|
764
804
|
}
|
|
765
805
|
|
|
806
|
+
export interface FusionCandidateOutputRecovery {
|
|
807
|
+
kind: 'same_session_compression';
|
|
808
|
+
limit_bytes: number;
|
|
809
|
+
original_record_index: number;
|
|
810
|
+
replacement_record_index: number | null;
|
|
811
|
+
original_json_rendered_bytes: number;
|
|
812
|
+
replacement_json_rendered_bytes: number | null;
|
|
813
|
+
original_text_sha256: string;
|
|
814
|
+
original_text: string;
|
|
815
|
+
status: 'completed' | 'failed';
|
|
816
|
+
}
|
|
817
|
+
|
|
766
818
|
export interface FusionChildRunResult {
|
|
767
819
|
stage: FusionStage;
|
|
768
820
|
slot?: 1 | 2 | 3;
|
|
@@ -777,6 +829,7 @@ export interface FusionChildRunResult {
|
|
|
777
829
|
firstRequestUsage?: FusionUsage;
|
|
778
830
|
/** Number of provider requests represented by aggregate usage. */
|
|
779
831
|
providerRequestCount?: number;
|
|
832
|
+
outputRecovery?: FusionCandidateOutputRecovery;
|
|
780
833
|
events: Buffer;
|
|
781
834
|
stderr: Buffer;
|
|
782
835
|
exitCode: number;
|
|
@@ -784,11 +837,24 @@ export interface FusionChildRunResult {
|
|
|
784
837
|
toolCallTrace?: FusionToolCallTrace;
|
|
785
838
|
}
|
|
786
839
|
|
|
840
|
+
export interface FusionAttemptOutputRecoveryRecord {
|
|
841
|
+
kind: 'same_session_compression';
|
|
842
|
+
status: 'completed' | 'failed';
|
|
843
|
+
limit_bytes: number;
|
|
844
|
+
original_response_path: string;
|
|
845
|
+
original_record_index: number;
|
|
846
|
+
replacement_record_index: number | null;
|
|
847
|
+
original_json_rendered_bytes: number;
|
|
848
|
+
replacement_json_rendered_bytes: number | null;
|
|
849
|
+
original_text_sha256: string;
|
|
850
|
+
}
|
|
851
|
+
|
|
787
852
|
export interface FusionAttemptArtifactRecord {
|
|
788
853
|
stage: FusionStage;
|
|
789
854
|
slot?: 1 | 2 | 3;
|
|
790
855
|
attempt: number;
|
|
791
856
|
status: 'completed' | 'failed' | 'cancelled';
|
|
857
|
+
child_created: boolean;
|
|
792
858
|
prompt_path: string;
|
|
793
859
|
events_path?: string;
|
|
794
860
|
stderr_path?: string;
|
|
@@ -796,6 +862,7 @@ export interface FusionAttemptArtifactRecord {
|
|
|
796
862
|
partial_response_path?: string;
|
|
797
863
|
tool_calls_path?: string;
|
|
798
864
|
tool_calls?: FusionToolCallLogSummary;
|
|
865
|
+
output_recovery?: FusionAttemptOutputRecoveryRecord;
|
|
799
866
|
provider?: string;
|
|
800
867
|
model?: string;
|
|
801
868
|
qualifiedId?: string;
|