Skip to content

fix(capabilities): enable dynamic capability discovery and mid-conversation delegation#330

Merged
zhubzy merged 1 commit intomainfrom
feat/multi-credential
Mar 20, 2026
Merged

fix(capabilities): enable dynamic capability discovery and mid-conversation delegation#330
zhubzy merged 1 commit intomainfrom
feat/multi-credential

Conversation

@zhubzy
Copy link
Contributor

@zhubzy zhubzy commented Mar 20, 2026

Summary

  • Changed use-capability tool schema from z.enum(capIds) to z.string() so dynamically-added capabilities can be delegated to mid-conversation
  • Added fallback capConfig construction for capabilities added via manage_capability that aren't in the startup capability list
  • Broadened delegation rules to require get_capabilities check before telling users a task can't be done

Context

Pearl accuracy testing (0/10 → 10/10) revealed three cascading issues when a user asks for a task that requires an unattached capability:

  1. Pearl wouldn't call get_capabilities to discover available capabilities (fixed in capability-pipeline.ts)
  2. After adding a capability, Pearl would stop because the response said "takes effect on next conversation" (fixed in Pro)
  3. Even when Pearl tried to delegate, use-capability's z.enum rejected the dynamically-added capability ID (fixed here)

Test plan

  • Pearl accuracy test: 10/10 pass rate (discover → add → delegate → sub-agent executes)
  • Verify existing multi-capability delegation still works (enum removal shouldn't affect pre-attached caps)
  • Verify invalid capability IDs still get a clear error message

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Chores

    • Bumped package versions to 0.1.246 across the workspace.
  • Improvements

    • Relaxed capability identifier validation to accept any string.
    • Delegation now proceeds with minimal capability info when an exact match isn’t found and reports available capability IDs in errors.
    • Multi-capability delegation will proactively check available capabilities before claiming inability and treats general knowledge questions as direct responses.

Copilot AI review requested due to automatic review settings March 20, 2026 12:26
@coderabbitai
Copy link

coderabbitai bot commented Mar 20, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0e1fe72e-40c1-4c4c-b130-6e4fb36b46a0

📥 Commits

Reviewing files that changed from the base of the PR and between 55445eb and 738d479.

📒 Files selected for processing (9)
  • packages/bubble-core/package.json
  • packages/bubble-core/src/bubbles/service-bubble/ai-agent.ts
  • packages/bubble-core/src/bubbles/service-bubble/capability-pipeline.ts
  • packages/bubble-runtime/package.json
  • packages/bubble-scope-manager/package.json
  • packages/bubble-shared-schemas/package.json
  • packages/create-bubblelab-app/package.json
  • packages/create-bubblelab-app/templates/basic/package.json
  • packages/create-bubblelab-app/templates/reddit-scraper/package.json

📝 Walkthrough

Walkthrough

Bumped several package versions to 0.1.246 and relaxed/adjusted capability delegation logic: use-capability now accepts arbitrary string IDs, fallback minimal capability config is used if lookup fails, error messages list available IDs, and the delegation system prompt requires checking capabilities before refusing.

Changes

Cohort / File(s) Summary
Package Version Updates
packages/bubble-core/package.json, packages/bubble-runtime/package.json, packages/bubble-scope-manager/package.json, packages/bubble-shared-schemas/package.json, packages/create-bubblelab-app/package.json, packages/create-bubblelab-app/templates/basic/package.json, packages/create-bubblelab-app/templates/reddit-scraper/package.json
Bumped package versions/dependency version pins from 0.1.245 → 0.1.246 across packages and templates.
AI Agent Capability Delegation
packages/bubble-core/src/bubbles/service-bubble/ai-agent.ts
Relaxed use-capability tool schema: capabilityId is now z.string() instead of a fixed enum. If capabilityId not found in agent caps, code proceeds with minimal { id: capabilityId } config. Improved error messages to include available capability IDs. Credential/capability lookup and delegation flow adjusted accordingly.
Capability Pipeline Prompt Rules
packages/bubble-core/src/bubbles/service-bubble/capability-pipeline.ts
Updated system prompt: broadened direct-response cases to include general knowledge questions; added explicit rule requiring get_capabilities (no id) when none of the active capabilities cover the user's task before claiming inability; other prompt wording adjusted (credential listing/instructions).

Sequence Diagram(s)

mermaid
sequenceDiagram
participant User
participant Agent
participant CapRegistry as Capabilities
participant Delegate as CapabilityTool
User->>Agent: Request task
Agent->>CapRegistry: find capability by id/name
alt capability found
Agent->>Delegate: call use-capability (with resolved config)
Delegate-->>Agent: response, toolCalls
Agent-->>User: aggregated response
else not found
Agent->>CapRegistry: get_capabilities() -- checks available caps
CapRegistry-->>Agent: list of caps
Agent-->>User: indicates discovery and either delegates or responds

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Poem

🐰
I hopped through code with nimble paws,
Name-first credentials, no numeric claws.
When caps are hidden I peek and see,
Then nudge the agent to ask and be free.
Hooray — 0.1.246, from me, a happy bunny! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly summarizes the main change: enabling dynamic capability discovery and mid-conversation delegation by removing capability ID enum restrictions.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/multi-credential
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

CodeRabbit can scan for known vulnerabilities in your dependencies using OSV Scanner.

OSV Scanner will automatically detect and report security vulnerabilities in your project's dependencies. No additional configuration is required.

@cloudflare-workers-and-pages
Copy link

Deploying bubblelab-documentation with  Cloudflare Pages  Cloudflare Pages

Latest commit: 55445eb
Status: ✅  Deploy successful!
Preview URL: https://fe47b87e.bubblelab-documentation.pages.dev
Branch Preview URL: https://feat-multi-credential.bubblelab-documentation.pages.dev

View logs

@zhubzy
Copy link
Contributor Author

zhubzy commented Mar 20, 2026

Companion PR: bubblelabai/BubbleLab-Pro#179

@zhubzy zhubzy force-pushed the feat/multi-credential branch from 55445eb to 738d479 Compare March 20, 2026 12:31
@zhubzy zhubzy merged commit 7363da2 into main Mar 20, 2026
4 checks passed
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Enables more flexible capability delegation by allowing dynamically-added capabilities (mid-conversation) to be targeted by use-capability, and updates capability prompting to encourage discovery via get_capabilities before claiming a task is unsupported.

Changes:

  • Relaxed use-capability input validation (capabilityId now accepts any string) and added a fallback capConfig for dynamically-added capabilities.
  • Updated credential selection to support choosing accounts by name (in addition to ID), and adjusted capability summaries accordingly.
  • Adjusted sub-agent default reasoning effort and broadened delegation rules in the multi-capability system prompt.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/bubble-core/src/bubbles/service-bubble/ai-agent.ts Updates use-capability schema/behavior (dynamic IDs, credential selectors by name/ID, output shape).
packages/bubble-core/src/bubbles/service-bubble/capability-pipeline.ts Updates delegation/system prompt guidance and sub-agent default reasoning effort.
packages/create-bubblelab-app/package.json Bumps create-bubblelab-app version to 0.1.246.
packages/bubble-core/package.json Bumps @bubblelab/bubble-core version to 0.1.246.
packages/bubble-runtime/package.json Bumps @bubblelab/bubble-runtime version to 0.1.246.
packages/bubble-shared-schemas/package.json Bumps @bubblelab/shared-schemas version to 0.1.246.
packages/bubble-scope-manager/package.json Bumps @bubblelab/ts-scope-manager version to 0.1.246.
packages/create-bubblelab-app/templates/basic/package.json Updates template dependencies to ^0.1.246.
packages/create-bubblelab-app/templates/reddit-scraper/package.json Updates template dependencies to ^0.1.246.

Comment on lines 36 to 41
// Single-cap: sub-agents (multi-cap delegation) default to Gemini 3 Flash + low thinking
const isSubAgent = params.name?.startsWith('Capability Agent: ');
if (isSubAgent) {
params.model.model =
RECOMMENDED_MODELS.GOOGLE_FLAGSHIP as typeof params.model.model;
params.model.reasoningEffort = 'low';
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing the sub-agent default from reasoningEffort = undefined to 'low' will enable provider “thinking”/reasoning mode by default (see model client construction in ai-agent.ts), which can materially increase latency and cost for every delegated capability call. If the goal is still “no thinking” for sub-agents, keep this undefined (or gate it behind an explicit config). If the goal is to improve reliability, consider documenting/measuring the cost impact or limiting it to specific providers/models.

Copilot uses AI. Check for mistakes.
Comment on lines 1674 to +1707
@@ -1684,4 +1684,4 @@
credentials: z
.record(
z.nativeEnum(CredentialType),
z.union([z.string(), z.number()])
@@ -1697,10 +1697,14 @@
const credentialOverrides = input.credentials as
| Record<string, string | number>
| undefined;
const capConfig = caps.find((c) => c.id === capabilityId);
const capConfig = caps.find((c) => c.id === capabilityId) ?? {
id: capabilityId,
};
const capDef = getCapability(capabilityId);
if (!capConfig || !capDef)
return { error: `Capability "${capabilityId}" not found` };
if (!capDef)
return {
error: `Capability "${capabilityId}" not found. Available: ${capIds.join(', ')}`,
};
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The use-capability tool now accepts any string capabilityId and has additional behavior (fallback capConfig, name-based credential selection, revised error message). There doesn’t appear to be automated test coverage exercising these new paths (dynamic capability IDs, unknown IDs, credential selection by name/ID). Adding a focused test would help prevent regressions, especially around delegation correctness and error messages.

Copilot uses AI. Check for mistakes.
credentials: z
.record(
z.nativeEnum(CredentialType),
z.union([z.string(), z.number()])
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

credentials schema now allows any number, including non-integers. Since credential IDs are integers and matching later uses strict equality against c.id, non-integer inputs will never match and will silently fall back to the default credential. Consider constraining the number branch to an int (and/or rejecting other number types) to keep validation aligned with runtime behavior.

Suggested change
z.union([z.string(), z.number().int()])

Copilot uses AI. Check for mistakes.
Comment on lines 1726 to 1745
// Match by name first (string), fall back to ID (number)
let match: (typeof pool)[number] | undefined;
if (typeof credSelector === 'string') {
const sel = credSelector.toLowerCase();
// Exact match first, then substring (handles "email (label)" format)
match = pool.find((c) => c.name.toLowerCase() === sel);
if (!match) {
match = pool.find((c) =>
c.name.toLowerCase().includes(sel)
);
}
}
if (!match && typeof credSelector === 'number') {
match = pool.find((c) => c.id === credSelector);
}
// Also try parsing string as number for ID fallback
if (!match && typeof credSelector === 'string') {
const asNum = Number(credSelector);
if (!Number.isNaN(asNum)) {
match = pool.find((c) => c.id === asNum);
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The credential selection logic falls back to substring matching (includes(sel)) when a string selector isn’t an exact match. This can select the wrong account when multiple credential names share a common substring (e.g., multiple emails/domains), which is risky because it may run actions under an unintended account. Prefer exact (case-insensitive) match only; if there’s no exact match (or there are multiple candidates), return an error listing available account names for that credential type so the caller can disambiguate.

Suggested change
// Match by name first (string), or by ID (number/stringified number).
let match: (typeof pool)[number] | undefined;
if (typeof credSelector === 'string') {
const sel = credSelector.toLowerCase();
const exactNameMatches = pool.filter(
(c) => c.name.toLowerCase() === sel
);
if (exactNameMatches.length === 1) {
match = exactNameMatches[0];
} else if (exactNameMatches.length > 1) {
const matchingNames = exactNameMatches
.map((c) => c.name)
.join(', ');
throw new Error(
`Multiple ${credType} credentials match override "${credSelector}". Matching names: ${matchingNames}`
);
} else {
// No exact name match; try interpreting the selector as an ID.
const asNum = Number(credSelector);
if (!Number.isNaN(asNum)) {
const idMatches = pool.filter((c) => c.id === asNum);
if (idMatches.length === 1) {
match = idMatches[0];
} else if (idMatches.length > 1) {
const matchingNames = idMatches.map((c) => c.name).join(', ');
throw new Error(
`Multiple ${credType} credentials have ID ${asNum}. Matching names: ${matchingNames}`
);
} else {
const availableNames =
pool.map((c) => c.name).join(', ') || '<none>';
throw new Error(
`No ${credType} credential matches override "${credSelector}". Available names: ${availableNames}`
);
}
} else {
const availableNames =
pool.map((c) => c.name).join(', ') || '<none>';
throw new Error(
`No ${credType} credential matches override "${credSelector}". Available names: ${availableNames}`
);
}
}
} else if (typeof credSelector === 'number') {
const idMatches = pool.filter((c) => c.id === credSelector);
if (idMatches.length === 1) {
match = idMatches[0];
} else if (idMatches.length > 1) {
const matchingNames = idMatches.map((c) => c.name).join(', ');
throw new Error(
`Multiple ${credType} credentials have ID ${credSelector}. Matching names: ${matchingNames}`
);
} else {
const availableNames =
pool.map((c) => c.name).join(', ') || '<none>';
throw new Error(
`No ${credType} credential found with ID ${credSelector}. Available names: ${availableNames}`
);

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants