Definitions
Two constructors are what a project actually builds. Agents, Capabilities, and Workflows cover why each argument exists and how they compose; this page is the exhaustive one — every field and every method, with its type and default.
Agent
from agentdeck import Agent| Argument | Type | Default | Purpose |
|---|---|---|---|
base | type[AgentDeclaration] | None | None | a shareable set of defaults (see below); keyword-only |
name | str | base.name, or the base class name | registry key and SDK agent name |
instructions | str | Callable | base.instructions | the system prompt, or a callable computing it per turn |
handoff_description | str | None | base.handoff_description | how this agent describes itself to a peer deciding whether to hand off |
model | str | None | base.model | per-agent override; None uses the OPENAI_MODEL setting |
model_settings | Mapping[str, Any] | base.model_settings | forwarded to agents.ModelSettings(**model_settings) |
tools | Sequence[Any] | base.tools | plain functions (compiled at build()), already-built SDK tool objects, or a Workflow to expose as a tool |
handoffs | Sequence[Any] | base.handoffs | peer agents this one can transfer to — a registry name (str), a built Agent, or a Handoff |
output_type | type | AgentOutputSchemaBase | None | base.output_type | a Pydantic model (or AgentOutputSchema) for structured output; None for free text |
hooks | AgentHooks | None | base.hooks | SDK lifecycle callbacks; a method may declare a Context first instead of the SDK’s wrapper |
skills | Sequence[str] | base.skills | names resolved against the owning Deck’s skills= roots |
mcp | Sequence[str] | base.mcp | names resolved against the owning Deck’s mcp= file |
Agent is immutable once constructed (AttributeError on assignment) — a Deck compiles it once
at build(), and nothing about the compiled result can drift out from under a mutation
afterwards. A value explicitly passed here always wins over base’s, including an explicit empty
value: omission, not falsiness, is what defers to the base. base= is keyword-only by
construction, so Agent(SomeDeclaration, name=...) is a TypeError rather than a silently
accepted positional base.
A tool is a plain function. build() compiles it: the name comes from the function, the
description from its docstring, and the schema the model sees from its annotated parameters.
def find_slots(day: str) -> str:
"""Find free appointment slots on a given day."""
...One parameter is not shown to the model. A parameter annotated agentdeck.Context[T] receives
the value passed to deck.run(..., context=...) — see Deck — and is absent
from the tool schema entirely, whatever it is named:
from agentdeck import Context
def find_slots(day: str, environment: Context[Calendar]) -> str:
"""Find free appointment slots on a given day."""
return environment.data.lookup(day)Declaring two such parameters is a build() error. So is a callable whose signature cannot be
read — a decorator that dropped functools.wraps is the usual cause — since there is then no
honest schema to show the model and no way to tell “declares no context” from “could not look”.
And so is a T the owning deck’s Deck(context=...) cannot satisfy, when it declared one: a
ContextTypeError naming both types, at build(). See
Deck for what counts as compatible and what is
deferred to invocation instead.
An already-built Agents SDK tool object (@function_tool, WebSearchTool(), …) is still
accepted and passed straight through. It is engine-native: agentdeck introspects nothing
about it, it gets no portability guarantee, and it cannot receive a Context.
The same annotation works at the other three injection sites, with the same rules. instructions=
may be a callable taking at most one Context[T] and nothing else, in which case only the string
it returns reaches the model. A hooks= method may name a Context[T] where the SDK’s own
wrapper would go — it has to be the first parameter, since that is where the SDK passes its
context — and a hooks object declaring none is passed through untouched. A workflow node takes
one alongside its state, and the two stay strictly separate: state is the workflow’s mutable
data, ctx.data is the environment the run was handed.
| Method | Signature | Behavior |
|---|---|---|
build() | () -> agents.Agent | compiles a fresh SDK agent from the fields above — no caching, and no catalog: handoffs by name and tools=[a_workflow] need Deck.build()’s two-pass compile instead |
run() | (message=None, **runner_options) -> agents.RunResult | one-shot headless run |
Agent.run() is not Deck.run(). It calls the SDK’s own Runner.run directly — no Runtime, no
event log, and it returns the SDK’s RunResult, not a TurnResult. Reach for it inside a script
or a test that only needs one headless call and does not care about the log; reach for
Deck.run() (see Deck) for anything that should be recorded and readable back
afterwards.
It also passes no context. There is no context= on the headless runner, so a tool declaring one
refuses when the model calls it, saying the run “carries NoneType rather than an AgentDeck run
context”. A context-declaring agent is a Deck.run() agent.
skills=/mcp= names unresolved against the owning Deck’s catalog fail Deck.build() naming
the offending agent and the names it declared that nothing provides.
AgentDeclaration
from agentdeck.authoring import AgentDeclarationA reusable set of defaults: subclass it, set class attributes, and pass the subclass as base=
to as many Agent(...) calls as need it.
class BookingBase(AgentDeclaration):
instructions = "You handle bookings."
model_settings = {"temperature": 0.2}
booking = Agent(base=BookingBase, name="booking", tools=[find_slots]) # find_slots: see above
support = Agent(base=BookingBase, name="support", instructions="You handle support tickets.")AgentDeclaration is never constructed or run directly — every attribute it carries is a
ClassVar, and it exists only to be named as Agent(base=...).
Workflow
from agentdeck import Workflow| Argument | Type | Default | Purpose |
|---|---|---|---|
base | type[WorkflowDeclaration] | None | None | a shareable graph-building declaration (see below); keyword-only |
name | str | base.name, or the base class name | registry key |
description | str | base.description | tool description when exposed via as_tool() |
state | type (a Pydantic model) | base.state | the graph’s state schema — required one way or the other |
durable | bool | base.durable (False) | compiles with a checkpointer (AGENTDECK_CHECKPOINT_*) when True, so a run can resume by thread_id after the process dies |
graph | () -> StateGraph | base.build_graph | a bare graph-building factory, in place of overriding build_graph() on a base= |
Exactly one of base= (a WorkflowDeclaration subclass overriding build_graph()) or graph=
(a bare () -> StateGraph factory) supplies the graph — the same override-on-construction shape
Agent(base=...) has, so the two constructors read as one pattern. Workflow is immutable once
constructed, for the same reason Agent is.
| Method | Signature | Behavior |
|---|---|---|
build_graph() | () -> StateGraph | the graph, uncompiled — delegates to graph=/base.build_graph() |
build() | () -> CompiledStateGraph | compiles the graph, with a checkpointer iff durable |
run() | (state=None, *, thread_id=None, **runner_options) -> Any | runs the graph once, direct-call (no event log); returns the final state or an InterruptResult |
run_stream() | (state=None, *, thread_id=None, **runner_options) -> AsyncIterator | node_update/custom events, then done — or an InterruptResult in its place |
resume() | (thread_id, value, **runner_options) -> Any | continues a paused run; interrupt() returns value |
pending() | () -> list[InterruptResult] | every thread of this workflow currently paused on an interrupt |
as_tool() | (*, name=None, description=None, output_keys=None, defaults=None, strict_json_schema=False) -> FunctionTool | exposes the workflow as a tool |
node_names() | () -> list[str] | node names on the graph, excluding START/END |
resume() and a paused interrupt() both raise ConfigError when durable is False — there is
no checkpointer to resume from. thread_id is required when durable is True (a ValueError
otherwise) and ignored when it is not.
The same asymmetry as Agent.run() applies here. Calling some_workflow.run(...) directly drives
the compiled graph and writes nothing to the event log; Deck.run() and Deck.answer() are what
the Runtime actually plays, and record the run — see Deck. Deck.stream()
plays a workflow the same way, so nothing on Deck calls run_stream() — it stays useful for a
script or a test that wants the raw graph stream with no log.
The asymmetry goes further for a node that declares a Context[T]. The bridge that puts a context
into a node is installed when a Deck builds its catalog, so run(), resume() and as_tool()
never install it: such a node reached by one of those paths dies with LangGraph’s own
TypeError: <node>() missing 1 required positional argument, not with agentdeck’s message. Loud
and immediate, but it is the raw engine error — the price of leaving a log-free convenience alone.
as_tool() requires state to be a Pydantic model (TypeError otherwise, since the tool’s JSON
schema comes from state.model_json_schema()), and requires durable=False — a tool call
supplies no thread_id, which a durable workflow needs for its checkpoint, so Deck.build()
raises ConfigError naming the agent and the workflow. A durable workflow is a root invocable:
reach it through deck.run(...) with a session, not as an agent’s ability. defaults pins specific state fields to fixed
values regardless of what the model passes, and strips those fields from the schema the model
sees; output_keys filters the final state down to a subset of channels in the tool’s return
value. Both are optional — the default is the whole schema, the whole final state.
WorkflowDeclaration
from agentdeck.authoring import WorkflowDeclarationOverride state and build_graph():
class Booking(WorkflowDeclaration):
state = BookingState
durable = True
@classmethod
def build_graph(cls):
g = StateGraph(cls.state)
...
return g
booking_flow = Workflow(base=Booking, name="Booking")Never constructed or run directly — it exists only to be named as Workflow(base=...).