6. Справочник хуков
🔵 Хуки перехватывают жизненный цикл агента. Это YAML-файлы в
config/agents/<agent>/hooks/, подключаются через hook_files: (или по имени в
hooks:). Поле kind: выбирает хук.
6.1 Точки жизненного цикла
| Точка | Сигнатура | Что делает |
|---|---|---|
before_user_message |
(ctx, msg) |
впрыснуть, переписать, отбросить сообщение или ответить самому |
after_turn |
(ctx, question, answer) |
после финального ответа; может выдать пост-ходовое уведомление |
after_loop |
(ctx, reason) |
при остановке цикла работы (job_done, terminated, blocked, budget_exhausted, max_turns, …) |
Исход before_user_message
Хук может вернуть карту с любым из полей (все опциональны):
| Поле | Эффект |
|---|---|
notice |
текст, стримящийся в чат-UI; не попадает в контекст LLM |
reply |
полный ответ — ход LLM пропускается |
suppress |
полностью отбросить сообщение пользователя |
inject |
дополнительные сообщения перед сообщением пользователя |
replace_message |
заменить сообщение пользователя |
Ошибка хука никогда не ломает ход — исключение логируется, хук возвращает нейтральный результат.
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
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
# флаги
--db <path> # перекрыть db_path (для тестов — временный файл)
--live # судья/запоминатель — реальный дочерний процесс (нужен ключ API)
--agent <name> # имя процесса в контексте хука
--template <name> # имя шаблона
--user <id> # user_id
--session <id> # session_id