Pular para o conteúdo

Detecção e Conexão de Agentes ACP

O ThairaAI pode controlar muitos agentes de código diferentes (Claude Code, Qwen, Codex, Goose, …) que todos falam o Agent Communication Protocol (ACP) via CLI. Disso decorrem duas perguntas: como o app encontra quais agentes estão instalados e como ele conversa com um, uma vez selecionado? Esta página responde ambas — primeiro o motor de detecção, depois um trace completo de conexão para o Claude Code, e então um mergulho no pacote bridge que de fato roda o Claude.

O AcpDetector (src/process/agent/acp/AcpDetector.ts) é o motor singleton de descoberta de agentes no processo principal. Ele detecta todos os agentes de CLI ACP disponíveis no sistema a partir de três fontes em paralelo, mescla e deduplica os resultados, e produz uma lista unificada de agentes para a UI e o IPC bridge.

acpDetector = new AcpDetector() // singleton de nível de módulo, exportado para uso global

Suas dependências-chave são src/process/utils/shellEnv.ts (resolução de ambiente) e src/common/types/acpTypes.ts (tipos).

initialize()
├── Promise.all([
│ ├── detectBuiltinAgents() ← Fonte 1: lista conhecida de CLIs embutidas
│ ├── detectExtensionAgents() ← Fonte 2: registro de extensões
│ └── detectCustomAgents() ← Fonte 3: configuração definida pelo usuário
│])
├── sintetiza o agente Gemini (sempre presente, sem detecção de CLI)
├── mescla: [Aionrs, Gemini, ...Builtin, ...Other, ...Remote, ...Extension]
└── this.isInitialized = true

Nota sobre o SaaS gerenciado. O AgentRegistry ainda sintetiza uma entrada gemini (e a ordem de mesclagem abaixo ainda a lista) por compatibilidade, mas o agente Gemini em si foi removido: não tem fábrica de agentes e não pode rodar. No modo gerenciado, o IPC do seletor (getAvailableAgents em acpConversationBridge.ts) filtra a lista detectada para apenas aionrs — todos os outros backends, incluindo gemini, ficam ocultos a menos que THAIRA_DEV_AGENTS=1. Portanto o gemini sintetizado é efetivamente legado/só-dev e não aparece para o usuário.

Fonte 1 — detectBuiltinAgents()

Itera sobre todas as ferramentas de CLI conhecidas em POTENTIAL_ACP_CLIS, chamando isCliAvailable(cli.cmd) para cada uma para verificar se existe no PATH do sistema.

POTENTIAL_ACP_CLIS é gerado dinamicamente a partir de ACP_BACKENDS_ALL (um Proxy com inicialização lazy), com estas regras de filtro:

  • Precisa ter um cliCommand.
  • Precisa estar enabled: true.
  • Exclui gemini (embutido, sem detecção), custom (configurado pelo usuário) e aionrs (protocolo não-ACP).

Retorna um DetectedAgent[] contendo backend, name, cliPath e acpArgs.

Fonte 2 — detectExtensionAgents()

Recupera os adaptadores ACP contribuídos por extensões via ExtensionRegistry.getInstance().getAcpAdapters(). Condições de filtro:

  • connectionType precisa ser 'cli' ou 'stdio'.
  • Precisa ter um cliCommand não vazio.
  • Chama isCliAvailable(cliCommand) para cada adaptador para verificar.

Os agentes retornados têm um backend: 'custom' fixo e carregam os marcadores isExtension: true e extensionName. O método inteiro é envolvido em try/catch; se o ExtensionRegistry falhar ao carregar, ele retorna silenciosamente um array vazio.

Fonte 3 — detectCustomAgents()

Lê a configuração de assistentes de ProcessConfig.get('assistants'). Condições de filtro:

  • enabled === true.
  • defaultCliPath não vazio ou isPreset === true.

Nenhuma verificação de disponibilidade de CLI é feita — o usuário é responsável por garantir que a CLI existe. Erros ENOENT/not found são silenciosamente ignorados (o arquivo de config pode ainda não existir); outros erros são logados como warnings.

