Программный доступ к агенту существует в трёх видах, и третий устроен принципиально иначе первых двух. Родной набор для TypeScript даёт агента прямо внутри вашего процесса; набор для Python говорит с тем же мостом, только упакованным в пакет и поднимаемым им самим. Мост - это небольшой локальный сервер, внутри которого работает набор для TypeScript, а наружу он отдаёт ту же самую поверхность агента по устойчивому протоколу поверх Connect и protobuf. Нужен он там, где родного набора нет: Go, Rust, Java, C#, любой другой язык.
Наивный ход мысли здесь такой: раз набора для моего языка нет, напишу клиент к сетевому интерфейсу напрямую, HTTP умеют все. Мысль естественная и наполовину верная. Отдельный сетевой интерфейс облачных агентов действительно существует, работает поверх HTTP и не требует ничего локального. Если задача в том, чтобы запустить агента в облаке и забрать результат, это и есть правильный выбор, а мост тут лишнее звено, которое нечем оправдать.
Ломается это на втором требовании - когда агент нужен локально, с вашими инструментами и вашим хранением состояния. Поверхность агента не сводится к одному вызову с ответом: это создание разговора, продолжение прерванного, отправка сообщений, чтение потока событий по мере работы, обращение к собственным инструментам вызывающей стороны и обращение к хранилищу состояния. Написать это заново на своём языке означает переписать не клиент, а сам цикл работы агента, и повторять упражнение при каждом изменении на той стороне.
Мост снимает этот вопрос тем, что цикл остаётся внутри набора для TypeScript, а наружу выставлен контракт. Контракт описан пакетом protobuf и разбит на службы, и разводить их полезно не только по назначению, но и по направлению вызова. Ниже такая карта: службу агента, справочную службу и управление мостом вызывает адаптер; обратные вызовы инструментов и хранилища мост адресует адаптеру сам; общий слой сообщений и ошибок принадлежит обеим сторонам.
Обратное направление и есть самая неочевидная часть устройства. Адаптер на вашем языке не только клиент - он ещё и сервер: чтобы агент мог вызвать ваш инструмент или прочитать ваше состояние, мосту нужно куда-то постучаться. Отсюда практическое требование к коду адаптера: он держит две роли одновременно и живёт столько, сколько живёт разговор. Клиент, написанный как последовательность запросов с ожиданием ответа, встанет на первом же собственном инструменте.
| Служба | Что в ней | Направление вызова |
|---|---|---|
| Служба агента | Создание и продолжение агентов, отправка запросов, поток запусков, артефакты и расход | Адаптер вызывает мост |
| Справочная служба | Личность, доступные модели, репозитории | Адаптер вызывает мост |
| Управление мостом | Проверка связи, версия, остановка, регистрация обратных вызовов | Адаптер вызывает мост |
| Обратные вызовы инструментов | Собственные инструменты вызывающей стороны | Мост вызывает адаптер |
| Обратные вызовы хранилища | Собственное хранение состояния | Мост вызывает адаптер |
| Общие сообщения и ошибки | Типы и коды, единые для всех служб | Общий слой контракта |
Секретов в этой схеме два, и путать их не стоит. Первый - ключ доступа пользователя или служебной учётной записи; он берётся в панели управления и передаётся мосту параметром или переменной окружения, и им мост представляется наружу, обращаясь к сетевому интерфейсу по HTTPS. Второй - токен предъявителя, который мост выдаёт при рукопожатии; его адаптер прикладывает к каждому вызову. Сам сервер поднимается на петлевом адресе, то есть слушает только эту машину и никого больше.
Про транспорт есть оговорка, на которой спотыкаются чаще всего. Протокол выглядит как gRPC и описан теми же средствами, но классический gRPC поверх HTTP/2 к мосту не подключится: сервер говорит по HTTP/1.1, и обращаться к нему нужно либо клиентом Connect, либо обычными POST-запросами с телом в protobuf или JSON. Версионирование при этом обещано мягкое: ломающие изменения приходят отдельным пакетом рядом с существующим, а не переписывают его на месте.
Цена лишнего звена складывается из трёх вещей. Первая - процесс: его надо скачать под нужную платформу и архитектуру, запустить, дождаться готовности, следить за живостью и корректно остановить, и всё это ложится на ваш код, а не на библиотеку. Вторая - отладка: между вашей программой и агентом теперь стоит посредник, и к любому сбою добавляется лишний вопрос, чья это сторона. Третья - поддержка: адаптеры для языков без родного набора пишет сообщество, и ответственность за их совместимость с новыми версиями лежит на том, кто их взял. Учёт запросов и оплата при этом идут по тем же правилам, что у редактора и облачных агентов; отдельной экономики мост не создаёт.
Выбирают мост по остаточному принципу, и это правильно. TypeScript и JavaScript - родной набор. Python - свой набор, внутри которого мост уже упакован, поднимать его отдельно не нужно. Только облачные агенты по HTTP, без локального выполнения - сетевой интерфейс облака. Всё остальное, то есть Go, Rust, Java, C#, - мост. Проверяют результат в два шага: сначала управляющими вызовами убеждаются, что процесс поднялся и отвечает ожидаемой версией, потом гоняют настоящий разговор с одним собственным инструментом. Второй шаг обязателен: он единственный доказывает, что обратное направление вызовов работает, а не только прямое.
Типичные провалы предсказуемы. Взять мост там, где хватило бы сетевого интерфейса облака, и получить процесс на сопровождение вместо одного запроса. Поднимать мост вручную рядом с набором для Python, где он уже внутри. Ткнуться классическим клиентом gRPC и долго читать ошибки соединения. Написать адаптер только как клиент и узнать об этом на первом собственном инструменте. Забыть про остановку процесса и оставить его висеть после завершения программы. И считать адаптер, написанный сообществом, частью продукта, а его совместимость - чужой заботой.