pi-background-tasks 0.7.3 → 0.7.6
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/PUBLISHING.md +10 -7
- package/README.md +98 -6
- package/TESTING.md +13 -3
- package/TEST_PLAN.md +10 -5
- package/package.json +10 -8
- package/src/core/attested-pi-run.ts +19 -35
- package/src/core/common.ts +124 -8
- package/src/core/durable-fs.ts +400 -0
- package/src/core/fusion/artifacts.ts +20 -36
- package/src/core/fusion/budget.ts +656 -0
- package/src/core/fusion/config.ts +4 -36
- package/src/core/fusion/context.ts +507 -47
- package/src/core/fusion/orchestrator.ts +43 -4
- package/src/core/fusion/pi-child.ts +28 -6
- package/src/core/fusion/prompts.ts +21 -7
- package/src/core/fusion/types.ts +284 -4
- package/src/core/pi-launch.ts +225 -0
- package/src/core/registry.ts +507 -56
- package/src/core/windows-taskkill.ts +250 -0
- package/src/fusion-extension.ts +6 -1
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { createHash
|
|
2
|
-
import {
|
|
3
|
-
import { chmod, mkdir, open, readFile, rm, writeFile } from 'node:fs/promises';
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { chmod, mkdir, open, readFile, rm } from 'node:fs/promises';
|
|
4
3
|
import { basename, dirname, join } from 'node:path';
|
|
5
4
|
import { getAgentDir } from '@earendil-works/pi-coding-agent';
|
|
6
5
|
import type { Api, Model } from '@earendil-works/pi-ai';
|
|
7
6
|
import { isJsonObject, parseJsonText, type JsonObject } from '../common.js';
|
|
7
|
+
import { replaceFileDurable } from '../durable-fs.js';
|
|
8
8
|
import {
|
|
9
9
|
FUSION_MODEL_CONFIG_SCHEMA_VERSION,
|
|
10
10
|
FusionError,
|
|
@@ -260,25 +260,6 @@ export function resolveFusionModels(input: ResolveFusionModelsInput): ResolvedFu
|
|
|
260
260
|
};
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
-
async function fsyncFile(path: string): Promise<void> {
|
|
264
|
-
const handle = await open(path, 'r');
|
|
265
|
-
try {
|
|
266
|
-
await handle.sync();
|
|
267
|
-
} finally {
|
|
268
|
-
await handle.close();
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
async function fsyncDirectory(path: string): Promise<void> {
|
|
273
|
-
if (process.platform === 'win32') return;
|
|
274
|
-
const fd = openSync(path, 'r');
|
|
275
|
-
try {
|
|
276
|
-
fsyncSync(fd);
|
|
277
|
-
} finally {
|
|
278
|
-
closeSync(fd);
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
263
|
async function delay(ms: number): Promise<void> {
|
|
283
264
|
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
284
265
|
}
|
|
@@ -348,20 +329,7 @@ export async function saveFusionModelConfig(
|
|
|
348
329
|
childCreated: false,
|
|
349
330
|
});
|
|
350
331
|
}
|
|
351
|
-
|
|
352
|
-
dir,
|
|
353
|
-
`.${basename(path)}.${String(process.pid)}.${randomBytes(6).toString('hex')}.tmp`,
|
|
354
|
-
);
|
|
355
|
-
const text = prettyConfig(config);
|
|
356
|
-
try {
|
|
357
|
-
await writeFile(tmp, text, { encoding: 'utf8', mode: 0o600 });
|
|
358
|
-
await fsyncFile(tmp);
|
|
359
|
-
renameSync(tmp, path);
|
|
360
|
-
await fsyncDirectory(dir);
|
|
361
|
-
} catch (error) {
|
|
362
|
-
await rm(tmp, { force: true });
|
|
363
|
-
throw error;
|
|
364
|
-
}
|
|
332
|
+
await replaceFileDurable(path, prettyConfig(config));
|
|
365
333
|
return revisionForPath(path);
|
|
366
334
|
});
|
|
367
335
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
1
2
|
import {
|
|
2
3
|
buildSessionContext,
|
|
3
4
|
convertToLlm,
|
|
@@ -7,10 +8,31 @@ import type { Message } from '@earendil-works/pi-ai';
|
|
|
7
8
|
import { canonicalJson } from '../attested-pi-run.js';
|
|
8
9
|
import { isJsonObject, type JsonObject } from '../common.js';
|
|
9
10
|
import {
|
|
11
|
+
FUSION_BRANCH_FILTER_ID,
|
|
12
|
+
FUSION_COMMAND_CONTEXT_POLICY_ID,
|
|
13
|
+
FUSION_CONTEXT_LEDGER_SCHEMA_VERSION,
|
|
14
|
+
FUSION_CONTEXT_TRANSFORM_ID,
|
|
15
|
+
FUSION_IMAGE_OMISSION_PREFIX,
|
|
10
16
|
FUSION_INPUT_SCHEMA_VERSION,
|
|
17
|
+
FUSION_TOOL_CONTEXT_POLICY_ID,
|
|
11
18
|
FusionError,
|
|
12
|
-
type
|
|
19
|
+
type FusionBranchFilterDescriptor,
|
|
20
|
+
type FusionCanonicalInputV3,
|
|
21
|
+
type FusionCanonicalRequestV3,
|
|
22
|
+
type FusionContextOmissionLedgerV2,
|
|
23
|
+
type FusionContextPolicyDescriptor,
|
|
24
|
+
type FusionContextProjectionMapEntry,
|
|
25
|
+
type FusionConversationProjectionV3,
|
|
26
|
+
type FusionOmittedEventKind,
|
|
27
|
+
type FusionOmittedEventRecord,
|
|
28
|
+
type FusionOmittedRunCounts,
|
|
29
|
+
type FusionProjectionAccounting,
|
|
30
|
+
type FusionProjectionEntry,
|
|
31
|
+
type FusionProjectionOmissionEntry,
|
|
32
|
+
type FusionProjectionTextEntry,
|
|
33
|
+
type FusionRequestAuthority,
|
|
13
34
|
type FusionSource,
|
|
35
|
+
type FusionToolCallNameCount,
|
|
14
36
|
} from './types.js';
|
|
15
37
|
|
|
16
38
|
export const FUSION_BRAINSTORM_TOOL_NAME = 'fusion_brainstorm';
|
|
@@ -35,8 +57,9 @@ export interface BuildFusionCanonicalInputOptions {
|
|
|
35
57
|
}
|
|
36
58
|
|
|
37
59
|
export interface BuiltFusionCanonicalInput {
|
|
38
|
-
input:
|
|
60
|
+
input: FusionCanonicalInputV3;
|
|
39
61
|
serialized: string;
|
|
62
|
+
ledger: FusionContextOmissionLedgerV2;
|
|
40
63
|
transcriptLeafId: string | null;
|
|
41
64
|
}
|
|
42
65
|
|
|
@@ -90,25 +113,32 @@ function messageContainsToolCall(
|
|
|
90
113
|
return false;
|
|
91
114
|
}
|
|
92
115
|
|
|
116
|
+
interface EffectiveLeaf {
|
|
117
|
+
leafId: string | null;
|
|
118
|
+
activeToolCallLeafExcluded: boolean;
|
|
119
|
+
}
|
|
120
|
+
|
|
93
121
|
function effectiveLeafForTool(
|
|
94
122
|
sessionManager: FusionReadonlySessionManager,
|
|
95
123
|
toolCallId: string | undefined,
|
|
96
124
|
toolName: string,
|
|
97
|
-
):
|
|
125
|
+
): EffectiveLeaf {
|
|
98
126
|
const leaf = sessionManager.getLeafEntry();
|
|
99
|
-
if (leaf === undefined)
|
|
127
|
+
if (leaf === undefined)
|
|
128
|
+
return { leafId: sessionManager.getLeafId(), activeToolCallLeafExcluded: false };
|
|
100
129
|
const message = entryMessage(leaf);
|
|
101
130
|
if (message !== undefined && messageContainsToolCall(message, toolCallId, toolName)) {
|
|
102
|
-
return leaf.parentId;
|
|
131
|
+
return { leafId: leaf.parentId, activeToolCallLeafExcluded: true };
|
|
103
132
|
}
|
|
104
|
-
return sessionManager.getLeafId();
|
|
133
|
+
return { leafId: sessionManager.getLeafId(), activeToolCallLeafExcluded: false };
|
|
105
134
|
}
|
|
106
135
|
|
|
107
|
-
function
|
|
136
|
+
function effectiveLeaf(
|
|
108
137
|
sessionManager: FusionReadonlySessionManager,
|
|
109
138
|
options: BuildFusionCanonicalInputOptions,
|
|
110
|
-
):
|
|
111
|
-
if (options.source !== 'tool')
|
|
139
|
+
): EffectiveLeaf {
|
|
140
|
+
if (options.source !== 'tool')
|
|
141
|
+
return { leafId: sessionManager.getLeafId(), activeToolCallLeafExcluded: false };
|
|
112
142
|
return effectiveLeafForTool(
|
|
113
143
|
sessionManager,
|
|
114
144
|
options.toolCallId,
|
|
@@ -116,42 +146,453 @@ function effectiveLeafId(
|
|
|
116
146
|
);
|
|
117
147
|
}
|
|
118
148
|
|
|
119
|
-
function
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
149
|
+
function utf8Bytes(value: string): number {
|
|
150
|
+
return Buffer.byteLength(value, 'utf8');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function sha256Hex(bytes: Buffer): string {
|
|
154
|
+
return createHash('sha256').update(bytes).digest('hex');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function sha256Text(value: string): string {
|
|
158
|
+
return sha256Hex(Buffer.from(value, 'utf8'));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function uint64be(value: number): Buffer {
|
|
162
|
+
const out = Buffer.alloc(8);
|
|
163
|
+
out.writeBigUInt64BE(BigInt(value));
|
|
164
|
+
return out;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Length-prefixed framing so concatenated fields cannot collide across boundaries. */
|
|
168
|
+
function lengthPrefixed(bytes: Buffer): Buffer {
|
|
169
|
+
return Buffer.concat([uint64be(bytes.length), bytes]);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function ledgerLeafHash(index: number, record: FusionOmittedEventRecord): Buffer {
|
|
173
|
+
return createHash('sha256')
|
|
174
|
+
.update(Buffer.from('pi-fusion-ledger-leaf-v1\0', 'utf8'))
|
|
175
|
+
.update(uint64be(index))
|
|
176
|
+
.update(lengthPrefixed(Buffer.from(canonicalJson(record), 'utf8')))
|
|
177
|
+
.digest();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function ledgerRootHash(leaves: readonly Buffer[]): string {
|
|
181
|
+
const hash = createHash('sha256')
|
|
182
|
+
.update(Buffer.from('pi-fusion-ledger-root-v1\0', 'utf8'))
|
|
183
|
+
.update(uint64be(leaves.length));
|
|
184
|
+
for (const leaf of leaves) hash.update(leaf);
|
|
185
|
+
return hash.digest('hex');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function unsupportedBlock(label: string): FusionError {
|
|
189
|
+
return new FusionError(
|
|
190
|
+
`fusion context projection encountered an unsupported conversation block: ${label}`,
|
|
191
|
+
{ code: 'context_policy_unsupported_block', childCreated: false },
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function contextPolicyId(source: FusionSource): string {
|
|
196
|
+
return source === 'tool' ? FUSION_TOOL_CONTEXT_POLICY_ID : FUSION_COMMAND_CONTEXT_POLICY_ID;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function requestAuthority(source: FusionSource): FusionRequestAuthority {
|
|
200
|
+
return source === 'tool' ? 'explicit_text' : 'directive_over_projected_conversation';
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function policyDescriptor(source: FusionSource): FusionContextPolicyDescriptor {
|
|
204
|
+
return {
|
|
205
|
+
id: contextPolicyId(source),
|
|
206
|
+
transform: FUSION_CONTEXT_TRANSFORM_ID,
|
|
207
|
+
version: 2,
|
|
208
|
+
receipt_format: 'omitted_activity.v2',
|
|
209
|
+
user_text: 'verbatim',
|
|
210
|
+
assistant_text: 'verbatim',
|
|
211
|
+
assistant_thinking: 'ledger_only',
|
|
212
|
+
tool_call_arguments: 'ledger_only',
|
|
213
|
+
tool_results: 'ledger_only',
|
|
214
|
+
tool_payload_preview_bytes: 0,
|
|
215
|
+
images: 'marker_or_ledger_only',
|
|
216
|
+
unknown_block_behavior: 'error',
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Accumulates omitted-event ledger rows and turns maximal contiguous non-image
|
|
222
|
+
* omission runs into compact, source-ordered receipts inside the canonical input.
|
|
223
|
+
*/
|
|
224
|
+
class ProjectionBuilder {
|
|
225
|
+
private readonly entries: FusionProjectionEntry[] = [];
|
|
226
|
+
private readonly ledger: FusionOmittedEventRecord[] = [];
|
|
227
|
+
private readonly leaves: Buffer[] = [];
|
|
228
|
+
private readonly projectionMap: FusionContextProjectionMapEntry[] = [];
|
|
229
|
+
private readonly toolCallNames = new Map<string, number>();
|
|
230
|
+
private pendingCounts: Required<FusionOmittedRunCounts> | undefined;
|
|
231
|
+
private pendingBytes = 0;
|
|
232
|
+
private pendingLedgerFirst = 0;
|
|
233
|
+
private pendingLedgerLast = 0;
|
|
234
|
+
private pendingSourceFirst = 0;
|
|
235
|
+
private pendingSourceLast = 0;
|
|
236
|
+
|
|
237
|
+
includedUserTextBytes = 0;
|
|
238
|
+
includedAssistantTextBytes = 0;
|
|
239
|
+
includedImageMarkers = 0;
|
|
240
|
+
emptyTextBlocks = 0;
|
|
241
|
+
|
|
242
|
+
addText(entry: FusionProjectionTextEntry): void {
|
|
243
|
+
this.flush();
|
|
244
|
+
this.entries.push(entry);
|
|
245
|
+
if (entry.role === 'user') this.includedUserTextBytes += utf8Bytes(entry.text);
|
|
246
|
+
else this.includedAssistantTextBytes += utf8Bytes(entry.text);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
addOmission(
|
|
250
|
+
sourceOrdinal: number,
|
|
251
|
+
blockOrdinal: number,
|
|
252
|
+
kind: FusionOmittedEventKind,
|
|
253
|
+
payload: Buffer,
|
|
254
|
+
extra: { toolName?: string; toolCallId?: string; mimeType?: string } = {},
|
|
255
|
+
): void {
|
|
256
|
+
const index = this.ledger.length;
|
|
257
|
+
const record: FusionOmittedEventRecord = {
|
|
258
|
+
index,
|
|
259
|
+
source_ordinal: sourceOrdinal,
|
|
260
|
+
block_ordinal: blockOrdinal,
|
|
261
|
+
kind,
|
|
262
|
+
payload_bytes: payload.length,
|
|
263
|
+
payload_sha256: sha256Hex(payload),
|
|
264
|
+
};
|
|
265
|
+
if (extra.toolName !== undefined) record.tool_name = extra.toolName;
|
|
266
|
+
if (extra.toolCallId !== undefined) record.tool_call_id = extra.toolCallId;
|
|
267
|
+
if (extra.mimeType !== undefined) record.mime_type = extra.mimeType;
|
|
268
|
+
this.ledger.push(record);
|
|
269
|
+
this.leaves.push(ledgerLeafHash(index, record));
|
|
270
|
+
|
|
271
|
+
if (extra.toolName !== undefined && kind === 'tool_call') {
|
|
272
|
+
this.toolCallNames.set(extra.toolName, (this.toolCallNames.get(extra.toolName) ?? 0) + 1);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (kind === 'tool_result_image') {
|
|
276
|
+
this.flush();
|
|
277
|
+
this.addLedgerOnlyImageMap(index);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (this.pendingCounts === undefined) {
|
|
282
|
+
this.pendingCounts = {
|
|
283
|
+
assistant_thinking: 0,
|
|
284
|
+
tool_calls: 0,
|
|
285
|
+
tool_result_texts: 0,
|
|
286
|
+
};
|
|
287
|
+
this.pendingBytes = 0;
|
|
288
|
+
this.pendingLedgerFirst = index;
|
|
289
|
+
this.pendingSourceFirst = sourceOrdinal;
|
|
290
|
+
}
|
|
291
|
+
this.pendingLedgerLast = index;
|
|
292
|
+
this.pendingSourceLast = sourceOrdinal;
|
|
293
|
+
this.pendingBytes += payload.length;
|
|
294
|
+
if (kind === 'assistant_thinking') this.pendingCounts.assistant_thinking += 1;
|
|
295
|
+
else if (kind === 'tool_call') this.pendingCounts.tool_calls += 1;
|
|
296
|
+
else this.pendingCounts.tool_result_texts += 1;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private addLedgerOnlyImageMap(index: number): void {
|
|
300
|
+
const previous = this.projectionMap[this.projectionMap.length - 1];
|
|
301
|
+
if (
|
|
302
|
+
previous !== undefined &&
|
|
303
|
+
previous.entry_kind === 'ledger_only_tool_result_image' &&
|
|
304
|
+
previous.ledger_index_last + 1 === index
|
|
305
|
+
) {
|
|
306
|
+
previous.ledger_index_last = index;
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
this.projectionMap.push({
|
|
310
|
+
entry_kind: 'ledger_only_tool_result_image',
|
|
311
|
+
ledger_index_first: index,
|
|
312
|
+
ledger_index_last: index,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
private flush(): void {
|
|
317
|
+
const counts = this.pendingCounts;
|
|
318
|
+
if (counts === undefined) return;
|
|
319
|
+
const canonicalEntryIndex = this.entries.length;
|
|
320
|
+
const entry: FusionProjectionOmissionEntry = {
|
|
321
|
+
at: [this.pendingSourceFirst, this.pendingSourceLast],
|
|
322
|
+
bytes: this.pendingBytes,
|
|
323
|
+
counts: compactCounts(counts),
|
|
324
|
+
kind: 'omitted_activity',
|
|
325
|
+
};
|
|
326
|
+
this.entries.push(entry);
|
|
327
|
+
this.projectionMap.push({
|
|
328
|
+
canonical_entry_index: canonicalEntryIndex,
|
|
329
|
+
entry_kind: 'omitted_activity',
|
|
330
|
+
ledger_index_first: this.pendingLedgerFirst,
|
|
331
|
+
ledger_index_last: this.pendingLedgerLast,
|
|
332
|
+
});
|
|
333
|
+
this.pendingCounts = undefined;
|
|
334
|
+
this.pendingBytes = 0;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
finish(
|
|
338
|
+
source: FusionSource,
|
|
339
|
+
branchFilter: FusionBranchFilterDescriptor,
|
|
340
|
+
messageCount: number,
|
|
341
|
+
): { projection: FusionConversationProjectionV3; ledger: FusionContextOmissionLedgerV2 } {
|
|
342
|
+
this.flush();
|
|
343
|
+
const rootSha256 = ledgerRootHash(this.leaves);
|
|
344
|
+
let omittedRunCount = 0;
|
|
345
|
+
let includedTextEntries = 0;
|
|
346
|
+
let receiptBytes = 0;
|
|
347
|
+
for (const entry of this.entries) {
|
|
348
|
+
if (entry.kind === 'text') includedTextEntries += 1;
|
|
349
|
+
else {
|
|
350
|
+
omittedRunCount += 1;
|
|
351
|
+
receiptBytes += utf8Bytes(canonicalJson(entry));
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
let thinkingBytes = 0;
|
|
355
|
+
let toolCallCount = 0;
|
|
356
|
+
let toolArgumentBytes = 0;
|
|
357
|
+
let toolResultTextCount = 0;
|
|
358
|
+
let toolResultTextBytes = 0;
|
|
359
|
+
let toolResultImageCount = 0;
|
|
360
|
+
let toolResultImageBytes = 0;
|
|
361
|
+
for (const row of this.ledger) {
|
|
362
|
+
if (row.kind === 'assistant_thinking') thinkingBytes += row.payload_bytes;
|
|
363
|
+
else if (row.kind === 'tool_call') {
|
|
364
|
+
toolCallCount += 1;
|
|
365
|
+
toolArgumentBytes += row.payload_bytes;
|
|
366
|
+
} else if (row.kind === 'tool_result_text') {
|
|
367
|
+
toolResultTextCount += 1;
|
|
368
|
+
toolResultTextBytes += row.payload_bytes;
|
|
369
|
+
} else {
|
|
370
|
+
toolResultImageCount += 1;
|
|
371
|
+
toolResultImageBytes += row.payload_bytes;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
const toolCallNames: FusionToolCallNameCount[] = [...this.toolCallNames.entries()]
|
|
375
|
+
.map(([name, calls]) => ({ name, calls }))
|
|
376
|
+
.sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));
|
|
377
|
+
const accounting: FusionProjectionAccounting = {
|
|
378
|
+
message_count: messageCount,
|
|
379
|
+
included_text_entry_count: includedTextEntries,
|
|
380
|
+
included_user_text_bytes: this.includedUserTextBytes,
|
|
381
|
+
included_assistant_text_bytes: this.includedAssistantTextBytes,
|
|
382
|
+
included_image_marker_count: this.includedImageMarkers,
|
|
383
|
+
empty_text_block_count: this.emptyTextBlocks,
|
|
384
|
+
omitted_run_count: omittedRunCount,
|
|
385
|
+
omitted_event_count: this.ledger.length,
|
|
386
|
+
omitted_thinking_bytes: thinkingBytes,
|
|
387
|
+
omitted_tool_call_count: toolCallCount,
|
|
388
|
+
omitted_tool_call_argument_bytes: toolArgumentBytes,
|
|
389
|
+
omitted_tool_result_text_count: toolResultTextCount,
|
|
390
|
+
omitted_tool_result_text_bytes: toolResultTextBytes,
|
|
391
|
+
omitted_tool_result_image_count: toolResultImageCount,
|
|
392
|
+
omitted_tool_result_image_bytes: toolResultImageBytes,
|
|
393
|
+
tool_call_names: toolCallNames,
|
|
394
|
+
ledger_entry_count: this.ledger.length,
|
|
395
|
+
ledger_root_sha256: rootSha256,
|
|
396
|
+
omission_receipt_utf8_bytes: receiptBytes,
|
|
397
|
+
};
|
|
398
|
+
return {
|
|
399
|
+
projection: {
|
|
400
|
+
policy: policyDescriptor(source),
|
|
401
|
+
branch_filter: branchFilter,
|
|
402
|
+
entries: this.entries,
|
|
403
|
+
accounting,
|
|
404
|
+
},
|
|
405
|
+
ledger: {
|
|
406
|
+
schema_version: FUSION_CONTEXT_LEDGER_SCHEMA_VERSION,
|
|
407
|
+
policy_id: contextPolicyId(source),
|
|
408
|
+
transform: FUSION_CONTEXT_TRANSFORM_ID,
|
|
409
|
+
entries: this.ledger,
|
|
410
|
+
projection_map: this.projectionMap,
|
|
411
|
+
root_sha256: rootSha256,
|
|
412
|
+
},
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function imageMarker(mimeType: string): string {
|
|
418
|
+
return `${FUSION_IMAGE_OMISSION_PREFIX}${mimeType}]`;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Fixed policy rule: a zero-valued kind is absent from the receipt rather than
|
|
423
|
+
* serialized as `0`. This keeps receipts compact without losing information —
|
|
424
|
+
* absent is defined by the policy version to mean exactly zero — and it is
|
|
425
|
+
* applied unconditionally, never adaptively because an input happens to be large.
|
|
426
|
+
*/
|
|
427
|
+
function compactCounts(counts: Required<FusionOmittedRunCounts>): FusionOmittedRunCounts {
|
|
428
|
+
const out: FusionOmittedRunCounts = {};
|
|
429
|
+
if (counts.assistant_thinking > 0) out.assistant_thinking = counts.assistant_thinking;
|
|
430
|
+
if (counts.tool_calls > 0) out.tool_calls = counts.tool_calls;
|
|
431
|
+
if (counts.tool_result_texts > 0) out.tool_result_texts = counts.tool_result_texts;
|
|
432
|
+
return out;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Projects one user-role message. User text is retained verbatim; images stay
|
|
437
|
+
* marker-only so the child never receives raw bytes.
|
|
438
|
+
*/
|
|
439
|
+
function projectUserMessage(
|
|
440
|
+
builder: ProjectionBuilder,
|
|
441
|
+
content: Message['content'],
|
|
442
|
+
sourceOrdinal: number,
|
|
443
|
+
): void {
|
|
444
|
+
if (typeof content === 'string') {
|
|
445
|
+
if (content.length === 0) {
|
|
446
|
+
builder.emptyTextBlocks += 1;
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
builder.addText({
|
|
450
|
+
kind: 'text',
|
|
451
|
+
source_ordinal: sourceOrdinal,
|
|
452
|
+
block_ordinal: 0,
|
|
453
|
+
role: 'user',
|
|
454
|
+
text: content,
|
|
455
|
+
});
|
|
456
|
+
return;
|
|
126
457
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
for (const message of messages) {
|
|
133
|
-
if (message.role === 'user') {
|
|
134
|
-
const content = textContentForTranscript(message.content);
|
|
135
|
-
if (content.length > 0) parts.push(`[User]: ${content}`);
|
|
136
|
-
} else if (message.role === 'assistant') {
|
|
137
|
-
const thinkingParts: string[] = [];
|
|
138
|
-
const textParts: string[] = [];
|
|
139
|
-
const toolCalls: string[] = [];
|
|
140
|
-
for (const block of message.content) {
|
|
141
|
-
if (block.type === 'thinking') thinkingParts.push(block.thinking);
|
|
142
|
-
else if (block.type === 'text') textParts.push(block.text);
|
|
143
|
-
else if (block.type === 'toolCall')
|
|
144
|
-
toolCalls.push(`${block.name}(${canonicalJson(block.arguments)})`);
|
|
458
|
+
for (const [blockOrdinal, block] of content.entries()) {
|
|
459
|
+
if (block.type === 'text') {
|
|
460
|
+
if (block.text.length === 0) {
|
|
461
|
+
builder.emptyTextBlocks += 1;
|
|
462
|
+
continue;
|
|
145
463
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
464
|
+
builder.addText({
|
|
465
|
+
kind: 'text',
|
|
466
|
+
source_ordinal: sourceOrdinal,
|
|
467
|
+
block_ordinal: blockOrdinal,
|
|
468
|
+
role: 'user',
|
|
469
|
+
text: block.text,
|
|
470
|
+
});
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
if (block.type === 'image') {
|
|
474
|
+
builder.includedImageMarkers += 1;
|
|
475
|
+
builder.addText({
|
|
476
|
+
kind: 'text',
|
|
477
|
+
source_ordinal: sourceOrdinal,
|
|
478
|
+
block_ordinal: blockOrdinal,
|
|
479
|
+
role: 'user',
|
|
480
|
+
text: imageMarker(block.mimeType),
|
|
481
|
+
});
|
|
482
|
+
continue;
|
|
152
483
|
}
|
|
484
|
+
throw unsupportedBlock(`user block ${String(Reflect.get(block, 'type'))}`);
|
|
153
485
|
}
|
|
154
|
-
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function projectAssistantMessage(
|
|
489
|
+
builder: ProjectionBuilder,
|
|
490
|
+
content: Extract<Message, { role: 'assistant' }>['content'],
|
|
491
|
+
sourceOrdinal: number,
|
|
492
|
+
): void {
|
|
493
|
+
for (const [blockOrdinal, block] of content.entries()) {
|
|
494
|
+
if (block.type === 'text') {
|
|
495
|
+
if (block.text.length === 0) {
|
|
496
|
+
builder.emptyTextBlocks += 1;
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
builder.addText({
|
|
500
|
+
kind: 'text',
|
|
501
|
+
source_ordinal: sourceOrdinal,
|
|
502
|
+
block_ordinal: blockOrdinal,
|
|
503
|
+
role: 'assistant',
|
|
504
|
+
text: block.text,
|
|
505
|
+
});
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
if (block.type === 'thinking') {
|
|
509
|
+
builder.addOmission(
|
|
510
|
+
sourceOrdinal,
|
|
511
|
+
blockOrdinal,
|
|
512
|
+
'assistant_thinking',
|
|
513
|
+
Buffer.from(block.thinking, 'utf8'),
|
|
514
|
+
);
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
if (block.type === 'toolCall') {
|
|
518
|
+
builder.addOmission(
|
|
519
|
+
sourceOrdinal,
|
|
520
|
+
blockOrdinal,
|
|
521
|
+
'tool_call',
|
|
522
|
+
Buffer.from(canonicalJson(block.arguments), 'utf8'),
|
|
523
|
+
{ toolName: block.name, toolCallId: block.id },
|
|
524
|
+
);
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
throw unsupportedBlock(`assistant block ${String(Reflect.get(block, 'type'))}`);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function projectToolResultMessage(
|
|
532
|
+
builder: ProjectionBuilder,
|
|
533
|
+
message: Extract<Message, { role: 'toolResult' }>,
|
|
534
|
+
sourceOrdinal: number,
|
|
535
|
+
): void {
|
|
536
|
+
const content = message.content;
|
|
537
|
+
if (typeof content === 'string') {
|
|
538
|
+
builder.addOmission(
|
|
539
|
+
sourceOrdinal,
|
|
540
|
+
0,
|
|
541
|
+
'tool_result_text',
|
|
542
|
+
Buffer.from(content, 'utf8'),
|
|
543
|
+
{ toolName: message.toolName, toolCallId: message.toolCallId },
|
|
544
|
+
);
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
for (const [blockOrdinal, block] of content.entries()) {
|
|
548
|
+
if (block.type === 'text') {
|
|
549
|
+
builder.addOmission(
|
|
550
|
+
sourceOrdinal,
|
|
551
|
+
blockOrdinal,
|
|
552
|
+
'tool_result_text',
|
|
553
|
+
Buffer.from(block.text, 'utf8'),
|
|
554
|
+
{ toolName: message.toolName, toolCallId: message.toolCallId },
|
|
555
|
+
);
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
if (block.type === 'image') {
|
|
559
|
+
builder.addOmission(
|
|
560
|
+
sourceOrdinal,
|
|
561
|
+
blockOrdinal,
|
|
562
|
+
'tool_result_image',
|
|
563
|
+
Buffer.from(block.data, 'utf8'),
|
|
564
|
+
{
|
|
565
|
+
toolName: message.toolName,
|
|
566
|
+
toolCallId: message.toolCallId,
|
|
567
|
+
mimeType: block.mimeType,
|
|
568
|
+
},
|
|
569
|
+
);
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
throw unsupportedBlock(`tool result block ${String(Reflect.get(block, 'type'))}`);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Deterministic conversation projection. Every retained source block receives
|
|
578
|
+
* exactly one disposition: included verbatim, or represented as an omission
|
|
579
|
+
* ledger row. Unknown block types fail loudly instead of disappearing.
|
|
580
|
+
*/
|
|
581
|
+
export function projectFusionConversation(
|
|
582
|
+
messages: readonly Message[],
|
|
583
|
+
source: FusionSource,
|
|
584
|
+
branchFilter: FusionBranchFilterDescriptor,
|
|
585
|
+
): { projection: FusionConversationProjectionV3; ledger: FusionContextOmissionLedgerV2 } {
|
|
586
|
+
const builder = new ProjectionBuilder();
|
|
587
|
+
for (const [sourceOrdinal, message] of messages.entries()) {
|
|
588
|
+
if (message.role === 'user') projectUserMessage(builder, message.content, sourceOrdinal);
|
|
589
|
+
else if (message.role === 'assistant')
|
|
590
|
+
projectAssistantMessage(builder, message.content, sourceOrdinal);
|
|
591
|
+
else if (message.role === 'toolResult')
|
|
592
|
+
projectToolResultMessage(builder, message, sourceOrdinal);
|
|
593
|
+
else throw unsupportedBlock(`message role ${String(Reflect.get(message, 'role'))}`);
|
|
594
|
+
}
|
|
595
|
+
return builder.finish(source, branchFilter, messages.length);
|
|
155
596
|
}
|
|
156
597
|
|
|
157
598
|
export function buildFusionCanonicalInput(
|
|
@@ -165,15 +606,34 @@ export function buildFusionCanonicalInput(
|
|
|
165
606
|
});
|
|
166
607
|
}
|
|
167
608
|
const entries = ctx.sessionManager.getEntries();
|
|
168
|
-
const
|
|
169
|
-
const sessionContext = buildSessionContext(entries, leafId, entriesById(entries));
|
|
609
|
+
const leaf = effectiveLeaf(ctx.sessionManager, options);
|
|
610
|
+
const sessionContext = buildSessionContext(entries, leaf.leafId, entriesById(entries));
|
|
170
611
|
const llmMessages = convertToLlm(sessionContext.messages);
|
|
171
|
-
const
|
|
612
|
+
const toolName = options.toolName ?? FUSION_BRAINSTORM_TOOL_NAME;
|
|
613
|
+
const branchFilter: FusionBranchFilterDescriptor = {
|
|
614
|
+
id: FUSION_BRANCH_FILTER_ID,
|
|
615
|
+
tool_name: toolName,
|
|
616
|
+
tool_call_id: options.source === 'tool' ? (options.toolCallId ?? null) : null,
|
|
617
|
+
active_tool_call_leaf_excluded: leaf.activeToolCallLeafExcluded,
|
|
618
|
+
};
|
|
619
|
+
const projected = projectFusionConversation(llmMessages, options.source, branchFilter);
|
|
620
|
+
const request: FusionCanonicalRequestV3 = {
|
|
621
|
+
source: options.source,
|
|
622
|
+
authority: requestAuthority(options.source),
|
|
623
|
+
text: options.request,
|
|
624
|
+
sha256: sha256Text(options.request),
|
|
625
|
+
};
|
|
626
|
+
const input: FusionCanonicalInputV3 = {
|
|
172
627
|
schema_version: FUSION_INPUT_SCHEMA_VERSION,
|
|
173
628
|
cwd: ctx.cwd,
|
|
174
629
|
system_prompt: ctx.getSystemPrompt(),
|
|
175
|
-
|
|
176
|
-
|
|
630
|
+
request,
|
|
631
|
+
conversation_projection: projected.projection,
|
|
632
|
+
};
|
|
633
|
+
return {
|
|
634
|
+
input,
|
|
635
|
+
serialized: canonicalJson(input),
|
|
636
|
+
ledger: projected.ledger,
|
|
637
|
+
transcriptLeafId: leaf.leafId,
|
|
177
638
|
};
|
|
178
|
-
return { input, serialized: canonicalJson(input), transcriptLeafId: leafId };
|
|
179
639
|
}
|