O método central isCliAvailable difere por plataforma.

macOS / Linux

Terminal window
which <command>
  • Timeout: 1 segundo.
  • Usa enhancedEnv como ambiente (PATH do shell + caminhos de ferramentas adicionais).
  • Sucesso: execSync não lança exceção → true. Falha → false.

Windows — fallback de dois níveis:

  1. Primário: where <command> (timeout de 1 segundo).
  2. Fallback: PowerShell Get-Command -All <command> | Select-Object -First 1 | Out-Null (timeout de 1 segundo).

getEnhancedEnv() (de shellEnv.ts) é uma dependência crítica para a detecção de CLI. Ela garante que as ferramentas de CLI possam ser descobertas, seja o app iniciado por um terminal, seja pelo Finder/launchd.

Ordem de mesclagem do PATH (da maior para a menor prioridade):

1. bundledBunDir ← maior prioridade, runtime bun empacotado
2. process.env.PATH ← ambiente do processo atual
3. shellEnv.PATH ← ambiente do shell de login do usuário
4. caminhos extras por plataforma ← escaneados por plataforma de dirs de instalação conhecidos
5. customEnv.PATH ← custom definido pelo chamador (se houver)

Carregamento do ambiente de shell do usuário:

PlataformaMétodo de resolução do shellShell padrãoComando de load
macOSdscl . -read /Users/<username> UserShell/bin/zsh<shell> -l -c env
Linuxgetent passwd <username>/bin/bash<shell> -l -c env
WindowsPula o carregamento do ambiente de shellN/AN/A

Allowlist de variáveis de ambiente herdadas: PATH, NODE_EXTRA_CA_CERTS, SSL_CERT_FILE, SSL_CERT_DIR, REQUESTS_CA_BUNDLE, CURL_CA_BUNDLE, NODE_TLS_REJECT_UNAUTHORIZED, ANTHROPIC_AUTH_TOKEN, ANTHROPIC_API_KEY, ANTHROPIC_BASE_URL.

Caminhos extras de ferramentas — macOS / Linux:

CaminhoPropósito
~/.bun/binpacotes globais bun
~/.cargo/binRust / cargo
~/go/binGo
~/.deno/binDeno
~/.local/binpip, pipx, etc.

Caminhos extras de ferramentas — Windows:

CaminhoPropósito
%APPDATA%\npmpacotes globais npm
%ProgramFiles%\nodejsinstalação oficial do Node.js
%APPDATA%\nvm / %NVM_HOME%nvm-windows
%ProgramFiles%\nodejs / %NVM_SYMLINK%symlink da versão ativa do nvm
%LOCALAPPDATA%\fnm_multishellsfnm-windows
~\.volta\binVolta
~\scoop\shims / %SCOOP%\shimsScoop
%LOCALAPPDATA%\pnpmpnpm global
%ChocolateyInstall%\binChocolatey
%ProgramFiles%\Git\{cmd,bin,usr\bin}Git for Windows (inclui cygpath)
%ProgramFiles(x86)%\Git\{cmd,bin,usr\bin}Git for Windows (x86)
C:\cygwin64\bin, C:\cygwin\binCygwin
~\.bun\binpacotes globais bun

Todos os caminhos só são anexados se existirem e ainda não estiverem no PATH atual.

Backend IDComando CLIArgs de lançamento ACPNome
claudeclaude['--experimental-acp']Claude Code
qwenqwen['--acp']Qwen Code
codexcodex[]Codex
codebuddycodebuddy['--acp']CodeBuddy
goosegoose['acp']Goose
auggieauggie['--acp']Augment Code
kimikimi['acp']Kimi CLI
opencodeopencode['acp']OpenCode
droiddroid['exec', '--output-format', 'acp']Factory Droid
copilotcopilot['--acp', '--stdio']GitHub Copilot
qoderqodercli['--acp']Qoder CLI
vibevibe-acp[]Mistral Vibe
openclaw-gatewayopenclaw['gateway']OpenClaw
nanobotnanobot['--experimental-acp']Nano Bot
cursoragent['acp']Cursor Agent
kirokiro-cli['acp']Kiro

