{"record":{"id":"84b5650897bc3c9e","repo":"langchain-ai/deepagents","slug":"human-decision-count-does-not-match-pending-approv","errorCode":null,"errorMessage":"Human decision count does not match pending approval calls","messagePattern":"Human decision count does not match pending approval 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":"Auto Mode's human-in-the-loop review requires the model/user to return exactly one decision for each tool call that is pending approval. This ValueError is raised in `_validate_human_decision_count` (called from `_human_review`) when `len(decisions) != len(calls)`, i.e. decisions were missing, duplicated, or extra. It guards the invariant that each pending approval call is resolved exactly once before execution proceeds.","triggerScenarios":"Calling `_human_review` with a decisions list whose length differs from the pending approval calls list — e.g. a human reviewer skipped an item, a UI submitted only approvals and omitted rejections, or duplicate decisions were included for one call id.","commonSituations":"Building a custom HITL front-end that submits partial decision sets; programmatically resuming an interrupted graph with a hand-built decisions array; a classifier or reviewer returning fewer/more entries than the batch of pending calls after a prompt or schema change.","solutions":["Count the pending approval calls and ensure the decisions list has exactly one decision per call, in any order but with matching tool_call_ids.","Deduplicate decisions for the same tool_call_id and add entries for any call that has no decision (default to reject if unsure).","If resuming from an interrupt payload, pass the decisions straight from the user's responses rather than re-deriving them."],"exampleFix":"// before\nrespond({ decisions: [ {tool_call_id: 'call_1', decision: 'approve'} ] })  // 2 calls pending\n// after\nrespond({ decisions: [ {tool_call_id: 'call_1', decision: 'approve'}, {tool_call_id: 'call_2', decision: 'reject'} ] })","handlingStrategy":"validation","validationCode":"pending_ids = {c['id'] for c in pending_calls}\ndec_ids = [d['tool_call_id'] for d in decisions]\nassert len(dec_ids) == len(set(dec_ids)), 'duplicate decisions'\nassert set(dec_ids) == pending_ids, f'decisions {set(dec_ids)} != pending {pending_ids}'","typeGuard":"def has_decision_per_call(decisions: list[dict], calls: list[dict]) -> bool:\n    ids = [d.get('tool_call_id') for d in decisions]\n    return len(ids) == len(set(ids)) == len(calls) and set(ids) == {c['id'] for c in calls}","tryCatchPattern":"try:\n    result = human_review(calls, decisions)\nexcept ValueError as e:\n    if 'decision count does not match' in str(e):\n        # re-prompt the reviewer for the missing/duplicated calls\n        decisions = re_prompt_missing(calls, decisions)\n        result = human_review(calls, decisions)\n    else:\n        raise","preventionTips":["Always derive decisions from the interrupt payload's pending calls rather than rebuilding them from memory.","After collecting decisions, assert id-set equality with pending calls before submitting.","Default undecided calls to 'reject' so a partial UI submission never produces a mismatch.","Deduplicate by tool_call_id before submitting."],"tags":["validation","human-in-the-loop","auto-mode"],"backgroundTag":"decision-count-mismatch","analyzedSha":"a1af029e6e73cb17c36bff823d227747b28e91e1","analyzedAt":"2026-08-29T11:43:24.718Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}