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.
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-compatiblefeature is not a wrapper. It speaks the chat API and runs the tool loop in Rust. - Add your own agent. Implement
Harnessin your own crate and register it. No fork. - Stable wire format.
RunEventserializes 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
= "0.5"
# Add ACP agents and the built-in agent for open models
= { = "0.5", = ["acp", "openai-compatible"] }
# Or take just one adapter
= { = "0.5", = false, = ["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 ;
let = new.run?;
for event in events
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 = new.run?;
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 = ollama;
let = ollama.run?;
A provider is configuration, not code. Point the same type at any OpenAI-compatible endpoint:
use ;
let openrouter = custom;
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:
use ;
let lm_studio = custom
.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.
if !harness.readiness.installed
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 = new
.register
.register;
What the trait gives you
Harness—info,features,readiness,run,credential,login,list_models. Object-safe, soBox<dyn Harness>works.InfoandFeaturesare separate. Identity is asked once; ability is asked constantly, so capabilities live onfeatures()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 aListing— 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 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.