xr_ai_routing#

Public contracts and router for participant-scoped agent routing.

Classes#

Admission

Destination decision made before it speaks or starts work.

AgentExchange

One completed user request and destination-provided response context.

AgentTurn

The current user turn and complete exchanges selected for this destination.

DestinationResult

Optional completed response context and the router's focus transition.

RoutableAgent

An agent that owns its state and handles admitted or fallback turns.

RoutingAid

Brief routing metadata, separate from the destination's full instructions.

RoutingTurnContext

Read-only routing metadata captured from the current asynchronous context.

AgentRouter

Route a turn, check admission before speech, and retain explicit focus.

Functions#

current_turn_context(→ xr_ai_routing.RoutingTurnContext)

Return a stable snapshot of metadata for the active routed turn.

Package Contents#

class xr_ai_routing.Admission#

Destination decision made before it speaks or starts work.

accept permits work, decline returns control for another route, clarify ends routing with a question, and release clears focus and returns the same query to ordinary routing. Declining a turn does not clear existing focus or call the focused destination’s release(). That owner receives the next turn unless it releases focus, becomes ineligible, another destination explicitly takes focus, or the participant leaves.

disposition: Literal['accept', 'decline', 'clarify', 'release']#

Whether the destination may begin work or should return control.

preamble: str = ''#

Optional short acknowledgment emitted only after acceptance.

text: str = ''#

Optional text for clarification or release; release text precedes rerouting.

class xr_ai_routing.AgentExchange#

One completed user request and destination-provided response context.

user: str#

User request in the completed exchange.

assistant: str#

Completed response context, excluding progress acknowledgments.

class xr_ai_routing.AgentTurn#

The current user turn and complete exchanges selected for this destination.

participant_id: str#

Participant whose state and focus apply to this turn.

turn_id: str#

Stable identity used to correlate streamed output and cancellation.

query: str#

Unmodified current user request.

history: tuple[AgentExchange, Ellipsis] = ()#

Newest whole completed exchanges that fit the router’s byte budget.

timestamp_us: int | None = None#

Originating input time in Unix microseconds, when supplied by the caller.

class xr_ai_routing.DestinationResult#

Optional completed response context and the router’s focus transition.

disposition: Literal['answer', 'focus', 'release', 'clarify'] = 'answer'#

Destinations return answer, focus, or release; the router returns clarify before answer work.

A post-answer release consumes this turn and clears focus. It does not reroute the request; use Admission("release") for that behavior. Returning focus takes focus and calls the previous owner’s release() if a different destination held it.

text: str = ''#

Optional completed response context to retain in history.

This text is not delivered as output by the router. Destinations can omit it when no routing context is needed; progress acknowledgements are excluded.

class xr_ai_routing.RoutableAgent#

An agent that owns its state and handles admitted or fallback turns.

The router calls release() when it releases a destination during rerouting or clears its focus. On participant departure it calls an optional participant_left(participant_id) method when present, otherwise it calls release(). Implement that optional hook when departure should clear durable participant state that route release retains; it is not required by this protocol.

routing_aid(participant_id: str) → RoutingAid#

Describe this participant’s current eligibility and route scope.

async admit(turn: AgentTurn, *, focused: bool) → Admission#

Accept, decline, clarify, or release before producing spoken output.

A focused destination can decline this turn without ending its session. Use release to clear focus and reroute the same query instead.

async answer(
turn: AgentTurn,
*,
started: collections.abc.Callable[[], collections.abc.Awaitable[None]],
) → DestinationResult#

Answer a turn after admission or as the configured default fallback.

Deliver output directly and return answer, focus, or release. After selected destinations decline through the bounded routing attempts, the front end may call an eligible configured default once without admit(), unless that default already declined the current turn; destinations must support that standalone answer path.

Destinations decline or clarify in admit(), before output or work begins. Returning DestinationResult("release") consumes the current request after the answer and clears focus; it does not reroute that request. Destination exceptions and task cancellation propagate to the caller. Await started() immediately before the first meaningful output or application action. It suppresses an acknowledgment that is not yet committed, waits until a committed one is delivered, supplies the first-response timing sample. Repeated calls wait for the same delivery, and calls after answer() returns have no effect. Output published with the turn’s turn_id as its request ID plays in order after the router’s own control text; the destination never chooses interruption. Completion and focus still use the return value, with optional response context in DestinationResult.text for the router’s history. Destinations own output publication and stream finalization. A failure before started() is reported to the participant by the router; after it, the destination owns any failure output. Cancellation must drain their output and model work before returning. A routing clarify choice with usable scores selects the highest-scoring eligible destination through normal admission. Without eligible scores the router asks for clarification. After exhausted declines, the default is not retried if it already declined this turn, and clarification is also used when the configured default is unavailable.

