サイト内検索

Hermes Agent v2026.9.7 の Computer Use 内部実装を読む — capture の宛先固定と background-first の verify → escalate

重岡 正 · Mon, September 7, 2026

先日、macOS と Windows 両対応の Computer Use エージェント選定メモHermes Agent の Computer Use を macOS で有効化した記録 の 2 本を書きました。前者は候補比較、後者は実機セットアップの記録でしたが、実際にエージェントに電卓を背面で操作させる様子を眺めているうちに、「モデルが computer_use を 1 回呼ぶだけで、内部では何がどう動いているのか」を確かめたくなりました。この記事はその続きにあたる、Hermes Agent の Git tag v2026.9.7(commit 2237be35、製品版表記 v0.21.1)のコードリーディングノートです。

対象は Hermes 側の実装だけに絞りました。trycua/cua 側の native runtime(cua-driver)は公式ドキュメントの記述までを参照して、ここでは Rust ソース全体は audit していません。以降のコードリンクは v2026.9.7 の該当行を指しています。

結論を先に書くと

読んでみて分かったのは、Hermes 自身は macOS の Accessibility API・Windows の UI Automation・Linux の AT-SPI を直接叩いていない、という事実でした。Hermes の責務は、モデルに 1 つの computer_use ツールを見せて、抽象的な操作要求を検査・セッション分離・結果整形したうえで、外部プログラム cua-driverMCP サーバーへ渡すことです。OS 固有のウィンドウ列挙・アクセシビリティツリー・画面キャプチャ・入力配信はすべて cua-driver 側にあります。

つまり Hermes の Computer Use は「VLM が画面を見て座標を当て続けるだけ」の実装ではなく、AX・UIA・AT-SPI の要素参照、ウィンドウ単位の target binding、画像認識、座標操作を組み合わせたハイブリッド型で、Hermes 側はその制御ループとモデルへの変換層を担当している、と表現するのが正確です。

コードを追って印象に残った設計上の分かれ道は次の 5 点でした。

  • モデル非依存の単一 tool schema。clickcapture ごとに tool を増やさず、action discriminator を持つ 1 つの JSON Schema にまとめる
  • capture が画像取得だけでなく sticky target binding を兼ねる。以後の clicktypekey は原則としてその (pid, window_id) へ送られる
  • background-first と verify → escalate。背景操作を先に試し、effectverified から必要な一段だけ上げる
  • MCP timeout や transport drop では書き込み系操作を自動再送しない。読み取り系だけ安全に再実行する
  • 画像は multimodal tool result・auxiliary vision・text-only の 3 経路で使い分けて、context cost を抑える

以下、それぞれをコードに当てながら書きます。

全体像

処理経路は 2 段に分けて描くと見通しがよくなります。まず Hermes プロセス内で computer_use の呼び出しがどう流れるか、続いて backend から先の transport と native runtime です。

Hermes 内の経路はこうです。

flowchart TB
    U[ユーザー] --> L[LLM / Hermes turn loop]
    L -->|computer_use JSON| T[tool executor]
    T --> R[model_tools / tools.registry]
    R --> H[handle_computer_use]
    H --> S[hard block / approval / session lock]
    S --> B[CuaDriverBackend]
    B --> V[ActionResult / CaptureResult / verdict]
    V --> X[text または multimodal tool result]
    X --> L

CuaDriverBackend から先の transport と native runtime はこうです。

flowchart TB
    B[CuaDriverBackend] <--> A[asyncio bridge thread]
    A <--> M[MCP ClientSession over stdio]
    M <--> P[cua-driver mcp proxy]
    P <--> D[cua-driver native runtime]
    D <--> O[AX / UIA / AT-SPI / capture / input]

責務を分けると次のようになります。

