@nightowlsdev/core 0.3.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/LICENSE +21 -0
- package/dist/index.cjs +1588 -0
- package/dist/index.d.cts +814 -0
- package/dist/index.d.ts +814 -0
- package/dist/index.js +1534 -0
- package/dist/test-utils.cjs +75 -0
- package/dist/test-utils.d.cts +87 -0
- package/dist/test-utils.d.ts +87 -0
- package/dist/test-utils.js +44 -0
- package/package.json +58 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1534 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
var SCRATCHPAD_MAX_ENTRY_CHARS = 4e3;
|
|
3
|
+
var SCRATCHPAD_MAX_KEYS = 64;
|
|
4
|
+
|
|
5
|
+
// src/events.ts
|
|
6
|
+
function ev(type, base2, data) {
|
|
7
|
+
return { type, schemaVersion: 1, ...base2, data };
|
|
8
|
+
}
|
|
9
|
+
function isEvent(e, type) {
|
|
10
|
+
return e.type === type;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// src/define.ts
|
|
14
|
+
import { z as z4 } from "zod";
|
|
15
|
+
import { createTool as createTool4 } from "@mastra/core/tools";
|
|
16
|
+
|
|
17
|
+
// src/engine.ts
|
|
18
|
+
import { Mastra } from "@mastra/core/mastra";
|
|
19
|
+
import { InMemoryStore } from "@mastra/core/storage";
|
|
20
|
+
import { RequestContext } from "@mastra/core/request-context";
|
|
21
|
+
|
|
22
|
+
// src/mastra-map.ts
|
|
23
|
+
import { Agent } from "@mastra/core/agent";
|
|
24
|
+
|
|
25
|
+
// src/prompt.ts
|
|
26
|
+
var GUARDRAILS = [
|
|
27
|
+
"You are honest and proactive. Never invent facts or tool results.",
|
|
28
|
+
"Stay strictly focused on the task you were given; do not expand scope.",
|
|
29
|
+
"If you are blocked or missing information, ASK a follow-up question (to the user or another agent) using the `ask` tool rather than guessing."
|
|
30
|
+
].join(" ");
|
|
31
|
+
function composeSystemPrompt(row) {
|
|
32
|
+
const persona = [
|
|
33
|
+
`You are the "${row.slug}" agent.`,
|
|
34
|
+
`Personality: ${row.personality}.`,
|
|
35
|
+
row.capabilities.length ? `You are allowed to: ${row.capabilities.join("; ")}.` : "",
|
|
36
|
+
row.skillNames.length ? `Tools you may invoke: ${row.skillNames.join(", ")}.` : "",
|
|
37
|
+
row.delegateSlugs.length ? `You may delegate to: ${row.delegateSlugs.join(", ")}.` : ""
|
|
38
|
+
].filter(Boolean).join("\n");
|
|
39
|
+
return [
|
|
40
|
+
{ role: "system", content: GUARDRAILS },
|
|
41
|
+
{ role: "system", content: persona }
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
function composeScratchpadPrompt(entries) {
|
|
45
|
+
const render = (section) => {
|
|
46
|
+
const rows = entries.filter((e) => e.section === section).map((e) => `- [${e.key}] (${e.author} \u2190 ${e.requestedBy}) ${e.content}`);
|
|
47
|
+
return rows.length ? rows.join("\n") : "(none)";
|
|
48
|
+
};
|
|
49
|
+
const content = [
|
|
50
|
+
"Shared scratchpad for THIS conversation (visible to every agent here). Record important facts PROACTIVELY via `scratchpad_write` under a short KEY; reuse a key to revise that fact. public = shown to the user, meta = internal to agents.",
|
|
51
|
+
`## Public (also shown to the user)
|
|
52
|
+
${render("public")}`,
|
|
53
|
+
`## Meta (internal to agents)
|
|
54
|
+
${render("meta")}`
|
|
55
|
+
].join("\n\n");
|
|
56
|
+
return { role: "system", content };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/mastra-map.ts
|
|
60
|
+
var MAX_DELEGATION_DEPTH = 4;
|
|
61
|
+
function memoryFor(args, row) {
|
|
62
|
+
return args.resolveMemory ? args.resolveMemory(row) : args.memory;
|
|
63
|
+
}
|
|
64
|
+
function toolsFor(args, row) {
|
|
65
|
+
const out = { ...args.builtinTools ?? {} };
|
|
66
|
+
for (const name of row.skillNames) {
|
|
67
|
+
const skill = args.resolveSkill(name);
|
|
68
|
+
const mt = skill && __getMastraTool(skill);
|
|
69
|
+
if (mt) out[name] = mt;
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
async function withScratchpad(args, base2, rc) {
|
|
74
|
+
if (!args.loadScratchpad) return base2;
|
|
75
|
+
const tenantId = rc.get("tenantId") ?? "default";
|
|
76
|
+
const container = (rc.get("threadId") ?? "").split(":")[0] ?? "";
|
|
77
|
+
const entries = await args.loadScratchpad(container, tenantId);
|
|
78
|
+
return [...base2, composeScratchpadPrompt(entries)];
|
|
79
|
+
}
|
|
80
|
+
async function modelFor(args, row, tenantId) {
|
|
81
|
+
const id = await args.model.resolve(row.modelId, { tenantId });
|
|
82
|
+
return args.modelFactory(id, row.slug);
|
|
83
|
+
}
|
|
84
|
+
function buildSubAgent(args, row, depth, path) {
|
|
85
|
+
const nextPath = [...path, row.slug];
|
|
86
|
+
return new Agent({
|
|
87
|
+
id: `swarm-sub-${row.slug}`,
|
|
88
|
+
name: row.slug,
|
|
89
|
+
// Model-facing tool description for the `agent-<slug>` delegation tool — use the agent's own
|
|
90
|
+
// personality so the orchestrator's LLM knows WHAT this delegate is for (role is a coarse enum).
|
|
91
|
+
description: row.personality || `Agent ${row.slug} (${row.role})`,
|
|
92
|
+
...memoryFor(args, row) ? { memory: memoryFor(args, row) } : {},
|
|
93
|
+
instructions: async ({ requestContext }) => withScratchpad(args, composeSystemPrompt(row), requestContext),
|
|
94
|
+
model: async ({ requestContext }) => await modelFor(args, row, requestContext.get("tenantId") ?? "default"),
|
|
95
|
+
tools: toolsFor(args, row),
|
|
96
|
+
agents: async ({ requestContext }) => await buildSubAgentMap(
|
|
97
|
+
args,
|
|
98
|
+
row.delegateSlugs ?? [],
|
|
99
|
+
depth + 1,
|
|
100
|
+
nextPath,
|
|
101
|
+
requestContext.get("tenantId") ?? "default"
|
|
102
|
+
)
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
async function buildSubAgentMap(args, slugs, depth, path, tenantId) {
|
|
106
|
+
if (depth > MAX_DELEGATION_DEPTH || slugs.length === 0) return {};
|
|
107
|
+
const out = {};
|
|
108
|
+
for (const slug of slugs) {
|
|
109
|
+
if (path.includes(slug)) continue;
|
|
110
|
+
const row = await args.loadRow(slug, tenantId);
|
|
111
|
+
out[slug] = buildSubAgent(args, row, depth, path);
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
function buildMastraAgent(args) {
|
|
116
|
+
const load = (rc) => args.loadRow(rc.get("agentSlug") ?? "", rc.get("tenantId") ?? "default");
|
|
117
|
+
return new Agent({
|
|
118
|
+
id: "swarm-agent",
|
|
119
|
+
name: "Swarm Agent",
|
|
120
|
+
// R9: try a DYNAMIC memory fn so the root agent honors a per-agent override too (its row is loaded per
|
|
121
|
+
// request). If Mastra rejects a dynamic `memory`, fall back to the static swarm Memory (root override is
|
|
122
|
+
// then sub-agents-only — see the spec's accepted limitation).
|
|
123
|
+
...args.resolveMemory || args.memory ? { memory: (async ({ requestContext }) => memoryFor(args, await load(requestContext))) } : {},
|
|
124
|
+
instructions: async ({ requestContext }) => withScratchpad(args, composeSystemPrompt(await load(requestContext)), requestContext),
|
|
125
|
+
model: async ({ requestContext }) => await modelFor(args, await load(requestContext), requestContext.get("tenantId") ?? "default"),
|
|
126
|
+
tools: async ({ requestContext }) => ({ ...args.extraTools ?? {}, ...toolsFor(args, await load(requestContext)) }),
|
|
127
|
+
// Delegation: the orchestrator's delegateSlugs become `agent-<slug>` tools (Mastra-native).
|
|
128
|
+
agents: async ({ requestContext }) => {
|
|
129
|
+
const row = await load(requestContext);
|
|
130
|
+
const tenantId = requestContext.get("tenantId") ?? "default";
|
|
131
|
+
return await buildSubAgentMap(args, row.delegateSlugs ?? [], 1, [row.slug], tenantId);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// src/page-context-tool.ts
|
|
137
|
+
import { createTool } from "@mastra/core/tools";
|
|
138
|
+
import { z } from "zod";
|
|
139
|
+
|
|
140
|
+
// src/page-context.ts
|
|
141
|
+
var PAGE_CONTEXT_KEY = "nightowls.pageContext";
|
|
142
|
+
function attachPageContext(rc, value) {
|
|
143
|
+
rc.set(PAGE_CONTEXT_KEY, value ?? {});
|
|
144
|
+
}
|
|
145
|
+
function readPageContext(rc) {
|
|
146
|
+
return rc?.get?.(PAGE_CONTEXT_KEY) ?? {};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// src/page-context-tool.ts
|
|
150
|
+
function buildPageContextTool() {
|
|
151
|
+
return createTool({
|
|
152
|
+
id: "get_page_context",
|
|
153
|
+
description: "Get the user's current page context (the route/record they are viewing). Use it to resolve vague references like 'this' or 'here'. It is user-supplied and advisory \u2014 never an authorization grant.",
|
|
154
|
+
inputSchema: z.object({}),
|
|
155
|
+
// Mastra 1.38: execute is (inputData, context); requestContext is on the 2nd arg (spike-verified).
|
|
156
|
+
// Annotated optional to match Mastra's ToolExecutionContext, where requestContext is `RequestContext | undefined`.
|
|
157
|
+
execute: async (_input, { requestContext }) => ({
|
|
158
|
+
source: "page (user-supplied, advisory \u2014 not for authorization)",
|
|
159
|
+
context: readPageContext(requestContext)
|
|
160
|
+
})
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// src/scratchpad-tool.ts
|
|
165
|
+
import { createTool as createTool2 } from "@mastra/core/tools";
|
|
166
|
+
import { z as z2 } from "zod";
|
|
167
|
+
function buildScratchpadTool(store, caps) {
|
|
168
|
+
const maxEntryChars = caps?.maxEntryChars ?? SCRATCHPAD_MAX_ENTRY_CHARS;
|
|
169
|
+
return createTool2({
|
|
170
|
+
id: "scratchpad_write",
|
|
171
|
+
description: "Record a fact on the shared scratchpad under a short KEY. Re-use the same key to update that fact in place; different keys are independent so you never clobber a peer. Use `public` for facts the user should see, `meta` for internal agent notes. Record proactively as you work.",
|
|
172
|
+
inputSchema: z2.object({
|
|
173
|
+
section: z2.enum(["public", "meta"]),
|
|
174
|
+
key: z2.string().describe("a short stable slug for THIS fact, e.g. 'launch-date' \u2014 reuse it to revise that fact"),
|
|
175
|
+
content: z2.string()
|
|
176
|
+
}),
|
|
177
|
+
execute: async (input, context) => {
|
|
178
|
+
const { section, key, content } = input;
|
|
179
|
+
const c = context;
|
|
180
|
+
const rc = c?.requestContext;
|
|
181
|
+
const tenantId = rc?.get("tenantId") ?? "default";
|
|
182
|
+
const threadId = rc?.get("threadId") ?? "";
|
|
183
|
+
const userId = rc?.get("userId") ?? "unknown";
|
|
184
|
+
const runSlug = rc?.get("agentSlug") ?? "unknown";
|
|
185
|
+
const agentId = c?.agent?.agentId ?? "";
|
|
186
|
+
const isSub = agentId.startsWith("swarm-sub-");
|
|
187
|
+
const author = isSub ? agentId.slice("swarm-sub-".length) : runSlug;
|
|
188
|
+
const requestedBy = isSub ? runSlug : "user";
|
|
189
|
+
const container = threadId.split(":")[0] || threadId;
|
|
190
|
+
const capped = content.length > maxEntryChars ? content.slice(0, maxEntryChars) + "\u2026[truncated]" : content;
|
|
191
|
+
await store.write(tenantId, container, section, key, capped, { agentSlug: author, userId, requestedBy, maxKeys: caps?.maxKeys });
|
|
192
|
+
return { ok: true, section, key };
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// src/recall-lane-tool.ts
|
|
198
|
+
import { createTool as createTool3 } from "@mastra/core/tools";
|
|
199
|
+
import { z as z3 } from "zod";
|
|
200
|
+
var RECALL_LANE_LIMIT = 20;
|
|
201
|
+
function buildRecallLaneTool(read) {
|
|
202
|
+
return createTool3({
|
|
203
|
+
id: "recall_lane",
|
|
204
|
+
description: "Read the recent transcript of another agent's lane in THIS conversation. Use it when you suspect a peer agent discussed something relevant that isn't on the shared scratchpad. Pass the peer's agent slug; you get back their last messages. Pass the coordinator's (main agent's) slug to read the main conversation thread. Read-only.",
|
|
205
|
+
inputSchema: z3.object({ agentSlug: z3.string() }),
|
|
206
|
+
execute: async (input, { requestContext }) => {
|
|
207
|
+
const { agentSlug } = input;
|
|
208
|
+
const get = (k) => requestContext?.get(k) ?? "";
|
|
209
|
+
const threadId = get("threadId");
|
|
210
|
+
const container = threadId.split(":")[0] || threadId;
|
|
211
|
+
const runSlug = get("agentSlug");
|
|
212
|
+
const onBareContainer = !threadId.includes(":");
|
|
213
|
+
const isRoot = onBareContainer && agentSlug === runSlug;
|
|
214
|
+
const peerLane = isRoot ? container : `${container}:${agentSlug}`;
|
|
215
|
+
const ctx = {
|
|
216
|
+
tenantId: get("tenantId") || "default",
|
|
217
|
+
userId: get("userId") || "unknown",
|
|
218
|
+
agentSlug: get("agentSlug") || "unknown",
|
|
219
|
+
threadId: peerLane,
|
|
220
|
+
runId: get("runId") || "recall"
|
|
221
|
+
};
|
|
222
|
+
const messages = await read(peerLane, ctx);
|
|
223
|
+
return { agentSlug, messages: messages.map((m) => ({ role: m.role, text: m.text, ts: m.ts })) };
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// src/memory.ts
|
|
229
|
+
import { Memory } from "@mastra/memory";
|
|
230
|
+
function mergeMemoryOptions(base2, override) {
|
|
231
|
+
return {
|
|
232
|
+
...base2,
|
|
233
|
+
...override.lastMessages !== void 0 ? { lastMessages: override.lastMessages } : {},
|
|
234
|
+
...override.workingMemory !== void 0 ? { workingMemory: override.workingMemory } : {},
|
|
235
|
+
...override.semanticRecall !== void 0 ? { semanticRecall: override.semanticRecall } : {},
|
|
236
|
+
...override.observationalMemory !== void 0 ? { observationalMemory: override.observationalMemory } : {}
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function buildMemoryResolver(memCfg) {
|
|
240
|
+
const memory = buildMemory(memCfg);
|
|
241
|
+
const cache = /* @__PURE__ */ new Map();
|
|
242
|
+
const resolveMemory = (row) => {
|
|
243
|
+
if (!row.memory) return memory;
|
|
244
|
+
const key = JSON.stringify(row.memory);
|
|
245
|
+
let m = cache.get(key);
|
|
246
|
+
if (!m) {
|
|
247
|
+
m = buildMemory(mergeMemoryOptions(memCfg, row.memory));
|
|
248
|
+
cache.set(key, m);
|
|
249
|
+
}
|
|
250
|
+
return m;
|
|
251
|
+
};
|
|
252
|
+
return { memory, resolveMemory };
|
|
253
|
+
}
|
|
254
|
+
function resourceId(ctx) {
|
|
255
|
+
const container = ctx.threadId.split(":")[0] || ctx.threadId;
|
|
256
|
+
return `${ctx.tenantId}:${container}`;
|
|
257
|
+
}
|
|
258
|
+
function buildMemory(cfg) {
|
|
259
|
+
const wantRecall = cfg.semanticRecall !== void 0 && cfg.semanticRecall !== false;
|
|
260
|
+
if (wantRecall && (!cfg.vector || !cfg.embedder)) {
|
|
261
|
+
throw new Error("memory.semanticRecall requires both `vector` and `embedder`");
|
|
262
|
+
}
|
|
263
|
+
const wm = cfg.workingMemory;
|
|
264
|
+
const sr = cfg.semanticRecall;
|
|
265
|
+
const wmOption = wm ? {
|
|
266
|
+
enabled: true,
|
|
267
|
+
...typeof wm === "object" ? wm : {},
|
|
268
|
+
scope: "thread"
|
|
269
|
+
// thread-only: working memory never spans a user's other threads
|
|
270
|
+
} : void 0;
|
|
271
|
+
const srOption = wantRecall ? {
|
|
272
|
+
topK: 5,
|
|
273
|
+
messageRange: 2,
|
|
274
|
+
...typeof sr === "object" ? sr : {},
|
|
275
|
+
scope: "thread"
|
|
276
|
+
// thread-only: recall never surfaces messages from other threads
|
|
277
|
+
} : false;
|
|
278
|
+
return new Memory({
|
|
279
|
+
storage: cfg.store,
|
|
280
|
+
vector: cfg.vector ?? void 0,
|
|
281
|
+
embedder: cfg.embedder ?? void 0,
|
|
282
|
+
options: {
|
|
283
|
+
lastMessages: cfg.lastMessages ?? 20,
|
|
284
|
+
workingMemory: wmOption,
|
|
285
|
+
semanticRecall: srOption,
|
|
286
|
+
// R10: forward Mastra's built-in observational memory (LLM observation/reflection compaction) when opted in.
|
|
287
|
+
...cfg.observationalMemory !== void 0 ? { observationalMemory: cfg.observationalMemory } : {}
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// src/floor.ts
|
|
293
|
+
var InMemoryContainerFloor = class {
|
|
294
|
+
/** @param maxHoldMs force-release backstop for a hung grant (a run that never releases). */
|
|
295
|
+
constructor(maxHoldMs = 18e4) {
|
|
296
|
+
this.maxHoldMs = maxHoldMs;
|
|
297
|
+
}
|
|
298
|
+
maxHoldMs;
|
|
299
|
+
// Slots are intentionally never evicted: a freed slot is a tiny `{held:null, timer:null, waiters:[]}`,
|
|
300
|
+
// and containers are conversation-keyed (bounded by real usage), so retention is negligible.
|
|
301
|
+
slots = /* @__PURE__ */ new Map();
|
|
302
|
+
slot(container) {
|
|
303
|
+
let s = this.slots.get(container);
|
|
304
|
+
if (!s) {
|
|
305
|
+
s = { held: null, timer: null, waiters: [] };
|
|
306
|
+
this.slots.set(container, s);
|
|
307
|
+
}
|
|
308
|
+
return s;
|
|
309
|
+
}
|
|
310
|
+
async holder(container) {
|
|
311
|
+
return this.slots.get(container)?.held ?? null;
|
|
312
|
+
}
|
|
313
|
+
async queueDepth(container) {
|
|
314
|
+
return this.slots.get(container)?.waiters.filter((w) => !w.cancelled).length ?? 0;
|
|
315
|
+
}
|
|
316
|
+
async tryAcquire(container, who) {
|
|
317
|
+
const s = this.slot(container);
|
|
318
|
+
return s.held ? null : this.grant(container, s, who);
|
|
319
|
+
}
|
|
320
|
+
acquire(container, who, signal) {
|
|
321
|
+
const s = this.slot(container);
|
|
322
|
+
if (!s.held) return Promise.resolve(this.grant(container, s, who));
|
|
323
|
+
return new Promise((resolve) => {
|
|
324
|
+
const waiter = { who, resolve, cancelled: false };
|
|
325
|
+
s.waiters.push(waiter);
|
|
326
|
+
if (!signal) return;
|
|
327
|
+
const onAbort = () => {
|
|
328
|
+
if (waiter.cancelled) return;
|
|
329
|
+
waiter.cancelled = true;
|
|
330
|
+
const i = s.waiters.indexOf(waiter);
|
|
331
|
+
if (i >= 0) {
|
|
332
|
+
s.waiters.splice(i, 1);
|
|
333
|
+
resolve(() => {
|
|
334
|
+
});
|
|
335
|
+
} else if (s.held === who) {
|
|
336
|
+
this.release(container, s, who);
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
if (signal.aborted) onAbort();
|
|
340
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
/** Take the floor for `who`, arm the backstop, and return an idempotent release that hands off FIFO. */
|
|
344
|
+
grant(container, s, who) {
|
|
345
|
+
s.held = who;
|
|
346
|
+
if (s.timer) clearTimeout(s.timer);
|
|
347
|
+
s.timer = setTimeout(() => {
|
|
348
|
+
console.warn(`[nightowls] container floor force-released after ${this.maxHoldMs}ms: ${container} (held by ${who.label})`);
|
|
349
|
+
this.release(container, s, who);
|
|
350
|
+
}, this.maxHoldMs);
|
|
351
|
+
if (typeof s.timer.unref === "function") s.timer.unref();
|
|
352
|
+
let released = false;
|
|
353
|
+
return () => {
|
|
354
|
+
if (released) return;
|
|
355
|
+
released = true;
|
|
356
|
+
this.release(container, s, who);
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
/** Release `who`'s hold (if still current) and grant the next live FIFO waiter, or free the floor. */
|
|
360
|
+
release(container, s, who) {
|
|
361
|
+
if (s.held !== who) return;
|
|
362
|
+
if (s.timer) {
|
|
363
|
+
clearTimeout(s.timer);
|
|
364
|
+
s.timer = null;
|
|
365
|
+
}
|
|
366
|
+
let next = s.waiters.shift();
|
|
367
|
+
while (next && next.cancelled) next = s.waiters.shift();
|
|
368
|
+
if (next) {
|
|
369
|
+
next.resolve(this.grant(container, s, next.who));
|
|
370
|
+
} else {
|
|
371
|
+
s.held = null;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
var containerFloor = new InMemoryContainerFloor();
|
|
376
|
+
|
|
377
|
+
// src/cost.ts
|
|
378
|
+
var PRICE_TABLE = {
|
|
379
|
+
// built-in, host-overridable (CONTRACTS §10). Values are placeholders pending a real price feed.
|
|
380
|
+
"openai/gpt-5.5": { inUsdPerMtok: 2.5, outUsdPerMtok: 10 },
|
|
381
|
+
"openai/gpt-5.5-mini": { inUsdPerMtok: 0.3, outUsdPerMtok: 1.2 }
|
|
382
|
+
};
|
|
383
|
+
function priceUsage(prices, modelId, u) {
|
|
384
|
+
const p = prices[modelId] ?? { inUsdPerMtok: 0, outUsdPerMtok: 0 };
|
|
385
|
+
return u.inputTokens / 1e6 * p.inUsdPerMtok + u.outputTokens / 1e6 * p.outUsdPerMtok;
|
|
386
|
+
}
|
|
387
|
+
var CostGovernor = class {
|
|
388
|
+
constructor(opts) {
|
|
389
|
+
this.opts = opts;
|
|
390
|
+
this.prices = { ...PRICE_TABLE, ...opts.prices ?? {} };
|
|
391
|
+
}
|
|
392
|
+
opts;
|
|
393
|
+
steps = 0;
|
|
394
|
+
usd = 0;
|
|
395
|
+
prices;
|
|
396
|
+
step() {
|
|
397
|
+
this.steps++;
|
|
398
|
+
}
|
|
399
|
+
/** Price a single usage WITHOUT accumulating it (for per-generation telemetry cost). */
|
|
400
|
+
priceOf(modelId, u) {
|
|
401
|
+
return priceUsage(this.prices, modelId, u);
|
|
402
|
+
}
|
|
403
|
+
addUsage(modelId, u) {
|
|
404
|
+
this.usd += this.priceOf(modelId, u);
|
|
405
|
+
}
|
|
406
|
+
costUsd() {
|
|
407
|
+
return this.usd;
|
|
408
|
+
}
|
|
409
|
+
shouldStop() {
|
|
410
|
+
if (this.steps >= this.opts.maxSteps) return { stop: true, reason: "step cap reached" };
|
|
411
|
+
if (this.usd >= this.opts.maxCostUsd) return { stop: true, reason: "USD cap reached" };
|
|
412
|
+
return { stop: false };
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
var DelegateBudgets = class {
|
|
416
|
+
constructor(cfg, rootSlug, prices) {
|
|
417
|
+
this.cfg = cfg;
|
|
418
|
+
this.rootSlug = rootSlug;
|
|
419
|
+
this.prices = { ...PRICE_TABLE, ...prices ?? {} };
|
|
420
|
+
}
|
|
421
|
+
cfg;
|
|
422
|
+
rootSlug;
|
|
423
|
+
usd = /* @__PURE__ */ new Map();
|
|
424
|
+
prices;
|
|
425
|
+
/** The USD cap for a delegate: its `bySlug` override if present, else the default. `undefined` → uncapped. */
|
|
426
|
+
capFor(slug) {
|
|
427
|
+
return this.cfg.bySlug?.[slug]?.maxCostUsd ?? this.cfg.maxCostUsd;
|
|
428
|
+
}
|
|
429
|
+
/** Accumulate one generation's usage against a delegate. No-op for the root orchestrator (not a delegate). */
|
|
430
|
+
addUsage(slug, modelId, u) {
|
|
431
|
+
if (slug === this.rootSlug) return;
|
|
432
|
+
this.usd.set(slug, (this.usd.get(slug) ?? 0) + priceUsage(this.prices, modelId, u));
|
|
433
|
+
}
|
|
434
|
+
/** The first delegate that has met or exceeded its USD cap, or null. */
|
|
435
|
+
exceeded() {
|
|
436
|
+
for (const [slug, spent] of this.usd) {
|
|
437
|
+
const cap = this.capFor(slug);
|
|
438
|
+
if (cap != null && spent >= cap) {
|
|
439
|
+
return { slug, reason: `delegate '${slug}' exceeded its per-delegate USD budget ($${cap})` };
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
// src/telemetry.ts
|
|
447
|
+
function customTelemetry(fn) {
|
|
448
|
+
return {
|
|
449
|
+
export: async (spans) => {
|
|
450
|
+
await fn(spans);
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
function compositeTelemetry(exporters) {
|
|
455
|
+
return {
|
|
456
|
+
export: async (spans) => {
|
|
457
|
+
const results = await Promise.allSettled(exporters.map((e) => e.export(spans)));
|
|
458
|
+
for (const r of results) {
|
|
459
|
+
if (r.status === "rejected") {
|
|
460
|
+
console.warn("[nightowls] telemetry exporter failed:", r.reason);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
function resolveTelemetry(t) {
|
|
467
|
+
if (!t) return null;
|
|
468
|
+
return Array.isArray(t) ? compositeTelemetry(t) : t;
|
|
469
|
+
}
|
|
470
|
+
var CapturingExporter = class {
|
|
471
|
+
spans = [];
|
|
472
|
+
async export(spans) {
|
|
473
|
+
this.spans.push(...spans);
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
var SpanCollector = class {
|
|
477
|
+
constructor(traceId, now, runName, runAttrs = {}) {
|
|
478
|
+
this.traceId = traceId;
|
|
479
|
+
this.now = now;
|
|
480
|
+
this.runSpan = { name: runName, kind: "run", traceId, attributes: runAttrs, startedAt: now() };
|
|
481
|
+
this.spans.push(this.runSpan);
|
|
482
|
+
}
|
|
483
|
+
traceId;
|
|
484
|
+
now;
|
|
485
|
+
spans = [];
|
|
486
|
+
runSpan;
|
|
487
|
+
gen = null;
|
|
488
|
+
tools = /* @__PURE__ */ new Map();
|
|
489
|
+
/** Ensure a generation span is open (lazy — first model activity in this step). */
|
|
490
|
+
openGeneration(modelId) {
|
|
491
|
+
if (this.gen) return;
|
|
492
|
+
this.gen = {
|
|
493
|
+
name: "generation",
|
|
494
|
+
kind: "generation",
|
|
495
|
+
traceId: this.traceId,
|
|
496
|
+
attributes: { modelId },
|
|
497
|
+
startedAt: this.now()
|
|
498
|
+
};
|
|
499
|
+
this.spans.push(this.gen);
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Close the open generation with this step's usage + its own per-call cost (already priced from
|
|
503
|
+
* the step usage by the engine). `costUsd` is per-generation — never a cumulative running total.
|
|
504
|
+
*/
|
|
505
|
+
closeGeneration(usage, costUsd) {
|
|
506
|
+
if (!this.gen) return;
|
|
507
|
+
this.gen.attributes = {
|
|
508
|
+
...this.gen.attributes,
|
|
509
|
+
inputTokens: usage.inputTokens,
|
|
510
|
+
outputTokens: usage.outputTokens,
|
|
511
|
+
costUsd: Math.max(0, costUsd)
|
|
512
|
+
};
|
|
513
|
+
this.gen.endedAt = this.now();
|
|
514
|
+
this.gen = null;
|
|
515
|
+
}
|
|
516
|
+
openTool(toolCallId, name) {
|
|
517
|
+
const s = {
|
|
518
|
+
name: `tool:${name}`,
|
|
519
|
+
kind: "tool",
|
|
520
|
+
traceId: this.traceId,
|
|
521
|
+
attributes: { toolCallId, name },
|
|
522
|
+
startedAt: this.now()
|
|
523
|
+
};
|
|
524
|
+
this.tools.set(toolCallId, s);
|
|
525
|
+
this.spans.push(s);
|
|
526
|
+
}
|
|
527
|
+
closeTool(toolCallId, ok) {
|
|
528
|
+
const s = this.tools.get(toolCallId);
|
|
529
|
+
if (!s) return;
|
|
530
|
+
s.attributes = { ...s.attributes, ok };
|
|
531
|
+
s.endedAt = this.now();
|
|
532
|
+
this.tools.delete(toolCallId);
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Seal: close the run span and any dangling open spans, return the batch. A run that suspends
|
|
536
|
+
* mid-step (e.g. an `ask` tool-call → `tool-call-suspended` with no `step-finish`) leaves the
|
|
537
|
+
* generation/tool spans open with no usage/result — those are tagged `incomplete:true` so
|
|
538
|
+
* downstream consumers don't mistake a sealed-but-unfinished span for a completed one.
|
|
539
|
+
*/
|
|
540
|
+
finish() {
|
|
541
|
+
const t = this.now();
|
|
542
|
+
if (this.gen && this.gen.endedAt == null) {
|
|
543
|
+
this.gen.attributes = { ...this.gen.attributes, incomplete: true };
|
|
544
|
+
this.gen.endedAt = t;
|
|
545
|
+
}
|
|
546
|
+
for (const s of this.tools.values()) {
|
|
547
|
+
if (s.endedAt == null) {
|
|
548
|
+
s.attributes = { ...s.attributes, incomplete: true };
|
|
549
|
+
s.endedAt = t;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
if (this.runSpan.endedAt == null) this.runSpan.endedAt = t;
|
|
553
|
+
return this.spans;
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
// src/cache.ts
|
|
558
|
+
var RowCache = class {
|
|
559
|
+
constructor(opts) {
|
|
560
|
+
this.opts = opts;
|
|
561
|
+
}
|
|
562
|
+
opts;
|
|
563
|
+
map = /* @__PURE__ */ new Map();
|
|
564
|
+
now() {
|
|
565
|
+
return this.opts.now ? this.opts.now() : Date.now();
|
|
566
|
+
}
|
|
567
|
+
async get(key, load) {
|
|
568
|
+
const hit = this.map.get(key);
|
|
569
|
+
if (hit && hit.exp > this.now()) {
|
|
570
|
+
this.map.delete(key);
|
|
571
|
+
this.map.set(key, hit);
|
|
572
|
+
return hit.v;
|
|
573
|
+
}
|
|
574
|
+
const v = await load();
|
|
575
|
+
this.map.set(key, { v, exp: this.now() + this.opts.ttlMs });
|
|
576
|
+
if (this.map.size > this.opts.max) {
|
|
577
|
+
const oldest = this.map.keys().next().value;
|
|
578
|
+
if (oldest !== void 0) this.map.delete(oldest);
|
|
579
|
+
}
|
|
580
|
+
return v;
|
|
581
|
+
}
|
|
582
|
+
/** Synchronous read of an already-cached, unexpired value (no load, no LRU touch). */
|
|
583
|
+
peek(key) {
|
|
584
|
+
const hit = this.map.get(key);
|
|
585
|
+
return hit && hit.exp > this.now() ? hit.v : void 0;
|
|
586
|
+
}
|
|
587
|
+
invalidate(key) {
|
|
588
|
+
this.map.delete(key);
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
// src/engine.ts
|
|
593
|
+
var AGENT_KEY = "swarm";
|
|
594
|
+
var SwarmEngine = class {
|
|
595
|
+
constructor(opts) {
|
|
596
|
+
this.opts = opts;
|
|
597
|
+
if (opts.scratchpad && !opts.storage.scratchpad) {
|
|
598
|
+
throw new Error("scratchpad: true requires a StorageAdapter with a scratchpad store");
|
|
599
|
+
}
|
|
600
|
+
const { memory, resolveMemory } = opts.memory ? buildMemoryResolver(opts.memory) : { memory: void 0, resolveMemory: void 0 };
|
|
601
|
+
this.memory = memory;
|
|
602
|
+
this.floor = opts.floor ?? containerFloor;
|
|
603
|
+
opts.storage.subscribeInvalidations?.((key) => this.rowCache.invalidate(key));
|
|
604
|
+
const agent = buildMastraAgent({
|
|
605
|
+
loadRow: (slug, tenantId) => this.loadRow(tenantId, slug),
|
|
606
|
+
resolveMemory,
|
|
607
|
+
resolveSkill: (n) => opts.resolveSkill?.(n),
|
|
608
|
+
model: opts.model,
|
|
609
|
+
modelFactory: opts.modelFactory,
|
|
610
|
+
builtinTools: {
|
|
611
|
+
[ASK_TOOL_NAME]: buildAskMastraTool(),
|
|
612
|
+
...opts.scratchpad ? { scratchpad_write: buildScratchpadTool(opts.storage.scratchpad, typeof opts.scratchpad === "object" ? opts.scratchpad : void 0) } : {},
|
|
613
|
+
...opts.recallLane ? { recall_lane: buildRecallLaneTool((tid, ctx) => this.history(tid, ctx, { limit: RECALL_LANE_LIMIT })) } : {}
|
|
614
|
+
},
|
|
615
|
+
// Opt-in page-context channel: expose `get_page_context` on the user-facing orchestrator
|
|
616
|
+
// ONLY (never sub-agents) so the model can pull the host page's advisory RunInput.context.
|
|
617
|
+
...opts.pageContext ? { extraTools: { get_page_context: buildPageContextTool() } } : {},
|
|
618
|
+
loadScratchpad: opts.scratchpad ? (c, t) => opts.storage.scratchpad.list(t, c) : void 0,
|
|
619
|
+
memory
|
|
620
|
+
});
|
|
621
|
+
this.mastra = new Mastra({
|
|
622
|
+
storage: opts.mastraStore ?? new InMemoryStore(),
|
|
623
|
+
agents: { [AGENT_KEY]: agent }
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
opts;
|
|
627
|
+
mastra;
|
|
628
|
+
// One cache per engine: the three dynamic agent fns (instructions/model/tools)
|
|
629
|
+
// each call loadRow within a single request, so without this the row is fetched
|
|
630
|
+
// 3× per turn. Short TTL keeps published-version changes fresh.
|
|
631
|
+
rowCache = new RowCache({ max: 256, ttlMs: 3e4 });
|
|
632
|
+
// The one Mastra Memory this engine streams against (undefined when stateless). Held so
|
|
633
|
+
// `history` can read a thread's persisted conversation from the SAME memory used for streaming.
|
|
634
|
+
// Typed `unknown` to keep the engine wall: no engine-vendor type escapes the public surface.
|
|
635
|
+
memory;
|
|
636
|
+
floor;
|
|
637
|
+
/** Cached agent-row load shared by the three dynamic agent fns AND run/resume. */
|
|
638
|
+
loadRow(tenantId, slug) {
|
|
639
|
+
return this.rowCache.get(`${tenantId}:${slug}`, async () => {
|
|
640
|
+
const row = await this.opts.storage.agents.head(tenantId, slug);
|
|
641
|
+
if (!row) throw new Error(`unknown agent: ${slug}`);
|
|
642
|
+
return row;
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
agent() {
|
|
646
|
+
return this.mastra.getAgent(AGENT_KEY);
|
|
647
|
+
}
|
|
648
|
+
requestContext(ctx) {
|
|
649
|
+
const rc = new RequestContext();
|
|
650
|
+
for (const [k, v] of Object.entries(ctx)) {
|
|
651
|
+
if (v !== void 0) rc.set(k, v);
|
|
652
|
+
}
|
|
653
|
+
return rc;
|
|
654
|
+
}
|
|
655
|
+
/** Per-call Mastra memory ids + delegation, only when memory is configured (else stream is unchanged). */
|
|
656
|
+
memoryOpts(ctx) {
|
|
657
|
+
if (!this.opts.memory) return {};
|
|
658
|
+
return {
|
|
659
|
+
memory: { thread: ctx.threadId, resource: resourceId(ctx) },
|
|
660
|
+
delegation: { includeSubAgentToolResultsInModelContext: true }
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Read a thread's persisted conversation from the engine's Memory and return it as wall-safe
|
|
665
|
+
* `SwarmMessage[]` (no engine-vendor type in the signature or the result — powers "reload keeps the chat").
|
|
666
|
+
*
|
|
667
|
+
* Shapes are spike-proven against the Mastra memory surface (`memory.recall`, NOT `memory.query`): each
|
|
668
|
+
* recalled row is a MastraDBMessage whose text lives in `content.parts[]` (NOT a flat `m.text`/string
|
|
669
|
+
* `m.content`). The vendor value is cast THROUGH a local structural type so no vendor type leaks to the
|
|
670
|
+
* public `.d.ts` (engine wall). Returns `[]` when the engine is stateless (no memory configured).
|
|
671
|
+
*/
|
|
672
|
+
async history(threadId, ctx, opts) {
|
|
673
|
+
if (!this.memory) return [];
|
|
674
|
+
const mem = this.memory;
|
|
675
|
+
const limit = opts?.limit ?? 200;
|
|
676
|
+
const recallStart = Date.now();
|
|
677
|
+
let recalled;
|
|
678
|
+
try {
|
|
679
|
+
recalled = await mem.recall({ threadId, resourceId: resourceId(ctx), threadConfig: { lastMessages: limit } });
|
|
680
|
+
} catch {
|
|
681
|
+
this.emitRecallSpan(ctx, threadId, recallStart, { limit, messageCount: 0, ok: false });
|
|
682
|
+
return [];
|
|
683
|
+
}
|
|
684
|
+
const mapped = recalled.messages.map((m) => {
|
|
685
|
+
const role = m.role === "user" ? "user" : "assistant";
|
|
686
|
+
const raw = (m.content?.parts ?? []).filter((p) => p.type === "text").map((p) => p.text ?? "").join("");
|
|
687
|
+
const speaker = role === "assistant" ? /^\[([a-z0-9][a-z0-9-]*)\]\s([\s\S]*)$/.exec(raw) : null;
|
|
688
|
+
const sender = role === "user" ? /^\[user:([^\]]*)\]\s([\s\S]*)$/.exec(raw) : null;
|
|
689
|
+
return {
|
|
690
|
+
threadId: m.threadId ?? threadId,
|
|
691
|
+
role,
|
|
692
|
+
text: speaker ? speaker[2] : sender ? sender[2] : raw,
|
|
693
|
+
ts: +new Date(m.createdAt),
|
|
694
|
+
...speaker ? { agentSlug: speaker[1] } : {},
|
|
695
|
+
...sender ? { userId: sender[1] } : {}
|
|
696
|
+
};
|
|
697
|
+
});
|
|
698
|
+
const result = mapped.length > limit ? mapped.slice(-limit) : mapped;
|
|
699
|
+
this.emitRecallSpan(ctx, threadId, recallStart, { limit, messageCount: result.length, ok: true });
|
|
700
|
+
return result;
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* R4: emit a single `recall` telemetry span for a `history` recall, best-effort. Fire-and-forget — a
|
|
704
|
+
* throwing/slow exporter must never delay or break a history read, so we don't await it and swallow errors.
|
|
705
|
+
* traceId is the invoking ctx's runId so a recall inside a run (via `recall_lane`) joins that run's trace.
|
|
706
|
+
*/
|
|
707
|
+
emitRecallSpan(ctx, threadId, startedAt, attrs) {
|
|
708
|
+
const telemetry = this.opts.telemetry;
|
|
709
|
+
if (!telemetry) return;
|
|
710
|
+
const span = {
|
|
711
|
+
name: "recall",
|
|
712
|
+
kind: "recall",
|
|
713
|
+
traceId: ctx.runId,
|
|
714
|
+
attributes: { threadId, resourceId: resourceId(ctx), ...attrs },
|
|
715
|
+
startedAt,
|
|
716
|
+
endedAt: Date.now()
|
|
717
|
+
};
|
|
718
|
+
void Promise.resolve().then(() => telemetry.export([span])).catch(() => {
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Multi-agent shared-thread attribution (the persist boundary). Mastra recall carries NO per-message
|
|
723
|
+
* author field (spike-proven), so on a thread several agents share, a later agent can't tell who said an
|
|
724
|
+
* earlier assistant turn. After a run drains, this PREFIXES that run's just-persisted assistant turn(s)
|
|
725
|
+
* with `"[<slug>] "` — the only place the speaker survives in Mastra memory — so a subsequent agent's
|
|
726
|
+
* recall sees it inline; `history` decodes + strips it for display. Best-effort: never throws into the
|
|
727
|
+
* run, and no-op when the engine is stateless. Idempotent (skips an already-prefixed turn) and bounded to
|
|
728
|
+
* THIS run's responses (only assistant turns AFTER the most-recent user message) so a wide recall window
|
|
729
|
+
* can never re-stamp an older agent's turn with the wrong slug.
|
|
730
|
+
*/
|
|
731
|
+
async attributeRun(ctx) {
|
|
732
|
+
if (!this.memory) return;
|
|
733
|
+
const mem = this.memory;
|
|
734
|
+
const rid = resourceId(ctx);
|
|
735
|
+
try {
|
|
736
|
+
const { messages } = await mem.recall({ threadId: ctx.threadId, resourceId: rid, threadConfig: { lastMessages: 30 } });
|
|
737
|
+
let lastUserIdx = -1;
|
|
738
|
+
for (let i = 0; i < messages.length; i++) if (messages[i].role === "user") lastUserIdx = i;
|
|
739
|
+
if (lastUserIdx >= 0) {
|
|
740
|
+
const um = messages[lastUserIdx];
|
|
741
|
+
const content = um.content ?? {};
|
|
742
|
+
const parts = Array.isArray(content.parts) ? content.parts : [];
|
|
743
|
+
const idx = parts.findIndex((p) => p?.type === "text");
|
|
744
|
+
const cur = idx >= 0 ? String(parts[idx].text ?? "") : "";
|
|
745
|
+
const existing = /^\[user:([^\]]*)\]/.exec(cur);
|
|
746
|
+
if (idx >= 0 && (!existing || existing[1] === ctx.userId)) {
|
|
747
|
+
const stripped = cur.replace(/^(\[user:[^\]]*\]\s*)+/, "");
|
|
748
|
+
const stampedParts = parts.map((p, i) => i === idx ? { ...p, text: `[user:${ctx.userId}] ${stripped}` } : p);
|
|
749
|
+
const flatU = stampedParts.filter((p) => p?.type === "text").map((p) => String(p.text ?? "")).join("");
|
|
750
|
+
await mem.updateMessages({
|
|
751
|
+
messages: [{ id: um.id, role: "user", threadId: um.threadId ?? ctx.threadId, resourceId: rid, content: { ...content, format: 2, parts: stampedParts, content: flatU } }]
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
const prefix = `[${ctx.agentSlug}] `;
|
|
756
|
+
for (const m of messages.slice(lastUserIdx + 1)) {
|
|
757
|
+
if (m.role !== "assistant") continue;
|
|
758
|
+
const content = m.content ?? {};
|
|
759
|
+
const parts = Array.isArray(content.parts) ? content.parts : [];
|
|
760
|
+
const idx = parts.findIndex((p) => p?.type === "text");
|
|
761
|
+
if (idx < 0) continue;
|
|
762
|
+
const firstText = String(parts[idx].text ?? "");
|
|
763
|
+
if (firstText.startsWith("[")) continue;
|
|
764
|
+
const newParts = parts.map((p, i) => i === idx ? { ...p, text: prefix + firstText } : p);
|
|
765
|
+
const flat = newParts.filter((p) => p?.type === "text").map((p) => String(p.text ?? "")).join("");
|
|
766
|
+
await mem.updateMessages({
|
|
767
|
+
messages: [{ id: m.id, role: "assistant", threadId: m.threadId ?? ctx.threadId, resourceId: rid, content: { ...content, format: 2, parts: newParts, content: flat } }]
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
} catch {
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Part B — mirror a delegation into the sub-agent's LANE thread. A delegation runs the sub-agent as a
|
|
775
|
+
* Mastra `agent-<slug>` tool inside the orchestrator's turn, so its work lands in the orchestrator's
|
|
776
|
+
* thread, not the sub-agent's lane (`<container>:<slug>`). After the run drains, this recalls THIS run's
|
|
777
|
+
* assistant turn(s) and, for each `agent-<slug>` tool-invocation part (which carries BOTH the task =
|
|
778
|
+
* `args.prompt` and the answer = `result.text`), `saveMessages` the pair into the sub-agent's lane thread
|
|
779
|
+
* — so switching to that agent's lane shows the delegated task + result, durably. Best-effort; Mastra-only
|
|
780
|
+
* (no nightowls.runs row ⇒ no thread-backing FK). Bounded to this run's turns ⇒ each run mirrors once.
|
|
781
|
+
*/
|
|
782
|
+
async mirrorDelegations(ctx) {
|
|
783
|
+
if (!this.memory) return;
|
|
784
|
+
const mem = this.memory;
|
|
785
|
+
const rid = resourceId(ctx);
|
|
786
|
+
try {
|
|
787
|
+
const { messages } = await mem.recall({ threadId: ctx.threadId, resourceId: rid, threadConfig: { lastMessages: 30 } });
|
|
788
|
+
let lastUserIdx = -1;
|
|
789
|
+
for (let i = 0; i < messages.length; i++) if (messages[i].role === "user") lastUserIdx = i;
|
|
790
|
+
for (const m of messages.slice(lastUserIdx + 1)) {
|
|
791
|
+
if (m.role !== "assistant") continue;
|
|
792
|
+
const parts = Array.isArray(m.content?.parts) ? m.content.parts : [];
|
|
793
|
+
for (const p of parts) {
|
|
794
|
+
if (p?.type !== "tool-invocation") continue;
|
|
795
|
+
const ti = p.toolInvocation;
|
|
796
|
+
if (!ti || ti.state !== "result" || !ti.toolName?.startsWith("agent-")) continue;
|
|
797
|
+
const subSlug = ti.toolName.slice("agent-".length);
|
|
798
|
+
const task = (ti.args?.prompt ?? "").trim() || "(no task)";
|
|
799
|
+
const answer = (ti.result?.text ?? "").trim();
|
|
800
|
+
if (!answer) continue;
|
|
801
|
+
const laneTid = `${ctx.threadId}:${subSlug}`;
|
|
802
|
+
await mem.createThread({ threadId: laneTid, resourceId: rid, metadata: {} });
|
|
803
|
+
await mem.saveMessages({
|
|
804
|
+
messages: [
|
|
805
|
+
{ role: "user", threadId: laneTid, resourceId: rid, content: { format: 2, parts: [{ type: "text", text: `\u21AA ${ctx.agentSlug} asked: ${task}` }] } },
|
|
806
|
+
{ role: "assistant", threadId: laneTid, resourceId: rid, content: { format: 2, parts: [{ type: "text", text: answer }] } }
|
|
807
|
+
]
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
} catch {
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* List the user's conversations from the engine's Memory and return them as wall-safe
|
|
816
|
+
* `ThreadSummary[]` (no engine-vendor type in the signature or the result — powers a threads-list
|
|
817
|
+
* sidebar). Returns `[]` when the engine is stateless (no memory configured).
|
|
818
|
+
*
|
|
819
|
+
* Shape is spike-proven against the Mastra memory surface (`memory.listThreads`, NOT the
|
|
820
|
+
* non-existent `getThreadsByResourceId`): resourceId goes under `filter`, the envelope is
|
|
821
|
+
* `{ threads }`, and each thread row carries `{ id, title?, createdAt, updatedAt }` (see the
|
|
822
|
+
* threads-spike SPIKE-FINDINGS). The vendor value is cast THROUGH a local structural type so no
|
|
823
|
+
* vendor type leaks to the public `.d.ts` (engine wall). Already DESC by `updatedAt`, so the
|
|
824
|
+
* newest-active thread is first; no per-thread recall (avoids N+1) — the engine auto-titles threads.
|
|
825
|
+
*/
|
|
826
|
+
async listThreads(ctx, opts) {
|
|
827
|
+
if (!this.memory) return [];
|
|
828
|
+
const mem = this.memory;
|
|
829
|
+
const limit = opts?.limit ?? 50;
|
|
830
|
+
const toSummary = (t) => ({
|
|
831
|
+
threadId: t.id,
|
|
832
|
+
title: (t.title ?? "").trim() || new Date(t.createdAt).toLocaleString(),
|
|
833
|
+
lastActivityAt: +new Date(t.updatedAt)
|
|
834
|
+
});
|
|
835
|
+
const lister = this.opts.storage.runs.listUserContainers;
|
|
836
|
+
if (lister) {
|
|
837
|
+
const containers = await lister.call(this.opts.storage.runs, ctx.tenantId, ctx.userId, limit);
|
|
838
|
+
const out = await Promise.all(
|
|
839
|
+
containers.map(async (container) => {
|
|
840
|
+
const { threads: threads2 } = await mem.listThreads({ filter: { resourceId: `${ctx.tenantId}:${container}` }, perPage: 8, page: 0 });
|
|
841
|
+
const bare = threads2.find((t) => t.id === container) ?? threads2.find((t) => !t.id.includes(":"));
|
|
842
|
+
return bare ? toSummary(bare) : { threadId: container, title: container, lastActivityAt: 0 };
|
|
843
|
+
})
|
|
844
|
+
);
|
|
845
|
+
return out.slice(0, limit);
|
|
846
|
+
}
|
|
847
|
+
const { threads } = await mem.listThreads({
|
|
848
|
+
filter: { resourceId: resourceId(ctx) },
|
|
849
|
+
orderBy: { field: "updatedAt", direction: "DESC" },
|
|
850
|
+
perPage: limit * 4,
|
|
851
|
+
page: 0
|
|
852
|
+
});
|
|
853
|
+
return threads.filter((t) => !t.id.includes(":")).map(toSummary).slice(0, limit);
|
|
854
|
+
}
|
|
855
|
+
/** The PUBLIC entries of a conversation's scratchpad (empty array when the feature is off or unset). */
|
|
856
|
+
async scratchpadPublic(container, ctx) {
|
|
857
|
+
const all = await this.opts.storage.scratchpad?.list(ctx.tenantId, container) ?? [];
|
|
858
|
+
return all.filter((e) => e.section === "public");
|
|
859
|
+
}
|
|
860
|
+
/** In-flight runs (running|suspended) for a container + its lanes — powers cross-lane background presence (E5). */
|
|
861
|
+
async activeRuns(container, ctx) {
|
|
862
|
+
return this.opts.storage.runs.listActive(ctx.tenantId, container);
|
|
863
|
+
}
|
|
864
|
+
/** The tenant's agent roster (slug, title-cased display name, role, delegate graph) as wall-safe
|
|
865
|
+
* AgentSummary[]. Sourced from the agent rows; no vendor type in the signature or result. Powers
|
|
866
|
+
* the multi-agent pile / @mention UI. */
|
|
867
|
+
async listAgents(ctx) {
|
|
868
|
+
const slugs = await this.opts.storage.agents.listSlugs(ctx.tenantId);
|
|
869
|
+
const rows = await Promise.all(slugs.map((s) => this.opts.storage.agents.head(ctx.tenantId, s)));
|
|
870
|
+
return rows.filter((r) => !!r).map((r) => ({
|
|
871
|
+
slug: r.slug,
|
|
872
|
+
name: titleCase(r.slug),
|
|
873
|
+
role: r.role,
|
|
874
|
+
delegateSlugs: r.delegateSlugs ?? []
|
|
875
|
+
}));
|
|
876
|
+
}
|
|
877
|
+
async *run(input, ctx) {
|
|
878
|
+
const modelId = (await this.loadRow(ctx.tenantId, ctx.agentSlug)).modelId ?? "unknown";
|
|
879
|
+
await this.opts.storage.runs.create({
|
|
880
|
+
runId: ctx.runId,
|
|
881
|
+
tenantId: ctx.tenantId,
|
|
882
|
+
userId: ctx.userId,
|
|
883
|
+
threadId: ctx.threadId,
|
|
884
|
+
agentSlug: ctx.agentSlug
|
|
885
|
+
});
|
|
886
|
+
const modelIdFor = (slug) => this.rowCache.peek(`${ctx.tenantId}:${slug}`)?.modelId ?? modelId;
|
|
887
|
+
const gov = new CostGovernor(this.opts.cost);
|
|
888
|
+
const delegateBudgets = this.opts.cost.perDelegate ? new DelegateBudgets(this.opts.cost.perDelegate, ctx.agentSlug) : null;
|
|
889
|
+
const streamed = /* @__PURE__ */ new Set();
|
|
890
|
+
const rc = this.requestContext(ctx);
|
|
891
|
+
if (this.opts.pageContext) attachPageContext(rc, input.context);
|
|
892
|
+
const collector = this.opts.telemetry ? new SpanCollector(ctx.runId, () => Date.now(), "run", { agentSlug: ctx.agentSlug }) : null;
|
|
893
|
+
let ts = 0;
|
|
894
|
+
const emit = async (e) => {
|
|
895
|
+
e.seq = await this.opts.storage.events.append(e);
|
|
896
|
+
return e;
|
|
897
|
+
};
|
|
898
|
+
const floorKey = ctx.threadId;
|
|
899
|
+
const me = { label: titleCase(ctx.agentSlug), runId: ctx.runId };
|
|
900
|
+
const floorAbort = new AbortController();
|
|
901
|
+
let releaseFloor = await this.floor.tryAcquire(floorKey, me);
|
|
902
|
+
try {
|
|
903
|
+
if (!releaseFloor) {
|
|
904
|
+
const held = await this.floor.holder(floorKey);
|
|
905
|
+
const position = await this.floor.queueDepth(floorKey) + 1;
|
|
906
|
+
yield await emit(ev("swarm.status", base(ctx, ts++), { state: "blocked", note: held?.label ?? "another agent", position }));
|
|
907
|
+
releaseFloor = await this.floor.acquire(floorKey, me, floorAbort.signal);
|
|
908
|
+
if (floorAbort.signal.aborted) return;
|
|
909
|
+
}
|
|
910
|
+
yield await emit(ev("swarm.status", base(ctx, ts++), { state: "thinking" }));
|
|
911
|
+
const userMessage = input.message.replace(/^(\[user:[^\]]*\]\s*)+/, "");
|
|
912
|
+
const result = await this.agent().stream(userMessage, { runId: ctx.runId, requestContext: rc, ...this.memoryOpts(ctx) });
|
|
913
|
+
for await (const part of result.fullStream) {
|
|
914
|
+
if (part?.type === "step-finish") gov.step();
|
|
915
|
+
if (part?.type === "tool-call-suspended") {
|
|
916
|
+
const payload = part.payload ?? {};
|
|
917
|
+
const toolCallId = payload.toolCallId ?? "";
|
|
918
|
+
const followupId = `${ctx.runId}:${toolCallId}`;
|
|
919
|
+
const sp = payload.suspendPayload ?? {};
|
|
920
|
+
await recordSuspend(this.opts.storage, ctx, followupId, toolCallId);
|
|
921
|
+
await this.opts.storage.runs.setStatus(ctx.runId, "suspended");
|
|
922
|
+
await this.opts.storage.runs.saveSnapshot(ctx.runId, { pending: { toolCallId } });
|
|
923
|
+
yield await emit(ev("swarm.status", base(ctx, ts++), { state: "waiting" }));
|
|
924
|
+
yield await emit(
|
|
925
|
+
ev("swarm.question", base(ctx, ts++), {
|
|
926
|
+
followupId,
|
|
927
|
+
toolCallId,
|
|
928
|
+
to: sp.to ?? "user",
|
|
929
|
+
from: sp.asker || ctx.agentSlug,
|
|
930
|
+
// the agent that actually asked (a delegate), for UI attribution
|
|
931
|
+
prompt: sp.prompt ?? "",
|
|
932
|
+
field: sp.field
|
|
933
|
+
})
|
|
934
|
+
);
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
if (part?.type === "error") {
|
|
938
|
+
await this.opts.storage.runs.setStatus(ctx.runId, "failed");
|
|
939
|
+
yield await emit(
|
|
940
|
+
ev("swarm.run_failed", base(ctx, ts++), {
|
|
941
|
+
stage: "stream",
|
|
942
|
+
message: streamErrorMessage(part),
|
|
943
|
+
retryable: false
|
|
944
|
+
})
|
|
945
|
+
);
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
for (const e of mapChunk(part, ctx, gov, modelIdFor, () => ts++, streamed, delegateBudgets)) yield await emit(e);
|
|
949
|
+
collectSpans(collector, part, modelId, gov);
|
|
950
|
+
const overDelegate = delegateBudgets?.exceeded();
|
|
951
|
+
if (gov.shouldStop().stop || overDelegate) {
|
|
952
|
+
await this.opts.storage.runs.setStatus(ctx.runId, "failed");
|
|
953
|
+
yield await emit(
|
|
954
|
+
ev("swarm.run_failed", base(ctx, ts++), {
|
|
955
|
+
stage: "cost",
|
|
956
|
+
message: overDelegate?.reason ?? gov.shouldStop().reason,
|
|
957
|
+
retryable: false
|
|
958
|
+
})
|
|
959
|
+
);
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
await this.mirrorDelegations(ctx);
|
|
964
|
+
await this.attributeRun(ctx);
|
|
965
|
+
await this.opts.storage.runs.setStatus(ctx.runId, "done");
|
|
966
|
+
yield await emit(ev("swarm.status", base(ctx, ts++), { state: "done" }));
|
|
967
|
+
} catch (err) {
|
|
968
|
+
console.error(`[nightowls] run ${ctx.runId} threw:`, err);
|
|
969
|
+
try {
|
|
970
|
+
await this.opts.storage.runs.setStatus(ctx.runId, "failed");
|
|
971
|
+
} catch {
|
|
972
|
+
}
|
|
973
|
+
yield await emit(ev("swarm.run_failed", base(ctx, ts++), { stage: "exception", message: errMessage(err), retryable: false }));
|
|
974
|
+
} finally {
|
|
975
|
+
floorAbort.abort();
|
|
976
|
+
await releaseFloor?.();
|
|
977
|
+
await exportSpans(this.opts.telemetry, collector);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
async *resume(args, ctx) {
|
|
981
|
+
const snap = await this.opts.storage.runs.loadSnapshot(ctx.tenantId, args.runId);
|
|
982
|
+
if (!snap) throw new Error(`no suspended run: ${args.runId}`);
|
|
983
|
+
await this.opts.storage.markFollowupAnswered?.(args.followupId, ctx.tenantId);
|
|
984
|
+
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) => this.rowCache.peek(`${ctx.tenantId}:${slug}`)?.modelId ?? modelId;
|
|
987
|
+
const gov = new CostGovernor(this.opts.cost);
|
|
988
|
+
const delegateBudgets = this.opts.cost.perDelegate ? new DelegateBudgets(this.opts.cost.perDelegate, ctx.agentSlug) : null;
|
|
989
|
+
const streamed = /* @__PURE__ */ new Set();
|
|
990
|
+
const collector = this.opts.telemetry ? new SpanCollector(args.runId, () => Date.now(), "resume", { agentSlug: ctx.agentSlug }) : null;
|
|
991
|
+
let ts = 1e3;
|
|
992
|
+
const emit = async (e) => {
|
|
993
|
+
e.seq = await this.opts.storage.events.append(e);
|
|
994
|
+
return e;
|
|
995
|
+
};
|
|
996
|
+
const floorKey = ctx.threadId;
|
|
997
|
+
const me = { label: titleCase(ctx.agentSlug), runId: args.runId };
|
|
998
|
+
const floorAbort = new AbortController();
|
|
999
|
+
let releaseFloor = await this.floor.tryAcquire(floorKey, me);
|
|
1000
|
+
const rctx = { ...ctx, runId: args.runId };
|
|
1001
|
+
try {
|
|
1002
|
+
if (!releaseFloor) {
|
|
1003
|
+
const held = await this.floor.holder(floorKey);
|
|
1004
|
+
const position = await this.floor.queueDepth(floorKey) + 1;
|
|
1005
|
+
yield await emit(ev("swarm.status", base(rctx, ts++), { state: "blocked", note: held?.label ?? "another agent", position }));
|
|
1006
|
+
releaseFloor = await this.floor.acquire(floorKey, me, floorAbort.signal);
|
|
1007
|
+
if (floorAbort.signal.aborted) return;
|
|
1008
|
+
}
|
|
1009
|
+
yield await emit(
|
|
1010
|
+
ev("swarm.answer", base({ ...ctx, runId: args.runId }, ts++), {
|
|
1011
|
+
followupId: args.followupId,
|
|
1012
|
+
from: "user",
|
|
1013
|
+
answer: args.answer
|
|
1014
|
+
})
|
|
1015
|
+
);
|
|
1016
|
+
const rc = this.requestContext({ ...ctx, runId: args.runId });
|
|
1017
|
+
if (this.opts.pageContext) attachPageContext(rc, args.context);
|
|
1018
|
+
const result = await this.agent().resumeStream(
|
|
1019
|
+
{ answer: args.answer },
|
|
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") {
|
|
1046
|
+
await this.opts.storage.runs.setStatus(args.runId, "failed");
|
|
1047
|
+
yield await emit(
|
|
1048
|
+
ev("swarm.run_failed", base(rctx, ts++), {
|
|
1049
|
+
stage: "stream",
|
|
1050
|
+
message: streamErrorMessage(part),
|
|
1051
|
+
retryable: false
|
|
1052
|
+
})
|
|
1053
|
+
);
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
collectSpans(collector, part, modelId, gov);
|
|
1057
|
+
for (const e of mapChunk(part, rctx, gov, modelIdFor, () => ts++, streamed, delegateBudgets)) yield await emit(e);
|
|
1058
|
+
const overDelegate = delegateBudgets?.exceeded();
|
|
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;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
await this.attributeRun(rctx);
|
|
1072
|
+
await this.opts.storage.runs.setStatus(args.runId, "done");
|
|
1073
|
+
yield await emit(ev("swarm.status", base(rctx, ts++), { state: "done" }));
|
|
1074
|
+
} catch (err) {
|
|
1075
|
+
console.error(`[nightowls] resume ${args.runId} threw:`, err);
|
|
1076
|
+
try {
|
|
1077
|
+
await this.opts.storage.runs.setStatus(args.runId, "failed");
|
|
1078
|
+
} catch {
|
|
1079
|
+
}
|
|
1080
|
+
yield await emit(ev("swarm.run_failed", base(rctx, ts++), { stage: "exception", message: errMessage(err), retryable: false }));
|
|
1081
|
+
} finally {
|
|
1082
|
+
floorAbort.abort();
|
|
1083
|
+
await releaseFloor?.();
|
|
1084
|
+
await exportSpans(this.opts.telemetry, collector);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
};
|
|
1088
|
+
function errMessage(err) {
|
|
1089
|
+
return err instanceof Error ? err.message : String(err);
|
|
1090
|
+
}
|
|
1091
|
+
function base(ctx, ts) {
|
|
1092
|
+
return { runId: ctx.runId, agentSlug: ctx.agentSlug, ts };
|
|
1093
|
+
}
|
|
1094
|
+
function titleCase(slug) {
|
|
1095
|
+
return slug.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
1096
|
+
}
|
|
1097
|
+
function safeParse(s) {
|
|
1098
|
+
try {
|
|
1099
|
+
const v = JSON.parse(s);
|
|
1100
|
+
return v && typeof v === "object" ? v : {};
|
|
1101
|
+
} catch {
|
|
1102
|
+
return {};
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
function streamErrorMessage(part) {
|
|
1106
|
+
const err = part.payload?.error;
|
|
1107
|
+
return String(err?.message ?? "stream error");
|
|
1108
|
+
}
|
|
1109
|
+
function collectSpans(collector, part, modelId, gov) {
|
|
1110
|
+
if (!collector) return;
|
|
1111
|
+
const p = part.payload ?? {};
|
|
1112
|
+
switch (part.type) {
|
|
1113
|
+
case "text-delta":
|
|
1114
|
+
collector.openGeneration(modelId);
|
|
1115
|
+
break;
|
|
1116
|
+
case "tool-call":
|
|
1117
|
+
collector.openGeneration(modelId);
|
|
1118
|
+
collector.openTool(p.toolCallId, p.toolName);
|
|
1119
|
+
break;
|
|
1120
|
+
case "tool-result":
|
|
1121
|
+
collector.closeTool(p.toolCallId, true);
|
|
1122
|
+
break;
|
|
1123
|
+
case "step-finish": {
|
|
1124
|
+
const output = p.output;
|
|
1125
|
+
const usage = output?.usage;
|
|
1126
|
+
const u = { inputTokens: usage?.inputTokens ?? 0, outputTokens: usage?.outputTokens ?? 0 };
|
|
1127
|
+
collector.openGeneration(modelId);
|
|
1128
|
+
collector.closeGeneration(u, gov.priceOf(modelId, u));
|
|
1129
|
+
break;
|
|
1130
|
+
}
|
|
1131
|
+
default:
|
|
1132
|
+
break;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
async function exportSpans(telemetry, collector) {
|
|
1136
|
+
if (!telemetry || !collector) return;
|
|
1137
|
+
try {
|
|
1138
|
+
await telemetry.export(collector.finish());
|
|
1139
|
+
} catch {
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
var warnedNoRecordSuspend = false;
|
|
1143
|
+
async function recordSuspend(storage, ctx, followupId, toolCallId) {
|
|
1144
|
+
if (!storage.recordSuspend && !warnedNoRecordSuspend) {
|
|
1145
|
+
warnedNoRecordSuspend = true;
|
|
1146
|
+
console.warn(
|
|
1147
|
+
"[nightowls] storage adapter does not implement recordSuspend() \u2014 human-in-the-loop resume will be forbidden (the followup index is never written). Implement recordSuspend on your StorageAdapter."
|
|
1148
|
+
);
|
|
1149
|
+
}
|
|
1150
|
+
await storage.recordSuspend?.(ctx.runId, ctx.tenantId, followupId, toolCallId);
|
|
1151
|
+
}
|
|
1152
|
+
function mapChunk(part, ctx, gov, modelIdFor, nextTs, streamed, delegateBudgets) {
|
|
1153
|
+
const p = part.payload ?? {};
|
|
1154
|
+
const modelId = modelIdFor(ctx.agentSlug);
|
|
1155
|
+
switch (part.type) {
|
|
1156
|
+
case "text-delta":
|
|
1157
|
+
return [ev("swarm.message", base(ctx, nextTs()), { role: "assistant", delta: p.text ?? "" })];
|
|
1158
|
+
case "tool-call": {
|
|
1159
|
+
const name = p.toolName;
|
|
1160
|
+
if (name?.startsWith("agent-")) {
|
|
1161
|
+
const to = name.slice("agent-".length);
|
|
1162
|
+
const a = typeof p.args === "string" ? safeParse(p.args) : p.args;
|
|
1163
|
+
const task = a?.prompt ?? "";
|
|
1164
|
+
return [
|
|
1165
|
+
ev("swarm.handoff", base(ctx, nextTs()), { from: ctx.agentSlug, to, task }),
|
|
1166
|
+
ev("swarm.status", base(ctx, nextTs()), { state: "delegating", note: to })
|
|
1167
|
+
];
|
|
1168
|
+
}
|
|
1169
|
+
return [
|
|
1170
|
+
ev("swarm.tool_call", base(ctx, nextTs()), {
|
|
1171
|
+
toolCallId: p.toolCallId,
|
|
1172
|
+
name,
|
|
1173
|
+
args: p.args,
|
|
1174
|
+
needsApproval: false
|
|
1175
|
+
})
|
|
1176
|
+
];
|
|
1177
|
+
}
|
|
1178
|
+
case "tool-result": {
|
|
1179
|
+
const name = p.toolName;
|
|
1180
|
+
if (name?.startsWith("agent-")) {
|
|
1181
|
+
const to = name.slice("agent-".length);
|
|
1182
|
+
const tcid = p.toolCallId;
|
|
1183
|
+
const out = [];
|
|
1184
|
+
if (!streamed.has(tcid)) {
|
|
1185
|
+
const text = p.result?.text;
|
|
1186
|
+
if (text) out.push(ev("swarm.message", base({ ...ctx, agentSlug: to }, nextTs()), { role: "assistant", text }));
|
|
1187
|
+
}
|
|
1188
|
+
out.push(ev("swarm.status", base(ctx, nextTs()), { state: "thinking" }));
|
|
1189
|
+
return out;
|
|
1190
|
+
}
|
|
1191
|
+
return [
|
|
1192
|
+
ev("swarm.tool_result", base(ctx, nextTs()), {
|
|
1193
|
+
toolCallId: p.toolCallId,
|
|
1194
|
+
ok: true,
|
|
1195
|
+
result: p.result
|
|
1196
|
+
})
|
|
1197
|
+
];
|
|
1198
|
+
}
|
|
1199
|
+
case "finish": {
|
|
1200
|
+
const output = p.output;
|
|
1201
|
+
const usage = output?.usage;
|
|
1202
|
+
if (usage) {
|
|
1203
|
+
const u = { inputTokens: usage.inputTokens ?? 0, outputTokens: usage.outputTokens ?? 0 };
|
|
1204
|
+
gov.addUsage(modelId, u);
|
|
1205
|
+
delegateBudgets?.addUsage(ctx.agentSlug, modelId, u);
|
|
1206
|
+
}
|
|
1207
|
+
return [];
|
|
1208
|
+
}
|
|
1209
|
+
case "tool-output": {
|
|
1210
|
+
const name = p.toolName ?? "";
|
|
1211
|
+
if (!name.startsWith("agent-")) return [];
|
|
1212
|
+
const subSlug = name.slice("agent-".length);
|
|
1213
|
+
const inner = p.output;
|
|
1214
|
+
if (!inner || typeof inner.type !== "string") return [];
|
|
1215
|
+
if (inner.type === "text-delta") streamed.add(p.toolCallId);
|
|
1216
|
+
return mapChunk(inner, { ...ctx, agentSlug: subSlug }, gov, modelIdFor, nextTs, streamed, delegateBudgets);
|
|
1217
|
+
}
|
|
1218
|
+
case "tool-error": {
|
|
1219
|
+
const name = p.toolName ?? "";
|
|
1220
|
+
const err = p.error;
|
|
1221
|
+
const error = typeof err === "string" ? err : err?.message ?? "tool error";
|
|
1222
|
+
const failed = ev("swarm.tool_result", base(ctx, nextTs()), {
|
|
1223
|
+
toolCallId: p.toolCallId,
|
|
1224
|
+
ok: false,
|
|
1225
|
+
error
|
|
1226
|
+
});
|
|
1227
|
+
return name.startsWith("agent-") ? [failed, ev("swarm.status", base(ctx, nextTs()), { state: "thinking" })] : [failed];
|
|
1228
|
+
}
|
|
1229
|
+
default:
|
|
1230
|
+
return [];
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
// src/model.ts
|
|
1235
|
+
function allowListModelProvider(opts) {
|
|
1236
|
+
const set = new Set(opts.allow);
|
|
1237
|
+
return {
|
|
1238
|
+
resolve: async (modelId) => {
|
|
1239
|
+
if (!set.has(modelId)) throw new Error(`model not allowed: ${modelId}`);
|
|
1240
|
+
return modelId;
|
|
1241
|
+
}
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
// src/define.ts
|
|
1246
|
+
var MASTRA = /* @__PURE__ */ new WeakMap();
|
|
1247
|
+
function defineTool(spec) {
|
|
1248
|
+
const origin = spec.origin ?? "first-party";
|
|
1249
|
+
const needsApproval = spec.needsApproval ?? origin === "mcp";
|
|
1250
|
+
const mastraTool = createTool4({
|
|
1251
|
+
id: spec.name,
|
|
1252
|
+
description: spec.description ?? spec.name,
|
|
1253
|
+
inputSchema: spec.inputSchema,
|
|
1254
|
+
outputSchema: spec.outputSchema,
|
|
1255
|
+
// Mastra 1.38 (per SPIKE-FINDINGS item 3): execute is `(inputData, context) => out`
|
|
1256
|
+
// with TWO positional args. `inputData` is the parsed input; tenant/user/run come
|
|
1257
|
+
// off `context.requestContext`.
|
|
1258
|
+
execute: async (inputData, context) => {
|
|
1259
|
+
const rc = context?.requestContext;
|
|
1260
|
+
const ctx = {
|
|
1261
|
+
tenantId: rc?.get?.("tenantId") ?? "default",
|
|
1262
|
+
userId: rc?.get?.("userId") ?? "",
|
|
1263
|
+
runId: rc?.get?.("runId") ?? ""
|
|
1264
|
+
};
|
|
1265
|
+
return spec.execute(inputData, ctx);
|
|
1266
|
+
}
|
|
1267
|
+
});
|
|
1268
|
+
const handle = { name: spec.name, needsApproval, origin };
|
|
1269
|
+
MASTRA.set(handle, mastraTool);
|
|
1270
|
+
return handle;
|
|
1271
|
+
}
|
|
1272
|
+
function defineSkill(tool) {
|
|
1273
|
+
return tool;
|
|
1274
|
+
}
|
|
1275
|
+
function __getMastraTool(t) {
|
|
1276
|
+
return MASTRA.get(t);
|
|
1277
|
+
}
|
|
1278
|
+
function defineAgent(spec) {
|
|
1279
|
+
const skills = spec.skills ?? [];
|
|
1280
|
+
return {
|
|
1281
|
+
slug: spec.slug,
|
|
1282
|
+
// The concrete skill handles ride along on the def so defineSwarm can build
|
|
1283
|
+
// a per-swarm resolver. No module-level registry → no cross-swarm leakage.
|
|
1284
|
+
skills,
|
|
1285
|
+
head: {
|
|
1286
|
+
slug: spec.slug,
|
|
1287
|
+
version: 1,
|
|
1288
|
+
role: spec.role ?? "specialist",
|
|
1289
|
+
personality: spec.personality,
|
|
1290
|
+
capabilities: spec.capabilities ?? [],
|
|
1291
|
+
skillNames: skills.map((s) => s.name),
|
|
1292
|
+
delegateSlugs: spec.delegates ?? [],
|
|
1293
|
+
modelId: spec.modelId ?? "openai/gpt-5.5",
|
|
1294
|
+
...spec.memory ? { memory: spec.memory } : {}
|
|
1295
|
+
}
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
function buildSkillResolver(agents) {
|
|
1299
|
+
const map = /* @__PURE__ */ new Map();
|
|
1300
|
+
for (const a of agents) for (const s of a.skills ?? []) map.set(s.name, s);
|
|
1301
|
+
return (name) => map.get(name);
|
|
1302
|
+
}
|
|
1303
|
+
var ASK_TOOL_NAME = "ask";
|
|
1304
|
+
var askFieldSchema = z4.object({
|
|
1305
|
+
kind: z4.enum(["confirm", "buttons", "select", "multiselect", "number", "text", "textarea"]),
|
|
1306
|
+
options: z4.array(z4.object({ label: z4.string(), value: z4.string(), description: z4.string().optional() })).optional(),
|
|
1307
|
+
min: z4.number().optional(),
|
|
1308
|
+
max: z4.number().optional(),
|
|
1309
|
+
step: z4.number().optional(),
|
|
1310
|
+
unit: z4.string().optional(),
|
|
1311
|
+
placeholder: z4.string().optional(),
|
|
1312
|
+
rows: z4.number().optional(),
|
|
1313
|
+
confirmLabel: z4.string().optional(),
|
|
1314
|
+
rejectLabel: z4.string().optional(),
|
|
1315
|
+
submitLabel: z4.string().optional(),
|
|
1316
|
+
default: z4.any().optional()
|
|
1317
|
+
}).optional();
|
|
1318
|
+
function buildAskMastraTool() {
|
|
1319
|
+
return createTool4({
|
|
1320
|
+
id: ASK_TOOL_NAME,
|
|
1321
|
+
description: "Ask a follow-up question to the user (or another agent) when blocked. Prefer a RICH `field` so the user gets a fitting control instead of a text box: {kind:'confirm'} for yes/no; {kind:'buttons'|'select'|'multiselect', options:[{label,value}]} for choices; {kind:'number', min,max,step,unit} for a number; {kind:'text'|'textarea', placeholder} for free text. Omit `field` for a plain text answer. `prompt` may be markdown.",
|
|
1322
|
+
inputSchema: z4.object({ to: z4.string().default("user"), prompt: z4.string(), field: askFieldSchema }),
|
|
1323
|
+
// Rich answer: boolean (confirm), string (text/select/buttons), number, or string[] (multiselect).
|
|
1324
|
+
outputSchema: z4.object({ answer: z4.any() }),
|
|
1325
|
+
suspendSchema: z4.object({ to: z4.string(), prompt: z4.string(), field: askFieldSchema, asker: z4.string().optional() }),
|
|
1326
|
+
resumeSchema: z4.object({ answer: z4.any() }),
|
|
1327
|
+
execute: async (inputData, context) => {
|
|
1328
|
+
const agentCtx = context?.agent;
|
|
1329
|
+
if (agentCtx?.resumeData) {
|
|
1330
|
+
return { answer: agentCtx.resumeData.answer };
|
|
1331
|
+
}
|
|
1332
|
+
const input = inputData;
|
|
1333
|
+
const rc = context?.requestContext;
|
|
1334
|
+
const runSlug = rc?.get("agentSlug") ?? "";
|
|
1335
|
+
const agentId = agentCtx?.agentId ?? "";
|
|
1336
|
+
const asker = agentId.startsWith("swarm-sub-") ? agentId.slice("swarm-sub-".length) : runSlug;
|
|
1337
|
+
await agentCtx?.suspend({ to: input.to ?? "user", prompt: input.prompt, field: input.field, asker });
|
|
1338
|
+
return { answer: "" };
|
|
1339
|
+
}
|
|
1340
|
+
});
|
|
1341
|
+
}
|
|
1342
|
+
function defineSwarm(cfg) {
|
|
1343
|
+
const seedable = cfg.storage;
|
|
1344
|
+
for (const a of cfg.agents) seedable.seedAgent?.(a.head);
|
|
1345
|
+
const engine = new SwarmEngine({
|
|
1346
|
+
storage: cfg.storage,
|
|
1347
|
+
model: allowListModelProvider({ allow: cfg.models.allow }),
|
|
1348
|
+
modelFactory: cfg.modelFactory,
|
|
1349
|
+
cost: cfg.cost,
|
|
1350
|
+
// Per-swarm skill registry, built from the agents passed in. No global state.
|
|
1351
|
+
resolveSkill: buildSkillResolver(cfg.agents),
|
|
1352
|
+
telemetry: resolveTelemetry(cfg.telemetry),
|
|
1353
|
+
mastraStore: cfg.mastraStore,
|
|
1354
|
+
memory: cfg.memory,
|
|
1355
|
+
pageContext: cfg.pageContext,
|
|
1356
|
+
scratchpad: cfg.scratchpad,
|
|
1357
|
+
recallLane: cfg.recallLane
|
|
1358
|
+
});
|
|
1359
|
+
return { engine };
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
// src/storage/memory.ts
|
|
1363
|
+
var InMemoryStorage = class {
|
|
1364
|
+
evts = [];
|
|
1365
|
+
seq = 0;
|
|
1366
|
+
runRows = /* @__PURE__ */ new Map();
|
|
1367
|
+
snapshots = /* @__PURE__ */ new Map();
|
|
1368
|
+
suspends = /* @__PURE__ */ new Map();
|
|
1369
|
+
// key: tenantId:followupId
|
|
1370
|
+
waitpoints = /* @__PURE__ */ new Map();
|
|
1371
|
+
// followupId -> token (durable runner only)
|
|
1372
|
+
msgs = [];
|
|
1373
|
+
agentRows = /* @__PURE__ */ new Map();
|
|
1374
|
+
// key: tenantId:slug:version
|
|
1375
|
+
heads = /* @__PURE__ */ new Map();
|
|
1376
|
+
// key: tenantId:slug -> version
|
|
1377
|
+
pads = /* @__PURE__ */ new Map();
|
|
1378
|
+
seedAgent(v, tenantId = "default") {
|
|
1379
|
+
this.agentRows.set(`${tenantId}:${v.slug}:${v.version}`, v);
|
|
1380
|
+
this.heads.set(`${tenantId}:${v.slug}`, v.version);
|
|
1381
|
+
}
|
|
1382
|
+
recordSuspend(runId, tenantId, followupId, toolCallId) {
|
|
1383
|
+
this.suspends.set(`${tenantId}:${followupId}`, { runId, toolCallId });
|
|
1384
|
+
}
|
|
1385
|
+
markFollowupAnswered(followupId, tenantId) {
|
|
1386
|
+
this.suspends.delete(`${tenantId}:${followupId}`);
|
|
1387
|
+
}
|
|
1388
|
+
/** Test/host helper: read a run row (the RunStore interface is write-mostly). */
|
|
1389
|
+
getRun(runId) {
|
|
1390
|
+
return this.runRows.get(runId);
|
|
1391
|
+
}
|
|
1392
|
+
events = {
|
|
1393
|
+
append: async (e) => {
|
|
1394
|
+
const seq = ++this.seq;
|
|
1395
|
+
this.evts.push({ ...e, seq });
|
|
1396
|
+
return seq;
|
|
1397
|
+
},
|
|
1398
|
+
// R11: tenant-scoped via the run's tenant. Lenient when no run row exists (orphan events in tests), strict
|
|
1399
|
+
// otherwise (the supabase store enforces org_id per event row — this dev store cross-refs the run).
|
|
1400
|
+
list: async (tenantId, runId, since) => this.evts.filter((e) => {
|
|
1401
|
+
if (e.runId !== runId || (e.seq ?? 0) <= since) return false;
|
|
1402
|
+
const r = this.runRows.get(runId);
|
|
1403
|
+
return !r || r.tenantId === tenantId;
|
|
1404
|
+
}),
|
|
1405
|
+
subscribe: async function* (runId) {
|
|
1406
|
+
for (const e of this.evts.filter((x) => x.runId === runId)) yield e;
|
|
1407
|
+
}.bind(this)
|
|
1408
|
+
};
|
|
1409
|
+
runs = {
|
|
1410
|
+
create: async (r) => {
|
|
1411
|
+
this.runRows.set(r.runId, { ...r, status: "running" });
|
|
1412
|
+
},
|
|
1413
|
+
setStatus: async (runId, status, patch) => {
|
|
1414
|
+
const cur = this.runRows.get(runId);
|
|
1415
|
+
if (cur) this.runRows.set(runId, { ...cur, ...patch, status });
|
|
1416
|
+
},
|
|
1417
|
+
saveSnapshot: async (runId, snap) => {
|
|
1418
|
+
this.snapshots.set(runId, snap);
|
|
1419
|
+
},
|
|
1420
|
+
loadSnapshot: async (tenantId, runId) => {
|
|
1421
|
+
const r = this.runRows.get(runId);
|
|
1422
|
+
if (r && r.tenantId !== tenantId) return null;
|
|
1423
|
+
return this.snapshots.get(runId) ?? null;
|
|
1424
|
+
},
|
|
1425
|
+
findSuspended: async (tenantId, followupId) => this.suspends.get(`${tenantId}:${followupId}`) ?? null,
|
|
1426
|
+
get: async (runId) => {
|
|
1427
|
+
const row = this.runRows.get(runId);
|
|
1428
|
+
if (!row) return null;
|
|
1429
|
+
return { ...row, status: row.status ?? "running" };
|
|
1430
|
+
},
|
|
1431
|
+
listActive: async (tenantId, container) => {
|
|
1432
|
+
const lanePrefix = `${container}:`;
|
|
1433
|
+
return [...this.runRows.values()].filter((r) => r.tenantId === tenantId && (r.threadId === container || r.threadId.startsWith(lanePrefix)) && (r.status === "running" || r.status === "suspended")).map((r) => ({ runId: r.runId, threadId: r.threadId, agentSlug: r.agentSlug, status: r.status }));
|
|
1434
|
+
},
|
|
1435
|
+
// R14: distinct containers the user has a run in, most-recently-active first. The dev store has no run
|
|
1436
|
+
// timestamps, so insertion order is the recency proxy (last-seen index per container).
|
|
1437
|
+
listUserContainers: async (tenantId, userId, limit = 50) => {
|
|
1438
|
+
const lastSeen = /* @__PURE__ */ new Map();
|
|
1439
|
+
let i = 0;
|
|
1440
|
+
for (const r of this.runRows.values()) {
|
|
1441
|
+
if (r.tenantId !== tenantId || r.userId !== userId) continue;
|
|
1442
|
+
lastSeen.set(r.threadId.split(":")[0] || r.threadId, i++);
|
|
1443
|
+
}
|
|
1444
|
+
return [...lastSeen.entries()].sort((a, b) => b[1] - a[1]).map(([c]) => c).slice(0, limit);
|
|
1445
|
+
},
|
|
1446
|
+
// R14 security: empty container OR the user participates (has a run in it).
|
|
1447
|
+
canAccessContainer: async (tenantId, userId, container) => {
|
|
1448
|
+
let hasRuns = false;
|
|
1449
|
+
for (const r of this.runRows.values()) {
|
|
1450
|
+
if (r.tenantId !== tenantId || (r.threadId.split(":")[0] || r.threadId) !== container) continue;
|
|
1451
|
+
hasRuns = true;
|
|
1452
|
+
if (r.userId === userId) return true;
|
|
1453
|
+
}
|
|
1454
|
+
return !hasRuns;
|
|
1455
|
+
},
|
|
1456
|
+
attachWaitpoint: async (followupId, token) => {
|
|
1457
|
+
this.waitpoints.set(followupId, token);
|
|
1458
|
+
},
|
|
1459
|
+
getWaitpoint: async (followupId) => this.waitpoints.get(followupId) ?? null
|
|
1460
|
+
};
|
|
1461
|
+
messages = {
|
|
1462
|
+
append: async (m) => {
|
|
1463
|
+
this.msgs.push(m);
|
|
1464
|
+
},
|
|
1465
|
+
// R11: accepts tenantId for the contract; the dev store has no thread→tenant map and this path is unused
|
|
1466
|
+
// in production (the runner's history goes through engine.history / Mastra recall), so it's best-effort.
|
|
1467
|
+
history: async (_tenantId, threadId, limit = 50) => this.msgs.filter((m) => m.threadId === threadId).slice(-limit)
|
|
1468
|
+
};
|
|
1469
|
+
scratchpad = {
|
|
1470
|
+
write: async (tenantId, container, section, key, content, by) => {
|
|
1471
|
+
const mapKey = `${tenantId}:${container}:${section}`;
|
|
1472
|
+
const sec = this.pads.get(mapKey) ?? /* @__PURE__ */ new Map();
|
|
1473
|
+
sec.set(key, { author: by.agentSlug, user: by.userId, requestedBy: by.requestedBy, content, ts: Date.now() });
|
|
1474
|
+
this.pads.set(mapKey, sec);
|
|
1475
|
+
const maxKeys = by.maxKeys ?? SCRATCHPAD_MAX_KEYS;
|
|
1476
|
+
if (sec.size > maxKeys) {
|
|
1477
|
+
const oldestFirst = [...sec.entries()].sort((a, b) => a[1].ts - b[1].ts);
|
|
1478
|
+
for (let i = 0; i < oldestFirst.length - maxKeys; i++) sec.delete(oldestFirst[i][0]);
|
|
1479
|
+
}
|
|
1480
|
+
},
|
|
1481
|
+
list: async (tenantId, container) => {
|
|
1482
|
+
const out = [];
|
|
1483
|
+
for (const section of ["public", "meta"]) {
|
|
1484
|
+
const sec = this.pads.get(`${tenantId}:${container}:${section}`);
|
|
1485
|
+
if (sec) for (const [key, e] of sec) out.push({ section, key, author: e.author, requestedBy: e.requestedBy, content: e.content, updatedAt: e.ts });
|
|
1486
|
+
}
|
|
1487
|
+
return out;
|
|
1488
|
+
}
|
|
1489
|
+
};
|
|
1490
|
+
agents = {
|
|
1491
|
+
head: async (tenantId, slug) => {
|
|
1492
|
+
const v = this.heads.get(`${tenantId}:${slug}`);
|
|
1493
|
+
return v ? this.agentRows.get(`${tenantId}:${slug}:${v}`) ?? null : null;
|
|
1494
|
+
},
|
|
1495
|
+
getVersion: async (tenantId, slug, version) => this.agentRows.get(`${tenantId}:${slug}:${version}`) ?? null,
|
|
1496
|
+
listSlugs: async (tenantId) => [...this.heads.keys()].filter((k) => k.startsWith(`${tenantId}:`)).map((k) => k.split(":")[1])
|
|
1497
|
+
};
|
|
1498
|
+
};
|
|
1499
|
+
|
|
1500
|
+
// src/auth.ts
|
|
1501
|
+
var customAuth = (fn) => ({ authenticate: fn });
|
|
1502
|
+
|
|
1503
|
+
// src/index.ts
|
|
1504
|
+
var VERSION = "0.0.0";
|
|
1505
|
+
export {
|
|
1506
|
+
ASK_TOOL_NAME,
|
|
1507
|
+
CapturingExporter,
|
|
1508
|
+
CostGovernor,
|
|
1509
|
+
DelegateBudgets,
|
|
1510
|
+
GUARDRAILS,
|
|
1511
|
+
InMemoryContainerFloor,
|
|
1512
|
+
InMemoryStorage,
|
|
1513
|
+
PRICE_TABLE,
|
|
1514
|
+
RowCache,
|
|
1515
|
+
SCRATCHPAD_MAX_ENTRY_CHARS,
|
|
1516
|
+
SCRATCHPAD_MAX_KEYS,
|
|
1517
|
+
SpanCollector,
|
|
1518
|
+
SwarmEngine,
|
|
1519
|
+
VERSION,
|
|
1520
|
+
allowListModelProvider,
|
|
1521
|
+
buildSkillResolver,
|
|
1522
|
+
composeSystemPrompt,
|
|
1523
|
+
compositeTelemetry,
|
|
1524
|
+
containerFloor,
|
|
1525
|
+
customAuth,
|
|
1526
|
+
customTelemetry,
|
|
1527
|
+
defineAgent,
|
|
1528
|
+
defineSkill,
|
|
1529
|
+
defineSwarm,
|
|
1530
|
+
defineTool,
|
|
1531
|
+
ev,
|
|
1532
|
+
isEvent,
|
|
1533
|
+
resolveTelemetry
|
|
1534
|
+
};
|