streamfold 0.1.4 → 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
@@ -29,11 +29,36 @@ stream.finish();
29
29
  stream.dispose();
30
30
  ```
31
31
 
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`.
36
+
32
37
  Use `streamfold/assistant-ui`, `streamfold/vercel-ai`, `streamfold/openai`,
33
38
  `streamfold/anthropic`, `streamfold/gemini`, `streamfold/langchain`, or
34
39
  `streamfold/ag-ui` for decoded SDK events. Integrations use structural event
35
40
  types and do not load provider SDKs.
36
41
 
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.
46
+
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.
53
+
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.
57
+
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.
61
+
37
62
  See the repository for the [API reference](https://github.com/assistant-ui/streamfold/blob/main/API.md),
38
63
  [integration guide](https://github.com/assistant-ui/streamfold/blob/main/MIGRATION.md),
39
64
  and [benchmarks](https://github.com/assistant-ui/streamfold#performance).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "streamfold",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Protocol-neutral incremental state for structured AI streams",
5
5
  "type": "module",
6
6
  "license": "MIT",
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