主な責務
モデル向け契約action schema、推奨手順、安全注意(tools/computer_use/schema.py
Hermes turn looptool call の永続化、実行、tool result 追加、次の API 呼び出し(agent/turn_tool_round.pyagent/tool_executor.py
Computer Use wrapperhard block、承認、セッション分離、target guard、response shaping(tools/computer_use/tool.py
Backend adaptersticky target、capture / input の型変換、driver capability 追従(tools/computer_use/cua_backend*.py
Transportasyncio thread、MCP stdio、再接続、CLI fallback(cua_backend_session.py
Native runtimeOS ごとの capture、AX・UIA・AT-SPI、イベント配信、effect 検証(外部の cua-driver

大事なのは、Hermes のモデルが cua-driver の多数の MCP tool を直接見るわけではない、という点です。Hermes は外部 surface を 1 つの computer_use に折り畳んで、内部では接続先 driver の tools/list から capability と live input schema を読み、古い driver に存在しないオプションを送らないようにしています。モデル向けの schema と system prompt を安定させて、prompt cache を壊しにくくするための構造です。該当箇所は schema.pycua_backend_session.py にあります。

モデルへ公開されるまで

tools/computer_use_tool.py は薄い discovery shim で、registry に次の 5 点を登録しています。

  • model-facing name: computer_use
  • toolset: computer_use
  • schema: COMPUTER_USE_SCHEMA
  • handler: handle_computer_use
  • availability check: check_computer_use_requirements

computer_use は core tool bundle に含まれますが、coding posture では外れます。check_fn は OS と driver binary を確認するので、対応外 OS や driver 未導入のときは定義自体がモデルへ出ません。registry の availability check には 30 秒 TTL と last-good grace があり、実行ファイルの解決までを検査します。macOS の TCC・MCP handshake・画面列挙の健全性は含まれないので、schema が見えても初回 backend.start() 時に詳細エラーになることがあります。起動を軽く保ちつつ、実際に使うセッションだけ native runtime を立ち上げる lazy startup、と読むと納得できます。

schema は capture・各種 click・drag・scroll・type・key・set_value・wait・list・focus を 1 つの action enum にまとめていて、capture の mode は somvisionax の 3 種類です。入力は element を座標より優先し、background を既定にして必要時だけ delivery_mode="foreground" を指定する形になっています。

短い運用規約は schema に置き、詳しい capture → act → verify の手順は built-in skill に置く、という分担も見えます。専用の system prompt block は追加していません(agent/prompt_builder.py にコメントあり)。会話中の system prompt と tool surface を安定させて prompt cache を壊しにくくする設計と整合しています。

turn loop の耐久性も面白い点でした。モデルが tool call を返すと、Hermes は副作用を実行する前に assistant の tool-call message を session DB へ flush します。永続化に失敗した場合はツールを実行しません。実行後も tool result を追加・flush してから次の API call へ進みます。PC 操作中に Hermes 自身が再起動・終了しても「実行したのに履歴には無い」という状態を減らすための、一般的な turn-loop invariant です。agent/tool_executor.py から model_tools.handle_function_call() へは Hermes の session_id が渡され、registry が登録済み handler を引いて最終的に handle_computer_use(args, session_id=...) が同期呼び出しされます。

1 回の computer_use を追う

たとえばモデルが次を返したとします。

{
  "action": "click",
  "element": 12,
  "capture_after": true
}

内部ではおおむね次の順で処理されます。

  1. handle_computer_use が action を正規化する
  2. 危険な key combo や shell 文字列の hard block を承認より前に評価する
  3. 変更系 action なら Hermes 側 approval scope を評価する(foreground 化は background 操作とは別 scope)
  4. session_id に対応する backend を取得し、そのセッション専用 lock を取る
  5. click handler が現在の sticky target と element index を CuaDriverBackend.click() へ渡す
  6. backend は (pid, window_id) と、対応 driver なら snapshot の element_token を付ける
  7. MCP の click または double_click を呼ぶ
  8. cua-driver の structured response を ActionResult へ正規化する
  9. ok だけでなく effectverifiedescalation から Hermes の verdict を生成する
  10. capture_after=true かつ transport-level ok なら、同じ (pid, window_id) を再 capture し、操作結果と証拠画像を 1 つの tool result にまとめる
  11. vision capability に応じて、画像付き結果または文章結果として会話へ追加する
  12. LLM が verdict と新しい画面を見て、終了・再 capture・一段階 escalation のいずれかを選ぶ

capture_after が失敗 action で走らないのは重要です。失敗直後の「一見普通のスクリーンショット」が成功証拠と誤解されるのを避ける意図が、tool.py のコメントに明記されています。

capture は観測と宛先確定を兼ねる

コードを読んで一番印象に残ったのがここでした。capture は画像を返すだけの副作用のない observation ではなく、「以降 clicktype が送られる相手」を確定させる binding 操作でもあります。

capture は 3 つの mode を持ちます。

mode主な出力向いている場面
som画像 + numbered elements通常の操作。番号で element を選ぶ
vision主に window screenshotcustom canvas、AX が弱い UI、見た目の確認
axelement tree のテキスト画像を使わず意味構造で操作したい場合

app="screen" のような full-screen sentinel は特別扱いで、window 列挙を迂回して composited desktop image を取ります。ただし full-screen capture は pixels only で element を持たないので、操作したい場合は app・window を指定した interactive lane に戻る必要があります。app="desktop" は desktop shell の window と icon elements を対象にするので、別物です。

通常の capture は list_windows を呼び、明示された pid / window_id、アプリ名、frontmost window などから対象を選びます。見つからなければ現在の foreground を代替対象にせず、失敗を返します。localized app name の不一致などを、誤入力で隠さないためです。選ばれた target は _active_pid_active_window_id に保存されて sticky target になります。

ここで面白いのが、入力 action の app= パラメータの扱いです。これは本来の targeting parameter ではなく、現在の sticky target と矛盾していないかを検査する guard として使われています。明確に異なる場合は input_target_mismatch を返して、先に capture(app=...)focus_app を要求します。したがって「前の capture ではメモ帳を見ていたのに、次の type で model が app="Calculator" と書いたから電卓へ自動で切り替わる」という挙動はしません。対象切替は別の観測・選択操作として明示させる、という規律になっています。

element には index・role・label・bounds に加えて、内部的には driver の element_token が対応付けられます。次の click 等で driver が capability を advertise していれば token も送って、古い snapshot の index が別 element に再解決されるのを避けています。transport reconnect 時には target / token cache 自体を破棄する作りです。pure coordinate automation より堅牢な仕組みで、skill 側も「状態が変わったら再 capture」を基本原則にしています。

background-first と verify → escalate

前回の macOS セットアップ記事では、電卓の背景操作を見て「Hermes は必ず background で動く」と読める書き方になっていましたが、実際は「background-first」であって「background-only」ではありませんでした。cua-driver 公式も best-effort background / No-foreground contract と説明しており、AX・UIA・AT-SPI の semantic action、targeted input、window capture を使える範囲では cursor / focus を維持し、できない surface では structured refusal か foreground escalation を返します。

Hermes の ladder はコード上こう読めます。

  1. element を使った background action
  2. effect=confirmed / verified=true なら終了
  3. effect=unverifiable なら再送せず fresh capture で確認
  4. suspected_noop やエラーで recommended="px" なら、同じ window の座標操作へ一段だけ上げる
  5. recommended="foreground" または pixel path も失敗なら、その一操作のみ foreground delivery を明示する
  6. 永続的な focus change が必要なときだけ、別 action の bring_to_front を使う

ここで大事なのは ActionResult.ok が transport-level success に過ぎない、という切り分けです。実際に UI が変わったかどうかは effectverified が担っています。Hermes が最終 response に verdict.decision を付けるのは、モデルが「RPC 200 = 操作成功」と短絡しないようにするためで、tool.py の verdict 分類 を読むと、その意図がコメントに残っています。

foreground を要求したのに古い driver schema が delivery_mode を受けない場合は、Hermes は background へ切り替えず foreground_unsupported で拒否します。要求と違う宛先へ入力が届くほうが危険だからです。

MCP timeout と at-most-once

Computer Use を安全に運用するうえで、この設計は特に効いていると感じました。

Hermes の tool handler は同期 API ですが、Python の MCP SDK は async です。そこで _AsyncBridge が専用 daemon thread に asyncio event loop を立て、同期側は asyncio.run_coroutine_threadsafe() で call を投げます。MCP の stdio context と ClientSession は 1 つの長寿命 coroutine が open / close して、anyio の cancel-scope invariant を守っています。

問題は、MCP timeout が起きたときです。driver が処理を終えたのに応答だけ失われた可能性がある。ここで自動再送してしまうと、二重クリック・二重入力・同じ文字列の二重送信という、単なる API retry より実害の大きい事故が起きます。

Hermes はセッションを suspect として次回前に再作成しますが、当該 action は再送しません。transport failure 時に CLI fallback や reconnect 後再送が許されるのは、list_windowsget_window_state のような idempotent read tool に限定されていて、変更系は timeout_outcome_unknown / transport_outcome_unknown を返します。at-most-once 寄りの割り切りです。

read だけを replay 対象にする分離は、CLI fallback にも一貫していました。重い get_window_state が stdio MCP で EAGAIN 等を起こす場合、read operation は cua-driver call へフォールバックできます。スクリーンショットは一時ファイルへ出して、巨大な base64 を daemon socket の JSON に詰め込まないようにしています。

画像がモデルへ戻るまで

capture の帰り道もコード読みどころでした。Hermes は capture を CaptureResult として受けて、画像の実寸を再計算します。画像が有効なら $HERMES_HOME/cache/images/computer_use_<uuid>.png にコピーして、チャット surface から添付できる path を summary に残します。保存数は最大 20 枚。要素が多い場合は最初の 100 件だけを inline にして、完全な tree は $HERMES_HOME/cache/computer_use/elements_<uuid>.json へ spill します。ラベルは inline で 120 文字に切ります。

Retina / HiDPI では画像の pixel と native element bounds の座標系が違う場合があるため、Hermes は element bounds の最大値と image size を比較し、差が大きければ bounds_scale と変換 note を返します。画像から読んだ座標をそのまま native click 座標に使う事故を減らす工夫です。

モデルへ戻す経路は 3 通りに分かれます。

  • main model が vision 対応で、provider も tool-result 内の image part を受けられる場合は、OpenAI-style の text + image_url data URL を持つ _multimodal envelope を作って、tool executor が provider-safe な message content に変換する
  • 明示設定・main model が非 vision・provider が multimodal tool result を受けない・capability が不明で安全に画像を送れない、といった条件のときは、screenshot を auxiliary vision model に先に解析させ、main model には text だけを渡す。auxiliary vision 用画像は長辺 1456px へ縮小して、座標を元の空間へ戻す倍率も prompt に添える
  • auxiliary route が失敗した場合は、画像を main model へ無理に送らず、element list で継続できる text-only result に degrade する

context cost の抑え方も明快でした。Anthropic message conversion では Computer Use screenshot を新しい順に 3 枚だけ残し、それ以前の image block は placeholder に置換されます。anthropic_message_convert.py のコメントで「1 枚あたりおよそ 1,465 tokens」と書かれていて、画面操作ループが prompt cache や context cost に与える影響を強く意識しているのが分かります。

さらに gateway/media_repair.py を読むと、Gateway で画像を利用者へ送る場合の細かい配慮も入っていました。モデルが Windows path を POSIX 風に書き換えることがあるので、media_repair.py は同じ turn の computer_use result に存在する UUID basename と完全一致する MEDIA: path だけを canonical path に戻します。勝手に画像を添付するのではなく、明示 directive の修復だけに限定する、というスコープの狭め方です。

承認と権限は 3 層で考える

もう 1 つ、コードを読むまで曖昧だったのが承認まわりです。実装を見ると、承認は 3 層に分けて理解するのが正しいと分かりました。

まず Hermes hard block。承認状態に関係なく、いくつかの system shortcut と、type で入力される危険な shell pattern は handler の入口で拒否されます。例は lock / logout、force-delete 系 shortcut、curl | bashsudo rm -rf、fork bomb などです。これは完全な command classifier ではなく、Computer Use 経由で典型的な破壊操作を直接入力させない最小限の hard block、と読むのが正確です。

次に Hermes approval。変更系 action は Hermes 側 approval scope の対象で、background と foreground を分けています。CLI は _install_tool_callbacks() で Computer Use 専用 callback を接続し、modal prompt の選択を approve_onceapprove_sessionalways_approvedeny に変換します。ただし、_request_approval() は callback 未設定なら default allow と明記されていて、テストもその挙動を固定しています(test_computer_use_approval_isolation.py)。今回の静的調査では CLI 以外の surface(Gateway / ACP)で Computer Use 固有の接続点まで確認できていないので、「全 surface で全 click が必ず Hermes の確認ボタンを出す」と断言はできませんでした。ここは実機かエンドツーエンドで確認したい残タスクです。

最後に cua-driver permission mode。standard / bounded / unrestricted は native runtime 側の認可であり、Hermes の UI approval とは別層です。公式ドキュメントは、standard では通常の click / type / scroll / focus を許可し、残余の高リスク境界を grant / host decision に残すと説明しています。従って「standard = 毎操作で Cua の確認」という理解は正確ではありません。Hermes の approval bypass(--yolo 等)がセッションで有効になると、backend permission mode も unrestricted へ上げられて警告が出ます。cua-driver の mode は起動後 immutable なので、そのセッション専用の private daemon を作り直します。version 3 capability manifest を併用すれば、bypass 中も ceiling を残せます。

その外側にさらに OS permission が乗ります。macOS TCC、Windows integrity / session boundary、Linux compositor policy などです。Hermes と cua-driver の approval を通っても、OS が拒否する操作は成功しません。逆に OS permission が付いていても、bounded manifest が拒否すれば実行できません。3 層を混ぜずに説明する必要があります。

macOS セットアップ記事の訂正メモ

コードを読んだ結果、Hermes Agent の Computer Use を macOS で有効化した記録 に書いていた内容のうち、以下は精度を上げたほうがよかったと分かりました。

  • HERMES_CUA_DRIVER_VERSION=0.26.0 によるピン留めは、v2026.9.7 のコード上には存在しない。installer が latest release を取る挙動にした結果、意図的にこの環境変数は提供していない。再現可能な driver を使うには HERMES_CUA_DRIVER_CMD で特定 binary へ向ける
  • Python 側 MCP 依存の記述は、当時の venv に依存した記録が混ざっていた。v2026.9.7 の tools/lazy_deps.pypyproject.toml では mcp==2.0.0httpx2==2.7.0starlette==1.3.1 になっている
  • 「破壊的 action は既定で承認必須」という趣旨の記述は静的調査だけでは断言できない。schema 上は変更系に approval をかける設計だが、handler は callback 未設定時を allow としているうえ、cua-driver standard 自体も routine click / type を許可する
  • タグとして書いた 61afcde8 は、実験時のローカル checkout または別時点の upstream の可能性がある。tag v2026.9.7 が指す commit は 2237be35 なので、以降は tag 名と full commit の両方を残すようにする
  • built-in skill には input action へ app= を渡すと auto-target するように読める箇所があるが、実装は input が sticky target へ送られ、app= は mismatch guard として使う。skill 記述より実装のほうを優先し、「app を変える前に capturefocus_app」と説明するのが安全

次にこれらを反映する機会は、upstream への doc-fix PR か追記記事のどちらかで扱おうと思います。

次に実機で確かめたいこと

コード上の結論と runtime 挙動を混同しないために、次はエージェント経由ではなくホストの端末から追試するつもりです。デイリースタンドアップやチームミーティングで結果を共有して、次に検証する人が、未確認の挙動と再現条件を把握できるように残しておきます。

  • git rev-parse v2026.9.7^{commit}cua-driver --version を同時に記録する
  • HERMES_CUA_DRIVER_CMD で検証する driver binary を固定し、hash も残す
  • CLI・TUI / Desktop・Telegram / Slack などの Gateway で、background click と foreground escalation の approval 表示を個別に確認する
  • standardbounded で実際に起動する process tree・socket path・TCC 帰属を比較する
  • MCP timeout を安全なテストアプリで人工的に起こし、mutation が再送されないことをログで確認する
  • element index 操作後の effectverifiedescalation の実例を保存する
  • window title が同じ複数 window で sticky target が維持されるかを確認する
  • 日本語の set_valuetype_text を分けて、IME composition の挙動を確認する
  • main model vision・auxiliary vision・text-only の 3 経路で tool result shape を比較する
  • Gateway から screenshot を MEDIA: 添付した際、Windows path repair の挙動を確認する

以上、Hermes Agent v2026.9.7 の Computer Use を tag に固定して読み、単一 tool schema と capture の宛先固定・background-first の verify → escalate・MCP timeout での at-most-once・3 経路の画像経由・3 層の承認、という設計上の分かれ道を整理した、現場からお送りしました。

参考情報