Repository navigation
Python: [Bug]: Function call arguments that are not a JSON object reach the tool as {"raw": ...} instead of failing validation #9562
Description
Observed Behavior
When the model returns function call arguments that are not a JSON object, Content.parse_arguments reports no error. It wraps what it got in a dictionary with one key, raw: text that is not valid JSON becomes {"raw": "<the text>"}, and valid JSON that is not an object, such as a list or a string, becomes {"raw": <the value>}.
The automatic function-calling loop parses with that method and then runs its top-level checks on the result. For a tool declared with a JSON schema that has no required properties and does not set additionalProperties: false, the checks pass, because raw is an extra key and extra keys are allowed. The function is then invoked with raw=... as if the model had made a well-formed call. The function receives an argument it never declared, the model receives whatever the function made of it, and nothing tells the model that its arguments were malformed.
In our case the tools are proxied to MCP servers, so a malformed call reaches the server carrying a bogus raw argument. A missing required argument or a wrong type is answered with a validation error the model can correct on; malformed arguments are not.
Expected Behavior
Arguments that are not a JSON object are invalid arguments. The loop should answer them the way it answers a missing required property: an error result that names the cause, for example "arguments are not a JSON object", so the model can retry. If the raw wrapping stays for direct callers of parse_arguments, the loop should still not treat its result as valid arguments.
Steps to Reproduce
- Build a
FunctionToolwith the schema{"type": "object", "properties": {}}. - Parse a function call whose
argumentsstring isnot {json,[1, 2]or"text". - Invoke the tool with the parsed arguments, as the loop does.
- The function runs with
{"raw": "not {json"}.
Minimal Reproduction
import asyncio
from typing import Any
from agent_framework import Content, FunctionTool
for text in ["not {json", "[1, 2]", '"text"']:
call = Content.from_function_call("call-1", "list_accounts", arguments=text)
print(f"{text!r:12} -> {call.parse_arguments()!r}")
async def list_accounts(**arguments: Any) -> str:
print("tool called with", arguments)
return "ok"
tool = FunctionTool(
name="list_accounts",
description="List the accounts",
func=list_accounts,
input_model={"type": "object", "properties": {}},
)
parsed = Content.from_function_call("call-1", "list_accounts", arguments="not {json").parse_arguments()
asyncio.run(tool.invoke(arguments=parsed))Output on agent-framework-core 1.22.0:
'not {json' -> {'raw': 'not {json'}
'[1, 2]' -> {'raw': [1, 2]}
'"text"' -> {'raw': 'text'}
tool called with {'raw': 'not {json'}
Where this happens on main at f7c6928:
parse_argumentsreturns therawdictionary for a value that is not an object and for a decode error:agent-framework/python/packages/core/agent_framework/_types.py
Lines 2188 to 2196 in f7c6928
if isinstance(self.arguments, str): # If arguments are a string, try to parse it as JSON try: loaded = json.loads(self.arguments) if isinstance(loaded, dict): return loaded # type: ignore[return-value] return {"raw": loaded} except (json.JSONDecodeError, TypeError): return {"raw": self.arguments} - The loop parses with it:
parsed_args: dict[str, Any] = dict(function_call_content.parse_arguments() or {}) - The top-level checks look at
required,additionalProperties: false,enumand the property types only, so an undeclaredrawkey passes unless the schema forbids extra keys:agent-framework/python/packages/core/agent_framework/_tools.py
Lines 1619 to 1631 in f7c6928
"""Run lightweight, top-level argument checks for schema-supplied tools.""" parsed_arguments = dict(arguments) required_fields = [field for field in schema.get("required", []) if isinstance(field, str)] missing_fields = [field for field in required_fields if field not in parsed_arguments] if missing_fields: raise TypeError(f"Missing required argument(s) for '{tool_name}': {', '.join(sorted(missing_fields))}") properties: Mapping[str, Any] = schema.get("properties", {}) if schema.get("additionalProperties") is False: unexpected_fields = sorted(field for field in parsed_arguments if field not in properties) if unexpected_fields: raise TypeError(f"Unexpected argument(s) for '{tool_name}': {', '.join(unexpected_fields)}")
Error Messages and Stack Traces
None. That is the problem: the malformed call is reported nowhere.
Package Versions
agent-framework-core: 1.22.0 (the code is the same on main at f7c6928)
Python Version
Python 3.12
Operating System
macOS
Metadata
Metadata
Labels
Type
Projects
- StatusShow more project fieldsNo status
Activity
github-actions commentedon Oct 20, 2026
🤖 Automated triage reproduction notes (agent-authored — trust but verify)
Agent analysis
Repro:
python/packages/core/agent_framework/_tools.py::_auto_invoke_functionat line 2242 acceptsContent.parse_arguments()fallback mappings as ordinary arguments. Invalid JSON or valid non-object JSON triggers this when the tool has a permissive object schema, causing its callable to receive a syntheticrawkeyword. Minimal repro: create thatFunctionToolwith a**kwargscallable and await_auto_invoke_functionon argumentsnot {json; the callable receives{"raw": "not {json"}.python/packages/core/tests/core/test_tools.py1.22.01.22.0Madin-H23 commentedon Oct 21, 2026
Fixed in PR #9573.
The cause is where the report pointed: the loop used
Content.parse_arguments()as the arguments of the call, and that method wraps a payload which is not a JSON object under a single"raw"key. For a tool whose schema has no required properties and does not setadditionalProperties: false, the loop's own checks then passed —rawis just an extra key — so the tool was invoked withraw=...as if the model had made a well-formed call.The pull request parses the payload strictly in the loop and answers a call whose arguments are not a JSON object the way it already answers a missing required property: with an argument-validation error result that names the cause, so the model can retry.
Two notes on the shape of the fix:
Content.parse_arguments()keeps its documented"raw"wrapping, so direct callers are unaffected; the check lives in the loop because that is the only place where the result becomes a tool invocation._FunctionArgumentValidationErrorpath, so a malformed call produces the same kind of result as a missing required property, including the redacted message when argument snapshots are in play.The reproduction from the report is in the pull request as a test, for all three shapes (
not {json,[1, 2]and"text"), plus a case that a well-formed call still reaches the tool with its arguments unchanged. On the unmodified file the tool is invoked with{'raw': 'not {json'}and with{'raw': [1, 2]}; with the change the tool is not invoked at all and the error names the cause.Thanks for the report — the
rawfallback and the loop's use of it were the whole bug.danielquintas8 commentedon Oct 21, 2026
H23 (@Madin-H23) Thanks for working on this. Has a maintainer confirmed the proposed direction? The contribution guide requires agreement before implementing non-trivial changes, or an explanation in the PR if the change qualifies as trivial. I could not find either here. Since this changes argument validation and whether tools execute, could we get maintainer confirmation?
Madin-H23 commentedon Oct 22, 2026
Thanks for the pointer — you are right, and we are not treating it as settled.
To answer directly: no maintainer has confirmed the direction yet. The pull request reuses this issue (the first step the contribution guide asks for), and the triage bot has labelled the issue
reproduced, but that is bot output, not maintainer agreement. The change also alters how the automatic function-calling loop validates arguments and whether a tool executes, so we do not think it qualifies as a trivial correction, and we are not claiming that it does.What we are doing now:
ready-for-implementationlabel on this issue, so that the agreed direction is explicit before the pull request goes through review. SergeyMenshykh — could you add the label, or point us at who should decide it?rawargument — which we are fixing together with a regression test. So this is a request for the direction to be agreed, not a request to merge now.One piece of context so you can see the whole picture: the same applies to our other two open pull requests in this repository — #9571 for #9549 and #9574 for #9438. They are in the same state: an existing issue, the triage bot's
reproducedlabel, and no maintainer agreement yet. (For #9549 the reporter also wrote in the issue that they would wait for explicit maintainer agreement before starting implementation, so asking is the only step we should have taken first.)If you read either change as trivial in the guide's sense — an obvious correction of a wrong result rather than a behaviour decision — say so and we will put that reasoning in the pull request description instead of asking for the label. Either way we will not keep a pull request open here without one of those two on record.
Madin-H23 commentedon Oct 22, 2026
Eduard van Valkenburg (@eavanvalkenburg) — one request about the label.
Per the contribution guide, a non-trivial change needs a maintainer to mark the direction as ready for implementation before the pull request is opened. We did not wait for that here, and we are asking to correct it: if the direction in this issue is welcome for implementation, could you add the
ready-for-implementationlabel? We asked SergeyMenshykh on 2026-10-22 as well, and the label history in this repository shows it comes from you.The pull request changes only how a function call whose arguments are not a JSON object is answered. The tool is no longer called with an undeclared
rawargument; the model gets an error that names the reason and can retry from it. The publicContent.parse_arguments()keeps its current wrapping, so direct callers are unaffected.#9573
Either answer is fine. If there is no confirmation by 2026-10-27 we will close the pull request ourselves, as we said on this issue. We will not ask again.