From 6e8514dc9df844cc566a9bf43a8e3265a486827a Mon Sep 17 00:00:00 2001 From: Bradley-Butcher Date: Thu, 6 Aug 2026 18:20:11 +0100 Subject: [PATCH] Add Prime Agent ACP support --- .../src/components/icons/PrimeAgentIcon.tsx | 14 +++++++++ apps/app/src/lib/provider-icon.ts | 5 ++++ .../src/services/system/known-acp-agents.ts | 27 +++++++++++++++++ .../test/public/public-thread-data.test.ts | 1 + .../src/acp/bridge/bridge.test.ts | 30 +++++++++++++++++++ .../agent-runtime/src/acp/bridge/bridge.ts | 3 +- .../src/acp/bridge/model-catalog.test.ts | 21 +++++++++++++ .../src/acp/bridge/model-catalog.ts | 9 ++++++ 8 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 apps/app/src/components/icons/PrimeAgentIcon.tsx diff --git a/apps/app/src/components/icons/PrimeAgentIcon.tsx b/apps/app/src/components/icons/PrimeAgentIcon.tsx new file mode 100644 index 0000000000..f065c51186 --- /dev/null +++ b/apps/app/src/components/icons/PrimeAgentIcon.tsx @@ -0,0 +1,14 @@ +export function PrimeAgentIcon({ className }: { className?: string }) { + return ( + + Prime Agent + + + + ); +} diff --git a/apps/app/src/lib/provider-icon.ts b/apps/app/src/lib/provider-icon.ts index 96b8d69d51..63e27e629b 100644 --- a/apps/app/src/lib/provider-icon.ts +++ b/apps/app/src/lib/provider-icon.ts @@ -13,6 +13,7 @@ import { OpenAiIcon } from "@/components/icons/OpenAiIcon"; import { OpencodeIcon } from "@/components/icons/OpencodeIcon"; import { OmpIcon } from "@/components/icons/OmpIcon"; import { PiIcon } from "@/components/icons/PiIcon"; +import { PrimeAgentIcon } from "@/components/icons/PrimeAgentIcon"; import { Icon } from "@bb/shared-ui/icon"; const ACP_ID_PREFIX = "acp-"; @@ -36,6 +37,7 @@ const KNOWN_ACP_BRAND_ICONS: Record< "hermes-agent": HermesAgentIcon, opencode: OpencodeIcon, omp: OmpIcon, + "prime-agent": PrimeAgentIcon, }; const configuredProviderLogoIcons = new Map< @@ -151,5 +153,8 @@ export function getProviderIconColorClass(providerId: string): string { if (providerId === "acp-omp") { return "text-[#9333EA]"; } + if (providerId === "acp-prime-agent") { + return "text-[#9333EA]"; + } return "text-foreground"; } diff --git a/apps/server/src/services/system/known-acp-agents.ts b/apps/server/src/services/system/known-acp-agents.ts index 41fecf016b..fce897ec06 100644 --- a/apps/server/src/services/system/known-acp-agents.ts +++ b/apps/server/src/services/system/known-acp-agents.ts @@ -13,6 +13,33 @@ export interface KnownAcpAgentExecutableQuery { } export const KNOWN_ACP_AGENTS: readonly KnownAcpAgent[] = [ + { + // bb owns transcript persistence, so Prime must not reuse saved sessions. + id: "acp-prime-agent", + displayName: "Prime Agent", + command: "prime-agent", + args: ["--mode", "acp", "--no-session"], + env: {}, + executableName: "prime-agent", + modelCli: { + listArgs: ["model", "list"], + selectFlag: "--model", + primaryModels: [ + "openai-codex/gpt-5.6-luna", + "openai-codex/gpt-5.6-sol", + "openai-codex/gpt-5.6-terra", + "anthropic/claude-opus-5", + "anthropic/claude-sonnet-5", + "anthropic/claude-fable-5", + ], + }, + reasoningCli: { + flag: "--thinking", + supportedLevels: ["low", "medium", "high", "xhigh", "max"], + levelValues: { low: "minimal" }, + defaultLevel: "medium", + }, + }, { id: "acp-opencode", displayName: "opencode", diff --git a/apps/server/test/public/public-thread-data.test.ts b/apps/server/test/public/public-thread-data.test.ts index 77bac0e860..34a67238da 100644 --- a/apps/server/test/public/public-thread-data.test.ts +++ b/apps/server/test/public/public-thread-data.test.ts @@ -3212,6 +3212,7 @@ describe("public thread data routes", () => { { type: "known_acp_agents.status", agents: [ + { id: "acp-prime-agent", executableName: "prime-agent" }, { id: "acp-opencode", executableName: "opencode" }, { id: "acp-omp", executableName: "omp" }, { id: "acp-grok", executableName: "grok" }, diff --git a/packages/agent-runtime/src/acp/bridge/bridge.test.ts b/packages/agent-runtime/src/acp/bridge/bridge.test.ts index ba0db124d1..c3a4f70825 100644 --- a/packages/agent-runtime/src/acp/bridge/bridge.test.ts +++ b/packages/agent-runtime/src/acp/bridge/bridge.test.ts @@ -97,6 +97,7 @@ interface StartThreadArgs { selectFlag: string; model: string; reasoningLevel?: ReasoningLevel; + serviceTier?: "default" | "fast"; } | { modelId: string; reasoningLevel?: ReasoningLevel }; launchReasoningLevel?: ReasoningLevel; @@ -870,6 +871,35 @@ describe("acp bridge", () => { ).toBe(true); }); + it("passes a provider-qualified fast model through the model flag", async () => { + chmodSync(FAKE_AGENT_PATH, 0o755); + + const { providerThreadId } = await startThread({ + agent: { command: FAKE_AGENT_PATH, args: [] }, + modelSelection: { + listCommand: { + command: process.execPath, + args: [ + "-e", + 'console.error("provider model context\\nanthropic claude-opus-5 1M\\nanthropic claude-opus-5-fast 1M")', + ], + }, + selectFlag: "--model", + model: "anthropic/claude-opus-5", + serviceTier: "fast", + }, + }); + sendRequest("turn/start", { + threadId: providerThreadId, + input: [{ type: "text", text: "echo-argv", mentions: [] }], + }); + await waitForTurnCompleted(); + + expect(agentMessageTexts()).toContain( + "argv:--model anthropic/claude-opus-5-fast", + ); + }); + it("selects ACP-native models with session/set_config_option before the first prompt", async () => { const { providerThreadId } = await startThread({ envVars: { FAKE_ACP_MODEL_CONFIG: "1" }, diff --git a/packages/agent-runtime/src/acp/bridge/bridge.ts b/packages/agent-runtime/src/acp/bridge/bridge.ts index 3831576bc7..51e9620037 100644 --- a/packages/agent-runtime/src/acp/bridge/bridge.ts +++ b/packages/agent-runtime/src/acp/bridge/bridge.ts @@ -665,7 +665,8 @@ async function loadAgentModelCatalog( }, (error, out, stderr) => { if (!error) { - resolveExec(out); + // Prime Agent writes its model table to stderr. + resolveExec(out.trim().length > 0 ? out : stderr); return; } if (isMissingExecutableError(error)) { diff --git a/packages/agent-runtime/src/acp/bridge/model-catalog.test.ts b/packages/agent-runtime/src/acp/bridge/model-catalog.test.ts index 313fc6174d..a9f242db1e 100644 --- a/packages/agent-runtime/src/acp/bridge/model-catalog.test.ts +++ b/packages/agent-runtime/src/acp/bridge/model-catalog.test.ts @@ -62,6 +62,27 @@ describe("acp model catalog", () => { ]); }); + it("parses Prime Agent's provider/model table", () => { + expect( + parseAgentModelLines( + [ + "provider model context max-out thinking images", + "openai gpt-5.6-sol 1.1M 128K yes yes", + "openai-codex gpt-5.6-terra 272K 128K yes yes", + ].join("\n"), + ), + ).toEqual([ + { + id: "openai/gpt-5.6-sol", + displayName: "gpt-5.6-sol", + }, + { + id: "openai-codex/gpt-5.6-terra", + displayName: "gpt-5.6-terra", + }, + ]); + }); + it("parses Grok's bulleted model list", () => { expect( parseAgentModelLines( diff --git a/packages/agent-runtime/src/acp/bridge/model-catalog.ts b/packages/agent-runtime/src/acp/bridge/model-catalog.ts index 0489ee21e5..456b1c2e52 100644 --- a/packages/agent-runtime/src/acp/bridge/model-catalog.ts +++ b/packages/agent-runtime/src/acp/bridge/model-catalog.ts @@ -66,6 +66,8 @@ interface AgentModelVariant extends RawAgentModel { const MODEL_LINE_PATTERN = /^(\S+) - (.+)$/; const BARE_PROVIDER_MODEL_LINE_PATTERN = /^\S+\/\S+$/; const BULLETED_MODEL_LINE_PATTERN = /^[*-]\s+(\S+)(?:\s+\([^)]*\))?$/u; +// Prime prints provider and model as the first two fixed-width columns. +const PRIME_MODEL_TABLE_ROW_PATTERN = /^(\S+)\s{2,}(\S+)\s{2,}\S+/; // Trailing id tokens that mark a reasoning-effort variant, longest first so // `extra-high` wins over `high`. `none` maps onto bb's "none" (thinking-off) @@ -118,6 +120,13 @@ export function parseAgentModelLines(stdout: string): RawAgentModel[] { if (BARE_PROVIDER_MODEL_LINE_PATTERN.test(trimmed)) { models.push({ id: trimmed, displayName: trimmed }); } + const primeTableRow = PRIME_MODEL_TABLE_ROW_PATTERN.exec(trimmed); + if (primeTableRow) { + const [, provider, model] = primeTableRow; + if (provider !== "provider" && model !== "model") { + models.push({ id: `${provider}/${model}`, displayName: model }); + } + } continue; } const [, id, displayName] = match;