Skip to main content
A blocked HITL turn asks any connected gateway client for an approval, a choice, or free-text input — and the request survives every reconnect until the client answers or the turn times out.
The gateway ships in the praisonai-bot package. praisonai serve gateway works exactly as documented here; for a standalone install see praisonai-bot Migration. No operator action is required to opt in beyond running the gateway.

Quick Start

1

Run an agent that blocks on approval

An agent that already elicits approval gets this path automatically once a running gateway binds the request channel — no extra flags.
2

Handle the frame from a client

The client receives a server_request when a turn blocks and posts back a server_reply with the answer.
3

Survive a reconnect

Restart the client mid-prompt. On the next hello (or legacy join), the gateway re-issues every still-open request after replaying events, so the prompt re-renders on the new socket.
4

Timeout fallback

A request defaults to a 120.0s timeout. On timeout the waiting turn receives None, the open request is cleared, and the elicitation code decides the fallback — typically deny/default.

How It Works

A turn blocks, the gateway tracks the open request, delivers a frame, and resolves the waiting future when the correlated reply arrives — even across a mid-prompt reconnect. The core reaches the channel through a task-local slot: get_request_channel() returns the bound implementation (or None in CLI/one-shot, falling back to the existing elicitation path). The gateway registers the channel with register_request_channel(...) at the start of a turn and restores the previous slot with clear_request_channel(token) — the same pattern as the outbound messenger and conversation requester.

Wire Frames

Copy these frames verbatim — integrators code straight against them. Server → client — delivered when a turn blocks, and re-issued on reconnect:
kind is one of "approval", "choice", "input". options is present only when kind == "choice". Client → server — the correlated answer:
Server → client — correlation ack (always sent; never leaks another session’s request kind):
An unknown_request ack is a safe no-op: an unknown or already-answered id acks without leaking state. Server → client — structured error frames, each carrying a code:

Request Kinds and Value Contract

Three request kinds map to three value contracts, validated at the handler. An unrelated value is rejected with {"code": "invalid_value"} and the request stays open.

Reconnect Replay

Still-open requests are re-issued whenever a client resumes, which is the whole reason this channel exists. Replay runs at the end of both the hello (protocol v2+ resume with since) and legacy join handlers, right after event replay. Only entries still present in the session’s open-request set are re-sent, so an already-answered or timed-out request is never re-issued. The join path also rebinds the session’s preferred delivery target (session._client_id = client_id) so the joining client actually receives the next server_request — without this rebind, a new request after join would be routed to the stale client. This puts join on parity with hello for this feature.

Security Model

Every server_reply passes three checks, in order: ownership → scope → value.

Session ownership

Only a client bound to the request’s own session_id may answer it. A WRITE client in another session that guesses a request_id is rejected with {"code": "session_mismatch"} — the victim turn stays open.

Scopes

server_reply classifies as write in the central registry. The handler escalates to approvals when the request’s kind == "approval" (parity with approvals.resolve). Choice and input still require write. A WRITE-only client can answer choice/input but gets {"code": "insufficient_scope", "required_scope": "approvals"} for an approval. See Gateway Operator Scopes.

Value contract

The reply value is enforced per the table above; an invalid value is rejected with {"code": "invalid_value"} and the request stays open.

Timeout

request() defaults to a timeout_s of 120.0. On timeout it returns None and clears the open-request entry, so a later reconnect does not re-issue a timed-out prompt. The blocked turn’s elicitation code interprets None as its fallback — typically deny/default.

Backward Compatibility

Unbound (CLI / one-shot) behaviour is unchanged — get_request_channel() returns None and existing elicitation paths run as before. The praisonai-bot import of the two core shapes is guarded, so it keeps working against an older core. There is no new user-authored surface, no new Agent parameter, and no new dependency.

Building a Client

The minimum a client needs: receive a server_request, render the prompt, post back a server_reply, and clear on the ack — keyed idempotently on request_id.

Best Practices

The default 120s is safe for a human-in-the-loop web UI. Interactive shells that expect a fast answer may want a shorter timeout so a blocked turn does not sit idle.
A reconnect can re-issue a prompt the user has already seen. Render idempotently, keyed on request_id, so a replayed request updates the existing prompt instead of duplicating it.
A WRITE-only client can answer choice and input but gets insufficient_scope for approval prompts. Size tokens so any role that resolves approvals over the transport holds the approvals scope.
The ownership check rejects cross-session replies with session_mismatch. Treat an unknown_request ack as a signal that client state has drifted, not as a retry cue.
The timeout returns None and clears the request. Do not retry the same request_id after that — start a fresh turn if the action is still needed.

Gateway Session Continuity

How event replay and this open-request replay compose on hello / join.

Gateway Frame Codec

The inbound frame codec that decodes server_reply.

Approval Protocol

The elicitation surface the blocked turn calls into.

Gateway Operator Scopes

Where server_reply sits in the scope table, and the approval escalation.

Interactive Approval

The terminal-side sibling capability.

Gateway Scoped Approvals

Durable allow-always grants that compose with an approval-kind reply.