fix(code): stream tool-call args in linear time - #5712
Merged
Conversation
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Mason Daugherty (mdrxy)
marked this pull request as ready for review
August 20, 2026 18:16
`parse_args` short-circuits on `warned` to avoid re-running `json.loads` on a payload that can never parse. That makes the latch gate data, not only logging, so its lifetime must match the payload it blocks. `_reset_args_fragment_state` cleared the eleven lexer fields but not `warned`. A whole-value chunk resets the fragment state mid-buffer, so a malformed payload could strand the next one: a valid fragment stream returned `None` for the life of the buffer and dropped its `tool.use`. Also correct the warning text. The over-closed path skips the balance check, so the old "look complete but failed to parse" could describe a payload that ends mid-string, which sends a reader after a stream-reassembly bug that does not exist. Restore the rationale for the paths that stayed: the trailing-junk trade, the string-aware balance check, the `RecursionError` arm, and the deliberate non-warning of bare scalars. Name the preview cap as a constant, and record that the bare-scalar path still joins per chunk. Add coverage for the escape flag across a fragment boundary, the parse memo invalidating, the open-string term in the completeness gate, the depth guard at its boundary, incremental over-closing, the preview cap, and the latch reset.
Mason Daugherty (mdrxy)
pushed a commit
that referenced
this pull request
Aug 20, 2026
> [!CAUTION] > Merging this PR will automatically publish to **PyPI** and create a **GitHub release**. For the full release process, see [`.github/RELEASING.md`](https://github.com/langchain-ai/deepagents/blob/main/.github/RELEASING.md). --- _Release notes preview: keep this section in sync with the package `CHANGELOG.md`. Publish reads the merged CHANGELOG via `release.yml`, not this PR description — keep them aligned anyway so the PR stays an accurate historical record for reviewers and anyone returning later._ --- ## [0.1.59](deepagents-code==0.1.58...deepagents-code==0.1.59) (2026-08-20) ### Features - Added support for `managed_config.toml` configuration ([#5604](#5604)) - Multi-select `ask_user` answers are now encoded as JSON arrays ([#5660](#5660)) - Made teardown usage stats configurable ([#5696](#5696)) - Footer pickers now open on click ([#5674](#5674)) - Replaced Gemini 3.6 Flash with Gemini 3.7 Flash ([#5681](#5681)) ### Bug fixes - Made tool argument validation errors recoverable ([#5659](#5659)) - Improved streaming performance for tool-call arguments to run in linear time ([#5712](#5712)) - Fixed durable-mask config resolution with ranked resolver behavior ([#5672](#5672)) - Skipped background sync in Apple Terminal ([#5666](#5666)) - Hid thread IDs when tracing is disabled ([#5692](#5692)) - Kept installed providers visible in `/auth` ([#5689](#5689)) - Preloaded the auth UI before notification handoff ([#5697](#5697)) - Updated and clarified UI copy across Auto mode, YOLO hints, classifier notices, `/tokens`, line-number toggles, review failures, onboarding Tavily cancellation, and OpenAI subscription login labels ([#5685](#5685), [#5694](#5694), [#5684](#5684), [#5687](#5687), [#5680](#5680), [#5688](#5688), [#5686](#5686), [#5691](#5691), [#5693](#5693)) - Removed the `Muse Spark 1.1` recommendation ([#5683](#5683)) _End release notes preview._ --- > [!NOTE] > A **community contributors** list and a **Special thanks** section (crediting the users who filed the issues this release's PRs closed) are appended to the GitHub release notes automatically at publish time (see [Release Pipeline](https://github.com/langchain-ai/deepagents/blob/main/.github/RELEASING.md#release-pipeline), step 3). --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: langchain-oss-automated-triage[bot] <248757908+langchain-oss-automated-triage[bot]@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Large streamed tool calls no longer repeatedly copy their full argument payload while rendering.
ToolCallBufferscanned the whole accumulated prefix on every chunk to decide whether the payload was complete, so a long argument value cost O(n^2) over its stream. It now tracks JSON lexical state per fragment — first and last non-whitespace character, container depth and its running maximum, string and escape state, over-closing — and defers the join and thejson.loadsuntil that state says the value is structurally complete. The parsed result is memoized, and invalidated as soon as another fragment arrives.Each fragment is scanned once, so the container and string-scalar paths are linear over a stream. A bare non-string scalar (a number or literal) has no closing token to wait for and still re-joins per chunk; tool arguments are never that shape.
Measured cost of
ingest+parse_argsper fragment:Latch lifetime
parse_argsshort-circuits onwarned, since every condition that sets it is permanent: depth exceeded (the running maximum never falls), over-closed (a closer at depth 0 cannot be undone), or balanced-but-unparseable (the top-level value has already closed, so any suffix is trailing junk). That makes the latch gate data rather than only logging, so_reset_args_fragment_stateclears it with the rest of the per-payload state. Otherwise a malformed payload strands the next one in the same buffer:Warning text
The over-closed path skips the balance check, so a payload can be reported unparseable while it ends mid-string. The old wording described that as complete:
%rkeeps control characters in model output escaped, and the preview is capped byINVALID_ARGS_PREVIEW_LIMIT.Behavior
Verified unchanged against the previous implementation by differential fuzzing over randomized payloads and fragmentations, comparing the full per-fragment return sequence and the warning counts. Deliberate trade-offs are preserved and now documented at the lines that implement them: a bracketed value with trailing junk is treated as still-streaming, and a bare scalar that is complete but malformed is not warned about, being indistinguishable from a still-streaming fragment.
Coverage was added for the state that crosses fragment boundaries, which the previous single-pass helpers did not have: the escape flag straddling a boundary, the parse memo invalidating, the open-string term in the completeness gate, the depth guard at its boundary (nesting that
json.loadswould accept, so only the guard can produce the skip), incremental over-closing, the preview cap, and the latch reset.