import { formatAnswerScalar } from "./format-answer.js"; import type { QuestionAnswer, QuestionnaireResult, QuestionParams } from "./types.js"; export const DECLINE_MESSAGE = "User declined to answer questions"; export const ENVELOPE_PREFIX = "User has answered your questions:"; export const ENVELOPE_SUFFIX = "You can now continue with the user's answers in mind."; /** * Map a `QuestionnaireResult` (or null/cancelled) to the LLM-facing tool envelope. * Pure of `(result, params)`; cancelled and "no segments" both fall to `DECLINE_MESSAGE` * so the model sees a single canonical "didn't answer" signal regardless of why. */ export function buildQuestionnaireResponse(result: QuestionnaireResult | null | undefined, params: QuestionParams) { if (!result || result.cancelled) { return buildToolResult(DECLINE_MESSAGE, { answers: result?.answers ?? [], cancelled: true, }); } const segments: string[] = []; for (let i = 0; i < params.questions.length; i++) { const a = result.answers.find((x) => x.questionIndex === i); if (a) segments.push(buildAnswerSegment(a)); } if (segments.length === 0) { return buildToolResult(DECLINE_MESSAGE, { answers: result.answers, cancelled: true }); } return buildToolResult(`${ENVELOPE_PREFIX} ${segments.join(" ")} ${ENVELOPE_SUFFIX}`, result); } /** * Format a single answer segment for the envelope. Pure of `a`. The `"Q"="A"` shape and * the optional `selected preview:` / `user notes:` suffixes are pinned by envelope tests. */ export function buildAnswerSegment(a: QuestionAnswer): string { const parts: string[] = [`"${a.question}"="${formatAnswerScalar(a, "envelope")}"`]; if (a.preview && a.preview.length > 0) parts.push(`selected preview: ${a.preview}`); if (a.notes && a.notes.length > 0) parts.push(`user notes: ${a.notes}`); return `${parts.join(". ")}.`; } export function buildToolResult(text: string, details: QuestionnaireResult) { return { content: [{ type: "text" as const, text }], details, }; }