Parameters:
  • turn – Current request and bounded completed history.

  • started – Response-start signal for this answer.

async release(participant_id: str) → None#

Handle route release; the router also uses it for departure fallback.

This method runs when the router releases a destination during rerouting or clears its focus. It is also the departure fallback when no participant_left() hook is present. Declining a turn while focused does not call this method; a different destination taking focus does. A destination can clear active resources here while retaining completed participant data; use the optional departure hook for stronger cleanup.

class xr_ai_routing.RoutingAid#

Brief routing metadata, separate from the destination’s full instructions.

name: str#

Stable route name used for attribution and decision labels.

description: str#

Concise description used as a route-selection hint, not an authorization rule.

priority: int = 0#

Tie-breaking preference after request relevance.

eligible: bool = True#

Whether this destination may be selected for this participant now.

eligibility: str = 'available'#

Current participant-specific eligibility state.

class xr_ai_routing.RoutingTurnContext#

Read-only routing metadata captured from the current asynchronous context.

The snapshot is stable after it is returned, including when the router later updates its private context for another completed exchange. Outside an active routed turn, the identifiers are None and the history metadata tuples are empty.

exchange_id: object | None#

Identity of the current exchange, when one is active.

history_exchange_ids: tuple[object | None, Ellipsis]#

Exchange identities aligned with the active bounded history.

history_timestamps: tuple[int | None, Ellipsis]#

History timestamps aligned with the active history, in Unix microseconds.

xr_ai_routing.current_turn_context() → xr_ai_routing.RoutingTurnContext#

Return a stable snapshot of metadata for the active routed turn.

Agent code called within a routed turn can use this accessor to associate work with the current exchange and its bounded history. The returned snapshot does not change when routing advances to another exchange. Outside an active routed turn, it contains None and empty history tuples.

class xr_ai_routing.AgentRouter(
destinations: collections.abc.Mapping[str, xr_ai_routing.RoutableAgent],
decisions: xr_ai_models.DecisionService,
*,
output: collections.abc.Callable[[xr_ai_routing.AgentTurn, str, str], collections.abc.Awaitable[None]] | None = None,
default_destination: str = 'conversation',
context_budget_bytes: int = _DEFAULT_CONTEXT_BUDGET_BYTES,
acknowledgment_llm: xr_ai_models.LLMService | None = None,
acknowledgment_delay_secs: float | None = None,
query_topic: xr_ai_runtime.Topic[xr_ai_interaction.UserQuery] | None = None,
output_topic: xr_ai_runtime.Topic[xr_ai_interaction.ResponseOutput] | None = None,
participant_joined_topic: xr_ai_runtime.Topic[xr_ai_interaction.ParticipantJoined] | None = None,
participant_left_topic: xr_ai_runtime.Topic[xr_ai_interaction.ParticipantLeft] | None = None,
interrupted_topic: xr_ai_runtime.Topic[xr_ai_interaction.Interrupted] | None = None,
initialize_participant: collections.abc.Callable[[str], collections.abc.Awaitable[None]] | None = None,
cleanup_participant: collections.abc.Callable[[str], collections.abc.Awaitable[None]] | None = None,
set_status: collections.abc.Callable[[Literal['processing', 'idle'], str], collections.abc.Awaitable[None]] | None = None,
)#

Route a turn, check admission before speech, and retain explicit focus.

A focused destination receives the participant’s next turn first. It can answer, release focus, or decline only this turn while retaining focus. Route choices use the typed decision service; a declined route is excluded from one bounded reroute. If selected destinations decline through those attempts, the configured eligible default gets one final answer attempt without another admission check unless it already declined this turn. An uncertain routing choice uses the highest-scoring eligible destination when usable scores are available. If routing cannot select an eligible destination, the router asks for clarification. Destinations own their model and application state.

acknowledgment_llm and acknowledgment_delay_secs opt into a brief, route-aware intention acknowledgment. Both must be supplied together. A configured acknowledgment client must default to non-reasoning generation; the per-call setting cannot override every provider’s model-level extras. Acknowledgments use a bounded output-token budget. A positive delay sets the fallback silence threshold; a confident prediction of a slower first answer can start the acknowledgment earlier. The destination still starts immediately. Its started() response signal cancels any acknowledgment that has not yet been emitted. This suppresses pending output, not audio already queued or played by downstream transports. The delay and timing examples start after admission and measure when the destination signals its first meaningful response, excluding routing, admission, waiting for committed acknowledgment delivery, and audio playback.

Supply query_topic and output_topic together, then register runtime_subscriber() with xr_ai_runtime.AgentRuntime. This requires the xr-ai-routing[runtime] extra; ordinary handle() use never imports the runtime or interaction messages. No input or lifecycle topic is subscribed unless explicitly supplied. Runtime queries use their publication identity and originating timestamp. Destinations publish their own output; the router publishes only finite admission, clarification, acknowledgment, and failure messages for turns that fail before the destination calls started(). After that call, the destination owns failure output and stream finalization. Runtime background failures are logged once.

