@dbx-tools/appkit-mastra 0.1.3 → 0.1.5

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/dist/src/genie.js CHANGED
@@ -11,32 +11,218 @@
11
11
  * upstream change in `@databricks/appkit` flows in automatically.
12
12
  *
13
13
  * As Genie streams its long-running events (`FETCHING_METADATA` →
14
- * `ASKING_AI` → `EXECUTING_QUERY` → `COMPLETED`, plus SQL queries and
15
- * row data in `message_result.attachments` / `query_result`), the tool
16
- * forwards a normalised {@link GenieProgress} discriminated union out
17
- * through `ctx.writer` so the client can render incremental feedback
18
- * (status pill, SQL code block, row count) while the LLM still sees a
19
- * single clean final payload.
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.
20
21
  */
22
+ import { randomUUID } from "node:crypto";
21
23
  import { genie } from "@databricks/appkit";
22
24
  import { stringUtils } from "@dbx-tools/appkit-shared";
23
25
  import { createTool } from "@mastra/core/tools";
24
26
  import { z } from "zod";
27
+ /**
28
+ * Per-dataset metadata surfaced to the LLM. The actual rows are
29
+ * dispatched separately as a `kind: "chart"` writer event so the
30
+ * model never has the rows in its context (token cost stays flat
31
+ * regardless of dataset size). The model uses `chartId` to
32
+ * reference the chart inline via the `[[chart:<chartId>]]` marker.
33
+ */
34
+ const datasetSchema = z.object({
35
+ chartId: z.string().describe(stringUtils.toDescription `
36
+ Short id (8 hex chars) for the chart-render slot the host UI
37
+ has staged for this dataset. Embed
38
+ \`[[chart:<chartId>]]\` on its own line in your reply at the
39
+ position you want the chart to appear; the client renders it
40
+ inline. Do not paraphrase the dataset's rows in prose - the
41
+ chart is the rendering.
42
+ `),
43
+ title: z.string().optional().describe(stringUtils.toDescription `
44
+ Genie's own title for the SQL that produced this dataset.
45
+ Useful as a label when you reference the chart in prose.
46
+ `),
47
+ description: z.string().optional().describe(stringUtils.toDescription `
48
+ Genie's prose description of the SQL, if any.
49
+ `),
50
+ columns: z.array(z.string()).describe(stringUtils.toDescription `
51
+ Column names in display order. Use these when describing what
52
+ is being charted (e.g. "trend of fill_rate over date").
53
+ `),
54
+ rowCount: z.number().describe(stringUtils.toDescription `
55
+ Total rows in this dataset. Mention only if it adds context
56
+ (e.g. "across the last 90 days").
57
+ `),
58
+ sql: z
59
+ .string()
60
+ .optional()
61
+ .describe(stringUtils.toDescription `
62
+ SQL Genie generated and executed. The host UI shows this on
63
+ demand; you do not need to repeat it.
64
+ `),
65
+ });
66
+ /**
67
+ * Top-level output schema returned to the LLM from a Genie tool
68
+ * call. The `datasets` array is intentionally metadata-only - row
69
+ * data rides a writer event the host UI consumes directly and is
70
+ * not in the model's context.
71
+ */
72
+ const genieToolOutputSchema = z.object({
73
+ conversationId: z
74
+ .string()
75
+ .optional()
76
+ .describe(stringUtils.toDescription `
77
+ Pass back on the next call to continue the same Genie thread.
78
+ `),
79
+ genieAnswer: z
80
+ .string()
81
+ .optional()
82
+ .describe(stringUtils.toDescription `
83
+ Genie's natural-language answer to the question. Pass this
84
+ through to the user (verbatim, or as the basis of your
85
+ reply). Genie may have run multiple SQL queries and tools to
86
+ produce this; the full text is the answer.
87
+ `),
88
+ datasets: z
89
+ .array(datasetSchema)
90
+ .optional()
91
+ .describe(stringUtils.toDescription `
92
+ Datasets Genie produced for this turn (one per executed SQL
93
+ statement). Each entry is metadata only; the rows are
94
+ streamed to the host UI out-of-band. To render any of these
95
+ as a chart inline in your reply, embed
96
+ \`[[chart:<chartId>]]\` where you want the chart to appear.
97
+ Do not paraphrase the rows - the chart is what the user
98
+ should see; your prose should add interpretation
99
+ (highlights, deltas, anomalies) around the chart.
100
+ `),
101
+ suggestedFollowUps: z
102
+ .array(z.string())
103
+ .optional()
104
+ .describe(stringUtils.toDescription `
105
+ Follow-up question suggestions Genie produced. The host UI
106
+ renders these as clickable buttons; you do not need to list
107
+ them in your reply.
108
+ `),
109
+ error: z
110
+ .string()
111
+ .optional()
112
+ .describe(stringUtils.toDescription `
113
+ Genie-side error message if the request failed.
114
+ `),
115
+ });
25
116
  const sendMessageSchema = z.object({
26
- content: z.string().describe("Natural-language question to send to the Genie space."),
117
+ content: z.string().describe(stringUtils.toDescription `
118
+ Natural-language question to send to the Genie space.
119
+ `),
27
120
  conversationId: z
28
121
  .string()
29
122
  .optional()
30
- .describe("Optional Genie conversation id to continue an earlier thread. " +
31
- "Omit on the first call; pass the id returned in the previous " +
32
- "result's `conversationId` to follow up."),
123
+ .describe(stringUtils.toDescription `
124
+ Optional Genie conversation id to continue an earlier thread.
125
+ Omit on the first call; pass the id returned in the previous
126
+ result's \`conversationId\` to follow up.
127
+ `),
33
128
  });
34
129
  const getConversationSchema = z.object({
35
- alias: z
36
- .string()
37
- .describe("Alias of the Genie space the conversation belongs to (matches the " +
38
- "key in the genie plugin's `spaces` config)."),
39
- conversationId: z.string().describe("Genie conversation id whose history to fetch."),
130
+ alias: z.string().describe(stringUtils.toDescription `
131
+ Alias of the Genie space the conversation belongs to (matches
132
+ the key in the genie plugin's \`spaces\` config).
133
+ `),
134
+ conversationId: z.string().describe(stringUtils.toDescription `
135
+ Genie conversation id whose history to fetch.
136
+ `),
137
+ });
138
+ /** Per-attachment shape returned inside a stored Genie message. */
139
+ const genieAttachmentSchema = z.object({
140
+ attachmentId: z.string().optional().describe(stringUtils.toDescription `
141
+ Genie attachment id; internal bookkeeping.
142
+ `),
143
+ query: z
144
+ .object({
145
+ title: z.string().optional().describe(stringUtils.toDescription `
146
+ Genie's title for the SQL, if any.
147
+ `),
148
+ description: z.string().optional().describe(stringUtils.toDescription `
149
+ Genie's prose description of the SQL, if any.
150
+ `),
151
+ query: z.string().optional().describe(stringUtils.toDescription `
152
+ SQL Genie generated and executed.
153
+ `),
154
+ statementId: z.string().optional().describe(stringUtils.toDescription `
155
+ Statement-execution id; internal bookkeeping.
156
+ `),
157
+ })
158
+ .optional()
159
+ .describe(stringUtils.toDescription `
160
+ SQL Genie attached to this message, if it ran any.
161
+ `),
162
+ text: z
163
+ .object({
164
+ content: z.string().optional().describe(stringUtils.toDescription `
165
+ Genie's natural-language answer text for this attachment.
166
+ `),
167
+ })
168
+ .optional()
169
+ .describe(stringUtils.toDescription `
170
+ Per-attachment text content (independent of the message-level
171
+ \`content\` field).
172
+ `),
173
+ suggestedQuestions: z
174
+ .array(z.string())
175
+ .optional()
176
+ .describe(stringUtils.toDescription `
177
+ Follow-up question suggestions Genie generated for this turn.
178
+ `),
179
+ });
180
+ /** Single message inside a Genie conversation history page. */
181
+ const genieMessageSchema = z.object({
182
+ messageId: z.string().describe(stringUtils.toDescription `
183
+ Genie message id; internal bookkeeping.
184
+ `),
185
+ conversationId: z.string().describe(stringUtils.toDescription `
186
+ Conversation id this message belongs to.
187
+ `),
188
+ spaceId: z.string().describe(stringUtils.toDescription `
189
+ Genie space id this message belongs to.
190
+ `),
191
+ status: z.string().describe(stringUtils.toDescription `
192
+ Genie message status (\`COMPLETED\`, \`FAILED\`, etc.).
193
+ `),
194
+ content: z.string().describe(stringUtils.toDescription `
195
+ Outer message-level natural-language content Genie wrote.
196
+ `),
197
+ attachments: z
198
+ .array(genieAttachmentSchema)
199
+ .optional()
200
+ .describe(stringUtils.toDescription `
201
+ Attachments (SQL queries, text blocks, suggested follow-ups)
202
+ Genie produced for this message.
203
+ `),
204
+ error: z.string().optional().describe(stringUtils.toDescription `
205
+ Genie-side error attached to this message, if any.
206
+ `),
207
+ });
208
+ /**
209
+ * Output schema for the \`genie_get_conversation\` tool. Mirrors
210
+ * AppKit's \`GenieConversationHistoryResponse\` so the model gets a
211
+ * clear, typed view of prior messages instead of an opaque blob.
212
+ */
213
+ const genieGetConversationOutputSchema = z.object({
214
+ conversationId: z.string().describe(stringUtils.toDescription `
215
+ Conversation id you fetched.
216
+ `),
217
+ spaceId: z.string().describe(stringUtils.toDescription `
218
+ Genie space the conversation belongs to.
219
+ `),
220
+ messages: z.array(genieMessageSchema).describe(stringUtils.toDescription `
221
+ Messages in the conversation, oldest to newest. Each
222
+ \`message.content\` is Genie's natural-language answer for
223
+ that turn; attachments carry the SQL and follow-ups Genie
224
+ produced.
225
+ `),
40
226
  });
41
227
  /**
42
228
  * Default tool name for a wired Genie alias. The well-known `default`
@@ -61,13 +247,29 @@ export function buildGenieTools(opts) {
61
247
  const id = defaultGenieToolName(alias);
62
248
  tools[id] = createTool({
63
249
  id,
64
- description: `Ask the Databricks Genie space "${alias}" a natural-language ` +
65
- "question. Genie translates the question to SQL, runs it against " +
66
- "the configured datasets, and returns a written answer plus any " +
67
- "SQL statements it executed. Returns `{ conversationId, content, " +
68
- "queries, ... }`; pass `conversationId` back in to follow up in " +
69
- "the same Genie thread.",
250
+ description: stringUtils.toDescription `
251
+ Ask the Databricks Genie space "${alias}" a single
252
+ natural-language question. Genie translates it to SQL,
253
+ runs the SQL against the configured datasets, and returns
254
+ \`genieAnswer\` (its prose answer) plus \`datasets[]\`
255
+ (one metadata entry per executed query). Each dataset
256
+ carries a short \`chartId\`; embed
257
+ \`[[chart:<chartId>]]\` on its own line in your reply at
258
+ the position where you want that data rendered as an
259
+ inline chart. Do not paraphrase row values - the chart is
260
+ the rendering. Add interpretation around the chart
261
+ (highlights, deltas, anomalies, takeaways) instead of
262
+ repeating numbers.
263
+
264
+ Calling this tool is expensive; issue **one** focused
265
+ question per user turn. If the first answer doesn't fit,
266
+ ask the user a clarifying question rather than
267
+ re-querying with rephrased intent. Prefer aggregated
268
+ questions over raw-row queries (e.g. ask for "monthly
269
+ averages" instead of "all rows" for time-series).
270
+ `,
70
271
  inputSchema: sendMessageSchema,
272
+ outputSchema: genieToolOutputSchema,
71
273
  execute: async ({ content, conversationId }, ctx) => {
72
274
  const stream = opts.exports.sendMessage(alias, content, conversationId, {
73
275
  signal: opts.signal,
@@ -78,10 +280,13 @@ export function buildGenieTools(opts) {
78
280
  }
79
281
  tools.genie_get_conversation = createTool({
80
282
  id: "genie_get_conversation",
81
- description: "Fetch the full message history of a prior Genie conversation by id. " +
82
- "Use when the user references an earlier Genie thread by id, or to " +
83
- "inspect attachments / SQL from previous turns.",
283
+ description: stringUtils.toDescription `
284
+ Fetch the full message history of a prior Genie conversation
285
+ by id. Use when the user references an earlier Genie thread
286
+ by id, or to inspect attachments / SQL from previous turns.
287
+ `,
84
288
  inputSchema: getConversationSchema,
289
+ outputSchema: genieGetConversationOutputSchema,
85
290
  execute: async ({ alias, conversationId }) => {
86
291
  return opts.exports.getConversation(alias, conversationId, opts.signal);
87
292
  },
@@ -89,26 +294,49 @@ export function buildGenieTools(opts) {
89
294
  return tools;
90
295
  }
91
296
  /**
92
- * Drain the genie `sendMessage` AsyncGenerator into a flat result the
93
- * agent's calling LLM can reason about. Final assistant text is pulled
94
- * from the last `message_result`; SQL statements are extracted from
95
- * `query_result` events; conversation / message ids are surfaced so the
96
- * caller can pass `conversationId` back into a follow-up tool call.
297
+ * Drain the genie `sendMessage` AsyncGenerator into a flat result
298
+ * the agent's calling LLM can reason about, while forwarding
299
+ * progress and chart events to the host UI.
97
300
  *
98
- * When a Mastra `writer` is passed (i.e. the tool runs inside an agent
99
- * stream), normalised {@link GenieProgress} events are pushed mid-flight
100
- * so the UI can show status changes, SQL, and row counts as they
101
- * happen instead of staring at a spinner for the full Genie round-trip.
301
+ * Three streams of output happen in parallel:
302
+ *
303
+ * 1. {@link GenieProgress} pill events on the writer (`started`,
304
+ * `status`, `sql`, `suggested`, `error`) drive the loading
305
+ * pill in the chat bubble.
306
+ * 2. `kind: "chart"` events on the writer carry the row payload
307
+ * from each Genie SQL statement so the host UI's
308
+ * `<ChartSlot>` can render the chart inline at the marker
309
+ * position the model picked. The data never reaches the LLM.
310
+ * 3. The `DrainResult` returned to the LLM contains
311
+ * Genie's prose answer plus a `datasets[]` array of metadata
312
+ * (chartId, title, columns, rowCount, sql) the model uses to
313
+ * cite charts via `[[chart:<chartId>]]` markers.
314
+ *
315
+ * `query_result` and `message_result` events arrive in either
316
+ * order; we buffer per-statement metadata in
317
+ * {@link DatasetMeta} so each half can fill in the bits it knows
318
+ * about and we emit the chart event once `query_result` lands
319
+ * (with whatever title was already set, falling back to a
320
+ * generic label otherwise).
102
321
  */
103
322
  async function drainGenieStream(stream, writer) {
104
323
  let conversationId;
105
- let messageId;
106
- let spaceId;
107
- let status;
108
- let content;
109
- let attachments;
324
+ let genieAnswer;
325
+ let suggestedFollowUps;
110
326
  let error;
111
- const queries = [];
327
+ // AppKit's `streamSendMessage` forwards every SDK `onProgress`
328
+ // callback verbatim - the same `EXECUTING_QUERY` can fire several
329
+ // times during a single poll loop. AppKit's other path,
330
+ // `streamGetMessage`, dedupes on the connector side; we mirror that
331
+ // behaviour here so the UI status pill doesn't flicker and we don't
332
+ // burn writer bytes on no-op events.
333
+ let lastStatus;
334
+ // Per-statement scratch keyed by Genie's `statementId`. Filled in
335
+ // by both `query_result` (rows + columns) and `message_result`
336
+ // (sql + title + description); the LLM-bound `datasets[]` is
337
+ // built from this at end-of-stream, and chart writer events fire
338
+ // when `query_result` lands.
339
+ const datasetsByStatementId = new Map();
112
340
  // Best-effort progress emission. Awaited so the underlying agent
113
341
  // stream sees events in order; write failures are swallowed so a
114
342
  // dead writer (e.g. closed downstream) can't take the tool down.
@@ -123,20 +351,27 @@ async function drainGenieStream(stream, writer) {
123
351
  }
124
352
  };
125
353
  for await (const event of stream) {
354
+ // Uncomment to log every raw Genie wire event before the switch
355
+ // routes it through the writer / DrainResult. Useful when tuning
356
+ // the pill / answer pipeline against real Genie payloads (status
357
+ // codes, attachment shapes, query_result manifests Genie surfaces
358
+ // only on certain question types, etc.).
359
+ // eslint-disable-next-line no-console
360
+ // console.log("[mastra/genie] event", event);
126
361
  switch (event.type) {
127
362
  case "message_start":
128
363
  conversationId = event.conversationId;
129
- messageId = event.messageId;
130
- spaceId = event.spaceId;
131
364
  await emit({
132
365
  kind: "started",
133
- conversationId,
134
- messageId,
135
- spaceId,
366
+ conversationId: event.conversationId,
367
+ messageId: event.messageId,
368
+ spaceId: event.spaceId,
136
369
  });
137
370
  break;
138
371
  case "status":
139
- status = event.status;
372
+ if (event.status === lastStatus)
373
+ break;
374
+ lastStatus = event.status;
140
375
  await emit({
141
376
  kind: "status",
142
377
  status: event.status,
@@ -144,34 +379,53 @@ async function drainGenieStream(stream, writer) {
144
379
  });
145
380
  break;
146
381
  case "query_result": {
147
- queries.push({
148
- attachmentId: event.attachmentId,
149
- statementId: event.statementId,
150
- data: event.data,
151
- });
152
- const rowCount = event.data?.result?.data_array?.length ?? 0;
153
382
  const columns = (event.data?.manifest?.schema?.columns ?? []).map((c) => c.name);
154
- await emit({ kind: "data", rowCount, columns });
383
+ const dataArray = (event.data?.result?.data_array ?? []);
384
+ const rows = genieRowsToObjects(columns, dataArray);
385
+ const meta = upsertDatasetMeta(datasetsByStatementId, event.statementId, {
386
+ columns,
387
+ rowCount: rows.length,
388
+ });
389
+ await emit({
390
+ kind: "chart",
391
+ chartId: meta.chartId,
392
+ title: meta.title ?? `Genie query`,
393
+ ...(meta.description ? { description: meta.description } : {}),
394
+ data: rows,
395
+ });
155
396
  break;
156
397
  }
157
398
  case "message_result":
158
- content = event.message.content;
159
- attachments = event.message.attachments;
160
- status = event.message.status;
161
- for (const attachment of attachments ?? []) {
162
- if (attachment.query?.query) {
399
+ genieAnswer = event.message.content;
400
+ for (const attachment of event.message.attachments ?? []) {
401
+ const sqlText = attachment.query?.query;
402
+ const stmtId = attachment.query?.statementId;
403
+ if (sqlText && stmtId) {
404
+ upsertDatasetMeta(datasetsByStatementId, stmtId, {
405
+ sql: sqlText,
406
+ ...(attachment.query?.title ? { title: attachment.query.title } : {}),
407
+ ...(attachment.query?.description
408
+ ? { description: attachment.query.description }
409
+ : {}),
410
+ });
411
+ }
412
+ if (sqlText) {
163
413
  await emit({
164
414
  kind: "sql",
165
- sql: attachment.query.query,
166
- title: attachment.query.title,
167
- description: attachment.query.description,
168
- statementId: attachment.query.statementId,
415
+ sql: sqlText,
416
+ title: attachment.query?.title,
417
+ description: attachment.query?.description,
418
+ statementId: stmtId,
169
419
  });
170
420
  }
171
421
  if (attachment.text?.content) {
172
422
  await emit({ kind: "text", content: attachment.text.content });
173
423
  }
174
424
  if (attachment.suggestedQuestions?.length) {
425
+ // Last attachment with suggestions wins (same merge rule
426
+ // the UI uses via `collectSuggestions`); keeping just one
427
+ // copy per turn caps token usage.
428
+ suggestedFollowUps = attachment.suggestedQuestions;
175
429
  await emit({
176
430
  kind: "suggested",
177
431
  questions: attachment.suggestedQuestions,
@@ -187,16 +441,83 @@ async function drainGenieStream(stream, writer) {
187
441
  break;
188
442
  }
189
443
  }
444
+ // Strip statementId / row-only fields when handing the LLM the
445
+ // datasets - the model never references statementId, and the
446
+ // chartId is what the marker uses.
447
+ const datasets = [];
448
+ for (const meta of datasetsByStatementId.values()) {
449
+ datasets.push({
450
+ chartId: meta.chartId,
451
+ ...(meta.title ? { title: meta.title } : {}),
452
+ ...(meta.description ? { description: meta.description } : {}),
453
+ columns: meta.columns,
454
+ rowCount: meta.rowCount,
455
+ ...(meta.sql ? { sql: meta.sql } : {}),
456
+ });
457
+ }
190
458
  return {
191
- conversationId,
192
- messageId,
193
- spaceId,
194
- status,
195
- content,
196
- attachments,
197
- queries,
198
- error,
459
+ ...(conversationId ? { conversationId } : {}),
460
+ ...(genieAnswer ? { genieAnswer } : {}),
461
+ ...(datasets.length > 0 ? { datasets } : {}),
462
+ ...(suggestedFollowUps ? { suggestedFollowUps } : {}),
463
+ ...(error ? { error } : {}),
464
+ };
465
+ }
466
+ /**
467
+ * Get-or-create-and-merge the per-statement scratch entry. Both
468
+ * `query_result` and `message_result` paths call this with their
469
+ * partial bag of fields; the resulting record is the union of
470
+ * everything we know about that statement so far.
471
+ */
472
+ function upsertDatasetMeta(store, statementId, patch) {
473
+ const existing = store.get(statementId);
474
+ const merged = {
475
+ chartId: existing?.chartId ?? randomUUID().replace(/-/g, "").slice(0, 8),
476
+ statementId,
477
+ columns: patch.columns ?? existing?.columns ?? [],
478
+ rowCount: patch.rowCount ?? existing?.rowCount ?? 0,
479
+ ...(patch.title ?? existing?.title
480
+ ? { title: patch.title ?? existing?.title }
481
+ : {}),
482
+ ...(patch.description ?? existing?.description
483
+ ? { description: patch.description ?? existing?.description }
484
+ : {}),
485
+ ...(patch.sql ?? existing?.sql ? { sql: patch.sql ?? existing?.sql } : {}),
199
486
  };
487
+ store.set(statementId, merged);
488
+ return merged;
489
+ }
490
+ /**
491
+ * Convert Genie's `data_array` (column-positional `string | null`
492
+ * tuples) into plain JS row objects keyed by column name. Numeric
493
+ * strings are coerced to numbers so the chart-planner picks
494
+ * `value` axes instead of `category` axes; everything else passes
495
+ * through verbatim. `null` becomes `null`.
496
+ */
497
+ function genieRowsToObjects(columns, dataArray) {
498
+ const out = [];
499
+ for (const row of dataArray) {
500
+ const obj = {};
501
+ columns.forEach((col, i) => {
502
+ const cell = row[i] ?? null;
503
+ obj[col] = coerceCell(cell);
504
+ });
505
+ out.push(obj);
506
+ }
507
+ return out;
508
+ }
509
+ /** Best-effort numeric coercion for Genie's all-strings cells. */
510
+ function coerceCell(cell) {
511
+ if (cell === null)
512
+ return null;
513
+ // Anchored to keep `12.5px` / `123abc` as strings; only fully
514
+ // numeric values become JS numbers.
515
+ if (/^-?\d+(\.\d+)?$/.test(cell)) {
516
+ const n = Number(cell);
517
+ if (Number.isFinite(n))
518
+ return n;
519
+ }
520
+ return cell;
200
521
  }
201
522
  /**
202
523
  * Toolkit provider built from a live AppKit `GeniePlugin` instance.
@@ -266,6 +587,8 @@ function humanizeGenieStatus(status) {
266
587
  case "FAILED":
267
588
  return "Failed";
268
589
  default:
269
- return status.toLowerCase().replace(/_/g, " ");
590
+ return [
591
+ ...stringUtils.tokenizeWithOptions({ capitalize: true, lowerCase: true }, status),
592
+ ].join(" ");
270
593
  }
271
594
  }
@@ -0,0 +1,67 @@
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;