Skip to Content
Guia do DesenvolvedorDaemonACP Bridge

ACP Bridge

Visão geral

packages/acp-bridge/ é responsável pela fronteira entre a camada HTTP do daemon e o processo filho ACP. É consumido por packages/cli/src/serve/ (o daemon qwen serve) e foi extraído no passo 3 do #4175 F1 para que futuros consumidores (channels/base/AcpBridge.ts, o companion de IDE do VS Code) possam usar o mesmo núcleo da bridge sem acessar diretamente o pacote CLI.

Cada WorkspaceRuntime ativo possui uma instância de HttpAcpBridge. A produção tenta pré-aquecer a bridge primária e tenta novamente no primeiro uso após falha. Um secundário confiável abre seu AcpChannel e inicia seu filho sob demanda; um secundário não confiável não consegue iniciar o ACP. Dentro do runtime, a bridge fornece sessões multiplexadas sobre o canal, EventBuses por sessão, um MultiClientPermissionMediator, um adaptador BridgeFileSystem e auxiliares orientados a ACP (spawnOrAttach, loadSession, resumeSession, sendPrompt, cancelSession, respondToPermission, além de RPCs extMethod para status do workspace e reinício do MCP). Bridges e filhos nunca são compartilhados entre runtimes de workspace.

Responsabilidades

  • Iniciar ou anexar ao processo filho ACP por meio de um ChannelFactory plugável. Fábrica padrão: defaultSpawnChannelFactory (subprocesso qwen --acp). Testes injetam inMemoryChannel.
  • Manter aliveChannels (registro de canais) e byId (registro de sessões).
  • Multiplexar N sessões do lado HTTP em um único processo filho ACP via connection.newSession().
  • Serializar prompts por sessão através de promptQueue (o ACP impõe um prompt ativo por sessão).
  • FIFO por sessão para chamadas de setSessionModel, garantindo que anexações concorrentes com modelos diferentes não gerem race conditions no agente.
  • EventBus por sessão que aciona GET /session/:id/events (ver 10-event-bus.md).
  • Fluxo de permissão: BridgeClient.requestPermissionMultiClientPermissionMediator.request → fan-out → coleta de votos → resposta ACP (ver 04-permission-mediation.md).
  • E/S de arquivos: adaptador BridgeFileSystem para leituras e escritas ACP; runtimes do daemon no mesmo host anunciam readTextFile: false para que leituras de texto normais permaneçam no filho enquanto as escritas finais de texto continuam delegadas (ver 07-workspace-filesystem.md).
  • RPCs extMethod para status em nível de workspace (/workspace/mcp, /workspace/skills, /workspace/providers), reinício do MCP e o callback privado e gerenciado opcional do Tool Guard.
  • Ciclo de vida: shutdown() gracioso com KILL_HARD_DEADLINE_MS (10s) por canal; killAllSync() síncrono para forçar saída no segundo sinal.

Arquitetura

Ponto de entrada público: createHttpAcpBridge(opts: BridgeOptions): HttpAcpBridge em packages/acp-bridge/src/bridge.ts.

Tipos principais:

TypeFileRole
HttpAcpBridgebridgeTypes.tsInterface pública: spawnOrAttach, loadSession, resumeSession, sendPrompt, cancelSession, subscribeEvents, respondToPermission, getWorkspaceMcpStatus, restartMcpServer, shutdown, killAllSync, …
BridgeSessionbridgeTypes.ts{ sessionId, workspaceCwd, attached, clientId?, createdAt? } retornado para os handlers HTTP.
BridgeOptionsbridgeOptions.tsConfiguração em tempo de construção (ver Configuração).
AcpChannelchannel.ts{ stream, kill(), killSync(), exited } — um canal ACP NDJSON.
ChannelFactorychannel.ts(workspaceCwd, childEnvOverrides?) => Promise<AcpChannel>.
BridgeClientbridgeClient.tsEncapsula uma ClientSideConnection do ACP; implementa o Client do ACP (requestPermission, readTextFile, writeTextFile, sessionUpdate, extNotification).
EventBuseventBus.tsPub/sub em memória por sessão. Ver 10-event-bus.md.
MultiClientPermissionMediatorpermissionMediator.tsMediador de quatro políticas. Ver 04-permission-mediation.md.

Estado interno (fechado por createHttpAcpBridge):

