pi-background-tasks 2.0.0 → 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 +2 -2
- package/TESTING.md +1 -1
- package/TEST_PLAN.md +4 -4
- package/docs/manifest.json +12 -2
- package/docs/operations/configuration.md +5 -3
- package/docs/read-before-edit.md +2 -0
- package/docs/reference/runtime-contracts.md +58 -56
- package/docs/subsystems/docs-freshness-gate.md +3 -3
- package/docs/subsystems/fusion.md +10 -7
- package/package.json +1 -1
- package/src/core/fusion/anthropic-attribution.ts +1930 -0
- package/src/core/fusion/artifacts.ts +96 -1
- 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 +128 -10
- 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 +30 -3
- package/src/core/fusion/types.ts +58 -0
- package/src/fusion-child-extension.ts +117 -2
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createHash, randomBytes as nodeRandomBytes } from 'node:crypto';
|
|
2
2
|
import { canonicalJson } from '../attested-pi-run.js';
|
|
3
3
|
import { parseJsonText } from '../common.js';
|
|
4
|
-
import { FUSION_BUDGET_POLICY, FusionBudget
|
|
4
|
+
import { FUSION_BUDGET_POLICY, FusionBudget } from './budget.js';
|
|
5
|
+
import { assertChildOutputWithinContract } from './output-contract.js';
|
|
5
6
|
import {
|
|
6
7
|
FusionArtifactStore,
|
|
7
8
|
type CreateFusionArtifactStoreOptions,
|
|
@@ -39,7 +40,9 @@ import {
|
|
|
39
40
|
FUSION_VALIDATE_CANDIDATE_SCHEMA_VERSION,
|
|
40
41
|
FusionError,
|
|
41
42
|
addFusionUsage,
|
|
43
|
+
cloneFusionUsage,
|
|
42
44
|
createEmptyFusionUsage,
|
|
45
|
+
type FusionArtifactManifest,
|
|
43
46
|
type FusionCalibrationViolation,
|
|
44
47
|
type FusionCapability,
|
|
45
48
|
type FusionCanonicalInputV3,
|
|
@@ -50,6 +53,7 @@ import {
|
|
|
50
53
|
type FusionEvaluationV1,
|
|
51
54
|
type FusionModelConfigV1,
|
|
52
55
|
type FusionProgressEvent,
|
|
56
|
+
type FusionRunProgress,
|
|
53
57
|
type FusionRunResult,
|
|
54
58
|
type FusionSource,
|
|
55
59
|
type FusionStage,
|
|
@@ -162,6 +166,7 @@ function asFusionError(error: unknown, artifactDir: string, messageOverride?: st
|
|
|
162
166
|
if (error.slot !== undefined) details.slot = error.slot;
|
|
163
167
|
if (error.attempt !== undefined) details.attempt = error.attempt;
|
|
164
168
|
if (error.budget !== undefined) details.budget = error.budget;
|
|
169
|
+
if (error.runProgress !== undefined) details.runProgress = error.runProgress;
|
|
165
170
|
return new FusionError(messageOverride ?? error.message, details);
|
|
166
171
|
}
|
|
167
172
|
return new FusionError(messageOverride ?? errorText(error), {
|
|
@@ -171,6 +176,103 @@ function asFusionError(error: unknown, artifactDir: string, messageOverride?: st
|
|
|
171
176
|
});
|
|
172
177
|
}
|
|
173
178
|
|
|
179
|
+
function fusionStageProgress(
|
|
180
|
+
manifest: FusionArtifactManifest,
|
|
181
|
+
stage: FusionStage,
|
|
182
|
+
): FusionRunProgress['candidates'] {
|
|
183
|
+
const attempts = manifest.attempts.filter((attempt) => attempt.stage === stage);
|
|
184
|
+
const created = attempts.filter((attempt) => attempt.child_created).length;
|
|
185
|
+
const completed = attempts.filter(
|
|
186
|
+
(attempt) => attempt.child_created && attempt.status === 'completed',
|
|
187
|
+
).length;
|
|
188
|
+
const failed = attempts.filter(
|
|
189
|
+
(attempt) => attempt.child_created && attempt.status === 'failed',
|
|
190
|
+
).length;
|
|
191
|
+
const cancelled = attempts.filter(
|
|
192
|
+
(attempt) => attempt.child_created && attempt.status === 'cancelled',
|
|
193
|
+
).length;
|
|
194
|
+
const completedByState =
|
|
195
|
+
stage === 'candidate'
|
|
196
|
+
? completed >= 3
|
|
197
|
+
: stage === 'evaluation'
|
|
198
|
+
? manifest.artifacts['evaluation.json'] !== undefined ||
|
|
199
|
+
manifest.state === 'evaluation_complete' ||
|
|
200
|
+
manifest.state === 'merging' ||
|
|
201
|
+
manifest.state === 'completed'
|
|
202
|
+
: manifest.artifacts['merged.md'] !== undefined || manifest.state === 'completed';
|
|
203
|
+
const progress: FusionRunProgress['candidates'] = {
|
|
204
|
+
status: completedByState ? 'completed' : created === 0 ? 'not_started' : 'incomplete',
|
|
205
|
+
attempts_recorded: attempts.length,
|
|
206
|
+
children_created: created,
|
|
207
|
+
children_completed: completed,
|
|
208
|
+
children_failed: failed,
|
|
209
|
+
children_cancelled: cancelled,
|
|
210
|
+
};
|
|
211
|
+
if (stage === 'candidate') {
|
|
212
|
+
const createdSlots = new Set(
|
|
213
|
+
attempts.flatMap((attempt) =>
|
|
214
|
+
attempt.child_created && attempt.slot !== undefined ? [attempt.slot] : [],
|
|
215
|
+
),
|
|
216
|
+
);
|
|
217
|
+
progress.not_started_slots = 3 - createdSlots.size;
|
|
218
|
+
}
|
|
219
|
+
return progress;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function buildFusionRunProgress(manifest: FusionArtifactManifest): FusionRunProgress {
|
|
223
|
+
return {
|
|
224
|
+
manifest_state: manifest.state,
|
|
225
|
+
candidates: fusionStageProgress(manifest, 'candidate'),
|
|
226
|
+
evaluation: fusionStageProgress(manifest, 'evaluation'),
|
|
227
|
+
merge: fusionStageProgress(manifest, 'merge'),
|
|
228
|
+
usage_so_far: cloneFusionUsage(manifest.usage),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function formatFusionRunStage(name: string, stage: FusionRunProgress['candidates']): string {
|
|
233
|
+
const notStarted =
|
|
234
|
+
stage.not_started_slots === undefined
|
|
235
|
+
? ''
|
|
236
|
+
: `, ${String(stage.not_started_slots)} slot(s) not started`;
|
|
237
|
+
return `${name}=${stage.status} (${String(stage.children_created)} created, ${String(stage.children_completed)} completed, ${String(stage.children_failed)} failed, ${String(stage.children_cancelled)} cancelled${notStarted})`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function formatFusionRunProgress(progress: FusionRunProgress): string {
|
|
241
|
+
const usage = progress.usage_so_far;
|
|
242
|
+
const optionalUsage = [
|
|
243
|
+
usage.cacheWrite1h === undefined ? undefined : `cacheWrite1h=${String(usage.cacheWrite1h)}`,
|
|
244
|
+
usage.reasoning === undefined ? undefined : `reasoning=${String(usage.reasoning)}`,
|
|
245
|
+
].filter((value): value is string => value !== undefined);
|
|
246
|
+
const optionalText = optionalUsage.length === 0 ? '' : `, ${optionalUsage.join(', ')}`;
|
|
247
|
+
return (
|
|
248
|
+
`Run progress from durable attempts: ${formatFusionRunStage('candidates', progress.candidates)}; ` +
|
|
249
|
+
`${formatFusionRunStage('evaluation', progress.evaluation)}; ` +
|
|
250
|
+
`${formatFusionRunStage('merge', progress.merge)}. ` +
|
|
251
|
+
`Usage so far: input=${String(usage.input)}, output=${String(usage.output)}, cacheRead=${String(usage.cacheRead)}, cacheWrite=${String(usage.cacheWrite)}${optionalText}, totalTokens=${String(usage.totalTokens)}, ` +
|
|
252
|
+
`cost.input=${String(usage.cost.input)}, cost.output=${String(usage.cost.output)}, cost.cacheRead=${String(usage.cost.cacheRead)}, cost.cacheWrite=${String(usage.cost.cacheWrite)}, cost.total=${String(usage.cost.total)}.`
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function withRunProgress(
|
|
257
|
+
error: unknown,
|
|
258
|
+
artifactDir: string,
|
|
259
|
+
progress: FusionRunProgress,
|
|
260
|
+
): FusionError {
|
|
261
|
+
const base = asFusionError(error, artifactDir);
|
|
262
|
+
const details: FusionErrorDetails = {
|
|
263
|
+
code: base.code,
|
|
264
|
+
artifactDir,
|
|
265
|
+
transient: base.transient,
|
|
266
|
+
childCreated: base.childCreated,
|
|
267
|
+
runProgress: progress,
|
|
268
|
+
};
|
|
269
|
+
if (base.stage !== undefined) details.stage = base.stage;
|
|
270
|
+
if (base.slot !== undefined) details.slot = base.slot;
|
|
271
|
+
if (base.attempt !== undefined) details.attempt = base.attempt;
|
|
272
|
+
if (base.budget !== undefined) details.budget = base.budget;
|
|
273
|
+
return new FusionError(`${base.message}\n${formatFusionRunProgress(progress)}`, details);
|
|
274
|
+
}
|
|
275
|
+
|
|
174
276
|
function withTerminalArtifactFailure(
|
|
175
277
|
error: unknown,
|
|
176
278
|
artifactDir: string,
|
|
@@ -201,7 +303,9 @@ function recordFailureInput(
|
|
|
201
303
|
error: error.message,
|
|
202
304
|
status: error.code === 'child_cancelled' ? 'cancelled' : 'failed',
|
|
203
305
|
responseKind,
|
|
306
|
+
childCreated: error.childCreated,
|
|
204
307
|
usage: error.usage,
|
|
308
|
+
...(error.outputRecovery === undefined ? {} : { outputRecovery: error.outputRecovery }),
|
|
205
309
|
};
|
|
206
310
|
if (slot !== undefined) base.slot = slot;
|
|
207
311
|
if (error.provider !== undefined) base.provider = error.provider;
|
|
@@ -221,6 +325,7 @@ function recordFailureInput(
|
|
|
221
325
|
status:
|
|
222
326
|
error instanceof FusionError && error.code === 'child_cancelled' ? 'cancelled' : 'failed',
|
|
223
327
|
responseKind,
|
|
328
|
+
childCreated: error instanceof FusionError ? error.childCreated : false,
|
|
224
329
|
};
|
|
225
330
|
if (slot !== undefined) base.slot = slot;
|
|
226
331
|
return base;
|
|
@@ -245,6 +350,7 @@ function childOptions(
|
|
|
245
350
|
slot?: CandidateSlot,
|
|
246
351
|
toolCallLogPath?: string,
|
|
247
352
|
sourcePolicy?: { path: string; sha256: string },
|
|
353
|
+
candidateOutputRecoveryPath?: string,
|
|
248
354
|
): RunPiChildOptions {
|
|
249
355
|
const out: RunPiChildOptions = {
|
|
250
356
|
stage,
|
|
@@ -259,6 +365,8 @@ function childOptions(
|
|
|
259
365
|
if (slot !== undefined) out.slot = slot;
|
|
260
366
|
if (toolCallLogPath !== undefined) out.toolCallLogPath = toolCallLogPath;
|
|
261
367
|
if (sourcePolicy !== undefined) out.sourcePolicy = sourcePolicy;
|
|
368
|
+
if (candidateOutputRecoveryPath !== undefined)
|
|
369
|
+
out.candidateOutputRecoveryPath = candidateOutputRecoveryPath;
|
|
262
370
|
return out;
|
|
263
371
|
}
|
|
264
372
|
|
|
@@ -848,10 +956,15 @@ export class FusionOrchestrator {
|
|
|
848
956
|
const cancelled =
|
|
849
957
|
input.signal?.aborted === true ||
|
|
850
958
|
(error instanceof FusionError && error.code === 'child_cancelled');
|
|
851
|
-
|
|
959
|
+
let terminalError: FusionError;
|
|
852
960
|
try {
|
|
853
961
|
await store.setUsage(usage);
|
|
854
|
-
|
|
962
|
+
terminalError = withRunProgress(
|
|
963
|
+
error,
|
|
964
|
+
store.artifactDir,
|
|
965
|
+
buildFusionRunProgress(store.snapshot()),
|
|
966
|
+
);
|
|
967
|
+
await store.writeError(cancelled ? 'cancelled' : 'failed', terminalError.message);
|
|
855
968
|
} catch (artifactError) {
|
|
856
969
|
throw withTerminalArtifactFailure(error, store.artifactDir, artifactError);
|
|
857
970
|
}
|
|
@@ -860,17 +973,17 @@ export class FusionOrchestrator {
|
|
|
860
973
|
type: 'cancelled',
|
|
861
974
|
runId: store.runId,
|
|
862
975
|
artifactDir: store.artifactDir,
|
|
863
|
-
reason: message,
|
|
976
|
+
reason: terminalError.message,
|
|
864
977
|
});
|
|
865
978
|
} else {
|
|
866
979
|
input.onProgress?.({
|
|
867
980
|
type: 'failed',
|
|
868
981
|
runId: store.runId,
|
|
869
982
|
artifactDir: store.artifactDir,
|
|
870
|
-
error: message,
|
|
983
|
+
error: terminalError.message,
|
|
871
984
|
});
|
|
872
985
|
}
|
|
873
|
-
throw
|
|
986
|
+
throw terminalError;
|
|
874
987
|
}
|
|
875
988
|
}
|
|
876
989
|
|
|
@@ -934,12 +1047,12 @@ export class FusionOrchestrator {
|
|
|
934
1047
|
result,
|
|
935
1048
|
slot,
|
|
936
1049
|
);
|
|
937
|
-
// The response
|
|
938
|
-
// answer is preserved
|
|
939
|
-
assertChildOutputWithinContract('candidate', result.text);
|
|
940
|
-
completed += 1;
|
|
1050
|
+
// The response and its consumed usage are durable before the contract
|
|
1051
|
+
// check, so an oversized answer is preserved and accounted rather than lost.
|
|
941
1052
|
addFusionUsage(usage, result.usage);
|
|
942
1053
|
await store.setUsage(usage);
|
|
1054
|
+
assertChildOutputWithinContract('candidate', result.text);
|
|
1055
|
+
completed += 1;
|
|
943
1056
|
input.onProgress?.({ type: 'candidate_completed', slot, completed, total: 3 });
|
|
944
1057
|
return { slot, result };
|
|
945
1058
|
});
|
|
@@ -1135,6 +1248,10 @@ export class FusionOrchestrator {
|
|
|
1135
1248
|
: undefined;
|
|
1136
1249
|
const sourcePolicy =
|
|
1137
1250
|
capability === 'research' ? store.sourcePolicyLaunchReference() : undefined;
|
|
1251
|
+
const candidateOutputRecoveryPath =
|
|
1252
|
+
stage === 'candidate' && slot !== undefined
|
|
1253
|
+
? store.childOutputRecoveryPath(slot, logicalAttempt, responseKind)
|
|
1254
|
+
: undefined;
|
|
1138
1255
|
try {
|
|
1139
1256
|
return await this.childRunner(
|
|
1140
1257
|
childOptions(
|
|
@@ -1149,6 +1266,7 @@ export class FusionOrchestrator {
|
|
|
1149
1266
|
slot,
|
|
1150
1267
|
toolCallLogPath,
|
|
1151
1268
|
sourcePolicy,
|
|
1269
|
+
candidateOutputRecoveryPath,
|
|
1152
1270
|
),
|
|
1153
1271
|
);
|
|
1154
1272
|
} catch (error) {
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { FusionError, type FusionStage } from './types.js';
|
|
2
|
+
|
|
3
|
+
/** Hard output contracts are measured over the JSON rendering embedded downstream. */
|
|
4
|
+
export const FUSION_CANDIDATE_MAX_OUTPUT_BYTES = 48 * 1024;
|
|
5
|
+
export const FUSION_EVALUATION_MAX_OUTPUT_BYTES = 64 * 1024;
|
|
6
|
+
export const FUSION_MERGE_MAX_OUTPUT_BYTES = 64 * 1024;
|
|
7
|
+
export const FUSION_DIAGNOSTICS_MAX_BYTES = 8 * 1024;
|
|
8
|
+
|
|
9
|
+
const FUSION_CANDIDATE_MAX_OUTPUT_BYTES_DISPLAY =
|
|
10
|
+
FUSION_CANDIDATE_MAX_OUTPUT_BYTES.toLocaleString('en-US');
|
|
11
|
+
|
|
12
|
+
export const FUSION_CANDIDATE_OUTPUT_CONTRACT_INSTRUCTION = `Your complete response must be at most ${FUSION_CANDIDATE_MAX_OUTPUT_BYTES_DISPLAY} JSON-rendered UTF-8 bytes. If the requested scope cannot fit, prioritize the most important findings and explicitly state limitations.`;
|
|
13
|
+
|
|
14
|
+
export const FUSION_CANDIDATE_OUTPUT_COMPRESSION_PROMPT = `Compress and restructure only your immediately previous answer so the complete replacement is at most ${FUSION_CANDIDATE_MAX_OUTPUT_BYTES_DISPLAY} JSON-rendered UTF-8 bytes. Do not investigate again, do not use tools, and do not add new evidence. Preserve the most important findings and evidence already present, state material limitations, obey the original output format, and output only the replacement answer.`;
|
|
15
|
+
|
|
16
|
+
export function fusionJsonRenderedTextBytes(text: string): number {
|
|
17
|
+
return Buffer.byteLength(JSON.stringify(text), 'utf8');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function fusionOutputContractBytes(stage: FusionStage): number {
|
|
21
|
+
if (stage === 'candidate') return FUSION_CANDIDATE_MAX_OUTPUT_BYTES;
|
|
22
|
+
if (stage === 'evaluation') return FUSION_EVALUATION_MAX_OUTPUT_BYTES;
|
|
23
|
+
return FUSION_MERGE_MAX_OUTPUT_BYTES;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function assertChildOutputWithinContract(stage: FusionStage, text: string): void {
|
|
27
|
+
const bytes = fusionJsonRenderedTextBytes(text);
|
|
28
|
+
const allowed = fusionOutputContractBytes(stage);
|
|
29
|
+
if (bytes <= allowed) return;
|
|
30
|
+
throw new FusionError(
|
|
31
|
+
`fusion ${stage} response is ${String(bytes)} JSON-rendered bytes, exceeding the ${String(allowed)}-byte output contract for that stage; the response is preserved in the run artifacts and is not forwarded or truncated`,
|
|
32
|
+
{ code: 'child_output_cap', stage, childCreated: true },
|
|
33
|
+
);
|
|
34
|
+
}
|