← Toda a documentação

Escrevendo um adaptador personalizado

Um adaptador envolve uma CLI ou servidor de agente e traduz sua saída num fluxo normalizado de blocos — texto em streaming, raciocínio, chamadas de ferramentas, cards de plano, diffs — para que todo agente pareça igual no app. Uma instância de adaptador equivale a uma sessão interativa com um agente. Os adaptadores integrados ficam em terminull_bridge/adapters/ (Claude Code, Codex, Cursor, Gemini, Kilo, OpenCode, Pi, Hermes, Droid); o seu pode ser adicionado sem tocar no núcleo.

O contrato BaseAdapter

Crie uma subclasse de BaseAdapter e implemente quatro corrotinas. O contrato é validado na importação — uma subclasse que pula uma declaração obrigatória gera TypeError imediatamente, então um adaptador não pode herdar silenciosamente um comportamento vazio.

MétodoRetornaResponsabilidade
start(session_id)AgentInfoBlockInicia/conecta o agente; retorna suas capacidades via make_agent_info().
send(text, attachments=None)—Envia uma mensagem do usuário para a sessão em execução.
stream()AsyncIterator[Block]Emite blocos normalizados conforme o agente os produz.
stop()—Encerra a sessão de forma limpa.

A cada turno, seu stream() deve terminar com um SessionEndBlock, e chamadas de ferramentas devem emitir um ToolCallBlock running e depois um done/error (mais um ToolResultBlock correspondente). Duas superfícies opcionais exigem desativação explícita — você precisa definir cada flag como True ou False:

FlagSe True, sobrescrevePara
provides_modelslist_models()O seletor de modelo de nova sessão.
provides_usageget_usage()Estatísticas de custo/tokens para /status.

Um adaptador mínimo

import asyncio, uuid
from terminull_bridge.adapters.base import BaseAdapter
from terminull_bridge.blocks import TextBlock, SessionEndBlock

class EchoAdapter(BaseAdapter):
    # ── detection identity ──
    agent_id     = "echo"
    display_name = "Echo"
    cli_binary   = "echo"        # default detect() probes `echo --version` on PATH

    # ── capabilities (single source of truth) ──
    cap_thinking    = False
    cap_permissions = False
    cap_interrupt   = False

    # ── optional surfaces: explicit opt-out required ──
    provides_models = False
    provides_usage  = False

    def __init__(self, cwd=None, model=None, **kw):
        self._cwd   = cwd
        self._queue = asyncio.Queue()

    async def start(self, session_id):
        self._sid = session_id
        return self.make_agent_info(name="Echo", version="1.0")

    async def send(self, text, attachments=None):
        await self._queue.put(text)

    async def stream(self):
        text = await self._queue.get()
        # stream the reply, then close the turn
        yield TextBlock(text=f"You said: {text}", done=True, id=str(uuid.uuid4()))
        yield SessionEndBlock(reason="completed")

    async def stop(self):
        pass

make_agent_info() lê seus atributos cap_* e adiciona automaticamente os comandos de barra universais do Terminull Service — monte o bloco de informações através dele em vez de montar as capacidades na mão. Seu __init__ é filtrado por assinatura: o Terminull Service oferece cwd, model, binary, resume_session_id, on_session_id (chame-o com o id de sessão do seu próprio agente para que um restart do Terminull Service consiga retomar essa conversa) e effort — mas só passa os que você realmente declarar, então um def __init__(self, cwd=None) mínimo já funciona. Declare o que você precisar.

Capacidades

Declaradas como atributos de classe; expostas no AgentInfoBlock para que o app mostre ou esconda a UI correspondente.

AtributoEfeito no app
cap_imagesSeletor de imagens habilitado no compositor.
cap_documentsAnexos de arquivos que não são imagem são aceitos.
cap_thinkingBlocos de raciocínio serão renderizados.
cap_permissionsCards de permissão vão aparecer.
cap_subagentsChamadas de ferramentas de subagentes são renderizadas como grupos aninhados.
cap_interruptBotão de parar ativo no meio do turno.
cap_input_during_turnO compositor continua habilitado durante a geração (caso contrário, as mensagens entram na fila).
cap_terminalPode abrir um terminal no dispositivo.

Detecção & instalação plug-and-play

