@assistant-ui/react-ai-sdk 1.3.7 → 1.3.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.
Files changed (37) hide show
  1. package/dist/ui/getVercelAIMessages.d.ts.map +1 -1
  2. package/dist/ui/getVercelAIMessages.js.map +1 -1
  3. package/dist/ui/use-chat/AssistantChatTransport.d.ts.map +1 -1
  4. package/dist/ui/use-chat/AssistantChatTransport.js.map +1 -1
  5. package/dist/ui/use-chat/useAISDKRuntime.d.ts.map +1 -1
  6. package/dist/ui/use-chat/useAISDKRuntime.js +22 -10
  7. package/dist/ui/use-chat/useAISDKRuntime.js.map +1 -1
  8. package/dist/ui/use-chat/useChatRuntime.d.ts.map +1 -1
  9. package/dist/ui/use-chat/useChatRuntime.js.map +1 -1
  10. package/dist/ui/use-chat/useExternalHistory.d.ts.map +1 -1
  11. package/dist/ui/use-chat/useExternalHistory.js +137 -24
  12. package/dist/ui/use-chat/useExternalHistory.js.map +1 -1
  13. package/dist/ui/use-chat/useStreamingTiming.d.ts +11 -0
  14. package/dist/ui/use-chat/useStreamingTiming.d.ts.map +1 -0
  15. package/dist/ui/use-chat/useStreamingTiming.js +84 -0
  16. package/dist/ui/use-chat/useStreamingTiming.js.map +1 -0
  17. package/dist/ui/utils/convertMessage.d.ts +4 -4
  18. package/dist/ui/utils/convertMessage.d.ts.map +1 -1
  19. package/dist/ui/utils/convertMessage.js +84 -75
  20. package/dist/ui/utils/convertMessage.js.map +1 -1
  21. package/dist/ui/utils/sliceMessagesUntil.d.ts.map +1 -1
  22. package/dist/ui/utils/sliceMessagesUntil.js.map +1 -1
  23. package/dist/ui/utils/vercelAttachmentAdapter.js +1 -1
  24. package/dist/ui/utils/vercelAttachmentAdapter.js.map +1 -1
  25. package/package.json +14 -8
  26. package/src/ui/use-chat/useAISDKRuntime.test.ts +225 -0
  27. package/src/ui/use-chat/{useAISDKRuntime.tsx → useAISDKRuntime.ts} +34 -10
  28. package/src/ui/use-chat/useExternalHistory.ts +283 -0
  29. package/src/ui/use-chat/useStreamingTiming.ts +102 -0
  30. package/src/ui/utils/convertMessage.test.ts +125 -0
  31. package/src/ui/utils/convertMessage.ts +98 -82
  32. package/src/ui/utils/vercelAttachmentAdapter.ts +1 -1
  33. package/src/ui/use-chat/useExternalHistory.tsx +0 -134
  34. /package/src/ui/{getVercelAIMessages.tsx → getVercelAIMessages.ts} +0 -0
  35. /package/src/ui/use-chat/{AssistantChatTransport.tsx → AssistantChatTransport.ts} +0 -0
  36. /package/src/ui/use-chat/{useChatRuntime.tsx → useChatRuntime.ts} +0 -0
  37. /package/src/ui/utils/{sliceMessagesUntil.tsx → sliceMessagesUntil.ts} +0 -0
@@ -3,55 +3,79 @@ import { unstable_createMessageConverter, } from "@assistant-ui/react";
3
3
  function stripClosingDelimiters(json) {
4
4
  return json.replace(/[}\]"]+$/, "");
5
5
  }
