agent-harness 0.5.2

Drive LLM coding agents — Claude Code, OpenAI Codex, and local Ollama / OpenAI-compatible models — from Rust behind one trait, with a normalized streaming event vocabulary. Bring your own agent too.
Documentation

agent-harness

Use popular coding agents from your Rust code.

Claude Code, OpenAI Codex, OpenCode, and any OpenAI-compatible model. Call them from your program instead of opening a terminal. You do not shell out, and you do not parse each agent's own output format. Every agent returns the same event stream, so adding an agent means writing a parser into RunEvent — not teaching your UI another format.

Imported as harness.

cargo add agent-harness

Highlights

  • No terminal. Run an agent from your code and read its output as typed events, not scraped text.
  • Every agent looks the same. Text, reasoning, tool calls, plans, token usage, and lifecycle all arrive as RunEvent.
  • Swap agents without rewriting. Change the constructor. Your loop stays.
  • A built-in agent for open models. The openai-compatible feature is not a wrapper. It speaks the chat API and runs the tool loop in Rust.
  • Add your own agent. Implement Harness in your own crate and register it. No fork.
  • Stable wire format. RunEvent serializes to camelCase JSON, so HTTP, SSE, and IPC transports all emit the same shape.

Features

The CLI adapters are on by default. Everything else is opt-in, so the core crate stays small.

# Claude Code and Codex
agent-harness = "0.4"

# Add ACP agents and the built-in agent for open models
agent-harness = { version = "0.4", features = ["acp", "openai-compatible"] }

# Or take just one adapter
agent-harness = { version = "0.4", default-features = false, features = ["claude"] }
feature what it adds
claude, codex the CLI adapters (default)
acp any Agent Client Protocol agent — OpenCode, Gemini, Goose
openai-compatible the built-in agent for open models. Pulls an HTTP client and search libraries.
models-dev live model lists from the models.dev catalog

Getting started

Build an agent. Call run. Read events.

(run hands back a receiver to loop over. start takes a callback instead — reach for it when you are forwarding events straight onto a socket and an intermediate hop would be waste. An adapter implements only start; every harness gets run for free.)

use harness::{Claude, Harness, RunEvent, RunRequest};

let (_handle, events) = Claude::new().run(RunRequest {
    run_id: "demo".into(),
    prompt: "In one sentence, what is a Markdown heading?".into(),
    ..Default::default()
})?;

for event in events {
    match event {
        RunEvent::Text { delta, .. } => print!("{delta}"),
        RunEvent::Exited { .. } => break,
        _ => {}
    }
}

Name what you mean; the rest defaults — no working directory, RunMode::Ask (answer only; Edit lets it write files), no resumed session, no attachments.

Keep _handle if you want to stop the run. Dropping it does not cancel.

To use Codex instead, change one line:

let (_handle, events) = harness::Codex::new().run(request)?;

Open models

The CLI adapters wrap an external agent. An open model has no agent to wrap, so the openai-compatible feature ships one. It calls the chat API and runs the tool loop in Rust: read, glob, grep, list, write, edit, bash, webfetch, websearch, todowrite, question, and apply_patch. It also handles sessions, skills, subagents, and MCP servers.

let ollama = harness::OpenHarness::ollama();
let (_handle, events) = ollama.run(request)?;

A provider is configuration, not code. Point the same type at any OpenAI-compatible endpoint:

use harness::{OpenHarness, OpenHarnessConfig};

let openrouter = OpenHarness::custom(OpenHarnessConfig {
    id: "openrouter".into(),
    display_name: "OpenRouter".into(),
    base_url: "https://openrouter.ai/api".into(),
    api_key_env: Some("OPENROUTER_API_KEY".into()),
    ..Default::default()
});

Ollama is the one exception. It uses its native /api/* endpoints, so the context window is set correctly and local models can be pulled and deleted. Use ollama_at(url) when your server is not on the default port.

Setup and sign-in

readiness() reports what is on the machine: installed, auth_configured, and a version.

This crate does not install agents. When one is missing, info().install_hint says where to get it.

if !harness.readiness().installed {
    if let Some(hint) = harness.info().install_hint {
        println!("Get it from {}", hint.url);
        if let Some(command) = hint.command {
            println!("  {command}");
        }
    }
}

login() runs an agent's own sign-in flow, which opens a browser. In CI, set the agent's API key in the environment instead. Then readiness() reports ready.

Bring your own agent

Harness only emits RunEvents. Your implementation can spawn a CLI or call an HTTP API. Register it next to the built-ins:

let registry = harness::Registry::new()
    .register(harness::Claude::new())
    .register(MyAgent::new());

What the trait gives you

  • Harnessinfo, readiness, run, credential, login, list_models. Object-safe, so Box<dyn Harness> works.
  • RunEvent — text, thinking, tool start and end, plan, usage with cache tokens, suggested edits, questions, and lifecycle. It follows the Agent Client Protocol vocabulary.
  • Registry — an open set. Built-ins and your own agents sit together.

The subprocess engine is a separate crate: cli-stream.

Examples

One per harness. Run with cargo run --example <name>.

example harness features
claude Claude Code default
codex OpenAI Codex default
acp OpenCode over ACP acp
gemini Gemini CLI over ACP acp
ollama a local model on Ollama openai-compatible
openrouter a hosted model on OpenRouter openai-compatible models-dev
llama_cpp a local llama-server openai-compatible
tool_call the agent loop calling tools and reading the results openai-compatible
setup readiness, install hints, and sign-in default
custom_harness your own agent default
playground a browser UI that streams a live run over SSE openai-compatible acp

claude and codex differ by one line. ollama, openrouter, and llama_cpp differ only in configuration — no provider has its own adapter.

License

MIT or Apache-2.0, at your option.