ai-sdk-graph 0.1.0

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 ADDED
@@ -0,0 +1,213 @@
1
+ # ai-sdk-graph
2
+
3
+ A TypeScript library for building stateful, resumable workflows with human-in-the-loop support.
4
+
5
+ ## Features
6
+
7
+ - **Human in the Loop** — Suspend execution to wait for user input, approvals, or external data, then resume seamlessly
8
+ - **Type-safe** — Full TypeScript support with generic state management
9
+ - **Resumable** — Checkpoint and resume execution from any point
10
+ - **Composable** — Nested subgraphs with state mapping
11
+ - **Parallel Execution** — Fork workflows with multiple edges from a single node
12
+ - **Conditional Routing** — Dynamic edges based on state
13
+ - **Visualization** — Generate Mermaid diagrams of your workflows
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm i ai-sdk-graph
19
+ ```
20
+
21
+ ## Quick Start
22
+
23
+ ```typescript
24
+ import { graph } from 'ai-sdk-graph'
25
+
26
+ const workflow = graph<{ value: number }>()
27
+ .node('validate', ({ update }) => {
28
+ update({ value: 10 })
29
+ })
30
+ .node('transform', ({ update, state }) => {
31
+ update({ value: state().value * 2 })
32
+ })
33
+ .edge('START', 'validate')
34
+ .edge('validate', 'transform')
35
+ .edge('transform', 'END')
36
+
37
+ // Execute the workflow
38
+ const stream = workflow.execute('run-1', { value: 0 })
39
+ ```
40
+
41
+ ## Core Concepts
42
+
43
+ ### Nodes
44
+
45
+ Nodes are execution units that receive a context object with:
46
+
47
+ - `state()` — Read the current state
48
+ - `update(changes)` — Update state with partial object or function
49
+ - `suspense(data?)` — Pause execution for human-in-the-loop
50
+ - `writer` — Stream writer for UI integration
51
+
52
+ ```typescript
53
+ graph<{ count: number }>()
54
+ .node('increment', ({ state, update }) => {
55
+ update({ count: state().count + 1 })
56
+ })
57
+ ```
58
+
59
+ ### Edges
60
+
61
+ Connect nodes with static or dynamic routing:
62
+
63
+ ```typescript
64
+ // Static edge
65
+ .edge('START', 'validate')
66
+
67
+ // Dynamic edge based on state
68
+ .edge('router', (state) => state.isValid ? 'process' : 'reject')
69
+ ```
70
+
71
+ ### State Management
72
+
73
+ State is type-safe and immutable. Updates can be partial objects or functions:
74
+
75
+ ```typescript
76
+ // Partial update
77
+ update({ status: 'complete' })
78
+
79
+ // Functional update
80
+ update((state) => ({ count: state.count + 1 }))
81
+ ```
82
+
83
+ ### Human in the Loop
84
+
85
+ Suspend execution to wait for user input, approvals, or external data:
86
+
87
+ ```typescript
88
+ const workflow = graph<{ approved: boolean }>()
89
+ .node('review', ({ state, suspense }) => {
90
+ if (!state().approved) {
91
+ suspense({ reason: 'Waiting for approval' })
92
+ }
93
+ })
94
+ .edge('START', 'review')
95
+ .edge('review', 'END')
96
+
97
+ // First execution suspends
98
+ workflow.execute('run-1', { approved: false })
99
+
100
+ // Resume with updated state after user approves
101
+ workflow.execute('run-1', (existing) => ({ ...existing, approved: true }))
102
+ ```
103
+
104
+ ## Parallel Execution
105
+
106
+ Multiple edges from the same node execute targets in parallel:
107
+
108
+ ```typescript
109
+ graph<{ results: string[] }>()
110
+ .node('fork', () => {})
111
+ .node('taskA', ({ update }) => update({ results: ['A'] }))
112
+ .node('taskB', ({ update }) => update({ results: ['B'] }))
113
+ .node('join', () => {})
114
+ .edge('START', 'fork')
115
+ .edge('fork', 'taskA') // Both taskA and taskB
116
+ .edge('fork', 'taskB') // execute in parallel
117
+ .edge('taskA', 'join')
118
+ .edge('taskB', 'join')
119
+ .edge('join', 'END')
120
+ ```
121
+
122
+ ## Subgraphs
123
+
124
+ Compose workflows with nested graphs:
125
+
126
+ ```typescript
127
+ const childGraph = graph<{ value: number }>()
128
+ .node('double', ({ update, state }) => {
129
+ update({ value: state().value * 2 })
130
+ })
131
+ .edge('START', 'double')
132
+ .edge('double', 'END')
133
+
134
+ const parentGraph = graph<{ input: number; result: number }>()
135
+ .graph('process', childGraph, {
136
+ input: (parentState) => ({ value: parentState.input }),
137
+ output: (childState) => ({ result: childState.value })
138
+ })
139
+ .edge('START', 'process')
140
+ .edge('process', 'END')
141
+ ```
142
+
143
+ ## Storage
144
+
145
+ By default, graphs use in-memory storage. For production, use Redis:
146
+
147
+ ```typescript
148
+ import { graph } from 'ai-sdk-graph'
149
+ import { RedisStorage } from 'ai-sdk-graph/storage'
150
+ import Redis from 'ioredis'
151
+
152
+ const redis = new Redis()
153
+ const storage = new RedisStorage(redis)
154
+
155
+ const workflow = graph<State>(storage)
156
+ // ... define nodes and edges
157
+ ```
158
+
159
+ ## Visualization
160
+
161
+ Generate Mermaid diagrams of your workflows:
162
+
163
+ ```typescript
164
+ const diagram = workflow.toMermaid()
165
+ // or with direction
166
+ const diagram = workflow.toMermaid({ direction: 'LR' })
167
+ ```
168
+
169
+ Output:
170
+ ```mermaid
171
+ flowchart TB
172
+ START([START])
173
+ validate[validate]
174
+ transform[transform]
175
+ END([END])
176
+ START --> validate
177
+ validate --> transform
178
+ transform --> END
179
+ ```
180
+
181
+ ## API Reference
182
+
183
+ ### `graph<State>(storage?)`
184
+
185
+ Create a new graph with optional storage backend.
186
+
187
+ ### `.node(id, handler)`
188
+
189
+ Add a node with an execution handler.
190
+
191
+ ### `.edge(from, to)`
192
+
193
+ Add a static edge between nodes.
194
+
195
+ ### `.edge(from, (state) => nodeId)`
196
+
197
+ Add a dynamic edge that routes based on state.
198
+
199
+ ### `.graph(id, subgraph, options)`
200
+
201
+ Add a nested subgraph with state mapping.
202
+
203
+ ### `.execute(runId, initialState)`
204
+
205
+ Execute the graph and return a readable stream.
206
+
207
+ ### `.toMermaid(options?)`
208
+
209
+ Generate a Mermaid flowchart diagram.
210
+
211
+ ## License
212
+
213
+ MIT
@@ -0,0 +1,69 @@
1
+ import type { GraphSDK } from './types';
2
+ import { createUIMessageStream } from 'ai';
3
+ export declare class SuspenseError extends Error {
4
+ readonly data?: unknown;
5
+ constructor(data?: unknown);
6
+ }
7
+ type Writer = Parameters<Parameters<typeof createUIMessageStream>[0]['execute']>[0]['writer'];
8
+ export declare class Graph<State extends Record<string, unknown>, NodeKeys extends string = 'START' | 'END'> {
9
+ private readonly nodeRegistry;
10
+ private readonly edgeRegistry;
11
+ private readonly subgraphRegistry;
12
+ private readonly storage;
13
+ private readonly emitter;
14
+ private readonly stateManager;
15
+ constructor(storage?: GraphSDK.GraphStorage<State, NodeKeys>);
16
+ node<NewKey extends string>(id: NewKey, execute: ({ state, writer, suspense, update }: {
17
+ state: () => Readonly<State>;
18
+ writer: Writer;
19
+ suspense: (data?: unknown) => never;
20
+ update: (update: GraphSDK.StateUpdate<State>) => void;
21
+ }) => Promise<void> | void): Graph<State, NodeKeys | NewKey>;
22
+ edge(from: NodeKeys, to: NodeKeys | ((state: State) => NodeKeys)): Graph<State, NodeKeys>;
23
+ graph<NewKey extends string, ChildState extends Record<string, unknown>, ChildNodeKeys extends string = 'START' | 'END'>(id: NewKey, subgraph: Graph<ChildState, ChildNodeKeys>, options: GraphSDK.SubgraphOptions<State, ChildState>): Graph<State, NodeKeys | NewKey>;
24
+ get nodes(): ReadonlyMap<NodeKeys, GraphSDK.Node<State, NodeKeys>>;
25
+ get edges(): ReadonlyMap<NodeKeys, GraphSDK.Edge<State, NodeKeys>[]>;
26
+ get subgraphs(): ReadonlyMap<NodeKeys, {
27
+ subgraph: Graph<any, any>;
28
+ options: GraphSDK.SubgraphOptions<State, any>;
29
+ }>;
30
+ toMermaid(options?: {
31
+ direction?: 'TB' | 'LR';
32
+ }): string;
33
+ private generateMermaid;
34
+ private extractPossibleTargets;
35
+ execute(runId: string, initialState: State | ((state: State | undefined) => State)): ReadableStream<import("ai").InferUIMessageChunk<import("ai").UIMessage<unknown, import("ai").UIDataTypes, import("ai").UITools>>>;
36
+ executeInternal(runId: string, initialState: State, writer: Writer): Promise<State>;
37
+ private registerBuiltInNodes;
38
+ private addEdgeToRegistry;
39
+ private createExecutionContext;
40
+ private runExecutionLoop;
41
+ private runExecutionLoopInternal;
42
+ private executeWithStrategy;
43
+ private resumeWithStrategy;
44
+ private executeNodesWithStrategy;
45
+ private handleBatchResultWithStrategy;
46
+ private hasSuspendedNodes;
47
+ private hasNodesToExecute;
48
+ private restoreCheckpoint;
49
+ private isValidCheckpoint;
50
+ private hasNodeIds;
51
+ private hasAtLeastOneNode;
52
+ private restoreFromCheckpoint;
53
+ private createFreshExecution;
54
+ private persistCheckpoint;
55
+ private executeBatch;
56
+ private executeSingleNode;
57
+ private createNodeExecutionParams;
58
+ private executeSubgraphNode;
59
+ private generateSubgraphRunId;
60
+ private createSuspenseFunction;
61
+ private resolveNodeIds;
62
+ private computeNextNodes;
63
+ private findSuccessors;
64
+ private resolveEdgeTarget;
65
+ private deduplicateNodes;
66
+ private excludeTerminalNodes;
67
+ }
68
+ export declare function graph<State extends Record<string, unknown>, NodeKeys extends string = 'START' | 'END'>(storage?: GraphSDK.GraphStorage<State, NodeKeys>): Graph<State, NodeKeys>;
69
+ export {};
@@ -0,0 +1,3 @@
1
+ export * from './graph';
2
+ export * from './storage';
3
+ export * from './types';
package/dist/index.js ADDED
@@ -0,0 +1,379 @@
1
+ // src/storage.ts
2
+ import Redis from "ioredis";
3
+
4
+ class InMemoryStorage {
5
+ store = new Map;
6
+ async save(runId, checkpoint) {
7
+ this.store.set(runId, checkpoint);
8
+ }
9
+ async load(runId) {
10
+ return this.store.get(runId) ?? null;
11
+ }
12
+ async delete(runId) {
13
+ this.store.delete(runId);
14
+ }
15
+ }
16
+
17
+ class RedisStorage {
18
+ redis;
19
+ constructor(redisUrl) {
20
+ this.redis = new Redis(redisUrl);
21
+ }
22
+ async save(runId, checkpoint) {
23
+ await this.redis.set(runId, JSON.stringify(checkpoint));
24
+ }
25
+ async load(runId) {
26
+ const data = await this.redis.get(runId);
27
+ return data ? JSON.parse(data) : null;
28
+ }
29
+ async delete(runId) {
30
+ await this.redis.del(runId);
31
+ }
32
+ }
33
+
34
+ // src/graph.ts
35
+ import { createUIMessageStream } from "ai";
36
+ var BUILT_IN_NODES = {
37
+ START: "START",
38
+ END: "END"
39
+ };
40
+
41
+ class SuspenseError extends Error {
42
+ data;
43
+ constructor(data) {
44
+ super("Suspense");
45
+ this.name = "SuspenseError";
46
+ this.data = data;
47
+ }
48
+ }
49
+
50
+ class Graph {
51
+ nodeRegistry = new Map;
52
+ edgeRegistry = new Map;
53
+ subgraphRegistry = new Map;
54
+ storage;
55
+ emitter = new NodeEventEmitter;
56
+ stateManager = new StateManager;
57
+ constructor(storage = new InMemoryStorage) {
58
+ this.storage = storage;
59
+ this.registerBuiltInNodes();
60
+ }
61
+ node(id, execute) {
62
+ const node = { id, execute };
63
+ this.nodeRegistry.set(node.id, node);
64
+ return this;
65
+ }
66
+ edge(from, to) {
67
+ const edge = { from, to };
68
+ this.addEdgeToRegistry(edge);
69
+ return this;
70
+ }
71
+ graph(id, subgraph, options) {
72
+ this.subgraphRegistry.set(id, { subgraph, options });
73
+ const node = {
74
+ id,
75
+ execute: async () => {}
76
+ };
77
+ this.nodeRegistry.set(node.id, node);
78
+ return this;
79
+ }
80
+ get nodes() {
81
+ return this.nodeRegistry;
82
+ }
83
+ get edges() {
84
+ return this.edgeRegistry;
85
+ }
86
+ get subgraphs() {
87
+ return this.subgraphRegistry;
88
+ }
89
+ toMermaid(options) {
90
+ const direction = options?.direction ?? "TB";
91
+ return this.generateMermaid(direction, "");
92
+ }
93
+ generateMermaid(direction, prefix) {
94
+ const lines = [];
95
+ const indent = prefix ? " " : " ";
96
+ if (!prefix) {
97
+ lines.push(`flowchart ${direction}`);
98
+ }
99
+ for (const [nodeId] of this.nodeRegistry) {
100
+ const prefixedId = prefix ? `${prefix}_${nodeId}` : nodeId;
101
+ const subgraphEntry = this.subgraphRegistry.get(nodeId);
102
+ if (subgraphEntry) {
103
+ lines.push(`${indent}subgraph ${prefixedId}[${nodeId}]`);
104
+ lines.push(`${indent} direction ${direction}`);
105
+ const subgraphContent = subgraphEntry.subgraph.generateMermaid(direction, prefixedId);
106
+ const subgraphLines = subgraphContent.split(`
107
+ `);
108
+ lines.push(...subgraphLines.map((line) => `${indent}${line}`));
109
+ lines.push(`${indent}end`);
110
+ } else if (nodeId === "START" || nodeId === "END") {
111
+ lines.push(`${indent}${prefixedId}([${nodeId}])`);
112
+ } else {
113
+ lines.push(`${indent}${prefixedId}[${nodeId}]`);
114
+ }
115
+ }
116
+ for (const [fromId, edges] of this.edgeRegistry) {
117
+ for (const edge of edges) {
118
+ const prefixedFrom = prefix ? `${prefix}_${fromId}` : fromId;
119
+ if (typeof edge.to === "function") {
120
+ const possibleTargets = this.extractPossibleTargets(edge.to);
121
+ for (const targetId of possibleTargets) {
122
+ const prefixedTo = prefix ? `${prefix}_${targetId}` : targetId;
123
+ lines.push(`${indent}${prefixedFrom} -.-> ${prefixedTo}`);
124
+ }
125
+ } else {
126
+ const prefixedTo = prefix ? `${prefix}_${edge.to}` : edge.to;
127
+ lines.push(`${indent}${prefixedFrom} --> ${prefixedTo}`);
128
+ }
129
+ }
130
+ }
131
+ return lines.join(`
132
+ `);
133
+ }
134
+ extractPossibleTargets(edgeFn) {
135
+ const fnString = edgeFn.toString();
136
+ const nodeIds = Array.from(this.nodeRegistry.keys());
137
+ return nodeIds.filter((nodeId) => {
138
+ const patterns = [
139
+ `'${nodeId}'`,
140
+ `"${nodeId}"`,
141
+ `\`${nodeId}\``
142
+ ];
143
+ return patterns.some((pattern) => fnString.includes(pattern));
144
+ });
145
+ }
146
+ execute(runId, initialState) {
147
+ return createUIMessageStream({
148
+ execute: async ({ writer }) => {
149
+ const context = await this.createExecutionContext(runId, initialState, writer);
150
+ await this.runExecutionLoop(context);
151
+ }
152
+ });
153
+ }
154
+ async executeInternal(runId, initialState, writer) {
155
+ const context = await this.createExecutionContext(runId, initialState, writer);
156
+ await this.runExecutionLoopInternal(context);
157
+ return context.state;
158
+ }
159
+ registerBuiltInNodes() {
160
+ this.node(BUILT_IN_NODES.START, () => {});
161
+ this.node(BUILT_IN_NODES.END, () => {});
162
+ }
163
+ addEdgeToRegistry(edge) {
164
+ const existingEdges = this.edgeRegistry.get(edge.from) ?? [];
165
+ existingEdges.push(edge);
166
+ this.edgeRegistry.set(edge.from, existingEdges);
167
+ }
168
+ async createExecutionContext(runId, initialState, writer) {
169
+ const restored = await this.restoreCheckpoint(runId, initialState);
170
+ return { runId, writer, ...restored };
171
+ }
172
+ async runExecutionLoop(context) {
173
+ await this.executeWithStrategy(context, "return");
174
+ }
175
+ async runExecutionLoopInternal(context) {
176
+ await this.executeWithStrategy(context, "throw");
177
+ }
178
+ async executeWithStrategy(context, strategy) {
179
+ if (this.hasSuspendedNodes(context)) {
180
+ const shouldContinue = await this.resumeWithStrategy(context, strategy);
181
+ if (!shouldContinue)
182
+ return;
183
+ }
184
+ await this.executeNodesWithStrategy(context, strategy);
185
+ }
186
+ async resumeWithStrategy(context, strategy) {
187
+ const suspenses = await this.executeBatch(context.suspendedNodes, context);
188
+ return this.handleBatchResultWithStrategy(context, suspenses, strategy);
189
+ }
190
+ async executeNodesWithStrategy(context, strategy) {
191
+ while (this.hasNodesToExecute(context)) {
192
+ const suspenses = await this.executeBatch(context.currentNodes, context);
193
+ const shouldContinue = await this.handleBatchResultWithStrategy(context, suspenses, strategy);
194
+ if (!shouldContinue)
195
+ return;
196
+ }
197
+ await this.storage.delete(context.runId);
198
+ }
199
+ async handleBatchResultWithStrategy(context, suspenses, strategy) {
200
+ if (suspenses.length > 0) {
201
+ context.suspendedNodes = suspenses.map((s) => s.node);
202
+ await this.persistCheckpoint(context);
203
+ if (strategy === "throw") {
204
+ throw new SuspenseError({ type: "subgraph-suspended" });
205
+ }
206
+ return false;
207
+ }
208
+ context.currentNodes = this.computeNextNodes(context.currentNodes, context.state);
209
+ context.suspendedNodes = [];
210
+ await this.persistCheckpoint(context);
211
+ return true;
212
+ }
213
+ hasSuspendedNodes(context) {
214
+ return context.suspendedNodes.length > 0;
215
+ }
216
+ hasNodesToExecute(context) {
217
+ return context.currentNodes.length > 0;
218
+ }
219
+ async restoreCheckpoint(runId, initialState) {
220
+ const checkpoint = await this.storage.load(runId);
221
+ if (this.isValidCheckpoint(checkpoint)) {
222
+ return this.restoreFromCheckpoint(checkpoint, initialState);
223
+ }
224
+ return this.createFreshExecution(initialState);
225
+ }
226
+ isValidCheckpoint(checkpoint) {
227
+ return this.hasNodeIds(checkpoint) && this.hasAtLeastOneNode(checkpoint);
228
+ }
229
+ hasNodeIds(checkpoint) {
230
+ return checkpoint?.nodeIds != null;
231
+ }
232
+ hasAtLeastOneNode(checkpoint) {
233
+ return checkpoint.nodeIds.length > 0;
234
+ }
235
+ restoreFromCheckpoint(checkpoint, initialState) {
236
+ const state = this.stateManager.resolve(initialState, checkpoint.state);
237
+ return {
238
+ state,
239
+ currentNodes: this.resolveNodeIds(checkpoint.nodeIds),
240
+ suspendedNodes: this.resolveNodeIds(checkpoint.suspendedNodes)
241
+ };
242
+ }
243
+ createFreshExecution(initialState) {
244
+ const state = this.stateManager.resolve(initialState, undefined);
245
+ const startNode = this.nodeRegistry.get(BUILT_IN_NODES.START);
246
+ return {
247
+ state,
248
+ currentNodes: [startNode],
249
+ suspendedNodes: []
250
+ };
251
+ }
252
+ async persistCheckpoint(context) {
253
+ await this.storage.save(context.runId, {
254
+ state: context.state,
255
+ nodeIds: context.currentNodes.map((n) => n.id),
256
+ suspendedNodes: context.suspendedNodes.map((n) => n.id)
257
+ });
258
+ }
259
+ async executeBatch(nodes, context) {
260
+ const results = await Promise.all(nodes.map((node) => this.executeSingleNode(node, context)));
261
+ return results.filter((r) => r !== null);
262
+ }
263
+ async executeSingleNode(node, context) {
264
+ const subgraphEntry = this.subgraphRegistry.get(node.id);
265
+ if (subgraphEntry) {
266
+ return this.executeSubgraphNode(node, context, subgraphEntry);
267
+ }
268
+ this.emitter.emitStart(context.writer, node.id);
269
+ try {
270
+ await node.execute(this.createNodeExecutionParams(context));
271
+ this.emitter.emitEnd(context.writer, node.id);
272
+ return null;
273
+ } catch (error) {
274
+ if (error instanceof SuspenseError) {
275
+ this.emitter.emitSuspense(context.writer, node.id, error.data);
276
+ return { node, error };
277
+ }
278
+ throw error;
279
+ }
280
+ }
281
+ createNodeExecutionParams(context) {
282
+ return {
283
+ state: () => context.state,
284
+ writer: context.writer,
285
+ suspense: this.createSuspenseFunction(),
286
+ update: (update) => {
287
+ context.state = this.stateManager.apply(context.state, update);
288
+ }
289
+ };
290
+ }
291
+ async executeSubgraphNode(node, context, entry) {
292
+ const { subgraph, options } = entry;
293
+ this.emitter.emitStart(context.writer, node.id);
294
+ const subgraphRunId = this.generateSubgraphRunId(context.runId, node.id);
295
+ try {
296
+ const childFinalState = await subgraph.executeInternal(subgraphRunId, options.input(context.state), context.writer);
297
+ const parentUpdate = options.output(childFinalState, context.state);
298
+ context.state = this.stateManager.apply(context.state, parentUpdate);
299
+ this.emitter.emitEnd(context.writer, node.id);
300
+ return null;
301
+ } catch (error) {
302
+ if (error instanceof SuspenseError) {
303
+ this.emitter.emitSuspense(context.writer, node.id, error.data);
304
+ return { node, error };
305
+ }
306
+ throw error;
307
+ }
308
+ }
309
+ generateSubgraphRunId(parentRunId, nodeId) {
310
+ return `${parentRunId}:subgraph:${nodeId}`;
311
+ }
312
+ createSuspenseFunction() {
313
+ return (data) => {
314
+ throw new SuspenseError(data);
315
+ };
316
+ }
317
+ resolveNodeIds(nodeIds) {
318
+ return nodeIds.map((id) => this.nodeRegistry.get(id)).filter((node) => node != null);
319
+ }
320
+ computeNextNodes(currentNodes, state) {
321
+ const successors = currentNodes.flatMap((node) => this.findSuccessors(node.id, state));
322
+ const uniqueSuccessors = this.deduplicateNodes(successors);
323
+ return this.excludeTerminalNodes(uniqueSuccessors);
324
+ }
325
+ findSuccessors(nodeId, state) {
326
+ const outgoingEdges = this.edgeRegistry.get(nodeId) ?? [];
327
+ return outgoingEdges.map((edge) => this.resolveEdgeTarget(edge, state)).filter((node) => node != null);
328
+ }
329
+ resolveEdgeTarget(edge, state) {
330
+ const targetId = typeof edge.to === "function" ? edge.to(state) : edge.to;
331
+ return this.nodeRegistry.get(targetId);
332
+ }
333
+ deduplicateNodes(nodes) {
334
+ return [...new Set(nodes)];
335
+ }
336
+ excludeTerminalNodes(nodes) {
337
+ return nodes.filter((node) => node.id !== BUILT_IN_NODES.END);
338
+ }
339
+ }
340
+ function graph(storage) {
341
+ return new Graph(storage);
342
+ }
343
+
344
+ class NodeEventEmitter {
345
+ emitStart(writer, nodeId) {
346
+ writer.write({ type: "data-node-start", data: nodeId });
347
+ }
348
+ emitEnd(writer, nodeId) {
349
+ writer.write({ type: "data-node-end", data: nodeId });
350
+ }
351
+ emitSuspense(writer, nodeId, data) {
352
+ writer.write({ type: "data-node-suspense", data: { nodeId, data } });
353
+ }
354
+ }
355
+
356
+ class StateManager {
357
+ apply(state, update) {
358
+ return {
359
+ ...state,
360
+ ...typeof update === "function" ? update(state) : update
361
+ };
362
+ }
363
+ resolve(initialState, existingState) {
364
+ if (this.isStateFactory(initialState)) {
365
+ return initialState(existingState);
366
+ }
367
+ return existingState ?? initialState;
368
+ }
369
+ isStateFactory(initialState) {
370
+ return typeof initialState === "function";
371
+ }
372
+ }
373
+ export {
374
+ graph,
375
+ SuspenseError,
376
+ RedisStorage,
377
+ InMemoryStorage,
378
+ Graph
379
+ };
@@ -0,0 +1,14 @@
1
+ import type { GraphSDK } from './types';
2
+ export declare class InMemoryStorage<State extends Record<string, unknown>, NodeKeys extends string> implements GraphSDK.GraphStorage<State, NodeKeys> {
3
+ private store;
4
+ save(runId: string, checkpoint: GraphSDK.Checkpoint<State, NodeKeys>): Promise<void>;
5
+ load(runId: string): Promise<GraphSDK.Checkpoint<State, NodeKeys> | null>;
6
+ delete(runId: string): Promise<void>;
7
+ }
8
+ export declare class RedisStorage<State extends Record<string, unknown>, NodeKeys extends string> implements GraphSDK.GraphStorage<State, NodeKeys> {
9
+ private redis;
10
+ constructor(redisUrl: string);
11
+ save(runId: string, checkpoint: GraphSDK.Checkpoint<State, NodeKeys>): Promise<void>;
12
+ load(runId: string): Promise<GraphSDK.Checkpoint<State, NodeKeys> | null>;
13
+ delete(runId: string): Promise<void>;
14
+ }
@@ -0,0 +1,42 @@
1
+ import type { UIMessageStreamWriter } from 'ai';
2
+ export declare namespace GraphSDK {
3
+ type StateUpdate<State> = Partial<State> | ((state: State) => Partial<State>);
4
+ interface SubgraphOptions<ParentState extends Record<string, unknown>, ChildState extends Record<string, unknown>> {
5
+ input: (parentState: ParentState) => ChildState;
6
+ output: (childState: ChildState, parentState: ParentState) => Partial<ParentState>;
7
+ }
8
+ interface Graph<State extends Record<string, unknown>, NodeKeys extends string> {
9
+ nodes: Map<NodeKeys, Node<State, NodeKeys>>;
10
+ edges: Map<NodeKeys, Edge<State, NodeKeys>[]>;
11
+ }
12
+ interface Node<State extends Record<string, unknown>, NodeKeys extends string> {
13
+ id: NodeKeys;
14
+ execute: ({ state, writer, suspense, update }: {
15
+ state: () => Readonly<State>;
16
+ writer: UIMessageStreamWriter;
17
+ suspense: (data?: unknown) => never;
18
+ update: (update: StateUpdate<State>) => void;
19
+ }) => Promise<void> | void;
20
+ }
21
+ interface Edge<State extends Record<string, unknown>, NodeKeys extends string> {
22
+ from: NodeKeys;
23
+ to: NodeKeys | ((state: State) => NodeKeys);
24
+ }
25
+ interface Checkpoint<State extends Record<string, unknown>, NodeKeys extends string> {
26
+ state: State;
27
+ nodeIds: NodeKeys[];
28
+ suspendedNodes: NodeKeys[];
29
+ }
30
+ interface GraphStorage<State extends Record<string, unknown>, NodeKeys extends string> {
31
+ save(runId: string, checkpoint: Checkpoint<State, NodeKeys>): Promise<void>;
32
+ load(runId: string): Promise<Checkpoint<State, NodeKeys> | null>;
33
+ delete(runId: string): Promise<void>;
34
+ }
35
+ interface ExecutionContext<State extends Record<string, unknown>, NodeKeys extends string> {
36
+ runId: string;
37
+ state: State;
38
+ currentNodes: Node<State, NodeKeys>[];
39
+ suspendedNodes: Node<State, NodeKeys>[];
40
+ writer: UIMessageStreamWriter;
41
+ }
42
+ }
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "ai-sdk-graph",
3
+ "version": "0.1.0",
4
+ "description": "Graph-based workflows for the AI SDK",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "module": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "scripts": {
20
+ "test": "bun test",
21
+ "build": "bun build src/index.ts --outdir dist --target node --packages=external && tsc -p tsconfig.build.json"
22
+ },
23
+ "devDependencies": {
24
+ "@types/bun": "latest"
25
+ },
26
+ "peerDependencies": {
27
+ "typescript": "^5"
28
+ },
29
+ "dependencies": {
30
+ "ai": "^6.0.37",
31
+ "ioredis": "^5.9.2"
32
+ }
33
+ }