@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.
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Mastra input processor that strips `chartId` fields from every
3
+ * tool-invocation result in prior assistant messages before they
4
+ * reach the model.
5
+ *
6
+ * Why: chartIds are only meaningful within the assistant turn that
7
+ * minted them - the writer events backing them are gone after the
8
+ * stream closes. When the model sees old chartIds in memory recall
9
+ * (Mastra Memory persists tool results), it's tempted to type
10
+ * those ids into the new turn's `[[chart:<id>]]` markers, leaving
11
+ * the chat client's chart slots stuck with no matching event. This
12
+ * processor removes the temptation by deleting `chartId` keys from
13
+ * every assistant message's tool results before the prompt is
14
+ * built. The current turn's tool results don't exist yet at
15
+ * `processInput` time, so they pass through unmodified.
16
+ *
17
+ * The strip is recursive - any nested `chartId` field is removed,
18
+ * regardless of which tool produced the result. This covers Genie's
19
+ * `datasets[].chartId` and `render_data`'s top-level `chartId`
20
+ * uniformly without coupling to specific tool ids.
21
+ */
22
+ import { logUtils } from "@dbx-tools/appkit-shared";
23
+ const log = logUtils.logger("mastra/processor/strip-stale-charts");
24
+ /**
25
+ * Recursively clone `value`, omitting any property whose key is
26
+ * `chartId`. Arrays are mapped element-wise; primitives are
27
+ * returned as-is. The result is structurally identical to the
28
+ * input minus chartIds, so downstream message-shape consumers
29
+ * keep working.
30
+ */
31
+ function stripChartIds(value) {
32
+ if (Array.isArray(value)) {
33
+ return value.map(stripChartIds);
34
+ }
35
+ if (value && typeof value === "object") {
36
+ const obj = value;
37
+ const out = {};
38
+ for (const [key, val] of Object.entries(obj)) {
39
+ if (key === "chartId")
40
+ continue;
41
+ out[key] = stripChartIds(val);
42
+ }
43
+ return out;
44
+ }
45
+ return value;
46
+ }
47
+ /**
48
+ * Input processor that scrubs `chartId` from every tool-invocation
49
+ * result in the message list. Wired onto every agent by default
50
+ * via {@link buildAgents}; opt out with
51
+ * `MastraPluginConfig.stripStaleCharts: false`.
52
+ */
53
+ export const stripStaleChartsProcessor = {
54
+ id: "strip-stale-charts",
55
+ description: "Removes chartId fields from prior tool-invocation results so the model can't reuse turn-scoped ids from memory.",
56
+ processInput(args) {
57
+ let stripped = 0;
58
+ for (const message of args.messages) {
59
+ if (message.role !== "assistant")
60
+ continue;
61
+ const parts = message.content?.parts;
62
+ if (!Array.isArray(parts))
63
+ continue;
64
+ for (const part of parts) {
65
+ // Tool-invocation parts hold the persisted tool result.
66
+ // We don't scrub the input args (`rawInput` / `args`) because
67
+ // the chartId there is the model's outgoing claim, not
68
+ // anything it could re-reference; only `result` carries
69
+ // ids that subsequent turns might copy.
70
+ if (part.type !== "tool-invocation") {
71
+ continue;
72
+ }
73
+ const inv = part
74
+ .toolInvocation;
75
+ if (!inv || inv.result === undefined)
76
+ continue;
77
+ const before = inv.result;
78
+ const after = stripChartIds(before);
79
+ // Cheap structural check via JSON length - the actual
80
+ // strip writes a fresh object only when chartId keys
81
+ // existed, so different stringification length is a
82
+ // reliable signal that something was removed.
83
+ if (typeof before === "object" &&
84
+ before !== null &&
85
+ JSON.stringify(before).length !== JSON.stringify(after).length) {
86
+ inv.result = after;
87
+ stripped += 1;
88
+ }
89
+ }
90
+ }
91
+ if (stripped > 0) {
92
+ log.debug("stripped", { results: stripped });
93
+ }
94
+ return args.messages;
95
+ },
96
+ };
@@ -31,6 +31,16 @@ export class MastraServer extends MastraServerExpress {
31
31
  this.configureRequestContextUser(requestContext);
32
32
  this.configureRequestContextThreadId(req, res, requestContext);
33
33
  this.configureRequestContextModelOverride(req, requestContext);
34
+ this.log.debug("auth:middleware", {
35
+ method: req.method,
36
+ path: req.path,
37
+ threadId: requestContext.get(MASTRA_THREAD_ID_KEY),
38
+ resourceId: requestContext.get(MASTRA_RESOURCE_ID_KEY),
39
+ modelOverride: requestContext.get(
40
+ // imported below; logged so a misrouted request shows
41
+ // up alongside its model selection in `LOG_LEVEL=debug`.
42
+ "mastra__model_override"),
43
+ });
34
44
  next();
35
45
  });
