@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/dist/index.d.cts CHANGED
@@ -1,4 +1,175 @@
1
1
  import { z } from 'zod';
2
+ import { HookDispatcher, ToolApprovalPolicy, SwarmHooks } from '@nightowlsdev/hooks';
3
+ export { ALLOW, ALLOW_TOOL, DEFAULT_READ_ONLY_TOOLS, GuardMutationEvent, GuardMutationHook, HookDecision, HookDispatcher, PreGenerationEvent, PreGenerationHook, PreToolCallHook, SwarmHooks, ToolApprovalPolicy, ToolDecision, ToolPreCallEvent, ask, createHookDispatcher, defineHook, deny } from '@nightowlsdev/hooks';
4
+
5
+ interface Price {
6
+ inUsdPerMtok: number;
7
+ outUsdPerMtok: number;
8
+ /**
9
+ * Per-class USD rates per million tokens (SP1). All OPTIONAL + additive — a price entry without them
10
+ * prices exactly like before. When a class's rate is absent the engine falls back to the closest base
11
+ * rate, picked to match how providers bill these classes:
12
+ * - `cacheReadUsdPerMtok` → falls back to `inUsdPerMtok` (cache reads are discounted input; absent a
13
+ * discount rate we conservatively bill them at the full input rate rather than free).
14
+ * - `cacheWriteUsdPerMtok` → falls back to `inUsdPerMtok` (cache writes are an input-side surcharge).
15
+ * - `reasoningUsdPerMtok` → falls back to `outUsdPerMtok` (reasoning tokens are generated output).
16
+ */
17
+ cacheReadUsdPerMtok?: number;
18
+ cacheWriteUsdPerMtok?: number;
19
+ reasoningUsdPerMtok?: number;
20
+ }
21
+ /**
22
+ * Per-generation usage, by token class (SP1). `inputTokens`/`outputTokens` are always present (today's
23
+ * shape); the rest are OPTIONAL — a provider that does not report a class leaves it undefined and it is
24
+ * priced as zero (NEVER fabricated). `toolCalls`/`agentActivations` are activity counts for telemetry,
25
+ * not priced. Plain domain type — @mastra-free (engine wall, CONTRACTS §1).
26
+ */
27
+ interface UsageBreakdown {
28
+ inputTokens: number;
29
+ outputTokens: number;
30
+ cacheReadTokens?: number;
31
+ cacheWriteTokens?: number;
32
+ reasoningTokens?: number;
33
+ /** Number of (non-delegation) tool calls in this generation. Telemetry only — not priced. */
34
+ toolCalls?: number;
35
+ /** Number of sub-agent delegations (`agent-<slug>` calls) in this generation. Telemetry only — not priced. */
36
+ agentActivations?: number;
37
+ }
38
+ /** A priced usage: the computed USD plus the breakdown it was priced from. */
39
+ interface UsageCost {
40
+ usd: number;
41
+ breakdown: UsageBreakdown;
42
+ }
43
+ /**
44
+ * Optional seam that supplies/overrides per-model prices (SP1). Supplies NUMBERS only (no @mastra) so the
45
+ * engine wall holds. `prices()` returns a full `modelId → Price` map merged over the built-in `PRICE_TABLE`
46
+ * + any static `prices` config; a host can back it with a live price feed. Sync to keep governor construction
47
+ * synchronous on the hot path — resolve/refresh out of band and return the current snapshot here.
48
+ */
49
+ interface PriceFeed {
50
+ prices(): Record<string, Price>;
51
+ }
52
+ declare const PRICE_TABLE: Record<string, Price>;
53
+ /** Options shared by the pricing helpers + the governors that thread pricing config through. */
54
+ interface PricingOpts {
55
+ /**
56
+ * When TRUE an unpriced `modelId` THROWS instead of pricing at $0. Default FALSE so OSS users (the
57
+ * built-in `PRICE_TABLE` has only 2 entries) are not broken — an unknown model keeps the historical
58
+ * $0 fallback (the cost cap simply can't fire on it). A host that wants billing safety flips this on.
59
+ */
60
+ failOnUnknownModel?: boolean;
61
+ }
62
+ /** Price one usage at a model's rate, across EVERY token class (SP1). Shared by the global CostGovernor and
63
+ * per-delegate DelegateBudgets so the two caps always agree on what a token costs. Each class is priced at
64
+ * its explicit per-class rate, with the documented fallbacks (see `Price`) when a rate is absent; a token
65
+ * class the provider omitted is treated as zero (never fabricated). An unknown model prices at $0 by default
66
+ * (the cap can't fire on a model with no price entry — historical behavior) unless `failOnUnknownModel`. */
67
+ declare function priceUsage(prices: Record<string, Price>, modelId: string, u: UsageBreakdown, opts?: PricingOpts): number;
68
+ /**
69
+ * Sum a list of `UsageBreakdown` into a single turn-total breakdown (SP4 — the room-turn billing unit).
70
+ * The always-present base classes (`inputTokens`/`outputTokens`) sum as plain numbers. Each OPTIONAL class
71
+ * (cacheRead/cacheWrite/reasoning/toolCalls/agentActivations) sums with UNDEFINED as the additive identity:
72
+ * - undefined + number → the number (a class present in only some generations still totals correctly),
73
+ * - all-undefined → stays UNDEFINED (we never fabricate a class no provider reported as a zero),
74
+ * - present-and-zero → preserved as 0 (an explicit 0 means "reported zero", distinct from "not reported").
75
+ * An empty list yields the zero base breakdown `{ inputTokens: 0, outputTokens: 0 }`. @mastra-free.
76
+ */
77
+ declare function sumBreakdowns(items: UsageBreakdown[]): UsageBreakdown;
78
+ /** One generation's priced usage attributed to an agent slug — the unit the turn aggregates over. */
79
+ interface SlugUsage {
80
+ slug: string;
81
+ breakdown: UsageBreakdown;
82
+ cost: UsageCost;
83
+ }
84
+ /** A room-turn's aggregate usage (SP4): the summed breakdown + summed USD for the WHOLE turn, plus a
85
+ * per-agent-slug split so the platform can attribute credits per agent. `bySlug` is in first-seen order. */
86
+ interface TurnUsage {
87
+ breakdown: UsageBreakdown;
88
+ cost: UsageCost;
89
+ bySlug: SlugUsage[];
90
+ }
91
+ /**
92
+ * Aggregate every generation's `(slug, breakdown, cost)` over a room-turn into ONE turn total + a per-slug
93
+ * split (SP4). The turn total breakdown/USD are the sum across ALL generations; `bySlug` folds every
94
+ * generation of the same slug into one entry (so a slug that generated twice — e.g. an orchestrator before
95
+ * and after a delegation — appears once with its combined breakdown/USD), in FIRST-SEEN order for a stable
96
+ * split. By construction the per-slug entries sum back to the turn total (the invariant the platform's
97
+ * per-agent credit attribution relies on). An empty list yields a zero total + empty split. @mastra-free.
98
+ */
99
+ declare function sumTurnUsage(items: SlugUsage[]): TurnUsage;
100
+ declare class CostGovernor {
101
+ private opts;
102
+ private steps;
103
+ private usd;
104
+ private prices;
105
+ private failOnUnknownModel;
106
+ constructor(opts: {
107
+ maxSteps: number;
108
+ maxCostUsd: number;
109
+ /** Static per-model price overrides, merged over PRICE_TABLE. */
110
+ prices?: Record<string, Price>;
111
+ /** Optional live price seam (SP1). Merged OVER `prices` (a feed entry wins) at construction. */
112
+ priceFeed?: PriceFeed;
113
+ } & PricingOpts);
114
+ step(): void;
115
+ /** Price a single usage WITHOUT accumulating it (for per-generation telemetry cost). */
116
+ priceOf(modelId: string, u: UsageBreakdown): number;
117
+ /** Price a single usage WITHOUT accumulating it, returning the usd + the breakdown it was priced from. */
118
+ costOf(modelId: string, u: UsageBreakdown): UsageCost;
119
+ addUsage(modelId: string, u: UsageBreakdown): void;
120
+ costUsd(): number;
121
+ /** The current USD cap (SP9-core: the cap-that-asks reads this to surface "spend / cap" + to compute the raise). */
122
+ get maxCostUsd(): number;
123
+ /**
124
+ * SP9-core — RAISE the USD cap by `incrementUsd` (the budget an approved "Budget cap reached — continue?"
125
+ * grants). Mutates the governor's ceiling so a freshly-resumed generation isn't immediately re-blocked at the
126
+ * SAME cap; the run gets real additional headroom. Only the cap-that-asks resume path calls this; the default
127
+ * terminal-stop path never does, so today's behaviour is unchanged.
128
+ */
129
+ raiseCostCap(incrementUsd: number): void;
130
+ shouldStop(): {
131
+ stop: boolean;
132
+ reason?: string;
133
+ };
134
+ }
135
+ /**
136
+ * Per-delegate USD sub-budgets (R5). The global `cost.maxCostUsd` bounds the WHOLE turn; this adds an
137
+ * optional ceiling applied to EACH delegate (sub-agent) so one runaway sub-agent can't burn the entire
138
+ * turn before the global cap notices. `maxCostUsd` is the default ceiling for every delegate; `bySlug`
139
+ * overrides it per delegate slug (a slug listed there is capped even if there is no default). The run's
140
+ * root orchestrator is NOT a delegate and is never capped here — the global cap already covers it.
141
+ */
142
+ interface PerDelegateBudget {
143
+ /** Default USD ceiling per delegate. Omit to cap only the slugs named in `bySlug`. */
144
+ maxCostUsd?: number;
145
+ /** Per-slug overrides of `maxCostUsd`. */
146
+ bySlug?: Record<string, {
147
+ maxCostUsd?: number;
148
+ }>;
149
+ }
150
+ /** Tracks per-delegate USD spend and reports the first delegate to exceed its budget. Priced from the same
151
+ * table as CostGovernor (via the shared `priceUsage`) so the global and per-delegate caps agree. Usage
152
+ * attributed to the root orchestrator slug is ignored. */
153
+ declare class DelegateBudgets {
154
+ private cfg;
155
+ private rootSlug;
156
+ private usd;
157
+ private prices;
158
+ private failOnUnknownModel;
159
+ constructor(cfg: PerDelegateBudget, rootSlug: string, pricing?: {
160
+ prices?: Record<string, Price>;
161
+ priceFeed?: PriceFeed;
162
+ } & PricingOpts);
163
+ /** The USD cap for a delegate: its `bySlug` override if present, else the default. `undefined` → uncapped. */
164
+ private capFor;
165
+ /** Accumulate one generation's usage against a delegate. No-op for the root orchestrator (not a delegate). */
166
+ addUsage(slug: string, modelId: string, u: UsageBreakdown): void;
167
+ /** The first delegate that has met or exceeded its USD cap, or null. */
168
+ exceeded(): {
169
+ slug: string;
170
+ reason: string;
171
+ } | null;
172
+ }
2
173
 
