# Агентный пайплайн упал на шаге 4 из 7 — как не запускать всё заново
Каноническая страница: https://plainews.ru/posts/event-sourcing-llm-agents-resume-pipeline
Опубликовано: 2026-05-29T07:01:20.152Z

Агентный пайплайн упал на середине — как не начинать заново? 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 для старта не нужно.

## Источники

- Habr AI: Приручаем недетерминизм агентных систем
