Codemodeとは何か

Codemode はツール使用を、一連の構造化された関数呼び出しではなく、モデルが記述して実行するコードへと変える。Armin Ronacher が、これがツール設計にもたらす変化、計画と実行の分業、そして各部分がどのように編成されるかを説明する。

日本語
コピー

2026年10月6日

1年ほど前、私はこの場でいくつかの記事を書き、カスタムツール(あるいは MCP サーバー)をコンテキストに読み込ませるのではなく、もっとスクリプトを直接使うよう勧めた。中でも中心となる記事が Code Is All You Need で、ほかにも MCP needs code を書いた。Pi 1.0 では、Codemode を通じて MCP サポートを追加した。ある意味では時間の問題だったが、一部の人には少し意外に映るかもしれない。そこでこのブログでは、これが何を意味するのか、ついでに私の考えの更新について書いておきたい。

ツールとは何か

Pi のような harness が LLM に呼び出し可能なツールを提供するとき、渡しているのはツール定義の集合であり、サーバー側で何らかの token 構造に変換される。モデルが特定のツールを呼ぶよう促されるかどうかは、強化学習の結果だ。詳しく知りたい人は、以前この話題を書いた。

私たちが CLI と bash を強く推す理由の一つは、呼び出しを組み合わせやすいこと。もう一つは、モデルが訓練の中でファイルシステムの仕組みを学んでいることだ。だから echo foo > /tmp/test.txt のようなツールを呼び出したとき、モデルはその呼び出しの後、/tmp に test.txt というファイルが増えていることも同時に理解している。

だが bash には根本的な制約がある。組み合わせられるのは、実行できるプログラムだけだ。世の中には、プログラムではないが LLM のネイティブなツールであり、それ以外の形を取れないものがある。

最もわかりやすい例は read や view_image だ。マルチモーダルモデルが画像を読むのに cat は使えない。harness が実際の画像データを LLM のプロトコルに注入しなければならないからだ。

もう一つ直感的な例がサブエージェントだ。サブエージェントを作成しオーケストレーションするには、harness が提供するツールを避けて通るのは難しい。理論上は、エージェントが CLI ツールを提供し、環境変数と Unix socket で外側の harness と通信することもできるが、かなり粗削りな仕組みになる。しかも別の問題もつきまとう。コードがどこで動くのか、という問題だ。

脳と手

これを理解するには、各パーツがそれぞれどこで動いているかをはっきりさせる必要がある。通常、二つの異なるシステムが関わる。一つ目は脳、つまり harness だ。あるマシン上で動き、信頼できる。二つ目は多くの場合同じマシンだが、ツールが実際に実行される場所、つまり手だ。Pi ではこれを実行環境と呼んでいるが、すべての操作の対象だと考えればいい。

重要なのは、harness という脳と、bash を動かしツールを実行する対象環境との間に境界線があることだ。

二つに分けることには、かなり重大な帰結が伴う。まず、異なるファイルシステム上で動き、信頼レベルも異なる。たとえばGondolin のようなサンドボックス方式を使う場合、bash の側はきちんとサンドボックス化されるが、harness 自体はそうではない。

Harness をオーケストレーションする

ここで Codemode が実際にやっていることが見えてくる。LLM が複雑な操作を、実行環境側ではなく harness 側で表現しオーケストレーションできるようにするのだ。Codemode は harness の中で動き、独自のサンドボックスを持つ。Pi の場合、WASM ランタイム上の QuickJS で動き、意図的にさまざまな制限がかけられている。ネットワークなし、ファイルシステムなし、タイマーなし、メモリは限られる。唯一の手段は、さらにツールを呼び出すことだ。Codemode が Scheme や別の言語で動くという設計も考えられる。

Codemode に馴染みがなければ、要するに何らかの言語の内部からツール呼び出しを発行する仕組みで、私たちの場合は JavaScript だ。これにより、呼び出しを LLM のコンテキストを経由せずに組み合わせられる。名前は Cloudflare の友人たちによるもので、彼らが考案した。

たとえば、bash 呼び出しを LLM 内で通常のツール呼び出しとして発行すると、末尾 2000 行だけがコンテキストに渡され、それ以上欲しければ agent が自分でオーバーフローファイルを見に行くしかない。しかし agent が Codemode 経由でこの呼び出しを行えば、Codemode 側はより大きな出力を構造化された形で受け取れる。

何より、Codemode は JavaScript なので、agent が並行操作や基本的なワークフローを表現できる。よくある使い方としては、まずあるツールのレスポンスから 5 ~ 10 個の項目を調べてどんな形かを把握し、それから残りの n 個の項目を処理する Codemode スクリプトを書く、という流れだ。

