Skip to main content
The gateway now ships in the praisonai-bot package. praisonai serve gateway still works exactly as documented here; for a standalone install see praisonai-bot Migration.
The gateway handshake lets clients and servers agree on a protocol version, discover supported features, and recover from disconnects — all in one round trip.
The user connects via GatewayClient; hello and hello_ok negotiate protocol version, features, and session cursor in one round trip.

Quick Start

1

Minimal hello

2

Opt into capabilities

3

Resume a session

After hello_ok, replayed events arrive as {"type": "replay", "event": …} before normal traffic resumes.

How It Works

Negotiated protocol version: min(client_max, GATEWAY_PROTOCOL_VERSION) where server version is 1 and minimum accepted client version is 1. Legacy joinjoined still works for existing clients. New clients should prefer hello.

HelloParams (client → server)

Legacy nested protocol: {min, max} is also accepted; missing values fall back to 1.

HelloResult (server → client, hello_ok)

Example success frame:
Base methods: message, leave only (abort is not implemented). Base events: message, error.

HelloError (server → client, hello_error)

Wire frame also includes a legacy next key for backward compatibility — it mirrors next_action (or falls back to next_step.value). The next_step, retry_after_seconds, and next keys are omitted entirely when no recovery hint is set — they are never null.
Example wire frame (rate-limited):

ConnectErrorCode

The rate_limited code is driven by an injectable RateLimitPolicyProtocol — see Gateway Rate Limit Policy for SlidingWindowRateLimitPolicy, YAML gateway.rate_limit, and custom policies.

ConnectRecoveryStep

Machine-readable recovery hint. Clients branch on (code, next_step) instead of parsing message.

Capability Matrix

Events are advertised only if the client requested the matching capability.

Server-side state after handshake

After hello_ok is sent, the gateway records the negotiated protocol version and the client’s advertised capabilities on the session itself. Both are read-only and survive resume.
Both properties are populated on the hello path and the legacy join path, so server code can branch on session.capabilities without checking which handshake the client used.
Use this to tailor delivery — e.g. only enqueue token_stream events when "streaming" in session.capabilities — without re-parsing the original hello frame.

Client-side wiring (praisonai-bot)

The bundled praisonai-bot[gateway] client speaks the modern handshake for you — construct it with the capabilities you handle, connect(), then read the negotiated manifest off the instance.
The client always sends hello first — capabilities= controls what it advertises, not whether the modern handshake is used.

Client construction

Negotiated accessors

connect() populates these from the server’s hello_ok. Against a legacy gateway they stay at permissive defaults so existing code keeps working.

supports_event(event)

Gate optional behaviour on the negotiated event set instead of try: … except: probing.
  • Accepts a str (e.g. "token_stream") or an EventType member; the enum’s .value is used for lookup.
  • Returns True for legacy gateways (empty features["events"]) so code that assumed everything worked keeps working — this is the intentional permissive default.
  • Otherwise returns True iff event in client.features["events"].

Handshake flow — client branch on the server’s first reply

The wire is documented server-side above; this is what the client does with each reply.

PayloadTooLarge

Raised inside client.send(...), before the WebSocket write, when len(payload.encode("utf-8")) > client.policy["max_payload"].
  • The frame never leaves the client — nothing reaches the server and no round-trip is spent.
  • Size is measured in UTF-8 bytes, not len(str), matching how the server counts. Size payloads by encoded length, not string length.
  • Catch it and shrink/split the payload or accept the loss — retrying the same frame fails identically.
  • A max_payload of 0 is honoured as “reject everything”; None/missing means unbounded. On a legacy gateway (policy == {}) this exception is unreachable and the previous server-side reject still applies.

Policy Limits

Clients should self-configure from the policy object in hello_ok. See Gateway Flow Control for tuning max_buffered_bytes and max_queued_frames.

Persisted Session State

After hello_ok, the gateway records both the negotiated protocol version and the client-advertised capabilities on the GatewaySession. These values survive disconnect and resume.
Both properties are written into session.to_dict() and restored by from_dict(), so they survive server restarts and session persistence backends. Server-side hooks and custom routing code can read these off any live session:
If to_dict() / from_dict() encounters a non-list value for capabilities (e.g. from an older snapshot), it is safely restored as [] to avoid errors.

Best Practices

Even when you only support version 1 today, send an explicit range so future servers can negotiate.
Requesting streaming without handling token_stream events wastes bandwidth and confuses clients.
Resume cleanly after disconnect and process replay envelopes before sending new messages.
The manifest is the source of truth once negotiated; probing races the server’s advertisement. Use client.supports_event("token_stream") to gate optional behaviour.
The local guard is the fast-fail seam; without a catch, a single oversized user message crashes the send loop.
Fall through to supports_event(...)’s permissive True default so a mixed-version fleet keeps working.
Use the structured (code, next_step) pair instead of parsing message. The legacy next key is still emitted for older clients but new code should branch on next_step.

Gateway & Control Plane

Unified gateway architecture

Gateway Overview

WebSocket gateway features

Session Protocol

Session message format

Error Handling

Structured connection errors