import { z } from "zod"; export const INTERACTION_VERSION = "agent-interaction-request@1" as const; const id = z.string().min(1).max(128); const unique = (values: string[]) => new Set(values).size === values.length; export const interactionQuestionSchema = z.object({ question_id: id, title: z.string().min(1).max(200), body: z.string().min(1).max(2000), options: z.array(z.object({ id, label: z.string().min(1).max(200) }).strict()).max(12), mode: z.enum(["single", "multiple"]), allow_custom: z.boolean(), required: z.boolean(), }).strict().refine((q) => unique(q.options.map((o) => o.id)), "duplicate option ids") .refine((q) => q.options.length > 0 || q.allow_custom, "question must offer an answer input"); const byteLimit = (value: unknown, max: number) => Buffer.byteLength(JSON.stringify(value), "utf8") <= max; export const interactionQuestionsSchema = z.array(interactionQuestionSchema).min(1).max(3) .refine((qs) => unique(qs.map((q) => q.question_id)), "duplicate question ids") .refine((qs) => qs.reduce((n, q) => n + q.title.length + q.body.length + q.options.reduce((m, o) => m + o.label.length, 0), 0) <= 10000, "definition text too long") .refine((qs) => byteLimit(qs, 32768), "definition too large"); export const interactionAnswerSchema = z.object({ question_id: id, selected_option_ids: z.array(id).max(12), custom_text: z.string().max(2000) }).strict() .refine((a) => unique(a.selected_option_ids), "duplicate selected option ids"); export const interactionAnswersSchema = z.array(interactionAnswerSchema).max(3) .refine((as) => unique(as.map((a) => a.question_id)), "duplicate question ids") .refine((as) => as.reduce((n, a) => n + a.custom_text.length, 0) <= 10000, "answer text too long") .refine((as) => byteLimit(as, 16384), "answers too large"); const write = { version: z.literal(INTERACTION_VERSION), request_id: id }; export const interactionDraftInputSchema = z.object({ ...write, expected_draft_revision: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER), answers: interactionAnswersSchema }).strict(); export const interactionReplyInputSchema = z.object({ ...write, expected_revision: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER), answers: interactionAnswersSchema }).strict(); export const interactionMutationInputSchema = z.object({ ...write, expected_revision: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER) }).strict(); export type InteractionQuestion = z.infer; export type InteractionAnswer = z.infer; export type InteractionResolvedAnswer = InteractionAnswer & { selected_options: Array<{ id: string; label: string }> }; export type InteractionStatus = "pending" | "answered" | "skipped" | "cancelled" | "invalidated"; export type InteractionDelivery = { native_message_id: string; revision: number; status: "queued" | "dispatching" | "delivered" | "blocked" | "cancelled"; error_code?: string; response_id?: string }; export type InteractionRequest = { version: typeof INTERACTION_VERSION; interaction_id: string; message_id: string; turn_id: string; call_id: string; questions: InteractionQuestion[]; status: InteractionStatus; revision: number; draft_revision: number; draft: InteractionAnswer[]; answers?: InteractionResolvedAnswer[]; created_at: string; resolved_at?: string; delivery?: InteractionDelivery; }; export const interactionDeliverySchema = z.object({ native_message_id: z.string().regex(/^msg_[a-f0-9]{32}$/), revision: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER), status: z.enum(["queued", "dispatching", "delivered", "blocked", "cancelled"]), error_code: id.optional(), response_id: id.optional(), }).strict(); export const interactionRequestSchema = z.object({ version: z.literal(INTERACTION_VERSION), interaction_id: id, message_id: id, turn_id: id, call_id: id, questions: interactionQuestionsSchema, status: z.enum(["pending", "answered", "skipped", "cancelled", "invalidated"]), revision: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER), draft_revision: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER), draft: interactionAnswersSchema, answers: z.array(z.object({ question_id: id, selected_option_ids: z.array(id).max(12), custom_text: z.string().max(2000), selected_options: z.array(z.object({ id, label: z.string().min(1).max(200) }).strict()).max(12) }).strict()).max(3).optional(), created_at: z.string().datetime(), resolved_at: z.string().datetime().optional(), delivery: interactionDeliverySchema.optional(), }).strict(); export const interactionBlocksTurn = (r: InteractionRequest) => r.status === "pending" || Boolean(r.delivery && !["delivered", "cancelled"].includes(r.delivery.status)); export const renderInteractionReplyText = (record: InteractionRequest): string => { if (record.status === "skipped") return "已跳过本组可选问题。"; if (record.status !== "answered") throw new Error("interaction has no submitted answer"); const lines = (record.answers ?? []).map((answer) => { const question = record.questions.find((q) => q.question_id === answer.question_id); return `${question?.title ?? answer.question_id}: ${[...answer.selected_options.map((o) => o.label), answer.custom_text].filter(Boolean).join(";") || "未填写"}`; }); return lines.join("\n"); }; /** Model-facing protocol stays separate from the persisted human reply. */ export const renderInteractionAnswer = (record: InteractionRequest): string => { if (record.status === "skipped") return "用户明确跳过了本组非必填问题。"; const text = `用户提交的提问回复:\n${renderInteractionReplyText(record)}`; if (text.length > 10000) throw new Error("rendered interaction answer exceeds 10000 characters"); return text; };