/** * Agents API Routes * CRUD operations and status management for Claude Code agents */ import { db } from '../../db/client' import { agents, tasks } from '../../db/schema' import { eq, and } from 'drizzle-orm' import { randomUUID } from 'crypto' import { authenticateRequest } from '../middleware/auth' /** * Handle all agent routes */ export async function handleAgentRoutes(req: Request, url: URL): Promise { const method = req.method const pathParts = url.pathname.split('/').filter(Boolean) // Authenticate user for protected endpoints const publicEndpoints = ['heartbeat'] const isPublic = pathParts.length === 4 && publicEndpoints.includes(pathParts[3]) let auth = null if (!isPublic) { auth = await authenticateRequest(req) if (!auth) { return Response.json( { success: false, message: 'Unauthorized' }, { status: 401 } ) } } const userId = auth?.userId // GET /api/agents/my - List my agents (authenticated) if (method === 'GET' && pathParts.length === 3 && pathParts[2] === 'my') { return await listMyAgents(userId!) } // POST /api/agents/launch - Launch new agent (authenticated) if (method === 'POST' && pathParts.length === 3 && pathParts[2] === 'launch') { return await launchAgent(userId!, req) } // GET /api/agents - List all agents (admin only - for now authenticated user sees all) if (method === 'GET' && pathParts.length === 2) { return await listAgents(url) } // GET /api/agents/:id - Get single agent if (method === 'GET' && pathParts.length === 3) { const agentId = pathParts[2] return await getAgent(agentId, userId!) } // POST /api/agents - Register agent (called by agent pod on startup) if (method === 'POST' && pathParts.length === 2) { return await registerAgent(req) } // PATCH /api/agents/:id - Update agent if (method === 'PATCH' && pathParts.length === 3) { const agentId = pathParts[2] return await updateAgent(agentId, req, userId!) } // POST /api/agents/:id/heartbeat - Update heartbeat (public) if (method === 'POST' && pathParts.length === 4 && pathParts[3] === 'heartbeat') { const agentId = pathParts[2] return await updateHeartbeat(agentId) } // DELETE /api/agents/:id - Unregister agent if (method === 'DELETE' && pathParts.length === 3) { const agentId = pathParts[2] return await unregisterAgent(agentId, userId!) } return new Response('Not Found', { status: 404 }) } /** * List agents with optional status filter */ async function listAgents(url: URL): Promise { try { const status = url.searchParams.get('status') let query = db.select().from(agents) if (status) { query = query.where(eq(agents.status, status as any)) as any } const allAgents = await query return Response.json({ success: true, data: allAgents, count: allAgents.length, }) } catch (error) { console.error('Error listing agents:', error) return Response.json({ success: false, error: 'Failed to list agents', }, { status: 500 }) } } /** * Get single agent (verify ownership) */ async function getAgent(agentId: string, userId: string): Promise { try { const agent = await db .select() .from(agents) .where( and( eq(agents.id, agentId), eq(agents.userId, userId) ) ) .limit(1) if (agent.length === 0) { return Response.json({ success: false, error: 'Agent not found', }, { status: 404 }) } return Response.json({ success: true, data: agent[0], }) } catch (error) { console.error('Error getting agent:', error) return Response.json({ success: false, error: 'Failed to get agent', }, { status: 500 }) } } /** * Register new agent (called when pod starts) * userId is extracted from env var passed to pod */ async function registerAgent(req: Request): Promise { try { const body = await req.json() // Validate required fields if (!body.podName || !body.userId) { return Response.json({ success: false, error: 'podName and userId are required', }, { status: 400 }) } // Check if agent with this podName already exists const existing = await db .select() .from(agents) .where(eq(agents.podName, body.podName)) .limit(1) if (existing.length > 0) { // Agent already exists, return existing return Response.json({ success: true, data: existing[0], message: 'Agent already registered', }) } const newAgent = { id: randomUUID(), userId: body.userId, podName: body.podName, k8sNamespace: body.k8sNamespace || 'agents', status: 'idle' as const, currentTaskId: null, tasksCompleted: 0, lastHeartbeat: new Date(), } await db.insert(agents).values(newAgent) return Response.json({ success: true, data: newAgent, }, { status: 201 }) } catch (error) { console.error('Error registering agent:', error) return Response.json({ success: false, error: 'Failed to register agent', }, { status: 500 }) } } /** * Update agent status and current task (verify ownership) */ async function updateAgent(agentId: string, req: Request, userId: string): Promise { try { const body = await req.json() // Check if agent exists and belongs to user const existing = await db .select() .from(agents) .where( and( eq(agents.id, agentId), eq(agents.userId, userId) ) ) .limit(1) if (existing.length === 0) { return Response.json({ success: false, error: 'Agent not found or access denied', }, { status: 404 }) } // Build update object const updateData: any = {} if (body.status !== undefined) updateData.status = body.status if (body.currentTaskId !== undefined) updateData.currentTaskId = body.currentTaskId if (body.tasksCompleted !== undefined) updateData.tasksCompleted = body.tasksCompleted await db .update(agents) .set(updateData) .where(eq(agents.id, agentId)) // Get updated agent const updated = await db .select() .from(agents) .where(eq(agents.id, agentId)) .limit(1) return Response.json({ success: true, data: updated[0], }) } catch (error) { console.error('Error updating agent:', error) return Response.json({ success: false, error: 'Failed to update agent', }, { status: 500 }) } } /** * Update agent heartbeat (keep-alive) */ async function updateHeartbeat(agentId: string): Promise { try { // Check if agent exists const existing = await db .select() .from(agents) .where(eq(agents.id, agentId)) .limit(1) if (existing.length === 0) { return Response.json({ success: false, error: 'Agent not found', }, { status: 404 }) } // Update heartbeat timestamp await db .update(agents) .set({ lastHeartbeat: new Date() }) .where(eq(agents.id, agentId)) return Response.json({ success: true, message: 'Heartbeat updated', }) } catch (error) { console.error('Error updating heartbeat:', error) return Response.json({ success: false, error: 'Failed to update heartbeat', }, { status: 500 }) } } /** * Unregister agent (called when pod terminates, verify ownership) */ async function unregisterAgent(agentId: string, userId: string): Promise { try { // Check if agent exists and belongs to user const existing = await db .select() .from(agents) .where( and( eq(agents.id, agentId), eq(agents.userId, userId) ) ) .limit(1) if (existing.length === 0) { return Response.json({ success: false, error: 'Agent not found or access denied', }, { status: 404 }) } // If agent has a current task, set it to null if (existing[0].currentTaskId) { await db .update(tasks) .set({ assignedAgentId: null }) .where(eq(tasks.id, existing[0].currentTaskId)) } // Delete agent from DB await db.delete(agents).where(eq(agents.id, agentId)) // TODO: Delete K8s pod if it exists return Response.json({ success: true, message: 'Agent unregistered', }) } catch (error) { console.error('Error unregistering agent:', error) return Response.json({ success: false, error: 'Failed to unregister agent', }, { status: 500 }) } } /** * List my agents (filtered by userId) */ async function listMyAgents(userId: string): Promise { try { const myAgents = await db .select() .from(agents) .where(eq(agents.userId, userId)) return Response.json({ success: true, data: myAgents, count: myAgents.length, }) } catch (error) { console.error('Error listing my agents:', error) return Response.json({ success: false, error: 'Failed to list agents', }, { status: 500 }) } } /** * Launch new agent (create pod dynamically) * TODO: Integrate with K8s API to create pod */ async function launchAgent(userId: string, req: Request): Promise { try { const agentId = randomUUID() const podName = `claude-agent-${userId.slice(0, 8)}-${Date.now()}` // Create agent record in DB const newAgent = { id: agentId, userId, podName, k8sNamespace: 'agents', status: 'idle' as const, currentTaskId: null, tasksCompleted: 0, lastHeartbeat: new Date(), } await db.insert(agents).values(newAgent) // TODO: Create K8s pod using K8s API // For now, just return the agent record // In production, this would call kubectl or use @kubernetes/client-node return Response.json({ success: true, data: newAgent, message: 'Agent launch initiated. Pod will be created shortly.', }, { status: 201 }) } catch (error) { console.error('Error launching agent:', error) return Response.json({ success: false, error: 'Failed to launch agent', }, { status: 500 }) } }