bb.agents
The bb.agents plugin owns agent tools, helper AI services, skills, session configuration, and tool-call policy.
Purpose
bb.agents gives every agent capability a typed contract and a stable owner.
It lets plugins add or replace tools and AI services without private registries.
It also resolves skills and agent configuration before each agent turn.
Surfaces
| ID | kind | replaceable | props contract sketch | notes |
|---|---|---|---|---|
| — | — | — | — | bb.agents owns no app surface. bb.threads renders tool calls and agent state. |
These contracts have no spatial owner. They appear in the Plugin backend section of the contract inspector.
Services
| ID | kind | replaceable | contract sketch | notes |
|---|---|---|---|---|
bb.agents.tool |
keyed |
yes | AgentTool keyed by name |
One winner supplies each model-visible tool name. |
bb.agents.ai |
keyed |
yes | AiService keyed by id |
One winner supplies each helper AI service. |
bb.agents.configuration |
list |
no | AgentConfigurationContribution |
The resolver combines all active contributions before each turn. |
bb.agents.toolCalls |
chain |
no | AgentToolCallInterceptor |
The chain wraps the selected tool call. The user controls chain order. |
bb.agents.skills |
single |
yes | AgentSkillsService |
The default service owns workspace, registry, and CLI skill operations. |
bb.agents.tool
bb.agents.tool uses the model-visible tool name as its key.
The contract accepts Standard Schema values and JSON Schema objects.
The Standard Schema form gives execute a typed input.
import type { StandardSchemaV1 } from "@standard-schema/spec";
export type JsonObject = { readonly [key: string]: JsonValue };
export type JsonValue =
| null
| boolean
| number
| string
| readonly JsonValue[]
| JsonObject;
export type AgentToolContent =
| { type: "text"; text: string }
| { type: "image"; data: string; mimeType: string };
export type AgentToolResult =
| string
| {
content: readonly AgentToolContent[];
isError?: boolean;
};
export interface AgentToolLabels {
pending: string;
completed: string;
}
export interface AgentToolPresentation {
label?: AgentToolLabels;
icon?: { glyph: string };
suppress?: boolean;
tint?: { light: string; dark: string };
intent?: "generic" | "terminal" | "diff" | "search" | "read" | "list" | "web" | "image";
}
export interface AgentToolContext {
threadId: string;
projectId: string;
environmentId: string | null;
hostId: string;
providerId: string;
signal: AbortSignal;
}
export interface AgentToolBase {
name: string;
description: string;
instructions?: string;
presentation?: AgentToolPresentation;
}
export interface AgentTool<Parameters = unknown> extends AgentToolBase {
parameters: StandardSchemaV1<unknown, Parameters> | JsonObject;
execute(
parameters: Parameters,
context: AgentToolContext,
): AgentToolResult | Promise<AgentToolResult>;
}
export interface AgentToolsApi {
add<Schema extends StandardSchemaV1>(
tool: AgentTool<StandardSchemaV1.InferOutput<Schema>> & { parameters: Schema },
): { dispose(): void };
add(
tool: AgentTool<unknown> & { parameters: JsonObject },
): { dispose(): void };
}The name must match [A-Za-z0-9_-]+ and contain no more than 64 characters.
The plugin should prefix a new name with its plugin ID.
A deliberate collision creates another claimant for that key.
The manifest claim makes the key visible before server code starts. The factory call supplies the behavior during the atomic plugin commit.
{
"requires": [
{ "service": "bb.agents.tool", "range": "^1" }
],
"claims": [
{
"service": "bb.agents.tool",
"version": "1.0.0",
"key": "acme_search_issues"
}
]
}import { z } from "zod";
import { defineServerPlugin } from "@get-bb/plugin/server";
export default defineServerPlugin((api) => {
api.tools.add({
name: "acme_search_issues",
description: "Search Acme issues by text.",
instructions: "Use this tool before you create a duplicate issue.",
parameters: z.object({ query: z.string().min(1) }),
presentation: {
label: { pending: "Searching issues", completed: "Searched issues" },
icon: { glyph: "Search" },
intent: "search",
},
async execute({ query }, context) {
const issues = await searchIssues(query, context.signal);
return { content: [{ type: "text", text: formatIssues(issues) }] };
},
});
});api.tools.add stages one bb.agents.tool provision.
The loader checks its static claim, key, version, and contract.json schema.
The runtime releases the provision when the plugin stops.
Each tool key has one default claimant and one current winner.
A winner change starts the candidate before cutover.
The old call drains to the kernel deadline, and new calls use the new winner.
The backend never gives a replacement an Original handle.
bb.agents.ai
bb.agents.ai provides small helper AI operations.
It does not replace the provider bridge that runs an agent session.
bb.providers owns that provider bridge.
export type AiServiceKind = "inference" | "voice";
export type AiServiceErrorCode =
| "timeout"
| "rate_limited"
| "service_unavailable"
| "auth_required"
| "request_failed"
| "invalid_response";
export type AiServiceFailure = {
ok: false;
code: AiServiceErrorCode;
message: string;
};
export interface AiCallContext {
threadId?: string;
projectId?: string;
hostId: string;
signal: AbortSignal;
}
export interface AiInferenceRequest {
model: string;
reasoningEffort: "none";
prompt: string;
outputSchema: JsonObject;
timeoutMs: number;
}
export type AiInferenceResult =
| { ok: true; model: string; value: JsonObject }
| AiServiceFailure;
export interface AiVoiceRequest {
model: string;
audioBase64: string;
mimeType: string;
filename: string;
prompt: string | null;
timeoutMs: number;
}
export type AiVoiceResult =
| { ok: true; model: string; text: string }
| AiServiceFailure;
export interface AiService {
readonly id: string;
readonly displayName: string;
readonly kinds: readonly AiServiceKind[];
inference?: {
complete(input: AiInferenceRequest, context: AiCallContext): Promise<AiInferenceResult>;
};
voice?: {
transcribe(input: AiVoiceRequest, context: AiCallContext): Promise<AiVoiceResult>;
};
}
export interface AgentAiApi {
add(service: AiService): { dispose(): void };
}The kinds list and the method groups must match.
The contract build rejects a missing declared method.
It also rejects an undeclared method group.
The service ID is the keyed contract key. The public method input does not repeat that ID. The host role request includes the ID because one host artifact can serve several keys.
{
"requires": [
{ "service": "bb.agents.ai", "range": "^1" }
],
"claims": [
{
"service": "bb.agents.ai",
"version": "1.0.0",
"key": "acme.local-ai"
}
]
}const aiHost = api.host.role(aiHostRole);
api.ai.add({
id: "acme.local-ai",
displayName: "Acme Local AI",
kinds: ["inference", "voice"],
inference: {
complete: (input, context) =>
aiHost.call(
"inference.complete",
{ serviceId: "acme.local-ai", ...input },
{ hostId: context.hostId, signal: context.signal },
),
},
voice: {
transcribe: (input, context) =>
aiHost.call(
"voice.transcribe",
{ serviceId: "acme.local-ai", ...input },
{ hostId: context.hostId, signal: context.signal },
),
},
});Each AI service key follows the same winner and fallback rules as an agent tool. The default provider stays live until the selected candidate becomes ready.
bb.agents.configuration
This list contract combines contributions from all active plugins. The runtime resolves the list before each agent turn. A contribution can use stable thread, project, environment, host, provider, and origin data.
export interface AgentConfigurationContext {
thread: {
id: string;
title: string | null;
parentThreadId: string | null;
sourceThreadId: string | null;
};
project: {
id: string;
kind: "standard" | "personal";
name: string;
gitRemoteUrl: string | null;
};
environment: {
id: string;
name: string | null;
path: string | null;
workspaceProvisionType: string;
branchName: string | null;
};
host: { id: string; name: string };
provider: {
id: string;
model: string;
capabilities: { supportsNativeUserQuestion: boolean };
};
origin: {
kind: "fork" | null;
pluginId: string | null;
};
}
export type AgentConfigurationScope =
| { kind: "all" }
| { kind: "project"; projectId: string }
| { kind: "thread"; threadId: string };
export interface AgentToolSelection {
name: string;
parameters?: JsonObject;
}
export interface AgentToolSet {
include?: readonly (string | AgentToolSelection)[];
exclude?: readonly string[];
}
export interface AgentInstructions {
text: string;
mode: "append" | "prepend" | "replace";
}
export interface SkillRef {
rootId: string;
path: string;
}
export interface AgentConfiguration {
scope?: AgentConfigurationScope;
tools?: AgentToolSet;
skills?: readonly SkillRef[];
instructions?: AgentInstructions;
env?: Readonly<Record<`BB_${string}`, string>>;
}
export interface AgentConfigurationContribution {
id: string;
resolve(
context: AgentConfigurationContext,
): AgentConfiguration | null | Promise<AgentConfiguration | null>;
}
export interface AgentsApi {
configure(
contribution: AgentConfigurationContribution,
): { dispose(): void };
contributeInstructions(
provider: (
context: AgentConfigurationContext,
) => string | null | Promise<string | null>,
): { dispose(): void };
interceptToolCalls(
interceptor: AgentToolCallInterceptor,
): { dispose(): void };
}The contribution ID needs only plugin-local uniqueness.
The runtime forms the full item ID as <pluginId>/<contributionId>.
The contract inspector shows this full ID.
The resolver applies these rules in list order.
| Field | Merge rule |
|---|---|
scope |
The resolver ignores a contribution outside its scope. |
tools.exclude |
The names leave the active tool set. |
tools.include |
The names enter the active tool set. A later include can restore a name. |
tools.include[].parameters |
The schema override applies only to this turn. It cannot widen the tool's validated input. |
skills |
The resolver keeps the last equal {rootId, path} reference. |
instructions |
prepend adds text first. append adds text last. replace clears prior text first. |
env |
A later value wins. The key must start with BB_. |
contributeInstructions supplies an append contribution.
It gives the contribution a stable generated ID.
A resolver error removes only that contribution from the current turn.
The turn continues and the runtime records the error against its plugin.
bb.agents.toolCalls
This chain wraps every call after the runtime selects a tool winner. An interceptor can inspect input, replace input, call the next member, or return a result. It cannot change the selected tool name.
export interface AgentToolCall {
callId: string;
tool: {
name: string;
claimantPluginId: string;
description: string;
};
input: unknown;
context: AgentToolContext;
}
export interface AgentToolCallPatch {
input?: unknown;
}
export type AgentToolCallNext = (
patch?: AgentToolCallPatch,
) => Promise<AgentToolResult>;
export interface AgentToolCallInterceptor {
id: string;
intercept(
call: AgentToolCall,
next: AgentToolCallNext,
): Promise<AgentToolResult>;
}The manifest claims the chain contract. The server factory provides one member.
{
"claims": [
{ "service": "bb.agents.toolCalls", "version": "1.0.0" }
]
}api.agents.interceptToolCalls({
id: "redact-secrets",
async intercept(call, next) {
const input = redactKnownSecrets(call.input);
return next({ input });
},
});The runtime lets each member call next once.
A direct result stops the chain.
A thrown error becomes an error tool result with interceptor attribution.
The inspector shows the active order and lets the user change it.
bb.agents.skills
The skills service owns workspace skill files and registry operations. It uses stable identities instead of raw absolute paths at the service boundary.
export interface SkillIdentity {
rootId: string;
path: string;
}
export interface SkillSummary extends SkillIdentity {
name: string;
description: string;
scope: "user" | "project";
revision: string;
}
export interface SkillContent extends SkillSummary {
content: string;
}
export interface SkillFile {
path: string;
size: number;
}
export interface SkillRegistrySource {
id: string;
repository: string;
revision?: string;
}
export interface RegistrySkill {
id: string;
name: string;
description: string;
source: SkillRegistrySource;
}
export interface RegistrySkillPage {
items: readonly RegistrySkill[];
nextCursor: string | null;
}
export interface AgentSkillsRegistryService {
search(input?: {
query?: string;
cursor?: string;
limit?: number;
}): Promise<RegistrySkillPage>;
get(input: { id: string }): Promise<RegistrySkill>;
detail(input: {
source: SkillRegistrySource;
}): Promise<{ source: SkillRegistrySource; readme: string | null }>;
entries(input: {
source: SkillRegistrySource;
}): Promise<{ entries: readonly RegistrySkill[] }>;
install(input: {
id: string;
projectId?: string;
rootId?: string;
}): Promise<{ skill: SkillSummary; filePath: string }>;
repositoryStars(input: {
repository: string;
}): Promise<{ stars: number | null }>;
}
export interface AgentSkillsCliService {
status(input?: {
hostId?: string;
}): Promise<{
hosts: readonly {
hostId: string;
installed: boolean;
revision: string | null;
}[];
}>;
install(input: {
hostId?: string;
}): Promise<{
hosts: readonly {
hostId: string;
installedPath: string;
revision: string;
}[];
}>;
}
export interface AgentSkillsService {
list(input: {
projectId?: string;
scope?: "user" | "project";
}): Promise<readonly SkillSummary[]>;
getContent(input: SkillIdentity): Promise<SkillContent>;
listFiles(input: SkillIdentity): Promise<readonly SkillFile[]>;
update(input: SkillIdentity & {
content: string;
expectedRevision?: string;
}): Promise<{ filePath: string; revision: string }>;
remove(input: SkillIdentity & {
expectedRevision?: string;
}): Promise<{ deletedPath: string }>;
readonly registry: AgentSkillsRegistryService;
readonly cli: AgentSkillsCliService;
}The default service rejects a path that escapes its declared root.
update uses the optional revision for compare-and-set behavior.
A winner change restarts required consumers and preserves kernel-backed skill files.
A server plugin consumes the current skills winner through a required edge.
{
"requires": [
{ "service": "bb.agents.skills", "range": "^1" }
]
}import { bbAgentSkills } from "@bb/agents/contracts";
const skills = await api.services.use(bbAgentSkills);
const available = await skills.list({ projectId: "project_123" });Exports
Module imports select the exact first-party implementation. Service tokens select the user's current winner.
| module | exports | use |
|---|---|---|
@bb/agents/contracts |
bbAgentTool, bbAgentAi, bbAgentConfiguration, bbAgentToolCalls, bbAgentSkills |
Typed service tokens and contract types. |
@bb/agents/server |
bbAgentTools, bbAgentAi, bbAgentConfiguration, and definition helpers |
Typed domain helpers for server plugins. |
@bb/agents/host |
defineAiHostRole, AI host input schemas, AI host output schemas |
Shared shapes for a plugin-private AI host role. |
@bb/agents/skills |
parseSkillManifest, validateSkillRelativePath |
Pure skill helpers. |
bb.agents exports no React component.
bb.threads owns the timeline components that display agent tool calls.
Host roles
An AI service can run on the server or use a host artifact.
The server half still owns the public bb.agents.ai key.
The host artifact fulfills a private role with the plugin's namespace.
The shared module supplies this role shape.
The role ID in this example is acme.local-ai.host.
export interface AiInferenceHostInput extends AiInferenceRequest {
serviceId: string;
}
export interface AiVoiceHostInput extends AiVoiceRequest {
serviceId: string;
}
export interface AiHostCommands {
"inference.complete": {
input: AiInferenceHostInput;
output: AiInferenceResult;
};
"voice.transcribe": {
input: AiVoiceHostInput;
output: AiVoiceResult;
};
}
export const aiHostRole = defineHostRole<AiHostCommands>(
"acme.local-ai.host",
"1.0",
);{
"artifacts": {
"server": "./dist/server.js",
"host": "./dist/host.js"
},
"hostRoles": [
{
"id": "acme.local-ai.host",
"version": "1.0.0",
"contract": "./src/ai-host-contract.ts#aiHostRole",
"required": true
}
]
}// host.ts
export default defineHostPlugin((api) => {
api.roles.provide(aiHostRole, {
async "inference.complete"(input, context) {
return completeLocally(input, context.signal);
},
async "voice.transcribe"(input, context) {
return transcribeLocally(input, context.signal);
},
});
});The host role uses explicit typed verbs.
The server call always selects a host and carries an abort signal.
The inspector joins the private role with the public AI service record.
The host contract replaces ExperimentalAiServicesHostContract and its old gap alias.
Example
This plugin adds a repository facts tool and limits it to Acme projects. It also adds a tool-call audit member.
// bb.plugin.jsonc
{
"id": "acme.repo-facts",
"version": "2.0.0",
"claims": [
{
"service": "bb.agents.tool",
"version": "1.0.0",
"key": "acme_repo_facts"
},
{ "service": "bb.agents.configuration", "version": "1.0.0" },
{ "service": "bb.agents.toolCalls", "version": "1.0.0" }
],
"requires": [
{ "service": "bb.agents.tool", "range": "^1" },
{ "service": "bb.agents.configuration", "range": "^1" },
{ "service": "bb.agents.toolCalls", "range": "^1" }
],
"artifacts": {
"server": "./dist/server.js"
}
}// server.ts
import { z } from "zod";
import { defineServerPlugin } from "@get-bb/plugin/server";
const parameters = z.object({
question: z.string().min(1),
});
export default defineServerPlugin((api) => {
api.tools.add({
name: "acme_repo_facts",
description: "Answer one question from the current repository index.",
parameters,
presentation: {
label: { pending: "Reading repository facts", completed: "Read repository facts" },
icon: { glyph: "BookOpen" },
intent: "read",
},
async execute({ question }, context) {
const answer = await repositoryFacts.answer({
projectId: context.projectId,
question,
signal: context.signal,
});
return answer;
},
});
api.agents.configure({
id: "acme-projects",
resolve(context) {
const remote = context.project.gitRemoteUrl;
if (!remote?.includes("github.com/acme/")) return null;
return {
tools: { include: ["acme_repo_facts"] },
instructions: {
mode: "append",
text: "Use acme_repo_facts for questions about repository policy.",
},
};
},
});
api.agents.interceptToolCalls({
id: "audit-repo-facts",
async intercept(call, next) {
if (call.tool.name !== "acme_repo_facts") return next();
api.log.info(`repo facts call ${call.callId}`);
return next();
},
});
});The loader validates all three static claims before it starts the factory. The factory stages all three registrations in one atomic commit. An unload removes the tool, configuration item, and interceptor together.
Covers
| old item ID | new contract/verb | note |
|---|---|---|
server.agents |
api.agents and api.tools |
The direct APIs provide configuration, interception, and keyed tool registration. |
server.agents.configure |
api.agents.configure() |
A list contribution resolves configuration for each turn. |
server.agents.registerTool |
api.tools.add() |
The call provides one bb.agents.tool key. |
server.agents.contributeInstructions |
api.agents.contributeInstructions() |
The shortcut adds an append configuration contribution. |
server.agents.toolRegistration |
bb.agents.tool: AgentTool |
AgentTool holds metadata, parameters, and execution. |
server.agents.toolPresentation |
bb.agents.tool: AgentToolPresentation |
The presentation remains data for the timeline owner. |
server.agents.toolContext |
bb.agents.tool: AgentToolContext |
The context adds environment, host, and provider IDs. |
server.agents.configuration |
bb.agents.configuration: AgentConfiguration |
The list resolver combines this shape. |
server.experimental_aiServices |
api.ai |
The stable API registers bb.agents.ai keys. |
server.experimental_aiServices.register |
api.ai.add() |
The call provides one keyed AI service. |
server.experimental_aiServices.declaration |
bb.agents.ai: AiService |
The public service joins metadata and methods. |
server.agents.toolRegistration.name |
bb.agents.tool: AgentTool.name |
The name is the keyed contract key. |
server.agents.toolRegistration.description |
bb.agents.tool: AgentTool.description |
The model reads this description. |
server.agents.toolRegistration.instructions |
bb.agents.tool: AgentTool.instructions |
Active tool instructions join session instructions. |
server.agents.toolRegistration.parameters |
bb.agents.tool: AgentTool.parameters |
Standard Schema and JSON Schema forms remain available. |
server.agents.toolRegistration.execute |
bb.agents.tool: AgentTool.execute() |
The selected winner handles the call. |
server.agents.toolPresentation.label |
bb.agents.tool: AgentToolPresentation.label |
Pending and completed labels remain. |
server.agents.toolPresentation.icon |
bb.agents.tool: AgentToolPresentation.icon |
The glyph remains host-rendered data. |
server.agents.toolPresentation.suppress |
bb.agents.tool: AgentToolPresentation.suppress |
The default-collapse hint remains. |
server.agents.toolPresentation.tint |
bb.agents.tool: AgentToolPresentation.tint |
Light and dark tint values remain. |
server.agents.configuration.tools |
bb.agents.configuration: AgentToolSet |
The old array becomes the include list with an explicit exclude list. |
server.agents.configuration.skills |
bb.agents.configuration: AgentConfiguration.skills |
Values use stable {rootId, path} references. |
server.agents.configuration.instructions |
bb.agents.configuration: AgentInstructions |
The shape adds an explicit merge mode. |
server.agents.toolSelection.name |
bb.agents.configuration: AgentToolSelection.name |
The selection names one bb.agents.tool key. |
server.agents.toolSelection.parameters |
bb.agents.configuration: AgentToolSelection.parameters |
The per-turn schema restriction remains. |
server.experimental_aiServices.declaration.id |
bb.agents.ai: AiService.id |
The ID is the keyed contract key. |
server.experimental_aiServices.declaration.displayName |
bb.agents.ai: AiService.displayName |
The user-facing label remains. |
server.experimental_aiServices.declaration.kinds |
bb.agents.ai: AiService.kinds |
The list declares inference and voice methods. |
server.agents.toolResult |
bb.agents.tool: AgentToolResult |
Text and image content remain. |
server.agents.toolLabels |
bb.agents.tool: AgentToolLabels |
Pending and completed labels remain a pair. |
server.agents.configurationContext |
bb.agents.configuration: AgentConfigurationContext |
The stable session context remains available. |
server.sdk.skills |
bb.agents.skills service |
The named service replaces the broad SDK area. |
server.sdk.skills.getContent |
bb.agents.skills service: getContent() |
The method reads one skill. |
server.sdk.skills.list |
bb.agents.skills service: list() |
The method lists skill summaries. |
server.sdk.skills.listFiles |
bb.agents.skills service: listFiles() |
The method lists files under one skill. |
server.sdk.skills.remove |
bb.agents.skills service: remove() |
The method removes one skill by stable identity. |
server.sdk.skills.update |
bb.agents.skills service: update() |
The method writes content with an optional revision check. |
server.sdk.skills.registry |
bb.agents.skills service: registry |
The nested service owns registry operations. |
server.sdk.skills.registry.detail |
bb.agents.skills service: registry.detail() |
The method reads source details. |
server.sdk.skills.registry.entries |
bb.agents.skills service: registry.entries() |
The method lists source entries. |
server.sdk.skills.registry.get |
bb.agents.skills service: registry.get() |
The method reads one registry skill. |
server.sdk.skills.registry.install |
bb.agents.skills service: registry.install() |
The method installs one registry skill. |
server.sdk.skills.registry.repositoryStars |
bb.agents.skills service: registry.repositoryStars() |
The method reads repository stars. |
server.sdk.skills.registry.search |
bb.agents.skills service: registry.search() |
The method searches the registry. |
server.sdk.system.cliSkillsStatus |
bb.agents.skills service: cli.status() |
Skill install state moves out of the system SDK area. |
server.sdk.system.installCliSkills |
bb.agents.skills service: cli.install() |
CLI skill installation moves to the skill owner. |
host.ai.contract |
<pluginId>.aiHost role: AiHostCommands |
A private typed role replaces the shared experimental host contract. |
host.ai.errorCode |
@bb/agents/host: AiServiceErrorCode |
The stable error code union remains. |
host.ai.inference.input |
<pluginId>.aiHost role: AiInferenceHostInput |
The request keeps the service ID for host dispatch. |
host.ai.inference.output |
<pluginId>.aiHost role: AiInferenceResult |
The structured result and classified failure remain. |
host.ai.inference.complete |
<pluginId>.aiHost role: inference.complete |
The explicit host verb remains. |
host.ai.voice.input |
<pluginId>.aiHost role: AiVoiceHostInput |
The request keeps audio, file, prompt, and timeout data. |
host.ai.voice.output |
<pluginId>.aiHost role: AiVoiceResult |
The text result and classified failure remain. |
host.ai.voice.transcribe |
<pluginId>.aiHost role: voice.transcribe |
The explicit host verb remains. |
host.api.aiServices |
api.ai |
The stable direct API replaces the experimental property. |
host.api.aiServices.register |
api.ai.add() |
The server half provides a host-backed AI service. |
host.ai.declaration |
bb.agents.ai: AiService |
One public keyed service replaces the declaration-only form. |
host.ai.declaration.id |
bb.agents.ai: AiService.id |
The ID selects the keyed winner. |
host.ai.declaration.displayName |
bb.agents.ai: AiService.displayName |
The label remains. |
host.ai.declaration.kinds |
bb.agents.ai: AiService.kinds |
The list must match the service method groups. |
host.experimental_aiServicesHostContract |
<pluginId>.aiHost role: AiHostCommands |
This gap ID maps to the same private role as host.ai.contract. |
server.sdk.system.transcribeVoice |
bb.agents.ai: AiService.voice.transcribe() |
The selected AI service transcribes the recording. |