@assistant-ui/react-langchain 0.0.30 → 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.
Files changed (47) hide show
  1. package/README.md +1 -1
  2. package/dist/attachSubagentTranscripts.d.ts +24 -0
  3. package/dist/attachSubagentTranscripts.d.ts.map +1 -0
  4. package/dist/attachSubagentTranscripts.js +43 -0
  5. package/dist/attachSubagentTranscripts.js.map +1 -0
  6. package/dist/convertMessages.d.ts +4 -4
  7. package/dist/convertMessages.d.ts.map +1 -1
  8. package/dist/converter.d.ts +10 -11
  9. package/dist/converter.d.ts.map +1 -1
  10. package/dist/findForkCheckpointInHistory.d.ts +2 -3
  11. package/dist/findForkCheckpointInHistory.d.ts.map +1 -1
  12. package/dist/hooks.d.ts +14 -15
  13. package/dist/hooks.d.ts.map +1 -1
  14. package/dist/resolveForkCheckpoint.d.ts +1 -2
  15. package/dist/resolveForkCheckpoint.d.ts.map +1 -1
  16. package/dist/runtimeExtras.d.ts +1 -2
  17. package/dist/runtimeExtras.d.ts.map +1 -1
  18. package/dist/sdkIdentity.d.ts +5 -0
  19. package/dist/sdkIdentity.d.ts.map +1 -0
  20. package/dist/sdkIdentity.js +9 -0
  21. package/dist/sdkIdentity.js.map +1 -0
  22. package/dist/streamingTiming.d.ts +2 -3
  23. package/dist/streamingTiming.d.ts.map +1 -1
  24. package/dist/types.d.ts +8 -8
  25. package/dist/types.d.ts.map +1 -1
  26. package/dist/uiMessages.d.ts +22 -7
  27. package/dist/uiMessages.d.ts.map +1 -1
  28. package/dist/uiMessages.js +23 -5
  29. package/dist/uiMessages.js.map +1 -1
  30. package/dist/useStreamRuntime.d.ts +3 -3
  31. package/dist/useStreamRuntime.d.ts.map +1 -1
  32. package/dist/useStreamRuntime.js +26 -17
  33. package/dist/useStreamRuntime.js.map +1 -1
  34. package/dist/useSubagentTranscripts.d.ts +8 -0
  35. package/dist/useSubagentTranscripts.d.ts.map +1 -0
  36. package/dist/useSubagentTranscripts.js +265 -0
  37. package/dist/useSubagentTranscripts.js.map +1 -0
  38. package/package.json +17 -12
  39. package/src/attachSubagentTranscripts.test.ts +198 -0
  40. package/src/attachSubagentTranscripts.ts +91 -0
  41. package/src/sdkIdentity.ts +9 -0
  42. package/src/uiMessages.test.ts +43 -0
  43. package/src/uiMessages.ts +36 -4
  44. package/src/useStreamRuntime.test.tsx +218 -7
  45. package/src/useStreamRuntime.ts +40 -16
  46. package/src/useSubagentTranscripts.test.tsx +1258 -0
  47. package/src/useSubagentTranscripts.ts +526 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assistant-ui/react-langchain",
3
- "version": "0.0.30",
3
+ "version": "0.0.32",
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.18",
44
- "@assistant-ui/store": "^0.3.13",
43
+ "@assistant-ui/core": "^0.3.20",
44
+ "@assistant-ui/store": "^0.3.14",
45
45
  "assistant-cloud": "*",
46
- "assistant-stream": "^0.3.42"
46
+ "assistant-stream": "^0.3.44"
47
47
  },
48
48
  "peerDependencies": {
49
- "@langchain/react": "^1.0.2",
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.26",
63
- "@langchain/core": "^1.2.9",
64
- "@langchain/langgraph-sdk": "^1.10.2",
65
- "@langchain/react": "^1.0.35",
66
+ "@assistant-ui/x-buildutils": "0.0.28",
67
+ "@langchain/core": "^1.2.11",
68
+ "@langchain/langgraph-sdk": "^1.11.0",
69
+ "@langchain/react": "^1.1.0",
66
70
  "@testing-library/react": "^16.3.3",
67
- "@types/react": "^19.2.18",
71
+ "@types/react": "^19.3.0",
68
72
  "jsdom": "^30.0.1",
69
- "react": "^19.2.8",
70
- "vitest": "^4.1.11"
73
+ "react": "^19.3.0",
74
+ "vitest": "^5.0.1"
71
75
  },
72
76
  "publishConfig": {
73
77
  "access": "public",
@@ -85,6 +89,7 @@
85
89
  "scripts": {
86
90
  "build": "aui-build",
87
91
  "test": "vitest run",
92
+ "typecheck": "tsc --noEmit",
88
93
  "test:watch": "vitest"
89
94
  }
90
95
  }
