AgentsInternals

Harness кодинг‑агента: что я понял, вскрыв Codex, OpenCode и Pi

Автор:

Harness кодинг‑агента: что я понял, вскрыв Codex, OpenCode и Pi

TL;DR: Обвязка (harness) — это всё вокруг модели: агентный цикл, инструменты, права, песочница, сжатие контекста. Модель stateless и между ходами ничего не помнит, состояние и логику держит обвязка. Поэтому два кодинг-агента на одной и той же модели дают разный результат, и осей у этого результата сразу несколько: дотянет ли агент задачу до конца, за какое время и за какие деньги. Расходятся не модели, а архитектурные ставки их обвязок. Ниже разбираю эти ставки по исходникам Codex, OpenCode, Pi и своей, где живёт состояние, кому отданы права, есть ли песочница, и какие из ставок переживут следующий сильный релиз модели, а какие он обнулит.

В прошлой статье про промпт-кэш я сформулировал вещь, которая на самом деле важнее самого кэша: модель ничего не помнит, она stateless, а всю память за неё держит обвязка. Каждый ход обвязка заново собирает в один запрос системные инструкции, описания инструментов, историю и свежий ввод, скармливает модели и разбирает, что та попросила сделать. Кэш в той статье был поводом. Настоящий герой стоял за кадром, и это он, harness, та самая обвязка вокруг модели.

Я собираю своего агента поверх моделей OpenAI. Зовут его agent-core, внутри Python и Pydantic AI. Базовый агентный цикл я написал сам, и он довольно быстро заработал: модель просит инструмент, обвязка его выполняет, результат летит обратно, по кругу до ответа. А потом начались сложные узлы. Как правильно сжимать контекст, когда он распух. Как не дать агенту по второму разу делать то, что уже сделано. Как понять, что он действительно закончил, а не выдохся. На каждом таком узле я застревал и шёл смотреть, как это решают взрослые: благо исходники Codex, OpenCode и Pi лежат открыто, а архитектуру Claude Code Anthropic подробно описывает сам.

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

Весь код в статье я сверял в середине июля 2026 и закрепил на конкретных версиях: Codex 0.144.5, OpenCode 1.18.3, Pi 0.80.10.

Harness в цифрах

На Хабре фразу "одна и та же модель в разной обвязке даёт разный результат" повторили уже столько раз, что она стёрлась до присказки. Беда присказки в том, что её перестают проверять. Поэтому вместо лозунга три числа, и все на одной зафиксированной модели.

SWE-bench Pro устроен так, что scaffold (обвязку под прогон) можно зафиксировать и менять только его. Claude Opus 4.5 на стандартном scaffold от SEAL берёт 45.9% задач. Тот же Opus, та же модель, но обвязка от Claude Code, и уже 55.4%. Плюс 9.5 процентных пункта за счёт одной только обвязки. На SWE-bench Verified Mini разрыв ещё грубее: Sonnet 4.5 в scaffold от SWE-Agent решает 68% задач, а в generalist-scaffold от HAL те же 34%. Половина результата держится не на модели, а на том, как вокруг неё всё устроено.

Самое сильное число дал контролируемый эксперимент на 100 задачах SWE-bench Verified, где меняли по отдельности то модель, то обвязку и считали дисперсию. Разброс результата от смены обвязки оказался в 7.8 раза больше, чем от смены модели.

Тут надо притормозить, иначе получится та же присказка, только с цифрами. Есть встречная работа от IBM Research (arXiv 2602.22953), и она показывает обратное: на пяти универсальных архитектурах агента поверх пяти моделей именно модель объясняет 27.8% разброса в качестве, а архитектура агента всего 0.5%. Разница больше чем в 50 раз, и в другую сторону. IBM брал универсальные архитектуры, собранные по учебнику. Эксперимент с разбросом ×7.8 брал обвязки, заточенные под задачу. Отсюда правило, которое я для себя вынес: на универсальной обвязке решает модель, а на вылизанной под задачу решает обвязка. Мой agent-core пока ближе к универсальному краю, чем мне бы хотелось: это обвязка общего назначения под продукт, а не scaffold, вылизанный под одну узкую задачу, на которой можно было бы отточить каждую ставку.

04-same-model-different-harness.jpg

