Skip to content

YoloにおけるProgrammatic Tool Calling:QuickJS WASMサンドボックスでのJavaScript実行

Harry
Lang
原文 中文
日本語

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』カテゴリにある期限切れのタスクをすべて探し、それらを来週の月曜日に変更して。」

直接的なツール呼び出しワークフローは、通常以下のようになります。

  1. モデルが list_tasks を呼び出す。
  2. Yolo が一致するタスク一覧を返す。
  3. モデルがそれらの結果を検査し、必要な update_task 呼び出しを作成する。
  4. Yolo が更新を実行する。
  5. モデルが最終結果をまとめる。

現代のツール呼び出し API は1つのモデルレスポンスで複数の独立したツール呼び出しを返せるため、15件のタスクを更新するのに必ずしも15ターンのモデル対話が必要なわけではありません。独立した呼び出しはまとめて発行され、並列実行されます。

コアとなるボトルネックは**順次依存の深さ(sequential dependency depth)**です。

モデルは中間クエリの結果を受け取るまでデータ依存の呼び出しを構築できないため、依存関係のあるステップごとに別のモデルターンが強制されます。

このように、モデルのターン数はデータレコードの件数ではなく、順次意思決定ポイントの数に比例して増加します。

オーバーヘッドの発生源

実践からのサイドバー: 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 はモデルにプログラム本体の生成を求めます。実行エンジンはその本体を非同期関数内にラップするため、生成されたコード内では awaitreturn の両方が有効なステートメントとなります。

モデルによって生成された簡略化プログラム本体は次のようになります。

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つのモデル推論フェーズが含まれます。

  1. プログラムを生成する。
  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 内に存在するあらゆるオブジェクトにアクセスできてしまいます。

アプリケーションによっては、以下が含まれる可能性があります。

Tauri 自体は IPC コマンドに対して独自の権限と実行権限チェックを適用します。Webview 内で JavaScript が動いているからといって、無制限のネイティブアクセスが自動的に与えられるわけではありません。

しかし、メインアプリ Realm で生成コードを実行することは、不必要に広大な攻撃面を晒すことになります。

そのため、Yolo は WebAssembly にコンパイルされた分離 QuickJS インスタンス内でプログラムを実行します。

WebAssembly モジュールは、ホスト環境への暗黙的なアクセス権を持ちません。エンベダーは、インポートする関数やオブジェクトを制御することによって、どの機能を利用可能にするかを決定します。

QuickJS プログラムは以下へのアクセス権を自動的には受け取りません。

受け取るのは、Yolo が明示的に組み込んだホスト関数のみです。

これがケパビリティの閉じ込めです。プログラムはループ、変数、配列、条件分岐といった通常の言語機能を保持しつつ、外部への実行権限は極小のアローリストに制限されます。

B. 非同期ホストツールのブリッジ

生成されたプログラムは標準の async/await を使用しますが、底層のブリッジには明示的な取り扱いが必要です。

通常の quickjs-emscripten ホストコールバックは、ネイティブ JavaScript の Promise や通常の JavaScript オブジェクトを単に返して QuickJS に自動採用させることはできません。

サポートされている方法の1つは以下の通りです。

  1. QuickJS コンテキスト内で Promise を作成する。
  2. 非同期ホスト操作を開始する。
  3. 結果を QuickJS の値に変換する。
  4. QuickJS の Promise を Resolve または Reject する。
  5. ゲストプログラムが再開できるよう、保留中の 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 は複数の実行モードをサポートしています。

ツールがモデルから直接呼び出された場合でも、execute_program 経由で呼び出された場合でも、全く同じモードが適用されます。

ユーザー確認(Human Confirmation)

Plan モードまたは Ask モードでは、変更系ツールは次を返すことができます。

{
  "ok": true,
  "queued": true
}

Yolo はアプリケーションデータを無言で変更する代わりに、確認プレビューカードを描画します。

破壊的な操作には引き続き二重の確認が必要です。

Undo とバージョンチェック

成功した変更操作は、操作を元に戻すのに十分な情報を含む UndoOp を返すことができます。

Yolo は書き込み後の updated_at タイムスタンプも記録します。Undo 操作を適用する前に、対象のレコードがその後変更されていないかを検証できます。

これにより、過去の Undo 操作がユーザーによる最新の手動編集を上書きしてしまうのを防ぎます。

実装されているセーフガード

無限ループや過度なリソース消費を防ぐため、Yolo は厳格なランタイムセーフガードを強制します。

