@langchain/langgraph-sdk 0.0.39 → 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
  };
@@ -1,12 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SSEDecoder = exports.BytesLineDecoder = void 0;
4
- const mergeArrays = (a, b) => {
5
- const mergedArray = new Uint8Array(a.length + b.length);
6
- mergedArray.set(a);
7
- mergedArray.set(b, a.length);
8
- return mergedArray;
9
- };
10
4
  const CR = "\r".charCodeAt(0);
11
5
  const LF = "\n".charCodeAt(0);
12
6
  const NULL = "\0".charCodeAt(0);
@@ -15,11 +9,11 @@ const SPACE = " ".charCodeAt(0);
15
9
  const TRAILING_NEWLINE = [CR, LF];
16
10
  class BytesLineDecoder extends TransformStream {
17
11
  constructor() {
18
- let buffer = new Uint8Array();
12
+ let buffer = [];
19
13
  let trailingCr = false;
20
14
  super({
21
15
  start() {
22
- buffer = new Uint8Array();
16
+ buffer = [];
23
17
  trailingCr = false;
24
18
  },
25
19
  transform(chunk, controller) {
@@ -27,7 +21,7 @@ class BytesLineDecoder extends TransformStream {
27
21
  let text = chunk;
28
22
  // Handle trailing CR from previous chunk
29
23
  if (trailingCr) {
30
- text = mergeArrays([CR], text);
24
+ text = joinArrays([[CR], text]);
31
25
  trailingCr = false;
32
26
  }
33
27
  // Check for trailing CR in current chunk
@@ -38,40 +32,39 @@ class BytesLineDecoder extends TransformStream {
38
32
  if (!text.length)
39
33
  return;
40
34
  const trailingNewline = TRAILING_NEWLINE.includes(text.at(-1));
41
- // Pre-allocate lines array with estimated capacity
42
- let lines = [];
43
- for (let offset = 0; offset < text.byteLength;) {
44
- let idx = text.indexOf(CR, offset);
45
- if (idx === -1)
46
- idx = text.indexOf(LF, offset);
47
- if (idx === -1) {
48
- lines.push(text.subarray(offset));
49
- break;
35
+ const lastIdx = text.length - 1;
36
+ const { lines } = text.reduce((acc, cur, idx) => {
37
+ if (acc.from > idx)
38
+ return acc;
39
+ if (cur === CR || cur === LF) {
40
+ acc.lines.push(text.subarray(acc.from, idx));
41
+ if (cur === CR && text[idx + 1] === LF) {
42
+ acc.from = idx + 2;
43
+ }
44
+ else {
45
+ acc.from = idx + 1;
46
+ }
50
47
  }
51
- lines.push(text.subarray(offset, idx));
52
- if (text[idx] === CR && text[idx + 1] === LF) {
53
- offset = idx + 2;
48
+ if (idx === lastIdx && acc.from <= lastIdx) {
49
+ acc.lines.push(text.subarray(acc.from));
54
50
  }
55
- else {
56
- offset = idx + 1;
57
- }
58
- }
51
+ return acc;
52
+ }, { lines: [], from: 0 });
59
53
  if (lines.length === 1 && !trailingNewline) {
60
- buffer = mergeArrays(buffer, lines[0]);
54
+ buffer.push(lines[0]);
61
55
  return;
62
56
  }
63
57
  if (buffer.length) {
64
58
  // Include existing buffer in first line
65
- buffer = mergeArrays(buffer, lines[0]);
66
- lines = lines.slice(1);
67
- lines.unshift(buffer);
68
- buffer = new Uint8Array();
59
+ buffer.push(lines[0]);
60
+ lines[0] = joinArrays(buffer);
61
+ buffer = [];
69
62
  }
70
63
  if (!trailingNewline) {
71
64
  // If the last segment is not newline terminated,
72
65
  // buffer it for the next chunk
73
66
  if (lines.length)
74
- buffer = lines.pop();
67
+ buffer = [lines.pop()];
75
68
  }
76
69
  // Enqueue complete lines
77
70
  for (const line of lines) {
@@ -80,7 +73,7 @@ class BytesLineDecoder extends TransformStream {
80
73
  },
81
74
  flush(controller) {
82
75
  if (buffer.length) {
83
- controller.enqueue(buffer);
76
+ controller.enqueue(joinArrays(buffer));
84
77
  }
85
78
  },
86
79
  });
@@ -90,7 +83,7 @@ exports.BytesLineDecoder = BytesLineDecoder;
90
83
  class SSEDecoder extends TransformStream {
91
84
  constructor() {
92
85
  let event = "";
93
- let data = new Uint8Array();
86
+ let data = [];
94
87
  let lastEventId = "";
95
88
  let retry = null;
96
89
  const decoder = new TextDecoder();
@@ -102,11 +95,11 @@ class SSEDecoder extends TransformStream {
102
95
  return;
103
96
  const sse = {
104
97
  event,
105
- data: data.length ? JSON.parse(decoder.decode(data)) : null,
98
+ data: data.length ? decodeArraysToJson(decoder, data) : null,
106
99
  };
107
100
  // NOTE: as per the SSE spec, do not reset lastEventId
108
101
  event = "";
109
- data = new Uint8Array();
102
+ data = [];
110
103
  retry = null;
111
104
  controller.enqueue(sse);
112
105
  return;
@@ -125,7 +118,7 @@ class SSEDecoder extends TransformStream {
125
118
  event = decoder.decode(value);
126
119
  }
127
120
  else if (fieldName === "data") {
128
- data = mergeArrays(data, value);
121
+ data.push(value);
129
122
  }
130
123
  else if (fieldName === "id") {
131
124
  if (value.indexOf(NULL) === -1)
@@ -141,7 +134,7 @@ class SSEDecoder extends TransformStream {
141
134
  if (event) {
142
135
  controller.enqueue({
143
136
  event,
144
- data: data.length ? JSON.parse(decoder.decode(data)) : null,
137
+ data: data.length ? decodeArraysToJson(decoder, data) : null,
145
138
  });
146
139
  }
147
140
  },
@@ -149,3 +142,16 @@ class SSEDecoder extends TransformStream {
149
142
  }
150
143
  }
151
144
  exports.SSEDecoder = SSEDecoder;
145
+ function joinArrays(data) {
146
+ const totalLength = data.reduce((acc, curr) => acc + curr.length, 0);
147
+ let merged = new Uint8Array(totalLength);
148
+ let offset = 0;
149
+ for (const c of data) {
150
+ merged.set(c, offset);
151
+ offset += c.length;
152
+ }
153
+ return merged;
154
+ }
155
+ function decodeArraysToJson(decoder, data) {
156
+ return JSON.parse(decoder.decode(joinArrays(data)));
157
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@langchain/langgraph-sdk",
3
- "version": "0.0.39",
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
  },