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

Провайдеры агента и мост провайдера

Через провайдер агента плагин владеет агентом. Провайдер состоит из двух частей в одном артефакте: декларации на сервере и моста на машине. Ещё здесь описаны диалекты ACP и режим записи моста.

ЧастьИмпортГлавные имена
декларация (сервер)@get-bb/plugin-sdkbb.providers.register(declaration) → { dispose() } · PluginProviderDeclaration
мост (хост)@get-bb/plugin-sdk/provider-bridgeexperimental_defineProviderBridge · PROVIDER_BRIDGE_EXPORT_NAME · BRIDGE_REQUEST_METHODS · BRIDGE_JSON_RPC_ERRORS · PROVIDER_BRIDGE_PROTOCOL_VERSION · THREAD_DELTA_GRAMMAR_V2/V3 · runBridgeRequest · createBridgeIo · addTokenUsage
набор ACP…/provider-bridge/acpexperimental_acpProviderBridge · experimental_acpLaunchSpecSchema · experimental_probeAcpAgent
соответствие…/provider-bridge/testingexperimental_runBridgeConformance · experimental_captureBridgeJsonRpcOutput · experimental_createDeltaAssembler · experimental_replayRecording · experimental_compareParity
// examples/plugins/echo-provider/src/provider-bridge.ts (abridged) — a minimal bridge
const handlers: Record<string, RequestHandler> = {
[BRIDGE_REQUEST_METHODS.initialize]: (id, params) => {
io.sendResult(id, {
protocolVersion: PROVIDER_BRIDGE_PROTOCOL_VERSION, // exactly 2
capabilities: {
grammarVersions: [THREAD_DELTA_GRAMMAR_V3, THREAD_DELTA_GRAMMAR_V3],
sessionRestore: true, threadArchive: false, threadRename: false,
threadGoalClear: false, fork: "none",
approvalEnforcedBy: "runtime", steerMode: "queue",
},
});
},
[BRIDGE_REQUEST_METHODS.threadStart]: (id, params) => { /* … io.sendResult(id,
{ providerThreadId, sessionRestorable: true }) … */ },
};
export const experimental_providerBridge = experimental_defineProviderBridge({ handleLine, start });

Транспорт. JSON-RPC 2.0. Работает в обе стороны через stdin/stdout процесса моста, сообщения разделяются переносом строки. Запросы и ответы различаются по наличию поля method, «never by the shape of the result». Пространства идентификаторов у этих направлений независимы. Вывод в stdout вне протокола игнорируется; мост обязан защищать свой stdout.

Грамматика. Ассемблер говорит только на v3 ([3, 3]). Мост, чей диапазон не включает 3 (даже если написан до появления этого поля), отклоняется при запуске с понятной ошибкой. Факт рукопожатия может только сузить то, что объявила декларация, но не расширить это.

Кто чем владеет. Сервер генерирует threadId, провайдер — providerThreadId, а ассемблер в демоне — идентификаторы ходов и элементов. «The bridge performs no id translation.»

Минимальный корректный ход. input.accepted { clientRequestId } → turn.open → дельты элементов → turn.boundary { status }. Даже промпт, который ничего не делает, обязан выдать пару open+boundary: «zero-delta acceptance is hung-thread class #1431». После thread/stop мост ничего не удерживает: все долги, и прежде всего терминальная граница прерванного хода, должны уйти в канал до ответа.

recovery. Строгая типизация, ошибки не сопоставляются по тексту: sessionArchived, authRequired, restartRecommended, staleTurn, rateLimited. Пэйлоад один, носителя два: подсказка об отклонённом запросе уходит в error.data.recovery, а незапрошенная — как уведомление provider/recovery. «Never send both for one event.»

Ловушки. Держите SDK в dependencies (задокументированное исключение); приватные пакеты @bb/* запрещены. Мост не должен запускать себя сам: bootstrap демона владеет argv, dataDir/tempDir, фреймингом stdin и сигналами. Не генерируйте providerThreadId из счётчика процесса. Если мост порождает дочерние процессы, он обязан вызывать sanitizeInheritedChildProcessEnv.

Кто поставляет. provider-claude-code, provider-codex («the largest working example»), provider-pi, provider-acp и examples/echo-provider («the smallest»).

Справка: docs/provider-bridge-protocol.md · docs/provider-plugin-api.md.

Пять зарегистрированных диалектов, пять агентов из поставки bb.

id провайдераdisplayNamelaunch spec
acp-cursorCursorcursor-agent acp
acp-opencodeopencodeopencode acp
acp-ompompomp acp
acp-grokGrok Buildgrok agent stdio
acp-hermes-agentHermes Agenthermes acp

В коде есть диалекты acp (универсальный no-op), grok, cursor, omp и opencode. Их пять, хотя документация описывает три. Код считается истиной, но расхождение не проверено по changelog.

Диалект выбирается в два этапа: побеждает явный providerOptions.acpDialect; иначе basename(launch.command) сопоставляется с DIALECT_IDS_BY_COMMAND, а всё неизвестное откатывается к универсальному диалекту. Пользовательские агенты берутся из настройки customAgents, получают префикс acp- и глиф "Toolbox". Все регистрации ACP используют общую family: "acp". Реестр диалектов намеренно скрыт: «no plugin has needed it, and its shape — process-global, unversioned hooks — is still open».

Диагностика провайдера без догадок.

BB_PROVIDER_BRIDGE_RECORD_DIR=<dir>
// every bridge process mirrors BOTH boundaries into
// <dir>/<providerId>/<threadId>/<direction>.ndjson — four directions,
// one line of { ts, run, seq, dir, line } each, with seq a single counter across all tracks.
// A bridge that spawns a CLI calls experimental_recordProviderChildIo(child, { threadId })
// right after spawn(); one whose pipe belongs to the SDK checks
// experimental_isProviderBridgeRecording() and takes the spawn over itself.
// "A recording is never overwritten": a deliberate bridge change writes
// bridge→runtime.current.ndjson alongside it.