AG-UI 1.0: у протокола наконец появилась спецификация. Разбираем, что изменилось, на связке AG2 + CopilotKit

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.
AG-UI 1.0: что нового в протоколе
AG-UI 1.0: что нового в протоколе

Привет! Меня зовут Семён Фролов, я инженер в AG2.

15 октября Tesla показала Roadster, который умеет летать, и весь интернет разбирает эту презентацию по кадрам. А мы вместо этого рассмотрим не менее важную вещь: мажорный релиз AG-UI.

TL;DR:

  • 17 сентября вышли пакеты 1.0.0, 30 сентября CopilotKit объявил релиз;

  • из нового самое заметное: interrupts (агент ставит run на паузу и ждёт человека), субагенты, reasoning, usage и отмена run'а, мультимодальные результаты инструментов;

  • протокол стал строже: версионируется, а его поведение описано как нормативное (MUST/SHOULD), с JSON Schema, из которой генерируются SDK;

  • ниже на примере из ag2-samples смотрим, как это выглядит в живом приложении: AG2 на бэкенде, CopilotKit на фронте.

AG-UI в двух словах

Клиент шлёт один POST, сервер отвечает потоком SSE. Каждое событие в потоке - JSON с полем type. Вот так выглядит ответ, в котором агент вызвал инструмент и потом написал текст (JSON собран по схеме 1.0, значения условные):

Один run - один поток SSE: события run, tool call и message
Один run - один поток SSE: события run, tool call и message

Всё, что стримится (текст, аргументы тула, рассуждения), оформлено как START → CONTENT → END с идентификатором. Плюс есть события состояния (STATE_SNAPSHOT, STATE_DELTA) и служебные. Всего в 1.0 их 31. Агенту безразлично, что за интерфейс по ту сторону, а интерфейсу - на каком фреймворке написан агент.


Что нового в 1.0

Interrupts: агент ждёт человека

Раньше «агент просит подтверждения» каждый делал по-своему. Теперь это часть протокола: run заканчивается событием RUN_FINISHED, в котором outcome равен interrupt. Продолжается всё новым запросом на том же threadId, с ответом человека в поле resume.

Interrupt: пауза - это конец run'а, продолжение - новый run
Interrupt: пауза - это конец run'а, продолжение - новый run

Никакое соединение на время паузы держать не нужно: человек может подумать хоть час. Если interrupt'ов несколько, resume обязан ответить на каждый.

Субагенты

Если агент зовёт другого агента, интерфейс теперь видит это явно: SUBAGENT_STARTED, SUBAGENT_FINISHED, SUBAGENT_ERROR, а события субагента помечены subagentRunId. Можно показать «сейчас работает планировщик поездок», а не просто крутилку.

Reasoning вместо thinking

Старые THINKING_* заменены семейством REASONING_*. Появилось и событие для зашифрованных рассуждений, которые клиент не читает, а возвращает модели как есть.

Usage и отмена

В RUN_FINISHED теперь может приезжать расход токенов за run (с учётом субагентов), а отменённый пользователем run закрывается честно: outcome: cancelled, а не маскируется под успех.

Мультимодальность

Результат инструмента больше не обязан быть строкой: это может быть список частей, например текст плюс картинка по ссылке.

Протокол стал строже

  • События несут protocolVersion, и стороны могут понять, с кем разговаривают

  • Необязательное поле либо есть, либо его нет: null не допускается

  • Свои поля на события больше не вешаются, для этого есть metadata

  • Поведение (что обязано открываться, закрываться, как реагировать на неизвестное) описано как нормативное, а типы SDK генерируются из схемы.


Как это выглядит в AG2 + CopilotKit

В ag2-samples лежит «Weather AG UI»: агент погоды на AG2 и чат на Next.js с CopilotKit. В нём собраны почти все новые фичи

Архитектура примера ag2-samples: браузер, Next.js и FastAPI с AG2
Архитектура примера ag2-samples: браузер, Next.js и FastAPI с AG2

Бэкенд: AGUIStream вокруг агента

Агент оборачивается в AGUIStream, а дальше у вас два эндпоинта: GET отдаёт описание возможностей агента (capabilities), POST запускает run

@router.get("/weather")
async def capabilities() -> JSONResponse:
    return JSONResponse(stream.capabilities().model_dump(by_alias=True, exclude_none=True))

@router.post("/weather")
async def run(
    run_input: RunAgentInput,
    user: Annotated[User, Depends(authorised_user)],
    accept: Annotated[str | None, Header()] = None,
) -> StreamingResponse:
    events = stream.dispatch(
        run_input, 
        dependencies={"user": user, "http": http}, 
        config=config, accept=accept
    )
    return StreamingResponse(events)

