Перейти к содержимому
ColliePWA

09/Documentation

SDK для durable (экспериментально)

npm-пакет @colliepwa/durable-sdk по частям: точки входа и среды выполнения, openCollie, методы панели, четыре вида вопросов и requireApproval, хуки, история, caps, вспомогательные функции, интеграция с Pi и версии

Экспериментальная. Пакет, его API и сетевой протокол могут измениться, либо функциональность может быть удалена в следующем минорном релизе. Версии поясняет, что это означает для вашего durable agent.

@colliepwa/durable-sdk — это npm-пакет, с помощью которого durable agent отображает себя на вашем телефоне через Collie. На этой странице перечислены все его части. В Durable agents приведено краткое руководство и настройка на стороне Collie, а в Протокол durable wire объясняется, что передаётся между ними.

Установка и точки входа

Одна установка даёт пять точек входа: корневой импорт для большинства durable agents и четыре для более специфических задач.

npm install @colliepwa/durable-sdk@experimental
# or
bun add @colliepwa/durable-sdk@experimental
импортчто содержит
@colliepwa/durable-sdkopenCollie и весь API. На Bun сокет обслуживается сервером Bun, в остальных случаях — сервером node:http
@colliepwa/durable-sdk/bunТот же API, всегда с сервером Bun
@colliepwa/durable-sdk/nodeТот же API, всегда с сервером node:http, плюс NO_DESCENDANT_CHECK, строка, которую он выводит при запуске
@colliepwa/durable-sdk/coreИнтерфейс и сетевой протокол в виде одной функции, handle(request), для durable-агента со своим транспортом. Без импорта рантайма, поэтому работает везде, где работает веб-Request
@colliepwa/durable-sdk/piСвязующий код для durable-агента на базе Pi Durable (Вспомогательные функции Pi)

Пакету требуется Node 22.19 или новее, либо Bun 1.4 или новее, и у него нет runtime-зависимостей. Для ./pi нужны @earendil-works/pi-durable, @earendil-works/pi-ai и @earendil-works/chord версий от 1.1 до 2. Точка входа протестирована с Pi Durable 1.1.0 и 1.2.0. Эти три пакета являются опциональными peer-зависимостями: durable-агент, который не импортирует ./pi, не устанавливает ни одной из них.

Рантайм определяет, может ли durable-агент распознать автора ответа. Проверка потомков отклоняет действие от самого durable-агента или запущенного им процесса с кодом 403 caller-refused. Она распространяется на answer, send, stop, action, rename, close, reopen и starter. Чтение остаётся доступным.

рантаймпроверка потомковчто вы задали
Bun на LinuxВключено по умолчанию. Bun считывает идентификатор процесса вызывающей стороны из сокетаНичего. callerCheck: "off" отключает её
Bun на macOS или любой другой системе, кроме LinuxНе может работать. Запуск завершается ошибкой с указанием callerCheckcallerCheck: "off"
Node, любая системаНе может работать: Node не может прочитать вызывающую сторону сокета. Durable-агент сообщает об этом в одной строке при запускеНичего. callerCheck не считывается

Без этой проверки любой процесс вашего пользователя может ответить на карточку, включая запущенный агентом инструмент. Запускайте инструменты агента в песочнице, у которой нет доступа к сокету. Для сокета требуется система с владельцами файлов, поэтому на Windows openCollie завершает работу при старте. Вместо этого durable-агент там раздаёт ./core через TLS с токеном (Два способа связи с durable agent).

Открыть соединение

openCollie создаёт приватный сокет, а возвращённый дескриптор задаёт разметку для отображения на телефоне.

import { openCollie } from "@colliepwa/durable-sdk";

const collie = await openCollie({ hostName: "desk" });
const space = collie.workspace("support", { title: "Support" });
const tab = space.tab("main", { title: "Tickets" });
const pane = tab.pane("ticket-1", { title: "Maria Lopez", harness: "desk" });
collie.ready();

Если опция socket не задана, путь берётся из COLLIE_DURABLE_SOCKET — переменной, которую также читает Collie.

