@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.
@@ -0,0 +1,304 @@
1
+ "use client";
2
+
3
+ import { useEffect, useRef, useSyncExternalStore } from "react";
4
+ import type { ThreadMessage } from "@assistant-ui/core";
5
+ import {
6
+ convertExternalMessages,
7
+ createExternalMessageConversionCache,
8
+ type ExternalMessageConversionCache,
9
+ type useExternalMessageConverter,
10
+ } from "@assistant-ui/core/react";
11
+ import { STREAM_CONTROLLER, type AnyStream } from "@langchain/react";
12
+ import type { BaseMessage } from "@langchain/core/messages";
13
+ import { messagesProjection } from "@langchain/langgraph-sdk/stream";
14
+ import type { SubagentDiscoverySnapshot } from "@langchain/react";
15
+ import {
16
+ attachSubagentTranscripts,
17
+ type AttachMemo,
18
+ createAttachMemo,
19
+ } from "./attachSubagentTranscripts";
20
+ import { convertLangChainBaseMessage } from "./convertMessages";
21
+ import type { LangChainBaseMessage, UIMessage } from "./types";
22
+
23
+ export const MAX_SUBAGENT_DEPTH = 16;
24
+
25
+ const TRANSCRIPT_METADATA = {};
26
+
27
+ type ProjectionStore = {
28
+ getSnapshot(): BaseMessage[];
29
+ subscribe(listener: () => void): () => void;
30
+ };
31
+
32
+ type ProjectionResource = {
33
+ snapshot: SubagentDiscoverySnapshot;
34
+ namespace: readonly string[];
35
+ store: ProjectionStore;
36
+ unsubscribe: () => void;
37
+ release: () => void;
38
+ storeSnapshot: BaseMessage[] | undefined;
39
+ status: SubagentDiscoverySnapshot["status"] | undefined;
40
+ uiMessages: readonly UIMessage[];
41
+ converted: readonly ThreadMessage[] | undefined;
42
+ childTranscripts: ReadonlyMap<string, readonly ThreadMessage[]> | undefined;
43
+ transcript: readonly ThreadMessage[] | undefined;
44
+ memo: AttachMemo;
45
+ cache: ExternalMessageConversionCache;
46
+ };
47
+
48
+ type SubagentTranscriptSource = {
49
+ resources: Map<string, ProjectionResource>;
50
+ requestedNamespaceIds: Set<string>;
51
+ snapshot: ReadonlyMap<string, readonly ThreadMessage[]>;
52
+ listeners: Set<() => void>;
53
+ controller: AnyStream[typeof STREAM_CONTROLLER] | undefined;
54
+ uiMessagesByParent: Map<string, UIMessage[]>;
55
+ convert: useExternalMessageConverter.Callback<LangChainBaseMessage>;
56
+ subscribe(listener: () => void): () => void;
57
+ getSnapshot(): ReadonlyMap<string, readonly ThreadMessage[]>;
58
+ reconcile(
59
+ controller: AnyStream[typeof STREAM_CONTROLLER],
60
+ subagents: AnyStream["subagents"],
61
+ uiMessagesByParent: Map<string, UIMessage[]>,
62
+ ): void;
63
+ dispose(): void;
64
+ };
65
+
66
+ const sameNamespace = (a: readonly string[], b: readonly string[]) =>
67
+ a.length === b.length && a.every((segment, index) => segment === b[index]);
68
+
69
+ const sameTranscriptEntries = (
70
+ a: ReadonlyMap<string, readonly ThreadMessage[]> | undefined,
71
+ b: ReadonlyMap<string, readonly ThreadMessage[]>,
72
+ ) =>
73
+ a?.size === b.size &&
74
+ [...b].every(([id, transcript]) => a.get(id) === transcript);
75
+
76
+ const collectUIMessages = (
77
+ messages: readonly BaseMessage[],
78
+ uiMessagesByParent: Map<string, UIMessage[]>,
79
+ ) => {
80
+ const collected: UIMessage[] = [];
81
+ if (uiMessagesByParent.size === 0) return collected;
82
+ for (const message of messages) {
83
+ const uiMessages = message.id && uiMessagesByParent.get(message.id);
84
+ if (uiMessages) collected.push(...uiMessages);
85
+ }
86
+ return collected;
87
+ };
88
+
89
+ const sameUIMessages = (a: readonly UIMessage[], b: readonly UIMessage[]) =>
90
+ a.length === b.length && a.every((ui, index) => ui === b[index]);
91
+
92
+ const convertWithUIMessages =
93
+ (
94
+ uiMessagesByParent: Map<string, UIMessage[]>,
95
+ ): useExternalMessageConverter.Callback<LangChainBaseMessage> =>
96
+ (message, metadata) =>
97
+ convertLangChainBaseMessage(message, { ...metadata, uiMessagesByParent });
98
+
99
+ const createSubagentTranscriptSource = (): SubagentTranscriptSource => {
100
+ const uiMessagesByParent = new Map<string, UIMessage[]>();
101
+ const source: SubagentTranscriptSource = {
102
+ resources: new Map(),
103
+ requestedNamespaceIds: new Set(),
104
+ snapshot: new Map(),
105
+ listeners: new Set(),
106
+ controller: undefined,
107
+ uiMessagesByParent,
108
+ convert: convertWithUIMessages(uiMessagesByParent),
109
+ subscribe(listener) {
110
+ source.listeners.add(listener);
111
+ return () => source.listeners.delete(listener);
112
+ },
113
+ getSnapshot() {
114
+ return source.snapshot;
115
+ },
116
+ reconcile(controller, subagents, uiMessagesByParent) {
117
+ if (source.uiMessagesByParent !== uiMessagesByParent) {
118
+ source.uiMessagesByParent = uiMessagesByParent;
119
+ source.convert = convertWithUIMessages(uiMessagesByParent);
120
+ }
121
+
122
+ if (source.controller !== controller) {
123
+ source.dispose();
124
+ source.controller = controller;
125
+ }
126
+
127
+ for (const id of source.requestedNamespaceIds) {
128
+ if (!subagents.has(id)) source.requestedNamespaceIds.delete(id);
129
+ }
130
+
131
+ for (const [id, resource] of source.resources) {
132
+ const snapshot = subagents.get(id);
133
+ if (
134
+ !snapshot ||
135
+ snapshot.depth > MAX_SUBAGENT_DEPTH ||
136
+ !sameNamespace(resource.namespace, snapshot.namespace)
137
+ ) {
138
+ resource.unsubscribe();
139
+ resource.release();
140
+ source.resources.delete(id);
141
+ continue;
142
+ }
143
+ resource.snapshot = snapshot;
144
+ }
145
+
146
+ for (const snapshot of subagents.values()) {
147
+ if (snapshot.depth > MAX_SUBAGENT_DEPTH) continue;
148
+ if (!source.requestedNamespaceIds.has(snapshot.id)) {
149
+ source.requestedNamespaceIds.add(snapshot.id);
150
+ void controller.resolveSubagentNamespace(snapshot.id).catch(() => {
151
+ if (source.controller === controller)
152
+ source.requestedNamespaceIds.delete(snapshot.id);
153
+ });
154
+ }
155
+ if (source.resources.has(snapshot.id)) continue;
156
+ const acquired = controller.registry.acquire(
157
+ messagesProjection(snapshot.namespace),
158
+ );
159
+ const resource: ProjectionResource = {
160
+ snapshot,
161
+ namespace: snapshot.namespace,
162
+ store: acquired.store,
163
+ unsubscribe: () => {},
164
+ release: acquired.release,
165
+ storeSnapshot: undefined,
166
+ status: undefined,
167
+ uiMessages: [],
168
+ converted: undefined,
169
+ childTranscripts: undefined,
170
+ transcript: undefined,
171
+ memo: createAttachMemo(),
172
+ cache: createExternalMessageConversionCache(),
173
+ };
174
+ resource.unsubscribe = resource.store.subscribe(() => rebuild());
175
+ source.resources.set(snapshot.id, resource);
176
+ }
177
+
178
+ rebuild();
179
+ },
180
+ dispose() {
181
+ for (const resource of source.resources.values()) {
182
+ resource.unsubscribe();
183
+ resource.release();
184
+ }
185
+ source.resources.clear();
186
+ source.requestedNamespaceIds.clear();
187
+ source.snapshot = new Map();
188
+ for (const listener of source.listeners) listener();
189
+ },
190
+ };
191
+
192
+ const rebuild = () => {
193
+ const { convert, uiMessagesByParent } = source;
194
+ const resources = [...source.resources.values()];
195
+ const childrenByParent = new Map<string, ProjectionResource[]>();
196
+
197
+ for (const resource of resources) {
198
+ const parentId = resource.snapshot.parentId;
199
+ if (parentId == null) continue;
200
+ const children = childrenByParent.get(parentId);
201
+ if (children) children.push(resource);
202
+ else childrenByParent.set(parentId, [resource]);
203
+ }
204
+
205
+ const transcripts = new Map<string, readonly ThreadMessage[]>();
206
+ let changed = source.snapshot.size !== resources.length;
207
+ const built = new Set<string>();
208
+
209
+ const build = (resource: ProjectionResource, depth: number) => {
210
+ if (built.has(resource.snapshot.id)) return;
211
+ built.add(resource.snapshot.id);
212
+ const children =
213
+ depth < MAX_SUBAGENT_DEPTH
214
+ ? (childrenByParent.get(resource.snapshot.id) ?? [])
215
+ : [];
216
+ for (const child of children) build(child, depth + 1);
217
+ const childTranscripts = new Map(
218
+ children.flatMap((child) =>
219
+ child.transcript
220
+ ? [[child.snapshot.id, child.transcript] as const]
221
+ : [],
222
+ ),
223
+ );
224
+ const storeSnapshot = resource.store.getSnapshot();
225
+ const status = resource.snapshot.status;
226
+ const uiMessages = collectUIMessages(storeSnapshot, uiMessagesByParent);
227
+
228
+ const conversionChanged =
229
+ resource.converted === undefined ||
230
+ resource.storeSnapshot !== storeSnapshot ||
231
+ resource.status !== status ||
232
+ !sameUIMessages(resource.uiMessages, uiMessages);
233
+ if (conversionChanged) {
234
+ resource.converted = convertExternalMessages(
235
+ storeSnapshot as LangChainBaseMessage[],
236
+ convert,
237
+ status === "running",
238
+ TRANSCRIPT_METADATA,
239
+ resource.cache,
240
+ );
241
+ resource.storeSnapshot = storeSnapshot;
242
+ resource.status = status;
243
+ resource.uiMessages = uiMessages;
244
+ }
245
+
246
+ if (
247
+ resource.transcript === undefined ||
248
+ conversionChanged ||
249
+ !sameTranscriptEntries(resource.childTranscripts, childTranscripts)
250
+ ) {
251
+ const transcript = attachSubagentTranscripts(
252
+ resource.converted!,
253
+ childTranscripts,
254
+ resource.memo,
255
+ );
256
+ changed ||= resource.transcript !== transcript;
257
+ resource.transcript = transcript;
258
+ resource.childTranscripts = childTranscripts;
259
+ }
260
+
261
+ if (!source.snapshot.has(resource.snapshot.id)) changed = true;
262
+ transcripts.set(resource.snapshot.id, resource.transcript);
263
+ };
264
+
265
+ for (const resource of resources) {
266
+ if (
267
+ resource.snapshot.parentId == null ||
268
+ !source.resources.has(resource.snapshot.parentId)
269
+ )
270
+ build(resource, 1);
271
+ }
272
+ for (const resource of resources) build(resource, MAX_SUBAGENT_DEPTH);
273
+
274
+ if (!changed) return;
275
+ source.snapshot = transcripts;
276
+ for (const listener of source.listeners) listener();
277
+ };
278
+
279
+ return source;
280
+ };
281
+
282
+ export const useSubagentTranscripts = (
283
+ stream: AnyStream,
284
+ uiMessagesByParent: Map<string, UIMessage[]>,
285
+ ): ReadonlyMap<string, readonly ThreadMessage[]> => {
286
+ const sourceRef = useRef<SubagentTranscriptSource | undefined>(undefined);
287
+ if (!sourceRef.current) {
288
+ sourceRef.current = createSubagentTranscriptSource();
289
+ }
290
+ const source = sourceRef.current;
291
+ const controller = stream[STREAM_CONTROLLER];
292
+
293
+ useEffect(() => {
294
+ source.reconcile(controller, stream.subagents, uiMessagesByParent);
295
+ }, [controller, source, stream.subagents, uiMessagesByParent]);
296
+
297
+ useEffect(() => () => source.dispose(), [source]);
298
+
299
+ return useSyncExternalStore(
300
+ source.subscribe,
301
+ source.getSnapshot,
302
+ source.getSnapshot,
303
+ );
304
+ };