
Claude Code, Codex, Gemini CLI — мощные штуки. Но живут они в терминале: вы открываете окно, печатаете задачу, смотрите, как агент думает, лезет в файлы, гоняет тесты. Для человека — отлично. А теперь представьте, что в это кресло перед терминалом нужно посадить не человека, а вашу программу: чтобы она сама ставила агенту задачу, видела каждый его шаг и могла остановить опасное действие до того, как оно случится.
Вот тут и начинается ACP — протокол, который позволяет любому коду управлять CLI-агентом.
TL;DR
Проблема: у CLI-кодинг-агентов интерфейс для человека, а не для программы. Хотите управлять ими из кода — извольте парсить stdout и отвечать на вопросы «выполнить команду?», на которые некому ответить.
Решение: ACP (Agent Client Protocol) — открытый стандарт «хост ↔ кодинг-агент». В AG2 любой CLI-агент поднимается обычным Agent в пять строк, а всё, что он делает, прилетает к вам событиями.
Всем привет! Я Семен Фролов — контрибьютор AG2; недавно я и принёс в него поддержку ACP, модуль ag2.acp. В статье разберём ACP простыми словами: как он устроен, почему это удобно и как за пять строк поднять Claude Code управляемым агентом. А под конец заставим его слушаться удалённо — голосом, с другого хоста.
Сразу про путаницу. Аббревиатуру «ACP» носят два разных протокола. Есть Agent Communication Protocol от IBM (для BeeAI, март 2025) — но его передали в Linux Foundation и объединили с A2A, отдельную разработку сворачивают. В этой статье ACP — это всегда Agent Client Protocol от Zed (agentclientprotocol.com), тот, что живёт и развивается.
Как вы обычно говорите с Claude Code
Стандартный сценарий: терминал, интерактивный чат, вы внутри цикла. Напечатали «почини падающий тест» — агент пошёл читать файлы, запускать pytest, править код, и в какой-то момент просит разрешение: выполнить вот эту команду в шелле или применить правку к файлу? Вы смотрите глазами, отвечаете «да/нет», и так до конца.
Пока за рулём человек — это удобно. Человек видит вывод, человек принимает решения, человек ловит момент, когда агент собрался сделать глупость.
А теперь уберите человека и посадите за руль программу. Сразу всё ломается:
Нет структурированного потока. Агент сыплет текст в stdout вперемешку: где тут «мысль», где «вызов инструмента», где «финальный ответ»? Парсить терминальный вывод регулярками — тупиковый путь.
Некуда вклиниться. Агент спрашивает разрешение на опасную команду — а отвечать-то некому. Программе нужен способ перехватить этот вопрос и ответить по своей логике.
Не собрать конвейер. Хочется, чтобы один агент написал код, а другой его отревьюил — но два процесса в терминале друг о друге ничего не знают.
Получается, терминал — это интерфейс для человека, а программе нужен интерфейс для программы. Контракт. Им и стал ACP.
Что такое ACP, если в двух словах
Лучшая аналогия — LSP, Language Server Protocol. До LSP каждый редактор тащил свою поддержку каждого языка: автодополнение Python в Vim, в Emacs, в Sublime — три разные реализации одного и того же. LSP это развязал: язык реализует один сервер, редактор реализует одного клиента, и N редакторов × M языков превращаются из N×M костылей в N+M.
ACP делает ровно то же — но для кодинг-агентов. По одну сторону хост (редактор или ваш код), по другую — агент, а между ними единый контракт. Написали агента один раз — и он работает в любом ACP-хосте, без отдельного переходника под каждый.
Несколько фактов, чтобы прикинуть зрелость:
Август 2025 — Zed выкатывает ACP. Толчком стал Google: пришёл со своим Gemini CLI, и обеим командам захотелось интеграции глубже, чем «запустить тулзу в терминале».
Протокол открытый (Apache-2.0), вынесен в отдельную организацию github.com/agentclientprotocol.
Поддержка со стороны редакторов — Zed, JetBrains, VS Code, Neovim, Emacs; в реестре уже почти полсотни агентов: Claude Code, Codex CLI, Copilot CLI, Gemini CLI, OpenCode и другие.
Технически всё просто: JSON-RPC 2.0 поверх stdio. Хост запускает агента дочерним процессом и общается с ним через stdin/stdout. Никакого сетевого слоя, никаких портов — просто два процесса и один канал.
ваш код (ACP-клиент / хост) │ │ спавнит subprocess ▼ ┌───────────────────────┐ │ claude-agent-acp │ ← ACP-агент │ (Claude Code внутри) │ └───────────────────────┘ ▲ │ JSON-RPC 2.0 поверх stdin/stdout ▼ промпты → ← поток событий (мысли, tool-calls, план…)
Запомните роли: клиент — это хост, тот, кто запускает и управляет. Агент — дочерний процесс, который ходит в LLM и делает работу. Дальше всё вокруг этой пары.
Тут внимательный читатель спросит: а зачем протокол, если у Claude Code есть официальный Claude Agent SDK и headless-режим, у Codex — свой SDK? Ответ тот же, что и у LSP. Напишете интеграцию на Claude Agent SDK — получите интеграцию с Claude Code, и только с ним. Захотите завтра прогнать ту же задачу через Codex или OpenCode — переписывайте под второй SDK, потом под третий. ACP — это один контракт на всех: код хоста не меняется, меняется команда запуска агента. Плюс SDK решает задачу «мой код дёргает агента», а ACP заодно стандартизирует то, что обычно приходится изобретать самому: запросы разрешений, файловые операции через хост, формат потока событий.
Минимальный пример: Claude Code как обычный агент
Теперь — как это выглядит из кода. Вот как в AG2 поднять Claude Code и дать ему задачу:
from ag2 import Agent from ag2.acp import ClaudeCodeConfig agent = Agent("coder", config=ClaudeCodeConfig(cwd="/path/to/repo")) reply = await agent.ask("Отрефактори модуль auth и добавь тесты") print(reply.body)
Пять содержательных строк. Ни слова про subprocess, JSON-RPC или stdio. А под капотом случилось всё, что мы разбирали: ClaudeCodeConfig запустил адаптер claude-agent-acp дочерним процессом, сделал рукопожатие, создал сессию, отправил ваш промпт и дождался ответа.
Один ask() (или run()) — это один prompt-turn в терминах ACP. Внутри него у агента крутится его собственный цикл «подумал → вызвал инструмент → почитал результат → пошёл дальше», сколько нужно. Вам наружу возвращается финальный текст, а весь процесс по дороге — потоком событий (об этом — ниже).
Сменить движок — это сменить config, код вокруг не меняется:
from ag2.acp import CodexConfig, OpenCodeConfig agent = Agent("coder", config=CodexConfig(cwd="/path/to/repo")) # Codex agent = Agent("coder", config=OpenCodeConfig(cwd="/path/to/repo")) # OpenCode
Тут стоит сказать важное: «заговорить по ACP» агенты умеют по-разному. OpenCode и Gemini CLI поддерживают ACP сами — у них есть встроенный режим (opencode acp, флаг --acp). А вот Claude Code и Codex напрямую ACP не говорят — для них есть отдельные адаптеры-обёртки (claude-agent-acp, codex-acp): снаружи они принимают ACP, а внутрь дёргают агента на его «родном» языке. На стороне вашего кода разницы никакой — готовый конфиг в AG2 просто знает, какую команду запустить; всплывёт она только при установке.
Поэтому ставим две вещи: сам интеграционный модуль через extra и нужный CLI-агент или его адаптер (так, чтобы исполняемый файл лежал на PATH):
pip install "ag2[acp]" # модуль AG2 npm install -g @agentclientprotocol/claude-agent-acp # адаптер для Claude Code npm install -g @agentclientprotocol/codex-acp # адаптер для Codex
Видно каждый шаг: типизированный поток событий
В терминале работа агента — это бегущий текст. Через ACP она превращается в типизированный поток событий, на который можно подписаться.
Подписываемся на стрим прогона и разбираем события по типам:
from ag2 import Agent from ag2.acp import ClaudeCodeConfig from ag2.events import ModelMessageChunk, ModelReasoning from ag2.events.tool_events import BuiltinToolCallEvent from ag2.acp.events import ACPPlan agent = Agent("coder", config=ClaudeCodeConfig(cwd="/path/to/repo")) def observe(event): if isinstance(event, ModelReasoning): print("?", event.content) # агент думает elif isinstance(event, ModelMessageChunk): print(event.content, end="") # текст ответа по кускам elif isinstance(event, BuiltinToolCallEvent): print(f"? {event.name}({event.arguments})") # пошёл в инструмент провайдера elif isinstance(event, ACPPlan): for step in event.entries: print(f" [{step.status}] {step.content}") # план работы async with agent.run("Добавь healthcheck-эндпоинт") as run: run.stream.subscribe(observe) # подписаться до старта reply = await run.result()
Каждый тип обновления в ACP ложится на событие AG2:
Обновление ACP |
Событие AG2 |
текст агента |
|
размышления |
|
вызов / результат инструмента |
|
план работы |
|
смена режима |
|
доступные команды |
|
«Чёрный ящик, который что-то печатает в терминал» превращается в наблюдаемый процесс: можно логировать каждое действие, строить из него UI, считать токены, ловить момент, когда агент полез не туда. Программа наконец видит, что делает агент, — а не парсит его вывод.
Под капотом: рукопожатие и жизнь сессии
Если развернуть «магию» из примера, обмен по ACP выглядит так:
┌─ Хост (ваш код) ─┐ ┌─ Агент (subprocess) ─┐ │ │ initialize │ │ │ │ ───────────────────────► │ согласовали │ │ │ ◄───── capabilities ──── │ возможности │ │ │ │ │ │ │ session/new (cwd, …) │ │ │ │ ───────────────────────► │ завели сессию │ │ │ ◄──── session_id ─────── │ │ │ │ │ │ │ │ session/prompt │ │ │ │ ───────────────────────► │ ┌──────────────────┐ │ │ │ │ │ внутренний цикл │ │ │ │ ◄═ session/update ══════ │ │ агента: думает, │ │ │ │ ◄═ session/update ══════ │ │ зовёт tools, │ │ │ │ ◄═ session/update ══════ │ │ пишет файлы… │ │ │ │ │ └──────────────────┘ │ │ │ ◄──── stop_reason ────── │ turn завершён │ └──────────────────┘ └──────────────────────┘
Сначала initialize — стороны сверяют, кто что умеет (это и есть согласование возможностей). Потом session/new заводит сессию с рабочей директорией. Дальше каждый ваш ask() — это session/prompt, в ответ на который сыплются session/update (те самые события из прошлой секции), пока turn не дойдёт до stop_reason.
И ещё одна приятная деталь: subprocess спавнится лениво, на первом turn, и переиспользуется на весь прогон — сессия живёт, контекст копится, заново процесс не поднимается.
Разрешения и human-in-the-loop
Вернёмся к тому самому «выполнить эту команду?». Когда агент собирается сделать что-то чувствительное — запустить команду в шелле, переписать файл — по ACP он шлёт хосту запрос разрешения. И вот тут программа получает ровно ту власть, которой не было в голом терминале: решить, что ответить.
За это отвечает permission_policy:
Политика |
Что делает |
|
спросить человека — через |
|
разрешать автоматически (автономный прогон) |
|
запрещать автоматически |
agent = Agent( "coder", config=ClaudeCodeConfig( cwd="/repo", permission_policy="auto", # полностью автономно ), )
Песочница: файлы и терминал под контролем хоста
Ещё один нюанс, который в терминале незаметен, а из кода критичен: операции с файлами и запуск команд агент делает не сам напрямую, а просит об этом хост. То есть чтение/запись файла и выполнение команды проходят через ваш код — и AG2 ограничивает их корнем fs_root (по умолчанию это рабочая директория cwd).
Практический смысл простой: автономный агент с permission_policy="auto" не уедет писать за пределы выданной ему папки. Попытка вылезти из fs_root упирается в ошибку, а не в испорченную файловую систему. Когда за рулём программа, а не человек, такая песочница — не роскошь, а условие, при котором это вообще можно запускать без присмотра.
Оркестрация: несколько агентов вместе
Раз каждый CLI-агент — это обычный Agent, их можно собирать между собой. Например, сделать одного агента инструментом другого через .as_tool():
from ag2 import Agent from ag2.acp import ClaudeCodeConfig reviewer = Agent("reviewer", config=ClaudeCodeConfig(cwd="/repo")) manager = Agent( "manager", config=ClaudeCodeConfig(cwd="/repo"), tools=[reviewer.as_tool(description="Отревьюить изменения на корректность")], )
manager пишет код, а в нужный момент сам дёргает reviewer как инструмент — и вы видите оба прогона одним потоком событий. Никто не мешает посадить их на разные движки: один на Claude Code, другой на Codex.
Бонус: командуем Claude Code голосом — с другого хоста (ACP × A2A)
Раз уж дочитали — самое вкусное. ACP управляет агентом локально, через stdio. Но раз ACP-агент — это обычный Agent, ничто не мешает отдать его и по сети.
За сеть в AG2 отвечает модуль ag2.a2a: он публикует любого Agent наружу по протоколу A2A («агент ↔ агент через сеть»). Выставляем Claude Code как сетевой сервис:
import uvicorn from ag2 import Agent from ag2.acp import ClaudeCodeConfig from ag2.a2a import A2AServer, build_card # тот же агент на Claude Code — теперь публикуем его по сети coder = Agent("coder", config=ClaudeCodeConfig(cwd="/repo", permission_policy="auto")) server = A2AServer(coder) card = build_card(coder, url="http://0.0.0.0:8000") asgi = server.build_jsonrpc(url="http://0.0.0.0:8000", card=card) uvicorn.run(asgi, host="0.0.0.0", port=8000)
Теперь на стороне вызывающего этот удалённый Claude Code выглядит просто как адрес — обычный Agent с A2AConfig:
from ag2 import Agent from ag2.a2a import A2AConfig # Claude Code живёт на другом хосте — для нас это просто card_url remote_coder = Agent("coder", config=A2AConfig(card_url="http://coder-host:8000")) reply = await remote_coder.ask("Почини тест в core/")
А дальше — кульминация. В AG2 есть ещё и ag2.live — голосовые агенты (LiveAgent) поверх realtime-API OpenAI и Gemini: они слушают вашу речь (live STT), отвечают голосом и умеют вызывать инструменты. Отдайте такому агенту remote_coder как инструмент — и Claude Code на другом хосте становится тем, что голосовой агент дёргает по ходу разговора. Получается цепочка из трёх стандартных кусков:
Вы LiveAgent A2A-сервер Claude Code (голос) (OpenAI/Gemini live) (другой хост) (subprocess) │ │ │ │ │ речь (STT) │ задача по A2A │ │ │ ──────────► │ ─── по сети ───────────► │ prompt по ACP │ │ │ │ ──── по stdio ──► │ │ │ │ ◄══ шаги ═════════│ │ ◄─ голосом ─│ ◄──── результат ─────────│ │
Вы проговариваете задачу вслух → голосовой агент на realtime-API OpenAI/Gemini разбирает её и по A2A передаёт на другой хост → там Claude Code за рулём ACP пишет код, а его шаги (мысли, правки, запуски тестов) идут обратно по цепочке. По сути — управление кодинг-агентом голосом, через сеть: говорите здесь, код пишется там.
И вот это — вся прелесть стандартов. ACP закрывает «последнюю милю» до локального CLI-агента, A2A — связь по сети, realtime-слой — голос. Три независимых куска, а стыкуются без единой строчки самописного клея: каждый говорит на своём стандартном контракте, и их можно собирать как кубики.
Что в итоге
ACP сводится к одной идее: дать любому хосту — редактору или вашей программе — единый способ запускать кодинг-агента и видеть каждый его шаг. Под капотом всё скромно: JSON-RPC поверх stdio, initialize → session/new → session/prompt, поток session/update обратно. А наверху из этого получается то, чего у терминала нет: структурированный поток событий, контроль над разрешениями, песочница и композиция агентов — хоть в одном процессе, хоть по сети в связке с A2A.
В AG2 это спрятано за обычным Agent с другим конфигом — берёте ClaudeCodeConfig, и Claude Code из CLI превращается в управляемого агента у вас в коде.
А теперь вопрос, на который у меня самого нет однозначного ответа: доверили бы вы автономному агенту с permission_policy="auto" свой рабочий репозиторий — и где для вас проходит граница, за которой человек обязан остаться в цикле? Интересно почитать в комментариях, кто где её проводит.
Ссылки:
Сайт и спека: agentclientprotocol.com
ACP у Zed: zed.dev/acp
Реализация в AG2: модуль
ag2/acpРеестр агентов и SDK: agentclientprotocol.com/get-started/registry
Комментарии (14)

PapaAlex
15.07.2026 05:57Отличная статья, спасибо. Хочу добавить пару мыслей из практики - про то, где ACP-агенты в «авто»-режиме неожиданно больно кусаются.
1. Permission_policy="auto" не равен «безопасно» fs_root не контролирует сетевые вызовы. Агент может сходить в платный API, запушить в GitHub под вашими кредами, дёрнуть npm install из скомпрометированного пакета - и ни одна из этих операций не попадёт под fs_root. Граница, за которой человек обязан остаться в цикле - там, где действие имеет необратимые последствия за пределами файловой системы.
2. Стрим событий - главная фича, её недооценивают Через события можно строить дашборд, где сразу видно: сколько токенов ушло на размышления vs на код, какие файлы трогал, на каком шаге завис. Без стрима вы не отловите режим, когда агент 40 раз подряд читает один файл, потому что «забыл» его содержимое. Для production-эксплуатации события важнее, чем сам факт программного управления.
3. В протоколе не хватает rate limiting / cost control Нет механизма сказать агенту «у тебя осталось 5000 токенов» или «ты уже сделал 20 tool calls». Один неудачный промпт может сжечь квоту. Можно убить процесс на стороне хоста, но было бы удобнее, если бы протокол позволял передавать бюджет агенту.
4. ACP vs MCP - их путают MCP - tools layer (агент → инструменты), ACP - orchestration layer (код → агент), A2A - network layer (агент ↔ агент). Разные уровни абстракции, и хорошо, что стандартизированы независимо.

vvlrff Автор
15.07.2026 05:57Спасибо, правда не сталкивался с тем, что люди путают данные протоколы, ибо каждый под разную задачу)

feyd12
15.07.2026 05:57https://habr.com/ru/articles/468491/
Если статья понравилась, Вы же понимаете, какой показатель не слишком высок?)

feyd12
15.07.2026 05:57По поводу безопасности, все знают, что буква S в аббревиатурах ai, acp и a2a обозначает Security.:) Я думаю, самые громкие будущие атаки на доверенную среду будут так или иначе использовать внедренные мультиагентные системы...

AlexeyChijov
15.07.2026 05:57Полезная вводная информация про ACP (Agent Client Protocol) - инструмент, который будет управлять CLI, чтобы вам меньше самому переписываться с ии в консоли.

ARad
15.07.2026 05:57CLI программы умеют достаточно хорошо управлять ИИ агентами. И чтобы внутри вашей программы не изобретать сложной системы управления ии агентами. Программа теперь может не вызывать ИИ напрямую, а вызывать ии агентов через CLI упряжку, которая уже внутри себя управляет ИИ агентами, суб агентами и т. д.
Единственное что надо от вашей программы теперь это уже управление самой этой упряжкой, ответы на запросы от ИИ агентов и т. д.
dkeiz
что я не поняло, так зачем плодить сущности, и делать отдельный ACP и A2G если все равно оно работает через A2A.
Сам все это гоняю, и в отсутствие единого стандарта аля MCP получается какая каша.
vvlrff Автор
Не a2g, а ag2 - это фреймворк для создания агентов. В статье как раз написано, что данный протокол (ACP) регламентирует управление CLI агентами
feyd12
А есть нормальная обзорная статья по AG2? Может напишете
dkeiz
хорошо, у меня тогда более глубокий вопрос, на который я сам себе ответить не могу, что лучше реализовывать, A2A с подобным протоколом, что удобно, ведь из коробки, или все это через шину, с алгоритмическим управлением event'ов.
Если у пользователя идет общение с родительским агентом, то все event сообщения от вторичного агента становятся контекст-спамом, на который агент для пользователя будет отвлекаться. Если скипать, то где тогда event'ы брать?
Городить шину поверх A2A? или в AG2 есть хорошие решения?
Propan671
В AG2 есть встроенная поддержка A2A - https://docs.ag2.ai/docs/user-guide/a2a/client/
Ничего не надо самому писать: завернул одного агента в A2A-Server, в другом сделал A2A-Client для работы с этим сервером и воткнул сабагентом - все