import { randomUUID } from "node:crypto"; import type { UIMessageChunk } from "ai"; import { z } from "zod"; import { agentTriggerContextSchema } from "../routes/agentTrigger.js"; const text = z.string().min(1).max(10000); const inputMessage = z.object({ type: z.literal("message").optional(), role: z.literal("user"), content: z.union([text, z.array(z.object({ type: z.literal("input_text"), text }).strict()).min(1).max(20)]), }).strict(); // A Responses text-generation surface backed by the owned OpenCode session. // Unsupported OpenAI options fail validation instead of being silently ignored. export const responseRequestSchema = z.object({ input: z.union([text, z.array(inputMessage).min(1).max(20)]), model: z.string().min(1).optional(), stream: z.boolean().default(false), conversation: z.union([z.string().min(1).max(128), z.object({ id: z.string().min(1).max(128) }).strict()]).optional(), metadata: z.record(z.string().max(64), z.string().max(512)).refine((value) => Object.keys(value).length <= 16).optional(), tjwater: z.object({ topic_id: z.string().uuid().optional(), client_message_id: z.string().min(1).max(128).optional(), delivery: z.enum(["steer", "queue"]).optional(), input_id: z.string().regex(/^msg_[a-f0-9]{32}$/).optional(), interaction_id: z.string().min(1).max(128).optional(), approval_mode: z.enum(["request", "always"]).default("request"), capabilities: z.array(z.string().max(64)).max(20).default([]), trigger_context: agentTriggerContextSchema.optional(), task_context: z.object({ report_revision: z.number().int().positive().optional(), evidence_id: z.string().min(1).max(128).optional(), gap_id: z.string().min(1).max(128).optional(), }).strict().optional(), }).strict().default({}), }).strict(); export function responsePrompt(input: z.infer["input"]) { return typeof input === "string" ? input : input.map((message) => typeof message.content === "string" ? message.content : message.content.map((part) => part.text).join("\n")).join("\n\n"); } type OutputText = { type: "output_text"; text: string; annotations: unknown[]; logprobs: unknown[] }; type OutputMessage = { id: string; type: "message"; status: "in_progress" | "completed" | "incomplete"; role: "assistant"; content: OutputText[] }; export type ResponseObject = { id: string; object: "response"; created_at: number; completed_at: number | null; status: "in_progress" | "completed" | "failed" | "incomplete"; model: string; conversation: { id: string }; output: OutputMessage[]; usage: null; error: { code: string; message: string } | null; incomplete_details: { reason: string } | null; metadata: Record; }; export type ResponseEvent = Record & { type: string; sequence_number: number }; export function createResponsesProjection(options: { model: string; conversationId: string; metadata?: Record; emit: (event: ResponseEvent) => void; }) { const response: ResponseObject = { id: `resp_${randomUUID().replaceAll("-", "")}`, object: "response", created_at: Math.floor(Date.now() / 1000), completed_at: null, status: "in_progress", model: options.model, conversation: { id: options.conversationId }, output: [], usage: null, error: null, incomplete_details: null, metadata: options.metadata ?? {}, }; let sequence = 0; let started = false; const textIndices = new Map(); const closedText = new Set(); const emit = (type: string, value: Record) => options.emit({ type, sequence_number: sequence++, ...structuredClone(value) }); const begin = () => { if (started) return; started = true; emit("response.created", { response }); emit("response.in_progress", { response }); }; const message = (id?: string) => { if (!response.output.length) { response.output.push({ id: id ?? `msg_${randomUUID().replaceAll("-", "")}`, type: "message", status: "in_progress", role: "assistant", content: [] }); emit("response.output_item.added", { output_index: 0, item: response.output[0] }); } return response.output[0]!; }; const openText = (id: string) => { const item = message(); if (!textIndices.has(id)) { const index = item.content.length; textIndices.set(id, index); item.content.push({ type: "output_text", text: "", annotations: [], logprobs: [] }); emit("response.content_part.added", { item_id: item.id, output_index: 0, content_index: index, part: item.content[index] }); } return { item, index: textIndices.get(id)! }; }; const endText = (id: string) => { if (!textIndices.has(id) || closedText.has(id)) return; const { item, index } = openText(id); closedText.add(id); emit("response.output_text.done", { item_id: item.id, output_index: 0, content_index: index, text: item.content[index]!.text, logprobs: [] }); emit("response.content_part.done", { item_id: item.id, output_index: 0, content_index: index, part: item.content[index] }); }; const finish = (reason: "completed" | "failed" | "cancelled" = "completed") => { if (response.status !== "in_progress") return; begin(); for (const id of textIndices.keys()) endText(id); for (const item of response.output) { item.status = reason === "completed" ? "completed" : "incomplete"; emit("response.output_item.done", { output_index: 0, item }); } response.status = reason === "cancelled" ? "incomplete" : reason; if (reason === "completed") response.completed_at = Math.floor(Date.now() / 1000); emit(`response.${response.status}`, { response }); }; return { response, begin, finish, write(chunk: UIMessageChunk) { if (response.status !== "in_progress") return; begin(); if (chunk.type === "start") message(chunk.messageId); else if (chunk.type === "text-start") openText(chunk.id); else if (chunk.type === "text-delta") { const { item, index } = openText(chunk.id); item.content[index]!.text += chunk.delta; emit("response.output_text.delta", { item_id: item.id, output_index: 0, content_index: index, delta: chunk.delta, logprobs: [] }); } else if (chunk.type === "text-end") endText(chunk.id); else if (chunk.type === "error") response.error = { code: "agent_execution_error", message: chunk.errorText }; else if (chunk.type.startsWith("data-")) { const data = chunk as { type: string; data: unknown; id?: string; transient?: boolean }; emit(`tjwater.${chunk.type.slice(5)}`, { response_id: response.id, data: data.data, ...(data.id ? { id: data.id } : {}), ...(data.transient ? { transient: true } : {}) }); } // Finalization belongs to the route, after task completion and persistence. }, }; }