Hermes Agent v2026.9.7 の Computer Use 内部実装を読む — capture の宛先固定と background-first の verify → escalate
先日、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-driver の MCP サーバーへ渡すことです。OS 固有のウィンドウ列挙・アクセシビリティツリー・画面キャプチャ・入力配信はすべて cua-driver 側にあります。
つまり Hermes の Computer Use は「VLM が画面を見て座標を当て続けるだけ」の実装ではなく、AX・UIA・AT-SPI の要素参照、ウィンドウ単位の target binding、画像認識、座標操作を組み合わせたハイブリッド型で、Hermes 側はその制御ループとモデルへの変換層を担当している、と表現するのが正確です。
コードを追って印象に残った設計上の分かれ道は次の 5 点でした。
- モデル非依存の単一 tool schema。
clickやcaptureごとに tool を増やさず、actiondiscriminator を持つ 1 つの JSON Schema にまとめる captureが画像取得だけでなく sticky target binding を兼ねる。以後のclick・type・keyは原則としてその(pid, window_id)へ送られる- background-first と verify → escalate。背景操作を先に試し、
effectとverifiedから必要な一段だけ上げる - 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 loop | tool call の永続化、実行、tool result 追加、次の API 呼び出し(agent/turn_tool_round.py・agent/tool_executor.py) |
| Computer Use wrapper | hard block、承認、セッション分離、target guard、response shaping(tools/computer_use/tool.py) |
| Backend adapter | sticky target、capture / input の型変換、driver capability 追従(tools/computer_use/cua_backend*.py) |
| Transport | asyncio thread、MCP stdio、再接続、CLI fallback(cua_backend_session.py) |
| Native runtime | OS ごとの 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.py と cua_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 は som・vision・ax の 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
}内部ではおおむね次の順で処理されます。
handle_computer_useが action を正規化する- 危険な key combo や shell 文字列の hard block を承認より前に評価する
- 変更系 action なら Hermes 側 approval scope を評価する(foreground 化は background 操作とは別 scope)
session_idに対応する backend を取得し、そのセッション専用 lock を取る- click handler が現在の sticky target と element index を
CuaDriverBackend.click()へ渡す - backend は
(pid, window_id)と、対応 driver なら snapshot のelement_tokenを付ける - MCP の
clickまたはdouble_clickを呼ぶ - cua-driver の structured response を
ActionResultへ正規化する okだけでなくeffect・verified・escalationから Hermes のverdictを生成するcapture_after=trueかつ transport-levelokなら、同じ(pid, window_id)を再 capture し、操作結果と証拠画像を 1 つの tool result にまとめる- vision capability に応じて、画像付き結果または文章結果として会話へ追加する
- LLM が verdict と新しい画面を見て、終了・再 capture・一段階 escalation のいずれかを選ぶ
capture_after が失敗 action で走らないのは重要です。失敗直後の「一見普通のスクリーンショット」が成功証拠と誤解されるのを避ける意図が、tool.py のコメントに明記されています。
capture は観測と宛先確定を兼ねる
コードを読んで一番印象に残ったのがここでした。capture は画像を返すだけの副作用のない observation ではなく、「以降 click や type が送られる相手」を確定させる binding 操作でもあります。
capture は 3 つの mode を持ちます。
| mode | 主な出力 | 向いている場面 |
|---|---|---|
som | 画像 + numbered elements | 通常の操作。番号で element を選ぶ |
vision | 主に window screenshot | custom canvas、AX が弱い UI、見た目の確認 |
ax | element 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 はコード上こう読めます。
elementを使った background actioneffect=confirmed/verified=trueなら終了effect=unverifiableなら再送せず fresh capture で確認suspected_noopやエラーでrecommended="px"なら、同じ window の座標操作へ一段だけ上げるrecommended="foreground"または pixel path も失敗なら、その一操作のみ foreground delivery を明示する- 永続的な focus change が必要なときだけ、別 action の
bring_to_frontを使う
ここで大事なのは ActionResult.ok が transport-level success に過ぎない、という切り分けです。実際に UI が変わったかどうかは effect と verified が担っています。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_windows や get_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_urldata URL を持つ_multimodalenvelope を作って、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 | bash、sudo 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_once・approve_session・always_approve・deny に変換します。ただし、_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.pyとpyproject.tomlではmcp==2.0.0・httpx2==2.7.0・starlette==1.3.1になっている - 「破壊的 action は既定で承認必須」という趣旨の記述は静的調査だけでは断言できない。schema 上は変更系に approval をかける設計だが、handler は callback 未設定時を allow としているうえ、cua-driver
standard自体も routine click / type を許可する - タグとして書いた
61afcde8は、実験時のローカル checkout または別時点の upstream の可能性がある。tagv2026.9.7が指す commit は2237be35なので、以降は tag 名と full commit の両方を残すようにする - built-in skill には input action へ
app=を渡すと auto-target するように読める箇所があるが、実装は input が sticky target へ送られ、app=は mismatch guard として使う。skill 記述より実装のほうを優先し、「appを変える前にcaptureかfocus_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 表示を個別に確認する
standardとboundedで実際に起動する process tree・socket path・TCC 帰属を比較する- MCP timeout を安全なテストアプリで人工的に起こし、mutation が再送されないことをログで確認する
- element index 操作後の
effect・verified・escalationの実例を保存する - window title が同じ複数 window で sticky target が維持されるかを確認する
- 日本語の
set_valueとtype_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 層の承認、という設計上の分かれ道を整理した、現場からお送りしました。
参考情報
- Hermes Agent 公式サイト
- NousResearch/hermes-agent GitHub リポジトリ
- Hermes Agent
v2026.9.7ソースツリー - Hermes Agent commit
2237be35 - Hermes Agent Computer Use ドキュメント
- trycua/cua GitHub リポジトリ
- cua-driver-rs v0.26.0 リリース
- Best-effort background(cua 公式)
- cua-driver permission modes と capability manifest
- cua-driver Platform Support
- Model Context Protocol 公式サイト
- macOS Accessibility API ドキュメント
- Windows UI Automation ドキュメント
- AT-SPI2 リポジトリ
- macOS TCC ドキュメント
- Anthropic のスクリーンショット推奨サイズ
- 先日の選定メモ: macOS と Windows 両対応の Computer Use エージェント
- 先日の macOS 有効化ログ: Hermes Agent の Computer Use を macOS で有効化した記録