@langchain/langgraph-sdk 0.0.40 → 0.0.41

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.
@@ -55,6 +55,92 @@ function findLastIndex(array, predicate) {
55
55
  }
56
56
  return -1;
57
57
  }
58
+ function getBranchSequence(history) {
59
+ const childrenMap = {};
60
+ // First pass - collect nodes for each checkpoint
61
+ history.forEach((state) => {
62
+ const checkpointId = state.parent_checkpoint?.checkpoint_id ?? "$";
63
+ childrenMap[checkpointId] ??= [];
64
+ childrenMap[checkpointId].push(state);
65
+ });
66
+ const rootSequence = { type: "sequence", items: [] };
67
+ const queue = [{ id: "$", sequence: rootSequence, path: [] }];
68
+ const paths = [];
69
+ const visited = new Set();
70
+ while (queue.length > 0) {
71
+ const task = queue.shift();
72
+ if (visited.has(task.id))
73
+ continue;
74
+ visited.add(task.id);
75
+ const children = childrenMap[task.id];
76
+ if (children == null || children.length === 0)
77
+ continue;
78
+ // If we've encountered a fork (2+ children), push the fork
79
+ // to the sequence and add a new sequence for each child
80
+ let fork;
81
+ if (children.length > 1) {
82
+ fork = { type: "fork", items: [] };
83
+ task.sequence.items.push(fork);
84
+ }
85
+ for (const value of children) {
86
+ const id = value.checkpoint.checkpoint_id;
87
+ let sequence = task.sequence;
88
+ let path = task.path;
89
+ if (fork != null) {
90
+ sequence = { type: "sequence", items: [] };
91
+ fork.items.unshift(sequence);
92
+ path = path.slice();
93
+ path.push(id);
94
+ paths.push(path);
95
+ }
96
+ sequence.items.push({ type: "node", value, path });
97
+ queue.push({ id, sequence, path });
98
+ }
99
+ }
100
+ return { rootSequence, paths };
101
+ }
102
+ const PATH_SEP = ">";
103
+ const ROOT_ID = "$";
104
+ // Get flat view
105
+ function getBranchView(sequence, paths, branch) {
106
+ const path = branch.split(PATH_SEP);
107
+ const pathMap = {};
108
+ for (const path of paths) {
109
+ const parent = path.at(-2) ?? ROOT_ID;
110
+ pathMap[parent] ??= [];
111
+ pathMap[parent].unshift(path);
112
+ }
113
+ const history = [];
114
+ const branchByCheckpoint = {};
115
+ const forkStack = path.slice();
116
+ const queue = [...sequence.items];
117
+ while (queue.length > 0) {
118
+ const item = queue.shift();
119
+ if (item.type === "node") {
120
+ history.push(item.value);
121
+ branchByCheckpoint[item.value.checkpoint.checkpoint_id] = {
122
+ branch: item.path.join(PATH_SEP),
123
+ branchOptions: (item.path.length > 0
124
+ ? pathMap[item.path.at(-2) ?? ROOT_ID] ?? []
125
+ : []).map((p) => p.join(PATH_SEP)),
126
+ };
127
+ }
128
+ if (item.type === "fork") {
129
+ const forkId = forkStack.shift();
130
+ const index = forkId != null
131
+ ? item.items.findIndex((value) => {
132
+ const firstItem = value.items.at(0);
133
+ if (!firstItem || firstItem.type !== "node")
134
+ return false;
135
+ return firstItem.value.checkpoint.checkpoint_id === forkId;
136
+ })
137
+ : -1;
138
+ const nextItems = item.items.at(index)?.items ?? [];
139
+ queue.push(...nextItems);
140
+ }
141
+ }
142
+ return { history, branchByCheckpoint };
143
+ }
58
144
  function fetchHistory(client, threadId) {
59
145
  return client.threads.getHistory(threadId, { limit: 1000 });
60
146
  }
@@ -81,12 +167,26 @@ function useThreadHistory(threadId, client, clearCallbackRef, submittingRef) {
81
167
  mutate: (mutateId) => fetcher(mutateId ?? threadId),
82
168
  };
83
169
  }
