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 CHANGED
@@ -2,14 +2,18 @@
2
2
 
3
3
  Incremental structured state for streamed AI tool calls.
4
4
 
5
- Streamfold retains JSON parser and value state across deltas. Its Rust/WebAssembly
6
- engine emits compact patches that update a live partial JavaScript value without
7
- reparsing the complete accumulated input.
5
+ Streamfold retains JSON parser and value state across deltas. Its embedded
6
+ Rust/WebAssembly engine emits compact patches and works in Node.js 20+ and
7
+ modern browsers without native build tools or provider SDK dependencies.
8
+
9
+ ## Install
8
10
 
9
11
  ```bash
10
- pnpm add streamfold
12
+ npm install streamfold
11
13
  ```
12
14
 
15
+ ## Quick start
16
+
13
17
  ```ts
14
18
  import { createStructuredStream } from "streamfold";
15
19
 
@@ -17,39 +21,46 @@ const stream = createStructuredStream();
17
21
  const update = stream.push('{"city":"San');
18
22
 
19
23
  console.log(update.partialValue); // { city: "San" }
20
- console.log(update.changes); // compact set/append/complete patches
21
24
 
22
25
  stream.push(' Francisco"}');
23
26
  console.log(stream.getFieldState(["city"])); // "complete"
27
+
28
+ stream.finish();
29
+ stream.dispose();
24
30
  ```
25
31
 
26
- Resource limits are configurable and enabled by default:
32
+ Values are live by default. Use `createStructuredStream({ snapshots: "immutable" })`
33
+ for frozen snapshots that preserve earlier values and share unchanged branches.
34
+ Pools and custom adapter factories accept the same option; managed readers
35
+ forward it through `limits`.
27
36
 
28
- ```ts
29
- const stream = createStructuredStream({
30
- maxBytes: 2 * 1024 * 1024,
31
- maxDepth: 64,
32
- });
33
- ```
37
+ Use `streamfold/assistant-ui`, `streamfold/vercel-ai`, `streamfold/openai`,
38
+ `streamfold/anthropic`, `streamfold/gemini`, `streamfold/langchain`, or
39
+ `streamfold/ag-ui` for decoded SDK events. Integrations use structural event
40
+ types and do not load provider SDKs.
34
41
 
35
- Use an isolated event integration when consuming an SDK stream:
42
+ For custom protocols, import `defineAdapter` from `streamfold`. Translate each
43
+ event into an array of `start`, `delta`, `end`, or `abort` operations. The returned
44
+ factory creates an independent session with `pushAll(event)`, `finish()`, and
45
+ `dispose()`. See the API reference for a complete switch-based example.
36
46
 
37
- ```ts
38
- import { createStructuredStream } from "streamfold/assistant-ui";
47
+ For automatic lifecycle management, use
48
+ `readStructured(events, { adapter, limits })` in a `for await` loop. It wraps the
49
+ same batch adapter, finalizes remaining calls at the end of the source, and
50
+ disposes on completion, failure, or early exit. `limits` is optional.
51
+ For a built-in SDK factory, use `{ integration: assistantUI, limits }` instead.
52
+ Both paths yield the same lifecycle updates without replaying completions.
39
53
 
40
- const toolCalls = createStructuredStream();
54
+ Every built-in adapter supports `pushAll(event)`, returning all ordered
55
+ `start`, `update`, and `complete` updates, including multiple calls in one event.
56
+ Existing `push(event)` remains available. Call `finish()` at the stream boundary.
41
57
 
42
- for await (const event of assistantStream) {
43
- const update = toolCalls.push(event);
44
- if (update) renderToolInput(update.id, update.partialValue);
45
- }
46
- ```
58
+ Pass `onDiagnostic` in a custom factory's options, as an SDK factory's second
59
+ argument, or beside the factory in `readStructured`. Errors preserve their
60
+ native categories and include structured codes and call context where known.
47
61
 
