迁移

将基于 Messages API 或 Claude Agent SDK 构建的现有代理迁移到 Claude 托管代理。


Claude 托管代理用托管基础设施取代了您手写的代理循环。本页介绍了从基于 Messages API 构建的自定义循环或从 Claude Agent SDK 迁移时的变化。

Note

所有托管代理 API 请求都需要 managed-agents-2026-04-01 beta 头。SDK 会自动设置该 beta 头。

从 Messages API 代理循环迁移

如果您通过在 while 循环中调用 messages.create、自行执行工具调用并将结果附加到对话历史来构建代理,那么大部分代码都可以去掉。

您不再需要管理的内容

之前之后
您维护对话历史数组并在每一轮中传递它。会话在服务端存储历史。发送事件,接收事件。
您迭代 tool_use 内容块,运行每个工具,并用 tool_result 消息循环回来。预构建工具在容器内自动运行。您只需通过 agent.custom_tool_use 事件处理自定义工具。
您自己配置沙箱来运行代理生成的代码。会话容器处理代码执行、文件操作和 bash。
您决定循环何时结束。当代理没有更多任务时,会话发出 session.status_idle

代码对比

之前(Messages API 循环,简化版):

messages = [{"role": "user", "content": task}]
while True:
    response = client.messages.create(
        model="claude-opus-4-7",
        max_tokens=1024,
        messages=messages,
        tools=tools,
    )
    messages.append({"role": "assistant", "content": response.content})
    if response.stop_reason == "end_turn":
        break
    for block in response.content:
        if block.type == "tool_use":
            result = execute_tool(block.name, block.input)
            messages.append(
                {
                    "role": "user",
                    "content": [
                        {
                            "type": "tool_result",
                            "tool_use_id": block.id,
                            "content": result,
                        }
                    ],
                }
            )
const messages: Anthropic.MessageParam[] = [{ role: "user", content: task }];
while (true) {
  const response = await client.messages.create({
    model: "claude-opus-4-7",
    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.ClaudeOpus4_7,
        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.ModelClaudeOpus4_7,
		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_4_7)
        .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-4-7',
        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-4-7",
    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 托管代理):

agent=$(
  curl --fail-with-body -sS "https://api.anthropic.com/v1/agents?beta=true" \
    -H "x-api-key: ${ANTHROPIC_API_KEY}" \
    -H "anthropic-version: 2023-06-01" \
    -H "anthropic-beta: managed-agents-2026-04-01" \
    --json '{
      "name": "Task Runner",
      "model": "claude-opus-4-7",
      "tools": [{"type": "agent_toolset_20260401"}]
    }'
)
agent_id=$(jq -r '.id' <<< "${agent}")

session_id=$(
  curl --fail-with-body -sS "https://api.anthropic.com/v1/sessions?beta=true" \
    -H "x-api-key: ${ANTHROPIC_API_KEY}" \
    -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: ${ANTHROPIC_API_KEY}" \
  -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: ${ANTHROPIC_API_KEY}" \
  -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

# Read events until the session goes idle.
while IFS= read -r line; do
  [[ ${line} == data:* ]] || continue
  event_type=$(jq -r '.type // empty' 2>/dev/null <<< "${line#data: }" || true)
  [[ ${event_type} == "session.status_idle" ]] && break
done < <(tail -f -n +1 "${stream_log}")

kill "${stream_pid}" 2>/dev/null || true
{ read -r _ agent_id; read -r _ agent_version; } < <(ant beta:agents create \
  --name "Task Runner" \
  --model claude-opus-4-7 \
  --tool '{type: agent_toolset_20260401}' \
  --transform '{id,version}' --format yaml)

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

while IFS= read -r -u "$stream" type; do
  [[ $type == session.status_idle ]] && break
done
exec {stream}<&-
agent = client.beta.agents.create(
    name="Task Runner",
    model="claude-opus-4-7",
    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-4-7",
  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.ClaudeOpus4_7,
    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.BetaManagedAgentsModelClaudeOpus4_7,
		},
		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_4_7)
            .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-4-7',
    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-4-7",
  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

您仍然控制的内容

  • **系统提示和模型:**相同的字段,现在在代理定义上。
  • **自定义工具:**仍然用 JSON Schema 声明。执行从内联处理移至响应 agent.custom_tool_use 事件。请参阅会话事件流
  • **上下文:**您仍然可以通过系统提示、文件资源技能注入上下文。

从 Claude Agent SDK 迁移

如果您使用 Claude Agent SDK 构建,您已经在使用代理、工具和会话作为概念。区别在于它们的运行位置:SDK 在您操作的进程中执行,而托管代理在 Anthropic 的基础设施中运行。迁移的大部分内容是将 SDK 配置对象映射到其 API 端的等价物。

变化内容

Agent SDK托管代理
每次运行时构造 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 运行相同的工具。
cwdadd_dirs 指向本地路径上传或挂载文件作为会话资源。
system_promptCLAUDE.md 层次结构代理上的单个 system 字符串。每次更新都会产生新的服务端版本;将会话固定到特定版本以在不部署的情况下进行推广或回滚。请参阅代理设置
mcp_servers 在一个地方配置和认证在代理上声明服务器;通过会话上的保险库提供凭据。
permission_modecan_use_tool每个工具的 permission_policy;为 always_ask 工具发送 user.tool_confirmation 事件。

代码对比

之前(Agent SDK):

from claude_agent_sdk import (
    ClaudeAgentOptions,
    ClaudeSDKClient,
    create_sdk_mcp_server,
    tool,
)


@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-4-7",
    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)

之后(托管代理):

from anthropic import Anthropic

client = Anthropic()

agent = client.beta.agents.create(
    name="weather-agent",
    model="claude-opus-4-7",
    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 ev in stream:
        if ev.type == "agent.message":
            print("".join(block.text for block in ev.content if block.type == "text"))
        elif ev.type == "agent.custom_tool_use":
            result = get_weather(**ev.input)
            client.beta.sessions.events.send(
                session.id,
                events=[
                    {
                        "type": "user.custom_tool_result",
                        "custom_tool_use_id": ev.id,
                        "content": [{"type": "text", "text": result}],
                    }
                ],
            )
        elif (
            ev.type == "session.status_idle"
            and ev.stop_reason
            and ev.stop_reason.type == "end_turn"
        ):
            break

代理和环境只需创建一次,可在会话之间重用。工具函数仍然在您的进程中运行;区别在于您读取 agent.custom_tool_use 事件并显式发送结果,而不是由 SDK 为您分派。

移至客户端的功能

Anthropic 运行代理循环的权衡是,SDK 自动处理的一些事情变成了您客户端的责任。

SDK 功能托管代理方式
计划模式先运行一个仅计划的会话,然后运行第二个会话来执行计划。
输出样式、斜杠命令在发送 user.message 之前或接收 agent.message 之后在客户端中应用。
PreToolUse / PostToolUse 钩子您的客户端在响应之前已经看到了每个 agent.custom_tool_use 事件;将逻辑放在那里。对于内置工具,使用 permission_policy: always_ask
max_turns在客户端侧计数轮次。

迁移清单

  1. 创建环境,配置代理所需的网络和运行时。
  2. 将您的系统提示和工具选择移植到代理定义
  3. sessions.createsessions.events.stream 替换您的循环。
  4. 对于代理读取的任何本地文件,通过 Files API 上传并挂载为 resources
  5. 对于任何自定义工具处理程序,将执行移至您的事件循环中,作为对 agent.custom_tool_use 事件的响应。
  6. 在将生产流量指向新流程之前,使用测试会话进行验证。

在模型版本之间迁移

当新的 Claude 模型发布时,迁移 Claude 托管代理集成通常只需更改一个字段:更新代理定义上的 model,更改将在您创建的下一个会话中生效。

curl -sS --fail-with-body "https://api.anthropic.com/v1/agents/$AGENT_ID?beta=true" \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "anthropic-beta: managed-agents-2026-04-01" \
  --json "$(jq -n --argjson version "$AGENT_VERSION" '{version: $version, model: "claude-opus-4-7"}')"
ant beta:agents update \
  --agent-id "$AGENT_ID" \
  --version "$AGENT_VERSION" \
  --model claude-opus-4-7
client.beta.agents.update(
    agent.id,
    version=agent.version,
    model="claude-opus-4-7",
)
await client.beta.agents.update(agent.id, {
  version: agent.version,
  model: "claude-opus-4-7"
});
await client.Beta.Agents.Update(agent.ID, new()
{
    Version = agent.Version,
    Model = BetaManagedAgentsModel.ClaudeOpus4_7,
});
_, err = client.Beta.Agents.Update(ctx, agent.ID, anthropic.BetaAgentUpdateParams{
	Version: agent.Version,
	Model: anthropic.BetaManagedAgentsModelConfigParams{
		ID: anthropic.BetaManagedAgentsModelClaudeOpus4_7,
	},
})
if err != nil {
	panic(err)
}
client.beta().agents().update(
    agent.id(),
    AgentUpdateParams.builder()
        .version(agent.version())
        .model(BetaManagedAgentsModel.CLAUDE_OPUS_4_7)
        .build()
);
$client->beta->agents->update(
    $agent->id,
    version: $agent->version,
    model: 'claude-opus-4-7',
);
client.beta.agents.update(
  agent.id,
  version: agent.version,
  model: "claude-opus-4-7"
)

Messages API 迁移指南中记录的大多数模型级行为更改不需要您采取行动:

  • 请求参数更改max_tokens 默认值、thinking 配置)由 Claude 托管代理运行时处理。这些字段不会在代理定义上公开。
  • 助手消息预填充在基于事件的会话模型中不存在,因此在较新模型中删除它是一个无操作。
  • 工具参数 JSON 转义在您接收 agent.custom_tool_use 事件之前由运行时解析。您看到的是结构化数据,而不是原始字符串。

Messages API 指南中的行为描述(模型的不同行为)仍然适用。迁移步骤(如何更改请求代码)则不适用。