Skip to content
Night Owls.dev

Add agents to the app you already have. One, or a team that delegates.

Night Owls is a TypeScript framework for embedding agents in the product you already ship. Wrap your existing functions as typed tools, define one agent or a whole team, and get durable runs, human approval, memory, and governance without building any of it.

npm i @nightowlsdev/core
See the integration

MIT. Self-hosted. Your keys, your billing, your ops.

agent.tsruns with a model key and nothing else
import { defineTool, defineSkill, defineAgent, runAgent } from "@nightowlsdev/core";
import { openaiModels } from "@nightowlsdev/provider-openai";
import { z } from "zod";

// 1. wrap a function you already ship as a typed tool
const getWeather = defineTool({
  name: "get_weather",
  inputSchema: z.object({ city: z.string() }),
  execute: ({ city }) => weatherApi.today(city),
});

// 2. give it to an agent
const meteorologist = defineAgent({
  slug: "meteorologist",
  role: "specialist",
  personality: "You are a concise weather assistant.",
  skills: [defineSkill({ name: "forecasting", tools: [getWeather] })],
  modelId: "gpt-4o",
});

// 3. run it. ephemeral, in-memory, no database, no server.
const { output } = await runAgent(
  meteorologist,
  "What should I wear in Berlin tomorrow?",
  { modelFactory: openaiModels() },
);
Next.jsfirst-class runner: nine App Router route factories
Reactdrop-in chat UI, precompiled CSS, no Tailwind build in your app
Nodeplain Request to Response handlers() everywhere else
TypeScripttyped tools, typed events, end to end

Bring one model key: anthropic · openai · openrouter · vercel-gateway · groq · ollama (local, $0). Install a package per provider you use; swap or add one by swapping the modelFactory.

One agent in your app, in three moves.

The hero snippet already defined the agent. Wiring it into a running product is a swarm, a route, and a component.

01

Declare the swarm

A swarm is the unit the runner serves: agents, storage, and a model factory. One agent is a perfectly good swarm.

lib/swarm.ts
import { defineSwarm } from "@nightowlsdev/core";
import { support } from "./agents";  // defined exactly like the hero agent

export const swarm = defineSwarm({
  storage,        // storage-supabase in production
  agents: [support],
  modelFactory,
});
02

Mount one route

The runner turns your swarm into App Router handlers and projects the typed event stream to the client.

Identity is resolved server-side only. A forged tenantId or userId in the request body is ignored, and the runId is server-minted.

app/api/swarm/chat/route.ts
import { createNextjsRunner } from "@nightowlsdev/runner-nextjs";
import { swarm, storage, auth } from "@/lib/swarm";

export const { POST } = createNextjsRunner({
  swarm,
  storage,
  auth,                                // your existing auth
  onRunStart: (ctx, { input }) => {    // seed your app context into every run
    ctx.set("tenantId", input.tenantId);
  },
}).chatRoute();
03

Drop in the UI

A styled chat component, or a floating widget, or go headless and keep your own markup.

app/support/page.tsx
import { SwarmProvider, SwarmChat } from "@nightowlsdev/react";
import "@nightowlsdev/react/styles.css";

export default function SupportPage() {
  return (
    <SwarmProvider>
      <SwarmChat />   {/* or <SwarmFab /> for a floating widget */}
    </SwarmProvider>
  );
}

The same API scales from one agent to a team.

Delegation is not a different product. Add specialists, list them as delegates, keep the same runner and the same chat UI.

swarm.ts
import { defineAgent, defineSwarm } from "@nightowlsdev/core";

const support = defineAgent({
  slug: "support",
  role: "specialist",
  personality: "You resolve billing and order questions.",
  skills: [refunds, orders],
  modelId: "claude-sonnet-4-5",
});

export const swarm = defineSwarm({
  storage,
  agents: [support],
  modelFactory,
});
swarm.ts
import { defineAgent, defineSwarm } from "@nightowlsdev/core";

const planner = defineAgent({
  slug: "planner", role: "specialist",
  personality: "You sequence tasks and surface scheduling conflicts.",
  skills: [listTasks, workingDays], modelId: MODEL,
});

const analyst = defineAgent({
  slug: "analyst", role: "specialist",
  personality: "You assess delivery risk and explain the drivers.",
  skills: [assessRisk, listTasks], modelId: MODEL,
});

const coordinator = defineAgent({
  slug: "coordinator", role: "orchestrator",
  personality: "You delegate to planner and analyst and synthesize their work.",
  delegates: ["planner", "analyst"],   // each becomes an agent-<slug> tool
  skills: [listProjects],
  modelId: MODEL,
});

