From 1c90605d6a6e905dd8189d5a171517e7259eb28f Mon Sep 17 00:00:00 2001 From: CryptoKylan Date: Tue, 18 Aug 2026 03:48:23 +0200 Subject: [PATCH] fix: name max_buffer_size in the buffer-overflow error message When a CLI stdout message exceeds `max_buffer_size` the reader raises fatally and the session ends, but the error never mentions the option that fixes it. Users land on "modify the installed package" (as #416 described) instead of a one-line options change. Requested by @tmaxwell-anthropic on #98. #416 carried this alongside a default increase and was withdrawn, so the uncontroversial half was never landed. This PR does not change the default. Appending the guidance to the guard's message does not work: CLIJSONDecodeError truncates its `line` argument to 100 chars, so anything folded into it is cut off. Instead `hint` is an optional keyword appended after the truncated line, where it always survives. It is opt-in, so every existing call site and message is byte-identical. --- src/claude_agent_sdk/_errors.py | 10 ++++++++-- .../_internal/transport/subprocess_cli.py | 5 +++++ tests/test_errors.py | 10 ++++++++++ tests/test_subprocess_buffering.py | 3 +++ 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/claude_agent_sdk/_errors.py b/src/claude_agent_sdk/_errors.py index 5f3eb9c70..dbd69779b 100644 --- a/src/claude_agent_sdk/_errors.py +++ b/src/claude_agent_sdk/_errors.py @@ -120,10 +120,16 @@ def __init__( class CLIJSONDecodeError(ClaudeSDKError): """Raised when unable to decode JSON from CLI output.""" - def __init__(self, line: str, original_error: Exception): + def __init__(self, line: str, original_error: Exception, hint: str | None = None): self.line = line self.original_error = original_error - super().__init__(f"Failed to decode JSON: {line[:100]}...") + self.hint = hint + message = f"Failed to decode JSON: {line[:100]}..." + # Appended AFTER the truncated line so actionable guidance survives: + # `line` is cut at 100 chars, so anything folded into it can be lost. + if hint: + message = f"{message} {hint}" + super().__init__(message) class MessageParseError(ClaudeSDKError): diff --git a/src/claude_agent_sdk/_internal/transport/subprocess_cli.py b/src/claude_agent_sdk/_internal/transport/subprocess_cli.py index 58abc438d..fe0611f0f 100644 --- a/src/claude_agent_sdk/_internal/transport/subprocess_cli.py +++ b/src/claude_agent_sdk/_internal/transport/subprocess_cli.py @@ -1095,6 +1095,11 @@ def guard(length: int) -> None: ValueError( f"Buffer size {length} exceeds limit {self._max_buffer_size}" ), + hint=( + "Large tool results (file reads, images, MCP responses) " + "can exceed the default; raise it with " + "ClaudeAgentOptions(max_buffer_size=...)." + ), ) try: diff --git a/tests/test_errors.py b/tests/test_errors.py index 693def109..753b6ee9c 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -110,3 +110,13 @@ def test_json_decode_error(self): assert error.line == "{invalid json}" assert error.original_error == e assert "Failed to decode JSON" in str(error) + assert error.hint is None + + def test_json_decode_error_hint_survives_line_truncation(self): + """The hint must outlive the 100-char truncation of `line`.""" + error = CLIJSONDecodeError("x" * 500, ValueError("boom"), hint="Do the thing.") + assert error.hint == "Do the thing." + assert str(error).endswith("Do the thing.") + # The line is still truncated; the hint is what stays readable. + assert "x" * 100 in str(error) + assert "x" * 101 not in str(error) diff --git a/tests/test_subprocess_buffering.py b/tests/test_subprocess_buffering.py index daf79dc53..3091053af 100644 --- a/tests/test_subprocess_buffering.py +++ b/tests/test_subprocess_buffering.py @@ -281,6 +281,9 @@ async def _test() -> None: pass assert f"maximum buffer size of {custom_limit} bytes" in str(exc_info.value) + # The error must name the option that fixes it, so users reach for + # max_buffer_size instead of editing the installed package. + assert "max_buffer_size" in str(exc_info.value) anyio.run(_test)