StateShapePurpose
aliveChannelsMap<string, ChannelInfo>Registro de canais com chave pelo id do canal. Cada ChannelInfo contém channel, connection, client (um BridgeClient por canal), sessionIds: Set<string>, pendingRestoreIds, statusClosedReject?, isDying: boolean.
byIdMap<string, SessionEntry>Registro de sessões com chave pelo sessionId. Cada SessionEntry contém 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 | nullA sessão “única” usada quando sessionScope: 'single'.
defaultPolicyPermissionPolicyConfigurado via BridgeOptions.permissionPolicy.
mediatorMultiClientPermissionMediatorUm por instância da bridge.
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.

Invariante isDying: qualquer caminho de teardown deve definir ChannelInfo.isDying = true de forma síncrona antes de aguardar channel.kill(). ensureChannel trata um canal moribundo como ausente e cria (spawn) um novo. Sem essa flag, um spawnOrAttach concorrente chegando durante a janela de graça do SIGTERM (até 10s) se anexaria a um transporte prestes a fechar e o sessionId do chamador retornaria 404 em todas as requisições subsequentes. Locais de definição (devem ser mantidos sincronizados): ensureChannel (falha de inicialização + re-verificação de shutdown tardio), doSpawn (falha de newSession em canal vazio), killSession (última sessão saindo), shutdown (em massa).

Invariante de retenção de channelInfo: não limpe channelInfo ao definir isDying = true. killAllSync ainda deve encontrar o canal durante a janela de graça do SIGTERM para disparar SIGKILL em process.exit(1). aliveChannels mantém a entrada moribunda até que channel.exited seja disparado.

Buffering limitado do BridgeClient: Frames ACP extNotification chegando em BridgeClient para um sessionId ainda não presente em byId (porque a resposta de connection.newSession ainda não retornou, mas a descoberta do MCP dentro de newSession já disparou eventos de budget) são armazenados em buffer em uma fila de eventos iniciais limitada por MAX_EARLY_EVENT_SESSIONS = 64 × MAX_EARLY_EVENTS_PER_SESSION = 32 × EARLY_EVENT_TTL_MS = 60_000. O pior caso é aproximadamente 400 KB de heap. Sem o buffering, o primeiro slot do anel de replay SSE para uma nova sessão perderia os eventos disparados durante sua criação.

Fluxo de trabalho

spawnOrAttach (ponto de entrada principal)

