Files
orchestrator/docs/work-items/ORCH-099/02-trz.md

13 KiB
Raw Blame History

work_item, stage, author_agent, status, created_at, model_used
work_item stage author_agent status created_at model_used
ORCH-099 analysis analyst ready-for-review 2026-06-10 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_itemtasks.work_item_id.
  • stagetasks.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.
  • repotasks.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

  • countsdb.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).
  • breakerworker.breaker.snapshot(): state (closed/open/half-open), consecutive_transient, pause_remaining_s.
  • max_concurrencyworker.max_concurrency.

Инвариант: ни одно поле не обязано существовать ценой падения — недоступный breaker (например, worker не инициализирован в тесте) → breaker: null, не 500 (NFR-2).

FR-3 — Раздел agents (agent-liveness) — BR-4

Список running-jobs (из db.get_running_jobs()), по каждому:

  • agentagent_runs.agent (через джойн; роль: analyst/architect/developer/…).
  • run_idjobs.run_id (= agent_runs.id).
  • job_idjobs.id.
  • pidjobs.pid (может быть null, если процесс ещё не застамплен / уже завершён).
  • runtime_srunning_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 (поля 1415), плюс clk_tck (os.sysconf("SC_CLK_TCK")), чтобы sidecar посчитал CPU-дельту между опросами;
    • вариант B: орк сам не считает дельту (он опрашивается стейтлесс sidecar'ом) — отдаёт только сырые тики + временную метку выборки.
  • model, effortagent_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:

{
  "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 /metrics200 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»).