@assistant-ui/ai-sdk 0.0.7 → 0.0.9
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/LICENSE +1 -1
- package/README.md +1 -1
- package/dist/adapters/aiSDKFormatAdapter.d.ts +2 -2
- package/dist/adapters/aiSDKFormatAdapter.d.ts.map +1 -0
- package/dist/adapters/vercelAttachmentAdapter.d.ts +1 -3
- package/dist/adapters/vercelAttachmentAdapter.d.ts.map +1 -1
- package/dist/adapters/vercelAttachmentAdapter.js +3 -3
- package/dist/adapters/vercelAttachmentAdapter.js.map +1 -1
- package/dist/aiSDKExtras.d.ts +4 -6
- package/dist/aiSDKExtras.d.ts.map +1 -1
- package/dist/converters/convertMessage.d.ts +34 -24
- package/dist/converters/convertMessage.d.ts.map +1 -1
- package/dist/converters/convertMessage.js +56 -12
- package/dist/converters/convertMessage.js.map +1 -1
- package/dist/converters/modelContentEnvelope.d.ts +6 -7
- package/dist/converters/modelContentEnvelope.d.ts.map +1 -1
- package/dist/converters/toCreateMessage.d.ts +2 -4
- package/dist/converters/toCreateMessage.d.ts.map +1 -1
- package/dist/converters/toolOutputConversion.d.ts +20 -22
- package/dist/converters/toolOutputConversion.d.ts.map +1 -1
- package/dist/hooks.d.ts +2 -4
- package/dist/hooks.d.ts.map +1 -1
- package/dist/index.d.ts +17 -14
- package/dist/index.d.ts.map +1 -0
- package/dist/index.native.d.ts +15 -12
- package/dist/index.native.d.ts.map +1 -0
- package/dist/model-context/injectInteractableContext.d.ts +2 -4
- package/dist/model-context/injectInteractableContext.d.ts.map +1 -1
- package/dist/model-context/injectQuoteContext.d.ts +1 -3
- package/dist/model-context/injectQuoteContext.d.ts.map +1 -1
- package/dist/runtime/AISDKChat.d.ts +2 -4
- package/dist/runtime/AISDKChat.d.ts.map +1 -1
- package/dist/runtime/AISDKChat.js +1 -1
- package/dist/runtime/AISDKChat.js.map +1 -1
- package/dist/runtime/AISDKThreads.d.ts +29 -31
- package/dist/runtime/AISDKThreads.d.ts.map +1 -1
- package/dist/runtime/AISDKThreads.js +22 -23
- package/dist/runtime/AISDKThreads.js.map +1 -1
- package/dist/runtime/sdkIdentity.d.ts +1 -3
- package/dist/runtime/sdkIdentity.d.ts.map +1 -1
- package/dist/runtime/sdkIdentity.js +1 -1
- package/dist/runtime/useAISDKRuntime.d.ts +76 -70
- package/dist/runtime/useAISDKRuntime.d.ts.map +1 -1
- package/dist/runtime/useAISDKRuntime.js +311 -77
- package/dist/runtime/useAISDKRuntime.js.map +1 -1
- package/dist/runtime/useChatRuntime.d.ts +6 -8
- package/dist/runtime/useChatRuntime.d.ts.map +1 -1
- package/dist/runtime/useChatRuntime.js +14 -10
- package/dist/runtime/useChatRuntime.js.map +1 -1
- package/dist/runtime/useChatThread.d.ts +94 -89
- package/dist/runtime/useChatThread.d.ts.map +1 -1
- package/dist/runtime/useChatThread.js +44 -15
- package/dist/runtime/useChatThread.js.map +1 -1
- package/dist/runtime/useExternalHistory.d.ts +7 -7
- package/dist/runtime/useExternalHistory.d.ts.map +1 -1
- package/dist/runtime/useExternalHistory.js +231 -60
- package/dist/runtime/useExternalHistory.js.map +1 -1
- package/dist/runtime/useResourceCleanup.d.ts +1 -3
- package/dist/runtime/useResourceCleanup.d.ts.map +1 -1
- package/dist/runtime/useResourceCleanup.js +38 -14
- package/dist/runtime/useResourceCleanup.js.map +1 -1
- package/dist/runtime/useStreamingTiming.d.ts +3 -5
- package/dist/runtime/useStreamingTiming.d.ts.map +1 -1
- package/dist/runtime/useStreamingTiming.js +1 -1
- package/dist/tools/frontendTools.d.ts +21 -23
- package/dist/tools/frontendTools.d.ts.map +1 -1
- package/dist/tools/generativeTools.d.ts +24 -26
- package/dist/tools/generativeTools.d.ts.map +1 -1
- package/dist/tools/mcp-stdio.node.d.ts +2 -2
- package/dist/tools/mcp-stdio.node.d.ts.map +1 -0
- package/dist/tools/mcp-stdio.unsupported.d.ts +2 -4
- package/dist/tools/mcp-stdio.unsupported.d.ts.map +1 -1
- package/dist/transport/AssistantChatTransport.d.ts +14 -16
- package/dist/transport/AssistantChatTransport.d.ts.map +1 -1
- package/dist/transport/AssistantChatTransport.js +9 -2
- package/dist/transport/AssistantChatTransport.js.map +1 -1
- package/dist/transport/resumable.d.ts +25 -27
- package/dist/transport/resumable.d.ts.map +1 -1
- package/dist/usage.d.ts +8 -10
- package/dist/usage.d.ts.map +1 -1
- package/dist/usage.js.map +1 -1
- package/dist/utils/getVercelAIMessages.d.ts +2 -4
- package/dist/utils/getVercelAIMessages.d.ts.map +1 -1
- package/dist/utils/sliceMessagesUntil.d.ts +1 -3
- package/dist/utils/sliceMessagesUntil.d.ts.map +1 -1
- package/package.json +13 -9
- package/src/adapters/vercelAttachmentAdapter.test.ts +23 -0
- package/src/adapters/vercelAttachmentAdapter.ts +2 -2
- package/src/converters/convertMessage.test.ts +541 -3
- package/src/converters/convertMessage.ts +123 -15
- package/src/index.native.ts +1 -1
- package/src/index.ts +1 -1
- package/src/runtime/AISDKChat.ts +0 -4
- package/src/runtime/AISDKThreads.cloud.test.ts +60 -2
- package/src/runtime/AISDKThreads.test.ts +105 -46
- package/src/runtime/AISDKThreads.ts +1 -1
- package/src/runtime/useAISDKRuntime.approval.integration.test.tsx +1639 -19
- package/src/runtime/useAISDKRuntime.approval.test.tsx +252 -9
- package/src/runtime/useAISDKRuntime.fast-refresh.test.tsx +182 -0
- package/src/runtime/useAISDKRuntime.test.ts +275 -5
- package/src/runtime/useAISDKRuntime.ts +533 -60
- package/src/runtime/useAISDKRuntime.voice.test.tsx +50 -1
- package/src/runtime/useChatRuntime.fast-refresh.test.tsx +94 -0
- package/src/runtime/useChatRuntime.integration.test.tsx +143 -65
- package/src/runtime/useChatRuntime.test.ts +11 -7
- package/src/runtime/useChatRuntime.ts +6 -2
- package/src/runtime/useChatThread.test.ts +166 -2
- package/src/runtime/useChatThread.transport.test.tsx +152 -0
- package/src/runtime/useChatThread.ts +89 -22
- package/src/runtime/useExternalHistory.test.ts +943 -3
- package/src/runtime/useExternalHistory.ts +378 -15
- package/src/runtime/useResourceCleanup.ts +41 -15
- package/src/transport/AssistantChatTransport.test.ts +164 -0
- package/src/transport/AssistantChatTransport.ts +22 -2
- package/src/usage.ts +1 -1
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
type McpAppMetadata,
|
|
25
25
|
type MessagePartStreamStatus,
|
|
26
26
|
type RespondToToolApprovalOptions,
|
|
27
|
+
type Unstable_ToolInteractionLog,
|
|
27
28
|
} from "@assistant-ui/core";
|
|
28
29
|
import { stableStringifyToolArgs } from "@assistant-ui/core/internal";
|
|
29
30
|
import {
|
|
@@ -49,7 +50,7 @@ const THREAD_METADATA_KEYS = new Set([
|
|
|
49
50
|
const toThreadMetadata = (metadata: unknown): MessageMetadata => {
|
|
50
51
|
if (!metadata || typeof metadata !== "object") return undefined;
|
|
51
52
|
const result: Record<string, unknown> = {};
|
|
52
|
-
const extra
|
|
53
|
+
const extra = Object.create(null) as Record<string, unknown>;
|
|
53
54
|
for (const [key, value] of Object.entries(metadata)) {
|
|
54
55
|
(THREAD_METADATA_KEYS.has(key) ? result : extra)[key] = value;
|
|
55
56
|
}
|
|
@@ -61,16 +62,30 @@ const toThreadMetadata = (metadata: unknown): MessageMetadata => {
|
|
|
61
62
|
export type AISDKMessageConverterMetadata =
|
|
62
63
|
useExternalMessageConverter.Metadata & {
|
|
63
64
|
toolArgsKeyOrderCache?: Map<string, Map<string, string[]>>;
|
|
65
|
+
/**
|
|
66
|
+
* Frozen `argsText` keyed weakly by a settled tool call's input object, then
|
|
67
|
+
* by call, since the text carries the call's streamed key order. A known
|
|
68
|
+
* call/input pair skips serialization; the entries become collectible once
|
|
69
|
+
* the input is unreachable. A fresh input object re-serializes in its own
|
|
70
|
+
* deterministic key order.
|
|
71
|
+
*/
|
|
72
|
+
toolArgsTextCache?: WeakMap<ReadonlyJSONObject, Map<string, string>>;
|
|
64
73
|
toolLastInputCache?: Map<string, ReadonlyJSONObject>;
|
|
65
74
|
mcpAppMetadataCache?: Map<string, McpAppMetadata>;
|
|
75
|
+
toolArtifacts?: ReadonlyMap<string, unknown>;
|
|
76
|
+
toolInteractions?: ReadonlyMap<string, Unstable_ToolInteractionLog>;
|
|
66
77
|
supportsRichToolApprovalResponses?: boolean;
|
|
67
78
|
toolApprovalResponses?: ReadonlyMap<string, RespondToToolApprovalOptions>;
|
|
79
|
+
cancelledToolApprovalIds?: ReadonlySet<string>;
|
|
80
|
+
cancelledStatusMessageIds?: ReadonlySet<string>;
|
|
68
81
|
/** Id of the currently-streaming message, flagged optimistic (#4037). */
|
|
69
82
|
optimisticMessageId?: string | undefined;
|
|
70
83
|
};
|
|
71
84
|
|
|
72
85
|
function stripClosingDelimiters(json: string): string {
|
|
73
|
-
|
|
86
|
+
let end = json.length;
|
|
87
|
+
while (end > 0 && '}]"'.includes(json[end - 1]!)) end--;
|
|
88
|
+
return json.slice(0, end);
|
|
74
89
|
}
|
|
75
90
|
|
|
76
91
|
const MCP_APP_METADATA_CACHE_MAX = 100;
|
|
@@ -206,6 +221,38 @@ const normalizeToolApprovalOptions = (
|
|
|
206
221
|
});
|
|
207
222
|
};
|
|
208
223
|
|
|
224
|
+
const APPROVAL_DESCRIPTOR_FIELDS = [
|
|
225
|
+
"prompt",
|
|
226
|
+
"display",
|
|
227
|
+
"allowFreeform",
|
|
228
|
+
"dismissible",
|
|
229
|
+
"options",
|
|
230
|
+
"optionId",
|
|
231
|
+
"text",
|
|
232
|
+
"resolution",
|
|
233
|
+
] as const;
|
|
234
|
+
|
|
235
|
+
// The AI SDK's approval object declares none of the core request and answer
|
|
236
|
+
// fields and `validateUIMessages` strips unknown ones, so a host streams or
|
|
237
|
+
// persists them inside the opaque `approvalDescriptor`. Only those fields are
|
|
238
|
+
// read from it: a descriptor cannot approve its own request.
|
|
239
|
+
const readApprovalDescriptor = (
|
|
240
|
+
descriptor: unknown,
|
|
241
|
+
): Record<string, unknown> => {
|
|
242
|
+
if (
|
|
243
|
+
!descriptor ||
|
|
244
|
+
typeof descriptor !== "object" ||
|
|
245
|
+
Array.isArray(descriptor)
|
|
246
|
+
)
|
|
247
|
+
return {};
|
|
248
|
+
const fields: Record<string, unknown> = {};
|
|
249
|
+
for (const key of APPROVAL_DESCRIPTOR_FIELDS) {
|
|
250
|
+
if (Object.hasOwn(descriptor, key))
|
|
251
|
+
fields[key] = (descriptor as Record<string, unknown>)[key];
|
|
252
|
+
}
|
|
253
|
+
return fields;
|
|
254
|
+
};
|
|
255
|
+
|
|
209
256
|
function getToolApprovalAndInterrupt(
|
|
210
257
|
part: {
|
|
211
258
|
approval?: Record<string, unknown> | undefined;
|
|
@@ -215,17 +262,29 @@ function getToolApprovalAndInterrupt(
|
|
|
215
262
|
toolApprovalResponses:
|
|
216
263
|
| ReadonlyMap<string, RespondToToolApprovalOptions>
|
|
217
264
|
| undefined,
|
|
265
|
+
cancelledToolApprovalIds: ReadonlySet<string> | undefined,
|
|
218
266
|
): {
|
|
219
267
|
approval?: NonNullable<ToolCallMessagePart["approval"]>;
|
|
220
268
|
interrupt?: NonNullable<ToolCallMessagePart["interrupt"]>;
|
|
221
269
|
} {
|
|
222
270
|
if (part.approval) {
|
|
271
|
+
const rawApproval = {
|
|
272
|
+
...readApprovalDescriptor(part.approval.descriptor),
|
|
273
|
+
...part.approval,
|
|
274
|
+
};
|
|
275
|
+
const approval =
|
|
276
|
+
typeof rawApproval.id === "string" &&
|
|
277
|
+
rawApproval.approved === undefined &&
|
|
278
|
+
rawApproval.resolution === undefined &&
|
|
279
|
+
cancelledToolApprovalIds?.has(rawApproval.id)
|
|
280
|
+
? { ...rawApproval, resolution: "cancelled" as const }
|
|
281
|
+
: rawApproval;
|
|
223
282
|
const response =
|
|
224
|
-
typeof
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
? toolApprovalResponses?.get(
|
|
283
|
+
typeof approval.id === "string" &&
|
|
284
|
+
approval.approved === undefined &&
|
|
285
|
+
approval.resolution !== "cancelled" &&
|
|
286
|
+
approval.resolution !== "expired"
|
|
287
|
+
? toolApprovalResponses?.get(approval.id)
|
|
229
288
|
: undefined;
|
|
230
289
|
// The built-in AI SDK channel sends only id, approved and reason back to
|
|
231
290
|
// the server, so a request shape promising any other answer would render
|
|
@@ -239,19 +298,20 @@ function getToolApprovalAndInterrupt(
|
|
|
239
298
|
resolution,
|
|
240
299
|
display,
|
|
241
300
|
allowFreeform,
|
|
301
|
+
dismissible,
|
|
242
302
|
options,
|
|
243
303
|
optionId,
|
|
244
304
|
text,
|
|
245
305
|
...additionalApprovalFields
|
|
246
306
|
} = response
|
|
247
307
|
? {
|
|
248
|
-
...
|
|
308
|
+
...approval,
|
|
249
309
|
approved: response.approved,
|
|
250
310
|
...(response.reason != null && { reason: response.reason }),
|
|
251
311
|
...(response.optionId != null && { optionId: response.optionId }),
|
|
252
312
|
...(response.text != null && { text: response.text }),
|
|
253
313
|
}
|
|
254
|
-
:
|
|
314
|
+
: approval;
|
|
255
315
|
const normalizedOptions = supportsRichToolApprovalResponses
|
|
256
316
|
? normalizeToolApprovalOptions(options)
|
|
257
317
|
: undefined;
|
|
@@ -274,6 +334,7 @@ function getToolApprovalAndInterrupt(
|
|
|
274
334
|
display === "select" ||
|
|
275
335
|
display === "text") && { display }),
|
|
276
336
|
...(typeof allowFreeform === "boolean" && { allowFreeform }),
|
|
337
|
+
...(typeof dismissible === "boolean" && { dismissible }),
|
|
277
338
|
...(normalizedOptions && { options: normalizedOptions }),
|
|
278
339
|
...(typeof optionId === "string" && { optionId }),
|
|
279
340
|
...(typeof text === "string" && { text }),
|
|
@@ -354,7 +415,14 @@ function convertParts(
|
|
|
354
415
|
const toolCallId = part.toolCallId;
|
|
355
416
|
const argsKeyOrderCacheKey = `${message.id}:${toolCallId}`;
|
|
356
417
|
|
|
357
|
-
|
|
418
|
+
// A tool call that streamed complete arguments then failed schema
|
|
419
|
+
// validation keeps them in `rawInput`, not `input`; reading `input`
|
|
420
|
+
// alone would convert the error snapshot to `{}` and hide the input.
|
|
421
|
+
const rawInput = (part.input ??
|
|
422
|
+
("rawInput" in part ? part.rawInput : undefined)) as
|
|
423
|
+
| ReadonlyJSONObject
|
|
424
|
+
| null
|
|
425
|
+
| undefined;
|
|
358
426
|
let args: ReadonlyJSONObject;
|
|
359
427
|
if (
|
|
360
428
|
rawInput != null &&
|
|
@@ -387,12 +455,13 @@ function convertParts(
|
|
|
387
455
|
};
|
|
388
456
|
}
|
|
389
457
|
|
|
390
|
-
let argsText
|
|
391
|
-
metadata.toolArgsKeyOrderCache,
|
|
392
|
-
argsKeyOrderCacheKey,
|
|
393
|
-
args,
|
|
394
|
-
);
|
|
458
|
+
let argsText: string;
|
|
395
459
|
if (part.state === "input-streaming") {
|
|
460
|
+
argsText = stableStringifyToolArgs(
|
|
461
|
+
metadata.toolArgsKeyOrderCache,
|
|
462
|
+
argsKeyOrderCacheKey,
|
|
463
|
+
args,
|
|
464
|
+
);
|
|
396
465
|
// strip closing delimiters added by the AI SDK's fix-json
|
|
397
466
|
argsText = stripClosingDelimiters(argsText);
|
|
398
467
|
// Re-parse so args carries the partial-JSON meta that marks which
|
|
@@ -401,6 +470,27 @@ function convertParts(
|
|
|
401
470
|
// of the stripped text is the streaming frontier.
|
|
402
471
|
args = parsePartialJsonObject(argsText) ?? args;
|
|
403
472
|
} else {
|
|
473
|
+
// A settled part is re-converted whenever its message or the converter
|
|
474
|
+
// metadata changes; the text frozen on its input object skips
|
|
475
|
+
// re-serializing large args while the call keeps that input. Arrival
|
|
476
|
+
// order only matters while args stream, so the key-order entry is
|
|
477
|
+
// released.
|
|
478
|
+
const frozen =
|
|
479
|
+
metadata.toolArgsTextCache?.get(args) ?? new Map<string, string>();
|
|
480
|
+
const frozenText = frozen.get(argsKeyOrderCacheKey);
|
|
481
|
+
if (frozenText !== undefined) {
|
|
482
|
+
argsText = frozenText;
|
|
483
|
+
} else {
|
|
484
|
+
argsText = stableStringifyToolArgs(
|
|
485
|
+
metadata.toolArgsKeyOrderCache,
|
|
486
|
+
argsKeyOrderCacheKey,
|
|
487
|
+
args,
|
|
488
|
+
);
|
|
489
|
+
metadata.toolArgsTextCache?.set(
|
|
490
|
+
args,
|
|
491
|
+
frozen.set(argsKeyOrderCacheKey, argsText),
|
|
492
|
+
);
|
|
493
|
+
}
|
|
404
494
|
metadata.toolArgsKeyOrderCache?.delete(argsKeyOrderCacheKey);
|
|
405
495
|
if (
|
|
406
496
|
part.state === "output-available" ||
|
|
@@ -416,6 +506,8 @@ function convertParts(
|
|
|
416
506
|
part,
|
|
417
507
|
metadata.mcpAppMetadataCache,
|
|
418
508
|
);
|
|
509
|
+
const artifact = metadata.toolArtifacts?.get(toolCallId);
|
|
510
|
+
const interactions = metadata.toolInteractions?.get(toolCallId);
|
|
419
511
|
return {
|
|
420
512
|
type: "tool-call",
|
|
421
513
|
toolName,
|
|
@@ -424,6 +516,12 @@ function convertParts(
|
|
|
424
516
|
args,
|
|
425
517
|
result,
|
|
426
518
|
isError,
|
|
519
|
+
...(artifact !== undefined && { artifact }),
|
|
520
|
+
...(interactions !== undefined && {
|
|
521
|
+
unstable_interactions: interactions,
|
|
522
|
+
}),
|
|
523
|
+
...(part.state === "output-available" &&
|
|
524
|
+
part.preliminary === true && { isPreliminary: true }),
|
|
427
525
|
...(modelContent !== undefined && { modelContent }),
|
|
428
526
|
...(mcpApp && { mcp: { app: mcpApp } }),
|
|
429
527
|
...(part.callProviderMetadata != null
|
|
@@ -437,6 +535,7 @@ function convertParts(
|
|
|
437
535
|
toolStatus,
|
|
438
536
|
metadata.supportsRichToolApprovalResponses === true,
|
|
439
537
|
metadata.toolApprovalResponses,
|
|
538
|
+
metadata.cancelledToolApprovalIds,
|
|
440
539
|
),
|
|
441
540
|
} satisfies ToolCallMessagePart;
|
|
442
541
|
}
|
|
@@ -575,6 +674,15 @@ export const AISDKMessageConverter = unstable_createMessageConverter(
|
|
|
575
674
|
id: message.id,
|
|
576
675
|
createdAt,
|
|
577
676
|
content,
|
|
677
|
+
...(message.role === "assistant" &&
|
|
678
|
+
metadata.cancelledStatusMessageIds?.has(message.id)
|
|
679
|
+
? {
|
|
680
|
+
status: {
|
|
681
|
+
type: "incomplete" as const,
|
|
682
|
+
reason: "cancelled" as const,
|
|
683
|
+
},
|
|
684
|
+
}
|
|
685
|
+
: undefined),
|
|
578
686
|
metadata: {
|
|
579
687
|
...toThreadMetadata(message.metadata),
|
|
580
688
|
...(timing && { timing }),
|
package/src/index.native.ts
CHANGED
package/src/index.ts
CHANGED
package/src/runtime/AISDKChat.ts
CHANGED
|
@@ -18,10 +18,6 @@ const useAISDKChat = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
|
18
18
|
options?: AISDKChatOptions<UI_MESSAGE>,
|
|
19
19
|
) => {
|
|
20
20
|
const [id] = useState(() => options?.id ?? generateId());
|
|
21
|
-
// The transport resolves the request id from the thread list item, falling
|
|
22
|
-
// back to the runtime's main item, whose id here is the external store's
|
|
23
|
-
// placeholder constant. The single thread of this entry is the chat itself,
|
|
24
|
-
// so the handed-over item initializes to the chat id.
|
|
25
21
|
const [threadListItem] = useState(() => ({
|
|
26
22
|
initialize: async () => ({ remoteId: id, externalId: undefined }),
|
|
27
23
|
}));
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
2
|
|
|
3
|
+
import { getEventListeners } from "node:events";
|
|
3
4
|
import { describe, expect, it, vi } from "vitest";
|
|
4
|
-
import { flushTapSync } from "@assistant-ui/tap";
|
|
5
|
-
import {
|
|
5
|
+
import { flushTapSync, resource, useResource } from "@assistant-ui/tap";
|
|
6
|
+
import {
|
|
7
|
+
attachTransformScopes,
|
|
8
|
+
AuiConfig,
|
|
9
|
+
createAssistantClient,
|
|
10
|
+
} from "@assistant-ui/store/client";
|
|
11
|
+
import { useAssistantClientDestroySignal } from "@assistant-ui/store/internal";
|
|
12
|
+
import { inMemoryThreadListTransformScopes } from "@assistant-ui/core/store";
|
|
6
13
|
import type { AssistantCloud } from "assistant-cloud";
|
|
7
14
|
import type { RemoteThreadListAdapter } from "@assistant-ui/core";
|
|
8
15
|
import type { ThreadHistoryAdapter } from "@assistant-ui/core";
|
|
@@ -152,4 +159,55 @@ describe("AISDKThreads cloud", () => {
|
|
|
152
159
|
handle.destroy();
|
|
153
160
|
}
|
|
154
161
|
});
|
|
162
|
+
|
|
163
|
+
it("keeps cloud threads off the client destroy signal and stops them on destroy", async () => {
|
|
164
|
+
const chat = createCancellableTransport();
|
|
165
|
+
let destroySignal: AbortSignal | undefined;
|
|
166
|
+
function useThreads() {
|
|
167
|
+
destroySignal = useAssistantClientDestroySignal();
|
|
168
|
+
return useResource(
|
|
169
|
+
AISDKThreads({
|
|
170
|
+
cloud: {} as AssistantCloud,
|
|
171
|
+
threadId: "t1",
|
|
172
|
+
transport: () => chat.transport,
|
|
173
|
+
}),
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
attachTransformScopes(useThreads, inMemoryThreadListTransformScopes);
|
|
177
|
+
const handle = createAssistantClient(
|
|
178
|
+
AuiConfig({ threads: resource(useThreads)() }),
|
|
179
|
+
);
|
|
180
|
+
handle.subscribe(() => {});
|
|
181
|
+
const listeners = () => getEventListeners(destroySignal!, "abort").length;
|
|
182
|
+
try {
|
|
183
|
+
await handle.getClient().threads.getLoadThreadsPromise();
|
|
184
|
+
await vi.waitFor(() => {
|
|
185
|
+
expect(handle.getClient().threads.getState().mainThreadId).toBe("t1");
|
|
186
|
+
});
|
|
187
|
+
await vi.waitFor(() => {
|
|
188
|
+
expect(handle.getClient().thread.getState().isLoading).toBe(false);
|
|
189
|
+
});
|
|
190
|
+
const withFirstThread = listeners();
|
|
191
|
+
|
|
192
|
+
flushTapSync(() => handle.getClient().threads.switchToThread("t2"));
|
|
193
|
+
await vi.waitFor(() => {
|
|
194
|
+
expect(handle.getClient().threads.getState().mainThreadId).toBe("t2");
|
|
195
|
+
});
|
|
196
|
+
await vi.waitFor(() => {
|
|
197
|
+
expect(handle.getClient().thread.getState().isLoading).toBe(false);
|
|
198
|
+
});
|
|
199
|
+
expect(listeners()).toBe(withFirstThread);
|
|
200
|
+
|
|
201
|
+
flushTapSync(() => handle.getClient().composer.setText("stream me"));
|
|
202
|
+
flushTapSync(() => handle.getClient().composer.send());
|
|
203
|
+
await vi.waitFor(() => {
|
|
204
|
+
expect(handle.getClient().thread.getState().isRunning).toBe(true);
|
|
205
|
+
});
|
|
206
|
+
} finally {
|
|
207
|
+
handle.destroy();
|
|
208
|
+
}
|
|
209
|
+
await vi.waitFor(() => {
|
|
210
|
+
expect(chat.getCancelCount()).toBe(1);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
155
213
|
});
|
|
@@ -28,7 +28,60 @@ const threadText = (aui: ReturnType<typeof createAssistantClient>) =>
|
|
|
28
28
|
m.content.map((part) => (part.type === "text" ? part.text : "")).join(""),
|
|
29
29
|
);
|
|
30
30
|
|
|
31
|
+
const createLiveHandle = (
|
|
32
|
+
getOptions: () => Parameters<typeof AISDKThreads>[0],
|
|
33
|
+
) => {
|
|
34
|
+
const listeners = new Set<() => void>();
|
|
35
|
+
const handle = createAssistantClient({
|
|
36
|
+
getConfig: () => AuiConfig({ threads: AISDKThreads(getOptions()) }),
|
|
37
|
+
subscribe: (listener) => {
|
|
38
|
+
listeners.add(listener);
|
|
39
|
+
return () => listeners.delete(listener);
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
handle.subscribe(() => {});
|
|
43
|
+
return {
|
|
44
|
+
handle,
|
|
45
|
+
rerender: () =>
|
|
46
|
+
flushTapSync(() => listeners.forEach((listener) => listener())),
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const chatCallbacks = () => ({
|
|
51
|
+
onToolCall: vi.fn(),
|
|
52
|
+
onData: vi.fn(),
|
|
53
|
+
onFinish: vi.fn(),
|
|
54
|
+
onError: vi.fn(),
|
|
55
|
+
sendAutomaticallyWhen: vi.fn(() => false),
|
|
56
|
+
});
|
|
57
|
+
|
|
31
58
|
describe("AISDKThreads", () => {
|
|
59
|
+
it("emits the selected thread id when its composer sends", async () => {
|
|
60
|
+
const { transport } = createControlledTransport();
|
|
61
|
+
const handle = createAssistantClient(
|
|
62
|
+
AuiConfig({ threads: AISDKThreads({ transport }) }),
|
|
63
|
+
);
|
|
64
|
+
try {
|
|
65
|
+
handle.subscribe(() => {});
|
|
66
|
+
const aui = handle.getClient();
|
|
67
|
+
const sent = vi.fn();
|
|
68
|
+
aui.on({ scope: "thread", event: "composer.send" }, sent);
|
|
69
|
+
|
|
70
|
+
flushTapSync(() => aui.composer.setText("hello"));
|
|
71
|
+
flushTapSync(() => aui.composer.send());
|
|
72
|
+
|
|
73
|
+
await vi.waitFor(() => {
|
|
74
|
+
expect(sent).toHaveBeenCalledWith(
|
|
75
|
+
expect.objectContaining({
|
|
76
|
+
threadId: aui.threads.getState().mainThreadId,
|
|
77
|
+
}),
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
} finally {
|
|
81
|
+
handle.destroy();
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
32
85
|
it("runs one chat per thread and keeps histories isolated across switches", async () => {
|
|
33
86
|
const { transport, emit, close } = createControlledTransport();
|
|
34
87
|
const handle = createAssistantClient(
|
|
@@ -253,25 +306,19 @@ describe("AISDKThreads", () => {
|
|
|
253
306
|
|
|
254
307
|
it("forwards ChatInit callbacks to each thread's chat from the latest render", async () => {
|
|
255
308
|
const { transport, emit, close } = createControlledTransport();
|
|
256
|
-
const
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
subscribe: (listener) => {
|
|
266
|
-
listeners.add(listener);
|
|
267
|
-
return () => listeners.delete(listener);
|
|
268
|
-
},
|
|
269
|
-
});
|
|
270
|
-
handle.subscribe(() => {});
|
|
309
|
+
const sendMessages = vi.spyOn(transport, "sendMessages");
|
|
310
|
+
const mounted = chatCallbacks();
|
|
311
|
+
const latest = chatCallbacks();
|
|
312
|
+
latest.sendAutomaticallyWhen.mockReturnValueOnce(true);
|
|
313
|
+
let callbacks = mounted;
|
|
314
|
+
const { handle, rerender } = createLiveHandle(() => ({
|
|
315
|
+
transport: () => transport,
|
|
316
|
+
...callbacks,
|
|
317
|
+
}));
|
|
271
318
|
const aui = handle.getClient();
|
|
272
319
|
|
|
273
|
-
|
|
274
|
-
|
|
320
|
+
callbacks = latest;
|
|
321
|
+
rerender();
|
|
275
322
|
|
|
276
323
|
flushTapSync(() => aui.composer.setText("hi"));
|
|
277
324
|
flushTapSync(() => aui.composer.send());
|
|
@@ -280,10 +327,36 @@ describe("AISDKThreads", () => {
|
|
|
280
327
|
handle.getClient().thread.getState().messages.length,
|
|
281
328
|
).toBeGreaterThan(0);
|
|
282
329
|
});
|
|
283
|
-
emit(
|
|
330
|
+
emit(
|
|
331
|
+
{ type: "start" },
|
|
332
|
+
{
|
|
333
|
+
type: "tool-input-available",
|
|
334
|
+
toolCallId: "call-1",
|
|
335
|
+
toolName: "lookup",
|
|
336
|
+
input: {},
|
|
337
|
+
},
|
|
338
|
+
{ type: "data-note", data: "note" },
|
|
339
|
+
{ type: "finish" },
|
|
340
|
+
);
|
|
284
341
|
close();
|
|
285
|
-
await vi.waitFor(() => expect(
|
|
286
|
-
|
|
342
|
+
await vi.waitFor(() => expect(sendMessages).toHaveBeenCalledTimes(2));
|
|
343
|
+
emit({ type: "start" }, { type: "error", errorText: "boom" });
|
|
344
|
+
close();
|
|
345
|
+
await vi.waitFor(() => expect(latest.onFinish).toHaveBeenCalledTimes(2));
|
|
346
|
+
|
|
347
|
+
expect(latest.onToolCall).toHaveBeenCalledExactlyOnceWith({
|
|
348
|
+
toolCall: expect.objectContaining({ toolCallId: "call-1" }),
|
|
349
|
+
});
|
|
350
|
+
expect(latest.onData).toHaveBeenCalledExactlyOnceWith(
|
|
351
|
+
expect.objectContaining({ type: "data-note", data: "note" }),
|
|
352
|
+
);
|
|
353
|
+
expect(latest.onError).toHaveBeenCalledExactlyOnceWith(
|
|
354
|
+
expect.objectContaining({ message: "boom" }),
|
|
355
|
+
);
|
|
356
|
+
expect(latest.sendAutomaticallyWhen).toHaveBeenCalledOnce();
|
|
357
|
+
for (const callback of Object.values(mounted)) {
|
|
358
|
+
expect(callback).not.toHaveBeenCalled();
|
|
359
|
+
}
|
|
287
360
|
|
|
288
361
|
handle.destroy();
|
|
289
362
|
});
|
|
@@ -293,18 +366,10 @@ describe("AISDKThreads", () => {
|
|
|
293
366
|
const onFinishA = vi.fn();
|
|
294
367
|
const onFinishB = vi.fn();
|
|
295
368
|
let onFinish = onFinishA;
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
threads: AISDKThreads({ transport: () => transport, onFinish }),
|
|
301
|
-
}),
|
|
302
|
-
subscribe: (listener) => {
|
|
303
|
-
listeners.add(listener);
|
|
304
|
-
return () => listeners.delete(listener);
|
|
305
|
-
},
|
|
306
|
-
});
|
|
307
|
-
handle.subscribe(() => {});
|
|
369
|
+
const { handle, rerender } = createLiveHandle(() => ({
|
|
370
|
+
transport: () => transport,
|
|
371
|
+
onFinish,
|
|
372
|
+
}));
|
|
308
373
|
const aui = handle.getClient();
|
|
309
374
|
|
|
310
375
|
flushTapSync(() => aui.composer.setText("stream me"));
|
|
@@ -322,7 +387,7 @@ describe("AISDKThreads", () => {
|
|
|
322
387
|
|
|
323
388
|
flushTapSync(() => aui.threads.switchToNewThread());
|
|
324
389
|
onFinish = onFinishB;
|
|
325
|
-
|
|
390
|
+
rerender();
|
|
326
391
|
|
|
327
392
|
emit({ type: "text-end", id: "t1" }, { type: "finish" });
|
|
328
393
|
close();
|
|
@@ -361,18 +426,12 @@ describe("AISDKThreads", () => {
|
|
|
361
426
|
const onFinishA = vi.fn();
|
|
362
427
|
const onFinishB = vi.fn();
|
|
363
428
|
let onFinish = onFinishA;
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
subscribe: (listener) => {
|
|
371
|
-
listeners.add(listener);
|
|
372
|
-
return () => listeners.delete(listener);
|
|
373
|
-
},
|
|
374
|
-
});
|
|
375
|
-
handle.subscribe(() => {});
|
|
429
|
+
const { handle, rerender } = createLiveHandle(() => ({
|
|
430
|
+
cloud,
|
|
431
|
+
threadId: "t1",
|
|
432
|
+
transport,
|
|
433
|
+
onFinish,
|
|
434
|
+
}));
|
|
376
435
|
try {
|
|
377
436
|
await handle.getClient().threads.getLoadThreadsPromise();
|
|
378
437
|
await vi.waitFor(() => {
|
|
@@ -383,7 +442,7 @@ describe("AISDKThreads", () => {
|
|
|
383
442
|
});
|
|
384
443
|
|
|
385
444
|
onFinish = onFinishB;
|
|
386
|
-
|
|
445
|
+
rerender();
|
|
387
446
|
|
|
388
447
|
flushTapSync(() => handle.getClient().composer.setText("hi"));
|
|
389
448
|
flushTapSync(() => handle.getClient().composer.send());
|