Jinba Trail の Build Studio手順を生成 をクリックしたとき、実際にブラウザーを操作しているのは @browserflow/agent です。これは Browserflow monorepo 内のオープンソースパッケージであり、ホスト型プロダクト専用に作られた特別なものではありません。これは RecordDriver を実装しています。つまり、手書きのテストドライバーから record() が受け取るのと同じ接続点を、Claude と実際の Playwright ページを使う ReAct ループで裏付けたものです。
このページは、デモンストレーションを実行するエンジン側パッケージ(@browserflow/agent, Apache-2.0)について説明します。実行中に人間が見るもの、つまり2ペインのコックピット、引き継ぎプロンプト、レビュー手順については、Build Studio: 記録、レビュー、公開 を参照してください。

どこに接続されるか

createAgentDriver は、packages/browser/src/replay.ts のスクリプト化されたドライバーとまったく同じように RecordDriver を構築します。これは record({ driver }) に渡され、demonstrate()record()Recording に正規化する解決済みの Operation[] を返します。これをサポートするためにエンジンコードを変える必要はありません。モデルは、人間やfixtureが供給できるのと同じデモンストレーションを供給する、より賢い入力元にすぎません。 ループが触れるものはすべて注入されます。page(オーサリング時は実際の Chromium、テストではfake)、modelModelClient、テストではスクリプト化)、onEvent、そして ask です。ループモジュール自体は @anthropic-ai/sdkplaywright もimportしません。そうするのは anthropic-model.ts だけで、しかも遅延して行います。これにより、ドライバーのspec suiteは Chromium なし、モデル呼び出しなしで実行できます。

ReAct ループ

demonstrate() の各ターンは、maxIterations で制限された同じ4拍のサイクルを実行します。
1

observe

observe(page) はライブページを Observation としてスナップショットします。内容は、base64 PNGスクリーンショット(vision)、テキストとして描画されたアクセシビリティツリー(Locator.ariaSnapshot() 経由)、現在のURLです。これに加えて、タスク、宣言済みの入力/シークレットスロット名、ここまでにキャプチャされた操作、現在の手順に対する recentFailures があれば、それらがモデルの ObservationInput になります。
2

model.next

opts.model.next(input, { signal })ModelClientDecision を要求します。これは、(ナレーションとしてstudioへstreamされる)thought と、1つの Action で構成されます。Actionact(操作を実行)、need_login / need_input(人間のために一時停止)、または done です。具体的な実装である createAnthropicModelClient は、スクリーンショットとプロンプトを Claude(デフォルトでは claude-opus-4-8)へ送り、単一の decide tool call を強制します。これはループに届く前に zod で検証されます。
3

apply

act のdecisionでは、captureOpapplyToPage(page, op, samples) を呼びます。操作(クリック、入力、ナビゲーション)は実際にライブページで起こります。samples により、事前に宣言された入力パラメータは空のプレースホルダーではなく代表的なサンプル値を入力できるため、最初のデモンストレーションから実データでFlowを試せます。
4

resolveLocator

効果が発生した直後、resolveLocator(page, op)ライブページの証拠、つまり page.candidatesFor(target) からロケータを再導出し、候補を role → text → label → placeholder → testid の順にランク付けします。生の css は最後の手段です。記録されるのは、モデルが提案したロケータではなく、解決済みロケータです。
done はループを正常終了します。need_loginneed_inputawait イベントをemitし、人間が応答するまで opts.ask(...) でblockします(Build Studio の引き継ぎ手順を参照)。

ロケータの信頼境界

モデルはアクションを提案します。何が記録されるかを決めることはありません。
Action.actop: Operation を保持し、その locator は要素の見つけ方に関するモデルの推測です。それはもっともらしいが壊れやすいCSS selectorかもしれませんし、ページ上の3要素にたまたま一致するrole/nameの組かもしれません。ドライバーは次のように動きます。
  1. そのopをライブページに適用します(モデルの意図を尊重するため)。
  2. resolveLocator を呼び、同じ要素にも解決される安定したロケータを実際のDOM(page.candidatesFor)からすべて調べ、最も優先度の高いものを選びます。
  3. 解決済みロケータと完全な LocatorEvidencechosen, candidates, match_count)を記録します。モデルの生selectorが Recording に入ることはありません。
