You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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).
{"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:
Creates a per-turnInterruptController (not shared across turns/sessions — critical, because a single Agent instance may serve overlapping turns for several sessions).
Registers the driving task in _active_turns[session_id] = (task, controller).
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).
Wraps the task in asyncio.wait_for(..., timeout=per_turn_timeout) when the timeout is > 0.
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):
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 Controlbusy_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:
<Steptitle="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>
<Steptitle="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)
{"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:
<Cardtitle="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:
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.".
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.
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.
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/ — nodocs/concepts/ edits required. Safe for AI-agent authoring per AGENTS.md §1.8.
Summary
docs/features/gateway-abort-and-timeout.mdxcurrently carries an explicit placeholder note: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 ondocs/features/bot-commands.mdx.What the SDK now ships
Source of truth:
praisonai_bot/gateway/server.pyandpraisonaiagents/gateway/config.pyat commitaa379152e8cfa1e6a09f51f8edd922a2fc78a022.1. New config key:
gateway.per_turn_timeoutAdded to
GatewayConfig(praisonaiagents/gateway/config.py):Validation:
per_turn_timeout must be >= 0 (use 0 to disable the per-turn timeout).Config surface (all layers accept it):
GatewayConfig(per_turn_timeout=60.0)gateway.per_turn_timeout: 60.0GatewayServerSchema.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:
aborthello_okhandshake now advertises the new method and event (previously excluded with# abort not implemented):3. New WebSocket frame:
abort/message_abortHandled in
_handle_client_message:{"type": "abort", "reason": "..."}or{"type": "message_abort", "reason": "..."}.OperatorScope.WRITEscope 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"}{"type": "error", "message": "Not joined to any session"}{"type": "aborted", "session_id": "<sid>"}{"type": "no_active_turn", "session_id": "<sid>"}reasondefaults to"user"when omitted.4. New portable stop command in chat:
/stoporstopHandled in the
messagebranch of_handle_client_message. Any chat client can abort the in-flight turn by sending/stop(or barestop, case-insensitive) as message content — no dedicated abort frame needed. Returns the same{"type": "aborted" | "no_active_turn", "session_id": ...}reply as the WSabortframe.5. New per-turn cooperative cancellation semantics
Every turn now runs through
_drive_turn, which:InterruptController(not shared across turns/sessions — critical, because a singleAgentinstance may serve overlapping turns for several sessions)._active_turns[session_id] = (task, controller).agent.arun/agent.achat/agent.chatascancel_token=(fallback: stampsagent.interrupt_controlleronly when the entry point predatescancel_token).asyncio.wait_for(..., timeout=per_turn_timeout)when the timeout is> 0.TimeoutErrororCancelledError, first requests interruption via the controller (cooperative), then waits up to_ABORT_GRACE_SECONDS = 5.0for the turn to unwind before hard-cancelling the task (so a syncagent.chatrunning in a worker thread has a bounded chance to finish before the queue advances).6. Normalised terminal outcome strings
_finalise_aborted_turnreturns a typed terminal message (delivered as the turn's response, not a raw traceback):"Turn cancelled: exceeded per-turn timeout.""Turn cancelled: user.""Turn cancelled: <reason>."7.
/stop-via-abort works with any clientBecause the server-side handler recognises
/stopin the message content, it now works without requiring Bot Run Controlbusy_modeon adapters that route through the gateway (Telegram, Discord, Slack, WhatsApp). The existingdocs/features/bot-commands.mdxnote about/stopneedingbusy_modefor 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 editsa) Remove the placeholder
<Note>at the top (the "will be extended once that PR merges" one). Replace with: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 outc) 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:
Set a wall-clock ceiling per turn in the gateway config. `0` (default) disables the timeout — behaviour is byte-identical to earlier releases.reasonis optional (defaults to"user") and is echoed back in the terminal turn message.A timed-out turn's terminal response is
"Turn cancelled: exceeded per-turn timeout.".e) Add a new section
## Frame shapesdocumenting the exact wire formats:message_abortis accepted as an alias fortype;reasonis 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)
{"type": "aborted", "session_id": "<sid>"}{"type": "no_active_turn", "session_id": "<sid>"}{"type": "error", "code": "insufficient_scope", "message": "insufficient scope", "required_scope": "write"}{"type": "error", "message": "Not joined to any session"}Terminal turn message
The turn's own response (delivered on the normal
messagechannel) is a typed string:"Turn cancelled: exceeded per-turn timeout."/stopor WSabort)"Turn cancelled: user.""Turn cancelled: <reason>."g) Update the "Related" CardGroup — add:
2.
docs/features/bot-commands.mdx— small clarifier under/stopAdd a short paragraph inside the existing
### /stopsection, right after the current<Note>aboutbusy_mode:User interaction flow
Real scenarios the finalised page should let a reader pattern-match to:
/stop. Server-side/stophandler triggers_abort_active_turn, controller fires, turn unwinds within ~5s, user sees"Turn cancelled: user.".{"type": "abort", "reason": "operator-timeout"}. Gateway replies{"type": "aborted", "session_id": "..."}and the session's next turn appears in the queue.gateway.per_turn_timeout: 120.0in 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.Agentinstance serves ten Telegram users. User A hits/stopon their conversation. User B's parallel turn keeps running untouched — the controllers are per-turn, not per-agent.Acceptance criteria
<Note>at the top ofgateway-abort-and-timeout.mdxis removed and replaced with a "Available since PR #3472" note.per_turn_timeoutfield is fully documented with type (float), default (0.0= disabled), and validation rule (>= 0).abortframe shape is documented with the WRITE scope requirement and every possible reply (aborted,no_active_turn, botherrorshapes)./stopandstop(case-insensitive) are documented as portable message-channel cancellation."Turn cancelled: exceeded per-turn timeout."and"Turn cancelled: <reason>.")._ABORT_GRACE_SECONDS) is called out.docs/features/bot-commands.mdx/stopsection carries a<Tip>noting the gateway now provides/stopwithout needingbusy_modeon gateway-routed bots.#8B0000/#189AB4/#10B981/#F59E0B/#6366F1) with white text and#7C90A0strokes, perAGENTS.md§3.1.Layer placement / folder rules
docs/features/— nodocs/concepts/edits required. Safe for AI-agent authoring perAGENTS.md§1.8.References
/stopdocs (bot-commands.mdx#stopanchor): https://github.com/MervinPraison/praisonaidocs/blob/main/docs/features/bot-commands.mdxaa379152e8cfa1e6a09f51f8edd922a2fc78a022):praisonaiagents/gateway/config.py::GatewayConfig.per_turn_timeoutpraisonai_bot/gateway/server.py::_handle_client_message(abort branch,/stopbranch)praisonai_bot/gateway/server.py::_drive_turn,_abort_active_turn,_settle_cancelled_turn,_finalise_aborted_turnpraisonai_bot/gateway/server.py::_ABORT_GRACE_SECONDS = 5.0praisonai_bot/bots/_config_schema.py::GatewayServerSchema.per_turn_timeout