Глава 10: Задачи, координация и рои

Пределы одного потока

Глава 8 показала, как создать суб-агента — пятнадцатишаговый жизненный цикл, который строит изолированный контекст выполнения на основе определения агента. Глава 9 показала, как сделать параллельные порождения экономичными за счёт эксплуатации кэша промптов. Но создание агентов и управление агентами — это разные задачи. Эта глава разбирает вторую.

Один цикл агента — одна модель, один разговор, один инструмент за раз — способен сделать удивительно много. Он может читать файлы, редактировать код, запускать тесты, искать в интернете и рассуждать о сложных проблемах. Но у него есть потолок.

Потолок — не интеллект. Потолок — параллелизм и масштаб. Разработчику, который делает крупный рефакторинг, нужно обновить 40 файлов, прогонять тесты после каждой партии и убедиться, что ничего не сломалось. Миграция кодовой базы затрагивает фронтенд, бэкенд и слой базы данных одновременно. Тщательный код-ревью читает десятки файлов, пока тестовый набор выполняется в фоне. Это не более сложные задачи — они более широкие. Им нужна возможность делать несколько вещей одновременно, делегировать работу специалистам и координировать результат.

Ответ Claude Code на эту проблему — не один механизм, а многоуровневый стек оркестрационных паттернов, каждый из которых подходит под свой характер работы. Фоновые задачи для команд по принципу fire-and-forget. Режим координатора для иерархий «менеджер — исполнитель». Команды роя для однорангового взаимодействия. И единый коммуникационный протокол, который связывает всё это вместе.

Слой оркестрации охватывает примерно 40 файлов в tools/AgentTool/, tasks/, coordinator/, tools/SendMessageTool/ и utils/swarm/. Несмотря на этот размах, дизайн опирается на один автомат состояний, общий для всех паттернов. Понять этот автомат — абстракцию Task в Task.ts — значит получить ключ ко всему остальному.

Эта глава проводит вас по всему стеку: от базового автомата состояний задачи до самых сложных многoагентных топологий.


Автомат состояний задачи

Любая фоновая операция в Claude Code — shell-команда, суб-агент, удалённая сессия, workflow-скрипт — отслеживается как задача (task). Абстракция задачи живёт в Task.ts и даёт унифицированную модель состояния, на которую опирается остальной слой оркестрации.

Семь типов

Система определяет семь типов задач, каждый из которых представляет отдельную модель выполнения:

Семь типов задач: local_bash (фоновые shell-команды), local_agent (фоновые суб-агенты), remote_agent (удалённые сессии), in_process_teammate (одноранговые члены роя), local_workflow (выполнение workflow-скриптов), monitor_mcp (мониторы MCP-серверов) и dream (спекулятивное фоновое мышление).

local_bash и local_agent — рабочие лошадки: соответственно фоновые shell-команды и фоновые суб-агенты. in_process_teammate — примитив роя. remote_agent мостит путь к удалённым средам Claude Code Runtime. local_workflow запускает многошаговые скрипты. monitor_mcp следит за здоровьем MCP-серверов. dream — самый необычный тип: фоновая задача, которая позволяет агенту спекулятивно думать, пока он ждёт ввода пользователя.

Каждому типу назначается односимвольный префикс ID для мгновенной визуальной идентификации:

ТипПрефиксПример ID
local_bashbb4k2m8x1
local_agentaa7j3n9p2
remote_agentrr1h5q6w4
in_process_teammatett3f8s2v5
local_workflowww6c9d4y7
monitor_mcpmm2g7k1z8
dreamdd5b4n3r6

ID задач используют односимвольный префикс (a для агентов, b для bash, t для товарищей по рою и т. д.), за которым следуют 8 случайных буквенно-цифровых символов из алфавита, безопасного для смешанного регистра (цифры плюс строчные буквы). Это даёт примерно 2,8 триллиона комбинаций — достаточно, чтобы устоять против атак перебора symlink на файлы вывода задач на диске.

