@nightowlsdev/core 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +270 -0
- package/dist/index.cjs +1815 -148
- package/dist/index.d.cts +1154 -80
- package/dist/index.d.ts +1154 -80
- package/dist/index.js +1785 -147
- package/package.json +5 -4
package/dist/index.cjs
CHANGED
|
@@ -20,14 +20,21 @@ 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,
|
|
28
|
+
ClientToolError: () => ClientToolError,
|
|
25
29
|
CostGovernor: () => CostGovernor,
|
|
30
|
+
DEFAULT_READ_ONLY_TOOLS: () => import_hooks4.DEFAULT_READ_ONLY_TOOLS,
|
|
26
31
|
DelegateBudgets: () => DelegateBudgets,
|
|
27
32
|
GUARDRAILS: () => GUARDRAILS,
|
|
33
|
+
HookDispatcher: () => import_hooks4.HookDispatcher,
|
|
28
34
|
InMemoryContainerFloor: () => InMemoryContainerFloor,
|
|
29
35
|
InMemoryStorage: () => InMemoryStorage,
|
|
30
36
|
PRICE_TABLE: () => PRICE_TABLE,
|
|
37
|
+
ReserveDenied: () => ReserveDenied,
|
|
31
38
|
RowCache: () => RowCache,
|
|
32
39
|
SCRATCHPAD_MAX_ENTRY_CHARS: () => SCRATCHPAD_MAX_ENTRY_CHARS,
|
|
33
40
|
SCRATCHPAD_MAX_KEYS: () => SCRATCHPAD_MAX_KEYS,
|
|
@@ -35,23 +42,61 @@ __export(index_exports, {
|
|
|
35
42
|
SwarmEngine: () => SwarmEngine,
|
|
36
43
|
VERSION: () => VERSION,
|
|
37
44
|
allowListModelProvider: () => allowListModelProvider,
|
|
45
|
+
ask: () => import_hooks4.ask,
|
|
46
|
+
assertActorMayMutateDefinition: () => assertActorMayMutateDefinition,
|
|
47
|
+
buildSingleAgentSwarm: () => buildSingleAgentSwarm,
|
|
38
48
|
buildSkillResolver: () => buildSkillResolver,
|
|
49
|
+
composePolicyPrompt: () => composePolicyPrompt,
|
|
39
50
|
composeSystemPrompt: () => composeSystemPrompt,
|
|
40
51
|
compositeTelemetry: () => compositeTelemetry,
|
|
41
52
|
containerFloor: () => containerFloor,
|
|
53
|
+
createHookDispatcher: () => import_hooks4.createHookDispatcher,
|
|
54
|
+
createInMemoryRateLimitStore: () => createInMemoryRateLimitStore,
|
|
55
|
+
createRunState: () => createRunState,
|
|
42
56
|
customAuth: () => customAuth,
|
|
43
57
|
customTelemetry: () => customTelemetry,
|
|
58
|
+
decideFixedWindow: () => decideFixedWindow,
|
|
44
59
|
defineAgent: () => defineAgent,
|
|
60
|
+
defineBundle: () => defineBundle,
|
|
61
|
+
defineClientTool: () => defineClientTool,
|
|
62
|
+
defineHook: () => import_hooks4.defineHook,
|
|
63
|
+
defineRule: () => defineRule,
|
|
45
64
|
defineSkill: () => defineSkill,
|
|
46
65
|
defineSwarm: () => defineSwarm,
|
|
47
66
|
defineTool: () => defineTool,
|
|
67
|
+
defineWorkflow: () => defineWorkflow,
|
|
68
|
+
deny: () => import_hooks4.deny,
|
|
69
|
+
drainTrajectory: () => drainTrajectory,
|
|
48
70
|
ev: () => ev,
|
|
49
71
|
isEvent: () => isEvent,
|
|
50
|
-
|
|
72
|
+
isTierSentinel: () => isTierSentinel,
|
|
73
|
+
mergeBundle: () => mergeBundle,
|
|
74
|
+
priceUsage: () => priceUsage,
|
|
75
|
+
rateConfig: () => rateConfig,
|
|
76
|
+
resolveTelemetry: () => resolveTelemetry,
|
|
77
|
+
resolveTier: () => resolveTier,
|
|
78
|
+
runAgent: () => runAgent,
|
|
79
|
+
runToTrajectory: () => runToTrajectory,
|
|
80
|
+
sumBreakdowns: () => sumBreakdowns,
|
|
81
|
+
sumTurnUsage: () => sumTurnUsage,
|
|
82
|
+
tierModelId: () => tierModelId,
|
|
83
|
+
toBundleContent: () => toBundleContent
|
|
51
84
|
});
|
|
52
85
|
module.exports = __toCommonJS(index_exports);
|
|
53
86
|
|
|
54
87
|
// src/types.ts
|
|
88
|
+
function assertActorMayMutateDefinition(actor) {
|
|
89
|
+
if (actor.type === "agent") {
|
|
90
|
+
throw new AgentMutationForbidden(actor.agentSlug);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
var AgentMutationForbidden = class extends Error {
|
|
94
|
+
code = "AGENT_MUTATION_FORBIDDEN";
|
|
95
|
+
constructor(agentSlug) {
|
|
96
|
+
super(`agent principal "${agentSlug}" may not mutate an agent definition (publish/rollback)`);
|
|
97
|
+
this.name = "AgentMutationForbidden";
|
|
98
|
+
}
|
|
99
|
+
};
|
|
55
100
|
var SCRATCHPAD_MAX_ENTRY_CHARS = 4e3;
|
|
56
101
|
var SCRATCHPAD_MAX_KEYS = 64;
|
|
57
102
|
|
|
@@ -66,11 +111,269 @@ function isEvent(e, type) {
|
|
|
66
111
|
// src/define.ts
|
|
67
112
|
var import_zod4 = require("zod");
|
|
68
113
|
var import_tools4 = require("@mastra/core/tools");
|
|
114
|
+
var import_hooks3 = require("@nightowlsdev/hooks");
|
|
69
115
|
|
|
70
116
|
// src/engine.ts
|
|
71
117
|
var import_mastra = require("@mastra/core/mastra");
|
|
72
118
|
var import_storage = require("@mastra/core/storage");
|
|
73
119
|
var import_request_context = require("@mastra/core/request-context");
|
|
120
|
+
var import_hooks = require("@nightowlsdev/hooks");
|
|
121
|
+
|
|
122
|
+
// src/tool-gate.ts
|
|
123
|
+
var TOOL_GATE_KEY = "__nightowlsdev_toolGate";
|
|
124
|
+
var TOOL_EXECUTORS = /* @__PURE__ */ new WeakMap();
|
|
125
|
+
function setToolExecutor(handle, exec) {
|
|
126
|
+
TOOL_EXECUTORS.set(handle, exec);
|
|
127
|
+
}
|
|
128
|
+
function getToolExecutor(handle) {
|
|
129
|
+
return TOOL_EXECUTORS.get(handle);
|
|
130
|
+
}
|
|
131
|
+
var ToolBlockedError = class extends Error {
|
|
132
|
+
constructor(toolName, reason) {
|
|
133
|
+
super(`tool "${toolName}" blocked: ${reason}`);
|
|
134
|
+
this.toolName = toolName;
|
|
135
|
+
this.reason = reason;
|
|
136
|
+
this.name = "ToolBlockedError";
|
|
137
|
+
}
|
|
138
|
+
toolName;
|
|
139
|
+
reason;
|
|
140
|
+
blocked = true;
|
|
141
|
+
};
|
|
142
|
+
function approvalSuspendPayload(args) {
|
|
143
|
+
const prompt = args.reason?.trim() ? `Approve \`${args.toolName}\`? ${args.reason.trim()}` : `Approve running \`${args.toolName}\`?`;
|
|
144
|
+
return {
|
|
145
|
+
to: "user",
|
|
146
|
+
prompt,
|
|
147
|
+
field: { kind: "confirm", confirmLabel: "Approve", rejectLabel: "Reject" },
|
|
148
|
+
asker: args.asker,
|
|
149
|
+
kind: "approval",
|
|
150
|
+
toolName: args.toolName
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function isApproved(answer) {
|
|
154
|
+
if (typeof answer === "boolean") return answer;
|
|
155
|
+
if (typeof answer === "string") {
|
|
156
|
+
return /^(y|yes|approve|approved|ok|true|confirm|confirmed)$/i.test(answer.trim());
|
|
157
|
+
}
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
function gateErrMessage(err) {
|
|
161
|
+
return err instanceof Error ? err.message : String(err);
|
|
162
|
+
}
|
|
163
|
+
async function executeToolWithGate(opts) {
|
|
164
|
+
let decision;
|
|
165
|
+
try {
|
|
166
|
+
decision = await opts.gate(opts.ev);
|
|
167
|
+
} catch (err) {
|
|
168
|
+
return { ok: false, error: gateErrMessage(err), reason: gateErrMessage(err) };
|
|
169
|
+
}
|
|
170
|
+
if (decision.action === "deny") return { ok: false, error: decision.reason, reason: decision.reason };
|
|
171
|
+
if (decision.action === "ask") return { ok: false, suspended: true, reason: decision.reason };
|
|
172
|
+
try {
|
|
173
|
+
return { ok: true, result: await opts.run() };
|
|
174
|
+
} catch (err) {
|
|
175
|
+
return { ok: false, error: gateErrMessage(err) };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function toolPreCallEvent(args) {
|
|
179
|
+
return {
|
|
180
|
+
runId: args.runId,
|
|
181
|
+
tenantId: args.tenantId,
|
|
182
|
+
agentSlug: args.agentSlug,
|
|
183
|
+
toolName: args.toolName,
|
|
184
|
+
origin: args.origin,
|
|
185
|
+
needsApproval: args.needsApproval,
|
|
186
|
+
args: args.args
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// src/secrets.ts
|
|
191
|
+
var SECRET_RESOLVER_KEY = "__nightowlsdev_secretResolver";
|
|
192
|
+
function bindSecrets(resolver, ctx) {
|
|
193
|
+
return {
|
|
194
|
+
resolve: (ref) => resolver ? resolver.resolve(ref, ctx) : Promise.resolve(void 0)
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// src/run-state.ts
|
|
199
|
+
var RUN_STATE_KEY = "__nightowls_run_state__";
|
|
200
|
+
function snapshotCopy(store) {
|
|
201
|
+
const plain = Object.fromEntries(store);
|
|
202
|
+
try {
|
|
203
|
+
return (globalThis.structuredClone ?? ((v) => JSON.parse(JSON.stringify(v))))(plain);
|
|
204
|
+
} catch {
|
|
205
|
+
try {
|
|
206
|
+
return JSON.parse(JSON.stringify(plain));
|
|
207
|
+
} catch {
|
|
208
|
+
return plain;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function createRunState(seed) {
|
|
213
|
+
const m = new Map(seed ? Object.entries(seed) : void 0);
|
|
214
|
+
return {
|
|
215
|
+
get: (key) => m.get(key),
|
|
216
|
+
set: (key, value) => void m.set(key, value),
|
|
217
|
+
has: (key) => m.has(key),
|
|
218
|
+
delete: (key) => m.delete(key),
|
|
219
|
+
entries: () => snapshotCopy(m)
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// src/step-driver.ts
|
|
224
|
+
function initialWorkflowState(wf) {
|
|
225
|
+
return { workflow: wf.name, cursor: wf.start, outputs: {}, generationIndex: 0 };
|
|
226
|
+
}
|
|
227
|
+
function resolveRef(ref, state, input) {
|
|
228
|
+
if (ref === "input") return input.message;
|
|
229
|
+
if (ref.startsWith("steps.")) return state.outputs[ref.slice("steps.".length)];
|
|
230
|
+
return void 0;
|
|
231
|
+
}
|
|
232
|
+
function resolveValue(v, state, input) {
|
|
233
|
+
if (v && typeof v === "object" && "$ref" in v) {
|
|
234
|
+
const ref = String(v.$ref);
|
|
235
|
+
if (ref.startsWith("steps.")) {
|
|
236
|
+
const id = ref.slice("steps.".length);
|
|
237
|
+
if (!(id in state.outputs)) {
|
|
238
|
+
throw new Error(`workflow $ref "${ref}" references step "${id}" which has not run (skipped branch or forward reference)`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return resolveRef(ref, state, input);
|
|
242
|
+
}
|
|
243
|
+
return v;
|
|
244
|
+
}
|
|
245
|
+
function resolveMap(o, state, input) {
|
|
246
|
+
if (!o) return void 0;
|
|
247
|
+
const out = {};
|
|
248
|
+
for (const [k, v] of Object.entries(o)) out[k] = resolveValue(v, state, input);
|
|
249
|
+
return out;
|
|
250
|
+
}
|
|
251
|
+
function agentMessage(step, resolvedInput) {
|
|
252
|
+
const base2 = step.instruction ?? "";
|
|
253
|
+
if (resolvedInput && Object.keys(resolvedInput).length) return `${base2}
|
|
254
|
+
|
|
255
|
+
Context:
|
|
256
|
+
${JSON.stringify(resolvedInput)}`;
|
|
257
|
+
return base2;
|
|
258
|
+
}
|
|
259
|
+
var DEAD_END = /* @__PURE__ */ Symbol("dead-end");
|
|
260
|
+
function nextStep(step, state, input) {
|
|
261
|
+
if (step.next === void 0) return void 0;
|
|
262
|
+
if (typeof step.next === "string") return step.next;
|
|
263
|
+
for (const t of step.next) {
|
|
264
|
+
if (!t.when) return t.to;
|
|
265
|
+
const v = resolveRef(t.when.$ref, state, input);
|
|
266
|
+
if (t.when.exists !== void 0) {
|
|
267
|
+
if (v !== void 0 === t.when.exists) return t.to;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
if (t.when.eq !== void 0) {
|
|
271
|
+
if (v === t.when.eq) return t.to;
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
return t.to;
|
|
275
|
+
}
|
|
276
|
+
return DEAD_END;
|
|
277
|
+
}
|
|
278
|
+
var StepDriver = class {
|
|
279
|
+
constructor(wf, deps) {
|
|
280
|
+
this.wf = wf;
|
|
281
|
+
this.deps = deps;
|
|
282
|
+
}
|
|
283
|
+
wf;
|
|
284
|
+
deps;
|
|
285
|
+
ts = 0;
|
|
286
|
+
base(ctx) {
|
|
287
|
+
return { runId: ctx.runId, agentSlug: ctx.agentSlug, ts: this.deps.nextTs ? this.deps.nextTs() : this.ts++ };
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Drive the workflow from `state` (fresh or resumed). Yields the run's SwarmEvents. Returns a `DriveOutcome`
|
|
291
|
+
* so the engine can finalize. B2 scope: linear `agent`/`tool` steps + `$ref` wiring + per-step snapshot.
|
|
292
|
+
*/
|
|
293
|
+
async *drive(state, ctx, input) {
|
|
294
|
+
const byId = new Map(this.wf.steps.map((s) => [s.id, s]));
|
|
295
|
+
let guard = 0;
|
|
296
|
+
let retryStep = "";
|
|
297
|
+
let retriesLeft = 0;
|
|
298
|
+
const budget = this.wf.steps.length * 8 + 8;
|
|
299
|
+
while (true) {
|
|
300
|
+
if (guard++ > budget) return { kind: "failed", stage: "workflow", message: "step budget exceeded" };
|
|
301
|
+
const step = byId.get(state.cursor);
|
|
302
|
+
if (!step) return { kind: "failed", stage: "workflow", message: `unknown step "${state.cursor}"` };
|
|
303
|
+
yield ev("swarm.status", this.base(ctx), { state: step.tool ? "tool" : "thinking", note: `step:${step.id}` });
|
|
304
|
+
let stepError;
|
|
305
|
+
if (step.agent !== void 0) {
|
|
306
|
+
try {
|
|
307
|
+
const msg = agentMessage(step, resolveMap(step.input, state, input));
|
|
308
|
+
const { text } = yield* this.deps.runAgentStep(step.agent, msg, state.generationIndex, ctx);
|
|
309
|
+
state.outputs[step.id] = text;
|
|
310
|
+
state.generationIndex += 1;
|
|
311
|
+
} catch (err) {
|
|
312
|
+
if (err && typeof err === "object" && "stage" in err) throw err;
|
|
313
|
+
stepError = err instanceof Error ? err.message : String(err);
|
|
314
|
+
}
|
|
315
|
+
} else if (step.tool !== void 0) {
|
|
316
|
+
let args;
|
|
317
|
+
try {
|
|
318
|
+
args = resolveMap(step.args, state, input) ?? {};
|
|
319
|
+
} catch (err) {
|
|
320
|
+
stepError = err instanceof Error ? err.message : String(err);
|
|
321
|
+
}
|
|
322
|
+
if (args !== void 0) {
|
|
323
|
+
const toolCallId = `${ctx.runId}:wf:${step.id}`;
|
|
324
|
+
yield ev("swarm.tool_call", this.base(ctx), { toolCallId, name: step.tool, args, needsApproval: false });
|
|
325
|
+
const r = await this.deps.runToolStep(step.tool, args, ctx);
|
|
326
|
+
yield ev("swarm.tool_result", this.base(ctx), { toolCallId, ok: r.ok, result: r.result, error: r.error });
|
|
327
|
+
if (r.ok) state.outputs[step.id] = r.result;
|
|
328
|
+
else if (r.suspended) {
|
|
329
|
+
const followupId = `${ctx.runId}:wf:${step.id}`;
|
|
330
|
+
yield ev("swarm.question", this.base(ctx), { followupId, toolCallId: followupId, to: "user", prompt: r.reason ?? `Approve "${step.tool}"?`, field: { kind: "confirm" } });
|
|
331
|
+
state.pending = { kind: "approval", stepId: step.id, followupId, toolCallId: followupId };
|
|
332
|
+
await this.deps.saveState(ctx.runId, state);
|
|
333
|
+
return { kind: "suspended", state };
|
|
334
|
+
} else stepError = r.error ?? r.reason ?? "blocked";
|
|
335
|
+
}
|
|
336
|
+
} else if (step.human !== void 0) {
|
|
337
|
+
if (!(step.id in state.outputs)) {
|
|
338
|
+
const followupId = `${ctx.runId}:wf:${step.id}`;
|
|
339
|
+
yield ev("swarm.question", this.base(ctx), { followupId, toolCallId: followupId, to: "user", prompt: step.human.prompt, field: step.human.field });
|
|
340
|
+
state.pending = { kind: "human", stepId: step.id, followupId, toolCallId: followupId };
|
|
341
|
+
await this.deps.saveState(ctx.runId, state);
|
|
342
|
+
return { kind: "suspended", state };
|
|
343
|
+
}
|
|
344
|
+
state.pending = void 0;
|
|
345
|
+
}
|
|
346
|
+
if (stepError !== void 0) {
|
|
347
|
+
const oe = step.onError ?? "fail";
|
|
348
|
+
if (oe === "fail") return { kind: "failed", stage: "workflow", message: `step "${step.id}" failed: ${stepError}` };
|
|
349
|
+
if (typeof oe === "object" && "to" in oe) {
|
|
350
|
+
state.cursor = oe.to;
|
|
351
|
+
retryStep = "";
|
|
352
|
+
await this.deps.saveState(ctx.runId, state);
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
if (typeof oe === "object" && "retry" in oe) {
|
|
356
|
+
if (retryStep !== step.id) {
|
|
357
|
+
retryStep = step.id;
|
|
358
|
+
retriesLeft = oe.retry;
|
|
359
|
+
}
|
|
360
|
+
if (retriesLeft > 0) {
|
|
361
|
+
retriesLeft -= 1;
|
|
362
|
+
await this.deps.saveState(ctx.runId, state);
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
return { kind: "failed", stage: "workflow", message: `step "${step.id}" failed after retries: ${stepError}` };
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
retryStep = "";
|
|
369
|
+
const next = nextStep(step, state, input);
|
|
370
|
+
if (next === DEAD_END) return { kind: "failed", stage: "workflow", message: `no transition from step "${step.id}"` };
|
|
371
|
+
state.cursor = next ?? state.cursor;
|
|
372
|
+
await this.deps.saveState(ctx.runId, state);
|
|
373
|
+
if (next === void 0) return { kind: "done" };
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
};
|
|
74
377
|
|
|
75
378
|
// src/mastra-map.ts
|
|
76
379
|
var import_agent = require("@mastra/core/agent");
|
|
@@ -94,6 +397,16 @@ function composeSystemPrompt(row) {
|
|
|
94
397
|
{ role: "system", content: persona }
|
|
95
398
|
];
|
|
96
399
|
}
|
|
400
|
+
function composePolicyPrompt(lines) {
|
|
401
|
+
if (!lines.length) return [];
|
|
402
|
+
return [
|
|
403
|
+
{
|
|
404
|
+
role: "system",
|
|
405
|
+
content: `Policy \u2014 follow these unless the user explicitly overrides:
|
|
406
|
+
${lines.map((l) => `- ${l}`).join("\n")}`
|
|
407
|
+
}
|
|
408
|
+
];
|
|
409
|
+
}
|
|
97
410
|
function composeScratchpadPrompt(entries) {
|
|
98
411
|
const render = (section) => {
|
|
99
412
|
const rows = entries.filter((e) => e.section === section).map((e) => `- [${e.key}] (${e.author} \u2190 ${e.requestedBy}) ${e.content}`);
|
|
@@ -109,20 +422,100 @@ ${render("meta")}`
|
|
|
109
422
|
return { role: "system", content };
|
|
110
423
|
}
|
|
111
424
|
|
|
425
|
+
// src/tier.ts
|
|
426
|
+
var SENTINEL = "tier:";
|
|
427
|
+
function isTierSentinel(modelId) {
|
|
428
|
+
return typeof modelId === "string" && modelId.startsWith(SENTINEL);
|
|
429
|
+
}
|
|
430
|
+
function requestedTierFrom(modelId, cfg) {
|
|
431
|
+
const suffix = modelId.slice(SENTINEL.length).trim();
|
|
432
|
+
if (suffix === "swift" || suffix === "genius") return suffix;
|
|
433
|
+
return cfg.default ?? "swift";
|
|
434
|
+
}
|
|
435
|
+
function resolveTier(modelId, cfg, ctx) {
|
|
436
|
+
if (!isTierSentinel(modelId)) {
|
|
437
|
+
return { modelId, downgraded: false };
|
|
438
|
+
}
|
|
439
|
+
let requested = requestedTierFrom(modelId, cfg);
|
|
440
|
+
let escalated = false;
|
|
441
|
+
if (cfg.escalate) {
|
|
442
|
+
const bumped = cfg.escalate(ctx);
|
|
443
|
+
if (bumped === "genius" && requested !== "genius") {
|
|
444
|
+
requested = "genius";
|
|
445
|
+
escalated = true;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
if (requested === "genius") {
|
|
449
|
+
const geniusAllowed = cfg.allowGenius === true && typeof cfg.tiers.genius === "string";
|
|
450
|
+
if (geniusAllowed) {
|
|
451
|
+
return { modelId: cfg.tiers.genius, tier: "genius", downgraded: false, ...escalated ? { escalated: true } : {} };
|
|
452
|
+
}
|
|
453
|
+
return { modelId: cfg.tiers.swift, tier: "swift", downgraded: true, requestedTier: "genius" };
|
|
454
|
+
}
|
|
455
|
+
return { modelId: cfg.tiers.swift, tier: "swift", downgraded: false };
|
|
456
|
+
}
|
|
457
|
+
function tierModelId(modelId, cfg, ctx) {
|
|
458
|
+
if (!cfg) return modelId;
|
|
459
|
+
return resolveTier(modelId, cfg, ctx).modelId;
|
|
460
|
+
}
|
|
461
|
+
|
|
112
462
|
// src/mastra-map.ts
|
|
463
|
+
async function gateDelegation(rc, subSlug) {
|
|
464
|
+
const gate = rc.get(TOOL_GATE_KEY);
|
|
465
|
+
if (!gate) return;
|
|
466
|
+
const decision = await gate(
|
|
467
|
+
toolPreCallEvent({
|
|
468
|
+
runId: rc.get("runId") ?? "",
|
|
469
|
+
tenantId: rc.get("tenantId") ?? "default",
|
|
470
|
+
// The agent doing the delegating is the run owner / the parent in the path (the requestContext's agentSlug).
|
|
471
|
+
agentSlug: rc.get("agentSlug") ?? "",
|
|
472
|
+
toolName: `agent-${subSlug}`,
|
|
473
|
+
origin: "first-party",
|
|
474
|
+
// A delegation has no per-tool `needsApproval` flag; surface false so a "flag"-mode policy leaves it
|
|
475
|
+
// un-gated (today's behaviour) and only an "all-side-effecting" policy / an explicit hook can deny it.
|
|
476
|
+
needsApproval: false,
|
|
477
|
+
args: void 0
|
|
478
|
+
})
|
|
479
|
+
);
|
|
480
|
+
if (decision.action === "deny") {
|
|
481
|
+
throw new Error(`delegation to "${subSlug}" denied: ${decision.reason}`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
113
484
|
var MAX_DELEGATION_DEPTH = 4;
|
|
485
|
+
var CONNECTOR_TOOLS_CACHE_KEY = "__nightowls_connector_tools";
|
|
114
486
|
function memoryFor(args, row) {
|
|
115
487
|
return args.resolveMemory ? args.resolveMemory(row) : args.memory;
|
|
116
488
|
}
|
|
117
|
-
function toolsFor(args, row) {
|
|
489
|
+
function toolsFor(args, row, connectorByName) {
|
|
118
490
|
const out = { ...args.builtinTools ?? {} };
|
|
119
491
|
for (const name of row.skillNames) {
|
|
120
|
-
const skill = args.resolveSkill(name);
|
|
492
|
+
const skill = args.resolveSkill(name) ?? connectorByName?.[name];
|
|
121
493
|
const mt = skill && __getMastraTool(skill);
|
|
122
494
|
if (mt) out[name] = mt;
|
|
123
495
|
}
|
|
124
496
|
return out;
|
|
125
497
|
}
|
|
498
|
+
async function connectorByNameFor(args, rc, agentSlug) {
|
|
499
|
+
if (!args.connectorTools) return {};
|
|
500
|
+
const cached = rc.get(CONNECTOR_TOOLS_CACHE_KEY);
|
|
501
|
+
if (cached) return cached;
|
|
502
|
+
const resolve = args.connectorTools;
|
|
503
|
+
const build = (async () => {
|
|
504
|
+
const ctx = {
|
|
505
|
+
tenantId: rc.get("tenantId") ?? "default",
|
|
506
|
+
userId: rc.get("userId") ?? "",
|
|
507
|
+
runId: rc.get("runId") ?? "",
|
|
508
|
+
agentSlug,
|
|
509
|
+
// informational — materialize is tenant-scoped; first caller's slug seeds the shared cache
|
|
510
|
+
threadId: rc.get("threadId") ?? ""
|
|
511
|
+
};
|
|
512
|
+
const out = {};
|
|
513
|
+
for (const t of await resolve(ctx)) out[t.name] = t;
|
|
514
|
+
return out;
|
|
515
|
+
})();
|
|
516
|
+
rc.set?.(CONNECTOR_TOOLS_CACHE_KEY, build);
|
|
517
|
+
return build;
|
|
518
|
+
}
|
|
126
519
|
async function withScratchpad(args, base2, rc) {
|
|
127
520
|
if (!args.loadScratchpad) return base2;
|
|
128
521
|
const tenantId = rc.get("tenantId") ?? "default";
|
|
@@ -130,8 +523,13 @@ async function withScratchpad(args, base2, rc) {
|
|
|
130
523
|
const entries = await args.loadScratchpad(container, tenantId);
|
|
131
524
|
return [...base2, composeScratchpadPrompt(entries)];
|
|
132
525
|
}
|
|
526
|
+
function withSoftPolicy(args, base2, slug) {
|
|
527
|
+
const soft = args.softPolicy?.(slug) ?? [];
|
|
528
|
+
return soft.length ? [...base2, ...composePolicyPrompt(soft)] : base2;
|
|
529
|
+
}
|
|
133
530
|
async function modelFor(args, row, tenantId) {
|
|
134
|
-
const
|
|
531
|
+
const effective = tierModelId(row.modelId, args.tier, { tenantId, agentSlug: row.slug, pinnedModelId: row.modelId });
|
|
532
|
+
const id = await args.model.resolve(effective, { tenantId });
|
|
135
533
|
return args.modelFactory(id, row.slug);
|
|
136
534
|
}
|
|
137
535
|
function buildSubAgent(args, row, depth, path) {
|
|
@@ -143,9 +541,12 @@ function buildSubAgent(args, row, depth, path) {
|
|
|
143
541
|
// personality so the orchestrator's LLM knows WHAT this delegate is for (role is a coarse enum).
|
|
144
542
|
description: row.personality || `Agent ${row.slug} (${row.role})`,
|
|
145
543
|
...memoryFor(args, row) ? { memory: memoryFor(args, row) } : {},
|
|
146
|
-
instructions: async ({ requestContext }) =>
|
|
544
|
+
instructions: async ({ requestContext }) => {
|
|
545
|
+
await gateDelegation(requestContext, row.slug);
|
|
546
|
+
return withScratchpad(args, withSoftPolicy(args, composeSystemPrompt(row), row.slug), requestContext);
|
|
547
|
+
},
|
|
147
548
|
model: async ({ requestContext }) => await modelFor(args, row, requestContext.get("tenantId") ?? "default"),
|
|
148
|
-
tools: toolsFor(args, row),
|
|
549
|
+
tools: (async ({ requestContext }) => toolsFor(args, row, await connectorByNameFor(args, requestContext, row.slug))),
|
|
149
550
|
agents: async ({ requestContext }) => await buildSubAgentMap(
|
|
150
551
|
args,
|
|
151
552
|
row.delegateSlugs ?? [],
|
|
@@ -174,9 +575,16 @@ function buildMastraAgent(args) {
|
|
|
174
575
|
// request). If Mastra rejects a dynamic `memory`, fall back to the static swarm Memory (root override is
|
|
175
576
|
// then sub-agents-only — see the spec's accepted limitation).
|
|
176
577
|
...args.resolveMemory || args.memory ? { memory: (async ({ requestContext }) => memoryFor(args, await load(requestContext))) } : {},
|
|
177
|
-
instructions: async ({ requestContext }) =>
|
|
578
|
+
instructions: async ({ requestContext }) => {
|
|
579
|
+
const row = await load(requestContext);
|
|
580
|
+
return withScratchpad(args, withSoftPolicy(args, composeSystemPrompt(row), row.slug), requestContext);
|
|
581
|
+
},
|
|
178
582
|
model: async ({ requestContext }) => await modelFor(args, await load(requestContext), requestContext.get("tenantId") ?? "default"),
|
|
179
|
-
tools: async ({ requestContext }) =>
|
|
583
|
+
tools: (async ({ requestContext }) => {
|
|
584
|
+
const row = await load(requestContext);
|
|
585
|
+
const connectorByName = await connectorByNameFor(args, requestContext, row.slug);
|
|
586
|
+
return { ...args.extraTools ?? {}, ...toolsFor(args, row, connectorByName) };
|
|
587
|
+
}),
|
|
180
588
|
// Delegation: the orchestrator's delegateSlugs become `agent-<slug>` tools (Mastra-native).
|
|
181
589
|
agents: async ({ requestContext }) => {
|
|
182
590
|
const row = await load(requestContext);
|
|
@@ -191,7 +599,7 @@ var import_tools = require("@mastra/core/tools");
|
|
|
191
599
|
var import_zod = require("zod");
|
|
192
600
|
|
|
193
601
|
// src/page-context.ts
|
|
194
|
-
var PAGE_CONTEXT_KEY = "
|
|
602
|
+
var PAGE_CONTEXT_KEY = "__nightowlsdev_pageContext";
|
|
195
603
|
function attachPageContext(rc, value) {
|
|
196
604
|
rc.set(PAGE_CONTEXT_KEY, value ?? {});
|
|
197
605
|
}
|
|
@@ -398,7 +806,7 @@ var InMemoryContainerFloor = class {
|
|
|
398
806
|
s.held = who;
|
|
399
807
|
if (s.timer) clearTimeout(s.timer);
|
|
400
808
|
s.timer = setTimeout(() => {
|
|
401
|
-
console.warn(`[
|
|
809
|
+
console.warn(`[@nightowlsdev/core] container floor force-released after ${this.maxHoldMs}ms: ${container} (held by ${who.label})`);
|
|
402
810
|
this.release(container, s, who);
|
|
403
811
|
}, this.maxHoldMs);
|
|
404
812
|
if (typeof s.timer.unref === "function") s.timer.unref();
|
|
@@ -433,25 +841,84 @@ var PRICE_TABLE = {
|
|
|
433
841
|
"openai/gpt-5.5": { inUsdPerMtok: 2.5, outUsdPerMtok: 10 },
|
|
434
842
|
"openai/gpt-5.5-mini": { inUsdPerMtok: 0.3, outUsdPerMtok: 1.2 }
|
|
435
843
|
};
|
|
436
|
-
function priceUsage(prices, modelId, u) {
|
|
437
|
-
const p = prices[modelId]
|
|
438
|
-
|
|
844
|
+
function priceUsage(prices, modelId, u, opts = {}) {
|
|
845
|
+
const p = prices[modelId];
|
|
846
|
+
if (!p) {
|
|
847
|
+
if (opts.failOnUnknownModel) {
|
|
848
|
+
throw new Error(
|
|
849
|
+
`[@nightowlsdev/core] no price entry for model '${modelId}' (failOnUnknownModel=true). Add it to PRICE_TABLE, the swarm cost.prices map, or a priceFeed.`
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
return 0;
|
|
853
|
+
}
|
|
854
|
+
const cacheReadRate = p.cacheReadUsdPerMtok ?? p.inUsdPerMtok;
|
|
855
|
+
const cacheWriteRate = p.cacheWriteUsdPerMtok ?? p.inUsdPerMtok;
|
|
856
|
+
const reasoningRate = p.reasoningUsdPerMtok ?? p.outUsdPerMtok;
|
|
857
|
+
const M = 1e6;
|
|
858
|
+
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;
|
|
859
|
+
}
|
|
860
|
+
var OPTIONAL_USAGE_CLASSES = [
|
|
861
|
+
"cacheReadTokens",
|
|
862
|
+
"cacheWriteTokens",
|
|
863
|
+
"reasoningTokens",
|
|
864
|
+
"toolCalls",
|
|
865
|
+
"agentActivations"
|
|
866
|
+
];
|
|
867
|
+
function sumBreakdowns(items) {
|
|
868
|
+
const total = { inputTokens: 0, outputTokens: 0 };
|
|
869
|
+
for (const b of items) {
|
|
870
|
+
total.inputTokens += b.inputTokens ?? 0;
|
|
871
|
+
total.outputTokens += b.outputTokens ?? 0;
|
|
872
|
+
for (const k of OPTIONAL_USAGE_CLASSES) {
|
|
873
|
+
const v = b[k];
|
|
874
|
+
if (v != null) total[k] = (total[k] ?? 0) + v;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
return total;
|
|
878
|
+
}
|
|
879
|
+
function sumTurnUsage(items) {
|
|
880
|
+
const order = [];
|
|
881
|
+
const breakdownsBySlug = /* @__PURE__ */ new Map();
|
|
882
|
+
const usdBySlug = /* @__PURE__ */ new Map();
|
|
883
|
+
for (const it of items) {
|
|
884
|
+
if (!breakdownsBySlug.has(it.slug)) {
|
|
885
|
+
order.push(it.slug);
|
|
886
|
+
breakdownsBySlug.set(it.slug, []);
|
|
887
|
+
usdBySlug.set(it.slug, 0);
|
|
888
|
+
}
|
|
889
|
+
breakdownsBySlug.get(it.slug).push(it.breakdown);
|
|
890
|
+
usdBySlug.set(it.slug, usdBySlug.get(it.slug) + it.cost.usd);
|
|
891
|
+
}
|
|
892
|
+
const bySlug = order.map((slug) => {
|
|
893
|
+
const breakdown2 = sumBreakdowns(breakdownsBySlug.get(slug));
|
|
894
|
+
const usd2 = usdBySlug.get(slug);
|
|
895
|
+
return { slug, breakdown: breakdown2, cost: { usd: usd2, breakdown: breakdown2 } };
|
|
896
|
+
});
|
|
897
|
+
const breakdown = sumBreakdowns(items.map((it) => it.breakdown));
|
|
898
|
+
const usd = items.reduce((a, it) => a + it.cost.usd, 0);
|
|
899
|
+
return { breakdown, cost: { usd, breakdown }, bySlug };
|
|
439
900
|
}
|
|
440
901
|
var CostGovernor = class {
|
|
441
902
|
constructor(opts) {
|
|
442
903
|
this.opts = opts;
|
|
443
|
-
this.prices = { ...PRICE_TABLE, ...opts.prices ?? {} };
|
|
904
|
+
this.prices = { ...PRICE_TABLE, ...opts.prices ?? {}, ...opts.priceFeed?.prices() ?? {} };
|
|
905
|
+
this.failOnUnknownModel = opts.failOnUnknownModel ?? false;
|
|
444
906
|
}
|
|
445
907
|
opts;
|
|
446
908
|
steps = 0;
|
|
447
909
|
usd = 0;
|
|
448
910
|
prices;
|
|
911
|
+
failOnUnknownModel;
|
|
449
912
|
step() {
|
|
450
913
|
this.steps++;
|
|
451
914
|
}
|
|
452
915
|
/** Price a single usage WITHOUT accumulating it (for per-generation telemetry cost). */
|
|
453
916
|
priceOf(modelId, u) {
|
|
454
|
-
return priceUsage(this.prices, modelId, u);
|
|
917
|
+
return priceUsage(this.prices, modelId, u, { failOnUnknownModel: this.failOnUnknownModel });
|
|
918
|
+
}
|
|
919
|
+
/** Price a single usage WITHOUT accumulating it, returning the usd + the breakdown it was priced from. */
|
|
920
|
+
costOf(modelId, u) {
|
|
921
|
+
return { usd: this.priceOf(modelId, u), breakdown: u };
|
|
455
922
|
}
|
|
456
923
|
addUsage(modelId, u) {
|
|
457
924
|
this.usd += this.priceOf(modelId, u);
|
|
@@ -459,6 +926,19 @@ var CostGovernor = class {
|
|
|
459
926
|
costUsd() {
|
|
460
927
|
return this.usd;
|
|
461
928
|
}
|
|
929
|
+
/** The current USD cap (SP9-core: the cap-that-asks reads this to surface "spend / cap" + to compute the raise). */
|
|
930
|
+
get maxCostUsd() {
|
|
931
|
+
return this.opts.maxCostUsd;
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* SP9-core — RAISE the USD cap by `incrementUsd` (the budget an approved "Budget cap reached — continue?"
|
|
935
|
+
* grants). Mutates the governor's ceiling so a freshly-resumed generation isn't immediately re-blocked at the
|
|
936
|
+
* SAME cap; the run gets real additional headroom. Only the cap-that-asks resume path calls this; the default
|
|
937
|
+
* terminal-stop path never does, so today's behaviour is unchanged.
|
|
938
|
+
*/
|
|
939
|
+
raiseCostCap(incrementUsd) {
|
|
940
|
+
this.opts.maxCostUsd += incrementUsd;
|
|
941
|
+
}
|
|
462
942
|
shouldStop() {
|
|
463
943
|
if (this.steps >= this.opts.maxSteps) return { stop: true, reason: "step cap reached" };
|
|
464
944
|
if (this.usd >= this.opts.maxCostUsd) return { stop: true, reason: "USD cap reached" };
|
|
@@ -466,15 +946,17 @@ var CostGovernor = class {
|
|
|
466
946
|
}
|
|
467
947
|
};
|
|
468
948
|
var DelegateBudgets = class {
|
|
469
|
-
constructor(cfg, rootSlug,
|
|
949
|
+
constructor(cfg, rootSlug, pricing) {
|
|
470
950
|
this.cfg = cfg;
|
|
471
951
|
this.rootSlug = rootSlug;
|
|
472
|
-
this.prices = { ...PRICE_TABLE, ...prices ?? {} };
|
|
952
|
+
this.prices = { ...PRICE_TABLE, ...pricing?.prices ?? {}, ...pricing?.priceFeed?.prices() ?? {} };
|
|
953
|
+
this.failOnUnknownModel = pricing?.failOnUnknownModel ?? false;
|
|
473
954
|
}
|
|
474
955
|
cfg;
|
|
475
956
|
rootSlug;
|
|
476
957
|
usd = /* @__PURE__ */ new Map();
|
|
477
958
|
prices;
|
|
959
|
+
failOnUnknownModel;
|
|
478
960
|
/** The USD cap for a delegate: its `bySlug` override if present, else the default. `undefined` → uncapped. */
|
|
479
961
|
capFor(slug) {
|
|
480
962
|
return this.cfg.bySlug?.[slug]?.maxCostUsd ?? this.cfg.maxCostUsd;
|
|
@@ -482,7 +964,10 @@ var DelegateBudgets = class {
|
|
|
482
964
|
/** Accumulate one generation's usage against a delegate. No-op for the root orchestrator (not a delegate). */
|
|
483
965
|
addUsage(slug, modelId, u) {
|
|
484
966
|
if (slug === this.rootSlug) return;
|
|
485
|
-
this.usd.set(
|
|
967
|
+
this.usd.set(
|
|
968
|
+
slug,
|
|
969
|
+
(this.usd.get(slug) ?? 0) + priceUsage(this.prices, modelId, u, { failOnUnknownModel: this.failOnUnknownModel })
|
|
970
|
+
);
|
|
486
971
|
}
|
|
487
972
|
/** The first delegate that has met or exceeded its USD cap, or null. */
|
|
488
973
|
exceeded() {
|
|
@@ -510,7 +995,7 @@ function compositeTelemetry(exporters) {
|
|
|
510
995
|
const results = await Promise.allSettled(exporters.map((e) => e.export(spans)));
|
|
511
996
|
for (const r of results) {
|
|
512
997
|
if (r.status === "rejected") {
|
|
513
|
-
console.warn("[
|
|
998
|
+
console.warn("[@nightowlsdev/core] telemetry exporter failed:", r.reason);
|
|
514
999
|
}
|
|
515
1000
|
}
|
|
516
1001
|
}
|
|
@@ -557,10 +1042,17 @@ var SpanCollector = class {
|
|
|
557
1042
|
*/
|
|
558
1043
|
closeGeneration(usage, costUsd) {
|
|
559
1044
|
if (!this.gen) return;
|
|
1045
|
+
const extra = {};
|
|
1046
|
+
if (usage.cacheReadTokens != null) extra.cacheReadTokens = usage.cacheReadTokens;
|
|
1047
|
+
if (usage.cacheWriteTokens != null) extra.cacheWriteTokens = usage.cacheWriteTokens;
|
|
1048
|
+
if (usage.reasoningTokens != null) extra.reasoningTokens = usage.reasoningTokens;
|
|
1049
|
+
if (usage.toolCalls != null) extra.toolCalls = usage.toolCalls;
|
|
1050
|
+
if (usage.agentActivations != null) extra.agentActivations = usage.agentActivations;
|
|
560
1051
|
this.gen.attributes = {
|
|
561
1052
|
...this.gen.attributes,
|
|
562
1053
|
inputTokens: usage.inputTokens,
|
|
563
1054
|
outputTokens: usage.outputTokens,
|
|
1055
|
+
...extra,
|
|
564
1056
|
costUsd: Math.max(0, costUsd)
|
|
565
1057
|
};
|
|
566
1058
|
this.gen.endedAt = this.now();
|
|
@@ -644,6 +1136,21 @@ var RowCache = class {
|
|
|
644
1136
|
|
|
645
1137
|
// src/engine.ts
|
|
646
1138
|
var AGENT_KEY = "swarm";
|
|
1139
|
+
var MAX_CONTINUE_NUDGES = 2;
|
|
1140
|
+
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.";
|
|
1141
|
+
function verifyNudge(missing) {
|
|
1142
|
+
const gap = (missing ?? "").trim();
|
|
1143
|
+
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;
|
|
1144
|
+
}
|
|
1145
|
+
var VERIFY_TRANSCRIPT_CAP = 6e3;
|
|
1146
|
+
function appendTranscript(t, e) {
|
|
1147
|
+
let add = "";
|
|
1148
|
+
if (e.type === "swarm.message") add = e.data.delta ?? e.data.text ?? "";
|
|
1149
|
+
else if (e.type === "swarm.tool_call") add = `
|
|
1150
|
+
\xAB${e.agentSlug} \u2192 ${e.data.name}\xBB
|
|
1151
|
+
`;
|
|
1152
|
+
return add ? (t + add).slice(-VERIFY_TRANSCRIPT_CAP) : t;
|
|
1153
|
+
}
|
|
647
1154
|
var SwarmEngine = class {
|
|
648
1155
|
constructor(opts) {
|
|
649
1156
|
this.opts = opts;
|
|
@@ -653,6 +1160,7 @@ var SwarmEngine = class {
|
|
|
653
1160
|
const { memory, resolveMemory } = opts.memory ? buildMemoryResolver(opts.memory) : { memory: void 0, resolveMemory: void 0 };
|
|
654
1161
|
this.memory = memory;
|
|
655
1162
|
this.floor = opts.floor ?? containerFloor;
|
|
1163
|
+
this.hooks = opts.hooks ?? new import_hooks.HookDispatcher({}, opts.toolApproval ?? { mode: "flag" });
|
|
656
1164
|
opts.storage.subscribeInvalidations?.((key) => this.rowCache.invalidate(key));
|
|
657
1165
|
const agent = buildMastraAgent({
|
|
658
1166
|
loadRow: (slug, tenantId) => this.loadRow(tenantId, slug),
|
|
@@ -660,6 +1168,8 @@ var SwarmEngine = class {
|
|
|
660
1168
|
resolveSkill: (n) => opts.resolveSkill?.(n),
|
|
661
1169
|
model: opts.model,
|
|
662
1170
|
modelFactory: opts.modelFactory,
|
|
1171
|
+
// SP10: hand the cheap-model router to the per-agent model resolver. Undefined ⇒ no routing (today).
|
|
1172
|
+
tier: opts.tier,
|
|
663
1173
|
builtinTools: {
|
|
664
1174
|
[ASK_TOOL_NAME]: buildAskMastraTool(),
|
|
665
1175
|
...opts.scratchpad ? { scratchpad_write: buildScratchpadTool(opts.storage.scratchpad, typeof opts.scratchpad === "object" ? opts.scratchpad : void 0) } : {},
|
|
@@ -669,6 +1179,9 @@ var SwarmEngine = class {
|
|
|
669
1179
|
// ONLY (never sub-agents) so the model can pull the host page's advisory RunInput.context.
|
|
670
1180
|
...opts.pageContext ? { extraTools: { get_page_context: buildPageContextTool() } } : {},
|
|
671
1181
|
loadScratchpad: opts.scratchpad ? (c, t) => opts.storage.scratchpad.list(t, c) : void 0,
|
|
1182
|
+
softPolicy: opts.softPolicy,
|
|
1183
|
+
// PR2: per-request connector-tools resolver, granted to the orchestrator + sub-agents by skillNames.
|
|
1184
|
+
connectorTools: opts.connectorTools,
|
|
672
1185
|
memory
|
|
673
1186
|
});
|
|
674
1187
|
this.mastra = new import_mastra.Mastra({
|
|
@@ -687,6 +1200,51 @@ var SwarmEngine = class {
|
|
|
687
1200
|
// Typed `unknown` to keep the engine wall: no engine-vendor type escapes the public surface.
|
|
688
1201
|
memory;
|
|
689
1202
|
floor;
|
|
1203
|
+
// SP2: the decision-hook dispatcher. Always present — defaults to an allow-all dispatcher when the engine is
|
|
1204
|
+
// built without one (e.g. unit tests), so the preGeneration seam is uniform with no per-call null checks.
|
|
1205
|
+
hooks;
|
|
1206
|
+
/** SP1: the swarm's metering config, in the shape DelegateBudgets/priceUsage expect. CostGovernor reads the
|
|
1207
|
+
* same fields directly off `opts.cost`; this packs them for the per-delegate tracker so both caps price
|
|
1208
|
+
* tokens identically (built-in PRICE_TABLE ← static `prices` ← live `priceFeed`, with `failOnUnknownModel`). */
|
|
1209
|
+
pricingOpts() {
|
|
1210
|
+
return {
|
|
1211
|
+
prices: this.opts.cost.prices,
|
|
1212
|
+
priceFeed: this.opts.cost.priceFeed,
|
|
1213
|
+
failOnUnknownModel: this.opts.cost.failOnUnknownModel
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
/** Fire the best-effort per-event observer (`EngineOpts.onEvent`). Awaited so an async observer (e.g. a
|
|
1217
|
+
* metering debit) completes in order, but FAIL-SAFE: a throw is swallowed (the host logs its own), never
|
|
1218
|
+
* breaking the run — same contract as the telemetry exporter. No-op when no observer is configured. */
|
|
1219
|
+
async notifyEvent(e, ctx) {
|
|
1220
|
+
if (!this.opts.onEvent) return;
|
|
1221
|
+
try {
|
|
1222
|
+
await this.opts.onEvent(e, ctx);
|
|
1223
|
+
} catch {
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
/** Run the completion supervisor (`EngineOpts.verifyCompletion`), FAIL-OPEN: no verifier, or a throwing one,
|
|
1227
|
+
* yields `{ complete: true }` so a missing/broken judge never traps a run in a verify loop. */
|
|
1228
|
+
async safeVerify(request, transcript, ctx) {
|
|
1229
|
+
if (!this.opts.verifyCompletion) return { complete: true };
|
|
1230
|
+
try {
|
|
1231
|
+
return await this.opts.verifyCompletion({ request, transcript, ctx });
|
|
1232
|
+
} catch (err) {
|
|
1233
|
+
console.error(`[@nightowlsdev/core] verifyCompletion threw for run ${ctx.runId} \u2014 treating as complete:`, err);
|
|
1234
|
+
return { complete: true };
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
/** Best-effort recall of the run's ORIGINAL request (first user message on the thread) for the completion
|
|
1238
|
+
* verifier on RESUME, where the engine doesn't hold the opening message. Empty on any failure / no verifier. */
|
|
1239
|
+
async recallRequest(ctx) {
|
|
1240
|
+
if (!this.opts.verifyCompletion) return "";
|
|
1241
|
+
try {
|
|
1242
|
+
const msgs = await this.history(ctx.threadId, ctx, { limit: 50 });
|
|
1243
|
+
return msgs.find((m) => m.role === "user")?.text ?? "";
|
|
1244
|
+
} catch {
|
|
1245
|
+
return "";
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
690
1248
|
/** Cached agent-row load shared by the three dynamic agent fns AND run/resume. */
|
|
691
1249
|
loadRow(tenantId, slug) {
|
|
692
1250
|
return this.rowCache.get(`${tenantId}:${slug}`, async () => {
|
|
@@ -695,16 +1253,60 @@ var SwarmEngine = class {
|
|
|
695
1253
|
return row;
|
|
696
1254
|
});
|
|
697
1255
|
}
|
|
1256
|
+
/** Resolve an agent's STORED modelId — which may be a tier sentinel (`"tier:"` / `"tier:swift"`) — to the
|
|
1257
|
+
* CONCRETE model id the generation actually runs on, so metering/pricing + the preGeneration event see the
|
|
1258
|
+
* real model, not the sentinel (which has no price → every tier-routed turn would meter at $0). Mirrors
|
|
1259
|
+
* mastra-map's modelFor routing; with no tier config it returns the id unchanged. (SP10 pricing follow-up.) */
|
|
1260
|
+
priceModelId(rawModelId, tenantId, agentSlug) {
|
|
1261
|
+
return tierModelId(rawModelId, this.opts.tier, { tenantId, agentSlug, pinnedModelId: rawModelId });
|
|
1262
|
+
}
|
|
698
1263
|
agent() {
|
|
699
1264
|
return this.mastra.getAgent(AGENT_KEY);
|
|
700
1265
|
}
|
|
701
|
-
requestContext(ctx) {
|
|
1266
|
+
requestContext(ctx, state) {
|
|
702
1267
|
const rc = new import_request_context.RequestContext();
|
|
703
1268
|
for (const [k, v] of Object.entries(ctx)) {
|
|
704
1269
|
if (v !== void 0) rc.set(k, v);
|
|
705
1270
|
}
|
|
1271
|
+
rc.set(TOOL_GATE_KEY, this.toolGate);
|
|
1272
|
+
if (this.opts.secrets) rc.set(SECRET_RESOLVER_KEY, this.opts.secrets);
|
|
1273
|
+
if (state) rc.set(RUN_STATE_KEY, state);
|
|
706
1274
|
return rc;
|
|
707
1275
|
}
|
|
1276
|
+
/**
|
|
1277
|
+
* SP5 — the action-approval gate handed to every gated tool via the RequestContext. Bound once (stable
|
|
1278
|
+
* reference). Delegates to the dispatcher's `preToolCall`, which is fail-closed (a throwing configured hook ⇒
|
|
1279
|
+
* deny) and applies the non-removable policy. The defineTool wrapper turns the returned `ToolDecision` into:
|
|
1280
|
+
* allow → run; deny → blocked result; ask → suspend-and-ask (the existing `swarm.question`/resume machinery).
|
|
1281
|
+
*/
|
|
1282
|
+
toolGate = (ev2) => this.hooks.preToolCall(ev2);
|
|
1283
|
+
/**
|
|
1284
|
+
* SP5 truth-fix — resolve whether a tool WILL require approval, for the `swarm.tool_call` event's
|
|
1285
|
+
* `needsApproval` (the react reducer reads it to render an approval card). The mapChunk emit currently
|
|
1286
|
+
* hardcodes `false` (the truth-bug). This computes the truthful value from the SAME policy + per-tool flag the
|
|
1287
|
+
* gate uses: the tool's resolved `needsApproval` (its own flag, defaulting by origin) run through the
|
|
1288
|
+
* dispatcher's SYNC `policyDecision` — `ask` ⇒ true (it will gate), else false. The async `preToolCall` hook
|
|
1289
|
+
* can still escalate a specific call at execute time, but the policy-derived baseline is the truthful default
|
|
1290
|
+
* the UI needs without speculatively running the hook for every tool_call event.
|
|
1291
|
+
*/
|
|
1292
|
+
gatesApproval(toolName) {
|
|
1293
|
+
const skill = this.opts.resolveSkill?.(toolName);
|
|
1294
|
+
const origin = skill?.origin ?? "first-party";
|
|
1295
|
+
const needsApproval = skill?.needsApproval ?? origin === "mcp";
|
|
1296
|
+
const decision = this.hooks.policyDecision({ runId: "", agentSlug: "", toolName, origin, needsApproval });
|
|
1297
|
+
return decision.action === "ask";
|
|
1298
|
+
}
|
|
1299
|
+
/**
|
|
1300
|
+
* SP2: the preGeneration DECISION seam. Awaited immediately before each model launch (run + resume). The
|
|
1301
|
+
* dispatcher is fail-closed (a throwing hook ⇒ deny), so this only ever sees a clean `allow`/`deny`; a `deny`
|
|
1302
|
+
* THROWS `ReserveDenied` so the model call below never happens and the run/resume catch-all maps it to a
|
|
1303
|
+
* terminal `run_failed` stage "reserve" (NOT the generic "exception"). Allow-all + zero-overhead when no
|
|
1304
|
+
* hooks are configured (the default dispatcher returns allow synchronously-ish without invoking anything).
|
|
1305
|
+
*/
|
|
1306
|
+
async guardGeneration(ev2) {
|
|
1307
|
+
const decision = await this.hooks.preGeneration(ev2);
|
|
1308
|
+
if (decision.action === "deny") throw new ReserveDenied(decision.reason);
|
|
1309
|
+
}
|
|
708
1310
|
/** Per-call Mastra memory ids + delegation, only when memory is configured (else stream is unchanged). */
|
|
709
1311
|
memoryOpts(ctx) {
|
|
710
1312
|
if (!this.opts.memory) return {};
|
|
@@ -914,6 +1516,13 @@ var SwarmEngine = class {
|
|
|
914
1516
|
async activeRuns(container, ctx) {
|
|
915
1517
|
return this.opts.storage.runs.listActive(ctx.tenantId, container);
|
|
916
1518
|
}
|
|
1519
|
+
/** The full, globally-ordered event log for a thread's CONTAINER (all its runs + lane sub-threads) — lets a host
|
|
1520
|
+
* rebuild the RICH timeline (tool calls + delegation cards) on reload, since message history is text-only.
|
|
1521
|
+
* Returns [] when the store has no events table (`listForContainer` unset). */
|
|
1522
|
+
async threadEvents(threadId, ctx) {
|
|
1523
|
+
const container = threadId.split(":")[0] || threadId;
|
|
1524
|
+
return await this.opts.storage.events.listForContainer?.(ctx.tenantId, container) ?? [];
|
|
1525
|
+
}
|
|
917
1526
|
/** The tenant's agent roster (slug, title-cased display name, role, delegate graph) as wall-safe
|
|
918
1527
|
* AgentSummary[]. Sourced from the agent rows; no vendor type in the signature or result. Powers
|
|
919
1528
|
* the multi-agent pile / @mention UI. */
|
|
@@ -928,7 +1537,12 @@ var SwarmEngine = class {
|
|
|
928
1537
|
}));
|
|
929
1538
|
}
|
|
930
1539
|
async *run(input, ctx) {
|
|
931
|
-
const modelId = (await this.loadRow(ctx.tenantId, ctx.agentSlug)).modelId ?? "unknown";
|
|
1540
|
+
const modelId = this.priceModelId((await this.loadRow(ctx.tenantId, ctx.agentSlug)).modelId ?? "unknown", ctx.tenantId, ctx.agentSlug);
|
|
1541
|
+
const workflowDef = input.workflow ? this.opts.workflows?.find((w) => w.name === input.workflow) : this.opts.agentWorkflows?.[ctx.agentSlug];
|
|
1542
|
+
if (input.workflow && !workflowDef) throw new Error(`unknown workflow: ${input.workflow}`);
|
|
1543
|
+
if (this.opts.storage.threads) {
|
|
1544
|
+
await this.opts.storage.threads.ensure({ id: ctx.threadId, orgId: ctx.tenantId, userId: ctx.userId });
|
|
1545
|
+
}
|
|
932
1546
|
await this.opts.storage.runs.create({
|
|
933
1547
|
runId: ctx.runId,
|
|
934
1548
|
tenantId: ctx.tenantId,
|
|
@@ -936,18 +1550,40 @@ var SwarmEngine = class {
|
|
|
936
1550
|
threadId: ctx.threadId,
|
|
937
1551
|
agentSlug: ctx.agentSlug
|
|
938
1552
|
});
|
|
939
|
-
const modelIdFor = (slug) =>
|
|
1553
|
+
const modelIdFor = (slug) => {
|
|
1554
|
+
const raw = this.rowCache.peek(`${ctx.tenantId}:${slug}`)?.modelId;
|
|
1555
|
+
return raw ? this.priceModelId(raw, ctx.tenantId, slug) : modelId;
|
|
1556
|
+
};
|
|
1557
|
+
const gatesApproval = (name) => this.gatesApproval(name);
|
|
940
1558
|
const gov = new CostGovernor(this.opts.cost);
|
|
941
|
-
const delegateBudgets = this.opts.cost.perDelegate ? new DelegateBudgets(this.opts.cost.perDelegate, ctx.agentSlug) : null;
|
|
1559
|
+
const delegateBudgets = this.opts.cost.perDelegate ? new DelegateBudgets(this.opts.cost.perDelegate, ctx.agentSlug, this.pricingOpts()) : null;
|
|
942
1560
|
const streamed = /* @__PURE__ */ new Set();
|
|
943
|
-
const
|
|
1561
|
+
const activity = /* @__PURE__ */ new Map();
|
|
1562
|
+
const turnUsage = [];
|
|
1563
|
+
const runState = createRunState();
|
|
1564
|
+
const rc = this.requestContext(ctx, runState);
|
|
944
1565
|
if (this.opts.pageContext) attachPageContext(rc, input.context);
|
|
1566
|
+
let outcome = "failed";
|
|
1567
|
+
if (this.opts.onRunStart) {
|
|
1568
|
+
try {
|
|
1569
|
+
await this.opts.onRunStart(ctx, { input, state: runState });
|
|
1570
|
+
} catch (err) {
|
|
1571
|
+
console.error(`[@nightowlsdev/core] onRunStart threw for run ${ctx.runId}:`, err);
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
945
1574
|
const collector = this.opts.telemetry ? new SpanCollector(ctx.runId, () => Date.now(), "run", { agentSlug: ctx.agentSlug }) : null;
|
|
946
1575
|
let ts = 0;
|
|
947
1576
|
const emit = async (e) => {
|
|
948
1577
|
e.seq = await this.opts.storage.events.append(e);
|
|
1578
|
+
await this.notifyEvent(e, ctx);
|
|
949
1579
|
return e;
|
|
950
1580
|
};
|
|
1581
|
+
let turnEmitted = false;
|
|
1582
|
+
const emitTurn = async () => {
|
|
1583
|
+
if (turnEmitted) return null;
|
|
1584
|
+
turnEmitted = true;
|
|
1585
|
+
return emit(turnUsageEvent(ctx, ts++, turnUsage, 0));
|
|
1586
|
+
};
|
|
951
1587
|
const floorKey = ctx.threadId;
|
|
952
1588
|
const me = { label: titleCase(ctx.agentSlug), runId: ctx.runId };
|
|
953
1589
|
const floorAbort = new AbortController();
|
|
@@ -961,96 +1597,298 @@ var SwarmEngine = class {
|
|
|
961
1597
|
if (floorAbort.signal.aborted) return;
|
|
962
1598
|
}
|
|
963
1599
|
yield await emit(ev("swarm.status", base(ctx, ts++), { state: "thinking" }));
|
|
1600
|
+
if (workflowDef) {
|
|
1601
|
+
const wfMessage = input.message.replace(/^(\[user:[^\]]*\]\s*)+/, "");
|
|
1602
|
+
await emit(ev("swarm.message", base(ctx, ts++), { role: "user", text: wfMessage }));
|
|
1603
|
+
outcome = yield* this.driveWorkflow(workflowDef, initialWorkflowState(workflowDef), ctx, { message: wfMessage, context: input.context }, {
|
|
1604
|
+
gov,
|
|
1605
|
+
modelIdFor,
|
|
1606
|
+
streamed,
|
|
1607
|
+
delegateBudgets,
|
|
1608
|
+
activity,
|
|
1609
|
+
gatesApproval,
|
|
1610
|
+
turnUsage,
|
|
1611
|
+
nextTs: () => ts++,
|
|
1612
|
+
emit,
|
|
1613
|
+
emitTurn,
|
|
1614
|
+
segmentIndex: 0,
|
|
1615
|
+
// FR-004: a run segment starts at generation 0
|
|
1616
|
+
state: runState
|
|
1617
|
+
// FR-003: workflow steps' tools see the run's ctx.state
|
|
1618
|
+
});
|
|
1619
|
+
return;
|
|
1620
|
+
}
|
|
1621
|
+
const generationIndex = 0;
|
|
1622
|
+
await this.guardGeneration({ runId: ctx.runId, tenantId: ctx.tenantId, agentSlug: ctx.agentSlug, modelId, generationIndex, kind: "run" });
|
|
964
1623
|
const userMessage = input.message.replace(/^(\[user:[^\]]*\]\s*)+/, "");
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
})
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1624
|
+
await emit(ev("swarm.message", base(ctx, ts++), { role: "user", text: userMessage }));
|
|
1625
|
+
let turnMessage = userMessage;
|
|
1626
|
+
let continueNudges = 0;
|
|
1627
|
+
let transcript = "";
|
|
1628
|
+
let incompleteVerdict = null;
|
|
1629
|
+
for (; ; ) {
|
|
1630
|
+
const result = await this.agent().stream(turnMessage, { runId: ctx.runId, requestContext: rc, ...this.memoryOpts(ctx) });
|
|
1631
|
+
let sawStep = false;
|
|
1632
|
+
let lastOutputSlug;
|
|
1633
|
+
for await (const part of result.fullStream) {
|
|
1634
|
+
if (part?.type === "step-finish") {
|
|
1635
|
+
gov.step();
|
|
1636
|
+
sawStep = true;
|
|
1637
|
+
}
|
|
1638
|
+
if (part?.type === "tool-call-suspended") {
|
|
1639
|
+
const payload = part.payload ?? {};
|
|
1640
|
+
const toolCallId = payload.toolCallId ?? "";
|
|
1641
|
+
const followupId = `${ctx.runId}:${toolCallId}`;
|
|
1642
|
+
const sp = payload.suspendPayload ?? {};
|
|
1643
|
+
await recordSuspend(this.opts.storage, ctx, followupId, toolCallId);
|
|
1644
|
+
await this.opts.storage.runs.setStatus(ctx.runId, "suspended");
|
|
1645
|
+
await this.opts.storage.runs.saveSnapshot(ctx.runId, { pending: { toolCallId }, genIndex: generationIndex + 1, state: runState.entries() });
|
|
1646
|
+
{
|
|
1647
|
+
const t = await emitTurn();
|
|
1648
|
+
if (t) yield t;
|
|
1649
|
+
}
|
|
1650
|
+
yield await emit(ev("swarm.status", base(ctx, ts++), { state: "waiting" }));
|
|
1651
|
+
yield await emit(clientActionOrQuestion(ctx, ts++, followupId, toolCallId, sp));
|
|
1652
|
+
outcome = "suspended";
|
|
1653
|
+
return;
|
|
1654
|
+
}
|
|
1655
|
+
if (part?.type === "error") {
|
|
1656
|
+
await this.opts.storage.runs.setStatus(ctx.runId, "failed");
|
|
1657
|
+
{
|
|
1658
|
+
const t = await emitTurn();
|
|
1659
|
+
if (t) yield t;
|
|
1660
|
+
}
|
|
1661
|
+
yield await emit(
|
|
1662
|
+
ev("swarm.run_failed", base(ctx, ts++), {
|
|
1663
|
+
stage: "stream",
|
|
1664
|
+
message: streamErrorMessage(part),
|
|
1665
|
+
retryable: false
|
|
1666
|
+
})
|
|
1667
|
+
);
|
|
1668
|
+
return;
|
|
1669
|
+
}
|
|
1670
|
+
for (const e of mapChunk(part, ctx, gov, modelIdFor, () => ts++, streamed, delegateBudgets, activity, gatesApproval, turnUsage, 0)) {
|
|
1671
|
+
if (e.type === "swarm.message" || e.type === "swarm.tool_call") {
|
|
1672
|
+
lastOutputSlug = e.agentSlug;
|
|
1673
|
+
if (this.opts.verifyCompletion) transcript = appendTranscript(transcript, e);
|
|
1674
|
+
}
|
|
1675
|
+
yield await emit(e);
|
|
1676
|
+
}
|
|
1677
|
+
collectSpans(collector, part, modelId, gov);
|
|
1678
|
+
const overDelegate = delegateBudgets?.exceeded();
|
|
1679
|
+
const stop = gov.shouldStop();
|
|
1680
|
+
if (stop.stop || overDelegate) {
|
|
1681
|
+
if (this.opts.cost.onCapHit === "ask" && stop.stop && !overDelegate) {
|
|
1682
|
+
const followupId = `${ctx.runId}:${CAP_FOLLOWUP_SUFFIX}`;
|
|
1683
|
+
await recordSuspend(this.opts.storage, ctx, followupId, CAP_FOLLOWUP_SUFFIX);
|
|
1684
|
+
await this.opts.storage.runs.setStatus(ctx.runId, "suspended");
|
|
1685
|
+
await this.opts.storage.runs.saveSnapshot(ctx.runId, {
|
|
1686
|
+
capHit: { message: userMessage, spentUsd: gov.costUsd() },
|
|
1687
|
+
genIndex: generationIndex + 1,
|
|
1688
|
+
state: runState.entries()
|
|
1689
|
+
// FR-003: persist per-run state across the cap-ask boundary
|
|
1690
|
+
});
|
|
1691
|
+
{
|
|
1692
|
+
const t = await emitTurn();
|
|
1693
|
+
if (t) yield t;
|
|
1694
|
+
}
|
|
1695
|
+
yield await emit(ev("swarm.status", base(ctx, ts++), { state: "waiting" }));
|
|
1696
|
+
yield await emit(ev("swarm.question", base(ctx, ts++), capQuestion(ctx, followupId, gov)));
|
|
1697
|
+
outcome = "suspended";
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1700
|
+
await this.opts.storage.runs.setStatus(ctx.runId, "failed");
|
|
1701
|
+
{
|
|
1702
|
+
const t = await emitTurn();
|
|
1703
|
+
if (t) yield t;
|
|
1704
|
+
}
|
|
1705
|
+
yield await emit(
|
|
1706
|
+
ev("swarm.run_failed", base(ctx, ts++), {
|
|
1707
|
+
stage: "cost",
|
|
1708
|
+
message: overDelegate?.reason ?? stop.reason,
|
|
1709
|
+
retryable: false
|
|
1710
|
+
})
|
|
1711
|
+
);
|
|
1712
|
+
return;
|
|
1713
|
+
}
|
|
1000
1714
|
}
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
return;
|
|
1715
|
+
if (this.opts.verifyCompletion) {
|
|
1716
|
+
const verdict = await this.safeVerify(userMessage, transcript, ctx);
|
|
1717
|
+
if (!verdict.complete && continueNudges < MAX_CONTINUE_NUDGES) {
|
|
1718
|
+
continueNudges++;
|
|
1719
|
+
turnMessage = verifyNudge(verdict.missing);
|
|
1720
|
+
continue;
|
|
1721
|
+
}
|
|
1722
|
+
incompleteVerdict = verdict.complete ? null : verdict;
|
|
1723
|
+
} else if (sawStep && lastOutputSlug !== ctx.agentSlug && continueNudges < MAX_CONTINUE_NUDGES) {
|
|
1724
|
+
continueNudges++;
|
|
1725
|
+
turnMessage = CONTINUE_NUDGE;
|
|
1726
|
+
continue;
|
|
1014
1727
|
}
|
|
1728
|
+
break;
|
|
1015
1729
|
}
|
|
1016
1730
|
await this.mirrorDelegations(ctx);
|
|
1017
1731
|
await this.attributeRun(ctx);
|
|
1018
|
-
|
|
1019
|
-
|
|
1732
|
+
if (incompleteVerdict) {
|
|
1733
|
+
await this.opts.storage.runs.setStatus(ctx.runId, "failed");
|
|
1734
|
+
{
|
|
1735
|
+
const t = await emitTurn();
|
|
1736
|
+
if (t) yield t;
|
|
1737
|
+
}
|
|
1738
|
+
yield await emit(ev("swarm.run_failed", base(ctx, ts++), { stage: "incomplete", message: incompleteVerdict.missing ?? "The task was not completed.", retryable: true }));
|
|
1739
|
+
} else {
|
|
1740
|
+
await this.opts.storage.runs.setStatus(ctx.runId, "done");
|
|
1741
|
+
{
|
|
1742
|
+
const t = await emitTurn();
|
|
1743
|
+
if (t) yield t;
|
|
1744
|
+
}
|
|
1745
|
+
yield await emit(ev("swarm.status", base(ctx, ts++), { state: "done" }));
|
|
1746
|
+
outcome = "done";
|
|
1747
|
+
}
|
|
1020
1748
|
} catch (err) {
|
|
1021
|
-
|
|
1749
|
+
const stage = err instanceof ReserveDenied ? "reserve" : "exception";
|
|
1750
|
+
if (stage !== "reserve") console.error(`[@nightowlsdev/core] run ${ctx.runId} threw:`, err);
|
|
1022
1751
|
try {
|
|
1023
1752
|
await this.opts.storage.runs.setStatus(ctx.runId, "failed");
|
|
1024
1753
|
} catch {
|
|
1025
1754
|
}
|
|
1026
|
-
|
|
1755
|
+
{
|
|
1756
|
+
const t = await emitTurn();
|
|
1757
|
+
if (t) yield t;
|
|
1758
|
+
}
|
|
1759
|
+
yield await emit(ev("swarm.run_failed", base(ctx, ts++), { stage, message: errMessage(err), retryable: false }));
|
|
1027
1760
|
} finally {
|
|
1761
|
+
if (this.opts.onRunEnd) {
|
|
1762
|
+
try {
|
|
1763
|
+
await this.opts.onRunEnd(ctx, { state: runState, outcome });
|
|
1764
|
+
} catch (err) {
|
|
1765
|
+
console.error(`[@nightowlsdev/core] onRunEnd threw for run ${ctx.runId}:`, err);
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1028
1768
|
floorAbort.abort();
|
|
1029
1769
|
await releaseFloor?.();
|
|
1030
1770
|
await exportSpans(this.opts.telemetry, collector);
|
|
1031
1771
|
}
|
|
1032
1772
|
}
|
|
1773
|
+
/**
|
|
1774
|
+
* Phase B — drive a STRICT workflow IN PLACE OF the free-form continue-nudge loop. Shared by `run()` (fresh)
|
|
1775
|
+
* and `resume()` (re-entry after a human/approval suspend). An `agent` step reuses `this.agent().stream()`
|
|
1776
|
+
* with a per-step requestContext (agentSlug = the step's agent) so it inherits persona/tools/gate/model/cost;
|
|
1777
|
+
* a `tool` step runs `executeToolWithGate`; a `human`/approval pause suspends SP9-style. Reserve, usage, and
|
|
1778
|
+
* the terminal turn_usage flow through the caller's machinery (`m`). Handles the terminal status/setStatus.
|
|
1779
|
+
*/
|
|
1780
|
+
async *driveWorkflow(wf, state, ctx, input, m) {
|
|
1781
|
+
const driver = new StepDriver(wf, {
|
|
1782
|
+
nextTs: m.nextTs,
|
|
1783
|
+
runAgentStep: (slug, message, genIndex) => this.streamWorkflowAgentStep(slug, message, genIndex, ctx, input, m),
|
|
1784
|
+
runToolStep: (toolName, args) => this.runWorkflowToolStep(toolName, args, ctx, m.state),
|
|
1785
|
+
saveState: (rid, st) => this.opts.storage.runs.saveSnapshot(rid, { workflow: st })
|
|
1786
|
+
});
|
|
1787
|
+
const it = driver.drive(state, ctx, input);
|
|
1788
|
+
let r = await it.next();
|
|
1789
|
+
while (!r.done) {
|
|
1790
|
+
yield await m.emit(r.value);
|
|
1791
|
+
r = await it.next();
|
|
1792
|
+
}
|
|
1793
|
+
const outcome = r.value;
|
|
1794
|
+
if (outcome.kind === "suspended") {
|
|
1795
|
+
const p = outcome.state.pending;
|
|
1796
|
+
await recordSuspend(this.opts.storage, ctx, p.followupId, p.toolCallId);
|
|
1797
|
+
await this.opts.storage.runs.setStatus(ctx.runId, "suspended");
|
|
1798
|
+
yield await m.emit(ev("swarm.status", base(ctx, m.nextTs()), { state: "waiting" }));
|
|
1799
|
+
{
|
|
1800
|
+
const t = await m.emitTurn();
|
|
1801
|
+
if (t) yield t;
|
|
1802
|
+
}
|
|
1803
|
+
return "suspended";
|
|
1804
|
+
}
|
|
1805
|
+
if (outcome.kind === "failed") {
|
|
1806
|
+
await this.opts.storage.runs.setStatus(ctx.runId, "failed");
|
|
1807
|
+
{
|
|
1808
|
+
const t = await m.emitTurn();
|
|
1809
|
+
if (t) yield t;
|
|
1810
|
+
}
|
|
1811
|
+
yield await m.emit(ev("swarm.run_failed", base(ctx, m.nextTs()), { stage: outcome.stage, message: outcome.message, retryable: true }));
|
|
1812
|
+
return "failed";
|
|
1813
|
+
}
|
|
1814
|
+
await this.mirrorDelegations(ctx);
|
|
1815
|
+
await this.attributeRun(ctx);
|
|
1816
|
+
await this.opts.storage.runs.setStatus(ctx.runId, "done");
|
|
1817
|
+
{
|
|
1818
|
+
const t = await m.emitTurn();
|
|
1819
|
+
if (t) yield t;
|
|
1820
|
+
}
|
|
1821
|
+
yield await m.emit(ev("swarm.status", base(ctx, m.nextTs()), { state: "done" }));
|
|
1822
|
+
return "done";
|
|
1823
|
+
}
|
|
1824
|
+
/** A workflow `agent` step: stream `slug` with `message` (a per-step requestContext so it inherits the agent's
|
|
1825
|
+
* persona/tools/gate/model), reserving + metering through the caller's machinery, returning the final text. */
|
|
1826
|
+
async *streamWorkflowAgentStep(slug, message, genIndex, ctx, input, m) {
|
|
1827
|
+
await this.guardGeneration({ runId: ctx.runId, tenantId: ctx.tenantId, agentSlug: slug, modelId: m.modelIdFor(slug), generationIndex: genIndex, kind: "run" });
|
|
1828
|
+
const sctx = { ...ctx, agentSlug: slug };
|
|
1829
|
+
const stepRc = this.requestContext(sctx, m.state);
|
|
1830
|
+
if (this.opts.pageContext) attachPageContext(stepRc, input.context);
|
|
1831
|
+
const result = await this.agent().stream(message, { runId: ctx.runId, requestContext: stepRc, ...this.memoryOpts(sctx) });
|
|
1832
|
+
let text = "";
|
|
1833
|
+
for await (const part of result.fullStream) {
|
|
1834
|
+
if (part?.type === "step-finish") m.gov.step();
|
|
1835
|
+
for (const e of mapChunk(part, sctx, m.gov, m.modelIdFor, m.nextTs, m.streamed, m.delegateBudgets, m.activity, m.gatesApproval, m.turnUsage, m.segmentIndex)) {
|
|
1836
|
+
if (e.type === "swarm.message") text += e.data.delta ?? e.data.text ?? "";
|
|
1837
|
+
yield e;
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
return { text };
|
|
1841
|
+
}
|
|
1842
|
+
/** A workflow `tool` step: run the gate-free tool body through `executeToolWithGate` (the engine-owned gate). */
|
|
1843
|
+
async runWorkflowToolStep(toolName, args, ctx, state) {
|
|
1844
|
+
const skill = this.opts.resolveSkill?.(toolName);
|
|
1845
|
+
const exec = skill ? getToolExecutor(skill) : void 0;
|
|
1846
|
+
if (!skill || !exec) return { ok: false, error: `unknown tool "${toolName}"` };
|
|
1847
|
+
const toolCtx = { tenantId: ctx.tenantId, userId: ctx.userId, runId: ctx.runId, secrets: bindSecrets(this.opts.secrets, ctx), state };
|
|
1848
|
+
return executeToolWithGate({
|
|
1849
|
+
ev: toolPreCallEvent({ runId: ctx.runId, tenantId: ctx.tenantId, agentSlug: ctx.agentSlug, toolName, origin: skill.origin ?? "first-party", needsApproval: this.gatesApproval(toolName), args }),
|
|
1850
|
+
gate: this.toolGate,
|
|
1851
|
+
run: () => exec(args, toolCtx)
|
|
1852
|
+
});
|
|
1853
|
+
}
|
|
1033
1854
|
async *resume(args, ctx) {
|
|
1034
1855
|
const snap = await this.opts.storage.runs.loadSnapshot(ctx.tenantId, args.runId);
|
|
1035
1856
|
if (!snap) throw new Error(`no suspended run: ${args.runId}`);
|
|
1857
|
+
const resumedState = createRunState(snap.state ?? void 0);
|
|
1858
|
+
const generationIndex = typeof snap.genIndex === "number" ? snap.genIndex : 1;
|
|
1859
|
+
const capHit = snap.capHit;
|
|
1036
1860
|
await this.opts.storage.markFollowupAnswered?.(args.followupId, ctx.tenantId);
|
|
1037
1861
|
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) =>
|
|
1862
|
+
const modelId = this.priceModelId((await this.loadRow(ctx.tenantId, ctx.agentSlug)).modelId ?? "unknown", ctx.tenantId, ctx.agentSlug);
|
|
1863
|
+
const modelIdFor = (slug) => {
|
|
1864
|
+
const raw = this.rowCache.peek(`${ctx.tenantId}:${slug}`)?.modelId;
|
|
1865
|
+
return raw ? this.priceModelId(raw, ctx.tenantId, slug) : modelId;
|
|
1866
|
+
};
|
|
1867
|
+
const gatesApproval = (name) => this.gatesApproval(name);
|
|
1040
1868
|
const gov = new CostGovernor(this.opts.cost);
|
|
1041
|
-
const delegateBudgets = this.opts.cost.perDelegate ? new DelegateBudgets(this.opts.cost.perDelegate, ctx.agentSlug) : null;
|
|
1869
|
+
const delegateBudgets = this.opts.cost.perDelegate ? new DelegateBudgets(this.opts.cost.perDelegate, ctx.agentSlug, this.pricingOpts()) : null;
|
|
1042
1870
|
const streamed = /* @__PURE__ */ new Set();
|
|
1871
|
+
const activity = /* @__PURE__ */ new Map();
|
|
1872
|
+
const turnUsage = [];
|
|
1043
1873
|
const collector = this.opts.telemetry ? new SpanCollector(args.runId, () => Date.now(), "resume", { agentSlug: ctx.agentSlug }) : null;
|
|
1044
1874
|
let ts = 1e3;
|
|
1045
1875
|
const emit = async (e) => {
|
|
1046
1876
|
e.seq = await this.opts.storage.events.append(e);
|
|
1877
|
+
await this.notifyEvent(e, ctx);
|
|
1047
1878
|
return e;
|
|
1048
1879
|
};
|
|
1880
|
+
let turnEmitted = false;
|
|
1049
1881
|
const floorKey = ctx.threadId;
|
|
1050
1882
|
const me = { label: titleCase(ctx.agentSlug), runId: args.runId };
|
|
1051
1883
|
const floorAbort = new AbortController();
|
|
1052
1884
|
let releaseFloor = await this.floor.tryAcquire(floorKey, me);
|
|
1053
1885
|
const rctx = { ...ctx, runId: args.runId };
|
|
1886
|
+
const emitTurn = async () => {
|
|
1887
|
+
if (turnEmitted) return null;
|
|
1888
|
+
turnEmitted = true;
|
|
1889
|
+
return emit(turnUsageEvent(rctx, ts++, turnUsage, generationIndex));
|
|
1890
|
+
};
|
|
1891
|
+
let resumeOutcome = "failed";
|
|
1054
1892
|
try {
|
|
1055
1893
|
if (!releaseFloor) {
|
|
1056
1894
|
const held = await this.floor.holder(floorKey);
|
|
@@ -1066,84 +1904,285 @@ var SwarmEngine = class {
|
|
|
1066
1904
|
answer: args.answer
|
|
1067
1905
|
})
|
|
1068
1906
|
);
|
|
1069
|
-
const
|
|
1070
|
-
if (
|
|
1071
|
-
|
|
1072
|
-
|
|
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") {
|
|
1907
|
+
const wfState = snap.workflow;
|
|
1908
|
+
if (wfState) {
|
|
1909
|
+
const wf = this.opts.workflows?.find((w) => w.name === wfState.workflow) ?? this.opts.agentWorkflows?.[ctx.agentSlug];
|
|
1910
|
+
if (!wf) {
|
|
1099
1911
|
await this.opts.storage.runs.setStatus(args.runId, "failed");
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
})
|
|
1106
|
-
);
|
|
1912
|
+
{
|
|
1913
|
+
const t = await emitTurn();
|
|
1914
|
+
if (t) yield t;
|
|
1915
|
+
}
|
|
1916
|
+
yield await emit(ev("swarm.run_failed", base(rctx, ts++), { stage: "workflow", message: `unknown workflow: ${wfState.workflow}`, retryable: false }));
|
|
1107
1917
|
return;
|
|
1108
1918
|
}
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1919
|
+
if (wfState.pending) {
|
|
1920
|
+
wfState.outputs[wfState.pending.stepId] = args.answer;
|
|
1921
|
+
wfState.pending = void 0;
|
|
1922
|
+
}
|
|
1923
|
+
resumeOutcome = yield* this.driveWorkflow(wf, wfState, rctx, { message: "", context: args.context }, {
|
|
1924
|
+
gov,
|
|
1925
|
+
modelIdFor,
|
|
1926
|
+
streamed,
|
|
1927
|
+
delegateBudgets,
|
|
1928
|
+
activity,
|
|
1929
|
+
gatesApproval,
|
|
1930
|
+
turnUsage,
|
|
1931
|
+
nextTs: () => ts++,
|
|
1932
|
+
emit,
|
|
1933
|
+
emitTurn,
|
|
1934
|
+
segmentIndex: generationIndex,
|
|
1935
|
+
// FR-004: a resume segment starts at the snapshot's genIndex
|
|
1936
|
+
state: resumedState
|
|
1937
|
+
// FR-003: workflow steps' tools see the restored ctx.state
|
|
1938
|
+
});
|
|
1939
|
+
return;
|
|
1940
|
+
}
|
|
1941
|
+
if (capHit && !isApproved(args.answer)) {
|
|
1942
|
+
await this.opts.storage.runs.setStatus(args.runId, "failed");
|
|
1943
|
+
{
|
|
1944
|
+
const t = await emitTurn();
|
|
1945
|
+
if (t) yield t;
|
|
1122
1946
|
}
|
|
1947
|
+
yield await emit(
|
|
1948
|
+
ev("swarm.run_failed", base(rctx, ts++), {
|
|
1949
|
+
stage: "cost",
|
|
1950
|
+
message: "budget cap reached \u2014 continuation declined by the user",
|
|
1951
|
+
retryable: false
|
|
1952
|
+
})
|
|
1953
|
+
);
|
|
1954
|
+
return;
|
|
1955
|
+
}
|
|
1956
|
+
const rc = this.requestContext({ ...ctx, runId: args.runId }, resumedState);
|
|
1957
|
+
if (this.opts.pageContext) attachPageContext(rc, args.context);
|
|
1958
|
+
await this.guardGeneration({ runId: args.runId, tenantId: ctx.tenantId, agentSlug: ctx.agentSlug, modelId, generationIndex, kind: "resume" });
|
|
1959
|
+
let resumeNudges = 0;
|
|
1960
|
+
let firstPass = true;
|
|
1961
|
+
const request = await this.recallRequest(rctx);
|
|
1962
|
+
let nudgeMessage = CONTINUE_NUDGE;
|
|
1963
|
+
let transcript = "";
|
|
1964
|
+
let incompleteVerdict = null;
|
|
1965
|
+
for (; ; ) {
|
|
1966
|
+
const result = firstPass ? capHit ? await (async () => {
|
|
1967
|
+
gov.raiseCostCap(this.opts.cost.capIncrementUsd ?? this.opts.cost.maxCostUsd);
|
|
1968
|
+
return this.agent().stream(capHit.message, { runId: args.runId, requestContext: rc, ...this.memoryOpts({ ...ctx, runId: args.runId }) });
|
|
1969
|
+
})() : await this.agent().resumeStream(
|
|
1970
|
+
{ answer: args.answer },
|
|
1971
|
+
{ runId: args.runId, toolCallId: args.toolCallId, requestContext: rc, ...this.memoryOpts({ ...ctx, runId: args.runId }) }
|
|
1972
|
+
) : await this.agent().stream(nudgeMessage, { runId: args.runId, requestContext: rc, ...this.memoryOpts({ ...ctx, runId: args.runId }) });
|
|
1973
|
+
firstPass = false;
|
|
1974
|
+
let sawStep = false;
|
|
1975
|
+
let lastOutputSlug;
|
|
1976
|
+
for await (const part of result.fullStream) {
|
|
1977
|
+
if (part?.type === "step-finish") {
|
|
1978
|
+
gov.step();
|
|
1979
|
+
sawStep = true;
|
|
1980
|
+
}
|
|
1981
|
+
if (part?.type === "tool-call-suspended") {
|
|
1982
|
+
const payload = part.payload ?? {};
|
|
1983
|
+
const toolCallId = payload.toolCallId ?? "";
|
|
1984
|
+
const followupId = `${args.runId}:${toolCallId}`;
|
|
1985
|
+
const sp = payload.suspendPayload ?? {};
|
|
1986
|
+
await recordSuspend(this.opts.storage, rctx, followupId, toolCallId);
|
|
1987
|
+
await this.opts.storage.runs.setStatus(args.runId, "suspended");
|
|
1988
|
+
await this.opts.storage.runs.saveSnapshot(args.runId, { pending: { toolCallId }, genIndex: generationIndex + 1, state: resumedState.entries() });
|
|
1989
|
+
{
|
|
1990
|
+
const t = await emitTurn();
|
|
1991
|
+
if (t) yield t;
|
|
1992
|
+
}
|
|
1993
|
+
yield await emit(ev("swarm.status", base(rctx, ts++), { state: "waiting" }));
|
|
1994
|
+
yield await emit(clientActionOrQuestion(rctx, ts++, followupId, toolCallId, sp));
|
|
1995
|
+
resumeOutcome = "suspended";
|
|
1996
|
+
return;
|
|
1997
|
+
}
|
|
1998
|
+
if (part?.type === "error") {
|
|
1999
|
+
await this.opts.storage.runs.setStatus(args.runId, "failed");
|
|
2000
|
+
{
|
|
2001
|
+
const t = await emitTurn();
|
|
2002
|
+
if (t) yield t;
|
|
2003
|
+
}
|
|
2004
|
+
yield await emit(
|
|
2005
|
+
ev("swarm.run_failed", base(rctx, ts++), {
|
|
2006
|
+
stage: "stream",
|
|
2007
|
+
message: streamErrorMessage(part),
|
|
2008
|
+
retryable: false
|
|
2009
|
+
})
|
|
2010
|
+
);
|
|
2011
|
+
return;
|
|
2012
|
+
}
|
|
2013
|
+
collectSpans(collector, part, modelId, gov);
|
|
2014
|
+
for (const e of mapChunk(part, rctx, gov, modelIdFor, () => ts++, streamed, delegateBudgets, activity, gatesApproval, turnUsage, generationIndex)) {
|
|
2015
|
+
if (e.type === "swarm.message" || e.type === "swarm.tool_call") {
|
|
2016
|
+
lastOutputSlug = e.agentSlug;
|
|
2017
|
+
if (this.opts.verifyCompletion) transcript = appendTranscript(transcript, e);
|
|
2018
|
+
}
|
|
2019
|
+
yield await emit(e);
|
|
2020
|
+
}
|
|
2021
|
+
const overDelegate = delegateBudgets?.exceeded();
|
|
2022
|
+
const stop = gov.shouldStop();
|
|
2023
|
+
if (stop.stop || overDelegate) {
|
|
2024
|
+
if (this.opts.cost.onCapHit === "ask" && stop.stop && !overDelegate && capHit) {
|
|
2025
|
+
const followupId = `${args.runId}:${CAP_FOLLOWUP_SUFFIX}`;
|
|
2026
|
+
await recordSuspend(this.opts.storage, rctx, followupId, CAP_FOLLOWUP_SUFFIX);
|
|
2027
|
+
await this.opts.storage.runs.setStatus(args.runId, "suspended");
|
|
2028
|
+
await this.opts.storage.runs.saveSnapshot(args.runId, {
|
|
2029
|
+
capHit: { message: capHit.message, spentUsd: gov.costUsd() },
|
|
2030
|
+
genIndex: generationIndex + 1,
|
|
2031
|
+
state: resumedState.entries()
|
|
2032
|
+
// FR-003: persist per-run state across the cap-ask boundary
|
|
2033
|
+
});
|
|
2034
|
+
{
|
|
2035
|
+
const t = await emitTurn();
|
|
2036
|
+
if (t) yield t;
|
|
2037
|
+
}
|
|
2038
|
+
yield await emit(ev("swarm.status", base(rctx, ts++), { state: "waiting" }));
|
|
2039
|
+
yield await emit(ev("swarm.question", base(rctx, ts++), capQuestion(rctx, followupId, gov)));
|
|
2040
|
+
resumeOutcome = "suspended";
|
|
2041
|
+
return;
|
|
2042
|
+
}
|
|
2043
|
+
await this.opts.storage.runs.setStatus(args.runId, "failed");
|
|
2044
|
+
{
|
|
2045
|
+
const t = await emitTurn();
|
|
2046
|
+
if (t) yield t;
|
|
2047
|
+
}
|
|
2048
|
+
yield await emit(
|
|
2049
|
+
ev("swarm.run_failed", base(rctx, ts++), {
|
|
2050
|
+
stage: "cost",
|
|
2051
|
+
message: overDelegate?.reason ?? stop.reason,
|
|
2052
|
+
retryable: false
|
|
2053
|
+
})
|
|
2054
|
+
);
|
|
2055
|
+
return;
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
if (this.opts.verifyCompletion) {
|
|
2059
|
+
const verdict = await this.safeVerify(request, transcript, rctx);
|
|
2060
|
+
if (!verdict.complete && resumeNudges < MAX_CONTINUE_NUDGES) {
|
|
2061
|
+
resumeNudges++;
|
|
2062
|
+
nudgeMessage = verifyNudge(verdict.missing);
|
|
2063
|
+
continue;
|
|
2064
|
+
}
|
|
2065
|
+
incompleteVerdict = verdict.complete ? null : verdict;
|
|
2066
|
+
} else if (sawStep && lastOutputSlug !== rctx.agentSlug && resumeNudges < MAX_CONTINUE_NUDGES) {
|
|
2067
|
+
resumeNudges++;
|
|
2068
|
+
continue;
|
|
2069
|
+
}
|
|
2070
|
+
break;
|
|
1123
2071
|
}
|
|
1124
2072
|
await this.attributeRun(rctx);
|
|
1125
|
-
|
|
1126
|
-
|
|
2073
|
+
if (incompleteVerdict) {
|
|
2074
|
+
await this.opts.storage.runs.setStatus(args.runId, "failed");
|
|
2075
|
+
{
|
|
2076
|
+
const t = await emitTurn();
|
|
2077
|
+
if (t) yield t;
|
|
2078
|
+
}
|
|
2079
|
+
yield await emit(ev("swarm.run_failed", base(rctx, ts++), { stage: "incomplete", message: incompleteVerdict.missing ?? "The task was not completed.", retryable: true }));
|
|
2080
|
+
} else {
|
|
2081
|
+
await this.opts.storage.runs.setStatus(args.runId, "done");
|
|
2082
|
+
{
|
|
2083
|
+
const t = await emitTurn();
|
|
2084
|
+
if (t) yield t;
|
|
2085
|
+
}
|
|
2086
|
+
yield await emit(ev("swarm.status", base(rctx, ts++), { state: "done" }));
|
|
2087
|
+
resumeOutcome = "done";
|
|
2088
|
+
}
|
|
1127
2089
|
} catch (err) {
|
|
1128
|
-
|
|
2090
|
+
const stage = err instanceof ReserveDenied ? "reserve" : "exception";
|
|
2091
|
+
if (stage !== "reserve") console.error(`[@nightowlsdev/core] resume ${args.runId} threw:`, err);
|
|
1129
2092
|
try {
|
|
1130
2093
|
await this.opts.storage.runs.setStatus(args.runId, "failed");
|
|
1131
2094
|
} catch {
|
|
1132
2095
|
}
|
|
1133
|
-
|
|
2096
|
+
{
|
|
2097
|
+
const t = await emitTurn();
|
|
2098
|
+
if (t) yield t;
|
|
2099
|
+
}
|
|
2100
|
+
yield await emit(ev("swarm.run_failed", base(rctx, ts++), { stage, message: errMessage(err), retryable: false }));
|
|
1134
2101
|
} finally {
|
|
2102
|
+
if (this.opts.onRunEnd) {
|
|
2103
|
+
try {
|
|
2104
|
+
await this.opts.onRunEnd(ctx, { state: resumedState, outcome: resumeOutcome });
|
|
2105
|
+
} catch (err) {
|
|
2106
|
+
console.error(`[@nightowlsdev/core] onRunEnd threw for resume ${args.runId}:`, err);
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
1135
2109
|
floorAbort.abort();
|
|
1136
2110
|
await releaseFloor?.();
|
|
1137
2111
|
await exportSpans(this.opts.telemetry, collector);
|
|
1138
2112
|
}
|
|
1139
2113
|
}
|
|
1140
2114
|
};
|
|
2115
|
+
var ReserveDenied = class extends Error {
|
|
2116
|
+
stage = "reserve";
|
|
2117
|
+
constructor(reason) {
|
|
2118
|
+
super(reason);
|
|
2119
|
+
this.name = "ReserveDenied";
|
|
2120
|
+
}
|
|
2121
|
+
};
|
|
1141
2122
|
function errMessage(err) {
|
|
1142
2123
|
return err instanceof Error ? err.message : String(err);
|
|
1143
2124
|
}
|
|
1144
2125
|
function base(ctx, ts) {
|
|
1145
2126
|
return { runId: ctx.runId, agentSlug: ctx.agentSlug, ts };
|
|
1146
2127
|
}
|
|
2128
|
+
function clientActionOrQuestion(ctx, ts, followupId, toolCallId, sp) {
|
|
2129
|
+
if (sp.clientAction) {
|
|
2130
|
+
return ev("swarm.client_action", base(ctx, ts), {
|
|
2131
|
+
followupId,
|
|
2132
|
+
toolCallId,
|
|
2133
|
+
tool: sp.clientAction.tool,
|
|
2134
|
+
input: sp.clientAction.input,
|
|
2135
|
+
needsApproval: sp.clientAction.needsApproval ?? false,
|
|
2136
|
+
from: sp.asker || ctx.agentSlug
|
|
2137
|
+
});
|
|
2138
|
+
}
|
|
2139
|
+
return ev("swarm.question", base(ctx, ts), {
|
|
2140
|
+
followupId,
|
|
2141
|
+
toolCallId,
|
|
2142
|
+
to: sp.to ?? "user",
|
|
2143
|
+
from: sp.asker || ctx.agentSlug,
|
|
2144
|
+
prompt: sp.prompt ?? "",
|
|
2145
|
+
field: sp.field
|
|
2146
|
+
});
|
|
2147
|
+
}
|
|
2148
|
+
var CAP_FOLLOWUP_SUFFIX = "cap";
|
|
2149
|
+
function capQuestion(ctx, followupId, gov) {
|
|
2150
|
+
const spent = gov.costUsd();
|
|
2151
|
+
const cap = gov.maxCostUsd;
|
|
2152
|
+
return {
|
|
2153
|
+
followupId,
|
|
2154
|
+
toolCallId: CAP_FOLLOWUP_SUFFIX,
|
|
2155
|
+
to: "user",
|
|
2156
|
+
from: ctx.agentSlug,
|
|
2157
|
+
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.`,
|
|
2158
|
+
field: {
|
|
2159
|
+
kind: "confirm",
|
|
2160
|
+
confirmLabel: "Continue",
|
|
2161
|
+
rejectLabel: "Stop"
|
|
2162
|
+
}
|
|
2163
|
+
};
|
|
2164
|
+
}
|
|
2165
|
+
function turnUsageEvent(ctx, ts, turnUsage, segmentIndex) {
|
|
2166
|
+
const total = sumTurnUsage(turnUsage);
|
|
2167
|
+
return ev("swarm.turn_usage", base(ctx, ts), {
|
|
2168
|
+
breakdown: total.breakdown,
|
|
2169
|
+
cost: total.cost,
|
|
2170
|
+
bySlug: total.bySlug,
|
|
2171
|
+
generations: turnUsage.length,
|
|
2172
|
+
segmentIndex
|
|
2173
|
+
});
|
|
2174
|
+
}
|
|
2175
|
+
function extractUsage(usage) {
|
|
2176
|
+
const u = usage ?? {};
|
|
2177
|
+
const cacheRead = u.cachedInputTokens ?? u.inputTokenDetails?.cacheReadTokens ?? u.raw?.inputTokenDetails?.cacheReadTokens;
|
|
2178
|
+
const cacheWrite = u.inputTokenDetails?.cacheWriteTokens ?? u.raw?.inputTokenDetails?.cacheWriteTokens;
|
|
2179
|
+
const reasoning = u.reasoningTokens ?? u.outputTokenDetails?.reasoningTokens ?? u.raw?.outputTokenDetails?.reasoningTokens;
|
|
2180
|
+
const b = { inputTokens: u.inputTokens ?? 0, outputTokens: u.outputTokens ?? 0 };
|
|
2181
|
+
if (cacheRead != null) b.cacheReadTokens = cacheRead;
|
|
2182
|
+
if (cacheWrite != null) b.cacheWriteTokens = cacheWrite;
|
|
2183
|
+
if (reasoning != null) b.reasoningTokens = reasoning;
|
|
2184
|
+
return b;
|
|
2185
|
+
}
|
|
1147
2186
|
function titleCase(slug) {
|
|
1148
2187
|
return slug.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
1149
2188
|
}
|
|
@@ -1175,8 +2214,7 @@ function collectSpans(collector, part, modelId, gov) {
|
|
|
1175
2214
|
break;
|
|
1176
2215
|
case "step-finish": {
|
|
1177
2216
|
const output = p.output;
|
|
1178
|
-
const
|
|
1179
|
-
const u = { inputTokens: usage?.inputTokens ?? 0, outputTokens: usage?.outputTokens ?? 0 };
|
|
2217
|
+
const u = extractUsage(output?.usage);
|
|
1180
2218
|
collector.openGeneration(modelId);
|
|
1181
2219
|
collector.closeGeneration(u, gov.priceOf(modelId, u));
|
|
1182
2220
|
break;
|
|
@@ -1197,14 +2235,19 @@ async function recordSuspend(storage, ctx, followupId, toolCallId) {
|
|
|
1197
2235
|
if (!storage.recordSuspend && !warnedNoRecordSuspend) {
|
|
1198
2236
|
warnedNoRecordSuspend = true;
|
|
1199
2237
|
console.warn(
|
|
1200
|
-
"[
|
|
2238
|
+
"[@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
2239
|
);
|
|
1202
2240
|
}
|
|
1203
2241
|
await storage.recordSuspend?.(ctx.runId, ctx.tenantId, followupId, toolCallId);
|
|
1204
2242
|
}
|
|
1205
|
-
function mapChunk(part, ctx, gov, modelIdFor, nextTs, streamed, delegateBudgets) {
|
|
2243
|
+
function mapChunk(part, ctx, gov, modelIdFor, nextTs, streamed, delegateBudgets, activity, gatesApproval, turnUsage, segmentIndex) {
|
|
1206
2244
|
const p = part.payload ?? {};
|
|
1207
2245
|
const modelId = modelIdFor(ctx.agentSlug);
|
|
2246
|
+
const act = (slug) => {
|
|
2247
|
+
let a = activity.get(slug);
|
|
2248
|
+
if (!a) activity.set(slug, a = { toolCalls: 0, agentActivations: 0 });
|
|
2249
|
+
return a;
|
|
2250
|
+
};
|
|
1208
2251
|
switch (part.type) {
|
|
1209
2252
|
case "text-delta":
|
|
1210
2253
|
return [ev("swarm.message", base(ctx, nextTs()), { role: "assistant", delta: p.text ?? "" })];
|
|
@@ -1214,17 +2257,21 @@ function mapChunk(part, ctx, gov, modelIdFor, nextTs, streamed, delegateBudgets)
|
|
|
1214
2257
|
const to = name.slice("agent-".length);
|
|
1215
2258
|
const a = typeof p.args === "string" ? safeParse(p.args) : p.args;
|
|
1216
2259
|
const task = a?.prompt ?? "";
|
|
2260
|
+
act(ctx.agentSlug).agentActivations++;
|
|
1217
2261
|
return [
|
|
1218
2262
|
ev("swarm.handoff", base(ctx, nextTs()), { from: ctx.agentSlug, to, task }),
|
|
1219
2263
|
ev("swarm.status", base(ctx, nextTs()), { state: "delegating", note: to })
|
|
1220
2264
|
];
|
|
1221
2265
|
}
|
|
2266
|
+
act(ctx.agentSlug).toolCalls++;
|
|
1222
2267
|
return [
|
|
1223
2268
|
ev("swarm.tool_call", base(ctx, nextTs()), {
|
|
1224
2269
|
toolCallId: p.toolCallId,
|
|
1225
2270
|
name,
|
|
1226
2271
|
args: p.args,
|
|
1227
|
-
|
|
2272
|
+
// SP5 truth-fix: emit the RESOLVED needsApproval (policy + the tool's flag), not a hardcoded false, so
|
|
2273
|
+
// the UI reflects reality — a tool that will suspend-for-approval is shown as needing approval.
|
|
2274
|
+
needsApproval: gatesApproval(name)
|
|
1228
2275
|
})
|
|
1229
2276
|
];
|
|
1230
2277
|
}
|
|
@@ -1253,9 +2300,19 @@ function mapChunk(part, ctx, gov, modelIdFor, nextTs, streamed, delegateBudgets)
|
|
|
1253
2300
|
const output = p.output;
|
|
1254
2301
|
const usage = output?.usage;
|
|
1255
2302
|
if (usage) {
|
|
1256
|
-
const
|
|
2303
|
+
const counters = activity.get(ctx.agentSlug);
|
|
2304
|
+
const u = extractUsage(usage);
|
|
2305
|
+
if (counters && (counters.toolCalls || counters.agentActivations)) {
|
|
2306
|
+
u.toolCalls = counters.toolCalls;
|
|
2307
|
+
u.agentActivations = counters.agentActivations;
|
|
2308
|
+
}
|
|
2309
|
+
activity.delete(ctx.agentSlug);
|
|
1257
2310
|
gov.addUsage(modelId, u);
|
|
1258
2311
|
delegateBudgets?.addUsage(ctx.agentSlug, modelId, u);
|
|
2312
|
+
const cost = gov.costOf(modelId, u);
|
|
2313
|
+
const generationId = `${ctx.runId}:${segmentIndex}:${turnUsage.length}`;
|
|
2314
|
+
turnUsage.push({ slug: ctx.agentSlug, breakdown: u, cost });
|
|
2315
|
+
return [ev("swarm.usage", base(ctx, nextTs()), { slug: ctx.agentSlug, modelId, breakdown: u, cost, generationId })];
|
|
1259
2316
|
}
|
|
1260
2317
|
return [];
|
|
1261
2318
|
}
|
|
@@ -1266,7 +2323,7 @@ function mapChunk(part, ctx, gov, modelIdFor, nextTs, streamed, delegateBudgets)
|
|
|
1266
2323
|
const inner = p.output;
|
|
1267
2324
|
if (!inner || typeof inner.type !== "string") return [];
|
|
1268
2325
|
if (inner.type === "text-delta") streamed.add(p.toolCallId);
|
|
1269
|
-
return mapChunk(inner, { ...ctx, agentSlug: subSlug }, gov, modelIdFor, nextTs, streamed, delegateBudgets);
|
|
2326
|
+
return mapChunk(inner, { ...ctx, agentSlug: subSlug }, gov, modelIdFor, nextTs, streamed, delegateBudgets, activity, gatesApproval, turnUsage, segmentIndex);
|
|
1270
2327
|
}
|
|
1271
2328
|
case "tool-error": {
|
|
1272
2329
|
const name = p.toolName ?? "";
|
|
@@ -1295,8 +2352,121 @@ function allowListModelProvider(opts) {
|
|
|
1295
2352
|
};
|
|
1296
2353
|
}
|
|
1297
2354
|
|
|
2355
|
+
// src/rules.ts
|
|
2356
|
+
var import_hooks2 = require("@nightowlsdev/hooks");
|
|
2357
|
+
var GLOB_CACHE = /* @__PURE__ */ new Map();
|
|
2358
|
+
function globRegex(pattern) {
|
|
2359
|
+
let re = GLOB_CACHE.get(pattern);
|
|
2360
|
+
if (!re) {
|
|
2361
|
+
re = new RegExp("^" + pattern.replace(/[.*+?^${}()|[\]\\]/g, (c) => c === "*" ? ".*" : "\\" + c) + "$");
|
|
2362
|
+
GLOB_CACHE.set(pattern, re);
|
|
2363
|
+
}
|
|
2364
|
+
return re;
|
|
2365
|
+
}
|
|
2366
|
+
function globMatch(pattern, value) {
|
|
2367
|
+
if (pattern === value) return true;
|
|
2368
|
+
if (!pattern.includes("*")) return false;
|
|
2369
|
+
return globRegex(pattern).test(value);
|
|
2370
|
+
}
|
|
2371
|
+
function matchField(field, value) {
|
|
2372
|
+
if (field === void 0) return true;
|
|
2373
|
+
const arr = Array.isArray(field) ? field : [field];
|
|
2374
|
+
return arr.some((p) => globMatch(p, value));
|
|
2375
|
+
}
|
|
2376
|
+
function ruleMatchesTool(rule, ev2) {
|
|
2377
|
+
if (rule.seam !== "tool") return false;
|
|
2378
|
+
if (rule.scopeAgent !== void 0 && rule.scopeAgent !== ev2.agentSlug) return false;
|
|
2379
|
+
const w = rule.when;
|
|
2380
|
+
if (!matchField(w.agent, ev2.agentSlug)) return false;
|
|
2381
|
+
if (!matchField(w.tool, ev2.toolName)) return false;
|
|
2382
|
+
if (w.origin !== void 0 && w.origin !== ev2.origin) return false;
|
|
2383
|
+
return true;
|
|
2384
|
+
}
|
|
2385
|
+
function ruleMatchesGeneration(rule, ev2) {
|
|
2386
|
+
if (rule.seam !== "generation") return false;
|
|
2387
|
+
if (rule.scopeAgent !== void 0 && rule.scopeAgent !== ev2.agentSlug) return false;
|
|
2388
|
+
const w = rule.when;
|
|
2389
|
+
if (!matchField(w.agent, ev2.agentSlug)) return false;
|
|
2390
|
+
if (!matchField(w.model, ev2.modelId)) return false;
|
|
2391
|
+
return true;
|
|
2392
|
+
}
|
|
2393
|
+
var TOOL_RANK = { deny: 2, ask: 1, allow: 0 };
|
|
2394
|
+
function mostRestrictiveTool(a, b) {
|
|
2395
|
+
return TOOL_RANK[b.action] > TOOL_RANK[a.action] ? b : a;
|
|
2396
|
+
}
|
|
2397
|
+
function errMessage2(err) {
|
|
2398
|
+
return err instanceof Error ? err.message : String(err);
|
|
2399
|
+
}
|
|
2400
|
+
function composeToolHooks(opts) {
|
|
2401
|
+
const rules = opts.rules.filter((r) => r.seam === "tool" && r.level === "enforce");
|
|
2402
|
+
return async (ev2) => {
|
|
2403
|
+
let decision = (0, import_hooks2.toolPolicyDecision)(ev2, opts.policy);
|
|
2404
|
+
if (opts.host) {
|
|
2405
|
+
try {
|
|
2406
|
+
decision = mostRestrictiveTool(decision, await opts.host(ev2));
|
|
2407
|
+
} catch (err) {
|
|
2408
|
+
return (0, import_hooks2.deny)(`preToolCall hook threw: ${errMessage2(err)}`);
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
if (decision.action === "deny") return decision;
|
|
2412
|
+
for (const r of rules) {
|
|
2413
|
+
if (!ruleMatchesTool(r, ev2)) continue;
|
|
2414
|
+
const rd = r.action.do === "deny" ? (0, import_hooks2.deny)(r.action.reason ?? r.statement) : (0, import_hooks2.ask)(r.action.reason ?? r.statement);
|
|
2415
|
+
decision = mostRestrictiveTool(decision, rd);
|
|
2416
|
+
if (decision.action === "deny") return decision;
|
|
2417
|
+
}
|
|
2418
|
+
return decision;
|
|
2419
|
+
};
|
|
2420
|
+
}
|
|
2421
|
+
function composeGenerationHooks(opts) {
|
|
2422
|
+
const rules = opts.rules.filter((r) => r.seam === "generation" && r.level === "enforce");
|
|
2423
|
+
if (!rules.length && !opts.host) return void 0;
|
|
2424
|
+
return async (ev2) => {
|
|
2425
|
+
if (opts.host) {
|
|
2426
|
+
try {
|
|
2427
|
+
const d = await opts.host(ev2);
|
|
2428
|
+
if (d.action === "deny") return d;
|
|
2429
|
+
} catch (err) {
|
|
2430
|
+
return (0, import_hooks2.deny)(`preGeneration hook threw: ${errMessage2(err)}`);
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
for (const r of rules) {
|
|
2434
|
+
if (ruleMatchesGeneration(r, ev2)) return (0, import_hooks2.deny)(r.action?.reason ?? r.statement);
|
|
2435
|
+
}
|
|
2436
|
+
return { action: "allow" };
|
|
2437
|
+
};
|
|
2438
|
+
}
|
|
2439
|
+
function softPolicyFor(slug, rules, workflows) {
|
|
2440
|
+
const out = [];
|
|
2441
|
+
for (const r of rules) {
|
|
2442
|
+
if (r.level !== "advise") continue;
|
|
2443
|
+
if (r.scopeAgent !== void 0 && r.scopeAgent !== slug) continue;
|
|
2444
|
+
if (!matchField(r.when.agent, slug)) continue;
|
|
2445
|
+
out.push(r.statement);
|
|
2446
|
+
}
|
|
2447
|
+
for (const w of workflows) {
|
|
2448
|
+
if (w.compliance !== "advisory" || !w.description) continue;
|
|
2449
|
+
if (w.scopeAgent !== void 0 && w.scopeAgent !== slug) continue;
|
|
2450
|
+
out.push(`Suggested procedure "${w.name}": ${w.description}`);
|
|
2451
|
+
}
|
|
2452
|
+
return out;
|
|
2453
|
+
}
|
|
2454
|
+
|
|
1298
2455
|
// src/define.ts
|
|
1299
2456
|
var MASTRA = /* @__PURE__ */ new WeakMap();
|
|
2457
|
+
var APPROVAL_SUSPEND_SCHEMA = import_zod4.z.object({
|
|
2458
|
+
to: import_zod4.z.string(),
|
|
2459
|
+
prompt: import_zod4.z.string(),
|
|
2460
|
+
field: import_zod4.z.object({
|
|
2461
|
+
kind: import_zod4.z.enum(["confirm", "buttons", "select", "multiselect", "number", "text", "textarea"]),
|
|
2462
|
+
confirmLabel: import_zod4.z.string().optional(),
|
|
2463
|
+
rejectLabel: import_zod4.z.string().optional()
|
|
2464
|
+
}).optional(),
|
|
2465
|
+
asker: import_zod4.z.string().optional(),
|
|
2466
|
+
kind: import_zod4.z.literal("approval").optional(),
|
|
2467
|
+
toolName: import_zod4.z.string().optional()
|
|
2468
|
+
});
|
|
2469
|
+
var APPROVAL_RESUME_SCHEMA = import_zod4.z.object({ answer: import_zod4.z.any() });
|
|
1300
2470
|
function defineTool(spec) {
|
|
1301
2471
|
const origin = spec.origin ?? "first-party";
|
|
1302
2472
|
const needsApproval = spec.needsApproval ?? origin === "mcp";
|
|
@@ -1305,26 +2475,135 @@ function defineTool(spec) {
|
|
|
1305
2475
|
description: spec.description ?? spec.name,
|
|
1306
2476
|
inputSchema: spec.inputSchema,
|
|
1307
2477
|
outputSchema: spec.outputSchema,
|
|
2478
|
+
// SP5: declare suspend/resume schemas so the action-approval gate can suspend-and-ask via
|
|
2479
|
+
// context.agent.suspend (Mastra gates `suspend` on a declared suspendSchema — same as the built-in `ask`).
|
|
2480
|
+
// The suspend payload is `ask`-shaped (+ approval metadata) so the engine emits the existing `swarm.question`;
|
|
2481
|
+
// resume carries the human's `{ answer }` (a confirm → boolean approve/reject).
|
|
2482
|
+
suspendSchema: APPROVAL_SUSPEND_SCHEMA,
|
|
2483
|
+
resumeSchema: APPROVAL_RESUME_SCHEMA,
|
|
1308
2484
|
// Mastra 1.38 (per SPIKE-FINDINGS item 3): execute is `(inputData, context) => out`
|
|
1309
2485
|
// with TWO positional args. `inputData` is the parsed input; tenant/user/run come
|
|
1310
|
-
// off `context.requestContext`.
|
|
2486
|
+
// off `context.requestContext`. `context.agent` (when an agent drives the call) carries the
|
|
2487
|
+
// suspend/resumeData handles SP5's action-approval gate uses to suspend-and-ask the human.
|
|
2488
|
+
//
|
|
2489
|
+
// SP5 — the mandatory action-approval HITL gate (the enforcement POINT). Before the side effect runs we
|
|
2490
|
+
// resolve the effective ToolDecision via the per-run `ToolGate` the engine injected on the RequestContext
|
|
2491
|
+
// (policy + the resolved needsApproval + the preToolCall hook):
|
|
2492
|
+
// • allow → execute as today.
|
|
2493
|
+
// • deny → return a blocked tool-result; the side effect NEVER runs.
|
|
2494
|
+
// • ask → SUSPEND via context.agent.suspend(...) with an `ask`-SHAPED payload, so the engine's existing
|
|
2495
|
+
// `tool-call-suspended` handler emits the SAME `swarm.question`; on resume the tool re-executes
|
|
2496
|
+
// with context.agent.resumeData = { answer } → approve runs the side effect, reject blocks it.
|
|
2497
|
+
// We reuse the EXISTING suspend/resume + question/answer machinery — no parallel one.
|
|
1311
2498
|
execute: async (inputData, context) => {
|
|
1312
2499
|
const rc = context?.requestContext;
|
|
2500
|
+
const tenantId = rc?.get?.("tenantId") ?? "default";
|
|
2501
|
+
const userId = rc?.get?.("userId") ?? "";
|
|
2502
|
+
const runId = rc?.get?.("runId") ?? "";
|
|
2503
|
+
const resolver = rc?.get?.(SECRET_RESOLVER_KEY);
|
|
2504
|
+
const scopedCtx = {
|
|
2505
|
+
tenantId,
|
|
2506
|
+
userId,
|
|
2507
|
+
runId,
|
|
2508
|
+
agentSlug: rc?.get?.("agentSlug") ?? "",
|
|
2509
|
+
threadId: rc?.get?.("threadId") ?? "",
|
|
2510
|
+
...(() => {
|
|
2511
|
+
const v = rc?.get?.("agentVersion");
|
|
2512
|
+
return typeof v === "number" ? { agentVersion: v } : {};
|
|
2513
|
+
})()
|
|
2514
|
+
};
|
|
1313
2515
|
const ctx = {
|
|
1314
|
-
tenantId
|
|
1315
|
-
userId
|
|
1316
|
-
runId
|
|
2516
|
+
tenantId,
|
|
2517
|
+
userId,
|
|
2518
|
+
runId,
|
|
2519
|
+
secrets: bindSecrets(resolver, scopedCtx),
|
|
2520
|
+
// FR-003: the per-run state handle the engine put on the rc (same object across the run's tool calls +
|
|
2521
|
+
// delegated sub-agents). Absent on a raw test stream built without the engine — then `ctx.state` is
|
|
2522
|
+
// undefined, unchanged from prior behaviour.
|
|
2523
|
+
state: rc?.get?.(RUN_STATE_KEY)
|
|
1317
2524
|
};
|
|
1318
|
-
|
|
2525
|
+
const run = () => spec.execute(inputData, ctx);
|
|
2526
|
+
const agentCtx = context?.agent;
|
|
2527
|
+
if (agentCtx?.resumeData !== void 0 && agentCtx.resumeData !== null) {
|
|
2528
|
+
const answer = agentCtx.resumeData.answer;
|
|
2529
|
+
if (isApproved(answer)) return run();
|
|
2530
|
+
throw new ToolBlockedError(spec.name, "rejected by approver");
|
|
2531
|
+
}
|
|
2532
|
+
const gate = rc?.get?.(TOOL_GATE_KEY);
|
|
2533
|
+
if (!gate || typeof agentCtx?.suspend !== "function") return run();
|
|
2534
|
+
const agentSlug = deriveAsker(agentCtx, rc);
|
|
2535
|
+
const decision = await gate(
|
|
2536
|
+
toolPreCallEvent({
|
|
2537
|
+
runId: ctx.runId,
|
|
2538
|
+
tenantId: ctx.tenantId,
|
|
2539
|
+
agentSlug,
|
|
2540
|
+
toolName: spec.name,
|
|
2541
|
+
origin,
|
|
2542
|
+
needsApproval,
|
|
2543
|
+
args: inputData
|
|
2544
|
+
})
|
|
2545
|
+
);
|
|
2546
|
+
if (decision.action === "allow") return run();
|
|
2547
|
+
if (decision.action === "deny") throw new ToolBlockedError(spec.name, decision.reason);
|
|
2548
|
+
await agentCtx.suspend(approvalSuspendPayload({ toolName: spec.name, asker: agentSlug, reason: decision.reason }));
|
|
2549
|
+
throw new ToolBlockedError(spec.name, "awaiting approval");
|
|
1319
2550
|
}
|
|
1320
2551
|
});
|
|
1321
2552
|
const handle = { name: spec.name, needsApproval, origin };
|
|
1322
2553
|
MASTRA.set(handle, mastraTool);
|
|
2554
|
+
setToolExecutor(handle, (args, c) => spec.execute(args, c));
|
|
1323
2555
|
return handle;
|
|
1324
2556
|
}
|
|
1325
2557
|
function defineSkill(tool) {
|
|
1326
2558
|
return tool;
|
|
1327
2559
|
}
|
|
2560
|
+
function deriveAsker(agentCtx, rc) {
|
|
2561
|
+
const agentId = agentCtx?.agentId ?? "";
|
|
2562
|
+
if (agentId.startsWith("swarm-sub-")) return agentId.slice("swarm-sub-".length);
|
|
2563
|
+
return rc?.get?.("agentSlug") ?? "";
|
|
2564
|
+
}
|
|
2565
|
+
var CLIENT_ACTION_SUSPEND_SCHEMA = import_zod4.z.object({
|
|
2566
|
+
clientAction: import_zod4.z.object({ tool: import_zod4.z.string(), input: import_zod4.z.any(), needsApproval: import_zod4.z.boolean().optional() }),
|
|
2567
|
+
asker: import_zod4.z.string().optional()
|
|
2568
|
+
});
|
|
2569
|
+
var CLIENT_ACTION_RESUME_SCHEMA = import_zod4.z.object({ answer: import_zod4.z.any() });
|
|
2570
|
+
var ClientToolError = class extends Error {
|
|
2571
|
+
constructor(toolName, reason) {
|
|
2572
|
+
super(reason ? `client tool ${toolName} failed: ${reason}` : `client tool ${toolName} failed`);
|
|
2573
|
+
this.name = "ClientToolError";
|
|
2574
|
+
}
|
|
2575
|
+
};
|
|
2576
|
+
function defineClientTool(spec) {
|
|
2577
|
+
const needsApproval = spec.needsApproval ?? false;
|
|
2578
|
+
const mastraTool = (0, import_tools4.createTool)({
|
|
2579
|
+
id: spec.name,
|
|
2580
|
+
description: spec.description ?? spec.name,
|
|
2581
|
+
inputSchema: spec.inputSchema,
|
|
2582
|
+
outputSchema: spec.outputSchema,
|
|
2583
|
+
suspendSchema: CLIENT_ACTION_SUSPEND_SCHEMA,
|
|
2584
|
+
resumeSchema: CLIENT_ACTION_RESUME_SCHEMA,
|
|
2585
|
+
execute: async (inputData, context) => {
|
|
2586
|
+
const rc = context?.requestContext;
|
|
2587
|
+
const agentCtx = context?.agent;
|
|
2588
|
+
if (agentCtx?.resumeData !== void 0 && agentCtx.resumeData !== null) {
|
|
2589
|
+
const answer = agentCtx.resumeData.answer;
|
|
2590
|
+
if (answer && typeof answer === "object" && "error" in answer && answer.error) {
|
|
2591
|
+
throw new ClientToolError(spec.name, String(answer.error));
|
|
2592
|
+
}
|
|
2593
|
+
return answer && typeof answer === "object" && "output" in answer ? answer.output : answer;
|
|
2594
|
+
}
|
|
2595
|
+
if (typeof agentCtx?.suspend !== "function") {
|
|
2596
|
+
throw new ClientToolError(spec.name, "client tools require an agent-driven run (no server execute)");
|
|
2597
|
+
}
|
|
2598
|
+
const asker = deriveAsker(agentCtx, rc);
|
|
2599
|
+
await agentCtx.suspend({ clientAction: { tool: spec.name, input: inputData, needsApproval }, asker });
|
|
2600
|
+
throw new ClientToolError(spec.name, "awaiting client action");
|
|
2601
|
+
}
|
|
2602
|
+
});
|
|
2603
|
+
const handle = { name: spec.name, needsApproval, origin: "first-party" };
|
|
2604
|
+
MASTRA.set(handle, mastraTool);
|
|
2605
|
+
return handle;
|
|
2606
|
+
}
|
|
1328
2607
|
function __getMastraTool(t) {
|
|
1329
2608
|
return MASTRA.get(t);
|
|
1330
2609
|
}
|
|
@@ -1335,6 +2614,10 @@ function defineAgent(spec) {
|
|
|
1335
2614
|
// The concrete skill handles ride along on the def so defineSwarm can build
|
|
1336
2615
|
// a per-swarm resolver. No module-level registry → no cross-swarm leakage.
|
|
1337
2616
|
skills,
|
|
2617
|
+
// Per-agent policy rides on the def (engine-local), stamped with this agent's scope so defineSwarm can
|
|
2618
|
+
// collect + apply it without persisting to the versioned AgentVersion row (D3).
|
|
2619
|
+
...spec.rules ? { rules: spec.rules.map((r) => ({ ...r, scopeAgent: spec.slug })) } : {},
|
|
2620
|
+
...spec.workflow ? { workflow: { ...spec.workflow, scopeAgent: spec.slug } } : {},
|
|
1338
2621
|
head: {
|
|
1339
2622
|
slug: spec.slug,
|
|
1340
2623
|
version: 1,
|
|
@@ -1348,11 +2631,217 @@ function defineAgent(spec) {
|
|
|
1348
2631
|
}
|
|
1349
2632
|
};
|
|
1350
2633
|
}
|
|
2634
|
+
function defineRule(spec) {
|
|
2635
|
+
if (!spec.id) throw new Error("defineRule: `id` is required");
|
|
2636
|
+
if (!spec.statement) throw new Error(`defineRule(${spec.id}): \`statement\` is required`);
|
|
2637
|
+
const tools = spec.when.tool === void 0 ? [] : Array.isArray(spec.when.tool) ? spec.when.tool : [spec.when.tool];
|
|
2638
|
+
const seam = spec.on ?? (spec.when.model !== void 0 ? "generation" : "tool");
|
|
2639
|
+
if (spec.level === "enforce") {
|
|
2640
|
+
if (!spec.action) throw new Error(`defineRule(${spec.id}): enforce rules require an \`action\``);
|
|
2641
|
+
if (!spec.on && spec.when.tool === void 0 && spec.when.model === void 0) {
|
|
2642
|
+
throw new Error(`defineRule(${spec.id}): an enforce rule with an empty \`when\` must set \`on\` ("tool" | "generation")`);
|
|
2643
|
+
}
|
|
2644
|
+
if (spec.action.do === "ask") {
|
|
2645
|
+
if (seam !== "tool") throw new Error(`defineRule(${spec.id}): \`ask\` is tool-seam only (preGeneration cannot suspend)`);
|
|
2646
|
+
if (tools.some((t) => t.startsWith("agent-"))) {
|
|
2647
|
+
throw new Error(`defineRule(${spec.id}): \`ask\` cannot target a delegation (\`agent-*\`) \u2014 gateDelegation defers \`ask\`; use \`deny\``);
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
return { id: spec.id, statement: spec.statement, when: spec.when, level: spec.level, action: spec.action, seam };
|
|
2652
|
+
}
|
|
2653
|
+
function workflowRefTargets(step) {
|
|
2654
|
+
const out = [];
|
|
2655
|
+
const scan = (o) => {
|
|
2656
|
+
for (const v of Object.values(o ?? {})) {
|
|
2657
|
+
if (v && typeof v === "object" && "$ref" in v) out.push(String(v.$ref));
|
|
2658
|
+
}
|
|
2659
|
+
};
|
|
2660
|
+
scan(step.args);
|
|
2661
|
+
scan(step.input);
|
|
2662
|
+
if (Array.isArray(step.next)) {
|
|
2663
|
+
for (const t of step.next) if (t.when?.$ref) out.push(t.when.$ref);
|
|
2664
|
+
}
|
|
2665
|
+
return out;
|
|
2666
|
+
}
|
|
2667
|
+
function defineWorkflow(spec) {
|
|
2668
|
+
if (!spec.name) throw new Error("defineWorkflow: `name` is required");
|
|
2669
|
+
if (!spec.steps?.length) throw new Error(`defineWorkflow(${spec.name}): at least one step is required`);
|
|
2670
|
+
const ids = /* @__PURE__ */ new Set();
|
|
2671
|
+
for (const s of spec.steps) {
|
|
2672
|
+
if (ids.has(s.id)) throw new Error(`defineWorkflow(${spec.name}): duplicate step id "${s.id}"`);
|
|
2673
|
+
ids.add(s.id);
|
|
2674
|
+
const kinds = [s.agent !== void 0, s.tool !== void 0, s.human !== void 0].filter(Boolean).length;
|
|
2675
|
+
if (kinds !== 1) throw new Error(`defineWorkflow(${spec.name}): step "${s.id}" must have exactly one of agent/tool/human`);
|
|
2676
|
+
}
|
|
2677
|
+
const start = spec.start ?? spec.steps[0].id;
|
|
2678
|
+
if (!ids.has(start)) throw new Error(`defineWorkflow(${spec.name}): start "${start}" is not a known step`);
|
|
2679
|
+
const nextOf = (s) => {
|
|
2680
|
+
const outs = s.next === void 0 ? [] : typeof s.next === "string" ? [s.next] : s.next.map((t) => t.to);
|
|
2681
|
+
if (s.onError && typeof s.onError === "object" && "to" in s.onError) outs.push(s.onError.to);
|
|
2682
|
+
return outs;
|
|
2683
|
+
};
|
|
2684
|
+
for (const s of spec.steps) {
|
|
2685
|
+
for (const t of nextOf(s)) if (!ids.has(t)) throw new Error(`defineWorkflow(${spec.name}): step "${s.id}" \u2192 unknown step "${t}"`);
|
|
2686
|
+
for (const r of workflowRefTargets(s)) {
|
|
2687
|
+
if (r !== "input" && !(r.startsWith("steps.") && ids.has(r.slice("steps.".length)))) {
|
|
2688
|
+
throw new Error(`defineWorkflow(${spec.name}): step "${s.id}" has an invalid $ref "${r}"`);
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
const byId = new Map(spec.steps.map((s) => [s.id, s]));
|
|
2693
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2694
|
+
const stack = /* @__PURE__ */ new Set();
|
|
2695
|
+
const visit = (id) => {
|
|
2696
|
+
if (stack.has(id)) throw new Error(`defineWorkflow(${spec.name}): cycle detected at step "${id}"`);
|
|
2697
|
+
if (seen.has(id)) return;
|
|
2698
|
+
seen.add(id);
|
|
2699
|
+
stack.add(id);
|
|
2700
|
+
for (const t of nextOf(byId.get(id))) visit(t);
|
|
2701
|
+
stack.delete(id);
|
|
2702
|
+
};
|
|
2703
|
+
visit(start);
|
|
2704
|
+
return { name: spec.name, compliance: spec.compliance, description: spec.description, steps: spec.steps, start };
|
|
2705
|
+
}
|
|
1351
2706
|
function buildSkillResolver(agents) {
|
|
1352
2707
|
const map = /* @__PURE__ */ new Map();
|
|
1353
2708
|
for (const a of agents) for (const s of a.skills ?? []) map.set(s.name, s);
|
|
1354
2709
|
return (name) => map.get(name);
|
|
1355
2710
|
}
|
|
2711
|
+
function isConnectorLooking(name) {
|
|
2712
|
+
return name.includes(".");
|
|
2713
|
+
}
|
|
2714
|
+
function ruleToolRefs(rule) {
|
|
2715
|
+
if (rule.seam !== "tool") return [];
|
|
2716
|
+
const t = rule.when.tool;
|
|
2717
|
+
const names = t === void 0 ? [] : Array.isArray(t) ? t : [t];
|
|
2718
|
+
return names.filter((n) => !n.includes("*"));
|
|
2719
|
+
}
|
|
2720
|
+
var CRED_REF_KEYS = /* @__PURE__ */ new Set(["secretref", "credentialref", "connectionid", "owlconnections"]);
|
|
2721
|
+
var normKey = (k) => k.toLowerCase().replace(/[-_]/g, "");
|
|
2722
|
+
function assertNoCredRefs(obj, where) {
|
|
2723
|
+
const walk = (v) => {
|
|
2724
|
+
if (Array.isArray(v)) {
|
|
2725
|
+
v.forEach(walk);
|
|
2726
|
+
return;
|
|
2727
|
+
}
|
|
2728
|
+
if (v && typeof v === "object") {
|
|
2729
|
+
for (const [k, val] of Object.entries(v)) {
|
|
2730
|
+
if (CRED_REF_KEYS.has(normKey(k))) {
|
|
2731
|
+
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`);
|
|
2732
|
+
}
|
|
2733
|
+
walk(val);
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
};
|
|
2737
|
+
walk(obj);
|
|
2738
|
+
}
|
|
2739
|
+
function defineBundle(spec) {
|
|
2740
|
+
if (!spec.slug) throw new Error("defineBundle: `slug` is required");
|
|
2741
|
+
if (!spec.agents?.length) throw new Error(`defineBundle(${spec.slug}): at least one agent is required`);
|
|
2742
|
+
const members = /* @__PURE__ */ new Set();
|
|
2743
|
+
const handles = /* @__PURE__ */ new Set();
|
|
2744
|
+
for (const a of spec.agents) {
|
|
2745
|
+
if (members.has(a.slug)) throw new Error(`defineBundle(${spec.slug}): duplicate agent slug "${a.slug}"`);
|
|
2746
|
+
members.add(a.slug);
|
|
2747
|
+
for (const s of a.skills ?? []) handles.add(s.name);
|
|
2748
|
+
}
|
|
2749
|
+
const requires = spec.requires ?? [];
|
|
2750
|
+
const requiredSlugs = new Set(requires.map((r) => r.slug));
|
|
2751
|
+
const connectorGrants = spec.connectorGrants ?? [];
|
|
2752
|
+
const grantedByMember = /* @__PURE__ */ new Map();
|
|
2753
|
+
const allGranted = /* @__PURE__ */ new Set();
|
|
2754
|
+
for (const g of connectorGrants) {
|
|
2755
|
+
if (!members.has(g.agentSlug)) {
|
|
2756
|
+
throw new Error(`defineBundle(${spec.slug}): connector grant targets unknown agent "${g.agentSlug}" (not a bundle member)`);
|
|
2757
|
+
}
|
|
2758
|
+
const set = grantedByMember.get(g.agentSlug) ?? /* @__PURE__ */ new Set();
|
|
2759
|
+
for (const action of g.actions) {
|
|
2760
|
+
const full = action.includes(".") ? action : `${g.provider}.${action}`;
|
|
2761
|
+
set.add(full);
|
|
2762
|
+
allGranted.add(full);
|
|
2763
|
+
}
|
|
2764
|
+
grantedByMember.set(g.agentSlug, set);
|
|
2765
|
+
}
|
|
2766
|
+
const agents = spec.agents.map((a) => {
|
|
2767
|
+
const granted = grantedByMember.get(a.slug);
|
|
2768
|
+
if (!granted || granted.size === 0) return a;
|
|
2769
|
+
const extra = [...granted].filter((n) => !a.head.skillNames.includes(n));
|
|
2770
|
+
return extra.length ? { ...a, head: { ...a.head, skillNames: [...a.head.skillNames, ...extra] } } : a;
|
|
2771
|
+
});
|
|
2772
|
+
const requireResolvable = (name, allowed, where) => {
|
|
2773
|
+
if (handles.has(name)) return;
|
|
2774
|
+
if (allowed?.has(name)) return;
|
|
2775
|
+
if (isConnectorLooking(name)) {
|
|
2776
|
+
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`);
|
|
2777
|
+
}
|
|
2778
|
+
throw new Error(`defineBundle(${spec.slug}): ${where} references skill/tool "${name}" with no first-party handle in the bundle`);
|
|
2779
|
+
};
|
|
2780
|
+
for (const a of agents) {
|
|
2781
|
+
const granted = grantedByMember.get(a.slug);
|
|
2782
|
+
for (const name of a.head.skillNames) requireResolvable(name, granted, `agent "${a.slug}"`);
|
|
2783
|
+
for (const d of a.head.delegateSlugs) {
|
|
2784
|
+
if (!members.has(d) && !requiredSlugs.has(d)) {
|
|
2785
|
+
throw new Error(`defineBundle(${spec.slug}): agent "${a.slug}" delegates to "${d}", which is neither a bundle member nor a declared \`requires\` dependency`);
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
const allRules = [...spec.rules ?? [], ...spec.agents.flatMap((a) => a.rules ?? [])];
|
|
2790
|
+
for (const r of allRules) {
|
|
2791
|
+
for (const t of ruleToolRefs(r)) {
|
|
2792
|
+
if (t.startsWith("agent-")) continue;
|
|
2793
|
+
requireResolvable(t, allGranted, `rule "${r.id}"`);
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
const allWorkflows = [...spec.workflows ?? [], ...spec.agents.flatMap((a) => a.workflow ? [a.workflow] : [])];
|
|
2797
|
+
for (const w of allWorkflows) {
|
|
2798
|
+
for (const step of w.steps) {
|
|
2799
|
+
if (step.tool !== void 0) requireResolvable(step.tool, allGranted, `workflow "${w.name}" step "${step.id}"`);
|
|
2800
|
+
assertNoCredRefs(step.args, `workflow "${w.name}" step "${step.id}" args`);
|
|
2801
|
+
assertNoCredRefs(step.input, `workflow "${w.name}" step "${step.id}" input`);
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
return {
|
|
2805
|
+
slug: spec.slug,
|
|
2806
|
+
...spec.title ? { title: spec.title } : {},
|
|
2807
|
+
agents,
|
|
2808
|
+
// members carry any granted connector action names in skillNames
|
|
2809
|
+
rules: spec.rules ?? [],
|
|
2810
|
+
// SWARM-scoped only (per-agent rules stay on the AgentDefs)
|
|
2811
|
+
workflows: spec.workflows ?? [],
|
|
2812
|
+
// SWARM-scoped only
|
|
2813
|
+
connectorGrants,
|
|
2814
|
+
requires
|
|
2815
|
+
};
|
|
2816
|
+
}
|
|
2817
|
+
function mergeBundle(cfg, bundle) {
|
|
2818
|
+
const existing = new Set(cfg.agents.map((a) => a.slug));
|
|
2819
|
+
for (const a of bundle.agents) {
|
|
2820
|
+
if (existing.has(a.slug)) {
|
|
2821
|
+
throw new Error(`mergeBundle(${bundle.slug}): agent "${a.slug}" already exists in the swarm config \u2014 bundle members must not shadow host agents`);
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
return {
|
|
2825
|
+
...cfg,
|
|
2826
|
+
agents: [...cfg.agents, ...bundle.agents],
|
|
2827
|
+
rules: [...cfg.rules ?? [], ...bundle.rules],
|
|
2828
|
+
workflows: [...cfg.workflows ?? [], ...bundle.workflows]
|
|
2829
|
+
};
|
|
2830
|
+
}
|
|
2831
|
+
function toBundleContent(def) {
|
|
2832
|
+
return {
|
|
2833
|
+
slug: def.slug,
|
|
2834
|
+
...def.title ? { title: def.title } : {},
|
|
2835
|
+
agents: def.agents.map((a) => {
|
|
2836
|
+
const { version: _version, ...content } = a.head;
|
|
2837
|
+
return content;
|
|
2838
|
+
}),
|
|
2839
|
+
rules: def.rules,
|
|
2840
|
+
workflows: def.workflows,
|
|
2841
|
+
connectorGrants: def.connectorGrants,
|
|
2842
|
+
requires: def.requires
|
|
2843
|
+
};
|
|
2844
|
+
}
|
|
1356
2845
|
var ASK_TOOL_NAME = "ask";
|
|
1357
2846
|
var askFieldSchema = import_zod4.z.object({
|
|
1358
2847
|
kind: import_zod4.z.enum(["confirm", "buttons", "select", "multiselect", "number", "text", "textarea"]),
|
|
@@ -1395,23 +2884,63 @@ function buildAskMastraTool() {
|
|
|
1395
2884
|
function defineSwarm(cfg) {
|
|
1396
2885
|
const seedable = cfg.storage;
|
|
1397
2886
|
for (const a of cfg.agents) seedable.seedAgent?.(a.head);
|
|
2887
|
+
const rules = [...cfg.rules ?? [], ...cfg.agents.flatMap((a) => a.rules ?? [])];
|
|
2888
|
+
const workflows = [...cfg.workflows ?? [], ...cfg.agents.flatMap((a) => a.workflow ? [a.workflow] : [])];
|
|
2889
|
+
const namedWorkflows = workflows.filter((w) => w.compliance === "strict" && w.scopeAgent === void 0);
|
|
2890
|
+
const agentWorkflows = {};
|
|
2891
|
+
for (const w of workflows) if (w.compliance === "strict" && w.scopeAgent !== void 0) agentWorkflows[w.scopeAgent] = w;
|
|
2892
|
+
const hasSoft = rules.some((r) => r.level === "advise") || workflows.some((w) => w.compliance === "advisory" && !!w.description);
|
|
2893
|
+
const softPolicy = hasSoft ? (slug) => softPolicyFor(slug, rules, workflows) : void 0;
|
|
2894
|
+
const policy = cfg.toolApproval ?? { mode: "flag" };
|
|
2895
|
+
const enforceTool = rules.filter((r) => r.seam === "tool" && r.level === "enforce");
|
|
2896
|
+
const enforceGen = rules.filter((r) => r.seam === "generation" && r.level === "enforce");
|
|
2897
|
+
const composedHooks = { ...cfg.hooks };
|
|
2898
|
+
if (enforceTool.length) composedHooks.preToolCall = composeToolHooks({ rules: enforceTool, host: cfg.hooks?.preToolCall, policy });
|
|
2899
|
+
if (enforceGen.length) composedHooks.preGeneration = composeGenerationHooks({ rules: enforceGen, host: cfg.hooks?.preGeneration });
|
|
1398
2900
|
const engine = new SwarmEngine({
|
|
2901
|
+
softPolicy,
|
|
2902
|
+
workflows: namedWorkflows,
|
|
2903
|
+
agentWorkflows,
|
|
1399
2904
|
storage: cfg.storage,
|
|
1400
2905
|
model: allowListModelProvider({ allow: cfg.models.allow }),
|
|
1401
2906
|
modelFactory: cfg.modelFactory,
|
|
2907
|
+
// SP10: thread the optional cheap-model router onto the engine. The allow-list above still validates every
|
|
2908
|
+
// resolved model — incl. a routed tier model. Undefined ⇒ no routing (identical to today).
|
|
2909
|
+
tier: cfg.models.tier,
|
|
1402
2910
|
cost: cfg.cost,
|
|
1403
2911
|
// Per-swarm skill registry, built from the agents passed in. No global state.
|
|
1404
2912
|
resolveSkill: buildSkillResolver(cfg.agents),
|
|
2913
|
+
// PR2: opt-in connector-tools resolver (built by the host via materializeConnectors). Undefined ⇒ no-op.
|
|
2914
|
+
connectorTools: cfg.connectorTools,
|
|
1405
2915
|
telemetry: resolveTelemetry(cfg.telemetry),
|
|
1406
2916
|
mastraStore: cfg.mastraStore,
|
|
1407
2917
|
memory: cfg.memory,
|
|
1408
2918
|
pageContext: cfg.pageContext,
|
|
1409
2919
|
scratchpad: cfg.scratchpad,
|
|
1410
|
-
recallLane: cfg.recallLane
|
|
2920
|
+
recallLane: cfg.recallLane,
|
|
2921
|
+
// SP2 + SP5: build the decision-hook dispatcher once per swarm, baking in the non-removable tool-approval
|
|
2922
|
+
// policy. Always present (allow-all hooks + `{ mode: "flag" }` policy when unconfigured, so the engine's
|
|
2923
|
+
// seams stay uniform — no null-checks on the hot path). The dispatcher combines policy + the per-tool flag +
|
|
2924
|
+
// the optional `preToolCall` hook into the effective ToolDecision.
|
|
2925
|
+
hooks: (0, import_hooks3.createHookDispatcher)(composedHooks, cfg.toolApproval),
|
|
2926
|
+
// SP5: also pass the policy standalone so an engine built without a dispatcher (direct construction) still
|
|
2927
|
+
// enforces it; redundant here (the dispatcher already carries it) but keeps EngineOpts self-describing.
|
|
2928
|
+
toolApproval: cfg.toolApproval,
|
|
2929
|
+
// SP15: thread the optional SecretResolver onto the engine, which injects it per-run on the RequestContext.
|
|
2930
|
+
secrets: cfg.secrets,
|
|
2931
|
+
// SP3: best-effort per-event observer (metering debit/settle) — transport-agnostic, fired in run + resume.
|
|
2932
|
+
onEvent: cfg.onEvent,
|
|
2933
|
+
verifyCompletion: cfg.verifyCompletion,
|
|
2934
|
+
// FR-003: per-run lifecycle hooks (seed `ctx.state` at run start, drain at run end).
|
|
2935
|
+
onRunStart: cfg.onRunStart,
|
|
2936
|
+
onRunEnd: cfg.onRunEnd
|
|
1411
2937
|
});
|
|
1412
2938
|
return { engine };
|
|
1413
2939
|
}
|
|
1414
2940
|
|
|
2941
|
+
// src/index.ts
|
|
2942
|
+
var import_hooks4 = require("@nightowlsdev/hooks");
|
|
2943
|
+
|
|
1415
2944
|
// src/storage/memory.ts
|
|
1416
2945
|
var InMemoryStorage = class {
|
|
1417
2946
|
evts = [];
|
|
@@ -1428,6 +2957,8 @@ var InMemoryStorage = class {
|
|
|
1428
2957
|
heads = /* @__PURE__ */ new Map();
|
|
1429
2958
|
// key: tenantId:slug -> version
|
|
1430
2959
|
pads = /* @__PURE__ */ new Map();
|
|
2960
|
+
threadRows = /* @__PURE__ */ new Map();
|
|
2961
|
+
// FR-009
|
|
1431
2962
|
seedAgent(v, tenantId = "default") {
|
|
1432
2963
|
this.agentRows.set(`${tenantId}:${v.slug}:${v.version}`, v);
|
|
1433
2964
|
this.heads.set(`${tenantId}:${v.slug}`, v.version);
|
|
@@ -1436,7 +2967,7 @@ var InMemoryStorage = class {
|
|
|
1436
2967
|
this.suspends.set(`${tenantId}:${followupId}`, { runId, toolCallId });
|
|
1437
2968
|
}
|
|
1438
2969
|
markFollowupAnswered(followupId, tenantId) {
|
|
1439
|
-
this.suspends.delete(`${tenantId}:${followupId}`);
|
|
2970
|
+
return this.suspends.delete(`${tenantId}:${followupId}`);
|
|
1440
2971
|
}
|
|
1441
2972
|
/** Test/host helper: read a run row (the RunStore interface is write-mostly). */
|
|
1442
2973
|
getRun(runId) {
|
|
@@ -1511,6 +3042,18 @@ var InMemoryStorage = class {
|
|
|
1511
3042
|
},
|
|
1512
3043
|
getWaitpoint: async (followupId) => this.waitpoints.get(followupId) ?? null
|
|
1513
3044
|
};
|
|
3045
|
+
// FR-009: idempotent thread-row creation. The dev store has no FK, so `messages.append` never threw `unknown
|
|
3046
|
+
// thread` here — this implements the contract so the engine's run-start ensure works against the in-memory store
|
|
3047
|
+
// too, and a host can read back the recorded thread (getThread) in tests.
|
|
3048
|
+
threads = {
|
|
3049
|
+
ensure: async ({ id, orgId, userId, projectId }) => {
|
|
3050
|
+
if (!this.threadRows.has(id)) this.threadRows.set(id, { id, orgId, userId, projectId });
|
|
3051
|
+
}
|
|
3052
|
+
};
|
|
3053
|
+
/** Test/host helper: read a recorded thread row. */
|
|
3054
|
+
getThread(id) {
|
|
3055
|
+
return this.threadRows.get(id);
|
|
3056
|
+
}
|
|
1514
3057
|
messages = {
|
|
1515
3058
|
append: async (m) => {
|
|
1516
3059
|
this.msgs.push(m);
|
|
@@ -1550,21 +3093,119 @@ var InMemoryStorage = class {
|
|
|
1550
3093
|
};
|
|
1551
3094
|
};
|
|
1552
3095
|
|
|
3096
|
+
// src/run-agent.ts
|
|
3097
|
+
function uid() {
|
|
3098
|
+
return globalThis.crypto?.randomUUID?.() ?? `id-${Math.random().toString(36).slice(2)}-${Date.now().toString(36)}`;
|
|
3099
|
+
}
|
|
3100
|
+
async function drainTrajectory(stream) {
|
|
3101
|
+
const events = [];
|
|
3102
|
+
let output = "";
|
|
3103
|
+
for await (const e of stream) {
|
|
3104
|
+
events.push(e);
|
|
3105
|
+
if (e.type === "swarm.message") {
|
|
3106
|
+
const d = e.data;
|
|
3107
|
+
if (d.role === "assistant") output += d.delta ?? d.text ?? "";
|
|
3108
|
+
}
|
|
3109
|
+
}
|
|
3110
|
+
return { events, output };
|
|
3111
|
+
}
|
|
3112
|
+
async function runToTrajectory(target, input, ctx) {
|
|
3113
|
+
const engine = "engine" in target ? target.engine : target;
|
|
3114
|
+
const runInput = typeof input === "string" ? { message: input } : input;
|
|
3115
|
+
const full = ephemeralCtx(ctx?.agentSlug ?? "agent", ctx);
|
|
3116
|
+
return drainTrajectory(engine.run(runInput, full));
|
|
3117
|
+
}
|
|
3118
|
+
function ephemeralCtx(agentSlug, over) {
|
|
3119
|
+
return {
|
|
3120
|
+
tenantId: over?.tenantId ?? "default",
|
|
3121
|
+
userId: over?.userId ?? "local",
|
|
3122
|
+
agentSlug: over?.agentSlug ?? agentSlug,
|
|
3123
|
+
runId: over?.runId ?? uid(),
|
|
3124
|
+
threadId: over?.threadId ?? uid(),
|
|
3125
|
+
...over?.agentVersion !== void 0 ? { agentVersion: over.agentVersion } : {}
|
|
3126
|
+
};
|
|
3127
|
+
}
|
|
3128
|
+
function buildSingleAgentSwarm(def, opts) {
|
|
3129
|
+
const t = opts.models?.tier;
|
|
3130
|
+
const tierAllow = t ? [t.tiers.swift, ...t.tiers.genius ? [t.tiers.genius] : []] : [];
|
|
3131
|
+
const allow = opts.models?.allow ?? [def.head.modelId, ...tierAllow];
|
|
3132
|
+
const storage = opts.storage ?? new InMemoryStorage();
|
|
3133
|
+
const cfg = {
|
|
3134
|
+
storage,
|
|
3135
|
+
agents: [def],
|
|
3136
|
+
models: { allow, ...opts.models?.tier ? { tier: opts.models.tier } : {} },
|
|
3137
|
+
modelFactory: opts.modelFactory,
|
|
3138
|
+
cost: { maxSteps: 50, maxCostUsd: 10, ...opts.cost },
|
|
3139
|
+
...opts.telemetry !== void 0 ? { telemetry: opts.telemetry } : {},
|
|
3140
|
+
...opts.memory !== void 0 ? { memory: opts.memory } : {},
|
|
3141
|
+
...opts.hooks !== void 0 ? { hooks: opts.hooks } : {},
|
|
3142
|
+
...opts.toolApproval !== void 0 ? { toolApproval: opts.toolApproval } : {},
|
|
3143
|
+
...opts.secrets !== void 0 ? { secrets: opts.secrets } : {},
|
|
3144
|
+
...opts.onEvent !== void 0 ? { onEvent: opts.onEvent } : {},
|
|
3145
|
+
...opts.onRunStart !== void 0 ? { onRunStart: opts.onRunStart } : {},
|
|
3146
|
+
...opts.onRunEnd !== void 0 ? { onRunEnd: opts.onRunEnd } : {},
|
|
3147
|
+
...opts.pageContext !== void 0 ? { pageContext: opts.pageContext } : {},
|
|
3148
|
+
...opts.mastraStore !== void 0 ? { mastraStore: opts.mastraStore } : {}
|
|
3149
|
+
};
|
|
3150
|
+
return defineSwarm(cfg);
|
|
3151
|
+
}
|
|
3152
|
+
async function runAgent(def, input, opts) {
|
|
3153
|
+
const swarm = buildSingleAgentSwarm(def, opts);
|
|
3154
|
+
const runInput = typeof input === "string" ? { message: input } : input;
|
|
3155
|
+
const ctx = ephemeralCtx(def.head.slug, { ...opts.ctx, agentSlug: def.head.slug });
|
|
3156
|
+
return drainTrajectory(swarm.engine.run(runInput, ctx));
|
|
3157
|
+
}
|
|
3158
|
+
|
|
1553
3159
|
// src/auth.ts
|
|
1554
3160
|
var customAuth = (fn) => ({ authenticate: fn });
|
|
1555
3161
|
|
|
3162
|
+
// src/rate-limit.ts
|
|
3163
|
+
function decideFixedWindow(prev, cfg, nowSec) {
|
|
3164
|
+
const windowValid = prev && nowSec - prev.windowStartSec < cfg.windowSec;
|
|
3165
|
+
const state = windowValid ? { count: prev.count + 1, windowStartSec: prev.windowStartSec } : { count: 1, windowStartSec: nowSec };
|
|
3166
|
+
const resetSec = Math.max(0, state.windowStartSec + cfg.windowSec - nowSec);
|
|
3167
|
+
const allow = state.count <= cfg.max;
|
|
3168
|
+
return { decision: { allow, remaining: Math.max(0, cfg.max - state.count), resetSec }, state };
|
|
3169
|
+
}
|
|
3170
|
+
function createInMemoryRateLimitStore() {
|
|
3171
|
+
const states = /* @__PURE__ */ new Map();
|
|
3172
|
+
let lastPruneSec = 0;
|
|
3173
|
+
return {
|
|
3174
|
+
async hit(key, cfg, nowSec) {
|
|
3175
|
+
if (nowSec > lastPruneSec) {
|
|
3176
|
+
for (const [k, s] of states) if (nowSec - s.windowStartSec >= cfg.windowSec) states.delete(k);
|
|
3177
|
+
lastPruneSec = nowSec;
|
|
3178
|
+
}
|
|
3179
|
+
const { decision, state } = decideFixedWindow(states.get(key) ?? null, cfg, nowSec);
|
|
3180
|
+
states.set(key, state);
|
|
3181
|
+
return decision;
|
|
3182
|
+
}
|
|
3183
|
+
};
|
|
3184
|
+
}
|
|
3185
|
+
function rateConfig(max, windowSec, fallbackMax) {
|
|
3186
|
+
const m = Number.isFinite(max) && max > 0 ? Math.floor(max) : fallbackMax;
|
|
3187
|
+
return { windowSec, max: m };
|
|
3188
|
+
}
|
|
3189
|
+
|
|
1556
3190
|
// src/index.ts
|
|
1557
3191
|
var VERSION = "0.0.0";
|
|
1558
3192
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1559
3193
|
0 && (module.exports = {
|
|
3194
|
+
ALLOW,
|
|
3195
|
+
ALLOW_TOOL,
|
|
1560
3196
|
ASK_TOOL_NAME,
|
|
3197
|
+
AgentMutationForbidden,
|
|
1561
3198
|
CapturingExporter,
|
|
3199
|
+
ClientToolError,
|
|
1562
3200
|
CostGovernor,
|
|
3201
|
+
DEFAULT_READ_ONLY_TOOLS,
|
|
1563
3202
|
DelegateBudgets,
|
|
1564
3203
|
GUARDRAILS,
|
|
3204
|
+
HookDispatcher,
|
|
1565
3205
|
InMemoryContainerFloor,
|
|
1566
3206
|
InMemoryStorage,
|
|
1567
3207
|
PRICE_TABLE,
|
|
3208
|
+
ReserveDenied,
|
|
1568
3209
|
RowCache,
|
|
1569
3210
|
SCRATCHPAD_MAX_ENTRY_CHARS,
|
|
1570
3211
|
SCRATCHPAD_MAX_KEYS,
|
|
@@ -1572,17 +3213,43 @@ var VERSION = "0.0.0";
|
|
|
1572
3213
|
SwarmEngine,
|
|
1573
3214
|
VERSION,
|
|
1574
3215
|
allowListModelProvider,
|
|
3216
|
+
ask,
|
|
3217
|
+
assertActorMayMutateDefinition,
|
|
3218
|
+
buildSingleAgentSwarm,
|
|
1575
3219
|
buildSkillResolver,
|
|
3220
|
+
composePolicyPrompt,
|
|
1576
3221
|
composeSystemPrompt,
|
|
1577
3222
|
compositeTelemetry,
|
|
1578
3223
|
containerFloor,
|
|
3224
|
+
createHookDispatcher,
|
|
3225
|
+
createInMemoryRateLimitStore,
|
|
3226
|
+
createRunState,
|
|
1579
3227
|
customAuth,
|
|
1580
3228
|
customTelemetry,
|
|
3229
|
+
decideFixedWindow,
|
|
1581
3230
|
defineAgent,
|
|
3231
|
+
defineBundle,
|
|
3232
|
+
defineClientTool,
|
|
3233
|
+
defineHook,
|
|
3234
|
+
defineRule,
|
|
1582
3235
|
defineSkill,
|
|
1583
3236
|
defineSwarm,
|
|
1584
3237
|
defineTool,
|
|
3238
|
+
defineWorkflow,
|
|
3239
|
+
deny,
|
|
3240
|
+
drainTrajectory,
|
|
1585
3241
|
ev,
|
|
1586
3242
|
isEvent,
|
|
1587
|
-
|
|
3243
|
+
isTierSentinel,
|
|
3244
|
+
mergeBundle,
|
|
3245
|
+
priceUsage,
|
|
3246
|
+
rateConfig,
|
|
3247
|
+
resolveTelemetry,
|
|
3248
|
+
resolveTier,
|
|
3249
|
+
runAgent,
|
|
3250
|
+
runToTrajectory,
|
|
3251
|
+
sumBreakdowns,
|
|
3252
|
+
sumTurnUsage,
|
|
3253
|
+
tierModelId,
|
|
3254
|
+
toBundleContent
|
|
1588
3255
|
});
|