TL;DR — Most agent frameworks ask you to draw a graph. A knowledge question
doesn't have a fixed path, and you can't ship an answer you can't audit. We built
reactifact: think Celery, but tasks wake on typed artifacts, not messages.
It's pip install reactifact, 3 core deps, and a run you can reproduce.
We kept drawing graphs we couldn't finish
We were building a devops knowledge assistant. Ask it a real question —
"why did our infra costs jump in Q2?" — and answering it needs a Confluence
page, two GitLab MRs, a CSV of cloud costs, some arithmetic, a verification step,
and maybe a follow-up question. The next question needs a different subset of
all that.
Every framework we tried asked us to draw that path: nodes, edges,
conditional routing, a shared state dict. But there is no universal graph. The
path depends on the question and on the data. Encoding every combination as edges
turned into a wiring exercise — and the framework still couldn't tell us why it
produced the answer it produced.
So we stopped describing execution and described state instead.
The idea: state is primary, execution is derived
Instead of "node A then node B", you declare:
-
what artifacts exist — typed objects like
Question,Evidence,Claim,Calculation,Answer; -
what each agent consumes and produces —
Consume(Evidence),produces=Answer.
The runtime looks at what just changed and derives what runs next. Nothing calls
anything. There is no graph to draw — the schedule is a function of state.
If you know Celery, you already know the model:
| Celery | reactifact |
|---|---|
| a task | @produce(Model) |
delay() / apply_async()
|
you don't call it — creating the input artifact is the trigger |
| routing key / queue | Consume(Type) |
| chain / group / chord | several consumes / produces; the runtime derives the order |
retries, acks_late
|
guards + Budget, an honest None instead of a wrong result |
| result backend | the Context — typed, versioned artifacts |
| worker | Runtime |
Single process today (no broker, no worker pool) — it's a Celery-shaped model,
not its distributed runtime.
The second thing we couldn't get: an answer you can audit
The other half of the problem is trust. An LLM writes confident prose and
unreliable arithmetic, and most frameworks leave "who computed this number, from
which source?" to whatever logs you remembered to add.
In reactifact, the answer is an artifact like everything else:
-
Calculations are computed, not generated. A
Producedoing arithmetic over a CSV computes it in plain Python and hands the model the result to explain — never the raw numbers to guess from. -
Every derived artifact links to its inputs —
Answer —supported_by→ Variance —calculated_from→ Spend, Table. That's a queryable graph, not a log line. -
A run is reproducible. Artifacts are versioned, and a content hash
(
context_hash) is identical across deterministic runs, so you can re-run and verify:reactifact replay <store> --session <id> --verify <hash>. -
The audit is a report.
reactifact.auditrenders the answer with a sha256 per artifact, its producing author, and the provenance edges.
Proof, offline, in one command
examples/fintech_audit answers a finance question — "what's the Q2 cloud spend
variance, and does policy require approval?" — from a transactions CSV, a budget
CSV, and a policy document. No API key, nothing leaves the machine:
$ python -m examples.fintech_audit.main # from a repo checkout, no API key
variance vs budget: +12.5% ($45,000 actual vs $40,000 budget, 10% policy threshold → over)
answer: Q2 cloud spend was $45,000 against a $40,000 budget (+12.5%) —
exceeds the 10% policy threshold. CFO approval is required.
audit: Answer —supported_by→ {Variance, Spend, Table, Policy}
answer sha256 5461290d… · context sha256 24449f6f…
Run it twice: the hash is the same. Change a fact, and only what actually
consumed it recomputes — because Artifact.version tracks real reads, not a
graph edge someone drew by hand.
The number is computed, and its provenance recorded, in plain Python (trimmed
from examples/fintech_audit/produce.py):
@produce(Variance, reacts_to=Spend) # wakes when a Spend artifact exists
async def compute_variance(call: ProduceCall) -> None:
spend = call.trigger # the artifact that triggered this run
budget = cloud_budget(call.context) # read from budget.csv
pct = (spend.data.total - budget) / budget # deterministic — never the model
variance = call.effects.create_once_from(
spend, # stable id → idempotent re-runs
Variance(actual=spend.data.total, budget=budget, pct=pct,
threshold=0.10, within_policy=abs(pct) <= 0.10),
)
variance.link("calculated_from", spend) # provenance edge, queryable
What you get beyond the model
A task queue gives you execution. Layering state, versions, and provenance on top
of the type system gives you a few things for free:
- Determinism where it matters — compute in code, let the model reason.
-
Branching and merge — fork an exploration, run different agents on each
fork, three-way
merge()with an explicit conflict instead of silent last-write-wins. -
Human-in-the-loop as state —
effects.ask(...)is just another effect; a human is another reaction, not a special case. - Replay — reproduce a past run from its recorded model calls and commit chain, offline.
-
MCP both ways — call any MCP server's tools, or expose your own
Contextas one.
And it's small: three core dependencies (pydantic, httpx, python-dotenv),
fully typed, no framework-of-the-week dependency tree.
Honest limits
reactifact is pre-1.0 (0.10.0), maintained by one person, and
single-process today — no broker, no worker pool, no managed platform. If you
need a mature ecosystem and hosted execution right now, use LangGraph; it's a
better fit. reactifact is for teams building open-ended, auditable knowledge
agents — internal assistants over docs, ops/incident tooling, finance/compliance
workflows — where "why did it say that?" has to have an answer.
Try it
pip install reactifact
Repo: https://github.com/bzdvdn/reactifact · Docs and offline demos:
https://bzdvdn.github.io/reactifact/
If you build agents over docs, CSVs, or repos, I want to hear what breaks for
you. That's more useful to me than a star.

Top comments (0)