48
- Available subpaths are `assistant-ui`, `vercel-ai`, `openai`, `anthropic`,
49
- `gemini`, `langchain`, and `ag-ui`. They consume structural event shapes and do
50
- not install or load provider SDKs.
62
+ See the repository for the [API reference](https://github.com/assistant-ui/streamfold/blob/main/API.md),
63
+ [integration guide](https://github.com/assistant-ui/streamfold/blob/main/MIGRATION.md),
64
+ and [benchmarks](https://github.com/assistant-ui/streamfold#performance).
51
65
 
52
- See the [repository README](https://github.com/assistant-ui/streamfold#readme)
53
- for benchmarks, methodology, and development instructions. The
54
- [migration guide](https://github.com/assistant-ui/streamfold/blob/main/MIGRATION.md)
55
- covers direct streams and every bundled integration.
66
+ MIT © Streamfold contributors
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "streamfold",
3
- "version": "0.1.2",
3
+ "version": "0.1.5",
4
4
  "description": "Protocol-neutral incremental state for structured AI streams",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -67,9 +67,11 @@
67
67
  "tool-calling",
68
68
  "webassembly"
69
69
  ],
70
+ "scripts": {
71
+ "prepack": "node ../../scripts/build-wasm.mjs"
72
+ },
70
73
  "publishConfig": {
71
74
  "access": "public",
72
75
  "provenance": true
73
- },
74
- "scripts": {}
75
- }
76
+ }
77
+ }
package/src/adapter.js ADDED
@@ -0,0 +1,161 @@
1
+ import { createStructuredStreamPool } from "./index.js";
2
+ import {
3
+ annotateError,
4
+ isStructuredStreamError,
5
+ streamError,
6
+ } from "./internal/errors.js";
7
+ import { emitDiagnostic } from "./internal/diagnostics.js";
8
+
9
+ export function defineAdapter(mapEvent) {
10
+ if (typeof mapEvent !== "function") {
11
+ throw streamError(
12
+ new TypeError("An adapter mapper must be a function"),
13
+ "INVALID_OPTIONS",
14
+ );
15
+ }
16
+
17
+ return (options) => {
18
+ const pool = createStructuredStreamPool(options);
19
+ let status = "active";
20
+ let failure;
21
+ let eventType;
22
+ let operationContext;
23
+ let receivedEvents = false;
24
+ let matched = false;
25
+
26
+ const diagnose = (diagnostic) =>
27
+ emitDiagnostic(options, { adapter: "custom", eventType, ...diagnostic });
28
+ const disposeActive = () => {
29
+ for (const id of pool.activeIds) pool.abort(id);
30
+ };
31
+
32
+ const fail = (error) => {
33
+ const original =
34
+ error instanceof Error
35
+ ? error
36
+ : new Error("Adapter stream failed", { cause: error });
37
+ const context = { adapter: "custom", eventType };
38
+ failure = isStructuredStreamError(original)
39
+ ? annotateError(original, context)
40
+ : streamError(original, "INTEGRATION_ERROR", {
41
+ ...context,
42
+ ...operationContext,
43
+ });
44
+ disposeActive();
45
+ diagnose({
46
+ code: "STREAM_ERROR",
47
+ message: failure.message,
48
+ error: failure,
49
+ id: failure.id,
50
+ });
51
+ return failure;
52
+ };
53
+
54
+ const assertActive = () => {
55
+ if (failure !== undefined) throw failure;
56
+ if (status !== "active") {
57
+ throw streamError(
58
+ new Error(`Adapter stream has been ${status}`),
59
+ status === "disposed" ? "STREAM_DISPOSED" : "INTEGRATION_ERROR",
60
+ { adapter: "custom" },
61
+ );
62
+ }
63
+ };
64
+
65
+ return {
66
+ pushAll(event) {
67
+ assertActive();
68
+ receivedEvents = true;
69
+ try {
70
+ const type = event?.type ?? event?.event_type ?? event?.kind;
71
+ eventType =
72
+ typeof type === "string"
73
+ ? type
74
+ : Array.isArray(event)
75
+ ? "operations"
76
+ : undefined;
77
+ const operations = mapEvent(event);
78
+ if (!Array.isArray(operations)) {
79
+ throw new TypeError("An adapter mapper must return an array");
80
+ }
81
+
82
+ const updates = [];
83
+ for (const operation of operations) {
84
+ operationContext = undefined;
85
+ if (
86
+ operation === null ||
87
+ typeof operation !== "object" ||
88
+ !("id" in operation)
89
+ ) {
90
+ throw new TypeError("An adapter operation must include an id");
91
+ }
92
+ operationContext = { id: operation.id };
93
+ switch (operation.type) {
94
+ case "start":
95
+ operationContext.operation = "start";
96
+ updates.push({ type: "start", ...pool.start(operation.id) });
97
+ break;
98
+ case "delta":
99
+ operationContext.operation = "push";
100
+ if (typeof operation.text !== "string") {
101
+ throw new TypeError("An adapter delta must include text");
102
+ }
103
+ updates.push({
104
+ type: "update",
105
+ ...pool.push(operation.id, operation.text),
106
+ });
107
+ break;
108
+ case "end":
109
+ operationContext.operation = "finish";
110
+ updates.push({
111
+ type: "complete",
112
+ ...pool.finish(operation.id),
113
+ });
114
+ break;
115
+ case "abort":
116
+ pool.abort(operation.id);
117
+ break;
118
+ default:
119
+ throw new TypeError("Unknown adapter operation type");
120
+ }
121
+ matched = true;
122
+ }
123
+ return updates;
124
+ } catch (error) {
125
+ throw fail(error);
126
+ } finally {
127
+ eventType = undefined;
128
+ operationContext = undefined;
129
+ }
130
+ },
131
+
132
+ finish() {
133
+ if (failure !== undefined) throw failure;
134
+ if (status === "finished") return [];
135
+ assertActive();
136
+ try {
137
+ const completed = pool.activeIds.map((id) => ({
138
+ type: "complete",
139
+ ...pool.finish(id),
140
+ }));
141
+ status = "finished";
142
+ if (receivedEvents && !matched) {
143
+ diagnose({
144
+ code: "NO_TOOL_EVENTS",
145
+ message:
146
+ "No tool operations were mapped; text-only streams are valid, otherwise check the event mapper",
147
+ });
148
+ }
149
+ return completed;
150
+ } catch (error) {
151
+ throw fail(error);
152
+ }
153
+ },
154
+
155
+ dispose() {
156
+ disposeActive();
157
+ if (status === "active") status = "disposed";
158
+ },
159
+ };
160
+ };
161
+ }
package/src/ag-ui.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type {
2
- EventStructuredStream,
3
- StructuredStreamIntegration,
2
+ BatchEventStructuredStream,
3
+ BatchStructuredStreamIntegration,
4
+ StructuredStreamIntegrationOptions,
4
5
  StructuredStreamPool,
5
6
  } from "./index.js";
6
7
 
@@ -24,8 +25,9 @@ export type AgUiToolCallEvent =
24
25
  readonly [key: string]: unknown;
25
26
  };
