infiniflow/ragflow · error · TypeError

Tool arguments for {name} must be an object, got {type(argum

Error message

Tool arguments for {name} must be an object, got {type(arguments).__name__}

What it means

Raised by LLMToolPluginCallSession.tool_call_async when the `arguments` value dispatched for a tool call is not a Mapping (dict-like). LLMs sometimes emit a JSON string, array, or null for arguments; the session explicitly type-checks before splatting (`**arguments`) or forwarding to MCP bindings, and raises TypeError with the offending Python type name.

Source

Thrown at agent/tools/base.py:62

    displayName: str
    description: str
    displayDescription: str
    parameters: dict[str, ToolParameter]


class LLMToolPluginCallSession(ToolCallSession):
    def __init__(self, tools_map: dict[str, object], callback: partial):
        self.tools_map = tools_map
        self.callback = callback

    def tool_call(self, name: str, arguments: dict[str, Any], timeout: float | int = 10) -> Any:
        return asyncio.run(self.tool_call_async(name, arguments, request_timeout=timeout))

    async def tool_call_async(self, name: str, arguments: dict[str, Any], request_timeout: float | int = 10) -> Any:
        assert name in self.tools_map, f"LLM tool {name} does not exist"
        logging.info(f"[ToolCall] invoke name={name} arguments={str(arguments)[:200]}")
        if not isinstance(arguments, Mapping):
            raise TypeError(f"Tool arguments for {name} must be an object, got {type(arguments).__name__}")
        st = timer()
        tool_obj = self.tools_map[name]
        if isinstance(tool_obj, MCPToolBinding):
            resp = await thread_pool_exec(tool_obj.session.tool_call, tool_obj.original_name, arguments, request_timeout)
        elif isinstance(tool_obj, MCPToolCallSession):
            resp = await thread_pool_exec(tool_obj.tool_call, name, arguments, request_timeout)
        elif hasattr(tool_obj, "invoke_async") and asyncio.iscoroutinefunction(tool_obj.invoke_async):
            resp = await tool_obj.invoke_async(**arguments)
        else:
            resp = await thread_pool_exec(tool_obj.invoke, **arguments)

        if resp is None and hasattr(tool_obj, "output") and callable(tool_obj.output):
            try:
                fallback_output = tool_obj.output()
                if isinstance(fallback_output, dict) and fallback_output.get("content") not in (None, ""):
                    resp = fallback_output["content"]
                elif fallback_output not in (None, ""):
                    resp = fallback_output

View on GitHub (pinned to 554fb1133a)

Solutions

  1. Parse/normalize arguments before dispatch: `if isinstance(arguments, str): arguments = json.loads(arguments)`.
  2. Reject non-object argument payloads at the LLM-response parsing layer and re-prompt the model with the schema error.
  3. Use the type guard below in custom tool-dispatch code that calls tool_call/tool_call_async.
  4. Upgrade or switch the model endpoint if it persistently emits stringified arguments.

Example fix

# before
resp = await session.tool_call_async(name, raw_args)  # raw_args is a JSON string -> TypeError

# after
args = json.loads(raw_args) if isinstance(raw_args, str) else raw_args
resp = await session.tool_call_async(name, args)
Defensive patterns

Strategy: type-guard

Validate before calling

import json
from collections.abc import Mapping

if isinstance(arguments, str):
    arguments = json.loads(arguments)  # may raise; handle parse errors at this boundary
if not isinstance(arguments, Mapping):
    raise ValueError(f"tool arguments must be an object, got {type(arguments).__name__}")
resp = await session.tool_call_async(name, dict(arguments))

Type guard

from collections.abc import Mapping

def is_tool_arguments_object(arguments) -> bool:
    """True when arguments is usable as **kwargs for a tool call."""
    return isinstance(arguments, Mapping)

Try / catch

try:
    resp = await session.tool_call_async(name, arguments)
except TypeError as e:
    if "must be an object" in str(e):
        arguments = json.loads(arguments) if isinstance(arguments, str) else {}
        resp = await session.tool_call_async(name, arguments)
    else:
        raise

Prevention

When it happens

Trigger: An LLM returns tool-call arguments as a raw JSON string (e.g. `'{"query": "x"}'`), a list, or None instead of a parsed object; or custom orchestration forwards unparsed JSON into session.tool_call(). Note the assert above it also requires the tool name to exist in tools_map.

Common situations: Models that stringify JSON arguments (common with some OpenAI-compatible/older models); frameworks that skip json.loads on the arguments field; function-calling payloads where arguments arrive as an array of positional values.

Related errors


AI-assisted analysis of infiniflow/ragflow@554fb1133a (2026-08-15). Data as JSON: /api/errors/9b435aa1c7aef657. Report an issue: GitHub.