{"record":{"id":"38d49846abdaf18d","repo":"langchain-ai/deepagents","slug":"human-decision-count-does-not-match-manual-pending","errorCode":null,"errorMessage":"Human decision count does not match Manual pending calls","messagePattern":"Human decision count does not match Manual pending calls","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"libs/code/deepagents_code/auto_mode.py","lineNumber":1736,"sourceCode":"        return f\"untrusted-{id(runtime):x}:{_batch_id(calls)}\"\n    return f\"{thread_key}:{_batch_id(calls)}\"\n\n\ndef _validate_human_decision_count(\n    decisions: Sequence[object], calls: Sequence[ToolCall], *, manual: bool\n) -> None:\n    \"\"\"Reject incomplete human responses before applying their decisions.\n\n    Raises:\n        ValueError: If the response has the wrong number of decisions.\n    \"\"\"\n    if len(decisions) == len(calls):\n        return\n    if manual:\n        msg = \"Human decision count does not match Manual pending calls\"\n    else:\n        msg = \"Human decision count does not match pending approval calls\"\n    raise ValueError(msg)\n\n\ndef _resolved_tools(request: ModelRequest) -> dict[str, BaseTool]:\n    return {\n        tool.name: tool\n        for tool in request.tools\n        if isinstance(tool, BaseTool) and isinstance(tool.name, str)\n    }\n\n\ndef _resolve_path(root: Path, raw: object) -> Path | None:\n    \"\"\"Return the absolute path a model-authored path argument names.\n\n    The argument is untrusted model output, so expansion is part of what can\n    fail: `Path.expanduser` raises `RuntimeError` for a `~name` prefix that\n    names no account on this host. Expansion runs inside the guard for that\n    reason, and every failure yields `None`.\n","sourceCodeStart":1718,"sourceCodeEnd":1754,"githubUrl":"https://github.com/langchain-ai/deepagents/blob/a1af029e6e73cb17c36bff823d227747b28e91e1/libs/code/deepagents_code/auto_mode.py#L1718-L1754","documentation":"Raised by `_validate_human_decision_count` inside `_human_review` when the number of human decisions supplied for a pending approval or Manual batch does not equal the number of pending tool calls in that batch. Auto mode requires a one-to-one decision per pending call to construct valid `ToolMessage` results; the sibling message 'does not match pending approval calls' is used when not in Manual mode.","triggerScenarios":"Resuming an interrupted turn with a decision list that is shorter or longer than the pending calls — e.g. submitting answers from a stale UI snapshot after the model proposed a different number of calls, replaying a saved receipt against a changed batch, or a frontend dropping decisions for some calls.","commonSituations":"UI state drift: user approves in one window while the agent re-proposed a different batch; resuming a checkpoint with a hardcoded decisions array; Manual-mode flows where the operator supplies decisions for only some calls; serialization bugs in custom approval frontends.","solutions":["Re-fetch the current pending calls and resubmit exactly one decision per call in batch order","Discard stale decision payloads saved from a previous turn and capture fresh decisions against the current batch","Check the approval frontend for UI-state races that let users submit before the batch renders fully","If resuming from a checkpoint, regenerate the decision list from the checkpoint's pending calls rather than reusing old data"],"exampleFix":"// before: 1 decision for 2 pending calls\ndecisions = [{\"type\": \"approve\"}]\n\n// after: one decision per pending call\ndecisions = [{\"type\": \"approve\"}, {\"type\": \"reject\", \"reason\": \"not needed\"}]","handlingStrategy":"validation","validationCode":"def decisions_match_pending(decisions: list, pending_calls: list) -> bool:\n    return len(decisions) == len(pending_calls)","typeGuard":null,"tryCatchPattern":"try:\n    resumed = agent.invoke(Command(resume=decisions), config)\nexcept ValueError as exc:\n    if \"decision count does not match\" in str(exc):\n        decisions = fetch_fresh_pending_and_decide_again()\n        resumed = agent.invoke(Command(resume=decisions), config)\n    else:\n        raise","preventionTips":["Always derive decisions from the current pending-calls list, never cached state","Disable or debounce approval UIs until the batch has fully rendered","On resume, rebuild the decision array from the checkpoint's pending calls","Add a length assertion in approval frontends before submitting"],"tags":["auto-mode","human-in-the-loop","validation","approval"],"backgroundTag":"decision-count-mismatch","analyzedSha":"a1af029e6e73cb17c36bff823d227747b28e91e1","analyzedAt":"2026-08-29T11:43:24.718Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}