@assistant-ui/ai-sdk 0.0.6 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/dist/adapters/vercelAttachmentAdapter.d.ts +1 -2
  2. package/dist/adapters/vercelAttachmentAdapter.d.ts.map +1 -1
  3. package/dist/aiSDKExtras.d.ts +2 -3
  4. package/dist/aiSDKExtras.d.ts.map +1 -1
  5. package/dist/converters/convertMessage.d.ts +5 -4
  6. package/dist/converters/convertMessage.d.ts.map +1 -1
  7. package/dist/converters/convertMessage.js +39 -3
  8. package/dist/converters/convertMessage.js.map +1 -1
  9. package/dist/converters/modelContentEnvelope.d.ts +4 -5
  10. package/dist/converters/modelContentEnvelope.d.ts.map +1 -1
  11. package/dist/converters/toCreateMessage.d.ts +1 -2
  12. package/dist/converters/toCreateMessage.d.ts.map +1 -1
  13. package/dist/converters/toolOutputConversion.d.ts +2 -3
  14. package/dist/converters/toolOutputConversion.d.ts.map +1 -1
  15. package/dist/hooks.d.ts +2 -3
  16. package/dist/hooks.d.ts.map +1 -1
  17. package/dist/model-context/injectInteractableContext.d.ts +1 -2
  18. package/dist/model-context/injectInteractableContext.d.ts.map +1 -1
  19. package/dist/model-context/injectQuoteContext.d.ts +1 -2
  20. package/dist/model-context/injectQuoteContext.d.ts.map +1 -1
  21. package/dist/runtime/AISDKChat.d.ts +2 -3
  22. package/dist/runtime/AISDKChat.d.ts.map +1 -1
  23. package/dist/runtime/AISDKThreads.d.ts +2 -3
  24. package/dist/runtime/AISDKThreads.d.ts.map +1 -1
  25. package/dist/runtime/AISDKThreads.js +4 -10
  26. package/dist/runtime/AISDKThreads.js.map +1 -1
  27. package/dist/runtime/sdkIdentity.d.ts +1 -2
  28. package/dist/runtime/sdkIdentity.d.ts.map +1 -1
  29. package/dist/runtime/sdkIdentity.js +1 -1
  30. package/dist/runtime/useAISDKRuntime.d.ts +17 -5
  31. package/dist/runtime/useAISDKRuntime.d.ts.map +1 -1
  32. package/dist/runtime/useAISDKRuntime.js +95 -37
  33. package/dist/runtime/useAISDKRuntime.js.map +1 -1
  34. package/dist/runtime/useChatRuntime.d.ts +2 -3
  35. package/dist/runtime/useChatRuntime.d.ts.map +1 -1
  36. package/dist/runtime/useChatThread.d.ts +19 -5
  37. package/dist/runtime/useChatThread.d.ts.map +1 -1
  38. package/dist/runtime/useChatThread.js +30 -9
  39. package/dist/runtime/useChatThread.js.map +1 -1
  40. package/dist/runtime/useExternalHistory.d.ts +2 -3
  41. package/dist/runtime/useExternalHistory.d.ts.map +1 -1
  42. package/dist/runtime/useExternalHistory.js +28 -20
  43. package/dist/runtime/useExternalHistory.js.map +1 -1
  44. package/dist/runtime/useResourceCleanup.d.ts +1 -2
  45. package/dist/runtime/useResourceCleanup.d.ts.map +1 -1
  46. package/dist/runtime/useStreamingTiming.d.ts +2 -3
  47. package/dist/runtime/useStreamingTiming.d.ts.map +1 -1
  48. package/dist/tools/frontendTools.d.ts +3 -4
  49. package/dist/tools/frontendTools.d.ts.map +1 -1
  50. package/dist/tools/generativeTools.d.ts +5 -6
  51. package/dist/tools/generativeTools.d.ts.map +1 -1
  52. package/dist/tools/mcp-stdio.unsupported.d.ts +1 -2
  53. package/dist/tools/mcp-stdio.unsupported.d.ts.map +1 -1
  54. package/dist/transport/AssistantChatTransport.d.ts +3 -4
  55. package/dist/transport/AssistantChatTransport.d.ts.map +1 -1
  56. package/dist/transport/resumable.d.ts +4 -5
  57. package/dist/transport/resumable.d.ts.map +1 -1
  58. package/dist/usage.d.ts +4 -5
  59. package/dist/usage.d.ts.map +1 -1
  60. package/dist/utils/getVercelAIMessages.d.ts +1 -2
  61. package/dist/utils/getVercelAIMessages.d.ts.map +1 -1
  62. package/dist/utils/sliceMessagesUntil.d.ts +1 -2
  63. package/dist/utils/sliceMessagesUntil.d.ts.map +1 -1
  64. package/package.json +15 -14
  65. package/src/converters/convertMessage.test.ts +138 -0
  66. package/src/converters/convertMessage.ts +94 -5
  67. package/src/runtime/AISDKChat.test.ts +4 -5
  68. package/src/runtime/AISDKThreads.test.ts +9 -5
  69. package/src/runtime/AISDKThreads.ts +3 -12
  70. package/src/runtime/useAISDKRuntime.approval.integration.test.tsx +287 -0
  71. package/src/runtime/useAISDKRuntime.approval.test.tsx +257 -1
  72. package/src/runtime/useAISDKRuntime.test.ts +7 -3
  73. package/src/runtime/useAISDKRuntime.ts +147 -23
  74. package/src/runtime/useAISDKRuntime.voice.test.tsx +270 -0
  75. package/src/runtime/useChatRuntime.test.ts +72 -4
  76. package/src/runtime/useChatThread.ts +44 -5
  77. package/src/runtime/useExternalHistory.test.ts +46 -0
  78. package/src/runtime/useExternalHistory.ts +24 -10
  79. package/src/tools/generativeTools.test.ts +7 -1
