Hermes 架构全解Hermes Architecture, in Full第 11 章 · 14 章Chapter 11 of 14
全文目录Contents
  1. 0 · 项目全景与代码地图
    1. 0.1 这个软件是什么
    2. 0.2 目录地图
    3. 0.3 从这张地图能读出的三件事
    4. 0.4 一条消息的完整旅程
    5. 0.5 全文章节索引
  2. 1 · 网关层
    1. 1.1 它解决什么问题
    2. 1.2 平台适配器抽象基类
    3. 1.3 消息事件的归一化
    4. 1.4 网关主循环里的那些防御机制
    5. 1.5 状态消息与进度反馈
    6. 1.6 中断与恢复
    7. 1.7 网关内置钩子
    8. 1.8 这一层的代价
  3. 2 · 身份与会话路由
    1. 2.1 Profile:一个进程承载多个身份
    2. 2.2 四级路由与「具体度」打分
    3. 2.3 显式路由被拒绝的情况
    4. 2.4 SOUL.md:人格文件
    5. 2.5 会话路由:跨平台的连续性
    6. 2.6 智能体实例的缓存
  4. 3 · 主循环 ★
    1. 3.1 循环入口:三重预算闸门
    2. 3.2 每轮的准备工作:build_turn_context
    3. 3.3 中途插话:/steer
    4. 3.4 墙上时钟预算的收尾提醒
    5. 3.5 中断检查
    6. 3.6 步骤回调:给网关的观测点
    7. 3.7 循环里的其他状态
    8. 3.8 循环退出原因
    9. 3.9 一个可选的旁路:Codex 应用服务运行时
  5. 4 · 工具系统
    1. 4.1 最重要的设计:实现与投放分离
    2. 4.2 工具集的组合与解析
    3. 4.3 工具的中心分发
    4. 4.4 参数强制矫正层
    5. 4.5 工具错误消息的净化
    6. 4.6 工具结果的观测与钩子
    7. 4.7 委派上下文识别
    8. 4.8 工具定义的缓存
  6. 5 · 审批与安全红线 ★
    1. 5.1 整体结构
    2. 5.2 12 条硬红线
    3. 5.3 真正的难点:区分「命令」和「数据」
    4. 5.4 引号遮蔽:但要给「真会执行的部分」留后门
    5. 5.5 引号不能成为绕过手段
    6. 5.6 敏感路径与写入目标
    7. 5.7 sudo 标准输入守卫
    8. 5.8 性能:预编译的理由
    9. 5.9 被拦截命令的留存
    10. 5.10 智能审批:可选的模型判断
    11. 5.11 上下文感知:不同场景不同策略
    12. 5.12 这一层的定位:最外面也最薄
  7. 6 · 执行环境
    1. 6.1 七种可选环境
    2. 6.2 必须诚实说明的一件事
    3. 6.3 抽象基类里的共用逻辑
    4. 6.4 文件同步
    5. 6.5 环境的选择时机
  8. 7 · 上下文引擎 ★
    1. 7.1 它定义的是什么
    2. 7.2 生命周期
    3. 7.3 三个必须实现的方法
    4. 7.4 最精辟的设计:select 和 compress 是两个正交动词
    5. 7.5 后置观察钩子
    6. 7.6 其他可选钩子
    7. 7.7 默认参数值
    8. 7.8 用户可见状态的可控性
    9. 7.9 内置实现的体量对比
  9. 8 · 记忆系统 ★
    1. 8.1 记忆提供者接口
    2. 8.2 生命周期与钩子
    3. 8.3 接口的版本化契约
    4. 8.4 琐碎提问过滤器
    5. 8.5 记忆使用指示器
    6. 8.6 内置的全息记忆
    7. 8.7 存储层与信任分
    8. 8.8 SQLite 状态层
    9. 8.9 三种记忆的分工总结
  10. 9 · 插件系统
    1. 9.1 三个发现来源
    2. 9.2 插件能提供什么
    3. 9.3 最重要的设计:区分「可叠加能力」与「互斥策略」
    4. 9.4 插件的存储
    5. 9.5 插件与工具集的联动
    6. 9.6 插件钩子
    7. 9.7 MCP:另一条扩展路径
    8. 9.8 这套扩展体系的整体形状
  11. 10 · 委派与多智能体
    1. 10.1 为什么需要委派
    2. 10.2 深度限制:只允许一层
    3. 10.3 并发限制
    4. 10.4 子智能体的工具限制
    5. 10.5 子智能体的审批策略
    6. 10.6 运行中的控制
    7. 10.7 亲缘关系检查
    8. 10.8 看板:智能体之间的协作
    9. 10.9 委派系统为什么有 5,071 行
  12. 11 · 模型供应商与凭据池
    1. 11.1 为什么不能只支持一家
    2. 11.2 供应商适配器清单
    3. 11.3 凭据池:核心机制
    4. 11.4 PooledCredential:单个凭据的状态
    5. 11.5 「持久化」为什么必要
    6. 11.6 与其他机制的配合
    7. 11.7 供应商抽象要抽象到哪一层
  13. 12 · 定时任务
    1. 12.1 「cron」是什么
    2. 12.2 定时智能体的场景
    3. 12.3 最重要的一个类:CronPromptInjectionBlocked
    4. 12.4 定时任务的工具集收窄
    5. 12.5 失败处理
    6. 12.6 防重复执行
    7. 12.7 为什么 scheduler.py 有 367 KB
    8. 12.8 定时任务在整个架构里的位置
  14. 13 · 技能系统
    1. 13.1 技能是什么
    2. 13.2 前置元数据逐字段解释
    3. 13.3 渐进式披露:技能系统的核心机制
    4. 13.4 技能的支撑设施
    5. 13.5 技能 vs 工具 vs 插件
    6. 13.6 技能系统的一个隐含设计:可组合
    7. 13.7 15 个技能类别
    8. 13.8 全文回顾:Hermes 的整体形状
  1. 0 · The Project at a Glance, and a Map of the Code
    1. 0.1 What This Software Is
    2. 0.2 Directory Map
    3. 0.3 Three Things You Can Read Off This Map
    4. 0.4 The Full Journey of One Message
    5. 0.5 Chapter Index
  2. 1 · The Gateway Layer
    1. 1.1 The Problem It Solves
    2. 1.2 The Platform Adapter Abstract Base Class
    3. 1.3 Normalizing Message Events
    4. 1.4 The Defensive Mechanisms in the Gateway Main Loop
    5. 1.5 Status Messages and Progress Feedback
    6. 1.6 Interruption and Recovery
    7. 1.7 Built-in Gateway Hooks
    8. 1.8 What This Layer Costs
  3. 2 · Identity and Session Routing
    1. 2.1 Profiles: One Process, Many Identities
    2. 2.2 Four-Level Routing and the “Specificity” Score
    3. 2.3 When an Explicit Route Is Rejected
    4. 2.4 SOUL.md: The Persona File
    5. 2.5 Session Routing: Continuity Across Platforms
    6. 2.6 Caching Agent Instances
  4. 3 · The Main Loop ★
    1. 3.1 The Loop Entry: A Triple Budget Gate
    2. 3.2 Per-Turn Preparation: build_turn_context
    3. 3.3 Mid-Turn Interjection: /steer
    4. 3.4 The Wall-Clock Budget Wrap-Up Reminder
    5. 3.5 The Interrupt Check
    6. 3.6 The Step Callback: An Observation Point for the Gateway
    7. 3.7 Other State Inside the Loop
    8. 3.8 Why the Loop Exited
    9. 3.9 An Optional Bypass: The Codex App Server Runtime
  5. 4 · The Tool System
    1. 4.1 The Most Important Design Decision: Implementation Separated from Exposure
    2. 4.2 Composing and Resolving Toolsets
    3. 4.3 Central Tool Dispatch
    4. 4.4 The Argument Coercion Layer
    5. 4.5 Sanitizing Tool Error Messages
    6. 4.6 Observing Tool Results, and Hooks
    7. 4.7 Recognizing a Delegation Context
    8. 4.8 Caching Tool Definitions
  6. 5 · Approval and Safety Red Lines ★
    1. 5.1 The Overall Structure
    2. 5.2 The 12 Hard Red Lines
    3. 5.3 The Real Difficulty: Telling “Command” from “Data”
    4. 5.4 Quote Masking: But Leave a Way In for “the Part That Really Executes”
    5. 5.5 Quoting Is Not a Bypass
    6. 5.6 Sensitive Paths and Write Targets
    7. 5.7 The sudo Stdin Guard
    8. 5.8 Performance: Why Precompile
    9. 5.9 Retaining Blocked Commands
    10. 5.10 Smart Approval: An Optional Model Judgment
    11. 5.11 Context Awareness: Different Situations, Different Policies
    12. 5.12 Where This Layer Sits: Outermost, and Thinnest
  7. 6 · Execution Environments
    1. 6.1 Seven Environments to Choose From
    2. 6.2 One Thing That Has to Be Said Plainly
    3. 6.3 Shared Logic in the Abstract Base Class
    4. 6.4 File Synchronization
    5. 6.5 When the Environment Is Chosen
  8. 7 · The Context Engine ★
    1. 7.1 What It Defines
    2. 7.2 Lifecycle
    3. 7.3 The Three Methods You Must Implement
    4. 7.4 The Sharpest Design Decision: select and compress Are Two Orthogonal Verbs
    5. 7.5 The Post-Turn Observation Hook
    6. 7.6 Other Optional Hooks
    7. 7.7 Default Parameter Values
    8. 7.8 Control Over User-Visible Status
    9. 7.9 Size Comparison of the Built-in Implementation
  9. 8 · The Memory System ★
    1. 8.1 The Memory Provider Interface
    2. 8.2 Lifecycle and Hooks
    3. 8.3 The Interface's Versioned Contract
    4. 8.4 The Trivial-Prompt Filter
    5. 8.5 The Memory-Usage Indicator
    6. 8.6 The Built-in Holographic Memory
    7. 8.7 The Storage Layer and Trust Scores
    8. 8.8 The SQLite State Layer
    9. 8.9 Summing Up the Division of Labor Among Three Kinds of Memory
  10. 9 · The Plugin System
    1. 9.1 Three Discovery Sources
    2. 9.2 What a Plugin Can Provide
    3. 9.3 The Most Important Design Decision: “Stackable Capabilities” vs. “Mutually Exclusive Strategies”
    4. 9.4 Plugin Storage
    5. 9.5 How Plugins Tie into Toolsets
    6. 9.6 Plugin Hooks
    7. 9.7 MCP: The Other Extension Path
    8. 9.8 The Overall Shape of This Extension System
  11. 10 · Delegation and Multi-Agent
    1. 10.1 Why Delegation Is Needed
    2. 10.2 The Depth Limit: One Level Only
    3. 10.3 Concurrency Limits
    4. 10.4 Tool Restrictions on Subagents
    5. 10.5 Approval Policy for Subagents
    6. 10.6 In-Flight Control
    7. 10.7 The Lineage Check
    8. 10.8 Kanban: Collaboration Between Agents
    9. 10.9 Why the Delegation System Is 5,071 Lines
  12. 11 · Model Providers and the Credential Pool
    1. 11.1 Why You Can't Support Just One
    2. 11.2 The Provider Adapter Roster
    3. 11.3 The Credential Pool: The Core Mechanism
    4. 11.4 PooledCredential: The State of a Single Credential
    5. 11.5 Why “Persistent” Is Necessary
    6. 11.6 How It Works with Other Mechanisms
    7. 11.7 How Deep the Provider Abstraction Should Go
  13. 12 · Scheduled Tasks (Cron)
    1. 12.1 What “cron” Is
    2. 12.2 Scenarios for Scheduled Agents
    3. 12.3 The Single Most Important Class: CronPromptInjectionBlocked
    4. 12.4 Narrowing the Toolset for Scheduled Tasks
    5. 12.5 Failure Handling
    6. 12.6 Preventing Duplicate Runs
    7. 12.7 Why scheduler.py Is 367 KB
    8. 12.8 Where Scheduled Tasks Sit in the Overall Architecture
  14. 13 · The Skill System
    1. 13.1 What a Skill Is
    2. 13.2 The Front Matter, Field by Field
    3. 13.3 Progressive Disclosure: The Core Mechanism of the Skill System
    4. 13.4 The Infrastructure Around Skills
    5. 13.5 Skills vs. Tools vs. Plugins
    6. 13.6 An Implicit Design in the Skill System: Composability
    7. 13.7 The 15 Skill Categories
    8. 13.8 Looking Back Across the Book: The Overall Shape of Hermes

