Release management¶
How a reactifact version is cut, built, verified, and published.
Versioning¶
- SemVer; pre-releases carry an
rcmark (e.g.0.20.0rc1), dropped for the stable cut (0.20.0). - The version lives in one place:
pyproject.toml→[project] version.reactifact.__version__reads the installed distribution metadata;tests/test_version.pyguards the match.
Changelog rule¶
Every user-visible change lands in CHANGELOG.md (Keep a Changelog). Cut the
entry when the version is bumped:
- move unreleased items under a new
## [X.Y.Z] — <date>heading; - group them as
Added/Changed/Removed(deprecations too); - mark breaking changes explicitly, even in
rcs.
Upgrading¶
0.20.0 has a dedicated migration guide. CHANGELOG.md
remains the source of truth for what changed between versions, and breaking
entries are marked per the rule above. A few changes worth knowing if you're
crossing them (see also migrating.md for porting from other
frameworks):
- 0.20.0 — the new core line.
Step/@stepreplace theProduceauthoring surface; class steps takeeffects: Effects(self.effectsis gone);Artifactis a frozen handle over a content-addressed version store; sessions use the v2blobs/data_refformat; and the legacy example set was removed.Produce,ProduceCall,@produce,Agent.produces,Artifact.at/history/ diffand the top-level tool-module shims remain for the 0.20 compatibility window. - 0.14.0 — behavior-visible (non-breaking) changes to know about:
Budget.max_secondsis now a hard turn deadline: the runtime cancels the generation in flight when it passes (a hung LLM call is actually stopped) and ends the turn withRunOutcome.BUDGET_TIME_EXCEEDED; the cancelled generation's trigger batch is left queued for a resume. Before, the check only ran between generations, so a run could overshoot it.- A new
RunOutcome.STOPPEDis reported whenRuntime.request_stop()/ashutdown()ends a run at a generation boundary. - Logging is silent by default — nothing is emitted under
reactifact.*untilconfigure_logging()is called. - 0.11.0 — behavior-visible (non-breaking) changes to know about:
RunTrace.duration_msis now milliseconds (it carried rawtime.monotonic()seconds before, so the dashboard showed0 msand the Langfuse run span ended ~1000× too early).RunStats.durationstays seconds.RunTrace.started_atis now the turn start (previously the end), andAgentSpan.started_atis persisted (SQLite/Postgres auto-migrate) to power the dashboard timeline.- The
TraceReaderprotocol grew asessions()read; the dashboard'screate_trace_routernow expects aTraceStoreProtocol(read + the tag annotation methods). The built-inTraceStore/PostgresStoresatisfy it; a custom reader implementing onlyquery/getneedssessions()and the annotation methods for the new pages to work. - 0.10.0 — behavior-visible (non-breaking) changes to know about:
PromptTemplate.rendernow substitutes only identifier-shaped{field}placeholders and leaves other braces verbatim, so a literal JSON example in a prompt needs no{{/}}escaping. Templates that relied onstr.formattreating a stray{...}as an error/format-spec now pass it through.reactifact.chat.ChatEvent.kindis a closedLiteral["session","status","message"]; constructing one with an arbitrarykindnow fails validation.- Provider/
WebSource/OTLP/Langfuse HTTP clients are created lazily per event loop (no moreRuntimeError: Event loop is closedacross loops); an injectedclient=is still used as-is. Runtime.arun/astreamgained a keyword-onlyrequest=;Runtime.run/run_oncelikewise. Existing positional calls are unaffected.- 0.7.0 —
Context.merge_fromnow preserves theidof an artifact that exists inotherbut not intarget(it previously minted a fresh one). If you relied on the old id-regenerating behavior — unlikely, since it silently detached the merged artifact from any relation pointing at its original id — pass the artifact throughcreate(data, id=new_id())yourself before merging to keep the old effect. - 0.2.0 —
reactifact/__init__.pyre-exports only the core surface (~40 names, down from ~150); eval, tracing, checkpoint/branch backends, the chat/web layer, the adaptive scheduler, replay, structured-LLM helpers, viz, and prompt templates moved to their own submodule imports. Nothing renamed — see the### Breakingentry inCHANGELOG.mdfor the full before/after import list. - 0.4.0-rc1 —
LLMRequest.temperaturechanged from a hard-coded0.7tofloat | None;Nonenow means "omit → provider default" instead of "use0.7". Same call shape, different generation behavior, no error raised — passtemperature=0.7explicitly (per-call or on the provider) if your code relied on the old implicit default. - 0.1.0-rc1 —
Produceno longer returns aPatch; it writeseffects.create/update/link/ask/resumeand returnsNone(see effects).InterruptPatch,Patch.merge_existing_patchandPatch.to_dictwere removed.
Release checklist¶
What must be true before cutting a release — and, specifically, the bar for leaving pre-1.0 and shipping 1.0.0:
- [ ]
uv run python -m pytest -qis green, coverage ≥ thefail_underfloor (andreactifact/quota.py,metrics.py,eval/not the tail). - [ ]
uv run python -m mypyanduv run python -m ruff check/ruff format --checkare clean. - [ ]
uv run --group docs mkdocs build --strictis clean (CI enforces it). - [ ] The public surface is unchanged except as the freeze test
(
tests/test_public_api.py) anddocs/en/api.md§Stability record; any rename/removal has a### Breakingchangelog entry and a deprecation window behind it. - [ ] The wheel smoke job passes in a clean venv (
uv build, import, CLI). - [ ]
CHANGELOG.mdmoves[Unreleased]under## [X.Y.Z] — <date>. - [ ]
tests/test_version.pypasses (distribution metadata matchesreactifact.__version__).
1.0 criteria (all of the above, plus):
- [ ] Two consecutive minor releases with no
### Breakingentry. - [ ] Every documented submodule
__all__is pinned by the freeze test and reflected indocs/en/api.md. - [ ] The safety/eval/observability surfaces (guardrails, authz, quota, eval, online eval, metrics, tracing) each have usage docs and tests.
- [ ] No
rc/Unreleased-only guarantees leak into the contract.
The release loop¶
# 1) sanity
.venv/bin/python -m pytest
.venv/bin/python -m mypy
.venv/bin/python -m ruff check
.venv/bin/python -m ruff format --check
# 2) version + changelog (see above)
# 3) build artifacts
uv build # dist/reactifact-0.20.0-py3-none-any.whl + sdist
# 4) verify the wheel in a scratch venv (not the workspace, so no PYTHONPATH)
uv venv /tmp/reactifact-rc
/tmp/reactifact-rc/bin/python -m pip install --quiet dist/reactifact-0.20.0-py3-none-any.whl
/tmp/reactifact-rc/bin/python -c "import reactifact; print(reactifact.__version__)"
/tmp/reactifact-rc/bin/python -m reactifact graph examples.knowledge.agents 2>/dev/null \
|| /tmp/reactifact-rc/bin/reactifact --help >/dev/null # console script present
# confirm the wheel contains reactifact + tracing templates and NOT examples/tests:
unzip -l dist/reactifact-0.20.0-py3-none-any.whl | grep -E "examples/|tests/|tracing/templates"
# 5) tag
git tag v0.20.0
git push origin v0.20.0
# 6) publish (PyPI token in env)
uv publish --publish-url https://upload.pypi.org/legacy/
What ships¶
uv build packages only the reactifact package (setuptools packages.find
excludes examples/tests) plus the trace dashboard assets
(reactifact/tracing/templates/*.html and *.css). Examples, tests and docs
stay in the repository and are the documentation-by-example.
Rollback¶
A broken rc is fixed in the next rc/release — never rewrite history of a
tagged version. Keep patch releases strictly backwards-compatible (§61: the
framework is stable at the stated surface).