Documentation

Step 6: Agent 辅助模块

06 — Agent 辅助模块深度分析

分析范围:agent/ 目录下的辅助模块(display, error_classifier, insights, retry_utils, usage_pricing, skill_commands, skill_utils, trajectory, title_generator, subdirectory_hints, google_oauth, google_code_assist, copilot_acp_client, manual_compression_feedback)


1. 概述

模块 行数 职责
display.py 996 CLI 反馈:KawaiiSpinner 动画、工具预览格式化、行内 diff 渲染
error_classifier.py 829 API 错误分类体系,决定恢复策略(重试/轮转/压缩/回退/中止)
insights.py 768 会话指标收集与报告生成(token 用量、成本、模型/平台/工具分布)
retry_utils.py 57 去相关抖动指数退避(jittered exponential backoff)
usage_pricing.py 687 Token 计费追踪:多 Provider 定价表、CanonicalUsage 规范化、成本估算
skill_commands.py 377 技能斜杠命令扫描与消息构建(CLI 和 Gateway 共享)
skill_utils.py 465 技能元数据工具:frontmatter 解析、平台匹配、配置变量发现
trajectory.py 56 轨迹保存(ShareGPT 格式 JSONL)+ scratchpad 转换
title_generator.py 125 异步会话标题生成(使用辅助 LLM)
subdirectory_hints.py 224 渐进式子目录上下文发现(AGENTS.md/CLAUDE.md/.cursorrules)
google_oauth.py 1048 Google OAuth PKCE 完整流程(浏览器+headless 两种模式)
google_code_assist.py 417 Google Code Assist API 客户端(项目发现、入驻、配额查询)
copilot_acp_client.py 586 Copilot ACP 协议客户端(JSON-RPC 子进程桥接)
manual_compression_feedback.py 49 手动压缩命令用户反馈摘要

2. 错误处理体系

2.1 error_classifier.py — 错误分类层次