26
27
 
27
- export const agUI: StructuredStreamIntegration<AgUiToolCallEvent, string>;
28
+ export const agUI: BatchStructuredStreamIntegration<AgUiToolCallEvent, string>;
28
29
 
29
30
  export function createStructuredStream(
30
31
  pool?: StructuredStreamPool<string>,
31
- ): EventStructuredStream<AgUiToolCallEvent, string>;
32
+ options?: StructuredStreamIntegrationOptions,
33
+ ): BatchEventStructuredStream<AgUiToolCallEvent, string>;
package/src/ag-ui.js CHANGED
@@ -1,14 +1,22 @@
1
1
  import { createStructuredStreamPool } from "./index.js";
2
2
  import { createIntegration } from "./internal/integration.js";
3
3
 
4
- export const agUI = (pool = createStructuredStreamPool()) =>
5
- createIntegration(pool, (event, complete) => {
6
- if (event.type === "TOOL_CALL_START") return pool.start(event.toolCallId);
7
- if (event.type === "TOOL_CALL_ARGS") {
8
- return pool.push(event.toolCallId, event.delta);
9
- }
10
- if (event.type === "TOOL_CALL_END") return complete(event.toolCallId);
11
- return undefined;
12
- });
4
+ export const agUI = (pool = createStructuredStreamPool(), options) =>
5
+ createIntegration(
6
+ pool,
7
+ (event, calls) => {
8
+ if (event.type === "TOOL_CALL_START")
9
+ return calls.start(event.toolCallId);
10
+ if (event.type === "TOOL_CALL_ARGS") {
11
+ return calls.push(event.toolCallId, event.delta);
12
+ }
13
+ if (event.type === "TOOL_CALL_END")
14
+ return calls.complete(event.toolCallId);
15
+ return undefined;
16
+ },
17
+ undefined,
18
+ "ag-ui",
19
+ options,
20
+ );
13
21
 
14
22
  export { agUI as createStructuredStream };
@@ -1,6 +1,7 @@
1
1
  import type {
2
- EventStructuredStream,
3
- StructuredStreamIntegration,
2
+ BatchEventStructuredStream,
3
+ BatchStructuredStreamIntegration,
4
+ StructuredStreamIntegrationOptions,
4
5
  StructuredStreamPool,
5
6
  } from "./index.js";
6
7
 
@@ -33,11 +34,12 @@ export type AnthropicToolInputEvent =
33
34
  readonly [key: string]: unknown;
34
35
  };
35
36
 
36
- export const anthropic: StructuredStreamIntegration<
37
+ export const anthropic: BatchStructuredStreamIntegration<
37
38
  AnthropicToolInputEvent,
38
39
  string
39
40
  >;
40
41
 
