From 5b1cded65d7acda268599fb434f93ae55a4f8822 Mon Sep 17 00:00:00 2001 From: BlueX888 <140241684+BlueX888@users.noreply.github.com> Date: Wed, 16 Sep 2026 14:35:27 +0800 Subject: [PATCH 1/2] fix(agents): tolerate unparsable tool call arguments `_handle_batch_response` parsed `tool_call.function.arguments` with a bare `json.loads`, so a tool call whose arguments string is empty or truncated raised `json.JSONDecodeError` out of `ChatAgent.step()` before any tool ran. OpenAI-compatible servers (vLLM, Ollama, Together, ...) send `arguments: ""` for zero-argument tools and can truncate arguments, so a parse failure is an expected input rather than an invariant violation. The streaming accumulators and `_utils.extract_tool_call` already catch `json.JSONDecodeError` for the same field. `ToolCallRequest.args` is typed `Dict[str, Any]`, so unlike the streaming accumulators the batch path cannot keep the raw string; fall back to `{}` and let the tool call proceed. --- camel/agents/chat_agent.py | 6 ++- test/agents/test_chat_agent.py | 70 ++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/camel/agents/chat_agent.py b/camel/agents/chat_agent.py index ca172d4d8d..29a95643fe 100644 --- a/camel/agents/chat_agent.py +++ b/camel/agents/chat_agent.py @@ -4036,7 +4036,11 @@ def _handle_batch_response( for tool_call in tool_calls: tool_name = tool_call.function.name # type: ignore[union-attr] tool_call_id = tool_call.id - args = json.loads(tool_call.function.arguments) # type: ignore[union-attr] + try: + args = json.loads(tool_call.function.arguments) # type: ignore[union-attr] + except json.JSONDecodeError: + # `arguments` may be empty or truncated on the wire. + args = {} extra_content = getattr(tool_call, 'extra_content', None) tool_call_request = ToolCallRequest( diff --git a/test/agents/test_chat_agent.py b/test/agents/test_chat_agent.py index da43a41611..e4061b0bac 100644 --- a/test/agents/test_chat_agent.py +++ b/test/agents/test_chat_agent.py @@ -44,6 +44,7 @@ BaseModelBackend, ModelFactory, OpenAIModel, + StubModel, ) from camel.terminators import ResponseWordsTerminator from camel.toolkits import ( @@ -2371,3 +2372,72 @@ def test_rate_limit_retry_respects_anthropic_error_when_installed(): from openai import RateLimitError as OpenAIRateLimitError assert _RATE_LIMIT_ERRORS == (OpenAIRateLimitError,) + + +@pytest.mark.parametrize( + "arguments", + ["", '{"city": "San Fra'], + ids=["empty", "truncated"], +) +def test_chat_agent_step_tolerates_unparsable_tool_arguments(arguments): + r"""A tool call whose `arguments` string is not valid JSON is executed. + + OpenAI-compatible servers (vLLM, Ollama, Together, ...) send + `arguments: ""` for zero-argument tools, and arguments can be truncated + on the wire. The streaming path tolerates both, so the non-streaming path + must not let a JSONDecodeError escape ChatAgent.step(). + """ + + def ping() -> str: + r"""A zero-argument tool.""" + return "pong" + + model_backend_rsp_tool = ChatCompletion( + id='mock_id_123456', + choices=[ + Choice( + finish_reason='tool_calls', + index=0, + logprobs=None, + message=ChatCompletionMessage( + content=None, + role='assistant', + tool_calls=[ + ChatCompletionMessageFunctionToolCall( + id='call_mock_123456', + function=Function( + arguments=arguments, + name='ping', + ), + type='function', + ), + ], + ), + ) + ], + created=1730752528, + model='stub', + object='chat.completion', + usage=CompletionUsage( + completion_tokens=5, + prompt_tokens=10, + total_tokens=15, + ), + ) + + model = StubModel(ModelType.STUB, model_config_dict={"stream": False}) + agent = ChatAgent( + system_message="You are a help assistant.", + model=model, + tools=[ping], + max_iteration=1, + ) + agent.model_backend.run = MagicMock(return_value=model_backend_rsp_tool) + + response = agent.step("Ping the tool.") + + tool_calls = response.info['tool_calls'] + assert len(tool_calls) == 1 + assert tool_calls[0].tool_name == 'ping' + assert tool_calls[0].args == {} + assert tool_calls[0].result == 'pong' From e97d75900162828094c98af8035ca53d64db6dc1 Mon Sep 17 00:00:00 2001 From: BlueX888 <140241684+BlueX888@users.noreply.github.com> Date: Thu, 17 Sep 2026 10:01:31 +0800 Subject: [PATCH 2/2] fix(agents): treat non-object tool call arguments as empty A payload that decodes to valid JSON that is not an object (`null`, `[]`, a bare scalar) clears the JSONDecodeError guard but cannot fill `ToolCallRequest.args`, so pydantic raises and the error still escapes `ChatAgent.step()`. Verified against the PR head: `null`, `[]`, `[1, 2]`, `123`, `true` and `"ping"` all raise ValidationError out of step(), while `_execute_tool_from_stream_data` handles every one of them without raising. Extends the regression test to cover both classes. --- camel/agents/chat_agent.py | 5 +++++ test/agents/test_chat_agent.py | 14 ++++++++------ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/camel/agents/chat_agent.py b/camel/agents/chat_agent.py index 29a95643fe..25e988998e 100644 --- a/camel/agents/chat_agent.py +++ b/camel/agents/chat_agent.py @@ -4041,6 +4041,11 @@ def _handle_batch_response( except json.JSONDecodeError: # `arguments` may be empty or truncated on the wire. args = {} + if not isinstance(args, dict): + # `arguments` may also be valid JSON that is not an + # object (`null`, `[]`, a bare scalar), which `args` + # cannot hold any more than a malformed payload. + args = {} extra_content = getattr(tool_call, 'extra_content', None) tool_call_request = ToolCallRequest( diff --git a/test/agents/test_chat_agent.py b/test/agents/test_chat_agent.py index e4061b0bac..638f865f56 100644 --- a/test/agents/test_chat_agent.py +++ b/test/agents/test_chat_agent.py @@ -2376,16 +2376,18 @@ def test_rate_limit_retry_respects_anthropic_error_when_installed(): @pytest.mark.parametrize( "arguments", - ["", '{"city": "San Fra'], - ids=["empty", "truncated"], + ["", '{"city": "San Fra', "null", "[]", "123", '"ping"'], + ids=["empty", "truncated", "null", "list", "number", "string"], ) -def test_chat_agent_step_tolerates_unparsable_tool_arguments(arguments): - r"""A tool call whose `arguments` string is not valid JSON is executed. +def test_chat_agent_step_tolerates_unusable_tool_arguments(arguments): + r"""A tool call whose `arguments` string cannot fill `args` is executed. OpenAI-compatible servers (vLLM, Ollama, Together, ...) send `arguments: ""` for zero-argument tools, and arguments can be truncated - on the wire. The streaming path tolerates both, so the non-streaming path - must not let a JSONDecodeError escape ChatAgent.step(). + on the wire. A payload may also be well-formed JSON that is not an + object (`null`, `[]`, a bare scalar), which `ToolCallRequest.args` cannot + hold either. The streaming path lets none of them escape, so the + non-streaming path must not either. """ def ping() -> str: