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

  1. Parse before canonicalizing: json.loads(raw_args) if isinstance(raw_args, str) — then pass the resulting dict.
  2. Use {} (not None or []) for no-argument tools.
  3. 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

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


AI-assisted analysis of NousResearch/hermes-agent@c896c09c42 (2026-08-14). Data as JSON: /api/errors/21d295f4a1d366e6. Report an issue: GitHub.