@assistant-ui/ai-sdk 0.0.7 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/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 +32 -24
- package/dist/converters/convertMessage.d.ts.map +1 -1
- package/dist/converters/convertMessage.js +43 -9
- 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/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 +69 -70
- package/dist/runtime/useAISDKRuntime.d.ts.map +1 -1
- package/dist/runtime/useAISDKRuntime.js +112 -56
- 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 +4 -5
- 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 +229 -59
- 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/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 +9 -8
- package/src/adapters/vercelAttachmentAdapter.test.ts +23 -0
- package/src/adapters/vercelAttachmentAdapter.ts +2 -2
- package/src/converters/convertMessage.test.ts +478 -3
- package/src/converters/convertMessage.ts +100 -14
- package/src/index.native.ts +1 -1
- package/src/index.ts +1 -1
- package/src/runtime/AISDKThreads.cloud.test.ts +60 -2
- package/src/runtime/AISDKThreads.test.ts +79 -46
- package/src/runtime/AISDKThreads.ts +1 -1
- package/src/runtime/useAISDKRuntime.approval.integration.test.tsx +51 -0
- package/src/runtime/useAISDKRuntime.approval.test.tsx +225 -9
- package/src/runtime/useAISDKRuntime.test.ts +275 -5
- package/src/runtime/useAISDKRuntime.ts +141 -37
- package/src/runtime/useAISDKRuntime.voice.test.tsx +50 -1
- package/src/runtime/useChatRuntime.integration.test.tsx +56 -0
- package/src/runtime/useChatRuntime.test.ts +5 -1
- package/src/runtime/useChatRuntime.ts +6 -2
- package/src/runtime/useChatThread.transport.test.tsx +149 -0
- package/src/runtime/useChatThread.ts +33 -5
- package/src/runtime/useExternalHistory.test.ts +943 -3
- package/src/runtime/useExternalHistory.ts +366 -14
- package/src/runtime/useResourceCleanup.ts +41 -15
- 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,8 +62,18 @@ 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>;
|
|
68
79
|
/** Id of the currently-streaming message, flagged optimistic (#4037). */
|
|
@@ -206,6 +217,38 @@ const normalizeToolApprovalOptions = (
|
|
|
206
217
|
});
|
|
207
218
|
};
|
|
208
219
|
|
|
220
|
+
const APPROVAL_DESCRIPTOR_FIELDS = [
|
|
221
|
+
"prompt",
|
|
222
|
+
"display",
|
|
223
|
+
"allowFreeform",
|
|
224
|
+
"dismissible",
|
|
225
|
+
"options",
|
|
226
|
+
"optionId",
|
|
227
|
+
"text",
|
|
228
|
+
"resolution",
|
|
229
|
+
] as const;
|
|
230
|
+
|
|
231
|
+
// The AI SDK's approval object declares none of the core request and answer
|
|
232
|
+
// fields and `validateUIMessages` strips unknown ones, so a host streams or
|
|
233
|
+
// persists them inside the opaque `approvalDescriptor`. Only those fields are
|
|
234
|
+
// read from it: a descriptor cannot approve its own request.
|
|
235
|
+
const readApprovalDescriptor = (
|
|
236
|
+
descriptor: unknown,
|
|
237
|
+
): Record<string, unknown> => {
|
|
238
|
+
if (
|
|
239
|
+
!descriptor ||
|
|
240
|
+
typeof descriptor !== "object" ||
|
|
241
|
+
Array.isArray(descriptor)
|
|
242
|
+
)
|
|
243
|
+
return {};
|
|
244
|
+
const fields: Record<string, unknown> = {};
|
|
245
|
+
for (const key of APPROVAL_DESCRIPTOR_FIELDS) {
|
|
246
|
+
if (Object.hasOwn(descriptor, key))
|
|
247
|
+
fields[key] = (descriptor as Record<string, unknown>)[key];
|
|
248
|
+
}
|
|
249
|
+
return fields;
|
|
250
|
+
};
|
|
251
|
+
|
|
209
252
|
function getToolApprovalAndInterrupt(
|
|
210
253
|
part: {
|
|
211
254
|
approval?: Record<string, unknown> | undefined;
|
|
@@ -220,12 +263,16 @@ function getToolApprovalAndInterrupt(
|
|
|
220
263
|
interrupt?: NonNullable<ToolCallMessagePart["interrupt"]>;
|
|
221
264
|
} {
|
|
222
265
|
if (part.approval) {
|
|
266
|
+
const approval = {
|
|
267
|
+
...readApprovalDescriptor(part.approval.descriptor),
|
|
268
|
+
...part.approval,
|
|
269
|
+
};
|
|
223
270
|
const response =
|
|
224
|
-
typeof
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
? toolApprovalResponses?.get(
|
|
271
|
+
typeof approval.id === "string" &&
|
|
272
|
+
approval.approved === undefined &&
|
|
273
|
+
approval.resolution !== "cancelled" &&
|
|
274
|
+
approval.resolution !== "expired"
|
|
275
|
+
? toolApprovalResponses?.get(approval.id)
|
|
229
276
|
: undefined;
|
|
230
277
|
// The built-in AI SDK channel sends only id, approved and reason back to
|
|
231
278
|
// the server, so a request shape promising any other answer would render
|
|
@@ -239,19 +286,20 @@ function getToolApprovalAndInterrupt(
|
|
|
239
286
|
resolution,
|
|
240
287
|
display,
|
|
241
288
|
allowFreeform,
|
|
289
|
+
dismissible,
|
|
242
290
|
options,
|
|
243
291
|
optionId,
|
|
244
292
|
text,
|
|
245
293
|
...additionalApprovalFields
|
|
246
294
|
} = response
|
|
247
295
|
? {
|
|
248
|
-
...
|
|
296
|
+
...approval,
|
|
249
297
|
approved: response.approved,
|
|
250
298
|
...(response.reason != null && { reason: response.reason }),
|
|
251
299
|
...(response.optionId != null && { optionId: response.optionId }),
|
|
252
300
|
...(response.text != null && { text: response.text }),
|
|
253
301
|
}
|
|
254
|
-
:
|
|
302
|
+
: approval;
|
|
255
303
|
const normalizedOptions = supportsRichToolApprovalResponses
|
|
256
304
|
? normalizeToolApprovalOptions(options)
|
|
257
305
|
: undefined;
|
|
@@ -274,6 +322,7 @@ function getToolApprovalAndInterrupt(
|
|
|
274
322
|
display === "select" ||
|
|
275
323
|
display === "text") && { display }),
|
|
276
324
|
...(typeof allowFreeform === "boolean" && { allowFreeform }),
|
|
325
|
+
...(typeof dismissible === "boolean" && { dismissible }),
|
|
277
326
|
...(normalizedOptions && { options: normalizedOptions }),
|
|
278
327
|
...(typeof optionId === "string" && { optionId }),
|
|
279
328
|
...(typeof text === "string" && { text }),
|
|
@@ -354,7 +403,14 @@ function convertParts(
|
|
|
354
403
|
const toolCallId = part.toolCallId;
|
|
355
404
|
const argsKeyOrderCacheKey = `${message.id}:${toolCallId}`;
|
|
356
405
|
|
|
357
|
-
|
|
406
|
+
// A tool call that streamed complete arguments then failed schema
|
|
407
|
+
// validation keeps them in `rawInput`, not `input`; reading `input`
|
|
408
|
+
// alone would convert the error snapshot to `{}` and hide the input.
|
|
409
|
+
const rawInput = (part.input ??
|
|
410
|
+
("rawInput" in part ? part.rawInput : undefined)) as
|
|
411
|
+
| ReadonlyJSONObject
|
|
412
|
+
| null
|
|
413
|
+
| undefined;
|
|
358
414
|
let args: ReadonlyJSONObject;
|
|
359
415
|
if (
|
|
360
416
|
rawInput != null &&
|
|
@@ -387,12 +443,13 @@ function convertParts(
|
|
|
387
443
|
};
|
|
388
444
|
}
|
|
389
445
|
|
|
390
|
-
let argsText
|
|
391
|
-
metadata.toolArgsKeyOrderCache,
|
|
392
|
-
argsKeyOrderCacheKey,
|
|
393
|
-
args,
|
|
394
|
-
);
|
|
446
|
+
let argsText: string;
|
|
395
447
|
if (part.state === "input-streaming") {
|
|
448
|
+
argsText = stableStringifyToolArgs(
|
|
449
|
+
metadata.toolArgsKeyOrderCache,
|
|
450
|
+
argsKeyOrderCacheKey,
|
|
451
|
+
args,
|
|
452
|
+
);
|
|
396
453
|
// strip closing delimiters added by the AI SDK's fix-json
|
|
397
454
|
argsText = stripClosingDelimiters(argsText);
|
|
398
455
|
// Re-parse so args carries the partial-JSON meta that marks which
|
|
@@ -401,6 +458,27 @@ function convertParts(
|
|
|
401
458
|
// of the stripped text is the streaming frontier.
|
|
402
459
|
args = parsePartialJsonObject(argsText) ?? args;
|
|
403
460
|
} else {
|
|
461
|
+
// A settled part is re-converted whenever its message or the converter
|
|
462
|
+
// metadata changes; the text frozen on its input object skips
|
|
463
|
+
// re-serializing large args while the call keeps that input. Arrival
|
|
464
|
+
// order only matters while args stream, so the key-order entry is
|
|
465
|
+
// released.
|
|
466
|
+
const frozen =
|
|
467
|
+
metadata.toolArgsTextCache?.get(args) ?? new Map<string, string>();
|
|
468
|
+
const frozenText = frozen.get(argsKeyOrderCacheKey);
|
|
469
|
+
if (frozenText !== undefined) {
|
|
470
|
+
argsText = frozenText;
|
|
471
|
+
} else {
|
|
472
|
+
argsText = stableStringifyToolArgs(
|
|
473
|
+
metadata.toolArgsKeyOrderCache,
|
|
474
|
+
argsKeyOrderCacheKey,
|
|
475
|
+
args,
|
|
476
|
+
);
|
|
477
|
+
metadata.toolArgsTextCache?.set(
|
|
478
|
+
args,
|
|
479
|
+
frozen.set(argsKeyOrderCacheKey, argsText),
|
|
480
|
+
);
|
|
481
|
+
}
|
|
404
482
|
metadata.toolArgsKeyOrderCache?.delete(argsKeyOrderCacheKey);
|
|
405
483
|
if (
|
|
406
484
|
part.state === "output-available" ||
|
|
@@ -416,6 +494,8 @@ function convertParts(
|
|
|
416
494
|
part,
|
|
417
495
|
metadata.mcpAppMetadataCache,
|
|
418
496
|
);
|
|
497
|
+
const artifact = metadata.toolArtifacts?.get(toolCallId);
|
|
498
|
+
const interactions = metadata.toolInteractions?.get(toolCallId);
|
|
419
499
|
return {
|
|
420
500
|
type: "tool-call",
|
|
421
501
|
toolName,
|
|
@@ -424,6 +504,12 @@ function convertParts(
|
|
|
424
504
|
args,
|
|
425
505
|
result,
|
|
426
506
|
isError,
|
|
507
|
+
...(artifact !== undefined && { artifact }),
|
|
508
|
+
...(interactions !== undefined && {
|
|
509
|
+
unstable_interactions: interactions,
|
|
510
|
+
}),
|
|
511
|
+
...(part.state === "output-available" &&
|
|
512
|
+
part.preliminary === true && { isPreliminary: true }),
|
|
427
513
|
...(modelContent !== undefined && { modelContent }),
|
|
428
514
|
...(mcpApp && { mcp: { app: mcpApp } }),
|
|
429
515
|
...(part.callProviderMetadata != null
|
package/src/index.native.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -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,6 +28,33 @@ 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", () => {
|
|
32
59
|
it("runs one chat per thread and keeps histories isolated across switches", async () => {
|
|
33
60
|
const { transport, emit, close } = createControlledTransport();
|
|
@@ -253,25 +280,19 @@ describe("AISDKThreads", () => {
|
|
|
253
280
|
|
|
254
281
|
it("forwards ChatInit callbacks to each thread's chat from the latest render", async () => {
|
|
255
282
|
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(() => {});
|
|
283
|
+
const sendMessages = vi.spyOn(transport, "sendMessages");
|
|
284
|
+
const mounted = chatCallbacks();
|
|
285
|
+
const latest = chatCallbacks();
|
|
286
|
+
latest.sendAutomaticallyWhen.mockReturnValueOnce(true);
|
|
287
|
+
let callbacks = mounted;
|
|
288
|
+
const { handle, rerender } = createLiveHandle(() => ({
|
|
289
|
+
transport: () => transport,
|
|
290
|
+
...callbacks,
|
|
291
|
+
}));
|
|
271
292
|
const aui = handle.getClient();
|
|
272
293
|
|
|
273
|
-
|
|
274
|
-
|
|
294
|
+
callbacks = latest;
|
|
295
|
+
rerender();
|
|
275
296
|
|
|
276
297
|
flushTapSync(() => aui.composer.setText("hi"));
|
|
277
298
|
flushTapSync(() => aui.composer.send());
|
|
@@ -280,10 +301,36 @@ describe("AISDKThreads", () => {
|
|
|
280
301
|
handle.getClient().thread.getState().messages.length,
|
|
281
302
|
).toBeGreaterThan(0);
|
|
282
303
|
});
|
|
283
|
-
emit(
|
|
304
|
+
emit(
|
|
305
|
+
{ type: "start" },
|
|
306
|
+
{
|
|
307
|
+
type: "tool-input-available",
|
|
308
|
+
toolCallId: "call-1",
|
|
309
|
+
toolName: "lookup",
|
|
310
|
+
input: {},
|
|
311
|
+
},
|
|
312
|
+
{ type: "data-note", data: "note" },
|
|
313
|
+
{ type: "finish" },
|
|
314
|
+
);
|
|
284
315
|
close();
|
|
285
|
-
await vi.waitFor(() => expect(
|
|
286
|
-
|
|
316
|
+
await vi.waitFor(() => expect(sendMessages).toHaveBeenCalledTimes(2));
|
|
317
|
+
emit({ type: "start" }, { type: "error", errorText: "boom" });
|
|
318
|
+
close();
|
|
319
|
+
await vi.waitFor(() => expect(latest.onFinish).toHaveBeenCalledTimes(2));
|
|
320
|
+
|
|
321
|
+
expect(latest.onToolCall).toHaveBeenCalledExactlyOnceWith({
|
|
322
|
+
toolCall: expect.objectContaining({ toolCallId: "call-1" }),
|
|
323
|
+
});
|
|
324
|
+
expect(latest.onData).toHaveBeenCalledExactlyOnceWith(
|
|
325
|
+
expect.objectContaining({ type: "data-note", data: "note" }),
|
|
326
|
+
);
|
|
327
|
+
expect(latest.onError).toHaveBeenCalledExactlyOnceWith(
|
|
328
|
+
expect.objectContaining({ message: "boom" }),
|
|
329
|
+
);
|
|
330
|
+
expect(latest.sendAutomaticallyWhen).toHaveBeenCalledOnce();
|
|
331
|
+
for (const callback of Object.values(mounted)) {
|
|
332
|
+
expect(callback).not.toHaveBeenCalled();
|
|
333
|
+
}
|
|
287
334
|
|
|
288
335
|
handle.destroy();
|
|
289
336
|
});
|
|
@@ -293,18 +340,10 @@ describe("AISDKThreads", () => {
|
|
|
293
340
|
const onFinishA = vi.fn();
|
|
294
341
|
const onFinishB = vi.fn();
|
|
295
342
|
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(() => {});
|
|
343
|
+
const { handle, rerender } = createLiveHandle(() => ({
|
|
344
|
+
transport: () => transport,
|
|
345
|
+
onFinish,
|
|
346
|
+
}));
|
|
308
347
|
const aui = handle.getClient();
|
|
309
348
|
|
|
310
349
|
flushTapSync(() => aui.composer.setText("stream me"));
|
|
@@ -322,7 +361,7 @@ describe("AISDKThreads", () => {
|
|
|
322
361
|
|
|
323
362
|
flushTapSync(() => aui.threads.switchToNewThread());
|
|
324
363
|
onFinish = onFinishB;
|
|
325
|
-
|
|
364
|
+
rerender();
|
|
326
365
|
|
|
327
366
|
emit({ type: "text-end", id: "t1" }, { type: "finish" });
|
|
328
367
|
close();
|
|
@@ -361,18 +400,12 @@ describe("AISDKThreads", () => {
|
|
|
361
400
|
const onFinishA = vi.fn();
|
|
362
401
|
const onFinishB = vi.fn();
|
|
363
402
|
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(() => {});
|
|
403
|
+
const { handle, rerender } = createLiveHandle(() => ({
|
|
404
|
+
cloud,
|
|
405
|
+
threadId: "t1",
|
|
406
|
+
transport,
|
|
407
|
+
onFinish,
|
|
408
|
+
}));
|
|
376
409
|
try {
|
|
377
410
|
await handle.getClient().threads.getLoadThreadsPromise();
|
|
378
411
|
await vi.waitFor(() => {
|
|
@@ -383,7 +416,7 @@ describe("AISDKThreads", () => {
|
|
|
383
416
|
});
|
|
384
417
|
|
|
385
418
|
onFinish = onFinishB;
|
|
386
|
-
|
|
419
|
+
rerender();
|
|
387
420
|
|
|
388
421
|
flushTapSync(() => handle.getClient().composer.setText("hi"));
|
|
389
422
|
flushTapSync(() => handle.getClient().composer.send());
|
|
@@ -130,6 +130,57 @@ describe("useAISDKRuntime tool approvals with a Chat", () => {
|
|
|
130
130
|
expect(sendMessages).toHaveBeenCalledTimes(1);
|
|
131
131
|
});
|
|
132
132
|
|
|
133
|
+
it("renders a streamed request as its approvalDescriptor declares", async () => {
|
|
134
|
+
const descriptor = {
|
|
135
|
+
prompt: "Which environment?",
|
|
136
|
+
display: "select",
|
|
137
|
+
options: [{ id: "once", kind: "allow-once", label: "Staging once" }],
|
|
138
|
+
scope: "deploy",
|
|
139
|
+
};
|
|
140
|
+
const handler = vi.fn<ApprovalHandler>(async () => {});
|
|
141
|
+
const { approval, part, toolPart } = await setup(() => handler, {
|
|
142
|
+
request: [
|
|
143
|
+
{ type: "start", messageId: "assistant-1" },
|
|
144
|
+
{ type: "start-step" },
|
|
145
|
+
{
|
|
146
|
+
type: "tool-input-available",
|
|
147
|
+
toolCallId: "tool-1",
|
|
148
|
+
toolName: "deploy",
|
|
149
|
+
input: {},
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
type: "tool-approval-request",
|
|
153
|
+
approvalId: "approval-1",
|
|
154
|
+
toolCallId: "tool-1",
|
|
155
|
+
approvalDescriptor: descriptor,
|
|
156
|
+
},
|
|
157
|
+
{ type: "finish-step" },
|
|
158
|
+
{ type: "finish" },
|
|
159
|
+
],
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
expect(toolPart()).toMatchObject({
|
|
163
|
+
state: "approval-requested",
|
|
164
|
+
approval: { id: "approval-1", descriptor },
|
|
165
|
+
});
|
|
166
|
+
expect(approval()).toEqual({
|
|
167
|
+
id: "approval-1",
|
|
168
|
+
prompt: "Which environment?",
|
|
169
|
+
display: "select",
|
|
170
|
+
options: [{ id: "once", kind: "allow-once", label: "Staging once" }],
|
|
171
|
+
descriptor,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
await act(() => part().respondToToolApproval({ optionId: "once" }));
|
|
175
|
+
|
|
176
|
+
expect(handler).toHaveBeenCalledWith(
|
|
177
|
+
{ approvalId: "approval-1", approved: true, optionId: "once" },
|
|
178
|
+
expect.objectContaining({ toolCallId: "tool-1", toolName: "deploy" }),
|
|
179
|
+
);
|
|
180
|
+
expect(approval()).toMatchObject({ approved: true, optionId: "once" });
|
|
181
|
+
expect(toolPart()).toMatchObject({ state: "approval-requested" });
|
|
182
|
+
});
|
|
183
|
+
|
|
133
184
|
it("keeps a host answer out of the chat's automatic sends", async () => {
|
|
134
185
|
const { chat, sendMessages, respond } = await setup(() => async () => {}, {
|
|
135
186
|
request: approvalStep([
|