опциядействиепо умолчанию
socketПуть к сокету: абсолютный, не более 103 байт. Его каталог создаётся с правами 0700 или уже должен принадлежать вам с правами 0700Переменная COLLIE_DURABLE_SOCKET. Если не задана ни одна, запуск завершается ошибкой
hostNameСобственное имя durable-агента в его приветствии, усечённое до 64 байт. ОбязательноНет
tailLimitСколько необработанных событий хранит каждая панель для чтения Collie500, максимум 1000
capsЗаявленные возможности durable-агента (Возможности)Всё, на что способен SDK
callerCheckdescendant или off. Считывается только на Bundescendant
hooksОдно именованное место для каждого действия, отправляемого телефоном (Хуки и история)Нет
warnКуда SDK записывает свои однострочные примечания, например о Collie, использующем другую версию протоколаconsole.warn
onRenameУстарело. Старое имя для hooks.onRename, оставленное на один релиз, без id запросаНет

Ещё два параметра существуют только в createCore, но не в openCollie: receipts и allowAlways (Вопросы).

на дескрипторечто это такое
addressПуть к сокету, который он слушает
epochНовый и случайный при каждом запуске, 128 бит
coreЯдро за сокетом. Тест или второй транспорт вызывает core.handle напрямую
workspace(id, { title, cwd? })Рабочее пространство, создаваемое один раз на id. Его tab(id, { title }) даёт вкладку, а pane(id, options) вкладки — панель
starters(list)Что телефон может запустить со страницы New, не более 50. Каждое значение — { id, label, run }, и run разрешается в { paneId } показанной им панели
history(provider)Отдавать собственную запись бесед долговечного агента (Хуки и история)
ready()Вызывайте один раз, когда показаны все панели, и после перезапуска, когда все открытые ask снова подняты. До этого момента каждый маршрут, кроме hello, отвечает 503 starting
close()Остановить сервер. Ответы, которые уже отправляются, всё ещё уходят (максимум 2 с), затем файл сокета удаляется

close() намеренно оставляет открытые ask открытыми: остановка не является ответом, и долговечный агент сам решает, что происходит с ожидающей их работой.

tab.pane(id, { title, harness, cwd?, sessionId? }) возвращает панель с этим id, если она существует, чтобы долговечный агент мог показать беседу дважды без создания второй панели. Заголовки усекаются до 256 байт, а harness — до 64. cwd должен быть абсолютным путём, иначе он опускается.

Каждый id следует правилу для id: от 1 до 128 байт, без управляющих символов, без пробелов и без /, ? или #. Недопустимый id возвращает TypeError. Id рабочего пространства или вкладки не может содержать :, так как в протоколе вкладка называется <workspace>:<tab>.

Панели

Панель — это одна беседа, и её методы определяют, что телефон отображает для неё.

методчто отображает телефонлимит
state(s)working, waiting или idle. Изменение working начинает новый шагПока открыт любой ask, состояние панели — waiting
event(e)Элементы чата: события item, remove, settled, reset и syncedСм. таблицу элементов ниже
subtitle(text)Одна строка под заголовком. null очищает её120 символов
badge(value){ count } или { text } рядом с заголовком. null очищает егоЧисло от 0 до 9999, текст из 12 символов
progress(value){ kind: "steps", steps } или { kind: "percent", value, label? }, отображается, пока панель работает или ожидает. null очищает его12 шагов, каждая метка до 60 символов, значение от 0 до 100
actions(list)Кнопки в секции belt долгоживущего агента: { id, label, icon?, busy?, disabled? }. null очищает их4 кнопки, метка до 16 символов
onAction(fn)Выполняет нажатие на одну из этих кнопок. Верните false, чтобы отклонить егоТелефон не отправляет нажатие для кнопки busy или disabled
onSend(fn)Принимает текст с телефона в виде fn(text, { steer, requestId })32 КиБ текста
onAbort(fn)Принимает Stop в виде fn({ requestId })Нет
ask(input, { signal? })Карточка вопроса (Вопросы)50 открытых запросов ask на панель
updateAsk, decideAsk, outcomeOfИзменить, решить или прочитать запрос ask (Вопросы)Нет
close()Убирает панель из макета. Её открытые запросы ask закрываются со статусом stoppedНет

У панели также есть четыре поля только для чтения: id, sessionId, turnId (текущий шаг выполнения или последний) и openAsks. Поле icon действия принимает одно из значений: play, pause, check, x, refresh, archive, flag, star, send, note, search и more. При другом имени значок не отображается.

Событие item содержит один элемент с id и необязательным ts. Элемент с известным id заменяет предыдущий на месте.

