@assistant-ui/react-langchain 0.0.24 → 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.
@@ -22,9 +22,7 @@ const getTextLength = (messages, messageId) => {
22
22
  case "thinking":
23
23
  if (typeof part.thinking === "string") len += part.thinking.length;
24
24
  break;
25
- case "reasoning":
26
- len += reasoningTextLength(part);
27
- break;
25
+ case "reasoning": len += reasoningTextLength(part);
28
26
  }
29
27
  return len;
30
28
  };
@@ -1 +1 @@
1
- {"version":3,"file":"streamingTiming.js","names":[],"sources":["../src/streamingTiming.ts"],"sourcesContent":["\"use client\";\n\nimport type { MessageTiming } from \"@assistant-ui/core\";\nimport {\n useStreamingTiming,\n type StreamingTimingAccessors,\n} from \"@assistant-ui/core/react\";\nimport type { LangChainBaseMessage, LangChainContentBlock } from \"./types\";\nimport { getMessageType } from \"./convertMessages\";\n\nconst findAiMessage = (\n messages: readonly LangChainBaseMessage[],\n messageId: string,\n): LangChainBaseMessage | undefined =>\n messages.find((m) => getMessageType(m) === \"ai\" && m.id === messageId);\n\nconst reasoningTextLength = (part: {\n readonly summary?: ReadonlyArray<{ readonly text?: string }>;\n readonly reasoning?: string;\n}): number => {\n if (part.summary && part.summary.length > 0)\n return part.summary.map((s) => s?.text ?? \"\").join(\"\\n\\n\\n\").length;\n return part.reasoning?.length ?? 0;\n};\n\nconst getTextLength = (\n messages: readonly LangChainBaseMessage[],\n messageId: string,\n): number => {\n const m = findAiMessage(messages, messageId);\n if (!m) return 0;\n const content = m.content;\n if (typeof content === \"string\") return content.length;\n if (!Array.isArray(content)) return 0;\n let len = 0;\n for (const part of content as readonly LangChainContentBlock[]) {\n switch (part.type) {\n case \"text\":\n case \"text_delta\":\n if (typeof part.text === \"string\") len += part.text.length;\n break;\n case \"thinking\":\n if (typeof part.thinking === \"string\") len += part.thinking.length;\n break;\n case \"reasoning\":\n len += reasoningTextLength(part);\n break;\n }\n }\n return len;\n};\n\nconst getToolCallCount = (\n messages: readonly LangChainBaseMessage[],\n messageId: string,\n): number => findAiMessage(messages, messageId)?.tool_calls?.length ?? 0;\n\nconst getAssistantMessageId = (\n messages: readonly LangChainBaseMessage[],\n): string | undefined => {\n for (let i = messages.length - 1; i >= 0; i--) {\n const m = messages[i];\n if (m && getMessageType(m) === \"ai\" && m.id) return m.id;\n }\n return undefined;\n};\n\nexport const langChainStreamingTimingAccessors: StreamingTimingAccessors<LangChainBaseMessage> =\n {\n getAssistantMessageId,\n getTextLength,\n getToolCallCount,\n };\n\n/**\n * Tracks per-message streaming timing for LangChain messages. Delegates to\n * the shared `useStreamingTiming` primitive in `@assistant-ui/core/react`,\n * adapted to the `LangChainBaseMessage` shape (`_getType() -> \"ai\"`, content\n * blocks including text/thinking/reasoning, `tool_calls`).\n */\nexport const useLangChainStreamingTiming = (\n messages: readonly LangChainBaseMessage[],\n isRunning: boolean,\n): Record<string, MessageTiming> =>\n useStreamingTiming(messages, isRunning, langChainStreamingTimingAccessors);\n"],"mappings":";;;;AAUA,MAAM,iBACJ,UACA,cAEA,SAAS,MAAM,MAAM,eAAe,CAAC,MAAM,QAAQ,EAAE,OAAO,SAAS;AAEvE,MAAM,uBAAuB,SAGf;CACZ,IAAI,KAAK,WAAW,KAAK,QAAQ,SAAS,GACxC,OAAO,KAAK,QAAQ,KAAK,MAAM,GAAG,QAAQ,EAAE,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAC;CAC/D,OAAO,KAAK,WAAW,UAAU;AACnC;AAEA,MAAM,iBACJ,UACA,cACW;CACX,MAAM,IAAI,cAAc,UAAU,SAAS;CAC3C,IAAI,CAAC,GAAG,OAAO;CACf,MAAM,UAAU,EAAE;CAClB,IAAI,OAAO,YAAY,UAAU,OAAO,QAAQ;CAChD,IAAI,CAAC,MAAM,QAAQ,OAAO,GAAG,OAAO;CACpC,IAAI,MAAM;CACV,KAAK,MAAM,QAAQ,SACjB,QAAQ,KAAK,MAAb;EACE,KAAK;EACL,KAAK;GACH,IAAI,OAAO,KAAK,SAAS,UAAU,OAAO,KAAK,KAAK;GACpD;EACF,KAAK;GACH,IAAI,OAAO,KAAK,aAAa,UAAU,OAAO,KAAK,SAAS;GAC5D;EACF,KAAK;GACH,OAAO,oBAAoB,IAAI;GAC/B;CACJ;CAEF,OAAO;AACT;AAEA,MAAM,oBACJ,UACA,cACW,cAAc,UAAU,SAAS,CAAC,EAAE,YAAY,UAAU;AAEvE,MAAM,yBACJ,aACuB;CACvB,KAAK,IAAI,IAAI,SAAS,SAAS,GAAG,KAAK,GAAG,KAAK;EAC7C,MAAM,IAAI,SAAS;EACnB,IAAI,KAAK,eAAe,CAAC,MAAM,QAAQ,EAAE,IAAI,OAAO,EAAE;CACxD;AAEF;AAEA,MAAa,oCACX;CACE;CACA;CACA;AACF;;;;;;;AAQF,MAAa,+BACX,UACA,cAEA,mBAAmB,UAAU,WAAW,iCAAiC"}
1
+ {"version":3,"file":"streamingTiming.js","names":[],"sources":["../src/streamingTiming.ts"],"sourcesContent":["\"use client\";\n\nimport type { MessageTiming } from \"@assistant-ui/core\";\nimport {\n useStreamingTiming,\n type StreamingTimingAccessors,\n} from \"@assistant-ui/core/react\";\nimport type { LangChainBaseMessage, LangChainContentBlock } from \"./types\";\nimport { getMessageType } from \"./convertMessages\";\n\nconst findAiMessage = (\n messages: readonly LangChainBaseMessage[],\n messageId: string,\n): LangChainBaseMessage | undefined =>\n messages.find((m) => getMessageType(m) === \"ai\" && m.id === messageId);\n\nconst reasoningTextLength = (part: {\n readonly summary?: ReadonlyArray<{ readonly text?: string }>;\n readonly reasoning?: string;\n}): number => {\n if (part.summary && part.summary.length > 0)\n return part.summary.map((s) => s?.text ?? \"\").join(\"\\n\\n\\n\").length;\n return part.reasoning?.length ?? 0;\n};\n\nconst getTextLength = (\n messages: readonly LangChainBaseMessage[],\n messageId: string,\n): number => {\n const m = findAiMessage(messages, messageId);\n if (!m) return 0;\n const content = m.content;\n if (typeof content === \"string\") return content.length;\n if (!Array.isArray(content)) return 0;\n let len = 0;\n for (const part of content as readonly LangChainContentBlock[]) {\n switch (part.type) {\n case \"text\":\n case \"text_delta\":\n if (typeof part.text === \"string\") len += part.text.length;\n break;\n case \"thinking\":\n if (typeof part.thinking === \"string\") len += part.thinking.length;\n break;\n case \"reasoning\":\n len += reasoningTextLength(part);\n break;\n }\n }\n return len;\n};\n\nconst getToolCallCount = (\n messages: readonly LangChainBaseMessage[],\n messageId: string,\n): number => findAiMessage(messages, messageId)?.tool_calls?.length ?? 0;\n\nconst getAssistantMessageId = (\n messages: readonly LangChainBaseMessage[],\n): string | undefined => {\n for (let i = messages.length - 1; i >= 0; i--) {\n const m = messages[i];\n if (m && getMessageType(m) === \"ai\" && m.id) return m.id;\n }\n return undefined;\n};\n\nexport const langChainStreamingTimingAccessors: StreamingTimingAccessors<LangChainBaseMessage> =\n {\n getAssistantMessageId,\n getTextLength,\n getToolCallCount,\n };\n\n/**\n * Tracks per-message streaming timing for LangChain messages. Delegates to\n * the shared `useStreamingTiming` primitive in `@assistant-ui/core/react`,\n * adapted to the `LangChainBaseMessage` shape (`_getType() -> \"ai\"`, content\n * blocks including text/thinking/reasoning, `tool_calls`).\n */\nexport const useLangChainStreamingTiming = (\n messages: readonly LangChainBaseMessage[],\n isRunning: boolean,\n): Record<string, MessageTiming> =>\n useStreamingTiming(messages, isRunning, langChainStreamingTimingAccessors);\n"],"mappings":";;;;AAUA,MAAM,iBACJ,UACA,cAEA,SAAS,MAAM,MAAM,eAAe,CAAC,MAAM,QAAQ,EAAE,OAAO,SAAS;AAEvE,MAAM,uBAAuB,SAGf;CACZ,IAAI,KAAK,WAAW,KAAK,QAAQ,SAAS,GACxC,OAAO,KAAK,QAAQ,KAAK,MAAM,GAAG,QAAQ,EAAE,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAC;CAC/D,OAAO,KAAK,WAAW,UAAU;AACnC;AAEA,MAAM,iBACJ,UACA,cACW;CACX,MAAM,IAAI,cAAc,UAAU,SAAS;CAC3C,IAAI,CAAC,GAAG,OAAO;CACf,MAAM,UAAU,EAAE;CAClB,IAAI,OAAO,YAAY,UAAU,OAAO,QAAQ;CAChD,IAAI,CAAC,MAAM,QAAQ,OAAO,GAAG,OAAO;CACpC,IAAI,MAAM;CACV,KAAK,MAAM,QAAQ,SACjB,QAAQ,KAAK,MAAb;EACE,KAAK;EACL,KAAK;GACH,IAAI,OAAO,KAAK,SAAS,UAAU,OAAO,KAAK,KAAK;GACpD;EACF,KAAK;GACH,IAAI,OAAO,KAAK,aAAa,UAAU,OAAO,KAAK,SAAS;GAC5D;EACF,KAAK,aACH,OAAO,oBAAoB,IAAI;CAEnC;CAEF,OAAO;AACT;AAEA,MAAM,oBACJ,UACA,cACW,cAAc,UAAU,SAAS,CAAC,EAAE,YAAY,UAAU;AAEvE,MAAM,yBACJ,aACuB;CACvB,KAAK,IAAI,IAAI,SAAS,SAAS,GAAG,KAAK,GAAG,KAAK;EAC7C,MAAM,IAAI,SAAS;EACnB,IAAI,KAAK,eAAe,CAAC,MAAM,QAAQ,EAAE,IAAI,OAAO,EAAE;CACxD;AAEF;AAEA,MAAa,oCACX;CACE;CACA;CACA;AACF;;;;;;;AAQF,MAAa,+BACX,UACA,cAEA,mBAAmB,UAAU,WAAW,iCAAiC"}
@@ -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);
@@ -68,20 +69,56 @@ const useStreamThreadRuntime = (options) => {
68
69
  const mergedUiMessages = useMemo(() => mergeUIMessages(liveUiMessages, uiStateValue), [liveUiMessages, uiStateValue]);
69
70
  const visibleMessages = stagedMessages ?? stream.messages;
70
71
  const messageTiming = useLangChainStreamingTiming(visibleMessages, effectiveIsRunning);
72
+ const convertWithUI = useMemo(() => {
73
+ const uiMessagesByParent = groupUIMessagesByParent(mergedUiMessages);
74
+ return (message, metadata) => convertLangChainBaseMessage(message, {
75
+ ...metadata,
76
+ uiMessagesByParent,
77
+ messageTiming
78
+ });
79
+ }, [mergedUiMessages, messageTiming]);
71
80
  const threadMessages = useExternalMessageConverter({
72
- callback: useMemo(() => {
73
- const uiMessagesByParent = groupUIMessagesByParent(mergedUiMessages);
74
- return (message, metadata) => convertLangChainBaseMessage(message, {
75
- ...metadata,
76
- uiMessagesByParent,
77
- messageTiming
78
- });
79
- }, [mergedUiMessages, messageTiming]),
81
+ callback: convertWithUI,
80
82
  messages: visibleMessages,
81
83
  isRunning: effectiveIsRunning
82
84
  });
83
85
  const streamRef = useRef(stream);
84
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]);
85
122
  const visibleMessagesRef = useRef(visibleMessages);
