> ## Documentation Index
> Fetch the complete documentation index at: https://praison.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Gateway Turn Executor

> Place each session's turn on its own worker so a wedged turn is torn down scoped to that session

The turn executor decides *where* a session's agent turn runs, so a wedged, runaway, or crashed turn is contained to its own worker instead of taking down every session on the gateway.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    S1[👤 Session A] --> P1[📍 place]
    S2[👤 Session B] --> P2[📍 place]
    P1 --> W1[🖥️ Worker A]
    P2 --> W2[🖥️ Worker B<br/>wedges 🔥]
    W1 --> R1[✅ result]
    W2 --> WW[⚠️ WorkerWedgedError]
    WW --> T[🧹 teardown]
    T --> P2b[📍 re-place<br/>epoch+1]

    classDef session fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef process fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef result fill:#10B981,stroke:#7C90A0,color:#fff
    classDef warn fill:#F59E0B,stroke:#7C90A0,color:#fff

    class S1,S2 session
    class P1,P2,P2b,T process
    class W1,R1 result
    class W2,WW warn
```

## Quick Start

**1. Default — start an agent, the gateway calls the in-process executor**

Set no executor and the gateway drives every turn through `InProcessTurnExecutor`, running on the current event loop exactly as today.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent
from praisonai_bot.gateway import WebSocketGateway

# No executor set → gateway uses InProcessTurnExecutor by default.
# Every turn runs on the current loop, byte-for-byte compatible.
agent = Agent(name="Support", instructions="Help users")
WebSocketGateway(agent=agent).start()
```

**2. Confirm the active executor from to\_dict()**

Read the active executor off the config — the same string `gateway doctor` reports.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import GatewayConfig

GatewayConfig().to_dict()["executor"]   # → "inprocess"
```

**3. Plug in an isolated executor via GatewayConfig**

Pass any object satisfying `TurnExecutorProtocol` to `GatewayConfig.executor`; the gateway resolves it once at start-up.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent, GatewayConfig
from praisonaiagents.gateway import InProcessTurnExecutor
from praisonai_bot.gateway import WebSocketGateway

# Any object satisfying TurnExecutorProtocol works here.
config = GatewayConfig(executor=InProcessTurnExecutor())
gateway = WebSocketGateway(agent=Agent(name="Support", instructions="Help"),
                           config=config)
gateway.start()

config.to_dict()["executor"]   # → "InProcessTurnExecutor"
```

