Skip to content

fix(agents): malformed tool-call JSON no longer kills the rollout#480

Merged
jdchawla29 merged 3 commits into
mainfrom
fix/malformed-toolcall-args
Jul 7, 2026
Merged

fix(agents): malformed tool-call JSON no longer kills the rollout#480
jdchawla29 merged 3 commits into
mainfrom
fix/malformed-toolcall-args

Conversation

@mariobrajkovski

@mariobrajkovski mariobrajkovski commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

A completion cut off mid-arguments raised JSONDecodeError from the bare json.loads in get_response, erroring the whole trace (Unterminated string starting at: line 1 column 13). Truncated arguments happen for reasons the client cannot distinguish — output-token caps, or a provider finalizing a stalled generation into a 200 — so the SDK must survive them.

  • Parse failures ride on the call. MCPToolCall.arguments widens to dict | str | None: a str is the raw provider string that didn't parse as JSON and is never executed. _dispatch_call answers such calls with an isError result so every call id gets a reply and the model can re-issue — the default behavior. The recorded chat history replays "{}" for those calls (backends that re-parse history 500 on broken argument strings), while the untouched response stays on the recorded step for debugging.
  • Stop conditions are config. AgentConfig.stop_on: set[StopCondition] (default empty). Training configs set {"length", "malformed_tool_call"} to end the rollout cleanly at the offending turn — nothing dispatched, no errored trace, truncation zeroes out through normal grading.
  • Trace.stop_reason is a typed field (done | max_steps | length | timeout | malformed_tool_call) written by the loop, with token-cap finish reasons normalized across provider vocabularies (length/max_tokens/MAX_TOKENS); Trace.is_truncated derives from it, and it reports on trace exit as a first-class key.

Applied to both the chat.completions and Responses parse sites. Tests exercise both policy modes through the loop lifecycle, dispatch, history recording, and stop_reason.


Note

Medium Risk
Changes core agent loop dispatch, parsing, and trace metadata used for grading and platform reporting; behavior shifts from hard failures to recoverable errors with optional early stop.

Overview
Truncated or invalid tool-call JSON no longer crashes the agent loop. OpenAI Responses and Chat Completions parsers catch JSONDecodeError, keep the raw argument string on MCPToolCall, and the tool loop answers those calls with an error result so the model can retry. Chat history replays "{}" for broken arguments so re-parsed backends stay valid.

AgentConfig.stop_on (length, malformed_tool_call) can end the rollout at that turn instead of dispatching tools—intended for training configs. Trace.stop_reason is now a first-class field (done, max_steps, length, timeout, malformed_tool_call) with is_truncated derived from it; token-cap finish reasons are normalized across providers. Timeouts and trace exit reporting use stop_reason directly rather than trace.extra.

Reviewed by Cursor Bugbot for commit 3f8c6d7. Bugbot is set up for automated code reviews on this repo. Configure here.

@mariobrajkovski mariobrajkovski force-pushed the fix/malformed-toolcall-args branch 2 times, most recently from b02daeb to dbe4b66 Compare July 6, 2026 15:37
A completion truncated at the output-token limit mid-arguments (e.g. Qwen
emitting '{"command": "' and stopping) raised JSONDecodeError from the bare
json.loads in get_response, which escaped the agent loop and errored the whole
trace with 'Unterminated string starting at: line 1 column 13'.

Parse failures now produce the tool call with a sentinel argument
(MALFORMED_TOOL_ARGS_KEY); the dispatcher returns an isError tool result for
that call id without executing anything, so the provider contract holds and
the model can re-issue the call. The recorded assistant message replays "{}"
instead of the raw malformed string — some backends (Qwen/vLLM) re-parse
arguments when templating history and 500 on the raw string (verified live).
Applied to both the chat.completions and Responses parse sites.

Verified end-to-end against Qwen/Qwen3.5-397B-A17B via the HUD gateway with
forced wire truncation: no crash, replay accepted, call re-issued with
complete JSON.
@mariobrajkovski mariobrajkovski force-pushed the fix/malformed-toolcall-args branch from dbe4b66 to fb9e937 Compare July 6, 2026 15:39
@mariobrajkovski

Copy link
Copy Markdown
Contributor Author

Verified end-to-end against qwen via our gateway with forced wire truncation: no crash, replay accepted, call re-issued

@mintlify

mintlify Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
hud 🟢 Ready View Preview Jul 7, 2026, 2:59 AM

A completion cut mid-arguments (provider stall or token cap) raised
JSONDecodeError out of get_response, erroring the whole trace and losing
the fatal response. Parse failures now leave the raw provider string on
MCPToolCall.arguments (widened to dict | str | None): dispatch answers
such calls with an error result so the model can re-issue, the recorded
chat history replays "{}" so backends that re-parse history don't 500,
and the offending response is preserved on the recorded step.

AgentConfig.stop_on ({"length", "malformed_tool_call"}) instead ends the
rollout at the offending turn, for training rollouts that must not
continue past truncation. The loop records why a rollout ended on the
typed Trace.stop_reason, Trace.is_truncated derives from it, and the
trace-exit report sends it as a first-class key.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 795b745. Configure here.

Comment thread hud/agents/openai/agent.py
The Responses API has no finish_reason; a cap surfaces as
incomplete_details.reason. Map it onto AgentStep.finish_reason and add
"max_output_tokens" to the truncation vocabulary so stop_reason="length"
and stop_on={"length"} work for OpenAIAgent, not just the chat agents.
@jdchawla29 jdchawla29 merged commit 17300e8 into main Jul 7, 2026
10 checks passed
@mintlify

mintlify Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Docs PR opened: #482

Replaced generic one-word titles on the two v6 reference pages (agents and types) with descriptive titles under 60 characters.

@jdchawla29 jdchawla29 deleted the fix/malformed-toolcall-args branch July 7, 2026 22:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants