@assistant-ui/react-langchain 0.0.29 → 0.0.31
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/README.md +1 -1
- package/dist/attachSubagentTranscripts.d.ts +16 -0
- package/dist/attachSubagentTranscripts.d.ts.map +1 -0
- package/dist/attachSubagentTranscripts.js +39 -0
- package/dist/attachSubagentTranscripts.js.map +1 -0
- package/dist/convertMessages.d.ts +1 -1
- package/dist/converter.d.ts +1 -1
- package/dist/converter.d.ts.map +1 -1
- package/dist/converter.js +27 -19
- package/dist/converter.js.map +1 -1
- package/dist/sdkIdentity.d.ts +6 -0
- package/dist/sdkIdentity.d.ts.map +1 -0
- package/dist/sdkIdentity.js +9 -0
- package/dist/sdkIdentity.js.map +1 -0
- package/dist/useStreamRuntime.d.ts.map +1 -1
- package/dist/useStreamRuntime.js +22 -13
- package/dist/useStreamRuntime.js.map +1 -1
- package/dist/useSubagentTranscripts.d.ts +9 -0
- package/dist/useSubagentTranscripts.d.ts.map +1 -0
- package/dist/useSubagentTranscripts.js +171 -0
- package/dist/useSubagentTranscripts.js.map +1 -0
- package/package.json +13 -9
- package/src/attachSubagentTranscripts.test.ts +190 -0
- package/src/attachSubagentTranscripts.ts +72 -0
- package/src/convertMessages.test.ts +72 -3
- package/src/converter.ts +20 -17
- package/src/sdkIdentity.ts +9 -0
- package/src/streamingTiming.test.tsx +50 -0
- package/src/useStreamRuntime.test.tsx +106 -7
- package/src/useStreamRuntime.ts +34 -10
- package/src/useSubagentTranscripts.test.tsx +515 -0
- package/src/useSubagentTranscripts.ts +304 -0
package/src/converter.ts
CHANGED
|
@@ -61,7 +61,10 @@ export const convertLangChainContentBlock = (
|
|
|
61
61
|
switch (type) {
|
|
62
62
|
case "text":
|
|
63
63
|
case "text_delta":
|
|
64
|
-
return {
|
|
64
|
+
return {
|
|
65
|
+
type: "text" as const,
|
|
66
|
+
text: typeof part.text === "string" ? part.text : "",
|
|
67
|
+
};
|
|
65
68
|
case "image_url": {
|
|
66
69
|
const image =
|
|
67
70
|
typeof part.image_url === "string"
|
|
@@ -98,15 +101,13 @@ export const convertLangChainContentBlock = (
|
|
|
98
101
|
};
|
|
99
102
|
}
|
|
100
103
|
case "thinking":
|
|
101
|
-
return
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
: (part.reasoning ?? ""),
|
|
109
|
-
};
|
|
104
|
+
return hasVisibleText(part.thinking)
|
|
105
|
+
? { type: "reasoning" as const, text: part.thinking }
|
|
106
|
+
: null;
|
|
107
|
+
case "reasoning": {
|
|
108
|
+
const text = getReasoningText(part);
|
|
109
|
+
return text ? { type: "reasoning" as const, text } : null;
|
|
110
|
+
}
|
|
110
111
|
case "tool_use":
|
|
111
112
|
case "input_json_delta":
|
|
112
113
|
return null;
|
|
@@ -262,13 +263,14 @@ export const getMessageContent = (msg: AppendMessage) => {
|
|
|
262
263
|
return content;
|
|
263
264
|
};
|
|
264
265
|
|
|
265
|
-
const
|
|
266
|
+
const getReasoningText = (part: {
|
|
266
267
|
readonly summary?: ReadonlyArray<{ readonly text?: string }>;
|
|
267
268
|
readonly reasoning?: string;
|
|
268
|
-
}):
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
269
|
+
}): string => {
|
|
270
|
+
const summary = part.summary?.map((s) => s?.text ?? "").join("\n\n\n") ?? "";
|
|
271
|
+
if (hasVisibleText(summary)) return summary;
|
|
272
|
+
const reasoning = part.reasoning ?? "";
|
|
273
|
+
return hasVisibleText(reasoning) ? reasoning : "";
|
|
272
274
|
};
|
|
273
275
|
|
|
274
276
|
export const createLangChainStreamingTimingAccessors = <
|
|
@@ -299,16 +301,17 @@ export const createLangChainStreamingTimingAccessors = <
|
|
|
299
301
|
if (!Array.isArray(content)) return 0;
|
|
300
302
|
let len = 0;
|
|
301
303
|
for (const part of content as readonly LangChainContentBlock[]) {
|
|
304
|
+
if (typeof part !== "object" || part === null) continue;
|
|
302
305
|
switch (part.type) {
|
|
303
306
|
case "text":
|
|
304
307
|
case "text_delta":
|
|
305
308
|
if (typeof part.text === "string") len += part.text.length;
|
|
306
309
|
break;
|
|
307
310
|
case "thinking":
|
|
308
|
-
if (
|
|
311
|
+
if (hasVisibleText(part.thinking)) len += part.thinking.length;
|
|
309
312
|
break;
|
|
310
313
|
case "reasoning":
|
|
311
|
-
len +=
|
|
314
|
+
len += getReasoningText(part).length;
|
|
312
315
|
break;
|
|
313
316
|
}
|
|
314
317
|
}
|
|
@@ -34,4 +34,54 @@ describe("useLangChainStreamingTiming", () => {
|
|
|
34
34
|
Math.ceil("deduced".length / 4),
|
|
35
35
|
);
|
|
36
36
|
});
|
|
37
|
+
|
|
38
|
+
it("counts a thinking block only when the converter renders it", () => {
|
|
39
|
+
const tokenCount = (thinking: string) => {
|
|
40
|
+
const messages: LangChainBaseMessage[] = [
|
|
41
|
+
{
|
|
42
|
+
id: "msg-1",
|
|
43
|
+
_getType: () => "ai",
|
|
44
|
+
content: [{ type: "thinking", thinking }],
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
const { result, rerender } = renderHook(
|
|
48
|
+
({ msgs, running }) => useLangChainStreamingTiming(msgs, running),
|
|
49
|
+
{ initialProps: { msgs: messages, running: true } },
|
|
50
|
+
);
|
|
51
|
+
act(() => {
|
|
52
|
+
rerender({ msgs: messages, running: false });
|
|
53
|
+
});
|
|
54
|
+
return result.current["msg-1"]?.tokenCount;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
expect(tokenCount("deduced")).toBe(Math.ceil("deduced".length / 4));
|
|
58
|
+
expect(tokenCount(" ")).toBeUndefined();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("counts the summary text the converter renders, not the reasoning it shadows", () => {
|
|
62
|
+
const messages: LangChainBaseMessage[] = [
|
|
63
|
+
{
|
|
64
|
+
id: "msg-1",
|
|
65
|
+
_getType: () => "ai",
|
|
66
|
+
content: [
|
|
67
|
+
{
|
|
68
|
+
type: "reasoning",
|
|
69
|
+
reasoning: "partial thinking",
|
|
70
|
+
summary: [{ type: "summary_text", text: "first summary" }],
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
];
|
|
75
|
+
const { result, rerender } = renderHook(
|
|
76
|
+
({ running }) => useLangChainStreamingTiming(messages, running),
|
|
77
|
+
{ initialProps: { running: true } },
|
|
78
|
+
);
|
|
79
|
+
act(() => {
|
|
80
|
+
rerender({ running: false });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
expect(result.current["msg-1"]?.tokenCount).toBe(
|
|
84
|
+
Math.ceil("first summary".length / 4),
|
|
85
|
+
);
|
|
86
|
+
});
|
|
37
87
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
2
|
|
|
3
3
|
import { act, render, renderHook, waitFor } from "@testing-library/react";
|
|
4
|
-
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
5
|
import { AssistantRuntimeProvider } from "@assistant-ui/core/react";
|
|
6
6
|
import type {
|
|
7
7
|
AssistantRuntime,
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
} from "./hooks";
|
|
21
21
|
|
|
22
22
|
const { mockUseChannel, mockUseStream, streamController } = vi.hoisted(() => ({
|
|
23
|
-
mockUseChannel: vi.fn(() => []),
|
|
23
|
+
mockUseChannel: vi.fn((): unknown[] => []),
|
|
24
24
|
mockUseStream: vi.fn(),
|
|
25
25
|
streamController: Symbol("STREAM_CONTROLLER"),
|
|
26
26
|
}));
|
|
@@ -40,7 +40,7 @@ type MockStream = {
|
|
|
40
40
|
values: Record<string, unknown>;
|
|
41
41
|
interrupts: unknown[];
|
|
42
42
|
toolCalls: unknown[];
|
|
43
|
-
subagents: unknown
|
|
43
|
+
subagents: ReadonlyMap<string, unknown>;
|
|
44
44
|
subgraphs: unknown[];
|
|
45
45
|
error: unknown;
|
|
46
46
|
submit: ReturnType<typeof vi.fn>;
|
|
@@ -53,6 +53,10 @@ type MockStream = {
|
|
|
53
53
|
messageMetadataStore: {
|
|
54
54
|
getSnapshot: ReturnType<typeof vi.fn>;
|
|
55
55
|
};
|
|
56
|
+
resolveSubagentNamespace: ReturnType<typeof vi.fn>;
|
|
57
|
+
registry: {
|
|
58
|
+
acquire: ReturnType<typeof vi.fn>;
|
|
59
|
+
};
|
|
56
60
|
};
|
|
57
61
|
};
|
|
58
62
|
|
|
@@ -75,7 +79,7 @@ const createMockStream = (
|
|
|
75
79
|
values: {},
|
|
76
80
|
interrupts: [],
|
|
77
81
|
toolCalls: [],
|
|
78
|
-
subagents:
|
|
82
|
+
subagents: new Map(),
|
|
79
83
|
subgraphs: [],
|
|
80
84
|
error: undefined,
|
|
81
85
|
submit: vi.fn(async () => {}),
|
|
@@ -88,6 +92,10 @@ const createMockStream = (
|
|
|
88
92
|
messageMetadataStore: {
|
|
89
93
|
getSnapshot: vi.fn(),
|
|
90
94
|
},
|
|
95
|
+
resolveSubagentNamespace: vi.fn(async () => {}),
|
|
96
|
+
registry: {
|
|
97
|
+
acquire: vi.fn(),
|
|
98
|
+
},
|
|
91
99
|
},
|
|
92
100
|
});
|
|
93
101
|
|
|
@@ -177,7 +185,11 @@ describe("useStreamRuntime thread options", () => {
|
|
|
177
185
|
unstable_threadListAdapter: threadListAdapter,
|
|
178
186
|
} as never);
|
|
179
187
|
capture.runtime = runtime;
|
|
180
|
-
return
|
|
188
|
+
return (
|
|
189
|
+
<AssistantRuntimeProvider runtime={runtime}>
|
|
190
|
+
{null}
|
|
191
|
+
</AssistantRuntimeProvider>
|
|
192
|
+
);
|
|
181
193
|
};
|
|
182
194
|
|
|
183
195
|
const view = render(<TestRuntime />);
|
|
@@ -286,7 +298,11 @@ describe("useStreamRuntime thread options", () => {
|
|
|
286
298
|
const TestRuntime = () => {
|
|
287
299
|
const runtime = useStreamRuntime({ apiUrl: "/api" } as never);
|
|
288
300
|
capture.runtime = runtime;
|
|
289
|
-
return
|
|
301
|
+
return (
|
|
302
|
+
<AssistantRuntimeProvider runtime={runtime}>
|
|
303
|
+
{null}
|
|
304
|
+
</AssistantRuntimeProvider>
|
|
305
|
+
);
|
|
290
306
|
};
|
|
291
307
|
const view = render(<TestRuntime />);
|
|
292
308
|
await waitFor(() => expect(capture.runtime).not.toBeNull());
|
|
@@ -721,7 +737,9 @@ describe("useStreamRuntime run configuration", () => {
|
|
|
721
737
|
});
|
|
722
738
|
|
|
723
739
|
await act(async () => {
|
|
724
|
-
await capture.send!([
|
|
740
|
+
await capture.send!([
|
|
741
|
+
{ type: "human", content: "next" } as unknown as LangChainBaseMessage,
|
|
742
|
+
]);
|
|
725
743
|
});
|
|
726
744
|
|
|
727
745
|
expect(stream.submit).toHaveBeenLastCalledWith(
|
|
@@ -971,3 +989,84 @@ describe("useStreamRuntime committed refs", () => {
|
|
|
971
989
|
host.unmount();
|
|
972
990
|
});
|
|
973
991
|
});
|
|
992
|
+
|
|
993
|
+
describe("useStreamRuntime subagent transcripts", () => {
|
|
994
|
+
afterEach(() => {
|
|
995
|
+
mockUseChannel.mockReset();
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
it("renders live UI messages inside the transcript of the subagent that pushed them", async () => {
|
|
999
|
+
const stream = createMockStream([
|
|
1000
|
+
message("human-1", "human", "delegate"),
|
|
1001
|
+
{
|
|
1002
|
+
id: "root-ai",
|
|
1003
|
+
_getType: () => "ai",
|
|
1004
|
+
content: "",
|
|
1005
|
+
tool_calls: [{ id: "task-one", name: "task", args: {} }],
|
|
1006
|
+
},
|
|
1007
|
+
]);
|
|
1008
|
+
const transcript = [message("nested-ai", "ai", "nested answer")];
|
|
1009
|
+
stream.subagents = new Map([
|
|
1010
|
+
[
|
|
1011
|
+
"task-one",
|
|
1012
|
+
{
|
|
1013
|
+
id: "task-one",
|
|
1014
|
+
namespace: ["tools:task-one"],
|
|
1015
|
+
status: "running",
|
|
1016
|
+
parentId: null,
|
|
1017
|
+
depth: 1,
|
|
1018
|
+
},
|
|
1019
|
+
],
|
|
1020
|
+
]);
|
|
1021
|
+
stream[streamController]!.registry.acquire.mockReturnValue({
|
|
1022
|
+
store: { getSnapshot: () => transcript, subscribe: () => () => {} },
|
|
1023
|
+
release: vi.fn(),
|
|
1024
|
+
});
|
|
1025
|
+
const uiEvent = {
|
|
1026
|
+
method: "custom",
|
|
1027
|
+
params: {
|
|
1028
|
+
namespace: ["tools:task-one"],
|
|
1029
|
+
data: {
|
|
1030
|
+
type: "ui",
|
|
1031
|
+
id: "ui-1",
|
|
1032
|
+
name: "chart",
|
|
1033
|
+
props: { points: [1, 2] },
|
|
1034
|
+
metadata: { message_id: "nested-ai" },
|
|
1035
|
+
},
|
|
1036
|
+
},
|
|
1037
|
+
};
|
|
1038
|
+
mockUseChannel.mockReturnValue([uiEvent]);
|
|
1039
|
+
const { auiResult, rerender } = renderAui(stream);
|
|
1040
|
+
const nestedTranscript = () => {
|
|
1041
|
+
const { messages } = auiResult.current.thread.getState();
|
|
1042
|
+
for (const threadMessage of messages) {
|
|
1043
|
+
for (const part of threadMessage.content) {
|
|
1044
|
+
if (part.type === "tool-call" && part.toolCallId === "task-one")
|
|
1045
|
+
return part.messages;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
return undefined;
|
|
1049
|
+
};
|
|
1050
|
+
|
|
1051
|
+
await waitFor(() =>
|
|
1052
|
+
expect(nestedTranscript()?.[0]?.content).toMatchObject([
|
|
1053
|
+
{ type: "text", text: "nested answer" },
|
|
1054
|
+
{ type: "data", name: "chart", data: { points: [1, 2] } },
|
|
1055
|
+
]),
|
|
1056
|
+
);
|
|
1057
|
+
const rendered = nestedTranscript();
|
|
1058
|
+
|
|
1059
|
+
mockUseChannel.mockReturnValue([
|
|
1060
|
+
uiEvent,
|
|
1061
|
+
{
|
|
1062
|
+
method: "custom",
|
|
1063
|
+
params: { namespace: [], data: { name: "progress", payload: 1 } },
|
|
1064
|
+
},
|
|
1065
|
+
]);
|
|
1066
|
+
await act(async () => {
|
|
1067
|
+
rerender();
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
expect(nestedTranscript()).toBe(rendered);
|
|
1071
|
+
});
|
|
1072
|
+
});
|
package/src/useStreamRuntime.ts
CHANGED
|
@@ -43,10 +43,16 @@ import {
|
|
|
43
43
|
getMessageContent,
|
|
44
44
|
getMessageType,
|
|
45
45
|
} from "./convertMessages";
|
|
46
|
+
import {
|
|
47
|
+
attachSubagentTranscripts,
|
|
48
|
+
createAttachMemo,
|
|
49
|
+
} from "./attachSubagentTranscripts";
|
|
50
|
+
import { useSubagentTranscripts } from "./useSubagentTranscripts";
|
|
46
51
|
import { foldUIUpdates, mergeUIMessages } from "./uiMessages";
|
|
47
52
|
import { langChainExtras } from "./runtimeExtras";
|
|
48
53
|
import { resolveForkCheckpoint } from "./resolveForkCheckpoint";
|
|
49
54
|
import { useLangChainStreamingTiming } from "./streamingTiming";
|
|
55
|
+
import { LANGCHAIN_SDK } from "./sdkIdentity";
|
|
50
56
|
|
|
51
57
|
const UI_CUSTOM_CHANNELS: readonly Channel[] = ["custom"];
|
|
52
58
|
|
|
@@ -171,6 +177,11 @@ const useStreamThreadRuntime = (
|
|
|
171
177
|
[liveUiMessages, uiStateValue],
|
|
172
178
|
);
|
|
173
179
|
|
|
180
|
+
const uiMessagesByParent = useMemo(
|
|
181
|
+
() => groupUIMessagesByParent<UIMessage>(mergedUiMessages),
|
|
182
|
+
[mergedUiMessages],
|
|
183
|
+
);
|
|
184
|
+
|
|
174
185
|
const visibleMessages =
|
|
175
186
|
stagedMessages ?? (stream.messages as LangChainBaseMessage[]);
|
|
176
187
|
|
|
@@ -179,24 +190,33 @@ const useStreamThreadRuntime = (
|
|
|
179
190
|
effectiveIsRunning,
|
|
180
191
|
);
|
|
181
192
|
|
|
193
|
+
const subagentTranscripts = useSubagentTranscripts(
|
|
194
|
+
stream,
|
|
195
|
+
uiMessagesByParent,
|
|
196
|
+
);
|
|
197
|
+
|
|
182
198
|
const convertWithUI = useMemo<
|
|
183
199
|
useExternalMessageConverter.Callback<LangChainBaseMessage>
|
|
184
|
-
>(
|
|
185
|
-
|
|
186
|
-
groupUIMessagesByParent<UIMessage>(mergedUiMessages);
|
|
187
|
-
return (message, metadata) =>
|
|
200
|
+
>(
|
|
201
|
+
() => (message, metadata) =>
|
|
188
202
|
convertLangChainBaseMessage(message, {
|
|
189
203
|
...metadata,
|
|
190
204
|
uiMessagesByParent,
|
|
191
205
|
messageTiming,
|
|
192
|
-
})
|
|
193
|
-
|
|
206
|
+
}),
|
|
207
|
+
[uiMessagesByParent, messageTiming],
|
|
208
|
+
);
|
|
194
209
|
|
|
195
210
|
const threadMessages = useExternalMessageConverter({
|
|
196
211
|
callback: convertWithUI,
|
|
197
212
|
messages: visibleMessages,
|
|
198
213
|
isRunning: effectiveIsRunning,
|
|
199
214
|
});
|
|
215
|
+
const [memo] = useState(createAttachMemo);
|
|
216
|
+
const messagesWithTranscripts = useMemo(
|
|
217
|
+
() => attachSubagentTranscripts(threadMessages, subagentTranscripts, memo),
|
|
218
|
+
[threadMessages, subagentTranscripts, memo],
|
|
219
|
+
);
|
|
200
220
|
|
|
201
221
|
const streamRef = useRef(stream);
|
|
202
222
|
useInsertionEffect(() => {
|
|
@@ -269,10 +289,10 @@ const useStreamThreadRuntime = (
|
|
|
269
289
|
visibleMessagesRef.current = visibleMessages;
|
|
270
290
|
}, [visibleMessages]);
|
|
271
291
|
|
|
272
|
-
const threadMessagesRef = useRef(
|
|
292
|
+
const threadMessagesRef = useRef(messagesWithTranscripts);
|
|
273
293
|
useInsertionEffect(() => {
|
|
274
|
-
threadMessagesRef.current =
|
|
275
|
-
}, [
|
|
294
|
+
threadMessagesRef.current = messagesWithTranscripts;
|
|
295
|
+
}, [messagesWithTranscripts]);
|
|
276
296
|
|
|
277
297
|
const stagedMessagesRef = useRef(
|
|
278
298
|
new Map<
|
|
@@ -324,6 +344,9 @@ const useStreamThreadRuntime = (
|
|
|
324
344
|
if (remainingStagedMessages.length === 0) {
|
|
325
345
|
stagedBaseMessagesRef.current = null;
|
|
326
346
|
visibleMessagesRef.current = baseMessages;
|
|
347
|
+
// Reconciling against the upstream stream mutates the staged refs above,
|
|
348
|
+
// which cannot happen during render.
|
|
349
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
327
350
|
setStagedMessages(null);
|
|
328
351
|
return;
|
|
329
352
|
}
|
|
@@ -409,7 +432,7 @@ const useStreamThreadRuntime = (
|
|
|
409
432
|
...pickExternalStoreSharedOptions(options),
|
|
410
433
|
isRunning: stream.isLoading,
|
|
411
434
|
isLoading: stream.isThreadLoading,
|
|
412
|
-
messages:
|
|
435
|
+
messages: messagesWithTranscripts,
|
|
413
436
|
adapters,
|
|
414
437
|
extras,
|
|
415
438
|
unstable_enableToolInvocations: true,
|
|
@@ -628,6 +651,7 @@ export const useStreamRuntime = (rawOptions: UseStreamRuntimeOptions) => {
|
|
|
628
651
|
|
|
629
652
|
const aui = useAui();
|
|
630
653
|
const cloudAdapter = useCloudThreadListAdapter({
|
|
654
|
+
sdk: LANGCHAIN_SDK,
|
|
631
655
|
cloud,
|
|
632
656
|
create: createCloudThreadListAdapterCreateFallback(
|
|
633
657
|
create,
|