Skip to content
Docs for briefcase-ai v3.3.0see what’s new.

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.

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.