Este tutorial construye un agente de gestión de calendario en cinco anillos concéntricos. Cada anillo es un programa completo y ejecutable que agrega exactamente un concepto al anillo anterior. Al final habrás escrito el bucle agéntico a mano y luego lo habrás reemplazado con la abstracción Tool Runner del SDK.
La herramienta de ejemplo es create_calendar_event. Su esquema usa objetos anidados, arreglos y campos opcionales, por lo que verás cómo Claude maneja formas de entrada realistas en lugar de una sola cadena plana.
El programa de uso de herramientas más pequeño posible: una herramienta, un mensaje de usuario, una llamada a herramienta, un resultado. El código está ampliamente comentado para que puedas relacionar cada línea con el ciclo de vida del uso de herramientas.
La solicitud envía un arreglo tools junto con el mensaje del usuario. Cuando Claude determina que se necesita una llamada a herramienta, la respuesta regresa con stop_reason: "tool_use" y un bloque de contenido tool_use que contiene el nombre de la herramienta, un id único y el input estructurado. Tu código ejecuta la herramienta y luego envía el resultado de vuelta en un bloque tool_result cuyo tool_use_id coincide con el id de la llamada.
# Anillo 1: Una sola herramienta, un solo turno.
import json
import anthropic
# Crea un cliente. Lee ANTHROPIC_API_KEY del entorno.
client = anthropic.Anthropic()
# Define una herramienta. El input_schema es un objeto JSON Schema que describe
# los argumentos que Claude debe pasar al llamar a esta herramienta. Este esquema
# incluye objetos anidados (recurrence), arreglos (attendees) y campos
# opcionales, más cercano a herramientas reales que un argumento de cadena plano.
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"],
},
}
]
# Envía la solicitud del usuario junto con la definición de la herramienta. Claude
# decide si llamarla según la solicitud y la descripción de la herramienta.
response = client.messages.create(
model="claude-opus-5",
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] on Monday, March 30, 2026 at 10am.",
}
],
)
# Cuando Claude llama a una herramienta, la respuesta tiene stop_reason "tool_use"
# y el arreglo content contiene un bloque tool_use junto con cualquier texto.
print(f"stop_reason: {response.stop_reason}")
# Busca el bloque tool_use. Una respuesta puede contener bloques de texto antes del
# bloque tool_use, así que recorre el arreglo content en vez de asumir la posición.
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}")
# Ejecuta la herramienta. En un sistema real esto llamaría a tu API de calendario.
# Aquí el resultado está codificado de forma fija para mantener el ejemplo autónomo.
result = {"event_id": "evt_123", "status": "created"}
# Envía el resultado de vuelta. El bloque tool_result va en un mensaje de usuario y
# su tool_use_id debe coincidir con el id del bloque tool_use anterior. La
# respuesta previa del asistente se incluye para que Claude tenga el historial completo.
followup = client.messages.create(
model="claude-opus-5",
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] on Monday, March 30, 2026 at 10am.",
},
{"role": "assistant", "content": response.content},
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": tool_use.id,
"content": json.dumps(result),
}
],
},
],
)
# Con el resultado de la herramienta en mano, Claude produce una respuesta final en
# lenguaje natural y stop_reason pasa a ser "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)Qué esperar
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 Monday, March 30 at 10am.El primer stop_reason es tool_use porque Claude está esperando el resultado del calendario. Después de que envías el resultado, el segundo stop_reason es end_turn y el contenido es lenguaje natural para el usuario.
El Anillo 1 asumía que Claude llamaría a la herramienta exactamente una vez. Las tareas reales a menudo necesitan varias llamadas: Claude podría crear un evento, leer la confirmación y luego crear otro. La solución es un bucle while que sigue ejecutando herramientas y devolviendo resultados hasta que stop_reason ya no sea "tool_use".
El otro cambio es el historial de conversación. En lugar de reconstruir el arreglo messages desde cero en cada solicitud, mantén una lista continua y agrega elementos a ella. Cada turno ve el contexto previo completo.
# Anillo 2: El bucle agéntico.
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}"}
# Mantén el historial completo de la conversación en una lista para que cada turno vea el contexto previo.
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-5",
max_tokens=1024,
tools=tools,
tool_choice={"type": "auto", "disable_parallel_tool_use": True},
messages=messages,
)
# Itera hasta que Claude deje de solicitar herramientas. Cada iteración ejecuta la herramienta
# solicitada, añade el resultado al historial y le pide a Claude que continúe.
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-5",
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)Qué esperar
I've set up your weekly team standup for the next 4 Mondays at 9am with Alice, Bob, and Carol invited.El bucle podría ejecutarse una vez o varias veces dependiendo de cómo Claude descomponga la tarea. Tu código ya no necesita saberlo de antemano.
Los agentes rara vez tienen una sola capacidad. Agrega una segunda herramienta, list_calendar_events, para que Claude pueda revisar la agenda existente antes de crear algo nuevo.
Cuando Claude tiene múltiples llamadas a herramientas independientes que hacer, podría devolver varios bloques tool_use en una sola respuesta. Tu bucle necesita procesarlos todos y enviar de vuelta todos los resultados juntos en un solo mensaje de usuario. Itera sobre cada bloque tool_use en response.content, no solo el primero.
# Anillo 3: Múltiples herramientas, llamadas paralelas.
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-5",
max_tokens=1024,
tools=tools,
messages=messages,
)
while response.stop_reason == "tool_use":
# Una sola respuesta puede contener múltiples bloques tool_use. Procesa todos
# y devuelve todos los resultados juntos en un solo mensaje de usuario.
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-5",
max_tokens=1024,
tools=tools,
messages=messages,
)
final_text = next(block for block in response.content if block.type == "text")
print(final_text.text)Qué esperar
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.Para más información sobre la ejecución concurrente y las garantías de orden, consulta Uso de herramientas en paralelo.
Las herramientas fallan. Una API de calendario podría rechazar un evento con demasiados asistentes, o una fecha podría estar mal formada. Cuando una herramienta genera un error, envía el mensaje de error de vuelta con is_error: true en lugar de fallar. Claude lee el error y puede reintentar con una entrada corregida, pedir aclaración al usuario o explicar la limitación.
# Anillo 4: Manejo de errores.
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-5",
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:
# Señala el fallo para que Claude pueda reintentar o pedir aclaración.
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-5",
max_tokens=1024,
tools=tools,
messages=messages,
)
final_text = next(block for block in response.content if block.type == "text")
print(final_text.text)Qué esperar
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.La bandera is_error es la única diferencia respecto a un resultado exitoso. Claude ve la bandera y el texto del error, y responde en consecuencia. Consulta Manejar llamadas a herramientas para la referencia completa de manejo de errores.
Los anillos 2 al 4 escribieron el mismo bucle a mano: llamar a la API, verificar stop_reason, ejecutar herramientas, agregar resultados, repetir. El Tool Runner hace esto por ti. Define cada herramienta como una función, pasa la lista a tool_runner y recupera el mensaje final una vez que el bucle se completa. El envoltorio de errores, el formateo de resultados y la gestión de la conversación se manejan internamente.
Cada SDK proporciona un ayudante que convierte una función ordinaria en una herramienta ejecutable y deriva el esquema de entrada a partir de su firma; las pestañas a continuación muestran la forma idiomática para cada lenguaje.
# Anillo 5: La abstracción del SDK Tool Runner.
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-5",
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)Qué esperar
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.La salida es idéntica a la del Anillo 3. La diferencia está en el código: aproximadamente la mitad de las líneas, sin bucle manual, y el esquema vive junto a la implementación.
Comenzaste con una sola llamada a herramienta codificada de forma fija y terminaste con un agente con forma de producción que maneja múltiples herramientas, llamadas en paralelo y errores, y luego condensaste todo eso en el Tool Runner. En el camino viste cada pieza del protocolo de uso de herramientas: bloques tool_use, bloques tool_result, coincidencia de tool_use_id, verificación de stop_reason y señalización con is_error.
Especificación de esquemas y mejores prácticas.
La referencia completa de la abstracción del SDK.
Corrige errores comunes del uso de herramientas.
Was this page helpful?