streamfold 0.1.2 → 0.1.5

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/src/index.d.ts CHANGED
@@ -14,6 +14,8 @@ export interface StructuredStreamOptions {
14
14
  readonly maxBytes?: number;
15
15
  /** Maximum nested object/array depth. Defaults to 128. */
16
16
  readonly maxDepth?: number;
17
+ /** Live views by default; immutable snapshots are frozen with structural sharing. */
18
+ readonly snapshots?: "live" | "immutable";
17
19
  }
18
20
 
19
21
  export interface StructuredStreamPoolOptions extends StructuredStreamOptions {
@@ -44,7 +46,7 @@ export interface StreamState {
44
46
  readonly complete: boolean;
45
47
  readonly inString: boolean;
46
48
  readonly changes: readonly StructuredStreamPatch[];
47
- /** A live view updated in place; use `changes` for reactive state updates. */
49
+ /** Live by default; a stable, deeply frozen value with `snapshots: "immutable"`. */
48
50
  readonly partialValue: JsonValue | undefined;
49
51
  }
50
52
 
@@ -68,12 +70,131 @@ export interface EventStructuredStream<Event, Id = string> {
68
70
  finish(): readonly CompletedStructuredStream<Id>[];
69
71
  }
70
72
 
73
+ export type StructuredStreamLifecycleUpdate<Id = string> =
74
+ | (ActiveStructuredStream<Id> & { readonly type: "start" | "update" })
75
+ | (CompletedStructuredStream<Id> & { readonly type: "complete" });
76
+
77
+ export interface BatchEventStructuredStream<Event, Id = string>
78
+ extends EventStructuredStream<Event, Id> {
79
+ /** Consume once and return every call update, in event order. */
80
+ pushAll(event: Event): readonly StructuredStreamLifecycleUpdate<Id>[];
81
+ }
82
+
83
+ export type StructuredStreamErrorCode =
84
+ | "UNEXPECTED_TOKEN"
85
+ | "MISMATCHED_CLOSING"
86
+ | "TRAILING_DATA"
87
+ | "EMPTY_INPUT"
88
+ | "INCOMPLETE_JSON"
89
+ | "INVALID_JSON"
90
+ | "PARSER_ERROR"
91
+ | "MAX_BYTES_EXCEEDED"
92
+ | "MAX_DEPTH_EXCEEDED"
93
+ | "MAX_ACTIVE_STREAMS_EXCEEDED"
94
+ | "DUPLICATE_STREAM"
95
+ | "UNKNOWN_STREAM"
96
+ | "STREAM_DISPOSED"
97
+ | "INVALID_CHUNK"
98
+ | "INVALID_OPTIONS"
99
+ | "INTEGRATION_ERROR";
100
+
101
+ /** Metadata on the original SyntaxError, RangeError, TypeError, or Error. */
102
+ export interface StructuredStreamError extends Error {
103
+ readonly streamfold: true;
104
+ readonly code: StructuredStreamErrorCode;
105
+ /** Zero-based UTF-8 byte offset, when supplied by the parser. */
106
+ readonly byteOffset?: number;
107
+ readonly id?: unknown;
108
+ readonly operation?: "start" | "push" | "finish" | "getFieldState";
109
+ readonly adapter?: string;
110
+ readonly eventType?: string;
111
+ }
112
+
113
+ export function isStructuredStreamError(
114
+ error: unknown,
115
+ ): error is StructuredStreamError;
116
+
117
+ export interface StructuredStreamDiagnostic {
118
+ readonly code: "NO_TOOL_EVENTS" | "UNMATCHED_TOOL_EVENT" | "STREAM_ERROR";
119
+ readonly message: string;
120
+ readonly adapter: string;
121
+ readonly eventType?: string;
122
+ readonly id?: unknown;
123
+ readonly error?: Error;
124
+ }
125
+
126
+ export interface StructuredStreamIntegrationOptions {
127
+ /** Opt-in diagnostics. No logging by default. Callback exceptions are ignored. */
128
+ readonly onDiagnostic?: (diagnostic: StructuredStreamDiagnostic) => void;
129
+ }
130
+
71
131
  export interface StructuredStreamIntegration<Event, Id = string> {
132
+ (pool?: StructuredStreamPool<Id>): EventStructuredStream<Event, Id>;
133
+ }
134
+
135
+ export interface BatchStructuredStreamIntegration<Event, Id = string> {
72
136
  (
73
137
  pool?: StructuredStreamPool<Id>,
74
- ): EventStructuredStream<Event, Id>;
138
+ options?: StructuredStreamIntegrationOptions,
139
+ ): BatchEventStructuredStream<Event, Id>;
75
140
  }
