diff --git a/src/trpc/router.ts b/src/trpc/router.ts index da55246..c45762b 100644 --- a/src/trpc/router.ts +++ b/src/trpc/router.ts @@ -5,9 +5,10 @@ * Uses Zod for runtime validation of procedure inputs/outputs. */ -import { initTRPC } from '@trpc/server'; +import { initTRPC, TRPCError } from '@trpc/server'; import { z } from 'zod'; import type { TRPCContext } from './context.js'; +import type { AgentInfo, AgentResult } from '../agent/types.js'; /** * Initialize tRPC with our context type. @@ -79,6 +80,102 @@ export const statusResponseSchema = z.object({ export type StatusResponse = z.infer; +// ============================================================================= +// Agent Input Schemas +// ============================================================================= + +/** + * Schema for spawning a new agent. + */ +export const spawnAgentInputSchema = z.object({ + /** Human-readable name for the agent (required, must be unique) */ + name: z.string().min(1), + /** Task ID to assign to agent */ + taskId: z.string().min(1), + /** Initial prompt/instruction for the agent */ + prompt: z.string().min(1), + /** Optional working directory (defaults to worktree path) */ + cwd: z.string().optional(), +}); + +export type SpawnAgentInput = z.infer; + +/** + * Schema for identifying an agent by name or ID. + */ +export const agentIdentifierSchema = z.object({ + /** Lookup by human-readable name (preferred) */ + name: z.string().optional(), + /** Or lookup by ID */ + id: z.string().optional(), +}).refine(data => data.name || data.id, { + message: 'Either name or id must be provided', +}); + +export type AgentIdentifier = z.infer; + +/** + * Schema for resuming an agent with a prompt. + */ +export const resumeAgentInputSchema = z.object({ + /** Lookup by human-readable name (preferred) */ + name: z.string().optional(), + /** Or lookup by ID */ + id: z.string().optional(), + /** User response to continue the agent */ + prompt: z.string().min(1), +}).refine(data => data.name || data.id, { + message: 'Either name or id must be provided', +}); + +export type ResumeAgentInput = z.infer; + +// ============================================================================= +// Helper Functions +// ============================================================================= + +/** + * Helper to resolve agent by name or id. + * Throws TRPCError if agent not found. + */ +async function resolveAgent( + ctx: TRPCContext, + input: { name?: string; id?: string } +): Promise { + if (!ctx.agentManager) { + throw new TRPCError({ + code: 'INTERNAL_SERVER_ERROR', + message: 'Agent manager not available', + }); + } + + const agent = input.name + ? await ctx.agentManager.getByName(input.name) + : await ctx.agentManager.get(input.id!); + + if (!agent) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: `Agent '${input.name ?? input.id}' not found`, + }); + } + + return agent; +} + +/** + * Helper to ensure agentManager is available in context. + */ +function requireAgentManager(ctx: TRPCContext) { + if (!ctx.agentManager) { + throw new TRPCError({ + code: 'INTERNAL_SERVER_ERROR', + message: 'Agent manager not available', + }); + } + return ctx.agentManager; +} + // ============================================================================= // Application Router with Procedures // ============================================================================= @@ -89,6 +186,13 @@ export type StatusResponse = z.infer; * Procedures: * - health: Quick health check with uptime and process count * - status: Full server status with process list + * - spawnAgent: Spawn a new agent to work on a task + * - stopAgent: Stop a running agent + * - listAgents: List all agents with status + * - getAgent: Get agent details by name or ID + * - getAgentByName: Get agent by name + * - resumeAgent: Resume an agent waiting for input + * - getAgentResult: Get result of agent's work */ export const appRouter = router({ /** @@ -134,6 +238,91 @@ export const appRouter = router({ processes: [], }; }), + + // =========================================================================== + // Agent Procedures + // =========================================================================== + + /** + * Spawn a new agent to work on a task. + * Creates isolated worktree, starts Claude CLI, persists state. + */ + spawnAgent: publicProcedure + .input(spawnAgentInputSchema) + .mutation(async ({ ctx, input }) => { + const agentManager = requireAgentManager(ctx); + return agentManager.spawn({ + name: input.name, + taskId: input.taskId, + prompt: input.prompt, + cwd: input.cwd, + }); + }), + + /** + * Stop a running agent by name or ID. + */ + stopAgent: publicProcedure + .input(agentIdentifierSchema) + .mutation(async ({ ctx, input }) => { + const agentManager = requireAgentManager(ctx); + const agent = await resolveAgent(ctx, input); + await agentManager.stop(agent.id); + return { success: true, name: agent.name }; + }), + + /** + * List all agents with their current status. + */ + listAgents: publicProcedure + .query(async ({ ctx }) => { + const agentManager = requireAgentManager(ctx); + return agentManager.list(); + }), + + /** + * Get agent details by name or ID. + */ + getAgent: publicProcedure + .input(agentIdentifierSchema) + .query(async ({ ctx, input }) => { + return resolveAgent(ctx, input); + }), + + /** + * Get agent by name (convenience method). + */ + getAgentByName: publicProcedure + .input(z.object({ name: z.string().min(1) })) + .query(async ({ ctx, input }) => { + const agentManager = requireAgentManager(ctx); + return agentManager.getByName(input.name); + }), + + /** + * Resume an agent that is waiting for input. + * Uses stored session ID to continue with full context. + */ + resumeAgent: publicProcedure + .input(resumeAgentInputSchema) + .mutation(async ({ ctx, input }) => { + const agentManager = requireAgentManager(ctx); + const agent = await resolveAgent(ctx, input); + await agentManager.resume(agent.id, input.prompt); + return { success: true, name: agent.name }; + }), + + /** + * Get the result of an agent's work. + * Only available after agent completes or stops. + */ + getAgentResult: publicProcedure + .input(agentIdentifierSchema) + .query(async ({ ctx, input }): Promise => { + const agentManager = requireAgentManager(ctx); + const agent = await resolveAgent(ctx, input); + return agentManager.getResult(agent.id); + }), }); /**