@assistant-ui/react-langchain 0.0.15 → 0.0.18

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.
Files changed (64) hide show
  1. package/dist/convertMessages.d.ts +37 -3
  2. package/dist/convertMessages.d.ts.map +1 -1
  3. package/dist/convertMessages.js +57 -11
  4. package/dist/convertMessages.js.map +1 -1
  5. package/dist/findForkCheckpointInHistory.d.ts +26 -0
  6. package/dist/findForkCheckpointInHistory.d.ts.map +1 -0
  7. package/dist/findForkCheckpointInHistory.js +34 -0
  8. package/dist/findForkCheckpointInHistory.js.map +1 -0
  9. package/dist/hooks.d.ts +88 -0
  10. package/dist/hooks.d.ts.map +1 -0
  11. package/dist/hooks.js +99 -0
  12. package/dist/hooks.js.map +1 -0
  13. package/dist/index.d.ts +6 -3
  14. package/dist/index.js +4 -2
  15. package/dist/resolveForkCheckpoint.d.ts +17 -0
  16. package/dist/resolveForkCheckpoint.d.ts.map +1 -0
  17. package/dist/resolveForkCheckpoint.js +26 -0
  18. package/dist/resolveForkCheckpoint.js.map +1 -0
  19. package/dist/runtimeExtras.d.ts +7 -0
  20. package/dist/runtimeExtras.d.ts.map +1 -0
  21. package/dist/runtimeExtras.js +7 -0
  22. package/dist/runtimeExtras.js.map +1 -0
  23. package/dist/streamingTiming.d.ts +16 -0
  24. package/dist/streamingTiming.d.ts.map +1 -0
  25. package/dist/streamingTiming.js +53 -0
  26. package/dist/streamingTiming.js.map +1 -0
  27. package/dist/types.d.ts +105 -5
  28. package/dist/types.d.ts.map +1 -1
  29. package/dist/uiMessages.d.ts +24 -0
  30. package/dist/uiMessages.d.ts.map +1 -0
  31. package/dist/uiMessages.js +68 -0
  32. package/dist/uiMessages.js.map +1 -0
  33. package/dist/useStreamRuntime.d.ts +10 -85
  34. package/dist/useStreamRuntime.d.ts.map +1 -1
  35. package/dist/useStreamRuntime.js +186 -135
  36. package/dist/useStreamRuntime.js.map +1 -1
  37. package/package.json +9 -9
  38. package/src/__tests__/langChainTestUtils.ts +3 -3
  39. package/src/convertMessages.test.ts +160 -3
  40. package/src/convertMessages.ts +104 -11
  41. package/src/findForkCheckpointInHistory.test.ts +263 -0
  42. package/src/findForkCheckpointInHistory.ts +68 -0
  43. package/src/groupUIMessagesByParent.test.ts +61 -0
  44. package/src/hooks.ts +156 -0
  45. package/src/index.ts +18 -3
  46. package/src/resolveForkCheckpoint.test.ts +202 -0
  47. package/src/resolveForkCheckpoint.ts +49 -0
  48. package/src/runtimeExtras.ts +5 -0
  49. package/src/streamingTiming.test.ts +118 -0
  50. package/src/streamingTiming.ts +85 -0
  51. package/src/types.ts +140 -3
  52. package/src/uiMessages.test.ts +190 -0
  53. package/src/uiMessages.ts +91 -0
  54. package/src/useLangChainError.test.tsx +1 -1
  55. package/src/useLangChainInterrupts.test.tsx +48 -0
  56. package/src/useLangChainRespond.test.tsx +49 -0
  57. package/src/useLangChainRespondAll.test.tsx +52 -0
  58. package/src/useLangChainState.test.tsx +1 -1
  59. package/src/useLangChainStream.test.tsx +42 -0
  60. package/src/useLangChainSubagents.test.tsx +48 -0
  61. package/src/useLangChainSubgraphs.test.tsx +48 -0
  62. package/src/useLangChainToolCalls.test.tsx +1 -1
  63. package/src/useStreamRuntime.test.tsx +232 -0
  64. package/src/useStreamRuntime.ts +301 -268