типчто содержитлимит
userСтрока, отправленная пользователем. requestId указывает отправку, из которой она получена, поэтому телефон заменяет свою строку на неё64 КиБ текста
reply, thinking, noticeСлова агента64 КиБ текста
markdownТекст, который телефон отображает как markdown: заголовки, списки, выделение, фрагменты кода и ссылки32 КиБ текста
codeБлок кода с lang, например ts32 КиБ текста. lang содержит от 1 до 24 букв, цифр или _ + # . -
linkhref и необязательный labelhref содержит только https://, не более 2048 символов. label не более 120 символов
toolВызов инструмента: tool: { kind: "other", name, summary, output? } и status со значением running, done, failed или deniedname 128 байт, summary 1 КиБ, output 16 КиБ

Элемент reply, thinking, notice или markdown может содержать partial: true, пока его текст увеличивается. Передавайте тот же id по мере роста текста не чаще чем каждые 250 мс, затем ещё один раз с итоговым текстом и без partial, до того как панель покинет working.

Текст сверх лимита обрезается, а управляющие и невидимые символы удаляются. Некорректный lang, некорректный href, некорректный badge или некорректный progress заменяются на TypeError или RangeError, так как Collie отбросил бы их. Долгоживущий агент не может сам добавлять события ask, ask-closed или state: их записывают ask() и state().

Вопросы

pane.ask показывает карточку и возвращает ответ, а requireApproval запрашивает подтверждение для одного вызова инструмента в одну строку.

import { requireApproval } from "@colliepwa/durable-sdk";

const args = { order: "5521", amount: 40 };
const approval = await requireApproval("refund", args, {
  pane,
  toolCallId: "call-1",
  timeoutMs: 10 * 60_000,
});
if (!approval.approved) throw new Error(`refund not approved: ${approval.decidedBy}`);

requireApproval(toolName, args, options) формирует карточку на основе самого вызова, а не на основе слов модели о нём. Заголовок — «refund wants to run», строка инструмента — refund(order: "5521", amount: 40), в push-уведомлении указано «refund needs a yes», а кнопки — Allow и Deny. Возвращает { approved, decidedBy, decidedAt, askId }. Отклонение, тайм-аут, остановка и прерывание возвращают approved: false.

параметр requireApprovalдействиепо умолчанию
paneПанель, которая запрашивает. ОбязательноНет
toolCallIdСобственный id вызова инструмента, сохраняющийся при перезапуске. Любая строка: значение, нарушающее правила для id, экранируетсяОбязательно
itemIdЭлемент, под которым находится карточкаId вызова инструмента
timeoutMsСколько времени есть у телефона. По истечении вызов отклоняется с решением от timeoutБез ограничений
signalAbortSignal. Прерывание отклоняет вызов с решением от programНет
labelsТекст на двух кнопках, { allow?, deny? }Allow и Deny

Используются две части, callSummary(name, args) и argumentsView(args): вызов в одну строку и аргументы в виде пар key: value, где каждое значение представлено в JSON.

pane.ask(input) принимает один из четырёх видов. Промис разрешается ответом в форме этого вида.

kindчто показывает карточкаответлимит
permissionПо одной кнопке на вариант, каждая с effect: allow, allow-always, deny или cancel{ optionId, note? }От 1 до 6 вариантов
choiceПо одной кнопке на вариант. При multiple: true пользователь выбирает несколько{ optionIds }От 1 до 8 вариантов
textОдно текстовое поле с опциональным placeholder{ text }maxLength от 1 до 2000, по умолчанию 500. Плейсхолдер — 60 символов
formОт 1 до 6 полей, каждое вида text, choice или toggle, некоторые required{ values } с ключами по id полейПоле выбора содержит от 1 до 8 вариантов

Каждый вид принимает toolCallId, itemId и title (256 байт). Может принимать reason (2 КиБ), tool, push: { title?, line? } (60 и 120 символов, текст уведомления) и expiresAt (Date или строка ISO 8601 с часовым поясом).

Опустите id. SDK вычисляет его через askIdFor(paneId, toolCallId): SHA-256 от двух id, первые 32 шестнадцатеричных символа. Тот же вызов инструмента, повторённый после перезапуска, получает тот же id, и ответ, отправленный телефоном во время перезапуска, будет доставлен. Повторный вызов с уже открытым id возвращает TypeError.

