/** * Agent Sessions API * * POST /api/projects/[projectId]/agent/sessions * Create a new agent session and kick it off via vibn-agent-runner. * Body: { appName, appPath, task } * * GET /api/projects/[projectId]/agent/sessions * List all sessions for a project, newest first. */ import { NextResponse } from "next/server"; import { getServerSession } from "next-auth"; import { authOptions } from "@/lib/auth/authOptions"; import { query } from "@/lib/db-postgres"; const AGENT_RUNNER_URL = process.env.AGENT_RUNNER_URL ?? "http://localhost:3333"; // Verify the agent_sessions table is reachable. If it doesn't exist yet, // throw a descriptive error instead of a generic "Failed to create session". // Run POST /api/admin/migrate once to create the table. async function ensureTable() { await query( `SELECT 1 FROM agent_sessions LIMIT 0`, [] ); } // ── POST — create session ──────────────────────────────────────────────────── export async function POST( req: Request, { params }: { params: Promise<{ projectId: string }> } ) { try { const { projectId } = await params; const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } const body = await req.json(); const { appName, appPath, task } = body as { appName: string; appPath: string; task: string; }; if (!appName || !appPath || !task?.trim()) { return NextResponse.json({ error: "appName, appPath and task are required" }, { status: 400 }); } await ensureTable(); // Verify ownership and fetch giteaRepo const owns = await query<{ id: string; data: Record }>( `SELECT p.id, p.data FROM fs_projects p JOIN fs_users u ON u.id = p.user_id WHERE p.id::text = $1 AND u.data->>'email' = $2 LIMIT 1`, [projectId, session.user.email] ); if (owns.length === 0) { return NextResponse.json({ error: "Project not found" }, { status: 404 }); } const giteaRepo = owns[0].data?.giteaRepo as string | undefined; // Create the session row const rows = await query<{ id: string }>( `INSERT INTO agent_sessions (project_id, app_name, app_path, task, status, started_at) VALUES ($1::uuid, $2, $3, $4, 'running', now()) RETURNING id`, [projectId, appName, appPath, task.trim()] ); const sessionId = rows[0].id; // Fire-and-forget: call agent-runner to start the execution loop // The agent runner is responsible for updating the session row as it works. fetch(`${AGENT_RUNNER_URL}/agent/execute`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sessionId, projectId, appName, appPath, giteaRepo, // e.g. "mark/sportsy" — agent runner uses this to clone/update the repo task: task.trim(), }), }).catch(err => { // Agent runner may not be wired yet — log but don't fail console.warn("[agent] runner not reachable:", err.message); // Mark session as failed if runner unreachable query( `UPDATE agent_sessions SET status = 'failed', error = 'Agent runner not reachable', completed_at = now(), output = jsonb_build_array(jsonb_build_object( 'ts', now()::text, 'type', 'error', 'text', 'Agent runner service is not connected yet. Phase 2 implementation pending.' )) WHERE id = $1::uuid`, [sessionId] ).catch(() => {}); }); return NextResponse.json({ sessionId }, { status: 201 }); } catch (err) { console.error("[agent/sessions POST]", err); return NextResponse.json( { error: "Failed to create session", details: err instanceof Error ? err.message : String(err) }, { status: 500 } ); } } // ── GET — list sessions ────────────────────────────────────────────────────── export async function GET( req: Request, { params }: { params: Promise<{ projectId: string }> } ) { try { const { projectId } = await params; const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } await ensureTable(); const sessions = await query<{ id: string; app_name: string; task: string; status: string; created_at: string; started_at: string | null; completed_at: string | null; output: Array<{ ts: string; type: string; text: string }>; changed_files: Array<{ path: string; status: string }>; error: string | null; }>( `SELECT s.id, s.app_name, s.task, s.status, s.created_at, s.started_at, s.completed_at, s.output, s.changed_files, s.error FROM agent_sessions s JOIN fs_projects p ON p.id::text = s.project_id::text JOIN fs_users u ON u.id = p.user_id WHERE s.project_id::text = $1 AND u.data->>'email' = $2 ORDER BY s.created_at DESC LIMIT 50`, [projectId, session.user.email] ); return NextResponse.json({ sessions }); } catch (err) { console.error("[agent/sessions GET]", err); return NextResponse.json( { error: "Failed to list sessions", details: err instanceof Error ? err.message : String(err) }, { status: 500 } ); } }