import { Hono } from "hono"; import { streamSSE } from "hono/streaming"; import type { AgentRunService, RunEvent } from "@great-agent/agent-core"; import { startRunRequestSchema } from "@great-agent/web-contracts"; import type { RunRegistry } from "../composition/run-registry"; export function createRunRoutes( service: AgentRunService, registry: RunRegistry, ): Hono { const routes = new Hono(); routes.get("/runs/:runId", async (context) => context.json(await service.getRun(context.req.param("runId"))), ); routes.post("/runs", async (context) => { const input = startRunRequestSchema.parse(await context.req.json()); const controller = new AbortController(); const started = await service.start(input, controller.signal); void consume(started.events, registry); return context.json( { conversationId: started.run.conversationId, runId: started.run.id, status: "running" as const, }, 202, ); }); routes.post("/runs/:runId/cancel", async (context) => { const events = await service.cancel(context.req.param("runId")); for (const event of events) await registry.publish(event); if (events.length === 0) await registry.waitForTerminal(context.req.param("runId")); const run = await service.getRun(context.req.param("runId")); return context.json({ runId: context.req.param("runId"), status: run.status, }); }); routes.post("/runs/:runId/retry", async (context) => { const started = await service.retry( context.req.param("runId"), new AbortController().signal, ); void consume(started.events, registry); return context.json( { conversationId: started.run.conversationId, runId: started.run.id, status: "running" as const, }, 202, ); }); routes.get("/runs/:runId/events", (context) => streamSSE(context, async (stream) => { const runId = context.req.param("runId"); await registry.hydrate(await service.listEvents(runId)); for (const event of registry.events(runId)) await writeEvent(stream, event); if (registry.isFinished(runId) || registry.isPaused(runId)) { // EventSource needs the terminal event to reach the browser before the // server closes a replay-only stream. await stream.sleep(100); return; } await new Promise((resolve) => { const unsubscribe = registry.subscribe(runId, async (event) => { await writeEvent(stream, event); if ( event.type === "run.completed" || event.type === "run.failed" || event.type === "run.cancelled" || event.type === "interaction.requested" ) { unsubscribe(); resolve(); } }); stream.onAbort(() => { unsubscribe(); resolve(); }); }); }), ); return routes; } export async function consume( events: AsyncIterable, registry: RunRegistry, ): Promise { for await (const event of events) await registry.publish(event); } async function writeEvent( stream: Parameters[1]>[0], event: RunEvent, ): Promise { await stream.writeSSE({ event: event.type, id: String(event.sequence), data: JSON.stringify(event), }); }