temporality
Изменить страницу↗
Инженерные документы

Делегирование задач между агентами

#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 независимо»). Такие запуски выполняются конкурентно, а не по очереди:

#Причинность фиксируется в журнале

Родитель эмитит 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?)

Результат инструмента — статус, идентификатор дочернего рана и его финальный ответ (ограниченный по объёму).

Несколько вызовов delegate в одном ответе модели — параллельные независимые запуски (см. «Параллельное делегирование» выше): родитель получает все результаты одновременно в конце хода.

Отклонения (невалидные аргументы, лимит глубины, неизвестный агент) происходят до запуска дочернего рана и не создают эффектов: effect=none.

#Планы (DAG): инструмент plan

plan(goal?, max_rework?, tasks: [{id, agent_id, prompt, depends_on?, review_of?, max_turns?}])

Когда работа — это граф с зависимостями (несколько задач параллельно, потом ревью комплекта, потом QA пачкой), родитель выдаёт один вызов plan вместо серии delegate. Дальше граф исполняет ядро — механически, без участия модели-родителя:

#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?: "короткое итоговое ревью")

Вызов вердикта завершает ран ревьюера. Семантика:

Инвалидация потребителей. Результат доработки меняет вход всех, кто уже его потребил: транзитивные зависимые открытой задачи сбрасываются в 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 на «ребёнок завершился ИЛИ пришла заметка»):

Механизм рекурсивен: ребёнок, сам ждущий внуков, применяет тот же selector и форвардит дальше.


#6. Ограничения (зафиксировано осознанно)


#7. UI


#8. План реализации

  1. Модель: AgentCapabilities.Delegation *bool (delegation, nil = запрещено).
  2. Ядро: RunInput.DelegationDepth, MaxDelegationDepth=2, инструмент delegate в KernelTools, ветка в tool-loop workflow, активность kernel.resolve_agent, agent.WorkflowID.
  3. Workspace API: GET /agents/{id}/run-config, запрет delegate в applyAgentConfig при выключенной capability.
  4. Тесты: workflow-тест делегирования (события, результат, parent-ссылки), тест лимита глубины.
  5. UI: переключатель capabilities + локализация.
  6. Live-проверка: включить делегирование агенту, делегировать из чата.

#9. Критерии готовности