@shpitdev/convex-cliproxy-observability 0.3.5 → 0.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +2 -2
  3. package/dist/capture/content-health.d.ts +29 -0
  4. package/dist/capture/content-health.d.ts.map +1 -0
  5. package/dist/capture/content-health.js +29 -0
  6. package/dist/capture/content-health.js.map +1 -0
  7. package/dist/capture/index.d.ts +26 -2
  8. package/dist/capture/index.d.ts.map +1 -1
  9. package/dist/capture/index.js +99 -7
  10. package/dist/capture/index.js.map +1 -1
  11. package/dist/client.d.ts.map +1 -1
  12. package/dist/client.js +3 -0
  13. package/dist/client.js.map +1 -1
  14. package/dist/component/_generated/api.d.ts.map +1 -1
  15. package/dist/component/_generated/api.js.map +1 -1
  16. package/dist/component/_generated/component.d.ts.map +1 -1
  17. package/dist/component/ingest.d.ts +13 -0
  18. package/dist/component/ingest.d.ts.map +1 -1
  19. package/dist/component/ingest.js +4 -0
  20. package/dist/component/ingest.js.map +1 -1
  21. package/dist/model-call/index.d.ts +3 -1
  22. package/dist/model-call/index.d.ts.map +1 -1
  23. package/dist/model-call/index.js.map +1 -1
  24. package/dist/projection.d.ts +6 -3
  25. package/dist/projection.d.ts.map +1 -1
  26. package/dist/projection.js +55 -7
  27. package/dist/projection.js.map +1 -1
  28. package/dist/protocols/body-parts.d.ts +19 -0
  29. package/dist/protocols/body-parts.d.ts.map +1 -0
  30. package/dist/protocols/body-parts.js +56 -0
  31. package/dist/protocols/body-parts.js.map +1 -0
  32. package/dist/protocols/checkpoint.d.ts +16 -1
  33. package/dist/protocols/checkpoint.d.ts.map +1 -1
  34. package/dist/protocols/checkpoint.js +69 -24
  35. package/dist/protocols/checkpoint.js.map +1 -1
  36. package/dist/protocols/index.d.ts +3 -1
  37. package/dist/protocols/index.d.ts.map +1 -1
  38. package/dist/protocols/index.js +2 -0
  39. package/dist/protocols/index.js.map +1 -1
  40. package/dist/protocols/stock-block.d.ts +3 -0
  41. package/dist/protocols/stock-block.d.ts.map +1 -0
  42. package/dist/protocols/stock-block.js +17 -0
  43. package/dist/protocols/stock-block.js.map +1 -0
  44. package/package.json +1 -1
  45. package/src/capture/content-health.ts +32 -0
  46. package/src/capture/index.ts +139 -9
  47. package/src/client.ts +3 -0
  48. package/src/component/_generated/api.ts +5 -6
  49. package/src/component/_generated/component.ts +6 -6
  50. package/src/component/ingest.ts +3 -0
  51. package/src/model-call/index.ts +3 -1
  52. package/src/projection.ts +71 -16
  53. package/src/protocols/body-parts.ts +58 -0
  54. package/src/protocols/checkpoint.ts +76 -22
  55. package/src/protocols/index.ts +4 -1
  56. package/src/protocols/stock-block.ts +18 -0
@@ -1,3 +1,4 @@
1
+ import { stockHookPieces } from "./body-parts.js";
1
2
  import {
2
3
  providerIdentity,
3
4
  TOKEN_FIELDS,
@@ -6,7 +7,7 @@ import {
6
7
  type PrivateContentReference,
7
8
  } from "../model-call/index.js";
8
9
  import type { CaptureObservationV1 } from "../capture/index.js";
9
- import { decodeBody } from "../capture/index.js";
10
+ import { continuesBodyPart, decodeBody } from "../capture/index.js";
10
11
  import { assembleResponsesStream } from "./responses.js";
11
12
  import { SSEReader, type SSECheckpoint } from "./framing.js";
12
13
  import { count, list, nanoTime, object, parseObject } from "./values.js";
@@ -27,6 +28,10 @@ export type ProjectionCheckpoint = {
27
28
  streamed?: boolean;
28
29
  inputSeen?: boolean;
29
30
  chatFinished?: boolean;
31
+ /** First segment of an unfinished split body; projection reassembles from there. */
32
+ bodyPartSegment?: number;
33
+ /** Last part of an unfinished split body; the next observation must continue it. */
34
+ openBodyPart?: Pick<CaptureObservationV1, "kind" | "sequence" | "bodyPart">;
30
35
  };
31
36
  const usageFields = [
32
37
  "input_tokens",
@@ -185,19 +190,55 @@ function observeProtocol(
185
190
  }
186
191
  }
187
192
  }