на панелидействие
updateAsk(askId, patch)Изменяет открытый запрос на месте: title, reason, tool, options, placeholder, maxLength или fields. Номер ревизии увеличивается на единицу, поэтому ответ на старую форму получает 409 stale, и телефон просит нажать снова. False, если запрос не открыт
decideAsk(askId, answer?)Durable-агент разрешил запрос сам, вне телефона. При answer ожидающий ask() разрешается этим значением. Без него он отклоняется с AskClosed. Позднее нажатие на телефоне получает 409 decided, а карточка сообщает: «Decided in the durable agent». False, если запрос не открыт
outcomeOf(askId)Первое решение: { decidedBy, decidedAt, optionId?, effect? }. Панель помнит свои последние 500. Undefined для запроса, который она не создавала или забыла

decidedBy имеет значение phone, program или timeout. Запрос преодолевает свой expiresAt и закрывается с решением от timeout: ask() отклоняется с AskClosed, у которого decidedBy равен timeout. Карточка показывает оставшееся до этого момента время. Запрос, завершившийся по таймауту или разрешённый в durable-агенте, оставляет одну строку в Chat, например: «Timed out before an answer: refund wants to run».

Прерванный signal, pane.close() и закрытие с телефона закрывают запрос с решением от program, а ask() отклоняется с AskClosed.

Внимание. Вариант allow-always пропускает все последующие вызовы после одного ответа. По https токен служит единственной защитой, поэтому durable agent там предлагает только Allow и Deny. createCore с allowAlways: false превращает вариант allow-always в TypeError. Телефон не предлагает allow-always для запроса, слова которого были обрезаны или очищены, либо в котором не указан инструмент.

Каждое действие с телефона содержит request id, и ядро сохраняет квитанцию: повторный вызов ничего не запускает и возвращает первый ответ с replayed: true. Стандартный MemoryReceiptStore хранит самые свежие RECEIPTS_PER_GROUP (1000) для каждой панели, а также для каждой группы для стартеров, переименований, закрытий и повторных открытий. Повтор сброшенного id возвращает 500 receipt-dropped и ничего не запускает. Хранилище сбрасывается вместе с процессом.

Чтобы сохранять квитанции между перезапусками, напишите ReceiptStore с get(requestId) и put(requestId, record, group) и передайте его в createCore как receipts. openCollie всегда хранит их в памяти.

Хуки и история

Хук — это единая точка для действия, отправленного телефоном, а history provider отдаёт вашу собственную историю.

хуккогда выполняетсякак отклоняет
onSend(pane, { text, steer, turnId, requestId })Текст для панели без pane.onSendИсключение: 500 internal, неизвестный результат для телефона
onAbort(pane, { turnId, requestId })Остановка для панели без pane.onAbortИсключение, как указано выше
onAction(pane, { actionId, turnId?, requestId })Нажатие пояса на панели без pane.onActionВозврат false: 422 action-refused
onRename({ kind, id, title }, requestId)Новый заголовок для рабочей области или вкладкиВозврат false: 422 rename-refused
onClose({ kind, id, paneIds }, requestId)Телефон закрывает панель, вкладку или рабочую областьВозврат false: 422 close-refused
onReopen({ id, paneId? }, requestId)Телефон повторно открывает закрытую беседуВозврат false: 422 reopen-refused

Хук выполняется, только если включён соответствующий cap и его request id ещё не встречался. Отказ ничего не сохраняет, поэтому тот же request id может повторить попытку. Обработчики уровня панели имеют приоритет над хуком. Панель без того и другого отвечает 409 not-supported на отправку, остановку или нажатие.

SDK применяет принятое действие самостоятельно. Переименование задаёт заголовок. Закрытие удаляет цель со всем содержимым и закрывает их открытые запросы как остановленные. Повторное открытие без хука восстанавливает панель, закрытую этим процессом; для беседы, закрытой до перезапуска, требуется onReopen. Разметка создаётся заново при каждом запуске, поэтому durable agent, которому нужно сохранить заголовок или факт закрытия после завершения процесса, сохраняет это в своём хуке.

collie.history(provider) отдаёт все беседы, открытые и закрытые, из собственного хранилища durable agent. Включает cap history, если caps опущен или указывает на history: true.

