Skip to Content
Руководство для разработчиковDaemonACP Bridge

ACP Bridge

Обзор

packages/acp-bridge/ отвечает за границу между HTTP-слоем демона и дочерним процессом ACP. Он используется в packages/cli/src/serve/ (демон qwen serve) и был вынесен в #4175 F1 шаг 3, чтобы будущие потребители (channels/base/AcpBridge.ts, компаньон VS Code IDE) могли использовать то же ядро моста, не обращаясь напрямую к пакету CLI.

Каждая активная WorkspaceRuntime владеет одним экземпляром HttpAcpBridge. Продакшен пытается выполнить прогрев основного bridge и повторяет попытку при первом использовании после сбоя. Доверенный вторичный открывает свой AcpChannel и запускает свой дочерний процесс по требованию; недоверенный вторичный не может запустить ACP. Внутри среды выполнения bridge предоставляет мультиплексированные сессии через канал, EventBus для каждой сессии, MultiClientPermissionMediator, адаптер BridgeFileSystem и ACP-ориентированные помощники (spawnOrAttach, loadSession, resumeSession, sendPrompt, cancelSession, respondToPermission, а также RPC-вызовы extMethod для статуса рабочего пространства и перезапуска MCP). Bridge и дочерние процессы никогда не используются совместно между средами выполнения рабочих пространств.

Обязанности

  • Запуск или подключение к дочернему процессу ACP через подключаемую ChannelFactory. Фабрика по умолчанию: defaultSpawnChannelFactory (подпроцесс qwen --acp). В тестах используется inMemoryChannel.
  • Поддержание aliveChannels (реестр каналов) и byId (реестр сессий).
  • Мультиплексирование N сессий на стороне HTTP в один дочерний процесс ACP через connection.newSession().
  • Сериализация промптов для каждой сессии через promptQueue (ACP требует только один активный промпт на сессию).
  • FIFO-очередь для вызовов setSessionModel в каждой сессии, чтобы одновременные подключения с разными моделями не создавали гонку у агента.
  • EventBus для каждой сессии, который обслуживает GET /session/:id/events (см. 10-event-bus.md).
  • Поток разрешений: BridgeClient.requestPermissionMultiClientPermissionMediator.request → рассылка → сбор голосов → ответ ACP (см. 04-permission-mediation.md).
  • Файловый ввод-вывод: адаптер BridgeFileSystem для чтений и записей ACP; среды выполнения демона на одном хосте объявляют readTextFile: false, чтобы обычное чтение текста оставалось в дочернем процессе, а итоговые записи текста по-прежнему делегировались (см. 07-workspace-filesystem.md).
  • RPC-вызовы extMethod для статуса на уровне рабочего пространства (/workspace/mcp, /workspace/skills, /workspace/providers), перезапуска MCP и опционального приватного управляемого Tool Guard callback.
  • Жизненный цикл: корректный shutdown() с KILL_HARD_DEADLINE_MS (10 с) на канал; синхронный killAllSync() для принудительного завершения по второму сигналу.

Архитектура

Публичная точка входа: createHttpAcpBridge(opts: BridgeOptions): HttpAcpBridge в packages/acp-bridge/src/bridge.ts.

Ключевые типы:

TypeFileRole
HttpAcpBridgebridgeTypes.tsПубличный интерфейс: spawnOrAttach, loadSession, resumeSession, sendPrompt, cancelSession, subscribeEvents, respondToPermission, getWorkspaceMcpStatus, restartMcpServer, shutdown, killAllSync, …
BridgeSessionbridgeTypes.ts{ sessionId, workspaceCwd, attached, clientId?, createdAt? }, возвращается HTTP-обработчикам.
BridgeOptionsbridgeOptions.tsКонфигурация на этапе создания (см. Конфигурация).
AcpChannelchannel.ts{ stream, kill(), killSync(), exited } — один NDJSON-канал ACP.
ChannelFactorychannel.ts(workspaceCwd, childEnvOverrides?) => Promise<AcpChannel>.
BridgeClientbridgeClient.tsОбертка над одним ACP ClientSideConnection; реализует ACP Client (requestPermission, readTextFile, writeTextFile, sessionUpdate, extNotification).
EventBuseventBus.tsIn-memory pub/sub для каждой сессии. См. 10-event-bus.md.
MultiClientPermissionMediatorpermissionMediator.tsМедиатор с четырьмя политиками. См. 04-permission-mediation.md.

