@assistant-ui/react-langchain 0.0.25 → 0.0.26

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 +1 @@
1
- {"version":3,"file":"useStreamRuntime.d.ts","names":[],"sources":["../src/useStreamRuntime.ts"],"mappings":";;;cAsCa,2BACX,WAAW;;kBAAA;;;;;;;;;cAYA,0BACX,mBACC,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;cAwZF,mBAAoB,YAAY,yDAAuB"}
1
+ {"version":3,"file":"useStreamRuntime.d.ts","names":[],"sources":["../src/useStreamRuntime.ts"],"mappings":";;;cAsCa,2BACX,WAAW;;kBAAA;;;;;;;;;cAgBA,0BACX,mBACC,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;cAmfF,mBAAoB,YAAY,yDAAuB"}
@@ -4,9 +4,9 @@ import { langChainExtras } from "./runtimeExtras.js";
4
4
  import { foldUIUpdates, mergeUIMessages } from "./uiMessages.js";
5
5
  import { resolveForkCheckpoint } from "./resolveForkCheckpoint.js";
6
6
  import { useLangChainStreamingTiming } from "./streamingTiming.js";
7
- import { useAuiState } from "@assistant-ui/store";
7
+ import { useAui, useAuiState } from "@assistant-ui/store";
8
8
  import { useCloudThreadListAdapter, useExternalMessageConverter, useExternalStoreRuntime, useRemoteThreadListRuntime } from "@assistant-ui/core/react";
9
- import { useEffect, useMemo, useRef, useState } from "react";
9
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
10
10
  import { generateId, getExternalStoreMessages, pickExternalStoreSharedOptions } from "@assistant-ui/core";
11
11
  import { STREAM_CONTROLLER, useChannel, useStream } from "@langchain/react";
12
12
  //#region src/useStreamRuntime.ts
@@ -54,6 +54,7 @@ const truncateLangChainBaseMessages = (threadMessages, parentId) => {
54
54
  };
55
55
  const useStreamThreadRuntime = (options) => {
56
56
  const { adapters, autoCancelPendingToolCalls, unstable_allowCancellation } = options;
57
+ const aui = useAui();
57
58
  const messagesKey = options.messagesKey ?? "messages";
58
59
  const uiStateKey = options.uiStateKey ?? "ui";
59
60
  const externalId = useAuiState((s) => s.threadListItem.externalId);
@@ -83,6 +84,41 @@ const useStreamThreadRuntime = (options) => {
83
84
  });
84
85
  const streamRef = useRef(stream);
85
86
  streamRef.current = stream;
87
+ const activeRunConfigRef = useRef(void 0);
88
+ const runConfigByMessageIdRef = useRef(/* @__PURE__ */ new Map());
89
+ const activeThreadIdRef = useRef(externalId);
90
+ const setActiveRunConfig = useCallback((runConfig) => {
91
+ activeRunConfigRef.current = runConfigToSubmitOptions(runConfig)?.config;
92
+ }, []);
93
+ const withActiveRunConfig = useCallback((submitOptions) => {
94
+ if (submitOptions && "config" in submitOptions) return submitOptions;
95
+ if (activeRunConfigRef.current === void 0) return submitOptions;
96
+ return {
97
+ ...submitOptions,
98
+ config: activeRunConfigRef.current
99
+ };
100
+ }, []);
101
+ useEffect(() => {
102
+ if (activeThreadIdRef.current !== null && activeThreadIdRef.current !== externalId) {
103
+ activeRunConfigRef.current = void 0;
104
+ runConfigByMessageIdRef.current.clear();
105
+ }
106
+ activeThreadIdRef.current = externalId;
107
+ }, [externalId]);
108
+ useEffect(() => {
109
+ const messages = stream.messages;
110
+ const owned = runConfigByMessageIdRef.current;
111
+ for (let i = messages.length - 1; i >= 0; i--) {
112
+ const message = messages.at(i);
113
+ if (!message?.id || getMessageType(message) !== "ai" || !message.tool_calls?.length) continue;
114
+ if (owned.has(message.id)) return;
115
+ break;
116
+ }
117
+ for (const message of messages) {
118
+ if (!message.id || getMessageType(message) !== "ai" || !message.tool_calls?.length || owned.has(message.id)) continue;
119
+ owned.set(message.id, activeRunConfigRef.current);
120
+ }
121
+ }, [stream.messages]);
86
122
  const visibleMessagesRef = useRef(visibleMessages);
87
123
  visibleMessagesRef.current = visibleMessages;
88
124
  const threadMessagesRef = useRef(threadMessages);
@@ -143,12 +179,19 @@ const useStreamThreadRuntime = (options) => {
143
179
  subgraphs: stream.subgraphs,
144
180
  stream,
145
181
  error: stream.error,
146
- submit: stream.submit,
147
- respond: stream.respond,
148
- respondAll: stream.respondAll,
182
+ submit: (values, submitOptions) => {
183
+ const isResume = values == null || submitOptions?.command != null;
184
+ return stream.submit(values, isResume ? withActiveRunConfig(submitOptions) : submitOptions);
185
+ },
186
+ respond: (response, respondOptions) => stream.respond(response, withActiveRunConfig(respondOptions)),
187
+ respondAll: (responsesById, respondOptions) => stream.respondAll(responsesById, withActiveRunConfig(respondOptions)),
149
188
  values: stream.values,
150
189
  messagesKey
151
- }), [stream, messagesKey]);
190
+ }), [
191
+ stream,
192
+ messagesKey,
193
+ withActiveRunConfig
194
+ ]);
152
195
  return useExternalStoreRuntime({
153
196
  ...pickExternalStoreSharedOptions(options),
154
197
  isRunning: effectiveIsRunning,
@@ -163,6 +206,7 @@ const useStreamThreadRuntime = (options) => {
163
206
  stageUserMessage(msg);
164
207
  return;
165
208
  }
209
+ setActiveRunConfig(msg.runConfig);
166
210
  const content = getMessageContent(msg);
167
211
  const cancellations = autoCancelPendingToolCalls !== false ? getPendingToolCalls(streamRef.current.messages).map((t) => ({
168
212
  type: "tool",
@@ -171,12 +215,17 @@ const useStreamThreadRuntime = (options) => {
171
215
  content: JSON.stringify({ cancelled: true }),
172
216
  status: "error"
173
217
  })) : [];
174
- await stream.submit({ [messagesKey]: [...cancellations, {
218
+ const { externalId } = await aui.threadListItem.initialize();
219
+ await streamRef.current.submit({ [messagesKey]: [...cancellations, {
175
220
  type: "human",
176
221
  content
177
- }] }, runConfigToSubmitOptions(msg.runConfig));
222
+ }] }, {
223
+ ...runConfigToSubmitOptions(msg.runConfig),
224
+ ...externalId != null ? { threadId: externalId } : {}
225
+ });
178
226
  },
179
- onAddToolResult: async ({ toolCallId, toolName, result, isError, artifact }) => {
227
+ onAddToolResult: async ({ messageId, toolCallId, toolName, result, isError, artifact }) => {
228
+ const runConfig = runConfigByMessageIdRef.current.has(messageId) ? runConfigByMessageIdRef.current.get(messageId) : activeRunConfigRef.current;
180
229
  await stream.submit({ [messagesKey]: [{
181
230
  type: "tool",
182
231
  name: toolName,
@@ -184,7 +233,7 @@ const useStreamThreadRuntime = (options) => {
184
233
  content: JSON.stringify(result),
185
234
  ...artifact !== void 0 && { artifact },
186
235
  status: isError ? "error" : "success"
187
- }] });
236
+ }] }, runConfig === void 0 ? void 0 : { config: runConfig });
188
237
  },
189
238
  onReload: async (parentId, config) => {
190
239
  const stagedRun = getStagedRun(parentId);
@@ -201,11 +250,13 @@ const useStreamThreadRuntime = (options) => {
201
250
  visibleMessagesRef.current = nextMessages;
202
251
  setStagedMessages(nextMessages);
203
252
  } else setStagedMessages(null);
253
+ const runConfig = config.runConfig ?? stagedRun.runConfig;
254
+ setActiveRunConfig(runConfig);
204
255
  await stream.submit({ [messagesKey]: stagedRun.messages.map((message) => ({
205
256
  id: message.id,
206
257
  type: "human",
207
258
  content: message.content
208
- })) }, runConfigToSubmitOptions(config.runConfig ?? stagedRun.runConfig));
259
+ })) }, runConfigToSubmitOptions(runConfig));
209
260
  return;
210
261
  }
211
262
  const threadId = externalId;
