Перейти к содержимому

Human-in-the-loop: агент предлагает

Продуктная ставка — «агенты строят, люди отлаживают». Полный автопилот хорош для черновиков; для продакшена нужна граница доверия. Proposal-режим (ADR 0007) даёт её на существующей MCP-поверхности — без второго протокола.

Включение

const disconnect = await editor.connectMCP('ws://localhost:6410/ws?token=…', {
mode: 'propose', // по умолчанию 'auto' — мутации применяются сразу
clientId: 'support-agent',
})
const queue = editor.mcpProposals! // не-null после подключения propose-сессии

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

Что видит агент

Мутирующие вызовы перестают применяться. Каждый возвращает честную квитанцию:

{ "proposed": true, "proposalId": 7, "provisionalNodeId": "01a0…", "queued": 3 }

…так что промпт агента может объяснить модели наличие очереди. Инструменты, чеканящие ноды (add_node, instantiate_recipe), выдают провизорные id, на которые агент может ссылаться в последующих предложениях (connect_pins, set_widget_value, …) — approval переведёт их в реальные. Read/view-инструменты (get_graph, find_nodes, …) работают LIVE по пред-approval-графу.

Очередь ревью

queue.entries() // [{ id, ts, clientId, tool, summary, args }, …] старые первыми
queue.onChange(size => renderBadge(size))
queue.approve() // ОДНА транзакция command bus: один undo-шаг, АТОМАРНО
queue.reject() // выбросить всё (или передать id)

Семантика (ADR 0007):

  • Атомарные батчи. Approve проигрывает очередь по порядку в одной транзакции. Любой сбой откатывает ВЕСЬ батч — очередь сохраняется для повтора.
  • Резолв на approval. Предложения проигрываются по ТЕКУЩЕМУ графу: label’ы пинов и layout резолвятся на момент одобрения, а не предложения. Протухшее предложение падает безопасно.
  • Undo — на транзакцию. Один Ctrl+Z откатывает последний одобренный батч (или последнее человеческое действие) — правки агента и человека законно чередуются через сериализованную шину. CRDT-магии нет; merge-семантика одновременного редактирования явно вне скопа.
  • Аудит на approval. Предложение ничего не меняет и не аудитирует; при применении батча audit-кольцо записывает его с дельтами эффектов и clientId автора (audit log).

Встроенная панель ревью

Свой UI писать не обязательно: с первого propose-подключения редактор показывает бейдж, пока предложения ждут, а клик по нему открывает встроенную панель ревью — каждое ожидающее действие с инструментом, digest аргументов, предсказанным эффектом и идентичностью клиента (помечена как transport-provided, не аутентифицирована), плюс Approve all / Reject all и выбрасывание отдельных записей. Approve all идёт через queue.approve() — весь батч становится одним атомарным undo-шагом.

editor.chrome.showProposals() // открыть панель (false, если propose-сессии не было)
editor.chrome.hideProposals()
editor.chrome.isProposalsVisible // boolean

Декларативные обёртки — монтирование → открытие, размонтирование → скрытие (своей DOM не рендерят):

<XenolithGraph …>
<XenolithProposalQueue /> {/* из @xenolithengine/graph-react */}
</XenolithGraph>

Хостам с полностью кастомным UI достаточно просто не открывать встроенную панель и работать с editor.mcpProposals напрямую (ниже).

Минимальный свой UI ревью

function AgentInbox() {
const editor = useEditor()
const [size, setSize] = useState(0)
useEffect(() => editor?.mcpProposals?.onChange(setSize) ?? undefined, [editor])
if (!size) return null
return (
<XenolithPanel position="top-right">
<b>{size} предложений агента</b>
<XenolithButton onClick={() => editor!.mcpProposals!.approve()}>Одобрить всё</XenolithButton>
<XenolithButton onClick={() => editor!.mcpProposals!.reject()}>Отклонить</XenolithButton>
</XenolithPanel>
)
}

Рендерь внутри <XenolithGraph>; API очереди синхронный, так что обычного state + хука onChange достаточно.