@assistant-ui/react-ai-sdk 1.3.32 → 1.3.35
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/client.d.ts +9 -0
- package/dist/client.js +8 -0
- package/dist/frontendTools.d.ts +1 -1
- package/dist/frontendTools.js.map +1 -1
- package/dist/generativeTools.d.ts +18 -4
- package/dist/generativeTools.d.ts.map +1 -1
- package/dist/generativeTools.js +14 -5
- package/dist/generativeTools.js.map +1 -1
- package/dist/index.d.ts +3 -4
- package/dist/index.js +3 -4
- package/dist/index.native.d.ts +9 -0
- package/dist/index.native.js +8 -0
- package/dist/injectQuoteContext.js.map +1 -1
- package/dist/mcp-stdio.node.d.ts +2 -0
- package/dist/mcp-stdio.node.js +2 -0
- package/dist/mcp-stdio.unsupported.d.ts +7 -0
- package/dist/mcp-stdio.unsupported.d.ts.map +1 -0
- package/dist/mcp-stdio.unsupported.js +11 -0
- package/dist/mcp-stdio.unsupported.js.map +1 -0
- package/dist/modelContentEnvelope.d.ts +2 -1
- package/dist/modelContentEnvelope.d.ts.map +1 -1
- package/dist/toolOutputConversion.d.ts +1 -1
- package/dist/ui/resumable.d.ts +2 -1
- package/dist/ui/resumable.d.ts.map +1 -1
- package/dist/ui/resumable.js +1 -1
- package/dist/ui/use-chat/AssistantChatTransport.js +2 -2
- package/dist/ui/use-chat/AssistantChatTransport.js.map +1 -1
- package/dist/ui/use-chat/useAISDKRuntime.d.ts +10 -0
- package/dist/ui/use-chat/useAISDKRuntime.d.ts.map +1 -1
- package/dist/ui/use-chat/useAISDKRuntime.js +13 -4
- package/dist/ui/use-chat/useAISDKRuntime.js.map +1 -1
- package/dist/ui/use-chat/useChatRuntime.d.ts +1 -0
- package/dist/ui/use-chat/useChatRuntime.d.ts.map +1 -1
- package/dist/ui/use-chat/useChatRuntime.js +3 -2
- package/dist/ui/use-chat/useChatRuntime.js.map +1 -1
- package/dist/ui/use-chat/useExternalHistory.d.ts +4 -1
- package/dist/ui/use-chat/useExternalHistory.d.ts.map +1 -1
- package/dist/ui/use-chat/useExternalHistory.js +25 -1
- package/dist/ui/use-chat/useExternalHistory.js.map +1 -1
- package/dist/ui/use-chat/useStreamingTiming.js +1 -1
- package/dist/ui/utils/convertMessage.d.ts +2 -2
- package/dist/ui/utils/convertMessage.d.ts.map +1 -1
- package/dist/ui/utils/convertMessage.js +5 -2
- package/dist/ui/utils/convertMessage.js.map +1 -1
- package/dist/ui/utils/sliceMessagesUntil.js.map +1 -1
- package/dist/ui/utils/toCreateMessage.js.map +1 -1
- package/dist/usage.js.map +1 -1
- package/package.json +28 -12
- package/src/client.ts +18 -0
- package/src/generativeTools.test.ts +29 -47
- package/src/generativeTools.ts +18 -4
- package/src/index.native.ts +3 -0
- package/src/index.ts +1 -16
- package/src/mcp-stdio.node.ts +1 -0
- package/src/mcp-stdio.unsupported.ts +12 -0
- package/src/ui/use-chat/useAISDKRuntime.test.ts +104 -1
- package/src/ui/use-chat/useAISDKRuntime.ts +31 -1
- package/src/ui/use-chat/useChatRuntime.ts +3 -0
- package/src/ui/use-chat/useExternalHistory.ts +30 -1
- package/src/ui/utils/convertMessage.test.ts +36 -5
- package/src/ui/utils/convertMessage.ts +9 -1
- package/dist/assistant-stream/dist/core/AssistantStream.d.ts +0 -1
- package/dist/assistant-stream/dist/core/AssistantStreamChunk.d.ts +0 -1
- package/dist/assistant-stream/dist/core/accumulators/AssistantMessageStream.d.ts +0 -1
- package/dist/assistant-stream/dist/core/accumulators/assistant-message-accumulator.d.ts +0 -1
- package/dist/assistant-stream/dist/core/modules/assistant-stream.d.ts +0 -1
- package/dist/assistant-stream/dist/core/modules/text.d.ts +0 -1
- package/dist/assistant-stream/dist/core/modules/tool-call.d.ts +0 -1
- package/dist/assistant-stream/dist/core/serialization/PlainText.d.ts +0 -1
- package/dist/assistant-stream/dist/core/serialization/assistant-transport/AssistantTransport.d.ts +0 -1
- package/dist/assistant-stream/dist/core/serialization/data-stream/DataStream.d.ts +0 -1
- package/dist/assistant-stream/dist/core/serialization/ui-message-stream/UIMessageStream.d.ts +0 -1
- package/dist/assistant-stream/dist/core/tool/ToolExecutionStream.d.ts +0 -1
- package/dist/assistant-stream/dist/core/tool/schema-utils.d.ts +0 -15
- package/dist/assistant-stream/dist/core/tool/schema-utils.d.ts.map +0 -1
- package/dist/assistant-stream/dist/core/tool/schema-utils.js +0 -59
- package/dist/assistant-stream/dist/core/tool/schema-utils.js.map +0 -1
- package/dist/assistant-stream/dist/core/tool/tool-types.d.ts +0 -69
- package/dist/assistant-stream/dist/core/tool/tool-types.d.ts.map +0 -1
- package/dist/assistant-stream/dist/core/tool/toolResultStream.d.ts +0 -1
- package/dist/assistant-stream/dist/core/utils/stream/AssistantMetaTransformStream.d.ts +0 -1
- package/dist/assistant-stream/dist/core/utils/stream/AssistantTransformStream.d.ts +0 -1
- package/dist/assistant-stream/dist/core/utils/types.d.ts +0 -1
- package/dist/assistant-stream/dist/index.d.ts +0 -2
- package/dist/assistant-stream/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.ts +0 -140
- package/dist/assistant-stream/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.ts.map +0 -1
- package/dist/assistant-stream/dist/resumable/createResumableAssistantStreamResponse.d.ts +0 -6
- package/dist/assistant-stream/dist/resumable/createResumableAssistantStreamResponse.d.ts.map +0 -1
- package/dist/assistant-stream/dist/resumable/createResumableAssistantStreamResponse.js +0 -6
- package/dist/assistant-stream/dist/resumable/createResumableAssistantStreamResponse.js.map +0 -1
- package/dist/assistant-stream/dist/resumable/index.d.ts +0 -1
- package/dist/assistant-stream/dist/utils/json/json-value.d.ts +0 -10
- package/dist/assistant-stream/dist/utils/json/json-value.d.ts.map +0 -1
- package/dist/assistant-stream/dist/utils.d.ts +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
import { AISDKToolkit
|
|
2
|
+
import { AISDKToolkit } from "./generativeTools";
|
|
3
3
|
import { wrapModelContentEnvelope } from "./modelContentEnvelope";
|
|
4
4
|
|
|
5
5
|
const mocks = vi.hoisted(() => ({
|
|
@@ -19,20 +19,9 @@ vi.mock("@ai-sdk/mcp/mcp-stdio", () => ({
|
|
|
19
19
|
})),
|
|
20
20
|
}));
|
|
21
21
|
|
|
22
|
-
describe("
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
mocks.tools.mockReset();
|
|
26
|
-
mocks.createMCPClient.mockReset();
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it("merges frontend tools with toolkit tools", () => {
|
|
30
|
-
const toolSet = generativeTools({
|
|
31
|
-
frontendTools: {
|
|
32
|
-
clientTool: {
|
|
33
|
-
parameters: { type: "object", properties: {} },
|
|
34
|
-
},
|
|
35
|
-
},
|
|
22
|
+
describe("AISDKToolkit.tools()", () => {
|
|
23
|
+
it("merges frontend tools with toolkit tools", async () => {
|
|
24
|
+
const toolSet = await new AISDKToolkit({
|
|
36
25
|
toolkit: {
|
|
37
26
|
serverTool: {
|
|
38
27
|
type: "backend",
|
|
@@ -41,6 +30,12 @@ describe("generativeTools", () => {
|
|
|
41
30
|
execute: async () => "ok",
|
|
42
31
|
} as never,
|
|
43
32
|
},
|
|
33
|
+
}).tools({
|
|
34
|
+
frontend: {
|
|
35
|
+
clientTool: {
|
|
36
|
+
parameters: { type: "object", properties: {} },
|
|
37
|
+
},
|
|
38
|
+
},
|
|
44
39
|
});
|
|
45
40
|
|
|
46
41
|
expect(toolSet.clientTool).toBeDefined();
|
|
@@ -48,8 +43,8 @@ describe("generativeTools", () => {
|
|
|
48
43
|
expect(toolSet.serverTool?.execute).toBeTypeOf("function");
|
|
49
44
|
});
|
|
50
45
|
|
|
51
|
-
it("keeps a flat toolkit tool named tools", () => {
|
|
52
|
-
const toolSet =
|
|
46
|
+
it("keeps a flat toolkit tool named tools", async () => {
|
|
47
|
+
const toolSet = await new AISDKToolkit({
|
|
53
48
|
toolkit: {
|
|
54
49
|
tools: {
|
|
55
50
|
type: "backend",
|
|
@@ -58,27 +53,14 @@ describe("generativeTools", () => {
|
|
|
58
53
|
execute: async () => "ok",
|
|
59
54
|
} as never,
|
|
60
55
|
},
|
|
61
|
-
});
|
|
56
|
+
}).tools();
|
|
62
57
|
|
|
63
58
|
expect(toolSet.tools?.description).toBe("Actually a tool, not config");
|
|
64
59
|
expect(toolSet.tools?.execute).toBeTypeOf("function");
|
|
65
60
|
});
|
|
66
61
|
|
|
67
|
-
it("
|
|
68
|
-
|
|
69
|
-
generativeTools({
|
|
70
|
-
toolkit: {
|
|
71
|
-
docs: {
|
|
72
|
-
type: "mcp",
|
|
73
|
-
server: { type: "http", url: "http://localhost:3001/mcp" },
|
|
74
|
-
},
|
|
75
|
-
},
|
|
76
|
-
}),
|
|
77
|
-
).toThrow(/requires AISDKToolkit/);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it("converts provider tools without an execute function", () => {
|
|
81
|
-
const toolSet = generativeTools({
|
|
62
|
+
it("converts provider tools without an execute function", async () => {
|
|
63
|
+
const toolSet = await new AISDKToolkit({
|
|
82
64
|
toolkit: {
|
|
83
65
|
web_search: {
|
|
84
66
|
type: "provider",
|
|
@@ -86,7 +68,7 @@ describe("generativeTools", () => {
|
|
|
86
68
|
args: { searchContextSize: "low" },
|
|
87
69
|
},
|
|
88
70
|
},
|
|
89
|
-
});
|
|
71
|
+
}).tools();
|
|
90
72
|
|
|
91
73
|
expect(toolSet.web_search).toMatchObject({
|
|
92
74
|
type: "provider",
|
|
@@ -97,8 +79,8 @@ describe("generativeTools", () => {
|
|
|
97
79
|
expect(toolSet.web_search).not.toHaveProperty("execute");
|
|
98
80
|
});
|
|
99
81
|
|
|
100
|
-
it("forwards provider tool parameters and providerOptions when present", () => {
|
|
101
|
-
const toolSet =
|
|
82
|
+
it("forwards provider tool parameters and providerOptions when present", async () => {
|
|
83
|
+
const toolSet = await new AISDKToolkit({
|
|
102
84
|
toolkit: {
|
|
103
85
|
web_search: {
|
|
104
86
|
type: "provider",
|
|
@@ -116,7 +98,7 @@ describe("generativeTools", () => {
|
|
|
116
98
|
},
|
|
117
99
|
},
|
|
118
100
|
},
|
|
119
|
-
});
|
|
101
|
+
}).tools();
|
|
120
102
|
|
|
121
103
|
expect(toolSet.web_search).toMatchObject({
|
|
122
104
|
type: "provider",
|
|
@@ -129,8 +111,8 @@ describe("generativeTools", () => {
|
|
|
129
111
|
expect(toolSet.web_search).toHaveProperty("inputSchema");
|
|
130
112
|
});
|
|
131
113
|
|
|
132
|
-
it("forwards explicit false supportsDeferredResults", () => {
|
|
133
|
-
const toolSet =
|
|
114
|
+
it("forwards explicit false supportsDeferredResults", async () => {
|
|
115
|
+
const toolSet = await new AISDKToolkit({
|
|
134
116
|
toolkit: {
|
|
135
117
|
web_search: {
|
|
136
118
|
type: "provider",
|
|
@@ -139,7 +121,7 @@ describe("generativeTools", () => {
|
|
|
139
121
|
supportsDeferredResults: false,
|
|
140
122
|
},
|
|
141
123
|
},
|
|
142
|
-
});
|
|
124
|
+
}).tools();
|
|
143
125
|
|
|
144
126
|
expect(toolSet.web_search).toMatchObject({
|
|
145
127
|
supportsDeferredResults: false,
|
|
@@ -324,18 +306,18 @@ describe("AISDKToolkit", () => {
|
|
|
324
306
|
});
|
|
325
307
|
});
|
|
326
308
|
|
|
327
|
-
describe("
|
|
309
|
+
describe("AISDKToolkit toModelOutput", () => {
|
|
328
310
|
const createWeatherTools = (toModelOutput?: any) =>
|
|
329
|
-
|
|
311
|
+
new AISDKToolkit({
|
|
330
312
|
toolkit: {
|
|
331
313
|
get_weather: {
|
|
332
314
|
...(toModelOutput && { toModelOutput }),
|
|
333
315
|
},
|
|
334
316
|
} as any,
|
|
335
|
-
});
|
|
317
|
+
}).tools();
|
|
336
318
|
|
|
337
319
|
it("adapts assistant-ui model content parts to the AI SDK tool output shape", async () => {
|
|
338
|
-
const tools = createWeatherTools(({ output }: any) => [
|
|
320
|
+
const tools = await createWeatherTools(({ output }: any) => [
|
|
339
321
|
{ type: "text", text: `Weather card displayed: ${output.location}` },
|
|
340
322
|
]);
|
|
341
323
|
|
|
@@ -353,7 +335,7 @@ describe("generativeTools toModelOutput", () => {
|
|
|
353
335
|
|
|
354
336
|
it("uses stored model content envelopes without re-running the custom projector", async () => {
|
|
355
337
|
let called = false;
|
|
356
|
-
const tools = createWeatherTools(() => {
|
|
338
|
+
const tools = await createWeatherTools(() => {
|
|
357
339
|
called = true;
|
|
358
340
|
return [{ type: "text", text: "recomputed" }];
|
|
359
341
|
});
|
|
@@ -374,7 +356,7 @@ describe("generativeTools toModelOutput", () => {
|
|
|
374
356
|
});
|
|
375
357
|
|
|
376
358
|
it("falls back to default model output when no custom projector is defined", async () => {
|
|
377
|
-
const tools = createWeatherTools();
|
|
359
|
+
const tools = await createWeatherTools();
|
|
378
360
|
|
|
379
361
|
const output = await tools.get_weather!.toModelOutput!({
|
|
380
362
|
toolCallId: "tc-weather",
|
|
@@ -389,7 +371,7 @@ describe("generativeTools toModelOutput", () => {
|
|
|
389
371
|
});
|
|
390
372
|
|
|
391
373
|
it("uses stored model content envelopes when no custom projector is defined", async () => {
|
|
392
|
-
const tools = createWeatherTools();
|
|
374
|
+
const tools = await createWeatherTools();
|
|
393
375
|
|
|
394
376
|
const output = await tools.get_weather!.toModelOutput!({
|
|
395
377
|
toolCallId: "tc-weather",
|
package/src/generativeTools.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsonSchema, type ToolSet } from "ai";
|
|
2
2
|
import type { MCPClient, MCPClientConfig } from "@ai-sdk/mcp";
|
|
3
3
|
import { createMCPClient } from "@ai-sdk/mcp";
|
|
4
|
-
import { Experimental_StdioMCPTransport } from "
|
|
4
|
+
import { Experimental_StdioMCPTransport } from "#mcp-stdio";
|
|
5
5
|
import {
|
|
6
6
|
toJSONSchema,
|
|
7
7
|
type Tool,
|
|
@@ -31,6 +31,11 @@ const neverAbort = new AbortController().signal;
|
|
|
31
31
|
const parametersToInputSchema = (parameters: Tool["parameters"] | undefined) =>
|
|
32
32
|
jsonSchema(parameters ? toJSONSchema(parameters) : EMPTY_SCHEMA);
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* @deprecated Options for the deprecated {@link generativeTools}. Use
|
|
36
|
+
* {@link AISDKToolkit} with {@link AISDKToolkitOptions} /
|
|
37
|
+
* {@link AISDKToolkitToolsOptions} instead.
|
|
38
|
+
*/
|
|
34
39
|
export interface GenerativeToolsOptions {
|
|
35
40
|
/**
|
|
36
41
|
* The server build of a generative toolkit (schema + server `execute`). Typed
|
|
@@ -66,16 +71,25 @@ export type AISDKToolkitToolsOptions = {
|
|
|
66
71
|
* resolves to the server build — schema + `execute`, with `render` stripped) and
|
|
67
72
|
* pass it here. Tools without an `execute` are still exposed to the model but
|
|
68
73
|
* left for the client to fulfill. `frontendTools` lets the client contribute
|
|
69
|
-
* tools that aren't in the static toolkit.
|
|
70
|
-
*
|
|
74
|
+
* tools that aren't in the static toolkit.
|
|
75
|
+
*
|
|
76
|
+
* @deprecated Use {@link AISDKToolkit} instead:
|
|
77
|
+
* `new AISDKToolkit({ toolkit }).tools({ frontend })`. It is a strict superset
|
|
78
|
+
* (it also opens MCP server connections), so it replaces `generativeTools`
|
|
79
|
+
* everywhere. The `frontendTools` option is named `frontend` on `.tools()`, and
|
|
80
|
+
* `.tools()` is async. `generativeTools` will be removed in a future version.
|
|
71
81
|
*
|
|
72
82
|
* @example
|
|
73
83
|
* ```ts
|
|
84
|
+
* // Define once at module scope so any MCP connections pool across requests.
|
|
85
|
+
* const aiToolkit = new AISDKToolkit({ toolkit: docsToolkit });
|
|
86
|
+
*
|
|
87
|
+
* // In your route handler:
|
|
74
88
|
* const { tools } = await req.json();
|
|
75
89
|
* streamText({
|
|
76
90
|
* model,
|
|
77
91
|
* messages,
|
|
78
|
-
* tools:
|
|
92
|
+
* tools: await aiToolkit.tools({ frontend: tools }),
|
|
79
93
|
* });
|
|
80
94
|
* ```
|
|
81
95
|
*/
|
package/src/index.ts
CHANGED
|
@@ -1,18 +1,6 @@
|
|
|
1
1
|
/// <reference types="@assistant-ui/core/react" />
|
|
2
2
|
|
|
3
|
-
export
|
|
4
|
-
export { useChatRuntime } from "./ui/use-chat/useChatRuntime";
|
|
5
|
-
export type { UseChatRuntimeOptions } from "./ui/use-chat/useChatRuntime";
|
|
6
|
-
export { AssistantChatTransport } from "./ui/use-chat/AssistantChatTransport";
|
|
7
|
-
export {
|
|
8
|
-
RESUMABLE_STREAM_ID_HEADER,
|
|
9
|
-
createResumableSessionStorage,
|
|
10
|
-
} from "./ui/resumable";
|
|
11
|
-
export type {
|
|
12
|
-
AssistantChatResumableOptions,
|
|
13
|
-
ResumableClientStorage,
|
|
14
|
-
} from "./ui/resumable";
|
|
15
|
-
export { frontendTools } from "./frontendTools";
|
|
3
|
+
export * from "./client";
|
|
16
4
|
export {
|
|
17
5
|
AISDKToolkit,
|
|
18
6
|
generativeTools,
|
|
@@ -20,6 +8,3 @@ export {
|
|
|
20
8
|
type AISDKToolkitToolsOptions,
|
|
21
9
|
type GenerativeToolsOptions,
|
|
22
10
|
} from "./generativeTools";
|
|
23
|
-
export { injectQuoteContext } from "./injectQuoteContext";
|
|
24
|
-
export type { ThreadTokenUsage, TokenUsageExtractableMessage } from "./usage";
|
|
25
|
-
export { getThreadMessageTokenUsage, useThreadTokenUsage } from "./usage";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Experimental_StdioMCPTransport } from "@ai-sdk/mcp/mcp-stdio";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Experimental_StdioMCPTransport as NodeStdioMCPTransport } from "@ai-sdk/mcp/mcp-stdio";
|
|
2
|
+
|
|
3
|
+
class UnsupportedStdioMCPTransport {
|
|
4
|
+
constructor() {
|
|
5
|
+
throw new Error(
|
|
6
|
+
"stdio MCP transport requires a runtime that can spawn a subprocess, such as Node, Bun, or Deno (with --allow-run). Use an HTTP or SSE MCP server config in browser, React Native, edge, or worker runtimes.",
|
|
7
|
+
);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const Experimental_StdioMCPTransport =
|
|
12
|
+
UnsupportedStdioMCPTransport as unknown as typeof NodeStdioMCPTransport;
|
|
@@ -8,10 +8,14 @@ import { validateUIMessages } from "ai";
|
|
|
8
8
|
// in isolation). Every other dependency — useExternalStoreRuntime,
|
|
9
9
|
// useToolInvocations, the message converter — runs for real.
|
|
10
10
|
vi.mock("./useExternalHistory", () => ({
|
|
11
|
-
useExternalHistory: vi.fn(() =>
|
|
11
|
+
useExternalHistory: vi.fn(() => ({
|
|
12
|
+
isLoading: false,
|
|
13
|
+
deleteMessage: vi.fn().mockResolvedValue(undefined),
|
|
14
|
+
})),
|
|
12
15
|
toExportedMessageRepository: vi.fn(),
|
|
13
16
|
}));
|
|
14
17
|
|
|
18
|
+
import { useExternalHistory } from "./useExternalHistory";
|
|
15
19
|
import { useAISDKRuntime } from "./useAISDKRuntime";
|
|
16
20
|
|
|
17
21
|
const createChatHelpers = (messages: any[] = []) => {
|
|
@@ -37,9 +41,19 @@ const createChatHelpers = (messages: any[] = []) => {
|
|
|
37
41
|
return chatHelpers;
|
|
38
42
|
};
|
|
39
43
|
|
|
44
|
+
const textOf = (message: any): string =>
|
|
45
|
+
message.content
|
|
46
|
+
.filter((part: any) => part.type === "text")
|
|
47
|
+
.map((part: any) => part.text)
|
|
48
|
+
.join("|");
|
|
49
|
+
|
|
40
50
|
describe("useAISDKRuntime", () => {
|
|
41
51
|
beforeEach(() => {
|
|
42
52
|
vi.clearAllMocks();
|
|
53
|
+
vi.mocked(useExternalHistory).mockReturnValue({
|
|
54
|
+
isLoading: false,
|
|
55
|
+
deleteMessage: vi.fn().mockResolvedValue(undefined),
|
|
56
|
+
});
|
|
43
57
|
});
|
|
44
58
|
|
|
45
59
|
it("sends a new user message through the runtime", async () => {
|
|
@@ -271,6 +285,45 @@ describe("useAISDKRuntime", () => {
|
|
|
271
285
|
);
|
|
272
286
|
});
|
|
273
287
|
|
|
288
|
+
it("deletes only the selected message from AI SDK state", async () => {
|
|
289
|
+
const deleteMessage = vi.fn().mockResolvedValue(undefined);
|
|
290
|
+
vi.mocked(useExternalHistory).mockReturnValue({
|
|
291
|
+
isLoading: false,
|
|
292
|
+
deleteMessage,
|
|
293
|
+
});
|
|
294
|
+
const chat = createChatHelpers([
|
|
295
|
+
{ id: "u1", role: "user", parts: [{ type: "text", text: "first" }] },
|
|
296
|
+
{
|
|
297
|
+
id: "a1",
|
|
298
|
+
role: "assistant",
|
|
299
|
+
parts: [{ type: "text", text: "first-answer" }],
|
|
300
|
+
},
|
|
301
|
+
{ id: "u2", role: "user", parts: [{ type: "text", text: "second" }] },
|
|
302
|
+
{
|
|
303
|
+
id: "a2",
|
|
304
|
+
role: "assistant",
|
|
305
|
+
parts: [{ type: "text", text: "second-answer" }],
|
|
306
|
+
},
|
|
307
|
+
]);
|
|
308
|
+
|
|
309
|
+
const { result } = renderHook(() => useAISDKRuntime(chat));
|
|
310
|
+
|
|
311
|
+
await waitFor(() => {
|
|
312
|
+
expect(result.current.thread.getState().messages).toHaveLength(4);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
await act(async () => {
|
|
316
|
+
result.current.thread.getMessageById("u2").delete();
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
expect(deleteMessage).toHaveBeenCalledWith("u2");
|
|
320
|
+
expect(chat.messages.map((message: any) => message.id)).toEqual([
|
|
321
|
+
"u1",
|
|
322
|
+
"a1",
|
|
323
|
+
"a2",
|
|
324
|
+
]);
|
|
325
|
+
});
|
|
326
|
+
|
|
274
327
|
it("edit slices history to parentId and sends the edited message", async () => {
|
|
275
328
|
const chat = createChatHelpers([
|
|
276
329
|
{ id: "u1", role: "user", parts: [{ type: "text", text: "first" }] },
|
|
@@ -439,4 +492,54 @@ describe("useAISDKRuntime", () => {
|
|
|
439
492
|
const { result } = renderHook(() => useAISDKRuntime(chat, { suggestions }));
|
|
440
493
|
expect(result.current.thread.getState().suggestions).toEqual(suggestions);
|
|
441
494
|
});
|
|
495
|
+
|
|
496
|
+
it("merges consecutive assistant messages into one turn by default", async () => {
|
|
497
|
+
const chat = createChatHelpers([
|
|
498
|
+
{ id: "u1", role: "user", parts: [{ type: "text", text: "hi" }] },
|
|
499
|
+
{ id: "a1", role: "assistant", parts: [{ type: "text", text: "first" }] },
|
|
500
|
+
{
|
|
501
|
+
id: "a2",
|
|
502
|
+
role: "assistant",
|
|
503
|
+
parts: [{ type: "text", text: "second" }],
|
|
504
|
+
},
|
|
505
|
+
]);
|
|
506
|
+
|
|
507
|
+
const { result } = renderHook(() => useAISDKRuntime(chat));
|
|
508
|
+
|
|
509
|
+
await waitFor(() => {
|
|
510
|
+
expect(result.current.thread.getState().messages.length).toBe(2);
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
const messages = result.current.thread.getState().messages;
|
|
514
|
+
expect(messages.map((m: any) => m.role)).toEqual(["user", "assistant"]);
|
|
515
|
+
expect(textOf(messages[1])).toBe("first|second");
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
it('keeps consecutive assistant messages separate when joinStrategy is "none"', async () => {
|
|
519
|
+
const chat = createChatHelpers([
|
|
520
|
+
{ id: "u1", role: "user", parts: [{ type: "text", text: "hi" }] },
|
|
521
|
+
{ id: "a1", role: "assistant", parts: [{ type: "text", text: "first" }] },
|
|
522
|
+
{
|
|
523
|
+
id: "a2",
|
|
524
|
+
role: "assistant",
|
|
525
|
+
parts: [{ type: "text", text: "second" }],
|
|
526
|
+
},
|
|
527
|
+
]);
|
|
528
|
+
|
|
529
|
+
const { result } = renderHook(() =>
|
|
530
|
+
useAISDKRuntime(chat, { joinStrategy: "none" }),
|
|
531
|
+
);
|
|
532
|
+
|
|
533
|
+
await waitFor(() => {
|
|
534
|
+
expect(result.current.thread.getState().messages.length).toBe(3);
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
const messages = result.current.thread.getState().messages;
|
|
538
|
+
expect(messages.map((m: any) => m.role)).toEqual([
|
|
539
|
+
"user",
|
|
540
|
+
"assistant",
|
|
541
|
+
"assistant",
|
|
542
|
+
]);
|
|
543
|
+
expect(messages.slice(1).map(textOf)).toEqual(["first", "second"]);
|
|
544
|
+
});
|
|
442
545
|
});
|
|
@@ -6,6 +6,7 @@ import { isToolUIPart, generateId } from "ai";
|
|
|
6
6
|
import {
|
|
7
7
|
useExternalStoreRuntime,
|
|
8
8
|
useRuntimeAdapters,
|
|
9
|
+
type JoinStrategy,
|
|
9
10
|
} from "@assistant-ui/core/react";
|
|
10
11
|
import type { ToolExecutionStatus } from "@assistant-ui/core";
|
|
11
12
|
import type {
|
|
@@ -82,6 +83,15 @@ export type AISDKRuntimeAdapter = ExternalStoreSharedOptions & {
|
|
|
82
83
|
* (for example, an SSE reconnect endpoint keyed by turn id).
|
|
83
84
|
*/
|
|
84
85
|
onResume?: ExternalStoreAdapter["onResume"];
|
|
86
|
+
/**
|
|
87
|
+
* How consecutive assistant messages are rendered.
|
|
88
|
+
*
|
|
89
|
+
* `"concat-content"` (the default) merges them into a single thread message.
|
|
90
|
+
* `"none"` keeps each assistant message as its own thread message, which is
|
|
91
|
+
* useful when a backend persists proactive or consecutive assistant messages
|
|
92
|
+
* as separate entries.
|
|
93
|
+
*/
|
|
94
|
+
joinStrategy?: JoinStrategy | undefined;
|
|
85
95
|
};
|
|
86
96
|
|
|
87
97
|
export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
@@ -93,6 +103,7 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
|
93
103
|
toCreateMessage: customToCreateMessage,
|
|
94
104
|
cancelPendingToolCallsOnSend = true,
|
|
95
105
|
onResume,
|
|
106
|
+
joinStrategy,
|
|
96
107
|
} = adapter;
|
|
97
108
|
const contextAdapters = useRuntimeAdapters();
|
|
98
109
|
const [toolStatuses, setToolStatuses] = useState<
|
|
@@ -126,6 +137,7 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
|
126
137
|
const messages = AISDKMessageConverter.useThreadMessages({
|
|
127
138
|
isRunning,
|
|
128
139
|
messages: chatHelpers.messages,
|
|
140
|
+
joinStrategy,
|
|
129
141
|
metadata: useMemo(
|
|
130
142
|
() => ({
|
|
131
143
|
toolStatuses,
|
|
@@ -146,7 +158,7 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
|
146
158
|
},
|
|
147
159
|
}));
|
|
148
160
|
|
|
149
|
-
const isLoading = useExternalHistory(
|
|
161
|
+
const { isLoading, deleteMessage: deleteHistoryMessage } = useExternalHistory(
|
|
150
162
|
runtimeRef,
|
|
151
163
|
adapters?.history ?? contextAdapters?.history,
|
|
152
164
|
AISDKMessageConverter.toThreadMessages as (
|
|
@@ -303,6 +315,24 @@ export const useAISDKRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
|
303
315
|
metadata: message.runConfig,
|
|
304
316
|
});
|
|
305
317
|
},
|
|
318
|
+
onDelete: async (messageId) => {
|
|
319
|
+
const threadMessages = runtimeRef.current.thread.getState().messages;
|
|
320
|
+
const messageIndex = threadMessages.findIndex(
|
|
321
|
+
(message) => message.id === messageId,
|
|
322
|
+
);
|
|
323
|
+
if (messageIndex === -1) return;
|
|
324
|
+
|
|
325
|
+
await deleteHistoryMessage(messageId);
|
|
326
|
+
|
|
327
|
+
const deleteIds = new Set(
|
|
328
|
+
getExternalStoreMessages<UI_MESSAGE>(threadMessages[messageIndex]!).map(
|
|
329
|
+
(message) => message.id,
|
|
330
|
+
),
|
|
331
|
+
);
|
|
332
|
+
chatHelpers.setMessages((current) =>
|
|
333
|
+
current.filter((message) => !deleteIds.has(message.id)),
|
|
334
|
+
);
|
|
335
|
+
},
|
|
306
336
|
onReload: async (parentId: string | null, config) => {
|
|
307
337
|
lastRunConfigRef.current = config.runConfig;
|
|
308
338
|
const newMessages = sliceMessagesUntil(chatHelpers.messages, parentId);
|
|
@@ -29,6 +29,7 @@ export type UseChatRuntimeOptions<UI_MESSAGE extends UIMessage = UIMessage> =
|
|
|
29
29
|
adapters?: AISDKRuntimeAdapter["adapters"] | undefined;
|
|
30
30
|
toCreateMessage?: CustomToCreateMessageFunction;
|
|
31
31
|
onResume?: AISDKRuntimeAdapter["onResume"];
|
|
32
|
+
joinStrategy?: AISDKRuntimeAdapter["joinStrategy"];
|
|
32
33
|
};
|
|
33
34
|
|
|
34
35
|
const useDynamicChatTransport = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
@@ -78,6 +79,7 @@ const useChatThreadRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
|
78
79
|
unstable_capabilities: _unstable_capabilities,
|
|
79
80
|
suggestions: _suggestions,
|
|
80
81
|
onResume,
|
|
82
|
+
joinStrategy,
|
|
81
83
|
...chatOptions
|
|
82
84
|
} = options ?? {};
|
|
83
85
|
// peel guard: any shared key left in `chatOptions` collapses this to `never`
|
|
@@ -103,6 +105,7 @@ const useChatThreadRuntime = <UI_MESSAGE extends UIMessage = UIMessage>(
|
|
|
103
105
|
...pickExternalStoreSharedOptions(options ?? {}),
|
|
104
106
|
...(toCreateMessage && { toCreateMessage }),
|
|
105
107
|
...(onResume && { onResume }),
|
|
108
|
+
...(joinStrategy && { joinStrategy }),
|
|
106
109
|
});
|
|
107
110
|
|
|
108
111
|
if (transport instanceof AssistantChatTransport) {
|
|
@@ -292,5 +292,34 @@ export const useExternalHistory = <TMessage>(
|
|
|
292
292
|
};
|
|
293
293
|
}, [formatAdapter, storageFormatAdapter, runtimeRef]);
|
|
294
294
|
|
|
295
|
-
|
|
295
|
+
const deleteMessage = useCallback(
|
|
296
|
+
async (messageId: string) => {
|
|
297
|
+
if (!formatAdapter?.delete) return;
|
|
298
|
+
|
|
299
|
+
const messages = runtimeRef.current.thread.getState().messages;
|
|
300
|
+
const messageIndex = messages.findIndex((m) => m.id === messageId);
|
|
301
|
+
if (messageIndex === -1) return;
|
|
302
|
+
|
|
303
|
+
const previousInnerMessages = messages
|
|
304
|
+
.slice(0, messageIndex)
|
|
305
|
+
.flatMap(getExternalStoreMessages<TMessage>);
|
|
306
|
+
let parentId = previousInnerMessages.at(-1)
|
|
307
|
+
? storageFormatAdapter.getId(previousInnerMessages.at(-1)!)
|
|
308
|
+
: null;
|
|
309
|
+
const itemsToDelete = getExternalStoreMessages<TMessage>(
|
|
310
|
+
messages[messageIndex]!,
|
|
311
|
+
).map((message) => {
|
|
312
|
+
const item = { parentId, message };
|
|
313
|
+
parentId = storageFormatAdapter.getId(message);
|
|
314
|
+
return item;
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
await formatAdapter.delete(itemsToDelete);
|
|
318
|
+
|
|
319
|
+
historyIds.current.delete(messageId);
|
|
320
|
+
},
|
|
321
|
+
[formatAdapter, runtimeRef, storageFormatAdapter],
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
return { isLoading, deleteMessage };
|
|
296
325
|
};
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import
|
|
2
|
+
import {
|
|
3
|
+
getPartialJsonObjectFieldState,
|
|
4
|
+
type ReadonlyJSONObject,
|
|
5
|
+
} from "assistant-stream/utils";
|
|
3
6
|
import {
|
|
4
7
|
AISDKMessageConverter,
|
|
5
8
|
type AISDKMessageConverterMetadata,
|
|
@@ -365,6 +368,34 @@ describe("AISDKMessageConverter", () => {
|
|
|
365
368
|
expect(toolCall?.argsText).toBe('{"city":"NYC');
|
|
366
369
|
});
|
|
367
370
|
|
|
371
|
+
it("attaches partial-JSON meta marking the trailing streaming field", () => {
|
|
372
|
+
const converted = AISDKMessageConverter.toThreadMessages([
|
|
373
|
+
{
|
|
374
|
+
id: "a1",
|
|
375
|
+
role: "assistant",
|
|
376
|
+
parts: [
|
|
377
|
+
{
|
|
378
|
+
type: "tool-weather",
|
|
379
|
+
toolCallId: "tc-1",
|
|
380
|
+
state: "input-streaming",
|
|
381
|
+
input: { city: "NYC", units: "F" },
|
|
382
|
+
},
|
|
383
|
+
],
|
|
384
|
+
} as any,
|
|
385
|
+
]);
|
|
386
|
+
|
|
387
|
+
const toolCall = converted[0]?.content.find(
|
|
388
|
+
(part): part is any => part.type === "tool-call",
|
|
389
|
+
);
|
|
390
|
+
expect(toolCall?.args).toMatchObject({ city: "NYC", units: "F" });
|
|
391
|
+
expect(getPartialJsonObjectFieldState(toolCall!.args, ["city"])).toBe(
|
|
392
|
+
"complete",
|
|
393
|
+
);
|
|
394
|
+
expect(getPartialJsonObjectFieldState(toolCall!.args, ["units"])).toBe(
|
|
395
|
+
"partial",
|
|
396
|
+
);
|
|
397
|
+
});
|
|
398
|
+
|
|
368
399
|
it("keeps observed key order from streaming snapshots for final tool args", () => {
|
|
369
400
|
const metadata: AISDKMessageConverterMetadata = {
|
|
370
401
|
toolArgsKeyOrderCache: new Map<string, Map<string, string[]>>(),
|
|
@@ -518,19 +549,19 @@ describe("AISDKMessageConverter", () => {
|
|
|
518
549
|
|
|
519
550
|
const first = convertWithInput({ city: "NYC" });
|
|
520
551
|
expect(first?.argsText).toBe('{"city":"NYC');
|
|
521
|
-
expect(first?.args).
|
|
552
|
+
expect(first?.args).toMatchObject({ city: "NYC" });
|
|
522
553
|
|
|
523
554
|
const dropped = convertWithInput(null);
|
|
524
555
|
expect(dropped?.argsText).toBe('{"city":"NYC');
|
|
525
|
-
expect(dropped?.args).
|
|
556
|
+
expect(dropped?.args).toMatchObject({ city: "NYC" });
|
|
526
557
|
|
|
527
558
|
const undef = convertWithInput(undefined);
|
|
528
559
|
expect(undef?.argsText).toBe('{"city":"NYC');
|
|
529
|
-
expect(undef?.args).
|
|
560
|
+
expect(undef?.args).toMatchObject({ city: "NYC" });
|
|
530
561
|
|
|
531
562
|
const grown = convertWithInput({ city: "NYC", units: "F" });
|
|
532
563
|
expect(grown?.argsText).toBe('{"city":"NYC","units":"F');
|
|
533
|
-
expect(grown?.args).
|
|
564
|
+
expect(grown?.args).toMatchObject({ city: "NYC", units: "F" });
|
|
534
565
|
});
|
|
535
566
|
|
|
536
567
|
it("preserves last good input across terminal state transitions", () => {
|
|
@@ -15,7 +15,10 @@ import {
|
|
|
15
15
|
type ThreadMessageLike,
|
|
16
16
|
type McpAppMetadata,
|
|
17
17
|
} from "@assistant-ui/core";
|
|
18
|
-
import
|
|
18
|
+
import {
|
|
19
|
+
parsePartialJsonObject,
|
|
20
|
+
type ReadonlyJSONObject,
|
|
21
|
+
} from "assistant-stream/utils";
|
|
19
22
|
import { unwrapModelContentEnvelope } from "../../modelContentEnvelope";
|
|
20
23
|
|
|
21
24
|
type MessageMetadata = ThreadMessageLike["metadata"];
|
|
@@ -259,6 +262,11 @@ function convertParts(
|
|
|
259
262
|
if (part.state === "input-streaming") {
|
|
260
263
|
// strip closing delimiters added by the AI SDK's fix-json
|
|
261
264
|
argsText = stripClosingDelimiters(argsText);
|
|
265
|
+
// Re-parse so args carries the partial-JSON meta that marks which
|
|
266
|
+
// field is still mid-arrival, like every argsText-based runtime.
|
|
267
|
+
// The key-order cache appends new keys last, so the trailing field
|
|
268
|
+
// of the stripped text is the streaming frontier.
|
|
269
|
+
args = parsePartialJsonObject(argsText) ?? args;
|
|
262
270
|
} else {
|
|
263
271
|
metadata.toolArgsKeyOrderCache?.delete(argsKeyOrderCacheKey);
|
|
264
272
|
if (
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { };
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { };
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { };
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { };
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { };
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { };
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { };
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { };
|
package/dist/assistant-stream/dist/core/serialization/assistant-transport/AssistantTransport.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { };
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { };
|