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/README.md +39 -28
- package/package.json +6 -4
- package/src/adapter.js +161 -0
- package/src/ag-ui.d.ts +6 -4
- package/src/ag-ui.js +17 -9
- package/src/anthropic.d.ts +6 -4
- package/src/anthropic.js +38 -24
- package/src/assistant-ui.d.ts +6 -4
- package/src/assistant-ui.js +29 -18
- package/src/gemini.d.ts +6 -4
- package/src/gemini.js +8 -5
- package/src/index.d.ts +137 -8
- package/src/index.js +89 -24
- package/src/internal/diagnostics.js +9 -0
- package/src/internal/errors.js +14 -0
- package/src/internal/integration.js +105 -15
- package/src/internal/snapshots.js +58 -0
- package/src/internal/wasm-binary.js +1 -1
- package/src/internal/wasm-runtime.js +31 -5
- package/src/langchain.d.ts +6 -4
- package/src/langchain.js +8 -4
- package/src/openai.d.ts +6 -4
- package/src/openai.js +22 -15
- package/src/read-structured.js +48 -0
- package/src/vercel-ai.d.ts +6 -4
- package/src/vercel-ai.js +34 -16
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
/**
|
|
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<
|
|
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
|
|
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
|
|
147
|
-
|
|
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
|
|
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
|
|
173
|
-
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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,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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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 = (
|
|
41
|
-
if (!
|
|
42
|
-
|
|
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
|
+
};
|