11 · 模型供应商与凭据池

agent/credential_pool.py + plugins/model-providers/。这一章讲系统怎么和多家模型供应商打交道,以及一个 API key 用完了怎么办

11.1 为什么不能只支持一家

原因说明
能力差异不同模型擅长的事不一样。写代码、写文案、做视觉理解,各有强弱
成本差异同样一个任务,用最贵的模型和用便宜模型的价差可能有十倍
可用性任何一家都会宕机、限流、改价格、改条款
合规某些企业只能用云厂商托管的版本(数据不出自己的云账户)
地域不同地区可访问的服务不同

11.2 供应商适配器清单

agent/providers/
├── anthropic.py           Anthropic 官方 API
├── bedrock.py             AWS Bedrock(在自己的 AWS 账户里调 Claude)
├── vertex.py              Google Cloud Vertex AI(在 GCP 里调 Claude)
├── azure.py               Azure OpenAI
├── gemini_native.py       Google Gemini 原生接口
└── codex_responses.py     OpenAI Codex 的 responses 接口
注意前三个:同一个模型,三条接入路径

anthropic / bedrock / vertex 背后可能是同一个 Claude 模型,但:

  • 认证方式不同 —— API key vs AWS 签名 vs Google 服务账号
  • 请求格式有差异 —— 字段名、嵌套结构、必填项都不完全一样
  • 错误码不同 —— 同样是「限流」,三家返回的状态码和错误体格式都不同
  • 可用功能不同 —— 提示词缓存、扩展思考这些特性,各平台支持的版本和参数可能落后于官方

