This file explains the technical terms used in the public README and release notes. It is written as interview prep for the project author: first in plain language, then with the exact place where the concept appears in AgentFlow, then with the reason it matters.
Это текущий размер полного локального тестового gate: сколько автотестов прогоняется, чтобы убедиться, что проект не сломался в базовых и расширенных сценариях. Важно не само красивое число, а то, что тесты покрывают разные слои системы, а не только отдельные функции.
В AgentFlow последний полный локальный gate запускает python -m pytest -p no:schemathesis -q --tb=short --durations=30 --timeout=300 с Redis и project-local temp paths; последний завершённый локальный full-suite gate на 2026-04-27 дал 668 passed, 8 skipped. Текущий fresh pre-commit gate заблокирован chaos smoke hang, см. docs/release-readiness.md. Основные директории: tests/unit, tests/integration, tests/sdk, tests/contract, tests/property, tests/chaos, tests/e2e. Релизный статус и команда верификации зафиксированы в release-readiness.md.
Одни тесты проверяют маленькие функции, другие проверяют весь путь запроса через API, третьи проверяют SDK как внешний контракт для пользователя. Если оставить только unit-тесты, можно пропустить ситуацию, где каждая часть по отдельности работает, но связка между ними уже нет.
- "Почему вы не ограничились unit-тестами?" -> Потому что API, SDK и интеграции ломаются чаще всего на стыках модулей, а не внутри одного
if. - "663 - это много или мало?" -> Само по себе число ничего не гарантирует; важно, что тесты разделены по типам риска и реально закрывают релизный путь.
Это перцентили времени ответа. p50 - медиана, то есть половина запросов быстрее этого значения. p95 и p99 показывают хвост задержек: насколько медленными становятся не типичные, а худшие реалистичные запросы.
Benchmarks собираются через scripts/run_benchmark.py и профиль нагрузки из tests/load/locustfile.py. Зафиксированный baseline лежит в benchmark-baseline.json: aggregate p50=56 ms, p95=260 ms, p99=330 ms; по entity endpoints p50=38-55 ms, p99=290-320 ms.
Average легко маскирует проблему. Если почти все запросы быстрые, а редкие запросы очень медленные, среднее значение выглядит "нормально", хотя пользователь всё равно периодически утыкается в длинные паузы. Для AI-агента это особенно важно: один ответ может включать несколько API-вызовов подряд, и именно хвост задержек замедляет весь диалог.
- "Почему вы смотрите на p99, а не на average?" -> Потому что average скрывает плохие хвосты, а p99 показывает реальный worst-case для живого пользователя.
- "Что важнее: p50 или p99?" -> Оба.
p50показывает типичный опыт,p99показывает, насколько неприятны редкие плохие случаи.
Baseline - это зафиксированная точка сравнения. Она нужна, чтобы сравнивать новые прогоны не с ощущением "кажется стало лучше", а с конкретным числом, сохранённым в репозитории.
Исторически ранний baseline был около 26 000 ms по entity p50, а после исправлений v8-v12 путь стал работать в диапазоне 43-55 ms. Текущий baseline закреплён в benchmark-baseline.json, а gate сравнения реализован в scripts/check_performance.py.
Такой baseline делает performance claim проверяемым. Он также защищает от самообмана: если исторически было 43 ms, а текущий checked-in baseline уже 55 ms, это надо честно сказать в документации, а не продолжать цитировать старое красивое число как будто ничего не изменилось.
- "За счёт чего был такой большой скачок?" -> За счёт ремонта hot path: async offload, параметризованные запросы, кеш и пересмотр serving-path bottleneck'ов.
- "Почему вы пишете диапазон
43-55 ms, а не одно число?" -> Потому что43 ms- важный исторический этап, а checked-in release baseline сейчас честно показывает38-55 msпо entity endpoints.
Dual SDK означает, что проект даёт клиентские библиотеки сразу для двух основных экосистем. Идея не в том, чтобы написать два разных продукта, а в том, чтобы один и тот же API был удобен и для backend/agent кода на Python, и для TypeScript/Node/browser-потребителей.
Python-клиент живёт в sdk/agentflow/client.py, TypeScript-клиент - в sdk-ts/src/client.ts. Оба клиента умеют вызывать тот же HTTP surface: entity lookup, metrics, query, catalog, batch и потоковые события. Паритет дополнительно страхуется тестами из tests/sdk и контрактами в config/contracts.
Если у тебя агентные workflow живут в разных средах, отсутствие официального SDK быстро превращается в ручные fetch/httpx-обвязки, которые расходятся между командами. Dual SDK удерживает один контракт и снижает риск того, что Python-клиент умеет одно, а TypeScript-клиент - другое.
- "Почему вообще понадобились оба SDK?" -> Потому что Python типичен для agent/backoffice workflow, а TypeScript часто нужен для web, edge и Node-интеграций.
- "Как вы удерживаете паритет?" -> Через единый HTTP contract, tests и version-aware schema contracts.
Retry - это повтор запроса после сбоя. Exponential backoff означает, что пауза между попытками растёт: сначала маленькая, потом больше. Jitter - это случайное отклонение этой паузы, чтобы много клиентов не начали повторять запрос одновременно и не устроили новый всплеск нагрузки.
Python-реализация находится в sdk/agentflow/retry.py, TypeScript-версия - в sdk-ts/src/retry.ts. В Python RetryPolicy.compute_delay() считает задержку на основе initial_delay_s, max_delay_s и jitter_factor. Ретрай разрешён для идемпотентных методов и для POST только если есть idempotency-key.
Не every failure означает "сломалось навсегда". Бывают сетевые дёргания, краткий 429, временный 503. Retry помогает пережить такие короткие проблемы без ручного вмешательства, но делает это осторожно, чтобы не усилить аварию.
- "Почему не ретраить всё подряд?" -> Потому что неидемпотентный повтор может создать дубликаты или повторить опасное действие.
- "Зачем нужен jitter?" -> Чтобы тысяча клиентов не проснулась через одинаковые
500 msи не ударила сервис второй волной.
Circuit breaker - это защитный автомат для удалённого вызова. Пока всё нормально, он в состоянии closed и пропускает запросы. Когда подряд идёт слишком много ошибок, он "выбивает" в open и на время перестаёт даже пытаться стучаться в проблемный сервис. Потом он даёт одну пробную попытку в half-open.
Состояния и переходы видно в sdk/agentflow/circuit_breaker.py: CLOSED, OPEN, HALF_OPEN. Клиент вызывает before_call(), а затем record_success() или record_failure(). Аналогичная логика есть и в TypeScript-версии в sdk-ts/src/circuitBreaker.ts.
Без такого механизма клиент во время инцидента продолжает спамить уже падающий сервис. Это вредно и для клиента, и для сервера. Circuit breaker делает поведение более "вежливым": сначала фиксирует деградацию, потом берёт паузу, потом осторожно проверяет, ожил ли сервис.
- "Чем breaker отличается от retry?" -> Retry пытается пережить короткий сбой, а breaker защищает систему от постоянного битья в уже мёртвый dependency.
- "Почему аналогия с автоматом в доме уместна?" -> Потому что логика похожа: при перегрузке цепь временно размыкается, а потом проверяется заново.
Backwards compatibility - это способность обновить библиотеку, не ломая существующий код пользователя. Иногда API нужно улучшить, но если сделать это жёстко, все клиенты вынуждены мгновенно переписываться.
В sdk/agentflow/client.py публичный конструктор остаётся компактным: base_url, api_key, timeout, contract_version. При этом legacy-параметры resilience всё ещё поддерживаются через совместимый слой, который внутри вызывает configure_resilience(). Это поведение и сама сигнатура закреплены тестами в tests/unit/test_sdk_backwards_compat.py.
SDK - это внешний интерфейс проекта. Даже если внутри ты всё переделал правильно, ломающая сигнатура в клиенте превращает "обновление" в неприятный миграционный проект для каждого пользователя. Совместимость снижает стоимость апдейта.
- "Почему не сломать API и не начать заново?" -> Потому что у SDK есть потребители; чистота внутренней реализации не должна перекладывать миграционную цену на них без серьёзной причины.
- "Как вы проверяете, что совместимость действительно сохранена?" -> Через тесты на публичную сигнатуру, экспортируемые методы и семантику deprecated-path.
Контракт - это формальное описание того, какие поля, типы и версии данных разрешены. Versioning нужен, чтобы схема могла меняться без тихой поломки клиентов.
Контракты лежат в config/contracts, загружаются через src/serving/semantic_layer/contract_registry.py и генерируются из моделей скриптом scripts/generate_contracts.py. Registry умеет отдавать latest stable, конкретную версию и diff с классификацией на breaking/additive changes. SDK может пиновать нужную версию контракта перед валидацией ответа.
Для агентов schema drift особенно болезнен. Если поле исчезло или поменяло тип, LLM-обвязка и downstream code часто падают не сразу и не очевидно. Контракт делает изменение явным: либо версия не совпала, либо diff показал, что изменение breaking.
- "Почему не ограничиться Pydantic-моделями?" -> Потому что нужен внешний, сериализуемый, versioned contract, доступный API и SDK независимо от внутреннего Python-кода.
- "Что даёт diff контрактов?" -> Возможность явно сказать, что поменялось: добавили поле безопасно или сломали старый consumer.
Параметризованный запрос отделяет структуру SQL от пользовательского значения. В текст запроса ставится placeholder, а реальное значение передаётся отдельно.
Backend интерфейс принимает sql и params в src/serving/backends/duckdb_backend.py. В entity lookup путь использует ? placeholders в src/serving/semantic_layer/query/entity_queries.py. Отдельные инъекционные кейсы покрыты в tests/unit/test_query_engine_injection.py.
Если подставлять строку пользователя прямо внутрь SQL, атакующий может подменить смысл запроса. Параметризация передаёт значение как данные, а не как часть SQL-команды, и этим закрывает классический путь к SQL injection.
- "Где в SQL всё ещё остаётся string interpolation?" -> Только там, где строка не приходит от пользователя, а берётся из внутреннего allowlist-каталога, например имя таблицы из метаданных.
- "Почему это важнее именно в hot path?" -> Потому что hot path - это самый вызываемый путь; если он небезопасен, риск и поверхность атаки растут многократно.
AST validator сначала парсит SQL в дерево, а потом проверяет это дерево по правилам: какие типы команд разрешены, к каким таблицам можно обращаться, нет ли опасных конструкций. Это намного надёжнее, чем проверять SQL регулярками.
В src/serving/semantic_layer/sql_guard.py функция validate_nl_sql() разрешает только один SELECT, запрещает DDL/DML-узлы (DROP, DELETE, UPDATE и т.д.), учитывает CTE aliases и проверяет, что реальные таблицы входят в allowlist. Это и есть защитный фильтр для NL-to-SQL пути.
Regex не понимает структуру SQL. Он легко ломается на CTE, подзапросах, комментариях и экранировании. Парсер, наоборот, видит именно синтаксическое дерево и позволяет проверять не "есть ли подозрительная подстрока", а "что этот запрос реально пытается сделать".
- "Почему regex недостаточно?" -> Потому что SQL - это грамматика, а не просто строка; регулярка не умеет надёжно отличать harmless текст от опасной конструкции.
- "Можно ли после такого валидатора использовать CTE и joins?" -> Да, если итоговый запрос остаётся
SELECTи трогает только разрешённые таблицы.
Это подход "не пускаем новые security findings, но не делаем вид, что старых не существует". Baseline хранит уже известные результаты, а gate падает только тогда, когда появляется новое предупреждение.
Скан запускается в ../.github/workflows/security.yml. Скрипт ../scripts/bandit_diff.py сравнивает текущий JSON-репорт с ../.bandit-baseline.json и завершает job ошибкой, если появляются новые issue keys по test_id, файлу и номеру строки.
На зрелом проекте редко получается разом починить весь исторический security debt. Baseline-first подход даёт реалистичную дисциплину: старые долги видны и задокументированы, но новые долги больше не допускаются.
- "Раз baseline не скрывает уже известные проблемы?" -> Он не скрывает, а фиксирует их как текущий остаток долга; новые находки всё равно режут CI.
- "Почему это лучше, чем требовать ноль finding'ов сразу?" -> Потому что иначе команда либо отключит скан, либо будет жить в перманентно красном CI.
Chaos testing - это намеренное создание поломок в зависимостях, чтобы увидеть, как система деградирует в реальности. Идея не "сломать ради шоу", а заранее доказать, что сервис ведёт себя предсказуемо при частичных авариях.
Локальный chaos stack описан в ../docker-compose.chaos.yml, сетевые сбои моделируются через ../config/toxiproxy.json, smoke-сценарии лежат в ../tests/chaos/test_chaos_smoke.py, а CI-логика разделяет PR smoke и scheduled full run в ../.github/workflows/chaos.yml.
Graceful degradation нельзя доказать только код-ревью и happy-path тестами. Пока ты не симулировал timeout у DuckDB proxy или недоступность Redis, ты не знаешь, как поведёт себя система под реальным сбоем.
- "Почему на PR идёт только smoke, а не весь chaos suite?" -> Потому что PR должен оставаться достаточно быстрым, а полный прогон дороже и подходит для scheduled verification.
- "Что именно вы проверяете хаосом?" -> Что API отдаёт ожидаемые ошибки, кеш корректно деградирует, а система не зависает в непредсказуемом состоянии.
Это проверка, которая не просто запускает нагрузочный тест, а сравнивает новый результат с baseline и режет PR при заметной деградации. То есть performance становится частью definition of done, а не послерелизной надежды.
Смешанный профиль нагрузки живёт в ../tests/load/locustfile.py: entity lookups, metric queries, NL query, batch и health. Сравнение baseline/current выполняет ../scripts/check_performance.py в CI/load gates, а PR-job ../.github/workflows/perf-regression.yml теперь держит быстрый entity-only perf-smoke с порогом p99 <= 500 ms.
Регресс производительности часто не выглядит как "всё сломалось". Он выглядит как "стало чуть медленнее", потом ещё чуть, и через несколько недель API уже неприятный. Gate ловит это до merge в main.
- "Почему мало абсолютного SLA?" -> Потому что абсолютный порог не замечает медленное сползание внутри допустимого окна.
- "Почему мало только относительного сравнения?" -> Потому что можно стабильно сравниваться с уже плохим baseline; нужен и абсолютный sanity check.
Terraform описывает инфраструктуру как код. plan показывает, что изменится, а apply применяет изменения. OIDC позволяет GitHub Actions получать облачные креды без статических long-lived secret keys.
Основной IaC лежит в ../infrastructure/terraform. Workflow ../.github/workflows/terraform-apply.yml делает отдельный plan, сохраняет артефакт tfplan, а затем даёт apply только после environment-gated шага. AWS-доступ настраивается через aws-actions/configure-aws-credentials и role-to-assume.
Инфраструктура - такая же часть системы, как код. Если деплой делать руками, знание остаётся в головах, а не в репозитории. Разделение plan/apply плюс approval делает изменения видимыми и безопаснее.
- "Почему
applyне автоматический?" -> Потому что инфраструктурные изменения дороже ошибки; ручное подтверждение здесь осознанно. - "Что ещё осталось ручным?" -> GitHub environments уже настроены с required reviewers; AWS OIDC role wiring,
AWS_TERRAFORM_ROLE_ARN, real tfvars и explicit workflow enablement всё ещё требуют operator setup перед real apply.
DuckDB - лёгкая columnar база для локальной аналитики и быстрых single-node чтений. Iceberg - это table format для больших аналитических данных с time travel, snapshot-ами и schema evolution.
Архитектурный контекст описан в architecture.md. Локальный serving path использует ../src/serving/backends/duckdb_backend.py, а local pipeline пишет demo-данные через ../src/processing/local_pipeline.py. Production-shaped путь ориентируется на Iceberg и его каталоги/таблицы.
Эта связка позволяет не выбирать между "удобно локально" и "похоже на прод". DuckDB даёт быстрый dev/demo loop, а Iceberg оставляет дверь в production-friendly lakehouse path с time travel и управляемой эволюцией схемы.
- "Когда DuckDB подходит?" -> Когда нужен быстрый локальный или single-node serving/analytics path без отдельного кластера.
- "Когда уже нужен Iceberg?" -> Когда важны snapshot semantics, schema evolution, большой объём данных и production-shaped storage layout.
Это минимальный операционный интерфейс, который показывает состояние системы, usage и health без отдельного тяжёлого frontend stack. По сути, это read-only dashboard для администратора.
Роуты лежат в ../src/serving/api/routers/admin_ui.py, шаблон - в ../src/serving/api/templates/admin.html, поведение проверяется тестами из ../tests/integration/test_admin_ui.py. Страница собирает health, cache stats, db pool stats и usage, а summary обновляется периодическим partial-refresh.
Для такой поверхности React не обязателен. Если интерфейс в основном читает данные и не требует сложного client-side state, server-rendered страница быстрее появляется, проще поддерживается и не требует второй крупной системы внутри проекта.
- "Почему не React?" -> Потому что здесь нужен небольшой ops dashboard, а не отдельное frontend-приложение со сложным интерактивным состоянием.
- "Когда SSR-подход перестанет хватать?" -> Когда появится тяжёлая интерактивность, сложная клиентская маршрутизация или большой объём client-side state transitions.
Landing page - это публичная страница, которая быстро объясняет проект человеку, не открывая сразу десятки markdown-файлов. Fly.io demo config - это минимальный deploy path, чтобы поднять демонстрационный инстанс без полной production-инфраструктуры.
Публичная страница лежит в ../site/index.html. Там зафиксированы проблема, user journeys, ключевые differentiators и baseline numbers без hype. Hosted demo path описан в ../deploy/fly/fly.toml и ../deploy/fly/README.md: volume mount для DuckDB, health check на /v1/health, минимальная конфигурация для single-app demo.
Для публичного GitHub-репозитория мало иметь хороший код. Нужен ещё короткий входной слой: "что это", "зачем это существует", "как быстро посмотреть". Landing page решает narrative side, а Fly config решает demo side.
- "Почему Fly.io?" -> Потому что для демо это простой hosted path с понятной конфигурацией и persistent volume, без разворачивания всей production topology.
- "Это production deployment?" -> Нет, это demo path. Полный production-shaped story в проекте шире и строится вокруг Terraform, Helm и streaming stack.