@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
package/src/client/streaming.ts
CHANGED
|
@@ -244,16 +244,25 @@ export class DeltaStreamer<T> {
|
|
|
244
244
|
this.abortController = new AbortController();
|
|
245
245
|
if (config.abortSignal) {
|
|
246
246
|
config.abortSignal.addEventListener("abort", async () => {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
247
|
+
try {
|
|
248
|
+
if (this.abortController.signal.aborted) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
251
|
this.abortController.abort();
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
252
|
+
// Wait for in-flight stream creation before trying to abort it
|
|
253
|
+
if (this.#creatingStreamIdPromise) {
|
|
254
|
+
await this.#creatingStreamIdPromise;
|
|
255
|
+
}
|
|
256
|
+
if (this.streamId) {
|
|
257
|
+
await this.#ongoingWrite;
|
|
258
|
+
await this.ctx.runMutation(this.component.streams.abort, {
|
|
259
|
+
streamId: this.streamId,
|
|
260
|
+
reason: "abortSignal",
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
} catch {
|
|
264
|
+
// Best-effort cleanup — the stream will be garbage-collected
|
|
265
|
+
// by the 10-minute timeout if this fails.
|
|
257
266
|
}
|
|
258
267
|
});
|
|
259
268
|
}
|
|
@@ -293,7 +302,10 @@ export class DeltaStreamer<T> {
|
|
|
293
302
|
await this.addParts([chunk]);
|
|
294
303
|
}
|
|
295
304
|
// Skip finish if it will be handled externally (atomically with message save)
|
|
296
|
-
if (
|
|
305
|
+
// or if the stream was aborted (e.g., due to a failed delta write).
|
|
306
|
+
// Aborted streams are cleaned up via streams.abort (called by the abort
|
|
307
|
+
// signal handler), so we don't need to call finish() for them.
|
|
308
|
+
if (!this.#finishedExternally && !this.abortController.signal.aborted) {
|
|
297
309
|
await this.finish();
|
|
298
310
|
}
|
|
299
311
|
}
|
|
@@ -339,7 +351,7 @@ export class DeltaStreamer<T> {
|
|
|
339
351
|
e instanceof Error ? e.message : "unknown error",
|
|
340
352
|
);
|
|
341
353
|
this.abortController.abort();
|
|
342
|
-
|
|
354
|
+
return;
|
|
343
355
|
}
|
|
344
356
|
// Now that we've sent the delta, check if we need to send another one.
|
|
345
357
|
if (
|
|
@@ -375,7 +387,10 @@ export class DeltaStreamer<T> {
|
|
|
375
387
|
return;
|
|
376
388
|
}
|
|
377
389
|
await this.#ongoingWrite;
|
|
378
|
-
await this.#sendDelta();
|
|
390
|
+
await this.#sendDelta(); // #sendDelta checks aborted internally
|
|
391
|
+
if (this.abortController.signal.aborted) {
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
379
394
|
await this.ctx.runMutation(this.component.streams.finish, {
|
|
380
395
|
streamId: this.streamId,
|
|
381
396
|
});
|
|
@@ -432,16 +447,15 @@ export function compressTextStreamParts(
|
|
|
432
447
|
} else {
|
|
433
448
|
compressed.push(part);
|
|
434
449
|
}
|
|
450
|
+
} else if (part.type === "file") {
|
|
451
|
+
compressed.push({
|
|
452
|
+
type: "file",
|
|
453
|
+
file: {
|
|
454
|
+
...part.file,
|
|
455
|
+
uint8Array: undefined as unknown as Uint8Array,
|
|
456
|
+
},
|
|
457
|
+
});
|
|
435
458
|
} else {
|
|
436
|
-
if (part.type === "file") {
|
|
437
|
-
compressed.push({
|
|
438
|
-
type: "file",
|
|
439
|
-
file: {
|
|
440
|
-
...part.file,
|
|
441
|
-
uint8Array: undefined as unknown as Uint8Array,
|
|
442
|
-
},
|
|
443
|
-
});
|
|
444
|
-
}
|
|
445
459
|
compressed.push(part);
|
|
446
460
|
}
|
|
447
461
|
}
|
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,7 +7,7 @@ import {
|
|
|
7
7
|
toModelMessage,
|
|
8
8
|
serializeContent,
|
|
9
9
|
toModelMessageContent,
|
|
10
|
-
|
|
10
|
+
autoDenyUnresolvedApprovals,
|
|
11
11
|
} from "./mapping.js";
|
|
12
12
|
import { api } from "./component/_generated/api.js";
|
|
13
13
|
import type { AgentComponent, ActionCtx } from "./client/types.js";
|
|
@@ -259,79 +259,144 @@ describe("mapping", () => {
|
|
|
259
259
|
expect((content as unknown[])[0]).toMatchObject(approvalResponse);
|
|
260
260
|
});
|
|
261
261
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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;
|
|
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;
|
|
287
280
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
});
|
|
281
|
+
const result = autoDenyUnresolvedApprovals(messages);
|
|
282
|
+
expect(result).toBe(messages); // same reference, no changes
|
|
283
|
+
});
|
|
296
284
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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;
|
|
308
297
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
+
});
|
|
317
312
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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);
|
|
333
391
|
|
|
334
|
-
|
|
335
|
-
|
|
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
|
+
});
|
|
336
401
|
});
|
|
337
402
|
});
|
package/src/mapping.ts
CHANGED
|
@@ -140,41 +140,94 @@ export function docsToModelMessages(messages: MessageDoc[]): ModelMessage[] {
|
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
/**
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
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.
|
|
148
150
|
*/
|
|
149
|
-
export function
|
|
151
|
+
export function autoDenyUnresolvedApprovals(
|
|
150
152
|
messages: ModelMessage[],
|
|
151
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
|
|
152
213
|
const result: ModelMessage[] = [];
|
|
153
|
-
for (
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
(cloned.content as any[]).push(...(msg.content as any[]));
|
|
167
|
-
} else {
|
|
168
|
-
result.push(msg);
|
|
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
|
+
});
|
|
169
227
|
}
|
|
170
228
|
}
|
|
171
|
-
return result;
|
|
172
|
-
}
|
|
173
229
|
|
|
174
|
-
|
|
175
|
-
return content.some(
|
|
176
|
-
(p: any) => p.type === "tool-approval-response",
|
|
177
|
-
);
|
|
230
|
+
return result;
|
|
178
231
|
}
|
|
179
232
|
|
|
180
233
|
export function serializeUsage(usage: LanguageModelUsage): Usage {
|
|
@@ -244,21 +297,49 @@ export function toModelMessageWarnings(
|
|
|
244
297
|
}) as any;
|
|
245
298
|
}
|
|
246
299
|
|
|
300
|
+
/**
|
|
301
|
+
* Serialize explicitly provided response messages for a step.
|
|
302
|
+
* Used by the streaming/generation loop where the caller tracks which
|
|
303
|
+
* messages are new via slicing.
|
|
304
|
+
*/
|
|
305
|
+
export async function serializeResponseMessages<TOOLS extends ToolSet>(
|
|
306
|
+
ctx: ActionCtx,
|
|
307
|
+
component: AgentComponent,
|
|
308
|
+
step: StepResult<TOOLS>,
|
|
309
|
+
model: ModelOrMetadata | undefined,
|
|
310
|
+
responseMessages: ModelMessage[],
|
|
311
|
+
): Promise<{ messages: MessageWithMetadata[] }> {
|
|
312
|
+
return serializeStepMessages(ctx, component, step, model, responseMessages);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Serialize the new messages from a step using a heuristic to determine
|
|
317
|
+
* which response messages are new (last 1-2 messages).
|
|
318
|
+
*/
|
|
247
319
|
export async function serializeNewMessagesInStep<TOOLS extends ToolSet>(
|
|
248
320
|
ctx: ActionCtx,
|
|
249
321
|
component: AgentComponent,
|
|
250
322
|
step: StepResult<TOOLS>,
|
|
251
323
|
model: ModelOrMetadata | undefined,
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
324
|
+
): Promise<{ messages: MessageWithMetadata[] }> {
|
|
325
|
+
const hasToolMessage = step.response.messages.at(-1)?.role === "tool";
|
|
326
|
+
let messagesToSerialize: ModelMessage[];
|
|
327
|
+
if (hasToolMessage) {
|
|
328
|
+
messagesToSerialize = step.response.messages.slice(-2);
|
|
329
|
+
} else if (step.content.length) {
|
|
330
|
+
messagesToSerialize = step.response.messages.slice(-1);
|
|
331
|
+
} else {
|
|
332
|
+
messagesToSerialize = [{ role: "assistant" as const, content: [] }];
|
|
333
|
+
}
|
|
334
|
+
return serializeStepMessages(ctx, component, step, model, messagesToSerialize);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function serializeStepMessages<TOOLS extends ToolSet>(
|
|
338
|
+
ctx: ActionCtx,
|
|
339
|
+
component: AgentComponent,
|
|
340
|
+
step: StepResult<TOOLS>,
|
|
341
|
+
model: ModelOrMetadata | undefined,
|
|
342
|
+
messagesToSerialize: ModelMessage[],
|
|
262
343
|
): Promise<{ messages: MessageWithMetadata[] }> {
|
|
263
344
|
// If there are tool results, there's another message with the tool results
|
|
264
345
|
// ref: https://github.com/vercel/ai/blob/main/packages/ai/src/generate-text/to-response-messages.ts#L120
|
|
@@ -277,18 +358,6 @@ export async function serializeNewMessagesInStep<TOOLS extends ToolSet>(
|
|
|
277
358
|
} satisfies Omit<MessageWithMetadata, "message" | "text" | "fileIds">;
|
|
278
359
|
const toolFields = { sources: step.sources };
|
|
279
360
|
|
|
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
|
-
|
|
292
361
|
const messages: MessageWithMetadata[] = await Promise.all(
|
|
293
362
|
messagesToSerialize.map(async (msg): Promise<MessageWithMetadata> => {
|
|
294
363
|
const { message, fileIds } = await serializeMessage(ctx, component, msg);
|