v5.0.0
Added
- Lifecycle & Control docs now include a capability matrix. A table on the
Lifecycle & Control page shows which of
pause(),resume(),cancel(), andanswer()are legal from each run status, for quick
reference alongside the existing prose. - Agents can select OpenAI, Anthropic, Gemini, Ollama, or OpenRouter by model prefix. Each
provider reads its own environment credential, andDeck.build()reports every missing
requirement before execution. Runtime settings now come only from environment variables or
the project.env; the undocumentedconfig.yamlsource is removed. run.cansays which lifecycle controls are available before you call one.run.can.pause,
run.can.resumeandrun.can.cancelcombine the engine's own capability with the run's current
state, so a UI can enable its buttons and a workflow can branch without guessing. Informational
by design: it reads the status the handle last saw, and the lifecycle methods stay the
authoritative answer.@tooland@workflowdeclare AgentDeck-native code: ordinary Python, no engine. A
@workflowbody takesWorkflowCtxand runs as a coroutine that suspends in place - at
ctx.safepoint(), or atctx.ask(question, options=...)- and continues on the next line
rather than replaying, because its own locals are the checkpoint. A@tooltakesToolCtxand
is a leaf capability: it can report and safepoint, but notaskor start other runs. Both are
played by a new native executor that parks a suspended body's coroutine in memory and cancels
it onDeck.aclose().ctx.invoke()starts a child run from inside a@workflow, andctx.parallel()composes
several.ctx.invoke(target, *args, **kwargs)binds to the target's own signature and hands
back aRun:await ctx.invoke(...)is the result,child = ctx.invoke(...)is the handle,
with its own id, its own log and its ownchild.can.*/pause/resume/cancel. It takes
a catalog name or a@tool/@workflowdefinition the deck holds; any other object waits for
the invocation resolver.ctx.parallel(*runs)is all-or-nothing: the first failure cancels its
siblings and propagates, so no child is left running behind a parent that gave up.Agent(subagents=[...])lets an agent delegate a bounded task to another agent. Names
resolve against the catalog atbuild(), likehandoffs=, and each becomes one tool the model
may call with a task; calling it runs that agent as a child run and hands its final output back.
Not a handoff: the conversation stays with the parent, and the child sees only the task.ctx.agents.create()andctx.agents.fork()mint an agent the catalog does not hold.
create(**declaration)takes the keywordsAgent(...)takes,fork(source, **overrides)copies
a catalog name, anAgentor another instance, and either is invoked withctx.invokelike any
other target. The deck holds what it minted, which is what makes a child run of it answerable,
resumable and cancellable.ctx.agentis the agent whose turn is running, orNone.run.startedcarriesparent_run_id. A delegated turn's cost rolls up into its parent's
run.completed.usage, cancelling a parent cancels the children it started, and a reader of the
log follows the same edge afterwards. Delegation is bounded at depth 3 and fan-out 8, with an
error naming which bound was hit and who hit it; there is no setting for either.
Changed
-
EventSinkPortis renamedObserver, and it gainsagentdeck.views.views.all,
views.chat,views.tools,views.reports,views.lifecycle,views.errorsand
views.usageare composable predicates over the event stream (|,&,~); pass
view=to any observer to filter what it receives.agentdeck.observers.Langfuseis
renamedLangfuseObserver, andConsoleObserver/FileObserverjoin it: one prints a
line per event, the other appends one JSONL line per event, both with no configuration. -
The event schema is
major=4, and v5.0.0 does not read a log 4.x wrote. The payload
vocabulary moved (see the reporter entry below, andRunStarted.kind_of_invocablegaining
"tool"), so the major bumps, and this release does not carry a compatibility window:Event
refuses any major but its own, by name, saying that the log has to be replayed into a new store
or read with the version that wrote it. Every store behaves the same way, so there is one rule to
know rather than one per backend. -
One executor method:
execute.startandresumeare one call.resumewas never the
pause's resume - lifting a pause already re-enteredstartwith the log as history - so it
existed only to answer an interrupt, which the log already records: the answer rides on
run.resumedand the thread onrun.interrupted. An executor now reads which of the three
plays it is (fresh, a replayed pause, an answered interrupt) offhistory, and
Runtime.resume(...)no longer takes athread_id. A custom executor implements one method,
and a target that never suspends implements nothing extra. -
An answer the log cannot hold is refused.
run.answer(...)with a value JSON cannot carry
used to log a warning, record nothing, and hand the value to the engine in memory anyway - a
run resumed on an answer no replay and no other process could reproduce. It raisesValueError
now, before anything is claimed, and the run stays answerable. -
The engine port is
Executor.agentdeck.core.ports.EnginePortbecomesExecutor,
adapters/engines/becomesadapters/executors/,LangGraphEngine/OpenAIAgentsEngine/
StubEnginebecomeLangGraphExecutor/OpenAIAgentsExecutor/StubExecutor, and
InvocableSpec.enginebecomesInvocableSpec.executor. One word for the thing that executes a
target, in the type, the module path and the spec. Wire values are untouched: an executor is
still named"langgraph","openai-agents"or"stub", andrun.failedstill carries
error_code="engine_error". -
The reporter has four methods and one event kind.
ctx.reporter.status(...)and
ctx.reporter.progress(...)are replaced byinfo,warning,errorandreport, each taking
arbitrary keyword fields:ctx.reporter.warning("Primary source unavailable", source="drive"),
ctx.reporter.report("candidate_found", score=0.91). Thestatus.reportedand
progress.reportedevents become onereportevent carryinglevel,messageandfields.
A stage count is nowreport("reviewing", current=2, total=4), and nothing validates the pair. -
Context[T]isToolCtx[T], andctx.checkpoint()isctx.safepoint(). A tool declares
ToolCtx[T]; the orchestration surface an imperative workflow gets isWorkflowCtx[T], so one
type no longer has to mean both. The method rename follows the concept the docstrings already
used: a safe point is where a run can be stopped. -
run.pause()andrun.cancel()refuse loudly instead of returningFalse. Both used to
answerbool, whereFalsemeant "this deck has no control backend" and was indistinguishable
from "the run had already ended". They now returnNone, raiseRunStateErrorwhen the run's
state refuses the operation, and raise the newUnsupportedControlErrorwhen the control can
never be applied. An operation with nothing to do (pausing a paused run, cancelling a finished
one) still returns quietly.run.resume()is strict on the same terms and no longer ignores a
run that is waiting for an answer. -
A run that belongs to no conversation now belongs to no session.
RunContext.log_key
(session_id or run_id) is gone. It answered "which stream do these events go in" by encoding
two different things as one string, and a store handed it could not tell a session named after a
run from that run itself. Event stores hold a nullablesession_idinstead, and every
EventStorePortmethod takesctxplus only what is genuinely not identity:read(log_key, ctx)
isread_session(ctx),read_run(log_key, run_id, ctx)isread_run(ctx),
claim_resume(log_key, run_id, resumed, ctx, origin)isclaim_resume(resumed, ctx, origin), and
RunSummary.log_keyisRunSummary.session_id. A caller reading another run builds the context
for it (replace(ctx, run_id=...)). No migration: 5.0 does not read a log 4.x wrote, on any
store. Opening a SQLite or Postgres log 4.x wrote raises a clearStoreError; a Redis log
written by 4.x reads as empty, since its keys were shaped by the old encoding. Drain or discard
a 4.x log, or replay it into a new store, before upgrading. -
A completed handoff is the core event
agent.changed, not a namespacedcustomone. The
OpenAI Agents executor stopped emittingcustomnamedopenai_agents.handoff; a handoff between
agents in the same run and conversation now lands asagent.changed, carryingprevious_agent
andnext_agent, and only once the handoff has actually happened. A handoff that was requested
but failed or was refused still emits nothing. The schema ismajor=4, minor=1.
Removed
- LangGraph is gone:
Workflow,WorkflowDeclaration,graph=,durable=, the executor, the
checkpointer andAGENTDECK_CHECKPOINT. What hung off them goes too:sleep_untiland the
timer sweep (AGENTDECK_RUNTIME_SWEEP_INTERVAL_SECONDS),Workflow.pending(),as_tool(), the
AgentNode/LoadFileNodegraph nodes, thenode.updatedevent, and the HTTP surface's
/workflows/*routes. A fresh install pulls nolanggraph,langchain-coreor
langgraph-checkpoint-sqlite. A 4.x graph user stays on 4.x, or ports the graph to an
imperative@workflow. - The
durabilityextra is nowpostgres. Installagentdeck-sdk[postgres]: what remains in
it is the Postgres event log's driver, and the checkpointer the old name meant is gone.
Fixed
deck.runs.getno longer corrupts a session id that happens to equal a run id. It recovered
the session by comparing the stored key against the run id, so a caller-chosensession_idthat
matched came back asNone.- A busy standalone run no longer names a session that does not exist. The refusal read
session '<the run's own id>' is held by run '<the same id>'; a run in no session now says so. Deck.aclose()always returns. A run whose task never took the cancellationaclose()sent
it held the close open forever, wedging a shutting-down server. The close now asks twice, a
second apart, and then stops waiting: it logs the run id and records the run's own
run.cancelled, so an abandoned run does not stay open in the log with nothing playing it. The
run's own later writes are refused rather than appended past that event.