@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.js
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
// src/types.ts
|
|
2
|
+
function assertActorMayMutateDefinition(actor) {
|
|
3
|
+
if (actor.type === "agent") {
|
|
4
|
+
throw new AgentMutationForbidden(actor.agentSlug);
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
var AgentMutationForbidden = class extends Error {
|
|
8
|
+
code = "AGENT_MUTATION_FORBIDDEN";
|
|
9
|
+
constructor(agentSlug) {
|
|
10
|
+
super(`agent principal "${agentSlug}" may not mutate an agent definition (publish/rollback)`);
|
|
11
|
+
this.name = "AgentMutationForbidden";
|
|
12
|
+
}
|
|
13
|
+
};
|
|
2
14
|
var SCRATCHPAD_MAX_ENTRY_CHARS = 4e3;
|
|
3
15
|
var SCRATCHPAD_MAX_KEYS = 64;
|
|
4
16
|
|
|
@@ -13,11 +25,269 @@ function isEvent(e, type) {
|
|
|
13
25
|
// src/define.ts
|
|
14
26
|
import { z as z4 } from "zod";
|
|
15
27
|
import { createTool as createTool4 } from "@mastra/core/tools";
|
|
28
|
+
import { createHookDispatcher } from "@nightowlsdev/hooks";
|
|
16
29
|
|
|
17
30
|
// src/engine.ts
|
|
18
31
|
import { Mastra } from "@mastra/core/mastra";
|
|
19
32
|
import { InMemoryStore } from "@mastra/core/storage";
|
|
20
33
|
import { RequestContext } from "@mastra/core/request-context";
|
|
34
|
+
import { HookDispatcher } from "@nightowlsdev/hooks";
|
|
35
|
+
|
|
36
|
+
// src/tool-gate.ts
|
|
37
|
+
var TOOL_GATE_KEY = "__nightowlsdev_toolGate";
|
|
38
|
+
var TOOL_EXECUTORS = /* @__PURE__ */ new WeakMap();
|
|
39
|
+
function setToolExecutor(handle, exec) {
|
|
40
|
+
TOOL_EXECUTORS.set(handle, exec);
|
|
41
|
+
}
|
|
42
|
+
function getToolExecutor(handle) {
|
|
43
|
+
return TOOL_EXECUTORS.get(handle);
|
|
44
|
+
}
|
|
45
|
+
var ToolBlockedError = class extends Error {
|
|
46
|
+
constructor(toolName, reason) {
|
|
47
|
+
super(`tool "${toolName}" blocked: ${reason}`);
|
|
48
|
+
this.toolName = toolName;
|
|
49
|
+
this.reason = reason;
|
|
50
|
+
this.name = "ToolBlockedError";
|
|
51
|
+
}
|
|
52
|
+
toolName;
|
|
53
|
+
reason;
|
|
54
|
+
blocked = true;
|
|
55
|
+
};
|
|
56
|
+
function approvalSuspendPayload(args) {
|
|
57
|
+
const prompt = args.reason?.trim() ? `Approve \`${args.toolName}\`? ${args.reason.trim()}` : `Approve running \`${args.toolName}\`?`;
|
|
58
|
+
return {
|
|
59
|
+
to: "user",
|
|
60
|
+
prompt,
|
|
61
|
+
field: { kind: "confirm", confirmLabel: "Approve", rejectLabel: "Reject" },
|
|
62
|
+
asker: args.asker,
|
|
63
|
+
kind: "approval",
|
|
64
|
+
toolName: args.toolName
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function isApproved(answer) {
|
|
68
|
+
if (typeof answer === "boolean") return answer;
|
|
69
|
+
if (typeof answer === "string") {
|
|
70
|
+
return /^(y|yes|approve|approved|ok|true|confirm|confirmed)$/i.test(answer.trim());
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
function gateErrMessage(err) {
|
|
75
|
+
return err instanceof Error ? err.message : String(err);
|
|
76
|
+
}
|
|
77
|
+
async function executeToolWithGate(opts) {
|
|
78
|
+
let decision;
|
|
79
|
+
try {
|
|
80
|
+
decision = await opts.gate(opts.ev);
|
|
81
|
+
} catch (err) {
|
|
82
|
+
return { ok: false, error: gateErrMessage(err), reason: gateErrMessage(err) };
|
|
83
|
+
}
|
|
84
|
+
if (decision.action === "deny") return { ok: false, error: decision.reason, reason: decision.reason };
|
|
85
|
+
if (decision.action === "ask") return { ok: false, suspended: true, reason: decision.reason };
|
|
86
|
+
try {
|
|
87
|
+
return { ok: true, result: await opts.run() };
|
|
88
|
+
} catch (err) {
|
|
89
|
+
return { ok: false, error: gateErrMessage(err) };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function toolPreCallEvent(args) {
|
|
93
|
+
return {
|
|
94
|
+
runId: args.runId,
|
|
95
|
+
tenantId: args.tenantId,
|
|
96
|
+
agentSlug: args.agentSlug,
|
|
97
|
+
toolName: args.toolName,
|
|
98
|
+
origin: args.origin,
|
|
99
|
+
needsApproval: args.needsApproval,
|
|
100
|
+
args: args.args
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// src/secrets.ts
|
|
105
|
+
var SECRET_RESOLVER_KEY = "__nightowlsdev_secretResolver";
|
|
106
|
+
function bindSecrets(resolver, ctx) {
|
|
107
|
+
return {
|
|
108
|
+
resolve: (ref) => resolver ? resolver.resolve(ref, ctx) : Promise.resolve(void 0)
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// src/run-state.ts
|
|
113
|
+
var RUN_STATE_KEY = "__nightowls_run_state__";
|
|
114
|
+
function snapshotCopy(store) {
|
|
115
|
+
const plain = Object.fromEntries(store);
|
|
116
|
+
try {
|
|
117
|
+
return (globalThis.structuredClone ?? ((v) => JSON.parse(JSON.stringify(v))))(plain);
|
|
118
|
+
} catch {
|
|
119
|
+
try {
|
|
120
|
+
return JSON.parse(JSON.stringify(plain));
|
|
121
|
+
} catch {
|
|
122
|
+
return plain;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function createRunState(seed) {
|
|
127
|
+
const m = new Map(seed ? Object.entries(seed) : void 0);
|
|
128
|
+
return {
|
|
129
|
+
get: (key) => m.get(key),
|
|
130
|
+
set: (key, value) => void m.set(key, value),
|
|
131
|
+
has: (key) => m.has(key),
|
|
132
|
+
delete: (key) => m.delete(key),
|
|
133
|
+
entries: () => snapshotCopy(m)
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// src/step-driver.ts
|
|
138
|
+
function initialWorkflowState(wf) {
|
|
139
|
+
return { workflow: wf.name, cursor: wf.start, outputs: {}, generationIndex: 0 };
|
|
140
|
+
}
|
|
141
|
+
function resolveRef(ref, state, input) {
|
|
142
|
+
if (ref === "input") return input.message;
|
|
143
|
+
if (ref.startsWith("steps.")) return state.outputs[ref.slice("steps.".length)];
|
|
144
|
+
return void 0;
|
|
145
|
+
}
|
|
146
|
+
function resolveValue(v, state, input) {
|
|
147
|
+
if (v && typeof v === "object" && "$ref" in v) {
|
|
148
|
+
const ref = String(v.$ref);
|
|
149
|
+
if (ref.startsWith("steps.")) {
|
|
150
|
+
const id = ref.slice("steps.".length);
|
|
151
|
+
if (!(id in state.outputs)) {
|
|
152
|
+
throw new Error(`workflow $ref "${ref}" references step "${id}" which has not run (skipped branch or forward reference)`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return resolveRef(ref, state, input);
|
|
156
|
+
}
|
|
157
|
+
return v;
|
|
158
|
+
}
|
|
159
|
+
function resolveMap(o, state, input) {
|
|
160
|
+
if (!o) return void 0;
|
|
161
|
+
const out = {};
|
|
162
|
+
for (const [k, v] of Object.entries(o)) out[k] = resolveValue(v, state, input);
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
function agentMessage(step, resolvedInput) {
|
|
166
|
+
const base2 = step.instruction ?? "";
|
|
167
|
+
if (resolvedInput && Object.keys(resolvedInput).length) return `${base2}
|
|
168
|
+
|
|
169
|
+
Context:
|
|
170
|
+
${JSON.stringify(resolvedInput)}`;
|
|
171
|
+
return base2;
|
|
172
|
+
}
|
|
173
|
+
var DEAD_END = /* @__PURE__ */ Symbol("dead-end");
|
|
174
|
+
function nextStep(step, state, input) {
|
|
175
|
+
if (step.next === void 0) return void 0;
|
|
176
|
+
if (typeof step.next === "string") return step.next;
|
|
177
|
+
for (const t of step.next) {
|
|
178
|
+
if (!t.when) return t.to;
|
|
179
|
+
const v = resolveRef(t.when.$ref, state, input);
|
|
180
|
+
if (t.when.exists !== void 0) {
|
|
181
|
+
if (v !== void 0 === t.when.exists) return t.to;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (t.when.eq !== void 0) {
|
|
185
|
+
if (v === t.when.eq) return t.to;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
return t.to;
|
|
189
|
+
}
|
|
190
|
+
return DEAD_END;
|
|
191
|
+
}
|
|
192
|
+
var StepDriver = class {
|
|
193
|
+
constructor(wf, deps) {
|
|
194
|
+
this.wf = wf;
|
|
195
|
+
this.deps = deps;
|
|
196
|
+
}
|
|
197
|
+
wf;
|
|
198
|
+
deps;
|
|
199
|
+
ts = 0;
|
|
200
|
+
base(ctx) {
|
|
201
|
+
return { runId: ctx.runId, agentSlug: ctx.agentSlug, ts: this.deps.nextTs ? this.deps.nextTs() : this.ts++ };
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Drive the workflow from `state` (fresh or resumed). Yields the run's SwarmEvents. Returns a `DriveOutcome`
|
|
205
|
+
* so the engine can finalize. B2 scope: linear `agent`/`tool` steps + `$ref` wiring + per-step snapshot.
|
|
206
|
+
*/
|
|
207
|
+
async *drive(state, ctx, input) {
|
|
208
|
+
const byId = new Map(this.wf.steps.map((s) => [s.id, s]));
|
|
209
|
+
let guard = 0;
|
|
210
|
+
let retryStep = "";
|
|
211
|
+
let retriesLeft = 0;
|
|
212
|
+
const budget = this.wf.steps.length * 8 + 8;
|
|
213
|
+
while (true) {
|
|
214
|
+
if (guard++ > budget) return { kind: "failed", stage: "workflow", message: "step budget exceeded" };
|
|
215
|
+
const step = byId.get(state.cursor);
|
|
216
|
+
if (!step) return { kind: "failed", stage: "workflow", message: `unknown step "${state.cursor}"` };
|
|
217
|
+
yield ev("swarm.status", this.base(ctx), { state: step.tool ? "tool" : "thinking", note: `step:${step.id}` });
|
|
218
|
+
let stepError;
|
|
219
|
+
if (step.agent !== void 0) {
|
|
220
|
+
try {
|
|
221
|
+
const msg = agentMessage(step, resolveMap(step.input, state, input));
|
|
222
|
+
const { text } = yield* this.deps.runAgentStep(step.agent, msg, state.generationIndex, ctx);
|
|
223
|
+
state.outputs[step.id] = text;
|
|
224
|
+
state.generationIndex += 1;
|
|
225
|
+
} catch (err) {
|
|
226
|
+
if (err && typeof err === "object" && "stage" in err) throw err;
|
|
227
|
+
stepError = err instanceof Error ? err.message : String(err);
|
|
228
|
+
}
|
|
229
|
+
} else if (step.tool !== void 0) {
|
|
230
|
+
let args;
|
|
231
|
+
try {
|
|
232
|
+
args = resolveMap(step.args, state, input) ?? {};
|
|
233
|
+
} catch (err) {
|
|
234
|
+
stepError = err instanceof Error ? err.message : String(err);
|
|
235
|
+
}
|
|
236
|
+
if (args !== void 0) {
|
|
237
|
+
const toolCallId = `${ctx.runId}:wf:${step.id}`;
|
|
238
|
+
yield ev("swarm.tool_call", this.base(ctx), { toolCallId, name: step.tool, args, needsApproval: false });
|
|
239
|
+
const r = await this.deps.runToolStep(step.tool, args, ctx);
|
|
240
|
+
yield ev("swarm.tool_result", this.base(ctx), { toolCallId, ok: r.ok, result: r.result, error: r.error });
|
|
241
|
+
if (r.ok) state.outputs[step.id] = r.result;
|
|
242
|
+
else if (r.suspended) {
|
|
243
|
+
const followupId = `${ctx.runId}:wf:${step.id}`;
|
|
244
|
+
yield ev("swarm.question", this.base(ctx), { followupId, toolCallId: followupId, to: "user", prompt: r.reason ?? `Approve "${step.tool}"?`, field: { kind: "confirm" } });
|
|
245
|
+
state.pending = { kind: "approval", stepId: step.id, followupId, toolCallId: followupId };
|
|
246
|
+
await this.deps.saveState(ctx.runId, state);
|
|
247
|
+
return { kind: "suspended", state };
|
|
248
|
+
} else stepError = r.error ?? r.reason ?? "blocked";
|
|
249
|
+
}
|
|
250
|
+
} else if (step.human !== void 0) {
|
|
251
|
+
if (!(step.id in state.outputs)) {
|
|
252
|
+
const followupId = `${ctx.runId}:wf:${step.id}`;
|
|
253
|
+
yield ev("swarm.question", this.base(ctx), { followupId, toolCallId: followupId, to: "user", prompt: step.human.prompt, field: step.human.field });
|
|
254
|
+
state.pending = { kind: "human", stepId: step.id, followupId, toolCallId: followupId };
|
|
255
|
+
await this.deps.saveState(ctx.runId, state);
|
|
256
|
+
return { kind: "suspended", state };
|
|
257
|
+
}
|
|
258
|
+
state.pending = void 0;
|
|
259
|
+
}
|
|
260
|
+
if (stepError !== void 0) {
|
|
261
|
+
const oe = step.onError ?? "fail";
|
|
262
|
+
if (oe === "fail") return { kind: "failed", stage: "workflow", message: `step "${step.id}" failed: ${stepError}` };
|
|
263
|
+
if (typeof oe === "object" && "to" in oe) {
|
|
264
|
+
state.cursor = oe.to;
|
|
265
|
+
retryStep = "";
|
|
266
|
+
await this.deps.saveState(ctx.runId, state);
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (typeof oe === "object" && "retry" in oe) {
|
|
270
|
+
if (retryStep !== step.id) {
|
|
271
|
+
retryStep = step.id;
|
|
272
|
+
retriesLeft = oe.retry;
|
|
273
|
+
}
|
|
274
|
+
if (retriesLeft > 0) {
|
|
275
|
+
retriesLeft -= 1;
|
|
276
|
+
await this.deps.saveState(ctx.runId, state);
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
return { kind: "failed", stage: "workflow", message: `step "${step.id}" failed after retries: ${stepError}` };
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
retryStep = "";
|
|
283
|
+
const next = nextStep(step, state, input);
|
|
284
|
+
if (next === DEAD_END) return { kind: "failed", stage: "workflow", message: `no transition from step "${step.id}"` };
|
|
285
|
+
state.cursor = next ?? state.cursor;
|
|
286
|
+
await this.deps.saveState(ctx.runId, state);
|
|
287
|
+
if (next === void 0) return { kind: "done" };
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
};
|
|
21
291
|
|
|
22
292
|
// src/mastra-map.ts
|
|
23
293
|
import { Agent } from "@mastra/core/agent";
|
|
@@ -41,6 +311,16 @@ function composeSystemPrompt(row) {
|
|
|
41
311
|
{ role: "system", content: persona }
|
|
42
312
|
];
|
|
43
313
|
}
|
|
314
|
+
function composePolicyPrompt(lines) {
|
|
315
|
+
if (!lines.length) return [];
|
|
316
|
+
return [
|
|
317
|
+
{
|
|
318
|
+
role: "system",
|
|
319
|
+
content: `Policy \u2014 follow these unless the user explicitly overrides:
|
|
320
|
+
${lines.map((l) => `- ${l}`).join("\n")}`
|
|
321
|
+
}
|
|
322
|
+
];
|
|
323
|
+
}
|
|
44
324
|
function composeScratchpadPrompt(entries) {
|
|
45
325
|
const render = (section) => {
|
|
46
326
|
const rows = entries.filter((e) => e.section === section).map((e) => `- [${e.key}] (${e.author} \u2190 ${e.requestedBy}) ${e.content}`);
|
|
@@ -56,20 +336,100 @@ ${render("meta")}`
|
|
|
56
336
|
return { role: "system", content };
|
|
57
337
|
}
|
|
58
338
|
|
|
339
|
+
// src/tier.ts
|
|
340
|
+
var SENTINEL = "tier:";
|
|
341
|
+
function isTierSentinel(modelId) {
|
|
342
|
+
return typeof modelId === "string" && modelId.startsWith(SENTINEL);
|
|
343
|
+
}
|
|
344
|
+
function requestedTierFrom(modelId, cfg) {
|
|
345
|
+
const suffix = modelId.slice(SENTINEL.length).trim();
|
|
346
|
+
if (suffix === "swift" || suffix === "genius") return suffix;
|
|
347
|
+
return cfg.default ?? "swift";
|
|
348
|
+
}
|
|
349
|
+
function resolveTier(modelId, cfg, ctx) {
|
|
350
|
+
if (!isTierSentinel(modelId)) {
|
|
351
|
+
return { modelId, downgraded: false };
|
|
352
|
+
}
|
|
353
|
+
let requested = requestedTierFrom(modelId, cfg);
|
|
354
|
+
let escalated = false;
|
|
355
|
+
if (cfg.escalate) {
|
|
356
|
+
const bumped = cfg.escalate(ctx);
|
|
357
|
+
if (bumped === "genius" && requested !== "genius") {
|
|
358
|
+
requested = "genius";
|
|
359
|
+
escalated = true;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
if (requested === "genius") {
|
|
363
|
+
const geniusAllowed = cfg.allowGenius === true && typeof cfg.tiers.genius === "string";
|
|
364
|
+
if (geniusAllowed) {
|
|
365
|
+
return { modelId: cfg.tiers.genius, tier: "genius", downgraded: false, ...escalated ? { escalated: true } : {} };
|
|
366
|
+
}
|
|
367
|
+
return { modelId: cfg.tiers.swift, tier: "swift", downgraded: true, requestedTier: "genius" };
|
|
368
|
+
}
|
|
369
|
+
return { modelId: cfg.tiers.swift, tier: "swift", downgraded: false };
|
|
370
|
+
}
|
|
371
|
+
function tierModelId(modelId, cfg, ctx) {
|
|
372
|
+
if (!cfg) return modelId;
|
|
373
|
+
return resolveTier(modelId, cfg, ctx).modelId;
|
|
374
|
+
}
|
|
375
|
+
|
|
59
376
|
// src/mastra-map.ts
|
|
377
|
+
async function gateDelegation(rc, subSlug) {
|
|
378
|
+
const gate = rc.get(TOOL_GATE_KEY);
|
|
379
|
+
if (!gate) return;
|
|
380
|
+
const decision = await gate(
|
|
381
|
+
toolPreCallEvent({
|
|
382
|
+
runId: rc.get("runId") ?? "",
|
|
383
|
+
tenantId: rc.get("tenantId") ?? "default",
|
|
384
|
+
// The agent doing the delegating is the run owner / the parent in the path (the requestContext's agentSlug).
|
|
385
|
+
agentSlug: rc.get("agentSlug") ?? "",
|
|
386
|
+
toolName: `agent-${subSlug}`,
|
|
387
|
+
origin: "first-party",
|
|
388
|
+
// A delegation has no per-tool `needsApproval` flag; surface false so a "flag"-mode policy leaves it
|
|
389
|
+
// un-gated (today's behaviour) and only an "all-side-effecting" policy / an explicit hook can deny it.
|
|
390
|
+
needsApproval: false,
|
|
391
|
+
args: void 0
|
|
392
|
+
})
|
|
393
|
+
);
|
|
394
|
+
if (decision.action === "deny") {
|
|
395
|
+
throw new Error(`delegation to "${subSlug}" denied: ${decision.reason}`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
60
398
|
var MAX_DELEGATION_DEPTH = 4;
|
|
399
|
+
var CONNECTOR_TOOLS_CACHE_KEY = "__nightowls_connector_tools";
|
|
61
400
|
function memoryFor(args, row) {
|
|
62
401
|
return args.resolveMemory ? args.resolveMemory(row) : args.memory;
|
|
63
402
|
}
|
|
64
|
-
function toolsFor(args, row) {
|
|
403
|
+
function toolsFor(args, row, connectorByName) {
|
|
65
404
|
const out = { ...args.builtinTools ?? {} };
|
|
66
405
|
for (const name of row.skillNames) {
|
|
67
|
-
const skill = args.resolveSkill(name);
|
|
406
|
+
const skill = args.resolveSkill(name) ?? connectorByName?.[name];
|
|
68
407
|
const mt = skill && __getMastraTool(skill);
|
|
69
408
|
if (mt) out[name] = mt;
|
|
70
409
|
}
|
|
71
410
|
return out;
|
|
72
411
|
}
|
|
412
|
+
async function connectorByNameFor(args, rc, agentSlug) {
|
|
413
|
+
if (!args.connectorTools) return {};
|
|
414
|
+
const cached = rc.get(CONNECTOR_TOOLS_CACHE_KEY);
|
|
415
|
+
if (cached) return cached;
|
|
416
|
+
const resolve = args.connectorTools;
|
|
417
|
+
const build = (async () => {
|
|
418
|
+
const ctx = {
|
|
419
|
+
tenantId: rc.get("tenantId") ?? "default",
|
|
420
|
+
userId: rc.get("userId") ?? "",
|
|
421
|
+
runId: rc.get("runId") ?? "",
|
|
422
|
+
agentSlug,
|
|
423
|
+
// informational — materialize is tenant-scoped; first caller's slug seeds the shared cache
|
|
424
|
+
threadId: rc.get("threadId") ?? ""
|
|
425
|
+
};
|
|
426
|
+
const out = {};
|
|
427
|
+
for (const t of await resolve(ctx)) out[t.name] = t;
|
|
428
|
+
return out;
|
|
429
|
+
})();
|
|
430
|
+
rc.set?.(CONNECTOR_TOOLS_CACHE_KEY, build);
|
|
431
|
+
return build;
|
|
432
|
+
}
|
|
73
433
|
async function withScratchpad(args, base2, rc) {
|
|
74
434
|
if (!args.loadScratchpad) return base2;
|
|
75
435
|
const tenantId = rc.get("tenantId") ?? "default";
|
|
@@ -77,8 +437,13 @@ async function withScratchpad(args, base2, rc) {
|
|
|
77
437
|
const entries = await args.loadScratchpad(container, tenantId);
|
|
78
438
|
return [...base2, composeScratchpadPrompt(entries)];
|
|
79
439
|
}
|
|
440
|
+
function withSoftPolicy(args, base2, slug) {
|
|
441
|
+
const soft = args.softPolicy?.(slug) ?? [];
|
|
442
|
+
return soft.length ? [...base2, ...composePolicyPrompt(soft)] : base2;
|
|
443
|
+
}
|
|
80
444
|
async function modelFor(args, row, tenantId) {
|
|
81
|
-
const
|
|
445
|
+
const effective = tierModelId(row.modelId, args.tier, { tenantId, agentSlug: row.slug, pinnedModelId: row.modelId });
|
|
446
|
+
const id = await args.model.resolve(effective, { tenantId });
|
|
82
447
|
return args.modelFactory(id, row.slug);
|
|
83
448
|
}
|
|
84
449
|
function buildSubAgent(args, row, depth, path) {
|
|
@@ -90,9 +455,12 @@ function buildSubAgent(args, row, depth, path) {
|
|
|
90
455
|
// personality so the orchestrator's LLM knows WHAT this delegate is for (role is a coarse enum).
|
|
91
456
|
description: row.personality || `Agent ${row.slug} (${row.role})`,
|
|
92
457
|
...memoryFor(args, row) ? { memory: memoryFor(args, row) } : {},
|
|
93
|
-
instructions: async ({ requestContext }) =>
|
|
458
|
+
instructions: async ({ requestContext }) => {
|
|
459
|
+
await gateDelegation(requestContext, row.slug);
|
|
460
|
+
return withScratchpad(args, withSoftPolicy(args, composeSystemPrompt(row), row.slug), requestContext);
|
|
461
|
+
},
|
|
94
462
|
model: async ({ requestContext }) => await modelFor(args, row, requestContext.get("tenantId") ?? "default"),
|
|
95
|
-
tools: toolsFor(args, row),
|
|
463
|
+
tools: (async ({ requestContext }) => toolsFor(args, row, await connectorByNameFor(args, requestContext, row.slug))),
|
|
96
464
|
agents: async ({ requestContext }) => await buildSubAgentMap(
|
|
97
465
|
args,
|
|
98
466
|
row.delegateSlugs ?? [],
|
|
@@ -121,9 +489,16 @@ function buildMastraAgent(args) {
|
|
|
121
489
|
// request). If Mastra rejects a dynamic `memory`, fall back to the static swarm Memory (root override is
|
|
122
490
|
// then sub-agents-only — see the spec's accepted limitation).
|
|
123
491
|
...args.resolveMemory || args.memory ? { memory: (async ({ requestContext }) => memoryFor(args, await load(requestContext))) } : {},
|
|
124
|
-
instructions: async ({ requestContext }) =>
|
|
492
|
+
instructions: async ({ requestContext }) => {
|
|
493
|
+
const row = await load(requestContext);
|
|
494
|
+
return withScratchpad(args, withSoftPolicy(args, composeSystemPrompt(row), row.slug), requestContext);
|
|
495
|
+
},
|
|
125
496
|
model: async ({ requestContext }) => await modelFor(args, await load(requestContext), requestContext.get("tenantId") ?? "default"),
|
|
126
|
-
tools: async ({ requestContext }) =>
|
|
497
|
+
tools: (async ({ requestContext }) => {
|
|
498
|
+
const row = await load(requestContext);
|
|
499
|
+
const connectorByName = await connectorByNameFor(args, requestContext, row.slug);
|
|
500
|
+
return { ...args.extraTools ?? {}, ...toolsFor(args, row, connectorByName) };
|
|
501
|
+
}),
|
|
127
502
|
// Delegation: the orchestrator's delegateSlugs become `agent-<slug>` tools (Mastra-native).
|
|
128
503
|
agents: async ({ requestContext }) => {
|
|
129
504
|
const row = await load(requestContext);
|
|
@@ -138,7 +513,7 @@ import { createTool } from "@mastra/core/tools";
|
|
|
138
513
|
import { z } from "zod";
|
|
139
514
|
|
|
140
515
|
// src/page-context.ts
|
|
141
|
-
var PAGE_CONTEXT_KEY = "
|
|
516
|
+
var PAGE_CONTEXT_KEY = "__nightowlsdev_pageContext";
|
|
142
517
|
function attachPageContext(rc, value) {
|
|
143
518
|
rc.set(PAGE_CONTEXT_KEY, value ?? {});
|
|
144
519
|
}
|
|
@@ -345,7 +720,7 @@ var InMemoryContainerFloor = class {
|
|
|
345
720
|
s.held = who;
|
|
346
721
|
if (s.timer) clearTimeout(s.timer);
|
|
347
722
|
s.timer = setTimeout(() => {
|
|
348
|
-
console.warn(`[
|
|
723
|
+
console.warn(`[@nightowlsdev/core] container floor force-released after ${this.maxHoldMs}ms: ${container} (held by ${who.label})`);
|
|
349
724
|
this.release(container, s, who);
|
|
350
725
|
}, this.maxHoldMs);
|
|
351
726
|
if (typeof s.timer.unref === "function") s.timer.unref();
|
|
@@ -380,25 +755,84 @@ var PRICE_TABLE = {
|
|
|
380
755
|
"openai/gpt-5.5": { inUsdPerMtok: 2.5, outUsdPerMtok: 10 },
|
|
381
756
|
"openai/gpt-5.5-mini": { inUsdPerMtok: 0.3, outUsdPerMtok: 1.2 }
|
|
382
757
|
};
|
|
383
|
-
function priceUsage(prices, modelId, u) {
|
|
384
|
-
const p = prices[modelId]
|
|
385
|
-
|
|
758
|
+
function priceUsage(prices, modelId, u, opts = {}) {
|
|
759
|
+
const p = prices[modelId];
|
|
760
|
+
if (!p) {
|
|
761
|
+
if (opts.failOnUnknownModel) {
|
|
762
|
+
throw new Error(
|
|
763
|
+
`[@nightowlsdev/core] no price entry for model '${modelId}' (failOnUnknownModel=true). Add it to PRICE_TABLE, the swarm cost.prices map, or a priceFeed.`
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
return 0;
|
|
767
|
+
}
|
|
768
|
+
const cacheReadRate = p.cacheReadUsdPerMtok ?? p.inUsdPerMtok;
|
|
769
|
+
const cacheWriteRate = p.cacheWriteUsdPerMtok ?? p.inUsdPerMtok;
|
|
770
|
+
const reasoningRate = p.reasoningUsdPerMtok ?? p.outUsdPerMtok;
|
|
771
|
+
const M = 1e6;
|
|
772
|
+
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;
|
|
773
|
+
}
|
|
774
|
+
var OPTIONAL_USAGE_CLASSES = [
|
|
775
|
+
"cacheReadTokens",
|
|
776
|
+
"cacheWriteTokens",
|
|
777
|
+
"reasoningTokens",
|
|
778
|
+
"toolCalls",
|
|
779
|
+
"agentActivations"
|
|
780
|
+
];
|
|
781
|
+
function sumBreakdowns(items) {
|
|
782
|
+
const total = { inputTokens: 0, outputTokens: 0 };
|
|
783
|
+
for (const b of items) {
|
|
784
|
+
total.inputTokens += b.inputTokens ?? 0;
|
|
785
|
+
total.outputTokens += b.outputTokens ?? 0;
|
|
786
|
+
for (const k of OPTIONAL_USAGE_CLASSES) {
|
|
787
|
+
const v = b[k];
|
|
788
|
+
if (v != null) total[k] = (total[k] ?? 0) + v;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
return total;
|
|
792
|
+
}
|
|
793
|
+
function sumTurnUsage(items) {
|
|
794
|
+
const order = [];
|
|
795
|
+
const breakdownsBySlug = /* @__PURE__ */ new Map();
|
|
796
|
+
const usdBySlug = /* @__PURE__ */ new Map();
|
|
797
|
+
for (const it of items) {
|
|
798
|
+
if (!breakdownsBySlug.has(it.slug)) {
|
|
799
|
+
order.push(it.slug);
|
|
800
|
+
breakdownsBySlug.set(it.slug, []);
|
|
801
|
+
usdBySlug.set(it.slug, 0);
|
|
802
|
+
}
|
|
803
|
+
breakdownsBySlug.get(it.slug).push(it.breakdown);
|
|
804
|
+
usdBySlug.set(it.slug, usdBySlug.get(it.slug) + it.cost.usd);
|
|
805
|
+
}
|
|
806
|
+
const bySlug = order.map((slug) => {
|
|
807
|
+
const breakdown2 = sumBreakdowns(breakdownsBySlug.get(slug));
|
|
808
|
+
const usd2 = usdBySlug.get(slug);
|
|
809
|
+
return { slug, breakdown: breakdown2, cost: { usd: usd2, breakdown: breakdown2 } };
|
|
810
|
+
});
|
|
811
|
+
const breakdown = sumBreakdowns(items.map((it) => it.breakdown));
|
|
812
|
+
const usd = items.reduce((a, it) => a + it.cost.usd, 0);
|
|
813
|
+
return { breakdown, cost: { usd, breakdown }, bySlug };
|
|
386
814
|
}
|
|
387
815
|
var CostGovernor = class {
|
|
388
816
|
constructor(opts) {
|
|
389
817
|
this.opts = opts;
|
|
390
|
-
this.prices = { ...PRICE_TABLE, ...opts.prices ?? {} };
|
|
818
|
+
this.prices = { ...PRICE_TABLE, ...opts.prices ?? {}, ...opts.priceFeed?.prices() ?? {} };
|
|
819
|
+
this.failOnUnknownModel = opts.failOnUnknownModel ?? false;
|
|
391
820
|
}
|
|
392
821
|
opts;
|
|
393
822
|
steps = 0;
|
|
394
823
|
usd = 0;
|
|
395
824
|
prices;
|
|
825
|
+
failOnUnknownModel;
|
|
396
826
|
step() {
|
|
397
827
|
this.steps++;
|
|
398
828
|
}
|
|
399
829
|
/** Price a single usage WITHOUT accumulating it (for per-generation telemetry cost). */
|
|
400
830
|
priceOf(modelId, u) {
|
|
401
|
-
return priceUsage(this.prices, modelId, u);
|
|
831
|
+
return priceUsage(this.prices, modelId, u, { failOnUnknownModel: this.failOnUnknownModel });
|
|
832
|
+
}
|
|
833
|
+
/** Price a single usage WITHOUT accumulating it, returning the usd + the breakdown it was priced from. */
|
|
834
|
+
costOf(modelId, u) {
|
|
835
|
+
return { usd: this.priceOf(modelId, u), breakdown: u };
|
|
402
836
|
}
|
|
403
837
|
addUsage(modelId, u) {
|
|
404
838
|
this.usd += this.priceOf(modelId, u);
|
|
@@ -406,6 +840,19 @@ var CostGovernor = class {
|
|
|
406
840
|
costUsd() {
|
|
407
841
|
return this.usd;
|
|
408
842
|
}
|
|
843
|
+
/** The current USD cap (SP9-core: the cap-that-asks reads this to surface "spend / cap" + to compute the raise). */
|
|
844
|
+
get maxCostUsd() {
|
|
845
|
+
return this.opts.maxCostUsd;
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* SP9-core — RAISE the USD cap by `incrementUsd` (the budget an approved "Budget cap reached — continue?"
|
|
849
|
+
* grants). Mutates the governor's ceiling so a freshly-resumed generation isn't immediately re-blocked at the
|
|
850
|
+
* SAME cap; the run gets real additional headroom. Only the cap-that-asks resume path calls this; the default
|
|
851
|
+
* terminal-stop path never does, so today's behaviour is unchanged.
|
|
852
|
+
*/
|
|
853
|
+
raiseCostCap(incrementUsd) {
|
|
854
|
+
this.opts.maxCostUsd += incrementUsd;
|
|
855
|
+
}
|
|
409
856
|
shouldStop() {
|
|
410
857
|
if (this.steps >= this.opts.maxSteps) return { stop: true, reason: "step cap reached" };
|
|
411
858
|
if (this.usd >= this.opts.maxCostUsd) return { stop: true, reason: "USD cap reached" };
|
|
@@ -413,15 +860,17 @@ var CostGovernor = class {
|
|
|
413
860
|
}
|
|
414
861
|
};
|
|
415
862
|
var DelegateBudgets = class {
|
|
416
|
-
constructor(cfg, rootSlug,
|
|
863
|
+
constructor(cfg, rootSlug, pricing) {
|
|
417
864
|
this.cfg = cfg;
|
|
418
865
|
this.rootSlug = rootSlug;
|
|
419
|
-
this.prices = { ...PRICE_TABLE, ...prices ?? {} };
|
|
866
|
+
this.prices = { ...PRICE_TABLE, ...pricing?.prices ?? {}, ...pricing?.priceFeed?.prices() ?? {} };
|
|
867
|
+
this.failOnUnknownModel = pricing?.failOnUnknownModel ?? false;
|
|
420
868
|
}
|
|
421
869
|
cfg;
|
|
422
870
|
rootSlug;
|
|
423
871
|
usd = /* @__PURE__ */ new Map();
|
|
424
872
|
prices;
|
|
873
|
+
failOnUnknownModel;
|
|
425
874
|
/** The USD cap for a delegate: its `bySlug` override if present, else the default. `undefined` → uncapped. */
|
|
426
875
|
capFor(slug) {
|
|
427
876
|
return this.cfg.bySlug?.[slug]?.maxCostUsd ?? this.cfg.maxCostUsd;
|
|
@@ -429,7 +878,10 @@ var DelegateBudgets = class {
|
|
|
429
878
|
/** Accumulate one generation's usage against a delegate. No-op for the root orchestrator (not a delegate). */
|
|
430
879
|
addUsage(slug, modelId, u) {
|
|
431
880
|
if (slug === this.rootSlug) return;
|
|
432
|
-
this.usd.set(
|
|
881
|
+
this.usd.set(
|
|
882
|
+
slug,
|
|
883
|
+
(this.usd.get(slug) ?? 0) + priceUsage(this.prices, modelId, u, { failOnUnknownModel: this.failOnUnknownModel })
|
|
884
|
+
);
|
|
433
885
|
}
|
|
434
886
|
/** The first delegate that has met or exceeded its USD cap, or null. */
|
|
435
887
|
exceeded() {
|
|
@@ -457,7 +909,7 @@ function compositeTelemetry(exporters) {
|
|
|
457
909
|
const results = await Promise.allSettled(exporters.map((e) => e.export(spans)));
|
|
458
910
|
for (const r of results) {
|
|
459
911
|
if (r.status === "rejected") {
|
|
460
|
-
console.warn("[
|
|
912
|
+
console.warn("[@nightowlsdev/core] telemetry exporter failed:", r.reason);
|
|
461
913
|
}
|
|
462
914
|
}
|
|
463
915
|
}
|
|
@@ -504,10 +956,17 @@ var SpanCollector = class {
|
|
|
504
956
|
*/
|
|
505
957
|
closeGeneration(usage, costUsd) {
|
|
506
958
|
if (!this.gen) return;
|
|
959
|
+
const extra = {};
|
|
960
|
+
if (usage.cacheReadTokens != null) extra.cacheReadTokens = usage.cacheReadTokens;
|
|
961
|
+
if (usage.cacheWriteTokens != null) extra.cacheWriteTokens = usage.cacheWriteTokens;
|
|
962
|
+
if (usage.reasoningTokens != null) extra.reasoningTokens = usage.reasoningTokens;
|
|
963
|
+
if (usage.toolCalls != null) extra.toolCalls = usage.toolCalls;
|
|
964
|
+
if (usage.agentActivations != null) extra.agentActivations = usage.agentActivations;
|
|
507
965
|
this.gen.attributes = {
|
|
508
966
|
...this.gen.attributes,
|
|
509
967
|
inputTokens: usage.inputTokens,
|
|
510
968
|
outputTokens: usage.outputTokens,
|
|
969
|
+
...extra,
|
|
511
970
|
costUsd: Math.max(0, costUsd)
|
|
512
971
|
};
|
|
513
972
|
this.gen.endedAt = this.now();
|
|
@@ -591,6 +1050,21 @@ var RowCache = class {
|
|
|
591
1050
|
|
|
592
1051
|
// src/engine.ts
|
|
593
1052
|
var AGENT_KEY = "swarm";
|
|
1053
|
+
var MAX_CONTINUE_NUDGES = 2;
|
|
1054
|
+
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.";
|
|
1055
|
+
function verifyNudge(missing) {
|
|
1056
|
+
const gap = (missing ?? "").trim();
|
|
1057
|
+
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;
|
|
1058
|
+
}
|
|
1059
|
+
var VERIFY_TRANSCRIPT_CAP = 6e3;
|
|
1060
|
+
function appendTranscript(t, e) {
|
|
1061
|
+
let add = "";
|
|
1062
|
+
if (e.type === "swarm.message") add = e.data.delta ?? e.data.text ?? "";
|
|
1063
|
+
else if (e.type === "swarm.tool_call") add = `
|
|
1064
|
+
\xAB${e.agentSlug} \u2192 ${e.data.name}\xBB
|
|
1065
|
+
`;
|
|
1066
|
+
return add ? (t + add).slice(-VERIFY_TRANSCRIPT_CAP) : t;
|
|
1067
|
+
}
|
|
594
1068
|
var SwarmEngine = class {
|
|
595
1069
|
constructor(opts) {
|
|
596
1070
|
this.opts = opts;
|
|
@@ -600,6 +1074,7 @@ var SwarmEngine = class {
|
|
|
600
1074
|
const { memory, resolveMemory } = opts.memory ? buildMemoryResolver(opts.memory) : { memory: void 0, resolveMemory: void 0 };
|
|
601
1075
|
this.memory = memory;
|
|
602
1076
|
this.floor = opts.floor ?? containerFloor;
|
|
1077
|
+
this.hooks = opts.hooks ?? new HookDispatcher({}, opts.toolApproval ?? { mode: "flag" });
|
|
603
1078
|
opts.storage.subscribeInvalidations?.((key) => this.rowCache.invalidate(key));
|
|
604
1079
|
const agent = buildMastraAgent({
|
|
605
1080
|
loadRow: (slug, tenantId) => this.loadRow(tenantId, slug),
|
|
@@ -607,6 +1082,8 @@ var SwarmEngine = class {
|
|
|
607
1082
|
resolveSkill: (n) => opts.resolveSkill?.(n),
|
|
608
1083
|
model: opts.model,
|
|
609
1084
|
modelFactory: opts.modelFactory,
|
|
1085
|
+
// SP10: hand the cheap-model router to the per-agent model resolver. Undefined ⇒ no routing (today).
|
|
1086
|
+
tier: opts.tier,
|
|
610
1087
|
builtinTools: {
|
|
611
1088
|
[ASK_TOOL_NAME]: buildAskMastraTool(),
|
|
612
1089
|
...opts.scratchpad ? { scratchpad_write: buildScratchpadTool(opts.storage.scratchpad, typeof opts.scratchpad === "object" ? opts.scratchpad : void 0) } : {},
|
|
@@ -616,6 +1093,9 @@ var SwarmEngine = class {
|
|
|
616
1093
|
// ONLY (never sub-agents) so the model can pull the host page's advisory RunInput.context.
|
|
617
1094
|
...opts.pageContext ? { extraTools: { get_page_context: buildPageContextTool() } } : {},
|
|
618
1095
|
loadScratchpad: opts.scratchpad ? (c, t) => opts.storage.scratchpad.list(t, c) : void 0,
|
|
1096
|
+
softPolicy: opts.softPolicy,
|
|
1097
|
+
// PR2: per-request connector-tools resolver, granted to the orchestrator + sub-agents by skillNames.
|
|
1098
|
+
connectorTools: opts.connectorTools,
|
|
619
1099
|
memory
|
|
620
1100
|
});
|
|
621
1101
|
this.mastra = new Mastra({
|
|
@@ -634,6 +1114,51 @@ var SwarmEngine = class {
|
|
|
634
1114
|
// Typed `unknown` to keep the engine wall: no engine-vendor type escapes the public surface.
|
|
635
1115
|
memory;
|
|
636
1116
|
floor;
|
|
1117
|
+
// SP2: the decision-hook dispatcher. Always present — defaults to an allow-all dispatcher when the engine is
|
|
1118
|
+
// built without one (e.g. unit tests), so the preGeneration seam is uniform with no per-call null checks.
|
|
1119
|
+
hooks;
|
|
1120
|
+
/** SP1: the swarm's metering config, in the shape DelegateBudgets/priceUsage expect. CostGovernor reads the
|
|
1121
|
+
* same fields directly off `opts.cost`; this packs them for the per-delegate tracker so both caps price
|
|
1122
|
+
* tokens identically (built-in PRICE_TABLE ← static `prices` ← live `priceFeed`, with `failOnUnknownModel`). */
|
|
1123
|
+
pricingOpts() {
|
|
1124
|
+
return {
|
|
1125
|
+
prices: this.opts.cost.prices,
|
|
1126
|
+
priceFeed: this.opts.cost.priceFeed,
|
|
1127
|
+
failOnUnknownModel: this.opts.cost.failOnUnknownModel
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
/** Fire the best-effort per-event observer (`EngineOpts.onEvent`). Awaited so an async observer (e.g. a
|
|
1131
|
+
* metering debit) completes in order, but FAIL-SAFE: a throw is swallowed (the host logs its own), never
|
|
1132
|
+
* breaking the run — same contract as the telemetry exporter. No-op when no observer is configured. */
|
|
1133
|
+
async notifyEvent(e, ctx) {
|
|
1134
|
+
if (!this.opts.onEvent) return;
|
|
1135
|
+
try {
|
|
1136
|
+
await this.opts.onEvent(e, ctx);
|
|
1137
|
+
} catch {
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
/** Run the completion supervisor (`EngineOpts.verifyCompletion`), FAIL-OPEN: no verifier, or a throwing one,
|
|
1141
|
+
* yields `{ complete: true }` so a missing/broken judge never traps a run in a verify loop. */
|
|
1142
|
+
async safeVerify(request, transcript, ctx) {
|
|
1143
|
+
if (!this.opts.verifyCompletion) return { complete: true };
|
|
1144
|
+
try {
|
|
1145
|
+
return await this.opts.verifyCompletion({ request, transcript, ctx });
|
|
1146
|
+
} catch (err) {
|
|
1147
|
+
console.error(`[@nightowlsdev/core] verifyCompletion threw for run ${ctx.runId} \u2014 treating as complete:`, err);
|
|
1148
|
+
return { complete: true };
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
/** Best-effort recall of the run's ORIGINAL request (first user message on the thread) for the completion
|
|
1152
|
+
* verifier on RESUME, where the engine doesn't hold the opening message. Empty on any failure / no verifier. */
|
|
1153
|
+
async recallRequest(ctx) {
|
|
1154
|
+
if (!this.opts.verifyCompletion) return "";
|
|
1155
|
+
try {
|
|
1156
|
+
const msgs = await this.history(ctx.threadId, ctx, { limit: 50 });
|
|
1157
|
+
return msgs.find((m) => m.role === "user")?.text ?? "";
|
|
1158
|
+
} catch {
|
|
1159
|
+
return "";
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
637
1162
|
/** Cached agent-row load shared by the three dynamic agent fns AND run/resume. */
|
|
638
1163
|
loadRow(tenantId, slug) {
|
|
639
1164
|
return this.rowCache.get(`${tenantId}:${slug}`, async () => {
|
|
@@ -642,16 +1167,60 @@ var SwarmEngine = class {
|
|
|
642
1167
|
return row;
|
|
643
1168
|
});
|
|
644
1169
|
}
|
|
1170
|
+
/** Resolve an agent's STORED modelId — which may be a tier sentinel (`"tier:"` / `"tier:swift"`) — to the
|
|
1171
|
+
* CONCRETE model id the generation actually runs on, so metering/pricing + the preGeneration event see the
|
|
1172
|
+
* real model, not the sentinel (which has no price → every tier-routed turn would meter at $0). Mirrors
|
|
1173
|
+
* mastra-map's modelFor routing; with no tier config it returns the id unchanged. (SP10 pricing follow-up.) */
|
|
1174
|
+
priceModelId(rawModelId, tenantId, agentSlug) {
|
|
1175
|
+
return tierModelId(rawModelId, this.opts.tier, { tenantId, agentSlug, pinnedModelId: rawModelId });
|
|
1176
|
+
}
|
|
645
1177
|
agent() {
|
|
646
1178
|
return this.mastra.getAgent(AGENT_KEY);
|
|
647
1179
|
}
|
|
648
|
-
requestContext(ctx) {
|
|
1180
|
+
requestContext(ctx, state) {
|
|
649
1181
|
const rc = new RequestContext();
|
|
650
1182
|
for (const [k, v] of Object.entries(ctx)) {
|
|
651
1183
|
if (v !== void 0) rc.set(k, v);
|
|
652
1184
|
}
|
|
1185
|
+
rc.set(TOOL_GATE_KEY, this.toolGate);
|
|
1186
|
+
if (this.opts.secrets) rc.set(SECRET_RESOLVER_KEY, this.opts.secrets);
|
|
1187
|
+
if (state) rc.set(RUN_STATE_KEY, state);
|
|
653
1188
|
return rc;
|
|
654
1189
|
}
|
|
1190
|
+
/**
|
|
1191
|
+
* SP5 — the action-approval gate handed to every gated tool via the RequestContext. Bound once (stable
|
|
1192
|
+
* reference). Delegates to the dispatcher's `preToolCall`, which is fail-closed (a throwing configured hook ⇒
|
|
1193
|
+
* deny) and applies the non-removable policy. The defineTool wrapper turns the returned `ToolDecision` into:
|
|
1194
|
+
* allow → run; deny → blocked result; ask → suspend-and-ask (the existing `swarm.question`/resume machinery).
|
|
1195
|
+
*/
|
|
1196
|
+
toolGate = (ev2) => this.hooks.preToolCall(ev2);
|
|
1197
|
+
/**
|
|
1198
|
+
* SP5 truth-fix — resolve whether a tool WILL require approval, for the `swarm.tool_call` event's
|
|
1199
|
+
* `needsApproval` (the react reducer reads it to render an approval card). The mapChunk emit currently
|
|
1200
|
+
* hardcodes `false` (the truth-bug). This computes the truthful value from the SAME policy + per-tool flag the
|
|
1201
|
+
* gate uses: the tool's resolved `needsApproval` (its own flag, defaulting by origin) run through the
|
|
1202
|
+
* dispatcher's SYNC `policyDecision` — `ask` ⇒ true (it will gate), else false. The async `preToolCall` hook
|
|
1203
|
+
* can still escalate a specific call at execute time, but the policy-derived baseline is the truthful default
|
|
1204
|
+
* the UI needs without speculatively running the hook for every tool_call event.
|
|
1205
|
+
*/
|
|
1206
|
+
gatesApproval(toolName) {
|
|
1207
|
+
const skill = this.opts.resolveSkill?.(toolName);
|
|
1208
|
+
const origin = skill?.origin ?? "first-party";
|
|
1209
|
+
const needsApproval = skill?.needsApproval ?? origin === "mcp";
|
|
1210
|
+
const decision = this.hooks.policyDecision({ runId: "", agentSlug: "", toolName, origin, needsApproval });
|
|
1211
|
+
return decision.action === "ask";
|
|
1212
|
+
}
|
|
1213
|
+
/**
|
|
1214
|
+
* SP2: the preGeneration DECISION seam. Awaited immediately before each model launch (run + resume). The
|
|
1215
|
+
* dispatcher is fail-closed (a throwing hook ⇒ deny), so this only ever sees a clean `allow`/`deny`; a `deny`
|
|
1216
|
+
* THROWS `ReserveDenied` so the model call below never happens and the run/resume catch-all maps it to a
|
|
1217
|
+
* terminal `run_failed` stage "reserve" (NOT the generic "exception"). Allow-all + zero-overhead when no
|
|
1218
|
+
* hooks are configured (the default dispatcher returns allow synchronously-ish without invoking anything).
|
|
1219
|
+
*/
|
|
1220
|
+
async guardGeneration(ev2) {
|
|
1221
|
+
const decision = await this.hooks.preGeneration(ev2);
|
|
1222
|
+
if (decision.action === "deny") throw new ReserveDenied(decision.reason);
|
|
1223
|
+
}
|
|
655
1224
|
/** Per-call Mastra memory ids + delegation, only when memory is configured (else stream is unchanged). */
|
|
656
1225
|
memoryOpts(ctx) {
|
|
657
1226
|
if (!this.opts.memory) return {};
|
|
@@ -861,6 +1430,13 @@ var SwarmEngine = class {
|
|
|
861
1430
|
async activeRuns(container, ctx) {
|
|
862
1431
|
return this.opts.storage.runs.listActive(ctx.tenantId, container);
|
|
863
1432
|
}
|
|
1433
|
+
/** The full, globally-ordered event log for a thread's CONTAINER (all its runs + lane sub-threads) — lets a host
|
|
1434
|
+
* rebuild the RICH timeline (tool calls + delegation cards) on reload, since message history is text-only.
|
|
1435
|
+
* Returns [] when the store has no events table (`listForContainer` unset). */
|
|
1436
|
+
async threadEvents(threadId, ctx) {
|
|
1437
|
+
const container = threadId.split(":")[0] || threadId;
|
|
1438
|
+
return await this.opts.storage.events.listForContainer?.(ctx.tenantId, container) ?? [];
|
|
1439
|
+
}
|
|
864
1440
|
/** The tenant's agent roster (slug, title-cased display name, role, delegate graph) as wall-safe
|
|
865
1441
|
* AgentSummary[]. Sourced from the agent rows; no vendor type in the signature or result. Powers
|
|
866
1442
|
* the multi-agent pile / @mention UI. */
|
|
@@ -875,7 +1451,12 @@ var SwarmEngine = class {
|
|
|
875
1451
|
}));
|
|
876
1452
|
}
|
|
877
1453
|
async *run(input, ctx) {
|
|
878
|
-
const modelId = (await this.loadRow(ctx.tenantId, ctx.agentSlug)).modelId ?? "unknown";
|
|
1454
|
+
const modelId = this.priceModelId((await this.loadRow(ctx.tenantId, ctx.agentSlug)).modelId ?? "unknown", ctx.tenantId, ctx.agentSlug);
|
|
1455
|
+
const workflowDef = input.workflow ? this.opts.workflows?.find((w) => w.name === input.workflow) : this.opts.agentWorkflows?.[ctx.agentSlug];
|
|
1456
|
+
if (input.workflow && !workflowDef) throw new Error(`unknown workflow: ${input.workflow}`);
|
|
1457
|
+
if (this.opts.storage.threads) {
|
|
1458
|
+
await this.opts.storage.threads.ensure({ id: ctx.threadId, orgId: ctx.tenantId, userId: ctx.userId });
|
|
1459
|
+
}
|
|
879
1460
|
await this.opts.storage.runs.create({
|
|
880
1461
|
runId: ctx.runId,
|
|
881
1462
|
tenantId: ctx.tenantId,
|
|
@@ -883,18 +1464,40 @@ var SwarmEngine = class {
|
|
|
883
1464
|
threadId: ctx.threadId,
|
|
884
1465
|
agentSlug: ctx.agentSlug
|
|
885
1466
|
});
|
|
886
|
-
const modelIdFor = (slug) =>
|
|
1467
|
+
const modelIdFor = (slug) => {
|
|
1468
|
+
const raw = this.rowCache.peek(`${ctx.tenantId}:${slug}`)?.modelId;
|
|
1469
|
+
return raw ? this.priceModelId(raw, ctx.tenantId, slug) : modelId;
|
|
1470
|
+
};
|
|
1471
|
+
const gatesApproval = (name) => this.gatesApproval(name);
|
|
887
1472
|
const gov = new CostGovernor(this.opts.cost);
|
|
888
|
-
const delegateBudgets = this.opts.cost.perDelegate ? new DelegateBudgets(this.opts.cost.perDelegate, ctx.agentSlug) : null;
|
|
1473
|
+
const delegateBudgets = this.opts.cost.perDelegate ? new DelegateBudgets(this.opts.cost.perDelegate, ctx.agentSlug, this.pricingOpts()) : null;
|
|
889
1474
|
const streamed = /* @__PURE__ */ new Set();
|
|
890
|
-
const
|
|
1475
|
+
const activity = /* @__PURE__ */ new Map();
|
|
1476
|
+
const turnUsage = [];
|
|
1477
|
+
const runState = createRunState();
|
|
1478
|
+
const rc = this.requestContext(ctx, runState);
|
|
891
1479
|
if (this.opts.pageContext) attachPageContext(rc, input.context);
|
|
1480
|
+
let outcome = "failed";
|
|
1481
|
+
if (this.opts.onRunStart) {
|
|
1482
|
+
try {
|
|
1483
|
+
await this.opts.onRunStart(ctx, { input, state: runState });
|
|
1484
|
+
} catch (err) {
|
|
1485
|
+
console.error(`[@nightowlsdev/core] onRunStart threw for run ${ctx.runId}:`, err);
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
892
1488
|
const collector = this.opts.telemetry ? new SpanCollector(ctx.runId, () => Date.now(), "run", { agentSlug: ctx.agentSlug }) : null;
|
|
893
1489
|
let ts = 0;
|
|
894
1490
|
const emit = async (e) => {
|
|
895
1491
|
e.seq = await this.opts.storage.events.append(e);
|
|
1492
|
+
await this.notifyEvent(e, ctx);
|
|
896
1493
|
return e;
|
|
897
1494
|
};
|
|
1495
|
+
let turnEmitted = false;
|
|
1496
|
+
const emitTurn = async () => {
|
|
1497
|
+
if (turnEmitted) return null;
|
|
1498
|
+
turnEmitted = true;
|
|
1499
|
+
return emit(turnUsageEvent(ctx, ts++, turnUsage, 0));
|
|
1500
|
+
};
|
|
898
1501
|
const floorKey = ctx.threadId;
|
|
899
1502
|
const me = { label: titleCase(ctx.agentSlug), runId: ctx.runId };
|
|
900
1503
|
const floorAbort = new AbortController();
|
|
@@ -908,96 +1511,298 @@ var SwarmEngine = class {
|
|
|
908
1511
|
if (floorAbort.signal.aborted) return;
|
|
909
1512
|
}
|
|
910
1513
|
yield await emit(ev("swarm.status", base(ctx, ts++), { state: "thinking" }));
|
|
1514
|
+
if (workflowDef) {
|
|
1515
|
+
const wfMessage = input.message.replace(/^(\[user:[^\]]*\]\s*)+/, "");
|
|
1516
|
+
await emit(ev("swarm.message", base(ctx, ts++), { role: "user", text: wfMessage }));
|
|
1517
|
+
outcome = yield* this.driveWorkflow(workflowDef, initialWorkflowState(workflowDef), ctx, { message: wfMessage, context: input.context }, {
|
|
1518
|
+
gov,
|
|
1519
|
+
modelIdFor,
|
|
1520
|
+
streamed,
|
|
1521
|
+
delegateBudgets,
|
|
1522
|
+
activity,
|
|
1523
|
+
gatesApproval,
|
|
1524
|
+
turnUsage,
|
|
1525
|
+
nextTs: () => ts++,
|
|
1526
|
+
emit,
|
|
1527
|
+
emitTurn,
|
|
1528
|
+
segmentIndex: 0,
|
|
1529
|
+
// FR-004: a run segment starts at generation 0
|
|
1530
|
+
state: runState
|
|
1531
|
+
// FR-003: workflow steps' tools see the run's ctx.state
|
|
1532
|
+
});
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1535
|
+
const generationIndex = 0;
|
|
1536
|
+
await this.guardGeneration({ runId: ctx.runId, tenantId: ctx.tenantId, agentSlug: ctx.agentSlug, modelId, generationIndex, kind: "run" });
|
|
911
1537
|
const userMessage = input.message.replace(/^(\[user:[^\]]*\]\s*)+/, "");
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
})
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
1538
|
+
await emit(ev("swarm.message", base(ctx, ts++), { role: "user", text: userMessage }));
|
|
1539
|
+
let turnMessage = userMessage;
|
|
1540
|
+
let continueNudges = 0;
|
|
1541
|
+
let transcript = "";
|
|
1542
|
+
let incompleteVerdict = null;
|
|
1543
|
+
for (; ; ) {
|
|
1544
|
+
const result = await this.agent().stream(turnMessage, { runId: ctx.runId, requestContext: rc, ...this.memoryOpts(ctx) });
|
|
1545
|
+
let sawStep = false;
|
|
1546
|
+
let lastOutputSlug;
|
|
1547
|
+
for await (const part of result.fullStream) {
|
|
1548
|
+
if (part?.type === "step-finish") {
|
|
1549
|
+
gov.step();
|
|
1550
|
+
sawStep = true;
|
|
1551
|
+
}
|
|
1552
|
+
if (part?.type === "tool-call-suspended") {
|
|
1553
|
+
const payload = part.payload ?? {};
|
|
1554
|
+
const toolCallId = payload.toolCallId ?? "";
|
|
1555
|
+
const followupId = `${ctx.runId}:${toolCallId}`;
|
|
1556
|
+
const sp = payload.suspendPayload ?? {};
|
|
1557
|
+
await recordSuspend(this.opts.storage, ctx, followupId, toolCallId);
|
|
1558
|
+
await this.opts.storage.runs.setStatus(ctx.runId, "suspended");
|
|
1559
|
+
await this.opts.storage.runs.saveSnapshot(ctx.runId, { pending: { toolCallId }, genIndex: generationIndex + 1, state: runState.entries() });
|
|
1560
|
+
{
|
|
1561
|
+
const t = await emitTurn();
|
|
1562
|
+
if (t) yield t;
|
|
1563
|
+
}
|
|
1564
|
+
yield await emit(ev("swarm.status", base(ctx, ts++), { state: "waiting" }));
|
|
1565
|
+
yield await emit(clientActionOrQuestion(ctx, ts++, followupId, toolCallId, sp));
|
|
1566
|
+
outcome = "suspended";
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
if (part?.type === "error") {
|
|
1570
|
+
await this.opts.storage.runs.setStatus(ctx.runId, "failed");
|
|
1571
|
+
{
|
|
1572
|
+
const t = await emitTurn();
|
|
1573
|
+
if (t) yield t;
|
|
1574
|
+
}
|
|
1575
|
+
yield await emit(
|
|
1576
|
+
ev("swarm.run_failed", base(ctx, ts++), {
|
|
1577
|
+
stage: "stream",
|
|
1578
|
+
message: streamErrorMessage(part),
|
|
1579
|
+
retryable: false
|
|
1580
|
+
})
|
|
1581
|
+
);
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1584
|
+
for (const e of mapChunk(part, ctx, gov, modelIdFor, () => ts++, streamed, delegateBudgets, activity, gatesApproval, turnUsage, 0)) {
|
|
1585
|
+
if (e.type === "swarm.message" || e.type === "swarm.tool_call") {
|
|
1586
|
+
lastOutputSlug = e.agentSlug;
|
|
1587
|
+
if (this.opts.verifyCompletion) transcript = appendTranscript(transcript, e);
|
|
1588
|
+
}
|
|
1589
|
+
yield await emit(e);
|
|
1590
|
+
}
|
|
1591
|
+
collectSpans(collector, part, modelId, gov);
|
|
1592
|
+
const overDelegate = delegateBudgets?.exceeded();
|
|
1593
|
+
const stop = gov.shouldStop();
|
|
1594
|
+
if (stop.stop || overDelegate) {
|
|
1595
|
+
if (this.opts.cost.onCapHit === "ask" && stop.stop && !overDelegate) {
|
|
1596
|
+
const followupId = `${ctx.runId}:${CAP_FOLLOWUP_SUFFIX}`;
|
|
1597
|
+
await recordSuspend(this.opts.storage, ctx, followupId, CAP_FOLLOWUP_SUFFIX);
|
|
1598
|
+
await this.opts.storage.runs.setStatus(ctx.runId, "suspended");
|
|
1599
|
+
await this.opts.storage.runs.saveSnapshot(ctx.runId, {
|
|
1600
|
+
capHit: { message: userMessage, spentUsd: gov.costUsd() },
|
|
1601
|
+
genIndex: generationIndex + 1,
|
|
1602
|
+
state: runState.entries()
|
|
1603
|
+
// FR-003: persist per-run state across the cap-ask boundary
|
|
1604
|
+
});
|
|
1605
|
+
{
|
|
1606
|
+
const t = await emitTurn();
|
|
1607
|
+
if (t) yield t;
|
|
1608
|
+
}
|
|
1609
|
+
yield await emit(ev("swarm.status", base(ctx, ts++), { state: "waiting" }));
|
|
1610
|
+
yield await emit(ev("swarm.question", base(ctx, ts++), capQuestion(ctx, followupId, gov)));
|
|
1611
|
+
outcome = "suspended";
|
|
1612
|
+
return;
|
|
1613
|
+
}
|
|
1614
|
+
await this.opts.storage.runs.setStatus(ctx.runId, "failed");
|
|
1615
|
+
{
|
|
1616
|
+
const t = await emitTurn();
|
|
1617
|
+
if (t) yield t;
|
|
1618
|
+
}
|
|
1619
|
+
yield await emit(
|
|
1620
|
+
ev("swarm.run_failed", base(ctx, ts++), {
|
|
1621
|
+
stage: "cost",
|
|
1622
|
+
message: overDelegate?.reason ?? stop.reason,
|
|
1623
|
+
retryable: false
|
|
1624
|
+
})
|
|
1625
|
+
);
|
|
1626
|
+
return;
|
|
1627
|
+
}
|
|
947
1628
|
}
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
return;
|
|
1629
|
+
if (this.opts.verifyCompletion) {
|
|
1630
|
+
const verdict = await this.safeVerify(userMessage, transcript, ctx);
|
|
1631
|
+
if (!verdict.complete && continueNudges < MAX_CONTINUE_NUDGES) {
|
|
1632
|
+
continueNudges++;
|
|
1633
|
+
turnMessage = verifyNudge(verdict.missing);
|
|
1634
|
+
continue;
|
|
1635
|
+
}
|
|
1636
|
+
incompleteVerdict = verdict.complete ? null : verdict;
|
|
1637
|
+
} else if (sawStep && lastOutputSlug !== ctx.agentSlug && continueNudges < MAX_CONTINUE_NUDGES) {
|
|
1638
|
+
continueNudges++;
|
|
1639
|
+
turnMessage = CONTINUE_NUDGE;
|
|
1640
|
+
continue;
|
|
961
1641
|
}
|
|
1642
|
+
break;
|
|
962
1643
|
}
|
|
963
1644
|
await this.mirrorDelegations(ctx);
|
|
964
1645
|
await this.attributeRun(ctx);
|
|
965
|
-
|
|
966
|
-
|
|
1646
|
+
if (incompleteVerdict) {
|
|
1647
|
+
await this.opts.storage.runs.setStatus(ctx.runId, "failed");
|
|
1648
|
+
{
|
|
1649
|
+
const t = await emitTurn();
|
|
1650
|
+
if (t) yield t;
|
|
1651
|
+
}
|
|
1652
|
+
yield await emit(ev("swarm.run_failed", base(ctx, ts++), { stage: "incomplete", message: incompleteVerdict.missing ?? "The task was not completed.", retryable: true }));
|
|
1653
|
+
} else {
|
|
1654
|
+
await this.opts.storage.runs.setStatus(ctx.runId, "done");
|
|
1655
|
+
{
|
|
1656
|
+
const t = await emitTurn();
|
|
1657
|
+
if (t) yield t;
|
|
1658
|
+
}
|
|
1659
|
+
yield await emit(ev("swarm.status", base(ctx, ts++), { state: "done" }));
|
|
1660
|
+
outcome = "done";
|
|
1661
|
+
}
|
|
967
1662
|
} catch (err) {
|
|
968
|
-
|
|
1663
|
+
const stage = err instanceof ReserveDenied ? "reserve" : "exception";
|
|
1664
|
+
if (stage !== "reserve") console.error(`[@nightowlsdev/core] run ${ctx.runId} threw:`, err);
|
|
969
1665
|
try {
|
|
970
1666
|
await this.opts.storage.runs.setStatus(ctx.runId, "failed");
|
|
971
1667
|
} catch {
|
|
972
1668
|
}
|
|
973
|
-
|
|
1669
|
+
{
|
|
1670
|
+
const t = await emitTurn();
|
|
1671
|
+
if (t) yield t;
|
|
1672
|
+
}
|
|
1673
|
+
yield await emit(ev("swarm.run_failed", base(ctx, ts++), { stage, message: errMessage(err), retryable: false }));
|
|
974
1674
|
} finally {
|
|
1675
|
+
if (this.opts.onRunEnd) {
|
|
1676
|
+
try {
|
|
1677
|
+
await this.opts.onRunEnd(ctx, { state: runState, outcome });
|
|
1678
|
+
} catch (err) {
|
|
1679
|
+
console.error(`[@nightowlsdev/core] onRunEnd threw for run ${ctx.runId}:`, err);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
975
1682
|
floorAbort.abort();
|
|
976
1683
|
await releaseFloor?.();
|
|
977
1684
|
await exportSpans(this.opts.telemetry, collector);
|
|
978
1685
|
}
|
|
979
1686
|
}
|
|
1687
|
+
/**
|
|
1688
|
+
* Phase B — drive a STRICT workflow IN PLACE OF the free-form continue-nudge loop. Shared by `run()` (fresh)
|
|
1689
|
+
* and `resume()` (re-entry after a human/approval suspend). An `agent` step reuses `this.agent().stream()`
|
|
1690
|
+
* with a per-step requestContext (agentSlug = the step's agent) so it inherits persona/tools/gate/model/cost;
|
|
1691
|
+
* a `tool` step runs `executeToolWithGate`; a `human`/approval pause suspends SP9-style. Reserve, usage, and
|
|
1692
|
+
* the terminal turn_usage flow through the caller's machinery (`m`). Handles the terminal status/setStatus.
|
|
1693
|
+
*/
|
|
1694
|
+
async *driveWorkflow(wf, state, ctx, input, m) {
|
|
1695
|
+
const driver = new StepDriver(wf, {
|
|
1696
|
+
nextTs: m.nextTs,
|
|
1697
|
+
runAgentStep: (slug, message, genIndex) => this.streamWorkflowAgentStep(slug, message, genIndex, ctx, input, m),
|
|
1698
|
+
runToolStep: (toolName, args) => this.runWorkflowToolStep(toolName, args, ctx, m.state),
|
|
1699
|
+
saveState: (rid, st) => this.opts.storage.runs.saveSnapshot(rid, { workflow: st })
|
|
1700
|
+
});
|
|
1701
|
+
const it = driver.drive(state, ctx, input);
|
|
1702
|
+
let r = await it.next();
|
|
1703
|
+
while (!r.done) {
|
|
1704
|
+
yield await m.emit(r.value);
|
|
1705
|
+
r = await it.next();
|
|
1706
|
+
}
|
|
1707
|
+
const outcome = r.value;
|
|
1708
|
+
if (outcome.kind === "suspended") {
|
|
1709
|
+
const p = outcome.state.pending;
|
|
1710
|
+
await recordSuspend(this.opts.storage, ctx, p.followupId, p.toolCallId);
|
|
1711
|
+
await this.opts.storage.runs.setStatus(ctx.runId, "suspended");
|
|
1712
|
+
yield await m.emit(ev("swarm.status", base(ctx, m.nextTs()), { state: "waiting" }));
|
|
1713
|
+
{
|
|
1714
|
+
const t = await m.emitTurn();
|
|
1715
|
+
if (t) yield t;
|
|
1716
|
+
}
|
|
1717
|
+
return "suspended";
|
|
1718
|
+
}
|
|
1719
|
+
if (outcome.kind === "failed") {
|
|
1720
|
+
await this.opts.storage.runs.setStatus(ctx.runId, "failed");
|
|
1721
|
+
{
|
|
1722
|
+
const t = await m.emitTurn();
|
|
1723
|
+
if (t) yield t;
|
|
1724
|
+
}
|
|
1725
|
+
yield await m.emit(ev("swarm.run_failed", base(ctx, m.nextTs()), { stage: outcome.stage, message: outcome.message, retryable: true }));
|
|
1726
|
+
return "failed";
|
|
1727
|
+
}
|
|
1728
|
+
await this.mirrorDelegations(ctx);
|
|
1729
|
+
await this.attributeRun(ctx);
|
|
1730
|
+
await this.opts.storage.runs.setStatus(ctx.runId, "done");
|
|
1731
|
+
{
|
|
1732
|
+
const t = await m.emitTurn();
|
|
1733
|
+
if (t) yield t;
|
|
1734
|
+
}
|
|
1735
|
+
yield await m.emit(ev("swarm.status", base(ctx, m.nextTs()), { state: "done" }));
|
|
1736
|
+
return "done";
|
|
1737
|
+
}
|
|
1738
|
+
/** A workflow `agent` step: stream `slug` with `message` (a per-step requestContext so it inherits the agent's
|
|
1739
|
+
* persona/tools/gate/model), reserving + metering through the caller's machinery, returning the final text. */
|
|
1740
|
+
async *streamWorkflowAgentStep(slug, message, genIndex, ctx, input, m) {
|
|
1741
|
+
await this.guardGeneration({ runId: ctx.runId, tenantId: ctx.tenantId, agentSlug: slug, modelId: m.modelIdFor(slug), generationIndex: genIndex, kind: "run" });
|
|
1742
|
+
const sctx = { ...ctx, agentSlug: slug };
|
|
1743
|
+
const stepRc = this.requestContext(sctx, m.state);
|
|
1744
|
+
if (this.opts.pageContext) attachPageContext(stepRc, input.context);
|
|
1745
|
+
const result = await this.agent().stream(message, { runId: ctx.runId, requestContext: stepRc, ...this.memoryOpts(sctx) });
|
|
1746
|
+
let text = "";
|
|
1747
|
+
for await (const part of result.fullStream) {
|
|
1748
|
+
if (part?.type === "step-finish") m.gov.step();
|
|
1749
|
+
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)) {
|
|
1750
|
+
if (e.type === "swarm.message") text += e.data.delta ?? e.data.text ?? "";
|
|
1751
|
+
yield e;
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
return { text };
|
|
1755
|
+
}
|
|
1756
|
+
/** A workflow `tool` step: run the gate-free tool body through `executeToolWithGate` (the engine-owned gate). */
|
|
1757
|
+
async runWorkflowToolStep(toolName, args, ctx, state) {
|
|
1758
|
+
const skill = this.opts.resolveSkill?.(toolName);
|
|
1759
|
+
const exec = skill ? getToolExecutor(skill) : void 0;
|
|
1760
|
+
if (!skill || !exec) return { ok: false, error: `unknown tool "${toolName}"` };
|
|
1761
|
+
const toolCtx = { tenantId: ctx.tenantId, userId: ctx.userId, runId: ctx.runId, secrets: bindSecrets(this.opts.secrets, ctx), state };
|
|
1762
|
+
return executeToolWithGate({
|
|
1763
|
+
ev: toolPreCallEvent({ runId: ctx.runId, tenantId: ctx.tenantId, agentSlug: ctx.agentSlug, toolName, origin: skill.origin ?? "first-party", needsApproval: this.gatesApproval(toolName), args }),
|
|
1764
|
+
gate: this.toolGate,
|
|
1765
|
+
run: () => exec(args, toolCtx)
|
|
1766
|
+
});
|
|
1767
|
+
}
|
|
980
1768
|
async *resume(args, ctx) {
|
|
981
1769
|
const snap = await this.opts.storage.runs.loadSnapshot(ctx.tenantId, args.runId);
|
|
982
1770
|
if (!snap) throw new Error(`no suspended run: ${args.runId}`);
|
|
1771
|
+
const resumedState = createRunState(snap.state ?? void 0);
|
|
1772
|
+
const generationIndex = typeof snap.genIndex === "number" ? snap.genIndex : 1;
|
|
1773
|
+
const capHit = snap.capHit;
|
|
983
1774
|
await this.opts.storage.markFollowupAnswered?.(args.followupId, ctx.tenantId);
|
|
984
1775
|
await this.opts.storage.runs.setStatus(args.runId, "running");
|
|
985
|
-
const modelId = (await this.loadRow(ctx.tenantId, ctx.agentSlug)).modelId ?? "unknown";
|
|
986
|
-
const modelIdFor = (slug) =>
|
|
1776
|
+
const modelId = this.priceModelId((await this.loadRow(ctx.tenantId, ctx.agentSlug)).modelId ?? "unknown", ctx.tenantId, ctx.agentSlug);
|
|
1777
|
+
const modelIdFor = (slug) => {
|
|
1778
|
+
const raw = this.rowCache.peek(`${ctx.tenantId}:${slug}`)?.modelId;
|
|
1779
|
+
return raw ? this.priceModelId(raw, ctx.tenantId, slug) : modelId;
|
|
1780
|
+
};
|
|
1781
|
+
const gatesApproval = (name) => this.gatesApproval(name);
|
|
987
1782
|
const gov = new CostGovernor(this.opts.cost);
|
|
988
|
-
const delegateBudgets = this.opts.cost.perDelegate ? new DelegateBudgets(this.opts.cost.perDelegate, ctx.agentSlug) : null;
|
|
1783
|
+
const delegateBudgets = this.opts.cost.perDelegate ? new DelegateBudgets(this.opts.cost.perDelegate, ctx.agentSlug, this.pricingOpts()) : null;
|
|
989
1784
|
const streamed = /* @__PURE__ */ new Set();
|
|
1785
|
+
const activity = /* @__PURE__ */ new Map();
|
|
1786
|
+
const turnUsage = [];
|
|
990
1787
|
const collector = this.opts.telemetry ? new SpanCollector(args.runId, () => Date.now(), "resume", { agentSlug: ctx.agentSlug }) : null;
|
|
991
1788
|
let ts = 1e3;
|
|
992
1789
|
const emit = async (e) => {
|
|
993
1790
|
e.seq = await this.opts.storage.events.append(e);
|
|
1791
|
+
await this.notifyEvent(e, ctx);
|
|
994
1792
|
return e;
|
|
995
1793
|
};
|
|
1794
|
+
let turnEmitted = false;
|
|
996
1795
|
const floorKey = ctx.threadId;
|
|
997
1796
|
const me = { label: titleCase(ctx.agentSlug), runId: args.runId };
|
|
998
1797
|
const floorAbort = new AbortController();
|
|
999
1798
|
let releaseFloor = await this.floor.tryAcquire(floorKey, me);
|
|
1000
1799
|
const rctx = { ...ctx, runId: args.runId };
|
|
1800
|
+
const emitTurn = async () => {
|
|
1801
|
+
if (turnEmitted) return null;
|
|
1802
|
+
turnEmitted = true;
|
|
1803
|
+
return emit(turnUsageEvent(rctx, ts++, turnUsage, generationIndex));
|
|
1804
|
+
};
|
|
1805
|
+
let resumeOutcome = "failed";
|
|
1001
1806
|
try {
|
|
1002
1807
|
if (!releaseFloor) {
|
|
1003
1808
|
const held = await this.floor.holder(floorKey);
|
|
@@ -1013,84 +1818,285 @@ var SwarmEngine = class {
|
|
|
1013
1818
|
answer: args.answer
|
|
1014
1819
|
})
|
|
1015
1820
|
);
|
|
1016
|
-
const
|
|
1017
|
-
if (
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
{ runId: args.runId, toolCallId: args.toolCallId, requestContext: rc, ...this.memoryOpts({ ...ctx, runId: args.runId }) }
|
|
1021
|
-
);
|
|
1022
|
-
for await (const part of result.fullStream) {
|
|
1023
|
-
if (part?.type === "step-finish") gov.step();
|
|
1024
|
-
if (part?.type === "tool-call-suspended") {
|
|
1025
|
-
const payload = part.payload ?? {};
|
|
1026
|
-
const toolCallId = payload.toolCallId ?? "";
|
|
1027
|
-
const followupId = `${args.runId}:${toolCallId}`;
|
|
1028
|
-
const sp = payload.suspendPayload ?? {};
|
|
1029
|
-
await recordSuspend(this.opts.storage, rctx, followupId, toolCallId);
|
|
1030
|
-
await this.opts.storage.runs.setStatus(args.runId, "suspended");
|
|
1031
|
-
await this.opts.storage.runs.saveSnapshot(args.runId, { pending: { toolCallId } });
|
|
1032
|
-
yield await emit(ev("swarm.status", base(rctx, ts++), { state: "waiting" }));
|
|
1033
|
-
yield await emit(
|
|
1034
|
-
ev("swarm.question", base(rctx, ts++), {
|
|
1035
|
-
followupId,
|
|
1036
|
-
toolCallId,
|
|
1037
|
-
to: sp.to ?? "user",
|
|
1038
|
-
from: sp.asker || rctx.agentSlug,
|
|
1039
|
-
prompt: sp.prompt ?? "",
|
|
1040
|
-
field: sp.field
|
|
1041
|
-
})
|
|
1042
|
-
);
|
|
1043
|
-
return;
|
|
1044
|
-
}
|
|
1045
|
-
if (part?.type === "error") {
|
|
1821
|
+
const wfState = snap.workflow;
|
|
1822
|
+
if (wfState) {
|
|
1823
|
+
const wf = this.opts.workflows?.find((w) => w.name === wfState.workflow) ?? this.opts.agentWorkflows?.[ctx.agentSlug];
|
|
1824
|
+
if (!wf) {
|
|
1046
1825
|
await this.opts.storage.runs.setStatus(args.runId, "failed");
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
})
|
|
1053
|
-
);
|
|
1826
|
+
{
|
|
1827
|
+
const t = await emitTurn();
|
|
1828
|
+
if (t) yield t;
|
|
1829
|
+
}
|
|
1830
|
+
yield await emit(ev("swarm.run_failed", base(rctx, ts++), { stage: "workflow", message: `unknown workflow: ${wfState.workflow}`, retryable: false }));
|
|
1054
1831
|
return;
|
|
1055
1832
|
}
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
if (gov.shouldStop().stop || overDelegate) {
|
|
1060
|
-
await this.opts.storage.runs.setStatus(args.runId, "failed");
|
|
1061
|
-
yield await emit(
|
|
1062
|
-
ev("swarm.run_failed", base(rctx, ts++), {
|
|
1063
|
-
stage: "cost",
|
|
1064
|
-
message: overDelegate?.reason ?? gov.shouldStop().reason,
|
|
1065
|
-
retryable: false
|
|
1066
|
-
})
|
|
1067
|
-
);
|
|
1068
|
-
return;
|
|
1833
|
+
if (wfState.pending) {
|
|
1834
|
+
wfState.outputs[wfState.pending.stepId] = args.answer;
|
|
1835
|
+
wfState.pending = void 0;
|
|
1069
1836
|
}
|
|
1837
|
+
resumeOutcome = yield* this.driveWorkflow(wf, wfState, rctx, { message: "", context: args.context }, {
|
|
1838
|
+
gov,
|
|
1839
|
+
modelIdFor,
|
|
1840
|
+
streamed,
|
|
1841
|
+
delegateBudgets,
|
|
1842
|
+
activity,
|
|
1843
|
+
gatesApproval,
|
|
1844
|
+
turnUsage,
|
|
1845
|
+
nextTs: () => ts++,
|
|
1846
|
+
emit,
|
|
1847
|
+
emitTurn,
|
|
1848
|
+
segmentIndex: generationIndex,
|
|
1849
|
+
// FR-004: a resume segment starts at the snapshot's genIndex
|
|
1850
|
+
state: resumedState
|
|
1851
|
+
// FR-003: workflow steps' tools see the restored ctx.state
|
|
1852
|
+
});
|
|
1853
|
+
return;
|
|
1854
|
+
}
|
|
1855
|
+
if (capHit && !isApproved(args.answer)) {
|
|
1856
|
+
await this.opts.storage.runs.setStatus(args.runId, "failed");
|
|
1857
|
+
{
|
|
1858
|
+
const t = await emitTurn();
|
|
1859
|
+
if (t) yield t;
|
|
1860
|
+
}
|
|
1861
|
+
yield await emit(
|
|
1862
|
+
ev("swarm.run_failed", base(rctx, ts++), {
|
|
1863
|
+
stage: "cost",
|
|
1864
|
+
message: "budget cap reached \u2014 continuation declined by the user",
|
|
1865
|
+
retryable: false
|
|
1866
|
+
})
|
|
1867
|
+
);
|
|
1868
|
+
return;
|
|
1869
|
+
}
|
|
1870
|
+
const rc = this.requestContext({ ...ctx, runId: args.runId }, resumedState);
|
|
1871
|
+
if (this.opts.pageContext) attachPageContext(rc, args.context);
|
|
1872
|
+
await this.guardGeneration({ runId: args.runId, tenantId: ctx.tenantId, agentSlug: ctx.agentSlug, modelId, generationIndex, kind: "resume" });
|
|
1873
|
+
let resumeNudges = 0;
|
|
1874
|
+
let firstPass = true;
|
|
1875
|
+
const request = await this.recallRequest(rctx);
|
|
1876
|
+
let nudgeMessage = CONTINUE_NUDGE;
|
|
1877
|
+
let transcript = "";
|
|
1878
|
+
let incompleteVerdict = null;
|
|
1879
|
+
for (; ; ) {
|
|
1880
|
+
const result = firstPass ? capHit ? await (async () => {
|
|
1881
|
+
gov.raiseCostCap(this.opts.cost.capIncrementUsd ?? this.opts.cost.maxCostUsd);
|
|
1882
|
+
return this.agent().stream(capHit.message, { runId: args.runId, requestContext: rc, ...this.memoryOpts({ ...ctx, runId: args.runId }) });
|
|
1883
|
+
})() : await this.agent().resumeStream(
|
|
1884
|
+
{ answer: args.answer },
|
|
1885
|
+
{ runId: args.runId, toolCallId: args.toolCallId, requestContext: rc, ...this.memoryOpts({ ...ctx, runId: args.runId }) }
|
|
1886
|
+
) : await this.agent().stream(nudgeMessage, { runId: args.runId, requestContext: rc, ...this.memoryOpts({ ...ctx, runId: args.runId }) });
|
|
1887
|
+
firstPass = false;
|
|
1888
|
+
let sawStep = false;
|
|
1889
|
+
let lastOutputSlug;
|
|
1890
|
+
for await (const part of result.fullStream) {
|
|
1891
|
+
if (part?.type === "step-finish") {
|
|
1892
|
+
gov.step();
|
|
1893
|
+
sawStep = true;
|
|
1894
|
+
}
|
|
1895
|
+
if (part?.type === "tool-call-suspended") {
|
|
1896
|
+
const payload = part.payload ?? {};
|
|
1897
|
+
const toolCallId = payload.toolCallId ?? "";
|
|
1898
|
+
const followupId = `${args.runId}:${toolCallId}`;
|
|
1899
|
+
const sp = payload.suspendPayload ?? {};
|
|
1900
|
+
await recordSuspend(this.opts.storage, rctx, followupId, toolCallId);
|
|
1901
|
+
await this.opts.storage.runs.setStatus(args.runId, "suspended");
|
|
1902
|
+
await this.opts.storage.runs.saveSnapshot(args.runId, { pending: { toolCallId }, genIndex: generationIndex + 1, state: resumedState.entries() });
|
|
1903
|
+
{
|
|
1904
|
+
const t = await emitTurn();
|
|
1905
|
+
if (t) yield t;
|
|
1906
|
+
}
|
|
1907
|
+
yield await emit(ev("swarm.status", base(rctx, ts++), { state: "waiting" }));
|
|
1908
|
+
yield await emit(clientActionOrQuestion(rctx, ts++, followupId, toolCallId, sp));
|
|
1909
|
+
resumeOutcome = "suspended";
|
|
1910
|
+
return;
|
|
1911
|
+
}
|
|
1912
|
+
if (part?.type === "error") {
|
|
1913
|
+
await this.opts.storage.runs.setStatus(args.runId, "failed");
|
|
1914
|
+
{
|
|
1915
|
+
const t = await emitTurn();
|
|
1916
|
+
if (t) yield t;
|
|
1917
|
+
}
|
|
1918
|
+
yield await emit(
|
|
1919
|
+
ev("swarm.run_failed", base(rctx, ts++), {
|
|
1920
|
+
stage: "stream",
|
|
1921
|
+
message: streamErrorMessage(part),
|
|
1922
|
+
retryable: false
|
|
1923
|
+
})
|
|
1924
|
+
);
|
|
1925
|
+
return;
|
|
1926
|
+
}
|
|
1927
|
+
collectSpans(collector, part, modelId, gov);
|
|
1928
|
+
for (const e of mapChunk(part, rctx, gov, modelIdFor, () => ts++, streamed, delegateBudgets, activity, gatesApproval, turnUsage, generationIndex)) {
|
|
1929
|
+
if (e.type === "swarm.message" || e.type === "swarm.tool_call") {
|
|
1930
|
+
lastOutputSlug = e.agentSlug;
|
|
1931
|
+
if (this.opts.verifyCompletion) transcript = appendTranscript(transcript, e);
|
|
1932
|
+
}
|
|
1933
|
+
yield await emit(e);
|
|
1934
|
+
}
|
|
1935
|
+
const overDelegate = delegateBudgets?.exceeded();
|
|
1936
|
+
const stop = gov.shouldStop();
|
|
1937
|
+
if (stop.stop || overDelegate) {
|
|
1938
|
+
if (this.opts.cost.onCapHit === "ask" && stop.stop && !overDelegate && capHit) {
|
|
1939
|
+
const followupId = `${args.runId}:${CAP_FOLLOWUP_SUFFIX}`;
|
|
1940
|
+
await recordSuspend(this.opts.storage, rctx, followupId, CAP_FOLLOWUP_SUFFIX);
|
|
1941
|
+
await this.opts.storage.runs.setStatus(args.runId, "suspended");
|
|
1942
|
+
await this.opts.storage.runs.saveSnapshot(args.runId, {
|
|
1943
|
+
capHit: { message: capHit.message, spentUsd: gov.costUsd() },
|
|
1944
|
+
genIndex: generationIndex + 1,
|
|
1945
|
+
state: resumedState.entries()
|
|
1946
|
+
// FR-003: persist per-run state across the cap-ask boundary
|
|
1947
|
+
});
|
|
1948
|
+
{
|
|
1949
|
+
const t = await emitTurn();
|
|
1950
|
+
if (t) yield t;
|
|
1951
|
+
}
|
|
1952
|
+
yield await emit(ev("swarm.status", base(rctx, ts++), { state: "waiting" }));
|
|
1953
|
+
yield await emit(ev("swarm.question", base(rctx, ts++), capQuestion(rctx, followupId, gov)));
|
|
1954
|
+
resumeOutcome = "suspended";
|
|
1955
|
+
return;
|
|
1956
|
+
}
|
|
1957
|
+
await this.opts.storage.runs.setStatus(args.runId, "failed");
|
|
1958
|
+
{
|
|
1959
|
+
const t = await emitTurn();
|
|
1960
|
+
if (t) yield t;
|
|
1961
|
+
}
|
|
1962
|
+
yield await emit(
|
|
1963
|
+
ev("swarm.run_failed", base(rctx, ts++), {
|
|
1964
|
+
stage: "cost",
|
|
1965
|
+
message: overDelegate?.reason ?? stop.reason,
|
|
1966
|
+
retryable: false
|
|
1967
|
+
})
|
|
1968
|
+
);
|
|
1969
|
+
return;
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
if (this.opts.verifyCompletion) {
|
|
1973
|
+
const verdict = await this.safeVerify(request, transcript, rctx);
|
|
1974
|
+
if (!verdict.complete && resumeNudges < MAX_CONTINUE_NUDGES) {
|
|
1975
|
+
resumeNudges++;
|
|
1976
|
+
nudgeMessage = verifyNudge(verdict.missing);
|
|
1977
|
+
continue;
|
|
1978
|
+
}
|
|
1979
|
+
incompleteVerdict = verdict.complete ? null : verdict;
|
|
1980
|
+
} else if (sawStep && lastOutputSlug !== rctx.agentSlug && resumeNudges < MAX_CONTINUE_NUDGES) {
|
|
1981
|
+
resumeNudges++;
|
|
1982
|
+
continue;
|
|
1983
|
+
}
|
|
1984
|
+
break;
|
|
1070
1985
|
}
|
|
1071
1986
|
await this.attributeRun(rctx);
|
|
1072
|
-
|
|
1073
|
-
|
|
1987
|
+
if (incompleteVerdict) {
|
|
1988
|
+
await this.opts.storage.runs.setStatus(args.runId, "failed");
|
|
1989
|
+
{
|
|
1990
|
+
const t = await emitTurn();
|
|
1991
|
+
if (t) yield t;
|
|
1992
|
+
}
|
|
1993
|
+
yield await emit(ev("swarm.run_failed", base(rctx, ts++), { stage: "incomplete", message: incompleteVerdict.missing ?? "The task was not completed.", retryable: true }));
|
|
1994
|
+
} else {
|
|
1995
|
+
await this.opts.storage.runs.setStatus(args.runId, "done");
|
|
1996
|
+
{
|
|
1997
|
+
const t = await emitTurn();
|
|
1998
|
+
if (t) yield t;
|
|
1999
|
+
}
|
|
2000
|
+
yield await emit(ev("swarm.status", base(rctx, ts++), { state: "done" }));
|
|
2001
|
+
resumeOutcome = "done";
|
|
2002
|
+
}
|
|
1074
2003
|
} catch (err) {
|
|
1075
|
-
|
|
2004
|
+
const stage = err instanceof ReserveDenied ? "reserve" : "exception";
|
|
2005
|
+
if (stage !== "reserve") console.error(`[@nightowlsdev/core] resume ${args.runId} threw:`, err);
|
|
1076
2006
|
try {
|
|
1077
2007
|
await this.opts.storage.runs.setStatus(args.runId, "failed");
|
|
1078
2008
|
} catch {
|
|
1079
2009
|
}
|
|
1080
|
-
|
|
2010
|
+
{
|
|
2011
|
+
const t = await emitTurn();
|
|
2012
|
+
if (t) yield t;
|
|
2013
|
+
}
|
|
2014
|
+
yield await emit(ev("swarm.run_failed", base(rctx, ts++), { stage, message: errMessage(err), retryable: false }));
|
|
1081
2015
|
} finally {
|
|
2016
|
+
if (this.opts.onRunEnd) {
|
|
2017
|
+
try {
|
|
2018
|
+
await this.opts.onRunEnd(ctx, { state: resumedState, outcome: resumeOutcome });
|
|
2019
|
+
} catch (err) {
|
|
2020
|
+
console.error(`[@nightowlsdev/core] onRunEnd threw for resume ${args.runId}:`, err);
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
1082
2023
|
floorAbort.abort();
|
|
1083
2024
|
await releaseFloor?.();
|
|
1084
2025
|
await exportSpans(this.opts.telemetry, collector);
|
|
1085
2026
|
}
|
|
1086
2027
|
}
|
|
1087
2028
|
};
|
|
2029
|
+
var ReserveDenied = class extends Error {
|
|
2030
|
+
stage = "reserve";
|
|
2031
|
+
constructor(reason) {
|
|
2032
|
+
super(reason);
|
|
2033
|
+
this.name = "ReserveDenied";
|
|
2034
|
+
}
|
|
2035
|
+
};
|
|
1088
2036
|
function errMessage(err) {
|
|
1089
2037
|
return err instanceof Error ? err.message : String(err);
|
|
1090
2038
|
}
|
|
1091
2039
|
function base(ctx, ts) {
|
|
1092
2040
|
return { runId: ctx.runId, agentSlug: ctx.agentSlug, ts };
|
|
1093
2041
|
}
|
|
2042
|
+
function clientActionOrQuestion(ctx, ts, followupId, toolCallId, sp) {
|
|
2043
|
+
if (sp.clientAction) {
|
|
2044
|
+
return ev("swarm.client_action", base(ctx, ts), {
|
|
2045
|
+
followupId,
|
|
2046
|
+
toolCallId,
|
|
2047
|
+
tool: sp.clientAction.tool,
|
|
2048
|
+
input: sp.clientAction.input,
|
|
2049
|
+
needsApproval: sp.clientAction.needsApproval ?? false,
|
|
2050
|
+
from: sp.asker || ctx.agentSlug
|
|
2051
|
+
});
|
|
2052
|
+
}
|
|
2053
|
+
return ev("swarm.question", base(ctx, ts), {
|
|
2054
|
+
followupId,
|
|
2055
|
+
toolCallId,
|
|
2056
|
+
to: sp.to ?? "user",
|
|
2057
|
+
from: sp.asker || ctx.agentSlug,
|
|
2058
|
+
prompt: sp.prompt ?? "",
|
|
2059
|
+
field: sp.field
|
|
2060
|
+
});
|
|
2061
|
+
}
|
|
2062
|
+
var CAP_FOLLOWUP_SUFFIX = "cap";
|
|
2063
|
+
function capQuestion(ctx, followupId, gov) {
|
|
2064
|
+
const spent = gov.costUsd();
|
|
2065
|
+
const cap = gov.maxCostUsd;
|
|
2066
|
+
return {
|
|
2067
|
+
followupId,
|
|
2068
|
+
toolCallId: CAP_FOLLOWUP_SUFFIX,
|
|
2069
|
+
to: "user",
|
|
2070
|
+
from: ctx.agentSlug,
|
|
2071
|
+
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.`,
|
|
2072
|
+
field: {
|
|
2073
|
+
kind: "confirm",
|
|
2074
|
+
confirmLabel: "Continue",
|
|
2075
|
+
rejectLabel: "Stop"
|
|
2076
|
+
}
|
|
2077
|
+
};
|
|
2078
|
+
}
|
|
2079
|
+
function turnUsageEvent(ctx, ts, turnUsage, segmentIndex) {
|
|
2080
|
+
const total = sumTurnUsage(turnUsage);
|
|
2081
|
+
return ev("swarm.turn_usage", base(ctx, ts), {
|
|
2082
|
+
breakdown: total.breakdown,
|
|
2083
|
+
cost: total.cost,
|
|
2084
|
+
bySlug: total.bySlug,
|
|
2085
|
+
generations: turnUsage.length,
|
|
2086
|
+
segmentIndex
|
|
2087
|
+
});
|
|
2088
|
+
}
|
|
2089
|
+
function extractUsage(usage) {
|
|
2090
|
+
const u = usage ?? {};
|
|
2091
|
+
const cacheRead = u.cachedInputTokens ?? u.inputTokenDetails?.cacheReadTokens ?? u.raw?.inputTokenDetails?.cacheReadTokens;
|
|
2092
|
+
const cacheWrite = u.inputTokenDetails?.cacheWriteTokens ?? u.raw?.inputTokenDetails?.cacheWriteTokens;
|
|
2093
|
+
const reasoning = u.reasoningTokens ?? u.outputTokenDetails?.reasoningTokens ?? u.raw?.outputTokenDetails?.reasoningTokens;
|
|
2094
|
+
const b = { inputTokens: u.inputTokens ?? 0, outputTokens: u.outputTokens ?? 0 };
|
|
2095
|
+
if (cacheRead != null) b.cacheReadTokens = cacheRead;
|
|
2096
|
+
if (cacheWrite != null) b.cacheWriteTokens = cacheWrite;
|
|
2097
|
+
if (reasoning != null) b.reasoningTokens = reasoning;
|
|
2098
|
+
return b;
|
|
2099
|
+
}
|
|
1094
2100
|
function titleCase(slug) {
|
|
1095
2101
|
return slug.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
1096
2102
|
}
|
|
@@ -1122,8 +2128,7 @@ function collectSpans(collector, part, modelId, gov) {
|
|
|
1122
2128
|
break;
|
|
1123
2129
|
case "step-finish": {
|
|
1124
2130
|
const output = p.output;
|
|
1125
|
-
const
|
|
1126
|
-
const u = { inputTokens: usage?.inputTokens ?? 0, outputTokens: usage?.outputTokens ?? 0 };
|
|
2131
|
+
const u = extractUsage(output?.usage);
|
|
1127
2132
|
collector.openGeneration(modelId);
|
|
1128
2133
|
collector.closeGeneration(u, gov.priceOf(modelId, u));
|
|
1129
2134
|
break;
|
|
@@ -1144,14 +2149,19 @@ async function recordSuspend(storage, ctx, followupId, toolCallId) {
|
|
|
1144
2149
|
if (!storage.recordSuspend && !warnedNoRecordSuspend) {
|
|
1145
2150
|
warnedNoRecordSuspend = true;
|
|
1146
2151
|
console.warn(
|
|
1147
|
-
"[
|
|
2152
|
+
"[@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."
|
|
1148
2153
|
);
|
|
1149
2154
|
}
|
|
1150
2155
|
await storage.recordSuspend?.(ctx.runId, ctx.tenantId, followupId, toolCallId);
|
|
1151
2156
|
}
|
|
1152
|
-
function mapChunk(part, ctx, gov, modelIdFor, nextTs, streamed, delegateBudgets) {
|
|
2157
|
+
function mapChunk(part, ctx, gov, modelIdFor, nextTs, streamed, delegateBudgets, activity, gatesApproval, turnUsage, segmentIndex) {
|
|
1153
2158
|
const p = part.payload ?? {};
|
|
1154
2159
|
const modelId = modelIdFor(ctx.agentSlug);
|
|
2160
|
+
const act = (slug) => {
|
|
2161
|
+
let a = activity.get(slug);
|
|
2162
|
+
if (!a) activity.set(slug, a = { toolCalls: 0, agentActivations: 0 });
|
|
2163
|
+
return a;
|
|
2164
|
+
};
|
|
1155
2165
|
switch (part.type) {
|
|
1156
2166
|
case "text-delta":
|
|
1157
2167
|
return [ev("swarm.message", base(ctx, nextTs()), { role: "assistant", delta: p.text ?? "" })];
|
|
@@ -1161,17 +2171,21 @@ function mapChunk(part, ctx, gov, modelIdFor, nextTs, streamed, delegateBudgets)
|
|
|
1161
2171
|
const to = name.slice("agent-".length);
|
|
1162
2172
|
const a = typeof p.args === "string" ? safeParse(p.args) : p.args;
|
|
1163
2173
|
const task = a?.prompt ?? "";
|
|
2174
|
+
act(ctx.agentSlug).agentActivations++;
|
|
1164
2175
|
return [
|
|
1165
2176
|
ev("swarm.handoff", base(ctx, nextTs()), { from: ctx.agentSlug, to, task }),
|
|
1166
2177
|
ev("swarm.status", base(ctx, nextTs()), { state: "delegating", note: to })
|
|
1167
2178
|
];
|
|
1168
2179
|
}
|
|
2180
|
+
act(ctx.agentSlug).toolCalls++;
|
|
1169
2181
|
return [
|
|
1170
2182
|
ev("swarm.tool_call", base(ctx, nextTs()), {
|
|
1171
2183
|
toolCallId: p.toolCallId,
|
|
1172
2184
|
name,
|
|
1173
2185
|
args: p.args,
|
|
1174
|
-
|
|
2186
|
+
// SP5 truth-fix: emit the RESOLVED needsApproval (policy + the tool's flag), not a hardcoded false, so
|
|
2187
|
+
// the UI reflects reality — a tool that will suspend-for-approval is shown as needing approval.
|
|
2188
|
+
needsApproval: gatesApproval(name)
|
|
1175
2189
|
})
|
|
1176
2190
|
];
|
|
1177
2191
|
}
|
|
@@ -1200,9 +2214,19 @@ function mapChunk(part, ctx, gov, modelIdFor, nextTs, streamed, delegateBudgets)
|
|
|
1200
2214
|
const output = p.output;
|
|
1201
2215
|
const usage = output?.usage;
|
|
1202
2216
|
if (usage) {
|
|
1203
|
-
const
|
|
2217
|
+
const counters = activity.get(ctx.agentSlug);
|
|
2218
|
+
const u = extractUsage(usage);
|
|
2219
|
+
if (counters && (counters.toolCalls || counters.agentActivations)) {
|
|
2220
|
+
u.toolCalls = counters.toolCalls;
|
|
2221
|
+
u.agentActivations = counters.agentActivations;
|
|
2222
|
+
}
|
|
2223
|
+
activity.delete(ctx.agentSlug);
|
|
1204
2224
|
gov.addUsage(modelId, u);
|
|
1205
2225
|
delegateBudgets?.addUsage(ctx.agentSlug, modelId, u);
|
|
2226
|
+
const cost = gov.costOf(modelId, u);
|
|
2227
|
+
const generationId = `${ctx.runId}:${segmentIndex}:${turnUsage.length}`;
|
|
2228
|
+
turnUsage.push({ slug: ctx.agentSlug, breakdown: u, cost });
|
|
2229
|
+
return [ev("swarm.usage", base(ctx, nextTs()), { slug: ctx.agentSlug, modelId, breakdown: u, cost, generationId })];
|
|
1206
2230
|
}
|
|
1207
2231
|
return [];
|
|
1208
2232
|
}
|
|
@@ -1213,7 +2237,7 @@ function mapChunk(part, ctx, gov, modelIdFor, nextTs, streamed, delegateBudgets)
|
|
|
1213
2237
|
const inner = p.output;
|
|
1214
2238
|
if (!inner || typeof inner.type !== "string") return [];
|
|
1215
2239
|
if (inner.type === "text-delta") streamed.add(p.toolCallId);
|
|
1216
|
-
return mapChunk(inner, { ...ctx, agentSlug: subSlug }, gov, modelIdFor, nextTs, streamed, delegateBudgets);
|
|
2240
|
+
return mapChunk(inner, { ...ctx, agentSlug: subSlug }, gov, modelIdFor, nextTs, streamed, delegateBudgets, activity, gatesApproval, turnUsage, segmentIndex);
|
|
1217
2241
|
}
|
|
1218
2242
|
case "tool-error": {
|
|
1219
2243
|
const name = p.toolName ?? "";
|
|
@@ -1242,8 +2266,125 @@ function allowListModelProvider(opts) {
|
|
|
1242
2266
|
};
|
|
1243
2267
|
}
|
|
1244
2268
|
|
|
2269
|
+
// src/rules.ts
|
|
2270
|
+
import {
|
|
2271
|
+
deny,
|
|
2272
|
+
ask,
|
|
2273
|
+
toolPolicyDecision
|
|
2274
|
+
} from "@nightowlsdev/hooks";
|
|
2275
|
+
var GLOB_CACHE = /* @__PURE__ */ new Map();
|
|
2276
|
+
function globRegex(pattern) {
|
|
2277
|
+
let re = GLOB_CACHE.get(pattern);
|
|
2278
|
+
if (!re) {
|
|
2279
|
+
re = new RegExp("^" + pattern.replace(/[.*+?^${}()|[\]\\]/g, (c) => c === "*" ? ".*" : "\\" + c) + "$");
|
|
2280
|
+
GLOB_CACHE.set(pattern, re);
|
|
2281
|
+
}
|
|
2282
|
+
return re;
|
|
2283
|
+
}
|
|
2284
|
+
function globMatch(pattern, value) {
|
|
2285
|
+
if (pattern === value) return true;
|
|
2286
|
+
if (!pattern.includes("*")) return false;
|
|
2287
|
+
return globRegex(pattern).test(value);
|
|
2288
|
+
}
|
|
2289
|
+
function matchField(field, value) {
|
|
2290
|
+
if (field === void 0) return true;
|
|
2291
|
+
const arr = Array.isArray(field) ? field : [field];
|
|
2292
|
+
return arr.some((p) => globMatch(p, value));
|
|
2293
|
+
}
|
|
2294
|
+
function ruleMatchesTool(rule, ev2) {
|
|
2295
|
+
if (rule.seam !== "tool") return false;
|
|
2296
|
+
if (rule.scopeAgent !== void 0 && rule.scopeAgent !== ev2.agentSlug) return false;
|
|
2297
|
+
const w = rule.when;
|
|
2298
|
+
if (!matchField(w.agent, ev2.agentSlug)) return false;
|
|
2299
|
+
if (!matchField(w.tool, ev2.toolName)) return false;
|
|
2300
|
+
if (w.origin !== void 0 && w.origin !== ev2.origin) return false;
|
|
2301
|
+
return true;
|
|
2302
|
+
}
|
|
2303
|
+
function ruleMatchesGeneration(rule, ev2) {
|
|
2304
|
+
if (rule.seam !== "generation") return false;
|
|
2305
|
+
if (rule.scopeAgent !== void 0 && rule.scopeAgent !== ev2.agentSlug) return false;
|
|
2306
|
+
const w = rule.when;
|
|
2307
|
+
if (!matchField(w.agent, ev2.agentSlug)) return false;
|
|
2308
|
+
if (!matchField(w.model, ev2.modelId)) return false;
|
|
2309
|
+
return true;
|
|
2310
|
+
}
|
|
2311
|
+
var TOOL_RANK = { deny: 2, ask: 1, allow: 0 };
|
|
2312
|
+
function mostRestrictiveTool(a, b) {
|
|
2313
|
+
return TOOL_RANK[b.action] > TOOL_RANK[a.action] ? b : a;
|
|
2314
|
+
}
|
|
2315
|
+
function errMessage2(err) {
|
|
2316
|
+
return err instanceof Error ? err.message : String(err);
|
|
2317
|
+
}
|
|
2318
|
+
function composeToolHooks(opts) {
|
|
2319
|
+
const rules = opts.rules.filter((r) => r.seam === "tool" && r.level === "enforce");
|
|
2320
|
+
return async (ev2) => {
|
|
2321
|
+
let decision = toolPolicyDecision(ev2, opts.policy);
|
|
2322
|
+
if (opts.host) {
|
|
2323
|
+
try {
|
|
2324
|
+
decision = mostRestrictiveTool(decision, await opts.host(ev2));
|
|
2325
|
+
} catch (err) {
|
|
2326
|
+
return deny(`preToolCall hook threw: ${errMessage2(err)}`);
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
if (decision.action === "deny") return decision;
|
|
2330
|
+
for (const r of rules) {
|
|
2331
|
+
if (!ruleMatchesTool(r, ev2)) continue;
|
|
2332
|
+
const rd = r.action.do === "deny" ? deny(r.action.reason ?? r.statement) : ask(r.action.reason ?? r.statement);
|
|
2333
|
+
decision = mostRestrictiveTool(decision, rd);
|
|
2334
|
+
if (decision.action === "deny") return decision;
|
|
2335
|
+
}
|
|
2336
|
+
return decision;
|
|
2337
|
+
};
|
|
2338
|
+
}
|
|
2339
|
+
function composeGenerationHooks(opts) {
|
|
2340
|
+
const rules = opts.rules.filter((r) => r.seam === "generation" && r.level === "enforce");
|
|
2341
|
+
if (!rules.length && !opts.host) return void 0;
|
|
2342
|
+
return async (ev2) => {
|
|
2343
|
+
if (opts.host) {
|
|
2344
|
+
try {
|
|
2345
|
+
const d = await opts.host(ev2);
|
|
2346
|
+
if (d.action === "deny") return d;
|
|
2347
|
+
} catch (err) {
|
|
2348
|
+
return deny(`preGeneration hook threw: ${errMessage2(err)}`);
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
for (const r of rules) {
|
|
2352
|
+
if (ruleMatchesGeneration(r, ev2)) return deny(r.action?.reason ?? r.statement);
|
|
2353
|
+
}
|
|
2354
|
+
return { action: "allow" };
|
|
2355
|
+
};
|
|
2356
|
+
}
|
|
2357
|
+
function softPolicyFor(slug, rules, workflows) {
|
|
2358
|
+
const out = [];
|
|
2359
|
+
for (const r of rules) {
|
|
2360
|
+
if (r.level !== "advise") continue;
|
|
2361
|
+
if (r.scopeAgent !== void 0 && r.scopeAgent !== slug) continue;
|
|
2362
|
+
if (!matchField(r.when.agent, slug)) continue;
|
|
2363
|
+
out.push(r.statement);
|
|
2364
|
+
}
|
|
2365
|
+
for (const w of workflows) {
|
|
2366
|
+
if (w.compliance !== "advisory" || !w.description) continue;
|
|
2367
|
+
if (w.scopeAgent !== void 0 && w.scopeAgent !== slug) continue;
|
|
2368
|
+
out.push(`Suggested procedure "${w.name}": ${w.description}`);
|
|
2369
|
+
}
|
|
2370
|
+
return out;
|
|
2371
|
+
}
|
|
2372
|
+
|
|
1245
2373
|
// src/define.ts
|
|
1246
2374
|
var MASTRA = /* @__PURE__ */ new WeakMap();
|
|
2375
|
+
var APPROVAL_SUSPEND_SCHEMA = z4.object({
|
|
2376
|
+
to: z4.string(),
|
|
2377
|
+
prompt: z4.string(),
|
|
2378
|
+
field: z4.object({
|
|
2379
|
+
kind: z4.enum(["confirm", "buttons", "select", "multiselect", "number", "text", "textarea"]),
|
|
2380
|
+
confirmLabel: z4.string().optional(),
|
|
2381
|
+
rejectLabel: z4.string().optional()
|
|
2382
|
+
}).optional(),
|
|
2383
|
+
asker: z4.string().optional(),
|
|
2384
|
+
kind: z4.literal("approval").optional(),
|
|
2385
|
+
toolName: z4.string().optional()
|
|
2386
|
+
});
|
|
2387
|
+
var APPROVAL_RESUME_SCHEMA = z4.object({ answer: z4.any() });
|
|
1247
2388
|
function defineTool(spec) {
|
|
1248
2389
|
const origin = spec.origin ?? "first-party";
|
|
1249
2390
|
const needsApproval = spec.needsApproval ?? origin === "mcp";
|
|
@@ -1252,26 +2393,135 @@ function defineTool(spec) {
|
|
|
1252
2393
|
description: spec.description ?? spec.name,
|
|
1253
2394
|
inputSchema: spec.inputSchema,
|
|
1254
2395
|
outputSchema: spec.outputSchema,
|
|
2396
|
+
// SP5: declare suspend/resume schemas so the action-approval gate can suspend-and-ask via
|
|
2397
|
+
// context.agent.suspend (Mastra gates `suspend` on a declared suspendSchema — same as the built-in `ask`).
|
|
2398
|
+
// The suspend payload is `ask`-shaped (+ approval metadata) so the engine emits the existing `swarm.question`;
|
|
2399
|
+
// resume carries the human's `{ answer }` (a confirm → boolean approve/reject).
|
|
2400
|
+
suspendSchema: APPROVAL_SUSPEND_SCHEMA,
|
|
2401
|
+
resumeSchema: APPROVAL_RESUME_SCHEMA,
|
|
1255
2402
|
// Mastra 1.38 (per SPIKE-FINDINGS item 3): execute is `(inputData, context) => out`
|
|
1256
2403
|
// with TWO positional args. `inputData` is the parsed input; tenant/user/run come
|
|
1257
|
-
// off `context.requestContext`.
|
|
2404
|
+
// off `context.requestContext`. `context.agent` (when an agent drives the call) carries the
|
|
2405
|
+
// suspend/resumeData handles SP5's action-approval gate uses to suspend-and-ask the human.
|
|
2406
|
+
//
|
|
2407
|
+
// SP5 — the mandatory action-approval HITL gate (the enforcement POINT). Before the side effect runs we
|
|
2408
|
+
// resolve the effective ToolDecision via the per-run `ToolGate` the engine injected on the RequestContext
|
|
2409
|
+
// (policy + the resolved needsApproval + the preToolCall hook):
|
|
2410
|
+
// • allow → execute as today.
|
|
2411
|
+
// • deny → return a blocked tool-result; the side effect NEVER runs.
|
|
2412
|
+
// • ask → SUSPEND via context.agent.suspend(...) with an `ask`-SHAPED payload, so the engine's existing
|
|
2413
|
+
// `tool-call-suspended` handler emits the SAME `swarm.question`; on resume the tool re-executes
|
|
2414
|
+
// with context.agent.resumeData = { answer } → approve runs the side effect, reject blocks it.
|
|
2415
|
+
// We reuse the EXISTING suspend/resume + question/answer machinery — no parallel one.
|
|
1258
2416
|
execute: async (inputData, context) => {
|
|
1259
2417
|
const rc = context?.requestContext;
|
|
2418
|
+
const tenantId = rc?.get?.("tenantId") ?? "default";
|
|
2419
|
+
const userId = rc?.get?.("userId") ?? "";
|
|
2420
|
+
const runId = rc?.get?.("runId") ?? "";
|
|
2421
|
+
const resolver = rc?.get?.(SECRET_RESOLVER_KEY);
|
|
2422
|
+
const scopedCtx = {
|
|
2423
|
+
tenantId,
|
|
2424
|
+
userId,
|
|
2425
|
+
runId,
|
|
2426
|
+
agentSlug: rc?.get?.("agentSlug") ?? "",
|
|
2427
|
+
threadId: rc?.get?.("threadId") ?? "",
|
|
2428
|
+
...(() => {
|
|
2429
|
+
const v = rc?.get?.("agentVersion");
|
|
2430
|
+
return typeof v === "number" ? { agentVersion: v } : {};
|
|
2431
|
+
})()
|
|
2432
|
+
};
|
|
1260
2433
|
const ctx = {
|
|
1261
|
-
tenantId
|
|
1262
|
-
userId
|
|
1263
|
-
runId
|
|
2434
|
+
tenantId,
|
|
2435
|
+
userId,
|
|
2436
|
+
runId,
|
|
2437
|
+
secrets: bindSecrets(resolver, scopedCtx),
|
|
2438
|
+
// FR-003: the per-run state handle the engine put on the rc (same object across the run's tool calls +
|
|
2439
|
+
// delegated sub-agents). Absent on a raw test stream built without the engine — then `ctx.state` is
|
|
2440
|
+
// undefined, unchanged from prior behaviour.
|
|
2441
|
+
state: rc?.get?.(RUN_STATE_KEY)
|
|
1264
2442
|
};
|
|
1265
|
-
|
|
2443
|
+
const run = () => spec.execute(inputData, ctx);
|
|
2444
|
+
const agentCtx = context?.agent;
|
|
2445
|
+
if (agentCtx?.resumeData !== void 0 && agentCtx.resumeData !== null) {
|
|
2446
|
+
const answer = agentCtx.resumeData.answer;
|
|
2447
|
+
if (isApproved(answer)) return run();
|
|
2448
|
+
throw new ToolBlockedError(spec.name, "rejected by approver");
|
|
2449
|
+
}
|
|
2450
|
+
const gate = rc?.get?.(TOOL_GATE_KEY);
|
|
2451
|
+
if (!gate || typeof agentCtx?.suspend !== "function") return run();
|
|
2452
|
+
const agentSlug = deriveAsker(agentCtx, rc);
|
|
2453
|
+
const decision = await gate(
|
|
2454
|
+
toolPreCallEvent({
|
|
2455
|
+
runId: ctx.runId,
|
|
2456
|
+
tenantId: ctx.tenantId,
|
|
2457
|
+
agentSlug,
|
|
2458
|
+
toolName: spec.name,
|
|
2459
|
+
origin,
|
|
2460
|
+
needsApproval,
|
|
2461
|
+
args: inputData
|
|
2462
|
+
})
|
|
2463
|
+
);
|
|
2464
|
+
if (decision.action === "allow") return run();
|
|
2465
|
+
if (decision.action === "deny") throw new ToolBlockedError(spec.name, decision.reason);
|
|
2466
|
+
await agentCtx.suspend(approvalSuspendPayload({ toolName: spec.name, asker: agentSlug, reason: decision.reason }));
|
|
2467
|
+
throw new ToolBlockedError(spec.name, "awaiting approval");
|
|
1266
2468
|
}
|
|
1267
2469
|
});
|
|
1268
2470
|
const handle = { name: spec.name, needsApproval, origin };
|
|
1269
2471
|
MASTRA.set(handle, mastraTool);
|
|
2472
|
+
setToolExecutor(handle, (args, c) => spec.execute(args, c));
|
|
1270
2473
|
return handle;
|
|
1271
2474
|
}
|
|
1272
2475
|
function defineSkill(tool) {
|
|
1273
2476
|
return tool;
|
|
1274
2477
|
}
|
|
2478
|
+
function deriveAsker(agentCtx, rc) {
|
|
2479
|
+
const agentId = agentCtx?.agentId ?? "";
|
|
2480
|
+
if (agentId.startsWith("swarm-sub-")) return agentId.slice("swarm-sub-".length);
|
|
2481
|
+
return rc?.get?.("agentSlug") ?? "";
|
|
2482
|
+
}
|
|
2483
|
+
var CLIENT_ACTION_SUSPEND_SCHEMA = z4.object({
|
|
2484
|
+
clientAction: z4.object({ tool: z4.string(), input: z4.any(), needsApproval: z4.boolean().optional() }),
|
|
2485
|
+
asker: z4.string().optional()
|
|
2486
|
+
});
|
|
2487
|
+
var CLIENT_ACTION_RESUME_SCHEMA = z4.object({ answer: z4.any() });
|
|
2488
|
+
var ClientToolError = class extends Error {
|
|
2489
|
+
constructor(toolName, reason) {
|
|
2490
|
+
super(reason ? `client tool ${toolName} failed: ${reason}` : `client tool ${toolName} failed`);
|
|
2491
|
+
this.name = "ClientToolError";
|
|
2492
|
+
}
|
|
2493
|
+
};
|
|
2494
|
+
function defineClientTool(spec) {
|
|
2495
|
+
const needsApproval = spec.needsApproval ?? false;
|
|
2496
|
+
const mastraTool = createTool4({
|
|
2497
|
+
id: spec.name,
|
|
2498
|
+
description: spec.description ?? spec.name,
|
|
2499
|
+
inputSchema: spec.inputSchema,
|
|
2500
|
+
outputSchema: spec.outputSchema,
|
|
2501
|
+
suspendSchema: CLIENT_ACTION_SUSPEND_SCHEMA,
|
|
2502
|
+
resumeSchema: CLIENT_ACTION_RESUME_SCHEMA,
|
|
2503
|
+
execute: async (inputData, context) => {
|
|
2504
|
+
const rc = context?.requestContext;
|
|
2505
|
+
const agentCtx = context?.agent;
|
|
2506
|
+
if (agentCtx?.resumeData !== void 0 && agentCtx.resumeData !== null) {
|
|
2507
|
+
const answer = agentCtx.resumeData.answer;
|
|
2508
|
+
if (answer && typeof answer === "object" && "error" in answer && answer.error) {
|
|
2509
|
+
throw new ClientToolError(spec.name, String(answer.error));
|
|
2510
|
+
}
|
|
2511
|
+
return answer && typeof answer === "object" && "output" in answer ? answer.output : answer;
|
|
2512
|
+
}
|
|
2513
|
+
if (typeof agentCtx?.suspend !== "function") {
|
|
2514
|
+
throw new ClientToolError(spec.name, "client tools require an agent-driven run (no server execute)");
|
|
2515
|
+
}
|
|
2516
|
+
const asker = deriveAsker(agentCtx, rc);
|
|
2517
|
+
await agentCtx.suspend({ clientAction: { tool: spec.name, input: inputData, needsApproval }, asker });
|
|
2518
|
+
throw new ClientToolError(spec.name, "awaiting client action");
|
|
2519
|
+
}
|
|
2520
|
+
});
|
|
2521
|
+
const handle = { name: spec.name, needsApproval, origin: "first-party" };
|
|
2522
|
+
MASTRA.set(handle, mastraTool);
|
|
2523
|
+
return handle;
|
|
2524
|
+
}
|
|
1275
2525
|
function __getMastraTool(t) {
|
|
1276
2526
|
return MASTRA.get(t);
|
|
1277
2527
|
}
|
|
@@ -1282,6 +2532,10 @@ function defineAgent(spec) {
|
|
|
1282
2532
|
// The concrete skill handles ride along on the def so defineSwarm can build
|
|
1283
2533
|
// a per-swarm resolver. No module-level registry → no cross-swarm leakage.
|
|
1284
2534
|
skills,
|
|
2535
|
+
// Per-agent policy rides on the def (engine-local), stamped with this agent's scope so defineSwarm can
|
|
2536
|
+
// collect + apply it without persisting to the versioned AgentVersion row (D3).
|
|
2537
|
+
...spec.rules ? { rules: spec.rules.map((r) => ({ ...r, scopeAgent: spec.slug })) } : {},
|
|
2538
|
+
...spec.workflow ? { workflow: { ...spec.workflow, scopeAgent: spec.slug } } : {},
|
|
1285
2539
|
head: {
|
|
1286
2540
|
slug: spec.slug,
|
|
1287
2541
|
version: 1,
|
|
@@ -1295,11 +2549,217 @@ function defineAgent(spec) {
|
|
|
1295
2549
|
}
|
|
1296
2550
|
};
|
|
1297
2551
|
}
|
|
2552
|
+
function defineRule(spec) {
|
|
2553
|
+
if (!spec.id) throw new Error("defineRule: `id` is required");
|
|
2554
|
+
if (!spec.statement) throw new Error(`defineRule(${spec.id}): \`statement\` is required`);
|
|
2555
|
+
const tools = spec.when.tool === void 0 ? [] : Array.isArray(spec.when.tool) ? spec.when.tool : [spec.when.tool];
|
|
2556
|
+
const seam = spec.on ?? (spec.when.model !== void 0 ? "generation" : "tool");
|
|
2557
|
+
if (spec.level === "enforce") {
|
|
2558
|
+
if (!spec.action) throw new Error(`defineRule(${spec.id}): enforce rules require an \`action\``);
|
|
2559
|
+
if (!spec.on && spec.when.tool === void 0 && spec.when.model === void 0) {
|
|
2560
|
+
throw new Error(`defineRule(${spec.id}): an enforce rule with an empty \`when\` must set \`on\` ("tool" | "generation")`);
|
|
2561
|
+
}
|
|
2562
|
+
if (spec.action.do === "ask") {
|
|
2563
|
+
if (seam !== "tool") throw new Error(`defineRule(${spec.id}): \`ask\` is tool-seam only (preGeneration cannot suspend)`);
|
|
2564
|
+
if (tools.some((t) => t.startsWith("agent-"))) {
|
|
2565
|
+
throw new Error(`defineRule(${spec.id}): \`ask\` cannot target a delegation (\`agent-*\`) \u2014 gateDelegation defers \`ask\`; use \`deny\``);
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2569
|
+
return { id: spec.id, statement: spec.statement, when: spec.when, level: spec.level, action: spec.action, seam };
|
|
2570
|
+
}
|
|
2571
|
+
function workflowRefTargets(step) {
|
|
2572
|
+
const out = [];
|
|
2573
|
+
const scan = (o) => {
|
|
2574
|
+
for (const v of Object.values(o ?? {})) {
|
|
2575
|
+
if (v && typeof v === "object" && "$ref" in v) out.push(String(v.$ref));
|
|
2576
|
+
}
|
|
2577
|
+
};
|
|
2578
|
+
scan(step.args);
|
|
2579
|
+
scan(step.input);
|
|
2580
|
+
if (Array.isArray(step.next)) {
|
|
2581
|
+
for (const t of step.next) if (t.when?.$ref) out.push(t.when.$ref);
|
|
2582
|
+
}
|
|
2583
|
+
return out;
|
|
2584
|
+
}
|
|
2585
|
+
function defineWorkflow(spec) {
|
|
2586
|
+
if (!spec.name) throw new Error("defineWorkflow: `name` is required");
|
|
2587
|
+
if (!spec.steps?.length) throw new Error(`defineWorkflow(${spec.name}): at least one step is required`);
|
|
2588
|
+
const ids = /* @__PURE__ */ new Set();
|
|
2589
|
+
for (const s of spec.steps) {
|
|
2590
|
+
if (ids.has(s.id)) throw new Error(`defineWorkflow(${spec.name}): duplicate step id "${s.id}"`);
|
|
2591
|
+
ids.add(s.id);
|
|
2592
|
+
const kinds = [s.agent !== void 0, s.tool !== void 0, s.human !== void 0].filter(Boolean).length;
|
|
2593
|
+
if (kinds !== 1) throw new Error(`defineWorkflow(${spec.name}): step "${s.id}" must have exactly one of agent/tool/human`);
|
|
2594
|
+
}
|
|
2595
|
+
const start = spec.start ?? spec.steps[0].id;
|
|
2596
|
+
if (!ids.has(start)) throw new Error(`defineWorkflow(${spec.name}): start "${start}" is not a known step`);
|
|
2597
|
+
const nextOf = (s) => {
|
|
2598
|
+
const outs = s.next === void 0 ? [] : typeof s.next === "string" ? [s.next] : s.next.map((t) => t.to);
|
|
2599
|
+
if (s.onError && typeof s.onError === "object" && "to" in s.onError) outs.push(s.onError.to);
|
|
2600
|
+
return outs;
|
|
2601
|
+
};
|
|
2602
|
+
for (const s of spec.steps) {
|
|
2603
|
+
for (const t of nextOf(s)) if (!ids.has(t)) throw new Error(`defineWorkflow(${spec.name}): step "${s.id}" \u2192 unknown step "${t}"`);
|
|
2604
|
+
for (const r of workflowRefTargets(s)) {
|
|
2605
|
+
if (r !== "input" && !(r.startsWith("steps.") && ids.has(r.slice("steps.".length)))) {
|
|
2606
|
+
throw new Error(`defineWorkflow(${spec.name}): step "${s.id}" has an invalid $ref "${r}"`);
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
const byId = new Map(spec.steps.map((s) => [s.id, s]));
|
|
2611
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2612
|
+
const stack = /* @__PURE__ */ new Set();
|
|
2613
|
+
const visit = (id) => {
|
|
2614
|
+
if (stack.has(id)) throw new Error(`defineWorkflow(${spec.name}): cycle detected at step "${id}"`);
|
|
2615
|
+
if (seen.has(id)) return;
|
|
2616
|
+
seen.add(id);
|
|
2617
|
+
stack.add(id);
|
|
2618
|
+
for (const t of nextOf(byId.get(id))) visit(t);
|
|
2619
|
+
stack.delete(id);
|
|
2620
|
+
};
|
|
2621
|
+
visit(start);
|
|
2622
|
+
return { name: spec.name, compliance: spec.compliance, description: spec.description, steps: spec.steps, start };
|
|
2623
|
+
}
|
|
1298
2624
|
function buildSkillResolver(agents) {
|
|
1299
2625
|
const map = /* @__PURE__ */ new Map();
|
|
1300
2626
|
for (const a of agents) for (const s of a.skills ?? []) map.set(s.name, s);
|
|
1301
2627
|
return (name) => map.get(name);
|
|
1302
2628
|
}
|
|
2629
|
+
function isConnectorLooking(name) {
|
|
2630
|
+
return name.includes(".");
|
|
2631
|
+
}
|
|
2632
|
+
function ruleToolRefs(rule) {
|
|
2633
|
+
if (rule.seam !== "tool") return [];
|
|
2634
|
+
const t = rule.when.tool;
|
|
2635
|
+
const names = t === void 0 ? [] : Array.isArray(t) ? t : [t];
|
|
2636
|
+
return names.filter((n) => !n.includes("*"));
|
|
2637
|
+
}
|
|
2638
|
+
var CRED_REF_KEYS = /* @__PURE__ */ new Set(["secretref", "credentialref", "connectionid", "owlconnections"]);
|
|
2639
|
+
var normKey = (k) => k.toLowerCase().replace(/[-_]/g, "");
|
|
2640
|
+
function assertNoCredRefs(obj, where) {
|
|
2641
|
+
const walk = (v) => {
|
|
2642
|
+
if (Array.isArray(v)) {
|
|
2643
|
+
v.forEach(walk);
|
|
2644
|
+
return;
|
|
2645
|
+
}
|
|
2646
|
+
if (v && typeof v === "object") {
|
|
2647
|
+
for (const [k, val] of Object.entries(v)) {
|
|
2648
|
+
if (CRED_REF_KEYS.has(normKey(k))) {
|
|
2649
|
+
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`);
|
|
2650
|
+
}
|
|
2651
|
+
walk(val);
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
};
|
|
2655
|
+
walk(obj);
|
|
2656
|
+
}
|
|
2657
|
+
function defineBundle(spec) {
|
|
2658
|
+
if (!spec.slug) throw new Error("defineBundle: `slug` is required");
|
|
2659
|
+
if (!spec.agents?.length) throw new Error(`defineBundle(${spec.slug}): at least one agent is required`);
|
|
2660
|
+
const members = /* @__PURE__ */ new Set();
|
|
2661
|
+
const handles = /* @__PURE__ */ new Set();
|
|
2662
|
+
for (const a of spec.agents) {
|
|
2663
|
+
if (members.has(a.slug)) throw new Error(`defineBundle(${spec.slug}): duplicate agent slug "${a.slug}"`);
|
|
2664
|
+
members.add(a.slug);
|
|
2665
|
+
for (const s of a.skills ?? []) handles.add(s.name);
|
|
2666
|
+
}
|
|
2667
|
+
const requires = spec.requires ?? [];
|
|
2668
|
+
const requiredSlugs = new Set(requires.map((r) => r.slug));
|
|
2669
|
+
const connectorGrants = spec.connectorGrants ?? [];
|
|
2670
|
+
const grantedByMember = /* @__PURE__ */ new Map();
|
|
2671
|
+
const allGranted = /* @__PURE__ */ new Set();
|
|
2672
|
+
for (const g of connectorGrants) {
|
|
2673
|
+
if (!members.has(g.agentSlug)) {
|
|
2674
|
+
throw new Error(`defineBundle(${spec.slug}): connector grant targets unknown agent "${g.agentSlug}" (not a bundle member)`);
|
|
2675
|
+
}
|
|
2676
|
+
const set = grantedByMember.get(g.agentSlug) ?? /* @__PURE__ */ new Set();
|
|
2677
|
+
for (const action of g.actions) {
|
|
2678
|
+
const full = action.includes(".") ? action : `${g.provider}.${action}`;
|
|
2679
|
+
set.add(full);
|
|
2680
|
+
allGranted.add(full);
|
|
2681
|
+
}
|
|
2682
|
+
grantedByMember.set(g.agentSlug, set);
|
|
2683
|
+
}
|
|
2684
|
+
const agents = spec.agents.map((a) => {
|
|
2685
|
+
const granted = grantedByMember.get(a.slug);
|
|
2686
|
+
if (!granted || granted.size === 0) return a;
|
|
2687
|
+
const extra = [...granted].filter((n) => !a.head.skillNames.includes(n));
|
|
2688
|
+
return extra.length ? { ...a, head: { ...a.head, skillNames: [...a.head.skillNames, ...extra] } } : a;
|
|
2689
|
+
});
|
|
2690
|
+
const requireResolvable = (name, allowed, where) => {
|
|
2691
|
+
if (handles.has(name)) return;
|
|
2692
|
+
if (allowed?.has(name)) return;
|
|
2693
|
+
if (isConnectorLooking(name)) {
|
|
2694
|
+
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`);
|
|
2695
|
+
}
|
|
2696
|
+
throw new Error(`defineBundle(${spec.slug}): ${where} references skill/tool "${name}" with no first-party handle in the bundle`);
|
|
2697
|
+
};
|
|
2698
|
+
for (const a of agents) {
|
|
2699
|
+
const granted = grantedByMember.get(a.slug);
|
|
2700
|
+
for (const name of a.head.skillNames) requireResolvable(name, granted, `agent "${a.slug}"`);
|
|
2701
|
+
for (const d of a.head.delegateSlugs) {
|
|
2702
|
+
if (!members.has(d) && !requiredSlugs.has(d)) {
|
|
2703
|
+
throw new Error(`defineBundle(${spec.slug}): agent "${a.slug}" delegates to "${d}", which is neither a bundle member nor a declared \`requires\` dependency`);
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
const allRules = [...spec.rules ?? [], ...spec.agents.flatMap((a) => a.rules ?? [])];
|
|
2708
|
+
for (const r of allRules) {
|
|
2709
|
+
for (const t of ruleToolRefs(r)) {
|
|
2710
|
+
if (t.startsWith("agent-")) continue;
|
|
2711
|
+
requireResolvable(t, allGranted, `rule "${r.id}"`);
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2714
|
+
const allWorkflows = [...spec.workflows ?? [], ...spec.agents.flatMap((a) => a.workflow ? [a.workflow] : [])];
|
|
2715
|
+
for (const w of allWorkflows) {
|
|
2716
|
+
for (const step of w.steps) {
|
|
2717
|
+
if (step.tool !== void 0) requireResolvable(step.tool, allGranted, `workflow "${w.name}" step "${step.id}"`);
|
|
2718
|
+
assertNoCredRefs(step.args, `workflow "${w.name}" step "${step.id}" args`);
|
|
2719
|
+
assertNoCredRefs(step.input, `workflow "${w.name}" step "${step.id}" input`);
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
return {
|
|
2723
|
+
slug: spec.slug,
|
|
2724
|
+
...spec.title ? { title: spec.title } : {},
|
|
2725
|
+
agents,
|
|
2726
|
+
// members carry any granted connector action names in skillNames
|
|
2727
|
+
rules: spec.rules ?? [],
|
|
2728
|
+
// SWARM-scoped only (per-agent rules stay on the AgentDefs)
|
|
2729
|
+
workflows: spec.workflows ?? [],
|
|
2730
|
+
// SWARM-scoped only
|
|
2731
|
+
connectorGrants,
|
|
2732
|
+
requires
|
|
2733
|
+
};
|
|
2734
|
+
}
|
|
2735
|
+
function mergeBundle(cfg, bundle) {
|
|
2736
|
+
const existing = new Set(cfg.agents.map((a) => a.slug));
|
|
2737
|
+
for (const a of bundle.agents) {
|
|
2738
|
+
if (existing.has(a.slug)) {
|
|
2739
|
+
throw new Error(`mergeBundle(${bundle.slug}): agent "${a.slug}" already exists in the swarm config \u2014 bundle members must not shadow host agents`);
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
return {
|
|
2743
|
+
...cfg,
|
|
2744
|
+
agents: [...cfg.agents, ...bundle.agents],
|
|
2745
|
+
rules: [...cfg.rules ?? [], ...bundle.rules],
|
|
2746
|
+
workflows: [...cfg.workflows ?? [], ...bundle.workflows]
|
|
2747
|
+
};
|
|
2748
|
+
}
|
|
2749
|
+
function toBundleContent(def) {
|
|
2750
|
+
return {
|
|
2751
|
+
slug: def.slug,
|
|
2752
|
+
...def.title ? { title: def.title } : {},
|
|
2753
|
+
agents: def.agents.map((a) => {
|
|
2754
|
+
const { version: _version, ...content } = a.head;
|
|
2755
|
+
return content;
|
|
2756
|
+
}),
|
|
2757
|
+
rules: def.rules,
|
|
2758
|
+
workflows: def.workflows,
|
|
2759
|
+
connectorGrants: def.connectorGrants,
|
|
2760
|
+
requires: def.requires
|
|
2761
|
+
};
|
|
2762
|
+
}
|
|
1303
2763
|
var ASK_TOOL_NAME = "ask";
|
|
1304
2764
|
var askFieldSchema = z4.object({
|
|
1305
2765
|
kind: z4.enum(["confirm", "buttons", "select", "multiselect", "number", "text", "textarea"]),
|
|
@@ -1342,23 +2802,63 @@ function buildAskMastraTool() {
|
|
|
1342
2802
|
function defineSwarm(cfg) {
|
|
1343
2803
|
const seedable = cfg.storage;
|
|
1344
2804
|
for (const a of cfg.agents) seedable.seedAgent?.(a.head);
|
|
2805
|
+
const rules = [...cfg.rules ?? [], ...cfg.agents.flatMap((a) => a.rules ?? [])];
|
|
2806
|
+
const workflows = [...cfg.workflows ?? [], ...cfg.agents.flatMap((a) => a.workflow ? [a.workflow] : [])];
|
|
2807
|
+
const namedWorkflows = workflows.filter((w) => w.compliance === "strict" && w.scopeAgent === void 0);
|
|
2808
|
+
const agentWorkflows = {};
|
|
2809
|
+
for (const w of workflows) if (w.compliance === "strict" && w.scopeAgent !== void 0) agentWorkflows[w.scopeAgent] = w;
|
|
2810
|
+
const hasSoft = rules.some((r) => r.level === "advise") || workflows.some((w) => w.compliance === "advisory" && !!w.description);
|
|
2811
|
+
const softPolicy = hasSoft ? (slug) => softPolicyFor(slug, rules, workflows) : void 0;
|
|
2812
|
+
const policy = cfg.toolApproval ?? { mode: "flag" };
|
|
2813
|
+
const enforceTool = rules.filter((r) => r.seam === "tool" && r.level === "enforce");
|
|
2814
|
+
const enforceGen = rules.filter((r) => r.seam === "generation" && r.level === "enforce");
|
|
2815
|
+
const composedHooks = { ...cfg.hooks };
|
|
2816
|
+
if (enforceTool.length) composedHooks.preToolCall = composeToolHooks({ rules: enforceTool, host: cfg.hooks?.preToolCall, policy });
|
|
2817
|
+
if (enforceGen.length) composedHooks.preGeneration = composeGenerationHooks({ rules: enforceGen, host: cfg.hooks?.preGeneration });
|
|
1345
2818
|
const engine = new SwarmEngine({
|
|
2819
|
+
softPolicy,
|
|
2820
|
+
workflows: namedWorkflows,
|
|
2821
|
+
agentWorkflows,
|
|
1346
2822
|
storage: cfg.storage,
|
|
1347
2823
|
model: allowListModelProvider({ allow: cfg.models.allow }),
|
|
1348
2824
|
modelFactory: cfg.modelFactory,
|
|
2825
|
+
// SP10: thread the optional cheap-model router onto the engine. The allow-list above still validates every
|
|
2826
|
+
// resolved model — incl. a routed tier model. Undefined ⇒ no routing (identical to today).
|
|
2827
|
+
tier: cfg.models.tier,
|
|
1349
2828
|
cost: cfg.cost,
|
|
1350
2829
|
// Per-swarm skill registry, built from the agents passed in. No global state.
|
|
1351
2830
|
resolveSkill: buildSkillResolver(cfg.agents),
|
|
2831
|
+
// PR2: opt-in connector-tools resolver (built by the host via materializeConnectors). Undefined ⇒ no-op.
|
|
2832
|
+
connectorTools: cfg.connectorTools,
|
|
1352
2833
|
telemetry: resolveTelemetry(cfg.telemetry),
|
|
1353
2834
|
mastraStore: cfg.mastraStore,
|
|
1354
2835
|
memory: cfg.memory,
|
|
1355
2836
|
pageContext: cfg.pageContext,
|
|
1356
2837
|
scratchpad: cfg.scratchpad,
|
|
1357
|
-
recallLane: cfg.recallLane
|
|
2838
|
+
recallLane: cfg.recallLane,
|
|
2839
|
+
// SP2 + SP5: build the decision-hook dispatcher once per swarm, baking in the non-removable tool-approval
|
|
2840
|
+
// policy. Always present (allow-all hooks + `{ mode: "flag" }` policy when unconfigured, so the engine's
|
|
2841
|
+
// seams stay uniform — no null-checks on the hot path). The dispatcher combines policy + the per-tool flag +
|
|
2842
|
+
// the optional `preToolCall` hook into the effective ToolDecision.
|
|
2843
|
+
hooks: createHookDispatcher(composedHooks, cfg.toolApproval),
|
|
2844
|
+
// SP5: also pass the policy standalone so an engine built without a dispatcher (direct construction) still
|
|
2845
|
+
// enforces it; redundant here (the dispatcher already carries it) but keeps EngineOpts self-describing.
|
|
2846
|
+
toolApproval: cfg.toolApproval,
|
|
2847
|
+
// SP15: thread the optional SecretResolver onto the engine, which injects it per-run on the RequestContext.
|
|
2848
|
+
secrets: cfg.secrets,
|
|
2849
|
+
// SP3: best-effort per-event observer (metering debit/settle) — transport-agnostic, fired in run + resume.
|
|
2850
|
+
onEvent: cfg.onEvent,
|
|
2851
|
+
verifyCompletion: cfg.verifyCompletion,
|
|
2852
|
+
// FR-003: per-run lifecycle hooks (seed `ctx.state` at run start, drain at run end).
|
|
2853
|
+
onRunStart: cfg.onRunStart,
|
|
2854
|
+
onRunEnd: cfg.onRunEnd
|
|
1358
2855
|
});
|
|
1359
2856
|
return { engine };
|
|
1360
2857
|
}
|
|
1361
2858
|
|
|
2859
|
+
// src/index.ts
|
|
2860
|
+
import { HookDispatcher as HookDispatcher2, createHookDispatcher as createHookDispatcher2, defineHook, deny as deny2, ask as ask2, ALLOW, ALLOW_TOOL, DEFAULT_READ_ONLY_TOOLS } from "@nightowlsdev/hooks";
|
|
2861
|
+
|
|
1362
2862
|
// src/storage/memory.ts
|
|
1363
2863
|
var InMemoryStorage = class {
|
|
1364
2864
|
evts = [];
|
|
@@ -1375,6 +2875,8 @@ var InMemoryStorage = class {
|
|
|
1375
2875
|
heads = /* @__PURE__ */ new Map();
|
|
1376
2876
|
// key: tenantId:slug -> version
|
|
1377
2877
|
pads = /* @__PURE__ */ new Map();
|
|
2878
|
+
threadRows = /* @__PURE__ */ new Map();
|
|
2879
|
+
// FR-009
|
|
1378
2880
|
seedAgent(v, tenantId = "default") {
|
|
1379
2881
|
this.agentRows.set(`${tenantId}:${v.slug}:${v.version}`, v);
|
|
1380
2882
|
this.heads.set(`${tenantId}:${v.slug}`, v.version);
|
|
@@ -1383,7 +2885,7 @@ var InMemoryStorage = class {
|
|
|
1383
2885
|
this.suspends.set(`${tenantId}:${followupId}`, { runId, toolCallId });
|
|
1384
2886
|
}
|
|
1385
2887
|
markFollowupAnswered(followupId, tenantId) {
|
|
1386
|
-
this.suspends.delete(`${tenantId}:${followupId}`);
|
|
2888
|
+
return this.suspends.delete(`${tenantId}:${followupId}`);
|
|
1387
2889
|
}
|
|
1388
2890
|
/** Test/host helper: read a run row (the RunStore interface is write-mostly). */
|
|
1389
2891
|
getRun(runId) {
|
|
@@ -1458,6 +2960,18 @@ var InMemoryStorage = class {
|
|
|
1458
2960
|
},
|
|
1459
2961
|
getWaitpoint: async (followupId) => this.waitpoints.get(followupId) ?? null
|
|
1460
2962
|
};
|
|
2963
|
+
// FR-009: idempotent thread-row creation. The dev store has no FK, so `messages.append` never threw `unknown
|
|
2964
|
+
// thread` here — this implements the contract so the engine's run-start ensure works against the in-memory store
|
|
2965
|
+
// too, and a host can read back the recorded thread (getThread) in tests.
|
|
2966
|
+
threads = {
|
|
2967
|
+
ensure: async ({ id, orgId, userId, projectId }) => {
|
|
2968
|
+
if (!this.threadRows.has(id)) this.threadRows.set(id, { id, orgId, userId, projectId });
|
|
2969
|
+
}
|
|
2970
|
+
};
|
|
2971
|
+
/** Test/host helper: read a recorded thread row. */
|
|
2972
|
+
getThread(id) {
|
|
2973
|
+
return this.threadRows.get(id);
|
|
2974
|
+
}
|
|
1461
2975
|
messages = {
|
|
1462
2976
|
append: async (m) => {
|
|
1463
2977
|
this.msgs.push(m);
|
|
@@ -1497,20 +3011,118 @@ var InMemoryStorage = class {
|
|
|
1497
3011
|
};
|
|
1498
3012
|
};
|
|
1499
3013
|
|
|
3014
|
+
// src/run-agent.ts
|
|
3015
|
+
function uid() {
|
|
3016
|
+
return globalThis.crypto?.randomUUID?.() ?? `id-${Math.random().toString(36).slice(2)}-${Date.now().toString(36)}`;
|
|
3017
|
+
}
|
|
3018
|
+
async function drainTrajectory(stream) {
|
|
3019
|
+
const events = [];
|
|
3020
|
+
let output = "";
|
|
3021
|
+
for await (const e of stream) {
|
|
3022
|
+
events.push(e);
|
|
3023
|
+
if (e.type === "swarm.message") {
|
|
3024
|
+
const d = e.data;
|
|
3025
|
+
if (d.role === "assistant") output += d.delta ?? d.text ?? "";
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
3028
|
+
return { events, output };
|
|
3029
|
+
}
|
|
3030
|
+
async function runToTrajectory(target, input, ctx) {
|
|
3031
|
+
const engine = "engine" in target ? target.engine : target;
|
|
3032
|
+
const runInput = typeof input === "string" ? { message: input } : input;
|
|
3033
|
+
const full = ephemeralCtx(ctx?.agentSlug ?? "agent", ctx);
|
|
3034
|
+
return drainTrajectory(engine.run(runInput, full));
|
|
3035
|
+
}
|
|
3036
|
+
function ephemeralCtx(agentSlug, over) {
|
|
3037
|
+
return {
|
|
3038
|
+
tenantId: over?.tenantId ?? "default",
|
|
3039
|
+
userId: over?.userId ?? "local",
|
|
3040
|
+
agentSlug: over?.agentSlug ?? agentSlug,
|
|
3041
|
+
runId: over?.runId ?? uid(),
|
|
3042
|
+
threadId: over?.threadId ?? uid(),
|
|
3043
|
+
...over?.agentVersion !== void 0 ? { agentVersion: over.agentVersion } : {}
|
|
3044
|
+
};
|
|
3045
|
+
}
|
|
3046
|
+
function buildSingleAgentSwarm(def, opts) {
|
|
3047
|
+
const t = opts.models?.tier;
|
|
3048
|
+
const tierAllow = t ? [t.tiers.swift, ...t.tiers.genius ? [t.tiers.genius] : []] : [];
|
|
3049
|
+
const allow = opts.models?.allow ?? [def.head.modelId, ...tierAllow];
|
|
3050
|
+
const storage = opts.storage ?? new InMemoryStorage();
|
|
3051
|
+
const cfg = {
|
|
3052
|
+
storage,
|
|
3053
|
+
agents: [def],
|
|
3054
|
+
models: { allow, ...opts.models?.tier ? { tier: opts.models.tier } : {} },
|
|
3055
|
+
modelFactory: opts.modelFactory,
|
|
3056
|
+
cost: { maxSteps: 50, maxCostUsd: 10, ...opts.cost },
|
|
3057
|
+
...opts.telemetry !== void 0 ? { telemetry: opts.telemetry } : {},
|
|
3058
|
+
...opts.memory !== void 0 ? { memory: opts.memory } : {},
|
|
3059
|
+
...opts.hooks !== void 0 ? { hooks: opts.hooks } : {},
|
|
3060
|
+
...opts.toolApproval !== void 0 ? { toolApproval: opts.toolApproval } : {},
|
|
3061
|
+
...opts.secrets !== void 0 ? { secrets: opts.secrets } : {},
|
|
3062
|
+
...opts.onEvent !== void 0 ? { onEvent: opts.onEvent } : {},
|
|
3063
|
+
...opts.onRunStart !== void 0 ? { onRunStart: opts.onRunStart } : {},
|
|
3064
|
+
...opts.onRunEnd !== void 0 ? { onRunEnd: opts.onRunEnd } : {},
|
|
3065
|
+
...opts.pageContext !== void 0 ? { pageContext: opts.pageContext } : {},
|
|
3066
|
+
...opts.mastraStore !== void 0 ? { mastraStore: opts.mastraStore } : {}
|
|
3067
|
+
};
|
|
3068
|
+
return defineSwarm(cfg);
|
|
3069
|
+
}
|
|
3070
|
+
async function runAgent(def, input, opts) {
|
|
3071
|
+
const swarm = buildSingleAgentSwarm(def, opts);
|
|
3072
|
+
const runInput = typeof input === "string" ? { message: input } : input;
|
|
3073
|
+
const ctx = ephemeralCtx(def.head.slug, { ...opts.ctx, agentSlug: def.head.slug });
|
|
3074
|
+
return drainTrajectory(swarm.engine.run(runInput, ctx));
|
|
3075
|
+
}
|
|
3076
|
+
|
|
1500
3077
|
// src/auth.ts
|
|
1501
3078
|
var customAuth = (fn) => ({ authenticate: fn });
|
|
1502
3079
|
|
|
3080
|
+
// src/rate-limit.ts
|
|
3081
|
+
function decideFixedWindow(prev, cfg, nowSec) {
|
|
3082
|
+
const windowValid = prev && nowSec - prev.windowStartSec < cfg.windowSec;
|
|
3083
|
+
const state = windowValid ? { count: prev.count + 1, windowStartSec: prev.windowStartSec } : { count: 1, windowStartSec: nowSec };
|
|
3084
|
+
const resetSec = Math.max(0, state.windowStartSec + cfg.windowSec - nowSec);
|
|
3085
|
+
const allow = state.count <= cfg.max;
|
|
3086
|
+
return { decision: { allow, remaining: Math.max(0, cfg.max - state.count), resetSec }, state };
|
|
3087
|
+
}
|
|
3088
|
+
function createInMemoryRateLimitStore() {
|
|
3089
|
+
const states = /* @__PURE__ */ new Map();
|
|
3090
|
+
let lastPruneSec = 0;
|
|
3091
|
+
return {
|
|
3092
|
+
async hit(key, cfg, nowSec) {
|
|
3093
|
+
if (nowSec > lastPruneSec) {
|
|
3094
|
+
for (const [k, s] of states) if (nowSec - s.windowStartSec >= cfg.windowSec) states.delete(k);
|
|
3095
|
+
lastPruneSec = nowSec;
|
|
3096
|
+
}
|
|
3097
|
+
const { decision, state } = decideFixedWindow(states.get(key) ?? null, cfg, nowSec);
|
|
3098
|
+
states.set(key, state);
|
|
3099
|
+
return decision;
|
|
3100
|
+
}
|
|
3101
|
+
};
|
|
3102
|
+
}
|
|
3103
|
+
function rateConfig(max, windowSec, fallbackMax) {
|
|
3104
|
+
const m = Number.isFinite(max) && max > 0 ? Math.floor(max) : fallbackMax;
|
|
3105
|
+
return { windowSec, max: m };
|
|
3106
|
+
}
|
|
3107
|
+
|
|
1503
3108
|
// src/index.ts
|
|
1504
3109
|
var VERSION = "0.0.0";
|
|
1505
3110
|
export {
|
|
3111
|
+
ALLOW,
|
|
3112
|
+
ALLOW_TOOL,
|
|
1506
3113
|
ASK_TOOL_NAME,
|
|
3114
|
+
AgentMutationForbidden,
|
|
1507
3115
|
CapturingExporter,
|
|
3116
|
+
ClientToolError,
|
|
1508
3117
|
CostGovernor,
|
|
3118
|
+
DEFAULT_READ_ONLY_TOOLS,
|
|
1509
3119
|
DelegateBudgets,
|
|
1510
3120
|
GUARDRAILS,
|
|
3121
|
+
HookDispatcher2 as HookDispatcher,
|
|
1511
3122
|
InMemoryContainerFloor,
|
|
1512
3123
|
InMemoryStorage,
|
|
1513
3124
|
PRICE_TABLE,
|
|
3125
|
+
ReserveDenied,
|
|
1514
3126
|
RowCache,
|
|
1515
3127
|
SCRATCHPAD_MAX_ENTRY_CHARS,
|
|
1516
3128
|
SCRATCHPAD_MAX_KEYS,
|
|
@@ -1518,17 +3130,43 @@ export {
|
|
|
1518
3130
|
SwarmEngine,
|
|
1519
3131
|
VERSION,
|
|
1520
3132
|
allowListModelProvider,
|
|
3133
|
+
ask2 as ask,
|
|
3134
|
+
assertActorMayMutateDefinition,
|
|
3135
|
+
buildSingleAgentSwarm,
|
|
1521
3136
|
buildSkillResolver,
|
|
3137
|
+
composePolicyPrompt,
|
|
1522
3138
|
composeSystemPrompt,
|
|
1523
3139
|
compositeTelemetry,
|
|
1524
3140
|
containerFloor,
|
|
3141
|
+
createHookDispatcher2 as createHookDispatcher,
|
|
3142
|
+
createInMemoryRateLimitStore,
|
|
3143
|
+
createRunState,
|
|
1525
3144
|
customAuth,
|
|
1526
3145
|
customTelemetry,
|
|
3146
|
+
decideFixedWindow,
|
|
1527
3147
|
defineAgent,
|
|
3148
|
+
defineBundle,
|
|
3149
|
+
defineClientTool,
|
|
3150
|
+
defineHook,
|
|
3151
|
+
defineRule,
|
|
1528
3152
|
defineSkill,
|
|
1529
3153
|
defineSwarm,
|
|
1530
3154
|
defineTool,
|
|
3155
|
+
defineWorkflow,
|
|
3156
|
+
deny2 as deny,
|
|
3157
|
+
drainTrajectory,
|
|
1531
3158
|
ev,
|
|
1532
3159
|
isEvent,
|
|
1533
|
-
|
|
3160
|
+
isTierSentinel,
|
|
3161
|
+
mergeBundle,
|
|
3162
|
+
priceUsage,
|
|
3163
|
+
rateConfig,
|
|
3164
|
+
resolveTelemetry,
|
|
3165
|
+
resolveTier,
|
|
3166
|
+
runAgent,
|
|
3167
|
+
runToTrajectory,
|
|
3168
|
+
sumBreakdowns,
|
|
3169
|
+
sumTurnUsage,
|
|
3170
|
+
tierModelId,
|
|
3171
|
+
toBundleContent
|
|
1534
3172
|
};
|