@langchain/langgraph-sdk 0.0.112 → 0.1.1

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,7 +15,7 @@ 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) {
@@ -257,39 +79,7 @@ const useControllableThreadId = (options) => {
257
79
  }
258
80
  return [options.threadId ?? null, onThreadId];
259
81
  };
260
- function useStreamValuesState() {
261
- const [values, setValues] = useState(null);
262
- const setStreamValues = useCallback((values, kind = "stream") => {
263
- if (typeof values === "function") {
264
- setValues((prevTuple) => {
265
- const [prevValues, prevKind] = prevTuple ?? [null, "stream"];
266
- const next = values(prevValues, prevKind);
267
- if (next == null)
268
- return null;
269
- return [next, kind];
270
- });
271
- return;
272
- }
273
- if (values == null)
274
- setValues(null);
275
- setValues([values, kind]);
276
- }, []);
277
- const mutate = useCallback((kind, serverValues) => (update) => {
278
- setStreamValues((clientValues) => {
279
- const prev = { ...serverValues, ...clientValues };
280
- const next = typeof update === "function" ? update(prev) : update;
281
- return { ...prev, ...next };
282
- }, kind);
283
- }, [setStreamValues]);
284
- return [values?.[0] ?? null, setStreamValues, mutate];
285
- }
286
82
  export function useStream(options) {
287
- const matchEventType = (expected, actual, _data) => {
288
- return expected === actual || actual.startsWith(`${expected}|`);
289
- };
290
- let { messagesKey } = options;
291
- const { assistantId, fetchStateHistory } = options;
292
- const { onCreated, onError, onFinish } = options;
293
83
  const reconnectOnMountRef = useRef(options.reconnectOnMount);
294
84
  const runMetadataStorage = useMemo(() => {
295
85
  if (typeof window === "undefined")
@@ -301,7 +91,6 @@ export function useStream(options) {
301
91
  return storage();
302
92
  return null;
303
93
  }, []);
304
- messagesKey ??= "messages";
305
94
  const client = useMemo(() => options.client ??
306
95
  new Client({
307
96
  apiUrl: options.apiUrl,
@@ -315,20 +104,16 @@ export function useStream(options) {
315
104
  options.callerOptions,
316
105
  options.defaultHeaders,
317
106
  ]);
107
+ const [messageManager] = useState(() => new MessageTupleManager());
108
+ const [stream] = useState(() => new StreamManager(messageManager));
109
+ useSyncExternalStore(stream.subscribe, stream.getSnapshot, stream.getSnapshot);
318
110
  const [threadId, onThreadId] = useControllableThreadId(options);
319
- const [branch, setBranch] = useState("");
320
- const [isLoading, setIsLoading] = useState(false);
321
- const [streamError, setStreamError] = useState(undefined);
322
- const [streamValues, setStreamValues, getMutateFn] = useStreamValuesState();
323
- const messageManagerRef = useRef(new MessageTupleManager());
324
- const submittingRef = useRef(false);
325
- const abortRef = useRef(null);
326
111
  const trackStreamModeRef = useRef([]);
327
112
  const trackStreamMode = useCallback((...mode) => {
113
+ const ref = trackStreamModeRef.current;
328
114
  for (const m of mode) {
329
- if (!trackStreamModeRef.current.includes(m)) {
330
- trackStreamModeRef.current.push(m);
331
- }
115
+ if (!ref.includes(m))
116
+ ref.push(m);
332
117
  }
333
118
  }, []);
334
119
  const hasUpdateListener = options.onUpdateEvent != null;
@@ -361,35 +146,37 @@ export function useStream(options) {
361
146
  hasTaskListener,
362
147
  ]);
363
148
  const clearCallbackRef = useRef(null);
364
- clearCallbackRef.current = () => {
365
- setStreamError(undefined);
366
- setStreamValues(null);
367
- messageManagerRef.current.clear();
368
- };
149
+ clearCallbackRef.current = stream.clear;
150
+ const submittingRef = useRef(false);
151
+ submittingRef.current = stream.isLoading;
369
152
  const onErrorRef = useRef(undefined);
370
153
  onErrorRef.current = options.onError;
371
- const historyLimit = typeof fetchStateHistory === "object" && fetchStateHistory != null
372
- ? fetchStateHistory.limit ?? true
373
- : fetchStateHistory ?? true;
154
+ const historyLimit = typeof options.fetchStateHistory === "object" &&
155
+ options.fetchStateHistory != null
156
+ ? options.fetchStateHistory.limit ?? false
157
+ : options.fetchStateHistory ?? false;
374
158
  const history = useThreadHistory(threadId, client, historyLimit, clearCallbackRef, submittingRef, onErrorRef);
375
- const getMessages = useMemo(() => {
376
- return (value) => Array.isArray(value[messagesKey])
377
- ? value[messagesKey]
378
- : [];
379
- }, [messagesKey]);
380
- const { rootSequence, paths } = getBranchSequence(history.data ?? []);
381
- const { history: flatHistory, branchByCheckpoint } = getBranchView(rootSequence, paths, branch);
382
- const threadHead = flatHistory.at(-1);
383
- const historyValues = threadHead?.values ?? options.initialValues ?? {};
384
- const historyValueError = (() => {
385
- 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;
386
174
  if (error == null)
387
175
  return undefined;
388
176
  try {
389
177
  const parsed = JSON.parse(error);
390
- if (StreamError.isStructuredError(parsed)) {
178
+ if (StreamError.isStructuredError(parsed))
391
179
  return new StreamError(parsed);
392
- }
393
180
  return parsed;
394
181
  }
395
182
  catch {
@@ -401,16 +188,13 @@ export function useStream(options) {
401
188
  const alreadyShown = new Set();
402
189
  return getMessages(historyValues).map((message, idx) => {
403
190
  const messageId = message.id ?? idx;
404
- const streamMetadata = message.id != null
405
- ? messageManagerRef.current.get(message.id)?.metadata ?? undefined
406
- : undefined;
407
- 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)
408
193
  .map((m, idx) => m.id ?? idx)
409
194
  .includes(messageId));
410
- const firstSeen = history.data?.[firstSeenIdx];
411
- const checkpointId = firstSeen?.checkpoint?.checkpoint_id;
412
- let branch = firstSeen && checkpointId != null
413
- ? branchByCheckpoint[checkpointId]
195
+ const checkpointId = firstSeenState?.checkpoint?.checkpoint_id;
196
+ let branch = checkpointId != null
197
+ ? branchContext.branchByCheckpoint[checkpointId]
414
198
  : undefined;
415
199
  if (!branch?.branch?.length)
416
200
  branch = undefined;
@@ -423,158 +207,38 @@ export function useStream(options) {
423
207
  }
424
208
  return {
425
209
  messageId: messageId.toString(),
426
- firstSeenState: firstSeen,
210
+ firstSeenState,
427
211
  branch: branch?.branch,
428
212
  branchOptions: branch?.branchOptions,
429
- streamMetadata,
430
213
  };
431
214
  });
432
215
  })();
433
- const stop = () => {
434
- if (abortRef.current != null)
435
- abortRef.current.abort();
436
- abortRef.current = null;
437
- if (runMetadataStorage && threadId) {
438
- const runId = runMetadataStorage.getItem(`lg:stream:${threadId}`);
439
- if (runId)
440
- void client.runs.cancel(threadId, runId);
441
- runMetadataStorage.removeItem(`lg:stream:${threadId}`);
442
- }
443
- options?.onStop?.({ mutate: getMutateFn("stop", historyValues) });
444
- };
445
- async function consumeStream(action) {
446
- let getCallbackMeta;
447
- try {
448
- setIsLoading(true);
449
- setStreamError(undefined);
450
- submittingRef.current = true;
451
- abortRef.current = new AbortController();
452
- const run = await action(abortRef.current.signal);
453
- getCallbackMeta = run.getCallbackMeta;
454
- let streamError;
455
- for await (const { event, data } of run.stream) {
456
- if (event === "error") {
457
- streamError = new StreamError(data);
458
- break;
459
- }
460
- const namespace = event.includes("|")
461
- ? event.split("|").slice(1)
462
- : undefined;
463
- const mutate = getMutateFn("stream", historyValues);
464
- if (event === "metadata")
465
- options.onMetadataEvent?.(data);
466
- if (event === "events")
467
- options.onLangChainEvent?.(data);
468
- if (matchEventType("updates", event, data)) {
469
- options.onUpdateEvent?.(data, { namespace, mutate });
470
- }
471
- if (matchEventType("custom", event, data)) {
472
- options.onCustomEvent?.(data, { namespace, mutate });
473
- }
474
- if (matchEventType("checkpoints", event, data)) {
475
- options.onCheckpointEvent?.(data, { namespace });
476
- }
477
- if (matchEventType("tasks", event, data)) {
478
- options.onTaskEvent?.(data, { namespace });
479
- }
480
- if (matchEventType("debug", event, data)) {
481
- options.onDebugEvent?.(data, { namespace });
482
- }
483
- if (event === "values") {
484
- // don't update values on interrupt values event
485
- if ("__interrupt__" in data)
486
- continue;
487
- setStreamValues(data);
488
- }
489
- // Consume subgraph messages as well
490
- if (matchEventType("messages", event, data)) {
491
- const [serialized, metadata] = data;
492
- const messageId = messageManagerRef.current.add(serialized, metadata);
493
- if (!messageId) {
494
- console.warn("Failed to add message to manager, no message ID found");
495
- continue;
496
- }
497
- setStreamValues((streamValues) => {
498
- const values = { ...historyValues, ...streamValues };
499
- // Assumption: we're concatenating the message
500
- const messages = getMessages(values).slice();
501
- const { chunk, index } = messageManagerRef.current.get(messageId, messages.length) ?? {};
502
- if (!chunk || index == null)
503
- return values;
504
- messages[index] = toMessageDict(chunk);
505
- return { ...values, [messagesKey]: messages };
506
- });
507
- }
508
- }
509
- // TODO: stream created checkpoints to avoid an unnecessary network request
510
- const result = await run.onSuccess();
511
- setStreamValues((values, kind) => {
512
- // Do not clear out the user values set on `stop`.
513
- if (kind === "stop")
514
- return values;
515
- return null;
516
- });
517
- if (streamError != null)
518
- throw streamError;
519
- const lastHead = result.at(0);
520
- if (lastHead)
521
- onFinish?.(lastHead, getCallbackMeta?.());
522
- }
523
- catch (error) {
524
- if (!(error instanceof Error && // eslint-disable-line no-instanceof/no-instanceof
525
- (error.name === "AbortError" || error.name === "TimeoutError"))) {
526
- console.error(error);
527
- setStreamError(error);
528
- 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
+ };
529
232
  }
530
- }
531
- finally {
532
- setIsLoading(false);
533
- submittingRef.current = false;
534
- abortRef.current = null;
535
- }
536
- }
537
- const joinStream = async (runId, lastEventId, options) => {
538
- // eslint-disable-next-line no-param-reassign
539
- lastEventId ??= "-1";
540
- if (!threadId)
541
- return;
542
- await consumeStream(async (signal) => {
543
- const stream = client.runs.joinStream(threadId, runId, {
544
- signal,
545
- lastEventId,
546
- streamMode: options?.streamMode,
547
- });
548
- return {
549
- onSuccess: () => {
550
- runMetadataStorage?.removeItem(`lg:stream:${threadId}`);
551
- return history.mutate(threadId);
552
- },
553
- stream,
554
- getCallbackMeta: () => ({ thread_id: threadId, run_id: runId }),
555
- };
233
+ return { ...historyValues };
556
234
  });
557
- };
558
- const submit = async (values, submitOptions) => {
559
- await consumeStream(async (signal) => {
560
- // Unbranch things
561
- const newPath = submitOptions?.checkpoint?.checkpoint_id
562
- ? branchByCheckpoint[submitOptions?.checkpoint?.checkpoint_id]?.branch
563
- : undefined;
564
- if (newPath != null)
565
- setBranch(newPath ?? "");
566
- setStreamValues(() => {
567
- if (submitOptions?.optimisticValues != null) {
568
- return {
569
- ...historyValues,
570
- ...(typeof submitOptions.optimisticValues === "function"
571
- ? submitOptions.optimisticValues(historyValues)
572
- : submitOptions.optimisticValues),
573
- };
574
- }
575
- return { ...historyValues };
576
- });
577
- 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) => {
578
242
  if (!usableThreadId) {
579
243
  const thread = await client.threads.create({
580
244
  threadId: submitOptions?.threadId,
@@ -583,26 +247,28 @@ export function useStream(options) {
583
247
  onThreadId(thread.thread_id);
584
248
  usableThreadId = thread.thread_id;
585
249
  }
586
- if (!usableThreadId)
250
+ if (!usableThreadId) {
587
251
  throw new Error("Failed to obtain valid thread ID.");
252
+ }
588
253
  const streamMode = unique([
589
254
  ...(submitOptions?.streamMode ?? []),
590
255
  ...trackStreamModeRef.current,
591
256
  ...callbackStreamMode,
592
257
  ]);
593
- let checkpoint = submitOptions?.checkpoint ?? threadHead?.checkpoint ?? undefined;
258
+ let checkpoint = submitOptions?.checkpoint ??
259
+ (includeImplicitBranch
260
+ ? branchContext.threadHead?.checkpoint
261
+ : undefined) ??
262
+ undefined;
594
263
  // Avoid specifying a checkpoint if user explicitly set it to null
595
- if (submitOptions?.checkpoint === null) {
264
+ if (submitOptions?.checkpoint === null)
596
265
  checkpoint = undefined;
597
- }
598
266
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
599
267
  // @ts-expect-error
600
268
  if (checkpoint != null)
601
269
  delete checkpoint.thread_id;
602
- let rejoinKey;
603
- let callbackMeta;
604
270
  const streamResumable = submitOptions?.streamResumable ?? !!runMetadataStorage;
605
- const stream = client.runs.stream(usableThreadId, assistantId, {
271
+ return client.runs.stream(usableThreadId, options.assistantId, {
606
272
  input: values,
607
273
  config: submitOptions?.config,
608
274
  context: submitOptions?.context,
@@ -626,25 +292,77 @@ export function useStream(options) {
626
292
  thread_id: params.thread_id ?? usableThreadId,
627
293
  };
628
294
  if (runMetadataStorage) {
629
- rejoinKey = `lg:stream:${callbackMeta.thread_id}`;
295
+ rejoinKey = `lg:stream:${usableThreadId}`;
630
296
  runMetadataStorage.setItem(rejoinKey, callbackMeta.run_id);
631
297
  }
632
- onCreated?.(callbackMeta);
298
+ options.onCreated?.(callbackMeta);
633
299
  },
634
300
  });
635
- return {
636
- stream,
637
- getCallbackMeta: () => callbackMeta,
638
- onSuccess: () => {
639
- if (rejoinKey)
640
- runMetadataStorage?.removeItem(rejoinKey);
641
- return history.mutate(usableThreadId);
642
- },
643
- };
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
+ },
644
362
  });
645
363
  };
646
364
  const reconnectKey = useMemo(() => {
647
- if (!runMetadataStorage || isLoading)
365
+ if (!runMetadataStorage || stream.isLoading)
648
366
  return undefined;
649
367
  if (typeof window === "undefined")
650
368
  return undefined;
@@ -652,7 +370,7 @@ export function useStream(options) {
652
370
  if (!runId)
653
371
  return undefined;
654
372
  return { runId, threadId };
655
- }, [runMetadataStorage, isLoading, threadId]);
373
+ }, [runMetadataStorage, stream.isLoading, threadId]);
656
374
  const shouldReconnect = !!runMetadataStorage;
657
375
  const reconnectRef = useRef({ threadId, shouldReconnect });
658
376
  const joinStreamRef = useRef(joinStream);
@@ -669,38 +387,55 @@ export function useStream(options) {
669
387
  void joinStreamRef.current?.(reconnectKey.runId);
670
388
  }
671
389
  }, [reconnectKey]);
672
- const error = streamError ?? historyValueError ?? history.error;
673
- const values = streamValues ?? historyValues;
390
+ // --- END TRANSPORT ---
391
+ const error = stream.error ?? historyError ?? history.error;
392
+ const values = stream.values ?? historyValues;
674
393
  return {
675
394
  get values() {
676
395
  trackStreamMode("values");
677
396
  return values;
678
397
  },
679
398
  client,
680
- assistantId,
399
+ assistantId: options.assistantId,
681
400
  error,
682
- isLoading,
401
+ isLoading: stream.isLoading,
683
402
  stop,
684
- submit, // eslint-disable-line @typescript-eslint/no-misused-promises
403
+ submit,
685
404
  joinStream,
686
405
  branch,
687
406
  setBranch,
688
- 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
+ },
689
413
  isThreadLoading: history.isLoading && history.data == null,
690
414
  get experimental_branchTree() {
691
415
  if (historyLimit === false) {
692
- 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`");
693
417
  }
694
- return rootSequence;
418
+ return branchContext.branchTree;
695
419
  },
696
420
  get interrupt() {
697
- // Don't show the interrupt if the stream is loading
698
- if (isLoading)
421
+ if (values != null &&
422
+ "__interrupt__" in values &&
423
+ Array.isArray(values.__interrupt__)) {
424
+ const valueInterrupts = values.__interrupt__;
425
+ if (valueInterrupts.length === 0)
426
+ return { when: "breakpoint" };
427
+ if (valueInterrupts.length === 1)
428
+ return valueInterrupts[0];
429
+ // TODO: fix the typing of interrupts if multiple interrupts are returned
430
+ return valueInterrupts;
431
+ }
432
+ // If we're deferring to old interrupt detection logic, don't show the interrupt if the stream is loading
433
+ if (stream.isLoading)
699
434
  return undefined;
700
- const interrupts = threadHead?.tasks?.at(-1)?.interrupts;
435
+ const interrupts = branchContext.threadHead?.tasks?.at(-1)?.interrupts;
701
436
  if (interrupts == null || interrupts.length === 0) {
702
437
  // check if there's a next task present
703
- const next = threadHead?.next ?? [];
438
+ const next = branchContext.threadHead?.next ?? [];
704
439
  if (!next.length || error != null)
705
440
  return undefined;
706
441
  return { when: "breakpoint" };
@@ -714,7 +449,15 @@ export function useStream(options) {
714
449
  },
715
450
  getMessagesMetadata(message, index) {
716
451
  trackStreamMode("values");
717
- return messageMetadata?.find((m) => m.messageId === (message.id ?? index));
452
+ const streamMetadata = messageManager.get(message.id)?.metadata;
453
+ const historyMetadata = messageMetadata?.find((m) => m.messageId === (message.id ?? index));
454
+ if (streamMetadata != null || historyMetadata != null) {
455
+ return {
456
+ ...historyMetadata,
457
+ streamMetadata,
458
+ };
459
+ }
460
+ return undefined;
718
461
  },
719
462
  };
720
463
  }