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.
|
* 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 { z } from 'zod';
|
||||||
import type { TRPCContext } from './context.js';
|
import type { TRPCContext } from './context.js';
|
||||||
|
import type { AgentInfo, AgentResult } from '../agent/types.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize tRPC with our context type.
|
* Initialize tRPC with our context type.
|
||||||
@@ -79,6 +80,102 @@ export const statusResponseSchema = z.object({
|
|||||||
|
|
||||||
export type StatusResponse = z.infer<typeof statusResponseSchema>;
|
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
|
// Application Router with Procedures
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -89,6 +186,13 @@ export type StatusResponse = z.infer<typeof statusResponseSchema>;
|
|||||||
* Procedures:
|
* Procedures:
|
||||||
* - health: Quick health check with uptime and process count
|
* - health: Quick health check with uptime and process count
|
||||||
* - status: Full server status with process list
|
* - 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({
|
export const appRouter = router({
|
||||||
/**
|
/**
|
||||||
@@ -134,6 +238,91 @@ export const appRouter = router({
|
|||||||
processes: [],
|
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