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.
from praisonaiagents import Agent

agent = Agent(name="handshake-agent", instructions="Perform gateway connection handshake.")
agent.start("Establish a secure handshake with the remote gateway.")
The gateway handshake lets clients and servers agree on a protocol version, discover supported features, and recover from disconnects — all in one round trip.
import asyncio
from praisonai.gateway import GatewayClient

async def main():
    client = GatewayClient(url="ws://localhost:8765", agent_id="assistant")
    await client.connect()  # sends hello; receives hello_ok with negotiated protocol

asyncio.run(main())
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

{
  "type": "hello",
  "agent_id": "assistant",
  "protocol_min": 1,
  "protocol_max": 1
}
2

Opt into capabilities

{
  "type": "hello",
  "agent_id": "assistant",
  "protocol_min": 1,
  "protocol_max": 1,
  "capabilities": ["streaming", "presence", "ack"]
}
3

Resume a session

{
  "type": "hello",
  "agent_id": "assistant",
  "protocol_min": 1,
  "protocol_max": 1,
  "session_id": "abc-123",
  "since": 42
}
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)

FieldTypeDefaultDescription
agent_idstrAgent to connect to
protocol_minintMinimum protocol version client supports
protocol_maxintMaximum protocol version client supports
capabilitiesList[str][]e.g. streaming, presence, ack
session_idOptional[str]NoneExisting session to resume
sinceOptional[int]NoneEvent cursor for replay
Legacy nested protocol: {min, max} is also accepted; missing values fall back to 1.

HelloResult (server → client, hello_ok)

FieldTypeDescription
protocolintNegotiated protocol version
featuresDict[str, List[str]]Supported methods and events
policyDict[str, int]max_payload, max_buffered_bytes, max_queued_frames, heartbeat_ms
session_idstrSession ID (new or resumed)
resumedboolTrue if an existing session was resumed
cursorintCurrent event cursor
Example success frame:
{
  "type": "hello_ok",
  "protocol": 1,
  "features": {"methods": ["message", "leave"], "events": ["message", "error", "token_stream"]},
  "policy": {"max_payload": 1048576, "max_buffered_bytes": 8388608, "max_queued_frames": 1000, "heartbeat_ms": 30000},
  "session_id": "...",
  "resumed": false,
  "cursor": 0
}
Base methods: message, leave only (abort is not implemented). Base events: message, error.

HelloError (server → client, hello_error)

FieldTypeDescription
codeConnectErrorCodeStructured, machine-readable error code
messagestrHuman-readable explanation (display only)
next_stepOptional[ConnectRecoveryStep]Machine-readable recovery hint
retry_after_secondsOptional[int]Backoff hint, only meaningful with wait_then_retry
next_actionOptional[str]Deprecated free-text hint; prefer next_step
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):
{
  "type": "hello_error",
  "code": "rate_limited",
  "message": "Too many connection attempts",
  "next_step": "wait_then_retry",
  "retry_after_seconds": 30,
  "next": "wait_then_retry"
}

ConnectErrorCode

CodeMeaningTypical next_step
auth_requiredAuthentication missing on a non-loopback bindreauthenticate
auth_unauthorizedInvalid credentials or wrong agent for sessionreauthenticate
protocol_unsupportedClient/server version mismatchupgrade_client or downgrade_client
pairing_requiredClient must complete pairing firstrepair
agent_not_foundUnknown agent_iddo_not_retry
rate_limitedToo many connection attempts — driven by an injectable RateLimitPolicyProtocolwait_then_retry (+ retry_after_seconds)
origin_not_allowedOrigin not in allowed list (CSWSH defence)do_not_retry
configuration_errorServer is misconfigured (e.g. external bind with no allowlist)do_not_retry
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.
ValueMeaning
reauthenticateObtain fresh credentials, then reconnect
repairRe-run device pairing, then reconnect
upgrade_clientClient protocol too old — update the client
downgrade_clientClient protocol newer than server — use an older client
wait_then_retryBack off (retry_after_seconds), then reconnect
do_not_retryTerminal — reconnecting will not help

Capability Matrix

CapabilityEvents unlockedServer prerequisite
(none)message, error
streamingtoken_stream, tool_call_stream, stream_end
presencepresence_join, presence_leave, presence_updateserver has _presence_tracker
ackmessage_ack, message_nack, delivery_retryserver has _delivery_tracker
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.
session = gateway.get_session(session_id)
session.protocol_version    # → 1   (negotiated min(client_max, GATEWAY_PROTOCOL_VERSION))
session.capabilities        # → ["streaming", "presence", "ack"]   (defensive copy)
PropertyTypeNotes
session.protocol_versionintRead-only. Set by the hello handler from the negotiated value.
session.capabilitiesList[str]Read-only. Returns a copy — mutating the returned list does not change session state. Empty list when the client advertised no capabilities.
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.

Policy Limits

KeyDefaultDescription
max_payload1048576 (1 MB)Maximum message payload size
max_buffered_bytes8388608 (8 MB)Maximum buffered bytes per connection
max_queued_frames1000Maximum queued outbound frames per client. Clients can read this to pace their own sends.
heartbeat_ms30000Heartbeat interval (heartbeat_interval * 1000, default 30 s)
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.
import asyncio
from praisonai.gateway import GatewayClient

async def main():
    client = GatewayClient(
        url="ws://localhost:8765",
        agent_id="assistant",
        capabilities=["streaming", "ack"],
    )
    await client.connect()
    # After hello_ok, the server-side session exposes:
    # session.protocol_version  → "1"  (negotiated version string)
    # session.capabilities      → ["streaming", "ack"]  (client-advertised)

asyncio.run(main())
PropertyTypeDescription
session.protocol_versionstrThe negotiated protocol version agreed during hello.
session.capabilitieslist[str]The capabilities the client advertised in the hello message.
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:
def on_message(session, payload):
    if "streaming" in session.capabilities:
        # send token_stream events
        ...
    else:
        # send complete message at once
        ...
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.
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.
if frame.get("type") == "hello_error":
    step = frame.get("next_step")
    if step == "wait_then_retry":
        await asyncio.sleep(frame.get("retry_after_seconds", 1))
        await reconnect()
    elif step == "reauthenticate":
        await refresh_credentials()
        await reconnect()
    elif step in ("do_not_retry", "upgrade_client", "downgrade_client", "repair"):
        surface_to_user(frame["message"])
    else:
        await reconnect()  # default: try again

Gateway & Control Plane

Unified gateway architecture

Gateway Overview

WebSocket gateway features

Session Protocol

Session message format

Error Handling

Structured connection errors