2026-08-14 16:21:09 +08:00

107 lines
3.3 KiB
TypeScript

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<void>((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<RunEvent>,
registry: RunRegistry,
): Promise<void> {
for await (const event of events) await registry.publish(event);
}
async function writeEvent(
stream: Parameters<Parameters<typeof streamSSE>[1]>[0],
event: RunEvent,
): Promise<void> {
await stream.writeSSE({
event: event.type,
id: String(event.sequence),
data: JSON.stringify(event),
});
}