Claude Media
Tool RunnerでAnthropic APIのツール呼び出しループを自動化する

Tool RunnerでAnthropic APIのツール呼び出しループを自動化する

Anthropic APIのtool_runner()でツール実行ループを自動化する実装をPython/TypeScriptで解説。Agent SDKとの使い分けも扱います。

Tool Runnerとは — 往復を自動化する仕組み

Tool Runnerは、Anthropic APIでツールを使うときの「Claudeがツールを呼ぶ→自分のコードで実行する→結果を返す→また呼ぶ」という往復を、SDK側に肩代わりさせるベータ機能です。往復を自分で書かなくてよくなります。client.beta.messages.tool_runner()(Python)、client.beta.messages.toolRunner()(TypeScript)としてbeta名前空間に生えています。人間の承認を挟みたい、独自ログを取りたい、条件次第で実行を止めたいといった細かい制御が要る場面だけ、手動ループに戻る設計です。

自動化される範囲は4つです。Claudeがツールを呼んだら実行する、リクエストとレスポンスの往復を処理する、会話状態を管理する、型安全性と入力検証を提供する。これらを毎回自分で書いていたコードが、ツール定義とメッセージを渡すだけの数行に縮みます。Python / TypeScript / C# / Go / Java / PHP / Rubyの7つの公式SDKすべてでベータ提供されており、言語による機能差はありますが土台となる考え方はどのSDKでも共通です。本稿ではPythonを軸に実装し、TypeScriptとの違いは別見出しでまとめて扱います。

似た名前の選択肢としてClaude Agent SDKがありますが、両者は別物です。Tool Runnerは素のAnthropic Client SDKの一部で、ツールの中身は引き続き自分で実装します。対してAgent SDKはRead / Write / Bash等の組み込みツールとセッション管理まで込みの独立したエージェント実行基盤です。使い分けは後段の見出しでまとめます。

前提条件 — SDKとAPIキー

動かす前に次の3点を揃えます。

  • ANTHROPIC_API_KEY を環境変数に設定済みであること(Consoleで発行したキー)
  • Pythonなら pip install anthropic 済みで @beta_tool デコレータが使えること
  • TypeScriptで betaZodTool() を使うならZod 3.25.0以上が入っていること(betaTool() を使うJSON Schema方式ならZod自体不要)
pip install anthropic
 
# または TypeScript
npm install @anthropic-ai/sdk zod

最小実装 — ツールを1つ動かす

Pythonでは @beta_tool デコレータを関数に付けるだけで済みます。型ヒントとdocstringからJSON Schemaが自動生成されます。非同期クライアントを使う場合は @beta_async_tool に置き換え、async def で定義します。

import json
from anthropic import Anthropic, beta_tool
 
client = Anthropic()
 
 
@beta_tool
def get_weather(location: str, unit: str = "fahrenheit") -> str:
    """Get the current weather in a given location.
 
    Args:
        location: The city and state, e.g. San Francisco, CA
        unit: Temperature unit, either 'celsius' or 'fahrenheit'
    """
    return json.dumps({"temperature": "20°C", "condition": "Sunny"})
 
 
runner = client.beta.messages.tool_runner(
    model="claude-opus-5",
    max_tokens=1024,
    tools=[get_weather],
    messages=[{"role": "user", "content": "パリの天気は?"}],
)
final_message = runner.until_done()
for block in final_message.content:
    if block.type == "text":
        print(block.text)

ツールの戻り値は文字列でも、テキスト・画像・ドキュメントを組み合わせたコンテンツブロックでも構いません。文字列を返すと自動的に1つのテキストブロックへ変換されるので、画像を返したいときだけコンテンツブロック形式を使います。JSONオブジェクトや数値のような構造化データを返したい場合は、先に文字列へエンコードしておく必要があります。上の例で json.dumps を使っているのはそのためです。

runner はイテラブルです。for message in runner: と回せば、Claudeがツールを呼ぶたびに実行と結果送信を挟みながら、次のメッセージを順に受け取れます。ループはClaudeがツール呼び出しを含まないメッセージを返した時点で自然に終わります。途中経過が要らないなら、until_done() で最終メッセージだけを取り出すのが最短です。

TypeScriptで書く場合の違い

