feat: project durable agent conversations

This commit is contained in:
-Puter
2026-07-29 08:19:17 +05:30
parent 18eb150d7d
commit 3ffa1cfc7c
22 changed files with 800 additions and 309 deletions

View File

@@ -38,17 +38,6 @@ const markProcessingRef = makeFunctionReference<
},
boolean
>("conversationMessages:markProcessing");
const completeTurnRef = makeFunctionReference<
"mutation",
{
turnId: Id<"conversationTurns">;
attempt: number;
leaseOwner: string;
submissionId: string;
text: string;
},
boolean
>("conversationMessages:completeTurn");
const failTurnRef = makeFunctionReference<
"mutation",
{
@@ -268,47 +257,7 @@ export const markProcessing = internalMutation({
error: undefined,
leaseExpiresAt: Date.now() + 60_000,
leaseOwner: args.leaseOwner,
status: "processing",
});
return true;
},
});
export const completeTurn = internalMutation({
args: {
attempt: v.number(),
leaseOwner: v.string(),
submissionId: v.string(),
text: v.string(),
turnId: v.id("conversationTurns"),
},
handler: async (ctx, args): Promise<boolean> => {
const turn = await ctx.db.get(args.turnId);
if (
!turn ||
turn.status !== "processing" ||
(turn.attemptNumber ?? 1) !== args.attempt ||
turn.leaseOwner !== args.leaseOwner ||
(turn.leaseExpiresAt !== undefined && turn.leaseExpiresAt < Date.now())
) {
return false;
}
const assistant = await ctx.db
.query("conversationMessages")
.withIndex("by_turnId_and_role", (q) =>
q.eq("turnId", args.turnId).eq("role", "assistant")
)
.unique();
if (assistant) {
await ctx.db.patch(assistant._id, { content: args.text });
}
await ctx.db.patch(args.turnId, {
completedAt: Date.now(),
error: undefined,
leaseExpiresAt: undefined,
leaseOwner: undefined,
status: "completed",
submissionId: args.submissionId,
status: "dispatching",
});
return true;
},
@@ -326,10 +275,9 @@ export const failTurn = internalMutation({
const turn = await ctx.db.get(args.turnId);
if (
!turn ||
turn.status !== "processing" ||
(turn.attemptNumber ?? 1) !== args.attempt ||
turn.status !== "dispatching" ||
turn.leaseOwner !== args.leaseOwner ||
(turn.leaseExpiresAt !== undefined && turn.leaseExpiresAt < Date.now())
(turn.attemptNumber ?? 1) !== args.attempt
) {
return false;
}
@@ -392,7 +340,6 @@ export const runTurn = internalAction({
`agents/zopu/${encodeURIComponent(String(turn.organizationId))}`,
`${flueUrl.replace(/\/+$/u, "")}/`
);
endpoint.searchParams.set("wait", "result");
const response = await fetch(endpoint, {
body: JSON.stringify({ images, message: turn.user.content }),
headers: {
@@ -400,41 +347,29 @@ export const runTurn = internalAction({
"content-type": "application/json",
"x-zopu-organization-id": String(turn.organizationId),
"x-zopu-request-id": turn.turn.clientRequestId,
"x-zopu-turn-id": String(args.turnId),
},
method: "POST",
});
const payload: unknown = await response.json().catch(() => null);
if (
!response.ok ||
typeof payload !== "object" ||
payload === null ||
!("submissionId" in payload) ||
typeof payload.submissionId !== "string" ||
!("result" in payload) ||
typeof payload.result !== "object" ||
payload.result === null ||
!("text" in payload.result) ||
typeof payload.result.text !== "string"
) {
throw new Error(`Flue turn failed (${response.status})`);
if (!response.ok) {
throw new Error(
`Flue admission failed (${response.status}): ${await response.text().catch(() => "")}`
);
}
const admitted = await ctx.runQuery(getTurnRef, { turnId: args.turnId });
if (admitted?.turn.submissionId === undefined) {
throw new Error("Flue admission did not bind the product turn");
}
await ctx.runMutation(completeTurnRef, {
attempt: args.attempt,
leaseOwner,
submissionId: payload.submissionId,
text: payload.result.text,
turnId: args.turnId,
});
} catch (error) {
const retry = args.attempt < MAX_ATTEMPTS;
await ctx.runMutation(failTurnRef, {
const failed = await ctx.runMutation(failTurnRef, {
attempt: args.attempt,
error: error instanceof Error ? error.message : String(error),
leaseOwner,
retry,
turnId: args.turnId,
});
if (retry) {
if (failed && retry) {
await ctx.scheduler.runAfter(args.attempt * 1000, runTurnRef, {
attempt: args.attempt + 1,
turnId: args.turnId,
@@ -451,10 +386,18 @@ export const reconcileExpiredTurns = internalMutation({
const expired = await ctx.db
.query("conversationTurns")
.withIndex("by_status_and_leaseExpiresAt", (q) =>
q.eq("status", "processing").lt("leaseExpiresAt", Date.now())
q.eq("status", "dispatching").lt("leaseExpiresAt", Date.now())
)
.collect();
for (const turn of expired) {
if (turn.submissionId !== undefined) {
await ctx.db.patch(turn._id, {
leaseExpiresAt: undefined,
leaseOwner: undefined,
status: "running",
});
continue;
}
const attempt = turn.attemptNumber ?? 1;
const retry = attempt < MAX_ATTEMPTS;
await ctx.db.patch(turn._id, {