Skip to main content
Glama
README.md
# tender-agent

Инструменты и консольный агент: обнаруживают JSON API портала, собирают списки тендеров,
объединяют разделы и подбирают лоты по текущему запросу пользователя.
Тематика произвольная: мебель, строительство, транспорт, ПО и другие закупки.
Сохранённый профиль применяется только при явном выборе.
Диалог работает через TokenRouter, OpenRouter, Z.ai или Anthropic; регулярный запуск и отчёты — без LLM.
MCP-режим подключает эти же функции к внешнему AI-клиенту без внутренней модели.

## Установка из Git для своего AI

Нужны Node.js 20+, Git, Chrome/Chromium и AI-клиент с поддержкой локальных MCP-инструментов.
Лицензия — [MIT](LICENSE). Проект рассчитан на локальную работу у каждого пользователя.

После публикации репозитория `Firdavs-stack/tender-agent` установка для Codex:

```bash
npm install --global git+https://github.com/Firdavs-stack/tender-agent.git
tender-agent-setup --client codex
```

Перезапустите AI-клиент и напишите:

```text
Используй $tender-research: найди тендеры на ремонтные работы
на https://xt-xarid.uz/, проверь актуальность и покажи риски.
```

Установщик добавляет MCP в пользовательский `config.toml` и копирует навык в
`~/.agents/skills/tender-research`. Остальные настройки сохраняются, перед изменением
конфигурации создаётся резервная копия. Для предварительного просмотра:

```bash
tender-agent-setup --client codex --dry-run
```

Для другого AI-клиента с JSON-форматом `mcpServers` укажите его файл конфигурации:

```bash
tender-agent-setup --client json --config /absolute/path/to/mcp.json
```

Если клиент поддерживает локальные навыки, добавьте `--skill-dir /path/to/skills`.
Если не поддерживает, порядок работы доступен через MCP prompt `tender_research`
и ресурс `tender://instructions/research`. JSON с комментариями (JSONC) нужно
настроить вручную через `mcp:config`; автоматический установщик принимает JSON.
Поддержка локального stdio MCP и качество работы с инструментами зависят от клиента
и выбранной модели; обычного доступа к текстовому чату без инструментов недостаточно.

**Кто расходует токены:** модель подключённого AI-клиента выполняет рассуждение.
Его подписка, API-аккаунт, лимиты или локальная модель определяют расходы.
MCP-сервер не вызывает модель, не требует ключа автора и не использует его баланс.
Файлы, выгрузки и журналы хранятся на компьютере пользователя; центрального сервера
автора для поиска нет. Установщик задаёт отдельное личное хранилище:
`$XDG_DATA_HOME/tender-agent` или `~/.local/share/tender-agent` на macOS/Linux,
`%LOCALAPPDATA%/tender-agent` на Windows. Путь можно изменить через `--data-dir`.
Chrome/Chromium определяется автоматически; нестандартный путь — `--chromium PATH`.

Установка из исходников вместо глобального пакета:

```bash
git clone https://github.com/Firdavs-stack/tender-agent.git
cd tender-agent
npm ci --omit=dev
npm run setup -- --client codex
```

Для установки только в выбранный проект:

```bash
npm run setup -- --client codex --scope project --project /path/to/project
```

Клонированную папку оставьте на месте: MCP запускает код из неё. Для обновления
выполните `git pull`, `npm ci --omit=dev`, затем ту же команду `setup`; для глобальной
установки повторите `npm install --global git+https://github.com/Firdavs-stack/tender-agent.git`
и `tender-agent-setup --client codex`. Неизменённые установки не дублируются,
собственные изменения навыка и отличающаяся настройка сервера не перезаписываются.
Для удаления сначала уберите только запись MCP `tender-agent` и его установленный
навык из клиента, затем удалите глобальный пакет или клон; личные выгрузки останутся.

## Подготовка отдельного репозитория

Для переноса из родительского проекта экспортируйте чистое дерево:

```bash
npm run release:export -- /new/path/tender-agent
```

