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.

A pre-built agent is a starting persona, not a black box. The factory hands back an ordinary AgentDef — the same value defineAgent produces — so you can inspect it, spread it, override its personality or modelId, add delegates, or drop it into a bundle. The value it adds over hand-writing one is the curated skill manifest: a reviewed, pinned set of third-party technique skills (from skills.sh) that you import once into your own store, plus a fail-loud tool contract that refuses to construct the agent until you have wired the tools it genuinely needs.

PackageFactoryPersona
agent-researchercreateResearcherweb/corpus research; a required webSearch tool you inject
agent-marketercreateMarketerpositioning + campaign copy (also a createMarketingCrew bundle)
agent-designercreateDesignerdesign direction and critique
agent-writercreateWriterlong-form drafting and editing
agent-buildercreateBuildera meta-agent that imports skills and publishes agents/bundles — every mutation gated
agent-kit(shared)the import / verify / lifecycle machinery the personas rely on — not an agent itself

Adopt one, or write your own? Reach for a pre-built agent when the role is a well-trodden one and you want a reviewed skill set and a sane persona on day one — and you are happy to keep the package updated. Write your own with defineAgent when the role is domain-specific, when the persona is your product's voice, or when you do not want a third-party skill dependency. The two are not exclusive: a common path is to adopt a persona, then override its personality and add first-party tools. The researcher even ships a corpusOnly mode that drops all live-web tools and makes a knowledgeSearch tool required instead — a grounded, degraded persona for hosts with no web-search tool to inject.

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)

The manifest names skills; it does not ship their text. Importing pulls each reviewed snapshot from its provider into your versioned store as fenced third-party reference — a governed, one-time step per tenant, and again after a package upgrade. Pins are strict by default: an upstream that drifted from the reviewed snapshot is skipped for review, and a renamed skill is rejected outright. Then verifyGrants closes the silent-miss gap — a granted name with nothing behind it would inject nothing and fail quietly at run time.

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

defineSwarm seeds code-defined agents into an in-memory store only — a persisted agent repo (the Supabase adapter) needs an explicit lifecycle so restarts do not churn versions. ensurePrebuiltAgent is the idempotent boot step: seed-if-absent, no new immutable version across restarts. After you upgrade the package, publishPrebuiltAgent cuts a new version deliberately, and the prior version stays available for rollback.

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

The agent-builder is the sharpest of the six because it can change your swarm's own definitions. Its three mutating tools — import_skill, publish_agent, publish_bundlesuspend for human approval on every call, an enforce-level rule a permissive host hook cannot downgrade, and each runs under your own service actor with a fail-closed policyGuard. The prerequisite is that your host handles the suspend/resume round-trip: a @nightowlsdev/react host renders the approval as a question card automatically; a headless host resumes through the engine API.

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
});

Sharp edges, in one place

Every one of these is a fail-loud contract, not a silent trap — but each is a place a first integration stalls. The design bias is deliberate: a pre-built agent would rather refuse to construct or refuse to run than come up half-wired.

  • The factory throws on a missing required tool. There is no framework web search — createResearcher refuses to construct until you inject a webSearch tool (an MCP server, a custom defineTool, or a connector action), or opt into corpusOnly with a knowledgeSearch tool. That is the fail-loud contract working, not a bug.
  • A tier config is required. Factories default modelId to the sentinel "tier:", never a vendor pin — so without a models.tier map (and the tier models in models.allow) the agent fails the allow-list at run time.
  • Stored skills need the dynamicSkills seam. Importing a curated skill puts text in your store, but nothing reaches a prompt until you wire dynamicSkills: materializeSkillStore(storage.skills) on the swarm. Skip it and a granted skill is a silent no-op — which is exactly what verifyGrants is there to catch.
  • Re-import after an upgrade. The manifest is pinned to a reviewed snapshot, so a package bump can move the pins. Re-run importCuratedSkills after upgrading a package, and publish the new persona version deliberately.
  • The persisted lifecycle is yours to call. defineSwarm's seed hook is in-memory only; on a Supabase store you must call ensurePrebuiltAgent at boot or the persona never lands in the repo.
  • Read-only tools still gate on strict hosts. On anall-side-effecting approval mode, add PREBUILT_READONLY_TOOL_NAMES to the readOnly allowlist, or a designer reading its own skill library will suspend on every harmless read.