@assistant-ui/react-langchain 0.0.25 → 0.0.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useStreamRuntime.d.ts","names":[],"sources":["../src/useStreamRuntime.ts"],"mappings":";;;cAsCa,2BACX,WAAW;;kBAAA;;;;;;;;;
|
|
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"}
|
package/dist/useStreamRuntime.js
CHANGED
|
@@ -4,9 +4,9 @@ import { langChainExtras } from "./runtimeExtras.js";
|
|
|
4
4
|
import { foldUIUpdates, mergeUIMessages } from "./uiMessages.js";
|
|
5
5
|
import { resolveForkCheckpoint } from "./resolveForkCheckpoint.js";
|
|
6
6
|
import { useLangChainStreamingTiming } from "./streamingTiming.js";
|
|
7
|
-
import { useAuiState } from "@assistant-ui/store";
|
|
7
|
+
import { useAui, useAuiState } from "@assistant-ui/store";
|
|
8
8
|
import { useCloudThreadListAdapter, useExternalMessageConverter, useExternalStoreRuntime, useRemoteThreadListRuntime } from "@assistant-ui/core/react";
|
|
9
|
-
import { useEffect, useMemo, useRef, useState } from "react";
|
|
9
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
10
10
|
import { generateId, getExternalStoreMessages, pickExternalStoreSharedOptions } from "@assistant-ui/core";
|
|
11
11
|
import { STREAM_CONTROLLER, useChannel, useStream } from "@langchain/react";
|
|
12
12
|
//#region src/useStreamRuntime.ts
|
|
@@ -54,6 +54,7 @@ const truncateLangChainBaseMessages = (threadMessages, parentId) => {
|
|
|
54
54
|
};
|
|
55
55
|
const useStreamThreadRuntime = (options) => {
|
|
56
56
|
const { adapters, autoCancelPendingToolCalls, unstable_allowCancellation } = options;
|
|
57
|
+
const aui = useAui();
|
|
57
58
|
const messagesKey = options.messagesKey ?? "messages";
|
|
58
59
|
const uiStateKey = options.uiStateKey ?? "ui";
|
|
59
60
|
const externalId = useAuiState((s) => s.threadListItem.externalId);
|
|
@@ -83,6 +84,41 @@ const useStreamThreadRuntime = (options) => {
|
|
|
83
84
|
});
|
|
84
85
|
const streamRef = useRef(stream);
|
|
85
86
|
streamRef.current = stream;
|
|
87
|
+
const activeRunConfigRef = useRef(void 0);
|
|
88
|
+
const runConfigByMessageIdRef = useRef(/* @__PURE__ */ new Map());
|
|
89
|
+
const activeThreadIdRef = useRef(externalId);
|
|
90
|
+
const setActiveRunConfig = useCallback((runConfig) => {
|
|
91
|
+
activeRunConfigRef.current = runConfigToSubmitOptions(runConfig)?.config;
|
|
92
|
+
}, []);
|
|
93
|
+
const withActiveRunConfig = useCallback((submitOptions) => {
|
|
94
|
+
if (submitOptions && "config" in submitOptions) return submitOptions;
|
|
95
|
+
if (activeRunConfigRef.current === void 0) return submitOptions;
|
|
96
|
+
return {
|
|
97
|
+
...submitOptions,
|
|
98
|
+
config: activeRunConfigRef.current
|
|
99
|
+
};
|
|
100
|
+
}, []);
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
if (activeThreadIdRef.current !== null && activeThreadIdRef.current !== externalId) {
|
|
103
|
+
activeRunConfigRef.current = void 0;
|
|
104
|
+
runConfigByMessageIdRef.current.clear();
|
|
105
|
+
}
|
|
106
|
+
activeThreadIdRef.current = externalId;
|
|
107
|
+
}, [externalId]);
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
const messages = stream.messages;
|
|
110
|
+
const owned = runConfigByMessageIdRef.current;
|
|
111
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
112
|
+
const message = messages.at(i);
|
|
113
|
+
if (!message?.id || getMessageType(message) !== "ai" || !message.tool_calls?.length) continue;
|
|
114
|
+
if (owned.has(message.id)) return;
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
for (const message of messages) {
|
|
118
|
+
if (!message.id || getMessageType(message) !== "ai" || !message.tool_calls?.length || owned.has(message.id)) continue;
|
|
119
|
+
owned.set(message.id, activeRunConfigRef.current);
|
|
120
|
+
}
|
|
121
|
+
}, [stream.messages]);
|
|
86
122
|
const visibleMessagesRef = useRef(visibleMessages);
|
|
87
123
|
visibleMessagesRef.current = visibleMessages;
|
|
88
124
|
const threadMessagesRef = useRef(threadMessages);
|
|
@@ -143,12 +179,19 @@ const useStreamThreadRuntime = (options) => {
|
|
|
143
179
|
subgraphs: stream.subgraphs,
|
|
144
180
|
stream,
|
|
145
181
|
error: stream.error,
|
|
146
|
-
submit:
|
|
147
|
-
|
|
148
|
-
|
|
182
|
+
submit: (values, submitOptions) => {
|
|
183
|
+
const isResume = values == null || submitOptions?.command != null;
|
|
184
|
+
return stream.submit(values, isResume ? withActiveRunConfig(submitOptions) : submitOptions);
|
|
185
|
+
},
|
|
186
|
+
respond: (response, respondOptions) => stream.respond(response, withActiveRunConfig(respondOptions)),
|
|
187
|
+
respondAll: (responsesById, respondOptions) => stream.respondAll(responsesById, withActiveRunConfig(respondOptions)),
|
|
149
188
|
values: stream.values,
|
|
150
189
|
messagesKey
|
|
151
|
-
}), [
|
|
190
|
+
}), [
|
|
191
|
+
stream,
|
|
192
|
+
messagesKey,
|
|
193
|
+
withActiveRunConfig
|
|
194
|
+
]);
|
|
152
195
|
return useExternalStoreRuntime({
|
|
153
196
|
...pickExternalStoreSharedOptions(options),
|
|
154
197
|
isRunning: effectiveIsRunning,
|
|
@@ -163,6 +206,7 @@ const useStreamThreadRuntime = (options) => {
|
|
|
163
206
|
stageUserMessage(msg);
|
|
164
207
|
return;
|
|
165
208
|
}
|
|
209
|
+
setActiveRunConfig(msg.runConfig);
|
|
166
210
|
const content = getMessageContent(msg);
|
|
167
211
|
const cancellations = autoCancelPendingToolCalls !== false ? getPendingToolCalls(streamRef.current.messages).map((t) => ({
|
|
168
212
|
type: "tool",
|
|
@@ -171,12 +215,17 @@ const useStreamThreadRuntime = (options) => {
|
|
|
171
215
|
content: JSON.stringify({ cancelled: true }),
|
|
172
216
|
status: "error"
|
|
173
217
|
})) : [];
|
|
174
|
-
|
|
218
|
+
const { externalId } = await aui.threadListItem.initialize();
|
|
219
|
+
await streamRef.current.submit({ [messagesKey]: [...cancellations, {
|
|
175
220
|
type: "human",
|
|
176
221
|
content
|
|
177
|
-
}] },
|
|
222
|
+
}] }, {
|
|
223
|
+
...runConfigToSubmitOptions(msg.runConfig),
|
|
224
|
+
...externalId != null ? { threadId: externalId } : {}
|
|
225
|
+
});
|
|
178
226
|
},
|
|
179
|
-
onAddToolResult: async ({ toolCallId, toolName, result, isError, artifact }) => {
|
|
227
|
+
onAddToolResult: async ({ messageId, toolCallId, toolName, result, isError, artifact }) => {
|
|
228
|
+
const runConfig = runConfigByMessageIdRef.current.has(messageId) ? runConfigByMessageIdRef.current.get(messageId) : activeRunConfigRef.current;
|
|
180
229
|
await stream.submit({ [messagesKey]: [{
|
|
181
230
|
type: "tool",
|
|
182
231
|
name: toolName,
|
|
@@ -184,7 +233,7 @@ const useStreamThreadRuntime = (options) => {
|
|
|
184
233
|
content: JSON.stringify(result),
|
|
185
234
|
...artifact !== void 0 && { artifact },
|
|
186
235
|
status: isError ? "error" : "success"
|
|
187
|
-
}] });
|
|
236
|
+
}] }, runConfig === void 0 ? void 0 : { config: runConfig });
|
|
188
237
|
},
|
|
189
238
|
onReload: async (parentId, config) => {
|
|
190
239
|
const stagedRun = getStagedRun(parentId);
|
|
@@ -201,11 +250,13 @@ const useStreamThreadRuntime = (options) => {
|
|
|
201
250
|
visibleMessagesRef.current = nextMessages;
|
|
202
251
|
setStagedMessages(nextMessages);
|
|
203
252
|
} else setStagedMessages(null);
|
|
253
|
+
const runConfig = config.runConfig ?? stagedRun.runConfig;
|
|
254
|
+
setActiveRunConfig(runConfig);
|
|
204
255
|
await stream.submit({ [messagesKey]: stagedRun.messages.map((message) => ({
|
|
205
256
|
id: message.id,
|
|
206
257
|
type: "human",
|
|
207
258
|
content: message.content
|
|
208
|
-
})) }, runConfigToSubmitOptions(
|
|
259
|
+
})) }, runConfigToSubmitOptions(runConfig));
|
|
209
260
|
return;
|
|
210
261
|
}
|
|
211
262
|
const threadId = externalId;
|
|
@@ -213,6 +264,7 @@ const useStreamThreadRuntime = (options) => {
|
|
|
213
264
|
const s = streamRef.current;
|
|
214
265
|
const checkpointId = await resolveForkCheckpoint(s.client, threadId, s.messages, parentId, config.sourceId, s[STREAM_CONTROLLER]?.messageMetadataStore?.getSnapshot?.(), messagesKey);
|
|
215
266
|
if (!checkpointId) return;
|
|
267
|
+
setActiveRunConfig(config.runConfig);
|
|
216
268
|
await s.submit(null, {
|
|
217
269
|
forkFrom: checkpointId,
|
|
218
270
|
...runConfigToSubmitOptions(config.runConfig)
|
|
@@ -238,6 +290,7 @@ const useStreamThreadRuntime = (options) => {
|
|
|
238
290
|
const checkpointId = await resolveForkCheckpoint(s.client, threadId, s.messages, message.parentId, message.sourceId, s[STREAM_CONTROLLER]?.messageMetadataStore?.getSnapshot?.(), messagesKey);
|
|
239
291
|
if (!checkpointId) return;
|
|
240
292
|
const content = getMessageContent(message);
|
|
293
|
+
setActiveRunConfig(message.runConfig);
|
|
241
294
|
await s.submit({ [messagesKey]: [{
|
|
242
295
|
type: "human",
|
|
243
296
|
content
|
|
@@ -247,6 +300,7 @@ const useStreamThreadRuntime = (options) => {
|
|
|
247
300
|
});
|
|
248
301
|
},
|
|
249
302
|
onCancel: unstable_allowCancellation !== false ? async () => {
|
|
303
|
+
activeRunConfigRef.current = void 0;
|
|
250
304
|
await stream.stop();
|
|
251
305
|
} : void 0
|
|
252
306
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useStreamRuntime.js","names":[],"sources":["../src/useStreamRuntime.ts"],"sourcesContent":["/// <reference types=\"@assistant-ui/core/store\" />\n\"use client\";\n\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport type { AppendMessage, ToolExecutionStatus } from \"@assistant-ui/core\";\nimport {\n generateId,\n getExternalStoreMessages,\n pickExternalStoreSharedOptions,\n} from \"@assistant-ui/core\";\nimport type { ThreadMessage } from \"@assistant-ui/core\";\nimport {\n useCloudThreadListAdapter,\n useExternalStoreRuntime,\n useExternalMessageConverter,\n useRemoteThreadListRuntime,\n} from \"@assistant-ui/core/react\";\nimport { useAuiState } from \"@assistant-ui/store\";\nimport { STREAM_CONTROLLER, useChannel, useStream } from \"@langchain/react\";\nimport type { Channel } from \"@langchain/react\";\nimport type {\n LangChainBaseMessage,\n LangChainToolCall,\n UIMessage,\n UseStreamRuntimeOptions,\n} from \"./types\";\nimport {\n convertLangChainBaseMessage,\n getMessageContent,\n getMessageType,\n} from \"./convertMessages\";\nimport { foldUIUpdates, mergeUIMessages } from \"./uiMessages\";\nimport { langChainExtras } from \"./runtimeExtras\";\nimport { resolveForkCheckpoint } from \"./resolveForkCheckpoint\";\nimport { useLangChainStreamingTiming } from \"./streamingTiming\";\n\nconst UI_CUSTOM_CHANNELS: readonly Channel[] = [\"custom\"];\n\nexport const runConfigToSubmitOptions = (\n runConfig: AppendMessage[\"runConfig\"],\n) =>\n runConfig?.custom\n ? { config: { configurable: runConfig.custom } }\n : undefined;\n\n/**\n * Group the graph's accumulated `UIMessage`s by the assistant message they\n * belong to. Non-array state and entries without a parent link are dropped.\n * The parent id comes from `metadata.message_id` (Python SDK) or\n * `metadata.id` (JS SDK).\n */\nexport const groupUIMessagesByParent = (\n value: unknown,\n): Map<string, UIMessage[]> => {\n const map = new Map<string, UIMessage[]>();\n if (!Array.isArray(value)) return map;\n for (const ui of value as UIMessage[]) {\n const parentId = ui.metadata?.message_id ?? ui.metadata?.id;\n if (!parentId) continue;\n const existing = map.get(parentId);\n if (existing) {\n existing.push(ui);\n } else {\n map.set(parentId, [ui]);\n }\n }\n return map;\n};\n\nconst getPendingToolCalls = (\n messages: readonly LangChainBaseMessage[],\n): LangChainToolCall[] => {\n const pending = new Map<string, LangChainToolCall>();\n for (const m of messages) {\n const type = getMessageType(m);\n if (type === \"ai\") {\n for (const tc of m.tool_calls ?? []) pending.set(tc.id, tc);\n } else if (type === \"tool\" && m.tool_call_id) {\n pending.delete(m.tool_call_id);\n }\n }\n return [...pending.values()];\n};\n\nconst toStagedHumanMessage = (\n msg: AppendMessage,\n id = generateId(),\n): LangChainBaseMessage & { id: string } => ({\n id,\n _getType: () => \"human\",\n content: getMessageContent(msg),\n});\n\nconst truncateLangChainBaseMessages = (\n threadMessages: readonly ThreadMessage[],\n parentId: string | null,\n): LangChainBaseMessage[] => {\n if (parentId === null) return [];\n const parentIndex = threadMessages.findIndex((m) => m.id === parentId);\n if (parentIndex === -1) return [];\n const truncated: LangChainBaseMessage[] = [];\n for (let i = 0; i <= parentIndex && i < threadMessages.length; i++) {\n truncated.push(\n ...getExternalStoreMessages<LangChainBaseMessage>(threadMessages[i]!),\n );\n }\n return truncated;\n};\n\ntype DistributiveOmit<T, K extends keyof any> = T extends unknown\n ? Omit<T, K>\n : never;\n\nconst useStreamThreadRuntime = (\n options: DistributiveOmit<\n UseStreamRuntimeOptions,\n \"cloud\" | \"unstable_threadListAdapter\" | \"create\" | \"delete\"\n >,\n) => {\n const { adapters, autoCancelPendingToolCalls, unstable_allowCancellation } =\n options;\n const messagesKey = options.messagesKey ?? \"messages\";\n const uiStateKey = options.uiStateKey ?? \"ui\";\n\n const externalId = useAuiState((s) => s.threadListItem.externalId) as\n | string\n | null;\n // Object.assign preserves the discriminated transport union; object spread\n // collapses its arms and no longer satisfies UseStreamOptions.\n const stream = useStream(\n Object.assign({}, options, { threadId: externalId }),\n );\n const [stagedMessages, setStagedMessages] = useState<\n LangChainBaseMessage[] | null\n >(null);\n\n const [toolStatuses, setToolStatuses] = useState<\n Record<string, ToolExecutionStatus>\n >({});\n const hasExecutingTools = Object.values(toolStatuses).some(\n (s) => s?.type === \"executing\",\n );\n const effectiveIsRunning = stream.isLoading || hasExecutingTools;\n\n const uiStateValue = stream.values[uiStateKey];\n\n const customEvents = useChannel(stream, UI_CUSTOM_CHANNELS);\n const liveUiMessages = useMemo(\n () => foldUIUpdates(customEvents),\n [customEvents],\n );\n\n const mergedUiMessages = useMemo(\n () => mergeUIMessages(liveUiMessages, uiStateValue),\n [liveUiMessages, uiStateValue],\n );\n\n const visibleMessages =\n stagedMessages ?? (stream.messages as LangChainBaseMessage[]);\n\n const messageTiming = useLangChainStreamingTiming(\n visibleMessages,\n effectiveIsRunning,\n );\n\n const convertWithUI = useMemo<\n useExternalMessageConverter.Callback<LangChainBaseMessage>\n >(() => {\n const uiMessagesByParent = groupUIMessagesByParent(mergedUiMessages);\n return (message, metadata) =>\n convertLangChainBaseMessage(message, {\n ...metadata,\n uiMessagesByParent,\n messageTiming,\n });\n }, [mergedUiMessages, messageTiming]);\n\n const threadMessages = useExternalMessageConverter({\n callback: convertWithUI,\n messages: visibleMessages,\n isRunning: effectiveIsRunning,\n });\n\n const streamRef = useRef(stream);\n streamRef.current = stream;\n\n const visibleMessagesRef = useRef(visibleMessages);\n visibleMessagesRef.current = visibleMessages;\n\n const threadMessagesRef = useRef(threadMessages);\n threadMessagesRef.current = threadMessages;\n\n const stagedMessagesRef = useRef(\n new Map<\n string,\n {\n message: LangChainBaseMessage & { id: string };\n runConfig: AppendMessage[\"runConfig\"];\n }\n >(),\n );\n const stagedBaseMessagesRef = useRef<LangChainBaseMessage[] | null>(null);\n\n useEffect(() => {\n if (stagedMessagesRef.current.size === 0) return;\n\n // Staged edits must keep their truncated base while stream updates arrive before promotion.\n const baseMessages =\n stagedBaseMessagesRef.current ??\n (stream.messages as LangChainBaseMessage[]);\n const baseMessageIds = new Set(\n baseMessages.flatMap((message) => (message.id ? [message.id] : [])),\n );\n const remainingStagedMessages: LangChainBaseMessage[] = [];\n const seenStagedIds = new Set<string>();\n for (const message of visibleMessagesRef.current) {\n if (!message.id || seenStagedIds.has(message.id)) continue;\n if (baseMessageIds.has(message.id)) continue;\n const staged = stagedMessagesRef.current.get(message.id);\n if (!staged) continue;\n remainingStagedMessages.push(staged.message);\n seenStagedIds.add(message.id);\n }\n\n if (remainingStagedMessages.length === 0) {\n stagedBaseMessagesRef.current = null;\n visibleMessagesRef.current = baseMessages;\n setStagedMessages(null);\n return;\n }\n\n const nextMessages = [...baseMessages, ...remainingStagedMessages];\n visibleMessagesRef.current = nextMessages;\n setStagedMessages(nextMessages);\n }, [stream.messages]);\n\n const getStagedRun = (parentId: string | null) => {\n if (!parentId || !stagedMessagesRef.current.has(parentId)) return null;\n\n const staged: LangChainBaseMessage[] = [];\n for (const message of visibleMessagesRef.current) {\n if (message.id && stagedMessagesRef.current.has(message.id)) {\n staged.push(stagedMessagesRef.current.get(message.id)!.message);\n }\n if (message.id === parentId) break;\n }\n\n return {\n messages: staged,\n runConfig: stagedMessagesRef.current.get(parentId)!.runConfig,\n };\n };\n\n const stageUserMessage = (msg: AppendMessage) => {\n const stagedMessage = toStagedHumanMessage(msg);\n stagedMessagesRef.current.set(stagedMessage.id, {\n message: stagedMessage,\n runConfig: msg.runConfig,\n });\n const nextMessages = [...visibleMessagesRef.current, stagedMessage];\n visibleMessagesRef.current = nextMessages;\n setStagedMessages(nextMessages);\n };\n\n const extras = useMemo(\n () =>\n langChainExtras.provide({\n interrupt: stream.interrupt,\n interrupts: stream.interrupts,\n toolCalls: stream.toolCalls,\n subagents: stream.subagents,\n subgraphs: stream.subgraphs,\n stream,\n error: stream.error,\n submit: stream.submit,\n respond: stream.respond,\n respondAll: stream.respondAll,\n values: stream.values,\n messagesKey,\n }),\n [stream, messagesKey],\n );\n\n const runtime = useExternalStoreRuntime({\n ...pickExternalStoreSharedOptions(options),\n isRunning: effectiveIsRunning,\n isLoading: stream.isThreadLoading,\n messages: threadMessages,\n adapters,\n extras,\n unstable_enableToolInvocations: true,\n setToolStatuses,\n onNew: async (msg) => {\n if (!(msg.startRun ?? msg.role === \"user\")) {\n stageUserMessage(msg);\n return;\n }\n\n const content = getMessageContent(msg);\n const cancellations =\n autoCancelPendingToolCalls !== false\n ? getPendingToolCalls(\n streamRef.current.messages as readonly LangChainBaseMessage[],\n ).map((t) => ({\n type: \"tool\" as const,\n name: t.name,\n tool_call_id: t.id,\n content: JSON.stringify({ cancelled: true }),\n status: \"error\" as const,\n }))\n : [];\n await stream.submit(\n { [messagesKey]: [...cancellations, { type: \"human\", content }] },\n runConfigToSubmitOptions(msg.runConfig),\n );\n },\n onAddToolResult: async ({\n toolCallId,\n toolName,\n result,\n isError,\n artifact,\n }) => {\n await stream.submit({\n [messagesKey]: [\n {\n type: \"tool\",\n name: toolName,\n tool_call_id: toolCallId,\n content: JSON.stringify(result),\n ...(artifact !== undefined && { artifact }),\n status: isError ? \"error\" : \"success\",\n },\n ],\n });\n },\n onReload: async (parentId, config) => {\n const stagedRun = getStagedRun(parentId);\n if (stagedRun) {\n const promotedIds = new Set<string>();\n for (const message of stagedRun.messages) {\n if (!message.id) continue;\n promotedIds.add(message.id);\n stagedMessagesRef.current.delete(message.id);\n }\n stagedBaseMessagesRef.current = null;\n if (stagedMessagesRef.current.size > 0) {\n const nextMessages = visibleMessagesRef.current.filter(\n (message) => !message.id || !promotedIds.has(message.id),\n );\n visibleMessagesRef.current = nextMessages;\n setStagedMessages(nextMessages);\n } else {\n setStagedMessages(null);\n }\n await stream.submit(\n {\n [messagesKey]: stagedRun.messages.map((message) => ({\n id: message.id,\n type: \"human\",\n content: message.content,\n })),\n },\n runConfigToSubmitOptions(config.runConfig ?? stagedRun.runConfig),\n );\n return;\n }\n\n const threadId = externalId;\n if (!threadId || parentId == null) return;\n const s = streamRef.current;\n const checkpointId = await resolveForkCheckpoint(\n s.client,\n threadId,\n s.messages as readonly LangChainBaseMessage[],\n parentId,\n config.sourceId,\n s[STREAM_CONTROLLER]?.messageMetadataStore?.getSnapshot?.(),\n messagesKey,\n );\n if (!checkpointId) return;\n await s.submit(null, {\n forkFrom: checkpointId,\n ...runConfigToSubmitOptions(config.runConfig),\n });\n },\n onEdit: async (message) => {\n if (!(message.startRun ?? message.role === \"user\")) {\n const truncated = truncateLangChainBaseMessages(\n threadMessagesRef.current,\n message.parentId,\n );\n const stagedMessage = toStagedHumanMessage(message);\n stagedMessagesRef.current.set(stagedMessage.id, {\n message: stagedMessage,\n runConfig: message.runConfig,\n });\n stagedBaseMessagesRef.current = truncated;\n const nextMessages = [...truncated, stagedMessage];\n visibleMessagesRef.current = nextMessages;\n setStagedMessages(nextMessages);\n return;\n }\n\n const threadId = externalId;\n if (!threadId) return;\n const s = streamRef.current;\n const checkpointId = await resolveForkCheckpoint(\n s.client,\n threadId,\n s.messages as readonly LangChainBaseMessage[],\n message.parentId,\n message.sourceId,\n s[STREAM_CONTROLLER]?.messageMetadataStore?.getSnapshot?.(),\n messagesKey,\n );\n if (!checkpointId) return;\n const content = getMessageContent(message);\n await s.submit(\n { [messagesKey]: [{ type: \"human\", content }] },\n {\n forkFrom: checkpointId,\n ...runConfigToSubmitOptions(message.runConfig),\n },\n );\n },\n onCancel:\n unstable_allowCancellation !== false\n ? async () => {\n await stream.stop();\n }\n : undefined,\n });\n\n return runtime;\n};\n\n/**\n * Creates an assistant-ui runtime backed by LangChain's `useStream` hook.\n * Accepts the same options as `useStream` from `@langchain/react`, plus\n * `cloud` and `adapters`.\n *\n * @example\n * ```tsx\n * import { useStreamRuntime } from \"@assistant-ui/react-langchain\";\n * import { AssistantRuntimeProvider, Thread } from \"@assistant-ui/react\";\n *\n * function App() {\n * const runtime = useStreamRuntime({\n * assistantId: \"agent\",\n * apiUrl: \"http://localhost:2024\",\n * });\n *\n * return (\n * <AssistantRuntimeProvider runtime={runtime}>\n * <Thread />\n * </AssistantRuntimeProvider>\n * );\n * }\n * ```\n */\nexport const useStreamRuntime = (rawOptions: UseStreamRuntimeOptions) => {\n const {\n cloud,\n unstable_threadListAdapter,\n create,\n delete: deleteFn,\n onThreadIdChange,\n ...options\n } = rawOptions;\n\n const optionsRef = useRef(options);\n optionsRef.current = options;\n\n const cloudAdapter = useCloudThreadListAdapter({\n cloud,\n create,\n delete: deleteFn,\n });\n const adapter = unstable_threadListAdapter ?? cloudAdapter;\n\n return useRemoteThreadListRuntime({\n runtimeHook: function RuntimeHook() {\n return useStreamThreadRuntime(optionsRef.current);\n },\n adapter,\n allowNesting: true,\n onThreadIdChange,\n });\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useStreamRuntime.js","names":[],"sources":["../src/useStreamRuntime.ts"],"sourcesContent":["/// <reference types=\"@assistant-ui/core/store\" />\n\"use client\";\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport type { AppendMessage, ToolExecutionStatus } from \"@assistant-ui/core\";\nimport {\n generateId,\n getExternalStoreMessages,\n pickExternalStoreSharedOptions,\n} from \"@assistant-ui/core\";\nimport type { ThreadMessage } from \"@assistant-ui/core\";\nimport {\n useCloudThreadListAdapter,\n useExternalStoreRuntime,\n useExternalMessageConverter,\n useRemoteThreadListRuntime,\n} from \"@assistant-ui/core/react\";\nimport { useAui, useAuiState } from \"@assistant-ui/store\";\nimport { STREAM_CONTROLLER, useChannel, useStream } from \"@langchain/react\";\nimport type { Channel } from \"@langchain/react\";\nimport type {\n LangChainBaseMessage,\n LangChainToolCall,\n UIMessage,\n UseStreamRuntimeOptions,\n} from \"./types\";\nimport {\n convertLangChainBaseMessage,\n getMessageContent,\n getMessageType,\n} from \"./convertMessages\";\nimport { foldUIUpdates, mergeUIMessages } from \"./uiMessages\";\nimport { langChainExtras } from \"./runtimeExtras\";\nimport { resolveForkCheckpoint } from \"./resolveForkCheckpoint\";\nimport { useLangChainStreamingTiming } from \"./streamingTiming\";\n\nconst UI_CUSTOM_CHANNELS: readonly Channel[] = [\"custom\"];\n\nexport const runConfigToSubmitOptions = (\n runConfig: AppendMessage[\"runConfig\"],\n) =>\n runConfig?.custom\n ? { config: { configurable: runConfig.custom } }\n : undefined;\n\ntype NormalizedRunConfigOptions = NonNullable<\n ReturnType<typeof runConfigToSubmitOptions>\n>;\n\n/**\n * Group the graph's accumulated `UIMessage`s by the assistant message they\n * belong to. Non-array state and entries without a parent link are dropped.\n * The parent id comes from `metadata.message_id` (Python SDK) or\n * `metadata.id` (JS SDK).\n */\nexport const groupUIMessagesByParent = (\n value: unknown,\n): Map<string, UIMessage[]> => {\n const map = new Map<string, UIMessage[]>();\n if (!Array.isArray(value)) return map;\n for (const ui of value as UIMessage[]) {\n const parentId = ui.metadata?.message_id ?? ui.metadata?.id;\n if (!parentId) continue;\n const existing = map.get(parentId);\n if (existing) {\n existing.push(ui);\n } else {\n map.set(parentId, [ui]);\n }\n }\n return map;\n};\n\nconst getPendingToolCalls = (\n messages: readonly LangChainBaseMessage[],\n): LangChainToolCall[] => {\n const pending = new Map<string, LangChainToolCall>();\n for (const m of messages) {\n const type = getMessageType(m);\n if (type === \"ai\") {\n for (const tc of m.tool_calls ?? []) pending.set(tc.id, tc);\n } else if (type === \"tool\" && m.tool_call_id) {\n pending.delete(m.tool_call_id);\n }\n }\n return [...pending.values()];\n};\n\nconst toStagedHumanMessage = (\n msg: AppendMessage,\n id = generateId(),\n): LangChainBaseMessage & { id: string } => ({\n id,\n _getType: () => \"human\",\n content: getMessageContent(msg),\n});\n\nconst truncateLangChainBaseMessages = (\n threadMessages: readonly ThreadMessage[],\n parentId: string | null,\n): LangChainBaseMessage[] => {\n if (parentId === null) return [];\n const parentIndex = threadMessages.findIndex((m) => m.id === parentId);\n if (parentIndex === -1) return [];\n const truncated: LangChainBaseMessage[] = [];\n for (let i = 0; i <= parentIndex && i < threadMessages.length; i++) {\n truncated.push(\n ...getExternalStoreMessages<LangChainBaseMessage>(threadMessages[i]!),\n );\n }\n return truncated;\n};\n\ntype DistributiveOmit<T, K extends keyof any> = T extends unknown\n ? Omit<T, K>\n : never;\n\nconst useStreamThreadRuntime = (\n options: DistributiveOmit<\n UseStreamRuntimeOptions,\n \"cloud\" | \"unstable_threadListAdapter\" | \"create\" | \"delete\"\n >,\n) => {\n const { adapters, autoCancelPendingToolCalls, unstable_allowCancellation } =\n options;\n const aui = useAui();\n const messagesKey = options.messagesKey ?? \"messages\";\n const uiStateKey = options.uiStateKey ?? \"ui\";\n\n const externalId = useAuiState((s) => s.threadListItem.externalId) as\n | string\n | null;\n // Object.assign preserves the discriminated transport union; object spread\n // collapses its arms and no longer satisfies UseStreamOptions.\n const stream = useStream(\n Object.assign({}, options, { threadId: externalId }),\n );\n const [stagedMessages, setStagedMessages] = useState<\n LangChainBaseMessage[] | null\n >(null);\n\n const [toolStatuses, setToolStatuses] = useState<\n Record<string, ToolExecutionStatus>\n >({});\n const hasExecutingTools = Object.values(toolStatuses).some(\n (s) => s?.type === \"executing\",\n );\n const effectiveIsRunning = stream.isLoading || hasExecutingTools;\n\n const uiStateValue = stream.values[uiStateKey];\n\n const customEvents = useChannel(stream, UI_CUSTOM_CHANNELS);\n const liveUiMessages = useMemo(\n () => foldUIUpdates(customEvents),\n [customEvents],\n );\n\n const mergedUiMessages = useMemo(\n () => mergeUIMessages(liveUiMessages, uiStateValue),\n [liveUiMessages, uiStateValue],\n );\n\n const visibleMessages =\n stagedMessages ?? (stream.messages as LangChainBaseMessage[]);\n\n const messageTiming = useLangChainStreamingTiming(\n visibleMessages,\n effectiveIsRunning,\n );\n\n const convertWithUI = useMemo<\n useExternalMessageConverter.Callback<LangChainBaseMessage>\n >(() => {\n const uiMessagesByParent = groupUIMessagesByParent(mergedUiMessages);\n return (message, metadata) =>\n convertLangChainBaseMessage(message, {\n ...metadata,\n uiMessagesByParent,\n messageTiming,\n });\n }, [mergedUiMessages, messageTiming]);\n\n const threadMessages = useExternalMessageConverter({\n callback: convertWithUI,\n messages: visibleMessages,\n isRunning: effectiveIsRunning,\n });\n\n const streamRef = useRef(stream);\n streamRef.current = stream;\n\n const activeRunConfigRef = useRef<\n NormalizedRunConfigOptions[\"config\"] | undefined\n >(undefined);\n const runConfigByMessageIdRef = useRef(\n new Map<string, NormalizedRunConfigOptions[\"config\"] | undefined>(),\n );\n const activeThreadIdRef = useRef(externalId);\n const setActiveRunConfig = useCallback(\n (runConfig: AppendMessage[\"runConfig\"]) => {\n activeRunConfigRef.current = runConfigToSubmitOptions(runConfig)?.config;\n },\n [],\n );\n const withActiveRunConfig = useCallback(\n (submitOptions?: Record<string, unknown>) => {\n if (submitOptions && \"config\" in submitOptions) return submitOptions;\n if (activeRunConfigRef.current === undefined) return submitOptions;\n return { ...submitOptions, config: activeRunConfigRef.current };\n },\n [],\n );\n\n useEffect(() => {\n if (\n activeThreadIdRef.current !== null &&\n activeThreadIdRef.current !== externalId\n ) {\n activeRunConfigRef.current = undefined;\n runConfigByMessageIdRef.current.clear();\n }\n activeThreadIdRef.current = externalId;\n }, [externalId]);\n\n useEffect(() => {\n const messages = stream.messages as readonly LangChainBaseMessage[];\n const owned = runConfigByMessageIdRef.current;\n for (let i = messages.length - 1; i >= 0; i--) {\n const message = messages.at(i);\n if (\n !message?.id ||\n getMessageType(message) !== \"ai\" ||\n !message.tool_calls?.length\n ) {\n continue;\n }\n if (owned.has(message.id)) return;\n break;\n }\n for (const message of messages) {\n if (\n !message.id ||\n getMessageType(message) !== \"ai\" ||\n !message.tool_calls?.length ||\n owned.has(message.id)\n ) {\n continue;\n }\n owned.set(message.id, activeRunConfigRef.current);\n }\n }, [stream.messages]);\n\n const visibleMessagesRef = useRef(visibleMessages);\n visibleMessagesRef.current = visibleMessages;\n\n const threadMessagesRef = useRef(threadMessages);\n threadMessagesRef.current = threadMessages;\n\n const stagedMessagesRef = useRef(\n new Map<\n string,\n {\n message: LangChainBaseMessage & { id: string };\n runConfig: AppendMessage[\"runConfig\"];\n }\n >(),\n );\n const stagedBaseMessagesRef = useRef<LangChainBaseMessage[] | null>(null);\n\n useEffect(() => {\n if (stagedMessagesRef.current.size === 0) return;\n\n // Staged edits must keep their truncated base while stream updates arrive before promotion.\n const baseMessages =\n stagedBaseMessagesRef.current ??\n (stream.messages as LangChainBaseMessage[]);\n const baseMessageIds = new Set(\n baseMessages.flatMap((message) => (message.id ? [message.id] : [])),\n );\n const remainingStagedMessages: LangChainBaseMessage[] = [];\n const seenStagedIds = new Set<string>();\n for (const message of visibleMessagesRef.current) {\n if (!message.id || seenStagedIds.has(message.id)) continue;\n if (baseMessageIds.has(message.id)) continue;\n const staged = stagedMessagesRef.current.get(message.id);\n if (!staged) continue;\n remainingStagedMessages.push(staged.message);\n seenStagedIds.add(message.id);\n }\n\n if (remainingStagedMessages.length === 0) {\n stagedBaseMessagesRef.current = null;\n visibleMessagesRef.current = baseMessages;\n setStagedMessages(null);\n return;\n }\n\n const nextMessages = [...baseMessages, ...remainingStagedMessages];\n visibleMessagesRef.current = nextMessages;\n setStagedMessages(nextMessages);\n }, [stream.messages]);\n\n const getStagedRun = (parentId: string | null) => {\n if (!parentId || !stagedMessagesRef.current.has(parentId)) return null;\n\n const staged: LangChainBaseMessage[] = [];\n for (const message of visibleMessagesRef.current) {\n if (message.id && stagedMessagesRef.current.has(message.id)) {\n staged.push(stagedMessagesRef.current.get(message.id)!.message);\n }\n if (message.id === parentId) break;\n }\n\n return {\n messages: staged,\n runConfig: stagedMessagesRef.current.get(parentId)!.runConfig,\n };\n };\n\n const stageUserMessage = (msg: AppendMessage) => {\n const stagedMessage = toStagedHumanMessage(msg);\n stagedMessagesRef.current.set(stagedMessage.id, {\n message: stagedMessage,\n runConfig: msg.runConfig,\n });\n const nextMessages = [...visibleMessagesRef.current, stagedMessage];\n visibleMessagesRef.current = nextMessages;\n setStagedMessages(nextMessages);\n };\n\n const extras = useMemo(\n () =>\n langChainExtras.provide({\n interrupt: stream.interrupt,\n interrupts: stream.interrupts,\n toolCalls: stream.toolCalls,\n subagents: stream.subagents,\n subgraphs: stream.subgraphs,\n stream,\n error: stream.error,\n submit: (values, submitOptions) => {\n const isResume = values == null || submitOptions?.command != null;\n return stream.submit(\n values,\n isResume ? withActiveRunConfig(submitOptions) : submitOptions,\n );\n },\n respond: (response, respondOptions) =>\n stream.respond(response, withActiveRunConfig(respondOptions)),\n respondAll: (responsesById, respondOptions) =>\n stream.respondAll(responsesById, withActiveRunConfig(respondOptions)),\n values: stream.values,\n messagesKey,\n }),\n [stream, messagesKey, withActiveRunConfig],\n );\n\n const runtime = useExternalStoreRuntime({\n ...pickExternalStoreSharedOptions(options),\n isRunning: effectiveIsRunning,\n isLoading: stream.isThreadLoading,\n messages: threadMessages,\n adapters,\n extras,\n unstable_enableToolInvocations: true,\n setToolStatuses,\n onNew: async (msg) => {\n if (!(msg.startRun ?? msg.role === \"user\")) {\n stageUserMessage(msg);\n return;\n }\n\n setActiveRunConfig(msg.runConfig);\n const content = getMessageContent(msg);\n const cancellations =\n autoCancelPendingToolCalls !== false\n ? getPendingToolCalls(\n streamRef.current.messages as readonly LangChainBaseMessage[],\n ).map((t) => ({\n type: \"tool\" as const,\n name: t.name,\n tool_call_id: t.id,\n content: JSON.stringify({ cancelled: true }),\n status: \"error\" as const,\n }))\n : [];\n // A null threadId is not a no-op for the SDK: it rebinds the controller\n // away from its self-created thread and forces a fresh one, so the\n // submit waits for initialization to produce an identity; core no\n // longer holds appends on that barrier.\n const { externalId } = await aui.threadListItem.initialize();\n await streamRef.current.submit(\n { [messagesKey]: [...cancellations, { type: \"human\", content }] },\n {\n ...runConfigToSubmitOptions(msg.runConfig),\n ...(externalId != null ? { threadId: externalId } : {}),\n },\n );\n },\n onAddToolResult: async ({\n messageId,\n toolCallId,\n toolName,\n result,\n isError,\n artifact,\n }) => {\n const runConfig = runConfigByMessageIdRef.current.has(messageId)\n ? runConfigByMessageIdRef.current.get(messageId)\n : activeRunConfigRef.current;\n await stream.submit(\n {\n [messagesKey]: [\n {\n type: \"tool\",\n name: toolName,\n tool_call_id: toolCallId,\n content: JSON.stringify(result),\n ...(artifact !== undefined && { artifact }),\n status: isError ? \"error\" : \"success\",\n },\n ],\n },\n runConfig === undefined ? undefined : { config: runConfig },\n );\n },\n onReload: async (parentId, config) => {\n const stagedRun = getStagedRun(parentId);\n if (stagedRun) {\n const promotedIds = new Set<string>();\n for (const message of stagedRun.messages) {\n if (!message.id) continue;\n promotedIds.add(message.id);\n stagedMessagesRef.current.delete(message.id);\n }\n stagedBaseMessagesRef.current = null;\n if (stagedMessagesRef.current.size > 0) {\n const nextMessages = visibleMessagesRef.current.filter(\n (message) => !message.id || !promotedIds.has(message.id),\n );\n visibleMessagesRef.current = nextMessages;\n setStagedMessages(nextMessages);\n } else {\n setStagedMessages(null);\n }\n const runConfig = config.runConfig ?? stagedRun.runConfig;\n setActiveRunConfig(runConfig);\n await stream.submit(\n {\n [messagesKey]: stagedRun.messages.map((message) => ({\n id: message.id,\n type: \"human\",\n content: message.content,\n })),\n },\n runConfigToSubmitOptions(runConfig),\n );\n return;\n }\n\n const threadId = externalId;\n if (!threadId || parentId == null) return;\n const s = streamRef.current;\n const checkpointId = await resolveForkCheckpoint(\n s.client,\n threadId,\n s.messages as readonly LangChainBaseMessage[],\n parentId,\n config.sourceId,\n s[STREAM_CONTROLLER]?.messageMetadataStore?.getSnapshot?.(),\n messagesKey,\n );\n if (!checkpointId) return;\n setActiveRunConfig(config.runConfig);\n await s.submit(null, {\n forkFrom: checkpointId,\n ...runConfigToSubmitOptions(config.runConfig),\n });\n },\n onEdit: async (message) => {\n if (!(message.startRun ?? message.role === \"user\")) {\n const truncated = truncateLangChainBaseMessages(\n threadMessagesRef.current,\n message.parentId,\n );\n const stagedMessage = toStagedHumanMessage(message);\n stagedMessagesRef.current.set(stagedMessage.id, {\n message: stagedMessage,\n runConfig: message.runConfig,\n });\n stagedBaseMessagesRef.current = truncated;\n const nextMessages = [...truncated, stagedMessage];\n visibleMessagesRef.current = nextMessages;\n setStagedMessages(nextMessages);\n return;\n }\n\n const threadId = externalId;\n if (!threadId) return;\n const s = streamRef.current;\n const checkpointId = await resolveForkCheckpoint(\n s.client,\n threadId,\n s.messages as readonly LangChainBaseMessage[],\n message.parentId,\n message.sourceId,\n s[STREAM_CONTROLLER]?.messageMetadataStore?.getSnapshot?.(),\n messagesKey,\n );\n if (!checkpointId) return;\n const content = getMessageContent(message);\n setActiveRunConfig(message.runConfig);\n await s.submit(\n { [messagesKey]: [{ type: \"human\", content }] },\n {\n forkFrom: checkpointId,\n ...runConfigToSubmitOptions(message.runConfig),\n },\n );\n },\n onCancel:\n unstable_allowCancellation !== false\n ? async () => {\n activeRunConfigRef.current = undefined;\n await stream.stop();\n }\n : undefined,\n });\n\n return runtime;\n};\n\n/**\n * Creates an assistant-ui runtime backed by LangChain's `useStream` hook.\n * Accepts the same options as `useStream` from `@langchain/react`, plus\n * `cloud` and `adapters`.\n *\n * @example\n * ```tsx\n * import { useStreamRuntime } from \"@assistant-ui/react-langchain\";\n * import { AssistantRuntimeProvider, Thread } from \"@assistant-ui/react\";\n *\n * function App() {\n * const runtime = useStreamRuntime({\n * assistantId: \"agent\",\n * apiUrl: \"http://localhost:2024\",\n * });\n *\n * return (\n * <AssistantRuntimeProvider runtime={runtime}>\n * <Thread />\n * </AssistantRuntimeProvider>\n * );\n * }\n * ```\n */\nexport const useStreamRuntime = (rawOptions: UseStreamRuntimeOptions) => {\n const {\n cloud,\n unstable_threadListAdapter,\n create,\n delete: deleteFn,\n onThreadIdChange,\n ...options\n } = rawOptions;\n\n const optionsRef = useRef(options);\n optionsRef.current = options;\n\n const cloudAdapter = useCloudThreadListAdapter({\n cloud,\n create,\n delete: deleteFn,\n });\n const adapter = unstable_threadListAdapter ?? cloudAdapter;\n\n return useRemoteThreadListRuntime({\n runtimeHook: function RuntimeHook() {\n return useStreamThreadRuntime(optionsRef.current);\n },\n adapter,\n allowNesting: true,\n onThreadIdChange,\n });\n};\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@assistant-ui/react-langchain",
|
|
3
|
-
"version": "0.0.
|
|
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.
|
|
33
|
-
"@assistant-ui/store": "^0.3.
|
|
32
|
+
"@assistant-ui/core": "^0.3.14",
|
|
33
|
+
"@assistant-ui/store": "^0.3.10",
|
|
34
34
|
"assistant-cloud": "*",
|
|
35
|
-
"assistant-stream": "^0.3.
|
|
35
|
+
"assistant-stream": "^0.3.38"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"@langchain/react": "^1.0.2",
|
|
@@ -46,14 +46,14 @@
|
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@langchain/core": "^1.2.5",
|
|
49
|
-
"@langchain/langgraph-sdk": "^1.9.
|
|
50
|
-
"@langchain/react": "^1.0.
|
|
49
|
+
"@langchain/langgraph-sdk": "^1.9.29",
|
|
50
|
+
"@langchain/react": "^1.0.30",
|
|
51
51
|
"@testing-library/react": "^16.3.2",
|
|
52
52
|
"@types/react": "^19.2.18",
|
|
53
53
|
"jsdom": "^30.0.1",
|
|
54
54
|
"react": "^19.2.8",
|
|
55
55
|
"vitest": "^4.1.10",
|
|
56
|
-
"@assistant-ui/x-buildutils": "0.0.
|
|
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", () => {
|
package/src/useStreamRuntime.ts
CHANGED
|
@@ -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:
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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(
|
|
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,
|