@assistant-ui/react-langchain 0.0.24 → 0.0.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/streamingTiming.js +1 -3
- package/dist/streamingTiming.js.map +1 -1
- package/dist/useStreamRuntime.d.ts.map +1 -1
- package/dist/useStreamRuntime.js +74 -19
- package/dist/useStreamRuntime.js.map +1 -1
- package/package.json +10 -10
- package/src/useStreamRuntime.test.tsx +418 -0
- package/src/useStreamRuntime.ts +116 -21
package/dist/streamingTiming.js
CHANGED
|
@@ -22,9 +22,7 @@ const getTextLength = (messages, messageId) => {
|
|
|
22
22
|
case "thinking":
|
|
23
23
|
if (typeof part.thinking === "string") len += part.thinking.length;
|
|
24
24
|
break;
|
|
25
|
-
case "reasoning":
|
|
26
|
-
len += reasoningTextLength(part);
|
|
27
|
-
break;
|
|
25
|
+
case "reasoning": len += reasoningTextLength(part);
|
|
28
26
|
}
|
|
29
27
|
return len;
|
|
30
28
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"streamingTiming.js","names":[],"sources":["../src/streamingTiming.ts"],"sourcesContent":["\"use client\";\n\nimport type { MessageTiming } from \"@assistant-ui/core\";\nimport {\n useStreamingTiming,\n type StreamingTimingAccessors,\n} from \"@assistant-ui/core/react\";\nimport type { LangChainBaseMessage, LangChainContentBlock } from \"./types\";\nimport { getMessageType } from \"./convertMessages\";\n\nconst findAiMessage = (\n messages: readonly LangChainBaseMessage[],\n messageId: string,\n): LangChainBaseMessage | undefined =>\n messages.find((m) => getMessageType(m) === \"ai\" && m.id === messageId);\n\nconst reasoningTextLength = (part: {\n readonly summary?: ReadonlyArray<{ readonly text?: string }>;\n readonly reasoning?: string;\n}): number => {\n if (part.summary && part.summary.length > 0)\n return part.summary.map((s) => s?.text ?? \"\").join(\"\\n\\n\\n\").length;\n return part.reasoning?.length ?? 0;\n};\n\nconst getTextLength = (\n messages: readonly LangChainBaseMessage[],\n messageId: string,\n): number => {\n const m = findAiMessage(messages, messageId);\n if (!m) return 0;\n const content = m.content;\n if (typeof content === \"string\") return content.length;\n if (!Array.isArray(content)) return 0;\n let len = 0;\n for (const part of content as readonly LangChainContentBlock[]) {\n switch (part.type) {\n case \"text\":\n case \"text_delta\":\n if (typeof part.text === \"string\") len += part.text.length;\n break;\n case \"thinking\":\n if (typeof part.thinking === \"string\") len += part.thinking.length;\n break;\n case \"reasoning\":\n len += reasoningTextLength(part);\n break;\n }\n }\n return len;\n};\n\nconst getToolCallCount = (\n messages: readonly LangChainBaseMessage[],\n messageId: string,\n): number => findAiMessage(messages, messageId)?.tool_calls?.length ?? 0;\n\nconst getAssistantMessageId = (\n messages: readonly LangChainBaseMessage[],\n): string | undefined => {\n for (let i = messages.length - 1; i >= 0; i--) {\n const m = messages[i];\n if (m && getMessageType(m) === \"ai\" && m.id) return m.id;\n }\n return undefined;\n};\n\nexport const langChainStreamingTimingAccessors: StreamingTimingAccessors<LangChainBaseMessage> =\n {\n getAssistantMessageId,\n getTextLength,\n getToolCallCount,\n };\n\n/**\n * Tracks per-message streaming timing for LangChain messages. Delegates to\n * the shared `useStreamingTiming` primitive in `@assistant-ui/core/react`,\n * adapted to the `LangChainBaseMessage` shape (`_getType() -> \"ai\"`, content\n * blocks including text/thinking/reasoning, `tool_calls`).\n */\nexport const useLangChainStreamingTiming = (\n messages: readonly LangChainBaseMessage[],\n isRunning: boolean,\n): Record<string, MessageTiming> =>\n useStreamingTiming(messages, isRunning, langChainStreamingTimingAccessors);\n"],"mappings":";;;;AAUA,MAAM,iBACJ,UACA,cAEA,SAAS,MAAM,MAAM,eAAe,CAAC,MAAM,QAAQ,EAAE,OAAO,SAAS;AAEvE,MAAM,uBAAuB,SAGf;CACZ,IAAI,KAAK,WAAW,KAAK,QAAQ,SAAS,GACxC,OAAO,KAAK,QAAQ,KAAK,MAAM,GAAG,QAAQ,EAAE,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAC;CAC/D,OAAO,KAAK,WAAW,UAAU;AACnC;AAEA,MAAM,iBACJ,UACA,cACW;CACX,MAAM,IAAI,cAAc,UAAU,SAAS;CAC3C,IAAI,CAAC,GAAG,OAAO;CACf,MAAM,UAAU,EAAE;CAClB,IAAI,OAAO,YAAY,UAAU,OAAO,QAAQ;CAChD,IAAI,CAAC,MAAM,QAAQ,OAAO,GAAG,OAAO;CACpC,IAAI,MAAM;CACV,KAAK,MAAM,QAAQ,SACjB,QAAQ,KAAK,MAAb;EACE,KAAK;EACL,KAAK;GACH,IAAI,OAAO,KAAK,SAAS,UAAU,OAAO,KAAK,KAAK;GACpD;EACF,KAAK;GACH,IAAI,OAAO,KAAK,aAAa,UAAU,OAAO,KAAK,SAAS;GAC5D;EACF,KAAK
|
|
1
|
+
{"version":3,"file":"streamingTiming.js","names":[],"sources":["../src/streamingTiming.ts"],"sourcesContent":["\"use client\";\n\nimport type { MessageTiming } from \"@assistant-ui/core\";\nimport {\n useStreamingTiming,\n type StreamingTimingAccessors,\n} from \"@assistant-ui/core/react\";\nimport type { LangChainBaseMessage, LangChainContentBlock } from \"./types\";\nimport { getMessageType } from \"./convertMessages\";\n\nconst findAiMessage = (\n messages: readonly LangChainBaseMessage[],\n messageId: string,\n): LangChainBaseMessage | undefined =>\n messages.find((m) => getMessageType(m) === \"ai\" && m.id === messageId);\n\nconst reasoningTextLength = (part: {\n readonly summary?: ReadonlyArray<{ readonly text?: string }>;\n readonly reasoning?: string;\n}): number => {\n if (part.summary && part.summary.length > 0)\n return part.summary.map((s) => s?.text ?? \"\").join(\"\\n\\n\\n\").length;\n return part.reasoning?.length ?? 0;\n};\n\nconst getTextLength = (\n messages: readonly LangChainBaseMessage[],\n messageId: string,\n): number => {\n const m = findAiMessage(messages, messageId);\n if (!m) return 0;\n const content = m.content;\n if (typeof content === \"string\") return content.length;\n if (!Array.isArray(content)) return 0;\n let len = 0;\n for (const part of content as readonly LangChainContentBlock[]) {\n switch (part.type) {\n case \"text\":\n case \"text_delta\":\n if (typeof part.text === \"string\") len += part.text.length;\n break;\n case \"thinking\":\n if (typeof part.thinking === \"string\") len += part.thinking.length;\n break;\n case \"reasoning\":\n len += reasoningTextLength(part);\n break;\n }\n }\n return len;\n};\n\nconst getToolCallCount = (\n messages: readonly LangChainBaseMessage[],\n messageId: string,\n): number => findAiMessage(messages, messageId)?.tool_calls?.length ?? 0;\n\nconst getAssistantMessageId = (\n messages: readonly LangChainBaseMessage[],\n): string | undefined => {\n for (let i = messages.length - 1; i >= 0; i--) {\n const m = messages[i];\n if (m && getMessageType(m) === \"ai\" && m.id) return m.id;\n }\n return undefined;\n};\n\nexport const langChainStreamingTimingAccessors: StreamingTimingAccessors<LangChainBaseMessage> =\n {\n getAssistantMessageId,\n getTextLength,\n getToolCallCount,\n };\n\n/**\n * Tracks per-message streaming timing for LangChain messages. Delegates to\n * the shared `useStreamingTiming` primitive in `@assistant-ui/core/react`,\n * adapted to the `LangChainBaseMessage` shape (`_getType() -> \"ai\"`, content\n * blocks including text/thinking/reasoning, `tool_calls`).\n */\nexport const useLangChainStreamingTiming = (\n messages: readonly LangChainBaseMessage[],\n isRunning: boolean,\n): Record<string, MessageTiming> =>\n useStreamingTiming(messages, isRunning, langChainStreamingTimingAccessors);\n"],"mappings":";;;;AAUA,MAAM,iBACJ,UACA,cAEA,SAAS,MAAM,MAAM,eAAe,CAAC,MAAM,QAAQ,EAAE,OAAO,SAAS;AAEvE,MAAM,uBAAuB,SAGf;CACZ,IAAI,KAAK,WAAW,KAAK,QAAQ,SAAS,GACxC,OAAO,KAAK,QAAQ,KAAK,MAAM,GAAG,QAAQ,EAAE,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAC;CAC/D,OAAO,KAAK,WAAW,UAAU;AACnC;AAEA,MAAM,iBACJ,UACA,cACW;CACX,MAAM,IAAI,cAAc,UAAU,SAAS;CAC3C,IAAI,CAAC,GAAG,OAAO;CACf,MAAM,UAAU,EAAE;CAClB,IAAI,OAAO,YAAY,UAAU,OAAO,QAAQ;CAChD,IAAI,CAAC,MAAM,QAAQ,OAAO,GAAG,OAAO;CACpC,IAAI,MAAM;CACV,KAAK,MAAM,QAAQ,SACjB,QAAQ,KAAK,MAAb;EACE,KAAK;EACL,KAAK;GACH,IAAI,OAAO,KAAK,SAAS,UAAU,OAAO,KAAK,KAAK;GACpD;EACF,KAAK;GACH,IAAI,OAAO,KAAK,aAAa,UAAU,OAAO,KAAK,SAAS;GAC5D;EACF,KAAK,aACH,OAAO,oBAAoB,IAAI;CAEnC;CAEF,OAAO;AACT;AAEA,MAAM,oBACJ,UACA,cACW,cAAc,UAAU,SAAS,CAAC,EAAE,YAAY,UAAU;AAEvE,MAAM,yBACJ,aACuB;CACvB,KAAK,IAAI,IAAI,SAAS,SAAS,GAAG,KAAK,GAAG,KAAK;EAC7C,MAAM,IAAI,SAAS;EACnB,IAAI,KAAK,eAAe,CAAC,MAAM,QAAQ,EAAE,IAAI,OAAO,EAAE;CACxD;AAEF;AAEA,MAAa,oCACX;CACE;CACA;CACA;AACF;;;;;;;AAQF,MAAa,+BACX,UACA,cAEA,mBAAmB,UAAU,WAAW,iCAAiC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useStreamRuntime.d.ts","names":[],"sources":["../src/useStreamRuntime.ts"],"mappings":";;;cAsCa,2BACX,WAAW;;kBAAA;;;;;;;;;
|
|
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);
|
|
@@ -68,20 +69,56 @@ const useStreamThreadRuntime = (options) => {
|
|
|
68
69
|
const mergedUiMessages = useMemo(() => mergeUIMessages(liveUiMessages, uiStateValue), [liveUiMessages, uiStateValue]);
|
|
69
70
|
const visibleMessages = stagedMessages ?? stream.messages;
|
|
70
71
|
const messageTiming = useLangChainStreamingTiming(visibleMessages, effectiveIsRunning);
|
|
72
|
+
const convertWithUI = useMemo(() => {
|
|
73
|
+
const uiMessagesByParent = groupUIMessagesByParent(mergedUiMessages);
|
|
74
|
+
return (message, metadata) => convertLangChainBaseMessage(message, {
|
|
75
|
+
...metadata,
|
|
76
|
+
uiMessagesByParent,
|
|
77
|
+
messageTiming
|
|
78
|
+
});
|
|
79
|
+
}, [mergedUiMessages, messageTiming]);
|
|
71
80
|
const threadMessages = useExternalMessageConverter({
|
|
72
|
-
callback:
|
|
73
|
-
const uiMessagesByParent = groupUIMessagesByParent(mergedUiMessages);
|
|
74
|
-
return (message, metadata) => convertLangChainBaseMessage(message, {
|
|
75
|
-
...metadata,
|
|
76
|
-
uiMessagesByParent,
|
|
77
|
-
messageTiming
|
|
78
|
-
});
|
|
79
|
-
}, [mergedUiMessages, messageTiming]),
|
|
81
|
+
callback: convertWithUI,
|
|
80
82
|
messages: visibleMessages,
|
|
81
83
|
isRunning: effectiveIsRunning
|
|
82
84
|
});
|
|
83
85
|
const streamRef = useRef(stream);
|
|
84
86
|
streamRef.current = stream;
|
|
87
|
+
const activeRunConfigRef = useRef(void 0);
|
|
88
|
+
const runConfigByMessageIdRef = useRef(/* @__PURE__ */ new Map());
|
|
89
|
+
const activeThreadIdRef = useRef(externalId);
|
|
90
|
+
const setActiveRunConfig = useCallback((runConfig) => {
|
|
91
|
+
activeRunConfigRef.current = runConfigToSubmitOptions(runConfig)?.config;
|
|
92
|
+
}, []);
|
|
93
|
+
const withActiveRunConfig = useCallback((submitOptions) => {
|
|
94
|
+
if (submitOptions && "config" in submitOptions) return submitOptions;
|
|
95
|
+
if (activeRunConfigRef.current === void 0) return submitOptions;
|
|
96
|
+
return {
|
|
97
|
+
...submitOptions,
|
|
98
|
+
config: activeRunConfigRef.current
|
|
99
|
+
};
|
|
100
|
+
}, []);
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
if (activeThreadIdRef.current !== null && activeThreadIdRef.current !== externalId) {
|
|
103
|
+
activeRunConfigRef.current = void 0;
|
|
104
|
+
runConfigByMessageIdRef.current.clear();
|
|
105
|
+
}
|
|
106
|
+
activeThreadIdRef.current = externalId;
|
|
107
|
+
}, [externalId]);
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
const messages = stream.messages;
|
|
110
|
+
const owned = runConfigByMessageIdRef.current;
|
|
111
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
112
|
+
const message = messages.at(i);
|
|
113
|
+
if (!message?.id || getMessageType(message) !== "ai" || !message.tool_calls?.length) continue;
|
|
114
|
+
if (owned.has(message.id)) return;
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
for (const message of messages) {
|
|
118
|
+
if (!message.id || getMessageType(message) !== "ai" || !message.tool_calls?.length || owned.has(message.id)) continue;
|
|
119
|
+
owned.set(message.id, activeRunConfigRef.current);
|
|
120
|
+
}
|
|
121
|
+
}, [stream.messages]);
|
|
85
122
|
const visibleMessagesRef = useRef(visibleMessages);
|
|
86
123
|
visibleMessagesRef.current = visibleMessages;
|
|
87
124
|
const threadMessagesRef = useRef(threadMessages);
|
|
@@ -142,12 +179,19 @@ const useStreamThreadRuntime = (options) => {
|
|
|
142
179
|
subgraphs: stream.subgraphs,
|
|
143
180
|
stream,
|
|
144
181
|
error: stream.error,
|
|
145
|
-
submit:
|
|
146
|
-
|
|
147
|
-
|
|
182
|
+
submit: (values, submitOptions) => {
|
|
183
|
+
const isResume = values == null || submitOptions?.command != null;
|
|
184
|
+
return stream.submit(values, isResume ? withActiveRunConfig(submitOptions) : submitOptions);
|
|
185
|
+
},
|
|
186
|
+
respond: (response, respondOptions) => stream.respond(response, withActiveRunConfig(respondOptions)),
|
|
187
|
+
respondAll: (responsesById, respondOptions) => stream.respondAll(responsesById, withActiveRunConfig(respondOptions)),
|
|
148
188
|
values: stream.values,
|
|
149
189
|
messagesKey
|
|
150
|
-
}), [
|
|
190
|
+
}), [
|
|
191
|
+
stream,
|
|
192
|
+
messagesKey,
|
|
193
|
+
withActiveRunConfig
|
|
194
|
+
]);
|
|
151
195
|
return useExternalStoreRuntime({
|
|
152
196
|
...pickExternalStoreSharedOptions(options),
|
|
153
197
|
isRunning: effectiveIsRunning,
|
|
@@ -162,6 +206,7 @@ const useStreamThreadRuntime = (options) => {
|
|
|
162
206
|
stageUserMessage(msg);
|
|
163
207
|
return;
|
|
164
208
|
}
|
|
209
|
+
setActiveRunConfig(msg.runConfig);
|
|
165
210
|
const content = getMessageContent(msg);
|
|
166
211
|
const cancellations = autoCancelPendingToolCalls !== false ? getPendingToolCalls(streamRef.current.messages).map((t) => ({
|
|
167
212
|
type: "tool",
|
|
@@ -170,12 +215,17 @@ const useStreamThreadRuntime = (options) => {
|
|
|
170
215
|
content: JSON.stringify({ cancelled: true }),
|
|
171
216
|
status: "error"
|
|
172
217
|
})) : [];
|
|
173
|
-
|
|
218
|
+
const { externalId } = await aui.threadListItem.initialize();
|
|
219
|
+
await streamRef.current.submit({ [messagesKey]: [...cancellations, {
|
|
174
220
|
type: "human",
|
|
175
221
|
content
|
|
176
|
-
}] },
|
|
222
|
+
}] }, {
|
|
223
|
+
...runConfigToSubmitOptions(msg.runConfig),
|
|
224
|
+
...externalId != null ? { threadId: externalId } : {}
|
|
225
|
+
});
|
|
177
226
|
},
|
|
178
|
-
onAddToolResult: async ({ toolCallId, toolName, result, isError, artifact }) => {
|
|
227
|
+
onAddToolResult: async ({ messageId, toolCallId, toolName, result, isError, artifact }) => {
|
|
228
|
+
const runConfig = runConfigByMessageIdRef.current.has(messageId) ? runConfigByMessageIdRef.current.get(messageId) : activeRunConfigRef.current;
|
|
179
229
|
await stream.submit({ [messagesKey]: [{
|
|
180
230
|
type: "tool",
|
|
181
231
|
name: toolName,
|
|
@@ -183,7 +233,7 @@ const useStreamThreadRuntime = (options) => {
|
|
|
183
233
|
content: JSON.stringify(result),
|
|
184
234
|
...artifact !== void 0 && { artifact },
|
|
185
235
|
status: isError ? "error" : "success"
|
|
186
|
-
}] });
|
|
236
|
+
}] }, runConfig === void 0 ? void 0 : { config: runConfig });
|
|
187
237
|
},
|
|
188
238
|
onReload: async (parentId, config) => {
|
|
189
239
|
const stagedRun = getStagedRun(parentId);
|
|
@@ -200,11 +250,13 @@ const useStreamThreadRuntime = (options) => {
|
|
|
200
250
|
visibleMessagesRef.current = nextMessages;
|
|
201
251
|
setStagedMessages(nextMessages);
|
|
202
252
|
} else setStagedMessages(null);
|
|
253
|
+
const runConfig = config.runConfig ?? stagedRun.runConfig;
|
|
254
|
+
setActiveRunConfig(runConfig);
|
|
203
255
|
await stream.submit({ [messagesKey]: stagedRun.messages.map((message) => ({
|
|
204
256
|
id: message.id,
|
|
205
257
|
type: "human",
|
|
206
258
|
content: message.content
|
|
207
|
-
})) }, runConfigToSubmitOptions(
|
|
259
|
+
})) }, runConfigToSubmitOptions(runConfig));
|
|
208
260
|
return;
|
|
209
261
|
}
|
|
210
262
|
const threadId = externalId;
|
|
@@ -212,6 +264,7 @@ const useStreamThreadRuntime = (options) => {
|
|
|
212
264
|
const s = streamRef.current;
|
|
213
265
|
const checkpointId = await resolveForkCheckpoint(s.client, threadId, s.messages, parentId, config.sourceId, s[STREAM_CONTROLLER]?.messageMetadataStore?.getSnapshot?.(), messagesKey);
|
|
214
266
|
if (!checkpointId) return;
|
|
267
|
+
setActiveRunConfig(config.runConfig);
|
|
215
268
|
await s.submit(null, {
|
|
216
269
|
forkFrom: checkpointId,
|
|
217
270
|
...runConfigToSubmitOptions(config.runConfig)
|
|
@@ -237,6 +290,7 @@ const useStreamThreadRuntime = (options) => {
|
|
|
237
290
|
const checkpointId = await resolveForkCheckpoint(s.client, threadId, s.messages, message.parentId, message.sourceId, s[STREAM_CONTROLLER]?.messageMetadataStore?.getSnapshot?.(), messagesKey);
|
|
238
291
|
if (!checkpointId) return;
|
|
239
292
|
const content = getMessageContent(message);
|
|
293
|
+
setActiveRunConfig(message.runConfig);
|
|
240
294
|
await s.submit({ [messagesKey]: [{
|
|
241
295
|
type: "human",
|
|
242
296
|
content
|
|
@@ -246,6 +300,7 @@ const useStreamThreadRuntime = (options) => {
|
|
|
246
300
|
});
|
|
247
301
|
},
|
|
248
302
|
onCancel: unstable_allowCancellation !== false ? async () => {
|
|
303
|
+
activeRunConfigRef.current = void 0;
|
|
249
304
|
await stream.stop();
|
|
250
305
|
} : void 0
|
|
251
306
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useStreamRuntime.js","names":[],"sources":["../src/useStreamRuntime.ts"],"sourcesContent":["/// <reference types=\"@assistant-ui/core/store\" />\n\"use client\";\n\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport type { AppendMessage, ToolExecutionStatus } from \"@assistant-ui/core\";\nimport {\n generateId,\n getExternalStoreMessages,\n pickExternalStoreSharedOptions,\n} from \"@assistant-ui/core\";\nimport type { ThreadMessage } from \"@assistant-ui/core\";\nimport {\n useCloudThreadListAdapter,\n useExternalStoreRuntime,\n useExternalMessageConverter,\n useRemoteThreadListRuntime,\n} from \"@assistant-ui/core/react\";\nimport { useAuiState } from \"@assistant-ui/store\";\nimport { STREAM_CONTROLLER, useChannel, useStream } from \"@langchain/react\";\nimport type { Channel } from \"@langchain/react\";\nimport type {\n LangChainBaseMessage,\n LangChainToolCall,\n UIMessage,\n UseStreamRuntimeOptions,\n} from \"./types\";\nimport {\n convertLangChainBaseMessage,\n getMessageContent,\n getMessageType,\n} from \"./convertMessages\";\nimport { foldUIUpdates, mergeUIMessages } from \"./uiMessages\";\nimport { langChainExtras } from \"./runtimeExtras\";\nimport { resolveForkCheckpoint } from \"./resolveForkCheckpoint\";\nimport { useLangChainStreamingTiming } from \"./streamingTiming\";\n\nconst UI_CUSTOM_CHANNELS: readonly Channel[] = [\"custom\"];\n\nexport const runConfigToSubmitOptions = (\n runConfig: AppendMessage[\"runConfig\"],\n) =>\n runConfig?.custom\n ? { config: { configurable: runConfig.custom } }\n : undefined;\n\n/**\n * Group the graph's accumulated `UIMessage`s by the assistant message they\n * belong to. Non-array state and entries without a parent link are dropped.\n * The parent id comes from `metadata.message_id` (Python SDK) or\n * `metadata.id` (JS SDK).\n */\nexport const groupUIMessagesByParent = (\n value: unknown,\n): Map<string, UIMessage[]> => {\n const map = new Map<string, UIMessage[]>();\n if (!Array.isArray(value)) return map;\n for (const ui of value as UIMessage[]) {\n const parentId = ui.metadata?.message_id ?? ui.metadata?.id;\n if (!parentId) continue;\n const existing = map.get(parentId);\n if (existing) {\n existing.push(ui);\n } else {\n map.set(parentId, [ui]);\n }\n }\n return map;\n};\n\nconst getPendingToolCalls = (\n messages: readonly LangChainBaseMessage[],\n): LangChainToolCall[] => {\n const pending = new Map<string, LangChainToolCall>();\n for (const m of messages) {\n const type = getMessageType(m);\n if (type === \"ai\") {\n for (const tc of m.tool_calls ?? []) pending.set(tc.id, tc);\n } else if (type === \"tool\" && m.tool_call_id) {\n pending.delete(m.tool_call_id);\n }\n }\n return [...pending.values()];\n};\n\nconst toStagedHumanMessage = (\n msg: AppendMessage,\n id = generateId(),\n): LangChainBaseMessage & { id: string } => ({\n id,\n _getType: () => \"human\",\n content: getMessageContent(msg),\n});\n\nconst truncateLangChainBaseMessages = (\n threadMessages: readonly ThreadMessage[],\n parentId: string | null,\n): LangChainBaseMessage[] => {\n if (parentId === null) return [];\n const parentIndex = threadMessages.findIndex((m) => m.id === parentId);\n if (parentIndex === -1) return [];\n const truncated: LangChainBaseMessage[] = [];\n for (let i = 0; i <= parentIndex && i < threadMessages.length; i++) {\n truncated.push(\n ...getExternalStoreMessages<LangChainBaseMessage>(threadMessages[i]!),\n );\n }\n return truncated;\n};\n\ntype DistributiveOmit<T, K extends keyof any> = T extends unknown\n ? Omit<T, K>\n : never;\n\nconst useStreamThreadRuntime = (\n options: DistributiveOmit<\n UseStreamRuntimeOptions,\n \"cloud\" | \"unstable_threadListAdapter\" | \"create\" | \"delete\"\n >,\n) => {\n const { adapters, autoCancelPendingToolCalls, unstable_allowCancellation } =\n options;\n const messagesKey = options.messagesKey ?? \"messages\";\n const uiStateKey = options.uiStateKey ?? \"ui\";\n\n const externalId = useAuiState((s) => s.threadListItem.externalId) as\n | string\n | null;\n // Object.assign preserves the discriminated transport union; object spread\n // collapses its arms and no longer satisfies UseStreamOptions.\n const stream = useStream(\n Object.assign({}, options, { threadId: externalId }),\n );\n const [stagedMessages, setStagedMessages] = useState<\n LangChainBaseMessage[] | null\n >(null);\n\n const [toolStatuses, setToolStatuses] = useState<\n Record<string, ToolExecutionStatus>\n >({});\n const hasExecutingTools = Object.values(toolStatuses).some(\n (s) => s?.type === \"executing\",\n );\n const effectiveIsRunning = stream.isLoading || hasExecutingTools;\n\n const uiStateValue = stream.values[uiStateKey];\n\n const customEvents = useChannel(stream, UI_CUSTOM_CHANNELS);\n const liveUiMessages = useMemo(\n () => foldUIUpdates(customEvents),\n [customEvents],\n );\n\n const mergedUiMessages = useMemo(\n () => mergeUIMessages(liveUiMessages, uiStateValue),\n [liveUiMessages, uiStateValue],\n );\n\n const visibleMessages =\n stagedMessages ?? (stream.messages as LangChainBaseMessage[]);\n\n const messageTiming = useLangChainStreamingTiming(\n visibleMessages,\n effectiveIsRunning,\n );\n\n const convertWithUI = useMemo<\n useExternalMessageConverter.Callback<LangChainBaseMessage>\n >(() => {\n const uiMessagesByParent = groupUIMessagesByParent(mergedUiMessages);\n return (message, metadata) =>\n convertLangChainBaseMessage(message, {\n ...metadata,\n uiMessagesByParent,\n messageTiming,\n });\n }, [mergedUiMessages, messageTiming]);\n\n const threadMessages = useExternalMessageConverter({\n callback: convertWithUI,\n messages: visibleMessages,\n isRunning: effectiveIsRunning,\n });\n\n const streamRef = useRef(stream);\n streamRef.current = stream;\n\n const visibleMessagesRef = useRef(visibleMessages);\n visibleMessagesRef.current = visibleMessages;\n\n const threadMessagesRef = useRef(threadMessages);\n threadMessagesRef.current = threadMessages;\n\n const stagedMessagesRef = useRef(\n new Map<\n string,\n {\n message: LangChainBaseMessage & { id: string };\n runConfig: AppendMessage[\"runConfig\"];\n }\n >(),\n );\n const stagedBaseMessagesRef = useRef<LangChainBaseMessage[] | null>(null);\n\n useEffect(() => {\n if (stagedMessagesRef.current.size === 0) return;\n\n // Staged edits must keep their truncated base while stream updates arrive before promotion.\n const baseMessages =\n stagedBaseMessagesRef.current ??\n (stream.messages as LangChainBaseMessage[]);\n const baseMessageIds = new Set(\n baseMessages.flatMap((message) => (message.id ? [message.id] : [])),\n );\n const remainingStagedMessages: LangChainBaseMessage[] = [];\n const seenStagedIds = new Set<string>();\n for (const message of visibleMessagesRef.current) {\n if (!message.id || seenStagedIds.has(message.id)) continue;\n if (baseMessageIds.has(message.id)) continue;\n const staged = stagedMessagesRef.current.get(message.id);\n if (!staged) continue;\n remainingStagedMessages.push(staged.message);\n seenStagedIds.add(message.id);\n }\n\n if (remainingStagedMessages.length === 0) {\n stagedBaseMessagesRef.current = null;\n visibleMessagesRef.current = baseMessages;\n setStagedMessages(null);\n return;\n }\n\n const nextMessages = [...baseMessages, ...remainingStagedMessages];\n visibleMessagesRef.current = nextMessages;\n setStagedMessages(nextMessages);\n }, [stream.messages]);\n\n const getStagedRun = (parentId: string | null) => {\n if (!parentId || !stagedMessagesRef.current.has(parentId)) return null;\n\n const staged: LangChainBaseMessage[] = [];\n for (const message of visibleMessagesRef.current) {\n if (message.id && stagedMessagesRef.current.has(message.id)) {\n staged.push(stagedMessagesRef.current.get(message.id)!.message);\n }\n if (message.id === parentId) break;\n }\n\n return {\n messages: staged,\n runConfig: stagedMessagesRef.current.get(parentId)!.runConfig,\n };\n };\n\n const stageUserMessage = (msg: AppendMessage) => {\n const stagedMessage = toStagedHumanMessage(msg);\n stagedMessagesRef.current.set(stagedMessage.id, {\n message: stagedMessage,\n runConfig: msg.runConfig,\n });\n const nextMessages = [...visibleMessagesRef.current, stagedMessage];\n visibleMessagesRef.current = nextMessages;\n setStagedMessages(nextMessages);\n };\n\n const extras = useMemo(\n () =>\n langChainExtras.provide({\n interrupt: stream.interrupt,\n interrupts: stream.interrupts,\n toolCalls: stream.toolCalls,\n subagents: stream.subagents,\n subgraphs: stream.subgraphs,\n stream,\n error: stream.error,\n submit: stream.submit,\n respond: stream.respond,\n respondAll: stream.respondAll,\n values: stream.values,\n messagesKey,\n }),\n [stream, messagesKey],\n );\n\n const runtime = useExternalStoreRuntime({\n ...pickExternalStoreSharedOptions(options),\n isRunning: effectiveIsRunning,\n isLoading: stream.isThreadLoading,\n messages: threadMessages,\n adapters,\n extras,\n unstable_enableToolInvocations: true,\n setToolStatuses,\n onNew: async (msg) => {\n if (!(msg.startRun ?? msg.role === \"user\")) {\n stageUserMessage(msg);\n return;\n }\n\n const content = getMessageContent(msg);\n const cancellations =\n autoCancelPendingToolCalls !== false\n ? getPendingToolCalls(\n streamRef.current.messages as readonly LangChainBaseMessage[],\n ).map((t) => ({\n type: \"tool\" as const,\n name: t.name,\n tool_call_id: t.id,\n content: JSON.stringify({ cancelled: true }),\n status: \"error\" as const,\n }))\n : [];\n await stream.submit(\n { [messagesKey]: [...cancellations, { type: \"human\", content }] },\n runConfigToSubmitOptions(msg.runConfig),\n );\n },\n onAddToolResult: async ({\n toolCallId,\n toolName,\n result,\n isError,\n artifact,\n }) => {\n await stream.submit({\n [messagesKey]: [\n {\n type: \"tool\",\n name: toolName,\n tool_call_id: toolCallId,\n content: JSON.stringify(result),\n ...(artifact !== undefined && { artifact }),\n status: isError ? \"error\" : \"success\",\n },\n ],\n });\n },\n onReload: async (parentId, config) => {\n const stagedRun = getStagedRun(parentId);\n if (stagedRun) {\n const promotedIds = new Set<string>();\n for (const message of stagedRun.messages) {\n if (!message.id) continue;\n promotedIds.add(message.id);\n stagedMessagesRef.current.delete(message.id);\n }\n stagedBaseMessagesRef.current = null;\n if (stagedMessagesRef.current.size > 0) {\n const nextMessages = visibleMessagesRef.current.filter(\n (message) => !message.id || !promotedIds.has(message.id),\n );\n visibleMessagesRef.current = nextMessages;\n setStagedMessages(nextMessages);\n } else {\n setStagedMessages(null);\n }\n await stream.submit(\n {\n [messagesKey]: stagedRun.messages.map((message) => ({\n id: message.id,\n type: \"human\",\n content: message.content,\n })),\n },\n runConfigToSubmitOptions(config.runConfig ?? stagedRun.runConfig),\n );\n return;\n }\n\n const threadId = externalId;\n if (!threadId || parentId == null) return;\n const s = streamRef.current;\n const checkpointId = await resolveForkCheckpoint(\n s.client,\n threadId,\n s.messages as readonly LangChainBaseMessage[],\n parentId,\n config.sourceId,\n s[STREAM_CONTROLLER]?.messageMetadataStore?.getSnapshot?.(),\n messagesKey,\n );\n if (!checkpointId) return;\n await s.submit(null, {\n forkFrom: checkpointId,\n ...runConfigToSubmitOptions(config.runConfig),\n });\n },\n onEdit: async (message) => {\n if (!(message.startRun ?? message.role === \"user\")) {\n const truncated = truncateLangChainBaseMessages(\n threadMessagesRef.current,\n message.parentId,\n );\n const stagedMessage = toStagedHumanMessage(message);\n stagedMessagesRef.current.set(stagedMessage.id, {\n message: stagedMessage,\n runConfig: message.runConfig,\n });\n stagedBaseMessagesRef.current = truncated;\n const nextMessages = [...truncated, stagedMessage];\n visibleMessagesRef.current = nextMessages;\n setStagedMessages(nextMessages);\n return;\n }\n\n const threadId = externalId;\n if (!threadId) return;\n const s = streamRef.current;\n const checkpointId = await resolveForkCheckpoint(\n s.client,\n threadId,\n s.messages as readonly LangChainBaseMessage[],\n message.parentId,\n message.sourceId,\n s[STREAM_CONTROLLER]?.messageMetadataStore?.getSnapshot?.(),\n messagesKey,\n );\n if (!checkpointId) return;\n const content = getMessageContent(message);\n await s.submit(\n { [messagesKey]: [{ type: \"human\", content }] },\n {\n forkFrom: checkpointId,\n ...runConfigToSubmitOptions(message.runConfig),\n },\n );\n },\n onCancel:\n unstable_allowCancellation !== false\n ? async () => {\n await stream.stop();\n }\n : undefined,\n });\n\n return runtime;\n};\n\n/**\n * Creates an assistant-ui runtime backed by LangChain's `useStream` hook.\n * Accepts the same options as `useStream` from `@langchain/react`, plus\n * `cloud` and `adapters`.\n *\n * @example\n * ```tsx\n * import { useStreamRuntime } from \"@assistant-ui/react-langchain\";\n * import { AssistantRuntimeProvider, Thread } from \"@assistant-ui/react\";\n *\n * function App() {\n * const runtime = useStreamRuntime({\n * assistantId: \"agent\",\n * apiUrl: \"http://localhost:2024\",\n * });\n *\n * return (\n * <AssistantRuntimeProvider runtime={runtime}>\n * <Thread />\n * </AssistantRuntimeProvider>\n * );\n * }\n * ```\n */\nexport const useStreamRuntime = (rawOptions: UseStreamRuntimeOptions) => {\n const {\n cloud,\n unstable_threadListAdapter,\n create,\n delete: deleteFn,\n onThreadIdChange,\n ...options\n } = rawOptions;\n\n const optionsRef = useRef(options);\n optionsRef.current = options;\n\n const cloudAdapter = useCloudThreadListAdapter({\n cloud,\n create,\n delete: deleteFn,\n });\n const adapter = unstable_threadListAdapter ?? cloudAdapter;\n\n return useRemoteThreadListRuntime({\n runtimeHook: function RuntimeHook() {\n return useStreamThreadRuntime(optionsRef.current);\n },\n adapter,\n allowNesting: true,\n onThreadIdChange,\n });\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useStreamRuntime.js","names":[],"sources":["../src/useStreamRuntime.ts"],"sourcesContent":["/// <reference types=\"@assistant-ui/core/store\" />\n\"use client\";\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport type { AppendMessage, ToolExecutionStatus } from \"@assistant-ui/core\";\nimport {\n generateId,\n getExternalStoreMessages,\n pickExternalStoreSharedOptions,\n} from \"@assistant-ui/core\";\nimport type { ThreadMessage } from \"@assistant-ui/core\";\nimport {\n useCloudThreadListAdapter,\n useExternalStoreRuntime,\n useExternalMessageConverter,\n useRemoteThreadListRuntime,\n} from \"@assistant-ui/core/react\";\nimport { useAui, useAuiState } from \"@assistant-ui/store\";\nimport { STREAM_CONTROLLER, useChannel, useStream } from \"@langchain/react\";\nimport type { Channel } from \"@langchain/react\";\nimport type {\n LangChainBaseMessage,\n LangChainToolCall,\n UIMessage,\n UseStreamRuntimeOptions,\n} from \"./types\";\nimport {\n convertLangChainBaseMessage,\n getMessageContent,\n getMessageType,\n} from \"./convertMessages\";\nimport { foldUIUpdates, mergeUIMessages } from \"./uiMessages\";\nimport { langChainExtras } from \"./runtimeExtras\";\nimport { resolveForkCheckpoint } from \"./resolveForkCheckpoint\";\nimport { useLangChainStreamingTiming } from \"./streamingTiming\";\n\nconst UI_CUSTOM_CHANNELS: readonly Channel[] = [\"custom\"];\n\nexport const runConfigToSubmitOptions = (\n runConfig: AppendMessage[\"runConfig\"],\n) =>\n runConfig?.custom\n ? { config: { configurable: runConfig.custom } }\n : undefined;\n\ntype NormalizedRunConfigOptions = NonNullable<\n ReturnType<typeof runConfigToSubmitOptions>\n>;\n\n/**\n * Group the graph's accumulated `UIMessage`s by the assistant message they\n * belong to. Non-array state and entries without a parent link are dropped.\n * The parent id comes from `metadata.message_id` (Python SDK) or\n * `metadata.id` (JS SDK).\n */\nexport const groupUIMessagesByParent = (\n value: unknown,\n): Map<string, UIMessage[]> => {\n const map = new Map<string, UIMessage[]>();\n if (!Array.isArray(value)) return map;\n for (const ui of value as UIMessage[]) {\n const parentId = ui.metadata?.message_id ?? ui.metadata?.id;\n if (!parentId) continue;\n const existing = map.get(parentId);\n if (existing) {\n existing.push(ui);\n } else {\n map.set(parentId, [ui]);\n }\n }\n return map;\n};\n\nconst getPendingToolCalls = (\n messages: readonly LangChainBaseMessage[],\n): LangChainToolCall[] => {\n const pending = new Map<string, LangChainToolCall>();\n for (const m of messages) {\n const type = getMessageType(m);\n if (type === \"ai\") {\n for (const tc of m.tool_calls ?? []) pending.set(tc.id, tc);\n } else if (type === \"tool\" && m.tool_call_id) {\n pending.delete(m.tool_call_id);\n }\n }\n return [...pending.values()];\n};\n\nconst toStagedHumanMessage = (\n msg: AppendMessage,\n id = generateId(),\n): LangChainBaseMessage & { id: string } => ({\n id,\n _getType: () => \"human\",\n content: getMessageContent(msg),\n});\n\nconst truncateLangChainBaseMessages = (\n threadMessages: readonly ThreadMessage[],\n parentId: string | null,\n): LangChainBaseMessage[] => {\n if (parentId === null) return [];\n const parentIndex = threadMessages.findIndex((m) => m.id === parentId);\n if (parentIndex === -1) return [];\n const truncated: LangChainBaseMessage[] = [];\n for (let i = 0; i <= parentIndex && i < threadMessages.length; i++) {\n truncated.push(\n ...getExternalStoreMessages<LangChainBaseMessage>(threadMessages[i]!),\n );\n }\n return truncated;\n};\n\ntype DistributiveOmit<T, K extends keyof any> = T extends unknown\n ? Omit<T, K>\n : never;\n\nconst useStreamThreadRuntime = (\n options: DistributiveOmit<\n UseStreamRuntimeOptions,\n \"cloud\" | \"unstable_threadListAdapter\" | \"create\" | \"delete\"\n >,\n) => {\n const { adapters, autoCancelPendingToolCalls, unstable_allowCancellation } =\n options;\n const aui = useAui();\n const messagesKey = options.messagesKey ?? \"messages\";\n const uiStateKey = options.uiStateKey ?? \"ui\";\n\n const externalId = useAuiState((s) => s.threadListItem.externalId) as\n | string\n | null;\n // Object.assign preserves the discriminated transport union; object spread\n // collapses its arms and no longer satisfies UseStreamOptions.\n const stream = useStream(\n Object.assign({}, options, { threadId: externalId }),\n );\n const [stagedMessages, setStagedMessages] = useState<\n LangChainBaseMessage[] | null\n >(null);\n\n const [toolStatuses, setToolStatuses] = useState<\n Record<string, ToolExecutionStatus>\n >({});\n const hasExecutingTools = Object.values(toolStatuses).some(\n (s) => s?.type === \"executing\",\n );\n const effectiveIsRunning = stream.isLoading || hasExecutingTools;\n\n const uiStateValue = stream.values[uiStateKey];\n\n const customEvents = useChannel(stream, UI_CUSTOM_CHANNELS);\n const liveUiMessages = useMemo(\n () => foldUIUpdates(customEvents),\n [customEvents],\n );\n\n const mergedUiMessages = useMemo(\n () => mergeUIMessages(liveUiMessages, uiStateValue),\n [liveUiMessages, uiStateValue],\n );\n\n const visibleMessages =\n stagedMessages ?? (stream.messages as LangChainBaseMessage[]);\n\n const messageTiming = useLangChainStreamingTiming(\n visibleMessages,\n effectiveIsRunning,\n );\n\n const convertWithUI = useMemo<\n useExternalMessageConverter.Callback<LangChainBaseMessage>\n >(() => {\n const uiMessagesByParent = groupUIMessagesByParent(mergedUiMessages);\n return (message, metadata) =>\n convertLangChainBaseMessage(message, {\n ...metadata,\n uiMessagesByParent,\n messageTiming,\n });\n }, [mergedUiMessages, messageTiming]);\n\n const threadMessages = useExternalMessageConverter({\n callback: convertWithUI,\n messages: visibleMessages,\n isRunning: effectiveIsRunning,\n });\n\n const streamRef = useRef(stream);\n streamRef.current = stream;\n\n const activeRunConfigRef = useRef<\n NormalizedRunConfigOptions[\"config\"] | undefined\n >(undefined);\n const runConfigByMessageIdRef = useRef(\n new Map<string, NormalizedRunConfigOptions[\"config\"] | undefined>(),\n );\n const activeThreadIdRef = useRef(externalId);\n const setActiveRunConfig = useCallback(\n (runConfig: AppendMessage[\"runConfig\"]) => {\n activeRunConfigRef.current = runConfigToSubmitOptions(runConfig)?.config;\n },\n [],\n );\n const withActiveRunConfig = useCallback(\n (submitOptions?: Record<string, unknown>) => {\n if (submitOptions && \"config\" in submitOptions) return submitOptions;\n if (activeRunConfigRef.current === undefined) return submitOptions;\n return { ...submitOptions, config: activeRunConfigRef.current };\n },\n [],\n );\n\n useEffect(() => {\n if (\n activeThreadIdRef.current !== null &&\n activeThreadIdRef.current !== externalId\n ) {\n activeRunConfigRef.current = undefined;\n runConfigByMessageIdRef.current.clear();\n }\n activeThreadIdRef.current = externalId;\n }, [externalId]);\n\n useEffect(() => {\n const messages = stream.messages as readonly LangChainBaseMessage[];\n const owned = runConfigByMessageIdRef.current;\n for (let i = messages.length - 1; i >= 0; i--) {\n const message = messages.at(i);\n if (\n !message?.id ||\n getMessageType(message) !== \"ai\" ||\n !message.tool_calls?.length\n ) {\n continue;\n }\n if (owned.has(message.id)) return;\n break;\n }\n for (const message of messages) {\n if (\n !message.id ||\n getMessageType(message) !== \"ai\" ||\n !message.tool_calls?.length ||\n owned.has(message.id)\n ) {\n continue;\n }\n owned.set(message.id, activeRunConfigRef.current);\n }\n }, [stream.messages]);\n\n const visibleMessagesRef = useRef(visibleMessages);\n visibleMessagesRef.current = visibleMessages;\n\n const threadMessagesRef = useRef(threadMessages);\n threadMessagesRef.current = threadMessages;\n\n const stagedMessagesRef = useRef(\n new Map<\n string,\n {\n message: LangChainBaseMessage & { id: string };\n runConfig: AppendMessage[\"runConfig\"];\n }\n >(),\n );\n const stagedBaseMessagesRef = useRef<LangChainBaseMessage[] | null>(null);\n\n useEffect(() => {\n if (stagedMessagesRef.current.size === 0) return;\n\n // Staged edits must keep their truncated base while stream updates arrive before promotion.\n const baseMessages =\n stagedBaseMessagesRef.current ??\n (stream.messages as LangChainBaseMessage[]);\n const baseMessageIds = new Set(\n baseMessages.flatMap((message) => (message.id ? [message.id] : [])),\n );\n const remainingStagedMessages: LangChainBaseMessage[] = [];\n const seenStagedIds = new Set<string>();\n for (const message of visibleMessagesRef.current) {\n if (!message.id || seenStagedIds.has(message.id)) continue;\n if (baseMessageIds.has(message.id)) continue;\n const staged = stagedMessagesRef.current.get(message.id);\n if (!staged) continue;\n remainingStagedMessages.push(staged.message);\n seenStagedIds.add(message.id);\n }\n\n if (remainingStagedMessages.length === 0) {\n stagedBaseMessagesRef.current = null;\n visibleMessagesRef.current = baseMessages;\n setStagedMessages(null);\n return;\n }\n\n const nextMessages = [...baseMessages, ...remainingStagedMessages];\n visibleMessagesRef.current = nextMessages;\n setStagedMessages(nextMessages);\n }, [stream.messages]);\n\n const getStagedRun = (parentId: string | null) => {\n if (!parentId || !stagedMessagesRef.current.has(parentId)) return null;\n\n const staged: LangChainBaseMessage[] = [];\n for (const message of visibleMessagesRef.current) {\n if (message.id && stagedMessagesRef.current.has(message.id)) {\n staged.push(stagedMessagesRef.current.get(message.id)!.message);\n }\n if (message.id === parentId) break;\n }\n\n return {\n messages: staged,\n runConfig: stagedMessagesRef.current.get(parentId)!.runConfig,\n };\n };\n\n const stageUserMessage = (msg: AppendMessage) => {\n const stagedMessage = toStagedHumanMessage(msg);\n stagedMessagesRef.current.set(stagedMessage.id, {\n message: stagedMessage,\n runConfig: msg.runConfig,\n });\n const nextMessages = [...visibleMessagesRef.current, stagedMessage];\n visibleMessagesRef.current = nextMessages;\n setStagedMessages(nextMessages);\n };\n\n const extras = useMemo(\n () =>\n langChainExtras.provide({\n interrupt: stream.interrupt,\n interrupts: stream.interrupts,\n toolCalls: stream.toolCalls,\n subagents: stream.subagents,\n subgraphs: stream.subgraphs,\n stream,\n error: stream.error,\n submit: (values, submitOptions) => {\n const isResume = values == null || submitOptions?.command != null;\n return stream.submit(\n values,\n isResume ? withActiveRunConfig(submitOptions) : submitOptions,\n );\n },\n respond: (response, respondOptions) =>\n stream.respond(response, withActiveRunConfig(respondOptions)),\n respondAll: (responsesById, respondOptions) =>\n stream.respondAll(responsesById, withActiveRunConfig(respondOptions)),\n values: stream.values,\n messagesKey,\n }),\n [stream, messagesKey, withActiveRunConfig],\n );\n\n const runtime = useExternalStoreRuntime({\n ...pickExternalStoreSharedOptions(options),\n isRunning: effectiveIsRunning,\n isLoading: stream.isThreadLoading,\n messages: threadMessages,\n adapters,\n extras,\n unstable_enableToolInvocations: true,\n setToolStatuses,\n onNew: async (msg) => {\n if (!(msg.startRun ?? msg.role === \"user\")) {\n stageUserMessage(msg);\n return;\n }\n\n setActiveRunConfig(msg.runConfig);\n const content = getMessageContent(msg);\n const cancellations =\n autoCancelPendingToolCalls !== false\n ? getPendingToolCalls(\n streamRef.current.messages as readonly LangChainBaseMessage[],\n ).map((t) => ({\n type: \"tool\" as const,\n name: t.name,\n tool_call_id: t.id,\n content: JSON.stringify({ cancelled: true }),\n status: \"error\" as const,\n }))\n : [];\n // A null threadId is not a no-op for the SDK: it rebinds the controller\n // away from its self-created thread and forces a fresh one, so the\n // submit waits for initialization to produce an identity; core no\n // longer holds appends on that barrier.\n const { externalId } = await aui.threadListItem.initialize();\n await streamRef.current.submit(\n { [messagesKey]: [...cancellations, { type: \"human\", content }] },\n {\n ...runConfigToSubmitOptions(msg.runConfig),\n ...(externalId != null ? { threadId: externalId } : {}),\n },\n );\n },\n onAddToolResult: async ({\n messageId,\n toolCallId,\n toolName,\n result,\n isError,\n artifact,\n }) => {\n const runConfig = runConfigByMessageIdRef.current.has(messageId)\n ? runConfigByMessageIdRef.current.get(messageId)\n : activeRunConfigRef.current;\n await stream.submit(\n {\n [messagesKey]: [\n {\n type: \"tool\",\n name: toolName,\n tool_call_id: toolCallId,\n content: JSON.stringify(result),\n ...(artifact !== undefined && { artifact }),\n status: isError ? \"error\" : \"success\",\n },\n ],\n },\n runConfig === undefined ? undefined : { config: runConfig },\n );\n },\n onReload: async (parentId, config) => {\n const stagedRun = getStagedRun(parentId);\n if (stagedRun) {\n const promotedIds = new Set<string>();\n for (const message of stagedRun.messages) {\n if (!message.id) continue;\n promotedIds.add(message.id);\n stagedMessagesRef.current.delete(message.id);\n }\n stagedBaseMessagesRef.current = null;\n if (stagedMessagesRef.current.size > 0) {\n const nextMessages = visibleMessagesRef.current.filter(\n (message) => !message.id || !promotedIds.has(message.id),\n );\n visibleMessagesRef.current = nextMessages;\n setStagedMessages(nextMessages);\n } else {\n setStagedMessages(null);\n }\n const runConfig = config.runConfig ?? stagedRun.runConfig;\n setActiveRunConfig(runConfig);\n await stream.submit(\n {\n [messagesKey]: stagedRun.messages.map((message) => ({\n id: message.id,\n type: \"human\",\n content: message.content,\n })),\n },\n runConfigToSubmitOptions(runConfig),\n );\n return;\n }\n\n const threadId = externalId;\n if (!threadId || parentId == null) return;\n const s = streamRef.current;\n const checkpointId = await resolveForkCheckpoint(\n s.client,\n threadId,\n s.messages as readonly LangChainBaseMessage[],\n parentId,\n config.sourceId,\n s[STREAM_CONTROLLER]?.messageMetadataStore?.getSnapshot?.(),\n messagesKey,\n );\n if (!checkpointId) return;\n setActiveRunConfig(config.runConfig);\n await s.submit(null, {\n forkFrom: checkpointId,\n ...runConfigToSubmitOptions(config.runConfig),\n });\n },\n onEdit: async (message) => {\n if (!(message.startRun ?? message.role === \"user\")) {\n const truncated = truncateLangChainBaseMessages(\n threadMessagesRef.current,\n message.parentId,\n );\n const stagedMessage = toStagedHumanMessage(message);\n stagedMessagesRef.current.set(stagedMessage.id, {\n message: stagedMessage,\n runConfig: message.runConfig,\n });\n stagedBaseMessagesRef.current = truncated;\n const nextMessages = [...truncated, stagedMessage];\n visibleMessagesRef.current = nextMessages;\n setStagedMessages(nextMessages);\n return;\n }\n\n const threadId = externalId;\n if (!threadId) return;\n const s = streamRef.current;\n const checkpointId = await resolveForkCheckpoint(\n s.client,\n threadId,\n s.messages as readonly LangChainBaseMessage[],\n message.parentId,\n message.sourceId,\n s[STREAM_CONTROLLER]?.messageMetadataStore?.getSnapshot?.(),\n messagesKey,\n );\n if (!checkpointId) return;\n const content = getMessageContent(message);\n setActiveRunConfig(message.runConfig);\n await s.submit(\n { [messagesKey]: [{ type: \"human\", content }] },\n {\n forkFrom: checkpointId,\n ...runConfigToSubmitOptions(message.runConfig),\n },\n );\n },\n onCancel:\n unstable_allowCancellation !== false\n ? async () => {\n activeRunConfigRef.current = undefined;\n await stream.stop();\n }\n : undefined,\n });\n\n return runtime;\n};\n\n/**\n * Creates an assistant-ui runtime backed by LangChain's `useStream` hook.\n * Accepts the same options as `useStream` from `@langchain/react`, plus\n * `cloud` and `adapters`.\n *\n * @example\n * ```tsx\n * import { useStreamRuntime } from \"@assistant-ui/react-langchain\";\n * import { AssistantRuntimeProvider, Thread } from \"@assistant-ui/react\";\n *\n * function App() {\n * const runtime = useStreamRuntime({\n * assistantId: \"agent\",\n * apiUrl: \"http://localhost:2024\",\n * });\n *\n * return (\n * <AssistantRuntimeProvider runtime={runtime}>\n * <Thread />\n * </AssistantRuntimeProvider>\n * );\n * }\n * ```\n */\nexport const useStreamRuntime = (rawOptions: UseStreamRuntimeOptions) => {\n const {\n cloud,\n unstable_threadListAdapter,\n create,\n delete: deleteFn,\n onThreadIdChange,\n ...options\n } = rawOptions;\n\n const optionsRef = useRef(options);\n optionsRef.current = options;\n\n const cloudAdapter = useCloudThreadListAdapter({\n cloud,\n create,\n delete: deleteFn,\n });\n const adapter = unstable_threadListAdapter ?? cloudAdapter;\n\n return useRemoteThreadListRuntime({\n runtimeHook: function RuntimeHook() {\n return useStreamThreadRuntime(optionsRef.current);\n },\n adapter,\n allowNesting: true,\n onThreadIdChange,\n });\n};\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@assistant-ui/react-langchain",
|
|
3
|
-
"version": "0.0.
|
|
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",
|
|
@@ -45,15 +45,15 @@
|
|
|
45
45
|
}
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@langchain/core": "^1.2.
|
|
49
|
-
"@langchain/langgraph-sdk": "^1.9.
|
|
50
|
-
"@langchain/react": "^1.0.
|
|
48
|
+
"@langchain/core": "^1.2.5",
|
|
49
|
+
"@langchain/langgraph-sdk": "^1.9.29",
|
|
50
|
+
"@langchain/react": "^1.0.30",
|
|
51
51
|
"@testing-library/react": "^16.3.2",
|
|
52
|
-
"@types/react": "^19.2.
|
|
53
|
-
"jsdom": "^
|
|
52
|
+
"@types/react": "^19.2.18",
|
|
53
|
+
"jsdom": "^30.0.1",
|
|
54
54
|
"react": "^19.2.8",
|
|
55
55
|
"vitest": "^4.1.10",
|
|
56
|
-
"@assistant-ui/x-buildutils": "0.0.
|
|
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,
|