41
42
  export function createStructuredStream(
42
43
  pool?: StructuredStreamPool<string>,
43
- ): EventStructuredStream<AnthropicToolInputEvent, string>;
44
+ options?: StructuredStreamIntegrationOptions,
45
+ ): BatchEventStructuredStream<AnthropicToolInputEvent, string>;
package/src/anthropic.js CHANGED
@@ -1,34 +1,48 @@
1
1
  import { createStructuredStreamPool } from "./index.js";
2
2
  import { createIntegration } from "./internal/integration.js";
3
3
 
4
- export const anthropic = (pool = createStructuredStreamPool()) => {
4
+ export const anthropic = (pool = createStructuredStreamPool(), options) => {
5
5
  const idsByIndex = new Map();
6
6
 
7
- return createIntegration(pool, (event, complete) => {
8
- if (
9
- event.type === "content_block_start" &&
10
- (event.content_block.type === "tool_use" ||
11
- event.content_block.type === "server_tool_use")
12
- ) {
13
- idsByIndex.set(event.index, event.content_block.id);
14
- return pool.start(event.content_block.id);
15
- }
7
+ return createIntegration(
8
+ pool,
9
+ (event, calls) => {
10
+ if (
11
+ event.type === "content_block_start" &&
12
+ (event.content_block.type === "tool_use" ||
13
+ event.content_block.type === "server_tool_use")
14
+ ) {
15
+ idsByIndex.set(event.index, event.content_block.id);
16
+ return calls.start(event.content_block.id);
17
+ }
16
18
 
17
- const id = idsByIndex.get(event.index);
18
- if (id === undefined) return undefined;
19
+ const id = idsByIndex.get(event.index);
20
+ if (id === undefined) {
21
+ if (
22
+ event.type === "content_block_delta" &&
23
+ event.delta.type === "input_json_delta"
24
+ ) {
25
+ calls.unmatched("Arguments delta has no matching tool block index");
26
+ }
27
+ return undefined;
28
+ }
19
29
 
20
- if (
21
- event.type === "content_block_delta" &&
22
- event.delta.type === "input_json_delta"
23
- ) {
24
- return pool.push(id, event.delta.partial_json);
25
- }
26
- if (event.type === "content_block_stop") {
27
- idsByIndex.delete(event.index);
28
- return complete(id);
29
- }
30
- return undefined;
31
- });
30
+ if (
31
+ event.type === "content_block_delta" &&
32
+ event.delta.type === "input_json_delta"
33
+ ) {
34
+ return calls.push(id, event.delta.partial_json);
35
+ }
36
+ if (event.type === "content_block_stop") {
37
+ idsByIndex.delete(event.index);
38
+ return calls.complete(id);
39
+ }
40
+ return undefined;
41
+ },
42
+ undefined,
43
+ "anthropic",
44
+ options,
45
+ );
32
46
  };
33
47
 
34
48
  export { anthropic as createStructuredStream };
@@ -1,6 +1,7 @@
1
1
  import type {
2
- EventStructuredStream,
3
- StructuredStreamIntegration,
2
+ BatchEventStructuredStream,
3
+ BatchStructuredStreamIntegration,
4
+ StructuredStreamIntegrationOptions,
4
5
  StructuredStreamPool,
5
6
  } from "./index.js";
6
7
 
@@ -31,11 +32,12 @@ export type AssistantUiStreamEvent =
31
32
  readonly [key: string]: unknown;
32
33
  };
33
34
 
34
- export const assistantUI: StructuredStreamIntegration<
35
+ export const assistantUI: BatchStructuredStreamIntegration<
35
36
  AssistantUiStreamEvent,
36
37
  string
37
38
  >;
38
39
 
39
40
  export function createStructuredStream(
40
41
  pool?: StructuredStreamPool<string>,
41
- ): EventStructuredStream<AssistantUiStreamEvent, string>;
42
+ options?: StructuredStreamIntegrationOptions,
43
+ ): BatchEventStructuredStream<AssistantUiStreamEvent, string>;
@@ -3,29 +3,40 @@ import { createIntegration } from "./internal/integration.js";
3
3
 
4
4
  const pathKey = (path) => path.join("/");
5
5
 
