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 Chat Completions, OpenAI Responses, 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) - Web search routing — Explicit
Off,Auto,Native, orManagedpolicy across provider-hosted search and pluggable managed providers - 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
OpenAI Responses API
Use OpenAiResponsesModelClient for OpenAI's Responses API and GPT-5 /
reasoning-model workflows. The client appends /responses to base_url
unless the route is already present.
use ;
use PathBuf;
let model = new;
let harness = new;
let mut rx = harness.run_turn.await?;
Responses support is intentionally stateless: requests always send
store:false, and the harness replays the full local history each turn. When
reasoning is returned, the client requests
include:["reasoning.encrypted_content"] and stores the encrypted reasoning
state in AssistantThinking.signature so the next turn can replay it without
using previous_response_id or server-side item references.
Provider-hosted web_search is available on Responses through the explicit
search policy:
use WebSearchMode;
let harness = new
.with_web_search;
ToolChoice::None suppresses both function tools and hosted tools for that
turn. ToolChoice::Required applies to the advertised tool set, including a
hosted-only Responses turn.
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 and chooses exactly one search surface per turn.
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.
web_search routing
Search is disabled by default because it can incur network egress and provider charges. Enable it with an explicit mode:
| Mode | Behavior |
|---|---|
Off |
Advertise neither native nor managed search |
Auto |
Prefer proven native support, otherwise use a runtime web_search tool when present |
Native |
Require provider-hosted search; fail fast when explicitly unsupported |
Managed |
Require a runtime-provided web_search function tool |
ModelClient::hosted_capability reports Supported, Unsupported, or
Unknown for the client's actual API route. Official OpenAI Responses and
Anthropic endpoints report native web search as supported. OpenAI-compatible
Chat Completions reports unsupported. Custom endpoints report unknown, so
Auto does not risk sending provider-specific hosted tools to a gateway that
may reject them; Native can be used to force an attempt.
Custom ModelClient implementations must make this declaration explicitly;
there is no capability default and no model-name allowlist.
use ;
let harness = new
.with_web_search;
For managed search, compose WebSearchToolRuntime with the normal runtime.
The managed runtime is separate from built-in tools so a missing search API key
never creates a tool the application cannot execute:
use ;
use Arc;
let search = from_provider;
let tools = new;
let harness = new
.with_web_search;
The managed result contract contains query, provider, normalized
title/url/snippet rows, truncated, and an external_content.untrusted
marker. Use web_fetch to read a selected result in full.
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.12 will be 0.2.13.
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