Хуки — основы

Точки жизненного цикла, композиция, контекст хука и локальное тестирование. Список всех хуков — 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 или исчерпан. Срабатывает один раз на каждый слой. levelsession | 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_loopreason)

Решения 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]

Композиция по данным (bag)

Хуки могут передавать друг другу именованные значения через per-turn мешок (HookBag): один публикует ключ, другой читает его как входной порт. Это не зависит от priority и не требует переписывать сообщение пользователя.

inputs:
  query: { from: retrieval.query, default: "", required: false }  # читать ключ мешка
outputs:
  - retrieval.query            # публиковать ключ (или объект с key/doc)

Стандартные ключи: 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, а также:


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