@assistant-ui/react-langchain 0.0.23 → 0.0.24

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;;;;;;;;;;;;;;;;;;;;;;;;;cAyZF,mBAAoB,YAAY,yDAAuB"}
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"}
@@ -57,8 +57,7 @@ const useStreamThreadRuntime = (options) => {
57
57
  const messagesKey = options.messagesKey ?? "messages";
58
58
  const uiStateKey = options.uiStateKey ?? "ui";
59
59
  const externalId = useAuiState((s) => s.threadListItem.externalId);
60
- options.threadId = externalId;
61
- const stream = useStream(options);
60
+ const stream = useStream(Object.assign({}, options, { threadId: externalId }));
62
61
  const [stagedMessages, setStagedMessages] = useState(null);
63
62
  const [toolStatuses, setToolStatuses] = useState({});
64
63
  const hasExecutingTools = Object.values(toolStatuses).some((s) => s?.type === "executing");
@@ -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 // Mutate in place rather than `{ ...options, threadId }`: spreading\n // `UseStreamOptions` (a discriminated union on `transport`) into an object\n // literal merges both arms' transport types, breaking arm assignment.\n options.threadId = externalId;\n\n const stream = useStream(options);\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 { 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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assistant-ui/react-langchain",
3
- "version": "0.0.23",
3
+ "version": "0.0.24",
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.4",
33
- "@assistant-ui/store": "^0.3.3",
32
+ "@assistant-ui/core": "^0.3.6",
33
+ "@assistant-ui/store": "^0.3.4",
34
34
  "assistant-cloud": "*",
35
- "assistant-stream": "^0.3.32"
35
+ "assistant-stream": "^0.3.34"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "@langchain/react": "^1.0.2",
@@ -53,7 +53,7 @@
53
53
  "jsdom": "^29.1.1",
54
54
  "react": "^19.2.8",
55
55
  "vitest": "^4.1.10",
56
- "@assistant-ui/x-buildutils": "0.0.20"
56
+ "@assistant-ui/x-buildutils": "0.0.21"
57
57
  },
58
58
  "publishConfig": {
59
59
  "access": "public",
@@ -1,8 +1,12 @@
1
1
  // @vitest-environment jsdom
2
2
 
3
- import { act, renderHook, waitFor } from "@testing-library/react";
3
+ import { act, render, renderHook, waitFor } from "@testing-library/react";
4
4
  import { describe, expect, it, vi } from "vitest";
5
5
  import { AssistantRuntimeProvider } from "@assistant-ui/core/react";
6
+ import type {
7
+ AssistantRuntime,
8
+ RemoteThreadListAdapter,
9
+ } from "@assistant-ui/core";
6
10
  import { useAui } from "@assistant-ui/store";
7
11
  import type { LangChainBaseMessage } from "./types";
8
12
  import type { ReactNode } from "react";
@@ -110,6 +114,80 @@ const getText = (aui: ReturnType<typeof useAui>) =>
110
114
  .join(""),
111
115
  );
112
116
 
117
+ const makeThreadListAdapter = (): RemoteThreadListAdapter => ({
118
+ list: vi.fn(async () => ({
119
+ threads: [
120
+ {
121
+ status: "regular" as const,
122
+ remoteId: "thread-a",
123
+ externalId: "thread-a",
124
+ title: "Thread A",
125
+ },
126
+ {
127
+ status: "regular" as const,
128
+ remoteId: "thread-b",
129
+ externalId: "thread-b",
130
+ title: "Thread B",
131
+ },
132
+ ],
133
+ })),
134
+ initialize: vi.fn(async () => ({
135
+ remoteId: "thread-new",
136
+ externalId: "thread-new",
137
+ })),
138
+ rename: vi.fn(async () => {}),
139
+ archive: vi.fn(async () => {}),
140
+ unarchive: vi.fn(async () => {}),
141
+ delete: vi.fn(async () => {}),
142
+ generateTitle: vi.fn(async () => new ReadableStream()),
143
+ fetch: vi.fn(async (threadId) => ({
144
+ status: "regular" as const,
145
+ remoteId: threadId,
146
+ externalId: threadId,
147
+ })),
148
+ });
149
+
150
+ describe("useStreamRuntime thread options", () => {
151
+ it("keeps stream options isolated between mounted threads", async () => {
152
+ mockUseStream.mockReturnValue(createMockStream());
153
+ const capture: { runtime: AssistantRuntime | null } = { runtime: null };
154
+ const threadListAdapter = makeThreadListAdapter();
155
+
156
+ const TestRuntime = () => {
157
+ const runtime = useStreamRuntime({
158
+ apiUrl: "/api",
159
+ unstable_threadListAdapter: threadListAdapter,
160
+ } as never);
161
+ capture.runtime = runtime;
162
+ return <AssistantRuntimeProvider runtime={runtime} />;
163
+ };
164
+
165
+ const view = render(<TestRuntime />);
166
+
167
+ await act(async () => {
168
+ await capture.runtime!.threads.switchToThread("thread-a");
169
+ });
170
+
171
+ const threadAOptions = mockUseStream.mock.calls
172
+ .map(([options]) => options as { threadId?: string | null })
173
+ .findLast((options) => options.threadId === "thread-a");
174
+ expect(threadAOptions).toBeDefined();
175
+
176
+ await act(async () => {
177
+ await capture.runtime!.threads.switchToThread("thread-b");
178
+ });
179
+
180
+ const threadBOptions = mockUseStream.mock.calls
181
+ .map(([options]) => options as { threadId?: string | null })
182
+ .findLast((options) => options.threadId === "thread-b");
183
+ expect(threadBOptions).toBeDefined();
184
+ expect(threadAOptions).not.toBe(threadBOptions);
185
+ expect(threadAOptions?.threadId).toBe("thread-a");
186
+
187
+ view.unmount();
188
+ });
189
+ });
190
+
113
191
  describe("useStreamRuntime staged messages", () => {
114
192
  it("stages a new user message without submitting when startRun is false", async () => {
115
193
  const stream = createMockStream([message("u1", "human", "earlier")]);
@@ -125,12 +125,11 @@ const useStreamThreadRuntime = (
125
125
  const externalId = useAuiState((s) => s.threadListItem.externalId) as
126
126
  | string
127
127
  | null;
128
- // Mutate in place rather than `{ ...options, threadId }`: spreading
129
- // `UseStreamOptions` (a discriminated union on `transport`) into an object
130
- // literal merges both arms' transport types, breaking arm assignment.
131
- options.threadId = externalId;
132
-
133
- const stream = useStream(options);
128
+ // Object.assign preserves the discriminated transport union; object spread
129
+ // collapses its arms and no longer satisfies UseStreamOptions.
130
+ const stream = useStream(
131
+ Object.assign({}, options, { threadId: externalId }),
132
+ );
134
133
  const [stagedMessages, setStagedMessages] = useState<
135
134
  LangChainBaseMessage[] | null
136
135
  >(null);