feat: project durable agent conversations
This commit is contained in:
@@ -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, {
|
||||
|
||||
Reference in New Issue
Block a user