@@ -1,98 +1,40 @@
1
1
  /// <reference types="@assistant-ui/core/store" />
2
2
  "use client";
3
3
 
4
- import { useMemo, useRef, useState } from "react";
5
- import type {
6
- AppendMessage,
7
- AttachmentAdapter,
8
- DictationAdapter,
9
- ExternalStoreSharedOptions,
10
- FeedbackAdapter,
11
- RealtimeVoiceAdapter,
12
- RemoteThreadListAdapter,
13
- SpeechSynthesisAdapter,
14
- ToolExecutionStatus,
4
+ import { useEffect, useMemo, useRef, useState } from "react";
5
+ import type { AppendMessage, ToolExecutionStatus } from "@assistant-ui/core";
6
+ import {
7
+ generateId,
8
+ getExternalStoreMessages,
9
+ pickExternalStoreSharedOptions,
15
10
  } from "@assistant-ui/core";
16
- import { pickExternalStoreSharedOptions } from "@assistant-ui/core";
11
+ import type { ThreadMessage } from "@assistant-ui/core";
17
12
  import {
18
13
  useCloudThreadListAdapter,
19
14
  useExternalStoreRuntime,
20
15
  useExternalMessageConverter,
21
16
  useRemoteThreadListRuntime,
22
17
  } from "@assistant-ui/core/react";
23
- import { useAui, useAuiState } from "@assistant-ui/store";
24
- import type { AssistantCloud } from "assistant-cloud";
18
+ import { useAuiState } from "@assistant-ui/store";
19
+ import { STREAM_CONTROLLER, useChannel, useStream } from "@langchain/react";
20
+ import type { Channel } from "@langchain/react";
21
+ import type {
22
+ LangChainBaseMessage,
23
+ LangChainToolCall,
24
+ UIMessage,
25
+ UseStreamRuntimeOptions,
26
+ } from "./types";
25
27
  import {
26
- useStream,
27
- type UseStreamOptions,
28
- type AssembledToolCall,
29
- } from "@langchain/react";
30
- import type { LangChainBaseMessage, LangChainToolCall } from "./types";
31
- import { convertLangChainBaseMessage, getMessageType } from "./convertMessages";
32
-
33
- const symbolLangChainRuntimeExtras = Symbol("langchain-runtime-extras");
34
-
35
- const EMPTY_TOOL_CALLS: readonly AssembledToolCall[] = [];
36
-
37
- type LangChainRuntimeExtras = {
38
- [symbolLangChainRuntimeExtras]: true;
39
- interrupt: { value?: unknown } | undefined;
40
- interrupts: readonly { value?: unknown }[];
41
- toolCalls: readonly AssembledToolCall[];
42
- error: unknown;
43
- submit: (
44
- values: Record<string, unknown> | null | undefined,
45
- options?: Record<string, unknown>,
46
- ) => Promise<void>;
47
- values: Record<string, unknown>;
48
- messagesKey: string;
49
- };
50
-
51
- const asLangChainRuntimeExtras = (extras: unknown): LangChainRuntimeExtras => {
52
- if (
53
- typeof extras !== "object" ||
54
- extras == null ||
55
- !(symbolLangChainRuntimeExtras in extras)
56
- )
57
- throw new Error(
58
- "This method can only be called when you are using useStreamRuntime",
59
- );
60
- return extras as LangChainRuntimeExtras;
61
- };
28
+ convertLangChainBaseMessage,
29
+ getMessageContent,
30
+ getMessageType,
31
+ } from "./convertMessages";
32
+ import { foldUIUpdates, mergeUIMessages } from "./uiMessages";
33
+ import { langChainExtras } from "./runtimeExtras";
34
+ import { resolveForkCheckpoint } from "./resolveForkCheckpoint";
35
+ import { useLangChainStreamingTiming } from "./streamingTiming";
62
36
 
