langchain-ai/deepagents · error · ValueError

An offload {status!r} result must carry a reason.

Error message

An offload {status!r} result must carry a reason.

What it means

`unchanged_offload_result` builds the wire result for non-compacting offload outcomes. Because the flat wire shape types `error` as `str | None` for every status, a `denied` or `failed` result without a reason cannot be caught by the type checker, so this constructor enforces it at runtime: refusing without a reason would render as the client's generic 'server rejected the operation' message. Raises ValueError when status is denied/failed and error is falsy.

Source

Thrown at libs/code/deepagents_code/offload_middleware.py:674

        status: A non-compacting outcome.
        messages: Messages left in the conversation.
        tokens: Context estimate, unchanged by definition.
        error: Reason, required for `denied` and `failed`.

    Returns:
        Typed result containing unchanged context statistics.

    Raises:
        ValueError: If a refusal carries no reason.
    """
    if status in {"denied", "failed"} and not error:
        # `error` is `str | None` on every status because the wire shape is one
        # flat object, so the checker cannot make "a refusal has a reason" a
        # compile-time fact. Enforce it at the single construction point
        # instead: a reasonless refusal renders as the client's generic "the
        # server rejected the operation", which tells the user nothing.
        msg = f"An offload {status!r} result must carry a reason."
        raise ValueError(msg)
    return {
        "status": status,
        "messages_offloaded": 0,
        "messages_kept": messages,
        "tokens_before": tokens,
        "tokens_after": tokens,
        "archive_path": None,
        "archive_ephemeral": False,
        "error": error,
    }


class OffloadCompleteResponse(TypedDict):
    """Wire response for an attempt that finished without needing the client."""

    status: Literal["complete"]
    result: OffloadResult

View on GitHub (pinned to a1af029e6e)

Solutions

  1. Pass a user-meaningful `error` string when building denied/failed results.
  2. Use a status that permits no error (e.g. a complete/no-op status) if nothing was refused.
  3. Route all refusal construction through `unchanged_offload_result` so the check stays in one place.

Example fix

// before
result = unchanged_offload_result("denied", messages=3, tokens=900)
// after
result = unchanged_offload_result("denied", messages=3, tokens=900, error="offload disabled for this thread")
Defensive patterns

Strategy: validation

Validate before calling

assert status not in {"denied", "failed"} or error, f"{status!r} requires an error reason"

Type guard

def refusal_has_reason(status: OffloadStatus, error: str | None) -> bool:
    return status not in ("denied", "failed") or bool(error)

Try / catch

try:
    result = unchanged_offload_result(status, messages=m, tokens=t, error=reason)
except ValueError:
    result = unchanged_offload_result(status, messages=m, tokens=t, error=DEFAULT_DENIAL_REASON)

Prevention

When it happens

Trigger: Calling `unchanged_offload_result("denied", messages=..., tokens=...)` or with "failed" without passing `error=`; HTTP-boundary code building a refusal result without a reason string.

Common situations: Custom server middleware denying offloads (rate limits, auth) without setting a reason; tests or handlers constructing refusal results directly.

Understand the failure class

Background: "Missing required field" and "field is required" errors: why libraries reject payloads that omit mandatory fields — this error's family across 20 libraries.

Related errors


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