Агентный пайплайн упал на шаге 4 из 7 — как не запускать всё заново
Агентный пайплайн упал на середине — как не начинать заново? Event Sourcing даёт иммутабельный лог и точечный resume. Разбираем на примере zymi.

LLM-агенты ломаются не там, где вы ожидаете. Не на сложных задачах и не из-за плохой модели — а потому что каждый запуск чуть-чуть другой. Температура, тихий апдейт модели на стороне провайдера, изменившийся внешний API — и воспроизвести падение уже невозможно. Event Sourcing не убирает этот недетерминизм, но даёт конкретные инструменты: иммутабельный лог, точечный перезапуск и честный аудит.
Почему снапшот состояния не спасает
Стандартный подход — сохранять состояние агента как снапшот: JSON с текущим контекстом, промежуточными результатами, статусом шагов. Выглядит разумно, пока не нужно отлаживать.
Снапшот говорит «что есть сейчас», но не говорит «как мы сюда попали». Если агент на шаге 5 выдал странный результат, вы не знаете, какой именно LLM-вызов на шаге 2 его предопределил. Нет истории — нет отладки.
Event Sourcing переворачивает логику: состояние не хранится явно, оно выводится из журнала событий. Каждое действие — вызов инструмента, ответ LLM, решение агента — пишется в лог как иммутабельная запись. Состояние в любой момент — это проекция этого лога. Паттерн систематизировал Мартин Фаулер ещё в 2005-м, связку ES + CQRS закрепил Грег Янг в 2014-м. К LLM-агентам его прямо применили уже в 2026-м: работы ESAA (arXiv:2602.23193) и OpenKedge (arXiv:2604.08601).
Как выглядит ESA изнутри
Event-Sourced Architecture для агентов (ESA) — это event-driven шина с одним жёстким инвариантом: в лог можно только писать, никогда не редактировать.
Агенты на этой шине — обычные подписчики. Каждый ждёт нужных типов событий, забирает их в работу, результаты пишет туда же. Никакого прямого вызова между агентами — только через лог.
```python
Упрощённая схема: агент читает из лога и пишет в лог
def researcher_agent(stream_id: str, log: EventLog): events = log.read(stream_id, event_types=["TaskAssigned"]) for event in events: result = run_search(event.payload["query"]) log.append(stream_id, { "type": "ResearchCompleted", "payload": result, "prev_hash": log.last_hash(stream_id) }) ```
Три свойства, которые вы получаете от иммутабельного лога — перезапуск пайплайна, единый источник правды и аудируемость — это на самом деле одно и то же свойство, рассмотренное с трёх сторон. Если одно ломается (например, вам нужно «удалить» запись по запросу пользователя), ломаются все три сразу.
Перезапуск с произвольного шага
Допустим, у вас пайплайн: researcher собирает данные → writer готовит отчёт. Researcher отработал нормально, writer выдал мусор. Классический сценарий: переписать промпт writer'а и перезапустить — но не трогать дорогостоящий поиск.
В zymi это одна команда:
``bash zymi resume <stream_id> --from-step writer ``
Что происходит под капотом: создаётся новый стрим, в него физически копируется событийный префикс researcher'а — все его LLM-вызовы, результаты поиска, промежуточные артефакты. Writer перезапускается с обновлённым промптом из agents/writer.yml. Всё до точки resume — заморожено.
Отдельная проблема — внешние эффекты внутри переисполняемой части. Если writer на прошлом прогоне отправил email, повторно отправлять его нельзя. Для этого у инструментов есть флаг no_resume: true:
```yaml
agents/writer.yml
tools:
no_resume: true
no_resume: false ```
- name: send_email
- name: write_report
При resume инструмент с no_resume: true не вызывается. В журнал пишется:
``json { "type": "ToolCallCompleted", "tool": "send_email", "replayed": true, "result": "__skipped_on_resume__" } ``
Агент в следующем ходе видит это событие и знает: email уже был отправлен раньше, повторно не делал.
Лог как единственный источник правды
Выгода лога раскрывается только если все потребители и источники взаимодействуют исключительно через него. Никаких прямых вызовов между агентами, никаких side-channel обновлений состояния.
Это строгая дисциплина, но она окупается: подключить нового потребителя — значит создать новый файл, который читает тот же лог. Не новый интерфейс в рантайме, не новый эндпоинт — просто новый подписчик.
В zymi это проявляется буквально. Команда zymi verify проверяет целостность SHA-256 хэш-цепочки лога:
``bash zymi verify ``
Реализация — 76 строк, которые не импортируют рантайм вообще. Открывает тот же SQLite, перебирает стримы, валидирует хэши:
```python import sqlite3, hashlib
def verify_stream(db_path: str, stream_id: str) -> bool: conn = sqlite3.connect(db_path) rows = conn.execute( "SELECT hash, prev_hash, payload FROM events WHERE stream_id=? ORDER BY seq", (stream_id,) ).fetchall() for row in rows: expected = hashlib.sha256( (row[1] or "") + row[2] ).hexdigest() if expected != row[0]: return False return True ```
По тому же принципу работают zymi observe (TUI с живым графом пайплайна) и zymi runs (CLI-листинг прогонов) — каждая команда это самостоятельный потребитель поверх одного лога, в ядро никто не лезет.
Где это ломается
GDPR и право на удаление. Иммутабельный лог и «удалите мои данные» — прямое противоречие. Решения есть (crypto shredding, отдельный слой персональных данных), но они добавляют сложности и частично ломают гарантию единого источника правды.
Лог растёт бесконечно. Для долгоживущих агентов без компактизации лог становится узким местом по производительности. Нужна стратегия снапшотов или TTL на старые стримы — иначе проекция состояния начинает считаться секундами.
Недетерминизм никуда не девается. Fork-resume снижает его влияние, но не устраняет. Если вы перезапускаете writer с тем же промптом и той же моделью — результат всё равно может отличаться. Лог даёт воспроизводимость истории, но не детерминизм будущего.
Внешние события сложно вписать. Если агент реагирует на webhook от внешней системы, это событие нужно явно ввести в лог до того, как агент его обработает. Пропустили — и источник правды уже не единственный.
Что попробовать дальше
Если хотите глубже в теорию: ESAA (arXiv:2602.23193) — формализация Event Sourcing для агентов, OpenKedge (arXiv:2604.08601) — governance поверх event-sourced состояния.
Если интереснее production-решения: Temporal и Restate решают ту же задачу в словаре workflow/activity — «храним историю событий, реплеим детерминированно». Они старше и battle-tested, но требуют отдельной инфраструктуры.
Сам zymi — это Python-библиотека, которую можно попробовать как минимальную реализацию ESA для своих агентов. Команды resume, verify, observe работают поверх обычного SQLite — никакого Kafka для старта не нужно.