---
title: 오케스트레이션 모드 구축하기
url: https://platform.claude.com/docs/ko/build-with-claude/mid-conversation-effort-example
description: 멀티에이전트 팬아웃에 대한 상시 동의를 부여하고, 대화 중 시스템 메시지로 켜고 끌 수 있는 세션 수준 모드를 구축합니다.
---

"Orchestration mode"(오케스트레이션 모드)는 세션 수준의 스위치입니다. 이 모드가 켜져 있으면 모델은 모든 실질적인 요청에 최대한의 철저함을 기울여, 작업을 직접 탐색한 다음 기본적으로 병렬 서브에이전트로 작업을 "fan-out"(팬아웃)합니다. 모드가 꺼져 있으면 동일한 오케스트레이션 도구는 요청별 옵트인 방식으로 돌아갑니다.

이 모드는 API 매개변수가 아닙니다. 전적으로 문서화된 구성 요소로 만들어집니다:

1. **effort 수준:** 요청은 `xhigh`와 같은 문서화된 [Effort](https://platform.claude.com/docs/ko/build-with-claude/effort) 값으로 실행됩니다. 해당 페이지에 있는 수준 위에 숨겨진 수준은 없습니다. 이 예제는 각 요청의 최상위 수준에서 effort를 설정하며, 베타 헤더가 필요하지 않습니다.
2. **모드 리마인더:** [대화 중 시스템 메시지](https://platform.claude.com/docs/ko/build-with-claude/mid-conversation-system-messages)가 모델에게 모드가 활성화되어 있음을 알리며, 여러 턴마다 한 줄짜리 리프레셔를 보내고 모드가 꺼질 때 종료 알림을 보냅니다. 최상위 `system` 필드는 절대 변경되지 않으므로 캐시된 프리픽스가 그대로 유지됩니다.
3. **도구 설명 내 상시 동의:** 오케스트레이션 도구의 설명에는 모드가 켜져 있는 동안 모델이 먼저 묻지 않고 모든 실질적인 작업에 대해 워크플로를 작성하고 실행해야 한다고 명시되어 있습니다.

<Note>
  이 예제는 대화 중 시스템 메시지를 사용합니다. 이를 지원하는 모델과 플랫폼에 대해서는 [대화 중 시스템 메시지](https://platform.claude.com/docs/ko/build-with-claude/mid-conversation-system-messages)를 참조하세요. 팬아웃 자체는 토큰 사용량을 배로 늘립니다. 단일 요청이 많은 서브에이전트 대화를 생성할 수 있으므로, 비용을 정당화할 수 있는 작업에만 이 모드를 사용하세요.
</Note>

## 루프 설정하기

예제는 단일 파일입니다. 상수들은 effort 수준, 팬아웃 형태, 그리고 모드 리프레셔가 얼마나 자주 재전송되는지를 제어합니다. `MAX_CONCURRENT`는 동시에 실행되는 서브에이전트 수를 제한합니다(PHP 포트는 순차적이며 이를 무시합니다). `MAX_TOTAL_SUBTASKS`는 모델이 단일 Workflow 호출에서 대기열에 넣을 수 있는 수를 제한합니다. 이 둘을 분리하면 모델이 대규모 백로그를 한꺼번에 실행하지 않고도 계획할 수 있습니다. `DOC_TEST_MODE` 검사는 해당 환경 변수가 설정되어 있을 때 루프를 단일 턴으로 제한하여, 자동화된 문서 하네스가 전체 오케스트레이션을 실행하지 않고도 파일이 컴파일되고 빠르게 완료되는지 검증할 수 있게 합니다. 예제를 직접 실행할 때는 설정하지 않은 상태로 두세요.

<CodeGroup exclude="shell:cURL, shell:CLI">
  ```python Python
  import atexit
  import concurrent.futures
  import hashlib
  import json
  import os
  import shutil
  import subprocess
  import sys
  import tempfile
  import threading

  import anthropic

  client = anthropic.Anthropic()

  MODEL = "claude-opus-5-5"
  EFFORT = "xhigh"

  SYSTEM_PROMPT = (
      "You are a helpful general-purpose agent. Answer the user's request directly."
  )

  REQUEST_TIMEOUT_SECONDS = 600
  BASH_TIMEOUT_SECONDS = 60
  TOOL_RESULT_MAX_CHARS = 8000
  MAX_CONCURRENT = 10
  DOC_TEST_MODE = bool(os.environ.get("DOC_TEST_MODE"))
  MAX_TOTAL_SUBTASKS = 2 if DOC_TEST_MODE else 200
  MAX_SUBAGENT_TURNS = 1 if DOC_TEST_MODE else 15
  MAX_MAIN_TURNS = 1 if DOC_TEST_MODE else 30
  TURNS_BETWEEN_REFRESHERS = 10
  JOURNAL_PATH = os.environ.get("ORCH_JOURNAL") or "orchestration_journal.json"
  ```

  ```typescript TypeScript
  import { exec } from "node:child_process";
  import { createHash } from "node:crypto";
  import { mkdtempDisposable, readFile, rename, writeFile } from "node:fs/promises";
  import { tmpdir } from "node:os";
  import { join } from "node:path";

  import Anthropic from "@anthropic-ai/sdk";

  const client = new Anthropic();

  const MODEL = "claude-opus-5-5";
  const EFFORT = "xhigh";

  const SYSTEM_PROMPT =
    "You are a helpful general-purpose agent. Answer the user's request directly.";

  const REQUEST_TIMEOUT_SECONDS = 600;
  const BASH_TIMEOUT_SECONDS = 60;
  const TOOL_RESULT_MAX_CHARS = 8000;
  const MAX_CONCURRENT = 10;
  const DOC_TEST_MODE = Boolean(process.env.DOC_TEST_MODE);
  const MAX_TOTAL_SUBTASKS = DOC_TEST_MODE ? 2 : 200;
  const MAX_SUBAGENT_TURNS = DOC_TEST_MODE ? 1 : 15;
  const MAX_MAIN_TURNS = DOC_TEST_MODE ? 1 : 30;
  const TURNS_BETWEEN_REFRESHERS = 10;
  const JOURNAL_PATH = process.env.ORCH_JOURNAL || "orchestration_journal.json";
  ```

  ```csharp C#
  using System.Diagnostics;
  using System.Security.Cryptography;
  using System.Text;
  using System.Text.Json;
  using Anthropic;
  using Anthropic.Models.Messages;

  AnthropicClient client = new();

  const Model model = Model.ClaudeOpus5_5;
  var effort = Effort.Xhigh;

  const string systemPrompt = "You are a helpful general-purpose agent. Answer the user's request directly.";

  const int requestTimeoutSeconds = 600;
  const int bashTimeoutSeconds = 60;
  const int toolResultMaxChars = 8000;
  const int maxConcurrent = 10;
  var docTestMode = Environment.GetEnvironmentVariable("DOC_TEST_MODE") is { Length: > 0 };
  var maxTotalSubtasks = docTestMode ? 2 : 200;
  var maxSubagentTurns = docTestMode ? 1 : 15;
  var maxMainTurns = docTestMode ? 1 : 30;
  const int turnsBetweenRefreshers = 10;
  var journalPath = Environment.GetEnvironmentVariable("ORCH_JOURNAL") is { Length: > 0 } configuredPath
      ? configuredPath
      : "orchestration_journal.json";
  ```

  ```go Go
  import (
  	"bytes"
  	"cmp"
  	"context"
  	"crypto/sha256"
  	"encoding/hex"
  	"encoding/json"
  	"errors"
  	"fmt"
  	"log"
  	"os"
  	"os/exec"
  	"path/filepath"
  	"strings"
  	"sync"
  	"time"

  	"github.com/anthropics/anthropic-sdk-go"
  )

  var client = anthropic.NewClient()

  const (
  	modelID = anthropic.ModelClaudeOpus5_5
  	effort  = anthropic.OutputConfigEffortXhigh

  	systemPrompt = "You are a helpful general-purpose agent. Answer the user's request directly."

  	requestTimeoutSeconds  = 600
  	bashTimeoutSeconds     = 60
  	toolResultMaxChars     = 8000
  	maxConcurrent          = 10
  	turnsBetweenRefreshers = 10
  )

  var (
  	docTestMode      = os.Getenv("DOC_TEST_MODE") != ""
  	maxTotalSubtasks = ifTest(2, 200)
  	maxSubagentTurns = ifTest(1, 15)
  	maxMainTurns     = ifTest(1, 30)
  	journalPath      = cmp.Or(os.Getenv("ORCH_JOURNAL"), "orchestration_journal.json")
  )

  func ifTest(test, normal int) int {
  	if docTestMode {
  		return test
  	}
  	return normal
  }
  ```

  ```java Java
  // compact source file에서는 java.base (java.util, java.nio.file, java.util.concurrent, ...)가
  // 암묵적으로 import되므로 SDK와 Jackson만 import하면 됩니다.
  import com.anthropic.client.AnthropicClient;
  import com.anthropic.client.okhttp.AnthropicOkHttpClient;
  import com.anthropic.core.JsonArray;
  import com.anthropic.core.JsonBoolean;
  import com.anthropic.core.JsonObject;
  import com.anthropic.core.JsonString;
  import com.anthropic.core.JsonValue;
  import com.anthropic.core.RequestOptions;
  import com.anthropic.helpers.MessageAccumulator;
  import com.anthropic.models.messages.ContentBlockParam;
  import com.anthropic.models.messages.Message;
  import com.anthropic.models.messages.MessageCreateParams;
  import com.anthropic.models.messages.MessageParam;
  import com.anthropic.models.messages.Model;
  import com.anthropic.models.messages.OutputConfig;
  import com.anthropic.models.messages.StopReason;
  import com.anthropic.models.messages.TextBlock;
  import com.anthropic.models.messages.Tool;
  import com.anthropic.models.messages.ToolBash20250124;
  import com.anthropic.models.messages.ToolResultBlockParam;
  import com.anthropic.models.messages.ToolUseBlock;
  import com.fasterxml.jackson.core.JsonProcessingException;
  import com.fasterxml.jackson.core.type.TypeReference;
  import com.fasterxml.jackson.databind.JsonNode;
  import com.fasterxml.jackson.databind.ObjectMapper;

  AnthropicClient client = AnthropicOkHttpClient.fromEnv();

  static final Model MODEL = Model.CLAUDE_OPUS_5_5;
  static final boolean DOC_TEST_MODE =
          !Objects.requireNonNullElse(System.getenv("DOC_TEST_MODE"), "").isEmpty();
  static final OutputConfig.Effort EFFORT = OutputConfig.Effort.XHIGH;

  static final String SYSTEM_PROMPT =
          "You are a helpful general-purpose agent. Answer the user's request directly.";

  static final int REQUEST_TIMEOUT_SECONDS = 600;
  static final RequestOptions REQUEST_OPTIONS =
          RequestOptions.builder().timeout(Duration.ofSeconds(REQUEST_TIMEOUT_SECONDS)).build();
  static final int BASH_TIMEOUT_SECONDS = 60;
  static final int TOOL_RESULT_MAX_CHARS = 8000;
  static final int MAX_CONCURRENT = 10;
  static final int MAX_TOTAL_SUBTASKS = DOC_TEST_MODE ? 2 : 200;
  static final int MAX_SUBAGENT_TURNS = DOC_TEST_MODE ? 1 : 15;
  static final int MAX_MAIN_TURNS = DOC_TEST_MODE ? 1 : 30;
  static final int TURNS_BETWEEN_REFRESHERS = 10;
  static final Path JOURNAL_PATH = Path.of(Optional.ofNullable(System.getenv("ORCH_JOURNAL"))
          .filter(path -> !path.isEmpty())
          .orElse("orchestration_journal.json"));
  ```

  ```php PHP
  use Anthropic\Client;
  use Anthropic\Messages\ToolUseBlock;

  $client = new Client();

  const MODEL = 'claude-opus-5-5';
  define('DOC_TEST_MODE', (string) getenv('DOC_TEST_MODE') !== '');
  const EFFORT = 'xhigh';

  const SYSTEM_PROMPT = 'You are a helpful general-purpose agent. Answer the user\'s request directly.';

  const REQUEST_TIMEOUT_SECONDS = 600;
  const BASH_TIMEOUT_SECONDS = 60;
  const TOOL_RESULT_MAX_CHARS = 8000;
  const MAX_CONCURRENT = 10;
  define('MAX_TOTAL_SUBTASKS', DOC_TEST_MODE ? 2 : 200);
  define('MAX_SUBAGENT_TURNS', DOC_TEST_MODE ? 1 : 15);
  define('MAX_MAIN_TURNS', DOC_TEST_MODE ? 1 : 30);
  const TURNS_BETWEEN_REFRESHERS = 10;
  define('JOURNAL_PATH', getenv('ORCH_JOURNAL') ?: 'orchestration_journal.json');
  ```

  ```ruby Ruby
  require "anthropic"
  require "digest"
  require "fileutils"
  require "json"
  require "open3"
  require "tmpdir"

  CLIENT = Anthropic::Client.new

  MODEL = "claude-opus-5-5"
  EFFORT = :xhigh

  SYSTEM_PROMPT = "You are a helpful general-purpose agent. Answer the user's request directly."

  REQUEST_TIMEOUT_SECONDS = 600
  BASH_TIMEOUT_SECONDS = 60
  TOOL_RESULT_MAX_CHARS = 8000
  MAX_CONCURRENT = 10
  DOC_TEST_MODE = !ENV["DOC_TEST_MODE"].to_s.empty?
  MAX_TOTAL_SUBTASKS = DOC_TEST_MODE ? 2 : 200
  MAX_SUBAGENT_TURNS = DOC_TEST_MODE ? 1 : 15
  MAX_MAIN_TURNS = DOC_TEST_MODE ? 1 : 30
  TURNS_BETWEEN_REFRESHERS = 10
  JOURNAL_PATH = ENV["ORCH_JOURNAL"].to_s.empty? ? "orchestration_journal.json" : ENV["ORCH_JOURNAL"]
  ```
</CodeGroup>

## 모드 리마인더 정의하기

리마인더는 의도적으로 짧습니다. 모드를 전환하고, 무거운 지침이 담겨 있는 도구 설명을 가리킵니다. 전체 텍스트는 모드가 켜질 때 한 번 전송되고, 리프레셔는 여러 사용자 턴이 지난 후에만 재전송되며, 종료 알림은 모드가 꺼질 때 한 번 전송됩니다.

<CodeGroup exclude="shell:cURL, shell:CLI">
  ```python Python
  MODE_ENTER = (
      "Orchestration mode is on: optimize for the most exhaustive, correct answer rather than "
      "the fastest one. Use the Workflow tool on every substantive task, sized to the problem's "
      "natural decomposition rather than the maximum the tool allows. See the Workflow tool's "
      "description for standing consent, granularity guidance, and quality patterns. Work solo "
      "only on conversational or trivial turns."
  )
  MODE_REFRESH = "Orchestration mode is still on. Use the Workflow tool; see its standing consent section."
  MODE_EXIT = (
      "Orchestration mode is off. The Workflow tool's standard opt-in rule applies again."
  )
  ```

  ```typescript TypeScript
  const MODE_ENTER =
    "Orchestration mode is on: optimize for the most exhaustive, correct answer rather than " +
    "the fastest one. Use the Workflow tool on every substantive task, sized to the problem's " +
    "natural decomposition rather than the maximum the tool allows. See the Workflow tool's " +
    "description for standing consent, granularity guidance, and quality patterns. Work solo " +
    "only on conversational or trivial turns.";
  const MODE_REFRESH =
    "Orchestration mode is still on. Use the Workflow tool; see its standing consent section.";
  const MODE_EXIT =
    "Orchestration mode is off. The Workflow tool's standard opt-in rule applies again.";
  ```

  ```csharp C#
  const string modeEnter =
      "Orchestration mode is on: optimize for the most exhaustive, correct answer rather than "
      + "the fastest one. Use the Workflow tool on every substantive task, sized to the problem's "
      + "natural decomposition rather than the maximum the tool allows. See the Workflow tool's "
      + "description for standing consent, granularity guidance, and quality patterns. Work solo "
      + "only on conversational or trivial turns.";
  const string modeRefresh =
      "Orchestration mode is still on. Use the Workflow tool; see its standing consent section.";
  const string modeExit =
      "Orchestration mode is off. The Workflow tool's standard opt-in rule applies again.";
  ```

  ```go Go
  const (
  	modeEnter = "Orchestration mode is on: optimize for the most exhaustive, correct answer rather than " +
  		"the fastest one. Use the Workflow tool on every substantive task, sized to the problem's " +
  		"natural decomposition rather than the maximum the tool allows. See the Workflow tool's " +
  		"description for standing consent, granularity guidance, and quality patterns. Work solo " +
  		"only on conversational or trivial turns."
  	modeRefresh = "Orchestration mode is still on. Use the Workflow tool; see its standing consent section."
  	modeExit    = "Orchestration mode is off. The Workflow tool's standard opt-in rule applies again."
  )
  ```

  ```java Java
  static final String MODE_ENTER = """
          Orchestration mode is on: optimize for the most exhaustive, correct answer rather than \
          the fastest one. Use the Workflow tool on every substantive task, sized to the problem's \
          natural decomposition rather than the maximum the tool allows. See the Workflow tool's \
          description for standing consent, granularity guidance, and quality patterns. Work solo \
          only on conversational or trivial turns.""";
  static final String MODE_REFRESH =
          "Orchestration mode is still on. Use the Workflow tool; see its standing consent section.";
  static final String MODE_EXIT =
          "Orchestration mode is off. The Workflow tool's standard opt-in rule applies again.";
  ```

  ```php PHP
  const MODE_ENTER =
      'Orchestration mode is on: optimize for the most exhaustive, correct answer rather than '
      . 'the fastest one. Use the Workflow tool on every substantive task, sized to the problem\'s '
      . 'natural decomposition rather than the maximum the tool allows. See the Workflow tool\'s '
      . 'description for standing consent, granularity guidance, and quality patterns. Work solo '
      . 'only on conversational or trivial turns.';
  const MODE_REFRESH =
      'Orchestration mode is still on. Use the Workflow tool; see its standing consent section.';
  const MODE_EXIT =
      'Orchestration mode is off. The Workflow tool\'s standard opt-in rule applies again.';
  ```

  ```ruby Ruby
  MODE_ENTER =
    "Orchestration mode is on: optimize for the most exhaustive, correct answer rather than " \
    "the fastest one. Use the Workflow tool on every substantive task, sized to the problem's " \
    "natural decomposition rather than the maximum the tool allows. See the Workflow tool's " \
    "description for standing consent, granularity guidance, and quality patterns. Work solo " \
    "only on conversational or trivial turns."
  MODE_REFRESH =
    "Orchestration mode is still on. Use the Workflow tool; see its standing consent section."
  MODE_EXIT =
    "Orchestration mode is off. The Workflow tool's standard opt-in rule applies again."
  ```
</CodeGroup>

## 도구 설명에서 상시 동의 부여하기

Workflow 도구는 실제 행동 계약을 담고 있습니다. 옵트인 규칙, 모드가 켜져 있는 동안 적용되는 상시 동의, 팬아웃 규모를 정하기 위한 세분화 지침, 그리고 모델이 활용할 수 있는 품질 패턴(검증 웨이브, 완전성 비평가, 다단계 시퀀싱)이 포함됩니다. 서브에이전트는 결과가 산문 대신 구조화된 JSON으로 돌아오도록 `report_findings` 도구도 받으며, bash 도구는 로컬에서 실행되는 Anthropic 정의 `bash_20250124` 도구입니다.

<CodeGroup exclude="shell:cURL, shell:CLI">
  ```python Python
  WORKFLOW_TOOL = {
      "name": "Workflow",
      "description": (
          "Orchestrate a multiagent workflow: split a large task into independent subtasks "
          "and run them as parallel agents, then collect their results.\n\n"
          "Opt-in: only use this tool when the user explicitly asks for a workflow, or when a "
          "system message confirms that orchestration mode is on.\n\n"
          "Quality patterns: adversarial verification (a second wave of agents checks the first "
          "wave's findings against the source), a completeness critic (one agent hunts for what "
          "the others missed), and multiphase sequencing (understand, design, implement, and "
          "review as separate workflow calls, reading results between phases). A useful default "
          "is hybrid: scout inline first to discover the work-list, then fan out over it.\n\n"
          "Granularity: scope each subtask to a distinct concern, component, or question rather "
          "than per line or per file section. Scale the count to what the user asked for: a "
          "focused review of a module of a few hundred lines rarely needs more than about ten "
          "subtasks; a broad audit of a large codebase can justify more.\n\n"
          "Standing consent: while a system message confirms orchestration mode is on, that "
          "opt-in is standing. Author and run a workflow for every substantive task by default, "
          "and lean toward verifying findings adversarially. Work solo only on conversational "
          "turns or trivial mechanical edits. When a system message says the mode is off, "
          "revert to the opt-in rule above."
      ),
      "input_schema": {
          "type": "object",
          "properties": {
              "subtasks": {
                  "type": "array",
                  "items": {"type": "string"},
                  "description": "Independent subtask prompts to run as parallel agents",
              }
          },
          "required": ["subtasks"],
      },
  }

  BASH_TOOL = {"type": "bash_20250124", "name": "bash"}

  REPORT_TOOL = {
      "name": "report_findings",
      "description": (
          "Report the final findings for your subtask. Call this exactly once, when you are "
          "done investigating; it ends your task."
      ),
      "input_schema": {
          "type": "object",
          "properties": {
              "summary": {
                  "type": "string",
                  "description": "Two or three sentences of synthesis",
              },
              "findings": {
                  "type": "array",
                  "items": {
                      "type": "object",
                      "properties": {
                          "claim": {
                              "type": "string",
                              "description": "The finding, one sentence",
                          },
                          "evidence": {
                              "type": "string",
                              "description": "How it was verified (file, line, or command output)",
                          },
                          "severity": {
                              "type": "string",
                              "enum": ["high", "medium", "low", "info"],
                          },
                      },
                      "required": ["claim", "evidence", "severity"],
                  },
              },
          },
          "required": ["summary", "findings"],
      },
  }
  ```

  ```typescript TypeScript
  const WORKFLOW_TOOL: Anthropic.Tool = {
    name: "Workflow",
    description: [
      "Orchestrate a multiagent workflow: split a large task into independent subtasks " +
        "and run them as parallel agents, then collect their results.",
      "Opt-in: only use this tool when the user explicitly asks for a workflow, or when a " +
        "system message confirms that orchestration mode is on.",
      "Quality patterns: adversarial verification (a second wave of agents checks the first " +
        "wave's findings against the source), a completeness critic (one agent hunts for what " +
        "the others missed), and multiphase sequencing (understand, design, implement, and " +
        "review as separate workflow calls, reading results between phases). A useful default " +
        "is hybrid: scout inline first to discover the work-list, then fan out over it.",
      "Granularity: scope each subtask to a distinct concern, component, or question rather " +
        "than per line or per file section. Scale the count to what the user asked for: a " +
        "focused review of a module of a few hundred lines rarely needs more than about ten " +
        "subtasks; a broad audit of a large codebase can justify more.",
      "Standing consent: while a system message confirms orchestration mode is on, that " +
        "opt-in is standing. Author and run a workflow for every substantive task by default, " +
        "and lean toward verifying findings adversarially. Work solo only on conversational " +
        "turns or trivial mechanical edits. When a system message says the mode is off, " +
        "revert to the opt-in rule above."
    ].join("\n\n"),
    input_schema: {
      type: "object",
      properties: {
        subtasks: {
          type: "array",
          items: { type: "string" },
          description: "Independent subtask prompts to run as parallel agents"
        }
      },
      required: ["subtasks"]
    }
  };

  const BASH_TOOL: Anthropic.ToolBash20250124 = { type: "bash_20250124", name: "bash" };

  const REPORT_TOOL: Anthropic.Tool = {
    name: "report_findings",
    description:
      "Report the final findings for your subtask. Call this exactly once, when you are " +
      "done investigating; it ends your task.",
    input_schema: {
      type: "object",
      properties: {
        summary: { type: "string", description: "Two or three sentences of synthesis" },
        findings: {
          type: "array",
          items: {
            type: "object",
            properties: {
              claim: { type: "string", description: "The finding, one sentence" },
              evidence: {
                type: "string",
                description: "How it was verified (file, line, or command output)"
              },
              severity: { type: "string", enum: ["high", "medium", "low", "info"] }
            },
            required: ["claim", "evidence", "severity"]
          }
        }
      },
      required: ["summary", "findings"]
    }
  };
  ```

  ```csharp C#
  Tool workflowTool = new()
  {
      Name = "Workflow",
      Description = """
          Orchestrate a multiagent workflow: split a large task into independent subtasks and run them as parallel agents, then collect their results.

          Opt-in: only use this tool when the user explicitly asks for a workflow, or when a system message confirms that orchestration mode is on.

          Quality patterns: adversarial verification (a second wave of agents checks the first wave's findings against the source), a completeness critic (one agent hunts for what the others missed), and multiphase sequencing (understand, design, implement, and review as separate workflow calls, reading results between phases). A useful default is hybrid: scout inline first to discover the work-list, then fan out over it.

          Granularity: scope each subtask to a distinct concern, component, or question rather than per line or per file section. Scale the count to what the user asked for: a focused review of a module of a few hundred lines rarely needs more than about ten subtasks; a broad audit of a large codebase can justify more.

          Standing consent: while a system message confirms orchestration mode is on, that opt-in is standing. Author and run a workflow for every substantive task by default, and lean toward verifying findings adversarially. Work solo only on conversational turns or trivial mechanical edits. When a system message says the mode is off, revert to the opt-in rule above.
          """,
      InputSchema = new()
      {
          Properties = new Dictionary<string, JsonElement>
          {
              ["subtasks"] = JsonSerializer.SerializeToElement(new
              {
                  type = "array",
                  items = new { type = "string" },
                  description = "Independent subtask prompts to run as parallel agents",
              }),
          },
          Required = ["subtasks"],
      },
  };

  ToolBash20250124 bashTool = new();

  Tool reportTool = new()
  {
      Name = "report_findings",
      Description =
          "Report the final findings for your subtask. Call this exactly once, when you are "
          + "done investigating; it ends your task.",
      InputSchema = new()
      {
          Properties = new Dictionary<string, JsonElement>
          {
              ["summary"] = JsonSerializer.SerializeToElement(new
              {
                  type = "string",
                  description = "Two or three sentences of synthesis",
              }),
              ["findings"] = JsonSerializer.SerializeToElement(new
              {
                  type = "array",
                  items = new
                  {
                      type = "object",
                      properties = new
                      {
                          claim = new { type = "string", description = "The finding, one sentence" },
                          evidence = new
                          {
                              type = "string",
                              description = "How it was verified (file, line, or command output)",
                          },
                          severity = new { type = "string", @enum = new[] { "high", "medium", "low", "info" } },
                      },
                      required = new[] { "claim", "evidence", "severity" },
                  },
              }),
          },
          Required = ["summary", "findings"],
      },
  };
  ```

  ```go Go
  var workflowTool = anthropic.ToolUnionParam{
  	OfTool: &anthropic.ToolParam{
  		Name: "Workflow",
  		Description: anthropic.String("Orchestrate a multiagent workflow: split a large task into independent subtasks " +
  			"and run them as parallel agents, then collect their results.\n\n" +
  			"Opt-in: only use this tool when the user explicitly asks for a workflow, or when a " +
  			"system message confirms that orchestration mode is on.\n\n" +
  			"Quality patterns: adversarial verification (a second wave of agents checks the first " +
  			"wave's findings against the source), a completeness critic (one agent hunts for what " +
  			"the others missed), and multiphase sequencing (understand, design, implement, and " +
  			"review as separate workflow calls, reading results between phases). A useful default " +
  			"is hybrid: scout inline first to discover the work-list, then fan out over it.\n\n" +
  			"Granularity: scope each subtask to a distinct concern, component, or question rather " +
  			"than per line or per file section. Scale the count to what the user asked for: a " +
  			"focused review of a module of a few hundred lines rarely needs more than about ten " +
  			"subtasks; a broad audit of a large codebase can justify more.\n\n" +
  			"Standing consent: while a system message confirms orchestration mode is on, that " +
  			"opt-in is standing. Author and run a workflow for every substantive task by default, " +
  			"and lean toward verifying findings adversarially. Work solo only on conversational " +
  			"turns or trivial mechanical edits. When a system message says the mode is off, " +
  			"revert to the opt-in rule above."),
  		InputSchema: anthropic.ToolInputSchemaParam{
  			Properties: map[string]any{
  				"subtasks": map[string]any{
  					"type":        "array",
  					"items":       map[string]any{"type": "string"},
  					"description": "Independent subtask prompts to run as parallel agents",
  				},
  			},
  			Required: []string{"subtasks"},
  		},
  	},
  }

  var bashTool = anthropic.ToolUnionParam{
  	OfBashTool20250124: &anthropic.ToolBash20250124Param{},
  }

  var reportTool = anthropic.ToolUnionParam{
  	OfTool: &anthropic.ToolParam{
  		Name: "report_findings",
  		Description: anthropic.String("Report the final findings for your subtask. Call this exactly once, when you are " +
  			"done investigating; it ends your task."),
  		InputSchema: anthropic.ToolInputSchemaParam{
  			Properties: map[string]any{
  				"summary": map[string]any{"type": "string", "description": "Two or three sentences of synthesis"},
  				"findings": map[string]any{
  					"type": "array",
  					"items": map[string]any{
  						"type": "object",
  						"properties": map[string]any{
  							"claim": map[string]any{"type": "string", "description": "The finding, one sentence"},
  							"evidence": map[string]any{
  								"type":        "string",
  								"description": "How it was verified (file, line, or command output)",
  							},
  							"severity": map[string]any{"type": "string", "enum": []string{"high", "medium", "low", "info"}},
  						},
  						"required": []string{"claim", "evidence", "severity"},
  					},
  				},
  			},
  			Required: []string{"summary", "findings"},
  		},
  	},
  }
  ```

  ```java Java
  static final Tool WORKFLOW_TOOL = Tool.builder()
          .name("Workflow")
          .description("""
                  Orchestrate a multiagent workflow: split a large task into independent subtasks \
                  and run them as parallel agents, then collect their results.

                  Opt-in: only use this tool when the user explicitly asks for a workflow, or when a \
                  system message confirms that orchestration mode is on.

                  Quality patterns: adversarial verification (a second wave of agents checks the first \
                  wave's findings against the source), a completeness critic (one agent hunts for what \
                  the others missed), and multiphase sequencing (understand, design, implement, and \
                  review as separate workflow calls, reading results between phases). A useful default \
                  is hybrid: scout inline first to discover the work-list, then fan out over it.

                  Granularity: scope each subtask to a distinct concern, component, or question rather \
                  than per line or per file section. Scale the count to what the user asked for: a \
                  focused review of a module of a few hundred lines rarely needs more than about ten \
                  subtasks; a broad audit of a large codebase can justify more.

                  Standing consent: while a system message confirms orchestration mode is on, that \
                  opt-in is standing. Author and run a workflow for every substantive task by default, \
                  and lean toward verifying findings adversarially. Work solo only on conversational \
                  turns or trivial mechanical edits. When a system message says the mode is off, \
                  revert to the opt-in rule above.""")
          .inputSchema(Tool.InputSchema.builder()
                  .properties(Tool.InputSchema.Properties.builder()
                          .putAdditionalProperty("subtasks", JsonValue.from(Map.of(
                                  "type", "array",
                                  "items", Map.of("type", "string"),
                                  "description", "Independent subtask prompts to run as parallel agents")))
                          .build())
                  .putAdditionalProperty("required", JsonValue.from(List.of("subtasks")))
                  .build())
          .build();

  static final ToolBash20250124 BASH_TOOL = ToolBash20250124.builder().build();

  static final Tool REPORT_TOOL = Tool.builder()
          .name("report_findings")
          .description("""
                  Report the final findings for your subtask. Call this exactly once, when you are \
                  done investigating; it ends your task.""")
          .inputSchema(Tool.InputSchema.builder()
                  .properties(Tool.InputSchema.Properties.builder()
                          .putAdditionalProperty("summary", JsonValue.from(Map.of(
                                  "type", "string",
                                  "description", "Two or three sentences of synthesis")))
                          .putAdditionalProperty("findings", JsonValue.from(Map.of(
                                  "type", "array",
                                  "items", Map.of(
                                          "type", "object",
                                          "properties", Map.of(
                                                  "claim", Map.of(
                                                          "type", "string",
                                                          "description", "The finding, one sentence"),
                                                  "evidence", Map.of(
                                                          "type", "string",
                                                          "description", "How it was verified (file, line, or command output)"),
                                                  "severity", Map.of(
                                                          "type", "string",
                                                          "enum", List.of("high", "medium", "low", "info"))),
                                          "required", List.of("claim", "evidence", "severity")))))
                          .build())
                  .putAdditionalProperty("required", JsonValue.from(List.of("summary", "findings")))
                  .build())
          .build();
  ```

  ```php PHP
  const WORKFLOW_TOOL = [
      'name' => 'Workflow',
      'description' =>
          'Orchestrate a multiagent workflow: split a large task into independent subtasks '
          . "and run them as parallel agents, then collect their results.\n\n"
          . 'Opt-in: only use this tool when the user explicitly asks for a workflow, or when a '
          . "system message confirms that orchestration mode is on.\n\n"
          . 'Quality patterns: adversarial verification (a second wave of agents checks the first '
          . 'wave\'s findings against the source), a completeness critic (one agent hunts for what '
          . 'the others missed), and multiphase sequencing (understand, design, implement, and '
          . 'review as separate workflow calls, reading results between phases). A useful default '
          . "is hybrid: scout inline first to discover the work-list, then fan out over it.\n\n"
          . 'Granularity: scope each subtask to a distinct concern, component, or question rather '
          . 'than per line or per file section. Scale the count to what the user asked for: a '
          . 'focused review of a module of a few hundred lines rarely needs more than about ten '
          . "subtasks; a broad audit of a large codebase can justify more.\n\n"
          . 'Standing consent: while a system message confirms orchestration mode is on, that '
          . 'opt-in is standing. Author and run a workflow for every substantive task by default, '
          . 'and lean toward verifying findings adversarially. Work solo only on conversational '
          . 'turns or trivial mechanical edits. When a system message says the mode is off, '
          . 'revert to the opt-in rule above.',
      'input_schema' => [
          'type' => 'object',
          'properties' => [
              'subtasks' => [
                  'type' => 'array',
                  'items' => ['type' => 'string'],
                  'description' => 'Independent subtask prompts to run as parallel agents',
              ],
          ],
          'required' => ['subtasks'],
      ],
  ];

  const BASH_TOOL = ['type' => 'bash_20250124', 'name' => 'bash'];

  const REPORT_TOOL = [
      'name' => 'report_findings',
      'description' =>
          'Report the final findings for your subtask. Call this exactly once, when you are '
          . 'done investigating; it ends your task.',
      'input_schema' => [
          'type' => 'object',
          'properties' => [
              'summary' => ['type' => 'string', 'description' => 'Two or three sentences of synthesis'],
              'findings' => [
                  'type' => 'array',
                  'items' => [
                      'type' => 'object',
                      'properties' => [
                          'claim' => ['type' => 'string', 'description' => 'The finding, one sentence'],
                          'evidence' => [
                              'type' => 'string',
                              'description' => 'How it was verified (file, line, or command output)',
                          ],
                          'severity' => ['type' => 'string', 'enum' => ['high', 'medium', 'low', 'info']],
                      ],
                      'required' => ['claim', 'evidence', 'severity'],
                  ],
              ],
          ],
          'required' => ['summary', 'findings'],
      ],
  ];
  ```

  ```ruby Ruby
  WORKFLOW_TOOL = {
    name: "Workflow",
    description:
      "Orchestrate a multiagent workflow: split a large task into independent subtasks " \
      "and run them as parallel agents, then collect their results.\n\n" \
      "Opt-in: only use this tool when the user explicitly asks for a workflow, or when a " \
      "system message confirms that orchestration mode is on.\n\n" \
      "Quality patterns: adversarial verification (a second wave of agents checks the first " \
      "wave's findings against the source), a completeness critic (one agent hunts for what " \
      "the others missed), and multiphase sequencing (understand, design, implement, and " \
      "review as separate workflow calls, reading results between phases). A useful default " \
      "is hybrid: scout inline first to discover the work-list, then fan out over it.\n\n" \
      "Granularity: scope each subtask to a distinct concern, component, or question rather " \
      "than per line or per file section. Scale the count to what the user asked for: a " \
      "focused review of a module of a few hundred lines rarely needs more than about ten " \
      "subtasks; a broad audit of a large codebase can justify more.\n\n" \
      "Standing consent: while a system message confirms orchestration mode is on, that " \
      "opt-in is standing. Author and run a workflow for every substantive task by default, " \
      "and lean toward verifying findings adversarially. Work solo only on conversational " \
      "turns or trivial mechanical edits. When a system message says the mode is off, " \
      "revert to the opt-in rule above.",
    input_schema: {
      type: "object",
      properties: {
        subtasks: {
          type: "array",
          items: {type: "string"},
          description: "Independent subtask prompts to run as parallel agents"
        }
      },
      required: ["subtasks"]
    }
  }.freeze

  BASH_TOOL = {type: "bash_20250124", name: "bash"}.freeze

  REPORT_TOOL = {
    name: "report_findings",
    description:
      "Report the final findings for your subtask. Call this exactly once, when you are " \
      "done investigating; it ends your task.",
    input_schema: {
      type: "object",
      properties: {
        summary: {type: "string", description: "Two or three sentences of synthesis"},
        findings: {
          type: "array",
          items: {
            type: "object",
            properties: {
              claim: {type: "string", description: "The finding, one sentence"},
              evidence: {
                type: "string",
                description: "How it was verified (file, line, or command output)"
              },
              severity: {type: "string", enum: ["high", "medium", "low", "info"]}
            },
            required: ["claim", "evidence", "severity"]
          }
        }
      },
      required: ["summary", "findings"]
    }
  }.freeze
  ```
</CodeGroup>

## bash 도구를 로컬에서 실행하기

bash 핸들러는 요청된 명령을 타임아웃과 함께 실행하고, stdout과 stderr를 합쳐 캡처하며, 통제 불능 명령이 "context window"(컨텍스트 윈도우)를 넘치게 하지 못하도록 결과를 잘라냅니다. 명령은 예제를 실행한 디렉터리에서 실행되므로, 특정 프로젝트를 대상으로 하려면 그곳에서 시작해야 합니다. `DOC_TEST_MODE`가 설정되어 있으면 하네스는 대신 종료 시 제거되는 작은 임시 픽스처 디렉터리를 bash에 제공합니다. 여기에는 샌드박스가 없습니다. 명령은 예제를 실행한 프로세스의 권한으로 실행됩니다. 명확성을 위해 이 예제는 `bash_20250124` 계약이 설명하는 지속 세션을 유지하는 대신 각 호출을 새로운 서브셸에서 실행합니다. 프로덕션 에이전트는 작업 디렉터리, 환경, 그리고 `restart` 액션이 문서화된 대로 동작하도록 장기 실행 셸로 도구를 뒷받침해야 합니다.

<CodeGroup exclude="shell:cURL, shell:CLI">
  ```python Python
  # 예제가 실행된 위치에서 bash를 실행합니다. DOC_TEST_MODE에서는 문서 테스트 하네스가
  # 대신 종료 시 삭제되는 임시 픽스처 디렉터리를 가리키도록 합니다.
  if DOC_TEST_MODE:
      WORK_DIR = tempfile.mkdtemp(prefix="orchestration-")
      atexit.register(shutil.rmtree, WORK_DIR, ignore_errors=True)
      with open(os.path.join(WORK_DIR, "sample.py"), "w") as fixture:
          fixture.write(
              "def fib(n):\n"
              "    return n if n < 2 else fib(n - 1) + fib(n - 2)\n\n"
              "print(fib(10))\n"
          )
  else:
      WORK_DIR = os.getcwd()


  def run_bash(command: str) -> tuple[str, bool]:
      """Run a shell command and return (output, is_error). No sandbox: example code only."""
      print(f"[bash] {command}", file=sys.stderr)
      try:
          proc = subprocess.run(
              ["bash", "-c", command],
              cwd=WORK_DIR,
              capture_output=True,
              text=True,
              errors="replace",
              timeout=BASH_TIMEOUT_SECONDS,
          )
      except subprocess.TimeoutExpired:
          return f"command timed out after {BASH_TIMEOUT_SECONDS}s", True
      output = (proc.stdout + proc.stderr).strip() or "(no output)"
      if len(output) > TOOL_RESULT_MAX_CHARS:
          output = (
              output[:TOOL_RESULT_MAX_CHARS]
              + f"\n(truncated at {TOOL_RESULT_MAX_CHARS} chars)"
          )
      if proc.returncode != 0:
          output = f"(exit code {proc.returncode})\n{output}"
      return output, proc.returncode != 0


  def handle_bash_block(block) -> tuple[str, bool]:
      if block.input.get("restart") is True:
          return "Shell restarted.", False
      command = block.input.get("command")
      if not isinstance(command, str) or not command:
          return "bash error: no command was provided.", True
      return run_bash(command)
  ```

  ```typescript TypeScript
  type ToolOutcome = { output: string; isError: boolean };

  // 예제가 실행된 위치에서 bash를 실행합니다. DOC_TEST_MODE에서는 문서 테스트 하니스가
  // 대신 임시 fixture 디렉터리를 가리키며, 프로그램 종료 시 삭제됩니다.
  await using fixtureDir = DOC_TEST_MODE
    ? await mkdtempDisposable(join(tmpdir(), "orchestration-"))
    : null;
  const WORK_DIR = fixtureDir?.path ?? process.cwd();
  if (fixtureDir) {
    await writeFile(
      join(WORK_DIR, "sample.py"),
      `def fib(n):
      return n if n < 2 else fib(n - 1) + fib(n - 2)

  print(fib(10))
  `
    );
  }

  // 셸 명령을 실행하고 출력을 반환합니다. 샌드박스 없음: 예제 코드 전용입니다.
  function runBash(command: string): Promise<ToolOutcome> {
    console.error(`[bash] ${command}`);
    const options = {
      shell: "/bin/bash",
      cwd: WORK_DIR,
      timeout: BASH_TIMEOUT_SECONDS * 1000,
      maxBuffer: 16 * 1024 * 1024
    };
    return new Promise((resolve) => {
      exec(command, options, (error, stdout, stderr) => {
        if (error?.killed) {
          resolve({ output: `command timed out after ${BASH_TIMEOUT_SECONDS}s`, isError: true });
          return;
        }
        const exitCode = error === null ? 0 : typeof error.code === "number" ? error.code : 1;
        let output = (stdout + stderr).trim() || "(no output)";
        const codePoints = [...output];
        if (codePoints.length > TOOL_RESULT_MAX_CHARS) {
          const kept = codePoints.slice(0, TOOL_RESULT_MAX_CHARS).join("");
          output = `${kept}\n(truncated at ${TOOL_RESULT_MAX_CHARS} chars)`;
        }
        if (exitCode !== 0) {
          output = `(exit code ${exitCode})\n${output}`;
        }
        resolve({ output, isError: exitCode !== 0 });
      });
    });
  }

  async function handleBashBlock(block: Anthropic.ToolUseBlock): Promise<ToolOutcome> {
    const { command, restart } = block.input as { command?: string; restart?: boolean };
    if (restart === true) {
      return { output: "Shell restarted.", isError: false };
    }
    if (!command) {
      return { output: "bash error: no command was provided.", isError: true };
    }
    return runBash(command);
  }
  ```

  ```csharp C#
  // 예제가 실행된 위치에서 bash를 실행합니다. DOC_TEST_MODE에서는 문서 하네스가
  // 대신 일회용 픽스처 디렉터리를 가리키며, 종료 시 제거됩니다.
  var workDir = Environment.CurrentDirectory;
  if (docTestMode)
  {
      workDir = Directory.CreateTempSubdirectory("orchestration-").FullName;
      await File.WriteAllTextAsync(Path.Combine(workDir, "sample.py"), """
          def fib(n):
              return n if n < 2 else fib(n - 1) + fib(n - 2)

          print(fib(10))

          """);
      var fixtureDir = workDir;
      AppDomain.CurrentDomain.ProcessExit += (_, _) =>
      {
          try { Directory.Delete(fixtureDir, recursive: true); }
          catch { /* Best-effort cleanup; the OS tmp sweeper handles leftovers. */ }
      };
  }

  // 셸 명령을 실행하고 출력과 오류 플래그를 반환합니다. 샌드박스 없음: 예제 코드 전용.
  async Task<(string Output, bool IsError)> RunBash(string command)
  {
      Console.Error.WriteLine($"[bash] {command}");
      using var process = Process.Start(new ProcessStartInfo("bash")
      {
          ArgumentList = { "-c", command },
          WorkingDirectory = workDir,
          RedirectStandardOutput = true,
          RedirectStandardError = true,
      });
      if (process is null)
      {
          return ("bash error: the shell process failed to start.", true);
      }
      var stdoutTask = process.StandardOutput.ReadToEndAsync();
      var stderrTask = process.StandardError.ReadToEndAsync();
      using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(bashTimeoutSeconds));
      try
      {
          await process.WaitForExitAsync(timeout.Token);
      }
      catch (OperationCanceledException)
      {
          process.Kill(entireProcessTree: true);
          // 프로세스가 dispose되기 전에 리더 작업이 완료되도록 합니다.
          try
          {
              await Task.WhenAll(stdoutTask, stderrTask);
          }
          catch
          {
              // 타임아웃 시 출력은 버려지므로 리더 실패도 무시됩니다.
          }
          return ($"command timed out after {bashTimeoutSeconds}s", true);
      }
      var output = (await stdoutTask + await stderrTask).Trim();
      if (output.Length == 0)
      {
          output = "(no output)";
      }
      if (output.Length > toolResultMaxChars)
      {
          output = output[..toolResultMaxChars] + $"\n(truncated at {toolResultMaxChars} chars)";
      }
      if (process.ExitCode != 0)
      {
          output = $"(exit code {process.ExitCode})\n{output}";
      }
      return (output, process.ExitCode != 0);
  }

  // 모델이 요청한 bash 도구 호출 하나를 실행합니다.
  async Task<(string Output, bool IsError)> HandleBashBlock(ToolUseBlock block)
  {
      if (block.Input.GetValueOrDefault("restart").ValueKind is JsonValueKind.True)
      {
          return ("Shell restarted.", false);
      }
      return block.Input.GetValueOrDefault("command") is { ValueKind: JsonValueKind.String } command
          && command.GetString() is { Length: > 0 } commandText
          ? await RunBash(commandText)
          : ("bash error: no command was provided.", true);
  }
  ```

  ```go Go
  // 예제가 실행된 위치에서 bash를 실행합니다. DOC_TEST_MODE에서는 문서 하네스가
  // 대신 종료 시 제거되는 일회용 픽스처 디렉터리를 가리킵니다.
  var workDir = func() string {
  	if !docTestMode {
  		dir, err := os.Getwd()
  		if err != nil {
  			log.Fatal(err)
  		}
  		return dir
  	}
  	dir, err := os.MkdirTemp("", "orchestration-")
  	if err != nil {
  		log.Fatal(err)
  	}
  	const fixture = `def fib(n):
      return n if n < 2 else fib(n - 1) + fib(n - 2)

  print(fib(10))
  `
  	if err := os.WriteFile(filepath.Join(dir, "sample.py"), []byte(fixture), 0o644); err != nil {
  		log.Fatal(err)
  	}
  	return dir
  }()

  // runBash는 셸 명령을 실행하고 출력과 오류 플래그를 반환합니다.
  // 샌드박스 없음: 예제 코드 전용입니다.
  func runBash(ctx context.Context, command string) (string, bool) {
  	fmt.Fprintf(os.Stderr, "[bash] %s\n", command)
  	ctx, cancel := context.WithTimeout(ctx, bashTimeoutSeconds*time.Second)
  	defer cancel()
  	cmd := exec.CommandContext(ctx, "bash", "-c", command)
  	cmd.Dir = workDir
  	combined, err := cmd.CombinedOutput()
  	if errors.Is(ctx.Err(), context.DeadlineExceeded) {
  		return fmt.Sprintf("command timed out after %ds", bashTimeoutSeconds), true
  	}
  	output := strings.TrimSpace(string(combined))
  	if output == "" {
  		output = "(no output)"
  	}
  	if runes := []rune(output); len(runes) > toolResultMaxChars {
  		output = string(runes[:toolResultMaxChars]) + fmt.Sprintf("\n(truncated at %d chars)", toolResultMaxChars)
  	}
  	if err == nil {
  		return output, false
  	}
  	if exitErr, ok := errors.AsType[*exec.ExitError](err); ok {
  		return fmt.Sprintf("(exit code %d)\n%s", exitErr.ExitCode(), output), true
  	}
  	return fmt.Sprintf("(%s)\n%s", err, output), true
  }

  // handleBashBlock은 모델이 요청한 bash 도구 호출 하나를 실행합니다.
  func handleBashBlock(ctx context.Context, block anthropic.ToolUseBlock) (string, bool) {
  	var input struct {
  		Command string `json:"command"`
  		Restart bool   `json:"restart"`
  	}
  	if err := json.Unmarshal(block.Input, &input); err != nil {
  		return fmt.Sprintf("bash error: could not parse input: %s", err), true
  	}
  	if input.Restart {
  		return "Shell restarted.", false
  	}
  	if input.Command == "" {
  		return "bash error: no command was provided.", true
  	}
  	return runBash(ctx, input.Command)
  }
  ```

  ```java Java
  record ToolOutput(String output, boolean isError) {}

  // 예제가 실행된 위치에서 bash를 실행합니다. DOC_TEST_MODE에서는 문서 하네스가
  // 대신 종료 시 제거되는 일회용 픽스처 디렉터리를 가리킵니다.
  static final Path WORK_DIR = createWorkDir();

  static Path createWorkDir() {
      if (!DOC_TEST_MODE) {
          return Path.of(System.getProperty("user.dir"));
      }
      try {
          var dir = Files.createTempDirectory("orchestration-");
          Files.writeString(dir.resolve("sample.py"), """
                  def fib(n):
                      return n if n < 2 else fib(n - 1) + fib(n - 2)

                  print(fib(10))
                  """);
          Runtime.getRuntime().addShutdownHook(new Thread(() -> {
              // 최선의 노력으로 정리합니다. 남은 파일은 OS의 tmp 정리기가 처리합니다.
              try (var paths = Files.walk(dir)) {
                  paths.sorted(Comparator.reverseOrder()).forEach(path -> {
                      try { Files.deleteIfExists(path); } catch (IOException _) {}
                  });
              } catch (IOException _) {}
          }));
          return dir;
      } catch (IOException error) {
          throw new UncheckedIOException(error);
      }
  }

  // 셸 명령을 실행하고 출력과 오류 플래그를 반환합니다. 샌드박스 없음: 예제 코드 전용.
  ToolOutput runBash(String command) throws InterruptedException {
      System.err.println("[bash] " + command);
      Process process;
      try {
          process = new ProcessBuilder("bash", "-c", command)
                  .directory(WORK_DIR.toFile())
                  .redirectErrorStream(true)
                  .start();
      } catch (IOException error) {
          return new ToolOutput("(" + error + ")", true);
      }
      // 다른 스레드에서 stdout을 비워 파이프가 가득 차도 아래 타임아웃 대기가 멈추지 않게 합니다.
      CompletableFuture<String> outputReader = CompletableFuture.supplyAsync(() -> {
          try (var stdout = process.getInputStream()) {
              return new String(stdout.readAllBytes(), StandardCharsets.UTF_8);
          } catch (IOException _) {
              return "";
          }
      });
      if (!process.waitFor(BASH_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
          process.destroyForcibly();
          outputReader.cancel(true);
          return new ToolOutput("command timed out after " + BASH_TIMEOUT_SECONDS + "s", true);
      }
      String output = outputReader.join().strip();
      if (output.isEmpty()) {
          output = "(no output)";
      } else if (output.length() > TOOL_RESULT_MAX_CHARS) {
          output = output.substring(0, TOOL_RESULT_MAX_CHARS)
                  + "\n(truncated at " + TOOL_RESULT_MAX_CHARS + " chars)";
      }
      int exitCode = process.exitValue();
      return exitCode == 0
              ? new ToolOutput(output, false)
              : new ToolOutput("(exit code " + exitCode + ")\n" + output, true);
  }

  // 도구 호출의 입력 필드를 반환하며, 모델이 객체가 아닌 것을 보낸 경우 빈 맵을 반환합니다.
  Map<String, JsonValue> toolInput(ToolUseBlock toolUse) {
      return toolUse._input() instanceof JsonObject object ? object.values() : Map.of();
  }

  // 모델이 요청한 bash 도구 호출 하나를 실행합니다.
  ToolOutput handleBashBlock(ToolUseBlock toolUse) throws InterruptedException {
      Map<String, JsonValue> input = toolInput(toolUse);
      if (input.get("restart") instanceof JsonBoolean restart && restart.value()) {
          return new ToolOutput("Shell restarted.", false);
      }
      String command = input.get("command") instanceof JsonString text ? text.value() : "";
      if (command.isEmpty()) {
          return new ToolOutput("bash error: no command was provided.", true);
      }
      return runBash(command);
  }
  ```

  ```php PHP
  // 예제가 실행된 위치에서 bash를 실행합니다. DOC_TEST_MODE에서는 문서 하네스가
  // 대신 종료 시 제거되는 일회용 픽스처 디렉터리를 가리키도록 합니다.
  if (DOC_TEST_MODE) {
      $workDir = sys_get_temp_dir() . '/orchestration-' . bin2hex(random_bytes(8));
      if (!mkdir($workDir, 0700)) {
          throw new \RuntimeException("could not create working directory {$workDir}");
      }
      file_put_contents(
          $workDir . '/sample.py',
          "def fib(n):\n"
          . "    return n if n < 2 else fib(n - 1) + fib(n - 2)\n\n"
          . "print(fib(10))\n",
      );
      register_shutdown_function(function () use ($workDir): void {
          foreach (glob($workDir . '/*') ?: [] as $entry) {
              @unlink($entry);
          }
          @rmdir($workDir);
      });
  } else {
      $workDir = getcwd() ?: '.';
  }
  define('WORK_DIR', $workDir);

  /**
   * Run a shell command and return [output, isError]. The coreutils timeout command
   * enforces the time limit. No sandbox: example code only.
   */
  function runBash(string $command): array
  {
      fwrite(STDERR, "[bash] {$command}\n");
      // GNU coreutils의 'timeout'이 필요합니다. macOS에서는 brew install coreutils를 실행하거나 gtimeout으로 대체하세요.
      exec(
          'cd ' . escapeshellarg(WORK_DIR) . ' && timeout ' . BASH_TIMEOUT_SECONDS
              . ' bash -c ' . escapeshellarg($command) . ' 2>&1',
          $outputLines,
          $exitCode,
      );
      if ($exitCode === 124) {
          return ['command timed out after ' . BASH_TIMEOUT_SECONDS . 's', true];
      }
      $output = trim(implode("\n", $outputLines));
      if ($output === '') {
          $output = '(no output)';
      }
      if (mb_strlen($output) > TOOL_RESULT_MAX_CHARS) {
          $output = mb_substr($output, 0, TOOL_RESULT_MAX_CHARS)
              . "\n(truncated at " . TOOL_RESULT_MAX_CHARS . ' chars)';
      }
      if ($exitCode !== 0) {
          $output = "(exit code {$exitCode})\n{$output}";
      }
      return [$output, $exitCode !== 0];
  }

  /** Execute one bash tool call requested by the model. */
  function handleBashBlock(ToolUseBlock $block): array
  {
      if (($block->input['restart'] ?? null) === true) {
          return ['Shell restarted.', false];
      }
      $command = $block->input['command'] ?? '';
      if (!is_string($command) || $command === '') {
          return ['bash error: no command was provided.', true];
      }
      return runBash($command);
  }
  ```

  ```ruby Ruby
  # 예제가 실행된 위치에서 bash를 실행합니다. DOC_TEST_MODE에서는 문서 테스트 하네스가
  # 대신 일회용 픽스처 디렉터리를 가리키게 하며, 종료 시 제거됩니다.
  WORK_DIR =
    if DOC_TEST_MODE
      Dir.mktmpdir("orchestration-").tap do |dir|
        File.write(File.join(dir, "sample.py"), <<~PYTHON)
          def fib(n):
              return n if n < 2 else fib(n - 1) + fib(n - 2)

          print(fib(10))
        PYTHON
        at_exit { FileUtils.remove_entry(dir, true) }
      end
    else
      Dir.pwd
    end

  # 도구 입력은 Hash 또는 스트리밍 누산기에서 온 원시 JSON 문자열로 도착하므로,
  # 어느 형태든 문자열 키를 가진 Hash로 정규화합니다.
  def parse_tool_input(raw)
    return raw.transform_keys(&:to_s) if raw.is_a?(Hash)
    parsed = JSON.parse(raw.to_s) rescue nil
    parsed.is_a?(Hash) ? parsed : {}
  end

  # 셸 명령을 실행하고 [output, is_error]를 반환합니다. 샌드박스 없음: 예제 코드 전용입니다.
  def run_bash(command)
    warn "[bash] #{command}"
    begin
      stdin, stdout_and_stderr, wait_thr = Open3.popen2e("bash", "-c", command, pgroup: true, chdir: WORK_DIR)
      stdin.close
      reader = Thread.new { stdout_and_stderr.read.scrub }
      # 단조 시계(monotonic clock) 기반 데드라인으로 시간 제한을 적용하여, 시간이 초과된 명령이
      # 백그라운드에서 계속 실행되지 않고 종료되도록 합니다.
      deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + BASH_TIMEOUT_SECONDS
      until wait_thr.join(0.1)
        next if Process.clock_gettime(Process::CLOCK_MONOTONIC) < deadline

        begin
          Process.kill("-TERM", wait_thr.pid)
        rescue Errno::ESRCH
        end
        unless wait_thr.join(2)
          begin
            Process.kill("-KILL", wait_thr.pid)
          rescue Errno::ESRCH
          end
        end
        wait_thr.join(5)
        reader.join(1) || reader.kill
        stdout_and_stderr.close rescue nil
        return ["command timed out after #{BASH_TIMEOUT_SECONDS}s", true]
      end
      status = wait_thr.value
      output = reader.value.strip
      stdout_and_stderr.close
      output = "(no output)" if output.empty?
      if output.length > TOOL_RESULT_MAX_CHARS
        output = "#{output[0, TOOL_RESULT_MAX_CHARS]}\n(truncated at #{TOOL_RESULT_MAX_CHARS} chars)"
      end
      output = "(exit code #{status.exitstatus})\n#{output}" unless status.success?
      [output, !status.success?]
    rescue Errno::ENOENT => e
      return ["bash error: #{e.message}", true]
    end
  end

  # 모델이 요청한 bash 도구 호출 하나를 실행합니다.
  def handle_bash_block(block)
    input = parse_tool_input(block.input)
    return ["Shell restarted.", false] if input["restart"] == true

    command = input["command"]
    return ["bash error: no command was provided.", true] unless command.is_a?(String) && !command.empty?

    run_bash(command)
  end

  # 응답 콘텐츠를 요청 형태의 매개변수로 변환합니다. 스트리밍 누산기는
  # tool_use 입력을 원시 JSON 문자열로 반환하고 응답 전용 필드를 포함하므로,
  # 각 블록을 요청 스키마에 맞게 재구성한 뒤 다시 돌려보냅니다.
  def assistant_content_param(content)
    content.map do |block|
      case block
      when Anthropic::Models::ToolUseBlock
        input = parse_tool_input(block.input)
        {type: "tool_use", id: block.id, name: block.name, input: input}
      when Anthropic::Models::TextBlock
        {type: "text", text: block.text}
      when Anthropic::Models::ThinkingBlock
        {type: "thinking", thinking: block.thinking, signature: block.signature}
      when Anthropic::Models::RedactedThinkingBlock then {type: "redacted_thinking", data: block.data}
      else
        block.to_h
      end
    end
  end
  ```
</CodeGroup>

## 서브에이전트 하나 실행하기

각 워크플로 하위 작업은 bash 도구를 가진 자체적인 작은 에이전트 루프가 되며, 메인 루프와 동일한 effort로 실행됩니다. 요청별 타임아웃이 각 API 호출을 제한하므로, 연결이 끊어져도 전체 실행이 멈추는 대신 서브에이전트 하나만 성능이 저하됩니다.

<CodeGroup exclude="shell:cURL, shell:CLI">
  ```python Python
  def run_subagent(model: str, prompt: str) -> str:
      """One subagent: a small nested agent loop with the bash tool plus report_findings.
      Subagents inherit the main loop's effort level."""
      subagent_system = (
          "You are one agent in a larger parallel fan-out, assigned a single subtask. "
          "Investigate it directly, using bash to check facts rather than guessing, and finish "
          "by calling report_findings exactly once. Return findings, not narration."
      )
      messages = [{"role": "user", "content": prompt}]
      for _ in range(MAX_SUBAGENT_TURNS):
          with client.messages.stream(
              model=model,
              max_tokens=64000,
              system=subagent_system,
              output_config={"effort": EFFORT},
              tools=[BASH_TOOL, REPORT_TOOL],
              messages=messages,
              timeout=REQUEST_TIMEOUT_SECONDS,
          ) as stream:
              response = stream.get_final_message()
          messages.append({"role": "assistant", "content": response.content})
          if response.stop_reason == "pause_turn":
              continue
          if response.stop_reason != "tool_use":
              text = "".join(
                  block.text for block in response.content if block.type == "text"
              )
              if response.stop_reason == "max_tokens":
                  text += "\n\n(warning: subagent response was truncated at max_tokens)"
              return text
          tool_results = []
          report = None
          for block in response.content:
              if block.type != "tool_use":
                  continue
              match block.name:
                  case "report_findings":
                      report = json.dumps(block.input, indent=2)
                      output, is_error = "Findings recorded.", False
                  case "bash":
                      output, is_error = handle_bash_block(block)
                  case _:
                      output, is_error = f"unknown tool: {block.name}", True
              tool_results.append(
                  {
                      "type": "tool_result",
                      "tool_use_id": block.id,
                      "content": output,
                      "is_error": is_error,
                  }
              )
          if report is not None:
              return report
          messages.append({"role": "user", "content": tool_results})
      return "(subagent hit the turn limit before finishing)"
  ```

  ```typescript TypeScript
  // 서브에이전트 하나: bash 도구와 report_findings를 갖춘 작은 중첩 에이전트 루프입니다.
  // 서브에이전트는 메인 루프의 effort 수준을 상속합니다.
  async function runSubagent(model: string, prompt: string): Promise<string> {
    const subagentSystem =
      "You are one agent in a larger parallel fan-out, assigned a single subtask. " +
      "Investigate it directly, using bash to check facts rather than guessing, and finish " +
      "by calling report_findings exactly once. Return findings, not narration.";
    const messages: Anthropic.MessageParam[] = [{ role: "user", content: prompt }];
    for (let turn = 0; turn < MAX_SUBAGENT_TURNS; turn++) {
      const response = await client.messages
        .stream(
          {
            model,
            max_tokens: 64000,
            system: subagentSystem,
            output_config: { effort: EFFORT },
            tools: [BASH_TOOL, REPORT_TOOL],
            messages
          },
          { signal: AbortSignal.timeout(REQUEST_TIMEOUT_SECONDS * 1000) }
        )
        .finalMessage();
      messages.push({ role: "assistant", content: response.content });
      if (response.stop_reason === "pause_turn") {
        continue;
      }
      if (response.stop_reason !== "tool_use") {
        let text = response.content
          .flatMap((block) => (block.type === "text" ? [block.text] : []))
          .join("");
        if (response.stop_reason === "max_tokens") {
          text += "\n\n(warning: subagent response was truncated at max_tokens)";
        }
        return text;
      }
      const toolResults: Anthropic.ToolResultBlockParam[] = [];
      let report: string | null = null;
      for (const block of response.content) {
        if (block.type !== "tool_use") {
          continue;
        }
        let outcome: ToolOutcome;
        switch (block.name) {
          case "report_findings":
            report = JSON.stringify(block.input, null, 2);
            outcome = { output: "Findings recorded.", isError: false };
            break;
          case "bash":
            outcome = await handleBashBlock(block);
            break;
          default:
            outcome = { output: `unknown tool: ${block.name}`, isError: true };
        }
        toolResults.push({
          type: "tool_result",
          tool_use_id: block.id,
          content: outcome.output,
          is_error: outcome.isError
        });
      }
      if (report !== null) {
        return report;
      }
      messages.push({ role: "user", content: toolResults });
    }
    return "(subagent hit the turn limit before finishing)";
  }
  ```

  ```csharp C#
  // 서브에이전트 하나: bash 도구와 report_findings를 갖춘 작은 중첩 에이전트 루프.
  // 서브에이전트는 메인 루프의 노력 수준을 상속합니다.
  JsonSerializerOptions indented = new() { WriteIndented = true };

  async Task<string> RunSubagent(string prompt)
  {
      const string subagentSystem =
          "You are one agent in a larger parallel fan-out, assigned a single subtask. "
          + "Investigate it directly, using bash to check facts rather than guessing, and finish "
          + "by calling report_findings exactly once. Return findings, not narration.";
      List<MessageParam> messages = [new() { Role = Role.User, Content = prompt }];
      for (var turn = 0; turn < maxSubagentTurns; turn++)
      {
          using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(requestTimeoutSeconds));
          var response = await client.Messages.CreateStreaming(new MessageCreateParams
          {
              Model = model,
              MaxTokens = 64000,
              System = subagentSystem,
              OutputConfig = new OutputConfig { Effort = effort },
              Tools = [bashTool, reportTool],
              Messages = messages,
          }, cancellationToken: deadline.Token).Aggregate();
          messages.Add(new()
          {
              Role = Role.Assistant,
              Content = response.Content.Select(block => new ContentBlockParam(block.Json)).ToList(),
          });
          if (response.StopReason == StopReason.PauseTurn)
          {
              continue;
          }
          if (response.StopReason != StopReason.ToolUse)
          {
              var text = string.Concat(
                  response.Content.Select(block => block.TryPickText(out var textBlock) ? textBlock.Text : ""));
              if (response.StopReason == StopReason.MaxTokens)
              {
                  text += "\n\n(warning: subagent response was truncated at max_tokens)";
              }
              return text;
          }
          List<ContentBlockParam> toolResults = [];
          string? report = null;
          foreach (var block in response.Content)
          {
              if (!block.TryPickToolUse(out var toolUse))
              {
                  continue;
              }
              if (toolUse.Name == "report_findings")
              {
                  report = JsonSerializer.Serialize(toolUse.Input, indented);
              }
              var (output, isError) = toolUse.Name switch
              {
                  "report_findings" => ("Findings recorded.", false),
                  "bash" => await HandleBashBlock(toolUse),
                  _ => ($"unknown tool: {toolUse.Name}", true),
              };
              toolResults.Add(new ToolResultBlockParam(toolUse.ID) { Content = output, IsError = isError });
          }
          if (report is not null)
          {
              return report;
          }
          messages.Add(new() { Role = Role.User, Content = toolResults });
      }
      return "(subagent hit the turn limit before finishing)";
  }
  ```

  ```go Go
  // streamMessage는 요청 하나를 보내고 스트리밍된 이벤트를 누적하여
  // 최종 메시지를 만듭니다.
  func streamMessage(ctx context.Context, params anthropic.MessageNewParams) (anthropic.Message, error) {
  	ctx, cancel := context.WithTimeout(ctx, requestTimeoutSeconds*time.Second)
  	defer cancel()
  	stream := client.Messages.NewStreaming(ctx, params)
  	defer stream.Close()
  	var message anthropic.Message
  	for stream.Next() {
  		if err := message.Accumulate(stream.Current()); err != nil {
  			return message, err
  		}
  	}
  	return message, stream.Err()
  }

  // textOf는 응답의 텍스트 블록을 결합합니다.
  func textOf(message anthropic.Message) string {
  	var text strings.Builder
  	for _, block := range message.Content {
  		if textBlock, ok := block.AsAny().(anthropic.TextBlock); ok {
  			text.WriteString(textBlock.Text)
  		}
  	}
  	return text.String()
  }

  // runSubagent는 서브에이전트 하나를 실행합니다: bash 도구와 report_findings를 갖춘
  // 작은 중첩 에이전트 루프입니다. 서브에이전트는 메인 루프의 effort 수준을 상속합니다.
  func runSubagent(ctx context.Context, model string, prompt string) (string, error) {
  	const subagentSystem = "You are one agent in a larger parallel fan-out, assigned a single subtask. " +
  		"Investigate it directly, using bash to check facts rather than guessing, and finish " +
  		"by calling report_findings exactly once. Return findings, not narration."
  	messages := []anthropic.MessageParam{anthropic.NewUserMessage(anthropic.NewTextBlock(prompt))}
  	for range maxSubagentTurns {
  		response, err := streamMessage(ctx, anthropic.MessageNewParams{
  			Model:        model,
  			MaxTokens:    64000,
  			System:       []anthropic.TextBlockParam{{Text: subagentSystem}},
  			OutputConfig: anthropic.OutputConfigParam{Effort: effort},
  			Tools:        []anthropic.ToolUnionParam{bashTool, reportTool},
  			Messages:     messages,
  		})
  		if err != nil {
  			return "", err
  		}
  		messages = append(messages, response.ToParam())

  		switch response.StopReason {
  		case anthropic.StopReasonPauseTurn:
  			continue
  		case anthropic.StopReasonMaxTokens:
  			return textOf(response) + "\n\n(warning: subagent response was truncated at max_tokens)", nil
  		case anthropic.StopReasonToolUse:
  		default:
  			return textOf(response), nil
  		}

  		var toolResults []anthropic.ContentBlockParamUnion
  		var report string
  		var reportRecorded bool
  		for _, block := range response.Content {
  			toolUse, ok := block.AsAny().(anthropic.ToolUseBlock)
  			if !ok {
  				continue
  			}
  			var output string
  			var isError bool
  			switch toolUse.Name {
  			case "report_findings":
  				report = string(toolUse.Input)
  				var pretty bytes.Buffer
  				if err := json.Indent(&pretty, toolUse.Input, "", "  "); err == nil {
  					report = pretty.String()
  				}
  				reportRecorded = true
  				output = "Findings recorded."
  			case "bash":
  				output, isError = handleBashBlock(ctx, toolUse)
  			default:
  				output, isError = "unknown tool: "+toolUse.Name, true
  			}
  			toolResults = append(toolResults, anthropic.NewToolResultBlock(toolUse.ID, output, isError))
  		}
  		if reportRecorded {
  			return report, nil
  		}
  		messages = append(messages, anthropic.NewUserMessage(toolResults...))
  	}
  	return "(subagent hit the turn limit before finishing)", nil
  }
  ```

  ```java Java
  static final String SUBAGENT_SYSTEM = """
          You are one agent in a larger parallel fan-out, assigned a single subtask. \
          Investigate it directly, using bash to check facts rather than guessing, and finish \
          by calling report_findings exactly once. Return findings, not narration.""";

  // 서브에이전트 하나: bash 도구와 report_findings를 갖춘 작은 중첩 에이전트 루프.
  // 서브에이전트는 메인 루프의 노력 수준을 상속합니다.
  String runSubagent(Model model, String prompt) throws InterruptedException {
      List<MessageParam> messages = new ArrayList<>();
      messages.add(MessageParam.builder().role(MessageParam.Role.USER).content(prompt).build());
      for (int turn = 0; turn < MAX_SUBAGENT_TURNS; turn++) {
          MessageCreateParams params = MessageCreateParams.builder()
                  .model(model)
                  .maxTokens(64000L)
                  .system(SUBAGENT_SYSTEM)
                  .outputConfig(OutputConfig.builder().effort(EFFORT).build())
                  .addTool(BASH_TOOL)
                  .addTool(REPORT_TOOL)
                  .messages(messages)
                  .build();
          MessageAccumulator accumulator = MessageAccumulator.create();
          try (var stream = client.messages().createStreaming(params, REQUEST_OPTIONS)) {
              stream.stream().forEach(accumulator::accumulate);
          }
          Message response = accumulator.message();
          messages.add(response.toParam());
          StopReason stopReason = response.stopReason().orElse(null);
          if (StopReason.PAUSE_TURN.equals(stopReason)) {
              continue;
          }
          if (!StopReason.TOOL_USE.equals(stopReason)) {
              String text = response.content().stream()
                      .flatMap(block -> block.text().stream())
                      .map(TextBlock::text)
                      .collect(Collectors.joining());
              if (StopReason.MAX_TOKENS.equals(stopReason)) {
                  text += "\n\n(warning: subagent response was truncated at max_tokens)";
              }
              return text;
          }
          List<ToolUseBlock> toolUses = response.content().stream()
                  .flatMap(block -> block.toolUse().stream())
                  .toList();
          List<ContentBlockParam> toolResults = new ArrayList<>();
          String report = null;
          for (ToolUseBlock toolUse : toolUses) {
              ToolOutput result = switch (toolUse.name()) {
                  case "report_findings" -> {
                      report = toolUse._input().convert(JsonNode.class).toPrettyString();
                      yield new ToolOutput("Findings recorded.", false);
                  }
                  case "bash" -> handleBashBlock(toolUse);
                  default -> new ToolOutput("unknown tool: " + toolUse.name(), true);
              };
              toolResults.add(ContentBlockParam.ofToolResult(ToolResultBlockParam.builder()
                      .toolUseId(toolUse.id())
                      .content(result.output())
                      .isError(result.isError())
                      .build()));
          }
          if (report != null) {
              return report;
          }
          messages.add(MessageParam.builder()
                  .role(MessageParam.Role.USER)
                  .contentOfBlockParams(toolResults)
                  .build());
      }
      return "(subagent hit the turn limit before finishing)";
  }
  ```

  ```php PHP
  /**
   * Consume a message stream and assemble the final assistant turn from its events:
   * the full content-block list plus the stop reason, equivalent to what a
   * non-streaming create call returns.
   */
  function drainMessageStream(iterable $events): array
  {
      $stringValue = fn ($value) => $value instanceof \BackedEnum ? $value->value : $value;
      $blocks = [];
      $jsonBuffers = [];
      $stopReason = null;
      foreach ($events as $event) {
          $type = $stringValue($event->type);
          if ($type === 'content_block_start') {
              $blocks[$event->index] = $event->contentBlock;
              $jsonBuffers[$event->index] = '';
          } elseif ($type === 'content_block_delta') {
              $block = $blocks[$event->index];
              $delta = $event->delta;
              $deltaType = $stringValue($delta->type);
              if ($deltaType === 'text_delta') {
                  $blocks[$event->index] = $block->withText($block->text . $delta->text);
              } elseif ($deltaType === 'input_json_delta') {
                  $jsonBuffers[$event->index] .= $delta->partialJSON;
              } elseif ($deltaType === 'thinking_delta') {
                  $blocks[$event->index] = $block->withThinking($block->thinking . $delta->thinking);
              } elseif ($deltaType === 'signature_delta') {
                  $blocks[$event->index] = $block->withSignature($delta->signature);
              }
          } elseif ($type === 'message_delta') {
              $stopReason = $stringValue($event->delta->stopReason);
          }
      }
      foreach ($jsonBuffers as $index => $buffer) {
          if ($buffer !== '' && $blocks[$index] instanceof \Anthropic\Messages\ToolUseBlock) {
              $decoded = json_decode($buffer, true);
              $blocks[$index] = $blocks[$index]->withInput(is_array($decoded) ? $decoded : []);
          }
      }
      return [array_values($blocks), $stopReason];
  }

  /**
   * One subagent: a small nested agent loop with the bash tool plus report_findings.
   * Subagents inherit the main loop's effort level.
   */
  function runSubagent(Client $client, string $model, string $prompt): string
  {
      $subagentSystem =
          'You are one agent in a larger parallel fan-out, assigned a single subtask. '
          . 'Investigate it directly, using bash to check facts rather than guessing, and finish '
          . 'by calling report_findings exactly once. Return findings, not narration.';
      $messages = [['role' => 'user', 'content' => $prompt]];
      for ($turn = 0; $turn < MAX_SUBAGENT_TURNS; $turn++) {
          $stream = $client->messages->createStream(
              model: $model,
              maxTokens: 64000,
              system: $subagentSystem,
              outputConfig: ['effort' => EFFORT],
              tools: [BASH_TOOL, REPORT_TOOL],
              messages: $messages,
              requestOptions: ['timeout' => REQUEST_TIMEOUT_SECONDS],
          );
          [$content, $stopReason] = drainMessageStream($stream);
          $messages[] = ['role' => 'assistant', 'content' => $content];
          if ($stopReason === 'pause_turn') {
              continue;
          }
          if ($stopReason !== 'tool_use') {
              $text = '';
              foreach ($content as $block) {
                  if ($block instanceof \Anthropic\Messages\TextBlock) {
                      $text .= $block->text;
                  }
              }
              if ($stopReason === 'max_tokens') {
                  $text .= "\n\n(warning: subagent response was truncated at max_tokens)";
              }
              return $text;
          }
          $report = null;
          $toolResults = [];
          foreach ($content as $block) {
              if (!$block instanceof \Anthropic\Messages\ToolUseBlock) {
                  continue;
              }
              if ($block->name === 'report_findings') {
                  $report = json_encode($block->input, JSON_PRETTY_PRINT);
                  $output = 'Findings recorded.';
                  $isError = false;
              } elseif ($block->name === 'bash') {
                  [$output, $isError] = handleBashBlock($block);
              } else {
                  $output = "unknown tool: {$block->name}";
                  $isError = true;
              }
              $toolResults[] = [
                  'type' => 'tool_result',
                  'tool_use_id' => $block->id,
                  'content' => $output,
                  'is_error' => $isError,
              ];
          }
          if ($report !== null) {
              return $report;
          }
          $messages[] = ['role' => 'user', 'content' => $toolResults];
      }
      return '(subagent hit the turn limit before finishing)';
  }
  ```

  ```ruby Ruby
  # 서브에이전트 하나: bash 도구와 report_findings를 갖춘 작은 중첩 에이전트 루프입니다.
  # 서브에이전트는 메인 루프의 effort 수준을 상속합니다.
  def run_subagent(model, prompt)
    subagent_system =
      "You are one agent in a larger parallel fan-out, assigned a single subtask. " \
      "Investigate it directly, using bash to check facts rather than guessing, and finish " \
      "by calling report_findings exactly once. Return findings, not narration."
    messages = [{role: "user", content: prompt}]
    MAX_SUBAGENT_TURNS.times do
      stream = CLIENT.messages.stream(
        model: model,
        max_tokens: 64_000,
        system_: subagent_system,
        output_config: {effort: EFFORT},
        tools: [BASH_TOOL, REPORT_TOOL],
        messages: messages,
        request_options: {timeout: REQUEST_TIMEOUT_SECONDS}
      )
      response = stream.accumulated_message
      messages << {role: "assistant", content: assistant_content_param(response.content)}
      next if response.stop_reason == :pause_turn

      unless response.stop_reason == :tool_use
        text = response.content.select { |block| block.type == :text }.map(&:text).join
        text += "\n\n(warning: subagent response was truncated at max_tokens)" if response.stop_reason == :max_tokens
        return text
      end

      report = nil
      tool_results = []
      response.content.each do |block|
        next unless block.type == :tool_use

        input = parse_tool_input(block.input)
        case block.name
        when "report_findings"
          report = JSON.pretty_generate(input)
          output, is_error = "Findings recorded.", false
        when "bash"
          output, is_error = handle_bash_block(block)
        else
          output, is_error = "unknown tool: #{block.name}", true
        end
        tool_results << {
          type: "tool_result",
          tool_use_id: block.id,
          content: output,
          is_error: is_error
        }
      end
      return report unless report.nil?

      messages << {role: "user", content: tool_results}
    end
    "(subagent hit the turn limit before finishing)"
  end
  ```
</CodeGroup>

## 재실행 시 이어서 진행되도록 결과 저널링하기

수십 개의 서브에이전트를 생성하는 팬아웃은 처음부터 다시 시작하기에 비용이 많이 듭니다. 작은 콘텐츠 주소 지정 저널이 이를 멱등적으로 만듭니다. 서브에이전트를 디스패치하기 전에 로컬 JSON 파일에서 해당 프롬프트의 SHA-256을 조회하고, 기록된 결과가 있으면 그것을 반환합니다. 실행을 중단하고 다시 실행하면, 완료되지 않은 하위 작업만 다시 계산됩니다. 저널은 단일 팬아웃 웨이브 내에서가 아니라 실행 간에 중복을 제거합니다. 새로 시작하려면 저널 파일을 삭제하세요.

<CodeGroup exclude="shell:cURL, shell:CLI">
  ```python Python
  _journal_lock = threading.Lock()


  def _load_journal() -> dict:
      try:
          with open(JOURNAL_PATH) as file:
              return json.load(file) or {}
      except (OSError, json.JSONDecodeError):
          return {}


  def journaled(prompt: str, compute) -> str:
      """Return a cached result for this exact prompt, or compute and persist it. This
      makes the fan-out resumable: interrupt the run, rerun it, and only the subtasks
      that never finished are recomputed. Delete the journal file to start fresh."""
      key = hashlib.sha256(prompt.encode()).hexdigest()
      cached = _load_journal().get(key)
      if cached is not None:
          print(f"[journal] cache hit for {key[:12]}", file=sys.stderr)
          return cached
      result = compute()
      try:
          with _journal_lock:  # fan-out writes from many threads
              journal = _load_journal()
              journal[key] = result
              temp = f"{JOURNAL_PATH}.tmp"
              with open(temp, "w") as file:
                  json.dump(journal, file)
              os.replace(temp, JOURNAL_PATH)  # atomic on POSIX and Windows
      # 저널 쓰기가 실패하더라도 계산된 결과를 잃어서는 안 됩니다
      except OSError as error:
          print(f"[journal] write failed: {error}", file=sys.stderr)
      return result
  ```

  ```typescript TypeScript
  let journalWriteChain = Promise.resolve();

  async function loadJournal(): Promise<Record<string, string>> {
    try {
      return JSON.parse(await readFile(JOURNAL_PATH, "utf8")) ?? {};
    } catch (error) {
      if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
        console.error(`[journal] discarding unreadable journal: ${error}`);
      }
      return {};
    }
  }

  // 이 프롬프트와 정확히 일치하는 캐시된 결과를 반환하거나, 계산 후 저장합니다.
  // 이를 통해 팬아웃을 재개할 수 있습니다: 실행을 중단했다가 다시 실행하면 완료되지
  // 않은 하위 작업만 다시 계산됩니다. 처음부터 시작하려면 저널 파일을 삭제하세요.
  async function journaled(prompt: string, compute: () => Promise<string>): Promise<string> {
    const key = createHash("sha256").update(prompt).digest("hex");
    const cached = (await loadJournal())[key];
    if (cached !== undefined) {
      console.error(`[journal] cache hit for ${key.slice(0, 12)}`);
      return cached;
    }
    const result = await compute();
    // 동시 실행되는 서브에이전트가 서로의 항목을 덮어쓰지 않도록 쓰기를 체인으로 연결합니다.
    // 체인은 항상 settled 상태로 유지되어 한 번의 쓰기 실패가 이후 쓰기를 오염시키지 않습니다.
    await (journalWriteChain = journalWriteChain
      .then(async () => {
        const journal = await loadJournal();
        journal[key] = result;
        const temp = `${JOURNAL_PATH}.tmp`;
        await writeFile(temp, JSON.stringify(journal));
        await rename(temp, JOURNAL_PATH);
      })
      .catch((error) => console.error(`[journal] write failed: ${error}`)));
    return result;
  }
  ```

  ```csharp C#
  SemaphoreSlim journalLock = new(1, 1);

  async Task<Dictionary<string, string>> LoadJournal()
  {
      try
      {
          return JsonSerializer.Deserialize<Dictionary<string, string>>(await File.ReadAllTextAsync(journalPath)) ?? [];
      }
      catch (Exception error) when (error is IOException or UnauthorizedAccessException or JsonException)
      {
          return [];
      }
  }

  // 이 정확한 프롬프트에 대한 캐시된 결과를 반환하거나, 계산 후 저장합니다. 이를 통해
  // 팬아웃이 재개 가능해집니다: 실행을 중단하고 다시 실행하면 완료되지 않은 하위 작업만
  // 다시 계산됩니다. 처음부터 시작하려면 저널 파일을 삭제하세요.
  async Task<string> Journaled(string prompt, Func<Task<string>> compute)
  {
      var key = Convert.ToHexStringLower(SHA256.HashData(Encoding.UTF8.GetBytes(prompt)));
      if ((await LoadJournal()).TryGetValue(key, out var cached))
      {
          Console.Error.WriteLine($"[journal] cache hit for {key[..12]}");
          return cached;
      }
      var result = await compute();
      await journalLock.WaitAsync(); // fan-out writes from many tasks
      try
      {
          var journal = await LoadJournal();
          journal[key] = result;
          var temp = journalPath + ".tmp";
          await File.WriteAllTextAsync(temp, JsonSerializer.Serialize(journal));
          File.Move(temp, journalPath, overwrite: true);
      }
      catch (Exception error) when (error is IOException or UnauthorizedAccessException or NotSupportedException)
      {
          // 저널은 최선 노력 방식이므로 계산된 결과를 절대 버리지 않습니다.
          Console.Error.WriteLine($"[journal] write failed: {error.Message}");
      }
      finally
      {
          journalLock.Release();
      }
      return result;
  }
  ```

  ```go Go
  var journalMutex sync.Mutex

  func loadJournal() map[string]string {
  	data, err := os.ReadFile(journalPath)
  	if err != nil {
  		return map[string]string{}
  	}
  	var journal map[string]string
  	if err := json.Unmarshal(data, &journal); err != nil || journal == nil {
  		return map[string]string{}
  	}
  	return journal
  }

  // journaled는 이 프롬프트에 대한 캐시된 결과를 반환하거나, 계산 후 저장합니다.
  // 이로써 분산 작업을 재개할 수 있습니다: 실행을 중단하고 다시 실행하면 완료되지 않은
  // 하위 작업만 다시 계산됩니다. 새로 시작하려면 저널 파일을 삭제하세요.
  func journaled(prompt string, compute func() (string, error)) (string, error) {
  	sum := sha256.Sum256([]byte(prompt))
  	key := hex.EncodeToString(sum[:])
  	if cached, ok := loadJournal()[key]; ok {
  		fmt.Fprintf(os.Stderr, "[journal] cache hit for %s\n", key[:12])
  		return cached, nil
  	}
  	result, err := compute()
  	if err != nil {
  		return "", err
  	}
  	journalMutex.Lock() // fan-out writes from many goroutines
  	defer journalMutex.Unlock()
  	journal := loadJournal()
  	journal[key] = result
  	data, _ := json.Marshal(journal)
  	temp := journalPath + ".tmp"
  	if err := os.WriteFile(temp, data, 0o644); err != nil {
  		fmt.Fprintf(os.Stderr, "[journal] write failed: %s\n", err)
  	} else if err := os.Rename(temp, journalPath); err != nil {
  		fmt.Fprintf(os.Stderr, "[journal] write failed: %s\n", err)
  		_ = os.Remove(temp)
  	}
  	return result, nil
  }
  ```

  ```java Java
  static final ObjectMapper JOURNAL_MAPPER = new ObjectMapper();
  static final ReentrantLock JOURNAL_LOCK = new ReentrantLock();

  Map<String, String> loadJournal() {
      try {
          return Objects.requireNonNullElseGet(
                  JOURNAL_MAPPER.readValue(Files.readString(JOURNAL_PATH), new TypeReference<HashMap<String, String>>() {}),
                  HashMap::new);
      } catch (IOException _) {
          return new HashMap<>();
      }
  }

  // 이 정확한 프롬프트에 대한 캐시된 결과를 반환하거나, 계산 후 저장합니다. 이를 통해
  // 팬아웃이 재개 가능해집니다: 실행을 중단하고 다시 실행하면 완료되지 않은 하위 작업만
  // 다시 계산됩니다. 처음부터 시작하려면 저널 파일을 삭제하세요.
  String journaled(String prompt, Callable<String> compute) throws Exception {
      var digest = MessageDigest.getInstance("SHA-256").digest(prompt.getBytes(StandardCharsets.UTF_8));
      String key = HexFormat.of().formatHex(digest);
      String cached = loadJournal().get(key);
      if (cached != null) {
          System.err.println("[journal] cache hit for " + key.substring(0, 12));
          return cached;
      }
      String result = compute.call();
      JOURNAL_LOCK.lock(); // fan-out writes from many threads
      try {
          Map<String, String> journal = loadJournal();
          journal.put(key, result);
          Path temp = JOURNAL_PATH.resolveSibling(JOURNAL_PATH.getFileName() + ".tmp");
          Files.writeString(temp, JOURNAL_MAPPER.writeValueAsString(journal));
          Files.move(temp, JOURNAL_PATH, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
      } catch (IOException error) {
          // 저널은 최선 노력 방식입니다. 계산된 결과는 절대 버리지 마세요.
          System.err.println("[journal] write failed: " + error);
      } finally {
          JOURNAL_LOCK.unlock();
      }
      return result;
  }
  ```

  ```php PHP
  function loadJournal(): array
  {
      $raw = @file_get_contents(JOURNAL_PATH);
      if ($raw === false) {
          return [];
      }
      $decoded = json_decode($raw, true);
      return is_array($decoded) ? $decoded : [];
  }

  /**
   * Return a cached result for this exact prompt, or compute and persist it. This
   * makes the fan-out resumable: interrupt the run, rerun it, and only the subtasks
   * that never finished are recomputed. Delete the journal file to start fresh.
   */
  function journaled(string $prompt, callable $compute): string
  {
      $key = hash('sha256', $prompt);
      $journal = loadJournal();
      if (array_key_exists($key, $journal)) {
          fwrite(STDERR, '[journal] cache hit for ' . substr($key, 0, 12) . "\n");
          return $journal[$key];
      }
      $result = $compute();
      $journal = loadJournal();
      $journal[$key] = $result;
      $temp = JOURNAL_PATH . '.tmp';
      $encoded = json_encode($journal, JSON_INVALID_UTF8_SUBSTITUTE);
      if ($encoded === false || @file_put_contents($temp, $encoded) === false || !@rename($temp, JOURNAL_PATH)) {
          fwrite(STDERR, '[journal] write failed: ' . (error_get_last()['message'] ?? json_last_error_msg()) . "\n");
          @unlink($temp);
      }
      return $result;
  }
  ```

  ```ruby Ruby
  JOURNAL_LOCK = Mutex.new

  def load_journal
    JSON.parse(File.read(JOURNAL_PATH)) || {}
  rescue SystemCallError, JSON::ParserError
    {}
  end

  # 이 프롬프트와 정확히 일치하는 캐시된 결과를 반환하거나, 계산 후 저장합니다.
  # 이로써 팬아웃을 재개할 수 있습니다: 실행을 중단했다가 다시 실행하면 완료되지 않은
  # 하위 작업만 다시 계산됩니다. 처음부터 시작하려면 저널 파일을 삭제하세요.
  def journaled(prompt)
    key = Digest::SHA256.hexdigest(prompt)
    cached = load_journal[key]
    unless cached.nil?
      warn "[journal] cache hit for #{key[0, 12]}"
      return cached
    end
    result = yield
    begin
      JOURNAL_LOCK.synchronize do # fan-out writes from many threads
        journal = load_journal
        journal[key] = result
        temp = "#{JOURNAL_PATH}.tmp"
        File.write(temp, JSON.generate(journal))
        File.rename(temp, JOURNAL_PATH)
      end
    rescue SystemCallError => error # the journal is best-effort; never discard a computed result
      warn "[journal] write failed: #{error}"
    end
    result
  end
  ```
</CodeGroup>

## 팬아웃한 다음 검증하기

팬아웃은 최대 `MAX_TOTAL_SUBTASKS`개의 프롬프트를 받아, 최대 `MAX_CONCURRENT`개가 동시에 진행되도록(PHP 포트에서는 순차적으로) 저널을 통해 실행하며, 실패를 격리하여 고장 난 서브에이전트 하나가 실행을 종료시키는 대신 오류 문자열로 저하되도록 합니다. 첫 번째 웨이브가 끝나면, 두 번째 웨이브가 동일한 서브에이전트 경로를 재사용하여 각 결과를 반박하려고 시도합니다. 모든 검증자는 소스로부터 주장을 다시 도출하며, 불확실할 때는 기본적으로 반박된 것으로 처리합니다. 원래 결과와 그 판정 모두 오케스트레이터에 반환되어 함께 평가할 수 있습니다.

<CodeGroup exclude="shell:cURL, shell:CLI">
  ```python Python
  def normalize_subtasks(raw) -> list[str]:
      """Accept the subtasks input in whatever shape the model emits: an array, the array
      JSON-encoded as a single string, or a newline-separated list."""
      if isinstance(raw, str):
          try:
              raw = json.loads(raw)
          except json.JSONDecodeError:
              raw = raw.splitlines() if "\n" in raw else [raw]
      if not isinstance(raw, list):
          return []
      return [task.strip() for task in raw if isinstance(task, str) and task.strip()]


  def verify_prompt_for(subtask: str, result: str) -> str:
      return (
          "Adversarially verify the subagent result below: try to REFUTE it. Re-derive the "
          "claims yourself with bash rather than trusting the result, and look for evidence "
          "that contradicts them. Default to refuted if uncertain. Call report_findings with "
          "summary 'refuted: <why>' or 'confirmed: <why>', citing the file:line or command "
          "output that decided it.\n\n"
          f"""Subtask: {subtask}

  Result to verify:
  {result}"""
      )


  def run_workflow(model: str, raw_subtasks) -> tuple[str, bool]:
      """Run subtasks as parallel subagents, then run a second verification wave over
      the results, and return both. MAX_TOTAL_SUBTASKS bounds how many the model can
      queue; MAX_CONCURRENT bounds how many run at once."""
      all_subtasks = normalize_subtasks(raw_subtasks)
      subtasks = all_subtasks[:MAX_TOTAL_SUBTASKS]
      dropped = len(all_subtasks) - len(subtasks)
      if not subtasks:
          return "Workflow error: no usable subtasks were provided.", True
      print(f"[workflow] fanning out {len(subtasks)} agents", file=sys.stderr)

      def run_one(prompt: str) -> str:
          try:
              return journaled(prompt, lambda: run_subagent(model, prompt))
          # 하위 에이전트 하나가 실패해도 실행이 종료되어서는 안 됩니다
          except Exception as error:
              return f"(subagent failed: {type(error).__name__}: {error})"

      with concurrent.futures.ThreadPoolExecutor(max_workers=MAX_CONCURRENT) as pool:
          results = list(pool.map(run_one, subtasks))
          print(f"[workflow] verifying {len(results)} results", file=sys.stderr)
          verify_prompts = [
              verify_prompt_for(task, result) for task, result in zip(subtasks, results)
          ]
          verdicts = list(pool.map(run_one, verify_prompts))

      joined = "\n\n".join(
          f"""[agent {index + 1}: {task}]
  {result}

  [verify {index + 1}]
  {verdict}"""
          for index, (task, result, verdict) in enumerate(
              zip(subtasks, results, verdicts)
          )
      )
      if dropped > 0:
          joined = (
              f"(note: {dropped} subtasks beyond MAX_TOTAL_SUBTASKS={MAX_TOTAL_SUBTASKS} were not "
              "run; rerun them in a follow-up Workflow call)\n\n" + joined
          )
      return joined, False
  ```

  ```typescript TypeScript
  // 모델이 내보내는 어떤 형태의 subtasks 입력도 허용합니다: 배열, 단일 문자열로
  // JSON 인코딩된 배열, 또는 줄바꿈으로 구분된 목록.
  function normalizeSubtasks(raw: unknown): string[] {
    let value = raw;
    if (typeof raw === "string") {
      try {
        value = JSON.parse(raw);
      } catch {
        value = raw.includes("\n") ? raw.split("\n") : [raw];
      }
    }
    if (!Array.isArray(value)) {
      return [];
    }
    return value
      .filter((task) => typeof task === "string")
      .map((task) => task.trim())
      .filter((task) => task.length > 0);
  }

  function verifyPromptFor(subtask: string, result: string): string {
    const instructions =
      "Adversarially verify the subagent result below: try to REFUTE it. Re-derive the " +
      "claims yourself with bash rather than trusting the result, and look for evidence " +
      "that contradicts them. Default to refuted if uncertain. Call report_findings with " +
      "summary 'refuted: <why>' or 'confirmed: <why>', citing the file:line or command " +
      "output that decided it.";
    return `${instructions}

  Subtask: ${subtask}

  Result to verify:
  ${result}`;
  }

  // 동시성 제한이 있는 map: 한 번에 최대 `limit`개의 작업만 실행됩니다.
  async function mapWithLimit<In, Out>(
    items: readonly In[],
    limit: number,
    task: (item: In) => Promise<Out>
  ): Promise<Out[]> {
    const results = new Array<Out>(items.length);
    let cursor = 0;
    const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
      while (cursor < items.length) {
        const index = cursor++;
        results[index] = await task(items[index]);
      }
    });
    await Promise.all(workers);
    return results;
  }

  // 하위 작업을 병렬 서브에이전트로 실행한 다음, 결과에 대해 두 번째 검증 단계를
  // 실행하고 둘 다 반환합니다. MAX_TOTAL_SUBTASKS는 모델이 대기열에 넣을 수 있는 수를,
  // MAX_CONCURRENT는 동시에 실행되는 수를 제한합니다.
  async function runWorkflow(model: string, rawSubtasks: unknown): Promise<ToolOutcome> {
    const allSubtasks = normalizeSubtasks(rawSubtasks);
    const subtasks = allSubtasks.slice(0, MAX_TOTAL_SUBTASKS);
    const dropped = allSubtasks.length - subtasks.length;
    if (subtasks.length === 0) {
      return { output: "Workflow error: no usable subtasks were provided.", isError: true };
    }
    console.error(`[workflow] fanning out ${subtasks.length} agents`);

    const runOne = async (prompt: string): Promise<string> => {
      try {
        return await journaled(prompt, () => runSubagent(model, prompt));
      } catch (error) {
        // 격리 경계: 잘못된 서브에이전트 하나가 전체 실행을 중단시켜서는 안 됩니다.
        const reason =
          error instanceof Error ? `${error.name}: ${error.message}` : String(error);
        return `(subagent failed: ${reason})`;
      }
    };

    const results = await mapWithLimit(subtasks, MAX_CONCURRENT, runOne);
    console.error(`[workflow] verifying ${results.length} results`);
    const verifyPrompts = subtasks.map((task, index) => verifyPromptFor(task, results[index]));
    const verdicts = await mapWithLimit(verifyPrompts, MAX_CONCURRENT, runOne);

    const report = subtasks
      .map(
        (task, index) => `[agent ${index + 1}: ${task}]
  ${results[index]}

  [verify ${index + 1}]
  ${verdicts[index]}`
      )
      .join("\n\n");
    const droppedNote =
      dropped > 0
        ? `(note: ${dropped} subtasks beyond MAX_TOTAL_SUBTASKS=${MAX_TOTAL_SUBTASKS} were ` +
          "not run; rerun them in a follow-up Workflow call)\n\n"
        : "";
    return { output: droppedNote + report, isError: false };
  }
  ```

  ```csharp C#
  // 모델이 내보내는 어떤 형태의 subtasks 입력이든 수용합니다: 배열, 단일 문자열로
  // JSON 인코딩된 배열, 또는 줄바꿈으로 구분된 목록.
  List<string> NormalizeSubtasks(JsonElement raw)
  {
      IEnumerable<string?> tasks = raw.ValueKind switch
      {
          JsonValueKind.Array => raw.EnumerateArray()
              .Where(item => item.ValueKind == JsonValueKind.String)
              .Select(item => item.GetString()),
          JsonValueKind.String => ParseSubtaskString(raw.GetString()!),
          _ => [],
      };
      return [.. tasks.OfType<string>().Select(task => task.Trim()).Where(task => task.Length > 0)];
  }

  IEnumerable<string?> ParseSubtaskString(string single)
  {
      try
      {
          return JsonSerializer.Deserialize<List<string?>>(single) ?? [];
      }
      catch (JsonException)
      {
          return single.Split('\n');
      }
  }

  string VerifyPromptFor(string subtask, string result) => $"""
      Adversarially verify the subagent result below: try to REFUTE it. Re-derive the claims yourself with bash rather than trusting the result, and look for evidence that contradicts them. Default to refuted if uncertain. Call report_findings with summary 'refuted: <why>' or 'confirmed: <why>', citing the file:line or command output that decided it.

      Subtask: {subtask}

      Result to verify:
      {result}
      """;

  // 하위 작업을 병렬 서브에이전트로 실행한 다음, 결과에 대해 2차 검증 단계를 실행하고
  // 둘 다 반환합니다. maxTotalSubtasks는 모델이 큐에 넣을 수 있는 개수를 제한하고,
  // maxConcurrent는 동시에 실행되는 개수를 제한합니다.
  async Task<(string Output, bool IsError)> RunWorkflow(JsonElement rawSubtasks)
  {
      var allSubtasks = NormalizeSubtasks(rawSubtasks);
      List<string> subtasks = [.. allSubtasks.Take(maxTotalSubtasks)];
      var dropped = allSubtasks.Count - subtasks.Count;
      if (subtasks.Count == 0)
      {
          return ("Workflow error: no usable subtasks were provided.", true);
      }
      Console.Error.WriteLine($"[workflow] fanning out {subtasks.Count} agents");

      using SemaphoreSlim gate = new(maxConcurrent);
      async Task<string> RunOne(string prompt)
      {
          await gate.WaitAsync();
          try
          {
              return await Journaled(prompt, () => RunSubagent(prompt));
          }
          catch (Exception error)
          {
              // 격리 경계: 하나의 잘못된 서브에이전트가 전체 실행을 종료시켜서는 안 됩니다.
              return $"(subagent failed: {error.GetType().Name}: {error.Message})";
          }
          finally
          {
              gate.Release();
          }
      }

      var results = await Task.WhenAll(subtasks.Select(RunOne));
      Console.Error.WriteLine($"[workflow] verifying {results.Length} results");
      var verdicts = await Task.WhenAll(subtasks.Zip(results, VerifyPromptFor).Select(RunOne));

      var joined = string.Join("\n\n", subtasks.Select((task, index) => $"""
          [agent {index + 1}: {task}]
          {results[index]}

          [verify {index + 1}]
          {verdicts[index]}
          """));
      if (dropped > 0)
      {
          joined = $"""
              (note: {dropped} subtasks beyond maxTotalSubtasks={maxTotalSubtasks} were not run; rerun them in a follow-up Workflow call)

              {joined}
              """;
      }
      return (joined, false);
  }
  ```

  ```go Go
  // normalizeSubtasks는 모델이 내보내는 어떤 형태의 하위 작업 입력이든 받습니다:
  // 배열, 단일 문자열로 JSON 인코딩된 배열, 또는 줄바꿈으로 구분된 목록.
  func normalizeSubtasks(raw json.RawMessage) []string {
  	var tasks []string
  	if err := json.Unmarshal(raw, &tasks); err != nil {
  		var single string
  		if err := json.Unmarshal(raw, &single); err != nil {
  			return nil
  		}
  		if err := json.Unmarshal([]byte(single), &tasks); err != nil {
  			tasks = strings.Split(single, "\n")
  		}
  	}
  	cleaned := make([]string, 0, len(tasks))
  	for _, task := range tasks {
  		if trimmed := strings.TrimSpace(task); trimmed != "" {
  			cleaned = append(cleaned, trimmed)
  		}
  	}
  	return cleaned
  }

  func verifyPromptFor(subtask, result string) string {
  	return "Adversarially verify the subagent result below: try to REFUTE it. Re-derive the " +
  		"claims yourself with bash rather than trusting the result, and look for evidence " +
  		"that contradicts them. Default to refuted if uncertain. Call report_findings with " +
  		"summary 'refuted: <why>' or 'confirmed: <why>', citing the file:line or command " +
  		"output that decided it.\n\n" +
  		fmt.Sprintf(`Subtask: %s

  Result to verify:
  %s`, subtask, result)
  }

  // mapWithLimit는 최대 limit개의 고루틴을 동시에 띄워 items에 대해 task를 실행합니다.
  func mapWithLimit(items []string, limit int, task func(string) string) []string {
  	results := make([]string, len(items))
  	semaphore := make(chan struct{}, limit)
  	var waitGroup sync.WaitGroup
  	for index, item := range items {
  		semaphore <- struct{}{}
  		waitGroup.Go(func() {
  			defer func() { <-semaphore }()
  			results[index] = task(item)
  		})
  	}
  	waitGroup.Wait()
  	return results
  }

  // runWorkflow는 하위 작업을 병렬 서브에이전트로 실행한 뒤 결과에 대해 2차 검증을
  // 수행하고 둘 다 반환합니다. maxTotalSubtasks는 모델이 큐에 넣을 수 있는 수를,
  // maxConcurrent는 동시에 실행되는 수를 제한합니다.
  func runWorkflow(ctx context.Context, model string, rawSubtasks json.RawMessage) (string, bool) {
  	allSubtasks := normalizeSubtasks(rawSubtasks)
  	subtasks := allSubtasks[:min(len(allSubtasks), maxTotalSubtasks)]
  	dropped := len(allSubtasks) - len(subtasks)
  	if len(subtasks) == 0 {
  		return "Workflow error: no usable subtasks were provided.", true
  	}
  	fmt.Fprintf(os.Stderr, "[workflow] fanning out %d agents\n", len(subtasks))

  	runOne := func(prompt string) string {
  		report, err := journaled(prompt, func() (string, error) { return runSubagent(ctx, model, prompt) })
  		if err != nil {
  			// 격리 경계: 서브에이전트 하나가 실패해도 전체 실행이 종료되지 않아야 합니다.
  			return fmt.Sprintf("(subagent failed: %s)", err)
  		}
  		return report
  	}

  	results := mapWithLimit(subtasks, maxConcurrent, runOne)
  	fmt.Fprintf(os.Stderr, "[workflow] verifying %d results\n", len(results))
  	verifyPrompts := make([]string, len(subtasks))
  	for index, task := range subtasks {
  		verifyPrompts[index] = verifyPromptFor(task, results[index])
  	}
  	verdicts := mapWithLimit(verifyPrompts, maxConcurrent, runOne)

  	sections := make([]string, len(subtasks))
  	for index, task := range subtasks {
  		sections[index] = fmt.Sprintf(`[agent %d: %s]
  %s

  [verify %d]
  %s`,
  			index+1, task, results[index], index+1, verdicts[index])
  	}
  	joined := strings.Join(sections, "\n\n")
  	if dropped > 0 {
  		joined = fmt.Sprintf("(note: %d subtasks beyond maxTotalSubtasks=%d were not run; "+
  			"rerun them in a follow-up Workflow call)\n\n", dropped, maxTotalSubtasks) + joined
  	}
  	return joined, false
  }
  ```

  ```java Java
  // 모델이 내보내는 어떤 형태의 subtasks 입력이든 수용합니다: 배열, 단일 문자열로
  // JSON 인코딩된 배열, 또는 줄바꿈으로 구분된 목록.
  List<String> normalizeSubtasks(JsonValue rawSubtasks) {
      List<String> tasks = switch (rawSubtasks) {
          case JsonArray array -> array.values().stream()
                  .map(item -> item instanceof JsonString text ? text.value() : item.toString())
                  .toList();
          case JsonString string -> parseSubtaskString(string.value());
          default -> List.of();
      };
      return tasks.stream()
              .filter(Objects::nonNull)
              .map(String::strip)
              .filter(task -> !task.isEmpty())
              .toList();
  }

  List<String> parseSubtaskString(String encoded) {
      try {
          String[] parsed = new ObjectMapper().readValue(encoded, String[].class);
          return parsed == null ? List.of() : Arrays.asList(parsed);
      } catch (JsonProcessingException _) {
          return encoded.lines().toList();
      }
  }

  String verifyPromptFor(String subtask, String result) {
      return """
              Adversarially verify the subagent result below: try to REFUTE it. Re-derive the \
              claims yourself with bash rather than trusting the result, and look for evidence \
              that contradicts them. Default to refuted if uncertain. Call report_findings with \
              summary 'refuted: <why>' or 'confirmed: <why>', citing the file:line or command \
              output that decided it.

              Subtask: %s

              Result to verify:
              %s""".formatted(subtask, result);
  }

  // invokeAll은 모든 작업을 기다리므로, 여기서 읽을 때 각 future는 완료된 상태입니다.
  List<String> runAll(ExecutorService pool, List<String> prompts, Model model) throws InterruptedException {
      List<Callable<String>> jobs = prompts.stream()
              .<Callable<String>>map(prompt -> () -> journaled(prompt, () -> runSubagent(model, prompt)))
              .toList();
      return pool.invokeAll(jobs).stream()
              .map(future -> switch (future.state()) {
                  case SUCCESS -> future.resultNow();
                  // 격리 경계: 서브에이전트 하나가 실패해도 전체 실행이 끝나서는 안 됩니다.
                  case FAILED -> "(subagent failed: " + future.exceptionNow() + ")";
                  case CANCELLED, RUNNING -> "(subagent failed: cancelled)";
              })
              .toList();
  }

  // 하위 작업을 병렬 서브에이전트로 실행한 뒤, 결과에 대해 2차 검증 단계를 실행하고
  // 둘 다 반환합니다. MAX_TOTAL_SUBTASKS는 모델이 큐에 넣을 수 있는 개수를 제한하고,
  // MAX_CONCURRENT는 동시에 실행되는 개수를 제한합니다.
  ToolOutput runWorkflow(Model model, JsonValue rawSubtasks) throws InterruptedException {
      List<String> allSubtasks = normalizeSubtasks(rawSubtasks);
      List<String> subtasks = allSubtasks.stream().limit(MAX_TOTAL_SUBTASKS).toList();
      int dropped = allSubtasks.size() - subtasks.size();
      if (subtasks.isEmpty()) {
          return new ToolOutput("Workflow error: no usable subtasks were provided.", true);
      }
      System.err.println("[workflow] fanning out " + subtasks.size() + " agents");

      List<String> results;
      List<String> verdicts;
      try (ExecutorService pool = Executors.newFixedThreadPool(MAX_CONCURRENT, Thread.ofVirtual().factory())) {
          results = runAll(pool, subtasks, model);
          System.err.println("[workflow] verifying " + results.size() + " results");
          List<String> verifyPrompts = IntStream.range(0, subtasks.size())
                  .mapToObj(index -> verifyPromptFor(subtasks.get(index), results.get(index)))
                  .toList();
          verdicts = runAll(pool, verifyPrompts, model);
      }
      String joined = IntStream.range(0, subtasks.size())
              .mapToObj(index -> """
                      [agent %d: %s]
                      %s

                      [verify %d]
                      %s""".formatted(index + 1, subtasks.get(index), results.get(index), index + 1, verdicts.get(index)))
              .collect(Collectors.joining("\n\n"));
      if (dropped > 0) {
          joined = """
                  (note: %d subtasks beyond MAX_TOTAL_SUBTASKS=%d were not run; rerun them in a follow-up Workflow call)

                  %s""".formatted(dropped, MAX_TOTAL_SUBTASKS, joined);
      }
      return new ToolOutput(joined, false);
  }
  ```

  ```php PHP
  /**
   * Accept the subtasks input in whatever shape the model emits: an array, the array
   * JSON-encoded as a single string, or a newline-separated list.
   */
  function normalizeSubtasks(mixed $raw): array
  {
      if (is_string($raw)) {
          try {
              $raw = json_decode($raw, true, flags: JSON_THROW_ON_ERROR);
          } catch (\JsonException) {
              $raw = str_contains($raw, "\n") ? explode("\n", $raw) : [$raw];
          }
      }
      if (!is_array($raw)) {
          return [];
      }
      $tasks = array_map('trim', array_filter($raw, 'is_string'));
      return array_values(array_filter($tasks, fn ($task) => $task !== ''));
  }

  function verifyPromptFor(string $subtask, string $result): string
  {
      return 'Adversarially verify the subagent result below: try to REFUTE it. Re-derive the '
          . 'claims yourself with bash rather than trusting the result, and look for evidence '
          . 'that contradicts them. Default to refuted if uncertain. Call report_findings with '
          . "summary 'refuted: <why>' or 'confirmed: <why>', citing the file:line or command "
          . "output that decided it.\n\n"
          . <<<PROMPT
              Subtask: {$subtask}

              Result to verify:
              {$result}
              PROMPT;
  }

  /**
   * Run subtasks through the journal, then run a second verification wave over the
   * results, and return both. PHP's standard runtime has no lightweight thread pool,
   * so both waves run sequentially here (MAX_CONCURRENT is unused); the SDK examples
   * in other languages fan them out in parallel.
   */
  function runWorkflow(Client $client, string $model, mixed $rawSubtasks): array
  {
      $allSubtasks = normalizeSubtasks($rawSubtasks);
      $subtasks = array_slice($allSubtasks, 0, MAX_TOTAL_SUBTASKS);
      $dropped = count($allSubtasks) - count($subtasks);
      if ($subtasks === []) {
          return ['Workflow error: no usable subtasks were provided.', true];
      }
      fwrite(STDERR, '[workflow] running ' . count($subtasks) . " agents\n");

      $runOne = function (string $prompt) use ($client, $model): string {
          try {
              return journaled($prompt, fn () => runSubagent($client, $model, $prompt));
          } catch (\Throwable $error) {
              // 격리 경계: 잘못된 서브에이전트 하나가 전체 실행을 중단시켜서는 안 됩니다.
              return '(subagent failed: ' . $error::class . ': ' . $error->getMessage() . ')';
          }
      };

      $results = array_map($runOne, $subtasks);
      fwrite(STDERR, '[workflow] verifying ' . count($results) . " results\n");
      $verifyPrompts = array_map(verifyPromptFor(...), $subtasks, $results);
      $verdicts = array_map($runOne, $verifyPrompts);

      $sections = [];
      foreach ($subtasks as $index => $task) {
          $sections[] = '[agent ' . ($index + 1) . ": {$task}]\n{$results[$index]}"
              . "\n\n[verify " . ($index + 1) . "]\n{$verdicts[$index]}";
      }
      $joined = implode("\n\n", $sections);
      if ($dropped > 0) {
          $joined = '(note: ' . $dropped . ' subtasks beyond MAX_TOTAL_SUBTASKS=' . MAX_TOTAL_SUBTASKS
              . " were not run; rerun them in a follow-up Workflow call)\n\n" . $joined;
      }
      return [$joined, false];
  }
  ```

  ```ruby Ruby
  # 모델이 내보내는 어떤 형태의 subtasks 입력도 허용합니다: 배열, 단일 문자열로
  # JSON 인코딩된 배열, 또는 줄바꿈으로 구분된 목록.
  def normalize_subtasks(raw)
    if raw.is_a?(String)
      begin
        raw = JSON.parse(raw)
      rescue JSON::ParserError
        raw = raw.include?("\n") ? raw.split("\n") : [raw]
      end
    end
    return [] unless raw.is_a?(Array)
    raw.select { |task| task.is_a?(String) }.map(&:strip).reject(&:empty?)
  end

  def verify_prompt_for(subtask, result)
    "Adversarially verify the subagent result below: try to REFUTE it. Re-derive the " \
      "claims yourself with bash rather than trusting the result, and look for evidence " \
      "that contradicts them. Default to refuted if uncertain. Call report_findings with " \
      "summary 'refuted: <why>' or 'confirmed: <why>', citing the file:line or command " \
      "output that decided it.\n\n" +
      <<~PROMPT.chomp
        Subtask: #{subtask}

        Result to verify:
        #{result}
      PROMPT
  end

  # 동시성 제한이 있는 map: 한 번에 최대 `limit`개의 스레드만 실행됩니다.
  def map_with_limit(items, limit)
    results = Array.new(items.length)
    queue = Queue.new
    items.each_with_index { |item, index| queue << [index, item] }
    workers = Array.new([limit, items.length].min) do
      Thread.new do
        until queue.empty?
          index, item = queue.pop(true) rescue break
          results[index] = yield item
        end
      end
    end
    workers.each(&:join)
    results
  end

  # 하위 작업을 병렬 서브에이전트로 실행한 뒤, 결과에 대해 두 번째 검증 단계를
  # 실행하고 둘 다 반환합니다. MAX_TOTAL_SUBTASKS는 모델이 대기열에 넣을 수 있는 수를,
  # MAX_CONCURRENT는 동시에 실행되는 수를 제한합니다.
  def run_workflow(model, raw_subtasks)
    all_subtasks = normalize_subtasks(raw_subtasks)
    subtasks = all_subtasks.first(MAX_TOTAL_SUBTASKS)
    dropped = all_subtasks.length - subtasks.length
    return ["Workflow error: no usable subtasks were provided.", true] if subtasks.empty?

    warn "[workflow] fanning out #{subtasks.length} agents"
    run_one = lambda do |prompt|
      journaled(prompt) { run_subagent(model, prompt) }
    rescue => error # isolation boundary: one bad subagent should not end the run
      "(subagent failed: #{error.class}: #{error.message})"
    end

    results = map_with_limit(subtasks, MAX_CONCURRENT, &run_one)
    warn "[workflow] verifying #{results.length} results"
    verify_prompts = subtasks.zip(results).map { |task, result| verify_prompt_for(task, result) }
    verdicts = map_with_limit(verify_prompts, MAX_CONCURRENT, &run_one)

    joined = subtasks.each_with_index.map do |task, index|
      <<~ENTRY.chomp
        [agent #{index + 1}: #{task}]
        #{results[index]}

        [verify #{index + 1}]
        #{verdicts[index]}
      ENTRY
    end.join("\n\n")
    if dropped > 0
      joined =
        "(note: #{dropped} subtasks beyond MAX_TOTAL_SUBTASKS=#{MAX_TOTAL_SUBTASKS} were not " \
        "run; rerun them in a follow-up Workflow call)\n\n#{joined}"
    end
    [joined, false]
  end
  ```
</CodeGroup>

## 대화 중 시스템 메시지로 모드 전환하기

에이전트는 먼저 사용자의 메시지를 추가한 다음, 전송할 시점이 된 시스템 메시지를 추가합니다. 종료 알림, 진입 시의 전체 모드 텍스트, 또는 주기적 리프레셔가 이에 해당합니다. 시스템 메시지를 사용자 턴 뒤에 배치하면 그 앞의 모든 캐시된 바이트가 그대로 유지되며, 시스템 메시지가 사용자 턴 뒤에 와야 한다는 배치 규칙도 충족합니다.

<CodeGroup>
  ```bash cURL
  # 오케스트레이션 모드 한 턴: 모드 리마인더는 messages 배열에서
  # {"role": "system"} 항목으로 적용 대상 사용자 턴 뒤에 배치됩니다. 응답은
  # 첫 번째 도구 호출에서 멈춥니다. 도구 호출을 실행하고 서브에이전트를
  # 분산하는 에이전트 루프는 SDK 탭에 나와 있으며, Workflow 설명은 여기서는 축약되어
  # 있고 SDK 예제에 전체 상시 동의(standing-consent) 텍스트가 포함되어 있습니다.
  curl --fail-with-body -sS https://api.anthropic.com/v1/messages \
    -H "x-api-key: $ANTHROPIC_API_KEY" \
    -H "anthropic-version: 2023-06-01" \
    -H "content-type: application/json" \
    -d '{
      "model": "claude-opus-5-5",
      "max_tokens": 64000,
      "system": "You are a helpful general-purpose agent. Answer the request from the user directly.",
      "output_config": {"effort": "xhigh"},
      "tools": [
        {
          "name": "Workflow",
          "description": "Orchestrate a multiagent workflow: split a large task into independent subtasks and run them as parallel agents, then collect their results. Opt-in: only use this tool when the user explicitly asks for a workflow, or when a system message confirms that orchestration mode is on. Granularity: scope each subtask to a distinct concern rather than per line or file section; a review of a small module rarely needs more than about ten. Standing consent: while a system message confirms orchestration mode is on, author and run a workflow for every substantive task by default; work solo only on conversational turns or trivial mechanical edits.",
          "input_schema": {
            "type": "object",
            "properties": {
              "subtasks": {
                "type": "array",
                "items": {"type": "string"},
                "description": "Independent subtask prompts to run as parallel agents"
              }
            },
            "required": ["subtasks"]
          }
        },
        {"type": "bash_20250124", "name": "bash"}
      ],
      "messages": [
        {
          "role": "user",
          "content": "Explore the current directory, then give a thorough review: what it does, code-quality issues, and concrete improvements."
        },
        {
          "role": "system",
          "content": "Orchestration mode is on: optimize for the most exhaustive, correct answer rather than the fastest one. Use the Workflow tool on every substantive task, sized to the natural decomposition of the problem rather than the maximum the tool allows. See the description of the Workflow tool for standing consent, granularity guidance, and quality patterns. Work solo only on conversational or trivial turns."
        }
      ]
    }'
  ```

  ```bash CLI
  # 오케스트레이션 모드 턴 하나: 모드 리마인더는 messages 배열에서
  # 적용 대상인 사용자 턴 뒤에 놓인 system 역할 항목으로 전달됩니다. 응답은
  # 첫 번째 도구 호출에서 멈춥니다. 도구 호출을 실행하고 서브에이전트를
  # 분산 실행하는 에이전트 루프는 SDK 탭에 나와 있으며, Workflow 설명은 여기에 축약되어 있고,
  # 전체 상시 동의(standing-consent) 텍스트는 SDK 예제에 있습니다.
  ant messages create <<'YAML'
  model: claude-opus-5-5
  max_tokens: 64000
  system: You are a helpful general-purpose agent. Answer the user's request directly.
  output_config: {effort: xhigh}
  tools:
    - name: Workflow
      description: >-
        Orchestrate a multiagent workflow: split a large task into independent
        subtasks and run them as parallel agents, then collect their results.
        Opt-in: only use this tool when the user explicitly asks for a workflow,
        or when a system message confirms that orchestration mode is on.
        Granularity: scope each subtask to a distinct concern rather than per
        line or file section; a review of a small module rarely needs more than
        about ten. Standing consent: while a system message confirms
        orchestration mode is on, author and run a workflow for every
        substantive task by default; work solo only on conversational turns or
        trivial mechanical edits.
      input_schema:
        type: object
        properties:
          subtasks:
            type: array
            items: {type: string}
            description: Independent subtask prompts to run as parallel agents
        required: [subtasks]
    - {type: bash_20250124, name: bash}
  messages:
    - role: user
      content: >-
        Explore the current directory, then give a thorough review: what it
        does, code-quality issues, and concrete improvements.
    - role: system
      content: >-
        Orchestration mode is on: optimize for the most exhaustive, correct
        answer rather than the fastest one. Use the Workflow tool on every
        substantive task, sized to the problem's natural decomposition rather
        than the maximum the tool allows. See the Workflow tool's description
        for standing consent, granularity guidance, and quality patterns. Work
        solo only on conversational or trivial turns.
  YAML
  ```

  ```python Python
  class ModeAgent:
      """An agent loop whose orchestration mode is toggled with mid-conversation system messages."""

      def __init__(self, model: str, mode_on: bool = True):
          self.model = model
          self.mode_on = mode_on
          self.messages: list[dict] = []
          self._mode_announced = False
          self._exit_pending = False
          self._turns_since_reminder = 0

      def set_mode(self, mode_on: bool) -> None:
          """Turn the mode on or off. The notice is delivered with the next user turn."""
          if mode_on == self.mode_on:
              return
          if not mode_on:
              if self._mode_announced:
                  self._exit_pending = True
          else:
              self._exit_pending = False
          self.mode_on = mode_on

      def _due_system_messages(self) -> list[dict]:
          """System messages owed on this turn: an exit notice, the full mode text on entry,
          or a one-line refresher every TURNS_BETWEEN_REFRESHERS user turns."""
          due = []
          if self._exit_pending:
              self._exit_pending = False
              self._mode_announced = False
              due.append({"role": "system", "content": MODE_EXIT})
          if self.mode_on:
              if not self._mode_announced:
                  self._mode_announced = True
                  self._turns_since_reminder = 0
                  due.append({"role": "system", "content": MODE_ENTER})
              elif self._turns_since_reminder >= TURNS_BETWEEN_REFRESHERS:
                  self._turns_since_reminder = 0
                  due.append({"role": "system", "content": MODE_REFRESH})
          return due

      def turn(self, user_input: str) -> str:
          # 대화 중간의 시스템 메시지는 적용 대상인 사용자 턴 뒤에 오므로
          # 그 앞의 캐시된 프리픽스가 그대로 유지됩니다.
          self.messages.append({"role": "user", "content": user_input})
          self.messages.extend(self._due_system_messages())
          self._turns_since_reminder += 1

          for _ in range(MAX_MAIN_TURNS):
              with client.messages.stream(
                  model=self.model,
                  max_tokens=64000,
                  system=SYSTEM_PROMPT,  # static for the whole session
                  output_config={"effort": EFFORT},
                  tools=[WORKFLOW_TOOL, BASH_TOOL],
                  messages=self.messages,
                  timeout=REQUEST_TIMEOUT_SECONDS,
              ) as stream:
                  response = stream.get_final_message()
              self.messages.append({"role": "assistant", "content": response.content})

              if response.stop_reason == "pause_turn":
                  continue
              if response.stop_reason != "tool_use":
                  text = "".join(
                      block.text for block in response.content if block.type == "text"
                  )
                  if response.stop_reason == "max_tokens":
                      # 이후 턴이 잘린 어시스턴트 메시지를 기반으로 하지 않도록 해당 메시지를 제거합니다.
                      self.messages.pop()
                      text += "\n\n(warning: response was truncated at max_tokens)"
                  return text

              tool_results = []
              for block in response.content:
                  if block.type != "tool_use":
                      continue
                  match block.name:
                      case "Workflow":
                          output, is_error = run_workflow(
                              self.model, block.input.get("subtasks", [])
                          )
                      case "bash":
                          output, is_error = handle_bash_block(block)
                      case _:
                          output, is_error = f"unknown tool: {block.name}", True
                  tool_results.append(
                      {
                          "type": "tool_result",
                          "tool_use_id": block.id,
                          "content": output,
                          "is_error": is_error,
                      }
                  )
              self.messages.append({"role": "user", "content": tool_results})
          return "(hit the main loop turn limit before finishing)"
  ```

  ```typescript TypeScript
  // 대화 중간 시스템 메시지로 오케스트레이션 모드를 전환하는 에이전트 루프입니다.
  class ModeAgent {
    readonly #model: string;
    readonly #messages: Anthropic.MessageParam[] = [];
    #modeOn: boolean;
    #modeAnnounced = false;
    #exitPending = false;
    #turnsSinceReminder = 0;

    constructor(model: string, modeOn = true) {
      this.#model = model;
      this.#modeOn = modeOn;
    }

    // 모드를 켜거나 끕니다. 알림은 다음 사용자 턴과 함께 전달됩니다.
    setMode(modeOn: boolean): void {
      if (modeOn === this.#modeOn) {
        return;
      }
      // 종료 알림은 모델에 모드가 켜졌다고 알린 경우에만 필요합니다.
      this.#exitPending = !modeOn && this.#modeAnnounced;
      this.#modeOn = modeOn;
    }

    // 이번 턴에 보내야 할 시스템 메시지: 종료 알림, 진입 시 전체 모드 텍스트,
    // 또는 TURNS_BETWEEN_REFRESHERS 사용자 턴마다 한 줄짜리 리마인더.
    #dueSystemMessages(): Anthropic.MessageParam[] {
      const due: Anthropic.MessageParam[] = [];
      if (this.#exitPending) {
        this.#exitPending = false;
        this.#modeAnnounced = false;
        due.push({ role: "system", content: MODE_EXIT });
      }
      if (this.#modeOn) {
        if (!this.#modeAnnounced) {
          this.#modeAnnounced = true;
          this.#turnsSinceReminder = 0;
          due.push({ role: "system", content: MODE_ENTER });
        } else if (this.#turnsSinceReminder >= TURNS_BETWEEN_REFRESHERS) {
          this.#turnsSinceReminder = 0;
          due.push({ role: "system", content: MODE_REFRESH });
        }
      }
      return due;
    }

    async turn(userInput: string): Promise<string> {
      // 대화 중간 시스템 메시지는 적용 대상인 사용자 턴 뒤에 오므로, 그 앞의 캐시된
      // 접두사(prefix)는 그대로 유지됩니다.
      this.#messages.push({ role: "user", content: userInput }, ...this.#dueSystemMessages());
      this.#turnsSinceReminder += 1;

      for (let turn = 0; turn < MAX_MAIN_TURNS; turn++) {
        const response = await client.messages
          .stream(
            {
              model: this.#model,
              max_tokens: 64000,
              system: SYSTEM_PROMPT, // static for the whole session
              output_config: { effort: EFFORT },
              tools: [WORKFLOW_TOOL, BASH_TOOL],
              messages: this.#messages
            },
            { signal: AbortSignal.timeout(REQUEST_TIMEOUT_SECONDS * 1000) }
          )
          .finalMessage();
        this.#messages.push({ role: "assistant", content: response.content });

        if (response.stop_reason === "pause_turn") {
          continue;
        }
        if (response.stop_reason !== "tool_use") {
          let text = response.content
            .flatMap((block) => (block.type === "text" ? [block.text] : []))
            .join("");
          if (response.stop_reason === "max_tokens") {
            // 잘린 어시스턴트 메시지를 제거하여 이후 턴이 이를 기반으로 하지 않도록 합니다.
            this.#messages.pop();
            text += "\n\n(warning: response was truncated at max_tokens)";
          }
          return text;
        }

        const toolResults: Anthropic.ToolResultBlockParam[] = [];
        for (const block of response.content) {
          if (block.type !== "tool_use") {
            continue;
          }
          let outcome: ToolOutcome;
          switch (block.name) {
            case "Workflow": {
              const { subtasks } = block.input as { subtasks?: unknown };
              outcome = await runWorkflow(this.#model, subtasks);
              break;
            }
            case "bash":
              outcome = await handleBashBlock(block);
              break;
            default:
              outcome = { output: `unknown tool: ${block.name}`, isError: true };
          }
          toolResults.push({
            type: "tool_result",
            tool_use_id: block.id,
            content: outcome.output,
            is_error: outcome.isError
          });
        }
        this.#messages.push({ role: "user", content: toolResults });
      }
      return "(hit the main loop turn limit before finishing)";
    }
  }
  ```

  ```csharp C#
  // 대화 중 시스템 메시지로 오케스트레이션 모드를 전환하는 에이전트 루프.
  List<MessageParam> messages = [];
  var modeOn = true;
  var modeAnnounced = false;
  var exitPending = false;
  var turnsSinceReminder = 0;

  // 모드를 켜거나 끕니다. 알림은 다음 사용자 턴과 함께 전달됩니다.
  void SetMode(bool nextModeOn)
  {
      if (nextModeOn == modeOn)
      {
          return;
      }
      // 종료 알림은 모델에게 모드가 켜졌다고 알린 경우에만 필요합니다.
      exitPending = !nextModeOn && modeAnnounced;
      modeOn = nextModeOn;
  }

  MessageParam SystemMessage(string content) => new() { Role = Role.System, Content = content };

  // 이번 턴에 전달해야 할 시스템 메시지: 종료 알림, 진입 시 전체 모드 텍스트,
  // 또는 turnsBetweenRefreshers 사용자 턴마다 한 줄짜리 리마인더.
  List<MessageParam> DueSystemMessages()
  {
      List<MessageParam> due = [];
      if (exitPending)
      {
          exitPending = false;
          modeAnnounced = false;
          due.Add(SystemMessage(modeExit));
      }
      if (modeOn)
      {
          if (!modeAnnounced)
          {
              modeAnnounced = true;
              turnsSinceReminder = 0;
              due.Add(SystemMessage(modeEnter));
          }
          else if (turnsSinceReminder >= turnsBetweenRefreshers)
          {
              turnsSinceReminder = 0;
              due.Add(SystemMessage(modeRefresh));
          }
      }
      return due;
  }

  // 사용자 턴 하나를 루프로 보내고, 모델이 멈출 때까지 도구 호출을 실행합니다.
  async Task<string> Turn(string userInput)
  {
      // 대화 중 시스템 메시지는 적용되는 사용자 턴 뒤에 오므로, 그 앞의 캐시된
      // 프리픽스는 그대로 유지됩니다.
      messages.Add(new() { Role = Role.User, Content = userInput });
      messages.AddRange(DueSystemMessages());
      turnsSinceReminder++;

      for (var turn = 0; turn < maxMainTurns; turn++)
      {
          using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(requestTimeoutSeconds));
          var response = await client.Messages.CreateStreaming(new MessageCreateParams
          {
              Model = model,
              MaxTokens = 64000,
              System = systemPrompt, // static for the whole session
              OutputConfig = new OutputConfig { Effort = effort },
              Tools = [workflowTool, bashTool],
              Messages = messages,
          }, cancellationToken: deadline.Token).Aggregate();
          messages.Add(new()
          {
              Role = Role.Assistant,
              Content = response.Content.Select(block => new ContentBlockParam(block.Json)).ToList(),
          });

          if (response.StopReason == StopReason.PauseTurn)
          {
              continue;
          }
          if (response.StopReason != StopReason.ToolUse)
          {
              var text = string.Concat(
                  response.Content.Select(block => block.TryPickText(out var textBlock) ? textBlock.Text : ""));
              if (response.StopReason == StopReason.MaxTokens)
              {
                  // 이후 턴이 잘린 어시스턴트 메시지를 기반으로 하지 않도록 해당 메시지를 제거합니다.
                  messages.RemoveAt(messages.Count - 1);
                  text += "\n\n(warning: response was truncated at max_tokens)";
              }
              return text;
          }

          List<ContentBlockParam> toolResults = [];
          foreach (var block in response.Content)
          {
              if (!block.TryPickToolUse(out var toolUse))
              {
                  continue;
              }
              var (output, isError) = toolUse.Name switch
              {
                  "Workflow" => await RunWorkflow(toolUse.Input.GetValueOrDefault("subtasks")),
                  "bash" => await HandleBashBlock(toolUse),
                  _ => ($"unknown tool: {toolUse.Name}", true),
              };
              toolResults.Add(new ToolResultBlockParam(toolUse.ID) { Content = output, IsError = isError });
          }
          messages.Add(new() { Role = Role.User, Content = toolResults });
      }
      return "(hit the main loop turn limit before finishing)";
  }
  ```

  ```go Go
  // modeAgent는 대화 중 시스템 메시지로 오케스트레이션 모드를 전환하는
  // 에이전트 루프입니다.
  type modeAgent struct {
  	model              string
  	modeOn             bool
  	messages           []anthropic.MessageParam
  	modeAnnounced      bool
  	exitPending        bool
  	turnsSinceReminder int
  }

  func newModeAgent(model string) *modeAgent {
  	return &modeAgent{model: model, modeOn: true}
  }

  // setMode는 모드를 켜거나 끕니다. 알림은 다음 사용자 턴과 함께 전달됩니다.
  func (agent *modeAgent) setMode(modeOn bool) {
  	if modeOn == agent.modeOn {
  		return
  	}
  	if !modeOn {
  		if agent.modeAnnounced {
  			agent.exitPending = true
  		}
  	} else {
  		agent.exitPending = false
  	}
  	agent.modeOn = modeOn
  }

  // dueSystemMessages는 이번 턴에 전달해야 할 시스템 메시지를 반환합니다: 종료 알림,
  // 진입 시 전체 모드 텍스트, 또는 turnsBetweenRefreshers 사용자 턴마다 한 줄 리마인더.
  func (agent *modeAgent) dueSystemMessages() []anthropic.MessageParam {
  	systemMessage := func(content string) anthropic.MessageParam {
  		return anthropic.MessageParam{
  			Role:    anthropic.MessageParamRoleSystem,
  			Content: []anthropic.ContentBlockParamUnion{anthropic.NewTextBlock(content)},
  		}
  	}
  	var due []anthropic.MessageParam
  	if agent.exitPending {
  		agent.exitPending = false
  		agent.modeAnnounced = false
  		due = append(due, systemMessage(modeExit))
  	}
  	if agent.modeOn {
  		if !agent.modeAnnounced {
  			agent.modeAnnounced = true
  			agent.turnsSinceReminder = 0
  			due = append(due, systemMessage(modeEnter))
  		} else if agent.turnsSinceReminder >= turnsBetweenRefreshers {
  			agent.turnsSinceReminder = 0
  			due = append(due, systemMessage(modeRefresh))
  		}
  	}
  	return due
  }

  // turn은 사용자 턴 하나를 루프에 보내 모델이 멈출 때까지 도구 호출을 실행합니다.
  func (agent *modeAgent) turn(ctx context.Context, userInput string) (string, error) {
  	// 대화 중 시스템 메시지는 적용되는 사용자 턴 뒤에 오므로, 그 앞의 캐시된
  	// 프리픽스가 그대로 유지됩니다.
  	agent.messages = append(agent.messages, anthropic.NewUserMessage(anthropic.NewTextBlock(userInput)))
  	agent.messages = append(agent.messages, agent.dueSystemMessages()...)
  	agent.turnsSinceReminder++

  	for range maxMainTurns {
  		response, err := streamMessage(ctx, anthropic.MessageNewParams{
  			Model:        agent.model,
  			MaxTokens:    64000,
  			System:       []anthropic.TextBlockParam{{Text: systemPrompt}}, // static for the whole session
  			OutputConfig: anthropic.OutputConfigParam{Effort: effort},
  			Tools:        []anthropic.ToolUnionParam{workflowTool, bashTool},
  			Messages:     agent.messages,
  		})
  		if err != nil {
  			return "", err
  		}
  		agent.messages = append(agent.messages, response.ToParam())

  		switch response.StopReason {
  		case anthropic.StopReasonPauseTurn:
  			continue
  		case anthropic.StopReasonMaxTokens:
  			// 이후 턴이 이를 기반으로 하지 않도록 잘린 어시스턴트 메시지를 제거합니다.
  			agent.messages = agent.messages[:len(agent.messages)-1]
  			return textOf(response) + "\n\n(warning: response was truncated at max_tokens)", nil
  		case anthropic.StopReasonToolUse:
  		default:
  			return textOf(response), nil
  		}

  		var toolResults []anthropic.ContentBlockParamUnion
  		for _, block := range response.Content {
  			toolUse, ok := block.AsAny().(anthropic.ToolUseBlock)
  			if !ok {
  				continue
  			}
  			var output string
  			var isError bool
  			switch toolUse.Name {
  			case "Workflow":
  				var input struct {
  					Subtasks json.RawMessage `json:"subtasks"`
  				}
  				if err := json.Unmarshal(toolUse.Input, &input); err != nil {
  					output, isError = fmt.Sprintf("Workflow error: could not parse input: %s", err), true
  				} else {
  					output, isError = runWorkflow(ctx, agent.model, input.Subtasks)
  				}
  			case "bash":
  				output, isError = handleBashBlock(ctx, toolUse)
  			default:
  				output, isError = "unknown tool: "+toolUse.Name, true
  			}
  			toolResults = append(toolResults, anthropic.NewToolResultBlock(toolUse.ID, output, isError))
  		}
  		agent.messages = append(agent.messages, anthropic.NewUserMessage(toolResults...))
  	}
  	return "(hit the main loop turn limit before finishing)", nil
  }
  ```

  ```java Java
  // 대화 중 시스템 메시지로 오케스트레이션 모드를 전환하는 에이전트 루프.
  class ModeAgent {
      private final Model model;
      private final List<MessageParam> messages = new ArrayList<>();
      private boolean modeOn;
      private boolean modeAnnounced;
      private boolean exitPending;
      private int turnsSinceReminder;

      ModeAgent(Model model) {
          this(model, true);
      }

      ModeAgent(Model model, boolean modeOn) {
          this.model = model;
          this.modeOn = modeOn;
      }

      // 모드를 켜거나 끕니다. 알림은 다음 사용자 턴과 함께 전달됩니다:
      // 종료 알림은 모델이 모드가 켜졌다고 안내받은 경우에만 필요합니다.
      void setMode(boolean modeOn) {
          if (modeOn == this.modeOn) {
              return;
          }
          exitPending = !modeOn && modeAnnounced;
          this.modeOn = modeOn;
      }

      // 이번 턴에 전달해야 할 시스템 메시지: 종료 알림, 진입 시 전체 모드 텍스트,
      // 또는 TURNS_BETWEEN_REFRESHERS 사용자 턴마다 한 줄짜리 리마인더.
      private List<MessageParam> dueSystemMessages() {
          List<MessageParam> due = new ArrayList<>();
          if (exitPending) {
              exitPending = false;
              modeAnnounced = false;
              due.add(systemMessage(MODE_EXIT));
          }
          if (modeOn) {
              if (!modeAnnounced) {
                  modeAnnounced = true;
                  turnsSinceReminder = 0;
                  due.add(systemMessage(MODE_ENTER));
              } else if (turnsSinceReminder >= TURNS_BETWEEN_REFRESHERS) {
                  turnsSinceReminder = 0;
                  due.add(systemMessage(MODE_REFRESH));
              }
          }
          return due;
      }

      private MessageParam systemMessage(String content) {
          return MessageParam.builder()
                  .role(MessageParam.Role.SYSTEM)
                  .content(content)
                  .build();
      }

      // 사용자 턴 하나를 루프로 보내고, 모델이 멈출 때까지 도구 호출을 실행합니다.
      String turn(String userInput) throws InterruptedException {
          // 대화 중 시스템 메시지는 적용 대상인 사용자 턴 뒤에 오므로, 그 앞의
          // 캐시된 접두사는 그대로 유지됩니다.
          messages.add(MessageParam.builder().role(MessageParam.Role.USER).content(userInput).build());
          messages.addAll(dueSystemMessages());
          turnsSinceReminder++;

          for (int turn = 0; turn < MAX_MAIN_TURNS; turn++) {
              MessageCreateParams params = MessageCreateParams.builder()
                      .model(model)
                      .maxTokens(64000L)
                      .system(SYSTEM_PROMPT) // static for the whole session
                      .outputConfig(OutputConfig.builder().effort(EFFORT).build())
                      .addTool(WORKFLOW_TOOL)
                      .addTool(BASH_TOOL)
                      .messages(messages)
                      .build();
              MessageAccumulator accumulator = MessageAccumulator.create();
              try (var stream = client.messages().createStreaming(params, REQUEST_OPTIONS)) {
                  stream.stream().forEach(accumulator::accumulate);
              }
              Message response = accumulator.message();
              messages.add(response.toParam());

              StopReason stopReason = response.stopReason().orElse(null);
              if (StopReason.PAUSE_TURN.equals(stopReason)) {
                  continue;
              }
              if (!StopReason.TOOL_USE.equals(stopReason)) {
                  String text = response.content().stream()
                          .flatMap(block -> block.text().stream())
                          .map(TextBlock::text)
                          .collect(Collectors.joining());
                  if (StopReason.MAX_TOKENS.equals(stopReason)) {
                      // 이후 턴이 이를 기반으로 하지 않도록 잘린 어시스턴트 메시지를 제거합니다.
                      messages.removeLast();
                      text += "\n\n(warning: response was truncated at max_tokens)";
                  }
                  return text;
              }

              List<ToolUseBlock> toolUses = response.content().stream()
                      .flatMap(block -> block.toolUse().stream())
                      .toList();
              List<ContentBlockParam> toolResults = new ArrayList<>();
              for (ToolUseBlock toolUse : toolUses) {
                  ToolOutput result = switch (toolUse.name()) {
                      case "Workflow" -> runWorkflow(model,
                              toolInput(toolUse).getOrDefault("subtasks", JsonValue.from(List.of())));
                      case "bash" -> handleBashBlock(toolUse);
                      default -> new ToolOutput("unknown tool: " + toolUse.name(), true);
                  };
                  toolResults.add(ContentBlockParam.ofToolResult(ToolResultBlockParam.builder()
                          .toolUseId(toolUse.id())
                          .content(result.output())
                          .isError(result.isError())
                          .build()));
              }
              messages.add(MessageParam.builder()
                      .role(MessageParam.Role.USER)
                      .contentOfBlockParams(toolResults)
                      .build());
          }
          return "(hit the main loop turn limit before finishing)";
      }
  }
  ```

  ```php PHP
  /** An agent loop whose orchestration mode is toggled with mid-conversation system messages. */
  class ModeAgent
  {
      private array $messages = [];
      private bool $modeAnnounced = false;
      private bool $exitPending = false;
      private int $turnsSinceReminder = 0;

      public function __construct(
          private readonly Client $client,
          private readonly string $model,
          private bool $modeOn = true,
      ) {
      }

      /** Turn the mode on or off. The notice is delivered with the next user turn. */
      public function setMode(bool $modeOn): void
      {
          if ($modeOn === $this->modeOn) {
              return;
          }
          if ($modeOn) {
              $this->exitPending = false;
          } elseif ($this->modeAnnounced) {
              $this->exitPending = true;
          }
          $this->modeOn = $modeOn;
      }

      public function turn(string $userInput): string
      {
          // 대화 중간 시스템 메시지는 적용 대상인 사용자 턴 뒤에 오므로,
          // 그 앞의 캐시된 접두사가 그대로 유지됩니다.
          $this->messages[] = ['role' => 'user', 'content' => $userInput];
          array_push($this->messages, ...$this->dueSystemMessages());
          $this->turnsSinceReminder++;

          for ($turn = 0; $turn < MAX_MAIN_TURNS; $turn++) {
              $stream = $this->client->messages->createStream(
                  model: $this->model,
                  maxTokens: 64000,
                  system: SYSTEM_PROMPT, // static for the whole session
                  outputConfig: ['effort' => EFFORT],
                  tools: [WORKFLOW_TOOL, BASH_TOOL],
                  messages: $this->messages,
                  requestOptions: ['timeout' => REQUEST_TIMEOUT_SECONDS],
              );
              [$content, $stopReason] = drainMessageStream($stream);
              $this->messages[] = ['role' => 'assistant', 'content' => $content];

              if ($stopReason === 'pause_turn') {
                  continue;
              }
              if ($stopReason !== 'tool_use') {
                  $text = '';
                  foreach ($content as $block) {
                      if ($block instanceof \Anthropic\Messages\TextBlock) {
                          $text .= $block->text;
                      }
                  }
                  if ($stopReason === 'max_tokens') {
                      // 이후 턴이 이를 기반으로 하지 않도록 잘린 어시스턴트 메시지를 제거합니다.
                      array_pop($this->messages);
                      $text .= "\n\n(warning: response was truncated at max_tokens)";
                  }
                  return $text;
              }

              $toolResults = [];
              foreach ($content as $block) {
                  if (!$block instanceof \Anthropic\Messages\ToolUseBlock) {
                      continue;
                  }
                  if ($block->name === 'Workflow') {
                      [$output, $isError] =
                          runWorkflow($this->client, $this->model, $block->input['subtasks'] ?? []);
                  } elseif ($block->name === 'bash') {
                      [$output, $isError] = handleBashBlock($block);
                  } else {
                      $output = "unknown tool: {$block->name}";
                      $isError = true;
                  }
                  $toolResults[] = [
                      'type' => 'tool_result',
                      'tool_use_id' => $block->id,
                      'content' => $output,
                      'is_error' => $isError,
                  ];
              }
              $this->messages[] = ['role' => 'user', 'content' => $toolResults];
          }
          return '(hit the main loop turn limit before finishing)';
      }

      /**
       * System messages owed on this turn: an exit notice, the full mode text on entry,
       * or a one-line refresher every TURNS_BETWEEN_REFRESHERS user turns.
       */
      private function dueSystemMessages(): array
      {
          $due = [];
          if ($this->exitPending) {
              $this->exitPending = false;
              $this->modeAnnounced = false;
              $due[] = ['role' => 'system', 'content' => MODE_EXIT];
          }
          if ($this->modeOn) {
              if (!$this->modeAnnounced) {
                  $this->modeAnnounced = true;
                  $this->turnsSinceReminder = 0;
                  $due[] = ['role' => 'system', 'content' => MODE_ENTER];
              } elseif ($this->turnsSinceReminder >= TURNS_BETWEEN_REFRESHERS) {
                  $this->turnsSinceReminder = 0;
                  $due[] = ['role' => 'system', 'content' => MODE_REFRESH];
              }
          }
          return $due;
      }
  }
  ```

  ```ruby Ruby
  # 대화 중간 시스템 메시지로 오케스트레이션 모드를 전환하는 에이전트 루프입니다.
  class ModeAgent
    def initialize(model, mode_on: true)
      @model = model
      @mode_on = mode_on
      @messages = []
      @mode_announced = false
      @exit_pending = false
      @turns_since_reminder = 0
    end

    # 모드를 켜거나 끕니다. 알림은 다음 사용자 턴과 함께 전달됩니다.
    def set_mode(mode_on)
      return if mode_on == @mode_on

      if mode_on
        @exit_pending = false
      else
        @exit_pending = true if @mode_announced
      end
      @mode_on = mode_on
    end

    def turn(user_input)
      # 대화 중간 시스템 메시지는 적용 대상인 사용자 턴 뒤에 오므로, 그 앞의 캐시된
      # 접두사(prefix)는 그대로 유지됩니다.
      @messages << {role: "user", content: user_input}
      @messages.concat(due_system_messages)
      @turns_since_reminder += 1

      MAX_MAIN_TURNS.times do
        stream = CLIENT.messages.stream(
          model: @model,
          max_tokens: 64_000,
          system_: SYSTEM_PROMPT, # static for the whole session
          output_config: {effort: EFFORT},
          tools: [WORKFLOW_TOOL, BASH_TOOL],
          messages: @messages,
          request_options: {timeout: REQUEST_TIMEOUT_SECONDS}
        )
        response = stream.accumulated_message
        @messages << {role: "assistant", content: assistant_content_param(response.content)}

        next if response.stop_reason == :pause_turn

        unless response.stop_reason == :tool_use
          text = response.content.select { |block| block.type == :text }.map(&:text).join
          if response.stop_reason == :max_tokens
            # 잘린 어시스턴트 메시지를 제거하여 이후 턴이 이를 기반으로 하지 않도록 합니다.
            @messages.pop
            text += "\n\n(warning: response was truncated at max_tokens)"
          end
          return text
        end

        tool_results = []
        response.content.each do |block|
          next unless block.type == :tool_use

          input = parse_tool_input(block.input)
          case block.name
          when "Workflow"
            output, is_error = run_workflow(@model, input["subtasks"] || [])
          when "bash"
            output, is_error = handle_bash_block(block)
          else
            output, is_error = "unknown tool: #{block.name}", true
          end
          tool_results << {
            type: "tool_result",
            tool_use_id: block.id,
            content: output,
            is_error: is_error
          }
        end
        @messages << {role: "user", content: tool_results}
      end
      "(hit the main loop turn limit before finishing)"
    end

    private

    # 이번 턴에 전달해야 할 시스템 메시지: 종료 알림, 진입 시 전체 모드 텍스트,
    # 또는 TURNS_BETWEEN_REFRESHERS 사용자 턴마다 한 줄짜리 리마인더.
    def due_system_messages
      due = []
      if @exit_pending
        @exit_pending = false
        @mode_announced = false
        due << {role: "system", content: MODE_EXIT}
      end
      if @mode_on
        if !@mode_announced
          @mode_announced = true
          @turns_since_reminder = 0
          due << {role: "system", content: MODE_ENTER}
        elsif @turns_since_reminder >= TURNS_BETWEEN_REFRESHERS
          @turns_since_reminder = 0
          due << {role: "system", content: MODE_REFRESH}
        end
      end
      due
    end
  end
  ```
</CodeGroup>

## 실행하기

<Warning>
  이 예제의 bash 도구는 모델이 작성한 명령을 샌드박스 없이 여러분의 머신에서 직접 실행하며, 팬아웃은 이러한 에이전트 여러 개를 병렬로 실행합니다. 노출해도 괜찮은 디렉터리와 환경에서 실행하고, 로컬 실험 이상의 용도로 적용하기 전에 샌드박싱을 추가하세요.
</Warning>

<CodeGroup exclude="shell:cURL, shell:CLI">
  ```python Python
  if __name__ == "__main__":
      task = (
          sys.argv[1]
          if len(sys.argv) > 1
          else "Explore the current directory, then give a thorough review: what it does, "
          "code-quality issues, and concrete improvements."
      )
      agent = ModeAgent(MODEL)
      print(agent.turn(task))
      agent.set_mode(False)
      print(agent.turn("Briefly summarize what you found above, no fan-out needed."))
  ```

  ```typescript TypeScript
  const task =
    process.argv[2] ??
    "Explore the current directory, then give a thorough review: what it does, " +
      "code-quality issues, and concrete improvements.";
  const agent = new ModeAgent(MODEL);
  console.log(await agent.turn(task));
  agent.setMode(false);
  console.log(await agent.turn("Briefly summarize what you found above, no fan-out needed."));
  ```

  ```csharp C#
  var task = args.Length > 0
      ? args[0]
      : "Explore the current directory, then give a thorough review: what it does, "
          + "code-quality issues, and concrete improvements.";
  Console.WriteLine(await Turn(task));
  SetMode(false);
  Console.WriteLine(await Turn("Briefly summarize what you found above, no fan-out needed."));
  ```

  ```go Go
  func main() {
  	if err := run(context.Background()); err != nil {
  		log.Fatal(err)
  	}
  }

  func run(ctx context.Context) error {
  	if docTestMode {
  		defer os.RemoveAll(workDir)
  	}
  	task := "Explore the current directory, then give a thorough review: what it does, " +
  		"code-quality issues, and concrete improvements."
  	if len(os.Args) > 1 {
  		task = os.Args[1]
  	}
  	agent := newModeAgent(modelID)
  	answer, err := agent.turn(ctx, task)
  	if err != nil {
  		return err
  	}
  	fmt.Println(answer)

  	agent.setMode(false)
  	summary, err := agent.turn(ctx, "Briefly summarize what you found above, no fan-out needed.")
  	if err != nil {
  		return err
  	}
  	fmt.Println(summary)
  	return nil
  }
  ```

  ```java Java
  void main(String[] args) throws InterruptedException {
      String task = args.length > 0 ? args[0] : """
              Explore the current directory, then give a thorough review: what it does, \
              code-quality issues, and concrete improvements.""";
      ModeAgent agent = new ModeAgent(MODEL);
      IO.println(agent.turn(task));
      agent.setMode(false);
      IO.println(agent.turn("Briefly summarize what you found above, no fan-out needed."));
  }
  ```

  ```php PHP
  $task = $argv[1] ??
      'Explore the current directory, then give a thorough review: what it does, '
      . 'code-quality issues, and concrete improvements.';
  $agent = new ModeAgent($client, MODEL);
  echo $agent->turn($task), PHP_EOL;
  $agent->setMode(false);
  echo $agent->turn('Briefly summarize what you found above, no fan-out needed.'), PHP_EOL;
  ```

  ```ruby Ruby
  task = ARGV[0] ||
    "Explore the current directory, then give a thorough review: what it does, " \
    "code-quality issues, and concrete improvements."
  agent = ModeAgent.new(MODEL)
  puts agent.turn(task)
  agent.set_mode(false)
  puts agent.turn("Briefly summarize what you found above, no fan-out needed.")
  ```
</CodeGroup>

에이전트가 작업하기를 원하는 디렉터리, 예를 들어 검토할 리포지토리의 루트에서 예제를 시작하세요:

```bash
python orchestration_mode.py "Review this repository for flaky tests and propose fixes."
```

모드가 켜져 있으면, 모델이 몇 가지 bash 명령으로 탐색하고, 요청받지 않아도 Workflow 도구를 디스패치하며, 서브에이전트 보고서를 최종 답변으로 종합할 것으로 예상하세요. 사소하거나 대화적인 요청은 리마인더가 지시하는 대로 단독으로 처리됩니다.

## 프로덕션 하네스를 향하여

이 예제는 의도적으로 작습니다. 실제 워크로드를 위한 하네스는 일반적으로 다음을 추가합니다:

* **샌드박스화된 오케스트레이션 스크립트:** 하위 작업 문자열의 평면적인 목록만 받는 대신, 모델이 짧은 오케스트레이션 프로그램(분기, 루프, 리듀스 단계)을 출력하게 하고 이를 격리된 인터프리터 내에서 실행합니다.
* **내구성 있는 저널링:** 로컬 JSON 파일을 프로세스 재시작 후에도 유지되고 여러 머신에 걸친 동시 쓰기에도 안전한 저장소로 교체합니다.
* **예산 강제:** Workflow 호출별이 아니라 전체 세션에 걸쳐 실행된 총 서브에이전트 수를 추적하고, 통제 불능 계획이 할당량을 소진하지 못하도록 하드 캡 초과를 거부합니다.

이 예제의 패턴(모드 리마인더, 도구 설명 내 상시 동의, 저널링, 검증 웨이브)은 변경 없이 그대로 이어지며, 이를 둘러싼 실행 기반만 더 견고해집니다.

## 관련 항목

<CardGroup cols={2}>
  <Card title="대화 중 시스템 메시지" icon="message" href="https://platform.claude.com/docs/ko/build-with-claude/mid-conversation-system-messages">
    모드 리마인더가 사용하는 메커니즘과 이것이 프롬프트 캐싱과 상호작용하는 방식.
  </Card>

  <Card title="Effort" icon="gauge" href="https://platform.claude.com/docs/ko/build-with-claude/effort">
    API가 허용하는 effort 수준과 선택 방법.
  </Card>

  <Card title="Claude와 함께하는 도구 사용" icon="wrench" href="https://platform.claude.com/docs/ko/agents-and-tools/tool-use/overview">
    도구 정의, 도구 호출 처리, 그리고 도구 결과.
  </Card>

  <Card title="Bash 도구" icon="terminal" href="https://platform.claude.com/docs/ko/agents-and-tools/tool-use/bash-tool">
    이 예제가 로컬에서 실행하는 Anthropic 정의 bash 도구.
  </Card>
</CardGroup>
