@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.cjs CHANGED
@@ -20,14 +20,20 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ ALLOW: () => import_hooks4.ALLOW,
24
+ ALLOW_TOOL: () => import_hooks4.ALLOW_TOOL,
23
25
  ASK_TOOL_NAME: () => ASK_TOOL_NAME,
26
+ AgentMutationForbidden: () => AgentMutationForbidden,
24
27
  CapturingExporter: () => CapturingExporter,
25
28
  CostGovernor: () => CostGovernor,
29
+ DEFAULT_READ_ONLY_TOOLS: () => import_hooks4.DEFAULT_READ_ONLY_TOOLS,
26
30
  DelegateBudgets: () => DelegateBudgets,
27
31
  GUARDRAILS: () => GUARDRAILS,
32
+ HookDispatcher: () => import_hooks4.HookDispatcher,
28
33
  InMemoryContainerFloor: () => InMemoryContainerFloor,
29
34
  InMemoryStorage: () => InMemoryStorage,
30
35
  PRICE_TABLE: () => PRICE_TABLE,
36
+ ReserveDenied: () => ReserveDenied,
31
37
  RowCache: () => RowCache,
32
38
  SCRATCHPAD_MAX_ENTRY_CHARS: () => SCRATCHPAD_MAX_ENTRY_CHARS,
33
39
  SCRATCHPAD_MAX_KEYS: () => SCRATCHPAD_MAX_KEYS,
@@ -35,23 +41,55 @@ __export(index_exports, {
35
41
  SwarmEngine: () => SwarmEngine,
36
42
  VERSION: () => VERSION,
37
43
  allowListModelProvider: () => allowListModelProvider,
44
+ ask: () => import_hooks4.ask,
45
+ assertActorMayMutateDefinition: () => assertActorMayMutateDefinition,
38
46
  buildSkillResolver: () => buildSkillResolver,
47
+ composePolicyPrompt: () => composePolicyPrompt,
39
48
  composeSystemPrompt: () => composeSystemPrompt,
40
49
  compositeTelemetry: () => compositeTelemetry,
41
50
  containerFloor: () => containerFloor,
51
+ createHookDispatcher: () => import_hooks4.createHookDispatcher,
52
+ createInMemoryRateLimitStore: () => createInMemoryRateLimitStore,
42
53
  customAuth: () => customAuth,
43
54
  customTelemetry: () => customTelemetry,
55
+ decideFixedWindow: () => decideFixedWindow,
44
56
  defineAgent: () => defineAgent,
57
+ defineBundle: () => defineBundle,
58
+ defineHook: () => import_hooks4.defineHook,
59
+ defineRule: () => defineRule,
45
60
  defineSkill: () => defineSkill,
46
61
  defineSwarm: () => defineSwarm,
47
62
  defineTool: () => defineTool,
63
+ defineWorkflow: () => defineWorkflow,
64
+ deny: () => import_hooks4.deny,
48
65
  ev: () => ev,
49
66
  isEvent: () => isEvent,
50
- resolveTelemetry: () => resolveTelemetry
67
+ isTierSentinel: () => isTierSentinel,
68
+ mergeBundle: () => mergeBundle,
69
+ priceUsage: () => priceUsage,
70
+ rateConfig: () => rateConfig,
71
+ resolveTelemetry: () => resolveTelemetry,
72
+ resolveTier: () => resolveTier,
73
+ sumBreakdowns: () => sumBreakdowns,
74
+ sumTurnUsage: () => sumTurnUsage,
75
+ tierModelId: () => tierModelId,
76
+ toBundleContent: () => toBundleContent
51
77
  });
52
78
  module.exports = __toCommonJS(index_exports);
53
79
 
54
80
  // src/types.ts
81
+ function assertActorMayMutateDefinition(actor) {
82
+ if (actor.type === "agent") {
83
+ throw new AgentMutationForbidden(actor.agentSlug);
84
+ }
85
+ }
86
+ var AgentMutationForbidden = class extends Error {
87
+ code = "AGENT_MUTATION_FORBIDDEN";
88
+ constructor(agentSlug) {
89
+ super(`agent principal "${agentSlug}" may not mutate an agent definition (publish/rollback)`);
90
+ this.name = "AgentMutationForbidden";
91
+ }
92
+ };
55
93
  var SCRATCHPAD_MAX_ENTRY_CHARS = 4e3;
56
94
  var SCRATCHPAD_MAX_KEYS = 64;
57
95
 
@@ -66,11 +104,244 @@ function isEvent(e, type) {
66
104
  // src/define.ts
67
105
  var import_zod4 = require("zod");
68
106
  var import_tools4 = require("@mastra/core/tools");
107
+ var import_hooks3 = require("@nightowlsdev/hooks");
69
108
 
70
109
  // src/engine.ts
71
110
  var import_mastra = require("@mastra/core/mastra");
72
111
  var import_storage = require("@mastra/core/storage");
73
112
  var import_request_context = require("@mastra/core/request-context");
113
+ var import_hooks = require("@nightowlsdev/hooks");
114
+
115
+ // src/tool-gate.ts
116
+ var TOOL_GATE_KEY = "__nightowlsdev_toolGate";
117
+ var TOOL_EXECUTORS = /* @__PURE__ */ new WeakMap();
118
+ function setToolExecutor(handle, exec) {
119
+ TOOL_EXECUTORS.set(handle, exec);
120
+ }
121
+ function getToolExecutor(handle) {
122
+ return TOOL_EXECUTORS.get(handle);
123
+ }
124
+ var ToolBlockedError = class extends Error {
125
+ constructor(toolName, reason) {
126
+ super(`tool "${toolName}" blocked: ${reason}`);
127
+ this.toolName = toolName;
128
+ this.reason = reason;
129
+ this.name = "ToolBlockedError";
130
+ }
131
+ toolName;
132
+ reason;
133
+ blocked = true;
134
+ };
135
+ function approvalSuspendPayload(args) {
136
+ const prompt = args.reason?.trim() ? `Approve \`${args.toolName}\`? ${args.reason.trim()}` : `Approve running \`${args.toolName}\`?`;
137
+ return {
138
+ to: "user",
139
+ prompt,
140
+ field: { kind: "confirm", confirmLabel: "Approve", rejectLabel: "Reject" },
141
+ asker: args.asker,
142
+ kind: "approval",
143
+ toolName: args.toolName
144
+ };
145
+ }
146
+ function isApproved(answer) {
147
+ if (typeof answer === "boolean") return answer;
148
+ if (typeof answer === "string") {
149
+ return /^(y|yes|approve|approved|ok|true|confirm|confirmed)$/i.test(answer.trim());
150
+ }
151
+ return false;
152
+ }
153
+ function gateErrMessage(err) {
154
+ return err instanceof Error ? err.message : String(err);
155
+ }
156
+ async function executeToolWithGate(opts) {
157
+ let decision;
158
+ try {
159
+ decision = await opts.gate(opts.ev);
160
+ } catch (err) {
161
+ return { ok: false, error: gateErrMessage(err), reason: gateErrMessage(err) };
162
+ }
163
+ if (decision.action === "deny") return { ok: false, error: decision.reason, reason: decision.reason };
164
+ if (decision.action === "ask") return { ok: false, suspended: true, reason: decision.reason };
165
+ try {
166
+ return { ok: true, result: await opts.run() };
167
+ } catch (err) {
168
+ return { ok: false, error: gateErrMessage(err) };
169
+ }
170
+ }
171
+ function toolPreCallEvent(args) {
172
+ return {
173
+ runId: args.runId,
174
+ tenantId: args.tenantId,
175
+ agentSlug: args.agentSlug,
176
+ toolName: args.toolName,
177
+ origin: args.origin,
178
+ needsApproval: args.needsApproval,
179
+ args: args.args
180
+ };
181
+ }
182
+
183
+ // src/secrets.ts
184
+ var SECRET_RESOLVER_KEY = "__nightowlsdev_secretResolver";
185
+ function bindSecrets(resolver, ctx) {
186
+ return {
187
+ resolve: (ref) => resolver ? resolver.resolve(ref, ctx) : Promise.resolve(void 0)
188
+ };
189
+ }
190
+
191
+ // src/step-driver.ts
192
+ function initialWorkflowState(wf) {
193
+ return { workflow: wf.name, cursor: wf.start, outputs: {}, generationIndex: 0 };
194
+ }
195
+ function resolveRef(ref, state, input) {
196
+ if (ref === "input") return input.message;
197
+ if (ref.startsWith("steps.")) return state.outputs[ref.slice("steps.".length)];
198
+ return void 0;
199
+ }
200
+ function resolveValue(v, state, input) {
201
+ if (v && typeof v === "object" && "$ref" in v) {
202
+ const ref = String(v.$ref);
203
+ if (ref.startsWith("steps.")) {
204
+ const id = ref.slice("steps.".length);
205
+ if (!(id in state.outputs)) {
206
+ throw new Error(`workflow $ref "${ref}" references step "${id}" which has not run (skipped branch or forward reference)`);
207
+ }
208
+ }
209
+ return resolveRef(ref, state, input);
210
+ }
211
+ return v;
212
+ }
213
+ function resolveMap(o, state, input) {
214
+ if (!o) return void 0;
215
+ const out = {};
216
+ for (const [k, v] of Object.entries(o)) out[k] = resolveValue(v, state, input);
217
+ return out;
218
+ }
219
+ function agentMessage(step, resolvedInput) {
220
+ const base2 = step.instruction ?? "";
221
+ if (resolvedInput && Object.keys(resolvedInput).length) return `${base2}
222
+
223
+ Context:
224
+ ${JSON.stringify(resolvedInput)}`;
225
+ return base2;
226
+ }
227
+ var DEAD_END = /* @__PURE__ */ Symbol("dead-end");
228
+ function nextStep(step, state, input) {
229
+ if (step.next === void 0) return void 0;
230
+ if (typeof step.next === "string") return step.next;
231
+ for (const t of step.next) {
232
+ if (!t.when) return t.to;
233
+ const v = resolveRef(t.when.$ref, state, input);
234
+ if (t.when.exists !== void 0) {
235
+ if (v !== void 0 === t.when.exists) return t.to;
236
+ continue;
237
+ }
238
+ if (t.when.eq !== void 0) {
239
+ if (v === t.when.eq) return t.to;
240
+ continue;
241
+ }
242
+ return t.to;
243
+ }
244
+ return DEAD_END;
245
+ }
246
+ var StepDriver = class {
247
+ constructor(wf, deps) {
248
+ this.wf = wf;
249
+ this.deps = deps;
250
+ }
251
+ wf;
252
+ deps;
253
+ ts = 0;
254
+ base(ctx) {
255
+ return { runId: ctx.runId, agentSlug: ctx.agentSlug, ts: this.deps.nextTs ? this.deps.nextTs() : this.ts++ };
256
+ }
257
+ /**
258
+ * Drive the workflow from `state` (fresh or resumed). Yields the run's SwarmEvents. Returns a `DriveOutcome`
259
+ * so the engine can finalize. B2 scope: linear `agent`/`tool` steps + `$ref` wiring + per-step snapshot.
260
+ */
261
+ async *drive(state, ctx, input) {
262
+ const byId = new Map(this.wf.steps.map((s) => [s.id, s]));
263
+ let guard = 0;
264
+ let retryStep = "";
265
+ let retriesLeft = 0;
266
+ const budget = this.wf.steps.length * 8 + 8;
267
+ while (true) {
268
+ if (guard++ > budget) return { kind: "failed", stage: "workflow", message: "step budget exceeded" };
269
+ const step = byId.get(state.cursor);
270
+ if (!step) return { kind: "failed", stage: "workflow", message: `unknown step "${state.cursor}"` };
271
+ yield ev("swarm.status", this.base(ctx), { state: step.tool ? "tool" : "thinking", note: `step:${step.id}` });
272
+ let stepError;
273
+ if (step.agent !== void 0) {
274
+ try {
275
+ const msg = agentMessage(step, resolveMap(step.input, state, input));
276
+ const { text } = yield* this.deps.runAgentStep(step.agent, msg, state.generationIndex, ctx);
277
+ state.outputs[step.id] = text;
278
+ state.generationIndex += 1;
279
+ } catch (err) {
280
+ if (err && typeof err === "object" && "stage" in err) throw err;
281
+ stepError = err instanceof Error ? err.message : String(err);
282
+ }
283
+ } else if (step.tool !== void 0) {
284
+ let args;
285
+ try {
286
+ args = resolveMap(step.args, state, input) ?? {};
287
+ } catch (err) {
288
+ stepError = err instanceof Error ? err.message : String(err);
289
+ }
290
+ if (args !== void 0) {
291
+ const toolCallId = `${ctx.runId}:wf:${step.id}`;
292
+ yield ev("swarm.tool_call", this.base(ctx), { toolCallId, name: step.tool, args, needsApproval: false });
293
+ const r = await this.deps.runToolStep(step.tool, args, ctx);
294
+ yield ev("swarm.tool_result", this.base(ctx), { toolCallId, ok: r.ok, result: r.result, error: r.error });
295
+ if (r.ok) state.outputs[step.id] = r.result;
296
+ else if (r.suspended) {
297
+ const followupId = `${ctx.runId}:wf:${step.id}`;
298
+ yield ev("swarm.question", this.base(ctx), { followupId, toolCallId: followupId, to: "user", prompt: r.reason ?? `Approve "${step.tool}"?`, field: { kind: "confirm" } });
299
+ state.pending = { kind: "approval", stepId: step.id, followupId, toolCallId: followupId };
300
+ await this.deps.saveState(ctx.runId, state);
301
+ return { kind: "suspended", state };
302
+ } else stepError = r.error ?? r.reason ?? "blocked";
303
+ }
304
+ } else if (step.human !== void 0) {
305
+ if (!(step.id in state.outputs)) {
306
+ const followupId = `${ctx.runId}:wf:${step.id}`;
307
+ yield ev("swarm.question", this.base(ctx), { followupId, toolCallId: followupId, to: "user", prompt: step.human.prompt, field: step.human.field });
308
+ state.pending = { kind: "human", stepId: step.id, followupId, toolCallId: followupId };
309
+ await this.deps.saveState(ctx.runId, state);
310
+ return { kind: "suspended", state };
311
+ }
312
+ state.pending = void 0;
313
+ }
314
+ if (stepError !== void 0) {
315
+ const oe = step.onError ?? "fail";
316
+ if (oe === "fail") return { kind: "failed", stage: "workflow", message: `step "${step.id}" failed: ${stepError}` };
317
+ if (typeof oe === "object" && "to" in oe) {
318
+ state.cursor = oe.to;
319
+ retryStep = "";
320
+ await this.deps.saveState(ctx.runId, state);
321
+ continue;
322
+ }
323
+ if (typeof oe === "object" && "retry" in oe) {
324
+ if (retryStep !== step.id) {
325
+ retryStep = step.id;
326
+ retriesLeft = oe.retry;
327
+ }
328
+ if (retriesLeft > 0) {
329
+ retriesLeft -= 1;
330
+ await this.deps.saveState(ctx.runId, state);
331
+ continue;
332
+ }
333
+ return { kind: "failed", stage: "workflow", message: `step "${step.id}" failed after retries: ${stepError}` };
334
+ }
335
+ }
336
+ retryStep = "";
337
+ const next = nextStep(step, state, input);
338
+ if (next === DEAD_END) return { kind: "failed", stage: "workflow", message: `no transition from step "${step.id}"` };
339
+ state.cursor = next ?? state.cursor;
340
+ await this.deps.saveState(ctx.runId, state);
341
+ if (next === void 0) return { kind: "done" };
342
+ }
343
+ }
344
+ };
74
345
 
75
346
  // src/mastra-map.ts
76
347
  var import_agent = require("@mastra/core/agent");
@@ -94,6 +365,16 @@ function composeSystemPrompt(row) {
94
365
  { role: "system", content: persona }
95
366
  ];
96
367
  }
