/** * LLM-backed LiveAgent for the live-mode E2E suite. * * Implements the same interface as createFakeAgent() in * tests/live-e2e/agent.mjs: generateVariants(event, context) returns * { scopedCss, variants[] }, and applyManualEdits(event, context) returns the * production manual-edit Apply result shape. It also implements * handleSteer(event, context) for page-level Steer bar messages. The * orchestrator handles wrap, write, accept, and carbonize cleanup * deterministically. * * Primary provider/model: Anthropic + Claude Haiku 4.5. DeepSeek V4 Flash is * a secondary cheap fallback used only when ANTHROPIC_API_KEY is absent and * DEEPSEEK_API_KEY is present, or when explicitly forced with * IMPECCABLE_E2E_LLM_PROVIDER=deepseek. Override the model via { model } when * constructing, or via IMPECCABLE_E2E_LLM_MODEL at the call site. * * 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) when the selected provider honors it. * * Returns null from createLlmAgent() when the selected provider's 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'; import { applyManualEditBatchToSource, loadManualEditEventBatch } from '../agent.mjs'; import { applySteerEdits } from '../agent.mjs'; 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'); // Frontier default: gpt-5.6-terra at medium reasoning effort. Haiku stayed // too far below the models that actually drive live sessions in the field; // a spec change Haiku tolerates can still confuse or be confused by the // frontier tier, so the harness should exercise the tier users run. const DEFAULT_OPENAI_MODEL = 'gpt-5.6-terra'; const DEFAULT_OPENAI_REASONING_EFFORT = 'medium'; const DEFAULT_ANTHROPIC_MODEL = 'claude-haiku-4-5'; // DeepSeek model list: https://api-docs.deepseek.com/api/list-models const DEFAULT_DEEPSEEK_MODEL = 'deepseek-v4-flash'; const DEFAULT_DEEPSEEK_API_BASE_URL = 'https://api.deepseek.com/anthropic'; const LLM_REQUEST_MAX_RETRIES = 1; const VARIANT_REQUEST_TIMEOUT_MS = 105_000; const MANUAL_EDIT_REQUEST_TIMEOUT_MS = 55_000; const MANUAL_EDIT_RESPONSE_MAX_ATTEMPTS = 3; export const VARIANT_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; replace mode matches the picked element tag, insert mode is net-new content",', ' "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', '- Replace mode: each variant.innerHtml must be a single top-level HTML element using the EXACT same tag as the picked element.', '- Insert mode (`event.mode === "insert"`): each variant.innerHtml must be net-new content that honors event.freeformPrompt. It does NOT replace the anchor and does NOT need to use the anchor tag or preserve anchor copy.', '- Insert mode variants must contain visible inserted content. Do not return empty roots, placeholder-only roots, inline style= attributes, or test hooks like
.', '- Replace mode: the single top-level element is the replacement root itself. If the picked element is
...
, emit
...
with edited children directly; do not wrap a duplicate
inside another root.', '- Replace mode: PRESERVE the original element\'s className verbatim. If the picked element\'s outerHTML contains class="hero-title", every variant\'s innerHtml MUST contain exactly class="hero-title"; do not add, remove, or rename classes. This is a hard requirement — mapped-list fixtures depend on the class string staying stable across the variant set.', '- Replace mode: PRESERVE all existing visible copy exactly. GO variants change presentation, hierarchy, and styling; they must not rewrite titles, paragraphs, button labels, or user-applied manual copy edits.', '- Replace mode: use the visible literal copy from the picked element. Do not emit framework template expressions or placeholders such as {name}, {amount}, ${value}, or {{value}} in innerHtml.', '- Replace mode: for bare text elements, keep the full visible copy in one editable text node. If you add child markup for styling, wrap the entire copy; never split the copy across sibling text nodes.', '- Replace mode: PRESERVE existing class-bearing descendant elements in place. If the picked element contains

and

, keep those elements/classes as direct descendants of the replacement root; do not wrap them in a new structural div such as

.', '- Replace mode: Do not return source-identical variants. For a bare text element, preserve the root tag/class/copy but add a small child span or styling hook so Accept persists a real source change.', '- Replace mode: for non-bare elements where the existing children must stay in place, add a permanent styling hook such as data-design-variant="1" so the markup is materially changed without changing visible text. Never invent data-impeccable-* attributes: that namespace is reserved for temporary runtime state.', '- 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).', '- Put visual styling in scopedCss, not style= attributes inside variant.innerHtml.', '- 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