langchain-ai/deepagents · error · BrokenPipeError

Hook process pipes are unavailable

Error message

Hook process pipes are unavailable

What it means

`_communicate_bounded` writes the hook payload to the subprocess and reads bounded stdout/stderr over asyncio pipes. If any of the process's stdin/stdout/stderr streams is None, it raises `BrokenPipeError` — the subprocess was created without the piped streams the hook runner requires, so payload exchange is impossible.

Source

Thrown at libs/code/deepagents_code/hooks/runner.py:221

    completion = asyncio.gather(task, return_exceptions=True)
    try:
        await asyncio.wait_for(
            asyncio.shield(completion),
            timeout=_TERMINATE_WAIT_TIMEOUT,
        )
    except TimeoutError:
        task.cancel()
        await completion


async def _communicate_bounded(
    process: Process,
    payload: bytes,
    limit: int,
) -> tuple[bytes, bytes, bool, bool]:
    if process.stdin is None or process.stdout is None or process.stderr is None:
        msg = "Hook process pipes are unavailable"
        raise BrokenPipeError(msg)
    stdout_task = asyncio.create_task(_read_bounded(process.stdout, limit))
    stderr_task = asyncio.create_task(_read_bounded(process.stderr, limit))
    stdin_task = asyncio.create_task(_write_input(process, payload))
    try:
        await process.wait()
        stdout, stderr = await asyncio.gather(stdout_task, stderr_task)
        with contextlib.suppress(BrokenPipeError, ConnectionResetError):
            await stdin_task
    except BaseException:
        stdin_task.cancel()
        stdout_task.cancel()
        stderr_task.cancel()
        await asyncio.gather(
            stdin_task,
            stdout_task,
            stderr_task,
            return_exceptions=True,
        )

View on GitHub (pinned to a1af029e6e)

Solutions

  1. Create the hook process with `asyncio.create_subprocess_exec(..., stdin=PIPE, stdout=PIPE, stderr=PIPE)` so all three pipes exist
  2. Use the library's own process-creation path in `run_command_handler` rather than supplying an externally created Process
  3. If passing a Process across code paths, assert its stdin/stdout/stderr are non-None before communicating

Example fix

// before
proc = await asyncio.create_subprocess_exec(*argv, stdout=DEVNULL)
// after
proc = await asyncio.create_subprocess_exec(*argv, stdin=PIPE, stdout=PIPE, stderr=PIPE)
Defensive patterns

Strategy: try-catch

Validate before calling

if proc.stdin is None or proc.stdout is None or proc.stderr is None:
    raise BrokenPipeError("hook process must be created with stdin/stdout/stderr=PIPE")

Type guard

def has_pipes(p: asyncio.subprocess.Process) -> bool:
    return p.stdin is not None and p.stdout is not None and p.stderr is not None

Try / catch

try:
    stdout, stderr, _, _ = await _communicate_bounded(proc, payload, limit)
except BrokenPipeError as e:
    logger.error("hook subprocess pipes unavailable: %s", e)
    proc.kill()

Prevention

When it happens

Trigger: `run_command_handler` spawned the hook `Process` without `stdin=PIPE`, `stdout=PIPE`, `stderr=PIPE` (or the streams were closed/None), then called `_communicate_bounded` on it.

Common situations: Custom/forked runner code creating the subprocess with inherited or devnull streams; process created by a different code path and passed in; platform/asyncio edge cases where stream objects are None before/after process exit.

Related errors


AI-assisted analysis of langchain-ai/deepagents@a1af029e6e (2026-08-29). Data as JSON: /api/errors/6e264b7c0b9d97d3. Report an issue: GitHub.