Внутреннее состояние (замыкается в createHttpAcpBridge):

StateShapePurpose
aliveChannelsMap<string, ChannelInfo>Реестр каналов по id канала. Каждый ChannelInfo содержит channel, connection, client (один BridgeClient на канал), sessionIds: Set<string>, pendingRestoreIds, statusClosedReject?, isDying: boolean.
byIdMap<string, SessionEntry>Реестр сессий по sessionId. Каждый SessionEntry содержит channel, connection, events: EventBus, promptQueue: Promise<void>, modelChangeQueue: Promise<void>, pendingPermissionIds: Set<string>, clientIds: Map<string, count>, activePromptOriginatorClientId?, attachCount, spawnOwnerWantedKill, restoreState?, sessionLastSeenAt?, clientLastSeenAt: Map<string, ms>.
defaultEntrySessionEntry | null”Единственная” сессия, используемая при sessionScope: 'single'.
defaultPolicyPermissionPolicyНастраивается через BridgeOptions.permissionPolicy.
mediatorMultiClientPermissionMediatorОдин на экземпляр моста.
ConstantsDEFAULT_INIT_TIMEOUT_MS = 10_000, MCP_RESTART_TIMEOUT_MS = 300_000, DEFAULT_MAX_SESSIONS = 32, MAX_EVENT_RING_SIZE = 1_000_000, DEFAULT_PERMISSION_TIMEOUT_MS = 0, DEFAULT_MAX_PENDING_PER_SESSION = 64.

Инвариант isDying: любой путь завершения должен синхронно устанавливать ChannelInfo.isDying = true до ожидания channel.kill(). ensureChannel считает умирающий канал отсутствующим и создает новый. Без этого флага параллельный spawnOrAttach, поступающий во время окна корректного завершения SIGTERM (до 10 с), подключится к транспорту, который вот-вот закроется, и sessionId вызывающего будет возвращать 404 при всех последующих запросах. Места установки (должны быть синхронизированы): ensureChannel (ошибка инициализации + повторная проверка при позднем завершении), doSpawn (ошибка newSession на пустом канале), killSession (последняя сессия покидает канал), shutdown (массовое завершение).

Инвариант сохранения channelInfo: не очищайте channelInfo при установке isDying = true. killAllSync все равно должен найти канал во время окна корректного завершения SIGTERM, чтобы отправить SIGKILL при process.exit(1). aliveChannels хранит умирающую запись до тех пор, пока не сработает channel.exited.

Ограниченная буферизация BridgeClient: фреймы ACP extNotification, поступающие в BridgeClient для sessionId, которого еще нет в byId (поскольку ответ connection.newSession еще не вернулся, но обнаружение MCP внутри newSession уже сгенерировало события бюджета), буферизуются в очередь ранних событий, ограниченную MAX_EARLY_EVENT_SESSIONS = 64 × MAX_EARLY_EVENTS_PER_SESSION = 32 × EARLY_EVENT_TTL_MS = 60_000. В худшем случае это около 400 КБ памяти. Без буферизации первый слот кольца повторной передачи SSE для новой сессии не содержал бы событий, сгенерированных во время её создания.

Рабочий процесс

spawnOrAttach (основная точка входа)

