← Alle Docs

Einen eigenen Adapter schreiben

Ein Adapter kapselt eine Agenten-CLI oder einen Server und übersetzt dessen Ausgabe in einen normalisierten Block-Stream — gestreamter Text, Denkprozesse, Tool-Aufrufe, Plan-Karten, Diffs —, damit sich jeder Agent in der App gleich anfühlt. Eine Adapter-Instanz entspricht einer interaktiven Session mit einem Agenten. Eingebaute Adapter liegen in terminull_bridge/adapters/ (Claude Code, Codex, Cursor, Gemini, Kilo, OpenCode, Pi, Hermes, Droid); dein eigener lässt sich einbinden, ohne den Kern anzufassen.

Der BaseAdapter-Vertrag

Leite von BaseAdapter ab und implementiere vier Coroutinen. Der Vertrag wird beim Import erzwungen — eine Unterklasse, die eine erforderliche Deklaration auslässt, wirft sofort TypeError, sodass ein Adapter keine No-op-Implementierung stillschweigend erben kann.

MethodeRückgabeVerantwortung
start(session_id)AgentInfoBlockStartet/verbindet den Agenten; liefert seine Capabilities über make_agent_info().
send(text, attachments=None)—Speist eine Nutzernachricht in die laufende Session ein.
stream()AsyncIterator[Block]Liefert normalisierte Blöcke, sobald der Agent sie erzeugt.
stop()—Fährt die Session sauber herunter.

Jeder Turn deines stream() muss mit einem SessionEndBlock enden, und Tool-Aufrufe müssen einen running- gefolgt von einem done-/error-ToolCallBlock ausgeben (plus einem passenden ToolResultBlock). Zwei optionale Oberflächen sind explizites Opt-out — du musst jedes Flag auf True oder False setzen:

FlagWenn True, überschreibtFür
provides_modelslist_models()Der Modell-Picker für neue Sessions.
provides_usageget_usage()Kosten-/Token-Statistik für /status.

Ein minimaler Adapter

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() liest deine cap_*-Attribute und stellt automatisch die universellen Slash-Commands von Terminull Service voran — bau den Info-Block darüber, statt Capabilities selbst zusammenzustricken. Dein __init__ wird nach Signatur gefiltert: Terminull Service bietet cwd, model, binary, resume_session_id, on_session_id (ruf es mit der eigenen Session-ID deines Agenten auf, damit ein Neustart von Terminull Service dieses Gespräch fortsetzen kann) und effort an — übergibt aber nur die, die du tatsächlich deklarierst, sodass ein minimales def __init__(self, cwd=None) trotzdem funktioniert. Deklariere, was du brauchst.

Capabilities

Als Klassenattribute deklariert; im AgentInfoBlock sichtbar gemacht, damit die App die passende UI zeigt oder versteckt.

AttributEffekt in der App
cap_imagesBildauswahl im Composer aktiviert.
cap_documentsAnhänge außer Bildern werden akzeptiert.
cap_thinkingDenk-Blöcke werden gerendert.
cap_permissionsBerechtigungskarten erscheinen.
cap_subagentsSub-Agent-Tool-Aufrufe werden als verschachtelte Gruppen gerendert.
cap_interruptStopp-Button mitten im Turn aktiv.
cap_input_during_turnComposer bleibt während der Generierung aktiv (sonst werden Nachrichten in eine Warteschlange gestellt).
cap_terminalKann ein Terminal auf dem Gerät öffnen.

Erkennung & Drop-in-Installation

Erkennung sitzt am Adapter als classmethod detect() → DetectedAgent | None — derselbe Weg für eingebaute und externe Adapter. Das Standard-detect() erfordert agent_id und prüft, falls cli_binary gesetzt ist, per shutil.which() + --version. Überschreibe es für HTTP-/Config-basierte Prüfungen.

Ein externer Adapter ist einfach ein .py-Modul, das nach ~/.terminull/ext_adapters/ gelegt wird und eine BaseAdapter-Unterklasse mit mindestens agent_id + display_name definiert:

# ~/.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

