@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 +117 -0
- package/index.ts +13 -0
- package/package.json +45 -0
- package/src/feedback.ts +84 -0
- package/src/marker.ts +147 -0
- package/src/override.ts +24 -0
- package/src/routes.ts +26 -0
- package/src/thread.ts +19 -0
- package/src/wire.ts +649 -0
- package/tsconfig.json +40 -0
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
|
+
}
|
package/src/feedback.ts
ADDED
|
@@ -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
|
+
}
|
package/src/override.ts
ADDED
|
@@ -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
|
+
}
|