Gateway Runtime Contract

Let your custom channel adapter receive the gateway's five reliability seams — admission control, delivery routing, identity resolution, identity canonicalization, per-turn locking

Custom channel adapters implement SupportsGatewayRuntime to receive the gateway’s five reliability seams; miss the contract and the gateway fails loudly instead of silently dropping admission control, delivery routing, and cross-platform turn locking. The runtime methods Bot / BotOS drive (start, stop, send_message, probe, health) are already inherited from BasePlatformAdapter; this page covers the five gateway seams on top of them.

Diagram

Gateway Runtime Contract

🛡️ Gateway

📦 GatewayRuntimeSeams

🔌 attach_gateway_runtime

✅ 5 Seams Wired

Quick Start

Adapters that build a BotSessionManager satisfy the contract for free; others implement one method.

  1. You don't have to do anything

    Delegate to BotSessionManager — it already implements the contract, so your adapter inherits it through self._session.

    python
    from praisonai_bot.bots._session import BotSessionManager
    
    
    class MyBot:
        def __init__(self, agent):
            # The gateway finds attach_gateway_runtime on self._session
            # and wires all five seams automatically.
            self._session = BotSessionManager(agent=agent)
  2. Implement the contract yourself

    Define attach_gateway_runtime and copy each non-None seam exactly as BotSessionManager does — a None seam means “keep what you already have”.

    python
    from praisonaiagents.bots import GatewayRuntimeSeams, SupportsGatewayRuntime
    
    
    class MyBot:
        def attach_gateway_runtime(self, runtime: GatewayRuntimeSeams) -> None:
            if runtime.identity_resolver is not None:
                self._identity_resolver = runtime.identity_resolver
            if runtime.identity_canonicalizer is not None:
                self._identity_canonicalizer = runtime.identity_canonicalizer
            if runtime.delivery_router is not None:
                self._delivery_router = runtime.delivery_router
            if runtime.admission_gate is not None:
                self._admission_gate = runtime.admission_gate
            if runtime.turn_lock_map is not None:
                self._locks = runtime.turn_lock_map
    
    
    # Runtime-checkable: isinstance confirms the contract is satisfied.
    assert isinstance(MyBot(), SupportsGatewayRuntime)

How It Works

The gateway builds each adapter once, then hands it a typed GatewayRuntimeSeams carrier through attach_gateway_runtime.

Diagram
adapter.attach_gateway_runtimeBot._attach_gateway_runtimeGatewayUseradapter.attach_gateway_runtimeBot._attach_gateway_runtimeGatewayUserall five seams None? → no-op returnstart channelbuild adapterimport GatewayRuntimeSeamsattach_gateway_runtime(seams)apply non-None seamsseams wiredadapter ready

The gateway resolves the target in a fixed order:

StepConditionOutcome
1All five seams are NoneReturn immediately — pure no-op.
2from praisonaiagents.bots import ... failsFall back to the pre-contract splices (compatibility only).
3adapter.attach_gateway_runtime is callablePreferred path — call it with the seams.
4adapter._session / adapter._session_mgr implements itBuilt-in path via BotSessionManager.
5None of the aboveRaise GatewayAdapterContractError.

The Five Seams

Each seam maps to a gateway reliability guarantee and a feature doc.

FieldTypeDefaultDescriptionFeature doc
identity_resolverOptional[Any]NoneUnifies the same human across platforms onto one session key.Cross-Platform Mirror
identity_canonicalizerOptional[Any]NoneReconciles a volatile raw platform id (WhatsApp JID→LID, number↔UUID alias flip) to a stable canonical id before the identity_resolver runs, so the same human keeps one session instead of silently forking. Any object satisfying IdentityCanonicalizerProtocol — an active BasePlatformAdapter implements it.Identity Canonicalization
delivery_routerOptional[Any]NoneBacking router for the send_message tool (proactive mid-turn delivery).Proactive Delivery
admission_gateOptional[Any]NoneGateway-wide concurrency ceiling / fair queue / backpressure on inbound runs.Gateway Admission Control
turn_lock_mapOptional[Any]NoneShared per-turn lock map for cross-platform turn serialisation on the resolved session id. See Gateway Turn Lock for the pure protocol contract (TurnLockProtocol / LocalTurnLock) that this seam now conforms to.Cross-Platform Mirror

A seam left None means the gateway has nothing to inject for it; leave whatever value you already hold untouched.


The Contract

Three symbols define the contract, all imported from one place.

python
from praisonaiagents.bots import (
    GatewayRuntimeSeams,
    SupportsGatewayRuntime,
    GatewayAdapterContractError,
)

GatewayRuntimeSeams is the typed carrier the gateway fills and passes to your adapter.

python
from dataclasses import dataclass
from typing import Any, Optional


@dataclass
class GatewayRuntimeSeams:
    identity_resolver: Optional[Any] = None
    identity_canonicalizer: Optional[Any] = None
    delivery_router: Optional[Any] = None
    admission_gate: Optional[Any] = None
    turn_lock_map: Optional[Any] = None

SupportsGatewayRuntime is the runtime-checkable Protocol your adapter satisfies.

python
from typing import Protocol, runtime_checkable


@runtime_checkable
class SupportsGatewayRuntime(Protocol):
    def attach_gateway_runtime(self, runtime: "GatewayRuntimeSeams") -> None:
        ...

GatewayAdapterContractError is the loud failure raised when seams cannot be delivered.

python
class GatewayAdapterContractError(TypeError):
    """Raised when a channel adapter cannot receive the gateway runtime seams."""

When GatewayAdapterContractError Fires

The gateway raises this error only when it has seams to inject but your adapter neither implements attach_gateway_runtime nor exposes a _session / _session_mgr that does — the raise is intentional, replacing a silent loss of reliability guarantees.

Diagram

No

Yes

Yes

No

Yes

No

🔌 Adapter built

Seams to inject?

✅ No-op return

Adapter implements
attach_gateway_runtime?

✅ Call it with seams

_session / _session_mgr
implements it?

✅ Delegate to session

❌ GatewayAdapterContractError

The fix is one of the two Quick Start options: build a BotSessionManager on self._session, or implement attach_gateway_runtime directly.


Compatibility

The wrapper permits praisonaiagents >= 1.6.152; on a core that predates the contract the import in step 2 fails and the gateway transparently falls back to the pre-contract private-attribute splices (_identity_resolver, _delivery_router, _admission_gate, _locks) written onto adapter._session or adapter._session_mgr.


Best Practices

Delegate to BotSessionManager when you can

If your adapter already builds a BotSessionManager, expose it as self._session and do nothing else. The gateway finds attach_gateway_runtime on the session and wires all five seams for free.

Apply only non-None seams

Every seam is optional. A None seam means the gateway has nothing for it — leave your existing value in place. Copy each attribute only when it is not None, exactly as the reference implementation does.

Let the contract error fire — don't suppress it

GatewayAdapterContractError means the gateway had reliability seams to inject and your adapter could not receive them. Fix the adapter rather than catching the error; suppressing it silently drops admission control, delivery routing, identity resolution, and per-turn locking.

Migrating an old duck-typed adapter

If your adapter previously relied on the gateway splicing _session._identity_resolver and friends, add a single attach_gateway_runtime method that assigns each non-None seam to the same slot. This makes the wiring explicit and passes the SupportsGatewayRuntime isinstance check.


Related