36
46
  }
@@ -20,8 +20,9 @@
20
20
  * `plugin.ts` exposes the cached list at `GET /models`.
21
21
  */
22
22
  import { CacheManager } from "@databricks/appkit";
23
- import { stringUtils } from "@dbx-tools/appkit-shared";
23
+ import { logUtils, stringUtils } from "@dbx-tools/appkit-shared";
24
24
  import Fuse from "fuse.js";
25
+ const log = logUtils.logger("mastra/serving");
25
26
  /**
26
27
  * `RequestContext` key under which {@link MastraServer} stores the
27
28
  * per-request model override (header / query / body). `model.ts`
@@ -76,6 +77,7 @@ export async function listServingEndpoints(client, host, opts = {}) {
76
77
  return CacheManager.getInstanceSync().getOrExecute([CACHE_KEY_NAMESPACE, host], () => fetchEndpoints(client), SHARED_USER_KEY, { ttl: ttlSec });
77
78
  }
78
79
  async function fetchEndpoints(client) {
80
+ const startedAt = Date.now();
79
81
  const out = [];
80
82
  for await (const ep of client.servingEndpoints.list()) {
81
83
  if (!ep.name)
@@ -87,6 +89,7 @@ async function fetchEndpoints(client) {
87
89
  ...(ep.description !== undefined ? { description: ep.description } : {}),
88
90
  });
89
91
  }
92
+ log.debug("listed", { count: out.length, elapsedMs: Date.now() - startedAt });
90
93
  return out;
91
94
  }
92
95
  /**
@@ -127,10 +130,12 @@ export async function clearServingEndpointsCache(host) {
127
130
  */
