@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
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { act, renderHook, waitFor } from "@testing-library/react";
|
|
4
4
|
import { describe, expect, it, vi } from "vitest";
|
|
5
|
+
import type { ThreadMessage } from "@assistant-ui/core";
|
|
5
6
|
import type { LangChainBaseMessage, UIMessage } from "./types";
|
|
6
7
|
|
|
7
8
|
const { streamController } = vi.hoisted(() => ({
|
|
@@ -17,15 +18,15 @@ import {
|
|
|
17
18
|
useSubagentTranscripts,
|
|
18
19
|
} from "./useSubagentTranscripts";
|
|
19
20
|
|
|
20
|
-
type FakeStore = {
|
|
21
|
-
getSnapshot():
|
|
21
|
+
type FakeStore<T> = {
|
|
22
|
+
getSnapshot(): T;
|
|
22
23
|
subscribe(listener: () => void): () => void;
|
|
23
24
|
notify(): void;
|
|
24
|
-
setSnapshot(
|
|
25
|
+
setSnapshot(snapshot: T): void;
|
|
25
26
|
};
|
|
26
27
|
|
|
27
|
-
const
|
|
28
|
-
let snapshot =
|
|
28
|
+
const createFakeStore = <T,>(initial: T): FakeStore<T> => {
|
|
29
|
+
let snapshot = initial;
|
|
29
30
|
const listeners = new Set<() => void>();
|
|
30
31
|
return {
|
|
31
32
|
getSnapshot: () => snapshot,
|
|
@@ -36,13 +37,27 @@ const createStore = (messages: LangChainBaseMessage[] = []): FakeStore => {
|
|
|
36
37
|
notify() {
|
|
37
38
|
for (const listener of listeners) listener();
|
|
38
39
|
},
|
|
39
|
-
setSnapshot(
|
|
40
|
-
snapshot =
|
|
40
|
+
setSnapshot(next) {
|
|
41
|
+
snapshot = next;
|
|
41
42
|
for (const listener of listeners) listener();
|
|
42
43
|
},
|
|
43
44
|
};
|
|
44
45
|
};
|
|
45
46
|
|
|
47
|
+
const createStore = (messages: LangChainBaseMessage[] = []) =>
|
|
48
|
+
createFakeStore(messages);
|
|
49
|
+
|
|
50
|
+
const createUIStore = (events: UIChannelEvent[] = []) =>
|
|
51
|
+
createFakeStore(events);
|
|
52
|
+
|
|
53
|
+
const createDeferred = () => {
|
|
54
|
+
let resolve!: () => void;
|
|
55
|
+
const promise = new Promise<void>((res) => {
|
|
56
|
+
resolve = res;
|
|
57
|
+
});
|
|
58
|
+
return { promise, resolve };
|
|
59
|
+
};
|
|
60
|
+
|
|
46
61
|
const message = (
|
|
47
62
|
id: string,
|
|
48
63
|
type: "human" | "ai",
|
|
@@ -59,25 +74,42 @@ const subagent = (
|
|
|
59
74
|
status: "running" | "complete" | "error" = "running",
|
|
60
75
|
parentId: string | null = null,
|
|
61
76
|
depth = 1,
|
|
77
|
+
startedAt = new Date(1_000),
|
|
78
|
+
completedAt: Date | null = status === "running" ? null : new Date(4_000),
|
|
62
79
|
) => ({
|
|
63
80
|
id,
|
|
64
81
|
namespace,
|
|
65
82
|
status,
|
|
66
83
|
parentId,
|
|
67
84
|
depth,
|
|
85
|
+
startedAt,
|
|
86
|
+
completedAt,
|
|
68
87
|
});
|
|
69
88
|
|
|
89
|
+
const messagesOf = (
|
|
90
|
+
transcripts: ReadonlyMap<string, { messages: readonly ThreadMessage[] }>,
|
|
91
|
+
id: string,
|
|
92
|
+
) => transcripts.get(id)?.messages;
|
|
93
|
+
|
|
70
94
|
const createStream = (
|
|
71
95
|
subagents: ReadonlyMap<string, ReturnType<typeof subagent>>,
|
|
72
|
-
stores: Map<string, FakeStore
|
|
96
|
+
stores: Map<string, FakeStore<LangChainBaseMessage[]>>,
|
|
97
|
+
uiStores = new Map<string, FakeStore<UIChannelEvent[]>>(),
|
|
73
98
|
) => {
|
|
74
99
|
const releases = new Map<string, ReturnType<typeof vi.fn>>();
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
100
|
+
const uiReleases = new Map<string, ReturnType<typeof vi.fn>>();
|
|
101
|
+
const acquire = vi.fn(
|
|
102
|
+
(spec: { key: string; namespace: readonly string[] }) => {
|
|
103
|
+
const key = spec.namespace.join("/");
|
|
104
|
+
const release = vi.fn();
|
|
105
|
+
if (spec.key.startsWith("channel|")) {
|
|
106
|
+
uiReleases.set(key, release);
|
|
107
|
+
return { store: uiStores.get(key) ?? createUIStore(), release };
|
|
108
|
+
}
|
|
109
|
+
releases.set(key, release);
|
|
110
|
+
return { store: stores.get(key)!, release };
|
|
111
|
+
},
|
|
112
|
+
);
|
|
81
113
|
const resolveSubagentNamespace = vi.fn(async () => {});
|
|
82
114
|
return {
|
|
83
115
|
subagents,
|
|
@@ -87,6 +119,7 @@ const createStream = (
|
|
|
87
119
|
},
|
|
88
120
|
acquire,
|
|
89
121
|
releases,
|
|
122
|
+
uiReleases,
|
|
90
123
|
resolveSubagentNamespace,
|
|
91
124
|
};
|
|
92
125
|
};
|
|
@@ -101,6 +134,34 @@ const chart = (id: string, messageId: string): UIMessage => ({
|
|
|
101
134
|
metadata: { message_id: messageId },
|
|
102
135
|
});
|
|
103
136
|
|
|
137
|
+
type UIChannelEvent = {
|
|
138
|
+
method: "custom";
|
|
139
|
+
params: { namespace: readonly string[]; data: unknown };
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const uiEvent = (
|
|
143
|
+
namespace: readonly string[],
|
|
144
|
+
data: unknown,
|
|
145
|
+
): UIChannelEvent => ({
|
|
146
|
+
method: "custom",
|
|
147
|
+
params: { namespace, data },
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const nestedSubagents = () =>
|
|
151
|
+
new Map([
|
|
152
|
+
["task-parent", subagent("task-parent", ["tools:parent"])],
|
|
153
|
+
[
|
|
154
|
+
"task-child",
|
|
155
|
+
subagent(
|
|
156
|
+
"task-child",
|
|
157
|
+
["tools:parent", "tools:child"],
|
|
158
|
+
"running",
|
|
159
|
+
"task-parent",
|
|
160
|
+
2,
|
|
161
|
+
),
|
|
162
|
+
],
|
|
163
|
+
]);
|
|
164
|
+
|
|
104
165
|
describe("useSubagentTranscripts", () => {
|
|
105
166
|
it("acquires each namespace once and releases every projection on unmount", async () => {
|
|
106
167
|
const stores = new Map([
|
|
@@ -122,17 +183,139 @@ describe("useSubagentTranscripts", () => {
|
|
|
122
183
|
hook.rerender();
|
|
123
184
|
|
|
124
185
|
expect(stream.acquire).toHaveBeenCalledTimes(2);
|
|
125
|
-
expect(stream.resolveSubagentNamespace).
|
|
186
|
+
expect(stream.resolveSubagentNamespace).not.toHaveBeenCalled();
|
|
126
187
|
hook.unmount();
|
|
127
188
|
expect(stream.releases.get("tools:one")).toHaveBeenCalledOnce();
|
|
128
189
|
expect(stream.releases.get("tools:two")).toHaveBeenCalledOnce();
|
|
129
190
|
});
|
|
130
191
|
|
|
192
|
+
it("serializes a bounded retry for a later unresolved status", async () => {
|
|
193
|
+
const stores = new Map([["tools:task-one", createStore()]]);
|
|
194
|
+
const stream = createStream(
|
|
195
|
+
new Map([["task-one", subagent("task-one", ["tools:task-one"])]]),
|
|
196
|
+
stores,
|
|
197
|
+
);
|
|
198
|
+
const firstRequest = createDeferred();
|
|
199
|
+
const secondRequest = createDeferred();
|
|
200
|
+
stream.resolveSubagentNamespace
|
|
201
|
+
.mockImplementationOnce(() => firstRequest.promise)
|
|
202
|
+
.mockImplementationOnce(() => secondRequest.promise);
|
|
203
|
+
const hook = renderHook(() =>
|
|
204
|
+
useSubagentTranscripts(stream as never, noUIMessages),
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
await waitFor(() =>
|
|
208
|
+
expect(stream.resolveSubagentNamespace).toHaveBeenCalledOnce(),
|
|
209
|
+
);
|
|
210
|
+
act(() => {
|
|
211
|
+
stream.subagents = new Map([
|
|
212
|
+
["task-one", subagent("task-one", ["tools:task-one"])],
|
|
213
|
+
]);
|
|
214
|
+
hook.rerender();
|
|
215
|
+
});
|
|
216
|
+
expect(stream.resolveSubagentNamespace).toHaveBeenCalledOnce();
|
|
217
|
+
|
|
218
|
+
act(() => {
|
|
219
|
+
stream.subagents = new Map([
|
|
220
|
+
["task-one", subagent("task-one", ["tools:task-one"], "complete")],
|
|
221
|
+
]);
|
|
222
|
+
hook.rerender();
|
|
223
|
+
});
|
|
224
|
+
expect(stream.resolveSubagentNamespace).toHaveBeenCalledOnce();
|
|
225
|
+
|
|
226
|
+
await act(async () => {
|
|
227
|
+
firstRequest.resolve();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
await waitFor(() =>
|
|
231
|
+
expect(stream.resolveSubagentNamespace).toHaveBeenCalledTimes(2),
|
|
232
|
+
);
|
|
233
|
+
act(() => {
|
|
234
|
+
stream.subagents = new Map([
|
|
235
|
+
["task-one", subagent("task-one", ["tools:task-one"], "complete")],
|
|
236
|
+
]);
|
|
237
|
+
hook.rerender();
|
|
238
|
+
});
|
|
239
|
+
secondRequest.resolve();
|
|
240
|
+
await act(() => Promise.resolve());
|
|
241
|
+
expect(stream.resolveSubagentNamespace).toHaveBeenCalledTimes(2);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("retries only after an unresolved namespace changes status", async () => {
|
|
245
|
+
const stores = new Map([["tools:task-one", createStore()]]);
|
|
246
|
+
const stream = createStream(
|
|
247
|
+
new Map([["task-one", subagent("task-one", ["tools:task-one"])]]),
|
|
248
|
+
stores,
|
|
249
|
+
);
|
|
250
|
+
const hook = renderHook(() =>
|
|
251
|
+
useSubagentTranscripts(stream as never, noUIMessages),
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
await waitFor(() =>
|
|
255
|
+
expect(stream.resolveSubagentNamespace).toHaveBeenCalledOnce(),
|
|
256
|
+
);
|
|
257
|
+
await act(async () => {
|
|
258
|
+
await Promise.resolve();
|
|
259
|
+
await Promise.resolve();
|
|
260
|
+
stream.subagents = new Map([
|
|
261
|
+
["task-one", subagent("task-one", ["tools:task-one"])],
|
|
262
|
+
]);
|
|
263
|
+
hook.rerender();
|
|
264
|
+
});
|
|
265
|
+
expect(stream.resolveSubagentNamespace).toHaveBeenCalledOnce();
|
|
266
|
+
|
|
267
|
+
await act(async () => {
|
|
268
|
+
stream.subagents = new Map([
|
|
269
|
+
["task-one", subagent("task-one", ["tools:task-one"], "complete")],
|
|
270
|
+
]);
|
|
271
|
+
hook.rerender();
|
|
272
|
+
});
|
|
273
|
+
await waitFor(() =>
|
|
274
|
+
expect(stream.resolveSubagentNamespace).toHaveBeenCalledTimes(2),
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
act(() => {
|
|
278
|
+
stream.subagents = new Map([
|
|
279
|
+
["task-one", subagent("task-one", ["tools:task-one"], "complete")],
|
|
280
|
+
]);
|
|
281
|
+
hook.rerender();
|
|
282
|
+
});
|
|
283
|
+
expect(stream.resolveSubagentNamespace).toHaveBeenCalledTimes(2);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("retries an initially terminal unresolved namespace once", async () => {
|
|
287
|
+
const stores = new Map([["tools:task-one", createStore()]]);
|
|
288
|
+
const stream = createStream(
|
|
289
|
+
new Map([
|
|
290
|
+
["task-one", subagent("task-one", ["tools:task-one"], "complete")],
|
|
291
|
+
]),
|
|
292
|
+
stores,
|
|
293
|
+
);
|
|
294
|
+
const hook = renderHook(() =>
|
|
295
|
+
useSubagentTranscripts(stream as never, noUIMessages),
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
await waitFor(() =>
|
|
299
|
+
expect(stream.resolveSubagentNamespace).toHaveBeenCalledTimes(2),
|
|
300
|
+
);
|
|
301
|
+
await act(async () => {
|
|
302
|
+
await Promise.resolve();
|
|
303
|
+
stream.subagents = new Map([
|
|
304
|
+
["task-one", subagent("task-one", ["tools:task-one"], "complete")],
|
|
305
|
+
]);
|
|
306
|
+
hook.rerender();
|
|
307
|
+
});
|
|
308
|
+
expect(stream.resolveSubagentNamespace).toHaveBeenCalledTimes(2);
|
|
309
|
+
});
|
|
310
|
+
|
|
131
311
|
it("does not acquire depth-17 subagents and releases projections that move past the depth cap", async () => {
|
|
132
|
-
const stores = new Map([["tools:one", createStore()]]);
|
|
312
|
+
const stores = new Map([["tools:task-one", createStore()]]);
|
|
133
313
|
const stream = createStream(
|
|
134
314
|
new Map([
|
|
135
|
-
[
|
|
315
|
+
[
|
|
316
|
+
"task-one",
|
|
317
|
+
subagent("task-one", ["tools:task-one"], "running", null, 16),
|
|
318
|
+
],
|
|
136
319
|
]),
|
|
137
320
|
stores,
|
|
138
321
|
);
|
|
@@ -150,7 +333,7 @@ describe("useSubagentTranscripts", () => {
|
|
|
150
333
|
hook.rerender();
|
|
151
334
|
|
|
152
335
|
await waitFor(() =>
|
|
153
|
-
expect(stream.releases.get("tools:one")).toHaveBeenCalledOnce(),
|
|
336
|
+
expect(stream.releases.get("tools:task-one")).toHaveBeenCalledOnce(),
|
|
154
337
|
);
|
|
155
338
|
expect(stream.acquire).toHaveBeenCalledOnce();
|
|
156
339
|
expect(stream.resolveSubagentNamespace).toHaveBeenCalledOnce();
|
|
@@ -164,9 +347,9 @@ describe("useSubagentTranscripts", () => {
|
|
|
164
347
|
message("promoted-ai", "ai", "promoted"),
|
|
165
348
|
]);
|
|
166
349
|
const stream = createStream(
|
|
167
|
-
new Map([["task-one", subagent("task-one", ["tools:
|
|
350
|
+
new Map([["task-one", subagent("task-one", ["tools:task-one"])]]),
|
|
168
351
|
new Map([
|
|
169
|
-
["tools:
|
|
352
|
+
["tools:task-one", placeholderStore],
|
|
170
353
|
["tools:promoted", promotedStore],
|
|
171
354
|
]),
|
|
172
355
|
);
|
|
@@ -175,9 +358,9 @@ describe("useSubagentTranscripts", () => {
|
|
|
175
358
|
);
|
|
176
359
|
|
|
177
360
|
await waitFor(() =>
|
|
178
|
-
expect(
|
|
179
|
-
|
|
180
|
-
]),
|
|
361
|
+
expect(
|
|
362
|
+
messagesOf(hook.result.current, "task-one")?.[0]?.content,
|
|
363
|
+
).toMatchObject([{ type: "text", text: "placeholder" }]),
|
|
181
364
|
);
|
|
182
365
|
|
|
183
366
|
stream.subagents = new Map([
|
|
@@ -186,11 +369,11 @@ describe("useSubagentTranscripts", () => {
|
|
|
186
369
|
hook.rerender();
|
|
187
370
|
|
|
188
371
|
await waitFor(() =>
|
|
189
|
-
expect(
|
|
190
|
-
|
|
191
|
-
]),
|
|
372
|
+
expect(
|
|
373
|
+
messagesOf(hook.result.current, "task-one")?.[0]?.content,
|
|
374
|
+
).toMatchObject([{ type: "text", text: "promoted" }]),
|
|
192
375
|
);
|
|
193
|
-
expect(stream.releases.get("tools:
|
|
376
|
+
expect(stream.releases.get("tools:task-one")).toHaveBeenCalledOnce();
|
|
194
377
|
expect(stream.acquire).toHaveBeenCalledWith(
|
|
195
378
|
expect.objectContaining({ namespace: ["tools:promoted"] }),
|
|
196
379
|
);
|
|
@@ -223,9 +406,9 @@ describe("useSubagentTranscripts", () => {
|
|
|
223
406
|
});
|
|
224
407
|
|
|
225
408
|
expect(hook.result.current).not.toBe(initial);
|
|
226
|
-
expect(
|
|
227
|
-
|
|
228
|
-
]);
|
|
409
|
+
expect(
|
|
410
|
+
messagesOf(hook.result.current, "task-one")?.[0]?.content,
|
|
411
|
+
).toMatchObject([{ type: "text", text: "two" }]);
|
|
229
412
|
});
|
|
230
413
|
|
|
231
414
|
it("keeps unchanged transcript messages when a projection update replaces one", async () => {
|
|
@@ -240,15 +423,15 @@ describe("useSubagentTranscripts", () => {
|
|
|
240
423
|
);
|
|
241
424
|
|
|
242
425
|
await waitFor(() =>
|
|
243
|
-
expect(hook.result.current
|
|
426
|
+
expect(messagesOf(hook.result.current, "task-one")).toHaveLength(2),
|
|
244
427
|
);
|
|
245
|
-
const initial = hook.result.current
|
|
428
|
+
const initial = messagesOf(hook.result.current, "task-one");
|
|
246
429
|
|
|
247
430
|
await act(async () => {
|
|
248
431
|
store.setSnapshot([question, message("subagent-ai", "ai", "partial")]);
|
|
249
432
|
});
|
|
250
433
|
|
|
251
|
-
const updated = hook.result.current
|
|
434
|
+
const updated = messagesOf(hook.result.current, "task-one");
|
|
252
435
|
expect(updated).toHaveLength(2);
|
|
253
436
|
expect(updated).not.toBe(initial);
|
|
254
437
|
expect(updated?.[0]).toBe(initial?.[0]);
|
|
@@ -273,7 +456,9 @@ describe("useSubagentTranscripts", () => {
|
|
|
273
456
|
hook.rerender();
|
|
274
457
|
|
|
275
458
|
await waitFor(() =>
|
|
276
|
-
expect(
|
|
459
|
+
expect(
|
|
460
|
+
messagesOf(hook.result.current, "task-one")?.[0]?.content,
|
|
461
|
+
).toMatchObject([
|
|
277
462
|
{ type: "text", text: "answer" },
|
|
278
463
|
{ type: "data", name: "chart", data: { points: [1, 2, 3] } },
|
|
279
464
|
]),
|
|
@@ -309,7 +494,7 @@ describe("useSubagentTranscripts", () => {
|
|
|
309
494
|
uiMessagesByParent = new Map([["one-ai", [ui]]]);
|
|
310
495
|
hook.rerender();
|
|
311
496
|
await waitFor(() =>
|
|
312
|
-
expect(hook.result.current
|
|
497
|
+
expect(messagesOf(hook.result.current, "task-one")).not.toBe(
|
|
313
498
|
initial.get("task-one"),
|
|
314
499
|
),
|
|
315
500
|
);
|
|
@@ -332,7 +517,9 @@ describe("useSubagentTranscripts", () => {
|
|
|
332
517
|
);
|
|
333
518
|
|
|
334
519
|
await waitFor(() =>
|
|
335
|
-
expect(
|
|
520
|
+
expect(
|
|
521
|
+
messagesOf(hook.result.current, "task-one")?.[0]?.status,
|
|
522
|
+
).toMatchObject({
|
|
336
523
|
type: "running",
|
|
337
524
|
}),
|
|
338
525
|
);
|
|
@@ -343,7 +530,9 @@ describe("useSubagentTranscripts", () => {
|
|
|
343
530
|
hook.rerender();
|
|
344
531
|
|
|
345
532
|
await waitFor(() =>
|
|
346
|
-
expect(
|
|
533
|
+
expect(
|
|
534
|
+
messagesOf(hook.result.current, "task-one")?.[0]?.status,
|
|
535
|
+
).toMatchObject({
|
|
347
536
|
type: "complete",
|
|
348
537
|
}),
|
|
349
538
|
);
|
|
@@ -368,16 +557,16 @@ describe("useSubagentTranscripts", () => {
|
|
|
368
557
|
|
|
369
558
|
await waitFor(() => expect(hook.result.current.size).toBe(2));
|
|
370
559
|
const initial = hook.result.current;
|
|
371
|
-
const initialOne = initial
|
|
372
|
-
const initialTwo = initial
|
|
560
|
+
const initialOne = messagesOf(initial, "task-one");
|
|
561
|
+
const initialTwo = messagesOf(initial, "task-two");
|
|
373
562
|
|
|
374
563
|
await act(async () => {
|
|
375
564
|
oneStore.setSnapshot([message("one-ai", "ai", "updated")]);
|
|
376
565
|
});
|
|
377
566
|
|
|
378
567
|
expect(hook.result.current).not.toBe(initial);
|
|
379
|
-
expect(hook.result.current
|
|
380
|
-
expect(hook.result.current
|
|
568
|
+
expect(messagesOf(hook.result.current, "task-one")).not.toBe(initialOne);
|
|
569
|
+
expect(messagesOf(hook.result.current, "task-two")).toBe(initialTwo);
|
|
381
570
|
});
|
|
382
571
|
|
|
383
572
|
it("nests a child discovered at the same depth as its parent", async () => {
|
|
@@ -407,18 +596,19 @@ describe("useSubagentTranscripts", () => {
|
|
|
407
596
|
);
|
|
408
597
|
|
|
409
598
|
await waitFor(() => expect(hook.result.current.size).toBe(2));
|
|
410
|
-
const taskCall =
|
|
411
|
-
.
|
|
412
|
-
|
|
599
|
+
const taskCall = messagesOf(
|
|
600
|
+
hook.result.current,
|
|
601
|
+
"task-parent",
|
|
602
|
+
)?.[0]?.content.find((part) => part.type === "tool-call");
|
|
413
603
|
|
|
414
604
|
expect(taskCall).toMatchObject({
|
|
415
|
-
messages: hook.result.current
|
|
605
|
+
messages: messagesOf(hook.result.current, "task-child"),
|
|
416
606
|
});
|
|
417
607
|
});
|
|
418
608
|
|
|
419
609
|
it("stops attaching descendants past sixteen levels of actual nesting", async () => {
|
|
420
610
|
const subagents = new Map<string, ReturnType<typeof subagent>>();
|
|
421
|
-
const stores = new Map<string, FakeStore
|
|
611
|
+
const stores = new Map<string, FakeStore<LangChainBaseMessage[]>>();
|
|
422
612
|
const levels = MAX_SUBAGENT_DEPTH + 2;
|
|
423
613
|
for (let level = 1; level <= levels; level += 1) {
|
|
424
614
|
const id = `task-${level}`;
|
|
@@ -453,7 +643,7 @@ describe("useSubagentTranscripts", () => {
|
|
|
453
643
|
);
|
|
454
644
|
|
|
455
645
|
await waitFor(() => expect(hook.result.current.size).toBe(levels));
|
|
456
|
-
let transcript = hook.result.current
|
|
646
|
+
let transcript = messagesOf(hook.result.current, "task-1");
|
|
457
647
|
let nested = 1;
|
|
458
648
|
while (transcript) {
|
|
459
649
|
const taskCall = transcript[0]?.content.find(
|
|
@@ -465,9 +655,10 @@ describe("useSubagentTranscripts", () => {
|
|
|
465
655
|
}
|
|
466
656
|
|
|
467
657
|
expect(nested).toBe(MAX_SUBAGENT_DEPTH);
|
|
468
|
-
const deepest =
|
|
469
|
-
.
|
|
470
|
-
|
|
658
|
+
const deepest = messagesOf(
|
|
659
|
+
hook.result.current,
|
|
660
|
+
`task-${MAX_SUBAGENT_DEPTH + 1}`,
|
|
661
|
+
)?.[0]?.content.find((part) => part.type === "tool-call");
|
|
471
662
|
expect(deepest).not.toHaveProperty("messages");
|
|
472
663
|
});
|
|
473
664
|
|
|
@@ -504,12 +695,564 @@ describe("useSubagentTranscripts", () => {
|
|
|
504
695
|
);
|
|
505
696
|
|
|
506
697
|
await waitFor(() => expect(hook.result.current.size).toBe(2));
|
|
507
|
-
const parentTranscript = hook.result.current
|
|
508
|
-
const childTranscript = hook.result.current
|
|
698
|
+
const parentTranscript = messagesOf(hook.result.current, "task-parent");
|
|
699
|
+
const childTranscript = messagesOf(hook.result.current, "task-child");
|
|
509
700
|
const taskCall = parentTranscript?.[0]?.content.find(
|
|
510
701
|
(part) => part.type === "tool-call",
|
|
511
702
|
);
|
|
512
703
|
|
|
513
704
|
expect(taskCall).toMatchObject({ messages: childTranscript });
|
|
514
705
|
});
|
|
706
|
+
it("renders live UI a subagent pushed from its own nested namespace", async () => {
|
|
707
|
+
const childStore = createStore([message("child-ai", "ai", "child answer")]);
|
|
708
|
+
const childUIStore = createUIStore([
|
|
709
|
+
uiEvent(["tools:parent", "tools:child"], chart("ui-child", "child-ai")),
|
|
710
|
+
uiEvent(
|
|
711
|
+
["tools:parent", "tools:child", "tools:grandchild"],
|
|
712
|
+
chart("ui-grandchild", "grandchild-ai"),
|
|
713
|
+
),
|
|
714
|
+
]);
|
|
715
|
+
const stream = createStream(
|
|
716
|
+
nestedSubagents(),
|
|
717
|
+
new Map([
|
|
718
|
+
[
|
|
719
|
+
"tools:parent",
|
|
720
|
+
createStore([message("parent-ai", "ai", "delegating")]),
|
|
721
|
+
],
|
|
722
|
+
["tools:parent/tools:child", childStore],
|
|
723
|
+
]),
|
|
724
|
+
new Map([["tools:parent/tools:child", childUIStore]]),
|
|
725
|
+
);
|
|
726
|
+
const hook = renderHook(() =>
|
|
727
|
+
useSubagentTranscripts(stream as never, noUIMessages),
|
|
728
|
+
);
|
|
729
|
+
|
|
730
|
+
await waitFor(() =>
|
|
731
|
+
expect(
|
|
732
|
+
messagesOf(hook.result.current, "task-child")?.[0]?.content,
|
|
733
|
+
).toMatchObject([
|
|
734
|
+
{ type: "text", text: "child answer" },
|
|
735
|
+
{ type: "data", name: "chart", data: { points: [1, 2, 3] } },
|
|
736
|
+
]),
|
|
737
|
+
);
|
|
738
|
+
|
|
739
|
+
await act(async () => {
|
|
740
|
+
childUIStore.setSnapshot([
|
|
741
|
+
uiEvent(["tools:parent", "tools:child"], chart("ui-child", "child-ai")),
|
|
742
|
+
uiEvent(["tools:parent", "tools:child"], {
|
|
743
|
+
type: "remove-ui",
|
|
744
|
+
id: "ui-child",
|
|
745
|
+
}),
|
|
746
|
+
]);
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
expect(
|
|
750
|
+
messagesOf(hook.result.current, "task-child")?.[0]?.content.some(
|
|
751
|
+
(part) => part.type === "data",
|
|
752
|
+
),
|
|
753
|
+
).toBe(false);
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
it("opens a custom projection only for subagents the root channel cannot reach", async () => {
|
|
757
|
+
const stream = createStream(
|
|
758
|
+
nestedSubagents(),
|
|
759
|
+
new Map([
|
|
760
|
+
["tools:parent", createStore()],
|
|
761
|
+
["tools:parent/tools:child", createStore()],
|
|
762
|
+
]),
|
|
763
|
+
);
|
|
764
|
+
const hook = renderHook(() =>
|
|
765
|
+
useSubagentTranscripts(stream as never, noUIMessages),
|
|
766
|
+
);
|
|
767
|
+
|
|
768
|
+
await waitFor(() => expect(stream.acquire).toHaveBeenCalledTimes(3));
|
|
769
|
+
const channelSpecs = stream.acquire.mock.calls
|
|
770
|
+
.map(([spec]) => spec)
|
|
771
|
+
.filter((spec) => spec.key.startsWith("channel|"));
|
|
772
|
+
expect(channelSpecs).toHaveLength(1);
|
|
773
|
+
expect(channelSpecs[0]?.namespace).toEqual(["tools:parent", "tools:child"]);
|
|
774
|
+
|
|
775
|
+
hook.unmount();
|
|
776
|
+
expect(
|
|
777
|
+
stream.uiReleases.get("tools:parent/tools:child"),
|
|
778
|
+
).toHaveBeenCalledOnce();
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
it("ignores live UI whose message is absent from this transcript", async () => {
|
|
782
|
+
const childUI = uiEvent(
|
|
783
|
+
["tools:parent", "tools:child"],
|
|
784
|
+
chart("ui-child", "child-ai"),
|
|
785
|
+
);
|
|
786
|
+
const childUIStore = createUIStore([childUI]);
|
|
787
|
+
const stream = createStream(
|
|
788
|
+
nestedSubagents(),
|
|
789
|
+
new Map([
|
|
790
|
+
["tools:parent", createStore()],
|
|
791
|
+
[
|
|
792
|
+
"tools:parent/tools:child",
|
|
793
|
+
createStore([message("child-ai", "ai", "child answer")]),
|
|
794
|
+
],
|
|
795
|
+
]),
|
|
796
|
+
new Map([["tools:parent/tools:child", childUIStore]]),
|
|
797
|
+
);
|
|
798
|
+
const hook = renderHook(() =>
|
|
799
|
+
useSubagentTranscripts(stream as never, noUIMessages),
|
|
800
|
+
);
|
|
801
|
+
|
|
802
|
+
await waitFor(() =>
|
|
803
|
+
expect(
|
|
804
|
+
messagesOf(hook.result.current, "task-child")?.[0]?.content.some(
|
|
805
|
+
(part) => part.type === "data",
|
|
806
|
+
),
|
|
807
|
+
).toBe(true),
|
|
808
|
+
);
|
|
809
|
+
const transcript = messagesOf(hook.result.current, "task-child");
|
|
810
|
+
|
|
811
|
+
await act(async () => {
|
|
812
|
+
childUIStore.setSnapshot([
|
|
813
|
+
childUI,
|
|
814
|
+
uiEvent(
|
|
815
|
+
["tools:parent", "tools:child", "tools:grandchild"],
|
|
816
|
+
chart("ui-grandchild", "grandchild-ai"),
|
|
817
|
+
),
|
|
818
|
+
]);
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
expect(messagesOf(hook.result.current, "task-child")).toBe(transcript);
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
it("keeps a nested transcript stable while a streamed UI update repeats", async () => {
|
|
825
|
+
const streamedChart = (props: Record<string, unknown>): UIMessage => ({
|
|
826
|
+
type: "ui",
|
|
827
|
+
id: "ui-child",
|
|
828
|
+
name: "chart",
|
|
829
|
+
props,
|
|
830
|
+
metadata: { message_id: "child-ai", merge: true },
|
|
831
|
+
});
|
|
832
|
+
const parentStore = createStore([message("parent-ai", "ai", "delegating")]);
|
|
833
|
+
const childUIStore = createUIStore([
|
|
834
|
+
uiEvent(["tools:parent", "tools:child"], streamedChart({ points: [1] })),
|
|
835
|
+
uiEvent(["tools:parent", "tools:child"], streamedChart({ label: "a" })),
|
|
836
|
+
]);
|
|
837
|
+
const stream = createStream(
|
|
838
|
+
nestedSubagents(),
|
|
839
|
+
new Map([
|
|
840
|
+
["tools:parent", parentStore],
|
|
841
|
+
[
|
|
842
|
+
"tools:parent/tools:child",
|
|
843
|
+
createStore([message("child-ai", "ai", "child answer")]),
|
|
844
|
+
],
|
|
845
|
+
]),
|
|
846
|
+
new Map([["tools:parent/tools:child", childUIStore]]),
|
|
847
|
+
);
|
|
848
|
+
const hook = renderHook(() =>
|
|
849
|
+
useSubagentTranscripts(stream as never, noUIMessages),
|
|
850
|
+
);
|
|
851
|
+
|
|
852
|
+
await waitFor(() =>
|
|
853
|
+
expect(
|
|
854
|
+
messagesOf(hook.result.current, "task-child")?.[0]?.content.some(
|
|
855
|
+
(part) => part.type === "data",
|
|
856
|
+
),
|
|
857
|
+
).toBe(true),
|
|
858
|
+
);
|
|
859
|
+
const transcript = messagesOf(hook.result.current, "task-child");
|
|
860
|
+
|
|
861
|
+
await act(async () => {
|
|
862
|
+
parentStore.notify();
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
expect(messagesOf(hook.result.current, "task-child")).toBe(transcript);
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
it("keeps graph state authoritative over a live nested update", async () => {
|
|
869
|
+
const childUIStore = createUIStore([
|
|
870
|
+
uiEvent(["tools:parent", "tools:child"], chart("ui-child", "child-ai")),
|
|
871
|
+
]);
|
|
872
|
+
const stream = createStream(
|
|
873
|
+
nestedSubagents(),
|
|
874
|
+
new Map([
|
|
875
|
+
["tools:parent", createStore()],
|
|
876
|
+
[
|
|
877
|
+
"tools:parent/tools:child",
|
|
878
|
+
createStore([message("child-ai", "ai", "child answer")]),
|
|
879
|
+
],
|
|
880
|
+
]),
|
|
881
|
+
new Map([["tools:parent/tools:child", childUIStore]]),
|
|
882
|
+
);
|
|
883
|
+
const stateUiMessages = new Map<string, UIMessage[]>([
|
|
884
|
+
[
|
|
885
|
+
"child-ai",
|
|
886
|
+
[
|
|
887
|
+
{
|
|
888
|
+
type: "ui",
|
|
889
|
+
id: "ui-child",
|
|
890
|
+
name: "chart",
|
|
891
|
+
props: { points: [9] },
|
|
892
|
+
metadata: { message_id: "child-ai" },
|
|
893
|
+
},
|
|
894
|
+
],
|
|
895
|
+
],
|
|
896
|
+
]);
|
|
897
|
+
const hook = renderHook(() =>
|
|
898
|
+
useSubagentTranscripts(stream as never, stateUiMessages),
|
|
899
|
+
);
|
|
900
|
+
|
|
901
|
+
await waitFor(() =>
|
|
902
|
+
expect(
|
|
903
|
+
messagesOf(hook.result.current, "task-child")?.[0]?.content,
|
|
904
|
+
).toMatchObject([
|
|
905
|
+
{ type: "text", text: "child answer" },
|
|
906
|
+
{ type: "data", name: "chart", data: { points: [9] } },
|
|
907
|
+
]),
|
|
908
|
+
);
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
it("carries the subagent's wall clock onto its task call", async () => {
|
|
912
|
+
const parentMessage: LangChainBaseMessage = {
|
|
913
|
+
id: "parent-ai",
|
|
914
|
+
_getType: () => "ai",
|
|
915
|
+
content: "delegating",
|
|
916
|
+
tool_calls: [{ id: "task-child", name: "task", args: {} }],
|
|
917
|
+
};
|
|
918
|
+
const stream = createStream(
|
|
919
|
+
new Map([
|
|
920
|
+
["task-parent", subagent("task-parent", ["tools:parent"])],
|
|
921
|
+
[
|
|
922
|
+
"task-child",
|
|
923
|
+
subagent(
|
|
924
|
+
"task-child",
|
|
925
|
+
["tools:parent", "tools:child"],
|
|
926
|
+
"running",
|
|
927
|
+
"task-parent",
|
|
928
|
+
2,
|
|
929
|
+
new Date(1_000),
|
|
930
|
+
null,
|
|
931
|
+
),
|
|
932
|
+
],
|
|
933
|
+
]),
|
|
934
|
+
new Map([
|
|
935
|
+
["tools:parent", createStore([parentMessage])],
|
|
936
|
+
[
|
|
937
|
+
"tools:parent/tools:child",
|
|
938
|
+
createStore([message("child-ai", "ai", "child answer")]),
|
|
939
|
+
],
|
|
940
|
+
]),
|
|
941
|
+
);
|
|
942
|
+
const hook = renderHook(() =>
|
|
943
|
+
useSubagentTranscripts(stream as never, noUIMessages),
|
|
944
|
+
);
|
|
945
|
+
|
|
946
|
+
const taskCall = () =>
|
|
947
|
+
messagesOf(hook.result.current, "task-parent")?.[0]?.content.find(
|
|
948
|
+
(part) => part.type === "tool-call",
|
|
949
|
+
);
|
|
950
|
+
|
|
951
|
+
await waitFor(() =>
|
|
952
|
+
expect(taskCall()).toMatchObject({ timing: { startedAt: 1_000 } }),
|
|
953
|
+
);
|
|
954
|
+
expect(taskCall()).not.toHaveProperty("timing.completedAt");
|
|
955
|
+
|
|
956
|
+
stream.subagents = new Map([
|
|
957
|
+
["task-parent", subagent("task-parent", ["tools:parent"])],
|
|
958
|
+
[
|
|
959
|
+
"task-child",
|
|
960
|
+
subagent(
|
|
961
|
+
"task-child",
|
|
962
|
+
["tools:parent", "tools:child"],
|
|
963
|
+
"complete",
|
|
964
|
+
"task-parent",
|
|
965
|
+
2,
|
|
966
|
+
new Date(1_000),
|
|
967
|
+
new Date(4_500),
|
|
968
|
+
),
|
|
969
|
+
],
|
|
970
|
+
]);
|
|
971
|
+
hook.rerender();
|
|
972
|
+
|
|
973
|
+
await waitFor(() =>
|
|
974
|
+
expect(taskCall()).toMatchObject({
|
|
975
|
+
timing: { startedAt: 1_000, completedAt: 4_500 },
|
|
976
|
+
}),
|
|
977
|
+
);
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
it("records streaming timing for a nested message once its subagent finishes", async () => {
|
|
981
|
+
const store = createStore([message("child-ai", "ai", "partial")]);
|
|
982
|
+
const stream = createStream(
|
|
983
|
+
new Map([
|
|
984
|
+
[
|
|
985
|
+
"task-child",
|
|
986
|
+
subagent(
|
|
987
|
+
"task-child",
|
|
988
|
+
["tools:parent", "tools:child"],
|
|
989
|
+
"running",
|
|
990
|
+
null,
|
|
991
|
+
2,
|
|
992
|
+
),
|
|
993
|
+
],
|
|
994
|
+
]),
|
|
995
|
+
new Map([["tools:parent/tools:child", store]]),
|
|
996
|
+
);
|
|
997
|
+
const hook = renderHook(() =>
|
|
998
|
+
useSubagentTranscripts(stream as never, noUIMessages),
|
|
999
|
+
);
|
|
1000
|
+
|
|
1001
|
+
await waitFor(() =>
|
|
1002
|
+
expect(messagesOf(hook.result.current, "task-child")).toHaveLength(1),
|
|
1003
|
+
);
|
|
1004
|
+
expect(
|
|
1005
|
+
messagesOf(hook.result.current, "task-child")?.[0]?.metadata,
|
|
1006
|
+
).not.toHaveProperty("timing");
|
|
1007
|
+
|
|
1008
|
+
await act(async () => {
|
|
1009
|
+
store.setSnapshot([message("child-ai", "ai", "partial answer")]);
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
stream.subagents = new Map([
|
|
1013
|
+
[
|
|
1014
|
+
"task-child",
|
|
1015
|
+
subagent(
|
|
1016
|
+
"task-child",
|
|
1017
|
+
["tools:parent", "tools:child"],
|
|
1018
|
+
"complete",
|
|
1019
|
+
null,
|
|
1020
|
+
2,
|
|
1021
|
+
),
|
|
1022
|
+
],
|
|
1023
|
+
]);
|
|
1024
|
+
hook.rerender();
|
|
1025
|
+
|
|
1026
|
+
await waitFor(() =>
|
|
1027
|
+
expect(
|
|
1028
|
+
messagesOf(hook.result.current, "task-child")?.[0]?.metadata?.timing,
|
|
1029
|
+
).toMatchObject({ totalChunks: 2, toolCallCount: 0 }),
|
|
1030
|
+
);
|
|
1031
|
+
const timing = messagesOf(hook.result.current, "task-child")?.[0]?.metadata
|
|
1032
|
+
?.timing;
|
|
1033
|
+
expect(timing?.tokenCount).toBeGreaterThan(0);
|
|
1034
|
+
expect(timing?.totalStreamTime).toBeGreaterThanOrEqual(0);
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
it("gives a task seeded from a checkpoint no wall clock", async () => {
|
|
1038
|
+
const parentMessage: LangChainBaseMessage = {
|
|
1039
|
+
id: "parent-ai",
|
|
1040
|
+
_getType: () => "ai",
|
|
1041
|
+
content: "delegating",
|
|
1042
|
+
tool_calls: [{ id: "task-child", name: "task", args: {} }],
|
|
1043
|
+
};
|
|
1044
|
+
const seeded = new Date(2_000);
|
|
1045
|
+
const stream = createStream(
|
|
1046
|
+
new Map([
|
|
1047
|
+
["task-parent", subagent("task-parent", ["tools:parent"])],
|
|
1048
|
+
[
|
|
1049
|
+
"task-child",
|
|
1050
|
+
subagent(
|
|
1051
|
+
"task-child",
|
|
1052
|
+
["tools:parent", "tools:child"],
|
|
1053
|
+
"complete",
|
|
1054
|
+
"task-parent",
|
|
1055
|
+
2,
|
|
1056
|
+
seeded,
|
|
1057
|
+
seeded,
|
|
1058
|
+
),
|
|
1059
|
+
],
|
|
1060
|
+
]),
|
|
1061
|
+
new Map([
|
|
1062
|
+
["tools:parent", createStore([parentMessage])],
|
|
1063
|
+
[
|
|
1064
|
+
"tools:parent/tools:child",
|
|
1065
|
+
createStore([message("child-ai", "ai", "child answer")]),
|
|
1066
|
+
],
|
|
1067
|
+
]),
|
|
1068
|
+
);
|
|
1069
|
+
const hook = renderHook(() =>
|
|
1070
|
+
useSubagentTranscripts(stream as never, noUIMessages),
|
|
1071
|
+
);
|
|
1072
|
+
|
|
1073
|
+
await waitFor(() =>
|
|
1074
|
+
expect(hook.result.current.has("task-child")).toBe(true),
|
|
1075
|
+
);
|
|
1076
|
+
const taskCall = messagesOf(
|
|
1077
|
+
hook.result.current,
|
|
1078
|
+
"task-parent",
|
|
1079
|
+
)?.[0]?.content.find((part) => part.type === "tool-call");
|
|
1080
|
+
|
|
1081
|
+
expect(taskCall).toMatchObject({ toolCallId: "task-child" });
|
|
1082
|
+
expect(taskCall).not.toHaveProperty("timing");
|
|
1083
|
+
});
|
|
1084
|
+
|
|
1085
|
+
it("keeps the wall clock of a task whose namespace resolves after it finished", async () => {
|
|
1086
|
+
const stream = createStream(
|
|
1087
|
+
new Map([
|
|
1088
|
+
[
|
|
1089
|
+
"task-one",
|
|
1090
|
+
subagent(
|
|
1091
|
+
"task-one",
|
|
1092
|
+
["tools:task-one"],
|
|
1093
|
+
"running",
|
|
1094
|
+
null,
|
|
1095
|
+
1,
|
|
1096
|
+
new Date(1_000),
|
|
1097
|
+
null,
|
|
1098
|
+
),
|
|
1099
|
+
],
|
|
1100
|
+
]),
|
|
1101
|
+
new Map([
|
|
1102
|
+
["tools:task-one", createStore([message("one-ai", "ai", "partial")])],
|
|
1103
|
+
["tools:promoted", createStore([message("one-ai", "ai", "answer")])],
|
|
1104
|
+
]),
|
|
1105
|
+
);
|
|
1106
|
+
const hook = renderHook(() =>
|
|
1107
|
+
useSubagentTranscripts(stream as never, noUIMessages),
|
|
1108
|
+
);
|
|
1109
|
+
|
|
1110
|
+
await waitFor(() =>
|
|
1111
|
+
expect(hook.result.current.get("task-one")?.timing).toMatchObject({
|
|
1112
|
+
startedAt: 1_000,
|
|
1113
|
+
}),
|
|
1114
|
+
);
|
|
1115
|
+
|
|
1116
|
+
stream.subagents = new Map([
|
|
1117
|
+
[
|
|
1118
|
+
"task-one",
|
|
1119
|
+
subagent(
|
|
1120
|
+
"task-one",
|
|
1121
|
+
["tools:promoted"],
|
|
1122
|
+
"complete",
|
|
1123
|
+
null,
|
|
1124
|
+
1,
|
|
1125
|
+
new Date(1_000),
|
|
1126
|
+
new Date(6_000),
|
|
1127
|
+
),
|
|
1128
|
+
],
|
|
1129
|
+
]);
|
|
1130
|
+
hook.rerender();
|
|
1131
|
+
|
|
1132
|
+
await waitFor(() =>
|
|
1133
|
+
expect(hook.result.current.get("task-one")?.timing).toMatchObject({
|
|
1134
|
+
startedAt: 1_000,
|
|
1135
|
+
completedAt: 6_000,
|
|
1136
|
+
}),
|
|
1137
|
+
);
|
|
1138
|
+
});
|
|
1139
|
+
|
|
1140
|
+
it("keeps finalized nested timing when the namespace resolves after completion", async () => {
|
|
1141
|
+
const placeholderStore = createStore([message("one-ai", "ai", "partial")]);
|
|
1142
|
+
const stream = createStream(
|
|
1143
|
+
new Map([["task-one", subagent("task-one", ["tools:task-one"])]]),
|
|
1144
|
+
new Map([
|
|
1145
|
+
["tools:task-one", placeholderStore],
|
|
1146
|
+
["tools:promoted", createStore([message("one-ai", "ai", "answer")])],
|
|
1147
|
+
]),
|
|
1148
|
+
);
|
|
1149
|
+
const hook = renderHook(() =>
|
|
1150
|
+
useSubagentTranscripts(stream as never, noUIMessages),
|
|
1151
|
+
);
|
|
1152
|
+
|
|
1153
|
+
await waitFor(() =>
|
|
1154
|
+
expect(messagesOf(hook.result.current, "task-one")).toHaveLength(1),
|
|
1155
|
+
);
|
|
1156
|
+
await act(async () => {
|
|
1157
|
+
placeholderStore.setSnapshot([message("one-ai", "ai", "partial answer")]);
|
|
1158
|
+
});
|
|
1159
|
+
|
|
1160
|
+
stream.subagents = new Map([
|
|
1161
|
+
["task-one", subagent("task-one", ["tools:task-one"], "complete")],
|
|
1162
|
+
]);
|
|
1163
|
+
hook.rerender();
|
|
1164
|
+
await waitFor(() =>
|
|
1165
|
+
expect(
|
|
1166
|
+
messagesOf(hook.result.current, "task-one")?.[0]?.metadata?.timing,
|
|
1167
|
+
).toBeDefined(),
|
|
1168
|
+
);
|
|
1169
|
+
|
|
1170
|
+
stream.subagents = new Map([
|
|
1171
|
+
["task-one", subagent("task-one", ["tools:promoted"], "complete")],
|
|
1172
|
+
]);
|
|
1173
|
+
hook.rerender();
|
|
1174
|
+
|
|
1175
|
+
await waitFor(() =>
|
|
1176
|
+
expect(
|
|
1177
|
+
messagesOf(hook.result.current, "task-one")?.[0]?.content,
|
|
1178
|
+
).toMatchObject([{ type: "text", text: "answer" }]),
|
|
1179
|
+
);
|
|
1180
|
+
expect(
|
|
1181
|
+
messagesOf(hook.result.current, "task-one")?.[0]?.metadata?.timing,
|
|
1182
|
+
).toBeDefined();
|
|
1183
|
+
});
|
|
1184
|
+
|
|
1185
|
+
it("keeps counting a streaming message across a namespace rebind", async () => {
|
|
1186
|
+
const placeholderStore = createStore([message("one-ai", "ai", "partial")]);
|
|
1187
|
+
const promotedStore = createStore([
|
|
1188
|
+
message("one-ai", "ai", "partial answer"),
|
|
1189
|
+
]);
|
|
1190
|
+
const stream = createStream(
|
|
1191
|
+
new Map([["task-one", subagent("task-one", ["tools:task-one"])]]),
|
|
1192
|
+
new Map([
|
|
1193
|
+
["tools:task-one", placeholderStore],
|
|
1194
|
+
["tools:promoted", promotedStore],
|
|
1195
|
+
]),
|
|
1196
|
+
);
|
|
1197
|
+
const hook = renderHook(() =>
|
|
1198
|
+
useSubagentTranscripts(stream as never, noUIMessages),
|
|
1199
|
+
);
|
|
1200
|
+
|
|
1201
|
+
await waitFor(() =>
|
|
1202
|
+
expect(messagesOf(hook.result.current, "task-one")).toHaveLength(1),
|
|
1203
|
+
);
|
|
1204
|
+
|
|
1205
|
+
stream.subagents = new Map([
|
|
1206
|
+
["task-one", subagent("task-one", ["tools:promoted"])],
|
|
1207
|
+
]);
|
|
1208
|
+
hook.rerender();
|
|
1209
|
+
await waitFor(() =>
|
|
1210
|
+
expect(
|
|
1211
|
+
messagesOf(hook.result.current, "task-one")?.[0]?.content,
|
|
1212
|
+
).toMatchObject([{ type: "text", text: "partial answer" }]),
|
|
1213
|
+
);
|
|
1214
|
+
|
|
1215
|
+
stream.subagents = new Map([
|
|
1216
|
+
["task-one", subagent("task-one", ["tools:promoted"], "complete")],
|
|
1217
|
+
]);
|
|
1218
|
+
hook.rerender();
|
|
1219
|
+
|
|
1220
|
+
await waitFor(() =>
|
|
1221
|
+
expect(
|
|
1222
|
+
messagesOf(hook.result.current, "task-one")?.[0]?.metadata?.timing,
|
|
1223
|
+
).toMatchObject({ totalChunks: 2 }),
|
|
1224
|
+
);
|
|
1225
|
+
});
|
|
1226
|
+
|
|
1227
|
+
it("finalizes nested timing when one update both promotes and completes", async () => {
|
|
1228
|
+
const placeholderStore = createStore([message("one-ai", "ai", "partial")]);
|
|
1229
|
+
const stream = createStream(
|
|
1230
|
+
new Map([["task-one", subagent("task-one", ["tools:task-one"])]]),
|
|
1231
|
+
new Map([
|
|
1232
|
+
["tools:task-one", placeholderStore],
|
|
1233
|
+
[
|
|
1234
|
+
"tools:promoted",
|
|
1235
|
+
createStore([message("one-ai", "ai", "partial answer")]),
|
|
1236
|
+
],
|
|
1237
|
+
]),
|
|
1238
|
+
);
|
|
1239
|
+
const hook = renderHook(() =>
|
|
1240
|
+
useSubagentTranscripts(stream as never, noUIMessages),
|
|
1241
|
+
);
|
|
1242
|
+
|
|
1243
|
+
await waitFor(() =>
|
|
1244
|
+
expect(messagesOf(hook.result.current, "task-one")).toHaveLength(1),
|
|
1245
|
+
);
|
|
1246
|
+
|
|
1247
|
+
stream.subagents = new Map([
|
|
1248
|
+
["task-one", subagent("task-one", ["tools:promoted"], "complete")],
|
|
1249
|
+
]);
|
|
1250
|
+
hook.rerender();
|
|
1251
|
+
|
|
1252
|
+
await waitFor(() =>
|
|
1253
|
+
expect(
|
|
1254
|
+
messagesOf(hook.result.current, "task-one")?.[0]?.metadata?.timing,
|
|
1255
|
+
).toMatchObject({ totalChunks: 2 }),
|
|
1256
|
+
);
|
|
1257
|
+
});
|
|
515
1258
|
});
|