Хуки — основы
Точки жизненного цикла, композиция, контекст хука и локальное тестирование. Список всех хуков — 06-hooks-reference.
6.1 Точки жизненного цикла
| Точка | Сигнатура | Что делает |
|---|---|---|
before_user_message |
(ctx, msg) |
впрыснуть, переписать, отбросить сообщение или ответить самому |
before_tool_call |
(ctx, tool, args) |
переписать аргументы, отклонить (deny) или подменить результат (result) до вызова инструмента |
after_tool_result |
(ctx, tool, result) |
переписать результат инструмента (редакция, success, data, visual) |
before_inference |
(ctx, request) |
маршрутизация модели, параметры сэмплинга, переупаковка промпта/инструментов |
after_turn |
(ctx, question, answer) |
после финального ответа; может выдать пост-ходовое уведомление |
after_assistant_message |
(ctx, answer) |
переписать/подавить финальный ответ до сохранения (output-guardrail) |
on_token |
(ctx, token, final_answer) |
перехватить стримимый токен: заменить или подавить (модерация/редакция на лету; output-only) |
before_context_evict |
(ctx, evicted, summary) |
кастомная компакция: заменяет summary вытесненных из окна сообщений |
after_loop |
(ctx, reason) |
при остановке цикла работы (job_done, terminated, blocked, budget_exhausted, max_turns, …) |
on_spawn |
(ctx, child_pid) |
после создания процесса (хуки самого процесса) |
on_terminate |
(ctx, exit_code) |
при завершении процесса (до пробуждения ожидающих) |
on_error |
(ctx, message) |
при ошибке ядра у процесса (например, невосстановимый сбой инференса) |
on_session_created |
(ctx, session_id) |
после регистрации сессии процесса |
on_session_closed |
(ctx, session_id) |
при закрытии/экспирации сессии (до удаления записи) |
before_retry |
(ctx, attempt, error) |
решение о ретрае: retry=false запретить, delay_ms — backoff |
before_syscall |
(ctx, syscall) |
перехват syscall (в т.ч. IPC Send/Recv): deny с причиной |
on_budget_threshold |
(ctx, level, spent, max, ratio) |
бюджет пересёк warn_at (или исчерпан) — проактивная эскалация |
Исход before_tool_call
Все поля опциональны; хуки идут по порядку регистрации, каждый видит аргументы, оставленные предыдущим:
| Поле | Эффект |
|---|---|
arguments |
заменить аргументы вызова |
deny |
отклонить вызов с причиной → failed-результат, обработчик не запускается |
result |
полностью подменить результат (кэш/мок/предвычисленный ответ), обработчик не запускается |
notice |
текст, стримящийся в чат-UI; не попадает в контекст LLM |
result имеет приоритет над deny.
Исход after_tool_result
| Поле | Эффект |
|---|---|
content |
заменить текст для LLM |
success |
переопределить признак успеха |
data |
заменить структурные данные |
visual |
заменить UI-блок |
notice |
уведомление в чат-UI |
Исход before_inference
Хук получает полностью собранный запрос
(request: { model, messages, tools, max_tokens, temperature }) и может
вернуть любые из полей model / temperature / max_tokens / messages /
tools для переопределения. Вызывается на каждый вызов инференса в цикле хода
(включая ретраи), поэтому условия должны быть идемпотентны. Для llm(...)-
хелпера скриптов/хуков не вызывается (чтобы хук, зовущий llm, не зациклился).
Дополнительно можно управлять результатом вызова:
| Поле | Эффект |
|---|---|
deny |
отказ от инференса с причиной → процесс блокируется (Blocked) |
response |
синтетический ответ ассистента — провайдер не вызывается |
Исход before_retry
Вызывается, когда транзиентная ошибка инференса готова к ретраю (и на открытии провайдера, и в середине стрима). Все поля опциональны:
| Поле | Эффект |
|---|---|
retry |
false — запретить ретрай (перейти к следующему провайдеру) |
delay_ms |
переопределить backoff (мс) |
Исход before_syscall
Вызывается в handle_syscall до проверки capability — единая точка перехвата,
включая межагентные Send/Recv:
| Поле | Эффект |
|---|---|
deny |
отклонить syscall с причиной (AccessDenied) |
on_budget_threshold
Уведомление (без возврата): бюджет достиг warn_at или исчерпан.
Срабатывает один раз на каждый слой. level — session | group | user,
spent, max, ratio. Для сессии задаётся в budget: { warn_at: 0.8 }
агента; для группы и per-user — в config/budgets.yaml (см.
03-server-configuration).
Исход on_token
Вызывается на каждый чанк, стримящийся клиенту (final_answer: true — токены
ответа ассистента, false — служебные/статусные). Все поля опциональны:
| Поле | Эффект |
|---|---|
replace |
заменить текст токена |
suppress |
не отправлять токен клиенту |
Хук влияет только на поток клиенту: исходный текст всё равно попадает в контекст и транскрипт.
Исход before_context_evict
Когда окно контекста переполняется, ядро вытесняет старые сообщения. Хук
получает вытесненные сообщения (evicted: список { role, content }) и текущее
summary, и может вернуть своё:
| Поле | Эффект |
|---|---|
summary |
заменить summary вытесненного контекста (кастомная суммаризация) |
Трассировка вмешательств
Каждое решение хука пишется как событие hook_intercepted в logs/events.db и
per-process JSONL (logs/process-*.jsonl) — для аудита и SQL-запросов.
Точки и их action:
| Точка | action |
|---|---|
before_tool_call |
rewrite_args / deny / short_circuit |
after_tool_result |
transform_result |
before_inference |
rewrite_request |
on_token |
replace / suppress |
before_context_evict |
compact |
before_user_message |
reply / suppress / transform / inject / notice |
after_assistant_message |
replace / suppress / notice |
after_turn |
notice |
after_loop |
after_loop (с reason) |
Решения before_tool_call / after_tool_result / before_inference дополнительно
попадают в транскрипт сессии строкой kind hook (logs/messages.db) с
метаданными { point, hook, action, … } — видно через trace_tool и в UI.
Нейтральный (ничего не меняющий) хук событие не порождает.
Записывается что сделал хук, а не исходные данные, поэтому редакция не логирует то, что скрыла.
Исход before_user_message
Хук может вернуть карту с любым из полей (все опциональны):
| Поле | Эффект |
|---|---|
notice |
текст, стримящийся в чат-UI; не попадает в контекст LLM |
reply |
полный ответ — ход LLM пропускается |
suppress |
полностью отбросить сообщение пользователя |
inject |
дополнительные сообщения перед сообщением пользователя |
replace_message |
заменить сообщение пользователя |
Ошибка хука никогда не ломает ход — исключение логируется, хук возвращает нейтральный результат.
Исход after_assistant_message
Output-guardrail: вызывается с финальным ответом до его записи в контекст и транскрипт. Все поля опциональны:
| Поле | Эффект |
|---|---|
replace |
заменить сообщение ассистента целиком |
suppress |
не сохранять ответ (в контекст/транскрипт ничего не пишется) |
notice |
уведомление в чат-UI |
rework |
отклонить ответ и продолжить ход: ядро (опц.) очищает стрим (replace) и впрыскивает correction-сообщение, модель отвечает заново. Не более MAX_REWORK_ROUNDS (2) раз за ход. Очистка управляется Rework::clear_stream (по умолчанию true) |
Хук может и просто изменить answer на месте (Rust) — изменение детектируется.
Если ответ изменён или подавлен, ядро пере-эмитит исправленный ответ как
final_answer-токен (с replace: true), чтобы клиент показал его вместо
стримленного оригинала. rework не сохраняет отклонённый ответ и не вызывает
after_turn — управление возвращается в цикл на новый раунд инференса.
Пайплайн. При первом терминальном решении (rework/suppress) остальные
output-хуки не вызываются: победитель определяется priority (порядок
регистрации при равенстве). Те же хуки применяются и к завершению через
job_done: если guardrail отклоняет
completion, ядро не завершает процесс, а отдаёт корректировку модели как
результат job_done (veto) — не более MAX_REWORK_ROUNDS (2) раз за ход.
Изоляция. Каждый вызов хука обёрнут в защиту от паники: паникующий хук
пропускается с нейтральным результатом и не роняет ход. На вызов также
действует лимит времени — timeouts.hook_secs в YAML агента (перекрывает
общий дефолт ядра, по умолчанию 300 с). По таймауту хук пропускается с
нейтральным результатом; в лог пишется предупреждение.
Метрики. Каждый вызов хука учитывается ядром в метрике (hook, point):
число вызовов, паник, таймаутов, суммарная и средняя длительность, последняя
ошибка. Снимок доступен администратору: GET /v1/hooks/metrics.
6.23 Подключение хуков к агенту
# config/agents/<agent>/agent.yaml
hook_files:
- hooks/faq_cache.yaml
- hooks/memory.yaml
Во время работы хуки можно подключать/отключать у процесса через Kernel API —
паритет с инструментами: grant_hook(pid, name) / revoke_hook(pid, name) /
process_hooks(pid) (ср. grant_tool/revoke_tool/process_tools).
Подключённые хуки действуют со следующего вызова.
tool-шаги воркфлоу проходят через те же before_tool_call /
after_tool_result хуки процесса запуска (Kernel::run_tool_for); allowlist
процесса к ним не применяется — состав шагов задаёт автор воркфлоу.
6.24 Композиция и глобальные хуки
Любой YAML-хук принимает общие ключи композиции:
priority: 10 # выше — раньше; при равенстве сохраняется порядок регистрации
enabled: true # false = хук не вызывается
global: true # применять ко ВСЕМ процессам, а не только тем, что ссылаются
when: # условия активации (все заполненные поля должны совпасть)
templates: [ngu-agent]
agents: [ngu-1] # имена процессов
users: [alice]
sessions: [sess-42]
- Список — это альтернативы (OR); разные поля — AND. Пустое поле = без ограничения.
- Хуки, подключённые к агенту (
hooks:), всё равно фильтруются поwhen/enabled. - Глобальные хуки: положите YAML в
config/hooks/*.yaml— сервер загрузит их при старте и пометитglobal: true(единый DLP/аудит на всех агентов).
Композиция по данным (bag)
Хуки могут передавать друг другу именованные значения через per-turn мешок
(HookBag): один публикует ключ, другой читает его как входной порт. Это не
зависит от priority и не требует переписывать сообщение пользователя.
inputs:
query: { from: retrieval.query, default: "", required: false } # читать ключ мешка
outputs:
- retrieval.query # публиковать ключ (или объект с key/doc)
- У built-in-хуков порты заданы в коде и связываются автоматически: например,
condenseпубликуетretrieval.query, аragчитает его (иначе берёт сырое сообщение). Автору агента достаточно перечислить хуки. - Порядок разрешения входа: значение в мешке → литерал
default→ встроенный fallback хука. - Авто-порядок: производитель всегда раньше потребителя;
priority— тай-брейк, при цикле — откат наpriority. - Мешок скоупится процессом и очищается на новом сообщении пользователя.
- Скриптовые хуки читают
ctx.inputsи возвращаютoutputs({ "<ключ>": value }). - Схема и интроспекция:
GET /v1/hooks/bag(producers/consumers по ключам).
Стандартные ключи: retrieval.query, retrieval.source, intent.label,
lang.user, model.route (agent_os_core::bag_keys). Кастомные по умолчанию
неймспейсятся как <hook>.<port>.
6.25 Контекст хука
HookContext содержит: pid, agent, template, session_id, user_id,
config, а также:
parentиdepth— родитель и глубина в дереве процессов (субагенты);tools— allowlist инструментов процесса;budget_total/budget_spentи методbudget_remaining();inputs— разрешённые входные порты композиции ({ port: value });bag— per-turn мешок для публикации значений (см. §6.24).
6.26 Тестирование хуков локально
hook_tool <hook.yaml> info # показать распарсенный конфиг
hook_tool <hook.yaml> user "Привет, мир" # запустить before_user_message
hook_tool <hook.yaml> turn "question" "answer" # запустить after_turn
hook_tool <hook.yaml> loop "job_done" # запустить after_loop
hook_tool <hook.yaml> tool echo '{"msg":"x"}' # запустить before_tool_call
hook_tool <hook.yaml> result echo "text" false # запустить after_tool_result
hook_tool <hook.yaml> inference '{"model":"m"}'
hook_tool <hook.yaml> token "chunk" [true|false] # запустить on_token
hook_tool <hook.yaml> evict "summary" # запустить before_context_evict
hook_tool <hook.yaml> spawn # запустить on_spawn
hook_tool <hook.yaml> terminate 0 # запустить on_terminate
hook_tool <hook.yaml> error "boom" # запустить on_error
# флаги
--db <path> # перекрыть db_path (для тестов — временный файл)
--live # судья/запоминатель — реальный дочерний процесс (нужен ключ API)
--agent <name> # имя процесса в контексте хука
--template <name> # имя шаблона
--user <id> # user_id
--session <id> # session_id