TypeScriptには2つのツール定義方法があります。Zodでスキーマと実行関数をまとめて書く betaZodTool() が推奨で、Zod 3.25.0以上が必要です。Zodを使わずJSON Schemaを直接書きたいときは betaTool() を使いますが、この方式はClaudeが生成した入力をランタイムで検証しないため、run 関数の内側でバリデーションを自分で書く必要があります。

import Anthropic from "@anthropic-ai/sdk";
import { betaZodTool } from "@anthropic-ai/sdk/helpers/beta/zod";
import { z } from "zod";
 
const client = new Anthropic();
 
const getWeatherTool = betaZodTool({
  name: "get_weather",
  description: "Get the current weather in a given location",
  inputSchema: z.object({
    location: z.string().describe("The city and state, e.g. San Francisco, CA"),
    unit: z.enum(["celsius", "fahrenheit"]).default("fahrenheit"),
  }),
  run: async (input) => JSON.stringify({ temperature: "20°C", condition: "Sunny" }),
});
 
const finalMessage = await client.beta.messages.toolRunner({
  model: "claude-opus-5",
  max_tokens: 1024,
  tools: [getWeatherTool],
  messages: [{ role: "user", content: "パリの天気は?" }],
});

最終メッセージの取り方も変わります。Pythonはrunnerオブジェクトの until_done() を呼ぶのに対し、TypeScriptはrunner自体を await するだけで最終メッセージが返ります。ループを自分で回したいときは for await (const message of runner) です。

ループの制御 — 終了条件とストリーミング

runnerは無条件に回り続けるわけではありません。ループの内側で break すれば任意のタイミングで抜けられますし、max_iterations を設定すれば指定した回数で強制的に止まります。7つのSDKすべてがこの値をサポートしており、無限ループを避ける最後の砦です。特に後述する「会話履歴を自分で管理する」パターンを使うときは、ループが自然終了しなくなるリスクがあるため max_iterations を必ず添えます。

ストリーミングも組み込みです。stream=True(TypeScriptは stream: true)を渡すと、各ターンの応答をイベント単位で逐次受け取れます。Pythonでは各イテレーションが BetaMessageStream を返すので、for event in message_stream: でイベントを処理し、message_stream.get_final_message() でそのターンの累積メッセージを得る仕組みです。TypeScriptは同じ構造を finalMessage() で取得します。7つのSDKのうちPHP版だけはストリーミング未対応です。

長時間動かすエージェントタスク向けに、Python・TypeScript・Rubyのrunnerはトークン使用量が閾値を超えると要約を生成するクライアント側の自動コンパクションを持っていました。ただしこの機能はすでに非推奨です。3つのSDKいずれも、代わりにサーバー側のcontext editingへの移行が案内されています。context editingは全SDKで使えます。Go・Java・C#・PHPのrunnerはそもそもクライアント側コンパクションを持ちません。長時間ループを設計するなら、コンテキスト管理はサーバー側の仕組みに寄せるのが現行の推奨です。

エラー処理 — is_errorとToolError

ツール関数の中で例外が発生すると、runnerはそれを捕まえて is_error: true のツール結果に変換します。この結果はClaudeへ自動的に送り返されます。Claudeに渡るのは例外のメッセージ(Pythonでは型名とメッセージ)だけです。スタックトレース全体は含まれません。デバッグのために詳細を残したいなら、ツール関数の内側で自分でログを取る必要があります。

Pythonはやや特別で、未処理例外が発生すると標準の logging モジュール経由でスタックトレース込みの全体を自動的に記録します。加えてPython・TypeScript・Java SDKは環境変数 ANTHROPIC_LOG を読み、リクエスト/レスポンスの詳細ログを出せます。Go・Ruby・C#・PHPはこの環境変数を読みません。Python以外のSDKはツールの失敗を自動でログしないため、原因調査が必要なら例外を握って自前でログを出してから返すか投げ直す実装にしておきます。

export ANTHROPIC_LOG=debug

エラー内容そのものを細かく制御したいなら、Pythonは通常の例外の代わりに anthropic.lib.tools.ToolError を投げます。プレーンな例外は repr() がテキストとしてそのままClaudeへ渡り、自動的にログされる仕組みです。一方の ToolError はテキストと画像などのコンテンツブロックを自由に組み合わせて渡せます。意図的なエラー応答として扱われるため、ログには残りません。スクリーンショット取得ツールが失敗画面ごとClaudeに見せたい、といったケースに向きます。

