feat(04-04): add agent procedures to tRPC router

- Add spawnAgentInputSchema for spawn input validation
- Add agentIdentifierSchema for name/id lookup
- Add resumeAgentInputSchema for resume operations
- Add resolveAgent helper for name/id resolution
- Add requireAgentManager helper for context validation
- Add 7 agent procedures: spawn, stop, list, get, getByName, resume, getResult
This commit is contained in:
Lukas May
2026-01-30 20:11:59 +01:00
parent acf3b8dae3
commit 16f85cd22f

View File

@@ -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<typeof statusResponseSchema>;
// =============================================================================
// 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<typeof spawnAgentInputSchema>;
/**
* 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<typeof agentIdentifierSchema>;
/**
* 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<typeof resumeAgentInputSchema>;
// =============================================================================
// 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<AgentInfo> {
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<typeof statusResponseSchema>;
* 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<AgentResult | null> => {
const agentManager = requireAgentManager(ctx);
const agent = await resolveAgent(ctx, input);
return agentManager.getResult(agent.id);
}),
});
/**