langchain-ai/deepagents · error · ValueError
Human decision count does not match pending approval calls
Error message
Human decision count does not match pending approval calls
What it means
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.
Source
Thrown at libs/code/deepagents_code/auto_mode.py:1736
return f"untrusted-{id(runtime):x}:{_batch_id(calls)}"
return f"{thread_key}:{_batch_id(calls)}"
def _validate_human_decision_count(
decisions: Sequence[object], calls: Sequence[ToolCall], *, manual: bool
) -> None:
"""Reject incomplete human responses before applying their decisions.
Raises:
ValueError: If the response has the wrong number of decisions.
"""
if len(decisions) == len(calls):
return
if manual:
msg = "Human decision count does not match Manual pending calls"
else:
msg = "Human decision count does not match pending approval calls"
raise ValueError(msg)
def _resolved_tools(request: ModelRequest) -> dict[str, BaseTool]:
return {
tool.name: tool
for tool in request.tools
if isinstance(tool, BaseTool) and isinstance(tool.name, str)
}
def _resolve_path(root: Path, raw: object) -> Path | None:
"""Return the absolute path a model-authored path argument names.
The argument is untrusted model output, so expansion is part of what can
fail: `Path.expanduser` raises `RuntimeError` for a `~name` prefix that
names no account on this host. Expansion runs inside the guard for that
reason, and every failure yields `None`.
View on GitHub (pinned to a1af029e6e)
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.
Example fix
// before
respond({ decisions: [ {tool_call_id: 'call_1', decision: 'approve'} ] }) // 2 calls pending
// after
respond({ decisions: [ {tool_call_id: 'call_1', decision: 'approve'}, {tool_call_id: 'call_2', decision: 'reject'} ] }) Defensive patterns
Strategy: validation
Validate before calling
pending_ids = {c['id'] for c in pending_calls}
dec_ids = [d['tool_call_id'] for d in decisions]
assert len(dec_ids) == len(set(dec_ids)), 'duplicate decisions'
assert set(dec_ids) == pending_ids, f'decisions {set(dec_ids)} != pending {pending_ids}' Type guard
def has_decision_per_call(decisions: list[dict], calls: list[dict]) -> bool:
ids = [d.get('tool_call_id') for d in decisions]
return len(ids) == len(set(ids)) == len(calls) and set(ids) == {c['id'] for c in calls} Try / catch
try:
result = human_review(calls, decisions)
except ValueError as e:
if 'decision count does not match' in str(e):
# re-prompt the reviewer for the missing/duplicated calls
decisions = re_prompt_missing(calls, decisions)
result = human_review(calls, decisions)
else:
raise Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Human decision count does not match Manual pending calls
- tool_call_id must not be empty
- deny decisions require a reason
- trusted thread, turn, and tool-call identity are required
- Auto mode requires every proposed tool call to have an ID
AI-assisted analysis of langchain-ai/deepagents@a1af029e6e (2026-08-29).
Data as JSON: /api/errors/84b5650897bc3c9e.
Report an issue: GitHub.