钩子 — 基础

生命周期点、组合、钩子上下文和本地测试。所有钩子的列表见 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) 拦截流式 token:替换或抑制(实时审核/编辑;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 中、能力检查之前调用 — 统一的拦截点, 包括代理间 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 — 助手 回答的 token,false — 服务/状态)。所有字段均为可选:

字段 效果
replace 替换 token 文本
suppress 不向客户端发送 token

钩子只影响发往客户端的流:原始文本仍然会进入 上下文和转录。

结果 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

输出护栏:在最终回复写入上下文和转录之前调用。所有字段均为可选:

字段 效果
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)时,其余 输出钩子不会被调用:胜出者由 priority 决定(相同时按 注册顺序)。同样的钩子也适用于通过 job_done 完成的场景:如果护栏拒绝 completion,内核不会终止进程,而是将修正结果作为 job_done 的结果返回给模型(veto)——每回合不超过 MAX_REWORK_ROUNDS(2)次。

隔离。 每次钩子调用都包裹了防 panic 保护:发生 panic 的钩子 会被跳过并返回中立结果,不会使回合崩溃。调用还受 时间限制——在智能体的 YAML 中设置 timeouts.hook_secs(覆盖 内核的全局默认值,默认 300 秒)。超时后钩子被跳过并返回 中立结果;日志中会写入警告。

指标。 每次钩子调用都会被内核计入指标 (hook, point): 调用次数、panic 次数、超时次数、总时长和平均时长、最后一次 错误。管理员可以获取快照: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 本地测试钩子

agent-os hook <hook.yaml> info                    # показать распарсенный конфиг
agent-os hook <hook.yaml> user "Привет, мир"      # запустить before_user_message
agent-os hook <hook.yaml> turn "question" "answer" # запустить after_turn
agent-os hook <hook.yaml> loop "job_done"          # запустить after_loop
agent-os hook <hook.yaml> tool echo '{"msg":"x"}'  # запустить before_tool_call
agent-os hook <hook.yaml> result echo "text" false # запустить after_tool_result
agent-os hook <hook.yaml> inference '{"model":"m"}'
agent-os hook <hook.yaml> token "chunk" [true|false]   # запустить on_token
agent-os hook <hook.yaml> evict "summary"              # запустить before_context_evict
agent-os hook <hook.yaml> spawn                    # запустить on_spawn
agent-os hook <hook.yaml> terminate 0              # запустить on_terminate
agent-os hook <hook.yaml> error "boom"             # запустить on_error

# флаги
--db <path>       # перекрыть db_path (для тестов — временный файл)
--live            # судья/запоминатель — реальный дочерний процесс (нужен ключ API)
--agent <name>    # имя процесса в контексте хука
--template <name> # имя шаблона
--user <id>       # user_id
--session <id>    # session_id