Ключевые моменты:

  • sessionScope='single' при существующем defaultEntry только увеличивает attachCount, регистрирует clientId и возвращает attached: true.
  • Холодный путь запускает ChannelFactory, выполняет инициализацию ACP (DEFAULT_INIT_TIMEOUT_MS=10s), вызывает connection.newSession({cwd}), затем регистрирует новый SessionEntry.
  • SessionLimitExceededError выбрасывается, когда byId.size >= maxSessions.
  • InvalidClientIdError выбрасывается, если X-Qwen-Client-Id выходит за пределы [A-Za-z0-9._:-]{1,128}.
  • Сборщик отключений в server.ts отслеживает владельца процесса запуска через attachCount/spawnOwnerWantedKill, чтобы не сносить сессию, владелец которой отключился, но к ней уже подключились другие клиенты (см. #3889 BQ9tV).

Сериализация промптов

Ошибки в конце очереди проглатываются, чтобы отклонение предыдущего промпта не отравило последующие; исходный вызывающий все равно получит отклонение в своем возвращенном промисе. transportClosedReject, кэшированный в сессии, сравнивает промис промпта с channel.exited, чтобы сбой дочернего процесса проявился немедленно, а не привел к зависанию.

Поток разрешений (высокоуровнево)

InvalidPermissionOptionError выбрасывается до медиатора, если попытка голоса по сети пытается внедрить CANCEL_VOTE_SENTINEL через обычное поле optionId — этот сентинел является единственным механизмом моста для прерывания запроса как cancelled / agent_cancelled, и к нему не должно быть случайного доступа из сети. См. 04-permission-mediation.md.

Завершение работы

Фабрика каналов

AcpChannel (channel.ts) — это транспортная абстракция моста. В продакшене используется defaultSpawnChannelFactory в spawnChannel.ts, которая запускает qwen --acp как подпроцесс с парой stdio-каналов. В тестах внедряется inMemoryChannel для запуска агента внутри процесса. Мост ничего не знает о базовом механизме — ему нужны только { stream, kill, killSync, exited }.

ChannelFactory принимает childEnvOverrides, чтобы каждый дескриптор демона мог передавать свои переменные окружения бюджета MCP (QWEN_SERVE_MCP_CLIENT_BUDGET, QWEN_SERVE_MCP_BUDGET_MODE) без изменения process.env (что вызвало бы гонку при работе двух встроенных демонов в одном процессе Node).

Состояние и жизненный цикл

  • Создание моста синхронно. Вызывающий может выполнить прогрев канала перед первой сессией; в противном случае первый spawnOrAttach холодно запускает дочерний процесс ACP. Неудачный прогрев позволяет первой попытке повторить запуск.
  • defaultEntry живет в течение всего времени работы моста при sessionScope: 'single'; канал очищается, когда sessionIds.size === 0 (после killSession) И isDying становится true.
  • MAX_EVENT_RING_SIZE = 1_000_000 — это мягкий верхний предел для BridgeOptions.eventRingSize, чтобы отловить опечатки оператора до того, как они приведут к OOM (~500 МБ на сессию).
  • DEFAULT_PERMISSION_TIMEOUT_MS = 0 позволяет человеческим разрешениям и вопросам ожидать бесконечно по умолчанию. permissionResponseTimeoutMs включает ограничение по настенным часам, когда операторам это необходимо; отмена голосующим, отмена сессии и завершение работы остаются доступными без него.
  • DEFAULT_MAX_PENDING_PER_SESSION = 64 зеркально отражает DEFAULT_MAX_SUBSCRIBERS; избыточные вызовы requestPermission разрешаются как отмененные с предупреждением в stderr.

Зависимости

UpstreamDownstream
@agentclientprotocol/sdkClientSideConnection, PROTOCOL_VERSION, типы ACPpackages/cli/src/serve/ (демон)
@qwen-code/qwen-code-coreApprovalMode, TrustGateError, getCurrentGeminiMdFilenamepackages/channels/base/ (запланировано, F4)
node:crypto, node:fs, node:pathpackages/vscode-ide-companion/ (запланировано, F4)

Конфигурация

BridgeOptions (bridgeOptions.ts):

KeyDefaultPurpose
boundWorkspace(required)Канонический путь к рабочему пространству, который применяет мост.
sessionScope'single''single' разделяет одну сессию между всеми клиентами; 'thread' создает отдельную сессию для каждого потока разговора.
channelFactorydefaultSpawnChannelFactoryПодключаемая фабрика дочерних процессов ACP.
initializeTimeoutMsDEFAULT_INIT_TIMEOUT_MS = 10_000Таймаут рукопожатия initialize ACP.
sessionRestoreTimeoutMs60_000Таймаут ACP loadSession / unstable_resumeSession; по умолчанию 60 с, явно настроенный таймаут инициализации может увеличить его, но не уменьшить.
maxSessionsDEFAULT_MAX_SESSIONS = 32Ограничение на byId.size. 0 / Infinity = без ограничений; NaN/отрицательное значение вызывает ошибку.
eventRingSizeDEFAULT_RING_SIZE (из eventBus.ts)Кольцо событий для каждой сессии; мягко ограничено MAX_EVENT_RING_SIZE.
permissionResponseTimeoutMsDEFAULT_PERMISSION_TIMEOUT_MS = 0Настенное время ожидания медиатора на каждый запрос; 0 отключает его.
maxPendingPermissionsPerSessionDEFAULT_MAX_PENDING_PER_SESSION = 64Обратное давление для агентов с высокой нагрузкой.
childEnvOverrides{}Добавления / очистки переменных окружения для каждого дескриптора дочернего процесса ACP.
externalToolGuard(none)Опциональный обработчик приватного решения pre-execution от дочернего процесса к родителю. Bridge принимает его только от владеющего канала для текущего активного промпта.
persistApprovalMode, persistDisabledToolsХуки записи настроек для маршрутов мутации Wave 4.
contextFilenameиз context.fileName в settings.jsonПереопределяет getCurrentGeminiMdFilename.
statusProvider(none)Предварительные проверки хоста демона (DaemonStatusProvider).
delegateReadTextFileToClienttrueУстановите false только для сред выполнения на одном хосте, чтобы каждый потребитель FileSystemService.readTextFile в дочернем процессе использовал обычный сервис файловой системы CLI.
fileSystem(none)Адаптер BridgeFileSystem для ACP readTextFile / writeTextFile.
permissionPolicyиз policy.permissionStrategy в settings.jsonОдно из first-responder / designated / consensus / local-only.
permissionConsensusQuorumиз settings.jsonN для политики консенсуса.
permissionAuditcreateNoOpPermissionAuditPublisher()Подключение к PermissionAuditRing для журнала аудита.
channelIdleTimeoutMs0Поддерживать дочерний процесс ACP активным в течение этого количества миллисекунд после закрытия последней сессии.

Таймауты восстановлений не могут быть отменены в текущем ACP SDK. Поэтому мост сохраняет ограждение урегулирования (settlement fence) и допуск возможности (capacity admission) до тех пор, пока реальный запрос не урегулируется или его транспорт не закроется. Поздний результат закрывается ровно один раз и никогда не регистрируется. Неопределённость очистки помещает в карантин только новую работу сессии в этом рабочем пространстве; существующий трафик сессии и управления рабочим пространством продолжается до тех пор, пока канал не дренируется и не будет переработан.

Дополнительные методы bridge

В дополнение к основным вызовам spawnOrAttach, sendPrompt, cancelSession, respondToPermission, loadSession и resumeSession, интерфейс HttpAcpBridge теперь включает следующие вспомогательные методы для взаимодействия с демоном:

МетодНазначение
generateSessionRecap(sessionId, context?)Генерация краткого описания сессии в одну строку.
generateSessionBtw(sessionId, question, signal?, context?)Ответ на побочный вопрос / prompt “btw”.
executeShellCommand(sessionId, command, signal?, context?)Выполнение shell-команды на хосте демона.
getSessionContextUsageStatus(sessionId, opts?)Возврат информации об использовании context-window.
getSessionSupportedCommandsStatus(sessionId)Возврат списка доступных slash-команд.
getSessionTasksStatus(sessionId)Возврат снимка состояния фоновых задач.
getSessionStatsStatus(sessionId)Возврат статистики использования сессии.
setSessionApprovalMode(sessionId, mode, opts, context?)Обновление режима подтверждения (approval mode) для сессии.
detachClient(sessionId, clientId?)Явное отключение клиента.
addRuntimeMcpServer(name, config, originatorClientId)Добавление MCP-сервера в runtime.
removeRuntimeMcpServer(name, originatorClientId)Удаление MCP-сервера в runtime.
manageMcpServer(serverName, action, originatorClientId)Включение / отключение / аутентификация / сброс аутентификации.
generateWorkspaceAgent(description, originatorClientId)Генерация определения subagent с помощью ИИ.
preheat()Прогрев дочернего процесса ACP перед первой сессией.
getSessionLastEventId(sessionId)Чтение монотонного id события сессии.
getWorkspaceToolsStatus()Возврат снимка встроенного реестра инструментов.
getWorkspaceMcpToolsStatus(serverName)Возврат инструментов для конкретного MCP-сервера.

Поле BridgeSpawnRequest.sessionScope было переименовано из 'per-client' в 'thread'. BridgeRestoredSession теперь содержит compactedReplay, liveJournal и lastEventId. Эти поля повторного воспроизведения представляют ограниченное окно в памяти для live-сессий, ограниченное BridgeOptions.compactedReplayMaxBytes (по умолчанию 4 МиБ, жёсткий потолок 256 МиБ). liveJournal в полёте отдельно ограничен BridgeOptions.maxJournalEvents (по умолчанию 10 000 записей воспроизведения) и BridgeOptions.maxJournalBytes (по умолчанию 8 МиБ сериализованных исходных событий). Последовательные совместимые фрагменты текста или мыслей разделяют одну запись воспроизведения, с не более чем 256 исходными событиями на запись; другие границы событий и атрибуции остаются нетронутыми. Если более старые сохранённые данные повторного воспроизведения были отброшены, compactedReplay[0] является маркером history_truncated без id; если записи журнала были отброшены, liveJournal[0] содержит маркер history_truncated с scope: 'live_journal'. Его подсчёты сохранённых и усечённых событий описывают исходные события, а не записи воспроизведения. Полный сохранённый транскрипт остаётся на диске и не раскрывается этим ответом bridge. BridgeClientRequestContext — это контекст запроса, передаваемый через вызовы bridge; он включает clientId, fromLoopback: boolean и promptId.

Ограничения и известные проблемы

  • MCP_RESTART_TIMEOUT_MS = 300_000 (5 мин) — таймаут bridge для /workspace/mcp/:server/restart намеренно сделан большим, так как McpClientManager.MAX_DISCOVERY_TIMEOUT_MS для stdio-серверов может достигать 5 минут. Более короткий дедлайн приводил бы к ложным таймаутам, пока дочерний процесс ACP продолжает переподключение в фоне.
  • BridgeOptions.eventRingSize > 1_000_000 вызывает исключение при создании.
  • connection.unstable_resumeSession предоставляется через стабильную возможность (capability) демона session_resume; unstable_session_resume по-прежнему объявляется как устаревший алиас для обратной совместимости со старыми SDK. Клиентам следует использовать feature-detection для session_resume.
  • Пакет bridge — это @qwen-code/acp-bridge. Текущий код импортирует примитивы event-bus и status напрямую из подпутей пакета; serve/acp-session-bridge.ts остается как локальный для CLI фасад совместимости для более широкой поверхности bridge.

Ссылки

  • packages/acp-bridge/src/bridge.ts (в частности, createHttpAcpBridge на строке 350+)
  • packages/acp-bridge/src/bridgeClient.ts
  • packages/acp-bridge/src/bridgeTypes.ts
  • packages/acp-bridge/src/bridgeOptions.ts
  • packages/acp-bridge/src/channel.ts
  • packages/acp-bridge/src/spawnChannel.ts
  • packages/acp-bridge/src/bridgeErrors.ts
  • Задачи: #3803 , #4175 .
Last updated on