/** * LLM-backed VariantAgent for the live-mode E2E suite. * * Implements the same one-method interface as createFakeAgent() in * tests/live-e2e/agent.mjs: generateVariants(event, context) returns * { scopedCss, variants[] }. The orchestrator handles wrap, write, accept, * and carbonize cleanup deterministically, so this module's only job is * producing variant content for the wrapper. * * Default model: Claude Haiku 4.5 — fast, cheap, smart enough for variant * generation in test fixtures. Override via { model } when constructing, * or via the IMPECCABLE_E2E_LLM_MODEL env var at the call site (test runner). * * Prompt caching: live.md (the live-mode skill spec) is the bulk of the * system prompt and is stable across calls. We mark a cache_control breakpoint * on the last system block so both the JSON-contract instructions and the * spec are cached as one prefix. Subsequent calls in the same run pay only * the cache-read rate (~0.1× input). * * Returns null from createLlmAgent() when ANTHROPIC_API_KEY is unset; the * test runner reads that and skips the case rather than failing. */ import fs from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import Anthropic from '@anthropic-ai/sdk'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = path.join(__dirname, '..', '..', '..'); const LIVE_MD_PATH = path.join(REPO_ROOT, 'skill', 'reference', 'live.md'); const DEFAULT_MODEL = 'claude-haiku-4-5'; const SYSTEM_INSTRUCTIONS = [ 'You are an automated subagent inside Impeccable\'s live-mode test harness.', 'Given an element the user picked, an action, and a count, you produce variant DOM content in a strict JSON shape.', '', 'OUTPUT CONTRACT — return ONLY a JSON object with this exact shape. No prose, no code fences, no commentary:', '', '{', ' "scopedCss": "string — contents of the preview CSS block, authored according to wrapInfo.cssAuthoring",', ' "variants": [', ' {', ' "innerHtml": "string — single top-level HTML element matching the picked element\'s tag, e.g.

Title

",', ' "params": [/* optional 0-4 ParamSpec entries */]', ' }', ' ]', '}', '', 'ParamSpec is one of:', ' { "id": "string", "kind": "range", "min": number, "max": number, "step": number, "default": number, "label": "string" }', ' { "id": "string", "kind": "steps", "default": "string", "label": "string", "options": [{ "value": "string", "label": "string" }, ...] }', ' { "id": "string", "kind": "toggle", "default": boolean, "label": "string" }', '', 'REQUIREMENTS', '- Each variant.innerHtml must be a single top-level HTML element. Use the EXACT same tag as the picked element.', '- PRESERVE the original element\'s className verbatim. If the picked element\'s outerHTML contains class="hero-title", every variant\'s innerHtml MUST contain the same class="hero-title" string (you may add additional class names alongside, never remove or rename the original). This is a hard requirement — automated harnesses verify the original class survives across the variant set.', '- Generate exactly event.count variants — no more, no fewer.', '- Mix the param kinds across the variant set: include at least one range, one steps, and one toggle when count >= 3.', '- The scopedCss must follow wrapInfo.cssAuthoring exactly: use its selector strategy, rulePattern, requirements, and forbidden patterns.', '- Wire scopedCss rules against the params you emit (CSS vars for range/toggle, attribute selectors for steps/toggle).', '- Use HTML attribute syntax in innerHtml (class=, not className=). The orchestrator translates per file syntax.', '- Do NOT emit the wrapping
. The orchestrator wraps your content.', '- Do NOT emit the outer