@assistant-ui/react-langchain 0.0.14 → 0.0.17

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 (66) hide show
  1. package/README.md +1 -1
  2. package/dist/convertMessages.d.ts +37 -3
  3. package/dist/convertMessages.d.ts.map +1 -1
  4. package/dist/convertMessages.js +63 -11
  5. package/dist/convertMessages.js.map +1 -1
  6. package/dist/findForkCheckpointInHistory.d.ts +26 -0
  7. package/dist/findForkCheckpointInHistory.d.ts.map +1 -0
  8. package/dist/findForkCheckpointInHistory.js +34 -0
  9. package/dist/findForkCheckpointInHistory.js.map +1 -0
  10. package/dist/hooks.d.ts +88 -0
  11. package/dist/hooks.d.ts.map +1 -0
  12. package/dist/hooks.js +99 -0
  13. package/dist/hooks.js.map +1 -0
  14. package/dist/index.d.ts +6 -3
  15. package/dist/index.js +4 -2
  16. package/dist/resolveForkCheckpoint.d.ts +17 -0
  17. package/dist/resolveForkCheckpoint.d.ts.map +1 -0
  18. package/dist/resolveForkCheckpoint.js +26 -0
  19. package/dist/resolveForkCheckpoint.js.map +1 -0
  20. package/dist/runtimeExtras.d.ts +7 -0
  21. package/dist/runtimeExtras.d.ts.map +1 -0
  22. package/dist/runtimeExtras.js +7 -0
  23. package/dist/runtimeExtras.js.map +1 -0
  24. package/dist/streamingTiming.d.ts +16 -0
  25. package/dist/streamingTiming.d.ts.map +1 -0
  26. package/dist/streamingTiming.js +53 -0
  27. package/dist/streamingTiming.js.map +1 -0
  28. package/dist/types.d.ts +113 -5
  29. package/dist/types.d.ts.map +1 -1
  30. package/dist/uiMessages.d.ts +24 -0
  31. package/dist/uiMessages.d.ts.map +1 -0
  32. package/dist/uiMessages.js +68 -0
  33. package/dist/uiMessages.js.map +1 -0
  34. package/dist/useStreamRuntime.d.ts +15 -76
  35. package/dist/useStreamRuntime.d.ts.map +1 -1
  36. package/dist/useStreamRuntime.js +190 -111
  37. package/dist/useStreamRuntime.js.map +1 -1
  38. package/package.json +9 -9
  39. package/src/__tests__/langChainTestUtils.ts +22 -0
  40. package/src/convertMessages.test.ts +210 -0
  41. package/src/convertMessages.ts +111 -11
  42. package/src/findForkCheckpointInHistory.test.ts +263 -0
  43. package/src/findForkCheckpointInHistory.ts +68 -0
  44. package/src/groupUIMessagesByParent.test.ts +61 -0
  45. package/src/hooks.ts +156 -0
  46. package/src/index.ts +20 -3
  47. package/src/resolveForkCheckpoint.test.ts +202 -0
  48. package/src/resolveForkCheckpoint.ts +49 -0
  49. package/src/runConfigToSubmitOptions.test.ts +24 -0
  50. package/src/runtimeExtras.ts +5 -0
  51. package/src/streamingTiming.test.ts +118 -0
  52. package/src/streamingTiming.ts +85 -0
  53. package/src/types.ts +147 -3
  54. package/src/uiMessages.test.ts +190 -0
  55. package/src/uiMessages.ts +91 -0
  56. package/src/useLangChainError.test.tsx +48 -0
  57. package/src/useLangChainInterrupts.test.tsx +48 -0
  58. package/src/useLangChainRespond.test.tsx +49 -0
  59. package/src/useLangChainRespondAll.test.tsx +52 -0
  60. package/src/useLangChainState.test.tsx +14 -26
  61. package/src/useLangChainStream.test.tsx +42 -0
  62. package/src/useLangChainSubagents.test.tsx +48 -0
  63. package/src/useLangChainSubgraphs.test.tsx +48 -0
  64. package/src/useLangChainToolCalls.test.tsx +48 -0
  65. package/src/useStreamRuntime.test.tsx +232 -0
  66. package/src/useStreamRuntime.ts +311 -234
