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.
| Package | Factory | Persona |
|---|---|---|
| agent-researcher | createResearcher | web/corpus research; a required webSearch tool you inject |
| agent-marketer | createMarketer | positioning + campaign copy (also a createMarketingCrew bundle) |
| agent-designer | createDesigner | design direction and critique |
| agent-writer | createWriter | long-form drafting and editing |
| agent-builder | createBuilder | a 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
pnpm add @nightowlsdev/agent-researcher @nightowlsdev/agent-kit @nightowlsdev/skills
# every agent package peers on @nightowlsdev/core + agent-kit + skills, one engine, no duplicatesimport { 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.
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.
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.
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_bundle — suspend 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.
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.
Where to go next
The packages
Per-package docs: agent-kit, agent-builder, agent-researcher, agent-marketer, agent-designer, agent-writer.
Skills from anywhere
The governed import/refresh seam under the manifests: @nightowlsdev/skills.
Source
Night Owls is MIT licensed on GitHub.