DEV Community

Cover image for We stopped drawing graphs: an event-driven runtime for agents
Bogdan
Bogdan

Posted on

We stopped drawing graphs: an event-driven runtime for agents

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 producesConsume(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 Produce doing 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 inputsAnswer —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.audit renders 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…
Enter fullscreen mode Exit fullscreen mode

fintech_audit demo: the variance, the answer, and the reproducible context hash — a second run prints the same hash.

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
Enter fullscreen mode Exit fullscreen mode

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 stateeffects.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 Context as 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
Enter fullscreen mode Exit fullscreen mode

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)