@dbx-tools/appkit-mastra 0.1.4 → 0.1.5

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,158 @@
1
+ /**
2
+ * Thread history loader exposed as a Mastra custom API route.
3
+ *
4
+ * Backed entirely by native Mastra: looks up the active agent by id,
5
+ * asks its `Memory` instance to `recall` a page of `MastraDBMessage`s,
6
+ * and converts the result to AI SDK V5 `UIMessage`s with the official
7
+ * {@link toAISdkV5Messages} helper from `@mastra/ai-sdk/ui`. No direct
8
+ * database reads.
9
+ *
10
+ * The route is registered through {@link historyRoute} as a Mastra
11
+ * `registerApiRoute` so it sits in the same dispatcher pipeline as
12
+ * `chatRoute`. That means the `MastraServer` auth middleware (in
13
+ * `./server.ts`) has already populated `RequestContext` with
14
+ * `MASTRA_THREAD_ID_KEY` and `MASTRA_RESOURCE_ID_KEY` by the time
15
+ * the handler runs - no cookie or user lookups happen here, and the
16
+ * session-cookie logic stays the single source of truth in `server.ts`.
17
+ */
18
+ import { toAISdkV5Messages } from "@mastra/ai-sdk/ui";
19
+ import { MASTRA_RESOURCE_ID_KEY, MASTRA_THREAD_ID_KEY, } from "@mastra/core/request-context";
20
+ import { registerApiRoute } from "@mastra/core/server";
21
+ /** Default history page size; matches the Mastra storage default. */
22
+ const DEFAULT_PER_PAGE = 20;
23
+ /** Hard cap so a misbehaving client can't fetch the whole thread at once. */
24
+ const MAX_PER_PAGE = 200;
25
+ /**
26
+ * Fetch a page of UI-formatted messages for a thread.
27
+ *
28
+ * Uses the agent's resolved `Memory` (`getMemory()`) so per-agent
29
+ * storage namespaces (`mastra_<agentId>` schemas) and any future
30
+ * memory-side filters apply automatically. When the agent has no
31
+ * memory configured the response is a successful empty page so
32
+ * callers don't have to special-case stateless agents.
33
+ *
34
+ * Pagination is descending-by-default: page 0 is the most recent
35
+ * page, page 1 the page before that, etc. The returned `uiMessages`
36
+ * are always re-sorted into chronological order (oldest -> newest)
37
+ * so the client can prepend them above the existing transcript
38
+ * without sorting locally.
39
+ */
40
+ export async function loadHistory(opts) {
41
+ const perPage = clampPerPage(opts.perPage);
42
+ const page = Math.max(0, Math.trunc(opts.page ?? 0));
43
+ const memory = await opts.agent.getMemory();
44
+ if (!memory) {
45
+ return { uiMessages: [], page, perPage, total: 0, hasMore: false };
46
+ }
47
+ const result = await memory.recall({
48
+ threadId: opts.threadId,
49
+ ...(opts.resourceId ? { resourceId: opts.resourceId } : {}),
50
+ page,
51
+ perPage,
52
+ orderBy: {
53
+ field: "createdAt",
54
+ direction: opts.ascending ? "ASC" : "DESC",
55
+ },
56
+ });
57
+ const chronological = sortChronological(result.messages);
58
+ const uiMessages = toAISdkV5Messages(chronological);
59
+ return {
60
+ uiMessages,
61
+ page,
62
+ perPage,
63
+ total: result.total,
64
+ hasMore: result.hasMore,
65
+ };
66
+ }
67
+ /**
68
+ * Register a `GET <path>` Mastra custom API route that returns a page
69
+ * of AI SDK V5 `UIMessage`s for the caller's current thread.
70
+ *
71
+ * Modeled after `chatRoute` from `@mastra/ai-sdk`: pass `agent` for a
72
+ * fixed-agent mount, or include `:agentId` in the path for dynamic
73
+ * routing. Pairs cleanly with the AppKit Mastra plugin's chat route
74
+ * layout (`/route/chat` + `/route/chat/:agentId`).
75
+ *
76
+ * The handler reads `threadId` and `resourceId` from `RequestContext`
77
+ * (populated upstream by `MastraServer.registerAuthMiddleware`), so
78
+ * no cookie or user lookups happen here.
79
+ */
80
+ export function historyRoute(options) {
81
+ const { path } = options;
82
+ const fixedAgent = "agent" in options ? options.agent : undefined;
83
+ if (!fixedAgent && !path.includes(":agentId")) {
84
+ throw new Error("historyRoute path must include `:agentId` or `agent` must be passed explicitly");
85
+ }
86
+ return registerApiRoute(path, {
87
+ method: "GET",
88
+ handler: async (c) => {
89
+ const mastra = c.get("mastra");
90
+ const requestContext = c.get("requestContext");
91
+ const agentId = fixedAgent ?? c.req.param("agentId");
92
+ if (!agentId) {
93
+ return c.json({ error: "agentId is required" }, 400);
94
+ }
95
+ const agent = mastra.getAgentById(agentId);
96
+ if (!agent) {
97
+ return c.json({ error: `Unknown agent "${agentId}"` }, 404);
98
+ }
99
+ const threadId = requestContext.get(MASTRA_THREAD_ID_KEY);
100
+ if (!threadId) {
101
+ return c.json({ error: "thread id missing from request context" }, 400);
102
+ }
103
+ const resourceId = requestContext.get(MASTRA_RESOURCE_ID_KEY);
104
+ const payload = await loadHistory({
105
+ agent,
106
+ threadId,
107
+ ...(resourceId ? { resourceId } : {}),
108
+ page: parseIntParam(c.req.query("page")),
109
+ perPage: parseIntParam(c.req.query("perPage")),
110
+ });
111
+ return c.json(payload);
112
+ },
113
+ });
114
+ }
115
+ /** Coerce / clamp `perPage`; falls back to the page-size default. */
116
+ function clampPerPage(value) {
117
+ if (value === undefined || Number.isNaN(value))
118
+ return DEFAULT_PER_PAGE;
119
+ const n = Math.trunc(value);
120
+ if (n <= 0)
121
+ return DEFAULT_PER_PAGE;
122
+ return Math.min(n, MAX_PER_PAGE);
123
+ }
124
+ /**
125
+ * Sort messages oldest-first by `createdAt`, falling back to whatever
126
+ * order the storage returned them in. The native `recall` call honors
127
+ * `orderBy` but doesn't guarantee a stable secondary sort, so we
128
+ * normalize here before handing the page to the AI SDK converter.
129
+ */
130
+ function sortChronological(messages) {
131
+ return [...messages].sort((a, b) => {
132
+ const ta = toEpoch(a.createdAt);
133
+ const tb = toEpoch(b.createdAt);
134
+ return ta - tb;
135
+ });
136
+ }
137
+ function toEpoch(value) {
138
+ if (value instanceof Date)
139
+ return value.getTime();
140
+ if (typeof value === "string" || typeof value === "number") {
141
+ const parsed = new Date(value).getTime();
142
+ return Number.isNaN(parsed) ? 0 : parsed;
143
+ }
144
+ return 0;
145
+ }
146
+ /**
147
+ * Coerce a Hono query value into a non-negative integer. Returns
148
+ * `undefined` for empty / non-numeric / negative inputs so
149
+ * {@link loadHistory} can apply its built-in defaults.
150
+ */
151
+ function parseIntParam(value) {
152
+ if (!value)
153
+ return undefined;
154
+ const n = Number(value);
155
+ if (!Number.isFinite(n) || n < 0)
156
+ return undefined;
157
+ return Math.trunc(n);
158
+ }
@@ -109,6 +109,16 @@ export declare class MastraPlugin extends Plugin<MastraPluginConfig> {
109
109
  };
