このチュートリアルでは、5つの同心円状のリングでカレンダー管理エージェントを構築します。各リングは完結した実行可能なプログラムであり、前のリングにちょうど1つの概念を追加します。最後まで進めると、エージェントループを手作業で記述し、その後それをTool Runner SDKの抽象化に置き換えることになります。
例として使用するツールはcreate_calendar_eventです。そのスキーマはネストされたオブジェクト、配列、オプションフィールドを使用しているため、単一のフラットな文字列ではなく、現実的な入力形式をClaudeがどのように処理するかを確認できます。
すべてのリングは単独で実行できます。任意のリングを新しいファイルにコピーすれば、それより前のリングのコードがなくても実行されます。
ツールを使用する最小限のプログラムです。1つのツール、1つのユーザーメッセージ、1つのツール呼び出し、1つの結果で構成されます。コードには詳細なコメントが付いているため、各行をツール使用のライフサイクルに対応付けることができます。
リクエストでは、ユーザーメッセージとともにtools配列を送信します。Claudeがツールを呼び出すことを決定すると、レスポンスはstop_reason: "tool_use"とともに返され、ツール名、一意のid、構造化されたinputを含むtool_useコンテンツブロックが含まれます。コードはツールを実行し、その結果をtool_resultブロックで返送します。このブロックのtool_use_idは、呼び出し時のidと一致させます。
# リング1:単一ツール、単一ターン。
import json
import anthropic
# クライアントを作成します。環境変数からANTHROPIC_API_KEYを読み取ります。
client = anthropic.Anthropic()
# ツールを1つ定義します。input_schemaは、Claudeがこのツールを呼び出す際に
# 渡すべき引数を記述するJSON Schemaオブジェクトです。このスキーマには
# ネストされたオブジェクト(recurrence)、配列(attendees)、オプションの
# フィールドが含まれており、フラットな文字列引数よりも実際のツールに近いものです。
tools = [
{
"name": "create_calendar_event",
"description": "Create a calendar event with attendees and optional recurrence.",
"input_schema": {
"type": "object",
"properties": {
"title": {"type": "string"},
"start": {"type": "string", "format": "date-time"},
"end": {"type": "string", "format": "date-time"},
"attendees": {
"type": "array",
"items": {"type": "string", "format": "email"},
},
"recurrence": {
"type": "object",
"properties": {
"frequency": {"enum": ["daily", "weekly", "monthly"]},
"count": {"type": "integer", "minimum": 1},
},
},
},
"required": ["title", "start", "end"],
},
}
]
# ユーザーのリクエストをツール定義とともに送信します。Claudeはリクエストと
# ツールの説明に基づいて、ツールを呼び出すかどうかを判断します。
response = client.messages.create(
model="claude-opus-4-8",
max_tokens=1024,
tools=tools,
tool_choice={"type": "auto", "disable_parallel_tool_use": True},
messages=[
{
"role": "user",
"content": "Schedule a 30-minute sync with [email protected] and [email protected] next Monday at 10am.",
}
],
)
# Claudeがツールを呼び出すと、レスポンスのstop_reasonは"tool_use"となり、
# content配列にはテキストとともにtool_useブロックが含まれます。
print(f"stop_reason: {response.stop_reason}")
# tool_useブロックを見つけます。レスポンスにはtool_useブロックの前にテキスト
# ブロックが含まれる場合があるため、位置を仮定せずcontent配列を走査します。
tool_use = next(block for block in response.content if block.type == "tool_use")
print(f"Tool: {tool_use.name}")
print(f"Input: {tool_use.input}")
# ツールを実行します。実際のシステムではカレンダーAPIを呼び出すことになります。
# ここでは例を自己完結させるため、結果はハードコードされています。
result = {"event_id": "evt_123", "status": "created"}
# 結果を返送します。tool_resultブロックはuserメッセージに入れ、その
# tool_use_idは上記のtool_useブロックのidと一致する必要があります。
# アシスタントの前回のレスポンスも含めることで、Claudeは完全な履歴を把握できます。
followup = client.messages.create(
model="claude-opus-4-8",
max_tokens=1024,
tools=tools,
tool_choice={"type": "auto", "disable_parallel_tool_use": True},
messages=[
{
"role": "user",
"content": "Schedule a 30-minute sync with [email protected] and [email protected] next Monday at 10am.",
},
{"role": "assistant", "content": response.content},
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": tool_use.id,
"content": json.dumps(result),
}
],
},
],
)
# ツールの結果を受け取ると、Claudeは最終的な自然言語の回答を生成し、
# stop_reasonは"end_turn"になります。
print(f"stop_reason: {followup.stop_reason}")
final_text = next(block for block in followup.content if block.type == "text")
print(final_text.text)期待される結果
stop_reason: tool_use
Tool: create_calendar_event
Input: {'title': 'Sync', 'start': '2026-03-30T10:00:00', 'end': '2026-03-30T10:30:00', 'attendees': ['[email protected]', '[email protected]']}
stop_reason: end_turn
I've scheduled your 30-minute sync with Alice and Bob for next Monday at 10am.最初のstop_reasonはtool_useです。これはClaudeがカレンダーの結果を待っているためです。結果を送信すると、2回目のstop_reasonはend_turnとなり、コンテンツはユーザー向けの自然言語になります。
リング1では、Claudeがツールをちょうど1回だけ呼び出すことを前提としていました。実際のタスクでは複数回の呼び出しが必要になることがよくあります。Claudeはイベントを作成し、確認を読み取り、さらに別のイベントを作成するかもしれません。これを解決するには、stop_reasonが"tool_use"でなくなるまでツールを実行し続け、結果をフィードバックし続けるwhileループを使用します。
もう1つの変更点は会話履歴です。リクエストごとにmessages配列をゼロから再構築するのではなく、実行中のリストを保持してそれに追加していきます。各ターンで、それまでの完全なコンテキストが参照されます。
# リング2:エージェントループ。
import json
import anthropic
client = anthropic.Anthropic()
tools = [
{
"name": "create_calendar_event",
"description": "Create a calendar event with attendees and optional recurrence.",
"input_schema": {
"type": "object",
"properties": {
"title": {"type": "string"},
"start": {"type": "string", "format": "date-time"},
"end": {"type": "string", "format": "date-time"},
"attendees": {
"type": "array",
"items": {"type": "string", "format": "email"},
},
"recurrence": {
"type": "object",
"properties": {
"frequency": {"enum": ["daily", "weekly", "monthly"]},
"count": {"type": "integer", "minimum": 1},
},
},
},
"required": ["title", "start", "end"],
},
}
]
def run_tool(name, tool_input):
if name == "create_calendar_event":
return {"event_id": "evt_123", "status": "created", "title": tool_input["title"]}
return {"error": f"Unknown tool: {name}"}
# 会話履歴全体をリストに保持し、各ターンが以前のコンテキストを参照できるようにします。
messages = [
{
"role": "user",
"content": "Schedule a weekly team standup every Monday at 9am for the next 4 weeks. Invite the whole team: [email protected], [email protected], [email protected].",
}
]
response = client.messages.create(
model="claude-opus-4-8",
max_tokens=1024,
tools=tools,
tool_choice={"type": "auto", "disable_parallel_tool_use": True},
messages=messages,
)
# Claudeがツールを要求しなくなるまでループします。各反復で要求された
# ツールを実行し、結果を履歴に追加して、Claudeに続行を依頼します。
while response.stop_reason == "tool_use":
tool_use = next(block for block in response.content if block.type == "tool_use")
result = run_tool(tool_use.name, tool_use.input)
messages.append({"role": "assistant", "content": response.content})
messages.append(
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": tool_use.id,
"content": json.dumps(result),
}
],
}
)
response = client.messages.create(
model="claude-opus-4-8",
max_tokens=1024,
tools=tools,
tool_choice={"type": "auto", "disable_parallel_tool_use": True},
messages=messages,
)
final_text = next(block for block in response.content if block.type == "text")
print(final_text.text)期待される結果
I've set up your weekly team standup for the next 4 Mondays at 9am with Alice, Bob, and Carol invited.ループは、Claudeがタスクをどのように分解するかによって、1回または複数回実行される可能性があります。コード側で事前に回数を知っておく必要はなくなりました。
エージェントが1つの機能しか持たないことはまれです。2つ目のツールlist_calendar_eventsを追加して、Claudeが新しいイベントを作成する前に既存のスケジュールを確認できるようにします。
Claudeが複数の独立したツール呼び出しを行う場合、1つのレスポンスで複数のtool_useブロックを返すことがあります。ループはそれらすべてを処理し、すべての結果を1つのユーザーメッセージにまとめて返送する必要があります。response.content内の最初のブロックだけでなく、すべてのtool_useブロックを反復処理してください。
# リング3:複数ツール、並列呼び出し。
import json
import anthropic
client = anthropic.Anthropic()
tools = [
{
"name": "create_calendar_event",
"description": "Create a calendar event with attendees and optional recurrence.",
"input_schema": {
"type": "object",
"properties": {
"title": {"type": "string"},
"start": {"type": "string", "format": "date-time"},
"end": {"type": "string", "format": "date-time"},
"attendees": {
"type": "array",
"items": {"type": "string", "format": "email"},
},
"recurrence": {
"type": "object",
"properties": {
"frequency": {"enum": ["daily", "weekly", "monthly"]},
"count": {"type": "integer", "minimum": 1},
},
},
},
"required": ["title", "start", "end"],
},
},
{
"name": "list_calendar_events",
"description": "List all calendar events on a given date.",
"input_schema": {
"type": "object",
"properties": {
"date": {"type": "string", "format": "date"},
},
"required": ["date"],
},
},
]
def run_tool(name, tool_input):
if name == "create_calendar_event":
return {"event_id": "evt_123", "status": "created", "title": tool_input["title"]}
if name == "list_calendar_events":
return {"events": [{"title": "Existing meeting", "start": "14:00", "end": "15:00"}]}
return {"error": f"Unknown tool: {name}"}
messages = [
{
"role": "user",
"content": "Check what I have next Monday, then schedule a planning session that avoids any conflicts.",
}
]
response = client.messages.create(
model="claude-opus-4-8",
max_tokens=1024,
tools=tools,
messages=messages,
)
while response.stop_reason == "tool_use":
# 1つのレスポンスに複数のtool_useブロックが含まれる場合があります。
# すべて処理し、結果をまとめて1つのユーザーメッセージで返します。
tool_results = []
for block in response.content:
if block.type == "tool_use":
result = run_tool(block.name, block.input)
tool_results.append(
{
"type": "tool_result",
"tool_use_id": block.id,
"content": json.dumps(result),
}
)
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": tool_results})
response = client.messages.create(
model="claude-opus-4-8",
max_tokens=1024,
tools=tools,
messages=messages,
)
final_text = next(block for block in response.content if block.type == "text")
print(final_text.text)期待される結果
I checked your calendar for next Monday and found an existing meeting from 2pm to 3pm. I've scheduled the planning session for 10am to 11am to avoid the conflict.並行実行と順序保証の詳細については、並列ツール使用を参照してください。
ツールは失敗することがあります。カレンダーAPIは参加者が多すぎるイベントを拒否するかもしれませんし、日付の形式が不正な場合もあります。ツールがエラーを発生させた場合、クラッシュするのではなく、is_error: trueとともにエラーメッセージを返送します。Claudeはエラーを読み取り、修正された入力で再試行したり、ユーザーに確認を求めたり、制限について説明したりできます。
# リング4:エラー処理。
import json
import anthropic
client = anthropic.Anthropic()
tools = [
{
"name": "create_calendar_event",
"description": "Create a calendar event with attendees and optional recurrence.",
"input_schema": {
"type": "object",
"properties": {
"title": {"type": "string"},
"start": {"type": "string", "format": "date-time"},
"end": {"type": "string", "format": "date-time"},
"attendees": {
"type": "array",
"items": {"type": "string", "format": "email"},
},
"recurrence": {
"type": "object",
"properties": {
"frequency": {"enum": ["daily", "weekly", "monthly"]},
"count": {"type": "integer", "minimum": 1},
},
},
},
"required": ["title", "start", "end"],
},
},
{
"name": "list_calendar_events",
"description": "List all calendar events on a given date.",
"input_schema": {
"type": "object",
"properties": {
"date": {"type": "string", "format": "date"},
},
"required": ["date"],
},
},
]
def run_tool(name, tool_input):
if name == "create_calendar_event":
if "attendees" in tool_input and len(tool_input["attendees"]) > 10:
raise ValueError("Too many attendees (max 10)")
return {"event_id": "evt_123", "status": "created", "title": tool_input["title"]}
if name == "list_calendar_events":
return {"events": [{"title": "Existing meeting", "start": "14:00", "end": "15:00"}]}
raise ValueError(f"Unknown tool: {name}")
messages = [
{
"role": "user",
"content": "Schedule an all-hands with everyone: " + ", ".join(f"user{i}@example.com" for i in range(15)),
}
]
response = client.messages.create(
model="claude-opus-4-8",
max_tokens=1024,
tools=tools,
messages=messages,
)
while response.stop_reason == "tool_use":
tool_results = []
for block in response.content:
if block.type == "tool_use":
try:
result = run_tool(block.name, block.input)
tool_results.append(
{"type": "tool_result", "tool_use_id": block.id, "content": json.dumps(result)}
)
except Exception as exc:
# Claudeが再試行または明確化を求められるように失敗を通知します。
tool_results.append(
{
"type": "tool_result",
"tool_use_id": block.id,
"content": str(exc),
"is_error": True,
}
)
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": tool_results})
response = client.messages.create(
model="claude-opus-4-8",
max_tokens=1024,
tools=tools,
messages=messages,
)
final_text = next(block for block in response.content if block.type == "text")
print(final_text.text)期待される結果
I tried to schedule the all-hands but the calendar only allows 10 attendees per event. I can split this into two sessions, or you can let me know which 10 people to prioritize.is_errorフラグが、成功した結果との唯一の違いです。Claudeはこのフラグとエラーテキストを確認し、それに応じて応答します。エラー処理の完全なリファレンスについては、ツール呼び出しの処理を参照してください。
リング2から4では、同じループを手作業で記述しました。APIを呼び出し、stop_reasonを確認し、ツールを実行し、結果を追加し、繰り返す、という流れです。Tool Runnerはこれを代わりに行います。各ツールを関数として定義し、そのリストをtool_runnerに渡し、ループが完了したら最終メッセージを取得します。エラーのラップ、結果のフォーマット、会話管理は内部で処理されます。
Python SDKは@beta_toolデコレーターを使用して、型ヒントとdocstringからスキーマを推論します。TypeScript SDKはZodスキーマとともにbetaZodToolを使用します。他のSDKも、それぞれのヘルパーで同じパターンに従います。C#とPHPではBetaRunnableTool、JavaとRubyでは型付きツールクラス、Goではtoolrunner.NewBetaToolFromJSONSchemaを使用します。
Tool Runnerは、Python、TypeScript、C#、Go、Java、PHP、Rubyの7つのSDKすべてで利用可能です。完全なリファレンスについては、Tool Runnerを参照してください。cURLとCLIのタブにはコードの代わりに注記が表示されます。curlまたはCLIベースのスクリプトでは、リング4のループを引き続き使用してください。
# リング5:Tool Runner SDKの抽象化。
import json
import anthropic
from anthropic import beta_tool
client = anthropic.Anthropic()
@beta_tool
def create_calendar_event(
title: str,
start: str,
end: str,
attendees: list[str] | None = None,
recurrence: dict | None = None,
) -> str:
"""Create a calendar event with attendees and optional recurrence.
Args:
title: Event title.
start: Start time in ISO 8601 format.
end: End time in ISO 8601 format.
attendees: Email addresses to invite.
recurrence: Dict with 'frequency' (daily, weekly, monthly) and 'count'.
"""
if attendees and len(attendees) > 10:
raise ValueError("Too many attendees (max 10)")
return json.dumps({"event_id": "evt_123", "status": "created", "title": title})
@beta_tool
def list_calendar_events(date: str) -> str:
"""List all calendar events on a given date.
Args:
date: Date in YYYY-MM-DD format.
"""
return json.dumps({"events": [{"title": "Existing meeting", "start": "14:00", "end": "15:00"}]})
final_message = client.beta.messages.tool_runner(
model="claude-opus-4-8",
max_tokens=1024,
tools=[create_calendar_event, list_calendar_events],
messages=[
{
"role": "user",
"content": "Check what I have next Monday, then schedule a planning session that avoids any conflicts.",
}
],
).until_done()
for block in final_message.content:
if block.type == "text":
print(block.text)期待される結果
I checked your calendar for next Monday and found an existing meeting from 2pm to 3pm. I've scheduled the planning session for 10am to 11am to avoid the conflict.出力はリング3と同一です。違いはコードにあります。行数は約半分になり、手動ループがなくなり、スキーマが実装のすぐ隣に配置されます。
単一のハードコードされたツール呼び出しから始めて、複数のツール、並列呼び出し、エラーを処理する本番環境向けの形をしたエージェントを完成させ、さらにそれらすべてをTool Runnerに集約しました。その過程で、ツール使用プロトコルのすべての要素を確認しました。tool_useブロック、tool_resultブロック、tool_use_idのマッチング、stop_reasonのチェック、is_errorによるシグナリングです。
Was this page helpful?