3
174
  interface SwarmContext {
4
175
  tenantId: string;
@@ -17,8 +188,63 @@ interface AuthProvider {
17
188
  authenticate(req: Request): Promise<AuthContext | null>;
18
189
  can?(ctx: AuthContext, capability: string): boolean | Promise<boolean>;
19
190
  }
191
+ /**
192
+ * The PRINCIPAL performing a definition mutation (SP6) — a plain (@mastra-free) discriminated union covering
193
+ * every kind of actor that can publish/rollback an agent definition. Distinct from `AuthContext` (the run's
194
+ * end-user identity): an actor models WHO is mutating the swarm's own code, which may be a human in the
195
+ * no-code builder, a service (CI/seeding pipeline), or a system task.
196
+ *
197
+ * - `human` → a person acting in a tenant (the no-code builder's authenticated user).
198
+ * - `service` → a non-human automation acting in a tenant (CI, a seeding job, a migration runner).
199
+ * - `system` → an internal/un-tenanted operation (bootstrap seed, ops rollback) carrying a `reason`.
200
+ * - `agent` → an AGENT acting on its own behalf. This variant exists for ONE reason: so an agent
201
+ * principal is REPRESENTABLE and can therefore be UNCONDITIONALLY BARRED from mutating a
202
+ * definition (an agent must never be able to rewrite the swarm's own code). The bar is
203
+ * enforced in the repo contract layer (`assertActorMayMutateDefinition`) and CANNOT be
204
+ * overridden by any policy/hook. No code should ever construct an `agent` actor expecting a
205
+ * mutation to succeed — it is the deny sentinel.
206
+ */
207
+ type SwarmActor = {
208
+ type: "human";
209
+ userId: string;
210
+ tenantId: string;
211
+ } | {
212
+ type: "service";
213
+ serviceId: string;
214
+ tenantId: string;
215
+ } | {
216
+ type: "system";
217
+ reason: string;
218
+ } | {
219
+ type: "agent";
220
+ agentSlug: string;
221
+ tenantId: string;
222
+ };
223
+ /**
224
+ * The NON-BYPASSABLE security invariant for definition mutations (SP6): an `agent` principal can NEVER
225
+ * publish or roll back an agent definition, regardless of any configured policy/hook. Every writable-repo
226
+ * mutation path MUST call this FIRST (before the `guardDefinitionMutation` policy hook) so the bar cannot be
227
+ * weakened by an allow-all hook. Throws `AgentMutationForbidden` for an `agent` actor; a no-op otherwise.
228
+ *
229
+ * This lives in the contract layer (not behind a hook) on purpose: the hook is removable/host-configurable
230
+ * policy, whereas this bar is a framework invariant — fail-closed and non-negotiable.
231
+ */
232
+ declare function assertActorMayMutateDefinition(actor: SwarmActor): void;
233
+ /** Thrown by `assertActorMayMutateDefinition` when an `agent` principal attempts a definition mutation. A
234
+ * named class so callers can distinguish the security bar from an ordinary failure (e.g. a missing version). */
235
+ declare class AgentMutationForbidden extends Error {
236
+ readonly code: "AGENT_MUTATION_FORBIDDEN";
237
+ constructor(agentSlug: string);
238
+ }
239
+ /**
240
+ * The seam a platform vault (SP15-platform) implements: given a secret `ref` + the RUN's identity ctx, it
241
+ * supplies the secret VALUE. @mastra-free. Resolution is execution-time + ctx-scoped (the connector calls it at
242
+ * tool-call time with the live run ctx — see @nightowlsdev/mcp connector.ts), so the vault enforces per-tenant
243
+ * scoping off `ctx.tenantId`: a run must NOT be able to resolve another tenant's secret. Returns `undefined` for
244
+ * an unknown ref (a tool then sees "no secret") — the resolver should never leak across tenants.
245
+ */
20
246
  interface SecretResolver {
21
- resolve(ref: string, ctx: SwarmContext): Promise<string>;
247
+ resolve(ref: string, ctx: SwarmContext): Promise<string | undefined>;
22
248
  }
23
249
  /** An optional rich input the asking agent CONSTRUCTS for a HITL `ask`, so the UI can render a fitting
24
250
  * widget instead of a bare text box. Omitted ⇒ a plain text input. Answer shape by kind: confirm→boolean,
@@ -54,6 +280,9 @@ interface EvBase {
54
280
  ts: number;
55
281
  seq?: number;
56
282
  schemaVersion: 1;
283
+ /** The run's thread (the lane this event belongs to). NOT set on live events — the store fills it in on a
284
+ * container restore (listForContainer) so a client can tell a lane side-chat apart from the main thread. */
285
+ threadId?: string;
57
286
  }
