pi-background-tasks 0.7.3 → 0.7.4
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 +78 -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 +372 -0
- package/src/core/fusion/config.ts +4 -36
- package/src/core/fusion/context.ts +508 -47
- package/src/core/fusion/orchestrator.ts +41 -4
- package/src/core/fusion/pi-child.ts +28 -6
- package/src/core/fusion/prompts.ts +21 -7
- package/src/core/fusion/types.ts +258 -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 +4 -1
|
@@ -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 FusionCanonicalInputV2,
|
|
21
|
+
type FusionCanonicalRequestV2,
|
|
22
|
+
type FusionContextOmissionLedgerV1,
|
|
23
|
+
type FusionContextPolicyDescriptor,
|
|
24
|
+
type FusionConversationProjectionV2,
|
|
25
|
+
type FusionOmittedEventKind,
|
|
26
|
+
type FusionOmittedEventRecord,
|
|
27
|
+
type FusionOmittedRunBytes,
|
|
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: FusionCanonicalInputV2;
|
|
39
61
|
serialized: string;
|
|
62
|
+
ledger: FusionContextOmissionLedgerV1;
|
|
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,454 @@ 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 ledgerRunHash(leaves: readonly Buffer[], first: number): string {
|
|
181
|
+
const hash = createHash('sha256')
|
|
182
|
+
.update(Buffer.from('pi-fusion-ledger-run-v1\0', 'utf8'))
|
|
183
|
+
.update(uint64be(first))
|
|
184
|
+
.update(uint64be(leaves.length));
|
|
185
|
+
for (const leaf of leaves) hash.update(leaf);
|
|
186
|
+
return hash.digest('hex');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function ledgerRootHash(leaves: readonly Buffer[]): string {
|
|
190
|
+
const hash = createHash('sha256')
|
|
191
|
+
.update(Buffer.from('pi-fusion-ledger-root-v1\0', 'utf8'))
|
|
192
|
+
.update(uint64be(leaves.length));
|
|
193
|
+
for (const leaf of leaves) hash.update(leaf);
|
|
194
|
+
return hash.digest('hex');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function unsupportedBlock(label: string): FusionError {
|
|
198
|
+
return new FusionError(
|
|
199
|
+
`fusion context projection encountered an unsupported conversation block: ${label}`,
|
|
200
|
+
{ code: 'context_policy_unsupported_block', childCreated: false },
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function contextPolicyId(source: FusionSource): string {
|
|
205
|
+
return source === 'tool' ? FUSION_TOOL_CONTEXT_POLICY_ID : FUSION_COMMAND_CONTEXT_POLICY_ID;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function requestAuthority(source: FusionSource): FusionRequestAuthority {
|
|
209
|
+
return source === 'tool' ? 'explicit_text' : 'directive_over_projected_conversation';
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function policyDescriptor(source: FusionSource): FusionContextPolicyDescriptor {
|
|
213
|
+
return {
|
|
214
|
+
id: contextPolicyId(source),
|
|
215
|
+
transform: FUSION_CONTEXT_TRANSFORM_ID,
|
|
216
|
+
version: 1,
|
|
217
|
+
user_text: 'verbatim',
|
|
218
|
+
assistant_text: 'verbatim',
|
|
219
|
+
assistant_thinking: 'ledger_only',
|
|
220
|
+
tool_call_arguments: 'ledger_only',
|
|
221
|
+
tool_results: 'ledger_only',
|
|
222
|
+
tool_payload_preview_bytes: 0,
|
|
223
|
+
images: 'marker_or_ledger_only',
|
|
224
|
+
unknown_block_behavior: 'error',
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Accumulates omitted-event ledger rows and turns maximal contiguous omitted
|
|
230
|
+
* runs into compact, source-ordered receipts inside the canonical input.
|
|
231
|
+
*/
|
|
232
|
+
class ProjectionBuilder {
|
|
233
|
+
private readonly entries: FusionProjectionEntry[] = [];
|
|
234
|
+
private readonly ledger: FusionOmittedEventRecord[] = [];
|
|
235
|
+
private readonly leaves: Buffer[] = [];
|
|
236
|
+
private readonly toolCallNames = new Map<string, number>();
|
|
237
|
+
private pendingCounts: Required<FusionOmittedRunCounts> | undefined;
|
|
238
|
+
private pendingBytes: Required<FusionOmittedRunBytes> | undefined;
|
|
239
|
+
private pendingLeaves: Buffer[] = [];
|
|
240
|
+
private pendingLedgerFirst = 0;
|
|
241
|
+
private pendingSourceFirst = 0;
|
|
242
|
+
private pendingSourceLast = 0;
|
|
243
|
+
|
|
244
|
+
includedUserTextBytes = 0;
|
|
245
|
+
includedAssistantTextBytes = 0;
|
|
246
|
+
includedImageMarkers = 0;
|
|
247
|
+
emptyTextBlocks = 0;
|
|
248
|
+
|
|
249
|
+
addText(entry: FusionProjectionTextEntry): void {
|
|
250
|
+
this.flush();
|
|
251
|
+
this.entries.push(entry);
|
|
252
|
+
if (entry.role === 'user') this.includedUserTextBytes += utf8Bytes(entry.text);
|
|
253
|
+
else this.includedAssistantTextBytes += utf8Bytes(entry.text);
|
|
126
254
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
255
|
+
|
|
256
|
+
addOmission(
|
|
257
|
+
sourceOrdinal: number,
|
|
258
|
+
blockOrdinal: number,
|
|
259
|
+
kind: FusionOmittedEventKind,
|
|
260
|
+
payload: Buffer,
|
|
261
|
+
extra: { toolName?: string; toolCallId?: string; mimeType?: string } = {},
|
|
262
|
+
): void {
|
|
263
|
+
const index = this.ledger.length;
|
|
264
|
+
const record: FusionOmittedEventRecord = {
|
|
265
|
+
index,
|
|
266
|
+
source_ordinal: sourceOrdinal,
|
|
267
|
+
block_ordinal: blockOrdinal,
|
|
268
|
+
kind,
|
|
269
|
+
payload_bytes: payload.length,
|
|
270
|
+
payload_sha256: sha256Hex(payload),
|
|
271
|
+
};
|
|
272
|
+
if (extra.toolName !== undefined) record.tool_name = extra.toolName;
|
|
273
|
+
if (extra.toolCallId !== undefined) record.tool_call_id = extra.toolCallId;
|
|
274
|
+
if (extra.mimeType !== undefined) record.mime_type = extra.mimeType;
|
|
275
|
+
this.ledger.push(record);
|
|
276
|
+
const leaf = ledgerLeafHash(index, record);
|
|
277
|
+
this.leaves.push(leaf);
|
|
278
|
+
|
|
279
|
+
if (extra.toolName !== undefined && kind === 'tool_call') {
|
|
280
|
+
this.toolCallNames.set(extra.toolName, (this.toolCallNames.get(extra.toolName) ?? 0) + 1);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (this.pendingCounts === undefined || this.pendingBytes === undefined) {
|
|
284
|
+
this.pendingCounts = {
|
|
285
|
+
assistant_thinking: 0,
|
|
286
|
+
tool_calls: 0,
|
|
287
|
+
tool_result_texts: 0,
|
|
288
|
+
tool_result_images: 0,
|
|
289
|
+
};
|
|
290
|
+
this.pendingBytes = {
|
|
291
|
+
assistant_thinking: 0,
|
|
292
|
+
tool_call_arguments: 0,
|
|
293
|
+
tool_result_text: 0,
|
|
294
|
+
tool_result_image: 0,
|
|
295
|
+
};
|
|
296
|
+
this.pendingLeaves = [];
|
|
297
|
+
this.pendingLedgerFirst = index;
|
|
298
|
+
this.pendingSourceFirst = sourceOrdinal;
|
|
299
|
+
}
|
|
300
|
+
this.pendingSourceLast = sourceOrdinal;
|
|
301
|
+
this.pendingLeaves.push(leaf);
|
|
302
|
+
if (kind === 'assistant_thinking') {
|
|
303
|
+
this.pendingCounts.assistant_thinking += 1;
|
|
304
|
+
this.pendingBytes.assistant_thinking += payload.length;
|
|
305
|
+
} else if (kind === 'tool_call') {
|
|
306
|
+
this.pendingCounts.tool_calls += 1;
|
|
307
|
+
this.pendingBytes.tool_call_arguments += payload.length;
|
|
308
|
+
} else if (kind === 'tool_result_text') {
|
|
309
|
+
this.pendingCounts.tool_result_texts += 1;
|
|
310
|
+
this.pendingBytes.tool_result_text += payload.length;
|
|
149
311
|
} else {
|
|
150
|
-
|
|
151
|
-
|
|
312
|
+
this.pendingCounts.tool_result_images += 1;
|
|
313
|
+
this.pendingBytes.tool_result_image += payload.length;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private flush(): void {
|
|
318
|
+
const counts = this.pendingCounts;
|
|
319
|
+
const bytes = this.pendingBytes;
|
|
320
|
+
if (counts === undefined || bytes === undefined) return;
|
|
321
|
+
const entry: FusionProjectionOmissionEntry = {
|
|
322
|
+
kind: 'omitted_activity',
|
|
323
|
+
source_ordinal_first: this.pendingSourceFirst,
|
|
324
|
+
source_ordinal_last: this.pendingSourceLast,
|
|
325
|
+
ledger_index_first: this.pendingLedgerFirst,
|
|
326
|
+
ledger_index_last: this.pendingLedgerFirst + this.pendingLeaves.length - 1,
|
|
327
|
+
counts: compactCounts(counts),
|
|
328
|
+
payload_bytes: compactBytes(bytes),
|
|
329
|
+
ledger_run_sha256: ledgerRunHash(this.pendingLeaves, this.pendingLedgerFirst),
|
|
330
|
+
};
|
|
331
|
+
this.entries.push(entry);
|
|
332
|
+
this.pendingCounts = undefined;
|
|
333
|
+
this.pendingBytes = undefined;
|
|
334
|
+
this.pendingLeaves = [];
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
finish(
|
|
338
|
+
source: FusionSource,
|
|
339
|
+
branchFilter: FusionBranchFilterDescriptor,
|
|
340
|
+
messageCount: number,
|
|
341
|
+
): { projection: FusionConversationProjectionV2; ledger: FusionContextOmissionLedgerV1 } {
|
|
342
|
+
this.flush();
|
|
343
|
+
const rootSha256 = ledgerRootHash(this.leaves);
|
|
344
|
+
let omittedRunCount = 0;
|
|
345
|
+
let includedTextEntries = 0;
|
|
346
|
+
let thinkingBytes = 0;
|
|
347
|
+
let toolCallCount = 0;
|
|
348
|
+
let toolArgumentBytes = 0;
|
|
349
|
+
let toolResultTextCount = 0;
|
|
350
|
+
let toolResultTextBytes = 0;
|
|
351
|
+
let toolResultImageCount = 0;
|
|
352
|
+
let toolResultImageBytes = 0;
|
|
353
|
+
for (const entry of this.entries) {
|
|
354
|
+
if (entry.kind === 'text') {
|
|
355
|
+
includedTextEntries += 1;
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
omittedRunCount += 1;
|
|
359
|
+
thinkingBytes += entry.payload_bytes.assistant_thinking ?? 0;
|
|
360
|
+
toolCallCount += entry.counts.tool_calls ?? 0;
|
|
361
|
+
toolArgumentBytes += entry.payload_bytes.tool_call_arguments ?? 0;
|
|
362
|
+
toolResultTextCount += entry.counts.tool_result_texts ?? 0;
|
|
363
|
+
toolResultTextBytes += entry.payload_bytes.tool_result_text ?? 0;
|
|
364
|
+
toolResultImageCount += entry.counts.tool_result_images ?? 0;
|
|
365
|
+
toolResultImageBytes += entry.payload_bytes.tool_result_image ?? 0;
|
|
366
|
+
}
|
|
367
|
+
const toolCallNames: FusionToolCallNameCount[] = [...this.toolCallNames.entries()]
|
|
368
|
+
.map(([name, calls]) => ({ name, calls }))
|
|
369
|
+
.sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));
|
|
370
|
+
const accounting: FusionProjectionAccounting = {
|
|
371
|
+
message_count: messageCount,
|
|
372
|
+
included_text_entry_count: includedTextEntries,
|
|
373
|
+
included_user_text_bytes: this.includedUserTextBytes,
|
|
374
|
+
included_assistant_text_bytes: this.includedAssistantTextBytes,
|
|
375
|
+
included_image_marker_count: this.includedImageMarkers,
|
|
376
|
+
empty_text_block_count: this.emptyTextBlocks,
|
|
377
|
+
omitted_run_count: omittedRunCount,
|
|
378
|
+
omitted_event_count: this.ledger.length,
|
|
379
|
+
omitted_thinking_bytes: thinkingBytes,
|
|
380
|
+
omitted_tool_call_count: toolCallCount,
|
|
381
|
+
omitted_tool_call_argument_bytes: toolArgumentBytes,
|
|
382
|
+
omitted_tool_result_text_count: toolResultTextCount,
|
|
383
|
+
omitted_tool_result_text_bytes: toolResultTextBytes,
|
|
384
|
+
omitted_tool_result_image_count: toolResultImageCount,
|
|
385
|
+
omitted_tool_result_image_bytes: toolResultImageBytes,
|
|
386
|
+
tool_call_names: toolCallNames,
|
|
387
|
+
ledger_entry_count: this.ledger.length,
|
|
388
|
+
ledger_root_sha256: rootSha256,
|
|
389
|
+
};
|
|
390
|
+
return {
|
|
391
|
+
projection: {
|
|
392
|
+
policy: policyDescriptor(source),
|
|
393
|
+
branch_filter: branchFilter,
|
|
394
|
+
entries: this.entries,
|
|
395
|
+
accounting,
|
|
396
|
+
},
|
|
397
|
+
ledger: {
|
|
398
|
+
schema_version: FUSION_CONTEXT_LEDGER_SCHEMA_VERSION,
|
|
399
|
+
policy_id: contextPolicyId(source),
|
|
400
|
+
transform: FUSION_CONTEXT_TRANSFORM_ID,
|
|
401
|
+
entries: this.ledger,
|
|
402
|
+
root_sha256: rootSha256,
|
|
403
|
+
},
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function imageMarker(mimeType: string): string {
|
|
409
|
+
return `${FUSION_IMAGE_OMISSION_PREFIX}${mimeType}]`;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Fixed policy rule: a zero-valued kind is absent from the receipt rather than
|
|
414
|
+
* serialized as `0`. This keeps receipts compact without losing information —
|
|
415
|
+
* absent is defined by the policy version to mean exactly zero — and it is
|
|
416
|
+
* applied unconditionally, never adaptively because an input happens to be large.
|
|
417
|
+
*/
|
|
418
|
+
function compactCounts(counts: Required<FusionOmittedRunCounts>): FusionOmittedRunCounts {
|
|
419
|
+
const out: FusionOmittedRunCounts = {};
|
|
420
|
+
if (counts.assistant_thinking > 0) out.assistant_thinking = counts.assistant_thinking;
|
|
421
|
+
if (counts.tool_calls > 0) out.tool_calls = counts.tool_calls;
|
|
422
|
+
if (counts.tool_result_texts > 0) out.tool_result_texts = counts.tool_result_texts;
|
|
423
|
+
if (counts.tool_result_images > 0) out.tool_result_images = counts.tool_result_images;
|
|
424
|
+
return out;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function compactBytes(bytes: Required<FusionOmittedRunBytes>): FusionOmittedRunBytes {
|
|
428
|
+
const out: FusionOmittedRunBytes = {};
|
|
429
|
+
if (bytes.assistant_thinking > 0) out.assistant_thinking = bytes.assistant_thinking;
|
|
430
|
+
if (bytes.tool_call_arguments > 0) out.tool_call_arguments = bytes.tool_call_arguments;
|
|
431
|
+
if (bytes.tool_result_text > 0) out.tool_result_text = bytes.tool_result_text;
|
|
432
|
+
if (bytes.tool_result_image > 0) out.tool_result_image = bytes.tool_result_image;
|
|
433
|
+
return out;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Projects one user-role message. User text is retained verbatim; images stay
|
|
438
|
+
* marker-only so the child never receives raw bytes.
|
|
439
|
+
*/
|
|
440
|
+
function projectUserMessage(
|
|
441
|
+
builder: ProjectionBuilder,
|
|
442
|
+
content: Message['content'],
|
|
443
|
+
sourceOrdinal: number,
|
|
444
|
+
): void {
|
|
445
|
+
if (typeof content === 'string') {
|
|
446
|
+
if (content.length === 0) {
|
|
447
|
+
builder.emptyTextBlocks += 1;
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
builder.addText({
|
|
451
|
+
kind: 'text',
|
|
452
|
+
source_ordinal: sourceOrdinal,
|
|
453
|
+
block_ordinal: 0,
|
|
454
|
+
role: 'user',
|
|
455
|
+
text: content,
|
|
456
|
+
});
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
for (const [blockOrdinal, block] of content.entries()) {
|
|
460
|
+
if (block.type === 'text') {
|
|
461
|
+
if (block.text.length === 0) {
|
|
462
|
+
builder.emptyTextBlocks += 1;
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
builder.addText({
|
|
466
|
+
kind: 'text',
|
|
467
|
+
source_ordinal: sourceOrdinal,
|
|
468
|
+
block_ordinal: blockOrdinal,
|
|
469
|
+
role: 'user',
|
|
470
|
+
text: block.text,
|
|
471
|
+
});
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
if (block.type === 'image') {
|
|
475
|
+
builder.includedImageMarkers += 1;
|
|
476
|
+
builder.addText({
|
|
477
|
+
kind: 'text',
|
|
478
|
+
source_ordinal: sourceOrdinal,
|
|
479
|
+
block_ordinal: blockOrdinal,
|
|
480
|
+
role: 'user',
|
|
481
|
+
text: imageMarker(block.mimeType),
|
|
482
|
+
});
|
|
483
|
+
continue;
|
|
152
484
|
}
|
|
485
|
+
throw unsupportedBlock(`user block ${String(Reflect.get(block, 'type'))}`);
|
|
153
486
|
}
|
|
154
|
-
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function projectAssistantMessage(
|
|
490
|
+
builder: ProjectionBuilder,
|
|
491
|
+
content: Extract<Message, { role: 'assistant' }>['content'],
|
|
492
|
+
sourceOrdinal: number,
|
|
493
|
+
): void {
|
|
494
|
+
for (const [blockOrdinal, block] of content.entries()) {
|
|
495
|
+
if (block.type === 'text') {
|
|
496
|
+
if (block.text.length === 0) {
|
|
497
|
+
builder.emptyTextBlocks += 1;
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
builder.addText({
|
|
501
|
+
kind: 'text',
|
|
502
|
+
source_ordinal: sourceOrdinal,
|
|
503
|
+
block_ordinal: blockOrdinal,
|
|
504
|
+
role: 'assistant',
|
|
505
|
+
text: block.text,
|
|
506
|
+
});
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
if (block.type === 'thinking') {
|
|
510
|
+
builder.addOmission(
|
|
511
|
+
sourceOrdinal,
|
|
512
|
+
blockOrdinal,
|
|
513
|
+
'assistant_thinking',
|
|
514
|
+
Buffer.from(block.thinking, 'utf8'),
|
|
515
|
+
);
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
if (block.type === 'toolCall') {
|
|
519
|
+
builder.addOmission(
|
|
520
|
+
sourceOrdinal,
|
|
521
|
+
blockOrdinal,
|
|
522
|
+
'tool_call',
|
|
523
|
+
Buffer.from(canonicalJson(block.arguments), 'utf8'),
|
|
524
|
+
{ toolName: block.name, toolCallId: block.id },
|
|
525
|
+
);
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
throw unsupportedBlock(`assistant block ${String(Reflect.get(block, 'type'))}`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function projectToolResultMessage(
|
|
533
|
+
builder: ProjectionBuilder,
|
|
534
|
+
message: Extract<Message, { role: 'toolResult' }>,
|
|
535
|
+
sourceOrdinal: number,
|
|
536
|
+
): void {
|
|
537
|
+
const content = message.content;
|
|
538
|
+
if (typeof content === 'string') {
|
|
539
|
+
builder.addOmission(
|
|
540
|
+
sourceOrdinal,
|
|
541
|
+
0,
|
|
542
|
+
'tool_result_text',
|
|
543
|
+
Buffer.from(content, 'utf8'),
|
|
544
|
+
{ toolName: message.toolName, toolCallId: message.toolCallId },
|
|
545
|
+
);
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
for (const [blockOrdinal, block] of content.entries()) {
|
|
549
|
+
if (block.type === 'text') {
|
|
550
|
+
builder.addOmission(
|
|
551
|
+
sourceOrdinal,
|
|
552
|
+
blockOrdinal,
|
|
553
|
+
'tool_result_text',
|
|
554
|
+
Buffer.from(block.text, 'utf8'),
|
|
555
|
+
{ toolName: message.toolName, toolCallId: message.toolCallId },
|
|
556
|
+
);
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
if (block.type === 'image') {
|
|
560
|
+
builder.addOmission(
|
|
561
|
+
sourceOrdinal,
|
|
562
|
+
blockOrdinal,
|
|
563
|
+
'tool_result_image',
|
|
564
|
+
Buffer.from(block.data, 'utf8'),
|
|
565
|
+
{
|
|
566
|
+
toolName: message.toolName,
|
|
567
|
+
toolCallId: message.toolCallId,
|
|
568
|
+
mimeType: block.mimeType,
|
|
569
|
+
},
|
|
570
|
+
);
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
throw unsupportedBlock(`tool result block ${String(Reflect.get(block, 'type'))}`);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Deterministic conversation projection. Every retained source block receives
|
|
579
|
+
* exactly one disposition: included verbatim, or represented as an omission
|
|
580
|
+
* ledger row. Unknown block types fail loudly instead of disappearing.
|
|
581
|
+
*/
|
|
582
|
+
export function projectFusionConversation(
|
|
583
|
+
messages: readonly Message[],
|
|
584
|
+
source: FusionSource,
|
|
585
|
+
branchFilter: FusionBranchFilterDescriptor,
|
|
586
|
+
): { projection: FusionConversationProjectionV2; ledger: FusionContextOmissionLedgerV1 } {
|
|
587
|
+
const builder = new ProjectionBuilder();
|
|
588
|
+
for (const [sourceOrdinal, message] of messages.entries()) {
|
|
589
|
+
if (message.role === 'user') projectUserMessage(builder, message.content, sourceOrdinal);
|
|
590
|
+
else if (message.role === 'assistant')
|
|
591
|
+
projectAssistantMessage(builder, message.content, sourceOrdinal);
|
|
592
|
+
else if (message.role === 'toolResult')
|
|
593
|
+
projectToolResultMessage(builder, message, sourceOrdinal);
|
|
594
|
+
else throw unsupportedBlock(`message role ${String(Reflect.get(message, 'role'))}`);
|
|
595
|
+
}
|
|
596
|
+
return builder.finish(source, branchFilter, messages.length);
|
|
155
597
|
}
|
|
156
598
|
|
|
157
599
|
export function buildFusionCanonicalInput(
|
|
@@ -165,15 +607,34 @@ export function buildFusionCanonicalInput(
|
|
|
165
607
|
});
|
|
166
608
|
}
|
|
167
609
|
const entries = ctx.sessionManager.getEntries();
|
|
168
|
-
const
|
|
169
|
-
const sessionContext = buildSessionContext(entries, leafId, entriesById(entries));
|
|
610
|
+
const leaf = effectiveLeaf(ctx.sessionManager, options);
|
|
611
|
+
const sessionContext = buildSessionContext(entries, leaf.leafId, entriesById(entries));
|
|
170
612
|
const llmMessages = convertToLlm(sessionContext.messages);
|
|
171
|
-
const
|
|
613
|
+
const toolName = options.toolName ?? FUSION_BRAINSTORM_TOOL_NAME;
|
|
614
|
+
const branchFilter: FusionBranchFilterDescriptor = {
|
|
615
|
+
id: FUSION_BRANCH_FILTER_ID,
|
|
616
|
+
tool_name: toolName,
|
|
617
|
+
tool_call_id: options.source === 'tool' ? (options.toolCallId ?? null) : null,
|
|
618
|
+
active_tool_call_leaf_excluded: leaf.activeToolCallLeafExcluded,
|
|
619
|
+
};
|
|
620
|
+
const projected = projectFusionConversation(llmMessages, options.source, branchFilter);
|
|
621
|
+
const request: FusionCanonicalRequestV2 = {
|
|
622
|
+
source: options.source,
|
|
623
|
+
authority: requestAuthority(options.source),
|
|
624
|
+
text: options.request,
|
|
625
|
+
sha256: sha256Text(options.request),
|
|
626
|
+
};
|
|
627
|
+
const input: FusionCanonicalInputV2 = {
|
|
172
628
|
schema_version: FUSION_INPUT_SCHEMA_VERSION,
|
|
173
629
|
cwd: ctx.cwd,
|
|
174
630
|
system_prompt: ctx.getSystemPrompt(),
|
|
175
|
-
|
|
176
|
-
|
|
631
|
+
request,
|
|
632
|
+
conversation_projection: projected.projection,
|
|
633
|
+
};
|
|
634
|
+
return {
|
|
635
|
+
input,
|
|
636
|
+
serialized: canonicalJson(input),
|
|
637
|
+
ledger: projected.ledger,
|
|
638
|
+
transcriptLeafId: leaf.leafId,
|
|
177
639
|
};
|
|
178
|
-
return { input, serialized: canonicalJson(input), transcriptLeafId: leafId };
|
|
179
640
|
}
|