Andrey Epifancev 67f1a5a096 Каркас eva-agent: общие части jira-agent v14 (#1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 04:40:33 +04:00

142 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 <base>/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/<code>")`.
Цена ошибки: дерево не совпадёт с 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-значению.
Цена ошибки: адреса с `&amp;` не сопоставятся, вложение останется неразрешённым; правка одной строки.
13. **При расхождении плана с кодом хаба по протоколу прав код хаба.** Поручение владельца; расхождения фиксируются
находками `O-n`, хаб не правится.
Цена ошибки: агент подгонится под ошибочное поведение хаба; видно в e2e.
14. **Каркас `EvaClient` создан в задаче 1; `Runner(cfg, hub, eva, journal)`.** В `EvaClient` — только Bearer-заголовок,
TLS-настройки, счётчики `calls`/`rate_limited` и время последнего вызова (`last_call_at`, `last_call_iso`).
Обоснование: `Runner` и `Executor` из jira-agent опираются на эти поля клиента (пауза между командами, отчёт); без
каркаса общие тесты цикла не перенести. JSON-RPC и повторы добавляют задачи 2–3.
Цена ошибки: перенос полей в другой класс.
15. **`claim` шлёт `last_call_at`, а не `last_jira_call_at`.**
Обоснование: хаб (`core/api.py`) читает `body.get("last_call_at", body.get("last_jira_call_at"))`; новое имя не
привязано к Jira. Поля отчёта `jira_calls`/`rate_limited` остаются как есть: хаб читает только их.
Цена ошибки: если хаб уберёт новое имя, время последнего вызова перестанет доходить (только статус).
16. **Jira-специфичные тесты jira-agent не переносились; `test_clients.py` написан заново на `MiniServer`.**
Исключены: `JiraClient` (multipart, 429/503), `Executor`, `refresh`, история, вложения Jira. Сценарии `Http`
(перенаправления, обрывы, не-JSON, предел `fetch_bytes`) сохранены на маленьком локальном сервере, фейка Eva в
задаче 1 нет (появляется в задаче 2).
Обоснование: брифом перенос ограничен общими частями; Jira в агенте нет.
Цена ошибки: пропущенный сценарий `Http`; виден по сравнению со списком тестов jira-agent.
17. **Тесты цикла работают на заглушке `wiki_fetch` (`failed` «не реализовано»); паузу и время вызова Eva тесты
задают вручную (`eva.last_call_at`, `eva._touch()`).** Результат `done` проверяется через подмену
`Executor.run`/`do_wiki_fetch`.
Обоснование: уточнение к задаче 1. Цена ошибки: тесты цикла придётся уточнить, когда команды заработают.
18. **`test_run_py_is_copy` ищет `jira-agent/src/run.py` на 1–3 уровня выше корня репозитория; нет файла — skip с
причиной.** Путь из плана (`../../jira-agent`) верен для основного checkout, из worktree `.wt/<срез>` — на уровень
глубже. Цена ошибки: тест тихо пропускается на нестандартной раскладке каталогов.
19. **`.gitignore` дополнен `agent.py.new`, `agent.py.prev`, `__pycache__/`, `*.pyc`** (рабочие файлы самообновления
и кэш). Цена ошибки: нет.