Pontos principais:

  • sessionScope='single' com um defaultEntry existente apenas incrementa attachCount, registra clientId e retorna attached: true.
  • O caminho a frio executa o ChannelFactory, realiza o initialize do ACP (DEFAULT_INIT_TIMEOUT_MS=10s), chama connection.newSession({cwd}) e então registra o novo SessionEntry.
  • SessionLimitExceededError é lançado quando byId.size >= maxSessions.
  • InvalidClientIdError é lançado se X-Qwen-Client-Id estiver fora de [A-Za-z0-9._:-]{1,128}.
  • O reaper de desconexão em server.ts rastreia o proprietário do spawn via attachCount/spawnOwnerWantedKill para evitar derrubar uma sessão cujo proprietário do spawn desconectou, mas outros clientes já se anexaram (revisar #3889 BQ9tV).

Serialização de prompts

Falhas na cauda da fila são ignoradas para que a rejeição de um prompt anterior não envenene os prompts subsequentes; o chamador original ainda recebe a rejeição em sua própria promise retornada. O transportClosedReject armazenado em cache na sessão coloca a promise do prompt em corrida (race) contra channel.exited, de modo que um processo filho travado seja reportado imediatamente em vez de ficar pendurado.

Fluxo de permissão (alto nível)

InvalidPermissionOptionError é lançado pré-mediador quando um voto na rede tenta injetar CANCEL_VOTE_SENTINEL através do campo normal optionId — o sentinel é a única válvula de escape da bridge para interromper uma requisição como cancelled / agent_cancelled e não deve ser acessível acidentalmente pela rede. Ver 04-permission-mediation.md.

Shutdown

Channel factory

AcpChannel (channel.ts) é a abstração de transporte da bridge. A produção usa defaultSpawnChannelFactory em spawnChannel.ts, que executa qwen --acp como um subprocesso com um par de pipes stdio. Testes injetam inMemoryChannel para executar o agente no mesmo processo. A bridge não sabe nada sobre o mecanismo subjacente — ela só precisa de { stream, kill, killSync, exited }.

ChannelFactory aceita childEnvOverrides para que cada handle do daemon possa passar suas próprias variáveis de ambiente de budget do MCP (QWEN_SERVE_MCP_CLIENT_BUDGET, QWEN_SERVE_MCP_BUDGET_MODE) sem mutar process.env (o que causaria race conditions quando dois daemons embarcados rodam no mesmo processo Node).

Estado e ciclo de vida

  • A construção da bridge é síncrona. O chamador pode pré-aquecer o canal antes da primeira sessão; caso contrário, o primeiro spawnOrAttach inicializa a frio o processo filho ACP. Um pré-aquecimento com falha deixa o primeiro uso livre para tentar novamente.
  • defaultEntry vive durante todo o ciclo de vida da bridge sob sessionScope: 'single'; o canal é recolhido (reaped) quando sessionIds.size === 0 (após killSession) E isDying vira true.
  • MAX_EVENT_RING_SIZE = 1_000_000 é um limite superior flexível para BridgeOptions.eventRingSize para capturar erros de digitação do operador antes de OOMs de ~500 MB por sessão.
  • DEFAULT_PERMISSION_TIMEOUT_MS = 0 permite que permissões humanas e perguntas aguardem indefinidamente por padrão. permissionResponseTimeoutMs habilita um limite de relógio (wall-clock) quando os operadores precisarem de um; o cancelamento de voto, o cancelamento de sessão e o shutdown permanecem disponíveis sem ele.
  • DEFAULT_MAX_PENDING_PER_SESSION = 64 espelha DEFAULT_MAX_SUBSCRIBERS; chamadas excessivas de requestPermission são resolvidas como canceladas com um aviso no stderr.

Dependências

UpstreamDownstream
@agentclientprotocol/sdkClientSideConnection, PROTOCOL_VERSION, tipos ACPpackages/cli/src/serve/ (o daemon)
@qwen-code/qwen-code-coreApprovalMode, TrustGateError, getCurrentGeminiMdFilenamepackages/channels/base/ (planejado, F4)
node:crypto, node:fs, node:pathpackages/vscode-ide-companion/ (planejado, F4)

Configuração

BridgeOptions (bridgeOptions.ts):

KeyDefaultPurpose
boundWorkspace(obrigatório)Caminho canônico do workspace que a bridge impõe.
sessionScope'single''single' compartilha uma sessão entre todos os clientes; 'thread' cria uma sessão separada para cada thread de conversa.
channelFactorydefaultSpawnChannelFactoryFábrica plugável do processo filho ACP.
initializeTimeoutMsDEFAULT_INIT_TIMEOUT_MS = 10_000Timeout do handshake initialize do ACP.
sessionRestoreTimeoutMs60_000Timeout do ACP loadSession / unstable_resumeSession; padrão 60s, e um timeout de initialize configurado explicitamente pode elevá-lo, mas nunca reduzi-lo.
maxSessionsDEFAULT_MAX_SESSIONS = 32Limite para byId.size. 0 / Infinity = ilimitado; NaN/negativo lança erro.
eventRingSizeDEFAULT_RING_SIZE (de eventBus.ts)Anel de eventos por sessão; limite flexível em MAX_EVENT_RING_SIZE.
permissionResponseTimeoutMsDEFAULT_PERMISSION_TIMEOUT_MS = 0Tempo de relógio (wallclock) por requisição para o mediador; 0 desativa.
maxPendingPermissionsPerSessionDEFAULT_MAX_PENDING_PER_SESSION = 64Backpressure para agentes de alto volume.
childEnvOverrides{}Adições / limpezas de variáveis de ambiente por handle para o processo filho ACP.
externalToolGuard(nenhum)Handler opcional para a decisão privada filho-para-pai de pré-execução. A bridge o aceita apenas do canal proprietário para o Prompt ativo no momento.
persistApprovalMode, persistDisabledToolsHooks de escrita de configurações para as rotas de mutação da Wave 4.
contextFilenamede context.fileName em settings.jsonSobrescreve getCurrentGeminiMdFilename.
statusProvider(nenhum)Células de pré-voo do host do daemon (DaemonStatusProvider).
delegateReadTextFileToClienttrueDefina como false apenas para runtimes no mesmo host para que todo consumidor filho de FileSystemService.readTextFile use o serviço de filesystem regular da CLI.
fileSystem(nenhum)Adaptador BridgeFileSystem para readTextFile / writeTextFile do ACP.
permissionPolicyde policy.permissionStrategy em settings.jsonUm entre first-responder / designated / consensus / local-only.
permissionConsensusQuorumde settings.jsonN para a política de consenso.
permissionAuditcreateNoOpPermissionAuditPublisher()Conecta ao PermissionAuditRing para a trilha de auditoria.
channelIdleTimeoutMs0Mantém o processo filho ACP vivo por esta quantidade de milissegundos após o fechamento da última sessão.

Restaurações com timeout expirado não são canceláveis no SDK ACP atual. A bridge portanto mantém uma cerca de settlement e admissão de capacidade até que a requisição real seja concluída ou seu transporte feche. Um resultado tardio é fechado exatamente uma vez e nunca registrado. Incerteza de limpeza coloca em quarentena apenas trabalho novo de sessão naquele workspace; tráfego existente de sessão e controle de workspace continua até que o canal drene e seja reciclado.

Métodos adicionais da bridge

Além das chamadas principais spawnOrAttach, sendPrompt, cancelSession, respondToPermission, loadSession e resumeSession, a interface HttpAcpBridge agora inclui estes auxiliares voltados para o daemon:

MétodoPropósito
generateSessionRecap(sessionId, context?)Gera um resumo de uma linha da sessão.
generateSessionBtw(sessionId, question, signal?, context?)Responde a uma pergunta secundária ou prompt “btw”.
executeShellCommand(sessionId, command, signal?, context?)Executa um comando shell no host do daemon.
getSessionContextUsageStatus(sessionId, opts?)Retorna o uso da janela de contexto.
getSessionSupportedCommandsStatus(sessionId)Retorna os comandos slash disponíveis.
getSessionTasksStatus(sessionId)Retorna um snapshot das tarefas em segundo plano.
getSessionStatsStatus(sessionId)Retorna as estatísticas de uso da sessão.
setSessionApprovalMode(sessionId, mode, opts, context?)Atualiza o modo de aprovação para uma sessão.
detachClient(sessionId, clientId?)Desanexa explicitamente um cliente.
addRuntimeMcpServer(name, config, originatorClientId)Adiciona um servidor MCP em tempo de execução.
removeRuntimeMcpServer(name, originatorClientId)Remove um servidor MCP em tempo de execução.
manageMcpServer(serverName, action, originatorClientId)Habilita / desabilita / autentica / limpa autenticação.
generateWorkspaceAgent(description, originatorClientId)Gera uma definição de subagente com IA.
preheat()Aquece o processo filho ACP antes da primeira sessão.
getSessionLastEventId(sessionId)Lê o ID de evento monótono da sessão.
getWorkspaceToolsStatus()Retorna o snapshot do registro de ferramentas integradas.
getWorkspaceMcpToolsStatus(serverName)Retorna as ferramentas de um servidor MCP específico.

BridgeSpawnRequest.sessionScope foi renomeado de 'per-client' para 'thread'. BridgeRestoredSession agora carrega compactedReplay, liveJournal e lastEventId. Esses campos de replay são uma janela limitada em memória para sessões ativas, limitada por BridgeOptions.compactedReplayMaxBytes (padrão 4 MiB, teto rígido 256 MiB). O liveJournal em voo é limitado separadamente por BridgeOptions.maxJournalEvents (padrão 10 000 entradas de replay) e BridgeOptions.maxJournalBytes (padrão 8 MiB de eventos fonte serializados). Chunks de texto ou pensamento consecutivos e compatíveis compartilham uma entrada de replay, com no máximo 256 eventos fonte por entrada; outras fronteiras de evento e atribuição permanecem intactas. Se um replay retido mais antigo foi descartado, compactedReplay[0] é o marcador history_truncated sem id; se entradas do journal foram descartadas, liveJournal[0] carrega um marcador history_truncated com scope: 'live_journal'. Suas contagens de retidos e truncados descrevem eventos fonte, não entradas de replay. A transcrição persistida completa permanece em disco e não é exposta por essa resposta da bridge. BridgeClientRequestContext é o contexto de requisição propagado através das chamadas da bridge; ele carrega clientId, fromLoopback: boolean e promptId.

Ressalvas e Limitações Conhecidas

  • MCP_RESTART_TIMEOUT_MS = 300_000 (5 min) — o tempo limite da bridge para /workspace/mcp/:server/restart é intencionalmente grande porque McpClientManager.MAX_DISCOVERY_TIMEOUT_MS pode ser de até 5 min para servidores stdio. Um prazo mais curto geraria tempos limite falsos enquanto o processo filho ACP continuasse reconectando em segundo plano.
  • BridgeOptions.eventRingSize > 1_000_000 lança uma exceção na construção.
  • connection.unstable_resumeSession é exposto através da capacidade estável session_resume do daemon; unstable_session_resume continua sendo anunciado como um alias de compatibilidade obsoleto para SDKs mais antigos. Os clientes devem detectar a feature session_resume.
  • O pacote da bridge é @qwen-code/acp-bridge. O código atual importa primitivas de event-bus e status diretamente dos subcaminhos do pacote; serve/acp-session-bridge.ts permanece como a fachada de compatibilidade local da CLI para a superfície mais ampla da bridge.

Referências

  • packages/acp-bridge/src/bridge.ts (especialmente createHttpAcpBridge na linha 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
  • Issues: #3803 , #4175 .
Last updated on