368
+ function composePolicyPrompt(lines) {
369
+ if (!lines.length) return [];
370
+ return [
371
+ {
372
+ role: "system",
373
+ content: `Policy \u2014 follow these unless the user explicitly overrides:
374
+ ${lines.map((l) => `- ${l}`).join("\n")}`
375
+ }
376
+ ];
377
+ }
97
378
  function composeScratchpadPrompt(entries) {
98
379
  const render = (section) => {
99
380
  const rows = entries.filter((e) => e.section === section).map((e) => `- [${e.key}] (${e.author} \u2190 ${e.requestedBy}) ${e.content}`);
@@ -109,20 +390,100 @@ ${render("meta")}`
109
390
  return { role: "system", content };
110
391
  }
111
392
 
393
+ // src/tier.ts
394
+ var SENTINEL = "tier:";
395
+ function isTierSentinel(modelId) {
396
+ return typeof modelId === "string" && modelId.startsWith(SENTINEL);
397
+ }
398
+ function requestedTierFrom(modelId, cfg) {
399
+ const suffix = modelId.slice(SENTINEL.length).trim();
400
+ if (suffix === "swift" || suffix === "genius") return suffix;
401
+ return cfg.default ?? "swift";
402
+ }
403
+ function resolveTier(modelId, cfg, ctx) {
404
+ if (!isTierSentinel(modelId)) {
405
+ return { modelId, downgraded: false };
406
+ }
407
+ let requested = requestedTierFrom(modelId, cfg);
408
+ let escalated = false;
409
+ if (cfg.escalate) {
410
+ const bumped = cfg.escalate(ctx);
411
+ if (bumped === "genius" && requested !== "genius") {
412
+ requested = "genius";
413
+ escalated = true;
414
+ }
415
+ }
416
+ if (requested === "genius") {
417
+ const geniusAllowed = cfg.allowGenius === true && typeof cfg.tiers.genius === "string";
418
+ if (geniusAllowed) {
419
+ return { modelId: cfg.tiers.genius, tier: "genius", downgraded: false, ...escalated ? { escalated: true } : {} };
420
+ }
421
+ return { modelId: cfg.tiers.swift, tier: "swift", downgraded: true, requestedTier: "genius" };
422
+ }
423
+ return { modelId: cfg.tiers.swift, tier: "swift", downgraded: false };
424
+ }
425
+ function tierModelId(modelId, cfg, ctx) {
426
+ if (!cfg) return modelId;
427
+ return resolveTier(modelId, cfg, ctx).modelId;
428
+ }
429
+
112
430
  // src/mastra-map.ts
431
+ async function gateDelegation(rc, subSlug) {
432
+ const gate = rc.get(TOOL_GATE_KEY);
433
+ if (!gate) return;
434
+ const decision = await gate(
435
+ toolPreCallEvent({
436
+ runId: rc.get("runId") ?? "",
437
+ tenantId: rc.get("tenantId") ?? "default",
438
+ // The agent doing the delegating is the run owner / the parent in the path (the requestContext's agentSlug).
439
+ agentSlug: rc.get("agentSlug") ?? "",
440
+ toolName: `agent-${subSlug}`,
441
+ origin: "first-party",
442
+ // A delegation has no per-tool `needsApproval` flag; surface false so a "flag"-mode policy leaves it
443
+ // un-gated (today's behaviour) and only an "all-side-effecting" policy / an explicit hook can deny it.
444
+ needsApproval: false,
445
+ args: void 0
446
+ })
447
+ );
448
+ if (decision.action === "deny") {
449
+ throw new Error(`delegation to "${subSlug}" denied: ${decision.reason}`);
450
+ }
451
+ }
113
452
  var MAX_DELEGATION_DEPTH = 4;
453
+ var CONNECTOR_TOOLS_CACHE_KEY = "__nightowls_connector_tools";
114
454
  function memoryFor(args, row) {
115
455
  return args.resolveMemory ? args.resolveMemory(row) : args.memory;
116
456
  }
117
- function toolsFor(args, row) {
457
+ function toolsFor(args, row, connectorByName) {
118
458
  const out = { ...args.builtinTools ?? {} };
119
459
  for (const name of row.skillNames) {
120
- const skill = args.resolveSkill(name);
460
+ const skill = args.resolveSkill(name) ?? connectorByName?.[name];
121
461
  const mt = skill && __getMastraTool(skill);
122
462
  if (mt) out[name] = mt;
123
463
  }
124
464
  return out;
125
465
  }
466
+ async function connectorByNameFor(args, rc, agentSlug) {
467
+ if (!args.connectorTools) return {};
468
+ const cached = rc.get(CONNECTOR_TOOLS_CACHE_KEY);
469
+ if (cached) return cached;
470
+ const resolve = args.connectorTools;
471
+ const build = (async () => {
472
+ const ctx = {
473
+ tenantId: rc.get("tenantId") ?? "default",
474
+ userId: rc.get("userId") ?? "",
475
+ runId: rc.get("runId") ?? "",
476
+ agentSlug,
477
+ // informational — materialize is tenant-scoped; first caller's slug seeds the shared cache
478
+ threadId: rc.get("threadId") ?? ""
479
+ };
480
+ const out = {};
481
+ for (const t of await resolve(ctx)) out[t.name] = t;
482
+ return out;
483
+ })();
484
+ rc.set?.(CONNECTOR_TOOLS_CACHE_KEY, build);
485
+ return build;
486
+ }
126
487
  async function withScratchpad(args, base2, rc) {
127
488
  if (!args.loadScratchpad) return base2;
128
489
  const tenantId = rc.get("tenantId") ?? "default";
@@ -130,8 +491,13 @@ async function withScratchpad(args, base2, rc) {
130
491
  const entries = await args.loadScratchpad(container, tenantId);
131
492
  return [...base2, composeScratchpadPrompt(entries)];
132
493
  }
494
+ function withSoftPolicy(args, base2, slug) {
495
+ const soft = args.softPolicy?.(slug) ?? [];
496
+ return soft.length ? [...base2, ...composePolicyPrompt(soft)] : base2;
497
+ }
133
498
  async function modelFor(args, row, tenantId) {
134
- const id = await args.model.resolve(row.modelId, { tenantId });
499
+ const effective = tierModelId(row.modelId, args.tier, { tenantId, agentSlug: row.slug, pinnedModelId: row.modelId });
500
+ const id = await args.model.resolve(effective, { tenantId });
135
501
  return args.modelFactory(id, row.slug);
136
502
  }
137
503
  function buildSubAgent(args, row, depth, path) {
@@ -143,9 +509,12 @@ function buildSubAgent(args, row, depth, path) {
143
509
  // personality so the orchestrator's LLM knows WHAT this delegate is for (role is a coarse enum).
144
510
  description: row.personality || `Agent ${row.slug} (${row.role})`,
145
511
  ...memoryFor(args, row) ? { memory: memoryFor(args, row) } : {},
146
- instructions: async ({ requestContext }) => withScratchpad(args, composeSystemPrompt(row), requestContext),
512
+ instructions: async ({ requestContext }) => {
513
+ await gateDelegation(requestContext, row.slug);
514
+ return withScratchpad(args, withSoftPolicy(args, composeSystemPrompt(row), row.slug), requestContext);
515
+ },
147
516
  model: async ({ requestContext }) => await modelFor(args, row, requestContext.get("tenantId") ?? "default"),
148
- tools: toolsFor(args, row),
517
+ tools: (async ({ requestContext }) => toolsFor(args, row, await connectorByNameFor(args, requestContext, row.slug))),
149
518
  agents: async ({ requestContext }) => await buildSubAgentMap(
150
519
  args,
151
520
  row.delegateSlugs ?? [],
@@ -174,9 +543,16 @@ function buildMastraAgent(args) {
174
543
  // request). If Mastra rejects a dynamic `memory`, fall back to the static swarm Memory (root override is
175
544
  // then sub-agents-only — see the spec's accepted limitation).
176
545
  ...args.resolveMemory || args.memory ? { memory: (async ({ requestContext }) => memoryFor(args, await load(requestContext))) } : {},
177
- instructions: async ({ requestContext }) => withScratchpad(args, composeSystemPrompt(await load(requestContext)), requestContext),
546
+ instructions: async ({ requestContext }) => {
547
+ const row = await load(requestContext);
548
+ return withScratchpad(args, withSoftPolicy(args, composeSystemPrompt(row), row.slug), requestContext);
549
+ },
178
550
  model: async ({ requestContext }) => await modelFor(args, await load(requestContext), requestContext.get("tenantId") ?? "default"),
179
- tools: async ({ requestContext }) => ({ ...args.extraTools ?? {}, ...toolsFor(args, await load(requestContext)) }),
551
+ tools: (async ({ requestContext }) => {
552
+ const row = await load(requestContext);
553
+ const connectorByName = await connectorByNameFor(args, requestContext, row.slug);
554
+ return { ...args.extraTools ?? {}, ...toolsFor(args, row, connectorByName) };
555
+ }),
180
556
  // Delegation: the orchestrator's delegateSlugs become `agent-<slug>` tools (Mastra-native).
181
557
  agents: async ({ requestContext }) => {
182
558
  const row = await load(requestContext);
@@ -191,7 +567,7 @@ var import_tools = require("@mastra/core/tools");
191
567
  var import_zod = require("zod");
192
568
 
193
569
  // src/page-context.ts
194
- var PAGE_CONTEXT_KEY = "nightowls.pageContext";
570
+ var PAGE_CONTEXT_KEY = "__nightowlsdev_pageContext";
195
571
  function attachPageContext(rc, value) {
196
572
  rc.set(PAGE_CONTEXT_KEY, value ?? {});
197
573
  }
@@ -398,7 +774,7 @@ var InMemoryContainerFloor = class {
398
774
  s.held = who;
399
775
  if (s.timer) clearTimeout(s.timer);
400
776
  s.timer = setTimeout(() => {
401
- console.warn(`[nightowls] container floor force-released after ${this.maxHoldMs}ms: ${container} (held by ${who.label})`);
777
+ console.warn(`[@nightowlsdev/core] container floor force-released after ${this.maxHoldMs}ms: ${container} (held by ${who.label})`);
402
778
  this.release(container, s, who);
403
779
  }, this.maxHoldMs);
404
780
  if (typeof s.timer.unref === "function") s.timer.unref();
@@ -433,25 +809,84 @@ var PRICE_TABLE = {
433
809
  "openai/gpt-5.5": { inUsdPerMtok: 2.5, outUsdPerMtok: 10 },
434
810
  "openai/gpt-5.5-mini": { inUsdPerMtok: 0.3, outUsdPerMtok: 1.2 }
435
811
  };
436
- function priceUsage(prices, modelId, u) {
437
- const p = prices[modelId] ?? { inUsdPerMtok: 0, outUsdPerMtok: 0 };
438
- return u.inputTokens / 1e6 * p.inUsdPerMtok + u.outputTokens / 1e6 * p.outUsdPerMtok;
812
+ function priceUsage(prices, modelId, u, opts = {}) {
813
+ const p = prices[modelId];
814
+ if (!p) {
815
+ if (opts.failOnUnknownModel) {
816
+ throw new Error(
817
+ `[@nightowlsdev/core] no price entry for model '${modelId}' (failOnUnknownModel=true). Add it to PRICE_TABLE, the swarm cost.prices map, or a priceFeed.`
818
+ );
819
+ }
820
+ return 0;
821
+ }
822
+ const cacheReadRate = p.cacheReadUsdPerMtok ?? p.inUsdPerMtok;
823
+ const cacheWriteRate = p.cacheWriteUsdPerMtok ?? p.inUsdPerMtok;
824
+ const reasoningRate = p.reasoningUsdPerMtok ?? p.outUsdPerMtok;
825
+ const M = 1e6;
826
+ return (u.inputTokens ?? 0) / M * p.inUsdPerMtok + (u.outputTokens ?? 0) / M * p.outUsdPerMtok + (u.cacheReadTokens ?? 0) / M * cacheReadRate + (u.cacheWriteTokens ?? 0) / M * cacheWriteRate + (u.reasoningTokens ?? 0) / M * reasoningRate;
827
+ }
828
+ var OPTIONAL_USAGE_CLASSES = [
829
+ "cacheReadTokens",
830
+ "cacheWriteTokens",
831
+ "reasoningTokens",
832
+ "toolCalls",
833
+ "agentActivations"
834
+ ];
835
+ function sumBreakdowns(items) {
836
+ const total = { inputTokens: 0, outputTokens: 0 };
837
+ for (const b of items) {
838
+ total.inputTokens += b.inputTokens ?? 0;
839
+ total.outputTokens += b.outputTokens ?? 0;
840
+ for (const k of OPTIONAL_USAGE_CLASSES) {
841
+ const v = b[k];
842
+ if (v != null) total[k] = (total[k] ?? 0) + v;
843
+ }
844
+ }
845
+ return total;
846
+ }
847
+ function sumTurnUsage(items) {
848
+ const order = [];
849
+ const breakdownsBySlug = /* @__PURE__ */ new Map();
850
+ const usdBySlug = /* @__PURE__ */ new Map();
851
+ for (const it of items) {
852
+ if (!breakdownsBySlug.has(it.slug)) {
853
+ order.push(it.slug);
854
+ breakdownsBySlug.set(it.slug, []);
855
+ usdBySlug.set(it.slug, 0);
856
+ }
857
+ breakdownsBySlug.get(it.slug).push(it.breakdown);
858
+ usdBySlug.set(it.slug, usdBySlug.get(it.slug) + it.cost.usd);
859
+ }
860
+ const bySlug = order.map((slug) => {
861
+ const breakdown2 = sumBreakdowns(breakdownsBySlug.get(slug));
862
+ const usd2 = usdBySlug.get(slug);
863
+ return { slug, breakdown: breakdown2, cost: { usd: usd2, breakdown: breakdown2 } };
864
+ });
865
+ const breakdown = sumBreakdowns(items.map((it) => it.breakdown));
866
+ const usd = items.reduce((a, it) => a + it.cost.usd, 0);
867
+ return { breakdown, cost: { usd, breakdown }, bySlug };
439
868
  }
440
869
  var CostGovernor = class {
441
870
  constructor(opts) {
442
871
  this.opts = opts;
443
- this.prices = { ...PRICE_TABLE, ...opts.prices ?? {} };
872
+ this.prices = { ...PRICE_TABLE, ...opts.prices ?? {}, ...opts.priceFeed?.prices() ?? {} };
873
+ this.failOnUnknownModel = opts.failOnUnknownModel ?? false;
444
874
  }
445
875
  opts;
446
876
  steps = 0;
447
877
  usd = 0;
448
878
  prices;
879
+ failOnUnknownModel;
449
880
  step() {
450
881
  this.steps++;
451
882
  }
452
883
  /** Price a single usage WITHOUT accumulating it (for per-generation telemetry cost). */
453
884
  priceOf(modelId, u) {
454
- return priceUsage(this.prices, modelId, u);
885
+ return priceUsage(this.prices, modelId, u, { failOnUnknownModel: this.failOnUnknownModel });
886
+ }
887
+ /** Price a single usage WITHOUT accumulating it, returning the usd + the breakdown it was priced from. */
888
+ costOf(modelId, u) {
889
+ return { usd: this.priceOf(modelId, u), breakdown: u };
455
890
  }
456
891
  addUsage(modelId, u) {
457
892
  this.usd += this.priceOf(modelId, u);
@@ -459,6 +894,19 @@ var CostGovernor = class {
459
894
  costUsd() {
460
895
  return this.usd;
461
896
  }
897
+ /** The current USD cap (SP9-core: the cap-that-asks reads this to surface "spend / cap" + to compute the raise). */
898
+ get maxCostUsd() {
899
+ return this.opts.maxCostUsd;
900
+ }
901
+ /**
902
+ * SP9-core — RAISE the USD cap by `incrementUsd` (the budget an approved "Budget cap reached — continue?"
903
+ * grants). Mutates the governor's ceiling so a freshly-resumed generation isn't immediately re-blocked at the
904
+ * SAME cap; the run gets real additional headroom. Only the cap-that-asks resume path calls this; the default
905
+ * terminal-stop path never does, so today's behaviour is unchanged.
906
+ */
907
+ raiseCostCap(incrementUsd) {
908
+ this.opts.maxCostUsd += incrementUsd;
909
+ }
462
910
  shouldStop() {
463
911
  if (this.steps >= this.opts.maxSteps) return { stop: true, reason: "step cap reached" };
464
912
  if (this.usd >= this.opts.maxCostUsd) return { stop: true, reason: "USD cap reached" };
@@ -466,15 +914,17 @@ var CostGovernor = class {
466
914
  }
467
915
  };
468
916
  var DelegateBudgets = class {
469
- constructor(cfg, rootSlug, prices) {
917
+ constructor(cfg, rootSlug, pricing) {
470
918
  this.cfg = cfg;
471
919
  this.rootSlug = rootSlug;
472
- this.prices = { ...PRICE_TABLE, ...prices ?? {} };
920
+ this.prices = { ...PRICE_TABLE, ...pricing?.prices ?? {}, ...pricing?.priceFeed?.prices() ?? {} };
921
+ this.failOnUnknownModel = pricing?.failOnUnknownModel ?? false;
473
922
  }
474
923
  cfg;
475
924
  rootSlug;
476
925
  usd = /* @__PURE__ */ new Map();
477
926
  prices;
927
+ failOnUnknownModel;
478
928
  /** The USD cap for a delegate: its `bySlug` override if present, else the default. `undefined` → uncapped. */
479
929
  capFor(slug) {
480
930
  return this.cfg.bySlug?.[slug]?.maxCostUsd ?? this.cfg.maxCostUsd;
@@ -482,7 +932,10 @@ var DelegateBudgets = class {
482
932
  /** Accumulate one generation's usage against a delegate. No-op for the root orchestrator (not a delegate). */
483
933
  addUsage(slug, modelId, u) {
484
934
  if (slug === this.rootSlug) return;
485
- this.usd.set(slug, (this.usd.get(slug) ?? 0) + priceUsage(this.prices, modelId, u));
935
+ this.usd.set(
936
+ slug,
937
+ (this.usd.get(slug) ?? 0) + priceUsage(this.prices, modelId, u, { failOnUnknownModel: this.failOnUnknownModel })
938
+ );
486
939
  }
487
940
  /** The first delegate that has met or exceeded its USD cap, or null. */
488
941
  exceeded() {
@@ -510,7 +963,7 @@ function compositeTelemetry(exporters) {
510
963
  const results = await Promise.allSettled(exporters.map((e) => e.export(spans)));
511
964
  for (const r of results) {
512
965
  if (r.status === "rejected") {
513
- console.warn("[nightowls] telemetry exporter failed:", r.reason);
966
+ console.warn("[@nightowlsdev/core] telemetry exporter failed:", r.reason);
514
967
  }
515
968
  }
516
969
  }
@@ -557,10 +1010,17 @@ var SpanCollector = class {
557
1010
  */
558
1011
  closeGeneration(usage, costUsd) {
559
1012
  if (!this.gen) return;
1013
+ const extra = {};
1014
+ if (usage.cacheReadTokens != null) extra.cacheReadTokens = usage.cacheReadTokens;
1015
+ if (usage.cacheWriteTokens != null) extra.cacheWriteTokens = usage.cacheWriteTokens;
1016
+ if (usage.reasoningTokens != null) extra.reasoningTokens = usage.reasoningTokens;
1017
+ if (usage.toolCalls != null) extra.toolCalls = usage.toolCalls;
1018
+ if (usage.agentActivations != null) extra.agentActivations = usage.agentActivations;
560
1019
  this.gen.attributes = {
561
1020
  ...this.gen.attributes,
562
1021
  inputTokens: usage.inputTokens,
563
1022
  outputTokens: usage.outputTokens,
1023
+ ...extra,
564
1024
  costUsd: Math.max(0, costUsd)
565
1025
  };
566
1026
  this.gen.endedAt = this.now();
@@ -644,6 +1104,21 @@ var RowCache = class {
644
1104
 
645
1105
  // src/engine.ts
646
1106
  var AGENT_KEY = "swarm";
1107
+ var MAX_CONTINUE_NUDGES = 2;
1108
+ var CONTINUE_NUDGE = "[automated] Your previous turn ended without any message or tool call. If you have fully completed everything that was asked, give the final result now. Otherwise continue and finish every remaining step you intended \u2014 do not end your turn until the task is done.";
1109
+ function verifyNudge(missing) {
1110
+ const gap = (missing ?? "").trim();
1111
+ return gap ? `[automated] The task is NOT finished yet \u2014 still missing: ${gap}. Continue now and complete every remaining step. Do not end your turn until it is fully done.` : CONTINUE_NUDGE;
1112
+ }
1113
+ var VERIFY_TRANSCRIPT_CAP = 6e3;
1114
+ function appendTranscript(t, e) {
1115
+ let add = "";
1116
+ if (e.type === "swarm.message") add = e.data.delta ?? e.data.text ?? "";
1117
+ else if (e.type === "swarm.tool_call") add = `
1118
+ \xAB${e.agentSlug} \u2192 ${e.data.name}\xBB
1119
+ `;
1120
+ return add ? (t + add).slice(-VERIFY_TRANSCRIPT_CAP) : t;
1121
+ }
647
1122
  var SwarmEngine = class {
648
1123
  constructor(opts) {
649
1124
  this.opts = opts;
@@ -653,6 +1128,7 @@ var SwarmEngine = class {
653
1128
  const { memory, resolveMemory } = opts.memory ? buildMemoryResolver(opts.memory) : { memory: void 0, resolveMemory: void 0 };
654
1129
  this.memory = memory;
655
1130
  this.floor = opts.floor ?? containerFloor;
1131
+ this.hooks = opts.hooks ?? new import_hooks.HookDispatcher({}, opts.toolApproval ?? { mode: "flag" });
656
1132
  opts.storage.subscribeInvalidations?.((key) => this.rowCache.invalidate(key));
657
1133
  const agent = buildMastraAgent({
658
1134
  loadRow: (slug, tenantId) => this.loadRow(tenantId, slug),
@@ -660,6 +1136,8 @@ var SwarmEngine = class {
660
1136
  resolveSkill: (n) => opts.resolveSkill?.(n),
661
1137
  model: opts.model,
662
1138
  modelFactory: opts.modelFactory,
1139
+ // SP10: hand the cheap-model router to the per-agent model resolver. Undefined ⇒ no routing (today).
1140
+ tier: opts.tier,
663
1141
  builtinTools: {
664
1142
  [ASK_TOOL_NAME]: buildAskMastraTool(),
665
1143
  ...opts.scratchpad ? { scratchpad_write: buildScratchpadTool(opts.storage.scratchpad, typeof opts.scratchpad === "object" ? opts.scratchpad : void 0) } : {},
@@ -669,6 +1147,9 @@ var SwarmEngine = class {
669
1147
  // ONLY (never sub-agents) so the model can pull the host page's advisory RunInput.context.
670
1148
  ...opts.pageContext ? { extraTools: { get_page_context: buildPageContextTool() } } : {},
671
1149
  loadScratchpad: opts.scratchpad ? (c, t) => opts.storage.scratchpad.list(t, c) : void 0,
1150
+ softPolicy: opts.softPolicy,
1151
+ // PR2: per-request connector-tools resolver, granted to the orchestrator + sub-agents by skillNames.
1152
+ connectorTools: opts.connectorTools,
672
1153
  memory
673
1154
  });
674
1155
  this.mastra = new import_mastra.Mastra({
@@ -687,6 +1168,51 @@ var SwarmEngine = class {
687
1168
  // Typed `unknown` to keep the engine wall: no engine-vendor type escapes the public surface.
688
1169
  memory;
689
1170
  floor;
1171
+ // SP2: the decision-hook dispatcher. Always present — defaults to an allow-all dispatcher when the engine is
1172
+ // built without one (e.g. unit tests), so the preGeneration seam is uniform with no per-call null checks.
1173
+ hooks;
1174
+ /** SP1: the swarm's metering config, in the shape DelegateBudgets/priceUsage expect. CostGovernor reads the
1175
+ * same fields directly off `opts.cost`; this packs them for the per-delegate tracker so both caps price
1176
+ * tokens identically (built-in PRICE_TABLE ← static `prices` ← live `priceFeed`, with `failOnUnknownModel`). */
1177
+ pricingOpts() {
1178
+ return {
1179
+ prices: this.opts.cost.prices,
1180
+ priceFeed: this.opts.cost.priceFeed,
1181
+ failOnUnknownModel: this.opts.cost.failOnUnknownModel
1182
+ };
1183
+ }
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
+ async notifyEvent(e, ctx) {
1188
+ if (!this.opts.onEvent) return;
1189
+ try {
1190
+ await this.opts.onEvent(e, ctx);
1191
+ } catch {
1192
+ }
1193
+ }
1194
+ /** Run the completion supervisor (`EngineOpts.verifyCompletion`), FAIL-OPEN: no verifier, or a throwing one,
1195
+ * yields `{ complete: true }` so a missing/broken judge never traps a run in a verify loop. */
1196
+ async safeVerify(request, transcript, ctx) {
1197
+ if (!this.opts.verifyCompletion) return { complete: true };
1198
+ try {
1199
+ return await this.opts.verifyCompletion({ request, transcript, ctx });
1200
+ } catch (err) {
1201
+ console.error(`[@nightowlsdev/core] verifyCompletion threw for run ${ctx.runId} \u2014 treating as complete:`, err);
1202
+ return { complete: true };
1203
+ }
1204
+ }
1205
+ /** Best-effort recall of the run's ORIGINAL request (first user message on the thread) for the completion
1206
+ * verifier on RESUME, where the engine doesn't hold the opening message. Empty on any failure / no verifier. */
1207
+ async recallRequest(ctx) {
1208
+ if (!this.opts.verifyCompletion) return "";
1209
+ try {
1210
+ const msgs = await this.history(ctx.threadId, ctx, { limit: 50 });
1211
+ return msgs.find((m) => m.role === "user")?.text ?? "";
1212
+ } catch {
1213
+ return "";
1214
+ }
1215
+ }
690
1216
  /** Cached agent-row load shared by the three dynamic agent fns AND run/resume. */
691
1217
  loadRow(tenantId, slug) {
692
1218
  return this.rowCache.get(`${tenantId}:${slug}`, async () => {
@@ -695,6 +1221,13 @@ var SwarmEngine = class {
695
1221
  return row;
696
1222
  });
697
1223
  }
1224
+ /** Resolve an agent's STORED modelId — which may be a tier sentinel (`"tier:"` / `"tier:swift"`) — to the
1225
+ * CONCRETE model id the generation actually runs on, so metering/pricing + the preGeneration event see the
1226
+ * real model, not the sentinel (which has no price → every tier-routed turn would meter at $0). Mirrors
1227
+ * mastra-map's modelFor routing; with no tier config it returns the id unchanged. (SP10 pricing follow-up.) */
1228
+ priceModelId(rawModelId, tenantId, agentSlug) {
1229
+ return tierModelId(rawModelId, this.opts.tier, { tenantId, agentSlug, pinnedModelId: rawModelId });
1230
+ }
698
1231
  agent() {
699
1232
  return this.mastra.getAgent(AGENT_KEY);
700
1233
  }
@@ -703,8 +1236,44 @@ var SwarmEngine = class {
703
1236
  for (const [k, v] of Object.entries(ctx)) {
704
1237
  if (v !== void 0) rc.set(k, v);
705
1238
  }
1239
+ rc.set(TOOL_GATE_KEY, this.toolGate);
1240
+ if (this.opts.secrets) rc.set(SECRET_RESOLVER_KEY, this.opts.secrets);
706
1241
  return rc;
707
1242
  }
1243
+ /**
1244
+ * SP5 — the action-approval gate handed to every gated tool via the RequestContext. Bound once (stable
1245
+ * reference). Delegates to the dispatcher's `preToolCall`, which is fail-closed (a throwing configured hook ⇒
1246
+ * deny) and applies the non-removable policy. The defineTool wrapper turns the returned `ToolDecision` into:
1247
+ * allow → run; deny → blocked result; ask → suspend-and-ask (the existing `swarm.question`/resume machinery).
1248
+ */
1249
+ toolGate = (ev2) => this.hooks.preToolCall(ev2);
1250
+ /**
1251
+ * SP5 truth-fix — resolve whether a tool WILL require approval, for the `swarm.tool_call` event's
1252
+ * `needsApproval` (the react reducer reads it to render an approval card). The mapChunk emit currently
1253
+ * hardcodes `false` (the truth-bug). This computes the truthful value from the SAME policy + per-tool flag the
1254
+ * gate uses: the tool's resolved `needsApproval` (its own flag, defaulting by origin) run through the
1255
+ * dispatcher's SYNC `policyDecision` — `ask` ⇒ true (it will gate), else false. The async `preToolCall` hook
1256
+ * can still escalate a specific call at execute time, but the policy-derived baseline is the truthful default
1257
+ * the UI needs without speculatively running the hook for every tool_call event.
1258
+ */
1259
+ gatesApproval(toolName) {
1260
+ const skill = this.opts.resolveSkill?.(toolName);
1261
+ const origin = skill?.origin ?? "first-party";
1262
+ const needsApproval = skill?.needsApproval ?? origin === "mcp";
1263
+ const decision = this.hooks.policyDecision({ runId: "", agentSlug: "", toolName, origin, needsApproval });
1264
+ return decision.action === "ask";
1265
+ }
1266
+ /**
1267
+ * SP2: the preGeneration DECISION seam. Awaited immediately before each model launch (run + resume). The
1268
+ * dispatcher is fail-closed (a throwing hook ⇒ deny), so this only ever sees a clean `allow`/`deny`; a `deny`
1269
+ * THROWS `ReserveDenied` so the model call below never happens and the run/resume catch-all maps it to a
1270
+ * terminal `run_failed` stage "reserve" (NOT the generic "exception"). Allow-all + zero-overhead when no
1271
+ * hooks are configured (the default dispatcher returns allow synchronously-ish without invoking anything).
1272
+ */
1273
+ async guardGeneration(ev2) {
1274
+ const decision = await this.hooks.preGeneration(ev2);
1275
+ if (decision.action === "deny") throw new ReserveDenied(decision.reason);
1276
+ }
708
1277
  /** Per-call Mastra memory ids + delegation, only when memory is configured (else stream is unchanged). */
709
1278
  memoryOpts(ctx) {
710
1279
  if (!this.opts.memory) return {};
@@ -914,6 +1483,13 @@ var SwarmEngine = class {
914
1483
  async activeRuns(container, ctx) {
915
1484
  return this.opts.storage.runs.listActive(ctx.tenantId, container);
916
1485
  }
1486
+ /** The full, globally-ordered event log for a thread's CONTAINER (all its runs + lane sub-threads) — lets a host
1487
+ * rebuild the RICH timeline (tool calls + delegation cards) on reload, since message history is text-only.
1488
+ * Returns [] when the store has no events table (`listForContainer` unset). */
1489
+ async threadEvents(threadId, ctx) {
1490
+ const container = threadId.split(":")[0] || threadId;
1491
+ return await this.opts.storage.events.listForContainer?.(ctx.tenantId, container) ?? [];
1492
+ }
917
1493
  /** The tenant's agent roster (slug, title-cased display name, role, delegate graph) as wall-safe
918
1494
  * AgentSummary[]. Sourced from the agent rows; no vendor type in the signature or result. Powers
919
1495
  * the multi-agent pile / @mention UI. */
@@ -928,7 +1504,9 @@ var SwarmEngine = class {
928
1504
  }));
929
1505
  }
930
1506
  async *run(input, ctx) {
931
- const modelId = (await this.loadRow(ctx.tenantId, ctx.agentSlug)).modelId ?? "unknown";
1507
+ const modelId = this.priceModelId((await this.loadRow(ctx.tenantId, ctx.agentSlug)).modelId ?? "unknown", ctx.tenantId, ctx.agentSlug);
1508
+ const workflowDef = input.workflow ? this.opts.workflows?.find((w) => w.name === input.workflow) : this.opts.agentWorkflows?.[ctx.agentSlug];
1509
+ if (input.workflow && !workflowDef) throw new Error(`unknown workflow: ${input.workflow}`);
932
1510
  await this.opts.storage.runs.create({
933
1511
  runId: ctx.runId,
934
1512
  tenantId: ctx.tenantId,
@@ -936,18 +1514,31 @@ var SwarmEngine = class {
936
1514
  threadId: ctx.threadId,
937
1515
  agentSlug: ctx.agentSlug
938
1516
  });
939
- const modelIdFor = (slug) => this.rowCache.peek(`${ctx.tenantId}:${slug}`)?.modelId ?? modelId;
1517
+ const modelIdFor = (slug) => {
1518
+ const raw = this.rowCache.peek(`${ctx.tenantId}:${slug}`)?.modelId;
1519
+ return raw ? this.priceModelId(raw, ctx.tenantId, slug) : modelId;
1520
+ };
1521
+ const gatesApproval = (name) => this.gatesApproval(name);
940
1522
  const gov = new CostGovernor(this.opts.cost);
941
- const delegateBudgets = this.opts.cost.perDelegate ? new DelegateBudgets(this.opts.cost.perDelegate, ctx.agentSlug) : null;
1523
+ const delegateBudgets = this.opts.cost.perDelegate ? new DelegateBudgets(this.opts.cost.perDelegate, ctx.agentSlug, this.pricingOpts()) : null;
942
1524
  const streamed = /* @__PURE__ */ new Set();
1525
+ const activity = /* @__PURE__ */ new Map();
1526
+ const turnUsage = [];
943
1527
  const rc = this.requestContext(ctx);
944
1528
  if (this.opts.pageContext) attachPageContext(rc, input.context);
945
1529
  const collector = this.opts.telemetry ? new SpanCollector(ctx.runId, () => Date.now(), "run", { agentSlug: ctx.agentSlug }) : null;
946
1530
  let ts = 0;
947
1531
  const emit = async (e) => {
948
1532
  e.seq = await this.opts.storage.events.append(e);
1533
+ await this.notifyEvent(e, ctx);
949
1534
  return e;
950
1535
  };
1536
+ let turnEmitted = false;
1537
+ const emitTurn = async () => {
1538
+ if (turnEmitted) return null;
1539
+ turnEmitted = true;
1540
+ return emit(turnUsageEvent(ctx, ts++, turnUsage, 0));
1541
+ };
951
1542
  const floorKey = ctx.threadId;
952
1543
  const me = { label: titleCase(ctx.agentSlug), runId: ctx.runId };
953
1544
  const floorAbort = new AbortController();
@@ -961,96 +1552,289 @@ var SwarmEngine = class {
961
1552
  if (floorAbort.signal.aborted) return;
962
1553
  }
963
1554
  yield await emit(ev("swarm.status", base(ctx, ts++), { state: "thinking" }));
1555
+ if (workflowDef) {
1556
+ const wfMessage = input.message.replace(/^(\[user:[^\]]*\]\s*)+/, "");
1557
+ await emit(ev("swarm.message", base(ctx, ts++), { role: "user", text: wfMessage }));
1558
+ yield* this.driveWorkflow(workflowDef, initialWorkflowState(workflowDef), ctx, { message: wfMessage, context: input.context }, {
1559
+ gov,
1560
+ modelIdFor,
1561
+ streamed,
1562
+ delegateBudgets,
1563
+ activity,
1564
+ gatesApproval,
1565
+ turnUsage,
1566
+ nextTs: () => ts++,
1567
+ emit,
1568
+ emitTurn
1569
+ });
1570
+ return;
1571
+ }
1572
+ const generationIndex = 0;
1573
+ await this.guardGeneration({ runId: ctx.runId, tenantId: ctx.tenantId, agentSlug: ctx.agentSlug, modelId, generationIndex, kind: "run" });
964
1574
  const userMessage = input.message.replace(/^(\[user:[^\]]*\]\s*)+/, "");
965
- const result = await this.agent().stream(userMessage, { runId: ctx.runId, requestContext: rc, ...this.memoryOpts(ctx) });
966
- for await (const part of result.fullStream) {
967
- if (part?.type === "step-finish") gov.step();
968
- if (part?.type === "tool-call-suspended") {
969
- const payload = part.payload ?? {};
970
- const toolCallId = payload.toolCallId ?? "";
971
- const followupId = `${ctx.runId}:${toolCallId}`;
972
- const sp = payload.suspendPayload ?? {};
973
- await recordSuspend(this.opts.storage, ctx, followupId, toolCallId);
974
- await this.opts.storage.runs.setStatus(ctx.runId, "suspended");
975
- await this.opts.storage.runs.saveSnapshot(ctx.runId, { pending: { toolCallId } });
976
- yield await emit(ev("swarm.status", base(ctx, ts++), { state: "waiting" }));
977
- yield await emit(
978
- ev("swarm.question", base(ctx, ts++), {
979
- followupId,
980
- toolCallId,
981
- to: sp.to ?? "user",
982
- from: sp.asker || ctx.agentSlug,
983
- // the agent that actually asked (a delegate), for UI attribution
984
- prompt: sp.prompt ?? "",
985
- field: sp.field
986
- })
987
- );
988
- return;
989
- }
990
- if (part?.type === "error") {
991
- await this.opts.storage.runs.setStatus(ctx.runId, "failed");
992
- yield await emit(
993
- ev("swarm.run_failed", base(ctx, ts++), {
994
- stage: "stream",
995
- message: streamErrorMessage(part),
996
- retryable: false
997
- })
998
- );
999
- return;
1575
+ await emit(ev("swarm.message", base(ctx, ts++), { role: "user", text: userMessage }));
1576
+ let turnMessage = userMessage;
1577
+ let continueNudges = 0;
1578
+ let transcript = "";
1579
+ let incompleteVerdict = null;
1580
+ for (; ; ) {
1581
+ const result = await this.agent().stream(turnMessage, { runId: ctx.runId, requestContext: rc, ...this.memoryOpts(ctx) });
1582
+ let sawStep = false;
1583
+ let lastOutputSlug;
1584
+ for await (const part of result.fullStream) {
1585
+ if (part?.type === "step-finish") {
1586
+ gov.step();
1587
+ sawStep = true;
1588
+ }
1589
+ if (part?.type === "tool-call-suspended") {
1590
+ const payload = part.payload ?? {};
1591
+ const toolCallId = payload.toolCallId ?? "";
1592
+ const followupId = `${ctx.runId}:${toolCallId}`;
1593
+ const sp = payload.suspendPayload ?? {};
1594
+ await recordSuspend(this.opts.storage, ctx, followupId, toolCallId);
1595
+ await this.opts.storage.runs.setStatus(ctx.runId, "suspended");
1596
+ await this.opts.storage.runs.saveSnapshot(ctx.runId, { pending: { toolCallId }, genIndex: generationIndex + 1 });
1597
+ {
1598
+ const t = await emitTurn();
1599
+ if (t) yield t;
1600
+ }
1601
+ yield await emit(ev("swarm.status", base(ctx, ts++), { state: "waiting" }));
1602
+ yield await emit(
1603
+ ev("swarm.question", base(ctx, ts++), {
1604
+ followupId,
1605
+ toolCallId,
1606
+ to: sp.to ?? "user",
1607
+ from: sp.asker || ctx.agentSlug,
1608
+ // the agent that actually asked (a delegate), for UI attribution
1609
+ prompt: sp.prompt ?? "",
1610
+ field: sp.field
1611
+ })
1612
+ );
1613
+ return;
1614
+ }
1615
+ if (part?.type === "error") {
1616
+ await this.opts.storage.runs.setStatus(ctx.runId, "failed");
1617
+ {
1618
+ const t = await emitTurn();
1619
+ if (t) yield t;
1620
+ }
1621
+ yield await emit(
1622
+ ev("swarm.run_failed", base(ctx, ts++), {
1623
+ stage: "stream",
1624
+ message: streamErrorMessage(part),
1625
+ retryable: false
1626
+ })
1627
+ );
1628
+ return;
1629
+ }
1630
+ for (const e of mapChunk(part, ctx, gov, modelIdFor, () => ts++, streamed, delegateBudgets, activity, gatesApproval, turnUsage)) {
1631
+ if (e.type === "swarm.message" || e.type === "swarm.tool_call") {
1632
+ lastOutputSlug = e.agentSlug;
1633
+ if (this.opts.verifyCompletion) transcript = appendTranscript(transcript, e);
1634
+ }
1635
+ yield await emit(e);
1636
+ }
1637
+ collectSpans(collector, part, modelId, gov);
1638
+ const overDelegate = delegateBudgets?.exceeded();
1639
+ const stop = gov.shouldStop();
1640
+ if (stop.stop || overDelegate) {
1641
+ if (this.opts.cost.onCapHit === "ask" && stop.stop && !overDelegate) {
1642
+ const followupId = `${ctx.runId}:${CAP_FOLLOWUP_SUFFIX}`;
1643
+ await recordSuspend(this.opts.storage, ctx, followupId, CAP_FOLLOWUP_SUFFIX);
1644
+ await this.opts.storage.runs.setStatus(ctx.runId, "suspended");
1645
+ await this.opts.storage.runs.saveSnapshot(ctx.runId, {
1646
+ capHit: { message: userMessage, spentUsd: gov.costUsd() },
1647
+ genIndex: generationIndex + 1
1648
+ });
1649
+ {
1650
+ const t = await emitTurn();
1651
+ if (t) yield t;
1652
+ }
1653
+ yield await emit(ev("swarm.status", base(ctx, ts++), { state: "waiting" }));
1654
+ yield await emit(ev("swarm.question", base(ctx, ts++), capQuestion(ctx, followupId, gov)));
1655
+ return;
1656
+ }
1657
+ await this.opts.storage.runs.setStatus(ctx.runId, "failed");
1658
+ {
1659
+ const t = await emitTurn();
1660
+ if (t) yield t;
1661
+ }
1662
+ yield await emit(
1663
+ ev("swarm.run_failed", base(ctx, ts++), {
1664
+ stage: "cost",
1665
+ message: overDelegate?.reason ?? stop.reason,
1666
+ retryable: false
1667
+ })
1668
+ );
1669
+ return;
1670
+ }
1000
1671
  }
1001
- for (const e of mapChunk(part, ctx, gov, modelIdFor, () => ts++, streamed, delegateBudgets)) yield await emit(e);
1002
- collectSpans(collector, part, modelId, gov);
1003
- const overDelegate = delegateBudgets?.exceeded();
1004
- if (gov.shouldStop().stop || overDelegate) {
1005
- await this.opts.storage.runs.setStatus(ctx.runId, "failed");
1006
- yield await emit(
1007
- ev("swarm.run_failed", base(ctx, ts++), {
1008
- stage: "cost",
1009
- message: overDelegate?.reason ?? gov.shouldStop().reason,
1010
- retryable: false
1011
- })
1012
- );
1013
- return;
1672
+ if (this.opts.verifyCompletion) {
1673
+ const verdict = await this.safeVerify(userMessage, transcript, ctx);
1674
+ if (!verdict.complete && continueNudges < MAX_CONTINUE_NUDGES) {
1675
+ continueNudges++;
1676
+ turnMessage = verifyNudge(verdict.missing);
1677
+ continue;
1678
+ }
1679
+ incompleteVerdict = verdict.complete ? null : verdict;
1680
+ } else if (sawStep && lastOutputSlug !== ctx.agentSlug && continueNudges < MAX_CONTINUE_NUDGES) {
1681
+ continueNudges++;
1682
+ turnMessage = CONTINUE_NUDGE;
1683
+ continue;
1014
1684
  }
1685
+ break;
1015
1686
  }
1016
1687
  await this.mirrorDelegations(ctx);
1017
1688
  await this.attributeRun(ctx);
1018
- await this.opts.storage.runs.setStatus(ctx.runId, "done");
1019
- yield await emit(ev("swarm.status", base(ctx, ts++), { state: "done" }));
1689
+ if (incompleteVerdict) {
1690
+ await this.opts.storage.runs.setStatus(ctx.runId, "failed");
1691
+ {
1692
+ const t = await emitTurn();
1693
+ if (t) yield t;
1694
+ }
1695
+ yield await emit(ev("swarm.run_failed", base(ctx, ts++), { stage: "incomplete", message: incompleteVerdict.missing ?? "The task was not completed.", retryable: true }));
1696
+ } else {
1697
+ await this.opts.storage.runs.setStatus(ctx.runId, "done");
1698
+ {
1699
+ const t = await emitTurn();
1700
+ if (t) yield t;
1701
+ }
1702
+ yield await emit(ev("swarm.status", base(ctx, ts++), { state: "done" }));
1703
+ }
1020
1704
  } catch (err) {
1021
- console.error(`[nightowls] run ${ctx.runId} threw:`, err);
1705
+ const stage = err instanceof ReserveDenied ? "reserve" : "exception";
1706
+ if (stage !== "reserve") console.error(`[@nightowlsdev/core] run ${ctx.runId} threw:`, err);
1022
1707
  try {
1023
1708
  await this.opts.storage.runs.setStatus(ctx.runId, "failed");
1024
1709
  } catch {
1025
1710
  }
1026
- yield await emit(ev("swarm.run_failed", base(ctx, ts++), { stage: "exception", message: errMessage(err), retryable: false }));
1711
+ {
1712
+ const t = await emitTurn();
1713
+ if (t) yield t;
1714
+ }
1715
+ yield await emit(ev("swarm.run_failed", base(ctx, ts++), { stage, message: errMessage(err), retryable: false }));
1027
1716
  } finally {
1028
1717
  floorAbort.abort();
1029
1718
  await releaseFloor?.();
1030
1719
  await exportSpans(this.opts.telemetry, collector);
1031
1720
  }
1032
1721
  }
1722
+ /**
1723
+ * Phase B — drive a STRICT workflow IN PLACE OF the free-form continue-nudge loop. Shared by `run()` (fresh)
1724
+ * and `resume()` (re-entry after a human/approval suspend). An `agent` step reuses `this.agent().stream()`
1725
+ * with a per-step requestContext (agentSlug = the step's agent) so it inherits persona/tools/gate/model/cost;
1726
+ * a `tool` step runs `executeToolWithGate`; a `human`/approval pause suspends SP9-style. Reserve, usage, and
1727
+ * the terminal turn_usage flow through the caller's machinery (`m`). Handles the terminal status/setStatus.
1728
+ */
1729
+ async *driveWorkflow(wf, state, ctx, input, m) {
1730
+ const driver = new StepDriver(wf, {
1731
+ nextTs: m.nextTs,
1732
+ runAgentStep: (slug, message, genIndex) => this.streamWorkflowAgentStep(slug, message, genIndex, ctx, input, m),
1733
+ runToolStep: (toolName, args) => this.runWorkflowToolStep(toolName, args, ctx),
1734
+ saveState: (rid, st) => this.opts.storage.runs.saveSnapshot(rid, { workflow: st })
1735
+ });
1736
+ const it = driver.drive(state, ctx, input);
1737
+ let r = await it.next();
1738
+ while (!r.done) {
1739
+ yield await m.emit(r.value);
1740
+ r = await it.next();
1741
+ }
1742
+ const outcome = r.value;
1743
+ if (outcome.kind === "suspended") {
1744
+ const p = outcome.state.pending;
1745
+ await recordSuspend(this.opts.storage, ctx, p.followupId, p.toolCallId);
1746
+ await this.opts.storage.runs.setStatus(ctx.runId, "suspended");
1747
+ yield await m.emit(ev("swarm.status", base(ctx, m.nextTs()), { state: "waiting" }));
1748
+ {
1749
+ const t = await m.emitTurn();
1750
+ if (t) yield t;
1751
+ }
1752
+ return;
1753
+ }
1754
+ if (outcome.kind === "failed") {
1755
+ await this.opts.storage.runs.setStatus(ctx.runId, "failed");
1756
+ {
1757
+ const t = await m.emitTurn();
1758
+ if (t) yield t;
1759
+ }
1760
+ yield await m.emit(ev("swarm.run_failed", base(ctx, m.nextTs()), { stage: outcome.stage, message: outcome.message, retryable: true }));
1761
+ return;
1762
+ }
1763
+ await this.mirrorDelegations(ctx);
1764
+ await this.attributeRun(ctx);
1765
+ await this.opts.storage.runs.setStatus(ctx.runId, "done");
1766
+ {
1767
+ const t = await m.emitTurn();
1768
+ if (t) yield t;
1769
+ }
1770
+ yield await m.emit(ev("swarm.status", base(ctx, m.nextTs()), { state: "done" }));
1771
+ }
1772
+ /** A workflow `agent` step: stream `slug` with `message` (a per-step requestContext so it inherits the agent's
1773
+ * persona/tools/gate/model), reserving + metering through the caller's machinery, returning the final text. */
1774
+ async *streamWorkflowAgentStep(slug, message, genIndex, ctx, input, m) {
1775
+ await this.guardGeneration({ runId: ctx.runId, tenantId: ctx.tenantId, agentSlug: slug, modelId: m.modelIdFor(slug), generationIndex: genIndex, kind: "run" });
1776
+ const sctx = { ...ctx, agentSlug: slug };
1777
+ const stepRc = this.requestContext(sctx);
1778
+ if (this.opts.pageContext) attachPageContext(stepRc, input.context);
1779
+ const result = await this.agent().stream(message, { runId: ctx.runId, requestContext: stepRc, ...this.memoryOpts(sctx) });
1780
+ let text = "";
1781
+ for await (const part of result.fullStream) {
1782
+ if (part?.type === "step-finish") m.gov.step();
1783
+ for (const e of mapChunk(part, sctx, m.gov, m.modelIdFor, m.nextTs, m.streamed, m.delegateBudgets, m.activity, m.gatesApproval, m.turnUsage)) {
1784
+ if (e.type === "swarm.message") text += e.data.delta ?? e.data.text ?? "";
1785
+ yield e;
1786
+ }
1787
+ }
1788
+ return { text };
1789
+ }
1790
+ /** A workflow `tool` step: run the gate-free tool body through `executeToolWithGate` (the engine-owned gate). */
1791
+ async runWorkflowToolStep(toolName, args, ctx) {
1792
+ const skill = this.opts.resolveSkill?.(toolName);
1793
+ const exec = skill ? getToolExecutor(skill) : void 0;
1794
+ if (!skill || !exec) return { ok: false, error: `unknown tool "${toolName}"` };
1795
+ const toolCtx = { tenantId: ctx.tenantId, userId: ctx.userId, runId: ctx.runId, secrets: bindSecrets(this.opts.secrets, ctx) };
1796
+ return executeToolWithGate({
1797
+ ev: toolPreCallEvent({ runId: ctx.runId, tenantId: ctx.tenantId, agentSlug: ctx.agentSlug, toolName, origin: skill.origin ?? "first-party", needsApproval: this.gatesApproval(toolName), args }),
1798
+ gate: this.toolGate,
1799
+ run: () => exec(args, toolCtx)
1800
+ });
1801
+ }
1033
1802
  async *resume(args, ctx) {
1034
1803
  const snap = await this.opts.storage.runs.loadSnapshot(ctx.tenantId, args.runId);
1035
1804
  if (!snap) throw new Error(`no suspended run: ${args.runId}`);
1805
+ const generationIndex = typeof snap.genIndex === "number" ? snap.genIndex : 1;
1806
+ const capHit = snap.capHit;
1036
1807
  await this.opts.storage.markFollowupAnswered?.(args.followupId, ctx.tenantId);
1037
1808
  await this.opts.storage.runs.setStatus(args.runId, "running");
1038
- const modelId = (await this.loadRow(ctx.tenantId, ctx.agentSlug)).modelId ?? "unknown";
1039
- const modelIdFor = (slug) => this.rowCache.peek(`${ctx.tenantId}:${slug}`)?.modelId ?? modelId;
1809
+ const modelId = this.priceModelId((await this.loadRow(ctx.tenantId, ctx.agentSlug)).modelId ?? "unknown", ctx.tenantId, ctx.agentSlug);
1810
+ const modelIdFor = (slug) => {
1811
+ const raw = this.rowCache.peek(`${ctx.tenantId}:${slug}`)?.modelId;
1812
+ return raw ? this.priceModelId(raw, ctx.tenantId, slug) : modelId;
1813
+ };
1814
+ const gatesApproval = (name) => this.gatesApproval(name);
1040
1815
  const gov = new CostGovernor(this.opts.cost);
1041
- const delegateBudgets = this.opts.cost.perDelegate ? new DelegateBudgets(this.opts.cost.perDelegate, ctx.agentSlug) : null;
1816
+ const delegateBudgets = this.opts.cost.perDelegate ? new DelegateBudgets(this.opts.cost.perDelegate, ctx.agentSlug, this.pricingOpts()) : null;
1042
1817
  const streamed = /* @__PURE__ */ new Set();
1818
+ const activity = /* @__PURE__ */ new Map();
1819
+ const turnUsage = [];
1043
1820
  const collector = this.opts.telemetry ? new SpanCollector(args.runId, () => Date.now(), "resume", { agentSlug: ctx.agentSlug }) : null;
1044
1821
  let ts = 1e3;
1045
1822
  const emit = async (e) => {
1046
1823
  e.seq = await this.opts.storage.events.append(e);
1824
+ await this.notifyEvent(e, ctx);
1047
1825
  return e;
1048
1826
  };
1827
+ let turnEmitted = false;
1049
1828
  const floorKey = ctx.threadId;
1050
1829
  const me = { label: titleCase(ctx.agentSlug), runId: args.runId };
1051
1830
  const floorAbort = new AbortController();
1052
1831
  let releaseFloor = await this.floor.tryAcquire(floorKey, me);
1053
1832
  const rctx = { ...ctx, runId: args.runId };
1833
+ const emitTurn = async () => {
1834
+ if (turnEmitted) return null;
1835
+ turnEmitted = true;
1836
+ return emit(turnUsageEvent(rctx, ts++, turnUsage, generationIndex));
1837
+ };
1054
1838
  try {
1055
1839
  if (!releaseFloor) {
1056
1840
  const held = await this.floor.holder(floorKey);
@@ -1066,71 +1850,200 @@ var SwarmEngine = class {
1066
1850
  answer: args.answer
1067
1851
  })
1068
1852
  );
1069
- const rc = this.requestContext({ ...ctx, runId: args.runId });
1070
- if (this.opts.pageContext) attachPageContext(rc, args.context);
1071
- const result = await this.agent().resumeStream(
1072
- { answer: args.answer },
1073
- { runId: args.runId, toolCallId: args.toolCallId, requestContext: rc, ...this.memoryOpts({ ...ctx, runId: args.runId }) }
1074
- );
1075
- for await (const part of result.fullStream) {
1076
- if (part?.type === "step-finish") gov.step();
1077
- if (part?.type === "tool-call-suspended") {
1078
- const payload = part.payload ?? {};
1079
- const toolCallId = payload.toolCallId ?? "";
1080
- const followupId = `${args.runId}:${toolCallId}`;
1081
- const sp = payload.suspendPayload ?? {};
1082
- await recordSuspend(this.opts.storage, rctx, followupId, toolCallId);
1083
- await this.opts.storage.runs.setStatus(args.runId, "suspended");
1084
- await this.opts.storage.runs.saveSnapshot(args.runId, { pending: { toolCallId } });
1085
- yield await emit(ev("swarm.status", base(rctx, ts++), { state: "waiting" }));
1086
- yield await emit(
1087
- ev("swarm.question", base(rctx, ts++), {
1088
- followupId,
1089
- toolCallId,
1090
- to: sp.to ?? "user",
1091
- from: sp.asker || rctx.agentSlug,
1092
- prompt: sp.prompt ?? "",
1093
- field: sp.field
1094
- })
1095
- );
1096
- return;
1097
- }
1098
- if (part?.type === "error") {
1853
+ const wfState = snap.workflow;
1854
+ if (wfState) {
1855
+ const wf = this.opts.workflows?.find((w) => w.name === wfState.workflow) ?? this.opts.agentWorkflows?.[ctx.agentSlug];
1856
+ if (!wf) {
1099
1857
  await this.opts.storage.runs.setStatus(args.runId, "failed");
1100
- yield await emit(
1101
- ev("swarm.run_failed", base(rctx, ts++), {
1102
- stage: "stream",
1103
- message: streamErrorMessage(part),
1104
- retryable: false
1105
- })
1106
- );
1858
+ {
1859
+ const t = await emitTurn();
1860
+ if (t) yield t;
1861
+ }
1862
+ yield await emit(ev("swarm.run_failed", base(rctx, ts++), { stage: "workflow", message: `unknown workflow: ${wfState.workflow}`, retryable: false }));
1107
1863
  return;
1108
1864
  }
1109
- collectSpans(collector, part, modelId, gov);
1110
- for (const e of mapChunk(part, rctx, gov, modelIdFor, () => ts++, streamed, delegateBudgets)) yield await emit(e);
1111
- const overDelegate = delegateBudgets?.exceeded();
1112
- if (gov.shouldStop().stop || overDelegate) {
1113
- await this.opts.storage.runs.setStatus(args.runId, "failed");
1114
- yield await emit(
1115
- ev("swarm.run_failed", base(rctx, ts++), {
1116
- stage: "cost",
1117
- message: overDelegate?.reason ?? gov.shouldStop().reason,
1118
- retryable: false
1119
- })
1120
- );
1121
- return;
1865
+ if (wfState.pending) {
1866
+ wfState.outputs[wfState.pending.stepId] = args.answer;
1867
+ wfState.pending = void 0;
1868
+ }
1869
+ yield* this.driveWorkflow(wf, wfState, rctx, { message: "", context: args.context }, {
1870
+ gov,
1871
+ modelIdFor,
1872
+ streamed,
1873
+ delegateBudgets,
1874
+ activity,
1875
+ gatesApproval,
1876
+ turnUsage,
1877
+ nextTs: () => ts++,
1878
+ emit,
1879
+ emitTurn
1880
+ });
1881
+ return;
1882
+ }
1883
+ if (capHit && !isApproved(args.answer)) {
1884
+ await this.opts.storage.runs.setStatus(args.runId, "failed");
1885
+ {
1886
+ const t = await emitTurn();
1887
+ if (t) yield t;
1888
+ }
1889
+ yield await emit(
1890
+ ev("swarm.run_failed", base(rctx, ts++), {
1891
+ stage: "cost",
1892
+ message: "budget cap reached \u2014 continuation declined by the user",
1893
+ retryable: false
1894
+ })
1895
+ );
1896
+ return;
1897
+ }
1898
+ const rc = this.requestContext({ ...ctx, runId: args.runId });
1899
+ if (this.opts.pageContext) attachPageContext(rc, args.context);
1900
+ await this.guardGeneration({ runId: args.runId, tenantId: ctx.tenantId, agentSlug: ctx.agentSlug, modelId, generationIndex, kind: "resume" });
1901
+ let resumeNudges = 0;
1902
+ let firstPass = true;
1903
+ const request = await this.recallRequest(rctx);
1904
+ let nudgeMessage = CONTINUE_NUDGE;
1905
+ let transcript = "";
1906
+ let incompleteVerdict = null;
1907
+ for (; ; ) {
1908
+ const result = firstPass ? capHit ? await (async () => {
1909
+ gov.raiseCostCap(this.opts.cost.capIncrementUsd ?? this.opts.cost.maxCostUsd);
1910
+ return this.agent().stream(capHit.message, { runId: args.runId, requestContext: rc, ...this.memoryOpts({ ...ctx, runId: args.runId }) });
1911
+ })() : await this.agent().resumeStream(
1912
+ { answer: args.answer },
1913
+ { runId: args.runId, toolCallId: args.toolCallId, requestContext: rc, ...this.memoryOpts({ ...ctx, runId: args.runId }) }
1914
+ ) : await this.agent().stream(nudgeMessage, { runId: args.runId, requestContext: rc, ...this.memoryOpts({ ...ctx, runId: args.runId }) });
1915
+ firstPass = false;
1916
+ let sawStep = false;
1917
+ let lastOutputSlug;
1918
+ for await (const part of result.fullStream) {
1919
+ if (part?.type === "step-finish") {
1920
+ gov.step();
1921
+ sawStep = true;
1922
+ }
1923
+ if (part?.type === "tool-call-suspended") {
1924
+ const payload = part.payload ?? {};
1925
+ const toolCallId = payload.toolCallId ?? "";
1926
+ const followupId = `${args.runId}:${toolCallId}`;
1927
+ const sp = payload.suspendPayload ?? {};
1928
+ await recordSuspend(this.opts.storage, rctx, followupId, toolCallId);
1929
+ await this.opts.storage.runs.setStatus(args.runId, "suspended");
1930
+ await this.opts.storage.runs.saveSnapshot(args.runId, { pending: { toolCallId }, genIndex: generationIndex + 1 });
1931
+ {
1932
+ const t = await emitTurn();
1933
+ if (t) yield t;
1934
+ }
1935
+ yield await emit(ev("swarm.status", base(rctx, ts++), { state: "waiting" }));
1936
+ yield await emit(
1937
+ ev("swarm.question", base(rctx, ts++), {
1938
+ followupId,
1939
+ toolCallId,
1940
+ to: sp.to ?? "user",
1941
+ from: sp.asker || rctx.agentSlug,
1942
+ prompt: sp.prompt ?? "",
1943
+ field: sp.field
1944
+ })
1945
+ );
1946
+ return;
1947
+ }
1948
+ if (part?.type === "error") {
1949
+ await this.opts.storage.runs.setStatus(args.runId, "failed");
1950
+ {
1951
+ const t = await emitTurn();
1952
+ if (t) yield t;
1953
+ }
1954
+ yield await emit(
1955
+ ev("swarm.run_failed", base(rctx, ts++), {
1956
+ stage: "stream",
1957
+ message: streamErrorMessage(part),
1958
+ retryable: false
1959
+ })
1960
+ );
1961
+ return;
1962
+ }
1963
+ collectSpans(collector, part, modelId, gov);
1964
+ for (const e of mapChunk(part, rctx, gov, modelIdFor, () => ts++, streamed, delegateBudgets, activity, gatesApproval, turnUsage)) {
1965
+ if (e.type === "swarm.message" || e.type === "swarm.tool_call") {
1966
+ lastOutputSlug = e.agentSlug;
1967
+ if (this.opts.verifyCompletion) transcript = appendTranscript(transcript, e);
1968
+ }
1969
+ yield await emit(e);
1970
+ }
1971
+ const overDelegate = delegateBudgets?.exceeded();
1972
+ const stop = gov.shouldStop();
1973
+ if (stop.stop || overDelegate) {
1974
+ if (this.opts.cost.onCapHit === "ask" && stop.stop && !overDelegate && capHit) {
1975
+ const followupId = `${args.runId}:${CAP_FOLLOWUP_SUFFIX}`;
1976
+ await recordSuspend(this.opts.storage, rctx, followupId, CAP_FOLLOWUP_SUFFIX);
1977
+ await this.opts.storage.runs.setStatus(args.runId, "suspended");
1978
+ await this.opts.storage.runs.saveSnapshot(args.runId, {
1979
+ capHit: { message: capHit.message, spentUsd: gov.costUsd() },
1980
+ genIndex: generationIndex + 1
1981
+ });
1982
+ {
1983
+ const t = await emitTurn();
1984
+ if (t) yield t;
1985
+ }
1986
+ yield await emit(ev("swarm.status", base(rctx, ts++), { state: "waiting" }));
1987
+ yield await emit(ev("swarm.question", base(rctx, ts++), capQuestion(rctx, followupId, gov)));
1988
+ return;
1989
+ }
1990
+ await this.opts.storage.runs.setStatus(args.runId, "failed");
1991
+ {
1992
+ const t = await emitTurn();
1993
+ if (t) yield t;
1994
+ }
1995
+ yield await emit(
1996
+ ev("swarm.run_failed", base(rctx, ts++), {
1997
+ stage: "cost",
1998
+ message: overDelegate?.reason ?? stop.reason,
1999
+ retryable: false
2000
+ })
2001
+ );
2002
+ return;
2003
+ }
2004
+ }
2005
+ if (this.opts.verifyCompletion) {
2006
+ const verdict = await this.safeVerify(request, transcript, rctx);
2007
+ if (!verdict.complete && resumeNudges < MAX_CONTINUE_NUDGES) {
2008
+ resumeNudges++;
2009
+ nudgeMessage = verifyNudge(verdict.missing);
2010
+ continue;
2011
+ }
2012
+ incompleteVerdict = verdict.complete ? null : verdict;
2013
+ } else if (sawStep && lastOutputSlug !== rctx.agentSlug && resumeNudges < MAX_CONTINUE_NUDGES) {
2014
+ resumeNudges++;
2015
+ continue;
1122
2016
  }
2017
+ break;
1123
2018
  }
1124
2019
  await this.attributeRun(rctx);
1125
- await this.opts.storage.runs.setStatus(args.runId, "done");
1126
- yield await emit(ev("swarm.status", base(rctx, ts++), { state: "done" }));
2020
+ if (incompleteVerdict) {
2021
+ await this.opts.storage.runs.setStatus(args.runId, "failed");
2022
+ {
2023
+ const t = await emitTurn();
2024
+ if (t) yield t;
2025
+ }
2026
+ yield await emit(ev("swarm.run_failed", base(rctx, ts++), { stage: "incomplete", message: incompleteVerdict.missing ?? "The task was not completed.", retryable: true }));
2027
+ } else {
2028
+ await this.opts.storage.runs.setStatus(args.runId, "done");
2029
+ {
2030
+ const t = await emitTurn();
2031
+ if (t) yield t;
2032
+ }
2033
+ yield await emit(ev("swarm.status", base(rctx, ts++), { state: "done" }));
2034
+ }
1127
2035
  } catch (err) {
1128
- console.error(`[nightowls] resume ${args.runId} threw:`, err);
2036
+ const stage = err instanceof ReserveDenied ? "reserve" : "exception";
2037
+ if (stage !== "reserve") console.error(`[@nightowlsdev/core] resume ${args.runId} threw:`, err);
1129
2038
  try {
1130
2039
  await this.opts.storage.runs.setStatus(args.runId, "failed");
1131
2040
  } catch {
1132
2041
  }
1133
- yield await emit(ev("swarm.run_failed", base(rctx, ts++), { stage: "exception", message: errMessage(err), retryable: false }));
2042
+ {
2043
+ const t = await emitTurn();
2044
+ if (t) yield t;
2045
+ }
2046
+ yield await emit(ev("swarm.run_failed", base(rctx, ts++), { stage, message: errMessage(err), retryable: false }));
1134
2047
  } finally {
1135
2048
  floorAbort.abort();
1136
2049
  await releaseFloor?.();
@@ -1138,12 +2051,57 @@ var SwarmEngine = class {
1138
2051
  }
1139
2052
  }
1140
2053
  };
2054
+ var ReserveDenied = class extends Error {
2055
+ stage = "reserve";
2056
+ constructor(reason) {
2057
+ super(reason);
2058
+ this.name = "ReserveDenied";
2059
+ }
2060
+ };
1141
2061
  function errMessage(err) {
1142
2062
  return err instanceof Error ? err.message : String(err);
1143
2063
  }
1144
2064
  function base(ctx, ts) {
1145
2065
  return { runId: ctx.runId, agentSlug: ctx.agentSlug, ts };
1146
2066
  }
2067
+ var CAP_FOLLOWUP_SUFFIX = "cap";
2068
+ function capQuestion(ctx, followupId, gov) {
2069
+ const spent = gov.costUsd();
2070
+ const cap = gov.maxCostUsd;
2071
+ return {
2072
+ followupId,
2073
+ toolCallId: CAP_FOLLOWUP_SUFFIX,
2074
+ to: "user",
2075
+ from: ctx.agentSlug,
2076
+ prompt: `Budget cap reached \u2014 continue? This run has spent **$${spent.toFixed(2)}** of its $${cap.toFixed(2)} budget. Approve to grant more budget and keep going, or decline to stop the run here.`,
2077
+ field: {
2078
+ kind: "confirm",
2079
+ confirmLabel: "Continue",
2080
+ rejectLabel: "Stop"
2081
+ }
2082
+ };
2083
+ }
2084
+ function turnUsageEvent(ctx, ts, turnUsage, segmentIndex) {
2085
+ const total = sumTurnUsage(turnUsage);
2086
+ return ev("swarm.turn_usage", base(ctx, ts), {
2087
+ breakdown: total.breakdown,
2088
+ cost: total.cost,
2089
+ bySlug: total.bySlug,
2090
+ generations: turnUsage.length,
2091
+ segmentIndex
2092
+ });
2093
+ }
2094
+ function extractUsage(usage) {
2095
+ const u = usage ?? {};
2096
+ const cacheRead = u.cachedInputTokens ?? u.inputTokenDetails?.cacheReadTokens ?? u.raw?.inputTokenDetails?.cacheReadTokens;
2097
+ const cacheWrite = u.inputTokenDetails?.cacheWriteTokens ?? u.raw?.inputTokenDetails?.cacheWriteTokens;
2098
+ const reasoning = u.reasoningTokens ?? u.outputTokenDetails?.reasoningTokens ?? u.raw?.outputTokenDetails?.reasoningTokens;
2099
+ const b = { inputTokens: u.inputTokens ?? 0, outputTokens: u.outputTokens ?? 0 };
2100
+ if (cacheRead != null) b.cacheReadTokens = cacheRead;
2101
+ if (cacheWrite != null) b.cacheWriteTokens = cacheWrite;
2102
+ if (reasoning != null) b.reasoningTokens = reasoning;
2103
+ return b;
2104
+ }
1147
2105
  function titleCase(slug) {
1148
2106
  return slug.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
1149
2107
  }
@@ -1175,8 +2133,7 @@ function collectSpans(collector, part, modelId, gov) {
1175
2133
  break;
1176
2134
  case "step-finish": {
1177
2135
  const output = p.output;
1178
- const usage = output?.usage;
1179
- const u = { inputTokens: usage?.inputTokens ?? 0, outputTokens: usage?.outputTokens ?? 0 };
2136
+ const u = extractUsage(output?.usage);
1180
2137
  collector.openGeneration(modelId);
1181
2138
  collector.closeGeneration(u, gov.priceOf(modelId, u));
1182
2139
  break;
@@ -1197,14 +2154,19 @@ async function recordSuspend(storage, ctx, followupId, toolCallId) {
1197
2154
  if (!storage.recordSuspend && !warnedNoRecordSuspend) {
1198
2155
  warnedNoRecordSuspend = true;
1199
2156
  console.warn(
1200
- "[nightowls] storage adapter does not implement recordSuspend() \u2014 human-in-the-loop resume will be forbidden (the followup index is never written). Implement recordSuspend on your StorageAdapter."
2157
+ "[@nightowlsdev/core] storage adapter does not implement recordSuspend() \u2014 human-in-the-loop resume will be forbidden (the followup index is never written). Implement recordSuspend on your StorageAdapter."
1201
2158
  );
1202
2159
  }
1203
2160
  await storage.recordSuspend?.(ctx.runId, ctx.tenantId, followupId, toolCallId);
1204
2161
  }
1205
- function mapChunk(part, ctx, gov, modelIdFor, nextTs, streamed, delegateBudgets) {
2162
+ function mapChunk(part, ctx, gov, modelIdFor, nextTs, streamed, delegateBudgets, activity, gatesApproval, turnUsage) {
1206
2163
  const p = part.payload ?? {};
1207
2164
  const modelId = modelIdFor(ctx.agentSlug);
2165
+ const act = (slug) => {
2166
+ let a = activity.get(slug);
2167
+ if (!a) activity.set(slug, a = { toolCalls: 0, agentActivations: 0 });
2168
+ return a;
2169
+ };
1208
2170
  switch (part.type) {
1209
2171
  case "text-delta":
1210
2172
  return [ev("swarm.message", base(ctx, nextTs()), { role: "assistant", delta: p.text ?? "" })];
@@ -1214,17 +2176,21 @@ function mapChunk(part, ctx, gov, modelIdFor, nextTs, streamed, delegateBudgets)
1214
2176
  const to = name.slice("agent-".length);
1215
2177
  const a = typeof p.args === "string" ? safeParse(p.args) : p.args;
1216
2178
  const task = a?.prompt ?? "";
2179
+ act(ctx.agentSlug).agentActivations++;
1217
2180
  return [
1218
2181
  ev("swarm.handoff", base(ctx, nextTs()), { from: ctx.agentSlug, to, task }),
1219
2182
  ev("swarm.status", base(ctx, nextTs()), { state: "delegating", note: to })
1220
2183
  ];
1221
2184
  }
2185
+ act(ctx.agentSlug).toolCalls++;
1222
2186
  return [
1223
2187
  ev("swarm.tool_call", base(ctx, nextTs()), {
1224
2188
  toolCallId: p.toolCallId,
1225
2189
  name,
1226
2190
  args: p.args,
1227
- needsApproval: false
2191
+ // SP5 truth-fix: emit the RESOLVED needsApproval (policy + the tool's flag), not a hardcoded false, so
2192
+ // the UI reflects reality — a tool that will suspend-for-approval is shown as needing approval.
2193
+ needsApproval: gatesApproval(name)
1228
2194
  })
1229
2195
  ];
1230
2196
  }
@@ -1253,9 +2219,18 @@ function mapChunk(part, ctx, gov, modelIdFor, nextTs, streamed, delegateBudgets)
1253
2219
  const output = p.output;
1254
2220
  const usage = output?.usage;
1255
2221
  if (usage) {
1256
- const u = { inputTokens: usage.inputTokens ?? 0, outputTokens: usage.outputTokens ?? 0 };
2222
+ const counters = activity.get(ctx.agentSlug);
2223
+ const u = extractUsage(usage);
2224
+ if (counters && (counters.toolCalls || counters.agentActivations)) {
2225
+ u.toolCalls = counters.toolCalls;
2226
+ u.agentActivations = counters.agentActivations;
2227
+ }
2228
+ activity.delete(ctx.agentSlug);
1257
2229
  gov.addUsage(modelId, u);
1258
2230
  delegateBudgets?.addUsage(ctx.agentSlug, modelId, u);
2231
+ const cost = gov.costOf(modelId, u);
2232
+ turnUsage.push({ slug: ctx.agentSlug, breakdown: u, cost });
2233
+ return [ev("swarm.usage", base(ctx, nextTs()), { slug: ctx.agentSlug, modelId, breakdown: u, cost })];
1259
2234
  }
1260
2235
  return [];
1261
2236
  }
@@ -1266,7 +2241,7 @@ function mapChunk(part, ctx, gov, modelIdFor, nextTs, streamed, delegateBudgets)
1266
2241
  const inner = p.output;
1267
2242
  if (!inner || typeof inner.type !== "string") return [];
1268
2243
  if (inner.type === "text-delta") streamed.add(p.toolCallId);
1269
- return mapChunk(inner, { ...ctx, agentSlug: subSlug }, gov, modelIdFor, nextTs, streamed, delegateBudgets);
2244
+ return mapChunk(inner, { ...ctx, agentSlug: subSlug }, gov, modelIdFor, nextTs, streamed, delegateBudgets, activity, gatesApproval, turnUsage);
1270
2245
  }
1271
2246
  case "tool-error": {
1272
2247
  const name = p.toolName ?? "";
@@ -1295,8 +2270,121 @@ function allowListModelProvider(opts) {
1295
2270
  };
1296
2271
  }
1297
2272
 
2273
+ // src/rules.ts
2274
+ var import_hooks2 = require("@nightowlsdev/hooks");
2275
+ var GLOB_CACHE = /* @__PURE__ */ new Map();
2276
+ function globRegex(pattern) {
2277
+ let re = GLOB_CACHE.get(pattern);
2278
+ if (!re) {
2279
+ re = new RegExp("^" + pattern.replace(/[.*+?^${}()|[\]\\]/g, (c) => c === "*" ? ".*" : "\\" + c) + "$");
2280
+ GLOB_CACHE.set(pattern, re);
2281
+ }
2282
+ return re;
2283
+ }
2284
+ function globMatch(pattern, value) {
2285
+ if (pattern === value) return true;
2286
+ if (!pattern.includes("*")) return false;
2287
+ return globRegex(pattern).test(value);
2288
+ }
2289
+ function matchField(field, value) {
2290
+ if (field === void 0) return true;
2291
+ const arr = Array.isArray(field) ? field : [field];
2292
+ return arr.some((p) => globMatch(p, value));
2293
+ }
2294
+ function ruleMatchesTool(rule, ev2) {
2295
+ if (rule.seam !== "tool") return false;
2296
+ if (rule.scopeAgent !== void 0 && rule.scopeAgent !== ev2.agentSlug) return false;
2297
+ const w = rule.when;
2298
+ if (!matchField(w.agent, ev2.agentSlug)) return false;
2299
+ if (!matchField(w.tool, ev2.toolName)) return false;
2300
+ if (w.origin !== void 0 && w.origin !== ev2.origin) return false;
2301
+ return true;
2302
+ }
2303
+ function ruleMatchesGeneration(rule, ev2) {
2304
+ if (rule.seam !== "generation") return false;
2305
+ if (rule.scopeAgent !== void 0 && rule.scopeAgent !== ev2.agentSlug) return false;
2306
+ const w = rule.when;
2307
+ if (!matchField(w.agent, ev2.agentSlug)) return false;
2308
+ if (!matchField(w.model, ev2.modelId)) return false;
2309
+ return true;
2310
+ }
2311
+ var TOOL_RANK = { deny: 2, ask: 1, allow: 0 };
2312
+ function mostRestrictiveTool(a, b) {
2313
+ return TOOL_RANK[b.action] > TOOL_RANK[a.action] ? b : a;
2314
+ }
2315
+ function errMessage2(err) {
2316
+ return err instanceof Error ? err.message : String(err);
2317
+ }
2318
+ function composeToolHooks(opts) {
2319
+ const rules = opts.rules.filter((r) => r.seam === "tool" && r.level === "enforce");
2320
+ return async (ev2) => {
2321
+ let decision = (0, import_hooks2.toolPolicyDecision)(ev2, opts.policy);
2322
+ if (opts.host) {
2323
+ try {
2324
+ decision = mostRestrictiveTool(decision, await opts.host(ev2));
2325
+ } catch (err) {
2326
+ return (0, import_hooks2.deny)(`preToolCall hook threw: ${errMessage2(err)}`);
2327
+ }
2328
+ }
2329
+ if (decision.action === "deny") return decision;
2330
+ for (const r of rules) {
2331
+ if (!ruleMatchesTool(r, ev2)) continue;
2332
+ const rd = r.action.do === "deny" ? (0, import_hooks2.deny)(r.action.reason ?? r.statement) : (0, import_hooks2.ask)(r.action.reason ?? r.statement);
2333
+ decision = mostRestrictiveTool(decision, rd);
2334
+ if (decision.action === "deny") return decision;
2335
+ }
2336
+ return decision;
2337
+ };
2338
+ }
2339
+ function composeGenerationHooks(opts) {
2340
+ const rules = opts.rules.filter((r) => r.seam === "generation" && r.level === "enforce");
2341
+ if (!rules.length && !opts.host) return void 0;
2342
+ return async (ev2) => {
2343
+ if (opts.host) {
2344
+ try {
2345
+ const d = await opts.host(ev2);
2346
+ if (d.action === "deny") return d;
2347
+ } catch (err) {
2348
+ return (0, import_hooks2.deny)(`preGeneration hook threw: ${errMessage2(err)}`);
2349
+ }
2350
+ }
2351
+ for (const r of rules) {
2352
+ if (ruleMatchesGeneration(r, ev2)) return (0, import_hooks2.deny)(r.action?.reason ?? r.statement);
2353
+ }
2354
+ return { action: "allow" };
2355
+ };
2356
+ }
2357
+ function softPolicyFor(slug, rules, workflows) {
2358
+ const out = [];
2359
+ for (const r of rules) {
2360
+ if (r.level !== "advise") continue;
2361
+ if (r.scopeAgent !== void 0 && r.scopeAgent !== slug) continue;
2362
+ if (!matchField(r.when.agent, slug)) continue;
2363
+ out.push(r.statement);
2364
+ }
2365
+ for (const w of workflows) {
2366
+ if (w.compliance !== "advisory" || !w.description) continue;
2367
+ if (w.scopeAgent !== void 0 && w.scopeAgent !== slug) continue;
2368
+ out.push(`Suggested procedure "${w.name}": ${w.description}`);
2369
+ }
2370
+ return out;
2371
+ }
2372
+
1298
2373
  // src/define.ts
1299
2374
  var MASTRA = /* @__PURE__ */ new WeakMap();
2375
+ var APPROVAL_SUSPEND_SCHEMA = import_zod4.z.object({
2376
+ to: import_zod4.z.string(),
2377
+ prompt: import_zod4.z.string(),
2378
+ field: import_zod4.z.object({
2379
+ kind: import_zod4.z.enum(["confirm", "buttons", "select", "multiselect", "number", "text", "textarea"]),
2380
+ confirmLabel: import_zod4.z.string().optional(),
2381
+ rejectLabel: import_zod4.z.string().optional()
2382
+ }).optional(),
2383
+ asker: import_zod4.z.string().optional(),
2384
+ kind: import_zod4.z.literal("approval").optional(),
2385
+ toolName: import_zod4.z.string().optional()
2386
+ });
2387
+ var APPROVAL_RESUME_SCHEMA = import_zod4.z.object({ answer: import_zod4.z.any() });
1300
2388
  function defineTool(spec) {
1301
2389
  const origin = spec.origin ?? "first-party";
1302
2390
  const needsApproval = spec.needsApproval ?? origin === "mcp";
@@ -1305,26 +2393,89 @@ function defineTool(spec) {
1305
2393
  description: spec.description ?? spec.name,
1306
2394
  inputSchema: spec.inputSchema,
1307
2395
  outputSchema: spec.outputSchema,
2396
+ // SP5: declare suspend/resume schemas so the action-approval gate can suspend-and-ask via
2397
+ // context.agent.suspend (Mastra gates `suspend` on a declared suspendSchema — same as the built-in `ask`).
2398
+ // The suspend payload is `ask`-shaped (+ approval metadata) so the engine emits the existing `swarm.question`;
2399
+ // resume carries the human's `{ answer }` (a confirm → boolean approve/reject).
2400
+ suspendSchema: APPROVAL_SUSPEND_SCHEMA,
2401
+ resumeSchema: APPROVAL_RESUME_SCHEMA,
1308
2402
  // Mastra 1.38 (per SPIKE-FINDINGS item 3): execute is `(inputData, context) => out`
1309
2403
  // with TWO positional args. `inputData` is the parsed input; tenant/user/run come
1310
- // off `context.requestContext`.
2404
+ // off `context.requestContext`. `context.agent` (when an agent drives the call) carries the
2405
+ // suspend/resumeData handles SP5's action-approval gate uses to suspend-and-ask the human.
2406
+ //
2407
+ // SP5 — the mandatory action-approval HITL gate (the enforcement POINT). Before the side effect runs we
2408
+ // resolve the effective ToolDecision via the per-run `ToolGate` the engine injected on the RequestContext
2409
+ // (policy + the resolved needsApproval + the preToolCall hook):
2410
+ // • allow → execute as today.
2411
+ // • deny → return a blocked tool-result; the side effect NEVER runs.
2412
+ // • ask → SUSPEND via context.agent.suspend(...) with an `ask`-SHAPED payload, so the engine's existing
2413
+ // `tool-call-suspended` handler emits the SAME `swarm.question`; on resume the tool re-executes
2414
+ // with context.agent.resumeData = { answer } → approve runs the side effect, reject blocks it.
2415
+ // We reuse the EXISTING suspend/resume + question/answer machinery — no parallel one.
1311
2416
  execute: async (inputData, context) => {
1312
2417
  const rc = context?.requestContext;
2418
+ const tenantId = rc?.get?.("tenantId") ?? "default";
2419
+ const userId = rc?.get?.("userId") ?? "";
2420
+ const runId = rc?.get?.("runId") ?? "";
2421
+ const resolver = rc?.get?.(SECRET_RESOLVER_KEY);
2422
+ const scopedCtx = {
2423
+ tenantId,
2424
+ userId,
2425
+ runId,
2426
+ agentSlug: rc?.get?.("agentSlug") ?? "",
2427
+ threadId: rc?.get?.("threadId") ?? "",
2428
+ ...(() => {
2429
+ const v = rc?.get?.("agentVersion");
2430
+ return typeof v === "number" ? { agentVersion: v } : {};
2431
+ })()
2432
+ };
1313
2433
  const ctx = {
1314
- tenantId: rc?.get?.("tenantId") ?? "default",
1315
- userId: rc?.get?.("userId") ?? "",
1316
- runId: rc?.get?.("runId") ?? ""
2434
+ tenantId,
2435
+ userId,
2436
+ runId,
2437
+ secrets: bindSecrets(resolver, scopedCtx)
1317
2438
  };
1318
- return spec.execute(inputData, ctx);
2439
+ const run = () => spec.execute(inputData, ctx);
2440
+ const agentCtx = context?.agent;
2441
+ if (agentCtx?.resumeData !== void 0 && agentCtx.resumeData !== null) {
2442
+ const answer = agentCtx.resumeData.answer;
2443
+ if (isApproved(answer)) return run();
2444
+ throw new ToolBlockedError(spec.name, "rejected by approver");
2445
+ }
2446
+ const gate = rc?.get?.(TOOL_GATE_KEY);
2447
+ if (!gate || typeof agentCtx?.suspend !== "function") return run();
2448
+ const agentSlug = deriveAsker(agentCtx, rc);
2449
+ const decision = await gate(
2450
+ toolPreCallEvent({
2451
+ runId: ctx.runId,
2452
+ tenantId: ctx.tenantId,
2453
+ agentSlug,
2454
+ toolName: spec.name,
2455
+ origin,
2456
+ needsApproval,
2457
+ args: inputData
2458
+ })
2459
+ );
2460
+ if (decision.action === "allow") return run();
2461
+ if (decision.action === "deny") throw new ToolBlockedError(spec.name, decision.reason);
2462
+ await agentCtx.suspend(approvalSuspendPayload({ toolName: spec.name, asker: agentSlug, reason: decision.reason }));
2463
+ throw new ToolBlockedError(spec.name, "awaiting approval");
1319
2464
  }
1320
2465
  });
1321
2466
  const handle = { name: spec.name, needsApproval, origin };
1322
2467
  MASTRA.set(handle, mastraTool);
2468
+ setToolExecutor(handle, (args, c) => spec.execute(args, c));
1323
2469
  return handle;
1324
2470
  }
1325
2471
  function defineSkill(tool) {
1326
2472
  return tool;
1327
2473
  }
2474
+ function deriveAsker(agentCtx, rc) {
2475
+ const agentId = agentCtx?.agentId ?? "";
2476
+ if (agentId.startsWith("swarm-sub-")) return agentId.slice("swarm-sub-".length);
2477
+ return rc?.get?.("agentSlug") ?? "";
2478
+ }
1328
2479
  function __getMastraTool(t) {
1329
2480
  return MASTRA.get(t);
1330
2481
  }
@@ -1335,6 +2486,10 @@ function defineAgent(spec) {
1335
2486
  // The concrete skill handles ride along on the def so defineSwarm can build
1336
2487
  // a per-swarm resolver. No module-level registry → no cross-swarm leakage.
1337
2488
  skills,
2489
+ // Per-agent policy rides on the def (engine-local), stamped with this agent's scope so defineSwarm can
2490
+ // collect + apply it without persisting to the versioned AgentVersion row (D3).
2491
+ ...spec.rules ? { rules: spec.rules.map((r) => ({ ...r, scopeAgent: spec.slug })) } : {},
2492
+ ...spec.workflow ? { workflow: { ...spec.workflow, scopeAgent: spec.slug } } : {},
1338
2493
  head: {
1339
2494
  slug: spec.slug,
1340
2495
  version: 1,
@@ -1348,11 +2503,217 @@ function defineAgent(spec) {
1348
2503
  }
1349
2504
  };
1350
2505
  }
2506
+ function defineRule(spec) {
2507
+ if (!spec.id) throw new Error("defineRule: `id` is required");
2508
+ if (!spec.statement) throw new Error(`defineRule(${spec.id}): \`statement\` is required`);
2509
+ const tools = spec.when.tool === void 0 ? [] : Array.isArray(spec.when.tool) ? spec.when.tool : [spec.when.tool];
2510
+ const seam = spec.on ?? (spec.when.model !== void 0 ? "generation" : "tool");
2511
+ if (spec.level === "enforce") {
2512
+ if (!spec.action) throw new Error(`defineRule(${spec.id}): enforce rules require an \`action\``);
2513
+ if (!spec.on && spec.when.tool === void 0 && spec.when.model === void 0) {
2514
+ throw new Error(`defineRule(${spec.id}): an enforce rule with an empty \`when\` must set \`on\` ("tool" | "generation")`);
2515
+ }
2516
+ if (spec.action.do === "ask") {
2517
+ if (seam !== "tool") throw new Error(`defineRule(${spec.id}): \`ask\` is tool-seam only (preGeneration cannot suspend)`);
2518
+ if (tools.some((t) => t.startsWith("agent-"))) {
2519
+ throw new Error(`defineRule(${spec.id}): \`ask\` cannot target a delegation (\`agent-*\`) \u2014 gateDelegation defers \`ask\`; use \`deny\``);
2520
+ }
2521
+ }
2522
+ }
2523
+ return { id: spec.id, statement: spec.statement, when: spec.when, level: spec.level, action: spec.action, seam };
2524
+ }
2525
+ function workflowRefTargets(step) {
2526
+ const out = [];
2527
+ const scan = (o) => {
2528
+ for (const v of Object.values(o ?? {})) {
2529
+ if (v && typeof v === "object" && "$ref" in v) out.push(String(v.$ref));
2530
+ }
2531
+ };
2532
+ scan(step.args);
2533
+ scan(step.input);
2534
+ if (Array.isArray(step.next)) {
2535
+ for (const t of step.next) if (t.when?.$ref) out.push(t.when.$ref);
2536
+ }
2537
+ return out;
2538
+ }
2539
+ function defineWorkflow(spec) {
2540
+ if (!spec.name) throw new Error("defineWorkflow: `name` is required");
2541
+ if (!spec.steps?.length) throw new Error(`defineWorkflow(${spec.name}): at least one step is required`);
2542
+ const ids = /* @__PURE__ */ new Set();
2543
+ for (const s of spec.steps) {
2544
+ if (ids.has(s.id)) throw new Error(`defineWorkflow(${spec.name}): duplicate step id "${s.id}"`);
2545
+ ids.add(s.id);
2546
+ const kinds = [s.agent !== void 0, s.tool !== void 0, s.human !== void 0].filter(Boolean).length;
2547
+ if (kinds !== 1) throw new Error(`defineWorkflow(${spec.name}): step "${s.id}" must have exactly one of agent/tool/human`);
2548
+ }
2549
+ const start = spec.start ?? spec.steps[0].id;
2550
+ if (!ids.has(start)) throw new Error(`defineWorkflow(${spec.name}): start "${start}" is not a known step`);
2551
+ const nextOf = (s) => {
2552
+ const outs = s.next === void 0 ? [] : typeof s.next === "string" ? [s.next] : s.next.map((t) => t.to);
2553
+ if (s.onError && typeof s.onError === "object" && "to" in s.onError) outs.push(s.onError.to);
2554
+ return outs;
2555
+ };
2556
+ for (const s of spec.steps) {
2557
+ for (const t of nextOf(s)) if (!ids.has(t)) throw new Error(`defineWorkflow(${spec.name}): step "${s.id}" \u2192 unknown step "${t}"`);
2558
+ for (const r of workflowRefTargets(s)) {
2559
+ if (r !== "input" && !(r.startsWith("steps.") && ids.has(r.slice("steps.".length)))) {
2560
+ throw new Error(`defineWorkflow(${spec.name}): step "${s.id}" has an invalid $ref "${r}"`);
2561
+ }
2562
+ }
2563
+ }
2564
+ const byId = new Map(spec.steps.map((s) => [s.id, s]));
2565
+ const seen = /* @__PURE__ */ new Set();
2566
+ const stack = /* @__PURE__ */ new Set();
2567
+ const visit = (id) => {
2568
+ if (stack.has(id)) throw new Error(`defineWorkflow(${spec.name}): cycle detected at step "${id}"`);
2569
+ if (seen.has(id)) return;
2570
+ seen.add(id);
2571
+ stack.add(id);
2572
+ for (const t of nextOf(byId.get(id))) visit(t);
2573
+ stack.delete(id);
2574
+ };
2575
+ visit(start);
2576
+ return { name: spec.name, compliance: spec.compliance, description: spec.description, steps: spec.steps, start };
2577
+ }
1351
2578
  function buildSkillResolver(agents) {
1352
2579
  const map = /* @__PURE__ */ new Map();
1353
2580
  for (const a of agents) for (const s of a.skills ?? []) map.set(s.name, s);
1354
2581
  return (name) => map.get(name);
1355
2582
  }