HistoryProviderвозвращает
list(after, limit){ items, next? }: строки бесед, сначала новые, не более limit (100)
items(id, after, limit){ items, next? }: элементы беседы, сначала старые, каждый в последней версии, не более limit (1000). undefined для неизвестного id

Строка представляет собой { id, title, subtitle?, workspaceId?, tabId?, startedAt, endedAt?, state, items }, где state имеет значение open или closed, а items указывает количество. Курсор (after, next) — это ваша собственная строка от 1 до 512 байт печатных символов ASCII без пробелов. Отсутствие next завершает список.

Когда провайдер задан, sessionId каждой панели совпадает с её pane id, и история отдаёт под ним беседу панели. Передайте sessionId в tab.pane(), чтобы указать другой id, или null, если он не может оставаться неизменным между перезапусками.

tailLimit — это не история. Это необработанный хвост событий, который каждая панель хранит для чтения событий в Collie, по умолчанию 500 событий. Читатель, отставший от него, получает вместо этого весь текущий снимок панели целиком, не более 2000 самых свежих элементов.

Возможности

Cap указывает, что durable agent умеет выполнять одно действие, а телефон отображает элемент управления только для cap, объявленного durable agent.

ограничениечто принимает durable agent
asksЗадаёт вопросы и принимает ответы
startersВыводит список стартеров и запускает один со страницы New
sendПринимает текст с телефона
steerПринимает текст в текущий ход. Требуется send
abortОстанавливает текущий ход
renameПринимает новый заголовок для рабочей области или вкладки
closeЗакрывает панель, вкладку или рабочую область
actionsВыводит список кнопок панели инструментов и запускает одну из них
historyОтдаёт свои диалоги, открытые и закрытые
reopenСнова показывает закрытый диалог

Если параметр опущен, caps включает всё, что умеет SDK, а также history, как только у history() появится провайдер. Если указан, он задаёт весь список целиком: неназванное право отключается, поэтому { asks: true } объявляет только запросы. Чтобы отключить одно право и оставить остальные, разверните ALL_CAPS: { ...ALL_CAPS, steer: false }. Маршрут с отключённым правом возвращает 409 not-supported. Значение terminal всегда равно false.

Вспомогательные функции и константы

Корневой импорт также экспортирует части, из которых состоит openCollie, и применяемые им правила.

exportчто это такое
createCore(options)Ядро без сервера. Принимает параметры openCollie, кроме socket и callerCheck, а также receipts и allowAlways. Возвращает epoch, workspace, starters, history, ready, isReady и handle(request)
handleСетевой протокол в виде одной функции: веб-Request на входе и Response на выходе. Обычная точка входа: core.handle(request)
wireListener(core)core.handle в виде слушателя запросов node:http, с drained() для корректного закрытия
hello, versionLineОтвет на приветствие и одна строка, которую долгоживущий агент записывает при отказе из-за версии
mintId()Новый случайный идентификатор: 128 бит в виде 32 шестнадцатеричных символов
askIdFor(paneId, toolCallId)Производный идентификатор запроса
AskClosedОшибка, с которой отклоняется ask(), если ответ не пришёл. Содержит by и decidedBy
idOk(id), assertId(id, what), safeId(raw)Правило для идентификаторов: проверка, проверка с генерацией исключения и безопасный идентификатор из любой строки, одинаковый каждый раз
cursorOk(cursor)Правило для курсора истории
cut(text, max, multiline?)Очистите строку и обрежьте её до max байт с … на конце
hrefOf, langOf, badgeOf, progressOfЦель ссылки, язык кода, значок и прогресс, проверенные так же, как их проверяет панель
subtitleOf, pushTextOf, renameTitleOfПодзаголовок, ключевые слова отправки запроса и заголовок переименования, очищенные и обрезанные
CAPS, LIMITSОграничения на каждое поле в байтах и ограничения на каждый список
ACTION_LABEL_MAX, BADGE_TEXT_MAX, BADGE_COUNT_MAX16, 12 и 9999
SUBTITLE_MAX, LINK_LABEL_MAX, RENAME_TITLE_MAXПо 120 символов каждый
PROGRESS_STEPS_MAX, PROGRESS_LABEL_MAX12 шагов, 60 символов
PUSH_TITLE_MAX, PUSH_LINE_MAX, PLACEHOLDER_MAX60, 120 и 60 символов
ANSWER_TEXT_MAX, ANSWER_TEXT_DEFAULT, ANSWER_TEXT_BYTES2000 и 500 символов, а также 8000 байт
ALL_CAPS, PROTOCOL_RANGE, SOCKET_ENVВсе возможности протокола, версии сетевого протокола, поддерживаемые SDK, и имя COLLIE_DURABLE_SOCKET
MemoryReceiptStore, RECEIPTS_PER_GROUPХранилище квитанций по умолчанию и его лимит в 1000 на группу

