Skip to content

reactifact

Auditable agents for Python. Build knowledge assistants over docs, spreadsheets and the web — where the path per question isn't a graph you can draw. Every answer is computed, versioned and reproducible — not a string you have to trust.

Is: a Python library (3 core deps) · single-process · typed, versioned artifacts with provenance · deterministic replay · runs offline, no API key Isn't: a managed platform · a distributed task queue · a pre-built agent/tool marketplace

A question like "why did infra costs jump in Q2?" needs a spreadsheet, a policy doc and a page off the web — and the next question needs a different subset. That is not one graph you can draw up front. You declare what artifacts exist and what agents can do with them; the runtime derives the order — and the dependency graph — from state. You don't draw the graph. Every derived artifact then links to what produced it (Answer —supported_by→ Evidence —extracted_from→ Doc), so "why did it say that?" is a query, not a guess — and the run is reproducible (context_hash) and verifiable (reactifact replay --verify).

If you know Celery… — the programming model, not its distributed runtime Python developers already know this model from Celery: define a **task**, declare what triggers it, let the runtime run it. `reactifact` applies it to agents: a task reacts to a **typed, versioned artifact** in the context, not to a queued message or a graph edge. | Celery | reactifact | | --- | --- | | a task | `Step` / `@step(produces=Model, consumes=Input)` — a unit of work that writes an artifact | | `delay()` / `apply_async()` | you don't call it: creating the input artifact **is** the trigger | | routing key / queue | `Consume(Type)` — which artifact type wakes the task | | chain / group / chord | several step `Consume`s / `produces`; the runtime derives the order | | result backend | the `Context` — typed, versioned artifacts | | worker | `Runtime` | **Not inherited:** a broker, a worker pool, `acks_late`/redelivery, cross-process exactly-once — the *model* is Celery-shaped, the deployment is not.

On top: a provable answer

examples/fintech_audit — a transactions CSV, a budget CSV and a policy doc, no API key. The model never computes the number; plain Python does, and the answer is linked to its evidence:

variance vs budget: +12.5%   ($45,000 actual vs $40,000 budget, 10% policy threshold → over)
answer sha256 5461290d…  ·  context sha256 24449f6f…

re-running the pipeline hashes identically — or verify a saved run:
  reactifact replay <store> --session <id> --verify 24449f6f…

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

How it works

                                   EVENT (created/updated)
                                          │
                                          ▼
CONTEXT ───────► ARTIFACTS ──► AGENTS REACT ──effects──► EFFECTS
   ▲                 │                                            │
   │                 │                                            │  compile
   └────── CONTEXT' ◄────────────────┘ PATCH ◄────────────────────┘

You describe what data exists, what artifacts exist, and what agents can do with them. The runtime does the plumbing.

The mental model

Traditional agent reactifact
A program follows a graph / plan Agents react to state changes
Messages are strings Typed artifacts (Claim, Evidence, Answer, …)
Orchestration is explicit Orchestration falls out of the state
A unit of work returns a change A step writes effects (effects) — the runtime compiles them
Retries/rollback are manual Context is versioned (git-like commits, diff, rollback)
"Who produced this?" is lost Provenance links every derived artifact to its inputs

Why effects instead of "return a change"?

At the heart of the loop is how a step makes a change. Many frameworks ask a unit of work to return its result, and some orchestrator applies it. reactifact inverts authorship: a step states what should change via effects (create / update / link / ask); the runtime compiles the effect set into one atomic patch — the whole step lands (or none of it does).

async def run(self, doc: Document, turn: Turn, effects: Effects) -> Answer:
    evidence = effects.create(Evidence(...), id="evidence:q1")
    answer = effects.create(Answer(...), id="answer:q1")
    evidence.link("extracted_from", doc)
    answer.link("supported_by", evidence)
    effects.update(turn, status="answered")
    return answer

Because handles are objects, not ids, one statement can reference the artifact created by another — and because the runtime owns the compilation, you never assemble a Patch by hand. Human-in-the-loop is just another effect (effects.ask(...)). See Why reactifact for the full argument, and The produce contract for details.

Why artifacts instead of messages?

Messages are opaque; artifacts are inspectable. An Evidence object knows its text, its source, and its score. Because every artifact carries provenance, the runtime can answer "why did the agent say that?" by walking the links Answer —supported_by→ Claim —derived_from→ Evidence —extracted_from→ Doc.

