Skip to content
Night Owls.dev
Jump to a page
Concept · architecture

The run loop is swappable. Which engine you pick is a question about where the loop runs, not a feature checklist.

Six engines ship today, in two kinds. A native engine runs the agent loop in your process, so it reuses the full Night Owls governance plane verbatim. An adapter fronts a loop that runs on a remote product you don't own — which is genuinely useful, but means the pre-generation veto, the tool gate, and cost caps cannot reach inside it. That is not a maturity gap to be closed; it is where the loop physically executes. This guide is how to choose, and where durability actually comes from.

Native · 3 enginesAdapter · 3 enginesdefineSwarm({ engine })

The one axis that decides everything: where the loop runs

Governance in Night Owls is enforced around the generation call — a cost cap checked before the next step, a fail-closed tool gate that can refuse a call, a pre-generation veto, secret injection scoped to the run. All of that only works if the loop is in your process, where the framework sits between the model and the tools. A native engine is exactly that: the loop runs in-process, so the governance plane wraps it unchanged. An adapter hands the loop to a remote runtime (an A2A endpoint, a deployed Trigger chat.agent Session, a Vercel Eve session); the framework can relay a request and observe results, but it cannot step between the remote model and the remote tools. Reduced governance on an adapter is structural, not a second-class implementation.

"chat.agent has fewer features than Mastra" is true, and it is supposed to be. An adapter is not a weaker port of the native engine that will one day catch up. It is a bridge to a loop running somewhere the governance plane cannot follow. If you need the full plane, run a native engine — and if you also need durability, bundle a native engine into the background runner (step 04). If you specifically need to front an existing remote agent product, an adapter is the right tool, and its descriptor states in code precisely what it cannot enforce so nothing is hidden.

The capability matrix — every engine, honestly

This is the same descriptor data the home page and the Studio UI read, rendered here so you can choose from one table. Every engine publishes an honest capability descriptor; the UI gates on it, so a surface an adapter can't support is disabled rather than silently degraded.

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 v0.3-wire endpoint: Bedrock AgentCore, Azure AI Foundry, Google ADKon the remoterelayed, reducedtier 1on the remote
engine-trigger-chat ⚠ experimentala deployed Trigger chat.agent session (NOT a GA Trigger primitive)on the remoterelayed, reducedtier 2parks on Trigger's runtime (for GA durability use runner-background)
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.

How to choose

Answer two questions in order — do I need the full governance plane? and does the loop already run somewhere else? — and the engine falls out.

If you want…PickWhy
The default: full governance, interactive, least configengine-mastra (omit engine)In-process loop, full plane, tier-3 events, durable resume with semantic recall. This is the baseline every other row is measured against.
The AI SDK loop, still fully governed in-processengine-ai-sdkNative: streamText under the full plane; approvals map to the SDK's needsApproval. Single-agent in v1.
An OpenAI Agents SDK loop, fully governed in-processengine-openai-agentsNative, full plane. Single-agent in v1; the SDK's native handoffs become delegation in v1.1.
A run that parks on a human for days and survives a restart, with full governancerunner-background + a native engineDurability is a runner, not an engine. A native engine bundled into the durable runner keeps the whole plane while it parks on Trigger's durable primitives. See step 04.
To front an existing remote agent (Bedrock AgentCore, Azure AI Foundry, Google ADK)engine-a2aAdapter: any A2A v0.3-wire endpoint appears as a Night Owls agent. Governance is relayed and reduced because the loop runs on the remote — by design.
To front a deployed Trigger chat.agent Session you already runengine-trigger-chat experimentalAdapter, tier-2. For a durable run you own, use runner-background instead (step 04) — that is the first-class Trigger-durability path.
To front a deployed Vercel Eve NDJSON sessionengine-eveAdapter, tier-2; durability parks on Vercel Workflows on the remote. Reduced governance, by design.
picking an engine
import { defineSwarm } from "@nightowlsdev/core";

// 1) The default. Omit `engine` entirely and you get the built-in Mastra engine, byte-identical —
//    full delegation, workflows, the whole governance plane, tier-3 events, durable resume.
const swarm = defineSwarm({ /* … model, agents, tools … */ });

// 2) A different NATIVE loop, same full governance in-process. You still own the loop, so cost caps,
//    the fail-closed tool gate, the pre-generation veto, and secrets all still apply.
import { aiSdkEngine } from "@nightowlsdev/engine-ai-sdk";
const onAiSdk = defineSwarm({ engine: aiSdkEngine({ /* … */ }), /* … */ });

// 3) An ADAPTER — you are fronting a loop that runs on a remote product. The descriptor says, in code,
//    exactly what governance it cannot enforce, and the Studio UI gates on that descriptor.
import { a2aEngine } from "@nightowlsdev/engine-a2a";
const overRemote = defineSwarm({ engine: a2aEngine({ endpoint: "https://…/a2a" }), /* … */ });

Durability is a separate axis — and the governed path is a native engine

The most common confusion in the matrix is reading the durable resume column as an engine ranking. It isn't. "Can this run park on a human for days and survive the process dying?" is answered by the runner, not the engine. The interactive runner streams the loop to the browser and dies with the request; the background runner (@nightowlsdev/runner-background) parks a run with zero compute and resumes it later — and crucially, it parks a native engine, so you keep the entire governance plane while durable.

durable AND governed = native engine + background runner
// "I want a run that can park on a human for days and survive a restart" is a DIFFERENT question
// from "which engine". Durability is not an engine feature — it is a RUNNER. The first-class,
// fully-governed durable path is a NATIVE engine bundled into @nightowlsdev/runner-background:
import { createBackgroundRunner, createTriggerBackend } from "@nightowlsdev/runner-background";
import { SwarmEngine } from "@nightowlsdev/core"; // the native Mastra engine

const runner = createBackgroundRunner({
  engine: new SwarmEngine({ /* … */ }), // ← native ⇒ FULL governance, even while durable
  storage,                               // cross-process Postgres snapshot to resume from
  backend: createTriggerBackend(swarmRun), // Trigger v4 durable tasks + wait tokens (GA)
});

// This is the answer to "I need Trigger durability." It is NOT @nightowlsdev/engine-trigger-chat.
// engine-trigger-chat is an ADAPTER that fronts a deployed Trigger chat.agent Session (reduced
// governance, the loop runs on Trigger). runner-background parks a NATIVE loop on Trigger's durable
// PRIMITIVES (tasks + wait tokens) — you keep the whole governance plane AND get durability.

So "I need Trigger durability" and "I use engine-trigger-chat" are different decisions. runner-background rides Trigger v4's durable primitives (tasks + wait tokens) and parks a native loop — full governance, durable, GA. engine-trigger-chat is an adapter that fronts Trigger's chat.agent Sessions product — reduced governance, because that loop runs on Trigger. Reach for the adapter only when the Session is the thing you already have and want to expose; reach for the background runner when you want to own a durable, governed run.

You give up nothing by going through the engine wall

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.

You give up nothing by going through the wall

The default engine IS Mastra: omit engine and you get it, byte-identical. Everything Mastra exposes, full delegation and workflows, the full governance plane (fail-closed tool gate, cost caps, secrets, telemetry), tier-3 event granularity, and durable resume with semantic recall, is reachable through Night Owls' own SwarmContext / SwarmEvent / Runner / StorageAdapter types, with zero @mastra/* in any public .d.ts. Delegation, workflows, and semantic recall are Mastra-engine features today; the other native engines are honest that they run single-agent in v1.

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.