Author a capability once. Reuse it everywhere.
A capability bundle packages a working slice of a swarm, its agents, their rules, workflows, and connector grants, into one reusable, versioned unit. Compose it in code, publish immutable versions, apply it into a tenant's live agents, and upgrade downstream, without trampling a tenant's own tuning.
What a bundle is
Think of it as an npm package, but for a working piece of a swarm instead of a function. A bundle composes multiple agents with their personas, models, skills, and delegate wiring, plus their rules, workflows, and connector grants. You author it once and reuse it across projects and tenants, then evolve it once and upgrade everywhere downstream.
Not to be confused with a skill. A defineSkill is also sometimes called a capability bundle, but that packages instructions, tools, rules, and workflows for one agent. A defineBundle composes multiple agents. Different unit.
Compose it, in code
At the source level a bundle is just code you import and pass to defineSwarm, so its rules and workflows keep their normal runtime home and its skill handles are present in-process. Composition is closure-validated at author time, no runtime surprises.
import { defineAgent, defineBundle, mergeBundle, defineSwarm } from "@nightowlsdev/core";
// A bundle composes defineAgent OUTPUTS + their rules/workflows + connector grants.
export const contentStudio = defineBundle({
slug: "content-studio",
agents: [editor, reviewer],
connectorGrants: [
// names only, never a token. The host resolves per-tenant credentials at call time.
{ agentSlug: "editor", provider: "slack", actions: ["post_message"] },
],
});
// Drop it into any swarm. Closure-validated at author time: a typo in a skill,
// delegate, rule, or workflow reference throws HERE, not as a runtime run_failed.
const swarm = defineSwarm(mergeBundle(baseConfig, contentStudio));Publish and version it
To distribute a bundle across tenants, serialize it to an immutable, append-only version in your Postgres. A published version can never be edited, history only moves forward, and a rollback republishes a prior snapshot as a new head.
import { toBundleContent } from "@nightowlsdev/core";
// Publish an immutable, append-only version a host can apply to many tenants later.
await store.bundlesWritable.publish(orgId, "content-studio", toBundleContent(contentStudio), actor);
// Read the head, a specific version, or the full history.
await store.bundles.head(orgId, "content-studio");
await store.bundles.listVersions(orgId, "content-studio"); // v1, v2, ...
// Roll the head back to an earlier version (records a NEW audit row, git revert, not reset).
await store.bundlesWritable.rollback(orgId, "content-studio", 1, actor);Apply into a tenant's live agents
Materialize a version into a tenant's running agents. Apply is same-org, content-aware (a member already at the target is left untouched, no version churn), and fail-closed: you pass a handle manifest, and if the bundle references a skill this deployment can't resolve, the whole apply aborts rather than silently shipping a no-op tool.
import { applyBundle } from "@nightowlsdev/storage-supabase";
// The host asserts which skill names actually resolve in THIS deployment (fail-closed).
const manifest = { has: (name: string) => myHostRegistry.has(name) };
// Materialize the bundle into the tenant's running agents, in one transaction.
const r = await applyBundle(store.ctx, {
tenantId, slug: "content-studio", version: 1, actor, manifest,
});
// -> { applied: [{ slug: "editor", version: 1 }, ...], unchanged: [], skipped: { rules, workflows } }
// A missing skill name aborts the WHOLE apply, no half-applied bundle.Upgrade downstream, without clobbering local tuning
This is the point of the whole thing. When you publish a new version, upgrade a tenant from its pinned version to the latest. For each member that changed, a divergence guard compares the tenant's live agent against the snapshot last applied. If it drifted, a manual edit, or an automatic optimization pass, the upgrade skips that member and reports it as diverged rather than overwriting the tenant's tuning. You decide, per member, whether to keep it or take the new baseline.
import { upgradeBundle } from "@nightowlsdev/storage-supabase";
// After you publish content-studio v2, roll each tenant forward.
const r = await upgradeBundle(store.ctx, {
tenantId, slug: "content-studio", actor, manifest,
});
// -> {
// fromVersion: 1, toVersion: 2, pinAdvanced: true,
// applied: [{ slug: "editor", version: 2 }], // only the members that changed
// diverged: [], // members whose live head drifted since apply (GEPA / manual), SKIPPED
// unsupportedRemovals: [], // a member the new version drops (agents aren't deleted in v1)
// skipped: { rules: false, workflows: false },
// }
// A diverged member is never clobbered. Consent to overwrite it explicitly:
await upgradeBundle(store.ctx, { tenantId, slug: "content-studio", actor, manifest,
confirmDiverged: ["editor"] });Example use case
You run an agency platform. You've perfected a two-agent Content Studio, an editor that drafts and can post to Slack, and a reviewer that fact-checks, for one client.
- Publish the crew as bundle content-studio v1.
- Onboard 50 tenants by calling applyBundle in a loop, each gets the editor + reviewer as live agents, Slack wired per-tenant, and the manifest guarantees no unresolved skills slip through.
- Improve the editor and publish content-studio v2.
- Roll it out with upgradeBundle per tenant. Clean tenants move to v2. But the 3 tenants whose editor was auto-optimized against their own traffic come back as diverged, so you never destroy a locally-tuned agent with a generic push. You choose per-tenant.
Central reuse and evolution, without breaking per-tenant self-improvement. That balance is the reason bundles exist.