From 02613fe7d926c938bd5f278e09970b9c0558b534 Mon Sep 17 00:00:00 2001 From: MarioCadenas Date: Fri, 26 Jun 2026 19:01:42 +0200 Subject: [PATCH 1/7] feat(appkit): add mlflow tracing for agents Trace agent turns and tool calls to MLflow via the mlflow-tracing SDK. Adds an optional 'experiment' resource to the agents plugin; when bound (MLFLOW_EXPERIMENT_ID), each turn opens an AGENT span and each tool call a nested TOOL span, with auth resolved from the app's Databricks credentials. A turn's trace can be linked to an evaluation run via mlflow.sourceRun. Signed-off-by: MarioCadenas --- packages/appkit/package.json | 1 + packages/appkit/src/plugins/agents/agents.ts | 256 ++++++---- .../appkit/src/plugins/agents/manifest.json | 13 + packages/appkit/src/plugins/agents/mlflow.ts | 126 +++++ packages/appkit/src/plugins/agents/schemas.ts | 5 + pnpm-lock.yaml | 441 ++++++++++++++++++ template/appkit.plugins.json | 14 + 7 files changed, 763 insertions(+), 93 deletions(-) create mode 100644 packages/appkit/src/plugins/agents/mlflow.ts diff --git a/packages/appkit/package.json b/packages/appkit/package.json index 74fa1c920..6a1541c35 100644 --- a/packages/appkit/package.json +++ b/packages/appkit/package.json @@ -83,6 +83,7 @@ "get-port": "7.2.0", "js-yaml": "4.1.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/agents.ts b/packages/appkit/src/plugins/agents/agents.ts index c63ec094f..16289be30 100644 --- a/packages/appkit/src/plugins/agents/agents.ts +++ b/packages/appkit/src/plugins/agents/agents.ts @@ -52,6 +52,12 @@ import { agentStreamDefaults } from "./defaults"; import { EventChannel } from "./event-channel"; import { AgentEventTranslator } from "./event-translator"; import manifest from "./manifest.json"; +import { + currentTraceId, + initAgentTracing, + linkTraceToRun, + withAgentSpan, +} from "./mlflow"; import { approvalRequestSchema, cancelRequestSchema, @@ -130,7 +136,11 @@ interface RunState { } export class AgentsPlugin extends Plugin implements ToolProvider { - static manifest = manifest as PluginManifest; + // Routed through `unknown`: the optional resources have differing `fields` + // keys (serving `name`, experiment `experimentId`), which TS widens to an + // incompatible union on the JSON import. The shape is validated at runtime + // against the plugin-manifest schema. + static manifest = manifest as unknown as PluginManifest; static phase: PluginPhase = "deferred"; protected declare config: AgentsPluginConfig; @@ -273,6 +283,7 @@ export class AgentsPlugin extends Plugin implements ToolProvider { } async setup() { + await initAgentTracing(); const { agents, defaultAgentName } = await this.buildAgentRegistry(); this.agents = agents; this.defaultAgentName = defaultAgentName; @@ -1070,61 +1081,97 @@ export class AgentsPlugin extends Plugin implements ToolProvider { outboundEvents.push(evt); } - const pluginNames = this.context - ? this.context - .getPluginNames() - .filter((n) => n !== this.name && n !== "server") - : []; - const fullPrompt = composePromptForAgent( - registered, - this.config.baseSystemPrompt, + // Root MLflow span for the turn (AGENT). Tool calls dispatched while + // the adapter streams nest under it via the SDK's active context. + // No-op passthrough unless an MLflow experiment is bound. + await withAgentSpan( { - agentName: registered.name, - pluginNames, - toolNames: tools.map((t) => t.name), + name: registered.name ?? "agent", + type: "AGENT", + inputs: { + messages: thread.messages.map((m) => ({ + role: m.role, + content: m.content, + })), + }, }, - ); + async (span) => { + // Link this turn's trace to an eval run when the eval runner + // supplied one, so the trace shows under the MLflow evaluation run. + const evalRunId = (req.body as { mlflowRunId?: string }) + ?.mlflowRunId; + if (evalRunId) linkTraceToRun(evalRunId); + + const pluginNames = this.context + ? this.context + .getPluginNames() + .filter((n) => n !== this.name && n !== "server") + : []; + const fullPrompt = composePromptForAgent( + registered, + this.config.baseSystemPrompt, + { + agentName: registered.name, + pluginNames, + toolNames: tools.map((t) => t.name), + }, + ); - const messagesWithSystem: Message[] = [ - { - id: "system", - role: "system", - content: fullPrompt, - createdAt: new Date(), - }, - ...thread.messages, - ]; + const messagesWithSystem: Message[] = [ + { + id: "system", + role: "system", + content: fullPrompt, + createdAt: new Date(), + }, + ...thread.messages, + ]; + + const stream = registered.adapter.run( + { + messages: messagesWithSystem, + tools, + threadId: thread.id, + signal, + }, + { executeTool, signal }, + ); - const stream = registered.adapter.run( - { - messages: messagesWithSystem, - tools, - threadId: thread.id, - signal, - }, - { executeTool, signal }, - ); + // The accumulation rule (deltas append, `message` replaces) is + // shared with `runAgent` and `runSubAgent`; see + // `consumeAdapterStream` for the rationale. + const fullContent = await consumeAdapterStream(stream, { + signal, + onEvent: (event) => { + for (const translated of translator.translate(event)) { + outboundEvents.push(translated); + } + }, + }); + + if (fullContent) { + span?.setOutputs({ role: "assistant", content: fullContent }); + await this.threadStore.addMessage(thread.id, userId, { + id: randomUUID(), + role: "assistant", + content: fullContent, + createdAt: new Date(), + }); + } - // The accumulation rule (deltas append, `message` replaces) is shared - // with `runAgent` and `runSubAgent`; see `consumeAdapterStream` for - // the rationale. - const fullContent = await consumeAdapterStream(stream, { - signal, - onEvent: (event) => { - for (const translated of translator.translate(event)) { - outboundEvents.push(translated); + // Surface the MLflow trace id so eval runs can attach assessments + // to this turn's trace. No-op when tracing is disabled. + const mlflowTraceId = currentTraceId(); + if (mlflowTraceId) { + for (const evt of translator.translate({ + type: "metadata", + data: { mlflowTraceId }, + })) { + outboundEvents.push(evt); + } } }, - }); - - if (fullContent) { - await this.threadStore.addMessage(thread.id, userId, { - id: randomUUID(), - role: "assistant", - content: fullContent, - createdAt: new Date(), - }); - } + ); for (const evt of translator.finalize()) outboundEvents.push(evt); } catch (error) { @@ -1389,51 +1436,74 @@ export class AgentsPlugin extends Plugin implements ToolProvider { } } - let result: unknown; - if (entry.source === "toolkit") { - if (!this.context) { - throw new Error( - "Plugin tool execution requires PluginContext; this should never happen through createApp", - ); - } - result = await this.context.executeTool( - runState.req, - entry.pluginName, - entry.localName, - args, - runState.signal, - runState.limits.toolCallTimeoutMs, - ); - } else if (entry.source === "function") { - // Function tools declare their parameters as a JSON-object schema, - // so adapters always serialize `args` as an object. A non-object - // value here means the upstream model emitted malformed tool-call - // JSON; surface a clear error rather than silently passing through - // a wrong-shape value the tool will then choke on. - if (typeof args !== "object" || args === null || Array.isArray(args)) { - throw new Error( - `Function tool '${name}' received non-object arguments (got ${args === null ? "null" : Array.isArray(args) ? "array" : typeof args}); expected a JSON object.`, - ); - } - result = await entry.functionTool.execute( - args as Record, - ); - } else if (entry.source === "mcp") { - if (!this.mcpClient) throw new Error("MCP client not connected"); - const oboToken = runState.req.headers["x-forwarded-access-token"]; - const mcpAuth = - typeof oboToken === "string" - ? { Authorization: `Bearer ${oboToken}` } - : undefined; - result = await this.mcpClient.callTool(entry.mcpToolName, args, mcpAuth); - } else if (entry.source === "subagent") { - const childAgent = this.agents.get(entry.agentName); - if (!childAgent) - throw new Error(`Sub-agent not found: ${entry.agentName}`); - result = await this.runSubAgent(runState, childAgent, args, depth + 1); - } + // MLflow TOOL span — nests under the turn's AGENT span. Wraps the + // execution only (not the approval wait above, which is human latency). + const toolResult = await withAgentSpan( + { name, type: "TOOL", inputs: args }, + async (span) => { + let result: unknown; + if (entry.source === "toolkit") { + if (!this.context) { + throw new Error( + "Plugin tool execution requires PluginContext; this should never happen through createApp", + ); + } + result = await this.context.executeTool( + runState.req, + entry.pluginName, + entry.localName, + args, + runState.signal, + runState.limits.toolCallTimeoutMs, + ); + } else if (entry.source === "function") { + // Function tools declare their parameters as a JSON-object schema, + // so adapters always serialize `args` as an object. A non-object + // value here means the upstream model emitted malformed tool-call + // JSON; surface a clear error rather than silently passing through + // a wrong-shape value the tool will then choke on. + if ( + typeof args !== "object" || + args === null || + Array.isArray(args) + ) { + throw new Error( + `Function tool '${name}' received non-object arguments (got ${args === null ? "null" : Array.isArray(args) ? "array" : typeof args}); expected a JSON object.`, + ); + } + result = await entry.functionTool.execute( + args as Record, + ); + } else if (entry.source === "mcp") { + if (!this.mcpClient) throw new Error("MCP client not connected"); + const oboToken = runState.req.headers["x-forwarded-access-token"]; + const mcpAuth = + typeof oboToken === "string" + ? { Authorization: `Bearer ${oboToken}` } + : undefined; + result = await this.mcpClient.callTool( + entry.mcpToolName, + args, + mcpAuth, + ); + } else if (entry.source === "subagent") { + const childAgent = this.agents.get(entry.agentName); + if (!childAgent) + throw new Error(`Sub-agent not found: ${entry.agentName}`); + result = await this.runSubAgent( + runState, + childAgent, + args, + depth + 1, + ); + } + + span?.setOutputs(result); + return result; + }, + ); - return normalizeToolResult(result); + return normalizeToolResult(toolResult); } /** diff --git a/packages/appkit/src/plugins/agents/manifest.json b/packages/appkit/src/plugins/agents/manifest.json index f2e5420bc..4d6f52485 100644 --- a/packages/appkit/src/plugins/agents/manifest.json +++ b/packages/appkit/src/plugins/agents/manifest.json @@ -19,6 +19,19 @@ "description": "Default LLM serving endpoint name" } } + }, + { + "type": "experiment", + "alias": "MLflow experiment for agent traces", + "resourceKey": "agents-mlflow-experiment", + "description": "When bound, agent turns and tool calls are traced to this MLflow experiment via OpenTelemetry. Tracing is a no-op when unset.", + "permission": "CAN_EDIT", + "fields": { + "experimentId": { + "env": "MLFLOW_EXPERIMENT_ID", + "description": "MLflow experiment id traces are logged to" + } + } } ] } diff --git a/packages/appkit/src/plugins/agents/mlflow.ts b/packages/appkit/src/plugins/agents/mlflow.ts new file mode 100644 index 000000000..4d3230d08 --- /dev/null +++ b/packages/appkit/src/plugins/agents/mlflow.ts @@ -0,0 +1,126 @@ +import type { LiveSpan } from "mlflow-tracing"; +import { createLogger } from "../../logging/logger"; + +const logger = createLogger("agents"); + +type MlflowModule = typeof import("mlflow-tracing"); + +let mlflow: MlflowModule | undefined; +let enabled = false; +let initStarted = false; + +/** The bound MLflow experiment id, from the optional `experiment` resource. */ +function experimentId(): string | undefined { + const id = process.env.MLFLOW_EXPERIMENT_ID?.trim(); + return id || undefined; +} + +/** + * Databricks host with a scheme. The mlflow-tracing SDK 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), + * return undefined and let the SDK read the host from `~/.databrickscfg`. + */ +function normalizedDatabricksHost(): string | undefined { + const raw = process.env.DATABRICKS_HOST?.trim(); + if (!raw) return undefined; + return /^https?:\/\//i.test(raw) ? raw : `https://${raw}`; +} + +/** + * Initialize MLflow agent tracing once, when an experiment is bound — i.e. the + * agents plugin's optional `experiment` resource is set (`MLFLOW_EXPERIMENT_ID`). + * + * Auth is resolved by the `mlflow-tracing` SDK 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 + * leaves tracing disabled rather than breaking the agent. + * + * Safe to call repeatedly; only the first call does work. + */ +export async function initAgentTracing(): Promise { + if (initStarted) return; + initStarted = true; + + const id = experimentId(); + if (!id) return; + + try { + mlflow = await import("mlflow-tracing"); + const host = normalizedDatabricksHost(); + mlflow.init({ + trackingUri: process.env.MLFLOW_TRACKING_URI?.trim() || "databricks", + experimentId: id, + ...(host ? { host } : {}), + }); + enabled = true; + logger.info("MLflow agent tracing enabled (experiment %s)", id); + } catch (err) { + logger.warn("MLflow agent tracing disabled: %O", err); + } +} + +export type AgentSpanType = "AGENT" | "TOOL"; + +/** + * Run `fn` inside an MLflow span when tracing is enabled, otherwise just run it + * (zero overhead). Spans auto-nest via the SDK's active context, so a TOOL span + * opened inside an AGENT span's callback becomes its child. The callback gets + * the span (or `undefined` when disabled) to record outputs. + */ +export async function withAgentSpan( + options: { name: string; type: AgentSpanType; inputs?: unknown }, + fn: (span?: LiveSpan) => Promise, +): Promise { + if (!enabled || !mlflow) return fn(); + const spanType = + options.type === "AGENT" ? mlflow.SpanType.AGENT : mlflow.SpanType.TOOL; + return await mlflow.withSpan( + (span) => { + if (options.inputs !== undefined) span.setInputs(options.inputs); + return fn(span); + }, + { name: options.name, spanType }, + ); +} + +/** + * The MLflow trace id for the active turn, when tracing is enabled. Read inside + * an agent span so eval runs can correlate the turn to its trace and attach + * assessments. Returns undefined when tracing is off. + */ +export function currentTraceId(): string | undefined { + if (!enabled || !mlflow) return undefined; + try { + return mlflow.getLastActiveTraceId(); + } catch { + return undefined; + } +} + +/** + * Link the active turn's trace to an MLflow run by id, via the `mlflow.sourceRun` + * trace metadata. Used by eval runs so each case's trace shows under the run. + * Must be called while a trace is active (inside an agent span). No-op when + * tracing is disabled. + */ +export function linkTraceToRun(runId: string): void { + if (!enabled || !mlflow) return; + try { + mlflow.updateCurrentTrace({ metadata: { "mlflow.sourceRun": runId } }); + } catch (err) { + logger.warn("Failed to link trace to run %s: %O", runId, err); + } +} + +/** Flush buffered traces (e.g. on shutdown). No-op when tracing is disabled. */ +export async function flushAgentTraces(): Promise { + if (!enabled || !mlflow) return; + try { + await mlflow.flushTraces(); + } catch (err) { + logger.warn("MLflow trace flush failed: %O", err); + } +} diff --git a/packages/appkit/src/plugins/agents/schemas.ts b/packages/appkit/src/plugins/agents/schemas.ts index 395a8eab3..ff414fc2d 100644 --- a/packages/appkit/src/plugins/agents/schemas.ts +++ b/packages/appkit/src/plugins/agents/schemas.ts @@ -34,6 +34,11 @@ export const chatRequestSchema = z.object({ ), threadId: z.string().optional(), agent: z.string().optional(), + /** + * MLflow run id to link this turn's trace to (via `mlflow.sourceRun`), set by + * the eval runner so each case's trace shows under the evaluation run. + */ + mlflowRunId: z.string().optional(), }); const messageItemSchema = z.object({ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0d08e5808..4042be1b7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -317,6 +317,9 @@ 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 @@ -1942,6 +1945,10 @@ packages: engines: {node: ^20 || ^22 || ^24 || ^25, pnpm: '>=10'} hasBin: true + '@databricks/sdk-experimental@0.15.0': + resolution: {integrity: sha512-HkoMiF7dNDt6WRW0xhi7oPlBJQfxJ9suJhEZRFt08VwLMaWcw2PiF8monfHlkD4lkufEYV6CTxi5njQkciqiHA==} + engines: {node: '>=22.0', npm: '>=10.0.0'} + '@databricks/sdk-experimental@0.17.0': resolution: {integrity: sha512-dOJIt4F2nBk6HKObnv7Xbmy/qLYTy2835qhXSuW0Qw1QAXui9plmCet1KqG3yeQcMTyncWGbnhjGdQi8GEGQSA==} engines: {node: '>=22.0', npm: '>=10.0.0'} @@ -2771,6 +2778,10 @@ packages: '@octokit/types@16.0.0': resolution: {integrity: sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==} + '@opentelemetry/api-logs@0.205.0': + resolution: {integrity: sha512-wBlPk1nFB37Hsm+3Qy73yQSobVn28F4isnWIBvKpd5IUH/eat8bwcL02H9yzmHyyPmukeccSl2mbN5sDQZYnPg==} + engines: {node: '>=8.0.0'} + '@opentelemetry/api-logs@0.208.0': resolution: {integrity: sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg==} engines: {node: '>=8.0.0'} @@ -2786,6 +2797,12 @@ packages: '@opentelemetry/api': ^1.4.1 '@opentelemetry/core': ^2.0.0 + '@opentelemetry/context-async-hooks@2.1.0': + resolution: {integrity: sha512-zOyetmZppnwTyPrt4S7jMfXiSX9yyfF0hxlA8B5oo2TtKl+/RGCy7fi4DrBfIf3lCPrkKsRBWZZD7RFojK7FDg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/context-async-hooks@2.2.0': resolution: {integrity: sha512-qRkLWiUEZNAmYapZ7KGS5C4OmBLcP/H2foXeOEaowYCR0wi89fHejrfYfbuLVCMLp/dWZXKvQusdbUEZjERfwQ==} engines: {node: ^18.19.0 || >=20.6.0} @@ -2798,6 +2815,12 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/core@2.1.0': + resolution: {integrity: sha512-RMEtHsxJs/GiHHxYT58IY57UXAQTuUnZVco6ymDEqTNlJKTimM4qPUPVe8InNFyBjhHBEAx4k3Q8LtNayBsbUQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/core@2.2.0': resolution: {integrity: sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==} engines: {node: ^18.19.0 || >=20.6.0} @@ -2810,66 +2833,132 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/exporter-logs-otlp-grpc@0.205.0': + resolution: {integrity: sha512-jQlw7OHbqZ8zPt+pOrW2KGN7T55P50e3NXBMr4ckPOF+DWDwSy4W7mkG09GpYWlQAQ5C9BXg5gfUlv5ldTgWsw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/exporter-logs-otlp-grpc@0.208.0': resolution: {integrity: sha512-AmZDKFzbq/idME/yq68M155CJW1y056MNBekH9OZewiZKaqgwYN4VYfn3mXVPftYsfrCM2r4V6tS8H2LmfiDCg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 + '@opentelemetry/exporter-logs-otlp-http@0.205.0': + resolution: {integrity: sha512-5JteMyVWiro4ghF0tHQjfE6OJcF7UBUcoEqX3UIQ5jutKP1H+fxFdyhqjjpmeHMFxzOHaYuLlNR1Bn7FOjGyJg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/exporter-logs-otlp-http@0.208.0': resolution: {integrity: sha512-jOv40Bs9jy9bZVLo/i8FwUiuCvbjWDI+ZW13wimJm4LjnlwJxGgB+N/VWOZUTpM+ah/awXeQqKdNlpLf2EjvYg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 + '@opentelemetry/exporter-logs-otlp-proto@0.205.0': + resolution: {integrity: sha512-q3VS9wS+lpZ01txKxiDGBtBpTNge3YhbVEFDgem9ZQR9eI3EZ68+9tVZH9zJcSxI37nZPJ6lEEZO58yEjYZsVA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/exporter-logs-otlp-proto@0.208.0': resolution: {integrity: sha512-Wy8dZm16AOfM7yddEzSFzutHZDZ6HspKUODSUJVjyhnZFMBojWDjSNgduyCMlw6qaxJYz0dlb0OEcb4Eme+BfQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 + '@opentelemetry/exporter-metrics-otlp-grpc@0.205.0': + resolution: {integrity: sha512-1Vxlo4lUwqSKYX+phFkXHKYR3DolFHxCku6lVMP1H8sVE3oj4wwmwxMzDsJ7zF+sXd8M0FCr+ckK4SnNNKkV+w==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/exporter-metrics-otlp-grpc@0.208.0': resolution: {integrity: sha512-YbEnk7jjYmvhIwp2xJGkEvdgnayrA2QSr28R1LR1klDPvCxsoQPxE6TokDbQpoCEhD3+KmJVEXfb4EeEQxjymg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 + '@opentelemetry/exporter-metrics-otlp-http@0.205.0': + resolution: {integrity: sha512-fFxNQ/HbbpLmh1pgU6HUVbFD1kNIjrkoluoKJkh88+gnmpFD92kMQ8WFNjPnSbjg2mNVnEkeKXgCYEowNW+p1w==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/exporter-metrics-otlp-http@0.208.0': resolution: {integrity: sha512-QZ3TrI90Y0i1ezWQdvreryjY0a5TK4J9gyDLIyhLBwV+EQUvyp5wR7TFPKCAexD4TDSWM0t3ulQDbYYjVtzTyA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 + '@opentelemetry/exporter-metrics-otlp-proto@0.205.0': + resolution: {integrity: sha512-qIbNnedw9QfFjwpx4NQvdgjK3j3R2kWH/2T+7WXAm1IfMFe9fwatYxE61i7li4CIJKf8HgUC3GS8Du0C3D+AuQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/exporter-metrics-otlp-proto@0.208.0': resolution: {integrity: sha512-CvvVD5kRDmRB/uSMalvEF6kiamY02pB46YAqclHtfjJccNZFxbkkXkMMmcJ7NgBFa5THmQBNVQ2AHyX29nRxOw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 + '@opentelemetry/exporter-prometheus@0.205.0': + resolution: {integrity: sha512-xsot/Qm9VLDTag4GEwAunD1XR1U8eBHTLAgO7IZNo2JuD/c/vL7xmDP7mQIUr6Lk3gtj/yGGIR2h3vhTeVzv4w==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/exporter-prometheus@0.208.0': resolution: {integrity: sha512-Rgws8GfIfq2iNWCD3G1dTD9xwYsCof1+tc5S5X0Ahdb5CrAPE+k5P70XCWHqrFFurVCcKaHLJ/6DjIBHWVfLiw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 + '@opentelemetry/exporter-trace-otlp-grpc@0.205.0': + resolution: {integrity: sha512-ZBksUk84CcQOuDJB65yu5A4PORkC4qEsskNwCrPZxDLeWjPOFZNSWt0E0jQxKCY8PskLhjNXJYo12YaqsYvGFA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/exporter-trace-otlp-grpc@0.208.0': resolution: {integrity: sha512-E/eNdcqVUTAT7BC+e8VOw/krqb+5rjzYkztMZ/o+eyJl+iEY6PfczPXpwWuICwvsm0SIhBoh9hmYED5Vh5RwIw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 + '@opentelemetry/exporter-trace-otlp-http@0.205.0': + resolution: {integrity: sha512-vr2bwwPCSc9u7rbKc74jR+DXFvyMFQo9o5zs+H/fgbK672Whw/1izUKVf+xfWOdJOvuwTnfWxy+VAY+4TSo74Q==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/exporter-trace-otlp-http@0.208.0': resolution: {integrity: sha512-jbzDw1q+BkwKFq9yxhjAJ9rjKldbt5AgIy1gmEIJjEV/WRxQ3B6HcLVkwbjJ3RcMif86BDNKR846KJ0tY0aOJA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 + '@opentelemetry/exporter-trace-otlp-proto@0.205.0': + resolution: {integrity: sha512-bGtFzqiENO2GpJk988mOBMe0MfeNpTQjbLm/LBijas6VRyEDQarUzdBHpFlu89A25k1+BCntdWGsWTa9Ai4FyA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/exporter-trace-otlp-proto@0.208.0': resolution: {integrity: sha512-q844Jc3ApkZVdWYd5OAl+an3n1XXf3RWHa3Zgmnhw3HpsM3VluEKHckUUEqHPzbwDUx2lhPRVkqK7LsJ/CbDzA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 + '@opentelemetry/exporter-zipkin@2.1.0': + resolution: {integrity: sha512-0mEI0VDZrrX9t5RE1FhAyGz+jAGt96HSuXu73leswtY3L5YZD11gtcpARY2KAx/s6Z2+rj5Mhj566JsI2C7mfA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + '@opentelemetry/exporter-zipkin@2.2.0': resolution: {integrity: sha512-VV4QzhGCT7cWrGasBWxelBjqbNBbyHicWWS/66KoZoe9BzYwFB72SH2/kkc4uAviQlO8iwv2okIJy+/jqqEHTg==} engines: {node: ^18.19.0 || >=20.6.0} @@ -3122,36 +3211,72 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 + '@opentelemetry/instrumentation@0.205.0': + resolution: {integrity: sha512-cgvm7tvQdu9Qo7VurJP84wJ7ZV9F6WqDDGZpUc6rUEXwjV7/bXWs0kaYp9v+1Vh1+3TZCD3i6j/lUBcPhu8NhA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/instrumentation@0.208.0': resolution: {integrity: sha512-Eju0L4qWcQS+oXxi6pgh7zvE2byogAkcsVv0OjHF/97iOz1N/aKE6etSGowYkie+YA1uo6DNwdSxaaNnLvcRlA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 + '@opentelemetry/otlp-exporter-base@0.205.0': + resolution: {integrity: sha512-2MN0C1IiKyo34M6NZzD6P9Nv9Dfuz3OJ3rkZwzFmF6xzjDfqqCTatc9v1EpNfaP55iDOCLHFyYNCgs61FFgtUQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/otlp-exporter-base@0.208.0': resolution: {integrity: sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 + '@opentelemetry/otlp-grpc-exporter-base@0.205.0': + resolution: {integrity: sha512-AeuLfrciGYffqsp4EUTdYYc6Ee2BQS+hr08mHZk1C524SFWx0WnfcTnV0NFXbVURUNU6DZu1DhS89zRRrcx/hg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/otlp-grpc-exporter-base@0.208.0': resolution: {integrity: sha512-fGvAg3zb8fC0oJAzfz7PQppADI2HYB7TSt/XoCaBJFi1mSquNUjtHXEoviMgObLAa1NRIgOC1lsV1OUKi+9+lQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 + '@opentelemetry/otlp-transformer@0.205.0': + resolution: {integrity: sha512-KmObgqPtk9k/XTlWPJHdMbGCylRAmMJNXIRh6VYJmvlRDMfe+DonH41G7eenG8t4FXn3fxOGh14o/WiMRR6vPg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + '@opentelemetry/otlp-transformer@0.208.0': resolution: {integrity: sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 + '@opentelemetry/propagator-b3@2.1.0': + resolution: {integrity: sha512-yOdHmFseIChYanddMMz0mJIFQHyjwbNhoxc65fEAA8yanxcBPwoFDoh1+WBUWAO/Z0NRgk+k87d+aFIzAZhcBw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/propagator-b3@2.2.0': resolution: {integrity: sha512-9CrbTLFi5Ee4uepxg2qlpQIozoJuoAZU5sKMx0Mn7Oh+p7UrgCiEV6C02FOxxdYVRRFQVCinYR8Kf6eMSQsIsw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/propagator-jaeger@2.1.0': + resolution: {integrity: sha512-QYo7vLyMjrBCUTpwQBF/e+rvP7oGskrSELGxhSvLj5gpM0az9oJnu/0O4l2Nm7LEhAff80ntRYKkAcSwVgvSVQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/propagator-jaeger@2.2.0': resolution: {integrity: sha512-FfeOHOrdhiNzecoB1jZKp2fybqmqMPJUXe2ZOydP7QzmTPYcfPeuaclTLYVhK3HyJf71kt8sTl92nV4YIaLaKA==} engines: {node: ^18.19.0 || >=20.6.0} @@ -3192,6 +3317,12 @@ packages: peerDependencies: '@opentelemetry/api': ^1.0.0 + '@opentelemetry/resources@2.1.0': + resolution: {integrity: sha512-1CJjf3LCvoefUOgegxi8h6r4B/wLSzInyhGP2UmIBYNlo4Qk5CZ73e1eEyWmfXvFtm1ybkmfb2DqWvspsYLrWw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@opentelemetry/resources@2.2.0': resolution: {integrity: sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==} engines: {node: ^18.19.0 || >=20.6.0} @@ -3204,24 +3335,48 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@opentelemetry/sdk-logs@0.205.0': + resolution: {integrity: sha512-nyqhNQ6eEzPWQU60Nc7+A5LIq8fz3UeIzdEVBQYefB4+msJZ2vuVtRuk9KxPMw1uHoHDtYEwkr2Ct0iG29jU8w==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.4.0 <1.10.0' + '@opentelemetry/sdk-logs@0.208.0': resolution: {integrity: sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.4.0 <1.10.0' + '@opentelemetry/sdk-metrics@2.1.0': + resolution: {integrity: sha512-J9QX459mzqHLL9Y6FZ4wQPRZG4TOpMCyPOh6mkr/humxE1W2S3Bvf4i75yiMW9uyed2Kf5rxmLhTm/UK8vNkAw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.9.0 <1.10.0' + '@opentelemetry/sdk-metrics@2.2.0': resolution: {integrity: sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.9.0 <1.10.0' + '@opentelemetry/sdk-node@0.205.0': + resolution: {integrity: sha512-Y4Wcs8scj/Wy1u61pX1ggqPXPtCsGaqx/UnFu7BtRQE1zCQR+b0h56K7I0jz7U2bRlPUZIFdnNLtoaJSMNzz2g==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@opentelemetry/sdk-node@0.208.0': resolution: {integrity: sha512-pbAqpZ7zTMFuTf3YecYsecsto/mheuvnK2a/jgstsE5ynWotBjgF5bnz5500W9Xl2LeUfg04WMt63TWtAgzRMw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@opentelemetry/sdk-trace-base@2.1.0': + resolution: {integrity: sha512-uTX9FBlVQm4S2gVQO1sb5qyBLq/FPjbp+tmGoxu4tIgtYGmBYB44+KX/725RFDe30yBSaA9Ml9fqphe1hbUyLQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@opentelemetry/sdk-trace-base@2.2.0': resolution: {integrity: sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==} engines: {node: ^18.19.0 || >=20.6.0} @@ -3234,6 +3389,12 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' + '@opentelemetry/sdk-trace-node@2.1.0': + resolution: {integrity: sha512-SvVlBFc/jI96u/mmlKm86n9BbTCbQ35nsPoOohqJX6DXH92K0kTe73zGY5r8xoI1QkjR9PizszVJLzMC966y9Q==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + '@opentelemetry/sdk-trace-node@2.2.0': resolution: {integrity: sha512-+OaRja3f0IqGG2kptVeYsrZQK9nKRSpfFrKtRBq4uh6nIB8bTBgaGvYQrQoRrQWQMA5dK5yLhDMDc0dvYvCOIQ==} engines: {node: ^18.19.0 || >=20.6.0} @@ -7117,6 +7278,9 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} @@ -7804,6 +7968,9 @@ packages: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} + import-in-the-middle@1.15.0: + resolution: {integrity: sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA==} + import-in-the-middle@2.0.0: resolution: {integrity: sha512-yNZhyQYqXpkT0AKq3F3KLasUSK4fHvebNH5hOsKQw2dhGSALvQ4U0BqUc5suziKvydO5u5hgN2hy1RJaho8U5A==} @@ -7851,6 +8018,10 @@ packages: resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + ini@5.0.0: + resolution: {integrity: sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw==} + engines: {node: ^18.17.0 || >=20.5.0} + ini@6.0.0: resolution: {integrity: sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==} engines: {node: ^20.17.0 || >=22.9.0} @@ -8899,6 +9070,10 @@ 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==} @@ -10246,6 +10421,10 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + require-in-the-middle@7.5.2: + resolution: {integrity: sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==} + engines: {node: '>=8.6.0'} + require-in-the-middle@8.0.1: resolution: {integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==} engines: {node: '>=9.3.0 || >=8.10.0 <9.0.0'} @@ -13613,6 +13792,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@databricks/sdk-experimental@0.15.0': + dependencies: + google-auth-library: 10.5.0 + ini: 6.0.0 + reflect-metadata: 0.2.2 + semver: 7.7.3 + transitivePeerDependencies: + - supports-color + '@databricks/sdk-experimental@0.17.0': dependencies: google-auth-library: 10.5.0 @@ -15043,6 +15231,10 @@ snapshots: dependencies: '@octokit/openapi-types': 27.0.0 + '@opentelemetry/api-logs@0.205.0': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs@0.208.0': dependencies: '@opentelemetry/api': 1.9.0 @@ -15106,6 +15298,10 @@ snapshots: - encoding - supports-color + '@opentelemetry/context-async-hooks@2.1.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -15114,6 +15310,11 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 + '@opentelemetry/core@2.1.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -15124,6 +15325,16 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/exporter-logs-otlp-grpc@0.205.0(@opentelemetry/api@1.9.0)': + dependencies: + '@grpc/grpc-js': 1.14.1 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-grpc@0.208.0(@opentelemetry/api@1.9.0)': dependencies: '@grpc/grpc-js': 1.14.1 @@ -15134,6 +15345,15 @@ snapshots: '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-http@0.205.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.205.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-http@0.208.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -15143,6 +15363,17 @@ snapshots: '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-proto@0.205.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.205.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-proto@0.208.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -15154,6 +15385,18 @@ snapshots: '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-grpc@0.205.0(@opentelemetry/api@1.9.0)': + dependencies: + '@grpc/grpc-js': 1.14.1 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-grpc@0.208.0(@opentelemetry/api@1.9.0)': dependencies: '@grpc/grpc-js': 1.14.1 @@ -15166,6 +15409,15 @@ snapshots: '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-metrics': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http@0.205.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http@0.208.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -15175,6 +15427,16 @@ snapshots: '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-metrics': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-proto@0.205.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-proto@0.208.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -15185,6 +15447,13 @@ snapshots: '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-metrics': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-prometheus@0.205.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-prometheus@0.208.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -15192,6 +15461,17 @@ snapshots: '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-metrics': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-grpc@0.205.0(@opentelemetry/api@1.9.0)': + dependencies: + '@grpc/grpc-js': 1.14.1 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-grpc@0.208.0(@opentelemetry/api@1.9.0)': dependencies: '@grpc/grpc-js': 1.14.1 @@ -15203,6 +15483,15 @@ snapshots: '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-http@0.205.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-http@0.208.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -15212,6 +15501,15 @@ snapshots: '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-proto@0.205.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-proto@0.208.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -15221,6 +15519,14 @@ snapshots: '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-zipkin@2.1.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/exporter-zipkin@2.2.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -15570,6 +15876,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@opentelemetry/instrumentation@0.205.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.205.0 + import-in-the-middle: 1.15.0 + require-in-the-middle: 7.5.2 + transitivePeerDependencies: + - supports-color + '@opentelemetry/instrumentation@0.208.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -15579,12 +15894,26 @@ snapshots: transitivePeerDependencies: - supports-color + '@opentelemetry/otlp-exporter-base@0.205.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base@0.208.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base@0.205.0(@opentelemetry/api@1.9.0)': + dependencies: + '@grpc/grpc-js': 1.14.1 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base@0.208.0(@opentelemetry/api@1.9.0)': dependencies: '@grpc/grpc-js': 1.14.1 @@ -15593,6 +15922,17 @@ snapshots: '@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.0) '@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer@0.205.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.205.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.1.0(@opentelemetry/api@1.9.0) + protobufjs: 7.5.4 + '@opentelemetry/otlp-transformer@0.208.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -15604,11 +15944,21 @@ snapshots: '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) protobufjs: 7.5.4 + '@opentelemetry/propagator-b3@2.1.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-b3@2.2.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-jaeger@2.1.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-jaeger@2.2.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -15652,6 +16002,12 @@ snapshots: - encoding - supports-color + '@opentelemetry/resources@2.1.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/resources@2.2.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -15664,6 +16020,13 @@ snapshots: '@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/sdk-logs@0.205.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.205.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs@0.208.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -15671,12 +16034,46 @@ snapshots: '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics@2.1.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics@2.2.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-node@0.205.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.205.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-grpc': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-http': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-proto': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-grpc': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-proto': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-prometheus': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-grpc': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-http': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-proto': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-zipkin': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-b3': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-jaeger': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.205.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-node': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.38.0 + transitivePeerDependencies: + - supports-color + '@opentelemetry/sdk-node@0.208.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -15705,6 +16102,13 @@ snapshots: transitivePeerDependencies: - supports-color + '@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -15719,6 +16123,13 @@ snapshots: '@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/sdk-trace-node@2.1.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/context-async-hooks': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-node@2.2.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 @@ -19677,6 +20088,8 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-safe-stringify@2.1.1: {} + fast-uri@3.1.0: {} fastq@1.19.1: @@ -20611,6 +21024,13 @@ snapshots: parent-module: 1.0.1 resolve-from: 4.0.0 + import-in-the-middle@1.15.0: + dependencies: + acorn: 8.15.0 + acorn-import-attributes: 1.9.5(acorn@8.15.0) + cjs-module-lexer: 1.4.3 + module-details-from-path: 1.0.4 + import-in-the-middle@2.0.0: dependencies: acorn: 8.15.0 @@ -20642,6 +21062,8 @@ snapshots: ini@4.1.1: {} + ini@5.0.0: {} + ini@6.0.0: {} inline-style-parser@0.2.7: {} @@ -21932,6 +22354,17 @@ 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 @@ -23495,6 +23928,14 @@ snapshots: require-from-string@2.0.2: {} + require-in-the-middle@7.5.2: + dependencies: + debug: 4.4.3 + module-details-from-path: 1.0.4 + resolve: 1.22.10 + transitivePeerDependencies: + - supports-color + require-in-the-middle@8.0.1: dependencies: debug: 4.4.3 diff --git a/template/appkit.plugins.json b/template/appkit.plugins.json index a4aa1c26e..5675a1687 100644 --- a/template/appkit.plugins.json +++ b/template/appkit.plugins.json @@ -23,6 +23,20 @@ "origin": "user" } } + }, + { + "type": "experiment", + "alias": "MLflow experiment for agent traces", + "resourceKey": "agents-mlflow-experiment", + "description": "When bound, agent turns and tool calls are traced to this MLflow experiment via OpenTelemetry. Tracing is a no-op when unset.", + "permission": "CAN_EDIT", + "fields": { + "experimentId": { + "env": "MLFLOW_EXPERIMENT_ID", + "description": "MLflow experiment id traces are logged to", + "origin": "user" + } + } } ] }, From 3a17237d497c996d4edfbfb09c4e79330cd863ff Mon Sep 17 00:00:00 2001 From: MarioCadenas Date: Mon, 20 Jul 2026 15:42:37 +0200 Subject: [PATCH 2/7] refactor(appkit): simplify agent span tracing into traceAgent/traceTool Signed-off-by: MarioCadenas --- packages/appkit/src/plugins/agents/agents.ts | 128 +++++++++---------- packages/appkit/src/plugins/agents/mlflow.ts | 69 +++++++--- 2 files changed, 110 insertions(+), 87 deletions(-) diff --git a/packages/appkit/src/plugins/agents/agents.ts b/packages/appkit/src/plugins/agents/agents.ts index 16289be30..09a47e353 100644 --- a/packages/appkit/src/plugins/agents/agents.ts +++ b/packages/appkit/src/plugins/agents/agents.ts @@ -56,7 +56,8 @@ import { currentTraceId, initAgentTracing, linkTraceToRun, - withAgentSpan, + traceAgent, + traceTool, } from "./mlflow"; import { approvalRequestSchema, @@ -1084,16 +1085,13 @@ export class AgentsPlugin extends Plugin implements ToolProvider { // Root MLflow span for the turn (AGENT). Tool calls dispatched while // the adapter streams nest under it via the SDK's active context. // No-op passthrough unless an MLflow experiment is bound. - await withAgentSpan( + await traceAgent( + registered.name ?? "agent", { - name: registered.name ?? "agent", - type: "AGENT", - inputs: { - messages: thread.messages.map((m) => ({ - role: m.role, - content: m.content, - })), - }, + messages: thread.messages.map((m) => ({ + role: m.role, + content: m.content, + })), }, async (span) => { // Link this turn's trace to an eval run when the eval runner @@ -1150,7 +1148,7 @@ export class AgentsPlugin extends Plugin implements ToolProvider { }); if (fullContent) { - span?.setOutputs({ role: "assistant", content: fullContent }); + span.setOutputs({ role: "assistant", content: fullContent }); await this.threadStore.addMessage(thread.id, userId, { id: randomUUID(), role: "assistant", @@ -1438,70 +1436,58 @@ export class AgentsPlugin extends Plugin implements ToolProvider { // MLflow TOOL span — nests under the turn's AGENT span. Wraps the // execution only (not the approval wait above, which is human latency). - const toolResult = await withAgentSpan( - { name, type: "TOOL", inputs: args }, - async (span) => { - let result: unknown; - if (entry.source === "toolkit") { - if (!this.context) { - throw new Error( - "Plugin tool execution requires PluginContext; this should never happen through createApp", - ); - } - result = await this.context.executeTool( - runState.req, - entry.pluginName, - entry.localName, - args, - runState.signal, - runState.limits.toolCallTimeoutMs, - ); - } else if (entry.source === "function") { - // Function tools declare their parameters as a JSON-object schema, - // so adapters always serialize `args` as an object. A non-object - // value here means the upstream model emitted malformed tool-call - // JSON; surface a clear error rather than silently passing through - // a wrong-shape value the tool will then choke on. - if ( - typeof args !== "object" || - args === null || - Array.isArray(args) - ) { - throw new Error( - `Function tool '${name}' received non-object arguments (got ${args === null ? "null" : Array.isArray(args) ? "array" : typeof args}); expected a JSON object.`, - ); - } - result = await entry.functionTool.execute( - args as Record, + const toolResult = await traceTool(name, args, async () => { + let result: unknown; + if (entry.source === "toolkit") { + if (!this.context) { + throw new Error( + "Plugin tool execution requires PluginContext; this should never happen through createApp", ); - } else if (entry.source === "mcp") { - if (!this.mcpClient) throw new Error("MCP client not connected"); - const oboToken = runState.req.headers["x-forwarded-access-token"]; - const mcpAuth = - typeof oboToken === "string" - ? { Authorization: `Bearer ${oboToken}` } - : undefined; - result = await this.mcpClient.callTool( - entry.mcpToolName, - args, - mcpAuth, - ); - } else if (entry.source === "subagent") { - const childAgent = this.agents.get(entry.agentName); - if (!childAgent) - throw new Error(`Sub-agent not found: ${entry.agentName}`); - result = await this.runSubAgent( - runState, - childAgent, - args, - depth + 1, + } + result = await this.context.executeTool( + runState.req, + entry.pluginName, + entry.localName, + args, + runState.signal, + runState.limits.toolCallTimeoutMs, + ); + } else if (entry.source === "function") { + // Function tools declare their parameters as a JSON-object schema, + // so adapters always serialize `args` as an object. A non-object + // value here means the upstream model emitted malformed tool-call + // JSON; surface a clear error rather than silently passing through + // a wrong-shape value the tool will then choke on. + if (typeof args !== "object" || args === null || Array.isArray(args)) { + throw new Error( + `Function tool '${name}' received non-object arguments (got ${args === null ? "null" : Array.isArray(args) ? "array" : typeof args}); expected a JSON object.`, ); } + result = await entry.functionTool.execute( + args as Record, + ); + } else if (entry.source === "mcp") { + if (!this.mcpClient) throw new Error("MCP client not connected"); + const oboToken = runState.req.headers["x-forwarded-access-token"]; + const mcpAuth = + typeof oboToken === "string" + ? { Authorization: `Bearer ${oboToken}` } + : undefined; + result = await this.mcpClient.callTool( + entry.mcpToolName, + args, + mcpAuth, + ); + } else if (entry.source === "subagent") { + const childAgent = this.agents.get(entry.agentName); + if (!childAgent) + throw new Error(`Sub-agent not found: ${entry.agentName}`); + result = await this.runSubAgent(runState, childAgent, args, depth + 1); + } - span?.setOutputs(result); - return result; - }, - ); + // Auto-captured as the TOOL span's outputs by `traceTool`. + return result; + }); return normalizeToolResult(toolResult); } diff --git a/packages/appkit/src/plugins/agents/mlflow.ts b/packages/appkit/src/plugins/agents/mlflow.ts index 4d3230d08..0e17056e4 100644 --- a/packages/appkit/src/plugins/agents/mlflow.ts +++ b/packages/appkit/src/plugins/agents/mlflow.ts @@ -1,4 +1,3 @@ -import type { LiveSpan } from "mlflow-tracing"; import { createLogger } from "../../logging/logger"; const logger = createLogger("agents"); @@ -62,30 +61,68 @@ export async function initAgentTracing(): Promise { } } -export type AgentSpanType = "AGENT" | "TOOL"; +/** + * Records a span's outputs. Callers get one from `traceAgent`/`traceTool`; + * it's a no-op when tracing is disabled, so call sites never branch on it. + */ +export interface SpanRecorder { + setOutputs(outputs: unknown): void; +} + +const noopRecorder: SpanRecorder = { setOutputs() {} }; /** - * Run `fn` inside an MLflow span when tracing is enabled, otherwise just run it - * (zero overhead). Spans auto-nest via the SDK's active context, so a TOOL span - * opened inside an AGENT span's callback becomes its child. The callback gets - * the span (or `undefined` when disabled) to record outputs. + * 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 + * context, so a TOOL span opened inside an AGENT span's callback becomes its + * child. The callback's resolved value is recorded as the span's outputs unless + * it called `setOutputs` first; return `undefined` (or set outputs explicitly) + * when the return value isn't the output you want traced. */ -export async function withAgentSpan( - options: { name: string; type: AgentSpanType; inputs?: unknown }, - fn: (span?: LiveSpan) => Promise, +async function trace( + spanType: "AGENT" | "TOOL", + name: string, + inputs: unknown, + fn: (span: SpanRecorder) => Promise, ): Promise { - if (!enabled || !mlflow) return fn(); - const spanType = - options.type === "AGENT" ? mlflow.SpanType.AGENT : mlflow.SpanType.TOOL; + if (!enabled || !mlflow) return fn(noopRecorder); + const type = + spanType === "AGENT" ? mlflow.SpanType.AGENT : mlflow.SpanType.TOOL; return await mlflow.withSpan( - (span) => { - if (options.inputs !== undefined) span.setInputs(options.inputs); - return fn(span); + async (span) => { + if (inputs !== undefined) span.setInputs(inputs); + let outputsSet = false; + const result = await fn({ + setOutputs(outputs) { + outputsSet = true; + span.setOutputs(outputs); + }, + }); + if (!outputsSet && result !== undefined) span.setOutputs(result); + return result; }, - { name: options.name, spanType }, + { name, spanType: type }, ); } +/** Trace a turn's root AGENT span. See {@link trace}. */ +export function traceAgent( + name: string, + inputs: unknown, + fn: (span: SpanRecorder) => Promise, +): Promise { + return trace("AGENT", name, inputs, fn); +} + +/** Trace a TOOL span, nested under the active AGENT span. See {@link trace}. */ +export function traceTool( + name: string, + inputs: unknown, + fn: (span: SpanRecorder) => Promise, +): Promise { + return trace("TOOL", name, inputs, fn); +} + /** * The MLflow trace id for the active turn, when tracing is enabled. Read inside * an agent span so eval runs can correlate the turn to its trace and attach From af64fff1483615e181fc2456ad9c48aae3289789 Mon Sep 17 00:00:00 2001 From: MarioCadenas Date: Mon, 20 Jul 2026 16:19:15 +0200 Subject: [PATCH 3/7] refactor(appkit): drop unused flushAgentTraces Signed-off-by: MarioCadenas --- packages/appkit/src/plugins/agents/mlflow.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/packages/appkit/src/plugins/agents/mlflow.ts b/packages/appkit/src/plugins/agents/mlflow.ts index 0e17056e4..1dc877bde 100644 --- a/packages/appkit/src/plugins/agents/mlflow.ts +++ b/packages/appkit/src/plugins/agents/mlflow.ts @@ -151,13 +151,3 @@ export function linkTraceToRun(runId: string): void { logger.warn("Failed to link trace to run %s: %O", runId, err); } } - -/** Flush buffered traces (e.g. on shutdown). No-op when tracing is disabled. */ -export async function flushAgentTraces(): Promise { - if (!enabled || !mlflow) return; - try { - await mlflow.flushTraces(); - } catch (err) { - logger.warn("MLflow trace flush failed: %O", err); - } -} From 540dd02649fd685c2800f7acd2076c38a6c0311e Mon Sep 17 00:00:00 2001 From: MarioCadenas Date: Mon, 20 Jul 2026 16:34:07 +0200 Subject: [PATCH 4/7] refactor(appkit): trim redundant tracing comments Signed-off-by: MarioCadenas --- packages/appkit/src/plugins/agents/agents.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/appkit/src/plugins/agents/agents.ts b/packages/appkit/src/plugins/agents/agents.ts index 09a47e353..7e6e0ea26 100644 --- a/packages/appkit/src/plugins/agents/agents.ts +++ b/packages/appkit/src/plugins/agents/agents.ts @@ -1082,9 +1082,7 @@ export class AgentsPlugin extends Plugin implements ToolProvider { outboundEvents.push(evt); } - // Root MLflow span for the turn (AGENT). Tool calls dispatched while - // the adapter streams nest under it via the SDK's active context. - // No-op passthrough unless an MLflow experiment is bound. + // Root MLflow span for the turn; tool-call spans nest under it. await traceAgent( registered.name ?? "agent", { @@ -1434,8 +1432,8 @@ export class AgentsPlugin extends Plugin implements ToolProvider { } } - // MLflow TOOL span — nests under the turn's AGENT span. Wraps the - // execution only (not the approval wait above, which is human latency). + // Traced from here so the span covers execution only, not the approval + // wait above (which is human latency). const toolResult = await traceTool(name, args, async () => { let result: unknown; if (entry.source === "toolkit") { @@ -1485,7 +1483,6 @@ export class AgentsPlugin extends Plugin implements ToolProvider { result = await this.runSubAgent(runState, childAgent, args, depth + 1); } - // Auto-captured as the TOOL span's outputs by `traceTool`. return result; }); From 4122ac5e4609b3e87d4f357de444895c2a207955 Mon Sep 17 00:00:00 2001 From: MarioCadenas Date: Mon, 20 Jul 2026 17:23:13 +0200 Subject: [PATCH 5/7] chore: fixup --- packages/appkit/package.json | 2 +- pnpm-lock.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/appkit/package.json b/packages/appkit/package.json index 6a1541c35..40be21e73 100644 --- a/packages/appkit/package.json +++ b/packages/appkit/package.json @@ -83,7 +83,7 @@ "get-port": "7.2.0", "js-yaml": "4.1.1", "magic-string": "0.30.21", - "mlflow-tracing": "^0.1.3", + "mlflow-tracing": "0.1.3", "obug": "2.1.1", "pg": "8.18.0", "picocolors": "1.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4042be1b7..439b0abea 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -318,7 +318,7 @@ importers: specifier: 0.30.21 version: 0.30.21 mlflow-tracing: - specifier: ^0.1.3 + specifier: 0.1.3 version: 0.1.3 obug: specifier: 2.1.1 From b991a1efe0d609ad091892232749680c90166bdb Mon Sep 17 00:00:00 2001 From: MarioCadenas Date: Mon, 20 Jul 2026 17:44:47 +0200 Subject: [PATCH 6/7] fix(appkit): read active-span trace id and validate mlflowRunId MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit currentTraceId() used getLastActiveTraceId(), which is only set when a root span ends (on export). Read mid-turn it returned the previous turn's id — or another turn's under concurrency. Read the context-active span instead, matching linkTraceToRun. Also read mlflowRunId from the validated schema (capped at 64 chars) instead of casting raw req.body, and drop two unused imports. Adds tracing tests covering trace-id source and output auto-capture. Signed-off-by: MarioCadenas --- packages/appkit/src/plugins/agents/agents.ts | 11 +-- packages/appkit/src/plugins/agents/mlflow.ts | 12 ++- packages/appkit/src/plugins/agents/schemas.ts | 5 +- .../src/plugins/agents/tests/mlflow.test.ts | 91 +++++++++++++++++++ 4 files changed, 106 insertions(+), 13 deletions(-) create mode 100644 packages/appkit/src/plugins/agents/tests/mlflow.test.ts diff --git a/packages/appkit/src/plugins/agents/agents.ts b/packages/appkit/src/plugins/agents/agents.ts index 7e6e0ea26..c45831e98 100644 --- a/packages/appkit/src/plugins/agents/agents.ts +++ b/packages/appkit/src/plugins/agents/agents.ts @@ -4,7 +4,6 @@ import type express from "express"; import pc from "picocolors"; import type { AgentAdapter, - AgentEvent, AgentRunContext, AgentToolDefinition, IAppRouter, @@ -17,7 +16,6 @@ import type { ToolProvider, } from "shared"; import { AppKitMcpClient, buildMcpHostPolicy } from "../../connectors/mcp"; -import { getWorkspaceClient } from "../../context"; import { consumeAdapterStream } from "../../core/agent/consume-adapter-stream"; import { loadAgentsFromDir } from "../../core/agent/load-agents"; import { normalizeToolResult } from "../../core/agent/normalize-result"; @@ -856,7 +854,7 @@ export class AgentsPlugin extends Plugin implements ToolProvider { }); return; } - const { message, threadId, agent: agentName } = parsed.data; + const { message, threadId, agent: agentName, mlflowRunId } = parsed.data; const registered = this.resolveAgent(agentName); if (!registered) { @@ -910,7 +908,7 @@ export class AgentsPlugin extends Plugin implements ToolProvider { res.status(500).json({ error: "Thread operation failed" }); return; } - return this._streamAgent(req, res, registered, thread, userId); + return this._streamAgent(req, res, registered, thread, userId, mlflowRunId); } /** @@ -1037,6 +1035,7 @@ export class AgentsPlugin extends Plugin implements ToolProvider { registered: RegisteredAgent, thread: Thread, userId: string, + mlflowRunId?: string, ): Promise { const abortController = new AbortController(); const signal = abortController.signal; @@ -1094,9 +1093,7 @@ export class AgentsPlugin extends Plugin implements ToolProvider { async (span) => { // Link this turn's trace to an eval run when the eval runner // supplied one, so the trace shows under the MLflow evaluation run. - const evalRunId = (req.body as { mlflowRunId?: string }) - ?.mlflowRunId; - if (evalRunId) linkTraceToRun(evalRunId); + if (mlflowRunId) linkTraceToRun(mlflowRunId); const pluginNames = this.context ? this.context diff --git a/packages/appkit/src/plugins/agents/mlflow.ts b/packages/appkit/src/plugins/agents/mlflow.ts index 1dc877bde..cd48b8190 100644 --- a/packages/appkit/src/plugins/agents/mlflow.ts +++ b/packages/appkit/src/plugins/agents/mlflow.ts @@ -124,14 +124,18 @@ export function traceTool( } /** - * The MLflow trace id for the active turn, when tracing is enabled. Read inside - * an agent span so eval runs can correlate the turn to its trace and attach - * assessments. Returns undefined when tracing is off. + * The MLflow trace id for the active turn, when tracing is enabled. Must be + * read inside an agent span so eval runs can correlate the turn to its trace + * and attach assessments. Returns undefined when tracing is off. + * + * Reads the context-active span rather than `getLastActiveTraceId()`: the + * latter is only populated when a root span *ends* (on export), so mid-turn it + * returns the previous turn's id — or, under concurrent turns, another turn's. */ export function currentTraceId(): string | undefined { if (!enabled || !mlflow) return undefined; try { - return mlflow.getLastActiveTraceId(); + return mlflow.getCurrentActiveSpan()?.traceId; } catch { return undefined; } diff --git a/packages/appkit/src/plugins/agents/schemas.ts b/packages/appkit/src/plugins/agents/schemas.ts index ff414fc2d..dd76d463b 100644 --- a/packages/appkit/src/plugins/agents/schemas.ts +++ b/packages/appkit/src/plugins/agents/schemas.ts @@ -36,9 +36,10 @@ export const chatRequestSchema = z.object({ agent: z.string().optional(), /** * MLflow run id to link this turn's trace to (via `mlflow.sourceRun`), set by - * the eval runner so each case's trace shows under the evaluation run. + * the eval runner so each case's trace shows under the evaluation run. Capped + * to a run-id-shaped length since it reaches trace metadata and logs. */ - mlflowRunId: z.string().optional(), + mlflowRunId: z.string().max(64).optional(), }); const messageItemSchema = z.object({ diff --git a/packages/appkit/src/plugins/agents/tests/mlflow.test.ts b/packages/appkit/src/plugins/agents/tests/mlflow.test.ts new file mode 100644 index 000000000..4f9f441dc --- /dev/null +++ b/packages/appkit/src/plugins/agents/tests/mlflow.test.ts @@ -0,0 +1,91 @@ +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; + +/** + * 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. + */ + +function stubSdk(overrides: Record = {}) { + const setInputs = vi.fn(); + const setOutputs = vi.fn(); + const span = { setInputs, setOutputs }; + const sdk = { + init: vi.fn(), + SpanType: { AGENT: "AGENT", TOOL: "TOOL" }, + withSpan: vi.fn(async (fn: (s: unknown) => unknown) => fn(span)), + getCurrentActiveSpan: vi.fn(() => ({ traceId: "tr-active" })), + // Must NOT be consulted by currentTraceId — it only reflects the last + // root span that *ended*, i.e. a previous/other turn. + getLastActiveTraceId: vi.fn(() => "tr-STALE"), + updateCurrentTrace: vi.fn(), + flushTraces: vi.fn(), + ...overrides, + }; + vi.doMock("mlflow-tracing", () => sdk); + return { sdk, span, setInputs, setOutputs }; +} + +describe("agent tracing (mlflow)", () => { + beforeEach(() => { + vi.resetModules(); + delete process.env.MLFLOW_EXPERIMENT_ID; + }); + + afterEach(() => { + vi.doUnmock("mlflow-tracing"); + delete process.env.MLFLOW_EXPERIMENT_ID; + }); + + test("disabled (no experiment bound): still runs fn and returns its value", async () => { + const mod = await import("../mlflow"); + await mod.initAgentTracing(); + + const fn = vi.fn(async () => "result"); + await expect(mod.traceTool("t", { a: 1 }, fn)).resolves.toBe("result"); + expect(fn).toHaveBeenCalledOnce(); + expect(mod.currentTraceId()).toBeUndefined(); + }); + + test("currentTraceId reads the context-active span, not getLastActiveTraceId", async () => { + process.env.MLFLOW_EXPERIMENT_ID = "exp-123"; + const { sdk } = stubSdk(); + const mod = await import("../mlflow"); + await mod.initAgentTracing(); + + let seen: string | undefined; + await mod.traceAgent("agent", { messages: [] }, async () => { + seen = mod.currentTraceId(); + }); + + expect(seen).toBe("tr-active"); + expect(sdk.getCurrentActiveSpan).toHaveBeenCalled(); + expect(sdk.getLastActiveTraceId).not.toHaveBeenCalled(); + }); + + test("auto-captures the callback's return value as span outputs", async () => { + process.env.MLFLOW_EXPERIMENT_ID = "exp-123"; + const { setOutputs } = stubSdk(); + const mod = await import("../mlflow"); + await mod.initAgentTracing(); + + await mod.traceTool("t", { a: 1 }, async () => ({ ok: true })); + expect(setOutputs).toHaveBeenCalledExactlyOnceWith({ ok: true }); + }); + + test("explicit setOutputs wins over auto-capture (no double-set)", async () => { + process.env.MLFLOW_EXPERIMENT_ID = "exp-123"; + const { setOutputs } = stubSdk(); + const mod = await import("../mlflow"); + await mod.initAgentTracing(); + + await mod.traceAgent("agent", { messages: [] }, async (span) => { + span.setOutputs({ role: "assistant", content: "hi" }); + return "ignored-return"; + }); + expect(setOutputs).toHaveBeenCalledExactlyOnceWith({ + role: "assistant", + content: "hi", + }); + }); +}); From 412b08d78e03204e3cc7ba055d0038364711c32f Mon Sep 17 00:00:00 2001 From: MarioCadenas Date: Tue, 21 Jul 2026 10:31:09 +0200 Subject: [PATCH 7/7] fix(appkit): trace the non-streaming invoke path /invocations and /responses drove tool calls through traceTool but had no traceAgent wrapper, so with tracing enabled each tool call became an orphan root TOOL trace instead of nesting under a turn. Wrap the adapter run in traceAgent, mirroring the streaming path, so both surfaces produce the same trace shape. Also drop a dead flushTraces mock left from the flushAgentTraces removal. Signed-off-by: MarioCadenas --- packages/appkit/src/plugins/agents/agents.ts | 93 +++++++++++-------- .../src/plugins/agents/tests/mlflow.test.ts | 1 - 2 files changed, 54 insertions(+), 40 deletions(-) diff --git a/packages/appkit/src/plugins/agents/agents.ts b/packages/appkit/src/plugins/agents/agents.ts index c45831e98..ac6f52bcf 100644 --- a/packages/appkit/src/plugins/agents/agents.ts +++ b/packages/appkit/src/plugins/agents/agents.ts @@ -1276,51 +1276,66 @@ export class AgentsPlugin extends Plugin implements ToolProvider { let fullContent = ""; try { - const pluginNames = this.context - ? this.context - .getPluginNames() - .filter((n) => n !== this.name && n !== "server") - : []; - const fullPrompt = composePromptForAgent( - registered, - this.config.baseSystemPrompt, + // Root MLflow span for the turn; tool-call spans nest under it. Mirrors + // the streaming path so the invoke surface produces the same trace shape + // instead of orphan root TOOL spans. + await traceAgent( + registered.name ?? "agent", { - agentName: registered.name, - pluginNames, - toolNames: tools.map((t) => t.name), + messages: thread.messages.map((m) => ({ + role: m.role, + content: m.content, + })), }, - ); + async (span) => { + const pluginNames = this.context + ? this.context + .getPluginNames() + .filter((n) => n !== this.name && n !== "server") + : []; + const fullPrompt = composePromptForAgent( + registered, + this.config.baseSystemPrompt, + { + agentName: registered.name, + pluginNames, + toolNames: tools.map((t) => t.name), + }, + ); - const messagesWithSystem: Message[] = [ - { - id: "system", - role: "system", - content: fullPrompt, - createdAt: new Date(), - }, - ...thread.messages, - ]; + const messagesWithSystem: Message[] = [ + { + id: "system", + role: "system", + content: fullPrompt, + createdAt: new Date(), + }, + ...thread.messages, + ]; + + const stream = registered.adapter.run( + { + messages: messagesWithSystem, + tools, + threadId: thread.id, + signal, + }, + { executeTool, signal }, + ); - const stream = registered.adapter.run( - { - messages: messagesWithSystem, - tools, - threadId: thread.id, - signal, + fullContent = await consumeAdapterStream(stream, { signal }); + + if (fullContent) { + span.setOutputs({ role: "assistant", content: fullContent }); + await this.threadStore.addMessage(thread.id, userId, { + id: randomUUID(), + role: "assistant", + content: fullContent, + createdAt: new Date(), + }); + } }, - { executeTool, signal }, ); - - fullContent = await consumeAdapterStream(stream, { signal }); - - if (fullContent) { - await this.threadStore.addMessage(thread.id, userId, { - id: randomUUID(), - role: "assistant", - content: fullContent, - createdAt: new Date(), - }); - } } catch (error) { if (signal.aborted) { res.status(499).json({ error: "Request aborted" }); diff --git a/packages/appkit/src/plugins/agents/tests/mlflow.test.ts b/packages/appkit/src/plugins/agents/tests/mlflow.test.ts index 4f9f441dc..a43c29cec 100644 --- a/packages/appkit/src/plugins/agents/tests/mlflow.test.ts +++ b/packages/appkit/src/plugins/agents/tests/mlflow.test.ts @@ -19,7 +19,6 @@ function stubSdk(overrides: Record = {}) { // root span that *ended*, i.e. a previous/other turn. getLastActiveTraceId: vi.fn(() => "tr-STALE"), updateCurrentTrace: vi.fn(), - flushTraces: vi.fn(), ...overrides, }; vi.doMock("mlflow-tracing", () => sdk);