@dbx-tools/appkit-mastra-shared 0.1.12 → 0.1.18

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/index.ts CHANGED
@@ -9,4 +9,6 @@
9
9
  * them back via `usePluginClientConfig<MastraClientConfig>("mastra")`
10
10
  * and composes URLs with {@link chatUrl}.
11
11
  */
12
+ export * from "./src/genie.js";
13
+ export * from "./src/mastra.js";
12
14
  export * from "./src/protocol.js";
package/package.json CHANGED
@@ -9,9 +9,19 @@
9
9
  }
10
10
  },
11
11
  "name": "@dbx-tools/appkit-mastra-shared",
12
- "version": "0.1.12",
13
- "type": "module",
12
+ "version": "0.1.18",
13
+ "dependencies": {
14
+ "@dbx-tools/genie-shared": "0.1.18",
15
+ "zod": "^4.3.6"
16
+ },
17
+ "devDependencies": {
18
+ "@databricks/sdk-experimental": "^0.17"
19
+ },
14
20
  "module": "index.ts",
21
+ "peerDependencies": {
22
+ "@databricks/appkit": "*"
23
+ },
24
+ "type": "module",
15
25
  "files": [
16
26
  "dist",
17
27
  "index*.ts",
@@ -25,6 +35,6 @@
25
35
  "repository": {
26
36
  "type": "git",
27
37
  "url": "git+https://github.com/reggie-db/dbx-tools-appkit.git",
28
- "directory": "packages/mastra-shared"
38
+ "directory": "packages/appkit-mastra-shared"
29
39
  }
30
40
  }
