@pasko70/pibo 3.1.0 → 3.1.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.
|
@@ -3,6 +3,7 @@ const RENDER_SEQUENCE_SLOTS_PER_MILLISECOND = 1_000;
|
|
|
3
3
|
const MAX_TRACKED_SESSIONS = 1_024;
|
|
4
4
|
const MAX_RECENT_SEGMENTS_PER_SESSION = 1_024;
|
|
5
5
|
const MAX_RECENT_TOOL_INVOCATIONS_PER_SESSION = 1_024;
|
|
6
|
+
const MAX_RECENT_OUTPUT_PARTS_PER_SESSION = 1_024;
|
|
6
7
|
export function outputRenderHighWaterStore(value) {
|
|
7
8
|
if (!value || typeof value !== "object")
|
|
8
9
|
return undefined;
|
|
@@ -32,7 +33,8 @@ export class OutputRenderSequencer {
|
|
|
32
33
|
position(event) {
|
|
33
34
|
const state = this.sessionState(event.piboSessionId);
|
|
34
35
|
this.trackActiveTurn(state, event);
|
|
35
|
-
const
|
|
36
|
+
const eventWithPartIdentity = this.positionOutputPart(state, event);
|
|
37
|
+
const eventWithToolIdentity = this.positionToolInvocation(state, eventWithPartIdentity);
|
|
36
38
|
const key = outputRenderSegmentKey(eventWithToolIdentity, state.activeEventId);
|
|
37
39
|
const supplied = validRenderSequence(eventWithToolIdentity.renderSequence)
|
|
38
40
|
? eventWithToolIdentity.renderSequence
|
|
@@ -90,11 +92,61 @@ export class OutputRenderSequencer {
|
|
|
90
92
|
activeSegments: new Set(),
|
|
91
93
|
completedSegments: new Set(),
|
|
92
94
|
toolInvocations: new Map(),
|
|
95
|
+
outputParts: new Map(),
|
|
93
96
|
lastSequence: 0,
|
|
94
97
|
};
|
|
95
98
|
this.sessions.set(piboSessionId, state);
|
|
96
99
|
return state;
|
|
97
100
|
}
|
|
101
|
+
positionOutputPart(state, event) {
|
|
102
|
+
const transition = outputPartTransition(event, state.activeEventId);
|
|
103
|
+
if (!transition)
|
|
104
|
+
return event;
|
|
105
|
+
const key = outputPartCounterKey(transition.eventId, transition.kind);
|
|
106
|
+
const parts = state.outputParts.get(key) ?? [];
|
|
107
|
+
if (!state.outputParts.has(key))
|
|
108
|
+
state.outputParts.set(key, parts);
|
|
109
|
+
const exact = parts.find((part) => part.fingerprints.has(transition.fingerprint));
|
|
110
|
+
const latestOpen = [...parts].reverse().find((part) => !part.closed);
|
|
111
|
+
const localMaximum = parts.reduce((maximum, part) => Math.max(maximum, part.index), -1);
|
|
112
|
+
const localIndex = exact?.index
|
|
113
|
+
?? latestOpen?.index
|
|
114
|
+
?? (transition.proposedIndex > localMaximum ? transition.proposedIndex : localMaximum + 1);
|
|
115
|
+
const index = validRenderSequence(event.renderSequence) || exact || latestOpen
|
|
116
|
+
? localIndex
|
|
117
|
+
: this.highWaterStore?.claimOrAttachOutputPart?.({ ...transition, proposedIndex: localIndex }) ?? localIndex;
|
|
118
|
+
let part = parts.find((candidate) => candidate.index === index);
|
|
119
|
+
if (!part) {
|
|
120
|
+
part = { index, fingerprints: new Set(), closed: false };
|
|
121
|
+
parts.push(part);
|
|
122
|
+
}
|
|
123
|
+
part.fingerprints.add(transition.fingerprint);
|
|
124
|
+
if (transition.terminal)
|
|
125
|
+
part.closed = true;
|
|
126
|
+
this.trimOutputParts(state);
|
|
127
|
+
return withOutputPartIndex(event, transition.kind, index);
|
|
128
|
+
}
|
|
129
|
+
trimOutputParts(state) {
|
|
130
|
+
let total = 0;
|
|
131
|
+
for (const parts of state.outputParts.values())
|
|
132
|
+
total += parts.length;
|
|
133
|
+
while (total > MAX_RECENT_OUTPUT_PARTS_PER_SESSION) {
|
|
134
|
+
let removed = false;
|
|
135
|
+
for (const [key, parts] of state.outputParts) {
|
|
136
|
+
const closedIndex = parts.findIndex((part) => part.closed);
|
|
137
|
+
if (closedIndex === -1)
|
|
138
|
+
continue;
|
|
139
|
+
parts.splice(closedIndex, 1);
|
|
140
|
+
total -= 1;
|
|
141
|
+
if (!parts.length)
|
|
142
|
+
state.outputParts.delete(key);
|
|
143
|
+
removed = true;
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
if (!removed)
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
98
150
|
positionToolInvocation(state, event) {
|
|
99
151
|
if (!isToolIdentityEvent(event) || !event.toolCallId)
|
|
100
152
|
return event;
|
|
@@ -243,6 +295,101 @@ function createInvocation(invocations, ordinal) {
|
|
|
243
295
|
invocations.push(invocation);
|
|
244
296
|
return invocation;
|
|
245
297
|
}
|
|
298
|
+
function outputPartTransition(event, activeEventId) {
|
|
299
|
+
const eventId = ("eventId" in event ? event.eventId : undefined) ?? activeEventId;
|
|
300
|
+
if (!eventId)
|
|
301
|
+
return undefined;
|
|
302
|
+
if (event.type === "assistant_delta" || event.type === "assistant_message") {
|
|
303
|
+
return {
|
|
304
|
+
piboSessionId: event.piboSessionId,
|
|
305
|
+
eventId,
|
|
306
|
+
kind: "assistant",
|
|
307
|
+
proposedIndex: event.assistantIndex ?? event.contentIndex ?? 0,
|
|
308
|
+
fingerprint: outputPartFingerprint(event),
|
|
309
|
+
identityFingerprint: outputIdentityFingerprint(event),
|
|
310
|
+
terminal: event.type === "assistant_message",
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
if (event.type === "thinking_started" || event.type === "thinking_delta" || event.type === "thinking_finished") {
|
|
314
|
+
return {
|
|
315
|
+
piboSessionId: event.piboSessionId,
|
|
316
|
+
eventId,
|
|
317
|
+
kind: "thinking",
|
|
318
|
+
proposedIndex: event.thinkingIndex ?? event.contentIndex ?? 0,
|
|
319
|
+
fingerprint: outputPartFingerprint(event),
|
|
320
|
+
identityFingerprint: outputIdentityFingerprint(event),
|
|
321
|
+
terminal: event.type === "thinking_finished",
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
if (event.type === "assistant_usage") {
|
|
325
|
+
return {
|
|
326
|
+
piboSessionId: event.piboSessionId,
|
|
327
|
+
eventId,
|
|
328
|
+
kind: "usage",
|
|
329
|
+
proposedIndex: event.usageIndex ?? 0,
|
|
330
|
+
fingerprint: outputPartFingerprint(event),
|
|
331
|
+
identityFingerprint: outputIdentityFingerprint(event),
|
|
332
|
+
terminal: true,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
if (event.type === "compaction_start" || event.type === "compaction_end") {
|
|
336
|
+
return {
|
|
337
|
+
piboSessionId: event.piboSessionId,
|
|
338
|
+
eventId,
|
|
339
|
+
kind: "compaction",
|
|
340
|
+
proposedIndex: event.compactionIndex ?? 0,
|
|
341
|
+
fingerprint: outputPartFingerprint(event),
|
|
342
|
+
identityFingerprint: outputIdentityFingerprint(event),
|
|
343
|
+
terminal: event.type === "compaction_end",
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
return undefined;
|
|
347
|
+
}
|
|
348
|
+
export function outputIdentityFingerprint(event) {
|
|
349
|
+
const payload = { ...event };
|
|
350
|
+
delete payload.renderSequence;
|
|
351
|
+
return createHash("sha256").update(stableJson(payload)).digest("hex");
|
|
352
|
+
}
|
|
353
|
+
export function outputPartFingerprint(event) {
|
|
354
|
+
const payload = { ...event };
|
|
355
|
+
delete payload.renderSequence;
|
|
356
|
+
delete payload.assistantIndex;
|
|
357
|
+
delete payload.thinkingIndex;
|
|
358
|
+
delete payload.usageIndex;
|
|
359
|
+
delete payload.compactionIndex;
|
|
360
|
+
delete payload.contentIndex;
|
|
361
|
+
return createHash("sha256").update(stableJson(payload)).digest("hex");
|
|
362
|
+
}
|
|
363
|
+
function stableJson(value) {
|
|
364
|
+
if (Array.isArray(value))
|
|
365
|
+
return `[${value.map(stableJson).join(",")}]`;
|
|
366
|
+
if (value && typeof value === "object") {
|
|
367
|
+
return `{${Object.entries(value)
|
|
368
|
+
.filter(([, entry]) => entry !== undefined)
|
|
369
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
370
|
+
.map(([key, entry]) => `${JSON.stringify(key)}:${stableJson(entry)}`)
|
|
371
|
+
.join(",")}}`;
|
|
372
|
+
}
|
|
373
|
+
return JSON.stringify(value) ?? "null";
|
|
374
|
+
}
|
|
375
|
+
function outputPartCounterKey(eventId, kind) {
|
|
376
|
+
return JSON.stringify([eventId, kind]);
|
|
377
|
+
}
|
|
378
|
+
function withOutputPartIndex(event, kind, index) {
|
|
379
|
+
if (kind === "assistant" && (event.type === "assistant_delta" || event.type === "assistant_message")) {
|
|
380
|
+
return event.assistantIndex === index ? event : { ...event, assistantIndex: index };
|
|
381
|
+
}
|
|
382
|
+
if (kind === "thinking" && (event.type === "thinking_started" || event.type === "thinking_delta" || event.type === "thinking_finished")) {
|
|
383
|
+
return event.thinkingIndex === index ? event : { ...event, thinkingIndex: index };
|
|
384
|
+
}
|
|
385
|
+
if (kind === "usage" && event.type === "assistant_usage") {
|
|
386
|
+
return event.usageIndex === index ? event : { ...event, usageIndex: index };
|
|
387
|
+
}
|
|
388
|
+
if (kind === "compaction" && (event.type === "compaction_start" || event.type === "compaction_end")) {
|
|
389
|
+
return event.compactionIndex === index ? event : { ...event, compactionIndex: index };
|
|
390
|
+
}
|
|
391
|
+
return event;
|
|
392
|
+
}
|
|
246
393
|
function segmentRemainsActive(event) {
|
|
247
394
|
return event.type === "message_started"
|
|
248
395
|
|| event.type === "assistant_delta"
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { outputIdentityFingerprint, outputPartFingerprint } from "../core/output-render-sequence.js";
|
|
2
3
|
import { rootSessionId } from "./session-store.js";
|
|
3
4
|
export class PiboOutputIdentityCollisionError extends Error {
|
|
4
5
|
idempotencyKey;
|
|
@@ -89,7 +90,8 @@ export class ChatDataIngestService {
|
|
|
89
90
|
ingestOutputEvent(input) {
|
|
90
91
|
const event = input.event;
|
|
91
92
|
const idempotencyKey = outputIdempotencyKey(event);
|
|
92
|
-
const identityFingerprint =
|
|
93
|
+
const identityFingerprint = outputIdentityFingerprint(event);
|
|
94
|
+
const partFingerprint = isOutputPartEvent(event) ? outputPartFingerprint(event) : undefined;
|
|
93
95
|
if (idempotencyKey) {
|
|
94
96
|
const existing = this.store.eventLog.findByIdempotencyKey(idempotencyKey);
|
|
95
97
|
if (existing) {
|
|
@@ -156,6 +158,7 @@ export class ChatDataIngestService {
|
|
|
156
158
|
previewText: previewTextForOutputEvent(event),
|
|
157
159
|
attributes: compactObject({
|
|
158
160
|
identityFingerprint,
|
|
161
|
+
outputPartFingerprint: partFingerprint,
|
|
159
162
|
eventIdentityScoped: eventIdForOutputEvent(event) !== undefined,
|
|
160
163
|
semanticEventId: eventIdForOutputEvent(event),
|
|
161
164
|
legacyStreamId: input.legacyStreamId,
|
|
@@ -270,11 +273,6 @@ function deterministicId(prefix, value) {
|
|
|
270
273
|
function hashJson(value) {
|
|
271
274
|
return createHash("sha256").update(canonicalJson(value)).digest("hex").slice(0, 16);
|
|
272
275
|
}
|
|
273
|
-
function outputEventFingerprint(event) {
|
|
274
|
-
const identity = { ...event };
|
|
275
|
-
delete identity.renderSequence;
|
|
276
|
-
return createHash("sha256").update(canonicalJson(identity)).digest("hex");
|
|
277
|
-
}
|
|
278
276
|
function canonicalJson(value) {
|
|
279
277
|
if (value === null || typeof value !== "object")
|
|
280
278
|
return JSON.stringify(value) ?? "null";
|
|
@@ -470,6 +468,16 @@ function observationStatusForOutputEvent(event) {
|
|
|
470
468
|
return "error";
|
|
471
469
|
return "completed";
|
|
472
470
|
}
|
|
471
|
+
function isOutputPartEvent(event) {
|
|
472
|
+
return event.type === "assistant_delta"
|
|
473
|
+
|| event.type === "assistant_message"
|
|
474
|
+
|| event.type === "thinking_started"
|
|
475
|
+
|| event.type === "thinking_delta"
|
|
476
|
+
|| event.type === "thinking_finished"
|
|
477
|
+
|| event.type === "assistant_usage"
|
|
478
|
+
|| event.type === "compaction_start"
|
|
479
|
+
|| event.type === "compaction_end";
|
|
480
|
+
}
|
|
473
481
|
function isTerminalOutputEvent(event) {
|
|
474
482
|
return !event.type.endsWith("_started") && event.type !== "tool_call" && event.type !== "assistant_delta" && event.type !== "thinking_delta" && event.type !== "tool_execution_updated";
|
|
475
483
|
}
|
package/dist/data/schema.js
CHANGED
|
@@ -199,6 +199,16 @@ function applyPiboDataSchemaInTransaction(db, hooks, previousVersion, tablesToRe
|
|
|
199
199
|
FOREIGN KEY (pibo_session_id) REFERENCES sessions(id) ON DELETE CASCADE
|
|
200
200
|
);
|
|
201
201
|
|
|
202
|
+
CREATE TABLE IF NOT EXISTS session_output_part_counters (
|
|
203
|
+
pibo_session_id TEXT NOT NULL,
|
|
204
|
+
event_id TEXT NOT NULL,
|
|
205
|
+
part_kind TEXT NOT NULL CHECK(part_kind IN ('assistant', 'thinking', 'usage', 'compaction')),
|
|
206
|
+
next_index INTEGER NOT NULL CHECK(next_index >= 0),
|
|
207
|
+
updated_at TEXT NOT NULL,
|
|
208
|
+
PRIMARY KEY (pibo_session_id, event_id, part_kind),
|
|
209
|
+
FOREIGN KEY (pibo_session_id) REFERENCES sessions(id) ON DELETE CASCADE
|
|
210
|
+
);
|
|
211
|
+
|
|
202
212
|
CREATE TABLE IF NOT EXISTS session_tool_invocation_counters (
|
|
203
213
|
pibo_session_id TEXT NOT NULL,
|
|
204
214
|
event_id TEXT NOT NULL,
|
|
@@ -147,6 +147,65 @@ export class PiboDataSessionStore {
|
|
|
147
147
|
`).run(sequence, new Date().toISOString(), piboSessionId);
|
|
148
148
|
});
|
|
149
149
|
}
|
|
150
|
+
claimOrAttachOutputPart(input) {
|
|
151
|
+
return this.dataStore.transaction(() => {
|
|
152
|
+
const indexAttribute = outputPartIndexAttribute(input.kind);
|
|
153
|
+
const eventTypes = outputPartEventTypes(input.kind);
|
|
154
|
+
const placeholders = eventTypes.map(() => "?").join(", ");
|
|
155
|
+
const rows = this.db.prepare(`
|
|
156
|
+
SELECT type, attributes_json
|
|
157
|
+
FROM event_log
|
|
158
|
+
WHERE session_id = ? AND event_id = ? AND type IN (${placeholders})
|
|
159
|
+
ORDER BY stream_id ASC
|
|
160
|
+
`).all(input.piboSessionId, input.eventId, ...eventTypes);
|
|
161
|
+
let maximum = -1;
|
|
162
|
+
const latestTypeByIndex = new Map();
|
|
163
|
+
for (const row of rows) {
|
|
164
|
+
const attributes = parseJsonObject(row.attributes_json);
|
|
165
|
+
const index = attributes[indexAttribute];
|
|
166
|
+
if (typeof index !== "number" || !Number.isSafeInteger(index) || index < 0)
|
|
167
|
+
continue;
|
|
168
|
+
maximum = Math.max(maximum, index);
|
|
169
|
+
if (attributes.outputPartFingerprint === input.fingerprint
|
|
170
|
+
|| attributes.identityFingerprint === input.identityFingerprint) {
|
|
171
|
+
this.observeOutputPartIndex(input, index);
|
|
172
|
+
return index;
|
|
173
|
+
}
|
|
174
|
+
latestTypeByIndex.set(index, row.type);
|
|
175
|
+
}
|
|
176
|
+
const latestOpen = [...latestTypeByIndex.entries()]
|
|
177
|
+
.filter(([, eventType]) => !outputPartEventIsTerminal(eventType))
|
|
178
|
+
.sort(([left], [right]) => right - left)[0]?.[0];
|
|
179
|
+
if (latestOpen !== undefined) {
|
|
180
|
+
this.observeOutputPartIndex(input, latestOpen);
|
|
181
|
+
return latestOpen;
|
|
182
|
+
}
|
|
183
|
+
const minimum = Math.max(input.proposedIndex, maximum + 1);
|
|
184
|
+
const row = this.db.prepare(`
|
|
185
|
+
INSERT INTO session_output_part_counters (
|
|
186
|
+
pibo_session_id, event_id, part_kind, next_index, updated_at
|
|
187
|
+
) VALUES (?, ?, ?, ? + 1, ?)
|
|
188
|
+
ON CONFLICT(pibo_session_id, event_id, part_kind) DO UPDATE SET
|
|
189
|
+
next_index = MAX(session_output_part_counters.next_index, ?) + 1,
|
|
190
|
+
updated_at = excluded.updated_at
|
|
191
|
+
RETURNING next_index - 1 AS part_index
|
|
192
|
+
`).get(input.piboSessionId, input.eventId, input.kind, minimum, new Date().toISOString(), minimum);
|
|
193
|
+
return row.part_index;
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
observeOutputPartIndex(input, index) {
|
|
197
|
+
this.db.prepare(`
|
|
198
|
+
INSERT INTO session_output_part_counters (
|
|
199
|
+
pibo_session_id, event_id, part_kind, next_index, updated_at
|
|
200
|
+
) VALUES (?, ?, ?, ?, ?)
|
|
201
|
+
ON CONFLICT(pibo_session_id, event_id, part_kind) DO UPDATE SET
|
|
202
|
+
next_index = MAX(session_output_part_counters.next_index, excluded.next_index),
|
|
203
|
+
updated_at = CASE
|
|
204
|
+
WHEN excluded.next_index > session_output_part_counters.next_index THEN excluded.updated_at
|
|
205
|
+
ELSE session_output_part_counters.updated_at
|
|
206
|
+
END
|
|
207
|
+
`).run(input.piboSessionId, input.eventId, input.kind, index + 1, new Date().toISOString());
|
|
208
|
+
}
|
|
150
209
|
claimOutputToolInvocationOrdinal(piboSessionId, eventId, toolCallId) {
|
|
151
210
|
return this.dataStore.transaction(() => {
|
|
152
211
|
const durable = this.db.prepare(`
|
|
@@ -562,6 +621,28 @@ function runtimeBindingFromRow(row) {
|
|
|
562
621
|
function isRuntimeBindingState(value) {
|
|
563
622
|
return value === "unbound" || value === "bound" || value === "missing" || value === "error";
|
|
564
623
|
}
|
|
624
|
+
function outputPartIndexAttribute(kind) {
|
|
625
|
+
switch (kind) {
|
|
626
|
+
case "assistant": return "assistantIndex";
|
|
627
|
+
case "thinking": return "thinkingIndex";
|
|
628
|
+
case "usage": return "usageIndex";
|
|
629
|
+
case "compaction": return "compactionIndex";
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
function outputPartEventTypes(kind) {
|
|
633
|
+
switch (kind) {
|
|
634
|
+
case "assistant": return ["assistant_delta", "assistant_message"];
|
|
635
|
+
case "thinking": return ["thinking_started", "thinking_delta", "thinking_finished"];
|
|
636
|
+
case "usage": return ["assistant_usage"];
|
|
637
|
+
case "compaction": return ["compaction_start", "compaction_end"];
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
function outputPartEventIsTerminal(eventType) {
|
|
641
|
+
return eventType === "assistant_message"
|
|
642
|
+
|| eventType === "thinking_finished"
|
|
643
|
+
|| eventType === "assistant_usage"
|
|
644
|
+
|| eventType === "compaction_end";
|
|
645
|
+
}
|
|
565
646
|
function parseJsonObject(json) {
|
|
566
647
|
if (!json)
|
|
567
648
|
return {};
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pasko70/pibo",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.1",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@pasko70/pibo",
|
|
9
|
-
"version": "3.1.
|
|
9
|
+
"version": "3.1.1",
|
|
10
10
|
"workspaces": [
|
|
11
11
|
"packages/workflows"
|
|
12
12
|
],
|