110
110
  clientConfig(): Record<string, unknown>;
111
111
  injectRoutes(router: IAppRouter): void;
112
+ /**
113
+ * Return `this.asUser(req)` when the request carries an OBO token,
114
+ * otherwise return `this` directly. Prevents the noisy AppKit warn
115
+ * (`asUser() called without user token in development mode. Skipping
116
+ * user impersonation.`) on every request in local dev where the
117
+ * browser never sends `x-forwarded-access-token`. Behavior is
118
+ * unchanged in production: a missing token always means a real OBO
119
+ * proxy call (and AppKit will throw upstream if that's wrong).
120
+ */
121
+ private userScopedSelf;
112
122
  /**
113
123
  * Implementation backing both the `/models` route and the
114
124
  * `listModels` export. Runs inside the AppKit user-context proxy so
@@ -32,6 +32,8 @@ import { chatRoute } from "@mastra/ai-sdk";
32
32
  import { Mastra } from "@mastra/core/mastra";
33
33
  import express from "express";
34
34
  import { buildAgents, FALLBACK_AGENT_ID } from "./agents.js";
35
+ import { historyRoute } from "./history.js";
36
+ import { renderChartRoute } from "./render-chart-route.js";
35
37
  import { createMemoryBuilder, needsLakebase } from "./memory.js";
36
38
  import { attachRoutePatchMiddleware, MastraServer } from "./server.js";
37
39
  import { clearServingEndpointsCache, listServingEndpoints, resolveServingConfig, } from "./serving.js";
@@ -159,6 +161,9 @@ export class MastraPlugin extends Plugin {
159
161
  chatPath: `${basePath}/route/chat`,
160
162
  chatPathTemplate: `${basePath}/route/chat/:agentId`,
161
163
  modelsPath: `${basePath}/models`,
164
+ historyPath: `${basePath}/route/history`,
165
+ historyPathTemplate: `${basePath}/route/history/:agentId`,
166
+ renderChartPath: `${basePath}/route/render-chart`,
162
167
  defaultAgent: this.built?.defaultAgentId ?? FALLBACK_AGENT_ID,
163
168
  agents: Object.keys(this.built?.agents ?? {}),
164
169
  };
@@ -171,7 +176,7 @@ export class MastraPlugin extends Plugin {
171
176
  // the Mastra subapp. Errors propagate to Express's default error
172
177
  // handler via `next(err)` so callers see the real SDK message.
173
178
  router.get("/models", (req, res, next) => {
174
- this.asUser(req)
179
+ this.userScopedSelf(req)
175
180
  .listModels()
176
181
  .then((endpoints) => res.json({ endpoints }))
177
182
  .catch(next);
@@ -179,9 +184,21 @@ export class MastraPlugin extends Plugin {
179
184
  router.use("", (req, res, next) => {
180
185
  if (!this.mastraApp)
181
186
  return res.status(503).end();
182
- return this.asUser(req).mastraApp(req, res, next);
187
+ return this.userScopedSelf(req).mastraApp(req, res, next);
183
188
  });
184
189
  }
190
+ /**
191
+ * Return `this.asUser(req)` when the request carries an OBO token,
192
+ * otherwise return `this` directly. Prevents the noisy AppKit warn
193
+ * (`asUser() called without user token in development mode. Skipping
194
+ * user impersonation.`) on every request in local dev where the
195
+ * browser never sends `x-forwarded-access-token`. Behavior is
196
+ * unchanged in production: a missing token always means a real OBO
197
+ * proxy call (and AppKit will throw upstream if that's wrong).
198
+ */
199
+ userScopedSelf(req) {
200
+ return req.header("x-forwarded-access-token") ? this.asUser(req) : this;
201
+ }
185
202
  /**
186
203
  * Implementation backing both the `/models` route and the
187
204
  * `listModels` export. Runs inside the AppKit user-context proxy so
@@ -227,6 +244,9 @@ export class MastraPlugin extends Plugin {
227
244
  customApiRoutes: [
228
245
  chatRoute({ path: "/route/chat", agent: this.built.defaultAgentId }),
229
246
  chatRoute({ path: "/route/chat/:agentId" }),
247
+ historyRoute({ path: "/route/history", agent: this.built.defaultAgentId }),
248
+ historyRoute({ path: "/route/history/:agentId" }),
249
+ renderChartRoute({ path: "/route/render-chart", config: this.config }),
230
250
  ],
231
251
  });
232
252
  await this.mastraServer.init();
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Chart-render HTTP endpoint for the Mastra plugin.
3
+ *
4
+ * The `render_data` tool returns immediately with a `chartId` and
5
+ * emits the dataset over `ctx.writer`; the client then POSTs that
6
+ * dataset to this endpoint to actually run the chart-planner
7
+ * agent and get back an Echarts `EChartsOption` JSON. Planning
8
+ * happens out-of-band so the calling agent's response stream
9
+ * doesn't sit idle waiting for it - the model can finish the
10
+ * report while the client is still rendering charts.
11
+ *
12
+ * Auth flows through the standard Mastra middleware: the route
13
+ * sits in the same dispatcher pipeline as `chatRoute` /
14
+ * `historyRoute`, so by the time the handler runs the
15
+ * `RequestContext` is populated with the workspace user and
16
+ * the chart-planner's model resolver has the OBO token it
17
+ * needs.
18
+ */
19
+ import type { MastraPluginConfig } from "./config.js";
20
+ /** Options accepted by {@link renderChartRoute}. */
21
+ export interface RenderChartRouteOptions {
22
+ path: string;
23
+ config: MastraPluginConfig;
24
+ }
25
+ /**
26
+ * Register a `POST <path>` Mastra custom API route that runs the
27
+ * chart-planner agent against a dataset and returns an Echarts
28
+ * `EChartsOption` JSON.
29
+ *
30
+ * Body shape: {@link RenderChartRequest}; response:
31
+ * {@link RenderChartResponse}.
32
+ */
33
+ export declare function renderChartRoute(options: RenderChartRouteOptions): import("@mastra/core/server").ApiRoute;
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Chart-render HTTP endpoint for the Mastra plugin.
3
+ *
4
+ * The `render_data` tool returns immediately with a `chartId` and
5
+ * emits the dataset over `ctx.writer`; the client then POSTs that
6
+ * dataset to this endpoint to actually run the chart-planner
7
+ * agent and get back an Echarts `EChartsOption` JSON. Planning
8
+ * happens out-of-band so the calling agent's response stream
9
+ * doesn't sit idle waiting for it - the model can finish the
10
+ * report while the client is still rendering charts.
11
+ *
12
+ * Auth flows through the standard Mastra middleware: the route
13
+ * sits in the same dispatcher pipeline as `chatRoute` /
14
+ * `historyRoute`, so by the time the handler runs the
15
+ * `RequestContext` is populated with the workspace user and
16
+ * the chart-planner's model resolver has the OBO token it
17
+ * needs.
18
+ */
19
+ import { registerApiRoute } from "@mastra/core/server";
20
+ import { runChartPlanner } from "./chart.js";
21
+ /** Hard cap so a misbehaving client can't hand us a million-row payload. */
22
+ const MAX_ROWS = 5_000;
23
+ /**
24
+ * Hard cap on the JSON body the route accepts (in bytes). Mirrors
25
+ * the same intent as {@link MAX_ROWS}: bound the chart-planner's
26
+ * prompt size and protect against accidental denial-of-service
27
+ * from a runaway tool that ships an enormous payload.
28
+ */
29
+ const MAX_BODY_BYTES = 2 * 1024 * 1024;
30
+ /**
31
+ * Register a `POST <path>` Mastra custom API route that runs the
32
+ * chart-planner agent against a dataset and returns an Echarts
33
+ * `EChartsOption` JSON.
34
+ *
35
+ * Body shape: {@link RenderChartRequest}; response:
36
+ * {@link RenderChartResponse}.
37
+ */
38
+ export function renderChartRoute(options) {
39
+ const { path, config } = options;
40
+ return registerApiRoute(path, {
41
+ method: "POST",
42
+ handler: async (c) => {
43
+ const requestContext = c.get("requestContext");
44
+ // Hono parses the body as JSON; we still validate shape /
45
+ // size since the tool's structured output is a contract,
46
+ // not a guarantee, and the route is publicly mountable.
47
+ const raw = (await c.req.json().catch(() => null));
48
+ const validation = validateBody(raw);
49
+ if ("error" in validation) {
50
+ return c.json({ error: validation.error }, 400);
51
+ }
52
+ const { title, description, data } = validation.body;
53
+ try {
54
+ const result = await runChartPlanner({
55
+ config,
56
+ ...(requestContext ? { requestContext } : {}),
57
+ title,
58
+ ...(description ? { description } : {}),
59
+ data,
60
+ });
61
+ const payload = {
62
+ option: result.option,
63
+ chartType: result.chartType,
64
+ };
65
+ return c.json(payload);
66
+ }
67
+ catch (err) {
68
+ const message = err instanceof Error ? err.message : String(err);
69
+ return c.json({ error: message }, 500);
70
+ }
71
+ },
72
+ });
73
+ }
74
+ /**
75
+ * Best-effort body validation. Surfaces a 400 for malformed input
76
+ * instead of letting a downstream `.map` / `.length` blow up
77
+ * inside the planner agent. Field-level shape mirrors
78
+ * {@link RenderChartRequest}.
79
+ */
80
+ function validateBody(raw) {
81
+ if (!raw || typeof raw !== "object") {
82
+ return { error: "request body must be a JSON object" };
83
+ }
84
+ const r = raw;
85
+ const title = r.title;
86
+ if (typeof title !== "string" || title.length === 0) {
87
+ return { error: "`title` must be a non-empty string" };
88
+ }
89
+ if (r.description !== undefined && typeof r.description !== "string") {
90
+ return { error: "`description` must be a string when provided" };
91
+ }
92
+ if (!Array.isArray(r.data)) {
93
+ return { error: "`data` must be an array of row objects" };
94
+ }
95
+ if (r.data.length === 0) {
96
+ return { error: "`data` must contain at least one row" };
97
+ }
98
+ if (r.data.length > MAX_ROWS) {
99
+ return { error: `\`data\` exceeds the per-request limit of ${MAX_ROWS} rows` };
100
+ }
101
+ for (const [i, row] of r.data.entries()) {
102
+ if (!row || typeof row !== "object" || Array.isArray(row)) {
103
+ return { error: `data[${i}] must be a plain object` };
104
+ }
105
+ }
106
+ // Approximate body-size check; spares us pulling Buffer in.
107
+ const approximateBytes = JSON.stringify(r.data).length;
108
+ if (approximateBytes > MAX_BODY_BYTES) {
109
+ return {
110
+ error: `\`data\` exceeds the per-request size limit of ${MAX_BODY_BYTES} bytes`,
111
+ };
112
+ }
113
+ return {
114
+ body: {
115
+ title,
116
+ ...(typeof r.description === "string" ? { description: r.description } : {}),
117
+ data: r.data,
118
+ },
119
+ };
120
+ }
@@ -4,6 +4,7 @@
4
4
  * that lets `@mastra/ai-sdk` `chatRoute` work behind an Express mount
