튜토리얼: 도구 사용 에이전트 만들기
튜토리얼: 도구 사용 에이전트 만들기 (Tutorial: Build a tool-using agent)
이 튜토리얼은 다섯 개의 동심원(ring) 구조로 달력 관리 에이전트를 만들어요. 각 원은 완전히 실행 가능한 프로그램이고, 앞선 원에 정확히 한 가지 개념만 추가해요. 끝까지 따라 하면 에이전트 루프를 직접 작성했다가 Tool Runner SDK 추상화로 바꿔치기하게 돼요.
예제 도구는 create_calendar_event예요. 그 스키마는 중첩 객체, 배열, 선택 필드를 사용하므로 단일 평평한 문자열 대신 Claude가 현실적인 입력 형태를 어떻게 처리하는지 볼 수 있어요.
출처: 문서
본문
원 1: 단일 도구, 단일 턴
가능한 가장 작은 도구 사용 프로그램이에요. 도구 하나, 사용자 메시지 하나, 도구 호출 하나, 결과 하나. 코드에 주석이 풍부해서 각 줄을 도구 사용 수명 주기에 매핑할 수 있어요.
요청은 사용자 메시지와 함께 tools 배열을 보내요. Claude가 도구 호출이 필요하다고 판단하면 응답이 stop_reason: "tool_use"와 함께, 도구 이름·고유한 id·구조화된 input을 담은 tool_use 콘텐츠 블록을 담아 돌아와요. 여러분의 코드는 도구를 실행한 뒤 tool_use_id가 호출의 id와 일치하는 tool_result 블록으로 결과를 돌려보내요.
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] on Monday, March 30, 2026 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: $ANTHR...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-5-5",
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: $ANTHR...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-5-5",
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'
```bash CLI
#!/usr/bin/env bash
# Ring 1: Single tool, single turn.
# Uses jq for cross-turn message-array state — building an agentic loop in shell
# requires JSON manipulation beyond ant's single-call --transform scope.
set -euo pipefail
USER_MSG="Schedule a 30-minute sync with [email protected] and [email protected] on Monday, March 30, 2026 at 10am."
MESSAGES=$(jq -n --arg msg "$USER_MSG" '[{role: "user", content: $msg}]')
# Define one tool. 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.
call_api() {
# ant reads the request body as YAML on stdin: no auth headers, no
# hand-built JSON envelope. The static keys (model, tools, tool_choice)
# live in a quoted heredoc; the growing messages array is appended as
# JSON, which YAML accepts as flow syntax.
{
cat <<'YAML'
model: claude-opus-5-5
max_tokens: 1024
tool_choice: {type: auto, disable_parallel_tool_use: true}
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]
YAML
printf 'messages: %s\n' "$MESSAGES"
} | ant messages create --format json
}
# 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=$(call_api)
# 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: $(jq -r '.stop_reason' <<<"$RESPONSE")"
# 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=$(jq '.content[] | select(.type == "tool_use")' <<<"$RESPONSE")
TOOL_USE_ID=$(jq -r '.id' <<<"$TOOL_USE")
echo "Tool: $(jq -r '.name' <<<"$TOOL_USE")"
echo "Input: $(jq -c '.input' <<<"$TOOL_USE")"
# 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.
MESSAGES=$(jq \
--argjson assistant "$(jq '.content' <<<"$RESPONSE")" \
--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}
]}
]' <<<"$MESSAGES")
FOLLOWUP=$(call_api)
# With the tool result in hand, Claude produces a final natural-language
# answer and stop_reason becomes "end_turn".
echo "stop_reason: $(jq -r '.stop_reason' <<<"$FOLLOWUP")"
jq -r '.content[] | select(.type == "text") | .text' <<<"$FOLLOWUP"
# Ring 1: Single tool, single turn.
import json
import anthropic
# Create a client. It reads ANTHROPIC_API_KEY from the environment.
client = anthropic.Anthropic()
# Define one tool. 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"],
},
}
]
# 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 = client.messages.create(
model="claude-opus-5-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.",
}
],
)
# When Claude calls a tool, the response has stop_reason "tool_use"
# and the content array contains a tool_use block alongside any text.
print(f"stop_reason: {response.stop_reason}")
# Find the tool_use block. A response may contain text blocks before the
# tool_use block, so scan the content array rather than assuming position.
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}")
# 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.
followup = client.messages.create(
model="claude-opus-5-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),
}
],
},
],
)
# With the tool result in hand, Claude produces a final natural-language
# answer and stop_reason becomes "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)
// Ring 1: Single tool, single turn.
import Anthropic from "@anthropic-ai/sdk";
// Create a client. It reads ANTHROPIC_API_KEY from the environment.
const client = new Anthropic();
// Define one tool. 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.
const tools: Anthropic.Tool[] = [
{
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"],
},
},
];
// 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.
const response = await client.messages.create({
model: "claude-opus-5-5",
max_tokens: 1024,
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.",
},
],
});
// When Claude calls a tool, the response has stop_reason "tool_use"
// and the content array contains a tool_use block alongside any text.
console.log(`stop_reason: ${response.stop_reason}`);
// Find the tool_use block. A response may contain text blocks before the
// tool_use block, so scan the content array rather than assuming position.
const toolUse = response.content.find(
(block): block is Anthropic.ToolUseBlock => block.type === "tool_use",
)!;
console.log(`Tool: ${toolUse.name}`);
console.log(`Input: ${JSON.stringify(toolUse.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.
const 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.
const followup = await client.messages.create({
model: "claude-opus-5-5",
max_tokens: 1024,
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: toolUse.id,
content: JSON.stringify(result),
},
],
},
],
});
// With the tool result in hand, Claude produces a final natural-language
// answer and stop_reason becomes "end_turn".
console.log(`stop_reason: ${followup.stop_reason}`);
for (const block of followup.content) {
if (block.type === "text") {
console.log(block.text);
}
}
// Ring 1: Single tool, single turn.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Anthropic;
using Anthropic.Models.Messages;
// Create a client. It reads ANTHROPIC_API_KEY from the environment.
AnthropicClient client = new();
// Define one tool. 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.
List<ToolUnion> tools =
[
new ToolUnion(new Tool()
{
Name = "create_calendar_event",
Description = "Create a calendar event with attendees and optional recurrence.",
InputSchema = new InputSchema()
{
Properties = new Dictionary<string, JsonElement>
{
["title"] = JsonSerializer.SerializeToElement(new { type = "string" }),
["start"] = JsonSerializer.SerializeToElement(new { type = "string", format = "date-time" }),
["end"] = JsonSerializer.SerializeToElement(new { type = "string", format = "date-time" }),
["attendees"] = JsonSerializer.SerializeToElement(new
{
type = "array",
items = new { type = "string", format = "email" },
}),
["recurrence"] = JsonSerializer.SerializeToElement(new
{
type = "object",
properties = new
{
frequency = new { @enum = new[] { "daily", "weekly", "monthly" } },
count = new { type = "integer", minimum = 1 },
},
}),
},
Required = ["title", "start", "end"],
},
}),
];
// Ask for at most one tool call per turn so the single-turn flow below
// stays predictable.
var toolChoice = new ToolChoice(new ToolChoiceAuto { DisableParallelToolUse = true });
const string userPrompt =
"Schedule a 30-minute sync with [email protected] and [email protected] on Monday, March 30, 2026 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.
var response = await client.Messages.Create(new MessageCreateParams
{
Model = Model.ClaudeOpus5_5,
MaxTokens = 1024,
Tools = tools,
ToolChoice = toolChoice,
Messages = [new() { Role = Role.User, Content = userPrompt }],
});
// When Claude calls a tool, the response has stop_reason "tool_use"
// and the content array contains a tool_use block alongside any text.
Console.WriteLine($"stop_reason: {response.StopReason?.Raw()}");
// Find the tool_use block. A response may contain text blocks before the
// tool_use block, so scan the content array rather than assuming position.
ToolUseBlock? toolUse = null;
foreach (var block in response.Content)
{
if (block.TryPickToolUse(out var picked))
{
toolUse = picked;
break;
}
}
Console.WriteLine($"Tool: {toolUse!.Name}");
Console.WriteLine($"Input: {JsonSerializer.Serialize(toolUse.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.
var 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.
List<ContentBlockParam> toolResults =
[
new ContentBlockParam(new ToolResultBlockParam()
{
ToolUseID = toolUse.ID,
Content = result,
}),
];
var followup = await client.Messages.Create(new MessageCreateParams
{
Model = Model.ClaudeOpus5_5,
MaxTokens = 1024,
Tools = tools,
ToolChoice = toolChoice,
Messages =
[
new() { Role = Role.User, Content = userPrompt },
new() { Role = Role.Assistant, Content = response.Content.Select(block => new ContentBlockParam(block.Json)).ToList() },
new() { Role = Role.User, Content = new MessageParamContent(toolResults) },
],
});
// With the tool result in hand, Claude produces a final natural-language
// answer and stop_reason becomes "end_turn".
Console.WriteLine($"stop_reason: {followup.StopReason?.Raw()}");
foreach (var block in followup.Content)
{
if (block.TryPickText(out var text))
{
Console.WriteLine(text.Text);
}
}
// Ring 1: Single tool, single turn.
package main
import (
"context"
"fmt"
"log"
"github.com/anthropics/anthropic-sdk-go"
)
func main() {
// Create a client. It reads ANTHROPIC_API_KEY from the environment.
client := anthropic.NewClient()
ctx := context.Background()
// Define one tool. 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 := []anthropic.ToolUnionParam{
{OfTool: &anthropic.ToolParam{
Name: "create_calendar_event",
Description: anthropic.String("Create a calendar event with attendees and optional recurrence."),
InputSchema: anthropic.ToolInputSchemaParam{
Properties: map[string]any{
"title": map[string]any{"type": "string"},
"start": map[string]any{"type": "string", "format": "date-time"},
"end": map[string]any{"type": "string", "format": "date-time"},
"attendees": map[string]any{
"type": "array",
"items": map[string]any{"type": "string", "format": "email"},
},
"recurrence": map[string]any{
"type": "object",
"properties": map[string]any{
"frequency": map[string]any{"enum": []string{"daily", "weekly", "monthly"}},
"count": map[string]any{"type": "integer", "minimum": 1},
},
},
},
Required: []string{"title", "start", "end"},
},
}},
}
// Ask for at most one tool call per turn so the single-turn flow below
// stays predictable.
toolChoice := anthropic.ToolChoiceUnionParam{
OfAuto: &anthropic.ToolChoiceAutoParam{DisableParallelToolUse: anthropic.Bool(true)},
}
userMessage := anthropic.NewUserMessage(anthropic.NewTextBlock(
"Schedule a 30-minute sync with [email protected] and [email protected] on Monday, March 30, 2026 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, err := client.Messages.New(ctx, anthropic.MessageNewParams{
Model: anthropic.ModelClaudeOpus5_5,
MaxTokens: 1024,
Tools: tools,
ToolChoice: toolChoice,
Messages: []anthropic.MessageParam{userMessage},
})
if err != nil {
log.Fatal(err)
}
// When Claude calls a tool, the response has stop_reason "tool_use"
// and the content array contains a tool_use block alongside any text.
fmt.Printf("stop_reason: %s\n", response.StopReason)
// Find the tool_use block. A response may contain text blocks before the
// tool_use block, so scan the content array rather than assuming position.
var toolUse anthropic.ContentBlockUnion
for _, block := range response.Content {
if block.Type == "tool_use" {
toolUse = block
break
}
}
fmt.Printf("Tool: %s\n", toolUse.Name)
fmt.Printf("Input: %s\n", string(toolUse.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.
var assistantContent []anthropic.ContentBlockParamUnion
for _, block := range response.Content {
assistantContent = append(assistantContent, block.ToParam())
}
followup, err := client.Messages.New(ctx, anthropic.MessageNewParams{
Model: anthropic.ModelClaudeOpus5_5,
MaxTokens: 1024,
Tools: tools,
ToolChoice: toolChoice,
Messages: []anthropic.MessageParam{
userMessage,
anthropic.NewAssistantMessage(assistantContent...),
anthropic.NewUserMessage(anthropic.NewToolResultBlock(toolUse.ID, result, false)),
},
})
if err != nil {
log.Fatal(err)
}
// With the tool result in hand, Claude produces a final natural-language
// answer and stop_reason becomes "end_turn".
fmt.Printf("stop_reason: %s\n", followup.StopReason)
for _, block := range followup.Content {
if block.Type == "text" {
fmt.Println(block.Text)
}
}
}
// Ring 1: Single tool, single turn.
import com.anthropic.client.AnthropicClient;
import com.anthropic.client.okhttp.AnthropicOkHttpClient;
import com.anthropic.core.JsonValue;
import com.anthropic.models.messages.ContentBlockParam;
import com.anthropic.models.messages.Message;
import com.anthropic.models.messages.MessageCreateParams;
import com.anthropic.models.messages.Model;
import com.anthropic.models.messages.Tool;
import com.anthropic.models.messages.Tool.InputSchema;
import com.anthropic.models.messages.ToolChoiceAuto;
import com.anthropic.models.messages.ToolResultBlockParam;
import com.anthropic.models.messages.ToolUseBlock;
import java.util.List;
import java.util.Map;
void main() {
// Create a client. It reads ANTHROPIC_API_KEY from the environment.
AnthropicClient client = AnthropicOkHttpClient.fromEnv();
// Define one tool. 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.
Tool calendarTool = Tool.builder()
.name("create_calendar_event")
.description("Create a calendar event with attendees and optional recurrence.")
.inputSchema(InputSchema.builder()
.properties(JsonValue.from(Map.of(
"title", Map.of("type", "string"),
"start", Map.of("type", "string", "format", "date-time"),
"end", Map.of("type", "string", "format", "date-time"),
"attendees", Map.of(
"type", "array",
"items", Map.of("type", "string", "format", "email")
),
"recurrence", Map.of(
"type", "object",
"properties", Map.of(
"frequency", Map.of("enum", List.of("daily", "weekly", "monthly")),
"count", Map.of("type", "integer", "minimum", 1)
)
)
)))
.required(List.of("title", "start", "end"))
.build())
.build();
// Ask for at most one tool call per turn so the single-turn flow below
// stays predictable.
ToolChoiceAuto toolChoice = ToolChoiceAuto.builder()
.disableParallelToolUse(true)
.build();
String userPrompt =
"Schedule a 30-minute sync with [email protected] and [email protected] on Monday, March 30, 2026 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.
Message response = client.messages().create(MessageCreateParams.builder()
.model(Model.CLAUDE_OPUS_5_5)
.maxTokens(1024L)
.addTool(calendarTool)
.toolChoice(toolChoice)
.addUserMessage(userPrompt)
.build());
// When Claude calls a tool, the response has stop_reason "tool_use"
// and the content array contains a tool_use block alongside any text.
IO.println("stop_reason: " + response.stopReason().orElse(null));
// Find the tool_use block. A response may contain text blocks before the
// tool_use block, so scan the content array rather than assuming position.
ToolUseBlock toolUse = response.content().stream()
.flatMap(block -> block.toolUse().stream())
.findFirst()
.orElseThrow();
IO.println("Tool: " + toolUse.name());
IO.println("Input: " + toolUse._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.
String 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.
Message followup = client.messages().create(MessageCreateParams.builder()
.model(Model.CLAUDE_OPUS_5_5)
.maxTokens(1024L)
.addTool(calendarTool)
.toolChoice(toolChoice)
.addUserMessage(userPrompt)
.addMessage(response)
.addUserMessageOfBlockParams(List.of(ContentBlockParam.ofToolResult(
ToolResultBlockParam.builder()
.toolUseId(toolUse.id())
.content(result)
.build())))
.build());
// With the tool result in hand, Claude produces a final natural-language
// answer and stop_reason becomes "end_turn".
IO.println("stop_reason: " + followup.stopReason().orElse(null));
followup.content().stream()
.flatMap(block -> block.text().stream())
.forEach(textBlock -> IO.println(textBlock.text()));
}
<?php
// Ring 1: Single tool, single turn.
use Anthropic\Client;
use Anthropic\Messages\ToolChoiceAuto;
// Create a client. It reads ANTHROPIC_API_KEY from the environment.
$client = new Client();
// Define one tool. 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'],
],
],
];
$userMessage = [
'role' => 'user',
'content' => 'Schedule a 30-minute sync with [email protected] and [email protected] on Monday, March 30, 2026 at 10am.',
];
// Ask for at most one tool call per turn so the single-turn flow below
// stays predictable.
$toolChoice = ToolChoiceAuto::with(disableParallelToolUse: true);
// 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 = $client->messages->create(
model: 'claude-opus-5-5',
maxTokens: 1024,
tools: $tools,
toolChoice: $toolChoice,
messages: [$userMessage],
);
// When Claude calls a tool, the response has stop_reason "tool_use"
// and the content array contains a tool_use block alongside any text.
printf("stop_reason: %s\n", $response->stopReason);
// Find the tool_use block. A response may contain text blocks before the
// tool_use block, so scan the content array rather than assuming position.
$toolUse = null;
foreach ($response->content as $block) {
if ($block->type === 'tool_use') {
$toolUse = $block;
break;
}
}
printf("Tool: %s\n", $toolUse->name);
printf("Input: %s\n", json_encode($toolUse->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.
$followup = $client->messages->create(
model: 'claude-opus-5-5',
maxTokens: 1024,
tools: $tools,
toolChoice: $toolChoice,
messages: [
$userMessage,
['role' => 'assistant', 'content' => $response->content],
[
'role' => 'user',
'content' => [
[
'type' => 'tool_result',
'tool_use_id' => $toolUse->id,
'content' => json_encode($result),
],
],
],
],
);
// With the tool result in hand, Claude produces a final natural-language
// answer and stop_reason becomes "end_turn".
printf("stop_reason: %s\n", $followup->stopReason);
foreach ($followup->content as $block) {
if ($block->type === 'text') {
echo $block->text, "\n";
}
}
# Ring 1: Single tool, single turn.
require "anthropic"
# Create a client. It reads ANTHROPIC_API_KEY from the environment.
client = Anthropic::Client.new
# Define one tool. 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_message = {
role: "user",
content: "Schedule a 30-minute sync with [email protected] and [email protected] on Monday, March 30, 2026 at 10am."
}
# Ask for at most one tool call per turn so the single-turn flow below
# stays predictable.
tool_choice = {type: "auto", disable_parallel_tool_use: true}
# 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 = client.messages.create(
model: "claude-opus-5-5",
max_tokens: 1024,
tools: tools,
tool_choice: tool_choice,
messages: [user_message]
)
# When Claude calls a tool, the response has stop_reason "tool_use"
# and the content array contains a tool_use block alongside any text.
puts "stop_reason: #{response.stop_reason}"
# Find the tool_use block. A response may contain text blocks before the
# tool_use block, so scan the content array rather than assuming position.
tool_use = response.content.find { |block| block.type == :tool_use }
puts "Tool: #{tool_use.name}"
puts "Input: #{tool_use.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.
followup = client.messages.create(
model: "claude-opus-5-5",
max_tokens: 1024,
tools: tools,
tool_choice: tool_choice,
messages: [
user_message,
{role: "assistant", content: response.content},
{
role: "user",
content: [
{
type: "tool_result",
tool_use_id: tool_use.id,
content: JSON.generate(result)
}
]
}
]
)
# With the tool result in hand, Claude produces a final natural-language
# answer and stop_reason becomes "end_turn".
puts "stop_reason: #{followup.stop_reason}"
followup.content.each do |block|
puts block.text if block.type == :text
end
무엇을 기대할까요
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.
첫 stop_reason은 tool_use예요. Claude가 달력 결과를 기다리고 있기 때문이죠. 결과를 보낸 뒤 두 번째 stop_reason은 end_turn이고 콘텐츠는 사용자를 위한 자연어예요.
원 2: 에이전트 루프
원 1은 Claude가 도구를 정확히 한 번 호출할 거라고 가정했어요. 실제 작업은 여러 번의 호출이 필요한 경우가 많아요. Claude가 이벤트를 만들고, 확인을 읽고, 또 다른 것을 만드는 식이죠. 해결책은 stop_reason이 더는 "tool_use"가 아닐 때까지 도구를 계속 실행하고 결과를 다시 공급하는 while 루프예요.
다른 변화는 대화 히스토리예요. 매 요청마다 messages 배열을 처음부터 다시 만드는 대신, 실행 목록을 유지하고 그 위에 추가해요. 매 턴마다 완전한 이전 컨텍스트를 보게 돼요.
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, starting Monday, March 30, 2026. Invite the whole team: [email protected], [email protected], [email protected]."}]'
call_api() {
curl -s https://api.anthropic.com/v1/messages
-H "x-api-key: $ANTHR...KEY"
-H "anthropic-version: 2023-06-01"
-H "content-type: application/json"
-d "$(jq -n --argjson tools "$TOOLS" --argjson messages "$MESSAGES"
'{model: "claude-opus-5-5", 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'
```bash CLI
#!/usr/bin/env bash
# Ring 2: The agentic loop.
# Uses jq for cross-turn message-array state — building an agentic loop in shell
# requires JSON manipulation beyond ant's single-call --transform scope.
set -euo pipefail
run_tool() {
local name="$1" input="$2"
if [ "$name" = "create_calendar_event" ]; then
jq -n --arg title "$(jq -r '.title' <<<"$input")" \
'{event_id: "evt_123", status: "created", title: $title}'
else
printf '{"error": "Unknown tool: %s"}' "$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, starting Monday, March 30, 2026. Invite the whole team: [email protected], [email protected], [email protected]."}]'
call_api() {
# ant reads the request body as YAML on stdin: no auth headers, no
# hand-built JSON envelope. The static keys (model, tools, tool_choice)
# live in a quoted heredoc; the growing messages array is appended as
# JSON, which YAML accepts as flow syntax.
{
cat <<'YAML'
model: claude-opus-5-5
max_tokens: 1024
tool_choice: {type: auto, disable_parallel_tool_use: true}
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]
YAML
printf 'messages: %s\n' "$MESSAGES"
} | ant messages create --format json
}
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 [ "$(jq -r '.stop_reason' <<<"$RESPONSE")" = "tool_use" ]; do
TOOL_USE=$(jq '.content[] | select(.type == "tool_use")' <<<"$RESPONSE")
TOOL_NAME=$(jq -r '.name' <<<"$TOOL_USE")
TOOL_INPUT=$(jq -c '.input' <<<"$TOOL_USE")
TOOL_USE_ID=$(jq -r '.id' <<<"$TOOL_USE")
RESULT=$(run_tool "$TOOL_NAME" "$TOOL_INPUT")
MESSAGES=$(jq \
--argjson assistant "$(jq '.content' <<<"$RESPONSE")" \
--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}
]}
]' <<<"$MESSAGES")
RESPONSE=$(call_api)
done
jq -r '.content[] | select(.type == "text") | .text' <<<"$RESPONSE"
# Ring 2: The agentic loop.
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}"}
# Keep the full conversation history in a list so each turn sees prior context.
messages = [
{
"role": "user",
"content": "Schedule a weekly team standup every Monday at 9am for the next 4 weeks, starting Monday, March 30, 2026. Invite the whole team: [email protected], [email protected], [email protected].",
}
]
response = client.messages.create(
model="claude-opus-5-5",
max_tokens=1024,
tools=tools,
tool_choice={"type": "auto", "disable_parallel_tool_use": True},
messages=messages,
)
# Loop until Claude stops asking for tools. Each iteration runs the requested
# tool, appends the result to history, and asks Claude to continue.
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-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)
// Ring 2: The agentic loop.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const tools: Anthropic.Tool[] = [
{
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"],
},
},
];
function runTool(name: string, input: Record<string, unknown>) {
if (name === "create_calendar_event") {
return { event_id: "evt_123", status: "created", title: input.title };
}
return { error: `Unknown tool: ${name}` };
}
// Keep the full conversation history so each turn sees prior context.
const messages: Anthropic.MessageParam[] = [
{
role: "user",
content:
"Schedule a weekly team standup every Monday at 9am for the next 4 weeks, starting Monday, March 30, 2026. Invite the whole team: [email protected], [email protected], [email protected].",
},
];
let response = await client.messages.create({
model: "claude-opus-5-5",
max_tokens: 1024,
tools,
tool_choice: { type: "auto", disable_parallel_tool_use: true },
messages,
});
// Loop until Claude stops asking for tools. Each iteration runs the requested
// tool, appends the result to history, and asks Claude to continue.
while (response.stop_reason === "tool_use") {
const toolUse = response.content.find(
(block): block is Anthropic.ToolUseBlock => block.type === "tool_use",
)!;
const result = runTool(toolUse.name, toolUse.input as Record<string, unknown>);
messages.push({ role: "assistant", content: response.content });
messages.push({
role: "user",
content: [
{
type: "tool_result",
tool_use_id: toolUse.id,
content: JSON.stringify(result),
},
],
});
response = await client.messages.create({
model: "claude-opus-5-5",
max_tokens: 1024,
tools,
tool_choice: { type: "auto", disable_parallel_tool_use: true },
messages,
});
}
for (const block of response.content) {
if (block.type === "text") {
console.log(block.text);
}
}
// Ring 2: The agentic loop.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Anthropic;
using Anthropic.Models.Messages;
AnthropicClient client = new();
List<ToolUnion> tools =
[
new ToolUnion(new Tool()
{
Name = "create_calendar_event",
Description = "Create a calendar event with attendees and optional recurrence.",
InputSchema = new InputSchema()
{
Properties = new Dictionary<string, JsonElement>
{
["title"] = JsonSerializer.SerializeToElement(new { type = "string" }),
["start"] = JsonSerializer.SerializeToElement(new { type = "string", format = "date-time" }),
["end"] = JsonSerializer.SerializeToElement(new { type = "string", format = "date-time" }),
["attendees"] = JsonSerializer.SerializeToElement(new
{
type = "array",
items = new { type = "string", format = "email" },
}),
["recurrence"] = JsonSerializer.SerializeToElement(new
{
type = "object",
properties = new
{
frequency = new { @enum = new[] { "daily", "weekly", "monthly" } },
count = new { type = "integer", minimum = 1 },
},
}),
},
Required = ["title", "start", "end"],
},
}),
];
// Run the requested tool and return its result as a string.
string RunTool(ToolUseBlock toolUse)
{
if (toolUse.Name == "create_calendar_event")
{
var title = toolUse.Input.TryGetValue("title", out var t) ? t.GetString() : "";
return JsonSerializer.Serialize(new { event_id = "evt_123", status = "created", title });
}
return JsonSerializer.Serialize(new { error = $"Unknown tool: {toolUse.Name}" });
}
var toolChoice = new ToolChoice(new ToolChoiceAuto { DisableParallelToolUse = true });
// Keep the full conversation history in a list so each turn sees prior context.
List<MessageParam> messages =
[
new()
{
Role = Role.User,
Content = "Schedule a weekly team standup every Monday at 9am for the next 4 weeks, starting Monday, March 30, 2026. Invite the whole team: [email protected], [email protected], [email protected].",
},
];
var response = await client.Messages.Create(new MessageCreateParams
{
Model = Model.ClaudeOpus5_5,
MaxTokens = 1024,
Tools = tools,
ToolChoice = toolChoice,
Messages = messages,
});
// Loop until Claude stops asking for tools. Each iteration runs the requested
// tool, appends the result to history, and asks Claude to continue.
while (response.StopReason == StopReason.ToolUse)
{
ToolUseBlock? toolUse = null;
foreach (var block in response.Content)
{
if (block.TryPickToolUse(out var picked))
{
toolUse = picked;
break;
}
}
var result = RunTool(toolUse!);
messages.Add(new()
{
Role = Role.Assistant,
Content = response.Content.Select(block => new ContentBlockParam(block.Json)).ToList(),
});
messages.Add(new()
{
Role = Role.User,
Content = new MessageParamContent(
[
new ContentBlockParam(new ToolResultBlockParam() { ToolUseID = toolUse!.ID, Content = result }),
]),
});
response = await client.Messages.Create(new MessageCreateParams
{
Model = Model.ClaudeOpus5_5,
MaxTokens = 1024,
Tools = tools,
ToolChoice = toolChoice,
Messages = messages,
});
}
foreach (var block in response.Content)
{
if (block.TryPickText(out var text))
{
Console.WriteLine(text.Text);
}
}
// Ring 2: The agentic loop.
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"github.com/anthropics/anthropic-sdk-go"
)
func runTool(name string, input map[string]any) string {
if name == "create_calendar_event" {
title, _ := input["title"].(string)
return fmt.Sprintf(`{"event_id": "evt_123", "status": "created", "title": %q}`, title)
}
return fmt.Sprintf(`{"error": "Unknown tool: %s"}`, name)
}
func main() {
client := anthropic.NewClient()
ctx := context.Background()
tools := []anthropic.ToolUnionParam{
{OfTool: &anthropic.ToolParam{
Name: "create_calendar_event",
Description: anthropic.String("Create a calendar event with attendees and optional recurrence."),
InputSchema: anthropic.ToolInputSchemaParam{
Properties: map[string]any{
"title": map[string]any{"type": "string"},
"start": map[string]any{"type": "string", "format": "date-time"},
"end": map[string]any{"type": "string", "format": "date-time"},
"attendees": map[string]any{
"type": "array",
"items": map[string]any{"type": "string", "format": "email"},
},
"recurrence": map[string]any{
"type": "object",
"properties": map[string]any{
"frequency": map[string]any{"enum": []string{"daily", "weekly", "monthly"}},
"count": map[string]any{"type": "integer", "minimum": 1},
},
},
},
Required: []string{"title", "start", "end"},
},
}},
}
toolChoice := anthropic.ToolChoiceUnionParam{
OfAuto: &anthropic.ToolChoiceAutoParam{DisableParallelToolUse: anthropic.Bool(true)},
}
// Keep the full conversation history in a slice so each turn sees prior context.
messages := []anthropic.MessageParam{
anthropic.NewUserMessage(anthropic.NewTextBlock(
"Schedule a weekly team standup every Monday at 9am for the next 4 weeks, starting Monday, March 30, 2026. Invite the whole team: [email protected], [email protected], [email protected].",
)),
}
response, err := client.Messages.New(ctx, anthropic.MessageNewParams{
Model: anthropic.ModelClaudeOpus5_5,
MaxTokens: 1024,
Tools: tools,
ToolChoice: toolChoice,
Messages: messages,
})
if err != nil {
log.Fatal(err)
}
// Loop until Claude stops asking for tools. Each iteration runs the requested
// tool, appends the result to history, and asks Claude to continue.
for response.StopReason == "tool_use" {
var toolUse anthropic.ContentBlockUnion
for _, block := range response.Content {
if block.Type == "tool_use" {
toolUse = block
break
}
}
var input map[string]any
if err := json.Unmarshal(toolUse.Input, &input); err != nil {
log.Fatal(err)
}
result := runTool(toolUse.Name, input)
var assistantContent []anthropic.ContentBlockParamUnion
for _, block := range response.Content {
assistantContent = append(assistantContent, block.ToParam())
}
messages = append(messages, anthropic.NewAssistantMessage(assistantContent...))
messages = append(messages, anthropic.NewUserMessage(
anthropic.NewToolResultBlock(toolUse.ID, result, false),
))
response, err = client.Messages.New(ctx, anthropic.MessageNewParams{
Model: anthropic.ModelClaudeOpus5_5,
MaxTokens: 1024,
Tools: tools,
ToolChoice: toolChoice,
Messages: messages,
})
if err != nil {
log.Fatal(err)
}
}
for _, block := range response.Content {
if block.Type == "text" {
fmt.Println(block.Text)
}
}
}
// Ring 2: The agentic loop.
import com.anthropic.client.AnthropicClient;
import com.anthropic.client.okhttp.AnthropicOkHttpClient;
import com.anthropic.core.JsonValue;
import com.anthropic.models.messages.ContentBlockParam;
import com.anthropic.models.messages.Message;
import com.anthropic.models.messages.MessageCreateParams;
import com.anthropic.models.messages.MessageParam;
import com.anthropic.models.messages.Model;
import com.anthropic.models.messages.StopReason;
import com.anthropic.models.messages.Tool;
import com.anthropic.models.messages.Tool.InputSchema;
import com.anthropic.models.messages.ToolChoiceAuto;
import com.anthropic.models.messages.ToolResultBlockParam;
import com.anthropic.models.messages.ToolUseBlock;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
String runTool(ToolUseBlock toolUse) {
// The raw tool input is a JSON object; read fields out of it as a map.
Map<String, JsonValue> input = (Map<String, JsonValue>) toolUse._input().asObject().get();
if (toolUse.name().equals("create_calendar_event")) {
String title = input.containsKey("title") ? input.get("title").asStringOrThrow() : "";
return "{\"event_id\": \"evt_123\", \"status\": \"created\", \"title\": \"" + title + "\"}";
}
return "{\"error\": \"Unknown tool: " + toolUse.name() + "\"}";
}
void main() {
AnthropicClient client = AnthropicOkHttpClient.fromEnv();
Tool calendarTool = Tool.builder()
.name("create_calendar_event")
.description("Create a calendar event with attendees and optional recurrence.")
.inputSchema(InputSchema.builder()
.properties(JsonValue.from(Map.of(
"title", Map.of("type", "string"),
"start", Map.of("type", "string", "format", "date-time"),
"end", Map.of("type", "string", "format", "date-time"),
"attendees", Map.of(
"type", "array",
"items", Map.of("type", "string", "format", "email")
),
"recurrence", Map.of(
"type", "object",
"properties", Map.of(
"frequency", Map.of("enum", List.of("daily", "weekly", "monthly")),
"count", Map.of("type", "integer", "minimum", 1)
)
)
)))
.required(List.of("title", "start", "end"))
.build())
.build();
ToolChoiceAuto toolChoice = ToolChoiceAuto.builder()
.disableParallelToolUse(true)
.build();
// Keep the full conversation history in a list so each turn sees prior context.
List<MessageParam> messages = new ArrayList<>();
messages.add(MessageParam.builder()
.role(MessageParam.Role.USER)
.content("Schedule a weekly team standup every Monday at 9am for the next 4 weeks, starting Monday, March 30, 2026. Invite the whole team: [email protected], [email protected], [email protected].")
.build());
Message response = client.messages().create(MessageCreateParams.builder()
.model(Model.CLAUDE_OPUS_5_5)
.maxTokens(1024L)
.addTool(calendarTool)
.toolChoice(toolChoice)
.messages(messages)
.build());
// Loop until Claude stops asking for tools. Each iteration runs the requested
// tool, appends the result to history, and asks Claude to continue.
while (response.stopReason().isPresent()
&& response.stopReason().get().equals(StopReason.TOOL_USE)) {
ToolUseBlock toolUse = response.content().stream()
.flatMap(block -> block.toolUse().stream())
.findFirst()
.orElseThrow();
String result = runTool(toolUse);
messages.add(response.toParam());
messages.add(MessageParam.builder()
.role(MessageParam.Role.USER)
.contentOfBlockParams(List.of(ContentBlockParam.ofToolResult(
ToolResultBlockParam.builder()
.toolUseId(toolUse.id())
.content(result)
.build())))
.build());
response = client.messages().create(MessageCreateParams.builder()
.model(Model.CLAUDE_OPUS_5_5)
.maxTokens(1024L)
.addTool(calendarTool)
.toolChoice(toolChoice)
.messages(messages)
.build());
}
response.content().stream()
.flatMap(block -> block.text().stream())
.forEach(textBlock -> IO.println(textBlock.text()));
}
<?php
// Ring 2: The agentic loop.
use Anthropic\Client;
use Anthropic\Messages\ToolChoiceAuto;
$client = new Client();
$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'],
],
],
];
function runTool(string $name, array $input): string
{
if ($name === 'create_calendar_event') {
return json_encode([
'event_id' => 'evt_123',
'status' => 'created',
'title' => $input['title'],
]);
}
return json_encode(['error' => "Unknown tool: {$name}"]);
}
$toolChoice = ToolChoiceAuto::with(disableParallelToolUse: true);
// Keep the full conversation history in an 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, starting Monday, March 30, 2026. Invite the whole team: [email protected], [email protected], [email protected].',
],
];
$response = $client->messages->create(
model: 'claude-opus-5-5',
maxTokens: 1024,
tools: $tools,
toolChoice: $toolChoice,
messages: $messages,
);
// Loop until Claude stops asking for tools. Each iteration runs the requested
// tool, appends the result to history, and asks Claude to continue.
while ($response->stopReason === 'tool_use') {
$toolUse = null;
foreach ($response->content as $block) {
if ($block->type === 'tool_use') {
$toolUse = $block;
break;
}
}
$result = runTool($toolUse->name, $toolUse->input);
$messages[] = ['role' => 'assistant', 'content' => $response->content];
$messages[] = [
'role' => 'user',
'content' => [
[
'type' => 'tool_result',
'tool_use_id' => $toolUse->id,
'content' => $result,
],
],
];
$response = $client->messages->create(
model: 'claude-opus-5-5',
maxTokens: 1024,
tools: $tools,
toolChoice: $toolChoice,
messages: $messages,
);
}
foreach ($response->content as $block) {
if ($block->type === 'text') {
echo $block->text, "\n";
}
}
# Ring 2: The agentic loop.
require "anthropic"
client = Anthropic::Client.new
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, input)
case name
when "create_calendar_event"
JSON.generate({event_id: "evt_123", status: "created", title: input[:title]})
else
JSON.generate({error: "Unknown tool: #{name}"})
end
end
tool_choice = {type: "auto", disable_parallel_tool_use: true}
# Keep the full conversation history in an 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, starting Monday, March 30, 2026. Invite the whole team: [email protected], [email protected], [email protected]."
}
]
response = client.messages.create(
model: "claude-opus-5-5",
max_tokens: 1024,
tools: tools,
tool_choice: tool_choice,
messages: messages
)
# Loop until Claude stops asking for tools. Each iteration runs the requested
# tool, appends the result to history, and asks Claude to continue.
while response.stop_reason == :tool_use
tool_use = response.content.find { |block| block.type == :tool_use }
result = run_tool(tool_use.name, tool_use.input)
messages << {role: "assistant", content: response.content}
messages << {
role: "user",
content: [
{
type: "tool_result",
tool_use_id: tool_use.id,
content: result
}
]
}
response = client.messages.create(
model: "claude-opus-5-5",
max_tokens: 1024,
tools: tools,
tool_choice: tool_choice,
messages: messages
)
end
response.content.each do |block|
puts block.text if block.type == :text
end
무엇을 기대할까요
I've set up your weekly team standup for the next 4 Mondays at 9am with Alice, Bob, and Carol invited.
루프는 Claude가 작업을 어떻게 나누느냐에 따라 한 번 또는 여러 번 실행될 수 있어요. 이제 여러분의 코드는 미리 알 필요가 없어요.
원 3: 여러 도구, 병렬 호출
에이전트는 거의 항상 하나의 능력만 있지 않아요. 두 번째 도구 list_calendar_events를 추가해서 Claude가 새 것을 만들기 전에 기존 일정을 확인할 수 있게 해요.
Claude가 실행할 독립적인 도구 호출이 여러 개 있을 때, 단일 응답에 여러 tool_use 블록을 돌려줄 수 있어요. 루프는 그것들을 모두 처리하고 모든 결과를 하나의 사용자 메시지로 함께 돌려줘야 해요. response.content의 모든 tool_use 블록을 순회하세요. 첫 번째만이 아니라요.
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 on Monday, March 30, 2026, then schedule a one-hour planning session that day that avoids any conflicts."}]'
call_api() {
curl -s https://api.anthropic.com/v1/messages
-H "x-api-key: $ANTHR...KEY"
-H "anthropic-version: 2023-06-01"
-H "content-type: application/json"
-d "$(jq -n --argjson tools "$TOOLS" --argjson messages "$MESSAGES"
'{model: "claude-opus-5-5", 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'
```bash CLI
#!/usr/bin/env bash
# Ring 3: Multiple tools, parallel calls.
# Uses jq for cross-turn message-array state — building an agentic loop in shell
# requires JSON manipulation beyond ant's single-call --transform scope.
set -euo pipefail
run_tool() {
case "$1" in
create_calendar_event)
jq -n --arg title "$(jq -r '.title' <<<"$2")" \
'{event_id: "evt_123", status: "created", title: $title}' ;;
list_calendar_events)
echo '{"events": [{"title": "Existing meeting", "start": "14:00", "end": "15:00"}]}' ;;
*)
printf '{"error": "Unknown tool: %s"}' "$1" ;;
esac
}
MESSAGES='[{"role": "user", "content": "Check what I have on Monday, March 30, 2026, then schedule a one-hour planning session that day that avoids any conflicts."}]'
call_api() {
# ant reads the request body as YAML on stdin: no auth headers, no
# hand-built JSON envelope. The static keys (model, tools) live in a
# quoted heredoc; the growing messages array is appended as JSON,
# which YAML accepts as flow syntax.
{
cat <<'YAML'
model: claude-opus-5-5
max_tokens: 1024
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]
YAML
printf 'messages: %s\n' "$MESSAGES"
} | ant messages create --format json
}
RESPONSE=$(call_api)
while [ "$(jq -r '.stop_reason' <<<"$RESPONSE")" = "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=$(jq -r '.name' <<<"$block")
INPUT=$(jq -c '.input' <<<"$block")
ID=$(jq -r '.id' <<<"$block")
RESULT=$(run_tool "$NAME" "$INPUT")
TOOL_RESULTS=$(jq --arg id "$ID" --arg result "$RESULT" \
'. + [{type: "tool_result", tool_use_id: $id, content: $result}]' \
<<<"$TOOL_RESULTS")
done < <(jq -c '.content[] | select(.type == "tool_use")' <<<"$RESPONSE")
MESSAGES=$(jq \
--argjson assistant "$(jq '.content' <<<"$RESPONSE")" \
--argjson results "$TOOL_RESULTS" \
'. + [
{role: "assistant", content: $assistant},
{role: "user", content: $results}
]' <<<"$MESSAGES")
RESPONSE=$(call_api)
done
jq -r '.content[] | select(.type == "text") | .text' <<<"$RESPONSE"
# Ring 3: Multiple tools, parallel calls.
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 on Monday, March 30, 2026, then schedule a one-hour planning session that day that avoids any conflicts.",
}
]
response = client.messages.create(
model="claude-opus-5-5",
max_tokens=1024,
tools=tools,
messages=messages,
)
while response.stop_reason == "tool_use":
# A single response can contain multiple tool_use blocks. Process all of
# them and return all results together in one user message.
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-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)
// Ring 3: Multiple tools, parallel calls.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const tools: Anthropic.Tool[] = [
{
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"],
},
},
];
function runTool(name: string, input: Record<string, unknown>) {
if (name === "create_calendar_event") {
return { event_id: "evt_123", status: "created", title: input.title };
}
if (name === "list_calendar_events") {
return {
events: [{ title: "Existing meeting", start: "14:00", end: "15:00" }],
};
}
return { error: `Unknown tool: ${name}` };
}
const messages: Anthropic.MessageParam[] = [
{
role: "user",
content:
"Check what I have on Monday, March 30, 2026, then schedule a one-hour planning session that day that avoids any conflicts.",
},
];
let response = await client.messages.create({
model: "claude-opus-5-5",
max_tokens: 1024,
tools,
messages,
});
while (response.stop_reason === "tool_use") {
// A single response can contain multiple tool_use blocks. Process all of
// them and return all results together in one user message.
const toolResults: Anthropic.ToolResultBlockParam[] = [];
for (const block of response.content) {
if (block.type === "tool_use") {
const result = runTool(block.name, block.input as Record<string, unknown>);
toolResults.push({
type: "tool_result",
tool_use_id: block.id,
content: JSON.stringify(result),
});
}
}
messages.push({ role: "assistant", content: response.content });
messages.push({ role: "user", content: toolResults });
response = await client.messages.create({
model: "claude-opus-5-5",
max_tokens: 1024,
tools,
messages,
});
}
for (const block of response.content) {
if (block.type === "text") {
console.log(block.text);
}
}
// Ring 3: Multiple tools, parallel calls.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Anthropic;
using Anthropic.Models.Messages;
AnthropicClient client = new();
List<ToolUnion> tools =
[
new ToolUnion(new Tool()
{
Name = "create_calendar_event",
Description = "Create a calendar event with attendees and optional recurrence.",
InputSchema = new InputSchema()
{
Properties = new Dictionary<string, JsonElement>
{
["title"] = JsonSerializer.SerializeToElement(new { type = "string" }),
["start"] = JsonSerializer.SerializeToElement(new { type = "string", format = "date-time" }),
["end"] = JsonSerializer.SerializeToElement(new { type = "string", format = "date-time" }),
["attendees"] = JsonSerializer.SerializeToElement(new
{
type = "array",
items = new { type = "string", format = "email" },
}),
["recurrence"] = JsonSerializer.SerializeToElement(new
{
type = "object",
properties = new
{
frequency = new { @enum = new[] { "daily", "weekly", "monthly" } },
count = new { type = "integer", minimum = 1 },
},
}),
},
Required = ["title", "start", "end"],
},
}),
new ToolUnion(new Tool()
{
Name = "list_calendar_events",
Description = "List all calendar events on a given date.",
InputSchema = new InputSchema()
{
Properties = new Dictionary<string, JsonElement>
{
["date"] = JsonSerializer.SerializeToElement(new { type = "string", format = "date" }),
},
Required = ["date"],
},
}),
];
string RunTool(ToolUseBlock toolUse)
{
if (toolUse.Name == "create_calendar_event")
{
var title = toolUse.Input.TryGetValue("title", out var t) ? t.GetString() : "";
return JsonSerializer.Serialize(new { event_id = "evt_123", status = "created", title });
}
if (toolUse.Name == "list_calendar_events")
{
return """{"events": [{"title": "Existing meeting", "start": "14:00", "end": "15:00"}]}""";
}
return JsonSerializer.Serialize(new { error = $"Unknown tool: {toolUse.Name}" });
}
List<MessageParam> messages =
[
new()
{
Role = Role.User,
Content = "Check what I have on Monday, March 30, 2026, then schedule a one-hour planning session that day that avoids any conflicts.",
},
];
var response = await client.Messages.Create(new MessageCreateParams
{
Model = Model.ClaudeOpus5_5,
MaxTokens = 1024,
Tools = tools,
Messages = messages,
});
while (response.StopReason == StopReason.ToolUse)
{
// A single response can contain multiple tool_use blocks. Process all of
// them and return all results together in one user message.
List<ContentBlockParam> toolResults = [];
foreach (var block in response.Content)
{
if (block.TryPickToolUse(out var toolUse))
{
toolResults.Add(new ContentBlockParam(new ToolResultBlockParam()
{
ToolUseID = toolUse.ID,
Content = RunTool(toolUse),
}));
}
}
messages.Add(new()
{
Role = Role.Assistant,
Content = response.Content.Select(block => new ContentBlockParam(block.Json)).ToList(),
});
messages.Add(new() { Role = Role.User, Content = new MessageParamContent(toolResults) });
response = await client.Messages.Create(new MessageCreateParams
{
Model = Model.ClaudeOpus5_5,
MaxTokens = 1024,
Tools = tools,
Messages = messages,
});
}
foreach (var block in response.Content)
{
if (block.TryPickText(out var text))
{
Console.WriteLine(text.Text);
}
}
// Ring 3: Multiple tools, parallel calls.
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"github.com/anthropics/anthropic-sdk-go"
)
func runTool(name string, input map[string]any) string {
if name == "create_calendar_event" {
title, _ := input["title"].(string)
return fmt.Sprintf(`{"event_id": "evt_123", "status": "created", "title": %q}`, title)
}
if name == "list_calendar_events" {
return `{"events": [{"title": "Existing meeting", "start": "14:00", "end": "15:00"}]}`
}
return fmt.Sprintf(`{"error": "Unknown tool: %s"}`, name)
}
func main() {
client := anthropic.NewClient()
ctx := context.Background()
tools := []anthropic.ToolUnionParam{
{OfTool: &anthropic.ToolParam{
Name: "create_calendar_event",
Description: anthropic.String("Create a calendar event with attendees and optional recurrence."),
InputSchema: anthropic.ToolInputSchemaParam{
Properties: map[string]any{
"title": map[string]any{"type": "string"},
"start": map[string]any{"type": "string", "format": "date-time"},
"end": map[string]any{"type": "string", "format": "date-time"},
"attendees": map[string]any{
"type": "array",
"items": map[string]any{"type": "string", "format": "email"},
},
"recurrence": map[string]any{
"type": "object",
"properties": map[string]any{
"frequency": map[string]any{"enum": []string{"daily", "weekly", "monthly"}},
"count": map[string]any{"type": "integer", "minimum": 1},
},
},
},
Required: []string{"title", "start", "end"},
},
}},
{OfTool: &anthropic.ToolParam{
Name: "list_calendar_events",
Description: anthropic.String("List all calendar events on a given date."),
InputSchema: anthropic.ToolInputSchemaParam{
Properties: map[string]any{
"date": map[string]any{"type": "string", "format": "date"},
},
Required: []string{"date"},
},
}},
}
messages := []anthropic.MessageParam{
anthropic.NewUserMessage(anthropic.NewTextBlock(
"Check what I have on Monday, March 30, 2026, then schedule a one-hour planning session that day that avoids any conflicts.",
)),
}
response, err := client.Messages.New(ctx, anthropic.MessageNewParams{
Model: anthropic.ModelClaudeOpus5_5,
MaxTokens: 1024,
Tools: tools,
Messages: messages,
})
if err != nil {
log.Fatal(err)
}
for response.StopReason == "tool_use" {
// A single response can contain multiple tool_use blocks. Process all of
// them and return all results together in one user message.
var toolResults []anthropic.ContentBlockParamUnion
for _, block := range response.Content {
if block.Type == "tool_use" {
var input map[string]any
if err := json.Unmarshal(block.Input, &input); err != nil {
log.Fatal(err)
}
result := runTool(block.Name, input)
toolResults = append(toolResults, anthropic.NewToolResultBlock(block.ID, result, false))
}
}
var assistantContent []anthropic.ContentBlockParamUnion
for _, block := range response.Content {
assistantContent = append(assistantContent, block.ToParam())
}
messages = append(messages, anthropic.NewAssistantMessage(assistantContent...))
messages = append(messages, anthropic.NewUserMessage(toolResults...))
response, err = client.Messages.New(ctx, anthropic.MessageNewParams{
Model: anthropic.ModelClaudeOpus5_5,
MaxTokens: 1024,
Tools: tools,
Messages: messages,
})
if err != nil {
log.Fatal(err)
}
}
for _, block := range response.Content {
if block.Type == "text" {
fmt.Println(block.Text)
}
}
}
// Ring 3: Multiple tools, parallel calls.
import com.anthropic.client.AnthropicClient;
import com.anthropic.client.okhttp.AnthropicOkHttpClient;
import com.anthropic.core.JsonValue;
import com.anthropic.models.messages.ContentBlock;
import com.anthropic.models.messages.ContentBlockParam;
import com.anthropic.models.messages.Message;
import com.anthropic.models.messages.MessageCreateParams;
import com.anthropic.models.messages.MessageParam;
import com.anthropic.models.messages.Model;
import com.anthropic.models.messages.StopReason;
import com.anthropic.models.messages.Tool;
import com.anthropic.models.messages.Tool.InputSchema;
import com.anthropic.models.messages.ToolResultBlockParam;
import com.anthropic.models.messages.ToolUseBlock;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
String runTool(ToolUseBlock toolUse) {
// The raw tool input is a JSON object; read fields out of it as a map.
Map<String, JsonValue> input = (Map<String, JsonValue>) toolUse._input().asObject().get();
if (toolUse.name().equals("create_calendar_event")) {
String title = input.containsKey("title") ? input.get("title").asStringOrThrow() : "";
return "{\"event_id\": \"evt_123\", \"status\": \"created\", \"title\": \"" + title + "\"}";
}
if (toolUse.name().equals("list_calendar_events")) {
return "{\"events\": [{\"title\": \"Existing meeting\", \"start\": \"14:00\", \"end\": \"15:00\"}]}";
}
return "{\"error\": \"Unknown tool: " + toolUse.name() + "\"}";
}
void main() {
AnthropicClient client = AnthropicOkHttpClient.fromEnv();
Tool calendarTool = Tool.builder()
.name("create_calendar_event")
.description("Create a calendar event with attendees and optional recurrence.")
.inputSchema(InputSchema.builder()
.properties(JsonValue.from(Map.of(
"title", Map.of("type", "string"),
"start", Map.of("type", "string", "format", "date-time"),
"end", Map.of("type", "string", "format", "date-time"),
"attendees", Map.of(
"type", "array",
"items", Map.of("type", "string", "format", "email")
),
"recurrence", Map.of(
"type", "object",
"properties", Map.of(
"frequency", Map.of("enum", List.of("daily", "weekly", "monthly")),
"count", Map.of("type", "integer", "minimum", 1)
)
)
)))
.required(List.of("title", "start", "end"))
.build())
.build();
Tool listTool = Tool.builder()
.name("list_calendar_events")
.description("List all calendar events on a given date.")
.inputSchema(InputSchema.builder()
.properties(JsonValue.from(Map.of(
"date", Map.of("type", "string", "format", "date")
)))
.required(List.of("date"))
.build())
.build();
List<MessageParam> messages = new ArrayList<>();
messages.add(MessageParam.builder()
.role(MessageParam.Role.USER)
.content("Check what I have on Monday, March 30, 2026, then schedule a one-hour planning session that day that avoids any conflicts.")
.build());
Message response = client.messages().create(MessageCreateParams.builder()
.model(Model.CLAUDE_OPUS_5_5)
.maxTokens(1024L)
.addTool(calendarTool)
.addTool(listTool)
.messages(messages)
.build());
while (response.stopReason().isPresent()
&& response.stopReason().get().equals(StopReason.TOOL_USE)) {
// A single response can contain multiple tool_use blocks. Process all of
// them and return all results together in one user message.
List<ContentBlockParam> toolResults = new ArrayList<>();
for (ContentBlock block : response.content()) {
if (block.toolUse().isPresent()) {
ToolUseBlock toolUse = block.toolUse().get();
toolResults.add(ContentBlockParam.ofToolResult(
ToolResultBlockParam.builder()
.toolUseId(toolUse.id())
.content(runTool(toolUse))
.build()));
}
}
messages.add(response.toParam());
messages.add(MessageParam.builder()
.role(MessageParam.Role.USER)
.contentOfBlockParams(toolResults)
.build());
response = client.messages().create(MessageCreateParams.builder()
.model(Model.CLAUDE_OPUS_5_5)
.maxTokens(1024L)
.addTool(calendarTool)
.addTool(listTool)
.messages(messages)
.build());
}
response.content().stream()
.flatMap(block -> block.text().stream())
.forEach(textBlock -> IO.println(textBlock.text()));
}
<?php
// Ring 3: Multiple tools, parallel calls.
use Anthropic\Client;
$client = new Client();
$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'],
],
],
];
function runTool(string $name, array $input): string
{
if ($name === 'create_calendar_event') {
return json_encode([
'event_id' => 'evt_123',
'status' => 'created',
'title' => $input['title'],
]);
}
if ($name === 'list_calendar_events') {
return json_encode([
'events' => [['title' => 'Existing meeting', 'start' => '14:00', 'end' => '15:00']],
]);
}
return json_encode(['error' => "Unknown tool: {$name}"]);
}
$messages = [
[
'role' => 'user',
'content' => 'Check what I have on Monday, March 30, 2026, then schedule a one-hour planning session that day that avoids any conflicts.',
],
];
$response = $client->messages->create(
model: 'claude-opus-5-5',
maxTokens: 1024,
tools: $tools,
messages: $messages,
);
while ($response->stopReason === 'tool_use') {
// A single response can contain multiple tool_use blocks. Process all of
// them and return all results together in one user message.
$toolResults = [];
foreach ($response->content as $block) {
if ($block->type === 'tool_use') {
$toolResults[] = [
'type' => 'tool_result',
'tool_use_id' => $block->id,
'content' => runTool($block->name, $block->input),
];
}
}
$messages[] = ['role' => 'assistant', 'content' => $response->content];
$messages[] = ['role' => 'user', 'content' => $toolResults];
$response = $client->messages->create(
model: 'claude-opus-5-5',
maxTokens: 1024,
tools: $tools,
messages: $messages,
);
}
foreach ($response->content as $block) {
if ($block->type === 'text') {
echo $block->text, "\n";
}
}
# Ring 3: Multiple tools, parallel calls.
require "anthropic"
client = Anthropic::Client.new
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, input)
case name
when "create_calendar_event"
JSON.generate({event_id: "evt_123", status: "created", title: input[:title]})
when "list_calendar_events"
JSON.generate({events: [{title: "Existing meeting", start: "14:00", end: "15:00"}]})
else
JSON.generate({error: "Unknown tool: #{name}"})
end
end
messages = [
{
role: "user",
content: "Check what I have on Monday, March 30, 2026, then schedule a one-hour planning session that day that avoids any conflicts."
}
]
response = client.messages.create(
model: "claude-opus-5-5",
max_tokens: 1024,
tools: tools,
messages: messages
)
while response.stop_reason == :tool_use
# A single response can contain multiple tool_use blocks. Process all of
# them and return all results together in one user message.
tool_results = response.content.select { |block| block.type == :tool_use }.map do |tool_use|
{
type: "tool_result",
tool_use_id: tool_use.id,
content: run_tool(tool_use.name, tool_use.input)
}
end
messages << {role: "assistant", content: response.content}
messages << {role: "user", content: tool_results}
response = client.messages.create(
model: "claude-opus-5-5",
max_tokens: 1024,
tools: tools,
messages: messages
)
end
response.content.each do |block|
puts block.text if block.type == :text
end
무엇을 기대할까요
I checked your calendar for Monday, March 30 and found an existing meeting from 2pm to 3pm. I've scheduled the planning session for 10am to 11am to avoid the conflict.
동시 실행과 순서 보장에 대한 자세한 내용은 병렬 도구 사용 참고.
원 4: 오류 처리
도구는 실패해요. 달력 API가 참석자가 너무 많은 이벤트를 거부하거나, 날짜가 잘못될 수 있어요. 도구가 오류를 일으키면 크래시 대신 is_error: true로 오류 메시지를 돌려보내요. Claude는 오류를 읽고 수정된 입력으로 재시도하거나, 사용자에게 설명을 요청하거나, 한계를 설명할 수 있어요.
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 a one-hour all-hands on Monday, March 30, 2026 at 10am with everyone: $EMAILS"}]"
call_api() {
curl -s https://api.anthropic.com/v1/messages
-H "x-api-key: $ANTHR...KEY"
-H "anthropic-version: 2023-06-01"
-H "content-type: application/json"
-d "$(jq -n --argjson tools "$TOOLS" --argjson messages "$MESSAGES"
'{model: "claude-opus-5-5", 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'
```bash CLI
#!/usr/bin/env bash
# Ring 4: Error handling.
# Uses jq for cross-turn message-array state — building an agentic loop in shell
# requires JSON manipulation beyond ant's single-call --transform scope.
set -euo pipefail
run_tool() {
case "$1" in
create_calendar_event)
local count
count=$(jq '.attendees | length // 0' <<<"$2")
if [ "$count" -gt 10 ]; then
echo "ERROR: Too many attendees (max 10)"
return 1
fi
jq -n --arg title "$(jq -r '.title' <<<"$2")" \
'{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=$(jq -n --arg msg "Schedule a one-hour all-hands on Monday, March 30, 2026 at 10am with everyone: $EMAILS" \
'[{role: "user", content: $msg}]')
call_api() {
# ant reads the request body as YAML on stdin: no auth headers, no
# hand-built JSON envelope. The static keys (model, tools) live in a
# quoted heredoc; the growing messages array is appended as JSON,
# which YAML accepts as flow syntax.
{
cat <<'YAML'
model: claude-opus-5-5
max_tokens: 1024
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]
YAML
printf 'messages: %s\n' "$MESSAGES"
} | ant messages create --format json
}
RESPONSE=$(call_api)
while [ "$(jq -r '.stop_reason' <<<"$RESPONSE")" = "tool_use" ]; do
TOOL_RESULTS='[]'
while read -r block; do
NAME=$(jq -r '.name' <<<"$block")
INPUT=$(jq -c '.input' <<<"$block")
ID=$(jq -r '.id' <<<"$block")
if OUTPUT=$(run_tool "$NAME" "$INPUT"); then
TOOL_RESULTS=$(jq --arg id "$ID" --arg result "$OUTPUT" \
'. + [{type: "tool_result", tool_use_id: $id, content: $result}]' \
<<<"$TOOL_RESULTS")
else
# Signal failure so Claude can retry or ask for clarification.
TOOL_RESULTS=$(jq --arg id "$ID" --arg result "$OUTPUT" \
'. + [{type: "tool_result", tool_use_id: $id, content: $result, is_error: true}]' \
<<<"$TOOL_RESULTS")
fi
done < <(jq -c '.content[] | select(.type == "tool_use")' <<<"$RESPONSE")
MESSAGES=$(jq \
--argjson assistant "$(jq '.content' <<<"$RESPONSE")" \
--argjson results "$TOOL_RESULTS" \
'. + [
{role: "assistant", content: $assistant},
{role: "user", content: $results}
]' <<<"$MESSAGES")
RESPONSE=$(call_api)
done
jq -r '.content[] | select(.type == "text") | .text' <<<"$RESPONSE"
# Ring 4: Error handling.
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 a one-hour all-hands on Monday, March 30, 2026 at 10am with everyone: " + ", ".join(f"user{i}@example.com" for i in range(15)),
}
]
response = client.messages.create(
model="claude-opus-5-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:
# Signal failure so Claude can retry or ask for clarification.
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-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)
// Ring 4: Error handling.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const tools: Anthropic.Tool[] = [
{
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"],
},
},
];
function runTool(name: string, input: Record<string, unknown>) {
if (name === "create_calendar_event") {
const attendees = input.attendees as string[] | undefined;
if (attendees && attendees.length > 10) {
throw new Error("Too many attendees (max 10)");
}
return { event_id: "evt_123", status: "created", title: input.title };
}
if (name === "list_calendar_events") {
return {
events: [{ title: "Existing meeting", start: "14:00", end: "15:00" }],
};
}
throw new Error(`Unknown tool: ${name}`);
}
const emails = Array.from({ length: 15 }, (_, i) => `user${i}@example.com`);
const messages: Anthropic.MessageParam[] = [
{
role: "user",
content: `Schedule a one-hour all-hands on Monday, March 30, 2026 at 10am with everyone: ${emails.join(", ")}`,
},
];
let response = await client.messages.create({
model: "claude-opus-5-5",
max_tokens: 1024,
tools,
messages,
});
while (response.stop_reason === "tool_use") {
const toolResults: Anthropic.ToolResultBlockParam[] = [];
for (const block of response.content) {
if (block.type === "tool_use") {
try {
const result = runTool(block.name, block.input as Record<string, unknown>);
toolResults.push({
type: "tool_result",
tool_use_id: block.id,
content: JSON.stringify(result),
});
} catch (err) {
// Signal failure so Claude can retry or ask for clarification.
toolResults.push({
type: "tool_result",
tool_use_id: block.id,
content: String(err),
is_error: true,
});
}
}
}
messages.push({ role: "assistant", content: response.content });
messages.push({ role: "user", content: toolResults });
response = await client.messages.create({
model: "claude-opus-5-5",
max_tokens: 1024,
tools,
messages,
});
}
for (const block of response.content) {
if (block.type === "text") {
console.log(block.text);
}
}
// Ring 4: Error handling.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Anthropic;
using Anthropic.Models.Messages;
AnthropicClient client = new();
List<ToolUnion> tools =
[
new ToolUnion(new Tool()
{
Name = "create_calendar_event",
Description = "Create a calendar event with attendees and optional recurrence.",
InputSchema = new InputSchema()
{
Properties = new Dictionary<string, JsonElement>
{
["title"] = JsonSerializer.SerializeToElement(new { type = "string" }),
["start"] = JsonSerializer.SerializeToElement(new { type = "string", format = "date-time" }),
["end"] = JsonSerializer.SerializeToElement(new { type = "string", format = "date-time" }),
["attendees"] = JsonSerializer.SerializeToElement(new
{
type = "array",
items = new { type = "string", format = "email" },
}),
["recurrence"] = JsonSerializer.SerializeToElement(new
{
type = "object",
properties = new
{
frequency = new { @enum = new[] { "daily", "weekly", "monthly" } },
count = new { type = "integer", minimum = 1 },
},
}),
},
Required = ["title", "start", "end"],
},
}),
new ToolUnion(new Tool()
{
Name = "list_calendar_events",
Description = "List all calendar events on a given date.",
InputSchema = new InputSchema()
{
Properties = new Dictionary<string, JsonElement>
{
["date"] = JsonSerializer.SerializeToElement(new { type = "string", format = "date" }),
},
Required = ["date"],
},
}),
];
string RunTool(ToolUseBlock toolUse)
{
if (toolUse.Name == "create_calendar_event")
{
if (toolUse.Input.TryGetValue("attendees", out var attendees) && attendees.GetArrayLength() > 10)
{
throw new InvalidOperationException("Too many attendees (max 10)");
}
var title = toolUse.Input.TryGetValue("title", out var t) ? t.GetString() : "";
return JsonSerializer.Serialize(new { event_id = "evt_123", status = "created", title });
}
if (toolUse.Name == "list_calendar_events")
{
return """{"events": [{"title": "Existing meeting", "start": "14:00", "end": "15:00"}]}""";
}
throw new InvalidOperationException($"Unknown tool: {toolUse.Name}");
}
// Build a request that exceeds the tool's attendee limit so the error path runs.
var emails = string.Join(", ", Enumerable.Range(0, 15).Select(i => $"user{i}@example.com"));
List<MessageParam> messages =
[
new() { Role = Role.User, Content = $"Schedule a one-hour all-hands on Monday, March 30, 2026 at 10am with everyone: {emails}" },
];
var response = await client.Messages.Create(new MessageCreateParams
{
Model = Model.ClaudeOpus5_5,
MaxTokens = 1024,
Tools = tools,
Messages = messages,
});
while (response.StopReason == StopReason.ToolUse)
{
List<ContentBlockParam> toolResults = [];
foreach (var block in response.Content)
{
if (block.TryPickToolUse(out var toolUse))
{
ToolResultBlockParam toolResult;
try
{
toolResult = new ToolResultBlockParam() { ToolUseID = toolUse.ID, Content = RunTool(toolUse) };
}
catch (Exception e)
{
// Signal failure so Claude can retry or ask for clarification.
toolResult = new ToolResultBlockParam()
{
ToolUseID = toolUse.ID,
Content = e.Message,
IsError = true,
};
}
toolResults.Add(new ContentBlockParam(toolResult));
}
}
messages.Add(new()
{
Role = Role.Assistant,
Content = response.Content.Select(block => new ContentBlockParam(block.Json)).ToList(),
});
messages.Add(new() { Role = Role.User, Content = new MessageParamContent(toolResults) });
response = await client.Messages.Create(new MessageCreateParams
{
Model = Model.ClaudeOpus5_5,
MaxTokens = 1024,
Tools = tools,
Messages = messages,
});
}
foreach (var block in response.Content)
{
if (block.TryPickText(out var text))
{
Console.WriteLine(text.Text);
}
}
// Ring 4: Error handling.
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"strings"
"github.com/anthropics/anthropic-sdk-go"
)
func runTool(name string, input map[string]any) (string, error) {
if name == "create_calendar_event" {
if attendees, ok := input["attendees"].([]any); ok && len(attendees) > 10 {
return "", fmt.Errorf("too many attendees (max 10)")
}
title, _ := input["title"].(string)
return fmt.Sprintf(`{"event_id": "evt_123", "status": "created", "title": %q}`, title), nil
}
if name == "list_calendar_events" {
return `{"events": [{"title": "Existing meeting", "start": "14:00", "end": "15:00"}]}`, nil
}
return "", fmt.Errorf("unknown tool: %s", name)
}
func main() {
client := anthropic.NewClient()
ctx := context.Background()
tools := []anthropic.ToolUnionParam{
{OfTool: &anthropic.ToolParam{
Name: "create_calendar_event",
Description: anthropic.String("Create a calendar event with attendees and optional recurrence."),
InputSchema: anthropic.ToolInputSchemaParam{
Properties: map[string]any{
"title": map[string]any{"type": "string"},
"start": map[string]any{"type": "string", "format": "date-time"},
"end": map[string]any{"type": "string", "format": "date-time"},
"attendees": map[string]any{
"type": "array",
"items": map[string]any{"type": "string", "format": "email"},
},
"recurrence": map[string]any{
"type": "object",
"properties": map[string]any{
"frequency": map[string]any{"enum": []string{"daily", "weekly", "monthly"}},
"count": map[string]any{"type": "integer", "minimum": 1},
},
},
},
Required: []string{"title", "start", "end"},
},
}},
{OfTool: &anthropic.ToolParam{
Name: "list_calendar_events",
Description: anthropic.String("List all calendar events on a given date."),
InputSchema: anthropic.ToolInputSchemaParam{
Properties: map[string]any{
"date": map[string]any{"type": "string", "format": "date"},
},
Required: []string{"date"},
},
}},
}
// Build a request that exceeds the tool's attendee limit so the error path runs.
emails := make([]string, 15)
for i := range emails {
emails[i] = fmt.Sprintf("user%[email protected]", i)
}
messages := []anthropic.MessageParam{
anthropic.NewUserMessage(anthropic.NewTextBlock(
"Schedule a one-hour all-hands on Monday, March 30, 2026 at 10am with everyone: " + strings.Join(emails, ", "),
)),
}
response, err := client.Messages.New(ctx, anthropic.MessageNewParams{
Model: anthropic.ModelClaudeOpus5_5,
MaxTokens: 1024,
Tools: tools,
Messages: messages,
})
if err != nil {
log.Fatal(err)
}
for response.StopReason == "tool_use" {
var toolResults []anthropic.ContentBlockParamUnion
for _, block := range response.Content {
if block.Type == "tool_use" {
var input map[string]any
if err := json.Unmarshal(block.Input, &input); err != nil {
log.Fatal(err)
}
result, toolErr := runTool(block.Name, input)
if toolErr != nil {
// Signal failure so Claude can retry or ask for clarification.
toolResults = append(toolResults, anthropic.NewToolResultBlock(block.ID, toolErr.Error(), true))
} else {
toolResults = append(toolResults, anthropic.NewToolResultBlock(block.ID, result, false))
}
}
}
var assistantContent []anthropic.ContentBlockParamUnion
for _, block := range response.Content {
assistantContent = append(assistantContent, block.ToParam())
}
messages = append(messages, anthropic.NewAssistantMessage(assistantContent...))
messages = append(messages, anthropic.NewUserMessage(toolResults...))
response, err = client.Messages.New(ctx, anthropic.MessageNewParams{
Model: anthropic.ModelClaudeOpus5_5,
MaxTokens: 1024,
Tools: tools,
Messages: messages,
})
if err != nil {
log.Fatal(err)
}
}
for _, block := range response.Content {
if block.Type == "text" {
fmt.Println(block.Text)
}
}
}
// Ring 4: Error handling.
import com.anthropic.client.AnthropicClient;
import com.anthropic.client.okhttp.AnthropicOkHttpClient;
import com.anthropic.core.JsonValue;
import com.anthropic.models.messages.ContentBlock;
import com.anthropic.models.messages.ContentBlockParam;
import com.anthropic.models.messages.Message;
import com.anthropic.models.messages.MessageCreateParams;
import com.anthropic.models.messages.MessageParam;
import com.anthropic.models.messages.Model;
import com.anthropic.models.messages.StopReason;
import com.anthropic.models.messages.Tool;
import com.anthropic.models.messages.Tool.InputSchema;
import com.anthropic.models.messages.ToolResultBlockParam;
import com.anthropic.models.messages.ToolUseBlock;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
String runTool(ToolUseBlock toolUse) {
// The raw tool input is a JSON object; read fields out of it as a map.
Map<String, JsonValue> input = (Map<String, JsonValue>) toolUse._input().asObject().get();
if (toolUse.name().equals("create_calendar_event")) {
int attendeeCount = input.containsKey("attendees")
? ((List<?>) input.get("attendees").asArray().get()).size()
: 0;
if (attendeeCount > 10) {
throw new IllegalArgumentException("Too many attendees (max 10)");
}
String title = input.containsKey("title") ? input.get("title").asStringOrThrow() : "";
return "{\"event_id\": \"evt_123\", \"status\": \"created\", \"title\": \"" + title + "\"}";
}
if (toolUse.name().equals("list_calendar_events")) {
return "{\"events\": [{\"title\": \"Existing meeting\", \"start\": \"14:00\", \"end\": \"15:00\"}]}";
}
throw new IllegalArgumentException("Unknown tool: " + toolUse.name());
}
void main() {
AnthropicClient client = AnthropicOkHttpClient.fromEnv();
Tool calendarTool = Tool.builder()
.name("create_calendar_event")
.description("Create a calendar event with attendees and optional recurrence.")
.inputSchema(InputSchema.builder()
.properties(JsonValue.from(Map.of(
"title", Map.of("type", "string"),
"start", Map.of("type", "string", "format", "date-time"),
"end", Map.of("type", "string", "format", "date-time"),
"attendees", Map.of(
"type", "array",
"items", Map.of("type", "string", "format", "email")
),
"recurrence", Map.of(
"type", "object",
"properties", Map.of(
"frequency", Map.of("enum", List.of("daily", "weekly", "monthly")),
"count", Map.of("type", "integer", "minimum", 1)
)
)
)))
.required(List.of("title", "start", "end"))
.build())
.build();
Tool listTool = Tool.builder()
.name("list_calendar_events")
.description("List all calendar events on a given date.")
.inputSchema(InputSchema.builder()
.properties(JsonValue.from(Map.of(
"date", Map.of("type", "string", "format", "date")
)))
.required(List.of("date"))
.build())
.build();
// Build a request that exceeds the tool's attendee limit so the error path runs.
String emails = IntStream.range(0, 15)
.mapToObj(i -> "user" + i + "@example.com")
.collect(Collectors.joining(", "));
List<MessageParam> messages = new ArrayList<>();
messages.add(MessageParam.builder()
.role(MessageParam.Role.USER)
.content("Schedule a one-hour all-hands on Monday, March 30, 2026 at 10am with everyone: " + emails)
.build());
Message response = client.messages().create(MessageCreateParams.builder()
.model(Model.CLAUDE_OPUS_5_5)
.maxTokens(1024L)
.addTool(calendarTool)
.addTool(listTool)
.messages(messages)
.build());
while (response.stopReason().isPresent()
&& response.stopReason().get().equals(StopReason.TOOL_USE)) {
List<ContentBlockParam> toolResults = new ArrayList<>();
for (ContentBlock block : response.content()) {
if (block.toolUse().isPresent()) {
ToolUseBlock toolUse = block.toolUse().get();
ToolResultBlockParam.Builder resultBuilder = ToolResultBlockParam.builder()
.toolUseId(toolUse.id());
try {
resultBuilder.content(runTool(toolUse));
} catch (Exception e) {
// Signal failure so Claude can retry or ask for clarification.
resultBuilder.content(e.getMessage()).isError(true);
}
toolResults.add(ContentBlockParam.ofToolResult(resultBuilder.build()));
}
}
messages.add(response.toParam());
messages.add(MessageParam.builder()
.role(MessageParam.Role.USER)
.contentOfBlockParams(toolResults)
.build());
response = client.messages().create(MessageCreateParams.builder()
.model(Model.CLAUDE_OPUS_5_5)
.maxTokens(1024L)
.addTool(calendarTool)
.addTool(listTool)
.messages(messages)
.build());
}
response.content().stream()
.flatMap(block -> block.text().stream())
.forEach(textBlock -> IO.println(textBlock.text()));
}
<?php
// Ring 4: Error handling.
use Anthropic\Client;
$client = new Client();
$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'],
],
],
];
function runTool(string $name, array $input): string
{
if ($name === 'create_calendar_event') {
if (count($input['attendees'] ?? []) > 10) {
throw new InvalidArgumentException('Too many attendees (max 10)');
}
return json_encode([
'event_id' => 'evt_123',
'status' => 'created',
'title' => $input['title'],
]);
}
if ($name === 'list_calendar_events') {
return json_encode([
'events' => [['title' => 'Existing meeting', 'start' => '14:00', 'end' => '15:00']],
]);
}
throw new InvalidArgumentException("Unknown tool: {$name}");
}
// Build a request that exceeds the tool's attendee limit so the error path runs.
$emails = array_map(fn (int $i): string => "user{$i}@example.com", range(0, 14));
$messages = [
[
'role' => 'user',
'content' => 'Schedule a one-hour all-hands on Monday, March 30, 2026 at 10am with everyone: ' . implode(', ', $emails),
],
];
$response = $client->messages->create(
model: 'claude-opus-5-5',
maxTokens: 1024,
tools: $tools,
messages: $messages,
);
while ($response->stopReason === 'tool_use') {
$toolResults = [];
foreach ($response->content as $block) {
if ($block->type === 'tool_use') {
try {
$toolResults[] = [
'type' => 'tool_result',
'tool_use_id' => $block->id,
'content' => runTool($block->name, $block->input),
];
} catch (Exception $e) {
// Signal failure so Claude can retry or ask for clarification.
$toolResults[] = [
'type' => 'tool_result',
'tool_use_id' => $block->id,
'content' => $e->getMessage(),
'is_error' => true,
];
}
}
}
$messages[] = ['role' => 'assistant', 'content' => $response->content];
$messages[] = ['role' => 'user', 'content' => $toolResults];
$response = $client->messages->create(
model: 'claude-opus-5-5',
maxTokens: 1024,
tools: $tools,
messages: $messages,
);
}
foreach ($response->content as $block) {
if ($block->type === 'text') {
echo $block->text, "\n";
}
}
# Ring 4: Error handling.
require "anthropic"
client = Anthropic::Client.new
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, input)
case name
when "create_calendar_event"
attendees = input[:attendees]
raise ArgumentError, "Too many attendees (max 10)" if attendees && attendees.length > 10
JSON.generate({event_id: "evt_123", status: "created", title: input[:title]})
when "list_calendar_events"
JSON.generate({events: [{title: "Existing meeting", start: "14:00", end: "15:00"}]})
else
raise ArgumentError, "Unknown tool: #{name}"
end
end
# Build a request that exceeds the tool's attendee limit so the error path runs.
emails = (0...15).map { |i| "user#{i}@example.com" }
messages = [
{
role: "user",
content: "Schedule a one-hour all-hands on Monday, March 30, 2026 at 10am with everyone: #{emails.join(\", \")}"
}
]
response = client.messages.create(
model: "claude-opus-5-5",
max_tokens: 1024,
tools: tools,
messages: messages
)
while response.stop_reason == :tool_use
tool_results = response.content.select { |block| block.type == :tool_use }.map do |tool_use|
begin
{
type: "tool_result",
tool_use_id: tool_use.id,
content: run_tool(tool_use.name, tool_use.input)
}
rescue => e
# Signal failure so Claude can retry or ask for clarification.
{
type: "tool_result",
tool_use_id: tool_use.id,
content: e.message,
is_error: true
}
end
end
messages << {role: "assistant", content: response.content}
messages << {role: "user", content: tool_results}
response = client.messages.create(
model: "claude-opus-5-5",
max_tokens: 1024,
tools: tools,
messages: messages
)
end
response.content.each do |block|
puts block.text if block.type == :text
end
무엇을 기대할까요
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는 플래그와 오류 텍스트를 보고 그에 따라 응답해요. 전체 오류 처리 레퍼런스는 도구 호출 처리 참고.
원 5: Tool Runner SDK 추상화
원 2부터 4까지는 같은 루프를 직접 작성했어요. API 호출, stop_reason 확인, 도구 실행, 결과 추가, 반복. Tool Runner가 이걸 대신 해줘요. 각 도구를 함수로 정의하고, 목록을 tool_runner에 전달하고, 루프가 끝나면 최종 메시지를 가져오면 돼요. 오류 감싸기, 결과 형식화, 대화 관리는 내부적으로 처리돼요.
각 SDK는 일반 함수를 실행 가능한 도구로 바꾸고 서명에서 입력 스키마를 파생하는 헬퍼를 제공해요. 아래 탭은 각 언어의 관용적 형태를 보여줘요.
The Tool Runner SDK abstraction is available in all seven SDKs: Python,
TypeScript, C#, Go, Java, PHP, and Ruby. There is no equivalent for raw
curl requests. Switch to any SDK tab to see Ring 5, or keep the Ring 4
loop as your shell implementation.
```bash CLI
#!/usr/bin/env bash
# Ring 5: The Tool Runner SDK abstraction.
set -euo pipefail
# The Tool Runner SDK abstraction is available in all seven SDKs: Python,
# TypeScript, C#, Go, Java, PHP, and Ruby. The ant CLI exposes the Messages
# API directly and has no equivalent helper. Switch to any SDK tab to see
# Ring 5, or keep the Ring 4 loop as your CLI implementation.
# Ring 5: The Tool Runner SDK abstraction.
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-5",
max_tokens=1024,
tools=[create_calendar_event, list_calendar_events],
messages=[
{
"role": "user",
"content": "Check what I have on Monday, March 30, 2026, then schedule a one-hour planning session that day that avoids any conflicts.",
}
],
).until_done()
for block in final_message.content:
if block.type == "text":
print(block.text)
// Ring 5: The Tool Runner SDK abstraction.
import Anthropic from "@anthropic-ai/sdk";
import { betaZodTool } from "@anthropic-ai/sdk/helpers/beta/zod";
import { z } from "zod";
const client = new Anthropic();
const createCalendarEvent = betaZodTool({
name: "create_calendar_event",
description:
"Create a calendar event with attendees and optional recurrence.",
inputSchema: z.object({
title: z.string(),
start: z.string().datetime(),
end: z.string().datetime(),
attendees: z.array(z.string().email()).optional(),
recurrence: z
.object({
frequency: z.enum(["daily", "weekly", "monthly"]),
count: z.number().int().min(1),
})
.optional(),
}),
run: async (input) => {
if (input.attendees && input.attendees.length > 10) {
throw new Error("Too many attendees (max 10)");
}
return JSON.stringify({
event_id: "evt_123",
status: "created",
title: input.title,
});
},
});
const listCalendarEvents = betaZodTool({
name: "list_calendar_events",
description: "List all calendar events on a given date.",
inputSchema: z.object({
date: z.string().date(),
}),
run: async () => {
return JSON.stringify({
events: [{ title: "Existing meeting", start: "14:00", end: "15:00" }],
});
},
});
const finalMessage = await client.beta.messages.toolRunner({
model: "claude-opus-5-5",
max_tokens: 1024,
tools: [createCalendarEvent, listCalendarEvents],
messages: [
{
role: "user",
content:
"Check what I have on Monday, March 30, 2026, then schedule a one-hour planning session that day that avoids any conflicts.",
},
],
});
for (const block of finalMessage.content) {
if (block.type === "text") {
console.log(block.text);
}
}
// Ring 5: The Tool Runner SDK abstraction.
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading.Tasks;
using Anthropic;
using Anthropic.Helpers.Beta;
using Anthropic.Models.Beta.Messages;
using MessageCreateParams = Anthropic.Models.Beta.Messages.MessageCreateParams;
using InputSchema = Anthropic.Models.Beta.Messages.InputSchema;
using Role = Anthropic.Models.Beta.Messages.Role;
using Model = Anthropic.Models.Messages.Model;
AnthropicClient client = new();
// Define each tool as a runnable tool: the definition carries the JSON Schema
// and the Run callback holds the implementation. Throwing an exception sends
// the message back to Claude as a tool result with is_error set.
var createCalendarEvent = new BetaRunnableTool
{
Name = "create_calendar_event",
Definition = new BetaTool
{
Name = "create_calendar_event",
Description = "Create a calendar event with attendees and optional recurrence.",
InputSchema = new InputSchema
{
Properties = new Dictionary<string, JsonElement>
{
["title"] = JsonSerializer.SerializeToElement(new { type = "string", description = "Event title" }),
["start"] = JsonSerializer.SerializeToElement(new { type = "string", description = "Start time in ISO 8601 format" }),
["end"] = JsonSerializer.SerializeToElement(new { type = "string", description = "End time in ISO 8601 format" }),
["attendees"] = JsonSerializer.SerializeToElement(new
{
type = "array",
items = new { type = "string" },
description = "Email addresses to invite",
}),
["recurrence"] = JsonSerializer.SerializeToElement(new
{
type = "object",
properties = new
{
frequency = new { @enum = new[] { "daily", "weekly", "monthly" } },
count = new { type = "integer", minimum = 1 },
},
}),
},
Required = ["title", "start", "end"],
},
},
Run = (toolUse, _) =>
{
if (toolUse.Input.TryGetValue("attendees", out var attendees) && attendees.GetArrayLength() > 10)
{
throw new InvalidOperationException("Too many attendees (max 10)");
}
var title = toolUse.Input.TryGetValue("title", out var t) ? t.GetString() : "";
return Task.FromResult<BetaToolResultBlockParamContent>(
JsonSerializer.Serialize(new { event_id = "evt_123", status = "created", title })
);
},
};
var listCalendarEvents = new BetaRunnableTool
{
Name = "list_calendar_events",
Definition = new BetaTool
{
Name = "list_calendar_events",
Description = "List all calendar events on a given date.",
InputSchema = new InputSchema
{
Properties = new Dictionary<string, JsonElement>
{
["date"] = JsonSerializer.SerializeToElement(new { type = "string", description = "Date in YYYY-MM-DD format" }),
},
Required = ["date"],
},
},
Run = (toolUse, _) => Task.FromResult<BetaToolResultBlockParamContent>(
"""{"events": [{"title": "Existing meeting", "start": "14:00", "end": "15:00"}]}"""
),
};
// The runner calls the API, runs requested tools, and feeds results back
// until Claude produces a final answer.
var runner = client.Beta.Messages.ToolRunner(
new MessageCreateParams
{
Model = Model.ClaudeOpus5_5,
MaxTokens = 1024,
Messages =
[
new()
{
Role = Role.User,
Content = "Check what I have on Monday, March 30, 2026, then schedule a one-hour planning session that day that avoids any conflicts.",
},
],
},
[createCalendarEvent, listCalendarEvents]
);
BetaMessage? finalMessage = null;
await foreach (var message in runner)
{
finalMessage = message;
}
foreach (var block in finalMessage!.Content)
{
if (block.TryPickText(out var text))
{
Console.WriteLine(text.Text);
}
}
// Ring 5: The Tool Runner SDK abstraction.
package main
import (
"context"
"fmt"
"log"
"github.com/anthropics/anthropic-sdk-go"
"github.com/anthropics/anthropic-sdk-go/toolrunner"
)
// The input structs define each tool's schema. The tool runner generates the
// JSON Schema from the struct fields and their jsonschema tags.
type RecurrenceInput struct {
Frequency string `json:"frequency,omitempty" jsonschema:"enum=daily,enum=weekly,enum=monthly,description=How often the event repeats"`
Count int `json:"count,omitempty" jsonschema:"description=Number of occurrences"`
}
type CreateCalendarEventInput struct {
Title string `json:"title" jsonschema:"required,description=Event title"`
Start string `json:"start" jsonschema:"required,description=Start time in ISO 8601 format"`
End string `json:"end" jsonschema:"required,description=End time in ISO 8601 format"`
Attendees []string `json:"attendees,omitempty" jsonschema:"description=Email addresses to invite"`
Recurrence *RecurrenceInput `json:"recurrence,omitempty"`
}
type ListCalendarEventsInput struct {
Date string `json:"date" jsonschema:"required,description=Date in YYYY-MM-DD format"`
}
func main() {
client := anthropic.NewClient()
ctx := context.Background()
// Define each tool as a handler function. Returning an error sends the
// message back to Claude as a tool result with is_error set.
createCalendarEvent, err := toolrunner.NewBetaToolFromJSONSchema(
"create_calendar_event",
"Create a calendar event with attendees and optional recurrence.",
func(ctx context.Context, input CreateCalendarEventInput) (anthropic.BetaToolResultBlockParamContentUnion, error) {
if len(input.Attendees) > 10 {
return anthropic.BetaToolResultBlockParamContentUnion{}, fmt.Errorf("too many attendees (max 10)")
}
return anthropic.BetaToolResultBlockParamContentUnion{
OfText: &anthropic.BetaTextBlockParam{
Text: fmt.Sprintf(`{"event_id": "evt_123", "status": "created", "title": %q}`, input.Title),
},
}, nil
},
)
if err != nil {
log.Fatal(err)
}
listCalendarEvents, err := toolrunner.NewBetaToolFromJSONSchema(
"list_calendar_events",
"List all calendar events on a given date.",
func(ctx context.Context, input ListCalendarEventsInput) (anthropic.BetaToolResultBlockParamContentUnion, error) {
return anthropic.BetaToolResultBlockParamContentUnion{
OfText: &anthropic.BetaTextBlockParam{
Text: `{"events": [{"title": "Existing meeting", "start": "14:00", "end": "15:00"}]}`,
},
}, nil
},
)
if err != nil {
log.Fatal(err)
}
// The runner calls the API, runs requested tools, and feeds results back
// until Claude produces a final answer.
runner := client.Beta.Messages.NewToolRunner(
[]anthropic.BetaTool{createCalendarEvent, listCalendarEvents},
anthropic.BetaToolRunnerParams{
BetaMessageNewParams: anthropic.BetaMessageNewParams{
Model: anthropic.ModelClaudeOpus5_5,
MaxTokens: 1024,
Messages: []anthropic.BetaMessageParam{
anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock(
"Check what I have on Monday, March 30, 2026, then schedule a one-hour planning session that day that avoids any conflicts.",
)),
},
},
},
)
var finalMessage *anthropic.BetaMessage
for message, err := range runner.All(ctx) {
if err != nil {
log.Fatal(err)
}
finalMessage = message
}
for _, block := range finalMessage.Content {
if block.Type == "text" {
fmt.Println(block.Text)
}
}
}
// Ring 5: The Tool Runner SDK abstraction.
import com.anthropic.client.AnthropicClient;
import com.anthropic.client.okhttp.AnthropicOkHttpClient;
import com.anthropic.helpers.BetaToolRunner;
import com.anthropic.models.beta.messages.BetaMessage;
import com.anthropic.models.beta.messages.MessageCreateParams;
import com.anthropic.models.messages.Model;
import com.fasterxml.jackson.annotation.JsonClassDescription;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import java.util.List;
import java.util.function.Supplier;
// Define each tool as a class: the fields describe the input schema, and the
// get() method holds the implementation. Throwing an exception sends the
// message back to Claude as a tool result with is_error set.
@JsonClassDescription("Create a calendar event with attendees.")
static class CreateCalendarEvent implements Supplier<String> {
@JsonPropertyDescription("Event title")
public String title;
@JsonPropertyDescription("Start time in ISO 8601 format")
public String start;
@JsonPropertyDescription("End time in ISO 8601 format")
public String end;
@JsonPropertyDescription("Email addresses to invite")
public List<String> attendees;
@Override
public String get() {
if (attendees != null && attendees.size() > 10) {
throw new IllegalArgumentException("Too many attendees (max 10)");
}
return "{\"event_id\": \"evt_123\", \"status\": \"created\", \"title\": \"" + title + "\"}";
}
}
@JsonClassDescription("List all calendar events on a given date.")
static class ListCalendarEvents implements Supplier<String> {
@JsonPropertyDescription("Date in YYYY-MM-DD format")
public String date;
@Override
public String get() {
return "{\"events\": [{\"title\": \"Existing meeting\", \"start\": \"14:00\", \"end\": \"15:00\"}]}";
}
}
void main() {
AnthropicClient client = AnthropicOkHttpClient.fromEnv();
// The runner calls the API, runs requested tools, and feeds results back
// until Claude produces a final answer.
BetaToolRunner runner = client.beta()
.messages()
.toolRunner(MessageCreateParams.builder()
.model(Model.CLAUDE_OPUS_5_5)
.maxTokens(1024)
.addBeta("structured-outputs-2025-11-13")
.addUserMessage("Check what I have on Monday, March 30, 2026, then schedule a one-hour planning session that day that avoids any conflicts.")
.addTool(CreateCalendarEvent.class)
.addTool(ListCalendarEvents.class)
.build());
BetaMessage finalMessage = null;
for (BetaMessage message : runner) {
finalMessage = message;
}
finalMessage.content().stream()
.flatMap(block -> block.text().stream())
.forEach(textBlock -> IO.println(textBlock.text()));
}
<?php
// Ring 5: The Tool Runner SDK abstraction.
use Anthropic\Client;
use Anthropic\Lib\Tools\BetaRunnableTool;
use Anthropic\Messages\Model;
$client = new Client();
// Define each tool as a runnable tool: the definition carries the JSON Schema
// and the run closure holds the implementation. Throwing an exception sends the
// message back to Claude as a tool result with is_error set.
$createCalendarEvent = new BetaRunnableTool(
definition: [
'name' => 'create_calendar_event',
'description' => 'Create a calendar event with attendees and optional recurrence.',
'input_schema' => [
'type' => 'object',
'properties' => [
'title' => ['type' => 'string', 'description' => 'Event title'],
'start' => ['type' => 'string', 'description' => 'Start time in ISO 8601 format'],
'end' => ['type' => 'string', 'description' => 'End time in ISO 8601 format'],
'attendees' => [
'type' => 'array',
'items' => ['type' => 'string'],
'description' => 'Email addresses to invite',
],
'recurrence' => [
'type' => 'object',
'properties' => [
'frequency' => ['enum' => ['daily', 'weekly', 'monthly']],
'count' => ['type' => 'integer', 'minimum' => 1],
],
],
],
'required' => ['title', 'start', 'end'],
],
],
run: function (array $input): string {
if (count($input['attendees'] ?? []) > 10) {
throw new InvalidArgumentException('Too many attendees (max 10)');
}
return json_encode([
'event_id' => 'evt_123',
'status' => 'created',
'title' => $input['title'],
]);
},
);
$listCalendarEvents = new BetaRunnableTool(
definition: [
'name' => 'list_calendar_events',
'description' => 'List all calendar events on a given date.',
'input_schema' => [
'type' => 'object',
'properties' => [
'date' => ['type' => 'string', 'description' => 'Date in YYYY-MM-DD format'],
],
'required' => ['date'],
],
],
run: fn (array $input): string => json_encode([
'events' => [['title' => 'Existing meeting', 'start' => '14:00', 'end' => '15:00']],
]),
);
// The runner calls the API, runs requested tools, and feeds results back
// until Claude produces a final answer.
$runner = $client->beta->messages->toolRunner(
maxTokens: 1024,
messages: [
[
'role' => 'user',
'content' => 'Check what I have on Monday, March 30, 2026, then schedule a one-hour planning session that day that avoids any conflicts.',
],
],
model: Model::CLAUDE_OPUS_5_5,
tools: [$createCalendarEvent, $listCalendarEvents],
);
$finalMessage = null;
foreach ($runner as $message) {
$finalMessage = $message;
}
foreach ($finalMessage->content as $block) {
if ($block->type === 'text') {
echo $block->text, "\n";
}
}
# Ring 5: The Tool Runner SDK abstraction.
require "anthropic"
client = Anthropic::Client.new
# Define each tool as a class: a typed input model describes the schema, and
# the call method holds the implementation. Raising an error sends the message
# back to Claude as a tool result with is_error set.
class RecurrenceInput < Anthropic::BaseModel
optional :frequency, Anthropic::InputSchema::EnumOf["daily", "weekly", "monthly"],
doc: "How often the event repeats"
optional :count, Integer, doc: "Number of occurrences"
end
class CreateCalendarEventInput < Anthropic::BaseModel
required :title, String, doc: "Event title"
required :start, String, doc: "Start time in ISO 8601 format"
required :end, String, doc: "End time in ISO 8601 format"
optional :attendees, Anthropic::InputSchema::ArrayOf[String], doc: "Email addresses to invite"
optional :recurrence, RecurrenceInput, doc: "Optional recurrence rule"
end
class CreateCalendarEvent < Anthropic::BaseTool
doc "Create a calendar event with attendees and optional recurrence."
input_schema CreateCalendarEventInput
def call(input)
raise ArgumentError, "Too many attendees (max 10)" if input.attendees && input.attendees.length > 10
JSON.generate({event_id: "evt_123", status: "created", title: input.title})
end
end
class ListCalendarEventsInput < Anthropic::BaseModel
required :date, String, doc: "Date in YYYY-MM-DD format"
end
class ListCalendarEvents < Anthropic::BaseTool
doc "List all calendar events on a given date."
input_schema ListCalendarEventsInput
def call(input)
JSON.generate({events: [{title: "Existing meeting", start: "14:00", end: "15:00"}]})
end
end
# The runner calls the API, runs requested tools, and feeds results back
# until Claude produces a final answer.
runner = client.beta.messages.tool_runner(
model: "claude-opus-5-5",
max_tokens: 1024,
tools: [CreateCalendarEvent.new, ListCalendarEvents.new],
messages: [
{
role: "user",
content: "Check what I have on Monday, March 30, 2026, then schedule a one-hour planning session that day that avoids any conflicts."
}
]
)
final_message = nil
runner.each_message { |message| final_message = message }
final_message.content.each do |block|
puts block.text if block.type == :text
end
무엇을 기대할까요
I checked your calendar for Monday, March 30 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 신호.