@dbx-tools/appkit-mastra 0.1.26 → 0.1.28

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/README.md CHANGED
@@ -199,7 +199,7 @@ once regardless of how many spaces are wired:
199
199
  Genie statement. Use only when the agent needs to read values
200
200
  to reason about them; otherwise embed `[data:<statement_id>]`.
201
201
  - `prepare_chart({statement_id, title?, description?})` - mint a
202
- short `chartId`, kick off a background chart-planner task,
202
+ `chartId` (v4 UUID), kick off a background chart-planner task,
203
203
  cache the resolved Echarts spec under the id for one hour.
204
204
  Returns `{chartId}` synchronously so the agent can embed
205
205
  `[chart:<chartId>]` in prose without blocking.
@@ -254,8 +254,9 @@ Genie writer event flow:
254
254
  - Charts ride out-of-band: `prepare_chart` mints a `chartId`
255
255
  synchronously, the planner runs in the background and writes
256
256
  the spec to the chart cache (1h TTL). The host UI long-polls
257
- `${MastraClientConfig.chartsPathTemplate}` by id and renders
258
- inline at the matching `[chart:<chartId>]` marker.
257
+ `${MastraClientConfig.embedPathTemplate}` (`/embed/chart/:id`)
258
+ by id and renders inline at the matching `[chart:<chartId>]`
259
+ marker.
259
260
  - After a hard reload, the live `started` / `status` / `sql`
260
261
  events are gone (they only ride the writer, not persisted
261
262
  tool results); the central agent's text reply (with embedded
@@ -285,7 +286,7 @@ off the chart-planner in the background. The tool returns just
285
286
  `{ chartId }` so the model can embed `[chart:<chartId>]` in
286
287
  prose immediately without blocking on chart generation.
287
288
 
288
- 1. Mint a short `chartId` (8 hex chars).
289
+ 1. Mint a `chartId` (v4 UUID via `crypto.randomUUID()`).
289
290
  2. Cache an empty `{ chartId }` placeholder so the first
290
291
  `fetchChart` call always sees an entry (no spurious 404 race).