Каждый тип сетевого протокола и полезная нагрузка каждого хука также экспортируются из src/core/types.ts.

Вспомогательные функции Pi

./pi превращает диалог Pi Durable в панель, вызов рискованного инструмента — в карточку, а хранилище Pi — в историю.

exportдействие
showConversation(loc, harness, conversation, o)Отображает диалог в виде панели: сначала историю, затем события в реальном времени. Send отправляется в submit у Pi с идентификатором запроса, а Stop — в его abort. Безопасно вызывать дважды
PaneRegistryКакая панель отображает какой диалог. После перезапуска reRaised(risky) ждёт, пока каждый рискованный вызов без результата не будет запрошен снова, поэтому ready() идёт после запросов
riskGuard({ panes, risky, words?, timeoutMs?, onTimeout? })Расширение Pi, чей хук beforeTool превращает вызов инструмента из risky в карточку с вариантами Allow и Deny. Отклонённый вызов блокируется. При timeoutMs срок действия карточки истекает, оставшийся без ответа вызов блокируется, а onTimeout(tool, title) позволяет durable-агенту записать это в лог
askTools({ panes, words, timeoutMs?, onTimeout? })Два инструмента службы поддержки для запросов: ask_queue (выбор варианта) и ask_customer (текстовый запрос). При timeoutMs запрос без ответа истекает по таймауту, и инструмент возвращает "timed out"
spawnHelperTool(o)Инструмент spawn_helper: дочерний диалог, отображаемый как новая панель, которая возвращает ответ помощника
piHistory({ harness, panes, describe })HistoryProvider, который читает каждую запись каждого диалога Pi
firstMessageOf(conversation)Текст первого сообщения диалога для присвоения имени его панели
paneIdOf(id), conversationOfPaneId(paneId)Идентификатор панели диалога Pi, conv-<id>, и обратно
summaryOf(args, name?)Аргументы вызова инструмента словами, не более 200 символов
MapperЗаписи Pi и события агента в виде элементов. Увеличивающийся ответ сохраняет один идентификатор элемента
PARTIAL_MS, RE_RAISE_MS250 мс между двумя записями увеличивающегося ответа и максимум 5000 мс, которые ждёт reRaised
ESCALATEDПример словаря: уведомление службы поддержки для кнопки Escalate, которую showConversation добавляет с помощью escalate: true
ASKING_TOOLSПример словаря: имена двух инструментов службы поддержки для запросов
Submit, ReplyExtrasТипы пользовательского submit и элементов, которые durable-агент добавляет под готовым ответом

Текст на карточке передаётся через параметры, поэтому ./pi не содержит текста из примера, кроме этих двух констант. Он написан для Pi Durable 1.1.0. Служба поддержки в examples/durable-member/ объединяет все части воедино.

Версии

Пакет имеет собственную версию, независимую от версии Collie, и указывает версию сетевого протокола, с которой он работает.

сегоднячто оно означает
SDK_VERSION0.1.0Версия пакета. Пока пакет находится под dist-tag experimental, минорный релиз может изменить API
WIRE_VERSION1Версия сетевого протокола durable, с которой работает пакет

Collie работает с одной версией сетевого протокола и принимает предыдущую в течение одного минорного релиза после выхода новой. Если Collie не поддерживает ни одну из ваших версий, ядро выводит одну строку через warn, по разу на версию, например collie: Collie speaks wire v2, this durable agent speaks v1. Update the agent's SDK, or Collie. Телефон сообщает о несоответствии на главной странице durable-агента словами самого Collie. Обновите более старую сторону.

Пакет публикуется под npm dist-tag experimental, но никогда под latest. Если сетевой протокол durable будет удалён в выход, пакет объявляется устаревшим (ADR 0105).

Редактировать эту страницу на GitHub

The rest of the documentation