Skip to content
Night Owls.dev
Jump to a page
Guide

Adopt a pre-built agent, the complete journey, once

Six packages ship ready-made agents: a researcher, marketer, designer, writer, the agent-builder, and the kit they share. Each is a versioned persona plus a curated skills.sh manifest pinned to reviewed snapshots, and a fail-loud contract: you wire the tools, the factory throws on what's missing.

What you're adopting

A factory (createResearcher, createDesigner, and so on) returns a plain AgentDef, plus a manifest naming its curated external skills and required tools. Nothing runs at import time; nothing phones home. The curated skills live in YOUR versioned store after a governed import, fenced as third-party reference, pinned to the snapshot that was reviewed.

Install + storage

terminal
pnpm add @nightowlsdev/agent-researcher @nightowlsdev/agent-kit @nightowlsdev/skills

# every agent package peers on @nightowlsdev/core + agent-kit + skills, one engine, no duplicates
storage.ts
import { createSupabaseStorage } from "@nightowlsdev/storage-supabase";
import { skillsShProvider } from "@nightowlsdev/skills";

// The versioned skill store (migration 0020) + the versioned agent repo ship in the adapter.
export const storage = createSupabaseStorage({ url, secretKey, dbUrl });

// One provider registry, keyed by provider id, skills.sh here; github/http/custom also ship.
export const skillProviders = { "skills.sh": skillsShProvider() };

Import the curated skills (once per tenant)

seed.ts
import { importCuratedSkills, verifyGrants } from "@nightowlsdev/agent-kit";
import { manifest } from "@nightowlsdev/agent-researcher";

// Run ONCE per tenant (a seed script or admin action), and again after package upgrades.
// Strict pins by default: a drifted upstream is SKIPPED for review; a renamed one is rejected.
const report = await importCuratedSkills({
  sets: manifest.curatedSkills,
  providers: skillProviders,
  storage, // { skills, skillsWritable }
  tenantId,
  actor: { type: "service", serviceId: "seed", tenantId },
});

// The silent-miss check: a granted stored name with nothing behind it injects nothing.
const { missing } = await verifyGrants(
  storage.skills,
  tenantId,
  manifest.curatedSkills.flatMap((s) => s.skills.map((r) => r.name)),
);
if (missing.length) console.warn("import the curated skills before first use:", missing);

Wire the swarm

Three things are load-bearing: dynamicSkills (stored skills inject at runtime), a models.tier config (factories default to "tier:"), and the approval allowlist for strict hosts.

swarm.ts
import { defineSwarm, DEFAULT_READ_ONLY_TOOLS } from "@nightowlsdev/core";
import { materializeSkillStore } from "@nightowlsdev/skills";
import { PREBUILT_READONLY_TOOL_NAMES } from "@nightowlsdev/agent-kit";
import { createResearcher } from "@nightowlsdev/agent-researcher";

// The factory FAILS LOUD on missing required tools, there is no framework web search;
// you inject one (an MCP server, a custom defineTool, or a connector action).
const researcher = createResearcher({ tools: { webSearch: myWebSearchTool } });

const swarm = defineSwarm({
  storage,
  agents: [researcher],

  // REQUIRED for stored-skill grants: without this seam, an imported skill never reaches a prompt.
  dynamicSkills: materializeSkillStore(storage.skills),

  // Factories default to modelId "tier:" (never a vendor pin), so a tier config is REQUIRED
  // (without one, "tier:" fails the model allow-list, loudly, at run time).
  models: {
    allow: ["openai/gpt-5.5-mini", "openai/gpt-5.5"],
    tier: { tiers: { swift: "openai/gpt-5.5-mini", genius: "openai/gpt-5.5" }, default: "swift" },
  },
  modelFactory,
  cost: { maxSteps: 30, maxCostUsd: 0.5 },

  // Strict approval hosts: the prebuilt READ-ONLY tools ride the allowlist so a designer reading
  // its skill library doesn't suspend on every read. Egress/mutating tools still gate.
  toolApproval: {
    mode: "all-side-effecting",
    readOnly: [...DEFAULT_READ_ONLY_TOOLS, ...PREBUILT_READONLY_TOOL_NAMES],
  },
});

Seed and upgrade on a persisted store

lifecycle.ts
import { ensurePrebuiltAgent, publishPrebuiltAgent } from "@nightowlsdev/agent-kit";

// defineSwarm's seed hook is in-memory-only, a persisted store needs an explicit lifecycle.
// Boot: seed-if-absent (idempotent, NO version churn across restarts).
await ensurePrebuiltAgent({
  agents: storage.agents,
  agentsWritable: storage.agentsWritable,
  def: researcher,
  tenantId,
  actor: { type: "service", serviceId: "boot", tenantId },
});

// After a package upgrade: publish DELIBERATELY, a new immutable version; rollback stays available.
await publishPrebuiltAgent({ agentsWritable: storage.agentsWritable, def: upgradedResearcher, tenantId, actor });

Approvals and the agent-builder

builder.ts
import { createBuilder } from "@nightowlsdev/agent-builder";

// The agent-builder's mutating tools (import_skill / publish_agent / publish_bundle) SUSPEND for
// human approval on every call, an enforce-level rule a permissive host hook cannot downgrade.
// PREREQUISITE: your host handles the suspend/resume round-trip (every @nightowlsdev/react host
// already does, the approval renders as a question card; headless hosts resume via the engine API).
const builder = createBuilder({
  providers: skillProviders,
  storage: {
    skills: storage.skills, skillsWritable: storage.skillsWritable,
    agents: storage.agents, agentsWritable: storage.agentsWritable,
    bundles: storage.bundles, bundlesWritable: storage.bundlesWritable, // needed for publish_bundle
  },
  actor: { type: "service", serviceId: "builder-host", tenantId }, // YOUR service actor
  policy: { allowAuthors: ["anthropics", "vercel-labs", "coreyhaines31"] }, // import allowlists
  policyGuard: (op) => assertOrgAllows(op), // fail-closed; the agents-can't-mutate storage bar stays intact
});