convex-durable-agents 0.2.0 → 0.2.2
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/client/handler.d.ts +1 -1
- package/dist/client/handler.d.ts.map +1 -1
- package/dist/client/handler.js +137 -134
- package/dist/client/handler.js.map +1 -1
- package/dist/component/_generated/component.d.ts +2 -2
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/agent.d.ts.map +1 -1
- package/dist/component/agent.js +22 -3
- package/dist/component/agent.js.map +1 -1
- package/dist/component/messages.d.ts +7 -0
- package/dist/component/messages.d.ts.map +1 -1
- package/dist/component/messages.js +7 -1
- package/dist/component/messages.js.map +1 -1
- package/dist/component/threads.d.ts.map +1 -1
- package/dist/component/threads.js +6 -0
- package/dist/component/threads.js.map +1 -1
- package/dist/component/tool_calls.d.ts +2 -2
- package/dist/component/tool_calls.d.ts.map +1 -1
- package/dist/component/tool_calls.js +48 -13
- package/dist/component/tool_calls.js.map +1 -1
- package/dist/react/agent-chat.d.ts.map +1 -1
- package/dist/react/agent-chat.js +20 -1
- package/dist/react/agent-chat.js.map +1 -1
- package/package.json +1 -1
- package/src/client/handler.ts +148 -148
- package/src/component/_generated/component.ts +2 -2
- package/src/component/agent.ts +24 -3
- package/src/component/messages.ts +17 -3
- package/src/component/threads.ts +6 -0
- package/src/component/tool_calls.ts +50 -13
- package/src/react/agent-chat.ts +22 -1
- package/dist/shared/stream_seq.d.ts +0 -2
- package/dist/shared/stream_seq.d.ts.map +0 -1
- package/dist/shared/stream_seq.js +0 -4
- package/dist/shared/stream_seq.js.map +0 -1
package/src/client/handler.ts
CHANGED
|
@@ -178,7 +178,7 @@ export function streamHandlerAction(
|
|
|
178
178
|
});
|
|
179
179
|
if (stream == null) {
|
|
180
180
|
logger.debug("Stream lock acquisition failed, exiting handler");
|
|
181
|
-
return;
|
|
181
|
+
return null;
|
|
182
182
|
}
|
|
183
183
|
logger.debug(`Stream lock acquired (seq=${stream.seq})`);
|
|
184
184
|
streamer.startHeartbeat();
|
|
@@ -212,178 +212,178 @@ export function streamHandlerAction(
|
|
|
212
212
|
});
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
messages: modelMessages,
|
|
226
|
-
tools: handlerlessTools,
|
|
227
|
-
});
|
|
215
|
+
const uiMessages = messages.map((m) => messageDocToUIMessage(m));
|
|
216
|
+
logger.debug(`Converted ${uiMessages.length} UI messages, transforming to model messages...`);
|
|
217
|
+
const modelMessages = transformMessages(await convertToModelMessages(uiMessages, { tools: handlerlessTools }));
|
|
218
|
+
logger.debug(`Model messages ready (${modelMessages.length} messages), starting streamText...`);
|
|
219
|
+
const result = streamText({
|
|
220
|
+
...streamTextArgs,
|
|
221
|
+
prompt: undefined,
|
|
222
|
+
messages: modelMessages,
|
|
223
|
+
tools: handlerlessTools,
|
|
224
|
+
});
|
|
228
225
|
|
|
229
|
-
|
|
230
|
-
|
|
226
|
+
let finishReason: string | undefined;
|
|
227
|
+
let responseMessage: UIMessage | undefined;
|
|
231
228
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
229
|
+
const uiMessageStream = result.toUIMessageStream({
|
|
230
|
+
generateMessageId: generateId,
|
|
231
|
+
originalMessages: uiMessages,
|
|
232
|
+
onFinish: ({ responseMessage: finalResponseMessage }) => {
|
|
233
|
+
responseMessage = finalResponseMessage;
|
|
234
|
+
},
|
|
235
|
+
});
|
|
239
236
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
237
|
+
let msgId: string | undefined;
|
|
238
|
+
if (messages.length > 0) {
|
|
239
|
+
msgId = messages[messages.length - 1]!.id;
|
|
240
|
+
logger.debug(`Setting initial message ID from last message: ${msgId}`);
|
|
241
|
+
await streamer.setMessageId(msgId, true);
|
|
242
|
+
}
|
|
243
|
+
let toolCallCount = 0;
|
|
244
|
+
logger.debug("Processing UI message stream parts...");
|
|
245
|
+
for await (const part of uiMessageStream) {
|
|
246
|
+
if (part.type === "start") {
|
|
247
|
+
msgId = part.messageId;
|
|
248
|
+
logger.debug(`Stream part: start (messageId=${msgId})`);
|
|
249
|
+
await streamer.setMessageId(
|
|
250
|
+
msgId,
|
|
251
|
+
messages?.some((m) => m.id === msgId),
|
|
252
|
+
);
|
|
245
253
|
}
|
|
246
|
-
|
|
247
|
-
logger.debug("Processing UI message stream parts...");
|
|
248
|
-
for await (const part of uiMessageStream) {
|
|
249
|
-
if (part.type === "start") {
|
|
250
|
-
msgId = part.messageId;
|
|
251
|
-
logger.debug(`Stream part: start (messageId=${msgId})`);
|
|
252
|
-
await streamer.setMessageId(
|
|
253
|
-
msgId,
|
|
254
|
-
messages?.some((m) => m.id === msgId),
|
|
255
|
-
);
|
|
256
|
-
}
|
|
257
|
-
await streamer.process(part);
|
|
254
|
+
await streamer.process(part);
|
|
258
255
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
}
|
|
256
|
+
switch (part.type) {
|
|
257
|
+
case "tool-input-available":
|
|
258
|
+
toolCallCount++;
|
|
259
|
+
logger.debug(
|
|
260
|
+
`Stream part: tool-input-available (tool=${part.toolName}, callId=${part.toolCallId}, count=${toolCallCount})`,
|
|
261
|
+
);
|
|
262
|
+
await scheduleToolCall(
|
|
263
|
+
ctx,
|
|
264
|
+
{
|
|
265
|
+
toolCallId: part.toolCallId,
|
|
266
|
+
toolName: part.toolName,
|
|
267
|
+
args: part.input,
|
|
268
|
+
msgId: msgId,
|
|
269
|
+
threadId: args.threadId,
|
|
270
|
+
saveDelta: !!saveStreamDeltas,
|
|
271
|
+
},
|
|
272
|
+
toolDefinitions,
|
|
273
|
+
logger,
|
|
274
|
+
);
|
|
275
|
+
break;
|
|
276
|
+
case "finish":
|
|
277
|
+
finishReason = part.finishReason;
|
|
278
|
+
logger.debug(`Stream part: finish (reason=${finishReason})`);
|
|
279
|
+
break;
|
|
280
|
+
case "error":
|
|
281
|
+
logger.error("Stream error:", part.errorText);
|
|
282
|
+
throw new Error(`Stream error: ${part.errorText}`);
|
|
283
|
+
default:
|
|
284
|
+
// Ignore other part types
|
|
285
|
+
break;
|
|
290
286
|
}
|
|
291
|
-
|
|
287
|
+
}
|
|
288
|
+
logger.debug(`Stream iteration complete (toolCallCount=${toolCallCount}, finishReason=${finishReason})`);
|
|
292
289
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
290
|
+
// Finish the delta stream
|
|
291
|
+
logger.debug("Finishing delta stream...");
|
|
292
|
+
await streamer.finish();
|
|
296
293
|
|
|
297
|
-
|
|
298
|
-
|
|
294
|
+
if (!responseMessage) {
|
|
295
|
+
throw new Error("No response message");
|
|
296
|
+
}
|
|
297
|
+
logger.debug(`Response message received (role=${responseMessage.role}, parts=${responseMessage.parts.length})`);
|
|
298
|
+
|
|
299
|
+
const providerMetadata = await getStreamTextProviderMetadata(result);
|
|
300
|
+
const usage = await getStreamTextUsage(result, providerMetadata);
|
|
301
|
+
logger.debug(`Usage info: ${usage ? JSON.stringify(usage) : "none"}`);
|
|
302
|
+
if (usage && usageHandlerCallback) {
|
|
303
|
+
logger.debug("Invoking onMessageComplete callback...");
|
|
304
|
+
try {
|
|
305
|
+
await usageHandlerCallback(ctx as ActionCtx, {
|
|
306
|
+
threadId: args.threadId,
|
|
307
|
+
streamId: args.streamId,
|
|
308
|
+
message: responseMessage,
|
|
309
|
+
usage,
|
|
310
|
+
providerMetadata: serializeForConvex(providerMetadata),
|
|
311
|
+
});
|
|
312
|
+
logger.debug("onMessageComplete callback succeeded");
|
|
313
|
+
} catch (e) {
|
|
314
|
+
console.error("endOfTurnCallback callback failed:", e);
|
|
299
315
|
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Save the assistant response if we have one
|
|
319
|
+
if (responseMessage.role === "assistant" && responseMessage.parts.length > 0) {
|
|
300
320
|
logger.debug(
|
|
301
|
-
`
|
|
321
|
+
`Saving assistant response (id=${responseMessage.id}, parts=${responseMessage.parts.length}, seq=${stream.seq})`,
|
|
302
322
|
);
|
|
323
|
+
await ctx.runMutation(component.messages.add, {
|
|
324
|
+
threadId: args.threadId,
|
|
325
|
+
msg: responseMessage,
|
|
326
|
+
overwrite: true,
|
|
327
|
+
committedSeq: stream.seq,
|
|
328
|
+
});
|
|
329
|
+
logger.debug("Applying tool outcomes after saving response...");
|
|
330
|
+
await ctx.runMutation(component.messages.applyToolOutcomes, {
|
|
331
|
+
threadId: args.threadId,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
303
334
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
logger.debug(`
|
|
307
|
-
|
|
308
|
-
|
|
335
|
+
// Handle tool calls
|
|
336
|
+
if (toolCallCount > 0) {
|
|
337
|
+
logger.debug(`Setting thread status to awaiting_tool_results (${toolCallCount} tool calls)`);
|
|
338
|
+
finalStatus = "awaiting_tool_results";
|
|
339
|
+
} else if (finishReason && finishReason !== "tool-calls") {
|
|
340
|
+
logger.debug(`No tool calls, setting thread status to completed (finishReason=${finishReason})`);
|
|
341
|
+
finalStatus = "completed";
|
|
342
|
+
if (turnCompleteHandlerCallback) {
|
|
343
|
+
logger.debug("Invoking onTurnComplete callback...");
|
|
309
344
|
try {
|
|
310
|
-
await
|
|
345
|
+
await turnCompleteHandlerCallback(ctx as ActionCtx, {
|
|
311
346
|
threadId: args.threadId,
|
|
312
347
|
streamId: args.streamId,
|
|
313
|
-
message: responseMessage,
|
|
314
|
-
usage,
|
|
315
348
|
providerMetadata: serializeForConvex(providerMetadata),
|
|
349
|
+
finishReason,
|
|
316
350
|
});
|
|
317
|
-
logger.debug("
|
|
351
|
+
logger.debug("onTurnComplete callback succeeded");
|
|
318
352
|
} catch (e) {
|
|
319
|
-
console.error("
|
|
353
|
+
console.error("turnCompleteHandler callback failed:", e);
|
|
320
354
|
}
|
|
321
355
|
}
|
|
356
|
+
} else {
|
|
357
|
+
logger.debug(`Unhandled end state: toolCallCount=${toolCallCount}, finishReason=${finishReason}`);
|
|
358
|
+
}
|
|
322
359
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
|
|
360
|
+
logger.debug("Stream handler completed successfully");
|
|
361
|
+
return null;
|
|
362
|
+
} catch (error) {
|
|
363
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
364
|
+
const normalizedError = errorMessage || "Unknown error";
|
|
365
|
+
logger.error("Error in stream handler:", normalizedError);
|
|
366
|
+
try {
|
|
367
|
+
await streamer.fail(normalizedError);
|
|
368
|
+
} catch (streamAbortError) {
|
|
369
|
+
logger.error(
|
|
370
|
+
`Failed to abort stream after handler error: ${streamAbortError instanceof Error ? streamAbortError.message : String(streamAbortError)}`,
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
finalStatus = "failed";
|
|
374
|
+
if (errorHandlerCallback) {
|
|
375
|
+
logger.debug("Invoking onError callback...");
|
|
376
|
+
try {
|
|
377
|
+
await errorHandlerCallback(ctx as ActionCtx, {
|
|
336
378
|
threadId: args.threadId,
|
|
379
|
+
streamId: args.streamId,
|
|
380
|
+
error: normalizedError,
|
|
337
381
|
});
|
|
382
|
+
} catch (e) {
|
|
383
|
+
console.error("errorHandler callback failed:", e);
|
|
338
384
|
}
|
|
339
|
-
|
|
340
|
-
// Handle tool calls
|
|
341
|
-
if (toolCallCount > 0) {
|
|
342
|
-
logger.debug(`Setting thread status to awaiting_tool_results (${toolCallCount} tool calls)`);
|
|
343
|
-
finalStatus = "awaiting_tool_results";
|
|
344
|
-
} else if (finishReason && finishReason !== "tool-calls") {
|
|
345
|
-
logger.debug(`No tool calls, setting thread status to completed (finishReason=${finishReason})`);
|
|
346
|
-
finalStatus = "completed";
|
|
347
|
-
if (turnCompleteHandlerCallback) {
|
|
348
|
-
logger.debug("Invoking onTurnComplete callback...");
|
|
349
|
-
try {
|
|
350
|
-
await turnCompleteHandlerCallback(ctx as ActionCtx, {
|
|
351
|
-
threadId: args.threadId,
|
|
352
|
-
streamId: args.streamId,
|
|
353
|
-
providerMetadata: serializeForConvex(providerMetadata),
|
|
354
|
-
finishReason,
|
|
355
|
-
});
|
|
356
|
-
logger.debug("onTurnComplete callback succeeded");
|
|
357
|
-
} catch (e) {
|
|
358
|
-
console.error("turnCompleteHandler callback failed:", e);
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
} else {
|
|
362
|
-
logger.debug(`Unhandled end state: toolCallCount=${toolCallCount}, finishReason=${finishReason}`);
|
|
363
|
-
}
|
|
364
|
-
} catch (error) {
|
|
365
|
-
logger.error("Error in stream handler:", error instanceof Error ? error.message : String(error));
|
|
366
|
-
if (streamer) {
|
|
367
|
-
await streamer.fail(error instanceof Error ? error.message : "Unknown error");
|
|
368
|
-
}
|
|
369
|
-
finalStatus = "failed";
|
|
370
|
-
if (errorHandlerCallback) {
|
|
371
|
-
logger.debug("Invoking onError callback...");
|
|
372
|
-
try {
|
|
373
|
-
await errorHandlerCallback(ctx as ActionCtx, {
|
|
374
|
-
threadId: args.threadId,
|
|
375
|
-
streamId: args.streamId,
|
|
376
|
-
error: error instanceof Error ? error.message : "Unknown error",
|
|
377
|
-
});
|
|
378
|
-
} catch (e) {
|
|
379
|
-
console.error("errorHandler callback failed:", e);
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
throw error;
|
|
383
385
|
}
|
|
384
|
-
|
|
385
|
-
logger.debug("Stream handler completed successfully");
|
|
386
|
-
return null;
|
|
386
|
+
throw error;
|
|
387
387
|
} finally {
|
|
388
388
|
logger.debug("Finalizing stream turn and checking for continuation...");
|
|
389
389
|
const finalizeArgs: {
|
|
@@ -424,14 +424,14 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
424
424
|
"mutation",
|
|
425
425
|
"internal",
|
|
426
426
|
{ error: string; id: string },
|
|
427
|
-
|
|
427
|
+
boolean,
|
|
428
428
|
Name
|
|
429
429
|
>;
|
|
430
430
|
setResult: FunctionReference<
|
|
431
431
|
"mutation",
|
|
432
432
|
"internal",
|
|
433
433
|
{ id: string; result: any },
|
|
434
|
-
|
|
434
|
+
boolean,
|
|
435
435
|
Name
|
|
436
436
|
>;
|
|
437
437
|
setToolCallTimeout: FunctionReference<
|
package/src/component/agent.ts
CHANGED
|
@@ -60,11 +60,30 @@ export const continueStream = mutation({
|
|
|
60
60
|
|
|
61
61
|
// Check stop signal
|
|
62
62
|
if (thread.stopSignal) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
const previousStatus = thread.status;
|
|
64
|
+
const activeStreamId = thread.activeStream ?? null;
|
|
65
|
+
logger.debug(
|
|
66
|
+
`Stop signal detected, transitioning thread to stopped and clearing active stream=${activeStreamId ?? "none"}`,
|
|
67
|
+
);
|
|
68
|
+
await ctx.db.patch(thread._id, {
|
|
66
69
|
status: "stopped",
|
|
70
|
+
activeStream: null,
|
|
71
|
+
continue: false,
|
|
67
72
|
});
|
|
73
|
+
if (thread.onStatusChangeHandle && previousStatus !== "stopped") {
|
|
74
|
+
await ctx.runMutation(thread.onStatusChangeHandle as FunctionHandle<"mutation">, {
|
|
75
|
+
threadId: args.threadId,
|
|
76
|
+
status: "stopped",
|
|
77
|
+
previousStatus,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
if (activeStreamId) {
|
|
81
|
+
const activeStream = await ctx.db.get(activeStreamId);
|
|
82
|
+
if (activeStream) {
|
|
83
|
+
logger.debug(`Cancelling active stream=${activeStreamId} due to stop signal`);
|
|
84
|
+
await cancelStream(ctx, activeStream, "stopSignal");
|
|
85
|
+
}
|
|
86
|
+
}
|
|
68
87
|
return null;
|
|
69
88
|
}
|
|
70
89
|
|
|
@@ -141,6 +160,8 @@ export const tryContinueAllThreads = action({
|
|
|
141
160
|
args: {},
|
|
142
161
|
returns: v.null(),
|
|
143
162
|
handler: async (ctx) => {
|
|
163
|
+
// Manual/admin recovery entrypoint: intentionally unscheduled by default.
|
|
164
|
+
// Invoke this action after outages/deploy interruptions to re-drive incomplete threads.
|
|
144
165
|
const threads = await ctx.runQuery(api.threads.listIncomplete);
|
|
145
166
|
for (const threadId of threads) {
|
|
146
167
|
await ctx.runMutation(api.agent.continueStream, {
|
|
@@ -19,6 +19,20 @@ const vUIMessage = vUIMessageBase.extend({
|
|
|
19
19
|
id: v.string(),
|
|
20
20
|
});
|
|
21
21
|
|
|
22
|
+
type ToolInputAvailablePart = {
|
|
23
|
+
toolCallId: string;
|
|
24
|
+
state: "input-available";
|
|
25
|
+
callProviderMetadata?: unknown;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function isObjectLike(value: unknown): value is Record<string, unknown> {
|
|
29
|
+
return typeof value === "object" && value !== null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isToolInputAvailablePart(part: unknown): part is ToolInputAvailablePart {
|
|
33
|
+
return isObjectLike(part) && typeof part.toolCallId === "string" && part.state === "input-available";
|
|
34
|
+
}
|
|
35
|
+
|
|
22
36
|
// Message doc validator for return types
|
|
23
37
|
export const vMessageDoc = vUIMessage.extend({
|
|
24
38
|
_id: v.string(),
|
|
@@ -102,8 +116,8 @@ export const applyToolOutcomes = mutation({
|
|
|
102
116
|
.order("asc")) {
|
|
103
117
|
let modified = false;
|
|
104
118
|
const parts: UIMessagePart<any, any>[] = [];
|
|
105
|
-
for (const part of message.parts as
|
|
106
|
-
if (
|
|
119
|
+
for (const part of message.parts as unknown[]) {
|
|
120
|
+
if (isToolInputAvailablePart(part)) {
|
|
107
121
|
const toolCall = await ctx.db
|
|
108
122
|
.query("tool_calls")
|
|
109
123
|
.withIndex("by_thread_tool_call_id", (q) =>
|
|
@@ -119,7 +133,7 @@ export const applyToolOutcomes = mutation({
|
|
|
119
133
|
}
|
|
120
134
|
}
|
|
121
135
|
}
|
|
122
|
-
parts.push(part);
|
|
136
|
+
parts.push(part as UIMessagePart<any, any>);
|
|
123
137
|
}
|
|
124
138
|
if (modified) {
|
|
125
139
|
await ctx.db.patch(message._id, { parts });
|
package/src/component/threads.ts
CHANGED
|
@@ -397,6 +397,12 @@ export const remove = mutation({
|
|
|
397
397
|
.withIndex("by_thread", (q) => q.eq("threadId", args.threadId))
|
|
398
398
|
.collect();
|
|
399
399
|
for (const toolCall of toolCalls) {
|
|
400
|
+
if (toolCall.timeoutFnId) {
|
|
401
|
+
const timeoutFn = await ctx.db.system.get(toolCall.timeoutFnId);
|
|
402
|
+
if (timeoutFn?.state.kind === "pending") {
|
|
403
|
+
await ctx.scheduler.cancel(toolCall.timeoutFnId);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
400
406
|
await ctx.db.delete(toolCall._id);
|
|
401
407
|
}
|
|
402
408
|
// Delete the thread
|
|
@@ -176,16 +176,20 @@ export const setResult = mutation({
|
|
|
176
176
|
id: v.id("tool_calls"),
|
|
177
177
|
result: v.any(),
|
|
178
178
|
},
|
|
179
|
-
returns: v.
|
|
179
|
+
returns: v.boolean(),
|
|
180
180
|
handler: async (ctx, args) => {
|
|
181
181
|
const toolCall = await ctx.db.get(args.id);
|
|
182
182
|
if (!toolCall) {
|
|
183
183
|
throw new Error(`Tool call ${args.id} not found`);
|
|
184
184
|
}
|
|
185
|
+
if (toolCall.status !== "pending") {
|
|
186
|
+
logger.warn(`setResult: skipping overwrite for callId=${toolCall.toolCallId}, currentStatus=${toolCall.status}`);
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
185
189
|
logger.debug(`setResult: callId=${toolCall.toolCallId}, tool=${toolCall.toolName}`);
|
|
186
190
|
await cleanupTimeoutFn(ctx, toolCall);
|
|
187
191
|
await ctx.db.patch(args.id, { result: args.result, status: "completed" });
|
|
188
|
-
return
|
|
192
|
+
return true;
|
|
189
193
|
},
|
|
190
194
|
});
|
|
191
195
|
|
|
@@ -194,16 +198,20 @@ export const setError = mutation({
|
|
|
194
198
|
id: v.id("tool_calls"),
|
|
195
199
|
error: v.string(),
|
|
196
200
|
},
|
|
197
|
-
returns: v.
|
|
201
|
+
returns: v.boolean(),
|
|
198
202
|
handler: async (ctx, args) => {
|
|
199
203
|
const toolCall = await ctx.db.get(args.id);
|
|
200
204
|
if (!toolCall) {
|
|
201
205
|
throw new Error(`Tool call ${args.id} not found`);
|
|
202
206
|
}
|
|
207
|
+
if (toolCall.status !== "pending") {
|
|
208
|
+
logger.warn(`setError: skipping overwrite for callId=${toolCall.toolCallId}, currentStatus=${toolCall.status}`);
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
203
211
|
logger.debug(`setError: callId=${toolCall.toolCallId}, tool=${toolCall.toolName}, error=${args.error}`);
|
|
204
212
|
await cleanupTimeoutFn(ctx, toolCall);
|
|
205
213
|
await ctx.db.patch(args.id, { error: args.error, status: "failed", callbackLastError: args.error });
|
|
206
|
-
return
|
|
214
|
+
return true;
|
|
207
215
|
},
|
|
208
216
|
});
|
|
209
217
|
|
|
@@ -605,11 +613,32 @@ export const onToolComplete = internalMutation({
|
|
|
605
613
|
|
|
606
614
|
// Check stop signal
|
|
607
615
|
if (thread.stopSignal) {
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
616
|
+
const previousStatus = thread.status;
|
|
617
|
+
const activeStreamId = thread.activeStream ?? null;
|
|
618
|
+
logger.debug(
|
|
619
|
+
`onToolComplete: stop signal detected, transitioning thread to stopped and clearing active stream=${activeStreamId ?? "none"}`,
|
|
620
|
+
);
|
|
621
|
+
await ctx.db.patch(args.threadId, {
|
|
611
622
|
status: "stopped",
|
|
623
|
+
activeStream: null,
|
|
624
|
+
continue: false,
|
|
612
625
|
});
|
|
626
|
+
if (thread.onStatusChangeHandle && previousStatus !== "stopped") {
|
|
627
|
+
await ctx.runMutation(thread.onStatusChangeHandle as FunctionHandle<"mutation">, {
|
|
628
|
+
threadId: args.threadId,
|
|
629
|
+
status: "stopped",
|
|
630
|
+
previousStatus,
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
if (activeStreamId) {
|
|
634
|
+
const activeStream = await ctx.db.get(activeStreamId);
|
|
635
|
+
if (activeStream && (activeStream.state.kind === "pending" || activeStream.state.kind === "streaming")) {
|
|
636
|
+
await ctx.runMutation(api.streams.abort, {
|
|
637
|
+
streamId: activeStreamId,
|
|
638
|
+
reason: "stopSignal",
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
}
|
|
613
642
|
return null;
|
|
614
643
|
}
|
|
615
644
|
|
|
@@ -686,18 +715,22 @@ export const addToolResult = mutation({
|
|
|
686
715
|
}
|
|
687
716
|
|
|
688
717
|
const threadId = toolCall.threadId;
|
|
689
|
-
// Check if already completed
|
|
690
718
|
if (toolCall.status !== "pending") {
|
|
691
|
-
|
|
719
|
+
logger.warn(`addToolResult: ignoring duplicate completion for callId=${args.toolCallId}`);
|
|
720
|
+
return null;
|
|
692
721
|
}
|
|
693
722
|
|
|
694
723
|
logger.debug(`addToolResult: callId=${args.toolCallId}, tool=${toolCall.toolName}, thread=${threadId}`);
|
|
695
724
|
|
|
696
725
|
// Update the tool call record with the result
|
|
697
|
-
await ctx.runMutation(api.tool_calls.setResult, {
|
|
726
|
+
const transitioned = await ctx.runMutation(api.tool_calls.setResult, {
|
|
698
727
|
id: toolCall._id,
|
|
699
728
|
result: args.result,
|
|
700
729
|
});
|
|
730
|
+
if (!transitioned) {
|
|
731
|
+
logger.warn(`addToolResult: skipped duplicate completion race for callId=${args.toolCallId}`);
|
|
732
|
+
return null;
|
|
733
|
+
}
|
|
701
734
|
|
|
702
735
|
if (toolCall.saveDelta) {
|
|
703
736
|
logger.debug(`addToolResult: inserting tool outcome delta for callId=${args.toolCallId}`);
|
|
@@ -745,9 +778,9 @@ export const addToolError = mutation({
|
|
|
745
778
|
|
|
746
779
|
const threadId = toolCall.threadId;
|
|
747
780
|
|
|
748
|
-
// Check if already completed
|
|
749
781
|
if (toolCall.status !== "pending") {
|
|
750
|
-
|
|
782
|
+
logger.warn(`addToolError: ignoring duplicate completion for callId=${args.toolCallId}`);
|
|
783
|
+
return null;
|
|
751
784
|
}
|
|
752
785
|
|
|
753
786
|
logger.debug(
|
|
@@ -755,10 +788,14 @@ export const addToolError = mutation({
|
|
|
755
788
|
);
|
|
756
789
|
|
|
757
790
|
// Update the tool call record with the error
|
|
758
|
-
await ctx.runMutation(api.tool_calls.setError, {
|
|
791
|
+
const transitioned = await ctx.runMutation(api.tool_calls.setError, {
|
|
759
792
|
id: toolCall._id,
|
|
760
793
|
error: args.error,
|
|
761
794
|
});
|
|
795
|
+
if (!transitioned) {
|
|
796
|
+
logger.warn(`addToolError: skipped duplicate completion race for callId=${args.toolCallId}`);
|
|
797
|
+
return null;
|
|
798
|
+
}
|
|
762
799
|
|
|
763
800
|
if (toolCall.saveDelta) {
|
|
764
801
|
logger.debug(`addToolError: inserting tool outcome delta for callId=${args.toolCallId}`);
|
package/src/react/agent-chat.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useMutation } from "convex/react";
|
|
2
2
|
import type { FunctionReference } from "convex/server";
|
|
3
3
|
import { useCallback } from "react";
|
|
4
|
+
import type { MessageDoc } from "./types";
|
|
4
5
|
import {
|
|
5
6
|
type MessagesQuery,
|
|
6
7
|
type StreamingMessageUpdatesQuery,
|
|
@@ -15,6 +16,20 @@ type StopThreadMutation = FunctionReference<"mutation", "public", { threadId: st
|
|
|
15
16
|
|
|
16
17
|
type ResumeThreadMutation = FunctionReference<"mutation", "public", { threadId: string; prompt?: string }, null>;
|
|
17
18
|
|
|
19
|
+
function createOptimisticMessageDoc({ prompt, threadId }: { threadId: string; prompt: string }): MessageDoc {
|
|
20
|
+
const now = Date.now();
|
|
21
|
+
const suffix = globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
22
|
+
const id = `optimistic-${suffix}`;
|
|
23
|
+
return {
|
|
24
|
+
_id: id,
|
|
25
|
+
_creationTime: now,
|
|
26
|
+
threadId,
|
|
27
|
+
id,
|
|
28
|
+
role: "user",
|
|
29
|
+
parts: [{ type: "text", text: prompt }],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
18
33
|
export type UseAgentChatOptions = {
|
|
19
34
|
/** Query to get thread status */
|
|
20
35
|
getThread: ThreadQuery;
|
|
@@ -96,7 +111,13 @@ export function useAgentChat(options: UseAgentChatOptions): UseAgentChatReturn {
|
|
|
96
111
|
});
|
|
97
112
|
|
|
98
113
|
// Create mutation functions
|
|
99
|
-
const sendMessageMutation = useMutation(sendMessageRef)
|
|
114
|
+
const sendMessageMutation = useMutation(sendMessageRef).withOptimisticUpdate((localStore, args) => {
|
|
115
|
+
const currentMessages = localStore.getQuery(listMessages, { threadId: args.threadId }) ?? [];
|
|
116
|
+
localStore.setQuery(listMessages, { threadId: args.threadId }, [
|
|
117
|
+
...currentMessages,
|
|
118
|
+
createOptimisticMessageDoc(args),
|
|
119
|
+
]);
|
|
120
|
+
});
|
|
100
121
|
const stopThreadMutation = useMutation(stopThreadRef);
|
|
101
122
|
const resumeThreadMutation = useMutation(resumeThreadRef);
|
|
102
123
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"stream_seq.d.ts","sourceRoot":"","sources":["../../src/shared/stream_seq.ts"],"names":[],"mappings":"AAAA,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAE1E"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"stream_seq.js","sourceRoot":"","sources":["../../src/shared/stream_seq.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,iBAAiB,CAAC,YAAgC;IAChE,OAAO,YAAY,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,YAAY,GAAG,CAAC,CAAC;AAC3D,CAAC"}
|