@convex-dev/agent 0.6.0-alpha.0 → 0.6.0
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/MIGRATION.md +153 -0
- package/package.json +4 -2
- package/src/UIMessages.ts +135 -0
- package/src/client/approval.test.ts +350 -0
- package/src/client/createTool.ts +50 -52
- package/src/client/index.ts +162 -1
- package/src/client/mockModel.ts +9 -2
- package/src/client/search.test.ts +4 -5
- package/src/client/search.ts +47 -2
- package/src/client/start.ts +41 -24
- package/src/client/streamText.ts +27 -3
- package/src/client/streaming.integration.test.ts +1206 -0
- package/src/client/streaming.ts +41 -12
- package/src/client/types.ts +14 -12
- package/src/component/_generated/component.ts +53 -64
- package/src/component/messages.ts +12 -2
- package/src/deltas.ts +0 -1
- package/src/mapping.test.ts +143 -1
- package/src/mapping.ts +115 -6
- package/src/react/useDeltaStreams.ts +6 -0
- package/dist/UIMessages.d.ts +0 -46
- package/dist/UIMessages.d.ts.map +0 -1
- package/dist/UIMessages.js +0 -546
- package/dist/UIMessages.js.map +0 -1
- package/dist/client/createTool.d.ts +0 -170
- package/dist/client/createTool.d.ts.map +0 -1
- package/dist/client/createTool.js +0 -115
- package/dist/client/createTool.js.map +0 -1
- package/dist/client/defaultComponent.d.ts +0 -11
- package/dist/client/defaultComponent.d.ts.map +0 -1
- package/dist/client/defaultComponent.js +0 -7
- package/dist/client/defaultComponent.js.map +0 -1
- package/dist/client/definePlaygroundAPI.d.ts +0 -1725
- package/dist/client/definePlaygroundAPI.d.ts.map +0 -1
- package/dist/client/definePlaygroundAPI.js +0 -271
- package/dist/client/definePlaygroundAPI.js.map +0 -1
- package/dist/client/files.d.ts +0 -69
- package/dist/client/files.d.ts.map +0 -1
- package/dist/client/files.js +0 -181
- package/dist/client/files.js.map +0 -1
- package/dist/client/index.d.ts +0 -2044
- package/dist/client/index.d.ts.map +0 -1
- package/dist/client/index.js +0 -804
- package/dist/client/index.js.map +0 -1
- package/dist/client/messages.d.ts +0 -461
- package/dist/client/messages.d.ts.map +0 -1
- package/dist/client/messages.js +0 -106
- package/dist/client/messages.js.map +0 -1
- package/dist/client/mockModel.d.ts +0 -42
- package/dist/client/mockModel.d.ts.map +0 -1
- package/dist/client/mockModel.js +0 -175
- package/dist/client/mockModel.js.map +0 -1
- package/dist/client/saveInputMessages.d.ts +0 -20
- package/dist/client/saveInputMessages.d.ts.map +0 -1
- package/dist/client/saveInputMessages.js +0 -58
- package/dist/client/saveInputMessages.js.map +0 -1
- package/dist/client/search.d.ts +0 -493
- package/dist/client/search.d.ts.map +0 -1
- package/dist/client/search.js +0 -425
- package/dist/client/search.js.map +0 -1
- package/dist/client/start.d.ts +0 -84
- package/dist/client/start.d.ts.map +0 -1
- package/dist/client/start.js +0 -171
- package/dist/client/start.js.map +0 -1
- package/dist/client/streamText.d.ts +0 -46
- package/dist/client/streamText.d.ts.map +0 -1
- package/dist/client/streamText.js +0 -93
- package/dist/client/streamText.js.map +0 -1
- package/dist/client/streaming.d.ts +0 -3767
- package/dist/client/streaming.d.ts.map +0 -1
- package/dist/client/streaming.js +0 -294
- package/dist/client/streaming.js.map +0 -1
- package/dist/client/threads.d.ts +0 -46
- package/dist/client/threads.d.ts.map +0 -1
- package/dist/client/threads.js +0 -49
- package/dist/client/threads.js.map +0 -1
- package/dist/client/types.d.ts +0 -460
- package/dist/client/types.d.ts.map +0 -1
- package/dist/client/types.js +0 -2
- package/dist/client/types.js.map +0 -1
- package/dist/client/utils.d.ts +0 -4
- package/dist/client/utils.d.ts.map +0 -1
- package/dist/client/utils.js +0 -21
- package/dist/client/utils.js.map +0 -1
- package/dist/component/_generated/api.d.ts +0 -48
- package/dist/component/_generated/api.d.ts.map +0 -1
- package/dist/component/_generated/api.js +0 -31
- package/dist/component/_generated/api.js.map +0 -1
- package/dist/component/_generated/component.d.ts +0 -3119
- package/dist/component/_generated/component.d.ts.map +0 -1
- package/dist/component/_generated/component.js +0 -11
- package/dist/component/_generated/component.js.map +0 -1
- package/dist/component/_generated/dataModel.d.ts +0 -46
- package/dist/component/_generated/dataModel.d.ts.map +0 -1
- package/dist/component/_generated/dataModel.js +0 -11
- package/dist/component/_generated/dataModel.js.map +0 -1
- package/dist/component/_generated/server.d.ts +0 -121
- package/dist/component/_generated/server.d.ts.map +0 -1
- package/dist/component/_generated/server.js +0 -78
- package/dist/component/_generated/server.js.map +0 -1
- package/dist/component/apiKeys.d.ts +0 -11
- package/dist/component/apiKeys.d.ts.map +0 -1
- package/dist/component/apiKeys.js +0 -69
- package/dist/component/apiKeys.js.map +0 -1
- package/dist/component/convex.config.d.ts +0 -3
- package/dist/component/convex.config.d.ts.map +0 -1
- package/dist/component/convex.config.js +0 -3
- package/dist/component/convex.config.js.map +0 -1
- package/dist/component/files.d.ts +0 -97
- package/dist/component/files.d.ts.map +0 -1
- package/dist/component/files.js +0 -190
- package/dist/component/files.js.map +0 -1
- package/dist/component/messages.d.ts +0 -3850
- package/dist/component/messages.d.ts.map +0 -1
- package/dist/component/messages.js +0 -747
- package/dist/component/messages.js.map +0 -1
- package/dist/component/schema.d.ts +0 -8026
- package/dist/component/schema.d.ts.map +0 -1
- package/dist/component/schema.js +0 -147
- package/dist/component/schema.js.map +0 -1
- package/dist/component/streams.d.ts +0 -128
- package/dist/component/streams.d.ts.map +0 -1
- package/dist/component/streams.js +0 -413
- package/dist/component/streams.js.map +0 -1
- package/dist/component/threads.d.ts +0 -115
- package/dist/component/threads.d.ts.map +0 -1
- package/dist/component/threads.js +0 -208
- package/dist/component/threads.js.map +0 -1
- package/dist/component/users.d.ts +0 -52
- package/dist/component/users.d.ts.map +0 -1
- package/dist/component/users.js +0 -229
- package/dist/component/users.js.map +0 -1
- package/dist/component/vector/index.d.ts +0 -61
- package/dist/component/vector/index.d.ts.map +0 -1
- package/dist/component/vector/index.js +0 -146
- package/dist/component/vector/index.js.map +0 -1
- package/dist/component/vector/tables.d.ts +0 -58
- package/dist/component/vector/tables.d.ts.map +0 -1
- package/dist/component/vector/tables.js +0 -56
- package/dist/component/vector/tables.js.map +0 -1
- package/dist/deltas.d.ts +0 -43
- package/dist/deltas.d.ts.map +0 -1
- package/dist/deltas.js +0 -447
- package/dist/deltas.js.map +0 -1
- package/dist/mapping.d.ts +0 -54
- package/dist/mapping.d.ts.map +0 -1
- package/dist/mapping.js +0 -626
- package/dist/mapping.js.map +0 -1
- package/dist/react/SmoothText.d.ts +0 -5
- package/dist/react/SmoothText.d.ts.map +0 -1
- package/dist/react/SmoothText.js +0 -6
- package/dist/react/SmoothText.js.map +0 -1
- package/dist/react/index.d.ts +0 -25
- package/dist/react/index.d.ts.map +0 -1
- package/dist/react/index.js +0 -70
- package/dist/react/index.js.map +0 -1
- package/dist/react/optimisticallySendMessage.d.ts +0 -42
- package/dist/react/optimisticallySendMessage.d.ts.map +0 -1
- package/dist/react/optimisticallySendMessage.js +0 -74
- package/dist/react/optimisticallySendMessage.js.map +0 -1
- package/dist/react/types.d.ts +0 -12
- package/dist/react/types.d.ts.map +0 -1
- package/dist/react/types.js +0 -2
- package/dist/react/types.js.map +0 -1
- package/dist/react/useDeltaStreams.d.ts +0 -10
- package/dist/react/useDeltaStreams.d.ts.map +0 -1
- package/dist/react/useDeltaStreams.js +0 -101
- package/dist/react/useDeltaStreams.js.map +0 -1
- package/dist/react/useSmoothText.d.ts +0 -27
- package/dist/react/useSmoothText.d.ts.map +0 -1
- package/dist/react/useSmoothText.js +0 -68
- package/dist/react/useSmoothText.js.map +0 -1
- package/dist/react/useStreamingUIMessages.d.ts +0 -22
- package/dist/react/useStreamingUIMessages.d.ts.map +0 -1
- package/dist/react/useStreamingUIMessages.js +0 -92
- package/dist/react/useStreamingUIMessages.js.map +0 -1
- package/dist/react/useThreadMessages.d.ts +0 -104
- package/dist/react/useThreadMessages.d.ts.map +0 -1
- package/dist/react/useThreadMessages.js +0 -148
- package/dist/react/useThreadMessages.js.map +0 -1
- package/dist/react/useUIMessages.d.ts +0 -96
- package/dist/react/useUIMessages.d.ts.map +0 -1
- package/dist/react/useUIMessages.js +0 -108
- package/dist/react/useUIMessages.js.map +0 -1
- package/dist/shared.d.ts +0 -26
- package/dist/shared.d.ts.map +0 -1
- package/dist/shared.js +0 -67
- package/dist/shared.js.map +0 -1
- package/dist/validators.d.ts +0 -24516
- package/dist/validators.d.ts.map +0 -1
- package/dist/validators.js +0 -475
- package/dist/validators.js.map +0 -1
package/src/client/streaming.ts
CHANGED
|
@@ -210,6 +210,9 @@ export class DeltaStreamer<T> {
|
|
|
210
210
|
#ongoingWrite: Promise<void> | undefined;
|
|
211
211
|
#cursor: number = 0;
|
|
212
212
|
public abortController: AbortController;
|
|
213
|
+
// When true, the stream will be finished externally (e.g., atomically via addMessages)
|
|
214
|
+
// and consumeStream should skip calling finish().
|
|
215
|
+
#finishedExternally: boolean = false;
|
|
213
216
|
|
|
214
217
|
constructor(
|
|
215
218
|
public readonly component: AgentComponent,
|
|
@@ -259,17 +262,16 @@ export class DeltaStreamer<T> {
|
|
|
259
262
|
// Avoid race conditions by only creating once
|
|
260
263
|
#creatingStreamIdPromise: Promise<string> | undefined;
|
|
261
264
|
public async getStreamId() {
|
|
262
|
-
if (this.streamId) {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
265
|
+
if (!this.streamId) {
|
|
266
|
+
if (!this.#creatingStreamIdPromise) {
|
|
267
|
+
this.#creatingStreamIdPromise = this.ctx.runMutation(
|
|
268
|
+
this.component.streams.create,
|
|
269
|
+
this.metadata,
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
this.streamId = await this.#creatingStreamIdPromise;
|
|
267
273
|
}
|
|
268
|
-
|
|
269
|
-
this.component.streams.create,
|
|
270
|
-
this.metadata,
|
|
271
|
-
);
|
|
272
|
-
this.streamId = await this.#creatingStreamIdPromise;
|
|
274
|
+
return this.streamId;
|
|
273
275
|
}
|
|
274
276
|
|
|
275
277
|
public async addParts(parts: T[]) {
|
|
@@ -290,7 +292,28 @@ export class DeltaStreamer<T> {
|
|
|
290
292
|
for await (const chunk of stream) {
|
|
291
293
|
await this.addParts([chunk]);
|
|
292
294
|
}
|
|
293
|
-
|
|
295
|
+
// Skip finish if it will be handled externally (atomically with message save)
|
|
296
|
+
// or if the stream was aborted (e.g., due to a failed delta write)
|
|
297
|
+
if (!this.#finishedExternally && !this.abortController.signal.aborted) {
|
|
298
|
+
await this.finish();
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Mark the stream as being finished externally (e.g., atomically via addMessages).
|
|
304
|
+
* When called, consumeStream() will skip calling finish() since it will be
|
|
305
|
+
* handled elsewhere in the same mutation as message saving.
|
|
306
|
+
*/
|
|
307
|
+
public markFinishedExternally(): void {
|
|
308
|
+
this.#finishedExternally = true;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Get the stream ID, waiting for it to be created if necessary.
|
|
313
|
+
* Useful for passing to addMessages for atomic finish.
|
|
314
|
+
*/
|
|
315
|
+
public async getOrCreateStreamId(): Promise<string> {
|
|
316
|
+
return this.getStreamId();
|
|
294
317
|
}
|
|
295
318
|
|
|
296
319
|
async #sendDelta() {
|
|
@@ -317,7 +340,7 @@ export class DeltaStreamer<T> {
|
|
|
317
340
|
e instanceof Error ? e.message : "unknown error",
|
|
318
341
|
);
|
|
319
342
|
this.abortController.abort();
|
|
320
|
-
|
|
343
|
+
return;
|
|
321
344
|
}
|
|
322
345
|
// Now that we've sent the delta, check if we need to send another one.
|
|
323
346
|
if (
|
|
@@ -353,7 +376,13 @@ export class DeltaStreamer<T> {
|
|
|
353
376
|
return;
|
|
354
377
|
}
|
|
355
378
|
await this.#ongoingWrite;
|
|
379
|
+
if (this.abortController.signal.aborted) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
356
382
|
await this.#sendDelta();
|
|
383
|
+
if (this.abortController.signal.aborted) {
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
357
386
|
await this.ctx.runMutation(this.component.streams.finish, {
|
|
358
387
|
streamId: this.streamId,
|
|
359
388
|
});
|
package/src/client/types.ts
CHANGED
|
@@ -46,6 +46,14 @@ import type {
|
|
|
46
46
|
import type { StreamingOptions } from "./streaming.js";
|
|
47
47
|
import type { ComponentApi } from "../component/_generated/component.js";
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Type-level check that ensures models are from AI SDK v6.
|
|
51
|
+
* If a v5 model (LanguageModelV2) is passed, TypeScript will show the error message string.
|
|
52
|
+
*/
|
|
53
|
+
type AssertAISDKv6<T> = T extends { specificationVersion: "v3" }
|
|
54
|
+
? T
|
|
55
|
+
: "⚠️ @convex-dev/agent v0.6.0 requires AI SDK v6. Update your dependencies: npm install ai@^6.0.35 @ai-sdk/openai@^3.0.10 (or other provider). See: node_modules/@convex-dev/agent/MIGRATION.md";
|
|
56
|
+
|
|
49
57
|
export type AgentPrompt = {
|
|
50
58
|
/**
|
|
51
59
|
* System message to include in the prompt. Overwrites Agent instructions.
|
|
@@ -91,23 +99,17 @@ export type AgentPrompt = {
|
|
|
91
99
|
export type Config = {
|
|
92
100
|
/**
|
|
93
101
|
* The LLM model to use for generating / streaming text and objects.
|
|
94
|
-
*
|
|
102
|
+
* Requires AI SDK v6 (@ai-sdk/* packages v3.x).
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
95
105
|
* import { openai } from "@ai-sdk/openai"
|
|
96
106
|
* const myAgent = new Agent(components.agent, {
|
|
97
107
|
* languageModel: openai.chat("gpt-4o-mini"),
|
|
108
|
+
* })
|
|
98
109
|
*/
|
|
99
|
-
languageModel?: LanguageModel
|
|
110
|
+
languageModel?: AssertAISDKv6<LanguageModel>;
|
|
100
111
|
/**
|
|
101
|
-
*
|
|
102
|
-
* If specified, it will use this for generating vector embeddings
|
|
103
|
-
* of chats, and can opt-in to doing vector search for automatic context
|
|
104
|
-
* on generateText, etc.
|
|
105
|
-
* e.g.
|
|
106
|
-
* import { openai } from "@ai-sdk/openai"
|
|
107
|
-
* const myAgent = new Agent(components.agent, {
|
|
108
|
-
* ...
|
|
109
|
-
* textEmbeddingModel: openai.embedding("text-embedding-3-small")
|
|
110
|
-
* @deprecated — Use embeddingModel instead.
|
|
112
|
+
* @deprecated Use `embeddingModel` instead.
|
|
111
113
|
*/
|
|
112
114
|
textEmbeddingModel?: EmbeddingModel;
|
|
113
115
|
/**
|
|
@@ -154,6 +154,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
154
154
|
vectors: Array<Array<number> | null>;
|
|
155
155
|
};
|
|
156
156
|
failPendingSteps?: boolean;
|
|
157
|
+
finishStreamId?: string;
|
|
157
158
|
hideFromUserIdSearch?: boolean;
|
|
158
159
|
messages: Array<{
|
|
159
160
|
error?: string;
|
|
@@ -1420,22 +1421,19 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
1420
1421
|
toolName: string;
|
|
1421
1422
|
type: "tool-call";
|
|
1422
1423
|
}
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
toolName: string;
|
|
1437
|
-
type: "tool-call";
|
|
1438
|
-
}
|
|
1424
|
+
| {
|
|
1425
|
+
args: any;
|
|
1426
|
+
input?: any;
|
|
1427
|
+
providerExecuted?: boolean;
|
|
1428
|
+
providerMetadata?: Record<
|
|
1429
|
+
string,
|
|
1430
|
+
Record<string, any>
|
|
1431
|
+
>;
|
|
1432
|
+
providerOptions?: Record<string, Record<string, any>>;
|
|
1433
|
+
toolCallId: string;
|
|
1434
|
+
toolName: string;
|
|
1435
|
+
type: "tool-call";
|
|
1436
|
+
}
|
|
1439
1437
|
| {
|
|
1440
1438
|
args?: any;
|
|
1441
1439
|
experimental_content?: Array<
|
|
@@ -2533,22 +2531,19 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
2533
2531
|
toolName: string;
|
|
2534
2532
|
type: "tool-call";
|
|
2535
2533
|
}
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
toolName: string;
|
|
2550
|
-
type: "tool-call";
|
|
2551
|
-
}
|
|
2534
|
+
| {
|
|
2535
|
+
args: any;
|
|
2536
|
+
input?: any;
|
|
2537
|
+
providerExecuted?: boolean;
|
|
2538
|
+
providerMetadata?: Record<
|
|
2539
|
+
string,
|
|
2540
|
+
Record<string, any>
|
|
2541
|
+
>;
|
|
2542
|
+
providerOptions?: Record<string, Record<string, any>>;
|
|
2543
|
+
toolCallId: string;
|
|
2544
|
+
toolName: string;
|
|
2545
|
+
type: "tool-call";
|
|
2546
|
+
}
|
|
2552
2547
|
| {
|
|
2553
2548
|
args?: any;
|
|
2554
2549
|
experimental_content?: Array<
|
|
@@ -3054,22 +3049,19 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
3054
3049
|
toolName: string;
|
|
3055
3050
|
type: "tool-call";
|
|
3056
3051
|
}
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
toolName: string;
|
|
3071
|
-
type: "tool-call";
|
|
3072
|
-
}
|
|
3052
|
+
| {
|
|
3053
|
+
args: any;
|
|
3054
|
+
input?: any;
|
|
3055
|
+
providerExecuted?: boolean;
|
|
3056
|
+
providerMetadata?: Record<
|
|
3057
|
+
string,
|
|
3058
|
+
Record<string, any>
|
|
3059
|
+
>;
|
|
3060
|
+
providerOptions?: Record<string, Record<string, any>>;
|
|
3061
|
+
toolCallId: string;
|
|
3062
|
+
toolName: string;
|
|
3063
|
+
type: "tool-call";
|
|
3064
|
+
}
|
|
3073
3065
|
| {
|
|
3074
3066
|
args?: any;
|
|
3075
3067
|
experimental_content?: Array<
|
|
@@ -4063,22 +4055,19 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
4063
4055
|
toolName: string;
|
|
4064
4056
|
type: "tool-call";
|
|
4065
4057
|
}
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
toolName: string;
|
|
4080
|
-
type: "tool-call";
|
|
4081
|
-
}
|
|
4058
|
+
| {
|
|
4059
|
+
args: any;
|
|
4060
|
+
input?: any;
|
|
4061
|
+
providerExecuted?: boolean;
|
|
4062
|
+
providerMetadata?: Record<
|
|
4063
|
+
string,
|
|
4064
|
+
Record<string, any>
|
|
4065
|
+
>;
|
|
4066
|
+
providerOptions?: Record<string, Record<string, any>>;
|
|
4067
|
+
toolCallId: string;
|
|
4068
|
+
toolName: string;
|
|
4069
|
+
type: "tool-call";
|
|
4070
|
+
}
|
|
4082
4071
|
| {
|
|
4083
4072
|
args?: any;
|
|
4084
4073
|
experimental_content?: Array<
|
|
@@ -39,7 +39,7 @@ import {
|
|
|
39
39
|
vVectorId,
|
|
40
40
|
} from "./vector/tables.js";
|
|
41
41
|
import { changeRefcount } from "./files.js";
|
|
42
|
-
import { getStreamingMessagesWithMetadata } from "./streams.js";
|
|
42
|
+
import { getStreamingMessagesWithMetadata, finishHandler } from "./streams.js";
|
|
43
43
|
import { partial } from "convex-helpers/validators";
|
|
44
44
|
|
|
45
45
|
function publicMessage(message: Doc<"messages">): MessageDoc {
|
|
@@ -141,6 +141,9 @@ const addMessagesArgs = {
|
|
|
141
141
|
// if set to true, these messages will not show up in text or vector search
|
|
142
142
|
// results for the userId
|
|
143
143
|
hideFromUserIdSearch: v.optional(v.boolean()),
|
|
144
|
+
// If provided, finish this stream atomically with the message save.
|
|
145
|
+
// This prevents UI flickering from separate mutations (issue #181).
|
|
146
|
+
finishStreamId: v.optional(v.id("streamingMessages")),
|
|
144
147
|
};
|
|
145
148
|
export const addMessages = mutation({
|
|
146
149
|
args: addMessagesArgs,
|
|
@@ -161,6 +164,8 @@ async function addMessagesHandler(
|
|
|
161
164
|
const {
|
|
162
165
|
embeddings,
|
|
163
166
|
failPendingSteps,
|
|
167
|
+
// Destructured separately to exclude from `...rest` (used in addMessages args, not message fields)
|
|
168
|
+
finishStreamId,
|
|
164
169
|
messages,
|
|
165
170
|
promptMessageId,
|
|
166
171
|
pendingMessageId,
|
|
@@ -274,7 +279,7 @@ async function addMessagesHandler(
|
|
|
274
279
|
order: pendingMessage.order,
|
|
275
280
|
stepOrder: pendingMessage.stepOrder,
|
|
276
281
|
});
|
|
277
|
-
toReturn.push(pendingMessage);
|
|
282
|
+
toReturn.push((await ctx.db.get(pendingMessage._id))!);
|
|
278
283
|
continue;
|
|
279
284
|
}
|
|
280
285
|
if (message.message.role === "user") {
|
|
@@ -303,6 +308,11 @@ async function addMessagesHandler(
|
|
|
303
308
|
// TODO: delete the associated stream data for the order/stepOrder
|
|
304
309
|
toReturn.push((await ctx.db.get(messageId))!);
|
|
305
310
|
}
|
|
311
|
+
// Atomically finish the stream if requested, preventing UI flickering
|
|
312
|
+
// from separate mutations for message save and stream finish (issue #181).
|
|
313
|
+
if (finishStreamId) {
|
|
314
|
+
await finishHandler(ctx, { streamId: finishStreamId });
|
|
315
|
+
}
|
|
306
316
|
return { messages: toReturn.map(publicMessage) };
|
|
307
317
|
}
|
|
308
318
|
|
package/src/deltas.ts
CHANGED
|
@@ -124,7 +124,6 @@ export async function deriveUIMessagesFromDeltas(
|
|
|
124
124
|
blankUIMessage(streamMessage, threadId),
|
|
125
125
|
parts,
|
|
126
126
|
);
|
|
127
|
-
// TODO: this fails on partial tool calls
|
|
128
127
|
messages.push(uiMessage);
|
|
129
128
|
} else {
|
|
130
129
|
const [uiMessages] = deriveUIMessagesFromTextStreamParts(
|
package/src/mapping.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, test, expect } from "vitest";
|
|
1
|
+
import { describe, test, expect, vi } from "vitest";
|
|
2
2
|
import {
|
|
3
3
|
guessMimeType,
|
|
4
4
|
serializeDataOrUrl,
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
toModelMessage,
|
|
8
8
|
serializeContent,
|
|
9
9
|
toModelMessageContent,
|
|
10
|
+
autoDenyUnresolvedApprovals,
|
|
10
11
|
} from "./mapping.js";
|
|
11
12
|
import { api } from "./component/_generated/api.js";
|
|
12
13
|
import type { AgentComponent, ActionCtx } from "./client/types.js";
|
|
@@ -257,4 +258,145 @@ describe("mapping", () => {
|
|
|
257
258
|
expect(content).toHaveLength(1);
|
|
258
259
|
expect((content as unknown[])[0]).toMatchObject(approvalResponse);
|
|
259
260
|
});
|
|
261
|
+
|
|
262
|
+
describe("autoDenyUnresolvedApprovals", () => {
|
|
263
|
+
test("returns messages unchanged when no unresolved approvals", () => {
|
|
264
|
+
const messages = [
|
|
265
|
+
{ role: "user" as const, content: "hello" },
|
|
266
|
+
{
|
|
267
|
+
role: "assistant" as const,
|
|
268
|
+
content: [
|
|
269
|
+
{ type: "tool-call", toolCallId: "tc1", toolName: "a", input: {} },
|
|
270
|
+
{ type: "tool-approval-request", approvalId: "ap1", toolCallId: "tc1" },
|
|
271
|
+
],
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
role: "tool" as const,
|
|
275
|
+
content: [
|
|
276
|
+
{ type: "tool-approval-response", approvalId: "ap1", approved: true },
|
|
277
|
+
],
|
|
278
|
+
},
|
|
279
|
+
] as any;
|
|
280
|
+
|
|
281
|
+
const result = autoDenyUnresolvedApprovals(messages);
|
|
282
|
+
expect(result).toBe(messages); // same reference, no changes
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test("injects synthetic denial for a single unresolved approval", () => {
|
|
286
|
+
const messages = [
|
|
287
|
+
{ role: "user" as const, content: "hello" },
|
|
288
|
+
{
|
|
289
|
+
role: "assistant" as const,
|
|
290
|
+
content: [
|
|
291
|
+
{ type: "tool-call", toolCallId: "tc1", toolName: "a", input: {} },
|
|
292
|
+
{ type: "tool-approval-request", approvalId: "ap1", toolCallId: "tc1" },
|
|
293
|
+
],
|
|
294
|
+
},
|
|
295
|
+
{ role: "user" as const, content: "new message" },
|
|
296
|
+
] as any;
|
|
297
|
+
|
|
298
|
+
const result = autoDenyUnresolvedApprovals(messages);
|
|
299
|
+
expect(result).toHaveLength(4); // original 3 + 1 synthetic tool message
|
|
300
|
+
// Synthetic denial should be inserted right after the assistant message (index 1)
|
|
301
|
+
expect(result[2].role).toBe("tool");
|
|
302
|
+
const denialContent = result[2].content as any[];
|
|
303
|
+
expect(denialContent).toHaveLength(1);
|
|
304
|
+
expect(denialContent[0].type).toBe("tool-approval-response");
|
|
305
|
+
expect(denialContent[0].approvalId).toBe("ap1");
|
|
306
|
+
expect(denialContent[0].approved).toBe(false);
|
|
307
|
+
expect(denialContent[0].reason).toBe("auto-denied: new generation started");
|
|
308
|
+
// The new user message should follow
|
|
309
|
+
expect(result[3].role).toBe("user");
|
|
310
|
+
expect(result[3].content).toBe("new message");
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test("groups multiple unresolved approvals from the same step into a single synthetic message", () => {
|
|
314
|
+
const messages = [
|
|
315
|
+
{
|
|
316
|
+
role: "assistant" as const,
|
|
317
|
+
content: [
|
|
318
|
+
{ type: "tool-call", toolCallId: "tc1", toolName: "a", input: {} },
|
|
319
|
+
{ type: "tool-call", toolCallId: "tc2", toolName: "b", input: {} },
|
|
320
|
+
{ type: "tool-approval-request", approvalId: "ap1", toolCallId: "tc1" },
|
|
321
|
+
{ type: "tool-approval-request", approvalId: "ap2", toolCallId: "tc2" },
|
|
322
|
+
],
|
|
323
|
+
},
|
|
324
|
+
] as any;
|
|
325
|
+
|
|
326
|
+
const result = autoDenyUnresolvedApprovals(messages);
|
|
327
|
+
expect(result).toHaveLength(2); // assistant + 1 synthetic tool message
|
|
328
|
+
expect(result[1].role).toBe("tool");
|
|
329
|
+
const denialContent = result[1].content as any[];
|
|
330
|
+
expect(denialContent).toHaveLength(2);
|
|
331
|
+
expect(denialContent[0].approvalId).toBe("ap1");
|
|
332
|
+
expect(denialContent[0].approved).toBe(false);
|
|
333
|
+
expect(denialContent[1].approvalId).toBe("ap2");
|
|
334
|
+
expect(denialContent[1].approved).toBe(false);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test("only auto-denies unresolved approvals, leaves resolved ones alone", () => {
|
|
338
|
+
const messages = [
|
|
339
|
+
{
|
|
340
|
+
role: "assistant" as const,
|
|
341
|
+
content: [
|
|
342
|
+
{ type: "tool-call", toolCallId: "tc1", toolName: "a", input: {} },
|
|
343
|
+
{ type: "tool-call", toolCallId: "tc2", toolName: "b", input: {} },
|
|
344
|
+
{ type: "tool-approval-request", approvalId: "ap1", toolCallId: "tc1" },
|
|
345
|
+
{ type: "tool-approval-request", approvalId: "ap2", toolCallId: "tc2" },
|
|
346
|
+
],
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
role: "tool" as const,
|
|
350
|
+
content: [
|
|
351
|
+
{ type: "tool-approval-response", approvalId: "ap1", approved: true },
|
|
352
|
+
],
|
|
353
|
+
},
|
|
354
|
+
{ role: "user" as const, content: "next question" },
|
|
355
|
+
] as any;
|
|
356
|
+
|
|
357
|
+
const result = autoDenyUnresolvedApprovals(messages);
|
|
358
|
+
// Should inject a denial for ap2 (unresolved) after the assistant message
|
|
359
|
+
expect(result).toHaveLength(4); // assistant + existing tool + synthetic denial + user
|
|
360
|
+
// The synthetic denial is inserted after the assistant (index 0)
|
|
361
|
+
expect(result[0].role).toBe("assistant");
|
|
362
|
+
expect(result[1].role).toBe("tool"); // synthetic denial for ap2
|
|
363
|
+
const denialContent = result[1].content as any[];
|
|
364
|
+
expect(denialContent).toHaveLength(1);
|
|
365
|
+
expect(denialContent[0].approvalId).toBe("ap2");
|
|
366
|
+
expect(denialContent[0].approved).toBe(false);
|
|
367
|
+
// Original tool message (ap1 response) follows
|
|
368
|
+
expect(result[2].role).toBe("tool");
|
|
369
|
+
const originalToolContent = result[2].content as any[];
|
|
370
|
+
expect(originalToolContent[0].approvalId).toBe("ap1");
|
|
371
|
+
expect(originalToolContent[0].approved).toBe(true);
|
|
372
|
+
// User message last
|
|
373
|
+
expect(result[3].role).toBe("user");
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
test("emits console.warn for each auto-denied approval", () => {
|
|
377
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
378
|
+
const messages = [
|
|
379
|
+
{
|
|
380
|
+
role: "assistant" as const,
|
|
381
|
+
content: [
|
|
382
|
+
{ type: "tool-call", toolCallId: "tc1", toolName: "a", input: {} },
|
|
383
|
+
{ type: "tool-call", toolCallId: "tc2", toolName: "b", input: {} },
|
|
384
|
+
{ type: "tool-approval-request", approvalId: "ap1", toolCallId: "tc1" },
|
|
385
|
+
{ type: "tool-approval-request", approvalId: "ap2", toolCallId: "tc2" },
|
|
386
|
+
],
|
|
387
|
+
},
|
|
388
|
+
] as any;
|
|
389
|
+
|
|
390
|
+
autoDenyUnresolvedApprovals(messages);
|
|
391
|
+
|
|
392
|
+
expect(warnSpy).toHaveBeenCalledTimes(2);
|
|
393
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
394
|
+
expect.stringContaining("ap1"),
|
|
395
|
+
);
|
|
396
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
397
|
+
expect.stringContaining("ap2"),
|
|
398
|
+
);
|
|
399
|
+
warnSpy.mockRestore();
|
|
400
|
+
});
|
|
401
|
+
});
|
|
260
402
|
});
|
package/src/mapping.ts
CHANGED
|
@@ -139,6 +139,97 @@ export function docsToModelMessages(messages: MessageDoc[]): ModelMessage[] {
|
|
|
139
139
|
.map(toModelMessage);
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Scan messages for unresolved `tool-approval-request` parts and inject
|
|
144
|
+
* synthetic `tool-approval-response` denials so that the AI SDK receives
|
|
145
|
+
* a complete history (every tool-call has a corresponding result or denial).
|
|
146
|
+
*
|
|
147
|
+
* This handles the case where a user sends a new message instead of
|
|
148
|
+
* resolving pending approvals — the old approvals are auto-denied rather
|
|
149
|
+
* than silently dropped.
|
|
150
|
+
*/
|
|
151
|
+
export function autoDenyUnresolvedApprovals(
|
|
152
|
+
messages: ModelMessage[],
|
|
153
|
+
): ModelMessage[] {
|
|
154
|
+
// Collect all approval requests: approvalId → { toolCallId, messageIndex }
|
|
155
|
+
const requests = new Map<
|
|
156
|
+
string,
|
|
157
|
+
{ toolCallId: string; messageIndex: number }
|
|
158
|
+
>();
|
|
159
|
+
// Collect all resolved approval IDs
|
|
160
|
+
const resolvedIds = new Set<string>();
|
|
161
|
+
|
|
162
|
+
for (let i = 0; i < messages.length; i++) {
|
|
163
|
+
const msg = messages[i];
|
|
164
|
+
if (!Array.isArray(msg.content)) continue;
|
|
165
|
+
for (const part of msg.content as any[]) {
|
|
166
|
+
if (part.type === "tool-approval-request") {
|
|
167
|
+
requests.set(part.approvalId, {
|
|
168
|
+
toolCallId: part.toolCallId,
|
|
169
|
+
messageIndex: i,
|
|
170
|
+
});
|
|
171
|
+
} else if (part.type === "tool-approval-response") {
|
|
172
|
+
resolvedIds.add(part.approvalId);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Find unresolved approvals
|
|
178
|
+
const unresolved: Array<{
|
|
179
|
+
approvalId: string;
|
|
180
|
+
toolCallId: string;
|
|
181
|
+
messageIndex: number;
|
|
182
|
+
}> = [];
|
|
183
|
+
for (const [approvalId, info] of requests) {
|
|
184
|
+
if (!resolvedIds.has(approvalId)) {
|
|
185
|
+
unresolved.push({ approvalId, ...info });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (unresolved.length === 0) {
|
|
190
|
+
return messages;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Group unresolved approvals by the assistant message index they came from
|
|
194
|
+
const byMessageIndex = new Map<
|
|
195
|
+
number,
|
|
196
|
+
Array<{ approvalId: string; toolCallId: string }>
|
|
197
|
+
>();
|
|
198
|
+
for (const entry of unresolved) {
|
|
199
|
+
console.warn(
|
|
200
|
+
`Auto-denying unresolved tool approval ${entry.approvalId} ` +
|
|
201
|
+
`(toolCallId: ${entry.toolCallId}): new generation started`,
|
|
202
|
+
);
|
|
203
|
+
let group = byMessageIndex.get(entry.messageIndex);
|
|
204
|
+
if (!group) {
|
|
205
|
+
group = [];
|
|
206
|
+
byMessageIndex.set(entry.messageIndex, group);
|
|
207
|
+
}
|
|
208
|
+
group.push(entry);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Build result by inserting synthetic denial messages after each relevant
|
|
212
|
+
// assistant message
|
|
213
|
+
const result: ModelMessage[] = [];
|
|
214
|
+
for (let i = 0; i < messages.length; i++) {
|
|
215
|
+
result.push(messages[i]);
|
|
216
|
+
const group = byMessageIndex.get(i);
|
|
217
|
+
if (group) {
|
|
218
|
+
result.push({
|
|
219
|
+
role: "tool",
|
|
220
|
+
content: group.map((entry) => ({
|
|
221
|
+
type: "tool-approval-response" as const,
|
|
222
|
+
approvalId: entry.approvalId,
|
|
223
|
+
approved: false,
|
|
224
|
+
reason: "auto-denied: new generation started",
|
|
225
|
+
})),
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return result;
|
|
231
|
+
}
|
|
232
|
+
|
|
142
233
|
export function serializeUsage(usage: LanguageModelUsage): Usage {
|
|
143
234
|
return {
|
|
144
235
|
promptTokens: usage.inputTokens ?? 0,
|
|
@@ -211,6 +302,16 @@ export async function serializeNewMessagesInStep<TOOLS extends ToolSet>(
|
|
|
211
302
|
component: AgentComponent,
|
|
212
303
|
step: StepResult<TOOLS>,
|
|
213
304
|
model: ModelOrMetadata | undefined,
|
|
305
|
+
/**
|
|
306
|
+
* If provided, these are the new response messages for this step
|
|
307
|
+
* (pre-sliced by the caller). When not provided, falls back to the
|
|
308
|
+
* existing heuristic of slicing the last 1-2 messages.
|
|
309
|
+
*
|
|
310
|
+
* This is needed for tool approval flows where the SDK adds extra
|
|
311
|
+
* messages (e.g. approval tool-results) at the beginning of
|
|
312
|
+
* responseMessages that the old slice(-1/-2) logic would miss.
|
|
313
|
+
*/
|
|
314
|
+
newResponseMessages?: ModelMessage[],
|
|
214
315
|
): Promise<{ messages: MessageWithMetadata[] }> {
|
|
215
316
|
// If there are tool results, there's another message with the tool results
|
|
216
317
|
// ref: https://github.com/vercel/ai/blob/main/packages/ai/src/generate-text/to-response-messages.ts#L120
|
|
@@ -228,13 +329,21 @@ export async function serializeNewMessagesInStep<TOOLS extends ToolSet>(
|
|
|
228
329
|
sources: hasToolMessage ? undefined : step.sources,
|
|
229
330
|
} satisfies Omit<MessageWithMetadata, "message" | "text" | "fileIds">;
|
|
230
331
|
const toolFields = { sources: step.sources };
|
|
332
|
+
|
|
333
|
+
// Determine which messages to serialize for this step
|
|
334
|
+
let messagesToSerialize: ModelMessage[];
|
|
335
|
+
if (newResponseMessages) {
|
|
336
|
+
messagesToSerialize = newResponseMessages;
|
|
337
|
+
} else if (hasToolMessage) {
|
|
338
|
+
messagesToSerialize = step.response.messages.slice(-2);
|
|
339
|
+
} else if (step.content.length) {
|
|
340
|
+
messagesToSerialize = step.response.messages.slice(-1);
|
|
341
|
+
} else {
|
|
342
|
+
messagesToSerialize = [{ role: "assistant" as const, content: [] }];
|
|
343
|
+
}
|
|
344
|
+
|
|
231
345
|
const messages: MessageWithMetadata[] = await Promise.all(
|
|
232
|
-
(
|
|
233
|
-
? step.response.messages.slice(-2)
|
|
234
|
-
: step.content.length
|
|
235
|
-
? step.response.messages.slice(-1)
|
|
236
|
-
: [{ role: "assistant" as const, content: [] }]
|
|
237
|
-
).map(async (msg): Promise<MessageWithMetadata> => {
|
|
346
|
+
messagesToSerialize.map(async (msg): Promise<MessageWithMetadata> => {
|
|
238
347
|
const { message, fileIds } = await serializeMessage(ctx, component, msg);
|
|
239
348
|
return parse(vMessageWithMetadata, {
|
|
240
349
|
message,
|
|
@@ -83,6 +83,12 @@ export function useDeltaStreams<
|
|
|
83
83
|
),
|
|
84
84
|
);
|
|
85
85
|
|
|
86
|
+
// When no active streams remain, clear the stale state so we stop
|
|
87
|
+
// returning old streaming UIMessages.
|
|
88
|
+
if (streamMessages !== undefined && streamMessages.length === 0) {
|
|
89
|
+
state.deltaStreams = undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
86
92
|
// Get the deltas for all the active streams, if any.
|
|
87
93
|
const cursorQuery = useQuery(
|
|
88
94
|
query,
|