@dbx-tools/appkit-mastra 0.1.5 → 0.1.12

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/genie.ts CHANGED
@@ -20,14 +20,25 @@
20
20
  * `render_data` tool when the model decides one is useful.
21
21
  */
22
22
 
23
- import { randomUUID } from "node:crypto";
24
-
25
23
  import { genie } from "@databricks/appkit";
26
- import { stringUtils } from "@dbx-tools/appkit-shared";
24
+ import { logUtils, stringUtils } from "@dbx-tools/appkit-shared";
25
+ import type { RequestContext } from "@mastra/core/request-context";
27
26
  import { createTool } from "@mastra/core/tools";
28
27
  import type { ToolStream } from "@mastra/core/tools";
29
28
  import { z } from "zod";
30
29
 
30
+ import { emitChartWithPlanning } from "./chart.js";
31
+ import type { MastraPluginConfig } from "./config.js";
32
+
33
+ /**
34
+ * Module-level logger tagged `[mastra/genie]`. Uses the shared
35
+ * {@link logUtils.logger} so calls below `LOG_LEVEL` are
36
+ * discarded for free. Default `LOG_LEVEL` is `info`; flip to
37
+ * `debug` to see per-turn timing (`query_result` → planner
38
+ * waits → `drain:return`).
39
+ */
40
+ const log = logUtils.logger("mastra/genie");
41
+
31
42
  /** Live AppKit `GeniePlugin` instance. */
32
43
  export type GeniePluginInstance = InstanceType<ReturnType<typeof genie>["plugin"]>;
33
44
 
@@ -137,17 +148,21 @@ const genieToolOutputSchema = z.object({
137
148
  });
138
149
 
139
150
  type DrainResult = z.infer<typeof genieToolOutputSchema>;
140
- type DatasetMeta = z.infer<typeof datasetSchema> & { statementId: string };
141
151
 
142
152
  /**
143
153
  * Normalised progress event surfaced to the UI as a Mastra
144
154
  * `tool-output` chunk. Loading pill events (`started`, `status`,
145
155
  * `sql`, `suggested`, `error`) are pure UI metadata and never reach
146
- * the LLM. The `chart` variant carries the rows from a Genie SQL
147
- * statement so the host UI's `<ChartSlot>` can render them inline
148
- * via the same path as the `render_data` tool; the LLM still only
149
- * sees the matching {@link datasetSchema} metadata in
150
- * `genieAnswer`'s sibling `datasets[]` field.
156
+ * the LLM.
157
+ *
158
+ * The `chart` variant is the wire shape emitted by
159
+ * {@link emitChartWithPlanning} (used by both this Genie
160
+ * draining loop and the system-level `render_data` tool). All
161
+ * fields except `chartId` are optional because two events per
162
+ * chartId arrive on the wire: the first carries the rows
163
+ * (`title` + `description?` + `data`); the second, on planner
164
+ * success, carries just the resolved Echarts spec (`option`).
165
+ * The host UI's `<ChartSlot>` merges them by `chartId`.
151
166
  */
152
167
  export type GenieProgress =
153
168
  | { kind: "started"; conversationId: string; messageId: string; spaceId: string }
@@ -162,9 +177,10 @@ export type GenieProgress =
162
177
  | {
163
178
  kind: "chart";
164
179
  chartId: string;
165
- title: string;
180
+ title?: string;
166
181
  description?: string;
167
- data: Array<Record<string, unknown>>;
182
+ data?: Array<Record<string, unknown>>;
183
+ option?: Record<string, unknown>;
168
184
  }
169
185
  | { kind: "text"; content: string }
170
186
  | { kind: "suggested"; questions: string[] }