Когда вы видите a7j3n9p2 в строке лога, вы сразу понимаете, что это фоновый агент. Когда видите b4k2m8x1 — shell-команда. Префикс — микрооптимизация для человека, но в системе, где могут одновременно работать десятки задач, это важно.

Пять статусов

Жизненный цикл — простой ориентированный граф без циклов:

pending — короткое состояние между регистрацией и первым выполнением. running означает, что задача активно работает. Три конечных состояния — completed (успех), failed (ошибка) и killed (явно остановлена пользователем, координатором или сигналом прерывания). Вспомогательная функция защищает от взаимодействия с мёртвыми задачами:

export function isTerminalTaskStatus(status: TaskStatus): boolean {
  return status === 'completed' || status === 'failed' || status === 'killed'
}

Эта функция встречается повсюду — в защитах от внедрения сообщений, в логике эвакуации, в очистке осиротевших задач и в маршрутизации SendMessage, которая решает, поставить сообщение в очередь или возобновить мёртвого агента.

Базовое состояние

Каждое состояние задачи расширяет TaskStateBase, который несёт поля, общие для всех семи типов:

export type TaskStateBase = {
  id: string              // Случайный ID с префиксом
  type: TaskType          // Дискриминатор
  status: TaskStatus      // Текущая позиция в жизненном цикле
  description: string     // Человекочитаемое описание
  toolUseId?: string      // Блок `tool_use`, породивший эту задачу
  startTime: number       // Временная метка создания
  endTime?: number        // Временная метка конечного состояния
  totalPausedMs?: number  // Накопленное время пауз
  outputFile: string      // Путь на диске для потокового вывода
  outputOffset: number    // Позиция чтения для инкрементального вывода
  notified: boolean       // Было ли завершение сообщено родителю
}

Два поля особенно важны. outputFile — мост между асинхронным выполнением и разговором родителя: каждая задача пишет вывод в файл на диске, а родитель может читать его инкрементально через outputOffset. notified предотвращает дублирование сообщений о завершении; как только родителю сообщили, что задача закончилась, флаг становится true, и уведомление больше не отправляется. Без этой защиты задача, завершившаяся между двумя последовательными опросами очереди уведомлений, породила бы дубликаты и сбила бы модель с толку, заставив её думать, что завершились две задачи, когда завершилась одна.

Состояние задачи агента

LocalAgentTaskState — самый сложный вариант: он содержит всё, что нужно для управления полным жизненным циклом фонового суб-агента:

export type LocalAgentTaskState = TaskStateBase & {
  type: 'local_agent'
  agentId: string
  prompt: string
  selectedAgent?: AgentDefinition
  agentType: string
  model?: string
  abortController?: AbortController
  pendingMessages: string[]       // Поставлены в очередь через SendMessage
  isBackgrounded: boolean         // Была ли это изначально foreground-задача?
  retain: boolean                 // UI удерживает эту задачу
  diskLoaded: boolean             // Sidechain-транскрипт загружен
  evictAfter?: number             // Дедлайн GC
  progress?: AgentProgress
  lastReportedToolCount: number
  lastReportedTokenCount: number
  // ... дополнительные поля жизненного цикла
}

Три поля раскрывают важные решения дизайна. pendingMessages — это inbox: когда SendMessage адресует работающему агенту, сообщение ставится в очередь сюда, а не внедряется немедленно. Сообщения извлекаются на границах tool-round, что сохраняет структуру хода агента. isBackgrounded отличает агентов, которые изначально были асинхронными, от тех, кто стартовал как foreground-синхронные и позже был отправлен в фон нажатием клавиши пользователем. evictAfter — механизм сборки мусора: завершённые задачи, которые не удерживаются UI, получают льготный период, прежде чем их состояние будет очищено из памяти.

