Skip to content

docs: finalise gateway-abort-and-timeout page now that PR #3472 has merged #2490

Description

@MervinPraison

Summary

docs/features/gateway-abort-and-timeout.mdx currently carries an explicit placeholder note:

The wire-level message_abort event and the InterruptController primitive shown here are present in the SDK today. The gateway-side /stop chat command, the abort WebSocket frame, and the gateway.per_turn_timeout config key are delivered by PraisonAI#3472; this page documents the verified contract and will be extended with the exact per_turn_timeout values and terminal-frame shape once that PR merges.

PR #3472 was merged on 2026-07-29 (head SHA aa379152e8cfa1e6a09f51f8edd922a2fc78a022). The page now needs to be updated with the actual merged contract: remove the "will be extended" caveat and document the real shapes.

Type: content update on one existing page (docs/features/gateway-abort-and-timeout.mdx), plus a small clarifier on docs/features/bot-commands.mdx.


What the SDK now ships

Source of truth: praisonai_bot/gateway/server.py and praisonaiagents/gateway/config.py at commit aa379152e8cfa1e6a09f51f8edd922a2fc78a022.

1. New config key: gateway.per_turn_timeout

Added to GatewayConfig (praisonaiagents/gateway/config.py):

# Issue #3467: per-turn wall-clock ceiling. When > 0, a single agent turn
# that runs longer than this many seconds is cancelled (cooperatively via
# the agent's interrupt controller and by cancelling the driving task) so a
# runaway turn cannot wedge the serial per-session queue. 0 = no timeout
# (today's behaviour: a turn runs to completion).
per_turn_timeout: float = 0.0

Validation: per_turn_timeout must be >= 0 (use 0 to disable the per-turn timeout).

Config surface (all layers accept it):

Layer Key
Python GatewayConfig(per_turn_timeout=60.0)
YAML (multi-channel gateway config) gateway.per_turn_timeout: 60.0
Bot pydantic schema GatewayServerSchema.per_turn_timeout: Optional[float] = Field(None, ge=0)

Default is 0.0 = disabled (behaviour is byte-identical to earlier releases when unset).

2. New WebSocket capability: abort

hello_ok handshake now advertises the new method and event (previously excluded with # abort not implemented):

features = {
    "methods": ["message", "leave", "abort"],
    "events": [
        EventType.MESSAGE.value,     # "message"
        EventType.ERROR.value,       # "error"
        EventType.MESSAGE_ABORT.value,  # "message_abort"
    ],
}

3. New WebSocket frame: abort / message_abort

Handled in _handle_client_message:

  • Client sends {"type": "abort", "reason": "..."} or {"type": "message_abort", "reason": "..."}.
  • Requires the OperatorScope.WRITE scope on the client — same scope as sending a message. A client without it receives:
    {"type": "error", "code": "insufficient_scope", "message": "insufficient scope", "required_scope": "write"}
  • On a client not currently joined to a session:
    {"type": "error", "message": "Not joined to any session"}
  • On success:
    • If a turn was active: {"type": "aborted", "session_id": "<sid>"}
    • If no turn was active: {"type": "no_active_turn", "session_id": "<sid>"}
  • reason defaults to "user" when omitted.

4. New portable stop command in chat: /stop or stop

Handled in the message branch of _handle_client_message. Any chat client can abort the in-flight turn by sending /stop (or bare stop, case-insensitive) as message content — no dedicated abort frame needed. Returns the same {"type": "aborted" | "no_active_turn", "session_id": ...} reply as the WS abort frame.

5. New per-turn cooperative cancellation semantics

Every turn now runs through _drive_turn, which:

  1. Creates a per-turn InterruptController (not shared across turns/sessions — critical, because a single Agent instance may serve overlapping turns for several sessions).
  2. Registers the driving task in _active_turns[session_id] = (task, controller).
  3. Passes the controller into agent.arun / agent.achat / agent.chat as cancel_token= (fallback: stamps agent.interrupt_controller only when the entry point predates cancel_token).
  4. Wraps the task in asyncio.wait_for(..., timeout=per_turn_timeout) when the timeout is > 0.
  5. On TimeoutError or CancelledError, first requests interruption via the controller (cooperative), then waits up to _ABORT_GRACE_SECONDS = 5.0 for the turn to unwind before hard-cancelling the task (so a sync agent.chat running in a worker thread has a bounded chance to finish before the queue advances).

6. Normalised terminal outcome strings

_finalise_aborted_turn returns a typed terminal message (delivered as the turn's response, not a raw traceback):

  • Timeout: "Turn cancelled: exceeded per-turn timeout."
  • User/WS abort: "Turn cancelled: user."
  • Any other reason string: "Turn cancelled: <reason>."

7. /stop-via-abort works with any client

Because the server-side handler recognises /stop in the message content, it now works without requiring Bot Run Control busy_mode on adapters that route through the gateway (Telegram, Discord, Slack, WhatsApp). The existing docs/features/bot-commands.mdx note about /stop needing busy_mode for mid-run interruption is still accurate for bots that don't route through the gateway path, but should mention the gateway now provides a portable fallback.


Docs pages to update

1. docs/features/gateway-abort-and-timeout.mdx — main edits

a) Remove the placeholder <Note> at the top (the "will be extended once that PR merges" one). Replace with:

<Note>
Available since [PraisonAI#3472](https://github.com/MervinPraison/PraisonAI/pull/3472). The gateway advertises `abort` in the `hello_ok` capability handshake, accepts `abort` / `message_abort` WebSocket frames, honours `/stop` (or `stop`) in chat, and enforces a configurable per-turn timeout via `gateway.per_turn_timeout`.
</Note>

b) Rewrite the hero Mermaid to also show the terminal outcome and the WRITE scope:

graph LR
    S[/stop chat] --> H[Session Handler]
    W[abort WS frame<br/>WRITE scope] --> H
    T[per_turn_timeout] --> H
    H --> IC[Per-turn<br/>InterruptController]
    IC -->|cooperative<br/>request| A[Agent turn]
    A -->|unwinds<br/>≤ 5s| Fin[Turn cancelled: ...]
    Fin --> F[message_abort frame]
    F --> R[client render]

    classDef in fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef proc fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef cfg fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef out fill:#10B981,stroke:#7C90A0,color:#fff

    class S,W,T in
    class H,IC,A proc
    class Fin,F,R out
Loading

c) Rewrite Quick Start Step 2 — the current "Request cancellation" step calls controller.request() directly, which is the SDK primitive. Add two more real-world steps that use the merged surface:

