pi-background-tasks 0.7.4 → 0.7.7
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 +7 -7
- package/README.md +199 -15
- package/TESTING.md +95 -1
- package/TEST_PLAN.md +21 -4
- package/extensions/delegate-child.ts +1 -0
- package/package.json +4 -3
- package/src/core/common.ts +41 -0
- package/src/core/context/parent-snapshot.ts +142 -0
- package/src/core/context/token-budget.ts +890 -0
- package/src/core/context/visible-conversation-v2.ts +551 -0
- package/src/core/delegate/artifacts.ts +479 -0
- package/src/core/delegate/budget.ts +370 -0
- package/src/core/delegate/hook-contract-evidence.json +18 -0
- package/src/core/delegate/hook-contract.ts +153 -0
- package/src/core/delegate/launch.ts +459 -0
- package/src/core/delegate/result-package.ts +443 -0
- package/src/core/delegate/runner.ts +406 -0
- package/src/core/delegate/seed.ts +411 -0
- package/src/core/delegate/types.ts +304 -0
- package/src/core/fusion/artifacts.ts +17 -2
- package/src/core/fusion/budget.ts +884 -210
- package/src/core/fusion/context.ts +118 -520
- package/src/core/fusion/orchestrator.ts +133 -20
- package/src/core/fusion/prompts.ts +11 -7
- package/src/core/fusion/types.ts +230 -86
- package/src/core/registry.ts +174 -0
- package/src/delegate-child-extension.ts +673 -0
- package/src/delegate-extension.ts +587 -0
- package/src/extension.ts +10 -0
- package/src/fusion-extension.ts +4 -0
|
@@ -1,53 +1,43 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
+
import { canonicalJson } from '../attested-pi-run.js';
|
|
3
|
+
import {
|
|
4
|
+
projectVisibleConversationV2,
|
|
5
|
+
type OmittedRunCounts,
|
|
6
|
+
type ProjectedConversationV2,
|
|
7
|
+
type ProjectionEntry,
|
|
8
|
+
} from '../context/visible-conversation-v2.js';
|
|
2
9
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
type
|
|
6
|
-
|
|
10
|
+
snapshotParentConversation,
|
|
11
|
+
type ParentContextSource,
|
|
12
|
+
type ParentSnapshotOptions,
|
|
13
|
+
type ReadonlyParentSessionManager,
|
|
14
|
+
} from '../context/parent-snapshot.js';
|
|
7
15
|
import type { Message } from '@earendil-works/pi-ai';
|
|
8
|
-
import { canonicalJson } from '../attested-pi-run.js';
|
|
9
|
-
import { isJsonObject, type JsonObject } from '../common.js';
|
|
10
16
|
import {
|
|
11
17
|
FUSION_BRANCH_FILTER_ID,
|
|
12
18
|
FUSION_COMMAND_CONTEXT_POLICY_ID,
|
|
13
19
|
FUSION_CONTEXT_LEDGER_SCHEMA_VERSION,
|
|
14
20
|
FUSION_CONTEXT_TRANSFORM_ID,
|
|
15
|
-
FUSION_IMAGE_OMISSION_PREFIX,
|
|
16
21
|
FUSION_INPUT_SCHEMA_VERSION,
|
|
17
22
|
FUSION_TOOL_CONTEXT_POLICY_ID,
|
|
18
23
|
FusionError,
|
|
19
24
|
type FusionBranchFilterDescriptor,
|
|
20
|
-
type
|
|
21
|
-
type
|
|
22
|
-
type
|
|
25
|
+
type FusionCanonicalInputV3,
|
|
26
|
+
type FusionCanonicalRequestV3,
|
|
27
|
+
type FusionContextOmissionLedgerV2,
|
|
23
28
|
type FusionContextPolicyDescriptor,
|
|
24
|
-
type
|
|
25
|
-
type FusionOmittedEventKind,
|
|
26
|
-
type FusionOmittedEventRecord,
|
|
27
|
-
type FusionOmittedRunBytes,
|
|
28
|
-
type FusionOmittedRunCounts,
|
|
29
|
-
type FusionProjectionAccounting,
|
|
29
|
+
type FusionConversationProjectionV3,
|
|
30
30
|
type FusionProjectionEntry,
|
|
31
|
-
type
|
|
32
|
-
type FusionProjectionTextEntry,
|
|
31
|
+
type FusionProjectionOmissionCounts,
|
|
33
32
|
type FusionRequestAuthority,
|
|
34
33
|
type FusionSource,
|
|
35
|
-
type FusionToolCallNameCount,
|
|
36
34
|
} from './types.js';
|
|
37
35
|
|
|
38
36
|
export const FUSION_BRAINSTORM_TOOL_NAME = 'fusion_brainstorm';
|
|
39
37
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
getEntries(): SessionEntry[];
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export interface FusionContextSource {
|
|
47
|
-
cwd: string;
|
|
48
|
-
sessionManager: FusionReadonlySessionManager;
|
|
49
|
-
getSystemPrompt(): string;
|
|
50
|
-
}
|
|
38
|
+
/** Retained for source compatibility; Fusion's session access is the shared adapter. */
|
|
39
|
+
export type FusionReadonlySessionManager = ReadonlyParentSessionManager;
|
|
40
|
+
export type FusionContextSource = ParentContextSource;
|
|
51
41
|
|
|
52
42
|
export interface BuildFusionCanonicalInputOptions {
|
|
53
43
|
source: FusionSource;
|
|
@@ -57,9 +47,9 @@ export interface BuildFusionCanonicalInputOptions {
|
|
|
57
47
|
}
|
|
58
48
|
|
|
59
49
|
export interface BuiltFusionCanonicalInput {
|
|
60
|
-
input:
|
|
50
|
+
input: FusionCanonicalInputV3;
|
|
61
51
|
serialized: string;
|
|
62
|
-
ledger:
|
|
52
|
+
ledger: FusionContextOmissionLedgerV2;
|
|
63
53
|
transcriptLeafId: string | null;
|
|
64
54
|
}
|
|
65
55
|
|
|
@@ -67,138 +57,8 @@ export function normalizeFusionCommandRequest(args: string): string {
|
|
|
67
57
|
return args.trim();
|
|
68
58
|
}
|
|
69
59
|
|
|
70
|
-
function entriesById(entries: readonly SessionEntry[]): Map<string, SessionEntry> {
|
|
71
|
-
const byId = new Map<string, SessionEntry>();
|
|
72
|
-
for (const entry of entries) byId.set(entry.id, entry);
|
|
73
|
-
return byId;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function readArray(record: JsonObject, key: string): readonly unknown[] | undefined {
|
|
77
|
-
const value = record[key];
|
|
78
|
-
return Array.isArray(value) ? value : undefined;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function recordOf(value: unknown): JsonObject | undefined {
|
|
82
|
-
if (!isJsonObject(value) || Array.isArray(value)) return undefined;
|
|
83
|
-
return value;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function entryMessage(entry: SessionEntry): JsonObject | undefined {
|
|
87
|
-
if (entry.type !== 'message') return undefined;
|
|
88
|
-
return recordOf(entry.message);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function toolCallPartMatches(
|
|
92
|
-
part: unknown,
|
|
93
|
-
toolCallId: string | undefined,
|
|
94
|
-
toolName: string,
|
|
95
|
-
): boolean {
|
|
96
|
-
const record = recordOf(part);
|
|
97
|
-
if (record === undefined || record['type'] !== 'toolCall') return false;
|
|
98
|
-
if (toolCallId !== undefined) return record['id'] === toolCallId;
|
|
99
|
-
return record['name'] === toolName;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function messageContainsToolCall(
|
|
103
|
-
message: JsonObject,
|
|
104
|
-
toolCallId: string | undefined,
|
|
105
|
-
toolName: string,
|
|
106
|
-
): boolean {
|
|
107
|
-
if (message['role'] !== 'assistant') return false;
|
|
108
|
-
const content = readArray(message, 'content');
|
|
109
|
-
if (content === undefined) return false;
|
|
110
|
-
for (const part of content) {
|
|
111
|
-
if (toolCallPartMatches(part, toolCallId, toolName)) return true;
|
|
112
|
-
}
|
|
113
|
-
return false;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
interface EffectiveLeaf {
|
|
117
|
-
leafId: string | null;
|
|
118
|
-
activeToolCallLeafExcluded: boolean;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function effectiveLeafForTool(
|
|
122
|
-
sessionManager: FusionReadonlySessionManager,
|
|
123
|
-
toolCallId: string | undefined,
|
|
124
|
-
toolName: string,
|
|
125
|
-
): EffectiveLeaf {
|
|
126
|
-
const leaf = sessionManager.getLeafEntry();
|
|
127
|
-
if (leaf === undefined)
|
|
128
|
-
return { leafId: sessionManager.getLeafId(), activeToolCallLeafExcluded: false };
|
|
129
|
-
const message = entryMessage(leaf);
|
|
130
|
-
if (message !== undefined && messageContainsToolCall(message, toolCallId, toolName)) {
|
|
131
|
-
return { leafId: leaf.parentId, activeToolCallLeafExcluded: true };
|
|
132
|
-
}
|
|
133
|
-
return { leafId: sessionManager.getLeafId(), activeToolCallLeafExcluded: false };
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function effectiveLeaf(
|
|
137
|
-
sessionManager: FusionReadonlySessionManager,
|
|
138
|
-
options: BuildFusionCanonicalInputOptions,
|
|
139
|
-
): EffectiveLeaf {
|
|
140
|
-
if (options.source !== 'tool')
|
|
141
|
-
return { leafId: sessionManager.getLeafId(), activeToolCallLeafExcluded: false };
|
|
142
|
-
return effectiveLeafForTool(
|
|
143
|
-
sessionManager,
|
|
144
|
-
options.toolCallId,
|
|
145
|
-
options.toolName ?? FUSION_BRAINSTORM_TOOL_NAME,
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
|
|
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
60
|
function sha256Text(value: string): string {
|
|
158
|
-
return
|
|
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
|
-
);
|
|
61
|
+
return createHash('sha256').update(Buffer.from(value, 'utf8')).digest('hex');
|
|
202
62
|
}
|
|
203
63
|
|
|
204
64
|
function contextPolicyId(source: FusionSource): string {
|
|
@@ -213,7 +73,8 @@ function policyDescriptor(source: FusionSource): FusionContextPolicyDescriptor {
|
|
|
213
73
|
return {
|
|
214
74
|
id: contextPolicyId(source),
|
|
215
75
|
transform: FUSION_CONTEXT_TRANSFORM_ID,
|
|
216
|
-
version:
|
|
76
|
+
version: 2,
|
|
77
|
+
receipt_format: 'omitted_activity.v2',
|
|
217
78
|
user_text: 'verbatim',
|
|
218
79
|
assistant_text: 'verbatim',
|
|
219
80
|
assistant_thinking: 'ledger_only',
|
|
@@ -225,375 +86,110 @@ function policyDescriptor(source: FusionSource): FusionContextPolicyDescriptor {
|
|
|
225
86
|
};
|
|
226
87
|
}
|
|
227
88
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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);
|
|
254
|
-
}
|
|
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;
|
|
311
|
-
} else {
|
|
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
|
-
}
|
|
89
|
+
function compactOmissionCounts(counts: OmittedRunCounts): FusionProjectionOmissionCounts {
|
|
90
|
+
return [
|
|
91
|
+
counts.assistant_thinking ?? 0,
|
|
92
|
+
counts.tool_calls ?? 0,
|
|
93
|
+
counts.tool_result_texts ?? 0,
|
|
94
|
+
];
|
|
406
95
|
}
|
|
407
96
|
|
|
408
|
-
function
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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;
|
|
97
|
+
function expandOmissionCounts(counts: FusionProjectionOmissionCounts): OmittedRunCounts {
|
|
98
|
+
const out: OmittedRunCounts = {};
|
|
99
|
+
const [assistantThinking, toolCalls, toolResults] = counts;
|
|
100
|
+
if (assistantThinking > 0) out.assistant_thinking = assistantThinking;
|
|
101
|
+
if (toolCalls > 0) out.tool_calls = toolCalls;
|
|
102
|
+
if (toolResults > 0) out.tool_result_texts = toolResults;
|
|
424
103
|
return out;
|
|
425
104
|
}
|
|
426
105
|
|
|
427
|
-
function
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
106
|
+
export function compactFusionProjectionEntry(entry: ProjectionEntry): FusionProjectionEntry {
|
|
107
|
+
if (entry.kind === 'text') {
|
|
108
|
+
return [
|
|
109
|
+
't',
|
|
110
|
+
entry.role === 'user' ? 'u' : 'a',
|
|
111
|
+
entry.source_ordinal,
|
|
112
|
+
entry.block_ordinal,
|
|
113
|
+
entry.text,
|
|
114
|
+
];
|
|
115
|
+
}
|
|
116
|
+
return ['o', [entry.at[0], entry.at[1]], entry.bytes, compactOmissionCounts(entry.counts)];
|
|
434
117
|
}
|
|
435
118
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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({
|
|
119
|
+
export function expandFusionProjectionEntry(entry: FusionProjectionEntry): ProjectionEntry {
|
|
120
|
+
if (entry[0] === 't') {
|
|
121
|
+
return {
|
|
451
122
|
kind: 'text',
|
|
452
|
-
source_ordinal:
|
|
453
|
-
block_ordinal:
|
|
454
|
-
role: 'user',
|
|
455
|
-
text:
|
|
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;
|
|
484
|
-
}
|
|
485
|
-
throw unsupportedBlock(`user block ${String(Reflect.get(block, 'type'))}`);
|
|
123
|
+
source_ordinal: entry[2],
|
|
124
|
+
block_ordinal: entry[3],
|
|
125
|
+
role: entry[1] === 'u' ? 'user' : 'assistant',
|
|
126
|
+
text: entry[4],
|
|
127
|
+
};
|
|
486
128
|
}
|
|
129
|
+
return {
|
|
130
|
+
kind: 'omitted_activity',
|
|
131
|
+
at: [entry[1][0], entry[1][1]],
|
|
132
|
+
bytes: entry[2],
|
|
133
|
+
counts: expandOmissionCounts(entry[3]),
|
|
134
|
+
};
|
|
487
135
|
}
|
|
488
136
|
|
|
489
|
-
function
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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
|
-
}
|
|
137
|
+
function compactFusionProjectionEntries(
|
|
138
|
+
entries: readonly ProjectionEntry[],
|
|
139
|
+
): readonly FusionProjectionEntry[] {
|
|
140
|
+
return entries.map(compactFusionProjectionEntry);
|
|
530
141
|
}
|
|
531
142
|
|
|
532
|
-
function
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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'))}`);
|
|
143
|
+
function compactOmissionReceiptBytes(entries: readonly FusionProjectionEntry[]): number {
|
|
144
|
+
let total = 0;
|
|
145
|
+
for (const entry of entries) {
|
|
146
|
+
if (entry[0] === 'o') total += Buffer.byteLength(canonicalJson(entry), 'utf8');
|
|
574
147
|
}
|
|
148
|
+
return total;
|
|
575
149
|
}
|
|
576
150
|
|
|
577
151
|
/**
|
|
578
|
-
*
|
|
579
|
-
*
|
|
580
|
-
*
|
|
152
|
+
* Seal the shared transform output into Fusion's versioned envelopes.
|
|
153
|
+
*
|
|
154
|
+
* Fusion v4 compacts only the child-facing projection entries. Ledger rows are
|
|
155
|
+
* carried through unchanged, so the ledger root commits to exactly the same
|
|
156
|
+
* omitted payload records before and after tuple encoding. Golden tests pin the
|
|
157
|
+
* new canonical-input bytes and the unchanged ledger bytes.
|
|
581
158
|
*/
|
|
159
|
+
function sealFusionProjection(
|
|
160
|
+
projected: ProjectedConversationV2,
|
|
161
|
+
source: FusionSource,
|
|
162
|
+
branchFilter: FusionBranchFilterDescriptor,
|
|
163
|
+
): { projection: FusionConversationProjectionV3; ledger: FusionContextOmissionLedgerV2 } {
|
|
164
|
+
const entries = compactFusionProjectionEntries(projected.entries);
|
|
165
|
+
return {
|
|
166
|
+
projection: {
|
|
167
|
+
policy: policyDescriptor(source),
|
|
168
|
+
branch_filter: branchFilter,
|
|
169
|
+
entries,
|
|
170
|
+
accounting: {
|
|
171
|
+
...projected.accounting,
|
|
172
|
+
omission_receipt_utf8_bytes: compactOmissionReceiptBytes(entries),
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
ledger: {
|
|
176
|
+
schema_version: FUSION_CONTEXT_LEDGER_SCHEMA_VERSION,
|
|
177
|
+
policy_id: contextPolicyId(source),
|
|
178
|
+
transform: FUSION_CONTEXT_TRANSFORM_ID,
|
|
179
|
+
entries: projected.ledger.entries,
|
|
180
|
+
projection_map: projected.ledger.projection_map,
|
|
181
|
+
root_sha256: projected.ledger.root_sha256,
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Preserved public entry point; delegates to the shared frozen transform. */
|
|
582
187
|
export function projectFusionConversation(
|
|
583
188
|
messages: readonly Message[],
|
|
584
189
|
source: FusionSource,
|
|
585
190
|
branchFilter: FusionBranchFilterDescriptor,
|
|
586
|
-
): { projection:
|
|
587
|
-
|
|
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);
|
|
191
|
+
): { projection: FusionConversationProjectionV3; ledger: FusionContextOmissionLedgerV2 } {
|
|
192
|
+
return sealFusionProjection(projectVisibleConversationV2(messages), source, branchFilter);
|
|
597
193
|
}
|
|
598
194
|
|
|
599
195
|
export function buildFusionCanonicalInput(
|
|
@@ -606,25 +202,27 @@ export function buildFusionCanonicalInput(
|
|
|
606
202
|
childCreated: false,
|
|
607
203
|
});
|
|
608
204
|
}
|
|
609
|
-
const entries = ctx.sessionManager.getEntries();
|
|
610
|
-
const leaf = effectiveLeaf(ctx.sessionManager, options);
|
|
611
|
-
const sessionContext = buildSessionContext(entries, leaf.leafId, entriesById(entries));
|
|
612
|
-
const llmMessages = convertToLlm(sessionContext.messages);
|
|
613
205
|
const toolName = options.toolName ?? FUSION_BRAINSTORM_TOOL_NAME;
|
|
206
|
+
const snapshotOptions: ParentSnapshotOptions = {
|
|
207
|
+
toolName,
|
|
208
|
+
excludeActiveToolCallLeaf: options.source === 'tool',
|
|
209
|
+
};
|
|
210
|
+
if (options.toolCallId !== undefined) snapshotOptions.toolCallId = options.toolCallId;
|
|
211
|
+
const snapshot = snapshotParentConversation(ctx, snapshotOptions);
|
|
614
212
|
const branchFilter: FusionBranchFilterDescriptor = {
|
|
615
213
|
id: FUSION_BRANCH_FILTER_ID,
|
|
616
214
|
tool_name: toolName,
|
|
617
215
|
tool_call_id: options.source === 'tool' ? (options.toolCallId ?? null) : null,
|
|
618
|
-
active_tool_call_leaf_excluded:
|
|
216
|
+
active_tool_call_leaf_excluded: snapshot.activeToolCallLeafExcluded,
|
|
619
217
|
};
|
|
620
|
-
const projected = projectFusionConversation(
|
|
621
|
-
const request:
|
|
218
|
+
const projected = projectFusionConversation(snapshot.messages, options.source, branchFilter);
|
|
219
|
+
const request: FusionCanonicalRequestV3 = {
|
|
622
220
|
source: options.source,
|
|
623
221
|
authority: requestAuthority(options.source),
|
|
624
222
|
text: options.request,
|
|
625
223
|
sha256: sha256Text(options.request),
|
|
626
224
|
};
|
|
627
|
-
const input:
|
|
225
|
+
const input: FusionCanonicalInputV3 = {
|
|
628
226
|
schema_version: FUSION_INPUT_SCHEMA_VERSION,
|
|
629
227
|
cwd: ctx.cwd,
|
|
630
228
|
system_prompt: ctx.getSystemPrompt(),
|
|
@@ -635,6 +233,6 @@ export function buildFusionCanonicalInput(
|
|
|
635
233
|
input,
|
|
636
234
|
serialized: canonicalJson(input),
|
|
637
235
|
ledger: projected.ledger,
|
|
638
|
-
transcriptLeafId:
|
|
236
|
+
transcriptLeafId: snapshot.leafId,
|
|
639
237
|
};
|
|
640
238
|
}
|