Agent Harness

Harness — исполнительная обвязка агента: слой, который держит задачу, собирает контекст, вызывает инструменты, применяет границы песочницы, спрашивает подтверждения, отдаёт прогресс и переносит работу между ходами. Ключевое различие: фреймворк и SDK отвечают на вопрос «как агента построить», harness — на вопрос «как он работает», а модель — только на «чем он думает».

Суть: три разных вопроса, которые обычно смешивают

Разделение полезно тем, что показывает, где на самом деле лежит сложность.

Вопрос Кто отвечает Что меняется при замене
Чем агент думает модель качество решений
Как агента построить фреймворк, SDK форма кода приложения
Как агент работает harness поведение в проде: возобновляемость, границы, подтверждения, наблюдаемость

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

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

Что оказалось переиспользуемым, а что нет

OpenAI открыла три вещи: CLI, app-server и SDK; доступ к модели и управляемые сервисы остались закрытыми. Формулировка прямая: открытый слой — это обвязка и поверхность интеграции.

Разрез сам по себе содержателен. Открыто всё, что описывает, как агент исполняется, и закрыто то, что его питает. То есть заявление «ценность в обвязке» подкреплено действием, которое стоило бы дорого, будь оно неправдой.

Три поверхности отвечают трём способам встроить агента:

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

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

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

Три примитива, на которых стоит возобновляемость

  • Thread — разговор пользователя с агентом; содержит ходы.
  • Turn — один запрос пользователя и вся работа агента следом; содержит элементы и стримит инкрементальные обновления.
  • Item — единица ввода или вывода: сообщение пользователя, сообщение агента, запуск команды, правка файла, вызов инструмента.

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