A detecção fica no adaptador como um classmethod detect() → DetectedAgent | None — o mesmo caminho para adaptadores integrados e externos. O detect() padrão exige agent_id e, se cli_binary estiver definido, testa com shutil.which() + --version. Sobrescreva-o para verificações baseadas em HTTP/config.

Um adaptador externo é só um módulo .py colocado em ~/.terminull/ext_adapters/ que define uma subclasse de BaseAdapter com pelo menos agent_id + display_name:

# ~/.terminull/ext_adapters/echo.py
from terminull_bridge.adapters.base import BaseAdapter
class EchoAdapter(BaseAdapter):
    agent_id = "echo"; display_name = "Echo"; cli_binary = "echo"
    provides_models = False; provides_usage = False
    ...   # the four coroutines

O carregador importa cada módulo de forma protegida — ele roda a mesma verificação de contrato, rejeita colisões com ids integrados, registra em log e ignora qualquer coisa malformada, e registra os válidos. Eles são então detectados e instanciados exatamente como os integrados. Sem editar o núcleo, sem reiniciar nada além do Terminull Service.

Onde o código mora. Os adaptadores integrados vêm como código-fonte editável em terminull_bridge/adapters/*.py — mesmo no Terminull Service publicado, compilado em bytecode, essa pasta continua em .py puro para que você possa lê-los como exemplos prontos e copiar um como ponto de partida. O adaptador de referência completo, pronto para copiar e colar — documentando cada hook e a assinatura completa do __init__ acima — é examples/ext_adapter_example.py; coloque uma cópia em ~/.terminull/ext_adapters/ e edite. O contrato que você está implementando é terminull_bridge/adapters/base.py.

Ciclo de vida numa execução de duas superfícies

Uma sessão pode ser controlada a partir de duas superfícies — a CLI nativa do agente num terminal e o Terminull — ao longo da sua vida. A regra é um escritor, muitos observadores: o Terminull nunca deve se tornar um segundo escritor simultâneo numa conversa que o terminal está controlando ativamente. Como um adaptador participa depende de como ele mantém o agente entre os turnos:

TipoEntre turnosExemplos
Por turno / sem locaçãoInicia um processo novo a cada turno em stream() e depois encerra — deixando a CLI livre para retomar entre os turnos.Codex, Cursor, Gemini, Droid, Pi
PersistenteMantém o agente (um subprocesso ou servidor de longa duração) entre os turnos — precisa de uma liberação explícita antes que um terminal possa assumir o controle.Claude Code, OpenCode, Kilo

Para tornar a transferência de controle transparente, um adaptador pode implementar alguns métodos de classe opcionais (todos partem de "não suportado" por padrão, então um adaptador básico simplesmente não participa da transferência):

HookPapel
scan_external_sessions()Descobre sessões que esse agente iniciou fora do Terminull (lê seus registros salvos em disco) → elas aparecem na barra lateral do app.
is_cwd_externally_owned(cwd, sid)Existe um processo nativo controlando esse diretório/sessão agora? Controla a proteção contra conflito para que o Terminull fique em espera em vez de escrever em duplicidade.
make_transcript_mirror(cwd, sid)Leitura somente do final do registro de uma sessão controlada pelo terminal → espelhada ao vivo no app, sem escrever nela.
latest_sid_for_cwd(cwd)Reresolve a origem da linhagem após o terminal devolver o controle (o --resume pode gerar um novo id).
terminal_resume_command(sid)Comando de shell que retoma a sessão num terminal → alimenta o botão "Continuar no terminal" do app.

O fluxo, de ponta a ponta: o app mostra sessões de CLI externas via scan_external_sessions(); tocar em uma vincula uma sessão do Terminull a ela. Se o terminal for o dono do diretório, o Terminull Service estaciona a sessão (enfileirando mensagens e mostrando um estado de "observador desacoplado") e transmite um espelho somente leitura em vez de escrever. Quando o terminal fica ocioso, o Terminull Service reconecta automaticamente — reresolvendo a origem da linhagem e depois esvaziando a fila. Ao contrário, "Continuar no terminal" libera uma sessão ativa ociosa e a estaciona novamente como espelho. O design completo, com a matriz de cobertura por adaptador, está em docs/session-handoff.md.

O núcleo do Terminull Service é feito para ser aberto. A estrutura do código-fonte, a referência completa de blocos e o design de transferência de controle estão junto disso na pasta docs/ do repositório. Dúvidas: hello@terminull.dev.