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