61 lines
4.0 KiB
TypeScript
61 lines
4.0 KiB
TypeScript
import type { Request, Response, Router } from "express";
|
|
import { z } from "zod";
|
|
import { getLocalAgentContext } from "../context/localContext.js";
|
|
import { InteractionRequestError, type InteractionRequestStore } from "../sessions/interaction-request-store.js";
|
|
import { SessionContextMismatchError, type SessionMetadataStore } from "../sessions/metadataStore.js";
|
|
import type { OpencodeRuntimeAdapter } from "../runtime/opencode.js";
|
|
import { reconcileInteractionDelivery } from "../sessions/interaction-delivery-reconciliation.js";
|
|
|
|
const identifier = z.string().min(1).max(128);
|
|
const listQuerySchema = z.object({
|
|
limit: z.string().regex(/^[1-9]\d?$/).transform(Number).pipe(z.number().max(50)).optional(),
|
|
cursor: identifier.optional(),
|
|
}).strict();
|
|
|
|
type Options = { sessions: SessionMetadataStore; interactions: InteractionRequestStore;
|
|
runtime?: Pick<OpencodeRuntimeAdapter, "agentMessageState" | "readInteractionResult">;
|
|
isRunning?: (sessionId: string) => boolean };
|
|
|
|
/** These routes only persist user intent. Authorized /responses owns model continuation. */
|
|
export function registerInteractionRequestRoutes(router: Router, options: Options) {
|
|
const handle = (operation: "list" | "get" | "draft" | "reply" | "skip" | "cancel" | "resume") => async (req: Request, res: Response) => {
|
|
res.setHeader("Cache-Control", "private, no-store");
|
|
try {
|
|
const sessionId = identifier.parse(req.params.session_id);
|
|
const session = await options.sessions.get(getLocalAgentContext(req), sessionId);
|
|
if (!session) { res.status(404).json({ code: "SESSION_NOT_FOUND", detail: "session not found" }); return; }
|
|
const context = { sessionId: session.sessionId, actorKey: session.actorKey, projectKey: session.projectKey };
|
|
if (operation === "list") {
|
|
const page = await options.interactions.list(context, listQuerySchema.parse(req.query));
|
|
const items = await Promise.all(page.items.map((record) => reconcileInteractionDelivery(context, record, options.interactions, options.runtime, options.isRunning?.(sessionId))));
|
|
res.json({ ...page, items });
|
|
return;
|
|
}
|
|
const interactionId = identifier.parse(req.params.interaction_id);
|
|
if (operation === "get") {
|
|
if (Object.keys(req.query).length) throw new InteractionRequestError("invalid_query", 400);
|
|
const record = await options.interactions.get(context, interactionId);
|
|
if (!record) { res.status(404).json({ code: "interaction_not_found", detail: "interaction not found" }); return; }
|
|
res.json(await reconcileInteractionDelivery(context, record, options.interactions, options.runtime, options.isRunning?.(sessionId)));
|
|
return;
|
|
}
|
|
if (Object.keys(req.query).length) throw new InteractionRequestError("invalid_query", 400);
|
|
const record = operation === "draft"
|
|
? await options.interactions.updateDraft(context, interactionId, req.body)
|
|
: await options.interactions[operation](context, interactionId, req.body);
|
|
// A queued or blocked delivery is a successful saved answer, never an inference error.
|
|
res.json(record);
|
|
} catch (error) {
|
|
if (error instanceof z.ZodError) { res.status(400).json({ code: "INTERACTION_INVALID", detail: "invalid interaction request" }); return; }
|
|
if (error instanceof InteractionRequestError) { res.status(error.status).json({ code: error.code, detail: error.message, ...(error.current ? { current: error.current } : {}) }); return; }
|
|
if (error instanceof SessionContextMismatchError) { res.status(404).json({ code: "SESSION_NOT_FOUND", detail: "session not found" }); return; }
|
|
res.status(503).json({ code: "INTERACTION_UNAVAILABLE", detail: "interaction unavailable" });
|
|
}
|
|
};
|
|
const base = "/session/:session_id/interactions";
|
|
router.get(base, handle("list"));
|
|
router.get(`${base}/:interaction_id`, handle("get"));
|
|
router.put(`${base}/:interaction_id/draft`, handle("draft"));
|
|
for (const operation of ["reply", "skip", "cancel", "resume"] as const) router.post(`${base}/:interaction_id/${operation}`, handle(operation));
|
|
}
|