Choose the next tool inside an existing agent loop.
Harness Router takes a goal, the current observation, and a set of available tools, then returns a tool choice, confidence, or a fallback to the host planner. It uses Jev through OpenRouter for discrete decisions, caches reusable choices, and supports optional Monte Carlo Tree Search (MCTS) over simulated outcomes.
The host harness generates arguments, checks permissions, executes the tool, and processes its result. Router supplies the next-tool decision at the lifecycle boundary the host exposes.
Status: alpha · Python: 3.11+ · License: MIT
Website · Quick start · Integrations · MCP · Python API · MCTS
Install the CLI and optional MCP support with uv:
uv tool install --force --with 'mcp>=2,<3' \
'git+https://github.com/Protocol-Lattice/harness-router.git@main'
export OPENROUTER_API_KEY="your-key"The package exposes har, harness-router, and harness-router-mcp.
har and harness-router are aliases for the same CLI. The default provider model
is typesafe/jev-1.13.
Route a choice among tools:
har route \
--goal "Fix the failing parser test" \
--observation "Failure points to src/parser.py" \
--tools-json '[
{"name":"read_file","description":"Read a repository file","category":"inspect","risk":"low"},
{"name":"search_code","description":"Search repository source","category":"inspect","risk":"low"},
{"name":"run_tests","description":"Run tests","category":"verify","risk":"low"}
]'An illustrative response:
{
"tool": "read_file",
"category": "inspect",
"confidence": 0.93,
"fallback": false,
"fallback_reason": null,
"provider": "openrouter",
"provider_requests": 1
}Use the selected tool through your harness's normal execution path. When
fallback is true, return control to the planner and inspect fallback_reason.
The CLI example supplies descriptors only; Router does not implement those tools.
goal + observation + last action + available tools
│
▼
cached decision?
│ miss
▼
Jev choice
│
├── confident choice ──► next tool
├── uncertain choice ──► planner fallback
└── optional simulator ─► MCTS ─► next tool
│
▼
host arguments, permissions, execution
│
▼
new observation
For large registries, Jev can choose a category first and then a tool within that category. The default library configuration considers this above 24 tools and uses an input-size estimate to decide whether hierarchy is worthwhile.
JevToolRouter.route() performs cached Jev routing. MCTS is a separate search path:
invoke it explicitly, or configure a graph provider for the shared hook bridge.
Search predicts tool outcomes through a simulator and returns the first action.
The adapters connect Router to each host's available lifecycle callbacks. Current
hook integrations precompute a next-tool decision before a model request or after
a tool result. PreToolUse validates that stored decision locally, without making
another Jev request.
| Harness | Installer provider | Decision boundary and behavior |
|---|---|---|
| Codex | codex |
Discovers tools at SessionStart; precomputes at UserPromptSubmit and PostToolUse; a confident mismatch at PreToolUse requests a re-plan. |
| Claude Code | claude |
Uses the same discovery, prompt, result, and validation callbacks; a confident mismatch requests a re-plan. |
| ohmypi | ohmypi |
Reads the runtime tool registry, routes at before_agent_start and tool_result, and uses setActiveTools() to expose the selected tool. |
| Antigravity | antigravity |
Uses PreInvocation to expose or seed a decision, PostToolUse to update it, and PreToolUse to validate it. |
| DeepSeek Harness | deepseek |
Uses the dsh-hooks-codex pre/post execution bridge with a supplied catalog and goal context. |
The shared Stop hook can request another step when a fresh, confident next-tool
decision remains. Its default decision age limit is 15 seconds.
Install the package and set OPENROUTER_API_KEY first. The executable and key must
be available in the environment of the harness process.
From a local checkout, install into a target project:
git clone https://github.com/Protocol-Lattice/harness-router.git
cd harness-router
python3 scripts/install_hook.py \
--provider claude \
--project /path/to/project \
--source .
# Complete the current Claude Code adapter installation.
cp .claude/hooks/pre_decision.py .claude/hooks/stop.py \
/path/to/project/.claude/hooks/Replace claude with the provider from the table. Only Codex and Claude Code need
the extra copy step. For Codex, use .codex in the copy command and install at the
target Git repository root. The current installer
copies the shared hooks but omits the Codex and Claude Code pre_decision.py and
stop.py adapter scripts; the copy step supplies those registered callbacks.
The installer merges hook configuration, backs up changed files as
*.harness-router.bak, and adds provider-specific generated files to .gitignore.
Also ignore .harness-router/, which stores shared session state. Start a new
harness session after installation.
--provider both selects Codex and Claude Code. --provider all selects every
supported harness. Supply the two adapter scripts for each selected Codex or
Claude Code integration.
ohmypi installation requires Bun to install its development dependencies and run
the extension type check. --skip-ohmypi-deps copies the files without that setup.
Start ohmypi from the project root so it discovers .omp/extensions.
For DeepSeek Harness, supply the actual tool descriptors in
.dsh/harness-router-tools.json and start with:
dsh --patch .dsh/harness-router.patch.ymlThe bridge needs a goal from the hook payload or an existing session decision to
precompute the next choice. Missing catalog or goal context leaves normal host
behavior in place. Adapter READMEs provide inventory and host setup details;
their older descriptions of routing inside PreToolUse predate the shared
precomputation flow described here.
The shared bridge uses these environment variables:
| Variable | Default | Purpose |
|---|---|---|
HARNESS_ROUTER_BIN |
harness-router on PATH |
CLI executable for routing in installed projects. |
HARNESS_ROUTER_PREDECISION_TIMEOUT |
4 |
Precomputation deadline in seconds. |
HARNESS_ROUTER_POSTTOOL_TIMEOUT |
4 |
Post-tool bridge deadline in seconds. |
HARNESS_ROUTER_PREDECISION_THRESHOLD |
0.85 |
Minimum confidence for enforcing a stored choice. |
HARNESS_ROUTER_PREDECISION_TTL |
30 |
Stored-choice age limit in seconds for validation hooks. |
HARNESS_ROUTER_PREDECISION_CACHE_TTL |
300 |
Shared state-cache lifetime in seconds. |
HARNESS_ROUTER_PREDECISION_CACHE_SIZE |
512 |
Maximum shared state-cache entries per session. |
HARNESS_ROUTER_PREDECISION_BYPASS_CACHE |
unset | Set to 1 to bypass the shared state-cache lookup. |
HARNESS_ROUTER_PRETOOL_MCTS_GRAPH_CMD |
unset | Command that supplies a simulated graph for optional MCTS. |
HARNESS_ROUTER_PRETOOL_MCTS_THRESHOLD |
0.80 |
Confidence threshold considered for MCTS escalation. |
HARNESS_ROUTER_STOP_DECISION_TTL |
15 |
Stored-choice age limit for stop continuation. |
HARNESS_ROUTER_STOP_CONFIDENCE |
0.85 |
Minimum confidence for requesting continuation. |
Hook precomputations use a state key covering the goal, observation, last action, and compact tool catalog. Decisions and traces are stored under:
.harness-router/sessions/<session_id>.decision.json
.harness-router/sessions/<session_id>.decisions.jsonl
.harness-router/sessions/<session_id>.predecision-cache.json
The decision records include goal and observation text. Routing sends compact state and tool descriptions to the configured provider; keep secrets out of those inputs.
harness-router-mcp runs a stdio MCP server with two tools:
| Tool | Inputs | Result |
|---|---|---|
route |
Goal, tool descriptors, optional observation and last action. | Next tool, confidence, fallback, and reason. |
route_mcts |
Root state, simulated states and transitions, search budget, optional Jev prior. | First tool, confidence, fallback, principal variation, and search diagnostics. |
For a host using TOML MCP configuration:
[mcp_servers.harness-router]
command = "harness-router-mcp"Pass OPENROUTER_API_KEY through the host's environment configuration. Server
initialization and tool discovery work without it; route returns a
missing_openrouter_api_key fallback when the key is absent. MCTS can run locally
with use_jev_prior=false.
Use MCP for explicit routing calls. For agents that load skills, the Harness Router skill describes selective use for ambiguous choices.
har route --help
har route-mcts --helproute requires --goal and a non-empty --tools-json array. Each descriptor
needs a unique tool name; descriptions, categories, and risk levels help the
router distinguish candidates. Risk values are low, medium, high, and
critical.
| Option | Default | Effect |
|---|---|---|
--mode |
hybrid |
Select hybrid, jev_only, or planner_only. |
--direct-threshold |
0.85 |
Confidence required to accept a choice in hybrid mode. |
--fallback-threshold |
0.60 |
Confidence below which a choice falls back to the planner. |
--hierarchical-threshold |
24 |
Tool count above which category-first routing is considered. |
--last-action |
unset | Include the previous tool name in routing state. |
--verbose |
off | Pretty-print the JSON response. |
--no-daemon |
off | Route in the current process. |
--no-cache |
off | Disable the library router's decision caches for this request. |
On Unix-like systems, repeated CLI requests can reuse a local daemon's HTTP
connections and in-memory caches. Daemon responses also include daemon and
cache_hit. Set HARNESS_ROUTER_NO_DAEMON=1 to disable CLI daemon use through the
environment.
The route command exits with 0 when a provider request was attempted and 2
when none was attempted, including cache hits. Read the JSON fallback field to
determine whether routing produced a usable selection.
Install the library into your application's Python environment from a checkout:
python -m pip install -e .Basic Jev routing needs only the core dependencies. Add .[mcp] if the application
also uses the MCP server or the graph-based CLI command.
import asyncio
from harness_router import (
HarnessState,
JevToolRouter,
OpenRouterConfig,
OpenRouterJevProvider,
RiskLevel,
RoutingConfig,
ToolDescriptor,
)
async def main():
tools = [
ToolDescriptor(
name="read_file",
description="Read a repository file",
category="inspect",
risk=RiskLevel.LOW,
),
ToolDescriptor(
name="search_code",
description="Search repository source",
category="inspect",
risk=RiskLevel.LOW,
),
]
async with OpenRouterJevProvider.from_config(OpenRouterConfig()) as provider:
router = JevToolRouter(provider, RoutingConfig())
decision = await router.route(
HarnessState(
goal="Fix the failing parser test",
observation="Failure points to src/parser.py",
),
tools,
)
if decision.fallback:
print("Planner fallback:", decision.fallback_reason)
else:
print(decision.tool, decision.confidence)
asyncio.run(main())RoutingConfig controls confidence thresholds, hierarchy, context limits, and
caches. Its modes behave as follows:
| Mode | Behavior |
|---|---|
hybrid |
Accept choices at or above the direct threshold; return planner fallbacks for uncertainty and handled provider/router errors. |
jev_only |
Accept choices at or above the fallback threshold; propagate provider/router errors. |
planner_only |
Return a planner fallback without asking Jev. |
The library includes a short-lived cache that reuses high-confidence choices for
an unchanged goal and tool set, even when observations change. Set
RoutingConfig(obvious_cache_size=0) to disable that reuse; the shared hook bridge
uses a separate cache that includes observations in its key.
Use MCTS when the best first action depends on possible downstream outcomes. Supply predicted states and transitions; the search does not execute real tools.
This complete CLI example runs locally without an API key. It compares inspecting source with repeating a failing test in a small illustrative graph:
har route-mcts <<'JSON'
{
"root_state": "start",
"use_jev_prior": false,
"simulations": 64,
"max_depth": 3,
"states": [
{
"id": "start",
"goal": "Locate the parser failure",
"tools": [
{"name": "read_file", "description": "Inspect the failing source", "risk": "low"},
{"name": "run_tests", "description": "Repeat the failing test", "risk": "low"}
]
},
{"id": "located", "goal": "Locate the parser failure"},
{"id": "unchanged", "goal": "Locate the parser failure"}
],
"transitions": [
{"from_state": "start", "tool": "read_file", "to_state": "located", "reward": 1.0, "terminal": true},
{"from_state": "start", "tool": "run_tests", "to_state": "unchanged", "reward": 0.0, "terminal": true}
]
}
JSONThe graph-based CLI requires the MCP extra. It accepts JSON on stdin or through
--graph-json. The MCP route_mcts tool accepts the same graph fields and budgets
from 1 to 4,096 simulations and depths from 1 to 8.
For custom Python simulators, implement the async SearchEnvironment methods
tools, transition, and evaluate, then use MCTSToolRouter. A transition
returns a SimulatedStep containing the predicted state, reward, and terminal flag.
MCTSConfig defaults to 64 simulations, depth 4, and at most one Jev policy
evaluation per search.
MCTS confidence is the selected root action's share of visits. The principal variation is a predicted sequence; execute the first selected action through the host, then route again using the real result.
Routing helps when several tools plausibly fit the current step or the registry is large. An obvious tool choice may need no additional routing call. Evaluate task success, retries, total latency, token usage, and provider cost across the whole harness loop.
The controlled benchmark found increased token usage and elapsed time for the tested skill-driven Codex integration. It also describes a separate native-loop benchmark. Those results depend on the tested tasks and integration; they do not establish a general performance improvement for hooks or custom hosts.
Hybrid library routing returns planner fallbacks on low confidence and handled provider errors. MCP reports missing credentials as a fallback. Hook adapters are designed to leave normal host behavior available when routing context or a usable decision is missing; ohmypi restores its original active tool set.
Confidence describes a routing choice. The host still applies its permissions, approvals, credentials, sandbox, and execution policy.
From a checkout:
python -m pip install -e ".[dev]"
pytest
ruff check .
mypyFor ohmypi extension checks:
cd .omp
bun install --frozen-lockfile --ignore-scripts
bun run typecheckMIT © 2026 Protocol Lattice.
