@convex-dev/agent 0.6.4 → 0.7.0-alpha.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 +60 -116
- package/dist/client/messages.d.ts +1 -398
- package/dist/client/messages.d.ts.map +1 -1
- package/dist/client/messages.js +3 -47
- package/dist/client/messages.js.map +1 -1
- package/dist/client/types.d.ts +1 -455
- package/dist/client/types.d.ts.map +1 -1
- package/dist/component/_generated/component.d.ts +805 -45
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/messages.d.ts +1252 -201
- package/dist/component/messages.d.ts.map +1 -1
- package/dist/component/messages.js +49 -23
- package/dist/component/messages.js.map +1 -1
- package/dist/component/schema.d.ts +2593 -332
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/streams.d.ts +7 -1
- package/dist/component/streams.d.ts.map +1 -1
- package/dist/component/streams.js +24 -30
- package/dist/component/streams.js.map +1 -1
- package/dist/component/threads.d.ts.map +1 -1
- package/dist/component/threads.js +1 -2
- package/dist/component/threads.js.map +1 -1
- package/dist/react/index.d.ts +5 -5
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +5 -5
- package/dist/react/index.js.map +1 -1
- package/dist/shared.d.ts +19 -7
- package/dist/shared.d.ts.map +1 -1
- package/dist/shared.js +4 -4
- package/dist/shared.js.map +1 -1
- package/dist/streaming/materializePersistedUIMessageChunks.d.ts +16 -0
- package/dist/streaming/materializePersistedUIMessageChunks.d.ts.map +1 -0
- package/dist/streaming/materializePersistedUIMessageChunks.js +302 -0
- package/dist/streaming/materializePersistedUIMessageChunks.js.map +1 -0
- package/dist/streaming/persistedUIMessageChunks.d.ts +106 -0
- package/dist/streaming/persistedUIMessageChunks.d.ts.map +1 -0
- package/dist/streaming/persistedUIMessageChunks.js +475 -0
- package/dist/streaming/persistedUIMessageChunks.js.map +1 -0
- package/dist/validators.d.ts +10026 -1055
- package/dist/validators.d.ts.map +1 -1
- package/dist/validators.js +71 -9
- package/dist/validators.js.map +1 -1
- package/dist/{UIMessages.d.ts → vercel/UIMessages.d.ts} +1 -1
- package/dist/vercel/UIMessages.d.ts.map +1 -0
- package/dist/{UIMessages.js → vercel/UIMessages.js} +63 -1
- package/dist/vercel/UIMessages.js.map +1 -0
- package/dist/{client → vercel/client}/createTool.d.ts +23 -16
- package/dist/vercel/client/createTool.d.ts.map +1 -0
- package/dist/{client → vercel/client}/createTool.js +2 -0
- package/dist/vercel/client/createTool.js.map +1 -0
- package/dist/{client → vercel/client}/definePlaygroundAPI.d.ts +533 -79
- package/dist/{client → vercel/client}/definePlaygroundAPI.d.ts.map +1 -1
- package/dist/{client → vercel/client}/definePlaygroundAPI.js +8 -7
- package/dist/vercel/client/definePlaygroundAPI.js.map +1 -0
- package/dist/{client → vercel/client}/files.d.ts +2 -2
- package/dist/vercel/client/files.d.ts.map +1 -0
- package/dist/vercel/client/files.js.map +1 -0
- package/dist/vercel/client/messages.d.ts +542 -0
- package/dist/vercel/client/messages.d.ts.map +1 -0
- package/dist/vercel/client/messages.js +66 -0
- package/dist/vercel/client/messages.js.map +1 -0
- package/dist/{client → vercel/client}/mockModel.d.ts +17 -17
- package/dist/vercel/client/mockModel.d.ts.map +1 -0
- package/dist/{client → vercel/client}/mockModel.js +12 -13
- package/dist/vercel/client/mockModel.js.map +1 -0
- package/dist/{client → vercel/client}/saveInputMessages.d.ts +2 -2
- package/dist/vercel/client/saveInputMessages.d.ts.map +1 -0
- package/dist/vercel/client/saveInputMessages.js.map +1 -0
- package/dist/{client → vercel/client}/search.d.ts +119 -22
- package/dist/vercel/client/search.d.ts.map +1 -0
- package/dist/{client → vercel/client}/search.js +2 -2
- package/dist/vercel/client/search.js.map +1 -0
- package/dist/{client → vercel/client}/start.d.ts +26 -11
- package/dist/vercel/client/start.d.ts.map +1 -0
- package/dist/{client → vercel/client}/start.js +64 -22
- package/dist/vercel/client/start.js.map +1 -0
- package/dist/{client → vercel/client}/streamText.d.ts +13 -12
- package/dist/vercel/client/streamText.d.ts.map +1 -0
- package/dist/{client → vercel/client}/streamText.js +43 -9
- package/dist/vercel/client/streamText.js.map +1 -0
- package/dist/{client → vercel/client}/streaming.d.ts +1410 -183
- package/dist/vercel/client/streaming.d.ts.map +1 -0
- package/dist/{client → vercel/client}/streaming.js +120 -52
- package/dist/vercel/client/streaming.js.map +1 -0
- package/dist/vercel/client/types.d.ts +456 -0
- package/dist/vercel/client/types.d.ts.map +1 -0
- package/dist/vercel/client/types.js +3 -0
- package/dist/vercel/client/types.js.map +1 -0
- package/dist/{client → vercel/client}/utils.d.ts +3 -2
- package/dist/vercel/client/utils.d.ts.map +1 -0
- package/dist/{client → vercel/client}/utils.js +2 -2
- package/dist/vercel/client/utils.js.map +1 -0
- package/dist/{deltas.d.ts → vercel/deltas.d.ts} +3 -4
- package/dist/vercel/deltas.d.ts.map +1 -0
- package/dist/{deltas.js → vercel/deltas.js} +195 -99
- package/dist/vercel/deltas.js.map +1 -0
- package/dist/{client → vercel}/index.d.ts +482 -106
- package/dist/vercel/index.d.ts.map +1 -0
- package/dist/{client → vercel}/index.js +64 -46
- package/dist/vercel/index.js.map +1 -0
- package/dist/{mapping.d.ts → vercel/mapping.d.ts} +5 -19
- package/dist/vercel/mapping.d.ts.map +1 -0
- package/dist/{mapping.js → vercel/mapping.js} +411 -114
- package/dist/vercel/mapping.js.map +1 -0
- package/dist/{react → vercel/react}/optimisticallySendMessage.d.ts +2 -2
- package/dist/vercel/react/optimisticallySendMessage.d.ts.map +1 -0
- package/dist/vercel/react/optimisticallySendMessage.js.map +1 -0
- package/dist/{react → vercel/react}/types.d.ts +1 -1
- package/dist/vercel/react/types.d.ts.map +1 -0
- package/dist/{react → vercel/react}/types.js.map +1 -1
- package/dist/{react → vercel/react}/useDeltaStreams.d.ts +1 -1
- package/dist/vercel/react/useDeltaStreams.d.ts.map +1 -0
- package/dist/{react → vercel/react}/useDeltaStreams.js +1 -1
- package/dist/vercel/react/useDeltaStreams.js.map +1 -0
- package/dist/{react → vercel/react}/useStreamingUIMessages.d.ts +1 -1
- package/dist/vercel/react/useStreamingUIMessages.d.ts.map +1 -0
- package/dist/{react → vercel/react}/useStreamingUIMessages.js +7 -3
- package/dist/vercel/react/useStreamingUIMessages.js.map +1 -0
- package/dist/{react → vercel/react}/useThreadMessages.d.ts +1 -1
- package/dist/vercel/react/useThreadMessages.d.ts.map +1 -0
- package/dist/{react → vercel/react}/useThreadMessages.js +1 -1
- package/dist/vercel/react/useThreadMessages.js.map +1 -0
- package/dist/{react → vercel/react}/useUIMessages.d.ts +1 -1
- package/dist/vercel/react/useUIMessages.d.ts.map +1 -0
- package/dist/{react → vercel/react}/useUIMessages.js +1 -1
- package/dist/vercel/react/useUIMessages.js.map +1 -0
- package/package.json +29 -22
- package/src/client/messages.ts +5 -107
- package/src/client/types.ts +0 -608
- package/src/component/_generated/component.ts +1089 -48
- package/src/component/messages.test.ts +273 -1
- package/src/component/messages.ts +62 -32
- package/src/component/streams.ts +47 -47
- package/src/component/threads.ts +5 -2
- package/src/eslintConfig.test.ts +44 -0
- package/src/react/index.ts +8 -5
- package/src/shared.ts +26 -36
- package/src/streaming/materializePersistedUIMessageChunks.test.ts +513 -0
- package/src/streaming/materializePersistedUIMessageChunks.ts +398 -0
- package/src/streaming/persistedUIMessageChunks.test.ts +205 -0
- package/src/streaming/persistedUIMessageChunks.ts +658 -0
- package/src/validators.test.ts +4 -1
- package/src/validators.ts +89 -8
- package/src/{UIMessages.combineUIMessages.test.ts → vercel/UIMessages.combineUIMessages.test.ts} +4 -1
- package/src/{UIMessages.ts → vercel/UIMessages.ts} +95 -3
- package/src/{client → vercel/client}/approval.test.ts +23 -13
- package/src/{client → vercel/client}/createTool.ts +62 -22
- package/src/{client → vercel/client}/definePlaygroundAPI.ts +15 -16
- package/src/{client → vercel/client}/files.ts +2 -2
- package/src/{client → vercel/client}/index.test.ts +99 -31
- package/src/vercel/client/messages.ts +182 -0
- package/src/{client → vercel/client}/mockModel.ts +38 -39
- package/src/{client → vercel/client}/saveInputMessages.test.ts +1 -1
- package/src/{client → vercel/client}/saveInputMessages.ts +3 -3
- package/src/{client → vercel/client}/search.test.ts +3 -3
- package/src/{client → vercel/client}/search.ts +4 -4
- package/src/{client → vercel/client}/setup.test.ts +6 -3
- package/src/vercel/client/start.test.ts +14 -0
- package/src/{client → vercel/client}/start.ts +147 -36
- package/src/vercel/client/streamText.test.ts +223 -0
- package/src/{client → vercel/client}/streamText.ts +102 -31
- package/src/{client → vercel/client}/streaming.integration.test.ts +6 -3
- package/src/{client → vercel/client}/streaming.test.ts +178 -4
- package/src/{client → vercel/client}/streaming.ts +114 -52
- package/src/vercel/client/types.test.ts +13 -0
- package/src/vercel/client/types.ts +679 -0
- package/src/{client → vercel/client}/utils.ts +13 -7
- package/src/{deltas.test.ts → vercel/deltas.test.ts} +384 -93
- package/src/{deltas.ts → vercel/deltas.ts} +218 -115
- package/src/{fromUIMessages.test.ts → vercel/fromUIMessages.test.ts} +17 -1
- package/src/{client → vercel}/index.ts +167 -84
- package/src/{mapping.test.ts → vercel/mapping.test.ts} +377 -92
- package/src/{mapping.ts → vercel/mapping.ts} +501 -137
- package/src/{react → vercel/react}/optimisticallySendMessage.ts +2 -2
- package/src/{react → vercel/react}/types.ts +1 -1
- package/src/{react → vercel/react}/useDeltaStreams.ts +6 -2
- package/src/{react → vercel/react}/useStreamingUIMessages.ts +7 -3
- package/src/{react → vercel/react}/useThreadMessages.ts +2 -2
- package/src/{react → vercel/react}/useUIMessages.ts +2 -2
- package/src/{toUIMessages.test.ts → vercel/toUIMessages.test.ts} +49 -2
- package/dist/UIMessages.d.ts.map +0 -1
- package/dist/UIMessages.js.map +0 -1
- package/dist/client/createTool.d.ts.map +0 -1
- package/dist/client/createTool.js.map +0 -1
- package/dist/client/definePlaygroundAPI.js.map +0 -1
- package/dist/client/files.d.ts.map +0 -1
- package/dist/client/files.js.map +0 -1
- package/dist/client/index.d.ts.map +0 -1
- package/dist/client/index.js.map +0 -1
- package/dist/client/mockModel.d.ts.map +0 -1
- package/dist/client/mockModel.js.map +0 -1
- package/dist/client/saveInputMessages.d.ts.map +0 -1
- package/dist/client/saveInputMessages.js.map +0 -1
- package/dist/client/search.d.ts.map +0 -1
- package/dist/client/search.js.map +0 -1
- package/dist/client/start.d.ts.map +0 -1
- package/dist/client/start.js.map +0 -1
- package/dist/client/streamText.d.ts.map +0 -1
- package/dist/client/streamText.js.map +0 -1
- package/dist/client/streaming.d.ts.map +0 -1
- package/dist/client/streaming.js.map +0 -1
- package/dist/client/utils.d.ts.map +0 -1
- package/dist/client/utils.js.map +0 -1
- package/dist/deltas.d.ts.map +0 -1
- package/dist/deltas.js.map +0 -1
- package/dist/mapping.d.ts.map +0 -1
- package/dist/mapping.js.map +0 -1
- package/dist/react/optimisticallySendMessage.d.ts.map +0 -1
- package/dist/react/optimisticallySendMessage.js.map +0 -1
- package/dist/react/types.d.ts.map +0 -1
- package/dist/react/useDeltaStreams.d.ts.map +0 -1
- package/dist/react/useDeltaStreams.js.map +0 -1
- package/dist/react/useStreamingUIMessages.d.ts.map +0 -1
- package/dist/react/useStreamingUIMessages.js.map +0 -1
- package/dist/react/useThreadMessages.d.ts.map +0 -1
- package/dist/react/useThreadMessages.js.map +0 -1
- package/dist/react/useUIMessages.d.ts.map +0 -1
- package/dist/react/useUIMessages.js.map +0 -1
- package/src/client/streamText.test.ts +0 -102
- /package/dist/{client → vercel/client}/files.js +0 -0
- /package/dist/{client → vercel/client}/saveInputMessages.js +0 -0
- /package/dist/{react → vercel/react}/optimisticallySendMessage.js +0 -0
- /package/dist/{react → vercel/react}/types.js +0 -0
- /package/src/{UIMessages.test.ts → vercel/UIMessages.test.ts} +0 -0
- /package/src/{react → vercel/react}/useUIMessages.test.ts +0 -0
|
@@ -1,36 +1,87 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
type CallSettings,
|
|
2
|
+
isStepCount,
|
|
4
3
|
type GenerateObjectResult,
|
|
5
4
|
type IdGenerator,
|
|
6
5
|
type LanguageModel,
|
|
6
|
+
type Instructions,
|
|
7
7
|
type ModelMessage,
|
|
8
8
|
type StepResult,
|
|
9
9
|
type StopCondition,
|
|
10
10
|
type ToolSet,
|
|
11
11
|
} from "ai";
|
|
12
|
+
import type { Context } from "@ai-sdk/provider-utils";
|
|
12
13
|
import {
|
|
13
14
|
serializeResponseMessages,
|
|
14
15
|
serializeObjectResult,
|
|
15
16
|
} from "../mapping.js";
|
|
16
17
|
import { embedMessages, fetchContextWithPrompt } from "./search.js";
|
|
17
|
-
import type {
|
|
18
|
-
|
|
18
|
+
import type {
|
|
19
|
+
ActionCtx,
|
|
20
|
+
AgentCallSettings,
|
|
21
|
+
AgentComponent,
|
|
22
|
+
Config,
|
|
23
|
+
Options,
|
|
24
|
+
} from "./types.js";
|
|
25
|
+
import type { Message, MessageDoc } from "../../validators.js";
|
|
19
26
|
import {
|
|
20
27
|
getModelName,
|
|
21
28
|
getProviderName,
|
|
22
29
|
type ModelOrMetadata,
|
|
23
|
-
} from "
|
|
30
|
+
} from "../../shared.js";
|
|
24
31
|
import { wrapTools, type ToolCtx } from "./createTool.js";
|
|
25
|
-
import type { Agent } from "
|
|
32
|
+
import type { Agent } from "../index.js";
|
|
26
33
|
import { assert, omit } from "convex-helpers";
|
|
27
34
|
import { saveInputMessages } from "./saveInputMessages.js";
|
|
28
35
|
import type { GenericActionCtx, GenericDataModel } from "convex/server";
|
|
29
36
|
|
|
37
|
+
export function resolveUsageModel(
|
|
38
|
+
toSave:
|
|
39
|
+
| { step: { model?: ModelOrMetadata } }
|
|
40
|
+
| { object: unknown },
|
|
41
|
+
activeModel: ModelOrMetadata,
|
|
42
|
+
): ModelOrMetadata {
|
|
43
|
+
return "step" in toSave ? (toSave.step.model ?? activeModel) : activeModel;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type RawRequestResponseInclude = Record<string, boolean>;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The raw handler promises request and, for non-streaming calls, response
|
|
50
|
+
* bodies. Preserve explicit caller choices while opting in to those bodies.
|
|
51
|
+
*/
|
|
52
|
+
function rawRequestResponseInclude(
|
|
53
|
+
args: {
|
|
54
|
+
experimental_include?: RawRequestResponseInclude;
|
|
55
|
+
include?: RawRequestResponseInclude;
|
|
56
|
+
},
|
|
57
|
+
enabled: boolean,
|
|
58
|
+
operation:
|
|
59
|
+
| "generateText"
|
|
60
|
+
| "streamText"
|
|
61
|
+
| "generateObject"
|
|
62
|
+
| "streamObject"
|
|
63
|
+
| undefined,
|
|
64
|
+
): RawRequestResponseInclude | undefined {
|
|
65
|
+
if (!enabled) return undefined;
|
|
66
|
+
|
|
67
|
+
const requested = {
|
|
68
|
+
...args.experimental_include,
|
|
69
|
+
...args.include,
|
|
70
|
+
};
|
|
71
|
+
return {
|
|
72
|
+
...requested,
|
|
73
|
+
requestBody: requested.requestBody ?? true,
|
|
74
|
+
...((operation === "generateText" || operation === "generateObject")
|
|
75
|
+
? { responseBody: requested.responseBody ?? true }
|
|
76
|
+
: {}),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
30
80
|
export async function startGeneration<
|
|
31
81
|
T,
|
|
32
82
|
Tools extends ToolSet = ToolSet,
|
|
33
83
|
CustomCtx extends object = object,
|
|
84
|
+
RUNTIME_CONTEXT extends Context = Context,
|
|
34
85
|
>(
|
|
35
86
|
ctx: ActionCtx & CustomCtx,
|
|
36
87
|
component: AgentComponent,
|
|
@@ -74,13 +125,20 @@ export async function startGeneration<
|
|
|
74
125
|
* the promptMessageId message, if provided.
|
|
75
126
|
*/
|
|
76
127
|
messages?: (ModelMessage | Message)[];
|
|
128
|
+
instructions?: Instructions;
|
|
129
|
+
/** @deprecated Use instructions. */
|
|
130
|
+
system?: Instructions;
|
|
131
|
+
allowSystemInMessages?: boolean;
|
|
77
132
|
/**
|
|
78
133
|
* The abort signal to be passed to the LLM call. If triggered, it will
|
|
79
134
|
* mark the pending message as failed. If the generation is asynchronously
|
|
80
135
|
* aborted, it will trigger this signal when detected.
|
|
81
136
|
*/
|
|
82
137
|
abortSignal?: AbortSignal;
|
|
83
|
-
|
|
138
|
+
runtimeContext?: RUNTIME_CONTEXT;
|
|
139
|
+
stopWhen?:
|
|
140
|
+
| StopCondition<Tools, RUNTIME_CONTEXT>
|
|
141
|
+
| Array<StopCondition<Tools, RUNTIME_CONTEXT>>;
|
|
84
142
|
_internal?: { generateId?: IdGenerator };
|
|
85
143
|
},
|
|
86
144
|
{
|
|
@@ -94,14 +152,20 @@ export async function startGeneration<
|
|
|
94
152
|
agentName: string;
|
|
95
153
|
agentForToolCtx?: Agent;
|
|
96
154
|
},
|
|
155
|
+
_operation?:
|
|
156
|
+
| "generateText"
|
|
157
|
+
| "streamText"
|
|
158
|
+
| "generateObject"
|
|
159
|
+
| "streamObject",
|
|
97
160
|
): Promise<{
|
|
98
161
|
args: T & {
|
|
99
|
-
|
|
162
|
+
instructions?: Instructions;
|
|
100
163
|
model: LanguageModel;
|
|
101
164
|
messages: ModelMessage[];
|
|
102
165
|
prompt?: never;
|
|
103
166
|
tools?: Tools;
|
|
104
|
-
|
|
167
|
+
runtimeContext?: RUNTIME_CONTEXT;
|
|
168
|
+
} & AgentCallSettings;
|
|
105
169
|
order: number;
|
|
106
170
|
stepOrder: number;
|
|
107
171
|
userId: string | undefined;
|
|
@@ -109,7 +173,10 @@ export async function startGeneration<
|
|
|
109
173
|
updateModel: (model: ModelOrMetadata | undefined) => void;
|
|
110
174
|
save: <TOOLS extends ToolSet>(
|
|
111
175
|
toSave:
|
|
112
|
-
| {
|
|
176
|
+
| {
|
|
177
|
+
step: StepResult<TOOLS, RUNTIME_CONTEXT>;
|
|
178
|
+
responseMessages?: ModelMessage[];
|
|
179
|
+
}
|
|
113
180
|
| { object: GenerateObjectResult<unknown> },
|
|
114
181
|
createPendingMessage?: boolean,
|
|
115
182
|
finishStreamId?: string,
|
|
@@ -132,6 +199,15 @@ export async function startGeneration<
|
|
|
132
199
|
prompt: args.prompt,
|
|
133
200
|
promptMessageId: args.promptMessageId,
|
|
134
201
|
});
|
|
202
|
+
const allowSystemInMessages = args.allowSystemInMessages ?? true;
|
|
203
|
+
if (
|
|
204
|
+
!allowSystemInMessages &&
|
|
205
|
+
context.messages.some((message) => message.role === "system")
|
|
206
|
+
) {
|
|
207
|
+
throw new Error(
|
|
208
|
+
"System messages in assembled message history require allowSystemInMessages: true. Use instructions for top-level system guidance.",
|
|
209
|
+
);
|
|
210
|
+
}
|
|
135
211
|
|
|
136
212
|
const saveMessages = opts.storageOptions?.saveMessages ?? "promptAndOutput";
|
|
137
213
|
const { promptMessageId, pendingMessage, savedMessages } =
|
|
@@ -159,13 +235,22 @@ export async function startGeneration<
|
|
|
159
235
|
assert(model, "model is required");
|
|
160
236
|
let activeModel: ModelOrMetadata = model;
|
|
161
237
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
238
|
+
// Both the caller's AbortSignal listener and AI SDK's onAbort can report
|
|
239
|
+
// the same cancellation. Share the one finalization instead of racing two
|
|
240
|
+
// mutations for the pending message.
|
|
241
|
+
let pendingMessageFailure: Promise<void> | undefined;
|
|
242
|
+
const fail = (reason: string): Promise<void> => {
|
|
243
|
+
if (!pendingMessageId) return Promise.resolve();
|
|
244
|
+
if (!pendingMessageFailure) {
|
|
245
|
+
const messageId = pendingMessageId;
|
|
246
|
+
pendingMessageFailure = ctx
|
|
247
|
+
.runMutation(component.messages.finalizeMessage, {
|
|
248
|
+
messageId,
|
|
249
|
+
result: { status: "failed", error: reason },
|
|
250
|
+
})
|
|
251
|
+
.then(() => undefined);
|
|
168
252
|
}
|
|
253
|
+
return pendingMessageFailure;
|
|
169
254
|
};
|
|
170
255
|
if (args.abortSignal) {
|
|
171
256
|
const abortSignal = args.abortSignal;
|
|
@@ -185,34 +270,52 @@ export async function startGeneration<
|
|
|
185
270
|
agent: opts.agentForToolCtx,
|
|
186
271
|
} satisfies ToolCtx;
|
|
187
272
|
const tools = wrapTools(toolCtx, args.tools) as Tools;
|
|
273
|
+
const argsWithoutSystem = omit(
|
|
274
|
+
args as typeof args & { system?: Instructions },
|
|
275
|
+
["system"],
|
|
276
|
+
);
|
|
277
|
+
const {
|
|
278
|
+
promptMessageId: _promptMessageId,
|
|
279
|
+
messages: _messages,
|
|
280
|
+
prompt: _prompt,
|
|
281
|
+
instructions: _instructions,
|
|
282
|
+
onStepFinish: _onStepFinish,
|
|
283
|
+
...aiCallArgs
|
|
284
|
+
} = argsWithoutSystem as typeof argsWithoutSystem & {
|
|
285
|
+
onStepFinish?: unknown;
|
|
286
|
+
};
|
|
287
|
+
const include = rawRequestResponseInclude(
|
|
288
|
+
aiCallArgs as {
|
|
289
|
+
experimental_include?: RawRequestResponseInclude;
|
|
290
|
+
include?: RawRequestResponseInclude;
|
|
291
|
+
},
|
|
292
|
+
Boolean(opts.rawRequestResponseHandler),
|
|
293
|
+
_operation,
|
|
294
|
+
);
|
|
188
295
|
const aiArgs = {
|
|
189
296
|
...opts.callSettings,
|
|
190
297
|
providerOptions: opts.providerOptions,
|
|
191
|
-
...
|
|
298
|
+
...aiCallArgs,
|
|
192
299
|
model,
|
|
193
300
|
messages: context.messages,
|
|
301
|
+
instructions: args.instructions ?? args.system,
|
|
302
|
+
allowSystemInMessages,
|
|
194
303
|
stopWhen:
|
|
195
|
-
args.stopWhen ?? (opts.maxSteps ?
|
|
304
|
+
args.stopWhen ?? (opts.maxSteps ? isStepCount(opts.maxSteps) : undefined),
|
|
196
305
|
tools,
|
|
197
|
-
|
|
306
|
+
...(include ? { include } : {}),
|
|
307
|
+
} as unknown as T & {
|
|
198
308
|
model: LanguageModel;
|
|
199
309
|
messages: ModelMessage[];
|
|
200
310
|
prompt?: never;
|
|
201
311
|
tools?: Tools;
|
|
202
312
|
_internal?: { generateId?: IdGenerator };
|
|
203
|
-
} &
|
|
313
|
+
} & AgentCallSettings;
|
|
204
314
|
// NOTE: We intentionally do NOT override _internal.generateId here.
|
|
205
315
|
// The AI SDK uses generateId() for many internal IDs (approval IDs,
|
|
206
316
|
// tool execution IDs, message IDs, etc.) and they must be unique.
|
|
207
317
|
// The pending message is linked via the explicit `pendingMessageId`
|
|
208
318
|
// parameter passed to addMessages in the save closure.
|
|
209
|
-
// Track how many response messages we've already saved across steps.
|
|
210
|
-
// step.response.messages is cumulative — each step appends to it.
|
|
211
|
-
// We need to know which messages are new in each step to serialize
|
|
212
|
-
// only the new ones (important for tool approval flows where the SDK
|
|
213
|
-
// may add extra messages like approval tool-results).
|
|
214
|
-
let previousResponseMessageCount = 0;
|
|
215
|
-
|
|
216
319
|
return {
|
|
217
320
|
args: aiArgs,
|
|
218
321
|
order: order ?? 0,
|
|
@@ -228,7 +331,10 @@ export async function startGeneration<
|
|
|
228
331
|
fail,
|
|
229
332
|
save: async <TOOLS extends ToolSet>(
|
|
230
333
|
toSave:
|
|
231
|
-
| {
|
|
334
|
+
| {
|
|
335
|
+
step: StepResult<TOOLS, RUNTIME_CONTEXT>;
|
|
336
|
+
responseMessages?: ModelMessage[];
|
|
337
|
+
}
|
|
232
338
|
| { object: GenerateObjectResult<unknown> },
|
|
233
339
|
createPendingMessage?: boolean,
|
|
234
340
|
/**
|
|
@@ -247,17 +353,20 @@ export async function startGeneration<
|
|
|
247
353
|
activeModel,
|
|
248
354
|
);
|
|
249
355
|
} else {
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
356
|
+
const newResponseMessages =
|
|
357
|
+
toSave.responseMessages ?? toSave.step.response.messages;
|
|
358
|
+
// Even an empty completed step needs a durable assistant result so
|
|
359
|
+
// the pending message can be finalized at the storage boundary.
|
|
360
|
+
const responseMessagesToSave: ModelMessage[] =
|
|
361
|
+
newResponseMessages.length > 0
|
|
362
|
+
? newResponseMessages
|
|
363
|
+
: [{ role: "assistant", content: [] }];
|
|
255
364
|
serialized = await serializeResponseMessages(
|
|
256
365
|
ctx,
|
|
257
366
|
component,
|
|
258
367
|
toSave.step,
|
|
259
368
|
activeModel,
|
|
260
|
-
|
|
369
|
+
responseMessagesToSave,
|
|
261
370
|
);
|
|
262
371
|
}
|
|
263
372
|
const embeddings = await embedMessages(
|
|
@@ -294,6 +403,7 @@ export async function startGeneration<
|
|
|
294
403
|
);
|
|
295
404
|
} else {
|
|
296
405
|
pendingMessageId = lastMessage._id;
|
|
406
|
+
pendingMessageFailure = undefined;
|
|
297
407
|
savedMessages.push(...saved.messages.slice(0, -1));
|
|
298
408
|
}
|
|
299
409
|
} else {
|
|
@@ -312,12 +422,13 @@ export async function startGeneration<
|
|
|
312
422
|
});
|
|
313
423
|
}
|
|
314
424
|
if (opts.usageHandler && output.usage) {
|
|
425
|
+
const usageModel = resolveUsageModel(toSave, activeModel);
|
|
315
426
|
await opts.usageHandler(ctx, {
|
|
316
427
|
userId,
|
|
317
428
|
threadId,
|
|
318
429
|
agentName: opts.agentName,
|
|
319
|
-
model: getModelName(
|
|
320
|
-
provider: getProviderName(
|
|
430
|
+
model: getModelName(usageModel),
|
|
431
|
+
provider: getProviderName(usageModel),
|
|
321
432
|
usage: output.usage,
|
|
322
433
|
providerMetadata: output.providerMetadata,
|
|
323
434
|
});
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { Agent, createThread } from "../index.js";
|
|
3
|
+
import {
|
|
4
|
+
defineSchema,
|
|
5
|
+
type DataModelFromSchemaDefinition,
|
|
6
|
+
type ApiFromModules,
|
|
7
|
+
type ActionBuilder,
|
|
8
|
+
actionGeneric,
|
|
9
|
+
anyApi,
|
|
10
|
+
} from "convex/server";
|
|
11
|
+
import { v } from "convex/values";
|
|
12
|
+
import { components, initConvexTest } from "./setup.test.js";
|
|
13
|
+
import { mockModel } from "./mockModel.js";
|
|
14
|
+
import { runAbortCleanup } from "./streamText.js";
|
|
15
|
+
|
|
16
|
+
const schema = defineSchema({});
|
|
17
|
+
type DataModel = DataModelFromSchemaDefinition<typeof schema>;
|
|
18
|
+
const action = actionGeneric as ActionBuilder<DataModel, "public">;
|
|
19
|
+
|
|
20
|
+
const FINAL_TEXT = "Hello from the model";
|
|
21
|
+
|
|
22
|
+
const agent = new Agent(components.agent, {
|
|
23
|
+
name: "stream-test",
|
|
24
|
+
languageModel: mockModel({
|
|
25
|
+
content: [{ type: "text", text: FINAL_TEXT }],
|
|
26
|
+
}),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const emptyAgent = new Agent(components.agent, {
|
|
30
|
+
name: "empty-stream-test",
|
|
31
|
+
languageModel: mockModel({
|
|
32
|
+
content: [],
|
|
33
|
+
providerMetadata: { mock: { emptyResponse: true } },
|
|
34
|
+
}),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Action that exercises streamText with saveStreamDeltas.returnImmediately=true.
|
|
38
|
+
// It consumes the stream after streamText returns, simulating the HTTP response
|
|
39
|
+
// path described in issue #265.
|
|
40
|
+
export const streamTextReturnImmediately = action({
|
|
41
|
+
args: { threadId: v.string() },
|
|
42
|
+
handler: async (ctx, { threadId }) => {
|
|
43
|
+
const result = await agent.streamText(
|
|
44
|
+
ctx,
|
|
45
|
+
{ threadId },
|
|
46
|
+
{ prompt: "Test" },
|
|
47
|
+
{
|
|
48
|
+
saveStreamDeltas: {
|
|
49
|
+
returnImmediately: true,
|
|
50
|
+
chunking: "word",
|
|
51
|
+
throttleMs: 0,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
);
|
|
55
|
+
// Drain the stream the way an HTTP response would. This triggers
|
|
56
|
+
// onStepFinish for every step, including the final one.
|
|
57
|
+
await result.consumeStream();
|
|
58
|
+
return { ok: true };
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
export const streamTextEmptyAwaited = action({
|
|
63
|
+
args: { threadId: v.string() },
|
|
64
|
+
handler: async (ctx, { threadId }) => {
|
|
65
|
+
await emptyAgent.streamText(
|
|
66
|
+
ctx,
|
|
67
|
+
{ threadId },
|
|
68
|
+
{ prompt: "Test" },
|
|
69
|
+
{ saveStreamDeltas: true },
|
|
70
|
+
);
|
|
71
|
+
return { ok: true };
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
export const streamTextEmptyReturnImmediately = action({
|
|
76
|
+
args: { threadId: v.string() },
|
|
77
|
+
handler: async (ctx, { threadId }) => {
|
|
78
|
+
const result = await emptyAgent.streamText(
|
|
79
|
+
ctx,
|
|
80
|
+
{ threadId },
|
|
81
|
+
{ prompt: "Test" },
|
|
82
|
+
{
|
|
83
|
+
saveStreamDeltas: {
|
|
84
|
+
returnImmediately: true,
|
|
85
|
+
throttleMs: 0,
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
);
|
|
89
|
+
await result.consumeStream();
|
|
90
|
+
return { ok: true };
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const testApi: ApiFromModules<{
|
|
95
|
+
fns: {
|
|
96
|
+
streamTextReturnImmediately: typeof streamTextReturnImmediately;
|
|
97
|
+
streamTextEmptyAwaited: typeof streamTextEmptyAwaited;
|
|
98
|
+
streamTextEmptyReturnImmediately: typeof streamTextEmptyReturnImmediately;
|
|
99
|
+
};
|
|
100
|
+
}>["fns"] = anyApi["streamText.test"] as any;
|
|
101
|
+
|
|
102
|
+
describe("streamText with saveStreamDeltas.returnImmediately (issue #265)", () => {
|
|
103
|
+
test("persists the final assistant text to the messages table", async () => {
|
|
104
|
+
const t = initConvexTest(schema);
|
|
105
|
+
const threadId = await t.run(async (ctx) =>
|
|
106
|
+
createThread(ctx, components.agent, { userId: "u1" }),
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
await t.action(testApi.streamTextReturnImmediately, { threadId });
|
|
110
|
+
|
|
111
|
+
// Allow any background work scheduled by consumeStream to settle.
|
|
112
|
+
await t.finishAllScheduledFunctions(() => {});
|
|
113
|
+
|
|
114
|
+
const messages = await t.run(async (ctx) =>
|
|
115
|
+
agent.listMessages(ctx, {
|
|
116
|
+
threadId,
|
|
117
|
+
paginationOpts: { cursor: null, numItems: 50 },
|
|
118
|
+
}),
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const assistantTextMessages = messages.page.filter(
|
|
122
|
+
(m) =>
|
|
123
|
+
m.message?.role === "assistant" &&
|
|
124
|
+
typeof m.text === "string" &&
|
|
125
|
+
m.text.length > 0,
|
|
126
|
+
);
|
|
127
|
+
expect(
|
|
128
|
+
assistantTextMessages.length,
|
|
129
|
+
"expected at least one persisted assistant message with text",
|
|
130
|
+
).toBeGreaterThan(0);
|
|
131
|
+
|
|
132
|
+
const combined = assistantTextMessages.map((m) => m.text).join("");
|
|
133
|
+
expect(combined).toContain(FINAL_TEXT);
|
|
134
|
+
|
|
135
|
+
// The stream should be marked finished, not stuck in "streaming".
|
|
136
|
+
const stillStreaming = await t.run(async (ctx) =>
|
|
137
|
+
ctx.runQuery(components.agent.streams.list, {
|
|
138
|
+
threadId,
|
|
139
|
+
statuses: ["streaming"],
|
|
140
|
+
}),
|
|
141
|
+
);
|
|
142
|
+
expect(
|
|
143
|
+
stillStreaming,
|
|
144
|
+
"stream should not be stuck in 'streaming' status",
|
|
145
|
+
).toHaveLength(0);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe("streamText abort cleanup", () => {
|
|
150
|
+
test("attempts every cleanup and rethrows the first internal failure", async () => {
|
|
151
|
+
const calls: string[] = [];
|
|
152
|
+
const firstFailure = new Error("failed pending message cleanup");
|
|
153
|
+
|
|
154
|
+
await expect(
|
|
155
|
+
runAbortCleanup({
|
|
156
|
+
failCall: async () => {
|
|
157
|
+
calls.push("call.fail");
|
|
158
|
+
throw firstFailure;
|
|
159
|
+
},
|
|
160
|
+
failStreamer: async () => {
|
|
161
|
+
calls.push("streamer.fail");
|
|
162
|
+
throw new Error("failed stream cleanup");
|
|
163
|
+
},
|
|
164
|
+
onAbort: () => {
|
|
165
|
+
calls.push("user.onAbort");
|
|
166
|
+
},
|
|
167
|
+
}),
|
|
168
|
+
).rejects.toBe(firstFailure);
|
|
169
|
+
|
|
170
|
+
expect(calls).toEqual(["call.fail", "streamer.fail", "user.onAbort"]);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe("streamText with an empty final step (issue #274)", () => {
|
|
175
|
+
test.each([
|
|
176
|
+
["awaited", testApi.streamTextEmptyAwaited],
|
|
177
|
+
["returnImmediately", testApi.streamTextEmptyReturnImmediately],
|
|
178
|
+
])(
|
|
179
|
+
"finalizes the pending assistant message in the %s path",
|
|
180
|
+
async (_, fn) => {
|
|
181
|
+
const t = initConvexTest(schema);
|
|
182
|
+
const threadId = await t.run(async (ctx) =>
|
|
183
|
+
createThread(ctx, components.agent, { userId: "u1" }),
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
await t.action(fn, { threadId });
|
|
187
|
+
await t.finishAllScheduledFunctions(() => {});
|
|
188
|
+
|
|
189
|
+
const messages = await t.run(async (ctx) =>
|
|
190
|
+
emptyAgent.listMessages(ctx, {
|
|
191
|
+
threadId,
|
|
192
|
+
paginationOpts: { cursor: null, numItems: 50 },
|
|
193
|
+
}),
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
expect(
|
|
197
|
+
messages.page.filter((message) => message.status === "pending"),
|
|
198
|
+
).toHaveLength(0);
|
|
199
|
+
expect(messages.page).toContainEqual(
|
|
200
|
+
expect.objectContaining({
|
|
201
|
+
status: "success",
|
|
202
|
+
message: { role: "assistant", content: [] },
|
|
203
|
+
model: "mock-model-id",
|
|
204
|
+
provider: "mock-provider",
|
|
205
|
+
providerMetadata: { mock: { emptyResponse: true } },
|
|
206
|
+
usage: expect.objectContaining({
|
|
207
|
+
promptTokens: 3,
|
|
208
|
+
completionTokens: 10,
|
|
209
|
+
totalTokens: 13,
|
|
210
|
+
}),
|
|
211
|
+
}),
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
const stillStreaming = await t.run(async (ctx) =>
|
|
215
|
+
ctx.runQuery(components.agent.streams.list, {
|
|
216
|
+
threadId,
|
|
217
|
+
statuses: ["streaming"],
|
|
218
|
+
}),
|
|
219
|
+
);
|
|
220
|
+
expect(stillStreaming).toHaveLength(0);
|
|
221
|
+
},
|
|
222
|
+
);
|
|
223
|
+
});
|