Tool RunnerでAnthropic APIのツール呼び出しループを自動化する
Anthropic APIのtool_runner()でツール実行ループを自動化する実装をPython/TypeScriptで解説。Agent SDKとの使い分けも扱います。
Tool Runnerとは — 往復を自動化する仕組み
Tool Runnerは、Anthropic APIでツールを使うときの往復をSDK側に肩代わりさせるベータ機能です。往復とは「Claudeがツールを呼ぶ→自分のコードで実行する→結果を返す→また呼ぶ」の繰り返しのことです。往復を自分で書かなくてよくなります。Pythonでは client.beta.messages.tool_runner()、TypeScriptでは client.beta.messages.toolRunner() です。どちらもbeta名前空間にあります。人間の承認を挟みたい、独自ログを取りたい、条件次第で実行を止めたいといった細かい制御が要る場面だけ、手動ループに戻る設計です。ループを自分で回すのはクライアントツールだけで、その切り分けはClaude APIのツールは誰が実行するかにあります。手動ループを1から組む手順はClaude APIのtool useエージェントループを5段階で自作するにあります。
SDKが肩代わりする往復
- 1
Claudeのツール呼び出しを受ける
応答に
tool_useが含まれていたら、対応するツール関数を探します。 - 2
ツールを実行する
定義方法によっては入力を検証してから、自分で書いた関数を呼びます。
- 3
結果を返して履歴を積む
tool_resultを作り、アシスタントメッセージと一緒に会話状態へ追記して次のリクエストを送ります。 - 4
ツール呼び出しが無くなるまで繰り返す
ツールを呼ばない応答が来たら終了し、最終メッセージを返します。
毎回自分で書いていたこの往復が、ツール定義とメッセージを渡すだけの数行に縮みます。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デコレータが使えること(手順はClaudeのPython SDKで非同期実行とtool_runnerを実装する方法) - 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-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 を使っているのはそのためです。Tool Runnerが自動生成するtool_resultはこの変換規則に従いますが、手動でループを書く場合はtool_useの直後にtool_resultを置く整形ルールを自分で守る必要があります。破ったときに出る400エラーの実例はtool_resultとtool_useの400エラー — 正しい整形ルールにまとめています。
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-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版だけはストリーミング未対応です。
長時間動かすタスクでは、会話が長くなったときの要約(コンパクション)が要ります。以前はTypeScriptとRubyのrunnerに、トークン使用量が閾値を超えたら要約するクライアント側の自動コンパクションがありました。TypeScript SDK 0.129.0とRuby SDK 1.74.0(2026年9月28日)で削除されたので、いまはどのSDKにもありません。使うのはサーバー側のコンパクションです。常に閾値で要約させたいなら、context_management リクエストパラメータを使います。好きなタイミングで要約させたいなら、compact_before_next_turn()(TypeScriptは compactBeforeNextTurn())を呼びます。両者を併用するとエラーになるので、どちらか一方にします。
エラー処理 — 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() というフックを持ちます。ツール結果が送信される前に中身を確認し、必要なら例外を投げてループ自体を止められます。
Rubyは runner.next_message が返った後、runner.params[:messages] の末尾にあるツール結果を次のリクエスト前に確認する形です。C#・Go・Java・PHPには、送信前に結果を覗くフックがありません。C#はツール側で BetaToolError を投げて内容を作り、Go・Javaはハンドラ内でエラーを捕まえて返す結果を自分で組みます。PHPは pushMessages() で結果を自分で積む方法をとります。
同じフックはツール結果の加工にも使えます。たとえば結果の tool_result ブロックに cache_control を足してプロンプトキャッシュを効かせる場合、Pythonでは次のようにします。
for message in runner:
tool_response = runner.generate_tool_call_response()
if tool_response is not None:
for block in tool_response["content"]:
if block["type"] == "tool_result":
block["cache_control"] = {"type": "ephemeral"}
# 元の応答の自動追記を止めて、加工後を積む
runner.append_messages(message, tool_response)会話履歴を自分で管理する
runnerは既定でアシスタントメッセージとツール結果を自動的に履歴へ積み、次のリクエストを組み立てます。この自動追記を止めて自分で履歴を組み立てたい場面が3つあります。ターンをやり直したい、フォローアップの指示を差し込みたい、ツール結果を自分で構築したい、のいずれかです。
Pythonではループ内で runner.append_messages(...) を呼ぶと、その時点でrunnerは「状態を自分で管理する」と判断し、そのターンの自動追記を止めます。呼ぶときはアシスタントメッセージとツール結果を自分で含めるのが条件です。含めなければ会話が壊れます。
runner = client.beta.messages.tool_runner(
model="claude-opus-5-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側で書くなら、このテイクオーバーが起きる前提を踏まえる必要があります。
状態を自分で触る分、終了条件(ツール呼び出しの無い応答)に届かなくなる余地が生まれます。上限の置き方は前の節のとおりです。
Tool RunnerとAgent SDKはどう使い分けるか
同じ「ループを自動化する」という言葉が両方に付きますが、対象が違います。Tool RunnerはAnthropic Client SDKのヘルパーで、APIリクエストの往復だけを肩代わりします。ツールの中身、権限判断、実行環境はすべて自分のコードのままです。一方のClaude Agent SDKは、独立したエージェント実行基盤です。Read / Write / Bash / Glob / Grepといった組み込みツール一式を持ち、セッション管理・権限制御まで込みで動きます。query() を1回呼ぶだけでファイル操作からコマンド実行までこなす自律エージェントが立ち上がる代わりに、Claude Codeのランタイムに乗る前提が付きます。
3つの選択肢の住み分け
手動ループ
往復を自分で書きます。ツールの中身も自分で実装します。承認や条件分岐で止めたい用途向けです。
Tool Runner
往復はSDKが自動化し、ツールの中身は自分で実装します。単発の関数呼び出し中心のAPI利用に向きます。
Agent SDK
往復も組み込みツールも内蔵です。自作ツールも足せます。自社インフラ上で動く自律エージェント向けです。
Tool Runnerから手動ループへ戻る境目は、Python・TypeScriptなら append_messages() や generate_tool_call_response() のフックで足りるかどうかです。送信前に結果を覗いて止める、履歴を差し替える、といった範囲ならrunnerのまま届きます。tool_use を自分でパースしたくなるのは、フックでも表せない制御が出てからです。Agent SDKへ進む場合はAgent SDKのClaude Codeランタイム前提を受け入れることになります。Messages API全体の構造とモデル選択・料金はAnthropic API完全ガイドで扱っています。
手元のPython SDKで確かめた挙動
anthropic 1.11.0をvenvに入れ、APIは呼ばずにツール定義の変換と例外の経路だけを試しました。記事冒頭の get_weather に to_dict() を呼ぶと、docstringのArgs欄が各引数の description に入り、型ヒントと既定値からスキーマができます。
{"name": "get_weather",
"description": "Get the current weather in a given location.",
"input_schema": {
"additionalProperties": false,
"properties": {
"location": {"description": "The city and state, e.g. San Francisco, CA",
"title": "Location", "type": "string"},
"unit": {"default": "fahrenheit", "description": "Temperature unit, either 'celsius' or 'fahrenheit'",
"title": "Unit", "type": "string"}},
"required": ["location"], "type": "object"}}unit は既定値があるので required に入りません。docstringを書かない関数は description が空文字になります。ツールの説明文として使われるのはdocstringなので、書き忘れると説明の無いツールが渡ります。
例外の経路も確かめました。ValueError("boom") を投げるツールを call() で直接呼ぶと、例外はそのまま呼び出し側へ出ます。is_error: true への変換はrunnerがループの中で行う処理で、関数単体では起きません。n: int の引数に文字列 "a" を渡すと、Invalid arguments for function bad という例外になります。入力検証はこの層で働くので、Claudeが型の合わない入力を作っても、自分の関数の中まで届く前に弾かれます。
runnerオブジェクトには until_done() や append_messages() のほかに、add_tools() / remove_tools() もあります。docstringによると、tools パラメータを変えずにツールを増減することで、プロンプトキャッシュを外さずに済ませる仕組みです。使うには inline-tools-2026-09-15 ベータが必要と書かれています。
よくあるつまずき
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が自動でスタックトレースをログしますが、ほかの6つのSDKはエラーを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_choice の disable_parallel_tool_use を使います。
ツールを大量に渡すとトークンを圧迫しませんか
Tool Runner自体はツール数を制限しません。ただし、ツール定義が増えるほどコンテキストを消費するのは通常のツール呼び出しと同じです。数十〜数百のツールを扱う場合は、Anthropicが公開したAdvanced Tool UseのTool Search Toolのような仕組みと組み合わせる余地があります。ツールの説明文自体の書き方はエージェント向けツール設計の原則が参考になります。Tool Search Toolにはregex版とBM25版の2つの検索方式があり、クエリの書き方がまったく異なります。両者の違いはTool Search Toolのregex版とBM25版はクエリの書き方が全く違うで比較しています。
まとめ
Tool Runnerは、Anthropic APIでツールを使うときの往復コードを消すためのベータヘルパーです。ツールを自前の関数で持ち続ける限り、手動ループに戻る理由は多くありません。まず until_done() で往復だけ任せて動かし、制御が足りなくなった箇所からフックや手動ループへ降りていく順番なら、書き直す範囲を小さく保てます。ベータなので、SDKのリリースノートは追っておく価値があります。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。