86
123
  visibleMessagesRef.current = visibleMessages;
87
124
  const threadMessagesRef = useRef(threadMessages);
@@ -142,12 +179,19 @@ const useStreamThreadRuntime = (options) => {
142
179
  subgraphs: stream.subgraphs,
143
180
  stream,
144
181
  error: stream.error,
145
- submit: stream.submit,
146
- respond: stream.respond,
147
- 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)),
148
188
  values: stream.values,
149
189
  messagesKey
150
- }), [stream, messagesKey]);
190
+ }), [
191
+ stream,
192
+ messagesKey,
193
+ withActiveRunConfig
194
+ ]);
151
195
  return useExternalStoreRuntime({
152
196
  ...pickExternalStoreSharedOptions(options),
153
197
  isRunning: effectiveIsRunning,
@@ -162,6 +206,7 @@ const useStreamThreadRuntime = (options) => {
162
206
  stageUserMessage(msg);
163
207
  return;
164
208
  }
209
+ setActiveRunConfig(msg.runConfig);
165
210
  const content = getMessageContent(msg);
166
211
  const cancellations = autoCancelPendingToolCalls !== false ? getPendingToolCalls(streamRef.current.messages).map((t) => ({
167
212
  type: "tool",
@@ -170,12 +215,17 @@ const useStreamThreadRuntime = (options) => {
170
215
  content: JSON.stringify({ cancelled: true }),
171
216
  status: "error"
172
217
  })) : [];
173
- await stream.submit({ [messagesKey]: [...cancellations, {
218
+ const { externalId } = await aui.threadListItem.initialize();
219
+ await streamRef.current.submit({ [messagesKey]: [...cancellations, {
174
220
  type: "human",
175
221
  content
176
- }] }, runConfigToSubmitOptions(msg.runConfig));
222
+ }] }, {
223
+ ...runConfigToSubmitOptions(msg.runConfig),
224
+ ...externalId != null ? { threadId: externalId } : {}
225
+ });
177
226
  },
178
- 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;
179
229
  await stream.submit({ [messagesKey]: [{
180
230
  type: "tool",
181
231
  name: toolName,
@@ -183,7 +233,7 @@ const useStreamThreadRuntime = (options) => {
183
233
  content: JSON.stringify(result),
184
234
  ...artifact !== void 0 && { artifact },
185
235
  status: isError ? "error" : "success"
186
- }] });
236
+ }] }, runConfig === void 0 ? void 0 : { config: runConfig });
187
237
  },