Codemode では transcript に状態を置くこともできる。つまり、ある Codemode 呼び出しがデータを保存し、セッション内の次の呼び出しがそれを読み戻せる。念のため言っておくと、これはサンドボックス内ではなく harness のホスト上だ。

Pi では、Codemode によって、Pi の従来のインターフェースではそもそも意味を持たない呼び出しも発行できる。たとえば画像モデルで画像を生成したい、あるいは単発の分類モデルでテキストを分類したい。こうした Pi API は Codemode 経由で公開されるが、通常のツールにはしない。ツールにしてもコンテキストを無駄に食うだけだからだ。

実際の姿

ここまで話してきたので、具体的に見たほうがいい。以下では、私が最近の Pi セッションで Codemode を呼び出した実際の例を順にたどる。このコードは一行も人間が書いたものではない。すべて実際の Pi セッションから来ており、読みやすくするためにインデントを整えただけだ。Agent は自然に Codemode を使い始める。こうしたタスクではモデルがもともとこのツールを自然に選ぶか、ユーザーがそう指示するかのどちらかだ。

Codemode は Pi ではデフォルトでは MCP が有効なときしか動かないが、設定で "defaultTools": ["+codemode"] を使えばオンにできる。Pi に頼んで有効化してもらえばいい。

画像を生成する

まずは単純な画像生成から。画像生成は AI SDK のコア層で Pi がサポートしている機能だが、agent が呼び出せるツールではない。以前は画像モデルを使うには専用の拡張を書くか、agent に node を走らせて内部の画像 API を叩かせるしかなかった。Codemode では内部モデル API をかなり露出させているので、agent が直接使える:

const [painter] = await models.getAvailableOfType("image");
const result = await models.generateImages(painter, {
input: [{ type: "text", text: "A cute little puppy sitting on a grassy " +
"lawn, soft natural light, photorealistic" }],
});
if (result.stopReason !== "stop") return result.errorMessage;

for (const block of result.output) {
if (block.type === "image") image(block);
else text(block.text);
}

image() の呼び出しは画像を image content として LLM に返す点に注意。harness 側では、これをそのまま agent に渡しつつ、一時的な成果物としてディスクにも書き出す。agent が後でこの画像を bash に渡せるようにするためだ。

分類

分類モデルも似たようなものだ。たとえば Jev。これらも通常のツール経由で agent のワークフローに組み込むのには向かない。Codemode では専用ツールを作るのではなく、agent が直接 AI SDK に手を伸ばして呼び出す。以下は Jev で GitHub issue を一括処理し、手早く感情分析をかけた例:

const jev = await models.getModelOfType("classifier", "typesafe", "jev-latest");
const r = await tools.bash({
command: "gh issue list --state open --limit 100 " +
"--json number,title,body,comments",
});
const issues = JSON.parse(r.output);

const results = await Promise.all(issues.map(async (issue) => {
const res = await models.classify(jev, {
state: {
title: issue.title,
body: (issue.body || "").slice(0, 4000),
comments: issue.comments.slice(-5).map(c => c.body.slice(0, 800)),
},
questions: {
sentiment: {
type: "choice",
instructions: "What is the overall sentiment of the author towards pi?",
criteria: {
positive: "Appreciative, happy, constructive praise",
neutral: "Matter-of-fact report or request without emotion",
negative: "Frustrated, annoyed, upset, or angry",
},
},
frustration: {
type: "score",
instructions: "How frustrated is the reporter?",
criteria: ["not at all", "mildly", "clearly frustrated", "very angry"],
},
kind: {
type: "choice",
instructions: "What kind of issue is this?",
criteria: {
bug: "Bug report or regression",
feature: "Feature request or enhancement",
question: "Question or support request",
other: "Docs, discussion, meta, spam",
},
},
},
});
if (res.stopReason !== "stop") {
return { n: issue.number, title: issue.title, error: res.errorMessage };
}
return { n: issue.number, title: issue.title, ...res.answers };
}));

store("sentiment_results", results);
return results
.filter(r => !r.error)
.sort((a, b) => b.frustration.score - a.frustration.score)
.slice(0, 12)
.map(r => `#${r.n} ${r.frustration.score.toFixed(2)} [${r.kind.choice}] ${r.title}`);

上の例では store() も呼んでいる。これはこの実行の結果をセッション記録に書き込むもので、次に Codemode を呼んだときに必要ならこの結果を読み戻せる。

