跳到主要内容

钩子、确认与可扩展性:护栏与插件如何挂上去

30 秒导读: gptme 的主循环(→ 01)本身很瘦,真正让它"有护栏、能扩展"的是一套钩子系统。生命周期的每个关键时刻——会话开始、每一步生成前后、工具执行前后、文件保存、循环是否继续——都会 trigger_hook(...) 喊一嗓子;凡是想插一手的能力(要不要确认、要不要自动 git 提交、要不要注入成本提示、要不要拦截提示注入)都写成一个钩子挂上去。第三方插件也用同一套机制把自己的工具/钩子/命令带进来。这一章讲这套"挂载点"是怎么设计的。


1. 这是什么(零基础也能懂)

一句话定义

钩子(hook)= 一个"注册在某个时刻上、到点就被回调"的函数。 gptme 预先在生命周期里埋了一堆时刻(叫 HookType),你把函数 register_hook 到某个时刻,循环跑到那里就用 trigger_hook 把所有注册者依次叫起来。

它解决什么问题

主循环要保持简单,但一个真实的 agent 需要一大堆"横切"能力:

  • 执行危险命令前问一句 y/n(确认护栏);
  • 每一轮结束后自动 git 提交改动;
  • 生成前注入一条成本/token 提示给用户;
  • 工具输出里混进了可疑的"提示注入"文本时贴个警告;
  • 换了工作目录就把新目录的 AGENTS.md 注入上下文

如果把这些全写进主循环,循环会膨胀成一坨谁也读不懂的东西。gptme 的选择是:循环只负责在正确的时刻喊一声,具体做什么由挂在那个时刻上的钩子决定。 这就是"控制反转"——把可变的策略从固定的骨架里抽出来。

一句话直觉

把它想成前端的事件监听:循环 = DOM,HookType = 事件名(clicksubmit……),register_hook = addEventListener,trigger_hook = dispatchEvent。区别只在于:这里的"事件"是 agent 生命周期里的时刻,而监听器可以改写数据、甚至叫停后续监听器

用起来什么样

