174 lines
13 KiB
Markdown
174 lines
13 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
|
||
---
|
||
|
||
# 02 — ТЗ (TRZ): ORCH-099 — FND/F1a: лёгкий `/metrics` в орке (отдать сырьё)
|
||
|
||
Work Item: **ORCH-099** · Repo: **orchestrator** · Стадия: analysis
|
||
|
||
> ТЗ описывает **конкретные изменения к реализации**, выведенные из BRD и фактического кода.
|
||
> Архитектурное обоснование/решения (формат полей liveness, способ чтения CPU, версионирование
|
||
> контракта) — задача архитектора (`06-adr/`).
|
||
|
||
## 1. Сводка изменения
|
||
|
||
Добавить read-only HTTP-эндпоинт `GET /metrics`, отдающий JSON-снимок «сырья» о самом орке для
|
||
будущего sidecar (F1b): активные стадии задач, очередь jobs, agent-liveness, стоимость/токены.
|
||
Логика сборки выносится в **новый leaf-модуль** `src/metrics.py` (чистая функция-сборщик, never-raise,
|
||
без побочных эффектов — по образцу `serial_gate.snapshot()`/`task_deps.snapshot()`/`cancel.snapshot()`).
|
||
Эндпоинт в `src/main.py` — тонкая обёртка над сборщиком, в том же стиле, что `GET /queue`
|
||
(`src/main.py`, дикт с разделами). Никаких изменений `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/схемы
|
||
БД/machine-verdict ключей. Только чтение существующих таблиц и существующих in-memory-снапшотов.
|
||
|
||
## 2. Задействованные модули / пути
|
||
|
||
| Путь | Действие |
|
||
|------|----------|
|
||
| `src/metrics.py` | **создать** — leaf-сборщик снимка из БД (`build_metrics() -> dict`, never-raise) |
|
||
| `src/main.py` | изменить — добавить `@app.get("/metrics")` (тонкая обёртка над `metrics.build_metrics()`) |
|
||
| `src/db.py` | изменить (при необходимости) — добавить read-only helper(ы) для агрегатов `agent_runs` (напр. `agent_cost_totals()`); существующие `job_status_counts`/`get_running_jobs`/`recent_jobs`/`get_active_tasks_for_reconcile` переиспользуются как есть |
|
||
| `docs/architecture/README.md` | изменить — задокументировать контракт `/metrics` (формат для sidecar F1b) |
|
||
| `CHANGELOG.md` | изменить — запись `## [Unreleased]` (ORCH-099) |
|
||
| `tests/test_metrics.py` | **создать** — pytest на структуру/never-raise/read-only |
|
||
|
||
**Существующие источники данных (переиспользуются, НЕ дублируются):**
|
||
- `db.get_active_tasks_for_reconcile()` — задачи с `stage != 'done'` + вычисленный `age_s`
|
||
(секунды с `updated_at`). Базис для раздела стадий.
|
||
- `db.job_status_counts()` — `{queued, running, done, failed}` из `jobs`.
|
||
- `db.get_running_jobs()` — running-jobs с `running_age_s`, плюс джойн на `agent_runs` (`agent`,
|
||
`run_id`, `pid`, `started_at`, `model`, `effort`). Базис для liveness.
|
||
- `queue_worker.worker.status()` / `worker.breaker.snapshot()` — breaker-состояние in-memory
|
||
(`state`/`consecutive_transient`/`pause_remaining_s`), `max_concurrency`, `poll_interval`.
|
||
|
||
## 3. Функциональные требования
|
||
|
||
### FR-1 — Раздел `stages` (активные стадии задач) — BR-2
|
||
|
||
Список активных (незавершённых) задач. По каждой:
|
||
- `work_item` — `tasks.work_item_id`.
|
||
- `stage` — `tasks.stage` (значение слоя A, машина стадий).
|
||
- `age_in_stage_s` — целое; секунды с `tasks.updated_at` (= момент последней смены стадии).
|
||
Источник вычисления — SQL `CAST(strftime('%s','now') - strftime('%s', updated_at) AS INTEGER)`,
|
||
как в `get_active_tasks_for_reconcile`.
|
||
- `repo` — `tasks.repo` (sidecar мультипроектный; нужно отличать orchestrator от enduro-trails).
|
||
- (опционально) `task_id`, `created_age_s` (общий возраст задачи).
|
||
|
||
Инвариант: выборка только `stage NOT IN ('done', 'cancelled')` (терминальные исключены — см.
|
||
ORCH-090: множество терминалов `{done, cancelled}`). Пустой список — валидный ответ.
|
||
|
||
### FR-2 — Раздел `queue` (очередь jobs) — BR-3
|
||
|
||
- `counts` — `db.job_status_counts()` (`queued`/`running`/`done`/`failed`); при наличии —
|
||
добавить `cancelled` (ORCH-090 терминал).
|
||
- `depth` — глубина очереди = число `queued`-jobs, готовых к выдаче (можно вернуть как
|
||
`counts.queued`; при желании — отдельно «доступные сейчас» с учётом `available_at <= now`).
|
||
- `retries` — сырьё по ретраям: сумма/список `attempts` vs `max_attempts` и `transient_attempts`
|
||
по незавершённым jobs; как минимум агрегат «сколько jobs в backoff» (`available_at > now`).
|
||
- `breaker` — `worker.breaker.snapshot()`: `state` (`closed`/`open`/`half-open`),
|
||
`consecutive_transient`, `pause_remaining_s`.
|
||
- `max_concurrency` — `worker.max_concurrency`.
|
||
|
||
Инвариант: ни одно поле не обязано существовать ценой падения — недоступный breaker
|
||
(например, worker не инициализирован в тесте) → `breaker: null`, не 500 (NFR-2).
|
||
|
||
### FR-3 — Раздел `agents` (agent-liveness) — BR-4
|
||
|
||
Список running-jobs (из `db.get_running_jobs()`), по каждому:
|
||
- `agent` — `agent_runs.agent` (через джойн; роль: analyst/architect/developer/…).
|
||
- `run_id` — `jobs.run_id` (= `agent_runs.id`).
|
||
- `job_id` — `jobs.id`.
|
||
- `pid` — `jobs.pid` (может быть `null`, если процесс ещё не застамплен / уже завершён).
|
||
- `runtime_s` — `running_age_s` из `get_running_jobs` (секунды с `jobs.started_at`); как
|
||
альтернатива — секунды с `agent_runs.started_at`. Решение о базисе — за архитектором (ADR).
|
||
- **Сырьё для alive-детекта** — одно из (выбор реализации — ADR архитектора, BR-4 допускает оба):
|
||
- вариант A: `cpu_ticks` — суммарные utime+stime из `/proc/<pid>/stat` (поля 14–15), плюс
|
||
`clk_tck` (`os.sysconf("SC_CLK_TCK")`), чтобы sidecar посчитал CPU-дельту между опросами;
|
||
- вариант B: орк сам не считает дельту (он опрашивается стейтлесс sidecar'ом) — отдаёт только
|
||
сырые тики + временную метку выборки.
|
||
- `model`, `effort` — `agent_runs.model`/`effort` (контекст стоимости).
|
||
|
||
Инвариант (NFR-2): `pid is None` ИЛИ `/proc/<pid>` отсутствует/гонка (процесс умер) →
|
||
`cpu_ticks: null` для этого агента, остальные поля и весь эндпоинт целы. НЕ бросать, НЕ ждать.
|
||
|
||
### FR-4 — Раздел `cost` (стоимость/токены) — BR-5
|
||
|
||
- `running` — по каждому running-job текущие накопленные значения из `agent_runs`, если уже
|
||
застамплены (часто `null` до завершения — токены/cost парсятся из CLI-JSON в `_monitor_agent`
|
||
по окончании). Допустимо отдавать `null` для незавершённых — это честное сырьё.
|
||
- `aggregate` — агрегаты по `agent_runs`: суммарные `cost_usd`, `input_tokens`, `output_tokens`,
|
||
`cache_read_tokens`, `cache_creation_tokens`. Желателен срез: всего + за последние N (или
|
||
по `repo`). Реализуется новым read-only helper'ом `db.agent_cost_totals()` (чистый SELECT
|
||
с `COALESCE(SUM(...),0)`).
|
||
|
||
Инвариант: пустая `agent_runs` → нули, не ошибка.
|
||
|
||
### FR-5 — Конверт ответа (envelope) — BR-1, BR-6, NFR-6
|
||
|
||
`GET /metrics` возвращает JSON:
|
||
```json
|
||
{
|
||
"schema_version": 1,
|
||
"generated_at": "<ISO-8601 / datetime('now')>",
|
||
"stages": [ ... ],
|
||
"queue": { ... },
|
||
"agents": [ ... ],
|
||
"cost": { "running": [...], "aggregate": {...} }
|
||
}
|
||
```
|
||
- `schema_version` — целое; точка стабильности контракта для sidecar (NFR-6). Стартовое значение
|
||
и политика инкремента — за архитектором.
|
||
- `generated_at` — метка времени снимка (нужна sidecar'у для расчёта дельт).
|
||
- Точные имена ключей разделов/полей фиксируются в `docs/architecture/README.md` (BR-7) и являются
|
||
контрактом; reviewer/tester сверяют ответ с документом.
|
||
|
||
### FR-6 — Never-raise сборщик — NFR-2
|
||
|
||
`metrics.build_metrics()` строит ответ по-раздельно; каждый раздел — в своём `try/except`, в
|
||
`except` пишет `logger.warning(...)` и подставляет безопасный дефолт (`null`/`[]`/`{}`). Функция
|
||
**никогда** не пробрасывает исключение. Эндпоинт `main` дополнительно не нуждается в обработке, но
|
||
обязан вернуть результат сборщика как есть. Эталон — `serial_gate.snapshot()`.
|
||
|
||
## 4. Изменения API
|
||
|
||
**Новый эндпоинт:**
|
||
- `GET /metrics` → `200 application/json`, тело — конверт FR-5. Без параметров. Без аутентификации
|
||
сверх существующей (тот же уровень, что `/queue`/`/status`). Read-only.
|
||
|
||
**Изменённые эндпоинты:** Нет. `/health`, `/status`, `/queue`, `/webhook/*` — без изменений
|
||
(BR-6). Регресс-проверка: существующие тесты эндпоинтов остаются зелёными.
|
||
|
||
## 5. Изменения схемы БД
|
||
|
||
**Нет.** Новые таблицы/колонки/индексы/миграции не вводятся. Используются существующие
|
||
`tasks`/`jobs`/`agent_runs` и их колонки (перечислены в §2). Допускается добавление **read-only**
|
||
helper-функций в `src/db.py` (например `agent_cost_totals()`) — это код, не схема; `CREATE`/`ALTER`
|
||
не выполняются. `STAGE_TRANSITIONS`/`QG_CHECKS`/схема — байт-в-байт прежние (NFR-5).
|
||
|
||
## 6. Требования к новым/изменённым QG checks
|
||
|
||
**Нет.** `/metrics` — наблюдаемость, не гейт конвейера. `QG_CHECKS` / `check_*` / `_parse_*` /
|
||
machine-verdict ключи (`verdict:`/`result:`/`deploy_status:`/`staging_status:`/`security_status:`/
|
||
`coverage_status:`) — НЕ трогаются. Новых артефактов pipeline (`NN-*.md`) задача не создаёт.
|
||
|
||
## 7. Совместимость / регресс
|
||
|
||
- **Аддитивность:** новый модуль (`src/metrics.py`) + новый эндпоинт + read-only helper(ы).
|
||
Существующий код путей конвейера не модифицируется.
|
||
- **Read-only / never-raise:** по конструкции (NFR-1/NFR-2) эндпоинт не влияет на состояние и не
|
||
падает → нулевой риск для прод-конвейера, общего с enduro-trails (NFR-4).
|
||
- **Kill-switch:** жёсткий флаг не обязателен (эндпоинт инертен и не подключён к конвейеру). Если
|
||
архитектор сочтёт нужным — допустим конфиг-флаг включения `/metrics` (по образцу snapshot-флагов),
|
||
но это НЕ требование BRD; дефолт — эндпоинт доступен.
|
||
- **Обратимость:** удаление эндпоинта/модуля полностью откатывает изменение без следов в БД/схеме.
|
||
- **Контракт sidecar:** `schema_version` + документ в README обеспечивают, что F1b не сломается при
|
||
будущих аддитивных расширениях (NFR-6).
|
||
- **Артефакты pipeline, создаваемые/обновляемые задачей:** `01-brd.md`, `02-trz.md`,
|
||
`03-acceptance-criteria.md`, `04-test-plan.yaml` (analysis); далее — `06-adr/` (architect),
|
||
обновление `docs/architecture/README.md` и `CHANGELOG.md` (developer в том же PR — правило
|
||
«доки = golden source»).
|