Skip to content

Commit 4b9cfc7

Browse files
authored
Make CLI buffer limit configurable (#190)
1 parent d86c47f commit 4b9cfc7

3 files changed

Lines changed: 42 additions & 7 deletions

File tree

src/claude_agent_sdk/_internal/transport/subprocess_cli.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424

2525
logger = logging.getLogger(__name__)
2626

27-
_MAX_BUFFER_SIZE = 1024 * 1024 # 1MB buffer limit
27+
_DEFAULT_MAX_BUFFER_SIZE = 1024 * 1024 # 1MB buffer limit
2828

2929

3030
class SubprocessCLITransport(Transport):
@@ -48,6 +48,11 @@ def __init__(
4848
self._stderr_task_group: anyio.abc.TaskGroup | None = None
4949
self._ready = False
5050
self._exit_error: Exception | None = None # Track process exit errors
51+
self._max_buffer_size = (
52+
options.max_buffer_size
53+
if options.max_buffer_size is not None
54+
else _DEFAULT_MAX_BUFFER_SIZE
55+
)
5156

5257
def _find_cli(self) -> str:
5358
"""Find Claude Code CLI binary."""
@@ -402,12 +407,13 @@ async def _read_messages_impl(self) -> AsyncIterator[dict[str, Any]]:
402407
# Keep accumulating partial JSON until we can parse it
403408
json_buffer += json_line
404409

405-
if len(json_buffer) > _MAX_BUFFER_SIZE:
410+
if len(json_buffer) > self._max_buffer_size:
411+
buffer_length = len(json_buffer)
406412
json_buffer = ""
407413
raise SDKJSONDecodeError(
408-
f"JSON message exceeded maximum buffer size of {_MAX_BUFFER_SIZE} bytes",
414+
f"JSON message exceeded maximum buffer size of {self._max_buffer_size} bytes",
409415
ValueError(
410-
f"Buffer size {len(json_buffer)} exceeds limit {_MAX_BUFFER_SIZE}"
416+
f"Buffer size {buffer_length} exceeds limit {self._max_buffer_size}"
411417
),
412418
)
413419

@@ -418,7 +424,7 @@ async def _read_messages_impl(self) -> AsyncIterator[dict[str, Any]]:
418424
except json.JSONDecodeError:
419425
# We are speculatively decoding the buffer until we get
420426
# a full JSON object. If there is an actual issue, we
421-
# raise an error after _MAX_BUFFER_SIZE.
427+
# raise an error after exceeding the configured limit.
422428
continue
423429

424430
except anyio.ClosedResourceError:

src/claude_agent_sdk/types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,7 @@ class ClaudeAgentOptions:
320320
extra_args: dict[str, str | None] = field(
321321
default_factory=dict
322322
) # Pass arbitrary CLI flags
323+
max_buffer_size: int | None = None # Max bytes when buffering CLI stdout
323324
debug_stderr: Any = (
324325
sys.stderr
325326
) # Deprecated: File-like object for debug output. Use stderr callback instead.

tests/test_subprocess_buffering.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
from claude_agent_sdk._errors import CLIJSONDecodeError
1212
from claude_agent_sdk._internal.transport.subprocess_cli import (
13-
_MAX_BUFFER_SIZE,
13+
_DEFAULT_MAX_BUFFER_SIZE,
1414
SubprocessCLITransport,
1515
)
1616
from claude_agent_sdk.types import ClaudeAgentOptions
@@ -237,7 +237,7 @@ def test_buffer_size_exceeded(self) -> None:
237237
"""Test that exceeding buffer size raises an appropriate error."""
238238

239239
async def _test() -> None:
240-
huge_incomplete = '{"data": "' + "x" * (_MAX_BUFFER_SIZE + 1000)
240+
huge_incomplete = '{"data": "' + "x" * (_DEFAULT_MAX_BUFFER_SIZE + 1000)
241241

242242
transport = SubprocessCLITransport(
243243
prompt="test", options=ClaudeAgentOptions(), cli_path="/usr/bin/claude"
@@ -260,6 +260,34 @@ async def _test() -> None:
260260

261261
anyio.run(_test)
262262

263+
def test_buffer_size_option(self) -> None:
264+
"""Test that the configurable buffer size option is respected."""
265+
266+
async def _test() -> None:
267+
custom_limit = 512
268+
huge_incomplete = '{"data": "' + "x" * (custom_limit + 10)
269+
270+
transport = SubprocessCLITransport(
271+
prompt="test",
272+
options=ClaudeAgentOptions(max_buffer_size=custom_limit),
273+
cli_path="/usr/bin/claude",
274+
)
275+
276+
mock_process = MagicMock()
277+
mock_process.returncode = None
278+
mock_process.wait = AsyncMock(return_value=None)
279+
transport._process = mock_process
280+
transport._stdout_stream = MockTextReceiveStream([huge_incomplete])
281+
transport._stderr_stream = MockTextReceiveStream([])
282+
283+
with pytest.raises(CLIJSONDecodeError) as exc_info:
284+
async for _ in transport.read_messages():
285+
pass
286+
287+
assert f"maximum buffer size of {custom_limit} bytes" in str(exc_info.value)
288+
289+
anyio.run(_test)
290+
263291
def test_mixed_complete_and_split_json(self) -> None:
264292
"""Test handling a mix of complete and split JSON messages."""
265293

0 commit comments

Comments
 (0)