193
+ /**
194
+ * Split bodies up to this size are reassembled for scalar projection. Larger ones
195
+ * stay complete in raw segments; projection does not allocate them in a 64 MiB
196
+ * Convex action.
197
+ */
198
+ export const MAX_PROJECTED_SPLIT_BYTES = 8 * 1024 * 1024;
199
+ /**
200
+ * Earlier parts of a split body only advance the sequence. The final part needs
201
+ * the exact reassembled body (`BodyPartAssembler`) as `assembled`, or `null` when
202
+ * the body exceeds `MAX_PROJECTED_SPLIT_BYTES` and its semantics are not projected.
203
+ */
188
204
  export function applyObservation(
189
205
  call: ModelCallV1,
190
206
  state: ProjectionCheckpoint,
191
207
  o: CaptureObservationV1,
208
+ assembled?: Uint8Array | null,
192
209
  ): void {
193
210
  if (o.sequence !== call.capture.projectedThroughSequence + 1)
194
211
  throw new Error("projection sequence gap");
212
+ if (o.captureIncomplete) call.capture.incomplete = true;
213
+ if (o.lostContentBytes !== undefined)
214
+ call.capture.lostContentBytes = Math.max(
215
+ call.capture.lostContentBytes ?? 0,
216
+ o.lostContentBytes,
217
+ );
195
218
  if (o.gap) {
196
219
  state.rawGap = true;
197
220
  if (!call.capture.gaps.includes(o.gap) && call.capture.gaps.length < 8)
198
221
  call.capture.gaps.push(o.gap.slice(0, 128));
199
222
  }
200
- const body = decodeBody(o);
223
+ const part = o.bodyPart;
224
+ // Segments validate only their own neighbors; continuity across them is checked here.
225
+ if (
226
+ (state.openBodyPart || (part && part.index > 0)) &&
227
+ !(state.openBodyPart && continuesBodyPart(state.openBodyPart, o))
228
+ )
229
+ throw new Error("body part interrupted");
230
+ const deferred = part !== undefined && part.index < part.count - 1;
231
+ state.openBodyPart = deferred
232
+ ? { kind: o.kind, sequence: o.sequence, bodyPart: part }
233
+ : undefined;
234
+ if (part && !deferred && assembled === undefined)
235
+ throw new Error("split body requires reassembly");
236
+ const unprojected = part !== undefined && !deferred && assembled === null;
237
+ if (unprojected && part.totalBytes <= MAX_PROJECTED_SPLIT_BYTES)
238
+ throw new Error("split body within projection bound");
239
+ // Like an over-limit SSE frame: content is retained, its semantics are not decoded.
240
+ if (unprojected && o.kind !== "request") state.invalid = state.frameLimit = true;
241
+ const body = deferred || unprojected ? new Uint8Array() : (assembled ?? decodeBody(o));
201
242
  const decoder = new TextDecoder("utf-8", { fatal: true });
202
243
  // A new after-auth event replaces the selection, including unknown fields.
203
244
  // Stream initialization may omit auth; an explicit selection replaces the pair.
@@ -220,14 +261,21 @@ export function applyObservation(
220
261
  call.correlationConflicts = o.correlationConflicts ?? [];
221
262
  call.traceId = o.correlation?.traceId;
222
263
  call.sourceTraceId = o.sourceTraceId;
223
- try {
224
- const input = parseObject(decoder.decode(body));
225
- state.streamed = input.stream === true;
226
- call.streamed = state.streamed;
227
- } catch {
228
- state.invalid = true;
264
+ if (!deferred && !unprojected) {
265
+ try {
266
+ const input = parseObject(decoder.decode(body));
267
+ state.streamed = input.stream === true;
268
+ call.streamed = state.streamed;
269
+ } catch {
270
+ state.invalid = true;
271
+ }
229
272
  }
230
273
  }
274
+ // An unparsed request (e.g. a split body over the projection bound) takes
275
+ // streaming from the protocol evidence that follows it.
276
+ if (call.streamed === undefined && (o.kind === "stream_init" || o.kind === "stream_chunk"))
277
+ call.streamed = true;
278
+ else if (call.streamed === undefined && o.kind === "response") call.streamed = false;
231
279
  // Derived read fields: reads fill them only for summaries projected before 0.3.0.
232
280
  Object.assign(call, providerIdentity(call), { costProvenance: call.cost.kind });
233
281
  if (o.kind === "response" || o.kind === "stream_chunk" || o.kind === "completion") {
@@ -244,9 +292,13 @@ export function applyObservation(
244
292
  }
245
293
  if ((o.kind === "stream_chunk" || o.kind === "completion") && body.length) {
246
294
  const reader = new SSEReader(state.sse);
247
- for (const frame of o.bodyFraming === "stock_hook_chunk"
248
- ? reader.feedStock(body, call.clientProtocol, o.observedAt)
249
- : reader.feed(body, o.observedAt)) {
295
+ const pieces = o.bodyFraming === "stock_hook_block" ? stockHookPieces(o, body) : [body];
296
+ const frames = pieces.flatMap((piece) =>
297
+ o.bodyFraming === "stock_hook_chunk" || o.bodyFraming === "stock_hook_block"
298
+ ? reader.feedStock(piece, call.clientProtocol, o.observedAt)
299
+ : reader.feed(piece, o.observedAt),
300
+ );
301
+ for (const frame of frames) {
250
302
  if (frame.data === "[DONE]") {
251
303
  if (call.clientProtocol === "chat_completions" && state.chatFinished)
252
304
  state.terminal = "completed";
@@ -287,17 +339,19 @@ export function applyObservation(
287
339
  call.latencyMs = Number(BigInt(call.endTimeUnixNano) - BigInt(start)) / 1e6;
288
340
  call.capture.terminalSequence = o.sequence;
289
341
  call.state =
290
- o.completionOutcome === "canceled"
291
- ? "aborted"
292
- : o.completionOutcome === "failed"
293
- ? "failed"
294
- : o.completionOutcome === "rejected"
295
- ? "rejected"
296
- : state.terminal === "completed"
297
- ? "succeeded"
298
- : state.terminal === "failed" || state.terminal === "incomplete"
299
- ? "failed"
300
- : "unknown";
342
+ o.completionOutcome === undefined && o.captureIncomplete
343
+ ? "unknown"
344
+ : o.completionOutcome === "canceled"
345
+ ? "aborted"
346
+ : o.completionOutcome === "failed"
347
+ ? "failed"
348
+ : o.completionOutcome === "rejected"
349
+ ? "rejected"
350
+ : state.terminal === "completed"
351
+ ? "succeeded"
352
+ : state.terminal === "failed" || state.terminal === "incomplete"
353
+ ? "failed"
354
+ : "unknown";
301
355
  }
302
356
  call.protocolTerminal = state.terminal;
303
357
  const final =
@@ -25,7 +25,7 @@ export function projectCapturedPayloads(args: {
25
25
  response?: Uint8Array;
26
26
  chunks?: readonly Uint8Array[];
27
27
  complete?: boolean;
28
- /** Set only for capturePolicy hook-body-v1, never historical canonical SSE. */
28
+ /** Set only for expanded raw stock hooks, never historical canonical SSE. */
29
29
  stockHookChunks?: boolean;
30
30
  }): CliproxyProjection {
31
31
  const projection: CliproxyProjection = {
@@ -142,3 +142,6 @@ export function projectCapturedPayloads(args: {
142
142
  }
143
143
  return projection;
144
144
  }
145
+
146
+ export { expandStockHookBlock } from "./stock-block.js";
147
+ export { BodyPartAssembler, stockHookPieces } from "./body-parts.js";
@@ -0,0 +1,18 @@
1
+ /** Recover stock parser boundaries without altering the captured bytes. */
2
+ export function expandStockHookBlock(
3
+ body: Uint8Array,
4
+ lengths: readonly number[],
5
+ ): readonly Uint8Array[] {
6
+ if (!Array.isArray(lengths) || lengths.length === 0 || lengths.length > 1024)
7
+ throw new Error("invalid stock hook block lengths");
8
+ const pieces: Uint8Array[] = [];
9
+ let offset = 0;
10
+ for (const length of lengths) {
11
+ if (!Number.isSafeInteger(length) || length < 0 || length > body.byteLength - offset)
12
+ throw new Error("invalid stock hook block length");
13
+ pieces.push(body.subarray(offset, offset + length));
14
+ offset += length;
15
+ }
16
+ if (offset !== body.byteLength) throw new Error("stock hook block byte mismatch");
17
+ return pieces;
18
+ }