mirror of
https://github.com/BradGroux/veritas-kanban.git
synced 2026-08-28 02:44:59 +00:00
207 lines
6.1 KiB
TypeScript
207 lines
6.1 KiB
TypeScript
import { Router, type Router as RouterType } from 'express';
|
|
import { z } from 'zod';
|
|
import { AgentReadinessError, clawdbotAgentService } from '../services/clawdbot-agent-service.js';
|
|
import { getTelemetryService } from '../services/telemetry-service.js';
|
|
import { getTaskService } from '../services/task-service.js';
|
|
import type { AgentType, TokenTelemetryEvent } from '@veritas-kanban/shared';
|
|
import { asyncHandler } from '../middleware/async-handler.js';
|
|
import { NotFoundError, ValidationError } from '../middleware/error-handler.js';
|
|
|
|
const router: RouterType = Router();
|
|
|
|
// Validation schemas
|
|
const AgentTypeSchema = z.string().min(1).max(50);
|
|
|
|
const startAgentSchema = z.object({
|
|
agent: AgentTypeSchema.optional(),
|
|
overrideReason: z.string().trim().min(8).max(1000).optional(),
|
|
});
|
|
|
|
const completeAgentSchema = z.object({
|
|
success: z.boolean(),
|
|
summary: z.string().optional(),
|
|
error: z.string().optional(),
|
|
});
|
|
|
|
const reportTokensSchema = z.object({
|
|
attemptId: z.string().optional(),
|
|
inputTokens: z.number({ message: 'inputTokens is required' }).int().nonnegative(),
|
|
outputTokens: z.number({ message: 'outputTokens is required' }).int().nonnegative(),
|
|
totalTokens: z.number().int().nonnegative().optional(),
|
|
model: z.string().optional(),
|
|
agent: AgentTypeSchema.optional(),
|
|
});
|
|
|
|
// POST /api/agents/:taskId/start - Start agent on task (delegates to Clawdbot)
|
|
router.post(
|
|
'/:taskId/start',
|
|
asyncHandler(async (req, res) => {
|
|
let agent: AgentType | undefined;
|
|
let overrideReason: string | undefined;
|
|
try {
|
|
({ agent, overrideReason } = startAgentSchema.parse(req.body) as {
|
|
agent?: AgentType;
|
|
overrideReason?: string;
|
|
});
|
|
} catch (error) {
|
|
if (error instanceof z.ZodError) {
|
|
throw new ValidationError('Validation failed', error.issues);
|
|
}
|
|
throw error;
|
|
}
|
|
let status;
|
|
try {
|
|
status = await clawdbotAgentService.startAgent(req.params.taskId as string, agent, {
|
|
overrideReason,
|
|
});
|
|
} catch (error) {
|
|
if (error instanceof AgentReadinessError) {
|
|
throw new ValidationError(error.message, {
|
|
readiness: error.readiness,
|
|
});
|
|
}
|
|
throw error;
|
|
}
|
|
res.status(201).json(status);
|
|
})
|
|
);
|
|
|
|
// POST /api/agents/:taskId/complete - Callback from Clawdbot when agent finishes
|
|
router.post(
|
|
'/:taskId/complete',
|
|
asyncHandler(async (req, res) => {
|
|
let success: boolean;
|
|
let summary: string | undefined;
|
|
let error: string | undefined;
|
|
try {
|
|
({ success, summary, error } = completeAgentSchema.parse(req.body));
|
|
} catch (err) {
|
|
if (err instanceof z.ZodError) {
|
|
throw new ValidationError('Validation failed', err.issues);
|
|
}
|
|
throw err;
|
|
}
|
|
|
|
await clawdbotAgentService.completeAgent(req.params.taskId as string, {
|
|
success,
|
|
summary,
|
|
error,
|
|
});
|
|
res.json({ received: true });
|
|
})
|
|
);
|
|
|
|
// POST /api/agents/:taskId/stop - Stop running agent
|
|
router.post(
|
|
'/:taskId/stop',
|
|
asyncHandler(async (req, res) => {
|
|
await clawdbotAgentService.stopAgent(req.params.taskId as string);
|
|
res.json({ stopped: true });
|
|
})
|
|
);
|
|
|
|
// GET /api/agents/:taskId/status - Get agent status
|
|
router.get(
|
|
'/:taskId/status',
|
|
asyncHandler(async (req, res) => {
|
|
const status = clawdbotAgentService.getAgentStatus(req.params.taskId as string);
|
|
if (!status) {
|
|
return res.json({ running: false });
|
|
}
|
|
res.json({ running: true, ...status });
|
|
})
|
|
);
|
|
|
|
// GET /api/agents/pending - List pending agent requests (for Veritas to poll)
|
|
router.get(
|
|
'/pending',
|
|
asyncHandler(async (_req, res) => {
|
|
const requests = await clawdbotAgentService.listPendingRequests();
|
|
res.json(requests);
|
|
})
|
|
);
|
|
|
|
// GET /api/agents/:taskId/attempts - List attempts for task
|
|
router.get(
|
|
'/:taskId/attempts',
|
|
asyncHandler(async (req, res) => {
|
|
const attempts = await clawdbotAgentService.listAttempts(req.params.taskId as string);
|
|
res.json(attempts);
|
|
})
|
|
);
|
|
|
|
// GET /api/agents/:taskId/attempts/:attemptId/log - Get attempt log
|
|
router.get(
|
|
'/:taskId/attempts/:attemptId/log',
|
|
asyncHandler(async (req, res) => {
|
|
const log = await clawdbotAgentService.getAttemptLog(
|
|
req.params.taskId as string,
|
|
req.params.attemptId as string
|
|
);
|
|
res.type('text/markdown').send(log);
|
|
})
|
|
);
|
|
|
|
// POST /api/agents/:taskId/tokens - Report token usage for a run
|
|
router.post(
|
|
'/:taskId/tokens',
|
|
asyncHandler(async (req, res) => {
|
|
let attemptId: string | undefined;
|
|
let inputTokens: number;
|
|
let outputTokens: number;
|
|
let totalTokens: number | undefined;
|
|
let model: string | undefined;
|
|
let agent: AgentType | undefined;
|
|
try {
|
|
const parsed = reportTokensSchema.parse(req.body);
|
|
attemptId = parsed.attemptId;
|
|
inputTokens = parsed.inputTokens;
|
|
outputTokens = parsed.outputTokens;
|
|
totalTokens = parsed.totalTokens;
|
|
model = parsed.model;
|
|
agent = parsed.agent as AgentType | undefined;
|
|
} catch (error) {
|
|
if (error instanceof z.ZodError) {
|
|
throw new ValidationError('Validation failed', error.issues);
|
|
}
|
|
throw error;
|
|
}
|
|
|
|
const taskId = req.params.taskId as string;
|
|
|
|
// Get task to find project and current attempt
|
|
const taskService = getTaskService();
|
|
const task = await taskService.getTask(taskId);
|
|
|
|
if (!task) {
|
|
throw new NotFoundError('Task not found');
|
|
}
|
|
|
|
// Use provided attemptId or current attempt
|
|
const resolvedAttemptId = attemptId || task.attempt?.id || 'unknown';
|
|
const resolvedAgent = agent || task.attempt?.agent || 'claude-code';
|
|
|
|
// Emit telemetry event
|
|
const telemetry = getTelemetryService();
|
|
const event = await telemetry.emit<TokenTelemetryEvent>({
|
|
type: 'run.tokens',
|
|
taskId,
|
|
attemptId: resolvedAttemptId,
|
|
agent: resolvedAgent,
|
|
project: task.project,
|
|
inputTokens,
|
|
outputTokens,
|
|
totalTokens: totalTokens ?? inputTokens + outputTokens,
|
|
model,
|
|
});
|
|
|
|
res.status(201).json({
|
|
recorded: true,
|
|
eventId: event.id,
|
|
totalTokens: event.totalTokens,
|
|
});
|
|
})
|
|
);
|
|
|
|
// Export service for WebSocket use
|
|
export { router as agentRoutes, clawdbotAgentService as agentService };
|