langchain-ai/deepagents · error · ValueError

phase must be one of {sorted(_ATTEMPT_PHASES)}, got {phase!r

Error message

phase must be one of {sorted(_ATTEMPT_PHASES)}, got {phase!r}

What it means

build_attempt_event emits a model_attempt lifecycle event whose phase must be one of _ATTEMPT_PHASES = frozenset({"start", "complete"}) (model_retry.py:133). An unknown phase raises ValueError('phase must be one of [\'complete\', \'start\'], got {phase!r}'), keeping attempt-event consumers able to rely on a fixed phase vocabulary.

Source

Thrown at libs/code/deepagents_code/model_retry.py:950

def build_attempt_event(call_id: str, attempt: int, *, phase: str) -> dict[str, object]:
    """Build the custom-stream payload marking one model attempt boundary.

    Args:
        call_id: Opaque ID shared by every attempt of one model call.
        attempt: The 0-indexed attempt whose boundary is marked.
        phase: `"start"` before the handler runs, `"complete"` after it
            returns successfully.

    Returns:
        A stream-writer payload consumed by the client renderers.

    Raises:
        ValueError: If `phase` is not a known lifecycle phase.
    """
    if phase not in _ATTEMPT_PHASES:
        msg = f"phase must be one of {sorted(_ATTEMPT_PHASES)}, got {phase!r}"
        raise ValueError(msg)
    return {
        "type": "model_attempt",
        "phase": phase,
        "call_id": call_id,
        "attempt": attempt,
    }


def _validated_call_id(value: object) -> str | None:
    """Return `value` as a correlation ID, or `None` when it is untrusted."""
    if (
        not isinstance(value, str)
        or not 1 <= len(value) <= _CALL_ID_MAX_LENGTH
        or any(char not in _CALL_ID_CHARS for char in value)
    ):
        return None
    return value

View on GitHub (pinned to a1af029e6e)

Solutions

  1. Use exactly "start" or "complete" for the phase argument.
  2. Normalize labels before calling: map application states to the two supported phases.
  3. If a new phase is genuinely needed, check whether the library version supports it or propose the change upstream — do not pass ad-hoc strings.

Example fix

// before
build_attempt_event(phase="begin", call_id="call-1", attempt=1)  # ValueError
// after
build_attempt_event(phase="start", call_id="call-1", attempt=1)
Defensive patterns

Strategy: type-guard

Validate before calling

PHASES = {"start", "complete"}
if phase not in PHASES:
    phase = "complete" if phase in {"finish", "end", "done"} else "start"

Type guard

from typing import Literal, TypeGuard
Phase = Literal["start", "complete"]
def is_phase(value: object) -> TypeGuard[Phase]:
    return value in ("start", "complete")

Try / catch

try:
    event = build_attempt_event(phase=phase, call_id=call_id, attempt=attempt)
except ValueError as exc:
    logging.warning("%s; skipping attempt event", exc)

Prevention

When it happens

Trigger: Calling build_attempt_event with phase values like "begin", "finish", "error", "retry", or any casing variant ("Start"), since membership in the frozenset is exact-match.

Common situations: Inventing new lifecycle phases when instrumenting model calls; renaming phases in application code without updating the event builder; passing a human-readable label instead of the canonical phase string.

Understand the failure class

Background: Invalid enum value errors: "Unknown type", "Invalid scope", "must be one of" — when a string is not on the library's allowed list — this error's family across 23 libraries.

Related errors


AI-assisted analysis of langchain-ai/deepagents@a1af029e6e (2026-08-29). Data as JSON: /api/errors/58d04bf1ba440e2e. Report an issue: GitHub.