v3.0.0
The stable v3. Everything below landed on top of 3.0.0b1: multimodal input, a versioned event
envelope, one variable per configuration decision, Context[T] injection over both engines, and
observability declared where the deck is declared.
Ask AgentDeck ships alongside it — a documentation assistant built on the v3 public surface,
in examples/ask-agentdeck/. It exists as much to test the surface as
to answer questions: building a real application against it produced three findings, all recorded
rather than smoothed over, and none of them blocking.
Upgrading
-
A retired v2 environment variable now refuses to start, rather than being ignored. Nothing
bindsAGENTDECK_EVENTS_BACKEND/_URL,AGENTDECK_CONTROL_*,AGENTDECK_CHECKPOINT_*,
AGENTDECK_SESSION_REDIS_URL,AGENTDECK_LANGFUSE_HOSTorAPP_CONFIG_PATHany more, so a
deployment that still exports one would fall back to the default — and for the three store
variables that default is in-process memory, i.e. a durable event log silently becoming
ephemeral on upgrade. Setting one without its replacement is now an error naming both. A
leftover alongside a correctly-set replacement is fine: the migration has happened, and a stale
name inherited from a container environment should not stop a working process from booting. -
An event log written before v3.0.0 cannot be read by v3.0.0. The envelope's
vwas a plain
integer up to and including v3.0.0b1 and is now{major, minor}, which is a major bump — and a
major bump means exactly this: the two are not mutually readable. Only durable stores are
affected (sqlite,postgres,redis); the defaultmemorystore keeps nothing across a
restart, so most callers have nothing to migrate. An affected store must be replayed into a new
one, or read with the version that wrote it. The first read of an old event says so by name
rather than failing as a validation error on a model you have not met. -
Runtime(clock=...)andbuild_runtime(clock=...)are gone. Both keywords stopped
deciding anything once ADR-D11 moved timestamp assignment into the store; a caller that held
time through either one now gets aTypeErrorinstead of a run whose timestamps quietly kept
moving. Pass the clock to the store instead:MemoryEventStore(clock=...),
RedisEventStore(clock=...). -
A client built on the openai-agents engine's own session (
agents.SQLiteSession,
agents.extensions.memory.RedisSession) that inspects or prunes its content parts must add
input_image/input_audioto its matcher (#161). A turn carrying an image or audio block
now writes the SDK's own canonical part types into the session; a matcher written against the
old raw shapes (image_url,input_audiotuples, or similar) silently stops matching, which
for a pruning pass means the same image gets re-sent, and re-billed, on every later turn of
that conversation instead of being dropped after the turn that needed it. -
A program holding two
Deckinstances at once now raises instead of quietly misbehaving
(#204). One deck at a time is unchanged, including a deck mounted inside an existing service
throughasgi(). What breaks is a script that validates several projects in a loop, or a
notebook that re-runs itsDeck.from_project()cell: close the first (await deck.aclose(),
or run it underasync with) before constructing the next. Today those programs appear to
work while the second deck reads the first one's bundles, so the raise is the change you want.
Added
-
Context injection:
agentdeck.Context,Deck(context=T)andcontext=on every run
(#166). An application value — a database handle, a client, whatever the code a run reaches
needs — enters once at the run boundary and is delivered to any callable that declares it.Declaring it. Annotate a parameter
Context[T], whatever you name it: a tool, an
instructions=callable, an agent hook (first parameter, where the SDK's own wrapper goes), or
a workflow node alongside itsstate.ctx.datais the very object passed in, by reference;
ctx.reporter,ctx.run_id,ctx.session_idandawait ctx.checkpoint()come with it. A
plain function intools=is now compiled rather than rejected — a context-declaring tool
cannot be pre-wrapped with@function_tool, since that would put the context parameter in the
model-visible schema.The model never sees it. The context parameter is absent from the tool schema sent to the
model, an instructions callable contributes only its return value to the prompt, and the value
is never written to the event log.Supplying it.
deck.run(..., context=obj)anddeck.stream(...), plus
answer(run_id, value, context=...)andresume(run_id, context=...)— resupplied, never
recovered, because the value is deliberately never serialized, so the caller picking a paused
run back up is the only one who still has it.Both engines, one contract. The value travels on each engine's own runtime-context channel
— the SDK'sRunContextWrapper, LangGraph'sRuntime[T]— never onconfigurable, which
keepsthread_id, the reporter and the stream flag exactly as before. A contract test
parametrized over both engines pins that the two bridges deliver the same thing.Checking it.
Deck(context=MiddleContext)declares the context type (the class, not an
instance), andbuild()then checks everyContext[...]in the catalog against it, raising
ContextTypeErrornaming both types. It decides only what the runtime can decide — exact type,
subtype,Any, a runtime ABC's origin, a protocolissubclasswill rule on, a union arm by arm
— and defers everything else (a structural protocol, aTypeVar, an engine-native tool object)
to invocation rather than guessing. A deck that declares nocontext=is unchanged: nothing is
checked, andrun(context=...)works exactly the same.Where it does not reach, all documented in the
Deckreference: a context cannot cross the
HTTP surface at all (no wire form for a live object, so a served run carriesNone);tick()
takes none, so durable +sleep_until+Context[T]is unsupported in v3.0.0 and a timer
resume replays withctx.dataset toNone; the headlessAgent.run()and
Workflow.run()/as_tool()paths pass none either; and a skill is prose, not a callable, so
there is nothing there to inject into. -
Deck(observers=[...])— the event stream has a declared set of observers, and they start
with the deck (#181). An observer is anyEventSinkPort— telemetry, cost accounting, audit
— and the Runtime fans every run out to all of them, each with its own bounded queue.
agentdeck.observers.Langfuseis the one agentdeck ships.from agentdeck import Deck from agentdeck.observers import Langfuse deck = Deck(agents=[booking], observers=[Langfuse(), my_audit_observer]) async with deck: # every observer starts here, once, before any run await deck.run("booking", "hello")
Three states:
observers=None(the default) starts the configured Langfuse observer if
AGENTDECK_LANGFUSE_*names one and nothing otherwise, exactly as before; a sequence starts
exactly those, in order, and suppresses the settings-derived one;observers=()starts none.The altitude is the point. Observers start during
__aenter__, before the Runtime exists and
before any run can begin — they are no longer assembled underneath the composition root from
settings, nor started by whichever run happened to come first.build()shape-checks
observers=and does nothing else: nothing is started, no telemetry client is constructed and
no exporter contacted, so a deck with Langfuse configured still validates where Langfuse is
unreachable. There is nodeck.observersproperty, for the same reason there is noruntime
orstore.Langfuse(sdk_spans=True)adds the raw layer on top of the semantic one: OpenInference maps
every agent, generation and tool call the Agents SDK makes, with its input and output — detail
the event log does not record. It arrives as a second, separate trace per run rather than
nested under the first, because nesting would need the engine to establish an OTel context and
the engines are barred from the Langfuse SDK by design. Off by default and documented as
unnested, so nobody meets a trace they did not ask for — which is what #162 was filed about.
Correlating the two layers is #218. -
EventSinkPort.start()(#181) — anasyncno-op by default, called once while the Deck
opens, before any run, and pairing with the existingclose(). A sink that holds a client, a
connection or a file opens it there rather than on whichever event it happens to see first.
Additive: an existing sink that only implementsemitis unaffected. Raising fromstart()
refuses the open rather than leaving a deck running with an observer that silently never
worked — which is whatLangfuse()does when no keys are configured. -
SECURITY.mdandCODE_OF_CONDUCT.md(#132). The security policy says where to report a
vulnerability and what is in scope — including the two things that are deliberately not: a
model-chosen tool call runs with the full privileges of the host process, and nothing is
sandboxed. The code of conduct is the Contributor Covenant 2.1, unmodified. -
Package classifiers, so PyPI and every metadata reader can see what agentdeck is and which
Pythons it supports (#132). A test keeps the classified Python versions in step with
requires-python, and asserts the built metadata still names the MIT license. -
examples/: two decks you can copy (#132) — a chat agent with a tool, and a workflow that
pauses for a human approval. Each is a complete project directory with arun.pyand a README,
and each is built by the test suite on every run, so neither can quietly stop working. The
approval example makes no model call at all and runs offline. -
A docs-site page on choosing a store backend: the four independent storage decisions, one
environment variable each, and the trap wheredurable=Trueparks an approval that a second
process cannot see because the event log is still in memory. -
AudioBlock(#159): a fifth content-block kind, mirroringImageBlockfield-for-field
(media_type,data_b64) — the same problem (opaque bytes with a MIME type), so a different
shape would be asymmetry with no payoff. Additive/minor (CURRENT_VERSION.minor0 → 1): a
reader that predates this still parses the event and meets an audio block asUnknownBlock. -
ImageBlockandAudioBlocknow cap inline data at 1 MB decoded, raising at construction
and namingResourceBlockas the by-reference alternative for anything larger (#159). Base64
in an event lands in an append-only log and replays down every SSE connection for the life of
that run, so a documented-only limit shipped violated; the cap is deliberately low, since
raising it later is compatible and lowering it is not.
Changed
-
tools=now takes plain functions, and compiles them (#166) — reversing the guardrail
#172 shipped, which rejected a bare callable and told you to wrap it with@function_tool.
A function annotatedContext[...]cannot be pre-decorated, because@function_toolwould
put that parameter in the schema the model sees, so the plain callable had to become the
canonical declaration. An already-built Agents SDK tool object is still accepted, unchanged
and passed straight through — it is engine-native, introspected by nothing, and carries no
portability guarantee. The one thing still refused atbuild()is a callable whose signature
cannot be read (a decorator that droppedfunctools.wrapsis the usual cause): there is no
honest schema to show the model, and no way to tell "declares no context" from "could not
look", so compiling it would silently drop an argument the function needs. -
Breaking:
build_runtime(sinks=…)no longer defaults to the configured telemetry
(#181, #162). It defaults to no sinks at all, and the composition function reads no Langfuse
keys — an observer opens a live client, so when one is constructed is a lifecycle decision, and
doing it while a Runtime was assembled is what #162's first defect was. Resolving from settings
moved toagentdeck.composition.resolve_observers(), whichDeck.__aenter__calls (and then
start()s the result) as it opens; aDeckbehaves exactly as before. A caller that hand-wired
build_runtime(...)and relied on it picking Langfuse up from the environment now gets an
untraced Runtime — pass the observers yourself, remembering toawait observer.start()first,
or open aDeck.sinks=Noneis no longer accepted; the parameter is a plain sequence. -
The README now says what agentdeck is before it shows any code (#132): what it is, who it
is for, what it deliberately does not do, and how it divides work with the OpenAI Agents SDK
and LangGraph. It linksCONTRIBUTING.md,SECURITY.mdand the docs site rather than
restating them, and its install pin, its Python example and its docs links are all checked by
the test suite. -
Breaking: one
Deckper process, enforced at construction (#204). Constructing a
secondDeckwhile the first is still live now raisesConfigErrornaming both projects;
before, it succeeded and the second deck silently inherited the first one's bundles, because
every project mounts under a single module alias and MCP servers are registered process-wide.
A deck built but never opened holds the process just the same — the claim is taken at
construction and released byaclose(). Two decks side by side is a capability we intend to
add (#213); until then a deck per tenant is a process per tenant, which is what the code has
in fact always done. -
Breaking: one env var per infrastructure decision, not a
_BACKEND/_URLpair that can
disagree (#155).AGENTDECK_EVENTS_BACKEND=postgreswithAGENTDECK_EVENTS_URL=redis://...
used to boot clean and fail on the first event of the first run; the URL's own scheme now
is the backend, so that mismatch cannot be expressed at all, not merely rejected. No
deprecation shim — this is the one breaking-release window where renaming is free — so an old
name is simply never looked up:_BACKENDhad no field to bind to at all once the pair
collapsed to one, and the renamed field (url) maps to a different literal env var name than
the one it replaced, so setting the old name alongside the new one has no effect either way:Old New AGENTDECK_EVENTS_BACKEND+AGENTDECK_EVENTS_URLAGENTDECK_EVENTS=memory:///sqlite://<path>/redis://<url>/rediss://<url>/postgresql://<dsn>AGENTDECK_CONTROL_BACKEND+AGENTDECK_CONTROL_URLAGENTDECK_CONTROL=memory:///sqlite://<path>AGENTDECK_CHECKPOINT_BACKEND+AGENTDECK_CHECKPOINT_URLAGENTDECK_CHECKPOINT=memory:///sqlite://<path>(default:sqlite://.agentdeck/checkpoints.sqlite3) /postgresql://<dsn>AGENTDECK_SESSION_REDIS_URLAGENTDECK_SESSION=redis://<url>APP_CONFIG_PATHAGENTDECK_CONFIG_PATH— unprefixed and generic; any other tool claiming that name silently repointed agentdeck's configSelecting
memory://forAGENTDECK_EVENTS/AGENTDECK_CONTROLnow logs one WARNING at
composition time (resolve_event_store/resolve_control_port) naming what it costs — no
cross-process signals, no log after a restart — instead of that being discoverable only in
production.agentdeck-serve's own startup-time version of this same warning is gone; the
composition-time one covers it and every other entry point besides. -
Runtime.__init__no longer reads settings (#155): the five-parameter, ambient-config-free
constructor now has a sixth,stale_run_after, defaulted to one hour with noget_settings()
call at all.build_runtimeresolvesAGENTDECK_RUNTIME_STALE_RUN_AFTER_SECONDSand passes it
in, the same as its other adapters — an embedder constructingRuntime(...)directly, bypassing
build_runtime, now gets the literal one-hour default rather than whatever the process's
settings happened to say. -
The prefix rule is written down (#155):
docs/coding-standards.md§9 andCLAUDE.mdnow
state thatOPENAI_*/TAVILY_*keep their own names because the respective SDKs read them
natively — the only exceptions toAGENTDECK_*, not an open pattern. -
The openai-agents engine accepts image and audio input, not just text (#161).
TextBlock/ImageBlock/AudioBlockmap onto the SDK's own canonical multimodal input parts
(input_text/input_image/input_audio), which the SDK's chat-completions converter already
accepts and maps down for either API path — agentdeck writes no converter of its own. An
all-text turn still sends the identical joined string it always has; only a turn that actually
carries media takes the new shape.ResourceBlock,DataBlock, and any block a newer writer
invented still raiseConfigError, naming the block kind and the engine, never silently
dropped — and anAudioBlockunderuse_responses=Trueraises naming the Responses API,
which has no audio input member at thisopenai-agents==0.17.0pin;use_responses=False
(chat-completions) accepts it. Output is unchanged and stays text/data only: nothing on this
path produces an image or audio block. -
The event envelope's
vis now a{major, minor}object, not an integer (#156).major
is what a reader must already understand to parse the envelope at all —Eventrefuses one it
does not carry, even for a kind it has never seen, because a major bump can move or remove
envelope fields the unknown-kind fallback never checks.minorrecords an additive change (a
new kind, a new optional field) that an old reader already tolerates by construction and never
needs to consult. This is an intentional wire break: a reader built against the previous
scalarvcannot parse an event this tree writes. -
Breaking:
Runtime.__init__andbuild_runtimeno longer acceptclock(#158). ADR-D11
moved timestamp assignment into the store, so the keyword has decided nothing since #154,
which made it inert and warn rather than remove it outright; a caller still passing it now
gets aTypeErrorinstead of a silently-ignored no-op. Holding time still works at the seam
that owns the clock —MemoryEventStore(clock=...),RedisEventStore(clock=...)— which is
unaffected by this change.
Removed
-
agentdeck.runtime.observabilityis gone (#181, #162), and with itinit_observability,
trace_run,RunTrace,degrade_export_quietlyand the_should_export_spanfilter. It was
a second place tracing was assembled, below the composition root and started by the first run
— the source of both defects fixed below. Traces are rendered from the canonical event stream
byagentdeck.adapters.telemetry.langfuse, which was already the design of record; what this
removes is the parallel mechanism. A directWorkflow.run()orAgent-node turn, which
bypasses the event stream the same way it bypasses the event log, is therefore no longer
traced — run it through aDeckto trace it. -
The sandbox scaffolding is gone (#71). Sandboxing left v3 by ruling (#163 stays open as
the design issue), and the tree was carrying a port with no consumer, an adapter with no
caller and spec classes nothing constructed. Deleted:agentdeck.core.ports.sandboxin full
(SandboxPort,ExecResult,bind_sandbox,current_sandbox,require_sandbox, plus the
SandboxPort/ExecResultre-exports fromagentdeck.core.ports);agentdeck.adapters.caps
in full (UnixSandbox,open_sandbox,input_file_targets);agentdeck.authoring.capabilities
in full (CapabilitiesSpec,ShellSpec,FilesystemSpec,MemorySpec,CompactionSpec);
andagentdeck.runtime.capture.CAPTURE_ENV, whose only reader was the deleted adapter
(CaptureandCaptureActorstay — the tracer still uses them). Re-adding a designed port
later is additive, so nothing here is a one-way door. -
LoadFileNodenow refuses a relative path (#71) instead of resolving it through the
sandbox. That branch could only ever raise — nothing in v3 opened a sandbox for it to find —
so the node raises the refusal itself, still aRuntimeError, with a message that names the
absolute path it wants. It deliberately does not fall back to the process working
directory: quietly reading the host filesystem for a path a model influenced is the widening
the sandbox existed to prevent. -
agentdeck.runtime.observability.sandbox_trace_env()is gone (#71): it built the
LANGFUSE_*/TRACEPARENTenv for a sandboxed skill subprocess, and had no callers left once
sandboxing left v3. Same finding asSettings.sandbox_env()below, which #155 took early. -
agentdeck.surfaces.serve.compat.resume_result()is gone (#71): the v1 resume endpoint
answers throughDeck.answer()and has not gone through this helper since the v3 cutover.
The v1 resume wire format is unchanged — it is covered by the golden replay, which is
byte-identical. -
LangfuseSettings.hostand itsendpointproperty are gone (#155): a pre-4.x
compatibility alias for the Langfuse endpoint, with no reason to survive a major version.
base_urlis the only endpoint field now, and it carrieshost's old default
(http://localhost:3000) so an unconfigured deployment's effective endpoint is unchanged. -
Settings.sandbox_env()and the unboundedSKILL_*env namespace are gone (#155).
Sandboxing left v3 in #163;SkillExecutor,sandbox_env()'s only caller, was already
deleted in #164, so this was a deletion rather than theAGENTDECK_SKILL_*rename the issue
originally proposed.SkillsSettingsand theskill:config.yamlsection go with it. -
check_contiguous/check_terminalare no longer part ofagentdeck.core(#156). Neither
was read by a production path —seqcontiguity follows from how the store assigns it, and
the one-terminal-event-last invariant is enforced byRuntime.run/resumestopping the read
loop at a terminal payload — so keeping them in the schema module read as contract they were
never part of. Embedders who imported them for their own log-auditing should inline the same
two checks (each a few lines over alist[Event]) locally. -
A
durable=Trueworkflow used as an agent tool now failsbuild()(#193) instead of
raising the first time a model calls it.Workflow.as_tool()invokesrun(args)with no
thread_id, which a durable workflow requires to load and persist its checkpoint, so
Agent(tools=[durable_workflow])built clean and then threw mid-turn. The error names the
agent and the workflow and points atdurable=False, or calling it as a root invocable via
deck.run()where a session can be supplied. Giving a tool-invoked workflow a thread of its
own remains an open design question.
Removed
- The
openai_agents.structured_outputcustomevent (#105). #101 gave
RunCompleted.outputaDataBlockfor anoutput_typeagent's validated result; the
customevent carried the same value the older way and was kept only because retiring it
inside #101 would have meant editing an open PR's files.Deck.run()'sTurnResult.output
and v1's chat endpoints now read theDataBlockstraight offrun.completed— their
response bodies are unchanged, but a canonical event stream (Deck.stream(), the event
store) for a structured-output run now has one fewer event.
Fixed
-
A resumed run silently lost its application context (#166).
resume()andresume_run()
minted a freshRunContextwith nodata=at all, so a run paused or interrupted with a
context=came back withNone— and because the value is never serialized, nothing in the
log could be compared against what should have been there. A callable written defensively as
if ctx.data:would have returned a plausible wrong answer with no error anywhere. Only ever
reachable on this development line, sincerun(context=)and this landed in the same release. -
The Langfuse span filter never applied, because the client that carried it was never the
one that ran (#162).build_runtime()constructed a client from settings while the Runtime
was assembled, with noshould_export_span;init_observability()constructed a second one
later, on the first run, with the filter. Confirmed against langfuse 4.14.1: the SDK caches
oneLangfuseResourceManagerper public key and returns it from every laterLangfuse(...),
discarding that call's arguments — so the second construction was not a second client and its
filter was silently dropped. There is one construction point now, reached only when a deck
opens, and the filter is gone with thesandbox.*spans it existed to drop (sandboxing left
v3 in #163/#71, so nothing emits them any more). -
An agent turn exported one trace too many with Langfuse on (#162).
init_observability()
installed the OpenInferenceOpenAIAgentsInstrumentor, but thetrace_runroot that used to
group those spans and carrysession.idwas gone — so the SDK's spans exported as a second,
sessionless tree beside the sink's own trace. Nothing instruments the Agents SDK any more and
the direct-call runners open no observations, so one run is one trace again. -
Deck.from_project()reading a previous project's bundle files (#204). Mounting a project
rebound the module alias but left its already-imported submodules cached, so a second project
whose bundle directory happened to share a name — twoagents/greeter/— got the first one's
module back fromsys.modules. Stale submodules are now evicted on mount, which also means
editing a bundle and rebuilding in the same process picks the edit up. -
Docs:
/referencepages corrected against the current API (#192)./referenceno
longer claims MCP is covered by the Workflows page — it now points at/reference/deck
(Deck(mcp=...)) and/concepts/agents(MCP status at build vs. open), where the actual
coverage lives./reference/deckno longer saysstream()assembles aTurnResult— only
run()does;stream()yieldsAsyncGenerator[Event], as the page's own table already
said two sections up./reference/deckalso now documents the timer/event-log limit:
due_resumes()/tick()still list due timers off each workflow's own checkpointer, but
tick()'s resume now goes through the Runtime whenever a logged run matches (#191) —
closing that run's log entry and freeing its session claim — falling back to the direct
checkpointer resume only for a thread with no logged run. -
AGENTDECK_LANGFUSE_SERVICE_NAME's description no longer says "host process and sandboxed
skills" — sandboxed skills were removed in #164; the generated/reference/settingspage is
regenerated to match. -
tests/test_docs_site.py::test_pinned_install_versions_match_the_package_versionnow also
requires every fencedpip install/uv pip install/uv addline namingagentdeckon the
docs site to carry a version pin matchingpyproject.toml, not just validating pins that
already exist — closing the gap that let/guides/serve-over-httpship an unpinned
agentdeck[serve]install example. -
Three docs pages corrected against the current v3 code (#192).
/guides/human-approval's
cross-process example now says what it actually needs:durable=Truemakes the checkpointer
file-backed, butpending()/answer()read the event store instead, so a second process only
sees a paused run ifAGENTDECK_EVENTSis pointed at a shared backend too — the
in-process default is not enough on its own./guides/serve-over-http's install line now pins
a version (git+...@v3.0.0b1), matchinggetting-started.mdxinstead of an unqualified
agentdeck[serve]./operating/pause-resume-cancelno longer describes the503 no control backend configuredresponse as a deployment state operators can hit:resolve_control_port()
always wires a realControlPortforDeck/agentdeck-serve, so that response is reachable
only by an embedder constructing a bareRuntimeoutsideDeck. -
Seven more docs pages corrected against the current v3 code (#192).
/no longer calls
skills Python definitions (they areSKILL.mddirectories) or claimssession_idsurvives a
restart by default (it needsAGENTDECK_SESSION)./conceptsand
/concepts/runs-and-the-event-logno longer describe SQLite's cross-process story as "shared
memory" — it is a shared file, openable by several processes on one machine, not across
machines./concepts/agentsdescribes MCP status as it works today:build()stays
network-free, and status is re-resolved when theDeckopens (__aenter__, right after
MCPLifecycle.startup), not "dropped at build time"./concepts/protocols-and-surfacesno
longer says an HTTP handler builds aRunContext— theRuntimemints it for every caller.
/concepts/run-controlno longer recommendsctx.idempotency_key, a fieldRunContextdoes
not have./concepts/runs-and-the-event-logand/concepts/workflowsnow document that
Deck.due_resumes()/Deck.tick()'s listing of due timers reads each workflow's own
checkpointer, not the event log, for the same #22-driven reason as theDeck.tick()fix
below. -
Deck.tick()no longer leaves a ghostWAITING_HUMANrun in the event log when it
resumes a timer-paused thread that aDeck.run()/HTTP call parked (#120): it now resumes
such a thread through the Runtime, the same asDeck.answer()already did, so the run's
log entry closes and its session claim releases instead of blocking a fresh run on the
same thread untilstale_run_afterexpires.Deck.due_resumes()/Deck.tick()'s listing
is unchanged — still each workflow's own checkpointer, not the event log, because by
default the checkpoint backend is durable (sqlite) while the event store is not
(memory), and #22's guarantee that a due timer survives a process restart depends on
that. A thread with no logged run at all (parked by calling a durableWorkflow's own
run/resumedirectly — a deliberately log-free path, out of scope here) still resumes
the way it always did. -
A fan-out workflow whose one branch interrupts while a sibling completes now reports the
sibling'snode_updatebefore theinterrupt, instead of silently dropping it (#122). The
langgraph engine reports a pause as soon as the interrupting branch asks for one, not once
every branch in the same step has finished; a slower sibling's completion used to arrive on
the drained tail of that call and be discarded there, even though its write had already
landed in the engine's own checkpoint — the checkpoint and the canonical event log disagreed
about what had run. The pause is still reported last, and a run with a suspended branch never
reportsdone:RunStatus.status_ofalready derivedwaiting_human, non-terminal, from
run.interruptedalone, so nothing there needed to change. Pinned with a new golden fixture,
FanoutInterruptFlow, streamed and non-streamed. -
UnknownBlocknow dumps its original payload verbatim instead of nesting it under
raw_block(#200). Parsing an unfamiliar content block and dumping it straight back used to
produce{"type": ..., "raw_block": {...}}rather than the block that was actually read —
harmless today since nothing relays events, but a relay (#129's protocol adapters) would have
nested the payload one level deeper on every hop it passed through, silently. Known block
kinds (text/image/resource/data/audio) are unaffected.