@assistant-ui/react-langchain 0.0.31 → 0.0.33

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 (60) hide show
  1. package/dist/attachSubagentTranscripts.d.ts +19 -12
  2. package/dist/attachSubagentTranscripts.d.ts.map +1 -1
  3. package/dist/attachSubagentTranscripts.js +8 -4
  4. package/dist/attachSubagentTranscripts.js.map +1 -1
  5. package/dist/convertMessages.d.ts +8 -11
  6. package/dist/convertMessages.d.ts.map +1 -1
  7. package/dist/convertMessages.js +53 -15
  8. package/dist/convertMessages.js.map +1 -1
  9. package/dist/converter.d.ts +137 -138
  10. package/dist/converter.d.ts.map +1 -1
  11. package/dist/converter.js +2 -1
  12. package/dist/converter.js.map +1 -1
  13. package/dist/findForkCheckpointInHistory.d.ts +17 -19
  14. package/dist/findForkCheckpointInHistory.d.ts.map +1 -1
  15. package/dist/hooks.d.ts +19 -22
  16. package/dist/hooks.d.ts.map +1 -1
  17. package/dist/index.d.ts +7 -8
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/resolveForkCheckpoint.d.ts +5 -7
  20. package/dist/resolveForkCheckpoint.d.ts.map +1 -1
  21. package/dist/runtimeExtras.d.ts +2 -5
  22. package/dist/runtimeExtras.d.ts.map +1 -1
  23. package/dist/sdkIdentity.d.ts +2 -5
  24. package/dist/sdkIdentity.d.ts.map +1 -1
  25. package/dist/sdkIdentity.js +1 -1
  26. package/dist/streamingTiming.d.ts +4 -7
  27. package/dist/streamingTiming.d.ts.map +1 -1
  28. package/dist/subagentMessagesProjection.d.ts +17 -0
  29. package/dist/subagentMessagesProjection.d.ts.map +1 -0
  30. package/dist/subagentMessagesProjection.js +40 -0
  31. package/dist/subagentMessagesProjection.js.map +1 -0
  32. package/dist/types.d.ts +106 -109
  33. package/dist/types.d.ts.map +1 -1
  34. package/dist/uiMessages.d.ts +37 -10
  35. package/dist/uiMessages.d.ts.map +1 -1
  36. package/dist/uiMessages.js +55 -5
  37. package/dist/uiMessages.js.map +1 -1
  38. package/dist/useStreamRuntime.d.ts +9 -12
  39. package/dist/useStreamRuntime.d.ts.map +1 -1
  40. package/dist/useStreamRuntime.js +102 -28
  41. package/dist/useStreamRuntime.js.map +1 -1
  42. package/dist/useSubagentTranscripts.d.ts +5 -8
  43. package/dist/useSubagentTranscripts.d.ts.map +1 -1
  44. package/dist/useSubagentTranscripts.js +129 -34
  45. package/dist/useSubagentTranscripts.js.map +1 -1
  46. package/package.json +16 -11
  47. package/src/attachSubagentTranscripts.test.ts +13 -5
  48. package/src/attachSubagentTranscripts.ts +26 -7
  49. package/src/convertMessages.test.ts +69 -0
  50. package/src/convertMessages.ts +46 -8
  51. package/src/converter.ts +6 -0
  52. package/src/subagentMessagesProjection.test.ts +175 -0
  53. package/src/subagentMessagesProjection.ts +50 -0
  54. package/src/uiMessages.test.ts +149 -0
  55. package/src/uiMessages.ts +79 -4
  56. package/src/useStreamRuntime.test.tsx +212 -0
  57. package/src/useStreamRuntime.ts +202 -50
  58. package/src/useStreamRuntime.voice.test.tsx +712 -0
  59. package/src/useSubagentTranscripts.test.tsx +795 -52
  60. package/src/useSubagentTranscripts.ts +269 -50
@@ -1,7 +1,13 @@
1
1
  "use client";
2
2
 
3
3
  import { useEffect, useRef, useSyncExternalStore } from "react";
