Skip to content

ツールシステム:Map 登録と呼び出し

源码版本v2026.6.11

責務

ツール (tool) は agent メインループで唯一「手を動かせる」出口です:ファイル読み取り、コマンド実行、編集書き込み、外部 API 呼び出し,すべてツールの形式でモデルに公開されます。OpenClaw は各ツールを Map<string, AgentTool> で格納し,key がツール名,value がディスクリプタ (descriptor) とエグゼキュータ (executor) を含むオブジェクトです。モデルが応答内に tool_use ブロックを吐き出したとき,メインループは名前で Map から該当ツールを探し,そのエグゼキュータを呼び,結果を tool_result ブロックに包んで次ラウンドの対話に戻します。

ツールシステム自体はビジネスロジックを実行せず,4 件事だけを担当します:登録(プラグイン、ビルトイン、SDK カスタムツールを 1 枚の表に)、フィルタ(allow/deny ポリシー、availability シグナルで不可視なものを筛落とす)、ラップ(各ツールに before/after hook を取り付け)、スケジュール(モデルが tool_use を産出した後,正しい executor に落とす)。本当に「事をする」コードはツール自身の executor にあり,システムはそれをモデルに接続するだけです。

設計動機

なぜもっと派手な構造ではなく Map を使うのか?ツール呼び出しのホットパスが極めて単純だからです:モデルが名前を渡し,システムが executor を探す。Map の get(name) は O(1) で,名前による重複排除が自然な制約になります——同じセッションに 2 つの read ツールがあってはならず,さもなくばモデルは誰を呼ぶべきか分かりません。refreshToolRegistry(agent-session.ts refreshToolRegistry:L2414-L2511) は builtin、extension、custom の 3 つのソースをマージして重複排除し,最後に新しい Map を作って古いものを置き換えます。この「増分ではなく再構築」方式でポリシー変更(例:ユーザが一時的にあるツールを無効化)が即時反映され,増分更新の漏れを気にする必要がありません。

もう一つの動機はディスクリプタとエグゼキュータの階層分離です。ToolDescriptor はプランナー層 (planner) 向けの公開説明:名前、入力 schema、owner、availability 式。ToolExecutorRef が実行時にどの実行体に落ちるか(core/plugin/channel/mcp)。この階層化によりプランナー層は「このツールが現在看えるか」を実行体を読み込まずに評価でき,プラグイン作者も executor 実装を暴露せずにツールを宣言できます。プランナー(planner.ts buildToolPlan:L40-L67) はランタイムに触れずに可視/不可視 (visible/hidden) ツールを二分できます。

主要ファイル

データフロー

ツール登録は 3 つのソースから合流(agent-session.ts Map を再構築:L2426-L2486):

typescript
const registeredTools = this.currentExtensionRunner.getAllRegisteredTools();
const allCustomTools = [
  ...registeredTools,
  ...this.customTools.map((definition) => ({
    definition,
    sourceInfo: createSyntheticSourceInfo(`<sdk:${definition.name}>`, { source: "sdk" }),
  })),
].filter((tool) => isAllowedTool(tool.definition.name));
// ...
const toolRegistry = new Map(wrappedBuiltInTools.map((tool) => [tool.name, tool]));
for (const tool of wrappedExtensionTools) {
  toolRegistry.set(tool.name, tool);
}
this.toolRegistry = toolRegistry;

isAllowedTool が第一の関門:ビルトインツールが disableBuiltInTools で切られているか,ツール名が allowedToolNames ホワイトリストになければ,Map に入りません。builtin と extension はどちらも同じ wrapRegisteredTools(src/agents/sessions/agent-session.ts:L2469-L2480) を経由することに注意。このラップは before/after hook、エラーキャッチ、ログ埋め込みを統一的に施し,以降の executor 呼び出しでこれらのロジックを繰り返す必要がありません。

プランナー層はより手前で,ToolDescriptor を操作し,executor には触れません(planner.ts buildToolPlan:L40-L67):