@@ -302,10 +318,16 @@ export function defaultGenieToolName(alias: string): string {
302
318
  * Build one `sendMessage` tool per configured Genie alias plus a single
303
319
  * `getConversation` tool. Returns a record keyed by tool id, ready to
304
320
  * spread into an `Agent`'s `tools` map.
321
+ *
322
+ * `config` must be the active plugin config; Genie's
323
+ * `query_result` events are routed through
324
+ * {@link emitChartWithPlanning} which uses it to resolve the
325
+ * chart-planner's model.
305
326
  */
306
327
  export function buildGenieTools(opts: {
307
328
  aliases: string[];
308
329
  exports: GenieExports;
330
+ config: MastraPluginConfig;
309
331
  signal?: AbortSignal;
310
332
  }): Record<string, ReturnType<typeof createTool>> {
311
333
  const tools: Record<string, ReturnType<typeof createTool>> = {};
@@ -317,23 +339,17 @@ export function buildGenieTools(opts: {
317
339
  description: stringUtils.toDescription`
318
340
  Ask the Databricks Genie space "${alias}" a single
319
341
  natural-language question. Genie translates it to SQL,
320
- runs the SQL against the configured datasets, and returns
321
- \`genieAnswer\` (its prose answer) plus \`datasets[]\`
322
- (one metadata entry per executed query). Each dataset
323
- carries a short \`chartId\`; embed
324
- \`[[chart:<chartId>]]\` on its own line in your reply at
325
- the position where you want that data rendered as an
326
- inline chart. Do not paraphrase row values - the chart is
327
- the rendering. Add interpretation around the chart
328
- (highlights, deltas, anomalies, takeaways) instead of
329
- repeating numbers.
330
-
331
- Calling this tool is expensive; issue **one** focused
332
- question per user turn. If the first answer doesn't fit,
333
- ask the user a clarifying question rather than
334
- re-querying with rephrased intent. Prefer aggregated
335
- questions over raw-row queries (e.g. ask for "monthly
336
- averages" instead of "all rows" for time-series).
342
+ runs it, and returns \`genieAnswer\` (prose) plus
343
+ \`datasets[]\` (one entry per executed query, each with
344
+ a short \`chartId\`). Embed \`[[chart:<chartId>]]\` on
345
+ its own line at the position you want that data rendered
346
+ as an inline chart. Add interpretation around the chart
347
+ (deltas, anomalies, takeaways); do not paraphrase row
348
+ values.
349
+
350
+ Issue ONE focused question per user turn. Prefer
351
+ aggregated queries over raw-row queries for time-series
352
+ and distributions.
337
353
  `,
338
354
  inputSchema: sendMessageSchema,
339
355
  outputSchema: genieToolOutputSchema,
@@ -341,7 +357,12 @@ export function buildGenieTools(opts: {
341
357
  const stream = opts.exports.sendMessage(alias, content, conversationId, {
342
358
  signal: opts.signal,
343
359
  });
344
- return drainGenieStream(stream, ctx.writer);
360
+ const requestContext = (ctx as { requestContext?: RequestContext } | undefined)
361
+ ?.requestContext;
362
+ return drainGenieStream(stream, ctx.writer, {
363
+ config: opts.config,
364
+ ...(requestContext ? { requestContext } : {}),
365
+ });
345
366
  },
346
367
  });
347
368
  }
@@ -363,6 +384,12 @@ export function buildGenieTools(opts: {
363
384
  return tools;
364
385
  }
365
386
 
387
+ /** Inputs to {@link drainGenieStream}. */
388
+ interface DrainGenieStreamOptions {
389
+ config: MastraPluginConfig;
390
+ requestContext?: RequestContext;
391
+ }
392
+
366
393
  /**
367
394
  * Drain the genie `sendMessage` AsyncGenerator into a flat result
368
395
  * the agent's calling LLM can reason about, while forwarding
@@ -373,26 +400,33 @@ export function buildGenieTools(opts: {
373
400
  * 1. {@link GenieProgress} pill events on the writer (`started`,
374
401
  * `status`, `sql`, `suggested`, `error`) drive the loading
375
402
  * pill in the chat bubble.
376
- * 2. `kind: "chart"` events on the writer carry the row payload
377
- * from each Genie SQL statement so the host UI's
378
- * `<ChartSlot>` can render the chart inline at the marker
379
- * position the model picked. The data never reaches the LLM.
380
- * 3. The `DrainResult` returned to the LLM contains
381
- * Genie's prose answer plus a `datasets[]` array of metadata
382
- * (chartId, title, columns, rowCount, sql) the model uses to
383
- * cite charts via `[[chart:<chartId>]]` markers.
403
+ * 2. `kind: "chart"` events on the writer (emitted via
404
+ * {@link emitChartWithPlanning}) carry the row payload from
405
+ * each Genie SQL statement and, on planner success, a
406
+ * follow-up event with the rendered Echarts spec. The host
407
+ * UI's `<ChartSlot>` merges the two by `chartId` and
408
+ * renders inline at the marker position the model picked.
409
+ * The data never reaches the LLM.
410
+ * 3. The `DrainResult` returned to the LLM contains Genie's
411
+ * prose answer plus a `datasets[]` array of metadata
412
+ * (chartId, title, columns, rowCount, sql) the model uses
413
+ * to cite charts via `[[chart:<chartId>]]` markers.
384
414
  *
385
415
  * `query_result` and `message_result` events arrive in either
386
- * order; we buffer per-statement metadata in
387
- * {@link DatasetMeta} so each half can fill in the bits it knows
388
- * about and we emit the chart event once `query_result` lands
389
- * (with whatever title was already set, falling back to a
390
- * generic label otherwise).
416
+ * order; we buffer per-statement scratch keyed by `statementId`
417
+ * so each half can fill in what it knows. The chart event
418
+ * fires the moment `query_result` lands; the planner runs in
419
+ * the background. We `Promise.allSettled` every planner promise
420
+ * before returning so all chart work is attributed to the tool's
421
+ * trace span and so the LLM's `datasets[]` includes every
422
+ * chartId that has actually been queued.
391
423
  */
392
424
  async function drainGenieStream(
393
425
  stream: AsyncGenerator<GenieStreamEvent>,
394
- writer?: ToolStream,
426
+ writer: ToolStream | undefined,
427
+ opts: DrainGenieStreamOptions,
395
428
  ): Promise<DrainResult> {
429
+ const { config, requestContext } = opts;
396
430
  let conversationId: string | undefined;
397
431
  let genieAnswer: string | undefined;
398
432
  let suggestedFollowUps: string[] | undefined;
@@ -406,15 +440,37 @@ async function drainGenieStream(
406
440
  let lastStatus: string | undefined;
407
441
 
408
442
  // Per-statement scratch keyed by Genie's `statementId`. Filled in
409
- // by both `query_result` (rows + columns) and `message_result`
410
- // (sql + title + description); the LLM-bound `datasets[]` is
411
- // built from this at end-of-stream, and chart writer events fire
412
- // when `query_result` lands.
413
- const datasetsByStatementId = new Map<string, DatasetMeta>();
414
-
415
- // Best-effort progress emission. Awaited so the underlying agent
416
- // stream sees events in order; write failures are swallowed so a
417
- // dead writer (e.g. closed downstream) can't take the tool down.
443
+ // by both `query_result` (chartId + columns + rows) and
444
+ // `message_result` (sql + title + description). The LLM-bound
445
+ // `datasets[]` is built from this at end-of-stream, after all
446
+ // planner promises settle.
447
+ type Scratch = {
448
+ statementId: string;
449
+ chartId?: string;
450
+ title?: string;
451
+ description?: string;
452
+ sql?: string;
453
+ columns: string[];
454
+ rowCount: number;
455
+ };
456
+ const scratchByStatementId = new Map<string, Scratch>();
457
+ const getScratch = (statementId: string): Scratch => {
458
+ let s = scratchByStatementId.get(statementId);
459
+ if (!s) {
460
+ s = { statementId, columns: [], rowCount: 0 };
461
+ scratchByStatementId.set(statementId, s);
462
+ }
463
+ return s;
464
+ };
465
+ /**
466
+ * Planner promises kicked off per `query_result`. Awaited
467
+ * (Promise.allSettled) before drainGenieStream returns so the
468
+ * Genie tool's trace span covers the chart work and the LLM's
469
+ * `datasets[]` accurately reflects every chartId that's been
470
+ * queued for rendering.
471
+ */
472
+ const plannerPromises: Promise<void>[] = [];
473
+
418
474
  const emit = async (event: GenieProgress) => {
419
475
  if (!writer) return;
420
476
  try {
@@ -425,13 +481,12 @@ async function drainGenieStream(
425
481
  };
426
482
 
427
483
  for await (const event of stream) {
428
- // Uncomment to log every raw Genie wire event before the switch
429
- // routes it through the writer / DrainResult. Useful when tuning
430
- // the pill / answer pipeline against real Genie payloads (status
431
- // codes, attachment shapes, query_result manifests Genie surfaces
432
- // only on certain question types, etc.).
433
- // eslint-disable-next-line no-console
434
- // console.log("[mastra/genie] event", event);
484
+ // Per-event raw payload for tuning the pill / answer pipeline
485
+ // against real Genie traffic. At `info` (the default) this is
486
+ // discarded for free; flip `LOG_LEVEL=debug` to see every
487
+ // raw wire event before the switch routes it through writer
488
+ // and DrainResult.
489
+ log.debug("event", { type: event.type, payload: event });
435
490
  switch (event.type) {
436
491
  case "message_start":
437
492
  conversationId = event.conversationId;
@@ -459,17 +514,30 @@ async function drainGenieStream(
459
514
  Array<string | null>
460
515
  >;
461
516
  const rows = genieRowsToObjects(columns, dataArray);
462
- const meta = upsertDatasetMeta(datasetsByStatementId, event.statementId, {
463
- columns,
464
- rowCount: rows.length,
465
- });
466
- await emit({
467
- kind: "chart",
468
- chartId: meta.chartId,
469
- title: meta.title ?? `Genie query`,
470
- ...(meta.description ? { description: meta.description } : {}),
517
+ const scratch = getScratch(event.statementId);
518
+ // emitChartWithPlanning emits the dataset event immediately
519
+ // and kicks off the chart-planner agent in the background.
520
+ // It returns the chartId synchronously; the plannerPromise
521
+ // is awaited at end-of-stream so chart work shows up under
522
+ // this tool's trace span.
523
+ const { chartId, plannerPromise } = await emitChartWithPlanning({
524
+ ...(writer ? { writer } : {}),
525
+ config,
526
+ ...(requestContext ? { requestContext } : {}),
527
+ title: scratch.title ?? `Genie query`,
528
+ ...(scratch.description ? { description: scratch.description } : {}),
471
529
  data: rows,
472
530
  });
531
+ scratch.chartId = chartId;
532
+ scratch.columns = columns;
533
+ scratch.rowCount = rows.length;
534
+ plannerPromises.push(plannerPromise);
535
+ log.debug("query_result", {
536
+ statementId: event.statementId,
537
+ chartId,
538
+ rows: rows.length,
539
+ columns,
540
+ });
473
541
  break;
474
542
  }
475
543
  case "message_result":
@@ -477,14 +545,13 @@ async function drainGenieStream(
477
545
  for (const attachment of event.message.attachments ?? []) {
478
546
  const sqlText = attachment.query?.query;
479
547
  const stmtId = attachment.query?.statementId;
480
- if (sqlText && stmtId) {
481
- upsertDatasetMeta(datasetsByStatementId, stmtId, {
482
- sql: sqlText,
483
- ...(attachment.query?.title ? { title: attachment.query.title } : {}),
484
- ...(attachment.query?.description
485
- ? { description: attachment.query.description }
486
- : {}),
487
- });
548
+ if (stmtId) {
549
+ const scratch = getScratch(stmtId);
550
+ if (sqlText) scratch.sql = sqlText;
551
+ if (attachment.query?.title) scratch.title = attachment.query.title;
552
+ if (attachment.query?.description) {
553
+ scratch.description = attachment.query.description;
554
+ }
488
555
  }
489
556
  if (sqlText) {
490
557
  await emit({
@@ -519,21 +586,42 @@ async function drainGenieStream(
519
586
  }
520
587
  }
521
588
 
522
- // Strip statementId / row-only fields when handing the LLM the
523
- // datasets - the model never references statementId, and the
524
- // chartId is what the marker uses.
589
+ // Wait for all chart planners to settle before returning so the
590
+ // tool's trace span covers chart work and the LLM's
591
+ // `datasets[]` reflects only chartIds the client has actually
592
+ // received writer events for. Failures in `emitChartWithPlanning`
593
+ // are already swallowed inside the helper, so this never
594
+ // throws.
595
+ log.debug("planners:awaiting", { count: plannerPromises.length });
596
+ await Promise.allSettled(plannerPromises);
597
+ log.debug("planners:settled", { count: plannerPromises.length });
598
+
599
+ // Build the LLM-bound `datasets[]` from scratch entries that
600
+ // actually ran a query (chartId is assigned at `query_result`
601
+ // time). Entries that only saw `message_result` metadata
602
+ // without a row payload are skipped.
525
603
  const datasets: Array<z.infer<typeof datasetSchema>> = [];
526
- for (const meta of datasetsByStatementId.values()) {
604
+ for (const scratch of scratchByStatementId.values()) {
605
+ if (!scratch.chartId) continue;
527
606
  datasets.push({
528
- chartId: meta.chartId,
529
- ...(meta.title ? { title: meta.title } : {}),
530
- ...(meta.description ? { description: meta.description } : {}),
531
- columns: meta.columns,
532
- rowCount: meta.rowCount,
533
- ...(meta.sql ? { sql: meta.sql } : {}),
607
+ chartId: scratch.chartId,
608
+ ...(scratch.title ? { title: scratch.title } : {}),
609
+ ...(scratch.description ? { description: scratch.description } : {}),
610
+ columns: scratch.columns,
611
+ rowCount: scratch.rowCount,
612
+ ...(scratch.sql ? { sql: scratch.sql } : {}),
534
613
  });
535
614
  }
536
615
 
616
+ log.debug("drain:return", {
617
+ conversationId,
618
+ hasAnswer: typeof genieAnswer === "string",
619
+ answerLength: genieAnswer?.length ?? 0,
620
+ chartIds: datasets.map((d) => d.chartId),
621
+ suggestedCount: suggestedFollowUps?.length ?? 0,
622
+ error,
623
+ });
624
+
537
625
  return {
538
626
  ...(conversationId ? { conversationId } : {}),
539
627
  ...(genieAnswer ? { genieAnswer } : {}),
@@ -543,35 +631,6 @@ async function drainGenieStream(
543
631
  };
544
632
  }
545
633
 
546
- /**
547
- * Get-or-create-and-merge the per-statement scratch entry. Both
548
- * `query_result` and `message_result` paths call this with their
549
- * partial bag of fields; the resulting record is the union of
550
- * everything we know about that statement so far.
551
- */
552
- function upsertDatasetMeta(
553
- store: Map<string, DatasetMeta>,
554
- statementId: string,
555
- patch: Partial<Omit<DatasetMeta, "chartId" | "statementId">>,
556
- ): DatasetMeta {
557
- const existing = store.get(statementId);
558
- const merged: DatasetMeta = {
559
- chartId: existing?.chartId ?? randomUUID().replace(/-/g, "").slice(0, 8),
560
- statementId,
561
- columns: patch.columns ?? existing?.columns ?? [],
562
- rowCount: patch.rowCount ?? existing?.rowCount ?? 0,
563
- ...(patch.title ?? existing?.title
564
- ? { title: patch.title ?? existing?.title }
565
- : {}),
566
- ...(patch.description ?? existing?.description
567
- ? { description: patch.description ?? existing?.description }
568
- : {}),
569
- ...(patch.sql ?? existing?.sql ? { sql: patch.sql ?? existing?.sql } : {}),
570
- };
571
- store.set(statementId, merged);
572
- return merged;
573
- }
574
-
575
634
  /**
576
635
  * Convert Genie's `data_array` (column-positional `string | null`
577
636
  * tuples) into plain JS row objects keyed by column name. Numeric
@@ -626,7 +685,10 @@ function coerceCell(cell: string | null): unknown {
626
685
  * all-or-nothing bundle. Wire `only` / `except` / `prefix` / `rename`
627
686
  * later if a caller needs them.
628
687
  */
629
- export function buildGenieProvider(plugin: GeniePluginInstance): {
688
+ export function buildGenieProvider(
689
+ plugin: GeniePluginInstance,
690
+ opts: { config: MastraPluginConfig },
691
+ ): {
630
692
  toolkit(opts?: unknown): Record<string, ReturnType<typeof createTool>>;
631
693
  } {
632
694
  return {
@@ -638,6 +700,7 @@ export function buildGenieProvider(plugin: GeniePluginInstance): {
638
700
  sendMessage: plugin.sendMessage.bind(plugin),
639
701
  getConversation: plugin.getConversation.bind(plugin),
640
702
  },
703
+ config: opts.config,
641
704
  });
642
705
  },
643
706
  };
package/src/history.ts CHANGED
@@ -16,21 +16,23 @@
16
16
  * session-cookie logic stays the single source of truth in `server.ts`.
17
17
  */
18
18
 
19
+ import { logUtils } from "@dbx-tools/appkit-shared";
19
20
  import { toAISdkV5Messages } from "@mastra/ai-sdk/ui";
20
21
  import type { Agent } from "@mastra/core/agent";
21
- import type {
22
- MastraDBMessage,
23
- } from "@mastra/core/agent/message-list";
22
+ import type { MastraDBMessage } from "@mastra/core/agent/message-list";
24
23
  import {
25
24
  MASTRA_RESOURCE_ID_KEY,
26
25
  MASTRA_THREAD_ID_KEY,
27
26
  } from "@mastra/core/request-context";
28
27
  import { registerApiRoute } from "@mastra/core/server";
28
+ import type { ContextWithMastra } from "@mastra/core/server";
29
29
  import type {
30
30
  MastraHistoryResponse,
31
31
  MastraHistoryUIMessage,
32
32
  } from "@dbx-tools/appkit-mastra-shared";
33
33
 
34
+ const log = logUtils.logger("mastra/history");
35
+
34
36
  /** Default history page size; matches the Mastra storage default. */
35
37
  const DEFAULT_PER_PAGE = 20;
36
38
  /** Hard cap so a misbehaving client can't fetch the whole thread at once. */
@@ -69,8 +71,10 @@ export async function loadHistory(
69
71
  const page = Math.max(0, Math.trunc(opts.page ?? 0));
70
72
  const memory = await opts.agent.getMemory();
71
73
  if (!memory) {
74
+ log.debug("recall:no-memory", { agentId: opts.agent.id, threadId: opts.threadId });
72
75
  return { uiMessages: [], page, perPage, total: 0, hasMore: false };
73
76
  }
77
+ const startedAt = Date.now();
74
78
  const result = await memory.recall({
75
79
  threadId: opts.threadId,
76
80
  ...(opts.resourceId ? { resourceId: opts.resourceId } : {}),
@@ -85,6 +89,16 @@ export async function loadHistory(
85
89
  const uiMessages = toAISdkV5Messages(
86
90
  chronological,
87
91
  ) as unknown as MastraHistoryUIMessage[];
92
+ log.debug("recall:done", {
93
+ agentId: opts.agent.id,
94
+ threadId: opts.threadId,
95
+ page,
96
+ perPage,
97
+ returned: uiMessages.length,
98
+ total: result.total,
99
+ hasMore: result.hasMore,
100
+ elapsedMs: Date.now() - startedAt,
101
+ });
88
102
  return {
89
103
  uiMessages,
90
104
  page,
@@ -122,7 +136,7 @@ export function historyRoute(options: HistoryRouteOptions) {
122
136
  }
123
137
  return registerApiRoute(path, {
124
138
  method: "GET",
125
- handler: async (c) => {
139
+ handler: async (c: ContextWithMastra) => {
126
140
  const mastra = c.get("mastra");
127
141
  const requestContext = c.get("requestContext");
128
142
  const agentId = fixedAgent ?? c.req.param("agentId");
@@ -133,9 +147,7 @@ export function historyRoute(options: HistoryRouteOptions) {
133
147
  if (!agent) {
134
148
  return c.json({ error: `Unknown agent "${agentId}"` }, 404);
135
149
  }
136
- const threadId = requestContext.get(MASTRA_THREAD_ID_KEY) as
137
- | string
138
- | undefined;
150
+ const threadId = requestContext.get(MASTRA_THREAD_ID_KEY) as string | undefined;
139
151
  if (!threadId) {
140
152
  return c.json({ error: "thread id missing from request context" }, 400);
141
153
  }
package/src/memory.ts CHANGED
@@ -20,7 +20,7 @@
20
20
  */
21
21
 
22
22
  import { lakebase } from "@databricks/appkit";
23
- import { pluginUtils } from "@dbx-tools/appkit-shared";
23
+ import { logUtils, pluginUtils } from "@dbx-tools/appkit-shared";
24
24
  import { fastembed } from "@mastra/fastembed";
25
25
  import { Memory } from "@mastra/memory";
26
26
  import { PgVector, PostgresStore } from "@mastra/pg";
@@ -33,6 +33,8 @@ import type {
33
33
  } from "./agents.js";
34
34
  import type { MastraPluginConfig } from "./config.js";
35
35
 
36
+ const log = logUtils.logger("mastra/memory");
37
+
36
38
  /** Pool handle returned by the AppKit `lakebase` plugin `exports().pool`. */
37
39
  export type LakebasePool = ReturnType<
38
40
  InstanceType<ReturnType<typeof lakebase>["plugin"]>["exports"]
@@ -109,7 +111,22 @@ export class MemoryBuilder {
109
111
 
110
112
  const storage = this.buildStorage(agentId, storageSetting);
111
113
  const vector = this.buildVector(memorySetting);
112
- if (!storage && !vector) return undefined;
114
+ if (!storage && !vector) {
115
+ log.debug("agent:stateless", { agentId });
116
+ return undefined;
117
+ }
118
+
119
+ log.debug("agent:configured", {
120
+ agentId,
121
+ storage: storage !== undefined,
122
+ vector: vector !== undefined,
123
+ vectorMode:
124
+ vector === undefined
125
+ ? "off"
126
+ : typeof memorySetting === "object"
127
+ ? "dedicated"
128
+ : "shared",
129
+ });
113
130
 
114
131
  return new Memory({
115
132
  ...(storage ? { storage } : {}),
package/src/model.ts CHANGED
@@ -306,6 +306,7 @@ async function pickModelId(
306
306
  user: User,
307
307
  host: string,
308
308
  ): Promise<string> {
309
+ const log = logUtils.logger(config);
309
310
  const serving = resolveServingConfig(config, FALLBACK_MODEL_IDS);
310
311
  const override = serving.allowOverride
311
312
  ? (requestContext.get(MASTRA_MODEL_OVERRIDE_KEY) as string | undefined)
@@ -315,7 +316,10 @@ async function pickModelId(
315
316
 
316
317
  // Cheap exit: when the caller named a specific model and fuzzy
317
318
  // matching is off, there's no reason to touch the catalogue at all.
318
- if (explicit !== undefined && !serving.fuzzy) return explicit;
319
+ if (explicit !== undefined && !serving.fuzzy) {
320
+ log.debug("model selected", { modelId: explicit, source: "explicit" });
321
+ return explicit;
322
+ }
319
323
 
320
324
  const endpoints = await listServingEndpoints(user.executionContext.client, host, {
321
325
  ttlMs: serving.ttlMs,
@@ -324,7 +328,11 @@ async function pickModelId(
324
328
  explicit !== undefined
325
329
  ? resolveModelId(explicit, endpoints, { threshold: serving.threshold }).modelId
326
330
  : pickFirstAvailable(serving.fallbacks, endpoints);
327
- //logUtils.logger(config).debug(`model selected: ${modelId}`);
331
+ log.debug("model selected", {
332
+ modelId,
333
+ source: explicit !== undefined ? "fuzzy-match" : "fallback",
334
+ requestedExplicit: explicit,
335
+ });
328
336
  return modelId;
329
337
  }
330
338
 
@@ -369,9 +377,9 @@ interface ChatMessage {
369
377
  * 1. Rewrites the outgoing `messages` array to repair Mastra/AI SDK
370
378
  * stream-replay quirks that Databricks-hosted Claude rejects (see
371
379
  * {@link sanitizeServingMessages}).
372
- * 2. When `MASTRA_DEBUG_LLM=1`, dumps the (post-sanitize) JSON body
373
- * to stderr so 4xx debugging doesn't have to fight AI SDK's
374
- * `[Array]` formatter.
380
+ * 2. At `LOG_LEVEL=debug`, dumps the (post-sanitize) JSON body so
381
+ * 4xx debugging doesn't have to fight AI SDK's `[Array]`
382
+ * formatter.
375
383
  *
376
384
  * Safe to call from any hot path: {@link commonUtils.memoize} ensures
377
385
  * the wrapper is installed at most once per process, so subsequent
@@ -379,7 +387,7 @@ interface ChatMessage {
379
387
  * {@link buildModel} fires on every agent step.
380
388
  */
381
389
  const setupFetchInterceptor = commonUtils.memoize((): void => {
382
- const debug = Boolean(process.env.MASTRA_DEBUG_LLM);
390
+ const log = logUtils.logger("mastra/llm");
383
391
  const original = globalThis.fetch.bind(globalThis);
384
392
  globalThis.fetch = (async (input, init) => {
385
393
  const url = httpUtils.toURL(input);
@@ -394,13 +402,10 @@ const setupFetchInterceptor = commonUtils.memoize((): void => {
394
402
  if (rewritten !== init.body) {
395
403
  init = { ...init, body: rewritten };
396
404
  }
397
- if (debug) {
398
- try {
399
- console.error("[mastra:llm-debug] -> POST", url.toString());
400
- console.error(JSON.stringify(JSON.parse(rewritten), null, 2));
401
- } catch {
402
- console.error("[mastra:llm-debug] -> POST", url.toString(), "(non-JSON body)");
403
- }
405
+ try {
406
+ log.debug("POST", { url: url.toString(), body: JSON.parse(rewritten) });
407
+ } catch {
408
+ log.debug("POST", { url: url.toString(), bodyType: "non-JSON" });
404
409
  }
405
410
  return original(input, init);
406
411
  }) as typeof globalThis.fetch;
package/src/plugin.ts CHANGED
@@ -47,7 +47,6 @@ import { buildAgents, FALLBACK_AGENT_ID, type BuiltAgents } from "./agents.js";
47
47
  import type { MastraClientConfig } from "@dbx-tools/appkit-mastra-shared";
48
48
  import type { MastraPluginConfig } from "./config.js";
49
49
  import { historyRoute } from "./history.js";
50
- import { renderChartRoute } from "./render-chart-route.js";
51
50
  import { createMemoryBuilder, needsLakebase } from "./memory.js";
52
51
  import { attachRoutePatchMiddleware, MastraServer } from "./server.js";
53
52
  import {
@@ -191,7 +190,6 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
191
190
  modelsPath: `${basePath}/models`,
192
191
  historyPath: `${basePath}/route/history`,
193
192
  historyPathTemplate: `${basePath}/route/history/:agentId`,
194
- renderChartPath: `${basePath}/route/render-chart`,
195
193
  defaultAgent: this.built?.defaultAgentId ?? FALLBACK_AGENT_ID,
196
194
  agents: Object.keys(this.built?.agents ?? {}),
197
195
  };
@@ -251,6 +249,11 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
251
249
  ? createMemoryBuilder(this.config, this.context)
252
250
  : undefined;
253
251
 
252
+ this.log.debug("build:start", {
253
+ lakebase: memoryBuilder !== undefined,
254
+ stripStaleCharts: this.config.stripStaleCharts !== false,
255
+ });
256
+
254
257
  // Build every agent declared in `config.agents` (or the built-in
255
258
  // fallback when none are declared). Each agent's `model` resolves
256
259
  // workspace URL + bearer at call time so concurrent requests get
@@ -280,10 +283,14 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
280
283
  chatRoute({ path: "/route/chat/:agentId" }),
281
284
  historyRoute({ path: "/route/history", agent: this.built.defaultAgentId }),
282
285
  historyRoute({ path: "/route/history/:agentId" }),
283
- renderChartRoute({ path: "/route/render-chart", config: this.config }),
284
286
  ],
285
287
  });
286
288
  await this.mastraServer.init();
289
+ this.log.debug("build:done", {
290
+ agents: Object.keys(this.built.agents),
291
+ defaultAgent: this.built.defaultAgentId,
292
+ routes: ["/route/chat", "/route/history", "/models"],
293
+ });
287
294
  }
288
295
  }
289
296