Claude にツールを渡すと、Claude は必要に応じてツールの呼び出しを要求してきます。従来はその要求を受け取り、自分でツールを実行し、結果を tool_result として会話履歴に積み直し、もう一度 API を呼ぶ、という往復を自分で書く必要がありました。ツールランナー(tool runner)は、この往復ループを SDK 側が代わりに回してくれる仕組みです。本記事は Anthropic 公式ドキュメントTool runner (SDK)の非公式日本語解説です。ツールランナーは 2026 年 8 月時点でベータ機能です。
ツールランナーとは何か
ツールランナーは、いわゆるエージェントループを SDK に肩代わりさせるヘルパーです。公式ドキュメントは、ツールランナーが自動で行うこととして次の4点を挙げています。
- Claude がツールを呼んだときに、そのツールを実行する
- リクエストとレスポンスの往復を処理する
- 会話の状態(メッセージ履歴)を管理する
- 型の安全性と検証を提供する
手で書く場合、この4つはすべて自分の責任になります。とくに3番目の会話状態の管理は間違えやすいところで、アシスタントメッセージを積み忘れる、ツール結果の tool_use_id を取り違える、といった不具合が起きます。ツールランナーはここを定型処理として引き受けます。
逆に、使わないほうがよい場面も公式に明記されています。人間による承認(human-in-the-loop)を挟みたい、独自のログを取りたい、条件によってツールの実行可否を変えたい、という場合は手動ループのほうが適切です。ツールランナーは「Claude が呼んだら実行する」を前提に組まれているためです。ただし後述するとおり、ループの途中に割り込んで状態を書き換える余地は用意されています。
対応 SDK は Python、TypeScript、C#、Go、Java、PHP、Ruby の7つです。すべてベータ扱いで、メソッド名や API の形は言語ごとに違います。本記事では Python と TypeScript を中心に扱います。
基本の書き方
Python では @beta_tool デコレータで関数をツールとして宣言し、それを client.beta.messages.tool_runner() に渡します。JSON スキーマを自分で書く必要はありません。デコレータが関数の引数の型ヒントと docstring を読み取ってスキーマを生成します。
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"})
@beta_tool
def calculate_sum(a: int, b: int) -> str:
"""Add two numbers together.
Args:
a: First number
b: Second number
"""
return str(a + b)
runner = client.beta.messages.tool_runner(
model="claude-opus-5",
max_tokens=1024,
tools=[get_weather, calculate_sum],
messages=[
{
"role": "user",
"content": "What's the weather like in Paris? Also, what's 15 + 27?",
}
],
)
for message in runner:
print(message)
非同期クライアントを使う場合は、@beta_tool を @beta_async_tool に置き換え、関数を async def で定義します。
戻り値の型に注意してください。ツールが返せるのは文字列か、コンテンツブロック(text / image / document)です。文字列を返すと、それが1つのテキストブロックになります。JSON オブジェクトや数値のような構造化データを返したいときは、先に文字列へエンコードする必要があります。上のコードで json.dumps() や str() を挟んでいるのはこのためです。画像やドキュメントのブロックを返せるので、ツールの結果をマルチモーダルにすることもできます。
TypeScript には2つの書き方があります。推奨は Zod を使う betaZodTool() で、Zod 3.25.0 以上が必要です。
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").describe("Temperature unit")
}),
run: async (input) => {
return 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: "What's the weather like in Paris?" }]
});
Zod を使わず JSON スキーマを直接書く betaTool() もあります。ただしこちらは Claude が生成した入力を実行時に検証しません。検証が必要なら run 関数の中で自分で行ってください。この差はセキュリティにも効くので、外部入力を扱うツールでは Zod 版を選ぶ理由になります。
ループの回り方と途中介入
ツールランナーはイテラブルで、Claude からのメッセージを1件ずつ返します。各イテレーションで、ランナーは Claude がツール呼び出しを要求したかどうかを見ます。要求していればツールを実行し、結果を自動で Claude に送り返し、次のメッセージを返します。
ループが終わる条件は2つです。Claude がツール呼び出しを含まないメッセージを返したときか、max_iterations に達したときです。max_iterations は7つの SDK すべてが対応しています。ループの途中で break して抜けることもできます。
途中のメッセージが不要で、最終結果だけほしい場合は次のようにします。
# Python
final_message = runner.until_done()
for block in final_message.content:
if block.type == "text":
print(block.text)
// TypeScript: runner を await するだけ
const finalMessage = await runner;
言語ごとに名前が違います。C# は runner.RunUntilDoneAsync()、Go は runner.RunToCompletion(ctx) です。Java にはこの近道がありませんので、最後まで反復して最後のメッセージを保持する書き方になります。
より踏み込んだ制御として、会話履歴を自分で引き取ることができます。1回の応答を捨てて送り直したい(リトライ)、フォローアップのメッセージを差し込みたい、ツール結果を自分で組み立てたい、といった場面のための仕組みです。
Python では、ループ本体の中で runner.generate_tool_call_response() でツール結果を取得し、runner.append_messages() を呼びます。append_messages() を呼んだ時点で「履歴は自分が管理する」という宣言になり、そのイテレーションではランナーの自動追記が行われません。
runner = client.beta.messages.tool_runner(
model="claude-opus-5",
max_tokens=1024,
max_iterations=10,
tools=[get_weather],
messages=[{"role": "user", "content": "What's the weather in San Francisco?"}],
)
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": "Please be concise."},
)
ここで自分の責任になる点が3つあります。公式ドキュメントが明示している注意です。
- そのターンを会話として成立させたいなら、アシスタントメッセージとツール結果を自分で append する
- ツール呼び出しが無いときにループが正しく終われるよう、状態の書き換えを条件付きにする
- 暴走を防ぐため
max_iterationsを必ず渡す
2番目を怠ると、ループが終わらなくなります。毎回履歴を書き換えていると、ランナーは「ツール呼び出しが無いから終了」という判断に到達できないためです。
同じ仕組みを使って、ツール結果を送る前に加工することもできます。代表的な用途は、ツール結果に cache_control を付けてプロンプトキャッシュを効かせることです。
エラーの扱いとデバッグ
まず既定の動作を押さえます。ツールが例外を投げると、ツールランナーはそれを捕捉し、is_error: true を付けたツール結果として Claude に返します。プロセスは落ちません。Claude は「そのツールは失敗した」という情報を受け取って、別の手を試したり、ユーザーに報告したりできます。
ツール結果に載るのは例外のメッセージだけ(Python では例外の型とメッセージ)で、スタックトレースは含まれません。
ログの挙動は言語ごとにかなり違うので、ここは実務で効きます。
- Python SDK は、ツールが未捕捉の例外を投げたとき、標準の
loggingモジュール経由でスタックトレースを含む完全な例外を記録します - Python / TypeScript / Java SDK は環境変数
ANTHROPIC_LOGを読み、SDK 自体のログ(リクエストとレスポンスの詳細)を有効にできます - Go / Ruby / C# / PHP SDK は
ANTHROPIC_LOGを読みません
# info レベルで記録
export ANTHROPIC_LOG=info
# より詳細な debug レベル
export ANTHROPIC_LOG=debug
Python 以外の SDK は、失敗したツールを一切ログに残しません。したがって Go や C# でツールが落ちる理由を知りたいときは、ツール関数の中で例外を捕捉して自分でログを出してから、返すか再送出する必要があります。これを知らないと「Claude が変な答えを返すが原因が分からない」という状態に陥ります。
エラーを Claude に渡さず、自分で処理したい場合もあります。Python と TypeScript では、ツール結果を Claude に送る前に傍受できます。
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.get("is_error"):
# 例外を投げてループを止める
raise RuntimeError(f"Tool failed: {json.dumps(block['content'])}")
# あるいはログだけ出して Claude に任せる
# logger.error(f"Tool error: {json.dumps(block['content'])}")
print(message.content)
TypeScript では runner.generateToolResponse() が同じ役割を担います。この傍受フックを持つのは Python と TypeScript だけです。たとえば C# には同等のフックが無く、代わりにツール本体から BetaToolError を投げると、ランナーがそれを is_error: true のツール結果に変換します。エラー内容を制御したい場合はこちらを使います。
長時間動くエージェントの文脈管理についても、注意点が1つあります。TypeScript と Ruby のツールランナーにはクライアント側の自動コンパクションがありましたが、どちらも非推奨になりました。現在は context_management リクエストパラメータによるサーバー側コンパクションが推奨で、こちらは全 SDK のツールランナーで動きます。Python SDK(v1.0 以降)と Go / Java / C# / PHP のツールランナーには、そもそもクライアント側コンパクションが入っていません。
ストリーミングと使い分け
ターンごとの応答を逐次処理したい場合は、ストリーミングを有効にします。Python では stream=True を渡すと、各イテレーションが BetaMessageStream オブジェクトを返すようになります。メッセージそのものではなくストリームが返る点が変わります。
runner = client.beta.messages.tool_runner(
model="claude-opus-5",
max_tokens=1024,
tools=[calculate_sum],
messages=[{"role": "user", "content": "What is 15 + 27?"}],
stream=True,
)
for message_stream in runner:
for event in message_stream:
print("event:", event)
print("message:", message_stream.get_final_message())
print(runner.until_done())
累積されたメッセージは get_final_message() で取得します。
最後に使い分けを整理します。
- ツールランナーが向く場面 — ツールを呼んで結果を返すだけの定型的なエージェント。往復の管理を自分で書く価値が無く、型安全とスキーマ自動生成の恩恵が大きい
- 手動ループが向く場面 — 実行前に人間の承認を挟む、ツールごとに独自のログや計測を仕込む、条件によって実行を止める。公式もこの3つを手動ループの理由として挙げています
- その中間 — 基本はツールランナーに任せつつ、
generate_tool_call_response()とappend_messages()で必要なイテレーションだけ介入する。ただし履歴の整合性と終了条件は自分の責任になるので、max_iterationsを必ず設定する
ツールランナーはベータ機能です。メソッド名や引数は SDK のバージョンで変わりうるため、実装前に使用言語の SDK リポジトリのドキュメント(Python の tools.md、TypeScript の helpers.md)を確認してください。手動ループとの比較は公式のHandle tool callsが詳しいです。