5
5
  * point.
6
6
  */
7
+ import { type RequestContext } from "@mastra/core/request-context";
7
8
  import { MastraServer as MastraServerExpress } from "@mastra/express";
8
9
  import type express from "express";
9
10
  import { type MastraPluginConfig } from "./config.js";
@@ -17,6 +18,9 @@ export declare class MastraServer extends MastraServerExpress {
17
18
  private log;
18
19
  constructor(config: MastraPluginConfig, ...args: ConstructorParameters<typeof MastraServerExpress>);
19
20
  registerAuthMiddleware(): void;
21
+ configureRequestContextUser(requestContext: RequestContext): void;
22
+ configureRequestContextThreadId(req: express.Request, res: express.Response, requestContext: RequestContext): void;
23
+ configureRequestContextModelOverride(req: express.Request, requestContext: RequestContext): void;
20
24
  }
21
25
  /**
22
26
  * Patches around `@mastra/express`'s custom-route dispatcher so
@@ -27,55 +27,59 @@ 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);
76
34
  next();
77
35
  });
78
36
  }
37
+ configureRequestContextUser(requestContext) {
38
+ if ([MASTRA_USER_KEY, MASTRA_RESOURCE_ID_KEY].every((key) => requestContext.get(key)))
39
+ return;
40
+ const executionContext = getExecutionContext();
41
+ const user = {
42
+ id: "userId" in executionContext
43
+ ? executionContext.userId
44
+ : executionContext.serviceUserId,
45
+ executionContext,
46
+ };
47
+ requestContext.set(MASTRA_USER_KEY, user);
48
+ requestContext.set(MASTRA_RESOURCE_ID_KEY, user.id);
49
+ }
50
+ configureRequestContextThreadId(req, res, requestContext) {
51
+ if (requestContext.get(MASTRA_THREAD_ID_KEY))
52
+ return;
53
+ const cookies = httpUtils.parseCookies(req.headers.cookie);
54
+ const cookieName = stringUtils.toIdentifierWithOptions({ delimiter: "_", distinct: true }, "appkit", this.config.name, "sessionId");
55
+ let sessionId = cookies[cookieName];
56
+ if (!sessionId) {
57
+ sessionId = randomUUID();
58
+ res.cookie(cookieName, sessionId, {
59
+ httpOnly: true,
60
+ sameSite: "lax",
61
+ secure: req.secure,
62
+ path: "/",
63
+ });
64
+ }
65
+ requestContext.set(MASTRA_THREAD_ID_KEY, sessionId);
66
+ }
67
+ configureRequestContextModelOverride(req, requestContext) {
68
+ // Per-request model override: only honored when the plugin
69
+ // opts in (default). Sources, in priority order, are
70
+ // `X-Mastra-Model` header, `?model=` query, and `model` /
71
+ // `modelId` body field; see `serving.ts`.
72
+ const serving = resolveServingConfig(this.config);
73
+ if (serving.allowOverride) {
74
+ const override = extractModelOverride({
75
+ headers: req.headers,
76
+ query: req.query,
77
+ body: req.body,
78
+ });
79
+ if (override)
80
+ requestContext.set(MASTRA_MODEL_OVERRIDE_KEY, override);
81
+ }
82
+ }
79
83
  }
80
84
  /**
81
85
  * Patches around `@mastra/express`'s custom-route dispatcher so
package/index.ts CHANGED
@@ -13,6 +13,7 @@ export * from "./src/plugin.js";
13
13
  export * from "@dbx-tools/appkit-mastra-shared";
14
14
  export * from "./src/config.js";
15
15
  export * from "./src/agents.js";
16
+ export * from "./src/chart.js";
16
17
  export * from "./src/genie.js";
17
18
  export {
18
19
  clearServingEndpointsCache,
package/package.json CHANGED
@@ -28,12 +28,12 @@
28
28
  "directory": "packages/mastra"
29
29
  },
30
30
  "name": "@dbx-tools/appkit-mastra",
31
- "version": "0.1.4",
31
+ "version": "0.1.5",
32
32
  "module": "index.ts",
33
33
  "type": "module",
34
34
  "dependencies": {
35
- "@dbx-tools/appkit-shared": "0.1.3",
36
- "@dbx-tools/appkit-mastra-shared": "0.1.3",
35
+ "@dbx-tools/appkit-mastra-shared": "0.1.5",
36
+ "@dbx-tools/appkit-shared": "0.1.5",
37
37
  "@mastra/ai-sdk": "^1.3",
38
38
  "@mastra/core": "^1.32",
39
39
  "@mastra/express": "^1.3",
package/src/agents.ts CHANGED
@@ -21,6 +21,7 @@ import { createTool } from "@mastra/core/tools";
21
21
  import type { Tool } from "@mastra/core/tools";
22
22
  import type { PgVectorConfig, PostgresStoreConfig } from "@mastra/pg";
23
23
 
24
+ import { buildRenderDataTool } from "./chart.js";
24
25
  import type { MastraPluginConfig } from "./config.js";
25
26
  import { buildGenieProvider } from "./genie.js";
26
27
  import type { MemoryBuilder } from "./memory.js";
@@ -344,16 +345,21 @@ Rules:
344
345
  * Override globally via {@link MastraPluginConfig.styleInstructions}
345
346
  * (pass `false` to disable entirely, or a string to replace).
346
347
  */
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.`;
348
+ export const DEFAULT_STYLE_INSTRUCTIONS = [
349
+ "Output style:",
350
+ "",
351
+ "Use markdown formatting, including headings, lists, and code blocks.",
352
+ "Avoid lists and headers for short replies.",
353
+ "Plain prose.",
354
+ "Use hyphens (-) only. Never use em dashes or en dashes.",
355
+ "Never use emojis.",
356
+ "Skip openers like 'Great question', 'Absolutely', and 'I'd be happy to help'.",
357
+ "Skip closers like 'Let me know if you have any questions'.",
358
+ "Skip self-disclaimers like 'I should mention' and 'It's important to note'.",
359
+ "Answer directly.",
360
+ "Do not include a preamble before the actual answer.",
361
+ "Use lists and headers only when they clarify a multi-part answer.",
362
+ ].join("\n");
357
363
 
358
364
  /**
359
365
  * Resolve the style block to append to every agent's instructions.
@@ -364,6 +370,7 @@ function resolveStyleInstructions(config: MastraPluginConfig): string | null {
364
370
  if (typeof config.styleInstructions === "string") {
365
371
  return config.styleInstructions;
366
372
  }
373
+
367
374
  return DEFAULT_STYLE_INSTRUCTIONS;
368
375
  }
369
376
 
@@ -371,10 +378,7 @@ function resolveStyleInstructions(config: MastraPluginConfig): string | null {
371
378
  * Join an agent's bespoke instructions with the resolved style block.
372
379
  * Returns the bespoke text unchanged when the style block is disabled.
373
380
  */
374
- function composeInstructions(
375
- agentInstructions: string,
376
- style: string | null,
377
- ): string {
381
+ function composeInstructions(agentInstructions: string, style: string | null): string {
378
382
  if (!style) return agentInstructions;
379
383
  return `${agentInstructions.trimEnd()}\n\n${style}`;
380
384
  }
@@ -404,7 +408,15 @@ export async function buildAgents(opts: {
404
408
  const defaultAgentId = config.defaultAgent ?? ids[0] ?? FALLBACK_AGENT_ID;
405
409
 
406
410
  const plugins = buildPluginsMap(context);
407
- const ambientTools = config.tools ?? {};
411
+ // System-default ambient tools every agent gets out of the box.
412
+ // Currently just `render_data` for inline visualizations; the
413
+ // user can shadow it by including a same-named tool in their own
414
+ // `config.tools` or per-agent `tools`. Order in {@link resolveTools}
415
+ // is `system -> user-ambient -> per-agent`, last write wins.
416
+ const systemTools: MastraTools = {
417
+ render_data: buildRenderDataTool(config),
418
+ };
419
+ const ambientTools = { ...systemTools, ...(config.tools ?? {}) };
408
420
  const style = resolveStyleInstructions(config);
409
421
  const agents: Record<string, Agent> = {};
410
422