Делегирование задач между агентами
#1. Цель
Агент в ходе выполнения задачи должен уметь передать самодостаточную подзадачу другому агенту и получить её результат — так же, как специалист передаёт задачу коллеге с узкой компетенцией.
Делегирование — это runtime-инструмент delegate, доступный модели, а не
заранее сконфигурированный пайплайн. Состав команды не зашит в код: агент сам
решает, когда и к кому обратиться, в пределах выданных ему прав.
Пользователь при этом не составляет цепочки агентов вручную. Он лишь включает агенту возможность делегирования и наблюдает результат в журнале.
#2. Принципы
#Делегированный запуск — это настоящий run
Дочерняя задача выполняется как полноценный durable AgentRun
(Temporal child workflow): его можно отменить, он переживает рестарт воркера,
его траектория попадает в журнал Temporality.
Родитель дожидается результата: delegate — синхронный инструмент, ответ
дочернего рана возвращается модели как результат tool-вызова.
#Ребёнок работает только со своими правами
Конфигурация дочернего рана разрешается из определения дочернего агента:
модель, скомпилированный системный промт, capabilities, sandbox, MCP-серверы,
skills. Родитель не может ни расширить права ребёнка, ни передать ему свои:
enforcement жёсткий, на уровне инструментария, а не промта
(docs/plan-evaluable-agent.md §4.1).
Наследуется и security context инициатора: дочерние раны получают его
орг-позицию и замороженный снимок файловой политики (policy_version) —
вся ветка дерева работает полномочиями человека, запустившего работу.
#Делегирование — это capability, по умолчанию выключенная
В AgentCapabilities появляется поле delegation. В отличие от остальных
capabilities (nil = разрешено), делегирование по умолчанию запрещено:
nil или false → инструмент delegate не рекламируется и отклоняется.
Включает его только человек явным переключателем в карточке агента.
#Глубина ограничена
DelegationDepth рана: 0 у обычного запуска, +1 на каждый уровень.
Цепочка длиннее MaxDelegationDepth (2) отклоняется: агент на предельной
глубине обязан выполнить работу сам. Это защита и от циклов, и от «передач
по кругу».
#Параллельное делегирование
Модель может выдать несколько вызовов delegate в одном ответе — это
штатный способ раздробить работу («сделай X, Y и Z независимо»). Такие
запуски выполняются конкурентно, а не по очереди:
- каждый
delegateстартует дочерний workflow сразу, не дожидаясь предыдущих (delegation.startedс очереднымordinal); - родитель блокируется только на ожидании результата — все неоплаченные делегирования добираются в конце хода, поэтому каждый результат приземляется внутри того же хода, что и вызов;
- ширина ограничена
MaxDelegationWidth(8): перед стартом следующего ребёнка родитель ждёт освобождения слота — в журнале это видно какdelegation.completedперед очереднымdelegation.started; - запуск и ожидание строго FIFO — порядок событий детерминирован и воспроизводим при replay;
- идентичные (дедуплицированные) вызовы
delegateв одном ответе делят один дочерний ран: результат выдаётся каждому вызову, побочный эффект не размножается; - при
RequireToolApprovalодобренный вызовdelegateзапускает дочерний ран напрямую (не через исполнение обычного tool-activity); - если родитель упал или был отменён, дочерние раны останавливаются стандартной политикой Temporal (child cancellation / parent close), а их неопределённые эффекты классифицирует реконсиляция (docs/failure-reconciliation.md).
#Причинность фиксируется в журнале
Родитель эмитит delegation.started / completed / failed c child_run_id.
Дочерний ран стартует с parent_run_id / parent_frame_id / parent_event_id,
поэтому событийная цепочка непрерывна: из ответа агента можно провалиться
в delegating-событие, из него — в полную траекторию дочернего рана.
#Отмена распространяется вниз
Отмена родительского рана отменяет дочерние (Temporal child workflow cancellation). Остановка чата останавливает и делегированные работы.
#Ошибка ребёнка не роняет родителя
Если дочерний ран упал, родитель получает результат tool-вызова с описанием
ошибки и предупреждением о неопределённости эффектов — и продолжает работу
сам. delegation.failed фиксирует неудачу в журнале.
#Бюджеты
max_turns дочернего рана: 24–100 (значения ниже 24 поднимаются до 24);
без аргумента действует бюджет карточки агента.
Делегированные запуски не расходуют дневную квоту проекта — они часть
родительского рана.
#3. Инструмент delegate
delegate(agent_id, prompt, max_turns?)
agent_id— идентификатор агента из workspace (обязателен). Агент должен существовать и быть доступен в проекте (глобальный или привязанный к проекту).prompt— самодостаточная формулировка подзадачи (обязателен). В неё передаётся всё, что ребёнку нужно знать: родитель не может «дописать» ему права или системный промт.max_turns— бюджет ходов ребёнка, 24–100 (значения ниже 24 поднимаются до 24); без аргумента — бюджет карточки агента.
Результат инструмента — статус, идентификатор дочернего рана и его финальный ответ (ограниченный по объёму).
Несколько вызовов delegate в одном ответе модели — параллельные
независимые запуски (см. «Параллельное делегирование» выше): родитель
получает все результаты одновременно в конце хода.
Отклонения (невалидные аргументы, лимит глубины, неизвестный агент) происходят
до запуска дочернего рана и не создают эффектов: effect=none.
#Планы (DAG): инструмент plan
plan(goal?, max_rework?, tasks: [{id, agent_id, prompt, depends_on?, review_of?, max_turns?}])
Когда работа — это граф с зависимостями (несколько задач параллельно, потом
ревью комплекта, потом QA пачкой), родитель выдаёт один вызов plan вместо
серии delegate. Дальше граф исполняет ядро — механически, без участия
модели-родителя:
- задачи без общих зависимостей запускаются параллельно (до
MaxDelegationWidth=8одновременно); сверх этого — по мере освобождения слотов. Внимание: все задачи работают в ОДНОМ общем workspace проекта без изоляции — параллелить можно только задачи с непересекающимися файлами и read-only работой (исследования, поиск, ревью); всё, что пишет файлы, собирает, тестирует или исполняет git, должно быть сцеплено черезdepends_onили объединено в одну задачу — параллельные писатели на одном дереве затирают друг друга; - задача стартует только когда все её
depends_onуспешно завершились; - ответы завершённых задач автоматически дописываются в промт зависимой
задачи секцией
Upstream results; если модель хочет разместить их сама — плейсхолдеры{{task-id.answer}}в промте (тогда авто-секция не добавляется); - неудача задачи пропускает только её транзитивных зависимых
(
plan.task.skipped,reason=upstream_failed); независимые ветки дорабатывают. Аргументon_failureменяет цену неудачи:stop— после первой упавшей или незавершённой задачи новые запуски прекращаются (незапущенные пропускаются сreason=plan_stopped, включая rework-раунды), бегущие ветки дорабатывают, сводка приходит по их завершении (plan.stopped,plan.completed.stopped_by) — отсекает хвост без убийства живой работы;abort— жёсткий стоп: бегущие дети отменяются, сводка возвращается немедленно (plan.aborted,plan.completed.aborted_by) — только для планов, где хвост бессмыслен в любом случае; - режим
mode="wave"возвращает управление родителю после каждой готовой партии: вызов исполняет первый слой DAG, сводка называет незапущенные задачи (plan.completed.remaining), и родитель пере-выпускает остаток исправленным планом — ответы завершённых задач доступны через{{task-id.answer}}(состояние остатка DAG несёт модель, ядро хранит только ответы). Это штатный механизм пошагового контроля: фидбек после каждой волны, коррекция, остановка (не пере-выпускать) и перезапуск (включить задачу в новый план); - итог — сводка в tool-результате: статус каждой задачи и ограниченные по
объёму ответы; модель-родитель решает, чинить ли хвост или пере-планировать
(пере-план опирается на завершённые результаты через
{{task-id.answer}}).
#Acceptance gates и rework-циклы
DAG по определению ацикличен, но реальная работа — это цикл «сделал → принял/вернул на доработку». Поэтому rejects реализованы не ребром назад, а рантайм-состоянием исполнителя плана.
Задача-ревьюер объявляется явно:
{"id": "review", "agent_id": "reviewer", "review_of": ["api", "tests"], "prompt": "..."}
review_of автоматически входит в depends_on (ревьюер обязан потреблять
то, что принимает). Такому ребёнку в ран подмешивается дополнительный
инструмент submit_review:
submit_review(verdict: "accept" | "rework",
feedback?: {task-id: замечания},
summary?: "короткое итоговое ревью")
Вызов вердикта завершает ран ревьюера. Семантика:
accept— работа принята; план продолжает как обычно;rework— задачи из ключейfeedbackотправляются на доработку: задача перевозвращается в pending, её новый промт = исходный + «Reviewer feedback»- прошлый ответ, раунд фиксируется (
plan.task.rejected,plan.task.reopened); задачи не изfeedbackсчитаются принятыми;
- прошлый ответ, раунд фиксируется (
- нет вердикта (ран ревьюера упал или модель не вызвала инструмент) —
безопасный дефолт
accept, план не блокируется.
Инвалидация потребителей. Результат доработки меняет вход всех, кто уже
его потребил: транзитивные зависимые открытой задачи сбрасываются в pending
(их ответы удаляются, plan.task.invalidated), бегущие — отменяются
(RequestCancelExternalWorkflow), сам ревьюер тоже перезапускается.
Независимые ветки не трогаются и продолжают работать.
Ограничители. max_rework на план (0–4, по умолчанию 2): сколько раз
задачу можно вернуть. Исчерпание — честный plan.task.failed с
error_type=rework_exhausted, зависимые получают invalidated/
skipped(upstream_rework_exhausted) — результат отвергнут, а не «как будто
принят». Плюс общий бюджет запусков tasks × (1 + max_rework): исчерпан —
оставшиеся pending пропускаются (reason=execution_budget). Безлимитных
циклов «кодер ↔ ревьюер» нет.
В сводке плана видна история отбраковок: кто вернул, с какими замечаниями, сколько раундов заняло — качество работы видно из траектории, а не только из последнего кадра.
Re-plan. Ответы задач сохраняются на весь ран: следующий план того же
рана может ссылаться на них через {{task-id.answer}}, не перезапуская
работу. Так «первые две задачи → ревью → доработка» — это два плана, где
второй переиспользует ответы первого.
Валидация до запуска. Вся спецификация проверяется до старта первого
ребёнка: уникальность и формат id (^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$),
неизвестные и циклические зависимости, ссылки на чужие ответы, лимит
16 задач, разрешение всех агентов up front. Невалидный план —
tool.failed с error_type=plan_invalid, effect=none, ничего не
запустилось. Опечатка в agent_id не запускает частичный fan-out.
plan подчиняется тем же правилам, что и delegate: capability
Delegation, лимит глубины, approval-режим политики (план — не policy-safe
инструмент), права ребёнка не расширяются правами родителя.
#4. Разрешение дочерней конфигурации
Workflow не ходит в БД. Конфигурация ребёнка разрешается активностью
kernel.resolve_agent, которая вызывает workspace API:
GET /v1/workspace/agents/{id}/run-config?project={project}
Эндпоинт возвращает RunInput, собранный тем же кодом, что и запуск рана
вручную: applyAgentConfig (промт по определению, enforcement capabilities,
skills, MCP) + applyProjectModel. Требуется роль writer и принадлежность
агента проекту. Этим гарантируется одна кодовая точку правды для запуска
«вручную» и «через делегирование».
Фолбэк по имени. Модель подставляет в agent_id не id, а выведенное из
отображаемого имени значение («Lead» → lead), и id не всегда выводимы из
имён (lead-ai-agent). Поэтому {id} резолвится в два шага: точное
совпадение по id, затем — уникальное нечувствительное к регистру совпадение
по имени в скоупе проекта (глобальные + проектные агенты, как в ростере).
Неоднозначное имя → 404 со списком id-кандидатов, промах → обычный 404
(существование агентов других проектов не раскрывается). Тело ошибки
сервера доходит до модели в тексте AgentNotAvailable — кандидатам можно
следовать сразу, без повторного угадывания.
Ростер агентов. Инструмент agents (delegate/plan-цепочка: denied для
агентов без capability Delegation и на предельной глубине) отдаёт список
доступных для делегирования агентов проекта: точные agent_id, имена,
назначение и профиль capabilities (включая can_delegate цели — важно при
планировании цепочек). Описание инструмента прямо требует вызвать его до
delegate/plan и использовать выданный agent_id дословно.
Активность дополнительно вызывает PrepareRun (инструменты, sandbox-путь) и
отклоняет delegate в конфигурации ребёнка, находящегося на предельной
глубине.
Резолв на запуске, а не снапшот на старте плана. Валидация разрешает всех
агентов up front (эффект none для опечатки), но результат — только проверка.
Каждый запуск задачи — в том числе отложенный (после зависимостей) и раунд
доработки — резолвит конфиг заново: план живёт часами, и изменение конфига
агента в середине (поднятый бюджет ходов, другая модель, ротация политик)
должно доходить до запусков, которые ещё впереди. Провал резолва на запуске
валит только эту задачу (plan.task.failed, error_type= agent_resolution_failed, effect=none) и честно скипает зависимых.
#5. События
| Событие | Где | Данные |
|---|---|---|
tool.started / completed / failed |
родитель | обычная семантика tool-вызова |
delegation.started |
родитель | child_run_id, agent_id, ordinal, depth |
delegation.completed |
родитель | child_run_id, agent_id, child_status, turns |
delegation.failed |
родитель | child_run_id, agent_id, error_type |
plan.started |
родитель | operation_id, goal, tasks: [{id, agent_id, depends_on}] |
plan.task.started |
родитель | task_id, child_run_id, agent_id, depends_on, round, max_turns |
plan.task.completed |
родитель | task_id, child_run_id, child_status, turns |
plan.task.failed |
родитель | task_id, child_run_id, error_type, effect (+ child_ops_*) |
plan.task.skipped |
родитель | task_id, reason (upstream_failed / upstream_rework_exhausted / upstream_incomplete / run_time_limit / execution_budget / plan_stopped), blocked_by |
plan.task.incomplete |
родитель | task_id, child_run_id, child_status — задача завершилась не своим финальным ответом |
plan.completed |
родитель | statuses: финальный статус каждой задачи |
run.* / turn.* / model.* / tool.* |
ребёнок | обычная траектория, с parent_run_id и parent_event_id |
Идентификатор дочернего рана: <parent-run-id>/delegate/<NN> — стабильный,
читаемый в журнале. Для задач плана: <parent-run-id>/plan/<NN>-<task-id>.
Workflow ID дочернего рана вычисляется той же функцией
agent.WorkflowID(sourceID, project, runID), что и для обычных запусков.
#5.1. Steering: дополнения во время делегации
Пока лид заблокирован на ожидании дочерних ранов, он не выходит в модельный ход — заметка из чата, отправленная в этот момент, раньше ждала завершения всей делегации. Решение (Temporal-каноника — selector на «ребёнок завершился ИЛИ пришла заметка»):
- ожидание ребёнка (
delegateи обе фазыplan) ждёт одновременно будущее ребёнка и каналkernel.user_note; - пришедшая заметка немедленно сигналом (
SignalExternalWorkflow) доставляется в почтовый ящик каждого ещё выполняющегося ребёнка — тот дренирует её своим обычным механизмом на ближайшем собственном ходе и получает уточнение без перезапуска; - лид журналирует
run.user_noteсforwarded_to(списокchild_run_id) и кладёт заметку в свою историю (append-only, кэш-промпт стабилен), поэтому его модель на следующем ходе видит, что было уточнено в процессе; - журналирование на лиде best-effort: сбой записи не должен интерпретироваться как провал живого ребёнка;
- дети, завершившиеся между дренированием и сигналом, попадают в
forward_failed— заметка остаётся обычным дополнением лида.
Механизм рекурсивен: ребёнок, сам ждущий внуков, применяет тот же selector и форвардит дальше.
#6. Ограничения (зафиксировано осознанно)
- Дочерние раны не создают строк в списке Runs: они наблюдаемы через журнал
(timeline,
parent_run_id), но не через таблицу запусков. Отдельный вид «дерево рана» — Phase 2. - Approvals дочернего рана разрешаются его собственной политикой и каналом
одобрения; родитель не может одобрить за ребёнка.
planкак единый вызов одобряется целиком у родителя. - Сырые запуски через
POST /v1/agent/runs(без агента workspace) могут делегировать: это привилегированный путь с ролью writer, capability-гейт применяется к запускам агентов workspace. - План исполняется синхронно внутри одного tool-вызова: ход родителя заблокирован
до завершения графа (при тайм-лимите рана незапущенные задачи честно
пропускаются с
reason=run_time_limit, бегущие дети дорабатывают), поэтому пошаговый контроль родителя реализуется режимомmode="wave", а ранняя реакция на падение — политикамиon_failure=stop|abort; полная интерактивная модификация живого плана не реализована. Один план — до 16 задач, параллельность внутри плана иdelegateтого же хода ограничены каждая своимMaxDelegationWidth.
#7. UI
- Карточка агента → Capabilities: отдельный переключатель «Делегировать другим агентам» (инвертированная семантика: выключен по умолчанию).
- В чате
delegateотображается как обычный tool call; специальный виджет с деревом делегирования — Phase 2. - Builtin-агенты поставляются без делегирования; пользователь включает его сам, если хочет строить команды.
#8. План реализации
- Модель:
AgentCapabilities.Delegation *bool(delegation, nil = запрещено). - Ядро:
RunInput.DelegationDepth,MaxDelegationDepth=2, инструментdelegateвKernelTools, ветка в tool-loop workflow, активностьkernel.resolve_agent,agent.WorkflowID. - Workspace API:
GET /agents/{id}/run-config, запретdelegateвapplyAgentConfigпри выключенной capability. - Тесты: workflow-тест делегирования (события, результат, parent-ссылки), тест лимита глубины.
- UI: переключатель capabilities + локализация.
- Live-проверка: включить делегирование агенту, делегировать из чата.
#9. Критерии готовности
- Агент с выключенной capability не видит
delegate; с включённой — делегирует, получает ответ, в журнале видна полная цепочкаdelegation.started → дочерняя траектория → delegation.completed. - Глубина 3 блокируется, родитель получает понятное сообщение и продолжает.
- Отмена чата останавливает дочерние раны.
- Ребёнок не получает инструментов, отключённых его определением, независимо от прав родителя.