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 Reach for a concrete isolated executor (subprocess / container / remote) when you need blast-radius containment — see Common Patterns.
TurnExecutorProtocol to GatewayConfig.executor; the gateway resolves it once at start-up.How It Works
The gateway asks the executor toplace 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 throughGatewayConfig.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
Fence with epoch, always
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.Keep teardown scoped to one session
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.Reach for isolation only when you need blast-radius containment
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.
Never cross a boundary with a live callable
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.Leave executor=None unless you need isolation
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
Gateway Turn Lock
Sibling primitive — serialises turns per session across replicas.
Gateway Loop Watchdog
Detects a wedged event loop.