Числа, кстати, протухают быстро, так что не цепляйтесь к абсолютным значениям — важна пропорция, а не конкретный процент. Новейшие модели, Opus 4.8 и Sonnet 5, дают уже другие абсолютные цифры, поэтому дальше я и говорю про архитектурные ставки, а не про то, кто сегодня на сколько процентов впереди. Тот же Terminal-Bench 2.0 за полгода уехал с 62.9% в январе до 84.7% к лету.

Формулу Agent = Model + Harness ввёл Вив Триведи из LangChain, хотя половина вторичных источников приписывает её Митчеллу Хашимото (у того формулы нет вовсе). Определение у Триведи широкое и оно же лучший список осей для сравнения: harness это всё, что не модель. Системные промпты. Инструменты, скиллы, MCP. Встроенная инфраструктура: файловая система, песочница, браузер. Оркестрация. Хуки и middleware: сжатие, продолжение, линт. У слова два смысла, и их полезно не путать: широкий, продуктовый (вся обёртка вокруг модели, так говорят Anthropic и OpenAI), и узкий, архитектурный (только цикл исполнения). В документации Claude Code сказано прямо и в широком смысле: "Claude Code is the harness; Claude is the model inside it". Дальше я буду гулять между обоими смыслами, но каждый раз понятно, о каком речь.

Анатомия: что даёт фреймворк, а что дописываешь руками

Сердце любой обвязки это агентный цикл. У всех он про одно: позвать модель, посмотреть, что она просит, выполнить инструмент, вернуть результат, повторить, пока не будет финального ответа. Anthropic описывает его как три фазы, gather context, take action, verify results. Но дьявол в том, кто пишет диспетчер этого цикла и что он в него докладывает сверху.

Pydantic AI, на котором стоит мой agent-core, даёт граф из трёх узлов: узел пользовательского ввода, узел запроса к модели, узел вызова инструментов. Можно было бы отдать управление графу и не думать. Я отдавать не стал и обернул его в собственный цикл с явной диспетчеризацией, потому что мне нужен был контроль над каждым переходом:

while not isinstance(node, End):
    if isinstance(node, UserPromptNode):
        node = await agent_run.next(node)
        continue

    if isinstance(node, ModelRequestNode):
        model_request_count += 1
        if model_request_count > settings.max_loop_depth:
            raise ToolLoopLimitExceeded()
        await recorder.emit(
            "run.calling_model",
            data={"label": "Calling model"},
        )
        ...

    if isinstance(node, CallToolsNode):
        ...

(agent-core, agent/runtime.py)

Граф мне дал каркас перехода между узлами. Всё остальное в этом while я дописал сам: счётчик глубины с жёстким лимитом, чтобы агент не ушёл в бесконечную петлю инструментов, поток событий наружу (run.calling_model и десяток других), отдельный учёт вызовов веб-поиска со своим потолком. Это и есть та граница, которую полезно видеть в любой обвязке: вот тут работает фреймворк, а вот тут начинается то, что инженер посчитал нужным добавить от себя.

10-framework-boundary-v2.jpg

Любопытно посмотреть, как тот же цикл устроен у взрослых. Делают все одно и то же (позвать модель, выполнить инструменты, повторить), а вот ставка на то, кто держит состояние между итерациями цикла, у каждого своя. Codex крутит явный loop {} на Rust и решает, идти ли на следующий виток, по флагу needs_follow_up:

loop {
    …
    match sampling_request_result {
        Ok((sampling_request_output, sampling_request_input)) => {
            let SamplingRequestResult {
                needs_follow_up: model_needs_follow_up,
                …
            } = sampling_request_output;
            …
            let needs_follow_up = model_needs_follow_up || has_pending_input;
            …
            if !needs_follow_up {
                …
                break;
            }
            …
        }
        …
    }
}

(codex, codex-rs/core/src/session/turn.rs)

OpenCode держит один while (true), но всё в нём превращает в задачи: субагент, сжатие и обычный шаг становятся тремя ветками одного диспетчера.

while (true) {
  …
  const task = tasks.pop()
  if (task?.type === "subtask") {
    yield* handleSubtask({ task, model, lastUser, sessionID, session, msgs })
    continue
  }
  if (task?.type === "compaction") {
    const result = yield* compaction.process({ … })
    if (result === "stop") break
    continue
  }
  …
}

(opencode, packages/opencode/src/session/prompt.ts)

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

