Framework-agnostic Jev tool routing for agentic harnesses.
harness-router uses TypeSafeAI Jev through the OpenRouter Decisions API as a fast System-1 tool-selection layer.
At decision points where several tools are genuinely plausible, give Jev a compact harness state and a fixed set of candidate tools. Jev chooses the next action; your main planner remains responsible for deep reasoning, free-form argument generation, code generation, obvious linear steps, and ambiguous tasks.
User goal + observation
|
v
harness-router
|
+---- high confidence ----> selected tool
|
+---- low/uncertain ------> planner fallback
Agentic systems often spend an expensive model call on a decision that is fundamentally discrete:
harness-router separates tool selection from reasoning and execution.
The router is intentionally small:
route and bounded route_mctsharness-router and harJev confidence is never authorization. Keep your normal execution policy, permissions, approval gates, and sandbox boundaries around tool execution.
Requires Python 3.11+.
uv tool install --force --with 'mcp>=2,<3' 'git+https://github.com/Protocol-Lattice/harness-router.git@main'
For development:
git clone https://github.com/Protocol-Lattice/harness-router.git
cd harness-router
python -m pip install -e ".[dev]"
The default provider uses:
model: typesafe/jev-1.13
endpoint: https://openrouter.ai/api/alpha/decisions
env: OPENROUTER_API_KEY
Set your API key:
export OPENROUTER_API_KEY="your-key"
Do not commit, log, print, echo, or place the key in prompts/routing state. Treat credentials as non-observable runtime inputs. If you only need to check configuration, test whether the environment variable is present without printing its value.
The OpenRouter provider also refuses to send a Jev routing payload if it contains the configured API key value and redacts the key from provider error text.
After installation, both commands are available:
harness-router --version
har --version
Route a tool directly from the terminal:
har route \
--goal "Fix the failing parser test" \
--observation "The failing assertion references src/parser.py" \
--tools-json '[
{
"name": "read_file",
"description": "Read a repository file by path",
"category": "inspect",
"risk": "low"
},
{
"name": "search_code",
"description": "Search source code for a symbol or text",
"category": "inspect",
"risk": "low"
},
{
"name": "write_file",
"description": "Replace a repository file with new content",
"category": "mutate",
"risk": "medium"
}
]'
Example response:
{"category":"inspect","confidence":0.93,"fallback":false,"fallback_reason":null,"tool":"read_file"}
Pass --verbose when you need the full probability map for diagnostics.
import asyncio
from harness_router import (
HarnessState,
JevToolRouter,
OpenRouterConfig,
OpenRouterJevProvider,
RiskLevel,
RoutingConfig,
ToolDescriptor,
)
async def main() -> None:
provider = OpenRouterJevProvider.from_config(OpenRouterConfig())
router = JevToolRouter(provider, RoutingConfig())
tools = [
ToolDescriptor(
name="read_file",
description="Read a repository file by path",
category="inspect",
risk=RiskLevel.LOW,
),
ToolDescriptor(
name="search_code",
description="Search repository source code",
category="inspect",
risk=RiskLevel.LOW,
),
ToolDescriptor(
name="write_file",
description="Replace a repository file",
category="mutate",
risk=RiskLevel.MEDIUM,
),
]
try:
decision = await router.route(
HarnessState(
goal="Fix the failing parser test",
observation="The failure points to src/parser.py",
),
tools,
)
if decision.fallback:
print("Planner fallback:", decision.fallback_reason)
else:
print("Selected:", decision.tool)
print("Confidence:", decision.confidence)
finally:
await provider.aclose()
asyncio.run(main())
HarnessState keeps the decision context intentionally compact:
HarnessState(
goal="Fix the failing parser test",
observation="The assertion references src/parser.py",
last_action="search_code",
constraints=["Do not modify generated files"],
)
A candidate action is represented by ToolDescriptor:
ToolDescriptor(
name="read_file",
description="Read a repository file by path",
category="inspect",
risk=RiskLevel.LOW,
)
The result is a RouteDecision containing:
Three routing modes are available.
Recommended default.
from harness_router import RoutingConfig, RoutingMode
config = RoutingConfig(
mode=RoutingMode.HYBRID,
direct_execution_threshold=0.85,
fallback_threshold=0.60,
)
Default behavior:
| Confidence | Result |
|---|---|
| < 0.60 | planner fallback |
| 0.60 - 0.85 | planner confirmation |
| >= 0.85 | tool can be routed directly, subject to your execution policy |
RoutingConfig(mode=RoutingMode.JEV_ONLY)
Provider/router errors propagate instead of silently falling back to the planner.
RoutingConfig(mode=RoutingMode.PLANNER_ONLY)
The router immediately returns a planner fallback decision. No Jev provider is required.
For decisions where the best first tool depends on what is likely to happen several
steps later, use MCTSToolRouter.
MCTS is intentionally optional and bounded. It requires a side-effect-free
SearchEnvironment that predicts:
HarnessStateThe simulator must not execute real shell commands, writes, browser actions, or other external side effects. Only the final first action selected by the search should go through the normal harness executor and approval policy.
Jev can be supplied as a policy prior. By default, MCTS performs at most one policy-router
evaluation at the first ambiguous node (normally the root). The remaining simulations
are local, so increasing simulations does not automatically multiply policy-router
calls. For registries above hierarchical_threshold, that one router evaluation may
internally use category-first Jev routing.
from harness_router import (
HarnessState,
MCTSConfig,
MCTSToolRouter,
SimulatedStep,
)
class Simulator:
async def tools(self, state):
return available_tools_for(state)
async def transition(self, state, tool):
# Predict only; do not execute the real tool here.
next_state, reward, terminal = predict(state, tool)
return SimulatedStep(next_state, reward=reward, terminal=terminal)
async def evaluate(self, state):
return heuristic_value(state)
mcts = MCTSToolRouter(
Simulator(),
policy_router=router, # optional Jev prior
config=MCTSConfig(
simulations=64,
max_depth=4,
max_policy_evaluations=1,
),
)
result = await mcts.search(state, tools)
print(result.decision.tool)
print(result.principal_variation)
print(result.root_visits)
route(...) is also available when you only want the resulting RouteDecision.
Search uses a PUCT-style selection score. MCTSResult additionally exposes the principal
variation, root visit counts, root action values, simulation count, and the number of Jev
policy evaluations used. For an MCTS-produced RouteDecision, confidence is the
selected root action’s visit share and probabilities are normalized root visit
counts; they are not calibrated Jev confidence values. If the policy router falls back,
MCTS uses a neutral prior rather than overriding that fallback with its probability map.
Large flat tool lists are harder to route efficiently, but category-first routing costs an extra network round trip and repeats the state.
When the number of available tools exceeds hierarchical_threshold (default:
24), JevToolRouter now estimates the request size of flat routing
versus category-first routing. It only pays for the second Jev request when the hierarchical
shape is estimated to save at least hierarchical_min_savings_ratio (default:
15%) of the routing input. Otherwise it stays flat and completes in one request:
+--> inspect --> read_file / search_code / list_files
Harness state -->+--> mutate --> write_file / patch_file
+--> verify --> run_tests / lint
+--> git --> diff / commit
First Jev selects a category, then it selects a tool inside that category.
Set adaptive_hierarchy=False to force the previous threshold-only behavior.
Repeated calls with the exact same compact state and tool registry are served from a bounded
in-process LRU cache (default: 128 routes). Set route_cache_size=0
to disable it.
The OpenRouter provider also sends router-generated JSON state as a native JSON object rather than a JSON-escaped string. OpenRouter’s Decisions API supports structured state directly, which avoids unnecessary wire bytes while preserving the provider-agnostic string protocol used by custom providers.
You can provide categories explicitly or let the built-in adapter infer common categories such as:
inspectmutateexecuteverifygitbrowsermemorynetworkfinishgeneralYou do not need to convert every tool registry manually.
from harness_router import normalize_tools
tools = normalize_tools([
{
"name": "read_file",
"description": "Read a file",
"input_schema": {
"type": "object",
"properties": {
"path": {"type": "string"}
}
}
}
])
from harness_router import MCPToolAdapter, normalize_tools
tools = normalize_tools(mcp_tools, adapter=MCPToolAdapter())
The adapters normalize tool names, descriptions, schemas, categories, and risk metadata. The MCP adapter does not require an MCP SDK dependency.
Routing and execution are deliberately separate.
A high-confidence Jev decision means:
“This is probably the best candidate tool.”
It does not mean:
“This action is authorized.”
The package includes a conservative DefaultExecutionPolicy:
from harness_router import DefaultExecutionPolicy
policy = DefaultExecutionPolicy()
allowed = await policy.allow(decision, tool)
By default:
For production harnesses, implement your own ExecutionPolicy around the permissions and approval model of your application.
Jev should choose among known alternatives.
Good routing questions:
Keep these tasks in the main reasoning model:
A cost-aware agent loop looks like this:
1. Main planner creates/updates the goal
2. Harness builds compact state
3. If tool choice is genuinely ambiguous, harness-router chooses the next tool
4. If multi-step consequences matter and a safe simulator exists, optionally run MCTS
5. Otherwise, use the planner's obvious next tool directly
6. Main planner generates required arguments
7. Execution policy checks permission
8. Harness executes the tool
9. Result becomes the next observation
The core router is stateless.
For long-running agent loops, RoutingSession adds:
from harness_router import RoutingSession
session = RoutingSession(router)
decision = await session.route(state, tools)
loop_detected = session.record_execution(
"read_file",
arguments={"path": "src/parser.py"},
result_class="success",
)
if loop_detected:
# Escalate to the planner, change strategy, or stop.
...
The session reports loops and opens the fallback circuit after repeated planner fallbacks. Call reset_fallbacks() after the planner materially changes the routing state.
JevToolRouter depends on the small DecisionProvider protocol rather than directly on OpenRouter.
A custom provider only needs to implement:
async def choose(
*,
state: str,
instructions: str,
criteria: Mapping[str, str],
) -> ChoiceDecision:
...
This keeps the routing layer provider-agnostic while OpenRouterJevProvider provides the default Jev integration.
For Codex and other MCP hosts, prefer the native MCP server over the routing-helper skill.
It exposes two tools: fast route for ordinary ambiguity and bounded route_mcts for multi-step lookahead over a caller-supplied side-effect-free state graph. The Jev provider stays alive for the process lifetime so HTTP connections and the router cache are reused.
Install the MCP server:
uv tool install --force --with 'mcp>=2,<3' 'git+https://github.com/Protocol-Lattice/harness-router.git@main'
export OPENROUTER_API_KEY="your-key"
Start the stdio server:
harness-router-mcp
The MCP tool accepts a compact payload:
{
"goal": "Fix the failing parser test",
"observation": "Failure points to src/parser.py",
"tools": [
{"name": "read_file", "description": "Read source", "category": "inspect", "risk": "low"},
{"name": "search_code", "description": "Search repo", "category": "inspect", "risk": "low"},
{"name": "run_tests", "description": "Run tests", "category": "verify", "risk": "low"},
{"name": "write_file", "description": "Write source", "category": "mutate", "risk": "medium"}
]
}
The fast route response intentionally omits the full probability map:
{"tool":"read_file","confidence":0.93,"fallback":false,"reason":null}
MCTS is available through route_mcts. It does not execute real tools during search. The caller supplies predicted states, available tools, transitions, rewards, and heuristic state values. By default use about 32 simulations and depth 3; Jev may be used once as a root policy prior, while the remaining simulations stay local.
For Codex, add the stdio server to ~/.codex/config.toml:
[mcp_servers.harness-router]
command = "harness-router-mcp"
Then keep the instruction small: use route only at genuine ambiguity points; skip it for obvious linear steps. Use route_mcts only when downstream consequences matter and the host can provide a side-effect-free simulated graph. Never use real writes, shell commands, browser mutations, or network mutations as MCTS transitions. The fast route uses equal 0.72 direct/fallback thresholds, a 2 second provider timeout, compact state fields, and flat routing through 48 candidates.
This repository includes a ready-to-use skill at:
skills/harness-router/SKILL.md
For Codex, copy the skills/harness-router directory into a location Codex scans for skills, or expose that directory through your existing Codex skill configuration.
The skill tells Codex to use Jev selectively for ambiguous tool selection, while retaining Codex for reasoning and free-form argument generation.
For unmodified Codex, prefer the native MCP server above. If you use the helper skill instead, it is limited to at most one routing-helper call per task, skips obvious linear tool steps, and stops routing after the first fallback or planner override. The helper emits compact JSON by default; use --verbose only for diagnostics.
It also contains a direct routing helper:
python skills/harness-router/scripts/route.py \
--goal "Fix the failing parser test" \
--observation "The failing assertion points to src/parser.py" \
--tools-json '[
{"name":"read_file","description":"Read a source file","category":"inspect","risk":"low"},
{"name":"search_code","description":"Search repository text","category":"inspect","risk":"low"},
{"name":"write_file","description":"Write a source file","category":"mutate","risk":"medium"}
]'
RoutingConfig(
mode=RoutingMode.HYBRID,
direct_execution_threshold=0.85,
fallback_threshold=0.60,
hierarchical_threshold=24,
max_same_action_repeats=2,
max_route_steps=50,
max_consecutive_fallbacks=2,
description_limit=160,
history_limit=3,
state_field_limit=800,
constraint_limit=4,
)
MCTSConfig(
simulations=64,
max_depth=4,
exploration_constant=1.5,
discount=0.95,
max_policy_evaluations=1,
min_prior=1e-6,
)
OpenRouterConfig(
model="typesafe/jev-1.13",
url="https://openrouter.ai/api/alpha/decisions",
api_key_env="OPENROUTER_API_KEY",
timeout_seconds=5.0,
)
In hybrid mode, provider failures are converted into planner fallback decisions.
Common fallback reasons include:
planner_onlyno_toolsno_matching_toolno_matching_categorylow_confidencelow_category_confidenceplanner_confirmationprovider_errorrouter_errormax_route_stepsfallback_circuit_openIn jev_only mode, provider/router failures propagate so the harness can handle them explicitly.
flowchart LR
A[Harness state] --> B[JevToolRouter]
T[Tool registry] --> D[Adapters]
D --> B
B -->|small registry| J[Jev decision]
B -->|large registry| C[Category decision]
C --> J
B -->|optional lookahead| M[MCTS + simulator]
J -->|policy prior| M
J --> P{Confidence policy}
M --> X
P -->|high| X[Selected tool]
P -->|uncertain| F[Planner fallback]
X --> E[Execution policy]
E -->|allowed| R[Harness executor]
E -->|denied| F
Install development dependencies:
python -m pip install -e ".[dev]"
Run tests:
pytest
Lint:
ruff check .
Type-check:
mypy
harness-router is currently alpha software. APIs may still evolve as integrations with real coding, browser, computer-use, generic-tool, and MCP harnesses are exercised.
If you build an integration, benchmark the complete harness loop rather than assuming routing automatically improves latency or cost.
MIT.