@dbx-tools/appkit-mastra 0.1.4 → 0.1.9

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.
Files changed (46) hide show
  1. package/README.md +304 -503
  2. package/index.ts +46 -36
  3. package/package.json +58 -47
  4. package/src/agents.ts +255 -72
  5. package/src/chart.ts +695 -0
  6. package/src/config.ts +282 -18
  7. package/src/filesystems.ts +1090 -0
  8. package/src/genie.ts +1025 -288
  9. package/src/history.ts +297 -0
  10. package/src/mcp.ts +105 -0
  11. package/src/memory.ts +129 -74
  12. package/src/mlflow.ts +149 -0
  13. package/src/model.ts +80 -408
  14. package/src/observability.ts +144 -0
  15. package/src/pagination.ts +34 -0
  16. package/src/plugin.ts +587 -52
  17. package/src/processors.ts +168 -0
  18. package/src/rest.ts +67 -0
  19. package/src/server.ts +292 -80
  20. package/src/serving-sanitize.ts +167 -0
  21. package/src/serving.ts +27 -224
  22. package/src/statement.ts +89 -0
  23. package/src/storage-schema.ts +41 -0
  24. package/src/summarize.ts +176 -0
  25. package/src/threads.ts +338 -0
  26. package/src/workspaces.ts +346 -0
  27. package/src/writer.ts +44 -0
  28. package/tsconfig.json +41 -0
  29. package/dist/index.d.ts +0 -18
  30. package/dist/index.js +0 -18
  31. package/dist/src/agents.d.ts +0 -306
  32. package/dist/src/agents.js +0 -379
  33. package/dist/src/config.d.ts +0 -170
  34. package/dist/src/config.js +0 -12
  35. package/dist/src/genie.d.ts +0 -109
  36. package/dist/src/genie.js +0 -271
  37. package/dist/src/memory.d.ts +0 -79
  38. package/dist/src/memory.js +0 -197
  39. package/dist/src/model.d.ts +0 -159
  40. package/dist/src/model.js +0 -423
  41. package/dist/src/plugin.d.ts +0 -120
  42. package/dist/src/plugin.js +0 -235
  43. package/dist/src/server.d.ts +0 -42
  44. package/dist/src/server.js +0 -109
  45. package/dist/src/serving.d.ts +0 -156
  46. package/dist/src/serving.js +0 -214
package/src/agents.ts CHANGED
@@ -10,21 +10,27 @@
10
10
  *
11
11
  * When no agents are registered the plugin falls back to a single
12
12
  * built-in analyst so the bare `mastra()` call still mounts a working
13
- * `chatRoute` agent for demos.
13
+ * streamable agent for demos.
14
14
  */
15
15
 
16
- import { genie } from "@databricks/appkit";
17
- import { logUtils, pluginUtils, stringUtils } from "@dbx-tools/appkit-shared";
18
- import { Agent } from "@mastra/core/agent";
19
16
  import type { AgentConfig, ToolsInput } from "@mastra/core/agent";
20
- import { createTool } from "@mastra/core/tools";
17
+ import { Agent } from "@mastra/core/agent";
21
18
  import type { Tool } from "@mastra/core/tools";
19
+ import { createTool } from "@mastra/core/tools";
20
+ import type { Workspace } from "@mastra/core/workspace";
22
21
  import type { PgVectorConfig, PostgresStoreConfig } from "@mastra/pg";
23
22
 
24
- import type { MastraPluginConfig } from "./config.js";
25
- import { buildGenieProvider } from "./genie.js";
26
- import type { MemoryBuilder } from "./memory.js";
27
- import { buildModel } from "./model.js";
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, string } from "@dbx-tools/shared-core";
33
+ import { plugin } from "@dbx-tools/appkit";
28
34
 
29
35
  /**
30
36
  * Tool record accepted by every Mastra `Agent.tools` field and by the
@@ -120,12 +126,12 @@ export interface AppKitToolOptions {
120
126
 
121
127
  /**
122
128
  * Build a deterministic Mastra tool id from a description.
123
- * Delegates to {@link stringUtils.toUniqueSlug}: slug + always-on
124
- * SHA-1 suffix so two tools with the same leading words don't
125
- * collide in traces. Stable across runs.
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.
126
132
  */
127
133
  function deriveToolId(description: string): string {
128
- return stringUtils.toUniqueSlug(description, { fallbackPrefix: "tool" });
134
+ return string.toUniqueSlug(description, { fallbackPrefix: "tool" });
129
135
  }
130
136
 
131
137
  /**
@@ -144,7 +150,8 @@ function deriveToolId(description: string): string {
144
150
  * type inference and to match the AppKit API surface.
145
151
  */
