abi/screenshot-to-code · error · ValueError

Update history must include at least one user message

Error message

Update history must include at least one user message

What it means

Raised in build_update_prompt_from_history when no message in the history list has role "user". Update-from-history prompts are anchored on the first user message (found via a linear scan); a history containing only assistant/system messages, or an empty list, leaves no anchor and the builder refuses to construct the prompt.

Solutions

  1. Ensure the history array includes the original user message(s) with role exactly "user" (lowercase).
  2. Fix the upstream serializer that produced assistant-only history.
  3. If no user turn exists, use the file-snapshot strategy instead: pass fileState.content so plan.py picks "update_from_file_snapshot".
  4. Add a route-level check: reject update requests whose history has no user message with a 400.

Example fix

# before
history = [{"role": "assistant", "content": "..."}]

# after
history = [
    {"role": "user", "content": "make a landing page"},
    {"role": "assistant", "content": "..."},
]
Defensive patterns

Strategy: validation

Validate before calling

def history_has_user_message(history: list[dict]) -> bool:
    return any(item.get("role") == "user" for item in history)

Type guard

def is_valid_update_history(history: list[dict]) -> bool:
    """True when update-from-history can anchor on a user message."""
    return any(
        isinstance(item, dict) and item.get("role") == "user" for item in history
    )

Try / catch

try:
    prompt = build_update_prompt_from_history(stack, history, image_gen_enabled)
except ValueError as e:
    if "at least one user message" in str(e):
        return error_response("Update history is missing the original user message", 400)
    raise

Prevention

When it happens

Trigger: The plan strategy "update_from_history" was selected (history length > 0 per plan.py) but every item's role is "assistant" — e.g. a truncated or filtered history that dropped user turns; or roles serialized with different casing ("User") by a client; or history entries missing the role key entirely (item["role"] would KeyError first, so in practice it's all-assistant histories).

Common situations: History persisted/relayed from a client that strips user messages for size; role casing or naming drift between frontend and backend ("User" vs "user"); hand-built history arrays for API testing containing only model responses.

Related errors


AI-assisted analysis of abi/screenshot-to-code@d026163f58 (2026-08-14). Data as JSON: /api/errors/e74a0276b7b01a51. Report an issue: GitHub.

Appendix: source

Thrown at backend/prompts/update/from_history.py:23

from prompts import system_prompt
from prompts.design_system import build_design_system_prompt_block
from prompts.policies import build_selected_stack_policy, build_user_image_policy
from prompts.prompt_types import PromptHistoryMessage, Stack
from prompts.message_builder import Prompt, build_history_message


def build_update_prompt_from_history(
    stack: Stack,
    history: list[PromptHistoryMessage],
    image_generation_enabled: bool,
    design_system: str | None = None,
) -> Prompt:
    first_user_index = next(
        (index for index, item in enumerate(history) if item["role"] == "user"),
        -1,
    )
    if first_user_index == -1:
        raise ValueError("Update history must include at least one user message")

    prompt_messages: Prompt = [
        cast(
            ChatCompletionMessageParam,
            {
                "role": "system",
                "content": system_prompt.SYSTEM_PROMPT,
            },
        )
    ]
    selected_stack = build_selected_stack_policy(stack)
    image_policy = build_user_image_policy(image_generation_enabled)
    design_system_block = build_design_system_prompt_block(design_system)
    for index, item in enumerate(history):
        if index == first_user_index:
            stack_prefix_parts = [selected_stack, image_policy]
            if design_system_block:
                stack_prefix_parts.append(design_system_block.strip())

View on GitHub (pinned to d026163f58)