63
- type LangChainRuntimeExtraOptions = ExternalStoreSharedOptions & {
64
- cloud?: AssistantCloud | undefined;
65
- adapters?:
66
- | {
67
- attachments?: AttachmentAdapter | undefined;
68
- speech?: SpeechSynthesisAdapter | undefined;
69
- dictation?: DictationAdapter | undefined;
70
- voice?: RealtimeVoiceAdapter | undefined;
71
- feedback?: FeedbackAdapter | undefined;
72
- }
73
- | undefined;
74
- /**
75
- * When the user sends a new message while previous tool calls are
76
- * still pending, automatically submit `tool` messages that cancel
77
- * them so the agent's tool-call accounting stays consistent.
78
- * Defaults to `true`.
79
- */
80
- autoCancelPendingToolCalls?: boolean | undefined;
81
- /**
82
- * Routes the Cancel button's click to `useStream().stop()`. On by
83
- * default. Pass `false` to disable the Cancel button.
84
- */
85
- unstable_allowCancellation?: boolean | undefined;
86
- /**
87
- * Custom `RemoteThreadListAdapter`. When provided, replaces the
88
- * cloud-backed thread list adapter.
89
- */
90
- unstable_threadListAdapter?: RemoteThreadListAdapter | undefined;
91
- /** Custom thread-creation hook, forwarded to the cloud adapter. */
92
- create?: (() => Promise<{ externalId: string | undefined }>) | undefined;
93
- /** Custom thread-deletion hook, forwarded to the cloud adapter. */
94
- delete?: ((threadId: string) => Promise<void>) | undefined;
95
- };
37
+ const UI_CUSTOM_CHANNELS: readonly Channel[] = ["custom"];
96
38
 
