{"record":{"id":"1e8e7e2f08efc57b","repo":"langchain-ai/deepagents","slug":"deny-decisions-require-a-reason","errorCode":null,"errorMessage":"deny decisions require a reason","messagePattern":"deny decisions require a reason","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"libs/code/deepagents_code/auto_mode.py","lineNumber":285,"sourceCode":"\n    tool_call_id: str\n    decision: Literal[\"allow\", \"deny\"]\n    category: AutoDecisionCategory\n    reason: str\n\n    @field_validator(\"tool_call_id\")\n    @classmethod\n    def _nonempty_id(cls, value: str) -> str:\n        if not value:\n            msg = \"tool_call_id must not be empty\"\n            raise ValueError(msg)\n        return value\n\n    @model_validator(mode=\"after\")\n    def _denial_has_reason(self) -> AutoDecision:\n        if self.decision == \"deny\" and not self.reason.strip():\n            msg = \"deny decisions require a reason\"\n            raise ValueError(msg)\n        return self\n\n\nclass AutoDecisionBatch(BaseModel):\n    \"\"\"Validated classifier response for one unresolved action batch.\"\"\"\n\n    model_config = ConfigDict(extra=\"forbid\")\n\n    decisions: list[AutoDecision]\n\n\nclass AutoModeCounters(TypedDict):\n    \"\"\"Server-owned denial and availability counters for one thread.\"\"\"\n\n    consecutive_denials: int\n    total_denials: int\n    consecutive_unavailable: int\n    last_batch_id: str | None","sourceCodeStart":267,"sourceCodeEnd":303,"githubUrl":"https://github.com/langchain-ai/deepagents/blob/a1af029e6e73cb17c36bff823d227747b28e91e1/libs/code/deepagents_code/auto_mode.py#L267-L303","documentation":"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.","triggerScenarios":"Constructing AutoDecision(decision=\"deny\", reason=\"\") or reason=\"   \", or parsing an LLM classifier response where a deny decision omits the reason field.","commonSituations":"The classifier emitted `{\"decision\": \"deny\"}` with no reason; prompt changes dropped the reason requirement; whitespace-only reasons from truncated model output.","solutions":["Include a human-readable reason string whenever the decision is \"deny\".","Update the classifier prompt to require a reason for denies (few-shot example with deny + reason).","Pre-validate raw responses: if decision == \"deny\" and reason is blank, retry the classification or fall back to prompting the user.","Normalize whitespace-only reasons by rejecting and re-asking the model rather than passing them through."],"exampleFix":"// before\nAutoDecision(decision=\"deny\", reason=\"\")  # ValueError\n// after\nreason = raw.get(\"reason\") or \"No reason provided by classifier\"\nAutoDecision(decision=\"deny\", reason=reason)","handlingStrategy":"validation","validationCode":"def _safe_decision(raw: dict) -> AutoDecision | None:\n    if raw.get(\"decision\") == \"deny\" and not str(raw.get(\"reason\", \"\")).strip():\n        logger.warning(\"deny without reason: %r\", raw)\n        return None  # retry classification or escalate to the user\n    return AutoDecision(**raw)","typeGuard":"def _deny_has_reason(raw: object) -> TypeGuard[dict[str, str]]:\n    return (\n        isinstance(raw, dict)\n        and (raw.get(\"decision\") != \"deny\" or bool(str(raw.get(\"reason\", \"\")).strip()))\n    )","tryCatchPattern":"try:\n    decision = AutoDecision(**raw)\nexcept pydantic.ValidationError as exc:\n    if any(\"deny decisions require a reason\" in e[\"msg\"] for e in exc.errors()):\n        decision = reclassify_with_reason_required(raw)\n    else:\n        raise","preventionTips":["Instruct the classifier to always include a reason for denies; show a few-shot example.","Require `reason` in the JSON schema passed to the model for structured output.","Normalize blank reasons by retrying or substituting an explicit escalation to manual confirmation.","Review prompt changes that touch decision output for the reason requirement."],"tags":["pydantic","validation","llm-output","auto-mode"],"backgroundTag":"schema-validation-failed","analyzedSha":"a1af029e6e73cb17c36bff823d227747b28e91e1","analyzedAt":"2026-08-29T11:43:24.718Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}