fix(agents): malformed tool-call JSON no longer kills the rollout#480
Conversation
b02daeb to
dbe4b66
Compare
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.
dbe4b66 to
fb9e937
Compare
|
Verified end-to-end against qwen via our gateway with forced wire truncation: no crash, replay accepted, call re-issued |
|
Preview deployment for your docs. Learn more about Mintlify Previews.
|
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.
763dd9c to
795b745
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ 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.
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.
|
Docs PR opened: #482 Replaced generic one-word titles on the two v6 reference pages (agents and types) with descriptive titles under 60 characters. |

A completion cut off mid-arguments raised
JSONDecodeErrorfrom the barejson.loadsinget_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.MCPToolCall.argumentswidens todict | str | None: astris the raw provider string that didn't parse as JSON and is never executed._dispatch_callanswers such calls with anisErrorresult 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.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_reasonis 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_truncatedderives 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 onMCPToolCall, 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_reasonis now a first-class field (done,max_steps,length,timeout,malformed_tool_call) withis_truncatedderived from it; token-cap finish reasons are normalized across providers. Timeouts and trace exit reporting usestop_reasondirectly rather thantrace.extra.Reviewed by Cursor Bugbot for commit 3f8c6d7. Bugbot is set up for automated code reviews on this repo. Configure here.