Все состояния задач хранятся в AppState.tasks как Record<string, TaskState>, индексируемый по префиксному ID. Это плоская карта, а не дерево — система не моделирует отношения родитель/потомок в хранилище состояния. Отношение родитель/потомок неявно следует из потока разговора: родитель держит toolUseId, который породил дочернюю задачу.

Реестр задач

Каждый тип задачи поддерживается объектом Task с минимальным интерфейсом:

export type Task = {
  name: string
  type: TaskType
  kill(taskId: string, setAppState: SetAppState): Promise<void>
}

Реестр собирает все реализации задач:

export function getAllTasks(): Task[] {
  return [
    LocalShellTask,
    LocalAgentTask,
    RemoteAgentTask,
    DreamTask,
    ...(LocalWorkflowTask ? [LocalWorkflowTask] : []),
    ...(MonitorMcpTask ? [MonitorMcpTask] : []),
  ]
}

Заметьте условное включение: LocalWorkflowTask и MonitorMcpTask feature-gated и могут отсутствовать во время выполнения. Интерфейс Task намеренно минимален. Более ранние версии включали методы spawn() и render(), но их убрали, когда стало ясно, что spawning и rendering никогда не вызывались полиморфно. У каждого типа задачи своя логика запуска, своё управление состоянием и своё рендеринг-представление. Единственная операция, которой действительно нужен диспетчер по типу, — это kill(), и поэтому именно её требует интерфейс.

Это пример эволюции интерфейса через вычитание. Изначально дизайн предполагал, что все типы задач будут разделять общий lifecycle-интерфейс. На практике типы разошлись настолько, что общий интерфейс стал фикцией — spawn() для shell-команды и spawn() для in-process товарища почти не имеют ничего общего. Вместо того чтобы поддерживать протекающую абстракцию, команда убрала всё, кроме того метода, который действительно выигрывает от полиморфизма.


Паттерны коммуникации

Фоновая задача полезна только тогда, когда родитель может наблюдать её прогресс и получать результат. Claude Code поддерживает три канала связи, каждый из которых оптимизирован под свой способ доступа.

Foreground: цепочка генераторов

Когда агент работает синхронно, родитель напрямую итерирует его async-генератор runAgent(), пробрасывая каждое сообщение обратно вверх по стеку вызовов. Интересный механизм здесь — аварийный выход в фон: sync-loop устраивает гонку между «следующее сообщение от агента» и «сигналом на фоне»:

const agentIterator = runAgent({ ...params })[Symbol.asyncIterator]()

while (true) {
  const nextMessagePromise = agentIterator.next()
  const raceResult = backgroundPromise
    ? await Promise.race([nextMessagePromise.then(...), backgroundPromise])
    : { type: 'message', result: await nextMessagePromise }

  if (raceResult.type === 'background') {
    // Пользователь перевёл процесс в фон — переходим в async
    await agentIterator.return(undefined)
    void runAgent({ ...params, isAsync: true })
    return { data: { status: 'async_launched' } }
  }

  agentMessages.push(message)
}

Если пользователь в середине выполнения решает, что синхронный агент должен стать фоновой задачей, foreground-итератор корректно завершается (finally срабатывает для очистки ресурсов), и агент повторно запускается как асинхронная задача с тем же ID. Переход бесшовный: работа не теряется, и агент продолжает с того места, где остановился, уже с async abort controller, который не связан с ESC у родителя.

Это действительно сложный переход состояний. Foreground-агент разделяет abort controller родителя (ESC убивает обоих). Background-агенту нужен свой контроллер (ESC не должен его убивать). Сообщения агента должны перейти из потока foreground-generator в систему уведомлений background. Состояние задачи должно переключить isBackgrounded, чтобы UI показывал её в фоновой панели. И всё это должно случиться атомарно: без потерь сообщений, без оставленных работать «зомби»-итераторов. Гонка Promise.race между следующим сообщением и сигналом на перевод в фон — механизм, который делает это возможным.

Фон: три канала