ここでの Promise.all は問題ない。Pi 自体が並行するツール実行の総数を 4 に制限し、残りはキューで待たせているからだ。

もっと踏み込んだ例は、Jev でゲームエンジンを動かしてデバッグするもの:

Jev でゲームをデバッグする Codemode

自分の tankctl コマンドを把握していて、それを中心に最小限の harness を素早く組み、ゲームループを回してユーザーの問題を切り分ける。30 ステップのループを構築している点に注目。各ステップでゲームエンジンに戻って現在の状態をテキストでダンプし、また Jev に戻って次の行動を決める:

const jev = await models.getModelOfType("classifier", "typesafe", "jev-latest");
const tank = async (cmd) =>
(await tools.bash({ command: `tools/tankctl "${cmd}"` })).output;
await tank("start --map assets/maps/night_arena.map");

const questions = {
action: {
type: "choice",
instructions: "You control the tank '@' in a top-down tank game. " +
"Choose the best next action.",
criteria: {
attack: "an enemy has line of sight to you and you can fire at it",
approach: "no enemy has line of sight; drive toward the nearest enemy",
dodge: "an enemy shot is heading at you and will hit soon",
powerup: "a powerup is close and no enemy threatens you",
},
},
};

function commandFor(choice, st) {
const p = st.player;
const enemy = st.enemies.filter(e => !e.dead)
.sort((a, b) => (b.los - a.los) || (a.dist - b.dist))[0];
if (choice === "attack" && enemy) {
return `fire_at tank ${enemy.id}; frames 30 until clear,damage,kill`;
}
if (choice === "dodge") {
// move perpendicular to the closest incoming shot
const s = st.projectiles.filter(s => !s.yours)
.sort((a, b) => a.eta - b.eta)[0];
const dir = s && Math.abs(s.vel[0]) > Math.abs(s.vel[1])
? (p.pos[1] > s.pos[1] ? "+down" : "+up")
: (p.pos[0] > (s ? s.pos[0] : 0) ? "+right" : "+left");
return `input ${dir}; frames 20 until damage; input stop`;
}
const powerup = st.powerups.filter(u => u.available)
.sort((a, b) => a.dist - b.dist)[0];
if (choice === "powerup" && powerup) {
return `goto ${powerup.pos[0]} ${powerup.pos[1]} 180`;
}
return enemy ? `goto ${enemy.pos[0]} ${enemy.pos[1]} 90` : null;
}

const log = [];
for (let step = 0; step < 30; step++) {
const st = JSON.parse(await tank("state"));
if (st.state !== "playing") break;
const threats = st.projectiles
.filter(s => !s.yours && s.miss_dist < 1.5 && s.eta < 1.5)
.map(s => `incoming shot dist ${s.dist} eta ${s.eta}s`)
.join("\n") || "no incoming shots";
const r = await models.classify(jev, {
state: { map: await tank("view 8"), threats, hp: st.player.hp },
questions,
});
if (r.stopReason !== "stop") {
log.push(`#${step} classifier error: ${r.errorMessage}`);
break;
}
const choice = r.answers.action.choice;
const cmd = commandFor(choice, st);
if (!cmd) break;
log.push(`#${step} hp=${st.player.hp} ${choice} -> ${await tank(cmd)}`);
}
return log.join("\n");

MCP Server を呼び出す

最後に、Codemode は MCP server の呼び出しに明らかに向いている。MCP ツールは LLM に一切露出させていないので、agent はまず提供された API を使って Codemode 内でツール検索をかけ、接続済みの server で何ができるかを調べる。この漸進的な発見のおかげで、MCP の仕組みが今日の多くの場面で十分実用的になっている。

たとえばこの例では、agent がいきなり Sentry MCP を探しに行っている。ツールを先に発見することすらしていない。おそらく RL の過程で Sentry MCP がどんなものか既に知っていたのだろう。とはいえ Sentry server が使えると知ったのは、システムプロンプトに注入された内容からだ。完全に当てずっぽうではない。

const orgs = await tools.mcp__sentry__find_organizations({});
const { organizations } = orgs.structuredContent;
const results = await Promise.allSettled(organizations.map(org =>
tools.mcp__sentry__find_projects({
organizationSlug: org.slug,
regionUrl: org.regionUrl,
})
));
return organizations.map((org, i) => {
const r = results[i];
if (r.status !== "fulfilled") return { org: org.slug, error: String(r.reason) };
if (r.value.isError) return { org: org.slug, error: r.value.content };
return {
org: org.slug,
projects: r.value.structuredContent.projects.map(p => p.slug),
};
});