<Step title="Stop a turn from chat">
Any chat client can cancel the in-flight turn by sending `/stop` (or bare `stop`, case-insensitive). Reply is either `{"type":"aborted","session_id":"..."}` or `{"type":"no_active_turn","session_id":"..."}`.
</Step>

<Step title="Stop a turn from a WebSocket / operator client">
Send an `abort` frame — requires the `write` operator scope (same as sending a message as the agent):

```json
{"type": "abort", "reason": "user"}

reason is optional (defaults to "user") and is echoed back in the terminal turn message.

Set a wall-clock ceiling per turn in the gateway config. `0` (default) disables the timeout — behaviour is byte-identical to earlier releases.
gateway:
  per_turn_timeout: 60.0   # cancel any turn that runs longer than 60s

A timed-out turn's terminal response is "Turn cancelled: exceeded per-turn timeout.".


**d) Add a new section `## Config: gateway.per_turn_timeout`** with the field table:

```mdx
---

## Config: `gateway.per_turn_timeout`

Wall-clock ceiling for a single agent turn. When exceeded, the turn is cancelled cooperatively (via its `InterruptController`) and, if it does not unwind within a bounded grace window, the driving task is cancelled hard.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `per_turn_timeout` | `float` (seconds) | `0.0` | Wall-clock ceiling per turn. `0` disables the timeout entirely. A negative value raises `ValueError` at config load. |

Three ways to set it:

```python
from praisonaiagents.gateway.config import GatewayConfig

config = GatewayConfig(per_turn_timeout=60.0)
# multi-channel gateway config
gateway:
  per_turn_timeout: 60.0
# via the bot pydantic schema — same key
from praisonai_bot.bots._config_schema import GatewayServerSchema

GatewayServerSchema(per_turn_timeout=60.0)
Default `0.0` = disabled. Every turn runs to completion, exactly as in releases before #3472. ```

e) Add a new section ## Frame shapes documenting the exact wire formats:

---

## Frame shapes

### Client → Gateway

**Abort frame** — requires the `write` operator scope:

```json
{"type": "abort", "reason": "user"}

message_abort is accepted as an alias for type; reason is optional and defaults to "user".

Portable stop command — any client already joined to a session can cancel via the message channel:

{"type": "message", "content": "/stop"}

stop (no slash, case-insensitive) is also accepted.

Gateway → Client (reply to abort)

Reply When
{"type": "aborted", "session_id": "<sid>"} A turn was active and cancellation was signalled.
{"type": "no_active_turn", "session_id": "<sid>"} The client is joined but nothing is currently running.
{"type": "error", "code": "insufficient_scope", "message": "insufficient scope", "required_scope": "write"} The client is missing the WRITE scope.
{"type": "error", "message": "Not joined to any session"} The client hasn't joined a session.

Terminal turn message

The turn's own response (delivered on the normal message channel) is a typed string:

Reason Response text
Per-turn timeout expired "Turn cancelled: exceeded per-turn timeout."
User abort (chat /stop or WS abort) "Turn cancelled: user."
Any other reason string "Turn cancelled: <reason>."

**f) Replace the "Why cancellation is cooperative" and "One controller per turn surface" accordions** with a more accurate accordion group reflecting the merged design:

```mdx
<AccordionGroup>
  <Accordion title="Cooperative-first with a bounded grace window">
    Cancellation always requests interruption via the turn's `InterruptController` first, so the agent stops at its next safe checkpoint and partial output is preserved. A hard `task.cancel()` fires only if the turn hasn't unwound within **5 seconds** (`_ABORT_GRACE_SECONDS`) — this bounds the case where a sync `agent.chat` running in a worker thread cannot be force-killed but must not keep mutating shared state after the queue advances.
  </Accordion>
  <Accordion title="One controller per turn, not per agent">
    Each turn creates its own `InterruptController` and passes it as `cancel_token=` into `arun` / `achat` / `chat`. Overlapping turns from different sessions never share a controller, so one session's `/stop` never interrupts another's turn — even when they share the same `Agent` instance.
  </Accordion>
  <Accordion title="Legacy entry-point fallback">
    Agent entry points that predate `cancel_token=` fall back to stamping `agent.interrupt_controller` on the shared attribute for the turn's duration. This is best-effort and non-isolated — prefer keeping agents on the current SDK so per-turn isolation applies.
  </Accordion>
  <Accordion title="Backward compatible: unset timeout preserves today's behaviour">
    `per_turn_timeout` defaults to `0.0`, which means no wall-clock cancellation. Every turn runs to completion, exactly as in releases before #3472 — enabling the timeout is opt-in.
  </Accordion>
</AccordionGroup>

g) Update the "Related" CardGroup — add:

<Card title="Bot Chat Commands: /stop" icon="octagon-pause" href="/docs/features/bot-commands#stop">
  The chat-side twin of the gateway's abort surface.
</Card>

2. docs/features/bot-commands.mdx — small clarifier under /stop

Add a short paragraph inside the existing ### /stop section, right after the current <Note> about busy_mode:

<Tip>
When your bot routes through the gateway (Telegram, Discord, Slack, WhatsApp with `praisonai_bot`), `/stop` is handled server-side by the gateway's abort path introduced in [PraisonAI#3472](https://github.com/MervinPraison/PraisonAI/pull/3472) — it cancels the in-flight turn cooperatively without requiring `busy_mode`. The `busy_mode` path is still what enables pending-message handling and mid-run steering; `/stop` alone works with any gateway-routed bot.
</Tip>

User interaction flow

Real scenarios the finalised page should let a reader pattern-match to:

  1. Telegram user sends a heavy request; the agent gets stuck in a slow tool call. User types /stop. Server-side /stop handler triggers _abort_active_turn, controller fires, turn unwinds within ~5s, user sees "Turn cancelled: user.".
  2. Operator dashboard (WebSocket client with WRITE scope) shows a runaway turn. Operator sends {"type": "abort", "reason": "operator-timeout"}. Gateway replies {"type": "aborted", "session_id": "..."} and the session's next turn appears in the queue.
  3. Ops team sets gateway.per_turn_timeout: 120.0 in the gateway YAML. A wedged turn auto-cancels after 2 minutes with "Turn cancelled: exceeded per-turn timeout." — no operator intervention needed, and the serial session queue keeps moving.
  4. Multi-session gateway where one Agent instance serves ten Telegram users. User A hits /stop on their conversation. User B's parallel turn keeps running untouched — the controllers are per-turn, not per-agent.

Acceptance criteria

  • The placeholder <Note> at the top of gateway-abort-and-timeout.mdx is removed and replaced with a "Available since PR #3472" note.
  • per_turn_timeout field is fully documented with type (float), default (0.0 = disabled), and validation rule (>= 0).
  • Wire-level abort frame shape is documented with the WRITE scope requirement and every possible reply (aborted, no_active_turn, both error shapes).
  • /stop and stop (case-insensitive) are documented as portable message-channel cancellation.
  • Terminal outcome strings are quoted exactly ("Turn cancelled: exceeded per-turn timeout." and "Turn cancelled: <reason>.").
  • The 5-second cooperative grace window (_ABORT_GRACE_SECONDS) is called out.
  • docs/features/bot-commands.mdx /stop section carries a <Tip> noting the gateway now provides /stop without needing busy_mode on gateway-routed bots.
  • All Mermaid diagrams use the standard PraisonAI color scheme (#8B0000 / #189AB4 / #10B981 / #F59E0B / #6366F1) with white text and #7C90A0 strokes, per AGENTS.md §3.1.
  • Code snippets use friendly imports; no deep imports beyond what the SDK exposes at the surfaces documented.

Layer placement / folder rules

  • Both files are under docs/features/no docs/concepts/ edits required. Safe for AI-agent authoring per AGENTS.md §1.8.

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingclaudeTrigger Claude Code analysisdocumentationImprovements or additions to documentationperformance

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions