@dbx-tools/appkit-mastra 0.1.12 → 0.1.14

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.
Files changed (57) hide show
  1. package/README.md +303 -637
  2. package/index.ts +46 -38
  3. package/package.json +58 -43
  4. package/src/agents.ts +224 -66
  5. package/src/chart.ts +531 -429
  6. package/src/config.ts +270 -19
  7. package/src/filesystems.ts +1090 -0
  8. package/src/genie.ts +1000 -660
  9. package/src/history.ts +166 -79
  10. package/src/mcp.ts +105 -0
  11. package/src/memory.ts +119 -81
  12. package/src/mlflow.ts +149 -0
  13. package/src/model.ts +75 -408
  14. package/src/observability.ts +144 -0
  15. package/src/pagination.ts +34 -0
  16. package/src/plugin.ts +566 -59
  17. package/src/processors.ts +168 -0
  18. package/src/rest.ts +67 -0
  19. package/src/server.ts +232 -45
  20. package/src/serving-sanitize.ts +167 -0
  21. package/src/serving.ts +27 -243
  22. package/src/statement.ts +89 -0
  23. package/src/storage-schema.ts +41 -0
  24. package/src/summarize.ts +176 -0
  25. package/src/threads.ts +338 -0
  26. package/src/workspaces.ts +346 -0
  27. package/src/writer.ts +44 -0
  28. package/tsconfig.json +41 -0
  29. package/dist/index.d.ts +0 -20
  30. package/dist/index.js +0 -20
  31. package/dist/src/agents.d.ts +0 -306
  32. package/dist/src/agents.js +0 -403
  33. package/dist/src/chart.d.ts +0 -170
  34. package/dist/src/chart.js +0 -491
  35. package/dist/src/config.d.ts +0 -183
  36. package/dist/src/config.js +0 -12
  37. package/dist/src/genie.d.ts +0 -131
  38. package/dist/src/genie.js +0 -630
  39. package/dist/src/history.d.ts +0 -67
  40. package/dist/src/history.js +0 -172
  41. package/dist/src/memory.d.ts +0 -79
  42. package/dist/src/memory.js +0 -210
  43. package/dist/src/model.d.ts +0 -159
  44. package/dist/src/model.js +0 -427
  45. package/dist/src/plugin.d.ts +0 -130
  46. package/dist/src/plugin.js +0 -261
  47. package/dist/src/processors/strip-stale-charts.d.ts +0 -29
  48. package/dist/src/processors/strip-stale-charts.js +0 -96
  49. package/dist/src/server.d.ts +0 -46
  50. package/dist/src/server.js +0 -123
  51. package/dist/src/serving.d.ts +0 -156
  52. package/dist/src/serving.js +0 -231
  53. package/dist/src/tools/email.d.ts +0 -74
  54. package/dist/src/tools/email.js +0 -122
  55. package/dist/tsconfig.build.tsbuildinfo +0 -1
  56. package/src/processors/strip-stale-charts.ts +0 -105
  57. package/src/tools/email.ts +0 -147
package/dist/src/genie.js DELETED
@@ -1,630 +0,0 @@
1
- /**
2
- * Mastra tool wrappers around the AppKit `genie` plugin's exports.
3
- *
4
- * One `sendMessage` tool is registered per configured space alias so
5
- * the LLM picks the space by tool selection (the description bakes the
6
- * alias in). `getConversation` is registered once, taking `alias` as a
7
- * parameter.
8
- *
9
- * All Genie payload types are inferred from the public `genie` factory
10
- * (`genie().plugin` constructor → `exports()` return type), so any
11
- * upstream change in `@databricks/appkit` flows in automatically.
12
- *
13
- * As Genie streams its long-running events (`FETCHING_METADATA` →
14
- * `ASKING_AI` → `EXECUTING_QUERY` → `COMPLETED`, plus SQL text and
15
- * follow-ups in `message_result.attachments`), the tool forwards a
16
- * normalised {@link GenieProgress} discriminated union out through
17
- * `ctx.writer` so the client can render an incremental loading pill.
18
- * Row payloads from `query_result` are intentionally discarded - the
19
- * LLM never sees rows, and charts come from the separate
20
- * `render_data` tool when the model decides one is useful.
21
- */
22
- import { genie } from "@databricks/appkit";
23
- import { logUtils, stringUtils } from "@dbx-tools/appkit-shared";
24
- import { createTool } from "@mastra/core/tools";
25
- import { z } from "zod";
26
- import { emitChartWithPlanning } from "./chart.js";
27
- /**
28
- * Module-level logger tagged `[mastra/genie]`. Uses the shared
29
- * {@link logUtils.logger} so calls below `LOG_LEVEL` are
30
- * discarded for free. Default `LOG_LEVEL` is `info`; flip to
31
- * `debug` to see per-turn timing (`query_result` → planner
32
- * waits → `drain:return`).
33
- */
34
- const log = logUtils.logger("mastra/genie");
35
- /**
36
- * Per-dataset metadata surfaced to the LLM. The actual rows are
37
- * dispatched separately as a `kind: "chart"` writer event so the
38
- * model never has the rows in its context (token cost stays flat
39
- * regardless of dataset size). The model uses `chartId` to
40
- * reference the chart inline via the `[[chart:<chartId>]]` marker.
41
- */
42
- const datasetSchema = z.object({
43
- chartId: z.string().describe(stringUtils.toDescription `
44
- Short id (8 hex chars) for the chart-render slot the host UI
45
- has staged for this dataset. Embed
46
- \`[[chart:<chartId>]]\` on its own line in your reply at the
47
- position you want the chart to appear; the client renders it
48
- inline. Do not paraphrase the dataset's rows in prose - the
49
- chart is the rendering.
50
- `),
51
- title: z.string().optional().describe(stringUtils.toDescription `
52
- Genie's own title for the SQL that produced this dataset.
53
- Useful as a label when you reference the chart in prose.
54
- `),
55
- description: z.string().optional().describe(stringUtils.toDescription `
56
- Genie's prose description of the SQL, if any.
57
- `),
58
- columns: z.array(z.string()).describe(stringUtils.toDescription `
59
- Column names in display order. Use these when describing what
60
- is being charted (e.g. "trend of fill_rate over date").
61
- `),
62
- rowCount: z.number().describe(stringUtils.toDescription `
63
- Total rows in this dataset. Mention only if it adds context
64
- (e.g. "across the last 90 days").
65
- `),
66
- sql: z
67
- .string()
68
- .optional()
69
- .describe(stringUtils.toDescription `
70
- SQL Genie generated and executed. The host UI shows this on
71
- demand; you do not need to repeat it.
72
- `),
73
- });
74
- /**
75
- * Top-level output schema returned to the LLM from a Genie tool
76
- * call. The `datasets` array is intentionally metadata-only - row
77
- * data rides a writer event the host UI consumes directly and is
78
- * not in the model's context.
79
- */
80
- const genieToolOutputSchema = z.object({
81
- conversationId: z
82
- .string()
83
- .optional()
84
- .describe(stringUtils.toDescription `
85
- Pass back on the next call to continue the same Genie thread.
86
- `),
87
- genieAnswer: z
88
- .string()
89
- .optional()
90
- .describe(stringUtils.toDescription `
91
- Genie's natural-language answer to the question. Pass this
92
- through to the user (verbatim, or as the basis of your
93
- reply). Genie may have run multiple SQL queries and tools to
94
- produce this; the full text is the answer.
95
- `),
96
- datasets: z
97
- .array(datasetSchema)
98
- .optional()
99
- .describe(stringUtils.toDescription `
100
- Datasets Genie produced for this turn (one per executed SQL
101
- statement). Each entry is metadata only; the rows are
102
- streamed to the host UI out-of-band. To render any of these
103
- as a chart inline in your reply, embed
104
- \`[[chart:<chartId>]]\` where you want the chart to appear.
105
- Do not paraphrase the rows - the chart is what the user
106
- should see; your prose should add interpretation
107
- (highlights, deltas, anomalies) around the chart.
108
- `),
109
- suggestedFollowUps: z
110
- .array(z.string())
111
- .optional()
112
- .describe(stringUtils.toDescription `
113
- Follow-up question suggestions Genie produced. The host UI
114
- renders these as clickable buttons; you do not need to list
115
- them in your reply.
116
- `),
117
- error: z
118
- .string()
119
- .optional()
120
- .describe(stringUtils.toDescription `
121
- Genie-side error message if the request failed.
122
- `),
123
- });
124
- const sendMessageSchema = z.object({
125
- content: z.string().describe(stringUtils.toDescription `
126
- Natural-language question to send to the Genie space.
127
- `),
128
- conversationId: z
129
- .string()
130
- .optional()
131
- .describe(stringUtils.toDescription `
132
- Optional Genie conversation id to continue an earlier thread.
133
- Omit on the first call; pass the id returned in the previous
134
- result's \`conversationId\` to follow up.
135
- `),
136
- });
137
- const getConversationSchema = z.object({
138
- alias: z.string().describe(stringUtils.toDescription `
139
- Alias of the Genie space the conversation belongs to (matches
140
- the key in the genie plugin's \`spaces\` config).
141
- `),
142
- conversationId: z.string().describe(stringUtils.toDescription `
143
- Genie conversation id whose history to fetch.
144
- `),
145
- });
146
- /** Per-attachment shape returned inside a stored Genie message. */
147
- const genieAttachmentSchema = z.object({
148
- attachmentId: z.string().optional().describe(stringUtils.toDescription `
149
- Genie attachment id; internal bookkeeping.
150
- `),
151
- query: z
152
- .object({
153
- title: z.string().optional().describe(stringUtils.toDescription `
154
- Genie's title for the SQL, if any.
155
- `),
156
- description: z.string().optional().describe(stringUtils.toDescription `
157
- Genie's prose description of the SQL, if any.
158
- `),
159
- query: z.string().optional().describe(stringUtils.toDescription `
160
- SQL Genie generated and executed.
161
- `),
162
- statementId: z.string().optional().describe(stringUtils.toDescription `
163
- Statement-execution id; internal bookkeeping.
164
- `),
165
- })
166
- .optional()
167
- .describe(stringUtils.toDescription `
168
- SQL Genie attached to this message, if it ran any.
169
- `),
170
- text: z
171
- .object({
172
- content: z.string().optional().describe(stringUtils.toDescription `
173
- Genie's natural-language answer text for this attachment.
174
- `),
175
- })
176
- .optional()
177
- .describe(stringUtils.toDescription `
178
- Per-attachment text content (independent of the message-level
179
- \`content\` field).
180
- `),
181
- suggestedQuestions: z
182
- .array(z.string())
183
- .optional()
184
- .describe(stringUtils.toDescription `
185
- Follow-up question suggestions Genie generated for this turn.
186
- `),
187
- });
188
- /** Single message inside a Genie conversation history page. */
189
- const genieMessageSchema = z.object({
190
- messageId: z.string().describe(stringUtils.toDescription `
191
- Genie message id; internal bookkeeping.
192
- `),
193
- conversationId: z.string().describe(stringUtils.toDescription `
194
- Conversation id this message belongs to.
195
- `),
196
- spaceId: z.string().describe(stringUtils.toDescription `
197
- Genie space id this message belongs to.
198
- `),
199
- status: z.string().describe(stringUtils.toDescription `
200
- Genie message status (\`COMPLETED\`, \`FAILED\`, etc.).
201
- `),
202
- content: z.string().describe(stringUtils.toDescription `
203
- Outer message-level natural-language content Genie wrote.
204
- `),
205
- attachments: z
206
- .array(genieAttachmentSchema)
207
- .optional()
208
- .describe(stringUtils.toDescription `
209
- Attachments (SQL queries, text blocks, suggested follow-ups)
210
- Genie produced for this message.
211
- `),
212
- error: z.string().optional().describe(stringUtils.toDescription `
213
- Genie-side error attached to this message, if any.
214
- `),
215
- });
216
- /**
217
- * Output schema for the \`genie_get_conversation\` tool. Mirrors
218
- * AppKit's \`GenieConversationHistoryResponse\` so the model gets a
219
- * clear, typed view of prior messages instead of an opaque blob.
220
- */
221
- const genieGetConversationOutputSchema = z.object({
222
- conversationId: z.string().describe(stringUtils.toDescription `
223
- Conversation id you fetched.
224
- `),
225
- spaceId: z.string().describe(stringUtils.toDescription `
226
- Genie space the conversation belongs to.
227
- `),
228
- messages: z.array(genieMessageSchema).describe(stringUtils.toDescription `
229
- Messages in the conversation, oldest to newest. Each
230
- \`message.content\` is Genie's natural-language answer for
231
- that turn; attachments carry the SQL and follow-ups Genie
232
- produced.
233
- `),
234
- });
235
- /**
236
- * Default tool name for a wired Genie alias. The well-known `default`
237
- * alias collapses to `genie`; everything else gets a `genie_` prefix so
238
- * multiple spaces stay disambiguated when an agent has more than one
239
- * wired. Matches the `genie` / `genie_<alias>` naming used elsewhere in
240
- * dbx-tools AppKit demos.
241
- */
242
- export function defaultGenieToolName(alias) {
243
- if (alias === "default")
244
- return "genie";
245
- return stringUtils.toIdentifierWithOptions({ distinct: true }, "genie", alias);
246
- }
247
- /**
248
- * Build one `sendMessage` tool per configured Genie alias plus a single
249
- * `getConversation` tool. Returns a record keyed by tool id, ready to
250
- * spread into an `Agent`'s `tools` map.
251
- *
252
- * `config` must be the active plugin config; Genie's
253
- * `query_result` events are routed through
254
- * {@link emitChartWithPlanning} which uses it to resolve the
255
- * chart-planner's model.
256
- */
257
- export function buildGenieTools(opts) {
258
- const tools = {};
259
- for (const alias of opts.aliases) {
260
- const id = defaultGenieToolName(alias);
261
- tools[id] = createTool({
262
- id,
263
- description: stringUtils.toDescription `
264
- Ask the Databricks Genie space "${alias}" a single
265
- natural-language question. Genie translates it to SQL,
266
- runs it, and returns \`genieAnswer\` (prose) plus
267
- \`datasets[]\` (one entry per executed query, each with
268
- a short \`chartId\`). Embed \`[[chart:<chartId>]]\` on
269
- its own line at the position you want that data rendered
270
- as an inline chart. Add interpretation around the chart
271
- (deltas, anomalies, takeaways); do not paraphrase row
272
- values.
273
-
274
- Issue ONE focused question per user turn. Prefer
275
- aggregated queries over raw-row queries for time-series
276
- and distributions.
277
- `,
278
- inputSchema: sendMessageSchema,
279
- outputSchema: genieToolOutputSchema,
280
- execute: async ({ content, conversationId }, ctx) => {
281
- const stream = opts.exports.sendMessage(alias, content, conversationId, {
282
- signal: opts.signal,
283
- });
284
- const requestContext = ctx
285
- ?.requestContext;
286
- return drainGenieStream(stream, ctx.writer, {
287
- config: opts.config,
288
- ...(requestContext ? { requestContext } : {}),
289
- });
290
- },
291
- });
292
- }
293
- tools.genie_get_conversation = createTool({
294
- id: "genie_get_conversation",
295
- description: stringUtils.toDescription `
296
- Fetch the full message history of a prior Genie conversation
297
- by id. Use when the user references an earlier Genie thread
298
- by id, or to inspect attachments / SQL from previous turns.
299
- `,
300
- inputSchema: getConversationSchema,
301
- outputSchema: genieGetConversationOutputSchema,
302
- execute: async ({ alias, conversationId }) => {
303
- return opts.exports.getConversation(alias, conversationId, opts.signal);
304
- },
305
- });
306
- return tools;
307
- }
308
- /**
309
- * Drain the genie `sendMessage` AsyncGenerator into a flat result
310
- * the agent's calling LLM can reason about, while forwarding
311
- * progress and chart events to the host UI.
312
- *
313
- * Three streams of output happen in parallel:
314
- *
315
- * 1. {@link GenieProgress} pill events on the writer (`started`,
316
- * `status`, `sql`, `suggested`, `error`) drive the loading
317
- * pill in the chat bubble.
318
- * 2. `kind: "chart"` events on the writer (emitted via
319
- * {@link emitChartWithPlanning}) carry the row payload from
320
- * each Genie SQL statement and, on planner success, a
321
- * follow-up event with the rendered Echarts spec. The host
322
- * UI's `<ChartSlot>` merges the two by `chartId` and
323
- * renders inline at the marker position the model picked.
324
- * The data never reaches the LLM.
325
- * 3. The `DrainResult` returned to the LLM contains Genie's
326
- * prose answer plus a `datasets[]` array of metadata
327
- * (chartId, title, columns, rowCount, sql) the model uses
328
- * to cite charts via `[[chart:<chartId>]]` markers.
329
- *
330
- * `query_result` and `message_result` events arrive in either
331
- * order; we buffer per-statement scratch keyed by `statementId`
332
- * so each half can fill in what it knows. The chart event
333
- * fires the moment `query_result` lands; the planner runs in
334
- * the background. We `Promise.allSettled` every planner promise
335
- * before returning so all chart work is attributed to the tool's
336
- * trace span and so the LLM's `datasets[]` includes every
337
- * chartId that has actually been queued.
338
- */
339
- async function drainGenieStream(stream, writer, opts) {
340
- const { config, requestContext } = opts;
341
- let conversationId;
342
- let genieAnswer;
343
- let suggestedFollowUps;
344
- let error;
345
- // AppKit's `streamSendMessage` forwards every SDK `onProgress`
346
- // callback verbatim - the same `EXECUTING_QUERY` can fire several
347
- // times during a single poll loop. AppKit's other path,
348
- // `streamGetMessage`, dedupes on the connector side; we mirror that
349
- // behaviour here so the UI status pill doesn't flicker and we don't
350
- // burn writer bytes on no-op events.
351
- let lastStatus;
352
- const scratchByStatementId = new Map();
353
- const getScratch = (statementId) => {
354
- let s = scratchByStatementId.get(statementId);
355
- if (!s) {
356
- s = { statementId, columns: [], rowCount: 0 };
357
- scratchByStatementId.set(statementId, s);
358
- }
359
- return s;
360
- };
361
- /**
362
- * Planner promises kicked off per `query_result`. Awaited
363
- * (Promise.allSettled) before drainGenieStream returns so the
364
- * Genie tool's trace span covers the chart work and the LLM's
365
- * `datasets[]` accurately reflects every chartId that's been
366
- * queued for rendering.
367
- */
368
- const plannerPromises = [];
369
- const emit = async (event) => {
370
- if (!writer)
371
- return;
372
- try {
373
- await writer.write(event);
374
- }
375
- catch {
376
- // ignore: downstream stream is no longer interested
377
- }
378
- };
379
- for await (const event of stream) {
380
- // Per-event raw payload for tuning the pill / answer pipeline
381
- // against real Genie traffic. At `info` (the default) this is
382
- // discarded for free; flip `LOG_LEVEL=debug` to see every
383
- // raw wire event before the switch routes it through writer
384
- // and DrainResult.
385
- log.debug("event", { type: event.type, payload: event });
386
- switch (event.type) {
387
- case "message_start":
388
- conversationId = event.conversationId;
389
- await emit({
390
- kind: "started",
391
- conversationId: event.conversationId,
392
- messageId: event.messageId,
393
- spaceId: event.spaceId,
394
- });
395
- break;
396
- case "status":
397
- if (event.status === lastStatus)
398
- break;
399
- lastStatus = event.status;
400
- await emit({
401
- kind: "status",
402
- status: event.status,
403
- label: humanizeGenieStatus(event.status),
404
- });
405
- break;
406
- case "query_result": {
407
- const columns = (event.data?.manifest?.schema?.columns ?? []).map((c) => c.name);
408
- const dataArray = (event.data?.result?.data_array ?? []);
409
- const rows = genieRowsToObjects(columns, dataArray);
410
- const scratch = getScratch(event.statementId);
411
- // emitChartWithPlanning emits the dataset event immediately
412
- // and kicks off the chart-planner agent in the background.
413
- // It returns the chartId synchronously; the plannerPromise
414
- // is awaited at end-of-stream so chart work shows up under
415
- // this tool's trace span.
416
- const { chartId, plannerPromise } = await emitChartWithPlanning({
417
- ...(writer ? { writer } : {}),
418
- config,
419
- ...(requestContext ? { requestContext } : {}),
420
- title: scratch.title ?? `Genie query`,
421
- ...(scratch.description ? { description: scratch.description } : {}),
422
- data: rows,
423
- });
424
- scratch.chartId = chartId;
425
- scratch.columns = columns;
426
- scratch.rowCount = rows.length;
427
- plannerPromises.push(plannerPromise);
428
- log.debug("query_result", {
429
- statementId: event.statementId,
430
- chartId,
431
- rows: rows.length,
432
- columns,
433
- });
434
- break;
435
- }
436
- case "message_result":
437
- genieAnswer = event.message.content;
438
- for (const attachment of event.message.attachments ?? []) {
439
- const sqlText = attachment.query?.query;
440
- const stmtId = attachment.query?.statementId;
441
- if (stmtId) {
442
- const scratch = getScratch(stmtId);
443
- if (sqlText)
444
- scratch.sql = sqlText;
445
- if (attachment.query?.title)
446
- scratch.title = attachment.query.title;
447
- if (attachment.query?.description) {
448
- scratch.description = attachment.query.description;
449
- }
450
- }
451
- if (sqlText) {
452
- await emit({
453
- kind: "sql",
454
- sql: sqlText,
455
- title: attachment.query?.title,
456
- description: attachment.query?.description,
457
- statementId: stmtId,
458
- });
459
- }
460
- if (attachment.text?.content) {
461
- await emit({ kind: "text", content: attachment.text.content });
462
- }
463
- if (attachment.suggestedQuestions?.length) {
464
- // Last attachment with suggestions wins (same merge rule
465
- // the UI uses via `collectSuggestions`); keeping just one
466
- // copy per turn caps token usage.
467
- suggestedFollowUps = attachment.suggestedQuestions;
468
- await emit({
469
- kind: "suggested",
470
- questions: attachment.suggestedQuestions,
471
- });
472
- }
473
- }
474
- break;
475
- case "error":
476
- error = event.error;
477
- await emit({ kind: "error", error: event.error });
478
- break;
479
- default:
480
- break;
481
- }
482
- }
483
- // Wait for all chart planners to settle before returning so the
484
- // tool's trace span covers chart work and the LLM's
485
- // `datasets[]` reflects only chartIds the client has actually
486
- // received writer events for. Failures in `emitChartWithPlanning`
487
- // are already swallowed inside the helper, so this never
488
- // throws.
489
- log.debug("planners:awaiting", { count: plannerPromises.length });
490
- await Promise.allSettled(plannerPromises);
491
- log.debug("planners:settled", { count: plannerPromises.length });
492
- // Build the LLM-bound `datasets[]` from scratch entries that
493
- // actually ran a query (chartId is assigned at `query_result`
494
- // time). Entries that only saw `message_result` metadata
495
- // without a row payload are skipped.
496
- const datasets = [];
497
- for (const scratch of scratchByStatementId.values()) {
498
- if (!scratch.chartId)
499
- continue;
500
- datasets.push({
501
- chartId: scratch.chartId,
502
- ...(scratch.title ? { title: scratch.title } : {}),
503
- ...(scratch.description ? { description: scratch.description } : {}),
504
- columns: scratch.columns,
505
- rowCount: scratch.rowCount,
506
- ...(scratch.sql ? { sql: scratch.sql } : {}),
507
- });
508
- }
509
- log.debug("drain:return", {
510
- conversationId,
511
- hasAnswer: typeof genieAnswer === "string",
512
- answerLength: genieAnswer?.length ?? 0,
513
- chartIds: datasets.map((d) => d.chartId),
514
- suggestedCount: suggestedFollowUps?.length ?? 0,
515
- error,
516
- });
517
- return {
518
- ...(conversationId ? { conversationId } : {}),
519
- ...(genieAnswer ? { genieAnswer } : {}),
520
- ...(datasets.length > 0 ? { datasets } : {}),
521
- ...(suggestedFollowUps ? { suggestedFollowUps } : {}),
522
- ...(error ? { error } : {}),
523
- };
524
- }
525
- /**
526
- * Convert Genie's `data_array` (column-positional `string | null`
527
- * tuples) into plain JS row objects keyed by column name. Numeric
528
- * strings are coerced to numbers so the chart-planner picks
529
- * `value` axes instead of `category` axes; everything else passes
530
- * through verbatim. `null` becomes `null`.
531
- */
532
- function genieRowsToObjects(columns, dataArray) {
533
- const out = [];
534
- for (const row of dataArray) {
535
- const obj = {};
536
- columns.forEach((col, i) => {
537
- const cell = row[i] ?? null;
538
- obj[col] = coerceCell(cell);
539
- });
540
- out.push(obj);
541
- }
542
- return out;
543
- }
544
- /** Best-effort numeric coercion for Genie's all-strings cells. */
545
- function coerceCell(cell) {
546
- if (cell === null)
547
- return null;
548
- // Anchored to keep `12.5px` / `123abc` as strings; only fully
549
- // numeric values become JS numbers.
550
- if (/^-?\d+(\.\d+)?$/.test(cell)) {
551
- const n = Number(cell);
552
- if (Number.isFinite(n))
553
- return n;
554
- }
555
- return cell;
556
- }
557
- /**
558
- * Toolkit provider built from a live AppKit `GeniePlugin` instance.
559
- * Returned by {@link buildGenieProvider} so that
560
- * `plugins.genie?.toolkit()` inside an agent's `tools(plugins)` callback
561
- * resolves to the streaming-aware {@link buildGenieTools} record instead
562
- * of the AppKit default (which does one blocking call per tool with no
563
- * mid-flight events).
564
- *
565
- * The returned `toolkit()` reads alias names off the plugin's
566
- * `getAgentTools()` registry (each entry is `${alias}.sendMessage` or
567
- * `${alias}.getConversation`), then mints one `sendMessage` tool per
568
- * alias plus a shared `getConversation`. `sendMessage` / `getConversation`
569
- * are bound back to the plugin instance so they keep their `this`
570
- * (they are class methods, not free functions).
571
- *
572
- * `_opts` is accepted but unused for now - the streaming tools are an
573
- * all-or-nothing bundle. Wire `only` / `except` / `prefix` / `rename`
574
- * later if a caller needs them.
575
- */
576
- export function buildGenieProvider(plugin, opts) {
577
- return {
578
- toolkit(_opts) {
579
- const aliases = extractGenieAliases(plugin);
580
- return buildGenieTools({
581
- aliases,
582
- exports: {
583
- sendMessage: plugin.sendMessage.bind(plugin),
584
- getConversation: plugin.getConversation.bind(plugin),
585
- },
586
- config: opts.config,
587
- });
588
- },
589
- };
590
- }
591
- /**
592
- * Pull the configured space aliases out of a live AppKit `GeniePlugin`.
593
- * Reads them off `getAgentTools()` (public API) so we don't poke at the
594
- * `protected config.spaces` field: the plugin registers tools named
595
- * `${alias}.sendMessage` / `${alias}.getConversation`, so the unique
596
- * set of name prefixes is the alias list.
597
- */
598
- function extractGenieAliases(plugin) {
599
- const aliases = new Set();
600
- for (const t of plugin.getAgentTools()) {
601
- const dot = t.name.indexOf(".");
602
- if (dot > 0)
603
- aliases.add(t.name.slice(0, dot));
604
- }
605
- return [...aliases];
606
- }
607
- /**
608
- * Convert raw Genie status codes (`FETCHING_METADATA`, `ASKING_AI`,
609
- * `EXECUTING_QUERY`, `COMPLETED`, ...) into short, sentence-cased
610
- * labels safe to drop straight into a UI pill. Unknown codes are
611
- * lower-cased with underscores stripped so new states still render.
612
- */
613
- function humanizeGenieStatus(status) {
614
- switch (status) {
615
- case "FETCHING_METADATA":
616
- return "Fetching metadata";
617
- case "ASKING_AI":
618
- return "Asking Genie";
619
- case "EXECUTING_QUERY":
620
- return "Running SQL query";
621
- case "COMPLETED":
622
- return "Completed";
623
- case "FAILED":
624
- return "Failed";
625
- default:
626
- return [
627
- ...stringUtils.tokenizeWithOptions({ capitalize: true, lowerCase: true }, status),
628
- ].join(" ");
629
- }
630
- }
@@ -1,67 +0,0 @@
1
- /**
2
- * Thread history loader exposed as a Mastra custom API route.
3
- *
4
- * Backed entirely by native Mastra: looks up the active agent by id,
5
- * asks its `Memory` instance to `recall` a page of `MastraDBMessage`s,
6
- * and converts the result to AI SDK V5 `UIMessage`s with the official
7
- * {@link toAISdkV5Messages} helper from `@mastra/ai-sdk/ui`. No direct
8
- * database reads.
9
- *
10
- * The route is registered through {@link historyRoute} as a Mastra
11
- * `registerApiRoute` so it sits in the same dispatcher pipeline as
12
- * `chatRoute`. That means the `MastraServer` auth middleware (in
13
- * `./server.ts`) has already populated `RequestContext` with
14
- * `MASTRA_THREAD_ID_KEY` and `MASTRA_RESOURCE_ID_KEY` by the time
15
- * the handler runs - no cookie or user lookups happen here, and the
16
- * session-cookie logic stays the single source of truth in `server.ts`.
17
- */
18
- import type { Agent } from "@mastra/core/agent";
19
- import type { MastraHistoryResponse } from "@dbx-tools/appkit-mastra-shared";
20
- /** Inputs accepted by {@link loadHistory}. */
21
- export interface LoadHistoryOptions {
22
- agent: Agent;
23
- threadId: string;
24
- resourceId?: string;
25
- page?: number;
26
- perPage?: number;
27
- /** When true, returns the *oldest* page first (chronological). */
28
- ascending?: boolean;
29
- }
30
- /**
31
- * Fetch a page of UI-formatted messages for a thread.
32
- *
33
- * Uses the agent's resolved `Memory` (`getMemory()`) so per-agent
34
- * storage namespaces (`mastra_<agentId>` schemas) and any future
35
- * memory-side filters apply automatically. When the agent has no
36
- * memory configured the response is a successful empty page so
37
- * callers don't have to special-case stateless agents.
38
- *
39
- * Pagination is descending-by-default: page 0 is the most recent
40
- * page, page 1 the page before that, etc. The returned `uiMessages`
41
- * are always re-sorted into chronological order (oldest -> newest)
42
- * so the client can prepend them above the existing transcript
43
- * without sorting locally.
44
- */
45
- export declare function loadHistory(opts: LoadHistoryOptions): Promise<MastraHistoryResponse>;
46
- /** Options accepted by {@link historyRoute}. */
47
- export type HistoryRouteOptions = {
48
- path: `${string}:agentId${string}`;
49
- agent?: never;
50
- } | {
51
- path: string;
52
- agent: string;
53
- };
54
- /**
55
- * Register a `GET <path>` Mastra custom API route that returns a page
56
- * of AI SDK V5 `UIMessage`s for the caller's current thread.
57
- *
58
- * Modeled after `chatRoute` from `@mastra/ai-sdk`: pass `agent` for a
59
- * fixed-agent mount, or include `:agentId` in the path for dynamic
60
- * routing. Pairs cleanly with the AppKit Mastra plugin's chat route
61
- * layout (`/route/chat` + `/route/chat/:agentId`).
62
- *
63
- * The handler reads `threadId` and `resourceId` from `RequestContext`
64
- * (populated upstream by `MastraServer.registerAuthMiddleware`), so
65
- * no cookie or user lookups happen here.
66
- */
67
- export declare function historyRoute(options: HistoryRouteOptions): import("@mastra/core/server").ApiRoute;