Skip to main content
The gateway ships in the praisonai-bot package. praisonai serve gateway works exactly as documented here; for a standalone install see praisonai-bot Migration.
from praisonaiagents import Agent

agent = Agent(name="gateway-agent", instructions="Handle validated inbound gateway frames.")
agent.start("Process a client message that arrived over the gateway wire contract.")
decode_client_frame is the single validating decode step at the WebSocket boundary. Instead of each handler hand-parsing data.get(...), you decode a raw JSON frame once into a typed, discriminated ClientFrame — or get a FrameDecodeError carrying a structured HelloError that is safe to serialise straight back to the client.
from praisonaiagents.gateway import (
    decode_client_frame,
    FrameDecodeError,
    HelloParams,
    MessageParams,
    LeaveParams,
    JoinParams,
)

frame = decode_client_frame({
    "type": "message",
    "content": "Book the meeting for 3pm.",
    "session_id": "abc-123",
})

assert isinstance(frame, MessageParams)
print(frame.content)      # "Book the meeting for 3pm."
print(frame.session_id)   # "abc-123"
The client sends a raw JSON frame; decode_client_frame validates it once and dispatches on type to a typed handler, or rejects it with a structured error.

Quick Start

1

Decode once at the boundary

Swap per-handler data.get(...) for a single validating decode. Every advertised inbound method — hello, message, leave, and the legacy join — is decoded here into an already-typed object.
import json
from praisonaiagents.gateway import (
    decode_client_frame,
    FrameDecodeError,
    HelloParams,
    MessageParams,
    LeaveParams,
    JoinParams,
)

async def on_ws_frame(raw_bytes, ws, agent, session_store):
    try:
        frame = decode_client_frame(json.loads(raw_bytes))
    except FrameDecodeError as e:
        await ws.send_json(e.error.to_dict())   # structured hello_error / error
        return

    if isinstance(frame, HelloParams):
        ...  # negotiate protocol — see Handshake Protocol
    elif isinstance(frame, MessageParams):
        await agent.handle_message(frame.content, session_id=frame.session_id)
    elif isinstance(frame, LeaveParams):
        await session_store.close(frame.session_id, reason=frame.reason)
    elif isinstance(frame, JoinParams):
        ...  # legacy path
2

Reject invalid frames deterministically

When a raw frame is not a mapping, carries an unknown or missing type, or fails per-frame validation, decode_client_frame raises FrameDecodeError. The attached HelloError maps straight onto the outbound hello_error (or error) wire frame via error.to_dict().
import json
from praisonaiagents.gateway import decode_client_frame, FrameDecodeError

try:
    decode_client_frame({"type": "message", "content": ""})   # empty content is rejected
except FrameDecodeError as e:
    print(e.error.code)        # ConnectErrorCode.CONFIGURATION_ERROR
    print(e.error.next_step)   # ConnectRecoveryStep.DO_NOT_RETRY
    wire_frame = e.error.to_dict()
    print(json.dumps(wire_frame))   # ready to send back to the client

Inbound Frames

decode_client_frame returns one of four typed frames, discriminated on the wire type field:
ClientFrame = Union[HelloParams, MessageParams, LeaveParams, JoinParams]
HelloParams is documented on the Gateway Handshake Protocol page (the hello half of the same union). The codec adds two conveniences to the hello path: it tolerates both the flat protocol_min / protocol_max shape and the legacy nested protocol: {min, max} shape, and it accepts a capabilities / caps alias.

MessageParams

The validated message frame — previously hand-parsed per handler.
FieldTypeDefaultDescription
contentUnion[str, Dict[str, Any]]requiredThe message body — text, or a structured payload
session_idOptional[str]NoneOptional session the message belongs to
message_idOptional[str]NoneOptional client-supplied idempotency / correlation id
metadataDict[str, Any]{}Optional additional message metadata
typestr"message"Wire-type discriminant (set automatically)
content is required and also accepts the legacy text alias. Empty-string content is rejected, and content must be a str or dict. Non-dict metadata is silently coerced to {}.
{
  "type": "message",
  "content": "Book the meeting for 3pm.",
  "session_id": "abc-123",
  "message_id": "client-msg-001",
  "metadata": {"user_id": "alice"}
}

LeaveParams

The validated leave frame.
FieldTypeDefaultDescription
session_idOptional[str]NoneOptional session the client is leaving
reasonOptional[str]NoneOptional human-readable reason (display / logging only)
typestr"leave"Wire-type discriminant (set automatically)
{"type": "leave", "session_id": "abc-123", "reason": "user closed tab"}

JoinParams

The legacy join handshake. New clients should prefer hello, but the codec keeps join in the same single validating decode step so the wrapper can share validation.
FieldTypeDefaultDescription
agent_idstrrequiredThe agent to connect to
min_versionint1 (MIN_CLIENT_PROTOCOL_VERSION)Minimum protocol version the client supports
max_versionint1 (GATEWAY_PROTOCOL_VERSION)Maximum protocol version the client supports
session_idOptional[str]NoneOptional session to resume
typestr"join"Wire-type discriminant (set automatically)
agent_id is required and must be a non-empty string. An inverted version range (min_version > max_version) is rejected with a FrameDecodeError carrying PROTOCOL_UNSUPPORTED / UPGRADE_CLIENT.
{"type": "join", "agent_id": "assistant", "min_version": 1, "max_version": 1}

How It Works

Field coercion and validation happen once, at the WebSocket boundary. Handlers receive already-typed objects; the transport gets a deterministic rejection. Every rejection returns a FrameDecodeError whose .error is a HelloError with:
  • codeConnectErrorCode.CONFIGURATION_ERROR for a schema failure, or ConnectErrorCode.PROTOCOL_UNSUPPORTED for a version-range failure
  • next_stepConnectRecoveryStep.DO_NOT_RETRY for schema failures, or UPGRADE_CLIENT for version failures
The HelloError fields and full recovery-step vocabulary are documented on the Gateway Handshake Protocol and Gateway Error Handling pages — error.to_dict() produces the same wire shape used there.

Best Practices

Call decode_client_frame in one place — the WebSocket receive loop — and pass typed frames to handlers. Validation and coercion happen exactly once, so downstream code never re-checks types or defaults.
On FrameDecodeError, send e.error.to_dict() straight back to the client. It is a safe, structured hello_error / error frame carrying (code, next_step, retry_after_seconds) — no per-handler try/except or ad-hoc isinstance checks needed.
Hand-parsing each frame with data.get(...) drifts from the wire contract and duplicates validation. decode_client_frame is the single source of truth: it decodes every advertised inbound method plus the legacy join in one validating step.

Gateway Handshake Protocol

The hello half of the same union — HelloParams, HelloResult, and HelloError.

Gateway Error Handling

The code / next_step recovery-step vocabulary carried by FrameDecodeError.

Session Protocol

How session_id on message and leave frames maps to gateway sessions.

Gateway Overview

The full gateway architecture the frame codec plugs into.