← Вся документация

Написание собственного адаптера

Адаптер оборачивает CLI или сервер одного агента и преобразует его вывод в нормализованный поток блоков — потоковый текст, рассуждения, вызовы инструментов, карточки плана, diff'ы — так что в приложении все агенты выглядят одинаково. Один экземпляр адаптера соответствует одной интерактивной сессии с одним агентом. Встроенные адаптеры находятся в terminull_bridge/adapters/ (Claude Code, Codex, Cursor, Gemini, Kilo, OpenCode, Pi, Hermes, Droid); свой собственный можно добавить, не трогая ядро.

Контракт BaseAdapter

Унаследуйтесь от BaseAdapter и реализуйте четыре корутины. Контракт проверяется при импорте — если подкласс пропускает обязательное объявление, сразу возникает TypeError, поэтому адаптер не может незаметно унаследовать пустую заглушку.

МетодВозвращаетОтветственность
start(session_id)AgentInfoBlockЗапускает/подключает агента; возвращает его возможности через make_agent_info().
send(text, attachments=None)—Передаёт сообщение пользователя в работающую сессию.
stream()AsyncIterator[Block]Выдаёт нормализованные блоки по мере их производства агентом.
stop()—Корректно завершает сессию.

Каждый ход вашего stream() должен заканчиваться SessionEndBlock, а вызовы инструментов должны выдавать сначала running, а затем done/error ToolCallBlock (плюс парный ToolResultBlock). Две опциональные возможности требуют явного отказа — каждый флаг нужно установить в True или False:

ФлагЕсли True, переопределитьДля
provides_modelslist_models()Выбор модели при создании новой сессии.
provides_usageget_usage()Статистика стоимости/токенов для /status.

Минимальный адаптер

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() считывает ваши атрибуты cap_* и автоматически добавляет универсальные слэш-команды Terminull Service — стройте информационный блок через неё, а не описывайте возможности вручную. Ваш __init__ фильтруется по сигнатуре: Terminull Service предлагает cwd, model, binary, resume_session_id, on_session_id (вызовите его с собственным id сессии агента, чтобы перезапуск Terminull Service мог возобновить этот разговор) и effort — но передаёт только те параметры, что вы реально объявили, так что минимальный def __init__(self, cwd=None) тоже работает. Объявляйте то, что вам нужно.

Возможности

Объявляются как атрибуты класса; передаются в AgentInfoBlock, чтобы приложение показывало или скрывало соответствующий интерфейс.

АтрибутЭффект в приложении
cap_imagesВ composer включается выбор изображений.
cap_documentsПринимаются вложения файлов, не являющихся изображениями.
cap_thinkingБудут отображаться блоки рассуждений.
cap_permissionsБудут появляться карточки запроса разрешений.
cap_subagentsВызовы инструментов суб-агентов отображаются как вложенные группы.
cap_interruptКнопка остановки активна посреди хода.
cap_input_during_turnComposer остаётся активным во время генерации (иначе сообщения ставятся в очередь).
cap_terminalМожет открывать терминал на устройстве.

Обнаружение и установка без пересборки

Обнаружение реализуется в адаптере как classmethod detect() → DetectedAgent | None — один и тот же путь для встроенных и внешних адаптеров. Реализация detect() по умолчанию требует agent_id и, если задан cli_binary, проверяет через shutil.which() + --version. Переопределите её для проверок на основе HTTP или конфигурации.

Внешний адаптер — это просто модуль .py, помещённый в ~/.terminull/ext_adapters/, определяющий подкласс BaseAdapter как минимум с 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

Загрузчик импортирует каждый модуль с защитой — он выполняет ту же проверку контракта, отклоняет конфликты с встроенными id, логирует и пропускает всё некорректное, а валидные модули регистрирует. Дальше они обнаруживаются и создаются точно так же, как встроенные. Не нужно менять ядро, не нужно перезапускать ничего, кроме Terminull Service.

Где находится код. Встроенные адаптеры поставляются как редактируемый исходный код в terminull_bridge/adapters/*.py — даже в опубликованном, скомпилированном в байт-код Terminull Service эта папка остаётся обычным .py, так что вы можете читать их как рабочие примеры и скопировать один как отправную точку. Полный, готовый к копированию эталонный адаптер — документирующий каждый хук и полную сигнатуру __init__ выше — это examples/ext_adapter_example.py; скопируйте его в ~/.terminull/ext_adapters/ и отредактируйте. Контракт, который вы реализуете, — terminull_bridge/adapters/base.py.

Жизненный цикл при работе на двух поверхностях

На протяжении своего жизненного цикла сессией можно управлять с двух поверхностей — из нативного CLI агента в терминале и из Terminull. Правило простое: один автор, много наблюдателей — Terminull никогда не должен становиться вторым одновременным автором в разговоре, которым активно управляет терминал. То, как адаптер в этом участвует, зависит от того, как он удерживает агента между ходами:

ТипМежду ходамиПримеры
Для каждого хода / без удержанияЗапускает новый процесс на каждый ход в stream(), затем завершается — так что CLI свободен возобновить работу между ходами.Codex, Cursor, Gemini, Droid, Pi
ПостоянныйУдерживает агента (долгоживущий подпроцесс или сервер) между ходами — требует явного освобождения, прежде чем терминал сможет перехватить управление.Claude Code, OpenCode, Kilo

Чтобы передача управления проходила бесшовно, адаптер может реализовать несколько опциональных методов класса (по умолчанию все они означают «не поддерживается», так что базовый адаптер просто отказывается от передачи управления):

ХукРоль
scan_external_sessions()Находит сессии, которые этот агент запустил вне Terminull (читает его транскрипты на диске) → они появляются в боковой панели приложения.
is_cwd_externally_owned(cwd, sid)Управляет ли этой директорией/сессией сейчас нативный процесс? Задействует защиту от конфликта, чтобы Terminull приостанавливался вместо двойной записи.
make_transcript_mirror(cwd, sid)Чтение в реальном времени транскрипта сессии, которой владеет терминал, → зеркально отображается в приложении, без записи.
latest_sid_for_cwd(cwd)Заново определяет актуальную вершину цепочки после возврата управления из терминала (--resume может создать новый id).
terminal_resume_command(sid)Команда shell, возобновляющая сессию в терминале → лежит в основе кнопки приложения «Продолжить в терминале».

Весь процесс целиком: приложение показывает внешние CLI-сессии через scan_external_sessions(); нажатие на одну из них привязывает к ней сессию Terminull. Если директорией владеет терминал, Terminull Service приостанавливает сессию (ставя сообщения в очередь и показывая состояние «отсоединённый наблюдатель») и вместо записи стримит зеркало только для чтения. Когда терминал переходит в простой, Terminull Service подключается автоматически — заново определяя вершину цепочки, а затем разгружая очередь. В обратную сторону кнопка «Продолжить в терминале» освобождает простаивающую активную сессию и снова переводит её в режим зеркала. Весь дизайн, вместе с матрицей поддержки по адаптерам, описан в docs/session-handoff.md.

Ядро Terminull Service задумано открытым. Структура исходного кода, полный справочник блоков и дизайн передачи управления находятся рядом, в docs/ репозитория. Вопросы: hello@terminull.dev.