Внутреннее устройство Cron
Подсистема cron обеспечивает выполнение запланированных задач — от простых одноразовых задержек до повторяющихся заданий с cron-выражениями, включая внедрение навыков и кроссплатформенную доставку.
Ключевые файлы
| Файл | Назначение |
|---|---|
cron/jobs.py | Модель задания, хранилище, атомарное чтение/запись в jobs.json |
cron/scheduler.py | Цикл планировщика — обнаружение просроченных заданий, выполнение, отслеживание повторов |
tools/cronjob_tools.py | Регистрация и обработчик инструмента cronjob для модели |
gateway/run.py | Интеграция шлюза — тиканье cron в долгоиграющем цикле |
vibeos_cli/cron.py | Подкоманды CLI vibeos cron |
Модель планирования
Поддерживаются четыре формата расписания:
| Формат | Пример | Поведение |
|---|---|---|
| Относительная задержка | 30m, 2h, 1d | Одноразовое, срабатывает через указанный промежуток |
| Интервал | every 2h, every 30m | Повторяющееся, срабатывает через равные промежутки |
| Cron-выражение | 0 9 * * * | Стандартный 5-польный синтаксис cron (минута, час, день, месяц, день недели) |
| ISO-метка времени | 2025-01-15T09:00:00 | Одноразовое, срабатывает в точное время |
Поверхность для модели — это единый инструмент cronjob с операциями в стиле действий: create, list, update, pause, resume, run, remove.
Хранение заданий
Задания хранятся в ~/.vibeos/cron/jobs.json с семантикой атомарной записи (запись во временный файл, затем переименование). Каждая запись задания содержит:
{
"id": "a1b2c3d4e5f6",
"name": "Ежедневная сводка",
"prompt": "Обобщи сегодняшние новости об ИИ и раунды финансирования",
"schedule": {
"kind": "cron",
"expr": "0 9 * * *",
"display": "0 9 * * *"
},
"skills": ["ai-funding-daily-report"],
"deliver": "telegram:-1001234567890",
"repeat": {
"times": null,
"completed": 42
},
"state": "scheduled",
"enabled": true,
"next_run_at": "2025-01-16T09:00:00Z",
"last_run_at": "2025-01-15T09:00:00Z",
"last_status": "ok",
"created_at": "2025-01-01T00:00:00Z",
"model": null,
"provider": null,
"script": null
}
Состояния жизненного цикла задания
| Состояние | Значение |
|---|---|
scheduled | Активно, сработает в следующее запланированное время |
paused | Приостановлено — не сработает до возобновления |
completed | Лимит повторов исчерпан или одноразовое задание сработало |
running | В настоящее время выполняется (переходное состояние) |
Обратная совместимость
У старых заданий может быть одно поле skill вместо массива skills. Планировщик нормализует это при загрузке — одиночный skill повышается до skills: [skill].
Среда выполнения планировщика
Цикл тика
Планировщик работает с периодическим тиком (по умолчанию: каждые 60 секунд):
tick()
1. Захватить блокировку планировщика (предотвращает перекрывающиеся тики)
2. Загрузить все задания из jobs.json
3. Отфильтровать просроченные задания (next_run <= now И state == "scheduled")
4. Для каждого просроченного задания:
a. Установить состояние "running"
b. Создать новый сеанс AIAgent (без истории разговора)
c. Загрузить прикрепленные навыки по порядку (внедряются как сообщения пользователя)
d. Выполнить запрос задания через агента
e. Доставить ответ в настроенный целевой канал
f. Обновить run_count, вычислить next_run
g. Если лимит повторов исчерпан → state = "completed"
h. Иначе → state = "scheduled"
5. Записать обновленные задания обратно в jobs.json
6. Освободить блокировку планировщика
Интеграция шлюза
В режиме шлюза триггер cron (часть, которая решает, когда срабатывает просроченное задание — «Ось B») выбирается через подключаемого провайдера CronScheduler. Шлюз вызывает resolve_cron_scheduler() (cron/scheduler_provider.py) и запускает start() разрешенного провайдера в выделенном фоновом потоке, наряду с отдельным потоком обслуживания шлюза.
Активный провайдер выбирается ключом конфигурации cron.provider:
- пусто (по умолчанию) → встроенный
InProcessCronScheduler, который запускает исторический внутрипроцессный цикл, вызывающийscheduler.tick()каждые 60 секунд. Это побайтово идентично поведению до провайдера. - именованный провайдер (например,
chronos, управляемый cron-провайдер для развертываний с масштабированием до нуля) → обнаруживается изplugins/cron/<name>/или$VIBEOS_HOME/plugins/<name>/.
Если именованный провайдер отсутствует, не загружается или сообщает is_available() == False, резолвер возвращается к встроенному с предупреждением — cron никогда не остается без триггера. Встроенный провайдер находится в ядре (cron/scheduler_provider.py), а не в plugins/, поэтому его нельзя случайно удалить.
Что означает «срабатывание» (выполнение задания + доставка) — не меняется и является общим для всех провайдеров; оно остается в scheduler.run_job() / scheduler._deliver_result(). Провайдер управляет только триггером, никогда — выполнением.
В режиме CLI задания cron срабатывают только при выполнении команд vibeos cron или во время активных сеансов CLI.
Управляемый cron (Chronos) для масштабирования до нуля
Хостируемые шлюзы могут запускать провайдер Chronos (cron.provider: chronos) вместо встроенного тикера. Chronos позволяет простаивающему шлюзу масштабироваться до нуля и при этом выполнять задания cron: вместо 60-секундного внутрипроцессного цикла (который держал бы процесс активным) он просит инфраструктуру Nous вооружить ровно один управляемый одноразовый вызов на каждое задание в его реальное время следующего срабатывания. В момент срабатывания Nous вызывает шлюз обратно через аутентифицированный вебхук (POST /api/cron/fire); шлюз выполняет задание через тот же путь run_one_job, что и встроенный, затем перевооружает следующий одноразовый вызов. Между срабатываниями процесс может быть полностью остановлен — он просыпается только при реальном срабатывании, никогда по периодическому таймеру.
Поток (управляемый планировщик предоставляется Nous; агент не хранит учетные данные планировщика):
создание/обновление задания cron
→ Chronos просит Nous вооружить одноразовый вызов в next_run_at задания
(аутентифицируется существующим токеном Nous агента)
→ в момент срабатывания Nous вызывает шлюз: POST {callback_url}/api/cron/fire
(аутентифицируется краткосрочным, целевым JWT, выпущенным Nous)
→ шлюз проверяет токен, захватывает задание (хранилище compare-and-set, чтобы
развертывания с несколькими репликами срабатывали не более одного раза), выполняет его и перевооружает следующий одноразовый вызов
Конфигурация (все несекретно; на хостируемых агентах Nous устанавливает это при предоставлении):
| ключ | значение |
|---|---|
cron.provider | chronos для активации (пусто = встроенный тикер) |
cron.chronos.portal_url | Базовый URL Nous (вооружение + эмитент токена срабатывания) |
cron.chronos.callback_url | Собственный публичный базовый URL шлюза для входящих срабатываний |
cron.chronos.expected_audience | Аудитория токена срабатывания этого агента |
cron.chronos.nas_jwks_url | Набор ключей для проверки входящего токена срабатывания |
Если Chronos настроен неправильно или агент не вошел в Nous, resolve_cron_scheduler() возвращается к встроенному тикеру (с предупреждением в журнале) — cron никогда не теряет свой триггер. Повторяющиеся задания перевооружаются после каждого срабатывания; задания с repeat-N чисто останавливаются, когда счетчик исчерпан (без осиротевших одноразовых вызовов). Полный контракт взаимодействия агент↔Nous описан в docs/chronos-managed-cron-contract.md.
Изоляция свежего сеанса
Каждое задание cron выполняется в полностью новом сеансе агента:
- Нет истории разговора из предыдущих запусков
- Нет памяти о предыдущих выполнениях cron (если не сохранено в память/файлы)
- Запрос должен быть самодостаточным — задания cron не могут задавать уточняющие вопросы
- Набор инструментов
cronjobотключен (защита от рекурсии)
Задания с поддержкой навыков
Задание cron может прикрепить один или несколько навыков через поле skills. Во время выполнения:
- Навыки загружаются в указанном порядке
- Содержимое SKILL.md каждого навыка внедряется как контекст
- Запрос задания добавляется как инструкция задачи
- Агент обрабатывает объединенный контекст навыка + запрос
Это позволяет использовать повторно используемые, протестированные рабочие процессы без вставки полных инструкций в запросы cron. Например:
Создать ежедневный отчет о финансировании → прикрепить навык "ai-funding-daily-report"
Задания с поддержкой скриптов
Задания также могут прикреплять Python-скрипт через поле script. Скрипт выполняется до каждого хода агента, и его stdout внедряется в запрос как контекст. Это позволяет реализовать шаблоны сбора данных и обнаружения изменений:
# ~/.vibeos/scripts/check_competitors.py
import requests, json
# Получить примечания к релизам конкурентов, сравнить с последним запуском
# Вывести сводку в stdout — агент анализирует и сообщает
Тайм-аут скрипта по умолчанию составляет 120 секунд. _get_script_timeout() разрешает лимит через трехслойную цепочку:
- Переопределение на уровне модуля —
_SCRIPT_TIMEOUT(для тестов/подмены). Используется только когда отличается от значения по умолчанию. - Переменная окружения —
VIBEOS_CRON_SCRIPT_TIMEOUT - Конфигурация —
cron.script_timeout_secondsвconfig.yaml(читается черезload_config()) - По умолчанию — 120 секунд
Восстановление провайдера
run_job() передает настроенные пользователем резервные провайдеры и пул учетных данных в экземпляр AIAgent:
- Резервные провайдеры — читает
fallback_providers(список) илиfallback_model(устаревший словарь) изconfig.yaml, соответствуя шаблону_load_fallback_model()шлюза. Передается какfallback_model=вAIAgent.__init__, который нормализует оба формата в цепочку резервирования. - Пул учетных данных — загружается через
load_pool(provider)изagent.credential_pool, используя разрешенное имя провайдера времени выполнения. Передается только когда в пуле есть учетные данные (pool.has_credentials()). Обеспечивает ротацию ключей того же провайдера при ошибках 429/ограничения скорости.
Это зеркалирует поведение шлюза — без этого агенты cron не смогли бы восстанавливаться после ограничений скорости.
Модель доставки
Результаты заданий cron могут доставляться на любую поддерживаемую платформу:
| Цель | Синтаксис | Пример |
|---|---|---|
| Исходный чат | origin | Доставить в чат, где было создано задание |
| Локальный файл | local | Сохранить в ~/.vibeos/cron/output/ |
| Telegram | telegram или `telegram:<chat_id> | telegram:-1001234567890 |
| Discord | discord или discord:#channel | discord:#engineering |
| Slack | slack | Доставить в домашний канал Slack |
whatsapp | Доставить в WhatsApp | |
| Signal | signal | Доставить в Signal |
| Matrix | matrix | Доставить в домашнюю комнату Matrix |
| Mattermost | mattermost | Доставить в Mattermost |
email | Доставить по email | |
| SMS | sms | Доставить по SMS |
| Home Assistant | homeassistant | Доставить в разговор HA |
| DingTalk | dingtalk | Доставить в DingTalk |
| Feishu | feishu | Доставить в Feishu |
| WeCom | wecom | Доставить в WeCom |
| Weixin | weixin | Доставить в Weixin (WeChat) |
| BlueBubbles | bluebubbles | Доставить в iMessage через BlueBubbles |
| QQ Bot | qqbot | Доставить в QQ (Tencent) через Official API v2 |
Для тем Telegram используйте формат telegram:<chat_id>:<thread_id> (например, telegram:-1001234567890:17585`).
Оборачивание ответа
По умолчанию (cron.wrap_response: true) доставки cron оборачиваются:
- Заголовком, идентифицирующим имя задания cron и задачу
- Нижним колонтитулом, отмечающим, что агент не может видеть доставленное сообщение в разговоре
Префикс [SILENT] в ответе cron полностью подавляет доставку — полезно для заданий, которым нужно только записывать в файлы или выполнять побочные эффекты.
Изоляция сеанса
Доставки cron НЕ зеркалируются в историю разговора сеанса шлюза. Они существуют только в собственном сеансе задания cron. Это предотвращает нарушения чередования сообщений в разговоре целевого чата.
Защита от рекурсии
В сеансах, запущенных cron, набор инструментов cronjob отключен. Это предотвращает:
- Создание новых заданий cron запланированным заданием
- Рекурсивное планирование, которое могло бы взорвать использование токенов
- Случайную мутацию расписания задания изнутри самого задания
Блокировка
Планировщик использует межпроцессную файловую блокировку (fcntl.flock на Unix, msvcrt.locking на Windows) для предотвращения выполнения одного и того же пакета просроченных заданий дважды перекрывающимися тиками — даже между внутрипроцессным тикером шлюза и отдельным вызовом vibeos cron / ручным tick(). Если блокировку не удается захватить, tick() немедленно возвращает 0.
Интерфейс CLI
CLI vibeos cron предоставляет прямое управление заданиями:
vibeos cron list # Показать все задания
vibeos cron create # Интерактивное создание задания (псевдоним: add)
vibeos cron edit <job_id> # Редактировать конфигурацию задания
vibeos cron pause <job_id> # Приостановить выполняющееся задание
vibeos cron resume <job_id> # Возобновить приостановленное задание
vibeos cron run <job_id> # Запустить немедленное выполнение
vibeos cron remove <job_id> # Удалить задание