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 de agente manualmente y luego lo habrás reemplazado con la abstracción del SDK de Tool Runner.
La herramienta de ejemplo es create_calendar_event. Su esquema utiliza objetos anidados, matrices y campos opcionales, por lo 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 utiliza herramientas: una herramienta, un mensaje del usuario, una llamada de herramienta, un resultado. El código está muy comentado para que puedas mapear cada línea al ciclo de vida del uso de herramientas.
La solicitud envía un array 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 la input estructurada. Tu código ejecuta la herramienta, luego envía el resultado en un bloque tool_result cuyo tool_use_id coincide con el id de la llamada.
#!/bin/bash
# Ring 1: Single tool, single turn.
# Source for <CodeSource> in build-a-tool-using-agent.mdx.
# Define one tool as a JSON fragment. The input_schema is a JSON Schema
# object describing the arguments Claude should pass when it calls this
# tool. This schema includes nested objects (recurrence), arrays
# (attendees), and optional fields, which is closer to real-world tools
# than a flat string argument.
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"]
}
}
]'
USER_MSG="Schedule a 30-minute sync with [email protected] and [email protected] next Monday at 10am."
# Send the user's request along with the tool definition. Claude decides
# whether to call the tool based on the request and the tool description.
RESPONSE=$(curl -s https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d "$(jq -n \
--argjson tools "$TOOLS" \
--arg msg "$USER_MSG" \
'{
model: "claude-opus-4-6",
max_tokens: 1024,
tools: $tools,
tool_choice: {type: "auto", disable_parallel_tool_use: true},
messages: [{role: "user", content: $msg}]
}')")
# When Claude calls a tool, the response has stop_reason "tool_use"
# and the content array contains a tool_use block alongside any text.
echo "stop_reason: $(echo "$RESPONSE" | jq -r '.stop_reason')"
# Find the tool_use block. A response may contain text blocks before the
# tool_use block, so filter by type rather than assuming position.
TOOL_USE=$(echo "$RESPONSE" | jq '.content[] | select(.type == "tool_use")')
TOOL_USE_ID=$(echo "$TOOL_USE" | jq -r '.id')
echo "Tool: $(echo "$TOOL_USE" | jq -r '.name')"
echo "Input: $(echo "$TOOL_USE" | jq -c '.input')"
# Execute the tool. In a real system this would call your calendar API.
# Here the result is hardcoded to keep the example self-contained.
RESULT='{"event_id": "evt_123", "status": "created"}'
# Send the result back. The tool_result block goes in a user message and
# its tool_use_id must match the id from the tool_use block above. The
# assistant's previous response is included so Claude has the full history.
ASSISTANT_CONTENT=$(echo "$RESPONSE" | jq '.content')
FOLLOWUP=$(curl -s https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d "$(jq -n \
--argjson tools "$TOOLS" \
--arg msg "$USER_MSG" \
--argjson assistant "$ASSISTANT_CONTENT" \
--arg tool_use_id "$TOOL_USE_ID" \
--arg result "$RESULT" \
'{
model: "claude-opus-4-6",
max_tokens: 1024,
tools: $tools,
tool_choice: {type: "auto", disable_parallel_tool_use: true},
messages: [
{role: "user", content: $msg},
{role: "assistant", content: $assistant},
{role: "user", content: [
{type: "tool_result", tool_use_id: $tool_use_id, content: $result}
]}
]
}')")
# With the tool result in hand, Claude produces a final natural-language
# answer and stop_reason becomes "end_turn".
echo "stop_reason: $(echo "$FOLLOWUP" | jq -r '.stop_reason')"
echo "$FOLLOWUP" | jq -r '.content[] | select(.type == "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 next Monday at 10am.El primer stop_reason es tool_use porque Claude está esperando el resultado del calendario. Después de enviar 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 alimentando resultados hasta que stop_reason ya no sea "tool_use".
El otro cambio es el historial de conversación. En lugar de reconstruir el array messages desde cero en cada solicitud, mantén una lista en ejecución y añade a ella. Cada turno ve el contexto previo completo.
#!/bin/bash
# Ring 2: The agentic loop.
# Source for <CodeSource> in build-a-tool-using-agent.mdx.
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"]
}
}
]'
run_tool() {
local name="$1"
local input="$2"
if [ "$name" = "create_calendar_event" ]; then
local title=$(echo "$input" | jq -r '.title')
jq -n --arg title "$title" '{event_id: "evt_123", status: "created", title: $title}'
else
echo "{\"error\": \"Unknown tool: $name\"}"
fi
}
# Keep the full conversation history in a JSON array so each turn sees prior context.
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]."}]'
call_api() {
curl -s https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d "$(jq -n --argjson tools "$TOOLS" --argjson messages "$MESSAGES" \
'{model: "claude-opus-4-6", max_tokens: 1024, tools: $tools, tool_choice: {type: "auto", disable_parallel_tool_use: true}, messages: $messages}')"
}
RESPONSE=$(call_api)
# Loop until Claude stops asking for tools. Each iteration runs the requested
# tool, appends the result to history, and asks Claude to continue.
while [ "$(echo "$RESPONSE" | jq -r '.stop_reason')" = "tool_use" ]; do
TOOL_USE=$(echo "$RESPONSE" | jq '.content[] | select(.type == "tool_use")')
TOOL_NAME=$(echo "$TOOL_USE" | jq -r '.name')
TOOL_INPUT=$(echo "$TOOL_USE" | jq -c '.input')
TOOL_USE_ID=$(echo "$TOOL_USE" | jq -r '.id')
RESULT=$(run_tool "$TOOL_NAME" "$TOOL_INPUT")
ASSISTANT_CONTENT=$(echo "$RESPONSE" | jq '.content')
MESSAGES=$(echo "$MESSAGES" | jq \
--argjson assistant "$ASSISTANT_CONTENT" \
--arg tool_use_id "$TOOL_USE_ID" \
--arg result "$RESULT" \
'. + [
{role: "assistant", content: $assistant},
{role: "user", content: [{type: "tool_result", tool_use_id: $tool_use_id, content: $result}]}
]')
RESPONSE=$(call_api)
done
echo "$RESPONSE" | jq -r '.content[] | select(.type == "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 puede ejecutarse una o varias veces dependiendo de cómo Claude desglosa la tarea. Tu código ya no necesita saberlo de antemano.
Los agentes rara vez tienen solo una capacidad. Añade una segunda herramienta, list_calendar_events, para que Claude pueda verificar el cronograma existente antes de crear algo nuevo.
Cuando Claude tiene múltiples llamadas de herramientas independientes para hacer, puede devolver varios bloques tool_use en una única respuesta. Tu bucle necesita procesar todos ellos y enviar todos los resultados juntos en un mensaje del usuario. Itera sobre cada bloque tool_use en response.content, no solo el primero.
#!/bin/bash
# Ring 3: Multiple tools, parallel calls.
# Source for <CodeSource> in build-a-tool-using-agent.mdx.
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"]
}
}
]'
run_tool() {
case "$1" in
create_calendar_event)
jq -n --arg title "$(echo "$2" | jq -r '.title')" '{event_id: "evt_123", status: "created", title: $title}' ;;
list_calendar_events)
echo '{"events": [{"title": "Existing meeting", "start": "14:00", "end": "15:00"}]}' ;;
*)
echo "{\"error\": \"Unknown tool: $1\"}" ;;
esac
}
MESSAGES='[{"role": "user", "content": "Check what I have next Monday, then schedule a planning session that avoids any conflicts."}]'
call_api() {
curl -s https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d "$(jq -n --argjson tools "$TOOLS" --argjson messages "$MESSAGES" \
'{model: "claude-opus-4-6", max_tokens: 1024, tools: $tools, messages: $messages}')"
}
RESPONSE=$(call_api)
while [ "$(echo "$RESPONSE" | jq -r '.stop_reason')" = "tool_use" ]; do
# A single response can contain multiple tool_use blocks. Process all of
# them and return all results together in one user message.
TOOL_RESULTS='[]'
while read -r block; do
NAME=$(echo "$block" | jq -r '.name')
INPUT=$(echo "$block" | jq -c '.input')
ID=$(echo "$block" | jq -r '.id')
RESULT=$(run_tool "$NAME" "$INPUT")
TOOL_RESULTS=$(echo "$TOOL_RESULTS" | jq --arg id "$ID" --arg result "$RESULT" \
'. + [{type: "tool_result", tool_use_id: $id, content: $result}]')
done < <(echo "$RESPONSE" | jq -c '.content[] | select(.type == "tool_use")')
MESSAGES=$(echo "$MESSAGES" | jq \
--argjson assistant "$(echo "$RESPONSE" | jq '.content')" \
--argjson results "$TOOL_RESULTS" \
'. + [{role: "assistant", content: $assistant}, {role: "user", content: $results}]')
RESPONSE=$(call_api)
done
echo "$RESPONSE" | jq -r '.content[] | select(.type == "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 ejecución concurrente y garantías de ordenamiento, consulta Uso paralelo de herramientas.
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 con is_error: true en lugar de fallar. Claude lee el error y puede reintentar con entrada corregida, pedir aclaración al usuario o explicar la limitación.
#!/bin/bash
# Ring 4: Error handling.
# Source for <CodeSource> in build-a-tool-using-agent.mdx.
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"]
}
}
]'
run_tool() {
case "$1" in
create_calendar_event)
local count=$(echo "$2" | jq '.attendees | length // 0')
if [ "$count" -gt 10 ]; then
echo "ERROR: Too many attendees (max 10)"
return 1
fi
jq -n --arg title "$(echo "$2" | jq -r '.title')" '{event_id: "evt_123", status: "created", title: $title}' ;;
list_calendar_events)
echo '{"events": [{"title": "Existing meeting", "start": "14:00", "end": "15:00"}]}' ;;
*)
echo "ERROR: Unknown tool: $1"
return 1 ;;
esac
}
EMAILS=$(seq 0 14 | sed 's/.*/user&@example.com/' | paste -sd, -)
MESSAGES="[{\"role\": \"user\", \"content\": \"Schedule an all-hands with everyone: $EMAILS\"}]"
call_api() {
curl -s https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d "$(jq -n --argjson tools "$TOOLS" --argjson messages "$MESSAGES" \
'{model: "claude-opus-4-6", max_tokens: 1024, tools: $tools, messages: $messages}')"
}
RESPONSE=$(call_api)
while [ "$(echo "$RESPONSE" | jq -r '.stop_reason')" = "tool_use" ]; do
TOOL_RESULTS='[]'
while read -r block; do
NAME=$(echo "$block" | jq -r '.name')
INPUT=$(echo "$block" | jq -c '.input')
ID=$(echo "$block" | jq -r '.id')
if OUTPUT=$(run_tool "$NAME" "$INPUT"); then
TOOL_RESULTS=$(echo "$TOOL_RESULTS" | jq --arg id "$ID" --arg result "$OUTPUT" \
'. + [{type: "tool_result", tool_use_id: $id, content: $result}]')
else
# Signal failure so Claude can retry or ask for clarification.
TOOL_RESULTS=$(echo "$TOOL_RESULTS" | jq --arg id "$ID" --arg result "$OUTPUT" \
'. + [{type: "tool_result", tool_use_id: $id, content: $result, is_error: true}]')
fi
done < <(echo "$RESPONSE" | jq -c '.content[] | select(.type == "tool_use")')
MESSAGES=$(echo "$MESSAGES" | jq \
--argjson assistant "$(echo "$RESPONSE" | jq '.content')" \
--argjson results "$TOOL_RESULTS" \
'. + [{role: "assistant", content: $assistant}, {role: "user", content: $results}]')
RESPONSE=$(call_api)
done
echo "$RESPONSE" | jq -r '.content[] | select(.type == "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 de un resultado exitoso. Claude ve la bandera y el texto de error, y responde en consecuencia. Consulta Manejar llamadas de herramientas para la referencia completa de manejo de errores.
Los Anillos 2 a 4 escribieron el mismo bucle manualmente: llamar a la API, verificar stop_reason, ejecutar herramientas, añadir resultados, repetir. Tool Runner lo hace por ti. Define cada herramienta como una función, pasa la lista a tool_runner y recupera el mensaje final una vez que se completa el bucle. El envolvimiento de errores, el formato de resultados y la gestión de conversaciones se manejan internamente.
El SDK de Python utiliza el decorador @beta_tool para inferir el esquema de las sugerencias de tipo y la cadena de documentación. El SDK de TypeScript utiliza betaZodTool con un esquema Zod.
Tool Runner está disponible en los SDKs de Python, TypeScript y Ruby. Las pestañas de Shell y CLI muestran una nota en lugar de código; mantén el bucle del Anillo 4 para scripts basados en shell.
#!/bin/bash
# Ring 5: The Tool Runner SDK abstraction.
# Source for <CodeSource> in build-a-tool-using-agent.mdx.
# The Tool Runner SDK abstraction is available in the Python, TypeScript,
# and Ruby SDKs. There is no equivalent for raw curl requests. Switch to
# the Python or TypeScript tab to see Ring 5, or keep the Ring 4 loop as
# your shell implementation.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 al 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 única llamada de herramienta codificada y terminaste con un agente con forma de producción que maneja múltiples herramientas, llamadas paralelas y errores, luego colapsaste todo eso en 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 abstracción del SDK.
Corrige errores comunes de uso de herramientas.
Was this page helpful?