核心数据结构:

  • FailoverReason (Enum): 14 种错误原因,覆盖认证、计费、速率限制、服务端、传输、上下文溢出、请求格式、模型未找到、Provider 特殊场景
  • ClassifiedError (dataclass): 分类结果 + 恢复提示(retryable, should_compress, should_rotate_credential, should_fallback

分类管线(优先级从高到低):

1
2
3
4
5
6
7
1. Provider 特殊模式(Thinking签名、长上下文分层)
2. HTTP 状态码分类(401/403/402/404/413/429/400/5xx)
3. 错误码分类(resource_exhausted/insufficient_quota/context_length_exceeded 等)
4. 消息模式匹配(计费/限速/上下文溢出/认证/模型未找到)
5. 服务器断连 + 大会话 → 上下文溢出推断
6. 传输层错误推断(超时/连接错误)
7. 兜底 unknown(可重试+退避)

关键设计点:

  • 402 消歧 (_classify_402): 区分永久计费耗尽与周期性配额重置——检查 usage_limit + try_again/resets at 等瞬态信号
  • 400 大会话推断: 400 空消息体 + tokens > context*0.4 或 > 80000 → 推断为上下文溢出,避免格式错误误判
  • OpenRouter 元数据解包: 递归解析 error.metadata.raw JSON 中的嵌套错误消息
  • 传输错误类型表: 涵盖 httpx(ReadTimeout, ConnectError)、OpenAI SDK(APIConnectionError)等

风险点:

  • _TRANSPORT_ERROR_TYPES 使用字符串而非实际类型引用——拼写错误不会被类型检查器捕获
  • 错误消息模式匹配全部是字符串 in 检查,可能出现误报(如 "rate limit" 出现在正常语境中)
  • 没有日志/遥测输出——错误分类决策完全静默

2.2 retry_utils.py — 重试策略

极简模块,仅一个函数:

1
def jittered_backoff(attempt, *, base_delay=5.0, max_delay=120.0, jitter_ratio=0.5) -> float
  • 指数退避: min(base_delay * 2^(attempt-1), max_delay)
  • 去相关抖动: 使用 time.time_ns() ^ (counter * 0x9E3779B9) 作为种子,避免雷群效应
  • 线程安全: 全局 _jitter_counter_jitter_lock 保护
  • 溢出保护: exponent >= 63 时直接返回 max_delay

与 error_classifier 的交互:

` ClassifiedError.retryable 决定是否重试,jittered_backoff() 决定延迟;两者由 run_agent.py` 的主循环组合调用。模块间无直接依赖。

改进建议:

  • jittered_backoff 不接受 ClassifiedError 参数——调用者必须自行从分类结果映射退避参数(如 rate_limit 使用更长的 base_delay
  • 考虑添加 backoff_for_error(error: ClassifiedError) 便捷函数

3. 计费与指标

3.1 usage_pricing.py — Token 计费追踪

数据流:

1
API 响应 → normalize_usage() → CanonicalUsage → estimate_usage_cost() → CostResult

关键组件:

  1. CanonicalUsage (frozen dataclass): 规范化 token 桶——input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, reasoning_tokens。计算属性 prompt_tokenstotal_tokens

  2. normalize_usage(): 处理三种 API 形状:
    • Anthropic: 直接映射 input_tokens / cache_read_input_tokens / cache_creation_input_tokens
    • Codex Responses: input_tokens - cached_tokens - cache_creation_tokens(API 总量包含缓存)
    • OpenAI Chat Completions: prompt_tokens - cached_tokens - cache_write_tokens(同理)
  3. resolve_billing_route(): 根据模型名和 Provider 确定 BillingRoute,包括:
    • openai-codexsubscription_included(订阅包含)
    • openrouter → 动态 API 查询定价
    • anthropic/openai → 本地文档快照定价
    • custom/localhostunknown
  4. 定价数据源优先级:
    • subscription_included → 返回零价
    • OpenRouter → /api/api-reference/models/get-models 动态查询
    • 自定义 base_url → OpenAI 兼容 /models 端点
    • _OFFICIAL_DOCS_PRICING 硬编码快照(15 个模型)
  5. 定价表覆盖范围: Anthropic(4 模型)、OpenAI(6 模型)、DeepSeek(2 模型)、Google Gemini(3 模型)、AWS Bedrock(6 模型)

Decimal 精度: 使用 Decimal 类型进行所有金额计算,避免浮点误差

风险点:

  • 定价快照会随时间过时——无自动更新机制,仅有 pricing_version 标注
  • OpenRouter 动态定价依赖外部 API 可用性
  • normalize_usage 对未知模式使用 OpenAI 形状作为默认——可能导致缓存 token 计算错误

3.2 insights.py — 指标收集与展示

InsightsEngine 核心方法:

方法 功能
generate(days, source) 生成完整洞察报告
_compute_overview() 汇总会话统计(token、成本、持续时间)
_compute_model_breakdown() 按模型分组(token、成本、有无定价)
_compute_platform_breakdown() 按平台分组(CLI、Telegram 等)
_compute_tool_breakdown() 工具使用排名(合并两个数据源)
_compute_activity_patterns() 星期/小时分布、活跃天数、连续天数
_compute_top_sessions() 最长/最多消息/最多 token/最多工具调用的会话

双数据源合并策略_get_tool_usage):

  1. messages 表的 tool_name 列(Gateway 设置)
  2. messages 表的 tool_calls JSON 列(CLI 设置)
  3. 合并:取每个工具的最大计数,避免重复

两套格式化器

  • format_terminal() — CLI 友好的 Unicode 框线表
  • format_gateway() — Markdown 简要版(适用于 Telegram/Discord)

改进建议:

  • SQL 查询大量重复(有/无 source 过滤两套)——可抽象查询模板
  • _compute_top_sessions() 不防止单元素序列的 max() 边界情况
  • 缺少指标缓存——频繁调用会反复查询数据库

4. 显示系统

4.1 KawaiiSpinner 动画机制

架构:

1
2
3
4
5
6
7
8
9
KawaiiSpinner.__init__(message, spinner_type, print_fn)
  → 捕获 sys.stdout 作为 self._out
  → self._print_fn 可选路由(静默代理时替换为 lambda: None)

KawaiiSpinner._animate() [后台线程]
  → 检测 TTY 和 prompt_toolkit StdoutProxy
  → 非 TTY: 单行日志
  → StdoutProxy 下: 静默等待(避免换行叠加)
  → 真实 TTY: \r 覆写动画帧 + 计时器

Skin 集成:

  • get_waiting_faces(), get_thinking_faces(), get_thinking_verbs() — 从活跃皮肤获取自定义文字
  • get_skin_tool_prefix() — 替换默认 前缀
  • get_tool_emoji() — 皮肤覆盖 > 工具注册表 > 默认的优先级链

关键防坑设计:

  • _out__init__ 时捕获——防止子代理 redirect_stdout(devnull) 吞掉输出
  • stop() 使用空格清除而非 \033[K——避免 patch_stdout 下的转义泄漏
  • HERMES_SPINNER_PAUSE 环境变量——可暂停动画帧

4.2 工具预览格式化

build_tool_preview(): 为每个工具提取主要参数值的一行摘要。

  • 维护 primary_args 字典映射工具名→关键参数名
  • 特殊处理:process(多字段组合)、todo(读/更新/规划区分)、memory(增/改/删语法)、session_searchsend_messagerl_* 系列
  • 通用回退:遍历 query/text/command/path/name/prompt/code/goal 寻找首个存在的参数

4.3 行内 Diff 渲染

流程:

1
2
3
4
5
6
7
工具调用前: capture_local_edit_snapshot(tool_name, args)
  → 读取受影响文件的内容快照

工具调用后: render_edit_diff_with_delta(tool_name, result, snapshot, print_fn)
  → extract_edit_diff() 提取或生成 diff
  → _summarize_rendered_diff_sections() 限制文件数(≤6)和行数(≤80)
  → _emit_inline_diff() 通过 print_fn 输出

Skin 感知: diff 配色从 _diff_ansi() 懒加载——首次调用从 skin_engine 读取并缓存

特殊路径解析: skill_managecreate/edit/patch/write_file/remove_file/delete 操作各需不同的路径恢复逻辑


5. 技能系统辅助

5.1 skill_commands.py

核心流程:

  1. scan_skill_commands() — 扫描 ~/.hermes/skills/ + 外部目录,解析 SKILL.md frontmatter
  2. build_skill_invocation_message() — 构建斜杠命令的用户消息:
    • _load_skill_payload() → 调用 skill_view() 加载技能内容
    • _build_skill_message() → 组装: 激活标注 + 内容 + 配置注入 + 设置提示 + 支撑文件列表 + 用户指令
  3. _inject_skill_config() — 从 config.yaml 注入技能声明的配置值

命令名规范化: 空格/下划线→连字符,过滤非字母数字字符(支持 Telegram bot 命令限制)

5.2 skill_utils.py

轻量级工具集(无重型依赖):

  • parse_frontmatter(): YAML frontmatter 解析(CSafeLoader 优先)
  • skill_matches_platform(): 平台兼容性检查
  • get_disabled_skill_names(): 读取 config → skills.disabledskills.platform_disabled.{platform}
  • get_external_skills_dirs(): 从 config 读取 skills.external_dirs
  • extract_skill_conditions(): 提取 metadata.hermes.requires_toolsets 等条件
  • extract_skill_config_vars() / resolve_skill_config_values(): 技能配置变量发现与解析
  • discover_all_skill_config_vars(): 扫描所有技能的配置声明

设计亮点: 使用 SKILL_CONFIG_PREFIX = "skills.config" 命名空间存储,避免与全局配置冲突


6. OAuth 与集成

6.1 google_oauth.py — 安全性分析

OAuth 2.0 PKCE 流程:

1
2
3
4
5
6
start_oauth_flow()
  → _generate_pkce_pair() → (verifier, S256 challenge)
  → 启动本地回调服务器 (_bind_callback_server)
  → 打开浏览器 / 打印 URL
  → 等待回调 → exchange_code()
  → 持久化: save_credentials() (原子写入 + chmod 600)

安全特性:

  1. PKCE S256 — 不传输 client_secret 到授权端点
  2. State 参数 — CSRF 防护
  3. 原子文件写入 — tmp + os.replace() + os.fsync()
  4. 文件权限 0o600 — 仅所有者可读写
  5. 跨进程锁 — fcntl (POSIX) / msvcrt (Windows) 排他锁
  6. 并发刷新去重_refresh_inflight dict + threading.Event

Headless fallback: 检测 SSH/CI 环境变量,自动切换到粘贴模式

风险点:

  • _OAuthCallbackHandler 使用类属性存储状态(expected_state, captured_code)——多并发 OAuth 流程会互相干扰
  • Client ID/Secret 虽然是 Google 公开桌面客户端,但文档明确声明第三方使用违反 Google 政策——代码中有 confirm(default=False) 警告但此模块本身不执行该确认
  • _locate_gemini_cli_oauth_js() 递归搜索文件系统——潜在的性能/安全风险

6.2 google_code_assist.py

三层功能:

  1. load_code_assist() — 发现当前 tier + 已分配项目(prod → sandbox → 回退)
  2. onboard_user() — LRO 长轮询入场(12 次 × 5 秒)
  3. retrieve_user_quota() — 查询剩余配额

VPC-SC 处理: 检测 SECURITY_POLICY_VIOLATED 并回退到 standard-tier

User-Agent 欺骗: 使用 google-api-nodejs-client/9.15.1 (gzip) 模拟 Google 官方 gemini-cli——可能被 Google 检测

6.3 copilot_acp_client.py

架构:子进程 JSON-RPC 桥接

1
2
3
4
5
6
7
8
CopilotACPClient.chat.completions.create(messages, tools, ...)
  → _format_messages_as_prompt() — 将对话转为单个文本 prompt
  → _run_prompt() — 启动 copilot --acp --stdio 子进程
    → JSON-RPC: initialize → session/new → session/prompt
    → 流式监听: session/update (agent_message_chunk / agent_thought_chunk)
    → 处理: session/request_permission, fs/read_text_file, fs/write_text_file
  → _extract_tool_calls_from_text() — 从文本中提取 ◀{...}▶ 格式的工具调用
  → 返回 OpenAI 兼容的 SimpleNamespace 响应

安全特性:

  • _ensure_path_within_cwd() — 强制路径在 CWD 内
  • request_permission 自动回复 allow_once

风险点:

  • 子进程无超时强制终止——仅 terminate() + wait(2s)kill() 回退
  • _format_messages_as_prompt() 将完整对话压缩为单个 prompt——超长会话可能导致截断
  • fs/write_text_file 自动允许——无用户确认
  • 工具调用提取依赖正则表达式(◀{...}▶ 块和裸 JSON),脆弱且易误报

7. 其他辅助模块

7.1 subdirectory_hints.py

核心类 SubdirectoryHintTracker:

  • 维护 _loaded_dirs 已见目录集合(初始包含 CWD)
  • check_tool_call()_extract_directories() → 从 path/file_path/workdir 参数和 terminal 命令中提取路径
  • _add_path_candidate() — 向上遍历最多 5 级父目录
  • _load_hints_for_directory() — 查找 AGENTS.md/agents.md/CLAUDE.md/claude.md/.cursorrules
  • 截断限制 8000 字符,调用 _scan_context_content() 安全扫描

设计约束: 不修改系统提示——保持 prompt 缓存完整性

7.2 trajectory.py

极简模块,两个功能:

  • convert_scratchpad_to_think(): <REASONING_SCRATCHPAD><think> 标签转换
  • save_trajectory(): 追加 JSONL 条目(conversations + model + completed + timestamp

7.3 title_generator.py

  • generate_title(): 使用辅助 LLM(call_llm())生成 3-7 词标题
  • auto_title_session(): 检查现有标题后生成
  • maybe_auto_title(): 仅在前两次用户消息时触发,后台线程执行
  • 输入截断至 500 字符,temperature=0.3

7.4 manual_compression_feedback.py

单函数模块,生成压缩命令反馈的字典(noop, headline, token_line, note)。


8. 代码质量评估

8.1 优点

  1. 清晰的类型标注: ClassifiedError, CanonicalUsage, BillingRoute, PricingEntry, CostResult 等都是 frozen dataclass,不可变且类型安全
  2. Decimal 精度: usage_pricing.py 使用 Decimal 而非 float 进行金额计算
  3. Skin 集成: display.py 懒加载皮肤引擎,避免循环导入
  4. Profile 安全: google_oauth.py 使用 get_hermes_home() 而非硬编码 ~/.hermes
  5. 防御性编程: error_classifier.py 的多层分类管线,_extract_status_code() 遍历原因链
  6. Thread safety: retry_utils.py_jitter_lockgoogle_oauth.py_credentials_lock_refresh_inflight

8.2 问题与风险

模块 问题 严重度
error_classifier.py _TRANSPORT_ERROR_TYPES 用字符串而非类型引用,拼写错误不可检测
error_classifier.py 无日志输出——分类决策对调试不可见
usage_pricing.py 定价快照无自动更新机制,依赖手动同步
usage_pricing.py normalize_usage() 默认 OpenAI 形状——新 Provider 可能产生错误的缓存 token 计算
insights.py SQL 查询大量重复(有/无 source 两套)
display.py _diff_colors_cached 全局缓存不随皮肤切换刷新(仅在进程内首次求值)
skill_commands.py scan_skill_commands() 依赖 tools.skills_tool 的重型导入链——在 Gateway 启动时可能较慢
google_oauth.py _OAuthCallbackHandler 类属性存储状态不支持并发 OAuth
google_oauth.py _locate_gemini_cli_oauth_js() 递归 rglob("oauth2.js") 无深度限制
google_code_assist.py User-Agent 欺骗可能被 Google 检测和阻止
copilot_acp_client.py fs/write_text_file 自动允许写入无确认
copilot_acp_client.py 工具调用提取靠正则表达式,易误报
copilot_acp_client.py 完整对话压缩为单个 prompt——超长会话截断风险

8.3 改进建议

  1. usage_pricing.py: 添加 ProviderShape enum 并在 normalize_usage() 中检测实际响应结构,而非依赖 api_mode 参数预设
  2. error_classifier.py: 将 _TRANSPORT_ERROR_TYPES 改为类型引用集合;添加 classify_api_error()logger.debug() 日志
  3. display.py: 在 set_active_skin() 调用时清除 _diff_colors_cached = None,使 diff 颜色随皮肤切换更新
  4. insights.py: 抽取 SQL 查询为参数化模板,消除重复
  5. copilot_acp_client.py: 为 fs/write_text_file 添加 HERMES_ACP_AUTO_APPROVE 环境变量开关(默认 False)
  6. google_code_assist.py: 将 User-Agent 设置为 Hermes 自身标识而非模拟 gemini-cli,或在文档中记录模拟理由和风险
  7. skill_commands.py: 将 scan_skill_commands()SKILLS_DIR 导入延迟到函数内部(已部分实现),或使用轻量路径构造替代 skills_tool 导入
  8. retry_utils.py: 添加 backoff_for_classified_error(error: ClassifiedError) 便捷函数,根据 FailoverReason 类型选择不同的 base_delay/max_delay