Ship Paseo-equivalent provider snapshot, AgentComposerBar, ACP dispatch rewrite with streaming/persist, permission prompts, and agent commands. Follow-up: pane-scoped chat resolution, CoderMessageList tool timeline, WS user-delta replace, and inference orphan tool_call stripping. Archive openspec v2-2; update CHANGELOG and CURRENT. Co-authored-by: Cursor <cursoragent@cursor.com>
137 lines
4.1 KiB
TypeScript
137 lines
4.1 KiB
TypeScript
/**
|
|
* v2.0.5: Arena routes — competitive dispatch of the same task to multiple agents.
|
|
*
|
|
* POST /api/arena — create an arena with 2-5 contestants
|
|
* GET /api/arena/:id — get all tasks in an arena
|
|
* POST /api/arena/:id/select/:task_id — mark a task as the arena winner
|
|
*/
|
|
import type { FastifyInstance } from 'fastify';
|
|
import { z } from 'zod';
|
|
import type { Sql } from '../db.js';
|
|
|
|
const ContestantSchema = z.object({
|
|
agent: z.string().max(100).optional(),
|
|
model: z.string().max(200).optional(),
|
|
mode_id: z.string().max(200).optional(),
|
|
thinking_option_id: z.string().max(200).optional(),
|
|
});
|
|
|
|
const CreateArenaBody = z.object({
|
|
project_id: z.string().uuid(),
|
|
input: z.string().min(1).max(64_000),
|
|
contestants: z.array(ContestantSchema).min(2).max(5),
|
|
});
|
|
|
|
interface TaskRow {
|
|
id: string;
|
|
agent: string | null;
|
|
model: string | null;
|
|
mode_id: string | null;
|
|
thinking_option_id: string | null;
|
|
state: string;
|
|
}
|
|
|
|
export function registerArenaRoutes(app: FastifyInstance, sql: Sql): void {
|
|
// POST /api/arena — create a new arena
|
|
app.post('/api/arena', async (req, reply) => {
|
|
const parsed = CreateArenaBody.safeParse(req.body);
|
|
if (!parsed.success) {
|
|
reply.code(400);
|
|
return { error: 'invalid body', details: parsed.error.flatten() };
|
|
}
|
|
|
|
const { project_id, input, contestants } = parsed.data;
|
|
const arenaId = crypto.randomUUID();
|
|
|
|
const tasks: TaskRow[] = [];
|
|
for (const contestant of contestants) {
|
|
const [task] = await sql<TaskRow[]>`
|
|
INSERT INTO tasks (project_id, input, agent, model, mode_id, thinking_option_id, arena_id)
|
|
VALUES (
|
|
${project_id},
|
|
${input},
|
|
${contestant.agent ?? null},
|
|
${contestant.model ?? null},
|
|
${contestant.mode_id ?? null},
|
|
${contestant.thinking_option_id ?? null},
|
|
${arenaId}
|
|
)
|
|
RETURNING id, agent, model, mode_id, thinking_option_id, state
|
|
`;
|
|
tasks.push(task!);
|
|
}
|
|
|
|
reply.code(201);
|
|
return {
|
|
arena_id: arenaId,
|
|
tasks: tasks.map((t) => ({
|
|
id: t.id,
|
|
agent: t.agent,
|
|
model: t.model,
|
|
mode_id: t.mode_id,
|
|
thinking_option_id: t.thinking_option_id,
|
|
state: t.state,
|
|
})),
|
|
};
|
|
});
|
|
|
|
// GET /api/arena/:arena_id — list all tasks in an arena
|
|
app.get<{ Params: { arena_id: string } }>('/api/arena/:arena_id', async (req, reply) => {
|
|
const { arena_id } = req.params;
|
|
|
|
// Validate UUID format
|
|
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
if (!uuidRegex.test(arena_id)) {
|
|
reply.code(400);
|
|
return { error: 'invalid arena_id format' };
|
|
}
|
|
|
|
const tasks = await sql`
|
|
SELECT id, project_id, state, input, output_summary, agent, model, mode_id, thinking_option_id, execution_path, session_id, started_at, ended_at, created_at, arena_id
|
|
FROM tasks
|
|
WHERE arena_id = ${arena_id}
|
|
ORDER BY created_at
|
|
`;
|
|
|
|
if (tasks.length === 0) {
|
|
reply.code(404);
|
|
return { error: 'arena not found' };
|
|
}
|
|
|
|
return { arena_id, tasks };
|
|
});
|
|
|
|
// POST /api/arena/:arena_id/select/:task_id — mark the winner
|
|
app.post<{ Params: { arena_id: string; task_id: string } }>(
|
|
'/api/arena/:arena_id/select/:task_id',
|
|
async (req, reply) => {
|
|
const { arena_id, task_id } = req.params;
|
|
|
|
// Verify the task belongs to this arena
|
|
const rows = await sql<{ id: string; state: string; arena_id: string | null }[]>`
|
|
SELECT id, state, arena_id FROM tasks WHERE id = ${task_id}
|
|
`;
|
|
|
|
if (rows.length === 0) {
|
|
reply.code(404);
|
|
return { error: 'task not found' };
|
|
}
|
|
|
|
const task = rows[0]!;
|
|
if (task.arena_id !== arena_id) {
|
|
reply.code(409);
|
|
return { error: 'task does not belong to this arena' };
|
|
}
|
|
|
|
// Mark as selected via output_summary prefix (lightweight — no schema change)
|
|
await sql`
|
|
UPDATE tasks
|
|
SET output_summary = COALESCE('[SELECTED] ' || output_summary, '[SELECTED]')
|
|
WHERE id = ${task_id}
|
|
`;
|
|
|
|
return { selected: true, task_id, arena_id };
|
|
}
|
|
);
|
|
}
|