@assistant-ui/react-langchain 0.0.31 → 0.0.32
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/attachSubagentTranscripts.d.ts +14 -6
- package/dist/attachSubagentTranscripts.d.ts.map +1 -1
- package/dist/attachSubagentTranscripts.js +8 -4
- package/dist/attachSubagentTranscripts.js.map +1 -1
- package/dist/convertMessages.d.ts +3 -3
- package/dist/convertMessages.d.ts.map +1 -1
- package/dist/converter.d.ts +9 -10
- package/dist/converter.d.ts.map +1 -1
- package/dist/findForkCheckpointInHistory.d.ts +2 -3
- package/dist/findForkCheckpointInHistory.d.ts.map +1 -1
- package/dist/hooks.d.ts +14 -15
- package/dist/hooks.d.ts.map +1 -1
- package/dist/resolveForkCheckpoint.d.ts +1 -2
- package/dist/resolveForkCheckpoint.d.ts.map +1 -1
- package/dist/runtimeExtras.d.ts +1 -2
- package/dist/runtimeExtras.d.ts.map +1 -1
- package/dist/sdkIdentity.d.ts +1 -2
- package/dist/sdkIdentity.d.ts.map +1 -1
- package/dist/sdkIdentity.js +1 -1
- package/dist/streamingTiming.d.ts +2 -3
- package/dist/streamingTiming.d.ts.map +1 -1
- package/dist/types.d.ts +8 -8
- package/dist/types.d.ts.map +1 -1
- package/dist/uiMessages.d.ts +22 -7
- package/dist/uiMessages.d.ts.map +1 -1
- package/dist/uiMessages.js +23 -5
- package/dist/uiMessages.js.map +1 -1
- package/dist/useStreamRuntime.d.ts +3 -3
- package/dist/useStreamRuntime.d.ts.map +1 -1
- package/dist/useStreamRuntime.js +4 -4
- package/dist/useStreamRuntime.js.map +1 -1
- package/dist/useSubagentTranscripts.d.ts +3 -4
- package/dist/useSubagentTranscripts.d.ts.map +1 -1
- package/dist/useSubagentTranscripts.js +127 -33
- package/dist/useSubagentTranscripts.js.map +1 -1
- package/package.json +12 -11
- package/src/attachSubagentTranscripts.test.ts +13 -5
- package/src/attachSubagentTranscripts.ts +26 -7
- package/src/uiMessages.test.ts +43 -0
- package/src/uiMessages.ts +36 -4
- package/src/useStreamRuntime.test.tsx +112 -0
- package/src/useStreamRuntime.ts +9 -6
- package/src/useSubagentTranscripts.test.tsx +795 -52
- package/src/useSubagentTranscripts.ts +271 -49
|
@@ -7,6 +7,7 @@ import { describe, expect, it } from "vitest";
|
|
|
7
7
|
import {
|
|
8
8
|
attachSubagentTranscripts,
|
|
9
9
|
createAttachMemo,
|
|
10
|
+
type SubagentTranscript,
|
|
10
11
|
} from "./attachSubagentTranscripts";
|
|
11
12
|
|
|
12
13
|
const assistantMessage = (id: string, toolCallId?: string): ThreadMessage =>
|
|
@@ -35,7 +36,13 @@ const assistantMessage = (id: string, toolCallId?: string): ThreadMessage =>
|
|
|
35
36
|
},
|
|
36
37
|
}) as ThreadMessage;
|
|
37
38
|
|
|
38
|
-
const transcript = (
|
|
39
|
+
const transcript = (
|
|
40
|
+
id: string,
|
|
41
|
+
timing: SubagentTranscript["timing"] = {
|
|
42
|
+
startedAt: 1_000,
|
|
43
|
+
completedAt: 3_500,
|
|
44
|
+
},
|
|
45
|
+
): SubagentTranscript => ({ messages: [assistantMessage(id)], timing });
|
|
39
46
|
|
|
40
47
|
describe("attachSubagentTranscripts", () => {
|
|
41
48
|
it("returns the input array when no tool call has a transcript", () => {
|
|
@@ -68,7 +75,8 @@ describe("attachSubagentTranscripts", () => {
|
|
|
68
75
|
const part = attached[0]!.content[0]!;
|
|
69
76
|
expect(part.type).toBe("tool-call");
|
|
70
77
|
if (part.type !== "tool-call") throw new Error("expected a tool call");
|
|
71
|
-
expect(part.messages).toBe(childTranscript);
|
|
78
|
+
expect(part.messages).toBe(childTranscript.messages);
|
|
79
|
+
expect(part.timing).toBe(childTranscript.timing);
|
|
72
80
|
});
|
|
73
81
|
|
|
74
82
|
it("reuses the attached array when transcript identities are unchanged", () => {
|
|
@@ -141,10 +149,10 @@ describe("attachSubagentTranscripts", () => {
|
|
|
141
149
|
memo,
|
|
142
150
|
);
|
|
143
151
|
|
|
144
|
-
expect(before?.content[0]).toMatchObject({ messages: nested });
|
|
152
|
+
expect(before?.content[0]).toMatchObject({ messages: nested.messages });
|
|
145
153
|
expect(after).not.toBe(before);
|
|
146
154
|
expect(after?.content[0]).toBe(first);
|
|
147
|
-
expect(after?.content[1]).toMatchObject({ messages: nested });
|
|
155
|
+
expect(after?.content[1]).toMatchObject({ messages: nested.messages });
|
|
148
156
|
});
|
|
149
157
|
|
|
150
158
|
it("keeps untouched parts of a partially matched message by reference", () => {
|
|
@@ -164,7 +172,7 @@ describe("attachSubagentTranscripts", () => {
|
|
|
164
172
|
);
|
|
165
173
|
|
|
166
174
|
expect(attached?.content[0]).toBe(text);
|
|
167
|
-
expect(attached?.content[1]).toMatchObject({ messages: nested });
|
|
175
|
+
expect(attached?.content[1]).toMatchObject({ messages: nested.messages });
|
|
168
176
|
expect(attached?.content[2]).toBe(unmatched);
|
|
169
177
|
});
|
|
170
178
|
|
|
@@ -2,9 +2,20 @@ import {
|
|
|
2
2
|
bindExternalStoreMessage,
|
|
3
3
|
getExternalStoreMessages,
|
|
4
4
|
type ThreadMessage,
|
|
5
|
+
type ToolCallTiming,
|
|
5
6
|
} from "@assistant-ui/core";
|
|
6
7
|
|
|
7
|
-
type
|
|
8
|
+
export type SubagentTranscript = {
|
|
9
|
+
readonly messages: readonly ThreadMessage[];
|
|
10
|
+
/**
|
|
11
|
+
* Only present when the client watched the task run: the discovery
|
|
12
|
+
* timestamps are stamped when the task call is first seen, so a thread
|
|
13
|
+
* seeded from a checkpoint would report a duration of zero.
|
|
14
|
+
*/
|
|
15
|
+
readonly timing?: ToolCallTiming;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type AttachedTranscript = readonly [string, SubagentTranscript];
|
|
8
19
|
|
|
9
20
|
type AttachedMessage = {
|
|
10
21
|
transcripts: readonly AttachedTranscript[];
|
|
@@ -33,7 +44,7 @@ const sameTranscripts = (
|
|
|
33
44
|
|
|
34
45
|
export const attachSubagentTranscripts = (
|
|
35
46
|
messages: readonly ThreadMessage[],
|
|
36
|
-
transcripts: ReadonlyMap<string,
|
|
47
|
+
transcripts: ReadonlyMap<string, SubagentTranscript>,
|
|
37
48
|
memo: AttachMemo,
|
|
38
49
|
): readonly ThreadMessage[] => {
|
|
39
50
|
let attachedAny = false;
|
|
@@ -52,11 +63,19 @@ export const attachSubagentTranscripts = (
|
|
|
52
63
|
|
|
53
64
|
const attached = {
|
|
54
65
|
...message,
|
|
55
|
-
content: message.content.map((part) =>
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
66
|
+
content: message.content.map((part) => {
|
|
67
|
+
const transcript =
|
|
68
|
+
part.type === "tool-call"
|
|
69
|
+
? transcripts.get(part.toolCallId)
|
|
70
|
+
: undefined;
|
|
71
|
+
return transcript
|
|
72
|
+
? {
|
|
73
|
+
...part,
|
|
74
|
+
messages: transcript.messages,
|
|
75
|
+
...(transcript.timing && { timing: transcript.timing }),
|
|
76
|
+
}
|
|
77
|
+
: part;
|
|
78
|
+
}),
|
|
60
79
|
} as ThreadMessage;
|
|
61
80
|
memo.messages.set(message, { transcripts: attachedTranscripts, attached });
|
|
62
81
|
return attached;
|
package/src/uiMessages.test.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import {
|
|
3
3
|
applyUIUpdate,
|
|
4
|
+
createUIFoldMemo,
|
|
4
5
|
extractUIUpdate,
|
|
5
6
|
foldUIUpdates,
|
|
6
7
|
isUIUpdate,
|
|
@@ -187,4 +188,46 @@ describe("foldUIUpdates", () => {
|
|
|
187
188
|
it("returns an empty list for no events", () => {
|
|
188
189
|
expect(foldUIUpdates([])).toEqual([]);
|
|
189
190
|
});
|
|
191
|
+
|
|
192
|
+
const merge = (id: string, props: Record<string, unknown>) => ({
|
|
193
|
+
...ui(id, props),
|
|
194
|
+
metadata: { merge: true },
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("keeps the previous list when appended events carry no UI update", () => {
|
|
198
|
+
const memo = createUIFoldMemo();
|
|
199
|
+
const events = [evt(ui("a", { x: 1 })), evt(merge("a", { y: 2 }))];
|
|
200
|
+
const folded = foldUIUpdates(events, memo);
|
|
201
|
+
|
|
202
|
+
expect(foldUIUpdates([...events, evt({ progress: 1 })], memo)).toBe(folded);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("keeps entries that appended updates do not touch", () => {
|
|
206
|
+
const memo = createUIFoldMemo();
|
|
207
|
+
const events = [evt(ui("a", { x: 1 })), evt(merge("a", { y: 2 }))];
|
|
208
|
+
const [merged] = foldUIUpdates(events, memo);
|
|
209
|
+
|
|
210
|
+
const result = foldUIUpdates([...events, evt(ui("b"))], memo);
|
|
211
|
+
|
|
212
|
+
expect(result).toEqual([merge("a", { x: 1, y: 2 }), ui("b")]);
|
|
213
|
+
expect(result[0]).toBe(merged);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("keeps folded entries when the buffer drops its oldest events", () => {
|
|
217
|
+
const memo = createUIFoldMemo();
|
|
218
|
+
const progress = evt({ progress: 1 });
|
|
219
|
+
foldUIUpdates([evt(ui("a", { x: 1 })), progress], memo);
|
|
220
|
+
|
|
221
|
+
expect(foldUIUpdates([progress, evt(merge("a", { y: 2 }))], memo)).toEqual([
|
|
222
|
+
merge("a", { x: 1, y: 2 }),
|
|
223
|
+
]);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("folds from scratch when the buffer is replaced", () => {
|
|
227
|
+
const memo = createUIFoldMemo();
|
|
228
|
+
foldUIUpdates([evt(ui("a"))], memo);
|
|
229
|
+
|
|
230
|
+
expect(foldUIUpdates([evt(ui("b"))], memo)).toEqual([ui("b")]);
|
|
231
|
+
expect(foldUIUpdates([], memo)).toEqual([]);
|
|
232
|
+
});
|
|
190
233
|
});
|
package/src/uiMessages.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import type { Channel } from "@langchain/react";
|
|
1
2
|
import type { RemoveUIMessage, UIMessage } from "./types";
|
|
2
3
|
|
|
4
|
+
/** Channels the generative-UI fold reads, at the root and per subagent. */
|
|
5
|
+
export const UI_CUSTOM_CHANNELS: readonly Channel[] = ["custom"];
|
|
6
|
+
|
|
3
7
|
export type UIUpdate = UIMessage | RemoveUIMessage;
|
|
4
8
|
|
|
5
9
|
export const isUIUpdate = (
|
|
@@ -62,12 +66,40 @@ export const extractUIUpdate = (
|
|
|
62
66
|
return undefined;
|
|
63
67
|
};
|
|
64
68
|
|
|
65
|
-
export
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
+
export type UIFoldMemo = {
|
|
70
|
+
events: readonly unknown[];
|
|
71
|
+
messages: UIMessage[];
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const createUIFoldMemo = (): UIFoldMemo => ({
|
|
75
|
+
events: [],
|
|
76
|
+
messages: [],
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Folds `custom`-channel events into UI messages, continuing the fold held in
|
|
81
|
+
* `memo`. The channel buffer holds each event object once, appends new events
|
|
82
|
+
* and drops its oldest ones once full, so only events after the previously
|
|
83
|
+
* folded last event are applied, and entries no later event touches keep
|
|
84
|
+
* their identity. A buffer that no longer contains that event was replaced
|
|
85
|
+
* and is folded from scratch.
|
|
86
|
+
*/
|
|
87
|
+
export const foldUIUpdates = (
|
|
88
|
+
events: readonly unknown[],
|
|
89
|
+
memo: UIFoldMemo = createUIFoldMemo(),
|
|
90
|
+
): UIMessage[] => {
|
|
91
|
+
const previous = memo.events;
|
|
92
|
+
const resumeAt =
|
|
93
|
+
previous.length === 0
|
|
94
|
+
? 0
|
|
95
|
+
: events.lastIndexOf(previous.at(-1), previous.length - 1) + 1;
|
|
96
|
+
let acc = previous.length > 0 && resumeAt === 0 ? [] : memo.messages;
|
|
97
|
+
for (let i = resumeAt; i < events.length; i++) {
|
|
98
|
+
const update = extractUIUpdate(events[i]);
|
|
69
99
|
if (update) acc = applyUIUpdate(acc, update);
|
|
70
100
|
}
|
|
101
|
+
memo.events = events;
|
|
102
|
+
memo.messages = acc;
|
|
71
103
|
return acc;
|
|
72
104
|
};
|
|
73
105
|
|
|
@@ -1015,6 +1015,8 @@ describe("useStreamRuntime subagent transcripts", () => {
|
|
|
1015
1015
|
status: "running",
|
|
1016
1016
|
parentId: null,
|
|
1017
1017
|
depth: 1,
|
|
1018
|
+
startedAt: new Date(1_000),
|
|
1019
|
+
completedAt: null,
|
|
1018
1020
|
},
|
|
1019
1021
|
],
|
|
1020
1022
|
]);
|
|
@@ -1069,4 +1071,114 @@ describe("useStreamRuntime subagent transcripts", () => {
|
|
|
1069
1071
|
|
|
1070
1072
|
expect(nestedTranscript()).toBe(rendered);
|
|
1071
1073
|
});
|
|
1074
|
+
|
|
1075
|
+
it("keeps messages and transcripts when custom events carry no UI update", async () => {
|
|
1076
|
+
const stream = createMockStream([
|
|
1077
|
+
message("human-1", "human", "delegate"),
|
|
1078
|
+
{
|
|
1079
|
+
id: "root-ai",
|
|
1080
|
+
_getType: () => "ai",
|
|
1081
|
+
content: "",
|
|
1082
|
+
tool_calls: [{ id: "task-one", name: "task", args: {} }],
|
|
1083
|
+
},
|
|
1084
|
+
]);
|
|
1085
|
+
const transcript = [message("nested-ai", "ai", "nested answer")];
|
|
1086
|
+
stream.subagents = new Map([
|
|
1087
|
+
[
|
|
1088
|
+
"task-one",
|
|
1089
|
+
{
|
|
1090
|
+
id: "task-one",
|
|
1091
|
+
namespace: ["tools:task-one"],
|
|
1092
|
+
status: "running",
|
|
1093
|
+
parentId: null,
|
|
1094
|
+
depth: 1,
|
|
1095
|
+
startedAt: new Date(1_000),
|
|
1096
|
+
completedAt: null,
|
|
1097
|
+
},
|
|
1098
|
+
],
|
|
1099
|
+
]);
|
|
1100
|
+
stream[streamController]!.registry.acquire.mockReturnValue({
|
|
1101
|
+
store: { getSnapshot: () => transcript, subscribe: () => () => {} },
|
|
1102
|
+
release: vi.fn(),
|
|
1103
|
+
});
|
|
1104
|
+
const chartEvent = (
|
|
1105
|
+
props: Record<string, unknown>,
|
|
1106
|
+
metadata: Record<string, unknown> = {},
|
|
1107
|
+
) => ({
|
|
1108
|
+
method: "custom",
|
|
1109
|
+
params: {
|
|
1110
|
+
namespace: ["tools:task-one"],
|
|
1111
|
+
data: {
|
|
1112
|
+
type: "ui",
|
|
1113
|
+
id: "ui-1",
|
|
1114
|
+
name: "chart",
|
|
1115
|
+
props,
|
|
1116
|
+
metadata: { message_id: "nested-ai", ...metadata },
|
|
1117
|
+
},
|
|
1118
|
+
},
|
|
1119
|
+
});
|
|
1120
|
+
const events = [
|
|
1121
|
+
chartEvent({ points: [1, 2] }),
|
|
1122
|
+
chartEvent({ label: "first" }, { merge: true }),
|
|
1123
|
+
];
|
|
1124
|
+
mockUseChannel.mockReturnValue(events);
|
|
1125
|
+
const { auiResult, rerender } = renderAui(stream);
|
|
1126
|
+
const nestedTranscript = () => {
|
|
1127
|
+
const { messages } = auiResult.current.thread.getState();
|
|
1128
|
+
for (const threadMessage of messages) {
|
|
1129
|
+
for (const part of threadMessage.content) {
|
|
1130
|
+
if (part.type === "tool-call" && part.toolCallId === "task-one")
|
|
1131
|
+
return part.messages;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
return undefined;
|
|
1135
|
+
};
|
|
1136
|
+
|
|
1137
|
+
await waitFor(() =>
|
|
1138
|
+
expect(nestedTranscript()?.[0]?.content).toMatchObject([
|
|
1139
|
+
{ type: "text", text: "nested answer" },
|
|
1140
|
+
{
|
|
1141
|
+
type: "data",
|
|
1142
|
+
name: "chart",
|
|
1143
|
+
data: { points: [1, 2], label: "first" },
|
|
1144
|
+
},
|
|
1145
|
+
]),
|
|
1146
|
+
);
|
|
1147
|
+
const [human, ai] = auiResult.current.thread.getState().messages;
|
|
1148
|
+
const rendered = nestedTranscript();
|
|
1149
|
+
|
|
1150
|
+
const withProgress = [
|
|
1151
|
+
...events,
|
|
1152
|
+
{
|
|
1153
|
+
method: "custom",
|
|
1154
|
+
params: { namespace: [], data: { name: "progress", payload: 1 } },
|
|
1155
|
+
},
|
|
1156
|
+
];
|
|
1157
|
+
mockUseChannel.mockReturnValue(withProgress);
|
|
1158
|
+
await act(async () => {
|
|
1159
|
+
rerender();
|
|
1160
|
+
});
|
|
1161
|
+
|
|
1162
|
+
const messages = auiResult.current.thread.getState().messages;
|
|
1163
|
+
expect(messages[0]).toBe(human);
|
|
1164
|
+
expect(messages[1]).toBe(ai);
|
|
1165
|
+
expect(nestedTranscript()).toBe(rendered);
|
|
1166
|
+
|
|
1167
|
+
mockUseChannel.mockReturnValue([
|
|
1168
|
+
...withProgress,
|
|
1169
|
+
chartEvent({ label: "second" }, { merge: true }),
|
|
1170
|
+
]);
|
|
1171
|
+
await act(async () => {
|
|
1172
|
+
rerender();
|
|
1173
|
+
});
|
|
1174
|
+
|
|
1175
|
+
expect(nestedTranscript()?.[0]?.content).toMatchObject([
|
|
1176
|
+
{ type: "text", text: "nested answer" },
|
|
1177
|
+
{
|
|
1178
|
+
type: "data",
|
|
1179
|
+
name: "chart",
|
|
1180
|
+
data: { points: [1, 2], label: "second" },
|
|
1181
|
+
},
|
|
1182
|
+
]);
|
|
1183
|
+
});
|
|
1072
1184
|
});
|
package/src/useStreamRuntime.ts
CHANGED
|
@@ -29,7 +29,6 @@ import {
|
|
|
29
29
|
} from "@assistant-ui/core/react";
|
|
30
30
|
import { useAui, useAuiState } from "@assistant-ui/store";
|
|
31
31
|
import { STREAM_CONTROLLER, useChannel, useStream } from "@langchain/react";
|
|
32
|
-
import type { Channel } from "@langchain/react";
|
|
33
32
|
import type {
|
|
34
33
|
LangChainBaseMessage,
|
|
35
34
|
LangChainToolCall,
|
|
@@ -48,14 +47,17 @@ import {
|
|
|
48
47
|
createAttachMemo,
|
|
49
48
|
} from "./attachSubagentTranscripts";
|
|
50
49
|
import { useSubagentTranscripts } from "./useSubagentTranscripts";
|
|
51
|
-
import {
|
|
50
|
+
import {
|
|
51
|
+
createUIFoldMemo,
|
|
52
|
+
foldUIUpdates,
|
|
53
|
+
mergeUIMessages,
|
|
54
|
+
UI_CUSTOM_CHANNELS,
|
|
55
|
+
} from "./uiMessages";
|
|
52
56
|
import { langChainExtras } from "./runtimeExtras";
|
|
53
57
|
import { resolveForkCheckpoint } from "./resolveForkCheckpoint";
|
|
54
58
|
import { useLangChainStreamingTiming } from "./streamingTiming";
|
|
55
59
|
import { LANGCHAIN_SDK } from "./sdkIdentity";
|
|
56
60
|
|
|
57
|
-
const UI_CUSTOM_CHANNELS: readonly Channel[] = ["custom"];
|
|
58
|
-
|
|
59
61
|
export const runConfigToSubmitOptions = (
|
|
60
62
|
runConfig: AppendMessage["runConfig"],
|
|
61
63
|
) =>
|
|
@@ -167,9 +169,10 @@ const useStreamThreadRuntime = (
|
|
|
167
169
|
const uiStateValue = stream.values[uiStateKey];
|
|
168
170
|
|
|
169
171
|
const customEvents = useChannel(stream, UI_CUSTOM_CHANNELS);
|
|
172
|
+
const [uiFoldMemo] = useState(createUIFoldMemo);
|
|
170
173
|
const liveUiMessages = useMemo(
|
|
171
|
-
() => foldUIUpdates(customEvents),
|
|
172
|
-
[customEvents],
|
|
174
|
+
() => foldUIUpdates(customEvents, uiFoldMemo),
|
|
175
|
+
[customEvents, uiFoldMemo],
|
|
173
176
|
);
|
|
174
177
|
|
|
175
178
|
const mergedUiMessages = useMemo(
|