멀티에이전트 오케스트레이션

멀티에이전트 오케스트레이션 (Multiagent orchestration)

멀티에이전트 오케스트레이션은 하나의 에이전트가 다른 에이전트들과 협력해 복잡한 작업을 완료하게 해줘요. 에이전트들은 각자의 격리된 컨텍스트로 병렬로 동작할 수 있어서 출력 품질을 높이는 데 도움이 되고 완료 시간도 단축할 수 있어요.

출처: 문서

본문

멀티에이전트 오케스트레이션은 하나의 에이전트가 다른 에이전트들과 협력해 복잡한 작업을 완료하게 해줘요. 에이전트들은 각자의 격리된 컨텍스트로 병렬로 동작할 수 있어서 출력 품질을 높이는 데 도움이 되고 완료 시간도 단축할 수 있어요.

멀티에이전트 구성이 여러분의 문제에 맞는지 확신이 없나요? 멀티에이전트 시스템을 언제 사용해야(그리고 언제 하지 말아야) 하는지를 참고하세요.

동작 방식 (How it works)

모든 에이전트는 같은 샌드박스, 파일시스템, 볼트 자격 증명을 공유하지만, 각 에이전트는 자체 컨텍스트 격리 이벤트 스트림이자 자체 대화 기록을 가진 **세션 스레드(session thread)**에서 실행돼요. 코디네이터가 기본 스레드(primary thread)(세션 수준 이벤트 스트림과 같아요)에서 활동을 보고하고, 추가 스레드는 코디네이터가 작업을 위임할 때 런타임에서 생성돼요.

스레드는 지속돼요: 코디네이터가 이전에 호출한 에이전트에게 후속 요청을 보낼 수 있고, 그 에이전트는 이전 턴의 모든 것을 유지해요.

각 에이전트는 자체 구성을 사용해요: 모델, 시스템 프롬프트, 도구, MCP 서버, 스킬이요. 세션 수준의 에이전트 구성 재정의는 예외로, 코디네이터와 그 self 복사본에 적용돼요. 도구, MCP 서버, 컨텍스트는 공유되지 않아요.

무엇을 위임할까 (What to delegate)

멀티에이전트 조정은 다양한 표면에 걸친 작업을 요구하거나, 여러 개의 잘 범위가 지정된 작업이 전체 목표에 기여하는 복잡한 작업에 가장 적합해요.

잘 맞는 패턴:

  • 병렬화 (Parallelization): 독립적인 하위 작업을 동시에 배포하고(여러 소스 검색, 별도 파일 분석) 코디네이터가 결과를 종합하게 해요.
  • 전문화 (Specialization): 단일 에이전트에 모든 기능을 싣기보다 도메인에 초점을 맞춘 시스템 프롬프트와 도구를 가진 에이전트로 라우팅해요. 예: 보안 에이전트, 문서 에이전트.
  • 에스컬레이션 (Escalation): 복잡한 하위 작업의 일부에 더 유능한 에이전트나 모델을 참고해요.

코디네이터 구성하기 (Configure the coordinator)

에이전트를 정의할 때 multiagent를 설정해 코디네이터가 위임할 수 있는 에이전트 명단(roster)을 선언하세요:

```bash cURL coordinator=$(curl -fsS https://api.anthropic.com/v1/agents \ -H "x-api-key: $ANTHR...KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -d @- < ```bash CLI ant apply engineering-lead.md reviewer.md test-writer.md ```
<File filename="engineering-lead.md">
  ```markdown
  ---
  name: Engineering Lead
  model: claude-opus-5-5
  tools:
    - type: agent_toolset_20260401
  multiagent:
    type: coordinator
    agents: # paths: ant apply substitutes {type: agent, id, version}
      - ./reviewer.md
      - ./test-writer.md
  ---

  You coordinate engineering work. Delegate code review to the reviewer agent and test writing to the test agent.
  ```
</File>

<File filename="reviewer.md">
  ```markdown
  ---
  name: reviewer
  model: claude-haiku-4-5
  ---

  You are a code reviewer.
  ```
</File>

<File filename="test-writer.md">
  ```markdown
  ---
  name: test-writer
  model: claude-haiku-4-5
  ---

  You write unit tests.
  ```
</File>
coordinator = client.beta.agents.create(
    name="Engineering Lead",
    model="claude-opus-5-5",
    system="You coordinate engineering work. Delegate code review to the reviewer agent and test writing to the test agent.",
    tools=[
        {"type": "agent_toolset_20260401"},
    ],
    multiagent={
        "type": "coordinator",
        "agents": [
            {"type": "agent", "id": reviewer_agent.id},
            {"type": "agent", "id": test_writer_agent.id},
        ],
    },
)
const coordinator = await client.beta.agents.create({
  name: "Engineering Lead",
  model: "claude-opus-5-5",
  system:
    "You coordinate engineering work. Delegate code review to the reviewer agent and test writing to the test agent.",
  tools: [{ type: "agent_toolset_20260401" }],
  multiagent: {
    type: "coordinator",
    agents: [
      { type: "agent", id: reviewerAgent.id },
      { type: "agent", id: testWriterAgent.id },
    ],
  },
});
var coordinator = await client.Beta.Agents.Create(new()
{
    Name = "Engineering Lead",
    Model = BetaManagedAgentsModel.ClaudeOpus5_5,
    System = "You coordinate engineering work. Delegate code review to the reviewer agent and test writing to the test agent.",
    Tools =
    [
        new BetaManagedAgentsAgentToolset20260401Params
        {
            Type = BetaManagedAgentsAgentToolset20260401ParamsType.AgentToolset20260401,
        },
    ],
    Multiagent = new BetaManagedAgentsMultiagentParams
    {
        Type = BetaManagedAgentsMultiagentParamsType.Coordinator,
        Agents = [reviewerAgent.ID, testWriterAgent.ID],
    },
});
coordinator, err := client.Beta.Agents.New(ctx, anthropic.BetaAgentNewParams{
	Name:   "Engineering Lead",
	Model:  anthropic.BetaManagedAgentsModelConfigParams{ID: anthropic.BetaManagedAgentsModelClaudeOpus5_5},
	System: anthropic.String("You coordinate engineering work. Delegate code review to the reviewer agent and test writing to the test agent."),
	Tools: []anthropic.BetaAgentNewParamsToolUnion{{
		OfAgentToolset20260401: &anthropic.BetaManagedAgentsAgentToolset20260401Params{
			Type: anthropic.BetaManagedAgentsAgentToolset20260401ParamsTypeAgentToolset20260401,
		},
	}},
	Multiagent: anthropic.BetaManagedAgentsMultiagentParams{
		Type: anthropic.BetaManagedAgentsMultiagentParamsTypeCoordinator,
		Agents: []anthropic.BetaManagedAgentsMultiagentRosterEntryParamsUnion{
			{OfString: anthropic.String(reviewerAgent.ID)},
			{OfString: anthropic.String(testWriterAgent.ID)},
		},
	},
})
if err != nil {
	panic(err)
}
var coordinator = client.beta().agents().create(
    AgentCreateParams.builder()
        .name("Engineering Lead")
        .model(BetaManagedAgentsModel.CLAUDE_OPUS_5_5)
        .system("You coordinate engineering work. Delegate code review to the reviewer agent and test writing to the test agent.")
        .addTool(
            BetaManagedAgentsAgentToolset20260401Params.builder()
                .type(BetaManagedAgentsAgentToolset20260401Params.Type.AGENT_TOOLSET_20260401)
                .build()
        )
        .multiagent(BetaManagedAgentsMultiagentParams.builder()
            .type(BetaManagedAgentsMultiagentParams.Type.COORDINATOR)
            .addAgent(BetaManagedAgentsAgentParams.builder()
                .type(BetaManagedAgentsAgentParams.Type.AGENT)
                .id(reviewerAgent.id())
                .build())
            .addAgent(BetaManagedAgentsAgentParams.builder()
                .type(BetaManagedAgentsAgentParams.Type.AGENT)
                .id(testWriterAgent.id())
                .build())
            .build())
        .build()
);
$coordinator = $client->beta->agents->create(
    name: 'Engineering Lead',
    model: 'claude-opus-5-5',
    system: 'You coordinate engineering work. Delegate code review to the reviewer agent and test writing to the test agent.',
    tools: [
        ['type' => 'agent_toolset_20260401'],
    ],
    multiagent: [
        'type' => 'coordinator',
        'agents' => [
            ['type' => 'agent', 'id' => $reviewerAgent->id],
            ['type' => 'agent', 'id' => $testWriterAgent->id],
        ],
    ],
);
coordinator = client.beta.agents.create(
  name: "Engineering Lead",
  model: "claude-opus-5-5",
  system: "You coordinate engineering work. Delegate code review to the reviewer agent and test writing to the test agent.",
  tools: [
    {type: "agent_toolset_20260401"}
  ],
  multiagent: {
    type: "coordinator",
    agents: [
      {type: "agent", id: reviewer_agent.id},
      {type: "agent", id: test_writer_agent.id}
    ]
  }
)

multiagent.agents는 다음 중 어떤 것이든 받을 수 있어요:

  • {"type": "agent", "id": agent.id}는 이전에 만든 agent를 ID로 참조해요. version을 지정하지 않으면 참조는 코디네이터가 만들어진 시점의 그 에이전트 최신 버전에 고정돼요.
  • {"type": "agent", "id": agent.id, "version": agent.version}은 특정 에이전트 버전을 고정해요.
  • {"type": "self"}는 코디네이터가 자기 자신의 복사본을 생성하게 해줘요. 세션이 에이전트 구성 재정의로 만들어졌다면 그 재정의도 이 복사본에 적용돼요. ID로 참조된 명단 항목은 영향을 받지 않아요.
  • {"type": "advisor", "model": "<model id>"}는 세션의 기본 스레드에 턴 중간에 상담할 수 있는 어드바이저를 줘요. 명단당 최대 하나의 어드바이저 항목. 세션에 어드바이저 주기를 참고하세요.

ant apply 에이전트 파일(CLI 탭)에서는 명단 항목이 다른 에이전트 파일의 경로일 수도 있어요. 예: ./reviewer.md. Apply는 그 에이전트를 먼저 만들고 경로를 고정된 {"type": "agent", "id": ..., "version": ...} 참조로 교체해요.

코디네이터의 구성(그 multiagent.agents 명단 포함)은 코디네이터가 만들어지거나 업데이트될 때 스냅샷돼요. 참조된 에이전트는 그 시점에 해결된 버전에 고정된 채 남고, 나중에 정의에 대한 업데이트를 자동으로 받지 않아요. 참조된 에이전트의 더 새 버전에 위임하려면 코디네이터를 업데이트해 명단이 그 버전을 참조하게 하세요.

코디네이터는 한 단계의 에이전트에만 위임할 수 있어요. 자체 multiagent.agents 명단이 있는 에이전트를 참조하면 생성이나 업데이트 요청이 검증 오류로 실패해요. multiagent.agents에는 최대 20개의 고유 에이전트를 나열할 수 있지만, 코디네이터는 각 에이전트의 여러 복사본을 호출할 수 있어요.

에이전트가 추론 지리(에이전트 정의model.inference_geo)를 고정하면, 코디네이터의 고정과 모든 명단 구성원의 고정이 모두 같은 값으로 설정되거나 모두 설정되지 않아야 해요. 불일치 명단은 에이전트가 저장될 때와 세션 생성 재정의가 어떤 고정을 바꿀 때 모두 400 검증 오류로 거부돼요.

세션에 어드바이저 주기 (Give the session an advisor)

multiagent.agents의 어드바이저 항목은 세션의 기본 스레드에 어드바이저를 줘요: 턴 중간에 전략적 지침(접근 방식 계획, 막힘 해소, 끝내기 전 작업 검토 등)을 위해 상담할 수 있는 모델이에요. 항목은 정확히 두 필드 typemodel을 가져요:

curl -fsS https://api.anthropic.com/v1/agents \
  -H "x-api-key: $ANTHR...KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "anthropic-beta: managed-agents-2026-04-01" \
  -H "content-type: application/json" \
  -d '{
    "name": "Backend engineer",
    "model": "claude-sonnet-5",
    "system": "You implement backend features end to end. Consult the advisor before major backend design decisions.",
    "multiagent": {
      "type": "coordinator",
      "agents": [
        {"type": "advisor", "model": "claude-opus-5-5"}
      ]
    }
  }'

명단은 다른 명단 형태들과 함께 최대 하나의 어드바이저 항목을 포함할 수 있어요. 항목은 예약된 명단 이름 anthropic.advisor를 차지해요: 어드바이저 항목과 문자 그대로 anthropic.advisor라는 이름의 구성원을 모두 나열한 명단은 400 검증 오류로 거부돼요. 응답에서 어드바이저 항목은 제출된 위치와 무관하게 명단의 마지막에 다시 에코돼요.

어드바이저 모델은 최소 능력 기준을 충족해야 하고, 에이전트 자체 모델은 어드바이저보다 더 유능하면 안 돼요. 동등한 능력의 모델은 짝을 이룰 수 있어요. 유효하지 않은 짝은 에이전트가 저장될 때 400 검증 오류로 거부돼요. 유효한 짝은 어드바이저 도구의 모델 호환성 표를 따라요.

어드바이저는 Messages API의 서버 도구로도 사용할 수 있어요. Managed Agents 표면은 구성과 전달에서 차이가 나요: 명단 항목에는 max_uses, max_tokens, caching 필드가 없고, 조언이 advisor_tool_result 블록이 아닌 스레드 이벤트로 도착해요.

상담이 어떻게 동작하나 (How consultations work)

각 상담은 anthropic.advisor라는 이름의 플랫폼 생성 스레드로 실행되며, 상담이 완료되면 스스로 종료되고, 조언은 agent.thread_message_received 이벤트로 기본 스레드에 전달돼요. 상담은 표준 스레드 이벤트를 발행하는데, 예약된 이름 anthropic.advisor로 식별돼요(스레드 수명주기 이벤트는 그것을 agent_name으로, 조언 전달은 from_agent_name으로 싣고), 보통 다음 순서로 와요:

  1. session.thread_created
  2. session.thread_status_running
  3. agent.thread_message_received (the advice)
  4. session.thread_status_idle (stop_reason: end_turn)
  5. session.thread_status_terminated

상담에는 agent.tool_use 이벤트가 발행되지 않고, 세션의 이벤트 스트림에 agent.thread_message_sent 이벤트도 나타나지 않아요. 상담 입력은 에이전트가 보내는 것이 아니라 플랫폼이 구성하기 때문이에요. 어드바이저 스레드 자체의 이벤트를 나열하면 그 조언도 거기에 agent.thread_message_sent 이벤트로 나타나요. 조언 전달(이벤트 3)은 어드바이저 스레드의 idle과 terminated 이벤트보다 먼저 도착한다는 보장이 없으므로, 그것들을 조언이 이미 전달됐다는 신호로 취급하지 마세요.

클라이언트가 조언을 읽을 수 있는지는 어드바이저 모델의 정책이며, Messages API 어드바이저 도구의 결과 변형 분할을 반영해요. 거기서 일반텍스트 결과를 반환하는 어드바이저 모델은 여기서 조언을 읽을 수 있는 텍스트 콘텐츠로 전달해요. 거기서 redacted 결과를 반환하는 어드바이저 모델은 모든 클라이언트 표면에서 메시지 콘텐츠로 [{"type": "redacted"}] 플레이스홀더를 전달하는 반면, 에이전트 자체는 서버 측에서 전체 조언을 여전히 읽어요. 앞선 예시에서 Claude Opus 5는 redacted-결과 어드바이저라서 클라이언트는 플레이스홀더를 보고 에이전트는 전체 조언을 읽어요. 이벤트 스트림에서 조언을 읽을 수 있게 하려면 대신 Claude Opus 4.8을 어드바이저로 선택하세요. 어드바이저의 사고(thinking)는 결코 표면화되지 않아요. 클라이언트는 redacted 블록을 스스로 보낼 수 없어요. 그것을 포함한 이벤트는 400 검증 오류로 거부돼요.

실패하거나 중단된 상담은 에이전트의 턴을 결코 실패시키지 않아요. 에이전트는 상담이 실패했다는 일반 공지 후에 계속돼요. 상담 중 세션 수준의 user.interrupt는 조언이 전달되지 않은 채 어드바이저 스레드를 종료해요. 어드바이저 스레드의 session_thread_id를 가진 user.interrupt는 그 상담만 포기해요.

어드바이저 스레드 (Advisor threads)

어드바이저는 명단 에이전트가 아니에요: 코디네이터의 list_agents 도구에 보이지 않고, send_to_agent로 메시지를 보낼 수 없으며, 세션의 기본 스레드만 상담할 수 있어요. 명단 에이전트는 할 수 없어요.

어드바이저 스레드는 동시 스레드 한도에서 면제돼요. 세션의 스레드 목록agent가 정확히 구성된 어드바이저 형태({"type": "advisor", "model": ...})로, parent_thread_id가 기본 스레드로 설정된 채 나타나요.

어드바이저 쪽의 프롬프트 캐싱은 자동이에요. 구성할 것이 없어요. 상담은 어드바이저 모델 요금으로 청구되고, 그 토큰은 어드바이저 스레드의 사용량과 세션의 사용량 합계에 나타나요.

어드바이저 제거하기 (Removing the advisor)

어드바이저를 제거하려면 더 이상 어드바이저 항목을 포함하지 않는 명단으로 에이전트를 업데이트하세요. 어드바이저가 명단의 유일한 항목이면 "multiagent": null을 설정해 명단을 완전히 비우세요.

세션 만들기 (Create the session)

코디네이터를 참조하는 세션을 만드세요. 코디네이터는 필요에 따라 명단의 에이전트에게 위임해요.

```bash cURL session=$(curl -fsSL https://api.anthropic.com/v1/sessions \ -H "x-api-key: $ANTHR...KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -d @- <ant beta:sessions create \ --agent "$COORDINATOR_ID" \ --environment-id "$ENVIRONMENT_ID"
session = client.beta.sessions.create(
    agent=coordinator.id,
    environment_id=environment.id,
)
const session = await client.beta.sessions.create({
  agent: coordinator.id,
  environment_id: environment.id,
});
var session = await client.Beta.Sessions.Create(new()
{
    Agent = coordinator.ID,
    EnvironmentID = environment.ID,
});
session, err := client.Beta.Sessions.New(ctx, anthropic.BetaSessionNewParams{
	Agent: anthropic.BetaSessionNewParamsAgentUnion{
		OfString: anthropic.String(coordinator.ID),
	},
	EnvironmentID: environment.ID,
})
if err != nil {
	panic(err)
}
var session = client.beta().sessions().create(SessionCreateParams.builder()
    .agent(coordinator.id())
    .environmentId(environment.id())
    .build());
$session = $client->beta->sessions->create(
    agent: $coordinator->id,
    environmentID: $environment->id,
);
session = client.beta.sessions.create(
  agent: coordinator.id,
  environment_id: environment.id
)

에이전트를 MCP 서버에 연결하기 (Connect agents to MCP servers)

MCP 서버는 에이전트 범위로 지정돼요(각 에이전트 정의가 자체 서버와 도구를 선언해요), 반면 볼트 자격 증명은 세션 범위로 지정돼요(세션 생성 시 전달된 vault_ids가 모든 스레드에 적용돼요). 여러분의 통합에 두 가지 함의가 있어요:

  • MCP 서버를 인증하려면 모든 에이전트에서 사용하는 모든 MCP 서버에 대한 볼트 자격 증명을 포함하세요.
  • 에이전트의 접근을 제한하려면 그 정의에 필요한 서버만 선언하세요.

세션 생성 시의 에이전트 구성 재정의가 코디네이터와 그 self 복사본의 MCP 서버를 교체할 수 있어요.

GitHub MCP 서버를 선언하는 연구원(researcher)과 그 연구원에게 위임하는 코디네이터를 만드세요:

```bash cURL research_agent_id=$(curl --fail-with-body -sS "$BASE/v1/agents" "${H[@]}" --data @- <<'EOF' | jq -er '.id' { "name": "researcher", "model": "claude-haiku-4-5", "mcp_servers": [{"type": "url", "name": "github", "url": "https://api.githubcopilot.com/mcp/"}], "tools": [{"type": "mcp_toolset", "mcp_server_name": "github"}] } EOF )

coordinator_id=$(curl --fail-with-body -sS "$BASE/v1/agents" "${H[@]}" --data @- <<EOF | jq -er '.id' { "name": "coordinator", "model": "claude-opus-5-5", "tools": [{"type": "agent_toolset_20260401"}], "multiagent": { "type": "coordinator", "agents": [{"type": "agent", "id": "$research_agent_id"}] } } EOF )


<MultiFileExample language="cli" label="CLI">
  ```bash CLI
  ant apply coordinator.md researcher.md
  ```

  <File filename="coordinator.md">
    ```markdown
    ---
    name: coordinator
    model: claude-opus-5-5
    tools:
      - type: agent_toolset_20260401
    multiagent:
      type: coordinator
      agents: # path: ant apply substitutes {type: agent, id, version}
        - ./researcher.md
    ---
    ```
  </File>

  <File filename="researcher.md">
    ```markdown
    ---
    name: researcher
    model: claude-haiku-4-5
    mcp_servers:
      - type: url
        name: github
        url: https://api.githubcopilot.com/mcp/
    tools:
      - type: mcp_toolset
        mcp_server_name: github
    ---
    ```
  </File>
</MultiFileExample>

```python Python
research_agent = client.beta.agents.create(
    name="researcher",
    model="claude-haiku-4-5",
    mcp_servers=[
        {"type": "url", "name": "github", "url": "https://api.githubcopilot.com/mcp/"},
    ],
    tools=[{"type": "mcp_toolset", "mcp_server_name": "github"}],
)

coordinator = client.beta.agents.create(
    name="coordinator",
    model="claude-opus-5-5",
    tools=[{"type": "agent_toolset_20260401"}],
    multiagent={
        "type": "coordinator",
        "agents": [{"type": "agent", "id": research_agent.id}],
    },
)
const researchAgent = await client.beta.agents.create({
  name: "researcher",
  model: "claude-haiku-4-5",
  mcp_servers: [
    { type: "url", name: "github", url: "https://api.githubcopilot.com/mcp/" },
  ],
  tools: [{ type: "mcp_toolset", mcp_server_name: "github" }],
});

const coordinator = await client.beta.agents.create({
  name: "coordinator",
  model: "claude-opus-5-5",
  tools: [{ type: "agent_toolset_20260401" }],
  multiagent: {
    type: "coordinator",
    agents: [{ type: "agent", id: researchAgent.id }],
  },
});
var researchAgent = await client.Beta.Agents.Create(new()
{
    Name = "researcher",
    Model = BetaManagedAgentsModel.ClaudeHaiku4_5,
    McpServers =
    [
        new()
        {
            Type = BetaManagedAgentsUrlMcpServerParamsType.Url,
            Name = "github",
            Url = "https://api.githubcopilot.com/mcp/",
        },
    ],
    Tools =
    [
        new BetaManagedAgentsMcpToolsetParams
        {
            Type = BetaManagedAgentsMcpToolsetParamsType.McpToolset,
            McpServerName = "github",
        },
    ],
});

var coordinator = await client.Beta.Agents.Create(new()
{
    Name = "coordinator",
    Model = BetaManagedAgentsModel.ClaudeOpus5_5,
    Tools =
    [
        new BetaManagedAgentsAgentToolset20260401Params
        {
            Type = BetaManagedAgentsAgentToolset20260401ParamsType.AgentToolset20260401,
        },
    ],
    Multiagent = new()
    {
        Type = BetaManagedAgentsMultiagentParamsType.Coordinator,
        Agents =
        [
            new BetaManagedAgentsAgentParams
            {
                Type = BetaManagedAgentsAgentParamsType.Agent,
                ID = researchAgent.ID,
            },
        ],
    },
});
researcher, err := client.Beta.Agents.New(ctx, anthropic.BetaAgentNewParams{
	Name:  "researcher",
	Model: anthropic.BetaManagedAgentsModelConfigParams{ID: anthropic.BetaManagedAgentsModelClaudeHaiku4_5},
	MCPServers: []anthropic.BetaManagedAgentsURLMCPServerParams{{
		Type: anthropic.BetaManagedAgentsURLMCPServerParamsTypeURL,
		Name: "github",
		URL:  "https://api.githubcopilot.com/mcp/",
	}},
	Tools: []anthropic.BetaAgentNewParamsToolUnion{{
		OfMCPToolset: &anthropic.BetaManagedAgentsMCPToolsetParams{
			Type:          anthropic.BetaManagedAgentsMCPToolsetParamsTypeMCPToolset,
			MCPServerName: "github",
		},
	}},
})
if err != nil {
	panic(err)
}

coordinator, err := client.Beta.Agents.New(ctx, anthropic.BetaAgentNewParams{
	Name:  "coordinator",
	Model: anthropic.BetaManagedAgentsModelConfigParams{ID: anthropic.BetaManagedAgentsModelClaudeOpus5_5},
	Tools: []anthropic.BetaAgentNewParamsToolUnion{{
		OfAgentToolset20260401: &anthropic.BetaManagedAgentsAgentToolset20260401Params{
			Type: anthropic.BetaManagedAgentsAgentToolset20260401ParamsTypeAgentToolset20260401,
		},
	}},
	Multiagent: anthropic.BetaManagedAgentsMultiagentParams{
		Type: anthropic.BetaManagedAgentsMultiagentParamsTypeCoordinator,
		Agents: []anthropic.BetaManagedAgentsMultiagentRosterEntryParamsUnion{{
			OfBetaManagedAgentsAgents: &anthropic.BetaManagedAgentsAgentParams{
				Type: anthropic.BetaManagedAgentsAgentParamsTypeAgent,
				ID:   researcher.ID,
			},
		}},
	},
})
if err != nil {
	panic(err)
}
var researcher = client.beta().agents().create(
    AgentCreateParams.builder()
        .name("researcher")
        .model(BetaManagedAgentsModel.CLAUDE_HAIKU_4_5)
        .addMcpServer(BetaManagedAgentsUrlMcpServerParams.builder()
            .name("github")
            .type(BetaManagedAgentsUrlMcpServerParams.Type.URL)
            .url("https://api.githubcopilot.com/mcp/")
            .build())
        .addTool(BetaManagedAgentsMcpToolsetParams.builder()
            .type(BetaManagedAgentsMcpToolsetParams.Type.MCP_TOOLSET)
            .mcpServerName("github")
            .build())
        .build()
);

var coordinator = client.beta().agents().create(
    AgentCreateParams.builder()
        .name("coordinator")
        .model(BetaManagedAgentsModel.CLAUDE_OPUS_5_5)
        .addTool(BetaManagedAgentsAgentToolset20260401Params.builder()
            .type(BetaManagedAgentsAgentToolset20260401Params.Type.AGENT_TOOLSET_20260401)
            .build())
        .multiagent(BetaManagedAgentsMultiagentParams.builder()
            .type(BetaManagedAgentsMultiagentParams.Type.COORDINATOR)
            .addAgent(BetaManagedAgentsAgentParams.builder()
                .type(BetaManagedAgentsAgentParams.Type.AGENT)
                .id(researcher.id())
                .build())
            .build())
        .build()
);
$researchAgent = $client->beta->agents->create(
    name: 'researcher',
    model: 'claude-haiku-4-5',
    mcpServers: [
        ['type' => 'url', 'name' => 'github', 'url' => 'https://api.githubcopilot.com/mcp/'],
    ],
    tools: [
        ['type' => 'mcp_toolset', 'mcp_server_name' => 'github'],
    ],
);

$coordinator = $client->beta->agents->create(
    name: 'coordinator',
    model: 'claude-opus-5-5',
    tools: [
        ['type' => 'agent_toolset_20260401'],
    ],
    multiagent: [
        'type' => 'coordinator',
        'agents' => [
            ['type' => 'agent', 'id' => $researchAgent->id],
        ],
    ],
);
research_agent = client.beta.agents.create(
  name: "researcher",
  model: "claude-haiku-4-5",
  mcp_servers: [
    {type: "url", name: "github", url: "https://api.githubcopilot.com/mcp/"}
  ],
  tools: [
    {type: "mcp_toolset", mcp_server_name: "github"}
  ]
)

coordinator = client.beta.agents.create(
  name: "coordinator",
  model: "claude-opus-5-5",
  tools: [
    {type: "agent_toolset_20260401"}
  ],
  multiagent: {
    type: "coordinator",
    agents: [
      {type: "agent", id: research_agent.id}
    ]
  }
)

그다음 GitHub 자격 증명을 가진 볼트로 세션을 만드세요:

```bash cURL session_id=$(curl --fail-with-body -sS "$BASE/v1/sessions" "${H[@]}" --data @- <session_id=$(ant beta:sessions create \ --agent "$coordinator_id" \ --environment-id "$environment_id" \ --vault-id "$vault_id" \ --transform id --raw-output) echo "$session_id"
session = client.beta.sessions.create(
    agent=coordinator.id,
    environment_id=environment.id,
    vault_ids=[vault.id],
)
print(session.id)
const session = await client.beta.sessions.create({
  agent: coordinator.id,
  environment_id: environment.id,
  vault_ids: [vault.id],
});
console.log(session.id);
var session = await client.Beta.Sessions.Create(new()
{
    Agent = coordinator.ID,
    EnvironmentID = environment.ID,
    VaultIds = [vault.ID],
});
Console.WriteLine(session.ID);
session, err := client.Beta.Sessions.New(ctx, anthropic.BetaSessionNewParams{
	Agent: anthropic.BetaSessionNewParamsAgentUnion{
		OfString: anthropic.String(coordinator.ID),
	},
	EnvironmentID: environment.ID,
	VaultIDs:      []string{vault.ID},
})
if err != nil {
	panic(err)
}
fmt.Println(session.ID)
var session = client.beta().sessions().create(SessionCreateParams.builder()
    .agent(coordinator.id())
    .environmentId(environment.id())
    .vaultIds(List.of(vault.id()))
    .build());
IO.println(session.id());
$session = $client->beta->sessions->create(
    agent: $coordinator->id,
    environmentID: $environment->id,
    vaultIDs: [$vault->id],
);
echo "{$session->id}\n";
session = client.beta.sessions.create(
  agent: coordinator.id,
  environment_id: environment.id,
  vault_ids: [vault.id]
)
puts session.id

이 예시에서 GitHub MCP 서버를 선언한 것은 연구원뿐이라 코디네이터는 접근할 수 없어요. 세션의 vault_ids가 GitHub 자격 증명을 연구원의 스레드에 공급해요.

서버를 선언한 뒤 에이전트의 MCP 호출이 인증에 실패하면, 자격 증명의 `mcp_server_url`이 에이전트의 `mcp_servers[].url`과 같은 서버를 가리키는지 확인하세요. 두 URL은 매칭 전에 정규화돼요(스킴과 호스트는 소문자로, 기본 포트와 끝 슬래시는 제거), 그래서 호스트 대소문자, 기본 포트, 끝 슬래시의 차이는 매칭을 막지 못해요. 다른 경로, 하위 도메인, 비기본 포트는 막아요.

스레드 (Threads)

세션 수준 이벤트 스트림(/v1/sessions/{session_id}/events/stream)은 **기본 스레드(primary thread)**로 간주되며, 모든 스레드에 걸친 활동의 압축된 뷰를 담아요. 하위 에이전트의 전체 활동은 보이지 않지만, 그 작업의 시작과 끝, 그리고 도구 권한 요청 같은 차단 이벤트는 보여요.

**세션 스레드(session thread)**는 특정 에이전트의 활동으로 드릴다운하는 곳이에요.

세션 status는 모든 에이전트 활동의 집계예요. 최소 하나의 스레드가 running이면 전체 세션 상태도 running이에요.

세션 예산은 세션의 모든 스레드에 걸친 단일 공유 상한이에요. 상한에 도달하면 스레드가 독립적으로 멈추고, 각 스레드의 비용은 그 스레드가 서빙한 모델로 책정돼요.

최대 25개의 동시 스레드가 지원돼요. 코디네이터는 명단의 단일 에이전트의 여러 복사본을 호출해 하나의 `agent`와 연결된 여러 스레드를 만들 수 있어요. [어드바이저](https://platform.claude.com/docs/en/managed-agents/multiagent-orchestration#give-the-session-an-advisor) 상담 스레드는 이 한도에서 면제돼요. 세션과 연결된 모든 스레드를 다음과 같이 나열해요:
<CodeGroup>
  ```bash cURL
  curl -fsS "https://api.anthropic.com/v1/sessions/$SESSION_ID/threads" \
    -H "x-api-key: $ANTHR...KEY" \
    -H "anthropic-version: 2023-06-01" \
    -H "anthropic-beta: managed-agents-2026-04-01" \
    | jq -r '.data[] | "[\(.agent.name)] \(.status)"'
  ```

  ```bash CLI
  ant beta:sessions:threads list --session-id "$SESSION_ID"
  ```

  ```python Python
  for thread in client.beta.sessions.threads.list(session.id):
      print(f"[{thread.agent.name}] {thread.status}")
  ```

  ```typescript TypeScript
  for await (const thread of client.beta.sessions.threads.list(session.id)) {
    const name = thread.agent.type === "agent" ? thread.agent.name : "advisor";
    console.log(`[${name}] ${thread.status}`);
  }
  ```

  ```csharp C#
  await foreach (var thread in (await client.Beta.Sessions.Threads.List(session.ID)).Paginate())
  {
      Console.WriteLine($"[{thread.Agent.Name}] {thread.Status}");
  }
  ```

  ```go Go
  threads := client.Beta.Sessions.Threads.ListAutoPaging(ctx, session.ID, anthropic.BetaSessionThreadListParams{})
  for threads.Next() {
  	thread := threads.Current()
  	fmt.Printf("[%s] %s\n", thread.Agent.Name, thread.Status)
  }
  if err := threads.Err(); err != nil {
  	panic(err)
  }
  ```

  ```java Java
  for (var thread : client.beta().sessions().threads().list(session.id()).autoPager()) {
      var name = thread.agent().isAgent() ? thread.agent().asAgent().name() : "advisor";
      IO.println("[" + name + "] " + thread.status());
  }
  ```

  ```php PHP
  foreach ($client->beta->sessions->threads->list($session->id)->pagingEachItem() as $thread) {
      echo "[{$thread->agent->name}] {$thread->status}\n";
  }
  ```

  ```ruby Ruby
  client.beta.sessions.threads.list(session.id).auto_paging_each do |thread|
    puts "[#{thread.agent.name}] #{thread.status}"
  end
  ```
</CodeGroup>

전체 목록에는 기본 스레드가 포함돼요. `parent_thread_id`는 기본 스레드에서 null이에요.
특정 스레드를 멈추려면 `session_thread_id`와 함께 `user.interrupt`를 보내세요. `session_thread_id`를 생략하면 세션의 모든 비보관 스레드(기본 스레드 포함)를 중단해요.
<CodeGroup>
  ```bash cURL
  curl -fsS "https://api.anthropic.com/v1/sessions/$SESSION_ID/events?beta=true" \
    -H "x-api-key: $ANTHR...KEY" \
    -H "anthropic-version: 2023-06-01" \
    -H "anthropic-beta: managed-agents-2026-04-01" \
    -H "content-type: application/json" \
    -d "{\"events\": [{\"type\": \"user.interrupt\", \"session_thread_id\": \"$THREAD_ID\"}]}"
  ```

  ```bash CLI
  ant beta:sessions:events send \
    --session-id "$SESSION_ID" \
    --event "{type: user.interrupt, session_thread_id: $THREAD_ID}"
  ```

  ```python Python
  client.beta.sessions.events.send(
      session.id,
      events=[{"type": "user.interrupt", "session_thread_id": thread.id}],
  )
  ```

  ```typescript TypeScript
  await client.beta.sessions.events.send(session.id, {
    events: [{ type: "user.interrupt", session_thread_id: thread.id }],
  });
  ```

  ```csharp C#
  await client.Beta.Sessions.Events.Send(session.ID, new()
  {
      Events =
      [
          new BetaManagedAgentsUserInterruptEventParams
          {
              Type = BetaManagedAgentsUserInterruptEventParamsType.UserInterrupt,
              SessionThreadID = thread.ID,
          },
      ],
  });
  ```

  ```go Go
  if _, err := client.Beta.Sessions.Events.Send(ctx, session.ID, anthropic.BetaSessionEventSendParams{
  	Events: []anthropic.BetaManagedAgentsEventParamsUnion{{
  		OfUserInterrupt: &anthropic.BetaManagedAgentsUserInterruptEventParams{
  			Type:            anthropic.BetaManagedAgentsUserInterruptEventParamsTypeUserInterrupt,
  			SessionThreadID: anthropic.String(thread.ID),
  		},
  	}},
  }); err != nil {
  	panic(err)
  }
  ```

  ```java Java
  client.beta().sessions().events().send(
      session.id(),
      EventSendParams.builder()
          .addEvent(BetaManagedAgentsUserInterruptEventParams.builder()
              .type(BetaManagedAgentsUserInterruptEventParams.Type.USER_INTERRUPT)
              .sessionThreadId(thread.id())
              .build())
          .build());
  ```

  ```php PHP
  $client->beta->sessions->events->send(
      $session->id,
      events: [
          ['type' => 'user.interrupt', 'session_thread_id' => $thread->id],
      ],
  );
  ```

  ```ruby Ruby
  client.beta.sessions.events.send_(
    session.id,
    events: [{type: "user.interrupt", session_thread_id: thread.id}]
  )
  ```
</CodeGroup>

`requires_action`에 막힌 하위 스레드에 대해, 중단은 각 보류 도구 호출을 오류 도구 결과("Tool execution was interrupted before completion. Please retry.")로 닫고 모델을 샘플링하지 않고 직접 `stop_reason: end_turn`으로 `session.thread_status_idle`을 다시 발행해요. 이미 `idle`인 스레드에 대해 중단은 no-op이에요.
선택적으로 스레드가 작업을 완료했을 때 보관하세요. 그러면 25-스레드 한도에 반해 스레드 하나가 풀려요.
<CodeGroup>
  ```bash cURL
  curl -fsS -X POST "https://api.anthropic.com/v1/sessions/$SESSION_ID/threads/$THREAD_ID/archive" \
    -H "x-api-key: $ANTHR...KEY" \
    -H "anthropic-version: 2023-06-01" \
    -H "anthropic-beta: managed-agents-2026-04-01"
  ```

  ```bash CLI
  ant beta:sessions:threads archive \
    --session-id "$SESSION_ID" \
    --thread-id "$THREAD_ID"
  ```

  ```python Python
  archived = client.beta.sessions.threads.archive(thread.id, session_id=session.id)
  print(archived.status, archived.archived_at)
  ```

  ```typescript TypeScript
  const archived = await client.beta.sessions.threads.archive(thread.id, {
    session_id: session.id,
  });
  console.log(archived.status, archived.archived_at);
  ```

  ```csharp C#
  var archived = await client.Beta.Sessions.Threads.Archive(thread.ID, new() { SessionID = session.ID });
  Console.WriteLine($"{archived.Status} {archived.ArchivedAt}");
  ```

  ```go Go
  archived, err := client.Beta.Sessions.Threads.Archive(ctx, thread.ID, anthropic.BetaSessionThreadArchiveParams{
  	SessionID: session.ID,
  })
  if err != nil {
  	panic(err)
  }
  fmt.Println(archived.Status, archived.ArchivedAt)
  ```

  ```java Java
  var archived = client.beta().sessions().threads().archive(
      thread.id(),
      ThreadArchiveParams.builder()
          .sessionId(session.id())
          .build());
  IO.println(archived.status() + " " + archived.archivedAt().orElseThrow());
  ```

  ```php PHP
  $archived = $client->beta->sessions->threads->archive($thread->id, sessionID: $session->id);
  echo "{$archived->status} {$archived->archivedAt->format(DATE_ATOM)}\n";
  ```

  ```ruby Ruby
  archived = client.beta.sessions.threads.archive(thread.id, session_id: session.id)
  puts "#{archived.status} #{archived.archived_at}"
  ```
</CodeGroup>

보관은 스레드가 `idle`일 때만 성공해요. `requires_action`에 막힌 스레드는 idle로 간주되어 직접 보관할 수 있어요. running 스레드만 먼저 중단해야 해요:

<CodeGroup>
  ```bash cURL
  # Interrupt the thread, then archive it
  curl -fsS "https://api.anthropic.com/v1/sessions/$SESSION_ID/events?beta=true" \
    -H "x-api-key: $ANTHR...KEY" \
    -H "anthropic-version: 2023-06-01" \
    -H "anthropic-beta: managed-agents-2026-04-01" \
    -H "content-type: application/json" \
    -d "{\"events\": [{\"type\": \"user.interrupt\", \"session_thread_id\": \"$THREAD_ID\"}]}"

  curl -fsS -X POST "https://api.anthropic.com/v1/sessions/$SESSION_ID/threads/$THREAD_ID/archive" \
    -H "x-api-key: $ANTHR...KEY" \
    -H "anthropic-version: 2023-06-01" \
    -H "anthropic-beta: managed-agents-2026-04-01"
  ```

  ```bash CLI
  ant beta:sessions:events send \
    --session-id "$SESSION_ID" \
    --event "{type: user.interrupt, session_thread_id: $THREAD_ID}"

  ant beta:sessions:threads archive \
    --session-id "$SESSION_ID" \
    --thread-id "$THREAD_ID"
  ```

  ```python Python
  client.beta.sessions.events.send(
      session.id,
      events=[{"type": "user.interrupt", "session_thread_id": thread.id}],
  )
  archived = client.beta.sessions.threads.archive(thread.id, session_id=session.id)
  print(archived.status, archived.archived_at)
  ```

  ```typescript TypeScript
  await client.beta.sessions.events.send(session.id, {
    events: [{ type: "user.interrupt", session_thread_id: thread.id }],
  });
  const archived = await client.beta.sessions.threads.archive(thread.id, {
    session_id: session.id,
  });
  console.log(archived.status, archived.archived_at);
  ```

  ```csharp C#
  await client.Beta.Sessions.Events.Send(session.ID, new()
  {
      Events =
      [
          new BetaManagedAgentsUserInterruptEventParams
          {
              Type = BetaManagedAgentsUserInterruptEventParamsType.UserInterrupt,
              SessionThreadID = thread.ID,
          },
      ],
  });
  archived = await client.Beta.Sessions.Threads.Archive(thread.ID, new() { SessionID = session.ID });
  Console.WriteLine($"{archived.Status} {archived.ArchivedAt}");
  ```

  ```go Go
  if _, err := client.Beta.Sessions.Events.Send(ctx, session.ID, anthropic.BetaSessionEventSendParams{
  	Events: []anthropic.BetaManagedAgentsEventParamsUnion{{
  		OfUserInterrupt: &anthropic.BetaManagedAgentsUserInterruptEventParams{
  			Type:            anthropic.BetaManagedAgentsUserInterruptEventParamsTypeUserInterrupt,
  			SessionThreadID: anthropic.String(thread.ID),
  		},
  	}},
  }); err != nil {
  	panic(err)
  }

  archived, err := client.Beta.Sessions.Threads.Archive(ctx, thread.ID, anthropic.BetaSessionThreadArchiveParams{
  	SessionID: session.ID,
  })
  if err != nil {
  	panic(err)
  }
  fmt.Println(archived.Status, archived.ArchivedAt)
  ```

  ```java Java
  client.beta().sessions().events().send(
      session.id(),
      EventSendParams.builder()
          .addEvent(BetaManagedAgentsUserInterruptEventParams.builder()
              .type(BetaManagedAgentsUserInterruptEventParams.Type.USER_INTERRUPT)
              .sessionThreadId(thread.id())
              .build())
          .build());

  archived = client.beta().sessions().threads().archive(
      thread.id(),
      ThreadArchiveParams.builder()
          .sessionId(session.id())
          .build());
  IO.println(archived.status() + " " + archived.archivedAt().orElseThrow());
  ```

  ```php PHP
  $client->beta->sessions->events->send(
      $session->id,
      events: [['type' => 'user.interrupt', 'session_thread_id' => $thread->id]],
  );
  $archived = $client->beta->sessions->threads->archive($thread->id, sessionID: $session->id);
  echo "{$archived->status} {$archived->archivedAt->format(DATE_ATOM)}\n";
  ```

  ```ruby Ruby
  client.beta.sessions.events.send_(
    session.id,
    events: [{type: "user.interrupt", session_thread_id: thread.id}]
  )
  archived = client.beta.sessions.threads.archive(thread.id, session_id: session.id)
  puts "#{archived.status} #{archived.archived_at}"
  ```
</CodeGroup>

기본 스레드 이벤트 (Primary thread events)

이 이벤트들은 /v1/sessions/{session_id}/events/stream의 기본 스레드에 멀티에이전트 활동을 표면화해요. 메시지 방향 이벤트는 이벤트가 나타나는 스레드를 기준으로 이름이 지어져요: agent.thread_message_received는 다른 스레드에서 이 스레드로 메시지가 도착했음을, agent.thread_message_sent는 이 스레드가 하나를 보냈음을 뜻해요. 예를 들어 코디네이터가 위임하는 작업은 하위 스레드의 자체 스트림에 agent.thread_message_received 이벤트로 도착해요.

유형 (Type) 설명 (Description)
session.thread_created 스레드가 생성됨. session_thread_idagent_name를 포함해요.
session.thread_status_running 스레드가 활동을 시작함.
session.thread_status_idle 스레드와 연결된 에이전트가 입력을 기다리고 있음. 에이전트가 멈춘 이유를 나타내는 stop_reason 포함.
session.thread_status_terminated 스레드가 보관되었거나 종료 오류를 만남.
agent.thread_message_received 기본 스레드에서, 에이전트가 코디네이터에게 보고나 질문을 보냄. from_session_thread_id, from_agent_name, content 포함.
agent.thread_message_sent 기본 스레드에서, 코디네이터가 다른 에이전트에게 작업이나 후속 메시지를 보냄. to_session_thread_id, to_agent_name, content 포함.

어드바이저 상담은 예약된 이름 anthropic.advisor 아래에서(스레드 수명주기 이벤트의 agent_name으로, 조언 전달의 from_agent_name으로) 같은 스레드 이벤트를 발행해요. 순서는 세션에 어드바이저 주기를 참고하세요.

세션 스레드 이벤트 (Session thread events)

중요한 이벤트는 기본 스레드로 프록시돼요. 하지만 특정 에이전트의 추론과 도구 호출을 조사하고 싶을 수 있어요. 그렇게 하려면 연결된 세션 스레드의 이벤트를 스트리밍하거나 나열하세요.

각 세션 스레드는 /v1/sessions/{session_id}/threads/{thread_id}/stream에 자체 이벤트 스트림이 있고, 세션 수준 스트림과 같은 event_deltas[] 파라미터를 받아들이므로 모델이 생성할 때 하위 에이전트의 텍스트를 미리 볼 수 있어요. 연결은 읽고 있는 스레드만 미리 봐요: 하위 스레드의 미리보기가 세션 수준 스트림에는 결코 나타나지 않으므로, 하위 에이전트를 실시간으로 보려면 그 자체 스레드 스트림을 여세요. 미리보기 옵트인, 누적, 조정은 세션 스레드 이벤트 미리보기를 참고하세요.

```bash cURL curl -fsSN "https://api.anthropic.com/v1/sessions/$SESSION_ID/threads/$THREAD_ID/stream?beta=true" \ -H "x-api-key: $ANTHR...KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" | while IFS= read -r line; do [[ $line == data:* ]] || continue json=${line#data: } case $(jq -r '.type' <<<"$json") in agent.message) printf '%s' "$(jq -j '.content[] | select(.type == "text") | .text' <<<"$json")" ;; session.thread_status_idle) break ;; esac done ```
  ```bash CLI
  ant beta:sessions:threads:events stream \
    --session-id "$SESSION_ID" \
    --thread-id "$THREAD_ID"
  ```

  ```python Python
  with client.beta.sessions.threads.events.stream(
      thread.id,
      session_id=session.id,
  ) as stream:
      for event in stream:
          match event.type:
              case "agent.message":
                  for block in event.content:
                      if block.type == "text":
                          print(block.text, end="")
              case "session.thread_status_idle":
                  break
  ```

  ```typescript TypeScript
  const stream = await client.beta.sessions.threads.events.stream(thread.id, {
    session_id: session.id,
  });

  loop: for await (const event of stream) {
    switch (event.type) {
      case "agent.message":
        for (const block of event.content) {
          if (block.type === "text") {
            process.stdout.write(block.text);
          }
        }
        break;
      case "session.thread_status_idle":
        break loop;
    }
  }
  ```

  ```csharp C#
  await foreach (var evt in client.Beta.Sessions.Threads.Events.StreamStreaming(thread.ID, new() { SessionID = session.ID }))
  {
      if (evt.Value is BetaManagedAgentsAgentMessageEvent message)
      {
          foreach (var block in message.Content)
          {
              if (block.Type == "text")
              {
                  Console.Write(block.Text);
              }
          }
      }
      else if (evt.Value is BetaManagedAgentsSessionThreadStatusIdleEvent)
      {
          break;
      }
  }
  ```

  ```go Go
  	stream := client.Beta.Sessions.Threads.Events.StreamEvents(ctx, thread.ID, anthropic.BetaSessionThreadEventStreamParams{
  		SessionID: session.ID,
  	})
  	defer stream.Close()

  loop:
  	for stream.Next() {
  		event := stream.Current()
  		switch event.Type {
  		case "agent.message":
  			for _, block := range event.AsAgentMessage().Content {
  				if block.Type == "text" {
  					fmt.Print(block.Text)
  				}
  			}
  		case "session.thread_status_idle":
  			break loop
  		}
  	}
  	if err := stream.Err(); err != nil {
  		panic(err)
  	}
  ```

  ```java Java
  try (var streamResponse = client.beta().sessions().threads().events().streamStreaming(
      thread.id(),
      EventStreamParams.builder().sessionId(session.id()).build()
  )) {
      loop:
      for (var event : (Iterable<BetaManagedAgentsStreamSessionThreadEvents>) streamResponse.stream()::iterator) {
          switch (event.type().value()) {
              case AGENT_MESSAGE -> {
                  for (var block : event.asAgentMessage().content()) {
                      block.text().ifPresent(textBlock -> IO.print(textBlock.text()));
                  }
              }
              case SESSION_THREAD_STATUS_IDLE -> {
                  break loop;
              }
          }
      }
  }
  ```

  ```php PHP
  $stream = $client->beta->sessions->threads->events->streamStream(
      $thread->id,
      sessionID: $session->id,
  );

  foreach ($stream as $event) {
      switch (true) {
          case $event instanceof \Anthropic\Beta\Sessions\Events\ManagedAgentsAgentMessageEvent:
              foreach ($event->content as $block) {
                  if ($block instanceof \Anthropic\Beta\Sessions\Events\ManagedAgentsTextBlock) {
                      echo $block->text;
                  }
              }
              break;
          case $event instanceof \Anthropic\Beta\Sessions\Events\ManagedAgentsSessionThreadStatusIdleEvent:
              break 2;
      }
  }
  ```

  ```ruby Ruby
  client.beta.sessions.threads.events.stream_events(thread.id, session_id: session.id).each do |event|
    case event
    when Anthropic::Beta::Sessions::BetaManagedAgentsAgentMessageEvent
      event.content.each do |block|
        print block.text if block.is_a?(Anthropic::Beta::Sessions::BetaManagedAgentsTextBlock)
      end
    when Anthropic::Beta::Sessions::BetaManagedAgentsSessionThreadStatusIdleEvent
      break
    end
  end
  ```
</CodeGroup>
완전한 이력을 가져오려면 모든 과거 세션 스레드 이벤트를 나열해요.
<CodeGroup>
  ```bash cURL
  curl -fsS "https://api.anthropic.com/v1/sessions/$SESSION_ID/threads/$THREAD_ID/events" \
    -H "x-api-key: $ANTHR...KEY" \
    -H "anthropic-version: 2023-06-01" \
    -H "anthropic-beta: managed-agents-2026-04-01" \
    | jq -r '.data[] | "[\(.type)] \(.processed_at)"'
  ```

  ```bash CLI
  ant beta:sessions:threads:events list \
    --session-id "$SESSION_ID" \
    --thread-id "$THREAD_ID"
  ```

  ```python Python
  for event in client.beta.sessions.threads.events.list(
      thread.id,
      session_id=session.id,
  ):
      print(f"[{event.type}] {event.processed_at}")
  ```

  ```typescript TypeScript
  for await (const event of client.beta.sessions.threads.events.list(thread.id, {
    session_id: session.id,
  })) {
    console.log(`[${event.type}] ${event.processed_at}`);
  }
  ```

  ```csharp C#
  var page = await client.Beta.Sessions.Threads.Events.List(thread.ID, new() { SessionID = session.ID });
  await foreach (var evt in page.Paginate())
  {
      Console.WriteLine($"[{evt.Type}] {evt.ProcessedAt}");
  }
  ```

  ```go Go
  pager := client.Beta.Sessions.Threads.Events.ListAutoPaging(ctx, thread.ID, anthropic.BetaSessionThreadEventListParams{
  	SessionID: session.ID,
  })
  for pager.Next() {
  	event := pager.Current()
  	fmt.Printf("[%s] %s\n", event.Type, event.ProcessedAt)
  }
  if err := pager.Err(); err != nil {
  	panic(err)
  }
  ```

  ```java Java
  for (var event : client.beta().sessions().threads().events().list(
          thread.id(),
          EventListParams.builder().sessionId(session.id()).build()
      ).autoPager()) {
      var type = event._json().orElseThrow() instanceof JsonObject json
          ? json.values().get("type").asStringOrThrow()
          : "unknown";
      var processedAt = event.processedAt().map(OffsetDateTime::toString).orElse("pending");
      IO.println("[" + type + "] " + processedAt);
  }
  ```

  ```php PHP
  foreach (
      $client->beta->sessions->threads->events->list(
          $thread->id,
          sessionID: $session->id,
      )->pagingEachItem() as $event
  ) {
      echo "[{$event->type}] {$event->processedAt->format(DATE_RFC3339)}\n";
  }
  ```

  ```ruby Ruby
  client.beta.sessions.threads.events.list(
    thread.id,
    session_id: session.id
  ).auto_paging_each do |event|
    puts "[#{event.type}] #{event.processed_at}"
  end
  ```
</CodeGroup>

도구 권한과 커스텀 도구 (Tool permissions and custom tools)

하위 에이전트가 여러분의 클라이언트에서 무언가가 필요하면(도구 호출을 실행할 권한 또는 커스텀 도구의 결과 같은), 그 이벤트는 발원하는 세션 스레드를 식별하는 session_thread_id와 함께 기본 스레드에 교차 게시돼요. 도구 호출은 always_ask 아래에서, 또는 auto 아래에서 서버가 판단에 이르지 못할 때 여러분의 권한이 필요해요.

{
  "type": "session.thread_status_idle",
  "id": "sevt_01ABC...",
  "session_thread_id": "sth_01DEF...",
  "agent_name": "code-reviewer",
  "stop_reason": {
    "type": "requires_action",
    "event_ids": ["sevt_01XYZ..."]
  }
}

user.tool_confirmation(tool_use_id 포함) 또는 user.custom_tool_result(custom_tool_use_id 포함)를 게시하세요. 서버가 응답을 올바른 스레드로 자동 라우팅해요.

auto 아래에서, 여러분의 user.message 이벤트는 서버가 평소 거부할 호출을 허용하게 이끌 수 있어요. 하위 에이전트의 스레드에 있는 어떤 것도 여러분의 의도로 간주되지 않아요: 여러분의 클라이언트는 거기에 메시지를 게시하지 않고, 코디네이터가 하위 에이전트에게 보내는 메시지도 의도로 세지 않아요. auto 아래에서 서버가 호출을 거부할 때 아무것도 교차 게시되지 않아요: 이벤트와 오류 도구 결과는 하위 에이전트의 자체 스레드 스트림에만 나타나고, 하위 에이전트는 계속 실행돼요.

다음 예시는 도구 확인 핸들러를 확장해 응답을 라우팅해요. 같은 패턴이 user.custom_tool_result에 적용돼요.

```bash cURL while IFS= read -r event_id; do jq -n --arg id "$event_id" \ '{events: [{type: "user.tool_confirmation", tool_use_id: $id, result: "allow"}]}' | curl -fsS "https://api.anthropic.com/v1/sessions/$SESSION_ID/events?beta=true" \ -H "x-api-key: $ANTHR...KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: managed-agents-2026-04-01" \ -H "content-type: application/json" \ -d @- done < <(jq -r '.stop_reason.event_ids[]' <<<"$data") ```
# This workflow does not translate well to a one-off shell command.
# Use one of the SDK examples in this code group instead.
for event_id in stop.event_ids:
    client.beta.sessions.events.send(
        session.id,
        events=[
            {
                "type": "user.tool_confirmation",
                "tool_use_id": event_id,
                "result": "allow",
            }
        ],
    )
for (const eventId of stop.event_ids) {
  await client.beta.sessions.events.send(session.id, {
    events: [
      {
        type: "user.tool_confirmation",
        tool_use_id: eventId,
        result: "allow",
      },
    ],
  });
}
foreach (var eventId in requiresAction.EventIds)
{
    await client.Beta.Sessions.Events.Send(session.ID, new()
    {
        Events =
        [
            new BetaManagedAgentsUserToolConfirmationEventParams
            {
                Type = BetaManagedAgentsUserToolConfirmationEventParamsType.UserToolConfirmation,
                ToolUseID = eventId,
                Result = BetaManagedAgentsUserToolConfirmationEventParamsResult.Allow,
            },
        ],
    });
}
for _, eventID := range stopReason.EventIDs {
	params := anthropic.BetaManagedAgentsUserToolConfirmationEventParams{
		Type:      anthropic.BetaManagedAgentsUserToolConfirmationEventParamsTypeUserToolConfirmation,
		ToolUseID: eventID,
		Result:    anthropic.BetaManagedAgentsUserToolConfirmationEventParamsResultAllow,
	}
	if _, err := client.Beta.Sessions.Events.Send(ctx, session.ID, anthropic.BetaSessionEventSendParams{
		Events: []anthropic.BetaManagedAgentsEventParamsUnion{{OfUserToolConfirmation: &params}},
	}); err != nil {
		panic(err)
	}
}
for (var eventId : pendingToolUseIds) {
    client.beta().sessions().events().send(
        session.id(),
        EventSendParams.builder()
            .addEvent(BetaManagedAgentsUserToolConfirmationEventParams.builder()
                .toolUseId(eventId)
                .result(BetaManagedAgentsUserToolConfirmationEventParams.Result.ALLOW)
                .build())
            .build()
    );
}
foreach ($event->stopReason->eventIDs as $eventId) {
    $client->beta->sessions->events->send($session->id, events: [[
        'type' => 'user.tool_confirmation',
        'tool_use_id' => $eventId,
        'result' => 'allow',
    ]]);
}
event_ids.each do |event_id|
  client.beta.sessions.events.send_(session.id, events: [{
    type: "user.tool_confirmation",
    tool_use_id: event_id,
    result: "allow"
  }])
end

더 알아보기 (Learn more)