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
Compaction policy
Compaction is opt-in and pluggable. The default policy is
SummarizeCompactionStrategy: it prunes old oversized tool outputs first, then
summarizes the history when it still exceeds the configured context-window
threshold.
use ;
use Arc;
let context_window_tokens = 128_000;
let harness = new.with_compaction;
Consumers that need different retention rules can implement
CompactionStrategy and pass it through CompactionPolicy::new. The strategy
receives the full Vec<ChatMessage> plus CompactionContext containing the
system prompt, model client, context-window size, and current tool specs.
Returned histories must keep provider invariants intact: any retained assistant
tool calls still need their matching tool results, and strategies should return
the original messages unchanged when they cannot safely compact.
Raw tool-call arguments
ToolInvocation includes raw_emitted_args: Option<String> for consumers that
need to replay or inspect the exact streamed tool-call argument JSON emitted by
the model. It is populated when a provider streams raw argument deltas and those
bytes still parse to the same value as input.
Synthetic calls, restored histories that predate the field, provider paths that
only expose parsed input, and invocations repaired by schema/truncation handling
use None. OpenAI-compatible history projection uses the raw arguments when
they are present and still match input; otherwise it falls back to serializing
the structured input.
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