@dbx-tools/appkit-mastra 0.9.143 → 0.9.145

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/src/mlflow.ts CHANGED
@@ -49,6 +49,7 @@ let directMlflowInitStarted = false;
49
49
  let directMlflowProvider: NodeTracerProvider | undefined;
50
50
  let directMlflowProcessor: SpanProcessor | undefined;
51
51
  let appMlflowTraceInfoInitStarted = false;
52
+ let appMlflowTraceInfoRequestInitStarted = false;
52
53
  let appMlflowTraceInfoClient: WorkspaceClient | undefined;
53
54
  let appMlflowTracePrefix: string | undefined;
54
55
  const pendingAppMlflowTraceInfo = new Set<Promise<void>>();
@@ -209,7 +210,18 @@ async function resolveUcTracePrefix(client: WorkspaceClient): Promise<string | u
209
210
  if (loadedUcPrefix) return loadedUcPrefix.prefix;
210
211
  const experiment = await fetchExperiment(client);
211
212
  if (!experiment) return undefined;
212
- const prefix = ucTracePrefixFromExperimentTags(experiment.tags);
213
+ const location = experiment.trace_location?.uc_trace_location;
214
+ const prefix =
215
+ ucTracePrefixFromExperimentTags(experiment.tags) ??
216
+ (location
217
+ ? [
218
+ location.catalog,
219
+ location.schema,
220
+ location.effective_table_prefix ?? location.table_prefix,
221
+ ]
222
+ .filter(Boolean)
223
+ .join(".")
224
+ : undefined);
213
225
  loadedUcPrefix = { prefix };
214
226
  return prefix;
215
227
  }
@@ -229,6 +241,23 @@ const MlflowExperimentResponseSchema = z
229
241
  .object({
230
242
  experiment_id: z.string().min(1).describe("Workspace MLflow experiment identifier."),
231
243
  tags: z.unknown().optional().describe("Experiment tags, including the UC trace location."),
244
+ trace_location: z
245
+ .object({
246
+ uc_trace_location: z
247
+ .object({
248
+ catalog: z.string().describe("Unity Catalog catalog name."),
249
+ schema: z.string().describe("Unity Catalog schema name."),
250
+ table_prefix: z.string().describe("Configured trace table prefix."),
251
+ effective_table_prefix: z
252
+ .string()
253
+ .optional()
254
+ .describe("Effective trace table prefix selected by Databricks."),
255
+ })
256
+ .optional()
257
+ .describe("Unity Catalog trace location."),
258
+ })
259
+ .optional()
260
+ .describe("Experiment trace storage location."),
232
261
  })
233
262
  .passthrough()
234
263
  .describe("Configured MLflow experiment."),
@@ -354,13 +383,13 @@ export async function initializeAppMlflowTraceInfo(client?: WorkspaceClient): Pr
354
383
  appMlflowTraceInfoInitStarted = true;
355
384
  try {
356
385
  const resolvedClient = client ?? (createWorkspaceClient() as WorkspaceClient);
386
+ appMlflowTraceInfoClient ??= resolvedClient;
357
387
  const prefix = await resolveUcTracePrefix(resolvedClient);
358
388
  if (!prefix || prefix.split(".").length !== 3) {
359
389
  throw new ConfigurationError(
360
390
  "Databricks Apps MLflow trace-info promotion requires a UC-linked experiment or MLFLOW_UC_TRACE_PREFIX.",
361
391
  );
362
392
  }
363
- appMlflowTraceInfoClient = resolvedClient;
364
393
  appMlflowTracePrefix = prefix;
365
394
  logger.info("Databricks Apps MLflow trace-info promotion enabled", { prefix });
366
395
  return true;
@@ -368,10 +397,25 @@ export async function initializeAppMlflowTraceInfo(client?: WorkspaceClient): Pr
368
397
  logger.warn("Databricks Apps MLflow trace-info promotion disabled", {
369
398
  error: errorUtils.toError(err),
370
399
  });
400
+ // Startup uses the service principal. Permit one later request-scoped
401
+ // client to resolve experiment metadata when that principal cannot read it.
402
+ if (client === undefined) appMlflowTraceInfoInitStarted = false;
371
403
  return false;
372
404
  }
373
405
  }
374
406
 
407
+ /**
408
+ * Retry Apps trace-location discovery once with a request-scoped workspace
409
+ * client after service-principal startup discovery failed.
410
+ */
411
+ export async function initializeRequestMlflowTraceInfo(client: WorkspaceClient): Promise<boolean> {
412
+ if (appMlflowTraceInfoClient && appMlflowTracePrefix) return true;
413
+ if (appMlflowTraceInfoRequestInitStarted || !appMlflowTraceInfoConfigured()) return false;
414
+ appMlflowTraceInfoRequestInitStarted = true;
415
+ appMlflowTraceInfoInitStarted = false;
416
+ return initializeAppMlflowTraceInfo(client);
417
+ }
418
+
375
419
  /** Trace-level values persisted beside spans already exported by the Apps OTLP sidecar. */
376
420
  export interface AppMlflowTraceInfo {
377
421
  traceId: string;
@@ -434,6 +478,7 @@ export function persistAppMlflowTraceInfo(params: AppMlflowTraceInfo): void {
434
478
  /** Reset Apps trace-info promotion state for tests. */
435
479
  export function resetAppMlflowTraceInfo(): void {
436
480
  appMlflowTraceInfoInitStarted = false;
481
+ appMlflowTraceInfoRequestInitStarted = false;
437
482
  appMlflowTraceInfoClient = undefined;
438
483
  appMlflowTracePrefix = undefined;
439
484
  pendingAppMlflowTraceInfo.clear();
@@ -644,7 +689,7 @@ async function resolveMlflowExperimentManagerUrl(
644
689
 
645
690
  async function fetchExperiment(
646
691
  client: WorkspaceClient,
647
- ): Promise<{ experiment_id: string; tags?: unknown } | undefined> {
692
+ ): Promise<z.infer<typeof MlflowExperimentResponseSchema>["experiment"] | undefined> {
648
693
  const experiment = configuredExperiment();
649
694
  const path = experiment.id
650
695
  ? `/api/2.0/mlflow/experiments/get?experiment_id=${encodeURIComponent(experiment.id)}`
package/src/plugin.ts CHANGED
@@ -278,6 +278,11 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
278
278
  });
279
279
  this.config.feedbackMode = this.resolvedFeedbackMode;
280
280
  if (this.config.observability !== false) {
281
+ await appkitTelemetry.configureNativeHttpTelemetry({
282
+ // A low-level request without an active semantic span becomes a noisy
283
+ // standalone trace. Requests inside an agent/tool span remain visible.
284
+ outgoingFilter: appkitTelemetry.excludeUnparentedOutgoingHttpTelemetry,
285
+ });
281
286
  await registerAppMlflowTraceInfoProcessor();
282
287
  }
283
288
  // Wait until sibling plugins (e.g. `lakebase`) finish `setup()` so
@@ -386,8 +391,8 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
386
391
  getMastra: () => this.mastra,
387
392
  /**
388
393
  * Build the `RequestContext` an agent turn driven from OUTSIDE this
389
- * plugin's routes needs (a Teams activity on another plugin's endpoint, a
390
- * scheduled job).
394
+ * plugin's routes needs, such as another plugin's endpoint or a scheduled
395
+ * job.
391
396
  *
392
397
  * Mastra's user-scoped tools - `ask_genie` most visibly - read the AppKit
393
398
  * user off the request context, which only the HTTP middleware stamps.
package/src/server.ts CHANGED
@@ -35,6 +35,7 @@ import {
35
35
  FOLLOW_UP_USER_TEXT_KEY,
36
36
  } from "./follow-up-suggestions.ts";
37
37
  import { requestUserEmail, requestUserId } from "./identity.ts";
38
+ import { initializeRequestMlflowTraceInfo } from "./mlflow.ts";
38
39
  import { MASTRA_ORIGIN_KEY } from "./request-context.ts";
39
40
  import {
40
41
  extractModelOverride,
@@ -118,12 +119,11 @@ export interface AttributedIdentity {
118
119
  * `requestContext`.
119
120
  *
120
121
  * Split out of {@link MastraServer} because the HTTP middleware is not the only
121
- * caller that needs it: an out-of-band turn (a Teams activity arriving on
122
- * another plugin's route, a scheduled job) drives `agent.generate` directly, and
123
- * without this stamp every user-scoped tool - `ask_genie` above all - fails with
124
- * "invoke the tool from an agent turn served by the mastra plugin". Those
125
- * callers build a context with {@link createRequestContext} instead of a
126
- * request.
122
+ * caller that needs it: an out-of-band turn from another plugin route or a
123
+ * scheduled job drives `agent.generate` directly, and without this stamp every
124
+ * user-scoped tool - `ask_genie` above all - fails with "invoke the tool from an
125
+ * agent turn served by the mastra plugin". Those callers build a context with
126
+ * {@link createRequestContext} instead of a request.
127
127
  *
128
128
  * The Databricks credential always comes from the ambient execution context;
129
129
  * `attributed` only changes WHO the turn is attributed to when that context is
@@ -138,6 +138,7 @@ export async function stampRequestContextUser(
138
138
  ): Promise<void> {
139
139
  if ([MASTRA_USER_KEY, MASTRA_RESOURCE_ID_KEY].every((key) => requestContext.get(key))) return;
140
140
  const executionContext = getExecutionContext();
141
+ await initializeRequestMlflowTraceInfo(executionContext.client);
141
142
  const isUserContext = "isUserContext" in executionContext;
142
143
  // The Databricks CREDENTIAL is always the ambient execution context's client
143
144
  // (OBO in a user context, the service principal otherwise). WHO the turn is
@@ -185,8 +186,8 @@ export async function stampRequestContextUser(
185
186
  * a request id so the turn's spans join up in traces. The result is passed as
186
187
  * `requestContext` to `agent.generate` / `agent.stream`.
187
188
  *
188
- * This is what makes a card turn answer with the SAME data a chat turn does: the
189
- * difference between the two endpoints is presentation, not capability.
189
+ * This gives an out-of-band turn the same data and tool capability as a chat
190
+ * turn.
190
191
  */
191
192
  export async function createRequestContext(
192
193
  options: { threadId?: string; resourceId?: string; requestId?: string } = {},
@@ -194,8 +195,8 @@ export async function createRequestContext(
194
195
  const requestContext = new RequestContext();
195
196
  await stampRequestContextUser(requestContext);
196
197
  if (options.threadId) requestContext.set(MASTRA_THREAD_ID_KEY, options.threadId);
197
- // Stamped AFTER the user so an explicit resource id (a channel member, say)
198
- // wins over the ambient identity `stampRequestContextUser` defaulted to.
198
+ // Stamped AFTER the user so an explicit resource id wins over the ambient
199
+ // identity `stampRequestContextUser` defaulted to.
199
200
  if (options.resourceId) requestContext.set(MASTRA_RESOURCE_ID_KEY, options.resourceId);
200
201
  requestContext.set(MASTRA_REQUEST_ID_KEY, options.requestId ?? hash.id());
201
202
  return requestContext;
package/src/telemetry.ts CHANGED
@@ -42,6 +42,7 @@ import type express from "express";
42
42
  import { MASTRA_AUTH_MODE_KEY, MASTRA_USER_EMAIL_KEY, MASTRA_USER_NAME_KEY } from "./config.ts";
43
43
  import { USER_EMAIL_HEADER, USER_ID_HEADER } from "./identity.ts";
44
44
  import {
45
+ appMlflowTraceInfoConfigured,
45
46
  directMlflowTracingActive,
46
47
  initializeAppMlflowTraceInfo,
47
48
  persistAppMlflowTraceInfo,
@@ -118,7 +119,7 @@ export const GEN_AI_OPERATION_NAME_ATTR = "gen_ai.operation.name";
118
119
  * Root-span user id the MLflow experiment UI's User column reads from OTel
119
120
  * traces. Prefer the forwarded email when present so the column is human.
120
121
  */
121
- export const MLFLOW_USER_ATTR = "user.id";
122
+ export const MLFLOW_USER_ATTR = appkitTelemetry.USER_TRACE_ATTRIBUTE;
122
123
  /** Root-span session id that enables the MLflow Sessions tab. */
123
124
  export const MLFLOW_SESSION_ATTR = "session.id";
124
125
  /** Root-span type for a Mastra agent turn. */
@@ -507,7 +508,8 @@ let appMlflowTraceInfoProcessorRegistered = false;
507
508
  /** Register trace enrichment before AppKit constructs its native provider. */
508
509
  export async function registerAppMlflowTraceInfoProcessor(): Promise<boolean> {
509
510
  if (appMlflowTraceInfoProcessorRegistered) return true;
510
- if (!(await initializeAppMlflowTraceInfo())) return false;
511
+ if (!appMlflowTraceInfoConfigured()) return false;
512
+ await initializeAppMlflowTraceInfo();
511
513
  await appkitTelemetry.registerNativeSpanProcessor(new AppMlflowTraceInfoSpanProcessor());
512
514
  appMlflowTraceInfoProcessorRegistered = true;
513
515
  return true;