Skip to content

Routing

The routing module decides what happens to a decision: handle it automatically or escalate it to human review. A router takes a decision context and returns a RoutingDecision.

routing For: agent engineering

Simple vs. versioned routing

BaseRouter (this page)AgentRouter (versioned)
PurposeIn-process auto-vs-human gatePolicy-governed choice with attribution
Logic lives inYour subclass codeVersioned PolicyVersion rules
Call styleasync (I/O-bound)sync (pure, in-memory)
Versioned?NoYes — every version is an append
Reconstruct a past decision?NoYes — route as_of a date
Records which rule fired?NoYes — matched_rule_id
Backed byNothingBitemporal store
Reach for it whenA quick, non-audited gate is enoughYou must prove which rule fired, when

Install

Terminal window
pip install briefcase-ai[routing]
from briefcase.routing import BaseRouter, RoutingDecision

Route a Decision

  1. Subclass BaseRouter and implement the route coroutine.
  2. Read the decision context — for triage, the classifier’s confidence.
  3. Return a RoutingDecision with an action ("auto" or "human_review") and a reason you can attach to the decision record.

BaseRouter is an abstract base class with a single abstract coroutine, route(decision_context) -> RoutingDecision. Subclass it and implement route. The router is asynchronous because real routers usually call out to an external policy service or model.

import asyncio
import time
from briefcase.routing import BaseRouter, RoutingDecision
class ConfidenceRouter(BaseRouter):
"""Route a support ticket to automatic handling or human review."""
def __init__(self, auto_threshold: float = 0.85):
self.auto_threshold = auto_threshold
async def route(self, decision_context) -> RoutingDecision:
start = time.perf_counter()
confidence = decision_context.get("confidence", 0.0)
if confidence >= self.auto_threshold:
action = "auto"
reason = f"confidence {confidence:.2f} >= {self.auto_threshold}"
else:
action = "human_review"
reason = f"confidence {confidence:.2f} below threshold"
eval_time_ms = (time.perf_counter() - start) * 1000
return RoutingDecision(
action=action,
source="internal",
eval_time_ms=eval_time_ms,
reason=reason,
)
async def main():
router = ConfidenceRouter(auto_threshold=0.85)
high = await router.route({"ticket_id": "T-1001", "confidence": 0.93})
low = await router.route({"ticket_id": "T-1002", "confidence": 0.40})
print(high.action, high.reason) # auto ...
print(low.action, low.reason) # human_review ...
asyncio.run(main())

RoutingDecision

RoutingDecision is a dataclass with four fields:

FieldTypeDescription
actionstrThe routing outcome, e.g. "auto" or "human_review".
sourcestrWhere the decision came from, e.g. "internal", "opa".
eval_time_msfloatHow long evaluation took, in milliseconds.
reasonstr (optional)Human-readable explanation of the outcome.
flowchart LR
    A["Decision context"] --> B["BaseRouter.route"]
    B --> C{"meets criteria?"}
    C -- yes --> D["action = auto"]
    C -- no --> E["action = human_review"]
    D & E --> F["RoutingDecision"]

Choosing a Layer

BaseRouter is intentionally narrow: an in-process gate for the auto-versus-human question. It does not version its logic, and it cannot tell you later which rule fired or which configuration was active on a given date.

When the routing choice is governed by a policy that changes over time — and you need to reconstruct a past decision exactly — use the versioned routing layer instead, which adds AgentRouter, PolicyRegistry, and PolicyVersion for auditable, time-travel routing.

Route through OPA

The opa extra adds an HTTP router with cached policy decisions and a bounded internal fallback:

from briefcase.routing.opa import OPARouter
router = OPARouter(
"https://opa.example/v1/data/briefcase/routing",
timeout_ms=50,
cache_ttl_seconds=60,
fallback_threshold=0.85,
)
decision = await router.route(decision_context)

OPA receives confidence, context version, model name, and tags. On a timeout, network error, or non-success response, InternalRouter applies the configured confidence threshold. Use Invocation Controls for provider selection and credential-scoped failover; routing decides where a decision goes, while controls decide whether a model call may run.

Limits

The base package does not choose your policy. BaseRouter is abstract; InternalRouter supplies a confidence threshold and OPARouter delegates to an endpoint, but application policy and OPA availability remain yours.

action is an unvalidated string. "auto" and "human_review" are the convention on this page, not an enum. RoutingDecision(action="anything-at-all", ...) constructs fine, so agree on the vocabulary in your own code.

A RoutingDecision is a plain dataclass with four fields and no behavior. It does not export itself, persist, or reach an exporter. To keep the routing outcome in the audit trail, put it in the decision you capture, or use the versioned layer, whose AgentRouter records which policy version fired.

route is a coroutine with no timeout or retry. A router that calls a policy service inherits that service’s latency and failures directly. Wrap the call in asyncio.wait_for and decide what a routing failure means; unlike a guardrail, nothing here fails closed for you.

API reference

briefcase.routing has both layers: BaseRouter and RoutingDecision here, plus AgentRouter, PolicyRegistry, PolicyVersion, and PolicyRule for versioned routing.

Where this fits

Routing is part of the Control act: once an action is authorized by a guardrail, the router decides who handles it. For a production audit trail, route through the versioned layer.