TL;DR: AIエージェントがライブデータをクエリし、条件付きまたは繰り返しのアクションを実行する必要がある場合、中間結果をモデルに繰り返し返すことはラテンシー(遅延)を増加させ、コンテキストを消費します。私の個人向けAIネイティブ生産性アプリ「Yolo」では、ローカル形態の**Programmatic Tool Calling(プログラマティックなツール呼び出し)**を実装しました。モデルが小さなJavaScriptプログラムを生成し、YoloがそれをQuickJS WebAssemblyサンドボックス内で実行します。プログラムは明示的に公開されたアプリツールのみをオーケストレーションでき、ホストアプリケーションはスキーマ検証、権限、ユーザー確認、実行制限、Undoポリシーを継続して強制します。
1. 真のボトルネック:モデル経由のオーケストレーション
一般的なAIエージェントアーキテクチャでは、反復的な「モデル・ツール」ループが使用されます。
ユーザーリクエスト
│
▼
LLM ──► ツール呼び出し ──► ホストアプリ
▲ │
└──────── ツール実行結果 ◄──────┘
これはよく ReAct スタイルのループとして説明されますが、ReAct にはより具体的な意味があります。モデルが生成する思考プロセス(Reasoning)、アクション(Acting)、外部環境からの観察(Observation)を交互に行う手法です。詳細は ReAct: Synergizing Reasoning and Acting in Language Models を参照してください。
このパターンは、次のようなシンプルなかアクションには問題なく機能します。
「金曜日に『レポート提出』というタスクを作成して。」
モデルは1つのツール呼び出しを生成し、アプリがそれを実行し、モデルが結果を返します。
しかし、後続のアクションがライブデータに依存するようになると、状況はより興味深くなります。
「『Work』カテゴリにある期限切れのタスクをすべて探し、それらを来週の月曜日に変更して。」
直接的なツール呼び出しワークフローは、通常以下のようになります。
- モデルが
list_tasksを呼び出す。 - Yolo が一致するタスク一覧を返す。
- モデルがそれらの結果を検査し、必要な
update_task呼び出しを作成する。 - Yolo が更新を実行する。
- モデルが最終結果をまとめる。
現代のツール呼び出し API は1つのモデルレスポンスで複数の独立したツール呼び出しを返せるため、15件のタスクを更新するのに必ずしも15ターンのモデル対話が必要なわけではありません。独立した呼び出しはまとめて発行され、並列実行されます。
コアとなるボトルネックは**順次依存の深さ(sequential dependency depth)**です。
モデルは中間クエリの結果を受け取るまでデータ依存の呼び出しを構築できないため、依存関係のあるステップごとに別のモデルターンが強制されます。
- Turn 1: モデルが
list_tasksを呼び出してライブデータを取得する。 - Turn 2: タスク一覧を受け取ったモデルが結果を調べ、
update_task呼び出しを並列で発行する。 - Turn 3+: その後の判断がそれら更新の結果に依存する場合、さらに推論ターンが必要となる。
このように、モデルのターン数はデータレコードの件数ではなく、順次意思決定ポイントの数に比例して増加します。
オーバーヘッドの発生源
- モデルラテンシー: 依存するフェーズごとに、ネットワークリクエストとモデル生成の待ち時間が毎回発生します。
- コンテキストの膨張: 中間ツール結果(巨大なタスク一覧や実行トレース)が会話コンテキストに蓄積され、以降の各ターンでトークンを消費します。
- 重複するオーケストレーション: モデルは中間の観察結果を繰り返し次のアクションセットにパースする必要があります。確定的なループやフィルタリングにおいて、コードははるかに明確な制御機構です。
- 中間データの不必要な露出: モデルが必要とするのは最終的な要約だけである場合が多いですが、直接的なツール呼び出しでは中間データセット全体がモデルのコンテキストへ送られがちです。
実践からのサイドバー: Hermes Agent のような自律型エージェントで定期的なログ解析を設定した際、まったく同じ問題に遭遇しました。エージェントがクレンジングされていない生のログを直接コンテキストに引き込むたびに、ノイズを精査するだけで膨大なトークン予算を浪費していました。解決策はシンプルで、ローカルスクリプトでまずログをスクラブおよびフィルタリングし、凝縮されたサマリーのみをモデルに返すことでした。Programmatic Tool Calling はこのパターンを抽出して、ファーストクラスの自動化ランタイム機能として組み込んだものです。
2. Programmatic Tool Calling (プログラマティックなツール呼び出し)
モデルに対して複数の推論フェーズにわたり個々の操作を選択させる代わりに、Yolo は1つのメタツールを公開します。
execute_program
モデルは次のような小さな JavaScript プログラムを記述します。
- アプリケーションデータをクエリする。
- 結果をフィルタリングおよび変換する。
- ループを実行する。
- 条件に応じて分岐する。
- 複数のアプリケーションツールを呼び出す。
- 構造化されたコンパクトな結果を返す。
生成されたプログラムは、可行的な「実行可能制御計画」となります。
このアプローチは Anthropic がドキュメント化した Programmatic Tool Calling の概念に基づいています。モデルがコード実行環境内でツールを呼び出すコードを記述し、中間結果はモデルのコンテキスト外に保持されます。
Cloudflare は極めて類似したアーキテクチャを Code Mode として説明しています。モデルはコード実行ツールを受け取り、型定義されたツールを組み合わせ、結果を処理し、応答に必要な情報のみを返すプログラムを記述します。
広義のアイディアは学術研究にも登場します。CodeAct は実行可能コードをエージェントのアクション空間として扱い、PAL は言語モデル自体にすべての推論ステップを行わせるのではなく、確定的な計算をインタプリタに委任します。
Yolo はこのパラダイムを JavaScript と QuickJS を使用したローカルかつモデルアグノスティックなランタイムに適合させており、設定された異なる LLM が同じオーケストレーションランタイムを利用しつつ、セキュリティはホストアプリケーションによって強制され続けます。
3. 具体的な例
ユーザーが次のようにリクエストしたとします。
「『Work』の期限切れタスクをすべて探し、それらを来週の月曜日に移動して。」
Yolo はモデルにプログラム本体の生成を求めます。実行エンジンはその本体を非同期関数内にラップするため、生成されたコード内では await と return の両方が有効なステートメントとなります。
モデルによって生成された簡略化プログラム本体は次のようになります。
const targetDate = "2026-07-27";
const tasks = await list_tasks({
scope: "overdue",
category: "Work",
});
if (!tasks.ok) {
return {
ok: false,
error: tasks.error,
};
}
let applied = 0;
let queued = 0;
const failures = [];
for (const task of tasks.data) {
const update = await update_task({
task_id: task.id,
due_date: targetDate,
});
if (!update.ok) {
failures.push({
taskId: task.id,
error: update.error,
});
} else if (update.queued) {
queued += 1;
} else {
applied += 1;
}
}
log(
`Matched ${tasks.data.length} tasks: ` +
`${applied} applied, ${queued} queued, ` +
`${failures.length} failed.`,
);
return {
ok: failures.length === 0,
matched: tasks.data.length,
applied,
queued,
failed: failures.length,
failures,
targetDate,
};
ホスト実行エンジンは、即時実行される非同期関数でラップすることで、この生成された本体を評価します。
(async () => {
// 生成されたプログラム本体
})();
モデルの視点からは、ツールインターフェースは普通の非同期 JavaScript です。
const tasks = await list_tasks({ scope: "overdue" });
一般的な実行パスには2つのモデル推論フェーズが含まれます。
- プログラムを生成する。
- プログラム結果を要約する。
重要な違いは、15回のデータベース書き込みが1回の書き込みになるわけではないという点です。上記の例でもタスクごとに update_task を1回ずつ実行しています。
そうではなく、Yolo はループと中間判断を反復的なモデル推論から切り离し、ローカルで実行される構造化プログラムへと移動させています。
bulk_update_tasks のような専用の一括操作APIがあれば、ドメインツールの呼び出し回数をさらに削減できます。Programmatic Tool Calling が最も威力を発揮するのは、1つの固定された一括APIでは綺麗に表現できない柔軟なフィルタリング、条件分岐、構成、あるいはエラーハンドリングがワークフローに必要な場合です。
4. アーキテクチャ
Yolo は Tauri v2、React、TypeScript で構築されたデスクトップアプリケーションです。
モデルの下層において、プログラマティックランタイムは4つの主要レイヤーで構成されています。
┌───────────────────────────────────────────────────────────┐
│ AI モデル │
│ JavaScript プログラムを生成 │
└──────────────────────────┬────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────┐
│ プログラム検証層 │
│ サイズ制限・構文チェック・許可されたエントリーポイント │
└──────────────────────────┬────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────┐
│ QuickJS WebAssembly サンドボックス │
│ │
│ ループ · 条件分岐 · 一時状態 · JSON 処理 │
│ │
│ 公開された機能: │
│ list_tasks · update_task · log │
└──────────────────────────┬────────────────────────────────┘
│ 明示的なホスト関数ブリッジ
▼
┌───────────────────────────────────────────────────────────┐
│ Yolo 標準ツールレジストリ │
│ │
│ スキーマ検証 · 権限チェック · ユーザー確認 · Undo機能 │
└──────────────────────────┬────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────┐
│ SQLite / Tauri / アプリ状態 │
└───────────────────────────────────────────────────────────┘
A. ケパビリティの閉じ込め(Capability Containment)
Yolo の通常フロントエンド環境で eval を使用してモデル生成 JavaScript を実行した場合、そのコードは同じ JavaScript Realm 内に存在するあらゆるオブジェクトにアクセスできてしまいます。
アプリケーションによっては、以下が含まれる可能性があります。
- アプリのグローバル変数
- DOM API
- ブラウザストレージ
- ネットワーク API
- Tauri コマンドを呼び出す関数
Tauri 自体は IPC コマンドに対して独自の権限と実行権限チェックを適用します。Webview 内で JavaScript が動いているからといって、無制限のネイティブアクセスが自動的に与えられるわけではありません。
しかし、メインアプリ Realm で生成コードを実行することは、不必要に広大な攻撃面を晒すことになります。
そのため、Yolo は WebAssembly にコンパイルされた分離 QuickJS インスタンス内でプログラムを実行します。
WebAssembly モジュールは、ホスト環境への暗黙的なアクセス権を持ちません。エンベダーは、インポートする関数やオブジェクトを制御することによって、どの機能を利用可能にするかを決定します。
QuickJS プログラムは以下へのアクセス権を自動的には受け取りません。
- Node.js
- DOM
- ファイルシステム
- ネットワーク API
- Tauri IPC
- 動的パッケージインポート
- Yolo のアプリケーション状態
受け取るのは、Yolo が明示的に組み込んだホスト関数のみです。
これがケパビリティの閉じ込めです。プログラムはループ、変数、配列、条件分岐といった通常の言語機能を保持しつつ、外部への実行権限は極小のアローリストに制限されます。
B. 非同期ホストツールのブリッジ
生成されたプログラムは標準の async/await を使用しますが、底層のブリッジには明示的な取り扱いが必要です。
通常の quickjs-emscripten ホストコールバックは、ネイティブ JavaScript の Promise や通常の JavaScript オブジェクトを単に返して QuickJS に自動採用させることはできません。
サポートされている方法の1つは以下の通りです。
- QuickJS コンテキスト内で Promise を作成する。
- 非同期ホスト操作を開始する。
- 結果を QuickJS の値に変換する。
- QuickJS の Promise を Resolve または Reject する。
- ゲストプログラムが再開できるよう、保留中の QuickJS ジョブを実行する。
ライブラリは、WebAssembly 実行全体が非同期ホスト作業の前後で中断・停止する必要があるケース向けに Asyncify ベースのビルドも提供しています。Asyncify ビルドはサイズ、パフォーマンス、再入可能性、中断制約に関する追加のオーバーヘッドがあるため、選択は慎重に行う必要があります。
以下は、遅延 Promise ブリッジの簡略化バージョンです。
type JsonValue =
| null
| boolean
| number
| string
| JsonValue[]
| { [key: string]: JsonValue };
type ToolExecutor = (
args: Record<string, JsonValue>,
options: { signal: AbortSignal },
) => Promise<JsonValue>;
function installJsonTool(
name: string,
execute: ToolExecutor,
signal: AbortSignal,
): void {
const hostFunctionName = `__host_${name}`;
const hostFunction = vm.newFunction(
hostFunctionName,
(argsHandle) => {
const args = vm.dump(argsHandle) as Record<string, JsonValue>;
const deferred = vm.newPromise();
void execute(args, { signal }).then(
(result) => {
try {
const jsonString = JSON.stringify(result ?? null);
const resultHandle = vm.newString(jsonString);
deferred.resolve(resultHandle);
resultHandle.dispose();
} catch (err: unknown) {
const message =
err instanceof Error ? err.message : String(err);
const errorHandle = vm.newString(message);
deferred.reject(errorHandle);
errorHandle.dispose();
}
},
(error: unknown) => {
const message =
error instanceof Error
? error.message
: String(error);
const errorHandle = vm.newString(message);
deferred.reject(errorHandle);
errorHandle.dispose();
},
);
deferred.settled.then(() => {
vm.runtime.executePendingJobs();
});
return deferred.handle;
},
);
vm.setProp(
vm.global,
hostFunctionName,
hostFunction,
);
hostFunction.dispose();
const toolNameLiteral = JSON.stringify(name);
const hostNameLiteral = JSON.stringify(hostFunctionName);
const wrapperResult = vm.evalCode(`
globalThis[${toolNameLiteral}] = async (args) => {
const json = await globalThis[${hostNameLiteral}](args);
return JSON.parse(json);
};
`);
vm.unwrapResult(wrapperResult).dispose();
}
モデルの視点からは、このブリッジメカニズムは完全に不可視です。LLM はスキーマで定義されたツールインターフェース(await list_tasks(...) など)を使って慣用的な非同期 JavaScript を記述するだけであり、ホストランタイムがハンドルライフサイクルの管理、JSON シリアライズ、ジョブキューの再開を処理します。
5. アプリケーションの安全ポリシーの維持
プログラマティックランタイムは代替のオーケストレーションメカニズムであり、特権的なバックドアではありません。
公開されているすべての関数は、Yolo の既存のツールレジストリを経由してルーティングされます。
スキーマ検証
レジストリは、基盤となる操作を実行する前にツールの引数を検証します。
サンドボックスは、呼び出しが生成コードから発生したという理由だけでツールの入力スキーマをバイパスすることはできません。
権限モード
Yolo は複数の実行モードをサポートしています。
- Plan: 変更を適用せずに、提案される操作を生成して表示する。
- Ask: 変更をキューに入れ、ユーザーに確認を求める。
- Auto: 条件を満たす操作を自動実行する。
ツールがモデルから直接呼び出された場合でも、execute_program 経由で呼び出された場合でも、全く同じモードが適用されます。
ユーザー確認(Human Confirmation)
Plan モードまたは Ask モードでは、変更系ツールは次を返すことができます。
{
"ok": true,
"queued": true
}
Yolo はアプリケーションデータを無言で変更する代わりに、確認プレビューカードを描画します。
破壊的な操作には引き続き二重の確認が必要です。
Undo とバージョンチェック
成功した変更操作は、操作を元に戻すのに十分な情報を含む UndoOp を返すことができます。
Yolo は書き込み後の updated_at タイムスタンプも記録します。Undo 操作を適用する前に、対象のレコードがその後変更されていないかを検証できます。
これにより、過去の Undo 操作がユーザーによる最新の手動編集を上書きしてしまうのを防ぎます。
実装されているセーフガード
無限ループや過度なリソース消費を防ぐため、Yolo は厳格なランタイムセーフガードを強制します。
- メモリとスタック制限: QuickJS のヒープメモリと呼び出しスタック深度に対するハードキャップ。
- 実行タイムアウト: ゲスト実行用の QuickJS 中断ハンドラ経由の硬い制限時間に加え、保留中 I/O に対するホスト側の
AbortSignalキャンセル。 - ペイロードクォータ: 最大プログラムサイズ、総ツール呼び出し数、および出力ペイロードサイズへの制限。
6. セキュリティモデル
WebAssembly サンドボックスは有用ですが、セキュリティ設計の1つの層に過ぎません。
真のセキュリティ境界は以下の複数層で構成されています。
- QuickJS/Wasm 隔離層
- 注入されたホスト関数のセット
- ツール引数の検証
- アプリケーション権限チェック
- 確認ポリシー
- リソースクォータ
- Tauri Capabilities
- 監査および Undo 動作
最も重要なルールは以下の通りです。
生成されたコードは、ユーザーおよびアプリポリシーが意図した以上の権限を絶対に受け取ってはならない。
したがって、ホスト関数ブリッジ自体も攻撃面の一部です。
たとえば、安全な update_task 関数は任意の SQL フラグメントを受け取るべきではありません。検証済みの限定的なオブジェクトを受け取るべきです。
{
task_id: string;
due_date: string;
}
サンドボックスはコードが実行できる場所を制限し、ツールレジストリはコードが実行できる内容を制限します。双方とも不可欠です。
7. 部分的な失敗と冪等性
プログラムは、どれか1つの変更が失敗する前にいくつかの変更を実行する可能性があります。
たとえば:
タスク 1 更新成功
タスク 2 更新成功
タスク 3 更新成功
タスク 4 失敗
タスク 5 未試行
ランタイムはこれを単なる成功または失敗として報告してはなりません。次のような構造化された実行サマリーを返すべきです。
{
"matched": 5,
"applied": 3,
"queued": 0,
"failed": 1,
"notAttempted": 1
}
今後の強化作業
信頼性の高い一括ワークフローのため、Yolo では以下も考慮する必要があります。
- 呼び出しごとの Execution ID
- 冪等性キー(Idempotency Keys)
- リトライポリシー
- 順序付け要件
- 同時変更チェック
- レート制限
- 補償またはロールバック動作
- 最初の失敗後に実行を停止するかどうか
サンドボックスはプログラムを閉じ込めることはできますが、ビジネス操作を自動的にトランザクション化することはできません。
8. 直接的なツール呼び出し vs. Programmatic Tool Calling
以下のように定義します。
- $N$: ドメイン操作の数
- $D$: 順次発生するモデル依存の意思決定フェーズの数
| 項目 | 直接的なツール呼び出し(Direct Tool Calling) | Programmatic Tool Calling |
|---|---|---|
| モデル推論フェーズ | 通常、依存の深さ($D$)に比例して増加(必ずしも要素数 $N$ ではない) | 通常、プログラム生成に1フェーズ、結果要約に1フェーズに固定 |
| ドメインツールの実行 | $O(N)$(一括ツールがない場合) | $O(N)$(一括ツールがない場合) |
| 並列操作 | 呼び出しが独立している場合にサポート | ホストブリッジとビジネスルールが許可する場合に可能 |
| 中間結果 | 頻繁にモデルのコンテキストに入る | 実行環境の内部に保持可能 |
| 制御フロー | モデルのレスポンス間に分散 | JavaScript を通じて明示的に表現 |
| モデルラテンシー | 依存する意思決定フェーズごとに加算 | ローカルコードが中間決定を処理する際に削減 |
| ツールラテンシー | 引き続き存在 | 引き続き存在 |
| 実行オーバーヘッド | ツールシリアライズとモデルオーケストレーション | ツールシリアライズ、サンドボックス起動、VM実行 |
| 失敗処理 | モデルまたはホストが各フェーズを調整 | プログラムが失敗を集約可能だが、生成ロジック自体が誤るリスクあり |
| セキュリティ表面 | ツールスキーマとホストポリシー | ツールスキーマとホストポリシーに加え、サンドボックスとコードブリッジ |
| 最適なユースケース | 小型、固定、レビューが容易な単発アクション | データ依存のループ、フィルタリング、分岐、複数ツール構成 |
したがって、Programmatic Tool Calling がすべての状況で自動的に速くなるわけではありません。
その主な利点は以下の場合に顕著となります。
- 中間データが大きい場合。
- 複数の操作がライブ結果に依存する場合。
- ワークフローにループや条件分岐が含まれる場合。
- すべての中間結果をモデルに返すことが無駄である場合。
- 固定の一括 API では狭すぎる場合。
9. 使用すべきではないケース
以下の場合、直接的なツール呼び出しの方が通常適しています。
- リクエストが1つまたは2つのシンプルなアクションのみを必要とする場合。
- 各アクションを個別にレビューすべきである場合。
- 生成されるプログラムがタスク自体よりも複雑になる場合。
- 信頼できる一括 API が既に存在する場合。
- 操作に強力なデータベーストランザクションが必要な場合。
- ステップ間にモデルがユーザーへ質問する必要がある場合。
- ワークフローにハイリスクな外部副作用が含まれる場合。
たとえば、次のリクエストに対する最適な実装:
「選択したすべてのタスクを完了としてマークして。」
は、単に以下のような呼び出しであるべきです。
complete_tasks({
task_ids: selectedTaskIds,
});
狭い範囲でテスト済みのドメイン操作が既にユーザーの意図を明確に表現している場合、わざわざプログラムを生成する理由はありません。
Programmatic Tool Calling は優れたツール設計を補完するものであり、それに取って代わるものではありません。
結論
Programmatic Tool Calling は $N$ 回のデータベース操作を1回の操作に変換するものではありません。
これは、データ依存のオーケストレーションを反復的なモデル推論から切り离し、制限された環境内で実行される構造化プログラムへと移行させます。
Yolo では QuickJS が JavaScript ランタイムを提供し、WebAssembly が隔離された実行境界の確立を助け、既存のツールレジストリが権限、検証、確認、Undo 動作を保持します。
その結果が以下のハイブリッドアーキテクチャです。
- モデルが意図を解釈し、計画を記述する。
- JavaScript が確定的な制御フローを処理する。
- サンドボックスが環境機能を制限する。
- ホストアプリケーションがすべての副作用に対する権限を保持する。
複雑なエージェントワークフローにおいて、この明確な責任分離は単にツール呼び出し回数を減らすこと以上により重要です。
参考文献
- Yao et al., “ReAct: Synergizing Reasoning and Acting in Language Models”
- Anthropic, “Programmatic Tool Calling”
- Anthropic, “Introducing Advanced Tool Use on the Claude Developer Platform”
- Anthropic, “Code Execution with MCP: Building More Efficient AI Agents”
- Cloudflare, “Code Mode”
- Cloudflare, “Create a Durable Code Mode Runtime”
- Wang et al., “Executable Code Actions Elicit Better LLM Agents”
- Gao et al., “PAL: Program-Aided Language Models”
- quickjs-emscripten Documentation
- WebAssembly Core Specification
- Tauri v2 Capability Reference
- Tauri v2 Runtime Authority