2583
+ function isConnectorLooking(name) {
2584
+ return name.includes(".");
2585
+ }
2586
+ function ruleToolRefs(rule) {
2587
+ if (rule.seam !== "tool") return [];
2588
+ const t = rule.when.tool;
2589
+ const names = t === void 0 ? [] : Array.isArray(t) ? t : [t];
2590
+ return names.filter((n) => !n.includes("*"));
2591
+ }
2592
+ var CRED_REF_KEYS = /* @__PURE__ */ new Set(["secretref", "credentialref", "connectionid", "owlconnections"]);
2593
+ var normKey = (k) => k.toLowerCase().replace(/[-_]/g, "");
2594
+ function assertNoCredRefs(obj, where) {
2595
+ const walk = (v) => {
2596
+ if (Array.isArray(v)) {
2597
+ v.forEach(walk);
2598
+ return;
2599
+ }
2600
+ if (v && typeof v === "object") {
2601
+ for (const [k, val] of Object.entries(v)) {
2602
+ if (CRED_REF_KEYS.has(normKey(k))) {
2603
+ throw new Error(`defineBundle: ${where} carries a credential-ref key "${k}" \u2014 a bundle may not embed credential/connection handles; declare the capability and resolve creds per-tenant at call time`);
2604
+ }
2605
+ walk(val);
2606
+ }
2607
+ }
2608
+ };
2609
+ walk(obj);
2610
+ }
2611
+ function defineBundle(spec) {
2612
+ if (!spec.slug) throw new Error("defineBundle: `slug` is required");
2613
+ if (!spec.agents?.length) throw new Error(`defineBundle(${spec.slug}): at least one agent is required`);
2614
+ const members = /* @__PURE__ */ new Set();
2615
+ const handles = /* @__PURE__ */ new Set();
2616
+ for (const a of spec.agents) {
2617
+ if (members.has(a.slug)) throw new Error(`defineBundle(${spec.slug}): duplicate agent slug "${a.slug}"`);
2618
+ members.add(a.slug);
2619
+ for (const s of a.skills ?? []) handles.add(s.name);
2620
+ }
2621
+ const requires = spec.requires ?? [];
2622
+ const requiredSlugs = new Set(requires.map((r) => r.slug));
2623
+ const connectorGrants = spec.connectorGrants ?? [];
2624
+ const grantedByMember = /* @__PURE__ */ new Map();
2625
+ const allGranted = /* @__PURE__ */ new Set();
2626
+ for (const g of connectorGrants) {
2627
+ if (!members.has(g.agentSlug)) {
2628
+ throw new Error(`defineBundle(${spec.slug}): connector grant targets unknown agent "${g.agentSlug}" (not a bundle member)`);
2629
+ }
2630
+ const set = grantedByMember.get(g.agentSlug) ?? /* @__PURE__ */ new Set();
2631
+ for (const action of g.actions) {
2632
+ const full = action.includes(".") ? action : `${g.provider}.${action}`;
2633
+ set.add(full);
2634
+ allGranted.add(full);
2635
+ }
2636
+ grantedByMember.set(g.agentSlug, set);
2637
+ }
2638
+ const agents = spec.agents.map((a) => {
2639
+ const granted = grantedByMember.get(a.slug);
2640
+ if (!granted || granted.size === 0) return a;
2641
+ const extra = [...granted].filter((n) => !a.head.skillNames.includes(n));
2642
+ return extra.length ? { ...a, head: { ...a.head, skillNames: [...a.head.skillNames, ...extra] } } : a;
2643
+ });
2644
+ const requireResolvable = (name, allowed, where) => {
2645
+ if (handles.has(name)) return;
2646
+ if (allowed?.has(name)) return;
2647
+ if (isConnectorLooking(name)) {
2648
+ throw new Error(`defineBundle(${spec.slug}): ${where} references connector action "${name}" with no first-party handle and no matching connector grant \u2014 add a \`connectorGrants\` entry, or attach a first-party handle`);
2649
+ }
2650
+ throw new Error(`defineBundle(${spec.slug}): ${where} references skill/tool "${name}" with no first-party handle in the bundle`);
2651
+ };
2652
+ for (const a of agents) {
2653
+ const granted = grantedByMember.get(a.slug);
2654
+ for (const name of a.head.skillNames) requireResolvable(name, granted, `agent "${a.slug}"`);
2655
+ for (const d of a.head.delegateSlugs) {
2656
+ if (!members.has(d) && !requiredSlugs.has(d)) {
2657
+ throw new Error(`defineBundle(${spec.slug}): agent "${a.slug}" delegates to "${d}", which is neither a bundle member nor a declared \`requires\` dependency`);
2658
+ }
2659
+ }
2660
+ }
2661
+ const allRules = [...spec.rules ?? [], ...spec.agents.flatMap((a) => a.rules ?? [])];
2662
+ for (const r of allRules) {
2663
+ for (const t of ruleToolRefs(r)) {
2664
+ if (t.startsWith("agent-")) continue;
2665
+ requireResolvable(t, allGranted, `rule "${r.id}"`);
2666
+ }
2667
+ }
2668
+ const allWorkflows = [...spec.workflows ?? [], ...spec.agents.flatMap((a) => a.workflow ? [a.workflow] : [])];
2669
+ for (const w of allWorkflows) {
2670
+ for (const step of w.steps) {
2671
+ if (step.tool !== void 0) requireResolvable(step.tool, allGranted, `workflow "${w.name}" step "${step.id}"`);
2672
+ assertNoCredRefs(step.args, `workflow "${w.name}" step "${step.id}" args`);
2673
+ assertNoCredRefs(step.input, `workflow "${w.name}" step "${step.id}" input`);
2674
+ }
2675
+ }
2676
+ return {
2677
+ slug: spec.slug,
2678
+ ...spec.title ? { title: spec.title } : {},
2679
+ agents,
2680
+ // members carry any granted connector action names in skillNames
2681
+ rules: spec.rules ?? [],
2682
+ // SWARM-scoped only (per-agent rules stay on the AgentDefs)
2683
+ workflows: spec.workflows ?? [],
2684
+ // SWARM-scoped only
2685
+ connectorGrants,
2686
+ requires
2687
+ };
2688
+ }
2689
+ function mergeBundle(cfg, bundle) {
2690
+ const existing = new Set(cfg.agents.map((a) => a.slug));
2691
+ for (const a of bundle.agents) {
2692
+ if (existing.has(a.slug)) {
2693
+ throw new Error(`mergeBundle(${bundle.slug}): agent "${a.slug}" already exists in the swarm config \u2014 bundle members must not shadow host agents`);
2694
+ }
2695
+ }
2696
+ return {
2697
+ ...cfg,
2698
+ agents: [...cfg.agents, ...bundle.agents],
2699
+ rules: [...cfg.rules ?? [], ...bundle.rules],
2700
+ workflows: [...cfg.workflows ?? [], ...bundle.workflows]
2701
+ };
2702
+ }
2703
+ function toBundleContent(def) {
2704
+ return {
2705
+ slug: def.slug,
2706
+ ...def.title ? { title: def.title } : {},
2707
+ agents: def.agents.map((a) => {
2708
+ const { version: _version, ...content } = a.head;
2709
+ return content;
2710
+ }),
2711
+ rules: def.rules,
2712
+ workflows: def.workflows,
2713
+ connectorGrants: def.connectorGrants,
2714
+ requires: def.requires
2715
+ };
2716
+ }
1356
2717
  var ASK_TOOL_NAME = "ask";