現代の MCP は闘いだ

本当はここで MCP の話をあまりしたくないのだが、MCP は Codemode から大きな恩恵を受けられるプロトコルではある。問題の一端は、実践における MCP がしばしば Codemode を(まだ?)使っていない harness を前提としていることだ。ただ風向きは変わりつつある。当面の場当たり的な杖になるのが Cloudflare のやり方で、MCP server の内部に Codemode を実装する。だがこうなると Codemode の入れ子になり、かなりひどい。JSON の二重エスケープが発生し、小さいモデルは簡単に混乱する。しかも内側のコードから外側のツールを呼べない。Pi で Cloudflare の MCP server を使うと、agent は JavaScript を書き、その JavaScript をさらに別の JavaScript に詰め込むことになる。まったく理想的ではないが、なぜこうなったかは理解できる:

const accRes = await tools.mcp__cloudflare__execute({
code: `async () => {
    const r = await cloudflare.request({ method: "GET", path: "/accounts" });
    return r.result.map(a => ({ id: a.id, name: a.name }));
  }`,
});
const accounts = JSON.parse(accRes.content.map(c => c.text).join(""));

const out = [];
for (const account of accounts) {
const r = await tools.mcp__cloudflare__execute({
account_id: account.id,
code: `async () => {
      const r = await cloudflare.request({
        method: "GET",
        path: \`/accounts/\${accountId}/workers/scripts\`,
      });
      return r.result.map(s => ({ id: s.id, modified: s.modified_on }));
    }`,
});
out.push({ account: account.name, workers: r.content.map(c => c.text).join("") });
}
return out;

MCP への要望

最後に締めくくりとして、Codemode は今 MCP とどれだけうまくやれているのか。うーん……あまり良くはない。MCP サーバーがまだ Codemode を使う harness を本気で想定していないからだ(とはいえ、今やほとんどの harness が対応していると思うが)。

うまくやるための提案がいくつかある:

  • 構造化コンテンツ: Codemode は呼び出しが整形された JSON を返すことを期待する。これはサーバー側で返す必要があり、多くのサーバーはまだできていない。MCP の outputSchema の仕組みはこれに向いている。
  • 結果の一貫性: なかなか興味深い失敗例として、MCP サーバーが返すデータが一貫しないケースがある。たとえば結果セットに何件データがあるかでトークン最適化を変えてくる。すると最初に 5 件で探査したときは成功するのに、サーバーが最大バッチを返すと逆に失敗する。
  • 大きなバイナリデータ: MCP はまだ大きなバイナリデータをサポートしていないので、本来なら面白い多くのユースケースがそもそも動かない。ファイルアップロードを presigned URL で MCP 以外の経路に逃がすなど、奇妙な迂回策を編み出すしかない。
  • 組み合わせ可能なツール検索: あるタスクにどのツールを使うべきか、MCP クライアントよりも MCP サーバーのほうがよく分かっているかもしれない。だが harness がツール検索を複数の MCP サーバーに振り分ける良い仕組みが今はない。すべて自発的な実装頼みで、サーバーが増えると機能しなくなる。

Codemode の未来

では、これは何を意味するのか。1年前に CLI を勧めた記事を、これは覆すのか。そうは思わない。むしろ MCP エコシステムは、1年前に指摘した方向性——コード——が正しかったことを裏づけているように見える。ただ Codemode は MCP にとどまらない。harness の内部で強力な仕組みとして働き、agent にもっと自由な表現を与える。

とはいえ、片づけるべき問題は残っている。ひとつは Codemode の永続化が厄介なこと。呼び出しのスナップショットを取るために、永続化ワークフローエンジンの考え方を借りることになるかもしれない。あるいは Starlark の決定性を考えれば、組み合わせ言語としては JavaScript より向いている可能性もある。

画像やバイナリデータ、そしてこのパターンがより小さなモデルでは動かないこと——これらもまだ整理が必要だ。完璧な解ではないのは確かだが、かなり使えるパターンであり、これからもっと使うことになると見ている。

この記事には ai と pi のタグが付いている

コピー / 表示 markdown

© Copyright 2026 by Armin Ronacher.

内容は Creative Commons Attribution-NonCommercial 4.0 International License でライセンスされている。

mail、bluesky、x、github で連絡できる。

github でスポンサーになれる。

詳細:imprint と AI transparency。atom / RSS で購読。

配色:auto、light、dark。

出典: Armin Ronacher's Thoughts and Writings← ホームへ戻る