Фоновые агенты общаются через диск, уведомления и очередь сообщений.

Файлы вывода на диске. Каждая задача пишет в путь outputFile — symlink на JSONL-транскрипт агента. Родитель (или любой наблюдатель) может читать этот файл инкрементально, используя outputOffset, который показывает, насколько далеко файл уже прочитан. TaskOutputTool экспонирует это модели:

inputSchema = z.strictObject({
  task_id: z.string(),
  block: z.boolean().default(true),
  timeout: z.number().default(30000),
})

Когда block: true, инструмент опрашивает задачу, пока та не достигнет конечного состояния или пока не истечёт таймаут. Это основной механизм для координатора, который порождает исполнителя и ждёт его результата.

Уведомления о задачах. Когда фоновый агент завершается, система генерирует XML-уведомление и ставит его в очередь на доставку в разговор родителя:

<task-notification>
  <task-id>a7j3n9p2</task-id>
  <tool-use-id>toolu_abc123</tool-use-id>
  <output-file>/path/to/output</output-file>
  <status>completed</status>
  <summary>Agent "Investigate auth bug" completed</summary>
  <result>Found null pointer in src/auth/validate.ts:42...</result>
  <usage>
    <total_tokens>15000</total_tokens>
    <tool_uses>8</tool_uses>
    <duration_ms>12000</duration_ms>
  </usage>
</task-notification>

Уведомление внедряется как сообщение с ролью user в разговор родителя, а значит модель видит его в обычном потоке сообщений. Ей не нужен специальный инструмент, чтобы проверять завершения: они приходят как часть контекста. Флаг notified на состоянии задачи предотвращает повторную доставку.

Очередь команд. Массив pendingMessages на LocalAgentTaskState — третий канал. Когда SendMessage адресует работающему агенту, сообщение ставится в очередь:

if (isLocalAgentTask(task) && task.status === 'running') {
  queuePendingMessage(agentId, input.message, setAppState)
  return { data: { success: true, message: 'Сообщение поставлено в очередь...' } }
}

Эти сообщения извлекаются на границах tool-round функцией drainPendingMessages() и внедряются как user-сообщения в разговор агента. Это критическое решение дизайна: сообщения приходят между tool-round, а не посреди выполнения. Агент завершает свою текущую мысль, а потом получает новую информацию. Никаких race condition, никакого повреждённого состояния.

Отслеживание прогресса

ProgressTracker даёт визуализацию активности агента в реальном времени:

export type ProgressTracker = {
  toolUseCount: number
  latestInputTokens: number        // Накопительное значение (последнее, а не сумма)
  cumulativeOutputTokens: number   // Суммируется по ходам
  recentActivities: ToolActivity[] // Последние 5 использований инструментов
}

Различие между отслеживанием input и output токенов сделано намеренно и отражает тонкость модели биллинга API. Input tokens накопительны на каждый API-вызов, потому что каждый раз пересылается полный разговор — 15-й ход включает все 14 предыдущих, так что число input-токенов, сообщаемое API, уже отражает полный итог. Поэтому хранить последнее значение — правильно. Output tokens считаются по ходам — модель генерирует новые токены каждый раз — так что суммирование верно. Ошибка здесь либо сильно завысит значения (если суммировать накопительные input tokens), либо сильно занизит их (если хранить только последнее значение output tokens).

Массив recentActivities (ограничен 5 элементами) даёт человекочитаемую ленту того, что делает агент: «Read src/auth/validate.ts», «Bash: npm test», «Edit src/auth/validate.ts». Это отображается в панели суб-агентов VS Code и в индикаторе фоновых задач терминала, давая пользователям видимость работы агента без необходимости читать полные транскрипты.

Для фоновых агентов прогресс записывается в AppState через updateAsyncAgentProgress() и публикуется как события SDK через emitTaskProgress(). Панель суб-агентов VS Code потребляет эти события, чтобы рендерить живые прогресс-бары, счётчики инструментов и ленты активности. Отслеживание прогресса — не просто косметика: это основной механизм обратной связи, который сообщает пользователю, движется ли фоновый агент вперёд или застрял в петле.


Режим координатора

Режим координатора превращает Claude Code из одиночного агента с фоновыми помощниками в настоящую архитектуру «менеджер — исполнитель». Это самый opinionated-режим оркестрации в системе, и его дизайн показывает глубокое понимание того, как LLM должны и не должны делегировать работу.

Какую проблему решает режим координатора

Стандартный цикл агента имеет один разговор и одно окно контекста. Когда он порождает фонового агента, тот работает независимо и сообщает результаты через уведомления задач. Для простой делегации это отлично работает — «запусти тесты, пока я продолжаю редактировать» — но ломается на сложных многошаговых рабочих процессах.

Представьте миграцию кодовой базы. Агент должен: (1) понять текущие паттерны в 200 файлах, (2) спроектировать стратегию миграции, (3) применить изменения к каждому файлу и (4) проверить, что ничего не сломалось. Шаги 1 и 3 выигрывают от параллелизма. Шаг 2 требует синтеза результатов шага 1. Шаг 4 зависит от шага 3. Один агент, выполняющий всё последовательно, потратит большую часть бюджета токенов на повторное чтение файлов. Несколько фоновых агентов без координации породят несогласованные изменения.

Режим координатора решает это, разделяя «думающего» агента и «делающих» агентов. Координатор выполняет шаги 1 и 2 (поручает исследование исполнителям, затем синтезирует). Исполнители выполняют шаги 3 и 4 (вносят изменения, запускают тесты). Координатор видит всю картину; исполнители видят только свою конкретную задачу.

Активация

Один переменный среды переключает режим:

export function isCoordinatorMode(): boolean {
  if (feature('COORDINATOR_MODE')) {
    return isEnvTruthy(process.env.CLAUDE_CODE_COORDINATOR_MODE)
  }
  return false
}

При возобновлении сессии matchSessionMode() проверяет, совпадает ли сохранённый режим возобновляемой сессии с текущей средой. Если они расходятся, переменная среды переключается так, чтобы соответствовать. Это предотвращает запутанную ситуацию, когда сессия координатора возобновляется как обычный агент (теряя осведомлённость о своих исполнителях) или обычная сессия возобновляется как координатор (теряя доступ к своим инструментам). Режим сессии — источник истины; переменная среды — сигнал во время выполнения.

Ограничения инструментов

Сила координатора обусловлена не тем, что у него больше инструментов, а тем, что их меньше. В режиме координатора агент-координатор получает ровно три инструмента:

  • Agent — порождать исполнителей
  • SendMessage — общаться с уже существующими исполнителями
  • TaskStop — завершать работающих исполнителей

И всё. Никакого чтения файлов. Никакого редактирования кода. Никаких shell-команд. Координатор не может напрямую трогать кодовую базу. Это не ограничение — это основной принцип дизайна. Задача координатора — думать, планировать, декомпозировать и синтезировать. Исполнители делают работу.

Исполнители, наоборот, получают полный набор инструментов минус внутренние инструменты координации:

const INTERNAL_WORKER_TOOLS = new Set([
  TEAM_CREATE_TOOL_NAME,
  TEAM_DELETE_TOOL_NAME,
  SEND_MESSAGE_TOOL_NAME,
  SYNTHETIC_OUTPUT_TOOL_NAME,
])

Исполнители не могут порождать собственные под-команды или отправлять сообщения другим исполнителям. Они сообщают результаты через обычный механизм завершения задач, а координатор синтезирует их.

370-строчная системная подсказка

Системная подсказка координатора — буквально самый поучительный документ в кодовой базе о том, как использовать LLM для оркестрации. Она занимает примерно 370 строк и кодирует тяжело добытые уроки о паттернах делегирования. Главные уроки:

