ClaudeWrapper.Runner behaviour (ClaudeWrapper v0.15.2)

Copy Markdown View Source

How claude subprocesses are executed.

Two shapes cover the non-duplex paths: a bounded one-shot run/4 and an NDJSON stream_lines/4. The default implementation is ClaudeWrapper.Runner.Port, the System.cmd/Port code the library has always used.

For leak-free execution -- where a timeout, a halted stream, or BEAM death kills the whole claude process group (the CLI and every stdio MCP server it spawned) rather than abandoning it -- add forcola to your deps and select its runner:

# mix.exs
{:forcola, ">= 0.4.0 and < 0.7.0"}

# config/config.exs
config :claude_wrapper, runner: ClaudeWrapper.Runner.Forcola

Runner.Forcola only compiles when forcola is present, so the dependency stays optional. See the "Error handling" and leak-free sections of the README, and #185.

run_observed/5 is optional. It runs to transport completion while calling a trusted, nonblocking line observer with stdout lines only. The observer returns :continue, :observed to stop observing, or {:continue, next_observer} to replace itself with an observer carrying updated state. The runner must not call it again after :observed. It must execute synchronously in the process calling run_observed/5, before that function returns. This is an internal execution seam, not an API for caller-supplied callbacks.

Observed output is {stdout, exit_code, stderr} with newline-normalized stdout. Stderr stays separate and must never be passed to the observer. Forcola captures stderr on nonzero completion; clean completion returns an empty stderr field. The same whole-run deadline and cleanup as run/4 apply. Unsupported runners are rejected by Query.execute/3 before spawn.

Contract

run/4 returns System.cmd/3's {stdout, exit_code} on completion (a non-zero exit is not an error -- callers decide what an exit code means), {:error, :timeout} when the timeout elapsed, and other {:error, reason} tuples for spawn/io failures. stream_lines/4 returns a lazy Enumerable of complete stdout lines (no trailing newline); the caller parses each line.

Summary

Types

Runner error reasons. :timeout is common to both runners.

A trusted stdout line observer constructed by Query.

Execution options, a subset of System.cmd/3's

Functions

The configured runner module, ClaudeWrapper.Runner.Port by default.

Types

error()

@type error() :: :timeout | {:signal, term()} | {:spawn, term()} | {:io, term()}

Runner error reasons. :timeout is common to both runners.

line_observer()

@type line_observer() :: (String.t() ->
                      :continue | :observed | {:continue, line_observer()})

A trusted stdout line observer constructed by Query.

opts()

@type opts() :: keyword()

Execution options, a subset of System.cmd/3's:

  • :cd -- working directory (string) or nil
  • :env -- list of {name, value} string tuples
  • :stderr_to_stdout -- merge stderr into stdout (default false)

Callbacks

run(binary, args, opts, timeout)

@callback run(
  binary :: String.t(),
  args :: [String.t()],
  opts :: opts(),
  timeout :: timeout() | nil
) ::
  {:ok, {String.t(), non_neg_integer()}} | {:error, error()}

run_observed(binary, args, opts, timeout, observer)

(optional)
@callback run_observed(
  binary :: String.t(),
  args :: [String.t()],
  opts :: opts(),
  timeout :: timeout() | nil,
  observer :: line_observer()
) :: {:ok, {String.t(), non_neg_integer(), String.t()}} | {:error, error()}

stream_lines(binary, args, opts, timeout)

@callback stream_lines(
  binary :: String.t(),
  args :: [String.t()],
  opts :: opts(),
  timeout :: timeout() | nil
) :: Enumerable.t()

Functions

impl()

@spec impl() :: module()

The configured runner module, ClaudeWrapper.Runner.Port by default.

Set with config :claude_wrapper, runner: ClaudeWrapper.Runner.Forcola.