В него входят исходники, навык, зависимости с lock-файлом, тесты, CI, документация
и MIT-лицензия. Экспорт не копирует историю родительского Git, `.env*` (кроме
публичного `.env.example`), личный профиль, `data/`, `recipes/`, `node_modules/`
и настройки компьютера. Затем создайте Git-репозиторий именно в экспортированной
папке. Пакет npm дополнительно ограничен списком `files`; сборки проверяются на
отсутствие файлов с тестовыми секретами. `private:true` запрещает случайную публикацию
в реестр npm и не мешает установке из Git.

Проверки перед публикацией из исходников: `npm ci`, `npm test`, `npm run test:browser`,
`npm pack --dry-run --ignore-scripts`. CI запускает основные проверки на Node.js 22/24;
браузерные проверки запускаются отдельно при наличии Chrome/Chromium.

## MCP и навык

В MCP-режиме модель подключённого клиента (например, Codex или Claude) понимает
задачу и выбирает инструменты. `tender-agent` выполняет исследование, сбор,
проверку адаптеров, фильтрацию и построение фактического отчёта. Вложенного
агента нет: `ask`, GLM/OpenRouter и API-ключи в этом режиме не используются.
Токены модели учитываются клиентом; сервер не может управлять его общим бюджетом.

Нужны Node.js 20+ и Chrome/Chromium для браузерных инструментов.
Установите зависимости командой `npm ci` в каталоге `tender-agent`.
Сервер использует локальный транспорт **stdio**, без HTTP-порта:

```bash
node /absolute/path/to/tender-agent/src/mcp-server.mjs
```

Он ожидает MCP-запросы на stdin и пишет только протокол в stdout. Его запускает
MCP-клиент, поэтому ручной запуск без вывода — нормальное поведение.
Не настраивайте клиент на обычный `npm run mcp`: npm добавляет служебные строки
в stdout. Генератор ниже использует абсолютный путь к Node и серверу, включая
пути с пробелами; текущий рабочий каталог клиента не влияет на файлы программы.

### Конфигурация клиента

Готовая JSON-конфигурация `mcpServers` для клиента с таким форматом:

```bash
npm --silent run mcp:config -- json
```

Для Codex:

```bash
npm --silent run mcp:config -- codex
```

