fix(deepagents): keep surrogate pairs intact when chunking long lines - #632
Open
Anton Nak (antonnak) wants to merge 2 commits into
Open
Anton Nak (antonnak) wants to merge 2 commits into
Anton Nak (antonnak) wants to merge 2 commits into
Conversation
|
|
Anton Nak (@antonnak) is attempting to deploy a commit to the LangChain Team on Vercel. A member of the Team first needs to authorize it. |
deepagents-acp
deepagents
@langchain/sandbox-standard-tests
@langchain/daytona
@langchain/deno
@langchain/modal
@langchain/node-vfs
@langchain/quickjs
commit: |
Anton Nak (antonnak)
force-pushed
the
fix/surrogate-safe-line-chunking
branch
from
June 29, 2026 22:16
70dc452 to
1060977
Compare
`formatContentWithLineNumbers` chunks lines longer than `MAX_LINE_LENGTH` with `line.substring(start, end)` at fixed UTF-16 code-unit boundaries. When an astral-plane character (emoji, some CJK) straddles a boundary its surrogate pair is split: a lone high surrogate ends one chunk and a lone low surrogate begins the continuation line. `read_file` returns this text as tool output, so the lone surrogate reaches the model provider in the request body. Anthropic's strict JSON parser rejects it with "no low surrogate in string"; OpenAI tolerates it, so it only surfaces on Anthropic models (and hides behind a cross-provider fallback). Add `surrogateSafeCutIndex(text, max)`, which backs a cut index off by one when it would fall directly after a high surrogate, and use it in: - `formatContentWithLineNumbers`: a running cursor replaces fixed strides, so a whole surrogate pair moves to the next chunk instead of splitting (lossless — the character is preserved, just on the continuation line). - `truncateIfTooLong`: the string cut no longer splits a pair. Tests assert the formatted/truncated output is well-formed UTF-16 by checking that `encodeURIComponent` does not throw on it (it throws on a lone surrogate, exactly as Anthropic's request-body parser does). Refs langchain-ai#631 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Anton Nak (antonnak)
force-pushed
the
fix/surrogate-safe-line-chunking
branch
from
June 29, 2026 23:18
1060977 to
3c6be65
Compare
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.
Problem
formatContentWithLineNumberschunks lines longer thanMAX_LINE_LENGTHwithline.substring(start, end)at fixed UTF-16 code-unit boundaries. When an astral-plane character (emoji, some CJK) straddles a boundary, its surrogate pair is split: a lone high surrogate ends one chunk and a lone low surrogate begins the continuation line.read_filereturns this formatted text as tool output, so the lone surrogate reaches the model provider in the request body. Anthropic's strict JSON parser rejects it:OpenAI accepts the same content, so the bug only surfaces on Anthropic models and silently "works" behind a cross-provider fallback. The same unsafe slicing exists in
truncateIfTooLong.Fixes #631.
Fix
Add
surrogateSafeCutIndex(text, max): when a cut would fall directly after a high surrogate (U+D800-U+DBFF), back it off by one so the whole pair stays on one side. Used in:formatContentWithLineNumbers— a running cursor replaces fixedMAX_LINE_LENGTHstrides, so a straddling pair moves whole into the continuation chunk. Lossless: the character is preserved, just rendered on the next continuation line. Output is unchanged for content with no astral character at a chunk boundary (the back-off only triggers when the code unit before the cut is a high surrogate).truncateIfTooLong— the string cut no longer splits a pair.Tests
Three regression tests (
utils.test.ts) assert the output is well-formed UTF-16 by checking thatencodeURIComponentdoes not throw on it — it throws on a lone surrogate, the same way Anthropic's request-body parser rejects one. Without the fix they fail withURIError: URI malformed; with it, all 91utils.test.tstests pass.(
encodeURIComponentrather thanString.prototype.isWellFormed()because the repo targetslib: ["ES2023"], which has no ES2024 typings.)Scope / follow-ups
#631 also notes the same code-unit slicing in the
read_fileeviction truncation,compactToolResults, andtruncateArgs. Those can reusesurrogateSafeCutIndex(it would need to be exported). I kept this PR to the twoutils.tsfunctions for reviewability — happy to extend it to cover the rest in this PR if you'd prefer.