from anthropic import beta_tool
from anthropic.lib.tools import ToolError
 
 
@beta_tool
def take_screenshot(url: str) -> str:
    """Take a screenshot of a URL."""
    if not is_valid_url(url):
        raise ToolError(f"Invalid URL: {url}")
    result = capture(url)
    if result.error:
        raise ToolError([
            {"type": "text", "text": f"Failed to load page: {result.error}"},
            {"type": "image", "source": {"type": "base64", "data": result.screenshot, "media_type": "image/png"}},
        ])
    return result.data

もう一段踏み込んで、Claudeに送る前にエラーを検査したいこともあります。Python・TypeScriptだけは generate_tool_call_response() / generateToolResponse() というフックを持ちます。ツール結果が送信される直前に中身を確認し、必要なら例外を投げてループ自体を止められる仕組みです。他の5つのSDKにはこのフックがありません。代わりにC#はツール側で BetaToolError を投げて返す内容を作ります。Go/Javaはハンドラ内で例外を捕まえて結果へ変換する、といった代替手段になります。

会話履歴を自分で管理する

runnerは既定でアシスタントメッセージとツール結果を自動的に履歴へ積み、次のリクエストを組み立てます。この自動追記を止めて自分で履歴を組み立てたい場面が3つあります。ターンをやり直したい、フォローアップの指示を差し込みたい、ツール結果を自分で構築したい、のいずれかです。

Pythonではループ内で runner.append_messages(...) を呼ぶと、その時点でrunnerは「状態を自分で管理する」と判断し、そのターンの自動追記を止めます。呼ぶときはアシスタントメッセージとツール結果を自分で含めるのが条件です。含めなければ会話が壊れます。

runner = client.beta.messages.tool_runner(
    model="claude-opus-5",
    max_tokens=1024,
    max_iterations=10,
    tools=[get_weather],
    messages=[{"role": "user", "content": "サンフランシスコの天気は?"}],
)
 
for message in runner:
    tool_response = runner.generate_tool_call_response()
    if tool_response is not None:
        runner.append_messages(
            message,
            tool_response,
            {"role": "user", "content": "簡潔に答えてください。"},
        )
    # ツール呼び出しが無いターンでは何もしない。状態を触らなければループは自然終了する。

会話履歴を変更しないまま max_tokens のようなリクエストパラメータだけ変えたいときは、Pythonでは set_messages_params() を使います。テイクオーバーは伴いません。runnerは自動追記を続けたまま、次のリクエストのパラメータだけ差し替える仕組みです。一方、TypeScriptの setMessagesParams() は挙動が異なります。呼び出すとその回の状態は自分で管理する扱いになります。アシスタントメッセージとツール結果は自動追記からドロップされる点に注意してください(pushMessages() も同様です)。応答が max_tokens で打ち切られた回だけ予算を倍にしてリトライする、といった実装をTypeScript側で書くなら、このテイクオーバーが起きる前提を踏まえる必要があります。

先ほど触れた通り、この「自分で管理を引き継ぐ」パターンを使うときは max_iterations を必ず設定します。理由はシンプルです。状態の変更条件を誤ると、ループが自然終了する条件(ツール呼び出しの無い応答)に到達できず、回り続けるリスクがあります。

Tool RunnerとAgent SDKはどう使い分けるか

同じ「ループを自動化する」という言葉が両方に付きますが、対象が違います。Tool RunnerはAnthropic Client SDKのヘルパーで、APIリクエストの往復だけを肩代わりします。ツールの中身、権限判断、実行環境はすべて自分のコードのままです。一方のClaude Agent SDKは、独立したエージェント実行基盤です。Read / Write / Bash / Glob / Grepといった組み込みツール一式を持ち、セッション管理・権限制御まで込みで動きます。query() を1回呼ぶだけでファイル操作からコマンド実行までこなす自律エージェントが立ち上がる代わりに、Claude Codeのランタイムに乗る前提が付きます。

