6. Справочник хуков
🔵 Хуки перехватывают жизненный цикл агента. Это YAML-файлы в
config/agents/<agent>/hooks/, подключаются через hook_files: (или по имени в
hooks:). Поле kind: выбирает хук.
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) |
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) |
при ошибке ядра у процесса (например, невосстановимый сбой инференса) |
Исход 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, не
зациклился).
Трассировка вмешательств
Каждое решение хука пишется как событие 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 |
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 |
Хук может и просто изменить answer на месте (Rust) — изменение детектируется.
Если ответ изменён или подавлен, ядро пере-эмитит исправленный ответ как
final_answer-токен, чтобы клиент показал его вместо стримленного оригинала.
Изоляция. Каждый вызов хука обёрнут в защиту от паники: паникующий хук
пропускается с нейтральным результатом и не роняет ход. На вызов также
действует лимит времени — timeouts.hook_secs в YAML агента (перекрывает
общий дефолт ядра, по умолчанию 300 с). По таймауту хук пропускается с
нейтральным результатом; в лог пишется предупреждение.
6.2 Виды хуков
| kind | Назначение |
|---|---|
faq_cache |
кэш (вопрос → ответ) в DuckDB; отдавать повторы, пропуская LLM |
inject |
декларативно впрыскивать системные/пользовательские сообщения |
escalate |
эскалация на человека, когда агент не может помочь |
memory |
долговременная память на пользователя (факты + сводка) |
script_hook |
скриптовый хук на Rhai/JavaScript (см. 07-scripts) |
6.3 faq_cache
Запоминает пары (вопрос, ответ) и отдаёт кэшированный ответ на повторные или похожие вопросы:
- На сообщении пользователя ищет в хранилище уже отвеченные вопросы (префильтр по пересечению токенов), затем спрашивает судью (дочерний агент-процесс — свой PID, провайдер ядра, бюджет токенов), похож ли кандидат.
- При уверенном совпадении кэшированный ответ впрыскивается как подсказка
(
short_circuit: false) или возвращается напрямую, пропуская LLM (short_circuit: true). - После каждого хода новая пара записывается.
kind: faq_cache
name: faq_cache
db_path: data/faq_cache_ngu.db # запасной путь (в проде — messages.db)
max_candidates: 5
min_confidence: 0.7
short_circuit: true
hit_notice: "⚡ Отвечено из кэша FAQ"
hint_template: "" # подсказка при short_circuit: false
fallback_score: 0.6 # пересечение токенов, если судья недоступен
judge_instructions: | # опциональный промпт судьи (JSON-ответ)
You are a question similarity judge...
store:
enabled: true
min_question_chars: 8
min_answer_chars: 8
| Поле | Описание |
|---|---|
db_path |
файл DuckDB (для standalone/тестов); в проде — общий message store в скоупе faq:<template> |
max_candidates |
сколько кандидатов отдаётся судье |
min_confidence |
мин. уверенность судьи (0..1) для принятия совпадения |
short_circuit |
true = отвечать напрямую (пропустить LLM) |
hit_notice |
уведомление при попадании в кэш |
judge_instructions |
кастомный промпт судьи |
fallback_score |
порог пересечения токенов без судьи |
store.* |
что записывать после хода |
6.4 inject
Чистое YAML-впрыскивание контекста на сообщениях пользователя:
kind: inject
name: city_hint
first_n: 1 # сработать только на первые N сообщений (на процесс)
notice: "Reminder: city context active"
messages:
- role: system
content: "Important: the user's city is Moscow."
- role: user
content: "Context reminder: {{question}}"
| Поле | Описание |
|---|---|
first_n |
сработать только на первые N сообщений (опущено = на каждое) |
notice |
опциональное уведомление в чат-UI |
messages |
список { role, content } (роль по умолчанию system) |
{{question}} заменяется входящим сообщением. Роли: system | user |
assistant.
6.5 escalate
Детерминированная эскалация на контакт человека, когда агент не может помочь:
kind: escalate
name: escalate
message: |
Если это не то, что вы искали — напишите: 📞 +7 ... ✉️ a@b.c
off_topic_keywords:
- iphone
- доставка
suppress_on_off_topic: true
empty_answer_patterns:
- "не нашёл"
- "нет информации"
min_answer_chars: 30
remind_next_turn: true
| Поле | Описание |
|---|---|
message |
контакт, показываемый пользователю (поддерживается {{question}}) |
off_topic_keywords |
вопросы с любым из слов считаются вне домена |
suppress_on_off_topic |
true = отказаться сразу (пропустить LLM); false = впрыснуть контакт подсказкой |
empty_answer_patterns |
финальные ответы с этими подстроками — «бесполезные» |
min_answer_chars |
ответы короче — «бесполезные» |
remind_next_turn |
впрыснуть контакт в контекст перед следующим сообщением |
6.6 memory
Долговременная память на пользователя, переживающая сессии:
- Перед сообщением вспоминает релевантные факты (префильтр по стеммингованному
пересечению токенов — RU/EN словоформы совпадают) и просит судью отобрать
полезные. Кандидаты несут
importanceи давность, предранжируются по релевантности → важности → давности. Отобранные факты + скользящая сводка впрыскиваются системным сообщением. - После хода запоминатель извлекает устойчивые факты и обновляет
скользящую сводку на пользователя (не на сессию). Факты дедуплицируются,
ограничиваются, могут помечаться
stale. Новый факт, противоречащий старому, перечисляет старый вsupersedes— тот автоматически выводится. - Запоминатель видит существующие факты каждый ход и может их исправлять;
обратная связь пользователя (like/dislike) поднимает/опускает
importanceвспомненных фактов.
kind: memory
name: user_memory
db_path: data/memory_ngu.db
max_candidates: 8
min_confidence: 0.6
notice: "" # опциональное уведомление о воспоминании ({{count}})
inject_template: "" # шаблон: {{facts}}, {{summaries}}, {{memories}}
judge_instructions: "" # промпт судьи релевантности
judge_model: "" # более дешёвая модель для судьи
backfill: true # false → пропускать кандидатов при слабом пересечении
max_memories_per_scope: 200
dedup_threshold: 0.8
store:
enabled: true
min_question_chars: 4
min_answer_chars: 20
extractor_instructions: "" # промпт запоминателя
memorizer_model: "" # более дешёвая модель для запоминателя
memorize_every: 1 # запускать запоминатель каждые N ходов
max_episodes_per_scope: 50
summary_max_chars: 2000
Память скоупится на пользователя (user_id с фронтенда), с откатом к сессии →
шаблону → имени процесса, если user id нет.
| Поле | Описание |
|---|---|
judge_model / store.memorizer_model |
запускать вспомогательный процесс на другой (дешевле) модели |
backfill |
true (по умолч.) добирает недавние несовпавшие факты, чтобы судья видел факты при языковых расхождениях |
store.memorize_every |
запускать запоминатель раз в N подходящих ходов на пользователя |
dedup_threshold |
порог стеммингованного пересечения, при котором новый факт сливается с существующим |
inject_template поддерживает три плейсхолдера: {{facts}} (факты списком),
{{summaries}} (сводки списком), {{memories}} (всё вместе, для обратной
совместимости).
Посмотреть, что агент помнит — админ-эндпоинт GET /v1/memory/:scope (факты с
флагами importance/stale, скользящая сводка, эпизоды).
6.7 script_hook
См. 07-scripts (Rhai или JavaScript).
6.8 Подключение хуков к агенту
# 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.9 Тестирование хуков локально
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> 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