export const swarm = defineSwarm({
  storage,
  agents: [coordinator, planner, analyst],
  modelFactory,
});
your appSwarmChatsupportspecialistchatask() suspends, fail-closed

One specialist, one slug. The same defineSwarm call, the same route, the same component. When the agent needs a human decision it calls ask() and the run suspends until you answer.

your appcoordinatororchestratorplanneragent-planneranalystagent-analystagents as toolsask()bubbles up

Delegates become tools. The coordinator sees agent-planner and agent-analyst as callable tools, fans work out in one turn, and synthesizes the answers. Delegation is depth-capped and cycle-guarded, and a sub-agent can still ask() the user: the question bubbles up as a suspend on the coordinator's run.

Multi-agent delegation runs on the Mastra engine today. The engine-ai-sdk and engine-openai-agents engines are single-agent in v1; delegation lands in v1.1.

What comes built in.

Each capability is a published package you can take or leave. There are 49 of them on npm; take what you need.

Wrap your code as tools

defineTool is a typed wrapper over any async function you already ship: your queries, your REST clients, your business logic. Tools receive { tenantId, userId, runId } and close over their own handles. No rebuild, no data migration.

defineTool({ name: "lookup_order", execute: ({ id }) => orders.byId(id), })

Durable, resumable runs

createBackgroundRunner drives a three-verb DurableBackend seam with Trigger.dev v4 or Vercel Workflow behind it. A turn suspended on a question parks for up to seven days, across process death, at zero compute.

createBackgroundRunner({ swarm, storage, backend, }) // survives deploys and restarts

Human in the loop

The ask tool raises a swarm.question event and a fail-closed gate suspends the run until a human answers. ToolApprovalPolicy covers flagged tools or every side-effecting one. Approvals also travel out-of-band over Slack or email and resume the run on the correlated reply.

ask("Send all 60 drafts?") // suspends the run, fail-closed

Governance and rules

defineRule advises or enforces per swarm, per agent, or per skill, on top of a non-removable policy floor. A CostGovernor holds budgets with a kill switch, and a completion supervisor checks the run actually delivered what was asked.

defineRule({ mode: "enforce", ... }) CostGovernor // budgets + kill switch

Memory and RAG

Thread-scoped working memory plus pgvector semantic recall, with per-agent overrides. Document RAG is separate and explicit: a tenant-scoped search_knowledge tool over your own corpus.

search_knowledge("refund policy") // tenant-scoped, opt-in

Agents from your database

Tools stay in code. Persona, rules, workflow, memory, and skill grants live in a versioned row: publish new behavior with no redeploy, append-only and provenance-tracked. A publish invalidates every instance's cache over Postgres LISTEN/NOTIFY, and rollback is one call.

publishAgentVersion(storage.ctx, { slug, personality: row.persona, ... })

Audience-scoped access

A fourth identity axis beside tenant, user, and capability. AuthContext.audiences is intersected with each agent's audiences at the roster, addressing, resume, and delegation seams. It is fail-closed: a caller with no audience reaches only unrestricted agents.

callerMayReach(ctx, agent) // fail-closed, throws AudienceForbidden

Pull external skills

Import a SKILL.md from skills.sh, GitHub, or any HTTPS URL over the SkillProvider seam. Imported content is pinned and fenced as third-party reference, never a trusted instruction.

importSkill(ctx, { provider, ref }) // pinned, fenced, governed

Measure and improve

@nightowlsdev/eval scores runs with deterministic and LLM-judge scorers; @nightowlsdev/gepa reflects on that feedback to hill-climb better persona fields. Libraries you run, not a hosted service.

runEval(swarm, cases, scorers) // deterministic + llmJudgeScorer

Swappable engine

The run loop sits behind a structural Engine interface. Every engine declares an EngineCapabilities descriptor and the React layer reads it to gate UI affordances honestly. Six engines ship today: three native, three adapters.

defineSwarm({ engine }) // omit it: the default Mastra engine

Don't build every agent from scratch.

Eight open-source packages ship ready-made agents you can drop in today: a researcher, a marketer, a designer, a writer, a read-only SEO crew, a read-only diagnostics crew, an agent that builds other agents, and the agent-kit they all share. Each one is a factory that returns a plain agent definition plus a manifest of curated skills. Nothing runs at import, and nothing phones home.

Or compose your own on the same foundation. ensurePrebuiltAgent seeds an agent into your store once at boot, idempotent across restarts; publishPrebuiltAgent cuts a new immutable version after you upgrade a package, and rollback stays one call away.

