Глава 8: Порождение суб-агентов
Умножение интеллекта
Один агент — это уже мощный инструмент. Он может читать файлы, править код, запускать тесты, искать в сети и рассуждать о результатах. Но у одного агента есть жесткий предел: окно контекста заполняется, задача ветвится в направления, требующие разных навыков, и последовательная природа исполнения инструментов становится узким местом. Решение — не в большем моделировании, а в большем количестве агентов.
Система суб-агентов Claude Code позволяет модели запрашивать помощь. Когда родительский агент сталкивается с задачей, которой выгодно делегировать — поиск по кодовой базе, который не должен засорять основную беседу; проверочный проход, требующий антагонистического мышления; набор независимых правок, которые можно выполнить параллельно — он вызывает инструмент Agent. Этот вызов порождает ребёнка: полностью независимый агент со своим циклом общения, набором инструментов, границами разрешений и контроллером прерывания. Ребёнок выполняет свою работу и возвращает результат. Родитель никогда не видит внутреннее рассуждение ребёнка, только итоговый вывод.
Это не просто удобная функция. Это архитектурная основа для всего — от параллельного исследования файлов до иерархий координатор→воркер и команд-ройов. И всё это проходит через два файла: AgentTool.tsx, который определяет интерфейс, видимый модели, и runAgent.ts, который реализует жизненный цикл.
Задача проектирования нетривиальна. Суб-агенту нужен достаточный контекст, чтобы сделать работу, но не настолько большой, чтобы он терял токены на несущественное. Ему нужны строгие границы разрешений — ради безопасности — но и достаточно гибкие для практической пользы. Необходимо управлять жизненным циклом так, чтобы освобождались все ресурсы, без того чтобы вызывающий помнил всё вручную. И всё это должно работать для спектра типов агентов — от дешёвого быстрого поискового Haiku до дорогого тщательного верификатора на Opus, выполняющего антагонистические тесты в фоне.
Эта глава прослеживает путь от внутреннего «мне нужна помощь» модели до полностью работающего дочернего агента. Мы рассмотрим определение инструмента, которое видит модель, пятнадцатишаговый жизненный цикл, шесть встроенных типов агентов и то, на что каждый ориентирован, систему frontmatter, позволяющую пользователям определять кастомных агентов, и проектные принципы, вытекающие из этого.
Небольшая оговорка по терминологии: в этой главе «родитель» — агент, вызвавший Agent tool, а «ребёнок» — агент, который порождается. Обычно родитель — топовый REPL-агент, но не всегда. В режиме координатора координатор порождает воркеров (они — дети). В вложенных сценариях ребёнок может породить внуков — тот же жизненный цикл применяется рекурсивно.
Слой оркестровки охватывает примерно 40 файлов в tools/AgentTool/, tasks/, coordinator/, tools/SendMessageTool/ и utils/swarm/. Эта глава фокусируется на механике порождения — определении AgentTool и жизненном цикле runAgent. Следующая глава будет про рантайм: отслеживание прогресса, получение результатов и паттерны координации мультиагентных команд.
Определение AgentTool
AgentTool регистрируется под именем "Agent" с устаревшим алиасом "Task" для обратной совместимости с более старыми транскриптами, правилами разрешений и конфигурациями хуков. Он построен с помощью стандартного фабричного buildTool(), но его схема более динамична, чем у любого другого инструмента в системе.
Схема ввода
Схема ввода создаётся лениво через lazySchema() — паттерн, который мы видели в главе 6, откладывающий компиляцию zod до первого использования. Есть два слоя: базовая схема и полная схема, добавляющая параметры для мульти-агентных сценариев и изоляции.
Базовые поля всегда присутствуют:
| Поле | Тип | Обязательно | Назначение |
|---|---|---|---|
description | string | Да | Короткое резюме задачи (3–5 слов) |
prompt | string | Да | Полное описание задачи для агента |
subagent_type | string | Нет | Какой специализированный агент использовать |
model | enum('sonnet','opus','haiku') | Нет | Переопределение модели для этого агента |
run_in_background | boolean | Нет | Запуск в фоне (асинхронно) |
Полная схема добавляет параметры мульти-агентов (когда активны фичи swarm) и элементы управления изоляцией:
| Поле | Тип | Назначение |
|---|---|---|
name | string | Делает агента адресуемым через SendMessage({to: name}) |
team_name | string | Контекст команды для порождения |
mode | PermissionMode | Режим разрешений для порождённого товарища |
isolation | enum('worktree','remote') | Стратегия файловой изоляции |
cwd | string | Переопределение абсолютного пути рабочей директории |
Поля мульти-агента включают паттерн swarm, описанный в главе 9: именованные агенты, которые могут отправлять сообщения друг другу через SendMessage({to: name}), запускаясь параллельно. Поля изоляции обеспечивают безопасность файловой системы: worktree-изоляция создаёт временный git worktree, чтобы агент работал с копией репозитория и не создавал конфликтующих правок, когда несколько агентов одновременно работают над одной кодовой базой.
Необычность этой схемы в том, что она динамически формируется флагами фич:
// Pseudocode — illustrates the feature-gated schema pattern
inputSchema = lazySchema(() => {
let schema = baseSchema()
if (!featureEnabled('ASSISTANT_MODE')) schema = schema.omit({ cwd: true })
if (backgroundDisabled || forkMode) schema = schema.omit({ run_in_background: true })
return schema
})
Когда эксперимент fork активен, run_in_background полностью исчезает из схемы, потому что все порождения принудительно асинхронны в этом пути. Когда флаг KAIROS выключен, cwd опускается. Модель никогда не видит полей, которыми она не может воспользоваться.
Это тонкий, но важный проектный выбор. Схема — это не только валидация, это инструкция для модели. Каждое поле в схеме описано в определении инструмента, которое читает модель. Убрать поля, которые модель не должна использовать, эффективнее, чем добавлять в подсказку «не используйте это поле». Модель не может злоупотребить тем, чего она не видит.
Схема вывода
Вывод — это дискриминированный союз с двумя публичными вариантами:
{ status: 'completed', prompt, ...AgentToolResult }— синхронное завершение с финальным выводом агента{ status: 'async_launched', agentId, description, prompt, outputFile }— подтверждение фонового запуска
Есть ещё два внутренних варианта (TeammateSpawnedOutput и RemoteLaunchedOutput), но они исключены из экспортируемой схемы, чтобы позволить dead code elimination во внешних сборках. Бандлер удаляет эти варианты и соответствующие пути кода, когда соответствующие флаги фич отключены, уменьшая размер дистрибутива.
Вариант async_launched примечателен тем, что включает outputFile — путь на файловой системе, куда будет записан результат агента после завершения. Это даёт родителю (или любому другому потребителю) возможность опрашивать или наблюдать файл для получения результатов — файловый канал коммуникации, который выживает после перезапуска процесса.
Динамическая подсказка (prompt)
Подсказка AgentTool генерируется функцией getPrompt() и чувствительна к контексту. Она адаптируется на основе доступных агентов (встроенных в текст или приложенных как attachment, чтобы не ломать кэш подсказок), активности fork (добавляет руководство «Когда форкать»), режима сессии (если это coordinator, подсказка упрощается, так как системный prompt координатора уже покрывает использование) и уровня подписки. Для не-Pro пользователей добавляется пометка о запуске нескольких агентов одновременно.
Особое внимание заслуживает список агентов в виде вложения. Комментарии в кодовой базе указывают на то, что примерно «10.2% токенов создания кэша флота» вызывается динамическими описаниями инструментов. Перемещение списка агентов из описания инструмента в attachment-сообщение сохраняет описание инструмента статичным, так что подключение MCP-сервера или загрузка плагина не рушат кэш подсказок для всех последующих API-вызовов.
Это паттерн, который стоит запомнить для систем с динамическим содержимым в определениях инструментов. Anthropic API кэширует префикс подсказки — системный prompt, определения инструментов и историю беседы — и повторно использует кэш для последующих запросов, которые делят тот же префикс. Если определение инструмента меняется между вызовами (например, добавлен агент или подключён MCP-сервер), весь кэш инвалидируется. Перенос изменчивого содержимого из определения инструмента (в составе кешируемого префикса) в attachment-сообщение сохраняет кэш, при этом всё ещё передаёт информацию модели.
Поняв определение инструмента, можно проследить, что происходит после того, как модель действительно делает вызов.
Фиче-гейтинг
Система суб-агентов содержит самую сложную логику фиче-гейтинга в кодовой базе. По крайней мере двенадцать флагов фич и экспериментов GrowthBook контролируют, какие агенты доступны, какие параметры появляются в схеме и какие пути кода используются:
| Фича | Контролирует |
|---|---|
FORK_SUBAGENT | Путь fork-агента |
BUILTIN_EXPLORE_PLAN_AGENTS | Агентов Explore и Plan |
VERIFICATION_AGENT | Агент Verification |
KAIROS | Переопределение cwd, принудительный async для ассистента |
TRANSCRIPT_CLASSIFIER | Классификация handoff, переопределение auto режима |
PROACTIVE | Интеграцию модуля Proactive |
Каждый gate использует feature() из системы dead code elimination Bun (во время компиляции) или getFeatureValue_CACHED_MAY_BE_STALE() из GrowthBook (рантайм A/B тестирование). Gate-ы compile-time подменяются при сборке — когда FORK_SUBAGENT равен 'ant', весь путь fork включается; когда 'external', он может быть полностью исключён. GrowthBook даёт возможность live-экспериментов: эксперимент tengu_amber_stoat может A/B тестировать влияние удаления Explore и Plan агентов без выпуска нового бинарника.
Дерево решений в call()
Прежде чем runAgent() будет вызван, метод call() в AgentTool.tsx пропускает запрос через дерево решений, которое определяет какого агента породить и как это сделать:
1. Is this a teammate spawn? (team_name + name both set)
YES -> spawnTeammate() -> return teammate_spawned
NO -> continue
2. Resolve effective agent type
- subagent_type provided -> use it
- subagent_type omitted, fork enabled -> undefined (fork path)
- subagent_type omitted, fork disabled -> "general-purpose" (default)
3. Is this the fork path? (effectiveType === undefined)
YES -> Recursive fork guard check -> Use FORK_AGENT definition
4. Resolve agent definition from activeAgents list
- Filter by permission deny rules
- Filter by allowedAgentTypes
- Throw if not found or denied
5. Check required MCP servers (wait up to 30s for pending)
6. Resolve isolation mode (param overrides agent def)
- "remote" -> teleportToRemote() -> return remote_launched
- "worktree" -> createAgentWorktree()
- null -> normal execution
7. Determine sync vs async
shouldRunAsync = run_in_background || selectedAgent.background ||
isCoordinator || forceAsync || isProactiveActive
8. Assemble worker tool pool
9. Build system prompt and prompt messages
10. Execute (async -> registerAsyncAgent + void lifecycle; sync -> iterate runAgent)
Шаги 1–6 — это чистая маршрутизация — агент ещё не создан. Собственный жизненный цикл начинается в runAgent(), который итерирует синхронный путь напрямую, а асинхронный путь оборачивается в runAsyncAgentLifecycle().
Размещение маршрутизации в call(), а не в runAgent(), осознанно: runAgent() — чистая функция жизненного цикла, которая не знает про товарищей (teammates), remote-агентов или эксперимент fork. Ей передаётся уже разрешённое определение агента и она исполняет его. Решение о том, какое определение использовать, как изолировать агента и синхронно или асинхронно его запускать, принадлежит верхнему уровню. Такая разделённость делает runAgent() тестируемым и повторно используемым — он вызывается и из обычного пути AgentTool, и из обертки асинхронного жизненного цикла при возобновлении фонового агента.
Защитный механизм fork в шаге 3 заслуживает внимания. Fork-дети сохраняют инструмент Agent в своём пуле (для байтово-идентичных определений инструментов с родителем), но рекурсивный форк — патологичен. Два защитных механизма предотвращают это: querySource === 'agent:builtin:fork' (устанавливается в опциях контекста ребёнка, переживает autocompact) и isInForkChild(messages) (сканирует историю беседы на наличие тега <fork-boilerplate> как запасной вариант). Один механизм быстрый и надёжный; запасной ловит пограничные случаи, где querySource не был передан.
Жизненный цикл runAgent
runAgent() в runAgent.ts — это async-генератор, который управляет полным жизненным циклом суб-агента. Он отдаёт (yield) объекты Message по мере работы агента. Любой суб-агент — fork, встроенный, кастомный, воркер координатора — проходит через эту единую функцию. Функция чуть более 400 строк, и каждая строка там не случайно.
Сигнатура функции отражает сложность задачи:
export async function* runAgent({
agentDefinition, // What kind of agent
promptMessages, // What to tell it
toolUseContext, // Parent's execution context
canUseTool, // Permission callback
isAsync, // Background or blocking?
canShowPermissionPrompts,
forkContextMessages, // Parent's history (fork only)
querySource, // Origin tracking
override, // System prompt, abort controller, agent ID overrides
model, // Model override from caller
maxTurns, // Turn limit
availableTools, // Pre-assembled tool pool
allowedTools, // Permission scoping
onCacheSafeParams, // Callback for background summarization
useExactTools, // Fork path: use parent's exact tools
worktreePath, // Isolation directory
description, // Human-readable task description
// ...
}: { ... }): AsyncGenerator<Message, void>
Семнадцать параметров. Каждый представляет измерение вариативности, которое жизненный цикл должен учесть. Это не переусложнение — это следствие того, что одна функция обслуживает fork-агентов, встроенных, кастомных, синхронных, асинхронных, агентов с worktree-изоляцией и воркеров координатора. Альтернатива — семь разных функций жизненного цикла с дублирующейся логикой, что хуже.
Объект override особенно важен — это аварийный выход для fork-агентов и возобновляемых агентов, которые должны впрыскивать заранее вычисленные значения (системный prompt, abort controller, agent ID) в жизненный цикл без повторного вычисления.
Далее — пятнадцать шагов.
Шаг 1: Разрешение модели
const resolvedAgentModel = getAgentModel(
agentDefinition.model, // Agent's declared preference
toolUseContext.options.mainLoopModel, // Parent's model
model, // Caller's override (from input)
permissionMode, // Current permission mode
)
Цепочка разрешения: переопределение вызова > объявление агента > модель родителя > значение по умолчанию. getAgentModel() обрабатывает специальные значения вроде 'inherit' (использовать модель родителя) и GrowthBook-гейтованные переопределения для конкретных типов агентов. Например, Explore обычно по умолчанию использует Haiku для внешних пользователей — самый дешёвый и быстрый вариант, подходящий для read-only поискового специалиста, который запускается сотни миллионов раз в неделю.
Почему порядок важен: вызывающая сторона (родитель) может переопределить предпочтение определения агента, передав параметр model в вызове инструмента. Это позволяет повысить модель у типично дешёвого агента для сложного поиска или понизить дорогой агент, когда задача проста. Но предпочтение определения — это значение по умолчанию, а не сцепление с моделью родителя: Explore агент не должен случайно наследовать Opus модель родителя просто потому, что никто не указал иного.
Понимание этой цепочки разрешений формирует проектный принцип, который повторяется по всему жизненному циклу: явные переопределения побеждают декларации, декларации побеждают наследование, наследование побеждает дефолты. Та же логика применяется к режимам разрешений, контроллерам прерывания и системным подсказкам. Последовательность делает систему предсказуемой — познакомившись с одной цепочкой, вы поймёте их все.
Шаг 2: Создание ID агента
const agentId = override?.agentId ? override.agentId : createAgentId()
AgentId имеет формат agent-<hex>, где hex часть derived from crypto.randomUUID(). Брендовый тип AgentId предотвращает путаницу строк на уровне типов. Путь override нужен для возобновляемых агентов, которые должны сохранять свой исходный ID для непрерывности транскриптов.
Шаг 3: Подготовка контекста
Fork-агенты и свежие агенты расходятся здесь:
const contextMessages: Message[] = forkContextMessages
? filterIncompleteToolCalls(forkContextMessages)
: []
const initialMessages: Message[] = [...contextMessages, ...promptMessages]
const agentReadFileState = forkContextMessages !== undefined
? cloneFileStateCache(toolUseContext.readFileState)
: createFileStateCacheWithSizeLimit(READ_FILE_STATE_CACHE_SIZE)
Для fork-агентов в contextMessages клонируется вся история беседы родителя. Но критически важен фильтр: filterIncompleteToolCalls() отбрасывает любые tool_use блоки, у которых нет соответствующих tool_result. Без этого фильтра API мог бы отвергнуть некорректную беседу. Это случается, когда родитель был в середине выполнения инструмента в момент форка — tool_use уже был эмитирован, но результат ещё не пришёл.
Кеш состояния чтения файлов следует той же логике fork|fresh. Fork-дети получают клонированный кеш родителя (они уже «знают», какие файлы были прочитаны). Свежие агенты стартуют пусто. Клонирование — поверхностное: строки содержимого файлов разделяются по ссылке, не дублируются. Это важно для памяти: fork-ребёнок с кешем из 50 файлов не дублирует 50 содержимых файлов, он дублирует 50 указателей. Политика LRU-эвакуации независима — каждый кеш эвикуирует согласно собственным шаблонам доступа.
Шаг 4: Отсечение CLAUDE.md
Агенты только для чтения, такие как Explore и Plan, имеют omitClaudeMd: true в своих определениях:
const shouldOmitClaudeMd =
agentDefinition.omitClaudeMd &&
!override?.userContext &&
getFeatureValue_CACHED_MAY_BE_STALE('tengu_slim_subagent_claudemd', true)
const { claudeMd: _omittedClaudeMd, ...userContextNoClaudeMd } = baseUserContext
const resolvedUserContext = shouldOmitClaudeMd
? userContextNoClaudeMd
: baseUserContext
Файлы CLAUDE.md содержат инструкции проекта о сообщениях коммитов, конвенциях PR, правилах линтинга и стандартах кодирования. Поисковому агенту это всё не нужно — он не может коммитить, не может создать PR, не может редактировать файлы. Родительский агент имеет полный контекст и интерпретирует результаты поиска. Отбрасывание CLAUDE.md экономит миллиарды токенов в неделю по всему флоту — совокупное сокращение стоимости оправдывает добавленную сложность условной инъекции контекста.
Аналогично, Explore и Plan агенты имеют обрезанный gitStatus в системном контексте. Снимок git status, сделанный на старте сессии, может занимать до 40КБ и явно помечен как stale. Если этим агентам нужна git-информация, они могут выполнить git status самостоятельно и получить свежие данные.
Это не преждевременные оптимизации. При 34 миллионах Explore-порождений в неделю каждый лишний токен складывается в значимую статью расходов. Kill-switch (tengu_slim_subagent_claudemd) по умолчанию включён, но может быть переключён через GrowthBook, если отбрасывание вызывает регрессии.
Шаг 5: Изоляция разрешений
Это самый сложный шаг. Каждому агенту даётся кастомная обёртка getAppState(), которая накладывает его конфигурацию разрешений поверх состояния родителя:
const agentGetAppState = () => {
const state = toolUseContext.getAppState()
let toolPermissionContext = state.toolPermissionContext
// Override mode unless parent is in bypassPermissions, acceptEdits, or auto
if (agentPermissionMode && canOverride) {
toolPermissionContext = {
...toolPermissionContext,
mode: agentPermissionMode,
}
}
// Auto-deny prompts for agents that can't show UI
const shouldAvoidPrompts =
canShowPermissionPrompts !== undefined
? !canShowPermissionPrompts
: agentPermissionMode === 'bubble'
? false
: isAsync
if (shouldAvoidPrompts) {
toolPermissionContext = {
...toolPermissionContext,
shouldAvoidPermissionPrompts: true,
}
}
// Scope tool allow rules
if (allowedTools !== undefined) {
toolPermissionContext = {
...toolPermissionContext,
alwaysAllowRules: {
cliArg: state.toolPermissionContext.alwaysAllowRules.cliArg,
session: [...allowedTools],
},
}
}
return { ...state, toolPermissionContext, effortValue }
}
Здесь объединяются четыре разных соображения:
- Каскад режимов разрешений. Если родитель находится в
bypassPermissions,acceptEditsилиautoрежиме, режим родителя всегда побеждает — определение агента не может ослабить его. Это предотвращает ситуацию, когда пользователь намеренно выставил разрешения с меньшими ограничениями, а агент определением понижает безопасность. - Избегание диалогов. Фоновые агенты не могут показывать диалоги разрешений — у них нет терминала. Поэтому
shouldAvoidPermissionPromptsставится вtrue, что заставляет систему авто-отклонять такие запросы, вместо блокировки выполнения. Исключение — режимbubble: такие агенты могут транслировать диалоги в терминал родителя, поэтому они всегда могут показывать промпты, вне зависимости от sync/async. - Порядок автоматических проверок. Фоновые агенты, которые могут показывать промпты (bubble), выставляют
awaitAutomatedChecksBeforeDialog. Это означает, что классификатор и хуки проверок запускаются первыми; пользователь прерывается только если автоматическое разрешение не удалось. Для фоновой работы подождать дополнительную секунду ради классификатора — приемлемо; пользователя не должно прерывать по пустякам. - Скопирование правил позволения инструментов. Когда задан
allowedTools, он полностью заменяет сессионные правила allow. Это предотвращает утечку одобрений родителя к scoped-агенту. Но разрешения на уровне SDK (CLI флаг--allowedTools) сохраняются — это политика встраивающего приложения и должна действовать повсеместно.
Шаг 6: Разрешение инструментов
const resolvedTools = useExactTools
? availableTools
: resolveAgentTools(agentDefinition, availableTools, isAsync).resolvedTools
Fork-агенты используют useExactTools: true, пропуская массив инструментов родителя без изменений. Это не просто удобство — это оптимизация кэша. Различные определения инструментов сериализуются по-разному (различные режимы разрешений дают разный метаданный инструмента), и любое расхождение в блоке инструментов ломает префикс кэша подсказок. Fork-дети требуют байтово-идентичных префиксов.
Для обычных агентов resolveAgentTools() применяет многослойный фильтр:
tools: ['*']означает все инструменты;tools: ['Read', 'Bash']— только этиdisallowedTools: ['Agent', 'FileEdit']удаляет указанные из пула- У встроенных агентов и кастомных — разные базовые списки запрещённых инструментов
- Async-агенты фильтруются через
ASYNC_AGENT_ALLOWED_TOOLS
В результате каждый тип агента видит ровно те инструменты, которые ему положены. Explore не может вызывать FileEdit. Verification не может вызывать Agent (нет рекурсивного порождения от верификатора). Кастомные агенты по умолчанию строже, чем встроенные.
Шаг 7: Системный prompt
const agentSystemPrompt = override?.systemPrompt
? override.systemPrompt
: asSystemPrompt(
await getAgentSystemPrompt(
agentDefinition, toolUseContext,
resolvedAgentModel, additionalWorkingDirectories, resolvedTools
)
)
Fork-агенты получают пред-рендеренный системный prompt родителя через override.systemPrompt. Это проксируется из toolUseContext.renderedSystemPrompt — точно те же байты, которые использовал родитель в последнем API-вызове. Повторное вычисление системного prompt через getSystemPrompt() может привести к расхождению. GrowthBook-фичи могли измениться между вызовами родителя и ребёнка. Даже единичное байтовое отличие берегает целый кэш подсказок.
Для обычных агентов getAgentSystemPrompt() вызывает getSystemPrompt() определения агента, затем дополняет окружением — абсолютными путями, эмодзи-подсказками (Claude склонен чрезмерно использовать эмодзи в некоторых контекстах) и инструкциями, специфичными для модели.
Шаг 8: Изоляция контроллера прерывания
const agentAbortController = override?.abortController
? override.abortController
: isAsync
? new AbortController()
: toolUseContext.abortController
Три линии, три поведения:
- Override: используется при возобновлении фонового агента или для специфического управления жизненным циклом. Имеет приоритет.
- Асинхронным агентам даётся новый, независимый контроллер. Когда пользователь нажимает Escape, контроллер родителя срабатывает. Асинхронные агенты должны пережить это — это фоновая работа, которую пользователь делегировал. Их независимый контроллер позволяет им продолжать.
- Синхронные агенты разделяют контроллер родителя. Escape убивает и ребёнка, и родителя. Ребёнок блокирует родителя; если пользователь хочет остановить, он хочет остановить всё.
Это решение кажется очевидным в ретроспективе, но было бы катастрофическим, если бы было выбрано неправильно. Асинхронный агент, который отменяется вместе с родителем, потерял бы всю работу каждый раз, когда пользователь нажимает Escape, чтобы задать уточняющий вопрос. Синхронный агент, игнорирующий контроллер родителя, оставил бы пользователя в подвешенном интерфейсе.
Шаг 9: Регистрация хуков
if (agentDefinition.hooks && hooksAllowedForThisAgent) {
registerFrontmatterHooks(
rootSetAppState, agentId, agentDefinition.hooks,
`agent '${agentDefinition.agentType}'`, true
)
}
Определения агентов могут декларировать собственные хуки (PreToolUse, PostToolUse и т.д.) в frontmatter. Хуки привязываются к жизненному циклу агента через agentId — они срабатывают только для вызовов инструментов этого агента и автоматически удаляются в finally блоке при завершении агента.
Флаг isAgent: true (последний параметр true) конвертирует хуки Stop в SubagentStop. Суб-агенты вызывают SubagentStop, а не Stop, так что конверсия гарантирует, что хуки сработают в нужный момент.
Безопасность важна. Когда strictPluginOnlyCustomization активен для хуков, регистрируются только хуки плагинов, встроенные хуки и хуки, заданные политикой. Пользовательские агенты (из .claude/agents/) получают свои хуки пропущенными, чтобы пользователь не мог inject’ить хуки, которые обходят контроль безопасности.
Шаг 10: Предзагрузка скиллов
const skillsToPreload = agentDefinition.skills ?? []
if (skillsToPreload.length > 0) {
const allSkills = await getSkillToolCommands(getProjectRoot())
// resolve names, load content, prepend to initialMessages
}
Определения агентов могут указывать skills: ["my-skill"] в frontmatter. Разрешение использует три стратегии: точное совпадение, префикс с именем плагина агента ("my-skill" → "plugin:my-skill") и совпадение по суффиксу ":skillName" для неймспейсов плагинов. Три метода гарантируют, что ссылки на скиллы работают независимо от того, использовал ли автор полного имени, короткой формы или плагин-относительного имени.
Загруженные скиллы становятся user messages, добавленными в начало беседы агента. Агенты «читают» инструкции скилла до просмотра основной задачи — тот же механизм, что и для slash-команд в основном REPL, повторно используемый для автоматической инъекции скиллов. Содержимое скиллов загружается параллельно через Promise.all() для минимизации задержки старта при множестве скиллов.
Шаг 11: Инициализация MCP
const { clients: mergedMcpClients, tools: agentMcpTools, cleanup: mcpCleanup } =
await initializeAgentMcpServers(agentDefinition, toolUseContext.options.mcpClients)
Агенты могут определить свои MCP-серверы во frontmatter, которые добавляются к клиентам родителя. Поддерживается две формы:
- Ссылка по имени:
"slack"ищет существующую конфигурацию MCP и получает разделяемый memoized клиент - Inline-определение:
{ "my-server": { command: "...", args: [...] } }создаёт новый клиент, который очищается при завершении агента
Только вновь созданные (inline) клиенты очищаются. Разделяемые клиенты мемоизируются на уровне родителя и живут дольше жизни агента. Это предотвращает ситуацию, когда агент случайно ломает MCP-подключение, которое всё ещё нужно родителю или другим агентам.
Инициализация MCP происходит после регистрации хуков и предзагрузки скиллов, но до создания контекста. Порядок важен: MCP-инструменты должны быть объединены в пул инструментов до того, как createSubagentContext() снимет snapshot инструментов в опциях агента. Перестановка шагов привела бы к тому, что агент либо не получил бы MCP-инструменты, либо получил бы их, но они бы не были в его пуле.
Шаг 12: Создание контекста
const agentToolUseContext = createSubagentContext(toolUseContext, {
options: agentOptions,
agentId,
agentType: agentDefinition.agentType,
messages: initialMessages,
readFileState: agentReadFileState,
abortController: agentAbortController,
getAppState: agentGetAppState,
shareSetAppState: !isAsync,
shareSetResponseLength: true,
criticalSystemReminder_EXPERIMENTAL:
agentDefinition.criticalSystemReminder_EXPERIMENTAL,
contentReplacementState,
})
createSubagentContext() в utils/forkedAgent.ts собирает новый ToolUseContext. Ключевые решения по изоляции:
- Синхронные агенты делятся
setAppStateс родителем. Изменения состояния видны обоим. UI пользователя остаётся консистентным. - Асинхронные агенты получают изолированный
setAppState. Записи ребёнка в родительскую копию — noop. НоsetAppStateForTasksдостигает корневого стора — ребёнок всё ещё может обновлять состояние задач (прогресс, завершение), которое видит UI. - Обе стороны делят
setResponseLengthдля метрик отклика. - Fork-агенты наследуют
thinkingConfigдля байтово-идентичных API-запросов. Обычные агенты получают{ type: 'disabled' }— thinking (расширенное рассуждение токенами) отключено для контроля расходов. Родитель платит за thinking; дети исполняют. - messages, readFileState — свои массивы/кеши.
createSubagentContext() тщательно выбирает, что изолировать, а что — шарить; граница изоляции не абсолютна — это набор изолированных и общих каналов:
| Вопрос | Sync-агент | Async-агент |
|---|---|---|
setAppState | Shared (видно родителю) | Isolated (записи — noop) |
setAppStateForTasks | Shared | Shared (обновления задач глобальны) |
setResponseLength | Shared | Shared (метрики глобальны) |
readFileState | Собственный кеш | Собственный кеш |
abortController | Родительский | Независимый |
thinkingConfig | Fork: унаследован / Норм: отключён | Fork: унаследован / Норм: отключён |
messages | Собственный массив | Собственный массив |
Асимметрия между setAppState (изолирован для async) и setAppStateForTasks (всегда общий) — ключевое проектное решение. Асинхронный агент не должен внезапно менять реактивный стор родителя — это вызовет неожиданные скачки UI. Но агент обязан обновлять глобальный реестр задач, чтобы родитель узнал об окончании фоновой работы. Разделение каналов решает обе задачи.
Шаг 13: Callback cache-safe параметров
if (onCacheSafeParams) {
onCacheSafeParams({
systemPrompt: agentSystemPrompt,
userContext: resolvedUserContext,
systemContext: resolvedSystemContext,
toolUseContext: agentToolUseContext,
forkContextMessages: initialMessages,
})
}
Этот callback используют для фоновой суммаризации. Пока асинхронный агент выполняется, служба суммаризации может форкнуть беседу агента — используя эти параметры, чтобы сконструировать байтово-идентичный префикс — и генерировать периодические сводки прогресса, не мешая основной беседе. Параметры «cache-safe», потому что они дают тот же префикс API-запроса, который использует агент, максимизируя попадания в кэш.
Шаг 14: Цикл запросов (query loop)
try {
for await (const message of query({
messages: initialMessages,
systemPrompt: agentSystemPrompt,
userContext: resolvedUserContext,
systemContext: resolvedSystemContext,
canUseTool,
toolUseContext: agentToolUseContext,
querySource,
maxTurns: maxTurns ?? agentDefinition.maxTurns,
})) {
// Forward API request starts for metrics
// Yield attachment messages
// Record to sidechain transcript
// Yield recordable messages to caller
}
}
Тот же query() из главы 3 управляет беседой суб-агента. Сообщения суб-агента возвращаются вызывающему — либо AgentTool.call() для синхронных агентов (который итерирует генератор inline), либо runAsyncAgentLifecycle() для асинхронных агентов (который потребляет генератор в отделённом асинхронном контексте).
Каждое yield’нутое сообщение записывается в sidechain-транскрипт через recordSidechainTranscript() — append-only JSONL файл на агента. Это обеспечивает возможность возобновления: при прерывании сессии агент можно реконструировать из транскрипта. Запись — O(1) на сообщение, добавляется только новое сообщение с ссылкой на предыдущий UUID для непрерывности цепочки.
Шаг 15: Очистка (Cleanup)
finally блок выполняется при нормальном завершении, abort или ошибке. Это самый исчерпывающий набор очисток в кодовой базе:
finally {
await mcpCleanup() // Tear down agent-specific MCP servers
clearSessionHooks(rootSetAppState, agentId) // Remove agent-scoped hooks
cleanupAgentTracking(agentId) // Prompt cache tracking state
agentToolUseContext.readFileState.clear() // Release file state cache memory
initialMessages.length = 0 // Release fork context (GC hint)
unregisterPerfettoAgent(agentId) // Perfetto trace hierarchy
clearAgentTranscriptSubdir(agentId) // Transcript subdir mapping
rootSetAppState(prev => { // Remove agent's todo entries
const { [agentId]: _removed, ...todos } = prev.todos
return { ...prev, todos }
})
killShellTasksForAgent(agentId, ...) // Kill orphaned bash processes
}
Каждая подсистема, с которой агент взаимодействовал, очищается: MCP-подключения, хуки, отслеживание кеша, кеш состояния файлов, perfetto-трейсы, todo-записи и осиротевшие shell-процессы. Комментарий о «whale sessions», порождающих сотни агентов, говорит сам за себя — без этой очистки даже небольшие утечки накапливаются в измеримую нагрузку памяти за долгие сессии.
initialMessages.length = 0 — ручная подсказка GC. Для fork-агентов initialMessages содержат всю историю родителя. Установка длины в ноль освобождает ссылки, чтобы сборщик мусора мог освободить память. В сессии с контекстом в 200K токенов, порождающей пять fork-детей, это мегабайты дублированных объектов сообщений на каждого.
Здесь урок об управлении ресурсами в long-running агентных системах. Каждый шаг очистки адресует свой тип утечки: MCP (файловые дескрипторы), хуки (память в app state store), кеш состояния файлов (в памяти содержимое), perfetto-регистрации (метаданные трассировки), todo-entries (ключи реактивного состояния) и shell-процессы (процессы ОС). Агент взаимодействует с множеством подсистем — каждая должна быть уведомлена при его завершении. finally — единая точка, где происходят все уведомления, и протокол генератора гарантирует её выполнение. Это причина, по которой архитектура на основе генераторов — не просто удобство, а требование корректности.
Цепочка генераторов
Перед переходом к встроенным типам агентов стоит сделать шаг назад и посмотреть структурный паттерн, который всё это делает возможным. Вся система суб-агентов основана на async-генераторах. Цепочка выглядит так:
Эта архитектура на генераторах обеспечивает четыре критические возможности:
- Streaming. Сообщения проходят через систему по мере их генерации. Родитель (или обёртка асинхронного жизненного цикла) наблюдает каждое сообщение — обновляет индикаторы прогресса, форвардит метрики, записывает транскрипты — без буферизации всей беседы.
- Cancellation. Возврат итератора (return) в async-итераторе вызывает выполнение
finallyвrunAgent(). Пятнадцатишаговый cleanup выполняется как при нормальном завершении, так и при прерывании или ошибке. Протокол async-генераторов JS это гарантирует. - Backgrounding. Синхронный агент, занявший слишком много времени, можно перевести в фон прямо во время выполнения. Итератор передаётся из foreground (где
AgentTool.call()его итерировал) в асинхронный контекст (runAsyncAgentLifecycle()), и агент продолжает с того же места. - Progress tracking. Каждое yield’нутое сообщение — точка наблюдения. Обёртка асинхронного жизненного цикла использует эти точки для обновления стейта задачи, вычисления процентов прогресса и генерации уведомлений о завершении.
Встроенные типы агентов
Встроенные агенты регистрируются через getBuiltInAgents() в builtInAgents.ts. Реестр динамичен — доступность агентов зависит от флагов фич, экспериментов GrowthBook и типа входной точки сессии. В систему входит шесть встроенных агентов, каждый оптимизирован под конкретный класс работ.
General-Purpose
Агент по умолчанию, когда subagent_type пропущен и fork не активен. Полный доступ к инструментам, CLAUDE.md не отрезается, модель определяется через getDefaultSubagentModel(). Его системный prompt позиционирует его как воркера, ориентированного на завершение задачи: «Завершите задачу полностью — не переусложняйте, но и не оставляйте наполовину сделанной». Включены рекомендации по стратегии поиска (сначала широко, затем узко) и дисциплинам создания файлов (не создавать файлы без необходимости).
Это рабочая лошадка. Когда модель не знает, какой агент ей нужен, ей даётся general-purpose агент, который может делать всё, что может родитель, за исключением порождения собственных суб-агентов. Ограничение «не порождать» важно: иначе дочерний general-purpose мог бы порождать детей, которые порождают их — экспоненциальный фэн-аут, сжигающий бюджет API за секунды. Инструмент Agent в списке disallowed по умолчанию — не случайно.
Explore
Специалист по read-only поиску. Использует Haiku (дешёвая, быстрая модель). Обрезает CLAUDE.md и git status. Имеет удалённые FileEdit, FileWrite, NotebookEdit и Agent из пула инструментов, это обеспечено и на уровне инструментов, и через секцию === CRITICAL: READ-ONLY MODE === в системном prompt.
Explore — самый агрессивно оптимизированный встроенный агент, потому что он запускается чаще всех — 34 миллиона раз в неделю по всему флоту. Он помечен как one-shot (ONE_SHOT_BUILTIN_AGENT_TYPES), что означает, что agentId, инструкции SendMessage и usage trailer пропускаются из его prompt, экономя примерно 135 символов на вызов. При 34 миллионах вызовов эти 135 символов суммарно дают порядка 4.6 миллиарда символов, сэкономленных за неделю в prompt-токенах.
Доступность контролируется флагом BUILTIN_EXPLORE_PLAN_AGENTS И экспериментом GrowthBook tengu_amber_stoat, который A/B тестирует влияние удаления этих специализированных агентов.
Plan
Агент для архитектурного проектирования. Имеет тот же read-only набор инструментов, что и Explore, но использует 'inherit' для модели (наследует модель родителя). Его системный prompt ведёт через структурированный четырёхшаговый процесс: Понять требования, Исследовать тщательно, Спроектировать решение, Детализировать план. Должен закончить списоком «Critical Files for Implementation».
Plan наследует модель родителя, потому что архитектура требует тех же возможностей рассуждения, что и исполнение. Нельзя позволять Haiku делать архитектурные решения, которые затем Opus-агенту придётся реализовывать — несоответствие моделей приводит к планам, которые звучат правдоподобно, но оказываются ошибочными в деталях.
Те же гейты доступности, что и у Explore.
Verification
Антагонистический тестировщик. Read-only инструменты, модель 'inherit', всегда работает в фоне (background: true), отображается красным в терминале. Его системный prompt — самый подробный среди встроенных, около 130 строк.
Интерес Verification в его анти-избегающем программировании. Prompt явно перечисляет отговорки, к которым могла бы прибегнуть модель, и инструктирует «распознавать их и делать обратное». Каждая проверка должна включать блок «Command run» с реальным выводом терминала — никаких туманных утверждений «это должно работать». Агент обязан включить хотя бы один антагонистический зонд (конкурентность, граничные условия, идемпотентность, очистка осиротевших ресурсов). И прежде чем сообщать о баге, он должен проверить, намеренное ли это поведение или уже обработано где-то ещё.
Поле criticalSystemReminder_EXPERIMENTAL вставляет напоминание после каждого результата инструмента, укрепляя правило: это только верификация. Это предохранитель против дрейфа модели от «проверить» к «пофиксить» — естественной склонности моделей быть полезными, а «полезность» часто означает «исправить проблему». Ценность Verification — в сопротивлении этой склонности.
Флаг background: true означает, что Verification всегда выполняется асинхронно. Родитель не ждёт результатов — продолжает работу, а верификатор выполняет зондирование в фоне. Когда верификатор завершится, появляется уведомление с результатами. Это напоминает ревью кода человеком: разработчик не останавливает работу, пока рецензент читает PR.
Доступен через флаг VERIFICATION_AGENT и эксперимент tengu_hive_evidence.
Claude Code Guide
Агент для поиска документации по самому Claude Code, SDK и API Claude. Использует Haiku, режим разрешений dontAsk (никаких пользовательских промптов — только чтение документации) и имеет два хардкодных URL документации.
Его getSystemPrompt() уникален тем, что получает toolUseContext и динамически включает контекст о кастомных скиллах проекта, пользовательских агентах, настроенных MCP серверах, командах плагинов и настройках пользователя. Это позволяет отвечать на «как настроить X?» зная, что уже настроено.
Исключается, если входной путь — это SDK (TypeScript, Python или CLI), потому что пользователи SDK не спрашивают Claude Code, как использовать Claude Code — они строят свои собственные инструменты поверх него.
Guide — интересный кейс, потому что это единственный встроенный агент, чей системный prompt динамичен в зависимости от проекта пользователя. Ему нужно знать текущую конфигурацию, чтобы эффективно отвечать. Это делает getSystemPrompt() более сложным, но ценность очевидна — агент документации, не знающий, что уже настроено, даёт худшие ответы.
Statusline Setup
Специализированный агент для настройки статусной строки терминала. Использует Sonnet, отображается оранжевым, ограничен инструментами Read и Edit. Умеет конвертировать escape-последовательности PS1 в shell-команды, записывать ~/.claude/settings.json и обрабатывать JSON-формат команды statusLine.
Это самый узкоспециализированный встроенный агент — он существует потому, что конфигурация статусной строки — самостоятельная область с точными правилами форматирования, которые загромоздили бы контекст general-purpose агента. Специализированный Sonnet-агент с Read+Edit и фокусной подсказкой делает задачу быстрее, дешевле и надёжнее.
Statusline Setup иллюстрирует важный принцип: иногда специализированный агент лучше, чем general-purpose с большим контекстом. General-purpose с документацией по статусной строке мог бы сделать всё верно, но стоил бы дороже, был бы медленнее и вероятнее запутался в синтаксических нюансах.
Worker Agent (Coordinator Mode)
Не находится в built-in/ директории, но загружается динамически при активном режиме координатора:
if (isEnvTruthy(process.env.CLAUDE_CODE_COORDINATOR_MODE)) {
const { getCoordinatorAgents } = require('../../coordinator/workerAgent.js')
return getCoordinatorAgents()
}
Worker-agent заменяет все стандартные встроенные агенты в режиме координатора. У него один тип "worker" и полный доступ к инструментам. Это упрощение осознанно — когда координатор оркестрирует воркеров, координатор определяет, что воркер делает. Воркеру не нужны специализации Explore или Plan; ему нужна гибкость выполнять назначенное.
Fork-агенты
Fork-агенты — где ребёнок наследует всю историю беседы родителя, системный prompt и массив инструментов ради эксплуатации кэша подсказок родителя — подробно рассмотрены в главе 9. Путь fork срабатывает, когда модель опускает subagent_type в вызове Agent и эксперимент fork активен. Каждое проектное решение в системе fork сводится к единой цели: байтово-идентичные префиксы API-запросов для параллельных детей, что даёт до 90% скидки на входные токены. Для родителя это разница между тратой $4 и $0.50 на одинаковую параллельную рассылку.
Определения агентов через frontmatter
Пользователи и плагины могут определять кастомных агентов, помещая markdown-файлы в .claude/agents/. Схема frontmatter поддерживает полный спектр конфигураций агента:
---
description: "Когда использовать этого агента"
tools:
- Read
- Bash
- Grep
disallowedTools:
- FileWrite
model: haiku
permissionMode: dontAsk
maxTurns: 50
skills:
- my-custom-skill
mcpServers:
- slack
- my-inline-server:
command: node
args: ["./server.js"]
hooks:
PreToolUse:
- command: "echo validating"
event: PreToolUse
color: blue
background: false
isolation: worktree
effort: high
---
# Мой пользовательский агент
Вы — специализированный агент для...
Тело markdown становится системным prompt агента. Поля frontmatter напрямую мапятся на интерфейс AgentDefinition, который потребляет runAgent(). Пайплайн загрузки в loadAgentsDir.ts валидирует frontmatter против AgentJsonSchema, разрешает источник (user, plugin или policy) и регистрирует агента в списке доступных.
Существуют четыре источника определений агентов, в порядке приоритета:
- Встроенные агенты — хардкод в TypeScript, всегда доступны (при условии фиче-гейтов)
- Пользовательские агенты — markdown в
.claude/agents/ - Плагин-агенты — загружаются через
loadPluginAgents() - Политические агенты — загружаются через настройки организационной политики
Когда модель вызывает Agent с subagent_type, система резолвит имя в этом комбинированном списке, фильтрует по правилам разрешений (deny rules для Agent(AgentName)) и по allowedAgentTypes из спецификации инструмента. Если запрошенный тип не найден или запрещён, вызов инструмента падает с ошибкой.
Это даёт возможность организациям поставлять свои агенты через плагины (code review agent, security audit agent, deployment agent), и они появятся рядом со встроенными агентами. Модель видит их в одном списке, с одним интерфейсом, и делегирует им так же, как встроенным.
Сила frontmatter-определений в том, что для них не нужен TypeScript. Лид команды, желающий «agent для PR-review», пишет markdown с нужным frontmatter, кладёт его в .claude/agents/, и он появляется в списке агентов у всех участников команды на следующей сессии. Системный prompt — тело markdown. Поля frontmatter объявляются в YAML. runAgent() жизненный цикл обрабатывает всё остальное — те же пятнадцать шагов, та же очистка, те же гарантии изоляции.
Это также означает, что определения агентов версионируются вместе с кодовой базой. Репозиторий может поставлять агентов, привязанных к архитектуре, конвенциям и тулкиту. Агенты эволюционируют вместе с кодом. Когда команда переходит на новый тестовый фреймворк, prompt verification-агента обновляется в том же коммите, что и добавление новой зависимости.
Есть важное соображение безопасности: граница доверия. Пользовательские агенты (из .claude/agents/) контролируются пользователем — их хуки, MCP-серверы и конфигурации инструментов подлежат ограничению strictPluginOnlyCustomization, когда политика активна. Плагин-агенты и policy-агенты — доверенные админом и обходят эти ограничения. Встроенные агенты — часть бинарника Claude Code. Система точно отслеживает source каждого определения, чтобы политики безопасности могли отличать «это написал пользователь» от «это одобрила организация».
Поле source — не просто метаданные — оно контролирует поведение. Когда политика plugin-only активна для MCP, frontmatter пользовательского агента, объявляющий MCP-серверы, игнорируется (MCP-подключения не устанавливаются). Когда политика plugin-only активна для хуков, пользовательские хуки не регистрируются. Агент по-прежнему работает — просто без непроверенных расширений. Это принцип graceful degradation: агент полезен даже под ограничениями политики.
Примените это: проектирование типов агентов
Встроенные агенты демонстрируют языковой шаблон (pattern language) для дизайна агентов. Если вы строите систему порождения суб-агентов — будь то через AgentTool Claude Code или ваша собственная многопроцессная архитектура — пространство проектирования разбивается на пять измерений.
Измерение 1: Что он видит?
Комбинация omitClaudeMd, обрезки git status и предзагрузки скиллов контролирует осведомлённость агента. Read-only агенты видят меньше (им не нужны проектные соглашения). Специализированные агенты видят больше (предзагруженные скиллы инжектируют доменное знание).
Ключевая идея: контекст не бесплатен. Каждый токен в системном prompt, user context или истории беседы стоит и вытесняет память. Claude Code отрезает CLAUDE.md у Explore не потому, что инструкции вредны, а потому что они не релевантны — и на 34 миллионы вызовов в неделю иррелевантность становится статьёй затрат. При проектировании своих типов агентов спрашивайте: «Что нужно знать этому агенту, чтобы выполнить свою задачу?» — и отрезайте всё остальное.
Измерение 2: Что он может делать?
Поля tools и disallowedTools устанавливают жёсткие границы. Verification не может редактировать файлы. Explore не может ничего записывать. General-Purpose может всё, кроме порождения собственных суб-агентов.
Ограничения инструментов служат двум целям: безопасности (верификатор не может «подправлять» найденные баги, сохраняя независимость) и фокусу (агент с меньшим набором инструментов тратит меньше времени на решение, какой инструмент выбрать). Комбинация механического ограничения инструментов и системного prompt-а (например, === CRITICAL: READ-ONLY MODE ===) даёт defence in depth — инструменты механически соблюдают границу, а подсказка объясняет модели почему, чтобы не тратить ходы на попытки обойти это.
Измерение 3: Как он взаимодействует с пользователем?
permissionMode и canShowPermissionPrompts определяют, спрашивает ли агент разрешение, авто-отклоняет или «вынимает» промпт в терминал родителя (bubble). Фоновые агенты, которые не могут прерывать пользователя, либо работают в пределах предодобренных границ, либо делают bubble.
Настройка awaitAutomatedChecksBeforeDialog — важная тонкость. Фоновые агенты, которые могут показывать промпты, сначала запускают классификатор и хуки, и только если автоматическое разрешение не сработало, прерывают пользователя. В системе, где одновременно работают пять фоновых агентов, это разница между удобным интерфейсом и лавиной промптов.
Измерение 4: Как он относится к родителю?
Sync-агенты блокируют родителя и делят его состояние. Async-агенты работают независимо с собственным контроллером прерывания. Fork-агенты наследуют полную историю беседы родителя. Выбор формирует UX (ждёт ли родитель?) и поведение системы (Escape убивает ребёнка?).
Решение про abort controller в Шаге 8 воплощает это: синхронные агенты разделяют контроллер родителя (Escape убивает оба), асинхронные — независимы (Escape не останавливает их). Fork идёт дальше — наследует системный prompt, массив инструментов и историю сообщений ради максимального кеширования. Каждая связь имеет своё применение: sync для последовательной делегации, async для параллельной работы, fork для делегации, требующей всего контекста.
Измерение 5: Насколько он дорогой?
Выбор модели, thinking config и размер контекста влияют на стоимость. Haiku — для дешёвой read-only работы. Sonnet — для умеренных задач. Inherit — когда нужна та же способность рассуждения, что и у родителя. Thinking отключён для нестартовых агентов, чтобы контролировать расходы токенов — родитель платит за рассуждение, дети исполняют.
Экономическое измерение часто упускают в дизайне мультиагентных систем, но оно центрально в архитектуре Claude Code. Explore на Opus работал бы корректно для отдельного вызова. Но при 34 миллионах вызовов в неделю выбор модели — множитель стоимости. Оптимизация one-shot, экономящая 135 символов на вызов, превращается в 4.6 миллиарда символов за неделю. Это не микрооптимизации — это граница между жизнеспособным продуктом и неприемлемым расходом.
Единый жизненный цикл
Жизненный цикл runAgent() реализует все пять измерений через свои пятнадцать шагов, собирая уникальное окружение выполнения для каждого типа агента из одних и тех же строительных блоков. Результат — система, где порождение суб-агента не равно «запуск ещё одной копии родителя». Это создание точно-ограниченного, ресурсно-контролируемого, изолированного контекста выполнения — специализированного под задачу и полностью очищаемого по её завершении.
Архитектурная элегантность в единстве. Независимо от того, Haiku-агент read-only или Opus-fork child с полным доступом инструментов и bubble permissions — все они проходят через одни и те же пятнадцать шагов. Эти шаги не ветвятся по типу агента — они параметризуются. Разрешение модели выбирает подходящую модель. Подготовка контекста выбирает нужный кеш файлов. Изоляция разрешений задаёт режим. Тип агента не в управляющей логике, а в конфигурации. Это делает систему расширяемой: добавить новый тип — значит написать определение, а не менять жизненный цикл.
Резюме пространства проектирования
Шесть встроенных агентов охватывают спектр:
| Агент | Модель | Инструменты | Контекст | Sync/Async | Назначение |
|---|---|---|---|---|---|
| General-Purpose | Default | Все | Полный | Любой | Общая делегация |
| Explore | Haiku | Read-only | Обрезанный | Sync | Быстрый дешёвый поиск |
| Plan | Inherit | Read-only | Обрезанный | Sync | Проектирование архитектуры |
| Verification | Inherit | Read-only | Полный | Всегда async | Антагонистические тесты |
| Guide | Haiku | Read + Web | Динамичный | Sync | Поиск документации |
| Statusline | Sonnet | Read + Edit | Минимальный | Sync | Конфиг статусной строки |
Ни один агент не делает одинаковых выборов по всем пяти измерениям. Каждый оптимизирован под свою задачу. А runAgent() жизненный цикл обрабатывает их всех через те же пятнадцать шагов, параметризованных определением агента. Это сила архитектуры: жизненный цикл — универсальная машина, а определения агентов — программы, работающие на ней.
Примените это: проектирование типов агентов
Встроенные агенты демонстрируют языковой шаблон (pattern language) для дизайна агентов. Если вы строите систему порождения суб-агентов — будь то через AgentTool Claude Code или ваша собственная многопроцессная архитектура — пространство проектирования разбивается на пять измерений.
(дальше текст главы — см. полный перевод в файле)