마이그레이션
마이그레이션 (Migration)
Claude Managed Agents는 여러분이 직접 작성한 에이전트 루프를 관리형 인프라로 대체해요. 이 페이지는 Messages API 기반의 커스텀 루프에서 또는 Claude Agent SDK에서 마이그레이션할 때 무엇이 바뀌는지 다뤄요.
출처: 문서
본문
Claude Managed Agents는 여러분이 직접 작성한 에이전트 루프를 관리형 인프라로 대체해요. 이 페이지는 Messages API 기반의 커스텀 루프에서 또는 Claude Agent SDK에서 마이그레이션할 때 무엇이 바뀌는지 다뤄요.
Messages API 에이전트 루프에서 (From a Messages API agent loop)
while 루프 안에서 messages.create를 호출하고, 도구 호출을 직접 실행하고, 결과를 대화 기록에 추가하는 방식으로 에이전트를 만들었다면 그 코드의 대부분이 사라져요.
더 이상 관리하지 않게 되는 것 (What you stop managing)
| 이전 (Before) | 이후 (After) |
|---|---|
| 대화 기록 배열을 유지하고 매 턴마다 다시 전달했어요. | 세션이 서버 측에 기록을 저장해요. 이벤트를 보내고, 이벤트를 받아요. |
tool_use 콘텐츠 블록을 순회하며 각 도구를 실행하고 tool_result 메시지로 루프를 돌렸어요. |
사전 구축된 도구가 샌드박스 안에서 자동으로 실행돼요. 커스텀 도구만 agent.custom_tool_use 이벤트로 처리하면 돼요. |
| 에이전트가 생성한 코드를 실행하기 위해 자체 샌드박스를 프로비저닝했어요. | 세션 샌드박스가 코드 실행, 파일 작업, bash를 처리해요. |
| 루프가 끝났을 때를 직접 결정했어요. | 에이전트가 더 할 일이 없을 때 세션이 session.status_idle을 발행해요. |
코드 비교 (Code comparison)
이전 (Messages API 루프, 단순화):
const messages: Anthropic.MessageParam[] = [{ role: "user", content: task }];
while (true) {
const response = await client.messages.create({
model: "claude-opus-5-5",
max_tokens: 1024,
messages,
tools
});
messages.push({ role: "assistant", content: response.content });
if (response.stop_reason === "end_turn") {
break;
}
for (const block of response.content) {
if (block.type === "tool_use") {
const result = executeTool(block.name, block.input);
messages.push({
role: "user",
content: [
{
type: "tool_result",
tool_use_id: block.id,
content: result
}
]
});
}
}
}
List<MessageParam> messages = [new() { Role = Role.User, Content = task }];
while (true)
{
var response = await client.Messages.Create(new()
{
Model = Model.ClaudeOpus5_5,
MaxTokens = 1024,
Messages = messages,
Tools = tools,
});
messages.Add(new()
{
Role = Role.Assistant,
Content = new([.. response.Content.Select(block => new ContentBlockParam(block.Json))]),
});
if (response.StopReason == StopReason.EndTurn)
{
break;
}
foreach (var block in response.Content)
{
if (block.Value is ToolUseBlock toolUse)
{
var result = ExecuteTool(toolUse.Name, toolUse.Input);
messages.Add(new()
{
Role = Role.User,
Content = new([new ToolResultBlockParam { ToolUseID = toolUse.ID, Content = result }]),
});
}
}
}
messages := []anthropic.MessageParam{
anthropic.NewUserMessage(anthropic.NewTextBlock(task)),
}
for {
response, err := client.Messages.New(ctx, anthropic.MessageNewParams{
Model: anthropic.ModelClaudeOpus5_5,
MaxTokens: 1024,
Messages: messages,
Tools: tools,
})
if err != nil {
log.Fatal(err)
}
messages = append(messages, response.ToParam())
if response.StopReason == anthropic.StopReasonEndTurn {
break
}
for _, block := range response.Content {
if toolUse, ok := block.AsAny().(anthropic.ToolUseBlock); ok {
result := executeTool(toolUse.Name, toolUse.Input)
messages = append(messages, anthropic.NewUserMessage(
anthropic.NewToolResultBlock(toolUse.ID, result, false),
))
}
}
}
var messages = new ArrayList<MessageParam>();
messages.add(MessageParam.builder()
.role(MessageParam.Role.USER)
.content(task)
.build());
while (true) {
var response = client.messages().create(MessageCreateParams.builder()
.model(Model.CLAUDE_OPUS_5_5)
.maxTokens(1024)
.messages(messages)
.tools(tools)
.build());
messages.add(response.toParam());
if (StopReason.END_TURN.equals(response.stopReason().orElse(null))) {
break;
}
for (var block : response.content()) {
block.toolUse().ifPresent(toolUse -> {
var result = executeTool(toolUse.name(), toolUse._input());
messages.add(MessageParam.builder()
.role(MessageParam.Role.USER)
.contentOfBlockParams(List.of(
ContentBlockParam.ofToolResult(ToolResultBlockParam.builder()
.toolUseId(toolUse.id())
.content(result)
.build())))
.build());
});
}
}
$messages = [['role' => 'user', 'content' => $task]];
while (true) {
$response = $client->messages->create(
model: 'claude-opus-5-5',
maxTokens: 1024,
messages: $messages,
tools: $tools,
);
$messages[] = ['role' => 'assistant', 'content' => $response->content];
if ($response->stopReason === 'end_turn') {
break;
}
foreach ($response->content as $block) {
if ($block->type === 'tool_use') {
$result = executeTool($block->name, $block->input);
$messages[] = [
'role' => 'user',
'content' => [
[
'type' => 'tool_result',
'tool_use_id' => $block->id,
'content' => $result,
],
],
];
}
}
}
messages = [{ role: "user", content: task }]
loop do
response = client.messages.create(
model: "claude-opus-5-5",
max_tokens: 1024,
messages: messages,
tools: tools
)
messages << { role: "assistant", content: response.content }
break if response.stop_reason == :end_turn
response.content.each do |block|
next unless block.type == :tool_use
result = execute_tool(block.name, block.input)
messages << {
role: "user",
content: [
{
type: "tool_result",
tool_use_id: block.id,
content: result
}
]
}
end
end
이후 (Claude Managed Agents):
session_id=$(
curl --fail-with-body -sS "https://api.anthropic.com/v1/sessions?beta=true"
-H "x-api-key: ${ANTH...EY}"
-H "anthropic-version: 2023-06-01"
-H "anthropic-beta: managed-agents-2026-04-01"
--json "$(jq -n --argjson a "${agent}" --arg env "${environment_id}"
'{agent: {type: "agent", id: $a.id, version: $a.version}, environment_id: $env}')"
| jq -r '.id'
)
Open the SSE stream in the background, then send the user message.
stream_log=$(mktemp)
curl --fail-with-body -sS -N
"https://api.anthropic.com/v1/sessions/${session_id}/events/stream?beta=true"
-H "x-api-key: ${ANTH...EY}"
-H "anthropic-version: 2023-06-01"
-H "anthropic-beta: managed-agents-2026-04-01"
> "${stream_log}" &
stream_pid=$!
curl --fail-with-body -sS
"https://api.anthropic.com/v1/sessions/${session_id}/events?beta=true"
-H "x-api-key: ${ANTH...EY}"
-H "anthropic-version: 2023-06-01"
-H "anthropic-beta: managed-agents-2026-04-01"
--json "$(jq -n --arg text "${task}"
'{events: [{type: "user.message", content: [{type: "text", text: $text}]}]}')"
> /dev/null
Wait for the session to go idle. grep exits at the first match, and
reading via process substitution means the shell doesn't wait for
tail (a foreground tail -f | grep -m1 pipeline would hang: tail
only dies on its next write, which never comes once the stream is idle).
grep -m1 '"session.status_idle"' <(tail -f -n +1 "${stream_log}") > /dev/null
kill "${stream_pid}" 2>/dev/null || true
<MultiFileExample language="cli" label="CLI">
```bash CLI
ant apply agent.md
session_id=$(ant beta:sessions create \
--agent "{type: agent, id: $agent_id, version: $agent_version}" \
--environment-id "$environment_id" \
--transform id --raw-output)
# Open the stream first, then send the user message
exec {stream}< <(ant beta:sessions:events stream \
--session-id "$session_id" \
--transform type --raw-output)
ant beta:sessions:events send \
--session-id "$session_id" \
--event "{type: user.message, content: [{type: text, text: \"$task\"}]}" \
> /dev/null
# Wait for the session to go idle (grep exits at the first match)
grep -m1 -x 'session.status_idle' <&"$stream" > /dev/null
exec {stream}<&-
```
<File filename="agent.md">
```markdown
---
name: Task Runner
model: claude-opus-5-5
tools:
- type: agent_toolset_20260401
---
```
</File>
</MultiFileExample>
```python Python
agent = client.beta.agents.create(
name="Task Runner",
model="claude-opus-5-5",
tools=[{"type": "agent_toolset_20260401"}],
)
session = client.beta.sessions.create(
agent={"type": "agent", "id": agent.id, "version": agent.version},
environment_id=environment.id,
)
with client.beta.sessions.events.stream(session.id) as stream:
client.beta.sessions.events.send(
session.id,
events=[{"type": "user.message", "content": [{"type": "text", "text": task}]}],
)
for event in stream:
if event.type == "session.status_idle":
break
const agent = await client.beta.agents.create({
name: "Task Runner",
model: "claude-opus-5-5",
tools: [{ type: "agent_toolset_20260401" }]
});
const session = await client.beta.sessions.create({
agent: { type: "agent", id: agent.id, version: agent.version },
environment_id: environment.id
});
const stream = await client.beta.sessions.events.stream(session.id);
await client.beta.sessions.events.send(session.id, {
events: [
{
type: "user.message",
content: [{ type: "text", text: task }]
}
]
});
for await (const event of stream) {
if (event.type === "session.status_idle") {
break;
}
}
var agent = await client.Beta.Agents.Create(new()
{
Name = "Task Runner",
Model = BetaManagedAgentsModel.ClaudeOpus5_5,
Tools =
[
new BetaManagedAgentsAgentToolset20260401Params
{
Type = "agent_toolset_20260401",
},
],
});
var session = await client.Beta.Sessions.Create(new()
{
Agent = new BetaManagedAgentsAgentParams
{
Type = "agent",
ID = agent.ID,
Version = agent.Version,
},
EnvironmentID = environment.ID,
});
var stream = client.Beta.Sessions.Events.StreamStreaming(session.ID);
await client.Beta.Sessions.Events.Send(session.ID, new()
{
Events =
[
new BetaManagedAgentsUserMessageEventParams
{
Type = "user.message",
Content = [new BetaManagedAgentsTextBlock { Type = "text", Text = task }],
},
],
});
await foreach (var streamEvent in stream)
{
if (streamEvent.Value is BetaManagedAgentsSessionStatusIdleEvent)
{
break;
}
}
agent, err := client.Beta.Agents.New(ctx, anthropic.BetaAgentNewParams{
Name: "Task Runner",
Model: anthropic.BetaManagedAgentsModelConfigParams{
ID: anthropic.BetaManagedAgentsModelClaudeOpus5_5,
},
Tools: []anthropic.BetaAgentNewParamsToolUnion{{
OfAgentToolset20260401: &anthropic.BetaManagedAgentsAgentToolset20260401Params{
Type: anthropic.BetaManagedAgentsAgentToolset20260401ParamsTypeAgentToolset20260401,
},
}},
})
if err != nil {
log.Fatal(err)
}
session, err := client.Beta.Sessions.New(ctx, anthropic.BetaSessionNewParams{
Agent: anthropic.BetaSessionNewParamsAgentUnion{
OfBetaManagedAgentsAgents: &anthropic.BetaManagedAgentsAgentParams{
Type: anthropic.BetaManagedAgentsAgentParamsTypeAgent,
ID: agent.ID,
Version: anthropic.Int(agent.Version),
},
},
EnvironmentID: environment.ID,
})
if err != nil {
log.Fatal(err)
}
stream := client.Beta.Sessions.Events.StreamEvents(ctx, session.ID, anthropic.BetaSessionEventStreamParams{})
defer stream.Close()
_, err = client.Beta.Sessions.Events.Send(ctx, session.ID, anthropic.BetaSessionEventSendParams{
Events: []anthropic.BetaManagedAgentsEventParamsUnion{{
OfUserMessage: &anthropic.BetaManagedAgentsUserMessageEventParams{
Type: anthropic.BetaManagedAgentsUserMessageEventParamsTypeUserMessage,
Content: []anthropic.BetaManagedAgentsUserMessageEventParamsContentUnion{{
OfText: &anthropic.BetaManagedAgentsTextBlockParam{
Type: anthropic.BetaManagedAgentsTextBlockTypeText,
Text: task,
},
}},
},
}},
})
if err != nil {
log.Fatal(err)
}
for stream.Next() {
event := stream.Current()
if event.Type == "session.status_idle" {
break
}
}
if err := stream.Err(); err != nil {
log.Fatal(err)
}
var agent = client.beta().agents().create(
AgentCreateParams.builder()
.name("Task Runner")
.model(BetaManagedAgentsModel.CLAUDE_OPUS_5_5)
.addTool(
BetaManagedAgentsAgentToolset20260401Params.builder()
.type(BetaManagedAgentsAgentToolset20260401Params.Type.AGENT_TOOLSET_20260401)
.build()
)
.build()
);
var session = client.beta().sessions().create(
SessionCreateParams.builder()
.agent(
BetaManagedAgentsAgentParams.builder()
.type(BetaManagedAgentsAgentParams.Type.AGENT)
.id(agent.id())
.version(agent.version())
.build()
)
.environmentId(environment.id())
.build()
);
try (var stream = client.beta().sessions().events().streamStreaming(session.id())) {
client.beta().sessions().events().send(
session.id(),
EventSendParams.builder()
.addEvent(
BetaManagedAgentsUserMessageEventParams.builder()
.type(BetaManagedAgentsUserMessageEventParams.Type.USER_MESSAGE)
.addTextContent(task)
.build()
)
.build()
);
stream.stream()
.takeWhile(event -> !event.isSessionStatusIdle())
.forEach(_ -> {});
}
$agent = $client->beta->agents->create(
name: 'Task Runner',
model: 'claude-opus-5-5',
tools: [
BetaManagedAgentsAgentToolset20260401Params::with(
type: 'agent_toolset_20260401',
),
],
);
$session = $client->beta->sessions->create(
agent: BetaManagedAgentsAgentParams::with(
type: 'agent',
id: $agent->id,
version: $agent->version,
),
environmentID: $environment->id,
);
$stream = $client->beta->sessions->events->streamStream($session->id);
$client->beta->sessions->events->send(
$session->id,
events: [
[
'type' => 'user.message',
'content' => [['type' => 'text', 'text' => $task]],
],
],
);
foreach ($stream as $event) {
if ($event->type === 'session.status_idle') {
break;
}
}
agent = client.beta.agents.create(
name: "Task Runner",
model: "claude-opus-5-5",
tools: [{type: "agent_toolset_20260401"}]
)
session = client.beta.sessions.create(
agent: {type: "agent", id: agent.id, version: agent.version},
environment_id: environment.id
)
stream = client.beta.sessions.events.stream_events(session.id)
client.beta.sessions.events.send_(
session.id,
events: [{type: "user.message", content: [{type: "text", text: task}]}]
)
stream.each do
break if it.type == :"session.status_idle"
end
여전히 제어하는 것 (What you still control)
- 시스템 프롬프트와 모델: 동일한 필드, 이제 에이전트 정의에 있어요.
- 커스텀 도구: 여전히 JSON Schema로 선언해요. 실행이 인라인 처리에서
agent.custom_tool_use이벤트에 응답하는 것으로 옮겨요. 세션 이벤트 스트림을 참고하세요. - 웹 검색과 웹 가져오기 설정: 동일한
allowed_domains,blocked_domains,max_content_tokens,user_location필드, 이제는 매 요청마다가 아니라 에이전트 도구세트의configs배열에 있는web_search와web_fetch항목에 한 번 설정해요.max_uses,citations,cache_control필드는 사용할 수 없어요. 웹 검색과 웹 가져오기 도메인 제한하기를 참고하세요. - 컨텍스트: 시스템 프롬프트, 파일 리소스, 또는 스킬을 통해 컨텍스트를 주입할 수 있어요.
Claude Agent SDK에서 (From the Claude Agent SDK)
Claude Agent SDK로 구축했다면 이미 에이전트, 도구, 세션을 개념으로 다루고 있어요. 차이는 그것들이 어디서 실행되는가예요: SDK는 여러분이 운영하는 프로세스에서 실행되고, Managed Agents는 Anthropic의 인프라에서 실행돼요. 마이그레이션의 대부분은 SDK 구성 객체를 API 측 등가물로 매핑하는 것이에요.
무엇이 바뀌나 (What changes)
| Agent SDK | Managed Agents |
|---|---|
매 실행마다 구성되는 ClaudeAgentOptions(...) |
client.beta.agents.create(...)를 한 번 호출하고, 에이전트는 서버 측에 지속되고 버전이 매겨져요. 에이전트 설정을 참고하세요. |
async with ClaudeSDKClient(...) 또는 query(...) |
client.beta.sessions.create(...)를 호출한 뒤 이벤트를 보내고 받아요. |
SDK가 자동으로 디스패치하는 @tool 데코레이트 함수 |
에이전트에 {"type": "custom", ...}으로 선언하고, 클라이언트가 agent.custom_tool_use 이벤트를 처리하고 user.custom_tool_result로 응답해요. 도구를 참고하세요. |
| 내장 도구가 내 프로세스에서 내 파일시스템에 대해 실행됨 | {"type": "agent_toolset_20260401"}이 같은 도구를 세션 샌드박스 안에서 /workspace에 대해 실행해요. |
cwd, add_dirs가 로컬 경로를 가리킴 |
파일을 세션 리소스로 업로드하거나 마운트해요. |
system_prompt와 CLAUDE.md 계층 구조 |
에이전트에 단일 system 문자열. 에이전트를 바꾸는 각 업데이트는 새 서버 측 버전을 만들어요. 배포 없이 승격하거나 롤백하려면 세션을 특정 버전에 고정하세요. 에이전트 설정을 참고하세요. |
mcp_servers를 한 곳에서 구성·인증 |
에이전트에 서버를 선언하고, 세션의 볼트로 자격 증명을 제공해요. |
permission_mode, can_use_tool |
도구별 permission_policy(always_allow, always_ask 또는 auto); 승인을 위해 멈추는 호출에는 user.tool_confirmation 이벤트를 보내요. |
코드 비교 (Code comparison)
이전 (Agent SDK):
@tool("get_weather", "Get the current weather for a city.", {"city": str}) async def get_weather(args: dict) -> dict: return {"content": [{"type": "text", "text": f"{args['city']}: 18°C, clear"}]}
options = ClaudeAgentOptions( model="claude-opus-5-5", system_prompt="You are a concise weather assistant.", mcp_servers={ "weather": create_sdk_mcp_server("weather", "1.0", tools=[get_weather]) }, )
async with ClaudeSDKClient(options=options) as agent: await agent.query("What's the weather in Tokyo?") async for msg in agent.receive_response(): print(msg)
```typescript TypeScript
import { createSdkMcpServer, query, tool } from "@anthropic-ai/claude-agent-sdk";
import { z } from "zod";
const getWeather = tool(
"get_weather",
"Get the current weather for a city.",
{ city: z.string() },
async (args) => ({
content: [{ type: "text", text: `${args.city}: 18°C, clear` }]
})
);
for await (const message of query({
prompt: "What's the weather in Tokyo?",
options: {
model: "claude-opus-5-5",
systemPrompt: "You are a concise weather assistant.",
mcpServers: {
weather: createSdkMcpServer({ name: "weather", version: "1.0", tools: [getWeather] })
}
}
})) {
console.log(message);
}
이후 (Managed Agents):
client = Anthropic()
agent = client.beta.agents.create( name="weather-agent", model="claude-opus-5-5", system="You are a concise weather assistant.", tools=[ { "type": "custom", "name": "get_weather", "description": "Get the current weather for a city.", "input_schema": { "type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"], }, } ], ) environment = client.beta.environments.create( name="weather-env", config={"type": "cloud", "networking": {"type": "unrestricted"}}, )
session = client.beta.sessions.create( agent={"type": "agent", "id": agent.id, "version": agent.version}, environment_id=environment.id, )
def get_weather(city: str) -> str: return f"{city}: 18°C, clear"
with client.beta.sessions.events.stream(session.id) as stream: client.beta.sessions.events.send( session.id, events=[ { "type": "user.message", "content": [{"type": "text", "text": "What's the weather in Tokyo?"}], } ], ) for event in stream: match event.type: case "agent.message": print( "".join( block.text for block in event.content if block.type == "text" ) ) case "agent.custom_tool_use": result = get_weather(**event.input) client.beta.sessions.events.send( session.id, events=[ { "type": "user.custom_tool_result", "custom_tool_use_id": event.id, "content": [{"type": "text", "text": result}], } ], ) case "session.status_idle": if event.stop_reason and event.stop_reason.type == "end_turn": break
```typescript TypeScript
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const agent = await client.beta.agents.create({
name: "weather-agent",
model: "claude-opus-5-5",
system: "You are a concise weather assistant.",
tools: [
{
type: "custom",
name: "get_weather",
description: "Get the current weather for a city.",
input_schema: {
type: "object",
properties: { city: { type: "string" } },
required: ["city"]
}
}
]
});
const environment = await client.beta.environments.create({
name: "weather-env",
config: { type: "cloud", networking: { type: "unrestricted" } }
});
const session = await client.beta.sessions.create({
agent: { type: "agent", id: agent.id, version: agent.version },
environment_id: environment.id
});
function getWeather({ city }: Record<string, unknown>): string {
return `${city}: 18°C, clear`;
}
const stream = await client.beta.sessions.events.stream(session.id);
await client.beta.sessions.events.send(session.id, {
events: [
{
type: "user.message",
content: [{ type: "text", text: "What's the weather in Tokyo?" }]
}
]
});
loop: for await (const event of stream) {
switch (event.type) {
case "agent.message":
for (const block of event.content) {
if (block.type === "text") {
console.log(block.text);
}
}
break;
case "agent.custom_tool_use": {
const result = getWeather(event.input);
await client.beta.sessions.events.send(session.id, {
events: [
{
type: "user.custom_tool_result",
custom_tool_use_id: event.id,
content: [{ type: "text", text: result }]
}
]
});
break;
}
case "session.status_idle":
if (event.stop_reason?.type === "end_turn") {
break loop;
}
break;
}
}
using System.Text.Json;
using Anthropic.Models.Beta.Agents;
using Anthropic.Models.Beta.Environments;
using Anthropic.Models.Beta.Sessions;
using Anthropic.Models.Beta.Sessions.Events;
AnthropicClient client = new();
var agent = await client.Beta.Agents.Create(new()
{
Name = "weather-agent",
Model = BetaManagedAgentsModel.ClaudeOpus5_5,
System = "You are a concise weather assistant.",
Tools =
[
new BetaManagedAgentsCustomToolParams
{
Type = "custom",
Name = "get_weather",
Description = "Get the current weather for a city.",
InputSchema = new()
{
Properties = new Dictionary<string, JsonElement>
{
["city"] = JsonSerializer.SerializeToElement(new { type = "string" }),
},
Required = ["city"],
},
},
],
});
var environment = await client.Beta.Environments.Create(new()
{
Name = "weather-env",
Config = new BetaCloudConfigParams
{
Networking = new BetaUnrestrictedNetwork(),
},
});
var session = await client.Beta.Sessions.Create(new()
{
Agent = new BetaManagedAgentsAgentParams
{
Type = "agent",
ID = agent.ID,
Version = agent.Version,
},
EnvironmentID = environment.ID,
});
static string GetWeather(string city) => $"{city}: 18°C, clear";
using var stream = await client.Beta.Sessions.Events.WithRawResponse.StreamStreaming(session.ID);
await client.Beta.Sessions.Events.Send(session.ID, new()
{
Events =
[
new BetaManagedAgentsUserMessageEventParams
{
Type = "user.message",
Content = [new BetaManagedAgentsTextBlock { Type = "text", Text = "What's the weather in Tokyo?" }],
},
],
});
await foreach (var streamEvent in stream.Enumerate())
{
if (streamEvent.Value is BetaManagedAgentsAgentMessageEvent message)
{
var text = string.Concat(message.Content.Select(block =>
block.Value is BetaManagedAgentsTextBlock textBlock ? textBlock.Text : ""));
Console.WriteLine(text);
}
else if (streamEvent.Value is BetaManagedAgentsAgentCustomToolUseEvent toolUse)
{
var result = GetWeather(toolUse.Input["city"].GetString()!);
await client.Beta.Sessions.Events.Send(session.ID, new()
{
Events =
[
new BetaManagedAgentsUserCustomToolResultEventParams
{
Type = "user.custom_tool_result",
CustomToolUseID = toolUse.ID,
Content =
[
new BetaManagedAgentsTextBlock
{
Type = "text",
Text = result,
},
],
},
],
});
}
else if (streamEvent.Value is BetaManagedAgentsSessionStatusIdleEvent idle
&& idle.StopReason?.Value is BetaManagedAgentsSessionEndTurn)
{
break;
}
}
client := anthropic.NewClient()
ctx := context.Background()
agent, err := client.Beta.Agents.New(ctx, anthropic.BetaAgentNewParams{
Name: "weather-agent",
Model: anthropic.BetaManagedAgentsModelConfigParams{
ID: anthropic.BetaManagedAgentsModelClaudeOpus5_5,
},
System: anthropic.String("You are a concise weather assistant."),
Tools: []anthropic.BetaAgentNewParamsToolUnion{{
OfCustom: &anthropic.BetaManagedAgentsCustomToolParams{
Type: anthropic.BetaManagedAgentsCustomToolParamsTypeCustom,
Name: "get_weather",
Description: "Get the current weather for a city.",
InputSchema: anthropic.BetaManagedAgentsCustomToolInputSchemaParam{
Properties: map[string]any{
"city": map[string]any{"type": "string"},
},
Required: []string{"city"},
},
},
}},
})
if err != nil {
panic(err)
}
environment, err := client.Beta.Environments.New(ctx, anthropic.BetaEnvironmentNewParams{
Name: "weather-env",
Config: anthropic.BetaEnvironmentNewParamsConfigUnion{
OfCloud: &anthropic.BetaCloudConfigParams{
Networking: anthropic.BetaCloudConfigParamsNetworkingUnion{
OfUnrestricted: &anthropic.BetaUnrestrictedNetworkParam{},
},
},
},
})
if err != nil {
panic(err)
}
session, err := client.Beta.Sessions.New(ctx, anthropic.BetaSessionNewParams{
Agent: anthropic.BetaSessionNewParamsAgentUnion{
OfBetaManagedAgentsAgents: &anthropic.BetaManagedAgentsAgentParams{
Type: anthropic.BetaManagedAgentsAgentParamsTypeAgent,
ID: agent.ID,
Version: anthropic.Int(agent.Version),
},
},
EnvironmentID: environment.ID,
})
if err != nil {
panic(err)
}
getWeather := func(city string) string {
return fmt.Sprintf("%s: 18°C, clear", city)
}
stream := client.Beta.Sessions.Events.StreamEvents(ctx, session.ID, anthropic.BetaSessionEventStreamParams{})
defer stream.Close()
_, err = client.Beta.Sessions.Events.Send(ctx, session.ID, anthropic.BetaSessionEventSendParams{
Events: []anthropic.BetaManagedAgentsEventParamsUnion{{
OfUserMessage: &anthropic.BetaManagedAgentsUserMessageEventParams{
Type: anthropic.BetaManagedAgentsUserMessageEventParamsTypeUserMessage,
Content: []anthropic.BetaManagedAgentsUserMessageEventParamsContentUnion{{
OfText: &anthropic.BetaManagedAgentsTextBlockParam{
Type: anthropic.BetaManagedAgentsTextBlockTypeText,
Text: "What's the weather in Tokyo?",
},
}},
},
}},
})
if err != nil {
panic(err)
}
loop:
for stream.Next() {
event := stream.Current()
switch event.Type {
case "agent.message":
for _, block := range event.AsAgentMessage().Content {
if block.Type == "text" {
fmt.Println(block.Text)
}
}
case "agent.custom_tool_use":
toolUse := event.AsAgentCustomToolUse()
result := getWeather(toolUse.Input["city"].(string))
if _, err := client.Beta.Sessions.Events.Send(ctx, session.ID, anthropic.BetaSessionEventSendParams{
Events: []anthropic.BetaManagedAgentsEventParamsUnion{{
OfUserCustomToolResult: &anthropic.BetaManagedAgentsUserCustomToolResultEventParams{
Type: anthropic.BetaManagedAgentsUserCustomToolResultEventParamsTypeUserCustomToolResult,
CustomToolUseID: toolUse.ID,
Content: []anthropic.BetaManagedAgentsUserCustomToolResultEventParamsContentUnion{{
OfText: &anthropic.BetaManagedAgentsTextBlockParam{
Type: anthropic.BetaManagedAgentsTextBlockTypeText,
Text: result,
},
}},
},
}},
}); err != nil {
panic(err)
}
case "session.status_idle":
idle := event.AsSessionStatusIdle()
if _, ok := idle.StopReason.AsAny().(anthropic.BetaManagedAgentsSessionEndTurn); ok {
break loop
}
}
}
if err := stream.Err(); err != nil {
panic(err)
}
import java.util.Map;
import java.util.function.Function;
import com.anthropic.models.beta.agents.AgentCreateParams;
import com.anthropic.models.beta.agents.BetaManagedAgentsCustomToolInputSchema;
import com.anthropic.models.beta.agents.BetaManagedAgentsCustomToolParams;
import com.anthropic.models.beta.agents.BetaManagedAgentsModel;
import com.anthropic.models.beta.environments.BetaCloudConfigParams;
import com.anthropic.models.beta.environments.BetaUnrestrictedNetwork;
import com.anthropic.models.beta.environments.EnvironmentCreateParams;
import com.anthropic.models.beta.sessions.BetaManagedAgentsAgentParams;
import com.anthropic.models.beta.sessions.SessionCreateParams;
import com.anthropic.models.beta.sessions.events.BetaManagedAgentsStreamSessionEvents;
import com.anthropic.models.beta.sessions.events.BetaManagedAgentsUserCustomToolResultEventParams;
import com.anthropic.models.beta.sessions.events.BetaManagedAgentsUserMessageEventParams;
import com.anthropic.models.beta.sessions.events.EventSendParams;
var client = AnthropicOkHttpClient.fromEnv();
var agent = client.beta().agents().create(AgentCreateParams.builder()
.name("weather-agent")
.model(BetaManagedAgentsModel.CLAUDE_OPUS_5_5)
.system("You are a concise weather assistant.")
.addTool(BetaManagedAgentsCustomToolParams.builder()
.type(BetaManagedAgentsCustomToolParams.Type.CUSTOM)
.name("get_weather")
.description("Get the current weather for a city.")
.inputSchema(BetaManagedAgentsCustomToolInputSchema.builder()
.properties(BetaManagedAgentsCustomToolInputSchema.Properties.builder()
.putAdditionalProperty("city", JsonValue.from(Map.of("type", "string")))
.build())
.addRequired("city")
.build())
.build())
.build());
var environment = client.beta().environments().create(EnvironmentCreateParams.builder()
.name("weather-env")
.config(BetaCloudConfigParams.builder()
.networking(BetaUnrestrictedNetwork.builder().build())
.build())
.build());
var session = client.beta().sessions().create(SessionCreateParams.builder()
.agent(BetaManagedAgentsAgentParams.builder()
.type(BetaManagedAgentsAgentParams.Type.AGENT)
.id(agent.id())
.version(agent.version())
.build())
.environmentId(environment.id())
.build());
Function<String, String> getWeather = city -> city + ": 18°C, clear";
try (var stream = client.beta().sessions().events().streamStreaming(session.id())) {
client.beta().sessions().events().send(
session.id(),
EventSendParams.builder()
.addEvent(BetaManagedAgentsUserMessageEventParams.builder()
.type(BetaManagedAgentsUserMessageEventParams.Type.USER_MESSAGE)
.addTextContent("What's the weather in Tokyo?")
.build())
.build());
loop:
for (var event : (Iterable<BetaManagedAgentsStreamSessionEvents>) stream.stream()::iterator) {
switch (event.type().value()) {
case AGENT_MESSAGE -> {
for (var block : event.asAgentMessage().content()) {
block.text().ifPresent(textBlock -> IO.println(textBlock.text()));
}
}
case AGENT_CUSTOM_TOOL_USE -> {
var toolUse = event.asAgentCustomToolUse();
var city = toolUse.input()._additionalProperties().get("city").asStringOrThrow();
var result = getWeather.apply(city);
client.beta().sessions().events().send(
session.id(),
EventSendParams.builder()
.addEvent(BetaManagedAgentsUserCustomToolResultEventParams.builder()
.type(BetaManagedAgentsUserCustomToolResultEventParams.Type.USER_CUSTOM_TOOL_RESULT)
.customToolUseId(toolUse.id())
.addTextContent(result)
.build())
.build());
}
case SESSION_STATUS_IDLE -> {
if (event.asSessionStatusIdle().stopReason().isEndTurn()) {
break loop;
}
}
}
}
}
use Anthropic\Client;
use Anthropic\Beta\Agents\BetaManagedAgentsCustomToolInputSchema;
use Anthropic\Beta\Agents\BetaManagedAgentsCustomToolParams;
use Anthropic\Beta\Sessions\BetaManagedAgentsAgentParams;
use Anthropic\Beta\Sessions\Events\ManagedAgentsAgentCustomToolUseEvent;
use Anthropic\Beta\Sessions\Events\ManagedAgentsAgentMessageEvent;
use Anthropic\Beta\Sessions\Events\ManagedAgentsSessionEndTurn;
use Anthropic\Beta\Sessions\Events\ManagedAgentsSessionStatusIdleEvent;
use Anthropic\Beta\Sessions\Events\ManagedAgentsTextBlock;
$client = new Client();
$agent = $client->beta->agents->create(
name: 'weather-agent',
model: 'claude-opus-5-5',
system: 'You are a concise weather assistant.',
tools: [
BetaManagedAgentsCustomToolParams::with(
type: 'custom',
name: 'get_weather',
description: 'Get the current weather for a city.',
inputSchema: BetaManagedAgentsCustomToolInputSchema::with(
properties: ['city' => ['type' => 'string']],
required: ['city'],
),
),
],
);
$environment = $client->beta->environments->create(
name: 'weather-env',
config: ['type' => 'cloud', 'networking' => ['type' => 'unrestricted']],
);
$session = $client->beta->sessions->create(
agent: BetaManagedAgentsAgentParams::with(
type: 'agent',
id: $agent->id,
version: $agent->version,
),
environmentID: $environment->id,
);
function getWeather(string $city): string
{
return "{$city}: 18°C, clear";
}
$stream = $client->beta->sessions->events->streamStream($session->id);
$client->beta->sessions->events->send(
$session->id,
events: [
[
'type' => 'user.message',
'content' => [['type' => 'text', 'text' => "What's the weather in Tokyo?"]],
],
],
);
foreach ($stream as $event) {
switch (true) {
case $event instanceof ManagedAgentsAgentMessageEvent:
foreach ($event->content as $block) {
if ($block instanceof ManagedAgentsTextBlock) {
echo $block->text . "\n";
}
}
break;
case $event instanceof ManagedAgentsAgentCustomToolUseEvent:
$result = getWeather($event->input['city']);
$client->beta->sessions->events->send(
$session->id,
events: [
[
'type' => 'user.custom_tool_result',
'custom_tool_use_id' => $event->id,
'content' => [['type' => 'text', 'text' => $result]],
],
],
);
break;
case $event instanceof ManagedAgentsSessionStatusIdleEvent:
if ($event->stopReason instanceof ManagedAgentsSessionEndTurn) {
break 2;
}
break;
}
}
$stream->close();
require "anthropic"
client = Anthropic::Client.new
agent = client.beta.agents.create(
name: "weather-agent",
model: "claude-opus-5-5",
system_: "You are a concise weather assistant.",
tools: [
{
type: "custom",
name: "get_weather",
description: "Get the current weather for a city.",
input_schema: {
type: "object",
properties: {city: {type: "string"}},
required: ["city"]
}
}
]
)
environment = client.beta.environments.create(
name: "weather-env",
config: {type: "cloud", networking: {type: "unrestricted"}}
)
session = client.beta.sessions.create(
agent: {type: "agent", id: agent.id, version: agent.version},
environment_id: environment.id
)
def get_weather(city)
"#{city}: 18°C, clear"
end
stream = client.beta.sessions.events.stream_events(session.id)
client.beta.sessions.events.send_(
session.id,
events: [{type: "user.message", content: [{type: "text", text: "What's the weather in Tokyo?"}]}]
)
stream.each do |event|
case event
when Anthropic::Beta::Sessions::BetaManagedAgentsAgentMessageEvent
event.content.each do |block|
puts block.text if block.is_a?(Anthropic::Beta::Sessions::BetaManagedAgentsTextBlock)
end
when Anthropic::Beta::Sessions::BetaManagedAgentsAgentCustomToolUseEvent
result = get_weather(event.input[:city])
client.beta.sessions.events.send_(
session.id,
events: [
{
type: "user.custom_tool_result",
custom_tool_use_id: event.id,
content: [{type: "text", text: result}]
}
]
)
when Anthropic::Beta::Sessions::BetaManagedAgentsSessionStatusIdleEvent
break if event.stop_reason.is_a?(Anthropic::Beta::Sessions::BetaManagedAgentsSessionEndTurn)
end
end
에이전트와 환경은 한 번 만들어져 세션들에서 재사용돼요. 도구 함수는 여전히 여러분의 프로세스에서 실행돼요. 차이는 SDK가 디스패치해 주는 대신 여러분이 agent.custom_tool_use 이벤트를 읽고 결과를 명시적으로 보낸다는 것이에요.
클라이언트로 옮겨가는 기능들 (Features that move to your client)
Anthropic이 에이전트 루프를 실행하는 대가는, SDK가 자동으로 처리하던 몇 가지가 여러분의 클라이언트 책임이 된다는 것이에요.
| SDK 기능 | Managed Agents 방식 |
|---|---|
| Plan mode / 계획 모드 | 먼저 계획 전용 세션을 실행하고, 그다음 계획을 실행할 두 번째 세션을 실행해요. |
| 출력 스타일, 슬래시 명령 | user.message를 보내기 전이나 agent.message를 받은 후에 클라이언트에서 적용해요. |
PreToolUse / PostToolUse 훅 |
여러분의 클라이언트는 응답하기 전에 이미 모든 agent.custom_tool_use 이벤트를 봐요. 로직을 거기에 넣으세요. 내장 도구는 permission_policy: always_ask를 사용해 모든 호출을 검토하세요. auto는 대신 서버가 각 호출을 평가하게 하지만, 서버가 호출을 안전하다고 평가하면 여러분의 클라이언트에 닿지 않고 실행돼요. |
max_turns |
클라이언트 측에서 턴을 세세요. |
마이그레이션 체크리스트 (Migration checklist)
- 에이전트에 필요한 네트워킹과 런타임으로 환경을 만드세요.
- 시스템 프롬프트와 도구 선택을 에이전트 정의로 포팅하세요.
- 루프를
sessions.create와sessions.events.stream으로 교체하세요. - 에이전트가 읽는 로컬 파일은 Files API로 업로드하고
resources로 마운트하세요. - 커스텀 도구 핸들러는 실행을
agent.custom_tool_use이벤트에 응답하는 이벤트 루프로 옮기세요. - 새 흐름에 프로덕션 트래픽을 돌리기 전에 테스트 세션으로 검증하세요.
모델 버전 간 마이그레이션 (Migrating between model versions)
새 Claude 모델이 출시될 때, Claude Managed Agents 통합을 마이그레이션하는 것은 대개 한 필드 변경이에요: 에이전트 정의에서 model을 업데이트하면 다음에 만드는 세션에 그 변경이 적용돼요.
<File filename="agent.md">
```markdown
---
name: Task Runner
model: claude-opus-5-5
tools:
- type: agent_toolset_20260401
---
You are a task automation agent. Complete the task you are given end to end.
```
</File>
client.beta.agents.update(
agent.id,
version=agent.version,
model="claude-opus-5-5",
)
await client.beta.agents.update(agent.id, {
version: agent.version,
model: "claude-opus-5-5"
});
await client.Beta.Agents.Update(agent.ID, new()
{
Version = agent.Version,
Model = BetaManagedAgentsModel.ClaudeOpus5_5,
});
_, err = client.Beta.Agents.Update(ctx, agent.ID, anthropic.BetaAgentUpdateParams{
Version: agent.Version,
Model: anthropic.BetaManagedAgentsModelConfigParams{
ID: anthropic.BetaManagedAgentsModelClaudeOpus5_5,
},
})
if err != nil {
panic(err)
}
client.beta().agents().update(
agent.id(),
AgentUpdateParams.builder()
.version(agent.version())
.model(BetaManagedAgentsModel.CLAUDE_OPUS_5_5)
.build()
);
$client->beta->agents->update(
$agent->id,
version: $agent->version,
model: 'claude-opus-5-5',
);
client.beta.agents.update(
agent.id,
version: agent.version,
model: "claude-opus-5-5"
)
Messages API 마이그레이션 가이드에 문서화된 대부분의 모델 수준 동작 변경은 여러분 쪽에서 조치를 요구하지 않아요:
- 요청 파라미터 변경(
max_tokens기본값,thinking구성)은 Claude Managed Agents 런타임이 처리해요. 이 필드들은 에이전트 정의에 노출되지 않아요. - 어시스턴트 메시지 프리필링은 이벤트 기반 세션 모델에 존재하지 않아서, 새 모델에서의 제거는 no-op이에요.
- 도구 인자 JSON 이스케이핑은
agent.custom_tool_use이벤트를 받기 전에 런타임이 파싱해요. 여러분은 구조화된 데이터를 보지, 원시 문자열을 보지 않아요.
Messages API 가이드의 동작 설명(모델이 무엇을 다르게 하는지)은 여전히 적용돼요. 마이그레이션 단계(요청 코드를 어떻게 바꾸는지)는 적용되지 않아요.
더 알아보기 (Learn more)
- 에이전트 설정 — 에이전트 정의 만들기
- 세션 — 에이전트 세션 시작하기
- 세션 이벤트 스트림 — 이벤트 보내고 받기
- 도구 — 커스텀 도구와 도구세트