«Никогда не делегируй понимание». Это центральный тезис. Координатор должен синтезировать результаты исследования в конкретные подсказки с путями к файлам, номерами строк и точными изменениями. В подсказке прямо разоблачаются анти-паттерны вроде «на основе своих находок исправь баг» — подсказки, которая делегирует понимание исполнителю, заставляя его заново выводить контекст, который уже есть у координатора. Правильный паттерн: «В src/auth/validate.ts на строке 42 параметр userId может быть null, когда он вызывается из OAuth flow. Добавь проверку на null, которая возвращает ответ 401».

«Параллелизм — твоя суперсила». Подсказка задаёт чёткую модель конкурентности. Чтение запускается свободно в параллели — исследования, разведка, чтение файлов. Запись сериализуется по наборам файлов. Координатор должен понимать, какие задачи могут идти одновременно, а какие должны идти последовательно. Хороший координатор запускает пять исследовательских исполнителей одновременно, ждёт всех, синтезирует, затем запускает трёх исполнителей реализации, которые трогают непересекающиеся наборы файлов. Плохой координатор запускает одного исполнителя, ждёт, затем следующего, снова ждёт — сериализуя работу, которую можно было распараллелить.

Фазы рабочего процесса задач. Подсказка определяет четыре фазы:

  1. Исследование — исполнители параллельно изучают кодовую базу, читают файлы, запускают тесты, собирают информацию
  2. Синтез — координатор (не исполнитель) читает все результаты исследования и строит единое понимание
  3. Реализация — исполнители получают точные инструкции, основанные на синтезе
  4. Проверка — исполнители запускают тесты и проверяют изменения

Координатор не должен пропускать фазы. Самый распространённый сбой — перескочить прямо из исследования в реализацию без синтеза. Тогда координатор делегирует понимание исполнителям: каждый должен заново восстанавливать контекст, что приводит к несогласованным изменениям и напрасной трате токенов.

Решение: продолжать или порождать. Когда исполнитель завершил работу, и у координатора есть следующая задача, стоит ли отправить сообщение существующему исполнителю (через SendMessage) или породить нового (через Agent)? Решение зависит от перекрытия контекста:

  • Высокое перекрытие, те же файлы: продолжать. У исполнителя уже есть содержимое файлов в контексте, он понимает паттерны и может развивать предыдущую работу. Новый запуск заставил бы заново читать те же файлы и выводить то же понимание.
  • Низкое перекрытие, другая область: породить заново. Исполнитель, который только что изучал auth-систему, тащит за собой 20 000 токенов auth-контекста, которые бесполезны для CSS-рефакторинга. Чистый старт дешевле.
  • Высокое перекрытие, но исполнитель провалился: породить заново с явным объяснением, что пошло не так. Продолжение неудачного исполнителя часто означает борьбу с запутанным контекстом. Свежий старт с формулировкой «предыдущая попытка провалилась из-за X, избегай Y» надёжнее.
  • Следующий шаг зависит от вывода исполнителя: продолжать, включив вывод в SendMessage. Исполнителю не нужно заново выводить собственные результаты.

Написание промптов исполнителям и анти-паттерны. Подсказка учит координатора, как писать эффективные промпты для исполнителей, и прямо помечает плохие паттерны:

Анти-паттерн: «На основе результатов исследования реализуй исправление». Это делегирует понимание. Исполнитель не делал исследования — координатор уже прочитал результаты.

Анти-паттерн: «Исправь баг в модуле auth». Никаких путей к файлам, никаких номеров строк, никакого описания бага. Исполнитель должен заново искать по всей кодовой базе.

Анти-паттерн: «Сделай то же изменение во всех остальных файлах». Каких файлах? Какое изменение? Координатор это знает; он должен перечислить их явно.