@@ -213,6 +264,7 @@ const useStreamThreadRuntime = (options) => {
213
264
  const s = streamRef.current;
214
265
  const checkpointId = await resolveForkCheckpoint(s.client, threadId, s.messages, parentId, config.sourceId, s[STREAM_CONTROLLER]?.messageMetadataStore?.getSnapshot?.(), messagesKey);
215
266
  if (!checkpointId) return;
267
+ setActiveRunConfig(config.runConfig);
216
268
  await s.submit(null, {
217
269
  forkFrom: checkpointId,
218
270
  ...runConfigToSubmitOptions(config.runConfig)
@@ -238,6 +290,7 @@ const useStreamThreadRuntime = (options) => {
238
290
  const checkpointId = await resolveForkCheckpoint(s.client, threadId, s.messages, message.parentId, message.sourceId, s[STREAM_CONTROLLER]?.messageMetadataStore?.getSnapshot?.(), messagesKey);
239
291
  if (!checkpointId) return;
240
292
  const content = getMessageContent(message);
293
+ setActiveRunConfig(message.runConfig);
241
294
  await s.submit({ [messagesKey]: [{
242
295
  type: "human",
243
296
  content
@@ -247,6 +300,7 @@ const useStreamThreadRuntime = (options) => {
247
300
  });
248
301
  },
249
302
  onCancel: unstable_allowCancellation !== false ? async () => {
303
+ activeRunConfigRef.current = void 0;
250
304
  await stream.stop();
251
305
  } : void 0
252
306
  });
@@ -1 +1 @@
1
- {"version":3,"file":"useStreamRuntime.js","names":[],"sources":["../src/useStreamRuntime.ts"],"sourcesContent":["/// <reference types=\"@assistant-ui/core/store\" />\n\"use client\";\n\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport type { AppendMessage, ToolExecutionStatus } from \"@assistant-ui/core\";\nimport {\n generateId,\n getExternalStoreMessages,\n pickExternalStoreSharedOptions,\n} from \"@assistant-ui/core\";\nimport type { ThreadMessage } from \"@assistant-ui/core\";\nimport {\n useCloudThreadListAdapter,\n useExternalStoreRuntime,\n useExternalMessageConverter,\n useRemoteThreadListRuntime,\n} from \"@assistant-ui/core/react\";\nimport { useAuiState } from \"@assistant-ui/store\";\nimport { STREAM_CONTROLLER, useChannel, useStream } from \"@langchain/react\";\nimport type { Channel } from \"@langchain/react\";\nimport type {\n LangChainBaseMessage,\n LangChainToolCall,\n UIMessage,\n UseStreamRuntimeOptions,\n} from \"./types\";\nimport {\n convertLangChainBaseMessage,\n getMessageContent,\n getMessageType,\n} from \"./convertMessages\";\nimport { foldUIUpdates, mergeUIMessages } from \"./uiMessages\";\nimport { langChainExtras } from \"./runtimeExtras\";\nimport { resolveForkCheckpoint } from \"./resolveForkCheckpoint\";\nimport { useLangChainStreamingTiming } from \"./streamingTiming\";\n\nconst UI_CUSTOM_CHANNELS: readonly Channel[] = [\"custom\"];\n\nexport const runConfigToSubmitOptions = (\n runConfig: AppendMessage[\"runConfig\"],\n) =>\n runConfig?.custom\n ? { config: { configurable: runConfig.custom } }\n : undefined;\n\n/**\n * Group the graph's accumulated `UIMessage`s by the assistant message they\n * belong to. Non-array state and entries without a parent link are dropped.\n * The parent id comes from `metadata.message_id` (Python SDK) or\n * `metadata.id` (JS SDK).\n */\nexport const groupUIMessagesByParent = (\n value: unknown,\n): Map<string, UIMessage[]> => {\n const map = new Map<string, UIMessage[]>();\n if (!Array.isArray(value)) return map;\n for (const ui of value as UIMessage[]) {\n const parentId = ui.metadata?.message_id ?? ui.metadata?.id;\n if (!parentId) continue;\n const existing = map.get(parentId);\n if (existing) {\n existing.push(ui);\n } else {\n map.set(parentId, [ui]);\n }\n }\n return map;\n};\n\nconst getPendingToolCalls = (\n messages: readonly LangChainBaseMessage[],\n): LangChainToolCall[] => {\n const pending = new Map<string, LangChainToolCall>();\n for (const m of messages) {\n const type = getMessageType(m);\n if (type === \"ai\") {\n for (const tc of m.tool_calls ?? []) pending.set(tc.id, tc);\n } else if (type === \"tool\" && m.tool_call_id) {\n pending.delete(m.tool_call_id);\n }\n }\n return [...pending.values()];\n};\n\nconst toStagedHumanMessage = (\n msg: AppendMessage,\n id = generateId(),\n): LangChainBaseMessage & { id: string } => ({\n id,\n _getType: () => \"human\",\n content: getMessageContent(msg),\n});\n\nconst truncateLangChainBaseMessages = (\n threadMessages: readonly ThreadMessage[],\n parentId: string | null,\n): LangChainBaseMessage[] => {\n if (parentId === null) return [];\n const parentIndex = threadMessages.findIndex((m) => m.id === parentId);\n if (parentIndex === -1) return [];\n const truncated: LangChainBaseMessage[] = [];\n for (let i = 0; i <= parentIndex && i < threadMessages.length; i++) {\n truncated.push(\n ...getExternalStoreMessages<LangChainBaseMessage>(threadMessages[i]!),\n );\n }\n return truncated;\n};\n\ntype DistributiveOmit<T, K extends keyof any> = T extends unknown\n ? Omit<T, K>\n : never;\n\nconst useStreamThreadRuntime = (\n options: DistributiveOmit<\n UseStreamRuntimeOptions,\n \"cloud\" | \"unstable_threadListAdapter\" | \"create\" | \"delete\"\n >,\n) => {\n const { adapters, autoCancelPendingToolCalls, unstable_allowCancellation } =\n options;\n const messagesKey = options.messagesKey ?? \"messages\";\n const uiStateKey = options.uiStateKey ?? \"ui\";\n\n const externalId = useAuiState((s) => s.threadListItem.externalId) as\n | string\n | null;\n // Object.assign preserves the discriminated transport union; object spread\n // collapses its arms and no longer satisfies UseStreamOptions.\n const stream = useStream(\n Object.assign({}, options, { threadId: externalId }),\n );\n const [stagedMessages, setStagedMessages] = useState<\n LangChainBaseMessage[] | null\n >(null);\n\n const [toolStatuses, setToolStatuses] = useState<\n Record<string, ToolExecutionStatus>\n >({});\n const hasExecutingTools = Object.values(toolStatuses).some(\n (s) => s?.type === \"executing\",\n );\n const effectiveIsRunning = stream.isLoading || hasExecutingTools;\n\n const uiStateValue = stream.values[uiStateKey];\n\n const customEvents = useChannel(stream, UI_CUSTOM_CHANNELS);\n const liveUiMessages = useMemo(\n () => foldUIUpdates(customEvents),\n [customEvents],\n );\n\n const mergedUiMessages = useMemo(\n () => mergeUIMessages(liveUiMessages, uiStateValue),\n [liveUiMessages, uiStateValue],\n );\n\n const visibleMessages =\n stagedMessages ?? (stream.messages as LangChainBaseMessage[]);\n\n const messageTiming = useLangChainStreamingTiming(\n visibleMessages,\n effectiveIsRunning,\n );\n\n const convertWithUI = useMemo<\n useExternalMessageConverter.Callback<LangChainBaseMessage>\n >(() => {\n const uiMessagesByParent = groupUIMessagesByParent(mergedUiMessages);\n return (message, metadata) =>\n convertLangChainBaseMessage(message, {\n ...metadata,\n uiMessagesByParent,\n messageTiming,\n });\n }, [mergedUiMessages, messageTiming]);\n\n const threadMessages = useExternalMessageConverter({\n callback: convertWithUI,\n messages: visibleMessages,\n isRunning: effectiveIsRunning,\n });\n\n const streamRef = useRef(stream);\n streamRef.current = stream;\n\n const visibleMessagesRef = useRef(visibleMessages);\n visibleMessagesRef.current = visibleMessages;\n\n const threadMessagesRef = useRef(threadMessages);\n threadMessagesRef.current = threadMessages;\n\n const stagedMessagesRef = useRef(\n new Map<\n string,\n {\n message: LangChainBaseMessage & { id: string };\n runConfig: AppendMessage[\"runConfig\"];\n }\n >(),\n );\n const stagedBaseMessagesRef = useRef<LangChainBaseMessage[] | null>(null);\n\n useEffect(() => {\n if (stagedMessagesRef.current.size === 0) return;\n\n // Staged edits must keep their truncated base while stream updates arrive before promotion.\n const baseMessages =\n stagedBaseMessagesRef.current ??\n (stream.messages as LangChainBaseMessage[]);\n const baseMessageIds = new Set(\n baseMessages.flatMap((message) => (message.id ? [message.id] : [])),\n );\n const remainingStagedMessages: LangChainBaseMessage[] = [];\n const seenStagedIds = new Set<string>();\n for (const message of visibleMessagesRef.current) {\n if (!message.id || seenStagedIds.has(message.id)) continue;\n if (baseMessageIds.has(message.id)) continue;\n const staged = stagedMessagesRef.current.get(message.id);\n if (!staged) continue;\n remainingStagedMessages.push(staged.message);\n seenStagedIds.add(message.id);\n }\n\n if (remainingStagedMessages.length === 0) {\n stagedBaseMessagesRef.current = null;\n visibleMessagesRef.current = baseMessages;\n setStagedMessages(null);\n return;\n }\n\n const nextMessages = [...baseMessages, ...remainingStagedMessages];\n visibleMessagesRef.current = nextMessages;\n setStagedMessages(nextMessages);\n }, [stream.messages]);\n\n const getStagedRun = (parentId: string | null) => {\n if (!parentId || !stagedMessagesRef.current.has(parentId)) return null;\n\n const staged: LangChainBaseMessage[] = [];\n for (const message of visibleMessagesRef.current) {\n if (message.id && stagedMessagesRef.current.has(message.id)) {\n staged.push(stagedMessagesRef.current.get(message.id)!.message);\n }\n if (message.id === parentId) break;\n }\n\n return {\n messages: staged,\n runConfig: stagedMessagesRef.current.get(parentId)!.runConfig,\n };\n };\n\n const stageUserMessage = (msg: AppendMessage) => {\n const stagedMessage = toStagedHumanMessage(msg);\n stagedMessagesRef.current.set(stagedMessage.id, {\n message: stagedMessage,\n runConfig: msg.runConfig,\n });\n const nextMessages = [...visibleMessagesRef.current, stagedMessage];\n visibleMessagesRef.current = nextMessages;\n setStagedMessages(nextMessages);\n };\n\n const extras = useMemo(\n () =>\n langChainExtras.provide({\n interrupt: stream.interrupt,\n interrupts: stream.interrupts,\n toolCalls: stream.toolCalls,\n subagents: stream.subagents,\n subgraphs: stream.subgraphs,\n stream,\n error: stream.error,\n submit: stream.submit,\n respond: stream.respond,\n respondAll: stream.respondAll,\n values: stream.values,\n messagesKey,\n }),\n [stream, messagesKey],\n );\n\n const runtime = useExternalStoreRuntime({\n ...pickExternalStoreSharedOptions(options),\n isRunning: effectiveIsRunning,\n isLoading: stream.isThreadLoading,\n messages: threadMessages,\n adapters,\n extras,\n unstable_enableToolInvocations: true,\n setToolStatuses,\n onNew: async (msg) => {\n if (!(msg.startRun ?? msg.role === \"user\")) {\n stageUserMessage(msg);\n return;\n }\n\n const content = getMessageContent(msg);\n const cancellations =\n autoCancelPendingToolCalls !== false\n ? getPendingToolCalls(\n streamRef.current.messages as readonly LangChainBaseMessage[],\n ).map((t) => ({\n type: \"tool\" as const,\n name: t.name,\n tool_call_id: t.id,\n content: JSON.stringify({ cancelled: true }),\n status: \"error\" as const,\n }))\n : [];\n await stream.submit(\n { [messagesKey]: [...cancellations, { type: \"human\", content }] },\n runConfigToSubmitOptions(msg.runConfig),\n );\n },\n onAddToolResult: async ({\n toolCallId,\n toolName,\n result,\n isError,\n artifact,\n }) => {\n await stream.submit({\n [messagesKey]: [\n {\n type: \"tool\",\n name: toolName,\n tool_call_id: toolCallId,\n content: JSON.stringify(result),\n ...(artifact !== undefined && { artifact }),\n status: isError ? \"error\" : \"success\",\n },\n ],\n });\n },\n onReload: async (parentId, config) => {\n const stagedRun = getStagedRun(parentId);\n if (stagedRun) {\n const promotedIds = new Set<string>();\n for (const message of stagedRun.messages) {\n if (!message.id) continue;\n promotedIds.add(message.id);\n stagedMessagesRef.current.delete(message.id);\n }\n stagedBaseMessagesRef.current = null;\n if (stagedMessagesRef.current.size > 0) {\n const nextMessages = visibleMessagesRef.current.filter(\n (message) => !message.id || !promotedIds.has(message.id),\n );\n visibleMessagesRef.current = nextMessages;\n setStagedMessages(nextMessages);\n } else {\n setStagedMessages(null);\n }\n await stream.submit(\n {\n [messagesKey]: stagedRun.messages.map((message) => ({\n id: message.id,\n type: \"human\",\n content: message.content,\n })),\n },\n runConfigToSubmitOptions(config.runConfig ?? stagedRun.runConfig),\n );\n return;\n }\n\n const threadId = externalId;\n if (!threadId || parentId == null) return;\n const s = streamRef.current;\n const checkpointId = await resolveForkCheckpoint(\n s.client,\n threadId,\n s.messages as readonly LangChainBaseMessage[],\n parentId,\n config.sourceId,\n s[STREAM_CONTROLLER]?.messageMetadataStore?.getSnapshot?.(),\n messagesKey,\n );\n if (!checkpointId) return;\n await s.submit(null, {\n forkFrom: checkpointId,\n ...runConfigToSubmitOptions(config.runConfig),\n });\n },\n onEdit: async (message) => {\n if (!(message.startRun ?? message.role === \"user\")) {\n const truncated = truncateLangChainBaseMessages(\n threadMessagesRef.current,\n message.parentId,\n );\n const stagedMessage = toStagedHumanMessage(message);\n stagedMessagesRef.current.set(stagedMessage.id, {\n message: stagedMessage,\n runConfig: message.runConfig,\n });\n stagedBaseMessagesRef.current = truncated;\n const nextMessages = [...truncated, stagedMessage];\n visibleMessagesRef.current = nextMessages;\n setStagedMessages(nextMessages);\n return;\n }\n\n const threadId = externalId;\n if (!threadId) return;\n const s = streamRef.current;\n const checkpointId = await resolveForkCheckpoint(\n s.client,\n threadId,\n s.messages as readonly LangChainBaseMessage[],\n message.parentId,\n message.sourceId,\n s[STREAM_CONTROLLER]?.messageMetadataStore?.getSnapshot?.(),\n messagesKey,\n );\n if (!checkpointId) return;\n const content = getMessageContent(message);\n await s.submit(\n { [messagesKey]: [{ type: \"human\", content }] },\n {\n forkFrom: checkpointId,\n ...runConfigToSubmitOptions(message.runConfig),\n },\n );\n },\n onCancel:\n unstable_allowCancellation !== false\n ? async () => {\n await stream.stop();\n }\n : undefined,\n });\n\n return runtime;\n};\n\n/**\n * Creates an assistant-ui runtime backed by LangChain's `useStream` hook.\n * Accepts the same options as `useStream` from `@langchain/react`, plus\n * `cloud` and `adapters`.\n *\n * @example\n * ```tsx\n * import { useStreamRuntime } from \"@assistant-ui/react-langchain\";\n * import { AssistantRuntimeProvider, Thread } from \"@assistant-ui/react\";\n *\n * function App() {\n * const runtime = useStreamRuntime({\n * assistantId: \"agent\",\n * apiUrl: \"http://localhost:2024\",\n * });\n *\n * return (\n * <AssistantRuntimeProvider runtime={runtime}>\n * <Thread />\n * </AssistantRuntimeProvider>\n * );\n * }\n * ```\n */\nexport const useStreamRuntime = (rawOptions: UseStreamRuntimeOptions) => {\n const {\n cloud,\n unstable_threadListAdapter,\n create,\n delete: deleteFn,\n onThreadIdChange,\n ...options\n } = rawOptions;\n\n const optionsRef = useRef(options);\n optionsRef.current = options;\n\n const cloudAdapter = useCloudThreadListAdapter({\n cloud,\n create,\n delete: deleteFn,\n });\n const adapter = unstable_threadListAdapter ?? cloudAdapter;\n\n return useRemoteThreadListRuntime({\n runtimeHook: function RuntimeHook() {\n return useStreamThreadRuntime(optionsRef.current);\n },\n adapter,\n allowNesting: true,\n onThreadIdChange,\n });\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"useStreamRuntime.js","names":[],"sources":["../src/useStreamRuntime.ts"],"sourcesContent":["/// <reference types=\"@assistant-ui/core/store\" />\n\"use client\";\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport type { AppendMessage, ToolExecutionStatus } from \"@assistant-ui/core\";\nimport {\n generateId,\n getExternalStoreMessages,\n pickExternalStoreSharedOptions,\n} from \"@assistant-ui/core\";\nimport type { ThreadMessage } from \"@assistant-ui/core\";\nimport {\n useCloudThreadListAdapter,\n useExternalStoreRuntime,\n useExternalMessageConverter,\n useRemoteThreadListRuntime,\n} from \"@assistant-ui/core/react\";\nimport { useAui, useAuiState } from \"@assistant-ui/store\";\nimport { STREAM_CONTROLLER, useChannel, useStream } from \"@langchain/react\";\nimport type { Channel } from \"@langchain/react\";\nimport type {\n LangChainBaseMessage,\n LangChainToolCall,\n UIMessage,\n UseStreamRuntimeOptions,\n} from \"./types\";\nimport {\n convertLangChainBaseMessage,\n getMessageContent,\n getMessageType,\n} from \"./convertMessages\";\nimport { foldUIUpdates, mergeUIMessages } from \"./uiMessages\";\nimport { langChainExtras } from \"./runtimeExtras\";\nimport { resolveForkCheckpoint } from \"./resolveForkCheckpoint\";\nimport { useLangChainStreamingTiming } from \"./streamingTiming\";\n\nconst UI_CUSTOM_CHANNELS: readonly Channel[] = [\"custom\"];\n\nexport const runConfigToSubmitOptions = (\n runConfig: AppendMessage[\"runConfig\"],\n) =>\n runConfig?.custom\n ? { config: { configurable: runConfig.custom } }\n : undefined;\n\ntype NormalizedRunConfigOptions = NonNullable<\n ReturnType<typeof runConfigToSubmitOptions>\n>;\n\n/**\n * Group the graph's accumulated `UIMessage`s by the assistant message they\n * belong to. Non-array state and entries without a parent link are dropped.\n * The parent id comes from `metadata.message_id` (Python SDK) or\n * `metadata.id` (JS SDK).\n */\nexport const groupUIMessagesByParent = (\n value: unknown,\n): Map<string, UIMessage[]> => {\n const map = new Map<string, UIMessage[]>();\n if (!Array.isArray(value)) return map;\n for (const ui of value as UIMessage[]) {\n const parentId = ui.metadata?.message_id ?? ui.metadata?.id;\n if (!parentId) continue;\n const existing = map.get(parentId);\n if (existing) {\n existing.push(ui);\n } else {\n map.set(parentId, [ui]);\n }\n }\n return map;\n};\n\nconst getPendingToolCalls = (\n messages: readonly LangChainBaseMessage[],\n): LangChainToolCall[] => {\n const pending = new Map<string, LangChainToolCall>();\n for (const m of messages) {\n const type = getMessageType(m);\n if (type === \"ai\") {\n for (const tc of m.tool_calls ?? []) pending.set(tc.id, tc);\n } else if (type === \"tool\" && m.tool_call_id) {\n pending.delete(m.tool_call_id);\n }\n }\n return [...pending.values()];\n};\n\nconst toStagedHumanMessage = (\n msg: AppendMessage,\n id = generateId(),\n): LangChainBaseMessage & { id: string } => ({\n id,\n _getType: () => \"human\",\n content: getMessageContent(msg),\n});\n\nconst truncateLangChainBaseMessages = (\n threadMessages: readonly ThreadMessage[],\n parentId: string | null,\n): LangChainBaseMessage[] => {\n if (parentId === null) return [];\n const parentIndex = threadMessages.findIndex((m) => m.id === parentId);\n if (parentIndex === -1) return [];\n const truncated: LangChainBaseMessage[] = [];\n for (let i = 0; i <= parentIndex && i < threadMessages.length; i++) {\n truncated.push(\n ...getExternalStoreMessages<LangChainBaseMessage>(threadMessages[i]!),\n );\n }\n return truncated;\n};\n\ntype DistributiveOmit<T, K extends keyof any> = T extends unknown\n ? Omit<T, K>\n : never;\n\nconst useStreamThreadRuntime = (\n options: DistributiveOmit<\n UseStreamRuntimeOptions,\n \"cloud\" | \"unstable_threadListAdapter\" | \"create\" | \"delete\"\n >,\n) => {\n const { adapters, autoCancelPendingToolCalls, unstable_allowCancellation } =\n options;\n const aui = useAui();\n const messagesKey = options.messagesKey ?? \"messages\";\n const uiStateKey = options.uiStateKey ?? \"ui\";\n\n const externalId = useAuiState((s) => s.threadListItem.externalId) as\n | string\n | null;\n // Object.assign preserves the discriminated transport union; object spread\n // collapses its arms and no longer satisfies UseStreamOptions.\n const stream = useStream(\n Object.assign({}, options, { threadId: externalId }),\n );\n const [stagedMessages, setStagedMessages] = useState<\n LangChainBaseMessage[] | null\n >(null);\n\n const [toolStatuses, setToolStatuses] = useState<\n Record<string, ToolExecutionStatus>\n >({});\n const hasExecutingTools = Object.values(toolStatuses).some(\n (s) => s?.type === \"executing\",\n );\n const effectiveIsRunning = stream.isLoading || hasExecutingTools;\n\n const uiStateValue = stream.values[uiStateKey];\n\n const customEvents = useChannel(stream, UI_CUSTOM_CHANNELS);\n const liveUiMessages = useMemo(\n () => foldUIUpdates(customEvents),\n [customEvents],\n );\n\n const mergedUiMessages = useMemo(\n () => mergeUIMessages(liveUiMessages, uiStateValue),\n [liveUiMessages, uiStateValue],\n );\n\n const visibleMessages =\n stagedMessages ?? (stream.messages as LangChainBaseMessage[]);\n\n const messageTiming = useLangChainStreamingTiming(\n visibleMessages,\n effectiveIsRunning,\n );\n\n const convertWithUI = useMemo<\n useExternalMessageConverter.Callback<LangChainBaseMessage>\n >(() => {\n const uiMessagesByParent = groupUIMessagesByParent(mergedUiMessages);\n return (message, metadata) =>\n convertLangChainBaseMessage(message, {\n ...metadata,\n uiMessagesByParent,\n messageTiming,\n });\n }, [mergedUiMessages, messageTiming]);\n\n const threadMessages = useExternalMessageConverter({\n callback: convertWithUI,\n messages: visibleMessages,\n isRunning: effectiveIsRunning,\n });\n\n const streamRef = useRef(stream);\n streamRef.current = stream;\n\n const activeRunConfigRef = useRef<\n NormalizedRunConfigOptions[\"config\"] | undefined\n >(undefined);\n const runConfigByMessageIdRef = useRef(\n new Map<string, NormalizedRunConfigOptions[\"config\"] | undefined>(),\n );\n const activeThreadIdRef = useRef(externalId);\n const setActiveRunConfig = useCallback(\n (runConfig: AppendMessage[\"runConfig\"]) => {\n activeRunConfigRef.current = runConfigToSubmitOptions(runConfig)?.config;\n },\n [],\n );\n const withActiveRunConfig = useCallback(\n (submitOptions?: Record<string, unknown>) => {\n if (submitOptions && \"config\" in submitOptions) return submitOptions;\n if (activeRunConfigRef.current === undefined) return submitOptions;\n return { ...submitOptions, config: activeRunConfigRef.current };\n },\n [],\n );\n\n useEffect(() => {\n if (\n activeThreadIdRef.current !== null &&\n activeThreadIdRef.current !== externalId\n ) {\n activeRunConfigRef.current = undefined;\n runConfigByMessageIdRef.current.clear();\n }\n activeThreadIdRef.current = externalId;\n }, [externalId]);\n\n useEffect(() => {\n const messages = stream.messages as readonly LangChainBaseMessage[];\n const owned = runConfigByMessageIdRef.current;\n for (let i = messages.length - 1; i >= 0; i--) {\n const message = messages.at(i);\n if (\n !message?.id ||\n getMessageType(message) !== \"ai\" ||\n !message.tool_calls?.length\n ) {\n continue;\n }\n if (owned.has(message.id)) return;\n break;\n }\n for (const message of messages) {\n if (\n !message.id ||\n getMessageType(message) !== \"ai\" ||\n !message.tool_calls?.length ||\n owned.has(message.id)\n ) {\n continue;\n }\n owned.set(message.id, activeRunConfigRef.current);\n }\n }, [stream.messages]);\n\n const visibleMessagesRef = useRef(visibleMessages);\n visibleMessagesRef.current = visibleMessages;\n\n const threadMessagesRef = useRef(threadMessages);\n threadMessagesRef.current = threadMessages;\n\n const stagedMessagesRef = useRef(\n new Map<\n string,\n {\n message: LangChainBaseMessage & { id: string };\n runConfig: AppendMessage[\"runConfig\"];\n }\n >(),\n );\n const stagedBaseMessagesRef = useRef<LangChainBaseMessage[] | null>(null);\n\n useEffect(() => {\n if (stagedMessagesRef.current.size === 0) return;\n\n // Staged edits must keep their truncated base while stream updates arrive before promotion.\n const baseMessages =\n stagedBaseMessagesRef.current ??\n (stream.messages as LangChainBaseMessage[]);\n const baseMessageIds = new Set(\n baseMessages.flatMap((message) => (message.id ? [message.id] : [])),\n );\n const remainingStagedMessages: LangChainBaseMessage[] = [];\n const seenStagedIds = new Set<string>();\n for (const message of visibleMessagesRef.current) {\n if (!message.id || seenStagedIds.has(message.id)) continue;\n if (baseMessageIds.has(message.id)) continue;\n const staged = stagedMessagesRef.current.get(message.id);\n if (!staged) continue;\n remainingStagedMessages.push(staged.message);\n seenStagedIds.add(message.id);\n }\n\n if (remainingStagedMessages.length === 0) {\n stagedBaseMessagesRef.current = null;\n visibleMessagesRef.current = baseMessages;\n setStagedMessages(null);\n return;\n }\n\n const nextMessages = [...baseMessages, ...remainingStagedMessages];\n visibleMessagesRef.current = nextMessages;\n setStagedMessages(nextMessages);\n }, [stream.messages]);\n\n const getStagedRun = (parentId: string | null) => {\n if (!parentId || !stagedMessagesRef.current.has(parentId)) return null;\n\n const staged: LangChainBaseMessage[] = [];\n for (const message of visibleMessagesRef.current) {\n if (message.id && stagedMessagesRef.current.has(message.id)) {\n staged.push(stagedMessagesRef.current.get(message.id)!.message);\n }\n if (message.id === parentId) break;\n }\n\n return {\n messages: staged,\n runConfig: stagedMessagesRef.current.get(parentId)!.runConfig,\n };\n };\n\n const stageUserMessage = (msg: AppendMessage) => {\n const stagedMessage = toStagedHumanMessage(msg);\n stagedMessagesRef.current.set(stagedMessage.id, {\n message: stagedMessage,\n runConfig: msg.runConfig,\n });\n const nextMessages = [...visibleMessagesRef.current, stagedMessage];\n visibleMessagesRef.current = nextMessages;\n setStagedMessages(nextMessages);\n };\n\n const extras = useMemo(\n () =>\n langChainExtras.provide({\n interrupt: stream.interrupt,\n interrupts: stream.interrupts,\n toolCalls: stream.toolCalls,\n subagents: stream.subagents,\n subgraphs: stream.subgraphs,\n stream,\n error: stream.error,\n submit: (values, submitOptions) => {\n const isResume = values == null || submitOptions?.command != null;\n return stream.submit(\n values,\n isResume ? withActiveRunConfig(submitOptions) : submitOptions,\n );\n },\n respond: (response, respondOptions) =>\n stream.respond(response, withActiveRunConfig(respondOptions)),\n respondAll: (responsesById, respondOptions) =>\n stream.respondAll(responsesById, withActiveRunConfig(respondOptions)),\n values: stream.values,\n messagesKey,\n }),\n [stream, messagesKey, withActiveRunConfig],\n );\n\n const runtime = useExternalStoreRuntime({\n ...pickExternalStoreSharedOptions(options),\n isRunning: effectiveIsRunning,\n isLoading: stream.isThreadLoading,\n messages: threadMessages,\n adapters,\n extras,\n unstable_enableToolInvocations: true,\n setToolStatuses,\n onNew: async (msg) => {\n if (!(msg.startRun ?? msg.role === \"user\")) {\n stageUserMessage(msg);\n return;\n }\n\n setActiveRunConfig(msg.runConfig);\n const content = getMessageContent(msg);\n const cancellations =\n autoCancelPendingToolCalls !== false\n ? getPendingToolCalls(\n streamRef.current.messages as readonly LangChainBaseMessage[],\n ).map((t) => ({\n type: \"tool\" as const,\n name: t.name,\n tool_call_id: t.id,\n content: JSON.stringify({ cancelled: true }),\n status: \"error\" as const,\n }))\n : [];\n // A null threadId is not a no-op for the SDK: it rebinds the controller\n // away from its self-created thread and forces a fresh one, so the\n // submit waits for initialization to produce an identity; core no\n // longer holds appends on that barrier.\n const { externalId } = await aui.threadListItem.initialize();\n await streamRef.current.submit(\n { [messagesKey]: [...cancellations, { type: \"human\", content }] },\n {\n ...runConfigToSubmitOptions(msg.runConfig),\n ...(externalId != null ? { threadId: externalId } : {}),\n },\n );\n },\n onAddToolResult: async ({\n messageId,\n toolCallId,\n toolName,\n result,\n isError,\n artifact,\n }) => {\n const runConfig = runConfigByMessageIdRef.current.has(messageId)\n ? runConfigByMessageIdRef.current.get(messageId)\n : activeRunConfigRef.current;\n await stream.submit(\n {\n [messagesKey]: [\n {\n type: \"tool\",\n name: toolName,\n tool_call_id: toolCallId,\n content: JSON.stringify(result),\n ...(artifact !== undefined && { artifact }),\n status: isError ? \"error\" : \"success\",\n },\n ],\n },\n runConfig === undefined ? undefined : { config: runConfig },\n );\n },\n onReload: async (parentId, config) => {\n const stagedRun = getStagedRun(parentId);\n if (stagedRun) {\n const promotedIds = new Set<string>();\n for (const message of stagedRun.messages) {\n if (!message.id) continue;\n promotedIds.add(message.id);\n stagedMessagesRef.current.delete(message.id);\n }\n stagedBaseMessagesRef.current = null;\n if (stagedMessagesRef.current.size > 0) {\n const nextMessages = visibleMessagesRef.current.filter(\n (message) => !message.id || !promotedIds.has(message.id),\n );\n visibleMessagesRef.current = nextMessages;\n setStagedMessages(nextMessages);\n } else {\n setStagedMessages(null);\n }\n const runConfig = config.runConfig ?? stagedRun.runConfig;\n setActiveRunConfig(runConfig);\n await stream.submit(\n {\n [messagesKey]: stagedRun.messages.map((message) => ({\n id: message.id,\n type: \"human\",\n content: message.content,\n })),\n },\n runConfigToSubmitOptions(runConfig),\n );\n return;\n }\n\n const threadId = externalId;\n if (!threadId || parentId == null) return;\n const s = streamRef.current;\n const checkpointId = await resolveForkCheckpoint(\n s.client,\n threadId,\n s.messages as readonly LangChainBaseMessage[],\n parentId,\n config.sourceId,\n s[STREAM_CONTROLLER]?.messageMetadataStore?.getSnapshot?.(),\n messagesKey,\n );\n if (!checkpointId) return;\n setActiveRunConfig(config.runConfig);\n await s.submit(null, {\n forkFrom: checkpointId,\n ...runConfigToSubmitOptions(config.runConfig),\n });\n },\n onEdit: async (message) => {\n if (!(message.startRun ?? message.role === \"user\")) {\n const truncated = truncateLangChainBaseMessages(\n threadMessagesRef.current,\n message.parentId,\n );\n const stagedMessage = toStagedHumanMessage(message);\n stagedMessagesRef.current.set(stagedMessage.id, {\n message: stagedMessage,\n runConfig: message.runConfig,\n });\n stagedBaseMessagesRef.current = truncated;\n const nextMessages = [...truncated, stagedMessage];\n visibleMessagesRef.current = nextMessages;\n setStagedMessages(nextMessages);\n return;\n }\n\n const threadId = externalId;\n if (!threadId) return;\n const s = streamRef.current;\n const checkpointId = await resolveForkCheckpoint(\n s.client,\n threadId,\n s.messages as readonly LangChainBaseMessage[],\n message.parentId,\n message.sourceId,\n s[STREAM_CONTROLLER]?.messageMetadataStore?.getSnapshot?.(),\n messagesKey,\n );\n if (!checkpointId) return;\n const content = getMessageContent(message);\n setActiveRunConfig(message.runConfig);\n await s.submit(\n { [messagesKey]: [{ type: \"human\", content }] },\n {\n forkFrom: checkpointId,\n ...runConfigToSubmitOptions(message.runConfig),\n },\n );\n },\n onCancel:\n unstable_allowCancellation !== false\n ? async () => {\n activeRunConfigRef.current = undefined;\n await stream.stop();\n }\n : undefined,\n });\n\n return runtime;\n};\n\n/**\n * Creates an assistant-ui runtime backed by LangChain's `useStream` hook.\n * Accepts the same options as `useStream` from `@langchain/react`, plus\n * `cloud` and `adapters`.\n *\n * @example\n * ```tsx\n * import { useStreamRuntime } from \"@assistant-ui/react-langchain\";\n * import { AssistantRuntimeProvider, Thread } from \"@assistant-ui/react\";\n *\n * function App() {\n * const runtime = useStreamRuntime({\n * assistantId: \"agent\",\n * apiUrl: \"http://localhost:2024\",\n * });\n *\n * return (\n * <AssistantRuntimeProvider runtime={runtime}>\n * <Thread />\n * </AssistantRuntimeProvider>\n * );\n * }\n * ```\n */\nexport const useStreamRuntime = (rawOptions: UseStreamRuntimeOptions) => {\n const {\n cloud,\n unstable_threadListAdapter,\n create,\n delete: deleteFn,\n onThreadIdChange,\n ...options\n } = rawOptions;\n\n const optionsRef = useRef(options);\n optionsRef.current = options;\n\n const cloudAdapter = useCloudThreadListAdapter({\n cloud,\n create,\n delete: deleteFn,\n });\n const adapter = unstable_threadListAdapter ?? cloudAdapter;\n\n return useRemoteThreadListRuntime({\n runtimeHook: function RuntimeHook() {\n return useStreamThreadRuntime(optionsRef.current);\n },\n adapter,\n allowNesting: true,\n onThreadIdChange,\n });\n};\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assistant-ui/react-langchain",
3
- "version": "0.0.25",
3
+ "version": "0.0.26",
4
4
  "description": "LangChain useStream adapter for assistant-ui",
5
5
  "keywords": [
6
6
  "langchain",
@@ -29,10 +29,10 @@
29
29
  ],
30
30
  "sideEffects": false,
31
31
  "dependencies": {
32
- "@assistant-ui/core": "^0.3.10",
33
- "@assistant-ui/store": "^0.3.8",
32
+ "@assistant-ui/core": "^0.3.14",
33
+ "@assistant-ui/store": "^0.3.10",
34
34
  "assistant-cloud": "*",
35
- "assistant-stream": "^0.3.36"
35
+ "assistant-stream": "^0.3.38"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "@langchain/react": "^1.0.2",
@@ -46,14 +46,14 @@
46
46
  },
47
47
  "devDependencies": {
48
48
  "@langchain/core": "^1.2.5",
49
- "@langchain/langgraph-sdk": "^1.9.28",
50
- "@langchain/react": "^1.0.29",
49
+ "@langchain/langgraph-sdk": "^1.9.29",
50
+ "@langchain/react": "^1.0.30",
51
51
  "@testing-library/react": "^16.3.2",
52
52
  "@types/react": "^19.2.18",
53
53
  "jsdom": "^30.0.1",
54
54
  "react": "^19.2.8",
55
55
  "vitest": "^4.1.10",
56
- "@assistant-ui/x-buildutils": "0.0.22"
56
+ "@assistant-ui/x-buildutils": "0.0.23"
57
57
  },
58
58
  "publishConfig": {
59
59
  "access": "public",
@@ -10,6 +10,13 @@ import type {
10
10
  import { useAui } from "@assistant-ui/store";
11
11
  import type { LangChainBaseMessage } from "./types";
12
12
  import type { ReactNode } from "react";
13
+ import {
14
+ useLangChainRespond,
15
+ useLangChainRespondAll,
16
+ useLangChainSend,
17
+ useLangChainSendCommand,
18
+ useLangChainSubmit,
19
+ } from "./hooks";
13
20
 
14
21
  const { mockUseChannel, mockUseStream, streamController } = vi.hoisted(() => ({
15
22
  mockUseChannel: vi.fn(() => []),
@@ -147,6 +154,14 @@ const makeThreadListAdapter = (): RemoteThreadListAdapter => ({
147
154
  })),
148
155
  });
149
156
 
157
+ const deferred = <T,>() => {
158
+ let resolve!: (value: T) => void;
159
+ const promise = new Promise<T>((res) => {
160
+ resolve = res;
161
+ });
162
+ return { promise, resolve };
163
+ };
164
+
150
165
  describe("useStreamRuntime thread options", () => {
151
166
  it("keeps stream options isolated between mounted threads", async () => {
152
167
  mockUseStream.mockReturnValue(createMockStream());
@@ -186,6 +201,409 @@ describe("useStreamRuntime thread options", () => {
186
201
 
187
202
  view.unmount();
188
203
  });
204
+
205
+ it("submits with the initialized thread id when initialization resolves before rerender", async () => {
206
+ const stream = createMockStream();
207
+ mockUseStream.mockReturnValue(stream);
208
+ const initialization = deferred<{
209
+ remoteId: string;
210
+ externalId: string;
211
+ }>();
212
+ const threadListAdapter = makeThreadListAdapter();
213
+ threadListAdapter.list = vi.fn(async () => ({ threads: [] }));
214
+ threadListAdapter.initialize = vi.fn(() => initialization.promise);
215
+
216
+ const capture: { runtime: AssistantRuntime | null } = { runtime: null };
217
+ const TestRuntime = () => {
218
+ const runtime = useStreamRuntime({
219
+ apiUrl: "/api",
220
+ unstable_threadListAdapter: threadListAdapter,
221
+ } as never);
222
+ capture.runtime = runtime;
223
+ return <AssistantRuntimeProvider runtime={runtime} />;
224
+ };
225
+
226
+ const view = render(<TestRuntime />);
227
+ await waitFor(() => expect(capture.runtime).not.toBeNull());
228
+
229
+ await act(async () => {
230
+ capture.runtime!.thread.append({
231
+ role: "user",
232
+ content: [{ type: "text", text: "hello" }],
233
+ });
234
+ await Promise.resolve();
235
+ });
236
+
237
+ expect(stream.submit).not.toHaveBeenCalled();
238
+
239
+ await act(async () => {
240
+ initialization.resolve({ remoteId: "thread-b", externalId: "thread-b" });
241
+ });
242
+
243
+ await waitFor(() =>
244
+ expect(stream.submit).toHaveBeenCalledWith(
245
+ { messages: [{ type: "human", content: "hello" }] },
246
+ { threadId: "thread-b" },
247
+ ),
248
+ );
249
+ view.unmount();
250
+ });
251
+
252
+ it("omits the threadId override when initialization yields no external id", async () => {
253
+ const stream = createMockStream();
254
+ mockUseStream.mockReturnValue(stream);
255
+ const capture: { runtime: AssistantRuntime | null } = { runtime: null };
256
+ const TestRuntime = () => {
257
+ const runtime = useStreamRuntime({ apiUrl: "/api" } as never);
258
+ capture.runtime = runtime;
259
+ return <AssistantRuntimeProvider runtime={runtime} />;
260
+ };
261
+ const view = render(<TestRuntime />);
262
+ await waitFor(() => expect(capture.runtime).not.toBeNull());
263
+
264
+ await act(async () => {
265
+ await capture.runtime!.thread.append({
266
+ role: "user",
267
+ content: [{ type: "text", text: "one" }],
268
+ });
269
+ });
270
+ await act(async () => {
271
+ await capture.runtime!.thread.append({
272
+ role: "user",
273
+ content: [{ type: "text", text: "two" }],
274
+ });
275
+ });
276
+
277
+ expect(stream.submit).toHaveBeenCalledTimes(2);
278
+ for (const call of stream.submit.mock.calls) {
279
+ expect(call[1]).not.toHaveProperty("threadId");
280
+ }
281
+ view.unmount();
282
+ });
283
+ });
284
+
285
+ describe("useStreamRuntime run configuration", () => {
286
+ it("preserves custom configuration for automatic tool-result resumes", async () => {
287
+ const stream = createMockStream();
288
+ const { auiResult, rerender } = renderAui(stream);
289
+
290
+ await act(async () => {
291
+ await auiResult.current.thread.append({
292
+ role: "user",
293
+ content: [{ type: "text", text: "hello" }],
294
+ runConfig: { custom: { model_name: "gpt-5.4-nano" } },
295
+ });
296
+ });
297
+
298
+ stream.messages = [
299
+ {
300
+ id: "assistant-1",
301
+ _getType: () => "ai",
302
+ content: "",
303
+ tool_calls: [{ id: "tool-1", name: "lookup", args: {} }],
304
+ },
305
+ ];
306
+ rerender();
307
+
308
+ await waitFor(() => {
309
+ expect(auiResult.current.thread.getState().messages).toContainEqual(
310
+ expect.objectContaining({
311
+ id: "assistant-1",
312
+ content: expect.arrayContaining([
313
+ expect.objectContaining({
314
+ type: "tool-call",
315
+ toolCallId: "tool-1",
316
+ }),
317
+ ]),
318
+ }),
319
+ );
320
+ });
321
+
322
+ act(() => {
323
+ auiResult.current.thread
324
+ .message({ id: "assistant-1" })
325
+ .part({ toolCallId: "tool-1" })
326
+ .addToolResult({ answer: 42 });
327
+ });
328
+ await waitFor(() => expect(stream.submit).toHaveBeenCalledTimes(2));
329
+
330
+ const config = { config: { configurable: { model_name: "gpt-5.4-nano" } } };
331
+ expect(stream.submit).toHaveBeenNthCalledWith(
332
+ 1,
333
+ {
334
+ messages: [{ type: "human", content: "hello" }],
335
+ },
336
+ config,
337
+ );
338
+ expect(stream.submit).toHaveBeenNthCalledWith(
339
+ 2,
340
+ {
341
+ messages: [
342
+ {
343
+ type: "tool",
344
+ name: "lookup",
345
+ tool_call_id: "tool-1",
346
+ content: JSON.stringify({ answer: 42 }),
347
+ status: "success",
348
+ },
349
+ ],
350
+ },
351
+ config,
352
+ );
353
+ });
354
+
355
+ it("inherits custom configuration through exposed resume helpers", async () => {
356
+ const stream = createMockStream();
357
+ mockUseStream.mockReturnValue(stream);
358
+ const capture: {
359
+ runtime: AssistantRuntime | null;
360
+ respond?: ReturnType<typeof useLangChainRespond>;
361
+ respondAll?: ReturnType<typeof useLangChainRespondAll>;
362
+ sendCommand?: ReturnType<typeof useLangChainSendCommand>;
363
+ } = { runtime: null };
364
+
365
+ const Capture = () => {
366
+ capture.respond = useLangChainRespond();
367
+ capture.respondAll = useLangChainRespondAll();
368
+ capture.sendCommand = useLangChainSendCommand();
369
+ return null;
370
+ };
371
+ Capture.displayName = "Capture";
372
+
373
+ const TestRuntime = () => {
374
+ const runtime = useStreamRuntime({ apiUrl: "/api" } as never);
375
+ capture.runtime = runtime;
376
+ return (
377
+ <AssistantRuntimeProvider runtime={runtime}>
378
+ <Capture />
379
+ </AssistantRuntimeProvider>
380
+ );
381
+ };
382
+ TestRuntime.displayName = "TestRuntime";
383
+
384
+ const view = render(<TestRuntime />);
385
+ await waitFor(() => expect(capture.respond).toBeDefined());
386
+
387
+ await act(async () => {
388
+ await capture.runtime!.thread.append({
389
+ role: "user",
390
+ content: [{ type: "text", text: "hello" }],
391
+ runConfig: { custom: { model_name: "gpt-5.4-nano" } },
392
+ });
393
+ });
394
+
395
+ const config = { config: { configurable: { model_name: "gpt-5.4-nano" } } };
396
+ await act(async () => {
397
+ await capture.respond!({ approved: true });
398
+ await capture.respondAll!({ "interrupt-1": { approved: true } });
399
+ await capture.sendCommand!({ resume: "continue" });
400
+ });
401
+
402
+ expect(stream.respond).toHaveBeenCalledWith({ approved: true }, config);
403
+ expect(stream.respondAll).toHaveBeenCalledWith(
404
+ { "interrupt-1": { approved: true } },
405
+ config,
406
+ );
407
+ expect(stream.submit).toHaveBeenLastCalledWith(null, {
408
+ command: { resume: "continue" },
409
+ ...config,
410
+ });
411
+ view.unmount();
412
+ });
413
+
414
+ it("keeps a delayed tool result on the run that produced it", async () => {
415
+ const stream = createMockStream();
416
+ const { auiResult, rerender } = renderAui(stream);
417
+
418
+ await act(async () => {
419
+ await auiResult.current.thread.append({
420
+ role: "user",
421
+ content: [{ type: "text", text: "first" }],
422
+ runConfig: { custom: { model_name: "model-a" } },
423
+ });
424
+ });
425
+
426
+ stream.messages = [
427
+ {
428
+ id: "assistant-1",
429
+ _getType: () => "ai",
430
+ content: "",
431
+ tool_calls: [{ id: "tool-1", name: "lookup", args: {} }],
432
+ },
433
+ ];
434
+ rerender();
435
+ await waitFor(() => {
436
+ expect(auiResult.current.thread.getState().messages).toContainEqual(
437
+ expect.objectContaining({ id: "assistant-1" }),
438
+ );
439
+ });
440
+
441
+ await act(async () => {
442
+ await auiResult.current.thread.append({
443
+ role: "user",
444
+ content: [{ type: "text", text: "second" }],
445
+ runConfig: { custom: { model_name: "model-b" } },
446
+ });
447
+ });
448
+
449
+ act(() => {
450
+ auiResult.current.thread
451
+ .message({ id: "assistant-1" })
452
+ .part({ toolCallId: "tool-1" })
453
+ .addToolResult({ answer: 42 });
454
+ });
455
+ await waitFor(() => expect(stream.submit).toHaveBeenCalledTimes(3));
456
+
457
+ expect(stream.submit).toHaveBeenLastCalledWith(
458
+ {
459
+ messages: [
460
+ {
461
+ type: "tool",
462
+ name: "lookup",
463
+ tool_call_id: "tool-1",
464
+ content: JSON.stringify({ answer: 42 }),
465
+ status: "success",
466
+ },
467
+ ],
468
+ },
469
+ { config: { configurable: { model_name: "model-a" } } },
470
+ );
471
+ });
472
+
473
+ it("does not let a caller-supplied resume config replace the recorded configurable", async () => {
474
+ const stream = createMockStream();
475
+ mockUseStream.mockReturnValue(stream);
476
+ const capture: {
477
+ runtime: AssistantRuntime | null;
478
+ aui?: ReturnType<typeof useAui>;
479
+ submit?: ReturnType<typeof useLangChainSubmit>;
480
+ } = { runtime: null };
481
+
482
+ const Capture = () => {
483
+ capture.aui = useAui();
484
+ capture.submit = useLangChainSubmit();
485
+ return null;
486
+ };
487
+ Capture.displayName = "Capture";
488
+
489
+ const TestRuntime = () => {
490
+ const runtime = useStreamRuntime({ apiUrl: "/api" } as never);
491
+ capture.runtime = runtime;
492
+ return (
493
+ <AssistantRuntimeProvider runtime={runtime}>
494
+ <Capture />
495
+ </AssistantRuntimeProvider>
496
+ );
497
+ };
498
+ TestRuntime.displayName = "TestRuntime";
499
+
500
+ const view = render(<TestRuntime />);
501
+ await waitFor(() => expect(capture.submit).toBeDefined());
502
+
503
+ await act(async () => {
504
+ await capture.runtime!.thread.append({
505
+ role: "user",
506
+ content: [{ type: "text", text: "hello" }],
507
+ runConfig: { custom: { model_name: "gpt-5.4-nano" } },
508
+ });
509
+ });
510
+
511
+ await act(async () => {
512
+ await capture.submit!(null, { command: { resume: "continue" } });
513
+ await capture.submit!(null, {
514
+ command: { resume: "continue" },
515
+ config: { recursion_limit: 5 },
516
+ });
517
+ });
518
+
519
+ stream.messages = [
520
+ {
521
+ id: "assistant-1",
522
+ _getType: () => "ai",
523
+ content: "",
524
+ tool_calls: [{ id: "tool-1", name: "lookup", args: {} }],
525
+ },
526
+ ];
527
+ view.rerender(<TestRuntime />);
528
+ await waitFor(() => {
529
+ expect(capture.aui!.thread.getState().messages).toContainEqual(
530
+ expect.objectContaining({ id: "assistant-1" }),
531
+ );
532
+ });
533
+
534
+ act(() => {
535
+ capture
536
+ .aui!.thread.message({ id: "assistant-1" })
537
+ .part({ toolCallId: "tool-1" })
538
+ .addToolResult({ answer: 42 });
539
+ });
540
+ await waitFor(() => expect(stream.submit).toHaveBeenCalledTimes(4));
541
+
542
+ expect(stream.submit).toHaveBeenNthCalledWith(2, null, {
543
+ command: { resume: "continue" },
544
+ config: { configurable: { model_name: "gpt-5.4-nano" } },
545
+ });
546
+ expect(stream.submit).toHaveBeenNthCalledWith(3, null, {
547
+ command: { resume: "continue" },
548
+ config: { recursion_limit: 5 },
549
+ });
550
+ expect(stream.submit).toHaveBeenLastCalledWith(
551
+ expect.objectContaining({
552
+ messages: expect.arrayContaining([
553
+ expect.objectContaining({ tool_call_id: "tool-1" }),
554
+ ]),
555
+ }),
556
+ { config: { configurable: { model_name: "gpt-5.4-nano" } } },
557
+ );
558
+ view.unmount();
559
+ });
560
+
561
+ it("does not inject the recorded config into a raw new-run submit", async () => {
562
+ const stream = createMockStream();
563
+ mockUseStream.mockReturnValue(stream);
564
+ const capture: {
565
+ runtime: AssistantRuntime | null;
566
+ send?: ReturnType<typeof useLangChainSend>;
567
+ } = { runtime: null };
568
+
569
+ const Capture = () => {
570
+ capture.send = useLangChainSend();
571
+ return null;
572
+ };
573
+ Capture.displayName = "Capture";
574
+
575
+ const TestRuntime = () => {
576
+ const runtime = useStreamRuntime({ apiUrl: "/api" } as never);
577
+ capture.runtime = runtime;
578
+ return (
579
+ <AssistantRuntimeProvider runtime={runtime}>
580
+ <Capture />
581
+ </AssistantRuntimeProvider>
582
+ );
583
+ };
584
+ TestRuntime.displayName = "TestRuntime";
585
+
586
+ const view = render(<TestRuntime />);
587
+ await waitFor(() => expect(capture.send).toBeDefined());
588
+
589
+ await act(async () => {
590
+ await capture.runtime!.thread.append({
591
+ role: "user",
592
+ content: [{ type: "text", text: "hello" }],
593
+ runConfig: { custom: { model_name: "gpt-5.4-nano" } },
594
+ });
595
+ });
596
+
597
+ await act(async () => {
598
+ await capture.send!([{ type: "human", content: "next" }]);
599
+ });
600
+
601
+ expect(stream.submit).toHaveBeenLastCalledWith(
602
+ { messages: [{ type: "human", content: "next" }] },
603
+ undefined,
604
+ );
605
+ view.unmount();
606
+ });
189
607
  });
190
608
 
191
609
  describe("useStreamRuntime staged messages", () => {
@@ -1,7 +1,7 @@
1
1
  /// <reference types="@assistant-ui/core/store" />
2
2
  "use client";
3
3
 
4
- import { useEffect, useMemo, useRef, useState } from "react";
4
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
5
5
  import type { AppendMessage, ToolExecutionStatus } from "@assistant-ui/core";
6
6
  import {
7
7
  generateId,
@@ -15,7 +15,7 @@ import {
15
15
  useExternalMessageConverter,
16
16
  useRemoteThreadListRuntime,
17
17
  } from "@assistant-ui/core/react";
18
- import { useAuiState } from "@assistant-ui/store";
18
+ import { useAui, useAuiState } from "@assistant-ui/store";
19
19
  import { STREAM_CONTROLLER, useChannel, useStream } from "@langchain/react";
20
20
  import type { Channel } from "@langchain/react";
21
21
  import type {
@@ -43,6 +43,10 @@ export const runConfigToSubmitOptions = (
43
43
  ? { config: { configurable: runConfig.custom } }
44
44
  : undefined;
45
45
 
46
+ type NormalizedRunConfigOptions = NonNullable<
47
+ ReturnType<typeof runConfigToSubmitOptions>
48
+ >;
49
+
46
50
  /**
47
51
  * Group the graph's accumulated `UIMessage`s by the assistant message they
48
52
  * belong to. Non-array state and entries without a parent link are dropped.
@@ -119,6 +123,7 @@ const useStreamThreadRuntime = (
119
123
  ) => {
120
124
  const { adapters, autoCancelPendingToolCalls, unstable_allowCancellation } =
121
125
  options;
126
+ const aui = useAui();
122
127
  const messagesKey = options.messagesKey ?? "messages";
123
128
  const uiStateKey = options.uiStateKey ?? "ui";
124
129
 
@@ -184,6 +189,67 @@ const useStreamThreadRuntime = (
184
189
  const streamRef = useRef(stream);
185
190
  streamRef.current = stream;
186
191
 
192
+ const activeRunConfigRef = useRef<
193
+ NormalizedRunConfigOptions["config"] | undefined
194
+ >(undefined);
195
+ const runConfigByMessageIdRef = useRef(
196
+ new Map<string, NormalizedRunConfigOptions["config"] | undefined>(),
197
+ );
198
+ const activeThreadIdRef = useRef(externalId);
199
+ const setActiveRunConfig = useCallback(
200
+ (runConfig: AppendMessage["runConfig"]) => {
201
+ activeRunConfigRef.current = runConfigToSubmitOptions(runConfig)?.config;
202
+ },
203
+ [],
204
+ );
205
+ const withActiveRunConfig = useCallback(
206
+ (submitOptions?: Record<string, unknown>) => {
207
+ if (submitOptions && "config" in submitOptions) return submitOptions;
208
+ if (activeRunConfigRef.current === undefined) return submitOptions;
209
+ return { ...submitOptions, config: activeRunConfigRef.current };
210
+ },
211
+ [],
212
+ );
213
+
214
+ useEffect(() => {
215
+ if (
216
+ activeThreadIdRef.current !== null &&
217
+ activeThreadIdRef.current !== externalId
218
+ ) {
219
+ activeRunConfigRef.current = undefined;
220
+ runConfigByMessageIdRef.current.clear();
221
+ }
222
+ activeThreadIdRef.current = externalId;
223
+ }, [externalId]);
224
+
225
+ useEffect(() => {
226
+ const messages = stream.messages as readonly LangChainBaseMessage[];
227
+ const owned = runConfigByMessageIdRef.current;
228
+ for (let i = messages.length - 1; i >= 0; i--) {
229
+ const message = messages.at(i);
230
+ if (
231
+ !message?.id ||
232
+ getMessageType(message) !== "ai" ||
233
+ !message.tool_calls?.length
234
+ ) {
235
+ continue;
236
+ }
237
+ if (owned.has(message.id)) return;
238
+ break;
239
+ }
240
+ for (const message of messages) {
241
+ if (
242
+ !message.id ||
243
+ getMessageType(message) !== "ai" ||
244
+ !message.tool_calls?.length ||
245
+ owned.has(message.id)
246
+ ) {
247
+ continue;
248
+ }
249
+ owned.set(message.id, activeRunConfigRef.current);
250
+ }
251
+ }, [stream.messages]);
252
+
187
253
  const visibleMessagesRef = useRef(visibleMessages);
188
254
  visibleMessagesRef.current = visibleMessages;
189
255
 
@@ -272,13 +338,21 @@ const useStreamThreadRuntime = (
272
338
  subgraphs: stream.subgraphs,
273
339
  stream,
274
340
  error: stream.error,
275
- submit: stream.submit,
276
- respond: stream.respond,
277
- respondAll: stream.respondAll,
341
+ submit: (values, submitOptions) => {
342
+ const isResume = values == null || submitOptions?.command != null;
343
+ return stream.submit(
344
+ values,
345
+ isResume ? withActiveRunConfig(submitOptions) : submitOptions,
346
+ );
347
+ },
348
+ respond: (response, respondOptions) =>
349
+ stream.respond(response, withActiveRunConfig(respondOptions)),
350
+ respondAll: (responsesById, respondOptions) =>
351
+ stream.respondAll(responsesById, withActiveRunConfig(respondOptions)),
278
352
  values: stream.values,
279
353
  messagesKey,
280
354
  }),
281
- [stream, messagesKey],
355
+ [stream, messagesKey, withActiveRunConfig],
282
356
  );
283
357
 
284
358
  const runtime = useExternalStoreRuntime({
@@ -296,6 +370,7 @@ const useStreamThreadRuntime = (
296
370
  return;
297
371
  }
298
372
 
373
+ setActiveRunConfig(msg.runConfig);
299
374
  const content = getMessageContent(msg);
300
375
  const cancellations =
301
376
  autoCancelPendingToolCalls !== false
@@ -309,30 +384,45 @@ const useStreamThreadRuntime = (
309
384
  status: "error" as const,
310
385
  }))
311
386
  : [];
312
- await stream.submit(
387
+ // A null threadId is not a no-op for the SDK: it rebinds the controller
388
+ // away from its self-created thread and forces a fresh one, so the
389
+ // submit waits for initialization to produce an identity; core no
390
+ // longer holds appends on that barrier.
391
+ const { externalId } = await aui.threadListItem.initialize();
392
+ await streamRef.current.submit(
313
393
  { [messagesKey]: [...cancellations, { type: "human", content }] },
314
- runConfigToSubmitOptions(msg.runConfig),
394
+ {
395
+ ...runConfigToSubmitOptions(msg.runConfig),
396
+ ...(externalId != null ? { threadId: externalId } : {}),
397
+ },
315
398
  );
316
399
  },
317
400
  onAddToolResult: async ({
401
+ messageId,
318
402
  toolCallId,
319
403
  toolName,
320
404
  result,
321
405
  isError,
322
406
  artifact,
323
407
  }) => {
324
- await stream.submit({
325
- [messagesKey]: [
326
- {
327
- type: "tool",
328
- name: toolName,
329
- tool_call_id: toolCallId,
330
- content: JSON.stringify(result),
331
- ...(artifact !== undefined && { artifact }),
332
- status: isError ? "error" : "success",
333
- },
334
- ],
335
- });
408
+ const runConfig = runConfigByMessageIdRef.current.has(messageId)
409
+ ? runConfigByMessageIdRef.current.get(messageId)
410
+ : activeRunConfigRef.current;
411
+ await stream.submit(
412
+ {
413
+ [messagesKey]: [
414
+ {
415
+ type: "tool",
416
+ name: toolName,
417
+ tool_call_id: toolCallId,
418
+ content: JSON.stringify(result),
419
+ ...(artifact !== undefined && { artifact }),
420
+ status: isError ? "error" : "success",
421
+ },
422
+ ],
423
+ },
424
+ runConfig === undefined ? undefined : { config: runConfig },
425
+ );
336
426
  },
337
427
  onReload: async (parentId, config) => {
338
428
  const stagedRun = getStagedRun(parentId);
@@ -353,6 +443,8 @@ const useStreamThreadRuntime = (
353
443
  } else {
354
444
  setStagedMessages(null);
355
445
  }
446
+ const runConfig = config.runConfig ?? stagedRun.runConfig;
447
+ setActiveRunConfig(runConfig);
356
448
  await stream.submit(
357
449
  {
358
450
  [messagesKey]: stagedRun.messages.map((message) => ({
@@ -361,7 +453,7 @@ const useStreamThreadRuntime = (
361
453
  content: message.content,
362
454
  })),
363
455
  },
364
- runConfigToSubmitOptions(config.runConfig ?? stagedRun.runConfig),
456
+ runConfigToSubmitOptions(runConfig),
365
457
  );
366
458
  return;
367
459
  }
@@ -379,6 +471,7 @@ const useStreamThreadRuntime = (
379
471
  messagesKey,
380
472
  );
381
473
  if (!checkpointId) return;
474
+ setActiveRunConfig(config.runConfig);
382
475
  await s.submit(null, {
383
476
  forkFrom: checkpointId,
384
477
  ...runConfigToSubmitOptions(config.runConfig),
@@ -416,6 +509,7 @@ const useStreamThreadRuntime = (
416
509
  );
417
510
  if (!checkpointId) return;
418
511
  const content = getMessageContent(message);
512
+ setActiveRunConfig(message.runConfig);
419
513
  await s.submit(
420
514
  { [messagesKey]: [{ type: "human", content }] },
421
515
  {
@@ -427,6 +521,7 @@ const useStreamThreadRuntime = (
427
521
  onCancel:
428
522
  unstable_allowCancellation !== false
429
523
  ? async () => {
524
+ activeRunConfigRef.current = undefined;
430
525
  await stream.stop();
431
526
  }
432
527
  : undefined,