Skip to content

reactifact

Событийные агенты для Python-разработчиков — задачи просыпаются на типизированных артефактах, как задачи Celery просыпаются на сообщениях. Граф рисовать не нужно.

Python-разработчик уже знает эту модель по Celery: объяви задачу, скажи, что её запускает, — рантайм сам её выполнит. reactifact применяет это к агентам: задача реагирует на появление типизированного версионируемого артефакта в контексте, а не на сообщение, которое вы пушите, и не на ребро графа. Рантайм выводит, что запустится дальше, из состояния.

Celery reactifact
задача (task) @produce(Model) — единица работы, пишущая артефакт
delay() / apply_async() вызывать не нужно: создание входного артефакта и есть триггер
routing key / очередь Consume(Type) — какой тип артефакта будит задачу
chain / group / chord несколько consumes / produces; порядок рантайм выводит сам
retries, acks_late гарды + Budget, честный None вместо неверного результата
result backend Context — типизированные версионируемые артефакты
worker Runtime

Сегодня — один процесс (без брокера и пула воркеров): Celery-образная модель, а не его распределённый рантайм.

Сверх этой модели вы получаете то, чего у очереди задач нет: каждый артефакт версионируется с провенансом, поэтому запуск воспроизводим (context_hash) и аудируем (audit.report) бесплатно. Модель рассуждает; детерминированные части остаются в коде; каждое утверждение несёт провенанс.

Сверх этого: проверяемый ответ

examples/fintech_audit — CSV транзакций, CSV бюджета и документ политики, без API-ключа. Число никогда не считает модель; это делает обычный Python, а ответ связан со своими доказательствами:

variance vs budget: +12.5%   ($45,000 факт vs $40,000 бюджет, порог 10% → превышен)
answer sha256 5461290d…  ·  context sha256 24449f6f…

повторный прогон даёт тот же хеш — или проверьте сохранённый запуск:
  reactifact replay <store> --session <id> --verify 24449f6f…

fintech_audit demo: вариация, ответ и воспроизводимый context hash — повторный прогон печатает тот же хеш.

Как это работает

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

Вы описываете какие данные существуют, какие артефакты есть и что агенты могут с ними делать. Остальное делает runtime.

Ментальная модель

Традиционный агент reactifact
Программа идёт по графу/плану Агенты реагируют на изменения состояния
Сообщения — строки Типизированные артефакты (Claim, Evidence, Answer, …)
Оркестрация явная Оркестрация выводится из состояния
Компонент возвращает результат Produce пишет effects (self.effects) — компилирует runtime
Ретраи/откаты вручную Контекст версионируется (коммиты как в git, diff, rollback)
«Кто это произвёл?» теряется Провенанс связывает каждый производный артефакт с его входами

Почему effects вместо «вернуть изменение»?

В центре цикла — то, как produce вносит изменение. Многие фреймворки просят производителя вернуть результат, а какой-то оркестратор применяет его. reactifact переворачивает авторство: produce формулирует, что должно измениться, через self.effects (create / update / link / ask) и возвращает None; runtime компилирует набор эффектов в один атомарный патч — либо применяется весь шаг, либо ничего.

async def produce(self, call: ProduceCall) -> None:
    evidence = self.effects.create(Evidence(...), id="evidence:q1")
    answer = self.effects.create(Answer(...), id="answer:q1")
    evidence.link("extracted_from", doc)
    answer.link("supported_by", evidence)
    self.effects.update(turn, status="answered")
    return None

Поскольку handle — это объекты, а не id, одно выражение может ссылаться на артефакт, созданный другим. А так как компиляцией владеет runtime, ручной сборки Patch не бывает. Участие человека — просто ещё один эффект (effects.ask(...)). Подробнее — в Почему reactifact и в контракте produce.

Почему артефакты вместо сообщений?

Сообщения непрозрачны; артефакты инспектируемы. Объект Evidence знает свой текст, источник и оценку. Благодаря провенансу runtime отвечает на вопрос «почему агент так сказал?», проходя по связям Answer —supported_by→ Claim —derived_from→ Evidence —extracted_from→ Doc.

Почему версионируемый контекст?

Каждый запуск — это коммит:

  • Diff — что именно изменилось между двумя ходами.
  • Rollback — отменить неудачный шаг и перезапустить с чистого состояния.
  • Детерминированные повторы — та же история даёт тот же результат.
  • Инспектируемость — полная, запрашиваемая история всего, что произошло.

