エージェント主ループ
職務
run_conversation は Hermes の中心:ユーザーメッセージを受け取り、イテレーション予算 (iteration budget) が尽きるかタスクが完了するまで、モデル + ツール (tool) を繰り返し呼ぶ。同時にコンテキスト (context) 構築、ツールディスパッチ (tool dispatch)、停止条件判定、失敗時のフォールバック (fallback) を協調させる。
ファイル全長は約 5800 行、ロジック密度が高い——このページでは主線だけを見る。詳細はサブページに分割する。
主要ファイル
run_conversation 入口:588-720— 関数シグネチャ、初期化とループ前準備while 主ループ:721-760— 条件api_call_count < agent.max_iterations and agent.iteration_budget.remaining > 0、grace call 含むAPI 呼び出しリトライ副ループ:1227-1280—while retry_count < max_retriesbuild_turn_context— 毎ラウンドのコンテキスト組み立てprompt_builder— システムプロンプト (system prompt) 組み立てmoa_loop— Mixture-of-Agents 集約パスcontext_compressor— 閾値到達時に履歴を圧縮 (compression)IterationBudget— 予算 (budget) 計数コア
データフロー
run_agent.py AIAgentは薄い転送器、実依存注入はinit_agentで済む。- 呼び出し側が
user_messageを渡すと、run_conversationはまずbuild_turn_contextでこのラウンドのコンテキスト(api_messages)を組み立てる。 while主ループに入る:中断チェック → 予算減算(iteration_budget.consume())→api_messages構築/消毒 → 事前圧縮 →/steerテキスト排出 → MoA 集約 → API 呼び出しリトライ副ループへ。- モデル応答後、レスポンス検証と
finish_reason処理を行う。失敗時はagent._try_activate_fallback()に回る。 - 圧縮閾値に到達したら
context_compressorでスペースを回収。 - ループ退出後(予算尽き/タスク完了/中断)に最終メッセージを返し、ゲートウェイ (gateway) 層が配信する(ゲートウェイ層参照)。
主ループの実際の姿は agent/conversation_loop.py:721-740 にある。
while (api_call_count < agent.max_iterations and agent.iteration_budget.remaining > 0) or agent._budget_grace_call:
agent._checkpoint_mgr.new_turn()
if agent._interrupt_requested:
interrupted = True
_turn_exit_reason = "interrupted_by_user"
break
api_call_count += 1
if agent._budget_grace_call:
agent._budget_grace_call = False
elif not agent.iteration_budget.consume():
_turn_exit_reason = "budget_exhausted"
breakループ条件に外付けされた or agent._budget_grace_call は「予算がゼロになってももう一度だけチャンスをやる」ための支点だ。ラウンドに入った冒頭で即座に flag をクリアするので、「一回のチャンス」は本当に一回だけになる。
API 呼び出しのリトライは再帰ではなく、内側の while retry_count < max_retries だ。以下は agent/conversation_loop.py:1227-1245 の冒頭。
while retry_count < max_retries:
if agent.provider == "nous":
try:
from agent.nous_rate_guard import nous_rate_limit_remaining
if nous_rate_limit_remaining() is not None and nous_rate_limit_remaining() > 0:
if agent._try_activate_fallback():
active_system_prompt = _sync_failover_system_message(
agent, api_messages, active_system_prompt)
retry_count = 0
continue
return {"final_response": "⏳ rate-limited, no fallback.",
"messages": messages, "completed": False, "failed": True}
except Exception:
pass # Never let rate guard break the agent loopprovider を切り替えた後 retry_count = 0 でクリーンな状態からリトライする。最外層の except Exception: pass は意図的だ——ガードは最適化であり、逆に主ループを止めるべきでない。
設計動機
なぜリトライを再帰ではなく副ループで作るのか? Python では再帰はコールスタックを食う(深さ制限がある)うえ、一度の失敗の状態が複数のスタックフレームに散らばると一括クリーンアップが難しい。while retry_count < max_retries にすれば、すべてのリトライ状態(retry_count、compression_attempts)は同じスタックフレームのローカル変数になり、fallback 切り替え時に retry_count = 0 一行で全体をリセットできる。
なぜ grace call があるのか? 予算はハード上限だが、モデルが「あと一两個のツール呼び出しで終わる」ところで止まることがある。そこで硬く止めると、済んだ作業を捨てることになる。_budget_grace_call は内側が「もう少しで終わる」と判断したときに能動的に立ち、外側の or がそのラウンドを走らせる。ただし flag はループに入って即座にクリアされる——「一回のチャンス」は本当に一回だけだ。
なぜ単なるカウンタではなく iteration_budget オブジェクトにするのか? 予算はスレッドをまたいでアクセスされる(サブ agent、圧縮の refund)ので、IterationBudget は consume/refund をロックで包む。スレッド安全を保ちつつ、圧縮器が古いメッセージを除去した後に refund() でイテレーション枠を返せるようにする。単なる整数ではこの二つはできない。
境界と失敗
- 予算途中で尽きる:退出前に
_persist_session(messages, conversation_history)が現在の会話をディスクに書き出す。_turn_exit_reasonは"budget_exhausted"となり、上层が正常終了か打ち切りかを判定できる。 - ツール呼び出し例外:ツール側のエラーは内側のリトライで捕まり、
max_retriesまで試した後にagent._try_activate_fallback()に回る。fallback チェーンが空の場合、例外を外に投げるのではなくfailed: Trueの結果辞書を返す——上层のゲートウェイが見るのは構造化された失敗だけだ。 - レートガード自身のエラー:
nous_rate_guardの import 失敗や呼び出し例外はexcept Exceptionで拾われ、ガードは「効かない」状態に降格する。主ループは元のパスを続け、コメントも率直に "Never let rate guard break the agent loop" と書かれている。 - OAuth プールの無限リフレッシュ:
_auth_pool_refresh_countsは turn 冒頭でリセットされ、単一エントリの OAuth プールが連続 401 でtry_refresh_current()によって無限に「成功」するのを防ぐ(#26080の安全網)。
まとめ
主ループは編成だけを担う:ラウンド駆動、予算管理、リトライとフォールバック。真の能力は機能層のツールとスキルから来て、マルチプラットフォーム配信はゲートウェイ層に、自己進化は学習ループが担う。
公式の中文紹介を対照したい場合は、README.zh-CN と website/i18n/zh-Hans/ を参照。