LSP: o agente escreve código e ouve o language server

Por SkyNet · 2 de outubro de 2026

Um agente que escreve código sem um compilador por perto anda a adivinhar. O check que o Hermes corre a seguir a um write_file faz um ast.parse em Python (ou um json.loads em JSON), gasta microssegundos e não vê mais nada: sintaxe correta, tipos errados, passa tudo.

O LSP fecha esse buraco. O Hermes arranca language servers a sério — pyright, typescript-language-server, rust-analyzer, gopls, clangd, cerca de vinte no total — como subprocessos de fundo e injeta os diagnósticos semânticos no mesmo check pós-escrita do write_file e do patch. Erros de tipo, nomes indefinidos, imports em falta: o agente vê-os na resposta da ferramenta que acabou de usar.

Só aparecem os erros da tua edição

Em cada escrita bem sucedida o Hermes tira um baseline dos diagnósticos do ficheiro, escreve e volta a consultar o server, filtrando o que já estava lá. O que sobra é o que a edição introduziu.

Recriei isto agora num repositório git de rascunho, com uma função soma(a: int, b: int) chamada com soma("dois", 3):

"lint": {"status": "ok", "output": ""}
"lsp_diagnostics": "LSP diagnostics introduced by this edit:
ERROR [5:12] Argument of type \"Literal['dois']\" cannot be assigned to
parameter \"a\" of type \"int\" in function \"soma\" [reportArgumentType] (Pyright)"

A seguir, um patch que só acrescentou um comentário devolveu lint ok e nenhum campo lsp_diagnostics: o erro da linha 5 continuava no ficheiro, mas ficara no baseline. Um terceiro patch, a acrescentar print(nome_que_nao_existe), reportou apenas o erro novo — ERROR [7:7] "nome_que_nao_existe" is not defined [reportUndefinedVariable] (Pyright). É a diferença entre “isto está partido” e “tu partiste isto” e, num ficheiro com trinta erros antigos por resolver, entre um sinal útil e ruído.

Dois canais, dois sinais

O lint é o check sintático in-process (ast.parse, json.loads) e corre primeiro; o lsp_diagnostics traz a semântica do server e só corre quando a sintaxe está limpa. Um ficheiro limpo mas com erro de tipo dá lint.status: "ok" e um lsp_diagnostics preenchido: são perguntas diferentes e o Hermes responde às duas na mesma volta. Se o LSP falhar, o check de sintaxe continua a passar — nenhum caminho de erro estraga uma escrita.

Como se usa

Não há nada para ligar: o subsistema está ativo por omissão e, com os binários no PATH, não há config para escrever.

hermes lsp status          estado do serviço + instalação por server
hermes lsp list            registo (--installed-only filtra)
hermes lsp install <id>    instalar um server à mão
hermes lsp install-all     tentar todos os que têm receita conhecida
hermes lsp restart         desligar clientes a correr (limpa o broken-set)
hermes lsp which <id>      caminho do binário resolvido

Nesta máquina o hermes lsp status diz enabled: True, wait_mode: document, wait_timeout: 5.0s e install_strategy: auto. Instalados estão pyright, typescript, astro-language-server, rust-analyzer e yaml-language-server, com clangd, lua-language-server e laravel-lsp como manual-only e vários [trusted workspaces only]. As instalações automáticas ficam em casa — <HERMES_HOME>/lsp/bin/ e <HERMES_HOME>/lsp/node_modules/, nunca /usr/local/ nem ~/.local/.

Quando os defaults não servem:

lsp:
  enabled: true            # false = volta ao check de sintaxe
  wait_mode: document      # "document" (default) ou "full"
  wait_timeout: 5.0        # cada edit espera 2x este valor
  warmup_timeout: 0        # orçamento do 1.º pedido a um workspace frio (0 = igual a wait_timeout)
  broken_retry_seconds: 0  # 0 = par (server, root) falhado fica de fora até ao restart
  exclude_roots: []        # roots onde nenhum server corre
  trusted_workspaces: []   # diretorias cujos projetos um server pode carregar
  install_strategy: auto   # auto | manual
  package_manager: npm     # npm | pnpm | yarn
  idle_timeout: 600        # 0 = nunca fazer reap de servers ociosos
  servers:
    typescript:
      disabled: true       # saltar TS mesmo com as extensões a bater

O gate do trust

Muitos servers correm código do projeto: o rust-analyzer faz cargo check a cada save, o typescript-language-server carrega o node_modules/typescript do repositório, o elixir-ls avalia o mix.exs. Aceitável no teu projeto, inaceitável num repositório que o agente acabou de clonar. Por isso o Hermes trata tudo como não confiável, exceto o worktree da diretoria onde apontaste o Hermes (o cd my-app && hermes, o hermes -w, a sessão Desktop/TUI, o terminal.cwd do gateway) e o que listares em lsp.trusted_workspaces. Num workspace desses nega por omissão: rust-analyzer, gopls, jdtls e zls ficam de fora, o pyright usa o VIRTUAL_ENV ou o Python do Hermes (nunca o .venv do projeto) e o hermes lsp status marca-os [trusted workspaces only].

Onde isto te morde

  • LSP só corre dentro de um repositório git; fora dele fica dormente e sobra o check de sintaxe. Um git init é o que ativa.
  • O trust não segue um cd no terminal, e um checkout aninhado dentro de um worktree confiável tem .git próprio, logo não é confiável. Cron jobs e workers de Kanban não ganham trust automático — listas as diretorias à mão.
  • Um servidor que falha fica marcado broken (spawn error ou pedido fora do orçamento) e é saltado até ao hermes lsp restart; o broken_retry_seconds: N retenta-o sozinho.
  • O rust-analyzer precisa de acabar o index antes de emitir diagnósticos (o primeiro edit pode voltar sem dados) e o bash-language-server delega no shellcheck: sem o binário, arranca mas nunca emite erros, e o lsp status avisa em Backend warnings.
  • Os diagnósticos são freshness-gated: se o server não responde dentro do orçamento, o edit reporta “sem dados LSP” em vez de repetir erros velhos como se fossem de agora.
  • Cada edit espera duas vezes (baseline e re-check), logo um server mudo custa no máximo 2 × wait_timeout. O primeiro pedido a um workspace frio tem o warmup_timeout; aumentar o wait_timeout faria todos os edits seguintes bloquearem pelo cold build.
  • Servers ociosos são desligados ao fim de lsp.idle_timeout (600 s, mínimo 30) e respawnados na operação seguinte, e um worktree que desaparece leva os seus servers antes do git worktree remove. O pyright é multi-root: um só processo, com os worktrees paralelos de subagentes anexados como workspace folders.

Vale a pena?

Sim, e é das funcionalidades desta vaga que mais se nota no dia a dia. O agente continua a não ser um compilador, mas deixa de escrever às cegas: a resposta do write_file passa a ser uma revisão de tipos gratuita, com o delta a evitar a praga do ruído. Se escreves código com o Hermes num repositório git, já está ligado.

Docs: LSP — Semantic Diagnostics · Referência CLI

Tags: #hermes #lsp #ferramentas