Reach for a concrete isolated executor (subprocess / container / remote) when you need blast-radius containment — see [Common Patterns](#common-patterns).

***

## How It Works

The gateway asks the executor to `place` a session, runs the turn on that placement, and tears the placement down only when its worker wedges — never the whole process.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant G as Gateway
    participant E as TurnExecutor
    participant W as Worker

    G->>E: place(session_id)
    E-->>G: TurnPlacement(worker_id, epoch)
    G->>E: execute_turn(placement, turn, cancel_token, limits)
    E->>W: run turn
    alt worker makes progress
        W-->>E: result
        E-->>G: result
    else worker wedges
        W-->>E: no progress
        E-->>G: WorkerWedgedError
        G->>E: teardown(placement, reason)
        Note over G,E: re-place session with epoch+1
    end
```

| Piece | Owner | Role |
| - | - | - |
| `TurnExecutorProtocol` | Core (pure) | Async `place` / `execute_turn` / `teardown` contract for *where* a turn runs |
| `TurnPlacement` | Core (pure) | Frozen handle: which worker owns a session's turns, and at what `epoch` |
| `InProcessTurnExecutor` | Core (pure) | Default — runs each turn on the current loop, byte-for-byte compatible |
| `WorkerWedgedError` | Core (pure) | Scoped fault signal — tear down this placement, re-place the session |

`InProcessTurnExecutor` is the default and reproduces today's on-loop behaviour exactly: leaving the executor unset changes nothing and adds no dependency.

***

## How the gateway drives it

`WebSocketGateway` resolves the executor once at start-up, then routes every turn through `place()` → `execute_turn()`.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant User
    participant GW as WebSocketGateway
    participant EX as executor
    participant W as Worker

    User->>GW: message
    GW->>EX: place(session_id)
    EX-->>GW: TurnPlacement
    GW->>EX: execute_turn(placement, turn, cancel_token, limits)
    EX->>W: run turn
    alt worker replies
        W-->>EX: result
        EX-->>GW: result
        GW-->>User: reply (outcome=ok)
    else worker wedges
        EX-->>GW: WorkerWedgedError
        GW->>EX: teardown(placement, reason="wedged")
        GW-->>User: worker wedged; session re-placed (outcome=error)
    end
```

| Where | What it does |
| - | - |
| `GatewayConfig.executor` | Selects the executor. `None` ⇒ `InProcessTurnExecutor`. |
| `WebSocketGateway.__init__` | Resolves `self._executor = config.executor or InProcessTurnExecutor()`. |
| `WebSocketGateway._drive_turn` | Every turn goes through `executor.place()` → `executor.execute_turn()`. |
| `WorkerWedgedError` handler | Tears down **only that session's** worker (scoped, not process-wide). |
| Session queue outcome | Wedged turn reports `outcome_status="error"`, not the default `"ok"`. |
| `GatewayConfig.to_dict()["executor"]` | `"inprocess"` by default; the executor class name when explicit. |

> [!NOTE]
>
> Before the seam was wired, the gateway ran turns directly on the event loop and a wedged turn triggered a process-wide `os._exit`. Now `WebSocketGateway._drive_turn` routes through the configured executor, and a `WorkerWedgedError` tears down only the offending session's worker.

***

## The Contract

`TurnExecutorProtocol` is a runtime-checkable protocol with three async methods.

| Method | Signature | Does |
| - | - | - |
| `place` | `place(session_id: str) -> TurnPlacement` | Returns the placement that owns this session's turns; may bump `epoch` when the worker is replaced |
| `execute_turn` | `execute_turn(placement, turn, *, cancel_token=None, limits=None) -> Any` | Runs `turn` on `placement` and returns its result; raises `WorkerWedgedError` if the worker can't make progress |
| `teardown` | `teardown(placement, *, reason: str) -> None` | Reclaims the placement's worker, scoped to the owning session only |

`TurnPlacement` is a frozen dataclass — mutating any field raises `dataclasses.FrozenInstanceError`.

| Field | Type | Default | Description |
| - | - | - | - |
| `session_id` | `str` | — | The resolved session whose turns this placement serves |
| `worker_id` | `str` | — | Opaque id of the worker that owns the session's turns; a constant (`"inprocess"`) for the in-process default |
| `epoch` | `int` | `0` | Monotonic generation bumped when the backing worker is replaced — the fencing token; a turn carrying a stale epoch must not run |

`WorkerWedgedError` is a plain `Exception` subclass. It is **scoped**: the gateway tears down only the offending placement and re-places its session — never a process-wide `os._exit`. Ordinary turn errors surface as the turn's own exception and do not condemn the worker.

> [!NOTE]
>
> An isolated executor does **not** ship the live `turn` callable (and its captured loop/agent state) across a process boundary. The worker owns the session via `place()` and rebuilds the turn from serialisable inputs on its own side; the passed `turn` is the gateway-side await point for the worker's result.

***

## Configuration Options

The seam is configured through `GatewayConfig.executor` (defaults to `None`, which resolves to `InProcessTurnExecutor` at runtime). `GatewayConfig.to_dict()["executor"]` reports the active executor for `gateway doctor` / observability.

> [card]
>
> `GatewayConfig` — the `executor` field + full gateway config.
>
> [Open](https://praison.ai/docs/sdk/reference/praisonaiagents/classes/GatewayConfig.md)

> [card]
>
> `TurnExecutorProtocol` — auto-generated SDK reference.
>
> [Open](https://praison.ai/docs/sdk/reference/praisonaiagents/classes/TurnExecutorProtocol.md)

> [card]
>
> `TurnPlacement` — auto-generated SDK reference.
>
> [Open](https://praison.ai/docs/sdk/reference/praisonaiagents/classes/TurnPlacement.md)

> [card]
>
> `InProcessTurnExecutor` — auto-generated SDK reference.
>
> [Open](https://praison.ai/docs/sdk/reference/praisonaiagents/classes/InProcessTurnExecutor.md)

> [card]
>
> `WorkerWedgedError` — auto-generated SDK reference.
>
> [Open](https://praison.ai/docs/sdk/reference/praisonaiagents/classes/WorkerWedgedError.md)

> [!NOTE]
>
> `limits` on `execute_turn` is a typed `Any` today, accepted for protocol symmetry. It is inert in-process and honoured only by isolated executors — there is no `limits=` config class in core.

***

## Common Patterns

Three shapes cover almost every use.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
# 1. Default — no code needed. Leaving the executor unset runs turns on the
#    current loop exactly as today, byte-for-byte backward compatible.
from praisonaiagents.gateway import InProcessTurnExecutor

executor = InProcessTurnExecutor()
```

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
# 2. A wrapper's isolated executor (sketch — user code, not shipped by core).
from praisonaiagents.gateway import TurnPlacement

class SubprocessTurnExecutor:  # illustrative — your wrapper implements this
    async def place(self, session_id: str) -> TurnPlacement:
        worker_id = await self._spawn(session_id)
        return TurnPlacement(session_id=session_id, worker_id=worker_id, epoch=0)

    async def execute_turn(self, placement, turn, *, cancel_token=None, limits=None):
        return await self._dispatch(placement, cancel_token, limits)

    async def teardown(self, placement, *, reason: str) -> None:
        await self._kill(placement.worker_id)
```

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
# 3. Handle a wedged worker — tear down, then re-place with a bumped epoch.
from praisonaiagents.gateway import WorkerWedgedError, TurnPlacement

async def run_with_recovery(executor, session_id, turn):
    placement = await executor.place(session_id)
    try:
        return await executor.execute_turn(placement, turn)
    except WorkerWedgedError:
        await executor.teardown(placement, reason="wedged")
        fresh = await executor.place(session_id)  # new worker, epoch+1
        return await executor.execute_turn(fresh, turn)
```

***

## Best Practices

> [accordion] Fence with epoch, always
>
> `epoch` is the fencing token. Bump it whenever a session's worker is replaced, and refuse any turn carrying a stale epoch — that is what stops a reclaimed worker from executing against a session it no longer owns.

> [accordion] Keep teardown scoped to one session
>
> `teardown` reclaims a single placement's worker. A worker fault must fail scoped: tear down the offending placement and re-place its session, never the whole gateway. That is the whole point of the seam versus a process-wide `os._exit`.

> [accordion] Reach for isolation only when you need blast-radius containment
>
> The in-process default is zero-cost and dependency-free but offers no isolation — a turn that wedges the loop still affects the process. Switch to an isolated executor (subprocess / container / remote) when one bad session must not degrade the rest.

> [accordion] Never cross a boundary with a live callable
>
> An isolated worker owns the session via `place()` and rebuilds the turn from serialisable inputs on its own side. Do not try to send the live `turn` callable across a process boundary — treat it as the gateway-side await point for the worker's result.

> [accordion] Leave executor=None unless you need isolation
>
> The default `GatewayConfig(executor=None)` resolves to `InProcessTurnExecutor` — today's on-loop behaviour, no dependency, no cost. Only set `executor=` when you need blast-radius containment (subprocess / container / remote) or per-session limits. Verify what actually resolved with `config.to_dict()["executor"]` — that same string is what `gateway doctor` prints.

***

## Related

> [card] [Gateway Turn Lock](https://praison.ai/docs/features/gateway-turn-lock.md)
>
> Sibling primitive — serialises turns per session across replicas.

> [card] [Gateway Loop Watchdog](https://praison.ai/docs/features/gateway-loop-watchdog.md)
>
> Detects a wedged event loop.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.