76
141
 
142
+ export type StructuredStreamOperation<Id = string> =
143
+ | { readonly type: "start"; readonly id: Id }
144
+ | { readonly type: "delta"; readonly id: Id; readonly text: string }
145
+ | { readonly type: "end"; readonly id: Id }
146
+ | { readonly type: "abort"; readonly id: Id };
147
+
148
+ /** Translate one decoded event into zero or more ordered operations. */
149
+ export type StructuredStreamMapper<Event, Id = string> = (
150
+ event: Event,
151
+ ) => readonly StructuredStreamOperation<Id>[];
152
+
153
+ export interface BatchStructuredStream<Event, Id = string> {
154
+ pushAll(event: Event): readonly StructuredStreamLifecycleUpdate<Id>[];
155
+ /** Finalize remaining calls only. Repeated successful calls return []. */
156
+ finish(): readonly (CompletedStructuredStream<Id> & {
157
+ readonly type: "complete";
158
+ })[];
159
+ /** Release active parsers without finalizing their JSON. Idempotent. */
160
+ dispose(): void;
161
+ }
162
+
163
+ export interface StructuredStreamAdapterOptions
164
+ extends StructuredStreamPoolOptions,
165
+ StructuredStreamIntegrationOptions {}
166
+
167
+ /** Calling the adapter creates an independent parser pool. */
168
+ export interface StructuredStreamAdapter<Event, Id = string> {
169
+ (options?: StructuredStreamAdapterOptions): BatchStructuredStream<Event, Id>;
170
+ }
171
+
172
+ export function defineAdapter<Event, Id = string>(
173
+ mapEvent: StructuredStreamMapper<Event, Id>,
174
+ ): StructuredStreamAdapter<Event, Id>;
175
+
176
+ export interface ReadStructuredOptions<Event, Id = string>
177
+ extends StructuredStreamIntegrationOptions {
178
+ readonly adapter: StructuredStreamAdapter<Event, Id>;
179
+ readonly integration?: never;
180
+ readonly limits?: StructuredStreamPoolOptions;
181
+ }
182
+
183
+ export interface ReadStructuredIntegrationOptions<Event, Id = string>
184
+ extends StructuredStreamIntegrationOptions {
185
+ readonly integration: BatchStructuredStreamIntegration<Event, Id>;
186
+ readonly adapter?: never;
187
+ readonly limits?: StructuredStreamPoolOptions;
188
+ }
189
+
190
+ /** Consume decoded events, finalizing at EOF and disposing on every exit. */
191
+ export function readStructured<Event, Id = string>(
192
+ events: AsyncIterable<Event> | Iterable<Event>,
193
+ options:
194
+ | ReadStructuredOptions<Event, Id>
195
+ | ReadStructuredIntegrationOptions<Event, Id>,
196
+ ): AsyncGenerator<StructuredStreamLifecycleUpdate<Id>, void, unknown>;
197
+
77
198
  export class IncrementalJsonScanner {
78
199
  constructor(options?: StructuredStreamOptions);
79
200
  push(chunk: string): StreamState;
@@ -82,7 +203,7 @@ export class IncrementalJsonScanner {
82
203
  dispose(): void;
83
204
  readonly backend: "rust-wasm";
84
205
  readonly state: StreamState;
85
- /** A live view updated in place; use `StreamState.changes` for reactive updates. */
206
+ /** Live by default; a stable, deeply frozen value with `snapshots: "immutable"`. */
86
207
  readonly value: JsonValue | undefined;
87
208
  }
88
209
 
@@ -91,10 +212,7 @@ export class StructuredStreamPool<Id = string> {
91
212
  start(id: Id, initialChunk?: string): ActiveStructuredStream<Id>;
92
213
  push(id: Id, delta: string): ActiveStructuredStream<Id>;
93
214
  finish(id: Id): CompletedStructuredStream<Id>;
94
- getFieldState(
95
- id: Id,
96
- path: StructuredStreamPath,
97
- ): StructuredStreamFieldState;
215
+ getFieldState(id: Id, path: StructuredStreamPath): StructuredStreamFieldState;
98
216
  abort(id: Id): boolean;
99
217
  has(id: Id): boolean;
100
218
  readonly activeIds: readonly Id[];
@@ -105,6 +223,12 @@ export function createStructuredStream(): IncrementalJsonScanner;
105
223
  export function createStructuredStream(
106
224
  options: StructuredStreamOptions,
107
225
  ): IncrementalJsonScanner;
226
+ export function createStructuredStream<Event, Id = string>(
227
+ adapter: StructuredStreamAdapter<Event, Id>,
228
+ ): BatchStructuredStream<Event, Id>;
229
+ export function createStructuredStream<Event, Id = string>(
230
+ integration: BatchStructuredStreamIntegration<Event, Id>,
231
+ ): BatchEventStructuredStream<Event, Id>;
108
232
  export function createStructuredStream<Event, Id = string>(
109
233
  integration: StructuredStreamIntegration<Event, Id>,
110
234
  ): EventStructuredStream<Event, Id>;
@@ -114,5 +238,10 @@ export function createStructuredStreamPool<Id = string>(
114
238
 
115
239
  export const STREAMFOLD_ENGINE: "rust-wasm";
116
240
  export const DEFAULT_STREAM_LIMITS: Readonly<
117
- Required<StructuredStreamPoolOptions>
241
+ Required<
242
+ Pick<
243
+ StructuredStreamPoolOptions,
244
+ "maxBytes" | "maxDepth" | "maxActiveStreams"
245
+ >
246
+ >
118
247
  >;
package/src/index.js CHANGED
@@ -5,6 +5,12 @@ import {
5
5
  pushWasmParser,
6
6
  readWasmParser,
7
7
  } from "./internal/wasm-runtime.js";
8
+ import { applyImmutableChanges, freezeJson } from "./internal/snapshots.js";
9
+ import { annotateError, streamError } from "./internal/errors.js";
10
+
11
+ export { isStructuredStreamError } from "./internal/errors.js";
12
+ export { defineAdapter } from "./adapter.js";
13
+ export { readStructured } from "./read-structured.js";
8
14
 
9
15
  export const STREAMFOLD_ENGINE = "rust-wasm";
10
16
  export const DEFAULT_STREAM_LIMITS = Object.freeze({
@@ -23,9 +29,11 @@ export class IncrementalJsonScanner {
23
29
  #value;
24
30
  #completion = createCompletionNode();
25
31
  #error;
32
+ #immutable;
26
33
 
27
34
  constructor(options = {}) {
28
35
  const limits = normalizeStreamLimits(options);
36
+ this.#immutable = limits.snapshots === "immutable";
29
37
  this.#parser = createWasmParser(limits);
30
38
  parserFinalizer?.register(this, this.#parser, this);
31
39
  }
@@ -40,7 +48,7 @@ export class IncrementalJsonScanner {
40
48
  partialValue: this.#value,
41
49
  };
42
50
  } catch (error) {
43
- throw this.#fail(error);
51
+ throw this.#fail(error, "push");
44
52
  }
45
53
  }
46
54
 
@@ -54,7 +62,7 @@ export class IncrementalJsonScanner {
54
62
  partialValue: this.#value,
55
63
  };
56
64
  } catch (error) {
57
- throw this.#fail(error);
65
+ throw this.#fail(error, "finish");
58
66
  }
59
67
  }
60
68
 
@@ -71,9 +79,7 @@ export class IncrementalJsonScanner {
71
79
  }
72
80
 
73
81
  getFieldState(path) {
74
- return isFieldComplete(this.#completion, path)
75
- ? "complete"
76
- : "partial";
82
+ return isFieldComplete(this.#completion, path) ? "complete" : "partial";
77
83
  }
78
84
 
79
85
  get backend() {
@@ -92,12 +98,15 @@ export class IncrementalJsonScanner {
92
98
  #getParser() {
93
99
  if (this.#error !== undefined) throw this.#error;
94
100
  if (this.#parser === undefined) {
95
- throw new Error("Structured stream has been disposed");
101
+ throw streamError(
102
+ new Error("Structured stream has been disposed"),
103
+ "STREAM_DISPOSED",
104
+ );
96
105
  }
97
106
  return this.#parser;
98
107
  }
99
108
 
100
- #fail(error) {
109
+ #fail(error, operation) {
101
110
  const failure =
102
111
  error instanceof Error ? error : new Error("Structured stream failed");
103
112
  if (this.#parser !== undefined) {
@@ -106,22 +115,26 @@ export class IncrementalJsonScanner {
106
115
  this.#parser = undefined;
107
116
  }
108
117
  this.#error = failure;
118
+ if (failure.operation === undefined) annotateError(failure, { operation });
109
119
  return failure;
110
120
  }
111
121
 
112
122
  #apply(changes) {
123
+ if (this.#immutable)
124
+ this.#value = applyImmutableChanges(this.#value, changes);
113
125
  for (const change of changes) {
114
126
  if (change.op === "complete") {
115
127
  markFieldComplete(this.#completion, change.path);
116
128
  } else if (change.op === "set") {
117
129
  invalidateField(this.#completion, change.path);
130
+ if (this.#immutable) continue;
118
131
  const value = Array.isArray(change.value)
119
132
  ? []
120
133
  : change.value !== null && typeof change.value === "object"
121
134
  ? {}
122
135
  : change.value;
123
136
  this.#value = setAtPath(this.#value, change.path, value);
124
- } else {
137
+ } else if (!this.#immutable) {
125
138
  const current = getAtPath(this.#value, change.path);
126
139
  this.#value = setAtPath(
127
140
  this.#value,
@@ -143,8 +156,11 @@ export const createStructuredStream = (integrationOrOptions) => {
143
156
  return new IncrementalJsonScanner(integrationOrOptions);
144
157
  }
145
158
  if (typeof integrationOrOptions !== "function") {
146
- throw new TypeError(
147
- "A Streamfold argument must be an integration or options object",
159
+ throw streamError(
160
+ new TypeError(
161
+ "A Streamfold argument must be an integration or options object",
162
+ ),
163
+ "INVALID_OPTIONS",
148
164
  );
149
165
  }
150
166
  return integrationOrOptions();
@@ -161,16 +177,32 @@ export class StructuredStreamPool {
161
177
  this.#streamOptions = {
162
178
  maxBytes: limits.maxBytes,
163
179
  maxDepth: limits.maxDepth,
180
+ snapshots: limits.snapshots,
164
181
  };
165
182
  }
166
183
 
167
184
  start(id, initialChunk = "") {
185
+ if (typeof initialChunk !== "string") {
186
+ throw streamError(
187
+ new TypeError("A structured stream chunk must be a string"),
188
+ "INVALID_CHUNK",
189
+ { id, operation: "start" },
190
+ );
191
+ }
168
192
  if (this.#streams.has(id)) {
169
- throw new Error(`Structured stream already exists: ${String(id)}`);
193
+ throw streamError(
194
+ new Error(`Structured stream already exists: ${String(id)}`),
195
+ "DUPLICATE_STREAM",
196
+ { id, operation: "start" },
197
+ );
170
198
  }
171
199
  if (this.#streams.size >= this.#maxActiveStreams) {
172
- throw new RangeError(
173
- `Structured stream pool exceeds maxActiveStreams (${this.#maxActiveStreams})`,
200
+ throw streamError(
201
+ new RangeError(
202
+ `Structured stream pool exceeds maxActiveStreams (${this.#maxActiveStreams})`,
203
+ ),
204
+ "MAX_ACTIVE_STREAMS_EXCEEDED",
205
+ { id, operation: "start" },
174
206
  );
175
207
  }
176
208
 
@@ -179,35 +211,54 @@ export class StructuredStreamPool {
179
211
  chunks: [],
180
212
  };
181
213
  this.#streams.set(id, entry);
182
- if (initialChunk.length > 0) return this.push(id, initialChunk);
214
+ if (initialChunk.length > 0) {
215
+ try {
216
+ return this.push(id, initialChunk);
217
+ } catch (error) {
218
+ throw annotateError(error, { operation: "start" });
219
+ }
220
+ }
183
221
  return { id, ...entry.scanner.state };
184
222
  }
185
223
 
186
224
  push(id, delta) {
187
225
  const entry = this.#streams.get(id);
188
226
  if (entry === undefined) {
189
- throw new Error(`Unknown structured stream: ${String(id)}`);
227
+ throw streamError(
228
+ new Error(
229
+ `Unknown structured stream: ${String(id)} (push requires an active call)`,
230
+ ),
231
+ "UNKNOWN_STREAM",
232
+ { id, operation: "push" },
233
+ );
190
234
  }
191
235
  entry.chunks.push(delta);
192
236
  try {
193
237
  return { id, ...entry.scanner.push(delta) };
194
238
  } catch (error) {
195
239
  this.abort(id);
196
- throw error;
240
+ throw annotateError(error, { id, operation: "push" });
197
241
  }
198
242
  }
199
243
 
200
244
  finish(id) {
201
245
  const entry = this.#streams.get(id);
202
246
  if (entry === undefined) {
203
- throw new Error(`Unknown structured stream: ${String(id)}`);
247
+ throw streamError(
248
+ new Error(`Unknown structured stream: ${String(id)}`),
249
+ "UNKNOWN_STREAM",
250
+ { id, operation: "finish" },
251
+ );
204
252
  }
205
253
 
206
254
  try {
207
255
  const state = entry.scanner.finish();
208
256
  const text = entry.chunks.join("");
209
257
  const value = JSON.parse(text);
258
+ if (this.#streamOptions.snapshots === "immutable") freezeJson(value);
210
259
  return { id, text, value, ...state };
260
+ } catch (error) {
261
+ throw annotateError(error, { id, operation: "finish" });
211
262
  } finally {
212
263
  this.#streams.delete(id);
213
264
  entry.scanner.dispose();
@@ -217,7 +268,11 @@ export class StructuredStreamPool {
217
268
  getFieldState(id, path) {
218
269
  const entry = this.#streams.get(id);
219
270
  if (entry === undefined) {
220
- throw new Error(`Unknown structured stream: ${String(id)}`);
271
+ throw streamError(
272
+ new Error(`Unknown structured stream: ${String(id)}`),
273
+ "UNKNOWN_STREAM",
274
+ { id, operation: "getFieldState" },
275
+ );
221
276
  }
222
277
  return entry.scanner.getFieldState(path);
223
278
  }
@@ -312,17 +367,17 @@ const isFieldComplete = (root, path) => {
312
367
 
313
368
  const normalizeLimit = (value, fallback, name) => {
314
369
  const limit = value ?? fallback;
315
- if (
316
- !Number.isSafeInteger(limit) ||
317
- limit <= 0 ||
318
- limit > 0xffff_ffff
319
- ) {
320
- throw new RangeError(`${name} must be an integer between 1 and 4294967295`);
370
+ if (!Number.isSafeInteger(limit) || limit <= 0 || limit > 0xffff_ffff) {
371
+ throw streamError(
372
+ new RangeError(`${name} must be an integer between 1 and 4294967295`),
373
+ "INVALID_OPTIONS",
374
+ );
321
375
  }
322
376
  return limit;
323
377
  };
324
378
 
325
379
  const normalizeStreamLimits = (options) => ({
380
+ snapshots: normalizeSnapshots(options.snapshots),
326
381
  maxBytes: normalizeLimit(
327
382
  options.maxBytes,
328
383
  DEFAULT_STREAM_LIMITS.maxBytes,
@@ -335,6 +390,16 @@ const normalizeStreamLimits = (options) => ({
335
390
  ),
336
391
  });
337
392
 
393
+ const normalizeSnapshots = (snapshots = "live") => {
394
+ if (snapshots !== "live" && snapshots !== "immutable") {
395
+ throw streamError(
396
+ new TypeError('snapshots must be "live" or "immutable"'),
397
+ "INVALID_OPTIONS",
398
+ );
399
+ }
400
+ return snapshots;
401
+ };
402
+
338
403
  const normalizePoolLimits = (options) => ({
339
404
  ...normalizeStreamLimits(options),
340
405
  maxActiveStreams: normalizeLimit(
@@ -0,0 +1,9 @@
1
+ export const emitDiagnostic = (options, diagnostic) => {
2
+ try {
3
+ if (typeof options?.onDiagnostic === "function") {
4
+ options.onDiagnostic(diagnostic);
5
+ }
6
+ } catch {
7
+ // Observability must not interrupt parsing or resource cleanup.
8
+ }
9
+ };
@@ -0,0 +1,14 @@
1
+ export const annotateError = (error, details) => {
2
+ try {
3
+ Object.assign(error, details);
4
+ } catch {
5
+ // Preserve an upstream error even when it cannot accept metadata.
6
+ }
7
+ return error;
8
+ };
9
+
10
+ export const isStructuredStreamError = (error) =>
11
+ error instanceof Error && error.streamfold === true;
12
+
13
+ export const streamError = (error, code, details = {}) =>
14
+ annotateError(error, { streamfold: true, code, ...details });
@@ -1,34 +1,123 @@
1
- export const createIntegration = (pool, accept, finishActive) => {
1
+ import {
2
+ annotateError,
3
+ isStructuredStreamError,
4
+ streamError,
5
+ } from "./errors.js";
6
+ import { emitDiagnostic } from "./diagnostics.js";
7
+
8
+ export const createIntegration = (
9
+ pool,
10
+ accept,
11
+ finishActive,
12
+ adapter,
13
+ options = {},
14
+ ) => {
2
15
  const completed = [];
3
16
  let failure;
17
+ let batch;
18
+ let eventType;
19
+ let receivedEvents = false;
20
+ let matched = false;
21
+ let reportedNoMatch = false;
22
+
23
+ const diagnose = (diagnostic) =>
24
+ emitDiagnostic(options, { adapter, eventType, ...diagnostic });
25
+ const unmatched = (message, id) =>
26
+ diagnose({
27
+ code: "UNMATCHED_TOOL_EVENT",
28
+ message,
29
+ id,
30
+ });
31
+ const record = (type, result) => {
32
+ matched = true;
33
+ if (batch !== undefined) batch.push({ type, ...result });
34
+ return result;
35
+ };
36
+ const missingId = (id) => {
37
+ if (id !== undefined && id !== null) return false;
38
+ unmatched("Tool event is missing its call id");
39
+ return true;
40
+ };
4
41
 
5
42
  const complete = (id) => {
6
- if (!pool.has(id)) return undefined;
43
+ if (missingId(id)) return undefined;
44
+ if (!pool.has(id)) {
45
+ if (
46
+ options.onDiagnostic &&
47
+ !completed.some((result) => result.id === id)
48
+ ) {
49
+ unmatched("Tool completion has no matching active call", id);
50
+ }
51
+ return undefined;
52
+ }
7
53
  const result = pool.finish(id);
8
54
  completed.push(result);
9
- return result;
55
+ return record("complete", result);
56
+ };
57
+ const operations = {
58
+ start: (id, initialChunk) =>
59
+ missingId(id) ? undefined : record("start", pool.start(id, initialChunk)),
60
+ push: (id, delta) =>
61
+ missingId(id) ? undefined : record("update", pool.push(id, delta)),
62
+ has: (id) => pool.has(id),
63
+ complete,
64
+ unmatched,
10
65
  };
11
66
 
12
67
  const fail = (error) => {
13
- failure = error instanceof Error ? error : new Error("Integration failed");
68
+ const original =
69
+ error instanceof Error ? error : new Error("Integration failed");
70
+ failure = isStructuredStreamError(original)
71
+ ? annotateError(original, { adapter, eventType })
72
+ : streamError(original, "INTEGRATION_ERROR", { adapter, eventType });
14
73
  for (const id of pool.activeIds) pool.abort(id);
74
+ diagnose({
75
+ code: "STREAM_ERROR",
76
+ message: failure.message,
77
+ error: failure,
78
+ id: failure.id,
79
+ });
15
80
  return failure;
16
81
  };
17
82
 
83
+ const push = (event, collect) => {
84
+ if (failure !== undefined) throw failure;
85
+ receivedEvents = true;
86
+ batch = collect ? [] : undefined;
87
+ try {
88
+ const type = event?.type ?? event?.event_type;
89
+ eventType =
90
+ typeof type === "string"
91
+ ? type
92
+ : event?.tool_call_chunks
93
+ ? "tool_call_chunks"
94
+ : undefined;
95
+ const update = accept(event, operations);
96
+ return collect ? batch : update;
97
+ } catch (error) {
98
+ throw fail(error);
99
+ } finally {
100
+ batch = undefined;
101
+ eventType = undefined;
102
+ }
103
+ };
104
+
18
105
  return {
19
- push(event) {
20
- if (failure !== undefined) throw failure;
21
- try {
22
- return accept(event, complete);
23
- } catch (error) {
24
- throw fail(error);
25
- }
26
- },
106
+ push: (event) => push(event, false),
107
+ pushAll: (event) => push(event, true),
27
108
  finish() {
28
109
  if (failure !== undefined) throw failure;
29
110
  try {
30
111
  finishActive?.(complete);
31
112
  for (const id of pool.activeIds) complete(id);
113
+ if (receivedEvents && !matched && !reportedNoMatch) {
114
+ reportedNoMatch = true;
115
+ diagnose({
116
+ code: "NO_TOOL_EVENTS",
117
+ message:
118
+ "No tool argument events matched this adapter; text-only streams are valid, otherwise check the event format",
119
+ });
120
+ }
32
121
  return [...completed];
33
122
  } catch (error) {
34
123
  throw fail(error);
@@ -37,7 +126,8 @@ export const createIntegration = (pool, accept, finishActive) => {
37
126
  };
38
127
  };
39
128
 
40
- export const append = (pool, id, delta) => {
41
- if (!pool.has(id)) pool.start(id);
42
- return pool.push(id, delta);
129
+ export const append = (operations, id, delta) => {
130
+ if (!operations.has(id) && operations.start(id) === undefined)
131
+ return undefined;
132
+ return operations.push(id, delta);
43
133
  };
@@ -0,0 +1,58 @@
1
+ const setOwn = (target, key, value) => {
2
+ Object.defineProperty(target, key, {
3
+ value,
4
+ enumerable: true,
5
+ configurable: true,
6
+ writable: true,
7
+ });
8
+ };
9
+
10
+ export const applyImmutableChanges = (previous, changes) => {
11
+ const owned = new Set();
12
+ const copy = (value) => {
13
+ if (owned.has(value)) return value;
14
+ const result = Array.isArray(value) ? value.slice() : { ...value };
15
+ owned.add(result);
16
+ return result;
17
+ };
18
+
19
+ let next = previous;
20
+ for (const change of changes) {
21
+ if (change.op === "complete") continue;
22
+ const replacement = (current) => {
23
+ if (change.op === "append") return current + change.value;
24
+ return change.value !== null && typeof change.value === "object"
25
+ ? copy(change.value)
26
+ : change.value;
27
+ };
28
+ if (change.path.length === 0) {
29
+ next = replacement(next);
30
+ continue;
31
+ }
32
+
33
+ // Copy each changed container once per batch, even for many array appends.
34
+ next = copy(next);
35
+ let target = next;
36
+ for (const key of change.path.slice(0, -1)) {
37
+ const child = copy(Object.hasOwn(target, key) ? target[key] : undefined);
38
+ setOwn(target, key, child);
39
+ target = child;
40
+ }
41
+ const key = change.path.at(-1);
42
+ setOwn(
43
+ target,
44
+ key,
45
+ replacement(Object.hasOwn(target, key) ? target[key] : undefined),
46
+ );
47
+ }
48
+ for (const value of owned) Object.freeze(value);
49
+ return next;
50
+ };
51
+
52
+ export const freezeJson = (value) => {
53
+ if (value !== null && typeof value === "object" && !Object.isFrozen(value)) {
54
+ for (const child of Object.values(value)) freezeJson(child);
55
+ Object.freeze(value);
56
+ }
57
+ return value;
58
+ };