diff --git a/src/claude_agent_sdk/_internal/message_parser.py b/src/claude_agent_sdk/_internal/message_parser.py index f91081c3..e1defa66 100644 --- a/src/claude_agent_sdk/_internal/message_parser.py +++ b/src/claude_agent_sdk/_internal/message_parser.py @@ -157,6 +157,7 @@ def parse_message(data: dict[str, Any]) -> Message: usage=data.get("usage"), result=data.get("result"), structured_output=data.get("structured_output"), + errors=data.get("errors"), ) except KeyError as e: raise MessageParseError( diff --git a/src/claude_agent_sdk/_internal/transport/subprocess_cli.py b/src/claude_agent_sdk/_internal/transport/subprocess_cli.py index 1f0aac58..b89c18c0 100644 --- a/src/claude_agent_sdk/_internal/transport/subprocess_cli.py +++ b/src/claude_agent_sdk/_internal/transport/subprocess_cli.py @@ -59,6 +59,7 @@ def __init__( if options.max_buffer_size is not None else _DEFAULT_MAX_BUFFER_SIZE ) + self._stderr_lines: list[str] = [] self._write_lock: anyio.Lock = anyio.Lock() def _find_cli(self) -> str: @@ -357,11 +358,10 @@ async def connect(self) -> None: if self._cwd: process_env["PWD"] = self._cwd - # Pipe stderr if we have a callback OR debug mode is enabled - should_pipe_stderr = ( - self._options.stderr is not None - or "debug-to-stderr" in self._options.extra_args - ) + # Always pipe stderr so we can capture it for error reporting. + # The callback and debug mode flags control whether lines are + # forwarded in real-time, but we always collect them. + should_pipe_stderr = True # For backward compat: use debug_stderr file object if no callback and debug is on stderr_dest = PIPE if should_pipe_stderr else None @@ -420,6 +420,9 @@ async def _handle_stderr(self) -> None: if not line_str: continue + # Always collect stderr lines for error reporting + self._stderr_lines.append(line_str) + # Call the stderr callback if provided if self._options.stderr: self._options.stderr(line_str) @@ -569,18 +572,23 @@ async def _read_messages_impl(self) -> AsyncIterator[dict[str, Any]]: # Client disconnected pass - # Check process completion and handle errors - try: - returncode = await self._process.wait() - except Exception: - returncode = -1 + # Check process exit code (non-blocking if already exited) + returncode = self._process.returncode + if returncode is None: + try: + returncode = await self._process.wait() + except Exception: + returncode = -1 # Use exit code for error detection if returncode is not None and returncode != 0: + # Give stderr reader a moment to drain remaining output + await anyio.sleep(0.1) + stderr_output = "\n".join(self._stderr_lines) if self._stderr_lines else None self._exit_error = ProcessError( f"Command failed with exit code {returncode}", exit_code=returncode, - stderr="Check stderr output for details", + stderr=stderr_output, ) raise self._exit_error diff --git a/src/claude_agent_sdk/types.py b/src/claude_agent_sdk/types.py index 3ea89d5a..8c6ccd5b 100644 --- a/src/claude_agent_sdk/types.py +++ b/src/claude_agent_sdk/types.py @@ -681,6 +681,7 @@ class ResultMessage: usage: dict[str, Any] | None = None result: str | None = None structured_output: Any = None + errors: list[str] | None = None @dataclass