langchain-ai/deepagents · error · ValueError

deny decisions require a reason

Error message

deny decisions require a reason

What it means

A pydantic `model_validator(mode="after")` on `AutoDecision` enforces that any decision equal to "deny" carries a non-blank `reason`. This guarantees the user always sees why an action was blocked in auto mode; a deny without a reason is considered invalid and rejected at parse time.

Source

Thrown at libs/code/deepagents_code/auto_mode.py:285

    tool_call_id: str
    decision: Literal["allow", "deny"]
    category: AutoDecisionCategory
    reason: str

    @field_validator("tool_call_id")
    @classmethod
    def _nonempty_id(cls, value: str) -> str:
        if not value:
            msg = "tool_call_id must not be empty"
            raise ValueError(msg)
        return value

    @model_validator(mode="after")
    def _denial_has_reason(self) -> AutoDecision:
        if self.decision == "deny" and not self.reason.strip():
            msg = "deny decisions require a reason"
            raise ValueError(msg)
        return self


class AutoDecisionBatch(BaseModel):
    """Validated classifier response for one unresolved action batch."""

    model_config = ConfigDict(extra="forbid")

    decisions: list[AutoDecision]


class AutoModeCounters(TypedDict):
    """Server-owned denial and availability counters for one thread."""

    consecutive_denials: int
    total_denials: int
    consecutive_unavailable: int
    last_batch_id: str | None

View on GitHub (pinned to a1af029e6e)

Solutions

  1. Include a human-readable reason string whenever the decision is "deny".
  2. Update the classifier prompt to require a reason for denies (few-shot example with deny + reason).
  3. Pre-validate raw responses: if decision == "deny" and reason is blank, retry the classification or fall back to prompting the user.
  4. Normalize whitespace-only reasons by rejecting and re-asking the model rather than passing them through.

Example fix

// before
AutoDecision(decision="deny", reason="")  # ValueError
// after
reason = raw.get("reason") or "No reason provided by classifier"
AutoDecision(decision="deny", reason=reason)
Defensive patterns

Strategy: validation

Validate before calling

def _safe_decision(raw: dict) -> AutoDecision | None:
    if raw.get("decision") == "deny" and not str(raw.get("reason", "")).strip():
        logger.warning("deny without reason: %r", raw)
        return None  # retry classification or escalate to the user
    return AutoDecision(**raw)

Type guard

def _deny_has_reason(raw: object) -> TypeGuard[dict[str, str]]:
    return (
        isinstance(raw, dict)
        and (raw.get("decision") != "deny" or bool(str(raw.get("reason", "")).strip()))
    )

Try / catch

try:
    decision = AutoDecision(**raw)
except pydantic.ValidationError as exc:
    if any("deny decisions require a reason" in e["msg"] for e in exc.errors()):
        decision = reclassify_with_reason_required(raw)
    else:
        raise

Prevention

When it happens

Trigger: Constructing AutoDecision(decision="deny", reason="") or reason=" ", or parsing an LLM classifier response where a deny decision omits the reason field.

Common situations: The classifier emitted `{"decision": "deny"}` with no reason; prompt changes dropped the reason requirement; whitespace-only reasons from truncated model output.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


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