Files
next-tjwater-agent/src/routes/chat-interaction-bridge.ts
T

65 lines
4.9 KiB
TypeScript

import type { Express } from "express";
import { z } from "zod";
import { isTrustedInternalBridgeRequest } from "../product/internal-bridge-auth.js";
import { getRuntimeSessionContext } from "../runtime/sessionContext.js";
import { interactionQuestionsSchema } from "../chat/interaction-request-contract.js";
import type { InteractionRequestStore } from "../sessions/interaction-request-store.js";
import type { OpencodeRuntimeAdapter } from "../runtime/opencode.js";
const identity = { session_id: z.string().min(1).max(128) };
const call = z.object({ ...identity, call_id: z.string().min(1).max(128), tool: z.string().min(1).max(200) }).strict();
/** Only trusted executors enter this gate; model input never selects an owner or turn. */
export function registerChatInteractionBridge(app: Express, options: { internalToken: string; interactions: InteractionRequestStore; runtime?: OpencodeRuntimeAdapter }) {
const calls = new Map<string, { turn: string; active: Map<string, string> }>();
for (const operation of ["policy", "enter", "leave", "create"] as const) {
app.post(`/internal/chat-interactions/${operation}`, async (req, res) => {
if (!isTrustedInternalBridgeRequest(req, options.internalToken)) { res.status(403).json({ message: "forbidden" }); return; }
try {
const body = operation === "policy" ? z.object(identity).strict().parse(req.body)
: operation === "create" ? call.extend({ questions: interactionQuestionsSchema }).parse(req.body) : call.parse(req.body);
const context = getRuntimeSessionContext(body.session_id);
const enabled = Boolean(context?.hostTurnId && context.hostMessageId && context.capabilities?.includes("host-question@1")
&& context.purpose !== "task_execution" && context.learningMode !== "review");
if (operation === "policy") {
const requested = context && enabled ? await options.interactions.getForTurn(context, context.hostTurnId!) : null;
res.json({ enabled, waiting: Boolean(requested) }); return;
}
const input = call.parse({ session_id: body.session_id, call_id: "call_id" in body ? body.call_id : "", tool: "tool" in body ? body.tool : "" });
if (!context) { res.status(403).json({ message: "No authorized host turn is bound to this session" }); return; }
if (!enabled || !context) {
if (input.tool === "ask_user") { res.status(403).json({ message: "Host questions are not enabled for this turn" }); return; }
res.json({ allowed: true }); return;
}
let state = calls.get(context.sessionId);
if (!state || state.turn !== context.hostTurnId) { state = { turn: context.hostTurnId!, active: new Map() }; calls.set(context.sessionId, state); }
if (operation === "leave") { state.active.delete(input.call_id); res.json({ allowed: true }); return; }
if (context.hostQuestionClosed) { res.status(409).json({ message: "The owning turn has been stopped" }); return; }
if (operation === "create") {
if (input.tool !== "ask_user" || state.active.get(input.call_id) !== "ask_user" || !("questions" in body)) {
res.status(409).json({ message: "Question tool has not acquired the turn" }); return;
}
const previous = await options.interactions.getBlocking(context);
if (previous?.delivery && options.runtime && await options.runtime.agentMessageState(context.sessionId, previous.delivery.native_message_id) === "delivered") {
await options.interactions.updateDelivery(context, previous.interaction_id, { expected_revision: previous.delivery.revision, status: "delivered", response_id: previous.delivery.response_id });
}
const record = await options.interactions.create(context, { message_id: context.hostMessageId!, turn_id: context.hostTurnId!, call_id: input.call_id, questions: body.questions });
res.json(record); return;
}
const blocking = await options.interactions.getBlocking(context);
const requested = await options.interactions.getForTurn(context, context.hostTurnId!);
if (requested || blocking?.status === "pending" || input.tool === "question" || [...state.active.values()].includes("ask_user")
|| (input.tool === "ask_user" && state.active.size > 0)) {
res.status(409).json({ message: "Finish other tools before asking; after asking, wait for the user" }); return;
}
state.active.set(input.call_id, input.tool);
res.json({ allowed: true });
} catch (error) {
if (error instanceof z.ZodError) { res.status(400).json({ message: "invalid interaction bridge request" }); return; }
const status = error && typeof error === "object" && "status" in error && typeof error.status === "number" ? error.status : 503;
res.status(status).json({ message: "Interaction state is unavailable; no question was acknowledged" });
}
});
}
}