Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions docs/api_to_audit.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,38 @@ content-block vocabulary; decide whether legacy aggregate fields still need to
be accepted; and define any image MIME validation, decoding, or payload-size
policy at the server boundary before making the helper stable.

## The ACP bridge kit (`@get-bb/plugin-sdk/provider-bridge/acp`)

**What it does.** Publishes bb's generic Agent Client Protocol bridge so any
plugin can add an ACP agent without bb-side code. `experimental_acpProviderBridge`
is the bridge a plugin re-exports from its `bb.host` artifact; the agent to
launch arrives per command in `providerOptions.acpLaunchSpec`, so one
implementation serves every agent. `experimental_registerAcpDialect` and
`experimental_resolveAcpDialect` are the dialect hooks: version 1 of the
protocol has no sub-agent concept and standardizes nothing about `rawInput`,
so each agent's vendor side channels (grok's `_meta["x.ai/tool"]`, Cursor's
`cursor/task` request, an agent's own health/usage/installation surface) are
read by a small profile-keyed module that a plugin can supply for its own
agent and name in its registration's bridge options (`acpDialect`).
`experimental_handleAcpBridgeLine` is the raw line handler for harnesses.
`experimental_parseAcpAgentModelLines` / `experimental_buildAcpAgentModelCatalog`
/ `experimental_splitAcpPrimaryModels` build a model picker from an agent's
`--list-models` output. `experimental_acpProfileFromLaunchSpec` and
`experimental_ACP_*` expose the launch profile and the protocol vocabularies.

**Audit before stabilizing.** Decide whether `AcpDialect` is the right shape
for a third-party agent — today it has four optional hooks (`toolIdentity`,
`classifyToolCall`, `handleClientRequest`, `maintenance`) and no versioning,
so adding a fifth is a silent capability change for every dialect. Decide
whether `registerAcpDialect`'s process-global registry is right, or whether a
dialect should be named by value in the provider registration instead of by
id. Confirm the dialect id namespace (ids are unscoped strings today, so two
plugins can collide) and whether a plugin may override a built-in dialect.
Settle whether the bridge itself should be a factory rather than a module
singleton before a host artifact ever needs two configured differently, and
whether the model-catalog helpers belong in this kit at all or in a
CLI-model-discovery kit of their own.

## Bridge record mode (`experimental_recordProviderChildIo` and `experimental_isProviderBridgeRecording`)

**What it does.** `experimental_recordProviderChildIo` tees a provider
Expand Down
49 changes: 48 additions & 1 deletion packages/plugin-build/src/builtin-host-artifacts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@ import { resolvePluginBuildToolchain } from "./toolchain.js";

const repositoryRoot = resolve(import.meta.dirname, "../../..");

interface BuiltProviderBridge {
readonly experimental_apiVersion: 1;
readonly handleLine: (line: string) => void;
}

function isBuiltProviderBridge(value: unknown): value is BuiltProviderBridge {
if (typeof value !== "object" || value === null) return false;
return (
Reflect.get(value, "experimental_apiVersion") === 1 &&
typeof Reflect.get(value, "handleLine") === "function"
);
}

interface BuiltHostEntry {
readonly experimental_apiVersion: 1;
readonly handlers: Readonly<
Expand Down Expand Up @@ -78,4 +91,38 @@ describe("builtin host artifacts", () => {
supported: process.platform === "darwin",
});
}, 20_000);
});

