Write your own
A binding is any object satisfying the Binding protocol. Nothing under agentdeck.adapters or
agentdeck.runtime is needed to write one; Native, Terminal and AGUI are held to the same
SPI as a plugin outside this repository.
The shape
info: BindingInfo: what this binding is, validated before anything opens.build(gateway: DeckGateway) -> Endpoint: resolve and validate; no I/O, nothing opens here.async def start(): start binding-owned resources; theExposureowns any task spawned here.async def stop(): stop binding-owned resources; idempotent, may run without a precedingstart().
BindingInfo
name: unique within one exposure;requiresresolves by it.kind:"protocol" | "channel" | "surface", data, not behavior.transport:"http","stdio", or a transport this binding defines.spi_version: see below.advertises: capability names (streaming,hitl,control.cancel, …) a contract test holds this binding to.requires: other bindings, byname, this one needs in the same exposure.
DeckGateway
The one surface a binding talks to a Deck through:
targets(): every agent and workflow in the catalog.capabilities: what varies by deployment (control,durable), never by run.start(target, input, ...),get_run(run_id, ...),list_runs(...):Runs.start/get/list, every failure mapped.GatewayError(code, message): the one exception a binding catches;GatewayFailureCodemaps onto a wire status.
Endpoints
HttpEndpoint(path, app): an isolated ASGI app or router, mounted on the shared listener.StdioEndpoint(run): a coroutine over stdin/stdout, run as a task, no port opened.
The import boundary
A plugin package is held to this by tests/bindings/fixture_plugin/.importlinter’s own forbidden-modules contract, run against the fixture plugin the same way it would run against any out-of-tree package:
| may use | may not use |
|---|---|
agentdeck.bindings (gateway, Binding, endpoint, failure types) | agentdeck.runtime.* |
agentdeck.Run, agentdeck.Event | agentdeck.adapters.* |
content blocks from agentdeck (TextBlock, ImageBlock, …) | agentdeck.core.* |
the error taxonomy from agentdeck.errors | agentdeck.deck |
from agentdeck import Deck
from agentdeck_myprotocol import MyProtocol
app = Deck.from_project().asgi(MyProtocol.http())spi_version
PROTOCOL_SPI_VERSION = 1, frozen in 6.0. A binding declaring an unsupported one is refused when
you call deck.serve() or deck.asgi(), before anything opens, naming both versions.
Prove it
A checklist generalized from the SPI contract suite, run against a channel-shaped fixture plugin since a channel is the harder pattern:
- A message reaches the Deck only through
gateway.start, and what it starts is an ordinary run. - A disconnected reader never cancels the run; the run completes on its own.
- Resume re-tails from
last_seq + 1; nothing polls. - Two bindings on one exposure share one gateway and see the same runs.
- An unknown event kind is skipped, never raised.
- Every rejection maps to a real error, never a raw exception reaching the wire.
stop()cancels every in-flight task, and still surfaces whichever failure happened first.