13 KiB
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_item—tasks.work_item_id.stage—tasks.stage(значение слоя A, машина стадий).age_in_stage_s— целое; секунды сtasks.updated_at(= момент последней смены стадии). Источник вычисления — SQLCAST(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— сырьё по ретраям: сумма/списокattemptsvsmax_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'ом) — отдаёт только сырые тики + временную метку выборки.
- вариант A:
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:
{
"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»).