所以「支持 Claude」不是一件事,是三件事。每一条路径都要单独实现、单独测试、单独跟进版本变化。

11.3 凭据池:核心机制

agent/credential_pool.py

"""Persistent multi-credential pool for same-provider failover."""

译:「用于同一供应商内部故障转移的持久化多凭据池」。逐词拆开:

含义
multi-credential
多凭据
你有多个 API key(多个账号、多个组织、多个付费计划)
same-provider
同一供应商
这些 key 都是同一家的。不是「Anthropic 挂了切到 OpenAI」,而是「Anthropic 的 key A 限流了切到 key B」
failover
故障转移
一个不行了自动换下一个,对上层透明
persistent
持久化
状态存到磁盘 —— 重启后还记得哪个 key 已经用完了

为什么需要它

凌晨 3 点,定时任务开始跑 第 1 个任务:正常 第 2 个任务:正常 ... 第 47 个任务:API 返回 429 Too Many Requests (本小时的额度用完了) ↓ 没有凭据池:全部剩余任务失败,早上起来看到一堆报错 有凭据池: 自动切到第二个 key,继续跑 ★ 关键:第 48、49、50 个任务不应该再去试第一个 key —— 已经知道它满了,试也是白试,还浪费一次往返

11.4 PooledCredential:单个凭据的状态

