# eva-agent-mvp — решения агента
Спека и дизайн — в `jira-hub/openspec/changes/wiki-module/` (issue aep/jira-hub#91, PR aep/jira-hub#92):
- решения 10, 11, 14 и Context (форматы ссылок Eva);
- требования `wiki` «Синхронизация вики» и «Вложения вики по запросу»;
- `modules` «Агенты модулей»;
- `hub-api` «Маршруты агента» (общие `/agent/*`).
Issue агента — aep/eva-agent#1. Версия агента — 1. Агент работает за модуль `wiki`: имя агента в хабе `wiki`, токен —
`jira-hub tokens` → «агент wiki».
Владелец 24.10.2026 разрешил делать агента ночью без обсуждения («завтра хочу уже иметь рабочий mvp»). Поэтому все
решения ниже приняты агентом и перечислены в разделе «Decisions made without discussion».
## Context
- **Источник для копирования — jira-agent v14** (`../jira-agent/src/agent.py`, `run.py`). Его части, не связанные с
Jira, отлажены с v2 по v14:
- HTTP-слой: `Http`, `HttpError`, `RedirectAway`, `TooBig`, `_SameOriginRedirect`;
- транспорт `FolderTransport` и клиент хаба `HubClient`;
- журнал `Journal`, цикл `Runner`, самообновление `Updater`;
- `acquire_lock`, CLI `run|check|selftest|prune`, `launcher_version`.
- **Клиент Eva и обход дерева — по образцу** `../eva_wiki_export.py`:
- JSON-RPC 2.2: `POST /api/?m=<метод>`, `Authorization: Bearer`, тело `{jsonrpc: "2.2", method, callid,
kwargs}`;
- повторы на 429/502/503/504 и сетевых сбоях;
- `CmfDocument.get`/`list`, страницы списка по 200;
- дерево — по `parent`/`tree_parent` из списка документов проекта (`project.code`), запасной путь — обход через
`tree_parent.code`.
- **Хаб принимает:**
- пачки `POST /agent/wiki/pages {command_id, pages}` — до 50 страниц, тело до 16 МБ (`MAX_BODY` хаба);
- форму страницы проверяет `modules/wiki/stage.py`;
- отчёт `wiki_sync done {root_title, codes, eva_calls}`: `codes[0]` должен быть кодом корня;
- отчёт `wiki_fetch done {file, size}`: файл должен лежать в хранилище хаба с этим размером.
## Decisions made without discussion
Дата всех решений — 2026-10-24.
1. **Скопировать общие части jira-agent v14, а не выделять библиотеку.**
Обоснование: решение владельца 23.10 — однофайловый агент на stdlib на виртуалке. Копия сохраняет отлаженное
поведение.
Цена ошибки: правки общих частей придётся повторять в двух агентах.
2. **`run.py` — дословная копия запускателя jira-agent** (`LAUNCHER_VERSION = 2`), вместе с переменной окружения
`JIRA_AGENT_LAUNCHER`.
Обоснование: в протоколе самообновления важны имена `agent.py`, `update.json` и `AGENT_VERSION`; переменная —
внутреннее дело пары запускатель–агент.
Цена ошибки: переименование переменной в обоих файлах.
3. **Конфиг `agent.json`:**
- обязательны `hub_url`, `hub_token`, `eva.base_url`, `eva.token`;
- необязательны `verify_ssl`, `ca_bundle`, `timeout` (60), `poll_seconds` (5), `auto_update` (true).
Обоснование: формат jira-agent, где ключ `jira` заменён на `eva`.
Цена ошибки: правка шаблона и `load_config`.
4. **Пауза между вызовами Eva — `wiki_pacing` из `GET /agent/settings`** (по умолчанию 0.2 с).
- 429/502/503/504 и сетевые сбои: до 4 попыток с ожиданием 1, 2, 4 с.
- При 429 учитывается `Retry-After` (не больше 60 с).
- Число 429 уходит в `rate_limited` отчёта, число вызовов Eva — в `jira_calls`: поле протокола хаба, имя общее
для всех агентов.
Цена ошибки: Eva режет запросы; правится настройкой без выпуска агента.
5. **`wiki_sync` — порядок работы:**
1. Корень берётся через `CmfDocument.get` по коду из `params.root`. Нет корня — `failed` «страница <код> не найдена
или нет прав».
2. Дерево строится как в `collect_subtree`.
3. Обход в глубину от корня с защитой от циклов (`seen` по `id`). Соседи — по `(orderno is None, orderno, name)`.
4. По каждой странице отдельный `CmfDocument.get(fields=["code","name","text"])`.
5. Страницы уходят пачками по мере обхода: 50 страниц или 4 МБ JSON, что наступит раньше.
6. Отчёт `codes` — коды в порядке обхода, корень первым.
- Поля страницы: `eva_id = str(id)`, `parent_code` — код родителя по обходу (у корня `null`), `orderno` — целое или
`null`, `url = urljoin(base_url + "/", "project/Document/")`.
Цена ошибки: дерево не совпадёт с Eva; лечится правкой обхода и новой версией через самообновление.
6. **Ссылки разбирает `html.parser.HTMLParser` из stdlib.**
- Берутся `img[src]` и `a[href]`.
- Адрес разрешается `urljoin(base_url + "/", ref)`. «Свой» — тот же `scheme` и `netloc`, что у `eva.base_url`.
- **Вложение** — свой адрес с путём, начинающимся на `/files/`. Из `url` убираются query и fragment. Миниатюра
`<путь>.meta/thumbnail.jpg` означает файл `<путь>`.
- **Ссылка на страницу** — свой адрес с `DOC-<цифры>` в пути (код в верхнем регистре).
- **`static/…`** и прочие ссылки не передаются: значки убирает хаб, прочие остаются в тексте.
- **Имя вложения** — `alt` или `title`, затем текст ссылки, затем последний сегмент пути после `unquote`.
- Каждая ссылка передаётся отдельной записью `{ref, url, name}` или `{ref, code}`, `ref` — ровно значение
атрибута. Повторы по `url` склеивает хаб.
Обоснование: форматы из выгрузки ТЗ АРМ КА (design хаба, Context).
Цена ошибки: вложение продублируется или потеряется; проверяется на первой живой синхронизации, правится агентом.
7. **Без `?version=1` в `url` вложения.**
Обоснование: миниатюра и файл дают один адрес, хаб склеивает вложение.
Цена ошибки: если Eva без `version` не отдаёт файл, `wiki_fetch` падает; правка одной строки.
8. **`wiki_fetch`:**
- адрес не своего хоста — `failed` «чужой хост», запроса нет;
- файл скачивается с Bearer-токеном Eva потоком до 10 МБ + 1 байт;
- `Content-Length` больше 10 МБ или прочитано больше — `failed` «файл больше 10 МБ — откройте в Eva»;
- иначе `POST /agent/files` и `done {file, size}`;
- HTTP 404 — `failed` «файл не найден в Eva».
Цена ошибки: большие файлы только ссылкой.
9. **Обе команды только читают Eva, поэтому восстановление после перезапуска выполняет их заново.** Журнал нужен
только для досылки отчёта; `wiki_sync` не продолжается с середины. Хаб очищает стадию при повторной выдаче.
Цена ошибки: лишняя полная перекачка после перезапуска.
10. **`check`:**
- хаб: `GET /agent/settings` своим токеном;
- Eva: `CmfDocument.list(filter=[], fields=["code"], slice=[0, 1])`;
- вывод «хаб: ok», «eva: ok» или текст ошибки, код 0 или 3, как у jira-agent;
- `selftest` — версия плюс `check`.
11. **Тесты:**
- `unittest` на `/usr/bin/python3` (3.9);
- фейки `tests/fake_hub.py` (как у jira-agent, плюс `POST /agent/wiki/pages`), `tests/fake_eva.py` (JSON-RPC на
`http.server`), `tests/fake_relay.py`;
- `tests/test_py39.py` — копия.
- Тесты общих частей переносятся из jira-agent и адаптируются: журнал, транспорт, самообновление, запускатель.
12. **`ref` вложений и ссылок — значение атрибута после unescape** (так отдаёт `html.parser`).
Обоснование: хаб (`convert.py`, BeautifulSoup `.get("src"/"href")`) ищет запись по тому же unescaped-значению.
Цена ошибки: адреса с `&` не сопоставятся, вложение останется неразрешённым; правка одной строки.
13. **При расхождении плана с кодом хаба по протоколу прав код хаба.** Поручение владельца; расхождения фиксируются
находками `O-n`, хаб не правится.
Цена ошибки: агент подгонится под ошибочное поведение хаба; видно в e2e.