Backends excluídos da detecção de CLI:

Backend IDMotivo
geminiAgente embutido, sempre disponível, sem detecção de CLI
customDefinido pelo usuário, sem cliCommand
aionrsProtocolo não-ACP (JSON Lines), explicitamente excluído
remoteSem CLI local, conecta via URL WebSocket

Ordem de mesclagem: Aionrs > Gemini > Builtin > Other > Remote > Extension. Nenhuma deduplicação é feita — a mesma CLI pode existir simultaneamente como builtin e como extensão; a diferenciação é tratada na camada de UI.

MétodoEscopo do refreshLimpa cache de env
refreshBuiltinAgents()Apenas agentes de CLI embutidosSim
refreshExtensionAgents()Apenas agentes de extensãoSim
refreshRemoteAgents()Apenas agentes remotosNão
refreshAll()Todas as fontes redetectadasSim

Todos os métodos de refresh primeiro removem os agentes do tipo correspondente, depois redetectam e anexam.

Modo de lançamentoArquivo de entradaInvocação
Electronsrc/index.tsinitializeAcpDetector() (async paralelo)
Standalonesrc/process/utils/initBridgeStandalone.tsacpDetector.initialize() (chamada direta)
ConsumidorPropósito
src/process/bridge/acpConversationBridge.tsIPC bridge: obtém lista de agentes, health check, sondagem de model
src/process/extensions/hub/HubInstaller.tsrefreshAll + verifica resultados de detecção após instalar extensão
src/process/extensions/hub/HubStateManager.tsFaz refresh de agentes embutidos para determinar status de instalação de extensão
src/process/team/TeammateManager.tsFiltra tipos de agente disponíveis para a feature de Team
src/process/channels/actions/SystemActions.tsMonta a lista de agentes selecionáveis para os canais
  1. Cada fonte de detecção tem seu próprio try/catch — uma fonte falhar não afeta as outras.
  2. Gemini é sempre injetado — garante ao menos um agente disponível.
  3. Timeout de detecção de CLI de 1 segundo — impede which/where de travar.
  4. Timeout de carregamento do ambiente de shell de 5 segundos — impede configs de shell anômalas de travar a inicialização.
  5. Fallback duplo no Windows — se where falhar, tenta automaticamente o PowerShell Get-Command.
  6. A inicialização é idempotenteinitialize() usa o flag isDetected para evitar execução duplicada.
  7. Agentes custom pulam a validação de CLI — a verificação de disponibilidade é pulada; os usuários são responsáveis.
  8. cache de env limpo sob demanda — cada refresh limpa o enhancedEnv para capturar mudanças no PATH.

Uma vez selecionado um agente, a conexão percorre esta cadeia de arquivos:

AcpDetector.tsacpConversationBridge.tsAcpAgentManager.tsAcpAgent (index.ts)AcpConnection.tsacpConnectors.ts.

Usuário seleciona Claude Code → envia mensagem
├── [IPC Bridge] acpConversationBridge
│ └── workerTaskManager.getOrBuildTask(conversationId)
├── [Task Manager] AcpAgentManager
│ ├── resolveCliPath('claude') → 'claude' ou caminho configurado pelo usuário
│ ├── new AcpAgent({ backend: 'claude', cliPath, ... })
│ └── agent.start()
├── [Agent] AcpAgent.start()
│ ├── connection.connect('claude', cliPath, workingDir)
│ ├── performAuthentication()
│ ├── createOrResumeSession()
│ ├── applyYoloMode() (se habilitado)
│ └── applyModelFromSettings()
├── [Connection] AcpConnection.connect('claude')
│ └── doConnect() → switch('claude') → connectClaude()
├── [Connector] connectClaude()
│ └── connectNpxBackend({
│ npxPackage: '@zed-industries/claude-agent-acp@0.21.0',
│ prepareFn: prepareClaude,
│ })
├── [Spawn] spawnNpxBackend()
│ └── spawn(npxCommand, ['--yes', '--prefer-offline',
│ '@zed-industries/claude-agent-acp@0.21.0'], { stdio: 'pipe', detached: true })
├── [Protocol] setupChildProcessHandlers()
│ ├── stdout → parsing NDJSON → handleMessage()
│ ├── stderr → buffer de diagnóstico
│ └── initialize() → handshake JSON-RPC
└── [Session] newSession() / sendPrompt()
└── JSON-RPC via stdin/stdout

