107 lines
3.3 KiB
TypeScript
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),
|
|
});
|
|
}
|