記事一覧へ
# エージェントハーネスの苦いレッスン
LLM をラップするな。そのツールもラップするな。
必要なのは SKILL.md と多少の Python ヘルパーだけだ。LLM には完全な自由がある。何かが足りなければ、LLM 自身がそれを書く。
## 学び
数ヶ月前、我々は「エージェントフレームワークの苦いレッスン」を書いた。主張はこうだ。LLM を抽象化でラップするな。最大の行動空間を与えてから制約せよ。
我々はまだそのツールをラップしていた。
すべての `click()`、`type()`、`scroll()` ヘルパーは、モデルが必要とすると我々が決めた抽象化だ。その一つ一つが、RL 訓練されたモデルが回避しなければならない制約だ。
## なぜ生の CDP なのか
Browser Use の最初のバージョンを構築したとき、何千行もの要素抽出器・DOM インデクサー・クリックラッパーを出荷した。
LLM は CDP を知っている。LLM は何百万ものトークンの `Page.navigate`、`DOM.querySelector`、`Runtime.evaluate` で訓練された。
CDP は Chrome が公開する最低レベルだ。直接モデルに与えよ。
- **クロスオリジン iframe**:フレームの抽象化と戦わず、ターゲットに直接アタッチできる。
- **Shadow DOM**:モデルが一万回見てきたように `shadowRoot.querySelectorAll` を辿れる。
- **アンチボット注入**:Chrome 自身と Chrome が通信しているようなものだ。
## 我々が間違えたこと
数ヶ月前のブログ記事「Closer to the Metal: Leaving Playwright for CDP」でこう結論づけた。「エージェントは作業を完遂するために CDP ターゲットの細かいニュアンスを知る必要があるべきではない。」
間違いだった。
その記事は Chrome タブがクラッシュする 10 通りの方法をリストアップした。我々は各ケースに watchdog サービスを構築した——タブクラッシュ・ターゲットデタッチ・レンダラー OOM・ザイゴート死・GPU プロセスクラッシュ。それぞれにハンドラを設け、各ハンドラを Chrome の内部と同期し続けなければならなかった。
LLM に直接 CDP アクセスと自分自身のハーネスを編集する能力を与えれば、LLM 自身がすべてを処理する。ページが死に、ターゲットが誤ってアタッチされ、Chrome が止まる——エージェントはエラーを読んで、新しいターゲットに再アタッチし、リトライする。watchdog は不要だ。Chrome クラッシュに関する一万件のスレッドを読んでいる。何をすべきかすでに知っている。
我々が隠そうとしていた「CDP の複雑さ」は隠すべきものではなかった。モデルに見せるべきものだった。
## 4 つのファイル
ハーネス全体はそれだけだ:
- `run.py`(13 行)——ヘルパーをプリロードしてプレーンな Python を実行する
- `helpers.py`(192 行)——CDP の薄いラッパー、エージェントが編集する
- `daemon.py`(220 行)——CDP WebSocket を生かし続ける
- `SKILL.md`——上記の使い方をエージェントに伝える
合計約 600 行。
エージェントは Python を書く。Python がヘルパーをインポートする。ヘルパーが CDP を話す。Chrome が言われた通りにする。Chrome より上のものはすべて書き換え可能だ。
## 自己修復ループ
ツールが足りないときに何が起きるかを説明する。
ヘルパーが足りなければ、エージェントはどの Claude Code ユーザーでもやることをやる。`helpers.py` を grep し、関数を追加し、再実行する。
これをするよう指示したわけではない。Claude Code の通常の Read/Edit/Write に CDP アクセスを加えただけだ。コーディングエージェントはすでに足りないインポートの修正方法を知っている。
重要なポイント:エージェントは一から新しいコードを書いているのではない。足りなかった 1 つの関数を書いているだけであり、どんなコードベースでも足りないインポートを修正するのと同じやり方だ。
## 魔法の瞬間
**アップロード**:`upload_file()` を追加し忘れた。タスクの途中でエージェントがファイル入力に遭遇し、`helpers.py` を grep し、何もないことを確認し、生の `DOM.setFileInputFiles` を使って関数を書き、ファイルをアップロードした。git diff を読んで初めて知った。
**チャンクアップロード**:`upload_file` を書いた後、エージェントが 12MB のファイルをアップロードしようとした。CDP WebSocket のペイロードは約 10MB で上限がある。制限に達し、エラーを読み、チャンクアップロードのパターンに切り替えた。
**Gusto からカレンダーへ**:タスクは、すべての従業員の誕生日を共有カレンダーに入れること。Gusto の従業員タブをナビゲートし、DOM から日付を抽出し、Google Calendar イベントを作成する必要があった。
**Azure の管理者ロール**:Azure の管理ポータルは iframe の中の blade だらけだ。生の CDP では `Input.dispatchMouseEvent` を座標レベルで使い、コンポジターレベルで通過できる。
## 試してみる
Claude Code または Codex のセットアッププロンプト:
```markdown
Set up https://github.com/browser-use/browser-harness for me.
```
captcha/2FA 以外でタスクが失敗するものを最初に見つけた人には Mac Mini を贈る。本気だ。1 週間破ろうとして破れなかった。
Repo: https://github.com/browser-use/browser-harness
**エージェントハーネスの苦いレッスン:あなたのヘルパーも抽象化だ。削除せよ。エージェントが必要なものを書かせよ。**

