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