本文基于当前仓库实现梳理 Hermes Agent 从入口层到结果交付的真实调用链,重点覆盖:
- 核心运行系统
AIAgent - Prompt Builder / Prompt Caching / Context Compression
- Provider / Runtime Resolution
- Tool Dispatch
- Session Storage
- Response Delivery(CLI / gateway / cron)
不展开单个 tool 的内部业务逻辑,重点描述"谁调用谁、数据如何流动、哪些状态被缓存或持久化"。
入口层
CLI: cli.py Gateway: gateway/run.py Cron: cron/scheduler.py
↓
运行时解析 hermes_cli/runtime_provider.py hermes_cli/auth.py
↓
核心 Agent 初始化 run_agent.py -> AIAgent.init()
↓
系统提示词装配 AIAgent._build_system_prompt() agent/prompt_builder.py
↓
同步 agent loop AIAgent.run_conversation()
↓
模型响应二分 无 tool_calls -> 最终响应 有 tool_calls -> tool dispatch -> tool result 回填 -> 下一轮
↓
上下文管理 agent/prompt_caching.py agent/context_compressor.py
↓
持久化 hermes_state.py / gateway/session.py
↓
结果交付 CLI Rich/TUI Gateway adapter.send(…) Cron output file + auto delivery
2.1 CLI 路径
入口主要在 cli.py:
HermesCLI.chat()先调用_ensure_runtime_credentials()。_ensure_runtime_credentials()通过resolve_runtime_provider()解析 provider、api_mode、base_url、api_key。_init_agent()在首次使用或路由变化时创建AIAgent(…)。HermesCLI.chat()调用self.agent.run_conversation(…)。- 返回结果后,CLI 负责把 reasoning / response / TTS / streaming 内容显示到终端。
关键点:
- CLI 会在 provider、model、route、credential 变化时重建 agent,而不是复用旧 client。
- CLI 恢复会话时,会先从
SessionDB取历史消息,再交给AIAgent继续运行。
相关位置:
cli.py:2169_ensure_runtime_credentials()cli.py:2269_init_agent()cli.py:6309HermesCLI.chat()
2.2 Gateway 路径
入口主要在 gateway/run.py:
- 收到平台消息后,
SessionStore.get_or_create_session(source)建立/获取会话。 build_session_context(…)和build_session_context_prompt(…)生成平台上下文。session_store.load_transcript(session_id)读取历史对话。_run_agent(…)内部解析 runtime、构造或复用AIAgent。- 调用
agent.run_conversation(message, conversation_history=agent_history, …)。 - 将新消息写回 transcript / SQLite,并把最终结果返回给 adapter。
关键点:
- Gateway 不把 session context 写进持久化的 system prompt,而是作为
ephemeral_system_prompt每轮临时注入,避免破坏 prompt cache 前缀。 - Gateway 会按
session_key + agent config signature缓存AIAgent,尽量复用同一个 agent 实例,保持 system prompt 和 tool schema 稳定。
相关位置:
gateway/run.py:2308获取/创建 sessiongateway/run.py:2342构造 context promptgateway/run.py:2435加载 transcriptgateway/run.py:6647创建/复用AIAgentgateway/run.py:6856调用run_conversation()
2.3 Cron 路径
入口主要在 cron/scheduler.py 的 run_job(job):
- 构造 cron 专用 prompt(包含 skill 展开、cron 执行提示)。
resolve_runtime_provider()解析运行时 provider。- 创建
AIAgent(…, platform="cron", disabled_toolsets=["cronjob", "messaging", "clarify"], skip_memory=True)。 - 在线程池里执行
agent.run_conversation(prompt)。 - 生成 markdown output 文档并保存到
~/.hermes/cron/output/…。 - 如配置了
deliver,调用_deliver_result()发送到目标平台。
关键点:
- cron 是 headless 执行,不允许 clarify。
- cron 同样接入
SessionDB,因此历史可被session_search检索。 - cron 有"无活动超时"机制,不是简单 wall-clock timeout。
相关位置:
cron/scheduler.py:512run_job()cron/scheduler.py:623runtime 解析cron/scheduler.py:643创建AIAgentcron/scheduler.py:678调用run_conversation()cron/scheduler.py:199_deliver_result()
运行时解析由 hermes_cli/runtime_provider.py 统一封装,CLI / gateway / cron 都走这里。
3.1 入口函数
统一入口:
resolve_runtime_provider(...)inhermes_cli/runtime_provider.py:566
它返回一个 runtime dict,典型字段包括:
providerapi_modebase_urlapi_keycommand/args(外部进程型 provider)credential_poolrequested_provider
3.2 解析步骤
resolve_runtime_provider() 的主流程:
resolve_requested_provider()先决定"用户想要哪个 provider"。_resolve_named_custom_runtime()先尝试命名 custom provider。resolve_provider()inhermes_cli/auth.py解析最终 provider 名称。_resolve_explicit_runtime()处理显式 api_key/base_url 覆盖。- 如有 credential pool,优先从 pool 取 runtime credential。
- 按 provider 类型进入对应分支:
nous->resolve_nous_runtime_credentials()openai-codex->resolve_codex_runtime_credentials()copilot-acp->resolve_external_process_provider_credentials()anthropic-> 走 Anthropic token 解析- API-key provider ->
resolve_api_key_provider_credentials() - 其他 ->
_resolve_openrouter_runtime()
3.3 auth.py 的职责
hermes_cli/auth.py 主要负责:
resolve_provider():决定 provider 名称resolve_codex_runtime_credentials():读取/刷新 Codex tokenresolve_nous_runtime_credentials():刷新 Nous portal access token,并确保短期 inference key 可用resolve_api_key_provider_credentials():读取 API-key 类 provider 的密钥和 base_urlresolve_external_process_provider_credentials():解析copilot-acp这类本地子进程 provider
关键位置:
hermes_cli/auth.py:790resolve_provider()hermes_cli/auth.py:1174resolve_codex_runtime_credentials()hermes_cli/auth.py:1683resolve_nous_runtime_credentials()hermes_cli/auth.py:2084resolve_api_key_provider_credentials()hermes_cli/auth.py:2122resolve_external_process_provider_credentials()
3.4 运行时解析的输出如何进入 AIAgent
三个入口都会把 runtime dict 展开为 AIAgent(...) 参数:
api_keybase_urlproviderapi_modecommand/argscredential_pool
随后 AIAgent.__init__() 决定使用哪类 client:
anthropic_messages-> Anthropic native clientcodex_responses-> Responses API / raw codex path- 其他 -> OpenAI-compatible client
核心类位于 run_agent.py:
AIAgent定义:run_agent.py:470__init__():run_agent.py:487run_conversation():run_agent.py:6801chat():run_agent.py:9170
4.1 初始化时完成的事情
AIAgent.__init__() 不是只做 client 构造,它会一次性初始化整套运行时状态:
- 保存 model / provider / api_mode / iteration budget。
- 基于 provider/base_url 判断实际 API 模式。
- 创建底层 LLM client。
- 调用
get_tool_definitions(...)加载当前会话可用工具。 - 初始化
SessionDB会话行(如果传入了session_db)。 - 初始化 todo store、memory store、memory provider plugin。
- 初始化 context compressor。
- 计算 prompt caching 开关。
- 建立
_cached_system_prompt缓存槽。 - 建立 session log 文件路径和
_session_messages缓冲区。
4.2 初始化时和本题流程直接相关的状态
self.tools- 来自
model_tools.get_tool_definitions()
- 来自
self.valid_tool_names- 当前会话真实可调用的工具名集合
self._cached_system_prompt- 会话级稳定 system prompt 快照
self.context_compressor- 长会话压缩器
self._session_db- SQLite 会话存储
self._memory_store/self._memory_manager- memory 注入和外部 memory provider
self._use_prompt_caching- 是否启用 Anthropic/OpenRouter prompt cache 标记
AIAgent._build_system_prompt() 是总装函数,位于:
run_agent.py:2609
真正的提示词拼装细节在:
agent/prompt_builder.py
5.1 组装顺序
_build_system_prompt() 的拼接顺序非常明确:
- Agent identity
- 优先
SOUL.md - 否则
DEFAULT_AGENT_IDENTITY
- 优先
- tool-aware guidance
- 仅当对应 tool 存在时才注入 memory/session_search/skills guidance
- Nous subscription prompt
- tool-use enforcement
- 按模型名和 config 决定是否注入
- 调用方传入的
system_message - 内建 memory / user profile
- 外部 memory provider prompt block
- skills system prompt
- project context files
- 冻结时间戳 / session id / model / provider
- platform hint
5.2 prompt_builder.py 提供的核心能力
5.2.1 上下文文件发现与注入
build_context_files_prompt() 负责扫描并注入项目上下文文件:
.hermes.md/HERMES.mdAGENTS.mdCLAUDE.md.cursorrules/.cursor/rules/*.mdcSOUL.md
关键特性:
- 有 prompt injection 扫描:
_scan_context_content() - 有截断:
_truncate_content() - 有优先级:只取一种 project context 类型
关键位置:
agent/prompt_builder.py:55_scan_context_content()agent/prompt_builder.py:807load_soul_md()agent/prompt_builder.py:920build_context_files_prompt()
5.2.2 Skills 注入
build_skills_system_prompt() 会构建"可用技能索引",供模型在系统提示词里看到。
它有两层缓存:
- 进程内 LRU cache
- 磁盘 snapshot:
.skills_prompt_snapshot.json
注意:
- 这是"skills 索引构建缓存",不是模型 provider 的 prefix cache。
- 它的目标是减少每次扫描
skills/目录的成本。
关键位置:
agent/prompt_builder.py:505build_skills_system_prompt()
5.3 会话级 system prompt 稳定策略
Hermes 为了维持 prefix cache 命中,不会每轮都重建 system prompt。
策略是:
- 首轮调用时构建并缓存到
self._cached_system_prompt - 持续会话时,如果有
SessionDB.system_prompt,优先复用库里的快照 - 只有在 context compression 后才调用
_invalidate_system_prompt()并重建
相关位置:
run_agent.py:6953首轮构建/复用 system promptrun_agent.py:2929_invalidate_system_prompt()
这一层涉及三种"缓存",需要区分:
6.1 会话级 system prompt 缓存
由 AIAgent._cached_system_prompt 管理。
作用:
- 保证同一 session 内 system prompt 文本稳定
- 避免 memory/context/skills 每轮重建后发生微小漂移
6.2 Skills prompt 构建缓存
由 agent/prompt_builder.py 内部管理。
作用:
- 加速 skills 索引生成
- 不直接参与 provider prefix cache
6.3 Provider 级 prompt cache
真正的 provider prefix cache 逻辑在:
agent/prompt_caching.py
入口函数:
apply_anthropic_cache_control():agent/prompt_caching.py:41
策略:
- 最多放 4 个
cache_control断点 - system prompt 1 个
- 最近 3 条非 system message 3 个
在 run_conversation() 里,API 消息构造完成后,如果当前 session 启用了 prompt caching,则调用:
run_agent.py:7239-7244
这就是当前仓库里"保持 prefix cache"的实际执行点。
AIAgent.run_conversation() 是核心同步循环,整体是:
准备消息
↓ 拼 API payload ↓ 调用模型 ↓ 有 tool_calls ? ├─ 否 -> 最终响应 -> 持久化 -> 返回 └─ 是 -> 执行 tools -> tool result 写回 messages -> 下一轮
7.1 进入循环前的准备
主要步骤:
- 恢复 primary runtime(如果上轮触发了 fallback)。
- 清洗用户输入中的 surrogate 字符。
- 拷贝
conversation_history到本轮messages。 - 去掉旧 budget warning。
- 从历史中恢复 todo store。
- 追加本轮 user message。
- 复用或构建
_cached_system_prompt。 - 进行 preflight compression。
- 从 memory provider / plugin hook 获取额外上下文。
相关位置:
run_agent.py:6833-6938run_agent.py:6953-6991run_agent.py:6994-7050run_agent.py:7064-7085
7.2 单轮 API 调用前,如何拼 payload
每次循环会把内部 messages 转成 api_messages:
- 对当前 user message 注入 external memory / plugin context
- assistant message 里的
reasoning映射成 provider 能理解的字段 - 移除内部字段:
reasoning、finish_reason、_thinking_prefill - 对 strict provider 清理 Codex 专用 tool fields
- 前置 system prompt
- 注入
prefill_messages - 应用 Anthropic/OpenRouter prompt cache 标记
- 做 tool_call/tool_result 完整性修复
- 调用
_build_api_kwargs()产出 provider-specific kwargs
关键位置:
run_agent.py:7169-7250run_agent.py:5256_build_api_kwargs()
7.3 _build_api_kwargs() 如何分发到不同 API 形态
_build_api_kwargs() 按 self.api_mode 分三路:
anthropic_messages- 走 Anthropic adapter
codex_responses- system prompt 变成
instructions - history 变成 Responses
input - tools 转成 Responses schema
- 带
parallel_tool_calls=True
- system prompt 变成
chat_completions- 标准 OpenAI-compatible
messages + tools - 对 GPT-5 / Codex 会把第一条
systemrole 改写成developer
- 标准 OpenAI-compatible
关键位置:
run_agent.py:5256-5330run_agent.py:5367-5379
7.4 模型返回后,先做什么
模型返回后会进入一系列规范化/防御逻辑:
- 处理 incomplete / empty / reasoning-only 情况
- 修正 hallucinated tool name
- 校验 tool args JSON
- 去重 tool calls
- 限制同轮
delegate_task数量
然后才决定进入:
- tool 分支
- final response 分支
相关位置:
run_agent.py:8590-8775run_agent.py:8845-8977
7.5 chat() 只是外层薄封装
AIAgent.chat() 只是:
- 调
run_conversation() - 取出
result["final_response"]
位置:
run_agent.py:9170
8.1 tool schema 收集
model_tools.py 在 import 时就会执行 _discover_tools():
- import
tools/*.py - 每个 tool 模块通过
registry.register(…)自注册 - 之后构建
TOOL_TO_TOOLSET_MAP和TOOLSET_REQUIREMENTS
位置:
model_tools.py:132_discover_tools()model_tools.py:170调用_discover_tools()
8.2 get_tool_definitions() 如何做 availability gating
get_tool_definitions(…) 负责给模型暴露"当前会话真实可见的 tools"。
步骤:
- 根据
enabled_toolsets/disabled_toolsets解析 tool 名集合 - 调
registry.get_definitions(…) - 用每个 tool 的
check_fn做可用性过滤 - 对
execute_code动态重建 schema,只列出当前 sandbox 中真实可用的 tools - 对
browser_navigate清理失效的 cross-tool 描述 - 把最终可用 tool 名写入
_last_resolved_tool_names
位置:
model_tools.py:234get_tool_definitions()
8.3 模型发出 tool_calls 后的调用链
在 run_conversation() 中,如果 assistant_message.tool_calls 非空:
- 先校验 tool name 和 JSON 参数
- 生成 assistant message 并附加到
messages - 调
_execute_tool_calls(…)
位置:
run_agent.py:8590-8775run_agent.py:5948_execute_tool_calls()
8.4 并行与串行执行策略
_execute_tool_calls() 会根据 _should_parallelize_tool_batch() 决定走:
_execute_tool_calls_sequential()_execute_tool_calls_concurrent()
并行条件大致是:
- 多个 tool call
- 不包含
clarify - 只包含并行安全工具
- 或 path-scoped tool 且目标路径不冲突
位置:
run_agent.py:262_should_parallelize_tool_batch()run_agent.py:5948_execute_tool_calls()run_agent.py:6045_execute_tool_calls_concurrent()run_agent.py:6261_execute_tool_calls_sequential()
8.5 agent-level tool 与 registry-level tool 的分界
AIAgent._invoke_tool() 先拦截 agent 级工具:
todosession_searchmemoryclarifydelegate_task- memory provider tools
其他工具才走:
handle_function_call(…)inmodel_tools.py
位置:
run_agent.py:5971_invoke_tool()model_tools.py:459handle_function_call()
8.6 handle_function_call() 做什么
handle_function_call() 是 registry 的同步分发入口:
coerce_tool_args()按 JSON Schema 纠正参数类型- 对 read/search loop 做计数重置
- 执行 plugin
pre_tool_callhook registry.dispatch(…)- 执行 plugin
post_tool_callhook - 返回 JSON string
注意:
todo/memory/session_search/delegate_task在这里会返回"必须由 agent loop 处理"的 stub,因为它们依赖 agent-level state。
位置:
model_tools.py:372coerce_tool_args()model_tools.py:459handle_function_call()
核心类:
agent/context_compressor.py:53ContextCompressor
9.1 初始化
AIAgent.init() 会创建 self.context_compressor,传入:
- model
- provider
- base_url
- api_key
- context_length override
- threshold / protect_last_n / summary_model 等 config
位置:
run_agent.py:1186
9.2 压缩触发点
当前实现里至少有三类触发:
- Preflight compression
- 本轮 API 还没发出,粗估请求已经超过阈值
- 正常循环中的 post-tool compression
- 本轮 tool 执行后,根据真实 token 使用量判断
- 上下文错误恢复
- provider 返回 payload too large / context length exceeded 等错误时,进入压缩重试路径
本题主链里最核心的是前两种。
位置:
run_agent.py:6994preflight compressionrun_agent.py:8827post-tool compression
9.3 _compress_context() 的动作
_compress_context() 不是只改 messages,它还会处理 session 边界。
主流程:
flush_memories()先给模型一次机会把值得保留的信息写入 memory- 调用
ContextCompressor.compress(messages, current_tokens=…) - 追加 todo snapshot
_invalidate_system_prompt()- 重建 system prompt
- 若启用
SessionDB:end_session(old_session_id, "compression")- 创建新
session_id create_session(parent_session_id=old_session_id)- 继承并自动编号 title
update_system_prompt(new_session_id, new_system_prompt)
- 更新压缩后的 token 估算
- 重置 file read dedup cache
位置:
run_agent.py:5853_compress_context()
9.4 ContextCompressor.compress() 的内部算法
真实算法不是"简单保留头尾 + 做摘要",而是:
- 先裁剪老旧 tool result
- 保护头部消息
- 按 token budget 保护尾部消息
- 对中间消息做结构化摘要
- 修复 tool_call / tool_result 配对完整性
摘要模板包含:
- Goal
- Constraints & Preferences
- Progress
- Key Decisions
- Relevant Files
- Next Steps
- Critical Context
位置:
agent/context_compressor.py:155_prune_old_tool_results()agent/context_compressor.py:253_generate_summary()agent/context_compressor.py:565compress()
这里有两套相关但不重复的状态层。
10.1 SessionDB:结构化、可检索的长期存储
实现位于:
hermes_state.py
核心设计:
- SQLite
- WAL mode
sessions表 +messages表messages_ftsFTS5 虚表- 支持 parent_session_id 链接 compression continuation
关键位置:
hermes_state.py:41sessions表hermes_state.py:71messages表hermes_state.py:93FTS5 定义hermes_state.py:115SessionDB
主要接口:
create_session()end_session()update_system_prompt()append_message()get_messages_as_conversation()search_messages()
10.2 AIAgent 如何写入 SessionDB
每次 turn 结束时调用:
_persist_session()
它会做两件事:
_save_session_log()写 JSON session snapshot_flush_messages_to_session_db()把未刷新的消息增量写入 SQLite
_flush_messages_to_session_db() 用 _last_flushed_db_idx 避免重复写入。
位置:
run_agent.py:1869_persist_session()run_agent.py:1882_flush_messages_to_session_db()
10.3 SessionStore:gateway 的会话路由与 transcript 兼容层
实现位于:
gateway/session.py
它负责:
- 根据
SessionSource生成session_key - 管理 session reset policy
- 维护
SessionEntry - 管理 transcript(SQLite + JSONL)
- 生成 gateway 用的 session context prompt
关键位置:
gateway/session.py:202build_session_context_prompt()gateway/session.py:692get_or_create_session()gateway/session.py:942append_to_transcript()gateway/session.py:970rewrite_transcript()gateway/session.py:1002load_transcript()
10.4 SessionDB 与 SessionStore 的关系
可以把两者理解成:
SessionDB- 面向 agent / search / 压缩 continuation
- 是结构化 SQLite 正式存储
SessionStore- 面向 gateway 平台 session 生命周期
- 维护 session_key 与当前 session_id 的映射
- 同时保留 JSONL transcript 兼容层
也就是说:
AIAgent直接写SessionDBgateway通过SessionStore管理"当前聊天应该落在哪个 session_id 上"
11.1 CLI display
CLI 的交付不是简单 print(final_response),而是一套 callback + streaming UI:
_init_agent()给AIAgent注入:tool_progress_callbacktool_start_callbacktool_complete_callbackstream_delta_callbackthinking_callbackreasoning_callback
HermesCLI.chat()在线程中执行agent.run_conversation(…)- 运行过程中:
- streaming token 通过
_stream_delta(…)渲染 - reasoning 可单独显示
- tool progress 可实时显示
- streaming token 通过
- 结束后:
- 若没走 streaming,则用 Rich
Panel(…)包裹最终响应 - 可选 TTS 播放
- 若没走 streaming,则用 Rich
相关位置:
cli.py:2340-2378注入 callbackscli.py:6481调用run_conversation()cli.py:6621-6644reasoning displaycli.py:6645-6678最终 response panel
11.2 Gateway delivery
Gateway 的交付链分两段:
第一段:_run_agent() 负责生成 agent 结果
它会:
- 构造/复用
AIAgent - 绑定
tool_progress_callback/step_callback/stream_delta_callback/status_callback - 调
run_conversation() - 处理 transcript 持久化
- 更新
SessionStore.last_prompt_tokens - 返回最终
response
关键位置:
gateway/run.py:6647-6683gateway/run.py:6856gateway/run.py:2996-3078
第二段:平台 adapter 负责真正发送
Gateway 外层收到 _run_agent() 的返回值后,会:
- 在普通模式下调用
adapter.send(…) - 在 streaming 模式下由 stream consumer 边生成边发送
- 如响应里包含
MEDIA:或本地文件路径,调用_deliver_media_from_response(…) - 如需要自动语音回复,走
_send_voice_reply(…)
关键位置:
gateway/run.py:7273queued message 场景下的adapter.send(…)gateway/run.py:3080-3098already streamed 场景下的后处理gateway/run.py:4287_send_voice_reply()gateway/run.py:4345_deliver_media_from_response()
11.3 Cron delivery
cron 的结果交付分三步:
run_job()返回(success, output_doc, final_response, error)save_job_output()先把 markdown 文档落盘- 如果需要对外发送,
_deliver_result(job, content, adapters, loop)再做平台投递
_deliver_result() 细节:
- 先
_resolve_delivery_target(job) - 优先使用 gateway live adapter(支持 E2EE 等)
- 失败时 fallback 到 standalone
_send_to_platform(…) - 先抽取
MEDIA:标签,附件作为原生文件发送
位置:
cron/scheduler.py:199_deliver_result()cron/scheduler.py:856-873tick 内触发保存与投递
13.1 system prompt 必须尽量稳定
这直接关系到:
- Anthropic/OpenRouter prefix cache
- 多轮会话成本
- gateway 复用 cached agent 的收益
因此:
- gateway 的动态上下文放到
ephemeral_system_prompt - plugin context 放到 user message
- 只有 compression 后才主动重建 system prompt
13.2 tool_call / tool_result 配对必须完整
Hermes 在两个地方都做了防御:
- API 调用前
_sanitize_api_messages() - compression 后
ContextCompressor._sanitize_tool_pairs()
否则 Anthropic / OpenAI / strict provider 都可能拒绝请求。
13.3 session persistence 不是"最后一次统一保存",而是多层增量保存
当前实现同时维护:
- JSON session snapshot
- SQLite messages/session rows
- gateway transcript JSONL 兼容层
目标是:即使中途中断、压缩、provider 异常,也尽量不丢会话。
Hermes 的核心运行系统可以概括为:
入口层先通过
runtime_provider + auth解析可执行 runtime,再由AIAgent初始化稳定的 tool surface、system prompt 和 context compressor;随后run_conversation()以同步 loop 反复执行"模型调用 -> tool dispatch -> tool result 回填",并在整个过程中用SessionDB/SessionStore持久化状态,最后由 CLI、gateway 或 cron 各自的 display/delivery 层把结果送到用户。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/267078.html