@pi-unipi/background-tasks 2.16.0 → 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.
Files changed (46) hide show
  1. package/README.md +21 -27
  2. package/package.json +3 -4
  3. package/src/cards.ts +76 -0
  4. package/src/child-process.ts +1 -1
  5. package/src/config.ts +0 -42
  6. package/src/context-visible-conversation-v2.ts +1 -1
  7. package/src/delegate/artifacts.ts +1 -1
  8. package/src/delegate/launch.ts +17 -30
  9. package/src/delegate/result-package.ts +1 -1
  10. package/src/delegate/runner.ts +1 -20
  11. package/src/delegate/seed.ts +1 -1
  12. package/src/delegate-extension.ts +16 -168
  13. package/src/index.ts +53 -25
  14. package/src/json-utils.ts +56 -0
  15. package/src/package-assets.ts +51 -0
  16. package/src/registry.ts +8 -459
  17. package/src/task-manager.ts +13 -2
  18. package/src/tools.ts +4 -189
  19. package/src/types.ts +17 -70
  20. package/extensions/anthropic-attribution.ts +0 -1
  21. package/extensions/fusion-child.ts +0 -1
  22. package/src/anthropic-attribution-path.ts +0 -21
  23. package/src/anthropic-attribution.ts +0 -1983
  24. package/src/attested-pi-run.ts +0 -612
  25. package/src/fixtures/fusion-golden-bytes.json +0 -310
  26. package/src/fixtures/fusion-validate-golden-bytes.json +0 -282
  27. package/src/fusion/artifacts.ts +0 -967
  28. package/src/fusion/budget.ts +0 -1162
  29. package/src/fusion/child-protocol.ts +0 -305
  30. package/src/fusion/claude-cache.ts +0 -207
  31. package/src/fusion/clean-context.ts +0 -91
  32. package/src/fusion/config.ts +0 -449
  33. package/src/fusion/context.ts +0 -265
  34. package/src/fusion/evaluation.ts +0 -800
  35. package/src/fusion/orchestrator.ts +0 -1288
  36. package/src/fusion/output-contract.ts +0 -34
  37. package/src/fusion/pi-child.ts +0 -2373
  38. package/src/fusion/prompts.ts +0 -345
  39. package/src/fusion/result-package.ts +0 -959
  40. package/src/fusion/source-policy.ts +0 -257
  41. package/src/fusion/types.ts +0 -1139
  42. package/src/fusion/web-fetch.ts +0 -1060
  43. package/src/fusion/workflows.ts +0 -184
  44. package/src/fusion-child-extension.ts +0 -1052
  45. package/src/fusion-extension.ts +0 -1293
  46. package/src/ui/fusion-model-selector.ts +0 -322