while (true) {
  let hasMoreToolCalls = true;
  while (hasMoreToolCalls || pendingMessages.length > 0) {
    …
    const nextTurnSnapshot = await config.prepareNextTurn?.(nextTurnContext);
    …
  }
  const followUpMessages = (await config.getFollowUpMessages?.()) || [];
  if (followUpMessages.length > 0) {
    pendingMessages = followUpMessages;
    continue;
  }
  break;
}

(pi, packages/agent/src/agent-loop.ts)

Четыре цикла, четыре ответа на вопрос, где живёт состояние: у меня в графе Pydantic AI и ручной диспетчеризации, у Codex в флаге needs_follow_up, у OpenCode в очереди задач, у Pi нарочно нигде, всё снаружи. Claude Code сюда же кладёт свои три фазы (gather context, take action, verify results), но его цикл я смотрю только по описанию Anthropic, в код не лазил.

Ещё одна моя ставка видна в том, где вообще исполняются инструменты. В agent-core они не исполняются. Мозги (сборка запроса, цикл, решения) живут в одном сервисе, а руки (собственно запись файлов, выполнение кода) в отдельном agent-runtime, и общаются они по HTTP с подписью HMAC. Самые опасные инструменты, delete_file и move_file, прямо в манифесте помечены флагом blocked_in_model_loop: модель не может дёрнуть их сама внутри цикла. Песочницу я сейчас переношу в Daytona, пока на стадии разведки. Разделение мозги-руки это осознанная ставка, и дальше будет видно, что у взрослых она расставлена очень по-разному.

Чтобы держать весь зоопарк в голове, вот опорная матрица. Слева оси сравнения, сверху пять обвязок, которые я могу разобрать предметно: четыре по коду плюс Claude Code по официальным материалам.

Осьagent-core (мой)CodexOpenCodePiClaude Code
Агентный циклручной while поверх графа Pydantic AIrun_turn, сжатие до и внутри ходаrunLoop с диспетчером задаччистый runLoop, без состоянияgather → act → verify
Сжатие контекстанет/responses/compact, latent stateprune + суммаризация головысуммаризует головусжатие + "5 последних файлов"
Права и песочницавнешний agent-runtime, далее Daytonaпесочница в ядре, 3 ОСправила, без OS-песочницытолько как примерOS-песочница для Bash, права не у модели
Инструментыманифест с политикамиshell + apply_patch как конвенцияширокий набор + MCP7 штукинструменты + Skills + Tool Search
Скиллыфайловые, свой менеджер контекстаmarkdown в CODEX_HOMESKILL.md, несколько путейmarkdown с frontmatterAgent Skills
Субагентынетнативные дочерние потокив том же процессенетсвой контекст, возврат summary
Расширяемостьконтракт событий, domain hooksJSON-хуки + плагины + MCPJS-плагины + MCP + ACPTS-расширения + /reloadхуки + MCP + SDK
Модельсвой OpenAI-совместимый сервисOpenAI по умолчанию, есть OSSмультипровайдер36 провайдеровAnthropic + Bedrock/Vertex
05-architecture-bets-workshops-v2.jpg

Построчно разбирать таблицу не буду, получится нейтральная справка. Лучше возьму несколько узлов, на которых застревал сам, и покажу, как по-разному их проходят. Там и видно настоящую разницу.

Агент уверен, что закончил, и при этом ошибается

Первые версии моего агента вели себя так, что хотелось закрыть ноутбук. Просишь прогнать скрипт, скрипт падает, а агент бодро рапортует: получил ошибку, и на этом считает ход законченным. Не разбирается, не чинит, просто отдаёт ошибку как результат и замолкает. Лечил я это в лоб, через системный промпт (доводи работу до конца), помогло, но осадочек остался: модель по умолчанию слишком охотно решает, что сделала достаточно.

Оказалось, дело не в кривых руках и не в моей конкретной модели. Калеб с канала Caleb Writes Code показывает, как агент на долгой задаче доходит до края контекста, сам себя суммирует в короткий конспект и на голубом глазу заявляет, что всё сделано. Хотя сделано наполовину.

А потом я нашёл ровно это же в инженерном разборе Anthropic. Anthropic описывает два сценария провала длинных задач прямым текстом. Первый: агент в один заход доходит до исчерпания контекста и бросает дело недоделанным. Второй, тоньше и злее: следующий агент видит уже проделанную работу, видит прогресс и "declares the job done", объявляет задачу выполненной, не проверив. Модель оптимистична по своей природе. Поставь её оценивать собственную работу, и она, по формулировке Anthropic, "confidently praise the work even when quality is obviously mediocre", будет уверенно хвалить откровенно посредственный результат.