188
238
  onReload: async (parentId, config) => {
189
239
  const stagedRun = getStagedRun(parentId);
@@ -200,11 +250,13 @@ const useStreamThreadRuntime = (options) => {
200
250
  visibleMessagesRef.current = nextMessages;
201
251
  setStagedMessages(nextMessages);
202
252
  } else setStagedMessages(null);
253
+ const runConfig = config.runConfig ?? stagedRun.runConfig;
254
+ setActiveRunConfig(runConfig);
203
255
  await stream.submit({ [messagesKey]: stagedRun.messages.map((message) => ({
204
256
  id: message.id,
205
257
  type: "human",
206
258
  content: message.content
207
- })) }, runConfigToSubmitOptions(config.runConfig ?? stagedRun.runConfig));
259
+ })) }, runConfigToSubmitOptions(runConfig));
208
260
  return;
209
261
  }
210
262
  const threadId = externalId;
@@ -212,6 +264,7 @@ const useStreamThreadRuntime = (options) => {
212
264
  const s = streamRef.current;
213
265
  const checkpointId = await resolveForkCheckpoint(s.client, threadId, s.messages, parentId, config.sourceId, s[STREAM_CONTROLLER]?.messageMetadataStore?.getSnapshot?.(), messagesKey);
214
266
  if (!checkpointId) return;
267
+ setActiveRunConfig(config.runConfig);
215
268
  await s.submit(null, {
216
269
  forkFrom: checkpointId,
217
270
  ...runConfigToSubmitOptions(config.runConfig)
@@ -237,6 +290,7 @@ const useStreamThreadRuntime = (options) => {
237
290
  const checkpointId = await resolveForkCheckpoint(s.client, threadId, s.messages, message.parentId, message.sourceId, s[STREAM_CONTROLLER]?.messageMetadataStore?.getSnapshot?.(), messagesKey);
238
291
  if (!checkpointId) return;
239
292
  const content = getMessageContent(message);
293
+ setActiveRunConfig(message.runConfig);
240
294
  await s.submit({ [messagesKey]: [{
241
295
  type: "human",
242
296
  content
@@ -246,6 +300,7 @@ const useStreamThreadRuntime = (options) => {
246
300
  });
247
301
  },
248
302
  onCancel: unstable_allowCancellation !== false ? async () => {
303
+ activeRunConfigRef.current = void 0;
249
304
  await stream.stop();
250
305
  } : void 0
251
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.24",
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.6",
33
- "@assistant-ui/store": "^0.3.4",
32
+ "@assistant-ui/core": "^0.3.14",
33
+ "@assistant-ui/store": "^0.3.10",
34
34
  "assistant-cloud": "*",
35
- "assistant-stream": "^0.3.34"
35
+ "assistant-stream": "^0.3.38"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "@langchain/react": "^1.0.2",
@@ -45,15 +45,15 @@
45
45
  }
46
46
  },
47
47
  "devDependencies": {
48
- "@langchain/core": "^1.2.3",
49
- "@langchain/langgraph-sdk": "^1.9.28",
50
- "@langchain/react": "^1.0.29",
48
+ "@langchain/core": "^1.2.5",
49
+ "@langchain/langgraph-sdk": "^1.9.29",
50
+ "@langchain/react": "^1.0.30",
51
51
  "@testing-library/react": "^16.3.2",
52
- "@types/react": "^19.2.17",
53
- "jsdom": "^29.1.1",
52
+ "@types/react": "^19.2.18",
53
+ "jsdom": "^30.0.1",
54
54
  "react": "^19.2.8",
55
55
  "vitest": "^4.1.10",
56
- "@assistant-ui/x-buildutils": "0.0.21"
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,