ADR-0002 — Async-first core on anyio; sync facade derived¶
Status: Accepted Date: 2026-09-19 Related: ADR-0001 Related: ADR-0020 — cancellation and teardown semantics · ADR-0022 — facade mechanism, which supersedes the lean recorded below
Context¶
Agent workloads are I/O-bound and concurrent: many sandboxes, long-running executions, streamed output. Every target backend ships an async SDK. E2B and Daytona both expose dual sync/async APIs, which users now expect.
Hand-maintaining two implementations per adapter doubles the surface that provider churn can break, and the two copies drift. anyio additionally buys trio compatibility and — more importantly here — structured concurrency and cancel scopes, which is the only sane way to guarantee teardown of a remote resource.
Decision¶
We will make the core async-first on anyio:
- Adapters implement async only. A sync adapter implementation is not a supported thing.
- The sync facade contains no adapter logic, only delegation across an anyio blocking portal. The mechanism is settled in ADR-0022: hand-written delegation with a CI-enforced parity test.
- Sync entry is explicit (
create_sync). We reject context auto-detection that returns different types from one call site: it defeats type checkers and violates one-obvious-way. - Cancellation and teardown semantics are part of the port contract, not adapter discretion, and are proven by the contract suite. Defined in ADR-0020.
Consequences¶
- Sync users pay a thread-portal hop. Acceptable: sandbox operations are network-bound and measured in hundreds of milliseconds at best.
- anyio is a real base dependency, in tension with "near-zero deps" (ADR-0004). We accept it: it is small, pure-Python, import-cheap, and the structured-concurrency guarantees are load-bearing for guaranteed teardown.
- Tracebacks crossing the portal are worse than native ones. The facade must not swallow
__cause__, and the contract suite checks error identity through the sync path too. - Any adapter that blocks the event loop (a sync provider SDK called directly) is a bug, not
a style issue. Adapters wrapping sync SDKs must go through
anyio.to_thread.