170
+ const useControllableThreadId = (options) => {
171
+ const [localThreadId, _setLocalThreadId] = useState(options?.threadId ?? null);
172
+ const onThreadIdRef = useRef(options?.onThreadId);
173
+ onThreadIdRef.current = options?.onThreadId;
174
+ const onThreadId = useCallback((threadId) => {
175
+ _setLocalThreadId(threadId);
176
+ onThreadIdRef.current?.(threadId);
177
+ }, []);
178
+ if (typeof options?.threadId === "undefined") {
179
+ return [localThreadId, onThreadId];
180
+ }
181
+ return [options.threadId, onThreadId];
182
+ };
84
183
  export function useStream(options) {
85
- const { assistantId, threadId, withMessages, onError, onFinish } = options;
184
+ let { assistantId, messagesKey, onError, onFinish } = options;
185
+ messagesKey ??= "messages";
86
186
  const client = useMemo(() => new Client({ apiUrl: options.apiUrl, apiKey: options.apiKey }), [options.apiKey, options.apiUrl]);
87
- const [branchPath, setBranchPath] = useState([]);
187
+ const [threadId, onThreadId] = useControllableThreadId(options);
188
+ const [branch, setBranch] = useState("");
88
189
  const [isLoading, setIsLoading] = useState(false);
89
- const [_, setEvents] = useState([]);
90
190
  const [streamError, setStreamError] = useState(undefined);
91
191
  const [streamValues, setStreamValues] = useState(null);
92
192
  const messageManagerRef = useRef(new MessageTupleManager());
@@ -116,94 +216,13 @@ export function useStream(options) {
116
216
  // TODO: should we permit adapter? SWR / React Query?
117
217
  const history = useThreadHistory(threadId, client, clearCallbackRef, submittingRef);
118
218
  const getMessages = useMemo(() => {
119
- if (withMessages == null)
120
- return undefined;
121
- return (value) => Array.isArray(value[withMessages])
122
- ? value[withMessages]
219
+ return (value) => Array.isArray(value[messagesKey])
220
+ ? value[messagesKey]
123
221
  : [];
124
- }, [withMessages]);
125
- const [sequence, pathMap] = (() => {
126
- const childrenMap = {};
127
- // First pass - collect nodes for each checkpoint
128
- history.data.forEach((state) => {
129
- const checkpointId = state.parent_checkpoint?.checkpoint_id ?? "$";
130
- childrenMap[checkpointId] ??= [];
131
- childrenMap[checkpointId].push(state);
132
- });
133
- const rootSequence = { type: "sequence", items: [] };
134
- const queue = [{ id: "$", sequence: rootSequence, path: [] }];
135
- const paths = [];
136
- const visited = new Set();
137
- while (queue.length > 0) {
138
- const task = queue.shift();
139
- if (visited.has(task.id))
140
- continue;
141
- visited.add(task.id);
142
- const children = childrenMap[task.id];
143
- if (children == null || children.length === 0)
144
- continue;
145
- // If we've encountered a fork (2+ children), push the fork
146
- // to the sequence and add a new sequence for each child
147
- let fork;
148
- if (children.length > 1) {
149
- fork = { type: "fork", items: [] };
150
- task.sequence.items.push(fork);
151
- }
152
- for (const value of children) {
153
- const id = value.checkpoint.checkpoint_id;
154
- let sequence = task.sequence;
155
- let path = task.path;
156
- if (fork != null) {
157
- sequence = { type: "sequence", items: [] };
158
- fork.items.unshift(sequence);
159
- path = path.slice();
160
- path.push(id);
161
- paths.push(path);
162
- }
163
- sequence.items.push({ type: "node", value, path });
164
- queue.push({ id, sequence, path });
165
- }
166
- }
167
- // Third pass, create a map for available forks
168
- const pathMap = {};
169
- for (const path of paths) {
170
- const parent = path.at(-2) ?? "$";
171
- pathMap[parent] ??= [];
172
- pathMap[parent].unshift(path);
173
- }
174
- return [rootSequence, pathMap];
175
- })();
176
- const [flatValues, flatPaths] = (() => {
177
- const result = [];
178
- const flatPaths = {};
179
- const forkStack = branchPath.slice();
180
- const queue = [...sequence.items];
181
- while (queue.length > 0) {
182
- const item = queue.shift();
183
- if (item.type === "node") {
184
- result.push(item.value);
185
- flatPaths[item.value.checkpoint.checkpoint_id] = {
186
- current: item.path,
187
- branches: item.path.length > 0 ? pathMap[item.path.at(-2) ?? "$"] ?? [] : [],
188
- };
189
- }
190
- if (item.type === "fork") {
191
- const forkId = forkStack.shift();
192
- const index = forkId != null
193
- ? item.items.findIndex((value) => {
194
- const firstItem = value.items.at(0);
195
- if (!firstItem || firstItem.type !== "node")
196
- return false;
197
- return firstItem.value.checkpoint.checkpoint_id === forkId;
198
- })
199
- : -1;
200
- const nextItems = item.items.at(index)?.items ?? [];
201
- queue.push(...nextItems);
202
- }
203
- }
204
- return [result, flatPaths];
205
- })();
206
- const threadHead = flatValues.at(-1);
222
+ }, [messagesKey]);
223
+ const { rootSequence, paths } = getBranchSequence(history.data);
224
+ const { history: flatHistory, branchByCheckpoint } = getBranchView(rootSequence, paths, branch);
225
+ const threadHead = flatHistory.at(-1);
207
226
  const historyValues = threadHead?.values ?? {};