class PooledCredential:
    ...
    _exhausted_ttl        # 「耗尽」状态的存活时长
    priority              # 优先级排序

_exhausted_ttl:按 HTTP 状态码决定「冷却多久」

TTL = Time To Live(存活时间)。这里指的是「这个 key 被标记为不可用后,多久之后再试一次」。

关键设计:冷却时长不是固定的,而是根据 API 返回的错误码来决定的。

状态码含义合理的冷却策略
429 限流(Too Many Requests) 短冷却(几十秒到几分钟)。额度是按时间窗口重置的,等一会儿就恢复
401 认证失败(key 无效) 很长冷却,或者直接永久剔除。key 错了,等多久都不会自己变对
402 需要付费(余额不足) 长冷却(小时级)。要人工去充值,短时间重试没意义
403 无权限 长冷却。可能是这个 key 没开通某个模型的权限
5xx 服务端错误 很短冷却。不是 key 的问题,是供应商临时故障

为什么不能用统一的冷却时长?

统一设短(比如 30 秒)→ 一个 401 的错误 key 会每 30 秒被重试一次,永远浪费请求,而且日志里全是噪声。

统一设长(比如 1 小时)→ 一个只是被短暂限流的好 key,白白闲置 1 小时。你花钱买的额度用不上。

「用错误的类型来决定重试策略」是所有重试逻辑的核心。不区分错误类型的重试,要么太急要么太懒,没有中间状态。

