@nightowlsdev/core 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +270 -0
- package/dist/index.cjs +1598 -146
- package/dist/index.d.cts +979 -80
- package/dist/index.d.ts +979 -80
- package/dist/index.js +1575 -145
- package/package.json +3 -2
package/README.md
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# @nightowlsdev/core
|
|
2
|
+
|
|
3
|
+
> The engine: `defineAgent` / `defineSkill` / `defineSwarm`, the run loop, cost governor, HITL `ask`.
|
|
4
|
+
|
|
5
|
+
`@nightowlsdev/core` is the flagship engine of the Night Owls framework. It defines the assembly API you use to declare tools, skills, agents, and a swarm, and the `SwarmEngine` that runs them — streaming a typed `SwarmEvent` log, serializing per-lane turns, metering token spend, gating side-effecting tools behind human approval, and suspending/resuming for human-in-the-loop questions. It is engine-internals aware but keeps an "engine wall": nothing from `@mastra/*` leaks into its public types, so storage, auth, telemetry, models, and the runner are all pluggable adapters around this core.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add @nightowlsdev/core
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Peer dependency (you provide it):
|
|
14
|
+
|
|
15
|
+
- `ai` `^6.0.0` — the AI-SDK, used by the model layer and the `./test-utils` mock model.
|
|
16
|
+
|
|
17
|
+
`@mastra/core`, `@mastra/memory`, `@nightowlsdev/hooks`, and `zod` are regular dependencies and installed for you.
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
A minimal single-agent swarm with one skill, run against the in-memory dev store. (`modelFactory` returns your AI-SDK model for a given `modelId`; a real app passes a provider plugin like `@nightowlsdev/model-openai`.)
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { z } from "zod";
|
|
25
|
+
import {
|
|
26
|
+
defineTool,
|
|
27
|
+
defineSkill,
|
|
28
|
+
defineAgent,
|
|
29
|
+
defineSwarm,
|
|
30
|
+
InMemoryStorage,
|
|
31
|
+
} from "@nightowlsdev/core";
|
|
32
|
+
|
|
33
|
+
// 1. A tool = a typed, executable capability. `ctx` carries the run's tenant/user/run ids + `ctx.secrets`.
|
|
34
|
+
const getWeather = defineTool({
|
|
35
|
+
name: "get_weather",
|
|
36
|
+
description: "Look up the current weather for a city.",
|
|
37
|
+
inputSchema: z.object({ city: z.string() }),
|
|
38
|
+
outputSchema: z.object({ tempC: z.number() }),
|
|
39
|
+
execute: async ({ city }, ctx) => {
|
|
40
|
+
// ctx.tenantId / ctx.userId / ctx.runId are available here; ctx.secrets.resolve(ref) reaches the vault.
|
|
41
|
+
return { tempC: 21 };
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// 2. A skill is a tool an agent is allowed to invoke.
|
|
46
|
+
const weatherSkill = defineSkill(getWeather);
|
|
47
|
+
|
|
48
|
+
// 3. An agent = a persona + its skills (+ optional delegates / model pin).
|
|
49
|
+
const concierge = defineAgent({
|
|
50
|
+
slug: "concierge",
|
|
51
|
+
role: "orchestrator",
|
|
52
|
+
personality: "A concise, friendly travel concierge.",
|
|
53
|
+
skills: [weatherSkill],
|
|
54
|
+
modelId: "openai/gpt-5.5",
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// 4. A swarm wires agents to storage, the model allow-list + factory, and per-run caps.
|
|
58
|
+
const swarm = defineSwarm({
|
|
59
|
+
storage: new InMemoryStorage(),
|
|
60
|
+
agents: [concierge],
|
|
61
|
+
models: { allow: ["openai/gpt-5.5"] },
|
|
62
|
+
modelFactory: (modelId) => myAiSdkModel(modelId), // return an AI-SDK LanguageModel
|
|
63
|
+
cost: { maxSteps: 12, maxCostUsd: 1.0 },
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// 5. Run a turn — the engine yields a typed event stream you render / persist / meter.
|
|
67
|
+
const ctx = {
|
|
68
|
+
tenantId: "default",
|
|
69
|
+
userId: "u1",
|
|
70
|
+
agentSlug: "concierge",
|
|
71
|
+
runId: crypto.randomUUID(),
|
|
72
|
+
threadId: crypto.randomUUID(),
|
|
73
|
+
};
|
|
74
|
+
for await (const ev of swarm.engine.run({ message: "Weather in Lisbon?" }, ctx)) {
|
|
75
|
+
console.log(ev.type, ev); // swarm.status / swarm.message / swarm.tool_call / swarm.usage / swarm.turn_usage / ...
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## API
|
|
80
|
+
|
|
81
|
+
### Assembly
|
|
82
|
+
|
|
83
|
+
- **`defineTool<I, O>(spec: ToolSpec<I, O>): SwarmTool`** — declare a typed, executable tool. `spec` carries `name`, `inputSchema`/`outputSchema` (Zod), `execute(input, ctx)`, and `needsApproval` / `origin` (`"first-party"` default, or `"mcp"`). The execute body receives a `SwarmToolContext` (`tenantId`/`userId`/`runId` + `secrets`). Wraps a Mastra tool internally; the approval gate is enforced inside this wrapper.
|
|
84
|
+
- **`defineSkill(tool: SwarmTool): SwarmSkill`** — mark a tool as a skill an agent may invoke (identity passthrough; `SwarmSkill` is `SwarmTool`).
|
|
85
|
+
- **`defineAgent(spec: AgentSpec): AgentDef`** — declare an agent: `slug`, `personality`, optional `role` (`"orchestrator"|"specialist"`), `capabilities`, `skills`, `delegates` (slugs it may hand off to), `modelId` (a concrete id or a tier sentinel — see the tier router), and a per-agent `memory` override.
|
|
86
|
+
- **`defineSwarm(cfg: SwarmConfig): Swarm`** — assemble agents into a runnable `Swarm` (`{ engine: SwarmEngine }`). Seeds code-defined agents into storage and builds a per-swarm skill resolver and hook dispatcher (no module-level global state, no cross-swarm leakage).
|
|
87
|
+
- **`buildSkillResolver(agents: AgentDef[]): (name) => SwarmSkill | undefined`** — the per-swarm skill registry builder `defineSwarm` uses internally; exported for advanced/direct-engine assembly.
|
|
88
|
+
- **`defineBundle(spec: BundleSpec): BundleDef`** — compose + closure-validate a reusable **capability bundle** from `defineAgent` outputs (see *Capability bundles* below).
|
|
89
|
+
- **`mergeBundle(cfg: SwarmConfig, bundle: BundleDef): SwarmConfig`** — fold a validated bundle into a swarm config; the result is a drop-in `defineSwarm` input.
|
|
90
|
+
- **`ASK_TOOL_NAME`** — `"ask"`, the name of the built-in human-in-the-loop tool injected on every agent.
|
|
91
|
+
|
|
92
|
+
### Capability bundles (`defineBundle` / `mergeBundle`)
|
|
93
|
+
|
|
94
|
+
A **capability bundle** is a reusable, closure-validated *composition* of agents + their rules/workflows (+ connector grants) that you author once and reuse across projects — the unit gap-map #4 ("cross-project capability reuse") names. It's a **static, in-process** composition: a bundle is built from `defineAgent` outputs (skill handles present) and folded into a `SwarmConfig` at boot. No DB, no serialization, **no new runtime path** — `mergeBundle` is a front-end to `defineSwarm`.
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
import { defineAgent, defineBundle, mergeBundle, defineSwarm } from "@nightowlsdev/core";
|
|
98
|
+
|
|
99
|
+
// Author a reusable bundle from composed agents (skill handles ride along on each AgentDef).
|
|
100
|
+
const editor = defineAgent({ slug: "editor", personality: "Writes drafts.", skills: [draftSkill] });
|
|
101
|
+
const reviewer = defineAgent({ slug: "reviewer", personality: "Reviews drafts.", skills: [searchSkill] });
|
|
102
|
+
|
|
103
|
+
export const contentStudio = defineBundle({
|
|
104
|
+
slug: "content-studio",
|
|
105
|
+
title: "Content Studio",
|
|
106
|
+
agents: [editor, reviewer],
|
|
107
|
+
// optional swarm-scoped `rules` / `workflows`, validated against the members' handles
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Reuse it in any host swarm — mergeBundle folds it into the SwarmConfig, then defineSwarm runs it unchanged.
|
|
111
|
+
const swarm = defineSwarm(
|
|
112
|
+
mergeBundle(
|
|
113
|
+
{ storage, agents: [], models: { allow: ["openai/gpt-5.5"] }, modelFactory, cost: { maxSteps: 12, maxCostUsd: 1 } },
|
|
114
|
+
contentStudio,
|
|
115
|
+
),
|
|
116
|
+
);
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**`defineBundle` closure-validates at author time** — a missing handle or a typo fails loudly here, not as a runtime `run_failed`:
|
|
120
|
+
|
|
121
|
+
- every member `skillName` resolves to a first-party skill **handle** present on the bundle, **or** is covered by a BN1 connector grant (below) — a connector-looking `provider.action` name with neither is rejected;
|
|
122
|
+
- every `delegates` target is a bundle member or a declared `requires` dependency;
|
|
123
|
+
- every tool-seam **rule** tool-ref and every **workflow** `step.tool` resolves to a handle or a declared grant (globs and `agent-*` delegation gates are skipped — they aren't single-handle refs);
|
|
124
|
+
- no workflow step embeds a **credential ref** (`secretRef` / `credentialRef` / `connectionId` / `owl_connections`) — a bundle declares *which* capability it needs, never a handle to a credential.
|
|
125
|
+
|
|
126
|
+
**Connector grants (BN1).** A member can declare `connectorGrants` — permission to invoke a connector provider's actions (`{ agentSlug, provider, actions }`, **names only**, never creds). `defineBundle` folds the action names into that member's `skillNames` (expanding a short `"post_message"` to `"slack.post_message"`), so the host's `connectorTools` resolver grants the tool by membership at runtime (SP5-gated). Grants are an allowlist — an ungranted connector action is still rejected, and a grant for one member doesn't satisfy another's skills.
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
defineBundle({
|
|
130
|
+
slug: "content-studio",
|
|
131
|
+
agents: [editor, reviewer],
|
|
132
|
+
connectorGrants: [{ agentSlug: "editor", provider: "slack", actions: ["post_message"] }],
|
|
133
|
+
});
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**`mergeBundle(cfg, bundle)`** appends the bundle's agents + swarm-scoped rules/workflows to `cfg` (per-agent rules/workflows ride on the merged `AgentDef`s and are collected by `defineSwarm` as usual). A bundle whose member shadows an existing agent slug throws. Types: `BundleSpec`, `BundleDef`, `BundleDep`, `ConnectorGrant`.
|
|
137
|
+
|
|
138
|
+
> **Scope.** A bundle is *source-level* composition: code you import and pass to `defineSwarm`, so its rules/workflows keep their normal runtime home (engine config) and its skill handles are present in-process. **BN0** (static composition) and **BN1** (connector grants) have shipped; DB-persisted versioning + cross-tenant distribution + *evolve-once → upgrade-downstream* are later slices (BN2 versioning; BN3 apply/upgrade). Design + roadmap: [`docs/superpowers/specs/2026-06-19-capability-bundles-design.md`](../../docs/superpowers/specs/2026-06-19-capability-bundles-design.md) and [`docs/bundles/README.md`](../../docs/bundles/README.md).
|
|
139
|
+
|
|
140
|
+
### The run loop — `SwarmEngine`
|
|
141
|
+
|
|
142
|
+
- **`new SwarmEngine(opts: EngineOpts)`** — the engine. `defineSwarm` constructs one for you; construct it directly only for advanced/test wiring. `EngineOpts` is the low-level superset of `SwarmConfig` (it also accepts a prebuilt `hooks` `HookDispatcher`, an injectable `floor`, a `resolveSkill`, etc.).
|
|
143
|
+
- **`engine.run(input: RunInput, ctx: SwarmContext): AsyncIterable<SwarmEvent>`** — run one turn, yielding the typed event stream. `input.message` is the prompt; `input.context` is untrusted/advisory page context.
|
|
144
|
+
- **`engine.resume(args, ctx): AsyncIterable<SwarmEvent>`** — resume a suspended run (after a `swarm.question`) with `{ runId, toolCallId, followupId, answer, context? }`. Continues the same thread; requires a durable `mastraStore` to survive across processes.
|
|
145
|
+
- **`engine.history(threadId, ctx, opts?): Promise<SwarmMessage[]>`** — wall-safe persisted conversation (decodes the `[slug]`/`[user:id]` attribution prefixes). `[]` when stateless.
|
|
146
|
+
- **`engine.listThreads(ctx, opts?): Promise<ThreadSummary[]>`** — the user's conversations (participation-based when the adapter supports it).
|
|
147
|
+
- **`engine.listAgents(ctx): Promise<AgentSummary[]>`** — the tenant's agent roster (slug, display name, role, delegate graph).
|
|
148
|
+
- **`engine.threadEvents(threadId, ctx): Promise<SwarmEvent[]>`** — the full ordered event log for a thread's container (rich timeline restore).
|
|
149
|
+
- **`engine.activeRuns(container, ctx)` / `engine.scratchpadPublic(container, ctx)`** — in-flight runs and the public scratchpad for a conversation.
|
|
150
|
+
- **`ReserveDenied`** — the typed error thrown when a `preGeneration` hook vetoes a model launch; mapped to a terminal `run_failed` stage `"reserve"`.
|
|
151
|
+
|
|
152
|
+
### Events
|
|
153
|
+
|
|
154
|
+
- **`ev(type, base, data)`** — typed `SwarmEvent` constructor; **`isEvent(e, type)`** — typed narrowing guard.
|
|
155
|
+
- **`SwarmEvent`** (exported type) — the discriminated event union: `swarm.status`, `swarm.message`, `swarm.handoff`, `swarm.tool_call`, `swarm.tool_result`, `swarm.question`, `swarm.answer`, `swarm.usage`, `swarm.turn_usage`, `swarm.run_failed`.
|
|
156
|
+
|
|
157
|
+
### Human-in-the-loop `ask`
|
|
158
|
+
|
|
159
|
+
Every agent gets a built-in `ask` tool (`ASK_TOOL_NAME`). When an agent calls it, the run **suspends** and the engine emits a `swarm.question` event (carrying the prompt and an optional rich `AskField` widget spec). The host answers by calling `engine.resume(...)`, which feeds `{ answer }` back. The same suspend/resume machinery powers the tool-approval gate and the cap-that-asks.
|
|
160
|
+
|
|
161
|
+
### Storage (dev)
|
|
162
|
+
|
|
163
|
+
- **`InMemoryStorage`** — a zero-config, single-process `StorageAdapter` for tests/dev. Implements the read-only `agents` repo plus runs/events/messages/scratchpad and `seedAgent` (so `defineSwarm` can seed code-defined agents). Not durable — use `@nightowlsdev/storage-supabase` in production.
|
|
164
|
+
- Adapter contract types (from `./types`): `StorageAdapter`, `AgentRepo`, `VersionedRepo`, `RunStore`, `EventStore`, `MessageStore`, `ScratchpadStore`, plus `SwarmContext`, `AgentVersion`, `RunInput`, `SwarmMessage`, `ThreadSummary`, `AgentSummary`, `MemoryConfig`, `CompletionVerdict`.
|
|
165
|
+
|
|
166
|
+
### Prompt + model helpers
|
|
167
|
+
|
|
168
|
+
- **`GUARDRAILS`**, **`composeSystemPrompt(row)`** — the built-in safety preamble and per-agent system-prompt composer.
|
|
169
|
+
- **`allowListModelProvider({ allow })`** — the `ModelProvider` that validates every resolved model id (including tier-routed ones) against the swarm's allow-list.
|
|
170
|
+
|
|
171
|
+
### Subpath entry points
|
|
172
|
+
|
|
173
|
+
- **`@nightowlsdev/core`** — everything above.
|
|
174
|
+
- **`@nightowlsdev/core/test-utils`** — mock-model helpers for downstream tests (depends only on `ai/test` + `zod`, kept off the main barrel): `scriptedModel(scripts)` (an `ai/test` `MockLanguageModelV3` whose Nth `doStream` returns the Nth script), `textScript(chunks)` (a text-only generation script), `toolCallScript(toolName, toolCallId, input)` (a single tool-call generation), `partsStream(parts)`, and the `USAGE` fixture.
|
|
175
|
+
|
|
176
|
+
## Configuration / Environment
|
|
177
|
+
|
|
178
|
+
`@nightowlsdev/core` reads **no environment variables** — every behavior is configured via `SwarmConfig` (or the lower-level `EngineOpts`). The seams below are the ones most worth knowing.
|
|
179
|
+
|
|
180
|
+
### `toolApproval` — the non-removable safety control (lead with this)
|
|
181
|
+
|
|
182
|
+
`SwarmConfig.toolApproval?: ToolApprovalPolicy` forces human approval on side-effecting tools **regardless of a tool's own `needsApproval` flag**. It exists because spend caps limit *cost*, not *harm*: a consumer pack could ship a `needsApproval:false` $0.50 action that causes $50k of damage. It cannot be removed — when unset it defaults to `{ mode: "flag" }`.
|
|
183
|
+
|
|
184
|
+
Two modes:
|
|
185
|
+
|
|
186
|
+
- **`{ mode: "flag" }` (default)** — only tools declared `needsApproval:true` gate (suspend-and-ask). MCP tools default to `needsApproval:true`; first-party tools default to `false`.
|
|
187
|
+
- **`{ mode: "all-side-effecting" }`** — force-ask **every non-read-only tool**: every MCP tool and every first-party tool not on the read-only allowlist. The safe default for an untrusted consumer pack. The exempt set defaults to `DEFAULT_READ_ONLY_TOOLS` (`ask`, `get_page_context`, `recall_lane`); override it via `readOnly`.
|
|
188
|
+
|
|
189
|
+
`defineSwarm` bakes the policy into the hook dispatcher, which resolves each call to `allow` (run it), `deny` (block — the side effect never runs, the model gets a blocked result), or `ask` (suspend → `swarm.question` → resume; approve runs the side effect, reject blocks it). The `swarm.tool_call` event's `needsApproval` reflects whether the policy + flag will gate the call (so the UI can render an approval card).
|
|
190
|
+
|
|
191
|
+
### `secrets` — run-scoped secret resolution
|
|
192
|
+
|
|
193
|
+
`SwarmConfig.secrets?: SecretResolver` plugs a platform vault into the engine. When set, the engine scopes it per-run on the request context, so a first-party tool body can call `await ctx.secrets.resolve(ref)` (typed `BoundSecrets`) to fetch a tenant-scoped secret **at execution time**. The run's tenant/auth scope is captured by the binding — never passed by the tool — so a tool can never resolve another tenant's secret. Unset ⇒ `ctx.secrets.resolve(...)` yields `undefined` (no vault), never throws.
|
|
194
|
+
|
|
195
|
+
### Tier router — Swift / Genius
|
|
196
|
+
|
|
197
|
+
`SwarmConfig.models.tier?: TierConfig` enables cheap-default model routing layered over per-agent pinning:
|
|
198
|
+
|
|
199
|
+
- `tiers.swift` (required) is the cheap default every non-pinned agent lands on; `tiers.genius` (optional) is the frontier model.
|
|
200
|
+
- An agent opts into routing with a tier-sentinel `modelId` (`"tier:"`, `"tier:swift"`, `"tier:genius"`); a concrete `modelId` is kept verbatim (routing never overrides a pin).
|
|
201
|
+
- `allowGenius` (default `false`) is the **server-enforced paid gate** — a pack/agent config cannot grant itself Genius. A Genius request with the gate closed **downgrades** to Swift so the run still proceeds cheaply.
|
|
202
|
+
- An optional per-task `escalate(ctx)` hook may bump a generation to Genius, still subject to the gate.
|
|
203
|
+
|
|
204
|
+
Helpers: **`resolveTier(modelId, cfg, ctx): TierResolution`** (the full routing result — effective `modelId`, landed `tier`, `downgraded`/`escalated`), **`tierModelId(modelId, cfg, ctx): string`** (the engine convenience: effective id; identity when no config), **`isTierSentinel(modelId)`**. Types: `ModelTier`, `ModelRef`, `TierConfig`, `TierResolution`, `TierEscalationContext`. The routed model is always re-validated by the allow-list.
|
|
205
|
+
|
|
206
|
+
### `cost` — caps + metering + the cap-that-asks
|
|
207
|
+
|
|
208
|
+
`SwarmConfig.cost` carries `maxSteps` and `maxCostUsd` (per-run caps) plus metering:
|
|
209
|
+
|
|
210
|
+
- `prices` statically overrides the built-in `PRICE_TABLE`; `priceFeed` is an optional live numbers-only seam; `failOnUnknownModel` (default `false`) makes an unpriced model throw instead of pricing at $0.
|
|
211
|
+
- `perDelegate` adds optional per-delegate USD sub-budgets (`PerDelegateBudget`) so one runaway sub-agent can't burn the whole turn.
|
|
212
|
+
- **`onCapHit`** (default `"stop"`) — with `"ask"`, a global cost/step-cap hit **pauses and asks** ("Budget cap reached — continue?") instead of terminally failing; approve raises `maxCostUsd` by **`capIncrementUsd`** (defaults to the original `maxCostUsd`) and continues, reject stops. A server-side opt-in for consumer runs.
|
|
213
|
+
|
|
214
|
+
Cost helpers: **`CostGovernor`** (global step + USD cap enforcement, `addUsage`/`shouldStop`/`raiseCostCap`), **`DelegateBudgets`** (per-delegate sub-budgets), **`PRICE_TABLE`** (built-in, host-overridable model prices), **`priceUsage(prices, modelId, breakdown, opts?)`**, **`sumBreakdowns`**, **`sumTurnUsage`**. Types: `Price`, `PerDelegateBudget`, `UsageBreakdown`, `UsageCost`, `PriceFeed`, `PricingOpts`, `SlugUsage`, `TurnUsage`.
|
|
215
|
+
|
|
216
|
+
### Container floor — per-lane turn serialization
|
|
217
|
+
|
|
218
|
+
`EngineOpts.floor?: ContainerFloor` serializes runs per lane (so two agents in the same lane don't interleave, while different lanes run in parallel). **`containerFloor`** is the default in-memory process singleton; **`InMemoryContainerFloor`** is its class. For serverless/multi-instance deploys, pass a Postgres-backed floor (`createPostgresFloor` from `@nightowlsdev/storage-supabase`). Types: `ContainerFloor`, `FloorHolder`, `Release`.
|
|
219
|
+
|
|
220
|
+
### Rate limiting — fixed-window primitive
|
|
221
|
+
|
|
222
|
+
A generic, dependency-free fixed-window limiter — the building block for an abuse gateway. It knows nothing about tenants or billing; you supply the key. Wire it into the `preGeneration` / `preToolCall` hooks to cap a tenant's generations or side-effecting tool calls per window (the layer the per-run caps — `maxSteps`, `maxCostUsd` — can't cover).
|
|
223
|
+
|
|
224
|
+
- **`createInMemoryRateLimitStore(): RateLimitStore`** — a real single-instance store (`hit(key, cfg, nowSec)` → decision), pruning expired windows opportunistically. A horizontally-scaled deploy backs the same `RateLimitStore` interface with Redis/Postgres.
|
|
225
|
+
- **`decideFixedWindow(prev, cfg, nowSec)`** — the pure decision (no I/O): returns `{ decision, state }` so you can persist the next window state in any store.
|
|
226
|
+
- **`rateConfig(max, windowSec, fallbackMax)`** — build a `{ windowSec, max }` config from an env value, clamped to a sane positive integer.
|
|
227
|
+
- Types: `RateLimitConfig`, `RateLimitState`, `RateLimitDecision`, `RateLimitStore`.
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
const limiter = createInMemoryRateLimitStore();
|
|
231
|
+
const GEN = rateConfig(Number(process.env.GEN_PER_MIN), 60, 30);
|
|
232
|
+
// inside a preGeneration hook:
|
|
233
|
+
const rl = await limiter.hit(`gen:${ev.tenantId}`, GEN, Math.floor(Date.now() / 1000));
|
|
234
|
+
if (!rl.allow) return deny(`rate limit — retry in ${rl.resetSec}s`);
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Decision/observer hooks (re-exported from `@nightowlsdev/hooks`)
|
|
238
|
+
|
|
239
|
+
`@nightowlsdev/core` re-exports the engine-free hook substrate so hosts configure `SwarmConfig.hooks` without a second import:
|
|
240
|
+
|
|
241
|
+
- **`defineHook(hooks)`** — typed-identity helper to author a `SwarmHooks` bundle.
|
|
242
|
+
- **`HookDispatcher` / `createHookDispatcher(hooks?, policy?)`** — the dispatcher `defineSwarm` builds (combines the tool-approval policy + per-tool flag + the optional `preToolCall` hook); decision hooks are **fail-closed** (a throw ⇒ deny).
|
|
243
|
+
- Decision constructors/constants: **`deny(reason)`**, **`ask(reason?)`**, **`ALLOW`**, **`ALLOW_TOOL`**, **`DEFAULT_READ_ONLY_TOOLS`**.
|
|
244
|
+
- Types: `HookDecision`, `ToolDecision`, `SwarmHooks`, `PreGenerationEvent`/`PreGenerationHook`, `ToolPreCallEvent`/`PreToolCallHook`, `ToolApprovalPolicy`, `GuardMutationEvent`/`GuardMutationHook`.
|
|
245
|
+
|
|
246
|
+
`SwarmConfig.hooks` exposes `preGeneration` (awaited before each model launch — the platform billing-reserve seam; a `deny` vetoes the generation), `preToolCall` (the richer action-approval gate on top of `toolApproval`), and `guardDefinitionMutation` (gate who may publish/rollback an agent definition).
|
|
247
|
+
|
|
248
|
+
### `onEvent` — the per-event observer
|
|
249
|
+
|
|
250
|
+
`SwarmConfig.onEvent?: (ev, ctx) => void | Promise<void>` is a best-effort, transport-agnostic observer fired after each event is persisted (in `run` and `resume`). It is awaited but fail-safe (a throw is swallowed). This is where platform metering lives — debit on `swarm.turn_usage`, settle on a terminal. With `preGeneration` it forms the two halves of a credit ledger.
|
|
251
|
+
|
|
252
|
+
### `verifyCompletion` — the completion supervisor
|
|
253
|
+
|
|
254
|
+
`SwarmConfig.verifyCompletion?: CompletionVerifier` is a host-supplied check (typically a cheap LLM judge) fired when a turn would end: given the original request + a transcript, it returns `{ complete, missing? }`. If incomplete, the engine re-nudges the orchestrator with the specific gap; if it still can't finish, the run ends `run_failed` stage `"incomplete"` (refundable) instead of a silent `done`. Fail-safe (a throw ⇒ treated as complete). Omit ⇒ the cheap structural "did the root speak last?" fallback nudge.
|
|
255
|
+
|
|
256
|
+
### Host-owned billing (reference)
|
|
257
|
+
|
|
258
|
+
The engine exposes neutral seams (`preGeneration`, `onEvent`, `verifyCompletion`, `cost`) — it meters quantities but never prices them. Pricing, wallets, credits, and refund policies are host concerns. For a worked reference covering all five patterns (prepaid credit ledger, `onEvent` debit/settle/refund observer, per-agent attribution, model-tier wiring, and completion judge), see [`../../examples/billing-reference/README.md`](../../examples/billing-reference/README.md). Those patterns are **host-owned** (implemented in `apps/getnightowls`) and are not part of any `@nightowlsdev/*` API.
|
|
259
|
+
|
|
260
|
+
### `customAuth` — wrap an auth function
|
|
261
|
+
|
|
262
|
+
`customAuth(fn)` wraps an `authenticate(req)` function into an `AuthProvider`, the shape the runner uses to resolve server-side identity. Identity always comes from the server, never from tool args or page context.
|
|
263
|
+
|
|
264
|
+
### Telemetry
|
|
265
|
+
|
|
266
|
+
`SwarmConfig.telemetry?: TelemetryExporter | TelemetryExporter[]` collects `run`/`generation`/`tool`/`recall` spans, batch-exported once per run (best-effort — a throwing exporter never breaks a run). Helpers: **`customTelemetry(fn)`**, **`compositeTelemetry(exporters)`**, **`resolveTelemetry(t)`**, **`CapturingExporter`** (test/in-memory), **`SpanCollector`**.
|
|
267
|
+
|
|
268
|
+
### Other config options
|
|
269
|
+
|
|
270
|
+
`storage` (required `StorageAdapter`), `models.allow` (required), `modelFactory` (required), `mastraStore` (durable suspend/resume snapshot store — required for cross-process HITL), `memory` (opt-in conversational `MemoryConfig`), `pageContext`, `scratchpad`, and `recallLane` (opt-in collaboration tools). See the inline doc comments on `SwarmConfig`/`EngineOpts` for the full contract.
|