import { and, eq } from "drizzle-orm"; import { z } from "zod"; import { db } from "@tasks/database"; import { agentRuns, convoyEvents, objects } from "@tasks/database/schema"; import { auth } from "@/lib/auth"; import { resolveWorkspace } from "@/server/lib/resolve-workspace"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; const agUiEnvelopeSchema = z.object({ convoyId: z.string().uuid().optional(), briefId: z.string().uuid().nullable().optional(), roleId: z.string().min(1).max(120).optional(), eventType: z.string().min(1).max(64).optional(), outcome: z.string().min(1).max(20).optional(), harnessAlias: z.string().min(1).max(120).optional(), summary: z.string().max(5000).optional(), tokensTotal: z.number().int().nonnegative().optional(), metadata: z.record(z.unknown()).optional(), // Backlog lifecycle via ag-ui envelope (for remote harness that ran a task externally) backlogItemId: z.string().uuid().optional(), }); function json(status: number, body: unknown): Response { return new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json" } }); } export async function POST(req: Request, { params }: { params: Promise<{ workspace: string }> }) { const { workspace: handle } = await params; const session = await auth(); if (!session?.user?.id) return json(401, { error: "Unauthorized" }); let workspace; try { workspace = await resolveWorkspace({ handle, userId: session.user.id }); } catch (e: unknown) { const msg = e instanceof Error ? e.message : "Workspace not found"; const code = (e as { code?: string })?.code; const status = code === "FORBIDDEN" ? 403 : code === "NOT_FOUND" ? 404 : 400; return json(status, { error: msg }); } let body: z.infer; try { body = agUiEnvelopeSchema.parse(await req.json()); } catch (e) { if (e instanceof z.ZodError) return json(400, { error: e.issues[0]?.message ?? "Invalid body" }); return json(400, { error: "Invalid JSON" }); } // Tenant fence: if convoyId provided, ensure it belongs to this workspace if (body.convoyId) { const [convoy] = await db.select({ id: objects.id, workspaceId: objects.workspaceId }).from(objects).where(and(eq(objects.id, body.convoyId), eq(objects.workspaceId, workspace.id))).limit(1); if (!convoy) return json(403, { error: "convoyId not in workspace" }); } if (body.briefId) { const [brief] = await db.select({ id: objects.id, workspaceId: objects.workspaceId }).from(objects).where(and(eq(objects.id, body.briefId), eq(objects.workspaceId, workspace.id))).limit(1); if (!brief) return json(403, { error: "briefId not in workspace" }); } if (body.backlogItemId) { // validate backlog item belongs to workspace via agent_runs path? We check objects? backlog stored in markdown_backlog_items — skip heavy check, just allow if workspace matches via conduct? // For now allow but ensure if we can find agent run path. } // Write convoy event if we have convoy context if (body.convoyId && body.roleId) { await db.insert(convoyEvents).values({ workspaceId: workspace.id, convoyId: body.convoyId, briefId: body.briefId ?? null, role: body.roleId, actor: body.roleId, eventType: body.eventType ?? "ag_ui", outcome: body.outcome ?? null, harnessAlias: body.harnessAlias ?? null, metadata: { source: "ag-ui", summary: body.summary ?? null, tokensTotal: body.tokensTotal ?? null, ...(body.metadata ?? {}), }, }); } // Optionally touch agent run if backlogItemId provided — create or update latest open run if (body.backlogItemId && body.convoyId) { // best-effort: insert completed run for observability; if harness already claimed, this will be additive try { await db.insert(agentRuns).values({ workspaceId: workspace.id, backlogItemId: body.backlogItemId, harnessAlias: body.harnessAlias ?? null, outcome: body.outcome ?? null, tokensTotal: body.tokensTotal ?? null, notes: body.summary?.slice(0, 2000) ?? null, metadata: { source: "ag-ui", ...(body.metadata ?? {}) }, }); } catch { // ignore duplicate } } return json(200, { ok: true }); }