Incremental structured state for AI streams.
Streamfold turns fragmented tool-call JSON into live partial values. Its Rust/WebAssembly engine keeps parser state between deltas, avoiding a full reparse after every chunk. The ESM API works in Node.js 20+ and modern browsers without native build tools or provider SDK dependencies.
pnpm add streamfoldimport { createStructuredStream } from "streamfold";
const stream = createStructuredStream();
stream.push('{"city":"San');
const update = stream.push(' Francisco"}');
console.log(update.partialValue); // { city: "San Francisco" }
console.log(stream.getFieldState(["city"])); // "complete"
stream.finish();
stream.dispose();partialValue is updated in place by default. For stable values to pass to
React or a store, opt into createStructuredStream({ snapshots: "immutable" }).
Earlier snapshots stay unchanged, and unchanged branches keep their identity.
Reactive stores can also apply the compact set, append, and complete
patches in changes.
Call prepareStreamfold() while the app is idle to compile and cache the
embedded WebAssembly module asynchronously before first use. Scanner
construction remains synchronous.
import { createStructuredStream, prepareStreamfold } from "streamfold";
await prepareStreamfold();
const stream = createStructuredStream();Use a pool for interleaved tool calls:
import { createStructuredStreamPool } from "streamfold";
const streams = createStructuredStreamPool();
streams.start("call-1");
streams.push("call-1", '{"query":"stream');
streams.push("call-1", 'fold"}');
console.log(streams.finish("call-1").value);
// { query: "streamfold" }Streams default to 16 MiB, nesting depth 128, and 256 active calls per pool. All limits are configurable.
Streamfold includes isolated adapters for decoded SDK events:
| Import | Event source |
|---|---|
streamfold/assistant-ui |
assistant-stream |
streamfold/vercel-ai |
Vercel AI SDK |
streamfold/openai |
OpenAI Responses |
streamfold/anthropic |
Anthropic Messages |
streamfold/cohere |
Cohere v2 Chat |
streamfold/bedrock |
Amazon Bedrock ConverseStream |
streamfold/gemini |
Gemini Interactions |
streamfold/langchain |
LangChain message chunks |
streamfold/ag-ui |
AG-UI events |
import { createStructuredStreamPool } from "streamfold";
import { createStructuredStream } from "streamfold/assistant-ui";
const toolInputs = createStructuredStream(
createStructuredStreamPool({ snapshots: "immutable" }),
);
for await (const event of assistantStream) {
for (const update of toolInputs.pushAll(event)) {
renderToolInput(update.id, update.partialValue);
// update.type is "start", "update", or "complete".
}
}
const completedCalls = toolInputs.finish();To enable Streamfold inside assistant-stream's shared tool-argument parser, use
an assistant-stream build that exposes registerJsonParserBackend and initialize
once before starting streams:
import { initializeAssistantStream } from "streamfold/assistant-ui";
const enabled = await initializeAssistantStream();This is an explicit opt-in. Merely importing streamfold/assistant-ui keeps its
existing event adapter behavior and does not register anything. Initialization
returns false if the optional assistant-stream peer is absent, lacks the
registration API, or WebAssembly/finalization support is unavailable. Older
assistant-stream versions keep native parsing. Concurrent or repeated calls
share initialization; a failed attempt can be retried.
Once enabled, new shared parsers use Streamfold across copies of assistant-stream in the same JavaScript realm. Existing parsers retain their backend. Snapshots, partial-field metadata, accepted text, independent branches, and native fallback are owned by assistant-stream. Unsupported prefixes fall back; completed and rejected WASM sessions dispose immediately. Complete single-call input keeps the native fast path. Provider SDK parsers are unaffected. Small deltas can be slower, so measure representative arguments before opting in.
The assistant-ui adapter accepts native assistant-stream events: part-start
uses the root path [], while deltas and completion use the part index such as
[0]. Every part, including text and reasoning, advances that index. Explicitly
indexed part-start events from earlier integrations remain supported.
Adapters consume structural event shapes and do not install or load the
provider SDKs. pushAll() returns every update when one event contains
multiple calls; existing push() usage remains supported. Lifecycle
completion means the arguments are finalized, not that a tool has executed.
The Mistral Responses example reuses
streamfold/openai after unwrapping SDK frames and converting their itemId
field to item_id.
The Vercel AI SDK adapter uses the final tool-call input from fullStream or
tool-input-available input from toUIMessageStream(), preserving schema defaults
and transforms after streamed deltas. It also accepts these events without
preceding deltas and emits their completed input once.
For hosts exposing a tool-input parser factory, embed Streamfold directly:
import { createToolInputParser } from "streamfold/vercel-ai";
const messages = readUIMessageStream({
stream,
experimental_createToolInputParser: createToolInputParser,
});The generic Vercel hook is proposed in vercel/ai#24197.
Harness#527 accepts the same
factory through initialToolInputParser. These host APIs are not yet published.
Each call gets an isolated parser with immutable snapshots. Supply a closure
such as () => createToolInputParser({ maxBytes: 1024 }) to configure limits.
The host owns completion, cancellation, and authoritative final input. See the
embedded parser contract for lifecycle and
partial-value behavior.
Pass { onDiagnostic(diagnostic) { /* report diagnostic */ } } as an adapter's
second argument for opt-in event-matching and error diagnostics. Parser errors
preserve their native SyntaxError or RangeError type and include a stable
code, a UTF-8 byteOffset, and call/event context where available.
LangChain chunks can identify a call by index or, when the index is absent, by call ID. Each unindexed fragment must carry its call ID.
For a custom event protocol, defineAdapter(mapEvent) accepts a synchronous
mapper returning start, delta, end, or abort operations. Each factory
call creates its own parser pool; pushAll(event) returns all updates from an
event, including multi-call batches. See custom adapters
for a switch-based example and lifecycle rules.
Use readStructured(events, { adapter }) to consume that custom adapter with a
for await loop. It yields every update, finalizes remaining calls when the
source ends, and disposes the session when the loop exits. See
managed consumption for examples and cancellation
boundaries.
For any built-in SDK integration, use
readStructured(events, { integration: assistantUI }) instead. Both paths yield
the same lifecycle updates and clean up on early exit, without duplicate final
results. See the API reference for factory options.
On the published streamfold@0.1.1 benchmark, retaining state was 144–217×
faster than rebuilding the partial value after every 16-character delta for a
48–54 KB tool call. The advantage is largest for frequent, small deltas; plain
JSON.parse can be simpler and faster for one-shot JSON or large chunks.
See the benchmark report for fixtures, methodology, raw commands, and crossover points. These results use default live values; immutable snapshots add copying and freezing costs, especially for wide, growing arrays or objects.
- Runnable integration examples
- API reference
- Integration and migration guide
- Architecture
- Contributing
- Changelog
- Code of conduct
Requires Node.js 22+, pnpm 11, and Rustup.
pnpm install
pnpm check
pnpm pack:checkBenchmarks and the local dashboard are available through pnpm bench,
pnpm bench:sdk, and pnpm dashboard.
Streamfold is pre-1.0 and has not received a security audit. Treat parsed values as untrusted input and validate them before tool execution.
MIT © Streamfold contributors