Orchestrator Adapter

Тонкий слой между бизнес-логикой и фреймворком оркестрации: приложение вызывает свой интерфейс, адаптер переводит вызов в конкретный API фреймворка. Защита от того, что библиотека под вами меняется быстрее, чем ваш продукт.

Суть

Прямой вызов фреймворка из бизнес-логики означает, что его имена, сигнатуры и способ возобновления сессии расползаются по всему коду. Адаптер сводит эту связь к одной точке: наружу торчит протокол в терминах предметной области, внутри — вызовы конкретной библиотеки.

class BankAgentOrchestrator(Protocol):
    async def process_transaction(self, config, initial_state) -> dict: ...
    async def resume_transaction(self, config, decision) -> dict: ...

class LangGraphBankAgentAdapter:
    def __init__(self, compiled_graph):
        self.app = compiled_graph

    async def process_transaction(self, config, initial_state):
        return await self.app.ainvoke(initial_state, config=config)

    async def resume_transaction(self, config, decision):
        # возобновление после HITL — единственное место, где приложение
        # знало бы про Command(resume=...) из LangGraph
        return await self.app.ainvoke(Command(resume=decision), config=config)

Методы названы действиями бизнеса — «провести транзакцию», «возобновить», — а не операциями фреймворка. Это и есть смысл: код приложения не знает слова ainvoke.

Зачем это нужно

Framework churn. Библиотеки оркестрации агентов меняются быстро: ломаются сигнатуры, переименовываются классы, меняется способ передать возобновление. Без адаптера каждое такое изменение расходится по десяткам файлов.

Тестируемость. Бизнес-логику можно проверять на подставном оркестраторе, реализующем тот же протокол, — без графа, чекпоинтера и живых вызовов модели.

Смена фреймворка становится обозримой. Переезд с LangGraph на что-то другое превращается из переписывания приложения в написание второго адаптера. Это не бесплатно — семантика у фреймворков разная, — но объём работы становится измеримым.

Границы применимости

Адаптер оправдан там, где приложение крупнее прототипа и живёт дольше одного релиза фреймворка. Для скрипта на сто строк это лишний слой: вы платите за развязку, которой не воспользуетесь.

Вторая граница — не всё абстрагируется. Специфические возможности фреймворка (отмотка истории, тонкая работа с чекпоинтами, потоковая выдача событий) либо протекают в интерфейс, либо остаются недоступными. Честный адаптер покрывает основной путь и не притворяется, что фреймворка не существует.

Вторая поверхность привязки: обвязка

Адаптер выше защищает от привязки к фреймворку оркестрации. Точка привязки не одна: взяв готовую исполнительную обвязку (Agent Harness), приложение начинает зависеть ещё и от неё — от формы её протокола, от её примитивов разговора и хода, от её способа спрашивать подтверждение.

Отличие от фреймворка в том, где проходит шов. Фреймворк вызывается из кода приложения, и адаптер прячет его вызовы. Обвязка, наоборот, вызывает приложение сама: она инициирует запрос подтверждения, она стримит события, она решает, когда ход закончен. Спрятать это тем же приёмом нельзя — прятать пришлось бы не вызов, а протокол.

Практический вывод: границу здесь проводят не адаптером, а выбором, что остаётся у приложения. Обвязка Codex оставляет три вещи: свои MCP-сервисы, свои границы песочницы, свой поток подтверждений. Это и есть места, за которые стоит держаться — они же оказываются наименее переносимыми при смене обвязки.

Признак протёкшей абстракции проверяется одним вопросом: можно ли заменить фреймворк за адаптером, не меняя вызывающий код? Если в интерфейсе адаптера фигурируют понятия конкретного фреймворка — узлы, рёбра, чекпоинтеры, — ответ отрицательный, и адаптер лишь добавил слой, ничего не изолировав. Честный интерфейс говорит на языке вашей задачи: «выполнить сценарий», «получить статус», «продолжить с шага», а как это раскладывается в граф — деталь реализации.

Второй признак того же: каждое новое требование заставляет добавлять метод в адаптер. Значит абстракция повторяет API снизу, а не описывает потребность сверху.

Заводить сразу или по факту поломки. Сразу — обычно преждевременно: пока фреймворк один и опыта работы с ним нет, интерфейс придумывается умозрительно и получается тем самым зеркалом чужого API. Ждать первой поломки совместимости — поздно: переписывать придётся под давлением. Практический момент между этими крайностями — когда появляется второй способ делать то же самое: второй фреймворк, второй способ запуска, необходимость мокать оркестрацию в тестах. Тогда интерфейс выводится из двух реализаций, а не из одной, и получается честным почти автоматически.

Та же граница на другой оси

Приём не привязан к оркестрации. Ровно так же выносится среда исполнения: инструменты вызывают контракт readFile / exec / stop, а за ним стоят локальная, виртуальная и облачная реализации (Sandbox Abstraction). Соображение то же самое — интерфейс дорожает не в момент написания, а в момент второй реализации, — и вывод тот же: делать его настолько малым, насколько получится.

Полезное различие: у среды исполнения нашлась дешёвая проверка структурности рефакторинга, которой у оркестрации нет. Поведение агента на тех же запросах после перевода на контракт обязано не измениться; изменилось — значит где-то инструмент по-прежнему тянется мимо контракта.

Связано с

  • Sandbox Abstraction — тот же приём на оси среды исполнения
  • Agent Harness — вторая поверхность привязки, которую адаптером не спрятать
  • LangGraph — фреймворк, который этот паттерн изолирует
  • Agent Architecture — где слой встраивается в общую схему приложения
  • PydanticAI — альтернативный фреймворк, ради переезда на который адаптер и заводят
  • LangGraph HITL — возобновление после паузы, единственная нетривиальная операция интерфейса
  • Agent Governance — права и журнал, которые удобно вешать на тот же слой