harness-router
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@harness-routerRoute next tool for fixing the failing parser test given the observation about src/parser.py"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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 fallbackWhy harness-router?
Agentic systems often spend an expensive model call on a decision that is fundamentally discrete:
read a file or search the repository?
inspect or mutate?
run tests or continue editing?
click, navigate, or extract?
which MCP or generic tool should run next?
harness-router separates tool selection from reasoning and execution.
The router is intentionally small:
Jev-backed discrete routing through OpenRouter
hybrid Jev + planner fallback
hierarchical routing for larger tool registries
optional bounded Monte Carlo Tree Search (MCTS) for multi-step lookahead
confidence-aware decisions
MCP and generic tool adapters
native MCP server over stdio with fast route and bounded route_mcts
conservative risk metadata
optional loop detection for long-running harnesses
no required MCP SDK dependency in the core install
async Python API
CLI aliases: harness-router and har
Jev confidence is never authorization. Keep your normal execution policy, permissions, approval gates, and sandbox boundaries around tool execution.
Related MCP server: TypeSafe MCP
Installation
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]"OpenRouter setup
The default provider uses:
model: typesafe/jev-1.13
endpoint: https://openrouter.ai/api/alpha/decisions
env: OPENROUTER_API_KEYSet 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.
CLI
After installation, both commands are available:
harness-router --version
har --versionRoute 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.
Python quick start
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())The routing model
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:
selected tool, when one is appropriate
inferred or explicit category
confidence
per-choice probabilities
fallback flag
fallback reason
Routing modes
Three routing modes are available.
Hybrid
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 |
Jev only
RoutingConfig(mode=RoutingMode.JEV_ONLY)Provider/router errors propagate instead of silently falling back to the planner.
Planner only
RoutingConfig(mode=RoutingMode.PLANNER_ONLY)The router immediately returns a planner fallback decision. No Jev provider is required.
Monte Carlo Tree Search
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:
which tools would be available in a simulated state
the next simulated
HarnessStatean immediate reward
a heuristic value for a simulated state
The 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.
Hierarchical routing
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 / commitFirst 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:
inspect
mutate
execute
verify
git
browser
memory
network
finish
general
Tool adapters
You do not need to convert every tool registry manually.
Generic tools
from harness_router import normalize_tools
tools = normalize_tools([
{
"name": "read_file",
"description": "Read a file",
"input_schema": {
"type": "object",
"properties": {
"path": {"type": "string"}
}
}
}
])MCP
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.
Safety and execution policy
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:
low-risk tools may be allowed
medium-risk tools require an explicit configured confidence threshold
high- and critical-risk tools are denied
For production harnesses, implement your own ExecutionPolicy around the permissions and approval model of your application.
Keep argument generation in the planner
Jev should choose among known alternatives.
Good routing questions:
read vs search
inspect vs mutate
which browser action to take
which MCP or generic tool to call
run tests vs inspect another file
which small fixed action advances the current state
Keep these tasks in the main reasoning model:
generating source code
writing patches
constructing non-trivial shell commands
generating long free-form arguments
open-ended planning
interpreting ambiguous intent
deciding user authorization
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 observationStateful routing and loop detection
The core router is stateless.
For long-running agent loops, RoutingSession adds:
route-step counting
configurable maximum route steps
repeated-fallback circuit breaking
repeated-action detection
A/B loop detection
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.
Custom provider
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.
Native MCP server
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-mcpThe 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.
Codex skill
This repository includes a ready-to-use skill at:
skills/harness-router/SKILL.mdFor 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"}
]'Configuration
RoutingConfig
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
MCTSConfig(
simulations=64,
max_depth=4,
exploration_constant=1.5,
discount=0.95,
max_policy_evaluations=1,
min_prior=1e-6,
)OpenRouterConfig
OpenRouterConfig(
model="typesafe/jev-1.13",
url="https://openrouter.ai/api/alpha/decisions",
api_key_env="OPENROUTER_API_KEY",
timeout_seconds=5.0,
)Error handling
In hybrid mode, provider failures are converted into planner fallback decisions.
Common fallback reasons include:
planner_only
no_tools
no_matching_tool
no_matching_category
low_confidence
low_category_confidence
planner_confirmation
provider_error
router_error
max_route_steps
fallback_circuit_open
In jev_only mode, provider/router failures propagate so the harness can handle them explicitly.
Architecture
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| FDevelopment
Install development dependencies:
python -m pip install -e ".[dev]"Run tests:
pytestLint:
ruff check .Type-check:
mypyProject status
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.
License
MIT.
This server cannot be deployed
Maintenance
Related MCP Connectors
MCP server for progressive tool usage at any scale (see https://klavis.ai)
The OpenRouter for tools. One MCP connection gives any AI agent 254 hosted tools, pay per call.
Nifty's MCP server — exposes tasks, projects, messages, and files as tools for AI agents.
MCP-first toolbox for agents: KV storage, auth, queue, and utility tools. Free in early access.
Related MCP Servers
- AlicenseNot gradedqualityBmaintenanceEnables coding agents to query a Jev model for next-tool recommendations, exposing tools to check status and request tool-choice predictions, while logging all decisions for review.526 npm4MIT
- AlicenseAqualityAmaintenanceEnables MCP-compatible agent hosts to interact with TypeSafe AI's Jev System One API through dependency-free STDIO tools for classification, scoring, verification, gating, routing, review, and health checks.9MIT
- AlicenseNot gradedqualityAmaintenanceEnables Claude Code or any MCP client to ask TypeSafe's Jev for calibrated, typed judgments (probabilities, choices, scores) instead of prose, with local caching and cost tracking.MIT
- AlicenseNot gradedqualityCmaintenanceEnables MCP-capable agents to run TypeSafe's Jev judgment model as typed yes/no, choice, and score tools, with calibrated probabilities, confidence thresholds, escalation for uncertain or non-judgment tasks, and an optional action gate that fails open.MIT