Third-party canary: echo-provider exercises every provider-plugin capability - #2189
Third-party canary: echo-provider exercises every provider-plugin capability#2189SawyerHood wants to merge 3 commits into
Conversation
…public SDK The designated third-party canary only emitted basic shapes. It now registers every declaration field (strings, labelled reasoning levels and service tiers, fallback models, env passthrough, deriveProviderOptions reading a plugin setting, two extension kinds), ships a bb tool with experimental_presentation, and its bridge emits the whole grammar v3 stream: presentation on every item, command, fileRead, search, a delegation with a parentRef-linked child turn, planSteps, a suppressed tool, the bb tool over item/tool/call stamped server:"bb", an extension item and extension state, the providerOptions/env round trip, usage. Tests use only @get-bb/plugin-sdk/provider-bridge/testing: the twelve conformance scenarios, the assembled v3 stream, and a guard that fails on any @bb/* import in the package. Co-Authored-By: Claude <noreply@anthropic.com>
…ler and ingest Installs examples/plugins/echo-provider from its checkout path, builds the real thread.start command (settings-derived providerOptions, the bb tool with resolved presentation, the artifact bridgeLaunch), runs it on the real agent runtime, posts every event through /internal/session/events and answers the tool call through /internal/session/tool-call, then asserts the persisted rows: presentation on every item, the validated extension item and state, the parentToolCallId-linked child turn, planSteps, the server:"bb" tool row, and a malformed extension payload replaced by provider/unhandled. @bb/agent-runtime joins the server's devDependencies for this test only. Co-Authored-By: Claude <noreply@anthropic.com>
…blic testing kit The README lists each registration and bridge capability with where it lives and how it is proven, and states the only-public-SDK rule. The plugin-authoring skill and the protocol doc stop pointing at the private @bb/provider-bridge-protocol/conformance package. Co-Authored-By: Claude <noreply@anthropic.com>
|
Coordinator review — APPROVE as the canary; findings routed. (Draft is fine to keep until the G-A/G-C fixes have PRs; this PR's job was to find them.) The third-party path is proven. I verified: zero The three API gaps are real and are the most valuable output here:
The four observations (extension items don't render; labels ignore Rebasing onto
|
What was wrong
The provider-plugin migration's core principle is zero first-party privilege: a marketplace plugin must be able to do everything codex and claude do, using only the public SDK. The smoke test proved the first-party plugins; the designated third-party canary,
examples/plugins/echo-provider, did not. It emitted a bareagentMessagestream, declared no strings, reasoning levels, service tiers, env passthrough, provider options, or extension kinds, shipped no bb tool, and its README (and thebb-plugin-authoringskill) pointed at the private@bb/provider-bridge-protocol/conformance. So presentation, extension kinds, delegation,fileRead,search,planSteps,extension.state, and the settings →providerOptions→ bridge round trip had never been exercised by non-first-party code.This was written stacked on #2179. The whole chain (#2124 → #2136 → #2153 → #2148 → #2121 → #2164 → #2178 → #2179) squash-merged into
mainwhile it was in progress, so the branch is rebased ontomainand the PR targetsmain.What changed
examples/plugins/echo-provider(imports only@get-bb/plugin-sdk{,/provider-bridge,/host},zod, node built-ins):server.ts:bb.settings.define({ shout });bb.agents.registerTool({ name: "echo_stamp", experimental_presentation });bb.providers.registerwithexperimental_strings(signInHint, expiredHint, installUrl, brandPrefix, planModeCopy, iconTint),experimental_reasoningLevels,experimental_serviceTiers, fullcapabilities,composerActions: ["plan"],experimental_models.fallback,experimental_env.passthrough,experimental_deriveProviderOptions(reads theshoutsetting, the model, and the prompt mode), andexperimental_extensionKinds—receipt(item) andmood(state), zod schemas.src/vocabulary.ts: the kinds, schemas, tool presentation, and every row presentation, shared by the declaration and the bridge.src/provider-bridge.ts: one scripted grammar v3 turn per prompt —command(+outputDelta),fileRead,search,delegationwith a keyed child turn linked throughparentRef,planSteps, a suppressedtool, the bb tool called overitem/tool/call(providerNativeIds: true,turnId: null) and stampedserver: "bb"with the definition's presentation, theextensionitem,extension.state, the echoed message (reporting the derivedproviderOptionsand the passed-through env var),usage+contextWindow,turn.boundary. Presentation on everyitem.openanditem.close. Handshake:grammarVersions [3,3],sessionRestore: true,steerMode: "queue",approvalEnforcedBy: "runtime".model/listreturns one model. Directives:/noop(zero-work turn),malformed-receipt(schema-violating payload).@get-bb/plugin-sdk/provider-bridge/testing):provider-bridge.conformance.test.ts(12 scenarios incl.turn/settles-without-activity),provider-bridge.stream.test.ts(the whole turn through the kit's real assembler, the bridge's tool call answered),public-sdk-only.test.ts(fails on any@bb/*import anywhere in the package, and on imports outside the allowlist).README.md: the rule, a capability table, how each is proven.apps/server:test/providers/echo-provider-canary.test.tsinstalls the plugin from its path, builds the realthread.startcommand, runs it on the real@bb/agent-runtime(now a server devDependency), posts every event through/internal/session/events, answers the tool call through/internal/session/tool-call, and reads the rows back.turbo.json: the plugin's sources join@bb/server#testinputs.Docs: the
bb-plugin-authoringskill's Conformance paragraph anddocs/provider-bridge-protocol.mdpoint at the public kit.No wire change;
HOST_DAEMON_PROTOCOL_VERSIONuntouched.How you verified
Capability checklist → proof
experimental_stringsincl.iconTint,experimental_reasoningLevels,experimental_serviceTiers,capabilities,composerActions,experimental_models.fallbackGET /api/v1/system/providersforecho-agentprojects all of them (strings, reasoningLevels with labels, serviceTiers, extensionKinds)experimental_env.passthroughcommand.bridgeLaunch.envPassthrough = ["BB_ECHO_PROVIDER_GREETING"]; the runtime forwards it and the bridge echoesBB_ECHO_PROVIDER_GREETING=hello from the daemonbb.settings.define→experimental_deriveProviderOptions→providerOptions→ bridgeupdateSettings({shout:true})→command.options.providerOptions = {shout:true, model:"echo-1", promptMode:null}→ messageecho: HELLO CANARY … providerOptions (server): shout=true. Live:bb plugin config echo-provider set shout true→echo: HELLO LIVE CANARY … shout=trueexperimental_extensionKindsitem kind, validated at ingestitem/completed extensionkindecho-provider/receiptpayload{prompt, itemCount: 7, shouted: true}provider/unhandledmalformed-receiptturn persists twoprovider/unhandledrows,rawType: extension/item:echo-provider/receipt, no extension rows; the same turn's state row still persistsexperimental_extensionKindsstate kindthread/extensionState/updatedkindecho-provider/moodbb.agents.registerTool({experimental_presentation})→dynamicTools[].presentation→ bridge stampsserver:"bb"+ presentationcommand.dynamicToolscarries the presentation; persistedtoolCallrowserver:"bb",result:"stamped: hello canary"(the plugin's ownexecute, via the real tool-call route), presentation = the declaration's. Live row identicalitem.open/item.closelabel+icon; live: 0 item rows without presentationcommand+outputDeltafileRead,searchdelegationwith child turn viaparentRefturn/started.parentToolCallId === delegation.id, child messageparentToolCallId, different turn id. Live: seq 17turn/started parentToolCallId=da94261219-i4= delegation idplanStepstoolwithpresentation.suppressitem.textDelta/item.textClose+usage+contextWindowthread/tokenUsage/updated,thread/contextWindowUsage/updated), live rowsgrammarVersions [3,3],steerMode,sessionRestorehandshake/initialize; live bridge accepted by the daemonturn/settles-without-activity(/noop)public-sdk-only.test.ts(12 tests)Commands
pnpm exec turbo run typecheck test --filter=bb-plugin-echo-provider— 4 files, 17 tests pass. Conformance report: 12/12pass(rpc/unknown-method,rpc/invalid-params,rpc/non-json-ignored,rpc/response-not-request,handshake/initialize,session/start-identity,turn/lifecycle,events/schema-valid,item/opens-before-delta,stop/release-not-interrupted,session/resume-id-uniqueness,turn/settles-without-activity).pnpm exec turbo run typecheck --filter=@bb/server— pass.pnpm exec turbo run test --filter=@bb/server— 1893 pass, 1 fail:internal-skill-treesmode 0644 vs 0664, the known local-umask failure unrelated to this change.Live run (layer C)
Dev instance
bb-worktrees-env_7afqatsj6r-bb-52b06540aff5on this branch.bb plugin install ./examples/plugins/echo-provider --yes(path install; statusrunning, capabilityagent-tool echo_stamp,hasSettings).bb plugin config echo-provider set shout true. In the web app: New thread → picker → "E" (Echo) → model "1" → prompthello live canary.thr_nzk5zngiii, providerecho-agent, statusidle.item/started+item/completedforcommandExecution,fileRead,search,delegation,planSteps, 2×toolCall,extension, 2×agentMessage; 2×turn/started(childparentToolCallId=da94261219-i4), 2×turn/completed,thread/extensionState/updated,thread/tokenUsage/updated,thread/contextWindowUsage/updated.kind echo-provider/receipt, payload{prompt:"hello live canary", itemCount:7, shouted:true}, presentationWrote receipt/PackageReceive/ tint.echo_stamprow:server:"bb",result:"stamped: hello live canary", presentationStamped receipt/Check/ tint.echo: HELLO LIVE CANARY·providerOptions (server): shout=true model=echo-1 promptMode=none·BB_ECHO_PROVIDER_GREETING=<unset>(the dev daemon's env has no such variable) ·echo_stamp: stamped: hello live canary.SELECT count(*) … item_kind IS NOT NULL AND json_extract(data,'$.item.presentation') IS NULL→ 0.Downloading host artifact … digest 9be6015218e303b…, thenUsing cached host artifact.$BB_THREAD_STORAGE/echo-live-timeline.png(collapsed) and$BB_THREAD_STORAGE/echo-live-timeline-expanded.png(expanded:Ran echo "hello live canary",Read README.md,Searched for …,Ran subagent: Echo "hello live canary" one more time,Ran tool bb:echo_stamp { text: … }with the result body, then the echoed message). Thread storage/home/sawyer/.bb/thread-storage/thr_u89sy372gv/.scripts/bb-dev-app stop).main(810cf2e + this branch): the path install survived the restart (bb plugin list→running),bb plugin reload echo-provider, thenbb thread spawn --project proj_personal --provider echo-agent --prompt "hello rebased canary"andbb thread wait --status idle(the CLI/agent path). Threadthr_28wgdzvcgc: the same 37 rows, extension payload{prompt:"hello rebased canary", itemCount:7, shouted:true}, mood state{mood:"cheerful", turnsEchoed:1}, messageecho: HELLO REBASED CANARY … shout=true … echo_stamp: stamped: hello rebased canary, 0 item rows without presentation.API gaps and findings
Gaps — things a third party needs that the public SDK does not expose:
replayRecording,replayRecordedCells,compareParity,assembleRecordedEvents,checkRecordedCellReplay,RECORDED_CONFORMANCE_CELLS, and the recording readers (readBridgeRecording,listRecordedCells) are not re-exported from@get-bb/plugin-sdk/provider-bridge/testing; and even with the private import,resolveReplayProfilethrowsUnreplayableProviderErrorfor any provider not inFIRST_PARTY_BRIDGE_MODULES. The recording side (experimental_isProviderBridgeRecording,experimental_recordProviderChildIo) is public, so a third party can record but cannot replay or compare. This PR therefore ships no parity self-run for the echo bridge; the stream test (real assembler, asserted events) is the closest public equivalent.ThreadEventis not exported by name from the testing kit. The kit's assembler returns canonical events but the type is only reachable asReturnType<BridgeDeltaEventCollector["assembleMessage"]>[number](seeprovider-bridge.stream.test.ts). A one-lineexport type { ThreadEvent }would fix it.PluginProviderDeclaration.iconglyphs are dropped at projection.icon: "Zap"validates, butProviderInfoonly carrieslogoUrl(null for a glyph), so the picker shows the initial "E". A third party without an SVG asset cannot get an icon into the picker.Observations — declared and projected correctly, but not consumed by the web client yet (WS2a/WS3 consumers, not API gaps):
echo-provider/receiptrow is persisted with full presentation but nothing inpackages/thread-viewprojectsitem.type === "extension", so the row is invisible (the expanded screenshot shows every other row). The declarative base for extension kinds is WS3.echo_stamprow renders as the genericRan tool bb:echo_stamp { text: … }instead ofStamped receiptwith theCheckglyph; same for the core rows, which use core verbs. Persistence is correct; the renderer is WS3.reasoningLevelslabels,serviceTierslabels, andstrings.iconTintare projected onProviderInfobut unused by the app. The picker showedLow / Medium / Highrather thanWhisper / Speak / Shout, a generic "Fast mode" switch, and no tint.useThreadCreationOptions.tsreads onlybrandPrefix,planModeCopy,installUrl; the usage settings readsignInHint/expiredHint.item.textDelta/item.textClosehave nopresentationfield, so a stream that opens itself (the claude/pi/acp channel-key pattern) synthesizes anagentMessagewithout one. The echo bridge opens its messages withitem.openfirst and the close echoes the presentation; first-party bridges that stream anonymously will need the same change before presentation becomes required.