これは慣習ではなく構造的に強制されています。driver.tswithResolvedLocator は、opが history にpushされる前に、必ず op.locatorresolveLocator が返した値で上書きします。唯一の例外は navigate です。これは解決すべきロケータを持たないため、url はモデルが与えたとおりに記録されます。 同じパターンは need_input にも拡張されます。ドライバーは typeInto で人間の回答をページに入力し、その後も fill を記録する前に、モデルが提案したフィールドロケータに対して resolveLocator を呼びます。human-in-the-loop の回答であっても、この境界を迂回しません。

シークレットの二重防御

モデルが認証情報を入力することは実際のリスク面です。system promptは {from:"input",slot} / {from:"secret",slot} を優先し、literalを使わないよう指示しますが、promptは保証ではありません。コードベースはこれを、2つの異なるパッケージがパイプライン上の2つの異なる地点で二重に強制する境界として扱います。

Stream時: マスク

packages/agent/src/describe.tslooksLikeSecretLiteral / maskSecretsInText は、すべてのSSE emission(thought, recovering, error、および redactOpForStream 経由の構造化された step op)を、オーサリングbusに届く前にscrubします。studioから見えるものでは、シークレットらしいliteralは «redacted-secret» になります。

永続化時: 拒否

packages/browser/src/record.tsassertNoSecretLeak は、record() がドライバーの出力を正規化するときに実行されます。既知の認証情報の形状に一致する、高エントロピーである、または呼び出し元が供給した既知のシークレット値を含む {from:"literal"} 値は、完全に拒否されます。つまり、record() はそのバイト列を含む Recording を永続化するのではなくthrowします。
両方の半分は、同じheuristicである looksLikeSecretLiteral を共有します(加えて、その CREDENTIAL_PATTERNS list。PEM blocks、sk-…、AWS access keys、GitHub tokens、Slack tokens、JWTs、Google API keys、そして不透明な高エントロピーtoken向けのShannon entropy fallback)。どちらのパッケージも相手側のcopyをimportできません(browser packageはpublic entryだけをexportし、agent packageはbrowser internalsを取り込んではいけません)。そのため、この関数は両方のファイルで逐語的に重複しています。
describe.tsrecord.ts はどちらも、それぞれの looksLikeSecretLiteral / CREDENTIAL_PATTERNS のcopyの上に、明示的な KEEP IN LOCKSTEP コメントを持っています。これはstyle上の注記ではなく、実際の2パッケージ不変条件です。一方のファイルでheuristicを厳しくしたり緩めたりすると、もう一方と同期がずれます。その結果、stream時のmaskと永続化時のgateが、何をシークレットと見なすかで不一致になります。必ず両方を一緒に変更し、両方のファイルのtest suites(describe.spec.ts, record.spec.ts)がgreenのままであることを確認してください。
agent driverの describe.ts copyは、永続化時のcopyには不要な追加入力を1つ受け取ります。knownSecrets、つまりthis session中に人間が need_input/need_login promptへの応答として入力したplaintext値の集合です。これらの値は純粋にメモリ上で追跡され(createAgentDriverknownSecrets set)、エントロピーに関係なくすべてのSSE emissionからscrubされ、それ自体が永続化されることはありません。

暴走防止ガード