208
227
  const historyError = (() => {
209
228
  const error = threadHead?.tasks?.at(-1)?.error;
@@ -222,8 +241,6 @@ export function useStream(options) {
222
241
  return error;
223
242
  })();
224
243
  const messageMetadata = (() => {
225
- if (getMessages == null)
226
- return undefined;
227
244
  const alreadyShown = new Set();
228
245
  return getMessages(historyValues).map((message, idx) => {
229
246
  const messageId = message.id ?? idx;
@@ -232,12 +249,12 @@ export function useStream(options) {
232
249
  .includes(messageId));
233
250
  const firstSeen = history.data[firstSeenIdx];
234
251
  let branch = firstSeen
235
- ? flatPaths[firstSeen.checkpoint.checkpoint_id]
252
+ ? branchByCheckpoint[firstSeen.checkpoint.checkpoint_id]
236
253
  : undefined;
237
- if (!branch?.current?.length)
254
+ if (!branch?.branch?.length)
238
255
  branch = undefined;
239
256
  // serialize branches
240
- const optionsShown = branch?.branches?.flat(2).join(",");
257
+ const optionsShown = branch?.branchOptions?.flat(2).join(",");
241
258
  if (optionsShown) {
242
259
  if (alreadyShown.has(optionsShown))
243
260
  branch = undefined;
@@ -246,8 +263,8 @@ export function useStream(options) {
246
263
  return {
247
264
  messageId: messageId.toString(),
248
265
  firstSeenState: firstSeen,
249
- branch: branch?.current?.join(">"),
250
- branchOptions: branch?.branches?.map((b) => b.join(">")),
266
+ branch: branch?.branch,
267
+ branchOptions: branch?.branchOptions,
251
268
  };
252
269
  });
253
270
  })();
@@ -265,7 +282,7 @@ export function useStream(options) {
265
282
  let usableThreadId = threadId;
266
283
  if (!usableThreadId) {
267
284
  const thread = await client.threads.create();
268
- options?.onThreadId?.(thread.thread_id);
285
+ onThreadId(thread.thread_id);
269
286
  usableThreadId = thread.thread_id;
270
287
  }
271
288
  const streamMode = unique([
@@ -293,10 +310,10 @@ export function useStream(options) {
293
310
  }));
294
311
  // Unbranch things
295
312
  const newPath = submitOptions?.checkpoint?.checkpoint_id
296
- ? flatPaths[submitOptions?.checkpoint?.checkpoint_id]?.current
313
+ ? branchByCheckpoint[submitOptions?.checkpoint?.checkpoint_id]?.branch
297
314
  : undefined;
298
315
  if (newPath != null)
299
- setBranchPath(newPath ?? []);
316
+ setBranch(newPath ?? "");
300
317
  // Assumption: we're setting the initial value
301
318
  // Used for instant feedback
302
319
  setStreamValues(() => {
@@ -313,26 +330,19 @@ export function useStream(options) {
313
330
  });
314
331
  let streamError;
315
332
  for await (const { event, data } of run) {
316
- setEvents((events) => [...events, { event, data }]);
317
333
  if (event === "error") {
318
334
  streamError = new StreamError(data);
319
335
  break;
320
336
  }
321
- if (event === "updates") {
337
+ if (event === "updates")
322
338
  options.onUpdateEvent?.(data);
323
- }
324
- if (event === "custom") {
339
+ if (event === "custom")
325
340
  options.onCustomEvent?.(data);
326
- }
327
- if (event === "metadata") {
341
+ if (event === "metadata")
328
342
  options.onMetadataEvent?.(data);
329
- }
330
- if (event === "values") {
343
+ if (event === "values")
331
344
  setStreamValues(data);
332
- }
333
345
  if (event === "messages") {
334
- if (!getMessages)
335
- continue;
336
346
  const [serialized] = data;
337
347
  const messageId = messageManagerRef.current.add(serialized);
338
348
  if (!messageId) {
@@ -347,13 +357,12 @@ export function useStream(options) {
347
357
  if (!chunk || index == null)
348
358
  return values;
349
359
  messages[index] = toMessageDict(chunk);
350
- return { ...values, [withMessages]: messages };
360
+ return { ...values, [messagesKey]: messages };
351
361
  });
352
362
  }
353
363
  }
354
364
  // TODO: stream created checkpoints to avoid an unnecessary network request
355
365
  const result = await history.mutate(usableThreadId);
356
- // TODO: write tests verifying that stream values are properly handled lifecycle-wise
357
366
  setStreamValues(null);
358
367
  if (streamError != null)
359
368
  throw streamError;
@@ -378,7 +387,6 @@ export function useStream(options) {
378
387
  };
379
388
  const error = isLoading ? streamError : historyError;
380
389
  const values = streamValues ?? historyValues;
381
- const setBranch = useCallback((path) => setBranchPath(path.split(">")), [setBranchPath]);
382
390
  return {
383
391
  get values() {
384
392
  trackStreamMode("values");
@@ -388,19 +396,16 @@ export function useStream(options) {
388
396
  isLoading,
389
397
  stop,
390
398
  submit,
399
+ branch,
391
400
  setBranch,
401
+ history: flatHistory,
402
+ experimental_branchTree: rootSequence,
392
403
  get messages() {
393
404
  trackStreamMode("messages-tuple");
394
- if (getMessages == null) {
395
- throw new Error("No messages key provided. Make sure that `useStream` contains the `messagesKey` property.");
396
- }
397
405
  return getMessages(values);
398
406
  },
399
407
  getMessagesMetadata(message, index) {
400
408
  trackStreamMode("messages-tuple");
401
- if (getMessages == null) {
402
- throw new Error("No messages key provided. Make sure that `useStream` contains the `messagesKey` property.");
403
- }
404
409
  return messageMetadata?.find((m) => m.messageId === (message.id ?? index));
405
410
  },
406
411
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@langchain/langgraph-sdk",
3
- "version": "0.0.40",
3
+ "version": "0.0.41",
4
4
  "description": "Client library for interacting with the LangGraph API",
5
5
  "type": "module",
6
6
  "packageManager": "yarn@1.22.19",
@@ -10,7 +10,8 @@
10
10
  "prepublish": "yarn run build",
11
11
  "format": "prettier --write src",
12
12
  "lint": "prettier --check src && tsc --noEmit",
13
- "test": "NODE_OPTIONS=--experimental-vm-modules jest --testPathIgnorePatterns=\\.int\\.test.ts"
13
+ "test": "NODE_OPTIONS=--experimental-vm-modules jest --testPathIgnorePatterns=\\.int\\.test.ts",
14
+ "typedoc": "typedoc && typedoc src/react/index.ts --out docs/react --options typedoc.react.json"
14
15
  },
15
16
  "main": "index.js",
16
17
  "license": "MIT",
@@ -33,8 +34,8 @@
33
34
  "jest": "^29.7.0",
34
35
  "prettier": "^3.2.5",
35
36
  "ts-jest": "^29.1.2",
36
- "typedoc": "^0.26.1",
37
- "typedoc-plugin-markdown": "^4.1.0",
37
+ "typedoc": "^0.27.7",
38
+ "typedoc-plugin-markdown": "^4.4.2",
38
39
  "typescript": "^5.4.5",
39
40
  "react": "^18.3.1"
40
41
  },