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.
Simple vs. versioned routing
BaseRouter (this page) | AgentRouter (versioned) | |
|---|---|---|
| Purpose | In-process auto-vs-human gate | Policy-governed choice with attribution |
| Logic lives in | Your subclass code | Versioned PolicyVersion rules |
| Call style | async (I/O-bound) | sync (pure, in-memory) |
| Versioned? | No | Yes — every version is an append |
| Reconstruct a past decision? | No | Yes — route as_of a date |
| Records which rule fired? | No | Yes — matched_rule_id |
| Backed by | Nothing | Bitemporal store |
| Reach for it when | A quick, non-audited gate is enough | You must prove which rule fired, when |
Install
pip install briefcase-ai[routing]from briefcase.routing import BaseRouter, RoutingDecisionRoute a Decision
- Subclass
BaseRouterand implement theroutecoroutine. - Read the decision context — for triage, the classifier’s confidence.
- Return a
RoutingDecisionwith anaction("auto"or"human_review") and areasonyou 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 asyncioimport 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:
| Field | Type | Description |
|---|---|---|
action | str | The routing outcome, e.g. "auto" or "human_review". |
source | str | Where the decision came from, e.g. "internal", "opa". |
eval_time_ms | float | How long evaluation took, in milliseconds. |
reason | str (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.