@@ -1,305 +0,0 @@
1
- import { createHash } from 'node:crypto';
2
- import type { Usage } from '@earendil-works/pi-ai';
3
- import type { FusionClaudeCacheObservation } from './claude-cache.js';
4
- import {
5
- FUSION_CANDIDATE_MAX_OUTPUT_BYTES,
6
- fusionJsonRenderedTextBytes,
7
- } from './output-contract.js';
8
-
9
- export const FUSION_CHILD_RESULT_SCHEMA_VERSION =
10
- 'pi-background-tasks.fusion-child-result.v4' as const;
11
- export const FUSION_CHILD_RESULT_PREFIX = '\u001ePI_FUSION_CHILD_RESULT ';
12
- export const FUSION_CHILD_SETTLEMENT_SCHEMA_VERSION =
13
- 'pi-background-tasks.fusion-child-settlement.v3' as const;
14
- export const FUSION_CHILD_SETTLEMENT_PREFIX = '\u001ePI_FUSION_CHILD_SETTLEMENT ';
15
- export const FUSION_TOOL_CALL_LOG_PATH_ENV = 'PI_FUSION_TOOL_CALL_LOG_PATH';
16
- export const FUSION_CANDIDATE_OUTPUT_RECOVERY_PATH_ENV = 'PI_FUSION_CANDIDATE_OUTPUT_RECOVERY_PATH';
17
- export const FUSION_RESEARCH_ENABLED_ENV = 'PI_FUSION_RESEARCH_ENABLED';
18
- export const FUSION_SOURCE_POLICY_PATH_ENV = 'PI_FUSION_SOURCE_POLICY_PATH';
19
- export const FUSION_SOURCE_POLICY_SHA256_ENV = 'PI_FUSION_SOURCE_POLICY_SHA256';
20
- export const FUSION_TOOL_CALL_SEAL_SCHEMA_VERSION =
21
- 'pi-background-tasks.fusion-tool-call-seal.v1' as const;
22
- export const FUSION_TOOL_CALL_SEAL_SUFFIX = '.seal.json';
23
- export const FUSION_RUNTIME_GUARD_SCHEMA_VERSION =
24
- 'pi-background-tasks.fusion-runtime-guard.v2' as const;
25
- export const FUSION_RUNTIME_GUARD_PREFIX = '\u001ePI_FUSION_RUNTIME_GUARD ';
26
- export const FUSION_CHILD_MAX_PROVIDER_REQUESTS = 550;
27
- export const FUSION_CHILD_MAX_TOOL_CALLS = 600;
28
-
29
- /**
30
- * Aggregate ceiling on tool-result bytes a single candidate child may accumulate.
31
- *
32
- * The byte ceiling complements the tool/request count limits and pre-spawn stage
33
- * budgets. It remains an independent bound on total tool material across the child run.
34
- */
35
- export const FUSION_CHILD_MAX_TOTAL_TOOL_RESULT_BYTES = 32 * 1024 * 1024;
36
-
37
- export type FusionRuntimeGuardCode =
38
- | 'provider_request_limit'
39
- | 'provider_payload_invalid'
40
- | 'claude_cache_policy'
41
- | 'tool_call_limit';
42
-
43
- export interface FusionRuntimeGuardRecord {
44
- schema_version: typeof FUSION_RUNTIME_GUARD_SCHEMA_VERSION;
45
- code: FusionRuntimeGuardCode;
46
- provider: string;
47
- model: string;
48
- request_ordinal: number;
49
- tool_call_count: number;
50
- payload_bytes: number;
51
- payload_sha256: string;
52
- message: string;
53
- }
54
-
55
- export interface FusionChildTextBlockMetadata {
56
- utf8_bytes: number;
57
- sha256: string;
58
- }
59
-
60
- export type FusionChildResultUsageMetadata = Usage;
61
-
62
- export type FusionChildOutputRecoveryRole = 'none' | 'oversized_original' | 'replacement';
63
-
64
- export interface FusionChildOutputContractMetadata {
65
- json_rendered_bytes: number;
66
- candidate_limit_bytes: number | null;
67
- recovery_role: FusionChildOutputRecoveryRole;
68
- }
69
-
70
- export interface FusionChildResultMetadata {
71
- schema_version: typeof FUSION_CHILD_RESULT_SCHEMA_VERSION;
72
- provider: string;
73
- model: string;
74
- stop_reason: string;
75
- text_blocks: FusionChildTextBlockMetadata[];
76
- text_sha256: string;
77
- usage: FusionChildResultUsageMetadata;
78
- cache_observation: FusionClaudeCacheObservation;
79
- output_contract: FusionChildOutputContractMetadata;
80
- }
81
-
82
- export type FusionChildSettlementFailureReason =
83
- | 'no_records'
84
- | 'final_not_stop'
85
- | 'invalid_non_final'
86
- | 'runtime_guard'
87
- | 'cache_observation'
88
- | 'output_recovery';
89
-
90
- export interface FusionChildSettlementRecord {
91
- schema_version: typeof FUSION_CHILD_SETTLEMENT_SCHEMA_VERSION;
92
- status: 'complete' | 'failed';
93
- record_count: number;
94
- records_sha256: string;
95
- final_record_index: number | null;
96
- final_text_sha256: string | null;
97
- recovered_error_ordinals: number[];
98
- recovered_output_cap_ordinals: number[];
99
- failure_reason: FusionChildSettlementFailureReason | null;
100
- }
101
-
102
- function protocolSha256(value: string | Buffer): string {
103
- return createHash('sha256').update(value).digest('hex');
104
- }
105
-
106
- export function serializeFusionChildResultRecords(
107
- records: readonly FusionChildResultMetadata[],
108
- ): Buffer {
109
- return Buffer.from(
110
- records.length === 0 ? '' : `${records.map((record) => JSON.stringify(record)).join('\n')}\n`,
111
- 'utf8',
112
- );
113
- }
114
-
115
- function hasZeroUsage(record: FusionChildResultMetadata): boolean {
116
- const usage = record.usage;
117
- return (
118
- usage.input === 0 &&
119
- usage.output === 0 &&
120
- usage.cacheRead === 0 &&
121
- usage.cacheWrite === 0 &&
122
- usage.totalTokens === 0 &&
123
- usage.cost.input === 0 &&
124
- usage.cost.output === 0 &&
125
- usage.cost.cacheRead === 0 &&
126
- usage.cost.cacheWrite === 0 &&
127
- usage.cost.total === 0
128
- );
129
- }
130
-
131
- export function isRecoverableFusionChildErrorRecord(record: FusionChildResultMetadata): boolean {
132
- return (
133
- record.stop_reason === 'error' &&
134
- record.text_blocks.length === 0 &&
135
- record.text_sha256 === protocolSha256(Buffer.alloc(0)) &&
136
- hasZeroUsage(record) &&
137
- record.output_contract.recovery_role === 'none'
138
- );
139
- }
140
-
141
- function isOversizedOriginal(record: FusionChildResultMetadata): boolean {
142
- const output = record.output_contract;
143
- return (
144
- output.recovery_role === 'oversized_original' &&
145
- output.candidate_limit_bytes === FUSION_CANDIDATE_MAX_OUTPUT_BYTES &&
146
- output.json_rendered_bytes > FUSION_CANDIDATE_MAX_OUTPUT_BYTES &&
147
- record.stop_reason === 'stop'
148
- );
149
- }
150
-
151
- function outputRecoveryProtocolInvalid(records: readonly FusionChildResultMetadata[]): boolean {
152
- const originals = records.flatMap((record, ordinal) =>
153
- record.output_contract.recovery_role === 'oversized_original' ? [ordinal] : [],
154
- );
155
- const replacements = records.flatMap((record, ordinal) =>
156
- record.output_contract.recovery_role === 'replacement' ? [ordinal] : [],
157
- );
158
- for (const record of records) {
159
- const output = record.output_contract;
160
- if (
161
- output.candidate_limit_bytes !== null &&
162
- output.candidate_limit_bytes !== FUSION_CANDIDATE_MAX_OUTPUT_BYTES
163
- ) {
164
- return true;
165
- }
166
- if (output.recovery_role !== 'none' && output.candidate_limit_bytes === null) return true;
167
- if (output.recovery_role === 'oversized_original' && !isOversizedOriginal(record)) return true;
168
- }
169
- if (originals.length === 0 && replacements.length === 0) return false;
170
- if (originals.length !== 1 || replacements.length !== 1) return true;
171
- const original = originals[0];
172
- const replacement = replacements[0];
173
- return (
174
- original === undefined ||
175
- replacement === undefined ||
176
- original !== records.length - 2 ||
177
- replacement !== records.length - 1
178
- );
179
- }
180
-
181
- function finalCandidateOutputExceedsContract(
182
- records: readonly FusionChildResultMetadata[],
183
- ): boolean {
184
- const final = records.at(-1);
185
- if (final === undefined) return false;
186
- const output = final.output_contract;
187
- return (
188
- output.candidate_limit_bytes === FUSION_CANDIDATE_MAX_OUTPUT_BYTES &&
189
- output.json_rendered_bytes > FUSION_CANDIDATE_MAX_OUTPUT_BYTES
190
- );
191
- }
192
-
193
- export function buildFusionChildSettlement(
194
- records: readonly FusionChildResultMetadata[],
195
- runtimeGuardFailed = false,
196
- cacheObservationFailed = false,
197
- outputRecoveryFailed = false,
198
- ): FusionChildSettlementRecord {
199
- const finalRecordIndex = records.length === 0 ? null : records.length - 1;
200
- const final = records.at(-1);
201
- const recoveredErrorOrdinals = records.flatMap((record, ordinal) =>
202
- ordinal < records.length - 1 && isRecoverableFusionChildErrorRecord(record) ? [ordinal] : [],
203
- );
204
- const recoveredOutputCapOrdinals = records.flatMap((record, ordinal) =>
205
- ordinal < records.length - 1 && isOversizedOriginal(record) ? [ordinal] : [],
206
- );
207
- const invalidRecovery = outputRecoveryProtocolInvalid(records);
208
- const invalidNonFinal = records.some(
209
- (record, ordinal) =>
210
- ordinal < records.length - 1 &&
211
- record.stop_reason !== 'toolUse' &&
212
- !isRecoverableFusionChildErrorRecord(record) &&
213
- !isOversizedOriginal(record),
214
- );
215
- let failureReason: FusionChildSettlementFailureReason | null = null;
216
- if (runtimeGuardFailed) failureReason = 'runtime_guard';
217
- else if (cacheObservationFailed) failureReason = 'cache_observation';
218
- else if (final === undefined) failureReason = 'no_records';
219
- else if (final.stop_reason !== 'stop') failureReason = 'final_not_stop';
220
- else if (
221
- outputRecoveryFailed ||
222
- invalidRecovery ||
223
- finalCandidateOutputExceedsContract(records)
224
- ) {
225
- failureReason = 'output_recovery';
226
- } else if (invalidNonFinal) failureReason = 'invalid_non_final';
227
- return {
228
- schema_version: FUSION_CHILD_SETTLEMENT_SCHEMA_VERSION,
229
- status: failureReason === null ? 'complete' : 'failed',
230
- record_count: records.length,
231
- records_sha256: protocolSha256(serializeFusionChildResultRecords(records)),
232
- final_record_index: finalRecordIndex,
233
- final_text_sha256: final?.text_sha256 ?? null,
234
- recovered_error_ordinals: recoveredErrorOrdinals,
235
- recovered_output_cap_ordinals: recoveredOutputCapOrdinals,
236
- failure_reason: failureReason,
237
- };
238
- }
239
-
240
- export function buildFusionChildResultMetadata(
241
- message: {
242
- provider: string;
243
- model: string;
244
- stopReason: string;
245
- content: ReadonlyArray<{ type: string; text?: string }>;
246
- usage: Usage;
247
- },
248
- cacheObservation: FusionClaudeCacheObservation,
249
- outputContract: {
250
- candidateLimitBytes: number | null;
251
- recoveryRole: FusionChildOutputRecoveryRole;
252
- } = { candidateLimitBytes: null, recoveryRole: 'none' },
253
- ): FusionChildResultMetadata {
254
- if (
255
- outputContract.candidateLimitBytes !== null &&
256
- outputContract.candidateLimitBytes !== FUSION_CANDIDATE_MAX_OUTPUT_BYTES
257
- ) {
258
- throw new Error('fusion child candidate output limit does not match the shared contract');
259
- }
260
- if (outputContract.recoveryRole !== 'none' && outputContract.candidateLimitBytes === null) {
261
- throw new Error('fusion child output recovery role requires the candidate output contract');
262
- }
263
- const textBlocks = message.content.flatMap((part) =>
264
- part.type === 'text' && typeof part.text === 'string' ? [part.text] : [],
265
- );
266
- const text = textBlocks.join('');
267
- const usage: FusionChildResultUsageMetadata = {
268
- input: message.usage.input,
269
- output: message.usage.output,
270
- cacheRead: message.usage.cacheRead,
271
- cacheWrite: message.usage.cacheWrite,
272
- ...(message.usage.cacheWrite1h === undefined
273
- ? {}
274
- : { cacheWrite1h: message.usage.cacheWrite1h }),
275
- ...(message.usage["reasoning" as keyof typeof message.usage] === undefined
276
- ? {}
277
- : { reasoning: message.usage["reasoning" as keyof typeof message.usage] as number }),
278
- totalTokens: message.usage.totalTokens,
279
- cost: {
280
- input: message.usage.cost.input,
281
- output: message.usage.cost.output,
282
- cacheRead: message.usage.cost.cacheRead,
283
- cacheWrite: message.usage.cost.cacheWrite,
284
- total: message.usage.cost.total,
285
- },
286
- };
287
- return {
288
- schema_version: FUSION_CHILD_RESULT_SCHEMA_VERSION,
289
- provider: message.provider,
290
- model: message.model,
291
- stop_reason: message.stopReason,
292
- text_blocks: textBlocks.map((blockText) => ({
293
- utf8_bytes: Buffer.byteLength(blockText, 'utf8'),
294
- sha256: protocolSha256(blockText),
295
- })),
296
- text_sha256: protocolSha256(text),
297
- usage,
298
- cache_observation: cacheObservation,
299
- output_contract: {
300
- json_rendered_bytes: fusionJsonRenderedTextBytes(text),
301
- candidate_limit_bytes: outputContract.candidateLimitBytes,
302
- recovery_role: outputContract.recoveryRole,
303
- },
304
- };
305
- }
@@ -1,207 +0,0 @@
1
- import type { JsonObject } from '../types.js';
2
-
3
- export const FUSION_CLAUDE_CACHE_OBSERVATION_SCHEMA_VERSION =
4
- 'pi-background-tasks.fusion-claude-cache-observation.v1' as const;
5
- export const FUSION_CLAUDE_CACHE_RETENTION_ENV = 'PI_CACHE_RETENTION';
6
- export const FUSION_CLAUDE_CACHE_DEFAULT_RETENTION = 'long' as const;
7
- export const FUSION_CLAUDE_CACHE_BREAKPOINT_LIMIT = 4;
8
- export const FUSION_CLAUDE_PROMPT_CACHING_SCOPE_BETA = 'prompt-caching-scope-2026-01-05' as const;
9
-
10
- export type FusionClaudeCacheRetention = 'none' | 'short' | 'long';
11
- export type FusionClaudeCachePolicySource =
12
- | 'default'
13
- | typeof FUSION_CLAUDE_CACHE_RETENTION_ENV
14
- | 'not_applicable';
15
-
16
- export interface FusionClaudeCacheObservation {
17
- schema_version: typeof FUSION_CLAUDE_CACHE_OBSERVATION_SCHEMA_VERSION;
18
- applicability: 'anthropic' | 'not_applicable';
19
- source: FusionClaudeCachePolicySource;
20
- requested_retention: FusionClaudeCacheRetention | null;
21
- effective_retention: FusionClaudeCacheRetention | null;
22
- breakpoint_count: number;
23
- request_ordinal: number;
24
- }
25
-
26
- export interface FusionClaudeCacheNormalization {
27
- payload: JsonObject;
28
- observation: FusionClaudeCacheObservation;
29
- }
30
-
31
- function isRecord(value: unknown): value is JsonObject {
32
- return typeof value === 'object' && value !== null && !Array.isArray(value);
33
- }
34
-
35
- function unknownArray(value: unknown): unknown[] | undefined {
36
- return Array.isArray(value) ? (value as unknown[]) : undefined;
37
- }
38
-
39
- function requireRequestOrdinal(value: number): number {
40
- if (!Number.isSafeInteger(value) || value <= 0) {
41
- throw new Error('Fusion Claude cache request ordinal must be a positive safe integer');
42
- }
43
- return value;
44
- }
45
-
46
- function parseRetention(value: string): FusionClaudeCacheRetention {
47
- if (value === 'none' || value === 'short' || value === 'long') return value;
48
- throw new Error(
49
- `${FUSION_CLAUDE_CACHE_RETENTION_ENV} must be one of none, short, or long; got ${JSON.stringify(value)}`,
50
- );
51
- }
52
-
53
- export function resolveFusionClaudeCachePolicy(env: Readonly<NodeJS.ProcessEnv> = process.env): {
54
- retention: FusionClaudeCacheRetention;
55
- source: FusionClaudeCachePolicySource;
56
- } {
57
- const configured = env[FUSION_CLAUDE_CACHE_RETENTION_ENV];
58
- if (configured === undefined) {
59
- return { retention: FUSION_CLAUDE_CACHE_DEFAULT_RETENTION, source: 'default' };
60
- }
61
- return {
62
- retention: parseRetention(configured),
63
- source: FUSION_CLAUDE_CACHE_RETENTION_ENV,
64
- };
65
- }
66
-
67
- export function applyFusionClaudePromptCachingScopeHeader(
68
- headers: Record<string, string | null>,
69
- ): boolean {
70
- const matchingKey = Object.keys(headers).find((key) => key.toLowerCase() === 'anthropic-beta');
71
- const existing = matchingKey === undefined ? undefined : headers[matchingKey];
72
- const values =
73
- typeof existing === 'string'
74
- ? existing
75
- .split(',')
76
- .map((value) => value.trim())
77
- .filter((value) => value.length > 0)
78
- : [];
79
- if (!values.includes(FUSION_CLAUDE_PROMPT_CACHING_SCOPE_BETA)) {
80
- values.push(FUSION_CLAUDE_PROMPT_CACHING_SCOPE_BETA);
81
- }
82
- const targetKey = matchingKey ?? 'anthropic-beta';
83
- headers[targetKey] = values.join(',');
84
- return true;
85
- }
86
-
87
- function validateCacheControl(value: unknown): JsonObject {
88
- if (!isRecord(value)) {
89
- throw new Error('Fusion Claude cache_control must be an object');
90
- }
91
- if (value['type'] !== 'ephemeral') {
92
- throw new Error('Fusion Claude cache_control.type must be "ephemeral"');
93
- }
94
- const ttl = value['ttl'];
95
- if (ttl !== undefined && ttl !== '1h' && ttl !== '5m') {
96
- throw new Error('Fusion Claude cache_control.ttl must be "1h" or "5m" when present');
97
- }
98
- return value;
99
- }
100
-
101
- /**
102
- * Normalize only cache breakpoints already selected by Pi's Anthropic adapter.
103
- *
104
- * Not creating new breakpoints is deliberate: an empty marker set may represent
105
- * Pi's explicit cacheRetention="none" compaction request or a model compatibility
106
- * restriction. The package may strengthen or disable native markers, but it must
107
- * not override an upstream call-level opt-out that is no longer visible in the
108
- * final provider payload.
109
- */
110
- export function normalizeFusionClaudeCachePayload(input: {
111
- payload: unknown;
112
- requestOrdinal: number;
113
- env?: Readonly<NodeJS.ProcessEnv>;
114
- supportsLongCacheRetention?: boolean | undefined;
115
- }): FusionClaudeCacheNormalization {
116
- if (!isRecord(input.payload)) {
117
- throw new Error('Fusion Claude provider payload must be an object');
118
- }
119
- const requestOrdinal = requireRequestOrdinal(input.requestOrdinal);
120
- const policy = resolveFusionClaudeCachePolicy(input.env ?? process.env);
121
- const normalizedRetention: FusionClaudeCacheRetention =
122
- policy.retention === 'long' && input.supportsLongCacheRetention === false
123
- ? 'short'
124
- : policy.retention;
125
- let incomingBreakpoints = 0;
126
- let outputBreakpoints = 0;
127
-
128
- const normalizeBlock = (value: unknown): unknown => {
129
- if (!isRecord(value) || !Object.hasOwn(value, 'cache_control')) return value;
130
- const existing = value['cache_control'];
131
- if (existing === undefined) {
132
- const next = { ...value };
133
- Reflect.deleteProperty(next, 'cache_control');
134
- return next;
135
- }
136
- incomingBreakpoints += 1;
137
- if (incomingBreakpoints > FUSION_CLAUDE_CACHE_BREAKPOINT_LIMIT) {
138
- throw new Error(
139
- `Fusion Claude payload has ${String(incomingBreakpoints)} cache_control breakpoints; Anthropic supports at most ${String(FUSION_CLAUDE_CACHE_BREAKPOINT_LIMIT)}`,
140
- );
141
- }
142
- const control = validateCacheControl(existing);
143
- const next = { ...value };
144
- if (normalizedRetention === 'none') {
145
- Reflect.deleteProperty(next, 'cache_control');
146
- return next;
147
- }
148
- const normalizedControl = { ...control, type: 'ephemeral' };
149
- Reflect.deleteProperty(normalizedControl, 'ttl');
150
- if (normalizedRetention === 'long') Object.assign(normalizedControl, { ttl: '1h' });
151
- next['cache_control'] = normalizedControl;
152
- outputBreakpoints += 1;
153
- return next;
154
- };
155
-
156
- const system = unknownArray(input.payload['system']);
157
- const tools = unknownArray(input.payload['tools']);
158
- const messages = unknownArray(input.payload['messages']);
159
- const payload = {
160
- ...input.payload,
161
- ...(system === undefined ? {} : { system: system.map(normalizeBlock) }),
162
- ...(tools === undefined ? {} : { tools: tools.map(normalizeBlock) }),
163
- ...(messages === undefined
164
- ? {}
165
- : {
166
- messages: messages.map((message) => {
167
- if (!isRecord(message)) return message;
168
- const content = unknownArray(message['content']);
169
- return content === undefined
170
- ? message
171
- : { ...message, content: content.map(normalizeBlock) };
172
- }),
173
- }),
174
- };
175
- if (outputBreakpoints > FUSION_CLAUDE_CACHE_BREAKPOINT_LIMIT) {
176
- throw new Error(
177
- `Fusion Claude payload produced ${String(outputBreakpoints)} cache_control breakpoints; Anthropic supports at most ${String(FUSION_CLAUDE_CACHE_BREAKPOINT_LIMIT)}`,
178
- );
179
- }
180
-
181
- return {
182
- payload,
183
- observation: {
184
- schema_version: FUSION_CLAUDE_CACHE_OBSERVATION_SCHEMA_VERSION,
185
- applicability: 'anthropic',
186
- source: policy.source,
187
- requested_retention: policy.retention,
188
- effective_retention: outputBreakpoints === 0 ? 'none' : normalizedRetention,
189
- breakpoint_count: outputBreakpoints,
190
- request_ordinal: requestOrdinal,
191
- },
192
- };
193
- }
194
-
195
- export function nonAnthropicFusionCacheObservation(
196
- requestOrdinal: number,
197
- ): FusionClaudeCacheObservation {
198
- return {
199
- schema_version: FUSION_CLAUDE_CACHE_OBSERVATION_SCHEMA_VERSION,
200
- applicability: 'not_applicable',
201
- source: 'not_applicable',
202
- requested_retention: null,
203
- effective_retention: null,
204
- breakpoint_count: 0,
205
- request_ordinal: requireRequestOrdinal(requestOrdinal),
206
- };
207
- }
@@ -1,91 +0,0 @@
1
- import { createHash } from 'node:crypto';
2
- import { canonicalJson } from '../attested-pi-run.js';
3
- import { normalizeFusionDeclaredSources, type DeclaredFusionSourceInput } from './source-policy.js';
4
- import {
5
- FUSION_INPUT_SCHEMA_VERSION,
6
- FusionError,
7
- type FusionCanonicalRequestV3,
8
- type FusionCleanTaskCanonicalInputV5,
9
- type FusionDeclaredSourceV1,
10
- type FusionSource,
11
- type FusionWorkflowId,
12
- } from './types.js';
13
-
14
- export interface BuildFusionCleanTaskInputOptions {
15
- cwd: string;
16
- source: FusionSource;
17
- request: string;
18
- workflow: Exclude<FusionWorkflowId, 'reason'>;
19
- declaredSources?: readonly DeclaredFusionSourceInput[] | undefined;
20
- }
21
-
22
- /**
23
- * Public v1 clean builder. It is deliberately pure: callers provide cwd and
24
- * normalized request text explicitly, and this module has no dependency on Pi
25
- * session, snapshot, parent-context, or visible-conversation APIs.
26
- */
27
- export const buildCleanFusionCanonicalInput = buildFusionCleanTaskCanonicalInput;
28
-
29
- export interface BuiltFusionCleanTaskCanonicalInput {
30
- input: FusionCleanTaskCanonicalInputV5;
31
- serialized: string;
32
- declaredSources: readonly FusionDeclaredSourceV1[];
33
- transcriptLeafId: null;
34
- }
35
-
36
- function sha256Text(value: string): string {
37
- return createHash('sha256').update(Buffer.from(value, 'utf8')).digest('hex');
38
- }
39
-
40
- export function buildFusionCleanTaskCanonicalInput(
41
- options: BuildFusionCleanTaskInputOptions,
42
- ): BuiltFusionCleanTaskCanonicalInput {
43
- if (options.request.trim().length === 0) {
44
- throw new FusionError('fusion request must not be blank', {
45
- code: 'context_capture_failed',
46
- childCreated: false,
47
- });
48
- }
49
- if (!['investigate', 'research', 'validate'].includes(options.workflow)) {
50
- throw new FusionError('clean-task fusion input is available only to investigate, research, and validate workflows', {
51
- code: 'context_capture_failed',
52
- childCreated: false,
53
- });
54
- }
55
- const declaredSources = normalizeFusionDeclaredSources(options.declaredSources ?? []);
56
- if (options.workflow === 'research' && declaredSources.length === 0) {
57
- throw new FusionError('fusion research requires at least one declared source URL and purpose', {
58
- code: 'context_capture_failed',
59
- childCreated: false,
60
- });
61
- }
62
- if (options.workflow !== 'research' && declaredSources.length > 0) {
63
- throw new FusionError('declared sources are accepted only by the research workflow', {
64
- code: 'context_capture_failed',
65
- childCreated: false,
66
- });
67
- }
68
- const request: FusionCanonicalRequestV3 = {
69
- source: options.source,
70
- authority: 'explicit_text',
71
- text: options.request,
72
- sha256: sha256Text(options.request),
73
- };
74
- const input: FusionCleanTaskCanonicalInputV5 = {
75
- schema_version: FUSION_INPUT_SCHEMA_VERSION,
76
- workflow: options.workflow,
77
- cwd: options.cwd,
78
- request,
79
- context: {
80
- kind: 'clean_task',
81
- policy_id: 'fusion-clean-task-v1',
82
- declared_sources: declaredSources,
83
- },
84
- };
85
- return {
86
- input,
87
- serialized: canonicalJson(input),
88
- declaredSources,
89
- transcriptLeafId: null,
90
- };
91
- }