priority:优先级排序

凭据不是平等的。典型的排序理由:

  • 成本 —— 有的是包月账户(边际成本 0),有的是按量计费。优先用包月的
  • 额度 —— 有的账户额度高,有的是备用小号
  • 速度 —— 有的账户在更高的服务等级上

所以选凭据的逻辑是:从可用的凭据里,按优先级取第一个。不可用的(在冷却中的)直接跳过。

11.5 「持久化」为什么必要

不持久化的情况: 09:00 key-A 限流了 → 内存里标记为"耗尽,1 小时后重试" 09:05 进程重启(部署 / 崩溃 / 手动重启) 09:05 内存状态清空 → 系统以为 key-A 可用 09:05 用 key-A 发请求 → 又是 429 09:05 再次标记为耗尽 ↓ ★ 每次重启都要重新"撞一次墙"才知道 key 不能用 如果重启频繁(比如开发调试期间), 你会看到大量本可避免的 429 错误, 而且每一次都消耗了一个真实的 API 往返。

持久化的另一个价值:多进程共享。

Hermes 可能同时有:网关进程、定时任务进程、命令行会话进程。如果状态只在内存里,三个进程会各自撞墙三次。写到磁盘(或数据库)后,一个进程发现 key-A 满了,另外两个立刻就知道。

11.6 与其他机制的配合

和第 3 章的预算闸门

预算闸门管的是「这一轮对话花了多少钱」,凭据池管的是「用哪个 key 去花」。两者正交:预算闸门决定「还能不能花」,凭据池决定「从哪个口袋掏」。

和第 3 章的降级

回顾第 3 章:模型返回错误时会尝试降级到备用模型。现在可以看到完整的失败处理链条:

请求失败 ↓ ① 是凭据问题吗(429/401/402/403)? → 是:标记该凭据耗尽,换下一个凭据,重试 ↓ 换完了所有凭据还是不行 ② 是模型问题吗(模型过载 / 上下文超限)? → 是:降级到备用模型(第 3 章) ↓ 备用模型也不行 ③ 是上下文太长吗(413)? → 是:触发压缩,重试(第 7 章) ↓ 压缩后还是不行 ④ 真的失败了 → 把错误告诉用户 / 记录事件 / 触发告警

注意这四层的顺序不能乱:

换凭据是最便宜的(同一个模型、同样的上下文,只是换个身份)。
降级模型有质量代价
压缩上下文有信息损失

所以从代价最小的手段开始尝试。如果反过来,一遇到 429 就先压缩上下文,那就是白白丢了信息 —— 而问题根本不在上下文长度上。

11.7 供应商抽象要抽象到哪一层

这是设计多供应商支持时最容易做错的决策。

抽象层次做法问题
太薄 只统一「发消息」这个动作,其余暴露原始差异 上层代码里到处是 if provider == "bedrock",加一家供应商要改十处
太厚 抽象出一个「最大公约数」接口,只保留所有供应商都支持的功能 丢失特性。提示词缓存、扩展思考、结构化输出这些差异化能力全用不上 —— 而这些恰恰是最有价值的
刚好 统一核心流程(消息、工具调用、流式),把差异化能力做成可查询的能力位 上层写 if provider.supports_prompt_caching() 而不是 if provider == "anthropic"

「能力查询」这个模式在第 1 章的平台适配器里已经出现过一次。

BasePlatformAdaptersupports_threads() / supports_reactions() / supports_editing(),让上层代码问「你能不能做 X」而不是「你是谁」。