6
- export const assistantUI = (pool = createStructuredStreamPool()) => {
6
+ export const assistantUI = (pool = createStructuredStreamPool(), options) => {
7
7
  const idsByPath = new Map();
8
8
 
9
- return createIntegration(pool, (event, complete) => {
10
- const key = pathKey(event.path);
9
+ return createIntegration(
10
+ pool,
11
+ (event, calls) => {
12
+ const key = pathKey(event.path);
11
13
 
12
- if (event.type === "part-start" && event.part.type === "tool-call") {
13
- idsByPath.set(key, event.part.toolCallId);
14
- return pool.start(event.part.toolCallId);
15
- }
14
+ if (event.type === "part-start" && event.part.type === "tool-call") {
15
+ idsByPath.set(key, event.part.toolCallId);
16
+ return calls.start(event.part.toolCallId);
17
+ }
16
18
 
17
- const id = idsByPath.get(key);
18
- if (id === undefined) return undefined;
19
+ const id = idsByPath.get(key);
20
+ if (id === undefined) {
21
+ if (event.type === "tool-call-args-text-finish") {
22
+ calls.unmatched("Tool completion has no matching part path");
23
+ }
24
+ return undefined;
25
+ }
19
26
 
20
- if (event.type === "text-delta") {
21
- return pool.push(id, event.textDelta);
22
- }
23
- if (event.type === "tool-call-args-text-finish") {
24
- idsByPath.delete(key);
25
- return complete(id);
26
- }
27
- return undefined;
28
- });
27
+ if (event.type === "text-delta") {
28
+ return calls.push(id, event.textDelta);
29
+ }
30
+ if (event.type === "tool-call-args-text-finish") {
31
+ idsByPath.delete(key);
32
+ return calls.complete(id);
33
+ }
34
+ return undefined;
35
+ },
36
+ undefined,
37
+ "assistant-ui",
38
+ options,
39
+ );
29
40
  };
30
41
 
31
42
  export { assistantUI as createStructuredStream };
package/src/gemini.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type {
2
- EventStructuredStream,
3
- StructuredStreamIntegration,
2
+ BatchEventStructuredStream,
3
+ BatchStructuredStreamIntegration,
4
+ StructuredStreamIntegrationOptions,
4
5
  StructuredStreamPool,
5
6
  } from "./index.js";
6
7
 
@@ -32,11 +33,12 @@ export type GeminiToolCallEvent =
32
33
  readonly [key: string]: unknown;
33
34
  };
34
35
 
35
- export const gemini: StructuredStreamIntegration<
36
+ export const gemini: BatchStructuredStreamIntegration<
36
37
  GeminiToolCallEvent,
37
38
  string
38
39
  >;
39
40
 
40
41
  export function createStructuredStream(
41
42
  pool?: StructuredStreamPool<string>,
42
- ): EventStructuredStream<GeminiToolCallEvent, string>;
43
+ options?: StructuredStreamIntegrationOptions,
44
+ ): BatchEventStructuredStream<GeminiToolCallEvent, string>;
package/src/gemini.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import { createStructuredStreamPool } from "./index.js";
2
2
  import { createIntegration } from "./internal/integration.js";
3
3
 
4
- export const gemini = (pool = createStructuredStreamPool()) => {
4
+ export const gemini = (pool = createStructuredStreamPool(), options) => {
5
5
  const idsByIndex = new Map();
6
6
 
7
7
  return createIntegration(
8
8
  pool,
9
- (event, complete) => {
9
+ (event, calls) => {
10
10
  if (
11
11
  event.event_type === "step.start" &&
12
12
  event.step.type === "function_call"
@@ -18,7 +18,7 @@ export const gemini = (pool = createStructuredStreamPool()) => {
18
18
  : event.step.arguments
19
19
  ? JSON.stringify(event.step.arguments)
20
20
  : "";
21
- return pool.start(event.step.id, initial);
21
+ return calls.start(event.step.id, initial);
22
22
  }
23
23
 
24
24
  if (
@@ -27,15 +27,16 @@ export const gemini = (pool = createStructuredStreamPool()) => {
27
27
  ) {
28
28
  const id = idsByIndex.get(event.index);
29
29
  if (id !== undefined) {
30
- return pool.push(id, event.delta.partial_arguments);
30
+ return calls.push(id, event.delta.partial_arguments);
31
31
  }
32
+ calls.unmatched("Arguments delta has no matching function call index");
32
33
  }
33
34
 
34
35
  if (
35
36
  event.event_type === "interaction.completed" ||
36
37
  event.event_type === "interaction.complete"
37
38
  ) {
38
- for (const id of idsByIndex.values()) complete(id);
39
+ for (const id of idsByIndex.values()) calls.complete(id);
39
40
  idsByIndex.clear();
40
41
  }
41
42
  return undefined;
@@ -44,6 +45,8 @@ export const gemini = (pool = createStructuredStreamPool()) => {
44
45
  for (const id of idsByIndex.values()) complete(id);
45
46
  idsByIndex.clear();
46
47
  },
48
+ "gemini",
49
+ options,
47
50
  );
48
51
  };
49
52