TypeSafe AI の System One モデル Jev を MoonBit から触るためのプレイグラウンド。 Jev は「文字列ではなく型付きの確率判断を返す」意思決定専用モデルです(unstructured state in, typed probabilistic decisions out)。
このリポジトリの構成:
| パッケージ | 内容 |
|---|---|
lib/ |
Jev API クライアント(GET /v1/models・POST /v1/systemone、noul/choice/score 3 種の質問と型付き応答の JSON 変換) |
cmd/jev |
単発質問 CLI(モデル一覧・state に対する型付き質問) |
cmd/gomoku |
五目並べで Jev 同士を対戦させる CLI(毎手、盤面+着手候補を Jev に判断させる) |
cmd/gomoku_gif |
対局ログ(各手の実測ミリ秒付き)を実時間再生の GIF に変換するツール |
cmd/patterns |
公式ドキュメントの意思決定パターンを実 API に対して走らせ、効果を実測する CLI |
cmd/shellrisk |
シェルコマンドの危険度判定(エージェントの実行許可ゲート) |
| MOBA(3v3・5v5) | mizchi/jev-playground-moba に移しました —— moba/ moba5/ cmd/moba*、レポート 02・10・11・12・56、採点できるベンチマークとその記録 |
report/ |
実験 CLI 共通の整形と応答アクセサ |
experiments/ |
TypeScript / JS 側の実験(チェス・ブラウザ探索・エージェント生成プロンプト・ESLint 合否予測) |
experiments/eslint-plugin-jev |
eslint-plugin-jev — 判定を Jev がやる ESLint プラグイン(関数ごとの score と名前付きレビュー指標 + セレクタ 1 個と 1 文で書く ad-hoc ルール、ファイル単位でバッチ) |
experiments/task-filter |
タスク/テストランナーの filter — just の依存グラフ + diff で「今何を走らせるか」を採点 |
experiments/browser-chaos |
ブラウザ操作 — 操作ごとに分けた action space を投機的に 1 リクエストで聞く driver。ベンチ用アプリとモデル不要の検査 7 本つき |
justfile |
このリポジトリのチェックをタスクグラフにしたもの(just ci で全部、just test-lib で 1 つ) |
scripts/ |
依存ゼロのリポジトリ検査(doc アンカー、jevlang 2 実装の一致) |
hooks/ |
Claude Code の PreToolUse hook(Bash コマンドの実行許可ゲート。依存ゼロの Node スクリプト) |
jevdsl/, cmd/jevdsl |
jevdsl — 判断を match できる値にする薄いラッパー(MoonBit) |
jevlang/, cmd/jevlang |
jevlang(MoonBit 版)— 条件が Jev の判断である小さな言語 |
jevlang-js/ |
jevlang(JS 版)。同じ .jev を走らせ、結果の一致をテストしている |
examples/ |
.jev のサンプルと、API 不要で再現するための transcript |
どのパターンが優位かの実測レポートは docs/ にあります
(索引)。
入口は問いごとに 4 本です:
| 何が書いてあるか | |
|---|---|
| docs/when-to-use.md | どういう時に使えるか —— 入れる条件と入れない条件、置き場所の決め方 |
| docs/fit.md | 向いているケース —— 効く問題の形と効かない問題の形(実測つきの早見表) |
| docs/tuning.md | チューニング —— 決める順に(答えの形・質問・state・閾値・合成・運用・計測・行動)、やってはいけないこと一覧つき |
| docs/case-studies.md | ケーススタディ —— 題材 13 件(MOBA を含む)で、何が効いて何が効かなかったか |
| docs/findings.md | 実験ごとに何がわかったか(1 本 = 1 ブロック) |
| docs/summary.md | やったこと / わかったことの端的な要約 |
| TODO.md | 残作業。 「なぜ開いているか」と「何があれば閉じるか」を項目ごとに。閉じ方が分からないものはそう書いてあります |
- MoonBit ツールチェーン(
moon0.1.20260915 以降) - TypeSafe AI の API キー(早期アクセス)
export TYPESAFEAI_API_KEY=your_key_here
moon update # 依存(tapi: moonbitlang/async, moonbitlang/x, mizchi/image, WGYo90/moonbit-gif)を取得API キーはソースコードに書かず、環境変数 TYPESAFEAI_API_KEY か各 CLI の --api-key で渡します。
moon run --target native cmd/jev -- --models
# [{"name":"jev-latest",...},{"name":"jev-preview",...}]デモ(3 種の質問 — score / choice / noul — を 1 リクエストで並列評価):
moon run --target native cmd/jev -- --prettystate を直接指定(テキスト or JSON 文字列):
moon run --target native cmd/jev -- --state "This email is a phishing attempt" --pretty
moon run --target native cmd/jev -- --state '{"amount": 299, "currency": "USD"}' --prettystate と質問をファイルから:
moon run --target native cmd/jev -- --state-file state.json --questions-file questions.jsonquestions.json の形式(noul / choice / score を混在可能):
{
"is_spam": {
"type": "noul",
"instructions": "This message is spam.",
"criteria": {
"true": "Unsolicited advertising",
"false": "A legitimate conversation"
}
},
"tone": {
"type": "choice",
"instructions": "What is the tone of this message?",
"criteria": {
"polite": "Friendly and courteous",
"neutral": "Calm and factual",
"frustrated": "Angry or upset"
}
},
"handler": {
"type": "choice",
"instructions": "Which handler owns this? (選択肢名だけで解釈させる形)",
"criteria": { "billing": null, "technical": null, "sales": null }
},
"urgency": {
"type": "score",
"instructions": "How urgent is this request?",
"criteria": ["Can wait", "Needs attention this week", "Needs attention today"]
}
}オプション: --model(既定 jev-latest)、--base-url、--pretty、--api-key
moon run --target native cmd/gomoku -- # 15x15 で 1 局
moon run --target native cmd/gomoku -- --board 9 --games 2 --verbose
moon run --target native cmd/gomoku -- \
--player1 jev-latest --player2 jev-preview --board 15 # モデルを分けて対戦毎手、盤面を state に、着手候補(石の近傍セル)を choice 質問の criteria として Jev に問い、
WINS / BLOCK / ライン長(3o = 3 連・片端オープン)のヒント付きで最良手を選ばせます。
API 失敗・不正手のときだけ組み込みフォールバック(勝ち手 > ブロック > 中央)を使います(統計に fallback moves と表示)。
主要オプション: --board N(1..15 推奨)、--games N、--player1/--player2 <model>、
--verbose(毎手の盤面表示)、--log-path <file>(対局ログの JSONL を出力)
# 1. 対局をログ付きで実行(各手の実測ミリ秒を記録)
moon run --target native cmd/gomoku -- --board 15 --log-path game15.jsonl
# 2. ログから GIF を生成(フレーム遅延 = その手の実測時間 → 実時間再生)
moon run --target native cmd/gomoku_gif -- --log game15.jsonl --out gomoku.gif生成例: gomoku.gif(362x362 / 26 フレーム / 約 47KB、再生総時間 ≈ 対局の実測時間)。
gomoku_gif のオプション: --scale(セル辺長 px)、--padding、--hold-cs(最終フレーム表示、1/100 秒)、
--max-cs(フレーム遅延の上限)、--loop は未対応(GIF は常にループ再生)。
どの問題形状が Jev に向いているかを実 API で測った結果は docs/ にあります。
各レポートは生の数値と再現コマンド付きです。
| # | 内容 |
|---|---|
| when-to-use · fit · tuning · case-studies | ガイド 4 本 —— 使える条件・向いているケース・チューニング・ケーススタディ |
| findings | 実験ごとに何がわかったか |
| summary | やったこと / わかったことの要約 |
| 00 | API の実挙動(スキーマに書かれていない上限・挙動、公式パターン集の実測) |
| 01 | シェルコマンドの危険度判定 — エージェントの実行許可ゲート |
| 02・10・11・12・56 | MOBA(3v3・5v5)と採点できるベンチマーク —— jev-playground-moba に移しました(索引) |
| 03 | チェス、Jev vs Claude Sonnet 5 |
| 04 | エージェントに質問を設計させて動的にパイプラインを組む |
| 05 | chaosbringer の次操作選択を Jev に |
| 06 | 次に効きそうなことの提案(優先順位つき) |
| 07 | confidence でエスカレーションする二層構成 — 成立条件は 3 つある |
| 08 | skill suggestion cookbook の追試(68 スキル) |
| 09 | LLM guardrails cookbook の追試(入出力スクリーニング) |
| 16 | コードと ESLint ルールの評価基準だけ渡し、実装を伏せて合否を当てさせる |
| 17 | タスクランナーの大量のタスクから正しいものを選べるか |
| 18 | Claude Code の PreToolUse hook にして、Bash の実行許可をゲートする |
| 19 | jevlang — 条件が Jev の判断である小さな言語を 2 実装で作る |
| 20 | jevdsl — MoonBit から match できる薄いラッパー(設計ノート) |
| 21 | eslint-plugin-jev — 判定を Jev がやる ESLint プラグイン(関数ごとの score) |
| 22 | 具体的な「良いコード」の指標を名前で聞くと何が変わるか + 列挙の穴の深さ |
| 23 | タスク/テストランナーの filter — グラフが「走れるもの」、Jev が「走るべきもの」 |
| 24 | まだ存在しないルールを自然言語で書く — セレクタだけコードで書き、述語は 1 文 |
(索引の全文は docs/README.md にあります。)
一行でまとめると、一番効いたのは「答えの形を問題の形に合わせる」ことでした
(順序のある結論を choice から score に変えるだけで正解率 19/24 → 23/24)。
まとめ表に効果と落とし穴を並べてあります。
実行:
moon run --target native cmd/patterns -- # 公式パターン集の実測
moon run --target native cmd/shellrisk -- # シェルコマンド判定TypeScript / JS 側の実験(チェス・ブラウザ探索・エージェント生成・ESLint 合否予測・
ESLint プラグイン)は experiments/ 以下で、
各ディレクトリで npm install してから走ります。
cd experiments/eslint-oracle && npm install
npx tsx src/truth.ts # ESLint の正解ラベルだけ(API 不要)
npx tsx src/run.ts --repeat 5 # 実装を伏せて合否を当てさせる(要 API)
cd experiments/task-picker && npm install
npx tsx src/run.ts --repeat 3 --scale # 133 タスクから正しいものを選ばせるexperiments/eslint-plugin-jev は本物の ESLint プラグインで、ルールは 2 つです。
| ルール | 書くもの | 判定対象 |
|---|---|---|
jev/quality |
なし | 全関数を固定の質問セット + 8 指標で |
jev/rule |
セレクタと 1 文 | セレクタに当たったノードを、その文で |
jev/quality は関数ごとに「レビューでどれだけ押し返すか」を score で出し、
ファイル 1 個ぶんの全関数を 1 リクエストで聞きます(依存ゼロ、ビルド不要)。
1:8 warning Jev thinks `compareTokens` does the wrong thing for some realistic
input (misbehaves 0.71 (fires at 0.70); reviewer action 1.94/3) jev/quality
cd experiments/eslint-plugin-jev && npm install
npm test # 105 件、API 不要
npm run truth # ラベルをコード実行で検証、API 不要
npm run replay # 記録から全数値を再計算、API 不要
npm run rules # jev/rule の記録済み判定、API 不要
TYPESAFEAI_API_KEY=... npm run warm # jev/quality: 78 関数を 15 リクエスト、$0.001
TYPESAFEAI_API_KEY=... npm run warm:rules # jev/rule: 49 ノードを 12 リクエスト、$0.001
TYPESAFEAI_API_KEY=... npm run lint # eslint が Jev の判定を読む(両ルール)ESLint のルールは同期関数で await できないのが本題です。判定を事前に
バッチで済ませてキャッシュに置き、ルールはハッシュを引くだけにすることで
lint 時間は何もしない lint と誤差の範囲(31 ms 対 36 ms)に収まります。
onMiss: "ask" を選べばルールの中で同期リクエストもできますが 1 ファイル 361 ms。
実測は 自信のある指摘の 15/15 が本物のバグ・誤検出 0、ただし 12 バグ中 5 個しか 捕まえない(関数 1 個 $0.000017)。捕まえるのは契約の齟齬、落とすのは 特定の API の誤用でした。→ docs/21
--rubric full にすると 8 つの具体的な欠陥クラスを名前で聞きます
(関数ごとに 10 問、それでも 1 ファイル 1 リクエスト)。捕まる数が 33/51 → 41/51 に増え、
指摘が名前で返ります:
5:8 warning `applyDiscount` matches the review criterion `unit_or_arithmetic` (0.94, its cutoff is 0.28)
13:8 warning `increment` matches the review criterion `lost_update` (0.93, its cutoff is 0.73)
ただし名前を付けても戻ったのは見逃し 6 個のうち 2 個で、 一番効いたのは指標ではなく指標ごとに閾値を引くことでした (共通閾値 0.80 で 13/36、質問ごとなら 24/36)。
指標が効くのは見えにくいバグでだけです。関数の中だけ読めば分かるバグは
naming の有無にかかわらず 15/15 で、特定の API の挙動を知らないと見えないバグで
14% → 52% になります。そして 8 指標は重複していて、抜いて本当に困るのは 1 個だけ
(api_default、9/12 → 4/12)。→ docs/22
セレクタだけコードで書き、述語は 1 文で書きます。
CallExpression[callee.name='fetch'] は 10 秒で書けて、「ただしリトライ
ラッパの内側で既にタイムアウトが設定されている場合を除く」は 1 週間かかる —
チームの規約が lint ルールにならない理由はいつもこれです。
"jev/rule": ["warn", { rules: [{
id: "fetch-timeout",
selector: "CallExpression[callee.name='fetch']",
rule: "fetch は必ずタイムアウト (AbortSignal.timeout など) を渡すこと",
note: "リトライラッパの内側で既に設定されている場合は違反ではない",
}] }],当たったノードごとに score を付け、1 リクエストに最大 256 件まで詰めます
(実際にはファイル単位のまとまりの方が先に効きます)。セレクタはわざと
広く書くのが正解で、score の 0 は「セレクタが余計なものを拾った」を意味します。
コーパスで 5 ルール・49 ノードを 12 リクエスト・$0.0011 で判定して、 7 件の指摘。うち自信のある 5 件は 5/5 が本物のバグでした。外した 1 件は 意図的に catch している関数で、confidence 0.31 なので「指摘」ではなく 「質問」として出ました。
効くかどうかを読むのは閾値ではなく gap(違反と残りの点数差)です:
rule cutoff matches reported widest gap cutoff in gap
no-string-built-query 2.50 20 2 1.82 yes
atomic-read-modify-write 2.00 7 1 1.79 yes
no-stringly-arithmetic 2.00 14 1 2.16 yes
no-swallowed-catch 1.50 4 2 0.77 yes
gap が広ければ閾値はどこに置いても同じ答えになり、gap が狭いのは閾値の問題
ではなく文の問題です。実際 5 ルール中 2 つは最初の文が失敗(gap 0.16 と 0.28)し、
文を書き直して 2.16 と 0.77 になりました。閾値は質問に入らないので、
at を変えても再質問はゼロです。
一方、セレクタは静かに失敗します。当たらなかったノードはどの閾値でも 質問されず、レポートにも出ません(コーパスのバグ 1 件をこれで落としました)。
実測と、gap をどう読むかは → docs/24
プラグインとしての使い方(flat config、オプション全表、warm パスの CLI、閾値の チューニング、ルールの書き方、限界)は experiments/eslint-plugin-jev/README.md。
hooks/jev-permission-gate.mjs は、Bash コマンドの実行許可を Jev に判定させる
PreToolUse hook です(依存ゼロの Node スクリプト 1 枚、ビルド不要)。
既定では ask / deny しか返さず、安全と判定したときは何も出さないので、
あなた自身の permission ルールを上書きしません。
node hooks/test-gate.mjs --failsafe-only # 7 つの失敗経路だけ確認(API キー不要)
node hooks/test-gate.mjs --policy-logic # .jev ポリシーの規則を検証(API キー不要)
TYPESAFEAI_API_KEY=... node hooks/test-gate.mjs # docs/01 の 24 コマンドで採点有効化は hooks/settings.example.json の hooks ブロックを .claude/settings.json に
コピーします(閾値と文脈は hooks/jev-gate.json.example 参照)。
このリポジトリでは意図的に配線していません —— チェックアウトした人全員の
Bash がゲートされてしまうので。実測値と設計の理由は docs/18。
同じ判断を Pi の拡張として動かす部品は pi/ です(§11)——
Pi の tool_call は ask を返せないので、ask の解決の仕方だけが違います。
lib は API をそのまま写した生クライアントです。jevdsl は判断を
(result, confidence) のタプルにして、MoonBit の match に直接載せます。
let jev = @jevdsl.session(client, state)
match jev.noul("家に牛乳がない") {
(true, c) if c > 0.5 => buy("牛乳")
(true, _) => ask_the_user()
(false, _) => ()
}
match jev.choice("100円余ったときに買うもの", ["プリン", "ビール"]) {
(pick, c) if c > 0.5 => buy(pick)
(_, _) => ()
}
match jev.score("急ぎ度", ["待てる", "今日", "今すぐ"]) {
(s, c) if s >= 1.5 && c > 0.5 => go_now()
(s, _) if s >= 0.5 => go_today()
(_, _) => ()
}3 種すべて同じ形なので、guard に閾値を書けます。
noul は API が confidence を返さないのでコイン投げからの距離
(|p - 0.5| * 2)を confidence にしています(生の確率は probability())。
moon run --target native cmd/jevdsl -- # 1 判断 1 リクエスト(3 req / 1069 tok)
moon run --target native cmd/jevdsl -- --bundled # 3 判断を 1 リクエスト(1 req / 435 tok)
moon test --target native -p jevdsl # 10 件(API 不要)複数の判断があるなら Session::ask で束ねてください(アクセサは同じ)。
設計の理由と限界は docs/20。
if の条件や match の対象が自然文の確率判断である DSL です。
JS 版(jevlang-js/)と MoonBit 版(jevlang/ + cmd/jevlang)の 2 実装があり、
同じ .jev を走らせて結果が一致することをテストしています。
state { fridge: ["卵", "ビール"], wallet_yen: 100 }
threshold noul = 0.5
if noul("家に牛乳がない") {
buy("牛乳")
}
let result = match choice("100円余ったときに買うもの", ["プリン", "ビール", "うまい棒"]) {
"プリン" => buy("プリン")
"ビール" => buy("ビール")
"うまい棒" => buy("うまい棒")
else => buy("牛乳") # else 腕は gate noul を別の質問として生やす
}
let praised = score("${result} を買ったことで妻に褒められる確率", [
"褒められない", "何も言われない", "褒められる",
])
node jevlang-js/bin/jevlang.mjs examples/milk.jev # JS 版
moon run --target native cmd/jevlang -- examples/milk.jev # MoonBit 版
node jevlang-js/test.mjs # 24 件(API 不要)
moon test --target native -p jevlang # 31 件(API 不要)
scripts/jevlang-conformance.sh # 2 実装の一致(API 不要)hooks/policy.jev は、7 節の hook の判定を
この言語で書いたものです(--policy で差し替えられます)。
質問文が実行前に確定している judgment は 1 リクエストに巻き上げられます
(examples/milk.jev で 4 → 2 リクエスト、1501 → 867 トークン)。
"${result}" のように実行時の値に依存する質問だけが後から個別に聞かれます。
設計の理由と実測は docs/19。
just で依存グラフを先に作っておき、今の diff を state に、
ゴールタスク 1 つにつき score 1 問を 1 リクエストで投げて、
閾値で切った集合を走らせます。前提と順序はグラフの閉包が足すので、
e2e-auth を選べば build-web は自動で付いてきます(Jev は依存関係を知らなくていい)。
cd experiments/task-filter && npm install
npm test # 28 件(API 不要・just 不要)
TYPESAFEAI_API_KEY=... npx tsx src/cli.ts --base main # 手元の diff を採点して `just ...` を出す
TYPESAFEAI_API_KEY=... npx tsx src/run.ts --repeat 3 # 20 ブランチのコーパスで実測
npx tsx src/run.ts --replay out/raw.jsonl # 収集済みの行を再集計(API 不要) score conf cost task
1.71 0.56 4s RUN fmt-check
1.68 0.52 6s RUN docs-links
0.05 0.93 22s skip typecheck-web
0.02 0.96 320s skip e2e-web
p(this change alters behaviour) 0.63 p(nothing needs running) 0.18
5 of 26 tasks: 1.7m machine / 1.1m wall (everything: 18.5m / 7.2m, 91% saved)
just fmt-check docs-links docs-build lint
合成した monorepo での実測は docs/23: 検出 15/15 を保ったまま machine time 68.6%(閾値 1.25 で 75.8%)削減、 1 ブランチ 0.17 秒・$0.000131。静的な affected 判定は 30.9% しか削れず、 glob だけにすると 14/15 に取りこぼします。 17 との一番の違いはゴール文を渡さないことで、 その条件だと 17 §5 の 「文脈は払い損」が反転します(docs/23 §4)。
このリポジトリ自身でも測りました(docs/23 §12)。
根の justfile がこのリポジトリのチェックをタスクグラフにしていて、
15 個の実編集を適用して 20 レシピ全部を走らせ、終了コードを正解ラベルにしています
(規則ではなく実測)。結論は 2 つ反転しました:
- 検出は 18/18(見逃し 0)。ただしこのリポジトリでは glob だけの無料の静的判定に負ける (80.2% 対 65.9%)—— 差はモデルではなくグラフの扇形で、 共有パッケージから下流に伝播する形でないと静的解析は悲観的になりません
- 15 個のうち 9 個は何も壊しません。 2 実装の丸めの食い違い(コメントが警告している箇所)も
max_choices = 255も誰も固定していない —— フィルタの精度ではなく スイートの穴が出ました - 効いたのは doc コメント 1 行。「何をするか」を「何を守るか」に書き直すだけで 削減 53.3% → 65.9%(過剰選択 31/45 → 18/45)
11. pi エージェントとして動かす —— pi/
§7 は Claude Code の hook でした。同じ判断を Pi
の拡張として動かす部品は pi/ にまとめてあります。
packages/jev-<name>/src/pi.ts の 6 つがその実体で、
pi/ はそれを「1 コマンドで入る 1 つのもの」に組み立てたものです。
cd pi && npm install # 6 パッケージをローカルから symlink(コピーではない)
npm run probe # どの拡張がどの seam を取るか、実測。API キー不要
npm test # 12 件。API キー不要
npm run collide # 両方入れたら何が 2 回走るか、Pi のランナーで実測
npm run load # Pi 自身の resolver に読ませる(6 パッケージ + 2 プロファイル)
pi install ./pi/components # 5 つを別々に
pi install ./pi/resident # または jev-hermes 1 つ(同じ 5 つを内包)。両方は不可
pi install ./packages/jev-guard # 1 つだけ入れることもできる2 つのプロファイルは排他です —— jev-hermes は guard も自前で持つので
resident は components の代替です。両方入れると 5 seam 全部で衝突し、
ask に落ちたコマンドはユーザに確認ダイアログを 2 回出します
(npm run collide が Pi のランナーで実測。block されるコマンドは
最初の gate で短絡して 1 回、削除も 1 回 —— 2 倍になるのは ask だけでした)。
そして npm 上の jev-guard / jev-model-router / jev-compact は別人のパッケージです
(残り 4 つは 404 —— このリポジトリのものは未公開)。
だから入れ方はローカルパスだけで、pi/ の依存もすべて file: パス、
バージョン範囲を書くとテストが落ちます。
6 つのパッケージ README のレシピもそれに合わせて直してあり、
コードブロックに npm:<このリポジトリの名前> が現れたらテストが落ちます。
詳細は pi/README.md。
12. MOBA —— mizchi/jev-playground-moba に移しました
3v3(02・10・11・12)と、 5v5 と採点できるベンチマーク(56)は、 コード・レポート・記録した答え・残作業ごと jev-playground-moba に移しました(2026-09)。 他の評価と混ざって大きくなりすぎたためで、MOBA のファイルの git の履歴も向こうに引き継いでいます。
レポートの番号は向こうでもそのままなので、このリポジトリでは 02・10・11・12・56 が欠番です。
向こうには続きの 1 本(LoL と照らした 64-moba5-lol)があり、こちらの 64 とは別物です。
experiments/browser-chaos は「Jev にブラウザを操作させるとき、何を渡すか」だけを
変えながら実アプリ(付属のチェックアウト SPA)を歩かせる実験です。
05 から始まって
57–62 の 6 本になりました。
核は 1 リクエストで「どの操作をするか」と「操作ごとの target」を同時に聞くことです。
CLICK の head にはクリックできる要素だけ、SELECT の head には
index:option の対だけが入り、選ばれた操作が指す head だけ読んで残りは捨てます。
// 1 リクエスト。operation 1 問 + 空でない head ごとに 1 問。読むのは 1 つだけ。
{ operation: choice(["CLICK", "TYPE_TEXT", "CLEAR", "SELECT", "DONE"]),
CLICK: choice({ "3": "Continue to delivery", ... }),
SELECT: choice({ "7:1": "shipping = express", "7:2": "shipping = standard", ... }) }捨てる head の分は無駄になりません —— 操作が決まる前に選んだ target が、 決まった後に選んだものと全実行で同一でした。実行された head は全一致で、 しかも argmax が同じなのではなく分布が同じ(本番 12/12 は TV = 0.000、 敵対盤面 21/21 は平均 0.003 —— 差は head が 1 つしかない盤面だけ)。 リクエストは半分、モデル壁時計は −53%。理由は曖昧さが「操作」側にしかないからで、 全操作が必要な画面でも operation は 0.55、その target は 1.00 です。 4 盤面から壊しにいって壊れませんでした(61 §8)。
cd experiments/browser-chaos && npm install
npx tsx src/check-fanout.ts # action space・掃引・trap の採点(モデル不要)
npx tsx src/check-retrieve.ts # 候補検索の recall@k をオフラインで(モデル不要)
npx tsx src/check-cache.ts # action キャッシュのキーと閾値(モデル不要)
TYPESAFEAI_API_KEY=... npx tsx src/run-fanout.ts --select many # 4 アーム比較
TYPESAFEAI_API_KEY=... npx tsx src/run-adversarial.ts --fixture slots-hard
TYPESAFEAI_API_KEY=... npx tsx src/run-ablation.ts # leave-one-out
TYPESAFEAI_API_KEY=... npx tsx src/run-ablation.ts --clear # CLEAR の有無モデル不要の検査が 7 本あります(check-fanout check-retrieve check-cache
check-overlay check-bugs check-code-map check-coverage)。
API キーが要るのは run-* だけです。
他実装 4 つ(jev-ultrafast / playwright-mcp / stagehand / browser-use)を決定点でコードから読んで、 移せる機構を 1 つずつ実装して測りました。採用 2・却下 3 です:
| 移した機構 | 出どころ | 結果 |
|---|---|---|
| 型付き fan-out | jev-ultrafast | 採用 — 判断は同一でリクエスト半分、6 択で 5 手短い |
clear フラグ |
browser-use | 採用 — 無いと 0/2。fill() は置換なので値の変更には要らないが、空という終状態は他のどの操作でも表現できない |
候補検索(browser_find) |
playwright-mcp | 却下 — ゴール語彙との一致で絞ると recall@20 が 1/10(何もせず先頭 20 件なら 10/10) |
| action キャッシュ | stagehand | 却下 — 同一ページなら 10/10 再生・トークン 0。ただし注文を記録しないアプリでも 10/10 再生してゴール到達し ordered: no で終わる |
視野絞り + SCROLL |
playwright-mcp | 却下 — トークン −67% は本物だが「答えが今の画面にある」を担保に借りていて、SCROLL では返せない(効いていた盤面で 2/2 → 0/2) |
採用と却下はきれいに割れました。採用した 2 つはどちらも action space の表現力を上げる側で、 却下した 3 つはどちらも候補や質問を削って安くする側です。 → 精度を買えるのは前者だけで、後者が買えるのはトークンでした。
そして一番効いたのは、実は fan-out ではなく足りない操作を足すことでした。 「空にする」を操作として持たないだけで 0/2・43% 多くトークンを払ってルートを 3 周期し、 しかも対象フィールドに一度も触りません —— 必要な操作が無いとき、ドライバは「正しい要素に間違ったことをする」のではなく 画面を出て別の経路を探しに行きます。 62 §6.7
パターンとしてまとめたものは docs/62 §5、 やってはいけないこと一覧は docs/README.md にあります。
- 料金(2026-09 時点): 入力トークン
$0.042 / MTok、出力トークン無料。15×15 の五目並べ 1 局で入力約 5.8 万トークン(≈ $0.0024)程度。 - Jev の応答は毎回 1 回の
POST /v1/systemone(約 0.5 秒/手)。 - GIF エンジン(
WGYo90/moonbit-gif)はバリデータ上は有効な GIF を出力しますが、同エンジンのデコーダーは自分の出力を再デコードできません(標準のブラウザ/ビューアでは再生可能)。 - 参考: Jev に関する解説は Introducing System One Models & Jev(TypeSafe AI ブログ)、API 仕様は docs.typesafe.ai と
https://api.typesafe.ai/openapi.json。