@@ -0,0 +1,198 @@
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
+ type SubagentTranscript,
11
+ } from "./attachSubagentTranscripts";
12
+
13
+ const assistantMessage = (id: string, toolCallId?: string): ThreadMessage =>
14
+ ({
15
+ id,
16
+ createdAt: new Date(),
17
+ role: "assistant",
18
+ status: { type: "complete", reason: "unknown" },
19
+ content: toolCallId
20
+ ? [
21
+ {
22
+ type: "tool-call",
23
+ toolCallId,
24
+ toolName: "task",
25
+ args: {},
26
+ argsText: "{}",
27
+ },
28
+ ]
29
+ : [{ type: "text", text: id }],
30
+ metadata: {
31
+ unstable_state: null,
32
+ unstable_annotations: [],
33
+ unstable_data: [],
34
+ steps: [],
35
+ custom: {},
36
+ },
37
+ }) as ThreadMessage;
38
+
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 });
46
+
47
+ describe("attachSubagentTranscripts", () => {
48
+ it("returns the input array when no tool call has a transcript", () => {
49
+ const messages = [assistantMessage("message", "unknown")];
50
+
51
+ expect(
52
+ attachSubagentTranscripts(
53
+ messages,
54
+ new Map([["task", transcript("transcript")]]),
55
+ createAttachMemo(),
56
+ ),
57
+ ).toBe(messages);
58
+ });
59
+
60
+ it("attaches a transcript and preserves untouched message identities", () => {
61
+ const matching = assistantMessage("matching", "task");
62
+ const untouched = assistantMessage("untouched");
63
+ const messages = [matching, untouched];
64
+ const childTranscript = transcript("child");
65
+
66
+ const attached = attachSubagentTranscripts(
67
+ messages,
68
+ new Map([["task", childTranscript]]),
69
+ createAttachMemo(),
70
+ );
71
+
72
+ expect(attached).not.toBe(messages);
73
+ expect(attached[0]).not.toBe(matching);
74
+ expect(attached[1]).toBe(untouched);
75
+ const part = attached[0]!.content[0]!;
76
+ expect(part.type).toBe("tool-call");
77
+ if (part.type !== "tool-call") throw new Error("expected a tool call");
78
+ expect(part.messages).toBe(childTranscript.messages);
79
+ expect(part.timing).toBe(childTranscript.timing);
80
+ });
81
+
82
+ it("reuses the attached array when transcript identities are unchanged", () => {
83
+ const messages = [assistantMessage("matching", "task")];
84
+ const childTranscript = transcript("child");
85
+ const memo = createAttachMemo();
86
+
87
+ const first = attachSubagentTranscripts(
88
+ messages,
89
+ new Map([["task", childTranscript]]),
90
+ memo,
91
+ );
92
+ const second = attachSubagentTranscripts(
93
+ messages,
94
+ new Map([["task", childTranscript]]),
95
+ memo,
96
+ );
97
+
98
+ expect(second).toBe(first);
99
+ });
100
+
101
+ it("rebuilds only messages whose transcript changed", () => {
102
+ const messages = [
103
+ assistantMessage("first", "task-one"),
104
+ assistantMessage("second", "task-two"),
105
+ ];
106
+ const firstTranscript = transcript("first-transcript");
107
+ const secondTranscript = transcript("second-transcript");
108
+ const memo = createAttachMemo();
109
+ const first = attachSubagentTranscripts(
110
+ messages,
111
+ new Map([
112
+ ["task-one", firstTranscript],
113
+ ["task-two", secondTranscript],
114
+ ]),
115
+ memo,
116
+ );
117
+ const next = attachSubagentTranscripts(
118
+ messages,
119
+ new Map([
120
+ ["task-one", transcript("updated-transcript")],
121
+ ["task-two", secondTranscript],
122
+ ]),
123
+ memo,
124
+ );
125
+
126
+ expect(next).not.toBe(first);
127
+ expect(next[0]).not.toBe(first[0]);
128
+ expect(next[1]).toBe(first[1]);
129
+ });
130
+
131
+ it("rebuilds when the same transcript moves to another tool call", () => {
132
+ const first = assistantMessage("f", "call-a").content[0]!;
133
+ const second = assistantMessage("s", "call-b").content[0]!;
134
+ const message = {
135
+ ...assistantMessage("pair"),
136
+ content: [first, second],
137
+ } as ThreadMessage;
138
+ const nested = transcript("nested");
139
+ const memo = createAttachMemo();
140
+
141
+ const [before] = attachSubagentTranscripts(
142
+ [message],
143
+ new Map([["call-a", nested]]),
144
+ memo,
145
+ );
146
+ const [after] = attachSubagentTranscripts(
147
+ [message],
148
+ new Map([["call-b", nested]]),
149
+ memo,
150
+ );
151
+
152
+ expect(before?.content[0]).toMatchObject({ messages: nested.messages });
153
+ expect(after).not.toBe(before);
154
+ expect(after?.content[0]).toBe(first);
155
+ expect(after?.content[1]).toMatchObject({ messages: nested.messages });
156
+ });
157
+
158
+ it("keeps untouched parts of a partially matched message by reference", () => {
159
+ const text = { type: "text" as const, text: "hello" };
160
+ const matched = assistantMessage("m", "call-matched").content[0]!;
161
+ const unmatched = assistantMessage("u", "call-unmatched").content[0]!;
162
+ const message = {
163
+ ...assistantMessage("mixed"),
164
+ content: [text, matched, unmatched],
165
+ } as ThreadMessage;
166
+ const nested = transcript("nested");
167
+
168
+ const [attached] = attachSubagentTranscripts(
169
+ [message],
170
+ new Map([["call-matched", nested]]),
171
+ createAttachMemo(),
172
+ );
173
+
174
+ expect(attached?.content[0]).toBe(text);
175
+ expect(attached?.content[1]).toMatchObject({ messages: nested.messages });
176
+ expect(attached?.content[2]).toBe(unmatched);
177
+ });
178
+
179
+ it("preserves message and array external-store bindings", () => {
180
+ const message = assistantMessage("matching", "task");
181
+ const messages = [message];
182
+ const originalMessage = { id: "original-message" };
183
+ const originalMessages = [{ id: "original-messages" }];
184
+ bindExternalStoreMessage(message, originalMessage);
185
+ bindExternalStoreMessage(messages, originalMessages);
186
+
187
+ const attached = attachSubagentTranscripts(
188
+ messages,
189
+ new Map([["task", transcript("child")]]),
190
+ createAttachMemo(),
191
+ );
192
+
193
+ expect(getExternalStoreMessages(attached[0]!)).toEqual([originalMessage]);
194
+ expect(getExternalStoreMessages({ messages: attached })).toBe(
195
+ originalMessages,
196
+ );
197
+ });
198
+ });
@@ -0,0 +1,91 @@
1
+ import {
2
+ bindExternalStoreMessage,
3
+ getExternalStoreMessages,
4
+ type ThreadMessage,
5
+ type ToolCallTiming,
6
+ } from "@assistant-ui/core";
7
+
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];
19
+
20
+ type AttachedMessage = {
21
+ transcripts: readonly AttachedTranscript[];
22
+ attached: ThreadMessage;
23
+ };
24
+
25
+ export type AttachMemo = {
26
+ messages: WeakMap<ThreadMessage, AttachedMessage>;
27
+ results: WeakMap<readonly ThreadMessage[], readonly ThreadMessage[]>;
28
+ };
29
+
30
+ export const createAttachMemo = (): AttachMemo => ({
31
+ messages: new WeakMap(),
32
+ results: new WeakMap(),
33
+ });
34
+
35
+ const sameTranscripts = (
36
+ a: readonly AttachedTranscript[],
37
+ b: readonly AttachedTranscript[],
38
+ ) =>
39
+ a.length === b.length &&
40
+ a.every(
41
+ ([toolCallId, transcript], index) =>
42
+ toolCallId === b[index]![0] && transcript === b[index]![1],
43
+ );
44
+
45
+ export const attachSubagentTranscripts = (
46
+ messages: readonly ThreadMessage[],
47
+ transcripts: ReadonlyMap<string, SubagentTranscript>,
48
+ memo: AttachMemo,
49
+ ): readonly ThreadMessage[] => {
50
+ let attachedAny = false;
51
+ const next = messages.map((message) => {
52
+ const attachedTranscripts = message.content.flatMap((part) =>
53
+ part.type === "tool-call" && transcripts.has(part.toolCallId)
54
+ ? [[part.toolCallId, transcripts.get(part.toolCallId)!] as const]
55
+ : [],
56
+ );
57
+ if (attachedTranscripts.length === 0) return message;
58
+ attachedAny = true;
59
+
60
+ const cached = memo.messages.get(message);
61
+ if (cached && sameTranscripts(cached.transcripts, attachedTranscripts))
62
+ return cached.attached;
63
+
64
+ const attached = {
65
+ ...message,
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
+ }),
79
+ } as ThreadMessage;
80
+ memo.messages.set(message, { transcripts: attachedTranscripts, attached });
81
+ return attached;
82
+ });
83
+
84
+ if (!attachedAny) return messages;
85
+ const previous = memo.results.get(messages);
86
+ if (previous && previous.every((message, index) => message === next[index]))
87
+ return previous;
88
+ bindExternalStoreMessage(next, getExternalStoreMessages({ messages }));
89
+ memo.results.set(messages, next);
90
+ return next;
91
+ };
@@ -0,0 +1,9 @@
1
+ import type { SdkIdentity } from "assistant-cloud";
2
+
3
+ export const LANGCHAIN_SDK: SdkIdentity = {
4
+ name: "@assistant-ui/react-langchain",
5
+ version:
6
+ typeof __AUI_PACKAGE_VERSION__ === "string"
7
+ ? __AUI_PACKAGE_VERSION__
8
+ : "0.0.0",
9
+ };
@@ -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 const foldUIUpdates = (events: readonly unknown[]): UIMessage[] => {
66
- let acc: UIMessage[] = [];
67
- for (const event of events) {
68
- const update = extractUIUpdate(event);
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