这里是完全相同的模式,用在了不同的领域。两个地方都面对「一群做同一件事但能力不同的外部系统」,解法一致:抽象「做什么」,查询「能做什么」,永远不要在业务代码里判断「你是谁」。

这是一个可以直接搬到任何项目里的模式 —— 支付渠道、短信通道、对象存储、推送服务,全都适用。

11 · Model Providers and the Credential Pool

agent/credential_pool.py + plugins/model-providers/. This chapter covers how the system deals with multiple model providers, and what happens when an API key runs dry.

11.1 Why You Can't Support Just One

ReasonExplanation
Capability differencesDifferent models are good at different things. Writing code, writing copy, visual understanding — each has strengths and weaknesses
Cost differencesFor the same task, the price gap between the most expensive model and a cheap one can be tenfold
AvailabilityEvery provider goes down, throttles, changes prices, and changes terms
ComplianceSome enterprises may only use versions hosted by their cloud vendor (data never leaves their own cloud account)
GeographyDifferent regions can reach different services

11.2 The Provider Adapter Roster

agent/providers/
├── anthropic.py           Anthropic's official API
├── bedrock.py             AWS Bedrock (calling Claude inside your own AWS account)
├── vertex.py              Google Cloud Vertex AI (calling Claude inside GCP)
├── azure.py               Azure OpenAI
├── gemini_native.py       Google Gemini's native interface
└── codex_responses.py     OpenAI Codex's responses interface
Note the first three: one model, three access paths

Behind anthropic / bedrock / vertex may sit the very same Claude model, but:

  • Authentication differs — API key vs. AWS signature vs. Google service account
  • Request formats differ — field names, nesting, and required fields don't fully line up
  • Error codes differ — for the same “rate limited,” all three return different status codes and error-body formats
  • Available features differ — features like prompt caching and extended thinking may lag behind the official API in the versions and parameters each platform supports

So “supporting Claude” isn't one job; it's three. Each path has to be implemented separately, tested separately, and tracked separately as versions change.

11.3 The Credential Pool: The Core Mechanism

agent/credential_pool.py

"""Persistent multi-credential pool for same-provider failover."""

In plain terms: “a persistent pool of multiple credentials, for failover within a single provider.” Taking it word by word:

TermMeaning
multi-credential
several credentials
You have multiple API keys (multiple accounts, multiple organizations, multiple billing plans)
same-provider
a single provider
All of these keys belong to the same provider. This isn't “Anthropic is down, switch to OpenAI”; it's “Anthropic key A is rate-limited, switch to key B”
failover
automatic switchover
When one stops working, automatically move to the next, transparently to the layers above
persistent
survives restarts
State is saved to disk — after a restart, it still remembers which key is used up

Why it's needed

