> ## Documentation Index
> Fetch the complete documentation index at: https://praison.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Gateway Handshake Protocol

> Versioned WebSocket handshake with capability negotiation and structured errors

<Note>
  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](/docs/guides/praisonai-bot-migration).
</Note>

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    C[Client] -->|hello| G[Gateway]
    G -->|hello_ok| C
    G -.->|hello_error| C

    classDef agent fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef gateway fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef ok fill:#10B981,stroke:#7C90A0,color:#fff
    classDef err fill:#F59E0B,stroke:#7C90A0,color:#fff

    class C agent
    class G gateway
    class C ok

    classDef tool fill:#189AB4,color:#fff

    classDef agent fill:#8B0000,color:#fff
```

## Quick Start

<Steps>
  <Step title="Minimal hello">
    ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    {
      "type": "hello",
      "agent_id": "assistant",
      "protocol_min": 1,
      "protocol_max": 1
    }
    ```
  </Step>

  <Step title="Opt into capabilities">
    ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    {
      "type": "hello",
      "agent_id": "assistant",
      "protocol_min": 1,
      "protocol_max": 1,
      "capabilities": ["streaming", "presence", "ack"]
    }
    ```
  </Step>

  <Step title="Resume a session">
    ```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    {
      "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.
  </Step>
</Steps>

***

## How It Works

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant Client
    participant Gateway

    Client->>Gateway: WS upgrade
    alt rate limited
        Gateway-->>Client: hello_error (rate_limited, wait_then_retry, retry_after_seconds)
        Gateway-->>Client: close 4008
    else origin denied
        Gateway-->>Client: hello_error (origin_not_allowed, do_not_retry)
        Gateway-->>Client: close 4003
    else auth missing
        Gateway-->>Client: hello_error (auth_required, reauthenticate)
        Gateway-->>Client: close 4003
    else accepted
        Client->>Gateway: hello (agent_id, protocol_min/max)
        alt success
            Gateway-->>Client: hello_ok (protocol, features, policy, session_id)
        else version too old
            Gateway-->>Client: hello_error (protocol_unsupported, upgrade_client)
        else version too new
            Gateway-->>Client: hello_error (protocol_unsupported, downgrade_client)
        else unknown agent
            Gateway-->>Client: hello_error (agent_not_found, do_not_retry)
        end
    end
```

Negotiated protocol version: `min(client_max, GATEWAY_PROTOCOL_VERSION)` where server version is **1** and minimum accepted client version is **1**.

Legacy `join` → `joined` still works for existing clients. New clients should prefer `hello`.

***

## HelloParams (client → server)

| Field          | Type            | Default | Description                              |
| -------------- | --------------- | ------- | ---------------------------------------- |
| `agent_id`     | `str`           | —       | Agent to connect to                      |
| `protocol_min` | `int`           | —       | Minimum protocol version client supports |
| `protocol_max` | `int`           | —       | Maximum protocol version client supports |
| `capabilities` | `List[str]`     | `[]`    | e.g. `streaming`, `presence`, `ack`      |
| `session_id`   | `Optional[str]` | `None`  | Existing session to resume               |
| `since`        | `Optional[int]` | `None`  | Event cursor for replay                  |

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

***

## HelloResult (server → client, `hello_ok`)

| Field        | Type                   | Description                                                              |
| ------------ | ---------------------- | ------------------------------------------------------------------------ |
| `protocol`   | `int`                  | Negotiated protocol version                                              |
| `features`   | `Dict[str, List[str]]` | Supported `methods` and `events`                                         |
| `policy`     | `Dict[str, int]`       | `max_payload`, `max_buffered_bytes`, `max_queued_frames`, `heartbeat_ms` |
| `session_id` | `str`                  | Session ID (new or resumed)                                              |
| `resumed`    | `bool`                 | `True` if an existing session was resumed                                |
| `cursor`     | `int`                  | Current event cursor                                                     |

Example success frame:

```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
{
  "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`)

| Field                 | Type                            | Description                                          |
| --------------------- | ------------------------------- | ---------------------------------------------------- |
| `code`                | `ConnectErrorCode`              | Structured, machine-readable error code              |
| `message`             | `str`                           | Human-readable explanation (display only)            |
| `next_step`           | `Optional[ConnectRecoveryStep]` | Machine-readable recovery hint                       |
| `retry_after_seconds` | `Optional[int]`                 | Backoff hint, only meaningful with `wait_then_retry` |
| `next_action`         | `Optional[str]`                 | **Deprecated** free-text hint; prefer `next_step`    |

<Note>
  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`.
</Note>

Example wire frame (rate-limited):

```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
{
  "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

| Code                   | Meaning                                                                                                                      | Typical `next_step`                         |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- |
| `auth_required`        | Authentication missing on a non-loopback bind                                                                                | `reauthenticate`                            |
| `auth_unauthorized`    | Invalid credentials or wrong agent for session                                                                               | `reauthenticate`                            |
| `protocol_unsupported` | Client/server version mismatch                                                                                               | `upgrade_client` or `downgrade_client`      |
| `pairing_required`     | Client must complete pairing first                                                                                           | `repair`                                    |
| `agent_not_found`      | Unknown `agent_id`                                                                                                           | `do_not_retry`                              |
| `rate_limited`         | Too many connection attempts — driven by an injectable [`RateLimitPolicyProtocol`](/docs/features/gateway-rate-limit-policy) | `wait_then_retry` (+ `retry_after_seconds`) |
| `origin_not_allowed`   | Origin not in allowed list (CSWSH defence)                                                                                   | `do_not_retry`                              |
| `configuration_error`  | Server is misconfigured (e.g. external bind with no allowlist)                                                               | `do_not_retry`                              |

<Note>
  The `rate_limited` code is driven by an injectable `RateLimitPolicyProtocol` — see [Gateway Rate Limit Policy](/docs/features/gateway-rate-limit-policy) for `SlidingWindowRateLimitPolicy`, YAML `gateway.rate_limit`, and custom policies.
</Note>

### ConnectRecoveryStep

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

| Value              | Meaning                                                 |
| ------------------ | ------------------------------------------------------- |
| `reauthenticate`   | Obtain fresh credentials, then reconnect                |
| `repair`           | Re-run device pairing, then reconnect                   |
| `upgrade_client`   | Client protocol too old — update the client             |
| `downgrade_client` | Client protocol newer than server — use an older client |
| `wait_then_retry`  | Back off (`retry_after_seconds`), then reconnect        |
| `do_not_retry`     | Terminal — reconnecting will not help                   |

***

## Capability Matrix

| Capability  | Events unlocked                                      | Server prerequisite            |
| ----------- | ---------------------------------------------------- | ------------------------------ |
| (none)      | `message`, `error`                                   | —                              |
| `streaming` | `token_stream`, `tool_call_stream`, `stream_end`     | —                              |
| `presence`  | `presence_join`, `presence_leave`, `presence_update` | server has `_presence_tracker` |
| `ack`       | `message_ack`, `message_nack`, `delivery_retry`      | server 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.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
session = gateway.get_session(session_id)
session.protocol_version    # → 1   (negotiated min(client_max, GATEWAY_PROTOCOL_VERSION))
session.capabilities        # → ["streaming", "presence", "ack"]   (defensive copy)
```

| Property                   | Type        | Notes                                                                                                                                        |
| -------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `session.protocol_version` | `int`       | Read-only. Set by the `hello` handler from the negotiated value.                                                                             |
| `session.capabilities`     | `List[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.

<Tip>
  Use this to tailor delivery — e.g. only enqueue `token_stream` events when `"streaming" in session.capabilities` — without re-parsing the original `hello` frame.
</Tip>

***

## Policy Limits

| Key                  | Default          | Description                                                                               |
| -------------------- | ---------------- | ----------------------------------------------------------------------------------------- |
| `max_payload`        | `1048576` (1 MB) | Maximum message payload size                                                              |
| `max_buffered_bytes` | `8388608` (8 MB) | Maximum buffered bytes per connection                                                     |
| `max_queued_frames`  | `1000`           | Maximum queued outbound frames per client. Clients can read this to pace their own sends. |
| `heartbeat_ms`       | `30000`          | Heartbeat interval (`heartbeat_interval * 1000`, default 30 s)                            |

Clients should self-configure from the `policy` object in `hello_ok`. See [Gateway Flow Control](/docs/features/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.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
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())
```

| Property                   | Type        | Description                                                    |
| -------------------------- | ----------- | -------------------------------------------------------------- |
| `session.protocol_version` | `str`       | The negotiated protocol version agreed during `hello`.         |
| `session.capabilities`     | `list[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:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
def on_message(session, payload):
    if "streaming" in session.capabilities:
        # send token_stream events
        ...
    else:
        # send complete message at once
        ...
```

<Note>
  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.
</Note>

***

## Best Practices

<AccordionGroup>
  <Accordion title="Always advertise protocol_min and protocol_max">
    Even when you only support version 1 today, send an explicit range so future servers can negotiate.
  </Accordion>

  <Accordion title="Only request capabilities you handle">
    Requesting `streaming` without handling `token_stream` events wastes bandwidth and confuses clients.
  </Accordion>

  <Accordion title="Use session_id + since on reconnect">
    Resume cleanly after disconnect and process `replay` envelopes before sending new messages.
  </Accordion>

  <Accordion title="Branch on (code, next_step) from hello_error">
    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`.

    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    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
    ```
  </Accordion>
</AccordionGroup>

***

## Related

<CardGroup cols={2}>
  <Card title="Gateway & Control Plane" icon="gateway" href="/docs/gateway">
    Unified gateway architecture
  </Card>

  <Card title="Gateway Overview" icon="tower-broadcast" href="/docs/features/gateway-overview">
    WebSocket gateway features
  </Card>

  <Card title="Session Protocol" icon="messages" href="/docs/features/session-protocol">
    Session message format
  </Card>

  <Card title="Error Handling" icon="shield-check" href="/docs/features/gateway-error-handling">
    Structured connection errors
  </Card>
</CardGroup>
