@assistant-ui/ai-sdk 0.0.4 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/aiSDKFormatAdapter.d.ts +2 -8
- package/dist/adapters/aiSDKFormatAdapter.js +1 -25
- package/dist/converters/convertMessage.d.ts.map +1 -1
- package/dist/converters/convertMessage.js +9 -4
- package/dist/converters/convertMessage.js.map +1 -1
- package/dist/runtime/AISDKThreads.d.ts.map +1 -1
- package/dist/runtime/AISDKThreads.js +90 -65
- package/dist/runtime/AISDKThreads.js.map +1 -1
- package/dist/runtime/sdkIdentity.d.ts +6 -0
- package/dist/runtime/sdkIdentity.d.ts.map +1 -0
- package/dist/runtime/sdkIdentity.js +9 -0
- package/dist/runtime/sdkIdentity.js.map +1 -0
- package/dist/runtime/useAISDKRuntime.d.ts.map +1 -1
- package/dist/runtime/useAISDKRuntime.js +8 -1
- package/dist/runtime/useAISDKRuntime.js.map +1 -1
- package/dist/runtime/useChatRuntime.d.ts.map +1 -1
- package/dist/runtime/useChatRuntime.js +7 -2
- package/dist/runtime/useChatRuntime.js.map +1 -1
- package/dist/runtime/useChatThread.js +1 -1
- package/dist/runtime/useChatThread.js.map +1 -1
- package/dist/runtime/useExternalHistory.d.ts.map +1 -1
- package/dist/runtime/useExternalHistory.js +4 -1
- package/dist/runtime/useExternalHistory.js.map +1 -1
- package/dist/tools/generativeTools.js +7 -4
- package/dist/tools/generativeTools.js.map +1 -1
- package/dist/usage.d.ts +8 -0
- package/dist/usage.d.ts.map +1 -1
- package/dist/usage.js +8 -0
- package/dist/usage.js.map +1 -1
- package/package.json +12 -12
- package/src/adapters/aiSDKFormatAdapter.ts +4 -41
- package/src/converters/convertMessage.test.ts +116 -0
- package/src/converters/convertMessage.ts +39 -18
- package/src/runtime/AISDKChat.integration.test.tsx +48 -27
- package/src/runtime/AISDKThreads.cloud.test.ts +12 -3
- package/src/runtime/AISDKThreads.test.ts +131 -8
- package/src/runtime/AISDKThreads.ts +31 -3
- package/src/runtime/__tests__/controlled-transport.ts +21 -0
- package/src/runtime/sdkIdentity.ts +9 -0
- package/src/runtime/useAISDKRuntime.test.ts +39 -1
- package/src/runtime/useAISDKRuntime.ts +17 -1
- package/src/runtime/useChatRuntime.integration.test.tsx +137 -4
- package/src/runtime/useChatRuntime.test.ts +0 -1
- package/src/runtime/useChatRuntime.ts +3 -1
- package/src/runtime/useChatThread.test.ts +74 -0
- package/src/runtime/useChatThread.ts +1 -1
- package/src/runtime/useExternalHistory.test.ts +29 -0
- package/src/runtime/useExternalHistory.ts +7 -1
- package/src/tools/generativeTools.test.ts +79 -0
- package/src/tools/generativeTools.ts +7 -8
- package/src/transport/AssistantChatTransport.test.ts +1 -9
- package/src/usage.ts +8 -0
- package/dist/adapters/aiSDKFormatAdapter.d.ts.map +0 -1
- package/dist/adapters/aiSDKFormatAdapter.js.map +0 -1
|
@@ -188,7 +188,7 @@ export const useChatThread = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
|
188
188
|
id,
|
|
189
189
|
isMainThread,
|
|
190
190
|
getThreadListItem,
|
|
191
|
-
stopOnClientDestroy =
|
|
191
|
+
stopOnClientDestroy = true,
|
|
192
192
|
chat: externalChat,
|
|
193
193
|
messageRepositoryInstance,
|
|
194
194
|
} = env;
|
|
@@ -780,6 +780,35 @@ describe("useExternalHistory persistence", () => {
|
|
|
780
780
|
],
|
|
781
781
|
expect.any(Object),
|
|
782
782
|
);
|
|
783
|
+
expect(reportTelemetry.mock.calls[0]![1]).toMatchObject({
|
|
784
|
+
message: expect.objectContaining({
|
|
785
|
+
id: "assistant-a",
|
|
786
|
+
status: { type: "complete", reason: "stop" },
|
|
787
|
+
}),
|
|
788
|
+
});
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
it("reports a run that failed before any assistant message", async () => {
|
|
792
|
+
const { append, reportTelemetry, runCycle, flush } =
|
|
793
|
+
createPersistenceHarness(true);
|
|
794
|
+
const failed = createAssistantMessage(
|
|
795
|
+
{
|
|
796
|
+
type: "incomplete",
|
|
797
|
+
reason: "error",
|
|
798
|
+
error: { code: "AI_APICallError", message: "upstream failed" },
|
|
799
|
+
},
|
|
800
|
+
[],
|
|
801
|
+
);
|
|
802
|
+
|
|
803
|
+
await runCycle([failed]);
|
|
804
|
+
await flush();
|
|
805
|
+
|
|
806
|
+
expect(append).not.toHaveBeenCalled();
|
|
807
|
+
expect(reportTelemetry).toHaveBeenCalledTimes(1);
|
|
808
|
+
expect(reportTelemetry).toHaveBeenCalledWith(
|
|
809
|
+
[],
|
|
810
|
+
expect.objectContaining({ message: failed }),
|
|
811
|
+
);
|
|
783
812
|
});
|
|
784
813
|
|
|
785
814
|
it("restores deferred telemetry for reloaded paused messages", async () => {
|
|
@@ -143,6 +143,9 @@ export const useExternalHistory = <TMessage>(
|
|
|
143
143
|
|
|
144
144
|
const remoteId = optionalThreadListItem()?.getState().remoteId;
|
|
145
145
|
if (!remoteId) {
|
|
146
|
+
// History loads asynchronously against the thread list item; without a
|
|
147
|
+
// remote id there is nothing to await, so the flag settles here.
|
|
148
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
146
149
|
setHasLoaded(true);
|
|
147
150
|
return aui.subscribe(() => {
|
|
148
151
|
if (optionalThreadListItem()?.getState().remoteId) {
|
|
@@ -368,7 +371,10 @@ export const useExternalHistory = <TMessage>(
|
|
|
368
371
|
|
|
369
372
|
if (deferredTelemetryIds.current.has(message.id) && isTerminal) {
|
|
370
373
|
deferredTelemetryIds.current.delete(message.id);
|
|
371
|
-
adapter.reportTelemetry?.(batchItems,
|
|
374
|
+
adapter.reportTelemetry?.(batchItems, {
|
|
375
|
+
...telemetryOptions,
|
|
376
|
+
message,
|
|
377
|
+
});
|
|
372
378
|
}
|
|
373
379
|
}
|
|
374
380
|
})
|
|
@@ -139,6 +139,30 @@ describe("AISDKToolkit", () => {
|
|
|
139
139
|
mocks.createMCPClient.mockReset();
|
|
140
140
|
});
|
|
141
141
|
|
|
142
|
+
it("preserves prototype-named MCP tools", async () => {
|
|
143
|
+
const prototypeTool = { inputSchema: {} };
|
|
144
|
+
mocks.tools.mockResolvedValue(
|
|
145
|
+
Object.fromEntries([["__proto__", prototypeTool]]),
|
|
146
|
+
);
|
|
147
|
+
mocks.createMCPClient.mockResolvedValue({
|
|
148
|
+
tools: mocks.tools,
|
|
149
|
+
close: mocks.close,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const toolkit = new AISDKToolkit({
|
|
153
|
+
toolkit: {
|
|
154
|
+
docs: {
|
|
155
|
+
type: "mcp",
|
|
156
|
+
server: { type: "http", url: "http://localhost:3001/mcp" },
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const tools = await toolkit.tools();
|
|
162
|
+
expect(Object.hasOwn(tools, "__proto__")).toBe(true);
|
|
163
|
+
expect(tools["__proto__"]).toBe(prototypeTool);
|
|
164
|
+
});
|
|
165
|
+
|
|
142
166
|
it("loads MCP tools through pooled clients", async () => {
|
|
143
167
|
mocks.tools.mockResolvedValue({ echo: { inputSchema: {} } });
|
|
144
168
|
mocks.createMCPClient.mockResolvedValue({
|
|
@@ -454,6 +478,61 @@ describe("AISDKToolkit", () => {
|
|
|
454
478
|
}
|
|
455
479
|
});
|
|
456
480
|
|
|
481
|
+
it("does not evict a replacement client after an older listing timeout", async () => {
|
|
482
|
+
vi.useFakeTimers();
|
|
483
|
+
const oldClient = {
|
|
484
|
+
tools: vi.fn(() => never()),
|
|
485
|
+
close: vi.fn().mockResolvedValue(undefined),
|
|
486
|
+
};
|
|
487
|
+
const replacementClient = {
|
|
488
|
+
tools: vi.fn().mockResolvedValue({ echo: { inputSchema: {} } }),
|
|
489
|
+
close: vi.fn().mockResolvedValue(undefined),
|
|
490
|
+
};
|
|
491
|
+
mocks.createMCPClient
|
|
492
|
+
.mockResolvedValueOnce(oldClient)
|
|
493
|
+
.mockResolvedValue(replacementClient);
|
|
494
|
+
|
|
495
|
+
const toolkit = new AISDKToolkit({
|
|
496
|
+
toolkit: {
|
|
497
|
+
docs: {
|
|
498
|
+
type: "mcp",
|
|
499
|
+
server: {
|
|
500
|
+
type: "http",
|
|
501
|
+
url: "http://localhost:3001/mcp",
|
|
502
|
+
connectionTimeout: 100,
|
|
503
|
+
},
|
|
504
|
+
},
|
|
505
|
+
},
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
try {
|
|
509
|
+
const first = toolkit.tools();
|
|
510
|
+
const firstRejection = expect(first).rejects.toThrow(
|
|
511
|
+
/timed out while listing tools/,
|
|
512
|
+
);
|
|
513
|
+
await vi.advanceTimersByTimeAsync(50);
|
|
514
|
+
|
|
515
|
+
const second = toolkit.tools();
|
|
516
|
+
const secondRejection = expect(second).rejects.toThrow(
|
|
517
|
+
/timed out while listing tools/,
|
|
518
|
+
);
|
|
519
|
+
await vi.advanceTimersByTimeAsync(50);
|
|
520
|
+
await firstRejection;
|
|
521
|
+
|
|
522
|
+
await expect(toolkit.tools()).resolves.toHaveProperty("echo");
|
|
523
|
+
expect(mocks.createMCPClient).toHaveBeenCalledTimes(2);
|
|
524
|
+
|
|
525
|
+
await vi.advanceTimersByTimeAsync(50);
|
|
526
|
+
await secondRejection;
|
|
527
|
+
|
|
528
|
+
await expect(toolkit.tools()).resolves.toHaveProperty("echo");
|
|
529
|
+
expect(mocks.createMCPClient).toHaveBeenCalledTimes(2);
|
|
530
|
+
expect(oldClient.close).toHaveBeenCalledTimes(1);
|
|
531
|
+
} finally {
|
|
532
|
+
vi.useRealTimers();
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
|
|
457
536
|
it("includes the MCP toolkit entry name when listing tools fails", async () => {
|
|
458
537
|
const error = new Error("list failed");
|
|
459
538
|
mocks.tools.mockRejectedValue(error);
|
|
@@ -227,11 +227,8 @@ export class AISDKToolkit {
|
|
|
227
227
|
)
|
|
228
228
|
.map(async ([name, tool]) => {
|
|
229
229
|
const startedAt = Date.now();
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
tool.server,
|
|
233
|
-
startedAt,
|
|
234
|
-
).catch((error: unknown) => {
|
|
230
|
+
const clientPromise = this.#mcpClient(name, tool.server, startedAt);
|
|
231
|
+
const client = await clientPromise.catch((error: unknown) => {
|
|
235
232
|
if (error instanceof MCPConnectionTimeoutError) throw error;
|
|
236
233
|
throw toMcpToolkitError(name, "connect", error);
|
|
237
234
|
});
|
|
@@ -245,8 +242,10 @@ export class AISDKToolkit {
|
|
|
245
242
|
return [name, tool, tools] as const;
|
|
246
243
|
} catch (error) {
|
|
247
244
|
if (error instanceof MCPConnectionTimeoutError) {
|
|
248
|
-
this.#mcpClients.
|
|
249
|
-
|
|
245
|
+
if (this.#mcpClients.get(name) === clientPromise) {
|
|
246
|
+
this.#mcpClients.delete(name);
|
|
247
|
+
void client.close().catch(() => {});
|
|
248
|
+
}
|
|
250
249
|
throw error;
|
|
251
250
|
}
|
|
252
251
|
throw toMcpToolkitError(name, "list tools", error);
|
|
@@ -254,7 +253,7 @@ export class AISDKToolkit {
|
|
|
254
253
|
}),
|
|
255
254
|
);
|
|
256
255
|
|
|
257
|
-
const tools
|
|
256
|
+
const tools = Object.create(null) as ToolSet;
|
|
258
257
|
const toolSources = new Map<string, string>();
|
|
259
258
|
for (const [serverName, mcpTool, toolSet] of toolSets) {
|
|
260
259
|
for (const [toolName, tool] of Object.entries(toolSet)) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { UIMessage } from "ai";
|
|
2
|
-
import {
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
3
|
|
|
4
4
|
import { RESUMABLE_STREAM_ID_HEADER } from "./resumable";
|
|
5
5
|
import { AssistantChatTransport } from "./AssistantChatTransport";
|
|
@@ -26,10 +26,6 @@ const sendMessagesOptions = {
|
|
|
26
26
|
};
|
|
27
27
|
|
|
28
28
|
describe("AssistantChatTransport.prepareSendMessagesRequest", () => {
|
|
29
|
-
beforeEach(() => {
|
|
30
|
-
vi.clearAllMocks();
|
|
31
|
-
});
|
|
32
|
-
|
|
33
29
|
it("passes the initialized remote thread id to prepareSendMessagesRequest as options.id", async () => {
|
|
34
30
|
const threadListItem = createThreadListItem("remote-thread-id");
|
|
35
31
|
const captured: { id: unknown } = { id: undefined };
|
|
@@ -209,10 +205,6 @@ const wrappedFetchOf = (
|
|
|
209
205
|
).fetch;
|
|
210
206
|
|
|
211
207
|
describe("AssistantChatTransport resumable fetch wrapper", () => {
|
|
212
|
-
beforeEach(() => {
|
|
213
|
-
vi.clearAllMocks();
|
|
214
|
-
});
|
|
215
|
-
|
|
216
208
|
it("passes a 204 with a non-null empty body through untouched (WebKit)", async () => {
|
|
217
209
|
const response = nullBodyStatusWithBody(204);
|
|
218
210
|
const fetchMock = vi.fn(async () => response);
|
package/src/usage.ts
CHANGED
|
@@ -163,6 +163,14 @@ function findLatestMessageWithUsage(
|
|
|
163
163
|
return undefined;
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
+
/**
|
|
167
|
+
* Reads token usage from the newest assistant message that reports any.
|
|
168
|
+
*
|
|
169
|
+
* A route attaches usage through the AI SDK's `messageMetadata` option. Because
|
|
170
|
+
* a thread message carries a fixed metadata shape, the converter moves every
|
|
171
|
+
* other key the route returns into `metadata.custom`, which is where this hook
|
|
172
|
+
* looks.
|
|
173
|
+
*/
|
|
166
174
|
export function useThreadTokenUsage(): ThreadTokenUsage | undefined {
|
|
167
175
|
const msg = useAuiState((s) => findLatestMessageWithUsage(s.thread.messages));
|
|
168
176
|
return useMemo(() => getThreadMessageTokenUsage(msg), [msg]);
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"aiSDKFormatAdapter.d.ts","names":[],"sources":["../../src/adapters/aiSDKFormatAdapter.ts"],"mappings":";;;KAQY,qBAAqB,KAAK;cAEzB,sBAAsB,qBACjC,WACA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"aiSDKFormatAdapter.js","names":["UIMessage","MessageFormatAdapter","MessageFormatItem","MessageStorageEntry","AISDKStorageFormat","Omit","aiSDKV6FormatAdapter","format","encode","message","id","parts","decode","stored","parentId","parent_id","content","getId"],"sources":["../../src/adapters/aiSDKFormatAdapter.ts"],"sourcesContent":["import type { UIMessage } from \"ai\";\nimport type {\n MessageFormatAdapter,\n MessageFormatItem,\n MessageStorageEntry,\n} from \"@assistant-ui/core\";\n\n// Storage format for AI SDK messages - just the UIMessage\nexport type AISDKStorageFormat = Omit<UIMessage, \"id\">;\n\nexport const aiSDKV6FormatAdapter: MessageFormatAdapter<\n UIMessage,\n AISDKStorageFormat\n> = {\n format: \"ai-sdk/v6\",\n\n encode({\n message: { id, parts, ...message },\n }: MessageFormatItem<UIMessage>): AISDKStorageFormat {\n return {\n ...message,\n parts,\n };\n },\n\n decode(\n stored: MessageStorageEntry<AISDKStorageFormat>,\n ): MessageFormatItem<UIMessage> {\n return {\n parentId: stored.parent_id,\n message: {\n id: stored.id,\n ...stored.content,\n },\n };\n },\n\n getId(message: UIMessage): string {\n return message.id;\n },\n};\n"],"mappings":";AAUA,MAAaM,uBAGT;CACFC,QAAQ;CAERC,OAAO,EACLC,SAAS,EAAEC,IAAIC,OAAO,GAAGF,aAC0B;EACnD,OAAO;GACL,GAAGA;GACHE;EACF;CACF;CAEAC,OACEC,QAC8B;EAC9B,OAAO;GACLC,UAAUD,OAAOE;GACjBN,SAAS;IACPC,IAAIG,OAAOH;IACX,GAAGG,OAAOG;GACZ;EACF;CACF;CAEAC,MAAMR,SAA4B;EAChC,OAAOA,QAAQC;CACjB;AACF"}
|