観点手動ループTool RunnerAgent SDK
往復処理手動ループ自分で書くTool RunnerSDKが自動化Agent SDKSDKが内蔵
ツールの中身手動ループ自分で実装Tool Runner自分で実装Agent SDK組み込み+自作
向く用途手動ループ承認・条件分岐が要る細かい制御Tool Runner単発の関数呼び出し中心のAPI利用Agent SDK自社インフラ上で動く自律エージェント

判断の目安はシンプルです。ツール呼び出しの結果ごとに人間の承認を挟みたい、独自ログや条件分岐で早期終了したいなら、tool_use ブロックを自分でパースして tool_result を組み立てる手動ループに残ります。往復自体は自動化したいが、ツールは自前の関数のままでいい、というAPI利用者の大半にはTool Runnerが現実的な選択です。ファイル編集やコマンド実行を含む本格的な自律エージェントを組みたいなら、往復の自動化だけでは足りずAgent SDKの組み込みツール一式が要ります。Anthropic API完全ガイドではMessages API全体の構造とモデル選択・料金を扱っているので、Tool Runnerに入る前のAPI基礎はそちらで押さえられます。

よくあるつまずき

  • client.messages に生やそうとして属性エラーになる: Tool Runnerはbeta名前空間限定です。client.beta.messages.tool_runner()beta を書き忘れると存在しないメソッドを呼ぶことになります。
  • append_messages() を呼んだのに max_iterations を設定していない: 状態を自分で管理し始めると、条件を誤ったときにループが自然終了しなくなります。会話履歴を引き継ぐ実装には必ず上限を添えます。
  • TypeScriptでZodのバージョンが古い: betaZodTool() はZod 3.25.0未満だと動作しません。Zodを上げたくないプロジェクトでは betaTool() のJSON Schema方式に切り替えます。
  • ツールが失敗しても原因が分からない: PythonはSDKが自動でスタックトレースをログしますが、Go・Ruby・C#・PHPは黙ってエラーだけをClaudeへ返します。ツール関数の内側で自分のログにも残す実装にしておかないと、本番で原因追跡ができません。
  • PHPでストリーミングやgenerateToolResponse相当のフックを探して見つからない: 言語ごとに使える機能に差があります。実装前に対象SDKのタブを確認してから設計します。

よくある質問

Tool RunnerはAPIキーだけで使えますか

はい。Claude Codeのサブスクリプションやセッションは不要で、ANTHROPIC_API_KEY を設定したAnthropic Client SDKだけで動きます。Agent SDKのようにClaude Codeのネイティブバイナリを別途必要としません。

ツールの実行環境はどこになりますか

自分のプロセスの中です。Tool Runnerはあくまで往復のコードを自動生成するヘルパーで、ツール関数自体は呼び出し元のコード内で実行されます。この点はAgent SDKも同じで、サンドボックス実行を丸ごと引き受けるManaged Agentsとは異なります。

並列ツール呼び出しには対応していますか

はい。1ターンで複数のツールが呼ばれた場合も、そのターンのツール結果をまとめて履歴へ追記します。並列呼び出しをやめさせたい場合は、tool_choicedisable_parallel_tool_use を使います。

ツールを大量に渡すとトークンを圧迫しませんか

Tool Runner自体はツール数を制限しません。ただし、ツール定義が増えるほどコンテキストを消費するのは通常のツール呼び出しと同じです。数十〜数百のツールを扱う場合は、Anthropicが公開したAdvanced Tool UseのTool Search Toolのような仕組みと組み合わせる余地があります。ツールの説明文自体の書き方はエージェント向けツール設計の原則が参考になります。

まとめ

Tool Runnerは、Anthropic APIでツールを使うときの往復コードを消すためのベータヘルパーです。client.beta.messages.tool_runner() にツールとメッセージを渡すだけで済みます。実行・結果送信・履歴管理はSDKが肩代わりする仕組みです。until_done()await runner を使えば、最終メッセージだけ受け取ることもできます。細かく制御したい場面もあるはずです。max_iterationsbreak・ストリーミング・履歴の手動引き継ぎといったフックが用意されています。エラー処理も is_errorToolError で明示的に扱える設計です。組み込みツールやセッション管理までまとめて欲しいならAgent SDK、往復だけを自動化してツールは自前で持ちたいならTool Runner、という切り分けで選べば迷いません。

この記事を共有:XはてブLinkedIn