@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
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { attachSubagentTranscripts, createAttachMemo } from "./attachSubagentTranscripts.js";
|
|
3
|
+
import { convertLangChainBaseMessage } from "./convertMessages.js";
|
|
4
|
+
import { convertExternalMessages, createExternalMessageConversionCache } from "@assistant-ui/core/react";
|
|
5
|
+
import { useEffect, useRef, useSyncExternalStore } from "react";
|
|
6
|
+
import { STREAM_CONTROLLER } from "@langchain/react";
|
|
7
|
+
import { messagesProjection } from "@langchain/langgraph-sdk/stream";
|
|
8
|
+
//#region src/useSubagentTranscripts.ts
|
|
9
|
+
const MAX_SUBAGENT_DEPTH = 16;
|
|
10
|
+
const TRANSCRIPT_METADATA = {};
|
|
11
|
+
const sameNamespace = (a, b) => a.length === b.length && a.every((segment, index) => segment === b[index]);
|
|
12
|
+
const sameTranscriptEntries = (a, b) => a?.size === b.size && [...b].every(([id, transcript]) => a.get(id) === transcript);
|
|
13
|
+
const collectUIMessages = (messages, uiMessagesByParent) => {
|
|
14
|
+
const collected = [];
|
|
15
|
+
if (uiMessagesByParent.size === 0) return collected;
|
|
16
|
+
for (const message of messages) {
|
|
17
|
+
const uiMessages = message.id && uiMessagesByParent.get(message.id);
|
|
18
|
+
if (uiMessages) collected.push(...uiMessages);
|
|
19
|
+
}
|
|
20
|
+
return collected;
|
|
21
|
+
};
|
|
22
|
+
const sameUIMessages = (a, b) => a.length === b.length && a.every((ui, index) => ui === b[index]);
|
|
23
|
+
const convertWithUIMessages = (uiMessagesByParent) => (message, metadata) => convertLangChainBaseMessage(message, {
|
|
24
|
+
...metadata,
|
|
25
|
+
uiMessagesByParent
|
|
26
|
+
});
|
|
27
|
+
const createSubagentTranscriptSource = () => {
|
|
28
|
+
const uiMessagesByParent = /* @__PURE__ */ new Map();
|
|
29
|
+
const source = {
|
|
30
|
+
resources: /* @__PURE__ */ new Map(),
|
|
31
|
+
requestedNamespaceIds: /* @__PURE__ */ new Set(),
|
|
32
|
+
snapshot: /* @__PURE__ */ new Map(),
|
|
33
|
+
listeners: /* @__PURE__ */ new Set(),
|
|
34
|
+
controller: void 0,
|
|
35
|
+
uiMessagesByParent,
|
|
36
|
+
convert: convertWithUIMessages(uiMessagesByParent),
|
|
37
|
+
subscribe(listener) {
|
|
38
|
+
source.listeners.add(listener);
|
|
39
|
+
return () => source.listeners.delete(listener);
|
|
40
|
+
},
|
|
41
|
+
getSnapshot() {
|
|
42
|
+
return source.snapshot;
|
|
43
|
+
},
|
|
44
|
+
reconcile(controller, subagents, uiMessagesByParent) {
|
|
45
|
+
if (source.uiMessagesByParent !== uiMessagesByParent) {
|
|
46
|
+
source.uiMessagesByParent = uiMessagesByParent;
|
|
47
|
+
source.convert = convertWithUIMessages(uiMessagesByParent);
|
|
48
|
+
}
|
|
49
|
+
if (source.controller !== controller) {
|
|
50
|
+
source.dispose();
|
|
51
|
+
source.controller = controller;
|
|
52
|
+
}
|
|
53
|
+
for (const id of source.requestedNamespaceIds) if (!subagents.has(id)) source.requestedNamespaceIds.delete(id);
|
|
54
|
+
for (const [id, resource] of source.resources) {
|
|
55
|
+
const snapshot = subagents.get(id);
|
|
56
|
+
if (!snapshot || snapshot.depth > 16 || !sameNamespace(resource.namespace, snapshot.namespace)) {
|
|
57
|
+
resource.unsubscribe();
|
|
58
|
+
resource.release();
|
|
59
|
+
source.resources.delete(id);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
resource.snapshot = snapshot;
|
|
63
|
+
}
|
|
64
|
+
for (const snapshot of subagents.values()) {
|
|
65
|
+
if (snapshot.depth > 16) continue;
|
|
66
|
+
if (!source.requestedNamespaceIds.has(snapshot.id)) {
|
|
67
|
+
source.requestedNamespaceIds.add(snapshot.id);
|
|
68
|
+
controller.resolveSubagentNamespace(snapshot.id).catch(() => {
|
|
69
|
+
if (source.controller === controller) source.requestedNamespaceIds.delete(snapshot.id);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
if (source.resources.has(snapshot.id)) continue;
|
|
73
|
+
const acquired = controller.registry.acquire(messagesProjection(snapshot.namespace));
|
|
74
|
+
const resource = {
|
|
75
|
+
snapshot,
|
|
76
|
+
namespace: snapshot.namespace,
|
|
77
|
+
store: acquired.store,
|
|
78
|
+
unsubscribe: () => {},
|
|
79
|
+
release: acquired.release,
|
|
80
|
+
storeSnapshot: void 0,
|
|
81
|
+
status: void 0,
|
|
82
|
+
uiMessages: [],
|
|
83
|
+
converted: void 0,
|
|
84
|
+
childTranscripts: void 0,
|
|
85
|
+
transcript: void 0,
|
|
86
|
+
memo: createAttachMemo(),
|
|
87
|
+
cache: createExternalMessageConversionCache()
|
|
88
|
+
};
|
|
89
|
+
resource.unsubscribe = resource.store.subscribe(() => rebuild());
|
|
90
|
+
source.resources.set(snapshot.id, resource);
|
|
91
|
+
}
|
|
92
|
+
rebuild();
|
|
93
|
+
},
|
|
94
|
+
dispose() {
|
|
95
|
+
for (const resource of source.resources.values()) {
|
|
96
|
+
resource.unsubscribe();
|
|
97
|
+
resource.release();
|
|
98
|
+
}
|
|
99
|
+
source.resources.clear();
|
|
100
|
+
source.requestedNamespaceIds.clear();
|
|
101
|
+
source.snapshot = /* @__PURE__ */ new Map();
|
|
102
|
+
for (const listener of source.listeners) listener();
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
const rebuild = () => {
|
|
106
|
+
const { convert, uiMessagesByParent } = source;
|
|
107
|
+
const resources = [...source.resources.values()];
|
|
108
|
+
const childrenByParent = /* @__PURE__ */ new Map();
|
|
109
|
+
for (const resource of resources) {
|
|
110
|
+
const parentId = resource.snapshot.parentId;
|
|
111
|
+
if (parentId == null) continue;
|
|
112
|
+
const children = childrenByParent.get(parentId);
|
|
113
|
+
if (children) children.push(resource);
|
|
114
|
+
else childrenByParent.set(parentId, [resource]);
|
|
115
|
+
}
|
|
116
|
+
const transcripts = /* @__PURE__ */ new Map();
|
|
117
|
+
let changed = source.snapshot.size !== resources.length;
|
|
118
|
+
const built = /* @__PURE__ */ new Set();
|
|
119
|
+
const build = (resource, depth) => {
|
|
120
|
+
if (built.has(resource.snapshot.id)) return;
|
|
121
|
+
built.add(resource.snapshot.id);
|
|
122
|
+
const children = depth < 16 ? childrenByParent.get(resource.snapshot.id) ?? [] : [];
|
|
123
|
+
for (const child of children) build(child, depth + 1);
|
|
124
|
+
const childTranscripts = new Map(children.flatMap((child) => child.transcript ? [[child.snapshot.id, child.transcript]] : []));
|
|
125
|
+
const storeSnapshot = resource.store.getSnapshot();
|
|
126
|
+
const status = resource.snapshot.status;
|
|
127
|
+
const uiMessages = collectUIMessages(storeSnapshot, uiMessagesByParent);
|
|
128
|
+
const conversionChanged = resource.converted === void 0 || resource.storeSnapshot !== storeSnapshot || resource.status !== status || !sameUIMessages(resource.uiMessages, uiMessages);
|
|
129
|
+
if (conversionChanged) {
|
|
130
|
+
resource.converted = convertExternalMessages(storeSnapshot, convert, status === "running", TRANSCRIPT_METADATA, resource.cache);
|
|
131
|
+
resource.storeSnapshot = storeSnapshot;
|
|
132
|
+
resource.status = status;
|
|
133
|
+
resource.uiMessages = uiMessages;
|
|
134
|
+
}
|
|
135
|
+
if (resource.transcript === void 0 || conversionChanged || !sameTranscriptEntries(resource.childTranscripts, childTranscripts)) {
|
|
136
|
+
const transcript = attachSubagentTranscripts(resource.converted, childTranscripts, resource.memo);
|
|
137
|
+
changed ||= resource.transcript !== transcript;
|
|
138
|
+
resource.transcript = transcript;
|
|
139
|
+
resource.childTranscripts = childTranscripts;
|
|
140
|
+
}
|
|
141
|
+
if (!source.snapshot.has(resource.snapshot.id)) changed = true;
|
|
142
|
+
transcripts.set(resource.snapshot.id, resource.transcript);
|
|
143
|
+
};
|
|
144
|
+
for (const resource of resources) if (resource.snapshot.parentId == null || !source.resources.has(resource.snapshot.parentId)) build(resource, 1);
|
|
145
|
+
for (const resource of resources) build(resource, 16);
|
|
146
|
+
if (!changed) return;
|
|
147
|
+
source.snapshot = transcripts;
|
|
148
|
+
for (const listener of source.listeners) listener();
|
|
149
|
+
};
|
|
150
|
+
return source;
|
|
151
|
+
};
|
|
152
|
+
const useSubagentTranscripts = (stream, uiMessagesByParent) => {
|
|
153
|
+
const sourceRef = useRef(void 0);
|
|
154
|
+
if (!sourceRef.current) sourceRef.current = createSubagentTranscriptSource();
|
|
155
|
+
const source = sourceRef.current;
|
|
156
|
+
const controller = stream[STREAM_CONTROLLER];
|
|
157
|
+
useEffect(() => {
|
|
158
|
+
source.reconcile(controller, stream.subagents, uiMessagesByParent);
|
|
159
|
+
}, [
|
|
160
|
+
controller,
|
|
161
|
+
source,
|
|
162
|
+
stream.subagents,
|
|
163
|
+
uiMessagesByParent
|
|
164
|
+
]);
|
|
165
|
+
useEffect(() => () => source.dispose(), [source]);
|
|
166
|
+
return useSyncExternalStore(source.subscribe, source.getSnapshot, source.getSnapshot);
|
|
167
|
+
};
|
|
168
|
+
//#endregion
|
|
169
|
+
export { MAX_SUBAGENT_DEPTH, useSubagentTranscripts };
|
|
170
|
+
|
|
171
|
+
//# sourceMappingURL=useSubagentTranscripts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useSubagentTranscripts.js","names":[],"sources":["../src/useSubagentTranscripts.ts"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useRef, useSyncExternalStore } from \"react\";\nimport type { ThreadMessage } from \"@assistant-ui/core\";\nimport {\n convertExternalMessages,\n createExternalMessageConversionCache,\n type ExternalMessageConversionCache,\n type useExternalMessageConverter,\n} from \"@assistant-ui/core/react\";\nimport { STREAM_CONTROLLER, type AnyStream } from \"@langchain/react\";\nimport type { BaseMessage } from \"@langchain/core/messages\";\nimport { messagesProjection } from \"@langchain/langgraph-sdk/stream\";\nimport type { SubagentDiscoverySnapshot } from \"@langchain/react\";\nimport {\n attachSubagentTranscripts,\n type AttachMemo,\n createAttachMemo,\n} from \"./attachSubagentTranscripts\";\nimport { convertLangChainBaseMessage } from \"./convertMessages\";\nimport type { LangChainBaseMessage, UIMessage } from \"./types\";\n\nexport const MAX_SUBAGENT_DEPTH = 16;\n\nconst TRANSCRIPT_METADATA = {};\n\ntype ProjectionStore = {\n getSnapshot(): BaseMessage[];\n subscribe(listener: () => void): () => void;\n};\n\ntype ProjectionResource = {\n snapshot: SubagentDiscoverySnapshot;\n namespace: readonly string[];\n store: ProjectionStore;\n unsubscribe: () => void;\n release: () => void;\n storeSnapshot: BaseMessage[] | undefined;\n status: SubagentDiscoverySnapshot[\"status\"] | undefined;\n uiMessages: readonly UIMessage[];\n converted: readonly ThreadMessage[] | undefined;\n childTranscripts: ReadonlyMap<string, readonly ThreadMessage[]> | undefined;\n transcript: readonly ThreadMessage[] | undefined;\n memo: AttachMemo;\n cache: ExternalMessageConversionCache;\n};\n\ntype SubagentTranscriptSource = {\n resources: Map<string, ProjectionResource>;\n requestedNamespaceIds: Set<string>;\n snapshot: ReadonlyMap<string, readonly ThreadMessage[]>;\n listeners: Set<() => void>;\n controller: AnyStream[typeof STREAM_CONTROLLER] | undefined;\n uiMessagesByParent: Map<string, UIMessage[]>;\n convert: useExternalMessageConverter.Callback<LangChainBaseMessage>;\n subscribe(listener: () => void): () => void;\n getSnapshot(): ReadonlyMap<string, readonly ThreadMessage[]>;\n reconcile(\n controller: AnyStream[typeof STREAM_CONTROLLER],\n subagents: AnyStream[\"subagents\"],\n uiMessagesByParent: Map<string, UIMessage[]>,\n ): void;\n dispose(): void;\n};\n\nconst sameNamespace = (a: readonly string[], b: readonly string[]) =>\n a.length === b.length && a.every((segment, index) => segment === b[index]);\n\nconst sameTranscriptEntries = (\n a: ReadonlyMap<string, readonly ThreadMessage[]> | undefined,\n b: ReadonlyMap<string, readonly ThreadMessage[]>,\n) =>\n a?.size === b.size &&\n [...b].every(([id, transcript]) => a.get(id) === transcript);\n\nconst collectUIMessages = (\n messages: readonly BaseMessage[],\n uiMessagesByParent: Map<string, UIMessage[]>,\n) => {\n const collected: UIMessage[] = [];\n if (uiMessagesByParent.size === 0) return collected;\n for (const message of messages) {\n const uiMessages = message.id && uiMessagesByParent.get(message.id);\n if (uiMessages) collected.push(...uiMessages);\n }\n return collected;\n};\n\nconst sameUIMessages = (a: readonly UIMessage[], b: readonly UIMessage[]) =>\n a.length === b.length && a.every((ui, index) => ui === b[index]);\n\nconst convertWithUIMessages =\n (\n uiMessagesByParent: Map<string, UIMessage[]>,\n ): useExternalMessageConverter.Callback<LangChainBaseMessage> =>\n (message, metadata) =>\n convertLangChainBaseMessage(message, { ...metadata, uiMessagesByParent });\n\nconst createSubagentTranscriptSource = (): SubagentTranscriptSource => {\n const uiMessagesByParent = new Map<string, UIMessage[]>();\n const source: SubagentTranscriptSource = {\n resources: new Map(),\n requestedNamespaceIds: new Set(),\n snapshot: new Map(),\n listeners: new Set(),\n controller: undefined,\n uiMessagesByParent,\n convert: convertWithUIMessages(uiMessagesByParent),\n subscribe(listener) {\n source.listeners.add(listener);\n return () => source.listeners.delete(listener);\n },\n getSnapshot() {\n return source.snapshot;\n },\n reconcile(controller, subagents, uiMessagesByParent) {\n if (source.uiMessagesByParent !== uiMessagesByParent) {\n source.uiMessagesByParent = uiMessagesByParent;\n source.convert = convertWithUIMessages(uiMessagesByParent);\n }\n\n if (source.controller !== controller) {\n source.dispose();\n source.controller = controller;\n }\n\n for (const id of source.requestedNamespaceIds) {\n if (!subagents.has(id)) source.requestedNamespaceIds.delete(id);\n }\n\n for (const [id, resource] of source.resources) {\n const snapshot = subagents.get(id);\n if (\n !snapshot ||\n snapshot.depth > MAX_SUBAGENT_DEPTH ||\n !sameNamespace(resource.namespace, snapshot.namespace)\n ) {\n resource.unsubscribe();\n resource.release();\n source.resources.delete(id);\n continue;\n }\n resource.snapshot = snapshot;\n }\n\n for (const snapshot of subagents.values()) {\n if (snapshot.depth > MAX_SUBAGENT_DEPTH) continue;\n if (!source.requestedNamespaceIds.has(snapshot.id)) {\n source.requestedNamespaceIds.add(snapshot.id);\n void controller.resolveSubagentNamespace(snapshot.id).catch(() => {\n if (source.controller === controller)\n source.requestedNamespaceIds.delete(snapshot.id);\n });\n }\n if (source.resources.has(snapshot.id)) continue;\n const acquired = controller.registry.acquire(\n messagesProjection(snapshot.namespace),\n );\n const resource: ProjectionResource = {\n snapshot,\n namespace: snapshot.namespace,\n store: acquired.store,\n unsubscribe: () => {},\n release: acquired.release,\n storeSnapshot: undefined,\n status: undefined,\n uiMessages: [],\n converted: undefined,\n childTranscripts: undefined,\n transcript: undefined,\n memo: createAttachMemo(),\n cache: createExternalMessageConversionCache(),\n };\n resource.unsubscribe = resource.store.subscribe(() => rebuild());\n source.resources.set(snapshot.id, resource);\n }\n\n rebuild();\n },\n dispose() {\n for (const resource of source.resources.values()) {\n resource.unsubscribe();\n resource.release();\n }\n source.resources.clear();\n source.requestedNamespaceIds.clear();\n source.snapshot = new Map();\n for (const listener of source.listeners) listener();\n },\n };\n\n const rebuild = () => {\n const { convert, uiMessagesByParent } = source;\n const resources = [...source.resources.values()];\n const childrenByParent = new Map<string, ProjectionResource[]>();\n\n for (const resource of resources) {\n const parentId = resource.snapshot.parentId;\n if (parentId == null) continue;\n const children = childrenByParent.get(parentId);\n if (children) children.push(resource);\n else childrenByParent.set(parentId, [resource]);\n }\n\n const transcripts = new Map<string, readonly ThreadMessage[]>();\n let changed = source.snapshot.size !== resources.length;\n const built = new Set<string>();\n\n const build = (resource: ProjectionResource, depth: number) => {\n if (built.has(resource.snapshot.id)) return;\n built.add(resource.snapshot.id);\n const children =\n depth < MAX_SUBAGENT_DEPTH\n ? (childrenByParent.get(resource.snapshot.id) ?? [])\n : [];\n for (const child of children) build(child, depth + 1);\n const childTranscripts = new Map(\n children.flatMap((child) =>\n child.transcript\n ? [[child.snapshot.id, child.transcript] as const]\n : [],\n ),\n );\n const storeSnapshot = resource.store.getSnapshot();\n const status = resource.snapshot.status;\n const uiMessages = collectUIMessages(storeSnapshot, uiMessagesByParent);\n\n const conversionChanged =\n resource.converted === undefined ||\n resource.storeSnapshot !== storeSnapshot ||\n resource.status !== status ||\n !sameUIMessages(resource.uiMessages, uiMessages);\n if (conversionChanged) {\n resource.converted = convertExternalMessages(\n storeSnapshot as LangChainBaseMessage[],\n convert,\n status === \"running\",\n TRANSCRIPT_METADATA,\n resource.cache,\n );\n resource.storeSnapshot = storeSnapshot;\n resource.status = status;\n resource.uiMessages = uiMessages;\n }\n\n if (\n resource.transcript === undefined ||\n conversionChanged ||\n !sameTranscriptEntries(resource.childTranscripts, childTranscripts)\n ) {\n const transcript = attachSubagentTranscripts(\n resource.converted!,\n childTranscripts,\n resource.memo,\n );\n changed ||= resource.transcript !== transcript;\n resource.transcript = transcript;\n resource.childTranscripts = childTranscripts;\n }\n\n if (!source.snapshot.has(resource.snapshot.id)) changed = true;\n transcripts.set(resource.snapshot.id, resource.transcript);\n };\n\n for (const resource of resources) {\n if (\n resource.snapshot.parentId == null ||\n !source.resources.has(resource.snapshot.parentId)\n )\n build(resource, 1);\n }\n for (const resource of resources) build(resource, MAX_SUBAGENT_DEPTH);\n\n if (!changed) return;\n source.snapshot = transcripts;\n for (const listener of source.listeners) listener();\n };\n\n return source;\n};\n\nexport const useSubagentTranscripts = (\n stream: AnyStream,\n uiMessagesByParent: Map<string, UIMessage[]>,\n): ReadonlyMap<string, readonly ThreadMessage[]> => {\n const sourceRef = useRef<SubagentTranscriptSource | undefined>(undefined);\n if (!sourceRef.current) {\n sourceRef.current = createSubagentTranscriptSource();\n }\n const source = sourceRef.current;\n const controller = stream[STREAM_CONTROLLER];\n\n useEffect(() => {\n source.reconcile(controller, stream.subagents, uiMessagesByParent);\n }, [controller, source, stream.subagents, uiMessagesByParent]);\n\n useEffect(() => () => source.dispose(), [source]);\n\n return useSyncExternalStore(\n source.subscribe,\n source.getSnapshot,\n source.getSnapshot,\n );\n};\n"],"mappings":"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"}
|
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.
|
|
44
|
-
"@assistant-ui/store": "^0.3.
|
|
43
|
+
"@assistant-ui/core": "^0.3.19",
|
|
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,15 +63,15 @@
|
|
|
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
|
-
"@langchain/langgraph-sdk": "^1.10.
|
|
65
|
-
"@langchain/react": "^1.0.
|
|
68
|
+
"@langchain/langgraph-sdk": "^1.10.2",
|
|
69
|
+
"@langchain/react": "^1.0.35",
|
|
66
70
|
"@testing-library/react": "^16.3.3",
|
|
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
|
+
};
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
getMessageContent,
|
|
7
7
|
getMessageType,
|
|
8
8
|
} from "./convertMessages";
|
|
9
|
+
import { createLangChainStreamingTimingAccessors } from "./converter";
|
|
9
10
|
import type { LangChainBaseMessage, UIMessage } from "./types";
|
|
10
11
|
|
|
11
12
|
const humanMessage = (content: unknown): LangChainBaseMessage => ({
|
|
@@ -755,13 +756,38 @@ describe("convertLangChainBaseMessage reasoning content parts", () => {
|
|
|
755
756
|
]);
|
|
756
757
|
});
|
|
757
758
|
|
|
758
|
-
it("
|
|
759
|
+
it("omits reasoning parts that contain only provider metadata", () => {
|
|
759
760
|
const result = convertLangChainBaseMessage(
|
|
760
|
-
aiMessage([
|
|
761
|
+
aiMessage([
|
|
762
|
+
{ type: "reasoning", signature: "signed", index: 0 },
|
|
763
|
+
{ type: "reasoning", summary: [{ type: "summary_text" }], index: 1 },
|
|
764
|
+
{ type: "text", text: "Answer." },
|
|
765
|
+
]),
|
|
761
766
|
{},
|
|
762
767
|
);
|
|
763
768
|
|
|
764
|
-
expect(contentOf(result)).toEqual([{ type: "
|
|
769
|
+
expect(contentOf(result)).toEqual([{ type: "text", text: "Answer." }]);
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
it("renders the summary, falling back to the reasoning string when it is blank", () => {
|
|
773
|
+
const convert = (summary: string) =>
|
|
774
|
+
contentOf(
|
|
775
|
+
convertLangChainBaseMessage(
|
|
776
|
+
aiMessage([
|
|
777
|
+
{
|
|
778
|
+
type: "reasoning",
|
|
779
|
+
reasoning: "Check net",
|
|
780
|
+
summary: [{ type: "summary_text", text: summary }],
|
|
781
|
+
},
|
|
782
|
+
]),
|
|
783
|
+
{},
|
|
784
|
+
),
|
|
785
|
+
);
|
|
786
|
+
|
|
787
|
+
expect(convert("temperature")).toEqual([
|
|
788
|
+
{ type: "reasoning", text: "temperature" },
|
|
789
|
+
]);
|
|
790
|
+
expect(convert(" ")).toEqual([{ type: "reasoning", text: "Check net" }]);
|
|
765
791
|
});
|
|
766
792
|
|
|
767
793
|
it("tolerates null entries inside the summary array", () => {
|
|
@@ -1114,6 +1140,49 @@ describe("convertLangChainBaseMessage malformed messages", () => {
|
|
|
1114
1140
|
expect(contentOf(result)).toEqual([{ type: "text", text: "kept" }]);
|
|
1115
1141
|
});
|
|
1116
1142
|
|
|
1143
|
+
it("coalesces a text block without a text field to empty text", () => {
|
|
1144
|
+
const result = convertLangChainBaseMessage(
|
|
1145
|
+
humanMessage([{ type: "text" }]),
|
|
1146
|
+
{},
|
|
1147
|
+
);
|
|
1148
|
+
|
|
1149
|
+
expect(contentOf(result)).toEqual([{ type: "text", text: "" }]);
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1152
|
+
it("skips null entries when measuring streamed text length", () => {
|
|
1153
|
+
const { getTextLength } = createLangChainStreamingTimingAccessors<{
|
|
1154
|
+
id?: string | undefined;
|
|
1155
|
+
content?: unknown;
|
|
1156
|
+
_getType: () => string;
|
|
1157
|
+
}>((message) => message._getType());
|
|
1158
|
+
|
|
1159
|
+
expect(
|
|
1160
|
+
getTextLength(
|
|
1161
|
+
[
|
|
1162
|
+
{
|
|
1163
|
+
_getType: () => "ai",
|
|
1164
|
+
id: "ai-1",
|
|
1165
|
+
content: [null, { type: "text", text: "abc" }, undefined],
|
|
1166
|
+
},
|
|
1167
|
+
],
|
|
1168
|
+
"ai-1",
|
|
1169
|
+
),
|
|
1170
|
+
).toBe(3);
|
|
1171
|
+
});
|
|
1172
|
+
|
|
1173
|
+
it("renders a textless block as empty system text", () => {
|
|
1174
|
+
const result = convertLangChainBaseMessage(
|
|
1175
|
+
{
|
|
1176
|
+
_getType: () => "system",
|
|
1177
|
+
id: "msg-7",
|
|
1178
|
+
content: [{ type: "text" }, { type: "text", text: "kept" }],
|
|
1179
|
+
},
|
|
1180
|
+
{},
|
|
1181
|
+
);
|
|
1182
|
+
|
|
1183
|
+
expect(contentOf(result)).toEqual([{ type: "text", text: "kept" }]);
|
|
1184
|
+
});
|
|
1185
|
+
|
|
1117
1186
|
it("skips null entries when collecting system text", () => {
|
|
1118
1187
|
const result = convertLangChainBaseMessage(
|
|
1119
1188
|
{
|