Descoberta-chave: o Claude não é iniciado diretamente

Seção intitulada “Descoberta-chave: o Claude não é iniciado diretamente”

O Claude Code não é lançado diretamente via claude --experimental-acp. O que de fato é lançado é um pacote bridge npx:

Terminal window
# Comando realmente executado (macOS)
/usr/local/bin/npx --yes --prefer-offline @zed-industries/claude-agent-acp@0.21.0

@zed-industries/claude-agent-acp é um bridge ACP mantido pela Zed que internamente trata de lançar e gerenciar a CLI do Claude Code. O ThairaAI se comunica com esse bridge via JSON-RPC (NDJSON) por stdin/stdout. (Veja Dentro do bridge claude-agent-acp para o que acontece dentro dele.)

Resolução do caminho da CLI (AcpAgentManager.ts)

Seção intitulada “Resolução do caminho da CLI (AcpAgentManager.ts)”

Quando backend === 'claude', a lógica de resolução de caminho é:

  1. ProcessConfig.get('acp.config') para checar se o usuário configurou um cliPath custom.
  2. Se não, recorre a ACP_BACKENDS_ALL.claude.cliCommand = 'claude'.
  3. Para o Claude, acpArgs é undefined em ACP_BACKENDS_ALL, então o padrão ['--experimental-acp'] é usado.

No entanto, esse cliPath e acpArgs não afetam de fato a conexão para o Claude — ele segue o caminho do bridge npx, não um spawn genérico.

De acpConnectors.ts:

async function prepareClaude(): Promise<NpxPrepareResult> {
const cleanEnv = await prepareCleanEnv();
ensureMinNodeVersion(cleanEnv, 20, 10, 'Claude ACP bridge');
return { cleanEnv, npxCommand: resolveNpxPath(cleanEnv) };
}

O que prepareCleanEnv() faz:

  1. loadFullShellEnvironment() — carrega assincronamente o ambiente de shell completo do usuário (<shell> -i -l -c env, incluindo API keys exportadas no .zshrc, etc.).
  2. getEnhancedEnv() — mescla process.env + PATH do shell + caminhos de ferramentas por plataforma + bun empacotado.
  3. Mescla ambos: { ...fullShellEnv, ...enhancedEnv }.
  4. Limpa variáveis nocivas: remove NODE_OPTIONS, NODE_INSPECT, NODE_DEBUG; remove CLAUDECODE (previne detecção aninhada); remove todas as variáveis npm_* (previne interferência do ciclo de vida do npm).

O bridge ACP do Claude requer Node.js ≥ 20.10. Se uma versão mais antiga for detectada, ele escaneia os diretórios nvm/fnm/volta por uma versão adequada e corrige o PATH.

Uma estratégia de retentativa em duas fases:

Fase 1: npx --yes --prefer-offline @zed-industries/claude-agent-acp@0.21.0
↓ falhou?
Fase 2: npx --yes @zed-industries/claude-agent-acp@0.21.0 (sem --prefer-offline)
  • Fase 1: usa --prefer-offline para preferir o cache local do npm (~1-2s).
  • Fase 2: após a Fase 1 falhar, descarta --prefer-offline e busca do registro npm (~3-5s).

Argumentos reais do spawn:

spawn(
npxCommand,
[
'--yes', // auto-confirma a instalação
'--prefer-offline', // só na Fase 1
'@zed-industries/claude-agent-acp@0.21.0',
],
{
cwd: workingDir,
stdio: ['pipe', 'pipe', 'pipe'], // stdin/stdout/stderr todos em pipe
env: cleanEnv,
shell: false, // não usa shell no Unix
detached: true, // macOS/Linux: cria nova session, previne SIGTTOU
}
);
child.unref(); // permite o processo pai sair normalmente