一个最小的钩子长这样(风格取自内置钩子,# 示意,非源码):

from gptme.hooks import HookType, register_hook
from gptme.message import Message

# 在"每一步生成之前"注入一条系统消息
def remind_time(messages, **kwargs):
yield Message("system", "现在是深夜,注意别写破坏性命令") # 钩子用 yield 产出消息

def register():
register_hook("time_reminder", HookType.GENERATION_PRE, remind_time)

钩子产出(yield)消息,循环把这些消息灌回上下文;或者什么都不产出,只做副作用(比如提交、发通知)。就这么简单——难的是在哪些时刻埋点、按什么顺序叫、谁能叫停谁,这正是下面要讲的。


2. 顶层全景(它大概怎么转)

整套机制只有三个动词和一张枚举:

部件干什么在哪
HookType所有"可挂载时刻"的枚举(会话/回合/步/生成/工具/文件/确认/征询…)gptme/hooks/types.py:66
register_hook把函数按 HookType + 优先级登记进注册表gptme/hooks/registry.py:623
trigger_hook到点触发某类钩子,按优先级依次跑、yield 出消息gptme/hooks/registry.py:654
HookRegistry每个 HookType 一个有序钩子列表;线程本地gptme/hooks/registry.py:109
内置钩子(一堆文件)确认 / autocommit / 成本 / 缓存 / 上下文注入 / 注入拦截 …gptme/hooks/*.py
插件系统让第三方把工具/钩子/命令带进来gptme/plugins/

怎么读下面这张图: 竖着是主循环的时间线(自上而下走一遍),每个 trigger_hook 落点右边挂着"到这里会被叫起来的那类钩子"。循环骨架细节见 → 01,这里只标"钩子在哪落地"。

主循环时间线 trigger_hook 落点 挂在上面的内置钩子(举例)
───────────── ─────────────── ──────────────────────
会话开始 ──▶ SESSION_START cost_awareness(初始化预算)

├─ 每个回合(turn)开始 ──▶ TURN_PRE
│ │
│ ├─ 每一步(step)开始 ──▶ STEP_PRE
│ │ │
│ │ ├─ 生成前 ──▶ GENERATION_PRE active_context / cost 提示注入
│ │ │ ▼ LLM 生成
│ │ ├─ 生成后 ──▶ GENERATION_POST cache_awareness(记录耗时)
│ │ │
│ │ └─ 执行工具 ─────────────────────────────┐
│ │ ├─ TOOL_EXECUTE_PRE │ auto_snapshots(快照)
│ │ ├─ [工具内部] TOOL_CONFIRM ◀── 确认护栏(cli/server/auto/allowlist)
│ │ └─ TOOL_EXECUTE_POST │ injection_screening / 结果注入
│ │
│ └─ 回合结束 ──▶ TURN_POST autocommit(自动 git 提交)

└─ 该不该再转一圈? ──▶ LOOP_CONTINUE
会话结束 ──▶ SESSION_END cost_awareness(打印总花费)

一句话把大盘说清:循环是"骨",钩子是"肉"。 骨头只在固定关节处 trigger_hook,肉挂在哪个关节、几块肉、谁先谁后,全由注册决定。工具确认是唯一"下沉到关节内部"的特例(§6)。


3. HookType 全景:一共有哪些"时刻"

HookType 是个 str 枚举,命名沿用 OpenCode 风格的点号分层 <category>.<event>(gptme/hooks/types.py:66-137)。术语上要先分清两个词(枚举 docstring 里也定义了):

  • 回合(turn): 一次完整的"用户↔助手"交换,可能含多步。
  • 步(step): 一次"LLM 生成 + 工具执行"循环。

按类别把成员列全(值即字符串名):

类别成员(枚举名 = 值)触发时刻
步/回合STEP_PRE=step.pre / STEP_POST=step.post每一步前 / 后
TURN_PRE=turn.pre / TURN_POST=turn.post每回合前 / 全部步跑完后
MESSAGE_TRANSFORM=message.transform改写并持久化助手消息内容
工具TOOL_EXECUTE_PRE / TOOL_EXECUTE_POST任一工具执行前 / 后
TOOL_TRANSFORM=tool.transform改写工具的输入/输出
TOOL_CONFIRM=tool.confirm执行前确认(特殊,见 §6)
文件FILE_SAVE_PRE/POSTFILE_PATCH_PRE/POST保存/打补丁前后
会话SESSION_START / SESSION_END会话起止(长事件用 START/END)
生成GENERATION_PRE / GENERATION_POST生成响应前 / 后
GENERATION_CHUNK=generation.chunk流式响应每一行
GENERATION_INTERRUPT打断生成
循环LOOP_CONTINUE=loop.continue决定"是否/如何再转一圈"
目录CWD_CHANGED=cwd.changed工具执行中工作目录变了
缓存CACHE_INVALIDATED=cache.invalidated提示缓存失效(如压缩后)
确认TOOL_CONFIRM(同上)
征询ELICIT=elicitagent 反向向用户要结构化输入

命名约定值得记一下(源码注释明说):PRE/POST 一律表示"某事件前后"的时序;START/END 只留给会话级的长事件。这让 agent 光看名字就能判断一个钩子是"包在某动作两侧"还是"整段会话的边界"。

每个 HookType 都配了一个 Protocol 类声明它的调用签名(gptme/hooks/types.py:140-359)。例如 SessionStartHook(logdir, workspace, initial_msgs),FilePostSaveHook(log, workspace, path, content, created)。这些 Protocol 不强制运行时校验,但配合下面的多签名重载给静态检查用。


4. 注册表:优先级、叫停、同步/异步

这节讲 HookRegistry(gptme/hooks/registry.py:109)怎么管钩子。核心就三件事:排序、遍历、容错。

4.1 注册即排序:优先级高者先跑

register(registry.py:116)把钩子按 HookType 塞进 self.hooks[hook_type] 列表,同名先删旧再加新(可热替换),然后 list.sort()(registry.py:157)。排序规则藏在 Hook.__lt__(gptme/hooks/types.py:393-395):

def __lt__(self, other):
# 注意方向反了:priority 大的排前面,同优先级按 name
return (self.priority, self.name) > (other.priority, other.name)

所以 priority 越大越先跑。内置钩子正是靠这个编排先后,例如 injection_screeningpriority=100 抢在别的 TOOL_EXECUTE_POST 前面把警告贴到工具输出旁(gptme/hooks/injection_screening.py:124-128)。

4.2 触发:依次跑、yield 消息、可叫停

trigger(registry.py:178)是心脏。它做的事:

  1. 取出该类型下所有 enabled 的钩子,分成 sync / async 两拨(registry.py:199-200)。
  2. async 钩子:每个用 contextvars.copy_context() 拷一份上下文,丢进 daemon=True 后台线程 fire-and-forget(registry.py:213-222)——适合日志、遥测、通知这类"不该阻塞主流程"的副作用,它们产出的消息只记 log、不回灌。
  3. sync 钩子:按序调用,hook.func(*args, **过滤后的kwargs)。钩子既可以是"返回生成器逐个 yield 消息"的,也可以直接返回一个 Message(registry.py:230-275)。

叫停机制是这套设计的关键一笔。钩子可以 yield 一个 StopPropagation() 哨兵(gptme/hooks/types.py:54),trigger 一旦见到它就 return,同类型里优先级更低的钩子全部不再执行(registry.py:254-267)。这让"前置校验失败就短路后续"成为可能——典型用法:precommit 检查失败时 yield StopPropagation(),把同挂在 TURN_POST 但优先级更低的 autocommit 拦下(见 §6.4 的 autocommit 注释)。

容错:单个 sync 钩子抛异常会被 logger.exception 记下然后 continue(registry.py:304-321),不拖垮整条链;唯一例外是 SessionCompleteException 会被重新抛出以传播"会话结束"信号。跑得慢(>5s)会打 warning,每次调用都记遥测(_record_sync_hook_call)。

4.3 kwargs 过滤:让老签名钩子不被新字段撑爆

trigger 传参前会过一道 _filter_kwargs_for_hook(registry.py:47):用 inspect.signature 看钩子接不接受某个关键字参数,接受 **kwargs 的原样放行,否则只喂它认识的参数,多出来的丢掉并记 debug。这解决了一个很实际的演进问题:核心在 trigger_hook 里新增一个 kwarg(比如给 GENERATION_PREmodel=),不会让老插件里"签名写死、没写 **kwargs"的钩子当场崩掉。

4.4 类型安全的多签名重载

register_hook 用了一长串 @overload(registry.py:443-620)。为什么?因为不同 HookType 期望的 func 签名不同(§3 的 Protocol)。重载让静态检查器在你写 register_hook("x", HookType.FILE_SAVE_PRE, fn) 时,能核对 fn 是不是 FilePreSaveHook 形状;而最后一个"兜底"重载接受非字面量的 HookType,给运行期动态注册留口子(registry.py:612-620)。真正的实现体只有一个(registry.py:623),转发给 get_registry().register(...)

注册表是线程本地的。 _registry_var 是个 ContextVar(registry.py:409),get_registry() 按当前上下文取/建。这样 server 模式下多个并发会话各有各的钩子集,互不串味。


5. 循环里的落点(回指主循环)

主循环(gptme/chat.py,详见 → 01)就是"在关节处 from .hooks import trigger_hook 然后到点喊"。把落点和消息回灌串起来看:

时刻触发点(chat.py)回灌方式
会话开始SESSION_START(chat.py:96)产出的消息追加进初始日志
回合前TURN_PRE(chat.py:205263)注入系统消息
步前STEP_PRE(chat.py:360)注入系统消息
生成后GENERATION_POST(chat.py:567)记录/改写
回合后TURN_POST(chat.py:441)autocommit 等在此提交
循环续否LOOP_CONTINUE(chat.py:281)决定是否再转一圈
会话结束SESSION_END(chat.py:165310)收尾/打印花费

统一的写法是"海象赋值 + yield from":

# 示意,非源码 —— chat.py 里每个落点大致长这样
if turn_pre_msgs := trigger_hook(HookType.TURN_PRE, manager=manager, ...):
for msg in turn_pre_msgs:
# 钩子产出的系统消息被灌回对话
...

GENERATION_PRE 和工具相关的两个落点不在 chat.py,而在更内层(见 §6)——这是有意的下沉。


6. 工具确认:为什么它下沉进 ToolUse.execute

这节是本章的重头戏,也是钩子设计里最巧的一处。

6.1 为什么不放在循环里

朴素做法是"循环在执行工具前先弹确认"。但 gptme 把工具执行本身封在了 ToolUse.execute(gptme/tools/base.py:671)里(工具执行管线细节 → 02)。确认如果留在循环层,循环就得知道"哪些工具要确认、怎么生成预览、编辑后的内容怎么塞回去"——又把知识漏回了循环。

所以确认跟着执行一起下沉:ToolUse.execute 内部先触发 TOOL_EXECUTE_PRE(base.py:696),把当前 ToolUse 塞进上下文变量 _current_tool_use(base.py:715),再调工具函数,最后触发 TOOL_EXECUTE_POST(base.py:765)。而确认发生在工具函数体内部——工具需要确认时自己喊一声 get_confirmation(),它通过上下文变量拿到当前 ToolUse,不需要循环显式传参。

例如 python 工具执行前:

# gptme/tools/python.py:252
confirm_result = get_confirmation()
if confirm_result.action != ConfirmAction.CONFIRM:
yield Message("system", DECLINED_CONTENT) # 被拒就早退
return

tmux 工具同理(gptme/tools/tmux.py:468),shellexecute_with_confirmation 封装、内部也归到同一套(gptme/tools/shell.py:1668)。

6.2 TOOL_CONFIRM 是"另类钩子":返回值,不是 yield 消息

普通钩子 yieldMessage;TOOL_CONFIRM 不同——它返回一个 ConfirmationResult(gptme/hooks/confirm.py:83),因为确认的语义是"做还是不做/改一改再做",这是一个决策值,不是一段要灌进对话的文本。ToolConfirmHook 的 Protocol 明确写了这点(confirm.py:113-143):

  • (tool_use, preview, workspace);
  • 返回 ConfirmationResult(动作是 CONFIRM / SKIP / EDIT,confirm.py:75);
  • 返回 None = 我不处理,请交给下一个钩子

因为它不走 trigger 的 yield 通道,所以有专门的分发器 get_confirmation(confirm.py:164)。它手动取出所有 TOOL_CONFIRM 钩子,按优先级从高到低逐个试,谁先返回非 None 就用谁的结果,全 None 就用默认(confirm.py:199-250)。若一个钩子都没注册(自主模式),默认放行(confirm.py:202-209)。

6.3 三种确认实现 + 一条"分级放行"链

同一个 TOOL_CONFIRM 时刻,按运行模式挂不同实现;init_hooks(gptme/hooks/__init__.py:114)据 interactive / server / no_confirm 决定挂哪个(__init__.py:231-235):

实现模式优先级行为位置
cli_confirm交互式 CLI0打印预览 + 终端问 y/n/e/c/agptme/hooks/cli_confirm.py:53
server_confirmserver/WebUI100走 SSE 事件等前端点gptme/hooks/server_confirm.py:217
auto_confirm自主/非交互0无脑 confirm()gptme/hooks/auto_confirm.py:19
shell.allowlist随 shell 工具10allowlist 命令直接放行,否则返回 None 下沉gptme/tools/shell.py:1981

关键是**"高优先级的工具级自动批准 + 低优先级的用户确认"能叠起来用**。confirm.py 的 Protocol docstring 点破了这个设计意图(confirm.py:120-135):shell 的 allowlist 钩子挂 priority=10,先被问;allowlist 里的命令它直接 ConfirmationResult.confirm(),不在的返回 None,自然下沉到 priority=0cli_confirm 去弹终端确认。

一次 shell 确认的下沉链(优先级 高 → 低)
────────────────────────────────────────
shell.allowlist(10) ──命中白名单?── 是 ─▶ CONFIRM(直接放行)
│否 → 返回 None(下沉)

cli_confirm(0) ──▶ 终端问 y/n/e/c/a ─▶ CONFIRM / SKIP / EDIT

cli_confirm 还顺手实现了不少体验细节:总是先打印预览(连自动确认模式也打,便于监看 diff)、支持 e 用编辑器改内容后再执行(ConfirmationResult.edit)、c 复制到剪贴板、a [N] 进入自动确认(cli_confirm.py:106-159)。自动确认状态用 ContextVar 存(confirm.py:33-72),所以在 server 并发下每个上下文各算各的次数。

6.4 autocommit:一个"挂在 TURN_POST 上、会被叫停"的护栏

把叫停机制落到实处的好例子:autocommit 工具用 ToolSpec.hooks 把一个钩子挂在 TURN_POST,优先级只有 1(gptme/tools/autocommit.py:161-169):

hooks={
"autocommit": (HookType.TURN_POST.value, autocommit_on_message_complete, 1),
},

注释说明了为什么是 1:让它排在 precommit 检查(优先级 5)之后——如果 precommit 失败并 yield StopPropagation(),autocommit 就被叫停,不会把坏改动提交进去(autocommit.py:165-167)。这正是 §4.2 的 StopPropagation 在真实场景里的用法。


7. 内置钩子一览:护栏与"感知"能力

init_hooks 里的 available_hooks 字典(gptme/hooks/__init__.py:148-209)登记了所有内置钩子的注册入口,默认全挂(确认类除外,按模式挑)。挑几个有代表性的看它们挂在哪个时刻:

钩子挂载时刻干什么位置
cost_awarenessSESSION_START(pri10)/GENERATION_PRE/TURN_POST/SESSION_END(pri-10)预算初始化、生成前注入成本提示、结束打印总花费gptme/hooks/cost_awareness.py:311
cache_awarenessCACHE_INVALIDATED(pri100)/GENERATION_POST(pri100)/TURN_POST缓存失效后更新注意力状态、记录生成耗时gptme/hooks/cache_awareness.py:392
active_contextGENERATION_PRE生成前把"当前活跃上下文"注入(→ 04)gptme/hooks/active_context.py:147
agents_md_injectCWD_CHANGED切换工作目录后注入该目录的 AGENTS.mdgptme/hooks/agents_md_inject.py:230
auto_snapshotsTOOL_EXECUTE_PRE/POST(pri90)变更类工具前后打工作区快照,支持回滚gptme/hooks/auto_snapshots.py:289
injection_screeningTOOL_EXECUTE_POST(pri100)工具输出里检出可疑提示注入就贴警告gptme/hooks/injection_screening.py:124

这张表把一个道理讲透:看起来五花八门的能力,底层都是"选一个 HookType、定一个优先级、写一个回调"。 成本感知靠多点埋(开始初始化 / 生成前注入 / 结束打印),快照靠工具前后成对埋,注入拦截靠"抢在别人前面"的高优先级。没有一个是特例——除了确认那条返回值通道(§6)。

反向征询:ELICIT

大多数钩子是"循环触发、钩子响应"。ELICIT(gptme/hooks/types.py:136)反过来:agent 主动向用户要结构化输入(文本、选择、密码、表单)。入口是 elicit(request)(gptme/hooks/elicitation.py:373),它和 get_confirmation 一个套路——按优先级试 ELICIT 钩子、返回 None 就下沉、都没有就回退到 CLI(TTY 时)或返回取消。CLI 与 server 各有实现(register_cli_elicitation_hook 优先级 0;server_elicit 优先级 100)。密码类请求会标 sensitive,提醒调用方别把值塞进 LLM 上下文。这套是子 agent / 交互式工具"问一句再继续"的基础设施。


8. 插件:把外部的工具/钩子/命令挂进来

护栏是内置钩子,扩展则靠插件——但插件用的是同一套挂载机制。

8.1 两条发现路径,归一到 GptmePlugin

一个插件可以是四种能力的任意组合:LLM provider、工具、钩子、命令。不管从哪发现,最终都归一成一个 GptmePlugin dataclass(gptme/plugins/plugin.py:22):

  • 文件夹插件:从配置路径扫目录,含 __init__.py + tools/ / hooks/ / commands/ 子目录即算一个插件(discover_plugins,gptme/plugins/__init__.py:70;判定 _is_plugin_dir:148)。还支持 src/ 布局的 pip 包。
  • 入口点插件:第三方包在 pyproject.toml 里声明 [project.entry-points."gptme.plugins"],值是一个 GptmePlugin 实例(discover_entrypoint_plugins,gptme/plugins/entrypoints.py:26)。_coerce_to_plugin(entrypoints.py:48)很宽容:导出 GptmePlugin 直接用,导出裸 ToolSpecToolSpec 列表自动包一层,导出零参工厂就调一次——因为很多现存插件习惯直接导出 ToolSpec,这样不至于被静默跳过。

discover_all_plugins(gptme/plugins/registry.py:36)把两条路径合并去重(可编辑安装会同时以文件夹和入口点出现,以文件夹为准),应用 allowlist,再调各插件的 init(config)。结果缓存在 _all_plugins,get_all_plugins()(registry.py:28)供别处取。

发现与归一
──────────
文件夹路径 ─▶ discover_plugins ─┐
├─▶ discover_all_plugins ─▶ 去重/allowlist/init ─▶ GptmePlugin[]
入口点组 ───▶ discover_entrypoint ┘ (registry._all_plugins 缓存)
gptme.plugins

8.2 工具、钩子、命令各自怎么进来

三种能力,三条挂载路:

  • 工具:get_plugin_tool_modules(gptme/plugins/__init__.py:305)返回插件贡献的模块名列表,喂给既有的工具发现系统(→ 02),让里面顶层的 ToolSpec 被扫到。它复用工具管线,不另造轮子。
  • 钩子:init_hooks 收尾时遍历 get_all_plugins(),凡插件带 register_hooks 就调一次(gptme/hooks/__init__.py:248-259)。文件夹插件的 register_hooks 由适配器 _make_hook_registrar(registry.py:131)合成——它 import 每个 hooks/*.py 模块并调其 register(),里面就是普通的 register_hook(...)
  • 命令:对称地,register_commandscommands/*.py/slash 命令登记进命令系统(_make_command_registrar,registry.py:152)。

8.3 ToolSpec 也能自带钩子和命令

不止插件,单个工具就能带钩子和命令。ToolSpec(gptme/tools/base.py:306)有两个字段:hooks(base.py:349)和 commands(base.py:350)。工具加载时调 register_hooks()(base.py:415)把 hooks 里的 (类型字符串, 函数, 优先级) 逐个注册(钩子名前缀成 <工具名>.<钩子名>),register_commands()(base.py:430)登记 slash 命令。

§6.4 的 autocommit 正是这么做的:它是个几乎不"执行"什么的工具,全部价值就在自带的那个挂在 TURN_POST 的钩子和一个 /commit 命令(gptme/tools/autocommit.py:156-173)。shell 的 allowlist 确认钩子也是经 ToolSpec.hooks 挂上去的(shell.py:1981)。这让"一个能力自成一体"成为可能——工具、它的护栏、它的命令打包在同一个 ToolSpec 里。


9. 巧妙之处(可借鉴)

  • 确认是"返回值型钩子 + 优先级下沉链",而非 yield 型。 决策语义配决策通道;工具级自动批准(高优先级)与用户确认(低优先级)靠"返回 None 下沉"天然叠加,不用写 if-else 分派。见 gptme/hooks/confirm.py:199-250
  • 确认跟着执行一起下沉进 ToolUse.execute,靠 _current_tool_use 上下文变量拿参。 循环不必知道"哪些工具要确认",工具自己 get_confirmation() 即可。见 gptme/tools/base.py:715
  • StopPropagation + 优先级 = 声明式的前置校验短路。 precommit(5)失败叫停 autocommit(1),不用任何显式协调。见 gptme/hooks/registry.py:254gptme/tools/autocommit.py:161
  • kwargs 按签名过滤,让核心能给钩子加参数而不炸老插件。 演进友好。见 gptme/hooks/registry.py:47
  • async 钩子各拷一份 copy_context() 后台跑。 遥测/通知这类副作用不阻塞主流程,又能继承会话上下文。见 gptme/hooks/registry.py:213-222
  • 入口点插件的 _coerce_to_plugin 极宽容。ToolSpec、列表、工厂都收,兼容大量"只想导出一个工具"的现存插件。见 gptme/plugins/entrypoints.py:48

10. 边界与局限

  • Protocol 签名不做运行时强校验。 它们服务静态检查;真跑起来靠 _filter_kwargs_for_hook 兜底,签名写错(且没 **kwargs)可能被静默丢参而非报错。
  • async 钩子的产出被丢弃、StopPropagation 无效。 后台线程只记 log(registry.py:323-377),别把"需要回灌消息 / 需要叫停"的逻辑放异步钩子里。
  • TOOL_CONFIRM 钩子内抛异常一律按 SKIP 处理(confirm.py:240-243),这是"出错就别执行"的保守取向,但也意味着确认钩子里的 bug 会表现为"工具莫名被跳过"。
  • 注册表线程本地。 跨上下文(新线程未 copy_context)不共享钩子,server 场景要留意初始化时机。
  • 插件发现有缓存(_plugin_cache / lru_cache),运行时热加载需显式 clear_registry() / clear_entrypoint_cache()

11. 代码地图(导航索引)

主题文件关键符号
所有可挂载时刻gptme/hooks/types.pyHookTypeHookStopPropagation、各 *Hook Protocol
注册/触发/排序gptme/hooks/registry.pyHookRegistryregister_hooktrigger_hook_filter_kwargs_for_hook
确认基础设施gptme/hooks/confirm.pyget_confirmationConfirmationResultConfirmActionToolConfirmHook
三种确认实现gptme/hooks/{cli_confirm,server_confirm,auto_confirm}.pycli_confirm_hookserver_confirm_hookauto_confirm_hook
钩子初始化/挑选gptme/hooks/__init__.pyinit_hooksavailable_hooks
确认下沉点gptme/tools/base.pyToolUse.execute_current_tool_useToolSpec.register_hooks/register_commands
反向征询gptme/hooks/elicitation.pyelicitElicitationRequest
autocommit 护栏gptme/tools/autocommit.pytool(ToolSpec.hooks: TURN_POST pri1)
内置感知钩子gptme/hooks/{cost_awareness,cache_awareness,active_context,agents_md_inject,auto_snapshots,injection_screening}.py各文件 register()
插件归一gptme/plugins/plugin.pyGptmePlugin
插件发现gptme/plugins/{__init__,registry,entrypoints}.pydiscover_pluginsdiscover_all_pluginsget_plugin_tool_modules_coerce_to_plugin

同组其它章: index · 01 主循环 · 02 工具系统 · 03 内置工具 · 04 提示词与上下文 · 06 消息与持久化