harness-router

harness-router

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

Why harness-router?

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:

Jev confidence is never authorization. Keep your normal execution policy, permissions, approval gates, and sandbox boundaries around tool execution.

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_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.

CLI

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.

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:

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.

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:

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 / 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:

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:

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:

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

Stateful routing and loop detection

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.

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-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.

Codex skill

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"}
  ]'

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:

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| F

Development

Install development dependencies:

python -m pip install -e ".[dev]"

Run tests:

pytest

Lint:

ruff check .

Type-check:

mypy

Project 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.