@pi-unipi/background-tasks 2.6.1
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/README.md +87 -0
- package/extensions/anthropic-attribution.ts +1 -0
- package/extensions/delegate-child.ts +1 -0
- package/extensions/fusion-child.ts +1 -0
- package/package.json +40 -0
- package/src/__tests__/anthropic-attribution.test.ts +195 -0
- package/src/__tests__/config.test.ts +137 -0
- package/src/__tests__/core.test.ts +493 -0
- package/src/__tests__/delegate-artifacts.test.ts +528 -0
- package/src/__tests__/delegate-budget.test.ts +456 -0
- package/src/__tests__/delegate-launch.test.ts +676 -0
- package/src/__tests__/delegate-result-package.test.ts +350 -0
- package/src/__tests__/delegate-seed.test.ts +392 -0
- package/src/__tests__/durable-fs.test.ts +559 -0
- package/src/__tests__/extension-api.test.ts +579 -0
- package/src/__tests__/fusion-artifacts.test.ts +1039 -0
- package/src/__tests__/fusion-budget.test.ts +1356 -0
- package/src/__tests__/fusion-claude-cache.test.ts +320 -0
- package/src/__tests__/fusion-config.test.ts +335 -0
- package/src/__tests__/fusion-context-prompts.test.ts +670 -0
- package/src/__tests__/fusion-evaluation.test.ts +315 -0
- package/src/__tests__/fusion-extraction-equivalence.test.ts +58 -0
- package/src/__tests__/fusion-golden-bytes.test.ts +35 -0
- package/src/__tests__/fusion-high-cardinality.test.ts +192 -0
- package/src/__tests__/fusion-model-selector.test.ts +205 -0
- package/src/__tests__/fusion-orchestrator.test.ts +1194 -0
- package/src/__tests__/fusion-rpc.test.ts +369 -0
- package/src/__tests__/fusion-sdk.test.ts +1226 -0
- package/src/__tests__/fusion-v5-core.test.ts +219 -0
- package/src/__tests__/fusion-validate-orchestrator.test.ts +240 -0
- package/src/__tests__/fusion-web-fetch.test.ts +485 -0
- package/src/__tests__/fusion-workflows.test.ts +59 -0
- package/src/__tests__/helpers/delegate-deterministic-seed.ts +109 -0
- package/src/__tests__/helpers/delegate-seed-subprocess.ts +10 -0
- package/src/__tests__/helpers/fusion-canonical-subprocess.ts +21 -0
- package/src/__tests__/helpers/fusion-canonical.ts +140 -0
- package/src/__tests__/helpers/fusion-fake-pi.ts +279 -0
- package/src/__tests__/helpers/fusion-golden-corpus.ts +500 -0
- package/src/__tests__/helpers/fusion-high-cardinality.ts +140 -0
- package/src/__tests__/helpers/normalize.ts +22 -0
- package/src/__tests__/helpers/pi-hook-contract-evidence.json +18 -0
- package/src/__tests__/pi-launch.test.ts +202 -0
- package/src/__tests__/registry.test.ts +1580 -0
- package/src/__tests__/scripted-provider/delegate-ambient-provider.test.ts +130 -0
- package/src/__tests__/scripted-provider/delegate-child-guard.test.ts +631 -0
- package/src/__tests__/scripted-provider/delegate-guard-provider.ts +403 -0
- package/src/__tests__/scripted-provider/follow-up.test.ts +448 -0
- package/src/__tests__/scripted-provider/fusion-output-recovery.test.ts +132 -0
- package/src/__tests__/scripted-provider/fusion-reason.test.ts +310 -0
- package/src/__tests__/scripted-provider/fusion-runtime-guard.test.ts +163 -0
- package/src/__tests__/scripted-provider/hook-contract-provider.ts +179 -0
- package/src/__tests__/scripted-provider/hook-probe-a.ts +3 -0
- package/src/__tests__/scripted-provider/hook-probe-b.ts +3 -0
- package/src/__tests__/scripted-provider/hook-probe-extension.ts +126 -0
- package/src/__tests__/scripted-provider/output-recovery-provider.ts +153 -0
- package/src/__tests__/scripted-provider/pi-hook-contract-evidence.json +18 -0
- package/src/__tests__/scripted-provider/pi-hook-contract.test.ts +477 -0
- package/src/__tests__/scripted-provider/runtime-guard-probe.ts +28 -0
- package/src/__tests__/scripted-provider/runtime-guard-provider.ts +49 -0
- package/src/__tests__/scripted-provider/scripted-provider-extension.ts +408 -0
- package/src/__tests__/task-manager.test.ts +479 -0
- package/src/__tests__/windows-taskkill.test.ts +161 -0
- package/src/anthropic-attribution-path.ts +21 -0
- package/src/anthropic-attribution.ts +1983 -0
- package/src/attested-pi-run.ts +612 -0
- package/src/child-process.ts +55 -0
- package/src/common.ts +8 -0
- package/src/config.ts +292 -0
- package/src/context-parent-snapshot.ts +142 -0
- package/src/context-token-budget.ts +903 -0
- package/src/context-visible-conversation-v2.ts +551 -0
- package/src/delegate/artifacts.ts +487 -0
- package/src/delegate/budget.ts +415 -0
- package/src/delegate/hook-contract-evidence.json +18 -0
- package/src/delegate/hook-contract.ts +154 -0
- package/src/delegate/launch.ts +497 -0
- package/src/delegate/result-package.ts +459 -0
- package/src/delegate/runner.ts +449 -0
- package/src/delegate/seed.ts +423 -0
- package/src/delegate/types.ts +323 -0
- package/src/delegate-child-extension.ts +978 -0
- package/src/delegate-extension.ts +806 -0
- package/src/durable-fs.ts +386 -0
- package/src/extension-api.ts +548 -0
- package/src/fixtures/delegate-context-incident.json +17 -0
- package/src/fixtures/fusion-golden-bytes.json +310 -0
- package/src/fixtures/fusion-validate-golden-bytes.json +282 -0
- package/src/fusion/artifacts.ts +967 -0
- package/src/fusion/budget.ts +1162 -0
- package/src/fusion/child-protocol.ts +305 -0
- package/src/fusion/claude-cache.ts +207 -0
- package/src/fusion/clean-context.ts +91 -0
- package/src/fusion/config.ts +449 -0
- package/src/fusion/context.ts +265 -0
- package/src/fusion/evaluation.ts +800 -0
- package/src/fusion/orchestrator.ts +1288 -0
- package/src/fusion/output-contract.ts +34 -0
- package/src/fusion/pi-child.ts +2373 -0
- package/src/fusion/prompts.ts +345 -0
- package/src/fusion/result-package.ts +959 -0
- package/src/fusion/source-policy.ts +257 -0
- package/src/fusion/types.ts +1139 -0
- package/src/fusion/web-fetch.ts +1060 -0
- package/src/fusion/workflows.ts +184 -0
- package/src/fusion-child-extension.ts +1052 -0
- package/src/fusion-extension.ts +1293 -0
- package/src/index.ts +295 -0
- package/src/pi-launch.ts +225 -0
- package/src/registry.ts +2424 -0
- package/src/settings-overlay.ts +208 -0
- package/src/task-manager.ts +774 -0
- package/src/tools.ts +530 -0
- package/src/turndown.d.ts +15 -0
- package/src/types.ts +963 -0
- package/src/ui/fusion-model-selector.ts +322 -0
- package/src/windows-taskkill.ts +250 -0
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import type { Message } from '@earendil-works/pi-ai';
|
|
3
|
+
import { canonicalJson } from './attested-pi-run.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Frozen `visible-conversation-ledger-v2` transform.
|
|
7
|
+
*
|
|
8
|
+
* This is the single shared implementation of the package's conversation
|
|
9
|
+
* projection: visible user/assistant text verbatim, assistant thinking and all
|
|
10
|
+
* tool traffic replaced with deterministic hash-accounted omission receipts,
|
|
11
|
+
* images marker-only, unknown block types a loud error.
|
|
12
|
+
*
|
|
13
|
+
* The transform has **no behavioural knobs**. Every consumer receives exactly
|
|
14
|
+
* the same disposition for exactly the same input. Consumer identity — schema
|
|
15
|
+
* versions, policy ids, request authority, branch-filter provenance — lives
|
|
16
|
+
* entirely outside this module, so a consumer can never claim behaviour that did
|
|
17
|
+
* not occur. A semantic change requires a new transform id and a new
|
|
18
|
+
* implementation, never a flag added here.
|
|
19
|
+
*
|
|
20
|
+
* Fusion's persisted artifact bytes are frozen against this implementation and
|
|
21
|
+
* are proven bit-identical by `tests/unit/fusion-golden-bytes.test.ts`.
|
|
22
|
+
*/
|
|
23
|
+
export const VISIBLE_CONVERSATION_TRANSFORM_ID = 'visible-conversation-ledger-v2';
|
|
24
|
+
|
|
25
|
+
export const CONVERSATION_IMAGE_OMISSION_PREFIX =
|
|
26
|
+
'[Image omitted from fusion text transcript: ';
|
|
27
|
+
|
|
28
|
+
export const OMITTED_EVENT_KINDS = [
|
|
29
|
+
'assistant_thinking',
|
|
30
|
+
'tool_call',
|
|
31
|
+
'tool_result_text',
|
|
32
|
+
'tool_result_image',
|
|
33
|
+
] as const;
|
|
34
|
+
export type OmittedEventKind = (typeof OMITTED_EVENT_KINDS)[number];
|
|
35
|
+
|
|
36
|
+
export interface OmittedEventRecord {
|
|
37
|
+
index: number;
|
|
38
|
+
source_ordinal: number;
|
|
39
|
+
block_ordinal: number;
|
|
40
|
+
kind: OmittedEventKind;
|
|
41
|
+
payload_bytes: number;
|
|
42
|
+
payload_sha256: string;
|
|
43
|
+
tool_name?: string;
|
|
44
|
+
tool_call_id?: string;
|
|
45
|
+
mime_type?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface OmittedActivityProjectionMapEntry {
|
|
49
|
+
canonical_entry_index: number;
|
|
50
|
+
entry_kind: 'omitted_activity';
|
|
51
|
+
ledger_index_first: number;
|
|
52
|
+
ledger_index_last: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface LedgerOnlyImageProjectionMapEntry {
|
|
56
|
+
entry_kind: 'ledger_only_tool_result_image';
|
|
57
|
+
ledger_index_first: number;
|
|
58
|
+
ledger_index_last: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type ContextProjectionMapEntry =
|
|
62
|
+
| OmittedActivityProjectionMapEntry
|
|
63
|
+
| LedgerOnlyImageProjectionMapEntry;
|
|
64
|
+
|
|
65
|
+
export interface ProjectionTextEntry {
|
|
66
|
+
kind: 'text';
|
|
67
|
+
source_ordinal: number;
|
|
68
|
+
block_ordinal: number;
|
|
69
|
+
role: 'user' | 'assistant';
|
|
70
|
+
text: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Per-kind counts for one omitted run. Zero-valued kinds are omitted from the
|
|
75
|
+
* serialized receipt by a fixed policy rule so receipt size does not scale with
|
|
76
|
+
* the number of tracked kinds; absent means exactly zero.
|
|
77
|
+
*/
|
|
78
|
+
export interface OmittedRunCounts {
|
|
79
|
+
assistant_thinking?: number;
|
|
80
|
+
tool_calls?: number;
|
|
81
|
+
tool_result_texts?: number;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface ProjectionOmissionEntry {
|
|
85
|
+
kind: 'omitted_activity';
|
|
86
|
+
at: readonly [number, number];
|
|
87
|
+
bytes: number;
|
|
88
|
+
counts: OmittedRunCounts;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export type ProjectionEntry = ProjectionTextEntry | ProjectionOmissionEntry;
|
|
92
|
+
|
|
93
|
+
export interface ToolCallNameCount {
|
|
94
|
+
name: string;
|
|
95
|
+
calls: number;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface ProjectionAccounting {
|
|
99
|
+
message_count: number;
|
|
100
|
+
included_text_entry_count: number;
|
|
101
|
+
included_user_text_bytes: number;
|
|
102
|
+
included_assistant_text_bytes: number;
|
|
103
|
+
included_image_marker_count: number;
|
|
104
|
+
empty_text_block_count: number;
|
|
105
|
+
omitted_run_count: number;
|
|
106
|
+
omitted_event_count: number;
|
|
107
|
+
omitted_thinking_bytes: number;
|
|
108
|
+
omitted_tool_call_count: number;
|
|
109
|
+
omitted_tool_call_argument_bytes: number;
|
|
110
|
+
omitted_tool_result_text_count: number;
|
|
111
|
+
omitted_tool_result_text_bytes: number;
|
|
112
|
+
omitted_tool_result_image_count: number;
|
|
113
|
+
omitted_tool_result_image_bytes: number;
|
|
114
|
+
tool_call_names: readonly ToolCallNameCount[];
|
|
115
|
+
ledger_entry_count: number;
|
|
116
|
+
ledger_root_sha256: string;
|
|
117
|
+
omission_receipt_utf8_bytes: number;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Ledger body produced by the transform, carrying no consumer envelope.
|
|
122
|
+
*
|
|
123
|
+
* The ledger root commits only to the ledger rows, never to schema version,
|
|
124
|
+
* policy id, or transform id, so this body can be sealed by different consumers
|
|
125
|
+
* without changing the root. That property is pinned by a dedicated test.
|
|
126
|
+
*/
|
|
127
|
+
export interface LedgerBodyV2 {
|
|
128
|
+
entries: readonly OmittedEventRecord[];
|
|
129
|
+
projection_map: readonly ContextProjectionMapEntry[];
|
|
130
|
+
root_sha256: string;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface ProjectedConversationV2 {
|
|
134
|
+
entries: readonly ProjectionEntry[];
|
|
135
|
+
accounting: ProjectionAccounting;
|
|
136
|
+
ledger: LedgerBodyV2;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Raised when the transform meets a conversation block it refuses to silently drop. */
|
|
140
|
+
export class UnsupportedConversationBlockError extends Error {
|
|
141
|
+
readonly label: string;
|
|
142
|
+
|
|
143
|
+
constructor(label: string) {
|
|
144
|
+
super(
|
|
145
|
+
`fusion context projection encountered an unsupported conversation block: ${label}`,
|
|
146
|
+
);
|
|
147
|
+
this.name = 'UnsupportedConversationBlockError';
|
|
148
|
+
this.label = label;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function utf8Bytes(value: string): number {
|
|
153
|
+
return Buffer.byteLength(value, 'utf8');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function sha256Hex(bytes: Buffer): string {
|
|
157
|
+
return createHash('sha256').update(bytes).digest('hex');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function uint64be(value: number): Buffer {
|
|
161
|
+
const out = Buffer.alloc(8);
|
|
162
|
+
out.writeBigUInt64BE(BigInt(value));
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Length-prefixed framing so concatenated fields cannot collide across boundaries. */
|
|
167
|
+
function lengthPrefixed(bytes: Buffer): Buffer {
|
|
168
|
+
return Buffer.concat([uint64be(bytes.length), bytes]);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function ledgerLeafHash(index: number, record: OmittedEventRecord): Buffer {
|
|
172
|
+
return createHash('sha256')
|
|
173
|
+
.update(Buffer.from('pi-fusion-ledger-leaf-v1\0', 'utf8'))
|
|
174
|
+
.update(uint64be(index))
|
|
175
|
+
.update(lengthPrefixed(Buffer.from(canonicalJson(record), 'utf8')))
|
|
176
|
+
.digest();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function ledgerRootHash(leaves: readonly Buffer[]): string {
|
|
180
|
+
const hash = createHash('sha256')
|
|
181
|
+
.update(Buffer.from('pi-fusion-ledger-root-v1\0', 'utf8'))
|
|
182
|
+
.update(uint64be(leaves.length));
|
|
183
|
+
for (const leaf of leaves) hash.update(leaf);
|
|
184
|
+
return hash.digest('hex');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function unsupportedBlock(label: string): UnsupportedConversationBlockError {
|
|
188
|
+
return new UnsupportedConversationBlockError(label);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Fixed policy rule: a zero-valued kind is absent from the receipt rather than
|
|
193
|
+
* serialized as `0`. This keeps receipts compact without losing information —
|
|
194
|
+
* absent is defined by the policy version to mean exactly zero — and it is
|
|
195
|
+
* applied unconditionally, never adaptively because an input happens to be large.
|
|
196
|
+
*/
|
|
197
|
+
function compactCounts(counts: Required<OmittedRunCounts>): OmittedRunCounts {
|
|
198
|
+
const out: OmittedRunCounts = {};
|
|
199
|
+
if (counts.assistant_thinking > 0) out.assistant_thinking = counts.assistant_thinking;
|
|
200
|
+
if (counts.tool_calls > 0) out.tool_calls = counts.tool_calls;
|
|
201
|
+
if (counts.tool_result_texts > 0) out.tool_result_texts = counts.tool_result_texts;
|
|
202
|
+
return out;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function imageMarker(mimeType: string): string {
|
|
206
|
+
return `${CONVERSATION_IMAGE_OMISSION_PREFIX}${mimeType}]`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Accumulates omitted-event ledger rows and turns maximal contiguous non-image
|
|
211
|
+
* omission runs into compact, source-ordered receipts inside the projection.
|
|
212
|
+
*/
|
|
213
|
+
class ProjectionBuilder {
|
|
214
|
+
private readonly entries: ProjectionEntry[] = [];
|
|
215
|
+
private readonly ledger: OmittedEventRecord[] = [];
|
|
216
|
+
private readonly leaves: Buffer[] = [];
|
|
217
|
+
private readonly projectionMap: ContextProjectionMapEntry[] = [];
|
|
218
|
+
private readonly toolCallNames = new Map<string, number>();
|
|
219
|
+
private pendingCounts: Required<OmittedRunCounts> | undefined;
|
|
220
|
+
private pendingBytes = 0;
|
|
221
|
+
private pendingLedgerFirst = 0;
|
|
222
|
+
private pendingLedgerLast = 0;
|
|
223
|
+
private pendingSourceFirst = 0;
|
|
224
|
+
private pendingSourceLast = 0;
|
|
225
|
+
|
|
226
|
+
includedUserTextBytes = 0;
|
|
227
|
+
includedAssistantTextBytes = 0;
|
|
228
|
+
includedImageMarkers = 0;
|
|
229
|
+
emptyTextBlocks = 0;
|
|
230
|
+
|
|
231
|
+
addText(entry: ProjectionTextEntry): void {
|
|
232
|
+
this.flush();
|
|
233
|
+
this.entries.push(entry);
|
|
234
|
+
if (entry.role === 'user') this.includedUserTextBytes += utf8Bytes(entry.text);
|
|
235
|
+
else this.includedAssistantTextBytes += utf8Bytes(entry.text);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
addOmission(
|
|
239
|
+
sourceOrdinal: number,
|
|
240
|
+
blockOrdinal: number,
|
|
241
|
+
kind: OmittedEventKind,
|
|
242
|
+
payload: Buffer,
|
|
243
|
+
extra: { toolName?: string; toolCallId?: string; mimeType?: string } = {},
|
|
244
|
+
): void {
|
|
245
|
+
const index = this.ledger.length;
|
|
246
|
+
const record: OmittedEventRecord = {
|
|
247
|
+
index,
|
|
248
|
+
source_ordinal: sourceOrdinal,
|
|
249
|
+
block_ordinal: blockOrdinal,
|
|
250
|
+
kind,
|
|
251
|
+
payload_bytes: payload.length,
|
|
252
|
+
payload_sha256: sha256Hex(payload),
|
|
253
|
+
};
|
|
254
|
+
if (extra.toolName !== undefined) record.tool_name = extra.toolName;
|
|
255
|
+
if (extra.toolCallId !== undefined) record.tool_call_id = extra.toolCallId;
|
|
256
|
+
if (extra.mimeType !== undefined) record.mime_type = extra.mimeType;
|
|
257
|
+
this.ledger.push(record);
|
|
258
|
+
this.leaves.push(ledgerLeafHash(index, record));
|
|
259
|
+
|
|
260
|
+
if (extra.toolName !== undefined && kind === 'tool_call') {
|
|
261
|
+
this.toolCallNames.set(extra.toolName, (this.toolCallNames.get(extra.toolName) ?? 0) + 1);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (kind === 'tool_result_image') {
|
|
265
|
+
this.flush();
|
|
266
|
+
this.addLedgerOnlyImageMap(index);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (this.pendingCounts === undefined) {
|
|
271
|
+
this.pendingCounts = {
|
|
272
|
+
assistant_thinking: 0,
|
|
273
|
+
tool_calls: 0,
|
|
274
|
+
tool_result_texts: 0,
|
|
275
|
+
};
|
|
276
|
+
this.pendingBytes = 0;
|
|
277
|
+
this.pendingLedgerFirst = index;
|
|
278
|
+
this.pendingSourceFirst = sourceOrdinal;
|
|
279
|
+
}
|
|
280
|
+
this.pendingLedgerLast = index;
|
|
281
|
+
this.pendingSourceLast = sourceOrdinal;
|
|
282
|
+
this.pendingBytes += payload.length;
|
|
283
|
+
if (kind === 'assistant_thinking') this.pendingCounts.assistant_thinking += 1;
|
|
284
|
+
else if (kind === 'tool_call') this.pendingCounts.tool_calls += 1;
|
|
285
|
+
else this.pendingCounts.tool_result_texts += 1;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private addLedgerOnlyImageMap(index: number): void {
|
|
289
|
+
const previous = this.projectionMap[this.projectionMap.length - 1];
|
|
290
|
+
if (
|
|
291
|
+
previous !== undefined &&
|
|
292
|
+
previous.entry_kind === 'ledger_only_tool_result_image' &&
|
|
293
|
+
previous.ledger_index_last + 1 === index
|
|
294
|
+
) {
|
|
295
|
+
previous.ledger_index_last = index;
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
this.projectionMap.push({
|
|
299
|
+
entry_kind: 'ledger_only_tool_result_image',
|
|
300
|
+
ledger_index_first: index,
|
|
301
|
+
ledger_index_last: index,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private flush(): void {
|
|
306
|
+
const counts = this.pendingCounts;
|
|
307
|
+
if (counts === undefined) return;
|
|
308
|
+
const canonicalEntryIndex = this.entries.length;
|
|
309
|
+
const entry: ProjectionOmissionEntry = {
|
|
310
|
+
at: [this.pendingSourceFirst, this.pendingSourceLast],
|
|
311
|
+
bytes: this.pendingBytes,
|
|
312
|
+
counts: compactCounts(counts),
|
|
313
|
+
kind: 'omitted_activity',
|
|
314
|
+
};
|
|
315
|
+
this.entries.push(entry);
|
|
316
|
+
this.projectionMap.push({
|
|
317
|
+
canonical_entry_index: canonicalEntryIndex,
|
|
318
|
+
entry_kind: 'omitted_activity',
|
|
319
|
+
ledger_index_first: this.pendingLedgerFirst,
|
|
320
|
+
ledger_index_last: this.pendingLedgerLast,
|
|
321
|
+
});
|
|
322
|
+
this.pendingCounts = undefined;
|
|
323
|
+
this.pendingBytes = 0;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
finish(messageCount: number): ProjectedConversationV2 {
|
|
327
|
+
this.flush();
|
|
328
|
+
const rootSha256 = ledgerRootHash(this.leaves);
|
|
329
|
+
let omittedRunCount = 0;
|
|
330
|
+
let includedTextEntries = 0;
|
|
331
|
+
let receiptBytes = 0;
|
|
332
|
+
for (const entry of this.entries) {
|
|
333
|
+
if (entry.kind === 'text') includedTextEntries += 1;
|
|
334
|
+
else {
|
|
335
|
+
omittedRunCount += 1;
|
|
336
|
+
receiptBytes += utf8Bytes(canonicalJson(entry));
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
let thinkingBytes = 0;
|
|
340
|
+
let toolCallCount = 0;
|
|
341
|
+
let toolArgumentBytes = 0;
|
|
342
|
+
let toolResultTextCount = 0;
|
|
343
|
+
let toolResultTextBytes = 0;
|
|
344
|
+
let toolResultImageCount = 0;
|
|
345
|
+
let toolResultImageBytes = 0;
|
|
346
|
+
for (const row of this.ledger) {
|
|
347
|
+
if (row.kind === 'assistant_thinking') thinkingBytes += row.payload_bytes;
|
|
348
|
+
else if (row.kind === 'tool_call') {
|
|
349
|
+
toolCallCount += 1;
|
|
350
|
+
toolArgumentBytes += row.payload_bytes;
|
|
351
|
+
} else if (row.kind === 'tool_result_text') {
|
|
352
|
+
toolResultTextCount += 1;
|
|
353
|
+
toolResultTextBytes += row.payload_bytes;
|
|
354
|
+
} else {
|
|
355
|
+
toolResultImageCount += 1;
|
|
356
|
+
toolResultImageBytes += row.payload_bytes;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
const toolCallNames: ToolCallNameCount[] = [...this.toolCallNames.entries()]
|
|
360
|
+
.map(([name, calls]) => ({ name, calls }))
|
|
361
|
+
.sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));
|
|
362
|
+
const accounting: ProjectionAccounting = {
|
|
363
|
+
message_count: messageCount,
|
|
364
|
+
included_text_entry_count: includedTextEntries,
|
|
365
|
+
included_user_text_bytes: this.includedUserTextBytes,
|
|
366
|
+
included_assistant_text_bytes: this.includedAssistantTextBytes,
|
|
367
|
+
included_image_marker_count: this.includedImageMarkers,
|
|
368
|
+
empty_text_block_count: this.emptyTextBlocks,
|
|
369
|
+
omitted_run_count: omittedRunCount,
|
|
370
|
+
omitted_event_count: this.ledger.length,
|
|
371
|
+
omitted_thinking_bytes: thinkingBytes,
|
|
372
|
+
omitted_tool_call_count: toolCallCount,
|
|
373
|
+
omitted_tool_call_argument_bytes: toolArgumentBytes,
|
|
374
|
+
omitted_tool_result_text_count: toolResultTextCount,
|
|
375
|
+
omitted_tool_result_text_bytes: toolResultTextBytes,
|
|
376
|
+
omitted_tool_result_image_count: toolResultImageCount,
|
|
377
|
+
omitted_tool_result_image_bytes: toolResultImageBytes,
|
|
378
|
+
tool_call_names: toolCallNames,
|
|
379
|
+
ledger_entry_count: this.ledger.length,
|
|
380
|
+
ledger_root_sha256: rootSha256,
|
|
381
|
+
omission_receipt_utf8_bytes: receiptBytes,
|
|
382
|
+
};
|
|
383
|
+
return {
|
|
384
|
+
entries: this.entries,
|
|
385
|
+
accounting,
|
|
386
|
+
ledger: {
|
|
387
|
+
entries: this.ledger,
|
|
388
|
+
projection_map: this.projectionMap,
|
|
389
|
+
root_sha256: rootSha256,
|
|
390
|
+
},
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Projects one user-role message. User text is retained verbatim; images stay
|
|
397
|
+
* marker-only so the child never receives raw bytes.
|
|
398
|
+
*/
|
|
399
|
+
function projectUserMessage(
|
|
400
|
+
builder: ProjectionBuilder,
|
|
401
|
+
content: Message['content'],
|
|
402
|
+
sourceOrdinal: number,
|
|
403
|
+
): void {
|
|
404
|
+
if (typeof content === 'string') {
|
|
405
|
+
if (content.length === 0) {
|
|
406
|
+
builder.emptyTextBlocks += 1;
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
builder.addText({
|
|
410
|
+
kind: 'text',
|
|
411
|
+
source_ordinal: sourceOrdinal,
|
|
412
|
+
block_ordinal: 0,
|
|
413
|
+
role: 'user',
|
|
414
|
+
text: content,
|
|
415
|
+
});
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
for (const [blockOrdinal, block] of content.entries()) {
|
|
419
|
+
if (block.type === 'text') {
|
|
420
|
+
if (block.text.length === 0) {
|
|
421
|
+
builder.emptyTextBlocks += 1;
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
builder.addText({
|
|
425
|
+
kind: 'text',
|
|
426
|
+
source_ordinal: sourceOrdinal,
|
|
427
|
+
block_ordinal: blockOrdinal,
|
|
428
|
+
role: 'user',
|
|
429
|
+
text: block.text,
|
|
430
|
+
});
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
if (block.type === 'image') {
|
|
434
|
+
builder.includedImageMarkers += 1;
|
|
435
|
+
builder.addText({
|
|
436
|
+
kind: 'text',
|
|
437
|
+
source_ordinal: sourceOrdinal,
|
|
438
|
+
block_ordinal: blockOrdinal,
|
|
439
|
+
role: 'user',
|
|
440
|
+
text: imageMarker(block.mimeType),
|
|
441
|
+
});
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
throw unsupportedBlock(`user block ${String(Reflect.get(block, 'type'))}`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function projectAssistantMessage(
|
|
449
|
+
builder: ProjectionBuilder,
|
|
450
|
+
content: Extract<Message, { role: 'assistant' }>['content'],
|
|
451
|
+
sourceOrdinal: number,
|
|
452
|
+
): void {
|
|
453
|
+
for (const [blockOrdinal, block] of content.entries()) {
|
|
454
|
+
if (block.type === 'text') {
|
|
455
|
+
if (block.text.length === 0) {
|
|
456
|
+
builder.emptyTextBlocks += 1;
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
builder.addText({
|
|
460
|
+
kind: 'text',
|
|
461
|
+
source_ordinal: sourceOrdinal,
|
|
462
|
+
block_ordinal: blockOrdinal,
|
|
463
|
+
role: 'assistant',
|
|
464
|
+
text: block.text,
|
|
465
|
+
});
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
if (block.type === 'thinking') {
|
|
469
|
+
builder.addOmission(
|
|
470
|
+
sourceOrdinal,
|
|
471
|
+
blockOrdinal,
|
|
472
|
+
'assistant_thinking',
|
|
473
|
+
Buffer.from(block.thinking, 'utf8'),
|
|
474
|
+
);
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
if (block.type === 'toolCall') {
|
|
478
|
+
builder.addOmission(
|
|
479
|
+
sourceOrdinal,
|
|
480
|
+
blockOrdinal,
|
|
481
|
+
'tool_call',
|
|
482
|
+
Buffer.from(canonicalJson(block.arguments), 'utf8'),
|
|
483
|
+
{ toolName: block.name, toolCallId: block.id },
|
|
484
|
+
);
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
throw unsupportedBlock(`assistant block ${String(Reflect.get(block, 'type'))}`);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function projectToolResultMessage(
|
|
492
|
+
builder: ProjectionBuilder,
|
|
493
|
+
message: Extract<Message, { role: 'toolResult' }>,
|
|
494
|
+
sourceOrdinal: number,
|
|
495
|
+
): void {
|
|
496
|
+
const content = message.content;
|
|
497
|
+
if (typeof content === 'string') {
|
|
498
|
+
builder.addOmission(sourceOrdinal, 0, 'tool_result_text', Buffer.from(content, 'utf8'), {
|
|
499
|
+
toolName: message.toolName,
|
|
500
|
+
toolCallId: message.toolCallId,
|
|
501
|
+
});
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
for (const [blockOrdinal, block] of content.entries()) {
|
|
505
|
+
if (block.type === 'text') {
|
|
506
|
+
builder.addOmission(
|
|
507
|
+
sourceOrdinal,
|
|
508
|
+
blockOrdinal,
|
|
509
|
+
'tool_result_text',
|
|
510
|
+
Buffer.from(block.text, 'utf8'),
|
|
511
|
+
{ toolName: message.toolName, toolCallId: message.toolCallId },
|
|
512
|
+
);
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
if (block.type === 'image') {
|
|
516
|
+
builder.addOmission(
|
|
517
|
+
sourceOrdinal,
|
|
518
|
+
blockOrdinal,
|
|
519
|
+
'tool_result_image',
|
|
520
|
+
Buffer.from(block.data, 'utf8'),
|
|
521
|
+
{
|
|
522
|
+
toolName: message.toolName,
|
|
523
|
+
toolCallId: message.toolCallId,
|
|
524
|
+
mimeType: block.mimeType,
|
|
525
|
+
},
|
|
526
|
+
);
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
throw unsupportedBlock(`tool result block ${String(Reflect.get(block, 'type'))}`);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Deterministic conversation projection. Every retained source block receives
|
|
535
|
+
* exactly one disposition: included verbatim, or represented as an omission
|
|
536
|
+
* ledger row. Unknown block types fail loudly instead of disappearing.
|
|
537
|
+
*/
|
|
538
|
+
export function projectVisibleConversationV2(
|
|
539
|
+
messages: readonly Message[],
|
|
540
|
+
): ProjectedConversationV2 {
|
|
541
|
+
const builder = new ProjectionBuilder();
|
|
542
|
+
for (const [sourceOrdinal, message] of messages.entries()) {
|
|
543
|
+
if (message.role === 'user') projectUserMessage(builder, message.content, sourceOrdinal);
|
|
544
|
+
else if (message.role === 'assistant')
|
|
545
|
+
projectAssistantMessage(builder, message.content, sourceOrdinal);
|
|
546
|
+
else if (message.role === 'toolResult')
|
|
547
|
+
projectToolResultMessage(builder, message, sourceOrdinal);
|
|
548
|
+
else throw unsupportedBlock(`message role ${String(Reflect.get(message, 'role'))}`);
|
|
549
|
+
}
|
|
550
|
+
return builder.finish(messages.length);
|
|
551
|
+
}
|