Behaviour for a native CLI harness: an external coding-agent CLI (Claude Code, Cursor, ...) that owns its own agent loop and tool dispatch.
This is the "vendor owns the loop" half of the meta-harness. A backend built on
a native harness reports handles_tools_internally?() == true, so the
framework's ReAct loop does NOT drive tool calls; instead the CLI runs its own
loop and Raxol's tools are injected into it over MCP (see
Raxol.Agent.Harness.McpToolConfig).
A harness driver is a small, mostly-pure module that knows three things about its CLI:
executable/0-- the binary name to look up onPATH.args/1-- the argv (after the executable) to spawn one non-interactive run.parse_line/1-- turn one line of the CLI's stdout into normalized events.
The generic Port runtime (Raxol.Agent.Backend.Native) handles spawning,
line framing, streaming, and exit/timeout. Drivers stay protocol-only and are
trivially unit-testable.
Normalized events
parse_line/1 returns a (possibly empty) list of these tuples, in order:
{:text, binary}-- assistant text to surface to the user.{:reasoning, binary}-- chain-of-thought / thinking text.{:tool_call, map}-- the CLI invoked a tool (observability only; the tool itself is served by the injected MCP server, not by the framework).{:done, %{content: binary, usage: map}}-- the run finished;contentis the final answer,usageis token accounting (may be empty).{:error, term}-- the CLI reported an error.
Summary
Callbacks
The argv (after the executable) for one non-interactive run.
The CLI binary name, looked up on PATH.
Whether this harness exposes Raxol tools to the CLI over MCP.
Human-readable harness name.
Parse one stdout line into zero or more normalized events.
Functions
Whether the CLI for driver is available on this machine.
Query injects_mcp_tools?/0, defaulting to true.
Types
@type run_config() :: %{ optional(:prompt) => binary(), optional(:model) => binary() | nil, optional(:system_prompt) => binary() | nil, optional(:mcp_config_path) => Path.t() | nil, optional(:cwd) => Path.t() | nil, optional(:extra_args) => [binary()] }
Run configuration passed to args/1. Keys:
:prompt-- the user prompt for this run.:model-- model id, ornilfor the CLI default.:system_prompt-- optional system/append prompt.:mcp_config_path-- path to the MCP config file injecting Raxol tools, ornilwhen no tools are exposed.:cwd-- working directory.:extra_args-- raw argv appended verbatim.
Callbacks
@callback args(run_config()) :: [String.t()]
The argv (after the executable) for one non-interactive run.
@callback executable() :: String.t()
The CLI binary name, looked up on PATH.
@callback injects_mcp_tools?() :: boolean()
Whether this harness exposes Raxol tools to the CLI over MCP.
When true (the default for tool-using harnesses), the runtime builds an MCP
config from the agent's Actions and passes its path in run_config.mcp_config_path.
@callback name() :: String.t()
Human-readable harness name.
Parse one stdout line into zero or more normalized events.