Este tutorial construye un agente de gestión de calendario en cinco anillos concéntricos. Cada anillo es un programa completo y ejecutable que añade 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 del SDK Tool Runner.
La herramienta de ejemplo es create_calendar_event. Su esquema usa objetos anidados, arreglos y campos opcionales, así que verás cómo Claude maneja formas de entrada realistas en lugar de una única cadena plana.
Cada anillo se ejecuta de forma independiente. Copia cualquier anillo en un archivo nuevo y se ejecutará sin el código de los anillos anteriores.
El programa más pequeño posible que usa herramientas: una herramienta, un mensaje de usuario, una llamada a herramienta, un resultado. El código está ampliamente comentado para que puedas mapear cada línea al ciclo de vida del uso de herramientas.
La solicitud envía un arreglo tools junto con el mensaje del usuario. Cuando Claude decide llamar a una 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.
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 next Monday 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 en curso y añade elementos a ella. Cada turno ve el contexto previo completo.
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 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. Añade una segunda herramienta, list_calendar_events, para que Claude pueda revisar el calendario 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 todos los resultados juntos en un solo mensaje de usuario. Itera sobre cada bloque tool_use en response.content, no solo el primero.
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 ejecución concurrente y 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 abruptamente. Claude lee el error y puede reintentar con una entrada corregida, pedir aclaración al usuario o explicar la limitación.
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.El indicador is_error es la única diferencia respecto a un resultado exitoso. Claude ve el indicador 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, añadir 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.
El SDK de Python usa el decorador @beta_tool para inferir el esquema a partir de las anotaciones de tipo y el docstring. El SDK de TypeScript usa betaZodTool con un esquema de Zod. Los otros SDKs siguen el mismo patrón con sus propios helpers: BetaRunnableTool en C# y PHP, clases de herramientas tipadas en Java y Ruby, y toolrunner.NewBetaToolFromJSONSchema en Go.
Tool Runner está disponible en los siete SDKs: Python, TypeScript, C#, Go, Java, PHP y Ruby. Consulta Tool Runner para la referencia completa. Las pestañas de cURL y CLI muestran una nota en lugar de código; conserva el bucle del Anillo 4 para scripts basados en curl o CLI.
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 líneas, sin bucle manual, y el esquema vive junto a la implementación.
Comenzaste con una única llamada a herramienta codificada de forma fija y terminaste con un agente con forma de producción que maneja múltiples herramientas, llamadas paralelas y errores, y luego colapsaste 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 de 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?
# 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-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.",
}
],
)
# 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-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),
}
],
},
],
)
# 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)# 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-4-8",
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-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)# 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-4-8",
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-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)# 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-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:
# 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-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)# 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-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)