ego-browser
O runtime de automatização de browser que os agentes IA usam para conduzir a sessão Chromium real do ego lite.
O ego-browser é o runtime de automatização de browser que o ego lite fornece aos agentes de IA. Fala o Chrome DevTools Protocol com a sessão Chromium real dentro do ego lite e recebe como ponto de entrada um script Node.js em heredoc: o agente escreve o fluxo JS inteiro numa única entrega no stdin, todos os helpers já estão injetados no scope do script, e o estado do browser persiste num Space.
O ego-browser não foi pensado para uma pessoa conduzir o browser à mão, e não substitui o Playwright nem o Puppeteer. O leitor visado é um agente LLM.
Para quem é
- Agentes de coding IA que precisam de conduzir um browser: Claude Code, Codex, Cursor, agentes SDK próprios.
- Equipas que constroem agentes verticais: automatizar Lark, Google Docs, Salesforce e back-offices semelhantes.
- Fluxos web repetitivos: login, preenchimento de formulários, export, pesquisa, leitura de tabelas.
- Quem já tentou enfiar um DOM completo ou uma página de HTML num LLM e bateu no limite de tokens.
Instalação
Vai junto com o ego lite — ver Início rápido. Depois de instalado, basta chamar ego-browser a partir de qualquer diretório.
A skill também pode ser instalada em separado:
npx skills add github:CitroLabs/ego-lite/skills/ego-browser
Ciclo central
O ritmo típico do agente a conduzir uma página — tudo num único heredoc:
ego-browser nodejs <<'EOF'
const task = await useOrCreateTaskSpace('search github issues')
await openOrReuseTab('https://github.com/issues', { wait: true, timeout: 20 })
cliLog(await snapshotText())
EOF
- Reutilizar ou criar um Task Space (declarado em cada heredoc — ver Space).
- Abrir a página alvo.
- Ler o snapshot (
snapshotText()) para obter a árvore semântica com[ref=N, loc=..., url=...]. - Agir sobre a página por
@Nou seletor CSS. - Imprimir o resultado final com
cliLog(...).
Dentro do heredoc está num processo Node.js; dentro de
js(...)está no contexto da página. Não misture.
Referência de helpers
Todos os helpers ficam disponíveis no scope do script pelo nome camelCase. Não é preciso import.
Task Space
await listTaskSpaces()
const task = await useOrCreateTaskSpace('describe task') // reutilizar ou criar
await completeTaskSpace(task.name) // concluído, manter o separador
await closeTaskSpace(task.name) // fechar o espaço
name deve descrever a tarefa em 3 a 6 palavras, em linguagem natural. Não use placeholders.
Navegação e estado
await listTabs()
await openOrReuseTab(url, { wait: true, timeout: 20 })
await gotoAndWait(url, { timeout: 20, settle: 1 })
await newTab(url)
await switchTab(tabId)
await currentTab()
await pageInfo()
await ensureRealTab() // um task space recém-criado pode ainda não ter separador
Observação
await snapshotText() // snapshot semântico da página inteira (por defeito)
await snapshotText({ scope: 'only_within_viewport' })
await captureScreenshot('result.png')
await drainEvents() // consumir a fila de eventos de navegação / rede
Rato e scroll
click, doubleClick, hover e dragMouse aceitam o mesmo formato de alvo (pixels CSS):
'string': seletor CSS ou@ref. Clica no centro do elemento.[x, y]ou{x, y}: coordenadas do viewport.{selector, x, y}: deslocamento a partir do canto superior esquerdo do elemento.options.label: descrição em 3 a 6 palavras. Quando passada, a ação dispara uma animação de destaque.
await click('@21', { label: 'verificar a sessão' })
await click('button.primary', { label: 'clicar no botão Enviar' })
await click([420, 260])
await hover('@5', { label: 'passar o rato no menu' })
await dragMouse([from, to], { label: 'arrastar o cartão' })
await scrollBy(900)
await scroll({ dy: 900 })
await scrollToBottomUntil(
async () => await js(String.raw`document.querySelectorAll('article').length`) >= 20,
{ step: 900, wait: 1, maxSteps: 20 },
)
Teclado e introdução de texto
await typeText('hello world')
await fillInput('@2', 'user@test.com')
await pressKey('Enter')
await dispatchKey({ ... })
Ficheiros e rede
await uploadFile('input[type="file"]', '/absolute/path/to/file.pdf')
await httpGet('https://api.example.com/data') // GET disparado no contexto da página
Esperas
await wait(1) // segundos
await waitForLoad()
await waitForElement('@1')
await waitForNetworkIdle()
wait()etimeoutestão em segundos. Só os parâmetros terminados emMsé que são em milissegundos.
Execução no browser
js(source) é, na prática, Runtime.evaluate e aceita uma string. Não lhe passe função mais argumentos como no Puppeteer — dá warning, é embrulhado em .toString(), e as variáveis capturadas e o canal de argumentos perdem-se.
Para lógica em vários passos, embrulhe tudo numa IIFE que devolve uma única vez:
const data = await js(String.raw`(() => {
const items = [...document.querySelectorAll('article')]
return items.map(el => ({
text: el.innerText,
links: [...el.querySelectorAll('a')].map(a => a.href),
}))
})()`)
await elementEval('@1', el => el.getBoundingClientRect())
await cdp('Page.captureScreenshot', { format: 'png' })
Saída e auto-descoberta
cliLog(value) // o único canal de saída dentro de um heredoc
cliLog(help('click')) // consultar o uso de um helper
Fluxo recomendado
Comece por snapshotText mais ref / loc — mantém-se a semântica e evita-se a fragilidade das coordenadas:
- Reutilizar ou criar o Task Space.
- Abrir ou mudar de página (
openOrReuseTab/gotoAndWait). snapshotText()para obter a árvore[ref=N, loc=..., url=...]. As refs ficam automaticamente registadas no refMap.- Agir sobre
@Ncomclick/fillInput/elementEval, ou fazer uma extração de DOM em um únicojs(...). cliLog(...)do resultado final.
Outros caminhos para combinar:
captureScreenshot+click([x, y]): layouts visuais, UIs em canvas, listas virtualizadas, páginas com acessibilidade incompleta.js/elementEval/cdp: extrair DOM diretamente, inspecionar o estado do browser, ou tudo o que não encaixa bem num helper standard.
Mantenha navegação, observação, scroll, extração, filtragem, agregação e saída dentro de um único heredoc
ego-browser nodejs. Não repasse os mesmos dados a um segundo scriptnodelocal.
Validade das refs
@N só é válido para o refMap do último snapshotText. Cada snapshotText() reconstrói o refMap. Os números das refs vêm do backendNodeId CDP do elemento, por isso o mesmo elemento costuma manter o mesmo número entre snapshots — mas para operar @N, N tem de aparecer no snapshot mais recente.
Causas habituais de Unknown ref:
- O elemento saiu do viewport.
- O DOM voltou a renderizar.
- O snapshot anterior foi
scope: 'only_within_viewport'e o atual não cobre o elemento.
Se precisa de uma referência estável ao mesmo elemento ao longo de várias rondas, use o loc=... da saída do snapshot ou escreva um seletor CSS. É também a base da acumulação de Experience — ver Skills.
Skill workspace
O ego-browser não traz, por si só, experiência mutável de agente. Por defeito carrega extensões de helpers e a experiência aprendida sobre sites a partir do bundle de skills do repo:
../../skills/ego-browser
Pode ser sobreposto por variável de ambiente:
EGO_BROWSER_AGENT_WORKSPACE=/path/to/ego-browser ego-browser nodejs <<'EOF'
cliLog(await siteSkills())
EOF
A experiência por site em learnings/ está sempre ativa e é lida em cada chamada de helper. O modelo de escrita e descoberta de Experience está em Skills.
Validar a experiência aprendida:
npm run validate:learnings
Estrutura de pastas
package/ego-browser/
├── src/ # browser-runtime / helpers / run.js
│ ├── browser-runtime.js # ponte ego runtime do lado do browser
│ ├── helpers.js # helpers expostos ao script do agente
│ ├── run.js # entrada CLI (executa stdin)
│ └── learning/ # índice de experiência, validação de domínio, validação de formato
├── artifacts/ego-browser/ # resultado de build; npm bin aponta para aqui
└── test/ # testes unitários
skills/ego-browser/
├── SKILL.md / SKILL.zh.md # ponto de entrada para o agente
└── learnings/ # diretório de experiência por site
Notas
snapshotText()usascope: 'full_page'por defeito e cobre a página toda. Só passe'only_within_viewport'se realmente precisar apenas da área visível.js()devolve diretamente o resultado da expressão; não façaJSON.parse(...)por cima.- Quando escrever regex dentro de uma template string de
js(), duplique as barras invertidas (\\d,\\s) ou passe aString.raw. - Um
returnao nível de topo é embrulhado automaticamente em IIFE. Umreturndentro de um callback aninhado pode despoletar a mesma coisa, por isso escreva expressões complexas como(() => { ... })()à partida. - Quando o utilizador pediu explicitamente o ego-browser, o runtime já está pronto. Evite pré-verificações como
which ego-browser/node -v/ dump do help; só vá lá se uma primeira execução der erro.