Требования

  • Python 3.11–3.14 (.venv управляется через uv; CI гоняет весь набор тестов на всех четырёх).
  • pydantic для моделей артефактов; FastAPI/uvicorn — только для web-примеров.

Быстрый старт

Два агента, и между ними не объявлено ни одной связи в графе — второй реагирует потому, что появился результат первого, а ответ несёт доказательство того, откуда он взялся:

from pydantic import BaseModel

from reactifact import Budget, Consume, Context, Runtime, RuntimeResources, create_agent, produce


class Question(BaseModel):
    text: str


class Evidence(BaseModel):
    text: str


class Answer(BaseModel):
    text: str


DOCS = {
    "refund": "Возврат возможен в течение 14 дней с момента покупки.",
    "pricing": "Тариф Pro — $49/месяц при годовой оплате.",
}


@produce(Evidence)
async def find_evidence(call):
    question = next((a for a in call.inputs if isinstance(a.data, Question)), None)
    if question is None:
        return None
    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))


@produce(Answer)
async def answer_from_evidence(call):
    evidence = next((a for a in call.inputs if isinstance(a.data, Evidence)), None)
    if evidence is None:
        return None
    call.effects.create(Answer(text=evidence.data.text)).link("supported_by", evidence)


search_agent = create_agent("search", consumes=[Consume(Question)], produces=[find_evidence])
answer_agent = create_agent("answer", consumes=[Consume(Evidence)], produces=[answer_from_evidence])

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

ctx.create(Question(text="какая у вас политика возврата?"))
runtime.run()  # оба агента реагируют сами — никто их не связывал вручную

answer = ctx.latest(Answer)
evidence = ctx.related(answer.id, "supported_by")[0]
print(answer.data.text)                     # "Возврат возможен в течение 14 дней с момента покупки."
print("supported_by:", evidence.data.text)  # провенанс, который можно проследить, а не строка в логе

Это весь цикл: создан артефакт → агенты реагируют → применён патч → версия контекста продвинулась. Всё остальное в этой документации надстраивается над этим циклом.

Хотите чего-то ближе к реальному приложению? Quickstart — три рабочих, проверенных сниппета: tool-calling агент, поиск по своим документам, чат-бот с сохраняемыми сессиями — каждый со ссылкой на полный пример, из которого он урезан.

Куда дальше

Понять идею

  • Почему reactifactдизайн-аргумент: почему effects, почему без графа, почему детерминизм, почему версионируемое состояние.
  • Сравнение — reactifact vs LangGraph/CrewAI по пунктам, и когда reactifact не стоит использовать.
  • Concepts — Context, Artifact, Patch, Agent, Produce.

Строить на этом

  • Quickstart — три рабочих сниппета: tool-calling агент, поиск по своим документам, чат-бот с сохраняемыми сессиями.
  • Sources — откуда агенты берут информацию.
  • Providers — как подключить LLM/эмбеддер/провайдер изображений/речи в RuntimeResources.
  • Recipes — готовые search fan-out, материализация референсов, машины состояний жизненного цикла.
  • Patterns — переиспользуемые паттерны (reflection, map-reduce, supervisor, …), каждый — на конкретном примере.

Эксплуатировать

  • Observability — трейс каждого запуска: агентские спаны, чтения/записи, LLM-вызовы; офлайн-дашборд или экспорт в Langfuse/Postgres.
  • Диагностика проблем — «агент не запустился» / «запустился дважды» / «run остановился раньше времени» — по симптомам, не по фичам.
  • Evaluation — многоуровневая оценка финального Context (качество evidence, provenance grounding, корректность вычислений, …).
  • Branching & merge — форк состояния, исследование альтернатив, трёхстороннее слияние с явными конфликтами.
  • Replay — детерминированная реконструкция «почему агент ответил именно так», без повторного запуска агентов.
  • Visualization & CLI — Mermaid-диаграммы графа артефактов и трейса запуска; CLI-утилиты reactifact.
  • API reference — все верхнеуровневые символы, по строке на каждый.

Посмотреть в деле

  • Examples — четырнадцать работающих приложений, которые можно запустить.
  • Port matrix — какой классический паттерн LangGraph/CrewAI/DSPy соответствует какому примеру.
  • Design notes — более глубокое обоснование адаптивного планировщика; модель компиляции effects → Patch описана в English design note (пока не переведена — это архивный лог обсуждения, актуальное поведение уже в effects.md).