Gateway Inbound Frame Codec
Validate every inbound WebSocket frame once with decode_client_frame into typed, discriminated client frames
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
- Decode once at the boundary
Swap per-handler
data.get(...)for a single validating decode. Every advertised inbound method —hello,message,leave, and the legacyjoin— is decoded here into an already-typed object.python 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 - 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_frameraisesFrameDecodeError. The attachedHelloErrormaps straight onto the outboundhello_error(orerror) wire frame viaerror.to_dict().python 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.
| Field | Type | Default | Description |
|---|---|---|---|
content | Union[str, Dict[str, Any]] | required | The message body — text, or a structured payload |
session_id | Optional[str] | None | Optional session the message belongs to |
message_id | Optional[str] | None | Optional client-supplied idempotency / correlation id |
request_id | Optional[str] | None | Optional request-idempotency key. A resend of the same id runs the turn exactly once — see Gateway Client → Exactly-Once Send |
attachments | List[AttachmentRef] | [] | Optional first-class attachments (files) carried on the message turn — see Gateway Attachments. Each entry is either inline (data base64) or by reference (ref_id in the gateway attachment store), validated against the advertised policy |
metadata | Dict[str, Any] | {} | Optional additional message metadata |
type | str | "message" | Wire-type discriminant (set automatically) |
{
"type": "message",
"content": "Book the meeting for 3pm.",
"session_id": "abc-123",
"message_id": "client-msg-001",
"attachments": [
{"filename": "note.txt", "mime": "text/plain", "size": 12, "data": "SGVsbG8sIHdvcmxkIQ=="},
{"filename": "report.pdf", "mime": "application/pdf", "size": 4194304, "ref_id": "att_8f2c1a"}
],
"metadata": {"user_id": "alice"}
}LeaveParams
The validated leave frame.
| Field | Type | Default | Description |
|---|---|---|---|
session_id | Optional[str] | None | Optional session the client is leaving |
reason | Optional[str] | None | Optional human-readable reason (display / logging only) |
type | str | "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.
| Field | Type | Default | Description |
|---|---|---|---|
agent_id | str | required | The agent to connect to |
min_version | int | 1 (MIN_CLIENT_PROTOCOL_VERSION) | Minimum protocol version the client supports |
max_version | int | 1 (GATEWAY_PROTOCOL_VERSION) | Maximum protocol version the client supports |
session_id | Optional[str] | None | Optional session to resume |
type | str | "join" | Wire-type discriminant (set automatically) |
{"type": "join", "agent_id": "assistant", "min_version": 1, "max_version": 1}The gateway also accepts a server_reply inbound frame — a client’s correlated answer to a blocked HITL turn’s server→client request. The reply’s value is validated per the request’s kind (approval → "allow"/"deny", choice → an offered option, input → any string). See Gateway Server→Client Request Channel for the full contract.
decode_client_frame covers the inbound contract only. The gateway also relays outbound stream events — token_stream, tool_call_stream, stream_end, reasoning_stream, tool_progress_stream, and stream_error — plus accepted vs final response frames after a client negotiates streaming. Those six event types and the response-frame states are documented in Gateway Stream Events.
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:
code—ConnectErrorCode.CONFIGURATION_ERRORfor a schema failure, orConnectErrorCode.PROTOCOL_UNSUPPORTEDfor a version-range failurenext_step—ConnectRecoveryStep.DO_NOT_RETRYfor schema failures, orUPGRADE_CLIENTfor 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
Decode once at the boundary
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.
Return the attached HelloError on FrameDecodeError
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.
Prefer decode_client_frame over per-handler data.get(…)
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.
Related
The hello half of the same union — HelloParams, HelloResult, and HelloError.
The code / next_step recovery-step vocabulary carried by FrameDecodeError.
How session_id on message and leave frames maps to gateway sessions.
The full gateway architecture the frame codec plugs into.
Gateway Stream EventsThe outbound streaming events relayed after a client negotiates streaming.
The first-class attachments a message frame carries — inline or by reference.