Parameters:
  • destinations – Registered destinations keyed by their stable route names.

  • decisions – Typed Clef decision service used to select routes.

  • output – Async callback receiving the current AgentTurn, route name, and finite control text, including acknowledgments, admission preambles, and clarification or release messages. Required for direct handle() calls; runtime queries publish control text through output_topic. Destinations publish their own answer output directly.

  • default_destination – Registered route used for exhausted-decline fallback.

  • context_budget_bytes – UTF-8 byte budget for retained completed history and routing history input.

  • acknowledgment_llm – Optional non-reasoning client for intention acknowledgments and duration prediction.

  • acknowledgment_delay_secs – Finite positive delay before fallback acknowledgment; requires acknowledgment_llm.

  • query_topic – Explicit runtime input topic carrying xr_ai_interaction.UserQuery.

  • output_topic – Explicit topic carrying xr_ai_interaction.ResponseOutput for router control output.

  • participant_joined_topic – Optional explicit ParticipantJoined topic reopening admission.

  • participant_left_topic – Optional explicit ParticipantLeft topic for departure cleanup; requires participant_joined_topic so a departed participant can be readmitted.

  • interrupted_topic – Optional explicit Interrupted topic for participant or global interruption.

  • initialize_participant – Optional initialization, awaited once before the participant’s first turn, whether called directly or published. An interrupted initialization runs again on the next turn.

  • cleanup_participant – Optional idempotent application cleanup after destination work is drained. Used also for initialized participants who never selected a route; failed cleanup can be retried.

  • set_status – Optional asynchronous callback receiving processing or idle, then the participant ID, around each runtime turn. Its failures are logged and do not fail the turn. All callbacks run on the delivery event loop and must not reenter this router’s operations.

Topic payload classes must match exactly and configured topic names must be distinct. Incomplete runtime configuration raises ValueError; a wrong payload class raises TypeError. A missing runtime extra raises ImportError when runtime configuration is supplied. The application must await stop() before stopping its runtime or closing destination resources.

runtime_subscriber() → xr_ai_runtime.Agent#

Return the runtime agent that delivers configured topics to this router.

Register it once with the application’s AgentRuntime. Requires query_topic and output_topic on the constructor.

async handle(
turn: xr_ai_routing.AgentTurn,
) → xr_ai_routing.DestinationResult#

Route one turn while emitting only router-owned control text.

The supplied turn.history seeds this participant when the router has no completed history of its own. Later turns use the router’s completed history rather than merging a new caller-supplied history. The participant’s current turn is kept separate from that history.

A turn that fails before its destination signals a response start publishes one failure message through the router’s output; once the response starts, the destination owns failure output. Exceptions still propagate to the caller for supervision, not for further user-facing recovery. Destinations own direct output delivery, cancellation, and finalization. The router suppresses interrupted control output but cannot retract already-published output.

The application drives turns and lifecycle operations. Destination and output callbacks, including their child tasks, must not recursively await this router’s operations, which would wait for their own response or cleanup to finish.

Raises RuntimeError while departure or shutdown is pending, or after cleanup fails until the caller retries it successfully. Canceling a cleanup waiter does not abandon the owned operation; cancellation propagates after that operation completes.

Configure output on the constructor before calling this direct entry point.

Parameters:

turn – Current request and optional initial completed history.

Returns:

The final answer, focus, release, or clarification disposition.

async interrupt(participant_id: str | None = None) → None#

Cancel active response work while retaining completed history and focus.

If participant_id is None, cancel work for all participants. Awaiting this method drains the canceled response tasks. Output already delivered to a callback cannot be retracted.

async participant_left(participant_id: str) → None#

Release one participant’s state and history.

Attempts every destination used by the participant, calling its participant_left() hook when present and otherwise release(). Failed releases remain tracked for a caller-triggered retry and are raised together in an ExceptionGroup. Until this method completes successfully, handle() raises RuntimeError for that participant. Concurrent callers join the same owned departure. Canceling a waiter waits for that operation to finish before propagating cancellation. Retry after a cleanup failure. With runtime topics configured, departure also closes direct handle() admission until a participant-joined event. Without runtime topics, successful cleanup permits a fresh later turn.

async stop() → None#

Drain responses and release all participant state in one owned operation.

Concurrent callers join the same shutdown. Canceling a waiter does not cancel shutdown: cancellation propagates only after cleanup completes. Release failures are raised together in an ExceptionGroup after every participant is attempted; admission stays closed until a later stop() retries successfully. Successful shutdown clears history-only participants too, and permits later turns to begin a fresh lifecycle.