agent-harness-rs
Agent loop harness for building LLM-powered coding agents. Provides a complete runtime with tool execution, context management, MCP support, and e2b sandbox integration.
Features
- Agent loop — OpenAI-compatible and Anthropic streaming model clients with retry, reconnect, silent-stop detection, and compaction
- Local tools —
bash,read,write,edit,glob,grep,web_fetchwith approval gate (feature = "local-tools", default) - Hosted tools — Provider-native tools that execute at the model provider; currently Anthropic native
web_search - Sandbox tools — Generic
SandboxExecutortrait for any remote sandbox - E2b integration —
E2bToolRuntimevia Connect Protocol to envd (feature = "e2b") - Context persistence — JSONL-based context store with incremental append and compaction rewrite
- MCP support — HTTP and stdio MCP server integration via
CompositeToolRuntime - Model limits catalog — async
models.devfetch with disk cache, backoff retry, and offline fallback for context-window / output-token resolution
Quick start
[]
= "0.2"
# For e2b sandbox support:
= { = "0.2", = ["e2b"] }
use ;
use Arc;
use PathBuf;
// Local tool runtime (runs bash/read/write on your machine)
let tools = new;
let model = new;
let harness = new;
let mut rx = harness.run_turn.await?;
while let Some = rx.recv.await
Web tools
agent-harness-rs intentionally separates harness-executed tools from
provider-executed hosted tools.
web_fetch
web_fetch is a built-in read-only tool implemented by the harness. It fetches
a known HTTP/HTTPS URL and returns readable content. It does not search the
web or discover URLs.
Supported inputs:
| Field | Default | Purpose |
|---|---|---|
url |
required | HTTP/HTTPS URL to fetch |
format |
markdown |
One of markdown, text, or html |
max_length |
50000 |
Maximum returned characters, capped at 200000 |
timeout_ms |
20000 |
Request timeout, capped at 60000 |
The tool follows redirects, rejects non-HTTP schemes and obvious binary/image
content, caps downloaded bodies at 5 MiB, and returns JSON containing url,
final_url, status, content_type, format, content, and truncated.
PlanApproval treats web_fetch as read-only, so planning-mode agents can use
it alongside read, glob, and grep.
Hosted web_search
web_search is modeled as a provider-hosted tool, not a normal
ToolRuntime tool. The provider executes it server-side and the harness only
passes the capability through the model request.
Currently supported:
- Anthropic Messages API:
HostedTool::WebSearchis projected as{"type":"web_search_20250305","name":"web_search"}. - OpenAI-compatible Chat Completions: explicitly unsupported. OpenAI web search requires a future Responses API client; this crate fails fast instead of pretending search is available.
- Other providers: unsupported unless their model client adds a hosted-tool projection.
use ;
// Convenience: enable Anthropic native web_search with provider defaults.
let harness = new.with_web_search;
// Or set Anthropic's max_uses cap explicitly.
let harness = new
.with_hosted_tools;
Silent-stop detection
If a model step ends with no tool calls and no user-visible text, the harness no
longer treats it as a successful turn. Empty or whitespace-only final output with
stop_reason = "end_turn" or "max_tokens" returns a model error containing
silent_stop.
This catches provider/model failures where a turn would otherwise look successful while delivering no answer and taking no action.
Changelog
Every behavior change should be recorded in CHANGELOG.md before release. This
project uses patch-only version bumps within the current minor line, so the next
release after 0.2.5 is 0.2.6.
E2b sandbox
use ;
let tools = connect.await?;
let harness = new;
Model limits catalog
Context-window and output-token limits are resolved per model from
models.dev (the public model registry opencode
also uses), with a best-effort strategy that never blocks the agent loop:
- In-memory table populated by a fire-and-forget background fetch.
On the first
resolve_limits()call a fetch is spawned; while it is in flight (typically during early LLM warm-up) callers get the fallback value, and pick up the real value on the next turn. - Disk cache at
<cache_dir>/agent-harness-rs/models.json(5 min TTL, atomic tempfile +renamewrite) so a network blip mid-session still serves real values. - Offline fallback table — the legacy hand-encoded claude/gpt/ o-series/minimax/deepseek mappings, so behavior never regresses.
- Conservative default
{ context: 128_000, output: 8_192 }.
The fetch retries 3× with exponential backoff (+ jitter) and a 10 s per-request timeout; on final failure it logs and falls back silently.
use ;
// Optional: warm the cache before the first turn. Safe to skip — the
// first resolve_limits() triggers it lazily.
prefetch_model_limits;
// Fast, non-async, never blocks.
let limits = resolve_limits;
// limits.context — used for compaction thresholds
// limits.output — model's per-completion output cap
Configuration via environment variables:
| Variable | Default | Purpose |
|---|---|---|
AGENT_HARNESS_MODELS_URL |
https://models.dev/api.json |
Override the registry endpoint |
AGENT_HARNESS_CACHE_PATH |
<cache_dir>/agent-harness-rs/models.json |
Relocate the disk cache |
Approval modes
use ;
// Allow everything
new
// Read-only (hide bash/write/edit from model)
new
// Custom gate (e.g. ask user via UI)
;
Shell risk policy
bash commands are normally pre-classified with a conservative static
read-only checker before the approval gate runs. To temporarily bypass that
checker while still blocking session-destroying hard-deny commands, set:
AGENT_HARNESS_SHELL_RISK_POLICY=relaxed
Accepted relaxed values are relaxed, lenient, and permissive. Leave the
variable unset for the default strict behavior.
License
MIT