Why versioned context?

Every run is a commit. That gives you:

  • Diff — exactly what changed between two turns.
  • Rollback — undo a bad step and re-run from a clean state.
  • Deterministic replays — the same history reproduces the same result.
  • Inspectability — a full, queryable history of everything that happened.

Requires

  • Python 3.11–3.14 (.venv is managed by uv; CI runs the full suite on all four).
  • pydantic for artifact models; FastAPI/uvicorn only for the web demos.

Quick start

Two agents, no graph edge declared between them — the second reacts because the first one's output exists, and the answer carries proof of where it came from:

from pydantic import BaseModel

from reactifact import Budget, Context, Runtime, RuntimeResources, create_agent, step


class Question(BaseModel):
    text: str


class Evidence(BaseModel):
    text: str


class Answer(BaseModel):
    text: str


DOCS = {
    "refund": "Refunds are available within 14 days of purchase.",
    "pricing": "The Pro plan is $49/month, billed annually.",
}


@step(produces=Evidence, consumes=Question)
async def find_evidence(call):
    question = call.trigger
    hit = next((v for k, v in DOCS.items() if k in question.data.text.lower()), None)
    if hit is not None:
        call.effects.create(Evidence(text=hit))


@step(produces=Answer, consumes=Evidence)
async def answer_from_evidence(call):
    evidence = call.trigger
    call.effects.create(Answer(text=evidence.data.text)).link("supported_by", evidence)


search_agent = create_agent("search", steps=[find_evidence])
answer_agent = create_agent("answer", steps=[answer_from_evidence])

ctx = Context(resources=RuntimeResources())
runtime = Runtime(ctx, agents=[search_agent, answer_agent], budget=Budget(max_runs=10))

ctx.create(Question(text="what's your refund policy?"))
runtime.run()  # search_agent and answer_agent both react — nobody wired them together

answer = ctx.latest(Answer)
evidence = ctx.related(answer.id, "supported_by")[0]
print(answer.data.text)                     # "Refunds are available within 14 days of purchase."
print("supported_by:", evidence.data.text)  # provenance you can trace, not just a string in a log

That is the whole loop: create an artifact → agents react → a patch is applied → the context version advances. Everything else in this documentation builds on that loop.

Ready for something closer to a real app? Quickstart has three runnable, verified snippets — a tool-calling agent, retrieval over your own docs, a session-persisted chat bot — each pointing at the full example it's trimmed from.

Where to go next

Understand the idea

  • Why reactifact — the design argument: why effects, why no graph, why determinism, why versioned state.
  • Comparison — reactifact vs LangGraph/CrewAI, feature by feature, and when not to use reactifact.
  • Concepts — Context, Artifact, Patch, Step, Agent.

Build with it

  • Quickstart — three runnable snippets: tool-calling agent, retrieval over your docs, session-persisted chat bot.
  • Sources — where agents get information from.
  • Providers — wiring an LLM/embedder/image/speech provider into RuntimeResources.
  • Recipes — ready-made search fan-out, ref materialization, lifecycle state machines.
  • Patterns — reusable shapes (reflection, map-reduce, supervisor, …), each backed by a concrete example.

Operate it

  • Observability — every run traces agent spans, reads/writes, LLM calls; offline dashboard, or ship to Langfuse/Postgres.
  • Durability & resume — a session-backed run survives a process restart; what "at-least-once" means and how to make produces idempotent.
  • Logging — correlated structured logs (configure_logging()); each line carries run_id/session_id/agent.
  • Deadlines & shutdown — hard max_seconds, request_stop()/ashutdown(), active_runs() readiness.
  • Troubleshooting — "my agent didn't run" / "ran twice" / "the run stopped early" — organized by symptom, not by feature.
  • Evaluation — multi-level scoring over the final Context (evidence quality, provenance grounding, calculation correctness, …).
  • Branching & merge — fork state, explore alternatives, three-way merge with explicit conflicts.
  • Replay — reconstruct why the agent answered, deterministically, without re-running agents.
  • Visualization & CLI — Mermaid diagrams of the artifact graph and a run's trace; the reactifact command-line tools.
  • API reference — every top-level symbol, one line each.

See it run