agent-frameworkbrowser-useCDPharness-design
エージェントハーネスの苦いレッスン――抽象化を削除し、LLMに自由を与えよ
♥ 910↻ 71
原文を表示 / Show original
Gregor Zunic
@gregpr07
The Bitter Lesson of Agent Harnesses
25
88
910
159K
Don't wrap the LLM. Don't wrap its tools either.
All you need is a SKILL.md and some Python helpers. The LLM has complete freedom. If something's missing, it writes it.
The learning
A few months ago we wrote The Bitter Lesson of Agent Frameworks. The argument: don't wrap the LLM in abstractions. Maximal action space, then restrict.
We were still wrapping its tools.
Every click(), type(), scroll() helper is an abstraction you decided the model needs. Every one of them is a constraint the RL'd model has to fight around.
Why raw CDP
When we built the first version of Browser Use, we shipped thousands of lines of element extractors, DOM indexers, click wrappers.
LLMs know CDP. They were trained on millions of tokens of Page.navigate, DOM.querySelector, Runtime.evaluate.
CDP is the lowest level Chrome exposes. Give it directly to the model:
Cross-origin iframes. Attach to the target directly, no frame abstraction to fight.
Shadow DOM. Walk shadowRoot.querySelectorAll like the model has seen ten thousand times.
Anti-bot injection. It's Chrome talking to itself.
What we got wrong
A few months ago on this blog we wrote Closer to the Metal: Leaving Playwright for CDP. The conclusion of that post: "Our agents shouldn't have to know the nuances of CDP Targets in order to Get Stuff Done."
Turns out we were wrong.
That post listed ten ways a Chrome tab can crash. We built watchdog services to catch each one - tab crashes, target detach, renderer OOM, zygote death, GPU process crash. Each got a handler. Each handler had to be kept in sync with Chrome's internals.
Give the LLM direct CDP access and the ability to edit its own harness, and it handles all of that itself. Pages dying, targets wrongly attached, Chrome stalling - the agent reads the error, reattaches to a fresh target, retries. It doesn't need a watchdog. It's read ten thousand threads about Chrome crashes. It already knows what to do.
The "complexities of CDP" we were trying to hide weren't something to hide. They were something to let the model see.
Four files
That's the whole harness:
run.py (13 lines) - runs plain Python with helpers preloaded
helpers.py (192 lines) - thin wrappers around CDP, and the agent edits them
daemon.py (220 lines) - keeps the CDP websocket alive
SKILL.md - tells the agent how to use the above
~600 lines total.
The agent writes Python. The Python imports helpers. The helpers speak CDP. Chrome does what it's told. Everything above Chrome is rewriteable.
The self-heal loop
Here's what happens when a tool is missing.
When a helper is missing, the agent does what any Claude Code user would do: greps helpers.py, adds the function, reruns.
We didn't tell it to do this. We gave it Claude Code's normal Read/Edit/Write plus CDP access. Coding agents already know how to fix a missing import.
The key: the agent isn't writing new code from first principles. It's writing the one function that was missing, the same way it'd fix a missing import on any codebase.
Magical moments
Upload. We forgot to add upload_file(). Mid-task, the agent hit a file input, grepped helpers.py, saw nothing, wrote the function using raw DOM.setFileInputFiles, and uploaded the file. We found out when we read the git diff.
Chunked upload. After writing upload_file, the agent tried to upload a 12MB file. CDP websocket payloads cap around 10MB. It hit the limit, read the error, switched to a chunked upload pattern.
Gusto to calendar. Task: put every employee's birthday in our shared calendar. Required navigating Gusto's employee tab, extracting dates from the DOM, then creating Google Calendar events.
Azure admin roles. Azure's admin portal is a pile of blades inside iframes. Raw CDP, via coordinate-level Input.dispatchMouseEvent, passes through at the compositor level.
Try it
Setup prompt for Claude Code or Codex:
markdown
Set up https://github.com/browser-use/browser-harness for me.
First person to find a task it fails on (not captcha/2FA) gets a Mac Mini. Seriously. I've been trying to break it for a week and can't.
Repo: github.com/browser-use/browser-harness
The bitter lesson of agent harnesses: your helpers are abstractions too. Delete them. Let the agent write what it needs.
Want to publish your own Article?
Upgrade to Premium
1:53 AM · Apr 24, 2026
·
159.5K
Views
25
88
910
2K
Read 25 replies