The researcher needs a search tool you provide, an MCP server, a defineTool, or a connector action. There is no built-in web search, and a factory fails loud when a required tool is missing. Prebuilt packs ship with no audiences set, so installing one exposes nothing to a restricted caller until you open it.

Define agents in code, or from your database.

An agent is a role, a personality, and a set of skills. Write that in code with defineAgent, or author it as a versioned row your operators edit at runtime. Same shape, two sources of truth.

in code
agents.ts
import { defineAgent } from "@nightowlsdev/core";

const support = defineAgent({
  slug: "support",
  role: "specialist",
  personality: "You resolve billing and order questions.",
  skills: [refunds, orders],
  modelId: "claude-sonnet-4-5",
});

The definition lives in your repo, reviewed in a pull request like any other code.

from your database
publish.ts
import { publishAgentVersion } from "@nightowlsdev/core";

// the same agent as an immutable, versioned row.
await publishAgentVersion(storage.ctx, {
  slug: "support",
  personality: row.persona,
  rules: row.rules,
  workflow: row.workflow,
  memory: row.memory,
  // plus tenantId from your request context
});

Publish new behavior with no redeploy. Versions are append-only, a publish invalidates every instance over Postgres LISTEN/NOTIFY, and rollback is one call.

Both paths shape the same fields: persona, rules, workflow, memory, and grants. ensureAgentVersion seeds a code agent's head once; after that the database head wins, and resetToCode is the deliberate way back.

Database-driven config needs the storage backend; the writable surface is storage.agentsWritable. Capability grants version with every publish. Only who can reach an agent, its audiences, lives on the head row, so a rollback can never widen exposure.

Pull skills from anywhere, safely fenced.

Import a skill from skills.sh, a GitHub repo's SKILL.md, or any HTTPS URL, over one SkillProvider seam. Imported content is pinned to the snapshot you reviewed and fenced as third-party reference, so it guides an agent but never becomes a trusted instruction or grants a tool. A registry groups the sources a deployment offers behind one governance floor a per-call policy can only tighten, checkSkillUpdates reports drift without installing it, and <SkillStudio /> mounts the whole library-and-store screen, where Import is reachable only from a preview, because preview is what mints the token import redeems.

  • skillsShProvider pulls from the skills.sh registry.
  • githubSkillProvider pulls a repo's SKILL.md.
  • httpSkillProvider pulls from any HTTPS URL.

Import runs through an ImportPolicy gate before any network call. Strict pins mean a drifted upstream is skipped for review and a renamed one is rejected. Stored skills reach a prompt only through materializeSkillStore, and the store needs the storage backend.

import-skill.ts
import { skillsShProvider, importSkill } from "@nightowlsdev/skills";

// authorization-gated: a denied caller never triggers the fetch.
await importSkill({
  provider: skillsShProvider(),
  ref: "brand-voice",
  storage,
  tenantId,
  actor,
});

Point your agent at these and it can adopt Night Owls without you.

Night Owls publishes its own adoption skills as SKILL.md files. Import one into a coding agent and it gets the install commands, the wiring, the governance model, and the specific things that trip people up. They parse with this framework's own importer, so the skills that describe Night Owls are importable by Night Owls.

Machine-readable index at /llms.txt.

The chat UI is a component, not a project.

<SwarmChat /> is a component the way an auth provider's sign-in box is a component. Import it, point the provider at your route, ship. Styles come precompiled; there is no Tailwind build in your app. <SwarmFab /> is the same thing as a floating widget.

Prefer your own markup? The /headless export gives you useSwarmChat plus Timeline, MessageBubble, Composer, DelegationCard, and AgentPile. The hook parses the AI SDK stream itself.

the whole integration
import { SwarmProvider, SwarmChat } from "@nightowlsdev/react";
import "@nightowlsdev/react/styles.css";

<SwarmProvider>
  <SwarmChat />
</SwarmProvider>
Back office swarm
coordinatorplanneranalyst
Reconcile May orders against the Stripe payouts and draft correcting entries.
coordinator delegated
  • agent-plannermatched order lines to payoutsdone
  • agent-analystflagged the orphaned ordersdone
coordinator
Both ledgers are matched and I drafted correcting entries for the orphaned orders. The entries are queued, not posted.
swarm.question · fail-closed
Post the correcting entries to the ledger?
The run stays suspended until you answer. Nothing posts without a yes.

A rendition of <SwarmChat /> drawn with this page's stylesheet to mirror the real component's markup. The approval card is live: it demonstrates the fail-closed gate, the same swarm.question event your users answer.

