/stop, an abort WebSocket frame, or a configurable per-turn timeout — or stop the whole session (the running turn and everything queued behind it) with /stop all, /cancel, or abort carrying scope: "session".
Available since PraisonAI#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.Session-scoped stop (/stop all, /cancel, abort with scope: "session") added in PraisonAI#5130. Default remains turn-scoped for back-compat.message_abort event to clients when a turn is cancelled, so a custom client can render the turn as “cancelled” instead of hanging or leaking a raw traceback.
Pick the scope that matches what you want stopped — the running answer only, or that answer plus everything you queued behind it.
Quick Start
1
Cancel from any chat client with /stop
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":"..."}.2
Cancel the whole session with /stop all or /cancel
When the user typed several follow-ups and wants everything they queued cancelled too, a session-scoped stop cancels the in-flight turn and drains the session inbox.Reply carries the drained count:
/cancel is an alias for /stop all.3
Session-scope the abort WebSocket frame
The same
abort frame drains the backlog when you add scope: "session":scope is optional; absent or unknown falls back to TURN.4
Cancel with an abort WebSocket frame
Send an
abort frame — requires the write operator scope (same as sending a message as the agent):reason is optional (defaults to "user") and is echoed back in the terminal turn message.5
Set a per-turn wall-clock ceiling
Set a wall-clock ceiling per turn in the gateway config. A timed-out turn’s terminal response is
0 (default) disables the timeout — behaviour is byte-identical to earlier releases."Turn cancelled: exceeded per-turn timeout.".6
Render the message_abort event on the client
When the gateway cancels a turn it emits a
message_abort event; treat it as a terminal outcome for that turn.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.
Three ways to set it:
Default
0.0 = disabled. Every turn runs to completion, exactly as in releases before #3472.Stop Scopes
Two scopes.TURN stops the current answer. SESSION stops the current answer and drains everything the user queued behind it.
Both
StopScope and StopResult are exported from the public gateway surface:
StopResult.to_dict() serialises to exactly {"scope": ..., "turn_aborted": ..., "pending_cancelled": ...}. pending_cancelled is the count of queued messages a SESSION stop drained — a visible, intentional non-outcome, never a silent drop — and is always 0 for a TURN stop.
An unknown or malformed
scope never escalates to SESSION — it falls back to the caller’s default (TURN). A session drain always requires an explicit opt-in.Frame shapes
Client → Gateway
Abort frame — requires thewrite operator scope:
message_abort is accepted as an alias for type.
abort is classified as write in the gateway method registry — the same scope as sending a message, on the principle that aborting a turn mutates it exactly as sending one does.Gateway → Client (reply to abort)
The reply carries theStopResult fields alongside type and session_id.
type is "aborted" when either turn_aborted is true or pending_cancelled > 0; otherwise "no_active_turn". A SESSION stop with no active turn but 3 queued messages still replies "aborted" with pending_cancelled: 3 — the queued work counts as work stopped.
Turn-scoped reply:
Terminal turn message
The turn’s own response (delivered on the normalmessage channel) is a typed string:
The message_abort event
EventType.MESSAGE_ABORT is defined in praisonaiagents/gateway/protocols.py and serialises to the wire string "message_abort".
Because
EventType is a str Enum, EventType.MESSAGE_ABORT == "message_abort" compares True, so clients can match on the raw string without importing the enum.How It Works
Every turn runs through a per-turnInterruptController, so a /stop, an abort frame, or a timeout on one session never touches another session’s turn.
Why cancellation is cooperative
Why cancellation is cooperative
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.Fallback for older entry points
Fallback for older entry points
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.Timeout is opt-in
Timeout is opt-in
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.Default-path cooperative cancel needed PR #4301
Default-path cooperative cancel needed PR #4301
Prior to PraisonAI PR #4301, the gateway’s cooperative cancel reached the agent but the agent dropped the token before
OpenAIClient on the default sync path. So the gateway’s _ABORT_GRACE_SECONDS = 5.0 hard-cancel was the only real halt on plain Agent(llm="gpt-4o-mini"). Upgrade to get cooperative interruption between tool iterations, not just after the 5-second grace.Race-window: the dequeue-to-registration gap
Race-window: the dequeue-to-registration gap
The queue worker dequeues a message before registering it as an active turn. A
SESSION stop bumps a per-session _stop_epoch counter; the worker snapshots the epoch at dequeue and re-checks it just before dispatch. A mismatch means a stop landed in the gap — the message is dropped instead of executed, and it is counted toward pending_cancelled so the reported outcome is never a silent loss. See praisonai_bot/gateway/server.py (search dequeued_stop_epoch).Common Patterns
Real scenarios this page lets you pattern-match to:Related
Chat /stop Command
The chat-side twin of the gateway’s abort surface.
Handshake Protocol
Capability negotiation and the
hello_ok feature set.Gateway CLI
Process-level
praisonai gateway stop versus turn-level cancellation.Interactive TUI Ctrl-C
The terminal-side twin of the same cooperative primitive.

