pi-background-tasks 0.7.6 → 0.9.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.
- package/PUBLISHING.md +7 -7
- package/README.md +238 -17
- package/TESTING.md +94 -0
- package/TEST_PLAN.md +28 -4
- package/extensions/delegate-child.ts +1 -0
- package/package.json +10 -4
- 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 +460 -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 +64 -4
- package/src/core/fusion/budget.ts +464 -65
- package/src/core/fusion/context.ts +115 -511
- package/src/core/fusion/orchestrator.ts +184 -18
- package/src/core/fusion/pi-child.ts +473 -8
- package/src/core/fusion/prompts.ts +156 -4
- package/src/core/fusion/types.ts +237 -37
- package/src/core/fusion/web-fetch.ts +904 -0
- package/src/core/fusion/workflows.ts +130 -0
- 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-child-extension.ts +279 -2
- package/src/fusion-extension.ts +183 -26
|
@@ -1,18 +1,24 @@
|
|
|
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 {
|
|
9
|
-
import { isJsonObject, type JsonObject } from '../common.js';
|
|
16
|
+
import { FUSION_BRAINSTORM_TOOL_NAME as FUSION_BRAINSTORM_TOOL_NAME_VALUE } from './workflows.js';
|
|
10
17
|
import {
|
|
11
18
|
FUSION_BRANCH_FILTER_ID,
|
|
12
19
|
FUSION_COMMAND_CONTEXT_POLICY_ID,
|
|
13
20
|
FUSION_CONTEXT_LEDGER_SCHEMA_VERSION,
|
|
14
21
|
FUSION_CONTEXT_TRANSFORM_ID,
|
|
15
|
-
FUSION_IMAGE_OMISSION_PREFIX,
|
|
16
22
|
FUSION_INPUT_SCHEMA_VERSION,
|
|
17
23
|
FUSION_TOOL_CONTEXT_POLICY_ID,
|
|
18
24
|
FusionError,
|
|
@@ -21,33 +27,22 @@ import {
|
|
|
21
27
|
type FusionCanonicalRequestV3,
|
|
22
28
|
type FusionContextOmissionLedgerV2,
|
|
23
29
|
type FusionContextPolicyDescriptor,
|
|
24
|
-
type FusionContextProjectionMapEntry,
|
|
25
30
|
type FusionConversationProjectionV3,
|
|
26
|
-
type FusionOmittedEventKind,
|
|
27
|
-
type FusionOmittedEventRecord,
|
|
28
|
-
type FusionOmittedRunCounts,
|
|
29
|
-
type FusionProjectionAccounting,
|
|
30
31
|
type FusionProjectionEntry,
|
|
31
|
-
type
|
|
32
|
-
type FusionProjectionTextEntry,
|
|
32
|
+
type FusionProjectionOmissionCounts,
|
|
33
33
|
type FusionRequestAuthority,
|
|
34
34
|
type FusionSource,
|
|
35
|
-
type FusionToolCallNameCount,
|
|
36
35
|
} from './types.js';
|
|
37
36
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
getEntries(): SessionEntry[];
|
|
44
|
-
}
|
|
37
|
+
/**
|
|
38
|
+
* Re-exported from the workflow registry, which owns every workflow's tool name.
|
|
39
|
+
* Kept here so existing importers of this module keep working unchanged.
|
|
40
|
+
*/
|
|
41
|
+
export { FUSION_BRAINSTORM_TOOL_NAME, FUSION_VALIDATE_TOOL_NAME } from './workflows.js';
|
|
45
42
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
getSystemPrompt(): string;
|
|
50
|
-
}
|
|
43
|
+
/** Retained for source compatibility; Fusion's session access is the shared adapter. */
|
|
44
|
+
export type FusionReadonlySessionManager = ReadonlyParentSessionManager;
|
|
45
|
+
export type FusionContextSource = ParentContextSource;
|
|
51
46
|
|
|
52
47
|
export interface BuildFusionCanonicalInputOptions {
|
|
53
48
|
source: FusionSource;
|
|
@@ -67,129 +62,8 @@ export function normalizeFusionCommandRequest(args: string): string {
|
|
|
67
62
|
return args.trim();
|
|
68
63
|
}
|
|
69
64
|
|
|
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
65
|
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 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
|
-
);
|
|
66
|
+
return createHash('sha256').update(Buffer.from(value, 'utf8')).digest('hex');
|
|
193
67
|
}
|
|
194
68
|
|
|
195
69
|
function contextPolicyId(source: FusionSource): string {
|
|
@@ -217,382 +91,110 @@ function policyDescriptor(source: FusionSource): FusionContextPolicyDescriptor {
|
|
|
217
91
|
};
|
|
218
92
|
}
|
|
219
93
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
}
|
|
94
|
+
function compactOmissionCounts(counts: OmittedRunCounts): FusionProjectionOmissionCounts {
|
|
95
|
+
return [
|
|
96
|
+
counts.assistant_thinking ?? 0,
|
|
97
|
+
counts.tool_calls ?? 0,
|
|
98
|
+
counts.tool_result_texts ?? 0,
|
|
99
|
+
];
|
|
415
100
|
}
|
|
416
101
|
|
|
417
|
-
function
|
|
418
|
-
|
|
102
|
+
function expandOmissionCounts(counts: FusionProjectionOmissionCounts): OmittedRunCounts {
|
|
103
|
+
const out: OmittedRunCounts = {};
|
|
104
|
+
const [assistantThinking, toolCalls, toolResults] = counts;
|
|
105
|
+
if (assistantThinking > 0) out.assistant_thinking = assistantThinking;
|
|
106
|
+
if (toolCalls > 0) out.tool_calls = toolCalls;
|
|
107
|
+
if (toolResults > 0) out.tool_result_texts = toolResults;
|
|
108
|
+
return out;
|
|
419
109
|
}
|
|
420
110
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
return out;
|
|
111
|
+
export function compactFusionProjectionEntry(entry: ProjectionEntry): FusionProjectionEntry {
|
|
112
|
+
if (entry.kind === 'text') {
|
|
113
|
+
return [
|
|
114
|
+
't',
|
|
115
|
+
entry.role === 'user' ? 'u' : 'a',
|
|
116
|
+
entry.source_ordinal,
|
|
117
|
+
entry.block_ordinal,
|
|
118
|
+
entry.text,
|
|
119
|
+
];
|
|
120
|
+
}
|
|
121
|
+
return ['o', [entry.at[0], entry.at[1]], entry.bytes, compactOmissionCounts(entry.counts)];
|
|
433
122
|
}
|
|
434
123
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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({
|
|
124
|
+
export function expandFusionProjectionEntry(entry: FusionProjectionEntry): ProjectionEntry {
|
|
125
|
+
if (entry[0] === 't') {
|
|
126
|
+
return {
|
|
450
127
|
kind: 'text',
|
|
451
|
-
source_ordinal:
|
|
452
|
-
block_ordinal:
|
|
453
|
-
role: 'user',
|
|
454
|
-
text:
|
|
455
|
-
}
|
|
456
|
-
return;
|
|
457
|
-
}
|
|
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;
|
|
463
|
-
}
|
|
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;
|
|
483
|
-
}
|
|
484
|
-
throw unsupportedBlock(`user block ${String(Reflect.get(block, 'type'))}`);
|
|
128
|
+
source_ordinal: entry[2],
|
|
129
|
+
block_ordinal: entry[3],
|
|
130
|
+
role: entry[1] === 'u' ? 'user' : 'assistant',
|
|
131
|
+
text: entry[4],
|
|
132
|
+
};
|
|
485
133
|
}
|
|
134
|
+
return {
|
|
135
|
+
kind: 'omitted_activity',
|
|
136
|
+
at: [entry[1][0], entry[1][1]],
|
|
137
|
+
bytes: entry[2],
|
|
138
|
+
counts: expandOmissionCounts(entry[3]),
|
|
139
|
+
};
|
|
486
140
|
}
|
|
487
141
|
|
|
488
|
-
function
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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
|
-
}
|
|
142
|
+
function compactFusionProjectionEntries(
|
|
143
|
+
entries: readonly ProjectionEntry[],
|
|
144
|
+
): readonly FusionProjectionEntry[] {
|
|
145
|
+
return entries.map(compactFusionProjectionEntry);
|
|
529
146
|
}
|
|
530
147
|
|
|
531
|
-
function
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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'))}`);
|
|
148
|
+
function compactOmissionReceiptBytes(entries: readonly FusionProjectionEntry[]): number {
|
|
149
|
+
let total = 0;
|
|
150
|
+
for (const entry of entries) {
|
|
151
|
+
if (entry[0] === 'o') total += Buffer.byteLength(canonicalJson(entry), 'utf8');
|
|
573
152
|
}
|
|
153
|
+
return total;
|
|
574
154
|
}
|
|
575
155
|
|
|
576
156
|
/**
|
|
577
|
-
*
|
|
578
|
-
*
|
|
579
|
-
*
|
|
157
|
+
* Seal the shared transform output into Fusion's versioned envelopes.
|
|
158
|
+
*
|
|
159
|
+
* Fusion v4 compacts only the child-facing projection entries. Ledger rows are
|
|
160
|
+
* carried through unchanged, so the ledger root commits to exactly the same
|
|
161
|
+
* omitted payload records before and after tuple encoding. Golden tests pin the
|
|
162
|
+
* new canonical-input bytes and the unchanged ledger bytes.
|
|
580
163
|
*/
|
|
164
|
+
function sealFusionProjection(
|
|
165
|
+
projected: ProjectedConversationV2,
|
|
166
|
+
source: FusionSource,
|
|
167
|
+
branchFilter: FusionBranchFilterDescriptor,
|
|
168
|
+
): { projection: FusionConversationProjectionV3; ledger: FusionContextOmissionLedgerV2 } {
|
|
169
|
+
const entries = compactFusionProjectionEntries(projected.entries);
|
|
170
|
+
return {
|
|
171
|
+
projection: {
|
|
172
|
+
policy: policyDescriptor(source),
|
|
173
|
+
branch_filter: branchFilter,
|
|
174
|
+
entries,
|
|
175
|
+
accounting: {
|
|
176
|
+
...projected.accounting,
|
|
177
|
+
omission_receipt_utf8_bytes: compactOmissionReceiptBytes(entries),
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
ledger: {
|
|
181
|
+
schema_version: FUSION_CONTEXT_LEDGER_SCHEMA_VERSION,
|
|
182
|
+
policy_id: contextPolicyId(source),
|
|
183
|
+
transform: FUSION_CONTEXT_TRANSFORM_ID,
|
|
184
|
+
entries: projected.ledger.entries,
|
|
185
|
+
projection_map: projected.ledger.projection_map,
|
|
186
|
+
root_sha256: projected.ledger.root_sha256,
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Preserved public entry point; delegates to the shared frozen transform. */
|
|
581
192
|
export function projectFusionConversation(
|
|
582
193
|
messages: readonly Message[],
|
|
583
194
|
source: FusionSource,
|
|
584
195
|
branchFilter: FusionBranchFilterDescriptor,
|
|
585
196
|
): { projection: FusionConversationProjectionV3; ledger: FusionContextOmissionLedgerV2 } {
|
|
586
|
-
|
|
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);
|
|
197
|
+
return sealFusionProjection(projectVisibleConversationV2(messages), source, branchFilter);
|
|
596
198
|
}
|
|
597
199
|
|
|
598
200
|
export function buildFusionCanonicalInput(
|
|
@@ -605,18 +207,20 @@ export function buildFusionCanonicalInput(
|
|
|
605
207
|
childCreated: false,
|
|
606
208
|
});
|
|
607
209
|
}
|
|
608
|
-
const
|
|
609
|
-
const
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
210
|
+
const toolName = options.toolName ?? FUSION_BRAINSTORM_TOOL_NAME_VALUE;
|
|
211
|
+
const snapshotOptions: ParentSnapshotOptions = {
|
|
212
|
+
toolName,
|
|
213
|
+
excludeActiveToolCallLeaf: options.source === 'tool',
|
|
214
|
+
};
|
|
215
|
+
if (options.toolCallId !== undefined) snapshotOptions.toolCallId = options.toolCallId;
|
|
216
|
+
const snapshot = snapshotParentConversation(ctx, snapshotOptions);
|
|
613
217
|
const branchFilter: FusionBranchFilterDescriptor = {
|
|
614
218
|
id: FUSION_BRANCH_FILTER_ID,
|
|
615
219
|
tool_name: toolName,
|
|
616
220
|
tool_call_id: options.source === 'tool' ? (options.toolCallId ?? null) : null,
|
|
617
|
-
active_tool_call_leaf_excluded:
|
|
221
|
+
active_tool_call_leaf_excluded: snapshot.activeToolCallLeafExcluded,
|
|
618
222
|
};
|
|
619
|
-
const projected = projectFusionConversation(
|
|
223
|
+
const projected = projectFusionConversation(snapshot.messages, options.source, branchFilter);
|
|
620
224
|
const request: FusionCanonicalRequestV3 = {
|
|
621
225
|
source: options.source,
|
|
622
226
|
authority: requestAuthority(options.source),
|
|
@@ -634,6 +238,6 @@ export function buildFusionCanonicalInput(
|
|
|
634
238
|
input,
|
|
635
239
|
serialized: canonicalJson(input),
|
|
636
240
|
ledger: projected.ledger,
|
|
637
|
-
transcriptLeafId:
|
|
241
|
+
transcriptLeafId: snapshot.leafId,
|
|
638
242
|
};
|
|
639
243
|
}
|