reactifact¶
Аудируемые агенты для Python. Стройте knowledge-ассистентов по докам, таблицам и вебу — там, где путь к ответу зависит от вопроса и графом не ложится. Каждый ответ вычисляется, версионируется и воспроизводим — а не строка, которой приходится верить.
Это: Python-библиотека (3 core-зависимости) · один процесс · типизированные версионируемые артефакты с провенансом · детерминированный replay · работает офлайн, без API-ключа Это не: managed-платформа · распределённая очередь задач · готовый маркетплейс агентов/инструментов
Вопрос вроде «почему в Q2 выросли инфраструктурные расходы?» требует таблицы,
политики и страницы из веба — а следующий вопрос требует уже другого набора.
Это не один граф, который можно нарисовать заранее. Вы объявляете, какие
артефакты существуют и что агенты умеют с ними делать; порядок — и граф зависимостей — рантайм выводит из
состояния; рисовать граф не нужно. Каждый производный артефакт затем ссылается на то, что его создало
(Answer —supported_by→ Evidence —extracted_from→ Doc), поэтому «почему он так
сказал?» — это запрос, а не догадка, а запуск воспроизводим (context_hash) и
проверяем (reactifact replay --verify).
Если вы знаете Celery… — модель программирования, а не его распределённый рантайм
Python-разработчик уже знает эту модель по Celery: объяви **задачу**, скажи, что её запускает, — рантайм сам её выполнит. `reactifact` применяет это к агентам: задача реагирует на появление **типизированного версионируемого артефакта** в контексте, а не на сообщение в очереди и не на ребро графа. | Celery | reactifact | | --- | --- | | задача (task) | `Step` / `@step(produces=Model, consumes=Input)` — единица работы, пишущая артефакт | | `delay()` / `apply_async()` | вызывать не нужно: создание входного артефакта **и есть** триггер | | routing key / очередь | `Consume(Type)` — какой тип артефакта будит задачу | | chain / group / chord | несколько `consumes` / `produces`; порядок рантайм выводит сам | | result backend | `Context` — типизированные версионируемые артефакты | | worker | `Runtime` | **Не наследуется:** брокер, пул воркеров, `acks_late`/redelivery, cross-process exactly-once — *модель* Celery-образная, деплой — нет.Сверх этого: проверяемый ответ¶
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…

Как это работает¶
EVENT (created/updated)
│
▼
CONTEXT ───────► ARTIFACTS ──► AGENTS REACT ──effects──► EFFECTS
▲ │ │
│ │ │ compile
└────── CONTEXT' ◄────────────────┘ PATCH ◄────────────────────┘
Вы описываете какие данные существуют, какие артефакты есть и что агенты могут с ними делать. Остальное делает runtime.
Ментальная модель¶
| Традиционный агент | reactifact |
|---|---|
| Программа идёт по графу/плану | Агенты реагируют на изменения состояния |
| Сообщения — строки | Типизированные артефакты (Claim, Evidence, Answer, …) |
| Оркестрация явная | Оркестрация выводится из состояния |
| Компонент возвращает результат | Step пишет effects (effects) — компилирует runtime |
| Ретраи/откаты вручную | Контекст версионируется (коммиты как в git, diff, rollback) |
| «Кто это произвёл?» теряется | Провенанс связывает каждый производный артефакт с его входами |
Почему effects вместо «вернуть изменение»?¶
В центре цикла — то, как шаг вносит изменение. Многие фреймворки просят
единицу работы вернуть результат, а какой-то оркестратор применяет его.
reactifact переворачивает авторство: шаг формулирует, что должно
измениться, через effects (create / update / link / ask); runtime
компилирует набор эффектов в один атомарный патч — либо применяется весь шаг,
либо ничего.
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
Поскольку handle — это объекты, а не id, одно выражение может ссылаться на
артефакт, созданный другим. А так как компиляцией владеет runtime, ручной
сборки Patch не бывает. Участие человека — просто ещё один эффект
(effects.ask(...)). Подробнее — в Почему reactifact и в
контракте шага.
Почему артефакты вместо сообщений?¶
Сообщения непрозрачны; артефакты инспектируемы. Объект 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, Context, Runtime, RuntimeResources, create_agent, step
class Question(BaseModel):
text: str
class Evidence(BaseModel):
text: str
class Answer(BaseModel):
text: str
DOCS = {
"refund": "Возврат возможен в течение 14 дней с момента покупки.",
"pricing": "Тариф Pro — $49/месяц при годовой оплате.",
}
@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="какая у вас политика возврата?"))
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, Step, Agent.
Строить на этом
- Quickstart — три рабочих сниппета: tool-calling агент, поиск по своим документам, чат-бот с сохраняемыми сессиями.
- Sources — откуда агенты берут информацию.
- Providers — как подключить LLM/эмбеддер/провайдер
изображений/речи в
RuntimeResources. - Recipes — готовые search fan-out, материализация референсов, машины состояний жизненного цикла.
- Patterns — переиспользуемые паттерны (reflection, map-reduce, supervisor, …), каждый — на конкретном примере.
Эксплуатировать
- Observability — трейс каждого запуска: агентские спаны,
чтения/записи, LLM-вызовы; офлайн-дашборд или экспорт в Langfuse/Postgres;
коррелированные логи (
configure_logging()) несутrun_id/session_id/agentв каждой строке. - Устойчивость и resume — прогон на сессии переживает рестарт
процесса; что значит «at-least-once» и как сделать produce идемпотентным;
дедлайны и graceful shutdown (
Budget.max_seconds— жёсткий,request_stop()/ashutdown(),active_runs()для readiness). - Диагностика проблем — «агент не запустился» / «запустился дважды» / «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).