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:
@@ -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);
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user