全文目录Contents
- 0 · The Project at a Glance, and a Map of the Code
- 1 · The Gateway Layer
- 2 · Identity and Session Routing
- 3 · The Main Loop ★
- 3.1 The Loop Entry: A Triple Budget Gate
- 3.2 Per-Turn Preparation: build_turn_context
- 3.3 Mid-Turn Interjection: /steer
- 3.4 The Wall-Clock Budget Wrap-Up Reminder
- 3.5 The Interrupt Check
- 3.6 The Step Callback: An Observation Point for the Gateway
- 3.7 Other State Inside the Loop
- 3.8 Why the Loop Exited
- 3.9 An Optional Bypass: The Codex App Server Runtime
- 4 · The Tool System
- 4.1 The Most Important Design Decision: Implementation Separated from Exposure
- 4.2 Composing and Resolving Toolsets
- 4.3 Central Tool Dispatch
- 4.4 The Argument Coercion Layer
- 4.5 Sanitizing Tool Error Messages
- 4.6 Observing Tool Results, and Hooks
- 4.7 Recognizing a Delegation Context
- 4.8 Caching Tool Definitions
- 5 · Approval and Safety Red Lines ★
- 5.1 The Overall Structure
- 5.2 The 12 Hard Red Lines
- 5.3 The Real Difficulty: Telling “Command” from “Data”
- 5.4 Quote Masking: But Leave a Way In for “the Part That Really Executes”
- 5.5 Quoting Is Not a Bypass
- 5.6 Sensitive Paths and Write Targets
- 5.7 The sudo Stdin Guard
- 5.8 Performance: Why Precompile
- 5.9 Retaining Blocked Commands
- 5.10 Smart Approval: An Optional Model Judgment
- 5.11 Context Awareness: Different Situations, Different Policies
- 5.12 Where This Layer Sits: Outermost, and Thinnest
- 6 · Execution Environments
- 7 · The Context Engine ★
- 7.1 What It Defines
- 7.2 Lifecycle
- 7.3 The Three Methods You Must Implement
- 7.4 The Sharpest Design Decision: select and compress Are Two Orthogonal Verbs
- 7.5 The Post-Turn Observation Hook
- 7.6 Other Optional Hooks
- 7.7 Default Parameter Values
- 7.8 Control Over User-Visible Status
- 7.9 Size Comparison of the Built-in Implementation
- 8 · The Memory System ★
- 8.1 The Memory Provider Interface
- 8.2 Lifecycle and Hooks
- 8.3 The Interface's Versioned Contract
- 8.4 The Trivial-Prompt Filter
- 8.5 The Memory-Usage Indicator
- 8.6 The Built-in Holographic Memory
- 8.7 The Storage Layer and Trust Scores
- 8.8 The SQLite State Layer
- 8.9 Summing Up the Division of Labor Among Three Kinds of Memory
- 9 · The Plugin System
- 9.1 Three Discovery Sources
- 9.2 What a Plugin Can Provide
- 9.3 The Most Important Design Decision: “Stackable Capabilities” vs. “Mutually Exclusive Strategies”
- 9.4 Plugin Storage
- 9.5 How Plugins Tie into Toolsets
- 9.6 Plugin Hooks
- 9.7 MCP: The Other Extension Path
- 9.8 The Overall Shape of This Extension System
- 10 · Delegation and Multi-Agent
- 11 · Model Providers and the Credential Pool
- 12 · Scheduled Tasks (Cron)
- 12.1 What “cron” Is
- 12.2 Scenarios for Scheduled Agents
- 12.3 The Single Most Important Class: CronPromptInjectionBlocked
- 12.4 Narrowing the Toolset for Scheduled Tasks
- 12.5 Failure Handling
- 12.6 Preventing Duplicate Runs
- 12.7 Why scheduler.py Is 367 KB
- 12.8 Where Scheduled Tasks Sit in the Overall Architecture
- 13 · The Skill System
- 13.1 What a Skill Is
- 13.2 The Front Matter, Field by Field
- 13.3 Progressive Disclosure: The Core Mechanism of the Skill System
- 13.4 The Infrastructure Around Skills
- 13.5 Skills vs. Tools vs. Plugins
- 13.6 An Implicit Design in the Skill System: Composability
- 13.7 The 15 Skill Categories
- 13.8 Looking Back Across the Book: The Overall Shape of Hermes
9 · 插件系统
plugins/,351 个文件。这一章讲第三方怎么在不改核心代码的前提下扩展系统。
9.1 三个发现来源
~/.hermes/plugins/ 用户级 —— 对这台机器上的所有项目生效
./.hermes/plugins/ 项目级 —— 跟着代码仓库走,团队共享
pip entry points 包级 —— 用 pip install 安装某个包就自动生效
| 来源 | 适用场景 | 特点 |
|---|---|---|
| 用户级 | 「我个人习惯用的工具」 | 不进版本控制,不影响别人 |
| 项目级 | 「这个项目需要的能力」 | 提交进仓库,团队共享。新同事拉下代码就有 |
| 包级 | 「发布给社区用的插件」 | 走标准的 Python 包分发渠道,可以有版本、依赖、更新 |
「pip entry points」(入口点)是 Python 的标准机制:一个包可以在自己的元数据里声明「我提供了某类插件」,安装后框架自动发现,不需要用户手动注册。
9.2 插件能提供什么
plugins/
├── platforms/ 22 个聊天平台适配器 → 第 1 章
├── memory/ 8 种记忆后端 → 第 8 章
├── context_engine/ 上下文引擎 → 第 7 章
├── model-providers/ 模型供应商 → 第 11 章
├── cron_providers/ 定时任务提供者 → 第 12 章
├── kanban/ 看板协作 → 第 10 章
├── browser/ 浏览器自动化
├── image_gen/ 图像生成
├── video_gen/ 视频生成
├── observability/ 可观测性
├── dashboard_auth/ 仪表盘认证
├── security-guidance/ 安全指引
├── google_meet/ 会议集成
├── spotify/ 音乐
├── teams_pipeline/ Teams 流水线
├── disk-cleanup/ 磁盘清理
├── hermes-achievements/ 成就系统
├── web/ 网页相关
├── plugin_storage.py ★ 插件的持久化存储
└── plugin_utils.py ★ 插件工具函数
插件通过一套「上下文 API」向系统注册三类东西:工具、钩子、命令行子命令。
9.3 最重要的设计:区分「可叠加能力」与「互斥策略」
| 可叠加能力 | 互斥策略 | |
|---|---|---|
| 例子 | 工具插件、平台适配器、图像生成后端 | 记忆提供者、上下文引擎 |
| 装 3 个会怎样 | 有 3 份能力,互不冲突。装得越多能力越强 | 系统不知道该听谁的 |
| 系统的处理 | 全部加载 | 「单选」—— 只允许激活一个 |
两处源码明确了这个约束:
// 上下文引擎(第 7 章)
「Selection is config-driven: `context.engine` in config.yaml.
Default is "compressor" (the built-in). Only one engine is active.」
// 记忆提供者(第 8 章)
「The MemoryManager enforces a one-external-provider limit to prevent
tool schema bloat and conflicting memory backends.
Only one external provider runs at a time.」
如果不做这个区分会怎样
在你自己的插件系统里,这个区分要在设计阶段就做出来。
判据很简单:「装两个的语义是『两份能力』还是『两个互相矛盾的答案』?」
如果是后者,就必须标记为单选,并且在加载第二个时明确报错。留到运行时会产生极难排查的问题 —— 因为症状是「行为和预期不一样」,而不是「报错了」。
9.4 插件的存储
plugins/plugin_storage.py
插件需要持久化自己的数据(配置、缓存、状态)。系统提供统一的存储抽象,而不是让每个插件自己决定往哪写。
这解决三个问题:
- 路径统一 —— 备份、清理、迁移都有单一入口(呼应第 8 章的
backup_paths()) - 隔离 —— 插件之间互相看不到对方的数据
- 清理 —— 卸载插件时能完整清理它的数据
9.5 插件与工具集的联动
回顾第 4 章的工具集解析函数:
def _get_plugin_toolset_names() -> Set[str] # 插件提供的工具集
def _get_registry_toolset_aliases() -> Dict[str, str]
def resolve_toolset(name, visited=None, *, include_registry: bool = True)
插件不只是「注册几个工具」,它可以注册一整个工具集。这样用户在配置里写 toolsets: [my_plugin_set] 就能启用插件的全部能力,而不用逐个列工具名。
那个 include_registry 参数说明:系统区分「内置工具集」和「注册表里的工具集(含插件的)」,某些场景下可以只解析内置的 —— 大概是为了在插件还没加载完时也能工作,或者为了安全场景下排除第三方工具。
9.6 插件钩子
插件可以挂钩到系统的几个关键点:
| 钩子 | 时机与用途 |
|---|---|
pre_llm_call | 调模型前。注意它的约束:只能「追加到用户消息」,从不重写消息列表 —— 这是为了保护提示词缓存的前缀(第 7 章的 select_context() 才可以替换列表) |
post_tool_call | 工具执行后。可以观察、记录、告警 |
agent:step | 智能体每走一步(第 3.6 节的步骤回调) |
| 审批钩子 | 第 5 章的 _fire_approval_hook,让插件参与安全决策 |
| 网关钩子 | gateway/hooks.py + gateway/builtin_hooks/,消息进出的节点 |
pre_llm_call 与 select_context 的权限差别
第 7 章的原文:「Unlike the pre_llm_call plugin hook (which appends to the user message and intentionally never rewrites the list, to preserve the cache prefix), select_context() may replace the message list.」
译:不同于 pre_llm_call 插件钩子(它只追加到用户消息,并且刻意从不重写列表,以保护缓存前缀),select_context() 可以替换整个消息列表。
这是一个分级授权的设计:
· 普通插件(pre_llm_call)→ 只能追加,权限小,不会破坏缓存
· 上下文引擎(select_context)→ 可以整个替换,权限大 —— 但它是「单选」的,用户明确选择了它,而且它的输出仍要过所有校验器
权限的大小和「用户是否明确授权」成正比。一个可以随便装十个的普通插件,不该有替换整个上下文的权力。
9.7 MCP:另一条扩展路径
除了插件,Hermes 还支持 MCP(Model Context Protocol,模型上下文协议)—— 一个让智能体接入外部工具服务的开放标准。
tools/mcp_tool.py 378 KB MCP 客户端
mcp_serve.py 38 KB ★ 把 Hermes 自己作为 MCP 服务暴露
optional-mcps/ 65 个文件 内置的可选 MCP 服务
| 插件 | MCP | |
|---|---|---|
| 语言 | 必须是 Python | 任何语言(跨进程通信) |
| 进程 | 同进程 | 独立进程或远程服务 |
| 能力 | 深 —— 可以挂钩子、注册工具集、替换核心策略 | 浅 —— 主要是提供工具和资源 |
| 崩溃影响 | 可能影响主进程 | 隔离,不影响 |
| 生态 | Hermes 专属 | 跨智能体产品通用 |
mcp_serve.py 那一项值得注意:Hermes 可以把自己作为 MCP 服务暴露出去。也就是说另一个智能体可以把 Hermes 当成一个工具来调用 —— 这让「智能体调用智能体」成为可能。
9.8 这套扩展体系的整体形状
这个梯度是有意义的:扩展的门槛和它能造成的破坏成正比。
写一个技能只需要写 Markdown,任何人都能做,最多让智能体多知道一些操作步骤。
写一个上下文引擎需要理解整套契约(缓存不变式、生命周期、版本兼容),而它一旦出错会让整个系统的上下文管理失效。
系统通过「不同层级用不同机制」把这个梯度显式化了 —— 而不是提供一个万能的插件接口让所有人都能做所有事。
9 · The Plugin System
plugins/, 351 files. This chapter covers how third parties extend the system without touching core code.
9.1 Three Discovery Sources
~/.hermes/plugins/ user-level — applies to every project on this machine
./.hermes/plugins/ project-level — travels with the repo, shared by the team
pip entry points package-level — pip install a package and it takes effect automatically
| Source | Where it fits | Characteristics |
|---|---|---|
| User-level | “Tools I personally like to use” | Not under version control; affects nobody else |
| Project-level | “Capabilities this project needs” | Committed to the repo, shared by the team. A new teammate pulls the code and has them |
| Package-level | “Plugins published for the community” | Goes through the standard Python packaging channel, with versions, dependencies, and updates |
“pip entry points” are a standard Python mechanism: a package declares in its own metadata “I provide this kind of plugin,” and after installation the framework discovers it automatically, with no manual registration by the user.
9.2 What a Plugin Can Provide
plugins/
├── platforms/ 22 chat platform adapters → chapter 1
├── memory/ 8 memory backends → chapter 8
├── context_engine/ context engines → chapter 7
├── model-providers/ model providers → chapter 11
├── cron_providers/ scheduled-task providers → chapter 12
├── kanban/ kanban collaboration → chapter 10
├── browser/ browser automation
├── image_gen/ image generation
├── video_gen/ video generation
├── observability/ observability
├── dashboard_auth/ dashboard authentication
├── security-guidance/ security guidance
├── google_meet/ meeting integration
├── spotify/ music
├── teams_pipeline/ Teams pipeline
├── disk-cleanup/ disk cleanup
├── hermes-achievements/ achievement system
├── web/ web-related
├── plugin_storage.py ★ persistent storage for plugins
└── plugin_utils.py ★ plugin utility functions
Through a “context API,” a plugin registers three kinds of things with the system: tools, hooks, and CLI subcommands.
9.3 The Most Important Design Decision: “Stackable Capabilities” vs. “Mutually Exclusive Strategies”
| Stackable capability | Mutually exclusive strategy | |
|---|---|---|
| Examples | Tool plugins, platform adapters, image-generation backends | Memory providers, context engines |
| What happens if you install 3 | You have 3 capabilities that don't conflict. The more you install, the more you can do | The system doesn't know which one to listen to |
| How the system handles it | Loads them all | “Single-select” — only one may be active |
Two places in the source make this constraint explicit:
// Context engine (chapter 7)
“Selection is config-driven: `context.engine` in config.yaml.
Default is "compressor" (the built-in). Only one engine is active.”
// Memory provider (chapter 8)
“The MemoryManager enforces a one-external-provider limit to prevent
tool schema bloat and conflicting memory backends.
Only one external provider runs at a time.”
What happens if you don't make this distinction
In your own plugin system, make this distinction at the design stage.
The test is simple: “If two are installed, does that mean ‘two capabilities’ or ‘two contradictory answers’?”
If it's the latter, you must mark it single-select and fail explicitly when a second one is loaded. Leaving it to runtime produces problems that are extremely hard to track down — because the symptom is “behavior differs from expectations,” not “an error was thrown.”
9.4 Plugin Storage
plugins/plugin_storage.py
Plugins need to persist their own data (configuration, caches, state). The system provides a unified storage abstraction rather than letting each plugin decide where to write.
This solves three problems:
- Unified paths — backup, cleanup, and migration all have a single entry point (echoing
backup_paths()from chapter 8) - Isolation — plugins can't see each other's data
- Cleanup — uninstalling a plugin can remove all of its data cleanly
9.5 How Plugins Tie into Toolsets
Recall the toolset resolution functions from chapter 4:
def _get_plugin_toolset_names() -> Set[str] # toolsets provided by plugins
def _get_registry_toolset_aliases() -> Dict[str, str]
def resolve_toolset(name, visited=None, *, include_registry: bool = True)
A plugin doesn't just “register a few tools”; it can register an entire toolset. That way a user can write toolsets: [my_plugin_set] in their config to enable everything the plugin offers, without listing tool names one by one.
That include_registry parameter tells you something: the system distinguishes “built-in toolsets” from “toolsets in the registry (including plugins')”, and in some scenarios it can resolve only the built-in ones — presumably so things still work before plugins have finished loading, or to exclude third-party tools in security-sensitive contexts.
9.6 Plugin Hooks
Plugins can hook into several key points in the system:
| Hook | When and what for |
|---|---|
pre_llm_call | Before calling the model. Note its constraint: it may only “append to the user message,” never rewrite the message list — this protects the prompt-cache prefix (only select_context() from chapter 7 may replace the list) |
post_tool_call | After a tool runs. Can observe, log, or alert |
agent:step | Every step the agent takes (the step callback from section 3.6) |
| Approval hooks | _fire_approval_hook from chapter 5, letting plugins take part in safety decisions |
| Gateway hooks | gateway/hooks.py + gateway/builtin_hooks/, the points where messages enter and leave |
The permission gap between pre_llm_call and select_context
From chapter 7's source: “Unlike the pre_llm_call plugin hook (which appends to the user message and intentionally never rewrites the list, to preserve the cache prefix), select_context() may replace the message list.”
In plain terms: unlike the pre_llm_call plugin hook (which only appends to the user message and deliberately never rewrites the list, to preserve the cache prefix), select_context() may replace the entire message list.
This is tiered authorization by design:
· Ordinary plugins (pre_llm_call) → append only; small privilege; can't break the cache
· Context engines (select_context) → may replace the whole thing; large privilege — but they are “single-select,” the user explicitly chose them, and their output still passes through every validator
The size of the privilege scales with “did the user explicitly authorize it.” An ordinary plugin that you can casually install ten of should never have the power to replace the entire context.
9.7 MCP: The Other Extension Path
Besides plugins, Hermes also supports MCP (Model Context Protocol) — an open standard for connecting agents to external tool services.
tools/mcp_tool.py 378 KB MCP client
mcp_serve.py 38 KB ★ exposes Hermes itself as an MCP server
optional-mcps/ 65 files built-in optional MCP servers
| Plugin | MCP | |
|---|---|---|
| Language | Must be Python | Any language (cross-process communication) |
| Process | Same process | Separate process or remote service |
| Reach | Deep — can attach hooks, register toolsets, replace core strategies | Shallow — mainly provides tools and resources |
| Blast radius of a crash | Can take down the main process | Isolated; no effect |
| Ecosystem | Hermes-specific | Shared across agent products |
The mcp_serve.py entry deserves attention: Hermes can expose itself as an MCP server. In other words, another agent can call Hermes as a tool — which makes “agents calling agents” possible.
9.8 The Overall Shape of This Extension System
This gradient means something: the barrier to entry for an extension scales with the damage it can do.
Writing a skill just means writing Markdown; anyone can do it, and at most it teaches the agent a few more procedures.
Writing a context engine requires understanding the whole contract (the cache invariant, the lifecycle, version compatibility), and once it goes wrong, context management for the entire system breaks down.
The system makes this gradient explicit by “using different mechanisms at different tiers” — rather than offering one all-purpose plugin interface that lets everyone do everything.