事件 Hook 系统

生命周期 hook 让外部脚本无需修改 agent 代码即可响应 agent 和会话事件。当事件触发时,LibreFang 启动配置的可执行文件,通过环境变量传递结构化数据,然后继续运行——无论 hook 是否正常退出。

目录


目录结构

将每个 hook 放在 ~/.librefang/hooks/ 下的独立子目录中:

~/.librefang/hooks/
  notify-on-end/
    HOOK.yaml
    run.sh
  log-session-reset/
    HOOK.yaml
    run.py

每个子目录必须包含且只包含一个 HOOK.yamlcommand 中指定的可执行文件可以是同目录下的任意文件,也可以是系统上的绝对路径——只需确保它是可执行的(chmod +x)。


HOOK.yaml 格式

name: notify-on-agent-end
description: "POST to a webhook when an agent finishes a run"

# Events that trigger this hook. Wildcards are supported.
events:
  - agent:end

# Relative path (from this file's directory) or absolute path.
command: ./run.sh
字段是否必填说明
name在日志中显示的人类可读标识符
description用于文档目的的自由文本备注
events事件名称列表或 glob 模式(如 agent:**
command事件触发时要调用的可执行文件

支持的事件

事件触发时机
agent:startAgent 循环开始处理新请求
agent:endAgent 循环完成(成功、错误或达到最大步数)
agent:step一次运行中的每个中间推理步骤
session:start在 daemon 生命周期中首次创建或恢复会话
session:end会话被显式关闭
session:reset会话因自动重置策略或手动 API 调用而被清空
gateway:startupLibreFang daemon 完成启动序列

通配符模式:

模式匹配范围
agent:*agent:startagent:endagent:step
session:*session:startsession:endsession:reset
*所有事件

环境变量

变量类型内容
HOOK_EVENTstring触发的确切事件名称,如 agent:end
HOOK_EVENT_DATAJSON string事件的结构化载荷

载荷参考

agent:start

字段类型说明
agent_idstring (UUID)启动本次运行的 agent
session_idstring与本次运行关联的会话
senderstring触发本次运行的发送方标识符
message_previewstring入站消息的前 120 个字符

agent:end

字段类型说明
agent_idstring (UUID)完成本次运行的 agent
session_idstring与本次运行关联的会话
statusstring"ok""error""max_steps"
tokens_usedinteger消耗的总 token 数(prompt + completion)
stepsinteger执行的推理步骤数
duration_msinteger本次运行的墙钟时间(毫秒)
response_previewstring最终响应的前 120 个字符

agent:step

字段类型说明
agent_idstring (UUID)执行该步骤的 agent
session_idstring本次运行的会话
step_indexinteger当前运行中从零开始的步骤计数器
tool_callstring | null正在调用的工具名称(如有)

session:start

字段类型说明
agent_idstring (UUID)所属 agent
session_idstring新启动的会话

session:end

字段类型说明
agent_idstring (UUID)所属 agent
session_idstring已关闭的会话

session:reset

字段类型说明
agent_idstring (UUID)所属 agent
session_idstring被重置的会话
reset_reasonstring"idle""daily""suspended""manual"
message_count_clearedinteger已清除的历史消息数量

gateway:startup

字段类型说明
versionstringLibreFang daemon 版本
started_atstring (ISO 8601)启动完成的 UTC 时间戳

transform_tool_result

一个变换型 hook(其他都是观察型):handler 可以在原始 tool-result 字符串进入对话上下文之前重写它。在 after_tool_call 之后、sanitization 之前触发,所以重写器看到的是原始 tool 输出,runtime 看到的是重写后的。

字段类型说明
agent_idstring跑这个 tool 的 agent
tool_namestring执行的 tool 名
tool_inputobject传给 tool 的输入
tool_outputstringtool 返回的原始字符串结果

Handler 契约HookHandler::transform):

fn transform(&self, ctx: HookContext) -> Result<Option<String>, String>;
  • Ok(Some(replacement)) — 替换本回合剩余阶段看到的 tool_output
  • Ok(None) — handler 弃权;链上下一个 handler 上。
  • Err(msg)warn! 记日志后跳过(fail-open)。其他 handler 仍跑。

HookRegistry::fire_transform() 按注册顺序遍历链,第一个 Ok(Some(_))。适合:脱敏(在 LLM 看到前去掉 git diff 输出里的 secret)、摘要(把 5 MB JSON dump 截成 200 行摘要)、测试(给你不想真调的 MCP tool 注入确定性桩)。


示例脚本

任意 agent 结束时发送一条 Slack 兼容的 webhook 通知:

#!/usr/bin/env bash
# ~/.librefang/hooks/notify-on-end/run.sh
set -euo pipefail

# Only act on agent:end
if [ "$HOOK_EVENT" != "agent:end" ]; then
  exit 0
fi

AGENT_ID=$(echo "$HOOK_EVENT_DATA" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['agent_id'])")
STATUS=$(echo "$HOOK_EVENT_DATA"   | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['status'])")
TOKENS=$(echo "$HOOK_EVENT_DATA"   | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['tokens_used'])")

WEBHOOK_URL="${SLACK_WEBHOOK_URL:-}"
if [ -z "$WEBHOOK_URL" ]; then
  echo "SLACK_WEBHOOK_URL not set, skipping" >&2
  exit 0
fi

curl -s -X POST "$WEBHOOK_URL" \
  -H "Content-Type: application/json" \
  -d "{\"text\": \"Agent \`${AGENT_ID}\` finished with status *${STATUS}* (${TOKENS} tokens)\"}"

让 daemon 能执行它:

chmod +x ~/.librefang/hooks/notify-on-end/run.sh

daemon 在启动时以及收到 SIGHUP 后会扫描 hooks 目录。运行中的 daemon 不会在执行过程中热重载 hook 文件——新增 hook 后请重启或发送 SIGHUP


错误处理

  • 非零退出码:记录 WARN 并继续
  • 5 秒墙钟超时;超时后将被终止
  • 可执行文件缺失:记录为 WARN,其他 hook 仍正常触发

限制

  • hook 内部无法调用 LLM
  • 无返回值——hook 不能修改 agent 的响应
  • 无密钥注入
  • 每个 HOOK.yaml 只能指定一个可执行文件