typescript
export function buildToolPlan(options: BuildToolPlanOptions): ToolPlan {
  const descriptors = options.descriptors.toSorted(compareDescriptors);
  assertUniqueNames(descriptors);

  const visible: ToolPlanEntry[] = [];
  const hidden: HiddenToolPlanEntry[] = [];

  for (const descriptor of descriptors) {
    const diagnostics = [
      ...evaluateToolAvailability({ descriptor, context: options.availability }),
    ];
    if (diagnostics.length > 0) {
      hidden.push({ descriptor, diagnostics });
      continue;
    }
    if (!descriptor.executor) {
      throw new ToolPlanContractError({
        code: "missing-executor",
        toolName: descriptor.name,
        message: `Visible tool descriptor has no executor ref: ${descriptor.name}`,
      });
    }
    visible.push({ descriptor, executor: descriptor.executor });
  }

evaluateToolAvailability が各 descriptor の availability 式を評価します:auth 欠落、config 未記入、env 未設定、プラグイン未起用、context 不一致,いずれかのシグナルがヒットすればツールを hidden バケットに入れ diagnostics を付けます。visible バケットに「executor がない」descriptor が出現すると,プランナーは missing-executor をスローします——可視は即ち呼び出し可能でなければならず,これが契約です。

モデルが応答内に tool_use ブロックを産出すると,メインループは agent-session.ts installAgentToolHooks:L497-L551 で登録された hook を走ります:

typescript
this.agent.beforeToolCall = async ({ toolCall, args }) => {
  const runner = this.currentExtensionRunner;
  return await this.runWithSessionWriteLock(async () => {
    if (!runner.hasHandlers("tool_call")) {
      return undefined;
    }
    try {
      return await runner.emitToolCall({
        type: "tool_call",
        toolName: toolCall.name,
        toolCallId: toolCall.id,
        input: args as Record<string, unknown>,
      });
    } catch (err) {
      if (err instanceof Error) {
        throw err;
      }
      throw new Error(`Extension failed, blocking execution: ${String(err)}`, { cause: err });
    }
  });
};

beforeToolCall はプラグインに拦截の機会を与えます:undefined を返せば通し,上書き値を返せば入力を置換,エラーをスローすれば直接実行を阻断します。afterToolCall も同様ですが,executor 完了後にトリガーされ,tool_result の content を書き換えたり isError を true にしたりできます。hook は runWithSessionWriteLock を経由し,before/after 中にセッション状態が並発書き換えされないことを保証します。

境界と失敗

  • 重名は直接スロー:assertUniqueNames はプランナー段階で重複ツール名を拦截し,duplicate-tool-name をスローします——モデルに事後誰を呼ぶべきか困惑させるより安価です。src/agents/sessions/agent-session.ts:L2434-L2450 の方は「後書きが前書きを上書き」で,extension ツールが同名 builtin を上書きします,これは意図的です。しかしプランナー層は「重名を見たらスロー」です。2 層のルールは異なり,Map 層はマージを完了した後,プランナー層は契約検証だからです。
  • visible には executor が必須:descriptor が availability を通過したと宣言しながら executor フィールドを記入しないと,プランナーは missing-executor をスローします。これは「呼べると宣言したが実装がない」という宙に浮いたツールがモデル前に漏れるのを防ぐためです。
  • availability シグナル:auth/config/env/plugin-enabled/context の 5 種シグナルは allOf / anyOf ブール式に組み合わせられ(src/tools/types.ts:L33-L50) ます。いずれかのシグナル失敗で hidden バケットに入り,diagnostics が reason + message を与え,あるツールが突然見えなくなる理由のデバッグに役立ちます。
  • hook スローで実行阻断:beforeToolCall がスローしたエラーは runWithSessionWriteLock でキャッチされて上に伝播し,メインループはこの tool_use を失敗とマークします。afterToolCall がスローしたエラーは結果書き換えにだけ影響し,executor は既に実行完了しています。
  • Map 再構築のコスト:refreshToolRegistry は毎回 new で Map を作り,無駄に見えますが,ツール数は通常数十個で,再構築は増分状態を保守するよりずっと簡単です。セッション級状態の一貫性が微最適化より優先されます。
  • アクティブツールと登録表は分離:toolRegistry は「どのツールが存在するか」,activeToolNames は「今回の対話でどれを使うか」です。setActiveToolsByName(src/agents/sessions/agent-session.ts:L916-L931) は Map に存在する名前だけを取り出して agent.state.tools に入れ,欠落名は黙ってスキップされます——呼び出し側がツール未登録時にハードエラーではなくグレードダウンできます。

まとめ

ツールシステムは agent 唯一の「手足」:Map 登録 + プランナーのバケット分け + before/after hook ラップ + executor の本当の実行。descriptor/executor の階層分離によりプランナー層は純関数で可用性を評価しランタイムに触れません。Map 再構築ポリシーでツールセット変更が即時反映されます。モデルが tool_use を産出し,システムが Map を引き,hook を走り,executor を呼び,tool_result を還流する——このパスは Agent メインループ で毎ラウンド走るホットパスです。スキル との違いは:スキルは Markdown ガイドでモデル自身が読むか決め,ツールはシステムが登録した関数でモデルは名前を呼ぶだけです。プラグインはツールを登録でき(プラグイン 参照),MCP server が公開するツールも同じ Map の項目としてブリッジされます(MCP 参照)。

公式資料:Tools ドキュメント · README