Skip to Content
AgentDeck 6.0 is here: serve one deck over HTTP, AG-UI or the terminal.See what's new
BindingsWrite Your Own

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; the Exposure owns any task spawned here.
  • async def stop(): stop binding-owned resources; idempotent, may run without a preceding start().

BindingInfo

  • name: unique within one exposure; requires resolves 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, by name, 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; GatewayFailureCode maps 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 usemay not use
agentdeck.bindings (gateway, Binding, endpoint, failure types)agentdeck.runtime.*
agentdeck.Run, agentdeck.Eventagentdeck.adapters.*
content blocks from agentdeck (TextBlock, ImageBlock, …)agentdeck.core.*
the error taxonomy from agentdeck.errorsagentdeck.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.
Last updated on