Se a conexão falhar, connect() (em AcpConnection.ts) tem duas camadas adicionais de recuperação:

  1. notarget / version mismatch: executa npm cache clean --force, depois tenta de novo.
  2. cache do npx corrompido (ENOENT/ERR_MODULE_NOT_FOUND): deleta todo o diretório ~/.npm/_npx, depois tenta de novo.

Estabelecimento do protocolo (setupChildProcessHandlers)

Seção intitulada “Estabelecimento do protocolo (setupChildProcessHandlers)”

Parsing do stdout — protocolo NDJSON:

let buffer = '';
child.stdout?.on('data', (data: Buffer) => {
buffer += data.toString();
const lines = buffer.split('\n');
buffer = lines.pop() || ''; // retém a última linha incompleta
for (const line of lines) {
if (line.trim()) {
const message = JSON.parse(line) as AcpMessage;
this.handleMessage(message);
}
}
});

Cada linha do stdout é uma mensagem JSON-RPC completa. O protocolo é Newline-Delimited JSON (NDJSON).

Handshake de inicialização:

await Promise.race([
this.initialize(), // envia a requisição initialize
new Promise((reject) => setTimeout(reject, 60000)), // timeout de 60 segundos
processExitPromise, // falha imediatamente se o processo sair cedo
]);

A requisição initialize enviada ao stdin do filho:

{
"jsonrpc": "2.0",
"id": 0,
"method": "initialize",
"params": { "protocolVersion": 1, "clientCapabilities": { "fs": { "readTextFile": true, "writeTextFile": true } } }
}

A resposta contém authMethods, agentCapabilities (incluindo mcpCapabilities), etc.

Fluxo de autenticação do Claude (src/process/agent/acp/index.ts):

  1. Verifica authMethods na resposta do initialize.
  2. Primeiro tenta criar uma session diretamente.
  3. Se a autenticação falhar, executa claude /login para renovar o token.
  4. Tenta criar a session de novo.
// Enviado:
{"jsonrpc":"2.0","id":2,"method":"session/new","params":{
"cwd": ".",
"mcpServers": [...],
"_meta": {
"claudeCode": {
"options": {
"resume": "<existing-session-id>"
}
}
}
}}

Específico do Claude: a retomada de session é implementada via _meta.claudeCode.options.resume (outros backends usam o parâmetro resumeSessionId). A resposta contém sessionId, configOptions (model, mode) e models.

Enviando um prompt:

{
"jsonrpc": "2.0",
"id": 3,
"method": "session/prompt",
"params": {
"sessionId": "abc123",
"prompt": [{ "type": "text", "text": "Mensagem de entrada do usuário" }]
}
}

O filho envia mensagens NDJSON via stdout, despachadas por handleMessage():

Tipo de mensagemCondiçãoTratamento
ResponseTem id, sem methodCasa com uma Promise em pendingRequests, resolve/reject
NotificationTem method, sem idDespachada para handleIncomingRequest()
RequestTem method e idDespachada para handleIncomingRequest(), resposta escrita de volta

Tratamento de métodos de entrada:

MétodoPropósitoTratamento
session/updateConteúdo em streaming, tool calls, thinkingAcpAgentAcpAdapter → convertido em mensagens de UI
session/request_permissionRequisição de permissão de ferramentaPausa o timeout → mostra diálogo de UI → usuário seleciona → escreve o resultado de volta
fs/read_text_fileBackend lê um arquivoFaz parse do caminho → readTextFile() → escreve o conteúdo de volta
fs/write_text_fileBackend escreve um arquivoFaz parse do caminho → writeTextFile() → notifica a UI

O fluxo de dados para conteúdo em streaming:

AcpConnection.onSessionUpdate
→ AcpAgent.handleSessionUpdate()
→ AcpAdapter.convertSessionUpdate() // converte em TMessage[]
→ AcpAgent.emitMessage()
→ callback AcpAgentManager.onStreamEvent
→ transformMessage() → addOrUpdateMessage() (DB)
→ ipcBridge.acpConversation.responseStream.emit() (para o renderer)

Cancelar o prompt atual (sem matar o processo):

{ "jsonrpc": "2.0", "method": "session/cancel", "params": { "sessionId": "abc123" } }

Desconexão completa:

AcpConnection.disconnect()
→ stopPromptKeepalive()
→ terminateChild()
→ killChild()
macOS/Linux (detached): process.kill(-pid, 'SIGTERM') // mata o grupo de processos
Windows: taskkill /PID <pid> /T /F
→ espera até 3 segundos
→ reseta todo o estado

Comportamento específico do Claude vs. outros backends

Seção intitulada “Comportamento específico do Claude vs. outros backends”
RecursoClaudeOutros backends
Método de lançamentobridge npx (@zed-industries/claude-agent-acp)Spawn direto da CLI (goose acp, qwen --acp)
Retomada de session_meta.claudeCode.options.resumeparâmetro resumeSessionId
Fonte do modelANTHROPIC_MODEL de ~/.claude/settings.jsonresposta de session/new
Modo YOLO'bypassPermissions''yolo' (Qwen, etc.)
Recuperação de falha de authExecuta claude /login para renovar o tokenSem tratamento especial
Troca de modelInjeta <system-reminder> para notificar a IA de uma troca de modelNenhum
Requisito de versão do Node≥ 20.10≥ 18.17 (genérico)
Recuperação de cache do npxSuportada (membro de NPX_BACKENDS)Não aplicável
MóduloArquivoResponsabilidade
AcpDetectorAcpDetector.tsDetectar agentes de CLI instalados no sistema
acpConversationBridgeacpConversationBridge.tsIPC bridge entre o renderer e o processo principal
AcpAgentManagerAcpAgentManager.tsGestão do ciclo de vida da task: cria Agent, persiste, eventos IPC
AcpAgentindex.tsOrquestra conexão/auth/session/mensagens; permissões, troca de model
AcpConnectionAcpConnection.tsProtocolo central: gestão de processo filho, envio/recepção JSON-RPC, estado da session
acpConnectorsacpConnectors.tsLógica de spawn por backend, preparação de ambiente, Fase 1/2 do npx
AcpAdapterAcpAdapter.tsConverte updates de session ACP para o formato TMessage do AionUi
ApprovalStoreApprovalStore.tsCache de permissão “always allow” a nível de session
utilsutils.tsEscrita JSON-RPC no stdin, terminação de processo, I/O de arquivo
mcpSessionConfigmcpSessionConfig.tsMonta a lista de servidores MCP para session/new
modelInfomodelInfo.tsExtrai info do model de configOptions/models
constantsconstants.tsStrings de modo YOLO por backend

As seções acima mostraram como o ThairaAI lança @zed-industries/claude-agent-acp via npx. Esta seção mergulha no próprio pacote bridge, traçando como ele descobre e controla o runtime local do Claude via claude-code-sdk.

Referências de código-fonte:

O pacote foi migrado de @zed-industries/claude-agent-acp (v0.23.1) para @agentclientprotocol/claude-agent-acp (v0.25.3), compartilhando a mesma base de código.

@agentclientprotocol/claude-agent-acp
├── @agentclientprotocol/sdk (0.17.0) -- implementação do protocolo ACP
├── @anthropic-ai/claude-agent-sdk (0.2.83+) -- Claude Code SDK
│ ├── @anthropic-ai/sdk (^0.80.0) -- cliente da API Anthropic
│ ├── @modelcontextprotocol/sdk (^1.27.1) -- protocolo MCP
│ └── cli.js -- CLI Claude Code embutida (~13MB)
└── zod (^3.25.0 || ^4.0.0)

Descoberta central: usa uma CLI embutida, não o seu PATH

Seção intitulada “Descoberta central: usa uma CLI embutida, não o seu PATH”

