From c40ab7c41f6cfbb57f01644dad5b023fcb73428e Mon Sep 17 00:00:00 2001 From: MarioCadenas Date: Fri, 14 Aug 2026 11:50:02 +0200 Subject: [PATCH 1/9] refactor(appkit): let plugins contribute otel span processors Split TelemetryManager into two phases so a single global tracer provider can carry both the OTLP exporter and plugin-contributed processors (e.g. MLflow), instead of each SDK racing to register the global provider. - initialize() registers the meter and logger providers eagerly, because OTel's metrics API has no lazy proxy: an instrument bound against the NoOp meter stays NoOp for the process lifetime. - registerSpanProcessor() lets plugins contribute a span processor during setup(); ignored with a warning after start() since a started provider's processors are immutable in OTel JS 2.x. - start() (called after plugin setup) builds the global NodeTracerProvider with the OTLP processor plus every contributed one, and no-ops when nothing needs tracing. Deferring is safe: ProxyTracer rebinds tracers obtained earlier and no span is emitted during setup. Swaps the @opentelemetry/sdk-node dependency for @opentelemetry/sdk-trace-node, since the tracer provider is now built directly instead of via NodeSDK. Signed-off-by: MarioCadenas --- packages/appkit/package.json | 2 +- packages/appkit/src/core/appkit.ts | 6 + .../core/tests/appkit-as-user-exports.test.ts | 2 + .../appkit/src/telemetry/telemetry-manager.ts | 175 +++++++++++++++--- .../telemetry/tests/telemetry-manager.test.ts | 104 ++++++++++- pnpm-lock.yaml | 6 +- 6 files changed, 264 insertions(+), 31 deletions(-) diff --git a/packages/appkit/package.json b/packages/appkit/package.json index 081132e87..6d82f31ee 100644 --- a/packages/appkit/package.json +++ b/packages/appkit/package.json @@ -83,8 +83,8 @@ "@opentelemetry/resources": "2.8.0", "@opentelemetry/sdk-logs": "0.219.0", "@opentelemetry/sdk-metrics": "2.8.0", - "@opentelemetry/sdk-node": "0.219.0", "@opentelemetry/sdk-trace-base": "2.8.0", + "@opentelemetry/sdk-trace-node": "2.8.0", "@opentelemetry/semantic-conventions": "1.38.0", "@types/semver": "7.7.1", "apache-arrow": "21.1.0", diff --git a/packages/appkit/src/core/appkit.ts b/packages/appkit/src/core/appkit.ts index 0ccfd64e9..f5f4f2725 100644 --- a/packages/appkit/src/core/appkit.ts +++ b/packages/appkit/src/core/appkit.ts @@ -223,6 +223,12 @@ export class AppKit { const instance = new AppKit(mergedConfig); await Promise.all(instance.#setupPromises); + + // Build the global tracer provider now that every plugin's setup() has run + // and contributed any span processors. Deferred to here so a single provider + // carries all processors (OTLP + plugin-contributed); see TelemetryManager. + TelemetryManager.start(); + await instance.#context.emitLifecycle("setup:complete"); const handle = instance as unknown as PluginMap; diff --git a/packages/appkit/src/core/tests/appkit-as-user-exports.test.ts b/packages/appkit/src/core/tests/appkit-as-user-exports.test.ts index 45467625d..f644470bd 100644 --- a/packages/appkit/src/core/tests/appkit-as-user-exports.test.ts +++ b/packages/appkit/src/core/tests/appkit-as-user-exports.test.ts @@ -45,6 +45,8 @@ vi.mock("../../telemetry", async () => { ...actual, TelemetryManager: { initialize: vi.fn(), + start: vi.fn(), + registerSpanProcessor: vi.fn(), getProvider: () => ({ getTracer: () => ({ startActiveSpan: vi.fn((_name: string, fn: (span: any) => any) => diff --git a/packages/appkit/src/telemetry/telemetry-manager.ts b/packages/appkit/src/telemetry/telemetry-manager.ts index b19cd1a07..a19cafd57 100644 --- a/packages/appkit/src/telemetry/telemetry-manager.ts +++ b/packages/appkit/src/telemetry/telemetry-manager.ts @@ -1,3 +1,5 @@ +import { metrics } from "@opentelemetry/api"; +import { logs } from "@opentelemetry/api-logs"; import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node"; import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-proto"; import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-proto"; @@ -14,9 +16,19 @@ import { type Resource, resourceFromAttributes, } from "@opentelemetry/resources"; -import { BatchLogRecordProcessor } from "@opentelemetry/sdk-logs"; -import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics"; -import { NodeSDK } from "@opentelemetry/sdk-node"; +import { + BatchLogRecordProcessor, + LoggerProvider, +} from "@opentelemetry/sdk-logs"; +import { + MeterProvider, + PeriodicExportingMetricReader, +} from "@opentelemetry/sdk-metrics"; +import { + BatchSpanProcessor, + type SpanProcessor, +} from "@opentelemetry/sdk-trace-base"; +import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node"; import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION, @@ -30,12 +42,34 @@ import type { TelemetryConfig } from "./types"; const logger = createLogger("telemetry"); +/** + * Owns the app's OpenTelemetry providers, split into two phases so plugins can + * contribute trace span processors before the tracer provider is built. + * + * - `initialize()` runs at app bootstrap, before plugin setup. It registers the + * meter and logger providers eagerly, because OTel's metrics API has no lazy + * proxy: a counter/histogram bound against the NoOp meter (as every connector + * and the cache do in their constructors) stays NoOp for the process lifetime. + * It does NOT register a tracer provider. + * - `registerSpanProcessor()` is called by plugins during `setup()` to add a + * span processor (e.g. an MLflow exporter) to the not-yet-built tracer. + * - `start()` runs after all plugin `setup()` completes. It builds the single + * global tracer provider with the OTLP processor (if configured) plus every + * contributed processor. Deferring is safe for traces: OTel's ProxyTracer + * rebinds tracers obtained before registration, and no span is emitted during + * setup. + */ export class TelemetryManager { private static readonly DEFAULT_EXPORT_INTERVAL_MS = 10000; private static readonly DEFAULT_FALLBACK_APP_NAME = "databricks-app"; private static instance?: TelemetryManager; - private sdk?: NodeSDK; + private resource?: Resource; + private meterProvider?: MeterProvider; + private loggerProvider?: LoggerProvider; + private tracerProvider?: NodeTracerProvider; + private readonly spanProcessors: SpanProcessor[] = []; + private started = false; private shutdownPromise?: Promise; /** @@ -67,20 +101,49 @@ export class TelemetryManager { instance._initialize(config); } + /** + * Contribute a span processor to the not-yet-built tracer provider. Called by + * plugins during `setup()`. No-op with a warning once `start()` has run, since + * a started provider's processors are immutable in OTel JS 2.x. + */ + static registerSpanProcessor(processor: SpanProcessor): void { + TelemetryManager.getInstance()._registerSpanProcessor(processor); + } + + private _registerSpanProcessor(processor: SpanProcessor): void { + if (this.started) { + logger.warn( + "registerSpanProcessor called after start(); processor ignored. " + + "Contribute span processors during plugin setup().", + ); + return; + } + this.spanProcessors.push(processor); + } + + /** + * Phase 1: register the meter and logger providers eagerly (before plugin + * setup), so metric instruments bound in connector/cache constructors attach + * to real meters. The tracer provider is deferred to `start()`. + * + * When no OTLP endpoint is configured, meter/logger registration is skipped; + * a contributed span processor can still bring up tracing in `start()`. + */ private _initialize(config: Partial): void { - if (this.sdk) return; + if (this.resource) return; + this.resource = this.createResource(config); + // OTLP exporters need an endpoint. Without one there is nothing to export + // metrics/logs to, so skip those providers — but still capture the resource + // and let `start()` bring up a tracer if a plugin contributed a processor. if (!process.env.OTEL_EXPORTER_OTLP_ENDPOINT) { return; } try { - this.sdk = new NodeSDK({ - resource: this.createResource(config), - autoDetectResources: false, - sampler: new AppKitSampler(), - traceExporter: new OTLPTraceExporter({ headers: config.headers }), - metricReaders: [ + this.meterProvider = new MeterProvider({ + resource: this.resource, + readers: [ new PeriodicExportingMetricReader({ exporter: new OTLPMetricExporter({ headers: config.headers }), exportIntervalMillis: @@ -88,21 +151,72 @@ export class TelemetryManager { TelemetryManager.DEFAULT_EXPORT_INTERVAL_MS, }), ], - logRecordProcessors: [ + }); + metrics.setGlobalMeterProvider(this.meterProvider); + + this.loggerProvider = new LoggerProvider({ + resource: this.resource, + processors: [ new BatchLogRecordProcessor( new OTLPLogExporter({ headers: config.headers }), ), ], - instrumentations: this.getDefaultInstrumentations(), }); + logs.setGlobalLoggerProvider(this.loggerProvider); + + // The OTLP trace exporter is the first span processor; contributed + // processors join it in `start()`. + this.spanProcessors.push( + new BatchSpanProcessor( + new OTLPTraceExporter({ headers: config.headers }), + ), + ); - this.sdk.start(); - logger.debug("Initialized successfully"); + this.registerInstrumentations(this.getDefaultInstrumentations()); + logger.debug("Meter/logger providers initialized"); } catch (error) { logger.error("Failed to initialize: %O", error); } } + /** + * Phase 2: build and register the global tracer provider. Called by core + * after every plugin's `setup()` completes, so all contributed span + * processors are known. No-op when nothing needs tracing (no OTLP endpoint + * and no contributed processor), preserving "no telemetry unless configured". + * + * `NodeTracerProvider.register()` installs the async-hooks context manager and + * W3C propagators — the same wiring `NodeSDK.start()` did — so span nesting + * across awaits is preserved. + */ + static start(): void { + TelemetryManager.getInstance()._start(); + } + + private _start(): void { + if (this.started) return; + this.started = true; + + if (this.spanProcessors.length === 0) { + return; + } + + try { + this.tracerProvider = new NodeTracerProvider({ + resource: this.resource, + sampler: new AppKitSampler(), + spanProcessors: this.spanProcessors, + }); + this.tracerProvider.register(); + logger.debug( + "Tracer provider started with %d span processor(s)", + this.spanProcessors.length, + ); + } catch (error) { + logger.error("Failed to start tracer provider: %O", error); + } + } + /** * Register OpenTelemetry instrumentations. * Can be called at any time, but recommended to call in plugin constructor. @@ -160,23 +274,34 @@ export class TelemetryManager { } /** - * Flush and shut down the OpenTelemetry SDK. + * Flush and shut down the tracer, meter, and logger providers. * - * Idempotent: the SDK reference is cleared synchronously and concurrent + * Idempotent: the provider references are cleared synchronously and concurrent * or repeated calls await the same in-flight flush. Awaited by the core * lifecycle manager during graceful shutdown — that manager owns the * process signal handlers, so telemetry no longer registers its own. */ async shutdown(): Promise { - if (this.sdk) { - const sdk = this.sdk; - this.sdk = undefined; + const providers = [ + this.tracerProvider, + this.meterProvider, + this.loggerProvider, + ].filter((p): p is NonNullable => p !== undefined); + + if (providers.length > 0) { + this.tracerProvider = undefined; + this.meterProvider = undefined; + this.loggerProvider = undefined; this.shutdownPromise = (async () => { - try { - await sdk.shutdown(); - } catch (error) { - logger.error("Error shutting down: %O", error); - } + await Promise.all( + providers.map(async (provider) => { + try { + await provider.shutdown(); + } catch (error) { + logger.error("Error shutting down: %O", error); + } + }), + ); })(); } diff --git a/packages/appkit/src/telemetry/tests/telemetry-manager.test.ts b/packages/appkit/src/telemetry/tests/telemetry-manager.test.ts index 84be228b8..ffc506831 100644 --- a/packages/appkit/src/telemetry/tests/telemetry-manager.test.ts +++ b/packages/appkit/src/telemetry/tests/telemetry-manager.test.ts @@ -1,3 +1,5 @@ +import { context, metrics, trace } from "@opentelemetry/api"; +import { logs } from "@opentelemetry/api-logs"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { TelemetryManager } from "../telemetry-manager"; @@ -55,12 +57,21 @@ describe("TelemetryManager", () => { vi.clearAllMocks(); // @ts-expect-error - accessing private static property for testing TelemetryManager.instance = undefined; - // @ts-expect-error - accessing private static property for testing - TelemetryManager.shutdownRegistered = false; + // OTel's registerGlobal is allowOverride=false: a global registered by one + // test would make the next test's registration a silent no-op. Reset all + // global providers so each test starts clean. + trace.disable(); + metrics.disable(); + logs.disable(); + context.disable(); }); afterEach(() => { process.env = originalEnv; + trace.disable(); + metrics.disable(); + logs.disable(); + context.disable(); }); test("getInstance() should return singleton instance", () => { @@ -90,6 +101,7 @@ describe("TelemetryManager", () => { serviceName: "integration-test", serviceVersion: "1.0.0", }); + TelemetryManager.start(); const telemetryProvider = TelemetryManager.getProvider("test-plugin"); const tracer = telemetryProvider.getTracer(); @@ -186,6 +198,7 @@ describe("TelemetryManager", () => { serviceName: "span-test", serviceVersion: "1.0.0", }); + TelemetryManager.start(); const telemetryProvider = TelemetryManager.getProvider("span-test-plugin"); @@ -211,6 +224,7 @@ describe("TelemetryManager", () => { serviceName: "error-test", serviceVersion: "1.0.0", }); + TelemetryManager.start(); const telemetryProvider = TelemetryManager.getProvider("error-test-plugin"); @@ -224,4 +238,90 @@ describe("TelemetryManager", () => { ).rejects.toThrow("Test error in span"); }); }); + + describe("two-phase init (registerSpanProcessor + start)", () => { + /** Minimal SpanProcessor that records the names of spans it sees start. */ + function recordingProcessor() { + const startedSpans: string[] = []; + return { + startedSpans, + onStart: (span: { name: string }) => { + startedSpans.push(span.name); + }, + onEnd: () => {}, + forceFlush: () => Promise.resolve(), + shutdown: () => Promise.resolve(), + }; + } + + test("routes spans to a contributed processor with no OTLP endpoint", async () => { + process.env.OTEL_EXPORTER_OTLP_ENDPOINT = ""; + const processor = recordingProcessor(); + + TelemetryManager.initialize({ serviceName: "contrib-only" }); + TelemetryManager.registerSpanProcessor(processor as any); + TelemetryManager.start(); + + const tracer = TelemetryManager.getProvider("contrib-plugin").getTracer(); + await tracer.startActiveSpan("contributed.span", {}, async (span) => { + span.end(); + }); + + expect(processor.startedSpans).toContain("contributed.span"); + }); + + test("start() is idempotent", () => { + process.env.OTEL_EXPORTER_OTLP_ENDPOINT = ""; + const processor = recordingProcessor(); + + TelemetryManager.initialize({ serviceName: "idempotent" }); + TelemetryManager.registerSpanProcessor(processor as any); + TelemetryManager.start(); + + expect(() => TelemetryManager.start()).not.toThrow(); + }); + + test("registerSpanProcessor after start() is ignored (not attached)", async () => { + process.env.OTEL_EXPORTER_OTLP_ENDPOINT = ""; + const early = recordingProcessor(); + const late = recordingProcessor(); + + TelemetryManager.initialize({ serviceName: "late-register" }); + TelemetryManager.registerSpanProcessor(early as any); + TelemetryManager.start(); + TelemetryManager.registerSpanProcessor(late as any); + + const tracer = TelemetryManager.getProvider("late-plugin").getTracer(); + await tracer.startActiveSpan("post.start.span", {}, async (span) => { + span.end(); + }); + + expect(early.startedSpans).toContain("post.start.span"); + expect(late.startedSpans).toHaveLength(0); + }); + + test("start() with no OTLP endpoint and no processors is a no-op", () => { + process.env.OTEL_EXPORTER_OTLP_ENDPOINT = ""; + + TelemetryManager.initialize({ serviceName: "no-telemetry" }); + + expect(() => TelemetryManager.start()).not.toThrow(); + }); + + test("metrics obtained after initialize() (before start()) still record", () => { + // Guards the eager-metrics invariant: the meter provider is registered in + // initialize(), not start(), because OTel's metrics API has no lazy proxy. + process.env.OTEL_EXPORTER_OTLP_ENDPOINT = "http://localhost:4318"; + + TelemetryManager.initialize({ serviceName: "eager-metrics" }); + + // Instrument bound BEFORE start() — mirrors connector/cache constructors. + const meter = TelemetryManager.getProvider("metrics-plugin").getMeter(); + const counter = meter.createCounter("eager.counter"); + + TelemetryManager.start(); + + expect(() => counter.add(1, { label: "value" })).not.toThrow(); + }); + }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1657de0f8..9d00135ea 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -294,12 +294,12 @@ importers: '@opentelemetry/sdk-metrics': specifier: 2.8.0 version: 2.8.0(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-node': - specifier: 0.219.0 - version: 0.219.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': specifier: 2.8.0 version: 2.8.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-node': + specifier: 2.8.0 + version: 2.8.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': specifier: 1.38.0 version: 1.38.0 From 7b98fb89c062b75f1d77527c5ea895760f75f9b3 Mon Sep 17 00:00:00 2001 From: MarioCadenas Date: Fri, 21 Aug 2026 15:01:09 +0200 Subject: [PATCH 2/9] fix(agents): route mlflow tracing through AppKit's single otel provider The mlflow-tracing SDK's init() stands up and globally registers its own OpenTelemetry tracer provider. OTel's registerGlobal is allowOverride=false, so when an OTLP endpoint and agent tracing are both active, AppKit's provider and mlflow's race for the global slot and one exporter is silently dropped. Instead of calling init() (which self-registers), the agents plugin now builds mlflow's span processor itself and contributes it to AppKit's single provider via TelemetryManager.registerSpanProcessor() during setup(). mlflow's global config is seeded lazily on first trace (after start()), so init()'s own registration harmlessly loses the already-claimed global slot. A GatedMlflowSpanProcessor keeps the contributed processor inert until config is seeded, so AppKit's own spans created before the first agent turn don't hit mlflow's getConfig() throw. The processor/exporter are deep-imported from mlflow-tracing internals (no public export in 0.1.3), pinned and guarded by a tripwire test. Signed-off-by: MarioCadenas --- packages/appkit/src/plugins/agents/mlflow.ts | 137 ++++++++++++++- .../src/plugins/agents/tests/mlflow.test.ts | 158 +++++++++++++++++- 2 files changed, 288 insertions(+), 7 deletions(-) diff --git a/packages/appkit/src/plugins/agents/mlflow.ts b/packages/appkit/src/plugins/agents/mlflow.ts index cd48b8190..b3a63530d 100644 --- a/packages/appkit/src/plugins/agents/mlflow.ts +++ b/packages/appkit/src/plugins/agents/mlflow.ts @@ -1,12 +1,105 @@ +import type { SpanProcessor } from "@opentelemetry/sdk-trace-base"; + import { createLogger } from "../../logging/logger"; +import { TelemetryManager } from "../../telemetry"; const logger = createLogger("agents"); type MlflowModule = typeof import("mlflow-tracing"); +interface MlflowInitConfig { + trackingUri: string; + experimentId: string; + host?: string; +} + let mlflow: MlflowModule | undefined; let enabled = false; let initStarted = false; +let configured = false; +let initConfig: MlflowInitConfig | undefined; +let gatedProcessor: GatedMlflowSpanProcessor | undefined; + +/** + * Wraps mlflow's OTel `SpanProcessor` so it stays inert until mlflow's global + * config is seeded. mlflow's `onStart` calls its own `getConfig()`, which THROWS + * before `init()` runs — and that throw propagates out of `tracer.startSpan()`, + * so it would break unrelated AppKit spans (HTTP, analytics) created between + * `TelemetryManager.start()` and the first agent turn. We contribute this to + * AppKit's single tracer provider during `setup()`, but only start forwarding + * once `ready()` is called — right after the lazy `init()` in {@link ensureConfigured}. + */ +export class GatedMlflowSpanProcessor implements SpanProcessor { + #inner: SpanProcessor; + #ready = false; + // Spans we forwarded `onStart` for, so `onEnd` stays balanced — mlflow never + // sees an end without a matching start. + #forwarded = new WeakSet(); + + constructor(inner: SpanProcessor) { + this.#inner = inner; + } + + ready(): void { + this.#ready = true; + } + + onStart( + span: Parameters[0], + parentContext: Parameters[1], + ): void { + if (!this.#ready) return; + this.#forwarded.add(span); + this.#inner.onStart(span, parentContext); + } + + onEnd(span: Parameters[0]): void { + if (!this.#forwarded.has(span)) return; + this.#inner.onEnd(span); + } + + forceFlush(): Promise { + return this.#inner.forceFlush(); + } + + shutdown(): Promise { + return this.#inner.shutdown(); + } +} + +/** + * Build mlflow's OTel `SpanProcessor` ourselves rather than letting `init()` + * build and globally register its own tracer provider. This lets AppKit own the + * single global provider (OTLP + this processor), so agent spans reach both + * MLflow and any OTLP endpoint without two SDKs racing for the global slot. + * + * Deep-imports `mlflow-tracing` internals that aren't on its public entrypoint — + * pinned to the exact version in package.json and guarded by a test that fails + * loudly if a version bump renames them. + */ +async function buildMlflowSpanProcessor( + config: MlflowInitConfig, +): Promise { + const { createAuthProvider } = await import("mlflow-tracing/dist/auth"); + const { MlflowClient } = await import("mlflow-tracing"); + const { MlflowSpanExporter, MlflowSpanProcessor } = + await import("mlflow-tracing/dist/exporters/mlflow"); + + const authProvider = createAuthProvider({ + trackingUri: config.trackingUri, + ...(config.host ? { host: config.host } : {}), + }); + const client = new MlflowClient({ + trackingUri: config.trackingUri, + authProvider, + }); + // mlflow builds against a different @opentelemetry/sdk-trace-base major than + // AppKit; the SpanProcessor contract (onStart/onEnd/forceFlush/shutdown) is + // stable across them, so bridge the nominal type mismatch with one cast. + return new MlflowSpanProcessor( + new MlflowSpanExporter(client), + ) as unknown as SpanProcessor; +} /** The bound MLflow experiment id, from the optional `experiment` resource. */ function experimentId(): string | undefined { @@ -30,6 +123,13 @@ function normalizedDatabricksHost(): string | undefined { /** * Initialize MLflow agent tracing once, when an experiment is bound — i.e. the * agents plugin's optional `experiment` resource is set (`MLFLOW_EXPERIMENT_ID`). + * Called from the agents plugin's `setup()`, before `TelemetryManager.start()`. + * + * Rather than let `mlflow.init()` stand up and globally register its own tracer + * provider (which would race AppKit's and drop one exporter), we build mlflow's + * span processor ourselves and contribute it to AppKit's single provider via + * {@link TelemetryManager.registerSpanProcessor}. mlflow's global config is + * seeded lazily in {@link ensureConfigured} on first trace, after `start()`. * * Auth is resolved by the `mlflow-tracing` SDK from the app's own Databricks * credentials — `DATABRICKS_HOST`/`DATABRICKS_TOKEN` or a `~/.databrickscfg` @@ -49,11 +149,14 @@ export async function initAgentTracing(): Promise { try { mlflow = await import("mlflow-tracing"); const host = normalizedDatabricksHost(); - mlflow.init({ + initConfig = { trackingUri: process.env.MLFLOW_TRACKING_URI?.trim() || "databricks", experimentId: id, ...(host ? { host } : {}), - }); + }; + const processor = await buildMlflowSpanProcessor(initConfig); + gatedProcessor = new GatedMlflowSpanProcessor(processor); + TelemetryManager.registerSpanProcessor(gatedProcessor); enabled = true; logger.info("MLflow agent tracing enabled (experiment %s)", id); } catch (err) { @@ -71,6 +174,29 @@ export interface SpanRecorder { const noopRecorder: SpanRecorder = { setOutputs() {} }; +/** + * Seed mlflow's global config on first use — AFTER `TelemetryManager.start()` + * has registered AppKit's provider. `init()` also stands up its own tracer + * provider and tries to register it globally, but that loses to AppKit's + * already-registered provider (non-fatal); we call it only for the config + * side-effect mlflow's span processor requires, then enable forwarding on the + * gated processor. Returns whether tracing is usable. + */ +function ensureConfigured(): boolean { + if (configured) return enabled; + configured = true; + if (!mlflow || !initConfig) return false; + try { + mlflow.init(initConfig); + gatedProcessor?.ready(); + return true; + } catch (err) { + enabled = false; + logger.warn("MLflow agent tracing disabled (init failed): %O", err); + return false; + } +} + /** * Run `fn` inside an MLflow span of `spanType` when tracing is enabled, * otherwise just run it (zero overhead). Spans auto-nest via the SDK's active @@ -86,9 +212,10 @@ async function trace( fn: (span: SpanRecorder) => Promise, ): Promise { if (!enabled || !mlflow) return fn(noopRecorder); - const type = - spanType === "AGENT" ? mlflow.SpanType.AGENT : mlflow.SpanType.TOOL; - return await mlflow.withSpan( + const m = mlflow; + if (!ensureConfigured()) return fn(noopRecorder); + const type = spanType === "AGENT" ? m.SpanType.AGENT : m.SpanType.TOOL; + return await m.withSpan( async (span) => { if (inputs !== undefined) span.setInputs(inputs); let outputsSet = false; diff --git a/packages/appkit/src/plugins/agents/tests/mlflow.test.ts b/packages/appkit/src/plugins/agents/tests/mlflow.test.ts index a43c29cec..8e8645fab 100644 --- a/packages/appkit/src/plugins/agents/tests/mlflow.test.ts +++ b/packages/appkit/src/plugins/agents/tests/mlflow.test.ts @@ -1,9 +1,12 @@ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; +import { GatedMlflowSpanProcessor } from "../mlflow"; + /** * The tracing module keeps module-level singleton state (`enabled`, - * `initStarted`) and lazily `import()`s `mlflow-tracing`. Each test resets the - * module registry and re-mocks the SDK so init runs fresh. + * `initStarted`) and lazily `import()`s `mlflow-tracing` (plus two deep-import + * paths for the span processor). Each test resets the module registry and + * re-mocks the SDK so init runs fresh. */ function stubSdk(overrides: Record = {}) { @@ -12,6 +15,7 @@ function stubSdk(overrides: Record = {}) { const span = { setInputs, setOutputs }; const sdk = { init: vi.fn(), + MlflowClient: class {}, SpanType: { AGENT: "AGENT", TOOL: "TOOL" }, withSpan: vi.fn(async (fn: (s: unknown) => unknown) => fn(span)), getCurrentActiveSpan: vi.fn(() => ({ traceId: "tr-active" })), @@ -22,6 +26,24 @@ function stubSdk(overrides: Record = {}) { ...overrides, }; vi.doMock("mlflow-tracing", () => sdk); + // Deep imports used by buildMlflowSpanProcessor — kept as light stubs so + // setup() wires a processor without touching real Databricks auth. + vi.doMock("mlflow-tracing/dist/auth", () => ({ + createAuthProvider: vi.fn(() => ({})), + })); + vi.doMock("mlflow-tracing/dist/exporters/mlflow", () => ({ + MlflowSpanExporter: class {}, + MlflowSpanProcessor: class { + onStart() {} + onEnd() {} + forceFlush() { + return Promise.resolve(); + } + shutdown() { + return Promise.resolve(); + } + }, + })); return { sdk, span, setInputs, setOutputs }; } @@ -33,6 +55,10 @@ describe("agent tracing (mlflow)", () => { afterEach(() => { vi.doUnmock("mlflow-tracing"); + vi.doUnmock("mlflow-tracing/dist/auth"); + vi.doUnmock("mlflow-tracing/dist/exporters/mlflow"); + vi.doUnmock("../../../telemetry"); + vi.restoreAllMocks(); delete process.env.MLFLOW_EXPERIMENT_ID; }); @@ -46,6 +72,48 @@ describe("agent tracing (mlflow)", () => { expect(mod.currentTraceId()).toBeUndefined(); }); + test("no experiment bound: contributes no span processor", async () => { + const { TelemetryManager } = await import("../../../telemetry"); + const spy = vi + .spyOn(TelemetryManager, "registerSpanProcessor") + .mockImplementation(() => {}); + + const mod = await import("../mlflow"); + await mod.initAgentTracing(); + + expect(spy).not.toHaveBeenCalled(); + }); + + test("experiment bound: contributes a span processor during setup", async () => { + process.env.MLFLOW_EXPERIMENT_ID = "exp-123"; + stubSdk(); + const { TelemetryManager } = await import("../../../telemetry"); + const spy = vi + .spyOn(TelemetryManager, "registerSpanProcessor") + .mockImplementation(() => {}); + + const mod = await import("../mlflow"); + await mod.initAgentTracing(); + + expect(spy).toHaveBeenCalledOnce(); + }); + + test("init() is deferred until first trace, not called during setup", async () => { + process.env.MLFLOW_EXPERIMENT_ID = "exp-123"; + const { sdk } = stubSdk(); + vi.doMock("../../../telemetry", () => ({ + TelemetryManager: { registerSpanProcessor: vi.fn() }, + })); + const mod = await import("../mlflow"); + + await mod.initAgentTracing(); + expect(sdk.init).not.toHaveBeenCalled(); // deferred + + await mod.traceAgent("agent", { messages: [] }, async () => {}); + expect(sdk.init).toHaveBeenCalledOnce(); // seeded lazily on first trace + vi.doUnmock("../../../telemetry"); + }); + test("currentTraceId reads the context-active span, not getLastActiveTraceId", async () => { process.env.MLFLOW_EXPERIMENT_ID = "exp-123"; const { sdk } = stubSdk(); @@ -87,4 +155,90 @@ describe("agent tracing (mlflow)", () => { content: "hi", }); }); + + // Tripwire: fails loudly if a mlflow-tracing version bump moves or renames the + // deep-imported internals buildMlflowSpanProcessor() relies on. Runs against + // the REAL package (no mocks); an OSS trackingUri needs no Databricks creds. + test("mlflow-tracing exposes the deep-imported symbols we construct", async () => { + const { createAuthProvider } = await import("mlflow-tracing/dist/auth"); + const { MlflowSpanExporter, MlflowSpanProcessor } = + await import("mlflow-tracing/dist/exporters/mlflow"); + const { MlflowClient } = await import("mlflow-tracing"); + + expect(typeof createAuthProvider).toBe("function"); + expect(typeof MlflowClient).toBe("function"); + expect(typeof MlflowSpanExporter).toBe("function"); + expect(typeof MlflowSpanProcessor).toBe("function"); + + const authProvider = createAuthProvider({ + trackingUri: "http://localhost:5000", + }); + const client = new MlflowClient({ + trackingUri: "http://localhost:5000", + authProvider, + }); + const processor = new MlflowSpanProcessor(new MlflowSpanExporter(client)); + for (const method of ["onStart", "onEnd", "forceFlush", "shutdown"]) { + expect(typeof (processor as any)[method]).toBe("function"); + } + }); +}); + +describe("GatedMlflowSpanProcessor", () => { + function fakeInner() { + return { + onStart: vi.fn(), + onEnd: vi.fn(), + forceFlush: vi.fn(() => Promise.resolve()), + shutdown: vi.fn(() => Promise.resolve()), + }; + } + + test("stays inert before ready(): no forwarding, so onStart can't throw on early spans", () => { + const inner = fakeInner(); + const gated = new GatedMlflowSpanProcessor(inner as any); + const span = { name: "early" }; + + gated.onStart(span as any, {} as any); + gated.onEnd(span as any); + + expect(inner.onStart).not.toHaveBeenCalled(); + expect(inner.onEnd).not.toHaveBeenCalled(); + }); + + test("forwards onStart/onEnd once ready()", () => { + const inner = fakeInner(); + const gated = new GatedMlflowSpanProcessor(inner as any); + gated.ready(); + const span = { name: "s" }; + + gated.onStart(span as any, {} as any); + gated.onEnd(span as any); + + expect(inner.onStart).toHaveBeenCalledOnce(); + expect(inner.onEnd).toHaveBeenCalledOnce(); + }); + + test("onEnd is skipped for spans whose onStart was not forwarded (balanced)", () => { + const inner = fakeInner(); + const gated = new GatedMlflowSpanProcessor(inner as any); + const early = { name: "early" }; + + gated.onStart(early as any, {} as any); // dropped (not ready) + gated.ready(); + gated.onEnd(early as any); // must NOT forward — inner never saw its start + + expect(inner.onEnd).not.toHaveBeenCalled(); + }); + + test("delegates forceFlush and shutdown to the inner processor", async () => { + const inner = fakeInner(); + const gated = new GatedMlflowSpanProcessor(inner as any); + + await gated.forceFlush(); + await gated.shutdown(); + + expect(inner.forceFlush).toHaveBeenCalledOnce(); + expect(inner.shutdown).toHaveBeenCalledOnce(); + }); }); From 3fe5459264353c289262bdc0bb23ed1050f7bccc Mon Sep 17 00:00:00 2001 From: MarioCadenas Date: Fri, 21 Aug 2026 17:00:40 +0200 Subject: [PATCH 3/9] fix(agents): keep mlflow tracing off the exporter loop and reliable from turn 1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mlflow span processor rides AppKit's shared OTel provider, so it saw every span — including the exporters' own outbound HTTP calls. Each trace upload became a new span to trace and upload: a feedback loop that flooded the experiment and could wedge the process. - Drop parentless CLIENT spans in the gated processor. An outgoing request made outside any agent turn (mlflow/OTLP shipping a trace) is exactly the loop's return edge; spans inside a real request tree keep their parent (or are the incoming SERVER root), so agent turns are untouched. - Seed mlflow's config on the "setup:complete" lifecycle event — after TelemetryManager.start(), before the server serves — instead of lazily on the first trace. The first turn's request-root span is then already forwarded, so that turn assembles into a trace instead of being dropped as a cold-start. Signed-off-by: MarioCadenas --- packages/appkit/src/plugins/agents/agents.ts | 7 ++ packages/appkit/src/plugins/agents/mlflow.ts | 27 ++++++++ .../agents/tests/agents-plugin.test.ts | 2 + .../src/plugins/agents/tests/mlflow.test.ts | 69 ++++++++++++++++--- 4 files changed, 97 insertions(+), 8 deletions(-) diff --git a/packages/appkit/src/plugins/agents/agents.ts b/packages/appkit/src/plugins/agents/agents.ts index 8ebbf32c3..adcf2e077 100644 --- a/packages/appkit/src/plugins/agents/agents.ts +++ b/packages/appkit/src/plugins/agents/agents.ts @@ -64,6 +64,7 @@ import { currentTraceId, initAgentTracing, linkTraceToRun, + startAgentTracing, traceAgent, } from "./mlflow"; import { composePromptForAgent } from "./prompt"; @@ -201,6 +202,12 @@ export class AgentsPlugin extends Plugin implements ToolProvider { async setup() { await initAgentTracing(); + // Seed mlflow's config right after TelemetryManager.start() (before the + // server serves), so the first turn's request-root span is forwarded and + // that turn assembles into a trace — not dropped as a cold-start artifact. + this.context?.onLifecycle("setup:complete", () => { + startAgentTracing(); + }); const { agents, defaultAgentName } = await this.buildAgentRegistry(); this.agents = agents; this.defaultAgentName = defaultAgentName; diff --git a/packages/appkit/src/plugins/agents/mlflow.ts b/packages/appkit/src/plugins/agents/mlflow.ts index b3a63530d..e1b790fac 100644 --- a/packages/appkit/src/plugins/agents/mlflow.ts +++ b/packages/appkit/src/plugins/agents/mlflow.ts @@ -1,3 +1,4 @@ +import { SpanKind } from "@opentelemetry/api"; import type { SpanProcessor } from "@opentelemetry/sdk-trace-base"; import { createLogger } from "../../logging/logger"; @@ -28,6 +29,11 @@ let gatedProcessor: GatedMlflowSpanProcessor | undefined; * `TelemetryManager.start()` and the first agent turn. We contribute this to * AppKit's single tracer provider during `setup()`, but only start forwarding * once `ready()` is called — right after the lazy `init()` in {@link ensureConfigured}. + * + * It also drops the exporters' own outbound spans (parentless CLIENT spans — + * outgoing requests made outside any agent turn, e.g. mlflow/OTLP shipping a + * trace). Forwarding those would loop: each upload is an HTTP call that + * auto-instrumentation turns into a new span to trace and upload. */ export class GatedMlflowSpanProcessor implements SpanProcessor { #inner: SpanProcessor; @@ -49,6 +55,15 @@ export class GatedMlflowSpanProcessor implements SpanProcessor { parentContext: Parameters[1], ): void { if (!this.#ready) return; + // Drop the exporters' own outbound calls. A parentless (root) CLIENT span is + // an outgoing request made outside any agent turn — e.g. mlflow or OTLP + // shipping a trace. Forwarding those would loop: each upload is itself an + // HTTP call that auto-instrumentation turns into a new span to trace and + // upload. Spans inside a real request tree keep their parent (or are the + // incoming SERVER root), so they still flow through. + if (span.kind === SpanKind.CLIENT && !span.parentSpanContext?.spanId) { + return; + } this.#forwarded.add(span); this.#inner.onStart(span, parentContext); } @@ -197,6 +212,18 @@ function ensureConfigured(): boolean { } } +/** + * Seed mlflow's config eagerly, right after `TelemetryManager.start()` — the + * agents plugin wires this to the `"setup:complete"` lifecycle event, before the + * server serves any request. Doing it here means the request's own root span is + * already forwarded when the first turn runs, so that turn assembles into a + * trace instead of being dropped (mlflow roots a trace only at the top-level + * span). Idempotent, and `trace()` still seeds lazily as a fallback. + */ +export function startAgentTracing(): void { + ensureConfigured(); +} + /** * Run `fn` inside an MLflow span of `spanType` when tracing is enabled, * otherwise just run it (zero overhead). Spans auto-nest via the SDK's active diff --git a/packages/appkit/src/plugins/agents/tests/agents-plugin.test.ts b/packages/appkit/src/plugins/agents/tests/agents-plugin.test.ts index e57225dd2..ee438c1d6 100644 --- a/packages/appkit/src/plugins/agents/tests/agents-plugin.test.ts +++ b/packages/appkit/src/plugins/agents/tests/agents-plugin.test.ts @@ -32,6 +32,7 @@ interface FakeContext { getToolProviders(): Array<{ name: string; provider: ToolProvider }>; getPluginNames(): string[]; addRoute(): void; + onLifecycle(): void; executeTool: ( req: unknown, pluginName: string, @@ -48,6 +49,7 @@ function fakeContext( getToolProviders: () => providers, getPluginNames: () => providers.map((p) => p.name), addRoute: vi.fn(), + onLifecycle: vi.fn(), executeTool: vi.fn(async (_req, p, n, args) => ({ plugin: p, tool: n, diff --git a/packages/appkit/src/plugins/agents/tests/mlflow.test.ts b/packages/appkit/src/plugins/agents/tests/mlflow.test.ts index 8e8645fab..04b02dd82 100644 --- a/packages/appkit/src/plugins/agents/tests/mlflow.test.ts +++ b/packages/appkit/src/plugins/agents/tests/mlflow.test.ts @@ -1,3 +1,4 @@ +import { SpanKind } from "@opentelemetry/api"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { GatedMlflowSpanProcessor } from "../mlflow"; @@ -114,6 +115,24 @@ describe("agent tracing (mlflow)", () => { vi.doUnmock("../../../telemetry"); }); + test("startAgentTracing seeds config eagerly, before any trace", async () => { + process.env.MLFLOW_EXPERIMENT_ID = "exp-123"; + const { sdk } = stubSdk(); + vi.doMock("../../../telemetry", () => ({ + TelemetryManager: { registerSpanProcessor: vi.fn() }, + })); + const mod = await import("../mlflow"); + + await mod.initAgentTracing(); + expect(sdk.init).not.toHaveBeenCalled(); // not during setup + + // Fired from the "setup:complete" lifecycle hook, after start(), before any + // request — so the first turn's root span is already forwarded. + mod.startAgentTracing(); + expect(sdk.init).toHaveBeenCalledOnce(); + vi.doUnmock("../../../telemetry"); + }); + test("currentTraceId reads the context-active span, not getLastActiveTraceId", async () => { process.env.MLFLOW_EXPERIMENT_ID = "exp-123"; const { sdk } = stubSdk(); @@ -194,10 +213,19 @@ describe("GatedMlflowSpanProcessor", () => { }; } + // Defaults to an in-turn child span (INTERNAL, has a parent) — the case we + // want forwarded. Override kind/parent for the edge cases. + const mkSpan = (over: Record = {}) => ({ + name: "s", + kind: SpanKind.INTERNAL, + parentSpanContext: { spanId: "parent" }, + ...over, + }); + test("stays inert before ready(): no forwarding, so onStart can't throw on early spans", () => { const inner = fakeInner(); const gated = new GatedMlflowSpanProcessor(inner as any); - const span = { name: "early" }; + const span = mkSpan(); gated.onStart(span as any, {} as any); gated.onEnd(span as any); @@ -206,23 +234,48 @@ describe("GatedMlflowSpanProcessor", () => { expect(inner.onEnd).not.toHaveBeenCalled(); }); - test("forwards onStart/onEnd once ready()", () => { + test("once ready(), forwards in-turn spans and the incoming request root", () => { const inner = fakeInner(); const gated = new GatedMlflowSpanProcessor(inner as any); gated.ready(); - const span = { name: "s" }; - gated.onStart(span as any, {} as any); - gated.onEnd(span as any); + const child = mkSpan(); // agent/tool span inside the turn + const requestRoot = mkSpan({ + kind: SpanKind.SERVER, + parentSpanContext: undefined, + }); // incoming /chat span — mlflow roots the trace here + const outgoingChild = mkSpan({ kind: SpanKind.CLIENT }); // LLM call under the agent + + for (const s of [child, requestRoot, outgoingChild]) { + gated.onStart(s as any, {} as any); + gated.onEnd(s as any); + } + + expect(inner.onStart).toHaveBeenCalledTimes(3); + expect(inner.onEnd).toHaveBeenCalledTimes(3); + }); + + test("drops the exporters' own outbound spans (parentless CLIENT) — breaks the loop", () => { + const inner = fakeInner(); + const gated = new GatedMlflowSpanProcessor(inner as any); + gated.ready(); + // An mlflow/OTLP upload: outgoing HTTP with no parent (made outside any turn). + const uploadSpan = mkSpan({ + kind: SpanKind.CLIENT, + parentSpanContext: undefined, + }); - expect(inner.onStart).toHaveBeenCalledOnce(); - expect(inner.onEnd).toHaveBeenCalledOnce(); + gated.onStart(uploadSpan as any, {} as any); + gated.onEnd(uploadSpan as any); + + expect(inner.onStart).not.toHaveBeenCalled(); + expect(inner.onEnd).not.toHaveBeenCalled(); }); test("onEnd is skipped for spans whose onStart was not forwarded (balanced)", () => { const inner = fakeInner(); const gated = new GatedMlflowSpanProcessor(inner as any); - const early = { name: "early" }; + const early = mkSpan(); gated.onStart(early as any, {} as any); // dropped (not ready) gated.ready(); From e7b7618cd535791669aa6c14eeb9495f9af15346 Mon Sep 17 00:00:00 2001 From: MarioCadenas Date: Tue, 1 Sep 2026 10:21:26 +0200 Subject: [PATCH 4/9] fix(agents): scope mlflow tracing to agent traces, not every request MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mlflow span processor rides AppKit's shared OTel provider, which carries every HTTP/DB span, and mlflow roots a trace at every parentless span. So once an experiment was bound, every request (analytics, genie, static assets) became an MLflow trace — a createTrace/uploadTraceData per request, plus non-agent payloads landing in a GenAI experiment. GatedMlflowSpanProcessor now decides at the root span's onEnd: export only if some span in the trace carried mlflow.spanType (an AGENT/TOOL span), otherwise popTrace to discard the in-memory trace mlflow built. Plain HTTP requests no longer reach the MLflow backend. Deep-imports InMemoryTraceManager.popTrace and SpanAttributeKey.SPAN_TYPE (pinned 0.1.3, tripwire-guarded). Hardening from review: - ensureConfigured() guards on gatedProcessor, so a processor-build failure can't let mlflow.init() register its own ungated global provider and win the empty slot (which would re-introduce the over-tracing + exporter loop). - #agentTraceIds is FIFO-bounded so an orphaned entry (root ends before or without its agent child) can't leak over process uptime. - Three telemetry-manager tests that passed against a NoOp meter or asserted only not.toThrow now assert their real invariant. Signed-off-by: MarioCadenas --- packages/appkit/src/plugins/agents/mlflow.ts | 153 +++++++++--- .../src/plugins/agents/tests/mlflow.test.ts | 228 ++++++++++++++++-- .../appkit/src/telemetry/telemetry-manager.ts | 3 +- .../telemetry/tests/telemetry-manager.test.ts | 24 +- 4 files changed, 354 insertions(+), 54 deletions(-) diff --git a/packages/appkit/src/plugins/agents/mlflow.ts b/packages/appkit/src/plugins/agents/mlflow.ts index e1b790fac..4e8a95d20 100644 --- a/packages/appkit/src/plugins/agents/mlflow.ts +++ b/packages/appkit/src/plugins/agents/mlflow.ts @@ -22,18 +22,35 @@ let initConfig: MlflowInitConfig | undefined; let gatedProcessor: GatedMlflowSpanProcessor | undefined; /** - * Wraps mlflow's OTel `SpanProcessor` so it stays inert until mlflow's global - * config is seeded. mlflow's `onStart` calls its own `getConfig()`, which THROWS - * before `init()` runs — and that throw propagates out of `tracer.startSpan()`, - * so it would break unrelated AppKit spans (HTTP, analytics) created between - * `TelemetryManager.start()` and the first agent turn. We contribute this to - * AppKit's single tracer provider during `setup()`, but only start forwarding - * once `ready()` is called — right after the lazy `init()` in {@link ensureConfigured}. + * Wraps mlflow's OTel `SpanProcessor` and scopes it to agent traces. Two jobs: * - * It also drops the exporters' own outbound spans (parentless CLIENT spans — - * outgoing requests made outside any agent turn, e.g. mlflow/OTLP shipping a - * trace). Forwarding those would loop: each upload is an HTTP call that - * auto-instrumentation turns into a new span to trace and upload. + * 1. Stay inert until ready. mlflow's `onStart` calls its own `getConfig()`, + * which THROWS before `init()` runs — and that throw propagates out of + * `tracer.startSpan()`, so it would break unrelated AppKit spans (HTTP, + * analytics) created between `TelemetryManager.start()` and the first agent + * turn. We contribute this to AppKit's single tracer provider during + * `setup()`, but only start forwarding once `ready()` is called — right after + * the `init()` in {@link ensureConfigured}. + * + * 2. Let only agent turns become MLflow traces. mlflow roots a trace at EVERY + * parentless span, and AppKit's single provider carries every HTTP/DB span — + * so unscoped, every request would become an MLflow trace. mlflow tags only + * its own spans (`mlflow.spanType`, set AFTER `onStart`), so the call is made + * at the root's `onEnd`: forward it (mlflow exports the trace) only if some + * span in the trace was an mlflow span; otherwise `popTrace` to discard the + * trace mlflow built in memory. Children end before their root, so the flag + * is set by the time the root decides. + * + * It also drops the exporters' own outbound spans at `onStart` (parentless + * CLIENT — outgoing requests made outside any agent turn, e.g. mlflow/OTLP + * shipping a trace). Forwarding those would loop: each upload is an HTTP call + * that auto-instrumentation turns into a new span to trace and upload. + * + * ponytail: non-agent requests still build (then discard) an in-memory trace + * tree — allocation-only, no network (export happens only when we forward the + * root's `onEnd`). Fine at normal QPS; if a very high-QPS app makes the churn + * matter, root MLflow at a detached agent span instead (costs the HTTP envelope + * on the trace and splits the OTLP trace). */ export class GatedMlflowSpanProcessor implements SpanProcessor { #inner: SpanProcessor; @@ -41,9 +58,26 @@ export class GatedMlflowSpanProcessor implements SpanProcessor { // Spans we forwarded `onStart` for, so `onEnd` stays balanced — mlflow never // sees an end without a matching start. #forwarded = new WeakSet(); + // OTel trace ids that contained at least one mlflow (AGENT/TOOL) span, so the + // root's `onEnd` exports rather than discards. Cleared as each root ends. + #agentTraceIds = new Set(); + #popTrace: (otelTraceId: string) => void; + #spanTypeKey: string; + // Leak backstop for #agentTraceIds — far above real concurrency. See onEnd. + #maxTracked: number; - constructor(inner: SpanProcessor) { + constructor( + inner: SpanProcessor, + deps: { + popTrace: (otelTraceId: string) => void; + spanTypeKey: string; + maxTracked?: number; + }, + ) { this.#inner = inner; + this.#popTrace = deps.popTrace; + this.#spanTypeKey = deps.spanTypeKey; + this.#maxTracked = deps.maxTracked ?? 1024; } ready(): void { @@ -59,8 +93,7 @@ export class GatedMlflowSpanProcessor implements SpanProcessor { // an outgoing request made outside any agent turn — e.g. mlflow or OTLP // shipping a trace. Forwarding those would loop: each upload is itself an // HTTP call that auto-instrumentation turns into a new span to trace and - // upload. Spans inside a real request tree keep their parent (or are the - // incoming SERVER root), so they still flow through. + // upload. if (span.kind === SpanKind.CLIENT && !span.parentSpanContext?.spanId) { return; } @@ -70,7 +103,40 @@ export class GatedMlflowSpanProcessor implements SpanProcessor { onEnd(span: Parameters[0]): void { if (!this.#forwarded.has(span)) return; - this.#inner.onEnd(span); + const traceId = span.spanContext().traceId; + // An mlflow span (AGENT/TOOL) ended in this trace — mark it for export. The + // `mlflow.spanType` attribute is only present after `onStart`, so this is the + // earliest we can see it. + if ( + span.attributes[this.#spanTypeKey] !== undefined && + !this.#agentTraceIds.has(traceId) + ) { + // Normally an entry lives only until its root's onEnd deletes it. But a + // root that ends BEFORE its agent child (streaming client-disconnect) or + // never ends (crash) orphans the entry, and #agentTraceIds — unlike the + // GC-safe #forwarded WeakSet — is keyed by string, so it can't self-clean. + // FIFO-evict at the cap so an abandoned-trace pattern can't grow it + // unboundedly over process uptime. + // ponytail: evicting a still-live trace only mis-discards it, and that + // needs >#maxTracked concurrent agent turns — far above real load. + if (this.#agentTraceIds.size >= this.#maxTracked) { + const oldest = this.#agentTraceIds.values().next().value; + if (oldest !== undefined) this.#agentTraceIds.delete(oldest); + } + this.#agentTraceIds.add(traceId); + } + if (span.parentSpanContext?.spanId) { + // Non-root: mlflow's own `onEnd` early-returns, but forward for balance. + this.#inner.onEnd(span); + return; + } + // Root span: export only agent traces; discard everything else so plain HTTP + // requests never become MLflow traces. `delete` reports whether it was agent. + if (this.#agentTraceIds.delete(traceId)) { + this.#inner.onEnd(span); + } else { + this.#popTrace(traceId); + } } forceFlush(): Promise { @@ -88,17 +154,28 @@ export class GatedMlflowSpanProcessor implements SpanProcessor { * single global provider (OTLP + this processor), so agent spans reach both * MLflow and any OTLP endpoint without two SDKs racing for the global slot. * + * Also resolves the two hooks {@link GatedMlflowSpanProcessor} needs to scope + * forwarding to agent traces — `popTrace` (to discard non-agent traces) and the + * `mlflow.spanType` attribute key (to recognize an mlflow span) — so the gate's + * `onEnd` stays synchronous and all mlflow-internal coupling resolves here. + * * Deep-imports `mlflow-tracing` internals that aren't on its public entrypoint — * pinned to the exact version in package.json and guarded by a test that fails * loudly if a version bump renames them. */ -async function buildMlflowSpanProcessor( - config: MlflowInitConfig, -): Promise { +async function buildMlflowSpanProcessor(config: MlflowInitConfig): Promise<{ + processor: SpanProcessor; + popTrace: (otelTraceId: string) => void; + spanTypeKey: string; +}> { const { createAuthProvider } = await import("mlflow-tracing/dist/auth"); const { MlflowClient } = await import("mlflow-tracing"); const { MlflowSpanExporter, MlflowSpanProcessor } = await import("mlflow-tracing/dist/exporters/mlflow"); + const { InMemoryTraceManager } = + await import("mlflow-tracing/dist/core/trace_manager"); + const { SpanAttributeKey } = + await import("mlflow-tracing/dist/core/constants"); const authProvider = createAuthProvider({ trackingUri: config.trackingUri, @@ -111,9 +188,15 @@ async function buildMlflowSpanProcessor( // mlflow builds against a different @opentelemetry/sdk-trace-base major than // AppKit; the SpanProcessor contract (onStart/onEnd/forceFlush/shutdown) is // stable across them, so bridge the nominal type mismatch with one cast. - return new MlflowSpanProcessor( + const processor = new MlflowSpanProcessor( new MlflowSpanExporter(client), ) as unknown as SpanProcessor; + return { + processor, + popTrace: (otelTraceId) => + InMemoryTraceManager.getInstance().popTrace(otelTraceId), + spanTypeKey: SpanAttributeKey.SPAN_TYPE, + }; } /** The bound MLflow experiment id, from the optional `experiment` resource. */ @@ -144,7 +227,8 @@ function normalizedDatabricksHost(): string | undefined { * provider (which would race AppKit's and drop one exporter), we build mlflow's * span processor ourselves and contribute it to AppKit's single provider via * {@link TelemetryManager.registerSpanProcessor}. mlflow's global config is - * seeded lazily in {@link ensureConfigured} on first trace, after `start()`. + * seeded by {@link startAgentTracing} on the `"setup:complete"` lifecycle event + * (after `start()`), with {@link ensureConfigured} as an idempotent lazy fallback. * * Auth is resolved by the `mlflow-tracing` SDK from the app's own Databricks * credentials — `DATABRICKS_HOST`/`DATABRICKS_TOKEN` or a `~/.databrickscfg` @@ -169,8 +253,12 @@ export async function initAgentTracing(): Promise { experimentId: id, ...(host ? { host } : {}), }; - const processor = await buildMlflowSpanProcessor(initConfig); - gatedProcessor = new GatedMlflowSpanProcessor(processor); + const { processor, popTrace, spanTypeKey } = + await buildMlflowSpanProcessor(initConfig); + gatedProcessor = new GatedMlflowSpanProcessor(processor, { + popTrace, + spanTypeKey, + }); TelemetryManager.registerSpanProcessor(gatedProcessor); enabled = true; logger.info("MLflow agent tracing enabled (experiment %s)", id); @@ -190,9 +278,11 @@ export interface SpanRecorder { const noopRecorder: SpanRecorder = { setOutputs() {} }; /** - * Seed mlflow's global config on first use — AFTER `TelemetryManager.start()` - * has registered AppKit's provider. `init()` also stands up its own tracer - * provider and tries to register it globally, but that loses to AppKit's + * Seed mlflow's global config once, AFTER `TelemetryManager.start()` has + * registered AppKit's provider — driven eagerly by {@link startAgentTracing} on + * `"setup:complete"`, or lazily by {@link trace} as a fallback. `init()` also + * stands up its own tracer provider and tries to register it globally, but that + * loses to AppKit's * already-registered provider (non-fatal); we call it only for the config * side-effect mlflow's span processor requires, then enable forwarding on the * gated processor. Returns whether tracing is usable. @@ -200,7 +290,13 @@ const noopRecorder: SpanRecorder = { setOutputs() {} }; function ensureConfigured(): boolean { if (configured) return enabled; configured = true; - if (!mlflow || !initConfig) return false; + // `gatedProcessor` guard is load-bearing: if buildMlflowSpanProcessor threw, + // `mlflow` and `initConfig` are still set but there is no gate. Calling + // `mlflow.init()` then would stand up mlflow's OWN ungated provider — and if + // AppKit registered none (no OTLP, no processor) it wins the global slot, + // routing every span into mlflow un-gated: the exact over-tracing + exporter + // loop this file exists to prevent. + if (!mlflow || !initConfig || !gatedProcessor) return false; try { mlflow.init(initConfig); gatedProcessor?.ready(); @@ -218,7 +314,10 @@ function ensureConfigured(): boolean { * server serves any request. Doing it here means the request's own root span is * already forwarded when the first turn runs, so that turn assembles into a * trace instead of being dropped (mlflow roots a trace only at the top-level - * span). Idempotent, and `trace()` still seeds lazily as a fallback. + * span). Idempotent. `trace()` also seeds lazily, but that only fully rescues a + * turn whose agent span is itself the trace root; an HTTP-wrapped first turn + * seeded lazily loses its root span (already started — and dropped — before + * `ready()`), so this eager path is the reliable one. */ export function startAgentTracing(): void { ensureConfigured(); diff --git a/packages/appkit/src/plugins/agents/tests/mlflow.test.ts b/packages/appkit/src/plugins/agents/tests/mlflow.test.ts index 04b02dd82..3d71bec58 100644 --- a/packages/appkit/src/plugins/agents/tests/mlflow.test.ts +++ b/packages/appkit/src/plugins/agents/tests/mlflow.test.ts @@ -45,6 +45,12 @@ function stubSdk(overrides: Record = {}) { } }, })); + vi.doMock("mlflow-tracing/dist/core/trace_manager", () => ({ + InMemoryTraceManager: { getInstance: () => ({ popTrace: vi.fn() }) }, + })); + vi.doMock("mlflow-tracing/dist/core/constants", () => ({ + SpanAttributeKey: { SPAN_TYPE: "mlflow.spanType" }, + })); return { sdk, span, setInputs, setOutputs }; } @@ -58,6 +64,8 @@ describe("agent tracing (mlflow)", () => { vi.doUnmock("mlflow-tracing"); vi.doUnmock("mlflow-tracing/dist/auth"); vi.doUnmock("mlflow-tracing/dist/exporters/mlflow"); + vi.doUnmock("mlflow-tracing/dist/core/trace_manager"); + vi.doUnmock("mlflow-tracing/dist/core/constants"); vi.doUnmock("../../../telemetry"); vi.restoreAllMocks(); delete process.env.MLFLOW_EXPERIMENT_ID; @@ -133,6 +141,30 @@ describe("agent tracing (mlflow)", () => { vi.doUnmock("../../../telemetry"); }); + test("processor build failure: never runs mlflow.init (no ungated global provider)", async () => { + process.env.MLFLOW_EXPERIMENT_ID = "exp-123"; + const { sdk } = stubSdk(); + // buildMlflowSpanProcessor throws (bad creds, or a renamed internal): the + // gate never registers, but `mlflow` and `initConfig` are already set. If + // ensureConfigured still called init(), mlflow would stand up its own + // ungated provider and (with no AppKit provider) win the global slot. + vi.doMock("mlflow-tracing/dist/auth", () => ({ + createAuthProvider: () => { + throw new Error("bad creds"); + }, + })); + vi.doMock("../../../telemetry", () => ({ + TelemetryManager: { registerSpanProcessor: vi.fn() }, + })); + const mod = await import("../mlflow"); + + await mod.initAgentTracing(); // build throws, swallowed + mod.startAgentTracing(); // "setup:complete" → ensureConfigured + + expect(sdk.init).not.toHaveBeenCalled(); + vi.doUnmock("../../../telemetry"); + }); + test("currentTraceId reads the context-active span, not getLastActiveTraceId", async () => { process.env.MLFLOW_EXPERIMENT_ID = "exp-123"; const { sdk } = stubSdk(); @@ -200,31 +232,50 @@ describe("agent tracing (mlflow)", () => { for (const method of ["onStart", "onEnd", "forceFlush", "shutdown"]) { expect(typeof (processor as any)[method]).toBe("function"); } + + // The gate also deep-imports popTrace (to discard non-agent traces) and the + // span-type attribute key (to recognize an mlflow span at onEnd). + const { InMemoryTraceManager } = + await import("mlflow-tracing/dist/core/trace_manager"); + const { SpanAttributeKey } = + await import("mlflow-tracing/dist/core/constants"); + expect(typeof InMemoryTraceManager.getInstance().popTrace).toBe("function"); + expect(SpanAttributeKey.SPAN_TYPE).toBe("mlflow.spanType"); }); }); describe("GatedMlflowSpanProcessor", () => { - function fakeInner() { - return { + const SPAN_TYPE_KEY = "mlflow.spanType"; + + function mkGated(maxTracked?: number) { + const inner = { onStart: vi.fn(), onEnd: vi.fn(), forceFlush: vi.fn(() => Promise.resolve()), shutdown: vi.fn(() => Promise.resolve()), }; + const popTrace = vi.fn(); + const gated = new GatedMlflowSpanProcessor(inner as any, { + popTrace, + spanTypeKey: SPAN_TYPE_KEY, + maxTracked, + }); + return { inner, popTrace, gated }; } - // Defaults to an in-turn child span (INTERNAL, has a parent) — the case we - // want forwarded. Override kind/parent for the edge cases. + // Defaults to an in-turn child span (INTERNAL, has a parent). Override + // kind/parentSpanContext/attributes/traceId for other cases. const mkSpan = (over: Record = {}) => ({ name: "s", kind: SpanKind.INTERNAL, parentSpanContext: { spanId: "parent" }, + attributes: {} as Record, + spanContext: () => ({ traceId: (over.traceId as string) ?? "tr-1" }), ...over, }); test("stays inert before ready(): no forwarding, so onStart can't throw on early spans", () => { - const inner = fakeInner(); - const gated = new GatedMlflowSpanProcessor(inner as any); + const { inner, gated } = mkGated(); const span = mkSpan(); gated.onStart(span as any, {} as any); @@ -234,35 +285,166 @@ describe("GatedMlflowSpanProcessor", () => { expect(inner.onEnd).not.toHaveBeenCalled(); }); - test("once ready(), forwards in-turn spans and the incoming request root", () => { - const inner = fakeInner(); - const gated = new GatedMlflowSpanProcessor(inner as any); + test("exports the trace when it contains an mlflow (agent) span", () => { + const { inner, popTrace, gated } = mkGated(); gated.ready(); - const child = mkSpan(); // agent/tool span inside the turn const requestRoot = mkSpan({ kind: SpanKind.SERVER, parentSpanContext: undefined, - }); // incoming /chat span — mlflow roots the trace here - const outgoingChild = mkSpan({ kind: SpanKind.CLIENT }); // LLM call under the agent + traceId: "tr-agent", + }); // incoming request — mlflow roots the trace here + const agentChild = mkSpan({ + attributes: { [SPAN_TYPE_KEY]: '"AGENT"' }, + traceId: "tr-agent", + }); + + // Real nesting order: root starts, child starts, child ends, root ends last. + gated.onStart(requestRoot as any, {} as any); + gated.onStart(agentChild as any, {} as any); + gated.onEnd(agentChild as any); + gated.onEnd(requestRoot as any); + + expect(inner.onStart).toHaveBeenCalledTimes(2); + expect(inner.onEnd).toHaveBeenCalledWith(requestRoot); // exported + expect(popTrace).not.toHaveBeenCalled(); + }); + + test("discards the trace when no agent span appears — a plain HTTP request never becomes an MLflow trace", () => { + const { inner, popTrace, gated } = mkGated(); + gated.ready(); - for (const s of [child, requestRoot, outgoingChild]) { - gated.onStart(s as any, {} as any); - gated.onEnd(s as any); + const requestRoot = mkSpan({ + kind: SpanKind.SERVER, + parentSpanContext: undefined, + traceId: "tr-plain", + }); // e.g. /api/analytics/query + const dbChild = mkSpan({ kind: SpanKind.CLIENT, traceId: "tr-plain" }); // SQL warehouse call — no mlflow.spanType + + gated.onStart(requestRoot as any, {} as any); + gated.onStart(dbChild as any, {} as any); + gated.onEnd(dbChild as any); + gated.onEnd(requestRoot as any); + + // Root discarded from mlflow, not exported. + expect(popTrace).toHaveBeenCalledExactlyOnceWith("tr-plain"); + expect(inner.onEnd).not.toHaveBeenCalledWith(requestRoot); + }); + + test("scopes per-trace under interleaved concurrent traffic (not a single flag)", () => { + const { inner, popTrace, gated } = mkGated(); + gated.ready(); + + const agentRoot = mkSpan({ + kind: SpanKind.SERVER, + parentSpanContext: undefined, + traceId: "tr-A", + }); + const agentChild = mkSpan({ + attributes: { [SPAN_TYPE_KEY]: '"AGENT"' }, + traceId: "tr-A", + }); + const plainRoot = mkSpan({ + kind: SpanKind.SERVER, + parentSpanContext: undefined, + traceId: "tr-B", + }); + const dbChild = mkSpan({ kind: SpanKind.CLIENT, traceId: "tr-B" }); + + // Both traces in flight; the plain one (tr-B) finishes first, the agent one last. + gated.onStart(agentRoot as any, {} as any); + gated.onStart(plainRoot as any, {} as any); + gated.onStart(agentChild as any, {} as any); + gated.onStart(dbChild as any, {} as any); + gated.onEnd(dbChild as any); + gated.onEnd(plainRoot as any); + gated.onEnd(agentChild as any); + gated.onEnd(agentRoot as any); + + // A single boolean 'sawAgentSpan' flag would cross-contaminate these. + expect(popTrace).toHaveBeenCalledExactlyOnceWith("tr-B"); + expect(inner.onEnd).toHaveBeenCalledWith(agentRoot); + expect(inner.onEnd).not.toHaveBeenCalledWith(plainRoot); + }); + + test("root ending before its agent child (streaming disconnect): discards that turn, stays balanced", () => { + const { inner, popTrace, gated } = mkGated(); + gated.ready(); + const root = mkSpan({ + kind: SpanKind.SERVER, + parentSpanContext: undefined, + traceId: "tr-race", + }); + const agentChild = mkSpan({ + attributes: { [SPAN_TYPE_KEY]: '"AGENT"' }, + traceId: "tr-race", + }); + + gated.onStart(root as any, {} as any); + gated.onStart(agentChild as any, {} as any); + // HTTP root ends before the agent span's finally runs; the late child must + // not throw. Documented tradeoff: the aborted turn's trace is discarded. + gated.onEnd(root as any); + gated.onEnd(agentChild as any); + + expect(popTrace).toHaveBeenCalledExactlyOnceWith("tr-race"); + expect(inner.onEnd).not.toHaveBeenCalledWith(root); + }); + + test("bounds #agentTraceIds so an abandoned-trace pattern can't leak unboundedly", () => { + const { popTrace, gated } = mkGated(2); // cap = 2 + gated.ready(); + + // Three agent children whose roots never end — orphans their trace ids. + for (const id of ["tr-1", "tr-2", "tr-3"]) { + const child = mkSpan({ + attributes: { [SPAN_TYPE_KEY]: '"AGENT"' }, + traceId: id, + }); + gated.onStart(child as any, {} as any); + gated.onEnd(child as any); } - expect(inner.onStart).toHaveBeenCalledTimes(3); - expect(inner.onEnd).toHaveBeenCalledTimes(3); + // tr-1 was FIFO-evicted at the cap, so if its root ever does arrive it's + // treated as non-agent (mis-discarded) rather than leaking forever. + const root1 = mkSpan({ + kind: SpanKind.SERVER, + parentSpanContext: undefined, + traceId: "tr-1", + }); + gated.onStart(root1 as any, {} as any); + gated.onEnd(root1 as any); + + expect(popTrace).toHaveBeenCalledWith("tr-1"); + }); + + test("agent span that is itself the root (no HTTP wrapper, e.g. eval/CLI) is exported", () => { + const { inner, popTrace, gated } = mkGated(); + gated.ready(); + // No SERVER wrapper: the AGENT span is the parentless root and marks itself, + // so the same onEnd both adds the traceId and hits the root branch. + const agentRoot = mkSpan({ + kind: SpanKind.INTERNAL, + parentSpanContext: undefined, + attributes: { [SPAN_TYPE_KEY]: '"AGENT"' }, + traceId: "tr-eval", + }); + + gated.onStart(agentRoot as any, {} as any); + gated.onEnd(agentRoot as any); + + expect(inner.onEnd).toHaveBeenCalledWith(agentRoot); // exported + expect(popTrace).not.toHaveBeenCalled(); }); test("drops the exporters' own outbound spans (parentless CLIENT) — breaks the loop", () => { - const inner = fakeInner(); - const gated = new GatedMlflowSpanProcessor(inner as any); + const { inner, popTrace, gated } = mkGated(); gated.ready(); // An mlflow/OTLP upload: outgoing HTTP with no parent (made outside any turn). const uploadSpan = mkSpan({ kind: SpanKind.CLIENT, parentSpanContext: undefined, + traceId: "tr-upload", }); gated.onStart(uploadSpan as any, {} as any); @@ -270,11 +452,11 @@ describe("GatedMlflowSpanProcessor", () => { expect(inner.onStart).not.toHaveBeenCalled(); expect(inner.onEnd).not.toHaveBeenCalled(); + expect(popTrace).not.toHaveBeenCalled(); // never registered, nothing to discard }); test("onEnd is skipped for spans whose onStart was not forwarded (balanced)", () => { - const inner = fakeInner(); - const gated = new GatedMlflowSpanProcessor(inner as any); + const { inner, popTrace, gated } = mkGated(); const early = mkSpan(); gated.onStart(early as any, {} as any); // dropped (not ready) @@ -282,11 +464,11 @@ describe("GatedMlflowSpanProcessor", () => { gated.onEnd(early as any); // must NOT forward — inner never saw its start expect(inner.onEnd).not.toHaveBeenCalled(); + expect(popTrace).not.toHaveBeenCalled(); }); test("delegates forceFlush and shutdown to the inner processor", async () => { - const inner = fakeInner(); - const gated = new GatedMlflowSpanProcessor(inner as any); + const { inner, gated } = mkGated(); await gated.forceFlush(); await gated.shutdown(); diff --git a/packages/appkit/src/telemetry/telemetry-manager.ts b/packages/appkit/src/telemetry/telemetry-manager.ts index a19cafd57..3d3815610 100644 --- a/packages/appkit/src/telemetry/telemetry-manager.ts +++ b/packages/appkit/src/telemetry/telemetry-manager.ts @@ -224,7 +224,8 @@ export class TelemetryManager { */ registerInstrumentations(instrumentations: Instrumentation[]): void { otelRegisterInstrumentations({ - // global providers set by NodeSDK.start() + // Instrumentations bind to the global providers registered by start() + // (tracer) and _initialize() (meter/logger). instrumentations, }); } diff --git a/packages/appkit/src/telemetry/tests/telemetry-manager.test.ts b/packages/appkit/src/telemetry/tests/telemetry-manager.test.ts index ffc506831..75453c877 100644 --- a/packages/appkit/src/telemetry/tests/telemetry-manager.test.ts +++ b/packages/appkit/src/telemetry/tests/telemetry-manager.test.ts @@ -270,15 +270,23 @@ describe("TelemetryManager", () => { expect(processor.startedSpans).toContain("contributed.span"); }); - test("start() is idempotent", () => { + test("start() is idempotent (no rebuild / double-attach)", async () => { process.env.OTEL_EXPORTER_OTLP_ENDPOINT = ""; const processor = recordingProcessor(); TelemetryManager.initialize({ serviceName: "idempotent" }); TelemetryManager.registerSpanProcessor(processor as any); TelemetryManager.start(); + TelemetryManager.start(); // second call must not rebuild or re-attach - expect(() => TelemetryManager.start()).not.toThrow(); + const tracer = + TelemetryManager.getProvider("idempotent-plugin").getTracer(); + await tracer.startActiveSpan("once.span", {}, async (span) => { + span.end(); + }); + + // A provider rebuilt on the second start() would double-record the span. + expect(processor.startedSpans).toEqual(["once.span"]); }); test("registerSpanProcessor after start() is ignored (not attached)", async () => { @@ -303,9 +311,13 @@ describe("TelemetryManager", () => { test("start() with no OTLP endpoint and no processors is a no-op", () => { process.env.OTEL_EXPORTER_OTLP_ENDPOINT = ""; + const before = trace.getTracerProvider(); TelemetryManager.initialize({ serviceName: "no-telemetry" }); + TelemetryManager.start(); - expect(() => TelemetryManager.start()).not.toThrow(); + // Nothing to export → the spanProcessors.length===0 early return means NO + // global tracer provider was registered (the API still returns the proxy). + expect(trace.getTracerProvider()).toBe(before); }); test("metrics obtained after initialize() (before start()) still record", () => { @@ -313,7 +325,13 @@ describe("TelemetryManager", () => { // initialize(), not start(), because OTel's metrics API has no lazy proxy. process.env.OTEL_EXPORTER_OTLP_ENDPOINT = "http://localhost:4318"; + const noop = metrics.getMeterProvider(); TelemetryManager.initialize({ serviceName: "eager-metrics" }); + // A REAL meter provider is registered by initialize(), before start() — + // not the NoOp. A `counter.add(...).not.toThrow()` assertion would pass + // against the NoOp meter too, so it can't detect a regression that moved + // registration into start(); this comparison can. + expect(metrics.getMeterProvider()).not.toBe(noop); // Instrument bound BEFORE start() — mirrors connector/cache constructors. const meter = TelemetryManager.getProvider("metrics-plugin").getMeter(); From 418c2e814cee13c29d74f6f394f89eaf5aa3f7a0 Mon Sep 17 00:00:00 2001 From: MarioCadenas Date: Wed, 2 Sep 2026 13:32:02 +0200 Subject: [PATCH 5/9] feat(agents): migrate mlflow tracing to @mlflow/core with Unity Catalog support Migrate agent tracing from mlflow-tracing@0.1.3 to @mlflow/core@0.4.0 and add Unity Catalog trace-location support, keeping AppKit's single-provider architecture from this branch. - Swap mlflow-tracing -> @mlflow/core@0.4.0 and declare @databricks/sdk-experimental@0.17.0 directly on appkit (matching shared) so the inlined workspace-client's hoist survives @mlflow/core's 0.15.0 pin, avoiding a runtime "Cannot find module '@databricks/sdk-experimental'". knip can't see that runtime require, so the dep is listed in its ignoreDependencies. - Resolve a UC trace location (MLFLOW_UC_CATALOG/SCHEMA/TABLE_PREFIX env override, else the experiment's databricksTraceDestinationPath tag for numeric ids) and build the UC processor/exporter when present, classic otherwise. Any failure falls back to classic so tracing never breaks the agent. The resolved mode is logged in the boot line. - Keep the single global OTel provider: the UC path reads no global config and never calls @mlflow/core's init() (no competing NodeSDK); only the classic processor needs init()'s config seed, whose provider loses the global race to AppKit's already-registered one, as before. - Move the symbols now public in @mlflow/core (createAuthProvider, MlflowClient, InMemoryTraceManager, SpanAttributeKey) off deep imports; only the exporter/processor classes remain deep-imported, guarded by the tripwire test. - Bound the gated processor's flush/shutdown so a stuck UC export can't hold graceful shutdown. Signed-off-by: MarioCadenas --- knip.json | 2 +- packages/appkit/package.json | 3 +- packages/appkit/src/plugins/agents/mlflow.ts | 247 +++++++--- .../src/plugins/agents/tests/mlflow.test.ts | 200 +++++--- pnpm-lock.yaml | 445 +++++++++++++++++- 5 files changed, 757 insertions(+), 140 deletions(-) diff --git a/knip.json b/knip.json index 1f3d29fa1..e605829f5 100644 --- a/knip.json +++ b/knip.json @@ -8,7 +8,7 @@ ], "workspaces": { "packages/appkit": { - "ignoreDependencies": ["vitest"] + "ignoreDependencies": ["vitest", "@databricks/sdk-experimental"] }, "packages/appkit-ui": { "ignoreDependencies": ["tailwindcss", "tw-animate-css"] diff --git a/packages/appkit/package.json b/packages/appkit/package.json index 6d82f31ee..a591ef9ea 100644 --- a/packages/appkit/package.json +++ b/packages/appkit/package.json @@ -71,6 +71,8 @@ "dependencies": { "@ast-grep/napi": "0.37.0", "@databricks/lakebase": "workspace:*", + "@databricks/sdk-experimental": "0.17.0", + "@mlflow/core": "0.4.0", "@opentelemetry/api": "1.9.0", "@opentelemetry/api-logs": "0.219.0", "@opentelemetry/auto-instrumentations-node": "0.77.0", @@ -95,7 +97,6 @@ "jiti": "2.6.1", "js-yaml": "4.3.1", "magic-string": "0.30.21", - "mlflow-tracing": "0.1.3", "obug": "2.1.1", "pg": "8.18.0", "picocolors": "1.1.1", diff --git a/packages/appkit/src/plugins/agents/mlflow.ts b/packages/appkit/src/plugins/agents/mlflow.ts index 4e8a95d20..48ae18e32 100644 --- a/packages/appkit/src/plugins/agents/mlflow.ts +++ b/packages/appkit/src/plugins/agents/mlflow.ts @@ -1,3 +1,4 @@ +import type { UnityCatalogLocation } from "@mlflow/core"; import { SpanKind } from "@opentelemetry/api"; import type { SpanProcessor } from "@opentelemetry/sdk-trace-base"; @@ -6,7 +7,8 @@ import { TelemetryManager } from "../../telemetry"; const logger = createLogger("agents"); -type MlflowModule = typeof import("mlflow-tracing"); +type MlflowModule = typeof import("@mlflow/core"); +type MlflowClientInstance = InstanceType; interface MlflowInitConfig { trackingUri: string; @@ -19,18 +21,21 @@ let enabled = false; let initStarted = false; let configured = false; let initConfig: MlflowInitConfig | undefined; +// The resolved UC trace location, or undefined for classic experiment storage. +let ucLocation: UnityCatalogLocation | undefined; let gatedProcessor: GatedMlflowSpanProcessor | undefined; /** * Wraps mlflow's OTel `SpanProcessor` and scopes it to agent traces. Two jobs: * - * 1. Stay inert until ready. mlflow's `onStart` calls its own `getConfig()`, - * which THROWS before `init()` runs — and that throw propagates out of - * `tracer.startSpan()`, so it would break unrelated AppKit spans (HTTP, - * analytics) created between `TelemetryManager.start()` and the first agent - * turn. We contribute this to AppKit's single tracer provider during - * `setup()`, but only start forwarding once `ready()` is called — right after - * the `init()` in {@link ensureConfigured}. + * 1. Stay inert until ready. The classic processor's `onStart` calls its own + * `getConfig()`, which THROWS before `init()` runs — and that throw + * propagates out of `tracer.startSpan()`, so it would break unrelated AppKit + * spans (HTTP, analytics) created between `TelemetryManager.start()` and the + * first agent turn. We contribute this to AppKit's single tracer provider + * during `setup()`, but only start forwarding once `ready()` is called by + * {@link ensureConfigured}. (The UC processor reads no global config and + * can't throw here, but forwarding is gated uniformly either way.) * * 2. Let only agent turns become MLflow traces. mlflow roots a trace at EVERY * parentless span, and AppKit's single provider carries every HTTP/DB span — @@ -65,6 +70,8 @@ export class GatedMlflowSpanProcessor implements SpanProcessor { #spanTypeKey: string; // Leak backstop for #agentTraceIds — far above real concurrency. See onEnd. #maxTracked: number; + // Cap on how long forceFlush/shutdown wait for a stuck export. + #flushTimeoutMs: number; constructor( inner: SpanProcessor, @@ -72,12 +79,14 @@ export class GatedMlflowSpanProcessor implements SpanProcessor { popTrace: (otelTraceId: string) => void; spanTypeKey: string; maxTracked?: number; + flushTimeoutMs?: number; }, ) { this.#inner = inner; this.#popTrace = deps.popTrace; this.#spanTypeKey = deps.spanTypeKey; this.#maxTracked = deps.maxTracked ?? 1024; + this.#flushTimeoutMs = deps.flushTimeoutMs ?? 5000; } ready(): void { @@ -140,62 +149,138 @@ export class GatedMlflowSpanProcessor implements SpanProcessor { } forceFlush(): Promise { - return this.#inner.forceFlush(); + return this.#boundedFlush(() => this.#inner.forceFlush()); } shutdown(): Promise { - return this.#inner.shutdown(); + return this.#boundedFlush(() => this.#inner.shutdown()); + } + + // Bound the inner flush/shutdown wait: both exporters export fire-and-forget, + // so a stuck export only wedges here (graceful shutdown), never a turn. + // Resolves — not rejects — on timeout: the caller is tearing down. + async #boundedFlush(op: () => Promise): Promise { + let timer: NodeJS.Timeout | undefined; + try { + await Promise.race([ + op().catch((err) => { + logger.warn("MLflow trace flush error: %O", err); + }), + new Promise((resolve) => { + timer = setTimeout(() => { + logger.warn( + "MLflow trace flush exceeded %dms; continuing (export may still be in flight)", + this.#flushTimeoutMs, + ); + resolve(); + }, this.#flushTimeoutMs); + timer.unref(); // don't keep the event loop alive on the timeout alone + }), + ]); + } finally { + if (timer) clearTimeout(timer); + } + } +} + +/** The Databricks experiment tag that links an experiment to a UC trace store. */ +const UC_DESTINATION_PATH_TAG = + "mlflow.experiment.databricksTraceDestinationPath"; + +/** + * Resolve the Unity Catalog trace location for the bound experiment, or + * `undefined` for classic experiment-backed storage. Any failure falls back to + * classic — a tracing misconfiguration must never break the agent. + * + * 1. Explicit env override — `MLFLOW_UC_CATALOG` + `MLFLOW_UC_SCHEMA` + + * `MLFLOW_UC_TABLE_PREFIX`, all three required. + * 2. Auto-detect from the linked Databricks experiment (numeric ids only, since + * `GetExperiment` only accepts them): read the + * `mlflow.experiment.databricksTraceDestinationPath` tag, whose value is + * `..`. + * 3. Otherwise classic. + * + * @mlflow/core ships its own tag parser (`core/destination`) but doesn't export + * it publicly, and its published `.d.ts` redacts the tag constant, so we read + * the tag directly here rather than deep-import a scrubbed internal. + */ +async function resolveUcLocation( + experimentId: string, + client: MlflowClientInstance, +): Promise { + const catalogName = process.env.MLFLOW_UC_CATALOG?.trim(); + const schemaName = process.env.MLFLOW_UC_SCHEMA?.trim(); + const tablePrefix = process.env.MLFLOW_UC_TABLE_PREFIX?.trim(); + if (catalogName && schemaName && tablePrefix) { + return { catalogName, schemaName, tablePrefix }; + } + + if (!/^\d+$/.test(experimentId)) return undefined; + + try { + const experiment = await client.getExperiment(experimentId); + const path = experiment?.tags?.[UC_DESTINATION_PATH_TAG]; + if (!path) return undefined; + // Must be exactly catalog.schema.prefix, all non-empty; else not a UC store. + const [cat, schema, prefix, ...rest] = path.split("."); + if (!cat || !schema || !prefix || rest.length > 0) return undefined; + return { catalogName: cat, schemaName: schema, tablePrefix: prefix }; + } catch (err) { + logger.warn( + "MLflow UC trace-location auto-detect failed; using classic experiment storage: %O", + err, + ); + return undefined; } } /** * Build mlflow's OTel `SpanProcessor` ourselves rather than letting `init()` - * build and globally register its own tracer provider. This lets AppKit own the - * single global provider (OTLP + this processor), so agent spans reach both - * MLflow and any OTLP endpoint without two SDKs racing for the global slot. + * build and globally register its own tracer provider (its own `NodeSDK`). This + * lets AppKit own the single global provider (OTLP + this processor), so agent + * spans reach both MLflow and any OTLP endpoint without two SDKs racing for the + * global slot. + * + * When a UC trace location is bound, builds the Unity Catalog processor + + * exporter (V4 trace ids, spans uploaded to the experiment's UC table); + * otherwise the classic experiment-backed processor. `createAuthProvider`, + * `MlflowClient`, `InMemoryTraceManager` and `SpanAttributeKey` are all public + * in `@mlflow/core`, so only the exporter/processor classes are deep-imported — + * pinned to the exact version in package.json and guarded by a test that fails + * loudly if a version bump renames them. * * Also resolves the two hooks {@link GatedMlflowSpanProcessor} needs to scope * forwarding to agent traces — `popTrace` (to discard non-agent traces) and the * `mlflow.spanType` attribute key (to recognize an mlflow span) — so the gate's - * `onEnd` stays synchronous and all mlflow-internal coupling resolves here. - * - * Deep-imports `mlflow-tracing` internals that aren't on its public entrypoint — - * pinned to the exact version in package.json and guarded by a test that fails - * loudly if a version bump renames them. + * `onEnd` stays synchronous. */ -async function buildMlflowSpanProcessor(config: MlflowInitConfig): Promise<{ +async function buildMlflowSpanProcessor( + m: MlflowModule, + client: MlflowClientInstance, + ucLoc: UnityCatalogLocation | undefined, +): Promise<{ processor: SpanProcessor; popTrace: (otelTraceId: string) => void; spanTypeKey: string; }> { - const { createAuthProvider } = await import("mlflow-tracing/dist/auth"); - const { MlflowClient } = await import("mlflow-tracing"); - const { MlflowSpanExporter, MlflowSpanProcessor } = - await import("mlflow-tracing/dist/exporters/mlflow"); - const { InMemoryTraceManager } = - await import("mlflow-tracing/dist/core/trace_manager"); - const { SpanAttributeKey } = - await import("mlflow-tracing/dist/core/constants"); - - const authProvider = createAuthProvider({ - trackingUri: config.trackingUri, - ...(config.host ? { host: config.host } : {}), - }); - const client = new MlflowClient({ - trackingUri: config.trackingUri, - authProvider, - }); - // mlflow builds against a different @opentelemetry/sdk-trace-base major than - // AppKit; the SpanProcessor contract (onStart/onEnd/forceFlush/shutdown) is - // stable across them, so bridge the nominal type mismatch with one cast. - const processor = new MlflowSpanProcessor( - new MlflowSpanExporter(client), - ) as unknown as SpanProcessor; + let processor: SpanProcessor; + if (ucLoc) { + const { DatabricksUCTableSpanExporter, DatabricksUCTableSpanProcessor } = + await import("@mlflow/core/dist/exporters/uc_table"); + processor = new DatabricksUCTableSpanProcessor( + new DatabricksUCTableSpanExporter(client), + ucLoc, + ); + } else { + const { MlflowSpanExporter, MlflowSpanProcessor } = + await import("@mlflow/core/dist/exporters/mlflow"); + processor = new MlflowSpanProcessor(new MlflowSpanExporter(client)); + } return { processor, popTrace: (otelTraceId) => - InMemoryTraceManager.getInstance().popTrace(otelTraceId), - spanTypeKey: SpanAttributeKey.SPAN_TYPE, + m.InMemoryTraceManager.getInstance().popTrace(otelTraceId), + spanTypeKey: m.SpanAttributeKey.SPAN_TYPE, }; } @@ -206,7 +291,7 @@ function experimentId(): string | undefined { } /** - * Databricks host with a scheme. The mlflow-tracing SDK uses `DATABRICKS_HOST` + * Databricks host with a scheme. `@mlflow/core` uses `DATABRICKS_HOST` * verbatim to build request URLs and doesn't add `https://`, so a bare host * (`workspace.cloud.databricks.com`) makes `new URL()` throw. Pass an explicit * normalized host when the env var is set; when it isn't (profile-based auth), @@ -223,14 +308,18 @@ function normalizedDatabricksHost(): string | undefined { * agents plugin's optional `experiment` resource is set (`MLFLOW_EXPERIMENT_ID`). * Called from the agents plugin's `setup()`, before `TelemetryManager.start()`. * - * Rather than let `mlflow.init()` stand up and globally register its own tracer - * provider (which would race AppKit's and drop one exporter), we build mlflow's + * Rather than let `@mlflow/core`'s `init()` stand up and globally register its + * own tracer provider (its `NodeSDK`, which would race AppKit's), we build the * span processor ourselves and contribute it to AppKit's single provider via - * {@link TelemetryManager.registerSpanProcessor}. mlflow's global config is - * seeded by {@link startAgentTracing} on the `"setup:complete"` lifecycle event - * (after `start()`), with {@link ensureConfigured} as an idempotent lazy fallback. + * {@link TelemetryManager.registerSpanProcessor}. For the classic + * experiment-backed processor, mlflow's global config is seeded by + * {@link startAgentTracing} on the `"setup:complete"` lifecycle event (after + * `start()`), with {@link ensureConfigured} as an idempotent lazy fallback; the + * UC processor needs no seeded config, so that path never calls `init()`. * - * Auth is resolved by the `mlflow-tracing` SDK from the app's own Databricks + * The trace store is resolved here: a UC table prefix (env-configured or + * auto-detected from the experiment's Databricks tags) vs. the classic + * experiment. Auth is resolved by `@mlflow/core` from the app's own Databricks * credentials — `DATABRICKS_HOST`/`DATABRICKS_TOKEN` or a `~/.databrickscfg` * profile (`MLFLOW_TRACKING_URI=databricks://profile`) — so no tokens or OTLP * headers are wired by hand. A failure (missing creds, bad experiment) logs and @@ -246,22 +335,45 @@ export async function initAgentTracing(): Promise { if (!id) return; try { - mlflow = await import("mlflow-tracing"); + mlflow = await import("@mlflow/core"); const host = normalizedDatabricksHost(); initConfig = { trackingUri: process.env.MLFLOW_TRACKING_URI?.trim() || "databricks", experimentId: id, ...(host ? { host } : {}), }; - const { processor, popTrace, spanTypeKey } = - await buildMlflowSpanProcessor(initConfig); + // One auth resolution + client, reused for UC auto-detect and the exporter. + const authProvider = mlflow.createAuthProvider({ + trackingUri: initConfig.trackingUri, + ...(host ? { host } : {}), + }); + const client = new mlflow.MlflowClient({ + trackingUri: initConfig.trackingUri, + authProvider, + }); + ucLocation = await resolveUcLocation(id, client); + const { processor, popTrace, spanTypeKey } = await buildMlflowSpanProcessor( + mlflow, + client, + ucLocation, + ); gatedProcessor = new GatedMlflowSpanProcessor(processor, { popTrace, spanTypeKey, }); TelemetryManager.registerSpanProcessor(gatedProcessor); enabled = true; - logger.info("MLflow agent tracing enabled (experiment %s)", id); + if (ucLocation) { + logger.info( + "MLflow agent tracing enabled (experiment %s, UC %s.%s.%s)", + id, + ucLocation.catalogName, + ucLocation.schemaName, + ucLocation.tablePrefix, + ); + } else { + logger.info("MLflow agent tracing enabled (experiment %s)", id); + } } catch (err) { logger.warn("MLflow agent tracing disabled: %O", err); } @@ -278,14 +390,19 @@ export interface SpanRecorder { const noopRecorder: SpanRecorder = { setOutputs() {} }; /** - * Seed mlflow's global config once, AFTER `TelemetryManager.start()` has - * registered AppKit's provider — driven eagerly by {@link startAgentTracing} on - * `"setup:complete"`, or lazily by {@link trace} as a fallback. `init()` also - * stands up its own tracer provider and tries to register it globally, but that - * loses to AppKit's - * already-registered provider (non-fatal); we call it only for the config - * side-effect mlflow's span processor requires, then enable forwarding on the - * gated processor. Returns whether tracing is usable. + * Seed the classic processor's global config once, AFTER + * `TelemetryManager.start()` has registered AppKit's provider — driven eagerly + * by {@link startAgentTracing} on `"setup:complete"`, or lazily by {@link trace} + * as a fallback — then enable forwarding on the gated processor. Returns whether + * tracing is usable. + * + * The classic experiment-backed `MlflowSpanProcessor` reads + * `getConfig().experimentId` in `onStart`, so it needs `init()` to seed mlflow's + * global config. `init()` also stands up its own `NodeSDK` whose provider loses + * the global slot to AppKit's already-registered one (non-fatal); we call it + * only for that config side-effect. The UC processor carries its location and + * reads no global config, so the UC path skips `init()` entirely — no second + * `NodeSDK`, no competing global registration by `@mlflow/core`. */ function ensureConfigured(): boolean { if (configured) return enabled; @@ -298,8 +415,8 @@ function ensureConfigured(): boolean { // loop this file exists to prevent. if (!mlflow || !initConfig || !gatedProcessor) return false; try { - mlflow.init(initConfig); - gatedProcessor?.ready(); + if (!ucLocation) mlflow.init(initConfig); + gatedProcessor.ready(); return true; } catch (err) { enabled = false; diff --git a/packages/appkit/src/plugins/agents/tests/mlflow.test.ts b/packages/appkit/src/plugins/agents/tests/mlflow.test.ts index 3d71bec58..7b977ca86 100644 --- a/packages/appkit/src/plugins/agents/tests/mlflow.test.ts +++ b/packages/appkit/src/plugins/agents/tests/mlflow.test.ts @@ -5,9 +5,9 @@ import { GatedMlflowSpanProcessor } from "../mlflow"; /** * The tracing module keeps module-level singleton state (`enabled`, - * `initStarted`) and lazily `import()`s `mlflow-tracing` (plus two deep-import - * paths for the span processor). Each test resets the module registry and - * re-mocks the SDK so init runs fresh. + * `initStarted`) and lazily `import()`s `@mlflow/core` (plus two deep-import + * paths for the exporter/processor classes). Each test resets the module + * registry and re-mocks the SDK so init runs fresh. */ function stubSdk(overrides: Record = {}) { @@ -16,7 +16,16 @@ function stubSdk(overrides: Record = {}) { const span = { setInputs, setOutputs }; const sdk = { init: vi.fn(), - MlflowClient: class {}, + // createAuthProvider, MlflowClient, InMemoryTraceManager and + // SpanAttributeKey are public on @mlflow/core's entrypoint (they were deep + // imports under mlflow-tracing 0.1.3), so they live on the main mock. + createAuthProvider: vi.fn(() => ({})), + MlflowClient: class { + // Auto-detect probes this; default: not a UC-backed experiment. + getExperiment = vi.fn(async () => null); + }, + InMemoryTraceManager: { getInstance: () => ({ popTrace: vi.fn() }) }, + SpanAttributeKey: { SPAN_TYPE: "mlflow.spanType" }, SpanType: { AGENT: "AGENT", TOOL: "TOOL" }, withSpan: vi.fn(async (fn: (s: unknown) => unknown) => fn(span)), getCurrentActiveSpan: vi.fn(() => ({ traceId: "tr-active" })), @@ -26,30 +35,28 @@ function stubSdk(overrides: Record = {}) { updateCurrentTrace: vi.fn(), ...overrides, }; - vi.doMock("mlflow-tracing", () => sdk); - // Deep imports used by buildMlflowSpanProcessor — kept as light stubs so - // setup() wires a processor without touching real Databricks auth. - vi.doMock("mlflow-tracing/dist/auth", () => ({ - createAuthProvider: vi.fn(() => ({})), - })); - vi.doMock("mlflow-tracing/dist/exporters/mlflow", () => ({ + vi.doMock("@mlflow/core", () => sdk); + // Deep imports used by buildMlflowSpanProcessor — the exporter/processor + // classes are the only symbols not on @mlflow/core's public entrypoint. Both + // modules stub the same SpanProcessor shape, so one class covers both; kept as + // light stubs so setup() wires a processor without touching Databricks. + class StubProcessor { + onStart() {} + onEnd() {} + forceFlush() { + return Promise.resolve(); + } + shutdown() { + return Promise.resolve(); + } + } + vi.doMock("@mlflow/core/dist/exporters/mlflow", () => ({ MlflowSpanExporter: class {}, - MlflowSpanProcessor: class { - onStart() {} - onEnd() {} - forceFlush() { - return Promise.resolve(); - } - shutdown() { - return Promise.resolve(); - } - }, + MlflowSpanProcessor: StubProcessor, })); - vi.doMock("mlflow-tracing/dist/core/trace_manager", () => ({ - InMemoryTraceManager: { getInstance: () => ({ popTrace: vi.fn() }) }, - })); - vi.doMock("mlflow-tracing/dist/core/constants", () => ({ - SpanAttributeKey: { SPAN_TYPE: "mlflow.spanType" }, + vi.doMock("@mlflow/core/dist/exporters/uc_table", () => ({ + DatabricksUCTableSpanExporter: class {}, + DatabricksUCTableSpanProcessor: StubProcessor, })); return { sdk, span, setInputs, setOutputs }; } @@ -58,17 +65,21 @@ describe("agent tracing (mlflow)", () => { beforeEach(() => { vi.resetModules(); delete process.env.MLFLOW_EXPERIMENT_ID; + delete process.env.MLFLOW_UC_CATALOG; + delete process.env.MLFLOW_UC_SCHEMA; + delete process.env.MLFLOW_UC_TABLE_PREFIX; }); afterEach(() => { - vi.doUnmock("mlflow-tracing"); - vi.doUnmock("mlflow-tracing/dist/auth"); - vi.doUnmock("mlflow-tracing/dist/exporters/mlflow"); - vi.doUnmock("mlflow-tracing/dist/core/trace_manager"); - vi.doUnmock("mlflow-tracing/dist/core/constants"); + vi.doUnmock("@mlflow/core"); + vi.doUnmock("@mlflow/core/dist/exporters/mlflow"); + vi.doUnmock("@mlflow/core/dist/exporters/uc_table"); vi.doUnmock("../../../telemetry"); vi.restoreAllMocks(); delete process.env.MLFLOW_EXPERIMENT_ID; + delete process.env.MLFLOW_UC_CATALOG; + delete process.env.MLFLOW_UC_SCHEMA; + delete process.env.MLFLOW_UC_TABLE_PREFIX; }); test("disabled (no experiment bound): still runs fn and returns its value", async () => { @@ -107,7 +118,7 @@ describe("agent tracing (mlflow)", () => { expect(spy).toHaveBeenCalledOnce(); }); - test("init() is deferred until first trace, not called during setup", async () => { + test("classic: init() is deferred until first trace, not called during setup", async () => { process.env.MLFLOW_EXPERIMENT_ID = "exp-123"; const { sdk } = stubSdk(); vi.doMock("../../../telemetry", () => ({ @@ -123,7 +134,7 @@ describe("agent tracing (mlflow)", () => { vi.doUnmock("../../../telemetry"); }); - test("startAgentTracing seeds config eagerly, before any trace", async () => { + test("classic: startAgentTracing seeds config eagerly, before any trace", async () => { process.env.MLFLOW_EXPERIMENT_ID = "exp-123"; const { sdk } = stubSdk(); vi.doMock("../../../telemetry", () => ({ @@ -141,18 +152,64 @@ describe("agent tracing (mlflow)", () => { vi.doUnmock("../../../telemetry"); }); - test("processor build failure: never runs mlflow.init (no ungated global provider)", async () => { + test("UC via env vars: contributes a processor and never calls init()", async () => { process.env.MLFLOW_EXPERIMENT_ID = "exp-123"; + process.env.MLFLOW_UC_CATALOG = "main"; + process.env.MLFLOW_UC_SCHEMA = "mario"; + process.env.MLFLOW_UC_TABLE_PREFIX = "exp-123"; const { sdk } = stubSdk(); + vi.doMock("../../../telemetry", () => ({ + TelemetryManager: { registerSpanProcessor: vi.fn() }, + })); + const mod = await import("../mlflow"); + + await mod.initAgentTracing(); + // The UC processor carries its location and reads no global config, so + // init() — which would stand up @mlflow/core's competing NodeSDK — is + // never called, not even eagerly on "setup:complete". + mod.startAgentTracing(); + expect(sdk.init).not.toHaveBeenCalled(); + vi.doUnmock("../../../telemetry"); + }); + + test("UC via experiment tag: auto-detects location and never calls init()", async () => { + process.env.MLFLOW_EXPERIMENT_ID = "123"; + const getExperiment = vi.fn(async () => ({ + experimentId: "123", + name: "n", + tags: { + "mlflow.experiment.databricksTraceDestinationPath": "main.mario.123", + }, + })); + const { sdk } = stubSdk({ + MlflowClient: class { + getExperiment = getExperiment; + }, + }); + vi.doMock("../../../telemetry", () => ({ + TelemetryManager: { registerSpanProcessor: vi.fn() }, + })); + const mod = await import("../mlflow"); + + await mod.initAgentTracing(); + mod.startAgentTracing(); + + expect(getExperiment).toHaveBeenCalledWith("123"); + expect(sdk.init).not.toHaveBeenCalled(); // UC path skips init() + vi.doUnmock("../../../telemetry"); + }); + + test("processor build failure: never runs mlflow.init (no ungated global provider)", async () => { + process.env.MLFLOW_EXPERIMENT_ID = "exp-123"; // buildMlflowSpanProcessor throws (bad creds, or a renamed internal): the // gate never registers, but `mlflow` and `initConfig` are already set. If // ensureConfigured still called init(), mlflow would stand up its own // ungated provider and (with no AppKit provider) win the global slot. - vi.doMock("mlflow-tracing/dist/auth", () => ({ + const { sdk } = stubSdk({ createAuthProvider: () => { throw new Error("bad creds"); }, - })); + }); vi.doMock("../../../telemetry", () => ({ TelemetryManager: { registerSpanProcessor: vi.fn() }, })); @@ -207,19 +264,30 @@ describe("agent tracing (mlflow)", () => { }); }); - // Tripwire: fails loudly if a mlflow-tracing version bump moves or renames the - // deep-imported internals buildMlflowSpanProcessor() relies on. Runs against + // Tripwire: fails loudly if a @mlflow/core version bump moves or renames the + // deep-imported exporter/processor classes buildMlflowSpanProcessor relies on, + // or moves the public symbols we now import from the entrypoint. Runs against // the REAL package (no mocks); an OSS trackingUri needs no Databricks creds. - test("mlflow-tracing exposes the deep-imported symbols we construct", async () => { - const { createAuthProvider } = await import("mlflow-tracing/dist/auth"); + test("@mlflow/core exposes the public + deep-imported symbols we construct", async () => { + const { + createAuthProvider, + MlflowClient, + InMemoryTraceManager, + SpanAttributeKey, + SpanType, + } = await import("@mlflow/core"); const { MlflowSpanExporter, MlflowSpanProcessor } = - await import("mlflow-tracing/dist/exporters/mlflow"); - const { MlflowClient } = await import("mlflow-tracing"); + await import("@mlflow/core/dist/exporters/mlflow"); + const { DatabricksUCTableSpanExporter, DatabricksUCTableSpanProcessor } = + await import("@mlflow/core/dist/exporters/uc_table"); + // Public entrypoint symbols (deep imports under mlflow-tracing 0.1.3). expect(typeof createAuthProvider).toBe("function"); expect(typeof MlflowClient).toBe("function"); - expect(typeof MlflowSpanExporter).toBe("function"); - expect(typeof MlflowSpanProcessor).toBe("function"); + expect(typeof InMemoryTraceManager.getInstance().popTrace).toBe("function"); + expect(SpanAttributeKey.SPAN_TYPE).toBe("mlflow.spanType"); + expect(SpanType.AGENT).toBe("AGENT"); + expect(SpanType.TOOL).toBe("TOOL"); const authProvider = createAuthProvider({ trackingUri: "http://localhost:5000", @@ -228,19 +296,22 @@ describe("agent tracing (mlflow)", () => { trackingUri: "http://localhost:5000", authProvider, }); - const processor = new MlflowSpanProcessor(new MlflowSpanExporter(client)); - for (const method of ["onStart", "onEnd", "forceFlush", "shutdown"]) { - expect(typeof (processor as any)[method]).toBe("function"); + + // Classic experiment-backed processor. + const classic = new MlflowSpanProcessor(new MlflowSpanExporter(client)); + // UC processor takes the exporter plus a UnityCatalogLocation. + const uc = new DatabricksUCTableSpanProcessor( + new DatabricksUCTableSpanExporter(client), + { catalogName: "c", schemaName: "s", tablePrefix: "p" }, + ); + for (const processor of [classic, uc]) { + for (const method of ["onStart", "onEnd", "forceFlush", "shutdown"]) { + expect(typeof (processor as any)[method]).toBe("function"); + } } - // The gate also deep-imports popTrace (to discard non-agent traces) and the - // span-type attribute key (to recognize an mlflow span at onEnd). - const { InMemoryTraceManager } = - await import("mlflow-tracing/dist/core/trace_manager"); - const { SpanAttributeKey } = - await import("mlflow-tracing/dist/core/constants"); - expect(typeof InMemoryTraceManager.getInstance().popTrace).toBe("function"); - expect(SpanAttributeKey.SPAN_TYPE).toBe("mlflow.spanType"); + // getExperiment backs UC trace-location auto-detect. + expect(typeof client.getExperiment).toBe("function"); }); }); @@ -476,4 +547,23 @@ describe("GatedMlflowSpanProcessor", () => { expect(inner.forceFlush).toHaveBeenCalledOnce(); expect(inner.shutdown).toHaveBeenCalledOnce(); }); + + test("bounds a stuck flush: resolves without hanging when the inner flush never settles", async () => { + const inner = { + onStart: vi.fn(), + onEnd: vi.fn(), + // Never resolves — simulates a wedged UC export. + forceFlush: vi.fn(() => new Promise(() => {})), + shutdown: vi.fn(() => new Promise(() => {})), + }; + const gated = new GatedMlflowSpanProcessor(inner as any, { + popTrace: vi.fn(), + spanTypeKey: SPAN_TYPE_KEY, + flushTimeoutMs: 5, + }); + + // Both resolve via the timeout rather than hanging on the inner promise. + await expect(gated.forceFlush()).resolves.toBeUndefined(); + await expect(gated.shutdown()).resolves.toBeUndefined(); + }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9d00135ea..4250716a9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -258,6 +258,12 @@ importers: '@databricks/lakebase': specifier: workspace:* version: link:../lakebase + '@databricks/sdk-experimental': + specifier: 0.17.0 + version: 0.17.0 + '@mlflow/core': + specifier: 0.4.0 + version: 0.4.0(bufferutil@4.0.9) '@opentelemetry/api': specifier: 1.9.0 version: 1.9.0 @@ -330,9 +336,6 @@ importers: magic-string: specifier: 0.30.21 version: 0.30.21 - mlflow-tracing: - specifier: 0.1.3 - version: 0.1.3 obug: specifier: 2.1.1 version: 2.1.1 @@ -2603,6 +2606,18 @@ packages: '@js-sdsl/ordered-map@4.4.2': resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} + '@jsep-plugin/assignment@1.3.0': + resolution: {integrity: sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==} + engines: {node: '>= 10.16.0'} + peerDependencies: + jsep: ^0.4.0||^1.0.0 + + '@jsep-plugin/regex@1.0.4': + resolution: {integrity: sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==} + engines: {node: '>= 10.16.0'} + peerDependencies: + jsep: ^0.4.0||^1.0.0 + '@jsonjoy.com/base64@1.1.2': resolution: {integrity: sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==} engines: {node: '>=10.0'} @@ -2642,6 +2657,9 @@ packages: '@keyv/serialize@1.1.1': resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} + '@kubernetes/client-node@1.4.0': + resolution: {integrity: sha512-Zge3YvF7DJi264dU1b3wb/GmzR99JhUpqTvp+VGHfwZT+g7EOOYNScDJNZwXy9cszyIGPIs0VHr+kk8e95qqrA==} + '@kwsites/file-exists@1.1.1': resolution: {integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==} @@ -2663,6 +2681,11 @@ packages: '@mermaid-js/parser@0.6.3': resolution: {integrity: sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==} + '@mlflow/core@0.4.0': + resolution: {integrity: sha512-z80wUaXWRsw5xquvpBW2BRJgjA7M+dj67JqR08i+cH8DZK+DV5y8RpaJyFzxGZkAXsy5GO4W+ndeL8tB5nlQaw==} + engines: {node: '>=18'} + hasBin: true + '@napi-rs/wasm-runtime@1.1.1': resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} @@ -5360,6 +5383,9 @@ packages: '@types/mysql@2.15.27': resolution: {integrity: sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA==} + '@types/node-fetch@2.6.13': + resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==} + '@types/node-forge@1.3.14': resolution: {integrity: sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==} @@ -5461,6 +5487,9 @@ packages: '@types/sockjs@0.3.36': resolution: {integrity: sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==} + '@types/stream-buffers@3.0.8': + resolution: {integrity: sha512-J+7VaHKNvlNPJPEJXX/fKa9DZtR/xPMwuIbe+yNOwp1YB+ApUOBv2aUpEoBJEi8nJgbgs1x8e73ttg0r1rSUdw==} + '@types/tedious@4.0.14': resolution: {integrity: sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==} @@ -5859,6 +5888,9 @@ packages: async-retry@1.3.3: resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + autocomplete.js@0.37.1: resolution: {integrity: sha512-PgSe9fHYhZEsm/9jggbjtVsGXJkPLvd+9mC7gZJ662vVL5CRWEtm/mIrrzCx0MrNxHVwxD5d00UOn6NsmL2LUQ==} @@ -5880,6 +5912,14 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} + b4a@1.8.1: + resolution: {integrity: sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==} + peerDependencies: + react-native-b4a: '*' + peerDependenciesMeta: + react-native-b4a: + optional: true + babel-loader@9.2.1: resolution: {integrity: sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA==} engines: {node: '>= 14.15.0'} @@ -5918,6 +5958,43 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} + bare-events@2.9.2: + resolution: {integrity: sha512-AIPKioV7/Y/8KfZ3AAhjPJxLLbY49S64Ym5DakZlUg75qQiTgUq9hEJoEwa4eUezPUlXRy/i5NpsKvo9jgKmoA==} + peerDependencies: + bare-abort-controller: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true + + bare-fs@4.8.1: + resolution: {integrity: sha512-N1nnXdHZAOSstz0XiHikGS4HGMH4CnSwhqWdGQQMqqdvp4Jybm9sE3R1WVnpWVd4SFkc8ryPDBLViNLwiEqECg==} + engines: {bare: '>=1.28.0'} + peerDependencies: + bare-buffer: '*' + peerDependenciesMeta: + bare-buffer: + optional: true + + bare-path@3.1.1: + resolution: {integrity: sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ==} + + bare-stream@2.13.4: + resolution: {integrity: sha512-PcrQ8lVLbiJscNm1Kez+Yp4Gy4AHGcN1lzwjvf5NybWen7VvEgUfyfnXYJ2zNqWnzOfCb1Abq6lH8ti0syQszA==} + peerDependencies: + bare-abort-controller: '*' + bare-buffer: '*' + bare-events: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true + bare-buffer: + optional: true + bare-events: + optional: true + + bare-url@2.5.2: + resolution: {integrity: sha512-L13PCJzKG8RGvx8V1/DdMi12ERhC3tprr7/8a94BxpmnRsFqxh5XZNdhtMxu5HPkRshYOOWRGY8lDP7ZhpG9Cg==} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -6293,6 +6370,10 @@ packages: resolution: {integrity: sha512-VcQB1ziGD0NXrhKxiwyNbCDmRzs/OShMs2GqW2DlU2A/Sd0nQxE1oWDAE5O0ygSx5mgQOn9eIFh7yKPgFRVkPQ==} engines: {node: '>=10'} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + comma-separated-tokens@1.0.8: resolution: {integrity: sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==} @@ -6924,6 +7005,10 @@ packages: delaunator@5.0.1: resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + depd@1.1.2: resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} engines: {node: '>= 0.6'} @@ -7276,6 +7361,10 @@ packages: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + esast-util-from-estree@2.0.0: resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==} @@ -7427,6 +7516,9 @@ packages: eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + events-universal@1.0.1: + resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} + events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} @@ -7474,6 +7566,9 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} @@ -7613,6 +7708,10 @@ packages: resolution: {integrity: sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw==} engines: {node: '>= 18'} + form-data@4.0.6: + resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} + engines: {node: '>= 6'} + format@0.2.2: resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} engines: {node: '>=0.4.x'} @@ -7885,6 +7984,10 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + hast-util-embedded@3.0.0: resolution: {integrity: sha512-naH8sld4Pe2ep03qqULEtvYr7EjrLK2QHY8KJR6RJkTUjPGObe1vnx585uzem2hGra+s1q08DZZpfgDVYRbaXA==} @@ -8012,6 +8115,10 @@ packages: hpack.js@2.1.6: resolution: {integrity: sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==} + hpagent@1.2.0: + resolution: {integrity: sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA==} + engines: {node: '>=14'} + html-encoding-sniffer@4.0.0: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} engines: {node: '>=18'} @@ -8455,6 +8562,11 @@ packages: resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} engines: {node: '>=0.10.0'} + isomorphic-ws@5.0.0: + resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} + peerDependencies: + ws: '*' + issue-parser@7.0.1: resolution: {integrity: sha512-3YZcUUR2Wt1WsapF+S/WiA2WmlW0cWAoPccMqne7AxEBhCdFeTPjfv/Axb8V2gyCgY3nRw+ksZ3xSUX+R47iAg==} engines: {node: ^18.17 || >=20.6.1} @@ -8505,6 +8617,9 @@ packages: joi@17.13.3: resolution: {integrity: sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==} + jose@6.2.10: + resolution: {integrity: sha512-iiW7J9qRFlGxvCOIBDBDxFePQSn7ZMAnrYGhrrOo6siO/MIqwfyilLR27pkfDgUk+raLuzADS8A3S/KLBisc0g==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -8528,6 +8643,10 @@ packages: canvas: optional: true + jsep@1.4.0: + resolution: {integrity: sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==} + engines: {node: '>= 10.16.0'} + jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -8581,6 +8700,11 @@ packages: resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} engines: {'0': node >= 0.2.0} + jsonpath-plus@10.4.0: + resolution: {integrity: sha512-T92WWatJXmhBbKsgH/0hl+jxjdXrifi5IKeMY02DWggRxX0UElcbVzPlmgLTbvsPeW1PasQ6xE2Q75stkhGbsA==} + engines: {node: '>=18.0.0'} + hasBin: true + just-diff-apply@5.5.0: resolution: {integrity: sha512-OYTthRfSh55WOItVqwpefPtNt2VdKsq5AnAK6apdtR6yCH8pr0CmSr710J0Mf+WdQy7K/OzMy7K2MgAfdQURDw==} @@ -9268,10 +9392,6 @@ packages: engines: {node: '>=10'} hasBin: true - mlflow-tracing@0.1.3: - resolution: {integrity: sha512-Koqkwaid5ubGHuLprBP6J7Su70WddlD11f2vgzgxbFFHYKsAsJatMGvjIck5CkyhT/gMUyBqpA3Lkl+zC3W3uQ==} - engines: {node: '>=18'} - mlly@1.8.0: resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} @@ -9369,6 +9489,15 @@ packages: node-fetch-native@1.6.7: resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + node-fetch@3.3.2: resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -9466,6 +9595,9 @@ packages: engines: {node: ^14.16.0 || >=16.10.0} hasBin: true + oauth4webapi@3.8.7: + resolution: {integrity: sha512-4RxcKxXjuItDFZ20RRPf4YTw3kpeXJyCgJFxVzJ068A7PNJ18st2Dg90tlC1LkSDS0GecroagCLHYEIVUhCAkw==} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -9530,6 +9662,9 @@ packages: resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} hasBin: true + openid-client@6.8.7: + resolution: {integrity: sha512-gtKthNu7evSBvTdrrlHb4F3Fi9dcwlb5QaITlCs+9mfpvuOi0Q3qtBf5+iY4sEP8hy1qCoAdxBNPDcmZeVSDzQ==} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -10665,6 +10800,9 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rfc4648@1.5.4: + resolution: {integrity: sha512-rRg/6Lb+IGfJqO05HZkN50UtY7K/JhxJag1kP23+zyMfrvoB0B7RWv06MbOzoc79RgCdNTiUaNsTT1AJZ7Z+cg==} + rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} @@ -11129,6 +11267,13 @@ packages: resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} engines: {node: '>=18'} + stream-buffers@3.0.3: + resolution: {integrity: sha512-pqMqwQCso0PBJt2PQmDO0cFj0lyqmiwOMiMSkVtRokl7e+ZTRYgDHKnuZNbqjiJXgsg4nuqtD/zxuo9KqTp0Yw==} + engines: {node: '>= 0.10.0'} + + streamx@2.28.1: + resolution: {integrity: sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA==} + string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} @@ -11275,14 +11420,23 @@ packages: tar-fs@2.1.4: resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} + tar-fs@3.1.3: + resolution: {integrity: sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==} + tar-stream@2.2.0: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} + tar-stream@3.2.1: + resolution: {integrity: sha512-nqsEO8zLZJvrOMdEwkA0QdCLFbetHMn95Zqu4fKwX+hkaTWJPZZOrxx/PwtxoK0MMGQmBQNRW3CPs8IFYQz4cQ==} + tar@7.5.10: resolution: {integrity: sha512-8mOPs1//5q/rlkNSPcCegA6hiHJYDmSLEI8aMH/CdSQJNWztHC9WHNam5zdQlfpTwB9Xp7IBEsHfV5LKMJGVAw==} engines: {node: '>=18'} + teex@1.0.1: + resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==} + terser-webpack-plugin@5.3.16: resolution: {integrity: sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==} engines: {node: '>= 10.13.0'} @@ -11308,6 +11462,9 @@ packages: resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} engines: {node: '>=18'} + text-decoder@1.2.7: + resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==} + text-extensions@2.4.0: resolution: {integrity: sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==} engines: {node: '>=8'} @@ -11397,6 +11554,9 @@ packages: resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} engines: {node: '>=16'} + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tr46@6.0.0: resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} engines: {node: '>=20'} @@ -12044,6 +12204,9 @@ packages: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + webidl-conversions@8.0.0: resolution: {integrity: sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==} engines: {node: '>=20'} @@ -12124,6 +12287,9 @@ packages: resolution: {integrity: sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==} engines: {node: '>=20'} + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + which-typed-array@1.1.20: resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} engines: {node: '>= 0.4'} @@ -15175,6 +15341,16 @@ snapshots: '@js-sdsl/ordered-map@4.4.2': {} + '@jsep-plugin/assignment@1.3.0(jsep@1.4.0)': + dependencies: + jsep: 1.4.0 + optional: true + + '@jsep-plugin/regex@1.0.4(jsep@1.4.0)': + dependencies: + jsep: 1.4.0 + optional: true + '@jsonjoy.com/base64@1.1.2(tslib@2.8.1)': dependencies: tslib: 2.8.1 @@ -15213,6 +15389,34 @@ snapshots: '@keyv/serialize@1.1.1': {} + '@kubernetes/client-node@1.4.0(bufferutil@4.0.9)': + dependencies: + '@types/js-yaml': 4.0.9 + '@types/node': 24.10.1 + '@types/node-fetch': 2.6.13 + '@types/stream-buffers': 3.0.8 + form-data: 4.0.6 + hpagent: 1.2.0 + isomorphic-ws: 5.0.0(ws@8.21.0(bufferutil@4.0.9)) + js-yaml: 4.2.0 + jsonpath-plus: 10.4.0 + node-fetch: 2.7.0 + openid-client: 6.8.7 + rfc4648: 1.5.4 + socks-proxy-agent: 8.0.5 + stream-buffers: 3.0.3 + tar-fs: 3.1.3 + ws: 8.21.0(bufferutil@4.0.9) + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - bufferutil + - encoding + - react-native-b4a + - supports-color + - utf-8-validate + optional: true + '@kwsites/file-exists@1.1.1': dependencies: debug: 4.4.3 @@ -15265,6 +15469,28 @@ snapshots: dependencies: langium: 3.3.1 + '@mlflow/core@0.4.0(bufferutil@4.0.9)': + dependencies: + '@databricks/sdk-experimental': 0.15.0 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/exporter-trace-otlp-proto': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-node': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.8.0(@opentelemetry/api@1.9.0) + bignumber.js: 9.3.1 + fast-safe-stringify: 2.1.1 + ini: 5.0.0 + optionalDependencies: + '@kubernetes/client-node': 1.4.0(bufferutil@4.0.9) + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - bufferutil + - encoding + - react-native-b4a + - supports-color + - utf-8-validate + '@napi-rs/wasm-runtime@1.1.1': dependencies: '@emnapi/core': 1.8.1 @@ -18007,6 +18233,12 @@ snapshots: dependencies: '@types/node': 25.2.3 + '@types/node-fetch@2.6.13': + dependencies: + '@types/node': 25.2.3 + form-data: 4.0.6 + optional: true + '@types/node-forge@1.3.14': dependencies: '@types/node': 25.2.3 @@ -18131,6 +18363,11 @@ snapshots: dependencies: '@types/node': 25.2.3 + '@types/stream-buffers@3.0.8': + dependencies: + '@types/node': 25.2.3 + optional: true + '@types/tedious@4.0.14': dependencies: '@types/node': 25.2.3 @@ -18636,6 +18873,9 @@ snapshots: dependencies: retry: 0.13.1 + asynckit@0.4.0: + optional: true + autocomplete.js@0.37.1: dependencies: immediate: 3.3.0 @@ -18663,6 +18903,9 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 + b4a@1.8.1: + optional: true + babel-loader@9.2.1(@babel/core@7.28.5)(webpack@5.103.0): dependencies: '@babel/core': 7.28.5 @@ -18706,6 +18949,40 @@ snapshots: balanced-match@4.0.4: {} + bare-events@2.9.2: + optional: true + + bare-fs@4.8.1: + dependencies: + bare-events: 2.9.2 + bare-path: 3.1.1 + bare-stream: 2.13.4(bare-events@2.9.2) + bare-url: 2.5.2 + fast-fifo: 1.3.2 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + optional: true + + bare-path@3.1.1: + optional: true + + bare-stream@2.13.4(bare-events@2.9.2): + dependencies: + b4a: 1.8.1 + streamx: 2.28.1 + teex: 1.0.1 + optionalDependencies: + bare-events: 2.9.2 + transitivePeerDependencies: + - react-native-b4a + optional: true + + bare-url@2.5.2: + dependencies: + bare-path: 3.1.1 + optional: true + base64-js@1.5.1: {} baseline-browser-mapping@2.8.32: {} @@ -19165,6 +19442,11 @@ snapshots: combine-promises@1.2.0: {} + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + optional: true + comma-separated-tokens@1.0.8: {} comma-separated-tokens@2.0.3: {} @@ -19824,6 +20106,9 @@ snapshots: dependencies: robust-predicates: 3.0.2 + delayed-stream@1.0.0: + optional: true + depd@1.1.2: {} depd@2.0.0: {} @@ -20066,6 +20351,14 @@ snapshots: dependencies: es-errors: 1.3.0 + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + optional: true + esast-util-from-estree@2.0.0: dependencies: '@types/estree-jsx': 1.0.5 @@ -20273,6 +20566,13 @@ snapshots: eventemitter3@5.0.1: {} + events-universal@1.0.1: + dependencies: + bare-events: 2.9.2 + transitivePeerDependencies: + - bare-abort-controller + optional: true + events@3.3.0: {} eventsource-parser@3.0.6: {} @@ -20357,6 +20657,9 @@ snapshots: fast-deep-equal@3.1.3: {} + fast-fifo@1.3.2: + optional: true + fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -20496,6 +20799,15 @@ snapshots: form-data-encoder@4.1.0: {} + form-data@4.0.6: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.4 + mime-types: 2.1.35 + optional: true + format@0.2.2: {} formatly@0.3.0: @@ -20816,6 +21128,11 @@ snapshots: dependencies: function-bind: 1.1.2 + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + optional: true + hast-util-embedded@3.0.0: dependencies: '@types/hast': 3.0.4 @@ -21113,6 +21430,9 @@ snapshots: readable-stream: 2.3.8 wbuf: 1.7.3 + hpagent@1.2.0: + optional: true + html-encoding-sniffer@4.0.0: dependencies: whatwg-encoding: 3.1.1 @@ -21490,6 +21810,11 @@ snapshots: isobject@3.0.1: {} + isomorphic-ws@5.0.0(ws@8.21.0(bufferutil@4.0.9)): + dependencies: + ws: 8.21.0(bufferutil@4.0.9) + optional: true + issue-parser@7.0.1: dependencies: lodash.capitalize: 4.2.1 @@ -21569,6 +21894,9 @@ snapshots: '@sideway/formula': 3.0.1 '@sideway/pinpoint': 2.0.0 + jose@6.2.10: + optional: true + js-tokens@4.0.0: {} js-tokens@9.0.1: {} @@ -21610,6 +21938,9 @@ snapshots: - supports-color - utf-8-validate + jsep@1.4.0: + optional: true + jsesc@3.1.0: {} json-bigint@1.0.0: @@ -21647,6 +21978,13 @@ snapshots: jsonparse@1.3.1: {} + jsonpath-plus@10.4.0: + dependencies: + '@jsep-plugin/assignment': 1.3.0(jsep@1.4.0) + '@jsep-plugin/regex': 1.0.4(jsep@1.4.0) + jsep: 1.4.0 + optional: true + just-diff-apply@5.5.0: {} just-diff@6.0.2: {} @@ -22615,17 +22953,6 @@ snapshots: mkdirp@3.0.1: {} - mlflow-tracing@0.1.3: - dependencies: - '@databricks/sdk-experimental': 0.15.0 - '@opentelemetry/api': 1.9.0 - '@opentelemetry/sdk-node': 0.205.0(@opentelemetry/api@1.9.0) - bignumber.js: 9.3.1 - fast-safe-stringify: 2.1.1 - ini: 5.0.0 - transitivePeerDependencies: - - supports-color - mlly@1.8.0: dependencies: acorn: 8.15.0 @@ -22707,6 +23034,11 @@ snapshots: node-fetch-native@1.6.7: {} + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + optional: true + node-fetch@3.3.2: dependencies: data-uri-to-buffer: 4.0.1 @@ -22811,6 +23143,9 @@ snapshots: pkg-types: 2.3.0 tinyexec: 1.0.2 + oauth4webapi@3.8.7: + optional: true + object-assign@4.1.1: {} object-inspect@1.13.4: {} @@ -22875,6 +23210,12 @@ snapshots: opener@1.5.2: {} + openid-client@6.8.7: + dependencies: + jose: 6.2.10 + oauth4webapi: 3.8.7 + optional: true + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -24244,6 +24585,9 @@ snapshots: reusify@1.1.0: {} + rfc4648@1.5.4: + optional: true + rfdc@1.4.1: {} rimraf@5.0.10: @@ -24807,6 +25151,19 @@ snapshots: stdin-discarder@0.2.2: {} + stream-buffers@3.0.3: + optional: true + + streamx@2.28.1: + dependencies: + events-universal: 1.0.1 + fast-fifo: 1.3.2 + text-decoder: 1.2.7 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + optional: true + string-argv@0.3.2: {} string-width@4.2.3: @@ -24956,6 +25313,19 @@ snapshots: tar-stream: 2.2.0 optional: true + tar-fs@3.1.3: + dependencies: + pump: 3.0.4 + tar-stream: 3.2.1 + optionalDependencies: + bare-fs: 4.8.1 + bare-path: 3.1.1 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + optional: true + tar-stream@2.2.0: dependencies: bl: 4.1.0 @@ -24965,6 +25335,18 @@ snapshots: readable-stream: 3.6.2 optional: true + tar-stream@3.2.1: + dependencies: + b4a: 1.8.1 + bare-fs: 4.8.1 + fast-fifo: 1.3.2 + streamx: 2.28.1 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + optional: true + tar@7.5.10: dependencies: '@isaacs/fs-minipass': 4.0.1 @@ -24973,6 +25355,14 @@ snapshots: minizlib: 3.1.0 yallist: 5.0.0 + teex@1.0.1: + dependencies: + streamx: 2.28.1 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + optional: true + terser-webpack-plugin@5.3.16(webpack@5.103.0): dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -24995,6 +25385,13 @@ snapshots: glob: 10.4.5 minimatch: 9.0.5 + text-decoder@1.2.7: + dependencies: + b4a: 1.8.1 + transitivePeerDependencies: + - react-native-b4a + optional: true + text-extensions@2.4.0: {} thingies@2.5.0(tslib@2.8.1): @@ -25061,6 +25458,9 @@ snapshots: dependencies: tldts: 7.0.17 + tr46@0.0.3: + optional: true + tr46@6.0.0: dependencies: punycode: 2.3.1 @@ -25744,6 +26144,9 @@ snapshots: web-streams-polyfill@3.3.3: {} + webidl-conversions@3.0.1: + optional: true + webidl-conversions@8.0.0: {} webpack-bundle-analyzer@4.10.2(bufferutil@4.0.9): @@ -25890,6 +26293,12 @@ snapshots: tr46: 6.0.0 webidl-conversions: 8.0.0 + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + optional: true + which-typed-array@1.1.20: dependencies: available-typed-arrays: 1.0.7 From 152db62d25e3eb8711e3e1e38aefeaf035e72601 Mon Sep 17 00:00:00 2001 From: MarioCadenas Date: Wed, 2 Sep 2026 15:31:51 +0200 Subject: [PATCH 6/9] chore(deps): regenerate lockfile after merging main Signed-off-by: MarioCadenas --- pnpm-lock.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4250716a9..5ee76abd4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15398,7 +15398,7 @@ snapshots: form-data: 4.0.6 hpagent: 1.2.0 isomorphic-ws: 5.0.0(ws@8.21.0(bufferutil@4.0.9)) - js-yaml: 4.2.0 + js-yaml: 4.3.1 jsonpath-plus: 10.4.0 node-fetch: 2.7.0 openid-client: 6.8.7 From 22309d0d77a7cef341b6c182db0d4c0e4401a92c Mon Sep 17 00:00:00 2001 From: MarioCadenas Date: Wed, 2 Sep 2026 16:11:21 +0200 Subject: [PATCH 7/9] chore(deps): pin @mlflow/core to 0.3.0 0.4.0 (released 2026-08-27) is inside the internal Artifactory registry's new-version quarantine window, so CI pnpm install --frozen-lockfile 403s on it. 0.3.0 (2026-07-07) is past the window and carries the same UC trace-location API (DatabricksUCTableSpanProcessor/Exporter, public createAuthProvider/ MlflowClient/InMemoryTraceManager, MlflowClient.getExperiment), so no code changes are needed. Signed-off-by: MarioCadenas --- packages/appkit/package.json | 2 +- pnpm-lock.yaml | 412 +---------------------------------- 2 files changed, 6 insertions(+), 408 deletions(-) diff --git a/packages/appkit/package.json b/packages/appkit/package.json index a591ef9ea..ec31d76ce 100644 --- a/packages/appkit/package.json +++ b/packages/appkit/package.json @@ -72,7 +72,7 @@ "@ast-grep/napi": "0.37.0", "@databricks/lakebase": "workspace:*", "@databricks/sdk-experimental": "0.17.0", - "@mlflow/core": "0.4.0", + "@mlflow/core": "0.3.0", "@opentelemetry/api": "1.9.0", "@opentelemetry/api-logs": "0.219.0", "@opentelemetry/auto-instrumentations-node": "0.77.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5ee76abd4..f70801806 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -262,8 +262,8 @@ importers: specifier: 0.17.0 version: 0.17.0 '@mlflow/core': - specifier: 0.4.0 - version: 0.4.0(bufferutil@4.0.9) + specifier: 0.3.0 + version: 0.3.0 '@opentelemetry/api': specifier: 1.9.0 version: 1.9.0 @@ -2606,18 +2606,6 @@ packages: '@js-sdsl/ordered-map@4.4.2': resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} - '@jsep-plugin/assignment@1.3.0': - resolution: {integrity: sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==} - engines: {node: '>= 10.16.0'} - peerDependencies: - jsep: ^0.4.0||^1.0.0 - - '@jsep-plugin/regex@1.0.4': - resolution: {integrity: sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==} - engines: {node: '>= 10.16.0'} - peerDependencies: - jsep: ^0.4.0||^1.0.0 - '@jsonjoy.com/base64@1.1.2': resolution: {integrity: sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==} engines: {node: '>=10.0'} @@ -2657,9 +2645,6 @@ packages: '@keyv/serialize@1.1.1': resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} - '@kubernetes/client-node@1.4.0': - resolution: {integrity: sha512-Zge3YvF7DJi264dU1b3wb/GmzR99JhUpqTvp+VGHfwZT+g7EOOYNScDJNZwXy9cszyIGPIs0VHr+kk8e95qqrA==} - '@kwsites/file-exists@1.1.1': resolution: {integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==} @@ -2681,8 +2666,8 @@ packages: '@mermaid-js/parser@0.6.3': resolution: {integrity: sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==} - '@mlflow/core@0.4.0': - resolution: {integrity: sha512-z80wUaXWRsw5xquvpBW2BRJgjA7M+dj67JqR08i+cH8DZK+DV5y8RpaJyFzxGZkAXsy5GO4W+ndeL8tB5nlQaw==} + '@mlflow/core@0.3.0': + resolution: {integrity: sha512-KfLwQv9wvgA9eo0H/S+1JRrfb1+kv1L0rWenSaxQS+RlCJ8hEg3uc/sai0SdNB4SOqkkJNBM5WMD02iI22walg==} engines: {node: '>=18'} hasBin: true @@ -5383,9 +5368,6 @@ packages: '@types/mysql@2.15.27': resolution: {integrity: sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA==} - '@types/node-fetch@2.6.13': - resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==} - '@types/node-forge@1.3.14': resolution: {integrity: sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==} @@ -5487,9 +5469,6 @@ packages: '@types/sockjs@0.3.36': resolution: {integrity: sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==} - '@types/stream-buffers@3.0.8': - resolution: {integrity: sha512-J+7VaHKNvlNPJPEJXX/fKa9DZtR/xPMwuIbe+yNOwp1YB+ApUOBv2aUpEoBJEi8nJgbgs1x8e73ttg0r1rSUdw==} - '@types/tedious@4.0.14': resolution: {integrity: sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==} @@ -5888,9 +5867,6 @@ packages: async-retry@1.3.3: resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==} - asynckit@0.4.0: - resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - autocomplete.js@0.37.1: resolution: {integrity: sha512-PgSe9fHYhZEsm/9jggbjtVsGXJkPLvd+9mC7gZJ662vVL5CRWEtm/mIrrzCx0MrNxHVwxD5d00UOn6NsmL2LUQ==} @@ -5912,14 +5888,6 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} - b4a@1.8.1: - resolution: {integrity: sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==} - peerDependencies: - react-native-b4a: '*' - peerDependenciesMeta: - react-native-b4a: - optional: true - babel-loader@9.2.1: resolution: {integrity: sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA==} engines: {node: '>= 14.15.0'} @@ -5958,43 +5926,6 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} - bare-events@2.9.2: - resolution: {integrity: sha512-AIPKioV7/Y/8KfZ3AAhjPJxLLbY49S64Ym5DakZlUg75qQiTgUq9hEJoEwa4eUezPUlXRy/i5NpsKvo9jgKmoA==} - peerDependencies: - bare-abort-controller: '*' - peerDependenciesMeta: - bare-abort-controller: - optional: true - - bare-fs@4.8.1: - resolution: {integrity: sha512-N1nnXdHZAOSstz0XiHikGS4HGMH4CnSwhqWdGQQMqqdvp4Jybm9sE3R1WVnpWVd4SFkc8ryPDBLViNLwiEqECg==} - engines: {bare: '>=1.28.0'} - peerDependencies: - bare-buffer: '*' - peerDependenciesMeta: - bare-buffer: - optional: true - - bare-path@3.1.1: - resolution: {integrity: sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ==} - - bare-stream@2.13.4: - resolution: {integrity: sha512-PcrQ8lVLbiJscNm1Kez+Yp4Gy4AHGcN1lzwjvf5NybWen7VvEgUfyfnXYJ2zNqWnzOfCb1Abq6lH8ti0syQszA==} - peerDependencies: - bare-abort-controller: '*' - bare-buffer: '*' - bare-events: '*' - peerDependenciesMeta: - bare-abort-controller: - optional: true - bare-buffer: - optional: true - bare-events: - optional: true - - bare-url@2.5.2: - resolution: {integrity: sha512-L13PCJzKG8RGvx8V1/DdMi12ERhC3tprr7/8a94BxpmnRsFqxh5XZNdhtMxu5HPkRshYOOWRGY8lDP7ZhpG9Cg==} - base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -6370,10 +6301,6 @@ packages: resolution: {integrity: sha512-VcQB1ziGD0NXrhKxiwyNbCDmRzs/OShMs2GqW2DlU2A/Sd0nQxE1oWDAE5O0ygSx5mgQOn9eIFh7yKPgFRVkPQ==} engines: {node: '>=10'} - combined-stream@1.0.8: - resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} - engines: {node: '>= 0.8'} - comma-separated-tokens@1.0.8: resolution: {integrity: sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==} @@ -7005,10 +6932,6 @@ packages: delaunator@5.0.1: resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==} - delayed-stream@1.0.0: - resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} - engines: {node: '>=0.4.0'} - depd@1.1.2: resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} engines: {node: '>= 0.6'} @@ -7361,10 +7284,6 @@ packages: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} - es-set-tostringtag@2.1.0: - resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} - engines: {node: '>= 0.4'} - esast-util-from-estree@2.0.0: resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==} @@ -7516,9 +7435,6 @@ packages: eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} - events-universal@1.0.1: - resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} - events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} @@ -7566,9 +7482,6 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - fast-fifo@1.3.2: - resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} - fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} @@ -7708,10 +7621,6 @@ packages: resolution: {integrity: sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw==} engines: {node: '>= 18'} - form-data@4.0.6: - resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} - engines: {node: '>= 6'} - format@0.2.2: resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} engines: {node: '>=0.4.x'} @@ -7984,10 +7893,6 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} - hasown@2.0.4: - resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} - engines: {node: '>= 0.4'} - hast-util-embedded@3.0.0: resolution: {integrity: sha512-naH8sld4Pe2ep03qqULEtvYr7EjrLK2QHY8KJR6RJkTUjPGObe1vnx585uzem2hGra+s1q08DZZpfgDVYRbaXA==} @@ -8115,10 +8020,6 @@ packages: hpack.js@2.1.6: resolution: {integrity: sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==} - hpagent@1.2.0: - resolution: {integrity: sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA==} - engines: {node: '>=14'} - html-encoding-sniffer@4.0.0: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} engines: {node: '>=18'} @@ -8562,11 +8463,6 @@ packages: resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} engines: {node: '>=0.10.0'} - isomorphic-ws@5.0.0: - resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} - peerDependencies: - ws: '*' - issue-parser@7.0.1: resolution: {integrity: sha512-3YZcUUR2Wt1WsapF+S/WiA2WmlW0cWAoPccMqne7AxEBhCdFeTPjfv/Axb8V2gyCgY3nRw+ksZ3xSUX+R47iAg==} engines: {node: ^18.17 || >=20.6.1} @@ -8617,9 +8513,6 @@ packages: joi@17.13.3: resolution: {integrity: sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==} - jose@6.2.10: - resolution: {integrity: sha512-iiW7J9qRFlGxvCOIBDBDxFePQSn7ZMAnrYGhrrOo6siO/MIqwfyilLR27pkfDgUk+raLuzADS8A3S/KLBisc0g==} - js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -8643,10 +8536,6 @@ packages: canvas: optional: true - jsep@1.4.0: - resolution: {integrity: sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==} - engines: {node: '>= 10.16.0'} - jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -8700,11 +8589,6 @@ packages: resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} engines: {'0': node >= 0.2.0} - jsonpath-plus@10.4.0: - resolution: {integrity: sha512-T92WWatJXmhBbKsgH/0hl+jxjdXrifi5IKeMY02DWggRxX0UElcbVzPlmgLTbvsPeW1PasQ6xE2Q75stkhGbsA==} - engines: {node: '>=18.0.0'} - hasBin: true - just-diff-apply@5.5.0: resolution: {integrity: sha512-OYTthRfSh55WOItVqwpefPtNt2VdKsq5AnAK6apdtR6yCH8pr0CmSr710J0Mf+WdQy7K/OzMy7K2MgAfdQURDw==} @@ -9489,15 +9373,6 @@ packages: node-fetch-native@1.6.7: resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} - node-fetch@2.7.0: - resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} - engines: {node: 4.x || >=6.0.0} - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - node-fetch@3.3.2: resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -9595,9 +9470,6 @@ packages: engines: {node: ^14.16.0 || >=16.10.0} hasBin: true - oauth4webapi@3.8.7: - resolution: {integrity: sha512-4RxcKxXjuItDFZ20RRPf4YTw3kpeXJyCgJFxVzJ068A7PNJ18st2Dg90tlC1LkSDS0GecroagCLHYEIVUhCAkw==} - object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -9662,9 +9534,6 @@ packages: resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} hasBin: true - openid-client@6.8.7: - resolution: {integrity: sha512-gtKthNu7evSBvTdrrlHb4F3Fi9dcwlb5QaITlCs+9mfpvuOi0Q3qtBf5+iY4sEP8hy1qCoAdxBNPDcmZeVSDzQ==} - optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -10800,9 +10669,6 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rfc4648@1.5.4: - resolution: {integrity: sha512-rRg/6Lb+IGfJqO05HZkN50UtY7K/JhxJag1kP23+zyMfrvoB0B7RWv06MbOzoc79RgCdNTiUaNsTT1AJZ7Z+cg==} - rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} @@ -11267,13 +11133,6 @@ packages: resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} engines: {node: '>=18'} - stream-buffers@3.0.3: - resolution: {integrity: sha512-pqMqwQCso0PBJt2PQmDO0cFj0lyqmiwOMiMSkVtRokl7e+ZTRYgDHKnuZNbqjiJXgsg4nuqtD/zxuo9KqTp0Yw==} - engines: {node: '>= 0.10.0'} - - streamx@2.28.1: - resolution: {integrity: sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA==} - string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} @@ -11420,23 +11279,14 @@ packages: tar-fs@2.1.4: resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} - tar-fs@3.1.3: - resolution: {integrity: sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==} - tar-stream@2.2.0: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} - tar-stream@3.2.1: - resolution: {integrity: sha512-nqsEO8zLZJvrOMdEwkA0QdCLFbetHMn95Zqu4fKwX+hkaTWJPZZOrxx/PwtxoK0MMGQmBQNRW3CPs8IFYQz4cQ==} - tar@7.5.10: resolution: {integrity: sha512-8mOPs1//5q/rlkNSPcCegA6hiHJYDmSLEI8aMH/CdSQJNWztHC9WHNam5zdQlfpTwB9Xp7IBEsHfV5LKMJGVAw==} engines: {node: '>=18'} - teex@1.0.1: - resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==} - terser-webpack-plugin@5.3.16: resolution: {integrity: sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==} engines: {node: '>= 10.13.0'} @@ -11462,9 +11312,6 @@ packages: resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} engines: {node: '>=18'} - text-decoder@1.2.7: - resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==} - text-extensions@2.4.0: resolution: {integrity: sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==} engines: {node: '>=8'} @@ -11554,9 +11401,6 @@ packages: resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} engines: {node: '>=16'} - tr46@0.0.3: - resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - tr46@6.0.0: resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} engines: {node: '>=20'} @@ -12204,9 +12048,6 @@ packages: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} - webidl-conversions@3.0.1: - resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - webidl-conversions@8.0.0: resolution: {integrity: sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==} engines: {node: '>=20'} @@ -12287,9 +12128,6 @@ packages: resolution: {integrity: sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==} engines: {node: '>=20'} - whatwg-url@5.0.0: - resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} - which-typed-array@1.1.20: resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} engines: {node: '>= 0.4'} @@ -15341,16 +15179,6 @@ snapshots: '@js-sdsl/ordered-map@4.4.2': {} - '@jsep-plugin/assignment@1.3.0(jsep@1.4.0)': - dependencies: - jsep: 1.4.0 - optional: true - - '@jsep-plugin/regex@1.0.4(jsep@1.4.0)': - dependencies: - jsep: 1.4.0 - optional: true - '@jsonjoy.com/base64@1.1.2(tslib@2.8.1)': dependencies: tslib: 2.8.1 @@ -15389,34 +15217,6 @@ snapshots: '@keyv/serialize@1.1.1': {} - '@kubernetes/client-node@1.4.0(bufferutil@4.0.9)': - dependencies: - '@types/js-yaml': 4.0.9 - '@types/node': 24.10.1 - '@types/node-fetch': 2.6.13 - '@types/stream-buffers': 3.0.8 - form-data: 4.0.6 - hpagent: 1.2.0 - isomorphic-ws: 5.0.0(ws@8.21.0(bufferutil@4.0.9)) - js-yaml: 4.3.1 - jsonpath-plus: 10.4.0 - node-fetch: 2.7.0 - openid-client: 6.8.7 - rfc4648: 1.5.4 - socks-proxy-agent: 8.0.5 - stream-buffers: 3.0.3 - tar-fs: 3.1.3 - ws: 8.21.0(bufferutil@4.0.9) - transitivePeerDependencies: - - bare-abort-controller - - bare-buffer - - bufferutil - - encoding - - react-native-b4a - - supports-color - - utf-8-validate - optional: true - '@kwsites/file-exists@1.1.1': dependencies: debug: 4.4.3 @@ -15469,7 +15269,7 @@ snapshots: dependencies: langium: 3.3.1 - '@mlflow/core@0.4.0(bufferutil@4.0.9)': + '@mlflow/core@0.3.0': dependencies: '@databricks/sdk-experimental': 0.15.0 '@opentelemetry/api': 1.9.0 @@ -15480,16 +15280,8 @@ snapshots: bignumber.js: 9.3.1 fast-safe-stringify: 2.1.1 ini: 5.0.0 - optionalDependencies: - '@kubernetes/client-node': 1.4.0(bufferutil@4.0.9) transitivePeerDependencies: - - bare-abort-controller - - bare-buffer - - bufferutil - - encoding - - react-native-b4a - supports-color - - utf-8-validate '@napi-rs/wasm-runtime@1.1.1': dependencies: @@ -18233,12 +18025,6 @@ snapshots: dependencies: '@types/node': 25.2.3 - '@types/node-fetch@2.6.13': - dependencies: - '@types/node': 25.2.3 - form-data: 4.0.6 - optional: true - '@types/node-forge@1.3.14': dependencies: '@types/node': 25.2.3 @@ -18363,11 +18149,6 @@ snapshots: dependencies: '@types/node': 25.2.3 - '@types/stream-buffers@3.0.8': - dependencies: - '@types/node': 25.2.3 - optional: true - '@types/tedious@4.0.14': dependencies: '@types/node': 25.2.3 @@ -18873,9 +18654,6 @@ snapshots: dependencies: retry: 0.13.1 - asynckit@0.4.0: - optional: true - autocomplete.js@0.37.1: dependencies: immediate: 3.3.0 @@ -18903,9 +18681,6 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 - b4a@1.8.1: - optional: true - babel-loader@9.2.1(@babel/core@7.28.5)(webpack@5.103.0): dependencies: '@babel/core': 7.28.5 @@ -18949,40 +18724,6 @@ snapshots: balanced-match@4.0.4: {} - bare-events@2.9.2: - optional: true - - bare-fs@4.8.1: - dependencies: - bare-events: 2.9.2 - bare-path: 3.1.1 - bare-stream: 2.13.4(bare-events@2.9.2) - bare-url: 2.5.2 - fast-fifo: 1.3.2 - transitivePeerDependencies: - - bare-abort-controller - - react-native-b4a - optional: true - - bare-path@3.1.1: - optional: true - - bare-stream@2.13.4(bare-events@2.9.2): - dependencies: - b4a: 1.8.1 - streamx: 2.28.1 - teex: 1.0.1 - optionalDependencies: - bare-events: 2.9.2 - transitivePeerDependencies: - - react-native-b4a - optional: true - - bare-url@2.5.2: - dependencies: - bare-path: 3.1.1 - optional: true - base64-js@1.5.1: {} baseline-browser-mapping@2.8.32: {} @@ -19442,11 +19183,6 @@ snapshots: combine-promises@1.2.0: {} - combined-stream@1.0.8: - dependencies: - delayed-stream: 1.0.0 - optional: true - comma-separated-tokens@1.0.8: {} comma-separated-tokens@2.0.3: {} @@ -20106,9 +19842,6 @@ snapshots: dependencies: robust-predicates: 3.0.2 - delayed-stream@1.0.0: - optional: true - depd@1.1.2: {} depd@2.0.0: {} @@ -20351,14 +20084,6 @@ snapshots: dependencies: es-errors: 1.3.0 - es-set-tostringtag@2.1.0: - dependencies: - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - has-tostringtag: 1.0.2 - hasown: 2.0.4 - optional: true - esast-util-from-estree@2.0.0: dependencies: '@types/estree-jsx': 1.0.5 @@ -20566,13 +20291,6 @@ snapshots: eventemitter3@5.0.1: {} - events-universal@1.0.1: - dependencies: - bare-events: 2.9.2 - transitivePeerDependencies: - - bare-abort-controller - optional: true - events@3.3.0: {} eventsource-parser@3.0.6: {} @@ -20657,9 +20375,6 @@ snapshots: fast-deep-equal@3.1.3: {} - fast-fifo@1.3.2: - optional: true - fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -20799,15 +20514,6 @@ snapshots: form-data-encoder@4.1.0: {} - form-data@4.0.6: - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - es-set-tostringtag: 2.1.0 - hasown: 2.0.4 - mime-types: 2.1.35 - optional: true - format@0.2.2: {} formatly@0.3.0: @@ -21128,11 +20834,6 @@ snapshots: dependencies: function-bind: 1.1.2 - hasown@2.0.4: - dependencies: - function-bind: 1.1.2 - optional: true - hast-util-embedded@3.0.0: dependencies: '@types/hast': 3.0.4 @@ -21430,9 +21131,6 @@ snapshots: readable-stream: 2.3.8 wbuf: 1.7.3 - hpagent@1.2.0: - optional: true - html-encoding-sniffer@4.0.0: dependencies: whatwg-encoding: 3.1.1 @@ -21810,11 +21508,6 @@ snapshots: isobject@3.0.1: {} - isomorphic-ws@5.0.0(ws@8.21.0(bufferutil@4.0.9)): - dependencies: - ws: 8.21.0(bufferutil@4.0.9) - optional: true - issue-parser@7.0.1: dependencies: lodash.capitalize: 4.2.1 @@ -21894,9 +21587,6 @@ snapshots: '@sideway/formula': 3.0.1 '@sideway/pinpoint': 2.0.0 - jose@6.2.10: - optional: true - js-tokens@4.0.0: {} js-tokens@9.0.1: {} @@ -21938,9 +21628,6 @@ snapshots: - supports-color - utf-8-validate - jsep@1.4.0: - optional: true - jsesc@3.1.0: {} json-bigint@1.0.0: @@ -21978,13 +21665,6 @@ snapshots: jsonparse@1.3.1: {} - jsonpath-plus@10.4.0: - dependencies: - '@jsep-plugin/assignment': 1.3.0(jsep@1.4.0) - '@jsep-plugin/regex': 1.0.4(jsep@1.4.0) - jsep: 1.4.0 - optional: true - just-diff-apply@5.5.0: {} just-diff@6.0.2: {} @@ -23034,11 +22714,6 @@ snapshots: node-fetch-native@1.6.7: {} - node-fetch@2.7.0: - dependencies: - whatwg-url: 5.0.0 - optional: true - node-fetch@3.3.2: dependencies: data-uri-to-buffer: 4.0.1 @@ -23143,9 +22818,6 @@ snapshots: pkg-types: 2.3.0 tinyexec: 1.0.2 - oauth4webapi@3.8.7: - optional: true - object-assign@4.1.1: {} object-inspect@1.13.4: {} @@ -23210,12 +22882,6 @@ snapshots: opener@1.5.2: {} - openid-client@6.8.7: - dependencies: - jose: 6.2.10 - oauth4webapi: 3.8.7 - optional: true - optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -24585,9 +24251,6 @@ snapshots: reusify@1.1.0: {} - rfc4648@1.5.4: - optional: true - rfdc@1.4.1: {} rimraf@5.0.10: @@ -25151,19 +24814,6 @@ snapshots: stdin-discarder@0.2.2: {} - stream-buffers@3.0.3: - optional: true - - streamx@2.28.1: - dependencies: - events-universal: 1.0.1 - fast-fifo: 1.3.2 - text-decoder: 1.2.7 - transitivePeerDependencies: - - bare-abort-controller - - react-native-b4a - optional: true - string-argv@0.3.2: {} string-width@4.2.3: @@ -25313,19 +24963,6 @@ snapshots: tar-stream: 2.2.0 optional: true - tar-fs@3.1.3: - dependencies: - pump: 3.0.4 - tar-stream: 3.2.1 - optionalDependencies: - bare-fs: 4.8.1 - bare-path: 3.1.1 - transitivePeerDependencies: - - bare-abort-controller - - bare-buffer - - react-native-b4a - optional: true - tar-stream@2.2.0: dependencies: bl: 4.1.0 @@ -25335,18 +24972,6 @@ snapshots: readable-stream: 3.6.2 optional: true - tar-stream@3.2.1: - dependencies: - b4a: 1.8.1 - bare-fs: 4.8.1 - fast-fifo: 1.3.2 - streamx: 2.28.1 - transitivePeerDependencies: - - bare-abort-controller - - bare-buffer - - react-native-b4a - optional: true - tar@7.5.10: dependencies: '@isaacs/fs-minipass': 4.0.1 @@ -25355,14 +24980,6 @@ snapshots: minizlib: 3.1.0 yallist: 5.0.0 - teex@1.0.1: - dependencies: - streamx: 2.28.1 - transitivePeerDependencies: - - bare-abort-controller - - react-native-b4a - optional: true - terser-webpack-plugin@5.3.16(webpack@5.103.0): dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -25385,13 +25002,6 @@ snapshots: glob: 10.4.5 minimatch: 9.0.5 - text-decoder@1.2.7: - dependencies: - b4a: 1.8.1 - transitivePeerDependencies: - - react-native-b4a - optional: true - text-extensions@2.4.0: {} thingies@2.5.0(tslib@2.8.1): @@ -25458,9 +25068,6 @@ snapshots: dependencies: tldts: 7.0.17 - tr46@0.0.3: - optional: true - tr46@6.0.0: dependencies: punycode: 2.3.1 @@ -26144,9 +25751,6 @@ snapshots: web-streams-polyfill@3.3.3: {} - webidl-conversions@3.0.1: - optional: true - webidl-conversions@8.0.0: {} webpack-bundle-analyzer@4.10.2(bufferutil@4.0.9): @@ -26293,12 +25897,6 @@ snapshots: tr46: 6.0.0 webidl-conversions: 8.0.0 - whatwg-url@5.0.0: - dependencies: - tr46: 0.0.3 - webidl-conversions: 3.0.1 - optional: true - which-typed-array@1.1.20: dependencies: available-typed-arrays: 1.0.7 From 45459ed12b49bd9175b55cab261d6b16cc372c55 Mon Sep 17 00:00:00 2001 From: MarioCadenas Date: Wed, 2 Sep 2026 18:32:02 +0200 Subject: [PATCH 8/9] fix(agents): scope mlflow export to AGENT/TOOL spans, not every request MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The gate decided 'agent trace, export it' on whether mlflow.spanType was present. But @mlflow/core's processor stamps mlflow.spanType on EVERY span it processes (createAndRegisterMlflowSpan defaults to SpanType.UNKNOWN), so the SERVER root of every recorded request carried the attribute — and every non-agent API request (e.g. POST /api/analytics/query) was exported as an MLflow trace, the exact over-tracing this branch exists to prevent. Discriminate on the AGENT/TOOL value instead of mere presence; buildMlflowSpanProcessor now returns the accepted JSON-stringified values. Also switch UC auto-detect to mlflow's own ucLocationFromExperimentTags (deep-imported like the exporter classes) instead of a hand-rolled tag split: it preserves backend-populated span/log storage-table names for custom- provisioned UC locations, and its .d.ts is not redacted in 0.3.0. Tests: regression for the UNKNOWN-stamped plain request; resolver branches (no-tag/getExperiment-failure/non-numeric-skip → classic; env + tag → UC with the location asserted on the processor); tripwire now covers the tracing entrypoints and ucLocationFromExperimentTags against the real package. Signed-off-by: MarioCadenas --- packages/appkit/src/plugins/agents/mlflow.ts | 79 ++++--- .../src/plugins/agents/tests/mlflow.test.ts | 200 +++++++++++++++--- 2 files changed, 215 insertions(+), 64 deletions(-) diff --git a/packages/appkit/src/plugins/agents/mlflow.ts b/packages/appkit/src/plugins/agents/mlflow.ts index 48ae18e32..5041c067d 100644 --- a/packages/appkit/src/plugins/agents/mlflow.ts +++ b/packages/appkit/src/plugins/agents/mlflow.ts @@ -39,12 +39,15 @@ let gatedProcessor: GatedMlflowSpanProcessor | undefined; * * 2. Let only agent turns become MLflow traces. mlflow roots a trace at EVERY * parentless span, and AppKit's single provider carries every HTTP/DB span — - * so unscoped, every request would become an MLflow trace. mlflow tags only - * its own spans (`mlflow.spanType`, set AFTER `onStart`), so the call is made - * at the root's `onEnd`: forward it (mlflow exports the trace) only if some - * span in the trace was an mlflow span; otherwise `popTrace` to discard the - * trace mlflow built in memory. Children end before their root, so the flag - * is set by the time the root decides. + * so unscoped, every request would become an MLflow trace. mlflow stamps + * `mlflow.spanType` on EVERY span it processes (defaulting to `UNKNOWN`), so + * presence alone can't tell an agent turn from a plain request — we key on + * the value (AGENT/TOOL) instead. At the root's `onEnd` we forward it (mlflow + * exports the trace) only if some span in the trace carried an AGENT/TOOL + * type; otherwise `popTrace` to discard the trace mlflow built in memory. The + * real span type is set after `onStart` (the constructor stamps UNKNOWN + * first), and children end before their root, so the flag is set by the time + * the root decides. * * It also drops the exporters' own outbound spans at `onStart` (parentless * CLIENT — outgoing requests made outside any agent turn, e.g. mlflow/OTLP @@ -68,6 +71,10 @@ export class GatedMlflowSpanProcessor implements SpanProcessor { #agentTraceIds = new Set(); #popTrace: (otelTraceId: string) => void; #spanTypeKey: string; + // The `mlflow.spanType` attribute values (JSON-stringified) that mark a trace + // as an agent turn — AGENT/TOOL. Every other value (notably UNKNOWN, which + // mlflow stamps on all non-agent spans) is treated as non-agent. + #agentSpanTypes: ReadonlySet; // Leak backstop for #agentTraceIds — far above real concurrency. See onEnd. #maxTracked: number; // Cap on how long forceFlush/shutdown wait for a stuck export. @@ -78,6 +85,7 @@ export class GatedMlflowSpanProcessor implements SpanProcessor { deps: { popTrace: (otelTraceId: string) => void; spanTypeKey: string; + agentSpanTypes: ReadonlySet; maxTracked?: number; flushTimeoutMs?: number; }, @@ -85,6 +93,7 @@ export class GatedMlflowSpanProcessor implements SpanProcessor { this.#inner = inner; this.#popTrace = deps.popTrace; this.#spanTypeKey = deps.spanTypeKey; + this.#agentSpanTypes = deps.agentSpanTypes; this.#maxTracked = deps.maxTracked ?? 1024; this.#flushTimeoutMs = deps.flushTimeoutMs ?? 5000; } @@ -113,11 +122,12 @@ export class GatedMlflowSpanProcessor implements SpanProcessor { onEnd(span: Parameters[0]): void { if (!this.#forwarded.has(span)) return; const traceId = span.spanContext().traceId; - // An mlflow span (AGENT/TOOL) ended in this trace — mark it for export. The - // `mlflow.spanType` attribute is only present after `onStart`, so this is the - // earliest we can see it. + // An AGENT/TOOL span ended in this trace — mark it for export. mlflow stamps + // `mlflow.spanType` on every span (UNKNOWN by default), so match the value, + // not mere presence; the real type is set after `onStart`, so `onEnd` is the + // earliest we can read it. if ( - span.attributes[this.#spanTypeKey] !== undefined && + this.#agentSpanTypes.has(span.attributes[this.#spanTypeKey] as string) && !this.#agentTraceIds.has(traceId) ) { // Normally an entry lives only until its root's onEnd deletes it. But a @@ -183,10 +193,6 @@ export class GatedMlflowSpanProcessor implements SpanProcessor { } } -/** The Databricks experiment tag that links an experiment to a UC trace store. */ -const UC_DESTINATION_PATH_TAG = - "mlflow.experiment.databricksTraceDestinationPath"; - /** * Resolve the Unity Catalog trace location for the bound experiment, or * `undefined` for classic experiment-backed storage. Any failure falls back to @@ -195,14 +201,13 @@ const UC_DESTINATION_PATH_TAG = * 1. Explicit env override — `MLFLOW_UC_CATALOG` + `MLFLOW_UC_SCHEMA` + * `MLFLOW_UC_TABLE_PREFIX`, all three required. * 2. Auto-detect from the linked Databricks experiment (numeric ids only, since - * `GetExperiment` only accepts them): read the - * `mlflow.experiment.databricksTraceDestinationPath` tag, whose value is - * `..`. + * `GetExperiment` only accepts them): parse its `databricksTrace*` tags with + * mlflow's own {@link ucLocationFromExperimentTags}, which also carries the + * backend-populated spans/logs table names for custom-provisioned locations. * 3. Otherwise classic. * - * @mlflow/core ships its own tag parser (`core/destination`) but doesn't export - * it publicly, and its published `.d.ts` redacts the tag constant, so we read - * the tag directly here rather than deep-import a scrubbed internal. + * `ucLocationFromExperimentTags` isn't on `@mlflow/core`'s public entrypoint, so + * it's deep-imported like the exporter classes and covered by the tripwire test. */ async function resolveUcLocation( experimentId: string, @@ -219,12 +224,10 @@ async function resolveUcLocation( try { const experiment = await client.getExperiment(experimentId); - const path = experiment?.tags?.[UC_DESTINATION_PATH_TAG]; - if (!path) return undefined; - // Must be exactly catalog.schema.prefix, all non-empty; else not a UC store. - const [cat, schema, prefix, ...rest] = path.split("."); - if (!cat || !schema || !prefix || rest.length > 0) return undefined; - return { catalogName: cat, schemaName: schema, tablePrefix: prefix }; + if (!experiment) return undefined; + const { ucLocationFromExperimentTags } = + await import("@mlflow/core/dist/core/destination"); + return ucLocationFromExperimentTags(experiment.tags) ?? undefined; } catch (err) { logger.warn( "MLflow UC trace-location auto-detect failed; using classic experiment storage: %O", @@ -249,10 +252,11 @@ async function resolveUcLocation( * pinned to the exact version in package.json and guarded by a test that fails * loudly if a version bump renames them. * - * Also resolves the two hooks {@link GatedMlflowSpanProcessor} needs to scope - * forwarding to agent traces — `popTrace` (to discard non-agent traces) and the - * `mlflow.spanType` attribute key (to recognize an mlflow span) — so the gate's - * `onEnd` stays synchronous. + * Also resolves the hooks {@link GatedMlflowSpanProcessor} needs to scope + * forwarding to agent traces: `popTrace` (to discard non-agent traces), the + * `mlflow.spanType` attribute key, and the JSON-stringified AGENT/TOOL values + * that mark a trace as an agent turn (mlflow stamps every span, defaulting to + * UNKNOWN, so the gate must match the value, not presence). */ async function buildMlflowSpanProcessor( m: MlflowModule, @@ -262,6 +266,7 @@ async function buildMlflowSpanProcessor( processor: SpanProcessor; popTrace: (otelTraceId: string) => void; spanTypeKey: string; + agentSpanTypes: ReadonlySet; }> { let processor: SpanProcessor; if (ucLoc) { @@ -281,6 +286,12 @@ async function buildMlflowSpanProcessor( popTrace: (otelTraceId) => m.InMemoryTraceManager.getInstance().popTrace(otelTraceId), spanTypeKey: m.SpanAttributeKey.SPAN_TYPE, + // mlflow JSON-stringifies attribute values, so the stored values are + // `"AGENT"`/`"TOOL"` (quoted). Match that exact form. + agentSpanTypes: new Set([ + JSON.stringify(m.SpanType.AGENT), + JSON.stringify(m.SpanType.TOOL), + ]), }; } @@ -352,14 +363,12 @@ export async function initAgentTracing(): Promise { authProvider, }); ucLocation = await resolveUcLocation(id, client); - const { processor, popTrace, spanTypeKey } = await buildMlflowSpanProcessor( - mlflow, - client, - ucLocation, - ); + const { processor, popTrace, spanTypeKey, agentSpanTypes } = + await buildMlflowSpanProcessor(mlflow, client, ucLocation); gatedProcessor = new GatedMlflowSpanProcessor(processor, { popTrace, spanTypeKey, + agentSpanTypes, }); TelemetryManager.registerSpanProcessor(gatedProcessor); enabled = true; diff --git a/packages/appkit/src/plugins/agents/tests/mlflow.test.ts b/packages/appkit/src/plugins/agents/tests/mlflow.test.ts index 7b977ca86..6747c2b34 100644 --- a/packages/appkit/src/plugins/agents/tests/mlflow.test.ts +++ b/packages/appkit/src/plugins/agents/tests/mlflow.test.ts @@ -10,7 +10,10 @@ import { GatedMlflowSpanProcessor } from "../mlflow"; * registry and re-mocks the SDK so init runs fresh. */ -function stubSdk(overrides: Record = {}) { +function stubSdk( + overrides: Record = {}, + ucFromTags: Record | null = null, +) { const setInputs = vi.fn(); const setOutputs = vi.fn(); const span = { setInputs, setOutputs }; @@ -21,8 +24,13 @@ function stubSdk(overrides: Record = {}) { // imports under mlflow-tracing 0.1.3), so they live on the main mock. createAuthProvider: vi.fn(() => ({})), MlflowClient: class { - // Auto-detect probes this; default: not a UC-backed experiment. - getExperiment = vi.fn(async () => null); + // Auto-detect probes this; default: experiment exists, UC-ness is then + // decided by the ucLocationFromExperimentTags mock below. + getExperiment = vi.fn(async () => ({ + experimentId: "e", + name: "n", + tags: {}, + })); }, InMemoryTraceManager: { getInstance: () => ({ popTrace: vi.fn() }) }, SpanAttributeKey: { SPAN_TYPE: "mlflow.spanType" }, @@ -37,9 +45,8 @@ function stubSdk(overrides: Record = {}) { }; vi.doMock("@mlflow/core", () => sdk); // Deep imports used by buildMlflowSpanProcessor — the exporter/processor - // classes are the only symbols not on @mlflow/core's public entrypoint. Both - // modules stub the same SpanProcessor shape, so one class covers both; kept as - // light stubs so setup() wires a processor without touching Databricks. + // classes are the only symbols not on @mlflow/core's public entrypoint. Kept + // as light stubs so setup() wires a processor without touching Databricks. class StubProcessor { onStart() {} onEnd() {} @@ -54,11 +61,32 @@ function stubSdk(overrides: Record = {}) { MlflowSpanExporter: class {}, MlflowSpanProcessor: StubProcessor, })); + // Distinct from the classic stub so a test can tell which processor was built + // and assert the resolved UC location actually reaches its constructor. + const uc: { built: boolean; location?: unknown } = { built: false }; vi.doMock("@mlflow/core/dist/exporters/uc_table", () => ({ DatabricksUCTableSpanExporter: class {}, - DatabricksUCTableSpanProcessor: StubProcessor, + DatabricksUCTableSpanProcessor: class { + constructor(_exporter: unknown, location: unknown) { + uc.built = true; + uc.location = location; + } + onStart() {} + onEnd() {} + forceFlush() { + return Promise.resolve(); + } + shutdown() { + return Promise.resolve(); + } + }, })); - return { sdk, span, setInputs, setOutputs }; + // mlflow's experiment-tag → UC-location parser (deep import), used by the + // numeric-id auto-detect path. Default: not a UC experiment (null → classic). + vi.doMock("@mlflow/core/dist/core/destination", () => ({ + ucLocationFromExperimentTags: vi.fn(() => ucFromTags), + })); + return { sdk, span, setInputs, setOutputs, uc }; } describe("agent tracing (mlflow)", () => { @@ -74,6 +102,7 @@ describe("agent tracing (mlflow)", () => { vi.doUnmock("@mlflow/core"); vi.doUnmock("@mlflow/core/dist/exporters/mlflow"); vi.doUnmock("@mlflow/core/dist/exporters/uc_table"); + vi.doUnmock("@mlflow/core/dist/core/destination"); vi.doUnmock("../../../telemetry"); vi.restoreAllMocks(); delete process.env.MLFLOW_EXPERIMENT_ID; @@ -152,38 +181,79 @@ describe("agent tracing (mlflow)", () => { vi.doUnmock("../../../telemetry"); }); - test("UC via env vars: contributes a processor and never calls init()", async () => { + test("UC via env vars: builds the UC processor with that location, never calls init()", async () => { process.env.MLFLOW_EXPERIMENT_ID = "exp-123"; process.env.MLFLOW_UC_CATALOG = "main"; process.env.MLFLOW_UC_SCHEMA = "mario"; process.env.MLFLOW_UC_TABLE_PREFIX = "exp-123"; - const { sdk } = stubSdk(); + const { sdk, uc } = stubSdk(); vi.doMock("../../../telemetry", () => ({ TelemetryManager: { registerSpanProcessor: vi.fn() }, })); const mod = await import("../mlflow"); await mod.initAgentTracing(); - // The UC processor carries its location and reads no global config, so - // init() — which would stand up @mlflow/core's competing NodeSDK — is - // never called, not even eagerly on "setup:complete". mod.startAgentTracing(); + + // The env location reaches the UC processor verbatim... + expect(uc.built).toBe(true); + expect(uc.location).toEqual({ + catalogName: "main", + schemaName: "mario", + tablePrefix: "exp-123", + }); + // ...and the UC processor reads no global config, so init() — which would + // stand up @mlflow/core's competing NodeSDK — is never called, not even + // eagerly on "setup:complete". expect(sdk.init).not.toHaveBeenCalled(); vi.doUnmock("../../../telemetry"); }); - test("UC via experiment tag: auto-detects location and never calls init()", async () => { + test("UC via experiment tag: auto-detects the location, builds the UC processor, no init()", async () => { + process.env.MLFLOW_EXPERIMENT_ID = "123"; // numeric → auto-detect runs + const location = { + catalogName: "main", + schemaName: "mario", + tablePrefix: "123", + }; + const { sdk, uc } = stubSdk({}, location); + vi.doMock("../../../telemetry", () => ({ + TelemetryManager: { registerSpanProcessor: vi.fn() }, + })); + const mod = await import("../mlflow"); + + await mod.initAgentTracing(); + mod.startAgentTracing(); + + expect(uc.built).toBe(true); + expect(uc.location).toEqual(location); + expect(sdk.init).not.toHaveBeenCalled(); // UC path skips init() + vi.doUnmock("../../../telemetry"); + }); + + test("numeric experiment with no UC tag: falls back to classic (init seeds config)", async () => { process.env.MLFLOW_EXPERIMENT_ID = "123"; - const getExperiment = vi.fn(async () => ({ - experimentId: "123", - name: "n", - tags: { - "mlflow.experiment.databricksTraceDestinationPath": "main.mario.123", - }, + const { sdk, uc } = stubSdk(); // ucFromTags defaults to null → classic + vi.doMock("../../../telemetry", () => ({ + TelemetryManager: { registerSpanProcessor: vi.fn() }, })); - const { sdk } = stubSdk({ + const mod = await import("../mlflow"); + + await mod.initAgentTracing(); + mod.startAgentTracing(); + + expect(uc.built).toBe(false); // classic processor, not UC + expect(sdk.init).toHaveBeenCalledOnce(); // classic needs the config seed + vi.doUnmock("../../../telemetry"); + }); + + test("getExperiment failure during auto-detect: classic fallback, tracing stays enabled", async () => { + process.env.MLFLOW_EXPERIMENT_ID = "123"; + const { sdk, uc } = stubSdk({ MlflowClient: class { - getExperiment = getExperiment; + getExperiment = vi.fn(async () => { + throw new Error("permission denied"); + }); }, }); vi.doMock("../../../telemetry", () => ({ @@ -194,8 +264,41 @@ describe("agent tracing (mlflow)", () => { await mod.initAgentTracing(); mod.startAgentTracing(); - expect(getExperiment).toHaveBeenCalledWith("123"); - expect(sdk.init).not.toHaveBeenCalled(); // UC path skips init() + // A UC-detect failure must never break the agent: classic fallback, enabled. + expect(uc.built).toBe(false); + expect(sdk.init).toHaveBeenCalledOnce(); + await mod.traceAgent("agent", { messages: [] }, async () => {}); + expect(sdk.withSpan).toHaveBeenCalled(); // still traces (classic) + vi.doUnmock("../../../telemetry"); + }); + + test("non-numeric experiment id: skips getExperiment (numeric-only) and uses classic", async () => { + process.env.MLFLOW_EXPERIMENT_ID = "exp-123"; // non-numeric + const getExperiment = vi.fn(async () => ({ + experimentId: "x", + name: "n", + tags: {}, + })); + // ucFromTags would return a UC location if the parser were reached... + const { sdk } = stubSdk( + { + MlflowClient: class { + getExperiment = getExperiment; + }, + }, + { catalogName: "main", schemaName: "mario", tablePrefix: "x" }, + ); + vi.doMock("../../../telemetry", () => ({ + TelemetryManager: { registerSpanProcessor: vi.fn() }, + })); + const mod = await import("../mlflow"); + + await mod.initAgentTracing(); + mod.startAgentTracing(); + + // ...but the numeric gate skips the lookup entirely, so it stays classic. + expect(getExperiment).not.toHaveBeenCalled(); + expect(sdk.init).toHaveBeenCalledOnce(); vi.doUnmock("../../../telemetry"); }); @@ -269,17 +372,20 @@ describe("agent tracing (mlflow)", () => { // or moves the public symbols we now import from the entrypoint. Runs against // the REAL package (no mocks); an OSS trackingUri needs no Databricks creds. test("@mlflow/core exposes the public + deep-imported symbols we construct", async () => { + const mlflow = await import("@mlflow/core"); const { createAuthProvider, MlflowClient, InMemoryTraceManager, SpanAttributeKey, SpanType, - } = await import("@mlflow/core"); + } = mlflow; const { MlflowSpanExporter, MlflowSpanProcessor } = await import("@mlflow/core/dist/exporters/mlflow"); const { DatabricksUCTableSpanExporter, DatabricksUCTableSpanProcessor } = await import("@mlflow/core/dist/exporters/uc_table"); + const { ucLocationFromExperimentTags } = + await import("@mlflow/core/dist/core/destination"); // Public entrypoint symbols (deep imports under mlflow-tracing 0.1.3). expect(typeof createAuthProvider).toBe("function"); @@ -289,6 +395,16 @@ describe("agent tracing (mlflow)", () => { expect(SpanType.AGENT).toBe("AGENT"); expect(SpanType.TOOL).toBe("TOOL"); + // The tracing entrypoints trace()/currentTraceId()/linkTraceToRun() call. + for (const fn of [ + "init", + "withSpan", + "getCurrentActiveSpan", + "updateCurrentTrace", + ]) { + expect(typeof (mlflow as any)[fn]).toBe("function"); + } + const authProvider = createAuthProvider({ trackingUri: "http://localhost:5000", }); @@ -310,13 +426,27 @@ describe("agent tracing (mlflow)", () => { } } - // getExperiment backs UC trace-location auto-detect. + // getExperiment + the tag parser back UC trace-location auto-detect. expect(typeof client.getExperiment).toBe("function"); + expect( + ucLocationFromExperimentTags({ + "mlflow.experiment.databricksTraceDestinationPath": "cat.schema.prefix", + }), + ).toMatchObject({ + catalogName: "cat", + schemaName: "schema", + tablePrefix: "prefix", + }); + expect(ucLocationFromExperimentTags({})).toBeNull(); }); }); describe("GatedMlflowSpanProcessor", () => { const SPAN_TYPE_KEY = "mlflow.spanType"; + // The JSON-stringified AGENT/TOOL values that mark a trace as an agent turn, + // exactly as buildMlflowSpanProcessor computes them. mlflow stamps UNKNOWN on + // every non-agent span, so this set must NOT match `'"UNKNOWN"'`. + const AGENT_SPAN_TYPES = new Set(['"AGENT"', '"TOOL"']); function mkGated(maxTracked?: number) { const inner = { @@ -329,6 +459,7 @@ describe("GatedMlflowSpanProcessor", () => { const gated = new GatedMlflowSpanProcessor(inner as any, { popTrace, spanTypeKey: SPAN_TYPE_KEY, + agentSpanTypes: AGENT_SPAN_TYPES, maxTracked, }); return { inner, popTrace, gated }; @@ -363,6 +494,8 @@ describe("GatedMlflowSpanProcessor", () => { const requestRoot = mkSpan({ kind: SpanKind.SERVER, parentSpanContext: undefined, + // mlflow stamps the SERVER root UNKNOWN; the AGENT child is what counts. + attributes: { [SPAN_TYPE_KEY]: '"UNKNOWN"' }, traceId: "tr-agent", }); // incoming request — mlflow roots the trace here const agentChild = mkSpan({ @@ -381,23 +514,31 @@ describe("GatedMlflowSpanProcessor", () => { expect(popTrace).not.toHaveBeenCalled(); }); - test("discards the trace when no agent span appears — a plain HTTP request never becomes an MLflow trace", () => { + // Regression: mlflow's real processor stamps `mlflow.spanType = "UNKNOWN"` on + // EVERY span it processes, so a presence check (`!== undefined`) would export + // every plain request. The gate must key on the AGENT/TOOL value instead. + test("discards a plain request even though mlflow stamps every span UNKNOWN", () => { const { inner, popTrace, gated } = mkGated(); gated.ready(); const requestRoot = mkSpan({ kind: SpanKind.SERVER, parentSpanContext: undefined, + attributes: { [SPAN_TYPE_KEY]: '"UNKNOWN"' }, // mlflow stamps the root traceId: "tr-plain", }); // e.g. /api/analytics/query - const dbChild = mkSpan({ kind: SpanKind.CLIENT, traceId: "tr-plain" }); // SQL warehouse call — no mlflow.spanType + const dbChild = mkSpan({ + kind: SpanKind.CLIENT, + attributes: { [SPAN_TYPE_KEY]: '"UNKNOWN"' }, // ...and the child + traceId: "tr-plain", + }); // SQL warehouse call gated.onStart(requestRoot as any, {} as any); gated.onStart(dbChild as any, {} as any); gated.onEnd(dbChild as any); gated.onEnd(requestRoot as any); - // Root discarded from mlflow, not exported. + // UNKNOWN is not AGENT/TOOL → discarded from mlflow, not exported. expect(popTrace).toHaveBeenCalledExactlyOnceWith("tr-plain"); expect(inner.onEnd).not.toHaveBeenCalledWith(requestRoot); }); @@ -559,6 +700,7 @@ describe("GatedMlflowSpanProcessor", () => { const gated = new GatedMlflowSpanProcessor(inner as any, { popTrace: vi.fn(), spanTypeKey: SPAN_TYPE_KEY, + agentSpanTypes: AGENT_SPAN_TYPES, flushTimeoutMs: 5, }); From fd96b7718b3ca3b89875ed095ddf0a32b5f2a7ba Mon Sep 17 00:00:00 2001 From: MarioCadenas Date: Thu, 3 Sep 2026 10:38:56 +0200 Subject: [PATCH 9/9] chore(deps): bump @mlflow/core to 0.4.0 0.4.0 has cleared the internal Artifactory registry's new-version quarantine window (released 2026-08-27), so CI can now fetch it. The API surface AppKit uses is unchanged from 0.3.0 (same public exports, deep-import paths, UC processor/exporter, ucLocationFromExperimentTags, getExperiment), so no code changes are needed; verified via typecheck + the mlflow suite tripwire against the real 0.4.0 package. Signed-off-by: MarioCadenas --- packages/appkit/package.json | 2 +- pnpm-lock.yaml | 412 ++++++++++++++++++++++++++++++++++- 2 files changed, 408 insertions(+), 6 deletions(-) diff --git a/packages/appkit/package.json b/packages/appkit/package.json index ec31d76ce..a591ef9ea 100644 --- a/packages/appkit/package.json +++ b/packages/appkit/package.json @@ -72,7 +72,7 @@ "@ast-grep/napi": "0.37.0", "@databricks/lakebase": "workspace:*", "@databricks/sdk-experimental": "0.17.0", - "@mlflow/core": "0.3.0", + "@mlflow/core": "0.4.0", "@opentelemetry/api": "1.9.0", "@opentelemetry/api-logs": "0.219.0", "@opentelemetry/auto-instrumentations-node": "0.77.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f70801806..5ee76abd4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -262,8 +262,8 @@ importers: specifier: 0.17.0 version: 0.17.0 '@mlflow/core': - specifier: 0.3.0 - version: 0.3.0 + specifier: 0.4.0 + version: 0.4.0(bufferutil@4.0.9) '@opentelemetry/api': specifier: 1.9.0 version: 1.9.0 @@ -2606,6 +2606,18 @@ packages: '@js-sdsl/ordered-map@4.4.2': resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} + '@jsep-plugin/assignment@1.3.0': + resolution: {integrity: sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==} + engines: {node: '>= 10.16.0'} + peerDependencies: + jsep: ^0.4.0||^1.0.0 + + '@jsep-plugin/regex@1.0.4': + resolution: {integrity: sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==} + engines: {node: '>= 10.16.0'} + peerDependencies: + jsep: ^0.4.0||^1.0.0 + '@jsonjoy.com/base64@1.1.2': resolution: {integrity: sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==} engines: {node: '>=10.0'} @@ -2645,6 +2657,9 @@ packages: '@keyv/serialize@1.1.1': resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} + '@kubernetes/client-node@1.4.0': + resolution: {integrity: sha512-Zge3YvF7DJi264dU1b3wb/GmzR99JhUpqTvp+VGHfwZT+g7EOOYNScDJNZwXy9cszyIGPIs0VHr+kk8e95qqrA==} + '@kwsites/file-exists@1.1.1': resolution: {integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==} @@ -2666,8 +2681,8 @@ packages: '@mermaid-js/parser@0.6.3': resolution: {integrity: sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==} - '@mlflow/core@0.3.0': - resolution: {integrity: sha512-KfLwQv9wvgA9eo0H/S+1JRrfb1+kv1L0rWenSaxQS+RlCJ8hEg3uc/sai0SdNB4SOqkkJNBM5WMD02iI22walg==} + '@mlflow/core@0.4.0': + resolution: {integrity: sha512-z80wUaXWRsw5xquvpBW2BRJgjA7M+dj67JqR08i+cH8DZK+DV5y8RpaJyFzxGZkAXsy5GO4W+ndeL8tB5nlQaw==} engines: {node: '>=18'} hasBin: true @@ -5368,6 +5383,9 @@ packages: '@types/mysql@2.15.27': resolution: {integrity: sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA==} + '@types/node-fetch@2.6.13': + resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==} + '@types/node-forge@1.3.14': resolution: {integrity: sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==} @@ -5469,6 +5487,9 @@ packages: '@types/sockjs@0.3.36': resolution: {integrity: sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==} + '@types/stream-buffers@3.0.8': + resolution: {integrity: sha512-J+7VaHKNvlNPJPEJXX/fKa9DZtR/xPMwuIbe+yNOwp1YB+ApUOBv2aUpEoBJEi8nJgbgs1x8e73ttg0r1rSUdw==} + '@types/tedious@4.0.14': resolution: {integrity: sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==} @@ -5867,6 +5888,9 @@ packages: async-retry@1.3.3: resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + autocomplete.js@0.37.1: resolution: {integrity: sha512-PgSe9fHYhZEsm/9jggbjtVsGXJkPLvd+9mC7gZJ662vVL5CRWEtm/mIrrzCx0MrNxHVwxD5d00UOn6NsmL2LUQ==} @@ -5888,6 +5912,14 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} + b4a@1.8.1: + resolution: {integrity: sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==} + peerDependencies: + react-native-b4a: '*' + peerDependenciesMeta: + react-native-b4a: + optional: true + babel-loader@9.2.1: resolution: {integrity: sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA==} engines: {node: '>= 14.15.0'} @@ -5926,6 +5958,43 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} + bare-events@2.9.2: + resolution: {integrity: sha512-AIPKioV7/Y/8KfZ3AAhjPJxLLbY49S64Ym5DakZlUg75qQiTgUq9hEJoEwa4eUezPUlXRy/i5NpsKvo9jgKmoA==} + peerDependencies: + bare-abort-controller: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true + + bare-fs@4.8.1: + resolution: {integrity: sha512-N1nnXdHZAOSstz0XiHikGS4HGMH4CnSwhqWdGQQMqqdvp4Jybm9sE3R1WVnpWVd4SFkc8ryPDBLViNLwiEqECg==} + engines: {bare: '>=1.28.0'} + peerDependencies: + bare-buffer: '*' + peerDependenciesMeta: + bare-buffer: + optional: true + + bare-path@3.1.1: + resolution: {integrity: sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ==} + + bare-stream@2.13.4: + resolution: {integrity: sha512-PcrQ8lVLbiJscNm1Kez+Yp4Gy4AHGcN1lzwjvf5NybWen7VvEgUfyfnXYJ2zNqWnzOfCb1Abq6lH8ti0syQszA==} + peerDependencies: + bare-abort-controller: '*' + bare-buffer: '*' + bare-events: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true + bare-buffer: + optional: true + bare-events: + optional: true + + bare-url@2.5.2: + resolution: {integrity: sha512-L13PCJzKG8RGvx8V1/DdMi12ERhC3tprr7/8a94BxpmnRsFqxh5XZNdhtMxu5HPkRshYOOWRGY8lDP7ZhpG9Cg==} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -6301,6 +6370,10 @@ packages: resolution: {integrity: sha512-VcQB1ziGD0NXrhKxiwyNbCDmRzs/OShMs2GqW2DlU2A/Sd0nQxE1oWDAE5O0ygSx5mgQOn9eIFh7yKPgFRVkPQ==} engines: {node: '>=10'} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + comma-separated-tokens@1.0.8: resolution: {integrity: sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==} @@ -6932,6 +7005,10 @@ packages: delaunator@5.0.1: resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + depd@1.1.2: resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} engines: {node: '>= 0.6'} @@ -7284,6 +7361,10 @@ packages: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + esast-util-from-estree@2.0.0: resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==} @@ -7435,6 +7516,9 @@ packages: eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + events-universal@1.0.1: + resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} + events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} @@ -7482,6 +7566,9 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} @@ -7621,6 +7708,10 @@ packages: resolution: {integrity: sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw==} engines: {node: '>= 18'} + form-data@4.0.6: + resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} + engines: {node: '>= 6'} + format@0.2.2: resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} engines: {node: '>=0.4.x'} @@ -7893,6 +7984,10 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + hast-util-embedded@3.0.0: resolution: {integrity: sha512-naH8sld4Pe2ep03qqULEtvYr7EjrLK2QHY8KJR6RJkTUjPGObe1vnx585uzem2hGra+s1q08DZZpfgDVYRbaXA==} @@ -8020,6 +8115,10 @@ packages: hpack.js@2.1.6: resolution: {integrity: sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==} + hpagent@1.2.0: + resolution: {integrity: sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA==} + engines: {node: '>=14'} + html-encoding-sniffer@4.0.0: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} engines: {node: '>=18'} @@ -8463,6 +8562,11 @@ packages: resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} engines: {node: '>=0.10.0'} + isomorphic-ws@5.0.0: + resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} + peerDependencies: + ws: '*' + issue-parser@7.0.1: resolution: {integrity: sha512-3YZcUUR2Wt1WsapF+S/WiA2WmlW0cWAoPccMqne7AxEBhCdFeTPjfv/Axb8V2gyCgY3nRw+ksZ3xSUX+R47iAg==} engines: {node: ^18.17 || >=20.6.1} @@ -8513,6 +8617,9 @@ packages: joi@17.13.3: resolution: {integrity: sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==} + jose@6.2.10: + resolution: {integrity: sha512-iiW7J9qRFlGxvCOIBDBDxFePQSn7ZMAnrYGhrrOo6siO/MIqwfyilLR27pkfDgUk+raLuzADS8A3S/KLBisc0g==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -8536,6 +8643,10 @@ packages: canvas: optional: true + jsep@1.4.0: + resolution: {integrity: sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==} + engines: {node: '>= 10.16.0'} + jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -8589,6 +8700,11 @@ packages: resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} engines: {'0': node >= 0.2.0} + jsonpath-plus@10.4.0: + resolution: {integrity: sha512-T92WWatJXmhBbKsgH/0hl+jxjdXrifi5IKeMY02DWggRxX0UElcbVzPlmgLTbvsPeW1PasQ6xE2Q75stkhGbsA==} + engines: {node: '>=18.0.0'} + hasBin: true + just-diff-apply@5.5.0: resolution: {integrity: sha512-OYTthRfSh55WOItVqwpefPtNt2VdKsq5AnAK6apdtR6yCH8pr0CmSr710J0Mf+WdQy7K/OzMy7K2MgAfdQURDw==} @@ -9373,6 +9489,15 @@ packages: node-fetch-native@1.6.7: resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + node-fetch@3.3.2: resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -9470,6 +9595,9 @@ packages: engines: {node: ^14.16.0 || >=16.10.0} hasBin: true + oauth4webapi@3.8.7: + resolution: {integrity: sha512-4RxcKxXjuItDFZ20RRPf4YTw3kpeXJyCgJFxVzJ068A7PNJ18st2Dg90tlC1LkSDS0GecroagCLHYEIVUhCAkw==} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -9534,6 +9662,9 @@ packages: resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} hasBin: true + openid-client@6.8.7: + resolution: {integrity: sha512-gtKthNu7evSBvTdrrlHb4F3Fi9dcwlb5QaITlCs+9mfpvuOi0Q3qtBf5+iY4sEP8hy1qCoAdxBNPDcmZeVSDzQ==} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -10669,6 +10800,9 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rfc4648@1.5.4: + resolution: {integrity: sha512-rRg/6Lb+IGfJqO05HZkN50UtY7K/JhxJag1kP23+zyMfrvoB0B7RWv06MbOzoc79RgCdNTiUaNsTT1AJZ7Z+cg==} + rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} @@ -11133,6 +11267,13 @@ packages: resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} engines: {node: '>=18'} + stream-buffers@3.0.3: + resolution: {integrity: sha512-pqMqwQCso0PBJt2PQmDO0cFj0lyqmiwOMiMSkVtRokl7e+ZTRYgDHKnuZNbqjiJXgsg4nuqtD/zxuo9KqTp0Yw==} + engines: {node: '>= 0.10.0'} + + streamx@2.28.1: + resolution: {integrity: sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA==} + string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} @@ -11279,14 +11420,23 @@ packages: tar-fs@2.1.4: resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} + tar-fs@3.1.3: + resolution: {integrity: sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==} + tar-stream@2.2.0: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} + tar-stream@3.2.1: + resolution: {integrity: sha512-nqsEO8zLZJvrOMdEwkA0QdCLFbetHMn95Zqu4fKwX+hkaTWJPZZOrxx/PwtxoK0MMGQmBQNRW3CPs8IFYQz4cQ==} + tar@7.5.10: resolution: {integrity: sha512-8mOPs1//5q/rlkNSPcCegA6hiHJYDmSLEI8aMH/CdSQJNWztHC9WHNam5zdQlfpTwB9Xp7IBEsHfV5LKMJGVAw==} engines: {node: '>=18'} + teex@1.0.1: + resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==} + terser-webpack-plugin@5.3.16: resolution: {integrity: sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==} engines: {node: '>= 10.13.0'} @@ -11312,6 +11462,9 @@ packages: resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} engines: {node: '>=18'} + text-decoder@1.2.7: + resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==} + text-extensions@2.4.0: resolution: {integrity: sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==} engines: {node: '>=8'} @@ -11401,6 +11554,9 @@ packages: resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} engines: {node: '>=16'} + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tr46@6.0.0: resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} engines: {node: '>=20'} @@ -12048,6 +12204,9 @@ packages: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + webidl-conversions@8.0.0: resolution: {integrity: sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==} engines: {node: '>=20'} @@ -12128,6 +12287,9 @@ packages: resolution: {integrity: sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==} engines: {node: '>=20'} + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + which-typed-array@1.1.20: resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} engines: {node: '>= 0.4'} @@ -15179,6 +15341,16 @@ snapshots: '@js-sdsl/ordered-map@4.4.2': {} + '@jsep-plugin/assignment@1.3.0(jsep@1.4.0)': + dependencies: + jsep: 1.4.0 + optional: true + + '@jsep-plugin/regex@1.0.4(jsep@1.4.0)': + dependencies: + jsep: 1.4.0 + optional: true + '@jsonjoy.com/base64@1.1.2(tslib@2.8.1)': dependencies: tslib: 2.8.1 @@ -15217,6 +15389,34 @@ snapshots: '@keyv/serialize@1.1.1': {} + '@kubernetes/client-node@1.4.0(bufferutil@4.0.9)': + dependencies: + '@types/js-yaml': 4.0.9 + '@types/node': 24.10.1 + '@types/node-fetch': 2.6.13 + '@types/stream-buffers': 3.0.8 + form-data: 4.0.6 + hpagent: 1.2.0 + isomorphic-ws: 5.0.0(ws@8.21.0(bufferutil@4.0.9)) + js-yaml: 4.3.1 + jsonpath-plus: 10.4.0 + node-fetch: 2.7.0 + openid-client: 6.8.7 + rfc4648: 1.5.4 + socks-proxy-agent: 8.0.5 + stream-buffers: 3.0.3 + tar-fs: 3.1.3 + ws: 8.21.0(bufferutil@4.0.9) + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - bufferutil + - encoding + - react-native-b4a + - supports-color + - utf-8-validate + optional: true + '@kwsites/file-exists@1.1.1': dependencies: debug: 4.4.3 @@ -15269,7 +15469,7 @@ snapshots: dependencies: langium: 3.3.1 - '@mlflow/core@0.3.0': + '@mlflow/core@0.4.0(bufferutil@4.0.9)': dependencies: '@databricks/sdk-experimental': 0.15.0 '@opentelemetry/api': 1.9.0 @@ -15280,8 +15480,16 @@ snapshots: bignumber.js: 9.3.1 fast-safe-stringify: 2.1.1 ini: 5.0.0 + optionalDependencies: + '@kubernetes/client-node': 1.4.0(bufferutil@4.0.9) transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - bufferutil + - encoding + - react-native-b4a - supports-color + - utf-8-validate '@napi-rs/wasm-runtime@1.1.1': dependencies: @@ -18025,6 +18233,12 @@ snapshots: dependencies: '@types/node': 25.2.3 + '@types/node-fetch@2.6.13': + dependencies: + '@types/node': 25.2.3 + form-data: 4.0.6 + optional: true + '@types/node-forge@1.3.14': dependencies: '@types/node': 25.2.3 @@ -18149,6 +18363,11 @@ snapshots: dependencies: '@types/node': 25.2.3 + '@types/stream-buffers@3.0.8': + dependencies: + '@types/node': 25.2.3 + optional: true + '@types/tedious@4.0.14': dependencies: '@types/node': 25.2.3 @@ -18654,6 +18873,9 @@ snapshots: dependencies: retry: 0.13.1 + asynckit@0.4.0: + optional: true + autocomplete.js@0.37.1: dependencies: immediate: 3.3.0 @@ -18681,6 +18903,9 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 + b4a@1.8.1: + optional: true + babel-loader@9.2.1(@babel/core@7.28.5)(webpack@5.103.0): dependencies: '@babel/core': 7.28.5 @@ -18724,6 +18949,40 @@ snapshots: balanced-match@4.0.4: {} + bare-events@2.9.2: + optional: true + + bare-fs@4.8.1: + dependencies: + bare-events: 2.9.2 + bare-path: 3.1.1 + bare-stream: 2.13.4(bare-events@2.9.2) + bare-url: 2.5.2 + fast-fifo: 1.3.2 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + optional: true + + bare-path@3.1.1: + optional: true + + bare-stream@2.13.4(bare-events@2.9.2): + dependencies: + b4a: 1.8.1 + streamx: 2.28.1 + teex: 1.0.1 + optionalDependencies: + bare-events: 2.9.2 + transitivePeerDependencies: + - react-native-b4a + optional: true + + bare-url@2.5.2: + dependencies: + bare-path: 3.1.1 + optional: true + base64-js@1.5.1: {} baseline-browser-mapping@2.8.32: {} @@ -19183,6 +19442,11 @@ snapshots: combine-promises@1.2.0: {} + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + optional: true + comma-separated-tokens@1.0.8: {} comma-separated-tokens@2.0.3: {} @@ -19842,6 +20106,9 @@ snapshots: dependencies: robust-predicates: 3.0.2 + delayed-stream@1.0.0: + optional: true + depd@1.1.2: {} depd@2.0.0: {} @@ -20084,6 +20351,14 @@ snapshots: dependencies: es-errors: 1.3.0 + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + optional: true + esast-util-from-estree@2.0.0: dependencies: '@types/estree-jsx': 1.0.5 @@ -20291,6 +20566,13 @@ snapshots: eventemitter3@5.0.1: {} + events-universal@1.0.1: + dependencies: + bare-events: 2.9.2 + transitivePeerDependencies: + - bare-abort-controller + optional: true + events@3.3.0: {} eventsource-parser@3.0.6: {} @@ -20375,6 +20657,9 @@ snapshots: fast-deep-equal@3.1.3: {} + fast-fifo@1.3.2: + optional: true + fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -20514,6 +20799,15 @@ snapshots: form-data-encoder@4.1.0: {} + form-data@4.0.6: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.4 + mime-types: 2.1.35 + optional: true + format@0.2.2: {} formatly@0.3.0: @@ -20834,6 +21128,11 @@ snapshots: dependencies: function-bind: 1.1.2 + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + optional: true + hast-util-embedded@3.0.0: dependencies: '@types/hast': 3.0.4 @@ -21131,6 +21430,9 @@ snapshots: readable-stream: 2.3.8 wbuf: 1.7.3 + hpagent@1.2.0: + optional: true + html-encoding-sniffer@4.0.0: dependencies: whatwg-encoding: 3.1.1 @@ -21508,6 +21810,11 @@ snapshots: isobject@3.0.1: {} + isomorphic-ws@5.0.0(ws@8.21.0(bufferutil@4.0.9)): + dependencies: + ws: 8.21.0(bufferutil@4.0.9) + optional: true + issue-parser@7.0.1: dependencies: lodash.capitalize: 4.2.1 @@ -21587,6 +21894,9 @@ snapshots: '@sideway/formula': 3.0.1 '@sideway/pinpoint': 2.0.0 + jose@6.2.10: + optional: true + js-tokens@4.0.0: {} js-tokens@9.0.1: {} @@ -21628,6 +21938,9 @@ snapshots: - supports-color - utf-8-validate + jsep@1.4.0: + optional: true + jsesc@3.1.0: {} json-bigint@1.0.0: @@ -21665,6 +21978,13 @@ snapshots: jsonparse@1.3.1: {} + jsonpath-plus@10.4.0: + dependencies: + '@jsep-plugin/assignment': 1.3.0(jsep@1.4.0) + '@jsep-plugin/regex': 1.0.4(jsep@1.4.0) + jsep: 1.4.0 + optional: true + just-diff-apply@5.5.0: {} just-diff@6.0.2: {} @@ -22714,6 +23034,11 @@ snapshots: node-fetch-native@1.6.7: {} + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + optional: true + node-fetch@3.3.2: dependencies: data-uri-to-buffer: 4.0.1 @@ -22818,6 +23143,9 @@ snapshots: pkg-types: 2.3.0 tinyexec: 1.0.2 + oauth4webapi@3.8.7: + optional: true + object-assign@4.1.1: {} object-inspect@1.13.4: {} @@ -22882,6 +23210,12 @@ snapshots: opener@1.5.2: {} + openid-client@6.8.7: + dependencies: + jose: 6.2.10 + oauth4webapi: 3.8.7 + optional: true + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -24251,6 +24585,9 @@ snapshots: reusify@1.1.0: {} + rfc4648@1.5.4: + optional: true + rfdc@1.4.1: {} rimraf@5.0.10: @@ -24814,6 +25151,19 @@ snapshots: stdin-discarder@0.2.2: {} + stream-buffers@3.0.3: + optional: true + + streamx@2.28.1: + dependencies: + events-universal: 1.0.1 + fast-fifo: 1.3.2 + text-decoder: 1.2.7 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + optional: true + string-argv@0.3.2: {} string-width@4.2.3: @@ -24963,6 +25313,19 @@ snapshots: tar-stream: 2.2.0 optional: true + tar-fs@3.1.3: + dependencies: + pump: 3.0.4 + tar-stream: 3.2.1 + optionalDependencies: + bare-fs: 4.8.1 + bare-path: 3.1.1 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + optional: true + tar-stream@2.2.0: dependencies: bl: 4.1.0 @@ -24972,6 +25335,18 @@ snapshots: readable-stream: 3.6.2 optional: true + tar-stream@3.2.1: + dependencies: + b4a: 1.8.1 + bare-fs: 4.8.1 + fast-fifo: 1.3.2 + streamx: 2.28.1 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + optional: true + tar@7.5.10: dependencies: '@isaacs/fs-minipass': 4.0.1 @@ -24980,6 +25355,14 @@ snapshots: minizlib: 3.1.0 yallist: 5.0.0 + teex@1.0.1: + dependencies: + streamx: 2.28.1 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + optional: true + terser-webpack-plugin@5.3.16(webpack@5.103.0): dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -25002,6 +25385,13 @@ snapshots: glob: 10.4.5 minimatch: 9.0.5 + text-decoder@1.2.7: + dependencies: + b4a: 1.8.1 + transitivePeerDependencies: + - react-native-b4a + optional: true + text-extensions@2.4.0: {} thingies@2.5.0(tslib@2.8.1): @@ -25068,6 +25458,9 @@ snapshots: dependencies: tldts: 7.0.17 + tr46@0.0.3: + optional: true + tr46@6.0.0: dependencies: punycode: 2.3.1 @@ -25751,6 +26144,9 @@ snapshots: web-streams-polyfill@3.3.3: {} + webidl-conversions@3.0.1: + optional: true + webidl-conversions@8.0.0: {} webpack-bundle-analyzer@4.10.2(bufferutil@4.0.9): @@ -25897,6 +26293,12 @@ snapshots: tr46: 6.0.0 webidl-conversions: 8.0.0 + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + optional: true + which-typed-array@1.1.20: dependencies: available-typed-arrays: 1.0.7