package/src/genie.ts ADDED
@@ -0,0 +1,407 @@
1
+ /**
2
+ * Mastra-only surface for the Genie agent that
3
+ * `@dbx-tools/appkit-mastra` runs server-side.
4
+ *
5
+ * The pure Genie wire vocabulary (chat events, terminal-status
6
+ * helpers, attachment shapes) lives in `@dbx-tools/genie-shared`
7
+ * so anything that doesn't speak Mastra (browser bundles,
8
+ * headless renderers, non-Mastra clients) can import the protocol
9
+ * without dragging Mastra in. We re-export that surface from this
10
+ * module so downstream callers keep a single
11
+ * `@dbx-tools/appkit-mastra-shared` import.
12
+ *
13
+ * What lives here:
14
+ *
15
+ * - {@link MinimalWriter}: structural shape of `ctx.writer`,
16
+ * used by every Mastra tool that publishes Genie events.
17
+ * - {@link GenieAgentEvent}: lifecycle and chart events the
18
+ * Mastra Genie agent emits that are NOT on the Genie wire
19
+ * (`started`, `ask_genie_done`, `error`, `chart`). Same flat
20
+ * `{type, ...fields}` shape as the wire's
21
+ * {@link GenieChatEvent} so subscribers union both with one
22
+ * `switch (event.type)`.
23
+ * - {@link GenieWriterEvent}: the unified vocabulary the Genie
24
+ * agent writes through `ctx.writer`. Subscribers narrow on
25
+ * `type` and read the event's fields directly - no
26
+ * translation layer.
27
+ * - Workflow output shapes ({@link GenieDataset},
28
+ * {@link GenieDatasetChart}, {@link GenieSummaryItem},
29
+ * {@link GenieAgentResult}): structurally Mastra-only because
30
+ * the agent's two-step workflow (agent step + finalize step)
31
+ * embeds a chart-planner output (`dataset.chart`) and a mixed
32
+ * `(string | visualize)[]` summary that the Genie wire knows
33
+ * nothing about.
34
+ * - {@link genieResultToWriterEvents}: helper that replays the
35
+ * terminal `error` event from a completed
36
+ * {@link GenieAgentResult} (e.g. on history reload). Chart
37
+ * replay is intentionally not supported - the resolved
38
+ * Echarts spec is held off-band on the per-request
39
+ * `RequestContext`, not on the persisted summary.
40
+ *
41
+ * Pure types and small helpers; no Node-only imports, safe for
42
+ * browser bundles.
43
+ */
44
+
45
+ import {
46
+ GenieChatEventSchema,
47
+ type GenieChatEvent,
48
+ type MessageStatus,
49
+ } from "@dbx-tools/genie-shared";
50
+ import { z } from "zod";
51
+
52
+ /* ----------------------------- writer surface ---------------------------- */
53
+
54
+ /**
55
+ * Minimal `ToolStream`-shaped writer the Genie agent and chart
56
+ * helpers publish events through. Defined here (vs imported from
57
+ * `@mastra/core`) so helpers in `@dbx-tools/appkit-mastra` can
58
+ * accept any object with a `.write` method without dragging
59
+ * Mastra's full `ToolStream` (and its agent / tool typings) into
60
+ * call sites. The actual Mastra `ctx.writer` is assignable to
61
+ * this shape so callers pass it straight through.
62
+ *
63
+ * Kept as a plain TypeScript interface (vs a zod schema) because
64
+ * the contract is a method - zod can only validate the shape via
65
+ * `z.custom`, which adds noise without buying any runtime check.
66
+ */
67
+ export interface MinimalWriter {
68
+ write: (chunk: unknown) => unknown;
69
+ }
70
+
71
+ /* ---------------- mastra-only genie-agent events ---------------- */
72
+
73
+ /**
74
+ * Mastra-only lifecycle event: the Genie tool invocation started.
75
+ * Emitted immediately when the calling agent invokes the Genie
76
+ * tool, before any inner agent / wire activity, so the UI can
77
+ * pop a "Thinking..." pill the instant the model decides to
78
+ * delegate. `conversationId` / `messageId` are absent on this
79
+ * first emit (no Genie round-trip yet). Field names are
80
+ * camelCase (vs the snake_case wire events) to mirror the
81
+ * Genie agent's own internal data plumbing.
82
+ */
83
+ export const StartedEventSchema = z.object({
84
+ type: z.literal("started"),
85
+ spaceId: z.string(),
86
+ /**
87
+ * Genie conversation id, populated only when this `started`
88
+ * event corresponds to a follow-up turn on an existing
89
+ * conversation. Absent on the first turn.
90
+ */
91
+ conversationId: z.string().optional(),
92
+ /**
93
+ * Genie message id, populated only after the first wire
94
+ * `message` event lands. Absent on the immediate-on-invoke
95
+ * emit.
96
+ */
97
+ messageId: z.string().optional(),
98
+ /** Question the Genie agent sent to Genie. */
99
+ content: z.string(),
100
+ });
101
+ export type StartedEvent = z.infer<typeof StartedEventSchema>;
102
+
103
+ /**
104
+ * Mastra-only lifecycle event: one `ask_genie` invocation
105
+ * finished. Carries the hydrated `statementIds` (rows are fetched
106
+ * via `getStatement` separately) and Genie's final prose answer
107
+ * so the UI can move from "thinking" to "answered" without
108
+ * waiting for the Genie agent's whole reasoning loop to end.
109
+ */
110
+ export const AskGenieDoneEventSchema = z.object({
111
+ type: z.literal("ask_genie_done"),
112
+ spaceId: z.string(),
113
+ conversationId: z.string().optional(),
114
+ messageId: z.string().optional(),
115
+ /** Genie's natural-language answer for the turn, if any. */
116
+ answer: z.string().optional(),
117
+ /** Statement ids for any non-empty result sets this turn produced. */
118
+ statementIds: z.array(z.string()),
119
+ /**
120
+ * Terminal wire status (`COMPLETED` / `FAILED` / `CANCELLED`).
121
+ * Mirrors the source `result` event's status so subscribers
122
+ * can react to ask-level completion without re-walking history.
123
+ * Treated as `z.custom<MessageStatus>` because the SDK is the
124
+ * source of truth for the enum values.
125
+ */
126
+ status: z.custom<MessageStatus>((v) => typeof v === "string"),
127
+ });
128
+ export type AskGenieDoneEvent = z.infer<typeof AskGenieDoneEventSchema>;
129
+
130
+ /**
131
+ * Mastra-only error event: terminal Genie agent / transport
132
+ * error. Genie's own `FAILED` / `CANCELLED` come through the
133
+ * wire's `result` event - this variant is for failures the wire
134
+ * can't represent (network, Genie agent crash, planner error,
135
+ * etc.) plus a UI-friendly mirror of `result` when the status is
136
+ * non-`COMPLETED`.
137
+ */
138
+ export const MastraGenieErrorEventSchema = z.object({
139
+ type: z.literal("error"),
140
+ spaceId: z.string().optional(),
141
+ conversationId: z.string().optional(),
142
+ messageId: z.string().optional(),
143
+ error: z.string(),
144
+ });
145
+ export type MastraGenieErrorEvent = z.infer<typeof MastraGenieErrorEventSchema>;
146
+
147
+ /**
148
+ * Mastra-only lifecycle event: the inner Genie agent's
149
+ * structured-output coercion has landed. Fires once per Genie
150
+ * tool invocation, AFTER `agent.generate(...)` completes (i.e.
151
+ * the inner loop + Mastra's structuring pass have both
152
+ * finished) and BEFORE the wrapper hydrates each `data` item
153
+ * with a chart. Signals to the host UI that the agent has
154
+ * stopped reasoning and is moving into chart generation.
155
+ *
156
+ * The structuring pass itself is opaque (it runs inside
157
+ * Mastra's `agent.generate(...)` together with the tool loop)
158
+ * so this is the earliest hook we can offer; we can't fire a
159
+ * "summary started" event the way we fire `started`.
160
+ */
161
+ export const SummaryEventSchema = z.object({
162
+ type: z.literal("summary"),
163
+ spaceId: z.string(),
164
+ /** Total number of items in the agent's structured summary. */
165
+ items: z.number().int().nonnegative(),
166
+ /** Count of `text` / prose items in the summary. */
167
+ textItems: z.number().int().nonnegative(),
168
+ /**
169
+ * Count of `data` items the wrapper will hydrate into charts.
170
+ * The host UI can use this to seed N chart skeletons before
171
+ * the per-chart events arrive.
172
+ */
173
+ dataItems: z.number().int().nonnegative(),
174
+ });
175
+ export type SummaryEvent = z.infer<typeof SummaryEventSchema>;
176
+
177
+ /**
178
+ * Mastra-only render event: a chart was rendered for the active
179
+ * turn. Emitted by the chart-rendering tool (and replayed from
180
+ * `genieResultToWriterEvents` on history reload) so the host UI
181
+ * can drop an `[[chart:<chartId>]]`-keyed slot inline. Carries
182
+ * the dataset (for the table fallback / hover) and the resolved
183
+ * Echarts `option` in a single event keyed by `chartId`.
184
+ */
185
+ export const ChartEventSchema = z.object({
186
+ type: z.literal("chart"),
187
+ chartId: z.string(),
188
+ title: z.string().optional(),
189
+ description: z.string().optional(),
190
+ /** Dataset rows; populated on the first emit per `chartId`. */
191
+ data: z.array(z.record(z.string(), z.unknown())).optional(),
192
+ /** Echarts option spec; populated on the follow-up emit. */
193
+ option: z.record(z.string(), z.unknown()).optional(),
194
+ /**
195
+ * Statement id the chart was built from, when known. Lets the
196
+ * host UI correlate the chart with the matching `query` /
197
+ * `statement` events from the same turn.
198
+ */
199
+ statementId: z.string().optional(),
200
+ /**
201
+ * Genie `message_id` the chart was built from. Stamped from the
202
+ * `ask_genie` turn whose statement produced these rows so the
203
+ * host UI can group the chart into the same pill bucket as the
204
+ * other `message_id`-keyed events from that turn.
205
+ */
206
+ messageId: z.string().optional(),
207
+ });
208
+ export type ChartEvent = z.infer<typeof ChartEventSchema>;
209
+
210
+ /**
211
+ * Mastra-only event union. Each variant uses the same flat
212
+ * `{type, ...fields}` shape as {@link GenieChatEvent} so
213
+ * subscribers union both with a single `switch (event.type)`.
214
+ */
215
+ export const GenieAgentEventSchema = z.discriminatedUnion("type", [
216
+ StartedEventSchema,
217
+ AskGenieDoneEventSchema,
218
+ MastraGenieErrorEventSchema,
219
+ SummaryEventSchema,
220
+ ChartEventSchema,
221
+ ]);
222
+ export type GenieAgentEvent = z.infer<typeof GenieAgentEventSchema>;
223
+
224
+ /**
225
+ * The unified writer-event vocabulary subscribers see on
226
+ * `ctx.writer`: the wire-derived {@link GenieChatEvent} union
227
+ * **plus** Mastra-only events from {@link GenieAgentEvent}. Each
228
+ * variant is a flat `{type, ...fields}` object; consumers narrow
229
+ * on `type` and read fields inline - there is no payload wrapper
230
+ * and no writer-boundary translator.
231
+ *
232
+ * Composed via `z.union` (not `z.discriminatedUnion`) because
233
+ * both halves are themselves discriminated unions on the same
234
+ * `type` key.
235
+ */
236
+ export const GenieWriterEventSchema = z.union([
237
+ GenieChatEventSchema,
238
+ GenieAgentEventSchema,
239
+ ]);
240
+ export type GenieWriterEvent = z.infer<typeof GenieWriterEventSchema>;
241
+
242
+ /** Discriminator type for {@link GenieWriterEvent}. */
243
+ export type GenieWriterEventType = GenieWriterEvent["type"];
244
+
245
+ /* ------------------------- summary + dataset ------------------------ */
246
+
247
+ /**
248
+ * Tabular payload embedded in every {@link GenieSummaryItem}
249
+ * `visualize` dataset. Always present: hydrated by the workflow's
250
+ * agent step before the finalize step runs, so consumers can render
251
+ * a table fallback regardless of chart-planner outcome.
252
+ *
253
+ * Fields:
254
+ * - `columns`: column names in display order.
255
+ * - `rows`: tabular rows keyed by column name.
256
+ * - `rowCount`: total row count Genie reported (may exceed
257
+ * `rows.length` when the statement was truncated).
258
+ */
259
+ export const GenieDatasetDataSchema = z.object({
260
+ columns: z.array(z.string()),
261
+ rows: z.array(z.record(z.string(), z.unknown())),
262
+ rowCount: z.number(),
263
+ });
264
+ export type GenieDatasetData = z.infer<typeof GenieDatasetDataSchema>;
265
+
266
+ /**
267
+ * Slim chart reference attached to a visualize dataset once the
268
+ * workflow's finalize step runs the chart-planner. Only present
269
+ * when planning succeeded.
270
+ *
271
+ * `option` is intentionally NOT included. The resolved Echarts
272
+ * spec lives off-band:
273
+ *
274
+ * - On the wire to the UI: in the matching {@link ChartEvent}
275
+ * writer event (the host UI receives both this dataset and
276
+ * the writer event and joins them on `chartId`).
277
+ * - On the server: in the per-request {@link RequestContext}
278
+ * under the chart inventory key (see appkit-mastra's
279
+ * `chartInventoryFromContext`), so output processors and
280
+ * downstream tools can look up the full payload by `chartId`
281
+ * without round-tripping through the LLM.
282
+ *
283
+ * Why slim: full Echarts options nest deeply and are several
284
+ * KB per chart. Embedding them in the tool result means every
285
+ * subsequent turn of the agent loop reads them back into context
286
+ * for zero LLM benefit (the model only needs the `chartId` to
287
+ * place a `[[chart:<chartId>]]` marker).
288
+ */
289
+ export const GenieDatasetChartSchema = z.object({
290
+ chartId: z.string(),
291
+ chartType: z.enum(["bar", "line", "area", "scatter", "pie"]),
292
+ });
293
+ export type GenieDatasetChart = z.infer<typeof GenieDatasetChartSchema>;
294
+
295
+ /**
296
+ * Dataset bundle attached to a {@link GenieSummaryItem} `visualize`
297
+ * item. `data` is always populated; `chart` is best-effort and
298
+ * absent when the workflow's chart-planner failed (timeout,
299
+ * malformed plan, abort) so the host UI can still render the
300
+ * underlying table.
301
+ */
302
+ export const GenieDatasetSchema = z.object({
303
+ data: GenieDatasetDataSchema,
304
+ chart: GenieDatasetChartSchema.optional(),
305
+ });
306
+ export type GenieDataset = z.infer<typeof GenieDatasetSchema>;
307
+
308
+ /**
309
+ * One item inside the Genie workflow's final summary. The
310
+ * workflow produces a mixed sequence of:
311
+ *
312
+ * - `string`: a markdown paragraph (interpretation, callouts,
313
+ * transitions between data blocks).
314
+ * - `visualize`: a request from the agent step to visualize a
315
+ * specific Genie statement at this position in the prose.
316
+ * The finalize step hydrates `dataset.data` (rows from the
317
+ * matching `statementId`) and attaches `dataset.chart` after
318
+ * running the chart-planner. The agent NEVER picks the chart
319
+ * type - it only marks where a visualization belongs.
320
+ *
321
+ * The host UI walks the array in display order to compose the
322
+ * final assistant message.
323
+ */
324
+ export const GenieSummaryItemSchema = z.discriminatedUnion("type", [
325
+ z.object({
326
+ type: z.literal("string"),
327
+ text: z.string(),
328
+ }),
329
+ z.object({
330
+ type: z.literal("visualize"),
331
+ statementId: z.string(),
332
+ title: z.string().optional(),
333
+ description: z.string().optional(),
334
+ dataset: GenieDatasetSchema,
335
+ }),
336
+ ]);
337
+ export type GenieSummaryItem = z.infer<typeof GenieSummaryItemSchema>;
338
+
339
+ /** Discriminator type for {@link GenieSummaryItem}. */
340
+ export type GenieSummaryItemType = GenieSummaryItem["type"];
341
+
342
+ /**
343
+ * The Genie agent's final output shape - what the calling agent's
344
+ * Genie tool returns to the calling LLM. The `summary` array is
345
+ * the user-facing renderable; `conversationId` lets the calling
346
+ * agent (or the UI) follow up in the same Genie thread on the
347
+ * next turn.
348
+ */
349
+ export const GenieAgentResultSchema = z.object({
350
+ spaceId: z.string(),
351
+ conversationId: z.string().optional(),
352
+ summary: z.array(GenieSummaryItemSchema),
353
+ error: z.string().optional(),
354
+ });
355
+ export type GenieAgentResult = z.infer<typeof GenieAgentResultSchema>;
356
+
357
+ /**
358
+ * Structural type guard for {@link GenieAgentResult}. Used by
359
+ * host UIs to detect the Genie agent's payload off Mastra's
360
+ * `tool-result` chunks without coupling to a specific Mastra tool
361
+ * name (per-space variants like `tool-genie-<alias>` all return
362
+ * the same shape).
363
+ *
364
+ * Cheap structural check (vs full `safeParse`) so the guard stays
365
+ * O(1) on the hot path; consumers that want full validation can
366
+ * call {@link GenieAgentResultSchema}`.safeParse(value)` directly.
367
+ */
368
+ export function isGenieAgentResult(value: unknown): value is GenieAgentResult {
369
+ if (!value || typeof value !== "object") return false;
370
+ const v = value as Record<string, unknown>;
371
+ return typeof v.spaceId === "string" && Array.isArray(v.summary);
372
+ }
373
+
374
+ /* ---------------------- result -> writer-event helpers ---------------------- */
375
+
376
+ /**
377
+ * Walk a {@link GenieAgentResult} and produce the lifecycle
378
+ * writer events a host UI needs to replay terminal state inline.
379
+ *
380
+ * Chart replay is intentionally NOT supported: the resolved
381
+ * Echarts `option` is held off-band in the per-request
382
+ * `RequestContext` (and on the live writer event when the run is
383
+ * in flight), not on the persisted summary, so a completed run
384
+ * read back from storage has no chart spec to replay. Host UIs
385
+ * that want post-reload chart rendering need to plumb the spec
386
+ * through a separate persisted side-channel.
387
+ *
388
+ * Currently extracted:
389
+ *
390
+ * - `type: "error"` when `output.error` is set (Genie returned
391
+ * `FAILED` / `CANCELLED`, `getStatement` errored, etc.).
392
+ *
393
+ * `string` summary items are not surfaced here - the calling
394
+ * LLM's text reply renders them inline.
395
+ */
396
+ export function genieResultToWriterEvents(output: GenieAgentResult): GenieAgentEvent[] {
397
+ const events: GenieAgentEvent[] = [];
398
+ if (output.error) {
399
+ events.push({
400
+ type: "error",
401
+ spaceId: output.spaceId,
402
+ ...(output.conversationId ? { conversationId: output.conversationId } : {}),
403
+ error: output.error,
404
+ });
405
+ }
406
+ return events;
407
+ }
package/src/mastra.ts ADDED
@@ -0,0 +1,53 @@
1
+ /**
2
+ * URL helpers for the Mastra plugin's published
3
+ * {@link MastraClientConfig} surface. Kept in a separate module
4
+ * from `protocol.ts` so the protocol stays purely declarative
5
+ * (schemas + inferred types) and consumers that only need URL
6
+ * composition import this file without re-evaluating the schemas.
7
+ *
8
+ * Both helpers accept a `Pick<MastraClientConfig, ...>` so callers
9
+ * can pass a freshly read config or any object that exposes the
10
+ * relevant fields - useful for tests and for partial configs the
11
+ * React client composes from `usePluginClientConfig`.
12
+ */
13
+
14
+ import type { MastraClientConfig } from "./protocol.js";
15
+
16
+ /**
17
+ * Compute the chat URL for a given agent, falling back to the
18
+ * default when `agentId` is omitted. Returns `config.chatPath`
19
+ * directly for the default agent (the `chatRoute` mount that does
20
+ * not require an `:agentId` segment).
21
+ */
22
+ export function chatUrl(
23
+ config: Pick<MastraClientConfig, "chatPath" | "defaultAgent">,
24
+ agentId?: string,
25
+ ): string {
26
+ const id = agentId ?? config.defaultAgent;
27
+ if (!id || id === config.defaultAgent) return config.chatPath;
28
+ return `${config.chatPath}/${encodeURIComponent(id)}`;
29
+ }
30
+
31
+ /**
32
+ * Build the history URL for a given agent + page. Mirrors
33
+ * {@link chatUrl}: the default agent uses the bare `historyPath`,
34
+ * any other agent appends `/<encoded id>` to it. `page` and
35
+ * `perPage` are appended as query parameters when provided.
36
+ */
37
+ export function historyUrl(
38
+ config: Pick<MastraClientConfig, "historyPath" | "defaultAgent">,
39
+ options: { agentId?: string; page?: number; perPage?: number } = {},
40
+ ): string {
41
+ const id = options.agentId ?? config.defaultAgent;
42
+ const base =
43
+ !id || id === config.defaultAgent
44
+ ? config.historyPath
45
+ : `${config.historyPath}/${encodeURIComponent(id)}`;
46
+ const params = new URLSearchParams();
47
+ if (options.page !== undefined) params.set("page", String(options.page));
48
+ if (options.perPage !== undefined) {
49
+ params.set("perPage", String(options.perPage));
50
+ }
51
+ const qs = params.toString();
52
+ return qs ? `${base}?${qs}` : base;
53
+ }