1357
2718
  var askFieldSchema = import_zod4.z.object({
1358
2719
  kind: import_zod4.z.enum(["confirm", "buttons", "select", "multiselect", "number", "text", "textarea"]),
@@ -1395,23 +2756,60 @@ function buildAskMastraTool() {
1395
2756
  function defineSwarm(cfg) {
1396
2757
  const seedable = cfg.storage;
1397
2758
  for (const a of cfg.agents) seedable.seedAgent?.(a.head);
2759
+ const rules = [...cfg.rules ?? [], ...cfg.agents.flatMap((a) => a.rules ?? [])];
2760
+ const workflows = [...cfg.workflows ?? [], ...cfg.agents.flatMap((a) => a.workflow ? [a.workflow] : [])];
2761
+ const namedWorkflows = workflows.filter((w) => w.compliance === "strict" && w.scopeAgent === void 0);
2762
+ const agentWorkflows = {};
2763
+ for (const w of workflows) if (w.compliance === "strict" && w.scopeAgent !== void 0) agentWorkflows[w.scopeAgent] = w;
2764
+ const hasSoft = rules.some((r) => r.level === "advise") || workflows.some((w) => w.compliance === "advisory" && !!w.description);
2765
+ const softPolicy = hasSoft ? (slug) => softPolicyFor(slug, rules, workflows) : void 0;
2766
+ const policy = cfg.toolApproval ?? { mode: "flag" };
2767
+ const enforceTool = rules.filter((r) => r.seam === "tool" && r.level === "enforce");
2768
+ const enforceGen = rules.filter((r) => r.seam === "generation" && r.level === "enforce");
2769
+ const composedHooks = { ...cfg.hooks };
2770
+ if (enforceTool.length) composedHooks.preToolCall = composeToolHooks({ rules: enforceTool, host: cfg.hooks?.preToolCall, policy });
2771
+ if (enforceGen.length) composedHooks.preGeneration = composeGenerationHooks({ rules: enforceGen, host: cfg.hooks?.preGeneration });
1398
2772
  const engine = new SwarmEngine({
2773
+ softPolicy,
2774
+ workflows: namedWorkflows,
2775
+ agentWorkflows,
1399
2776
  storage: cfg.storage,
1400
2777
  model: allowListModelProvider({ allow: cfg.models.allow }),
1401
2778
  modelFactory: cfg.modelFactory,
2779
+ // SP10: thread the optional cheap-model router onto the engine. The allow-list above still validates every
2780
+ // resolved model — incl. a routed tier model. Undefined ⇒ no routing (identical to today).
2781
+ tier: cfg.models.tier,
1402
2782
  cost: cfg.cost,
1403
2783
  // Per-swarm skill registry, built from the agents passed in. No global state.
1404
2784
  resolveSkill: buildSkillResolver(cfg.agents),
2785
+ // PR2: opt-in connector-tools resolver (built by the host via materializeConnectors). Undefined ⇒ no-op.
2786
+ connectorTools: cfg.connectorTools,
1405
2787
  telemetry: resolveTelemetry(cfg.telemetry),
1406
2788
  mastraStore: cfg.mastraStore,
1407
2789
  memory: cfg.memory,
1408
2790
  pageContext: cfg.pageContext,
1409
2791
  scratchpad: cfg.scratchpad,
1410
- recallLane: cfg.recallLane
2792
+ recallLane: cfg.recallLane,
2793
+ // SP2 + SP5: build the decision-hook dispatcher once per swarm, baking in the non-removable tool-approval
2794
+ // policy. Always present (allow-all hooks + `{ mode: "flag" }` policy when unconfigured, so the engine's
2795
+ // seams stay uniform — no null-checks on the hot path). The dispatcher combines policy + the per-tool flag +
2796
+ // the optional `preToolCall` hook into the effective ToolDecision.
2797
+ hooks: (0, import_hooks3.createHookDispatcher)(composedHooks, cfg.toolApproval),
2798
+ // SP5: also pass the policy standalone so an engine built without a dispatcher (direct construction) still
2799
+ // enforces it; redundant here (the dispatcher already carries it) but keeps EngineOpts self-describing.
2800
+ toolApproval: cfg.toolApproval,
2801
+ // SP15: thread the optional SecretResolver onto the engine, which injects it per-run on the RequestContext.
2802
+ secrets: cfg.secrets,
2803
+ // SP3: best-effort per-event observer (metering debit/settle) — transport-agnostic, fired in run + resume.
2804
+ onEvent: cfg.onEvent,
2805
+ verifyCompletion: cfg.verifyCompletion
1411
2806
  });
1412
2807
  return { engine };
1413
2808
  }
1414
2809
 
2810
+ // src/index.ts
2811
+ var import_hooks4 = require("@nightowlsdev/hooks");
2812
+
1415
2813
  // src/storage/memory.ts
1416
2814
  var InMemoryStorage = class {
1417
2815
  evts = [];
@@ -1436,7 +2834,7 @@ var InMemoryStorage = class {
1436
2834
  this.suspends.set(`${tenantId}:${followupId}`, { runId, toolCallId });
1437
2835
  }
1438
2836
  markFollowupAnswered(followupId, tenantId) {
1439
- this.suspends.delete(`${tenantId}:${followupId}`);
2837
+ return this.suspends.delete(`${tenantId}:${followupId}`);
1440
2838
  }
1441
2839
  /** Test/host helper: read a run row (the RunStore interface is write-mostly). */
1442
2840
  getRun(runId) {
@@ -1553,18 +2951,52 @@ var InMemoryStorage = class {
1553
2951
  // src/auth.ts
1554
2952
  var customAuth = (fn) => ({ authenticate: fn });
1555
2953
 
2954
+ // src/rate-limit.ts
2955
+ function decideFixedWindow(prev, cfg, nowSec) {
2956
+ const windowValid = prev && nowSec - prev.windowStartSec < cfg.windowSec;
2957
+ const state = windowValid ? { count: prev.count + 1, windowStartSec: prev.windowStartSec } : { count: 1, windowStartSec: nowSec };
2958
+ const resetSec = Math.max(0, state.windowStartSec + cfg.windowSec - nowSec);
2959
+ const allow = state.count <= cfg.max;
2960
+ return { decision: { allow, remaining: Math.max(0, cfg.max - state.count), resetSec }, state };
2961
+ }
2962
+ function createInMemoryRateLimitStore() {
2963
+ const states = /* @__PURE__ */ new Map();
2964
+ let lastPruneSec = 0;
2965
+ return {
2966
+ async hit(key, cfg, nowSec) {
2967
+ if (nowSec > lastPruneSec) {
2968
+ for (const [k, s] of states) if (nowSec - s.windowStartSec >= cfg.windowSec) states.delete(k);
2969
+ lastPruneSec = nowSec;
2970
+ }
2971
+ const { decision, state } = decideFixedWindow(states.get(key) ?? null, cfg, nowSec);
2972
+ states.set(key, state);
2973
+ return decision;
2974
+ }
2975
+ };
2976
+ }
2977
+ function rateConfig(max, windowSec, fallbackMax) {
2978
+ const m = Number.isFinite(max) && max > 0 ? Math.floor(max) : fallbackMax;
2979
+ return { windowSec, max: m };
2980
+ }
2981
+
1556
2982
  // src/index.ts
1557
2983
  var VERSION = "0.0.0";
1558
2984
  // Annotate the CommonJS export names for ESM import in node:
1559
2985
  0 && (module.exports = {
2986
+ ALLOW,
2987
+ ALLOW_TOOL,
1560
2988
  ASK_TOOL_NAME,
2989
+ AgentMutationForbidden,
1561
2990
  CapturingExporter,
1562
2991
  CostGovernor,
2992
+ DEFAULT_READ_ONLY_TOOLS,
1563
2993
  DelegateBudgets,
1564
2994
  GUARDRAILS,
2995
+ HookDispatcher,
1565
2996
  InMemoryContainerFloor,
1566
2997
  InMemoryStorage,
1567
2998
  PRICE_TABLE,
2999
+ ReserveDenied,
1568
3000
  RowCache,
1569
3001
  SCRATCHPAD_MAX_ENTRY_CHARS,
1570
3002
  SCRATCHPAD_MAX_KEYS,
@@ -1572,17 +3004,37 @@ var VERSION = "0.0.0";
1572
3004
  SwarmEngine,
1573
3005
  VERSION,
1574
3006
  allowListModelProvider,
3007
+ ask,
3008
+ assertActorMayMutateDefinition,
1575
3009
  buildSkillResolver,
3010
+ composePolicyPrompt,
1576
3011
  composeSystemPrompt,
1577
3012
  compositeTelemetry,
1578
3013
  containerFloor,
3014
+ createHookDispatcher,
3015
+ createInMemoryRateLimitStore,
1579
3016
  customAuth,
1580
3017
  customTelemetry,
3018
+ decideFixedWindow,
1581
3019
  defineAgent,
3020
+ defineBundle,
3021
+ defineHook,
3022
+ defineRule,
1582
3023
  defineSkill,
1583
3024
  defineSwarm,
1584
3025
  defineTool,
3026
+ defineWorkflow,
3027
+ deny,
1585
3028
  ev,
1586
3029
  isEvent,
1587
- resolveTelemetry
3030
+ isTierSentinel,
3031
+ mergeBundle,
3032
+ priceUsage,
3033
+ rateConfig,
3034
+ resolveTelemetry,
3035
+ resolveTier,
3036
+ sumBreakdowns,
3037
+ sumTurnUsage,
3038
+ tierModelId,
3039
+ toBundleContent
1588
3040
  });