6
- const convertParts = (message, metadata) => {
6
+ /**
7
+ * Resolves the interrupt fields for a tool call part.
8
+ *
9
+ * Two interrupt paths for tool approvals:
10
+ * 1. AI SDK server-side approval: approval-requested state with part.approval payload
11
+ * 2. Frontend tools: toolStatuses interrupt from context.human()
12
+ */
13
+ function getToolInterrupt(part, toolStatus) {
14
+ if (part.state === "approval-requested" && "approval" in part) {
15
+ return {
16
+ interrupt: {
17
+ type: "human",
18
+ payload: part.approval,
19
+ },
20
+ status: {
21
+ type: "requires-action",
22
+ reason: "interrupt",
23
+ },
24
+ };
25
+ }
26
+ if (toolStatus?.type === "interrupt") {
27
+ return {
28
+ interrupt: toolStatus.payload,
29
+ status: {
30
+ type: "requires-action",
31
+ reason: "interrupt",
32
+ },
33
+ };
34
+ }
35
+ return {};
36
+ }
37
+ function convertParts(message, metadata) {
7
38
  if (!message.parts || message.parts.length === 0) {
8
39
  return [];
9
40
  }
10
41
  const converted = message.parts
11
42
  .filter((p) => p.type !== "step-start" && p.type !== "file")
12
43
  .map((part) => {
13
- const type = part.type;
14
- // Handle text parts
15
- if (type === "text") {
44
+ if (part.type === "text") {
16
45
  return {
17
46
  type: "text",
18
47
  text: part.text,
19
48
  };
20
49
  }
21
- // Handle reasoning parts
22
- if (type === "reasoning") {
50
+ if (part.type === "reasoning") {
23
51
  return {
24
52
  type: "reasoning",
25
53
  text: part.text,
26
54
  };
27
55
  }
28
- // Handle tool parts (both static tool-* and dynamic-tool)
29
- // In AI SDK v6, isToolUIPart returns true for both static and dynamic tools
30
56
  if (isToolUIPart(part)) {
31
- // Use getToolName which works for both static and dynamic tools
32
57
  const toolName = getToolName(part);
33
58
  const toolCallId = part.toolCallId;
34
- // Extract args and result based on state
35
- let args = {};
59
+ const args = part.input || {};
36
60
  let result;
37
61
  let isError = false;
38
- if (part.state === "input-streaming" ||
39
- part.state === "input-available") {
40
- args = part.input || {};
41
- }
42
- else if (part.state === "output-available") {
43
- args = part.input || {};
62
+ if (part.state === "output-available") {
44
63
  result = part.output;
45
64
  }
46
65
  else if (part.state === "output-error") {
47
- args = part.input || {};
48
66
  isError = true;
49
67
  result = { error: part.errorText };
50
68
  }
69
+ else if (part.state === "output-denied") {
70
+ isError = true;
71
+ result = {
72
+ error: part.approval.reason ||
73
+ "Tool approval denied",
74
+ };
75
+ }
51
76
  let argsText = JSON.stringify(args);
52
77
  if (part.state === "input-streaming") {
53
- // the argsText is not complete, so we need to strip the closing delimiters
54
- // these are added by the AI SDK in fix-json
78
+ // strip closing delimiters added by the AI SDK's fix-json
55
79
  argsText = stripClosingDelimiters(argsText);
56
80
  }
57
81
  const toolStatus = metadata.toolStatuses?.[toolCallId];
@@ -63,17 +87,10 @@ const convertParts = (message, metadata) => {
63
87
  args,
64
88
  result,
65
89
  isError,
66
- ...(toolStatus?.type === "interrupt" && {
67
- interrupt: toolStatus.payload,
68
- status: {
69
- type: "requires-action",
70
- reason: "interrupt",
71
- },
72
- }),
90
+ ...getToolInterrupt(part, toolStatus),
73
91
  };
74
92
  }
75
- // Handle source-url parts
76
- if (type === "source-url") {
93
+ if (part.type === "source-url") {
77
94
  return {
78
95
  type: "source",
79
96
  sourceType: "url",
@@ -82,22 +99,18 @@ const convertParts = (message, metadata) => {
82
99
  title: part.title || "",
83
100
  };
84
101
  }
85
- // Handle source-document parts
86
- if (type === "source-document") {
87
- console.warn(`Source document part type ${type} is not yet supported in conversion`);
102
+ if (part.type === "source-document") {
103
+ console.warn("Source document parts are not yet supported in conversion");
88
104
  return null;
89
105
  }
90
- // Handle data-* parts (AI SDK v5 data parts)
91
- if (type.startsWith("data-")) {
92
- const name = type.substring(5);
106
+ if (part.type.startsWith("data-")) {
93
107
  return {
94
108
  type: "data",
95
- name,
109
+ name: part.type.substring(5),
96
110
  data: part.data,
97
111
  };
98
112
  }
99
- // For unsupported types, we'll skip them instead of throwing
100
- console.warn(`Unsupported message part type: ${type}`);
113
+ console.warn(`Unsupported message part type: ${part.type}`);
101
114
  return null;
102
115
  })
103
116
  .filter(Boolean);
@@ -110,60 +123,56 @@ const convertParts = (message, metadata) => {
110
123
  }
111
124
  return true;
112
125
  });
113
- };
126
+ }
114
127
  export const AISDKMessageConverter = unstable_createMessageConverter((message, metadata) => {
115
- // UIMessage doesn't have createdAt, so we'll use current date or undefined
116
128
  const createdAt = new Date();
129
+ const content = convertParts(message, metadata);
117
130
  switch (message.role) {
118
131
  case "user":
119
132
  return {
120
133
  role: "user",
121
134
  id: message.id,
122
135
  createdAt,
123
- content: convertParts(message, metadata),
136
+ content,
124
137
  attachments: message.parts
125
138
  ?.filter((p) => p.type === "file")
126
- .map((part, idx) => {
127
- return {
128
- id: idx.toString(),
129
- type: part.mediaType.startsWith("image/") ? "image" : "file",
130
- name: part.filename ?? "file",
131
- content: [
132
- part.mediaType.startsWith("image/")
133
- ? {
134
- type: "image",
135
- image: part.url,
136
- filename: part.filename,
137
- }
138
- : {
139
- type: "file",
140
- filename: part.filename,
141
- data: part.url,
142
- mimeType: part.mediaType,
143
- },
144
- ],
145
- contentType: part.mediaType ?? "unknown/unknown",
146
- status: { type: "complete" },
147
- };
148
- }),
139
+ .map((part, idx) => ({
140
+ id: idx.toString(),
141
+ type: part.mediaType.startsWith("image/") ? "image" : "file",
142
+ name: part.filename ?? "file",
143
+ content: [
144
+ part.mediaType.startsWith("image/")
145
+ ? {
146
+ type: "image",
147
+ image: part.url,
148
+ filename: part.filename,
149
+ }
150
+ : {
151
+ type: "file",
152
+ filename: part.filename,
153
+ data: part.url,
154
+ mimeType: part.mediaType,
155
+ },
156
+ ],
157
+ contentType: part.mediaType ?? "unknown/unknown",
158
+ status: { type: "complete" },
159
+ })),
149
160
  metadata: message.metadata,
150
161
  };
151
162
  case "system":
163
+ case "assistant": {
164
+ const timing = metadata.messageTiming?.[message.id];
152
165
  return {
153
- role: "system",
166
+ role: message.role,
154
167
  id: message.id,
155
168
  createdAt,
156
- content: convertParts(message, metadata),
157
- metadata: message.metadata,
158
- };
159
- case "assistant":
160
- return {
161
- role: "assistant",
162
- id: message.id,
163
- createdAt,
164
- content: convertParts(message, metadata),
165
- metadata: message.metadata,
169
+ content,
170
+ metadata: {
171
+ ...message.metadata,
172
+ ...(timing && { timing }),
173
+ },
166
174
  };
175
+ }
167
176
  default:
168
177
  console.warn(`Unsupported message role: ${message.role}`);
169
178
  return [];
@@ -1 +1 @@
1
- {"version":3,"file":"convertMessage.js","sourceRoot":"","sources":["../../../src/ui/utils/convertMessage.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"convertMessage.js","sourceRoot":"","sources":["../../../src/ui/utils/convertMessage.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"sliceMessagesUntil.d.ts","sourceRoot":"","sources":["../../../src/ui/utils/sliceMessagesUntil.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AAEpC,eAAO,MAAM,kBAAkB,GAAI,UAAU,SAAS,SAAS,GAAG,SAAS,EACzE,UAAU,UAAU,EAAE,EACtB,WAAW,MAAM,GAAG,IAAI,iBAezB,CAAC"}
1
+ {"version":3,"file":"sliceMessagesUntil.d.ts","sourceRoot":"","sources":["../../../src/ui/utils/sliceMessagesUntil.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AAEpC,eAAO,MAAM,kBAAkB,GAAI,UAAU,SAAS,SAAS,GAAG,SAAS,EACzE,UAAU,UAAU,EAAE,EACtB,WAAW,MAAM,GAAG,IAAI,iBAezB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"sliceMessagesUntil.js","sourceRoot":"","sources":["../../../src/ui/utils/sliceMessagesUntil.tsx"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAChC,QAAsB,EACtB,SAAwB,EACxB,EAAE;IACF,IAAI,SAAS,IAAI,IAAI;QAAE,OAAO,EAAE,CAAC;IAEjC,IAAI,UAAU,GAAG,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,SAAS,CAAC,CAAC;IAC/D,IAAI,UAAU,KAAK,CAAC,CAAC;QACnB,MAAM,IAAI,KAAK,CACb,4FAA4F,CAC7F,CAAC;IAEJ,OAAO,QAAQ,CAAC,UAAU,GAAG,CAAC,CAAC,EAAE,IAAI,KAAK,WAAW,EAAE,CAAC;QACtD,UAAU,EAAE,CAAC;IACf,CAAC;IAED,OAAO,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,GAAG,CAAC,CAAC,CAAC;AAC3C,CAAC,CAAC"}
1
+ {"version":3,"file":"sliceMessagesUntil.js","sourceRoot":"","sources":["../../../src/ui/utils/sliceMessagesUntil.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAChC,QAAsB,EACtB,SAAwB,EACxB,EAAE;IACF,IAAI,SAAS,IAAI,IAAI;QAAE,OAAO,EAAE,CAAC;IAEjC,IAAI,UAAU,GAAG,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,SAAS,CAAC,CAAC;IAC/D,IAAI,UAAU,KAAK,CAAC,CAAC;QACnB,MAAM,IAAI,KAAK,CACb,4FAA4F,CAC7F,CAAC;IAEJ,OAAO,QAAQ,CAAC,UAAU,GAAG,CAAC,CAAC,EAAE,IAAI,KAAK,WAAW,EAAE,CAAC;QACtD,UAAU,EAAE,CAAC;IACf,CAAC;IAED,OAAO,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,GAAG,CAAC,CAAC,CAAC;AAC3C,CAAC,CAAC"}
@@ -26,7 +26,7 @@ export const vercelAttachmentAdapter = {
26
26
  content: [
27
27
  {
28
28
  type: "file",
29
- mimeType: attachment.contentType,
29
+ mimeType: attachment.contentType ?? "",
30
30
  filename: attachment.name,
31
31
  data: await getFileDataURL(attachment.file),
32
32
  },
@@ -1 +1 @@
1
- {"version":3,"file":"vercelAttachmentAdapter.js","sourceRoot":"","sources":["../../../src/ui/utils/vercelAttachmentAdapter.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AAEhC,MAAM,cAAc,GAAG,CAAC,IAAU,EAAE,EAAE,CACpC,IAAI,OAAO,CAAS,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;IACtC,MAAM,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;IAEhC,MAAM,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,MAAgB,CAAC,CAAC;IACvD,MAAM,CAAC,OAAO,GAAG,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAE1C,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC;AAC7B,CAAC,CAAC,CAAC;AAEL,MAAM,CAAC,MAAM,uBAAuB,GAAsB;IACxD,MAAM,EACJ,wFAAwF;IAC1F,KAAK,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE;QAChB,OAAO;YACL,EAAE,EAAE,UAAU,EAAE;YAChB,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM;YACvD,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,IAAI;YACJ,WAAW,EAAE,IAAI,CAAC,IAAI;YACtB,OAAO,EAAE,EAAE;YACX,MAAM,EAAE,EAAE,IAAI,EAAE,iBAAiB,EAAE,MAAM,EAAE,eAAe,EAAE;SAC7D,CAAC;IACJ,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,UAAU;QACnB,OAAO;QACP,OAAO;YACL,GAAG,UAAU;YACb,MAAM,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE;YAC5B,OAAO,EAAE;gBACP;oBACE,IAAI,EAAE,MAAM;oBACZ,QAAQ,EAAE,UAAU,CAAC,WAAW;oBAChC,QAAQ,EAAE,UAAU,CAAC,IAAI;oBACzB,IAAI,EAAE,MAAM,cAAc,CAAC,UAAU,CAAC,IAAI,CAAC;iBAC5C;aACF;SACF,CAAC;IACJ,CAAC;IACD,KAAK,CAAC,MAAM;QACV,OAAO;IACT,CAAC;CACF,CAAC"}
1
+ {"version":3,"file":"vercelAttachmentAdapter.js","sourceRoot":"","sources":["../../../src/ui/utils/vercelAttachmentAdapter.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AAEhC,MAAM,cAAc,GAAG,CAAC,IAAU,EAAE,EAAE,CACpC,IAAI,OAAO,CAAS,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;IACtC,MAAM,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;IAEhC,MAAM,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,MAAgB,CAAC,CAAC;IACvD,MAAM,CAAC,OAAO,GAAG,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAE1C,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC;AAC7B,CAAC,CAAC,CAAC;AAEL,MAAM,CAAC,MAAM,uBAAuB,GAAsB;IACxD,MAAM,EACJ,wFAAwF;IAC1F,KAAK,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE;QAChB,OAAO;YACL,EAAE,EAAE,UAAU,EAAE;YAChB,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM;YACvD,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,IAAI;YACJ,WAAW,EAAE,IAAI,CAAC,IAAI;YACtB,OAAO,EAAE,EAAE;YACX,MAAM,EAAE,EAAE,IAAI,EAAE,iBAAiB,EAAE,MAAM,EAAE,eAAe,EAAE;SAC7D,CAAC;IACJ,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,UAAU;QACnB,OAAO;QACP,OAAO;YACL,GAAG,UAAU;YACb,MAAM,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE;YAC5B,OAAO,EAAE;gBACP;oBACE,IAAI,EAAE,MAAM;oBACZ,QAAQ,EAAE,UAAU,CAAC,WAAW,IAAI,EAAE;oBACtC,QAAQ,EAAE,UAAU,CAAC,IAAI;oBACzB,IAAI,EAAE,MAAM,cAAc,CAAC,UAAU,CAAC,IAAI,CAAC;iBAC5C;aACF;SACF,CAAC;IACJ,CAAC;IACD,KAAK,CAAC,MAAM;QACV,OAAO;IACT,CAAC;CACF,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assistant-ui/react-ai-sdk",
3
- "version": "1.3.7",
3
+ "version": "1.3.9",
4
4
  "description": "Vercel AI SDK adapter for assistant-ui",
5
5
  "keywords": [
6
6
  "ai-sdk",
@@ -31,12 +31,12 @@
31
31
  ],
32
32
  "sideEffects": false,
33
33
  "dependencies": {
34
- "@ai-sdk/react": "^3.0.75",
35
- "ai": "^6.0.73",
34
+ "@ai-sdk/react": "^3.0.100",
35
+ "ai": "^6.0.98",
36
36
  "zod": "^4.3.6"
37
37
  },
38
38
  "peerDependencies": {
39
- "@assistant-ui/react": "^0.12.10",
39
+ "@assistant-ui/react": "^0.12.12",
40
40
  "@types/react": "*",
41
41
  "assistant-cloud": "*",
42
42
  "react": "^18 || ^19"
@@ -50,12 +50,16 @@
50
50
  }
51
51
  },
52
52
  "devDependencies": {
53
+ "@testing-library/react": "^16.3.2",
53
54
  "@types/json-schema": "^7.0.15",
54
- "@types/react": "^19.2.13",
55
+ "@types/react": "^19.2.14",
56
+ "@types/react-dom": "^19.2.3",
57
+ "jsdom": "^28.1.0",
55
58
  "react": "^19.2.4",
56
- "@assistant-ui/react": "0.12.10",
59
+ "vitest": "^4.0.18",
60
+ "@assistant-ui/react": "0.12.12",
57
61
  "@assistant-ui/x-buildutils": "0.0.1",
58
- "assistant-stream": "0.3.2"
62
+ "assistant-stream": "0.3.4"
59
63
  },
60
64
  "publishConfig": {
61
65
  "access": "public",
@@ -71,6 +75,8 @@
71
75
  "url": "https://github.com/assistant-ui/assistant-ui/issues"
72
76
  },
73
77
  "scripts": {
74
- "build": "aui-build"
78
+ "build": "aui-build",
79
+ "test": "vitest run",
80
+ "test:watch": "vitest"
75
81
  }
76
82
  }
@@ -0,0 +1,225 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { act, renderHook, waitFor } from "@testing-library/react";
4
+ import { beforeEach, describe, expect, it, vi } from "vitest";
5
+
6
+ // Mock only the sibling module that requires AUI store context (not available
7
+ // in isolation). Every other dependency — useExternalStoreRuntime,
8
+ // useToolInvocations, the message converter — runs for real.
9
+ vi.mock("./useExternalHistory", () => ({
10
+ useExternalHistory: vi.fn(() => false),
11
+ toExportedMessageRepository: vi.fn(),
12
+ }));
13
+
14
+ import { useAISDKRuntime } from "./useAISDKRuntime";
15
+
16
+ const createChatHelpers = (messages: any[] = []) => {
17
+ let currentMessages = [...messages];
18
+
19
+ const chatHelpers: any = {
20
+ status: "ready",
21
+ error: null,
22
+ messages: currentMessages,
23
+ setMessages: vi.fn((next: any) => {
24
+ currentMessages =
25
+ typeof next === "function" ? next(currentMessages) : [...next];
26
+ chatHelpers.messages = currentMessages;
27
+ return currentMessages;
28
+ }),
29
+ sendMessage: vi.fn().mockResolvedValue(undefined),
30
+ regenerate: vi.fn().mockResolvedValue(undefined),
31
+ addToolResult: vi.fn(),
32
+ addToolOutput: vi.fn(),
33
+ stop: vi.fn(),
34
+ };
35
+
36
+ return chatHelpers;
37
+ };
38
+
39
+ describe("useAISDKRuntime", () => {
40
+ beforeEach(() => {
41
+ vi.clearAllMocks();
42
+ });
43
+
44
+ it("sends a new user message through the runtime", async () => {
45
+ const chat = createChatHelpers();
46
+
47
+ const { result } = renderHook(() => useAISDKRuntime(chat));
48
+
49
+ act(() => {
50
+ result.current.thread.append({
51
+ role: "user",
52
+ content: [{ type: "text", text: "hello" }],
53
+ });
54
+ });
55
+
56
+ await waitFor(() => {
57
+ expect(chat.sendMessage).toHaveBeenCalledTimes(1);
58
+ });
59
+
60
+ expect(chat.sendMessage).toHaveBeenCalledWith(
61
+ expect.objectContaining({
62
+ role: "user",
63
+ parts: expect.arrayContaining([
64
+ expect.objectContaining({ type: "text", text: "hello" }),
65
+ ]),
66
+ }),
67
+ expect.anything(),
68
+ );
69
+ });
70
+
71
+ it("forwards runConfig as metadata when sending", async () => {
72
+ const chat = createChatHelpers();
73
+
74
+ const { result } = renderHook(() => useAISDKRuntime(chat));
75
+
76
+ act(() => {
77
+ result.current.thread.append({
78
+ role: "user",
79
+ content: [{ type: "text", text: "hello" }],
80
+ runConfig: { custom: { model: "gpt-4.1" } },
81
+ });
82
+ });
83
+
84
+ await waitFor(() => {
85
+ expect(chat.sendMessage).toHaveBeenCalledWith(expect.anything(), {
86
+ metadata: { custom: { model: "gpt-4.1" } },
87
+ });
88
+ });
89
+ });
90
+
91
+ it("cancels pending tool calls before sending a new message", async () => {
92
+ const chat = createChatHelpers([
93
+ {
94
+ id: "a1",
95
+ role: "assistant",
96
+ parts: [
97
+ {
98
+ type: "tool-weather",
99
+ toolCallId: "tc-1",
100
+ state: "input-available",
101
+ input: { city: "NYC" },
102
+ },
103
+ {
104
+ type: "tool-weather",
105
+ toolCallId: "tc-2",
106
+ state: "output-available",
107
+ input: { city: "LA" },
108
+ output: { temp: 70 },
109
+ },
110
+ ],
111
+ },
112
+ ]);
113
+
114
+ const { result } = renderHook(() => useAISDKRuntime(chat));
115
+
116
+ // Wait for the runtime to process the initial messages
117
+ await waitFor(() => {
118
+ expect(result.current.thread.getState().messages.length).toBeGreaterThan(
119
+ 0,
120
+ );
121
+ });
122
+
123
+ act(() => {
124
+ result.current.thread.append({
125
+ role: "user",
126
+ content: [{ type: "text", text: "continue" }],
127
+ });
128
+ });
129
+
130
+ await waitFor(() => {
131
+ expect(chat.sendMessage).toHaveBeenCalledTimes(1);
132
+ });
133
+
134
+ // Pending tool (tc-1) should be marked as cancelled
135
+ expect(chat.messages[0].parts[0].state).toBe("output-error");
136
+ expect(chat.messages[0].parts[0].errorText).toBe(
137
+ "User cancelled tool call by sending a new message.",
138
+ );
139
+ // Completed tool (tc-2) should remain unchanged
140
+ expect(chat.messages[0].parts[1].state).toBe("output-available");
141
+ });
142
+
143
+ it("edit slices history to parentId and sends the edited message", async () => {
144
+ const chat = createChatHelpers([
145
+ { id: "u1", role: "user", parts: [{ type: "text", text: "first" }] },
146
+ {
147
+ id: "a1",
148
+ role: "assistant",
149
+ parts: [{ type: "text", text: "first-answer" }],
150
+ },
151
+ { id: "u2", role: "user", parts: [{ type: "text", text: "second" }] },
152
+ {
153
+ id: "a2",
154
+ role: "assistant",
155
+ parts: [{ type: "text", text: "second-answer" }],
156
+ },
157
+ ]);
158
+
159
+ const { result } = renderHook(() => useAISDKRuntime(chat));
160
+
161
+ await waitFor(() => {
162
+ expect(result.current.thread.getState().messages.length).toBe(4);
163
+ });
164
+
165
+ // Append with parentId != last message triggers onEdit
166
+ act(() => {
167
+ result.current.thread.append({
168
+ role: "user",
169
+ parentId: "u1",
170
+ content: [{ type: "text", text: "rewrite first" }],
171
+ runConfig: { custom: { temperature: 0.2 } },
172
+ });
173
+ });
174
+
175
+ await waitFor(() => {
176
+ expect(chat.sendMessage).toHaveBeenCalledTimes(1);
177
+ });
178
+
179
+ // sliceMessagesUntil("u1") keeps u1 + following assistant messages (a1)
180
+ expect(chat.messages.map((m: any) => m.id)).toEqual(["u1", "a1"]);
181
+ expect(chat.sendMessage).toHaveBeenCalledWith(
182
+ expect.objectContaining({ role: "user" }),
183
+ { metadata: { custom: { temperature: 0.2 } } },
184
+ );
185
+ });
186
+
187
+ it("reload slices history and regenerates with metadata", async () => {
188
+ const chat = createChatHelpers([
189
+ { id: "u1", role: "user", parts: [{ type: "text", text: "first" }] },
190
+ {
191
+ id: "a1",
192
+ role: "assistant",
193
+ parts: [{ type: "text", text: "first-answer" }],
194
+ },
195
+ { id: "u2", role: "user", parts: [{ type: "text", text: "second" }] },
196
+ {
197
+ id: "a2",
198
+ role: "assistant",
199
+ parts: [{ type: "text", text: "second-answer" }],
200
+ },
201
+ ]);
202
+
203
+ const { result } = renderHook(() => useAISDKRuntime(chat));
204
+
205
+ await waitFor(() => {
206
+ expect(result.current.thread.getState().messages.length).toBe(4);
207
+ });
208
+
209
+ act(() => {
210
+ result.current.thread.startRun({
211
+ parentId: "u1",
212
+ runConfig: { custom: { maxTokens: 100 } },
213
+ });
214
+ });
215
+
216
+ await waitFor(() => {
217
+ expect(chat.regenerate).toHaveBeenCalledTimes(1);
218
+ });
219
+
220
+ expect(chat.messages.map((m: any) => m.id)).toEqual(["u1", "a1"]);
221
+ expect(chat.regenerate).toHaveBeenCalledWith({
222
+ metadata: { custom: { maxTokens: 100 } },
223
+ });
224
+ });
225
+ });
@@ -10,6 +10,7 @@ import {
10
10
  type AssistantRuntime,
11
11
  type ThreadMessage,
12
12
  type MessageFormatAdapter,
13
+ type MessageFormatItem,
13
14
  type MessageFormatRepository,
14
15
  useRuntimeAdapters,
15
16
  INTERNAL,
@@ -30,6 +31,7 @@ import {
30
31
  useExternalHistory,
31
32
  toExportedMessageRepository,
32
33
  } from "./useExternalHistory";
34
+ import { useStreamingTiming } from "./useStreamingTiming";
33
35
 
34
36
  export type CustomToCreateMessageFunction = <
35
37
  UI_MESSAGE extends UIMessage = UIMessage,
@@ -76,15 +78,18 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
76
78
  chatHelpers.status === "streaming" ||
77
79
  hasExecutingTools;
78
80
 
81
+ const messageTiming = useStreamingTiming(chatHelpers.messages, isRunning);
82
+
79
83
  const messages = AISDKMessageConverter.useThreadMessages({
80
84
  isRunning,
81
85
  messages: chatHelpers.messages,
82
86
  metadata: useMemo(
83
87
  () => ({
84
88
  toolStatuses,
89
+ messageTiming,
85
90
  ...(chatHelpers.error && { error: chatHelpers.error.message }),
86
91
  }),
87
- [toolStatuses, chatHelpers.error],
92
+ [toolStatuses, messageTiming, chatHelpers.error],
88
93
  ),
89
94
  });
90
95
 
@@ -174,20 +179,39 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
174
179
  .flat(),
175
180
  ),
176
181
  onExportExternalState: (): MessageFormatRepository<UI_MESSAGE> => {
177
- // Export the thread's MessageRepository
178
182
  const exported = runtimeRef.current.thread.export();
179
183
 
180
- // Convert each ThreadMessage back to its original UI_MESSAGE format
184
+ const expandedMessages: MessageFormatItem<UI_MESSAGE>[] = [];
185
+ const lastInnerIdMap = new Map<string, string>();
186
+
187
+ for (const item of exported.messages) {
188
+ const innerMessages = getExternalStoreMessages<UI_MESSAGE>(
189
+ item.message,
190
+ );
191
+ let parentId =
192
+ item.parentId != null
193
+ ? (lastInnerIdMap.get(item.parentId) ?? item.parentId)
194
+ : null;
195
+ for (const innerMessage of innerMessages) {
196
+ expandedMessages.push({ parentId, message: innerMessage });
197
+ parentId = aiSDKV6FormatAdapter.getId(innerMessage as UIMessage);
198
+ }
199
+ if (innerMessages.length > 0) {
200
+ lastInnerIdMap.set(
201
+ item.message.id,
202
+ aiSDKV6FormatAdapter.getId(
203
+ innerMessages[innerMessages.length - 1]! as UIMessage,
204
+ ),
205
+ );
206
+ }
207
+ }
208
+
181
209
  const result: MessageFormatRepository<UI_MESSAGE> = {
182
- messages: exported.messages.map((item) => ({
183
- parentId: item.parentId,
184
- message: getExternalStoreMessages<UI_MESSAGE>(item.message)[0]!,
185
- })),
210
+ messages: expandedMessages,
186
211
  };
187
212
 
188
- // Only include headId if it's defined
189
- if (exported.headId !== undefined) {
190
- result.headId = exported.headId;
213
+ if (exported.headId != null) {
214
+ result.headId = lastInnerIdMap.get(exported.headId) ?? exported.headId;
191
215
  }
192
216
 
193
217
  return result;