v6.0.0
Added
-
The protocol SPI is frozen at v1 (#554):
DeckGateway,Binding/BindingInfo,
GatewayError/GatewayFailureCode,HttpEndpoint/StdioEndpoint, andExposure
(deck.serve/deck.asgi/deck.expose) are stable. A plugin author builds against them without
an SPI major bump until one of those types breaks; a new capability name or a new binding is not
a version bump (docs/design/protocols/spi.md). -
agentdeck.bindings, the protocol SPI (#545):DeckGateway(targets(),
capabilities,start/get_run/list_runs),GatewayError/GatewayFailureCode,Binding,
BindingInfo,HttpEndpoint/StdioEndpoint. No concrete binding ships yet; this is the
contract the first one (#548) builds against. -
Deck.expose(*bindings) -> Exposure(#546): validates duplicate HTTP paths, more than one
stdio binding, a repeated binding name, an unsupportedspi_version, and a missing prerequisite
binding, all before anything opens.exposure.asgi()mounts every
HttpEndpointon one Starlette app with the lifecycle bound to its lifespan;
exposure.serve(host=, port=)runs standalone, closing the Deck only if it opened it. A failed
start()on binding N stops N..1 in reverse, N included, and raises.Deck.is_openis new too. -
Native.http(), the AgentDeck protocol (#548):from agentdeck.bindings import Native, thendeck.serve(Native.http()). Ten routes overDeckGatewayand publicRun
methods (targets, start, get/list runs, an SSE tail withLast-Event-ID/from_seqreconnect,
cancel/pause/resume, pending/answer), frames asEvent.model_dump_json()verbatim, and a
versioned wire spec atdocs/design/protocols/native-wire.md. The implementation lives under
agentdeck/adapters/bindings/native/;adaptersis not a user import path. -
RunStatusis exported fromagentdeck.deck.runs.list(status=...)takes one and every
Native run summary reports one, so a caller reading runs needs the type. -
InputError(#579): a publicAgentdeckErrorfor content or an answer the caller supplied
that AgentDeck cannot take, raised bycoerce_inputand by an answer outside an ask's own
options. A binding maps it to its own bad-request code (Native: 422); an unrelated
TypeError/ValueErrorfrom a store or an executor stays internal. CatchInputError(or
AgentdeckError) where you caughtTypeErrororValueErrorfrom those calls before. -
AGUI.http(), the AG-UI protocol binding (#595/#596, slices AGUI-0 through AGUI-3):
pip install agentdeck-sdk[agui], thenfrom agentdeck.bindings import AGUIand
deck.serve(AGUI.http("/agui"), port=8000)for the whole catalog, orAGUI.http( "/support", target="Support")pinned. Officialag-ui-protocolmodels andEventEncoderover HTTP/SSE:
target routing viaforwardedProps.agentdeck.target, text/reasoning/backend-tool projection,
HITL ask/resume through the officialRunFinishedInterruptOutcome/resumeshapes, multimodal
input, and a client disconnect mapped toRun.cancel(). Frontend tools, shared state, and
steering are refused by name until their AgentDeck primitives exist
(docs/design/protocols/agui.md). -
A terminal chat example needing no API key (
examples/chat-in-the-terminal/): one
@workflowasking the questions, driven byagentdeck chat. The first example that runs with
no credentials at all. -
Terminal.stdio(), the first surface (#549):from agentdeck.bindings import Terminal, thendeck.serve(Terminal.stdio(target="Research")). One session per process over
stdin and stdout: prompts, streams the run's text back, renders a numbered prompt for
ctx.askand re-asks on a refused answer, and cancels an in-flight run on Ctrl-C.
agentdeck chat [TARGET]runs it, whereTARGETis any agent or workflow in the deck and may
be omitted when the deck holds exactly one. -
A run event stream example (#396,
examples/run-events-stream/): iterates
deck.stream(...)and prints every event kind and payload in order. Scripts the model with
agentdeck.testing, so it needs no API key and prints one fixed output. -
Deck.serve(*bindings, host=, port=)andDeck.asgi(*bindings)(#606): the front door,
one-line delegations toexpose(*bindings).serve()/.asgi().agentdeck.bindingsnow
lazily exportsNativeandTerminaltoo, sofrom agentdeck.bindings import Native, Terminal
works without importing either module until named.expose()is the lower-level call, returning
theExposureobject itself.
Removed
-
The v1 HTTP wire is gone (#550):
agentdeck/serve.py, all ofagentdeck/surfaces/, the
agentdeck-serveconsole script,Deck.asgi()and the byte-for-byte goldens under
tests/golden/. A Deck no longer serves itself: expose a binding.# before app = Deck.from_project().asgi() # agentdeck-serve # after app = Deck.from_project().asgi(Native.http())
The v1 routes (
/agents/{name}/chat,/v2/invocables/{name}/chat,/health) are replaced by
the native wire:
POST /runs,GET /runs/{run_id}/events,GET /targets. The[serve]extra now installs
starlette rather than fastapi, which nothing inagentdeck/imports any more.
Changed
-
Deck.serve()is synchronous and blocking (#623), breaking versus the unreleased #606
shape: it owns the event loop (asyncio.runinternally) instead of returning a coroutine, so
it runs from plain application code with noasyncio.run(deck.serve(...))of your own. The
previous coroutine is nowDeck.serve_async(), for a caller already running inside asyncio.
Ctrl-C stops the server and returns quietly, matchinguvicorn.run;serve_async()leaves its
ownKeyboardInterrupthandling to the caller.agentdeck chatcallsdeck.serve(...)
directly.Deck.asgi()is unchanged. -
DeckGateway.startdropscontext(#599): no binding ever passed one, and a served run
has no live Python execution context to hand it.DeckGatewayno longer stores or directly
exposes theDeckit was built from. -
agentdeck.errorsis the one import path for the error taxonomy.AgentdeckError,
ConfigError,ContextTypeError,NotFoundError,SessionBusyError,SkillErrorand
StoreErrorare no longer exported fromagentdeckitself:from agentdeck.errors import AgentdeckErrorfor a catch-all.agentdeck.errorshas always carried the complete set. -
Eventis exported fromagentdeck.run.events()yields them, so a caller that reads a
run needs the type; it was already documented as a public import for binding authors.The root keeps the everyday vocabulary, a feature namespace keeps its own, and no public name
lives at two paths (docs/engineering/architecture.md3). -
InterruptReasondrops"approval"(#468): nothing ever produced it -RunInterrupted
is built in one place withreason="human"hardcoded, and refusal always came from
payload["options"], never fromreason.PendingRun.reason's docstring now states that rule
instead of an approval check that didn't exist.
Fixed
- A
@toolis never a top-level target (#488): a.agentdeck/workflows/<name>/workflow.py
exporting a@toolalongside its@workflow(e.g. soctx.invokecan reach it) used to
sweep the tool intodeck.workflowstoo, as a runnable entry it never was.deck.workflows,
deck.run/.stream,Runs.startandGET /targetsno longer show or accept it by name (a
directdeck.run("the_tool", ...)now raisesInputError, 422 over HTTP);ctx.invoke
from inside a workflow keeps working. A workflow bundle contributing no@workflowat all
still names what it found ("workflow bundle 'x' exports no workflow; found tool 'y'").
Breaking: a code-firstDeck(workflows=[a_tool])naming no@workflowthat could
ctx.invokeit now raisesConfigErrorat construction instead of silently becoming a
runnable-by-name entry. - A workflow's own
InputErrorafterrun.startedreaches every wire as itself (#621):
RunFailed.error_codegains"invalid_input", and itsmessagecarries the exception's own
caller-safe text instead of being stripped to"InputError in engine ...". Every other
exception's shape is unchanged. Deck.__aenter__rolls back a late failure (#572): a failure after observers started - building the runtime, connecting MCP servers - used to leave every started observer open and the process claim held, blocking a secondDeck(...)in the same process. It now closes what it started and releases the claim before re-raising, same as an observer-start failure already did.- A workflow's input mapping that doesn't match its declared parameters raises
InputError,
notConfigError(#583). It is caller-supplied content the target can't take, not a
configuration fault. A same-process caller now catchesInputErrorinstead ofConfigError;
_map_failurealready mapsInputErrortoINVALID_INPUT.