58
287
  type SwarmEvent = (EvBase & {
59
288
  type: "swarm.status";
@@ -65,7 +294,7 @@ type SwarmEvent = (EvBase & {
65
294
  }) | (EvBase & {
66
295
  type: "swarm.message";
67
296
  data: {
68
- role: "assistant";
297
+ role: "assistant" | "user";
69
298
  delta?: string;
70
299
  text?: string;
71
300
  };
@@ -110,6 +339,31 @@ type SwarmEvent = (EvBase & {
110
339
  from: "user" | string;
111
340
  answer: unknown;
112
341
  };
342
+ }) | (EvBase & {
343
+ type: "swarm.usage";
344
+ data: {
345
+ slug: string;
346
+ modelId: string;
347
+ breakdown: UsageBreakdown;
348
+ cost: UsageCost;
349
+ };
350
+ }) | (EvBase & {
351
+ type: "swarm.turn_usage";
352
+ data: {
353
+ breakdown: UsageBreakdown;
354
+ cost: UsageCost;
355
+ bySlug: Array<{
356
+ slug: string;
357
+ breakdown: UsageBreakdown;
358
+ cost: UsageCost;
359
+ }>;
360
+ generations: number;
361
+ /** The segment's STARTING generation index — distinct per run/resume segment AND retry-stable (it's the
362
+ * monotonic, snapshot-persisted generation counter, not a fresh per-append seq). A host keys a PER-TURN
363
+ * billing debit on it so each segment of a suspend/resume run is charged exactly once (run segment = 0;
364
+ * each resume = the snapshot's next genIndex). The engine never prices — this is just a stable turn id. */
365
+ segmentIndex: number;
366
+ };
113
367
  }) | (EvBase & {
114
368
  type: "swarm.run_failed";
115
369
  data: {
@@ -142,6 +396,118 @@ interface AgentMemoryOverride {
142
396
  };
143
397
  observationalMemory?: boolean | Record<string, unknown>;
144
398
  }
399
+ /** Enforcement level a rule declares. `advise` = prompt-injected guidance; `enforce` = decision-hook veto. */
400
+ type RuleLevel = "advise" | "enforce";
401
+ /** Declarative match over a hook event. Empty = match-all within the resolved seam. */
402
+ interface RuleCondition {
403
+ /** agentSlug glob/exact. For a delegation the gate sees the PARENT (orchestrator) slug. */
404
+ agent?: string | string[];
405
+ /** toolName glob/exact (tool seam). */
406
+ tool?: string | string[];
407
+ /** tool provenance. */
408
+ origin?: "first-party" | "mcp";
409
+ /** modelId glob/exact (generation seam). */
410
+ model?: string | string[];
411
+ }
412
+ interface RuleAction {
413
+ do: "deny" | "ask";
414
+ reason?: string;
415
+ }
416
+ interface RuleSpec {
417
+ id: string;
418
+ statement: string;
419
+ when: RuleCondition;
420
+ level: RuleLevel;
421
+ /** REQUIRED when level==="enforce". `ask` is TOOL-SEAM ONLY (preGeneration cannot suspend). */
422
+ action?: RuleAction;
423
+ /** Seam; inferred from `when` when omitted (model ⇒ generation, else tool). */
424
+ on?: "tool" | "generation";
425
+ }
426
+ /** A normalized, engine-held rule (the output of `defineRule`). */
427
+ interface RuleDef {
428
+ id: string;
429
+ statement: string;
430
+ when: RuleCondition;
431
+ level: RuleLevel;
432
+ action?: RuleAction;
433
+ /** Resolved seam. */
434
+ seam: "tool" | "generation";
435
+ /** Set when authored via `defineAgent` (per-agent scope); undefined ⇒ swarm-wide. */
436
+ scopeAgent?: string;
437
+ }
438
+ /** Workflow compliance. v1: `advisory` (prompt) | `strict` (driver, Phase B — rejected by defineSwarm in Phase A). */
439
+ type WorkflowCompliance = "advisory" | "strict";
440
+ /** A flat data reference resolved at runtime: `{ $ref: "input" }` | `{ $ref: "steps.<id>" }`. */
441
+ type WorkflowRef = {
442
+ $ref: string;
443
+ };
444
+ interface WorkflowTransition {
445
+ to: string;
446
+ when?: {
447
+ $ref: string;
448
+ eq?: unknown;
449
+ exists?: boolean;
450
+ };
451
+ }
452
+ interface WorkflowStep {
453
+ id: string;
454
+ /** exactly ONE kind: */
455
+ agent?: string;
456
+ tool?: string;
457
+ human?: {
458
+ prompt: string;
459
+ field?: AskField;
460
+ };
461
+ /** agent steps: */
462
+ instruction?: string;
463
+ /** tool steps (values may be a WorkflowRef): */
464
+ args?: Record<string, unknown>;
465
+ /** agent steps (values may be a WorkflowRef): */
466
+ input?: Record<string, unknown>;
467
+ next?: string | WorkflowTransition[];
468
+ onError?: "fail" | {
469
+ to: string;
470
+ } | {
471
+ retry: number;
472
+ };
473
+ }
474
+ interface WorkflowSpec {
475
+ name: string;
476
+ compliance: WorkflowCompliance;
477
+ description?: string;
478
+ steps: WorkflowStep[];
479
+ start?: string;
480
+ }
481
+ /** A normalized, engine-held workflow (the output of `defineWorkflow`). */
482
+ interface WorkflowDef {
483
+ name: string;
484
+ compliance: WorkflowCompliance;
485
+ description?: string;
486
+ steps: WorkflowStep[];
487
+ /** Resolved start step id (default: steps[0].id). */
488
+ start: string;
489
+ /** Set when authored via `defineAgent.workflow` (per-agent scope). */
490
+ scopeAgent?: string;
491
+ }
492
+ /**
493
+ * The durable state of an in-flight STRICT workflow run (Phase B). Rides the existing run snapshot payload
494
+ * (not a new table). `cursor` = the step to run next; `outputs` = accumulated step results (for `$ref`);
495
+ * `generationIndex` = the monotonic per-run reserve counter continued across steps; `pending` marks a
496
+ * human/approval suspend so `resume()` re-enters the driver (synthetic `followupId`+`toolCallId` satisfy the
497
+ * resume-auth cross-check). Progress snapshotting only — NOT crash-mid-step replay.
498
+ */
499
+ interface WorkflowRunState {
500
+ workflow: string;
501
+ cursor: string;
502
+ outputs: Record<string, unknown>;
503
+ generationIndex: number;
504
+ pending?: {
505
+ kind: "human" | "approval";
506
+ stepId: string;
507
+ followupId: string;
508
+ toolCallId: string;
509
+ };
510
+ }
145
511
  interface AgentDef {
146
512
  slug: string;
147
513
  head: AgentVersion;
@@ -154,6 +520,82 @@ interface AgentDef {
154
520
  skills?: {
155
521
  name: string;
156
522
  }[];
523
+ /** Per-agent rules (engine-local, v1) — `defineAgent` stamps `scopeAgent` so `defineSwarm` can collect them. */
524
+ rules?: RuleDef[];
525
+ /** Per-agent workflow/procedure (engine-local, v1) — `scopeAgent`-stamped by `defineAgent`. */
526
+ workflow?: WorkflowDef;
527
+ }
528
+ /** A closure dependency satisfied by ANOTHER bundle: a delegate slug that is not a member of this bundle. Carried
529
+ * as a flat min-version floor (no transitive resolution in v1). */
530
+ interface BundleDep {
531
+ slug: string;
532
+ minVersion: number;
533
+ }
534
+ /**
535
+ * BN1 — a declarative connector grant: a member may invoke a connector's actions. The action names are folded into
536
+ * that member's `skillNames`, so the host's connector-tools resolver (`SwarmConfig.connectorTools`) materializes
537
+ * them per-tenant at CALL time, gated by the SP5 approval floor. The bundle carries NAMES ONLY — never a token,
538
+ * connection id, or backend; the host wires the connector + per-tenant credentials. A granted action need not have
539
+ * a first-party skill handle (it is connector-backed), so it is the explicit, validated exception to BN0's
540
+ * "every skill must resolve to a handle" closure rule.
541
+ */
542
+ interface ConnectorGrant {
543
+ /** The bundle member granted these actions (must be one of the bundle's `agents`). */
544
+ agentSlug: string;
545
+ /** The connector provider, e.g. `"slack"` — informational, and the prefix used to expand a short action name. */
546
+ provider: string;
547
+ /** Connector action names — full (`"slack.post_message"`) or short (`"post_message"`, expanded to `provider.action`). */
548
+ actions: string[];
549
+ }
550
+ /** Authoring input for `defineBundle`. It composes `defineAgent` OUTPUTS — it does not replace them. */
551
+ interface BundleSpec {
552
+ slug: string;
553
+ title?: string;
554
+ /** The composed members — exactly the output of `defineAgent` (skill handles ride along on each `AgentDef`). */
555
+ agents: AgentDef[];
556
+ /** SWARM-scoped rules (per-agent rules ride on the `AgentDef`s via `defineAgent`'s `scopeAgent` stamp). */
557
+ rules?: RuleDef[];
558
+ /** SWARM-scoped workflows (per-agent workflows ride on the `AgentDef`s). */
559
+ workflows?: WorkflowDef[];
560
+ /** BN1 — connector grants: a member may invoke a provider's actions (names only; the host materializes them). */
561
+ connectorGrants?: ConnectorGrant[];
562
+ /** Delegates that live in ANOTHER bundle (a delegate slug not among `agents`) — declared so closure validation
563
+ * can distinguish a legitimate external dependency from a typo. */
564
+ requires?: BundleDep[];
565
+ }
566
+ /** The validated, closure-checked composition `mergeBundle` folds into a `SwarmConfig`. The members carry any
567
+ * BN1 connector-grant action names folded into their `skillNames`. */
568
+ interface BundleDef {
569
+ slug: string;
570
+ title?: string;
571
+ agents: AgentDef[];
572
+ rules: RuleDef[];
573
+ workflows: WorkflowDef[];
574
+ connectorGrants: ConnectorGrant[];
575
+ requires: BundleDep[];
576
+ }
577
+ /** The publishable bundle payload (version is derived, not authored — mirrors `AgentVersionContent`). */
578
+ interface BundleVersionContent {
579
+ slug: string;
580
+ title?: string;
581
+ /** Composed members as serializable heads (no skill handles). */
582
+ agents: AgentVersionContent[];
583
+ rules: RuleDef[];
584
+ workflows: WorkflowDef[];
585
+ connectorGrants: ConnectorGrant[];
586
+ requires: BundleDep[];
587
+ }
588
+ /** One immutable, append-only bundle version (mirrors `AgentVersion` one level up). */
589
+ interface BundleVersion extends BundleVersionContent {
590
+ version: number;
591
+ }
592
+ /** A bundle version's summary row (for `listVersions` — mirrors `AgentVersionInfo`). */
593
+ interface BundleVersionInfo {
594
+ version: number;
595
+ title: string;
596
+ status: string;
597
+ isCurrent: boolean;
598
+ memberCount: number;
157
599
  }
158
600
  interface RunRow {
159
601
  runId: string;
@@ -210,6 +652,11 @@ interface EventStore {
210
652
  append(e: SwarmEvent): Promise<number>;
211
653
  /** Tenant-scoped (R11): a forged cross-org runId returns []. The store enforces tenancy, not just the caller. */
212
654
  list(tenantId: string, runId: string, sinceSeq: number): Promise<SwarmEvent[]>;
655
+ /** The full event log for a CONTAINER — every run in the conversation (the root thread + lane sub-threads
656
+ * `<container>:<slug>`), globally ordered by the generated `seq`. Lets a host rebuild a thread's RICH timeline
657
+ * (tool calls + delegation cards) on reload, where message-history is text-only. Tenant-scoped. Optional: a
658
+ * store without an events table may omit it (the host then falls back to message history). */
659
+ listForContainer?(tenantId: string, container: string): Promise<SwarmEvent[]>;
213
660
  subscribe(runId: string): AsyncIterable<SwarmEvent>;
214
661
  }
215
662
  interface RunStore {
@@ -276,8 +723,75 @@ interface AgentRepo {
276
723
  getVersion(tenantId: string, slug: string, version: number): Promise<AgentVersion | null>;
277
724
  listSlugs(tenantId: string): Promise<string[]>;
278
725
  }
726
+ /** The publishable content of one agent version — everything an `AgentVersion` carries EXCEPT the `version`
727
+ * number, which is derived (append-only `max+1`) by the repo, never supplied by the caller. */
728
+ type AgentVersionContent = Omit<AgentVersion, "version">;
729
+ /** One row of an agent's version history (for the no-code builder's rollback UX). Plain/@mastra-free. */
730
+ interface AgentVersionInfo {
731
+ version: number;
732
+ role: string;
733
+ modelId: string;
734
+ status: string;
735
+ isCurrent: boolean;
736
+ }
737
+ /**
738
+ * The WRITABLE agent definition contract (SP6) — "the one true framework extension" the no-code builder hangs
739
+ * off. Extends the read-only `AgentRepo` with the append-only mutation surface:
740
+ * - `publish` → commit a new head version of `content`; returns the derived version number.
741
+ * - `rollback` → republish a prior version's content as a NEW head (append-only, `git revert` not
742
+ * `reset`); returns the new version + the source it was restored from.
743
+ * - `listVersions` → the agent's version history (oldest→newest), flagging the current head.
744
+ *
745
+ * Every mutation takes a `SwarmActor` (the principal) — NOT a bare string — and the implementation MUST:
746
+ * 1. call `assertActorMayMutateDefinition(actor)` FIRST (the non-bypassable agent-bar), then
747
+ * 2. consult the configured `guardDefinitionMutation` policy hook (if any),
748
+ * before committing. `listVersions` takes the actor too so an impl can authorize reads consistently (and so
749
+ * the agent-bar applies uniformly); reads do not mutate, so an impl MAY allow an `agent` actor to list, but
750
+ * the shipped impls apply the same bar for symmetry.
751
+ */
752
+ interface VersionedRepo extends AgentRepo {
753
+ publish(tenantId: string, slug: string, content: AgentVersionContent, actor: SwarmActor): Promise<{
754
+ version: number;
755
+ }>;
756
+ rollback(tenantId: string, slug: string, toVersion: number, actor: SwarmActor): Promise<{
757
+ version: number;
758
+ restoredFrom: number;
759
+ }>;
760
+ listVersions(tenantId: string, slug: string, actor: SwarmActor): Promise<AgentVersionInfo[]>;
761
+ }
762
+ /** BN2 — the read-only bundle version repo (head/getVersion/listSlugs). Structurally identical to `AgentRepo`,
763
+ * one level up: a `BundleVersion` is the unit, not an agent. */
764
+ interface BundleRepo {
765
+ head(tenantId: string, slug: string): Promise<BundleVersion | null>;
766
+ getVersion(tenantId: string, slug: string, version: number): Promise<BundleVersion | null>;
767
+ listSlugs(tenantId: string): Promise<string[]>;
768
+ }
769
+ /** BN2 — the WRITABLE bundle version repo (append-only publish/rollback/listVersions). Mirrors `VersionedRepo`;
770
+ * every mutation enforces the same non-bypassable actor-bar (an `agent` principal can never publish a bundle). */
771
+ interface BundleWritableRepo extends BundleRepo {
772
+ publish(tenantId: string, slug: string, content: BundleVersionContent, actor: SwarmActor): Promise<{
773
+ version: number;
774
+ }>;
775
+ rollback(tenantId: string, slug: string, toVersion: number, actor: SwarmActor): Promise<{
776
+ version: number;
777
+ restoredFrom: number;
778
+ }>;
779
+ listVersions(tenantId: string, slug: string, actor: SwarmActor): Promise<BundleVersionInfo[]>;
780
+ }
279
781
  interface StorageAdapter {
782
+ /** Read-only agent definitions (always present). The supabase adapter's `agents` is also a `VersionedRepo`,
783
+ * but the base contract stays `AgentRepo` so a read-only adapter compiles without growing a write path. */
280
784
  agents: AgentRepo;
785
+ /** Opt-in WRITABLE agent definitions (SP6). Present on adapters that back a definition store (the supabase
786
+ * adapter); omitted by read-only adapters (storage-local, the in-memory dev store) — a no-code builder
787
+ * requires an adapter that provides it. When present it is the SAME object as `agents` (a `VersionedRepo`
788
+ * IS an `AgentRepo`), surfaced under a distinct field so the writable surface is explicit + tree-checkable. */
789
+ agentsWritable?: VersionedRepo;
790
+ /** BN2 — opt-in WRITABLE bundle versions (append-only, same actor-bar). Present on adapters that back a bundle
791
+ * store (the supabase adapter); omitted by read-only/in-memory adapters. `bundles` is the read surface,
792
+ * `bundlesWritable` the same object's write surface — mirrors the `agents`/`agentsWritable` pattern. */
793
+ bundles?: BundleRepo;
794
+ bundlesWritable?: BundleWritableRepo;
281
795
  runs: RunStore;
282
796
  events: EventStore;
283
797
  messages: MessageStore;
@@ -292,11 +806,16 @@ interface StorageAdapter {
292
806
  */
293
807
  recordSuspend?(runId: string, tenantId: string, followupId: string, toolCallId: string): void | Promise<void>;
294
808
  /**
295
- * Mark a followup as answered so it can no longer be resumed (the engine calls this when a `resume`
296
- * begins). Closes a replay hole: without it, `findSuspended` keeps returning the followup and the same
297
- * answer can be replayed indefinitely. Idempotent; tenant-scoped. Awaited by the engine.
809
+ * Mark a followup answered so it can no longer be resumed (the engine calls this when a `resume` begins).
810
+ * Closes a replay hole: without it, `findSuspended` keeps returning the followup and the same answer can be
811
+ * replayed indefinitely. Tenant-scoped.
812
+ *
813
+ * **Compare-and-set (K4):** returns `true` if THIS call transitioned the followup unanswered→answered, `false`
814
+ * if it was already answered. An out-of-band reply path uses this as the single answer-once guard — two
815
+ * distinct inbound replies for one followup race here, and only the `true` winner resumes (the loser ACKs).
816
+ * The engine's own resume ignores the return (it already passed `findSuspended`).
298
817
  */
299
- markFollowupAnswered?(followupId: string, tenantId: string): void | Promise<void>;
818
+ markFollowupAnswered?(followupId: string, tenantId: string): boolean | Promise<boolean>;
300
819
  /**
301
820
  * Cross-process cache invalidation (R12). Subscribe to agent-republish notifications so an engine on ANY
302
821
  * instance can evict its agent-row cache immediately instead of waiting out the TTL. `onInvalidate` receives
@@ -357,7 +876,29 @@ interface RunInput {
357
876
  * (those come from `SwarmContext`); treat its contents as opaque, attacker-controllable input.
358
877
  */
359
878
  context?: Record<string, unknown>;
879
+ /**
880
+ * Phase B: run a named STRICT workflow via the step-driver instead of the free-form agent turn. Engine/host-
881
+ * side seam — set by a host calling the runner/engine directly; NOT exposed on the public chat route or MCP
882
+ * (the wall promise). An unknown name throws before the run row is created.
883
+ */
884
+ workflow?: string;
885
+ }
886
+ /** The verdict from a {@link CompletionVerifier}: was the user's request actually satisfied, and if not, a
887
+ * short description of what's still missing (used to build a targeted continue-nudge). */
888
+ interface CompletionVerdict {
889
+ complete: boolean;
890
+ missing?: string;
360
891
  }
892
+ /**
893
+ * Completion supervisor hook (reliability) — see EngineOpts.verifyCompletion. Given the original request + a
894
+ * transcript of what the run produced, decide whether the task is genuinely done. Host-supplied (typically a
895
+ * cheap LLM judge). FAIL-SAFE at the call site: a throw/timeout is treated as "complete" (never trap a run).
896
+ */
897
+ type CompletionVerifier = (args: {
898
+ request: string;
899
+ transcript: string;
900
+ ctx: SwarmContext;
901
+ }) => Promise<CompletionVerdict> | CompletionVerdict;
361
902
  interface Runner {
362
903
  run(input: RunInput, ctx: SwarmContext): AsyncIterable<SwarmEvent>;
363
904
  enqueue(input: RunInput, ctx: SwarmContext): Promise<{
@@ -369,6 +910,22 @@ interface Runner {
369
910
  followupId: string;
370
911
  answer: unknown;
371
912
  }, ctx: SwarmContext): AsyncIterable<SwarmEvent>;
913
+ /**
914
+ * Durable resume (symmetric with `enqueue`): wake a suspended run and return its `runId` WITHOUT streaming the
915
+ * continuation — the resumed events reach the client over its EXISTING Realtime subscription. A streaming-only
916
+ * runner omits this; the durable runner provides it. Streaming the resume instead would never close (the
917
+ * Realtime subscribe has no terminal), so the client's `answer()` would hang. Also re-wakes a continuation lost
918
+ * to a process restart, from the durable snapshot (see the background runner).
919
+ */
920
+ resumeEnqueue?(args: {
921
+ runId: string;
922
+ toolCallId: string;
923
+ followupId: string;
924
+ answer: unknown;
925
+ context?: Record<string, unknown>;
926
+ }, ctx: SwarmContext): Promise<{
927
+ runId: string;
928
+ }>;
372
929
  }
373
930
 
374
931
  type ByType<T extends SwarmEvent["type"]> = Extract<SwarmEvent, {
@@ -422,88 +979,116 @@ declare class InMemoryContainerFloor implements ContainerFloor {
422
979
  /** Process-wide singleton. In-memory → single-process only (serverless instances don't share it). */
423
980
  declare const containerFloor: ContainerFloor;
424
981
 
425
- interface Price {
426
- inUsdPerMtok: number;
427
- outUsdPerMtok: number;
428
- }
429
- declare const PRICE_TABLE: Record<string, Price>;
430
- declare class CostGovernor {
431
- private opts;
432
- private steps;
433
- private usd;
434
- private prices;
435
- constructor(opts: {
436
- maxSteps: number;
437
- maxCostUsd: number;
438
- prices?: Record<string, Price>;
439
- });
440
- step(): void;
441
- /** Price a single usage WITHOUT accumulating it (for per-generation telemetry cost). */
442
- priceOf(modelId: string, u: {
443
- inputTokens: number;
444
- outputTokens: number;
445
- }): number;
446
- addUsage(modelId: string, u: {
447
- inputTokens: number;
448
- outputTokens: number;
449
- }): void;
450
- costUsd(): number;
451
- shouldStop(): {
452
- stop: boolean;
453
- reason?: string;
454
- };
982
+ type ModelTier = "swift" | "genius";
983
+ type ModelRef = string;
984
+ /** Context handed to the per-task escalation hook so it can decide whether a specific generation needs Genius. */
985
+ interface TierEscalationContext {
986
+ tenantId: string;
987
+ /** The agent the generation is for. */
988
+ agentSlug: string;
989
+ /** The agent's declared modelId (a tier sentinel when it opted into routing; the concrete pin otherwise). */
990
+ pinnedModelId?: string;
455
991
  }
456
992
  /**
457
- * Per-delegate USD sub-budgets (R5). The global `cost.maxCostUsd` bounds the WHOLE turn; this adds an
458
- * optional ceiling applied to EACH delegate (sub-agent) so one runaway sub-agent can't burn the entire
459
- * turn before the global cap notices. `maxCostUsd` is the default ceiling for every delegate; `bySlug`
460
- * overrides it per delegate slug (a slug listed there is capped even if there is no default). The run's
461
- * root orchestrator is NOT a delegate and is never capped here — the global cap already covers it.
993
+ * The tier configuration (lives on `SwarmConfig.models.tier` `EngineOpts.tier`). @mastra-free.
994
+ * - `tiers.swift` — the cheap DEFAULT model. REQUIRED (the floor every non-pinned agent lands on).
995
+ * - `tiers.genius` the frontier model. OPTIONAL; reachable ONLY through the premium gate below.
996
+ * - `default` — the tier the bare `"tier:"` sentinel resolves to. Default `"swift"` (cheap-default).
997
+ * - `allowGenius` — the SERVER-ENFORCED OPT-IN GATE. Default `false`. This is NOT a user-facing "smart"
998
+ * slider: it lives in the platform-set EngineOpts so a pack/agent config CANNOT grant itself
999
+ * Genius. With it false, any Genius request (a `tier:genius` agent OR an escalation) is
1000
+ * DOWNGRADED to Swift so the run still proceeds cheaply (deny-vs-downgrade: we DOWNGRADE).
1001
+ * - `escalate` — optional per-task hook (configurable per pack): given the generation's context it may bump
1002
+ * the chosen tier to `"genius"` (e.g. an ambiguous case). STILL subject to `allowGenius` —
1003
+ * escalation cannot bypass the premium gate.
462
1004
  */
463
- interface PerDelegateBudget {
464
- /** Default USD ceiling per delegate. Omit to cap only the slugs named in `bySlug`. */
465
- maxCostUsd?: number;
466
- /** Per-slug overrides of `maxCostUsd`. */
467
- bySlug?: Record<string, {
468
- maxCostUsd?: number;
469
- }>;
1005
+ interface TierConfig {
1006
+ tiers: {
1007
+ swift: ModelRef;
1008
+ genius?: ModelRef;
1009
+ };
1010
+ default?: ModelTier;
1011
+ allowGenius?: boolean;
1012
+ escalate?: (ctx: TierEscalationContext) => ModelTier | undefined;
470
1013
  }
471
- /** Tracks per-delegate USD spend and reports the first delegate to exceed its budget. Priced from the same
472
- * table as CostGovernor (via the shared `priceUsage`) so the global and per-delegate caps agree. Usage
473
- * attributed to the root orchestrator slug is ignored. */
474
- declare class DelegateBudgets {
475
- private cfg;
476
- private rootSlug;
477
- private usd;
478
- private prices;
479
- constructor(cfg: PerDelegateBudget, rootSlug: string, prices?: Record<string, Price>);
480
- /** The USD cap for a delegate: its `bySlug` override if present, else the default. `undefined` → uncapped. */
481
- private capFor;
482
- /** Accumulate one generation's usage against a delegate. No-op for the root orchestrator (not a delegate). */
483
- addUsage(slug: string, modelId: string, u: {
484
- inputTokens: number;
485
- outputTokens: number;
486
- }): void;
487
- /** The first delegate that has met or exceeded its USD cap, or null. */
488
- exceeded(): {
489
- slug: string;
490
- reason: string;
491
- } | null;
1014
+ /** The outcome of routing a single generation's model. `modelId` is always the EFFECTIVE id to use next. */
1015
+ interface TierResolution {
1016
+ /** The effective modelId to hand to the allow-list + factory. */
1017
+ modelId: string;
1018
+ /** The tier actually landed on. Undefined when the agent pinned a concrete model (a pin is not a tier). */
1019
+ tier?: ModelTier;
1020
+ /** True when a Genius request was downgraded to Swift (gate closed, or no genius model configured). */
1021
+ downgraded: boolean;
1022
+ /** The tier that was REQUESTED before gating (set when it differs from `tier`, i.e. on a downgrade). */
1023
+ requestedTier?: ModelTier;
1024
+ /** True when the escalation hook bumped the tier (to genius) and that bump survived the gate. */
1025
+ escalated?: boolean;
492
1026
  }
1027
+ /** Is this declared modelId a tier sentinel (opting into routing) rather than a concrete pin? */
1028
+ declare function isTierSentinel(modelId: string | undefined): boolean;
1029
+ /**
1030
+ * Resolve the effective model for ONE generation, applying tier routing + the premium Genius gate + escalation.
1031
+ *
1032
+ * Order of precedence:
1033
+ * 1. A CONCRETE PIN (non-sentinel modelId) is returned verbatim — routing/escalation never touch a pin.
1034
+ * 2. Otherwise the requested tier = the sentinel's tier (or the config default for a bare `"tier:"`).
1035
+ * 3. The optional `escalate` hook may bump the request to `"genius"`.
1036
+ * 4. THE GATE: a `"genius"` request survives ONLY when `allowGenius` is true AND a `genius` model is
1037
+ * configured; otherwise it DOWNGRADES to `"swift"` (the run proceeds cheaply; `downgraded` is flagged).
1038
+ */
1039
+ declare function resolveTier(modelId: string, cfg: TierConfig, ctx: TierEscalationContext): TierResolution;
1040
+ /**
1041
+ * The engine-facing convenience: given an agent's declared modelId, return the EFFECTIVE modelId after tier
1042
+ * routing. When no tier config is configured this is the identity function (today's behaviour). The resulting
1043
+ * modelId then flows through the SAME allow-list validation + factory mapping as before — a tier model is never
1044
+ * exempt from the allow-list.
1045
+ */
1046
+ declare function tierModelId(modelId: string, cfg: TierConfig | undefined, ctx: TierEscalationContext): string;
493
1047
 
494
1048
  interface EngineOpts {
495
1049
  storage: StorageAdapter;
496
1050
  model: ModelProvider;
497
1051
  modelFactory: (modelId: string, agentSlug?: string) => unknown;
498
- /** Global per-run caps + optional per-delegate sub-budgets (R5: `perDelegate` stops one runaway sub-agent
499
- * from burning the whole turn before the global `maxCostUsd` notices). */
1052
+ /**
1053
+ * SP10 the cheap-model router (Swift/Genius tiers). @mastra-free. When set, the per-agent model resolver
1054
+ * routes a tier-sentinel `modelId` (`"tier:swift"` / `"tier:genius"` / bare `"tier:"`) to the configured tier
1055
+ * model; a concrete pinned `modelId` is kept verbatim (routing layers OVER pinning). `tiers.swift` is the
1056
+ * cheap DEFAULT every non-pinned agent lands on; `tiers.genius` is reachable ONLY through the server-enforced
1057
+ * `allowGenius` premium gate (default false) — set HERE in EngineOpts so a pack/agent config cannot grant itself
1058
+ * Genius. An optional per-task `escalate` hook may bump a generation to Genius, still subject to the gate. When
1059
+ * a Genius request is denied by the gate it DOWNGRADES to Swift so the run proceeds cheaply. Omit ⇒ no routing
1060
+ * (identical to today: the agent's own `modelId` flows through, still allow-list-validated). The routed model
1061
+ * is ALWAYS re-validated by the allow-list `model` provider — a tier model is never exempt.
1062
+ */
1063
+ tier?: TierConfig;
1064
+ /** Global per-run caps + metering config (SP1) + optional per-delegate sub-budgets (R5: `perDelegate` stops
1065
+ * one runaway sub-agent from burning the whole turn before the global `maxCostUsd` notices).
1066
+ * - `prices`: static per-model overrides, merged over PRICE_TABLE.
1067
+ * - `priceFeed`: optional live price seam (numbers only — engine wall). Merged over `prices`.
1068
+ * - `failOnUnknownModel` (default false): an unpriced model THROWS instead of pricing at $0.
1069
+ * - `onCapHit` (SP9-core, default "stop"): the GLOBAL run-level cost/step cap's behaviour when reached.
1070
+ * "stop" (DEFAULT) = today's terminal `run_failed` stage "cost" — byte-identical to before.
1071
+ * "ask" = PAUSE the run and ASK the user "Budget cap reached — continue?" (a CONSUMER-context opt-in),
1072
+ * reusing SP5's suspend→swarm.question→resume machinery. Resume-approve RAISES the budget by
1073
+ * `capIncrementUsd` and continues; resume-reject terminally stops (stage "cost"). The
1074
+ * per-delegate cap (`perDelegate`) is NOT folded into the ask flow — it stays terminal.
1075
+ * - `capIncrementUsd` (SP9-core): the additional USD headroom an "ask" APPROVE grants (`maxCostUsd +=`).
1076
+ * Defaults to the original `maxCostUsd` ("another budget's worth"). Only meaningful with `onCapHit:"ask"`. */
500
1077
  cost: {
501
1078
  maxSteps: number;
502
1079
  maxCostUsd: number;
503
1080
  perDelegate?: PerDelegateBudget;
1081
+ prices?: Record<string, Price>;
1082
+ priceFeed?: PriceFeed;
1083
+ failOnUnknownModel?: boolean;
1084
+ onCapHit?: "stop" | "ask";
1085
+ capIncrementUsd?: number;
504
1086
  };
505
1087
  /** Per-swarm skill resolver. When omitted, agents expose only built-in tools. */
506
1088
  resolveSkill?: (name: string) => SwarmSkill | undefined;
1089
+ /** PR2 — opt-in per-request connector-tools resolver (tenant-scoped). Forwarded verbatim to `buildMastraAgent`
1090
+ * for the orchestrator + every sub-agent. Connector-agnostic: core never imports `@nightowlsdev/connectors`. */
1091
+ connectorTools?: (ctx: SwarmContext) => Promise<SwarmTool[]>;
507
1092
  /**
508
1093
  * Mastra storage backend for suspend/resume snapshots. Resume is storage-gated
509
1094
  * (SPIKE-FINDINGS item 5): the in-memory default cannot survive process death.
@@ -531,9 +1116,58 @@ interface EngineOpts {
531
1116
  };
532
1117
  /** Opt-in `recall_lane` tool (Part E). Read-only peer-lane transcript read. */
533
1118
  recallLane?: boolean;
1119
+ /** Phase A soft tier: per-agent soft-policy lines (advise rules + advisory-workflow summaries) appended to
1120
+ * each agent's system prompt. Built by `defineSwarm` from the swarm's rules/workflows. Omit ⇒ no policy. */
1121
+ softPolicy?: (slug: string) => string[];
1122
+ /** Phase B: swarm-level STRICT workflows, runnable by name via `RunInput.workflow`. Built by `defineSwarm`. */
1123
+ workflows?: WorkflowDef[];
1124
+ /** Phase B: per-agent STRICT workflows (keyed by agent slug) — replace that agent's turn when it owns the run. */
1125
+ agentWorkflows?: Record<string, WorkflowDef>;
534
1126
  /** Injectable per-lane floor (Part C / E3). Default: the in-memory process singleton. Pass a Postgres-backed
535
1127
  * floor (createPostgresFloor) for serverless / multi-instance deploys. */
536
1128
  floor?: ContainerFloor;
1129
+ /** Decision/observer hook dispatcher (SP2). `defineSwarm` always supplies one (allow-all when no hooks are
1130
+ * configured). When omitted (e.g. an engine built directly in a unit test), the engine defaults to an
1131
+ * allow-all dispatcher — behaviour identical to today. The engine AWAITS `preGeneration` before every model
1132
+ * launch; a `deny` vetoes the generation (terminal `run_failed` stage `"reserve"`). The same dispatcher's
1133
+ * `preToolCall` powers SP5's action-approval gate. */
1134
+ hooks?: HookDispatcher;
1135
+ /**
1136
+ * SP5 — the NON-REMOVABLE tool-approval policy (a P0 SAFETY control: spend caps limit cost, not harm). Forces
1137
+ * human approval on side-effecting tools regardless of the per-tool `needsApproval` flag, so a consumer pack
1138
+ * can't ship a `needsApproval:false` $0.50 action that causes $50k of damage. `defineSwarm` bakes this into the
1139
+ * `hooks` dispatcher (which combines policy + flag + the `preToolCall` hook), so when `hooks` is supplied THAT
1140
+ * dispatcher's policy is authoritative. This standalone field lets a DIRECT engine builder (e.g. a unit test
1141
+ * that passes no dispatcher) set the policy; the engine then builds an allow-all-hooks dispatcher WITH it.
1142
+ * Default `{ mode: "flag" }` — today's behaviour (only `needsApproval:true` tools gate).
1143
+ */
1144
+ toolApproval?: ToolApprovalPolicy;
1145
+ /**
1146
+ * SP15 — the optional SecretResolver the platform vault (SP15-platform) implements. When set, the engine
1147
+ * injects it on every run's RequestContext (SAME seam as SP5's ToolGate) so a first-party tool body can
1148
+ * `await ctx.secrets.resolve(ref)` to fetch a tenant-scoped secret at execution time. @mastra-free. Omit ⇒
1149
+ * `ctx.secrets.resolve(...)` yields `undefined` (no vault) and the no-secrets path is unchanged from today.
1150
+ */
1151
+ secrets?: SecretResolver;
1152
+ /**
1153
+ * SP3 — best-effort per-event OBSERVER, fired by the engine AFTER each event is persisted (in `emit`), for
1154
+ * BOTH `run` and `resume`. Transport-agnostic: it sees every event regardless of how it reaches the client
1155
+ * (interactive SSE vs durable + realtime), so platform metering (debit on `swarm.turn_usage`, settle on a
1156
+ * terminal) can live HERE rather than teeing the route's stream. Awaited but FAIL-SAFE — a throwing observer
1157
+ * is swallowed (the host logs its own errors), NEVER breaking the run, exactly like `telemetry`. Omit ⇒ no-op.
1158
+ */
1159
+ onEvent?: (ev: SwarmEvent, ctx: SwarmContext) => void | Promise<void>;
1160
+ /**
1161
+ * Completion supervisor (reliability) — an optional host check fired when a turn would END, to decide whether
1162
+ * the user's request was actually SATISFIED. The engine passes the original request + a transcript of what the
1163
+ * run produced; it returns `{ complete, missing? }`. When not complete the engine re-invokes the orchestrator
1164
+ * with a TARGETED nudge built from `missing` (same thread, full context), up to MAX_CONTINUE_NUDGES. If still
1165
+ * incomplete, the run ends `run_failed` stage "incomplete" (retryable) — a clear non-delivery the host can
1166
+ * refund — instead of a silent `done`. FAIL-SAFE: a throwing/rejecting verifier is treated as "complete"
1167
+ * (fail-open — never trap a run in a verify loop). Omit ⇒ the cheap structural "did the root speak last?"
1168
+ * fallback nudge is used instead.
1169
+ */
1170
+ verifyCompletion?: CompletionVerifier;
537
1171
  }
538
1172
  declare class SwarmEngine {
539
1173
  private opts;
@@ -541,11 +1175,56 @@ declare class SwarmEngine {
541
1175
  private rowCache;
542
1176
  private memory;
543
1177
  private floor;
1178
+ private hooks;
544
1179
  constructor(opts: EngineOpts);
1180
+ /** SP1: the swarm's metering config, in the shape DelegateBudgets/priceUsage expect. CostGovernor reads the
1181
+ * same fields directly off `opts.cost`; this packs them for the per-delegate tracker so both caps price
1182
+ * tokens identically (built-in PRICE_TABLE ← static `prices` ← live `priceFeed`, with `failOnUnknownModel`). */
1183
+ private pricingOpts;
1184
+ /** Fire the best-effort per-event observer (`EngineOpts.onEvent`). Awaited so an async observer (e.g. a
1185
+ * metering debit) completes in order, but FAIL-SAFE: a throw is swallowed (the host logs its own), never
1186
+ * breaking the run — same contract as the telemetry exporter. No-op when no observer is configured. */
1187
+ private notifyEvent;
1188
+ /** Run the completion supervisor (`EngineOpts.verifyCompletion`), FAIL-OPEN: no verifier, or a throwing one,
1189
+ * yields `{ complete: true }` so a missing/broken judge never traps a run in a verify loop. */
1190
+ private safeVerify;
1191
+ /** Best-effort recall of the run's ORIGINAL request (first user message on the thread) for the completion
1192
+ * verifier on RESUME, where the engine doesn't hold the opening message. Empty on any failure / no verifier. */
1193
+ private recallRequest;
545
1194
  /** Cached agent-row load shared by the three dynamic agent fns AND run/resume. */
546
1195
  private loadRow;
1196
+ /** Resolve an agent's STORED modelId — which may be a tier sentinel (`"tier:"` / `"tier:swift"`) — to the
1197
+ * CONCRETE model id the generation actually runs on, so metering/pricing + the preGeneration event see the
1198
+ * real model, not the sentinel (which has no price → every tier-routed turn would meter at $0). Mirrors
1199
+ * mastra-map's modelFor routing; with no tier config it returns the id unchanged. (SP10 pricing follow-up.) */
1200
+ private priceModelId;
547
1201
  private agent;
548
1202
  private requestContext;
1203
+ /**
1204
+ * SP5 — the action-approval gate handed to every gated tool via the RequestContext. Bound once (stable
1205
+ * reference). Delegates to the dispatcher's `preToolCall`, which is fail-closed (a throwing configured hook ⇒
1206
+ * deny) and applies the non-removable policy. The defineTool wrapper turns the returned `ToolDecision` into:
1207
+ * allow → run; deny → blocked result; ask → suspend-and-ask (the existing `swarm.question`/resume machinery).
1208
+ */
1209
+ private readonly toolGate;
1210
+ /**
1211
+ * SP5 truth-fix — resolve whether a tool WILL require approval, for the `swarm.tool_call` event's
1212
+ * `needsApproval` (the react reducer reads it to render an approval card). The mapChunk emit currently
1213
+ * hardcodes `false` (the truth-bug). This computes the truthful value from the SAME policy + per-tool flag the
1214
+ * gate uses: the tool's resolved `needsApproval` (its own flag, defaulting by origin) run through the
1215
+ * dispatcher's SYNC `policyDecision` — `ask` ⇒ true (it will gate), else false. The async `preToolCall` hook
1216
+ * can still escalate a specific call at execute time, but the policy-derived baseline is the truthful default
1217
+ * the UI needs without speculatively running the hook for every tool_call event.
1218
+ */
1219
+ private gatesApproval;
1220
+ /**
1221
+ * SP2: the preGeneration DECISION seam. Awaited immediately before each model launch (run + resume). The
1222
+ * dispatcher is fail-closed (a throwing hook ⇒ deny), so this only ever sees a clean `allow`/`deny`; a `deny`
1223
+ * THROWS `ReserveDenied` so the model call below never happens and the run/resume catch-all maps it to a
1224
+ * terminal `run_failed` stage "reserve" (NOT the generic "exception"). Allow-all + zero-overhead when no
1225
+ * hooks are configured (the default dispatcher returns allow synchronously-ish without invoking anything).
1226
+ */
1227
+ private guardGeneration;
549
1228
  /** Per-call Mastra memory ids + delegation, only when memory is configured (else stream is unchanged). */
550
1229
  private memoryOpts;
551
1230
  /**
@@ -606,11 +1285,28 @@ declare class SwarmEngine {
606
1285
  scratchpadPublic(container: string, ctx: SwarmContext): Promise<ScratchpadEntry[]>;
607
1286
  /** In-flight runs (running|suspended) for a container + its lanes — powers cross-lane background presence (E5). */
608
1287
  activeRuns(container: string, ctx: SwarmContext): Promise<ActiveRun[]>;
1288
+ /** The full, globally-ordered event log for a thread's CONTAINER (all its runs + lane sub-threads) — lets a host
1289
+ * rebuild the RICH timeline (tool calls + delegation cards) on reload, since message history is text-only.
1290
+ * Returns [] when the store has no events table (`listForContainer` unset). */
1291
+ threadEvents(threadId: string, ctx: SwarmContext): Promise<SwarmEvent[]>;
609
1292
  /** The tenant's agent roster (slug, title-cased display name, role, delegate graph) as wall-safe
610
1293
  * AgentSummary[]. Sourced from the agent rows; no vendor type in the signature or result. Powers
611
1294
  * the multi-agent pile / @mention UI. */
612
1295
  listAgents(ctx: SwarmContext): Promise<AgentSummary[]>;
613
1296
  run(input: RunInput, ctx: SwarmContext): AsyncIterable<SwarmEvent>;
1297
+ /**
1298
+ * Phase B — drive a STRICT workflow IN PLACE OF the free-form continue-nudge loop. Shared by `run()` (fresh)
1299
+ * and `resume()` (re-entry after a human/approval suspend). An `agent` step reuses `this.agent().stream()`
1300
+ * with a per-step requestContext (agentSlug = the step's agent) so it inherits persona/tools/gate/model/cost;
1301
+ * a `tool` step runs `executeToolWithGate`; a `human`/approval pause suspends SP9-style. Reserve, usage, and
1302
+ * the terminal turn_usage flow through the caller's machinery (`m`). Handles the terminal status/setStatus.
1303
+ */
1304
+ private driveWorkflow;
1305
+ /** A workflow `agent` step: stream `slug` with `message` (a per-step requestContext so it inherits the agent's
1306
+ * persona/tools/gate/model), reserving + metering through the caller's machinery, returning the final text. */
1307
+ private streamWorkflowAgentStep;
1308
+ /** A workflow `tool` step: run the gate-free tool body through `executeToolWithGate` (the engine-owned gate). */
1309
+ private runWorkflowToolStep;
614
1310
  resume(args: {
615
1311
  runId: string;
616
1312
  toolCallId: string;
@@ -619,6 +1315,24 @@ declare class SwarmEngine {
619
1315
  context?: Record<string, unknown>;
620
1316
  }, ctx: SwarmContext): AsyncIterable<SwarmEvent>;
621
1317
  }
1318
+ /**
1319
+ * SP2: a typed veto thrown when the `preGeneration` decision hook DENIES a model launch. Caught explicitly in
1320
+ * the run/resume catch-all so it maps to a TERMINAL `run_failed` stage `"reserve"` (mirroring the cost cap's
1321
+ * `"cost"` stage) instead of falling through to the generic `"exception"` stage. The model call has NOT
1322
+ * happened when this throws — the seam is BEFORE `stream`/`resumeStream`.
1323
+ */
1324
+ declare class ReserveDenied extends Error {
1325
+ readonly stage: "reserve";
1326
+ constructor(reason: string);
1327
+ }
1328
+
1329
+ /** The bound, run-scoped resolver handed to a tool body via `ctx.secrets`. `resolve(ref)` carries no ctx arg —
1330
+ * the run's tenant/auth scope is captured by the binding, so a tool can never resolve another tenant's secret
1331
+ * (the scope comes from the trusted RequestContext, NOT from tool args). Returns `undefined` when no resolver is
1332
+ * configured or the ref is unknown. */
1333
+ interface BoundSecrets {
1334
+ resolve(ref: string): Promise<string | undefined>;
1335
+ }
622
1336
 
623
1337
  interface ToolSpec<I, O> {
624
1338
  name: string;
@@ -633,9 +1347,15 @@ interface SwarmToolContext {
633
1347
  tenantId: string;
634
1348
  userId: string;
635
1349
  runId: string;
636
- secrets?: {
637
- resolve(ref: string): Promise<string>;
638
- };
1350
+ /**
1351
+ * SP15 — a run-scoped secret resolver (always present; bound to THIS run's tenant/auth ctx). A first-party
1352
+ * tool body calls `await ctx.secrets.resolve(ref)` to fetch a scoped secret value at execution time, mirroring
1353
+ * how the MCP connector resolves a credentialRef. The run's tenant scope is captured by the binding (NOT passed
1354
+ * by the tool), so a tool can never resolve another tenant's secret. Resolves to `undefined` when the swarm has
1355
+ * no SecretResolver configured (no vault) or the ref is unknown — never throws. Optional in the type only for
1356
+ * back-compat with code that constructs a bare ctx; the engine always populates it.
1357
+ */
1358
+ secrets?: BoundSecrets;
639
1359
  }
640
1360
  interface SwarmTool {
641
1361
  name: string;
@@ -655,22 +1375,100 @@ interface AgentSpec {
655
1375
  modelId?: string;
656
1376
  /** Per-agent memory OPTIONS override (R9), merged over the swarm `memory` config. Infra stays swarm-wide. */
657
1377
  memory?: AgentMemoryOverride;
1378
+ /** Per-agent rules (additive over swarm rules for THIS agent). Engine-local in v1 (not persisted/versioned). */
1379
+ rules?: RuleDef[];
1380
+ /** Per-agent workflow/procedure. Engine-local in v1. A strict one is rejected by `defineSwarm` until Phase B. */
1381
+ workflow?: WorkflowDef;
658
1382
  }
659
1383
  declare function defineAgent(spec: AgentSpec): AgentDef;
1384
+ /**
1385
+ * Normalize + validate a `RuleSpec` into a `RuleDef`. Plain data (no engine types) — the compiled rule the
1386
+ * engine holds. Validation is compile-time (throws): `enforce` requires an `action`; `ask` is TOOL-SEAM ONLY
1387
+ * (preGeneration is binary allow/deny — it cannot suspend); an `ask` cannot explicitly target a delegation
1388
+ * (`agent-*`) because `gateDelegation` defers `ask` to the sub-agent's inner gates (use `deny`).
1389
+ */
1390
+ declare function defineRule(spec: RuleSpec): RuleDef;
1391
+ /**
1392
+ * Normalize + validate a `WorkflowSpec` into a `WorkflowDef`. Validation is GRAPH-ONLY (pure code): unique step
1393
+ * ids, exactly one kind per step, `start`/`next`/`to`/`$ref` reference known steps, no cycles. Agent/tool
1394
+ * EXISTENCE is NOT validated here (it's runtime — `defineSwarm` doesn't validate delegate slugs and tenant DB
1395
+ * rows make it impossible; an unknown agent/tool surfaces as a runtime `run_failed` stage "workflow").
1396
+ */
1397
+ declare function defineWorkflow(spec: WorkflowSpec): WorkflowDef;
660
1398
  /** Build a per-swarm skill resolver from the agents' attached skill handles. */
661
1399
  declare function buildSkillResolver(agents: AgentDef[]): (name: string) => SwarmSkill | undefined;
1400
+ /**
1401
+ * Compose + CLOSURE-VALIDATE a capability bundle from `defineAgent` outputs (BN0 static composition + BN1 connector
1402
+ * grants). Pure normalizer/validator (no storage, no Mastra) — same posture as `defineRule`/`defineWorkflow`.
1403
+ * Validates, at author time, that the bundle is self-contained:
1404
+ * - every member `skillName` resolves to a first-party **handle** present on the bundle, OR is a declared
1405
+ * **connector grant** for that member (BN1 — connector-backed, materialized per-tenant by the host at runtime);
1406
+ * - every member delegate is a bundle member or a declared `requires` dependency;
1407
+ * - every tool-seam rule ref and every workflow `step.tool` resolves to a handle or any declared grant;
1408
+ * - no workflow step embeds a credential/connection ref.
1409
+ * So a missing handle/grant fails LOUD here, not as a runtime `run_failed`. Connector grants fold their action names
1410
+ * into the granted member's `skillNames` so the host's `connectorTools` resolver grants them by membership at call time.
1411
+ */
1412
+ declare function defineBundle(spec: BundleSpec): BundleDef;
1413
+ /**
1414
+ * Fold a validated bundle into a `SwarmConfig` (its agents + swarm-scoped rules/workflows) so the result is a
1415
+ * drop-in `defineSwarm` input. Per-agent rules/workflows ride on the merged `AgentDef`s and are collected by
1416
+ * `defineSwarm` exactly as for hand-authored agents — the bundle is a FRONT-END to `defineSwarm`, not a parallel
1417
+ * engine, so it adds no new runtime path. A bundle that re-declares an existing agent slug is a conflict (fail loud).
1418
+ */
1419
+ declare function mergeBundle(cfg: SwarmConfig, bundle: BundleDef): SwarmConfig;
1420
+ /**
1421
+ * Project a `BundleDef` (in-process, carrying skill HANDLES) into its SERIALIZABLE `BundleVersionContent` for
1422
+ * persistence (BN2): each member becomes its head's `AgentVersionContent` (the `version` + the handles dropped),
1423
+ * and the rules/workflows/connector-grants/deps carry through as the plain data they already are. This is the
1424
+ * bridge from BN0/BN1 (compose in-process) to BN2 (persist + version). The result has `skillNames` but no
1425
+ * handles — re-hydrating it into a live swarm (BN3 apply) needs a host-supplied handle manifest.
1426
+ */
1427
+ declare function toBundleContent(def: BundleDef): BundleVersionContent;
662
1428
  declare const ASK_TOOL_NAME = "ask";
663
1429
  interface SwarmConfig {
664
1430
  storage: StorageAdapter;
665
1431
  agents: AgentDef[];
1432
+ /**
1433
+ * The model allow-list + optional SP10 cheap-model router. `allow` is the per-tenant allow-set every
1434
+ * resolved model (incl. a tier model) must pass. `tier` (optional) enables Swift/Genius routing: a non-pinning
1435
+ * agent (tier-sentinel `modelId`) lands on the cheap `swift` model by default; `genius` is reachable ONLY via
1436
+ * the server-enforced `allowGenius` premium gate (a pack/agent config cannot grant itself Genius). See TierConfig.
1437
+ */
666
1438
  models: {
667
1439
  allow: string[];
1440
+ tier?: TierConfig;
668
1441
  };
669
1442
  modelFactory: (modelId: string, agentSlug?: string) => unknown;
1443
+ /**
1444
+ * PR2 — opt-in connector tools. Build with `materializeConnectors(connectors, backend)` from
1445
+ * `@nightowlsdev/connectors`; an agent gets a connector action only if the action `name` is in its
1446
+ * `skillNames` (i.e. it listed the action among its `skills`). Tenant-scoped + materialized per request.
1447
+ * Omit ⇒ no connector tools.
1448
+ */
1449
+ connectorTools?: (ctx: SwarmContext) => Promise<SwarmTool[]>;
1450
+ /**
1451
+ * Global per-run caps + metering config (SP1). `prices` statically overrides the built-in PRICE_TABLE;
1452
+ * `priceFeed` is an optional live seam supplying NUMBERS only (engine wall); `failOnUnknownModel` (default
1453
+ * false) makes an unpriced model THROW instead of pricing at $0. `perDelegate` adds optional per-delegate
1454
+ * USD sub-budgets (R5). All metering fields are threaded into the engine's CostGovernor + DelegateBudgets.
1455
+ *
1456
+ * SP9-core — the cap-that-asks (`onCapHit` + `capIncrementUsd`). `onCapHit:"ask"` (DEFAULT "stop") turns a
1457
+ * GLOBAL cost/step-cap hit into a PAUSE-and-ASK ("Budget cap reached — continue?") instead of a terminal
1458
+ * `run_failed`, so a consumer run can be granted more budget mid-task rather than dying. A pack sets this
1459
+ * SERVER-SIDE (it is not a per-agent flag). Resume-approve raises `maxCostUsd` by `capIncrementUsd` (default
1460
+ * = the original `maxCostUsd`) and continues; resume-reject terminally stops (stage "cost"). The per-delegate
1461
+ * cap is unaffected — it stays terminal. Leave `onCapHit` unset for today's behaviour (terminal stop).
1462
+ */
670
1463
  cost: {
671
1464
  maxSteps: number;
672
1465
  maxCostUsd: number;
673
1466
  perDelegate?: PerDelegateBudget;
1467
+ prices?: Record<string, Price>;
1468
+ priceFeed?: PriceFeed;
1469
+ failOnUnknownModel?: boolean;
1470
+ onCapHit?: "stop" | "ask";
1471
+ capIncrementUsd?: number;
674
1472
  };
675
1473
  /**
676
1474
  * Telemetry exporter(s). One or many — many are composed best-effort
@@ -701,6 +1499,62 @@ interface SwarmConfig {
701
1499
  /** Opt-in `recall_lane` tool (Part E): lets an agent read a peer agent's lane transcript in the same
702
1500
  * conversation. Read-only; reuses the engine's history (best-effort — empty without memory). */
703
1501
  recallLane?: boolean;
1502
+ /**
1503
+ * Opt-in decision/observer hooks (SP2). Types-only / engine-free (from `@nightowlsdev/hooks`). Today exposes a
1504
+ * `preGeneration` DECISION hook that the engine AWAITS immediately before EACH model launch (run + resume):
1505
+ * an `allow` proceeds, a `deny` VETOES the generation (terminal `run_failed` stage `"reserve"`, no model call).
1506
+ * Decision hooks are FAIL-CLOSED — a throwing hook is treated as a deny (a billing/safety veto must never
1507
+ * silently allow on error). Omit ⇒ allow-all, identical to prior behaviour with zero overhead. This is the
1508
+ * seam the platform's per-generation billing RESERVE (SP3) plugs into.
1509
+ */
1510
+ hooks?: SwarmHooks;
1511
+ /**
1512
+ * SP5 — the NON-REMOVABLE action-approval policy (a P0 SAFETY control). Forces human approval on
1513
+ * side-effecting tools regardless of the per-tool `needsApproval` flag, so a consumer pack cannot ship a
1514
+ * `needsApproval:false` $0.50 action that causes $50k of damage (spend caps limit cost, not harm). Two modes:
1515
+ * - `{ mode: "flag" }` (DEFAULT): today's behaviour — only `needsApproval:true` tools gate.
1516
+ * - `{ mode: "all-side-effecting" }`: force-ask EVERY non-read-only tool (every MCP tool + every first-party
1517
+ * tool not on the read-only allowlist), regardless of the per-tool flag. The safe default for an untrusted
1518
+ * consumer pack. Optionally override `readOnly` to customise the exempt set.
1519
+ * Baked into the `hooks` dispatcher, which combines policy + flag + the optional `preToolCall` hook into the
1520
+ * effective decision (allow / deny / ask-the-human). A `preToolCall` hook (in `hooks`) can add a richer gate.
1521
+ */
1522
+ toolApproval?: ToolApprovalPolicy;
1523
+ /**
1524
+ * SP15 — opt-in secret resolution for first-party tools. Pass a `SecretResolver` (the platform vault,
1525
+ * SP15-platform) and the engine scopes it per-run so a tool body can `await ctx.secrets.resolve(ref)` to fetch
1526
+ * a tenant-scoped secret at execution time — the same security posture as the MCP connector's credentialRef
1527
+ * resolution (resolve at execution, scoped by the live ctx, never from tool args). @mastra-free. Omit for
1528
+ * today's behaviour: `ctx.secrets.resolve(...)` yields `undefined` (no vault).
1529
+ */
1530
+ secrets?: SecretResolver;
1531
+ /**
1532
+ * SP3 — best-effort per-event OBSERVER, fired by the engine after each event is persisted (run + resume).
1533
+ * Transport-agnostic (sees every event regardless of interactive-SSE vs durable+realtime delivery), so the
1534
+ * platform's metering — debit on `swarm.turn_usage`, settle on a terminal — lives HERE instead of teeing the
1535
+ * route's stream. Awaited but FAIL-SAFE: a throwing observer is swallowed (log your own), never breaking the
1536
+ * run. The `preGeneration` reserve hook (above) + this observer are the two halves of the credit ledger.
1537
+ */
1538
+ onEvent?: (ev: SwarmEvent, ctx: SwarmContext) => void | Promise<void>;
1539
+ /**
1540
+ * Completion supervisor (reliability) — see EngineOpts.verifyCompletion. When set, the engine asks this at a
1541
+ * turn's end whether the user's request was actually satisfied, nudges the orchestrator with the specific gap
1542
+ * if not, and ends `run_failed:incomplete` (refundable) instead of a silent `done` if it still can't finish.
1543
+ * Omit ⇒ the cheap structural "did the root speak last?" fallback nudge.
1544
+ */
1545
+ verifyCompletion?: CompletionVerifier;
1546
+ /**
1547
+ * Declarative conditional policy (Phase A). `advise` rules are injected into the system prompt; `enforce`
1548
+ * rules compile into the decision hooks (deny/ask), folding in the non-removable SP5 policy floor. Per-agent
1549
+ * rules are authored via `defineAgent({ rules })` and applied additively.
1550
+ */
1551
+ rules?: RuleDef[];
1552
+ /**
1553
+ * Authorable procedures (Phase A: `advisory` only — an `advisory` workflow's `description` is injected as a
1554
+ * suggested procedure; `compliance: "strict"` is REJECTED until the Phase-B step-driver). Per-agent
1555
+ * procedures are authored via `defineAgent({ workflow })`.
1556
+ */
1557
+ workflows?: WorkflowDef[];
704
1558
  }
705
1559
  interface Swarm {
706
1560
  engine: SwarmEngine;
@@ -748,10 +1602,7 @@ declare class SpanCollector {
748
1602
  * Close the open generation with this step's usage + its own per-call cost (already priced from
749
1603
  * the step usage by the engine). `costUsd` is per-generation — never a cumulative running total.
750
1604
  */
751
- closeGeneration(usage: {
752
- inputTokens: number;
753
- outputTokens: number;
754
- }, costUsd: number): void;
1605
+ closeGeneration(usage: UsageBreakdown, costUsd: number): void;
755
1606
  openTool(toolCallId: string, name: string): void;
756
1607
  closeTool(toolCallId: string, ok: boolean): void;
757
1608
  /**
@@ -791,7 +1642,7 @@ declare class InMemoryStorage implements StorageAdapter {
791
1642
  private pads;
792
1643
  seedAgent(v: AgentVersion, tenantId?: string): void;
793
1644
  recordSuspend(runId: string, tenantId: string, followupId: string, toolCallId: string): void;
794
- markFollowupAnswered(followupId: string, tenantId: string): void;
1645
+ markFollowupAnswered(followupId: string, tenantId: string): boolean;
795
1646
  /** Test/host helper: read a run row (the RunStore interface is write-mostly). */
796
1647
  getRun(runId: string): RunRow | undefined;
797
1648
  events: EventStore;
@@ -806,9 +1657,57 @@ declare function composeSystemPrompt(row: AgentVersion): {
806
1657
  role: "system";
807
1658
  content: string;
808
1659
  }[];
1660
+ /**
1661
+ * Render the soft-policy lines for an agent (advise-rule statements + advisory-workflow summaries, from
1662
+ * `softPolicyFor`) as a single system message. Returns `[]` when there are none (zero overhead / no message).
1663
+ */
1664
+ declare function composePolicyPrompt(lines: string[]): {
1665
+ role: "system";
1666
+ content: string;
1667
+ }[];
809
1668
 
810
1669
  declare const customAuth: (fn: AuthProvider["authenticate"]) => AuthProvider;
811
1670
 
1671
+ interface RateLimitConfig {
1672
+ /** Window length in seconds. */
1673
+ windowSec: number;
1674
+ /** Max allowed events per window. */
1675
+ max: number;
1676
+ }
1677
+ interface RateLimitState {
1678
+ count: number;
1679
+ windowStartSec: number;
1680
+ }
1681
+ interface RateLimitDecision {
1682
+ allow: boolean;
1683
+ /** Remaining allowance in the current window (0 when denied). */
1684
+ remaining: number;
1685
+ /** Seconds until the window resets (when the count clears). */
1686
+ resetSec: number;
1687
+ }
1688
+ /**
1689
+ * Pure fixed-window rate-limit decision. If there's no prior state or the window has elapsed, a fresh window
1690
+ * starts at count 1 (this event). Otherwise the count increments. Allow while count ≤ max. Returns the decision
1691
+ * AND the next state to persist. Fixed-window is chosen for simplicity + O(1) state (one counter); its known
1692
+ * burst-at-boundary tradeoff is acceptable for an abuse backstop (not a billing meter).
1693
+ */
1694
+ declare function decideFixedWindow(prev: RateLimitState | null, cfg: RateLimitConfig, nowSec: number): {
1695
+ decision: RateLimitDecision;
1696
+ state: RateLimitState;
1697
+ };
1698
+ interface RateLimitStore {
1699
+ /** Record one event for `key` under `cfg` and return the decision. */
1700
+ hit(key: string, cfg: RateLimitConfig, nowSec: number): Promise<RateLimitDecision>;
1701
+ }
1702
+ /**
1703
+ * In-memory fixed-window store — a REAL limiter for a SINGLE instance. Keeps one window per key in a Map and
1704
+ * prunes expired keys opportunistically so memory stays bounded. NOT shared across instances: a horizontally
1705
+ * scaled deploy must back this with Redis/Postgres (same interface) or limits are per-instance.
1706
+ */
1707
+ declare function createInMemoryRateLimitStore(): RateLimitStore;
1708
+ /** Parse a "max/window" config from env (e.g. "60" with a default window), clamped to sane positive values. */
1709
+ declare function rateConfig(max: number | undefined, windowSec: number, fallbackMax: number): RateLimitConfig;
1710
+
812
1711
  declare const VERSION = "0.0.0";
813
1712
 
814
- export { ASK_TOOL_NAME, type ActiveRun, type AgentDef, type AgentMemoryOverride, type AgentRepo, type AgentSpec, type AgentSummary, type AgentVersion, type AskField, type AskFieldOption, type AuthContext, type AuthProvider, CapturingExporter, type ContainerFloor, CostGovernor, DelegateBudgets, type EngineOpts, type EventStore, type FloorHolder, GUARDRAILS, InMemoryContainerFloor, InMemoryStorage, type MemoryConfig, type MessageStore, type ModelProvider, type NewRun, PRICE_TABLE, type PerDelegateBudget, type Price, type Release, RowCache, type RunInput, type RunRow, type RunStatus, type RunStore, type Runner, SCRATCHPAD_MAX_ENTRY_CHARS, SCRATCHPAD_MAX_KEYS, type ScratchpadEntry, type ScratchpadStore, type SecretResolver, SpanCollector, type StorageAdapter, type Swarm, type SwarmConfig, type SwarmContext, SwarmEngine, type SwarmEvent, type SwarmMessage, type SwarmSkill, type SwarmSpan, type SwarmTool, type SwarmToolContext, type TelemetryExporter, type ThreadSummary, type ToolSpec, VERSION, allowListModelProvider, buildSkillResolver, composeSystemPrompt, compositeTelemetry, containerFloor, customAuth, customTelemetry, defineAgent, defineSkill, defineSwarm, defineTool, ev, isEvent, resolveTelemetry };
1713
+ export { ASK_TOOL_NAME, type ActiveRun, type AgentDef, type AgentMemoryOverride, AgentMutationForbidden, type AgentRepo, type AgentSpec, type AgentSummary, type AgentVersion, type AgentVersionContent, type AgentVersionInfo, type AskField, type AskFieldOption, type AuthContext, type AuthProvider, type BoundSecrets, type BundleDef, type BundleDep, type BundleRepo, type BundleSpec, type BundleVersion, type BundleVersionContent, type BundleVersionInfo, type BundleWritableRepo, CapturingExporter, type CompletionVerdict, type CompletionVerifier, type ConnectorGrant, type ContainerFloor, CostGovernor, DelegateBudgets, type EngineOpts, type EventStore, type FloorHolder, GUARDRAILS, InMemoryContainerFloor, InMemoryStorage, type MemoryConfig, type MessageStore, type ModelProvider, type ModelRef, type ModelTier, type NewRun, PRICE_TABLE, type PerDelegateBudget, type Price, type PriceFeed, type PricingOpts, type RateLimitConfig, type RateLimitDecision, type RateLimitState, type RateLimitStore, type Release, ReserveDenied, RowCache, type RuleAction, type RuleCondition, type RuleDef, type RuleLevel, type RuleSpec, type RunInput, type RunRow, type RunStatus, type RunStore, type Runner, SCRATCHPAD_MAX_ENTRY_CHARS, SCRATCHPAD_MAX_KEYS, type ScratchpadEntry, type ScratchpadStore, type SecretResolver, type SlugUsage, SpanCollector, type StorageAdapter, type Swarm, type SwarmActor, type SwarmConfig, type SwarmContext, SwarmEngine, type SwarmEvent, type SwarmMessage, type SwarmSkill, type SwarmSpan, type SwarmTool, type SwarmToolContext, type TelemetryExporter, type ThreadSummary, type TierConfig, type TierEscalationContext, type TierResolution, type ToolSpec, type TurnUsage, type UsageBreakdown, type UsageCost, VERSION, type VersionedRepo, type WorkflowCompliance, type WorkflowDef, type WorkflowRef, type WorkflowRunState, type WorkflowSpec, type WorkflowStep, type WorkflowTransition, allowListModelProvider, assertActorMayMutateDefinition, buildSkillResolver, composePolicyPrompt, composeSystemPrompt, compositeTelemetry, containerFloor, createInMemoryRateLimitStore, customAuth, customTelemetry, decideFixedWindow, defineAgent, defineBundle, defineRule, defineSkill, defineSwarm, defineTool, defineWorkflow, ev, isEvent, isTierSentinel, mergeBundle, priceUsage, rateConfig, resolveTelemetry, resolveTier, sumBreakdowns, sumTurnUsage, tierModelId, toBundleContent };