モデルがブラウザーを操作すると、スクリプト化されたドライバーでは起きない失敗の仕方をすることがあります。行き詰まる、ページを読み間違える、不正な内容を返す、といったものです。4つの独立したガードが被害を制限し、それぞれ何かをabortする前に recovering イベントとして表面化します。
opSignature(op) は、valueを無視して操作をop-kind + targetに折りたたみます。同じフィールドへの2回のfillは、slot名が異なっていても衝突します。解決済みopのsignatureがすでに seenSignatures にある場合、ドライバーは重複を記録しません。代わりに、モデルへrecovery hint(“interact with a DIFFERENT element … or return done”)を渡し、consecutiveRepeats をincrementします。真に新しいtargetはcounterをzeroにresetします。MAX_CONSECUTIVE_REPEATS3)に達するとthrowしてデモンストレーションを終了します。これにより、自分の効果を読み戻せないモデル(例: まだ目に見える結果がないsearch box)が Recording を大量の不要なrepeatで埋めることを止めます。
tool callがschema validationに失敗すると、model.next() はthrowすることがあります。これは、寛容な normalizeDecision/normalizeOp coercionでも救えなかったドリフトです(anthropic-model.ts で処理されている extractkind/read collision、GitHub #39を参照)。単発のbad turnは、session全体をkillするのではなく recovering として表面化し、新しいobservationでretryされます。turnのdecodeに成功した瞬間、consecutiveDecodeFailures はresetされます。MAX_CONSECUTIVE_DECODE_FAILURES3)に達するとthrowします。
外側のループは DEFAULT_MAX_ITERATIONS = 64 turnsで制限されます(AgentDriverOpts.maxIterations で設定可能)。そのbudget内にモデルが {kind:"done"} を返さない場合、demonstrate() は永遠にloopするのではなくthrowします。
AgentDriverOpts.signal は停止ボタンです。ループは各iteration境界で signal.aborted を確認しますが、境界だけのpollingでは十分ではありません。turnの途中で停止をクリックしても、進行中の model.next() callやすでに実行中のPlaywright actionは中断されないからです。raceAbort() は、すべての awaited step(モデル呼び出し、ページ操作)をsignalとraceさせます。そのため、停止が発火した瞬間にループは待機をやめDemonstrationCanceled でrejectします。たとえ基盤となるcallがbackgroundで完了しつつあってもです。さらに model.next は、実際のAnthropic HTTP requestへ signal をthreadするため、そのcall自体がabandonされるだけでなくabortされます。cancelは単一の {type:"canceled"} eventをemitします(error ではありません。意図的な停止はfailureではないためです)。また、呼び出し元が特に DemonstrationCanceled をcatchすることで、他のすべてのfailure modeと区別されます。

モデルの接続点

ModelClient は1メソッドのinterfaceです。model.ts で定義された next(input: ObservationInput, opts?): Promise<Decision> で、SDKはまったくimportしません。コードベース内で @anthropic-ai/sdk のimportを許されているモジュールは anthropic-model.ts だけで、それも next() 内で遅延して行います。そのため、clientを構築するだけ、あるいは @browserflow/agent をimportするだけでは、@anthropic-ai/sdk はloadされません。
// packages/agent/src/model.ts — the seam every consumer (driver, tests, service) depends on
export interface ModelClient {
  next(input: ObservationInput, opts?: ModelNextOptions): Promise<Decision>;
}

export type Action =
  | { readonly kind: "act"; readonly op: Operation }
  | { readonly kind: "need_login"; readonly ctx: AskCtx }
  | { readonly kind: "need_input"; readonly locator: Locator; readonly ctx: AskCtx }
  | { readonly kind: "done" };
現在存在する実装は2つです。createAnthropicModelClient(実際のClaude、強制された decide tool call、デフォルトは claude-opus-4-8、より安価なoptionとして claude-sonnet-4-6 が利用可能)と、ドライバー自身のtest suiteが供給する任意のスクリプト化された ModelClient です。ループは常にinterfaceだけに依存するため、driver spec(driver.spec.ts)は、上記すべてのガードを含む完全なReActループを、Chromiumなし、モデル呼び出しなしでexerciseします。
SDKからはまだ利用不可 / まだ配線されていません。モデル選択は AnthropicModelConfig 内の静的default(claude-opus-4-8cheapModel として claude-sonnet-4-6)であり、driver内でsessionごとにcheap modelを自動選択するものはまだありません。sessionごとのコスト調整が必要な場合は、それをfeatureとして文書化する前に、現在の挙動をownerに確認してください。

関連ページ

Build Studio

同じループの人間向け側面です。2ペインのコックピット、ライブブラウザー表示、そして need_login / need_input の引き継ぎプロンプトを扱います。

シークレットと境界

Recordingが公開された後、シークレット参照がFlowSpec、Journal、Packの外に留まる仕組みです。このページのstream/永続化防御がオーサリング時に保護する境界の、runtime側の半分です。

SDK & YAML

Recordingが最終的に browserTool() 経由でコンパイルされる先である、2つのオーサリングsurfaceです。

トラブルシューティング: サイトが変更された

このループがキャプチャした解決済みロケータが、リプレイ時にライブページと一致しなくなった場合に何が起きるかを説明します。