O claude-agent-acp não busca no PATH do sistema por uma CLI claude instalada localmente. Ele sempre usa o cli.js embutido dentro do pacote npm @anthropic-ai/claude-agent-sdk (versão travada, ex.: Claude Code v2.1.92). Esse cli.js é um runtime Claude Code completo, autocontido, minificado, de ~13MB.

Passo 1 — ponto de entrada (dist/index.js)

// modo --cli: roda diretamente a CLI Claude embutida do SDK
if (process.argv.includes('--cli')) {
await import(await claudeCliPath());
}
// modo padrão: roda como agente ACP
runAcp();

Passo 2 — estabelecimento do transporte ACP (runAcp())

export function runAcp() {
const input = nodeToWebWritable(process.stdout);
const output = nodeToWebReadable(process.stdin);
const stream = ndJsonStream(input, output);
new AgentSideConnection((client) => new ClaudeAcpAgent(client), stream);
}

O adaptador se comunica com o cliente ACP upstream (ThairaAI / editor Zed, etc.) via NDJSON por stdin/stdout. AgentSideConnection trata o dispatch JSON-RPC.

Passo 3 — como o adaptador chama o claude-code-sdk

Três funções-chave são importadas do SDK:

import { getSessionMessages, listSessions, query } from '@anthropic-ai/claude-agent-sdk';
FunçãoPropósito
query()Função central: cria o subprocesso da CLI para uma session
listSessions()Lista sessions existentes do disco
getSessionMessages()Recupera mensagens de uma session anterior para replay

Ao criar uma nova session ACP:

const q = query({
prompt: input, // AsyncIterable<SDKUserMessage> (Pushable)
options, // configuração da session
});

Passo 4 — como o SDK localiza o binário da CLI (caminho crítico)

Dentro de sdk.mjs, a função de inicialização resolve o caminho do executável:

// pseudocódigo desofuscado de sdk.mjs
let pathToClaudeCodeExecutable = options.pathToClaudeCodeExecutable;
if (!pathToClaudeCodeExecutable) {
const currentDir = fileURLToPath(import.meta.url);
const parentDir = path.join(currentDir, '..');
pathToClaudeCodeExecutable = path.join(parentDir, 'cli.js');
}

Comportamento padrão: usa o cli.js no mesmo diretório do pacote SDK, sem busca no PATH. O helper de resolução de caminho no lado do adaptador:

export async function claudeCliPath() {
return isStaticBinary()
? (await import('@anthropic-ai/claude-agent-sdk/embed')).default
: import.meta.resolve('@anthropic-ai/claude-agent-sdk').replace('sdk.mjs', 'cli.js');
}

Passo 5 — formas de sobrescrever o caminho da CLI

MecanismoPrioridadeDescrição
Variável de ambiente CLAUDE_CODE_EXECUTABLEMáximaSobrescreve diretamente o caminho do executável
Modo binário estático (CLAUDE_AGENT_ACP_IS_SINGLE_FILE_BUN)AltaBuild Bun --compile: extrai de $bunfs para dir temp
options.pathToClaudeCodeExecutableMédiaOpção de API a nível de SDK
Fallback padrãoMínimapath.join(dirname(import.meta.url), "cli.js")

No modo binário estático (bun build --compile), o cli.js é embutido no sistema de arquivos virtual do Bun. Em tempo de execução, extractFromBunfs() o copia para /tmp/claude-agent-sdk-<sha256hash>/cli.js, porque os subprocessos não conseguem acessar o $bunfs.

Passo 6 — spawn do subprocesso (ProcessTransport)

A classe ProcessTransport dentro do SDK lança a CLI:

// sdk.mjs desofuscado
const isNativeBinary =
!pathToClaudeCodeExecutable.endsWith('.js') &&
!pathToClaudeCodeExecutable.endsWith('.mjs') &&
!pathToClaudeCodeExecutable.endsWith('.tsx') &&
!pathToClaudeCodeExecutable.endsWith('.ts') &&
!pathToClaudeCodeExecutable.endsWith('.jsx');
const command = isNativeBinary ? pathToClaudeCodeExecutable : executable;
// executable padrão: isBun ? "bun" : "node"
this.process = spawn(command, args, {
cwd: cwd,
stdio: ['pipe', 'pipe', stderrMode],
signal: abortSignal,
env: env,
windowsHide: true,
});