Добавьте полученный TOML-блок в проектный `.codex/config.toml` или пользовательский
`~/.codex/config.toml`, сохранив остальные настройки. Не добавляйте сервер дважды.
Настройка использует `startup_timeout_sec=15`, `tool_timeout_sec=600`: полный сбор
может длиться дольше стандартного тайм-аута клиента. Перезапустите MCP-подключение
или клиент после настройки. Проектные настройки Codex применяются в доверенных
проектах. [Официальная документация MCP в Codex](https://developers.openai.com/codex/mcp).

`CHROMIUM_PATH` при необходимости задаётся в окружении MCP-процесса; `.env`
сервер намеренно не читает. `TENDER_DATA_ROOT` задаёт отдельный каталог состояния
с подкаталогами `data/`, `recipes/` и необязательным `profile.json`; по умолчанию
используется каталог `tender-agent`. Выгрузки и рецепты общие, критерии и журналы
исследований раздельные. Одновременные вызовы в одном процессе выполняются последовательно.

### Навык для AI

Готовый навык — [skills/tender-research/SKILL.md](skills/tender-research/SKILL.md).
В этом репозитории он подключён ссылкой `.agents/skills/tender-research`.
Codex обнаруживает навыки в `.agents/skills` проекта; для другой рабочей папки
скопируйте туда каталог `tender-research` или создайте ссылку на него.
MCP-сервер подключается отдельно: один файл навыка не запускает сервер.
[Обнаружение навыков Codex](https://developers.openai.com/codex/skills).

Пример запроса после подключения:

```text
Используй $tender-research: найди тендеры на ремонтные работы
на https://xt-xarid.uz/, проверь актуальность и покажи риски.
```

Для клиентов без поддержки локальных навыков тот же порядок работы доступен
как MCP prompt `tender_research` (аргумент `request`) и ресурс
`tender://instructions/research`. Префикс имён инструментов зависит от клиента;
имя сервера — `tender-agent`. Локальный stdio-сервер требует клиента на компьютере
с программой; подключение из удалённого веб-сервиса потребует отдельного транспорта.

### Порядок работы и восстановление

1. `start_research(request)` — исходный запрос пользователя; сохранить `session_id`.
2. `list_sources(session_id, host)` — готовые источники и рецепты. Знакомый сайт:
   `search_tenders(session_id, host, criteria)` обновляет и подбирает за один вызов.
3. Новый/сломанный источник: `plan_search`, `discover_api`, `inspect_capture`,
   `inspect_page`, `read_resource`, `probe_api`, `run_code`, `test_collection_plan`,
   `collect_tenders` или `collect_dom`, затем `select_tenders`.
4. `inspect_pages` — чтение карточек кандидатов; `research_status` — проверка
   прогресса. `finish_research` — проверенный отчёт; при реальном препятствии
   `partial_reason` сохраняет явно частичный результат с ограничениями.
5. `close_research` освобождает браузер и память. После переподключения
   `resume_research(session_id)` восстанавливает журнал, но не cookies браузера.

Кроме `start_research` все инструменты требуют `session_id`. Новая тема — новая
сессия. Успешно завершённая сессия доступна для чтения; изменить задачу можно в новом запуске.
Частичное исследование можно продолжить через `resume_research`: неудачные источники
станут доступны для повторного обновления, предыдущие попытки останутся в журнале.
Одновременно в памяти доступно до восьми исследований; закрывайте ненужные.
Офлайн-режим определяется по явному запросу работы без сети; сетевые инструменты
в такой сессии заблокированы кодом.

Полные ответы сохраняются в `data/runs/<session_id>/tools/<call_index>.json`,
в MCP передаётся короткий результат с адресом полного ответа. `read_tool_result`
читает поле `path` страницами; `run_id` у него обозначает исходный запуск старого
ответа и не заменяет текущий `session_id`. Отчёты сохраняются рядом с журналом
в `*.result.json/md` или `*.partial.json/md`. Финальные числа и статусы рассчитывает
код. Просмотр карточки не превращает неизвестное время окончания в подтверждённое.
Полнота отдельных снимков не означает охват всего портала.

Проверки MCP, включая настоящий stdio-клиент, оба режима согласования протокола,
восстановление, изоляцию критериев, отмену и отсутствие внутренней модели:

```bash
npm run test:mcp
npm test
npm run test:browser
```

## Запуск

Нужны Node.js 20+ и Chrome/Chromium для обнаружения API.

```bash
cd tender-agent
npm ci
npm test
npm run capture -- https://xarid.uzex.uz/home
npm run analyze -- xarid.uzex.uz
npm run collect -- xarid.uzex.uz
```

Браузер ищется в стандартных каталогах либо через `CHROMIUM_PATH`.
Для сайтов с разделами можно передать время ожидания и текст ссылки:

```bash
npm run capture -- https://xarid.uzex.uz/home 20 'Электронный тендер'
```

Диалог с моделью:

```bash
# Настройте .env по разделу ниже
npm run check:model  # два коротких API-вызова: модель → инструмент → ответ
npm run ask -- 'Найди тендеры на разработку приложений в Ташкенте на https://xarid.uzex.uz/home'
npm run ask  # интерактивный режим
```

Настройки GLM через TokenRouter в `tender-agent/.env`:

```dotenv
TENDER_PROVIDER=tokenrouter
TENDER_API_KEY=ключ_из_TokenRouter
TENDER_MODEL=z-ai/glm-5-turbo
```

Ключ, ранее записанный в `ANTHROPIC_API_KEY`, тоже читается для совместимости;
при явном `TENDER_PROVIDER=tokenrouter` он отправляется только в TokenRouter.
Переменные оболочки имеют приоритет над `.env`. После изменения `.env` перезапустите `ask`.
`check:model` использует платный API, но не запускает сбор тендеров.
HTTP 401 означает, что сервис не принял авторизацию: проверьте соответствие ключа
и адреса API. TokenRouter и OpenRouter используют разные ключи и адреса.
HTTP 403 с `gift balance` / `eligible quota` означает недостаток доступного
для выбранной модели баланса: наличие подарочных средств не гарантирует доступ
к `z-ai/glm-5-turbo`. Клиент сообщает эту причину и не повторяет запрос.

TokenRouter использует базу `https://api.tokenrouter.com/v1`.
OpenRouter — отдельный сервис: `TENDER_PROVIDER=openrouter`, ключ OpenRouter.
Прямой Z.ai: `TENDER_PROVIDER=zai`, `TENDER_MODEL=glm-5-turbo`, ключ Z.ai.
Anthropic: `TENDER_PROVIDER=anthropic`, модель `claude-...`, ключ Anthropic.
Другой совместимый Chat Completions API: `TENDER_PROVIDER=compatible`,
`TENDER_BASE_URL=https://ваш-сервис/v1`, `TENDER_MODEL=...`, `TENDER_API_KEY=...`.
Адрес указывает базу API; путь `/chat/completions` добавляется клиентом.
Тайм-аут одного обращения: `TENDER_MODEL_TIMEOUT_MS=120000`, лимит ответа
`TENDER_MAX_OUTPUT_TOKENS=4096`. Форматы инструментов и их результатов
преобразуются клиентом; reasoning-данные сохраняются для продолжения после инструментов.
Документация протоколов: [TokenRouter](https://www.tokenrouter.com/docs/zcode-setup/),
[OpenRouter](https://openrouter.ai/docs/api/reference/overview),
[reasoning и инструменты](https://openrouter.ai/docs/guides/best-practices/reasoning-tokens),
[Z.ai](https://docs.z.ai/api-reference/llm/chat-completion).

Основные инструменты модели: `list_sources`, `refresh_source`, `discover_api`,
`collect_tenders`, `query_data`, `summarize_field`, `select_tenders`, `get_saved_profile`.
Инструменты самостоятельного исследования и кода перечислены ниже.
Сначала агент проверяет готовые данные и рецепты, чтобы не разбирать сайт повторно. Модель переводит смысл запроса в параметры
отбора; сами фильтры, сроки и бюджет проверяются кодом.

После успешного полного сбора агент сохраняет рецепт. Сохранённые рецепты:

```bash
npm run run -- xarid.uzex.uz
npm run report -- xarid.uzex.uz  # только сохранённые данные, без сети и LLM
npm run report -- xarid.uzex.uz --query 'мебел*'
npm run report -- xarid.uzex.uz --all  # все темы
npm run report -- xarid.uzex.uz --use-profile  # только явное применение профиля
```

`--query` — лексический фильтр: все слова должны присутствовать в названии,
описании или категории. Семантическое преобразование запроса выполняется в `ask`.

Результаты: `data/matched.json` (подбор и аналитика) и `data/report.md` (отчёт).
`items` содержит подтверждённые по доступным полям совпадения, `review` — кандидатов
с недостатком данных или из смежных категорий. Это не подтверждение соответствия ТЗ.

Коды выхода запуска: `0` — сбор перечисленных источников полный; `2` — есть
неполные/неудачные источники; `1` — ошибка запуска/настройки. Нулевое число
совпадений само по себе не является ошибкой.

## Профиль и подбор

По умолчанию `profile.json` не нужен и не влияет на поиск.
Задачу определяет текущий запрос; ограничения прежней темы не должны переноситься
на новую автоматически. `--all` означает все темы. Без предмета запроса или `--all`
офлайн-отчёт показывает кандидатов для проверки без утверждения тематической релевантности.

Если нужен поиск по сохранённым предпочтениям, создайте `profile.json` по образцу
`profile.example.json` и включите `--use-profile`. В диалоге явно попросите
учесть сохранённый профиль; модель использует `get_saved_profile` и `use_profile:true`.
Поля необязательного профиля:

- `relevant_categories`, `relevant_keywords` — положительные признаки;
- `review_categories` — смежные категории, которые нельзя автоматически рекомендовать;
- `excluded_categories`, `excluded_keywords` — явные исключения;
- `regions` — допустимые регионы; пустой массив означает любые;
- `budget_min`, `budget_max`, `budget_currency` — пределы в одной указанной валюте;
- `min_hours_remaining` — минимальный запас времени до подачи;
- `timezone_offset` — пояс дат без зоны; по умолчанию `+05:00` для Узбекистана.

Слова/фразы сопоставляются по границам слов. `разработк*` означает основу,
`ПО` — аббревиатуру в верхнем регистре; обычное «по» не является признаком софта.
Не задавайте слишком общие слова. Свободные `notes` доступны модели только через явное чтение профиля;
в запуске без LLM при `--use-profile` действуют структурированные поля.

Если профиль явно включён, он действует вместе с ограничениями запроса.
Без явного включения ограничения профиля полностью игнорируются. Просроченные
лоты и известные суммы вне бюджета исключаются. Неизвестная сумма/валюта при
бюджетном фильтре, неизвестный срок или отсутствующий предмет → ручная проверка.
Бюджеты разных валют не складываются. Балл — эвристика соответствия, не вероятность победы.

## Хранение и восстановление старых данных

```bash
npm run repair -- xarid.uzex.uz
```

Миграция использует исходные `raw`, сохраняет оригинал в
`data/<host>/records.legacy-backup.json`, исправляет нормализацию и отключает
рецепты справочников. Дата исходного сбора сохраняется. При неоднозначном источнике
миграция останавливается, не присваивая записям выдуманный источник. Повторный запуск безопасен.

Каждый источник/набор фильтров имеет отдельный снимок в `data/<host>/sources/`.
`records.json` — объединённая выгрузка. Совпадения идентификаторов разных эндпоинтов
не склеиваются; пересечение фильтрованных выборок одного источника удаляется.
Новый сбор заменяет только свой снимок. Неполная попытка не затирает предыдущую
полную выгрузку и сохраняется в `attempts/`. Отчёт живого запуска показывает именно
его результаты и ошибки; офлайн-отчёт — сохранённые снимки с исходными датами.

## Надёжность и ограничения

Сборщик поддерживает GET/POST, параметры в query/body, пагинацию page/offset/range
с нулевым стартом, явный путь к массиву и полю total. Сумма и total распознаются
также в snake_case/camelCase. Есть тайм-аут, ограниченные повторы сети/429/5xx,
задержка между страницами, удаление дублей и остановка повторяющихся страниц.
Достижение лимита страниц не считается успешной полной выгрузкой. Неизвестный total
остаётся `null`; фактически собранное количество хранится отдельно.

Правила полей можно уточнить через `list.fieldMap`, например
`{"title":"product.name","amount":"starting_cost"}`. Ссылки берутся из данных
или шаблона `list.detailUrlTemplate`. Автоматически шаблон выводится только из
перехваченных реальных ссылок на карточки, совпавших с id в ответе API.

Пока не реализованы: загрузка и анализ ТЗ/вложений, проверка квалификационных
требований, вход по ЭЦП, веб-интерфейс, уведомления и встроенный планировщик.
Наличие полного снимка одного раздела не означает охват всего портала.
Актуальность источников и модель необходимо проверять отдельным живым прогоном.

Исходники и тесты включены в Git. `.env`, локальный профиль, рецепты и данные —
локальные файлы; не публикуйте ключи и сессионные заголовки.

## Самостоятельное исследование и исправление адаптеров

Агент получает инструменты исследования, а не только готовые функции сбора:

- `plan_search` — сохраняет предмет запроса и план до сетевых действий;
- `inspect_page` — читает текст/HTML/формы/ссылки и выполняет навигацию в одной сессии;
- `inspect_capture` — показывает отдельные связанные запросы и ответы;
- `read_resource` — исследует клиентские JS/JSON/HTML-ресурсы и ищет фрагменты;
- `probe_api` — проверяет самостоятельно найденный запрос чтения;
- `run_code` — выполняет написанный моделью JS над JSON захвата, ответа, записей или ресурса;
- `test_collection_plan` — испытывает план и адаптер на первых двух страницах;
- `collect_dom` — собирает HTML-карточки и проходит по кнопке следующей страницы;
- `research_status` — сверяет фактический охват и незавершённые этапы.

RPC/GraphQL-запросы одного URL различаются по телам и операциям. Каждая пара имеет
`candidate_id`; `request_id` позволяет открыть полный сохранённый ответ через
`run_code(dataset="response",id=...)`. Бакет из старого дампа с несвязанными телами
не считается достоверным RPC-запросом: агент переснимает его сам.

Пути `params.offset`, `variables.after`, `result.items`, cursor/hasMore и ошибки
JSON-RPC при HTTP 200 поддерживаются непосредственно. Неизвестный JSON можно
преобразовать собственным `transform_code`, например:

```js
return {
  items: data.data.search.edges.map(edge => edge.node),
  total: data.data.search.total,
  next: data.data.search.pageInfo.endCursor,
  more: data.data.search.pageInfo.hasNextPage
};
```

Модель передаёт такой код в `test_collection_plan`; после успешного испытания
вызывает `collect_tenders(plan_id=...)`. Рецепт сохраняется с кодом и свидетельствами
проверки. При изменении API агент исправляет этот адаптер, не переписывая ядро
приложения. Непроверенный план или повторяющаяся первая страница не становятся
рабочим рецептом. Сам факт присутствия полей не доказывает правильность всех
смысловых выводов: она проверяется отдельно по карточкам и задаче пользователя.

`run_code` — синхронная обработка JSON в отдельном Worker с лимитами времени и
памяти. Доступ к shell, модулям, сети, переменным окружения и записи исходников
не предоставляется. Это ограниченный инструмент обработки данных на Worker/vm,
а не полноценная ОС-песочница для выполнения произвольного чужого программного
обеспечения. Исходные данные сайта сохраняются отдельно от вывода адаптера.
Сгенерированная ссылка не подменяет подтверждённый маршрут карточки.

Журнал находится в `data/runs/<run-id>.json`: запрос пользователя, вызовы,
диагнозы, критерии, коллекции, оставшиеся разделы и доступная статистика токенов.
Полные ответы инструментов сохраняются в `data/runs/<run-id>/tools/`.
Модель получает короткие страницы и последние полные обмены с инструментами;
прежняя история заменяется фактическим состоянием: критериями, источниками,
счётчиками, ошибками, идентификаторами планов и кандидатами. Reasoning в оставленных
обменах сохраняется без изменений. Полный ответ можно прочитать адресно через
`read_tool_result(call_index, path, offset, limit)`; для прежнего запуска — с `run_id`.
Чтение целого объекта возвращает оглавление его полей. Повтор того же поля/смещения
не пересылает данные повторно; изменение только `limit` или регистра `path` не
обходит проверку. Следующие страницы и фрагменты по `next_offset` доступны.
Служебные ответы каталога/обновления уже учтены в состоянии и не считаются новым
знанием. После двух шагов без новых данных контроллер выполняет следующий
проверяемый шаг по готовым критериям, а при продолжающихся повторах останавливается
с частичным результатом.
Повтор одинакового локального запроса возвращает ссылку на прежний ответ;
после обновления данных этот кэш сбрасывается.

`query_data` ищет также архивные записи: его `matched` означает текстовое совпадение.
`field` применяется и к `contains`, и к точному `value`, включая пути `source.key`.
Несколько синонимов можно проверить одним `contains_any`. `select_tenders` возвращает
`matched`, `needs_review`, `diagnosis.exclusion_reasons`, `freshness` и следующий шаг.
Кандидаты для проверки видны в консоли и в частичном результате даже при лимите/API-сбое.
Причины исключения могут пересекаться.
Фактический итог сбора формируется кодом по последним критериям и данным:
модель не может своим свободным текстом переименовать `needs_review` в подтверждённое
совпадение, изменить суммы или заявить непроверенный охват. Результат сохраняется
в `<run-id>.result.json` и `.md`; при незавершённом исследовании — в `.partial.*`.

Для текущего поиска код заранее проверяет локальный каталог. Пока известные
рабочие рецепты не проверены, модель получает инструменты обновления источников;
после попыток обновления доступны инструменты исследования и исправления рецептов.
На знакомом сайте основной инструмент — `search_tenders(host, criteria)`:
модель задаёт смысловые критерии, а код за один вызов обновляет все известные
рецепты и выполняет подбор. `refresh_sources(host, keys?)` обновляет источники
пакетно без подбора. Ошибки отдельных источников не мешают остальным; прежние
полные снимки сохраняются. Уже проверенные источники в текущем запуске пропускаются.
Карточки можно прочитать одним `inspect_pages(urls)` (до пяти ссылок).
Для этого маршрута используются короткая системная инструкция, актуальные
инструменты и последний полный обмен с моделью. При неисправном рецепте или
явном запросе полного обхода сайта доступен режим исследования.
Когда известные источники обновлены и карточки кандидатов проверены в доступном
объёме, выполняется один заключительный анализ без инструментов (до 768 выходных
токенов), затем код формирует фактический отчёт. Перебирать старые данные после
этого этапа модель не может. В обычном успешном сценарии это три обращения:
критерии/пакетный поиск, пакетное чтение карточек, заключительный анализ.
Это не гарантия трёх обращений для неизвестных сайтов, неисправных рецептов
или неполного доступа. В офлайн-режиме сетевые инструменты выключены.
Контроль итога требует повторного подбора после обновления данных и проверки
карточек кандидатов. Офлайн-запрос «по сохранённым данным» разрешает работу без сети.
Произвольные жёсткие условия требуют `constraint_evidence` с точной цитатой пользователя;
это проверяет наличие цитаты, но не доказывает её правильную смысловую интерпретацию.
Необязательные числовые ограничения `null` означают отсутствие условия.
Сбой модели/баланса сохраняет `<run-id>.partial.json` и `.md`; частичный результат
не объявляется полным. Сохранённые снимки и рецепты доступны следующему запуску.

Лимиты можно задать в `.env`:

```dotenv
TENDER_MAX_STEPS=60
TENDER_MAX_SECONDS=900
TENDER_MAX_INPUT_TOKENS=500000
TENDER_MAX_TOTAL_OUTPUT_TOKENS=20000
TENDER_MAX_OUTPUT_TOKENS=4096
TENDER_MAX_CONTEXT_CHARS=40000
TENDER_MAX_NO_PROGRESS_TURNS=2
```

Это лимиты исследования, а не фиксированная денежная стоимость. Входные токены
суммируются по обращениям, включая кэшированные; один ответ модели может
пересечь остаток лимита. `TENDER_MAX_CONTEXT_CHARS` задаёт порог сокращения истории;
неделимый последний обмен с reasoning и текущий запрос сохраняются целиком.
Для знакомого сайта порог сокращения — не более 18000 символов (либо меньше,
если задан меньший `TENDER_MAX_CONTEXT_CHARS`). Это порог контекста, не обещание
фиксированного числа токенов. В конце запуска консоль показывает фактические
входные/выходные токены и число обращений к модели. Журнал содержит `model_calls`,
`read_stats`, `context_chars` и `no_progress_turns`.
При расходе 80% бюджета агент останавливает исследование и составляет итог кодом.
При исчерпании сохраняется частичный результат с кандидатами без дополнительных запросов
к модели. `--stub` не проверяет качество настоящего AI-планирования.
HTTP 429 с исчерпанной дневной квотой не повторяется; временные 429/5xx сохраняют
ограниченные повторы. Сам лимит провайдера изменением кода не снимается.

Проверки:

```bash
npm test                 # REST/RPC/GraphQL, код, восстановление, бюджеты, сохранение
npm run test:browser     # настоящий браузер + локальный HTTP-стенд, включая HTML-пагинацию
```

Невозможно гарантировать полный сбор любого произвольного портала: существуют
недоступная авторизация, CAPTCHA, закрытые данные, сбои и ограничения времени.
Агент обязан сам исследовать доступные альтернативы и показать проверенный охват
и остаток, вместо просьбы пользователю вручную искать параметры API.

Продолжение после пополнения API или устранения сбоя:

```bash
npm run ask -- --resume <run-id>
```

Команда восстанавливает исходную задачу, критерии, собранные разделы и последние
диагнозы в компактном контексте. Данные и рецепты повторно используются; доступность
модели и актуальность снимков всё равно проверяются. Один только файл журнала не
означает, что исследование успешно завершено.

Maintenance

ActivityMaintained
ResponsivenessNo issues