@dbx-tools/appkit-mastra 0.1.112 → 0.3.1

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/src/agents.ts ADDED
@@ -0,0 +1,858 @@
1
+ /**
2
+ * Agent registration for the Mastra AppKit plugin.
3
+ *
4
+ * Mirrors the shape of the AppKit `agents` plugin (`config.agents` map
5
+ * of {@link MastraAgentDefinition}, dual-form `tools` accepting a plain
6
+ * record or a `(plugins) => tools` callback). Resolves each definition
7
+ * into a Mastra `Agent` instance during plugin setup; user-supplied
8
+ * tool callbacks are invoked exactly once with a typed
9
+ * {@link MastraPlugins} map built from registered sibling plugins.
10
+ *
11
+ * When no agents are registered the plugin falls back to a single
12
+ * built-in analyst so the bare `mastra()` call still mounts a working
13
+ * streamable agent for demos.
14
+ */
15
+
16
+ import type { AgentConfig, ToolsInput } from "@mastra/core/agent";
17
+ import { Agent } from "@mastra/core/agent";
18
+ import type { Tool } from "@mastra/core/tools";
19
+ import { createTool } from "@mastra/core/tools";
20
+ import type { Workspace } from "@mastra/core/workspace";
21
+ import type { PgVectorConfig, PostgresStoreConfig } from "@mastra/pg";
22
+
23
+ import { buildRenderDataTool } from "./chart";
24
+ import type { MastraPluginConfig } from "./config";
25
+ import { buildGenieToolkitProvider, resolveGenieSpaces } from "./genie";
26
+ import type { MemoryBuilder } from "./memory";
27
+ import { buildModel } from "./model";
28
+ import { fallback } from "@dbx-tools/model";
29
+ import { ResultProcessor, stripStaleChartsProcessor } from "./processors";
30
+ import { buildSummarizeTool } from "./summarize";
31
+ import { createWorkspace } from "./workspaces";
32
+ import { log, object, string } from "@dbx-tools/shared-core";
33
+ import { plugin } from "@dbx-tools/appkit";
34
+
35
+ /**
36
+ * Tool record accepted by every Mastra `Agent.tools` field and by the
37
+ * `tools(plugins)` callback on {@link MastraAgentDefinition}.
38
+ *
39
+ * Alias of Mastra's `ToolsInput`, so it already accepts:
40
+ *
41
+ * - Mastra tools built with {@link createTool} (or `new Tool(...)`)
42
+ * - Mastra tools built with the AppKit-shaped {@link tool} wrapper
43
+ * below
44
+ * - Vercel AI SDK tools (`tool({ ... })` from `ai`)
45
+ * - Provider-defined tools (e.g. `openai.tools.webSearch(...)`)
46
+ *
47
+ * Existing tool libraries drop in as-is - nothing in this package
48
+ * forces a rebuild.
49
+ */
50
+ export type MastraTools = ToolsInput;
51
+
52
+ /** Re-export of Mastra's native `createTool` for full-feature access. */
53
+ export { createTool } from "@mastra/core/tools";
54
+
55
+ /**
56
+ * AppKit-shaped tool factory. Lets users mix-and-match tools across
57
+ * AppKit's `agents` plugin and `mastra` with a single import:
58
+ *
59
+ * ```ts
60
+ * import { tool } from "@dbx-tools/appkit-mastra";
61
+ * import { z } from "zod";
62
+ *
63
+ * get_weather: tool({
64
+ * description: "Weather",
65
+ * schema: z.object({ city: z.string() }),
66
+ * execute: async ({ city }) => `Sunny in ${city}`,
67
+ * }),
68
+ * ```
69
+ *
70
+ * Maps onto Mastra's `createTool`:
71
+ *
72
+ * - `description` -> `description` (required)
73
+ * - `schema` -> `inputSchema` (optional)
74
+ * - `execute(input)` -> `execute(input, ctx)` - Mastra already calls
75
+ * the first arg with the parsed inputs, so the body shape is
76
+ * identical. The Mastra `context` arg is forwarded as the second
77
+ * parameter when the caller declares it.
78
+ * - `id`: optional. Defaults to a stable identifier derived from
79
+ * `description` (slugified, with a short hash suffix for
80
+ * uniqueness). Pass an explicit `id` when you need a stable string
81
+ * for tracing or MCP exposure.
82
+ *
83
+ * Reach for {@link createTool} when you need Mastra-only fields
84
+ * (`outputSchema`, `suspendSchema`, `requireApproval`, `mcp`, etc.).
85
+ */
86
+ export function tool(opts: AppKitToolOptions): Tool {
87
+ const id = opts.id ?? deriveToolId(opts.description);
88
+ return createTool({
89
+ id,
90
+ description: opts.description,
91
+ ...(opts.schema ? { inputSchema: opts.schema as never } : {}),
92
+ execute: opts.execute as never,
93
+ });
94
+ }
95
+
96
+ /**
97
+ * Input shape for the AppKit-style {@link tool} factory. A trimmed
98
+ * subset of Mastra's `createTool` options that mirrors the
99
+ * `@databricks/appkit/beta` `tool({ description, schema, execute })`
100
+ * signature.
101
+ *
102
+ * Generics are intentionally absent - inference flows through the
103
+ * caller's `schema` (typically a Zod object), and the `execute` body
104
+ * destructures naturally from that. Reach for {@link createTool} when
105
+ * you need the fully-typed input/output schemas wired explicitly.
106
+ */
107
+ export interface AppKitToolOptions {
108
+ /** Optional stable identifier; auto-derived from `description` when omitted. */
109
+ id?: string;
110
+ /** Human-readable description shown to the model. Required. */
111
+ description: string;
112
+ /**
113
+ * Optional input schema (any Standard Schema instance, e.g. Zod).
114
+ * Maps to Mastra's `inputSchema`; passed through to the model
115
+ * verbatim.
116
+ */
117
+ schema?: unknown;
118
+ /**
119
+ * Execute body. First arg is the parsed input (typed off `schema`
120
+ * when supplied), second arg is the full Mastra execution context
121
+ * (request context, abort signal, mastra instance) if you need it.
122
+ */
123
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
124
+ execute: (input: any, context?: unknown) => unknown;
125
+ }
126
+
127
+ /**
128
+ * Build a deterministic Mastra tool id from a description.
129
+ * Delegates to {@link string.toUniqueSlug}: slug + always-on
130
+ * 6-char FNV-1a base-32 suffix so two tools with the same leading
131
+ * words don't collide in traces. Stable across runs.
132
+ */
133
+ function deriveToolId(description: string): string {
134
+ return string.toUniqueSlug(description, { fallbackPrefix: "tool" });
135
+ }
136
+
137
+ /**
138
+ * Identity helper that brands a definition as a Mastra agent. Mirrors
139
+ * AppKit's `createAgent(def)` so the registration shape matches:
140
+ *
141
+ * ```ts
142
+ * const support = createAgent({
143
+ * instructions: "...",
144
+ * model: "databricks-claude-sonnet-4-6",
145
+ * tools(plugins) { return { ... }; },
146
+ * });
147
+ * ```
148
+ *
149
+ * Returns the definition unchanged - the wrapper exists only to anchor
150
+ * type inference and to match the AppKit API surface.
151
+ */
152
+ export function createAgent<T extends MastraAgentDefinition>(def: T): T {
153
+ const workspace = def.workspace ?? createWorkspace();
154
+ return { ...def, workspace };
155
+ }
156
+
157
+ /**
158
+ * Filter / rename options accepted by every plugin's `.toolkit()`
159
+ * method. Mirrors AppKit's `ToolkitOptions` verbatim so options pass
160
+ * through unchanged - the underlying AppKit plugin does the filtering
161
+ * and we just adapt the resulting entries into Mastra tools.
162
+ */
163
+ export interface ToolkitOptions {
164
+ /**
165
+ * Key prefix prepended to every tool name. AppKit's default is
166
+ * `${pluginName}.` when omitted; pass an explicit `""` to drop it.
167
+ */
168
+ prefix?: string;
169
+ /** Allowlist of local tool names. */
170
+ only?: string[];
171
+ /** Denylist of local tool names. */
172
+ except?: string[];
173
+ /** Remap specific local names to different keys. */
174
+ rename?: Record<string, string>;
175
+ }
176
+
177
+ /**
178
+ * Toolkit provider shape every entry in the {@link MastraPlugins} map
179
+ * exposes. Identical to AppKit's `PluginToolkitProvider` - any AppKit
180
+ * plugin that implements the standard `ToolProvider` interface
181
+ * (`getAgentTools` + `executeAgentTool` + `toolkit`) is reachable
182
+ * through this surface automatically.
183
+ */
184
+ export interface MastraPluginToolkitProvider {
185
+ /**
186
+ * Returns a Mastra-shaped tools record adapted from the plugin's
187
+ * agent tools. Each tool dispatches back through the plugin's
188
+ * `executeAgentTool` so OBO auth and telemetry spans stay intact.
189
+ */
190
+ toolkit(opts?: ToolkitOptions): MastraTools;
191
+ }
192
+
193
+ /**
194
+ * Plugin map handed to the function form of
195
+ * {@link MastraAgentDefinition.tools}. Mirrors AppKit's `Plugins`
196
+ * type exactly: a string-keyed record where every value exposes
197
+ * `.toolkit(opts)`.
198
+ *
199
+ * Implemented as a runtime Proxy that auto-discovers any registered
200
+ * AppKit plugin implementing the standard `ToolProvider` interface
201
+ * (`analytics`, `files`, `lakebase`, `genie`, plus any third-party
202
+ * plugin that does the same). Unknown names resolve to `undefined`
203
+ * at runtime, so guard with `?.` and `?? {}` when spreading from a
204
+ * plugin that may not be registered in every environment.
205
+ *
206
+ * @example
207
+ * ```ts
208
+ * createAgent({
209
+ * instructions: "...",
210
+ * tools(plugins) {
211
+ * return {
212
+ * ...plugins.analytics.toolkit(),
213
+ * ...plugins.files.toolkit({ only: ["uploads.read"] }),
214
+ * get_weather: tool({
215
+ * description: "Weather",
216
+ * schema: z.object({ city: z.string() }),
217
+ * execute: async ({ city }) => `Sunny in ${city}`,
218
+ * }),
219
+ * };
220
+ * },
221
+ * });
222
+ * ```
223
+ */
224
+ export type MastraPlugins = Record<string, MastraPluginToolkitProvider>;
225
+
226
+ /** Function form of {@link MastraAgentDefinition.tools}. */
227
+ export type MastraToolsFn = (plugins: MastraPlugins) => MastraTools | Promise<MastraTools>;
228
+
229
+ /** Function form of {@link MastraAgentDefinition.workspace}. */
230
+ export type MastraAgentWorkspaceResolver = () => Workspace | undefined;
231
+
232
+ /**
233
+ * A code-defined Mastra agent. Mirrors the shape AppKit's `agents`
234
+ * plugin uses for `AgentDefinition`. The registry key under
235
+ * `config.agents` is the `agentId` the client streams against; `name`
236
+ * is purely informational (defaults to the key).
237
+ */
238
+ export interface MastraAgentDefinition {
239
+ /** Display name used as `Agent.name`. Defaults to the registry key. */
240
+ name?: string;
241
+ /** Optional long-form description; surfaced as `Agent.description`. */
242
+ description?: string;
243
+ /** System prompt body. */
244
+ instructions: string;
245
+ /**
246
+ * Per-agent model override.
247
+ *
248
+ * - `undefined` (default): falls back to the workspace
249
+ * `/serving-endpoints` resolver that {@link buildModel} configures
250
+ * from the per-request `WorkspaceClient`.
251
+ * - `string`: shorthand for "use the default resolver but swap the
252
+ * `modelId`" (e.g. `"databricks-meta-llama-3-3-70b-instruct"`).
253
+ * - Any other Mastra `DynamicArgument<MastraModelConfig>`: passed
254
+ * straight through to `Agent.model`. Use this when you need full
255
+ * control over auth or providerId.
256
+ */
257
+ model?: AgentConfig["model"] | string;
258
+ /**
259
+ * Per-agent tool record. Either a plain map or a callback that
260
+ * receives the typed {@link MastraPlugins} sibling-plugin index and
261
+ * returns a map. The callback runs exactly once at agent setup; the
262
+ * result is cached for the agent's lifetime.
263
+ */
264
+ tools?: MastraTools | MastraToolsFn;
265
+ /**
266
+ * Per-agent semantic recall (PgVector) override. Cascades from
267
+ * `config.memory`; the agent value wins when set.
268
+ *
269
+ * - `undefined` (default): inherit `config.memory`. When that's
270
+ * enabled, the agent **shares the plugin-level singleton `PgVector`
271
+ * instance** (cross-agent semantic recall across the same index).
272
+ * - `false`: disable semantic recall for this agent only.
273
+ * - `true`: enable using the shared singleton (same as default when
274
+ * plugin memory is enabled; useful to opt in when plugin disabled).
275
+ * - {@link MastraMemoryConfig} object: dedicated `PgVector` for this
276
+ * agent (private recall index). Bypasses the shared singleton.
277
+ */
278
+ memory?: boolean | MastraMemoryConfigOverride;
279
+ /**
280
+ * Per-agent thread/message storage (`PostgresStore`) override.
281
+ * Cascades from `config.storage`; the agent value wins when set.
282
+ *
283
+ * - `undefined` (default): inherit `config.storage`. When that's
284
+ * enabled, the agent gets its **own per-agent `PostgresStore`**
285
+ * keyed by {@link agentStorageSchemaName} so threads and
286
+ * messages stay isolated between agents in the same database.
287
+ * - `false`: disable storage for this agent only (purely in-memory).
288
+ * - `true`: enable with the per-agent default schema.
289
+ * - {@link MastraStorageConfigOverride} object: dedicated
290
+ * `PostgresStore` config (custom schema, connection, etc.).
291
+ */
292
+ storage?: boolean | MastraStorageConfigOverride;
293
+ /**
294
+ * Mastra {@link Workspace} for this agent (filesystem, sandbox, or other
295
+ * providers). Assistant skills from Databricks workspace paths are wired
296
+ * via {@link createWorkspace}.
297
+ */
298
+ workspace?: Workspace | MastraAgentWorkspaceResolver;
299
+ }
300
+
301
+ /**
302
+ * Distributive `Omit` so unions in `PostgresStoreConfig` /
303
+ * `PgVectorConfig` keep their discriminants after the override types
304
+ * strip `id`. The built-in `Omit` collapses unions to one shape with
305
+ * common fields only, which loses the connection-style discriminants.
306
+ */
307
+ type DistributiveOmit<T, K extends keyof never> = T extends unknown ? Omit<T, K> : never;
308
+
309
+ /**
310
+ * `PostgresStoreConfig` minus `id` - per-agent overrides accept any
311
+ * Mastra-supported storage shape. `id` is filled in automatically
312
+ * from the agent registry key so traces stay stable.
313
+ */
314
+ export type MastraStorageConfigOverride = DistributiveOmit<PostgresStoreConfig, "id"> & {
315
+ id?: string;
316
+ };
317
+
318
+ /**
319
+ * `PgVectorConfig` minus `id` - per-agent overrides accept any
320
+ * Mastra-supported vector shape. `id` is filled in automatically
321
+ * from the agent registry key.
322
+ */
323
+ export type MastraMemoryConfigOverride = DistributiveOmit<PgVectorConfig, "id"> & {
324
+ id?: string;
325
+ };
326
+
327
+ /** Output of {@link buildAgents}: resolved agents plus the default id. */
328
+ export interface BuiltAgents {
329
+ agents: Record<string, Agent>;
330
+ defaultAgentId: string;
331
+ /**
332
+ * Ambient tools shared across every agent (the built-in system tools
333
+ * spread with `config.tools`). Surfaced so the optional MCP server
334
+ * can re-expose them when {@link MastraMcpConfig.tools} is enabled.
335
+ */
336
+ ambientTools: MastraTools;
337
+ }
338
+
339
+ /** Fallback agent id used when `config.agents` is omitted entirely. */
340
+ export const FALLBACK_AGENT_ID = "default";
341
+
342
+ /**
343
+ * Default per-turn step ceiling applied to every registered agent
344
+ * when {@link MastraPluginConfig.agentMaxSteps} is unset. Sized to
345
+ * fit a decomposed Genie turn (grounding + several `ask_genie`
346
+ * calls + `prepare_chart` per dataset + the final-text reply) with
347
+ * headroom for the model to chain a couple of follow-ups before
348
+ * answering - well above Mastra's own `agent.generate` default of
349
+ * 5, which would cut multi-step orchestration off mid-loop.
350
+ */
351
+ export const DEFAULT_AGENT_MAX_STEPS = 25;
352
+
353
+ const FALLBACK_AGENT_INSTRUCTIONS = `You are a data analyst. The user will ask questions about
354
+ business metrics and may share personal preferences you should remember across turns.
355
+
356
+ Rules:
357
+
358
+ 1. Quote numbers exactly. Never invent data.
359
+ 2. When the user states a preference or durable fact about themselves
360
+ ("I'm in EU so use EUR", "always show me the SQL"), acknowledge that
361
+ you will remember it.
362
+ 3. If you don't have enough information to answer, ask a clarifying
363
+ question instead of guessing.`;
364
+
365
+ /**
366
+ * Style guardrails appended to every agent's `instructions` to curb
367
+ * common LLM-isms (em dashes, emojis, sycophantic openers, excessive
368
+ * hedging, throwaway closers). Appended rather than prepended so the
369
+ * agent's role/context comes first; the model's recency bias then
370
+ * helps the style rules dominate the response surface.
371
+ *
372
+ * Override globally via {@link MastraPluginConfig.styleInstructions}
373
+ * (pass `false` to disable entirely, or a string to replace).
374
+ */
375
+ export const DEFAULT_STYLE_INSTRUCTIONS = [
376
+ "Output style:",
377
+ "",
378
+ "Use markdown formatting, including headings, lists, and code blocks.",
379
+ "Avoid lists and headers for short replies.",
380
+ "Plain prose.",
381
+ "Use hyphens (-) only. Never use em dashes or en dashes.",
382
+ "Never use emojis.",
383
+ "Skip openers like 'Great question', 'Absolutely', and 'I'd be happy to help'.",
384
+ "Skip closers like 'Let me know if you have any questions'.",
385
+ "Skip self-disclaimers like 'I should mention' and 'It's important to note'.",
386
+ "Answer directly.",
387
+ "Do not include a preamble before the actual answer.",
388
+ "Use lists and headers only when they clarify a multi-part answer.",
389
+ ].join("\n");
390
+
391
+ /**
392
+ * Resolve the style block to append to every agent's instructions.
393
+ * Returns `null` when the caller opted out (`styleInstructions: false`).
394
+ */
395
+ function resolveStyleInstructions(config: MastraPluginConfig): string | null {
396
+ if (config.styleInstructions === false) return null;
397
+ if (typeof config.styleInstructions === "string") {
398
+ return config.styleInstructions;
399
+ }
400
+
401
+ return DEFAULT_STYLE_INSTRUCTIONS;
402
+ }
403
+
404
+ /**
405
+ * Join an agent's bespoke instructions with the resolved style block.
406
+ * Returns the bespoke text unchanged when the style block is disabled.
407
+ */
408
+ function composeInstructions(agentInstructions: string, style: string | null): string {
409
+ if (!style) return agentInstructions;
410
+ return `${agentInstructions.trimEnd()}\n\n${style}`;
411
+ }
412
+
413
+ /**
414
+ * Generated description for an agent whose definition omits one. Kept
415
+ * generic (the agent's own name is the only reliable signal at build
416
+ * time) so it reads sensibly as the `ask_<id>` MCP tool's description
417
+ * and in agent metadata.
418
+ */
419
+ function defaultAgentDescription(name: string): string {
420
+ return `Conversational AI agent "${name}".`;
421
+ }
422
+
423
+ /**
424
+ * Resolve every entry in `config.agents` into a Mastra `Agent`
425
+ * instance. When `config.agents` is omitted the plugin registers a
426
+ * single built-in `default` analyst so the bare `mastra()` call still
427
+ * yields a working agent.
428
+ *
429
+ * Per-agent tool callbacks are invoked once with a typed
430
+ * {@link MastraPlugins} index built from registered sibling plugins
431
+ * (currently `genie`; extend `MastraPlugins` to surface more).
432
+ *
433
+ * @throws when `config.defaultAgent` is set to an id that isn't in the
434
+ * resolved registry; this is a wiring bug, not a runtime condition.
435
+ */
436
+ export async function buildAgents(opts: {
437
+ config: MastraPluginConfig;
438
+ context: plugin.PluginContextLike | undefined;
439
+ memoryBuilder?: MemoryBuilder;
440
+ log: log.Logger;
441
+ }): Promise<BuiltAgents> {
442
+ const { config, context, memoryBuilder, log } = opts;
443
+ const definitions = resolveDefinitions(config);
444
+ const ids = Object.keys(definitions);
445
+ const defaultAgentId = config.defaultAgent ?? ids[0] ?? FALLBACK_AGENT_ID;
446
+
447
+ const plugins = buildPluginsMap(config, context);
448
+ // System-default ambient tools every agent gets out of the box:
449
+ // `render_data` for inline visualizations and `summarize` for
450
+ // offloading text condensing to the fast / small chat tier. The
451
+ // user can shadow either by including a same-named tool in their own
452
+ // `config.tools` or per-agent `tools`. Order in {@link resolveTools}
453
+ // is `system -> user-ambient -> per-agent`, last write wins.
454
+ const systemTools: MastraTools = {
455
+ render_data: buildRenderDataTool(config),
456
+ summarize: buildSummarizeTool(config),
457
+ };
458
+ const ambientTools = {
459
+ ...systemTools,
460
+ ...(config.tools ?? {}),
461
+ };
462
+ const style = resolveStyleInstructions(config);
463
+ // Default-on protection against the model copying turn-scoped
464
+ // chartIds from prior assistant tool results into the new
465
+ // turn's `[chart:<id>]` markers. Opt out per-plugin via
466
+ // `config.stripStaleCharts: false`.
467
+ const outputProcessors = [new ResultProcessor()];
468
+ const inputProcessors = [
469
+ ...(config.stripStaleCharts === false ? [] : [stripStaleChartsProcessor]),
470
+ ];
471
+ const agents: Record<string, Agent> = {};
472
+ const approvalGatedByAgent: Array<{ agentId: string; toolIds: string[] }> = [];
473
+
474
+ for (const [id, def] of Object.entries(definitions)) {
475
+ const tools = await resolveTools(def.tools, plugins, ambientTools);
476
+ const workspace = resolveAgentWorkspace(def.workspace);
477
+ const gated = approvalGatedToolIds(tools);
478
+ if (gated.length > 0) approvalGatedByAgent.push({ agentId: id, toolIds: gated });
479
+ const memory = memoryBuilder?.forAgent(id, def);
480
+ agents[id] = new Agent({
481
+ id,
482
+ name: def.name ?? id,
483
+ // Always carry a non-empty description: it's surfaced to MCP
484
+ // clients (which reject a description-less agent) and in agent
485
+ // metadata. Fall back to a generated one when the definition
486
+ // omits it.
487
+ description: def.description?.trim() || defaultAgentDescription(def.name ?? id),
488
+ instructions: composeInstructions(def.instructions, style),
489
+ model: resolveModel(config, def.model),
490
+ defaultOptions: {
491
+ maxSteps: config.agentMaxSteps ?? DEFAULT_AGENT_MAX_STEPS,
492
+ },
493
+ tools,
494
+ ...(memory ? { memory } : {}),
495
+ ...(workspace ? { workspace } : {}),
496
+ inputProcessors,
497
+ outputProcessors,
498
+ });
499
+ // Surface the effective default model per agent so operators can
500
+ // see at a glance which endpoint each agent points at without
501
+ // having to fire a request and inspect a trace. The value is the
502
+ // *static* default; per-request overrides (header / query /
503
+ // body) and the workspace-catalogue fuzzy match still apply at
504
+ // call time.
505
+ log.info("agent registered", {
506
+ id,
507
+ name: def.name ?? id,
508
+ defaultModel: describeAgentDefaultModel(config, def),
509
+ tools: Object.keys(tools),
510
+ });
511
+ }
512
+
513
+ if (!agents[defaultAgentId]) {
514
+ throw new Error(
515
+ `mastra: defaultAgent "${defaultAgentId}" not found in registered agents (${ids.join(", ") || "none"})`,
516
+ );
517
+ }
518
+
519
+ assertApprovalGatedToolsHaveStorage(approvalGatedByAgent, memoryBuilder);
520
+
521
+ log.info("agents ready", { ids, defaultAgentId });
522
+ return { agents, defaultAgentId, ambientTools };
523
+ }
524
+
525
+ /**
526
+ * Tool ids on `tools` that are approval-gated (`requireApproval: true`).
527
+ * Keys are used as a fallback when a tool omits an explicit `id`.
528
+ */
529
+ export function approvalGatedToolIds(tools: MastraTools): string[] {
530
+ if (!tools || typeof tools !== "object") return [];
531
+ return [
532
+ ...object
533
+ .sequence(Object.entries(tools))
534
+ .filter(([, tool]) => isApprovalGatedTool(tool))
535
+ .map(([key, tool]) => resolveToolId(tool, key)),
536
+ ];
537
+ }
538
+
539
+ /** True when a Mastra / AI SDK tool pauses for human approval before execute. */
540
+ function isApprovalGatedTool(tool: unknown): boolean {
541
+ if (!tool || typeof tool !== "object") return false;
542
+ return (tool as { requireApproval?: boolean }).requireApproval === true;
543
+ }
544
+
545
+ function resolveToolId(tool: unknown, fallbackKey: string): string {
546
+ if (tool && typeof tool === "object" && typeof (tool as { id?: string }).id === "string") {
547
+ return (tool as { id: string }).id;
548
+ }
549
+ return fallbackKey;
550
+ }
551
+
552
+ /**
553
+ * Approval-gated tools suspend the agent loop until a human approves.
554
+ * Mastra persists those suspended runs in Mastra-instance-level storage
555
+ * (`memoryBuilder.instanceStorage()`), not per-agent memory - so boot
556
+ * must fail fast when such a tool is registered without storage.
557
+ */
558
+ function assertApprovalGatedToolsHaveStorage(
559
+ gated: Array<{ agentId: string; toolIds: string[] }>,
560
+ memoryBuilder: MemoryBuilder | undefined,
561
+ ): void {
562
+ if (gated.length === 0) return;
563
+ if (memoryBuilder?.instanceStorage()) return;
564
+
565
+ const detail = gated
566
+ .map(({ agentId, toolIds }) => `${agentId}: ${toolIds.join(", ")}`)
567
+ .join("; ");
568
+ throw new Error(
569
+ "mastra: approval-gated tools require plugin storage (PostgresStore) to persist suspended runs. " +
570
+ `Affected agents/tools: ${detail}. ` +
571
+ "Register lakebase() before mastra() so storage auto-enables, or pass storage: true explicitly.",
572
+ );
573
+ }
574
+
575
+ /**
576
+ * Best-effort description of the *static* default model an agent will
577
+ * resolve to at call time. Walks the same precedence ladder as
578
+ * {@link resolveModel} / {@link buildModel}:
579
+ *
580
+ * 1. Per-agent `def.model` (string sugar -> the literal id;
581
+ * function / `DynamicArgument` -> `"<dynamic>"` because the
582
+ * resolver decides at call time).
583
+ * 2. Plugin-level `config.defaultModel` (same rules).
584
+ * 3. `DATABRICKS_SERVING_ENDPOINT_NAME` env var.
585
+ * 4. First entry of `config.defaultModelFallbacks ?? fallback.FALLBACK_MODEL_IDS`.
586
+ *
587
+ * Used for the startup `agent registered` log so operators can see
588
+ * which endpoint each agent points at by default. Per-request
589
+ * overrides (`X-Mastra-Model` etc.) and the workspace-catalogue
590
+ * fuzzy match are still applied at runtime.
591
+ */
592
+ function describeAgentDefaultModel(config: MastraPluginConfig, def: MastraAgentDefinition): string {
593
+ const effective = def.model ?? config.defaultModel;
594
+ if (typeof effective === "string") return effective;
595
+ if (effective !== undefined) return "<dynamic>";
596
+ return (
597
+ process.env.DATABRICKS_SERVING_ENDPOINT_NAME ??
598
+ config.defaultModelFallbacks?.[0] ??
599
+ fallback.FALLBACK_MODEL_IDS[0]!
600
+ );
601
+ }
602
+
603
+ /**
604
+ * Normalize `config.agents` into a `Record<id, definition>`. Accepts
605
+ * any of the three shapes documented on
606
+ * {@link MastraPluginConfig.agents}:
607
+ *
608
+ * - Record - returned as-is when non-empty.
609
+ * - Single definition (detected via the required `instructions`
610
+ * field) - keyed by `slugify(def.name)` or `FALLBACK_AGENT_ID`.
611
+ * - Array - keyed by `slugify(def.name)` or `agent_${i}`; duplicate
612
+ * slugs fail loudly so users know to set explicit names.
613
+ *
614
+ * Omitted or empty inputs fall back to a single built-in analyst so
615
+ * the bare `mastra()` call still mounts a working chat route.
616
+ */
617
+ function resolveDefinitions(config: MastraPluginConfig): Record<string, MastraAgentDefinition> {
618
+ const input = config.agents;
619
+ if (!input) return fallbackDefinitions();
620
+
621
+ if (Array.isArray(input)) {
622
+ if (input.length === 0) return fallbackDefinitions();
623
+ const out: Record<string, MastraAgentDefinition> = {};
624
+ input.forEach((def, i) => {
625
+ const key = deriveAgentKey(def, i);
626
+ if (out[key]) {
627
+ throw new Error(
628
+ `mastra: duplicate agent id "${key}" derived from name "${def.name ?? ""}"; ` +
629
+ `set unique \`name\`s on each definition`,
630
+ );
631
+ }
632
+ out[key] = def;
633
+ });
634
+ return out;
635
+ }
636
+
637
+ // Single-definition shorthand: an agent always has `instructions: string`,
638
+ // a record-of-agents never has that field directly.
639
+ if (typeof (input as MastraAgentDefinition).instructions === "string") {
640
+ const def = input as MastraAgentDefinition;
641
+ const key = deriveAgentKey(def);
642
+ return { [key]: def };
643
+ }
644
+
645
+ const record = input as Record<string, MastraAgentDefinition>;
646
+ if (Object.keys(record).length === 0) return fallbackDefinitions();
647
+ return record;
648
+ }
649
+
650
+ /** Derive a registry id from a definition's `name`, with a fallback. */
651
+ function deriveAgentKey(def: MastraAgentDefinition, index?: number): string {
652
+ if (def.name) {
653
+ const slug = string.toIdentifier(def.name);
654
+ if (slug) return slug;
655
+ }
656
+ return index === undefined ? FALLBACK_AGENT_ID : `agent_${index}`;
657
+ }
658
+
659
+ /** Built-in fallback registry used when `agents` is omitted / empty. */
660
+ function fallbackDefinitions(): Record<string, MastraAgentDefinition> {
661
+ return {
662
+ [FALLBACK_AGENT_ID]: {
663
+ name: "Default Agent",
664
+ instructions: FALLBACK_AGENT_INSTRUCTIONS,
665
+ },
666
+ };
667
+ }
668
+
669
+ /**
670
+ * Pick the effective model spec for an agent. Fallback ladder, in
671
+ * order:
672
+ *
673
+ * 1. Per-agent `def.model` (string sugar or `DynamicArgument`).
674
+ * 2. Plugin-level `config.defaultModel` (string sugar or
675
+ * `DynamicArgument`) - mirrors AppKit's `agents({ defaultModel })`.
676
+ * 3. The auto-resolver that mints user-scoped tokens against
677
+ * `/serving-endpoints` via {@link buildModel}.
678
+ *
679
+ * String values are treated as `modelId` sugar and threaded through
680
+ * `buildModel`'s override hook so the runtime fuzzy matcher and the
681
+ * per-request `X-Mastra-Model` override layer on top of the static
682
+ * choice. Non-string `DynamicArgument`s are passed through verbatim;
683
+ * callers that need full control over `providerId` / `headers` /
684
+ * `modelId` bypass the resolver pipeline entirely.
685
+ */
686
+ function resolveModel(
687
+ config: MastraPluginConfig,
688
+ override: MastraAgentDefinition["model"],
689
+ ): AgentConfig["model"] {
690
+ const effective = override ?? config.defaultModel;
691
+ if (effective === undefined) {
692
+ return ({ requestContext }) => buildModel(config, requestContext);
693
+ }
694
+ if (typeof effective === "string") {
695
+ const modelId = effective;
696
+ return ({ requestContext }) => buildModel(config, requestContext, { modelId });
697
+ }
698
+ return effective;
699
+ }
700
+
701
+ /**
702
+ * Resolve a definition's `tools` field to a flat `MastraTools` record,
703
+ * merging in plugin-level ambient tools (per-agent tools win on key
704
+ * collision). Callback errors propagate verbatim so the original stack
705
+ * survives - the caller already knows which agent was registering.
706
+ */
707
+ async function resolveTools(
708
+ defTools: MastraAgentDefinition["tools"],
709
+ plugins: MastraPlugins,
710
+ ambientTools: MastraTools,
711
+ ): Promise<MastraTools> {
712
+ if (!defTools) return { ...ambientTools };
713
+ const resolved = typeof defTools === "function" ? await defTools(plugins) : defTools;
714
+ return { ...ambientTools, ...resolved };
715
+ }
716
+
717
+ function resolveAgentWorkspace(
718
+ workspace: MastraAgentDefinition["workspace"],
719
+ ): Workspace | undefined {
720
+ if (!workspace) return undefined;
721
+ return typeof workspace === "function" ? workspace() : workspace;
722
+ }
723
+
724
+ /**
725
+ * Build the {@link MastraPlugins} runtime proxy handed to
726
+ * `tools(plugins)` callbacks.
727
+ *
728
+ * Implemented as a `Proxy` over the AppKit plugin context so
729
+ * `plugins.<name>` resolves at first access. Any sibling plugin that
730
+ * implements AppKit's standard `ToolProvider` interface
731
+ * (`toolkit(opts?)` + `executeAgentTool(name, args, signal?)`) is
732
+ * auto-adapted into Mastra tools. Unknown names return `undefined`,
733
+ * matching AppKit's `Plugins` semantics so `plugins.foo?.toolkit()`
734
+ * remains safe in environments where `foo` isn't registered.
735
+ *
736
+ * `genie` is special-cased to swap the generic AppKit toolkit (which
737
+ * runs `executeAgentTool` and only emits a single final `tool-result`
738
+ * chunk per call) for the streaming-aware tools built by
739
+ * {@link buildGenieProvider}. The streaming variant forwards each
740
+ * Genie wire event (status, SQL, row counts, errors) out through the
741
+ * Mastra `ctx.writer`, so the UI gets `tool-output` chunks in real
742
+ * time instead of staring at a spinner for the full Genie round-trip.
743
+ */
744
+ function buildPluginsMap(
745
+ config: MastraPluginConfig,
746
+ context: plugin.PluginContextLike | undefined,
747
+ ): MastraPlugins {
748
+ const cache = new Map<string, MastraPluginToolkitProvider | null>();
749
+ return new Proxy({} as MastraPlugins, {
750
+ get(_target, propName) {
751
+ if (typeof propName !== "string") return undefined;
752
+ if (cache.has(propName)) return cache.get(propName) ?? undefined;
753
+ const provider = resolveProvider(config, context, propName);
754
+ cache.set(propName, provider);
755
+ return provider ?? undefined;
756
+ },
757
+ });
758
+ }
759
+
760
+ /**
761
+ * Pick the right {@link MastraPluginToolkitProvider} for a sibling
762
+ * plugin lookup. Returns the Genie agent-backed adapter when
763
+ * the caller asks for `genie` AND at least one space is reachable
764
+ * via {@link resolveGenieSpaces} (the explicit
765
+ * `config.genieSpaces`, the registered AppKit `genie()` plugin's
766
+ * `spaces` config, or the `DATABRICKS_GENIE_SPACE_ID` env var).
767
+ * Falls back to the generic AppKit `ToolProvider` adapter for
768
+ * every other plugin name. `config` is threaded through so the
769
+ * Genie agent inherits the same model resolver / fallback
770
+ * ladder the calling agents use.
771
+ *
772
+ * The Genie agent talks to Genie directly via `@dbx-tools/genie`
773
+ * (`genieEventChat`) and the workspace
774
+ * `statementExecution.getStatement` API. AppKit's stock `genie`
775
+ * plugin is honored only for its resource manifest and `spaces`
776
+ * config so existing `app.yaml` configs and `genie({ spaces })`
777
+ * wiring keep working without change.
778
+ */
779
+ function resolveProvider(
780
+ config: MastraPluginConfig,
781
+ context: plugin.PluginContextLike | undefined,
782
+ propName: string,
783
+ ): MastraPluginToolkitProvider | null {
784
+ if (propName === "genie") {
785
+ const spaces = resolveGenieSpaces(config, context);
786
+ if (Object.keys(spaces).length === 0) return null;
787
+ return buildGenieToolkitProvider({
788
+ spaces,
789
+ config,
790
+ }) as MastraPluginToolkitProvider;
791
+ }
792
+ const plugin = context?.getPlugins().get(propName);
793
+ return adaptPluginToolkit(plugin);
794
+ }
795
+
796
+ /**
797
+ * AppKit `ToolProvider` shape we duck-type against any registered
798
+ * plugin. Defined structurally to avoid coupling to AppKit's internal
799
+ * type module layout.
800
+ */
801
+ interface AppKitToolkitProvider {
802
+ toolkit?: (opts?: ToolkitOptions) => Record<string, AppKitToolkitEntry>;
803
+ executeAgentTool?: (name: string, args: unknown, signal?: AbortSignal) => Promise<unknown>;
804
+ }
805
+
806
+ /** Single entry returned by an AppKit plugin's `.toolkit(opts)` call. */
807
+ interface AppKitToolkitEntry {
808
+ pluginName: string;
809
+ localName: string;
810
+ def: {
811
+ name: string;
812
+ description: string;
813
+ parameters: unknown;
814
+ };
815
+ }
816
+
817
+ /**
818
+ * Adapt an AppKit `ToolProvider` plugin instance into a
819
+ * {@link MastraPluginToolkitProvider}. Returns `null` for any plugin
820
+ * that doesn't implement both `toolkit` and `executeAgentTool` (e.g.
821
+ * `server`, `lakebase` when used only as a Postgres pool, etc.).
822
+ */
823
+ function adaptPluginToolkit(plugin: unknown): MastraPluginToolkitProvider | null {
824
+ if (!plugin || typeof plugin !== "object") return null;
825
+ const p = plugin as AppKitToolkitProvider;
826
+ if (typeof p.toolkit !== "function" || typeof p.executeAgentTool !== "function") {
827
+ return null;
828
+ }
829
+ return {
830
+ toolkit(opts?: ToolkitOptions): MastraTools {
831
+ const entries = p.toolkit!(opts);
832
+ const tools: MastraTools = {};
833
+ for (const [key, entry] of Object.entries(entries)) {
834
+ tools[key] = toolkitEntryToMastraTool(entry, p);
835
+ }
836
+ return tools;
837
+ },
838
+ };
839
+ }
840
+
841
+ /**
842
+ * Wrap a single {@link AppKitToolkitEntry} as a Mastra tool whose
843
+ * `execute` dispatches back through `plugin.executeAgentTool(...)` so
844
+ * AppKit's OBO auth (`asUser`) and telemetry spans stay intact. JSON
845
+ * Schema parameters pass through unchanged - Mastra's `PublicSchema`
846
+ * accepts `JSONSchema7` directly via `@mastra/schema-compat`.
847
+ */
848
+ function toolkitEntryToMastraTool(entry: AppKitToolkitEntry, plugin: AppKitToolkitProvider): Tool {
849
+ return createTool({
850
+ id: `${entry.pluginName}__${entry.localName}`,
851
+ description: entry.def.description,
852
+ ...(entry.def.parameters ? { inputSchema: entry.def.parameters as never } : {}),
853
+ execute: async (input: unknown, context: unknown) => {
854
+ const signal = (context as { abortSignal?: AbortSignal } | undefined)?.abortSignal;
855
+ return plugin.executeAgentTool!(entry.localName, input, signal);
856
+ },
857
+ });
858
+ }