And one console for the people who run them.

<AIStudio /> mounts the whole operator surface from @nightowlsdev/react/studio: agents, skills, knowledge, tools and the graph in one console. A section whose client you do not pass simply does not render, so you get exactly the deployment you wired. Underneath: the agent roster with a full editor, visual rule and workflow builders, tool and skill grants, audiences, version history with a field-level diff, and CAS-safe publish and rollback. <AIStudio /> and <SkillStudio /> are stable, with props frozen on arrival; <AgentStudio /> alone stays experimental and can still be mounted on its own.

app/admin/page.tsx
import {
  AIStudio, createEndpointsClient, createSkillEndpointsClient,
} from "@nightowlsdev/react/studio";

export default function StudioPage() {
  return (
    <AIStudio
      clients={{
        agents: createEndpointsClient({ base: "/api/admin", getToken }),
        skills: createSkillEndpointsClient({ base: "/api/admin", getToken }),
        // knowledge / connections / graph are optional. Pass what you run.
      }}
    />
  );
}
  • AIStudio
  • SkillStudio
  • AgentList
  • AgentEditor
  • PersonaEditor
  • MemoryEditor
  • RuleBuilder
  • WorkflowBuilder
  • AudienceEditor
  • GrantPicker
  • VersionHistory
  • DiffView

The studio is untrusted UI. Every write is authorized server-side by a default-deny admin guard, and the browser never constructs an actor or sends tenant or user ids. readOnly can only ever narrow what the server already permits, never widen it.

Governed by default, not as an add-on.

The controls that make an agent safe to point at your business live in the core, not bolted on. Rules merge most-restrictive-wins, and a database publish can only ever tighten what code already allows.

Non-removable approval gate

The ask tool and toolApproval modes suspend a run, fail-closed, until a human answers. A read-only allowlist keeps safe tools from suspending on every call.

Cost governance

CostGovernor enforces maxSteps and maxCostUsd and stops the run at the cap. DelegateBudgets adds per-delegate sub-budgets.

Capability grants

Tools and actions are gated by an agent's capabilities and skillNames. A grant is versioned content, so it travels with every publish and rollback.

Per-caller audiences

AuthContext.audiences decides who can see and address each agent, a fourth identity axis beside tenant, user, and capability. The host owns the vocabulary and the caller mapping.

Audiences are fail-closed. A caller with no audience reaches only unrestricted agents, a resumed run can only get more restrictive, and AudienceForbidden is thrown before any side effect. An unset caller stays unrestricted for back-compat, but an empty filter is never quietly widened.

Measure a version, then improve it.

Shipping an agent is not the end. Two libraries close the loop: one scores behavior against fixtures, the other reflects on those scores to propose a better prompt.

@nightowlsdev/eval

Drive an agent over a case set and score each run. Deterministic scorers ship built in, no model call. llmJudgeScorer adds qualitative grading when a deterministic check cannot express the bar.

measure.ts
import { runEval, defaultDeterministicScorers, llmJudgeScorer } from "@nightowlsdev/eval";

const report = await runEval({
  suite: "support-bot-regression",
  agentSlug: "support-bot",
  dataset,
  scorers: [...defaultDeterministicScorers, llmJudgeScorer({ name: "helpfulness", rubric, judge })],
  runAgent,
});

@nightowlsdev/gepa

A reflective optimizer. It reads the eval's textual feedback on the worstCases, proposes improved persona fields, evaluates each candidate against the metric, and keeps the best. The base prompt is always the floor.

optimize.ts
import { optimizePrompt } from "@nightowlsdev/gepa";

const result = await optimizePrompt({ base, rounds: 3, evaluate, reflect });

These are libraries you run, not a hosted service. There is no dashboard and no SaaS. GEPA captures the core of the technique, reflection on feedback with metric-gated selection, without the external DSPy or Ax dependency.

Why not just call the model?

You can. Then the loop, the approvals, the state, and the failure modes are yours to build and to keep.

the loop you end up owning
let messages = await loadThread(threadId);
let state = await loadRunState(runId);

while (!finished(state)) {
  const res = await callModel(messages, tools);

  for (const call of res.toolCalls) {
    if (isSideEffecting(call)) {
      await askAHumanSomehow(call);      // UI, persistence, resume token
    }
    await withRetries(() => exec(call)); // your retry policy
  }

  await save(runId, state);              // survives a deploy? sometimes
  // cancellation, cost ceilings, telemetry: also yours
}
the same surface on Night Owls
const swarm = defineSwarm({ storage, agents, modelFactory });

