@langchain/langgraph-sdk 0.0.111 → 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.
@@ -1,190 +1,12 @@
1
1
  /* __LC_ALLOW_ENTRYPOINT_SIDE_EFFECTS__ */
2
2
  "use client";
3
- import { useCallback, useEffect, useMemo, useRef, useState, } from "react";
4
- import { coerceMessageLikeToMessage, convertToChunk, isBaseMessageChunk, } from "@langchain/core/messages";
3
+ import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore, } from "react";
4
+ import { findLast, unique } from "./utils.js";
5
+ import { StreamError } from "./errors.js";
6
+ import { getBranchContext } from "./branching.js";
7
+ import { StreamManager } from "./manager.js";
5
8
  import { Client, getClientConfigHash } from "../client.js";
6
- class StreamError extends Error {
7
- constructor(data) {
8
- super(data.message);
9
- this.name = data.name ?? data.error ?? "StreamError";
10
- }
11
- static isStructuredError(error) {
12
- return typeof error === "object" && error != null && "message" in error;
13
- }
14
- }
15
- function tryConvertToChunk(message) {
16
- try {
17
- return convertToChunk(message);
18
- }
19
- catch {
20
- return null;
21
- }
22
- }
23
- class MessageTupleManager {
24
- constructor() {
25
- Object.defineProperty(this, "chunks", {
26
- enumerable: true,
27
- configurable: true,
28
- writable: true,
29
- value: {}
30
- });
31
- this.chunks = {};
32
- }
33
- add(serialized, metadata) {
34
- // TODO: this is sometimes sent from the API
35
- // figure out how to prevent this or move this to LC.js
36
- if (serialized.type.endsWith("MessageChunk")) {
37
- // eslint-disable-next-line no-param-reassign
38
- serialized.type = serialized.type
39
- .slice(0, -"MessageChunk".length)
40
- .toLowerCase();
41
- }
42
- const message = coerceMessageLikeToMessage(serialized);
43
- const chunk = tryConvertToChunk(message);
44
- const { id } = chunk ?? message;
45
- if (!id) {
46
- console.warn("No message ID found for chunk, ignoring in state", serialized);
47
- return null;
48
- }
49
- this.chunks[id] ??= {};
50
- this.chunks[id].metadata = metadata ?? this.chunks[id].metadata;
51
- if (chunk) {
52
- const prev = this.chunks[id].chunk;
53
- this.chunks[id].chunk =
54
- (isBaseMessageChunk(prev) ? prev : null)?.concat(chunk) ?? chunk;
55
- }
56
- else {
57
- this.chunks[id].chunk = message;
58
- }
59
- return id;
60
- }
61
- clear() {
62
- this.chunks = {};
63
- }
64
- get(id, defaultIndex) {
65
- if (this.chunks[id] == null)
66
- return null;
67
- if (defaultIndex != null)
68
- this.chunks[id].index ??= defaultIndex;
69
- return this.chunks[id];
70
- }
71
- }
72
- const toMessageDict = (chunk) => {
73
- const { type, data } = chunk.toDict();
74
- return { ...data, type };
75
- };
76
- function unique(array) {
77
- return [...new Set(array)];
78
- }
79
- function findLastIndex(array, predicate) {
80
- for (let i = array.length - 1; i >= 0; i -= 1) {
81
- if (predicate(array[i]))
82
- return i;
83
- }
84
- return -1;
85
- }
86
- function getBranchSequence(history) {
87
- const childrenMap = {};
88
- // Short circuit if there's only a singular one state
89
- // TODO: I think we can make this more generalizable for all `fetchStateHistory` values.
90
- if (history.length <= 1) {
91
- return {
92
- rootSequence: {
93
- type: "sequence",
94
- items: history.map((value) => ({ type: "node", value, path: [] })),
95
- },
96
- paths: [],
97
- };
98
- }
99
- // First pass - collect nodes for each checkpoint
100
- history.forEach((state) => {
101
- const checkpointId = state.parent_checkpoint?.checkpoint_id ?? "$";
102
- childrenMap[checkpointId] ??= [];
103
- childrenMap[checkpointId].push(state);
104
- });
105
- const rootSequence = { type: "sequence", items: [] };
106
- const queue = [{ id: "$", sequence: rootSequence, path: [] }];
107
- const paths = [];
108
- const visited = new Set();
109
- while (queue.length > 0) {
110
- const task = queue.shift();
111
- if (visited.has(task.id))
112
- continue;
113
- visited.add(task.id);
114
- const children = childrenMap[task.id];
115
- if (children == null || children.length === 0)
116
- continue;
117
- // If we've encountered a fork (2+ children), push the fork
118
- // to the sequence and add a new sequence for each child
119
- let fork;
120
- if (children.length > 1) {
121
- fork = { type: "fork", items: [] };
122
- task.sequence.items.push(fork);
123
- }
124
- for (const value of children) {
125
- const id = value.checkpoint?.checkpoint_id;
126
- if (id == null)
127
- continue;
128
- let { sequence } = task;
129
- let { path } = task;
130
- if (fork != null) {
131
- sequence = { type: "sequence", items: [] };
132
- fork.items.unshift(sequence);
133
- path = path.slice();
134
- path.push(id);
135
- paths.push(path);
136
- }
137
- sequence.items.push({ type: "node", value, path });
138
- queue.push({ id, sequence, path });
139
- }
140
- }
141
- return { rootSequence, paths };
142
- }
143
- const PATH_SEP = ">";
144
- const ROOT_ID = "$";
145
- // Get flat view
146
- function getBranchView(sequence, paths, branch) {
147
- const path = branch.split(PATH_SEP);
148
- const pathMap = {};
149
- for (const path of paths) {
150
- const parent = path.at(-2) ?? ROOT_ID;
151
- pathMap[parent] ??= [];
152
- pathMap[parent].unshift(path);
153
- }
154
- const history = [];
155
- const branchByCheckpoint = {};
156
- const forkStack = path.slice();
157
- const queue = [...sequence.items];
158
- while (queue.length > 0) {
159
- const item = queue.shift();
160
- if (item.type === "node") {
161
- history.push(item.value);
162
- const checkpointId = item.value.checkpoint?.checkpoint_id;
163
- if (checkpointId == null)
164
- continue;
165
- branchByCheckpoint[checkpointId] = {
166
- branch: item.path.join(PATH_SEP),
167
- branchOptions: (item.path.length > 0
168
- ? pathMap[item.path.at(-2) ?? ROOT_ID] ?? []
169
- : []).map((p) => p.join(PATH_SEP)),
170
- };
171
- }
172
- if (item.type === "fork") {
173
- const forkId = forkStack.shift();
174
- const index = forkId != null
175
- ? item.items.findIndex((value) => {
176
- const firstItem = value.items.at(0);
177
- if (!firstItem || firstItem.type !== "node")
178
- return false;
179
- return firstItem.value.checkpoint?.checkpoint_id === forkId;
180
- })
181
- : -1;
182
- const nextItems = item.items.at(index)?.items ?? [];
183
- queue.push(...nextItems);
184
- }
185
- }
186
- return { history, branchByCheckpoint };
187
- }
9
+ import { MessageTupleManager } from "./messages.js";
188
10
  function fetchHistory(client, threadId, options) {
189
11
  if (options?.limit === false) {
190
12
  return client.threads.getState(threadId).then((state) => {
@@ -193,12 +15,16 @@ function fetchHistory(client, threadId, options) {
193
15
  return [state];
194
16
  });
195
17
  }
196
- const limit = typeof options?.limit === "number" ? options.limit : 1000;
18
+ const limit = typeof options?.limit === "number" ? options.limit : 10;
197
19
  return client.threads.getHistory(threadId, { limit });
198
20
  }
199
21
  function useThreadHistory(threadId, client, limit, clearCallbackRef, submittingRef, onErrorRef) {
200
22
  const [history, setHistory] = useState(undefined);
201
- const [isLoading, setIsLoading] = useState(false);
23
+ const [isLoading, setIsLoading] = useState(() => {
24
+ if (threadId == null)
25
+ return false;
26
+ return true;
27
+ });
202
28
  const [error, setError] = useState(undefined);
203
29
  const clientHash = getClientConfigHash(client);
204
30
  const clientRef = useRef(client);
@@ -232,7 +58,7 @@ function useThreadHistory(threadId, client, limit, clearCallbackRef, submittingR
232
58
  if (submittingRef.current)
233
59
  return;
234
60
  void fetcher(threadId);
235
- }, [fetcher, clientHash, limit, submittingRef, threadId]);
61
+ }, [fetcher, submittingRef, clientHash, limit, threadId]);
236
62
  return {
237
63
  data: history,
238
64
  isLoading,
@@ -253,39 +79,7 @@ const useControllableThreadId = (options) => {
253
79
  }
254
80
  return [options.threadId ?? null, onThreadId];
255
81
  };
256
- function useStreamValuesState() {
257
- const [values, setValues] = useState(null);
258
- const setStreamValues = useCallback((values, kind = "stream") => {
259
- if (typeof values === "function") {
260
- setValues((prevTuple) => {
261
- const [prevValues, prevKind] = prevTuple ?? [null, "stream"];
262
- const next = values(prevValues, prevKind);
263
- if (next == null)
264
- return null;
265
- return [next, kind];
266
- });
267
- return;
268
- }
269
- if (values == null)
270
- setValues(null);
271
- setValues([values, kind]);
272
- }, []);
273
- const mutate = useCallback((kind, serverValues) => (update) => {
274
- setStreamValues((clientValues) => {
275
- const prev = { ...serverValues, ...clientValues };
276
- const next = typeof update === "function" ? update(prev) : update;
277
- return { ...prev, ...next };
278
- }, kind);
279
- }, [setStreamValues]);
280
- return [values?.[0] ?? null, setStreamValues, mutate];
281
- }
282
82
  export function useStream(options) {
283
- const matchEventType = (expected, actual, _data) => {
284
- return expected === actual || actual.startsWith(`${expected}|`);
285
- };
286
- let { messagesKey } = options;
287
- const { assistantId, fetchStateHistory } = options;
288
- const { onCreated, onError, onFinish } = options;
289
83
  const reconnectOnMountRef = useRef(options.reconnectOnMount);
290
84
  const runMetadataStorage = useMemo(() => {
291
85
  if (typeof window === "undefined")
@@ -297,7 +91,6 @@ export function useStream(options) {
297
91
  return storage();
298
92
  return null;
299
93
  }, []);
300
- messagesKey ??= "messages";
301
94
  const client = useMemo(() => options.client ??
302
95
  new Client({
303
96
  apiUrl: options.apiUrl,
@@ -311,20 +104,16 @@ export function useStream(options) {
311
104
  options.callerOptions,
312
105
  options.defaultHeaders,
313
106
  ]);
107
+ const [messageManager] = useState(() => new MessageTupleManager());
108
+ const [stream] = useState(() => new StreamManager(messageManager));
109
+ useSyncExternalStore(stream.subscribe, stream.getSnapshot, stream.getSnapshot);
314
110
  const [threadId, onThreadId] = useControllableThreadId(options);
315
- const [branch, setBranch] = useState("");
316
- const [isLoading, setIsLoading] = useState(false);
317
- const [streamError, setStreamError] = useState(undefined);
318
- const [streamValues, setStreamValues, getMutateFn] = useStreamValuesState();
319
- const messageManagerRef = useRef(new MessageTupleManager());
320
- const submittingRef = useRef(false);
321
- const abortRef = useRef(null);
322
111
  const trackStreamModeRef = useRef([]);
323
112
  const trackStreamMode = useCallback((...mode) => {
113
+ const ref = trackStreamModeRef.current;
324
114
  for (const m of mode) {
325
- if (!trackStreamModeRef.current.includes(m)) {
326
- trackStreamModeRef.current.push(m);
327
- }
115
+ if (!ref.includes(m))
116
+ ref.push(m);
328
117
  }
329
118
  }, []);
330
119
  const hasUpdateListener = options.onUpdateEvent != null;
@@ -357,35 +146,37 @@ export function useStream(options) {
357
146
  hasTaskListener,
358
147
  ]);
359
148
  const clearCallbackRef = useRef(null);
360
- clearCallbackRef.current = () => {
361
- setStreamError(undefined);
362
- setStreamValues(null);
363
- messageManagerRef.current.clear();
364
- };
149
+ clearCallbackRef.current = stream.clear;
150
+ const submittingRef = useRef(false);
151
+ submittingRef.current = stream.isLoading;
365
152
  const onErrorRef = useRef(undefined);
366
153
  onErrorRef.current = options.onError;
367
- const historyLimit = typeof fetchStateHistory === "object" && fetchStateHistory != null
368
- ? fetchStateHistory.limit ?? true
369
- : fetchStateHistory ?? true;
154
+ const historyLimit = typeof options.fetchStateHistory === "object" &&
155
+ options.fetchStateHistory != null
156
+ ? options.fetchStateHistory.limit ?? false
157
+ : options.fetchStateHistory ?? false;
370
158
  const history = useThreadHistory(threadId, client, historyLimit, clearCallbackRef, submittingRef, onErrorRef);
371
- const getMessages = useMemo(() => {
372
- return (value) => Array.isArray(value[messagesKey])
373
- ? value[messagesKey]
374
- : [];
375
- }, [messagesKey]);
376
- const { rootSequence, paths } = getBranchSequence(history.data ?? []);
377
- const { history: flatHistory, branchByCheckpoint } = getBranchView(rootSequence, paths, branch);
378
- const threadHead = flatHistory.at(-1);
379
- const historyValues = threadHead?.values ?? options.initialValues ?? {};
380
- const historyValueError = (() => {
381
- const error = threadHead?.tasks?.at(-1)?.error;
159
+ const getMessages = (value) => {
160
+ const messagesKey = options.messagesKey ?? "messages";
161
+ return Array.isArray(value[messagesKey]) ? value[messagesKey] : [];
162
+ };
163
+ const setMessages = (current, messages) => {
164
+ const messagesKey = options.messagesKey ?? "messages";
165
+ return { ...current, [messagesKey]: messages };
166
+ };
167
+ const [branch, setBranch] = useState("");
168
+ const branchContext = getBranchContext(branch, history.data);
169
+ const historyValues = branchContext.threadHead?.values ??
170
+ options.initialValues ??
171
+ {};
172
+ const historyError = (() => {
173
+ const error = branchContext.threadHead?.tasks?.at(-1)?.error;
382
174
  if (error == null)
383
175
  return undefined;
384
176
  try {
385
177
  const parsed = JSON.parse(error);
386
- if (StreamError.isStructuredError(parsed)) {
178
+ if (StreamError.isStructuredError(parsed))
387
179
  return new StreamError(parsed);
388
- }
389
180
  return parsed;
390
181
  }
391
182
  catch {
@@ -397,16 +188,13 @@ export function useStream(options) {
397
188
  const alreadyShown = new Set();
398
189
  return getMessages(historyValues).map((message, idx) => {
399
190
  const messageId = message.id ?? idx;
400
- const streamMetadata = message.id != null
401
- ? messageManagerRef.current.get(message.id)?.metadata ?? undefined
402
- : undefined;
403
- const firstSeenIdx = findLastIndex(history.data ?? [], (state) => getMessages(state.values)
191
+ // Find the first checkpoint where the message was seen
192
+ const firstSeenState = findLast(history.data ?? [], (state) => getMessages(state.values)
404
193
  .map((m, idx) => m.id ?? idx)
405
194
  .includes(messageId));
406
- const firstSeen = history.data?.[firstSeenIdx];
407
- const checkpointId = firstSeen?.checkpoint?.checkpoint_id;
408
- let branch = firstSeen && checkpointId != null
409
- ? branchByCheckpoint[checkpointId]
195
+ const checkpointId = firstSeenState?.checkpoint?.checkpoint_id;
196
+ let branch = checkpointId != null
197
+ ? branchContext.branchByCheckpoint[checkpointId]
410
198
  : undefined;
411
199
  if (!branch?.branch?.length)
412
200
  branch = undefined;
@@ -419,158 +207,38 @@ export function useStream(options) {
419
207
  }
420
208
  return {
421
209
  messageId: messageId.toString(),
422
- firstSeenState: firstSeen,
210
+ firstSeenState,
423
211
  branch: branch?.branch,
424
212
  branchOptions: branch?.branchOptions,
425
- streamMetadata,
426
213
  };
427
214
  });
428
215
  })();
429
- const stop = () => {
430
- if (abortRef.current != null)
431
- abortRef.current.abort();
432
- abortRef.current = null;
433
- if (runMetadataStorage && threadId) {
434
- const runId = runMetadataStorage.getItem(`lg:stream:${threadId}`);
435
- if (runId)
436
- void client.runs.cancel(threadId, runId);
437
- runMetadataStorage.removeItem(`lg:stream:${threadId}`);
438
- }
439
- options?.onStop?.({ mutate: getMutateFn("stop", historyValues) });
440
- };
441
- async function consumeStream(action) {
442
- let getCallbackMeta;
443
- try {
444
- setIsLoading(true);
445
- setStreamError(undefined);
446
- submittingRef.current = true;
447
- abortRef.current = new AbortController();
448
- const run = await action(abortRef.current.signal);
449
- getCallbackMeta = run.getCallbackMeta;
450
- let streamError;
451
- for await (const { event, data } of run.stream) {
452
- if (event === "error") {
453
- streamError = new StreamError(data);
454
- break;
455
- }
456
- const namespace = event.includes("|")
457
- ? event.split("|").slice(1)
458
- : undefined;
459
- const mutate = getMutateFn("stream", historyValues);
460
- if (event === "metadata")
461
- options.onMetadataEvent?.(data);
462
- if (event === "events")
463
- options.onLangChainEvent?.(data);
464
- if (matchEventType("updates", event, data)) {
465
- options.onUpdateEvent?.(data, { namespace, mutate });
466
- }
467
- if (matchEventType("custom", event, data)) {
468
- options.onCustomEvent?.(data, { namespace, mutate });
469
- }
470
- if (matchEventType("checkpoints", event, data)) {
471
- options.onCheckpointEvent?.(data, { namespace });
472
- }
473
- if (matchEventType("tasks", event, data)) {
474
- options.onTaskEvent?.(data, { namespace });
475
- }
476
- if (matchEventType("debug", event, data)) {
477
- options.onDebugEvent?.(data, { namespace });
478
- }
479
- if (event === "values") {
480
- // don't update values on interrupt values event
481
- if ("__interrupt__" in data)
482
- continue;
483
- setStreamValues(data);
484
- }
485
- // Consume subgraph messages as well
486
- if (matchEventType("messages", event, data)) {
487
- const [serialized, metadata] = data;
488
- const messageId = messageManagerRef.current.add(serialized, metadata);
489
- if (!messageId) {
490
- console.warn("Failed to add message to manager, no message ID found");
491
- continue;
492
- }
493
- setStreamValues((streamValues) => {
494
- const values = { ...historyValues, ...streamValues };
495
- // Assumption: we're concatenating the message
496
- const messages = getMessages(values).slice();
497
- const { chunk, index } = messageManagerRef.current.get(messageId, messages.length) ?? {};
498
- if (!chunk || index == null)
499
- return values;
500
- messages[index] = toMessageDict(chunk);
501
- return { ...values, [messagesKey]: messages };
502
- });
503
- }
504
- }
505
- // TODO: stream created checkpoints to avoid an unnecessary network request
506
- const result = await run.onSuccess();
507
- setStreamValues((values, kind) => {
508
- // Do not clear out the user values set on `stop`.
509
- if (kind === "stop")
510
- return values;
511
- return null;
512
- });
513
- if (streamError != null)
514
- throw streamError;
515
- const lastHead = result.at(0);
516
- if (lastHead)
517
- onFinish?.(lastHead, getCallbackMeta?.());
518
- }
519
- catch (error) {
520
- if (!(error instanceof Error && // eslint-disable-line no-instanceof/no-instanceof
521
- (error.name === "AbortError" || error.name === "TimeoutError"))) {
522
- console.error(error);
523
- setStreamError(error);
524
- onError?.(error, getCallbackMeta?.());
216
+ const stop = () => stream.stop(historyValues, { onStop: options.onStop });
217
+ // --- TRANSPORT ---
218
+ const submit = async (values, submitOptions) => {
219
+ // Unbranch things
220
+ const checkpointId = submitOptions?.checkpoint?.checkpoint_id;
221
+ setBranch(checkpointId != null
222
+ ? branchContext.branchByCheckpoint[checkpointId]?.branch ?? ""
223
+ : "");
224
+ stream.setStreamValues(() => {
225
+ if (submitOptions?.optimisticValues != null) {
226
+ return {
227
+ ...historyValues,
228
+ ...(typeof submitOptions.optimisticValues === "function"
229
+ ? submitOptions.optimisticValues(historyValues)
230
+ : submitOptions.optimisticValues),
231
+ };
525
232
  }
526
- }
527
- finally {
528
- setIsLoading(false);
529
- submittingRef.current = false;
530
- abortRef.current = null;
531
- }
532
- }
533
- const joinStream = async (runId, lastEventId, options) => {
534
- // eslint-disable-next-line no-param-reassign
535
- lastEventId ??= "-1";
536
- if (!threadId)
537
- return;
538
- await consumeStream(async (signal) => {
539
- const stream = client.runs.joinStream(threadId, runId, {
540
- signal,
541
- lastEventId,
542
- streamMode: options?.streamMode,
543
- });
544
- return {
545
- onSuccess: () => {
546
- runMetadataStorage?.removeItem(`lg:stream:${threadId}`);
547
- return history.mutate(threadId);
548
- },
549
- stream,
550
- getCallbackMeta: () => ({ thread_id: threadId, run_id: runId }),
551
- };
233
+ return { ...historyValues };
552
234
  });
553
- };
554
- const submit = async (values, submitOptions) => {
555
- await consumeStream(async (signal) => {
556
- // Unbranch things
557
- const newPath = submitOptions?.checkpoint?.checkpoint_id
558
- ? branchByCheckpoint[submitOptions?.checkpoint?.checkpoint_id]?.branch
559
- : undefined;
560
- if (newPath != null)
561
- setBranch(newPath ?? "");
562
- setStreamValues(() => {
563
- if (submitOptions?.optimisticValues != null) {
564
- return {
565
- ...historyValues,
566
- ...(typeof submitOptions.optimisticValues === "function"
567
- ? submitOptions.optimisticValues(historyValues)
568
- : submitOptions.optimisticValues),
569
- };
570
- }
571
- return { ...historyValues };
572
- });
573
- let usableThreadId = threadId;
235
+ // When `fetchStateHistory` is requested, thus we assume that branching
236
+ // is enabled. We then need to include the implicit branch.
237
+ const includeImplicitBranch = historyLimit === true || typeof historyLimit === "number";
238
+ let callbackMeta;
239
+ let rejoinKey;
240
+ let usableThreadId = threadId;
241
+ await stream.start(async (signal) => {
574
242
  if (!usableThreadId) {
575
243
  const thread = await client.threads.create({
576
244
  threadId: submitOptions?.threadId,
@@ -579,26 +247,28 @@ export function useStream(options) {
579
247
  onThreadId(thread.thread_id);
580
248
  usableThreadId = thread.thread_id;
581
249
  }
582
- if (!usableThreadId)
250
+ if (!usableThreadId) {
583
251
  throw new Error("Failed to obtain valid thread ID.");
252
+ }
584
253
  const streamMode = unique([
585
254
  ...(submitOptions?.streamMode ?? []),
586
255
  ...trackStreamModeRef.current,
587
256
  ...callbackStreamMode,
588
257
  ]);
589
- let checkpoint = submitOptions?.checkpoint ?? threadHead?.checkpoint ?? undefined;
258
+ let checkpoint = submitOptions?.checkpoint ??
259
+ (includeImplicitBranch
260
+ ? branchContext.threadHead?.checkpoint
261
+ : undefined) ??
262
+ undefined;
590
263
  // Avoid specifying a checkpoint if user explicitly set it to null
591
- if (submitOptions?.checkpoint === null) {
264
+ if (submitOptions?.checkpoint === null)
592
265
  checkpoint = undefined;
593
- }
594
266
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
595
267
  // @ts-expect-error
596
268
  if (checkpoint != null)
597
269
  delete checkpoint.thread_id;
598
- let rejoinKey;
599
- let callbackMeta;
600
270
  const streamResumable = submitOptions?.streamResumable ?? !!runMetadataStorage;
601
- const stream = client.runs.stream(usableThreadId, assistantId, {
271
+ return client.runs.stream(usableThreadId, options.assistantId, {
602
272
  input: values,
603
273
  config: submitOptions?.config,
604
274
  context: submitOptions?.context,
@@ -615,31 +285,84 @@ export function useStream(options) {
615
285
  streamMode,
616
286
  streamSubgraphs: submitOptions?.streamSubgraphs,
617
287
  streamResumable,
288
+ durability: submitOptions?.durability,
618
289
  onRunCreated(params) {
619
290
  callbackMeta = {
620
291
  run_id: params.run_id,
621
292
  thread_id: params.thread_id ?? usableThreadId,
622
293
  };
623
294
  if (runMetadataStorage) {
624
- rejoinKey = `lg:stream:${callbackMeta.thread_id}`;
295
+ rejoinKey = `lg:stream:${usableThreadId}`;
625
296
  runMetadataStorage.setItem(rejoinKey, callbackMeta.run_id);
626
297
  }
627
- onCreated?.(callbackMeta);
298
+ options.onCreated?.(callbackMeta);
628
299
  },
629
300
  });
630
- return {
631
- stream,
632
- getCallbackMeta: () => callbackMeta,
633
- onSuccess: () => {
634
- if (rejoinKey)
635
- runMetadataStorage?.removeItem(rejoinKey);
636
- return history.mutate(usableThreadId);
637
- },
638
- };
301
+ }, {
302
+ getMessages,
303
+ setMessages,
304
+ initialValues: historyValues,
305
+ callbacks: options,
306
+ async onSuccess() {
307
+ if (rejoinKey)
308
+ runMetadataStorage?.removeItem(rejoinKey);
309
+ const shouldRefetch =
310
+ // We're expecting the whole thread state in onFinish
311
+ options.onFinish != null ||
312
+ // We're fetching history, thus we need the latest checkpoint
313
+ // to ensure we're not accidentally submitting to a wrong branch
314
+ includeImplicitBranch;
315
+ if (shouldRefetch) {
316
+ const newHistory = await history.mutate(usableThreadId);
317
+ const lastHead = newHistory.at(0);
318
+ if (lastHead) {
319
+ // We now have the latest update from /history
320
+ // Thus we can clear the local stream state
321
+ options.onFinish?.(lastHead, callbackMeta);
322
+ return null;
323
+ }
324
+ }
325
+ return undefined;
326
+ },
327
+ onError(error) {
328
+ options.onError?.(error, callbackMeta);
329
+ },
330
+ });
331
+ };
332
+ const joinStream = async (runId, lastEventId, joinOptions) => {
333
+ // eslint-disable-next-line no-param-reassign
334
+ lastEventId ??= "-1";
335
+ if (!threadId)
336
+ return;
337
+ const callbackMeta = {
338
+ thread_id: threadId,
339
+ run_id: runId,
340
+ };
341
+ await stream.start(async (signal) => {
342
+ return client.runs.joinStream(threadId, runId, {
343
+ signal,
344
+ lastEventId,
345
+ streamMode: joinOptions?.streamMode,
346
+ });
347
+ }, {
348
+ getMessages,
349
+ setMessages,
350
+ initialValues: historyValues,
351
+ callbacks: options,
352
+ async onSuccess() {
353
+ runMetadataStorage?.removeItem(`lg:stream:${threadId}`);
354
+ const newHistory = await history.mutate(threadId);
355
+ const lastHead = newHistory.at(0);
356
+ if (lastHead)
357
+ options.onFinish?.(lastHead, callbackMeta);
358
+ },
359
+ onError(error) {
360
+ options.onError?.(error, callbackMeta);
361
+ },
639
362
  });
640
363
  };
641
364
  const reconnectKey = useMemo(() => {
642
- if (!runMetadataStorage || isLoading)
365
+ if (!runMetadataStorage || stream.isLoading)
643
366
  return undefined;
644
367
  if (typeof window === "undefined")
645
368
  return undefined;
@@ -647,7 +370,7 @@ export function useStream(options) {
647
370
  if (!runId)
648
371
  return undefined;
649
372
  return { runId, threadId };
650
- }, [runMetadataStorage, isLoading, threadId]);
373
+ }, [runMetadataStorage, stream.isLoading, threadId]);
651
374
  const shouldReconnect = !!runMetadataStorage;
652
375
  const reconnectRef = useRef({ threadId, shouldReconnect });
653
376
  const joinStreamRef = useRef(joinStream);
@@ -664,38 +387,44 @@ export function useStream(options) {
664
387
  void joinStreamRef.current?.(reconnectKey.runId);
665
388
  }
666
389
  }, [reconnectKey]);
667
- const error = streamError ?? historyValueError ?? history.error;
668
- const values = streamValues ?? historyValues;
390
+ // --- END TRANSPORT ---
391
+ const error = stream.error ?? historyError ?? history.error;
392
+ const values = stream.values ?? historyValues;
669
393
  return {
670
394
  get values() {
671
395
  trackStreamMode("values");
672
396
  return values;
673
397
  },
674
398
  client,
675
- assistantId,
399
+ assistantId: options.assistantId,
676
400
  error,
677
- isLoading,
401
+ isLoading: stream.isLoading,
678
402
  stop,
679
- submit, // eslint-disable-line @typescript-eslint/no-misused-promises
403
+ submit,
680
404
  joinStream,
681
405
  branch,
682
406
  setBranch,
683
- history: flatHistory,
407
+ get history() {
408
+ if (historyLimit === false) {
409
+ throw new Error("`fetchStateHistory` must be set to `true` to use `history`");
410
+ }
411
+ return branchContext.flatHistory;
412
+ },
684
413
  isThreadLoading: history.isLoading && history.data == null,
685
414
  get experimental_branchTree() {
686
415
  if (historyLimit === false) {
687
- throw new Error("`experimental_branchTree` is not available when `fetchStateHistory` is set to `false`");
416
+ throw new Error("`fetchStateHistory` must be set to `true` to use `experimental_branchTree`");
688
417
  }
689
- return rootSequence;
418
+ return branchContext.branchTree;
690
419
  },
691
420
  get interrupt() {
692
421
  // Don't show the interrupt if the stream is loading
693
- if (isLoading)
422
+ if (stream.isLoading)
694
423
  return undefined;
695
- const interrupts = threadHead?.tasks?.at(-1)?.interrupts;
424
+ const interrupts = branchContext.threadHead?.tasks?.at(-1)?.interrupts;
696
425
  if (interrupts == null || interrupts.length === 0) {
697
426
  // check if there's a next task present
698
- const next = threadHead?.next ?? [];
427
+ const next = branchContext.threadHead?.next ?? [];
699
428
  if (!next.length || error != null)
700
429
  return undefined;
701
430
  return { when: "breakpoint" };
@@ -709,7 +438,15 @@ export function useStream(options) {
709
438
  },
710
439
  getMessagesMetadata(message, index) {
711
440
  trackStreamMode("values");
712
- return messageMetadata?.find((m) => m.messageId === (message.id ?? index));
441
+ const streamMetadata = messageManager.get(message.id)?.metadata;
442
+ const historyMetadata = messageMetadata?.find((m) => m.messageId === (message.id ?? index));
443
+ if (streamMetadata != null || historyMetadata != null) {
444
+ return {
445
+ ...historyMetadata,
446
+ streamMetadata,
447
+ };
448
+ }
449
+ return undefined;
713
450
  },
714
451
  };
715
452
  }