dispatch сам переводит внутренние события агента в события AG-UI. Роут написан руками, потому что стандартный stream.build_asgi() не отдаёт заголовки запроса, а здесь из Authorization нужно достать пользователя и передать его в зависимости агента. Пользователь едет именно в dependencies, а не в state, потому что state клиент может перезаписать.

Субагенты: два агента как инструменты

agent = Agent(
    "WeatherAgent",
    prompt=PROMPT,
    config=CONFIG,
    tools=[
        get_coords_by_city,
        get_current_weather_by_coords,
        get_weather_next_week,
        save_favorite_city,
        clothing_advisor.as_tool(description="Suggest what to wear for the given weather. ..."),
        trip_planner.as_tool(description="Compare the weather of several cities and recommend where to go."),
    ],
)

Субагент подключается через .as_tool(), а фронт получает SUBAGENT_*-события, на которые подписана боковая панель:

const { agent } = useAgent({ agentId: "agenticChatAgent" });

agent.subscribe({
  onSubagentStartedEvent:  ({ event }) => setStarted((all) => [...all, event]),
  onSubagentFinishedEvent: ({ event }) => setFinished((all) => ({ ...all, [event.subagentRunId]: event })),
  onSubagentErrorEvent:    ({ event }) => setFailed((all) => ({ ...all, [event.subagentRunId]: event })),
});

Interrupt: подтверждение в одну строчку

На бэкенде инструмент помечается middleware:

@tool(
    description="Save a city to the user's favorites. The user is asked to confirm before anything is saved.",
    middleware=[ApprovalRequired("Save this city to your favorites?\n{tool_arguments}", allow_always=False)],
)
async def save_favorite_city(context: Context, city: ..., country: ... = "") -> list[str]:
    ...

На фронте один хук рисует кнопки:

useInterrupt({
  agentId: "agenticChatAgent",
  render: ({ interrupt, resolve }) => (
    <div>
      <p>{interrupt?.message ?? "The assistant needs your confirmation."}</p>
      <button onClick={() => resolve(true)}>Approve</button>
      <button onClick={() => resolve(false)}>Reject</button>
    </div>
  ),
});

Нажатие на «Approve» отправляет новый запрос с resume, и агент выполняет инструмент. Попробуйте в примере «Save Lisbon to my favorites».

Остальное на фронте

  • Generative UI. useRenderTool превращает результат инструмента в React-компонент, например, карточку погоды вместо текста. Имя должно совпадать с именем инструмента на бэкенде.

  • Shared state. Изменённые context.variables (избранные города, найденная локация) уезжают клиенту в STATE_SNAPSHOT, и панель читает их из agent.state.

  • Инструмент в браузере. На «погода здесь» агент вызывает getUserLocation, который через useFrontendTool берёт геолокацию браузера. Отказ пользователя возвращается как данные, а не исключение.

Итог

В 1.0 мне нравится, что вещи, которые каждый делал по-своему (подтверждение человеком, субагенты, отмена, расход токенов), стали частью протокола. Для интерфейса это значит меньше самописного клея, для агента - одно место, куда всё выводить.

Ссылки:

- Репозиторий примера ag2-samples https://github.com/ag2ai/ag2-samples

- Спецификация AG-UI 1.0 https://docs.ag-ui.com/spec/1.0

- AG2: AG-UI https://docs.ag2.ai/docs/user-guide/ag-ui/overview/

- CopilotKit: AG2 https://docs.copilotkit.ai/ag2

- Разбор миграции 0.x → 1.0 https://nerdleveltech.com/ag-ui-1-0-migration-tested

Мой GitHub: github.com/vvlrff

Источник: https://habr.com/ru/articles/1095968/


Интересные статьи

Интересные статьи

Привет, Хабр! Меня зовут Кирилл, и я технический директор в компании АйПиДжи (интегратор Битрикс24). Хочу рассказать, как размещать статичные приложения, которые не требуют баз данных и не содержат оч...
Российский сервис управления бизнес‑процессами «Битрикс24» представил новую версию собственной ИИ‑модели — BitrixGPT 4x, созданную для упрощения выполнения повседневных задач пользователями....
Итак после первых плагинов для ВордПресс и джумла пришла пора сделать что-то похожее в Битрикс.Наш путь познания разных движков для сайтов проходит под идеей принести новый функционал, а именно принят...
В этой статье я расскажу о советском шахматном компьютере "Интеллект-02", сделанном на основе 8-разрядного микропроцессора КР580ИК80А, и покажу, как можно внедриться вместо его ПЗУ и сделать его измен...
Введение: - обычный ChatGPT 3.5 работает тут https://chat.openai.com, но для генерации картинок нужен Dall-E 3 отсюда https://openai.com/dall-e-3 и желательно ChatGPT 4 (правильно осознать ваш креатив...