@@ -1,7 +1,7 @@
1
1
  //#region src/transport/resumable.d.ts
2
2
  /** Response header used by the [Resumable Streams](/docs/guides/resumable-streams) server and client wiring. */
3
- declare const RESUMABLE_STREAM_ID_HEADER = "x-resumable-stream-id";
4
- type ResumableClientStorage = {
3
+ export declare const RESUMABLE_STREAM_ID_HEADER = "x-resumable-stream-id";
4
+ export type ResumableClientStorage = {
5
5
  getStreamId(threadId?: string): string | null;
6
6
  setStreamId(id: string, threadId?: string): void;
7
7
  clear(threadId?: string): void;
@@ -9,7 +9,7 @@ type ResumableClientStorage = {
9
9
  subscribe?(listener: () => void, threadId?: string): () => void;
10
10
  };
11
11
  /** `sessionStorage`-backed storage for the pending resumable stream id. See the [Resumable Streams](/docs/guides/resumable-streams) guide for end-to-end wiring. */
12
- declare function createResumableSessionStorage(options?: {
12
+ export declare function createResumableSessionStorage(options?: {
13
13
  /**
14
14
  * Storage key for the pending stream id. A static string namespaces per route
15
15
  * or chat surface. A getter is read lazily on every access, so the key can be
@@ -24,7 +24,7 @@ declare function createResumableSessionStorage(options?: {
24
24
  */
25
25
  key?: string | (() => string | undefined);
26
26
  }): ResumableClientStorage;
27
- type AssistantChatResumableOptions = {
27
+ export type AssistantChatResumableOptions = {
28
28
  storage: ResumableClientStorage;
29
29
  resumeApi: string | ((streamId: string) => string);
30
30
  /**
@@ -34,5 +34,4 @@ type AssistantChatResumableOptions = {
34
34
  isFinishEvent?: (chunk: Uint8Array, accumulator: string) => boolean;
35
35
  };
36
36
  //#endregion
37
- export { AssistantChatResumableOptions, RESUMABLE_STREAM_ID_HEADER, ResumableClientStorage, createResumableSessionStorage };
38
37
  //# sourceMappingURL=resumable.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"resumable.d.ts","names":[],"sources":["../../src/transport/resumable.ts"],"mappings":";;cAKa;KAID;EACV,YAAY;EACZ,YAAY,YAAY;EACxB,MAAM;;EAEN,WAAW,sBAAsB;;;iBAkBnB,8BAA8B;;;;;;;;;;;;;EAa5C;IACE;KA0GQ;EACV,SAAS;EACT,sBAAsB;;;;;EAKtB,iBAAiB,OAAO,YAAY"}
1
+ {"version":3,"file":"resumable.d.ts","names":[],"sources":["../../src/transport/resumable.ts"],"mappings":";;qBAKa;YAID;EACV,YAAY;EACZ,YAAY,YAAY;EACxB,MAAM;;EAEN,WAAW,sBAAsB;;;wBAkBnB,8BAA8B;;;;;;;;;;;;;EAa5C;IACE;YA0GQ;EACV,SAAS;EACT,sBAAsB;;;;;EAKtB,iBAAiB,OAAO,YAAY"}
package/dist/usage.d.ts CHANGED
@@ -1,17 +1,17 @@
1
1
  /// <reference types="@assistant-ui/core/react" />
2
2
  //#region src/usage.d.ts
3
- type ThreadTokenUsage = {
3
+ export type ThreadTokenUsage = {
4
4
  totalTokens?: number;
5
5
  inputTokens?: number;
6
6
  outputTokens?: number;
7
7
  reasoningTokens?: number;
8
8
  cachedInputTokens?: number;
9
9
  };
10
- interface TokenUsageExtractableMessage {
10
+ export interface TokenUsageExtractableMessage {
11
11
  role?: string;
12
12
  metadata?: unknown;
13
13
  }
14
- declare function getThreadMessageTokenUsage(message: TokenUsageExtractableMessage | undefined): ThreadTokenUsage | undefined;
14
+ export declare function getThreadMessageTokenUsage(message: TokenUsageExtractableMessage | undefined): ThreadTokenUsage | undefined;
15
15
  /**
16
16
  * Reads token usage from the newest assistant message that reports any.
17
17
  *
@@ -20,7 +20,6 @@ declare function getThreadMessageTokenUsage(message: TokenUsageExtractableMessag
20
20
  * other key the route returns into `metadata.custom`, which is where this hook
21
21
  * looks.
22
22
  */
23
- declare function useThreadTokenUsage(): ThreadTokenUsage | undefined;
23
+ export declare function useThreadTokenUsage(): ThreadTokenUsage | undefined;
24
24
  //#endregion
25
- export { ThreadTokenUsage, TokenUsageExtractableMessage, getThreadMessageTokenUsage, useThreadTokenUsage };
26
25
  //# sourceMappingURL=usage.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"usage.d.ts","names":[],"sources":["../src/usage.ts"],"mappings":";KAIY;EACV;EACA;EACA;EACA;EACA;;UAGe;EACf;EACA;;iBAuHc,2BACd,SAAS,2CACR;;;;;;;;;iBAsCa,uBAAuB"}
1
+ {"version":3,"file":"usage.d.ts","names":[],"sources":["../src/usage.ts"],"mappings":";YAIY;EACV;EACA;EACA;EACA;EACA;;iBAGe;EACf;EACA;;wBAuHc,2BACd,SAAS,2CACR;;;;;;;;;wBAsCa,uBAAuB"}
@@ -1,7 +1,6 @@
1
1
  import { UIMessage } from "ai";
2
2
  import { ThreadMessage } from "@assistant-ui/core";
3
3
  //#region src/utils/getVercelAIMessages.d.ts
4
- declare const getVercelAIMessages: <UI_MESSAGE extends UIMessage = UIMessage>(message: ThreadMessage) => UI_MESSAGE[];
4
+ export declare const getVercelAIMessages: <UI_MESSAGE extends UIMessage = UIMessage>(message: ThreadMessage) => UI_MESSAGE[];
5
5
  //#endregion
6
- export { getVercelAIMessages };
7
6
  //# sourceMappingURL=getVercelAIMessages.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"getVercelAIMessages.d.ts","names":[],"sources":["../../src/utils/getVercelAIMessages.ts"],"mappings":";;;cAMa,sBAAuB,mBAAmB,YAAY,WACjE,SAAS,kBAEmC"}
1
+ {"version":3,"file":"getVercelAIMessages.d.ts","names":[],"sources":["../../src/utils/getVercelAIMessages.ts"],"mappings":";;;qBAMa,sBAAuB,mBAAmB,YAAY,WACjE,SAAS,kBAEmC"}
@@ -1,6 +1,5 @@
1
1
  import { UIMessage } from "ai";
2
2
  //#region src/utils/sliceMessagesUntil.d.ts
3
- declare const sliceMessagesUntil: <UI_MESSAGE extends UIMessage = UIMessage>(messages: UI_MESSAGE[], messageId: string | null) => UI_MESSAGE[];
3
+ export declare const sliceMessagesUntil: <UI_MESSAGE extends UIMessage = UIMessage>(messages: UI_MESSAGE[], messageId: string | null) => UI_MESSAGE[];
4
4
  //#endregion
5
- export { sliceMessagesUntil };
6
5
  //# sourceMappingURL=sliceMessagesUntil.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sliceMessagesUntil.d.ts","names":[],"sources":["../../src/utils/sliceMessagesUntil.ts"],"mappings":";;cAEa,qBAAsB,mBAAmB,YAAY,WAChE,UAAU,cACV,6BAAwB"}
1
+ {"version":3,"file":"sliceMessagesUntil.d.ts","names":[],"sources":["../../src/utils/sliceMessagesUntil.ts"],"mappings":";;qBAEa,qBAAsB,mBAAmB,YAAY,WAChE,UAAU,cACV,6BAAwB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assistant-ui/ai-sdk",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "description": "Vercel AI SDK adapter for assistant-ui",
5
5
  "keywords": [
6
6
  "ai-sdk",
@@ -46,14 +46,14 @@
46
46
  ],
47
47
  "sideEffects": false,
48
48
  "dependencies": {
49
- "@ai-sdk/mcp": "^2.0.45",
50
- "@ai-sdk/react": "^4.0.96",
51
- "@assistant-ui/core": "^0.3.19",
52
- "@assistant-ui/store": "^0.3.13",
53
- "@assistant-ui/tap": "^0.9.17",
54
- "ai": "^7.0.93",
55
- "assistant-cloud": "^0.2.1",
56
- "assistant-stream": "^0.3.43"
49
+ "@ai-sdk/mcp": "^2.0.49",
50
+ "@ai-sdk/react": "^4.0.104",
51
+ "@assistant-ui/core": "^0.3.20",
52
+ "@assistant-ui/store": "^0.3.14",
53
+ "@assistant-ui/tap": "^0.9.18",
54
+ "ai": "^7.0.101",
55
+ "assistant-cloud": "^0.2.2",
56
+ "assistant-stream": "^0.3.44"
57
57
  },
58
58
  "peerDependencies": {
59
59
  "@types/react": "*",
@@ -68,14 +68,14 @@
68
68
  }
69
69
  },
70
70
  "devDependencies": {
71
- "@assistant-ui/x-buildutils": "0.0.27",
71
+ "@assistant-ui/x-buildutils": "0.0.28",
72
72
  "@testing-library/react": "^16.3.3",
73
73
  "@types/json-schema": "^7.0.15",
74
- "@types/react": "^19.2.18",
75
- "@types/react-dom": "^19.2.7",
74
+ "@types/react": "^19.3.0",
75
+ "@types/react-dom": "^19.3.0",
76
76
  "jsdom": "^30.0.1",
77
- "react": "^19.2.8",
78
- "vitest": "^5.0.0"
77
+ "react": "^19.3.0",
78
+ "vitest": "^5.0.1"
79
79
  },
80
80
  "publishConfig": {
81
81
  "access": "public",
@@ -93,6 +93,7 @@
93
93
  "scripts": {
94
94
  "build": "aui-build",
95
95
  "test": "vitest run",
96
+ "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json",
96
97
  "test:react-compiler": "vitest run --config vitest.react-compiler.config.ts",
97
98
  "test:standalone": "vitest run --project standalone",
98
99
  "test:watch": "vitest"
@@ -487,6 +487,144 @@ describe("AISDKMessageConverter", () => {
487
487
  });
488
488
  });
489
489
 
490
+ it("preserves rich approval fields for a custom response channel", () => {
491
+ const metadata: AISDKMessageConverterMetadata = {
492
+ supportsRichToolApprovalResponses: true,
493
+ };
494
+ const converted = AISDKMessageConverter.toThreadMessages(
495
+ [
496
+ {
497
+ id: "a1",
498
+ role: "assistant",
499
+ parts: [
500
+ {
501
+ type: "tool-deploy",
502
+ toolCallId: "tc-1",
503
+ state: "approval-responded",
504
+ input: {},
505
+ approval: {
506
+ id: "approval-1",
507
+ display: "select",
508
+ allowFreeform: true,
509
+ options: [
510
+ {
511
+ id: "once",
512
+ kind: "allow-once",
513
+ label: "Only once",
514
+ grants: ["repository", 42],
515
+ confirm: {
516
+ title: "Confirm access",
517
+ description: { invalid: true },
518
+ },
519
+ },
520
+ "invalid",
521
+ { id: 1, kind: "allow-always" },
522
+ { id: "always", kind: 2 },
523
+ ],
524
+ optionId: "once",
525
+ text: "an answer",
526
+ },
527
+ },
528
+ ],
529
+ } as any,
530
+ ],
531
+ false,
532
+ metadata,
533
+ );
534
+
535
+ const toolCall = converted[0]?.content.find(
536
+ (part): part is any => part.type === "tool-call",
537
+ );
538
+ expect(toolCall?.approval).toEqual({
539
+ id: "approval-1",
540
+ display: "select",
541
+ allowFreeform: true,
542
+ options: [
543
+ {
544
+ id: "once",
545
+ kind: "allow-once",
546
+ label: "Only once",
547
+ grants: ["repository"],
548
+ confirm: { title: "Confirm access" },
549
+ },
550
+ ],
551
+ optionId: "once",
552
+ text: "an answer",
553
+ });
554
+ });
555
+
556
+ it("applies a host answer to an approval the message has not recorded", () => {
557
+ const metadata: AISDKMessageConverterMetadata = {
558
+ supportsRichToolApprovalResponses: true,
559
+ toolApprovalResponses: new Map([
560
+ [
561
+ "approval-1",
562
+ {
563
+ approvalId: "approval-1",
564
+ approved: true,
565
+ optionId: "staging",
566
+ text: "only staging",
567
+ },
568
+ ],
569
+ ["approval-2", { approvalId: "approval-2", approved: true }],
570
+ ["approval-3", { approvalId: "approval-3", approved: true }],
571
+ ]),
572
+ };
573
+ const converted = AISDKMessageConverter.toThreadMessages(
574
+ [
575
+ {
576
+ id: "a1",
577
+ role: "assistant",
578
+ parts: [
579
+ {
580
+ type: "tool-deploy",
581
+ toolCallId: "tc-1",
582
+ state: "approval-requested",
583
+ input: {},
584
+ approval: {
585
+ id: "approval-1",
586
+ display: "select",
587
+ options: [{ id: "staging", kind: "_target" }],
588
+ },
589
+ },
590
+ {
591
+ type: "tool-deploy",
592
+ toolCallId: "tc-2",
593
+ state: "approval-responded",
594
+ input: {},
595
+ approval: { id: "approval-2", approved: false, reason: "no" },
596
+ },
597
+ {
598
+ type: "tool-deploy",
599
+ toolCallId: "tc-3",
600
+ state: "approval-requested",
601
+ input: {},
602
+ approval: { id: "approval-3", resolution: "expired" },
603
+ },
604
+ ],
605
+ } as any,
606
+ ],
607
+ false,
608
+ metadata,
609
+ );
610
+
611
+ const approvals = converted[0]?.content.map(
612
+ (part) => (part as { approval?: unknown }).approval,
613
+ );
614
+ expect(approvals).toEqual([
615
+ {
616
+ id: "approval-1",
617
+ display: "select",
618
+ options: [{ id: "staging", kind: "_target" }],
619
+ approved: true,
620
+ optionId: "staging",
621
+ text: "only staging",
622
+ },
623
+ { id: "approval-2", approved: false, reason: "no" },
624
+ { id: "approval-3", resolution: "expired" },
625
+ ]);
626
+ });
627
+
490
628
  it("drops a resolution the core contract does not declare", () => {
491
629
  const converted = AISDKMessageConverter.toThreadMessages([
492
630
  {
@@ -12,6 +12,7 @@ import {
12
12
  import {
13
13
  isMcpAppUri,
14
14
  type ReasoningMessagePart,
15
+ type ToolApprovalOption,
15
16
  type ToolCallMessagePart,
16
17
  type TextMessagePart,
17
18
  type DataMessagePart,
@@ -22,6 +23,7 @@ import {
22
23
  type ThreadMessageLike,
23
24
  type McpAppMetadata,
24
25
  type MessagePartStreamStatus,
26
+ type RespondToToolApprovalOptions,
25
27
  } from "@assistant-ui/core";
26
28
  import { stableStringifyToolArgs } from "@assistant-ui/core/internal";
27
29
  import {
@@ -61,6 +63,8 @@ export type AISDKMessageConverterMetadata =
61
63
  toolArgsKeyOrderCache?: Map<string, Map<string, string[]>>;
62
64
  toolLastInputCache?: Map<string, ReadonlyJSONObject>;
63
65
  mcpAppMetadataCache?: Map<string, McpAppMetadata>;
66
+ supportsRichToolApprovalResponses?: boolean;
67
+ toolApprovalResponses?: ReadonlyMap<string, RespondToToolApprovalOptions>;
64
68
  /** Id of the currently-streaming message, flagged optimistic (#4037). */
65
69
  optimisticMessageId?: string | undefined;
66
70
  };
@@ -153,19 +157,79 @@ function extractMcpAppMetadata(
153
157
  return out;
154
158
  }
155
159
 
160
+ const normalizeToolApprovalOptions = (
161
+ options: unknown,
162
+ ): readonly ToolApprovalOption[] | undefined => {
163
+ if (!Array.isArray(options)) return undefined;
164
+
165
+ return options.flatMap<ToolApprovalOption>((value) => {
166
+ if (!value || typeof value !== "object" || Array.isArray(value)) return [];
167
+ const option = value as Record<string, unknown>;
168
+ if (typeof option.id !== "string" || typeof option.kind !== "string")
169
+ return [];
170
+
171
+ const confirm = option.confirm;
172
+ const confirmDetails =
173
+ confirm && typeof confirm === "object" && !Array.isArray(confirm)
174
+ ? (confirm as Record<string, unknown>)
175
+ : undefined;
176
+
177
+ return [
178
+ {
179
+ id: option.id,
180
+ kind: option.kind,
181
+ ...(typeof option.label === "string" && { label: option.label }),
182
+ ...(typeof option.description === "string" && {
183
+ description: option.description,
184
+ }),
185
+ ...(Array.isArray(option.grants) && {
186
+ grants: option.grants.filter(
187
+ (grant): grant is string => typeof grant === "string",
188
+ ),
189
+ }),
190
+ ...(typeof confirm === "boolean"
191
+ ? { confirm }
192
+ : confirmDetails
193
+ ? {
194
+ confirm: {
195
+ ...(typeof confirmDetails.title === "string" && {
196
+ title: confirmDetails.title,
197
+ }),
198
+ ...(typeof confirmDetails.description === "string" && {
199
+ description: confirmDetails.description,
200
+ }),
201
+ },
202
+ }
203
+ : {}),
204
+ },
205
+ ];
206
+ });
207
+ };
208
+
156
209
  function getToolApprovalAndInterrupt(
157
210
  part: {
158
211
  approval?: Record<string, unknown> | undefined;
159
212
  },
160
213
  toolStatus: { type: string; payload?: unknown } | undefined,
214
+ supportsRichToolApprovalResponses: boolean,
215
+ toolApprovalResponses:
216
+ | ReadonlyMap<string, RespondToToolApprovalOptions>
217
+ | undefined,
161
218
  ): {
162
219
  approval?: NonNullable<ToolCallMessagePart["approval"]>;
163
220
  interrupt?: NonNullable<ToolCallMessagePart["interrupt"]>;
164
221
  } {
165
222
  if (part.approval) {
166
- // The AI SDK sends only id, approved and reason back to the server, so a
167
- // request shape promising any other answer would render controls whose
168
- // response cannot travel.
223
+ const response =
224
+ typeof part.approval.id === "string" &&
225
+ part.approval.approved === undefined &&
226
+ part.approval.resolution !== "cancelled" &&
227
+ part.approval.resolution !== "expired"
228
+ ? toolApprovalResponses?.get(part.approval.id)
229
+ : undefined;
230
+ // The built-in AI SDK channel sends only id, approved and reason back to
231
+ // the server, so a request shape promising any other answer would render
232
+ // controls whose response cannot travel.
169
233
  const {
170
234
  id,
171
235
  prompt,
@@ -179,7 +243,18 @@ function getToolApprovalAndInterrupt(
179
243
  optionId,
180
244
  text,
181
245
  ...additionalApprovalFields
182
- } = part.approval;
246
+ } = response
247
+ ? {
248
+ ...part.approval,
249
+ approved: response.approved,
250
+ ...(response.reason != null && { reason: response.reason }),
251
+ ...(response.optionId != null && { optionId: response.optionId }),
252
+ ...(response.text != null && { text: response.text }),
253
+ }
254
+ : part.approval;
255
+ const normalizedOptions = supportsRichToolApprovalResponses
256
+ ? normalizeToolApprovalOptions(options)
257
+ : undefined;
183
258
  const requestReason = additionalApprovalFields.requestReason;
184
259
  if (typeof id === "string")
185
260
  return {
@@ -194,6 +269,15 @@ function getToolApprovalAndInterrupt(
194
269
  ...(typeof approved === "boolean" && { approved }),
195
270
  ...(typeof reason === "string" && { reason }),
196
271
  ...(isAutomatic === true && { isAutomatic: true }),
272
+ ...(supportsRichToolApprovalResponses && {
273
+ ...((display === "decision" ||
274
+ display === "select" ||
275
+ display === "text") && { display }),
276
+ ...(typeof allowFreeform === "boolean" && { allowFreeform }),
277
+ ...(normalizedOptions && { options: normalizedOptions }),
278
+ ...(typeof optionId === "string" && { optionId }),
279
+ ...(typeof text === "string" && { text }),
280
+ }),
197
281
  ...((resolution === "cancelled" || resolution === "expired") && {
198
282
  resolution,
199
283
  }),
@@ -348,7 +432,12 @@ function convertParts(
348
432
  part.callProviderMetadata as PartProviderMetadata,
349
433
  }
350
434
  : undefined),
351
- ...getToolApprovalAndInterrupt(part, toolStatus),
435
+ ...getToolApprovalAndInterrupt(
436
+ part,
437
+ toolStatus,
438
+ metadata.supportsRichToolApprovalResponses === true,
439
+ metadata.toolApprovalResponses,
440
+ ),
352
441
  } satisfies ToolCallMessagePart;
353
442
  }
354
443
 
@@ -153,7 +153,9 @@ describe("AISDKChat as a standalone client config entry", () => {
153
153
  ]
154
154
  .map((chunk) => `data: ${JSON.stringify(chunk)}\n\n`)
155
155
  .join("");
156
- const fetchMock = vi.fn(
156
+ const fetchMock = vi.fn<
157
+ (input: RequestInfo | URL, init: RequestInit) => Promise<Response>
158
+ >(
157
159
  async () =>
158
160
  new Response(sse, {
159
161
  headers: { "content-type": "text/event-stream" },
@@ -178,10 +180,7 @@ describe("AISDKChat as a standalone client config entry", () => {
178
180
  expect(state.messages).toHaveLength(2);
179
181
  });
180
182
 
181
- const [url, init] = fetchMock.mock.calls[0]! as [
182
- RequestInfo,
183
- RequestInit,
184
- ];
183
+ const [url, init] = fetchMock.mock.calls[0]!;
185
184
  expect(String(url)).toContain("/api/chat");
186
185
  const body = JSON.parse(init.body as string);
187
186
  expect(body.id).toBe("test-thread-1");
@@ -455,7 +455,9 @@ describe("AISDKThreads", () => {
455
455
  const list = vi.fn(async () => ({
456
456
  threads: [cloudThread("cloud-1"), cloudThread("cloud-2")],
457
457
  }));
458
- const create = vi.fn(async () => ({ thread_id: "cloud-created" }));
458
+ const create = vi.fn<AssistantCloud["threads"]["create"]>(async () => ({
459
+ thread_id: "cloud-created",
460
+ }));
459
461
  const deleteThread = vi.fn(async () => {});
460
462
  const cloud = {
461
463
  threads: {
@@ -521,7 +523,9 @@ describe("AISDKThreads", () => {
521
523
  external_id: null,
522
524
  metadata: null,
523
525
  });
524
- const create = vi.fn(async () => ({ message_id: "remote-message-1" }));
526
+ const create = vi.fn<AssistantCloud["threads"]["messages"]["create"]>(
527
+ async () => ({ message_id: "remote-message-1" }),
528
+ );
525
529
  const cloud = {
526
530
  threads: {
527
531
  list: vi.fn(async () => ({
@@ -602,9 +606,9 @@ describe("AISDKThreads", () => {
602
606
  external_id: null,
603
607
  metadata: null,
604
608
  });
605
- const createMessage = vi.fn(async () => ({
606
- message_id: "remote-message-1",
607
- }));
609
+ const createMessage = vi.fn<
610
+ AssistantCloud["threads"]["messages"]["create"]
611
+ >(async () => ({ message_id: "remote-message-1" }));
608
612
  const cloud = {
609
613
  threads: {
610
614
  list: vi.fn(async () => ({ threads: [cloudThread("t1")] })),
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { resource, useResource, withKey } from "@assistant-ui/tap";
4
4
  import { useEffect, useMemo, useState } from "react";
5
- import { Chat, type UIMessage } from "@ai-sdk/react";
5
+ import type { Chat, UIMessage } from "@ai-sdk/react";
6
6
  import type { ChatTransport } from "ai";
7
7
  import type { AssistantCloud } from "assistant-cloud";
8
8
  import {
@@ -20,6 +20,7 @@ import {
20
20
  import { useAui } from "@assistant-ui/store";
21
21
  import { AssistantChatTransport } from "../transport/AssistantChatTransport";
22
22
  import {
23
+ createChat,
23
24
  splitChatThreadOptions,
24
25
  useChatThread,
25
26
  type ChatThreadOptions,
@@ -91,17 +92,7 @@ const createChatEntry = <UI_MESSAGE extends UIMessage>(
91
92
  ? options.transport.__internal_clone()
92
93
  : options.transport;
93
94
  return {
94
- chat: new Chat<UI_MESSAGE>({
95
- ...chatInit,
96
- id: threadId,
97
- transport,
98
- onToolCall: (arg) => optionsRef.current?.onToolCall?.(arg),
99
- onData: (arg) => optionsRef.current?.onData?.(arg),
100
- onFinish: (arg) => optionsRef.current?.onFinish?.(arg),
101
- onError: (arg) => optionsRef.current?.onError?.(arg),
102
- sendAutomaticallyWhen: (arg) =>
103
- optionsRef.current?.sendAutomaticallyWhen?.(arg) ?? false,
104
- }),
95
+ chat: createChat({ ...chatInit, id: threadId, transport }, optionsRef),
105
96
  transport,
106
97
  repository: new MessageRepository(),
107
98
  optionsRef,