142 lines
12 KiB
Markdown
142 lines
12 KiB
Markdown
---
|
||
work_item: ORCH-099
|
||
stage: analysis
|
||
author_agent: analyst
|
||
status: ready-for-review
|
||
created_at: 2026-06-10
|
||
model_used: claude-opus-4-8
|
||
---
|
||
|
||
# 01 — BRD (бизнес-требования): ORCH-099 — FND/F1a: лёгкий `/metrics` в орке (отдать сырьё)
|
||
|
||
Work Item: **ORCH-099** · Repo: **orchestrator** · Стадия: analysis
|
||
|
||
## 1. Бизнес-контекст и проблема
|
||
|
||
Задача — фундаментный кирпич **F1a** домена 0 «Фундамент» эпика автономного саморазвития
|
||
(`docs/epics/self-evolution.md`). Архитектурная рамка наблюдаемости **зафиксирована заказчиком
|
||
(Слава, 09.06)** и для аналитика — установленный факт, не предмет переизобретения:
|
||
|
||
- **C-1/C-1б:** наблюдатель ОТДЕЛЁН от наблюдаемого. Мониторинг живёт в **отдельном sidecar-контейнере**
|
||
(`watchdog/`, рантайм — свой Dockerfile + сервис в compose), а НЕ внутри орка. Если орк
|
||
упал/завис/съел память — sidecar жив и репортит это.
|
||
- **C-2/C-3:** без внешнего плеча, тонкий стек (хост впритык: RAM 171Mi free, диск 92% — НЕ
|
||
Grafana/Prometheus).
|
||
- **Разделение ответственности:** орк отдаёт **только сырьё** (лёгкий read-only `/metrics` — свои
|
||
внутренние данные, которые знает только он сам), БЕЗ логики мониторинга/порогов/алертов/хранения.
|
||
Мозг (пороги, алерты, свой Telegram-канал, история) — это **F1b (sidecar)**, отдельная задача.
|
||
|
||
**Боль, которую закрывает задача.** Сегодня у орка нет машинного «сырья» о самом себе в одной
|
||
точке. `/health` отдаёт лишь `{"status":"ok"}`, `/status` — список активных задач, `/queue` —
|
||
богатый, но «человеческий» снимок очереди, перемешанный с конфигом демонов. Ни один из них не даёт
|
||
sidecar'у структурированный, стабильный КОНТРАКТ для детекта: застрявшая стадия, зависший агент
|
||
(liveness по pid/CPU), деградация очереди (breaker open, рост failed), всплеск стоимости токенов.
|
||
Без этого источника весь домен наблюдаемости (F1b и далее) слеп и не может стартовать.
|
||
|
||
**Self-hosting контекст.** Орк дорабатывает сам себя; прод-контейнер общий для всех проектов.
|
||
`/metrics` обязан быть **строго read-only** и **never-raise** — он не должен ни при каких входных
|
||
данных уронить или притормозить прод, обслуживающий enduro-trails.
|
||
|
||
## 2. Объём (scope)
|
||
|
||
### В объёме
|
||
- Новый **read-only** HTTP-эндпоинт (`GET /metrics`), отдающий JSON-снимок сырья о самом орке.
|
||
- Четыре раздела сырья: **активные стадии задач**, **очередь jobs**, **agent-liveness**,
|
||
**стоимость/токены** (`agent_runs`).
|
||
- Новый leaf-модуль `src/metrics.py` — сборка снимка из БД (чистый, never-raise, без побочных
|
||
эффектов), по образцу `snapshot()`-функций (`serial_gate`/`task_deps`/`cancel`).
|
||
- Документирование формата `/metrics` как **контракта для sidecar (F1b)** в
|
||
`docs/architecture/README.md` + запись в `CHANGELOG.md`.
|
||
- Pytest-покрытие: структура ответа, never-raise, read-only-инвариант.
|
||
|
||
### Вне объёма
|
||
- ❌ Любая логика мониторинга: пороги, алерты, Telegram, оценка «застрял/завис», хранение истории
|
||
— это **F1b (sidecar)**.
|
||
- ❌ Сам sidecar-контейнер (`watchdog/`, Dockerfile, compose-сервис) — отдельная задача F1b.
|
||
- ❌ Хостовые/контейнерные/внешние метрики (диск/RAM/CPU хоста, docker.sock, пинг
|
||
Plane/Gitea/Anthropic) — их собирает sidecar, не орк.
|
||
- ❌ Изменение `STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / схемы БД / любых machine-verdict
|
||
ключей.
|
||
- ❌ Дашборд/UI (упомянут в F1 эпика как отдельный последующий шаг).
|
||
- ❌ Прометей-совместимый text-формат — отдаём JSON (контракт под конкретный sidecar; OpenMetrics
|
||
не требование заказчика).
|
||
|
||
## 3. Заинтересованные стороны
|
||
|
||
- **Заказчик:** Слава (рамки наблюдаемости F1, эпик саморазвития).
|
||
- **Прямой потребитель контракта:** будущий sidecar **F1b** (`watchdog/`) — читает `/metrics` по
|
||
HTTP. Задача F1b **заблокирована** этой (ORCH-099 — источник контракта).
|
||
- **Затрагивается:** прод-инстанс орка (общий с enduro-trails) — поэтому жёсткое требование
|
||
read-only/never-raise.
|
||
- **Принимает результат:** reviewer/tester конвейера + Слава как владелец рамок.
|
||
|
||
## 4. Бизнес-требования (BR)
|
||
|
||
- **BR-1 — Эндпоинт сырья.** Орк предоставляет HTTP `GET /metrics`, отдающий JSON с четырьмя
|
||
разделами: (a) активные стадии задач, (b) очередь jobs, (c) agent-liveness, (d) стоимость/токены.
|
||
Состав полей каждого раздела — см. TRZ §3 (FR-1…FR-4).
|
||
- **BR-2 — Стадии задач.** По каждой незавершённой задаче отдаётся `work_item`, текущая `stage` и
|
||
«как давно в стадии» (секунды) — сырьё для детекта застреваний sidecar'ом.
|
||
- **BR-3 — Очередь jobs.** Отдаются счётчики по статусам (`queued`/`running`/`failed`/…), глубина
|
||
очереди, информация о ретраях и состояние circuit-breaker'а — сырьё для детекта деградации.
|
||
- **BR-4 — Agent-liveness.** По каждому running-job отдаётся `agent`, `run_id`, `pid`, `runtime_s`
|
||
и сырьё для alive-детекта (CPU-тики pid либо данные, по которым sidecar посчитает CPU-дельту).
|
||
sidecar — арбитр «жив/завис»; орк лишь поставляет факты.
|
||
- **BR-5 — Стоимость/токены.** Отдаётся текущая (по running-job) и агрегированная стоимость/токены
|
||
из `agent_runs` (`cost_usd`, `input/output/cache_*` токены) — сырьё для cost-наблюдаемости (D3).
|
||
- **BR-6 — Аддитивность.** Существующие `/health`, `/status`, `/queue` остаются байт-в-байт прежними
|
||
по контракту; `/metrics` добавляется рядом, ничего не ломая.
|
||
- **BR-7 — Документированный контракт.** Формат `/metrics` зафиксирован в
|
||
`docs/architecture/README.md` как стабильный контракт для sidecar (F1b) + `CHANGELOG.md`.
|
||
|
||
## 5. Нефункциональные требования (NFR)
|
||
|
||
- **NFR-1 — Read-only.** Эндпоинт НИЧЕГО не мутирует: не пишет в БД, не запускает/останавливает
|
||
процессы, не рестартит, не дёргает внешние API. Только SELECT'ы + чтение in-memory-снимков
|
||
демонов.
|
||
- **NFR-2 — Never-raise (по полям).** Любая ошибка при сборе отдельного поля/раздела → это поле
|
||
получает `null` (или раздел — безопасный дефолт), но эндпоинт **возвращает 200 и валидный JSON**,
|
||
никогда не 500. Эталон — `serial_gate.snapshot()` с fallback в `except`.
|
||
- **NFR-3 — Лёгкость.** Только быстрые запросы к локальной SQLite + чтение уже посчитанных
|
||
in-memory снапшотов; без тяжёлых вычислений, без сетевых вызовов, без сканирования файлов/git.
|
||
Цель — единичные мс на типовом объёме (десятки задач/jobs).
|
||
- **NFR-4 — Self-hosting-безопасность.** Эндпоинт физически не способен повлиять на прод-конвейер
|
||
(следствие NFR-1) — безопасен на общем инстансе с enduro-trails.
|
||
- **NFR-5 — Совместимость БД/гейтов.** `STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` /
|
||
machine-verdict ключи / схема БД — НЕ трогаются. Задача читает существующие таблицы
|
||
(`tasks`/`jobs`/`agent_runs`) и существующие in-memory снапшоты.
|
||
- **NFR-6 — Стабильность контракта.** Формат — аддитивный и версионируемый (поле `schema_version`),
|
||
чтобы будущие расширения не ломали уже написанный sidecar.
|
||
|
||
## 6. Допущения и ограничения
|
||
|
||
- **Данные уже есть в БД.** Все нужные поля присутствуют: `tasks(stage, work_item_id, updated_at,
|
||
created_at)`, `jobs(status, attempts, max_attempts, transient_attempts, available_at, pid,
|
||
run_id)`, `agent_runs(agent, started_at, finished_at, model, effort, cost_usd, input_tokens,
|
||
output_tokens, cache_read_tokens, cache_creation_tokens)`. **Новые колонки/таблицы не нужны.**
|
||
- **Breaker-состояние — in-memory** (`queue_worker.worker.status()` / `CircuitBreaker.snapshot()`);
|
||
читается без БД.
|
||
- **CPU-тики pid** читаются из `/proc/<pid>/stat` (Linux прод-контейнер). Допущение: контейнер
|
||
Linux; при отсутствии/гонке (процесс уже умер) — поле `null` (NFR-2), НЕ ошибка. Это согласуется
|
||
с рамкой C-1: «орк лёг → endpoint недоступен = сам сигнал тревоги» — детект делает sidecar.
|
||
- **Арбитраж liveness — на стороне sidecar.** Орк не решает «завис/жив»; он лишь отдаёт `pid`,
|
||
`runtime_s` и (по возможности) CPU-тики; sidecar считает дельту между опросами.
|
||
- **Формат — JSON**, не OpenMetrics/Prometheus (рамка C-3: тонкий кастомный sidecar, не Prometheus).
|
||
|
||
## 7. Критерии успеха
|
||
|
||
`GET /metrics` отдаёт лёгкий, read-only, never-raise JSON с четырьмя разделами сырья;
|
||
`/health`/`/status`/`/queue` не сломаны; формат задокументирован как контракт sidecar; pytest
|
||
зелёный. Детальные PASS/FAIL — `03-acceptance-criteria.md`.
|
||
|
||
## 8. Риски
|
||
|
||
- Гонка чтения `/proc/<pid>/stat` (процесс умер между выборкой job и чтением proc) → закрывается
|
||
NFR-2 (`null`, не ошибка).
|
||
- Расхождение контракта `/metrics` и ожиданий sidecar (F1b) → закрывается BR-7 (контракт в одном
|
||
репо, документирован) + `schema_version` (NFR-6).
|
||
- Соблазн «протащить» в `/metrics` логику алертинга → закрывается scope-границей (вне объёма) и
|
||
NFR-1.
|
||
|
||
Детальная оценка технических рисков — `10-tech-risks.md` (заполняет архитектор).
|