@pasko70/pibo 3.1.0 → 3.1.2

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 eventWithToolIdentity = this.positionToolInvocation(state, event);
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;
@@ -130,7 +182,7 @@ export class OutputRenderSequencer {
130
182
  if (!latest) {
131
183
  invocation = this.createToolInvocation(invocations, event.piboSessionId, eventId, event.toolCallId, transition);
132
184
  }
133
- else if (event.type === "tool_call" && (latest.seen.has("tool_call") || (latest.closed && latest.persistedIdentity))) {
185
+ else if (event.type === "tool_call" && toolCallStartsNewInvocation(latest, transition?.callFingerprint)) {
134
186
  invocation = this.createToolInvocation(invocations, event.piboSessionId, eventId, event.toolCallId, transition);
135
187
  }
136
188
  else if (event.type === "tool_execution_started" && latest.closed && latest.seen.has("tool_execution_started")) {
@@ -143,6 +195,8 @@ export class OutputRenderSequencer {
143
195
  }
144
196
  }
145
197
  invocation.seen.add(event.type);
198
+ if (event.type === "tool_call" && transition?.callFingerprint)
199
+ invocation.callFingerprint = transition.callFingerprint;
146
200
  if (event.type === "tool_execution_finished")
147
201
  invocation.closed = true;
148
202
  this.trimToolInvocations(state);
@@ -243,6 +297,110 @@ function createInvocation(invocations, ordinal) {
243
297
  invocations.push(invocation);
244
298
  return invocation;
245
299
  }
300
+ function toolCallStartsNewInvocation(invocation, callFingerprint) {
301
+ if (!invocation.seen.has("tool_call"))
302
+ return invocation.closed && invocation.persistedIdentity;
303
+ if (invocation.closed)
304
+ return true;
305
+ if (invocation.callFingerprint === undefined)
306
+ return false;
307
+ return callFingerprint === undefined || invocation.callFingerprint !== callFingerprint;
308
+ }
309
+ function outputPartTransition(event, activeEventId) {
310
+ const eventId = ("eventId" in event ? event.eventId : undefined) ?? activeEventId;
311
+ if (!eventId)
312
+ return undefined;
313
+ if (event.type === "assistant_delta" || event.type === "assistant_message") {
314
+ return {
315
+ piboSessionId: event.piboSessionId,
316
+ eventId,
317
+ kind: "assistant",
318
+ proposedIndex: event.assistantIndex ?? event.contentIndex ?? 0,
319
+ fingerprint: outputPartFingerprint(event),
320
+ identityFingerprint: outputIdentityFingerprint(event),
321
+ terminal: event.type === "assistant_message",
322
+ };
323
+ }
324
+ if (event.type === "thinking_started" || event.type === "thinking_delta" || event.type === "thinking_finished") {
325
+ return {
326
+ piboSessionId: event.piboSessionId,
327
+ eventId,
328
+ kind: "thinking",
329
+ proposedIndex: event.thinkingIndex ?? event.contentIndex ?? 0,
330
+ fingerprint: outputPartFingerprint(event),
331
+ identityFingerprint: outputIdentityFingerprint(event),
332
+ terminal: event.type === "thinking_finished",
333
+ };
334
+ }
335
+ if (event.type === "assistant_usage") {
336
+ return {
337
+ piboSessionId: event.piboSessionId,
338
+ eventId,
339
+ kind: "usage",
340
+ proposedIndex: event.usageIndex ?? 0,
341
+ fingerprint: outputPartFingerprint(event),
342
+ identityFingerprint: outputIdentityFingerprint(event),
343
+ terminal: true,
344
+ };
345
+ }
346
+ if (event.type === "compaction_start" || event.type === "compaction_end") {
347
+ return {
348
+ piboSessionId: event.piboSessionId,
349
+ eventId,
350
+ kind: "compaction",
351
+ proposedIndex: event.compactionIndex ?? 0,
352
+ fingerprint: outputPartFingerprint(event),
353
+ identityFingerprint: outputIdentityFingerprint(event),
354
+ terminal: event.type === "compaction_end",
355
+ };
356
+ }
357
+ return undefined;
358
+ }
359
+ export function outputIdentityFingerprint(event) {
360
+ const payload = { ...event };
361
+ delete payload.renderSequence;
362
+ return createHash("sha256").update(stableJson(payload)).digest("hex");
363
+ }
364
+ export function outputPartFingerprint(event) {
365
+ const payload = { ...event };
366
+ delete payload.renderSequence;
367
+ delete payload.assistantIndex;
368
+ delete payload.thinkingIndex;
369
+ delete payload.usageIndex;
370
+ delete payload.compactionIndex;
371
+ delete payload.contentIndex;
372
+ return createHash("sha256").update(stableJson(payload)).digest("hex");
373
+ }
374
+ function stableJson(value) {
375
+ if (Array.isArray(value))
376
+ return `[${value.map(stableJson).join(",")}]`;
377
+ if (value && typeof value === "object") {
378
+ return `{${Object.entries(value)
379
+ .filter(([, entry]) => entry !== undefined)
380
+ .sort(([left], [right]) => left.localeCompare(right))
381
+ .map(([key, entry]) => `${JSON.stringify(key)}:${stableJson(entry)}`)
382
+ .join(",")}}`;
383
+ }
384
+ return JSON.stringify(value) ?? "null";
385
+ }
386
+ function outputPartCounterKey(eventId, kind) {
387
+ return JSON.stringify([eventId, kind]);
388
+ }
389
+ function withOutputPartIndex(event, kind, index) {
390
+ if (kind === "assistant" && (event.type === "assistant_delta" || event.type === "assistant_message")) {
391
+ return event.assistantIndex === index ? event : { ...event, assistantIndex: index };
392
+ }
393
+ if (kind === "thinking" && (event.type === "thinking_started" || event.type === "thinking_delta" || event.type === "thinking_finished")) {
394
+ return event.thinkingIndex === index ? event : { ...event, thinkingIndex: index };
395
+ }
396
+ if (kind === "usage" && event.type === "assistant_usage") {
397
+ return event.usageIndex === index ? event : { ...event, usageIndex: index };
398
+ }
399
+ if (kind === "compaction" && (event.type === "compaction_start" || event.type === "compaction_end")) {
400
+ return event.compactionIndex === index ? event : { ...event, compactionIndex: index };
401
+ }
402
+ return event;
403
+ }
246
404
  function segmentRemainsActive(event) {
247
405
  return event.type === "message_started"
248
406
  || event.type === "assistant_delta"
@@ -278,6 +436,8 @@ function toolInvocationCounterKey(eventId, toolCallId) {
278
436
  return JSON.stringify([eventId ?? "unscoped", toolCallId]);
279
437
  }
280
438
  function toolCallFingerprint(event) {
439
+ if (!event.argsComplete)
440
+ return undefined;
281
441
  return createHash("sha256")
282
442
  .update(JSON.stringify([event.toolName, event.argsComplete, event.args]))
283
443
  .digest("hex");
@@ -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 = outputEventFingerprint(event);
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
  }
@@ -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(`
@@ -196,7 +255,9 @@ export class PiboDataSessionStore {
196
255
  let ordinal;
197
256
  if (input.eventType === "tool_call"
198
257
  && latestOpen
199
- && (latestOpen.seen_call === 0 || latestOpen.call_fingerprint === input.callFingerprint)) {
258
+ && (latestOpen.seen_call === 0
259
+ || latestOpen.call_fingerprint === null
260
+ || latestOpen.call_fingerprint === input.callFingerprint)) {
200
261
  ordinal = latestOpen.invocation_ordinal;
201
262
  }
202
263
  else if (input.eventType !== "tool_call" && latestOpen) {
@@ -562,6 +623,28 @@ function runtimeBindingFromRow(row) {
562
623
  function isRuntimeBindingState(value) {
563
624
  return value === "unbound" || value === "bound" || value === "missing" || value === "error";
564
625
  }
626
+ function outputPartIndexAttribute(kind) {
627
+ switch (kind) {
628
+ case "assistant": return "assistantIndex";
629
+ case "thinking": return "thinkingIndex";
630
+ case "usage": return "usageIndex";
631
+ case "compaction": return "compactionIndex";
632
+ }
633
+ }
634
+ function outputPartEventTypes(kind) {
635
+ switch (kind) {
636
+ case "assistant": return ["assistant_delta", "assistant_message"];
637
+ case "thinking": return ["thinking_started", "thinking_delta", "thinking_finished"];
638
+ case "usage": return ["assistant_usage"];
639
+ case "compaction": return ["compaction_start", "compaction_end"];
640
+ }
641
+ }
642
+ function outputPartEventIsTerminal(eventType) {
643
+ return eventType === "assistant_message"
644
+ || eventType === "thinking_finished"
645
+ || eventType === "assistant_usage"
646
+ || eventType === "compaction_end";
647
+ }
565
648
  function parseJsonObject(json) {
566
649
  if (!json)
567
650
  return {};
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@pasko70/pibo",
3
- "version": "3.1.0",
3
+ "version": "3.1.2",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@pasko70/pibo",
9
- "version": "3.1.0",
9
+ "version": "3.1.2",
10
10
  "workspaces": [
11
11
  "packages/workflows"
12
12
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pasko70/pibo",
3
- "version": "3.1.0",
3
+ "version": "3.1.2",
4
4
  "type": "module",
5
5
  "workspaces": [
6
6
  "packages/workflows"