@@ -1,89 +1,70 @@
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";
25
- import { useStream, type UseStreamOptions } from "@langchain/react";
26
- import type { LangChainBaseMessage, LangChainToolCall } from "./types";
27
- import { convertLangChainBaseMessage, getMessageType } from "./convertMessages";
28
-
29
- const symbolLangChainRuntimeExtras = Symbol("langchain-runtime-extras");
30
-
31
- type LangChainRuntimeExtras = {
32
- [symbolLangChainRuntimeExtras]: true;
33
- interrupt: { value?: unknown } | undefined;
34
- interrupts: readonly { value?: unknown }[];
35
- submit: (
36
- values: Record<string, unknown> | null | undefined,
37
- options?: Record<string, unknown>,
38
- ) => Promise<void>;
39
- values: Record<string, unknown>;
40
- messagesKey: string;
41
- };
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";
27
+ import {
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";
42
36
 
43
- const asLangChainRuntimeExtras = (extras: unknown): LangChainRuntimeExtras => {
44
- if (
45
- typeof extras !== "object" ||
46
- extras == null ||
47
- !(symbolLangChainRuntimeExtras in extras)
48
- )
49
- throw new Error(
50
- "This method can only be called when you are using useStreamRuntime",
51
- );
52
- return extras as LangChainRuntimeExtras;
53
- };
37
+ const UI_CUSTOM_CHANNELS: readonly Channel[] = ["custom"];
54
38
 
55
- type LangChainRuntimeExtraOptions = ExternalStoreSharedOptions & {
56
- cloud?: AssistantCloud | undefined;
57
- adapters?:
58
- | {
59
- attachments?: AttachmentAdapter | undefined;
60
- speech?: SpeechSynthesisAdapter | undefined;
61
- dictation?: DictationAdapter | undefined;
62
- voice?: RealtimeVoiceAdapter | undefined;
63
- feedback?: FeedbackAdapter | undefined;
64
- }
65
- | undefined;
66
- /**
67
- * When the user sends a new message while previous tool calls are
68
- * still pending, automatically submit `tool` messages that cancel
69
- * them so the agent's tool-call accounting stays consistent.
70
- * Defaults to `true`.
71
- */
72
- autoCancelPendingToolCalls?: boolean | undefined;
73
- /**
74
- * Routes the Cancel button's click to `useStream().stop()`. On by
75
- * default. Pass `false` to disable the Cancel button.
76
- */
77
- unstable_allowCancellation?: boolean | undefined;
78
- /**
79
- * Custom `RemoteThreadListAdapter`. When provided, replaces the
80
- * cloud-backed thread list adapter.
81
- */
82
- unstable_threadListAdapter?: RemoteThreadListAdapter | undefined;
83
- /** Custom thread-creation hook, forwarded to the cloud adapter. */
84
- create?: (() => Promise<{ externalId: string | undefined }>) | undefined;
85
- /** Custom thread-deletion hook, forwarded to the cloud adapter. */
86
- delete?: ((threadId: string) => Promise<void>) | undefined;
39
+ export const runConfigToSubmitOptions = (
40
+ runConfig: AppendMessage["runConfig"],
41
+ ) =>
42
+ runConfig?.custom
43
+ ? { config: { configurable: runConfig.custom } }
44
+ : undefined;
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;
87
68
  };
88
69
 
89
70
  const getPendingToolCalls = (
@@ -101,65 +82,29 @@ const getPendingToolCalls = (
101
82
  return [...pending.values()];
102
83
  };
103
84
 
104
- // Distribute the intersection through the union arms of `UseStreamOptions`
105
- // (`AgentServerOptions | CustomAdapterOptions`). Writing `UseStreamOptions & X`
106
- // directly collapses arm tracking, so `Omit<…, "cloud">` and the like would
107
- // produce a flattened structural type that no longer matches either arm.
108
- export type UseStreamRuntimeOptions = UseStreamOptions extends infer O
109
- ? O extends UseStreamOptions
110
- ? O & LangChainRuntimeExtraOptions
111
- : never
112
- : never;
113
-
114
- const getMessageContent = (msg: AppendMessage) => {
115
- const allContent = [
116
- ...msg.content,
117
- ...(msg.attachments?.flatMap((a) => a.content) ?? []),
118
- ];
119
-
120
- const hasNonText = allContent.some(
121
- (part) => part.type === "file" || part.type === "image",
122
- );
123
- const hasText = allContent.some((part) => part.type === "text");
124
- if (hasNonText && !hasText) {
125
- allContent.unshift({ type: "text", text: " " });
126
- }
127
-
128
- const content = allContent.map((part) => {
129
- const type = part.type;
130
- switch (type) {
131
- case "text":
132
- return { type: "text" as const, text: part.text };
133
- case "image":
134
- return { type: "image_url" as const, image_url: { url: part.image } };
135
- case "file":
136
- return {
137
- type: "file" as const,
138
- data: part.data,
139
- mime_type: part.mimeType,
140
- metadata: { filename: part.filename ?? "file" },
141
- source_type: "base64" as const,
142
- };
143
- case "tool-call":
144
- throw new Error("Tool call appends are not supported.");
145
- default: {
146
- const _exhaustiveCheck:
147
- | "reasoning"
148
- | "source"
149
- | "audio"
150
- | "data"
151
- | "generative-ui" = type;
152
- throw new Error(
153
- `Unsupported append message part type: ${_exhaustiveCheck}`,
154
- );
155
- }
156
- }
157
- });
85
+ const toStagedHumanMessage = (
86
+ msg: AppendMessage,
87
+ id = generateId(),
88
+ ): LangChainBaseMessage & { id: string } => ({
89
+ id,
90
+ _getType: () => "human",
91
+ content: getMessageContent(msg),
92
+ });
158
93
 
159
- if (content.length === 1 && content[0]?.type === "text") {
160
- return content[0].text ?? "";
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
+ );
161
106
  }
162
- return content;
107
+ return truncated;
163
108
  };
164
109
 
165
110
  type DistributiveOmit<T, K extends keyof any> = T extends unknown
@@ -175,6 +120,7 @@ const useStreamThreadRuntime = (
175
120
  const { adapters, autoCancelPendingToolCalls, unstable_allowCancellation } =
176
121
  options;
177
122
  const messagesKey = options.messagesKey ?? "messages";
123
+ const uiStateKey = options.uiStateKey ?? "ui";
178
124
 
179
125
  const externalId = useAuiState((s) => s.threadListItem.externalId) as
180
126
  | string
@@ -185,6 +131,9 @@ const useStreamThreadRuntime = (
185
131
  options.threadId = externalId;
186
132
 
187
133
  const stream = useStream(options);
134
+ const [stagedMessages, setStagedMessages] = useState<
135
+ LangChainBaseMessage[] | null
136
+ >(null);
188
137
 
189
138
  const [toolStatuses, setToolStatuses] = useState<
190
139
  Record<string, ToolExecutionStatus>
@@ -194,42 +143,160 @@ const useStreamThreadRuntime = (
194
143
  );
195
144
  const effectiveIsRunning = stream.isLoading || hasExecutingTools;
196
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
+
197
179
  const threadMessages = useExternalMessageConverter({
198
- callback: convertLangChainBaseMessage,
199
- messages: stream.messages as LangChainBaseMessage[],
180
+ callback: convertWithUI,
181
+ messages: visibleMessages,
200
182
  isRunning: effectiveIsRunning,
201
183
  });
202
184
 
203
185
  const streamRef = useRef(stream);
204
186
  streamRef.current = stream;
205
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
+
206
266
  const extras = useMemo(
207
- (): LangChainRuntimeExtras => ({
208
- [symbolLangChainRuntimeExtras]: true,
209
- interrupt: stream.interrupt,
210
- interrupts: stream.interrupts,
211
- submit: stream.submit,
212
- values: stream.values,
213
- messagesKey,
214
- }),
215
- [
216
- stream.interrupt,
217
- stream.interrupts,
218
- stream.submit,
219
- stream.values,
220
- messagesKey,
221
- ],
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],
222
283
  );
223
284
 
224
285
  const runtime = useExternalStoreRuntime({
225
286
  ...pickExternalStoreSharedOptions(options),
226
287
  isRunning: effectiveIsRunning,
288
+ isLoading: stream.isThreadLoading,
227
289
  messages: threadMessages,
228
290
  adapters,
229
291
  extras,
230
292
  unstable_enableToolInvocations: true,
231
293
  setToolStatuses,
232
294
  onNew: async (msg) => {
295
+ if (!(msg.startRun ?? msg.role === "user")) {
296
+ stageUserMessage(msg);
297
+ return;
298
+ }
299
+
233
300
  const content = getMessageContent(msg);
234
301
  const cancellations =
235
302
  autoCancelPendingToolCalls !== false
@@ -243,9 +310,10 @@ const useStreamThreadRuntime = (
243
310
  status: "error" as const,
244
311
  }))
245
312
  : [];
246
- await stream.submit({
247
- [messagesKey]: [...cancellations, { type: "human", content }],
248
- });
313
+ await stream.submit(
314
+ { [messagesKey]: [...cancellations, { type: "human", content }] },
315
+ runConfigToSubmitOptions(msg.runConfig),
316
+ );
249
317
  },
250
318
  onAddToolResult: async ({
251
319
  toolCallId,
@@ -267,6 +335,96 @@ const useStreamThreadRuntime = (
267
335
  ],
268
336
  });
269
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
+ },
270
428
  onCancel:
271
429
  unstable_allowCancellation !== false
272
430
  ? async () => {
@@ -308,6 +466,7 @@ export const useStreamRuntime = (rawOptions: UseStreamRuntimeOptions) => {
308
466
  unstable_threadListAdapter,
309
467
  create,
310
468
  delete: deleteFn,
469
+ onThreadIdChange,
311
470
  ...options
312
471
  } = rawOptions;
313
472
 
@@ -327,88 +486,6 @@ export const useStreamRuntime = (rawOptions: UseStreamRuntimeOptions) => {
327
486
  },
328
487
  adapter,
329
488
  allowNesting: true,
489
+ onThreadIdChange,
330
490
  });
