@assistant-ui/react-langchain 0.0.30 → 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/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 +10 -6
- package/src/attachSubagentTranscripts.test.ts +190 -0
- package/src/attachSubagentTranscripts.ts +72 -0
- package/src/sdkIdentity.ts +9 -0
- package/src/useStreamRuntime.test.tsx +106 -7
- package/src/useStreamRuntime.ts +31 -10
- package/src/useSubagentTranscripts.test.tsx +515 -0
- package/src/useSubagentTranscripts.ts +304 -0
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.31",
|
|
4
4
|
"description": "LangChain useStream adapter for assistant-ui",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"langchain",
|
|
@@ -40,17 +40,21 @@
|
|
|
40
40
|
],
|
|
41
41
|
"sideEffects": false,
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@assistant-ui/core": "^0.3.
|
|
43
|
+
"@assistant-ui/core": "^0.3.19",
|
|
44
44
|
"@assistant-ui/store": "^0.3.13",
|
|
45
45
|
"assistant-cloud": "*",
|
|
46
|
-
"assistant-stream": "^0.3.
|
|
46
|
+
"assistant-stream": "^0.3.43"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
|
-
"@langchain/
|
|
49
|
+
"@langchain/langgraph-sdk": "^1.9.20",
|
|
50
|
+
"@langchain/react": "^1.0.20",
|
|
50
51
|
"@types/react": "*",
|
|
51
52
|
"react": "^18 || ^19"
|
|
52
53
|
},
|
|
53
54
|
"peerDependenciesMeta": {
|
|
55
|
+
"@langchain/langgraph-sdk": {
|
|
56
|
+
"optional": true
|
|
57
|
+
},
|
|
54
58
|
"@langchain/react": {
|
|
55
59
|
"optional": true
|
|
56
60
|
},
|
|
@@ -59,7 +63,7 @@
|
|
|
59
63
|
}
|
|
60
64
|
},
|
|
61
65
|
"devDependencies": {
|
|
62
|
-
"@assistant-ui/x-buildutils": "0.0.
|
|
66
|
+
"@assistant-ui/x-buildutils": "0.0.27",
|
|
63
67
|
"@langchain/core": "^1.2.9",
|
|
64
68
|
"@langchain/langgraph-sdk": "^1.10.2",
|
|
65
69
|
"@langchain/react": "^1.0.35",
|
|
@@ -67,7 +71,7 @@
|
|
|
67
71
|
"@types/react": "^19.2.18",
|
|
68
72
|
"jsdom": "^30.0.1",
|
|
69
73
|
"react": "^19.2.8",
|
|
70
|
-
"vitest": "^
|
|
74
|
+
"vitest": "^5.0.0"
|
|
71
75
|
},
|
|
72
76
|
"publishConfig": {
|
|
73
77
|
"access": "public",
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import {
|
|
2
|
+
bindExternalStoreMessage,
|
|
3
|
+
getExternalStoreMessages,
|
|
4
|
+
type ThreadMessage,
|
|
5
|
+
} from "@assistant-ui/core";
|
|
6
|
+
import { describe, expect, it } from "vitest";
|
|
7
|
+
import {
|
|
8
|
+
attachSubagentTranscripts,
|
|
9
|
+
createAttachMemo,
|
|
10
|
+
} from "./attachSubagentTranscripts";
|
|
11
|
+
|
|
12
|
+
const assistantMessage = (id: string, toolCallId?: string): ThreadMessage =>
|
|
13
|
+
({
|
|
14
|
+
id,
|
|
15
|
+
createdAt: new Date(),
|
|
16
|
+
role: "assistant",
|
|
17
|
+
status: { type: "complete", reason: "unknown" },
|
|
18
|
+
content: toolCallId
|
|
19
|
+
? [
|
|
20
|
+
{
|
|
21
|
+
type: "tool-call",
|
|
22
|
+
toolCallId,
|
|
23
|
+
toolName: "task",
|
|
24
|
+
args: {},
|
|
25
|
+
argsText: "{}",
|
|
26
|
+
},
|
|
27
|
+
]
|
|
28
|
+
: [{ type: "text", text: id }],
|
|
29
|
+
metadata: {
|
|
30
|
+
unstable_state: null,
|
|
31
|
+
unstable_annotations: [],
|
|
32
|
+
unstable_data: [],
|
|
33
|
+
steps: [],
|
|
34
|
+
custom: {},
|
|
35
|
+
},
|
|
36
|
+
}) as ThreadMessage;
|
|
37
|
+
|
|
38
|
+
const transcript = (id: string) => [assistantMessage(id)];
|
|
39
|
+
|
|
40
|
+
describe("attachSubagentTranscripts", () => {
|
|
41
|
+
it("returns the input array when no tool call has a transcript", () => {
|
|
42
|
+
const messages = [assistantMessage("message", "unknown")];
|
|
43
|
+
|
|
44
|
+
expect(
|
|
45
|
+
attachSubagentTranscripts(
|
|
46
|
+
messages,
|
|
47
|
+
new Map([["task", transcript("transcript")]]),
|
|
48
|
+
createAttachMemo(),
|
|
49
|
+
),
|
|
50
|
+
).toBe(messages);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("attaches a transcript and preserves untouched message identities", () => {
|
|
54
|
+
const matching = assistantMessage("matching", "task");
|
|
55
|
+
const untouched = assistantMessage("untouched");
|
|
56
|
+
const messages = [matching, untouched];
|
|
57
|
+
const childTranscript = transcript("child");
|
|
58
|
+
|
|
59
|
+
const attached = attachSubagentTranscripts(
|
|
60
|
+
messages,
|
|
61
|
+
new Map([["task", childTranscript]]),
|
|
62
|
+
createAttachMemo(),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
expect(attached).not.toBe(messages);
|
|
66
|
+
expect(attached[0]).not.toBe(matching);
|
|
67
|
+
expect(attached[1]).toBe(untouched);
|
|
68
|
+
const part = attached[0]!.content[0]!;
|
|
69
|
+
expect(part.type).toBe("tool-call");
|
|
70
|
+
if (part.type !== "tool-call") throw new Error("expected a tool call");
|
|
71
|
+
expect(part.messages).toBe(childTranscript);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("reuses the attached array when transcript identities are unchanged", () => {
|
|
75
|
+
const messages = [assistantMessage("matching", "task")];
|
|
76
|
+
const childTranscript = transcript("child");
|
|
77
|
+
const memo = createAttachMemo();
|
|
78
|
+
|
|
79
|
+
const first = attachSubagentTranscripts(
|
|
80
|
+
messages,
|
|
81
|
+
new Map([["task", childTranscript]]),
|
|
82
|
+
memo,
|
|
83
|
+
);
|
|
84
|
+
const second = attachSubagentTranscripts(
|
|
85
|
+
messages,
|
|
86
|
+
new Map([["task", childTranscript]]),
|
|
87
|
+
memo,
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
expect(second).toBe(first);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("rebuilds only messages whose transcript changed", () => {
|
|
94
|
+
const messages = [
|
|
95
|
+
assistantMessage("first", "task-one"),
|
|
96
|
+
assistantMessage("second", "task-two"),
|
|
97
|
+
];
|
|
98
|
+
const firstTranscript = transcript("first-transcript");
|
|
99
|
+
const secondTranscript = transcript("second-transcript");
|
|
100
|
+
const memo = createAttachMemo();
|
|
101
|
+
const first = attachSubagentTranscripts(
|
|
102
|
+
messages,
|
|
103
|
+
new Map([
|
|
104
|
+
["task-one", firstTranscript],
|
|
105
|
+
["task-two", secondTranscript],
|
|
106
|
+
]),
|
|
107
|
+
memo,
|
|
108
|
+
);
|
|
109
|
+
const next = attachSubagentTranscripts(
|
|
110
|
+
messages,
|
|
111
|
+
new Map([
|
|
112
|
+
["task-one", transcript("updated-transcript")],
|
|
113
|
+
["task-two", secondTranscript],
|
|
114
|
+
]),
|
|
115
|
+
memo,
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
expect(next).not.toBe(first);
|
|
119
|
+
expect(next[0]).not.toBe(first[0]);
|
|
120
|
+
expect(next[1]).toBe(first[1]);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("rebuilds when the same transcript moves to another tool call", () => {
|
|
124
|
+
const first = assistantMessage("f", "call-a").content[0]!;
|
|
125
|
+
const second = assistantMessage("s", "call-b").content[0]!;
|
|
126
|
+
const message = {
|
|
127
|
+
...assistantMessage("pair"),
|
|
128
|
+
content: [first, second],
|
|
129
|
+
} as ThreadMessage;
|
|
130
|
+
const nested = transcript("nested");
|
|
131
|
+
const memo = createAttachMemo();
|
|
132
|
+
|
|
133
|
+
const [before] = attachSubagentTranscripts(
|
|
134
|
+
[message],
|
|
135
|
+
new Map([["call-a", nested]]),
|
|
136
|
+
memo,
|
|
137
|
+
);
|
|
138
|
+
const [after] = attachSubagentTranscripts(
|
|
139
|
+
[message],
|
|
140
|
+
new Map([["call-b", nested]]),
|
|
141
|
+
memo,
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
expect(before?.content[0]).toMatchObject({ messages: nested });
|
|
145
|
+
expect(after).not.toBe(before);
|
|
146
|
+
expect(after?.content[0]).toBe(first);
|
|
147
|
+
expect(after?.content[1]).toMatchObject({ messages: nested });
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("keeps untouched parts of a partially matched message by reference", () => {
|
|
151
|
+
const text = { type: "text" as const, text: "hello" };
|
|
152
|
+
const matched = assistantMessage("m", "call-matched").content[0]!;
|
|
153
|
+
const unmatched = assistantMessage("u", "call-unmatched").content[0]!;
|
|
154
|
+
const message = {
|
|
155
|
+
...assistantMessage("mixed"),
|
|
156
|
+
content: [text, matched, unmatched],
|
|
157
|
+
} as ThreadMessage;
|
|
158
|
+
const nested = transcript("nested");
|
|
159
|
+
|
|
160
|
+
const [attached] = attachSubagentTranscripts(
|
|
161
|
+
[message],
|
|
162
|
+
new Map([["call-matched", nested]]),
|
|
163
|
+
createAttachMemo(),
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
expect(attached?.content[0]).toBe(text);
|
|
167
|
+
expect(attached?.content[1]).toMatchObject({ messages: nested });
|
|
168
|
+
expect(attached?.content[2]).toBe(unmatched);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("preserves message and array external-store bindings", () => {
|
|
172
|
+
const message = assistantMessage("matching", "task");
|
|
173
|
+
const messages = [message];
|
|
174
|
+
const originalMessage = { id: "original-message" };
|
|
175
|
+
const originalMessages = [{ id: "original-messages" }];
|
|
176
|
+
bindExternalStoreMessage(message, originalMessage);
|
|
177
|
+
bindExternalStoreMessage(messages, originalMessages);
|
|
178
|
+
|
|
179
|
+
const attached = attachSubagentTranscripts(
|
|
180
|
+
messages,
|
|
181
|
+
new Map([["task", transcript("child")]]),
|
|
182
|
+
createAttachMemo(),
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
expect(getExternalStoreMessages(attached[0]!)).toEqual([originalMessage]);
|
|
186
|
+
expect(getExternalStoreMessages({ messages: attached })).toBe(
|
|
187
|
+
originalMessages,
|
|
188
|
+
);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import {
|
|
2
|
+
bindExternalStoreMessage,
|
|
3
|
+
getExternalStoreMessages,
|
|
4
|
+
type ThreadMessage,
|
|
5
|
+
} from "@assistant-ui/core";
|
|
6
|
+
|
|
7
|
+
type AttachedTranscript = readonly [string, readonly ThreadMessage[]];
|
|
8
|
+
|
|
9
|
+
type AttachedMessage = {
|
|
10
|
+
transcripts: readonly AttachedTranscript[];
|
|
11
|
+
attached: ThreadMessage;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type AttachMemo = {
|
|
15
|
+
messages: WeakMap<ThreadMessage, AttachedMessage>;
|
|
16
|
+
results: WeakMap<readonly ThreadMessage[], readonly ThreadMessage[]>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const createAttachMemo = (): AttachMemo => ({
|
|
20
|
+
messages: new WeakMap(),
|
|
21
|
+
results: new WeakMap(),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const sameTranscripts = (
|
|
25
|
+
a: readonly AttachedTranscript[],
|
|
26
|
+
b: readonly AttachedTranscript[],
|
|
27
|
+
) =>
|
|
28
|
+
a.length === b.length &&
|
|
29
|
+
a.every(
|
|
30
|
+
([toolCallId, transcript], index) =>
|
|
31
|
+
toolCallId === b[index]![0] && transcript === b[index]![1],
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
export const attachSubagentTranscripts = (
|
|
35
|
+
messages: readonly ThreadMessage[],
|
|
36
|
+
transcripts: ReadonlyMap<string, readonly ThreadMessage[]>,
|
|
37
|
+
memo: AttachMemo,
|
|
38
|
+
): readonly ThreadMessage[] => {
|
|
39
|
+
let attachedAny = false;
|
|
40
|
+
const next = messages.map((message) => {
|
|
41
|
+
const attachedTranscripts = message.content.flatMap((part) =>
|
|
42
|
+
part.type === "tool-call" && transcripts.has(part.toolCallId)
|
|
43
|
+
? [[part.toolCallId, transcripts.get(part.toolCallId)!] as const]
|
|
44
|
+
: [],
|
|
45
|
+
);
|
|
46
|
+
if (attachedTranscripts.length === 0) return message;
|
|
47
|
+
attachedAny = true;
|
|
48
|
+
|
|
49
|
+
const cached = memo.messages.get(message);
|
|
50
|
+
if (cached && sameTranscripts(cached.transcripts, attachedTranscripts))
|
|
51
|
+
return cached.attached;
|
|
52
|
+
|
|
53
|
+
const attached = {
|
|
54
|
+
...message,
|
|
55
|
+
content: message.content.map((part) =>
|
|
56
|
+
part.type === "tool-call" && transcripts.has(part.toolCallId)
|
|
57
|
+
? { ...part, messages: transcripts.get(part.toolCallId)! }
|
|
58
|
+
: part,
|
|
59
|
+
),
|
|
60
|
+
} as ThreadMessage;
|
|
61
|
+
memo.messages.set(message, { transcripts: attachedTranscripts, attached });
|
|
62
|
+
return attached;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (!attachedAny) return messages;
|
|
66
|
+
const previous = memo.results.get(messages);
|
|
67
|
+
if (previous && previous.every((message, index) => message === next[index]))
|
|
68
|
+
return previous;
|
|
69
|
+
bindExternalStoreMessage(next, getExternalStoreMessages({ messages }));
|
|
70
|
+
memo.results.set(messages, next);
|
|
71
|
+
return next;
|
|
72
|
+
};
|
|
@@ -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<
|
|
@@ -412,7 +432,7 @@ const useStreamThreadRuntime = (
|
|
|
412
432
|
...pickExternalStoreSharedOptions(options),
|
|
413
433
|
isRunning: stream.isLoading,
|
|
414
434
|
isLoading: stream.isThreadLoading,
|
|
415
|
-
messages:
|
|
435
|
+
messages: messagesWithTranscripts,
|
|
416
436
|
adapters,
|
|
417
437
|
extras,
|
|
418
438
|
unstable_enableToolInvocations: true,
|
|
@@ -631,6 +651,7 @@ export const useStreamRuntime = (rawOptions: UseStreamRuntimeOptions) => {
|
|
|
631
651
|
|
|
632
652
|
const aui = useAui();
|
|
633
653
|
const cloudAdapter = useCloudThreadListAdapter({
|
|
654
|
+
sdk: LANGCHAIN_SDK,
|
|
634
655
|
cloud,
|
|
635
656
|
create: createCloudThreadListAdapterCreateFallback(
|
|
636
657
|
create,
|