@dbx-tools/shared-mastra 0.1.9

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 ADDED
@@ -0,0 +1,117 @@
1
+ # @dbx-tools/shared-mastra
2
+
3
+ Browser-safe contract for the AppKit Mastra plugin.
4
+
5
+ Import this package when a client, test, or server route needs the same route
6
+ constants, header names, embed-marker parsing, feedback schemas, thread
7
+ selection fields, and Mastra response schemas used by
8
+ [`@dbx-tools/node-appkit-mastra`](../../node/appkit-mastra).
9
+
10
+ Key features:
11
+
12
+ - Route constants for the AppKit-Mastra client surface, including history,
13
+ threads, suggestions, model lists, embeds, and feedback.
14
+ - Header/query/body constants for thread selection and model override requests.
15
+ - Embed-marker parsing for delayed chart and statement-data payloads in
16
+ streaming assistant text.
17
+ - Zod schemas for plugin-published client config and route responses.
18
+ - MLflow feedback request/response schemas and trace-header constants.
19
+ - Browser-safe types that let UI packages stay aligned with the server without
20
+ importing Node or Mastra runtime code.
21
+
22
+ ## Parse Client Configuration
23
+
24
+ ```ts
25
+ import { wire } from "@dbx-tools/shared-mastra";
26
+
27
+ const config = wire.MastraClientConfigSchema.parse(await response.json());
28
+ ```
29
+
30
+ `wire.MastraClientConfigSchema` describes the plugin-published client config:
31
+ mount path, default agent, feedback enablement, and MCP route details. Use it to
32
+ bootstrap a UI without hard-coding server paths.
33
+
34
+ ## Use Route Constants
35
+
36
+ ```ts
37
+ import { routes } from "@dbx-tools/shared-mastra";
38
+
39
+ const historyUrl = `${basePath}${routes.MASTRA_ROUTES.history}`;
40
+ const threadsUrl = `${basePath}${routes.MASTRA_ROUTES.threads}`;
41
+ ```
42
+
43
+ `routes.MASTRA_ROUTES` keeps client fetch calls aligned with plugin route names
44
+ for history, threads, models, suggestions, feedback, and embeds.
45
+
46
+ ## Select Threads And Models
47
+
48
+ ```ts
49
+ import { override, thread } from "@dbx-tools/shared-mastra";
50
+
51
+ await fetch(chatUrl, {
52
+ method: "POST",
53
+ headers: {
54
+ [thread.THREAD_ID_HEADER]: activeThreadId,
55
+ [override.MODEL_OVERRIDE_HEADER]: "claude sonnet",
56
+ },
57
+ body: JSON.stringify({ messages }),
58
+ });
59
+ ```
60
+
61
+ Use the header/query constants instead of string literals so browser code,
62
+ server middleware, and tests agree on how a request selects the active
63
+ conversation and model.
64
+
65
+ ## Parse Embed Markers
66
+
67
+ ```ts
68
+ import { marker } from "@dbx-tools/shared-mastra";
69
+
70
+ const parts = marker.parseMarkers("Here is the chart:\n[chart:abc123]");
71
+ const safeText = marker.stripIncompleteMarkerTail(streamingText);
72
+ ```
73
+
74
+ Markers let an agent mention large or delayed artifacts by id, such as
75
+ `[chart:<id>]` and `[data:<statement_id>]`. The parser is useful for rendering
76
+ assistant text while a stream is still arriving.
77
+
78
+ ## Validate Feedback
79
+
80
+ ```ts
81
+ import { feedback } from "@dbx-tools/shared-mastra";
82
+
83
+ const body = feedback.MastraFeedbackRequestSchema.parse({
84
+ traceId: "tr-abc",
85
+ value: true,
86
+ comment: "Helpful",
87
+ });
88
+ ```
89
+
90
+ Feedback constants include the MLflow trace header and default assessment names.
91
+ The schemas validate thumbs/comment payloads before they are sent to the plugin.
92
+
93
+ ## Validate Plugin Responses
94
+
95
+ ```ts
96
+ const threads = wire.MastraThreadsResponseSchema.parse(await res.json());
97
+ const chart = wire.ChartSchema.parse(await chartRes.json());
98
+ const event = wire.GenieWriterEventSchema.parse(writerPayload);
99
+ ```
100
+
101
+ The `wire` module contains response schemas for history, threads, suggestions,
102
+ model lists, charts, statement data, and Genie writer events. It also defines the
103
+ chart/data result shapes consumed by embed renderers.
104
+
105
+ ## Modules
106
+
107
+ - `wire` - zod schemas and types for client config, history, threads, model
108
+ lists, suggestions, charts, statement data, and writer events.
109
+ - `routes` - plugin route segment constants.
110
+ - `marker` - embed-marker parsing and incomplete-tail stripping.
111
+ - `feedback` - MLflow feedback headers, assessment names, request/response
112
+ schemas.
113
+ - `override` - model override header/query/body constants.
114
+ - `thread` - thread id header/query constants.
115
+
116
+ Server-side implementation is in
117
+ [`@dbx-tools/node-appkit-mastra`](../../node/appkit-mastra).
package/index.ts ADDED
@@ -0,0 +1,13 @@
1
+ // GENERATED by projen watch - DO NOT EDIT.
2
+ // Regenerated from the exporting modules in ./src.
3
+ // Hand edits are overwritten on the next watch; this file is read-only.
4
+
5
+ export * as feedback from "./src/feedback";
6
+ export * as marker from "./src/marker";
7
+ export * as override from "./src/override";
8
+ export * as routes from "./src/routes";
9
+ export * as thread from "./src/thread";
10
+ export * as wire from "./src/wire";
11
+ export type { MastraFeedbackValue, MastraFeedbackRequest, MastraFeedbackResponse } from "./src/feedback";
12
+ export type { MarkerType, ParsedMarker } from "./src/marker";
13
+ export type { MastraClientConfig, ServingEndpointsResponse, MastraHistoryUIMessage, MastraHistoryResponse, MastraClearHistoryResponse, MastraThread, MastraThreadsResponse, MastraDeleteThreadResponse, MastraUpdateThreadRequest, MastraUpdateThreadResponse, MastraSuggestionsResponse, ChartType, ChartResult, Chart, StatementData, MastraWriter, StartedEvent, AskGenieDoneEvent, MastraGenieErrorEvent, SummaryEvent, GenieAgentEvent, GenieWriterEvent, GenieWriterEventType, GenieDatasetData, GenieDatasetChart, GenieDataset, GenieSummaryItem, GenieSummaryItemType, GenieAgentResult } from "./src/wire";
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@dbx-tools/shared-mastra",
3
+ "repository": {
4
+ "type": "git",
5
+ "url": "git+https://github.com/reggie-db/dbx-tools.git",
6
+ "directory": "workspaces/shared/mastra"
7
+ },
8
+ "devDependencies": {
9
+ "@types/node": "^24.6.0",
10
+ "tsx": "^4.23.0",
11
+ "typescript": "^5.9.3"
12
+ },
13
+ "dependencies": {
14
+ "zod": "^4.3.6",
15
+ "@dbx-tools/shared-core": "0.1.9",
16
+ "@dbx-tools/shared-genie": "0.1.9",
17
+ "@dbx-tools/shared-model": "0.1.9"
18
+ },
19
+ "main": "index.ts",
20
+ "license": "UNLICENSED",
21
+ "version": "0.1.9",
22
+ "types": "index.ts",
23
+ "type": "module",
24
+ "exports": {
25
+ ".": "./index.ts",
26
+ "./package.json": "./package.json"
27
+ },
28
+ "dbxToolsConfig": {
29
+ "tags": [
30
+ "shared"
31
+ ]
32
+ },
33
+ "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"pnpm exec projen\".",
34
+ "scripts": {
35
+ "build": "projen build",
36
+ "compile": "projen compile",
37
+ "default": "projen default",
38
+ "package": "projen package",
39
+ "post-compile": "projen post-compile",
40
+ "pre-compile": "projen pre-compile",
41
+ "test": "projen test",
42
+ "watch": "projen watch",
43
+ "projen": "projen"
44
+ }
45
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Wire-format contract for the Mastra plugin's user-feedback surface:
3
+ * the response header that carries a turn's MLflow trace id back to the
4
+ * browser, plus the request / response schemas for the feedback route
5
+ * (`MASTRA_ROUTES.feedback`).
6
+ *
7
+ * Feedback is logged to MLflow as a trace *assessment* (thumbs up/down
8
+ * as a boolean, or a freeform comment as text), so it must be tied to a
9
+ * trace. The plugin stamps the active OTel trace id of each turn on the
10
+ * response as {@link MLFLOW_TRACE_ID_HEADER} (`tr-<hex>`); the client
11
+ * captures it per assistant message and sends it back here when the
12
+ * user reacts. Kept dependency-free so both the browser client and the
13
+ * server plugin share one definition.
14
+ */
15
+
16
+ import { z } from "zod";
17
+
18
+ /**
19
+ * Response header carrying the turn's MLflow trace id (`tr-<hex>`),
20
+ * derived from the active OpenTelemetry trace id. Present only when
21
+ * MLflow logging is enabled and a trace was active for the request.
22
+ * The chat client reads it off the stream response and pairs it with
23
+ * the assistant message so a later thumbs / comment attaches to the
24
+ * right trace.
25
+ */
26
+ export const MLFLOW_TRACE_ID_HEADER = "x-mlflow-trace-id";
27
+
28
+ /** Assessment name used for thumbs feedback when the caller omits one. */
29
+ export const DEFAULT_FEEDBACK_NAME = "user_feedback";
30
+
31
+ /** Assessment name used for a freeform comment when no explicit value is sent. */
32
+ export const DEFAULT_COMMENT_NAME = "user_comment";
33
+
34
+ /**
35
+ * Feedback value the assessment records. Boolean for thumbs up/down,
36
+ * number for a rating scale, string for a short categorical label.
37
+ * A freeform comment travels in `comment` instead (or as the value
38
+ * when no thumbs value is present).
39
+ */
40
+ export const MastraFeedbackValueSchema = z.union([z.boolean(), z.number(), z.string()]);
41
+ export type MastraFeedbackValue = z.infer<typeof MastraFeedbackValueSchema>;
42
+
43
+ /**
44
+ * Request body for `POST ${basePath}/route/feedback`.
45
+ *
46
+ * Fields:
47
+ * - `traceId`: MLflow trace id the feedback attaches to (the
48
+ * `tr-<hex>` value the server sent via {@link MLFLOW_TRACE_ID_HEADER}).
49
+ * - `name`: assessment name. Defaults to {@link DEFAULT_FEEDBACK_NAME}
50
+ * for a value, {@link DEFAULT_COMMENT_NAME} for a comment-only submit.
51
+ * - `value`: the thumbs / rating value (omit for a comment-only submit).
52
+ * - `comment`: freeform rationale. Logged as the assessment value when
53
+ * `value` is absent, or as the rationale alongside a `value`.
54
+ *
55
+ * Refined so an empty submission (neither a value nor a non-empty
56
+ * comment) is rejected rather than logging a meaningless assessment.
57
+ */
58
+ export const MastraFeedbackRequestSchema = z
59
+ .object({
60
+ traceId: z.string().min(1),
61
+ name: z.string().optional(),
62
+ value: MastraFeedbackValueSchema.optional(),
63
+ comment: z.string().optional(),
64
+ })
65
+ .refine((v) => v.value !== undefined || (v.comment?.trim().length ?? 0) > 0, {
66
+ message: "feedback requires a value or a non-empty comment",
67
+ });
68
+ export type MastraFeedbackRequest = z.infer<typeof MastraFeedbackRequestSchema>;
69
+
70
+ /**
71
+ * Response from `POST ${basePath}/route/feedback`.
72
+ *
73
+ * `ok` is `true` when the assessment was recorded. It can be `false`
74
+ * (with `ok: false`) without the call throwing - most commonly because
75
+ * the trace hasn't finished exporting to MLflow yet (trace export is
76
+ * asynchronous); the client surfaces a soft "try again" hint rather
77
+ * than an error. `assessmentId` is the created assessment's id on
78
+ * success.
79
+ */
80
+ export const MastraFeedbackResponseSchema = z.object({
81
+ ok: z.boolean(),
82
+ assessmentId: z.string().optional(),
83
+ });
84
+ export type MastraFeedbackResponse = z.infer<typeof MastraFeedbackResponseSchema>;
package/src/marker.ts ADDED
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Agent-prose embed marker grammar.
3
+ *
4
+ * The Mastra agent is instructed to defer visualizations to the host
5
+ * UI by embedding markers in its markdown reply; the UI parses them
6
+ * out and swaps in the matching slot. This module is the single
7
+ * source of truth for that grammar so every consumer parses markers
8
+ * the same way and can't drift from the shapes documented on
9
+ * {@link ChartSchema} / {@link StatementDataSchema} in `protocol.ts`
10
+ * and the instructions the server hands the model.
11
+ *
12
+ * One grammar, open-ended type. A marker is `[<type>:<id>]`. The
13
+ * `<type>` is an opaque token that names the embed kind; the host
14
+ * resolves it against the generic `/embed/<type>/<id>` route, which
15
+ * 404s any type the server doesn't register. The grammar does NOT
16
+ * hard-code the known types - new embed kinds work end to end
17
+ * without touching this regex. Today the server registers:
18
+ *
19
+ * - `[chart:<chartId>]` - the `<chartId>` is a v4 UUID minted by
20
+ * the chart subsystem (`prepare_chart` / `render_data`) and
21
+ * resolves to a cached Echarts spec.
22
+ * - `[data:<statementId>]` - the `<statementId>` is a Databricks
23
+ * statement id (a time-ordered UUID) and resolves to the
24
+ * statement's rows.
25
+ *
26
+ * The `<id>` of a real embed is UUID-shaped (`8-4-4-4-12` hex): both
27
+ * id kinds above are UUIDs, just different versions (chart = v4,
28
+ * statement = v7-style). The grammar deliberately captures ANY
29
+ * non-bracket id token, not just UUID-shaped ones, so a fabricated
30
+ * marker the model glued together from a label
31
+ * (e.g. `[chart:placeholder]` or
32
+ * `[chart:01f163b6-1eac-region-fill-oos]`) still parses as a marker.
33
+ * Matching it - rather than letting the broad regex miss it - is what
34
+ * lets the host UI consume and obscure the bogus marker instead of
35
+ * leaking the literal `[chart:...]` text into the rendered prose.
36
+ * Whether an id is a genuine embed is a separate {@link isUuid}
37
+ * check the caller runs on {@link ParsedMarker.id}.
38
+ *
39
+ * Parse everything through {@link parseMarkers}, validate the id with
40
+ * {@link isUuid}, and branch on the returned {@link ParsedMarker.type}.
41
+ */
42
+
43
+ /**
44
+ * The embed kind named by a marker's `<type>` segment. Left as an
45
+ * open `string` (not a closed union) so the grammar stays generic:
46
+ * the authoritative set of supported types lives in the server's
47
+ * `/embed/:type/:id` resolver registry, and an unknown type simply
48
+ * 404s. The two types the server ships today are `"chart"` and
49
+ * `"data"`.
50
+ */
51
+ export type MarkerType = string;
52
+
53
+ /**
54
+ * Single matcher for ANY embed marker, regardless of type. Matches
55
+ * `[<type>:<id>]`. Capture group 1 is the marker type token (e.g.
56
+ * `chart`, `data`); group 2 is the id - any run of non-bracket,
57
+ * non-whitespace characters, NOT constrained to a UUID shape.
58
+ *
59
+ * Neither the type nor the id shape is enumerated here. The type is
60
+ * left open because the server's resolver registry decides which
61
+ * types are real and 404s the rest, so a new embed kind needs no
62
+ * grammar change. The id is left broad on purpose: a fabricated
63
+ * marker the model emits from a label (e.g. `[chart:placeholder]`)
64
+ * MUST still match so the host UI can consume and obscure it rather
65
+ * than leak the literal `[chart:...]` text into the prose. The id is
66
+ * then validated with {@link isUuid} downstream; non-UUID ids resolve
67
+ * to no embed.
68
+ *
69
+ * Safe to share as a `/g` constant: it's only ever consumed via
70
+ * {@link String.prototype.matchAll}, which clones the regex and
71
+ * never advances this object's `lastIndex`. (A `.exec()`/`.test()`
72
+ * loop would need a fresh regex each time, but we don't do that.)
73
+ */
74
+ const MARKER_RE = /\[([A-Za-z][A-Za-z0-9_-]*):([^\]\s]+)\]/g;
75
+
76
+ /**
77
+ * UUID matcher (`8-4-4-4-12` hex, any version) anchored to the whole
78
+ * string. Covers v4 chart ids and time-ordered (v7-style) statement
79
+ * ids alike. Used by {@link isUuid} to tell a genuine embed id apart
80
+ * from a label the model fabricated into a marker.
81
+ */
82
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
83
+
84
+ /**
85
+ * Whether `id` is a UUID-shaped embed id. The marker grammar
86
+ * ({@link MARKER_RE}) matches any non-bracket id token so fabricated
87
+ * markers don't leak as literal prose; this is the guard callers run
88
+ * to decide whether an id is a real embed (render the slot) or a
89
+ * bogus label to obscure (drop the marker entirely).
90
+ */
91
+ export function isUuid(id: string): boolean {
92
+ return UUID_RE.test(id);
93
+ }
94
+
95
+ /** One marker found in agent prose, with its source span. */
96
+ export interface ParsedMarker {
97
+ /** Marker kind token (e.g. `chart`, `data`); see {@link MarkerType}. */
98
+ type: MarkerType;
99
+ /** Raw id captured from the marker (chart id or statement id). */
100
+ id: string;
101
+ /** Index of the marker's first character in the source text. */
102
+ start: number;
103
+ /** Index one past the marker's last character. */
104
+ end: number;
105
+ }
106
+
107
+ /**
108
+ * Parse every embed marker out of `text`, in source order. A single
109
+ * regex pass means matches never overlap, so callers can splice the
110
+ * spans directly without a sort/dedupe step. This is the one entry
111
+ * point for marker parsing: type branching runs on the returned
112
+ * markers. The grammar ({@link MARKER_RE}) matches any non-bracket id
113
+ * token, so a returned {@link ParsedMarker.id} is NOT guaranteed to
114
+ * be a real embed id - run it through {@link isUuid} to tell a
115
+ * genuine embed apart from a fabricated label, which the caller then
116
+ * obscures rather than rendering.
117
+ */
118
+ export function parseMarkers(text: string): ParsedMarker[] {
119
+ const out: ParsedMarker[] = [];
120
+ for (const match of text.matchAll(MARKER_RE)) {
121
+ const start = match.index ?? 0;
122
+ out.push({
123
+ // Group 1 is the `<type>` token; the server's embed registry
124
+ // (not this parser) decides whether it's supported.
125
+ type: match[1] ?? "",
126
+ id: match[2] ?? "",
127
+ start,
128
+ end: start + match[0].length,
129
+ });
130
+ }
131
+ return out;
132
+ }
133
+
134
+ /**
135
+ * Strip a trailing partial marker (`[chart`, `[data:01f1`, ...) that
136
+ * hasn't received its closing `]` yet. While the model streams
137
+ * text-deltas a marker arrives across several chunks, and rendering
138
+ * each interim state would briefly flash the literal `[chart:abc-`
139
+ * prefix before the closing bracket swaps it for a slot. Hiding any
140
+ * trailing `[<non-whitespace-non-bracket>...` suffix keeps the prose
141
+ * stable until either a `]` (full marker) or whitespace (definitely
142
+ * not a marker) lands. Persisted transcripts never end mid-marker,
143
+ * so this is a no-op on reload.
144
+ */
145
+ export function stripIncompleteMarkerTail(text: string): string {
146
+ return text.replace(/\[[^\s[\]]*$/, "");
147
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Per-request model-override wire convention shared by the Mastra
3
+ * plugin server (`@dbx-tools/appkit-mastra`'s `extractModelOverride`)
4
+ * and the browser chat client (`@dbx-tools/appkit-mastra-ui`). Kept
5
+ * here so the header / query / body field names can never drift
6
+ * between the side that sets them and the side that reads them.
7
+ */
8
+
9
+ /** HTTP header inspected for a per-request model override. */
10
+ export const MODEL_OVERRIDE_HEADER = "x-mastra-model";
11
+
12
+ /**
13
+ * HTTP header that pins a specific thread id for the request. When
14
+ * present, the server uses it as the conversation thread id instead of
15
+ * the session cookie, enabling the client to switch between stored
16
+ * conversations without resetting the cookie.
17
+ */
18
+ export const THREAD_OVERRIDE_HEADER = "x-mastra-thread-id";
19
+
20
+ /** Query string parameter inspected for a per-request model override. */
21
+ export const MODEL_OVERRIDE_QUERY = "model";
22
+
23
+ /** Body fields (in priority order) inspected for a per-request model override. */
24
+ export const MODEL_OVERRIDE_BODY_FIELDS = ["model", "modelId"] as const;
package/src/routes.ts ADDED
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Route segments the Mastra plugin mounts under its `basePath`
3
+ * (`/api/<plugin-name>`). Shared between the server's route
4
+ * registration and the browser client (`MastraPluginClient` in
5
+ * `@dbx-tools/appkit-mastra-ui`) so a relayout - or a rename of a
6
+ * sub-path - is a one-line change here and the two can never drift.
7
+ *
8
+ * The agent-scoped segments (`history`, `threads`, `suggestions`) take
9
+ * an optional `/:agentId` suffix; the default agent uses the bare
10
+ * segment. Conversation streaming itself rides the standard Mastra
11
+ * agent routes (`@mastra/client-js`'s `getAgent(id).stream()`), so
12
+ * there's no chat segment here.
13
+ *
14
+ * `feedback` is the plugin-owned POST endpoint the chat UI calls to
15
+ * log a thumbs / comment assessment against a turn's MLflow trace (see
16
+ * `feedback.ts`); it is not agent-scoped (a trace id identifies the
17
+ * turn on its own).
18
+ */
19
+ export const MASTRA_ROUTES = {
20
+ history: "/route/history",
21
+ threads: "/route/threads",
22
+ feedback: "/route/feedback",
23
+ suggestions: "/suggestions",
24
+ models: "/models",
25
+ embed: "/embed",
26
+ } as const;
package/src/thread.ts ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Per-request thread-selection wire convention shared by the Mastra
3
+ * plugin server (`@dbx-tools/appkit-mastra`'s thread-id resolver) and
4
+ * the browser chat client (`@dbx-tools/appkit-mastra-ui`).
5
+ *
6
+ * A chat client owns multiple conversations ("threads") for the same
7
+ * resource (the authenticated user). It names the thread a given
8
+ * request targets by stamping its id here; the server reads it back
9
+ * and pins `RequestContext`'s thread id to that value (falling back to
10
+ * the per-session cookie when absent). Keeping the header / query
11
+ * names in one place means the side that sets them and the side that
12
+ * reads them can never drift.
13
+ */
14
+
15
+ /** HTTP header inspected for the thread id a request targets. */
16
+ export const THREAD_ID_HEADER = "x-mastra-thread-id";
17
+
18
+ /** Query string parameter inspected for the thread id a request targets. */
19
+ export const THREAD_ID_QUERY = "threadId";
package/src/wire.ts ADDED
@@ -0,0 +1,649 @@
1
+ /**
2
+ * Wire-format contract for `@dbx-tools/appkit-mastra`: the
3
+ * dependency-free zod schemas + inferred types every consumer (the
4
+ * React client, browser bundles, the server plugin) shares.
5
+ *
6
+ * Kept free of `pg`, `fastembed`, and the Mastra runtime so the
7
+ * client can import these schemas without dragging in server-only
8
+ * dependencies. Three layers live here:
9
+ *
10
+ * 1. The descriptor published by `MastraPlugin.clientConfig()`
11
+ * plus the REST payloads its routes return (models, history,
12
+ * suggestions, chart embeds, statement data). The server
13
+ * derives every path from the plugin mount and publishes the
14
+ * resolved paths so the client composes URLs without
15
+ * hard-coding `/api/mastra` - rename the plugin and the client
16
+ * keeps working.
17
+ * 2. The `ctx.writer` event vocabulary: the wire-derived
18
+ * {@link GenieChatEvent}s from `@dbx-tools/genie-shared` plus
19
+ * the Mastra-only agent lifecycle events, unified as
20
+ * {@link GenieWriterEvent} so subscribers narrow both halves
21
+ * with a single `switch (event.type)`.
22
+ * 3. The Genie agent's workflow output shapes
23
+ * ({@link GenieAgentResult} and its summary / dataset items).
24
+ *
25
+ * The route segments live in the sibling `routes.ts`
26
+ * ({@link MASTRA_ROUTES}) and the embed-marker grammar in `marker.ts`,
27
+ * so this file stays purely declarative (schemas + inferred types).
28
+ * The browser client that drives these routes (`MastraPluginClient`)
29
+ * ships from `@dbx-tools/appkit-mastra-ui`.
30
+ */
31
+
32
+ import { genieModel, type GenieChatEvent, type MessageStatus } from "@dbx-tools/shared-genie";
33
+ import { model } from "@dbx-tools/shared-model";
34
+ import { z } from "zod";
35
+
36
+ /* ---------------------------- client config ---------------------------- */
37
+
38
+ /**
39
+ * JSON-safe descriptor published by the Mastra plugin's
40
+ * `clientConfig()`.
41
+ *
42
+ * Only the irreducible data lives here: `basePath` (the one path the
43
+ * client can't otherwise know, since it encodes the plugin's mount
44
+ * name) plus the runtime agent roster. Every concrete endpoint URL is
45
+ * derived from `basePath` by the browser client
46
+ * (`MastraPluginClient`) using the shared {@link MASTRA_ROUTES}
47
+ * segments, so there's nothing to publish per-route.
48
+ *
49
+ * Fields:
50
+ * - `basePath`: plugin mount path. Always `/api/<pluginName>`. Feed
51
+ * it to `new MastraPluginClient(config)` (from
52
+ * `@dbx-tools/appkit-mastra-ui`) to get a typed client over every
53
+ * route plus the standard agent stream.
54
+ * - `defaultAgent`: agent id the client converses with when it
55
+ * doesn't name one.
56
+ * - `agents`: every registered agent id in registration order.
57
+ * - `feedbackEnabled`: whether the server can log user feedback to
58
+ * MLflow (MLflow tracing is configured). Drives whether the chat UI
59
+ * surfaces thumbs / comment controls. Defaults to `false` so a
60
+ * config published by an older server (without the field) parses
61
+ * cleanly to "off".
62
+ */
63
+ export const MastraClientConfigSchema = z.object({
64
+ basePath: z.string(),
65
+ defaultAgent: z.string(),
66
+ agents: z.array(z.string()),
67
+ feedbackEnabled: z.boolean().default(false),
68
+ });
69
+ export type MastraClientConfig = z.infer<typeof MastraClientConfigSchema>;
70
+
71
+ /* ---------------------------- model catalogue ---------------------------- */
72
+
73
+ /**
74
+ * JSON payload returned by `GET ${basePath}/models`. Wraps the shared
75
+ * {@link model.ServingEndpointSummarySchema} (re-exported from
76
+ * `@dbx-tools/shared-model`) so the catalogue
77
+ * descriptor has a single definition the client, server, and the
78
+ * standalone `@dbx-tools/model` toolkit all agree on.
79
+ */
80
+ export const ServingEndpointsResponseSchema = z.object({
81
+ endpoints: z.array(model.ServingEndpointSummarySchema),
82
+ });
83
+ export type ServingEndpointsResponse = z.infer<typeof ServingEndpointsResponseSchema>;
84
+
85
+ /* ----------------------------- chat history ----------------------------- */
86
+
87
+ /**
88
+ * Structural shape for an AI SDK V5 `UIMessage`. Defined locally
89
+ * so the shared types package stays dependency-free (no `ai`
90
+ * import). The runtime values returned by the `/history` endpoint
91
+ * are produced by `toAISdkV5Messages` and are 1:1 compatible with
92
+ * `UIMessage` from the `ai` package; clients can safely cast when
93
+ * needed.
94
+ */
95
+ export const MastraHistoryUIMessageSchema = z.object({
96
+ id: z.string(),
97
+ role: z.enum(["system", "user", "assistant"]),
98
+ parts: z.array(z.unknown()).readonly(),
99
+ metadata: z.unknown().optional(),
100
+ });
101
+ export type MastraHistoryUIMessage = z.infer<typeof MastraHistoryUIMessageSchema>;
102
+
103
+ /**
104
+ * JSON payload returned by `GET ${basePath}/history`.
105
+ *
106
+ * Fields:
107
+ * - `uiMessages`: page of UI-formatted messages, oldest -> newest.
108
+ * Always chronological regardless of the underlying pagination
109
+ * order so the client can prepend the array to the live
110
+ * transcript without sorting.
111
+ * - `page`: zero-indexed page that produced this response.
112
+ * - `perPage`: number of items requested per page.
113
+ * - `total`: total number of messages in the thread.
114
+ * - `hasMore`: true when at least one older page is still
115
+ * available.
116
+ */
117
+ export const MastraHistoryResponseSchema = z.object({
118
+ uiMessages: z.array(MastraHistoryUIMessageSchema),
119
+ page: z.number(),
120
+ perPage: z.number(),
121
+ total: z.number(),
122
+ hasMore: z.boolean(),
123
+ });
124
+ export type MastraHistoryResponse = z.infer<typeof MastraHistoryResponseSchema>;
125
+
126
+ /**
127
+ * JSON payload returned by `DELETE ${basePath}/history`. Deletes
128
+ * every persisted message + workflow snapshot tied to the caller's
129
+ * thread, so the next chat turn starts from a clean slate. The
130
+ * session cookie that anchors the thread id is preserved so the
131
+ * caller doesn't lose its identity - only the contents go away.
132
+ *
133
+ * `ok` is always `true` on success; the response object is kept
134
+ * as a struct (vs a bare 204) so future fields (e.g. `deletedAt`,
135
+ * `messages`) can be added without bumping the contract.
136
+ *
137
+ * Fields:
138
+ * - `ok`: literal `true` on success.
139
+ * - `agentId`: agent whose history was cleared.
140
+ * - `threadId`: thread id that was wiped.
141
+ * - `cleared`: number of messages the thread held before
142
+ * deletion. Useful for client-side "cleared 12 messages"
143
+ * toasts; `0` is reported when the thread was already empty
144
+ * (call is idempotent).
145
+ */
146
+ export const MastraClearHistoryResponseSchema = z.object({
147
+ ok: z.literal(true),
148
+ agentId: z.string(),
149
+ threadId: z.string(),
150
+ cleared: z.number(),
151
+ });
152
+ export type MastraClearHistoryResponse = z.infer<typeof MastraClearHistoryResponseSchema>;
153
+
154
+ /* -------------------------------- threads -------------------------------- */
155
+
156
+ /**
157
+ * A single conversation thread the resource (authenticated user) owns,
158
+ * as returned by `GET ${basePath}/threads`. Mirrors Mastra's
159
+ * `StorageThreadType` but with JSON-safe ISO-8601 timestamps (the wire
160
+ * can't carry `Date`).
161
+ *
162
+ * Fields:
163
+ * - `id`: thread id. Pass it back as the thread-selection header
164
+ * (`THREAD_ID_HEADER`) on a stream / history / delete call to act
165
+ * on this conversation.
166
+ * - `title`: human-readable title. Present once the agent's memory
167
+ * has auto-generated one (after the first turn); absent on a
168
+ * brand-new thread, so the UI falls back to a placeholder.
169
+ * - `resourceId`: owning resource (the user id). Always the caller's
170
+ * own resource - the list route filters by it server-side.
171
+ * - `createdAt` / `updatedAt`: ISO-8601 timestamps. `updatedAt` is
172
+ * the natural sort key for "most recent conversations first".
173
+ * - `metadata`: opaque thread metadata, passed through untouched.
174
+ */
175
+ export const MastraThreadSchema = z.object({
176
+ id: z.string(),
177
+ title: z.string().optional(),
178
+ resourceId: z.string(),
179
+ createdAt: z.string(),
180
+ updatedAt: z.string(),
181
+ metadata: z.unknown().optional(),
182
+ });
183
+ export type MastraThread = z.infer<typeof MastraThreadSchema>;
184
+
185
+ /**
186
+ * JSON payload returned by `GET ${basePath}/threads`. One page of the
187
+ * caller's conversation threads, newest (`updatedAt` DESC) first.
188
+ *
189
+ * Fields:
190
+ * - `threads`: page of threads for the caller's resource.
191
+ * - `page`: zero-indexed page that produced this response.
192
+ * - `perPage`: number of items requested per page.
193
+ * - `total`: total number of threads the resource owns.
194
+ * - `hasMore`: true when at least one more page is available.
195
+ */
196
+ export const MastraThreadsResponseSchema = z.object({
197
+ threads: z.array(MastraThreadSchema),
198
+ page: z.number(),
199
+ perPage: z.number(),
200
+ total: z.number(),
201
+ hasMore: z.boolean(),
202
+ });
203
+ export type MastraThreadsResponse = z.infer<typeof MastraThreadsResponseSchema>;
204
+
205
+ /**
206
+ * JSON payload returned by `DELETE ${basePath}/threads` (thread id
207
+ * supplied via the thread-selection header / `threadId` query). Wipes
208
+ * the named thread and every message on it.
209
+ *
210
+ * Fields:
211
+ * - `ok`: literal `true` on success.
212
+ * - `agentId`: agent whose thread was deleted.
213
+ * - `threadId`: thread id that was removed.
214
+ * - `deleted`: `true` when a thread row existed and was removed,
215
+ * `false` when it was already gone (call is idempotent).
216
+ */
217
+ export const MastraDeleteThreadResponseSchema = z.object({
218
+ ok: z.literal(true),
219
+ agentId: z.string(),
220
+ threadId: z.string(),
221
+ deleted: z.boolean(),
222
+ });
223
+ export type MastraDeleteThreadResponse = z.infer<typeof MastraDeleteThreadResponseSchema>;
224
+
225
+ /** Longest thread title the rename route accepts (trimmed server-side). */
226
+ export const MASTRA_THREAD_TITLE_MAX = 200;
227
+
228
+ /**
229
+ * JSON body for `PATCH ${basePath}/threads` (thread id supplied via the
230
+ * thread-selection header / `threadId` query). Renames a single
231
+ * conversation.
232
+ *
233
+ * Fields:
234
+ * - `title`: the new human-readable title. Trimmed and capped at
235
+ * {@link MASTRA_THREAD_TITLE_MAX} characters server-side; must be
236
+ * non-empty after trimming.
237
+ */
238
+ export const MastraUpdateThreadRequestSchema = z.object({
239
+ title: z.string().trim().min(1).max(MASTRA_THREAD_TITLE_MAX),
240
+ });
241
+ export type MastraUpdateThreadRequest = z.infer<typeof MastraUpdateThreadRequestSchema>;
242
+
243
+ /**
244
+ * JSON payload returned by `PATCH ${basePath}/threads`. Echoes the
245
+ * renamed thread in its post-update wire shape so the client can reflect
246
+ * the new title without a re-fetch.
247
+ *
248
+ * Fields:
249
+ * - `ok`: literal `true` on success.
250
+ * - `agentId`: agent whose thread was renamed.
251
+ * - `thread`: the updated thread (carries the new `title`).
252
+ */
253
+ export const MastraUpdateThreadResponseSchema = z.object({
254
+ ok: z.literal(true),
255
+ agentId: z.string(),
256
+ thread: MastraThreadSchema,
257
+ });
258
+ export type MastraUpdateThreadResponse = z.infer<typeof MastraUpdateThreadResponseSchema>;
259
+
260
+ /* ------------------------------ suggestions ------------------------------ */
261
+
262
+ /**
263
+ * JSON payload returned by `GET ${basePath}/suggestions`.
264
+ *
265
+ * Carries the curated starter questions for an agent's Genie space -
266
+ * the `sample_questions` an author configured on the space, surfaced
267
+ * as one-tap prompts on the chat's empty state. `questions` is empty
268
+ * when the agent has no Genie space (or the space defines none), so
269
+ * the client renders nothing in that case rather than falling back to
270
+ * built-in example prompts.
271
+ */
272
+ export const MastraSuggestionsResponseSchema = z.object({
273
+ questions: z.array(z.string()),
274
+ });
275
+ export type MastraSuggestionsResponse = z.infer<typeof MastraSuggestionsResponseSchema>;
276
+
277
+ /* --------------------------------- charts --------------------------------- */
278
+
279
+ /**
280
+ * Allowed chart types the planner can pick. Defined as a
281
+ * discriminated literal union so each variant carries its own
282
+ * `.describe()` clause - the server-side planner's prompt
283
+ * formatter walks `.options` to inline the descriptions into the
284
+ * model instructions, keeping the prompt in lock-step with the
285
+ * schema by construction. The runtime type is the plain string
286
+ * union (`"bar" | "line" | ...`), so consumers that just need a
287
+ * discriminator value behave the same as if it were a
288
+ * `z.enum([...])`.
289
+ */
290
+ export const ChartTypeSchema = z
291
+ .union([
292
+ z
293
+ .literal("bar")
294
+ .describe(
295
+ "comparing a numeric value across a small/medium set of discrete categories (top-N, ranking, group-by)",
296
+ ),
297
+ z
298
+ .literal("line")
299
+ .describe(
300
+ "ordered-axis trend (time series, sequence, rank) where the x-axis has natural order",
301
+ ),
302
+ z.literal("area").describe("stacked-trend emphasis - cumulative or composition over time"),
303
+ z
304
+ .literal("scatter")
305
+ .describe("two-numeric-axis correlations between fields (e.g. price vs. quantity)"),
306
+ z.literal("pie").describe("parts-of-a-whole when 2-7 categories sum to a meaningful total"),
307
+ ])
308
+ .describe("The chart shape that best matches the data and intent.");
309
+ export type ChartType = z.infer<typeof ChartTypeSchema>;
310
+
311
+ /**
312
+ * Resolved chart plan a settled chart entry carries on its
313
+ * `result` field. `option` is the full Echarts spec; pass
314
+ * straight into `<ReactECharts option={...}>`.
315
+ */
316
+ export const ChartResultSchema = z.object({
317
+ chartType: ChartTypeSchema,
318
+ option: z
319
+ .record(z.string(), z.unknown())
320
+ .describe(
321
+ "Fully-resolved Echarts `EChartsOption` JSON for this chart - drop directly into an Echarts instance (or `<ReactECharts option={...} />`) without further processing. Includes title / tooltip / legend / grid / axis / series defaults already merged in.",
322
+ ),
323
+ });
324
+ export type ChartResult = z.infer<typeof ChartResultSchema>;
325
+
326
+ /**
327
+ * Wire-format AND server-side cache shape for a chart entry.
328
+ * Three lifecycle states inferred from the two optional fields
329
+ * (no discriminator needed):
330
+ *
331
+ * - `result` set -> chart is ready to render
332
+ * - `error` set -> planner / data fetch failed
333
+ * - both `result` and `error` unset -> still processing
334
+ *
335
+ * `option` is typed as a generic record so this package stays
336
+ * dependency-free of `echarts`. The server (`chart.ts`) imports
337
+ * this schema directly; the demo client polls the
338
+ * `/embed/chart/:id` route and parses responses against it.
339
+ */
340
+ export const ChartSchema = z.object({
341
+ chartId: z
342
+ .string()
343
+ .describe(
344
+ "Opaque id minted by the chart subsystem. Embed verbatim as `[chart:<chartId>]` in agent prose; the host UI resolves it against this cache entry.",
345
+ ),
346
+ error: z
347
+ .string()
348
+ .optional()
349
+ .describe(
350
+ "Error message when the chart failed (data fetch error, planner error, empty dataset). Mutually exclusive with `result`; absent on success and while processing.",
351
+ ),
352
+ result: ChartResultSchema.optional().describe(
353
+ "Resolved chart plan. Absent while processing and when the run errored.",
354
+ ),
355
+ });
356
+ export type Chart = z.infer<typeof ChartSchema>;
357
+
358
+ /* ------------------------------- statements ------------------------------- */
359
+
360
+ /**
361
+ * Wire-format payload returned by `GET ${basePath}/embed/data/:id`.
362
+ *
363
+ * Mirrors the agent-side `get_statement` tool's output so the
364
+ * host UI and the LLM see the exact same shape for the same
365
+ * statement; the route is what resolves `[data:<statement_id>]`
366
+ * markers the agent embeds in prose.
367
+ *
368
+ * Fields:
369
+ * - `columns`: column names in declaration order.
370
+ * - `rows`: row records keyed by column name. Cell values are
371
+ * either coerced numbers or the original strings (Genie /
372
+ * Statement Execution returns every cell as `string | null`;
373
+ * numeric-looking cells are coerced server-side so the UI
374
+ * can format with `tabular-nums` without re-parsing).
375
+ * - `rowCount`: total row count upstream (independent of the
376
+ * `limit` cap). Compare against `rows.length` to detect
377
+ * truncation - `truncated` is the precomputed flag.
378
+ * - `truncated`: `true` when the server clipped rows to honor
379
+ * the route's row cap; the UI should surface a "showing N of
380
+ * M rows" affordance in that case.
381
+ */
382
+ export const StatementDataSchema = z.object({
383
+ columns: z.array(z.string()),
384
+ rows: z.array(z.record(z.string(), z.unknown())),
385
+ rowCount: z.number(),
386
+ truncated: z.boolean(),
387
+ });
388
+ export type StatementData = z.infer<typeof StatementDataSchema>;
389
+
390
+ /* ----------------------------- writer surface ---------------------------- */
391
+
392
+ /**
393
+ * The `ToolStream`-shaped writer the Mastra Genie agent and chart
394
+ * helpers publish events through. Defined here (vs imported from
395
+ * `@mastra/core`) so helpers in `@dbx-tools/appkit-mastra` can
396
+ * accept any object with a `.write` method without dragging
397
+ * Mastra's full `ToolStream` (and its agent / tool typings) into
398
+ * call sites. The actual Mastra `ctx.writer` is assignable to
399
+ * this shape so callers pass it straight through.
400
+ *
401
+ * Kept as a plain TypeScript interface (vs a zod schema) because
402
+ * the contract is a method - zod can only validate the shape via
403
+ * `z.custom`, which adds noise without buying any runtime check.
404
+ */
405
+ export interface MastraWriter {
406
+ write: (chunk: unknown) => unknown;
407
+ }
408
+
409
+ /* ---------------- mastra-only genie-agent events ---------------- */
410
+
411
+ /**
412
+ * Mastra-only lifecycle event: the Genie tool invocation started.
413
+ * Emitted immediately when the calling agent invokes the Genie
414
+ * tool, before any inner agent / wire activity, so the UI can
415
+ * pop a "Thinking..." pill the instant the model decides to
416
+ * delegate. `conversationId` / `messageId` are absent on this
417
+ * first emit (no Genie round-trip yet). Field names are
418
+ * camelCase (vs the snake_case wire events) to mirror the
419
+ * Genie agent's own internal data plumbing.
420
+ */
421
+ export const StartedEventSchema = z.object({
422
+ type: z.literal("started"),
423
+ spaceId: z.string(),
424
+ /**
425
+ * Genie conversation id, populated only when this `started`
426
+ * event corresponds to a follow-up turn on an existing
427
+ * conversation. Absent on the first turn.
428
+ */
429
+ conversationId: z.string().optional(),
430
+ /**
431
+ * Genie message id, populated only after the first wire
432
+ * `message` event lands. Absent on the immediate-on-invoke
433
+ * emit.
434
+ */
435
+ messageId: z.string().optional(),
436
+ /** Question the Genie agent sent to Genie. */
437
+ content: z.string(),
438
+ });
439
+ export type StartedEvent = z.infer<typeof StartedEventSchema>;
440
+
441
+ /**
442
+ * Mastra-only lifecycle event: one `ask_genie` invocation
443
+ * finished. Carries the hydrated `statementIds` (rows are fetched
444
+ * via `getStatement` separately) and Genie's final prose answer
445
+ * so the UI can move from "thinking" to "answered" without
446
+ * waiting for the Genie agent's whole reasoning loop to end.
447
+ */
448
+ export const AskGenieDoneEventSchema = z.object({
449
+ type: z.literal("ask_genie_done"),
450
+ spaceId: z.string(),
451
+ conversationId: z.string().optional(),
452
+ messageId: z.string().optional(),
453
+ /** Genie's natural-language answer for the turn, if any. */
454
+ answer: z.string().optional(),
455
+ /** Statement ids for any non-empty result sets this turn produced. */
456
+ statementIds: z.array(z.string()),
457
+ /**
458
+ * Terminal wire status (`COMPLETED` / `FAILED` / `CANCELLED`).
459
+ * Mirrors the source `result` event's status so subscribers
460
+ * can react to ask-level completion without re-walking history.
461
+ * Treated as `z.custom<MessageStatus>` because the SDK is the
462
+ * source of truth for the enum values.
463
+ */
464
+ status: z.custom<MessageStatus>((v) => typeof v === "string"),
465
+ });
466
+ export type AskGenieDoneEvent = z.infer<typeof AskGenieDoneEventSchema>;
467
+
468
+ /**
469
+ * Mastra-only error event: terminal Genie agent / transport
470
+ * error. Genie's own `FAILED` / `CANCELLED` come through the
471
+ * wire's `result` event - this variant is for failures the wire
472
+ * can't represent (network, Genie agent crash, planner error,
473
+ * etc.) plus a UI-friendly mirror of `result` when the status is
474
+ * non-`COMPLETED`.
475
+ */
476
+ export const MastraGenieErrorEventSchema = z.object({
477
+ type: z.literal("error"),
478
+ spaceId: z.string().optional(),
479
+ conversationId: z.string().optional(),
480
+ messageId: z.string().optional(),
481
+ error: z.string(),
482
+ });
483
+ export type MastraGenieErrorEvent = z.infer<typeof MastraGenieErrorEventSchema>;
484
+
485
+ /**
486
+ * Mastra-only lifecycle event: the inner Genie agent's
487
+ * structured-output coercion has landed. Fires once per Genie
488
+ * tool invocation, AFTER `agent.generate(...)` completes (i.e.
489
+ * the inner loop + Mastra's structuring pass have both
490
+ * finished) and BEFORE the wrapper hydrates each `data` item
491
+ * with a chart. Signals to the host UI that the agent has
492
+ * stopped reasoning and is moving into chart generation.
493
+ *
494
+ * The structuring pass itself is opaque (it runs inside
495
+ * Mastra's `agent.generate(...)` together with the tool loop)
496
+ * so this is the earliest hook we can offer; we can't fire a
497
+ * "summary started" event the way we fire `started`.
498
+ */
499
+ export const SummaryEventSchema = z.object({
500
+ type: z.literal("summary"),
501
+ spaceId: z.string(),
502
+ /** Total number of items in the agent's structured summary. */
503
+ items: z.number().int().nonnegative(),
504
+ /** Count of `text` / prose items in the summary. */
505
+ textItems: z.number().int().nonnegative(),
506
+ /**
507
+ * Count of `data` items the wrapper will hydrate into charts.
508
+ * The host UI can use this to seed N chart skeletons before
509
+ * the per-chart events arrive.
510
+ */
511
+ dataItems: z.number().int().nonnegative(),
512
+ });
513
+ export type SummaryEvent = z.infer<typeof SummaryEventSchema>;
514
+
515
+ /**
516
+ * Mastra-only event union. Each variant uses the same flat
517
+ * `{type, ...fields}` shape as {@link GenieChatEvent} so
518
+ * subscribers union both with a single `switch (event.type)`.
519
+ */
520
+ export const GenieAgentEventSchema = z.discriminatedUnion("type", [
521
+ StartedEventSchema,
522
+ AskGenieDoneEventSchema,
523
+ MastraGenieErrorEventSchema,
524
+ SummaryEventSchema,
525
+ ]);
526
+ export type GenieAgentEvent = z.infer<typeof GenieAgentEventSchema>;
527
+
528
+ /**
529
+ * The unified writer-event vocabulary subscribers see on
530
+ * `ctx.writer`: the wire-derived {@link GenieChatEvent} union
531
+ * **plus** Mastra-only events from {@link GenieAgentEvent}. Each
532
+ * variant is a flat `{type, ...fields}` object; consumers narrow
533
+ * on `type` and read fields inline - there is no payload wrapper
534
+ * and no writer-boundary translator.
535
+ *
536
+ * Composed via `z.union` (not `z.discriminatedUnion`) because
537
+ * both halves are themselves discriminated unions on the same
538
+ * `type` key.
539
+ */
540
+ export const GenieWriterEventSchema = z.union([
541
+ genieModel.GenieChatEventSchema,
542
+ GenieAgentEventSchema,
543
+ ]);
544
+ export type GenieWriterEvent = z.infer<typeof GenieWriterEventSchema>;
545
+
546
+ /** Discriminator type for {@link GenieWriterEvent}. */
547
+ export type GenieWriterEventType = GenieWriterEvent["type"];
548
+
549
+ /* ------------------------- summary + dataset ------------------------ */
550
+
551
+ /**
552
+ * Tabular payload embedded in every {@link GenieSummaryItem}
553
+ * `visualize` dataset. Always present: hydrated by the workflow's
554
+ * agent step before the finalize step runs, so consumers can render
555
+ * a table fallback regardless of chart-planner outcome.
556
+ *
557
+ * Fields:
558
+ * - `columns`: column names in display order.
559
+ * - `rows`: tabular rows keyed by column name.
560
+ * - `rowCount`: total row count Genie reported (may exceed
561
+ * `rows.length` when the statement was truncated).
562
+ */
563
+ export const GenieDatasetDataSchema = z.object({
564
+ columns: z.array(z.string()),
565
+ rows: z.array(z.record(z.string(), z.unknown())),
566
+ rowCount: z.number(),
567
+ });
568
+ export type GenieDatasetData = z.infer<typeof GenieDatasetDataSchema>;
569
+
570
+ /**
571
+ * Slim chart reference attached to a visualize dataset. Only
572
+ * present when planning succeeded.
573
+ *
574
+ * `option` is intentionally NOT included. The resolved Echarts
575
+ * spec lives off-band in the chart cache: the host UI fetches it
576
+ * by `chartId` via `${MastraClientConfig.embedPathTemplate}`
577
+ * (`/embed/chart/:id`, see {@link Chart}). Embedding the full spec inline would
578
+ * inflate every dataset by several KB per chart and round-trip
579
+ * through the LLM context for zero benefit (the model only needs
580
+ * the `chartId` to place a `[chart:<chartId>]` marker in its
581
+ * reply).
582
+ */
583
+ export const GenieDatasetChartSchema = z.object({
584
+ chartId: z.string(),
585
+ chartType: z.enum(["bar", "line", "area", "scatter", "pie"]),
586
+ });
587
+ export type GenieDatasetChart = z.infer<typeof GenieDatasetChartSchema>;
588
+
589
+ /**
590
+ * Dataset bundle attached to a {@link GenieSummaryItem} `visualize`
591
+ * item. `data` is always populated; `chart` is best-effort and
592
+ * absent when the workflow's chart-planner failed (timeout,
593
+ * malformed plan, abort) so the host UI can still render the
594
+ * underlying table.
595
+ */
596
+ export const GenieDatasetSchema = z.object({
597
+ data: GenieDatasetDataSchema,
598
+ chart: GenieDatasetChartSchema.optional(),
599
+ });
600
+ export type GenieDataset = z.infer<typeof GenieDatasetSchema>;
601
+
602
+ /**
603
+ * One item inside the Genie workflow's final summary. The
604
+ * workflow produces a mixed sequence of:
605
+ *
606
+ * - `string`: a markdown paragraph (interpretation, callouts,
607
+ * transitions between data blocks).
608
+ * - `visualize`: a request from the agent step to visualize a
609
+ * specific Genie statement at this position in the prose.
610
+ * The finalize step hydrates `dataset.data` (rows from the
611
+ * matching `statementId`) and attaches `dataset.chart` after
612
+ * running the chart-planner. The agent NEVER picks the chart
613
+ * type - it only marks where a visualization belongs.
614
+ *
615
+ * The host UI walks the array in display order to compose the
616
+ * final assistant message.
617
+ */
618
+ export const GenieSummaryItemSchema = z.discriminatedUnion("type", [
619
+ z.object({
620
+ type: z.literal("string"),
621
+ text: z.string(),
622
+ }),
623
+ z.object({
624
+ type: z.literal("visualize"),
625
+ statementId: z.string(),
626
+ title: z.string().optional(),
627
+ description: z.string().optional(),
628
+ dataset: GenieDatasetSchema,
629
+ }),
630
+ ]);
631
+ export type GenieSummaryItem = z.infer<typeof GenieSummaryItemSchema>;
632
+
633
+ /** Discriminator type for {@link GenieSummaryItem}. */
634
+ export type GenieSummaryItemType = GenieSummaryItem["type"];
635
+
636
+ /**
637
+ * The Genie agent's final output shape - what the calling agent's
638
+ * Genie tool returns to the calling LLM. The `summary` array is
639
+ * the user-facing renderable; `conversationId` lets the calling
640
+ * agent (or the UI) follow up in the same Genie thread on the
641
+ * next turn.
642
+ */
643
+ export const GenieAgentResultSchema = z.object({
644
+ spaceId: z.string(),
645
+ conversationId: z.string().optional(),
646
+ summary: z.array(GenieSummaryItemSchema),
647
+ error: z.string().optional(),
648
+ });
649
+ export type GenieAgentResult = z.infer<typeof GenieAgentResultSchema>;
package/tsconfig.json ADDED
@@ -0,0 +1,40 @@
1
+ // ~~ Generated by projen. To modify, edit .projenrc.js and run "pnpm exec projen".
2
+ {
3
+ "compilerOptions": {
4
+ "rootDir": "src",
5
+ "outDir": "lib",
6
+ "alwaysStrict": true,
7
+ "declaration": true,
8
+ "esModuleInterop": true,
9
+ "experimentalDecorators": true,
10
+ "inlineSourceMap": true,
11
+ "inlineSources": true,
12
+ "lib": [
13
+ "ES2022",
14
+ "WebWorker"
15
+ ],
16
+ "module": "ESNext",
17
+ "noEmitOnError": false,
18
+ "noFallthroughCasesInSwitch": true,
19
+ "noImplicitAny": true,
20
+ "noImplicitReturns": true,
21
+ "noImplicitThis": true,
22
+ "noUnusedLocals": true,
23
+ "noUnusedParameters": true,
24
+ "resolveJsonModule": true,
25
+ "strict": true,
26
+ "strictNullChecks": true,
27
+ "strictPropertyInitialization": true,
28
+ "stripInternal": true,
29
+ "target": "ES2022",
30
+ "types": [],
31
+ "moduleResolution": "bundler",
32
+ "skipLibCheck": true
33
+ },
34
+ "include": [
35
+ "src/**/*.ts"
36
+ ],
37
+ "exclude": [
38
+ "node_modules"
39
+ ]
40
+ }