Инженерные документы
Межагентное взаимодействие: заметки к реализации (docs/temporality-agent-collaboration-spec.md)
Сопроводительные заметки к ТЗ: решения, принятые при встраивании в существующую кодовую базу, и границы первого этапа. Обновляется по мере развития.
#1. Что реализовано
get_collaborators(тул, activity-side,kernel/agent/collab.go): участники саги — агенты (изrun.started+ терминальных событий задач саги, включая все вложенные делегации: дети наследуютtask_id, поэтому task-фильтр покрывает дерево целиком) и люди (подписантыapproval.granted/human.answered/ авторыrun.user_noteв скоупе). Формат — из ТЗ §3:id(agent:<agent_id>/human:<user_id>/ для легаси-ранов безagent_id—run:<run_id>),kind,role(изdata.role),task(title рана, ≤200),status,run_id.ask_agent(тул, inline-ветка workflow, режимыwait/async, дефолт —async): доставкаAgentMessageсигналомkernel.agent_messageв workflow адресата (SignalExternalWorkflow(WorkflowID(source, project, run))), ответ сигналомkernel.agent_replyназад.waitпаркует ход на ответ (детерминированный await по образцуawaitApproval); таймаут (дефолт 300с, потолок 3600с) не отменяет запрос — он остаётся у получателя, поздний ответ доезжает по асинхронному пути (ТЗ §4).answer_agent(тул получателя): ответ маршрутируется поrequest_idиз числа реально полученных этим ходом запросов; повторный ответ на тот же id отказывается (никакого двойного сигнала). Если отправитель к моменту ответа завершился — ответ всё равно журналируется (agent.replied), а тул сообщает, что доставить некуда.- Доставка входящих — на границах хода, по образцу user-note steering:
входящий запрос/ответ становится user-сообщением с фиксированной
обвязкой (request_id, отправитель, тип; для
wait— явная просьба ответить следующим батчем тулов). Никогда не прерывает текущую работу.
#2. Ключевые решения
| Решение | Почему |
|---|---|
| Состояние запроса — только журнал + история workflow (без новой таблицы) | Сигналы Temporal живут в event history: переживают рестарт worker/kernel в пределах гарантий инфраструктуры (§5); journal-only — устоявшаяся альтернатива (сага устроена так же). Отдельный стор дал бы вторую истину |
request_id = req- + hash(operationID) |
Стабилен при ретраях одного вызова (тот же operationID), уникален для нового вызова |
Адресация: agent:<id> / run:<id> / голый run id |
agent:<id> резолвится в ран агента в скоупе саги (последний по времени старта). Живой ран получает сообщение сигналом; завершённый — релейный follow-up run (см. ниже); новый ран не стартуется только если агент вообще не резолвится |
Релей-ран для завершённых агентов (<target>/ask/NN) |
Делегированные дети завершаются быстро — без релея задавать вопросы можно было бы только живому лиду. Релей — это дочерний AgentRun того же агента (ActivityResolveAgent + ExecuteChildWorkflow, механика делегирования): короткий бюджет (6 ходов перекрывают floor clamp после резолва), prompt самодостаточен (кто спросил, request_id, payload, указание ответить answer_agent и закончить), ParentRunID = исходный ран (линейность и сага по task), DelegationDepth = MaxDelegationDepth (ask-run не может делегировать), IncomingAsks в RunInput — request_id валиден с первого хода без гонки сигналов, ParentClosePolicy: ABANDON (asker может завершиться раньше). Файловый снапшот наследуется от инициатора (как у делегирования — §6: за пределами унаследованной области прав не прибавляется) |
Адресат проверяется kernel-side (ResolveCollaborator) |
§6: доверять to от модели нельзя; резолвер ограничен задачами саги просящего run'а — чужой агент не резолвится в принципе |
GetVersion("agent-collaboration") гейтит drain'ы и рекламу тулов |
Реплей-совместимость: in-flight раны со старой историей не видят ни сигналов, ни тулов (docs/failure-model.md) |
ask_agent/answer_agent/get_collaborators в policySafeTools |
Approval round-trip на каждый шаг диалога убил бы механизм (§8); deny через карточку агента (ToolAllowlist/DenyTools) остаётся оружием политики |
| Ответы «не по адресу» не теряются | awaitAgentReply не выбрасывает чужие ответы (в отличие от awaitApproval): они пере-очередуются и фреймятся на следующей границе хода. В relay-wait селекторе то же: child future ∥ reply-канал ∥ таймер |
#3. События журнала
agent.asked → agent.message.received (получатель) → agent.replied
(получатель) → agent.answer.received (отправитель); терминальные:
agent.ask_expired (таймаут wait-режима), agent.ask_undeliverable
(адресат не найден / релей не стартовал). В agent.asked при релее
добавляются relay: true и relay_run. Payload усечён до 4096
(как user-note). Каталог UI: группа «Межагентное взаимодействие» в
eventCatalog.
#4. Границы первого этапа (осознанные)
- Тулы не пробивают allowlist карточки агента: агент с непустым
списком
toolsне увидит новые тулы — миграцию карточек делает оператор (осознанно: allowlist — и есть политика). - Парковка в
ask_humanзадерживает доставку: входящий запрос ждёт в буфере сигналов до ближайшей границы хода (та же семантика, что у user-note в этом состоянии). Не теряется — задерживается. - Люди в списке — только акторы журнала (кто согласовывал/отвечал/ писал note). Проективные роли («assignee задачи») не выводятся — у задачи нет поля assignee.
roleучастника — изdata.roleрана; имена агентов не обогащаются (доступны через тулagents).- wait-режим считает wall-clock время против
TimeoutSecondsполитики рана — как иask_human; большие таймауты могут привести кpolicy.time_exhaustedсразу после ответа.