NousResearch/hermes-agent · error · TypeError
tool args must be a mapping, got {type(args).__name__}
Error message
tool args must be a mapping, got {type(args).__name__} What it means
Raised by canonical_tool_args() in agent/tool_guardrails.py when its args parameter is not a Mapping (dict-like). The function builds a deterministic, sorted, compact JSON representation of tool arguments for signature/failure tracking; lists, strings, bytes, or None are not valid because tool arguments are named parameters.
Source
Thrown at agent/tool_guardrails.py:228
return self.action in {"block", "halt"}
def to_metadata(self) -> dict[str, Any]:
data: dict[str, Any] = {
"action": self.action,
"code": self.code,
"message": self.message,
"tool_name": self.tool_name,
"count": self.count,
}
if self.signature is not None:
data["signature"] = self.signature.to_metadata()
return data
def canonical_tool_args(args: Mapping[str, Any]) -> str:
"""Return sorted compact JSON for parsed tool arguments."""
if not isinstance(args, Mapping):
raise TypeError(f"tool args must be a mapping, got {type(args).__name__}")
return json.dumps(
args,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
default=str,
)
def classify_tool_failure(tool_name: str, result: str | None) -> tuple[bool, str]:
"""Safety-fallback classifier used only when callers don't pass ``failed``.
Mirrors ``agent.display._detect_tool_failure`` exactly so the guardrail
never disagrees with the CLI's user-visible ``[error]`` tag. Production
callers in ``run_agent.py`` always pass an explicit ``failed=`` derived
from ``_detect_tool_failure``; this function exists so standalone callers
(tests, tooling) still get consistent behavior.
"""View on GitHub (pinned to c896c09c42)
Solutions
- Parse before canonicalizing: json.loads(raw_args) if isinstance(raw_args, str) — then pass the resulting dict.
- Use {} (not None or []) for no-argument tools.
- Type-check at the boundary: raise a clear error if args is not a Mapping before calling library code.
Example fix
# before
canonical = canonical_tool_args(tool_call.function.arguments) # str from provider
# after
import json
raw = tool_call.function.arguments
args = json.loads(raw) if isinstance(raw, str) else (raw or {})
canonical = canonical_tool_args(args) Defensive patterns
Strategy: type-guard
Validate before calling
import json
from typing import Mapping
raw = tool_call.get("function", {}).get("arguments", {})
args = json.loads(raw) if isinstance(raw, str) else (raw if isinstance(raw, Mapping) else {}) Type guard
from typing import Mapping
def is_tool_args_mapping(value: object) -> bool:
return isinstance(value, Mapping) Try / catch
try:
canonical = canonical_tool_args(args)
except TypeError as exc:
if "tool args must be a mapping" in str(exc):
args = json.loads(args) if isinstance(args, str) else {}
canonical = canonical_tool_args(args)
else:
raise Prevention
- Always json.loads() provider argument strings before passing them on.
- Use {} for tools invoked with no arguments, never None or [].
- Pass the parsed .args dict, not the enclosing tool_call object.
When it happens
Trigger: Passing a JSON string of arguments (from an LLM tool_call before parsing) instead of the parsed dict; passing a positional list of args; passing None when a tool has no arguments (use {} instead); a caller passing the whole tool_call object rather than its .args field.
Common situations: Integrating raw provider payloads where function arguments arrive as a JSON string; forwarding args from a schema-less source; default-None spreads like canonical_tool_args(args or []) picking a list.
Related errors
- Expected exported session JSON or JSONL
- metadata must be JSON-serializable.
- Preview mode — launching is disabled.
- no install root
- Cannot resolve ${installScriptName()}: no SOURCE_REPO_ROOT a
AI-assisted analysis of NousResearch/hermes-agent@c896c09c42 (2026-08-14).
Data as JSON: /api/errors/21d295f4a1d366e6.
Report an issue: GitHub.