← Toda la documentación

Escribir un adaptador personalizado

Un adaptador envuelve una CLI o servidor de agente y traduce su salida en un flujo de bloques normalizado — texto en streaming, razonamiento, llamadas a herramientas, tarjetas de plan, diffs — para que cada agente se sienta idéntico en la app. Una instancia de adaptador equivale a una sesión interactiva con un agente. Los adaptadores integrados viven en terminull_bridge/adapters/ (Claude Code, Codex, Cursor, Gemini, Kilo, OpenCode, Pi, Hermes, Droid); el tuyo se puede añadir sin tocar el núcleo.

El contrato de BaseAdapter

Crea una subclase de BaseAdapter e implementa cuatro corrutinas. El contrato se hace cumplir en el import — una subclase que se salta una declaración obligatoria lanza TypeError de inmediato, así un adaptador no puede heredar en silencio una implementación vacía.

MétodoDevuelveResponsabilidad
start(session_id)AgentInfoBlockInicia/conecta el agente; devuelve sus capacidades vía make_agent_info().
send(text, attachments=None)—Envía un mensaje del usuario a la sesión en curso.
stream()AsyncIterator[Block]Produce bloques normalizados a medida que el agente los genera.
stop()—Cierra la sesión de forma limpia.

Cada turno de tu stream() debe terminar con un SessionEndBlock, y las llamadas a herramientas deben emitir un ToolCallBlock en estado running y luego done/error (más un ToolResultBlock emparejado). Dos superficies opcionales requieren exclusión explícita — debes fijar cada flag en True o False:

FlagSi es True, anulaPara
provides_modelslist_models()El selector de modelo de la nueva sesión.
provides_usageget_usage()Estadísticas de costo/tokens para /status.

Un 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() lee tus atributos cap_* y antepone automáticamente los comandos de barra universales de Terminull Service — construye el bloque de información a través de él en lugar de armar las capacidades a mano. Tu __init__ se filtra por firma: Terminull Service ofrece cwd, model, binary, resume_session_id, on_session_id (llámalo con el id de sesión propio de tu agente para que un reinicio de Terminull Service pueda reanudar esta conversación) y effort — pero solo pasa los que realmente declares, así un def __init__(self, cwd=None) mínimo sigue funcionando. Declara solo lo que necesites.

Capacidades

Se declaran como atributos de clase; se exponen en el AgentInfoBlock para que la app muestre u oculte la interfaz correspondiente.

AtributoEfecto en la app
cap_imagesSelector de imágenes habilitado en el compositor.
cap_documentsSe aceptan adjuntos de archivos que no son imágenes.
cap_thinkingSe renderizarán los bloques de razonamiento.
cap_permissionsAparecerán tarjetas de permiso.
cap_subagentsLas llamadas a herramientas de sub-agentes se renderizan como grupos anidados.
cap_interruptBotón de detener activo a mitad de turno.
cap_input_during_turnEl compositor permanece habilitado durante la generación (de lo contrario los mensajes se encolan).
cap_terminalPuede abrir una terminal en el dispositivo.

Detección & instalación tipo drop-in

La detección vive en el adaptador como un classmethod detect() → DetectedAgent | None — la misma ruta para adaptadores integrados y externos. El detect() por defecto requiere agent_id y, si cli_binary está definido, prueba con shutil.which() + --version. Sobrescríbelo para sondeos basados en HTTP o configuración.

Un adaptador externo es simplemente un módulo .py colocado en ~/.terminull/ext_adapters/ que define una subclase de BaseAdapter con al 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

El cargador importa cada módulo de forma protegida — ejecuta la misma verificación de contrato, rechaza colisiones con los ids integrados, registra en el log y omite todo lo malformado, y registra los válidos. Luego se detectan e instancian exactamente igual que los integrados. Sin editar el núcleo, sin reiniciar nada más que Terminull Service.

Dónde vive el código. Los adaptadores integrados se distribuyen como código fuente editable en terminull_bridge/adapters/*.py — incluso en el Terminull Service publicado y compilado a bytecode, esa carpeta se mantiene en .py plano para que puedas leerlos como ejemplos trabajados y copiar uno como punto de partida. El adaptador de referencia completo, listo para copiar y pegar — que documenta cada hook y la firma completa de __init__ de arriba — es examples/ext_adapter_example.py; coloca una copia en ~/.terminull/ext_adapters/ y edítala. El contrato que estás implementando es terminull_bridge/adapters/base.py.

Ciclo de vida a través de una ejecución de dos superficies

Una sesión puede manejarse desde dos superficies — la CLI nativa del agente en una terminal y Terminull — a lo largo de su vida. La regla es un escritor, muchos observadores: Terminull nunca debe convertirse en un segundo escritor simultáneo en una conversación que la terminal está manejando activamente. Cómo participa un adaptador depende de cómo mantiene el agente entre turnos:

TipoEntre turnosEjemplos
Por turno / sin arrendamientoInicia un proceso nuevo en cada turno dentro de stream() y luego sale — así la CLI queda libre para reanudar en el intermedio.Codex, Cursor, Gemini, Droid, Pi
PersistenteMantiene el agente (un subproceso o servidor de larga duración) entre turnos — necesita una liberación explícita antes de que una terminal pueda tomar el control.Claude Code, OpenCode, Kilo

Para que la transferencia sea fluida, un adaptador puede implementar unos pocos métodos de clase opcionales (todos con valor por defecto de "no soportado", así un adaptador básico simplemente se excluye de la transferencia):

HookRol
scan_external_sessions()Descubre sesiones que este agente inició fuera de Terminull (lee sus transcripciones en disco) → aparecen en la barra lateral de la app.
is_cwd_externally_owned(cwd, sid)¿Hay un proceso nativo manejando actualmente este directorio/sesión? Controla la protección contra contención para que Terminull se detenga en vez de escribir por duplicado.
make_transcript_mirror(cwd, sid)Cola de solo lectura de la transcripción de una sesión propiedad de la terminal → se refleja en la app en vivo, sin escribir.
latest_sid_for_cwd(cwd)Vuelve a resolver la cabeza del linaje tras devolver el control desde una terminal (--resume puede generar un id nuevo).
terminal_resume_command(sid)Comando de shell que reanuda la sesión en una terminal → alimenta el botón "Continuar en terminal" de la app.

El flujo, de principio a fin: la app muestra sesiones de CLI externas vía scan_external_sessions(); tocar una vincula una sesión de Terminull a ella. Si la terminal es dueña del directorio, Terminull Service detiene la sesión (encolando mensajes y mostrando un estado de "observador desconectado") y transmite un reflejo de solo lectura en lugar de escribir. Cuando la terminal queda inactiva, Terminull Service se reconecta automáticamente — resolviendo de nuevo la cabeza del linaje y luego vaciando la cola. A la inversa, "Continuar en terminal" libera una sesión en vivo inactiva y la vuelve a detener como reflejo. El diseño completo, con la matriz de cobertura por adaptador, está en docs/session-handoff.md.

El núcleo de Terminull Service está pensado para ser abierto. La estructura del código fuente, la referencia completa de bloques y el diseño de transferencia viven junto a esto en el docs/ del repositorio. Preguntas: hello@terminull.dev.