97
39
  export const runConfigToSubmitOptions = (
98
40
  runConfig: AppendMessage["runConfig"],
@@ -101,6 +43,30 @@ export const runConfigToSubmitOptions = (
101
43
  ? { config: { configurable: runConfig.custom } }
102
44
  : undefined;
103
45
 
46
+ /**
47
+ * Group the graph's accumulated `UIMessage`s by the assistant message they
48
+ * belong to. Non-array state and entries without a parent link are dropped.
49
+ * The parent id comes from `metadata.message_id` (Python SDK) or
50
+ * `metadata.id` (JS SDK).
51
+ */
52
+ export const groupUIMessagesByParent = (
53
+ value: unknown,
54
+ ): Map<string, UIMessage[]> => {
55
+ const map = new Map<string, UIMessage[]>();
56
+ if (!Array.isArray(value)) return map;
57
+ for (const ui of value as UIMessage[]) {
58
+ const parentId = ui.metadata?.message_id ?? ui.metadata?.id;
59
+ if (!parentId) continue;
60
+ const existing = map.get(parentId);
61
+ if (existing) {
62
+ existing.push(ui);
63
+ } else {
64
+ map.set(parentId, [ui]);
65
+ }
66
+ }
67
+ return map;
68
+ };
69
+
104
70
  const getPendingToolCalls = (
105
71
  messages: readonly LangChainBaseMessage[],
106
72
  ): LangChainToolCall[] => {
@@ -116,65 +82,29 @@ const getPendingToolCalls = (
116
82
  return [...pending.values()];
117
83
  };
118
84
 
119
- // Distribute the intersection through the union arms of `UseStreamOptions`
120
- // (`AgentServerOptions | CustomAdapterOptions`). Writing `UseStreamOptions & X`
121
- // directly collapses arm tracking, so `Omit<…, "cloud">` and the like would
122
- // produce a flattened structural type that no longer matches either arm.
123
- export type UseStreamRuntimeOptions = UseStreamOptions extends infer O
124
- ? O extends UseStreamOptions
125
- ? O & LangChainRuntimeExtraOptions
126
- : never
127
- : never;
128
-
129
- const getMessageContent = (msg: AppendMessage) => {
130
- const allContent = [
131
- ...msg.content,
132
- ...(msg.attachments?.flatMap((a) => a.content) ?? []),
133
- ];
134
-
135
- const hasNonText = allContent.some(
136
- (part) => part.type === "file" || part.type === "image",
137
- );
138
- const hasText = allContent.some((part) => part.type === "text");
139
- if (hasNonText && !hasText) {
140
- allContent.unshift({ type: "text", text: " " });
141
- }
142
-
143
- const content = allContent.map((part) => {
144
- const type = part.type;
145
- switch (type) {
146
- case "text":
147
- return { type: "text" as const, text: part.text };
148
- case "image":
149
- return { type: "image_url" as const, image_url: { url: part.image } };
150
- case "file":
151
- return {
152
- type: "file" as const,
153
- data: part.data,
154
- mime_type: part.mimeType,
155
- metadata: { filename: part.filename ?? "file" },
156
- source_type: "base64" as const,
157
- };
158
- case "tool-call":
159
- throw new Error("Tool call appends are not supported.");
160
- default: {
161
- const _exhaustiveCheck:
162
- | "reasoning"
163
- | "source"
164
- | "audio"
165
- | "data"
166
- | "generative-ui" = type;
167
- throw new Error(
168
- `Unsupported append message part type: ${_exhaustiveCheck}`,
169
- );
170
- }
171
- }
172
- });
173
-
174
- if (content.length === 1 && content[0]?.type === "text") {
175
- return content[0].text ?? "";
85
+ const toStagedHumanMessage = (
86
+ msg: AppendMessage,
87
+ id = generateId(),
88
+ ): LangChainBaseMessage & { id: string } => ({
89
+ id,
90
+ _getType: () => "human",
91
+ content: getMessageContent(msg),
92
+ });
93
+
94
+ const truncateLangChainBaseMessages = (
95
+ threadMessages: readonly ThreadMessage[],
96
+ parentId: string | null,
97
+ ): LangChainBaseMessage[] => {
98
+ if (parentId === null) return [];
99
+ const parentIndex = threadMessages.findIndex((m) => m.id === parentId);
100
+ if (parentIndex === -1) return [];
101
+ const truncated: LangChainBaseMessage[] = [];
102
+ for (let i = 0; i <= parentIndex && i < threadMessages.length; i++) {
103
+ truncated.push(
104
+ ...getExternalStoreMessages<LangChainBaseMessage>(threadMessages[i]!),
105
+ );
176
106
  }
177
- return content;
107
+ return truncated;
178
108
  };
179
109
 
180
110
  type DistributiveOmit<T, K extends keyof any> = T extends unknown
@@ -190,6 +120,7 @@ const useStreamThreadRuntime = (
190
120
  const { adapters, autoCancelPendingToolCalls, unstable_allowCancellation } =
191
121
  options;
192
122
  const messagesKey = options.messagesKey ?? "messages";
123
+ const uiStateKey = options.uiStateKey ?? "ui";
193
124
 
194
125
  const externalId = useAuiState((s) => s.threadListItem.externalId) as
195
126
  | string
@@ -200,6 +131,9 @@ const useStreamThreadRuntime = (
200
131
  options.threadId = externalId;
201
132
 
202
133
  const stream = useStream(options);
134
+ const [stagedMessages, setStagedMessages] = useState<
135
+ LangChainBaseMessage[] | null
136
+ >(null);
203
137
 
204
138
  const [toolStatuses, setToolStatuses] = useState<
205
139
  Record<string, ToolExecutionStatus>
@@ -209,35 +143,143 @@ const useStreamThreadRuntime = (
209
143
  );
210
144
  const effectiveIsRunning = stream.isLoading || hasExecutingTools;
211
145
 
146
+ const uiStateValue = stream.values[uiStateKey];
147
+
148
+ const customEvents = useChannel(stream, UI_CUSTOM_CHANNELS);
149
+ const liveUiMessages = useMemo(
150
+ () => foldUIUpdates(customEvents),
151
+ [customEvents],
152
+ );
153
+
154
+ const mergedUiMessages = useMemo(
155
+ () => mergeUIMessages(liveUiMessages, uiStateValue),
156
+ [liveUiMessages, uiStateValue],
157
+ );
158
+
159
+ const visibleMessages =
160
+ stagedMessages ?? (stream.messages as LangChainBaseMessage[]);
161
+
162
+ const messageTiming = useLangChainStreamingTiming(
163
+ visibleMessages,
164
+ effectiveIsRunning,
165
+ );
166
+
167
+ const convertWithUI = useMemo<
168
+ useExternalMessageConverter.Callback<LangChainBaseMessage>
169
+ >(() => {
170
+ const uiMessagesByParent = groupUIMessagesByParent(mergedUiMessages);
171
+ return (message, metadata) =>
172
+ convertLangChainBaseMessage(message, {
173
+ ...metadata,
174
+ uiMessagesByParent,
175
+ messageTiming,
176
+ });
177
+ }, [mergedUiMessages, messageTiming]);
178
+
212
179
  const threadMessages = useExternalMessageConverter({
213
- callback: convertLangChainBaseMessage,
214
- messages: stream.messages as LangChainBaseMessage[],
180
+ callback: convertWithUI,
181
+ messages: visibleMessages,
215
182
  isRunning: effectiveIsRunning,
216
183
  });
217
184
 
218
185
  const streamRef = useRef(stream);
219
186
  streamRef.current = stream;
220
187
 
188
+ const visibleMessagesRef = useRef(visibleMessages);
189
+ visibleMessagesRef.current = visibleMessages;
190
+
191
+ const threadMessagesRef = useRef(threadMessages);
192
+ threadMessagesRef.current = threadMessages;
193
+
194
+ const stagedMessagesRef = useRef(
195
+ new Map<
196
+ string,
197
+ {
198
+ message: LangChainBaseMessage & { id: string };
199
+ runConfig: AppendMessage["runConfig"];
200
+ }
201
+ >(),
202
+ );
203
+ const stagedBaseMessagesRef = useRef<LangChainBaseMessage[] | null>(null);
204
+
205
+ useEffect(() => {
206
+ if (stagedMessagesRef.current.size === 0) return;
207
+
208
+ // Staged edits must keep their truncated base while stream updates arrive before promotion.
209
+ const baseMessages =
210
+ stagedBaseMessagesRef.current ??
211
+ (stream.messages as LangChainBaseMessage[]);
212
+ const baseMessageIds = new Set(
213
+ baseMessages.flatMap((message) => (message.id ? [message.id] : [])),
214
+ );
215
+ const remainingStagedMessages: LangChainBaseMessage[] = [];
216
+ const seenStagedIds = new Set<string>();
217
+ for (const message of visibleMessagesRef.current) {
218
+ if (!message.id || seenStagedIds.has(message.id)) continue;
219
+ if (baseMessageIds.has(message.id)) continue;
220
+ const staged = stagedMessagesRef.current.get(message.id);
221
+ if (!staged) continue;
222
+ remainingStagedMessages.push(staged.message);
223
+ seenStagedIds.add(message.id);
224
+ }
225
+
226
+ if (remainingStagedMessages.length === 0) {
227
+ stagedBaseMessagesRef.current = null;
228
+ visibleMessagesRef.current = baseMessages;
229
+ setStagedMessages(null);
230
+ return;
231
+ }
232
+
233
+ const nextMessages = [...baseMessages, ...remainingStagedMessages];
234
+ visibleMessagesRef.current = nextMessages;
235
+ setStagedMessages(nextMessages);
236
+ }, [stream.messages]);
237
+
238
+ const getStagedRun = (parentId: string | null) => {
239
+ if (!parentId || !stagedMessagesRef.current.has(parentId)) return null;
240
+
241
+ const staged: LangChainBaseMessage[] = [];
242
+ for (const message of visibleMessagesRef.current) {
243
+ if (message.id && stagedMessagesRef.current.has(message.id)) {
244
+ staged.push(stagedMessagesRef.current.get(message.id)!.message);
245
+ }
246
+ if (message.id === parentId) break;
247
+ }
248
+
249
+ return {
250
+ messages: staged,
251
+ runConfig: stagedMessagesRef.current.get(parentId)!.runConfig,
252
+ };
253
+ };
254
+
255
+ const stageUserMessage = (msg: AppendMessage) => {
256
+ const stagedMessage = toStagedHumanMessage(msg);
257
+ stagedMessagesRef.current.set(stagedMessage.id, {
258
+ message: stagedMessage,
259
+ runConfig: msg.runConfig,
260
+ });
261
+ const nextMessages = [...visibleMessagesRef.current, stagedMessage];
262
+ visibleMessagesRef.current = nextMessages;
263
+ setStagedMessages(nextMessages);
264
+ };
265
+
221
266
  const extras = useMemo(
222
- (): LangChainRuntimeExtras => ({
223
- [symbolLangChainRuntimeExtras]: true,
224
- interrupt: stream.interrupt,
225
- interrupts: stream.interrupts,
226
- toolCalls: stream.toolCalls,
227
- error: stream.error,
228
- submit: stream.submit,
229
- values: stream.values,
230
- messagesKey,
231
- }),
232
- [
233
- stream.interrupt,
234
- stream.interrupts,
235
- stream.toolCalls,
236
- stream.error,
237
- stream.submit,
238
- stream.values,
239
- messagesKey,
240
- ],
267
+ () =>
268
+ langChainExtras.provide({
269
+ interrupt: stream.interrupt,
270
+ interrupts: stream.interrupts,
271
+ toolCalls: stream.toolCalls,
272
+ subagents: stream.subagents,
273
+ subgraphs: stream.subgraphs,
274
+ stream,
275
+ error: stream.error,
276
+ submit: stream.submit,
277
+ respond: stream.respond,
278
+ respondAll: stream.respondAll,
279
+ values: stream.values,
280
+ messagesKey,
281
+ }),
282
+ [stream, messagesKey],
241
283
  );
242
284
 
243
285
  const runtime = useExternalStoreRuntime({
@@ -250,6 +292,11 @@ const useStreamThreadRuntime = (
250
292
  unstable_enableToolInvocations: true,
251
293
  setToolStatuses,
252
294
  onNew: async (msg) => {
295
+ if (!(msg.startRun ?? msg.role === "user")) {
296
+ stageUserMessage(msg);
297
+ return;
298
+ }
299
+
253
300
  const content = getMessageContent(msg);
254
301
  const cancellations =
255
302
  autoCancelPendingToolCalls !== false
@@ -288,6 +335,96 @@ const useStreamThreadRuntime = (
288
335
  ],
289
336
  });
290
337
  },
338
+ onReload: async (parentId, config) => {
339
+ const stagedRun = getStagedRun(parentId);
340
+ if (stagedRun) {
341
+ const promotedIds = new Set<string>();
342
+ for (const message of stagedRun.messages) {
343
+ if (!message.id) continue;
344
+ promotedIds.add(message.id);
345
+ stagedMessagesRef.current.delete(message.id);
346
+ }
347
+ stagedBaseMessagesRef.current = null;
348
+ if (stagedMessagesRef.current.size > 0) {
349
+ const nextMessages = visibleMessagesRef.current.filter(
350
+ (message) => !message.id || !promotedIds.has(message.id),
351
+ );
352
+ visibleMessagesRef.current = nextMessages;
353
+ setStagedMessages(nextMessages);
354
+ } else {
355
+ setStagedMessages(null);
356
+ }
357
+ await stream.submit(
358
+ {
359
+ [messagesKey]: stagedRun.messages.map((message) => ({
360
+ id: message.id,
361
+ type: "human",
362
+ content: message.content,
363
+ })),
364
+ },
365
+ runConfigToSubmitOptions(config.runConfig ?? stagedRun.runConfig),
366
+ );
367
+ return;
368
+ }
369
+
370
+ const threadId = externalId;
371
+ if (!threadId || parentId == null) return;
372
+ const s = streamRef.current;
373
+ const checkpointId = await resolveForkCheckpoint(
374
+ s.client,
375
+ threadId,
376
+ s.messages as readonly LangChainBaseMessage[],
377
+ parentId,
378
+ config.sourceId,
379
+ s[STREAM_CONTROLLER]?.messageMetadataStore?.getSnapshot?.(),
380
+ messagesKey,
381
+ );
382
+ if (!checkpointId) return;
383
+ await s.submit(null, {
384
+ forkFrom: checkpointId,
385
+ ...runConfigToSubmitOptions(config.runConfig),
386
+ });
387
+ },
388
+ onEdit: async (message) => {
389
+ if (!(message.startRun ?? message.role === "user")) {
390
+ const truncated = truncateLangChainBaseMessages(
391
+ threadMessagesRef.current,
392
+ message.parentId,
393
+ );
394
+ const stagedMessage = toStagedHumanMessage(message);
395
+ stagedMessagesRef.current.set(stagedMessage.id, {
396
+ message: stagedMessage,
397
+ runConfig: message.runConfig,
398
+ });
399
+ stagedBaseMessagesRef.current = truncated;
400
+ const nextMessages = [...truncated, stagedMessage];
401
+ visibleMessagesRef.current = nextMessages;
402
+ setStagedMessages(nextMessages);
403
+ return;
404
+ }
405
+
406
+ const threadId = externalId;
407
+ if (!threadId) return;
408
+ const s = streamRef.current;
409
+ const checkpointId = await resolveForkCheckpoint(
410
+ s.client,
411
+ threadId,
412
+ s.messages as readonly LangChainBaseMessage[],
413
+ message.parentId,
414
+ message.sourceId,
415
+ s[STREAM_CONTROLLER]?.messageMetadataStore?.getSnapshot?.(),
416
+ messagesKey,
417
+ );
418
+ if (!checkpointId) return;
419
+ const content = getMessageContent(message);
420
+ await s.submit(
421
+ { [messagesKey]: [{ type: "human", content }] },
422
+ {
423
+ forkFrom: checkpointId,
424
+ ...runConfigToSubmitOptions(message.runConfig),
425
+ },
426
+ );
427
+ },
291
428
  onCancel:
292
429
  unstable_allowCancellation !== false
293
430
  ? async () => {
@@ -329,6 +466,7 @@ export const useStreamRuntime = (rawOptions: UseStreamRuntimeOptions) => {
329
466
  unstable_threadListAdapter,
330
467
  create,
331
468
  delete: deleteFn,
469
+ onThreadIdChange,
332
470
  ...options
333
471
  } = rawOptions;
334
472
 
@@ -348,111 +486,6 @@ export const useStreamRuntime = (rawOptions: UseStreamRuntimeOptions) => {
348
486
  },
349
487
  adapter,
350
488
  allowNesting: true,
489
+ onThreadIdChange,
351
490
  });
352
491
  };
353
-
354
- /**
355
- * Read the current LangGraph interrupt state from the runtime extras.
356
- */
357
- export const useLangChainInterruptState = () => {
358
- return useAuiState((s) => {
359
- const extras = s.thread.extras;
360
- if (!extras) return undefined;
361
- return asLangChainRuntimeExtras(extras).interrupt;
362
- });
363
- };
364
-
365
- /** Read the last run/hydration error from the runtime extras. */
366
- export const useLangChainError = () => {
367
- return useAuiState((s) => {
368
- const extras = s.thread.extras;
369
- if (!extras) return undefined;
370
- return asLangChainRuntimeExtras(extras).error;
371
- });
372
- };
373
-
374
- /**
375
- * Read the root tool calls assembled by `useStream` from the `tools`
376
- * channel. Defaults to an empty array, so consumers can `.map` without
377
- * a guard. Useful for rendering pending/streamed tool calls and
378
- * approval UIs.
379
- */
380
- export const useLangChainToolCalls = () => {
381
- return useAuiState((s) => {
382
- const extras = s.thread.extras;
383
- if (!extras) return EMPTY_TOOL_CALLS;
384
- return asLangChainRuntimeExtras(extras).toolCalls ?? EMPTY_TOOL_CALLS;
385
- });
386
- };
387
-
388
- /**
389
- * Returns a function to submit raw state updates to the LangGraph agent,
390
- * bypassing the normal message flow. Useful for sending interrupt resume
391
- * commands.
392
- */
393
- export const useLangChainSubmit = () => {
394
- const aui = useAui();
395
- return (
396
- values: Record<string, unknown> | null | undefined,
397
- options?: Record<string, unknown>,
398
- ) => {
399
- const extras = aui.thread().getState().extras;
400
- const { submit } = asLangChainRuntimeExtras(extras);
401
- return submit(values, options);
402
- };
403
- };
404
-
405
- /**
406
- * Submit a list of LangChain-shaped messages on the current thread.
407
- * Parity helper for migrating from `useLangGraphSend`. Routes to
408
- * `useStream().submit({ [messagesKey]: messages }, options)`.
409
- */
410
- export const useLangChainSend = () => {
411
- const aui = useAui();
412
- return (
413
- messages: readonly LangChainBaseMessage[],
414
- options?: Record<string, unknown>,
415
- ) => {
416
- const { submit, messagesKey } = asLangChainRuntimeExtras(
417
- aui.thread().getState().extras,
418
- );
419
- return submit({ [messagesKey]: messages }, options);
420
- };
421
- };
422
-
423
- /**
424
- * Submit a `useStream` command (e.g. interrupt resume). Parity helper
425
- * for migrating from `useLangGraphSendCommand`. Note that v1's command
426
- * shape (`{ resume?, goto?, update? }`) differs from the legacy
427
- * `{ resume: string }` form — to carry a payload, use the input or
428
- * `stream.respond` instead.
429
- */
430
- export const useLangChainSendCommand = () => {
431
- const submit = useLangChainSubmit();
432
- return (command: Record<string, unknown>) => submit(null, { command });
433
- };
434
-
435
- /**
436
- * Read a custom LangGraph state key from the current thread. Mirrors
437
- * `useStream().values[key]` from `@langchain/react` and updates when the
438
- * stream emits new state.
439
- *
440
- * @example
441
- * ```tsx
442
- * const todos = useLangChainState<Todo[]>("todos");
443
- * const files = useLangChainState<Record<string, string>>("files", {});
444
- * ```
445
- */
446
- export function useLangChainState<T>(key: string): T | undefined;
447
- export function useLangChainState<T>(key: string, defaultValue: T): T;
448
- export function useLangChainState<T>(
449
- key: string,
450
- defaultValue?: T,
451
- ): T | undefined {
452
- return useAuiState((s) => {
453
- const extras = s.thread.extras;
454
- if (!extras) return defaultValue;
455
- const value = asLangChainRuntimeExtras(extras).values[key] as T | undefined;
456
- return value !== undefined ? value : defaultValue;
457
- });
458
- }