@dbx-tools/appkit-mastra 0.1.4 → 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.
@@ -27,55 +27,69 @@ export class MastraServer extends MastraServerExpress {
27
27
  registerAuthMiddleware() {
28
28
  super.registerAuthMiddleware();
29
29
  this.app.use((req, res, next) => {
30
- const executionContext = getExecutionContext();
31
- const user = {
32
- id: "userId" in executionContext
33
- ? executionContext.userId
34
- : executionContext.serviceUserId,
35
- executionContext,
36
- };
37
30
  const requestContext = res.locals.requestContext;
38
- requestContext.set(MASTRA_USER_KEY, user);
39
- if (!requestContext.get(MASTRA_RESOURCE_ID_KEY)) {
40
- this.log.debug(`Setting resource id: ${user.id}`);
41
- requestContext.set(MASTRA_RESOURCE_ID_KEY, user.id);
42
- }
43
- const cookies = httpUtils.parseCookies(req.headers.cookie);
44
- const cookieName = stringUtils.toIdentifierWithOptions({ delimiter: "_", distinct: true }, "appkit", this.config.name, "sessionId");
45
- let sessionId = cookies[cookieName];
46
- if (!sessionId) {
47
- sessionId = randomUUID();
48
- res.cookie(cookieName, sessionId, {
49
- httpOnly: true,
50
- sameSite: "lax",
51
- secure: req.secure,
52
- path: "/",
53
- });
54
- }
55
- res.locals.sessionId = sessionId;
56
- if (!requestContext.get(MASTRA_THREAD_ID_KEY)) {
57
- this.log.debug(`Setting thread id: ${sessionId}`);
58
- requestContext.set(MASTRA_THREAD_ID_KEY, sessionId);
59
- }
60
- // Per-request model override: only honored when the plugin
61
- // opts in (default). Sources, in priority order, are
62
- // `X-Mastra-Model` header, `?model=` query, and `model` /
63
- // `modelId` body field; see `serving.ts`.
64
- const serving = resolveServingConfig(this.config);
65
- if (serving.allowOverride) {
66
- const override = extractModelOverride({
67
- headers: req.headers,
68
- query: req.query,
69
- body: req.body,
70
- });
71
- if (override) {
72
- this.log.debug(`Model override: ${override}`);
73
- requestContext.set(MASTRA_MODEL_OVERRIDE_KEY, override);
74
- }
75
- }
31
+ this.configureRequestContextUser(requestContext);
32
+ this.configureRequestContextThreadId(req, res, requestContext);
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
+ });
76
44
  next();
77
45
  });
78
46
  }
47
+ configureRequestContextUser(requestContext) {
48
+ if ([MASTRA_USER_KEY, MASTRA_RESOURCE_ID_KEY].every((key) => requestContext.get(key)))
49
+ return;
50
+ const executionContext = getExecutionContext();
51
+ const user = {
52
+ id: "userId" in executionContext
53
+ ? executionContext.userId
54
+ : executionContext.serviceUserId,
55
+ executionContext,
56
+ };
57
+ requestContext.set(MASTRA_USER_KEY, user);
58
+ requestContext.set(MASTRA_RESOURCE_ID_KEY, user.id);
59
+ }
60
+ configureRequestContextThreadId(req, res, requestContext) {
61
+ if (requestContext.get(MASTRA_THREAD_ID_KEY))
62
+ return;
63
+ const cookies = httpUtils.parseCookies(req.headers.cookie);
64
+ const cookieName = stringUtils.toIdentifierWithOptions({ delimiter: "_", distinct: true }, "appkit", this.config.name, "sessionId");
65
+ let sessionId = cookies[cookieName];
66
+ if (!sessionId) {
67
+ sessionId = randomUUID();
68
+ res.cookie(cookieName, sessionId, {
69
+ httpOnly: true,
70
+ sameSite: "lax",
71
+ secure: req.secure,
72
+ path: "/",
73
+ });
74
+ }
75
+ requestContext.set(MASTRA_THREAD_ID_KEY, sessionId);
76
+ }
77
+ configureRequestContextModelOverride(req, requestContext) {
78
+ // Per-request model override: only honored when the plugin
79
+ // opts in (default). Sources, in priority order, are
80
+ // `X-Mastra-Model` header, `?model=` query, and `model` /
81
+ // `modelId` body field; see `serving.ts`.
82
+ const serving = resolveServingConfig(this.config);
83
+ if (serving.allowOverride) {
84
+ const override = extractModelOverride({
85
+ headers: req.headers,
86
+ query: req.query,
87
+ body: req.body,
88
+ });
89
+ if (override)
90
+ requestContext.set(MASTRA_MODEL_OVERRIDE_KEY, override);
91
+ }
92
+ }
79
93
  }
80
94
  /**
81
95
  * Patches around `@mastra/express`'s custom-route dispatcher so
@@ -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
+ }