@convex-dev/agent 0.6.0-alpha.1 → 0.6.0-beta.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/dist/UIMessages.d.ts.map +1 -1
- package/dist/UIMessages.js +88 -0
- package/dist/UIMessages.js.map +1 -1
- package/dist/client/definePlaygroundAPI.d.ts +17 -17
- package/dist/client/index.d.ts +44 -44
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +54 -20
- package/dist/client/index.js.map +1 -1
- package/dist/client/messages.d.ts +3 -3
- package/dist/client/search.d.ts +3 -3
- package/dist/client/search.d.ts.map +1 -1
- package/dist/client/search.js +14 -4
- package/dist/client/search.js.map +1 -1
- package/dist/client/start.js +2 -2
- package/dist/client/start.js.map +1 -1
- package/dist/client/streamText.d.ts.map +1 -1
- package/dist/client/streamText.js +10 -0
- package/dist/client/streamText.js.map +1 -1
- package/dist/client/streaming.d.ts +47 -47
- package/dist/client/streaming.d.ts.map +1 -1
- package/dist/client/streaming.js +37 -21
- package/dist/client/streaming.js.map +1 -1
- package/dist/component/messages.d.ts +47 -47
- package/dist/component/schema.d.ts +40 -40
- package/dist/component/streams.d.ts +2 -2
- package/dist/component/threads.d.ts +6 -6
- package/dist/component/vector/index.d.ts +1 -1
- package/dist/mapping.d.ts +19 -15
- package/dist/mapping.d.ts.map +1 -1
- package/dist/mapping.js +90 -47
- package/dist/mapping.js.map +1 -1
- package/dist/validators.d.ts +13 -13
- package/package.json +1 -1
- package/src/UIMessages.ts +126 -0
- package/src/client/approval.test.ts +144 -0
- package/src/client/index.ts +73 -23
- package/src/client/search.test.ts +4 -5
- package/src/client/search.ts +16 -4
- package/src/client/start.ts +2 -2
- package/src/client/streamText.ts +9 -0
- package/src/client/streaming.integration.test.ts +1206 -0
- package/src/client/streaming.ts +35 -21
- package/src/mapping.test.ts +136 -71
- package/src/mapping.ts +119 -50
|
@@ -57,6 +57,17 @@ function getApprovalIdFromSavedMessages(
|
|
|
57
57
|
return approvalRequest.approvalId;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
// Second tool that also requires approval
|
|
61
|
+
const renameFileTool = createTool({
|
|
62
|
+
description: "Rename a file",
|
|
63
|
+
inputSchema: z.object({
|
|
64
|
+
oldName: z.string(),
|
|
65
|
+
newName: z.string(),
|
|
66
|
+
}),
|
|
67
|
+
needsApproval: () => true,
|
|
68
|
+
execute: async (_ctx, input) => `Renamed: ${input.oldName} → ${input.newName}`,
|
|
69
|
+
});
|
|
70
|
+
|
|
60
71
|
// --- Agents (separate mock model instances to avoid shared callIndex) ---
|
|
61
72
|
|
|
62
73
|
const approvalAgent = new Agent(components.agent, {
|
|
@@ -257,6 +268,114 @@ export const testApproveFlowWithInterveningMessage = action({
|
|
|
257
268
|
},
|
|
258
269
|
});
|
|
259
270
|
|
|
271
|
+
// Agent that calls two tools in one step, both needing approval
|
|
272
|
+
const multiToolAgent = new Agent(components.agent, {
|
|
273
|
+
name: "multi-tool-test",
|
|
274
|
+
instructions: "You manage files.",
|
|
275
|
+
tools: { deleteFile: deleteFileTool, renameFile: renameFileTool },
|
|
276
|
+
languageModel: mockModel({
|
|
277
|
+
contentSteps: [
|
|
278
|
+
// Step 1: model calls two tools at once
|
|
279
|
+
[
|
|
280
|
+
{
|
|
281
|
+
type: "tool-call",
|
|
282
|
+
toolCallId: "tc-multi-1",
|
|
283
|
+
toolName: "deleteFile",
|
|
284
|
+
input: JSON.stringify({ filename: "old.txt" }),
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
type: "tool-call",
|
|
288
|
+
toolCallId: "tc-multi-2",
|
|
289
|
+
toolName: "renameFile",
|
|
290
|
+
input: JSON.stringify({ oldName: "a.txt", newName: "b.txt" }),
|
|
291
|
+
},
|
|
292
|
+
],
|
|
293
|
+
// Step 2: after both tools execute, model responds
|
|
294
|
+
[{ type: "text", text: "Done! Deleted old.txt and renamed a.txt to b.txt." }],
|
|
295
|
+
],
|
|
296
|
+
}),
|
|
297
|
+
stopWhen: stepCountIs(5),
|
|
298
|
+
usageHandler: testUsageHandler,
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
export const testMultiToolApproveFlow = action({
|
|
302
|
+
args: {},
|
|
303
|
+
handler: async (ctx) => {
|
|
304
|
+
const { thread } = await multiToolAgent.createThread(ctx, {
|
|
305
|
+
userId: "u-multi",
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// Step 1: Generate — model calls two tools, both need approval
|
|
309
|
+
const result1 = await thread.generateText({
|
|
310
|
+
prompt: "Delete old.txt and rename a.txt to b.txt",
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// Extract both approval IDs
|
|
314
|
+
const approvalParts = result1.savedMessages
|
|
315
|
+
?.flatMap((m) =>
|
|
316
|
+
Array.isArray(m.message?.content)
|
|
317
|
+
? (m.message.content as unknown[])
|
|
318
|
+
: [],
|
|
319
|
+
)
|
|
320
|
+
.filter(
|
|
321
|
+
(
|
|
322
|
+
p,
|
|
323
|
+
): p is {
|
|
324
|
+
type: "tool-approval-request";
|
|
325
|
+
approvalId: string;
|
|
326
|
+
toolCallId: string;
|
|
327
|
+
} => (p as { type?: string }).type === "tool-approval-request",
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
if (!approvalParts || approvalParts.length !== 2) {
|
|
331
|
+
throw new Error(
|
|
332
|
+
`Expected 2 approval requests, got ${approvalParts?.length ?? 0}`,
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Approve both tool calls
|
|
337
|
+
const { messageId: _msgId1 } = await ctx.runMutation(
|
|
338
|
+
anyApi["approval.test"].submitApprovalForMultiToolAgent,
|
|
339
|
+
{ threadId: thread.threadId, approvalId: approvalParts[0].approvalId },
|
|
340
|
+
);
|
|
341
|
+
const { messageId: msgId2 } = await ctx.runMutation(
|
|
342
|
+
anyApi["approval.test"].submitApprovalForMultiToolAgent,
|
|
343
|
+
{ threadId: thread.threadId, approvalId: approvalParts[1].approvalId },
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
// Continue generation with the last approval message
|
|
347
|
+
const result2 = await thread.generateText({
|
|
348
|
+
promptMessageId: msgId2,
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
const allMessages = await multiToolAgent.listMessages(ctx, {
|
|
352
|
+
threadId: thread.threadId,
|
|
353
|
+
paginationOpts: { cursor: null, numItems: 40 },
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
return {
|
|
357
|
+
approvalCount: approvalParts.length,
|
|
358
|
+
firstText: result1.text,
|
|
359
|
+
secondText: result2.text,
|
|
360
|
+
threadMessageRoles: allMessages.page.map((m) => m.message?.role),
|
|
361
|
+
// Check that both approvals were merged into one tool message
|
|
362
|
+
toolMessageCount: allMessages.page.filter(
|
|
363
|
+
(m) => m.message?.role === "tool",
|
|
364
|
+
).length,
|
|
365
|
+
};
|
|
366
|
+
},
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
export const submitApprovalForMultiToolAgent = mutation({
|
|
370
|
+
args: {
|
|
371
|
+
threadId: v.string(),
|
|
372
|
+
approvalId: v.string(),
|
|
373
|
+
},
|
|
374
|
+
handler: async (ctx, { threadId, approvalId }) => {
|
|
375
|
+
return multiToolAgent.approveToolCall(ctx, { threadId, approvalId });
|
|
376
|
+
},
|
|
377
|
+
});
|
|
378
|
+
|
|
260
379
|
export const submitApprovalForApprovalAgent = mutation({
|
|
261
380
|
args: { threadId: v.string(), approvalId: v.string(), reason: v.optional(v.string()) },
|
|
262
381
|
handler: async (ctx, { threadId, approvalId, reason }) => {
|
|
@@ -276,7 +395,9 @@ const testApi: ApiFromModules<{
|
|
|
276
395
|
testApproveFlow: typeof testApproveFlow;
|
|
277
396
|
testDenyFlow: typeof testDenyFlow;
|
|
278
397
|
testApproveFlowWithInterveningMessage: typeof testApproveFlowWithInterveningMessage;
|
|
398
|
+
testMultiToolApproveFlow: typeof testMultiToolApproveFlow;
|
|
279
399
|
submitApprovalForApprovalAgent: typeof submitApprovalForApprovalAgent;
|
|
400
|
+
submitApprovalForMultiToolAgent: typeof submitApprovalForMultiToolAgent;
|
|
280
401
|
submitDenialForDenialAgent: typeof submitDenialForDenialAgent;
|
|
281
402
|
};
|
|
282
403
|
}>["fns"] = anyApi["approval.test"] as any;
|
|
@@ -338,6 +459,29 @@ describe("Tool Approval Workflow", () => {
|
|
|
338
459
|
expect(result.lastUsage!.outputTokenDetails).toBeDefined();
|
|
339
460
|
});
|
|
340
461
|
|
|
462
|
+
test("multi-tool: approve two tool calls from the same step", async () => {
|
|
463
|
+
usageCalls.length = 0;
|
|
464
|
+
const t = initConvexTest(schema);
|
|
465
|
+
const result = await t.action(testApi.testMultiToolApproveFlow, {});
|
|
466
|
+
|
|
467
|
+
expect(result.approvalCount).toBe(2);
|
|
468
|
+
expect(result.firstText).toBe("");
|
|
469
|
+
expect(result.secondText).toBe(
|
|
470
|
+
"Done! Deleted old.txt and renamed a.txt to b.txt.",
|
|
471
|
+
);
|
|
472
|
+
// Both approval responses should be merged into one tool message
|
|
473
|
+
// (write-time merge in respondToToolCallApproval via findApprovalContext)
|
|
474
|
+
// Thread: user, assistant(2 tool-calls + 2 approvals),
|
|
475
|
+
// tool(2 approval-responses merged), tool(2 tool-results), assistant(text)
|
|
476
|
+
expect(result.threadMessageRoles).toEqual([
|
|
477
|
+
"assistant", // final text
|
|
478
|
+
"tool", // tool-results
|
|
479
|
+
"tool", // approval-responses (merged)
|
|
480
|
+
"assistant", // tool-calls + approval-requests
|
|
481
|
+
"user", // prompt
|
|
482
|
+
]);
|
|
483
|
+
});
|
|
484
|
+
|
|
341
485
|
test("approve remains valid with an intervening thread message", async () => {
|
|
342
486
|
usageCalls.length = 0;
|
|
343
487
|
const t = initConvexTest(schema);
|
package/src/client/index.ts
CHANGED
|
@@ -1070,10 +1070,35 @@ export class Agent<
|
|
|
1070
1070
|
reason?: string;
|
|
1071
1071
|
},
|
|
1072
1072
|
): Promise<{ messageId: string }> {
|
|
1073
|
-
const promptMessageId
|
|
1074
|
-
|
|
1073
|
+
const { promptMessageId, existingResponseMessage } =
|
|
1074
|
+
await this.findApprovalContext(ctx, {
|
|
1075
|
+
threadId: args.threadId,
|
|
1076
|
+
approvalId: args.approvalId,
|
|
1077
|
+
});
|
|
1078
|
+
|
|
1079
|
+
const newPart = {
|
|
1080
|
+
type: "tool-approval-response" as const,
|
|
1075
1081
|
approvalId: args.approvalId,
|
|
1076
|
-
|
|
1082
|
+
approved: args.approved,
|
|
1083
|
+
reason: args.reason,
|
|
1084
|
+
};
|
|
1085
|
+
|
|
1086
|
+
// Merge into an existing approval-response message for this step
|
|
1087
|
+
// so the AI SDK sees a single tool message per step.
|
|
1088
|
+
if (existingResponseMessage) {
|
|
1089
|
+
const existingContent = existingResponseMessage.message?.content;
|
|
1090
|
+
const mergedContent = Array.isArray(existingContent)
|
|
1091
|
+
? [...(existingContent as any[]), newPart]
|
|
1092
|
+
: [newPart];
|
|
1093
|
+
await this.updateMessage(ctx, {
|
|
1094
|
+
messageId: existingResponseMessage._id,
|
|
1095
|
+
patch: {
|
|
1096
|
+
message: { role: "tool", content: mergedContent },
|
|
1097
|
+
status: "success",
|
|
1098
|
+
},
|
|
1099
|
+
});
|
|
1100
|
+
return { messageId: existingResponseMessage._id };
|
|
1101
|
+
}
|
|
1077
1102
|
|
|
1078
1103
|
const { messageId } = await this.saveMessage(ctx, {
|
|
1079
1104
|
threadId: args.threadId,
|
|
@@ -1081,36 +1106,55 @@ export class Agent<
|
|
|
1081
1106
|
skipEmbeddings: true,
|
|
1082
1107
|
message: {
|
|
1083
1108
|
role: "tool",
|
|
1084
|
-
content: [
|
|
1085
|
-
{
|
|
1086
|
-
type: "tool-approval-response",
|
|
1087
|
-
approvalId: args.approvalId,
|
|
1088
|
-
approved: args.approved,
|
|
1089
|
-
reason: args.reason,
|
|
1090
|
-
},
|
|
1091
|
-
],
|
|
1109
|
+
content: [newPart],
|
|
1092
1110
|
},
|
|
1093
1111
|
});
|
|
1094
1112
|
return { messageId };
|
|
1095
1113
|
}
|
|
1096
1114
|
|
|
1097
|
-
private async
|
|
1115
|
+
private async findApprovalContext(
|
|
1098
1116
|
ctx: MutationCtx,
|
|
1099
1117
|
args: { threadId: string; approvalId: string },
|
|
1100
|
-
): Promise<
|
|
1118
|
+
): Promise<{
|
|
1119
|
+
promptMessageId: string;
|
|
1120
|
+
existingResponseMessage: MessageDoc | undefined;
|
|
1121
|
+
}> {
|
|
1101
1122
|
// NOTE: This pagination returns messages in descending order (newest first).
|
|
1102
1123
|
// The "already handled" check (tool-approval-response) relies on seeing
|
|
1103
1124
|
// responses before their corresponding requests. If the pagination order
|
|
1104
1125
|
// changes, this logic will need to be updated.
|
|
1105
|
-
let
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
}
|
|
1126
|
+
let existingResponseMessage: MessageDoc | undefined;
|
|
1127
|
+
// Limit the search to the most recent messages. Approvals should always
|
|
1128
|
+
// be near the end of the thread.
|
|
1129
|
+
const page = await this.listMessages(ctx, {
|
|
1130
|
+
threadId: args.threadId,
|
|
1131
|
+
paginationOpts: { cursor: null, numItems: 100 },
|
|
1132
|
+
});
|
|
1133
|
+
{
|
|
1111
1134
|
for (const message of page.page) {
|
|
1112
1135
|
const content = message.message?.content;
|
|
1113
1136
|
if (!Array.isArray(content)) continue;
|
|
1137
|
+
// Check if this assistant message starts a different approval step.
|
|
1138
|
+
// If so, any response message we've seen so far belongs to a newer
|
|
1139
|
+
// step — reset it so we don't merge across step boundaries.
|
|
1140
|
+
// Only reset if the target approval is NOT in this message (i.e.,
|
|
1141
|
+
// this is a genuinely different step, not the same step with
|
|
1142
|
+
// multiple tool calls).
|
|
1143
|
+
if (
|
|
1144
|
+
message.message?.role === "assistant" &&
|
|
1145
|
+
content.some(
|
|
1146
|
+
(p: any) =>
|
|
1147
|
+
p.type === "tool-approval-request" &&
|
|
1148
|
+
p.approvalId !== args.approvalId,
|
|
1149
|
+
) &&
|
|
1150
|
+
!content.some(
|
|
1151
|
+
(p: any) =>
|
|
1152
|
+
p.type === "tool-approval-request" &&
|
|
1153
|
+
p.approvalId === args.approvalId,
|
|
1154
|
+
)
|
|
1155
|
+
) {
|
|
1156
|
+
existingResponseMessage = undefined;
|
|
1157
|
+
}
|
|
1114
1158
|
for (const part of content) {
|
|
1115
1159
|
const typedPart = part as { type?: unknown; approvalId?: unknown };
|
|
1116
1160
|
if (
|
|
@@ -1119,19 +1163,25 @@ export class Agent<
|
|
|
1119
1163
|
) {
|
|
1120
1164
|
throw new Error(`Approval ${args.approvalId} was already handled`);
|
|
1121
1165
|
}
|
|
1166
|
+
// Track the most recent tool-approval-response message for merging
|
|
1167
|
+
if (
|
|
1168
|
+
typedPart.type === "tool-approval-response" &&
|
|
1169
|
+
!existingResponseMessage
|
|
1170
|
+
) {
|
|
1171
|
+
existingResponseMessage = message;
|
|
1172
|
+
}
|
|
1122
1173
|
if (
|
|
1123
1174
|
typedPart.type === "tool-approval-request" &&
|
|
1124
1175
|
typedPart.approvalId === args.approvalId
|
|
1125
1176
|
) {
|
|
1126
|
-
return message._id;
|
|
1177
|
+
return { promptMessageId: message._id, existingResponseMessage };
|
|
1127
1178
|
}
|
|
1128
1179
|
}
|
|
1129
1180
|
}
|
|
1130
|
-
|
|
1131
|
-
} while (cursor !== null);
|
|
1181
|
+
}
|
|
1132
1182
|
|
|
1133
1183
|
throw new Error(
|
|
1134
|
-
`Approval request ${args.approvalId} was not found in thread ${args.threadId}`,
|
|
1184
|
+
`Approval request ${args.approvalId} was not found in the last 100 messages of thread ${args.threadId}`,
|
|
1135
1185
|
);
|
|
1136
1186
|
}
|
|
1137
1187
|
|
|
@@ -292,7 +292,7 @@ describe("search.ts", () => {
|
|
|
292
292
|
expect(result[1]._id).toBe("2");
|
|
293
293
|
});
|
|
294
294
|
|
|
295
|
-
it("should
|
|
295
|
+
it("should keep tool calls with approval request but NO approval response (auto-deny handles them)", () => {
|
|
296
296
|
const messages: MessageDoc[] = [
|
|
297
297
|
{
|
|
298
298
|
_id: "1",
|
|
@@ -321,19 +321,18 @@ describe("search.ts", () => {
|
|
|
321
321
|
|
|
322
322
|
const result = filterOutOrphanedToolMessages(messages);
|
|
323
323
|
expect(result).toHaveLength(1);
|
|
324
|
-
// The assistant message should
|
|
324
|
+
// The assistant message should keep the tool-call (auto-deny resolves it downstream)
|
|
325
325
|
const assistantContent = result[0].message?.content;
|
|
326
326
|
expect(Array.isArray(assistantContent)).toBe(true);
|
|
327
327
|
if (Array.isArray(assistantContent)) {
|
|
328
|
-
|
|
329
|
-
expect(assistantContent).toHaveLength(2);
|
|
328
|
+
expect(assistantContent).toHaveLength(3);
|
|
330
329
|
expect(assistantContent.find((p) => p.type === "text")).toBeDefined();
|
|
331
330
|
expect(
|
|
332
331
|
assistantContent.find((p) => p.type === "tool-approval-request"),
|
|
333
332
|
).toBeDefined();
|
|
334
333
|
expect(
|
|
335
334
|
assistantContent.find((p) => p.type === "tool-call"),
|
|
336
|
-
).
|
|
335
|
+
).toBeDefined();
|
|
337
336
|
}
|
|
338
337
|
});
|
|
339
338
|
|
package/src/client/search.ts
CHANGED
|
@@ -30,8 +30,8 @@ import type {
|
|
|
30
30
|
} from "./types.js";
|
|
31
31
|
import { inlineMessagesFiles } from "./files.js";
|
|
32
32
|
import {
|
|
33
|
+
autoDenyUnresolvedApprovals,
|
|
33
34
|
docsToModelMessages,
|
|
34
|
-
mergeApprovalResponseMessages,
|
|
35
35
|
toModelMessage,
|
|
36
36
|
} from "../mapping.js";
|
|
37
37
|
|
|
@@ -289,6 +289,12 @@ export function filterOutOrphanedToolMessages(docs: MessageDoc[]) {
|
|
|
289
289
|
return approvalId !== undefined && approvalResponseIds.has(approvalId);
|
|
290
290
|
};
|
|
291
291
|
|
|
292
|
+
// Helper: check if tool call has a pending approval request
|
|
293
|
+
// (auto-deny handles these downstream, so they must survive the filter)
|
|
294
|
+
const hasApprovalRequest = (toolCallId: string) => {
|
|
295
|
+
return approvalRequestsByToolCallId.has(toolCallId);
|
|
296
|
+
};
|
|
297
|
+
|
|
292
298
|
for (const doc of docs) {
|
|
293
299
|
if (
|
|
294
300
|
doc.message?.role === "assistant" &&
|
|
@@ -298,7 +304,8 @@ export function filterOutOrphanedToolMessages(docs: MessageDoc[]) {
|
|
|
298
304
|
(p) =>
|
|
299
305
|
p.type !== "tool-call" ||
|
|
300
306
|
toolResultIds.has(p.toolCallId) ||
|
|
301
|
-
hasApprovalResponse(p.toolCallId)
|
|
307
|
+
hasApprovalResponse(p.toolCallId) ||
|
|
308
|
+
hasApprovalRequest(p.toolCallId),
|
|
302
309
|
);
|
|
303
310
|
if (content.length) {
|
|
304
311
|
result.push({
|
|
@@ -641,13 +648,13 @@ export async function fetchContextWithPrompt(
|
|
|
641
648
|
const inputPrompt = promptArray.map(toModelMessage);
|
|
642
649
|
const existingResponses = docsToModelMessages(existingResponseDocs);
|
|
643
650
|
|
|
644
|
-
const allMessages =
|
|
651
|
+
const allMessages = [
|
|
645
652
|
...search,
|
|
646
653
|
...recent,
|
|
647
654
|
...inputMessages,
|
|
648
655
|
...inputPrompt,
|
|
649
656
|
...existingResponses,
|
|
650
|
-
]
|
|
657
|
+
];
|
|
651
658
|
let processedMessages = args.contextHandler
|
|
652
659
|
? await args.contextHandler(ctx, {
|
|
653
660
|
allMessages,
|
|
@@ -661,6 +668,11 @@ export async function fetchContextWithPrompt(
|
|
|
661
668
|
})
|
|
662
669
|
: allMessages;
|
|
663
670
|
|
|
671
|
+
// Post-process: auto-deny unresolved approvals so the AI SDK sees a
|
|
672
|
+
// complete history. Applied after contextHandler so custom handlers
|
|
673
|
+
// don't need to handle this.
|
|
674
|
+
processedMessages = autoDenyUnresolvedApprovals(processedMessages);
|
|
675
|
+
|
|
664
676
|
// Process messages to inline localhost files (if not, file urls pointing to localhost will be sent to LLM providers)
|
|
665
677
|
if (process.env.CONVEX_CLOUD_URL?.startsWith("http://127.0.0.1")) {
|
|
666
678
|
processedMessages = await inlineMessagesFiles(processedMessages);
|
package/src/client/start.ts
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
type ToolSet,
|
|
11
11
|
} from "ai";
|
|
12
12
|
import {
|
|
13
|
-
|
|
13
|
+
serializeResponseMessages,
|
|
14
14
|
serializeObjectResult,
|
|
15
15
|
} from "../mapping.js";
|
|
16
16
|
import { embedMessages, fetchContextWithPrompt } from "./search.js";
|
|
@@ -252,7 +252,7 @@ export async function startGeneration<
|
|
|
252
252
|
previousResponseMessageCount,
|
|
253
253
|
);
|
|
254
254
|
previousResponseMessageCount = allResponseMessages.length;
|
|
255
|
-
serialized = await
|
|
255
|
+
serialized = await serializeResponseMessages(
|
|
256
256
|
ctx,
|
|
257
257
|
component,
|
|
258
258
|
toSave.step,
|
package/src/client/streamText.ts
CHANGED
|
@@ -168,6 +168,15 @@ export async function streamText<
|
|
|
168
168
|
// finish() was never called, leaving the streaming message stuck in
|
|
169
169
|
// "streaming" state. Clean it up by marking it as aborted.
|
|
170
170
|
await streamer?.fail(e instanceof Error ? e.message : String(e));
|
|
171
|
+
// Save the deferred final step if it was already generated but not yet persisted
|
|
172
|
+
if (pendingFinalStep) {
|
|
173
|
+
try {
|
|
174
|
+
await call.save({ step: pendingFinalStep }, false);
|
|
175
|
+
} catch (saveError) {
|
|
176
|
+
console.error("Failed to save deferred final step:", saveError);
|
|
177
|
+
}
|
|
178
|
+
pendingFinalStep = undefined;
|
|
179
|
+
}
|
|
171
180
|
throw e;
|
|
172
181
|
}
|
|
173
182
|
}
|