6. セキュリティモデル

WebAssembly サンドボックスは有用ですが、セキュリティ設計の1つの層に過ぎません。

真のセキュリティ境界は以下の複数層で構成されています。

  1. QuickJS/Wasm 隔離層
  2. 注入されたホスト関数のセット
  3. ツール引数の検証
  4. アプリケーション権限チェック
  5. 確認ポリシー
  6. リソースクォータ
  7. Tauri Capabilities
  8. 監査および 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 では以下も考慮する必要があります。

サンドボックスはプログラムを閉じ込めることはできますが、ビジネス操作を自動的にトランザクション化することはできません。

8. 直接的なツール呼び出し vs. Programmatic Tool Calling

以下のように定義します。

項目直接的なツール呼び出し(Direct Tool Calling)Programmatic Tool Calling
モデル推論フェーズ通常、依存の深さ($D$)に比例して増加(必ずしも要素数 $N$ ではない)通常、プログラム生成に1フェーズ、結果要約に1フェーズに固定
ドメインツールの実行$O(N)$(一括ツールがない場合)$O(N)$(一括ツールがない場合)
並列操作呼び出しが独立している場合にサポートホストブリッジとビジネスルールが許可する場合に可能
中間結果頻繁にモデルのコンテキストに入る実行環境の内部に保持可能
制御フローモデルのレスポンス間に分散JavaScript を通じて明示的に表現
モデルラテンシー依存する意思決定フェーズごとに加算ローカルコードが中間決定を処理する際に削減
ツールラテンシー引き続き存在引き続き存在
実行オーバーヘッドツールシリアライズとモデルオーケストレーションツールシリアライズ、サンドボックス起動、VM実行
失敗処理モデルまたはホストが各フェーズを調整プログラムが失敗を集約可能だが、生成ロジック自体が誤るリスクあり
セキュリティ表面ツールスキーマとホストポリシーツールスキーマとホストポリシーに加え、サンドボックスとコードブリッジ
最適なユースケース小型、固定、レビューが容易な単発アクションデータ依存のループ、フィルタリング、分岐、複数ツール構成

したがって、Programmatic Tool Calling がすべての状況で自動的に速くなるわけではありません。

その主な利点は以下の場合に顕著となります。

9. 使用すべきではないケース

以下の場合、直接的なツール呼び出しの方が通常適しています。

たとえば、次のリクエストに対する最適な実装:

「選択したすべてのタスクを完了としてマークして。」

は、単に以下のような呼び出しであるべきです。

complete_tasks({
  task_ids: selectedTaskIds,
});

狭い範囲でテスト済みのドメイン操作が既にユーザーの意図を明確に表現している場合、わざわざプログラムを生成する理由はありません。

Programmatic Tool Calling は優れたツール設計を補完するものであり、それに取って代わるものではありません。

結論

Programmatic Tool Calling は $N$ 回のデータベース操作を1回の操作に変換するものではありません。

これは、データ依存のオーケストレーションを反復的なモデル推論から切り离し、制限された環境内で実行される構造化プログラムへと移行させます。

Yolo では QuickJS が JavaScript ランタイムを提供し、WebAssembly が隔離された実行境界の確立を助け、既存のツールレジストリが権限、検証、確認、Undo 動作を保持します。

その結果が以下のハイブリッドアーキテクチャです。

複雑なエージェントワークフローにおいて、この明確な責任分離は単にツール呼び出し回数を減らすこと以上により重要です。

参考文献

  1. Yao et al., “ReAct: Synergizing Reasoning and Acting in Language Models”
  2. Anthropic, “Programmatic Tool Calling”
  3. Anthropic, “Introducing Advanced Tool Use on the Claude Developer Platform”
  4. Anthropic, “Code Execution with MCP: Building More Efficient AI Agents”
  5. Cloudflare, “Code Mode”
  6. Cloudflare, “Create a Durable Code Mode Runtime”
  7. Wang et al., “Executable Code Actions Elicit Better LLM Agents”
  8. Gao et al., “PAL: Program-Aided Language Models”
  9. quickjs-emscripten Documentation
  10. WebAssembly Core Specification
  11. Tauri v2 Capability Reference
  12. Tauri v2 Runtime Authority
Previous
Rust & Tauriプロジェクトで5分で23GBのストレージを回収した方法
Next
Yolo 開発記録 02:一日を記録することから、一日をクリアに見つめることへ