Тут стоит посмотреть, как ошибка инструмента вообще доходит до модели. Возьмём Codex: упавшая команда не роняет цикл и не бросает исключение, её вывод вместе с текстом ошибки заворачивается и возвращается модели как обычный результат инструмента.

let result = if exit_code == 0 {
    Ok(content)
} else {
    Err(FunctionCallError::RespondToModel(content))
};

(codex, codex-rs/core/src/tools/events.rs)

RespondToModel это и есть "держи, разбирайся сам". OpenCode делает то же самое, помечая часть результата output-error, Pi возвращает результат с флагом isError: true. Ошибку всегда отдают на откуп модели, а та, как мы только что видели, имеет полное право пожать плечами и закрыть задачу. Вот ровно в это я и упирался первые недели. Поэтому там, где исход проверяем, я стараюсь не оставлять решение модели одной.

06-done-too-early-v4.jpg

Решение взрослых тут архитектурное, а не "давайте напишем промпт получше". Anthropic разводит генерирующего агента и оценивающего по разным ролям, чтобы хвалить себя было некому. Проверяющий агент не верит на слово, а идёт в живое приложение через Playwright MCP и смотрит глазами. А список фич держат в JSON-файле, где у каждой по умолчанию стоит passes: false, и агенту разрешено только переключить флаг, но не удалять и не переписывать сами проверки. Чтобы нельзя было "закрыть" задачу, стерев тест.

Я к этому узлу подошёл с другой стороны, попроще, но в той же логике: не дать вероятностной модели самой решать, что работа сделана, там, где это можно проверить детерминированно. В agent-core есть domain hooks, и один из них ловит ситуацию, когда модель повторно пишет файл, а инструмент возвращает ошибку "файл уже существует". Наивный агент тут пожмёт плечами и решит, что раз существует, значит уже сделано. Мой не решает, а идёт и сверяет:

observed = await execute_tool(
    "read_file",
    {"path": path},
    deps,
)
…
if observed.get("content") != content:
    return None

return DeterministicStopDecision(
    outcome="already_done",
    message=f"Файл `{path}` уже содержит нужный текст, поэтому ничего менять не пришлось.",
)

(agent-core, agent/domain_hooks.py)

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

Скиллы, кстати, болеют похожим: агент может загрузить один и тот же скилл дважды и потерять на этом контекст. У меня они сделаны прямо по спецификации Agent Skills от Anthropic, контракт тот же: пакет с SKILL.md и YAML-фронтматтером, рядом папки references, scripts, assets. Ленивая подгрузка оттуда же: в системный промпт уходит только компактный список "имя: описание", а полный текст тянется через load_skill, когда скилл реально понадобился. Своего я дописал сверху один слой, которого в спецификации нет, менеджер контекста: он восстанавливает из истории, что уже загружено, и отдаёт флаг already_loaded. В системном промпте так и написано: "load_skill and read_skill_resource are idempotent; if either tool returns already_loaded, continue using the previously loaded instructions or resource from conversation context". Как такая ленивая подгрузка уживается с промпт-кэшем (а это отдельная головная боль, потому что инструменты живут в начале запроса), я подробно разбирал в прошлой статье про кэш.

Сжатие контекста: четыре ответа на один вопрос

Я довольно долго думал, что compaction это "ну подумаешь, попросим модель сделать конспект истории", и всё откладывал. А потом посмотрел, как с этим живут взрослые, и понял. Сжатие ломается не на технике сжатия, а на том, что агент рядом с границей контекстного окна начинает паниковать. У Anthropic для этого есть отдельный термин, "context anxiety": модель у границы окна сворачивает работу раньше времени, лишь бы успеть. У Sonnet 4.5 это выражено так сильно, что одним сжатием не отделаешься, нужны полные сбросы контекста с переносом состояния. У Opus 4.5 и 4.6, по словам Anthropic, эта тревожность "greatly diminished", заметно слабее. То есть поведение обвязки вокруг сжатия зависит от того, какая модель внутри, и под более умную модель тот же код надо переписывать.

07-context-compaction-missing-v2.jpg

И механизмы сжатия у всех разные, это не одна галочка. Codex не отдаёт сжатую историю строкой. Его специальный endpoint /responses/compact возвращает список с непрозрачным encrypted_content внутри, латентным состоянием модели, а не человекочитаемым конспектом. Прямо в типе:

Compaction {
    encrypted_content: String,
},

(codex, codex-rs/protocol/src/models.rs)

OpenCode делит сжатие на два механизма и оба держит задачами в своём цикле: prune идёт по истории с конца и выбрасывает старые результаты инструментов, не трогая последние два хода и последние 40 тысяч токенов, а сам compaction режет историю на голову и хвост, суммирует голову и хвост сохраняет. Claude Code у лимита делает похоже: сперва выкидывает старые результаты инструментов, потом суммирует, причём CLAUDE.md и память переживают сжатие (перечитываются с диска), а после сжатия в контекст возвращаются "пять последних файлов". Pi суммирует голову истории, сохраняя хвост. Четыре обвязки, четыре разных ответа на один вопрос. Как при этом не платить за сжатие дважды из-за поломки кэша, я опять же разбирал в статье про промпт-кэш, там у Claude Code красивое решение через тот же префикс. А почему кэш переиспользуется только на общем префиксе и рассыпается от одного лишнего символа, я разбирал во второй части, там уже KV-тензоры и потроха vLLM.

Спектр ставок: от минимализма Pi до брони Codex

Если выложить четыре обвязки на одну ось "сколько обвязка берёт на себя", по краям окажутся Pi и Codex.

Pi представлен как "кодинг-агент, который делает меньше". И это не поза, это видно по коду. Субагентов в ядре нет. MCP нет вообще: я для проверки пробежался по зависимостям всех пакетов Pi, и @modelcontextprotocol/sdk там просто отсутствует. Инструментов семь штук. Песочница лежит только как пример расширения, не как часть ядра. Pi демонстративно выкидывает то, что другие считают обязательным.

Зато у Pi есть ставка, которой нет почти ни у кого. Расширения это обычные TypeScript-файлы, и грузятся они через jiti, то есть транспилируются в рантайме, без отдельной сборки:

/**
 * Extension loader - loads TypeScript extension modules using jiti.
 */
import { createJiti } from "jiti/static";

(pi, packages/coding-agent/src/core/extensions/loader.ts)

А команда /reload перезагружает расширения, скиллы и настройки, не перезапуская сессию. Сложите одно с другим: агент может на лету дописать себе инструмент на TypeScript и тут же подгрузить его в работающую сессию. Расширение при этом импортирует само ядро агента как библиотеку. Это буквальное воплощение идеи "агент пишет код как продолжение себя", и Pi ради этой ставки отказался от MCP, субагентов и встроенной песочницы.

11-pi-self-extension.jpg

Когда я это увидел, первым желанием было растащить свою систему скиллов и сделать так же, на лету. Потом вспомнил, чем Pi за это платит, и желание прошло: мне нужны и MCP, и песочница, а Pi их сознательно не держит.

На другом краю оси Codex, и его ставка ровно противоположная: безопасность не опция, а фундамент. Песочница вшита в ядро, и профиль Seatbelt для macOS начинается с одной важной строки:

; start with closed-by-default
(deny default)

(codex, codex-rs/sandboxing/src/seatbelt_base_policy.sbpl)

Запрещено всё, а дальше точечно разрешается необходимое. На Linux то же самое собрано из bubblewrap, landlock и seccomp. Сеть по умолчанию выключена. Ещё одна жёсткая ставка Codex в том, что он принципиально stateless поверх Responses API: по документации OpenAI он намеренно не использует previous_response_id, чтобы не хранить состояние на стороне сервера (ради режима нулевого хранения данных), и потому каждый ход пересобирает запрос заново. Где Pi раздевается до минимума ради гибкости, Codex обрастает бронёй ради предсказуемости и изоляции.

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