4
- import type { ThreadMessage } from "@assistant-ui/core";
4
+ import {
5
+ stepStreamingTiming,
6
+ type MessageTiming,
7
+ type StreamingTimingState,
8
+ type ThreadMessage,
9
+ type ToolCallTiming,
10
+ } from "@assistant-ui/core";
5
11
  import {
6
12
  convertExternalMessages,
7
13
  createExternalMessageConversionCache,
@@ -10,51 +16,91 @@ import {
10
16
  } from "@assistant-ui/core/react";
11
17
  import { STREAM_CONTROLLER, type AnyStream } from "@langchain/react";
12
18
  import type { BaseMessage } from "@langchain/core/messages";
13
- import { messagesProjection } from "@langchain/langgraph-sdk/stream";
19
+ import { channelProjection, type Event } from "@langchain/langgraph-sdk/stream";
14
20
  import type { SubagentDiscoverySnapshot } from "@langchain/react";
15
21
  import {
16
22
  attachSubagentTranscripts,
17
23
  type AttachMemo,
18
24
  createAttachMemo,
25
+ type SubagentTranscript,
19
26
  } from "./attachSubagentTranscripts";
20
27
  import { convertLangChainBaseMessage } from "./convertMessages";
28
+ import { groupUIMessagesByParent } from "./converter";
29
+ import { langChainStreamingTimingAccessors } from "./streamingTiming";
30
+ import { subagentMessagesProjection } from "./subagentMessagesProjection";
21
31
  import type { LangChainBaseMessage, UIMessage } from "./types";
32
+ import {
33
+ createUIFoldMemo,
34
+ foldUIUpdates,
35
+ mergeUIMessages,
36
+ UI_CUSTOM_CHANNELS,
37
+ type UIFoldMemo,
38
+ } from "./uiMessages";
22
39
 
23
40
  export const MAX_SUBAGENT_DEPTH = 16;
24
41
 
42
+ const ROOT_UI_CHANNEL_DEPTH = 1;
43
+
25
44
  const TRANSCRIPT_METADATA = {};
26
45
 
27
- type ProjectionStore = {
28
- getSnapshot(): BaseMessage[];
46
+ const NO_UI_MESSAGES: readonly UIMessage[] = [];
47
+
48
+ const NO_MESSAGE_TIMING: Record<string, MessageTiming> = {};
49
+
50
+ type ProjectionStore<T> = {
51
+ getSnapshot(): T;
29
52
  subscribe(listener: () => void): () => void;
30
53
  };
31
54
 
32
55
  type ProjectionResource = {
33
56
  snapshot: SubagentDiscoverySnapshot;
34
57
  namespace: readonly string[];
35
- store: ProjectionStore;
36
- unsubscribe: () => void;
37
- release: () => void;
58
+ store: ProjectionStore<BaseMessage[]>;
59
+ uiStore: ProjectionStore<readonly Event[]> | undefined;
60
+ dispose: () => void;
38
61
  storeSnapshot: BaseMessage[] | undefined;
39
62
  status: SubagentDiscoverySnapshot["status"] | undefined;
63
+ uiFoldMemo: UIFoldMemo;
64
+ localUiMessages: readonly UIMessage[];
65
+ rootUiMessagesByParent: Map<string, UIMessage[]>;
66
+ uiMessagesByParent: Map<string, UIMessage[]>;
67
+ timingState: StreamingTimingState | null;
68
+ messageTiming: Record<string, MessageTiming>;
69
+ convertedMessageTiming: Record<string, MessageTiming> | undefined;
70
+ convert: useExternalMessageConverter.Callback<LangChainBaseMessage>;
40
71
  uiMessages: readonly UIMessage[];
41
72
  converted: readonly ThreadMessage[] | undefined;
42
- childTranscripts: ReadonlyMap<string, readonly ThreadMessage[]> | undefined;
73
+ childTranscripts: ReadonlyMap<string, SubagentTranscript> | undefined;
43
74
  transcript: readonly ThreadMessage[] | undefined;
75
+ timing: ToolCallTiming | undefined;
76
+ entry: SubagentTranscript | undefined;
44
77
  memo: AttachMemo;
45
78
  cache: ExternalMessageConversionCache;
46
79
  };
47
80
 
81
+ type MeasuredTiming = {
82
+ timing: ToolCallTiming | undefined;
83
+ timingState: StreamingTimingState | null;
84
+ messageTiming: Record<string, MessageTiming>;
85
+ };
86
+
87
+ type NamespaceRequest = {
88
+ id: string;
89
+ attempts: number;
90
+ pending: boolean;
91
+ retryQueued: boolean;
92
+ status: SubagentDiscoverySnapshot["status"];
93
+ };
94
+
48
95
  type SubagentTranscriptSource = {
49
96
  resources: Map<string, ProjectionResource>;
50
- requestedNamespaceIds: Set<string>;
51
- snapshot: ReadonlyMap<string, readonly ThreadMessage[]>;
97
+ namespaceRequests: Map<string, NamespaceRequest>;
98
+ snapshot: ReadonlyMap<string, SubagentTranscript>;
52
99
  listeners: Set<() => void>;
53
100
  controller: AnyStream[typeof STREAM_CONTROLLER] | undefined;
54
101
  uiMessagesByParent: Map<string, UIMessage[]>;
55
- convert: useExternalMessageConverter.Callback<LangChainBaseMessage>;
56
102
  subscribe(listener: () => void): () => void;
57
- getSnapshot(): ReadonlyMap<string, readonly ThreadMessage[]>;
103
+ getSnapshot(): ReadonlyMap<string, SubagentTranscript>;
58
104
  reconcile(
59
105
  controller: AnyStream[typeof STREAM_CONTROLLER],
60
106
  subagents: AnyStream["subagents"],
@@ -66,9 +112,47 @@ type SubagentTranscriptSource = {
66
112
  const sameNamespace = (a: readonly string[], b: readonly string[]) =>
67
113
  a.length === b.length && a.every((segment, index) => segment === b[index]);
68
114
 
115
+ const needsNamespaceResolution = (snapshot: SubagentDiscoverySnapshot) =>
116
+ snapshot.namespace.length === 1 &&
117
+ snapshot.namespace[0] === `tools:${snapshot.id}`;
118
+
119
+ const requestSubagentNamespace = (
120
+ source: SubagentTranscriptSource,
121
+ controller: AnyStream[typeof STREAM_CONTROLLER],
122
+ request: NamespaceRequest,
123
+ ) => {
124
+ request.attempts += 1;
125
+ request.pending = true;
126
+ request.retryQueued = false;
127
+ void controller
128
+ .resolveSubagentNamespace(request.id)
129
+ .catch(() => {})
130
+ .finally(() => {
131
+ if (
132
+ source.controller !== controller ||
133
+ source.namespaceRequests.get(request.id) !== request
134
+ )
135
+ return;
136
+
137
+ request.pending = false;
138
+ if (
139
+ !request.retryQueued &&
140
+ request.attempts === 1 &&
141
+ request.status !== "running"
142
+ ) {
143
+ request.retryQueued = true;
144
+ }
145
+ if (!request.retryQueued || request.attempts >= 2) {
146
+ request.retryQueued = false;
147
+ return;
148
+ }
149
+ requestSubagentNamespace(source, controller, request);
150
+ });
151
+ };
152
+
69
153
  const sameTranscriptEntries = (
70
- a: ReadonlyMap<string, readonly ThreadMessage[]> | undefined,
71
- b: ReadonlyMap<string, readonly ThreadMessage[]>,
154
+ a: ReadonlyMap<string, SubagentTranscript> | undefined,
155
+ b: ReadonlyMap<string, SubagentTranscript>,
72
156
  ) =>
73
157
  a?.size === b.size &&
74
158
  [...b].every(([id, transcript]) => a.get(id) === transcript);
@@ -89,23 +173,61 @@ const collectUIMessages = (
89
173
  const sameUIMessages = (a: readonly UIMessage[], b: readonly UIMessage[]) =>
90
174
  a.length === b.length && a.every((ui, index) => ui === b[index]);
91
175
 
92
- const convertWithUIMessages =
176
+ const foldLocalUIMessages = (resource: ProjectionResource) => {
177
+ const events = resource.uiStore?.getSnapshot();
178
+ if (events === undefined) return NO_UI_MESSAGES;
179
+ return foldUIUpdates(events, resource.uiFoldMemo);
180
+ };
181
+
182
+ const mergeLocalUIMessages = (
183
+ uiMessagesByParent: Map<string, UIMessage[]>,
184
+ localUiMessages: readonly UIMessage[],
185
+ ) => {
186
+ const merged = new Map(uiMessagesByParent);
187
+ for (const [parentId, messages] of groupUIMessagesByParent<UIMessage>(
188
+ localUiMessages,
189
+ )) {
190
+ merged.set(parentId, mergeUIMessages(messages, merged.get(parentId)));
191
+ }
192
+ return merged;
193
+ };
194
+
195
+ const createConverter =
93
196
  (
94
197
  uiMessagesByParent: Map<string, UIMessage[]>,
198
+ messageTiming: Record<string, MessageTiming>,
95
199
  ): useExternalMessageConverter.Callback<LangChainBaseMessage> =>
96
200
  (message, metadata) =>
97
- convertLangChainBaseMessage(message, { ...metadata, uiMessagesByParent });
201
+ convertLangChainBaseMessage(message, {
202
+ ...metadata,
203
+ uiMessagesByParent,
204
+ messageTiming,
205
+ });
206
+
207
+ const updateTiming = (resource: ProjectionResource) => {
208
+ if (resource.timing === undefined) return;
209
+ const startedAt = resource.snapshot.startedAt.getTime();
210
+ const completedAt = resource.snapshot.completedAt?.getTime();
211
+ if (
212
+ resource.timing.startedAt === startedAt &&
213
+ resource.timing.completedAt === completedAt
214
+ )
215
+ return;
216
+ resource.timing = {
217
+ startedAt,
218
+ ...(completedAt !== undefined && { completedAt }),
219
+ };
220
+ };
98
221
 
99
222
  const createSubagentTranscriptSource = (): SubagentTranscriptSource => {
100
223
  const uiMessagesByParent = new Map<string, UIMessage[]>();
101
224
  const source: SubagentTranscriptSource = {
102
225
  resources: new Map(),
103
- requestedNamespaceIds: new Set(),
226
+ namespaceRequests: new Map(),
104
227
  snapshot: new Map(),
105
228
  listeners: new Set(),
106
229
  controller: undefined,
107
230
  uiMessagesByParent,
108
- convert: convertWithUIMessages(uiMessagesByParent),
109
231
  subscribe(listener) {
110
232
  source.listeners.add(listener);
111
233
  return () => source.listeners.delete(listener);
@@ -114,18 +236,19 @@ const createSubagentTranscriptSource = (): SubagentTranscriptSource => {
114
236
  return source.snapshot;
115
237
  },
116
238
  reconcile(controller, subagents, uiMessagesByParent) {
117
- if (source.uiMessagesByParent !== uiMessagesByParent) {
118
- source.uiMessagesByParent = uiMessagesByParent;
119
- source.convert = convertWithUIMessages(uiMessagesByParent);
120
- }
239
+ source.uiMessagesByParent = uiMessagesByParent;
240
+ // A resource is rebuilt from scratch when its namespace resolves, which
241
+ // routinely happens after the subagent finished. Its timing was measured
242
+ // while the client watched the task run and cannot be measured again.
243
+ const rebound = new Map<string, MeasuredTiming>();
121
244
 
122
245
  if (source.controller !== controller) {
123
246
  source.dispose();
124
247
  source.controller = controller;
125
248
  }
126
249
 
127
- for (const id of source.requestedNamespaceIds) {
128
- if (!subagents.has(id)) source.requestedNamespaceIds.delete(id);
250
+ for (const id of source.namespaceRequests.keys()) {
251
+ if (!subagents.has(id)) source.namespaceRequests.delete(id);
129
252
  }
130
253
 
131
254
  for (const [id, resource] of source.resources) {
@@ -135,8 +258,13 @@ const createSubagentTranscriptSource = (): SubagentTranscriptSource => {
135
258
  snapshot.depth > MAX_SUBAGENT_DEPTH ||
136
259
  !sameNamespace(resource.namespace, snapshot.namespace)
137
260
  ) {
138
- resource.unsubscribe();
139
- resource.release();
261
+ if (snapshot && snapshot.depth <= MAX_SUBAGENT_DEPTH)
262
+ rebound.set(id, {
263
+ timing: resource.timing,
264
+ timingState: resource.timingState,
265
+ messageTiming: resource.messageTiming,
266
+ });
267
+ resource.dispose();
140
268
  source.resources.delete(id);
141
269
  continue;
142
270
  }
@@ -145,52 +273,96 @@ const createSubagentTranscriptSource = (): SubagentTranscriptSource => {
145
273
 
146
274
  for (const snapshot of subagents.values()) {
147
275
  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
- });
276
+ if (!needsNamespaceResolution(snapshot)) {
277
+ source.namespaceRequests.delete(snapshot.id);
278
+ } else {
279
+ const request = source.namespaceRequests.get(snapshot.id);
280
+ if (!request) {
281
+ const nextRequest: NamespaceRequest = {
282
+ id: snapshot.id,
283
+ attempts: 0,
284
+ pending: false,
285
+ retryQueued: false,
286
+ status: snapshot.status,
287
+ };
288
+ source.namespaceRequests.set(snapshot.id, nextRequest);
289
+ requestSubagentNamespace(source, controller, nextRequest);
290
+ } else if (request.status !== snapshot.status) {
291
+ request.status = snapshot.status;
292
+ if (request.pending) {
293
+ request.retryQueued = request.attempts < 2;
294
+ } else if (request.attempts < 2) {
295
+ requestSubagentNamespace(source, controller, request);
296
+ }
297
+ }
154
298
  }
155
299
  if (source.resources.has(snapshot.id)) continue;
156
300
  const acquired = controller.registry.acquire(
157
- messagesProjection(snapshot.namespace),
301
+ subagentMessagesProjection(snapshot.namespace),
158
302
  );
303
+ const acquiredUI =
304
+ snapshot.namespace.length > ROOT_UI_CHANNEL_DEPTH
305
+ ? controller.registry.acquire(
306
+ channelProjection(UI_CUSTOM_CHANNELS, snapshot.namespace),
307
+ )
308
+ : undefined;
159
309
  const resource: ProjectionResource = {
160
310
  snapshot,
161
311
  namespace: snapshot.namespace,
162
312
  store: acquired.store,
163
- unsubscribe: () => {},
164
- release: acquired.release,
313
+ uiStore: acquiredUI?.store,
314
+ dispose: () => {},
165
315
  storeSnapshot: undefined,
166
316
  status: undefined,
317
+ uiFoldMemo: createUIFoldMemo(),
318
+ localUiMessages: NO_UI_MESSAGES,
319
+ rootUiMessagesByParent: source.uiMessagesByParent,
320
+ uiMessagesByParent: source.uiMessagesByParent,
321
+ timingState: rebound.get(snapshot.id)?.timingState ?? null,
322
+ messageTiming:
323
+ rebound.get(snapshot.id)?.messageTiming ?? NO_MESSAGE_TIMING,
324
+ convertedMessageTiming: undefined,
325
+ convert: createConverter(
326
+ source.uiMessagesByParent,
327
+ NO_MESSAGE_TIMING,
328
+ ),
167
329
  uiMessages: [],
168
330
  converted: undefined,
169
331
  childTranscripts: undefined,
170
332
  transcript: undefined,
333
+ timing: rebound.has(snapshot.id)
334
+ ? rebound.get(snapshot.id)!.timing
335
+ : snapshot.status === "running"
336
+ ? { startedAt: snapshot.startedAt.getTime() }
337
+ : undefined,
338
+ entry: undefined,
171
339
  memo: createAttachMemo(),
172
340
  cache: createExternalMessageConversionCache(),
173
341
  };
174
- resource.unsubscribe = resource.store.subscribe(() => rebuild());
342
+ const unsubscribe = resource.store.subscribe(() => rebuild());
343
+ const unsubscribeUI = resource.uiStore?.subscribe(() => rebuild());
344
+ resource.dispose = () => {
345
+ unsubscribe();
346
+ unsubscribeUI?.();
347
+ acquired.release();
348
+ acquiredUI?.release();
349
+ };
175
350
  source.resources.set(snapshot.id, resource);
176
351
  }
177
352
 
178
353
  rebuild();
179
354
  },
180
355
  dispose() {
181
- for (const resource of source.resources.values()) {
182
- resource.unsubscribe();
183
- resource.release();
184
- }
356
+ for (const resource of source.resources.values()) resource.dispose();
185
357
  source.resources.clear();
186
- source.requestedNamespaceIds.clear();
358
+ source.namespaceRequests.clear();
187
359
  source.snapshot = new Map();
188
360
  for (const listener of source.listeners) listener();
189
361
  },
190
362
  };
191
363
 
192
364
  const rebuild = () => {
193
- const { convert, uiMessagesByParent } = source;
365
+ const { uiMessagesByParent } = source;
194
366
  const resources = [...source.resources.values()];
195
367
  const childrenByParent = new Map<string, ProjectionResource[]>();
196
368
 
@@ -202,7 +374,7 @@ const createSubagentTranscriptSource = (): SubagentTranscriptSource => {
202
374
  else childrenByParent.set(parentId, [resource]);
203
375
  }
204
376
 
205
- const transcripts = new Map<string, readonly ThreadMessage[]>();
377
+ const transcripts = new Map<string, SubagentTranscript>();
206
378
  let changed = source.snapshot.size !== resources.length;
207
379
  const built = new Set<string>();
208
380
 
@@ -216,24 +388,57 @@ const createSubagentTranscriptSource = (): SubagentTranscriptSource => {
216
388
  for (const child of children) build(child, depth + 1);
217
389
  const childTranscripts = new Map(
218
390
  children.flatMap((child) =>
219
- child.transcript
220
- ? [[child.snapshot.id, child.transcript] as const]
221
- : [],
391
+ child.entry ? [[child.snapshot.id, child.entry] as const] : [],
222
392
  ),
223
393
  );
224
394
  const storeSnapshot = resource.store.getSnapshot();
225
395
  const status = resource.snapshot.status;
226
- const uiMessages = collectUIMessages(storeSnapshot, uiMessagesByParent);
396
+ const stepped = stepStreamingTiming(
397
+ resource.timingState,
398
+ storeSnapshot as LangChainBaseMessage[],
399
+ status === "running",
400
+ langChainStreamingTimingAccessors,
401
+ undefined,
402
+ );
403
+ resource.timingState = stepped.state;
404
+ if (Object.keys(stepped.timings).length > 0) {
405
+ resource.messageTiming = {
406
+ ...resource.messageTiming,
407
+ ...stepped.timings,
408
+ };
409
+ }
410
+ const localUiMessages = foldLocalUIMessages(resource);
411
+ if (
412
+ resource.localUiMessages !== localUiMessages ||
413
+ resource.rootUiMessagesByParent !== uiMessagesByParent ||
414
+ resource.convertedMessageTiming !== resource.messageTiming
415
+ ) {
416
+ resource.localUiMessages = localUiMessages;
417
+ resource.rootUiMessagesByParent = uiMessagesByParent;
418
+ resource.uiMessagesByParent =
419
+ localUiMessages.length === 0
420
+ ? uiMessagesByParent
421
+ : mergeLocalUIMessages(uiMessagesByParent, localUiMessages);
422
+ resource.convert = createConverter(
423
+ resource.uiMessagesByParent,
424
+ resource.messageTiming,
425
+ );
426
+ }
427
+ const uiMessages = collectUIMessages(
428
+ storeSnapshot,
429
+ resource.uiMessagesByParent,
430
+ );
227
431
 
228
432
  const conversionChanged =
229
433
  resource.converted === undefined ||
230
434
  resource.storeSnapshot !== storeSnapshot ||
231
435
  resource.status !== status ||
436
+ resource.convertedMessageTiming !== resource.messageTiming ||
232
437
  !sameUIMessages(resource.uiMessages, uiMessages);
233
438
  if (conversionChanged) {
234
439
  resource.converted = convertExternalMessages(
235
440
  storeSnapshot as LangChainBaseMessage[],
236
- convert,
441
+ resource.convert,
237
442
  status === "running",
238
443
  TRANSCRIPT_METADATA,
239
444
  resource.cache,
@@ -241,6 +446,7 @@ const createSubagentTranscriptSource = (): SubagentTranscriptSource => {
241
446
  resource.storeSnapshot = storeSnapshot;
242
447
  resource.status = status;
243
448
  resource.uiMessages = uiMessages;
449
+ resource.convertedMessageTiming = resource.messageTiming;
244
450
  }
245
451
 
246
452
  if (
@@ -258,8 +464,21 @@ const createSubagentTranscriptSource = (): SubagentTranscriptSource => {
258
464
  resource.childTranscripts = childTranscripts;
259
465
  }
260
466
 
467
+ updateTiming(resource);
468
+ if (
469
+ resource.entry === undefined ||
470
+ resource.entry.messages !== resource.transcript ||
471
+ resource.entry.timing !== resource.timing
472
+ ) {
473
+ resource.entry = {
474
+ messages: resource.transcript!,
475
+ ...(resource.timing && { timing: resource.timing }),
476
+ };
477
+ changed = true;
478
+ }
479
+
261
480
  if (!source.snapshot.has(resource.snapshot.id)) changed = true;
262
- transcripts.set(resource.snapshot.id, resource.transcript);
481
+ transcripts.set(resource.snapshot.id, resource.entry);
263
482
  };
264
483
 
265
484
  for (const resource of resources) {
@@ -282,7 +501,7 @@ const createSubagentTranscriptSource = (): SubagentTranscriptSource => {
282
501
  export const useSubagentTranscripts = (
283
502
  stream: AnyStream,
284
503
  uiMessagesByParent: Map<string, UIMessage[]>,
285
- ): ReadonlyMap<string, readonly ThreadMessage[]> => {
504
+ ): ReadonlyMap<string, SubagentTranscript> => {
286
505
  const sourceRef = useRef<SubagentTranscriptSource | undefined>(undefined);
287
506
  if (!sourceRef.current) {
288
507
  sourceRef.current = createSubagentTranscriptSource();