Raxol.Agent.NativeHarness behaviour (Raxol Agent v2.6.0)

Copy Markdown View Source

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 on PATH.
  • 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; content is the final answer, usage is token accounting (may be empty).
  • {:error, term} -- the CLI reported an error.

Summary

Types

Run configuration passed to args/1. Keys

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

event()

@type event() ::
  {:text, binary()}
  | {:reasoning, binary()}
  | {:tool_call, map()}
  | {:done, %{content: binary(), usage: map()}}
  | {:error, term()}

run_config()

@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, or nil for the CLI default.
  • :system_prompt -- optional system/append prompt.
  • :mcp_config_path -- path to the MCP config file injecting Raxol tools, or nil when no tools are exposed.
  • :cwd -- working directory.
  • :extra_args -- raw argv appended verbatim.

Callbacks

args(run_config)

@callback args(run_config()) :: [String.t()]

The argv (after the executable) for one non-interactive run.

executable()

@callback executable() :: String.t()

The CLI binary name, looked up on PATH.

injects_mcp_tools?()

(optional)
@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.

name()

@callback name() :: String.t()

Human-readable harness name.

parse_line(line)

@callback parse_line(line :: String.t()) :: [event()]

Parse one stdout line into zero or more normalized events.

Functions

available?(driver)

@spec available?(module()) :: boolean()

Whether the CLI for driver is available on this machine.

injects_mcp_tools?(driver)

@spec injects_mcp_tools?(module()) :: boolean()

Query injects_mcp_tools?/0, defaulting to true.