У каждого элемента явный жизненный цикл из трёх стадий: item/started → необязательные item/*/deltaitem/completed с финальной нагрузкой. Это не украшение протокола, а то, что позволяет интерфейсу начать рисовать сразу, дописывать по мере поступления и зафиксировать результат в конце, — вместо ожидания готового ответа.

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

Разговор открывается рукопожатием initialize — единственным запросом, который клиент обязан послать до всех остальных. Смысл не в приветствии: на этом шаге стороны договариваются о версии протокола, флагах возможностей и умолчаниях. Именно это делает протокол расширяемым без поломки существующих клиентов, что было прямым требованием, когда обвязку начали встраивать в чужие продукты.

Подтверждение как примитив протокола, а не решение в коде

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

Запрос На что спрашивают
item/commandExecution/requestApproval выполнение команды оболочки
item/fileChange/requestApproval правку файлов
item/permissions/requestApproval выдачу доступа песочнице
tool/requestUserInput структурированный ввод от человека
mcpServer/elicitation/request запрос данных со стороны MCP-сервера

Клиент отвечает одним из трёх: accept, decline, cancel.

Два наблюдения, которые стоит унести независимо от продукта:

Типов запроса пять, а не один. «Подтверждение» — не однородная вещь: разрешить команду, разрешить правку файла и расширить права песочницы это разные решения с разной ценой ошибки, и сваливать их в один диалог «Разрешить действие? Да/Нет» значит терять различие ровно там, где оно нужно (Rollback Boundary, класс обратимости).

cancel отделён от decline. Отказать в конкретном действии и прервать всю работу — разные исходы, и путать их дорого: агент, получивший «нет» на один шаг, должен уметь пойти другим путём, а получивший «отмена» — остановиться. Интерфейс, где есть только «Да/Нет», этой разницы выразить не может.

Пункт чеклиста tools/approval-path спрашивает, определено ли, какие действия не выполняются без подтверждения человека. Здесь видно, как выглядит ответ, когда он дан конструкцией: не политикой в коде, а формой протокола, мимо которой не пройти.

Протокол — это перевод, а не внутренности наружу

App Server — одновременно протокол между клиентом и сервером и долгоживущий процесс, внутри которого работают ядра. Четыре части: читатель stdio, обработчик сообщений, менеджер потоков и сами ядра; менеджер поднимает по одному ядру на поток.

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

Это ответ на вопрос, который возникает у всякого, кто отдаёт наружу свой рантайм. Отдать внутренние события как есть — значит зафиксировать внутреннее устройство в публичном контракте и потерять свободу его менять. Слой перевода разводит две скорости: внутри можно перестраивать что угодно, снаружи набор уведомлений остаётся маленьким и стабильным. Тот же приём, что Orchestrator Adapter, но обращённый в другую сторону — не приложение защищается от фреймворка, а рантайм защищает свой контракт от собственных внутренностей.

Транспорт — JSON-RPC поверх stdio, построчным JSON. Оговорка авторов: это не строгий JSON-RPC 2.0 — заголовок "jsonrpc": "2.0" опущен, кадрирование построчное. Схему клиентских привязок генерируют командой codex app-server generate-json-schema, то есть привязку на своём языке не пишут руками.

Клиент не может быть источником истины

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

Локальный клиент несёт с собой бинарник обвязки, прибитый к проверенной версии, и держит с ним двусторонний канал. Веб-версия запускает обвязку в контейнере на рабочем месте с выгруженным репозиторием, а браузер разговаривает с бэкендом по HTTP и SSE.

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

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

Как выбирать поверхность

Пять вариантов, и у каждого назван свой размен:

Вариант Когда Чем платишь
Обвязка как MCP-сервер уже есть MCP-контур, нужен агент как вызываемый инструмент получаешь только то, что выражается через MCP; богатая семантика сессии (например, обновления диффов) через него не проходит
Кросс-провайдерный протокол обвязки нужна одна абстракция над несколькими провайдерами такие протоколы сходятся к общему подмножеству возможностей, и специфичное выразить труднее
App Server нужен весь цикл агента стабильным потоком событий интеграционная работа: клиентскую привязку пишешь сам
Однократный запуск командой автоматизация и пайплайны неинтерактивно: один прогон, структурный вывод, явный успех или отказ
SDK нативная библиотека внутри своего приложения меньше языков и меньшая поверхность, чем у протокола

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

Права песочницы как объявленное значение

Политика доступа выражается перечислимым значением, а не набором условий:

readOnly · workspaceWrite · dangerFullAccess · externalSandbox

и уточняется полями writableRoots, readableRoots, networkAccess, includePlatformDefaults.

Agent Sandboxing описывает чем изолировать — Docker, gVisor, микровиртуальные машины. Здесь другое измерение: как права объявляются. Перечислимое значение можно прочитать в конфиге, залогировать, сравнить между запусками и запретить на уровне политики; рассыпанные по коду проверки — нельзя. externalSandbox в этом списке отдельно ценен: он честно говорит «изоляцию обеспечивает вызывающая сторона», вместо того чтобы делать вид, что её обеспечивает обвязка.

Поток событий — это и есть трассировка

События идут пятью группами: жизненный цикл хода (turn/started, turn/completed, turn/diff/updated, turn/plan/updated), жизненный цикл элемента (item/started, item/completed), дельты (item/agentMessage/delta, item/plan/delta, item/reasoning/summaryTextDelta, item/commandExecution/outputDelta), события потока (thread/started, thread/archived, thread/closed, thread/status/changed) и предупреждения (configWarning, warning).

Существенно то, что этот поток не добавлен ради наблюдаемости — он и есть способ, которым клиент узнаёт о происходящем. Значит трассировка не может «отвалиться» или быть выключенной для экономии: без неё приложение просто не работает. Ср. Agent Observability, где трассировка обычно пристраивается сбоку и потому первой отключается под нагрузкой.

turn/plan/updated стоит отдельного упоминания: план агента — часть протокола, а не текст внутри сообщения. Это ровно тот минимум видимости, который Agent Execution Platform называет обязательным для эксплуатации.

Агент безголовый: поверхность решает, а не он

Разница между поверхностями исчерпывается тем, что добавляет обёртка, а не тем, что меняется в агенте:

Терминал Веб
вывод текст элементы интерфейса
вызовы инструментов строки в поток ошибок отдельные компоненты результата
подтверждение запрос в потоке ввода кнопки
время жизни до завершения процесса сессия переживает вкладку
ввод один раз при запуске непрерывно

Граница проходит там, где обычно её размывают: агент не знает про хранилище. Он получает историю сообщений или не получает и работает дальше; решение загружать и сохранять принимает поверхность. Как только агент начинает сам ходить в базу, поверхности перестают быть взаимозаменяемыми, и вторую уже не написать, не тронув первую.

Это же объясняет, почему состояние долгой задачи не может жить в клиенте: разобрано выше, в разделе про эфемерность веб-сессии.

Расширение событиями, а не наследованием

Точки расширения выше названы списком мест. Общая форма у них одна: обвязка объявляет события жизненного цикла, расширение подписывается. Подписчик может пропустить вызов, заблокировать его или изменить данные перед тем, как обвязка продолжит; несколько подписчиков выстраиваются в цепочку.

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

Почему именно события, а не наследование или конфигурация:

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

Широкий хук на высокочастотном инструменте — типовая ошибка этого слоя. Подписчик, который перехватывает вызов часто используемого инструмента и принудительно перенаправляет его на другой, чинит сценарий, ради которого написан, и ломает поведение в множестве несвязанных. Ошибка воспроизводимая: при автоматической оптимизации обвязки ровно такой хук сочиняли обе сравниваемые конфигурации, и разошлись они не в том, придумали или нет, а в том, было ли чем его отвергнуть (Harness Optimization). Практический вывод для событийного слоя: чем чаще вызывается инструмент, тем уже должна быть область действия подписчика на него.

Соотношение с подтверждениями: событийный слой и слой режима решают разные задачи, и это разобрано в Human in the Loop — режим отвечает «кто решает», события «какие правила действуют независимо от ответа».

Мягкий контур и жёсткий: чем принуждается прохождение шагов

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

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

Два механизма, которыми это делается, стоит назвать отдельно, потому что они разные:

  • Паспорт задачи — объект, который едет вместе с задачей через все этапы и накапливает результаты каждой проверки. Метрики качества собираются не в конце и не сбоку, а по ходу, и к финальному гейту приходят вместе с артефактом. Приём взят с конвейерного производства, где такой паспорт сопровождает изделие; смысл тот же — на выходе видно не «прошло / не прошло», а где именно и чем подтверждено.
  • Политики как код — требования, которые иначе проверялись бы чеклистом в конце, раскладываются в файлы инструкций и умения и применяются в момент работы. Тогда на гейт приходит уже соответствующий продукт, а не тот, который сейчас развернут обратно. Выигрыш здесь не в скорости проверки, а в снятии возвратов: разработчик перестаёт узнавать о требовании на сдаче.

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

Соотношение с соседями: событийный слой выше даёт точки, в которых можно заблокировать или изменить вызов, — жёсткий контур это те же точки, собранные в маршрут с состоянием. Кто вправе разрешить действие — вопрос отдельный и разобран в Agent Control Plane; здесь речь только о том, гарантировано ли прохождение объявленных шагов.

У обвязки есть измеримое качество, и оно настраивается

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

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

Чем это отличается от Agent Execution Platform

Заметки соседние, и границу надо держать явно:

  • Agent Execution Platform отвечает, из каких слоёв состоит рантайм, если строишь его сам: вход, идентичность, плоскость управления, оркестрация, модель, память, инструменты, телеметрия.
  • Эта заметка отвечает, что из этого берут готовым и по какому протоколу к нему подключаются.

Различие практическое: первая — про проектирование, вторая — про решение «строить или встроить», и про то, какой поверхностью потом придётся расплачиваться (Orchestrator Adapter: обвязка — вторая точка привязки помимо фреймворка оркестрации).

Связано с

  • Sandbox Abstraction — контракт со средой исполнения, одна из точек расширения обвязки
  • Agent First Repository — среда вокруг обвязки: как устроен репозиторий, код которого пишет агент
  • Agent Execution Platform — из чего рантайм состоит; здесь — что из этого берут готовым
  • Agent Architecture — сборка цикла своими руками, то есть противоположный выбор
  • Orchestrator Adapter — обвязка как ещё одна поверхность привязки
  • Human in the Loop — подтверждение человеком; здесь показана его протокольная форма
  • Agent Sandboxing — чем изолировать; здесь — как объявлять права
  • Agent Observability — трассировка, которая тут не пристроена сбоку, а несущая
  • Harness Optimization — как обвязку улучшать и чем мерить: устройство отвечает «как построена», это — «насколько хороша»
  • Durable Execution — возобновляемость со стороны графа, а не протокола
  • MCP — способ, которым приложение отдаёт обвязке свои инструменты и контекст
  • Agent Control Plane — кто вправе разрешить действие; здесь — принуждается ли объявленный маршрут
  • Parallel Agent Dispatch — та же обвязка со стороны человека, который ведёт несколько задач сразу