/// Spawn a subagent by forking persisted history from `forked_from_thread_id`.
pub async fn spawn_subagent(
    &self,
    forked_from_thread_id: ThreadId,
    mut options: StartThreadOptions,
) -> CodexResult<NewThread> {

(codex, codex-rs/core/src/thread_manager.rs)

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

const result = yield* ops.prompt({
  messageID: MessageID.ascending(),
  sessionID: nextSession.id,
  model: {
    modelID: model.modelID,
    providerID: model.providerID,
  },
  …
})

(opencode, packages/opencode/src/tool/task.ts)

Pi субагентов в ядре не держит вовсе. Они есть, но как пример-расширение, которое поднимает отдельный процесс pi через spawn:

/**
 * Subagent Tool - Delegate tasks to specialized agents
 *
 * Spawns a separate `pi` process for each subagent invocation,
 * giving it an isolated context window.
 * …
 */
import { spawn } from "node:child_process";

(pi, packages/coding-agent/examples/extensions/subagent/index.ts)

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

Между этими полюсами ложатся остальные. OpenCode держит полную поддержку MCP: локальные серверы через команду, удалённые по URL, с OAuth. Claude Code ставит права на уровень обвязки, а не модели, и это принципиально: в документации сказано, что инструкции в вашем промпте или в CLAUDE.md "don't change what Claude Code allows", не меняют того, что разрешает сама обвязка. Песочница у Claude Code на уровне ОС включается для Bash, но не для чтения и записи файлов. Мой agent-core со своим внешним agent-runtime живёт ближе к серьёзному краю, но без нативной песочницы, и меня это давно покусывает: руки от мозгов я отделил, а настоящую изоляцию только переношу в Daytona. Один и тот же вопрос, "что обвязка должна гарантировать вместо модели", и пять разных по жёсткости ответов.

Как из этого выбирать, для себя и для команды

На уровне кода я разобрал четыре обвязки плюс Claude Code, но рынок шире, и для карты полезно знать остальных хотя бы по их ставкам. Все они на середину 2026 живы и активно развиваются. Amp в декабре 2025 отделился от Sourcegraph в самостоятельную компанию и жонглирует четырьмя режимами, от low до ultra: режим переключает набор моделей под капотом, а платите вы всё равно по факту расхода, без наценки сверху. Droid от Factory целится в enterprise вплоть до полностью изолированного контура без доступа наружу, а постоянные виртуалки у него выросли в отдельный продукт, Droid Computers, который держит состояние между сессиями. Goose от Block ушёл под крыло Agentic AI Foundation, новой структуры Linux Foundation, куда в декабре 2025 одним пакетом сложили ещё MCP и AGENTS.md; провайдеров у него за полсотни, а расширения построены прямо на MCP. Antigravity от Google это agent-first среда сразу в нескольких обличьях, от IDE до CLI и SDK, и в неё же Google сворачивает Gemini CLI. Copilot CLI от GitHub с апреля 2026 пускает свои ключи и локальные модели, причём со своим провайдером ему не нужна даже авторизация в GitHub. Cursor катит облачных агентов и свою модель Composer, а в июне SpaceX объявила о покупке его материнской Anysphere за $60 млрд акциями, это крупнейшая сделка по венчурному стартапу в истории. Разброс ставок тот же, что я показал на коде, просто без возможности заглянуть внутрь.

Особняком стоит omp, главный форк Pi. Разраб сделал ровно наоборот: batteries included, "кодинг-агент со вшитой IDE". Внутрь он затащил всё, от чего Pi демонстративно отказался: LSP, настоящий отладчик через DAP, персистентный Python, субагенты первого класса, 32 инструмента вместо семи и полсотни тысяч строк Rust в ядре. Появился omp в конце 2025 и за полгода собрал 18 тысяч звёзд (у самого Pi их 72 тысячи).

Отдельный слой это выбор для команды, и тут самый показательный кейс снова у Codex, точнее у команды, которая его пишет. По их собственному рассказу, группа из трёх инженеров, выросшая до семи, за пять месяцев построила продукт примерно в миллион строк и около 1500 пул-реквестов с нулём строк, написанных руками. Весь код сгенерирован агентом, около 3.5 пул-реквестов на инженера в день. Первоначальный scaffold и первый AGENTS.md (строк на сто, скорее оглавление, чем инструкция) тоже сгенерированы Codex. Инварианты держатся не на code review, а на кастомных линтерах, а отдельный фоновый агент-сборщик мусора подчищает отклонения. Так OpenAI этот способ работы и называет: harness engineering.

Чтобы про этот слой думать, а не размахивать руками, есть удобный язык от Биргитты Бёкелер из Thoughtworks. Она описывает обвязку как кибернетический регулятор из двух частей. Guides это упреждающие подсказки, которые направляют агента до действия. Sensors это обратная связь после действия, и они тем сильнее, чем точнее сигнал заточен под модель: кастомное сообщение линтера, написанное так, чтобы его понял именно LLM, работает лучше сырой ошибки компилятора. Для тимлида это и есть рабочая оптика выбора: насколько в обвязке развиты сенсоры и можно ли заточить их под свою команду.

12-guides-and-sensors.jpg

Но карту рынка нельзя читать без смены поколения. Перелом случился поздней осенью 2025: я сам перешёл на Claude Code в ноябре, и данные это подхватывают, доля Claude Code в коммитах и опросах за ту зиму резко пошла вверх. Кого-то этой волной смыло. Cline растерял ядро команды (семеро с лишним ушли в OpenAI), хотя сама компания не закрылась и работает. Roo закрылся ради облачного продукта, и его нишу занял форк под названием Kilo. Aider формально жив, релизы идут, но из разговоров он практически исчез, и как референс я его уже не беру. Бывают ещё обвязки не для кода вовсе, вроде OpenClaw (мост между мессенджерами и моделями) или Hermes (самоулучшающийся персональный агент), но их с кодинг-агентами лучше не путать, это другой инструмент под другую задачу.

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

Build to delete: что строить сразу на выброс

У всего этого есть неуютное свойство. Часть обвязки вы пишете заведомо на выброс, просто пока не знаете, какую именно.

Филипп Шмид сформулировал это в начале 2026 года как принцип "build to delete": делай архитектуру модульной, новые модели заменят твою логику, будь готов выдрать код. Опирается он прямо на "горький урок" Саттона о том, что общие методы с ростом вычислений всегда побеждают рукотворные надстройки. Anthropic говорит то же самое, но строже, и эту формулировку стоит привести целиком: "every component in a harness encodes an assumption about what the model can't do on its own, and those assumptions are worth stress testing, both because they may be incorrect, and because they can quickly go stale as models improve". Каждый кусок обвязки это зашитое предположение о том, чего модель не умеет сама, и предположение это стоит регулярно проверять на прочность, потому что оно протухает по мере того, как модели умнеют.

Manus по их собственным словам переписывали свою обвязку четыре раза. Не рефакторили, а переписывали целиком, потому что под новую модель старые костыли становились лишними. А каноничный пример обвязки, построенной сразу на выброс, это Ralph Джеффри Хантли: голый bash-цикл while true, который на каждом витке поднимает свежий экземпляр агента с чистым контекстом, а память между витками держит снаружи, в git, в progress.txt и в prd.json с полем passes. Никакого хитрого управления контекстом внутри, вся сложность вынесена в дизайн цикла. Anthropic эту идею в ноябре 2025 даже оформила официальным плагином ralph-loop и демо-обвязкой для автономного кодинга. Маленький репозиторий у Ralph, кстати, не цель, а следствие: по словам автора, "essential to use as little as possible", чем меньше контекста тратишь на каждом витке, тем лучше.

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

У меня этот ratchet уже щёлкнул, и я могу увидеть полный цикл по коммитам. В начале марта я написал регулярку _needs_path_clarification: она разбирала сообщение пользователя ещё до модели, по спискам глаголов на двух языках ("исправ", "обнов", "edit", "update") решала, достаточно ли однозначен запрос, и если нет, требовала уточнить путь к файлу, который надо исправить, обновить или отредактировать. Через шесть дней я в неё же и упёрся, пришлось пробивать обход для .docx, и в комментарии к коммиту честно записал причину: эвристика перехватывает запрос раньше, чем модель успеет сама сходить в read_docx_structure. Ещё через девять дней я снёс её целиком, минус 854 строки, и отдал решение модели через системный промпт.

Костыль доживает ровно до того дня, когда начинает мешать сильнее, чем помогает. Сегодняшний кандидат на снос у меня куда более дурацкий: модель иногда отдаёт аргументы вызова инструмента склейкой одинаковых JSON-объектов, {"name":"docx"}{"name":"docx"}, и обычный json.loads на этом падает. Поэтому в рантайме сидит парсер, который распознаёт склейку и схлопывает обратно в один объект, если все куски совпали. Умрёт сам, когда провайдер починит сериализацию.

А через 43 минуты после сноса той регулярки я сделал ровно обратное движение и вернул в код детерминированную блокировку delete_file и move_file, тот самый флаг blocked_in_model_loop из начала статьи. Причина в коммите: до этого оба инструмента запрещались только инструкцией в системном промпте, а инструкция обходится prompt injection. Там, где речь про безопасность, промпт оказался негодной заменой коду.

А что я бы трогать не стал, даже зная про build to delete? Границы прав и песочницы. Контракт событий, на котором держится наблюдаемость. Возможность сменить модель под капотом. Эти ставки описывают устройство системы вокруг модели, и с сильной моделью они только важнее. Чем больше агент способен сделать сам, тем дороже обходится отсутствие границ. Вот по этой линии я и предлагаю выбирать обвязку: всё, что компенсирует сегодняшнюю глупость модели, считайте временным; всё, что задаёт границы и контракты, считайте несущим.

08-build-to-delete-v2.jpg

Я начинал с того, что застрял на паре узлов и полез внутрь чужих обвязок, чтобы просто понять, как там всё устроено. Понял я, в общем, неожиданное. Обвязку нельзя осмыслить как застывшую вещь, у неё нет правильной финальной формы. Она дышит вместе с моделью: то, что сегодня несущая балка, через два сильных релиза окажется лишней подпоркой, и весь фокус в том, чтобы заранее знать, что у тебя балка, а что подпорка. Хороший вопрос к любой строчке своей обвязки звучит так: я делаю это за модель потому, что она не может, или потому, что не могла вчера?

Исходники показывают, какие ставки сделаны, но молчат о том, во что они обходятся в деньгах. Меня это грызло всё время, пока я писал, так что я собрал стенд и померил: четыре задачи, семь связок харнесс × модель, 56 прогонов. Про это будет следующая статья.

Оставайтесь любопытными.

Пишу об искусственном интеллекте, языковых моделях и инструментах для разработчиков. Тестирую модели и сервисы на реальных задачах, а выводами делюсь в телеграм-канале.

Что такое обвязка (harness) агента?

Обвязка — это всё вокруг модели, что не сама модель: агентный цикл, системные промпты, инструменты, скиллы и MCP, файловая система и песочница, оркестрация, хуки сжатия и продолжения. Модель stateless, а состояние и логику держит обвязка. Формулу Agent = Model + Harness ввёл Вив Триведи из LangChain.

Чем кодинг-агенты различаются, если внутри одна и та же модель?

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

Codex или Claude Code: в чём разница подхода?

Codex ставит на безопасность как фундамент: песочница вшита в ядро (профиль начинается с deny default), сеть по умолчанию выключена, он stateless поверх Responses API. Claude Code ставит права на уровень обвязки, а не модели: инструкции в промпте или CLAUDE.md не меняют того, что обвязка разрешает, а песочница ОС включается для Bash. Внутренности Claude Code здесь разобраны по официальным материалам Anthropic.

Что такое harness engineering?

Подход, при котором обвязку строят как способ организовать работу команды и агентов, а не просто «помощник для кода». Команда Codex по своему рассказу за пять месяцев собрала около миллиона строк и порядка 1500 пул-реквестов с нулём строк, написанных руками: инварианты держат кастомные линтеры, а отклонения подчищает фоновый агент.

Что значит принцип build to delete?

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

Другие статьи на эту тему

AI Engineer World's Fair 2026: разбор докладов и куда движется AI-инженерия

Agents

Industry

AI Engineer World's Fair 2026: разбор докладов и куда движется AI-инженерия

Живьём 560 сессий не осилить, поэтому доступные записи я прогнал через агентный пайплайн. Рассказываю, почему почти все доклады сводятся к harness, а не к самой модели, и какие пять роликов посмотреть первыми.

03 июля 2026

Почему один символ ломает кэш: prompt caching под капотом

Internals

Practice

Почему один символ ломает кэш: prompt caching под капотом

Вторая часть серии про prompt caching. Лезем в исходники vLLM и paged attention, чтобы увидеть байтовую причину под правилом "стабильное в начало, изменчивое в хвост": что физически лежит на GPU и почему одного символа хватает, чтобы обрушить весь кэш.

26 июня 2026

Prompt caching: оптимизация, которая наказывает молчанием

Agents

Basis

Practice

Prompt caching: оптимизация, которая наказывает молчанием

Кэш режет цену входных токенов в десять раз и держит юнит-экономику агентов, но ломается без единой ошибки в логах. Что на самом деле кэшируется, чем отличаются OpenAI, Anthropic и Google и как собрать промпт, который не убивает собственный кэш.

10 июня 2026

Взгляд инди-хакера на AI и разработку: глубокое погружение в языковые модели, гаджеты и self-hosting через практический опыт.
© 2026 Gotacat Team