🤖 AI-агенты для бизнеса

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_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

Хук может и просто изменить 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

Запоминает пары (вопрос, ответ) и отдаёт кэшированный ответ на повторные или похожие вопросы:

  1. На сообщении пользователя ищет в хранилище уже отвеченные вопросы (префильтр по пересечению токенов), затем спрашивает судью (дочерний агент-процесс — свой PID, провайдер ядра, бюджет токенов), похож ли кандидат.
  2. При уверенном совпадении кэшированный ответ впрыскивается как подсказка (short_circuit: false) или возвращается напрямую, пропуская LLM (short_circuit: true).
  3. После каждого хода новая пара записывается.
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

Долговременная память на пользователя, переживающая сессии:

  1. Перед сообщением вспоминает релевантные факты (префильтр по стеммингованному пересечению токенов — RU/EN словоформы совпадают) и просит судью отобрать полезные. Кандидаты несут importance и давность, предранжируются по релевантности → важности → давности. Отобранные факты + скользящая сводка впрыскиваются системным сообщением.
  2. После хода запоминатель извлекает устойчивые факты и обновляет скользящую сводку на пользователя (не на сессию). Факты дедуплицируются, ограничиваются, могут помечаться stale. Новый факт, противоречащий старому, перечисляет старый в supersedes — тот автоматически выводится.
  3. Запоминатель видит существующие факты каждый ход и может их исправлять; обратная связь пользователя (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