/**
* The ACP plugin's whole host side is one re-export of the published kit
* (`@get-bb/plugin-sdk/provider-bridge/acp`), which is exactly what a
* third-party ACP plugin writes. This builds that artifact the way the
* daemon does — inlining the SDK's published bundle from the plugin's own
* node_modules — and imports the result, so a kit that only resolves
* through the workspace source condition cannot pass.
*/
it("builds the ACP provider bridge from the published SDK subpath", async () => {
const root = await mkdtemp(join(repositoryRoot, ".builtin-host-test-"));
tempDirs.push(root);
const source = join(repositoryRoot, "plugins", "provider-acp");
for (const fileName of ["package.json", "server.ts"]) {
await cp(join(source, fileName), join(root, fileName));
}
await cp(join(source, "src"), join(root, "src"), { recursive: true });
// The manifest's branding icon must resolve for the build to run.
await cp(join(source, "icons"), join(root, "icons"), { recursive: true });
await symlink(
join(source, "node_modules"),
join(root, "node_modules"),
"dir",
);
const toolchain = await resolvePluginBuildToolchain(
join(repositoryRoot, "node_modules", ".unused-toolchain"),
);
const built = await buildPluginHost(root, "0.9.0-test", toolchain);
const imported: unknown = await import(
`${pathToFileURL(built.jsPath).href}?test=${Date.now()}`
);
const bridge = Reflect.get(Object(imported), "experimental_providerBridge");
expect(isBuiltProviderBridge(bridge)).toBe(true);
}, 60_000);
});
7 changes: 7 additions & 0 deletions packages/plugin-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@
"import": "./dist/provider-bridge-testing.js",
"default": "./dist/provider-bridge-testing.js"
},
"./provider-bridge/acp": {
"source": "./src/provider-bridge-acp.ts",
"types": "./bundled-types/bb-plugin-sdk-provider-bridge-acp.d.ts",
"import": "./dist/provider-bridge-acp.js",
"default": "./dist/provider-bridge-acp.js"
},
"./app": {
"source": "./src/app.ts",
"types": "./bundled-types/bb-plugin-sdk-app.d.ts",
Expand Down Expand Up @@ -113,6 +119,7 @@
"@bb/domain": "workspace:*",
"@bb/host-daemon-contract": "workspace:*",
"@bb/process-utils": "workspace:*",
"@bb/provider-bridge-acp": "workspace:*",
"@bb/provider-bridge-protocol": "workspace:*",
"@bb/sdk": "workspace:*",
"@bb/server-contract": "workspace:*",
Expand Down
4 changes: 4 additions & 0 deletions packages/plugin-sdk/scripts/build-bundled-dts.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ const outputs = {
pkgRoot,
"src/provider-bridge-testing.ts",
),
"bb-plugin-sdk-provider-bridge-acp.d.ts": path.join(
pkgRoot,
"src/provider-bridge-acp.ts",
),
"bb-plugin-sdk-host.d.ts": path.join(pkgRoot, "src/host.ts"),
"bb-plugin-sdk-internal-composer-customization-validation.d.ts": path.join(
pkgRoot,
Expand Down
8 changes: 8 additions & 0 deletions packages/plugin-sdk/scripts/build-runtime.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ const entries = [
output: "dist/provider-bridge-testing.js",
external: ["zod", "zod/*"],
},
// The ACP kit: the generic Agent Client Protocol bridge a provider plugin
// re-exports from its host artifact, plus the dialect hooks. Real code, so
// only zod stays external.
{
source: "src/provider-bridge-acp.ts",
output: "dist/provider-bridge-acp.js",
external: ["zod", "zod/*"],
},
{ source: "src/host.ts", output: "dist/host.js", external: [] },
{
source: "src/internal/composer-customization-validation.ts",
Expand Down
1 change: 1 addition & 0 deletions packages/plugin-sdk/src/__tests__/package-exports.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ describe("packed plugin SDK exports", () => {
".",
"./provider-bridge",
"./provider-bridge/testing",
"./provider-bridge/acp",
"./app",
"./host",
"./internal/composer-customization-validation",
Expand Down
83 changes: 83 additions & 0 deletions packages/plugin-sdk/src/provider-bridge-acp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/**
* `@get-bb/plugin-sdk/provider-bridge/acp` — the published ACP bridge kit.
*
* The Agent Client Protocol (https://agentclientprotocol.com) is one wire
* protocol spoken by many agents, so bb runs all of them through one generic
* bridge: the agent to launch arrives per command in the provider options,
* and nothing in the bridge is bb-first-party. A plugin that wants to add an
* ACP agent re-exports the bridge from its `bb.host` artifact and registers
* its providers as any other plugin does:
*
* ```ts
* // host.ts (the plugin's `bb.host` entry)
* export { experimental_acpProviderBridge as experimental_providerBridge }
* from "@get-bb/plugin-sdk/provider-bridge/acp";
*
* // server.ts
* bb.providers.register({
* id: "amp",
* displayName: "Amp",
* experimental_bridgeOptions: {
* acpLaunchSpec: { displayName: "Amp", command: "amp", args: ["acp"], env: {} },
* acpDialect: "amp",
* },
* // …the rest of the declaration
* })
* ```
*
* **Dialects.** Version 1 of the protocol has no sub-agent concept and
* standardizes nothing about `rawInput`, so what most distinguishes one
* agent from another lives beside the protocol: grok stamps
* `_meta["x.ai/tool"]` on every tool event, Cursor reports sub-agents
* through a vendor `cursor/task` request. A dialect is a small module that
* reads those channels; a plugin registers one for its own agent with
* `experimental_registerAcpDialect` and names its id in the registration's
* bridge options. Everything a dialect does is optional — the shared
* classifier decides everything it declines.
*
* Curated by hand — named exports only, never `export *`. Value exports
* carry the `experimental_` prefix every new plugin API member ships with
* (see docs/api_to_audit.md); types are unprefixed.
*/
export {
acpProviderBridge as experimental_acpProviderBridge,
handleAcpBridgeLine as experimental_handleAcpBridgeLine,
} from "@bb/provider-bridge-acp";

export {
CURSOR_ACP_DIALECT as experimental_CURSOR_ACP_DIALECT,
GENERIC_ACP_DIALECT as experimental_GENERIC_ACP_DIALECT,
GROK_ACP_DIALECT as experimental_GROK_ACP_DIALECT,
acpDialectIds as experimental_acpDialectIds,
registerAcpDialect as experimental_registerAcpDialect,
resolveAcpDialect as experimental_resolveAcpDialect,
} from "@bb/provider-bridge-acp";
export type {
AcpClassifiedToolCall,
AcpClientRequestOutcome,
AcpDelegationReport,
AcpDialect,
AcpToolIdentity,
} from "@bb/provider-bridge-acp";

export { acpProfileFromLaunchSpec as experimental_acpProfileFromLaunchSpec } from "@bb/provider-bridge-acp";
export type { AcpAgentProfile } from "@bb/provider-bridge-acp";

export {
ACP_PROTOCOL_VERSION as experimental_ACP_PROTOCOL_VERSION,
ACP_TOOL_CALL_STATUSES as experimental_ACP_TOOL_CALL_STATUSES,
ACP_TOOL_KINDS as experimental_ACP_TOOL_KINDS,
} from "@bb/provider-bridge-acp";
export type {
AcpToolCallContent,
AcpToolCallStatus,
AcpToolCallUpdateEvent,
AcpToolKind,
} from "@bb/provider-bridge-acp";

export {
buildAgentModelCatalog as experimental_buildAcpAgentModelCatalog,
parseAgentModelLines as experimental_parseAcpAgentModelLines,
splitPrimaryModels as experimental_splitAcpPrimaryModels,
} from "@bb/provider-bridge-acp";
export type { AgentModelCatalog as AcpAgentModelCatalog } from "@bb/provider-bridge-acp";
33 changes: 33 additions & 0 deletions packages/provider-bridge-acp/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "@bb/provider-bridge-acp",
"version": "0.0.1",
"type": "module",
"exports": {
".": {
"source": "./src/index.ts",
"types": "./src/index.ts",
"default": "./src/index.ts"
}
},
"types": "./src/index.ts",
"scripts": {
"clean": "rimraf dist tsconfig.tsbuildinfo",
"typecheck": "tsc --noEmit",
"test": "vitest run --config vitest.config.ts"
},
"dependencies": {
"@bb/domain": "workspace:*",
"@bb/host-daemon-contract": "workspace:*",
"@bb/process-utils": "workspace:*",
"@bb/provider-bridge-protocol": "workspace:*",
"zod": "^4.3.6"
},
"devDependencies": {
"@bb/tsconfig": "workspace:*",
"@modelcontextprotocol/sdk": "^1.29.0",
"@types/node": "^22.0.0",
"typescript": "npm:@typescript/typescript6@^6.0.2",
"typescript-7": "npm:typescript@^7.0.2",
"vitest": "^4.1.1"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,8 @@
* why they are schemas rather than ad-hoc objects.
*/

import {
acpPermissionCliSchema as acpBridgePermissionCliSchema,
acpNativeReasoningSchema as acpBridgeNativeReasoningSchema,
acpReasoningCliSchema as acpBridgeReasoningCliSchema,
modelListParamsSchema as canonicalModelListParamsSchema,
threadDiscardParamsSchema as canonicalThreadDiscardParamsSchema,
threadForkParamsSchema as canonicalThreadForkParamsSchema,
threadResumeParamsSchema as canonicalThreadResumeParamsSchema,
threadStartParamsSchema as canonicalThreadStartParamsSchema,
threadStopParamsSchema as canonicalThreadStopParamsSchema,
turnStartParamsSchema as canonicalTurnStartParamsSchema,
turnSteerParamsSchema as canonicalTurnSteerParamsSchema,
skillsConfigureParamsSchema,
experimental_providerMaintenanceParamsSchema,
experimental_providerInstallationRunParamsSchema,
experimental_providerInstallationStatusParamsSchema,
} from "@get-bb/plugin-sdk/provider-bridge";
import { acpNativeReasoningSchema as acpBridgeNativeReasoningSchema, acpPermissionCliSchema as acpBridgePermissionCliSchema, acpReasoningCliSchema as acpBridgeReasoningCliSchema } from "@bb/domain";
import { experimental_providerInstallationRunParamsSchema, experimental_providerInstallationStatusParamsSchema, experimental_providerMaintenanceParamsSchema, modelListParamsSchema as canonicalModelListParamsSchema, skillsConfigureParamsSchema, threadDiscardParamsSchema as canonicalThreadDiscardParamsSchema, threadForkParamsSchema as canonicalThreadForkParamsSchema, threadResumeParamsSchema as canonicalThreadResumeParamsSchema, threadStartParamsSchema as canonicalThreadStartParamsSchema, threadStopParamsSchema as canonicalThreadStopParamsSchema, turnStartParamsSchema as canonicalTurnStartParamsSchema, turnSteerParamsSchema as canonicalTurnSteerParamsSchema } from "@bb/provider-bridge-protocol";
import { z } from "zod";
import { acpSessionUpdateSchema, acpStopReasonSchema } from "./wire.js";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import { spawn, type ChildProcess } from "node:child_process";
import { createInterface } from "node:readline";
import { experimental_recordProviderChildIo } from "@get-bb/plugin-sdk/provider-bridge";
import { experimental_recordProviderChildIo } from "@bb/provider-bridge-protocol/bridge-kit";
import type { z } from "zod";

const STDERR_TAIL_MAX_CHUNKS = 40;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@ import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { afterEach, beforeEach, expect, it } from "vitest";
import {
experimental_captureBridgeJsonRpcOutput as captureBridgeJsonRpcOutput,
experimental_createBridgeDeltaEventCollector as createBridgeDeltaEventCollector,
experimental_formatConformanceReport as formatConformanceReport,
experimental_runBridgeConformance as runBridgeConformance,
experimental_toConformanceMessages as toConformanceMessages,
} from "@get-bb/plugin-sdk/provider-bridge/testing";
import type {
BridgeConformanceTransport,
CapturedBridgeJsonRpcOutput,
} from "@get-bb/plugin-sdk/provider-bridge/testing";
formatConformanceReport,
runBridgeConformance,
} from "@bb/provider-bridge-protocol/conformance";
import type { BridgeConformanceTransport } from "@bb/provider-bridge-protocol/conformance";
import {
captureBridgeJsonRpcOutput,
createBridgeDeltaEventCollector,
toConformanceMessages,
} from "@bb/provider-bridge-protocol/testing";
import type { CapturedBridgeJsonRpcOutput } from "@bb/provider-bridge-protocol/testing";

import { handleLine } from "./bridge.js";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,15 @@ import { fileURLToPath } from "node:url";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createStandaloneBuiltinCompactCommandInput } from "@bb/domain";
import type { DynamicTool, ReasoningLevel } from "@bb/domain";
import { PROVIDER_BRIDGE_PROTOCOL_VERSION, THREAD_DELTA_NOTIFICATION_METHOD } from "@bb/provider-bridge-protocol";
import {
PROVIDER_BRIDGE_PROTOCOL_VERSION,
THREAD_DELTA_NOTIFICATION_METHOD,
} from "@bb/provider-bridge-protocol";
import {
experimental_assembleCapturedThreadEvents as assembleCapturedThreadEvents,
experimental_captureBridgeJsonRpcOutput as captureBridgeJsonRpcOutput,
} from "@get-bb/plugin-sdk/provider-bridge/testing";
assembleCapturedThreadEvents,
captureBridgeJsonRpcOutput,
} from "@bb/provider-bridge-protocol/testing";
import type {
BridgeJsonRpcOutputMessage,
CapturedBridgeJsonRpcOutput,
} from "@get-bb/plugin-sdk/provider-bridge/testing";
} from "@bb/provider-bridge-protocol/testing";

import { handleLine } from "./bridge.js";
import { ACP_BRIDGE_NO_ACTIVE_TURN_ERROR_CODE } from "../bridge-protocol.js";
Expand Down
Loading
Loading