146
152
  export function createAgent<T extends MastraAgentDefinition>(def: T): T {
147
- return def;
153
+ const workspace = def.workspace ?? createWorkspace();
154
+ return { ...def, workspace };
148
155
  }
149
156
 
150
157
  /**
@@ -217,15 +224,16 @@ export interface MastraPluginToolkitProvider {
217
224
  export type MastraPlugins = Record<string, MastraPluginToolkitProvider>;
218
225
 
219
226
  /** Function form of {@link MastraAgentDefinition.tools}. */
220
- export type MastraToolsFn = (
221
- plugins: MastraPlugins,
222
- ) => MastraTools | Promise<MastraTools>;
227
+ export type MastraToolsFn = (plugins: MastraPlugins) => MastraTools | Promise<MastraTools>;
228
+
229
+ /** Function form of {@link MastraAgentDefinition.workspace}. */
230
+ export type MastraAgentWorkspaceResolver = () => Workspace | undefined;
223
231
 
224
232
  /**
225
233
  * A code-defined Mastra agent. Mirrors the shape AppKit's `agents`
226
234
  * plugin uses for `AgentDefinition`. The registry key under
227
- * `config.agents` is what `chatRoute` matches on; `name` is purely
228
- * informational (defaults to the key).
235
+ * `config.agents` is the `agentId` the client streams against; `name`
236
+ * is purely informational (defaults to the key).
229
237
  */
