Skip to content

fix(code): stream tool-call args in linear time - #5712

Merged
Mason Daugherty (mdrxy) merged 4 commits into
mainfrom
mdrxy/code/linear-tool-arg-stream
Aug 20, 2026
Merged

fix(code): stream tool-call args in linear time#5712
Mason Daugherty (mdrxy) merged 4 commits into
mainfrom
mdrxy/code/linear-tool-arg-stream

Conversation

@mdrxy

@mdrxy Mason Daugherty (mdrxy) commented Aug 20, 2026

Copy link
Copy Markdown
Member

Large streamed tool calls no longer repeatedly copy their full argument payload while rendering.

ToolCallBuffer scanned 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 the json.loads until 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_args per fragment:

fragments container bare scalar
2000 0.68 ms 5.98 ms
4000 1.36 ms 19.22 ms
8000 2.58 ms 68.34 ms

Latch lifetime

parse_args short-circuits on warned, 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_state clears it with the rest of the per-payload state. Otherwise a malformed payload strands the next one in the same buffer:

ingest '{bad json}'   -> None          warned=True
ingest {'whole': 1}   -> {'whole': 1}  (whole-value chunk resets fragment state)
ingest '{"good": 1}'  -> None          <- valid payload, tool.use dropped

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:

before: Tool-call args look complete but failed to parse: '{"a":1}} {"b":"x\n}'
after:  Tool-call args are unparseable and cannot be completed: '{"a":1}} {"b":"x\n}'

%r keeps control characters in model output escaped, and the preview is capped by INVALID_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.loads would accept, so only the guard can produce the skip), incremental over-closing, the preview cap, and the latch reset.

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
@github-actions github-actions Bot added dcode Related to `deepagents-code` fix A bug fix (PATCH) internal User is a member of the `langchain-ai` GitHub organization size: M 200-499 LOC labels Aug 20, 2026
@mdrxy
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.
@github-actions github-actions Bot added size: L 500-999 LOC and removed size: M 200-499 LOC labels Aug 20, 2026
@mdrxy
Mason Daugherty (mdrxy) merged commit 57bfa6b into main Aug 20, 2026
57 checks passed
@mdrxy
Mason Daugherty (mdrxy) deleted the mdrxy/code/linear-tool-arg-stream branch August 20, 2026 20:55
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dcode Related to `deepagents-code` fix A bug fix (PATCH) internal User is a member of the `langchain-ai` GitHub organization size: L 500-999 LOC

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant