Troubleshooting
What AgentDeck raises when the fault is in the configuration or the call, and what to do about it. Defects in the release, where the fault is ours, are on Known Issues.
Every error below is in agentdeck.errors and every one is an AgentdeckError, so one except
catches the lot while the specific classes stay catchable on their own.
from agentdeck.errors import ConfigError, InputError, SessionBusyErrorAt build time
These raise from Deck(...) or Deck.build(), before anything runs.
| Error | What it means | What to do |
|---|---|---|
ConfigError | Invalid or incomplete configuration: a duplicate target name, a workflow bundle exporting no @workflow | The message names what collided or what was missing; fix the declaration it points at |
ContextTypeError | A tool declares ToolCtx[T] the deck's context= cannot satisfy | Give the deck the context type the tool asks for, or drop the annotation |
NotFoundError | An unknown agent, workflow or skill name, such as an unresolved @handoff target | The message lists the available names |
SkillError | A skill failed to load or execute | Read the SKILL.md frontmatter it names |
When exposing a deck
A Deck is valid on its own; a set of bindings is checked when you put them together, so these
raise from deck.expose(...) or deck.serve(...) rather than from Deck(...).
| Error | What it means | What to do |
|---|---|---|
ConfigError | Two bindings sharing a name, a binding declaring an SPI version this release does not support, a binding requiring one that is not in the exposure, two bindings claiming one HTTP path, or more than one stdio binding | The message names the collision and lists what is available |
When starting or driving a run
| Error | What it means | What to do |
|---|---|---|
InputError | Content or an answer AgentDeck cannot take: a @tool named as a top-level target, an input shape a workflow's parameters do not accept, an answer a ctx.ask() refuses | Caller-side fix; over a binding this is a 4xx, not a 500 |
SessionBusyError | Another run already holds this session_id, and sessions serialize turns | await the first run, cancel() it, or use a different session_id |
DuplicateKeyError | A run already claims this (namespace, key) | A duplicate start refuses rather than replaying; read the existing run with deck.runs.get(key=...) |
RunStateError | The run's current state does not admit the operation, such as resuming a run that is waiting for an answer | Check await run.status() or run.can, then send the control the state allows |
RunSuspendedError | await run reached a run that stopped PAUSED or WAITING_ANSWER | Answer it with run.answer(...) or resume it, then await again |
UnsupportedControlError | A control this run can never take, rather than one its state refuses now: usually no control backend | Set AGENTDECK_CONTROL to a durable URL, for example sqlite:///./control.db |
StoreError | A durable store failed: the event log, or the control rows beside it | The message names the backend; check AGENTDECK_EVENTS and the database behind it |
Ports and processes
deck.serve() takes the port as an argument, so there is no environment variable to set:
deck.serve(Native.http("/api"), port=8080)Two bindings cannot claim one path, and only one stdio binding may run per exposure: both are
ConfigError at expose(), before the listener opens.
A second Deck(...) in the same process fails while the first holds the process claim. Close the
first, or use async with deck: so it is released on the way out.
Nothing above matches
If the behaviour is wrong rather than refused, it belongs on Known Issues, or in a new issue with a reproduction.