export const { POST } = createNextjsRunner({
  swarm, storage, auth,
}).chatRoute();

// approvals suspend, fail-closed
// runs park and resume across restarts
// budgets, cancellation, telemetry ride along

The loop is swappable. Six engines ship today.

defineSwarm({ engine }) swaps the run loop. Omit it and you get the built-in Mastra engine, byte-identical. Every engine publishes an honest capability descriptor, and the UI gates on it.

enginerun loopmulti-agentgovernanceeventsdurable resume
Native, 3 engines · the loop runs in your process; the full governance plane is reused verbatim: fail-closed tool gate, cost caps, secrets, telemetry
engine-mastra defaultMastra agent loop, in processfull delegation + workflowsfull planetier 3yes, with semantic recall
engine-ai-sdkgoverned AI SDK streamText loopsingle-agent in v1full plane; approvals map to native needsApprovaltier 3yes
engine-openai-agentsgoverned @openai/agents run()single-agent in v1; handoffs become delegation in v1.1full plane, OTeltier 3yes
Adapter, 3 engines · the loop runs on a remote product, so our pre-generation veto, tool gate, and cost caps cannot reach it. The descriptor says so, and the UI gates on it.
engine-a2aany A2A v1.0 endpoint: Bedrock AgentCore, Azure AI Foundry, Google ADKon the remoterelayed, reducedtier 1on the remote
engine-trigger-chata deployed Trigger.dev chat.agent sessionon the remoterelayed, reducedtier 2parks on Trigger's runtime
engine-evea deployed Vercel Eve NDJSON sessionon the remoterelayed, reducedtier 2parks on Vercel Workflows

All six are on npm now. Native means the loop executes in your process with the full Night Owls governance plane. Adapter means the loop fronts a remote runtime with reduced governance and coarser events; each descriptor states exactly what it cannot enforce.

The engine wall

Only @nightowlsdev/core imports the Mastra engine, and no package re-exports a @mastra/* type in its public .d.ts. The boundary is linted in CI. You only ever see Night Owls types, which is exactly what keeps the engine swappable.

What durable means here

durable: true needs a backend, Trigger.dev v4 or Vercel Workflow, wire exactly one, plus storage that persists suspend and resume snapshots, such as storage-supabase. The in-memory store is not durable across a restart, and we will not pretend otherwise.

Start with a model key.

The hero example runs end to end with three commands. Everything past that is an adapter you add when you need it.

shell
$ npm i @nightowlsdev/core @nightowlsdev/provider-openai zod
$ export OPENAI_API_KEY=sk-your-key
$ npx tsx agent.ts
# no database, no server, no config file
  • A chat UI in your app: @nightowlsdev/runner-nextjs + @nightowlsdev/react.
  • Persistence, memory, versioned agents: @nightowlsdev/storage-supabase. Use the Session or Direct port 5432; the 6543 pooler throws.
  • Durable background runs: @nightowlsdev/runner-background plus Trigger.dev v4 or Vercel Workflow.
  • Ready-made agents to drop in: @nightowlsdev/agent-researcher and the rest of the agent-* family.
  • owl install <adapter> scaffolds config, merges env, and contributes migrations. It never runs DDL.

Limits, stated plainly.

  • Multi-agent is the Mastra engine today. engine-ai-sdk and engine-openai-agents are single-agent in v1; delegation lands in v1.1. Multi-agent packs require engine-mastra.
  • Adapter engines run remote. The pre-generation veto, tool gate, and cost kill switch cannot reach a loop that executes on someone else's product. Their descriptors declare it and the UI gates on it. Events are coarser, tier 1 and tier 2.
  • Next.js is the only first-class runner. Everything else wires plain handlers() itself. No Express, Hono, or Fastify adapter ships.
  • Durable runs need real infrastructure. A backend, Trigger.dev v4 or Vercel Workflow, plus storage that persists snapshots. The in-memory store does not survive a restart.
  • Production storage is Supabase and Postgres. storage-local is a CLI bootstrap store only.
  • Imported skills are instruction-only. A SKILL.md pulled from skills.sh, GitHub, or any https URL is fenced and untrusted. It can never grant a tool or override policy.
  • The Agent Studio is experimental. <AgentStudio /> ships from the @nightowlsdev/react/studio subpath, which may change shape in minor releases until it stabilizes. The package root and every other subpath keep normal semver.
  • engine-mastra is a v0 facade. The loop lives in core until core@1.0. Installing it changes imports, not behavior.
  • Proven parts are leaned on, not reinvented. Mastra for the default loop, Trigger.dev and Vercel Workflow for durability, Nango for OAuth.