291
292
  3. Fire-and-forget: resolve the dataset (trivial for
@@ -297,26 +298,27 @@ prose immediately without blocking on chart generation.
297
298
  4. Return `{ chartId }` to the LLM immediately.
298
299
 
299
300
  The host UI resolves `[chart:<chartId>]` markers by hitting
300
- the plugin's `GET /charts/:chartId` route, which long-polls
301
+ the plugin's generic `GET /embed/chart/:id` route, which long-polls
301
302
  until the entry settles or the server-side timeout elapses.
302
303
 
303
304
  #### Inline placement contract
304
305
 
305
- The model embeds `[[chart:<chartId>]]` on its own line in its
306
- markdown reply at the position where the chart should appear:
306
+ The model embeds `[chart:<chartId>]` on its own line in its
307
+ markdown reply at the position where the chart should appear
308
+ (the `<chartId>` is the v4 UUID the tool returned):
307
309
 
308
310
  ```markdown
309
311
  ## Audit Score
310
312
 
311
313
  Audit Score is stable at ~94%, hovering between 93.5 and 95.0.
312
314
 
313
- [[chart:a3f9c1d2]]
315
+ [chart:a3f9c1d2-7b4e-4c1a-9f2d-1e6b8c0a5d31]
314
316
 
315
317
  ## Service Time
316
318
 
317
319
  Service time is the outlier at 162.5s, up from a target of 150s.
318
320
 
319
- [[chart:b7e2d4f1]]
321
+ [chart:b7e2d4f1-3a9c-4e58-8d10-2f7a4b6c9e02]
320
322
  ```
321
323
 
322
324
  The chat client splits the assistant text on these markers and
@@ -330,7 +332,7 @@ frame so the layout doesn't jump as charts resolve.
330
332
  #### Trade-offs
331
333
 
332
334
  - Chart entries live in the cache with a 1h TTL. After expiry
333
- the `/charts/:chartId` route returns 404. The model can call
335
+ the `/embed/chart/:id` route returns 404. The model can call
334
336
  `render_data` again on the next turn if the user wants the
335
337
  chart back.
336
338
  - The chart-planner is a separate model call per dataset (fast
package/dist/index.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  /**
2
- * AppKit Mastra integration: {@link MastraPlugin} / {@link mastra},
3
- * plugin config types, agent registration helpers, Genie tool
4
- * builders, and dynamic Model Serving endpoint resolution.
2
+ * Server-side entry point for the AppKit Mastra integration. Mounts
3
+ * the plugin via {@link mastra} and re-exports the full server surface
4
+ * (config, agent wiring, Genie and chart tooling, and dynamic Model
5
+ * Serving resolution) so apps build agent backends from one import.
5
6
  *
6
7
  * Client-side consumers should import URL helpers and the
7
8
  * {@link MastraClientConfig} type from `@dbx-tools/appkit-mastra-shared`
package/dist/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  /**
2
- * AppKit Mastra integration: {@link MastraPlugin} / {@link mastra},
3
- * plugin config types, agent registration helpers, Genie tool
4
- * builders, and dynamic Model Serving endpoint resolution.
2
+ * Server-side entry point for the AppKit Mastra integration. Mounts
3
+ * the plugin via {@link mastra} and re-exports the full server surface
4
+ * (config, agent wiring, Genie and chart tooling, and dynamic Model
5
+ * Serving resolution) so apps build agent backends from one import.
5
6
  *
6
7
  * Client-side consumers should import URL helpers and the
7
8
  * {@link MastraClientConfig} type from `@dbx-tools/appkit-mastra-shared`
@@ -188,7 +188,7 @@ export async function buildAgents(opts) {
188
188
  const style = resolveStyleInstructions(config);
189
189
  // Default-on protection against the model copying turn-scoped
190
190
  // chartIds from prior assistant tool results into the new
191
- // turn's `[[chart:<id>]]` markers. Opt out per-plugin via
191
+ // turn's `[chart:<id>]` markers. Opt out per-plugin via
192
192
  // `config.stripStaleCharts: false`.
193
193
  const inputProcessors = config.stripStaleCharts === false ? [] : [stripStaleChartsProcessor];
194
194
  const agents = {};
@@ -20,18 +20,9 @@
20
20
  * dataset, custom API). The module has no knowledge of Genie or
21
21
  * statement ids; those concerns live in the tools that wrap it.
22
22
  *
23
- * Wire-format schemas (`ChartSchema`, `ChartResultSchema`,
24
- * {@link ChartTypeSchema}) live in
25
- * `@dbx-tools/appkit-mastra-shared` so the demo client and any
26
- * other UI consumer share the exact same shape this module reads
27
- * and writes.
28
- *
29
- * Public surface (everything else is module-private):
30
- * - {@link chartPlannerRequestSchema} / {@link ChartPlannerRequest}
31
- * - {@link prepareChart} / {@link PrepareChartOptions}
32
- * - {@link fetchChart} / {@link FetchChartOptions}
33
- * - {@link buildRenderDataTool} (the `render_data` Mastra tool
34
- * auto-wired on every agent in `agents.ts`)
23
+ * Wire-format schemas live in `@dbx-tools/appkit-mastra-shared` so
24
+ * the demo client and any other UI consumer share the exact same
25
+ * shape this module reads and writes.
35
26
  */
36
27
  import { type Chart } from "@dbx-tools/appkit-mastra-shared";
37
28
  import type { RequestContext } from "@mastra/core/request-context";
@@ -147,7 +138,7 @@ export declare function fetchChart(chartId: string, options?: FetchChartOptions)
147
138
  * an empty placeholder, and kicks off the chart-planner in the
148
139
  * background. Returns just the `chartId`; the host UI resolves
149
140
  * `[chart:<chartId>]` markers by hitting the plugin's
150
- * `/charts/:chartId` route.
141
+ * `/embed/chart/:id` route.
151
142
  *
152
143
  * For Genie statement results, prefer the Genie agent's
153
144
  * `prepare_chart` tool, which accepts a `statement_id` and
package/dist/src/chart.js CHANGED
@@ -20,18 +20,9 @@
20
20
  * dataset, custom API). The module has no knowledge of Genie or
21
21
  * statement ids; those concerns live in the tools that wrap it.
22
22
  *
23
- * Wire-format schemas (`ChartSchema`, `ChartResultSchema`,
24
- * {@link ChartTypeSchema}) live in
25
- * `@dbx-tools/appkit-mastra-shared` so the demo client and any
26
- * other UI consumer share the exact same shape this module reads
27
- * and writes.
28
- *
29
- * Public surface (everything else is module-private):
30
- * - {@link chartPlannerRequestSchema} / {@link ChartPlannerRequest}
31
- * - {@link prepareChart} / {@link PrepareChartOptions}
32
- * - {@link fetchChart} / {@link FetchChartOptions}
33
- * - {@link buildRenderDataTool} (the `render_data` Mastra tool
34
- * auto-wired on every agent in `agents.ts`)
23
+ * Wire-format schemas live in `@dbx-tools/appkit-mastra-shared` so
24
+ * the demo client and any other UI consumer share the exact same
25
+ * shape this module reads and writes.
35
26
  */
36
27
  import { CacheManager } from "@databricks/appkit";
37
28
  import { ChartSchema, ChartTypeSchema, } from "@dbx-tools/appkit-mastra-shared";
@@ -199,9 +190,7 @@ const chartPlanSchema = z.object({
199
190
  * sees a planner request, only the resolved {@link Chart}.
200
191
  */
201
192
  export const chartPlannerRequestSchema = z.object({
202
- title: z
203
- .string()
204
- .describe(stringUtils.toDescription(`
193
+ title: z.string().describe(stringUtils.toDescription(`
205
194
  Concise title shown above the chart (e.g. "Top 10 SKUs by Revenue").
206
195
  `)),
207
196
  description: z
@@ -530,7 +519,7 @@ function planToEchartsOption(plan, fallbackTitle) {
530
519
  * an empty placeholder, and kicks off the chart-planner in the
531
520
  * background. Returns just the `chartId`; the host UI resolves
532
521
  * `[chart:<chartId>]` markers by hitting the plugin's
533
- * `/charts/:chartId` route.
522
+ * `/embed/chart/:id` route.
534
523
  *
535
524
  * For Genie statement results, prefer the Genie agent's
536
525
  * `prepare_chart` tool, which accepts a `statement_id` and
@@ -191,7 +191,7 @@ export interface MastraPluginConfig extends BasePluginConfig {
191
191
  * processor that strips `chartId` fields from prior assistant
192
192
  * tool-invocation results before they reach the model. This
193
193
  * prevents the model from reusing turn-scoped chartIds it sees
194
- * in memory recall (which would leave `[[chart:<id>]]` markers
194
+ * in memory recall (which would leave `[chart:<id>]` markers
195
195
  * pointing at writer events that no longer exist).
196
196
  *
197
197
  * Set to `false` to opt out - useful if a non-default agent
@@ -1,41 +1,19 @@
1
1
  /**
2
2
  * Genie tools for Mastra.
3
3
  *
4
- * Each configured Genie space is surfaced as a small set of flat
5
- * Mastra tools the calling agent can drive directly - no inner
6
- * orchestrator agent. The central agent decomposes user questions,
7
- * picks which Genie space to ask, and composes the final reply.
8
- *
9
- * Per-space tools (suffixed with `_<alias>` for non-default
10
- * aliases):
11
- *
12
- * - `ask_genie`: drives one `genieEventChat` turn and
13
- * forwards every wire event (status, thinking, sql, rows)
14
- * through `ctx.writer` for streaming UI updates. Returns
15
- * the terminal `GenieMessage` only - rows are NOT fetched
16
- * eagerly.
17
- * - `get_space_description`: cheap title / description /
18
- * warehouse id lookup for grounding.
19
- * - `get_space_serialized`: full `GenieSpace` JSON for
20
- * column-level grounding when the description isn't enough.
21
- *
22
- * Space-agnostic shared tools (registered once, regardless of
23
- * how many spaces are wired):
24
- *
25
- * - `get_statement`: opt-in lookup of a Genie statement's rows
26
- * by `statement_id` (with a row `limit`). The agent calls
27
- * this only when it needs to read values to reason about
28
- * them; if the data is just being displayed, it embeds a
29
- * `[data:<statement_id>]` marker in prose instead and lets
30
- * the host UI resolve it.
31
- * - `prepare_chart`: mints a short `chartId`, kicks off a
32
- * background task that fetches the statement's rows and
33
- * runs the chart-planner, and caches the resolved Echarts
34
- * spec under the id (1h TTL). Returns the `chartId`
35
- * synchronously so the agent embeds `[chart:<chartId>]`
36
- * markers in prose without blocking on chart generation;
37
- * the host UI fetches the cached chart by id once it's
38
- * ready (see {@link fetchChart}).
4
+ * Surfaces each configured Genie space as a small set of flat Mastra
5
+ * tools the calling agent drives directly - no inner orchestrator
6
+ * agent. The central agent decomposes user questions, picks which
7
+ * space to ask, streams the per-turn wire events (status, thinking,
8
+ * sql, rows) through `ctx.writer`, and composes the final reply.
9
+ * Rows are never fetched eagerly: the agent reads a statement's
10
+ * values only when it needs to reason about them, otherwise it embeds
11
+ * a `[data:<statement_id>]` marker in prose and lets the host UI
12
+ * resolve the data. Charts are minted asynchronously and referenced
13
+ * by `[chart:<chartId>]` markers so prose isn't blocked on chart
14
+ * generation; the host UI fetches the cached spec by id once ready.
15
+ * Space description and serialized-space lookups are available for
16
+ * grounding when the agent needs schema context.
39
17
  *
40
18
  * Each tool's `execute` pulls the per-request
41
19
  * {@link WorkspaceClient} off `ctx.requestContext` (stamped by
package/dist/src/genie.js CHANGED
@@ -1,41 +1,19 @@
1
1
  /**
2
2
  * Genie tools for Mastra.
3
3
  *
4
- * Each configured Genie space is surfaced as a small set of flat
5
- * Mastra tools the calling agent can drive directly - no inner
6
- * orchestrator agent. The central agent decomposes user questions,
7
- * picks which Genie space to ask, and composes the final reply.
8
- *
9
- * Per-space tools (suffixed with `_<alias>` for non-default
10
- * aliases):
11
- *
12
- * - `ask_genie`: drives one `genieEventChat` turn and
13
- * forwards every wire event (status, thinking, sql, rows)
14
- * through `ctx.writer` for streaming UI updates. Returns
15
- * the terminal `GenieMessage` only - rows are NOT fetched
16
- * eagerly.
17
- * - `get_space_description`: cheap title / description /
18
- * warehouse id lookup for grounding.
19
- * - `get_space_serialized`: full `GenieSpace` JSON for
20
- * column-level grounding when the description isn't enough.
21
- *
22
- * Space-agnostic shared tools (registered once, regardless of
23
- * how many spaces are wired):
24
- *
25
- * - `get_statement`: opt-in lookup of a Genie statement's rows
26
- * by `statement_id` (with a row `limit`). The agent calls
27
- * this only when it needs to read values to reason about
28
- * them; if the data is just being displayed, it embeds a
29
- * `[data:<statement_id>]` marker in prose instead and lets
30
- * the host UI resolve it.
31
- * - `prepare_chart`: mints a short `chartId`, kicks off a
32
- * background task that fetches the statement's rows and
33
- * runs the chart-planner, and caches the resolved Echarts
34
- * spec under the id (1h TTL). Returns the `chartId`
35
- * synchronously so the agent embeds `[chart:<chartId>]`
36
- * markers in prose without blocking on chart generation;
37
- * the host UI fetches the cached chart by id once it's
38
- * ready (see {@link fetchChart}).
4
+ * Surfaces each configured Genie space as a small set of flat Mastra
5
+ * tools the calling agent drives directly - no inner orchestrator
6
+ * agent. The central agent decomposes user questions, picks which
7
+ * space to ask, streams the per-turn wire events (status, thinking,
8
+ * sql, rows) through `ctx.writer`, and composes the final reply.
9
+ * Rows are never fetched eagerly: the agent reads a statement's
10
+ * values only when it needs to reason about them, otherwise it embeds
11
+ * a `[data:<statement_id>]` marker in prose and lets the host UI
12
+ * resolve the data. Charts are minted asynchronously and referenced
13
+ * by `[chart:<chartId>]` markers so prose isn't blocked on chart
14
+ * generation; the host UI fetches the cached spec by id once ready.
15
+ * Space description and serialized-space lookups are available for
16
+ * grounding when the agent needs schema context.
39
17
  *
40
18
  * Each tool's `execute` pulls the per-request
41
19
  * {@link WorkspaceClient} off `ctx.requestContext` (stamped by
@@ -614,9 +592,13 @@ function buildPrepareChartTool(opts) {
614
592
  `
615
593
  To display the chart in your reply, embed
616
594
  \`[chart:<chartId>]\` on its own line at the position you
617
- want it to appear. The tool returns immediately - do NOT
618
- wait or call it again to "check progress"; the chart
619
- resolves asynchronously on the host UI's side.
595
+ want it to appear, using the EXACT \`chartId\` string this
596
+ call returned. Never construct a chart id yourself (it is
597
+ not the \`statement_id\` or any variation of it) - only a
598
+ value returned by this tool resolves to a real chart. The
599
+ tool returns immediately - do NOT wait or call it again to
600
+ "check progress"; the chart resolves asynchronously on the
601
+ host UI's side.
620
602
  `,
621
603
  `
622
604
  Use this only when the data has a story a chart conveys
@@ -728,6 +710,17 @@ export const GENIE_INSTRUCTIONS = stringUtils.toDescription([
728
710
  chart should appear. Use a chart when the data has a
729
711
  story a visual conveys better than a table (trends,
730
712
  rankings, distributions, parts-of-a-whole).
713
+
714
+ NEVER invent or hand-build a \`<chartId>\`. A valid
715
+ \`<chartId>\` is the opaque token a \`prepare_chart\`
716
+ call returned to you in THIS turn - nothing else. It
717
+ is NOT a \`statement_id\`, and it is NOT a
718
+ \`statement_id\` prefix with a label appended (e.g.
719
+ \`01f1...-region-fill\`). If you have not called
720
+ \`prepare_chart\` and received an id back, do not write
721
+ a \`[chart:...]\` marker at all - use \`[data:...]\`
722
+ instead. A fabricated chart id renders nothing and
723
+ wastes a request.
731
724
  `,
732
725
  ],
733
726
  },
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Server-side Server-Sent-Events frame interceptor for the native
3
+ * Mastra agent `/stream` endpoint.
4
+ *
5
+ * The Mastra stream (`@mastra/express`) emits one `data: <json>\n\n`
6
+ * SSE frame per chunk, where the decoded JSON carries a
7
+ * discriminating `type` (`text-delta`, `reasoning-delta`,
8
+ * `tool-call`, `step-finish`, ...) and is terminated by a
9
+ * `data: [DONE]` sentinel. {@link installStreamEventInterceptor} wraps
10
+ * an Express response and runs a caller-supplied
11
+ * {@link StreamFrameInterceptor} over each decoded frame so the caller
12
+ * can keep, rewrite, or drop it; every non-`data:` byte (SSE
13
+ * keep-alive comments, the `[DONE]` sentinel, blank padding) passes
14
+ * through verbatim.
15
+ *
16
+ * The wrap is inert unless the response turns out to be a streamed
17
+ * `200 text/event-stream` body, so non-streaming JSON routes and
18
+ * error responses are never touched.
19
+ */
20
+ import type express from "express";
21
+ /**
22
+ * Per-frame interceptor invoked with the `JSON.parse`d object of every
23
+ * `data: {json}` frame on the Mastra `/stream` response. The return
24
+ * value decides the frame's fate:
25
+ *
26
+ * - `true`: forward the frame's original bytes unchanged (no
27
+ * re-serialization).
28
+ * - `false`: drop the frame entirely.
29
+ * - `{ replace }`: re-emit the frame with the original `data:` prefix
30
+ * and trailing whitespace preserved and only the JSON body replaced
31
+ * by `JSON.stringify(replace)`.
32
+ */
33
+ export type StreamFrameInterceptor = (chunk: unknown) => {
34
+ replace: unknown;
35
+ } | true | false;
36
+ /**
37
+ * Wrap `res.write` / `res.end` so each SSE frame on a streamed Mastra
38
+ * response is run through `interceptor` (keep / rewrite / drop).
39
+ *
40
+ * Interception only engages once the response is confirmed to be a
41
+ * `200 text/event-stream` body (decided lazily on the first write,
42
+ * after the handler has set status + content-type); any other
43
+ * response streams through unchanged. A {@link StringDecoder}
44
+ * reassembles multi-byte UTF-8 sequences that straddle chunk
45
+ * boundaries, and a running buffer holds the trailing partial frame
46
+ * until its `\n\n` arrives.
47
+ */
48
+ export declare function installStreamEventInterceptor(res: express.Response, interceptor: StreamFrameInterceptor): void;
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Server-side Server-Sent-Events frame interceptor for the native
3
+ * Mastra agent `/stream` endpoint.
4
+ *
5
+ * The Mastra stream (`@mastra/express`) emits one `data: <json>\n\n`
6
+ * SSE frame per chunk, where the decoded JSON carries a
7
+ * discriminating `type` (`text-delta`, `reasoning-delta`,
8
+ * `tool-call`, `step-finish`, ...) and is terminated by a
9
+ * `data: [DONE]` sentinel. {@link installStreamEventInterceptor} wraps
10
+ * an Express response and runs a caller-supplied
11
+ * {@link StreamFrameInterceptor} over each decoded frame so the caller
12
+ * can keep, rewrite, or drop it; every non-`data:` byte (SSE
13
+ * keep-alive comments, the `[DONE]` sentinel, blank padding) passes
14
+ * through verbatim.
15
+ *
16
+ * The wrap is inert unless the response turns out to be a streamed
17
+ * `200 text/event-stream` body, so non-streaming JSON routes and
18
+ * error responses are never touched.
19
+ */
20
+ import { StringDecoder } from "node:string_decoder";
21
+ /** SSE event separator used by the Mastra stream wire format. */
22
+ const FRAME_DELIMITER = "\n\n";
23
+ /**
24
+ * Matches a whole SSE frame whose `data:` payload is a JSON object,
25
+ * capturing it in three parts: the `data:` field prefix (including
26
+ * whatever inter-token whitespace the producer used), the JSON object
27
+ * body, and any trailing whitespace. Anchored end-to-end so it only
28
+ * matches a single self-contained object frame; anything else (SSE
29
+ * comments, the `[DONE]` sentinel, text/array payloads) fails the
30
+ * match and is forwarded verbatim without a parse attempt. On rewrite
31
+ * the captured prefix/suffix are re-emitted as-is so only the body
32
+ * changes.
33
+ */
34
+ const DATA_OBJECT_RE = /^(data:\s*)(\{[\s\S]*\})(\s*)$/;
35
+ /**
36
+ * True when `res` is a `200 text/event-stream` body - the only
37
+ * response shape we intercept. Content-type based so it tracks the
38
+ * Mastra `/stream` route without hard-coding the path, and naturally
39
+ * skips the JSON routes (history, models, charts, statements) and any
40
+ * non-200 error response.
41
+ */
42
+ function isEventStream(res) {
43
+ if (res.statusCode !== 200)
44
+ return false;
45
+ const contentType = String(res.getHeader("content-type") ?? "").toLowerCase();
46
+ return contentType.includes("text/event-stream");
47
+ }
48
+ /**
49
+ * Run `interceptor` over a single SSE frame. Forwards anything that
50
+ * isn't a parseable `data: {json}` object frame verbatim (comments,
51
+ * the `[DONE]` sentinel, blank padding, unparseable / non-object
52
+ * payloads) so the interceptor only ever sees real chunk objects.
53
+ * Returns the frame string to emit, or `undefined` to drop the frame.
54
+ */
55
+ function interceptFrame(frame, interceptor) {
56
+ const match = DATA_OBJECT_RE.exec(frame);
57
+ if (!match)
58
+ return frame;
59
+ const [, prefix, body, suffix] = match;
60
+ let parsed;
61
+ try {
62
+ parsed = JSON.parse(body);
63
+ }
64
+ catch {
65
+ // Forward unparseable payloads untouched - feeding a partial /
66
+ // malformed frame to the interceptor risks corrupting bytes the
67
+ // client could still have handled.
68
+ return frame;
69
+ }
70
+ const result = interceptor(parsed);
71
+ if (result === true)
72
+ return frame;
73
+ if (result === false)
74
+ return undefined;
75
+ // Preserve the exact captured prefix/suffix so only the JSON body
76
+ // changes; the framing the client parses stays byte-identical.
77
+ return `${prefix}${JSON.stringify(result.replace)}${suffix}`;
78
+ }
79
+ /**
80
+ * Wrap `res.write` / `res.end` so each SSE frame on a streamed Mastra
81
+ * response is run through `interceptor` (keep / rewrite / drop).
82
+ *
83
+ * Interception only engages once the response is confirmed to be a
84
+ * `200 text/event-stream` body (decided lazily on the first write,
85
+ * after the handler has set status + content-type); any other
86
+ * response streams through unchanged. A {@link StringDecoder}
87
+ * reassembles multi-byte UTF-8 sequences that straddle chunk
88
+ * boundaries, and a running buffer holds the trailing partial frame
89
+ * until its `\n\n` arrives.
90
+ */
91
+ export function installStreamEventInterceptor(res, interceptor) {
92
+ const origWrite = res.write.bind(res);
93
+ const origEnd = res.end.bind(res);
94
+ const decoder = new StringDecoder("utf8");
95
+ let engaged;
96
+ let buffer = "";
97
+ // Append decoded text, emit every whole `\n\n`-delimited frame the
98
+ // interceptor keeps, and retain any trailing partial frame in
99
+ // `buffer`. On `final`, the residual buffer is flushed as the last
100
+ // frame so a stream that doesn't end on a delimiter isn't dropped.
101
+ const intercept = (text, final) => {
102
+ buffer += text;
103
+ const out = [];
104
+ let idx;
105
+ while ((idx = buffer.indexOf(FRAME_DELIMITER)) !== -1) {
106
+ const frame = buffer.slice(0, idx);
107
+ buffer = buffer.slice(idx + FRAME_DELIMITER.length);
108
+ const kept = interceptFrame(frame, interceptor);
109
+ if (kept !== undefined) {
110
+ out.push(kept, FRAME_DELIMITER);
111
+ }
112
+ }
113
+ if (final && buffer.length > 0) {
114
+ const kept = interceptFrame(buffer, interceptor);
115
+ if (kept !== undefined)
116
+ out.push(kept);
117
+ buffer = "";
118
+ }
119
+ return out.length === 0 ? "" : out.join("");
120
+ };
121
+ const toText = (chunk) => {
122
+ if (typeof chunk === "string")
123
+ return chunk;
124
+ if (Buffer.isBuffer(chunk))
125
+ return decoder.write(chunk);
126
+ if (chunk instanceof Uint8Array) {
127
+ return decoder.write(Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength));
128
+ }
129
+ return "";
130
+ };
131
+ res.write = function patchedWrite(chunk, encodingOrCb, cb) {
132
+ if (engaged === undefined)
133
+ engaged = isEventStream(res);
134
+ if (!engaged) {
135
+ return origWrite(chunk, encodingOrCb, cb);
136
+ }
137
+ const callback = typeof encodingOrCb === "function" ? encodingOrCb : cb;
138
+ const out = intercept(toText(chunk), false);
139
+ if (out.length === 0) {
140
+ // The chunk only advanced a partial frame; nothing to forward
141
+ // yet. Honor the write callback so backpressure-aware writers
142
+ // don't stall waiting on an ack that never comes.
143
+ if (callback)
144
+ queueMicrotask(() => callback());
145
+ return true;
146
+ }
147
+ return origWrite(out, callback);
148
+ };
149
+ res.end = function patchedEnd(chunk, encodingOrCb, cb) {
150
+ if (engaged === undefined)
151
+ engaged = isEventStream(res);
152
+ if (!engaged) {
153
+ return origEnd(chunk, encodingOrCb, cb);
154
+ }
155
+ const callback = typeof chunk === "function"
156
+ ? chunk
157
+ : typeof encodingOrCb === "function"
158
+ ? encodingOrCb
159
+ : cb;
160
+ const text = (typeof chunk !== "function" && chunk !== undefined ? toText(chunk) : "") +
161
+ decoder.end();
162
+ const out = intercept(text, true);
163
+ if (out.length > 0)
164
+ origWrite(out);
165
+ return origEnd(callback);
166
+ };
167
+ }
@@ -110,8 +110,8 @@ export declare class MastraPlugin extends Plugin<MastraPluginConfig> {
110
110
  clientConfig(): Record<string, unknown>;
111
111
  injectRoutes(router: IAppRouter): void;
112
112
  /**
113
- * Implementation backing the `/statements/:statementId` route.
114
- * Runs inside the AppKit user-context proxy so
113
+ * Implementation backing the `data` embed resolver
114
+ * (`GET /embed/data/:id`). Runs inside the AppKit user-context proxy so
115
115
  * `getExecutionContext()` returns the OBO-scoped workspace
116
116
  * client, then reuses the same `fetchStatementData` pipeline
117
117
  * the `get_statement` tool runs so the LLM and the UI see the