230
238
  export interface MastraAgentDefinition {
231
239
  /** Display name used as `Agent.name`. Defaults to the registry key. */
@@ -274,7 +282,7 @@ export interface MastraAgentDefinition {
274
282
  *
275
283
  * - `undefined` (default): inherit `config.storage`. When that's
276
284
  * enabled, the agent gets its **own per-agent `PostgresStore`**
277
- * keyed by `schemaName: "mastra_<agentId>"` so threads and
285
+ * keyed by {@link agentStorageSchemaName} so threads and
278
286
  * messages stay isolated between agents in the same database.
279
287
  * - `false`: disable storage for this agent only (purely in-memory).
280
288
  * - `true`: enable with the per-agent default schema.
@@ -282,6 +290,12 @@ export interface MastraAgentDefinition {
282
290
  * `PostgresStore` config (custom schema, connection, etc.).
283
291
  */
284
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;
285
299
  }
286
300
 
287
301
  /**
@@ -290,19 +304,16 @@ export interface MastraAgentDefinition {
290
304
  * strip `id`. The built-in `Omit` collapses unions to one shape with
291
305
  * common fields only, which loses the connection-style discriminants.
292
306
  */
293
- type DistributiveOmit<T, K extends keyof never> = T extends unknown
294
- ? Omit<T, K>
295
- : never;
307
+ type DistributiveOmit<T, K extends keyof never> = T extends unknown ? Omit<T, K> : never;
296
308
 
297
309
  /**
298
310
  * `PostgresStoreConfig` minus `id` - per-agent overrides accept any
299
311
  * Mastra-supported storage shape. `id` is filled in automatically
300
312
  * from the agent registry key so traces stay stable.
301
313
  */
302
- export type MastraStorageConfigOverride = DistributiveOmit<
303
- PostgresStoreConfig,
304
- "id"
305
- > & { id?: string };
314
+ export type MastraStorageConfigOverride = DistributiveOmit<PostgresStoreConfig, "id"> & {
315
+ id?: string;
316
+ };
306
317
 
307
318
  /**
308
319
  * `PgVectorConfig` minus `id` - per-agent overrides accept any
@@ -317,11 +328,28 @@ export type MastraMemoryConfigOverride = DistributiveOmit<PgVectorConfig, "id">
317
328
  export interface BuiltAgents {
318
329
  agents: Record<string, Agent>;
319
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;
320
337
  }
321
338
 
322
339
  /** Fallback agent id used when `config.agents` is omitted entirely. */
323
340
  export const FALLBACK_AGENT_ID = "default";
324
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
+
325
353
  const FALLBACK_AGENT_INSTRUCTIONS = `You are a data analyst. The user will ask questions about
326
354
  business metrics and may share personal preferences you should remember across turns.
327
355
 
@@ -344,16 +372,21 @@ Rules:
344
372
  * Override globally via {@link MastraPluginConfig.styleInstructions}
345
373
  * (pass `false` to disable entirely, or a string to replace).
346
374
  */
347
- export const DEFAULT_STYLE_INSTRUCTIONS = `Output style:
348
-
349
- - Plain prose. Use hyphens (-) only. Never use em dashes (—) or en dashes (–).
350
- - Never use emojis.
351
- - Skip openers like "Great question", "Absolutely", "I'd be happy to help".
352
- - Skip closers like "Let me know if you have any questions".
353
- - Skip self-disclaimers ("I should mention", "It's important to note").
354
- - Answer directly. No preamble before the actual answer.
355
- - Use lists and headers only when they clarify a multi-part answer; not for short replies.
356
- - Quote numbers, code, identifiers, and tool output verbatim. Never paraphrase them.`;
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");
357
390
 
358
391
  /**
359
392
  * Resolve the style block to append to every agent's instructions.
@@ -364,6 +397,7 @@ function resolveStyleInstructions(config: MastraPluginConfig): string | null {
364
397
  if (typeof config.styleInstructions === "string") {
365
398
  return config.styleInstructions;
366
399
  }
400
+
367
401
  return DEFAULT_STYLE_INSTRUCTIONS;
368
402
  }
369
403
 
@@ -371,14 +405,21 @@ function resolveStyleInstructions(config: MastraPluginConfig): string | null {
371
405
  * Join an agent's bespoke instructions with the resolved style block.
372
406
  * Returns the bespoke text unchanged when the style block is disabled.
373
407
  */
374
- function composeInstructions(
375
- agentInstructions: string,
376
- style: string | null,
377
- ): string {
408
+ function composeInstructions(agentInstructions: string, style: string | null): string {
378
409
  if (!style) return agentInstructions;
379
410
  return `${agentInstructions.trimEnd()}\n\n${style}`;
380
411
  }
381
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
+
382
423
  /**
383
424
  * Resolve every entry in `config.agents` into a Mastra `Agent`
384
425
  * instance. When `config.agents` is omitted the plugin registers a
@@ -394,31 +435,78 @@ function composeInstructions(
394
435
  */
395
436
  export async function buildAgents(opts: {
396
437
  config: MastraPluginConfig;
397
- context: pluginUtils.PluginContextLike | undefined;
438
+ context: plugin.PluginContextLike | undefined;
398
439
  memoryBuilder?: MemoryBuilder;
399
- log: logUtils.Logger;
440
+ log: log.Logger;
400
441
  }): Promise<BuiltAgents> {
401
442
  const { config, context, memoryBuilder, log } = opts;
402
443
  const definitions = resolveDefinitions(config);
403
444
  const ids = Object.keys(definitions);
404
445
  const defaultAgentId = config.defaultAgent ?? ids[0] ?? FALLBACK_AGENT_ID;
405
446
 
406
- const plugins = buildPluginsMap(context);
407
- const ambientTools = config.tools ?? {};
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
+ };
408
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
+ ];
409
471
  const agents: Record<string, Agent> = {};
472
+ const approvalGatedByAgent: Array<{ agentId: string; toolIds: string[] }> = [];
410
473
 
411
474
  for (const [id, def] of Object.entries(definitions)) {
412
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 });
413
479
  const memory = memoryBuilder?.forAgent(id, def);
414
480
  agents[id] = new Agent({
415
481
  id,
416
482
  name: def.name ?? id,
417
- ...(def.description !== undefined ? { description: def.description } : {}),
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),
418
488
  instructions: composeInstructions(def.instructions, style),
419
489
  model: resolveModel(config, def.model),
490
+ defaultOptions: {
491
+ maxSteps: config.agentMaxSteps ?? DEFAULT_AGENT_MAX_STEPS,
492
+ },
420
493
  tools,
421
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),
422
510
  });
423
511
  }
424
512
 
@@ -428,8 +516,88 @@ export async function buildAgents(opts: {
428
516
  );
429
517
  }
430
518
 
431
- log.info("agents registered", { ids, defaultAgentId });
432
- return { agents, defaultAgentId };
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
+ const ids: string[] = [];
532
+ for (const [key, tool] of Object.entries(tools)) {
533
+ if (!isApprovalGatedTool(tool)) continue;
534
+ ids.push(resolveToolId(tool, key));
535
+ }
536
+ return ids;
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
+ );
433
601
  }
434
602
 
435
603
  /**
@@ -446,9 +614,7 @@ export async function buildAgents(opts: {
446
614
  * Omitted or empty inputs fall back to a single built-in analyst so
447
615
  * the bare `mastra()` call still mounts a working chat route.
448
616
  */
449
- function resolveDefinitions(
450
- config: MastraPluginConfig,
451
- ): Record<string, MastraAgentDefinition> {
617
+ function resolveDefinitions(config: MastraPluginConfig): Record<string, MastraAgentDefinition> {
452
618
  const input = config.agents;
453
619
  if (!input) return fallbackDefinitions();
454
620
 
@@ -484,7 +650,7 @@ function resolveDefinitions(
484
650
  /** Derive a registry id from a definition's `name`, with a fallback. */
485
651
  function deriveAgentKey(def: MastraAgentDefinition, index?: number): string {
486
652
  if (def.name) {
487
- const slug = stringUtils.toIdentifier(def.name);
653
+ const slug = string.toIdentifier(def.name);
488
654
  if (slug) return slug;
489
655
  }
490
656
  return index === undefined ? FALLBACK_AGENT_ID : `agent_${index}`;
@@ -548,6 +714,13 @@ async function resolveTools(
548
714
  return { ...ambientTools, ...resolved };
549
715
  }
550
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
+
551
724
  /**
552
725
  * Build the {@link MastraPlugins} runtime proxy handed to
553
726
  * `tools(plugins)` callbacks.
@@ -569,14 +742,15 @@ async function resolveTools(
569
742
  * time instead of staring at a spinner for the full Genie round-trip.
570
743
  */
571
744
  function buildPluginsMap(
572
- context: pluginUtils.PluginContextLike | undefined,
745
+ config: MastraPluginConfig,
746
+ context: plugin.PluginContextLike | undefined,
573
747
  ): MastraPlugins {
574
748
  const cache = new Map<string, MastraPluginToolkitProvider | null>();
575
749
  return new Proxy({} as MastraPlugins, {
576
750
  get(_target, propName) {
577
751
  if (typeof propName !== "string") return undefined;
578
752
  if (cache.has(propName)) return cache.get(propName) ?? undefined;
579
- const provider = resolveProvider(context, propName);
753
+ const provider = resolveProvider(config, context, propName);
580
754
  cache.set(propName, provider);
581
755
  return provider ?? undefined;
582
756
  },
@@ -585,18 +759,35 @@ function buildPluginsMap(
585
759
 
586
760
  /**
587
761
  * Pick the right {@link MastraPluginToolkitProvider} for a sibling
588
- * plugin lookup. Returns the streaming-aware Genie adapter when the
589
- * caller asks for `genie`; falls back to the generic AppKit
590
- * `ToolProvider` adapter for every other plugin name.
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.
591
778
  */
592
779
  function resolveProvider(
593
- context: pluginUtils.PluginContextLike | undefined,
780
+ config: MastraPluginConfig,
781
+ context: plugin.PluginContextLike | undefined,
594
782
  propName: string,
595
783
  ): MastraPluginToolkitProvider | null {
596
784
  if (propName === "genie") {
597
- const geniePlugin = pluginUtils.instance(context, genie);
598
- if (!geniePlugin) return null;
599
- return buildGenieProvider(geniePlugin) as MastraPluginToolkitProvider;
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;
600
791
  }
601
792
  const plugin = context?.getPlugins().get(propName);
602
793
  return adaptPluginToolkit(plugin);
@@ -609,11 +800,7 @@ function resolveProvider(
609
800
  */
610
801
  interface AppKitToolkitProvider {
611
802
  toolkit?: (opts?: ToolkitOptions) => Record<string, AppKitToolkitEntry>;
612
- executeAgentTool?: (
613
- name: string,
614
- args: unknown,
615
- signal?: AbortSignal,
616
- ) => Promise<unknown>;
803
+ executeAgentTool?: (name: string, args: unknown, signal?: AbortSignal) => Promise<unknown>;
617
804
  }
618
805
 
619
806
  /** Single entry returned by an AppKit plugin's `.toolkit(opts)` call. */
@@ -658,17 +845,13 @@ function adaptPluginToolkit(plugin: unknown): MastraPluginToolkitProvider | null
658
845
  * Schema parameters pass through unchanged - Mastra's `PublicSchema`
659
846
  * accepts `JSONSchema7` directly via `@mastra/schema-compat`.
660
847
  */
661
- function toolkitEntryToMastraTool(
662
- entry: AppKitToolkitEntry,
663
- plugin: AppKitToolkitProvider,
664
- ): Tool {
848
+ function toolkitEntryToMastraTool(entry: AppKitToolkitEntry, plugin: AppKitToolkitProvider): Tool {
665
849
  return createTool({
666
850
  id: `${entry.pluginName}__${entry.localName}`,
667
851
  description: entry.def.description,
668
852
  ...(entry.def.parameters ? { inputSchema: entry.def.parameters as never } : {}),
669
853
  execute: async (input: unknown, context: unknown) => {
670
- const signal = (context as { abortSignal?: AbortSignal } | undefined)
671
- ?.abortSignal;
854
+ const signal = (context as { abortSignal?: AbortSignal } | undefined)?.abortSignal;
672
855
  return plugin.executeAgentTool!(entry.localName, input, signal);
673
856
  },
674
857
  });