128
131
  export function resolveModelId(input, endpoints, opts = {}) {
129
132
  if (endpoints.length === 0) {
133
+ log.debug("resolve:no-endpoints", { input });
130
134
  return { modelId: input, matched: false };
131
135
  }
132
136
  for (const ep of endpoints) {
133
137
  if (ep.name === input) {
138
+ log.debug("resolve:exact", { input });
134
139
  return { modelId: ep.name, matched: true, score: 0 };
135
140
  }
136
141
  }
@@ -148,13 +153,25 @@ export function resolveModelId(input, endpoints, opts = {}) {
148
153
  // lean on the shared tokenizer so the splitting rules stay
149
154
  // consistent with the rest of the toolkit.
150
155
  const query = Array.from(stringUtils.tokenizeWithOptions({ lowerCase: true, camelCase: false }, input)).join(" ");
151
- if (!query)
156
+ if (!query) {
157
+ log.debug("resolve:empty-tokens", { input });
152
158
  return { modelId: input, matched: false };
159
+ }
153
160
  const results = fuse.search(query);
154
161
  const best = results[0];
155
162
  if (best?.item.name && (best.score ?? 0) <= threshold) {
163
+ log.debug("resolve:fuzzy-match", {
164
+ input,
165
+ modelId: best.item.name,
166
+ score: best.score,
167
+ });
156
168
  return { modelId: best.item.name, matched: true, score: best.score };
157
169
  }
170
+ log.debug("resolve:no-match", {
171
+ input,
172
+ bestScore: best?.score,
173
+ threshold,
174
+ });
158
175
  return { modelId: input, matched: false };
159
176
  }
160
177
  /**
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Mastra tool: `send_email`. Gated behind {@link requireApproval}
3
+ * so the model can call it freely but execution is paused until a
4
+ * human approves via the chat UI.
5
+ *
6
+ * The execute body is a stub - it logs the would-be email to the
7
+ * server console (via `logUtils.logger`) and returns success. Swap
8
+ * in a real SMTP / SES / Resend / Workspace Mail call later by
9
+ * editing the `execute` body; the tool surface and approval gate
10
+ * stay the same.
11
+ *
12
+ * Approval flow (Mastra + AI SDK V5):
13
+ *
14
+ * 1. Model calls the tool with `{ to, subject, body, ... }`.
15
+ * 2. Mastra evaluates `requireApproval` (here always `true`),
16
+ * pauses the agent loop, and emits a `tool-call-approval`
17
+ * chunk on the response stream.
18
+ * 3. The chat client renders an approve/deny prompt against the
19
+ * `state: 'approval-requested'` tool part. On approve, it sends
20
+ * a `MastraToolApproval` response back; on deny, the tool call
21
+ * is rejected and the model sees an error.
22
+ * 4. On approve, this `execute` runs and logs the email.
23
+ *
24
+ * The tool is intentionally NOT auto-installed on every agent -
25
+ * email is domain-specific, not infrastructure. Spread it into the
26
+ * specific agents that should be able to draft emails.
27
+ */
28
+ import { z } from "zod";
29
+ declare const emailInputSchema: z.ZodObject<{
30
+ to: z.ZodString;
31
+ subject: z.ZodString;
32
+ body: z.ZodString;
33
+ cc: z.ZodOptional<z.ZodArray<z.ZodString>>;
34
+ bcc: z.ZodOptional<z.ZodArray<z.ZodString>>;
35
+ }, z.core.$strip>;
36
+ /** Options accepted by {@link buildEmailTool}. */
37
+ export interface BuildEmailToolOptions {
38
+ /**
39
+ * Override the tool id. Defaults to `"send_email"`. Useful if a
40
+ * caller wants `send_internal_email` / `send_external_email`
41
+ * variants.
42
+ */
43
+ id?: string;
44
+ /**
45
+ * Replace the default execute body with a real provider call.
46
+ * Receives the validated input and must return `{sent, recipient}`.
47
+ * The console-log default is meant for demos / dev; production
48
+ * deployments should wire SMTP / SES / Resend / Workspace Mail
49
+ * here.
50
+ */
51
+ send?: (input: z.infer<typeof emailInputSchema>) => Promise<void> | void;
52
+ }
53
+ /**
54
+ * Build the `send_email` tool. Approval-gated by default; the
55
+ * execute body either calls the supplied {@link send} hook or
56
+ * logs the email to the server console as a demo stub.
57
+ *
58
+ * @example
59
+ * ```ts
60
+ * import { buildEmailTool, createAgent, mastra } from "@dbx-tools/appkit-mastra";
61
+ *
62
+ * const support = createAgent({
63
+ * instructions: "...",
64
+ * tools(plugins) {
65
+ * return {
66
+ * ...(plugins.genie?.toolkit() ?? {}),
67
+ * send_email: buildEmailTool(),
68
+ * };
69
+ * },
70
+ * });
71
+ * ```
72
+ */
73
+ export declare function buildEmailTool(opts?: BuildEmailToolOptions): import("@mastra/core/tools").Tool<any, any, any, any, import("@mastra/core/tools").ToolExecutionContext<any, any, unknown>, string, unknown>;
74
+ export {};
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Mastra tool: `send_email`. Gated behind {@link requireApproval}
3
+ * so the model can call it freely but execution is paused until a
4
+ * human approves via the chat UI.
5
+ *
6
+ * The execute body is a stub - it logs the would-be email to the
7
+ * server console (via `logUtils.logger`) and returns success. Swap
8
+ * in a real SMTP / SES / Resend / Workspace Mail call later by
9
+ * editing the `execute` body; the tool surface and approval gate
10
+ * stay the same.
11
+ *
12
+ * Approval flow (Mastra + AI SDK V5):
13
+ *
14
+ * 1. Model calls the tool with `{ to, subject, body, ... }`.
15
+ * 2. Mastra evaluates `requireApproval` (here always `true`),
16
+ * pauses the agent loop, and emits a `tool-call-approval`
17
+ * chunk on the response stream.
18
+ * 3. The chat client renders an approve/deny prompt against the
19
+ * `state: 'approval-requested'` tool part. On approve, it sends
20
+ * a `MastraToolApproval` response back; on deny, the tool call
21
+ * is rejected and the model sees an error.
22
+ * 4. On approve, this `execute` runs and logs the email.
23
+ *
24
+ * The tool is intentionally NOT auto-installed on every agent -
25
+ * email is domain-specific, not infrastructure. Spread it into the
26
+ * specific agents that should be able to draft emails.
27
+ */
28
+ import { logUtils, stringUtils } from "@dbx-tools/appkit-shared";
29
+ import { createTool } from "@mastra/core/tools";
30
+ import { z } from "zod";
31
+ const log = logUtils.logger("mastra/tool/send-email");
32
+ const emailInputSchema = z.object({
33
+ to: z.string().describe(stringUtils.toDescription `
34
+ Single recipient email address (e.g. "alice@example.com"). For
35
+ multiple recipients, comma-separate them yourself.
36
+ `),
37
+ subject: z.string().describe(stringUtils.toDescription `
38
+ Subject line.
39
+ `),
40
+ body: z.string().describe(stringUtils.toDescription `
41
+ Email body. Plain text or markdown; the renderer downstream
42
+ decides which to honour. Be specific - the recipient may not
43
+ have any context the model has from prior chat turns.
44
+ `),
45
+ cc: z
46
+ .array(z.string())
47
+ .optional()
48
+ .describe(stringUtils.toDescription `
49
+ Optional CC recipients.
50
+ `),
51
+ bcc: z
52
+ .array(z.string())
53
+ .optional()
54
+ .describe(stringUtils.toDescription `
55
+ Optional BCC recipients.
56
+ `),
57
+ });
58
+ const emailOutputSchema = z.object({
59
+ sent: z.boolean().describe(stringUtils.toDescription `
60
+ True when the email was dispatched. The current implementation
61
+ always returns true after console-logging the would-be email;
62
+ swap in a real provider to make this meaningful.
63
+ `),
64
+ recipient: z.string().describe(stringUtils.toDescription `
65
+ Echo of the \`to\` field for confirmation.
66
+ `),
67
+ });
68
+ /**
69
+ * Build the `send_email` tool. Approval-gated by default; the
70
+ * execute body either calls the supplied {@link send} hook or
71
+ * logs the email to the server console as a demo stub.
72
+ *
73
+ * @example
74
+ * ```ts
75
+ * import { buildEmailTool, createAgent, mastra } from "@dbx-tools/appkit-mastra";
76
+ *
77
+ * const support = createAgent({
78
+ * instructions: "...",
79
+ * tools(plugins) {
80
+ * return {
81
+ * ...(plugins.genie?.toolkit() ?? {}),
82
+ * send_email: buildEmailTool(),
83
+ * };
84
+ * },
85
+ * });
86
+ * ```
87
+ */
88
+ export function buildEmailTool(opts = {}) {
89
+ return createTool({
90
+ id: opts.id ?? "send_email",
91
+ description: stringUtils.toDescription `
92
+ Send an email on the user's behalf. Pass a recipient
93
+ address, subject, and body; the user will be prompted to
94
+ approve the send before it goes out (the tool is
95
+ approval-gated). Use this when the user explicitly asks
96
+ to send / forward / share something via email - never
97
+ autonomously. Keep subjects short and bodies focused; the
98
+ recipient may not have any of the chat context.
99
+ `,
100
+ inputSchema: emailInputSchema,
101
+ outputSchema: emailOutputSchema,
102
+ requireApproval: true,
103
+ execute: async (input) => {
104
+ const { to, subject, body, cc, bcc } = input;
105
+ // Default behaviour: dump the email to the server console so
106
+ // demos can see the gate fire end-to-end without a real
107
+ // provider. Replace by passing `opts.send`.
108
+ log.info("send", {
109
+ to,
110
+ ...(cc && cc.length > 0 ? { cc } : {}),
111
+ ...(bcc && bcc.length > 0 ? { bcc } : {}),
112
+ subject,
113
+ bodyLength: body.length,
114
+ body,
115
+ });
116
+ if (opts.send) {
117
+ await opts.send(input);
118
+ }
119
+ return { sent: true, recipient: to };
120
+ },
121
+ });
122
+ }