Skip to main content
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.

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.
2

Confirm the active executor from to_dict()

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

Plug in an isolated executor via GatewayConfig

Pass any object satisfying TurnExecutorProtocol to GatewayConfig.executor; the gateway resolves it once at start-up.
Reach for a concrete isolated executor (subprocess / container / remote) when you need blast-radius containment — see 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. 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().
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. TurnPlacement is a frozen dataclass — mutating any field raises dataclasses.FrozenInstanceError. 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.
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.
GatewayConfig — the executor field + full gateway config.
TurnExecutorProtocol — auto-generated SDK reference.
TurnPlacement — auto-generated SDK reference.
InProcessTurnExecutor — auto-generated SDK reference.
WorkerWedgedError — auto-generated SDK reference.
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.

Best Practices

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.
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.
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.
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.
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.

Gateway Turn Lock

Sibling primitive — serialises turns per session across replicas.

Gateway Loop Watchdog

Detects a wedged event loop.