331
491
  };
332
-
333
- /**
334
- * Read the current LangGraph interrupt state from the runtime extras.
335
- */
336
- export const useLangChainInterruptState = () => {
337
- return useAuiState((s) => {
338
- const extras = s.thread.extras;
339
- if (!extras) return undefined;
340
- return asLangChainRuntimeExtras(extras).interrupt;
341
- });
342
- };
343
-
344
- /**
345
- * Returns a function to submit raw state updates to the LangGraph agent,
346
- * bypassing the normal message flow. Useful for sending interrupt resume
347
- * commands.
348
- */
349
- export const useLangChainSubmit = () => {
350
- const aui = useAui();
351
- return (
352
- values: Record<string, unknown> | null | undefined,
353
- options?: Record<string, unknown>,
354
- ) => {
355
- const extras = aui.thread().getState().extras;
356
- const { submit } = asLangChainRuntimeExtras(extras);
357
- return submit(values, options);
358
- };
359
- };
360
-
361
- /**
362
- * Submit a list of LangChain-shaped messages on the current thread.
363
- * Parity helper for migrating from `useLangGraphSend`. Routes to
364
- * `useStream().submit({ [messagesKey]: messages }, options)`.
365
- */
366
- export const useLangChainSend = () => {
367
- const aui = useAui();
368
- return (
369
- messages: readonly LangChainBaseMessage[],
370
- options?: Record<string, unknown>,
371
- ) => {
372
- const { submit, messagesKey } = asLangChainRuntimeExtras(
373
- aui.thread().getState().extras,
374
- );
375
- return submit({ [messagesKey]: messages }, options);
376
- };
377
- };
378
-
379
- /**
380
- * Submit a `useStream` command (e.g. interrupt resume). Parity helper
381
- * for migrating from `useLangGraphSendCommand`. Note that v1's command
382
- * shape (`{ resume?, goto?, update? }`) differs from the legacy
383
- * `{ resume: string }` form — to carry a payload, use the input or
384
- * `stream.respond` instead.
385
- */
386
- export const useLangChainSendCommand = () => {
387
- const submit = useLangChainSubmit();
388
- return (command: Record<string, unknown>) => submit(null, { command });
389
- };
390
-
391
- /**
392
- * Read a custom LangGraph state key from the current thread. Mirrors
393
- * `useStream().values[key]` from `@langchain/react` and updates when the
394
- * stream emits new state.
395
- *
396
- * @example
397
- * ```tsx
398
- * const todos = useLangChainState<Todo[]>("todos");
399
- * const files = useLangChainState<Record<string, string>>("files", {});
400
- * ```
401
- */
402
- export function useLangChainState<T>(key: string): T | undefined;
403
- export function useLangChainState<T>(key: string, defaultValue: T): T;
404
- export function useLangChainState<T>(
405
- key: string,
406
- defaultValue?: T,
407
- ): T | undefined {
408
- return useAuiState((s) => {
409
- const extras = s.thread.extras;
410
- if (!extras) return defaultValue;
411
- const value = asLangChainRuntimeExtras(extras).values[key] as T | undefined;
412
- return value !== undefined ? value : defaultValue;
413
- });
414
- }