Der Loader importiert jedes Modul abgesichert — er führt denselben Vertragscheck aus, weist Kollisionen mit eingebauten IDs ab, loggt und überspringt alles Fehlerhafte und registriert die gültigen. Sie werden dann genau wie eingebaute Adapter erkannt und instanziiert. Keine Kern-Änderung, kein Neustart von irgendwas außer Terminull Service.

Wo der Code liegt. Die eingebauten Adapter werden als editierbarer Quellcode unter terminull_bridge/adapters/*.py ausgeliefert — selbst im veröffentlichten, bytecode-kompilierten Terminull Service bleibt dieser Ordner reines .py, damit du sie als durchgearbeitete Beispiele lesen und einen als Ausgangspunkt kopieren kannst. Die vollständige, copy-paste-fertige Referenzimplementierung — die jeden Hook und die komplette obige __init__-Signatur dokumentiert — ist examples/ext_adapter_example.py; leg eine Kopie in ~/.terminull/ext_adapters/ und passe sie an. Der Vertrag, den du implementierst, ist terminull_bridge/adapters/base.py.

Lebenszyklus über einen Zwei-Oberflächen-Run

Eine Session kann über ihre Lebensdauer von zwei Oberflächen gesteuert werden — der nativen Agenten-CLI im Terminal und Terminull. Die Regel lautet ein Schreiber, viele Beobachter: Terminull darf nie zu einem zweiten gleichzeitigen Schreiber in einem Gespräch werden, das das Terminal gerade aktiv steuert. Wie ein Adapter dabei mitspielt, hängt davon ab, wie er den Agenten zwischen den Turns hält:

ArtZwischen TurnsBeispiele
Pro Turn / ohne LeaseStartet in stream() jeden Turn einen frischen Prozess und beendet sich dann — sodass die CLI dazwischen frei ist, fortzusetzen.Codex, Cursor, Gemini, Droid, Pi
PersistentHält den Agenten (einen langlebigen Subprozess oder Server) über Turns hinweg — braucht eine explizite Freigabe, bevor ein Terminal übernehmen kann.Claude Code, OpenCode, Kilo

Für eine nahtlose Übergabe kann ein Adapter ein paar optionale Klassenmethoden implementieren (alle standardmäßig „nicht unterstützt“, sodass ein einfacher Adapter die Übergabe einfach abwählt):

HookRolle
scan_external_sessions()Entdeckt Sessions, die dieser Agent außerhalb von Terminull gestartet hat (liest seine Transkripte auf der Platte) → sie erscheinen in der Seitenleiste der App.
is_cwd_externally_owned(cwd, sid)Steuert gerade ein nativer Prozess dieses Verzeichnis/diese Session? Steuert den Kontentionsschutz, sodass Terminull pausiert statt doppelt zu schreiben.
make_transcript_mirror(cwd, sid)Nur-Lese-Ende des Transkripts einer vom Terminal gehaltenen Session → live in die App gespiegelt, ohne zu schreiben.
latest_sid_for_cwd(cwd)Löst den Lineage-Kopf nach einer Terminal-Rückgabe neu auf (--resume kann eine neue ID prägen).
terminal_resume_command(sid)Shell-Befehl, der die Session in einem Terminal fortsetzt → treibt den „Im Terminal fortsetzen“-Button der App an.

Der Ablauf, Ende zu Ende: Die App zeigt externe CLI-Sessions über scan_external_sessions(); tippst du eine an, wird eine Terminull-Session daran gebunden. Gehört das Verzeichnis dem Terminal, pausiert Terminull Service die Session (stellt Nachrichten in eine Warteschlange und zeigt einen „Losgelöster Beobachter“-Zustand) und streamt stattdessen eine nur lesbare Spiegelung, statt zu schreiben. Wird das Terminal untätig, hängt sich Terminull Service automatisch wieder an — löst den Lineage-Kopf neu auf und leert dann die Warteschlange. Umgekehrt gibt „Im Terminal fortsetzen“ eine aktive Live-Session frei und pausiert sie wieder als Spiegelung. Das gesamte Design, mit der Coverage-Matrix pro Adapter, steht in docs/session-handoff.md.

Der Kern von Terminull Service soll offen sein. Quell-Layout, die vollständige Block-Referenz und das Übergabe-Design liegen daneben im docs/-Ordner des Repos. Fragen: hello@terminull.dev.