Argumentos-chave de CLI passados:

ArgumentoPropósito
--output-format stream-jsonA CLI emite JSON em streaming
--input-format stream-jsonA CLI aceita entrada JSON em streaming
--verboseHabilita saída verbosa
--permission-prompt-tool stdioRequisições de permissão retornadas via stdin/stdout
--model, --max-turns, --thinking, --cwdVárias opções de configuração da session

O SDK também herda o runtime que lançou o adaptador (executable: isStaticBinary() ? undefined : process.execPath), garantindo que o mesmo Node.js rode o subprocesso da CLI.

Passo 7 — protocolo de comunicação entre SDK e CLI

A comunicação é baseada no NDJSON em streaming via stdin/stdout do subprocesso:

  • Entrada (SDK → CLI): as mensagens do usuário são escritas no stdin como linhas JSON via ProcessTransport.write().
  • Saída (CLI → SDK): a CLI escreve mensagens JSON no stdout, uma por linha; o SDK as parseia linha a linha usando readline.createInterface().

Tipos de mensagem emitidos pela CLI:

TipoDescrição
systeminit, status, compact_boundary, session_state_changed
resultsuccess, error_during_execution, error_max_turns
stream_eventcontent_block_start, content_block_delta, message_start
user / assistantMensagens de conversa contendo blocos tool_use, text, thinking
tool_progressProgresso de execução de ferramenta
auth_statusStatus de autenticação
rate_limit_eventEvento de rate limit
┌─────────────────────────────────────────────────────┐
│ ThairaAI │
│ │
│ cria o adaptador como subprocesso via npx │
│ comunica via NDJSON stdin/stdout (JSON-RPC) │
└────────────────────┬────────────────────────────────┘
│ stdin/stdout (NDJSON, JSON-RPC)
┌─────────────────────────────────────────────────────┐
│ claude-agent-acp (ACP Agent Adapter) │
│ │
│ ClaudeAcpAgent: traduz mensagens ACP ↔ SDK │
│ AgentSideConnection: dispatch JSON-RPC do ACP │
│ SettingsManager: lê .claude/settings.json │
│ Proxy de permissão: canUseTool → requestPermission │
└────────────────────┬────────────────────────────────┘
│ API query() do SDK
┌─────────────────────────────────────────────────────┐
│ @anthropic-ai/claude-agent-sdk │
│ │
│ query(): cria o ProcessTransport │
│ ProcessTransport: cria cli.js como subprocesso │
│ Caminho padrão: │
│ path.join(dirname(import.meta.url), "cli.js") │
└────────────────────┬────────────────────────────────┘
│ child_process.spawn()
│ stdin/stdout (NDJSON em streaming)
┌─────────────────────────────────────────────────────┐
│ cli.js (Claude Code embutido ~13MB) │
│ │
│ Runtime Claude Code completo │
│ --output-format stream-json │
│ --input-format stream-json │
│ --permission-prompt-tool stdio │
│ comunica com a API Anthropic via HTTPS │
└─────────────────────────────────────────────────────┘

O ThairaAI lança claude-agent-acp via npx; uma vez que o processo bridge está rodando:

  1. Entre o ThairaAI e o bridge = protocolo ACP (NDJSON via stdin/stdout).
  2. O bridge internamente chama o query() do SDK = API do SDK.
  3. O SDK cria o subprocesso cli.js = NDJSON em streaming.
  4. cli.js conversa com a API Anthropic = HTTPS.

A cadeia inteira tem três níveis de subprocessos aninhados:

ThairaAI (Electron main)
→ npx claude-agent-acp (Node.js)
→ node cli.js (runtime Claude Code)
→ HTTPS → api.anthropic.com