🤖 AI-агенты для бизнеса

24. Асинхронные действия — generic wait-event

Статус: спека (проект). Проблема + предлагаемое решение для поддержки флоу вида «запрос → показать юзеру → дождаться внешнего коллбэка → вернуть результат».

Проблема

Инструменты Agent OS синхронны: вызов → результат. Но есть класс флоу, где результат приходит не сразу, а после внешнего действия, невидимого для агента:

Сейчас обобщённого примитива для этого нет:

Механизм Что умеет Чего не хватает
rest_tool / site_tool исходящий запрос (GET/POST/…, oauth2, sign) один запрос, не ждёт
oauth_connect suspend → resume по коллбэку захардкожен под OAuth-URL
request_attachment suspend, ждёт файл/фото от юзера специализирован на вложения
inbound webhook (POST /v1/inbound/:template) запускает новый ход по вебхуку не резолвит уже висящий ход; ждёт {message}

В итоге любой асинхронный флоу либо пишут нативно под себя, либо костылят поллингом.

Цели

  1. Обобщённый kind: await_event — декларативный флоу start → prompt → suspend → callback → resume.
  2. Один нативный движок + один общий коллбэк-роут; конкретные интеграции — YAML.
  3. Покрыть СБП, подтверждения по коду, «подтверди в приложении»; oauth_connect выразить как частный случай.

Не-цели (v1)

Существующие примитивы ядра

Уже используются: oauth_connect (URL авторизации) и request_attachment (файл/фото). Это ровно тот «suspend → resume» механизм, который нужен; не хватает только декларативной обёртки общего назначения и универсального коллбэк-роута.

Предложение: kind: await_event

YAML-схема

kind: await_event
name: sbp_payment
description: Принять оплату по СБП (QR) и дождаться подтверждения.
parameters:                       # JSON-Schema для LLM
  type: object
  properties:
    amount: { type: string, description: "Сумма, напр. 100.00" }
  required: [amount]

start:                            # 1. запрос-инициатор (поля как у rest_tool)
  method: POST
  url: "https://api.yookassa.ru/v3/payments"
  json:
    amount: { value: "{{ amount }}", currency: "RUB" }
    capture: true
    confirmation: { type: "redirect" }
    payment_method_data: { type: "sbp" }
  headers:
    Authorization: "Basic {{ (env_var('YOOKASSA_SHOP_ID') ~ ':' ~ env_var('YOOKASSA_SECRET_KEY')) | base64 }}"

prompt:                           # 2. что показать юзеру
  message: "Оплатите по QR:"
  url_pointer: /confirmation/confirmation_url    # JSON-pointer → URL для показа

resolve:
  id_pointer: /id                 # 3. JSON-pointer → id, по которому сопоставлять коллбэк

timeout_secs: 600                 # 4. таймаут ожидания (по истечении — «не дождались»)

Флоу выполнения

1. выполнить `start` (method/url/json/headers/oauth2/sign) → ответ JSON
2. извлечь `prompt.url_pointer` → URL, `resolve.id_pointer` → id
3. kernel.request_user_input_with_id(pid, "await_event", prompt, timeout, id)
       → эмитит input-request (показ URL) и ПРИОСТАНАВЛИВАЕТ ход
4. [ход висит; юзер платит/подтверждает вне диалога]
5. внешняя система шлёт коллбэк → POST /v1/events/callback { id, ... }
6. kernel.resolve_user_input(id, payload) → ход ВОЗОБНОВЛЯЕТСЯ
7. await_event возвращает `payload` (или результат сверки) агенту

Коллбэк-роут

POST /v1/events/callback — общий, не привязан к конкретной интеграции:

Показ юзеру (QR)

Источник — всегда prompt.url_pointer (URL). Рендер — на стороне канала:

Канал v1 (ссылка/кнопка) v2 (QR-картинка)
Виджет (web) кнопка «Оплатить» клиентский JS-QR из URL
Telegram / VK сообщение со ссылкой серверный PNG (qrcode-крейт) + фото
Android кнопка-ссылка Kotlin-QR

В await_event ничего QR-специфичного — он отдаёт URL, QR рисует канал.

Альтернативы (почему не они)

Вариант Минус
Поллинг (тул/агент крутит GET status) ест воркер и лимиты API; LLM ненадёжно «ждёт»; долгий ход
Отдельный payment_tool узко: не переиспользуется для кодов/подтверждений
Webhook → inbound inbound ждёт {message}, а провайдеры шлют свой формат → нужен шим; и это «новый ход», а не resume

Открытые вопросы

  1. Имя kind: await_event (рекомендую) vs async_action vs callback_tool.
  2. Что возвращает тул по коллбэку: сырой payload или {status, payload}?
  3. Персистентность pending между рестартами процесса (v2?) — сейчас pending в памяти.
  4. Проверка подписи коллбэка: общая (sign-подобная) или на уровне конкретной интеграции.

План внедрения

  1. ✅ Спек (этот документ).
  2. Нативный движок await_event + роут /v1/events/callback (+ юнит/интеграционные тесты).
  3. Примеры YAML: СБП (ЮKassa), подтверждение по коду; выразить oauth_connect как частный случай.
  4. QR-рендер: ссылка → кнопка → клиентский QR → серверный PNG.
  5. Док-глава «REST-интеграции» с рабочими примерами (Трекер, ЮKassa СБП, await_event).