@convex-dev/agent 0.6.0-alpha.0 → 0.6.0-alpha.1
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/dist/client/createTool.d.ts +18 -21
- package/dist/client/createTool.d.ts.map +1 -1
- package/dist/client/createTool.js +3 -2
- package/dist/client/createTool.js.map +1 -1
- package/dist/client/definePlaygroundAPI.d.ts +48 -48
- package/dist/client/index.d.ts +109 -62
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +92 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/messages.d.ts +12 -12
- package/dist/client/mockModel.d.ts.map +1 -1
- package/dist/client/mockModel.js +9 -2
- package/dist/client/mockModel.js.map +1 -1
- package/dist/client/search.d.ts +12 -12
- package/dist/client/search.d.ts.map +1 -1
- package/dist/client/search.js +3 -3
- package/dist/client/search.js.map +1 -1
- package/dist/client/start.d.ts +1 -1
- package/dist/client/start.d.ts.map +1 -1
- package/dist/client/start.js +28 -14
- package/dist/client/start.js.map +1 -1
- package/dist/client/streamText.d.ts.map +1 -1
- package/dist/client/streamText.js +27 -3
- package/dist/client/streamText.js.map +1 -1
- package/dist/client/streaming.d.ts +105 -94
- package/dist/client/streaming.d.ts.map +1 -1
- package/dist/client/streaming.js +28 -8
- package/dist/client/streaming.js.map +1 -1
- package/dist/client/types.d.ts +13 -12
- package/dist/client/types.d.ts.map +1 -1
- package/dist/component/_generated/component.d.ts +1 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/messages.d.ts +143 -142
- package/dist/component/messages.d.ts.map +1 -1
- package/dist/component/messages.js +13 -3
- package/dist/component/messages.js.map +1 -1
- package/dist/component/streams.d.ts +5 -5
- package/dist/component/threads.d.ts +19 -19
- package/dist/component/users.d.ts +3 -3
- package/dist/deltas.d.ts.map +1 -1
- package/dist/deltas.js +0 -1
- package/dist/deltas.js.map +1 -1
- package/dist/mapping.d.ts +19 -1
- package/dist/mapping.d.ts.map +1 -1
- package/dist/mapping.js +57 -6
- package/dist/mapping.js.map +1 -1
- package/dist/react/useDeltaStreams.d.ts.map +1 -1
- package/dist/react/useDeltaStreams.js +5 -0
- package/dist/react/useDeltaStreams.js.map +1 -1
- package/package.json +4 -2
- package/src/client/approval.test.ts +350 -0
- package/src/client/createTool.ts +50 -52
- package/src/client/index.ts +120 -1
- package/src/client/mockModel.ts +9 -2
- package/src/client/search.ts +7 -3
- package/src/client/start.ts +41 -24
- package/src/client/streamText.ts +27 -3
- package/src/client/streaming.ts +33 -11
- 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 +77 -0
- package/src/mapping.ts +62 -6
- package/src/react/useDeltaStreams.ts +6 -0
package/src/mapping.test.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
toModelMessage,
|
|
8
8
|
serializeContent,
|
|
9
9
|
toModelMessageContent,
|
|
10
|
+
mergeApprovalResponseMessages,
|
|
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,80 @@ describe("mapping", () => {
|
|
|
257
258
|
expect(content).toHaveLength(1);
|
|
258
259
|
expect((content as unknown[])[0]).toMatchObject(approvalResponse);
|
|
259
260
|
});
|
|
261
|
+
|
|
262
|
+
test("mergeApprovalResponseMessages merges consecutive tool approval messages", () => {
|
|
263
|
+
const messages = [
|
|
264
|
+
{ role: "user" as const, content: "hello" },
|
|
265
|
+
{
|
|
266
|
+
role: "assistant" as const,
|
|
267
|
+
content: [
|
|
268
|
+
{ type: "tool-call", toolCallId: "tc1", toolName: "a", input: {} },
|
|
269
|
+
{ type: "tool-call", toolCallId: "tc2", toolName: "b", input: {} },
|
|
270
|
+
{ type: "tool-approval-request", approvalId: "ap1", toolCallId: "tc1" },
|
|
271
|
+
{ type: "tool-approval-request", approvalId: "ap2", toolCallId: "tc2" },
|
|
272
|
+
],
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
role: "tool" as const,
|
|
276
|
+
content: [
|
|
277
|
+
{ type: "tool-approval-response", approvalId: "ap1", approved: true },
|
|
278
|
+
],
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
role: "tool" as const,
|
|
282
|
+
content: [
|
|
283
|
+
{ type: "tool-approval-response", approvalId: "ap2", approved: false, reason: "denied" },
|
|
284
|
+
],
|
|
285
|
+
},
|
|
286
|
+
] as any;
|
|
287
|
+
|
|
288
|
+
const merged = mergeApprovalResponseMessages(messages);
|
|
289
|
+
expect(merged).toHaveLength(3); // user, assistant, single tool
|
|
290
|
+
expect(merged[2].role).toBe("tool");
|
|
291
|
+
const toolContent = merged[2].content as Array<{ type: string; approvalId: string }>;
|
|
292
|
+
expect(toolContent).toHaveLength(2);
|
|
293
|
+
expect(toolContent[0].approvalId).toBe("ap1");
|
|
294
|
+
expect(toolContent[1].approvalId).toBe("ap2");
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test("mergeApprovalResponseMessages does not mutate original message content arrays", () => {
|
|
298
|
+
const msg1Content = [
|
|
299
|
+
{ type: "tool-approval-response", approvalId: "ap1", approved: true },
|
|
300
|
+
];
|
|
301
|
+
const msg2Content = [
|
|
302
|
+
{ type: "tool-approval-response", approvalId: "ap2", approved: false, reason: "denied" },
|
|
303
|
+
];
|
|
304
|
+
const messages = [
|
|
305
|
+
{ role: "tool" as const, content: msg1Content },
|
|
306
|
+
{ role: "tool" as const, content: msg2Content },
|
|
307
|
+
] as any;
|
|
308
|
+
|
|
309
|
+
const merged = mergeApprovalResponseMessages(messages);
|
|
310
|
+
// Merged result should combine both
|
|
311
|
+
expect(merged).toHaveLength(1);
|
|
312
|
+
expect((merged[0].content as any[]).length).toBe(2);
|
|
313
|
+
// Original arrays must be untouched
|
|
314
|
+
expect(msg1Content).toHaveLength(1);
|
|
315
|
+
expect(msg2Content).toHaveLength(1);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("mergeApprovalResponseMessages does not merge non-approval tool messages", () => {
|
|
319
|
+
const messages = [
|
|
320
|
+
{
|
|
321
|
+
role: "tool" as const,
|
|
322
|
+
content: [
|
|
323
|
+
{ type: "tool-result", toolCallId: "tc1", toolName: "a", output: { type: "text", value: "ok" } },
|
|
324
|
+
],
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
role: "tool" as const,
|
|
328
|
+
content: [
|
|
329
|
+
{ type: "tool-approval-response", approvalId: "ap1", approved: true },
|
|
330
|
+
],
|
|
331
|
+
},
|
|
332
|
+
] as any;
|
|
333
|
+
|
|
334
|
+
const merged = mergeApprovalResponseMessages(messages);
|
|
335
|
+
expect(merged).toHaveLength(2); // not merged since first has tool-result
|
|
336
|
+
});
|
|
260
337
|
});
|
package/src/mapping.ts
CHANGED
|
@@ -139,6 +139,44 @@ export function docsToModelMessages(messages: MessageDoc[]): ModelMessage[] {
|
|
|
139
139
|
.map(toModelMessage);
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Merge consecutive tool messages that contain `tool-approval-response` parts
|
|
144
|
+
* into a single tool message. The AI SDK's `collectToolApprovals` only examines
|
|
145
|
+
* the last tool message, so when multiple approvals are saved as separate
|
|
146
|
+
* messages (e.g. approve tool A, then deny tool B), they must be combined
|
|
147
|
+
* for the SDK to process them all.
|
|
148
|
+
*/
|
|
149
|
+
export function mergeApprovalResponseMessages(
|
|
150
|
+
messages: ModelMessage[],
|
|
151
|
+
): ModelMessage[] {
|
|
152
|
+
const result: ModelMessage[] = [];
|
|
153
|
+
for (const msg of messages) {
|
|
154
|
+
const prev = result.at(-1);
|
|
155
|
+
if (
|
|
156
|
+
msg.role === "tool" &&
|
|
157
|
+
prev?.role === "tool" &&
|
|
158
|
+
Array.isArray(msg.content) &&
|
|
159
|
+
Array.isArray(prev.content) &&
|
|
160
|
+
hasApprovalResponse(msg.content) &&
|
|
161
|
+
hasApprovalResponse(prev.content)
|
|
162
|
+
) {
|
|
163
|
+
// Clone before merging to avoid mutating the original message's content array
|
|
164
|
+
const cloned = { ...prev, content: [...(prev.content as any[])] };
|
|
165
|
+
result[result.length - 1] = cloned;
|
|
166
|
+
(cloned.content as any[]).push(...(msg.content as any[]));
|
|
167
|
+
} else {
|
|
168
|
+
result.push(msg);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function hasApprovalResponse(content: any[]): boolean {
|
|
175
|
+
return content.some(
|
|
176
|
+
(p: any) => p.type === "tool-approval-response",
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
142
180
|
export function serializeUsage(usage: LanguageModelUsage): Usage {
|
|
143
181
|
return {
|
|
144
182
|
promptTokens: usage.inputTokens ?? 0,
|
|
@@ -211,6 +249,16 @@ export async function serializeNewMessagesInStep<TOOLS extends ToolSet>(
|
|
|
211
249
|
component: AgentComponent,
|
|
212
250
|
step: StepResult<TOOLS>,
|
|
213
251
|
model: ModelOrMetadata | undefined,
|
|
252
|
+
/**
|
|
253
|
+
* If provided, these are the new response messages for this step
|
|
254
|
+
* (pre-sliced by the caller). When not provided, falls back to the
|
|
255
|
+
* existing heuristic of slicing the last 1-2 messages.
|
|
256
|
+
*
|
|
257
|
+
* This is needed for tool approval flows where the SDK adds extra
|
|
258
|
+
* messages (e.g. approval tool-results) at the beginning of
|
|
259
|
+
* responseMessages that the old slice(-1/-2) logic would miss.
|
|
260
|
+
*/
|
|
261
|
+
newResponseMessages?: ModelMessage[],
|
|
214
262
|
): Promise<{ messages: MessageWithMetadata[] }> {
|
|
215
263
|
// If there are tool results, there's another message with the tool results
|
|
216
264
|
// ref: https://github.com/vercel/ai/blob/main/packages/ai/src/generate-text/to-response-messages.ts#L120
|
|
@@ -228,13 +276,21 @@ export async function serializeNewMessagesInStep<TOOLS extends ToolSet>(
|
|
|
228
276
|
sources: hasToolMessage ? undefined : step.sources,
|
|
229
277
|
} satisfies Omit<MessageWithMetadata, "message" | "text" | "fileIds">;
|
|
230
278
|
const toolFields = { sources: step.sources };
|
|
279
|
+
|
|
280
|
+
// Determine which messages to serialize for this step
|
|
281
|
+
let messagesToSerialize: ModelMessage[];
|
|
282
|
+
if (newResponseMessages) {
|
|
283
|
+
messagesToSerialize = newResponseMessages;
|
|
284
|
+
} else if (hasToolMessage) {
|
|
285
|
+
messagesToSerialize = step.response.messages.slice(-2);
|
|
286
|
+
} else if (step.content.length) {
|
|
287
|
+
messagesToSerialize = step.response.messages.slice(-1);
|
|
288
|
+
} else {
|
|
289
|
+
messagesToSerialize = [{ role: "assistant" as const, content: [] }];
|
|
290
|
+
}
|
|
291
|
+
|
|
231
292
|
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> => {
|
|
293
|
+
messagesToSerialize.map(async (msg): Promise<MessageWithMetadata> => {
|
|
238
294
|
const { message, fileIds } = await serializeMessage(ctx, component, msg);
|
|
239
295
|
return parse(vMessageWithMetadata, {
|
|
240
296
|
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,
|