3 a.m., the scheduled tasks start running Task 1: fine Task 2: fine ... Task 47: the API returns 429 Too Many Requests (this hour's quota is used up) ↓ Without a credential pool: every remaining task fails; you wake up to a pile of errors With a credential pool: it switches to the second key automatically and keeps going ★ Key point: tasks 48, 49, and 50 should not try the first key again — you already know it's full; trying is pointless and wastes a round-trip

11.4 PooledCredential: The State of a Single Credential

class PooledCredential:
    ...
    _exhausted_ttl        # how long the "exhausted" state lives
    priority              # priority ordering

_exhausted_ttl: “how long to cool down” depends on the HTTP status code

TTL = Time To Live. Here it means “after this key is marked unusable, how long until we try it again.”

The key design decision: the cooldown isn't fixed; it's determined by the error code the API returned.

Status codeMeaningSensible cooldown policy
429 Rate limited (Too Many Requests) Short cooldown (tens of seconds to a few minutes). Quotas reset on a time window; wait a bit and it recovers
401 Authentication failed (invalid key) Very long cooldown, or just remove it permanently. A wrong key won't fix itself no matter how long you wait
402 Payment required (insufficient balance) Long cooldown (hours). A human has to top up the account; retrying soon is pointless
403 Forbidden Long cooldown. This key probably hasn't been granted access to a particular model
5xx Server-side error Very short cooldown. Not the key's fault; the provider is having a transient failure

Why can't you use one uniform cooldown?

Uniformly short (say, 30 seconds) → a bad key returning 401 gets retried every 30 seconds, wasting requests forever, and the logs fill up with noise.

Uniformly long (say, 1 hour) → a perfectly good key that was only briefly throttled sits idle for an hour. Quota you paid for goes unused.

“Let the type of error decide the retry policy” is the heart of all retry logic. Retries that don't distinguish error types are either too eager or too lazy, with no middle ground.

priority: priority ordering

Credentials aren't equal. Typical reasons to rank them:

  • Cost — some are flat-rate subscriptions (marginal cost zero), others are pay-as-you-go. Use the flat-rate ones first
  • Quota — some accounts have high limits; others are small backup accounts
  • Speed — some accounts sit on a higher service tier

So the credential selection logic is: from the available credentials, take the first by priority. Unavailable ones (in cooldown) are skipped outright.

11.5 Why “Persistent” Is Necessary

Without persistence: 09:00 key-A gets rate-limited → marked in memory as "exhausted, retry in 1 hour" 09:05 the process restarts (deploy / crash / manual restart) 09:05 in-memory state is wiped → the system thinks key-A is available 09:05 sends a request with key-A → 429 again 09:05 marks it exhausted again ↓ ★ Every restart has to "hit the wall" once more to learn the key is unusable If restarts are frequent (during development and debugging, say), you will see a flood of avoidable 429 errors, and every one of them costs a real API round-trip.

Persistence has another payoff: sharing across processes.

Hermes may have several processes running at once: the gateway process, the scheduled-task process, and CLI session processes. If the state lives only in memory, three processes each hit the wall three times. Written to disk (or a database), one process discovers key-A is full and the other two know immediately.

11.6 How It Works with Other Mechanisms

With the budget gate from chapter 3

The budget gate governs “how much has this conversation turn cost”; the credential pool governs “which key to spend with.” The two are orthogonal: the budget gate decides “can we still spend,” the credential pool decides “which pocket to pay from.”

With the fallback from chapter 3

Recall chapter 3: when the model returns an error, the system tries to fall back to a backup model. Now the complete failure-handling chain comes into view:

Request fails ↓ ① Is it a credential problem (429/401/402/403)? → Yes: mark that credential exhausted, switch to the next credential, retry ↓ all credentials tried, still failing ② Is it a model problem (model overloaded / context limit exceeded)? → Yes: fall back to the backup model (chapter 3) ↓ the backup model fails too ③ Is the context too long (413)? → Yes: trigger compaction, retry (chapter 7) ↓ still failing after compaction ④ It has really failed → report the error to the user / log the event / fire an alert

Note that the order of these four tiers must not be shuffled:

Switching credentials is the cheapest (same model, same context, just a different identity).
Falling back to another model has a quality cost.
Compacting the context has an information cost.

So you start with the least costly remedy. Done the other way round — compacting the context the moment you see a 429 — you'd throw away information for nothing, when the problem had nothing to do with context length.

11.7 How Deep the Provider Abstraction Should Go

This is the decision most often gotten wrong when designing multi-provider support.

Abstraction levelApproachProblem
Too thin Unify only the act of “sending a message”; expose every other raw difference Upstream code is littered with if provider == "bedrock"; adding one provider means touching ten places
Too thick Abstract a “greatest common denominator” interface, keeping only features every provider supports Lost features. Differentiating capabilities like prompt caching, extended thinking, and structured output go unused — and those are exactly the most valuable ones
Just right Unify the core flow (messages, tool calls, streaming) and turn the differentiating capabilities into queryable capability flags Upstream code writes if provider.supports_prompt_caching() instead of if provider == "anthropic"

The “capability query” pattern already appeared once, in chapter 1's platform adapters.

BasePlatformAdapter has supports_threads() / supports_reactions() / supports_editing(), so upstream code asks “can you do X” rather than “who are you.”

This is exactly the same pattern, applied in a different domain. Both places face “a group of external systems that do the same job but with different capabilities,” and the solution is the same: abstract “what to do,” query “what can you do,” and never test “who are you” in business logic.

This is a pattern you can lift straight into any project — payment channels, SMS gateways, object storage, push notification services, all of it applies.