# 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`.
```sh
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.
```toml
# Claude Code and Codex
agent-harness = "0.5"
# Add ACP agents and the built-in agent for open models
agent-harness = { version = "0.5", features = ["acp", "openai-compatible"] }
# Or take just one adapter
agent-harness = { version = "0.5", default-features = false, features = ["claude"] }
```
| `claude`, `codex` | the CLI adapters (default) |
| `acp` | any [Agent Client Protocol](https://agentclientprotocol.com) 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](https://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.)
```rust
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:
```rust
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.
```rust
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:
```rust
use harness::{ApiKey, OpenHarness, OpenHarnessConfig};
let openrouter = OpenHarness::custom(OpenHarnessConfig {
id: "openrouter".into(),
display_name: "OpenRouter".into(),
base_url: "https://openrouter.ai/api".into(),
// One field, four states: no key wanted, needed and absent, a variable to
// read, or the secret itself. A value is never exported to the child's
// environment, where the agent's own `bash` tool could read it back.
api_key: ApiKey::Env("OPENROUTER_API_KEY".into()),
..Default::default()
});
```
Point it at a server on this machine and let the endpoint say what it serves,
rather than naming a model you have to keep in sync:
```rust
use harness::{OpenHarness, OpenHarnessConfig};
let lm_studio = OpenHarness::custom(OpenHarnessConfig {
id: "lm-studio".into(),
display_name: "LM Studio".into(),
base_url: "http://localhost:1234".into(),
..Default::default()
})
.with_openai_models();
```
`with_openai_models()` reads the endpoint's own `/v1/models` — the list every
OpenAI-compatible server is expected to serve — so `list_models()` returns what
is actually loaded. Any models declared in the config become the fallback for a
server that does not serve that route.
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.
```rust
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 `RunEvent`s. Your implementation can spawn a CLI or call
an HTTP API. Register it next to the built-ins:
```rust
let registry = harness::Registry::new()
.register(harness::Claude::new())
.register(MyAgent::new());
```
## What the trait gives you
- **`Harness`** — `info`, `features`, `readiness`, `run`, `credential`,
`login`, `list_models`. Object-safe, so `Box<dyn Harness>` works.
- **`Info` and `Features` are separate.** Identity is asked once; ability is
asked constantly, so capabilities live on `features()` rather than on the
struct carrying a name and a description. `features()` defaults to supporting
nothing, so a minimal adapter never mentions the type. `Registry::catalog()`
yields a `Listing` — both halves together, which is what a picker needs for
every row.
- **`RunEvent`** — text, thinking, tool start and end, plan, usage with cache
tokens, suggested edits, questions, and lifecycle. It follows the [Agent
Client Protocol](https://agentclientprotocol.com) vocabulary.
- **`Registry`** — an open set. Built-ins and your own agents sit together.
The subprocess engine is a separate crate:
[`cli-stream`](https://crates.io/crates/cli-stream).
## Examples
One per harness. Run with `cargo run --example <name>`.
| `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.