@assistant-ui/react-langchain 0.0.15 → 0.0.17
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/convertMessages.d.ts +37 -3
- package/dist/convertMessages.d.ts.map +1 -1
- package/dist/convertMessages.js +57 -11
- package/dist/convertMessages.js.map +1 -1
- package/dist/findForkCheckpointInHistory.d.ts +26 -0
- package/dist/findForkCheckpointInHistory.d.ts.map +1 -0
- package/dist/findForkCheckpointInHistory.js +34 -0
- package/dist/findForkCheckpointInHistory.js.map +1 -0
- package/dist/hooks.d.ts +88 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +99 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.js +4 -2
- package/dist/resolveForkCheckpoint.d.ts +17 -0
- package/dist/resolveForkCheckpoint.d.ts.map +1 -0
- package/dist/resolveForkCheckpoint.js +26 -0
- package/dist/resolveForkCheckpoint.js.map +1 -0
- package/dist/runtimeExtras.d.ts +7 -0
- package/dist/runtimeExtras.d.ts.map +1 -0
- package/dist/runtimeExtras.js +7 -0
- package/dist/runtimeExtras.js.map +1 -0
- package/dist/streamingTiming.d.ts +16 -0
- package/dist/streamingTiming.d.ts.map +1 -0
- package/dist/streamingTiming.js +53 -0
- package/dist/streamingTiming.js.map +1 -0
- package/dist/types.d.ts +105 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/uiMessages.d.ts +24 -0
- package/dist/uiMessages.d.ts.map +1 -0
- package/dist/uiMessages.js +68 -0
- package/dist/uiMessages.js.map +1 -0
- package/dist/useStreamRuntime.d.ts +10 -85
- package/dist/useStreamRuntime.d.ts.map +1 -1
- package/dist/useStreamRuntime.js +186 -135
- package/dist/useStreamRuntime.js.map +1 -1
- package/package.json +9 -9
- package/src/__tests__/langChainTestUtils.ts +3 -3
- package/src/convertMessages.test.ts +160 -3
- package/src/convertMessages.ts +104 -11
- package/src/findForkCheckpointInHistory.test.ts +263 -0
- package/src/findForkCheckpointInHistory.ts +68 -0
- package/src/groupUIMessagesByParent.test.ts +61 -0
- package/src/hooks.ts +156 -0
- package/src/index.ts +18 -3
- package/src/resolveForkCheckpoint.test.ts +202 -0
- package/src/resolveForkCheckpoint.ts +49 -0
- package/src/runtimeExtras.ts +5 -0
- package/src/streamingTiming.test.ts +118 -0
- package/src/streamingTiming.ts +85 -0
- package/src/types.ts +140 -3
- package/src/uiMessages.test.ts +190 -0
- package/src/uiMessages.ts +91 -0
- package/src/useLangChainError.test.tsx +1 -1
- package/src/useLangChainInterrupts.test.tsx +48 -0
- package/src/useLangChainRespond.test.tsx +49 -0
- package/src/useLangChainRespondAll.test.tsx +52 -0
- package/src/useLangChainState.test.tsx +1 -1
- package/src/useLangChainStream.test.tsx +42 -0
- package/src/useLangChainSubagents.test.tsx +48 -0
- package/src/useLangChainSubgraphs.test.tsx +48 -0
- package/src/useLangChainToolCalls.test.tsx +1 -1
- package/src/useStreamRuntime.test.tsx +232 -0
- package/src/useStreamRuntime.ts +301 -268
package/src/useStreamRuntime.ts
CHANGED
|
@@ -1,98 +1,40 @@
|
|
|
1
1
|
/// <reference types="@assistant-ui/core/store" />
|
|
2
2
|
"use client";
|
|
3
3
|
|
|
4
|
-
import { useMemo, useRef, useState } from "react";
|
|
5
|
-
import type {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
FeedbackAdapter,
|
|
11
|
-
RealtimeVoiceAdapter,
|
|
12
|
-
RemoteThreadListAdapter,
|
|
13
|
-
SpeechSynthesisAdapter,
|
|
14
|
-
ToolExecutionStatus,
|
|
4
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
5
|
+
import type { AppendMessage, ToolExecutionStatus } from "@assistant-ui/core";
|
|
6
|
+
import {
|
|
7
|
+
generateId,
|
|
8
|
+
getExternalStoreMessages,
|
|
9
|
+
pickExternalStoreSharedOptions,
|
|
15
10
|
} from "@assistant-ui/core";
|
|
16
|
-
import {
|
|
11
|
+
import type { ThreadMessage } from "@assistant-ui/core";
|
|
17
12
|
import {
|
|
18
13
|
useCloudThreadListAdapter,
|
|
19
14
|
useExternalStoreRuntime,
|
|
20
15
|
useExternalMessageConverter,
|
|
21
16
|
useRemoteThreadListRuntime,
|
|
22
17
|
} from "@assistant-ui/core/react";
|
|
23
|
-
import {
|
|
24
|
-
import
|
|
18
|
+
import { useAuiState } from "@assistant-ui/store";
|
|
19
|
+
import { STREAM_CONTROLLER, useChannel, useStream } from "@langchain/react";
|
|
20
|
+
import type { Channel } from "@langchain/react";
|
|
21
|
+
import type {
|
|
22
|
+
LangChainBaseMessage,
|
|
23
|
+
LangChainToolCall,
|
|
24
|
+
UIMessage,
|
|
25
|
+
UseStreamRuntimeOptions,
|
|
26
|
+
} from "./types";
|
|
25
27
|
import {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
} from "
|
|
30
|
-
import
|
|
31
|
-
import {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const EMPTY_TOOL_CALLS: readonly AssembledToolCall[] = [];
|
|
36
|
-
|
|
37
|
-
type LangChainRuntimeExtras = {
|
|
38
|
-
[symbolLangChainRuntimeExtras]: true;
|
|
39
|
-
interrupt: { value?: unknown } | undefined;
|
|
40
|
-
interrupts: readonly { value?: unknown }[];
|
|
41
|
-
toolCalls: readonly AssembledToolCall[];
|
|
42
|
-
error: unknown;
|
|
43
|
-
submit: (
|
|
44
|
-
values: Record<string, unknown> | null | undefined,
|
|
45
|
-
options?: Record<string, unknown>,
|
|
46
|
-
) => Promise<void>;
|
|
47
|
-
values: Record<string, unknown>;
|
|
48
|
-
messagesKey: string;
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
const asLangChainRuntimeExtras = (extras: unknown): LangChainRuntimeExtras => {
|
|
52
|
-
if (
|
|
53
|
-
typeof extras !== "object" ||
|
|
54
|
-
extras == null ||
|
|
55
|
-
!(symbolLangChainRuntimeExtras in extras)
|
|
56
|
-
)
|
|
57
|
-
throw new Error(
|
|
58
|
-
"This method can only be called when you are using useStreamRuntime",
|
|
59
|
-
);
|
|
60
|
-
return extras as LangChainRuntimeExtras;
|
|
61
|
-
};
|
|
28
|
+
convertLangChainBaseMessage,
|
|
29
|
+
getMessageContent,
|
|
30
|
+
getMessageType,
|
|
31
|
+
} from "./convertMessages";
|
|
32
|
+
import { foldUIUpdates, mergeUIMessages } from "./uiMessages";
|
|
33
|
+
import { langChainExtras } from "./runtimeExtras";
|
|
34
|
+
import { resolveForkCheckpoint } from "./resolveForkCheckpoint";
|
|
35
|
+
import { useLangChainStreamingTiming } from "./streamingTiming";
|
|
62
36
|
|
|
63
|
-
|
|
64
|
-
cloud?: AssistantCloud | undefined;
|
|
65
|
-
adapters?:
|
|
66
|
-
| {
|
|
67
|
-
attachments?: AttachmentAdapter | undefined;
|
|
68
|
-
speech?: SpeechSynthesisAdapter | undefined;
|
|
69
|
-
dictation?: DictationAdapter | undefined;
|
|
70
|
-
voice?: RealtimeVoiceAdapter | undefined;
|
|
71
|
-
feedback?: FeedbackAdapter | undefined;
|
|
72
|
-
}
|
|
73
|
-
| undefined;
|
|
74
|
-
/**
|
|
75
|
-
* When the user sends a new message while previous tool calls are
|
|
76
|
-
* still pending, automatically submit `tool` messages that cancel
|
|
77
|
-
* them so the agent's tool-call accounting stays consistent.
|
|
78
|
-
* Defaults to `true`.
|
|
79
|
-
*/
|
|
80
|
-
autoCancelPendingToolCalls?: boolean | undefined;
|
|
81
|
-
/**
|
|
82
|
-
* Routes the Cancel button's click to `useStream().stop()`. On by
|
|
83
|
-
* default. Pass `false` to disable the Cancel button.
|
|
84
|
-
*/
|
|
85
|
-
unstable_allowCancellation?: boolean | undefined;
|
|
86
|
-
/**
|
|
87
|
-
* Custom `RemoteThreadListAdapter`. When provided, replaces the
|
|
88
|
-
* cloud-backed thread list adapter.
|
|
89
|
-
*/
|
|
90
|
-
unstable_threadListAdapter?: RemoteThreadListAdapter | undefined;
|
|
91
|
-
/** Custom thread-creation hook, forwarded to the cloud adapter. */
|
|
92
|
-
create?: (() => Promise<{ externalId: string | undefined }>) | undefined;
|
|
93
|
-
/** Custom thread-deletion hook, forwarded to the cloud adapter. */
|
|
94
|
-
delete?: ((threadId: string) => Promise<void>) | undefined;
|
|
95
|
-
};
|
|
37
|
+
const UI_CUSTOM_CHANNELS: readonly Channel[] = ["custom"];
|
|
96
38
|
|
|
97
39
|
export const runConfigToSubmitOptions = (
|
|
98
40
|
runConfig: AppendMessage["runConfig"],
|
|
@@ -101,6 +43,30 @@ export const runConfigToSubmitOptions = (
|
|
|
101
43
|
? { config: { configurable: runConfig.custom } }
|
|
102
44
|
: undefined;
|
|
103
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Group the graph's accumulated `UIMessage`s by the assistant message they
|
|
48
|
+
* belong to. Non-array state and entries without a parent link are dropped.
|
|
49
|
+
* The parent id comes from `metadata.message_id` (Python SDK) or
|
|
50
|
+
* `metadata.id` (JS SDK).
|
|
51
|
+
*/
|
|
52
|
+
export const groupUIMessagesByParent = (
|
|
53
|
+
value: unknown,
|
|
54
|
+
): Map<string, UIMessage[]> => {
|
|
55
|
+
const map = new Map<string, UIMessage[]>();
|
|
56
|
+
if (!Array.isArray(value)) return map;
|
|
57
|
+
for (const ui of value as UIMessage[]) {
|
|
58
|
+
const parentId = ui.metadata?.message_id ?? ui.metadata?.id;
|
|
59
|
+
if (!parentId) continue;
|
|
60
|
+
const existing = map.get(parentId);
|
|
61
|
+
if (existing) {
|
|
62
|
+
existing.push(ui);
|
|
63
|
+
} else {
|
|
64
|
+
map.set(parentId, [ui]);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return map;
|
|
68
|
+
};
|
|
69
|
+
|
|
104
70
|
const getPendingToolCalls = (
|
|
105
71
|
messages: readonly LangChainBaseMessage[],
|
|
106
72
|
): LangChainToolCall[] => {
|
|
@@ -116,65 +82,29 @@ const getPendingToolCalls = (
|
|
|
116
82
|
return [...pending.values()];
|
|
117
83
|
};
|
|
118
84
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
allContent.unshift({ type: "text", text: " " });
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const content = allContent.map((part) => {
|
|
144
|
-
const type = part.type;
|
|
145
|
-
switch (type) {
|
|
146
|
-
case "text":
|
|
147
|
-
return { type: "text" as const, text: part.text };
|
|
148
|
-
case "image":
|
|
149
|
-
return { type: "image_url" as const, image_url: { url: part.image } };
|
|
150
|
-
case "file":
|
|
151
|
-
return {
|
|
152
|
-
type: "file" as const,
|
|
153
|
-
data: part.data,
|
|
154
|
-
mime_type: part.mimeType,
|
|
155
|
-
metadata: { filename: part.filename ?? "file" },
|
|
156
|
-
source_type: "base64" as const,
|
|
157
|
-
};
|
|
158
|
-
case "tool-call":
|
|
159
|
-
throw new Error("Tool call appends are not supported.");
|
|
160
|
-
default: {
|
|
161
|
-
const _exhaustiveCheck:
|
|
162
|
-
| "reasoning"
|
|
163
|
-
| "source"
|
|
164
|
-
| "audio"
|
|
165
|
-
| "data"
|
|
166
|
-
| "generative-ui" = type;
|
|
167
|
-
throw new Error(
|
|
168
|
-
`Unsupported append message part type: ${_exhaustiveCheck}`,
|
|
169
|
-
);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
if (content.length === 1 && content[0]?.type === "text") {
|
|
175
|
-
return content[0].text ?? "";
|
|
85
|
+
const toStagedHumanMessage = (
|
|
86
|
+
msg: AppendMessage,
|
|
87
|
+
id = generateId(),
|
|
88
|
+
): LangChainBaseMessage & { id: string } => ({
|
|
89
|
+
id,
|
|
90
|
+
_getType: () => "human",
|
|
91
|
+
content: getMessageContent(msg),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const truncateLangChainBaseMessages = (
|
|
95
|
+
threadMessages: readonly ThreadMessage[],
|
|
96
|
+
parentId: string | null,
|
|
97
|
+
): LangChainBaseMessage[] => {
|
|
98
|
+
if (parentId === null) return [];
|
|
99
|
+
const parentIndex = threadMessages.findIndex((m) => m.id === parentId);
|
|
100
|
+
if (parentIndex === -1) return [];
|
|
101
|
+
const truncated: LangChainBaseMessage[] = [];
|
|
102
|
+
for (let i = 0; i <= parentIndex && i < threadMessages.length; i++) {
|
|
103
|
+
truncated.push(
|
|
104
|
+
...getExternalStoreMessages<LangChainBaseMessage>(threadMessages[i]!),
|
|
105
|
+
);
|
|
176
106
|
}
|
|
177
|
-
return
|
|
107
|
+
return truncated;
|
|
178
108
|
};
|
|
179
109
|
|
|
180
110
|
type DistributiveOmit<T, K extends keyof any> = T extends unknown
|
|
@@ -190,6 +120,7 @@ const useStreamThreadRuntime = (
|
|
|
190
120
|
const { adapters, autoCancelPendingToolCalls, unstable_allowCancellation } =
|
|
191
121
|
options;
|
|
192
122
|
const messagesKey = options.messagesKey ?? "messages";
|
|
123
|
+
const uiStateKey = options.uiStateKey ?? "ui";
|
|
193
124
|
|
|
194
125
|
const externalId = useAuiState((s) => s.threadListItem.externalId) as
|
|
195
126
|
| string
|
|
@@ -200,6 +131,9 @@ const useStreamThreadRuntime = (
|
|
|
200
131
|
options.threadId = externalId;
|
|
201
132
|
|
|
202
133
|
const stream = useStream(options);
|
|
134
|
+
const [stagedMessages, setStagedMessages] = useState<
|
|
135
|
+
LangChainBaseMessage[] | null
|
|
136
|
+
>(null);
|
|
203
137
|
|
|
204
138
|
const [toolStatuses, setToolStatuses] = useState<
|
|
205
139
|
Record<string, ToolExecutionStatus>
|
|
@@ -209,35 +143,143 @@ const useStreamThreadRuntime = (
|
|
|
209
143
|
);
|
|
210
144
|
const effectiveIsRunning = stream.isLoading || hasExecutingTools;
|
|
211
145
|
|
|
146
|
+
const uiStateValue = stream.values[uiStateKey];
|
|
147
|
+
|
|
148
|
+
const customEvents = useChannel(stream, UI_CUSTOM_CHANNELS);
|
|
149
|
+
const liveUiMessages = useMemo(
|
|
150
|
+
() => foldUIUpdates(customEvents),
|
|
151
|
+
[customEvents],
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const mergedUiMessages = useMemo(
|
|
155
|
+
() => mergeUIMessages(liveUiMessages, uiStateValue),
|
|
156
|
+
[liveUiMessages, uiStateValue],
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
const visibleMessages =
|
|
160
|
+
stagedMessages ?? (stream.messages as LangChainBaseMessage[]);
|
|
161
|
+
|
|
162
|
+
const messageTiming = useLangChainStreamingTiming(
|
|
163
|
+
visibleMessages,
|
|
164
|
+
effectiveIsRunning,
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const convertWithUI = useMemo<
|
|
168
|
+
useExternalMessageConverter.Callback<LangChainBaseMessage>
|
|
169
|
+
>(() => {
|
|
170
|
+
const uiMessagesByParent = groupUIMessagesByParent(mergedUiMessages);
|
|
171
|
+
return (message, metadata) =>
|
|
172
|
+
convertLangChainBaseMessage(message, {
|
|
173
|
+
...metadata,
|
|
174
|
+
uiMessagesByParent,
|
|
175
|
+
messageTiming,
|
|
176
|
+
});
|
|
177
|
+
}, [mergedUiMessages, messageTiming]);
|
|
178
|
+
|
|
212
179
|
const threadMessages = useExternalMessageConverter({
|
|
213
|
-
callback:
|
|
214
|
-
messages:
|
|
180
|
+
callback: convertWithUI,
|
|
181
|
+
messages: visibleMessages,
|
|
215
182
|
isRunning: effectiveIsRunning,
|
|
216
183
|
});
|
|
217
184
|
|
|
218
185
|
const streamRef = useRef(stream);
|
|
219
186
|
streamRef.current = stream;
|
|
220
187
|
|
|
188
|
+
const visibleMessagesRef = useRef(visibleMessages);
|
|
189
|
+
visibleMessagesRef.current = visibleMessages;
|
|
190
|
+
|
|
191
|
+
const threadMessagesRef = useRef(threadMessages);
|
|
192
|
+
threadMessagesRef.current = threadMessages;
|
|
193
|
+
|
|
194
|
+
const stagedMessagesRef = useRef(
|
|
195
|
+
new Map<
|
|
196
|
+
string,
|
|
197
|
+
{
|
|
198
|
+
message: LangChainBaseMessage & { id: string };
|
|
199
|
+
runConfig: AppendMessage["runConfig"];
|
|
200
|
+
}
|
|
201
|
+
>(),
|
|
202
|
+
);
|
|
203
|
+
const stagedBaseMessagesRef = useRef<LangChainBaseMessage[] | null>(null);
|
|
204
|
+
|
|
205
|
+
useEffect(() => {
|
|
206
|
+
if (stagedMessagesRef.current.size === 0) return;
|
|
207
|
+
|
|
208
|
+
// Staged edits must keep their truncated base while stream updates arrive before promotion.
|
|
209
|
+
const baseMessages =
|
|
210
|
+
stagedBaseMessagesRef.current ??
|
|
211
|
+
(stream.messages as LangChainBaseMessage[]);
|
|
212
|
+
const baseMessageIds = new Set(
|
|
213
|
+
baseMessages.flatMap((message) => (message.id ? [message.id] : [])),
|
|
214
|
+
);
|
|
215
|
+
const remainingStagedMessages: LangChainBaseMessage[] = [];
|
|
216
|
+
const seenStagedIds = new Set<string>();
|
|
217
|
+
for (const message of visibleMessagesRef.current) {
|
|
218
|
+
if (!message.id || seenStagedIds.has(message.id)) continue;
|
|
219
|
+
if (baseMessageIds.has(message.id)) continue;
|
|
220
|
+
const staged = stagedMessagesRef.current.get(message.id);
|
|
221
|
+
if (!staged) continue;
|
|
222
|
+
remainingStagedMessages.push(staged.message);
|
|
223
|
+
seenStagedIds.add(message.id);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (remainingStagedMessages.length === 0) {
|
|
227
|
+
stagedBaseMessagesRef.current = null;
|
|
228
|
+
visibleMessagesRef.current = baseMessages;
|
|
229
|
+
setStagedMessages(null);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const nextMessages = [...baseMessages, ...remainingStagedMessages];
|
|
234
|
+
visibleMessagesRef.current = nextMessages;
|
|
235
|
+
setStagedMessages(nextMessages);
|
|
236
|
+
}, [stream.messages]);
|
|
237
|
+
|
|
238
|
+
const getStagedRun = (parentId: string | null) => {
|
|
239
|
+
if (!parentId || !stagedMessagesRef.current.has(parentId)) return null;
|
|
240
|
+
|
|
241
|
+
const staged: LangChainBaseMessage[] = [];
|
|
242
|
+
for (const message of visibleMessagesRef.current) {
|
|
243
|
+
if (message.id && stagedMessagesRef.current.has(message.id)) {
|
|
244
|
+
staged.push(stagedMessagesRef.current.get(message.id)!.message);
|
|
245
|
+
}
|
|
246
|
+
if (message.id === parentId) break;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
messages: staged,
|
|
251
|
+
runConfig: stagedMessagesRef.current.get(parentId)!.runConfig,
|
|
252
|
+
};
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const stageUserMessage = (msg: AppendMessage) => {
|
|
256
|
+
const stagedMessage = toStagedHumanMessage(msg);
|
|
257
|
+
stagedMessagesRef.current.set(stagedMessage.id, {
|
|
258
|
+
message: stagedMessage,
|
|
259
|
+
runConfig: msg.runConfig,
|
|
260
|
+
});
|
|
261
|
+
const nextMessages = [...visibleMessagesRef.current, stagedMessage];
|
|
262
|
+
visibleMessagesRef.current = nextMessages;
|
|
263
|
+
setStagedMessages(nextMessages);
|
|
264
|
+
};
|
|
265
|
+
|
|
221
266
|
const extras = useMemo(
|
|
222
|
-
()
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
stream.values,
|
|
239
|
-
messagesKey,
|
|
240
|
-
],
|
|
267
|
+
() =>
|
|
268
|
+
langChainExtras.provide({
|
|
269
|
+
interrupt: stream.interrupt,
|
|
270
|
+
interrupts: stream.interrupts,
|
|
271
|
+
toolCalls: stream.toolCalls,
|
|
272
|
+
subagents: stream.subagents,
|
|
273
|
+
subgraphs: stream.subgraphs,
|
|
274
|
+
stream,
|
|
275
|
+
error: stream.error,
|
|
276
|
+
submit: stream.submit,
|
|
277
|
+
respond: stream.respond,
|
|
278
|
+
respondAll: stream.respondAll,
|
|
279
|
+
values: stream.values,
|
|
280
|
+
messagesKey,
|
|
281
|
+
}),
|
|
282
|
+
[stream, messagesKey],
|
|
241
283
|
);
|
|
242
284
|
|
|
243
285
|
const runtime = useExternalStoreRuntime({
|
|
@@ -250,6 +292,11 @@ const useStreamThreadRuntime = (
|
|
|
250
292
|
unstable_enableToolInvocations: true,
|
|
251
293
|
setToolStatuses,
|
|
252
294
|
onNew: async (msg) => {
|
|
295
|
+
if (!(msg.startRun ?? msg.role === "user")) {
|
|
296
|
+
stageUserMessage(msg);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
253
300
|
const content = getMessageContent(msg);
|
|
254
301
|
const cancellations =
|
|
255
302
|
autoCancelPendingToolCalls !== false
|
|
@@ -288,6 +335,96 @@ const useStreamThreadRuntime = (
|
|
|
288
335
|
],
|
|
289
336
|
});
|
|
290
337
|
},
|
|
338
|
+
onReload: async (parentId, config) => {
|
|
339
|
+
const stagedRun = getStagedRun(parentId);
|
|
340
|
+
if (stagedRun) {
|
|
341
|
+
const promotedIds = new Set<string>();
|
|
342
|
+
for (const message of stagedRun.messages) {
|
|
343
|
+
if (!message.id) continue;
|
|
344
|
+
promotedIds.add(message.id);
|
|
345
|
+
stagedMessagesRef.current.delete(message.id);
|
|
346
|
+
}
|
|
347
|
+
stagedBaseMessagesRef.current = null;
|
|
348
|
+
if (stagedMessagesRef.current.size > 0) {
|
|
349
|
+
const nextMessages = visibleMessagesRef.current.filter(
|
|
350
|
+
(message) => !message.id || !promotedIds.has(message.id),
|
|
351
|
+
);
|
|
352
|
+
visibleMessagesRef.current = nextMessages;
|
|
353
|
+
setStagedMessages(nextMessages);
|
|
354
|
+
} else {
|
|
355
|
+
setStagedMessages(null);
|
|
356
|
+
}
|
|
357
|
+
await stream.submit(
|
|
358
|
+
{
|
|
359
|
+
[messagesKey]: stagedRun.messages.map((message) => ({
|
|
360
|
+
id: message.id,
|
|
361
|
+
type: "human",
|
|
362
|
+
content: message.content,
|
|
363
|
+
})),
|
|
364
|
+
},
|
|
365
|
+
runConfigToSubmitOptions(config.runConfig ?? stagedRun.runConfig),
|
|
366
|
+
);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const threadId = externalId;
|
|
371
|
+
if (!threadId || parentId == null) return;
|
|
372
|
+
const s = streamRef.current;
|
|
373
|
+
const checkpointId = await resolveForkCheckpoint(
|
|
374
|
+
s.client,
|
|
375
|
+
threadId,
|
|
376
|
+
s.messages as readonly LangChainBaseMessage[],
|
|
377
|
+
parentId,
|
|
378
|
+
config.sourceId,
|
|
379
|
+
s[STREAM_CONTROLLER]?.messageMetadataStore?.getSnapshot?.(),
|
|
380
|
+
messagesKey,
|
|
381
|
+
);
|
|
382
|
+
if (!checkpointId) return;
|
|
383
|
+
await s.submit(null, {
|
|
384
|
+
forkFrom: checkpointId,
|
|
385
|
+
...runConfigToSubmitOptions(config.runConfig),
|
|
386
|
+
});
|
|
387
|
+
},
|
|
388
|
+
onEdit: async (message) => {
|
|
389
|
+
if (!(message.startRun ?? message.role === "user")) {
|
|
390
|
+
const truncated = truncateLangChainBaseMessages(
|
|
391
|
+
threadMessagesRef.current,
|
|
392
|
+
message.parentId,
|
|
393
|
+
);
|
|
394
|
+
const stagedMessage = toStagedHumanMessage(message);
|
|
395
|
+
stagedMessagesRef.current.set(stagedMessage.id, {
|
|
396
|
+
message: stagedMessage,
|
|
397
|
+
runConfig: message.runConfig,
|
|
398
|
+
});
|
|
399
|
+
stagedBaseMessagesRef.current = truncated;
|
|
400
|
+
const nextMessages = [...truncated, stagedMessage];
|
|
401
|
+
visibleMessagesRef.current = nextMessages;
|
|
402
|
+
setStagedMessages(nextMessages);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const threadId = externalId;
|
|
407
|
+
if (!threadId) return;
|
|
408
|
+
const s = streamRef.current;
|
|
409
|
+
const checkpointId = await resolveForkCheckpoint(
|
|
410
|
+
s.client,
|
|
411
|
+
threadId,
|
|
412
|
+
s.messages as readonly LangChainBaseMessage[],
|
|
413
|
+
message.parentId,
|
|
414
|
+
message.sourceId,
|
|
415
|
+
s[STREAM_CONTROLLER]?.messageMetadataStore?.getSnapshot?.(),
|
|
416
|
+
messagesKey,
|
|
417
|
+
);
|
|
418
|
+
if (!checkpointId) return;
|
|
419
|
+
const content = getMessageContent(message);
|
|
420
|
+
await s.submit(
|
|
421
|
+
{ [messagesKey]: [{ type: "human", content }] },
|
|
422
|
+
{
|
|
423
|
+
forkFrom: checkpointId,
|
|
424
|
+
...runConfigToSubmitOptions(message.runConfig),
|
|
425
|
+
},
|
|
426
|
+
);
|
|
427
|
+
},
|
|
291
428
|
onCancel:
|
|
292
429
|
unstable_allowCancellation !== false
|
|
293
430
|
? async () => {
|
|
@@ -329,6 +466,7 @@ export const useStreamRuntime = (rawOptions: UseStreamRuntimeOptions) => {
|
|
|
329
466
|
unstable_threadListAdapter,
|
|
330
467
|
create,
|
|
331
468
|
delete: deleteFn,
|
|
469
|
+
onThreadIdChange,
|
|
332
470
|
...options
|
|
333
471
|
} = rawOptions;
|
|
334
472
|
|
|
@@ -348,111 +486,6 @@ export const useStreamRuntime = (rawOptions: UseStreamRuntimeOptions) => {
|
|
|
348
486
|
},
|
|
349
487
|
adapter,
|
|
350
488
|
allowNesting: true,
|
|
489
|
+
onThreadIdChange,
|
|
351
490
|
});
|
|
352
491
|
};
|
|
353
|
-
|
|
354
|
-
/**
|
|
355
|
-
* Read the current LangGraph interrupt state from the runtime extras.
|
|
356
|
-
*/
|
|
357
|
-
export const useLangChainInterruptState = () => {
|
|
358
|
-
return useAuiState((s) => {
|
|
359
|
-
const extras = s.thread.extras;
|
|
360
|
-
if (!extras) return undefined;
|
|
361
|
-
return asLangChainRuntimeExtras(extras).interrupt;
|
|
362
|
-
});
|
|
363
|
-
};
|
|
364
|
-
|
|
365
|
-
/** Read the last run/hydration error from the runtime extras. */
|
|
366
|
-
export const useLangChainError = () => {
|
|
367
|
-
return useAuiState((s) => {
|
|
368
|
-
const extras = s.thread.extras;
|
|
369
|
-
if (!extras) return undefined;
|
|
370
|
-
return asLangChainRuntimeExtras(extras).error;
|
|
371
|
-
});
|
|
372
|
-
};
|
|
373
|
-
|
|
374
|
-
/**
|
|
375
|
-
* Read the root tool calls assembled by `useStream` from the `tools`
|
|
376
|
-
* channel. Defaults to an empty array, so consumers can `.map` without
|
|
377
|
-
* a guard. Useful for rendering pending/streamed tool calls and
|
|
378
|
-
* approval UIs.
|
|
379
|
-
*/
|
|
380
|
-
export const useLangChainToolCalls = () => {
|
|
381
|
-
return useAuiState((s) => {
|
|
382
|
-
const extras = s.thread.extras;
|
|
383
|
-
if (!extras) return EMPTY_TOOL_CALLS;
|
|
384
|
-
return asLangChainRuntimeExtras(extras).toolCalls ?? EMPTY_TOOL_CALLS;
|
|
385
|
-
});
|
|
386
|
-
};
|
|
387
|
-
|
|
388
|
-
/**
|
|
389
|
-
* Returns a function to submit raw state updates to the LangGraph agent,
|
|
390
|
-
* bypassing the normal message flow. Useful for sending interrupt resume
|
|
391
|
-
* commands.
|
|
392
|
-
*/
|
|
393
|
-
export const useLangChainSubmit = () => {
|
|
394
|
-
const aui = useAui();
|
|
395
|
-
return (
|
|
396
|
-
values: Record<string, unknown> | null | undefined,
|
|
397
|
-
options?: Record<string, unknown>,
|
|
398
|
-
) => {
|
|
399
|
-
const extras = aui.thread().getState().extras;
|
|
400
|
-
const { submit } = asLangChainRuntimeExtras(extras);
|
|
401
|
-
return submit(values, options);
|
|
402
|
-
};
|
|
403
|
-
};
|
|
404
|
-
|
|
405
|
-
/**
|
|
406
|
-
* Submit a list of LangChain-shaped messages on the current thread.
|
|
407
|
-
* Parity helper for migrating from `useLangGraphSend`. Routes to
|
|
408
|
-
* `useStream().submit({ [messagesKey]: messages }, options)`.
|
|
409
|
-
*/
|
|
410
|
-
export const useLangChainSend = () => {
|
|
411
|
-
const aui = useAui();
|
|
412
|
-
return (
|
|
413
|
-
messages: readonly LangChainBaseMessage[],
|
|
414
|
-
options?: Record<string, unknown>,
|
|
415
|
-
) => {
|
|
416
|
-
const { submit, messagesKey } = asLangChainRuntimeExtras(
|
|
417
|
-
aui.thread().getState().extras,
|
|
418
|
-
);
|
|
419
|
-
return submit({ [messagesKey]: messages }, options);
|
|
420
|
-
};
|
|
421
|
-
};
|
|
422
|
-
|
|
423
|
-
/**
|
|
424
|
-
* Submit a `useStream` command (e.g. interrupt resume). Parity helper
|
|
425
|
-
* for migrating from `useLangGraphSendCommand`. Note that v1's command
|
|
426
|
-
* shape (`{ resume?, goto?, update? }`) differs from the legacy
|
|
427
|
-
* `{ resume: string }` form — to carry a payload, use the input or
|
|
428
|
-
* `stream.respond` instead.
|
|
429
|
-
*/
|
|
430
|
-
export const useLangChainSendCommand = () => {
|
|
431
|
-
const submit = useLangChainSubmit();
|
|
432
|
-
return (command: Record<string, unknown>) => submit(null, { command });
|
|
433
|
-
};
|
|
434
|
-
|
|
435
|
-
/**
|
|
436
|
-
* Read a custom LangGraph state key from the current thread. Mirrors
|
|
437
|
-
* `useStream().values[key]` from `@langchain/react` and updates when the
|
|
438
|
-
* stream emits new state.
|
|
439
|
-
*
|
|
440
|
-
* @example
|
|
441
|
-
* ```tsx
|
|
442
|
-
* const todos = useLangChainState<Todo[]>("todos");
|
|
443
|
-
* const files = useLangChainState<Record<string, string>>("files", {});
|
|
444
|
-
* ```
|
|
445
|
-
*/
|
|
446
|
-
export function useLangChainState<T>(key: string): T | undefined;
|
|
447
|
-
export function useLangChainState<T>(key: string, defaultValue: T): T;
|
|
448
|
-
export function useLangChainState<T>(
|
|
449
|
-
key: string,
|
|
450
|
-
defaultValue?: T,
|
|
451
|
-
): T | undefined {
|
|
452
|
-
return useAuiState((s) => {
|
|
453
|
-
const extras = s.thread.extras;
|
|
454
|
-
if (!extras) return defaultValue;
|
|
455
|
-
const value = asLangChainRuntimeExtras(extras).values[key] as T | undefined;
|
|
456
|
-
return value !== undefined ? value : defaultValue;
|
|
457
|
-
});
|
|
458
|
-
}
|