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/coreMIT. Self-hosted. Your keys, your billing, your ops.
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() },
);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.
Declare the swarm
A swarm is the unit the runner serves: agents, storage, and a model factory. One agent is a perfectly good swarm.
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,
});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.
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();Drop in the UI
A styled chat component, or a floating widget, or go headless and keep your own markup.
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.
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,
});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,
});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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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 { 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.
- nightowls-adopt install, define a swarm, mount the routes and the chat UI.
- nightowls-governance tool approval, human-in-the-loop, rules, audiences.
- nightowls-ai-studio mount the operator console behind a default-deny guard.
- nightowls-skill-store import third-party skills behind a real review gate.
- nightowls-tools-connections connections, the MCP registry, capability audit.
- nightowls-knowledge retrieval over your documents, and the required migration.
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.
import { SwarmProvider, SwarmChat } from "@nightowlsdev/react";
import "@nightowlsdev/react/styles.css";
<SwarmProvider>
<SwarmChat />
</SwarmProvider>- agent-plannermatched order lines to payoutsdone
- agent-analystflagged the orphaned ordersdone
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.
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.
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.
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.
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
}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 alongThe 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.
| engine | run loop | multi-agent | governance | events | durable 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 default | Mastra agent loop, in process | full delegation + workflows | full plane | tier 3 | yes, with semantic recall |
| engine-ai-sdk | governed AI SDK streamText loop | single-agent in v1 | full plane; approvals map to native needsApproval | tier 3 | yes |
| engine-openai-agents | governed @openai/agents run() | single-agent in v1; handoffs become delegation in v1.1 | full plane, OTel | tier 3 | yes |
| 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-a2a | any A2A v1.0 endpoint: Bedrock AgentCore, Azure AI Foundry, Google ADK | on the remote | relayed, reduced | tier 1 | on the remote |
| engine-trigger-chat | a deployed Trigger.dev chat.agent session | on the remote | relayed, reduced | tier 2 | parks on Trigger's runtime |
| engine-eve | a deployed Vercel Eve NDJSON session | on the remote | relayed, reduced | tier 2 | parks 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.
$ 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.