Хороший паттерн: «В src/auth/validate.ts на строке 42 параметр userId может быть null, когда он вызывается из src/oauth/callback.ts:89. Добавь проверку на null: если userId равен null, верни { error: 'unauthorized', status: 401 }. Затем обнови тест в src/auth/__tests__/validate.test.ts, чтобы покрыть случай null».

Цена конкретного промпта платится один раз — координатором. Польза — исполнитель, который делает всё правильно с первой попытки — огромна. Размытые подсказки создают ложную экономию: координатор экономит 30 секунд на написании промпта, а исполнитель тратит 5 минут на разведку.

Контекст исполнителя

Координатор внедряет информацию о доступных инструментах в свой собственный контекст, чтобы модель знала, что могут делать исполнители:

export function getCoordinatorUserContext(mcpClients, scratchpadDir?) {
  return {
    workerToolsContext: `Исполнители, запущенные через Agent, имеют доступ к: ${workerTools}`
      + (mcpClients.length > 0
        ? `\nУ исполнителей также есть MCP-инструменты от: ${serverNames}` : '')
      + (scratchpadDir ? `\nScratchpad: ${scratchpadDir}` : '')
  }
}

Каталог scratchpad (защищённый флагом tengu_scratch) — это общее файловое пространство, где исполнители могут читать и писать без запросов на разрешение. Оно даёт устойчивый обмен знаниями между исполнителями: заметки одного исполнителя становятся входом для другого, причём посредником выступает файловая система, а не токеновое окно координатора.

Это важно, потому что так решается фундаментальное ограничение паттерна координатора. Без scratchpad все сведения проходят через координатора: исполнитель A выдаёт находки, координатор читает их через TaskOutput, синтезирует их в промпт исполнителя B. Окно контекста координатора становится узким местом — он должен удерживать все промежуточные результаты достаточно долго, чтобы их синтезировать. Со scratchpad исполнитель A пишет находки в /tmp/scratchpad/auth-analysis.md, а координатор говорит исполнителю B: «Прочитай анализ auth в /tmp/scratchpad/auth-analysis.md и примени паттерн к модулю OAuth». Координатор переносит информацию по ссылке, а не по значению.

Взаимное исключение с fork

Режим координатора и fork-based суб-агенты взаимно исключают друг друга:

export function isForkSubagentEnabled(): boolean {
  if (feature('FORK_SUBAGENT')) {
    if (isCoordinatorMode()) return false
    // ...
  }
}

Конфликт фундаментальный. Fork-агенты наследуют весь разговор родителя — это дешёвые клоны, которые разделяют кэш промпта. Исполнители координатора — независимые агенты со свежим контекстом и конкретными инструкциями. Это противоположные философии делегирования, и система закрепляет выбор на уровне feature flag.


Система роя

Режим координатора иерархичен: один менеджер, много исполнителей, управление сверху вниз. Система роя — одноранговая альтернатива: несколько экземпляров Claude Code работают как команда, а лидер координирует несколько товарищей через передачу сообщений.

Контекст команды

Команды идентифицируются по teamName и отслеживаются в AppState.teamContext:

teamContext?: {
  teamName: string
  teammates: {
    [id: string]: { name: string; color?: string; ... }
  }
}

Каждый товарищ получает имя (для обращения) и цвет (для визуального различия в UI). Файл команды сохраняется на диске, чтобы членство в команде переживало перезапуск процесса.

Реестр имён агентов

Фоновым агентам можно давать имена при запуске, что делает их адресуемыми по человекочитаемым идентификаторам вместо случайных ID задач:

if (name) {
  rootSetAppState(prev => {
    const next = new Map(prev.agentNameRegistry)
    next.set(name, asAgentId(asyncAgentId))
    return { ...prev, agentNameRegistry: next }
  })
}

agentNameRegistry — это Map<string, AgentId>. Когда SendMessage разрешает поле to, сначала проверяется реестр:

const registered = appState.agentNameRegistry.get(input.to)
const agentId = registered ?? toAgentId(input.to)

Это значит, что можно отправить сообщение на `