{"record":{"id":"9b435aa1c7aef657","repo":"infiniflow/ragflow","slug":"tool-arguments-for-name-must-be-an-object-got","errorCode":null,"errorMessage":"Tool arguments for {name} must be an object, got {type(arguments).__name__}","messagePattern":"Tool arguments for (.+?) must be an object, got (.+?)","errorType":"exception","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"agent/tools/base.py","lineNumber":62,"sourceCode":"    displayName: str\n    description: str\n    displayDescription: str\n    parameters: dict[str, ToolParameter]\n\n\nclass LLMToolPluginCallSession(ToolCallSession):\n    def __init__(self, tools_map: dict[str, object], callback: partial):\n        self.tools_map = tools_map\n        self.callback = callback\n\n    def tool_call(self, name: str, arguments: dict[str, Any], timeout: float | int = 10) -> Any:\n        return asyncio.run(self.tool_call_async(name, arguments, request_timeout=timeout))\n\n    async def tool_call_async(self, name: str, arguments: dict[str, Any], request_timeout: float | int = 10) -> Any:\n        assert name in self.tools_map, f\"LLM tool {name} does not exist\"\n        logging.info(f\"[ToolCall] invoke name={name} arguments={str(arguments)[:200]}\")\n        if not isinstance(arguments, Mapping):\n            raise TypeError(f\"Tool arguments for {name} must be an object, got {type(arguments).__name__}\")\n        st = timer()\n        tool_obj = self.tools_map[name]\n        if isinstance(tool_obj, MCPToolBinding):\n            resp = await thread_pool_exec(tool_obj.session.tool_call, tool_obj.original_name, arguments, request_timeout)\n        elif isinstance(tool_obj, MCPToolCallSession):\n            resp = await thread_pool_exec(tool_obj.tool_call, name, arguments, request_timeout)\n        elif hasattr(tool_obj, \"invoke_async\") and asyncio.iscoroutinefunction(tool_obj.invoke_async):\n            resp = await tool_obj.invoke_async(**arguments)\n        else:\n            resp = await thread_pool_exec(tool_obj.invoke, **arguments)\n\n        if resp is None and hasattr(tool_obj, \"output\") and callable(tool_obj.output):\n            try:\n                fallback_output = tool_obj.output()\n                if isinstance(fallback_output, dict) and fallback_output.get(\"content\") not in (None, \"\"):\n                    resp = fallback_output[\"content\"]\n                elif fallback_output not in (None, \"\"):\n                    resp = fallback_output","sourceCodeStart":44,"sourceCodeEnd":80,"githubUrl":"https://github.com/infiniflow/ragflow/blob/554fb1133ac3861732235ad9c377eb5e0a770665/agent/tools/base.py#L44-L80","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Parse/normalize arguments before dispatch: `if isinstance(arguments, str): arguments = json.loads(arguments)`.","Reject non-object argument payloads at the LLM-response parsing layer and re-prompt the model with the schema error.","Use the type guard below in custom tool-dispatch code that calls tool_call/tool_call_async.","Upgrade or switch the model endpoint if it persistently emits stringified arguments."],"exampleFix":"# before\nresp = await session.tool_call_async(name, raw_args)  # raw_args is a JSON string -> TypeError\n\n# after\nargs = json.loads(raw_args) if isinstance(raw_args, str) else raw_args\nresp = await session.tool_call_async(name, args)","handlingStrategy":"type-guard","validationCode":"import json\nfrom collections.abc import Mapping\n\nif isinstance(arguments, str):\n    arguments = json.loads(arguments)  # may raise; handle parse errors at this boundary\nif not isinstance(arguments, Mapping):\n    raise ValueError(f\"tool arguments must be an object, got {type(arguments).__name__}\")\nresp = await session.tool_call_async(name, dict(arguments))","typeGuard":"from collections.abc import Mapping\n\ndef is_tool_arguments_object(arguments) -> bool:\n    \"\"\"True when arguments is usable as **kwargs for a tool call.\"\"\"\n    return isinstance(arguments, Mapping)","tryCatchPattern":"try:\n    resp = await session.tool_call_async(name, arguments)\nexcept TypeError as e:\n    if \"must be an object\" in str(e):\n        arguments = json.loads(arguments) if isinstance(arguments, str) else {}\n        resp = await session.tool_call_async(name, arguments)\n    else:\n        raise","preventionTips":["json.loads string arguments once, at the LLM-response parsing layer, before dispatch.","Assert Mapping (not dict) — typed dicts, MappingProxyType and pydantic models all pass Mapping but some fail dict checks.","Constrain tool schemas so models emit object arguments; reject array-form arguments with a corrective re-prompt."],"tags":["llm","tool-call","type-error","json","arguments"],"backgroundTag":null,"analyzedSha":"554fb1133ac3861732235ad9c377eb5e0a770665","analyzedAt":"2026-08-15T09:20:16.380Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}