@databricks/appkit 0.82.0 → 0.84.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +21 -1
- package/dist/agents/databricks.d.ts +15 -3
- package/dist/agents/databricks.d.ts.map +1 -1
- package/dist/agents/databricks.js +25 -8
- package/dist/agents/databricks.js.map +1 -1
- package/dist/appkit/package.js +1 -1
- package/dist/beta.d.ts +2 -2
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +6 -1
- package/dist/cache/index.js.map +1 -1
- package/dist/cli/commands/agent/eval.js +1 -1
- package/dist/cli/commands/generate-types.js +1 -1
- package/dist/cli/commands/plugin/sync/sync.js +28 -15
- package/dist/cli/commands/plugin/sync/sync.js.map +1 -1
- package/dist/cli/commands/registry/add.js +3 -11
- package/dist/cli/commands/registry/add.js.map +1 -1
- package/dist/cli/commands/registry/config-writer.js +1 -1
- package/dist/connectors/lakebase/index.js +1 -1
- package/dist/connectors/lakebase/routing-pool.d.ts +1 -1
- package/dist/connectors/lakebase/routing-pool.d.ts.map +1 -1
- package/dist/connectors/lakebase/routing-pool.js +6 -12
- package/dist/connectors/lakebase/routing-pool.js.map +1 -1
- package/dist/context/caller-context.d.ts +25 -0
- package/dist/context/caller-context.d.ts.map +1 -0
- package/dist/context/caller-context.js +21 -0
- package/dist/context/caller-context.js.map +1 -0
- package/dist/context/deprecation.js +14 -0
- package/dist/context/deprecation.js.map +1 -0
- package/dist/context/execution-context.d.ts +40 -4
- package/dist/context/execution-context.d.ts.map +1 -1
- package/dist/context/execution-context.js +87 -33
- package/dist/context/execution-context.js.map +1 -1
- package/dist/context/index.d.ts +3 -2
- package/dist/context/index.js +3 -1
- package/dist/context/request-scope.d.ts +2 -0
- package/dist/context/request-scope.js +39 -0
- package/dist/context/request-scope.js.map +1 -0
- package/dist/context/resource-capabilities.js +59 -0
- package/dist/context/resource-capabilities.js.map +1 -0
- package/dist/context/scoped-api.js +104 -0
- package/dist/context/scoped-api.js.map +1 -0
- package/dist/context/service-context.d.ts +18 -12
- package/dist/context/service-context.d.ts.map +1 -1
- package/dist/context/service-context.js +45 -55
- package/dist/context/service-context.js.map +1 -1
- package/dist/context/user-context.d.ts +16 -22
- package/dist/context/user-context.d.ts.map +1 -1
- package/dist/context/user-context.js +57 -3
- package/dist/context/user-context.js.map +1 -1
- package/dist/core/agent/load-agents.d.ts.map +1 -1
- package/dist/core/agent/load-agents.js +5 -2
- package/dist/core/agent/load-agents.js.map +1 -1
- package/dist/core/agent/run-agent.d.ts +19 -6
- package/dist/core/agent/run-agent.d.ts.map +1 -1
- package/dist/core/agent/run-agent.js +53 -17
- package/dist/core/agent/run-agent.js.map +1 -1
- package/dist/core/agent/types.d.ts +18 -1
- package/dist/core/agent/types.d.ts.map +1 -1
- package/dist/core/agent/types.js.map +1 -1
- package/dist/core/appkit.d.ts +4 -3
- package/dist/core/appkit.d.ts.map +1 -1
- package/dist/core/appkit.js +34 -11
- package/dist/core/appkit.js.map +1 -1
- package/dist/core/plugin-context.d.ts +14 -15
- package/dist/core/plugin-context.d.ts.map +1 -1
- package/dist/core/plugin-context.js +24 -14
- package/dist/core/plugin-context.js.map +1 -1
- package/dist/errors/base.d.ts +2 -2
- package/dist/errors/base.js +2 -2
- package/dist/errors/base.js.map +1 -1
- package/dist/errors/identity-expired.d.ts +14 -0
- package/dist/errors/identity-expired.d.ts.map +1 -0
- package/dist/errors/identity-expired.js +33 -0
- package/dist/errors/identity-expired.js.map +1 -0
- package/dist/errors/index.js +1 -0
- package/dist/index.d.ts +12 -6
- package/dist/index.js +7 -2
- package/dist/logging/logger.js +1 -1
- package/dist/plugin/execution-result.d.ts +4 -1
- package/dist/plugin/execution-result.d.ts.map +1 -1
- package/dist/plugin/interceptors/telemetry.js +6 -4
- package/dist/plugin/interceptors/telemetry.js.map +1 -1
- package/dist/plugin/plugin.d.ts +14 -22
- package/dist/plugin/plugin.d.ts.map +1 -1
- package/dist/plugin/plugin.js +42 -123
- package/dist/plugin/plugin.js.map +1 -1
- package/dist/plugins/agents/agents.d.ts +13 -1
- package/dist/plugins/agents/agents.d.ts.map +1 -1
- package/dist/plugins/agents/agents.js +65 -15
- package/dist/plugins/agents/agents.js.map +1 -1
- package/dist/plugins/agents/auth-mode.js +42 -0
- package/dist/plugins/agents/auth-mode.js.map +1 -0
- package/dist/plugins/agents/index.d.ts +1 -1
- package/dist/plugins/agents/mlflow.js +11 -3
- package/dist/plugins/agents/mlflow.js.map +1 -1
- package/dist/plugins/agents/tool-dispatch.js +9 -1
- package/dist/plugins/agents/tool-dispatch.js.map +1 -1
- package/dist/plugins/ai-search/ai-search.d.ts +1 -1
- package/dist/plugins/ai-search/ai-search.d.ts.map +1 -1
- package/dist/plugins/ai-search/ai-search.js +4 -4
- package/dist/plugins/ai-search/ai-search.js.map +1 -1
- package/dist/plugins/analytics/analytics.d.ts +3 -3
- package/dist/plugins/analytics/analytics.d.ts.map +1 -1
- package/dist/plugins/analytics/analytics.js +9 -7
- package/dist/plugins/analytics/analytics.js.map +1 -1
- package/dist/plugins/database/crud/contract.js +2 -2
- package/dist/plugins/database/crud/contract.js.map +1 -1
- package/dist/plugins/database/database.d.ts +1 -1
- package/dist/plugins/files/plugin.d.ts +21 -15
- package/dist/plugins/files/plugin.d.ts.map +1 -1
- package/dist/plugins/files/plugin.js +50 -29
- package/dist/plugins/files/plugin.js.map +1 -1
- package/dist/plugins/genie/genie.d.ts +10 -1
- package/dist/plugins/genie/genie.d.ts.map +1 -1
- package/dist/plugins/genie/genie.js +12 -3
- package/dist/plugins/genie/genie.js.map +1 -1
- package/dist/plugins/genie/manifest.js +1 -0
- package/dist/plugins/jobs/plugin.d.ts +1 -1
- package/dist/plugins/jobs/plugin.js +2 -2
- package/dist/plugins/jobs/plugin.js.map +1 -1
- package/dist/plugins/lakebase/lakebase.d.ts +11 -18
- package/dist/plugins/lakebase/lakebase.d.ts.map +1 -1
- package/dist/plugins/lakebase/lakebase.js +16 -21
- package/dist/plugins/lakebase/lakebase.js.map +1 -1
- package/dist/plugins/server/client-config-sanitizer.js +1 -4
- package/dist/plugins/server/client-config-sanitizer.js.map +1 -1
- package/dist/plugins/server/dev-obo-middleware.js +60 -0
- package/dist/plugins/server/dev-obo-middleware.js.map +1 -0
- package/dist/plugins/server/index.d.ts +1 -1
- package/dist/plugins/server/index.d.ts.map +1 -1
- package/dist/plugins/server/index.js +3 -0
- package/dist/plugins/server/index.js.map +1 -1
- package/dist/plugins/serving/manifest.js +1 -0
- package/dist/plugins/serving/serving.d.ts +1 -1
- package/dist/plugins/serving/serving.js +6 -6
- package/dist/plugins/serving/serving.js.map +1 -1
- package/dist/resources/index.d.ts +1 -0
- package/dist/resources/index.js +3 -0
- package/dist/resources/warehouse.d.ts +16 -0
- package/dist/resources/warehouse.d.ts.map +1 -0
- package/dist/resources/warehouse.js +93 -0
- package/dist/resources/warehouse.js.map +1 -0
- package/dist/schemas/manifest.d.ts +44 -1
- package/dist/schemas/manifest.d.ts.map +1 -1
- package/dist/schemas/manifest.js +122 -3
- package/dist/schemas/manifest.js.map +1 -1
- package/dist/shared/src/dev-obo.js +86 -0
- package/dist/shared/src/dev-obo.js.map +1 -0
- package/dist/shared/src/index.d.ts +1 -1
- package/dist/shared/src/plugin.d.ts +12 -1
- package/dist/shared/src/plugin.d.ts.map +1 -1
- package/dist/shared/src/schemas/manifest.d.ts +17 -0
- package/dist/shared/src/schemas/manifest.d.ts.map +1 -1
- package/dist/shared/src/schemas/manifest.js +53 -3
- package/dist/shared/src/schemas/manifest.js.map +1 -1
- package/dist/stream/stream-manager.d.ts.map +1 -1
- package/dist/stream/stream-manager.js +5 -2
- package/dist/stream/stream-manager.js.map +1 -1
- package/dist/telemetry/execution-span-processor.js +21 -0
- package/dist/telemetry/execution-span-processor.js.map +1 -0
- package/dist/telemetry/telemetry-manager.js +2 -1
- package/dist/telemetry/telemetry-manager.js.map +1 -1
- package/dist/testing/create-test-app.d.ts +2 -2
- package/dist/testing/create-test-app.js +2 -2
- package/dist/testing/create-test-app.js.map +1 -1
- package/dist/testing/fixtures.d.ts +3 -2
- package/dist/testing/fixtures.d.ts.map +1 -1
- package/dist/testing/fixtures.js +21 -14
- package/dist/testing/fixtures.js.map +1 -1
- package/dist/testing/reset-singletons.js +1 -1
- package/dist/testing/test-plugin-context.d.ts +3 -12
- package/dist/testing/test-plugin-context.d.ts.map +1 -1
- package/dist/testing/test-plugin-context.js +24 -16
- package/dist/testing/test-plugin-context.js.map +1 -1
- package/dist/utils/is-plain-object.js +11 -0
- package/dist/utils/is-plain-object.js.map +1 -0
- package/docs/api/appkit/Class.AppKitError.md +2 -1
- package/docs/api/appkit/Class.AuthenticationError.md +1 -1
- package/docs/api/appkit/Class.ConfigurationError.md +1 -1
- package/docs/api/appkit/Class.ConnectionError.md +1 -1
- package/docs/api/appkit/Class.DatabaseValidationError.md +1 -1
- package/docs/api/appkit/Class.ExecutionError.md +1 -1
- package/docs/api/appkit/Class.IdentityExpiredError.md +190 -0
- package/docs/api/appkit/Class.InitializationError.md +1 -1
- package/docs/api/appkit/Class.Plugin.md +9 -13
- package/docs/api/appkit/Class.ServerError.md +1 -1
- package/docs/api/appkit/Class.ServiceContext.md +169 -0
- package/docs/api/appkit/Class.TunnelError.md +1 -1
- package/docs/api/appkit/Class.ValidationError.md +1 -1
- package/docs/api/appkit/Function.createApp.md +12 -12
- package/docs/api/appkit/Function.getCallerContext.md +12 -0
- package/docs/api/appkit/Function.getCurrentActorId.md +12 -0
- package/docs/api/appkit/Function.getCurrentPrincipalKey.md +12 -0
- package/docs/api/appkit/Function.getCurrentUserId.md +14 -0
- package/docs/api/appkit/Function.getExecutionContext.md +5 -3
- package/docs/api/appkit/Function.getUserContext.md +16 -0
- package/docs/api/appkit/Function.getWarehouseId.md +20 -0
- package/docs/api/appkit/Function.isInUserContext.md +12 -0
- package/docs/api/appkit/Function.isUserContext.md +20 -0
- package/docs/api/appkit/Function.runAgent.md +1 -1
- package/docs/api/appkit/Function.runInCallerContext.md +27 -0
- package/docs/api/appkit/Function.runInUserContext.md +29 -0
- package/docs/api/appkit/Interface.AgentDefinition.md +11 -0
- package/docs/api/appkit/Interface.AgentsPluginConfig.md +11 -0
- package/docs/api/appkit/Interface.CallerContext.md +54 -0
- package/docs/api/appkit/Interface.IndexConfig.md +1 -1
- package/docs/api/appkit/Interface.PluginManifest.md +33 -2
- package/docs/api/appkit/Interface.RegisteredAgent.md +11 -0
- package/docs/api/appkit/Interface.RunAgentInput.md +45 -1
- package/docs/api/appkit/TypeAlias.AgentAuth.md +8 -0
- package/docs/api/appkit/TypeAlias.AppKitApi.md +35 -0
- package/docs/api/appkit/TypeAlias.CallerPrincipal.md +13 -0
- package/docs/api/appkit/TypeAlias.ExecutionContext.md +9 -0
- package/docs/api/appkit/TypeAlias.ExecutionResult.md +65 -0
- package/docs/api/appkit/TypeAlias.ScopedPluginMap.md +12 -0
- package/docs/api/appkit/TypeAlias.UserContext.md +109 -0
- package/docs/api/appkit/TypeAlias.UserScopedApp.md +39 -0
- package/docs/api/appkit.md +20 -0
- package/docs/plugins/agents.md +45 -0
- package/docs/plugins/analytics.md +8 -0
- package/docs/plugins/execution-context.md +101 -50
- package/docs/plugins/lakebase.md +6 -82
- package/llms.txt +21 -1
- package/package.json +1 -1
- package/sbom.cdx.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mlflow.js","names":["#inner","#popTrace","#spanTypeKey","#agentSpanTypes","#maxTracked","#flushTimeoutMs","#ready","#forwarded","#agentTraceIds","#boundedFlush","trace"],"sources":["../../../src/plugins/agents/mlflow.ts"],"sourcesContent":["import type { UnityCatalogLocation } from \"@mlflow/core\";\nimport { SpanKind } from \"@opentelemetry/api\";\nimport type { SpanProcessor } from \"@opentelemetry/sdk-trace-base\";\n\nimport { createLogger } from \"../../logging/logger\";\nimport { TelemetryManager } from \"../../telemetry\";\n\nconst logger = createLogger(\"agents\");\n\ntype MlflowModule = typeof import(\"@mlflow/core\");\ntype MlflowClientInstance = InstanceType<MlflowModule[\"MlflowClient\"]>;\n\ninterface MlflowInitConfig {\n trackingUri: string;\n experimentId: string;\n host?: string;\n}\n\nlet mlflow: MlflowModule | undefined;\nlet enabled = false;\nlet initStarted = false;\nlet configured = false;\nlet initConfig: MlflowInitConfig | undefined;\n// The resolved UC trace location, or undefined for classic experiment storage.\nlet ucLocation: UnityCatalogLocation | undefined;\nlet gatedProcessor: GatedMlflowSpanProcessor | undefined;\n\n/**\n * Wraps mlflow's OTel `SpanProcessor` and scopes it to agent traces. Two jobs:\n *\n * 1. Stay inert until ready. The classic processor's `onStart` calls its own\n * `getConfig()`, which THROWS before `init()` runs — and that throw\n * propagates out of `tracer.startSpan()`, so it would break unrelated AppKit\n * spans (HTTP, analytics) created between `TelemetryManager.start()` and the\n * first agent turn. We contribute this to AppKit's single tracer provider\n * during `setup()`, but only start forwarding once `ready()` is called by\n * {@link ensureConfigured}. (The UC processor reads no global config and\n * can't throw here, but forwarding is gated uniformly either way.)\n *\n * 2. Let only agent turns become MLflow traces. mlflow roots a trace at EVERY\n * parentless span, and AppKit's single provider carries every HTTP/DB span —\n * so unscoped, every request would become an MLflow trace. mlflow stamps\n * `mlflow.spanType` on EVERY span it processes (defaulting to `UNKNOWN`), so\n * presence alone can't tell an agent turn from a plain request — we key on\n * the value (AGENT/TOOL) instead. At the root's `onEnd` we forward it (mlflow\n * exports the trace) only if some span in the trace carried an AGENT/TOOL\n * type; otherwise `popTrace` to discard the trace mlflow built in memory. The\n * real span type is set after `onStart` (the constructor stamps UNKNOWN\n * first), and children end before their root, so the flag is set by the time\n * the root decides.\n *\n * It also drops the exporters' own outbound spans at `onStart` (parentless\n * CLIENT — outgoing requests made outside any agent turn, e.g. mlflow/OTLP\n * shipping a trace). Forwarding those would loop: each upload is an HTTP call\n * that auto-instrumentation turns into a new span to trace and upload.\n *\n * ponytail: non-agent requests still build (then discard) an in-memory trace\n * tree — allocation-only, no network (export happens only when we forward the\n * root's `onEnd`). Fine at normal QPS; if a very high-QPS app makes the churn\n * matter, root MLflow at a detached agent span instead (costs the HTTP envelope\n * on the trace and splits the OTLP trace).\n */\nexport class GatedMlflowSpanProcessor implements SpanProcessor {\n #inner: SpanProcessor;\n #ready = false;\n // Spans we forwarded `onStart` for, so `onEnd` stays balanced — mlflow never\n // sees an end without a matching start.\n #forwarded = new WeakSet<object>();\n // OTel trace ids that contained at least one mlflow (AGENT/TOOL) span, so the\n // root's `onEnd` exports rather than discards. Cleared as each root ends.\n #agentTraceIds = new Set<string>();\n #popTrace: (otelTraceId: string) => void;\n #spanTypeKey: string;\n // The `mlflow.spanType` attribute values (JSON-stringified) that mark a trace\n // as an agent turn — AGENT/TOOL. Every other value (notably UNKNOWN, which\n // mlflow stamps on all non-agent spans) is treated as non-agent.\n #agentSpanTypes: ReadonlySet<string>;\n // Leak backstop for #agentTraceIds — far above real concurrency. See onEnd.\n #maxTracked: number;\n // Cap on how long forceFlush/shutdown wait for a stuck export.\n #flushTimeoutMs: number;\n\n constructor(\n inner: SpanProcessor,\n deps: {\n popTrace: (otelTraceId: string) => void;\n spanTypeKey: string;\n agentSpanTypes: ReadonlySet<string>;\n maxTracked?: number;\n flushTimeoutMs?: number;\n },\n ) {\n this.#inner = inner;\n this.#popTrace = deps.popTrace;\n this.#spanTypeKey = deps.spanTypeKey;\n this.#agentSpanTypes = deps.agentSpanTypes;\n this.#maxTracked = deps.maxTracked ?? 1024;\n this.#flushTimeoutMs = deps.flushTimeoutMs ?? 5000;\n }\n\n ready(): void {\n this.#ready = true;\n }\n\n onStart(\n span: Parameters<SpanProcessor[\"onStart\"]>[0],\n parentContext: Parameters<SpanProcessor[\"onStart\"]>[1],\n ): void {\n if (!this.#ready) return;\n // Drop the exporters' own outbound calls. A parentless (root) CLIENT span is\n // an outgoing request made outside any agent turn — e.g. mlflow or OTLP\n // shipping a trace. Forwarding those would loop: each upload is itself an\n // HTTP call that auto-instrumentation turns into a new span to trace and\n // upload.\n if (span.kind === SpanKind.CLIENT && !span.parentSpanContext?.spanId) {\n return;\n }\n this.#forwarded.add(span);\n this.#inner.onStart(span, parentContext);\n }\n\n onEnd(span: Parameters<SpanProcessor[\"onEnd\"]>[0]): void {\n if (!this.#forwarded.has(span)) return;\n const traceId = span.spanContext().traceId;\n // An AGENT/TOOL span ended in this trace — mark it for export. mlflow stamps\n // `mlflow.spanType` on every span (UNKNOWN by default), so match the value,\n // not mere presence; the real type is set after `onStart`, so `onEnd` is the\n // earliest we can read it.\n if (\n this.#agentSpanTypes.has(span.attributes[this.#spanTypeKey] as string) &&\n !this.#agentTraceIds.has(traceId)\n ) {\n // Normally an entry lives only until its root's onEnd deletes it. But a\n // root that ends BEFORE its agent child (streaming client-disconnect) or\n // never ends (crash) orphans the entry, and #agentTraceIds — unlike the\n // GC-safe #forwarded WeakSet — is keyed by string, so it can't self-clean.\n // FIFO-evict at the cap so an abandoned-trace pattern can't grow it\n // unboundedly over process uptime.\n // ponytail: evicting a still-live trace only mis-discards it, and that\n // needs >#maxTracked concurrent agent turns — far above real load.\n if (this.#agentTraceIds.size >= this.#maxTracked) {\n const oldest = this.#agentTraceIds.values().next().value;\n if (oldest !== undefined) this.#agentTraceIds.delete(oldest);\n }\n this.#agentTraceIds.add(traceId);\n }\n if (span.parentSpanContext?.spanId) {\n // Non-root: mlflow's own `onEnd` early-returns, but forward for balance.\n this.#inner.onEnd(span);\n return;\n }\n // Root span: export only agent traces; discard everything else so plain HTTP\n // requests never become MLflow traces. `delete` reports whether it was agent.\n if (this.#agentTraceIds.delete(traceId)) {\n this.#inner.onEnd(span);\n } else {\n this.#popTrace(traceId);\n }\n }\n\n forceFlush(): Promise<void> {\n return this.#boundedFlush(() => this.#inner.forceFlush());\n }\n\n shutdown(): Promise<void> {\n return this.#boundedFlush(() => this.#inner.shutdown());\n }\n\n // Bound the inner flush/shutdown wait: both exporters export fire-and-forget,\n // so a stuck export only wedges here (graceful shutdown), never a turn.\n // Resolves — not rejects — on timeout: the caller is tearing down.\n async #boundedFlush(op: () => Promise<void>): Promise<void> {\n let timer: NodeJS.Timeout | undefined;\n try {\n await Promise.race([\n op().catch((err) => {\n logger.warn(\"MLflow trace flush error: %O\", err);\n }),\n new Promise<void>((resolve) => {\n timer = setTimeout(() => {\n logger.warn(\n \"MLflow trace flush exceeded %dms; continuing (export may still be in flight)\",\n this.#flushTimeoutMs,\n );\n resolve();\n }, this.#flushTimeoutMs);\n timer.unref(); // don't keep the event loop alive on the timeout alone\n }),\n ]);\n } finally {\n if (timer) clearTimeout(timer);\n }\n }\n}\n\n/**\n * Resolve the Unity Catalog trace location for the bound experiment, or\n * `undefined` for classic experiment-backed storage. Any failure falls back to\n * classic — a tracing misconfiguration must never break the agent.\n *\n * 1. Explicit env override — `MLFLOW_UC_CATALOG` + `MLFLOW_UC_SCHEMA` +\n * `MLFLOW_UC_TABLE_PREFIX`, all three required.\n * 2. Auto-detect from the linked Databricks experiment (numeric ids only, since\n * `GetExperiment` only accepts them): parse its `databricksTrace*` tags with\n * mlflow's own {@link ucLocationFromExperimentTags}, which also carries the\n * backend-populated spans/logs table names for custom-provisioned locations.\n * 3. Otherwise classic.\n *\n * `ucLocationFromExperimentTags` isn't on `@mlflow/core`'s public entrypoint, so\n * it's deep-imported like the exporter classes and covered by the tripwire test.\n */\nasync function resolveUcLocation(\n experimentId: string,\n client: MlflowClientInstance,\n): Promise<UnityCatalogLocation | undefined> {\n const catalogName = process.env.MLFLOW_UC_CATALOG?.trim();\n const schemaName = process.env.MLFLOW_UC_SCHEMA?.trim();\n const tablePrefix = process.env.MLFLOW_UC_TABLE_PREFIX?.trim();\n if (catalogName && schemaName && tablePrefix) {\n return { catalogName, schemaName, tablePrefix };\n }\n\n if (!/^\\d+$/.test(experimentId)) return undefined;\n\n try {\n const experiment = await client.getExperiment(experimentId);\n if (!experiment) return undefined;\n const { ucLocationFromExperimentTags } =\n await import(\"@mlflow/core/dist/core/destination\");\n return ucLocationFromExperimentTags(experiment.tags) ?? undefined;\n } catch (err) {\n logger.warn(\n \"MLflow UC trace-location auto-detect failed; using classic experiment storage: %O\",\n err,\n );\n return undefined;\n }\n}\n\n/**\n * Build mlflow's OTel `SpanProcessor` ourselves rather than letting `init()`\n * build and globally register its own tracer provider (its own `NodeSDK`). This\n * lets AppKit own the single global provider (OTLP + this processor), so agent\n * spans reach both MLflow and any OTLP endpoint without two SDKs racing for the\n * global slot.\n *\n * When a UC trace location is bound, builds the Unity Catalog processor +\n * exporter (V4 trace ids, spans uploaded to the experiment's UC table);\n * otherwise the classic experiment-backed processor. `createAuthProvider`,\n * `MlflowClient`, `InMemoryTraceManager` and `SpanAttributeKey` are all public\n * in `@mlflow/core`, so only the exporter/processor classes are deep-imported —\n * pinned to the exact version in package.json and guarded by a test that fails\n * loudly if a version bump renames them.\n *\n * Also resolves the hooks {@link GatedMlflowSpanProcessor} needs to scope\n * forwarding to agent traces: `popTrace` (to discard non-agent traces), the\n * `mlflow.spanType` attribute key, and the JSON-stringified AGENT/TOOL values\n * that mark a trace as an agent turn (mlflow stamps every span, defaulting to\n * UNKNOWN, so the gate must match the value, not presence).\n */\nasync function buildMlflowSpanProcessor(\n m: MlflowModule,\n client: MlflowClientInstance,\n ucLoc: UnityCatalogLocation | undefined,\n): Promise<{\n processor: SpanProcessor;\n popTrace: (otelTraceId: string) => void;\n spanTypeKey: string;\n agentSpanTypes: ReadonlySet<string>;\n}> {\n let processor: SpanProcessor;\n if (ucLoc) {\n const { DatabricksUCTableSpanExporter, DatabricksUCTableSpanProcessor } =\n await import(\"@mlflow/core/dist/exporters/uc_table\");\n processor = new DatabricksUCTableSpanProcessor(\n new DatabricksUCTableSpanExporter(client),\n ucLoc,\n );\n } else {\n const { MlflowSpanExporter, MlflowSpanProcessor } =\n await import(\"@mlflow/core/dist/exporters/mlflow\");\n processor = new MlflowSpanProcessor(new MlflowSpanExporter(client));\n }\n return {\n processor,\n popTrace: (otelTraceId) =>\n m.InMemoryTraceManager.getInstance().popTrace(otelTraceId),\n spanTypeKey: m.SpanAttributeKey.SPAN_TYPE,\n // mlflow JSON-stringifies attribute values, so the stored values are\n // `\"AGENT\"`/`\"TOOL\"` (quoted). Match that exact form.\n agentSpanTypes: new Set([\n JSON.stringify(m.SpanType.AGENT),\n JSON.stringify(m.SpanType.TOOL),\n ]),\n };\n}\n\n/** The bound MLflow experiment id, from the optional `experiment` resource. */\nfunction experimentId(): string | undefined {\n const id = process.env.MLFLOW_EXPERIMENT_ID?.trim();\n return id || undefined;\n}\n\n/**\n * Databricks host with a scheme. `@mlflow/core` uses `DATABRICKS_HOST`\n * verbatim to build request URLs and doesn't add `https://`, so a bare host\n * (`workspace.cloud.databricks.com`) makes `new URL()` throw. Pass an explicit\n * normalized host when the env var is set; when it isn't (profile-based auth),\n * return undefined and let the SDK read the host from `~/.databrickscfg`.\n */\nfunction normalizedDatabricksHost(): string | undefined {\n const raw = process.env.DATABRICKS_HOST?.trim();\n if (!raw) return undefined;\n return /^https?:\\/\\//i.test(raw) ? raw : `https://${raw}`;\n}\n\n/**\n * Initialize MLflow agent tracing once, when an experiment is bound — i.e. the\n * agents plugin's optional `experiment` resource is set (`MLFLOW_EXPERIMENT_ID`).\n * Called from the agents plugin's `setup()`, before `TelemetryManager.start()`.\n *\n * Rather than let `@mlflow/core`'s `init()` stand up and globally register its\n * own tracer provider (its `NodeSDK`, which would race AppKit's), we build the\n * span processor ourselves and contribute it to AppKit's single provider via\n * {@link TelemetryManager.registerSpanProcessor}. For the classic\n * experiment-backed processor, mlflow's global config is seeded by\n * {@link startAgentTracing} on the `\"setup:complete\"` lifecycle event (after\n * `start()`), with {@link ensureConfigured} as an idempotent lazy fallback; the\n * UC processor needs no seeded config, so that path never calls `init()`.\n *\n * The trace store is resolved here: a UC table prefix (env-configured or\n * auto-detected from the experiment's Databricks tags) vs. the classic\n * experiment. Auth is resolved by `@mlflow/core` from the app's own Databricks\n * credentials — `DATABRICKS_HOST`/`DATABRICKS_TOKEN` or a `~/.databrickscfg`\n * profile (`MLFLOW_TRACKING_URI=databricks://profile`) — so no tokens or OTLP\n * headers are wired by hand. A failure (missing creds, bad experiment) logs and\n * leaves tracing disabled rather than breaking the agent.\n *\n * Safe to call repeatedly; only the first call does work.\n */\nexport async function initAgentTracing(): Promise<void> {\n if (initStarted) return;\n initStarted = true;\n\n const id = experimentId();\n if (!id) return;\n\n try {\n mlflow = await import(\"@mlflow/core\");\n const host = normalizedDatabricksHost();\n initConfig = {\n trackingUri: process.env.MLFLOW_TRACKING_URI?.trim() || \"databricks\",\n experimentId: id,\n ...(host ? { host } : {}),\n };\n // One auth resolution + client, reused for UC auto-detect and the exporter.\n const authProvider = mlflow.createAuthProvider({\n trackingUri: initConfig.trackingUri,\n ...(host ? { host } : {}),\n });\n const client = new mlflow.MlflowClient({\n trackingUri: initConfig.trackingUri,\n authProvider,\n });\n ucLocation = await resolveUcLocation(id, client);\n const { processor, popTrace, spanTypeKey, agentSpanTypes } =\n await buildMlflowSpanProcessor(mlflow, client, ucLocation);\n gatedProcessor = new GatedMlflowSpanProcessor(processor, {\n popTrace,\n spanTypeKey,\n agentSpanTypes,\n });\n TelemetryManager.registerSpanProcessor(gatedProcessor);\n enabled = true;\n if (ucLocation) {\n logger.info(\n \"MLflow agent tracing enabled (experiment %s, UC %s.%s.%s)\",\n id,\n ucLocation.catalogName,\n ucLocation.schemaName,\n ucLocation.tablePrefix,\n );\n } else {\n logger.info(\"MLflow agent tracing enabled (experiment %s)\", id);\n }\n } catch (err) {\n if (isModuleNotFound(err)) {\n logger.warn(\n \"MLflow agent tracing requires the optional `@mlflow/core` package. \" +\n \"Install it to enable tracing: npm i @mlflow/core\",\n );\n } else {\n logger.warn(\"MLflow agent tracing disabled: %O\", err);\n }\n }\n}\n\n/** True when a dynamic `import()` failed because the package isn't installed. */\nfunction isModuleNotFound(err: unknown): boolean {\n return (\n !!err &&\n typeof err === \"object\" &&\n \"code\" in err &&\n (err.code === \"ERR_MODULE_NOT_FOUND\" || err.code === \"MODULE_NOT_FOUND\")\n );\n}\n\n/**\n * Records a span's outputs. Callers get one from `traceAgent`/`traceTool`;\n * it's a no-op when tracing is disabled, so call sites never branch on it.\n */\nexport interface SpanRecorder {\n setOutputs(outputs: unknown): void;\n}\n\nconst noopRecorder: SpanRecorder = { setOutputs() {} };\n\n/**\n * Seed the classic processor's global config once, AFTER\n * `TelemetryManager.start()` has registered AppKit's provider — driven eagerly\n * by {@link startAgentTracing} on `\"setup:complete\"`, or lazily by {@link trace}\n * as a fallback — then enable forwarding on the gated processor. Returns whether\n * tracing is usable.\n *\n * The classic experiment-backed `MlflowSpanProcessor` reads\n * `getConfig().experimentId` in `onStart`, so it needs `init()` to seed mlflow's\n * global config. `init()` also stands up its own `NodeSDK` whose provider loses\n * the global slot to AppKit's already-registered one (non-fatal); we call it\n * only for that config side-effect. The UC processor carries its location and\n * reads no global config, so the UC path skips `init()` entirely — no second\n * `NodeSDK`, no competing global registration by `@mlflow/core`.\n */\nfunction ensureConfigured(): boolean {\n if (configured) return enabled;\n configured = true;\n // `gatedProcessor` guard is load-bearing: if buildMlflowSpanProcessor threw,\n // `mlflow` and `initConfig` are still set but there is no gate. Calling\n // `mlflow.init()` then would stand up mlflow's OWN ungated provider — and if\n // AppKit registered none (no OTLP, no processor) it wins the global slot,\n // routing every span into mlflow un-gated: the exact over-tracing + exporter\n // loop this file exists to prevent.\n if (!mlflow || !initConfig || !gatedProcessor) return false;\n try {\n if (!ucLocation) mlflow.init(initConfig);\n gatedProcessor.ready();\n return true;\n } catch (err) {\n enabled = false;\n logger.warn(\"MLflow agent tracing disabled (init failed): %O\", err);\n return false;\n }\n}\n\n/**\n * Seed mlflow's config eagerly, right after `TelemetryManager.start()` — the\n * agents plugin wires this to the `\"setup:complete\"` lifecycle event, before the\n * server serves any request. Doing it here means the request's own root span is\n * already forwarded when the first turn runs, so that turn assembles into a\n * trace instead of being dropped (mlflow roots a trace only at the top-level\n * span). Idempotent. `trace()` also seeds lazily, but that only fully rescues a\n * turn whose agent span is itself the trace root; an HTTP-wrapped first turn\n * seeded lazily loses its root span (already started — and dropped — before\n * `ready()`), so this eager path is the reliable one.\n */\nexport function startAgentTracing(): void {\n ensureConfigured();\n}\n\n/**\n * Run `fn` inside an MLflow span of `spanType` when tracing is enabled,\n * otherwise just run it (zero overhead). Spans auto-nest via the SDK's active\n * context, so a TOOL span opened inside an AGENT span's callback becomes its\n * child. The callback's resolved value is recorded as the span's outputs unless\n * it called `setOutputs` first; return `undefined` (or set outputs explicitly)\n * when the return value isn't the output you want traced.\n */\nasync function trace<T>(\n spanType: \"AGENT\" | \"TOOL\",\n name: string,\n inputs: unknown,\n fn: (span: SpanRecorder) => Promise<T>,\n): Promise<T> {\n if (!enabled || !mlflow) return fn(noopRecorder);\n const m = mlflow;\n if (!ensureConfigured()) return fn(noopRecorder);\n const type = spanType === \"AGENT\" ? m.SpanType.AGENT : m.SpanType.TOOL;\n return await m.withSpan<T>(\n async (span) => {\n if (inputs !== undefined) span.setInputs(inputs);\n let outputsSet = false;\n const result = await fn({\n setOutputs(outputs) {\n outputsSet = true;\n span.setOutputs(outputs);\n },\n });\n if (!outputsSet && result !== undefined) span.setOutputs(result);\n return result;\n },\n { name, spanType: type },\n );\n}\n\n/** Trace a turn's root AGENT span. See {@link trace}. */\nexport function traceAgent<T>(\n name: string,\n inputs: unknown,\n fn: (span: SpanRecorder) => Promise<T>,\n): Promise<T> {\n return trace(\"AGENT\", name, inputs, fn);\n}\n\n/** Trace a TOOL span, nested under the active AGENT span. See {@link trace}. */\nexport function traceTool<T>(\n name: string,\n inputs: unknown,\n fn: (span: SpanRecorder) => Promise<T>,\n): Promise<T> {\n return trace(\"TOOL\", name, inputs, fn);\n}\n\n/**\n * The MLflow trace id for the active turn, when tracing is enabled. Must be\n * read inside an agent span so eval runs can correlate the turn to its trace\n * and attach assessments. Returns undefined when tracing is off.\n *\n * Reads the context-active span rather than `getLastActiveTraceId()`: the\n * latter is only populated when a root span *ends* (on export), so mid-turn it\n * returns the previous turn's id — or, under concurrent turns, another turn's.\n */\nexport function currentTraceId(): string | undefined {\n if (!enabled || !mlflow) return undefined;\n try {\n return mlflow.getCurrentActiveSpan()?.traceId;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Link the active turn's trace to an MLflow run by id, via the `mlflow.sourceRun`\n * trace metadata. Used by eval runs so each case's trace shows under the run.\n * Must be called while a trace is active (inside an agent span). No-op when\n * tracing is disabled.\n */\nexport function linkTraceToRun(runId: string): void {\n if (!enabled || !mlflow) return;\n try {\n mlflow.updateCurrentTrace({ metadata: { \"mlflow.sourceRun\": runId } });\n } catch (err) {\n logger.warn(\"Failed to link trace to run %s: %O\", runId, err);\n }\n}\n"],"mappings":";;;;;;AAOA,MAAM,SAAS,aAAa,SAAS;AAWrC,IAAI;AACJ,IAAI,UAAU;AACd,IAAI,cAAc;AAClB,IAAI,aAAa;AACjB,IAAI;AAEJ,IAAI;AACJ,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqCJ,IAAa,2BAAb,MAA+D;CAC7D;CACA,SAAS;CAGT,6BAAa,IAAI,SAAiB;CAGlC,iCAAiB,IAAI,KAAa;CAClC;CACA;CAIA;CAEA;CAEA;CAEA,YACE,OACA,MAOA;AACA,QAAKA,QAAS;AACd,QAAKC,WAAY,KAAK;AACtB,QAAKC,cAAe,KAAK;AACzB,QAAKC,iBAAkB,KAAK;AAC5B,QAAKC,aAAc,KAAK,cAAc;AACtC,QAAKC,iBAAkB,KAAK,kBAAkB;;CAGhD,QAAc;AACZ,QAAKC,QAAS;;CAGhB,QACE,MACA,eACM;AACN,MAAI,CAAC,MAAKA,MAAQ;AAMlB,MAAI,KAAK,SAAS,SAAS,UAAU,CAAC,KAAK,mBAAmB,OAC5D;AAEF,QAAKC,UAAW,IAAI,KAAK;AACzB,QAAKP,MAAO,QAAQ,MAAM,cAAc;;CAG1C,MAAM,MAAmD;AACvD,MAAI,CAAC,MAAKO,UAAW,IAAI,KAAK,CAAE;EAChC,MAAM,UAAU,KAAK,aAAa,CAAC;AAKnC,MACE,MAAKJ,eAAgB,IAAI,KAAK,WAAW,MAAKD,aAAwB,IACtE,CAAC,MAAKM,cAAe,IAAI,QAAQ,EACjC;AASA,OAAI,MAAKA,cAAe,QAAQ,MAAKJ,YAAa;IAChD,MAAM,SAAS,MAAKI,cAAe,QAAQ,CAAC,MAAM,CAAC;AACnD,QAAI,WAAW,OAAW,OAAKA,cAAe,OAAO,OAAO;;AAE9D,SAAKA,cAAe,IAAI,QAAQ;;AAElC,MAAI,KAAK,mBAAmB,QAAQ;AAElC,SAAKR,MAAO,MAAM,KAAK;AACvB;;AAIF,MAAI,MAAKQ,cAAe,OAAO,QAAQ,CACrC,OAAKR,MAAO,MAAM,KAAK;MAEvB,OAAKC,SAAU,QAAQ;;CAI3B,aAA4B;AAC1B,SAAO,MAAKQ,mBAAoB,MAAKT,MAAO,YAAY,CAAC;;CAG3D,WAA0B;AACxB,SAAO,MAAKS,mBAAoB,MAAKT,MAAO,UAAU,CAAC;;CAMzD,OAAMS,aAAc,IAAwC;EAC1D,IAAI;AACJ,MAAI;AACF,SAAM,QAAQ,KAAK,CACjB,IAAI,CAAC,OAAO,QAAQ;AAClB,WAAO,KAAK,gCAAgC,IAAI;KAChD,EACF,IAAI,SAAe,YAAY;AAC7B,YAAQ,iBAAiB;AACvB,YAAO,KACL,gFACA,MAAKJ,eACN;AACD,cAAS;OACR,MAAKA,eAAgB;AACxB,UAAM,OAAO;KACb,CACH,CAAC;YACM;AACR,OAAI,MAAO,cAAa,MAAM;;;;;;;;;;;;;;;;;;;;AAqBpC,eAAe,kBACb,cACA,QAC2C;CAC3C,MAAM,cAAc,QAAQ,IAAI,mBAAmB,MAAM;CACzD,MAAM,aAAa,QAAQ,IAAI,kBAAkB,MAAM;CACvD,MAAM,cAAc,QAAQ,IAAI,wBAAwB,MAAM;AAC9D,KAAI,eAAe,cAAc,YAC/B,QAAO;EAAE;EAAa;EAAY;EAAa;AAGjD,KAAI,CAAC,QAAQ,KAAK,aAAa,CAAE,QAAO;AAExC,KAAI;EACF,MAAM,aAAa,MAAM,OAAO,cAAc,aAAa;AAC3D,MAAI,CAAC,WAAY,QAAO;EACxB,MAAM,EAAE,iCACN,MAAM,OAAO;AACf,SAAO,6BAA6B,WAAW,KAAK,IAAI;UACjD,KAAK;AACZ,SAAO,KACL,qFACA,IACD;AACD;;;;;;;;;;;;;;;;;;;;;;;;AAyBJ,eAAe,yBACb,GACA,QACA,OAMC;CACD,IAAI;AACJ,KAAI,OAAO;EACT,MAAM,EAAE,+BAA+B,mCACrC,MAAM,OAAO;AACf,cAAY,IAAI,+BACd,IAAI,8BAA8B,OAAO,EACzC,MACD;QACI;EACL,MAAM,EAAE,oBAAoB,wBAC1B,MAAM,OAAO;AACf,cAAY,IAAI,oBAAoB,IAAI,mBAAmB,OAAO,CAAC;;AAErE,QAAO;EACL;EACA,WAAW,gBACT,EAAE,qBAAqB,aAAa,CAAC,SAAS,YAAY;EAC5D,aAAa,EAAE,iBAAiB;EAGhC,gBAAgB,IAAI,IAAI,CACtB,KAAK,UAAU,EAAE,SAAS,MAAM,EAChC,KAAK,UAAU,EAAE,SAAS,KAAK,CAChC,CAAC;EACH;;;AAIH,SAAS,eAAmC;AAE1C,QADW,QAAQ,IAAI,sBAAsB,MAAM,IACtC;;;;;;;;;AAUf,SAAS,2BAA+C;CACtD,MAAM,MAAM,QAAQ,IAAI,iBAAiB,MAAM;AAC/C,KAAI,CAAC,IAAK,QAAO;AACjB,QAAO,gBAAgB,KAAK,IAAI,GAAG,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BtD,eAAsB,mBAAkC;AACtD,KAAI,YAAa;AACjB,eAAc;CAEd,MAAM,KAAK,cAAc;AACzB,KAAI,CAAC,GAAI;AAET,KAAI;AACF,WAAS,MAAM,OAAO;EACtB,MAAM,OAAO,0BAA0B;AACvC,eAAa;GACX,aAAa,QAAQ,IAAI,qBAAqB,MAAM,IAAI;GACxD,cAAc;GACd,GAAI,OAAO,EAAE,MAAM,GAAG,EAAE;GACzB;EAED,MAAM,eAAe,OAAO,mBAAmB;GAC7C,aAAa,WAAW;GACxB,GAAI,OAAO,EAAE,MAAM,GAAG,EAAE;GACzB,CAAC;EACF,MAAM,SAAS,IAAI,OAAO,aAAa;GACrC,aAAa,WAAW;GACxB;GACD,CAAC;AACF,eAAa,MAAM,kBAAkB,IAAI,OAAO;EAChD,MAAM,EAAE,WAAW,UAAU,aAAa,mBACxC,MAAM,yBAAyB,QAAQ,QAAQ,WAAW;AAC5D,mBAAiB,IAAI,yBAAyB,WAAW;GACvD;GACA;GACA;GACD,CAAC;AACF,mBAAiB,sBAAsB,eAAe;AACtD,YAAU;AACV,MAAI,WACF,QAAO,KACL,6DACA,IACA,WAAW,aACX,WAAW,YACX,WAAW,YACZ;MAED,QAAO,KAAK,gDAAgD,GAAG;UAE1D,KAAK;AACZ,MAAI,iBAAiB,IAAI,CACvB,QAAO,KACL,sHAED;MAED,QAAO,KAAK,qCAAqC,IAAI;;;;AAM3D,SAAS,iBAAiB,KAAuB;AAC/C,QACE,CAAC,CAAC,OACF,OAAO,QAAQ,YACf,UAAU,QACT,IAAI,SAAS,0BAA0B,IAAI,SAAS;;AAYzD,MAAM,eAA6B,EAAE,aAAa,IAAI;;;;;;;;;;;;;;;;AAiBtD,SAAS,mBAA4B;AACnC,KAAI,WAAY,QAAO;AACvB,cAAa;AAOb,KAAI,CAAC,UAAU,CAAC,cAAc,CAAC,eAAgB,QAAO;AACtD,KAAI;AACF,MAAI,CAAC,WAAY,QAAO,KAAK,WAAW;AACxC,iBAAe,OAAO;AACtB,SAAO;UACA,KAAK;AACZ,YAAU;AACV,SAAO,KAAK,mDAAmD,IAAI;AACnE,SAAO;;;;;;;;;;;;;;AAeX,SAAgB,oBAA0B;AACxC,mBAAkB;;;;;;;;;;AAWpB,eAAeK,QACb,UACA,MACA,QACA,IACY;AACZ,KAAI,CAAC,WAAW,CAAC,OAAQ,QAAO,GAAG,aAAa;CAChD,MAAM,IAAI;AACV,KAAI,CAAC,kBAAkB,CAAE,QAAO,GAAG,aAAa;CAChD,MAAM,OAAO,aAAa,UAAU,EAAE,SAAS,QAAQ,EAAE,SAAS;AAClE,QAAO,MAAM,EAAE,SACb,OAAO,SAAS;AACd,MAAI,WAAW,OAAW,MAAK,UAAU,OAAO;EAChD,IAAI,aAAa;EACjB,MAAM,SAAS,MAAM,GAAG,EACtB,WAAW,SAAS;AAClB,gBAAa;AACb,QAAK,WAAW,QAAQ;KAE3B,CAAC;AACF,MAAI,CAAC,cAAc,WAAW,OAAW,MAAK,WAAW,OAAO;AAChE,SAAO;IAET;EAAE;EAAM,UAAU;EAAM,CACzB;;;AAIH,SAAgB,WACd,MACA,QACA,IACY;AACZ,QAAOA,QAAM,SAAS,MAAM,QAAQ,GAAG;;;AAIzC,SAAgB,UACd,MACA,QACA,IACY;AACZ,QAAOA,QAAM,QAAQ,MAAM,QAAQ,GAAG;;;;;;;;;;;AAYxC,SAAgB,iBAAqC;AACnD,KAAI,CAAC,WAAW,CAAC,OAAQ,QAAO;AAChC,KAAI;AACF,SAAO,OAAO,sBAAsB,EAAE;SAChC;AACN;;;;;;;;;AAUJ,SAAgB,eAAe,OAAqB;AAClD,KAAI,CAAC,WAAW,CAAC,OAAQ;AACzB,KAAI;AACF,SAAO,mBAAmB,EAAE,UAAU,EAAE,oBAAoB,OAAO,EAAE,CAAC;UAC/D,KAAK;AACZ,SAAO,KAAK,sCAAsC,OAAO,IAAI"}
|
|
1
|
+
{"version":3,"file":"mlflow.js","names":["#inner","#popTrace","#spanTypeKey","#agentSpanTypes","#maxTracked","#flushTimeoutMs","#ready","#forwarded","#agentTraceIds","#boundedFlush","trace"],"sources":["../../../src/plugins/agents/mlflow.ts"],"sourcesContent":["import type { UnityCatalogLocation } from \"@mlflow/core\";\nimport { SpanKind } from \"@opentelemetry/api\";\nimport type { SpanProcessor } from \"@opentelemetry/sdk-trace-base\";\n\nimport { normalizeIdentityError } from \"../../context/execution-context\";\nimport { createLogger } from \"../../logging/logger\";\nimport { TelemetryManager } from \"../../telemetry\";\n\nconst logger = createLogger(\"agents\");\n\ntype MlflowModule = typeof import(\"@mlflow/core\");\ntype MlflowClientInstance = InstanceType<MlflowModule[\"MlflowClient\"]>;\n\ninterface MlflowInitConfig {\n trackingUri: string;\n experimentId: string;\n host?: string;\n}\n\nlet mlflow: MlflowModule | undefined;\nlet enabled = false;\nlet initStarted = false;\nlet configured = false;\nlet initConfig: MlflowInitConfig | undefined;\n// The resolved UC trace location, or undefined for classic experiment storage.\nlet ucLocation: UnityCatalogLocation | undefined;\nlet gatedProcessor: GatedMlflowSpanProcessor | undefined;\n\n/**\n * Wraps mlflow's OTel `SpanProcessor` and scopes it to agent traces. Two jobs:\n *\n * 1. Stay inert until ready. The classic processor's `onStart` calls its own\n * `getConfig()`, which THROWS before `init()` runs — and that throw\n * propagates out of `tracer.startSpan()`, so it would break unrelated AppKit\n * spans (HTTP, analytics) created between `TelemetryManager.start()` and the\n * first agent turn. We contribute this to AppKit's single tracer provider\n * during `setup()`, but only start forwarding once `ready()` is called by\n * {@link ensureConfigured}. (The UC processor reads no global config and\n * can't throw here, but forwarding is gated uniformly either way.)\n *\n * 2. Let only agent turns become MLflow traces. mlflow roots a trace at EVERY\n * parentless span, and AppKit's single provider carries every HTTP/DB span —\n * so unscoped, every request would become an MLflow trace. mlflow stamps\n * `mlflow.spanType` on EVERY span it processes (defaulting to `UNKNOWN`), so\n * presence alone can't tell an agent turn from a plain request — we key on\n * the value (AGENT/TOOL) instead. At the root's `onEnd` we forward it (mlflow\n * exports the trace) only if some span in the trace carried an AGENT/TOOL\n * type; otherwise `popTrace` to discard the trace mlflow built in memory. The\n * real span type is set after `onStart` (the constructor stamps UNKNOWN\n * first), and children end before their root, so the flag is set by the time\n * the root decides.\n *\n * It also drops the exporters' own outbound spans at `onStart` (parentless\n * CLIENT — outgoing requests made outside any agent turn, e.g. mlflow/OTLP\n * shipping a trace). Forwarding those would loop: each upload is an HTTP call\n * that auto-instrumentation turns into a new span to trace and upload.\n *\n * ponytail: non-agent requests still build (then discard) an in-memory trace\n * tree — allocation-only, no network (export happens only when we forward the\n * root's `onEnd`). Fine at normal QPS; if a very high-QPS app makes the churn\n * matter, root MLflow at a detached agent span instead (costs the HTTP envelope\n * on the trace and splits the OTLP trace).\n */\nexport class GatedMlflowSpanProcessor implements SpanProcessor {\n #inner: SpanProcessor;\n #ready = false;\n // Spans we forwarded `onStart` for, so `onEnd` stays balanced — mlflow never\n // sees an end without a matching start.\n #forwarded = new WeakSet<object>();\n // OTel trace ids that contained at least one mlflow (AGENT/TOOL) span, so the\n // root's `onEnd` exports rather than discards. Cleared as each root ends.\n #agentTraceIds = new Set<string>();\n #popTrace: (otelTraceId: string) => void;\n #spanTypeKey: string;\n // The `mlflow.spanType` attribute values (JSON-stringified) that mark a trace\n // as an agent turn — AGENT/TOOL. Every other value (notably UNKNOWN, which\n // mlflow stamps on all non-agent spans) is treated as non-agent.\n #agentSpanTypes: ReadonlySet<string>;\n // Leak backstop for #agentTraceIds — far above real concurrency. See onEnd.\n #maxTracked: number;\n // Cap on how long forceFlush/shutdown wait for a stuck export.\n #flushTimeoutMs: number;\n\n constructor(\n inner: SpanProcessor,\n deps: {\n popTrace: (otelTraceId: string) => void;\n spanTypeKey: string;\n agentSpanTypes: ReadonlySet<string>;\n maxTracked?: number;\n flushTimeoutMs?: number;\n },\n ) {\n this.#inner = inner;\n this.#popTrace = deps.popTrace;\n this.#spanTypeKey = deps.spanTypeKey;\n this.#agentSpanTypes = deps.agentSpanTypes;\n this.#maxTracked = deps.maxTracked ?? 1024;\n this.#flushTimeoutMs = deps.flushTimeoutMs ?? 5000;\n }\n\n ready(): void {\n this.#ready = true;\n }\n\n onStart(\n span: Parameters<SpanProcessor[\"onStart\"]>[0],\n parentContext: Parameters<SpanProcessor[\"onStart\"]>[1],\n ): void {\n if (!this.#ready) return;\n // Drop the exporters' own outbound calls. A parentless (root) CLIENT span is\n // an outgoing request made outside any agent turn — e.g. mlflow or OTLP\n // shipping a trace. Forwarding those would loop: each upload is itself an\n // HTTP call that auto-instrumentation turns into a new span to trace and\n // upload.\n if (span.kind === SpanKind.CLIENT && !span.parentSpanContext?.spanId) {\n return;\n }\n this.#forwarded.add(span);\n this.#inner.onStart(span, parentContext);\n }\n\n onEnd(span: Parameters<SpanProcessor[\"onEnd\"]>[0]): void {\n if (!this.#forwarded.has(span)) return;\n const traceId = span.spanContext().traceId;\n // An AGENT/TOOL span ended in this trace — mark it for export. mlflow stamps\n // `mlflow.spanType` on every span (UNKNOWN by default), so match the value,\n // not mere presence; the real type is set after `onStart`, so `onEnd` is the\n // earliest we can read it.\n if (\n this.#agentSpanTypes.has(span.attributes[this.#spanTypeKey] as string) &&\n !this.#agentTraceIds.has(traceId)\n ) {\n // Normally an entry lives only until its root's onEnd deletes it. But a\n // root that ends BEFORE its agent child (streaming client-disconnect) or\n // never ends (crash) orphans the entry, and #agentTraceIds — unlike the\n // GC-safe #forwarded WeakSet — is keyed by string, so it can't self-clean.\n // FIFO-evict at the cap so an abandoned-trace pattern can't grow it\n // unboundedly over process uptime.\n // ponytail: evicting a still-live trace only mis-discards it, and that\n // needs >#maxTracked concurrent agent turns — far above real load.\n if (this.#agentTraceIds.size >= this.#maxTracked) {\n const oldest = this.#agentTraceIds.values().next().value;\n if (oldest !== undefined) this.#agentTraceIds.delete(oldest);\n }\n this.#agentTraceIds.add(traceId);\n }\n if (span.parentSpanContext?.spanId) {\n // Non-root: mlflow's own `onEnd` early-returns, but forward for balance.\n this.#inner.onEnd(span);\n return;\n }\n // Root span: export only agent traces; discard everything else so plain HTTP\n // requests never become MLflow traces. `delete` reports whether it was agent.\n if (this.#agentTraceIds.delete(traceId)) {\n this.#inner.onEnd(span);\n } else {\n this.#popTrace(traceId);\n }\n }\n\n forceFlush(): Promise<void> {\n return this.#boundedFlush(() => this.#inner.forceFlush());\n }\n\n shutdown(): Promise<void> {\n return this.#boundedFlush(() => this.#inner.shutdown());\n }\n\n // Bound the inner flush/shutdown wait: both exporters export fire-and-forget,\n // so a stuck export only wedges here (graceful shutdown), never a turn.\n // Resolves — not rejects — on timeout: the caller is tearing down.\n async #boundedFlush(op: () => Promise<void>): Promise<void> {\n let timer: NodeJS.Timeout | undefined;\n try {\n await Promise.race([\n op().catch((err) => {\n logger.warn(\"MLflow trace flush error: %O\", err);\n }),\n new Promise<void>((resolve) => {\n timer = setTimeout(() => {\n logger.warn(\n \"MLflow trace flush exceeded %dms; continuing (export may still be in flight)\",\n this.#flushTimeoutMs,\n );\n resolve();\n }, this.#flushTimeoutMs);\n timer.unref(); // don't keep the event loop alive on the timeout alone\n }),\n ]);\n } finally {\n if (timer) clearTimeout(timer);\n }\n }\n}\n\n/**\n * Resolve the Unity Catalog trace location for the bound experiment, or\n * `undefined` for classic experiment-backed storage. Any failure falls back to\n * classic — a tracing misconfiguration must never break the agent.\n *\n * 1. Explicit env override — `MLFLOW_UC_CATALOG` + `MLFLOW_UC_SCHEMA` +\n * `MLFLOW_UC_TABLE_PREFIX`, all three required.\n * 2. Auto-detect from the linked Databricks experiment (numeric ids only, since\n * `GetExperiment` only accepts them): parse its `databricksTrace*` tags with\n * mlflow's own {@link ucLocationFromExperimentTags}, which also carries the\n * backend-populated spans/logs table names for custom-provisioned locations.\n * 3. Otherwise classic.\n *\n * `ucLocationFromExperimentTags` isn't on `@mlflow/core`'s public entrypoint, so\n * it's deep-imported like the exporter classes and covered by the tripwire test.\n */\nasync function resolveUcLocation(\n experimentId: string,\n client: MlflowClientInstance,\n): Promise<UnityCatalogLocation | undefined> {\n const catalogName = process.env.MLFLOW_UC_CATALOG?.trim();\n const schemaName = process.env.MLFLOW_UC_SCHEMA?.trim();\n const tablePrefix = process.env.MLFLOW_UC_TABLE_PREFIX?.trim();\n if (catalogName && schemaName && tablePrefix) {\n return { catalogName, schemaName, tablePrefix };\n }\n\n if (!/^\\d+$/.test(experimentId)) return undefined;\n\n try {\n const experiment = await client.getExperiment(experimentId);\n if (!experiment) return undefined;\n const { ucLocationFromExperimentTags } =\n await import(\"@mlflow/core/dist/core/destination\");\n return ucLocationFromExperimentTags(experiment.tags) ?? undefined;\n } catch (err) {\n logger.warn(\n \"MLflow UC trace-location auto-detect failed; using classic experiment storage: %O\",\n err,\n );\n return undefined;\n }\n}\n\n/**\n * Build mlflow's OTel `SpanProcessor` ourselves rather than letting `init()`\n * build and globally register its own tracer provider (its own `NodeSDK`). This\n * lets AppKit own the single global provider (OTLP + this processor), so agent\n * spans reach both MLflow and any OTLP endpoint without two SDKs racing for the\n * global slot.\n *\n * When a UC trace location is bound, builds the Unity Catalog processor +\n * exporter (V4 trace ids, spans uploaded to the experiment's UC table);\n * otherwise the classic experiment-backed processor. `createAuthProvider`,\n * `MlflowClient`, `InMemoryTraceManager` and `SpanAttributeKey` are all public\n * in `@mlflow/core`, so only the exporter/processor classes are deep-imported —\n * pinned to the exact version in package.json and guarded by a test that fails\n * loudly if a version bump renames them.\n *\n * Also resolves the hooks {@link GatedMlflowSpanProcessor} needs to scope\n * forwarding to agent traces: `popTrace` (to discard non-agent traces), the\n * `mlflow.spanType` attribute key, and the JSON-stringified AGENT/TOOL values\n * that mark a trace as an agent turn (mlflow stamps every span, defaulting to\n * UNKNOWN, so the gate must match the value, not presence).\n */\nasync function buildMlflowSpanProcessor(\n m: MlflowModule,\n client: MlflowClientInstance,\n ucLoc: UnityCatalogLocation | undefined,\n): Promise<{\n processor: SpanProcessor;\n popTrace: (otelTraceId: string) => void;\n spanTypeKey: string;\n agentSpanTypes: ReadonlySet<string>;\n}> {\n let processor: SpanProcessor;\n if (ucLoc) {\n const { DatabricksUCTableSpanExporter, DatabricksUCTableSpanProcessor } =\n await import(\"@mlflow/core/dist/exporters/uc_table\");\n processor = new DatabricksUCTableSpanProcessor(\n new DatabricksUCTableSpanExporter(client),\n ucLoc,\n );\n } else {\n const { MlflowSpanExporter, MlflowSpanProcessor } =\n await import(\"@mlflow/core/dist/exporters/mlflow\");\n processor = new MlflowSpanProcessor(new MlflowSpanExporter(client));\n }\n return {\n processor,\n popTrace: (otelTraceId) =>\n m.InMemoryTraceManager.getInstance().popTrace(otelTraceId),\n spanTypeKey: m.SpanAttributeKey.SPAN_TYPE,\n // mlflow JSON-stringifies attribute values, so the stored values are\n // `\"AGENT\"`/`\"TOOL\"` (quoted). Match that exact form.\n agentSpanTypes: new Set([\n JSON.stringify(m.SpanType.AGENT),\n JSON.stringify(m.SpanType.TOOL),\n ]),\n };\n}\n\n/** The bound MLflow experiment id, from the optional `experiment` resource. */\nfunction experimentId(): string | undefined {\n const id = process.env.MLFLOW_EXPERIMENT_ID?.trim();\n return id || undefined;\n}\n\n/**\n * Databricks host with a scheme. `@mlflow/core` uses `DATABRICKS_HOST`\n * verbatim to build request URLs and doesn't add `https://`, so a bare host\n * (`workspace.cloud.databricks.com`) makes `new URL()` throw. Pass an explicit\n * normalized host when the env var is set; when it isn't (profile-based auth),\n * return undefined and let the SDK read the host from `~/.databrickscfg`.\n */\nfunction normalizedDatabricksHost(): string | undefined {\n const raw = process.env.DATABRICKS_HOST?.trim();\n if (!raw) return undefined;\n return /^https?:\\/\\//i.test(raw) ? raw : `https://${raw}`;\n}\n\n/**\n * Initialize MLflow agent tracing once, when an experiment is bound — i.e. the\n * agents plugin's optional `experiment` resource is set (`MLFLOW_EXPERIMENT_ID`).\n * Called from the agents plugin's `setup()`, before `TelemetryManager.start()`.\n *\n * Rather than let `@mlflow/core`'s `init()` stand up and globally register its\n * own tracer provider (its `NodeSDK`, which would race AppKit's), we build the\n * span processor ourselves and contribute it to AppKit's single provider via\n * {@link TelemetryManager.registerSpanProcessor}. For the classic\n * experiment-backed processor, mlflow's global config is seeded by\n * {@link startAgentTracing} on the `\"setup:complete\"` lifecycle event (after\n * `start()`), with {@link ensureConfigured} as an idempotent lazy fallback; the\n * UC processor needs no seeded config, so that path never calls `init()`.\n *\n * The trace store is resolved here: a UC table prefix (env-configured or\n * auto-detected from the experiment's Databricks tags) vs. the classic\n * experiment. Auth is resolved by `@mlflow/core` from the app's own Databricks\n * credentials — `DATABRICKS_HOST`/`DATABRICKS_TOKEN` or a `~/.databrickscfg`\n * profile (`MLFLOW_TRACKING_URI=databricks://profile`) — so no tokens or OTLP\n * headers are wired by hand. A failure (missing creds, bad experiment) logs and\n * leaves tracing disabled rather than breaking the agent.\n *\n * Safe to call repeatedly; only the first call does work.\n */\nexport async function initAgentTracing(): Promise<void> {\n if (initStarted) return;\n initStarted = true;\n\n const id = experimentId();\n if (!id) return;\n\n try {\n mlflow = await import(\"@mlflow/core\");\n const host = normalizedDatabricksHost();\n initConfig = {\n trackingUri: process.env.MLFLOW_TRACKING_URI?.trim() || \"databricks\",\n experimentId: id,\n ...(host ? { host } : {}),\n };\n // One auth resolution + client, reused for UC auto-detect and the exporter.\n const authProvider = mlflow.createAuthProvider({\n trackingUri: initConfig.trackingUri,\n ...(host ? { host } : {}),\n });\n const client = new mlflow.MlflowClient({\n trackingUri: initConfig.trackingUri,\n authProvider,\n });\n ucLocation = await resolveUcLocation(id, client);\n const { processor, popTrace, spanTypeKey, agentSpanTypes } =\n await buildMlflowSpanProcessor(mlflow, client, ucLocation);\n gatedProcessor = new GatedMlflowSpanProcessor(processor, {\n popTrace,\n spanTypeKey,\n agentSpanTypes,\n });\n TelemetryManager.registerSpanProcessor(gatedProcessor);\n enabled = true;\n if (ucLocation) {\n logger.info(\n \"MLflow agent tracing enabled (experiment %s, UC %s.%s.%s)\",\n id,\n ucLocation.catalogName,\n ucLocation.schemaName,\n ucLocation.tablePrefix,\n );\n } else {\n logger.info(\"MLflow agent tracing enabled (experiment %s)\", id);\n }\n } catch (err) {\n if (isModuleNotFound(err)) {\n logger.warn(\n \"MLflow agent tracing requires the optional `@mlflow/core` package. \" +\n \"Install it to enable tracing: npm i @mlflow/core\",\n );\n } else {\n logger.warn(\"MLflow agent tracing disabled: %O\", err);\n }\n }\n}\n\n/** True when a dynamic `import()` failed because the package isn't installed. */\nfunction isModuleNotFound(err: unknown): boolean {\n return (\n !!err &&\n typeof err === \"object\" &&\n \"code\" in err &&\n (err.code === \"ERR_MODULE_NOT_FOUND\" || err.code === \"MODULE_NOT_FOUND\")\n );\n}\n\n/**\n * Records a span's outputs. Callers get one from `traceAgent`/`traceTool`;\n * it's a no-op when tracing is disabled, so call sites never branch on it.\n */\nexport interface SpanRecorder {\n setOutputs(outputs: unknown): void;\n}\n\nconst noopRecorder: SpanRecorder = { setOutputs() {} };\n\n/**\n * Seed the classic processor's global config once, AFTER\n * `TelemetryManager.start()` has registered AppKit's provider — driven eagerly\n * by {@link startAgentTracing} on `\"setup:complete\"`, or lazily by {@link trace}\n * as a fallback — then enable forwarding on the gated processor. Returns whether\n * tracing is usable.\n *\n * The classic experiment-backed `MlflowSpanProcessor` reads\n * `getConfig().experimentId` in `onStart`, so it needs `init()` to seed mlflow's\n * global config. `init()` also stands up its own `NodeSDK` whose provider loses\n * the global slot to AppKit's already-registered one (non-fatal); we call it\n * only for that config side-effect. The UC processor carries its location and\n * reads no global config, so the UC path skips `init()` entirely — no second\n * `NodeSDK`, no competing global registration by `@mlflow/core`.\n */\nfunction ensureConfigured(): boolean {\n if (configured) return enabled;\n configured = true;\n // `gatedProcessor` guard is load-bearing: if buildMlflowSpanProcessor threw,\n // `mlflow` and `initConfig` are still set but there is no gate. Calling\n // `mlflow.init()` then would stand up mlflow's OWN ungated provider — and if\n // AppKit registered none (no OTLP, no processor) it wins the global slot,\n // routing every span into mlflow un-gated: the exact over-tracing + exporter\n // loop this file exists to prevent.\n if (!mlflow || !initConfig || !gatedProcessor) return false;\n try {\n if (!ucLocation) mlflow.init(initConfig);\n gatedProcessor.ready();\n return true;\n } catch (err) {\n enabled = false;\n logger.warn(\"MLflow agent tracing disabled (init failed): %O\", err);\n return false;\n }\n}\n\n/**\n * Seed mlflow's config eagerly, right after `TelemetryManager.start()` — the\n * agents plugin wires this to the `\"setup:complete\"` lifecycle event, before the\n * server serves any request. Doing it here means the request's own root span is\n * already forwarded when the first turn runs, so that turn assembles into a\n * trace instead of being dropped (mlflow roots a trace only at the top-level\n * span). Idempotent. `trace()` also seeds lazily, but that only fully rescues a\n * turn whose agent span is itself the trace root; an HTTP-wrapped first turn\n * seeded lazily loses its root span (already started — and dropped — before\n * `ready()`), so this eager path is the reliable one.\n */\nexport function startAgentTracing(): void {\n ensureConfigured();\n}\n\n/**\n * Run `fn` inside an MLflow span of `spanType` when tracing is enabled,\n * otherwise just run it (zero overhead). Spans auto-nest via the SDK's active\n * context, so a TOOL span opened inside an AGENT span's callback becomes its\n * child. The callback's resolved value is recorded as the span's outputs unless\n * it called `setOutputs` first; return `undefined` (or set outputs explicitly)\n * when the return value isn't the output you want traced.\n */\nasync function trace<T>(\n spanType: \"AGENT\" | \"TOOL\",\n name: string,\n inputs: unknown,\n fn: (span: SpanRecorder) => Promise<T>,\n): Promise<T> {\n const execute = async (span: SpanRecorder): Promise<T> => {\n try {\n return await fn(span);\n } catch (error) {\n throw normalizeIdentityError(error);\n }\n };\n if (!enabled || !mlflow) return execute(noopRecorder);\n const m = mlflow;\n if (!ensureConfigured()) return execute(noopRecorder);\n const type = spanType === \"AGENT\" ? m.SpanType.AGENT : m.SpanType.TOOL;\n return await m.withSpan<T>(\n async (span) => {\n if (inputs !== undefined) span.setInputs(inputs);\n let outputsSet = false;\n const result = await execute({\n setOutputs(outputs) {\n outputsSet = true;\n span.setOutputs(outputs);\n },\n });\n if (!outputsSet && result !== undefined) span.setOutputs(result);\n return result;\n },\n { name, spanType: type },\n );\n}\n\n/** Trace a turn's root AGENT span. See {@link trace}. */\nexport function traceAgent<T>(\n name: string,\n inputs: unknown,\n fn: (span: SpanRecorder) => Promise<T>,\n): Promise<T> {\n return trace(\"AGENT\", name, inputs, fn);\n}\n\n/** Trace a TOOL span, nested under the active AGENT span. See {@link trace}. */\nexport function traceTool<T>(\n name: string,\n inputs: unknown,\n fn: (span: SpanRecorder) => Promise<T>,\n): Promise<T> {\n return trace(\"TOOL\", name, inputs, fn);\n}\n\n/**\n * The MLflow trace id for the active turn, when tracing is enabled. Must be\n * read inside an agent span so eval runs can correlate the turn to its trace\n * and attach assessments. Returns undefined when tracing is off.\n *\n * Reads the context-active span rather than `getLastActiveTraceId()`: the\n * latter is only populated when a root span *ends* (on export), so mid-turn it\n * returns the previous turn's id — or, under concurrent turns, another turn's.\n */\nexport function currentTraceId(): string | undefined {\n if (!enabled || !mlflow) return undefined;\n try {\n return mlflow.getCurrentActiveSpan()?.traceId;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Link the active turn's trace to an MLflow run by id, via the `mlflow.sourceRun`\n * trace metadata. Used by eval runs so each case's trace shows under the run.\n * Must be called while a trace is active (inside an agent span). No-op when\n * tracing is disabled.\n */\nexport function linkTraceToRun(runId: string): void {\n if (!enabled || !mlflow) return;\n try {\n mlflow.updateCurrentTrace({ metadata: { \"mlflow.sourceRun\": runId } });\n } catch (err) {\n logger.warn(\"Failed to link trace to run %s: %O\", runId, err);\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { createLogger } from "../../logging/logger.js";
|
|
2
|
+
import { normalizeIdentityError } from "../../context/execution-context.js";
|
|
3
|
+
import { createRequestScope } from "../../context/request-scope.js";
|
|
2
4
|
import { consumeAdapterStream } from "../../core/agent/consume-adapter-stream.js";
|
|
3
5
|
import { buildAdapterExtensions } from "./adapter-extensions.js";
|
|
4
6
|
import { requiresApproval } from "./approval.js";
|
|
7
|
+
import { isOboAgentRun, isOnBehalfOfUser, requireOboCaller, runInOboAgentRun } from "./auth-mode.js";
|
|
5
8
|
import { traceTool } from "./mlflow.js";
|
|
6
9
|
import { composePromptForAgent } from "./prompt.js";
|
|
7
10
|
import { normalizeToolResult } from "../../core/agent/normalize-result.js";
|
|
@@ -48,7 +51,8 @@ async function dispatchToolCall(deps, runState, toolIndex, name, args, depth) {
|
|
|
48
51
|
let toolResult;
|
|
49
52
|
try {
|
|
50
53
|
toolResult = await traceTool(name, args, () => runToolEntry(deps, runState, entry, name, args, depth));
|
|
51
|
-
} catch (
|
|
54
|
+
} catch (caught) {
|
|
55
|
+
const err = normalizeIdentityError(caught);
|
|
52
56
|
const error = err instanceof Error ? err.message : String(err);
|
|
53
57
|
logger.error("Tool '%s' failed (request %s): %O", name, runState.requestId, err);
|
|
54
58
|
runState.toolErrors.push({
|
|
@@ -102,6 +106,10 @@ async function runToolEntry(deps, runState, entry, name, args, depth) {
|
|
|
102
106
|
*/
|
|
103
107
|
async function runSubAgent(deps, runState, child, args, depth) {
|
|
104
108
|
if (depth > runState.limits.maxSubAgentDepth) throw new Error(`Sub-agent depth exceeded (limit ${runState.limits.maxSubAgentDepth}). Raise agents({ limits: { maxSubAgentDepth } }) or break the delegation cycle.`);
|
|
109
|
+
if (isOnBehalfOfUser(child.auth) && !isOboAgentRun()) {
|
|
110
|
+
requireOboCaller(runState.req);
|
|
111
|
+
return createRequestScope(runState.req).run(() => runInOboAgentRun(() => runSubAgent(deps, runState, child, args, depth)));
|
|
112
|
+
}
|
|
105
113
|
const input = typeof args === "object" && args !== null && typeof args.input === "string" ? args.input : JSON.stringify(args);
|
|
106
114
|
const childTools = Array.from(child.toolIndex.values()).filter((e) => e.source !== "hosted-supervisor").map((e) => e.def);
|
|
107
115
|
const childExecute = (name, childArgs) => dispatchToolCall(deps, runState, child.toolIndex, name, childArgs, depth);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tool-dispatch.js","names":[],"sources":["../../../src/plugins/agents/tool-dispatch.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\n\nimport type express from \"express\";\nimport type { AgentRunContext, Message, ResponseStreamEvent } from \"shared\";\n\nimport type { AppKitMcpClient } from \"../../connectors/mcp\";\nimport { consumeAdapterStream } from \"../../core/agent/consume-adapter-stream\";\nimport { normalizeToolResult } from \"../../core/agent/normalize-result\";\nimport type {\n BaseSystemPromptOption,\n RegisteredAgent,\n ResolvedToolEntry,\n} from \"../../core/agent/types\";\nimport type { PluginContext } from \"../../core/plugin-context\";\nimport { createLogger } from \"../../logging/logger\";\nimport { buildAdapterExtensions } from \"./adapter-extensions\";\nimport { requiresApproval } from \"./approval\";\nimport type { EventChannel } from \"./event-channel\";\nimport type { AgentEventTranslator } from \"./event-translator\";\nimport { traceTool } from \"./mlflow\";\nimport { composePromptForAgent } from \"./prompt\";\nimport type { ToolApprovalGate } from \"./tool-approval-gate\";\n\nconst logger = createLogger(\"agents:tools\");\n\n/**\n * Per-stream state shared between the top-level `executeTool` and any\n * `runSubAgent` calls below it. Carrying the budget counter, abort signal,\n * approval policy, and event-channel through one object is what lets the\n * sub-agent path enforce the same limits and approval gate as the parent.\n *\n * Without this shared state the sub-agent path silently bypassed both the\n * tool-call budget and the destructive-tool approval gate.\n */\nexport interface RunState {\n req: express.Request;\n userId: string;\n requestId: string;\n abortController: AbortController;\n signal: AbortSignal;\n approvalPolicy: { requireForDestructive: boolean; timeoutMs: number };\n limits: {\n maxConcurrentStreamsPerUser: number;\n maxToolCalls: number;\n maxSubAgentDepth: number;\n toolCallTimeoutMs: number;\n };\n translator: AgentEventTranslator;\n outboundEvents: EventChannel<ResponseStreamEvent>;\n /** Boxed mutable counter shared across parent + all sub-agent dispatches. */\n toolCallsUsed: { count: number };\n /**\n * Tool failures captured for the non-streaming `/invocations` response;\n * the streaming path already emits `tool_result` error events.\n */\n toolErrors: Array<{ tool: string; error: string }>;\n}\n\n/**\n * Plugin-instance collaborators the dispatch path needs. Passed as one object\n * so `dispatchToolCall`/`runSubAgent` stay free functions with a bounded\n * interface instead of reaching into the plugin. `getMcpClient` is a thunk so\n * a client connected after this object is built is still seen.\n */\nexport interface ToolDispatchDeps {\n approvalGate: ToolApprovalGate;\n context: PluginContext | undefined;\n getMcpClient: () => AppKitMcpClient | null;\n agents: Map<string, RegisteredAgent>;\n dispatchSkillTool: (\n entry: Extract<ResolvedToolEntry, { source: \"skill\" }>,\n args: unknown,\n ) => Promise<string>;\n pluginName: string;\n baseSystemPrompt: BaseSystemPromptOption | undefined;\n}\n\n/**\n * Dispatch a single tool call from either the top-level adapter or a\n * sub-agent. Centralising this in one function is what makes the budget\n * counter, approval gate, and abort signal observe sub-agent activity:\n * `runSubAgent` reuses the same `runState` and so increments the same\n * counter and emits approval events through the same channel.\n *\n * `depth` is the current sub-agent recursion depth (0 at the top level).\n * It is forwarded to `runSubAgent` when the dispatched entry is itself a\n * sub-agent, so depth limits remain enforced.\n */\nexport async function dispatchToolCall(\n deps: ToolDispatchDeps,\n runState: RunState,\n toolIndex: Map<string, ResolvedToolEntry>,\n name: string,\n args: unknown,\n depth: number,\n): Promise<unknown> {\n if (runState.toolCallsUsed.count >= runState.limits.maxToolCalls) {\n runState.abortController.abort(\n new Error(\n `Tool-call budget exhausted (limit ${runState.limits.maxToolCalls}).`,\n ),\n );\n throw new Error(\n `Tool-call budget exhausted (limit ${runState.limits.maxToolCalls}). Raise agents({ limits: { maxToolCalls } }) or review the agent's tool-selection logic.`,\n );\n }\n runState.toolCallsUsed.count++;\n\n const entry = toolIndex.get(name);\n if (!entry) throw new Error(`Unknown tool: ${name}`);\n\n if (\n runState.approvalPolicy.requireForDestructive &&\n requiresApproval(entry.def.annotations)\n ) {\n const approvalId = randomUUID();\n for (const ev of runState.translator.translate({\n type: \"approval_pending\",\n approvalId,\n streamId: runState.requestId,\n toolName: name,\n args,\n annotations: entry.def.annotations,\n })) {\n runState.outboundEvents.push(ev);\n }\n const decision = await deps.approvalGate.wait({\n approvalId,\n streamId: runState.requestId,\n userId: runState.userId,\n timeoutMs: runState.approvalPolicy.timeoutMs,\n });\n if (decision === \"deny\") {\n return `Tool execution denied by user approval gate (tool: ${name}).`;\n }\n }\n\n // Traced from here so the span covers execution only, not the approval\n // wait above (which is human latency).\n let toolResult: unknown;\n try {\n toolResult = await traceTool(name, args, () =>\n runToolEntry(deps, runState, entry, name, args, depth),\n );\n } catch (err) {\n const error = err instanceof Error ? err.message : String(err);\n logger.error(\n \"Tool '%s' failed (request %s): %O\",\n name,\n runState.requestId,\n err,\n );\n runState.toolErrors.push({ tool: name, error });\n throw err;\n }\n\n return normalizeToolResult(toolResult);\n}\n\n/**\n * Executes a resolved tool entry by source. Unknown sources fall through to\n * `undefined` (the tool index only holds these six).\n */\nasync function runToolEntry(\n deps: ToolDispatchDeps,\n runState: RunState,\n entry: ResolvedToolEntry,\n name: string,\n args: unknown,\n depth: number,\n): Promise<unknown> {\n switch (entry.source) {\n case \"toolkit\":\n if (!deps.context) {\n throw new Error(\n \"Plugin tool execution requires PluginContext; this should never happen through createApp\",\n );\n }\n return deps.context.executeTool(\n runState.req,\n entry.pluginName,\n entry.localName,\n args,\n runState.signal,\n runState.limits.toolCallTimeoutMs,\n );\n case \"function\": {\n // Function tools declare their parameters as a JSON-object schema, so\n // adapters always serialize `args` as an object. A non-object value here\n // means the model emitted malformed tool-call JSON; surface a clear error\n // rather than passing a wrong-shape value the tool will choke on.\n if (typeof args !== \"object\" || args === null || Array.isArray(args)) {\n throw new Error(\n `Function tool '${name}' received non-object arguments (got ${args === null ? \"null\" : Array.isArray(args) ? \"array\" : typeof args}); expected a JSON object.`,\n );\n }\n return entry.functionTool.execute(args as Record<string, unknown>);\n }\n case \"mcp\": {\n const mcpClient = deps.getMcpClient();\n if (!mcpClient) throw new Error(\"MCP client not connected\");\n const oboToken = runState.req.headers[\"x-forwarded-access-token\"];\n const mcpAuth =\n typeof oboToken === \"string\"\n ? { Authorization: `Bearer ${oboToken}` }\n : undefined;\n return mcpClient.callTool(entry.mcpToolName, args, mcpAuth);\n }\n case \"subagent\": {\n const childAgent = deps.agents.get(entry.agentName);\n if (!childAgent)\n throw new Error(`Sub-agent not found: ${entry.agentName}`);\n return runSubAgent(deps, runState, childAgent, args, depth + 1);\n }\n case \"hosted-supervisor\":\n // Defense-in-depth: should never fire. Hosted-supervisor entries are\n // routed via `AgentInput.extensions` and executed server-side; their\n // `def` is filtered out of the adapter's `tools`, so the model never\n // sees a callable schema. Reaching here means a non-SA adapter surfaced\n // the placeholder def — fail clearly rather than crash in normalize.\n throw new Error(\n `Tool '${name}' is a hosted-supervisor tool and cannot be invoked from the Node process. ` +\n \"It is executed server-side by the Databricks AI Gateway and is only reachable when the agent's model is a Supervisor API adapter.\",\n );\n case \"skill\":\n return deps.dispatchSkillTool(entry, args);\n }\n}\n\n/**\n * Runs a sub-agent in response to an `agent-<key>` tool call. Returns the\n * concatenated text output to hand back to the parent adapter as the tool\n * result.\n *\n * `depth` starts at 1 for a top-level sub-agent invocation and increments on\n * each nested call. Depths exceeding `limits.maxSubAgentDepth` are rejected\n * before any adapter work.\n *\n * Sub-agent tool calls run through `dispatchToolCall` with the same\n * `runState` as the parent — the budget counter and approval gate are\n * therefore enforced for every nested call, not only at the top level.\n */\nexport async function runSubAgent(\n deps: ToolDispatchDeps,\n runState: RunState,\n child: RegisteredAgent,\n args: unknown,\n depth: number,\n): Promise<string> {\n if (depth > runState.limits.maxSubAgentDepth) {\n throw new Error(\n `Sub-agent depth exceeded (limit ${runState.limits.maxSubAgentDepth}). ` +\n `Raise agents({ limits: { maxSubAgentDepth } }) or break the delegation cycle.`,\n );\n }\n\n const input =\n typeof args === \"object\" &&\n args !== null &&\n typeof (args as { input?: unknown }).input === \"string\"\n ? (args as { input: string }).input\n : JSON.stringify(args);\n // Same filter as the top-level path: hosted-supervisor `def` is a\n // placeholder, not a callable function — exclude from the adapter's\n // `tools` array. The specs are routed via `extensions` instead.\n const childTools = Array.from(child.toolIndex.values())\n .filter((e) => e.source !== \"hosted-supervisor\")\n .map((e) => e.def);\n\n const childExecute = (name: string, childArgs: unknown): Promise<unknown> =>\n dispatchToolCall(deps, runState, child.toolIndex, name, childArgs, depth);\n\n const runContext: AgentRunContext = {\n executeTool: childExecute,\n signal: runState.signal,\n };\n\n const pluginNames = deps.context\n ? deps.context\n .getPluginNames()\n .filter((n) => n !== deps.pluginName && n !== \"server\")\n : [];\n const systemPrompt = composePromptForAgent(child, deps.baseSystemPrompt, {\n agentName: child.name,\n pluginNames,\n toolNames: childTools.map((t) => t.name),\n });\n\n const messages: Message[] = [\n {\n id: \"system\",\n role: \"system\",\n content: systemPrompt,\n createdAt: new Date(),\n },\n {\n id: randomUUID(),\n role: \"user\",\n content: input,\n createdAt: new Date(),\n },\n ];\n\n return consumeAdapterStream(\n child.adapter.run(\n {\n messages,\n tools: childTools,\n threadId: randomUUID(),\n signal: runState.signal,\n extensions: buildAdapterExtensions(child.toolIndex),\n },\n runContext,\n ),\n {\n signal: runState.signal,\n // Forward every sub-agent event into the parent's outbound SSE\n // stream so the client sees nested tool_call / tool_result events\n // (UI-action tools like apply_filter / highlight_period rely on\n // this) and the sub-agent's streaming text as it's generated.\n //\n // `metadata` is the one exception: sub-agents have their own\n // threadId, and forwarding it would overwrite the parent's\n // thread state on the client and break multi-turn continuity.\n // Approval-pending events emitted by `dispatchToolCall` already\n // reach `outboundEvents` directly, so they are not routed here.\n onEvent: (event) => {\n if (event.type === \"metadata\") return;\n for (const translated of runState.translator.translate(event)) {\n runState.outboundEvents.push(translated);\n }\n },\n },\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"tool-dispatch.js","names":[],"sources":["../../../src/plugins/agents/tool-dispatch.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\n\nimport type express from \"express\";\nimport type { AgentRunContext, Message, ResponseStreamEvent } from \"shared\";\n\nimport type { AppKitMcpClient } from \"../../connectors/mcp\";\nimport { normalizeIdentityError } from \"../../context/execution-context\";\nimport { createRequestScope } from \"../../context/request-scope\";\nimport { consumeAdapterStream } from \"../../core/agent/consume-adapter-stream\";\nimport { normalizeToolResult } from \"../../core/agent/normalize-result\";\nimport type {\n BaseSystemPromptOption,\n RegisteredAgent,\n ResolvedToolEntry,\n} from \"../../core/agent/types\";\nimport type { PluginContext } from \"../../core/plugin-context\";\nimport { createLogger } from \"../../logging/logger\";\nimport { buildAdapterExtensions } from \"./adapter-extensions\";\nimport { requiresApproval } from \"./approval\";\nimport {\n isOboAgentRun,\n isOnBehalfOfUser,\n requireOboCaller,\n runInOboAgentRun,\n} from \"./auth-mode\";\nimport type { EventChannel } from \"./event-channel\";\nimport type { AgentEventTranslator } from \"./event-translator\";\nimport { traceTool } from \"./mlflow\";\nimport { composePromptForAgent } from \"./prompt\";\nimport type { ToolApprovalGate } from \"./tool-approval-gate\";\n\nconst logger = createLogger(\"agents:tools\");\n\n/**\n * Per-stream state shared between the top-level `executeTool` and any\n * `runSubAgent` calls below it. Carrying the budget counter, abort signal,\n * approval policy, and event-channel through one object is what lets the\n * sub-agent path enforce the same limits and approval gate as the parent.\n *\n * Without this shared state the sub-agent path silently bypassed both the\n * tool-call budget and the destructive-tool approval gate.\n */\nexport interface RunState {\n req: express.Request;\n userId: string;\n requestId: string;\n abortController: AbortController;\n signal: AbortSignal;\n approvalPolicy: { requireForDestructive: boolean; timeoutMs: number };\n limits: {\n maxConcurrentStreamsPerUser: number;\n maxToolCalls: number;\n maxSubAgentDepth: number;\n toolCallTimeoutMs: number;\n };\n translator: AgentEventTranslator;\n outboundEvents: EventChannel<ResponseStreamEvent>;\n /** Boxed mutable counter shared across parent + all sub-agent dispatches. */\n toolCallsUsed: { count: number };\n /**\n * Tool failures captured for the non-streaming `/invocations` response;\n * the streaming path already emits `tool_result` error events.\n */\n toolErrors: Array<{ tool: string; error: string }>;\n}\n\n/**\n * Plugin-instance collaborators the dispatch path needs. Passed as one object\n * so `dispatchToolCall`/`runSubAgent` stay free functions with a bounded\n * interface instead of reaching into the plugin. `getMcpClient` is a thunk so\n * a client connected after this object is built is still seen.\n */\nexport interface ToolDispatchDeps {\n approvalGate: ToolApprovalGate;\n context: PluginContext | undefined;\n getMcpClient: () => AppKitMcpClient | null;\n agents: Map<string, RegisteredAgent>;\n dispatchSkillTool: (\n entry: Extract<ResolvedToolEntry, { source: \"skill\" }>,\n args: unknown,\n ) => Promise<string>;\n pluginName: string;\n baseSystemPrompt: BaseSystemPromptOption | undefined;\n}\n\n/**\n * Dispatch a single tool call from either the top-level adapter or a\n * sub-agent. Centralising this in one function is what makes the budget\n * counter, approval gate, and abort signal observe sub-agent activity:\n * `runSubAgent` reuses the same `runState` and so increments the same\n * counter and emits approval events through the same channel.\n *\n * `depth` is the current sub-agent recursion depth (0 at the top level).\n * It is forwarded to `runSubAgent` when the dispatched entry is itself a\n * sub-agent, so depth limits remain enforced.\n */\nexport async function dispatchToolCall(\n deps: ToolDispatchDeps,\n runState: RunState,\n toolIndex: Map<string, ResolvedToolEntry>,\n name: string,\n args: unknown,\n depth: number,\n): Promise<unknown> {\n if (runState.toolCallsUsed.count >= runState.limits.maxToolCalls) {\n runState.abortController.abort(\n new Error(\n `Tool-call budget exhausted (limit ${runState.limits.maxToolCalls}).`,\n ),\n );\n throw new Error(\n `Tool-call budget exhausted (limit ${runState.limits.maxToolCalls}). Raise agents({ limits: { maxToolCalls } }) or review the agent's tool-selection logic.`,\n );\n }\n runState.toolCallsUsed.count++;\n\n const entry = toolIndex.get(name);\n if (!entry) throw new Error(`Unknown tool: ${name}`);\n\n if (\n runState.approvalPolicy.requireForDestructive &&\n requiresApproval(entry.def.annotations)\n ) {\n const approvalId = randomUUID();\n for (const ev of runState.translator.translate({\n type: \"approval_pending\",\n approvalId,\n streamId: runState.requestId,\n toolName: name,\n args,\n annotations: entry.def.annotations,\n })) {\n runState.outboundEvents.push(ev);\n }\n const decision = await deps.approvalGate.wait({\n approvalId,\n streamId: runState.requestId,\n userId: runState.userId,\n timeoutMs: runState.approvalPolicy.timeoutMs,\n });\n if (decision === \"deny\") {\n return `Tool execution denied by user approval gate (tool: ${name}).`;\n }\n }\n\n // Traced from here so the span covers execution only, not the approval\n // wait above (which is human latency).\n let toolResult: unknown;\n try {\n toolResult = await traceTool(name, args, () =>\n runToolEntry(deps, runState, entry, name, args, depth),\n );\n } catch (caught) {\n const err = normalizeIdentityError(caught);\n const error = err instanceof Error ? err.message : String(err);\n logger.error(\n \"Tool '%s' failed (request %s): %O\",\n name,\n runState.requestId,\n err,\n );\n runState.toolErrors.push({ tool: name, error });\n throw err;\n }\n\n return normalizeToolResult(toolResult);\n}\n\n/**\n * Executes a resolved tool entry by source. Unknown sources fall through to\n * `undefined` (the tool index only holds these six).\n */\nasync function runToolEntry(\n deps: ToolDispatchDeps,\n runState: RunState,\n entry: ResolvedToolEntry,\n name: string,\n args: unknown,\n depth: number,\n): Promise<unknown> {\n switch (entry.source) {\n case \"toolkit\":\n if (!deps.context) {\n throw new Error(\n \"Plugin tool execution requires PluginContext; this should never happen through createApp\",\n );\n }\n return deps.context.executeTool(\n runState.req,\n entry.pluginName,\n entry.localName,\n args,\n runState.signal,\n runState.limits.toolCallTimeoutMs,\n );\n case \"function\": {\n // Function tools declare their parameters as a JSON-object schema, so\n // adapters always serialize `args` as an object. A non-object value here\n // means the model emitted malformed tool-call JSON; surface a clear error\n // rather than passing a wrong-shape value the tool will choke on.\n if (typeof args !== \"object\" || args === null || Array.isArray(args)) {\n throw new Error(\n `Function tool '${name}' received non-object arguments (got ${args === null ? \"null\" : Array.isArray(args) ? \"array\" : typeof args}); expected a JSON object.`,\n );\n }\n return entry.functionTool.execute(args as Record<string, unknown>);\n }\n case \"mcp\": {\n const mcpClient = deps.getMcpClient();\n if (!mcpClient) throw new Error(\"MCP client not connected\");\n const oboToken = runState.req.headers[\"x-forwarded-access-token\"];\n const mcpAuth =\n typeof oboToken === \"string\"\n ? { Authorization: `Bearer ${oboToken}` }\n : undefined;\n return mcpClient.callTool(entry.mcpToolName, args, mcpAuth);\n }\n case \"subagent\": {\n const childAgent = deps.agents.get(entry.agentName);\n if (!childAgent)\n throw new Error(`Sub-agent not found: ${entry.agentName}`);\n return runSubAgent(deps, runState, childAgent, args, depth + 1);\n }\n case \"hosted-supervisor\":\n // Defense-in-depth: should never fire. Hosted-supervisor entries are\n // routed via `AgentInput.extensions` and executed server-side; their\n // `def` is filtered out of the adapter's `tools`, so the model never\n // sees a callable schema. Reaching here means a non-SA adapter surfaced\n // the placeholder def — fail clearly rather than crash in normalize.\n throw new Error(\n `Tool '${name}' is a hosted-supervisor tool and cannot be invoked from the Node process. ` +\n \"It is executed server-side by the Databricks AI Gateway and is only reachable when the agent's model is a Supervisor API adapter.\",\n );\n case \"skill\":\n return deps.dispatchSkillTool(entry, args);\n }\n}\n\n/**\n * Runs a sub-agent in response to an `agent-<key>` tool call. Returns the\n * concatenated text output to hand back to the parent adapter as the tool\n * result.\n *\n * `depth` starts at 1 for a top-level sub-agent invocation and increments on\n * each nested call. Depths exceeding `limits.maxSubAgentDepth` are rejected\n * before any adapter work.\n *\n * Sub-agent tool calls run through `dispatchToolCall` with the same\n * `runState` as the parent — the budget counter and approval gate are\n * therefore enforced for every nested call, not only at the top level.\n */\nexport async function runSubAgent(\n deps: ToolDispatchDeps,\n runState: RunState,\n child: RegisteredAgent,\n args: unknown,\n depth: number,\n): Promise<string> {\n if (depth > runState.limits.maxSubAgentDepth) {\n throw new Error(\n `Sub-agent depth exceeded (limit ${runState.limits.maxSubAgentDepth}). ` +\n `Raise agents({ limits: { maxSubAgentDepth } }) or break the delegation cycle.`,\n );\n }\n // An on-behalf-of-user child opens the user scope; under an on-behalf-of-user\n // parent the marker is already set, so every child stays the user.\n if (isOnBehalfOfUser(child.auth) && !isOboAgentRun()) {\n requireOboCaller(runState.req);\n return createRequestScope(runState.req).run(() =>\n runInOboAgentRun(() => runSubAgent(deps, runState, child, args, depth)),\n );\n }\n\n const input =\n typeof args === \"object\" &&\n args !== null &&\n typeof (args as { input?: unknown }).input === \"string\"\n ? (args as { input: string }).input\n : JSON.stringify(args);\n // Same filter as the top-level path: hosted-supervisor `def` is a\n // placeholder, not a callable function — exclude from the adapter's\n // `tools` array. The specs are routed via `extensions` instead.\n const childTools = Array.from(child.toolIndex.values())\n .filter((e) => e.source !== \"hosted-supervisor\")\n .map((e) => e.def);\n\n const childExecute = (name: string, childArgs: unknown): Promise<unknown> =>\n dispatchToolCall(deps, runState, child.toolIndex, name, childArgs, depth);\n\n const runContext: AgentRunContext = {\n executeTool: childExecute,\n signal: runState.signal,\n };\n\n const pluginNames = deps.context\n ? deps.context\n .getPluginNames()\n .filter((n) => n !== deps.pluginName && n !== \"server\")\n : [];\n const systemPrompt = composePromptForAgent(child, deps.baseSystemPrompt, {\n agentName: child.name,\n pluginNames,\n toolNames: childTools.map((t) => t.name),\n });\n\n const messages: Message[] = [\n {\n id: \"system\",\n role: \"system\",\n content: systemPrompt,\n createdAt: new Date(),\n },\n {\n id: randomUUID(),\n role: \"user\",\n content: input,\n createdAt: new Date(),\n },\n ];\n\n return consumeAdapterStream(\n child.adapter.run(\n {\n messages,\n tools: childTools,\n threadId: randomUUID(),\n signal: runState.signal,\n extensions: buildAdapterExtensions(child.toolIndex),\n },\n runContext,\n ),\n {\n signal: runState.signal,\n // Forward every sub-agent event into the parent's outbound SSE\n // stream so the client sees nested tool_call / tool_result events\n // (UI-action tools like apply_filter / highlight_period rely on\n // this) and the sub-agent's streaming text as it's generated.\n //\n // `metadata` is the one exception: sub-agents have their own\n // threadId, and forwarding it would overwrite the parent's\n // thread state on the client and break multi-turn continuity.\n // Approval-pending events emitted by `dispatchToolCall` already\n // reach `outboundEvents` directly, so they are not routed here.\n onEvent: (event) => {\n if (event.type === \"metadata\") return;\n for (const translated of runState.translator.translate(event)) {\n runState.outboundEvents.push(translated);\n }\n },\n },\n );\n}\n"],"mappings":"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"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { IAppRouter, ToPlugin } from "../../shared/src/plugin.js";
|
|
2
2
|
import "../../shared/src/index.js";
|
|
3
|
+
import { PluginManifest } from "../../registry/types.js";
|
|
3
4
|
import { Plugin } from "../../plugin/plugin.js";
|
|
4
5
|
import "../../plugin/index.js";
|
|
5
|
-
import { PluginManifest } from "../../registry/types.js";
|
|
6
6
|
import "../../index.js";
|
|
7
7
|
import { IAiSearchConfig, IndexSummary, SearchRequest, SearchResponse } from "./types.js";
|
|
8
8
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ai-search.d.ts","names":[],"sources":["../../../src/plugins/ai-search/ai-search.ts"],"mappings":";;;;;;;;;cAiCa,cAAA,SAAuB,MAAA,CAAO,eAAA;EAAA,OAClC,QAAA,EADiD,cAAA;EAAA,iBAGvC,WAAA;EAAA,UAEC,MAAA,EAAQ,eAAA;EAAA,QAElB,SAAA;cAEI,MAAA,EAAQ,eAAA;EATT;;;;EAAA,QAyBH,eAAA;EAKF,KAAA,CAAA,GAAS,OAAA;EArBK;;;;;;EAAA,QAqDN,oBAAA;EAAA,QAoCN,4BAAA;EAkBR,YAAA,CAAa,MAAA,EAAQ,UAAA;
|
|
1
|
+
{"version":3,"file":"ai-search.d.ts","names":[],"sources":["../../../src/plugins/ai-search/ai-search.ts"],"mappings":";;;;;;;;;cAiCa,cAAA,SAAuB,MAAA,CAAO,eAAA;EAAA,OAClC,QAAA,EADiD,cAAA;EAAA,iBAGvC,WAAA;EAAA,UAEC,MAAA,EAAQ,eAAA;EAAA,QAElB,SAAA;cAEI,MAAA,EAAQ,eAAA;EATT;;;;EAAA,QAyBH,eAAA;EAKF,KAAA,CAAA,GAAS,OAAA;EArBK;;;;;;EAAA,QAqDN,oBAAA;EAAA,QAoCN,4BAAA;EAkBR,YAAA,CAAa,MAAA,EAAQ,UAAA;EAiKlB;;;;EArBH,YAAA,CAAA;IAAkB,OAAA,EAAS,YAAA;EAAA;EAqBxB;;;;;;;EAHG,KAAA,WAAgB,MAAA,oBAA0B,MAAA,kBAAA,CAC9C,KAAA,UACA,OAAA,EAAS,aAAA,GACR,OAAA,CAAQ,cAAA,CAAe,CAAA;EAiCpB,QAAA,CAAA,GAAY,OAAA;EAIlB,OAAA,CAAA;sBAxCsB,MAAA,oBAAuB,MAAA,mBAAA,KAAA,UAC9B,OAAA,EACJ,aAAA,KACR,OAAA,CAAQ,cAAA,CAAe,CAAA;EAAA;EAAA,QA2ClB,aAAA;EAvTY;EAAA,QAkUZ,aAAA;EAlTA;EAAA,QAkUA,WAAA;EA7TO;;;;;EAAA,QAgVP,mBAAA;EAAA,QAcM,aAAA;EA5HI;;;;;;;;;EAAA,QA0KV,YAAA;EAAA,QAsBA,WAAA;EA3KkB;EAAA,QAgLlB,gBAAA;EA/IU;EAAA,QAgKV,gBAAA;EAAA,QAUA,gBAAA;EAAA,QAiBA,cAAA;EAAA,QA2BA,YAAA;AAAA;AAAA,cAcG,QAAA,EAAQ,QAAA,QAAA,cAAA,EAAA,eAAA"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createLogger } from "../../logging/logger.js";
|
|
2
|
-
import {
|
|
2
|
+
import { getCurrentPrincipalId, getWorkspaceClient } from "../../context/execution-context.js";
|
|
3
3
|
import { formatWarningBanner } from "../../utils/banner.js";
|
|
4
4
|
import "../../context/index.js";
|
|
5
5
|
import { Plugin } from "../../plugin/plugin.js";
|
|
@@ -97,7 +97,7 @@ var AiSearchPlugin = class extends Plugin {
|
|
|
97
97
|
}
|
|
98
98
|
const { columns: _clientColumns, ...safeBody } = body;
|
|
99
99
|
const isAsUser = indexConfig.auth === "on-behalf-of-user";
|
|
100
|
-
const plugin = isAsUser ? this.
|
|
100
|
+
const plugin = isAsUser ? this._asUserScoped(req) : this;
|
|
101
101
|
const queryType = safeBody.queryType ?? indexConfig.queryType ?? "hybrid";
|
|
102
102
|
const executorKey = isAsUser ? this.resolveUserId(req) : "global";
|
|
103
103
|
try {
|
|
@@ -145,7 +145,7 @@ var AiSearchPlugin = class extends Plugin {
|
|
|
145
145
|
}
|
|
146
146
|
const pageQueryType = queryType ?? indexConfig.queryType ?? "hybrid";
|
|
147
147
|
try {
|
|
148
|
-
const result = await (indexConfig.auth === "on-behalf-of-user" ? this.
|
|
148
|
+
const result = await (indexConfig.auth === "on-behalf-of-user" ? this._asUserScoped(req) : this).execute(async (signal) => this.connector.queryNextPage(getWorkspaceClient(), {
|
|
149
149
|
indexName: indexConfig.indexName,
|
|
150
150
|
endpointName: indexConfig.endpointName,
|
|
151
151
|
pageToken
|
|
@@ -202,7 +202,7 @@ var AiSearchPlugin = class extends Plugin {
|
|
|
202
202
|
indexName: indexConfig.indexName,
|
|
203
203
|
...prepared
|
|
204
204
|
}, signal);
|
|
205
|
-
}, this._executeSettings(request, indexConfig,
|
|
205
|
+
}, this._executeSettings(request, indexConfig, getCurrentPrincipalId()));
|
|
206
206
|
if (!result.ok) throw new Error(`Vector search query failed for index "${alias}": ${result.message}`);
|
|
207
207
|
return this._parseResponse(result.data, queryType);
|
|
208
208
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ai-search.js","names":["manifest"],"sources":["../../../src/plugins/ai-search/ai-search.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\n\nimport type express from \"express\";\nimport type { CacheConfig, IAppRouter, PluginExecutionSettings } from \"shared\";\n\nimport { AiSearchConnector } from \"../../connectors/ai-search/client\";\nimport type {\n VsQueryParams,\n VsRawResponse,\n} from \"../../connectors/ai-search/types\";\nimport { getCurrentUserId, getWorkspaceClient } from \"../../context\";\nimport { createLogger } from \"../../logging/logger\";\nimport { Plugin, toPlugin } from \"../../plugin\";\nimport { defineManifest } from \"../../registry\";\nimport { formatWarningBanner } from \"../../utils/banner\";\nimport { aiSearchDefaults } from \"./defaults\";\nimport manifest from \"./manifest.json\";\nimport type {\n IAiSearchConfig,\n IndexConfig,\n IndexSummary,\n SearchQueryType,\n SearchRequest,\n SearchResponse,\n SearchResult,\n} from \"./types\";\n\nconst logger = createLogger(\"ai-search\");\n\nconst querySettings: PluginExecutionSettings = {\n default: aiSearchDefaults,\n};\n\nexport class AiSearchPlugin extends Plugin<IAiSearchConfig> {\n static manifest = defineManifest<\"aiSearch\">(manifest);\n\n protected static description =\n \"Query Databricks Vector Search indexes with hybrid search, reranking, and pagination\";\n declare protected config: IAiSearchConfig;\n\n private connector: AiSearchConnector;\n\n constructor(config: IAiSearchConfig) {\n super(config);\n this.config = {\n ...config,\n indexes: config.indexes ?? this._defaultIndexes(),\n };\n this.connector = new AiSearchConnector({\n timeout: config.timeout,\n telemetry: config.telemetry,\n });\n }\n\n /**\n * Seeds a `default` index from `DATABRICKS_VS_INDEX_NAME` when no `indexes`\n * are configured, so `aiSearch()` works with just the env var.\n */\n private _defaultIndexes(): Record<string, IndexConfig> {\n const indexName = process.env.DATABRICKS_VS_INDEX_NAME;\n return indexName ? { default: { indexName } } : {};\n }\n\n async setup(): Promise<void> {\n // pagination needs an endpointName the framework's resource validation\n // can't see, so check it here.\n for (const [alias, idx] of Object.entries(this.config.indexes ?? {})) {\n if (idx.pagination && !idx.endpointName) {\n throw new Error(\n `Index \"${alias}\" has pagination enabled but is missing \"endpointName\"`,\n );\n }\n }\n\n // Dev fills in missing `columns` from the source table; prod can't query\n // without them (VS requires `columns`), so fail fast at boot.\n if (process.env.NODE_ENV === \"development\") {\n await this._autoDiscoverColumns();\n } else {\n for (const [alias, idx] of Object.entries(this.config.indexes ?? {})) {\n if (!idx.columns || idx.columns.length === 0) {\n throw new Error(\n `Index \"${alias}\" has no columns configured. Vector Search queries require \"columns\"; set them explicitly (auto-discovered only in development).`,\n );\n }\n }\n }\n }\n\n /**\n * For each configured index missing `columns`, fill them from its Delta-Sync\n * source table (all source columns minus embedding vectors). Best-effort:\n * failures are logged and skipped, never thrown. A partial `columns_to_sync`\n * isn't honored, so the discovered list is a starting point to trim.\n */\n private async _autoDiscoverColumns(): Promise<void> {\n const discovered: Record<string, string[]> = {};\n for (const [alias, idx] of Object.entries(this.config.indexes ?? {})) {\n if (idx.columns && idx.columns.length > 0) continue;\n const indexName = idx.indexName ?? process.env.DATABRICKS_VS_INDEX_NAME;\n if (!indexName) continue;\n try {\n const client = getWorkspaceClient();\n const info = await this.connector.getIndex(client, indexName);\n const sourceTable = info.delta_sync_index_spec?.source_table;\n if (!sourceTable) continue;\n const excluded = new Set(\n (info.delta_sync_index_spec?.embedding_vector_columns ?? []).map(\n (c) => c.name,\n ),\n );\n const columns = (\n await this.connector.getSourceColumns(client, sourceTable)\n ).filter((c) => !excluded.has(c));\n if (columns.length > 0) {\n idx.columns = columns;\n discovered[alias] = columns;\n }\n } catch (error) {\n logger.warn(\n 'Could not auto-discover columns for index \"%s\": %s',\n alias,\n error instanceof Error ? error.message : String(error),\n );\n }\n }\n if (Object.keys(discovered).length > 0) {\n logger.warn(\"\\n%s\", this._formatColumnDiscoveryBanner(discovered));\n }\n }\n\n private _formatColumnDiscoveryBanner(\n discovered: Record<string, string[]>,\n ): string {\n const lines = [\n \"AI SEARCH: columns auto-discovered (dev mode — would fail in production)\",\n \"\",\n ];\n for (const [alias, columns] of Object.entries(discovered)) {\n lines.push(` ${alias}: ${columns.join(\", \")}`);\n }\n lines.push(\"\");\n lines.push(\n \"Set `columns` explicitly in the plugin config before deploying.\",\n );\n\n return formatWarningBanner(lines);\n }\n\n injectRoutes(router: IAppRouter) {\n this.route(router, {\n name: \"query\",\n method: \"post\",\n path: \"/:alias/query\",\n handler: async (req: express.Request, res: express.Response) => {\n const indexConfig = this._resolveOr404(req, res);\n if (!indexConfig) return;\n\n const body: SearchRequest = req.body;\n if (!body.queryText && !body.queryVector) {\n res.status(400).json({\n error: \"queryText or queryVector is required\",\n plugin: this.name,\n });\n return;\n }\n\n // Drop client-supplied `columns` so an HTTP caller can't widen the\n // projection past what the app configured. (query() callers are\n // trusted and keep the override.)\n const { columns: _clientColumns, ...safeBody } = body;\n const isAsUser = indexConfig.auth === \"on-behalf-of-user\";\n const plugin = isAsUser ? this.asUser(req) : this;\n const queryType =\n safeBody.queryType ?? indexConfig.queryType ?? \"hybrid\";\n\n // Key per user for OBO so results never leak across users; SP shares \"global\".\n const executorKey = isAsUser ? this.resolveUserId(req) : \"global\";\n\n try {\n // Prepare inside execute so a self-managed embeddingFn runs in the\n // same OBO context as the query, not as the service principal.\n const result = await plugin.execute(\n async (signal) => {\n const prepared = await this._prepareQuery(safeBody, indexConfig);\n return this.connector.query(\n getWorkspaceClient(),\n { indexName: indexConfig.indexName, ...prepared },\n signal,\n );\n },\n this._executeSettings(safeBody, indexConfig, executorKey),\n );\n\n this._sendResult(res, result, queryType);\n } catch (error) {\n this._handleError(res, error, \"Query failed\");\n }\n },\n });\n\n this.route(router, {\n name: \"queryNextPage\",\n method: \"post\",\n path: \"/:alias/next-page\",\n handler: async (req: express.Request, res: express.Response) => {\n const indexConfig = this._resolveOr404(req, res);\n if (!indexConfig) return;\n\n if (!indexConfig.pagination) {\n res.status(400).json({\n error: `Pagination is not enabled for index \"${req.params.alias}\"`,\n plugin: this.name,\n });\n return;\n }\n\n if (!indexConfig.endpointName) {\n res.status(400).json({\n error: `Index \"${req.params.alias}\" is missing endpointName required for pagination`,\n plugin: this.name,\n });\n return;\n }\n\n const { pageToken, queryType } = req.body;\n if (!pageToken) {\n res.status(400).json({\n error: \"pageToken is required\",\n plugin: this.name,\n });\n return;\n }\n // Echo the original query's queryType so paged responses stay\n // consistent with page 1; fall back to the index default.\n const pageQueryType = queryType ?? indexConfig.queryType ?? \"hybrid\";\n\n try {\n const plugin =\n indexConfig.auth === \"on-behalf-of-user\" ? this.asUser(req) : this;\n\n // Uncached: a page token is a single-use cursor. `querySettings` has\n // no `cacheKey`, so caching stays off.\n const result = await plugin.execute(\n async (signal) =>\n this.connector.queryNextPage(\n getWorkspaceClient(),\n {\n indexName: indexConfig.indexName,\n endpointName: indexConfig.endpointName as string,\n pageToken,\n },\n signal,\n ),\n querySettings,\n );\n\n this._sendResult(res, result, pageQueryType);\n } catch (error) {\n this._handleError(res, error, \"Next-page query failed\");\n }\n },\n });\n\n this.route(router, {\n name: \"getConfig\",\n method: \"get\",\n path: \"/:alias/config\",\n handler: async (req: express.Request, res: express.Response) => {\n const indexConfig = this._resolveOr404(req, res);\n if (!indexConfig) return;\n res.json({\n alias: req.params.alias,\n columns: indexConfig.columns,\n queryType: indexConfig.queryType ?? \"hybrid\",\n numResults: indexConfig.numResults ?? 20,\n reranker: !!indexConfig.reranker,\n pagination: !!indexConfig.pagination,\n });\n },\n });\n }\n\n /**\n * Index aliases + non-sensitive query metadata, serialized to the client so\n * the UI can discover available indexes instead of hardcoding an alias.\n */\n clientConfig(): { indexes: IndexSummary[] } {\n const indexes = Object.entries(this.config.indexes ?? {}).map(\n ([alias, idx]) => ({\n alias,\n queryType: idx.queryType ?? \"hybrid\",\n pagination: !!idx.pagination,\n }),\n );\n return { indexes };\n }\n\n /**\n * Programmatic query API — available as `appkit.aiSearch.query()`.\n * When called through `asUser(req)`, executes with the user's credentials.\n *\n * @remarks `T` types each result's `data` but is an unchecked cast — the row\n * shape isn't validated at runtime.\n */\n async query<T extends Record<string, unknown> = Record<string, unknown>>(\n alias: string,\n request: SearchRequest,\n ): Promise<SearchResponse<T>> {\n const indexConfig = this._resolveIndex(alias);\n if (!indexConfig) {\n throw new Error(`No index configured with alias \"${alias}\"`);\n }\n\n // Resolve queryType for the response here; _prepareQuery runs inside\n // execute so a cache hit skips the embedding and the VS call.\n const { queryType } = this._resolveQueryParams(request, indexConfig);\n\n // getCurrentUserId() is the user's id under asUser(), the service id\n // otherwise — keying per caller like the route's executorKey.\n const result = await this.execute(\n async (signal) => {\n const prepared = await this._prepareQuery(request, indexConfig);\n return this.connector.query(\n getWorkspaceClient(),\n { indexName: indexConfig.indexName, ...prepared },\n signal,\n );\n },\n this._executeSettings(request, indexConfig, getCurrentUserId()),\n );\n\n if (!result.ok) {\n throw new Error(\n `Vector search query failed for index \"${alias}\": ${result.message}`,\n );\n }\n\n return this._parseResponse(result.data, queryType);\n }\n\n async shutdown(): Promise<void> {\n // No streams or persistent connections to clean up\n }\n\n exports() {\n return {\n query: this.query.bind(this),\n };\n }\n\n private _resolveIndex(\n alias: string,\n ): (IndexConfig & { indexName: string }) | undefined {\n const idx = this.config.indexes?.[alias];\n if (!idx) return undefined;\n const indexName = idx.indexName ?? process.env.DATABRICKS_VS_INDEX_NAME;\n if (!indexName) return undefined;\n return { ...idx, indexName };\n }\n\n /** Resolve an index by route alias, or send a 404 and return null. */\n private _resolveOr404(\n req: express.Request,\n res: express.Response,\n ): (IndexConfig & { indexName: string }) | null {\n const indexConfig = this._resolveIndex(req.params.alias);\n if (!indexConfig) {\n res.status(404).json({\n error: `No index configured with alias \"${req.params.alias}\"`,\n plugin: this.name,\n });\n return null;\n }\n return indexConfig;\n }\n\n /** Send an execution result as JSON, or its error status/message. */\n private _sendResult(\n res: express.Response,\n result: Awaited<ReturnType<typeof this.execute<VsRawResponse>>>,\n queryType: SearchQueryType,\n ): void {\n if (!result.ok) {\n res\n .status(result.status)\n .json({ error: result.message, plugin: this.name });\n return;\n }\n res.json(this._parseResponse(result.data, queryType));\n }\n\n /**\n * Resolve request-vs-index defaults for the result-determining fields.\n * Shared by `_prepareQuery` (the payload) and `_cacheKeyFor` (the key) so\n * the two can't drift.\n */\n private _resolveQueryParams(\n request: SearchRequest,\n indexConfig: IndexConfig,\n ): Pick<VsQueryParams, \"queryType\" | \"columns\" | \"numResults\" | \"reranker\"> {\n const queryType = request.queryType ?? indexConfig.queryType ?? \"hybrid\";\n const columns = request.columns ?? indexConfig.columns ?? [];\n return {\n queryType,\n columns,\n numResults: request.numResults ?? indexConfig.numResults ?? 20,\n reranker: this._resolveReranker(request.reranker, indexConfig, columns),\n };\n }\n\n private async _prepareQuery(\n request: SearchRequest,\n indexConfig: IndexConfig,\n ): Promise<Omit<VsQueryParams, \"indexName\">> {\n const { queryType, columns, numResults, reranker } =\n this._resolveQueryParams(request, indexConfig);\n let queryText = request.queryText;\n let queryVector = request.queryVector;\n\n // full_text uses no vector; hybrid keeps the text for its keyword half.\n if (\n indexConfig.embeddingFn &&\n queryText &&\n !queryVector &&\n queryType !== \"full_text\"\n ) {\n try {\n queryVector = await indexConfig.embeddingFn(queryText);\n if (queryType === \"ann\") queryText = undefined;\n } catch (error) {\n throw new Error(\n `Embedding generation failed: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n\n return {\n queryText,\n queryVector,\n queryType,\n columns,\n numResults,\n filters: request.filters,\n reranker,\n };\n }\n\n /**\n * Cache key for a query: every input that changes the VS result, resolved\n * via `_resolveQueryParams` so the key matches the payload (post-allowlist\n * `columns`, not the raw request). `queryVector` is hashed (vectors are\n * large); the key uses `queryText`, not the derived embedding, since the\n * embedding is a function of `queryText` and hasn't run at key-build time.\n * `columns`/`filters` are order-normalized so equivalent requests share an\n * entry.\n */\n private _cacheKeyFor(\n request: SearchRequest,\n indexConfig: IndexConfig & { indexName: string },\n executorKey: string,\n ): CacheConfig[\"cacheKey\"] {\n const { queryType, columns, numResults, reranker } =\n this._resolveQueryParams(request, indexConfig);\n return [\n \"ai-search:query\",\n indexConfig.indexName,\n request.queryText ?? \"\",\n request.queryVector ? this._hashVector(request.queryVector) : \"\",\n queryType,\n numResults,\n // columns is a projection; order doesn't affect results, so sort a copy.\n JSON.stringify([...columns].sort()),\n this._stableStringify(request.filters ?? null),\n String(!!reranker),\n executorKey,\n ];\n }\n\n private _hashVector(vector: number[]): string {\n return createHash(\"sha256\").update(JSON.stringify(vector)).digest(\"hex\");\n }\n\n /** Execute settings with the per-call cache key folded in. */\n private _executeSettings(\n request: SearchRequest,\n indexConfig: IndexConfig & { indexName: string },\n executorKey: string,\n ): PluginExecutionSettings {\n return {\n default: {\n ...aiSearchDefaults,\n cache: {\n ...aiSearchDefaults.cache,\n cacheKey: this._cacheKeyFor(request, indexConfig, executorKey),\n },\n },\n };\n }\n\n /** `JSON.stringify` with object keys sorted recursively; array order kept. */\n private _stableStringify(value: unknown): string {\n return JSON.stringify(value, (_key, val) =>\n val && typeof val === \"object\" && !Array.isArray(val)\n ? Object.fromEntries(\n Object.entries(val).sort(([a], [b]) => a.localeCompare(b)),\n )\n : val,\n );\n }\n\n private _resolveReranker(\n requestReranker: boolean | undefined,\n indexConfig: IndexConfig,\n columns: string[],\n ): { columnsToRerank: string[] } | undefined {\n const shouldRerank = requestReranker ?? indexConfig.reranker;\n if (!shouldRerank) return undefined;\n\n if (typeof indexConfig.reranker === \"object\") {\n return indexConfig.reranker;\n }\n // Auto-derive from returnable columns (excluding the id). With no columns\n // resolved there's nothing to rerank on, so skip it.\n const columnsToRerank = columns.filter((c) => c !== \"id\");\n return columnsToRerank.length > 0 ? { columnsToRerank } : undefined;\n }\n\n private _parseResponse<\n T extends Record<string, unknown> = Record<string, unknown>,\n >(raw: VsRawResponse, queryType: SearchQueryType): SearchResponse<T> {\n const columnNames = raw.manifest.columns.map((c) => c.name);\n const scoreIndex = columnNames.indexOf(\"score\");\n\n const results: SearchResult<T>[] = raw.result.data_array.map((row) => {\n const data: Record<string, unknown> = {};\n for (let i = 0; i < columnNames.length; i++) {\n if (i !== scoreIndex) data[columnNames[i]] = row[i];\n }\n return {\n score: scoreIndex >= 0 ? (row[scoreIndex] as number) : 0,\n data: data as T,\n };\n });\n\n return {\n results,\n totalCount: raw.result.row_count,\n queryTimeMs:\n raw.debug_info?.response_time ?? raw.debug_info?.latency_ms ?? 0,\n queryType,\n nextPageToken: raw.next_page_token ?? null,\n };\n }\n\n private _handleError(\n res: express.Response,\n error: unknown,\n fallbackMessage: string,\n ): void {\n logger.error(\"%s: %O\", fallbackMessage, error);\n // Match Plugin.execute(): the raw message is only exposed outside production.\n const isDev = process.env.NODE_ENV !== \"production\";\n const message =\n isDev && error instanceof Error ? error.message : fallbackMessage;\n res.status(500).json({ error: message, plugin: this.name });\n }\n}\n\nexport const aiSearch = toPlugin(AiSearchPlugin);\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ai-search.js","names":["manifest"],"sources":["../../../src/plugins/ai-search/ai-search.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\n\nimport type express from \"express\";\nimport type { CacheConfig, IAppRouter, PluginExecutionSettings } from \"shared\";\n\nimport { AiSearchConnector } from \"../../connectors/ai-search/client\";\nimport type {\n VsQueryParams,\n VsRawResponse,\n} from \"../../connectors/ai-search/types\";\nimport { getCurrentPrincipalId, getWorkspaceClient } from \"../../context\";\nimport { createLogger } from \"../../logging/logger\";\nimport { Plugin, toPlugin } from \"../../plugin\";\nimport { defineManifest } from \"../../registry\";\nimport { formatWarningBanner } from \"../../utils/banner\";\nimport { aiSearchDefaults } from \"./defaults\";\nimport manifest from \"./manifest.json\";\nimport type {\n IAiSearchConfig,\n IndexConfig,\n IndexSummary,\n SearchQueryType,\n SearchRequest,\n SearchResponse,\n SearchResult,\n} from \"./types\";\n\nconst logger = createLogger(\"ai-search\");\n\nconst querySettings: PluginExecutionSettings = {\n default: aiSearchDefaults,\n};\n\nexport class AiSearchPlugin extends Plugin<IAiSearchConfig> {\n static manifest = defineManifest<\"aiSearch\">(manifest);\n\n protected static description =\n \"Query Databricks Vector Search indexes with hybrid search, reranking, and pagination\";\n declare protected config: IAiSearchConfig;\n\n private connector: AiSearchConnector;\n\n constructor(config: IAiSearchConfig) {\n super(config);\n this.config = {\n ...config,\n indexes: config.indexes ?? this._defaultIndexes(),\n };\n this.connector = new AiSearchConnector({\n timeout: config.timeout,\n telemetry: config.telemetry,\n });\n }\n\n /**\n * Seeds a `default` index from `DATABRICKS_VS_INDEX_NAME` when no `indexes`\n * are configured, so `aiSearch()` works with just the env var.\n */\n private _defaultIndexes(): Record<string, IndexConfig> {\n const indexName = process.env.DATABRICKS_VS_INDEX_NAME;\n return indexName ? { default: { indexName } } : {};\n }\n\n async setup(): Promise<void> {\n // pagination needs an endpointName the framework's resource validation\n // can't see, so check it here.\n for (const [alias, idx] of Object.entries(this.config.indexes ?? {})) {\n if (idx.pagination && !idx.endpointName) {\n throw new Error(\n `Index \"${alias}\" has pagination enabled but is missing \"endpointName\"`,\n );\n }\n }\n\n // Dev fills in missing `columns` from the source table; prod can't query\n // without them (VS requires `columns`), so fail fast at boot.\n if (process.env.NODE_ENV === \"development\") {\n await this._autoDiscoverColumns();\n } else {\n for (const [alias, idx] of Object.entries(this.config.indexes ?? {})) {\n if (!idx.columns || idx.columns.length === 0) {\n throw new Error(\n `Index \"${alias}\" has no columns configured. Vector Search queries require \"columns\"; set them explicitly (auto-discovered only in development).`,\n );\n }\n }\n }\n }\n\n /**\n * For each configured index missing `columns`, fill them from its Delta-Sync\n * source table (all source columns minus embedding vectors). Best-effort:\n * failures are logged and skipped, never thrown. A partial `columns_to_sync`\n * isn't honored, so the discovered list is a starting point to trim.\n */\n private async _autoDiscoverColumns(): Promise<void> {\n const discovered: Record<string, string[]> = {};\n for (const [alias, idx] of Object.entries(this.config.indexes ?? {})) {\n if (idx.columns && idx.columns.length > 0) continue;\n const indexName = idx.indexName ?? process.env.DATABRICKS_VS_INDEX_NAME;\n if (!indexName) continue;\n try {\n const client = getWorkspaceClient();\n const info = await this.connector.getIndex(client, indexName);\n const sourceTable = info.delta_sync_index_spec?.source_table;\n if (!sourceTable) continue;\n const excluded = new Set(\n (info.delta_sync_index_spec?.embedding_vector_columns ?? []).map(\n (c) => c.name,\n ),\n );\n const columns = (\n await this.connector.getSourceColumns(client, sourceTable)\n ).filter((c) => !excluded.has(c));\n if (columns.length > 0) {\n idx.columns = columns;\n discovered[alias] = columns;\n }\n } catch (error) {\n logger.warn(\n 'Could not auto-discover columns for index \"%s\": %s',\n alias,\n error instanceof Error ? error.message : String(error),\n );\n }\n }\n if (Object.keys(discovered).length > 0) {\n logger.warn(\"\\n%s\", this._formatColumnDiscoveryBanner(discovered));\n }\n }\n\n private _formatColumnDiscoveryBanner(\n discovered: Record<string, string[]>,\n ): string {\n const lines = [\n \"AI SEARCH: columns auto-discovered (dev mode — would fail in production)\",\n \"\",\n ];\n for (const [alias, columns] of Object.entries(discovered)) {\n lines.push(` ${alias}: ${columns.join(\", \")}`);\n }\n lines.push(\"\");\n lines.push(\n \"Set `columns` explicitly in the plugin config before deploying.\",\n );\n\n return formatWarningBanner(lines);\n }\n\n injectRoutes(router: IAppRouter) {\n this.route(router, {\n name: \"query\",\n method: \"post\",\n path: \"/:alias/query\",\n handler: async (req: express.Request, res: express.Response) => {\n const indexConfig = this._resolveOr404(req, res);\n if (!indexConfig) return;\n\n const body: SearchRequest = req.body;\n if (!body.queryText && !body.queryVector) {\n res.status(400).json({\n error: \"queryText or queryVector is required\",\n plugin: this.name,\n });\n return;\n }\n\n // Drop client-supplied `columns` so an HTTP caller can't widen the\n // projection past what the app configured. (query() callers are\n // trusted and keep the override.)\n const { columns: _clientColumns, ...safeBody } = body;\n const isAsUser = indexConfig.auth === \"on-behalf-of-user\";\n const plugin = isAsUser ? this._asUserScoped(req) : this;\n const queryType =\n safeBody.queryType ?? indexConfig.queryType ?? \"hybrid\";\n\n // Key per user for OBO so results never leak across users; SP shares \"global\".\n const executorKey = isAsUser ? this.resolveUserId(req) : \"global\";\n\n try {\n // Prepare inside execute so a self-managed embeddingFn runs in the\n // same OBO context as the query, not as the service principal.\n const result = await plugin.execute(\n async (signal) => {\n const prepared = await this._prepareQuery(safeBody, indexConfig);\n return this.connector.query(\n getWorkspaceClient(),\n { indexName: indexConfig.indexName, ...prepared },\n signal,\n );\n },\n this._executeSettings(safeBody, indexConfig, executorKey),\n );\n\n this._sendResult(res, result, queryType);\n } catch (error) {\n this._handleError(res, error, \"Query failed\");\n }\n },\n });\n\n this.route(router, {\n name: \"queryNextPage\",\n method: \"post\",\n path: \"/:alias/next-page\",\n handler: async (req: express.Request, res: express.Response) => {\n const indexConfig = this._resolveOr404(req, res);\n if (!indexConfig) return;\n\n if (!indexConfig.pagination) {\n res.status(400).json({\n error: `Pagination is not enabled for index \"${req.params.alias}\"`,\n plugin: this.name,\n });\n return;\n }\n\n if (!indexConfig.endpointName) {\n res.status(400).json({\n error: `Index \"${req.params.alias}\" is missing endpointName required for pagination`,\n plugin: this.name,\n });\n return;\n }\n\n const { pageToken, queryType } = req.body;\n if (!pageToken) {\n res.status(400).json({\n error: \"pageToken is required\",\n plugin: this.name,\n });\n return;\n }\n // Echo the original query's queryType so paged responses stay\n // consistent with page 1; fall back to the index default.\n const pageQueryType = queryType ?? indexConfig.queryType ?? \"hybrid\";\n\n try {\n const plugin =\n indexConfig.auth === \"on-behalf-of-user\"\n ? this._asUserScoped(req)\n : this;\n\n // Uncached: a page token is a single-use cursor. `querySettings` has\n // no `cacheKey`, so caching stays off.\n const result = await plugin.execute(\n async (signal) =>\n this.connector.queryNextPage(\n getWorkspaceClient(),\n {\n indexName: indexConfig.indexName,\n endpointName: indexConfig.endpointName as string,\n pageToken,\n },\n signal,\n ),\n querySettings,\n );\n\n this._sendResult(res, result, pageQueryType);\n } catch (error) {\n this._handleError(res, error, \"Next-page query failed\");\n }\n },\n });\n\n this.route(router, {\n name: \"getConfig\",\n method: \"get\",\n path: \"/:alias/config\",\n handler: async (req: express.Request, res: express.Response) => {\n const indexConfig = this._resolveOr404(req, res);\n if (!indexConfig) return;\n res.json({\n alias: req.params.alias,\n columns: indexConfig.columns,\n queryType: indexConfig.queryType ?? \"hybrid\",\n numResults: indexConfig.numResults ?? 20,\n reranker: !!indexConfig.reranker,\n pagination: !!indexConfig.pagination,\n });\n },\n });\n }\n\n /**\n * Index aliases + non-sensitive query metadata, serialized to the client so\n * the UI can discover available indexes instead of hardcoding an alias.\n */\n clientConfig(): { indexes: IndexSummary[] } {\n const indexes = Object.entries(this.config.indexes ?? {}).map(\n ([alias, idx]) => ({\n alias,\n queryType: idx.queryType ?? \"hybrid\",\n pagination: !!idx.pagination,\n }),\n );\n return { indexes };\n }\n\n /**\n * Programmatic query API — available as `appkit.aiSearch.query()`.\n * When called through `asUser(req)`, executes with the user's credentials.\n *\n * @remarks `T` types each result's `data` but is an unchecked cast — the row\n * shape isn't validated at runtime.\n */\n async query<T extends Record<string, unknown> = Record<string, unknown>>(\n alias: string,\n request: SearchRequest,\n ): Promise<SearchResponse<T>> {\n const indexConfig = this._resolveIndex(alias);\n if (!indexConfig) {\n throw new Error(`No index configured with alias \"${alias}\"`);\n }\n\n // Resolve queryType for the response here; _prepareQuery runs inside\n // execute so a cache hit skips the embedding and the VS call.\n const { queryType } = this._resolveQueryParams(request, indexConfig);\n\n // getCurrentPrincipalId() is the user's id under asUser(), the service id\n // otherwise — keying per caller like the route's executorKey.\n const result = await this.execute(\n async (signal) => {\n const prepared = await this._prepareQuery(request, indexConfig);\n return this.connector.query(\n getWorkspaceClient(),\n { indexName: indexConfig.indexName, ...prepared },\n signal,\n );\n },\n this._executeSettings(request, indexConfig, getCurrentPrincipalId()),\n );\n\n if (!result.ok) {\n throw new Error(\n `Vector search query failed for index \"${alias}\": ${result.message}`,\n );\n }\n\n return this._parseResponse(result.data, queryType);\n }\n\n async shutdown(): Promise<void> {\n // No streams or persistent connections to clean up\n }\n\n exports() {\n return {\n query: this.query.bind(this),\n };\n }\n\n private _resolveIndex(\n alias: string,\n ): (IndexConfig & { indexName: string }) | undefined {\n const idx = this.config.indexes?.[alias];\n if (!idx) return undefined;\n const indexName = idx.indexName ?? process.env.DATABRICKS_VS_INDEX_NAME;\n if (!indexName) return undefined;\n return { ...idx, indexName };\n }\n\n /** Resolve an index by route alias, or send a 404 and return null. */\n private _resolveOr404(\n req: express.Request,\n res: express.Response,\n ): (IndexConfig & { indexName: string }) | null {\n const indexConfig = this._resolveIndex(req.params.alias);\n if (!indexConfig) {\n res.status(404).json({\n error: `No index configured with alias \"${req.params.alias}\"`,\n plugin: this.name,\n });\n return null;\n }\n return indexConfig;\n }\n\n /** Send an execution result as JSON, or its error status/message. */\n private _sendResult(\n res: express.Response,\n result: Awaited<ReturnType<typeof this.execute<VsRawResponse>>>,\n queryType: SearchQueryType,\n ): void {\n if (!result.ok) {\n res\n .status(result.status)\n .json({ error: result.message, plugin: this.name });\n return;\n }\n res.json(this._parseResponse(result.data, queryType));\n }\n\n /**\n * Resolve request-vs-index defaults for the result-determining fields.\n * Shared by `_prepareQuery` (the payload) and `_cacheKeyFor` (the key) so\n * the two can't drift.\n */\n private _resolveQueryParams(\n request: SearchRequest,\n indexConfig: IndexConfig,\n ): Pick<VsQueryParams, \"queryType\" | \"columns\" | \"numResults\" | \"reranker\"> {\n const queryType = request.queryType ?? indexConfig.queryType ?? \"hybrid\";\n const columns = request.columns ?? indexConfig.columns ?? [];\n return {\n queryType,\n columns,\n numResults: request.numResults ?? indexConfig.numResults ?? 20,\n reranker: this._resolveReranker(request.reranker, indexConfig, columns),\n };\n }\n\n private async _prepareQuery(\n request: SearchRequest,\n indexConfig: IndexConfig,\n ): Promise<Omit<VsQueryParams, \"indexName\">> {\n const { queryType, columns, numResults, reranker } =\n this._resolveQueryParams(request, indexConfig);\n let queryText = request.queryText;\n let queryVector = request.queryVector;\n\n // full_text uses no vector; hybrid keeps the text for its keyword half.\n if (\n indexConfig.embeddingFn &&\n queryText &&\n !queryVector &&\n queryType !== \"full_text\"\n ) {\n try {\n queryVector = await indexConfig.embeddingFn(queryText);\n if (queryType === \"ann\") queryText = undefined;\n } catch (error) {\n throw new Error(\n `Embedding generation failed: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n\n return {\n queryText,\n queryVector,\n queryType,\n columns,\n numResults,\n filters: request.filters,\n reranker,\n };\n }\n\n /**\n * Cache key for a query: every input that changes the VS result, resolved\n * via `_resolveQueryParams` so the key matches the payload (post-allowlist\n * `columns`, not the raw request). `queryVector` is hashed (vectors are\n * large); the key uses `queryText`, not the derived embedding, since the\n * embedding is a function of `queryText` and hasn't run at key-build time.\n * `columns`/`filters` are order-normalized so equivalent requests share an\n * entry.\n */\n private _cacheKeyFor(\n request: SearchRequest,\n indexConfig: IndexConfig & { indexName: string },\n executorKey: string,\n ): CacheConfig[\"cacheKey\"] {\n const { queryType, columns, numResults, reranker } =\n this._resolveQueryParams(request, indexConfig);\n return [\n \"ai-search:query\",\n indexConfig.indexName,\n request.queryText ?? \"\",\n request.queryVector ? this._hashVector(request.queryVector) : \"\",\n queryType,\n numResults,\n // columns is a projection; order doesn't affect results, so sort a copy.\n JSON.stringify([...columns].sort()),\n this._stableStringify(request.filters ?? null),\n String(!!reranker),\n executorKey,\n ];\n }\n\n private _hashVector(vector: number[]): string {\n return createHash(\"sha256\").update(JSON.stringify(vector)).digest(\"hex\");\n }\n\n /** Execute settings with the per-call cache key folded in. */\n private _executeSettings(\n request: SearchRequest,\n indexConfig: IndexConfig & { indexName: string },\n executorKey: string,\n ): PluginExecutionSettings {\n return {\n default: {\n ...aiSearchDefaults,\n cache: {\n ...aiSearchDefaults.cache,\n cacheKey: this._cacheKeyFor(request, indexConfig, executorKey),\n },\n },\n };\n }\n\n /** `JSON.stringify` with object keys sorted recursively; array order kept. */\n private _stableStringify(value: unknown): string {\n return JSON.stringify(value, (_key, val) =>\n val && typeof val === \"object\" && !Array.isArray(val)\n ? Object.fromEntries(\n Object.entries(val).sort(([a], [b]) => a.localeCompare(b)),\n )\n : val,\n );\n }\n\n private _resolveReranker(\n requestReranker: boolean | undefined,\n indexConfig: IndexConfig,\n columns: string[],\n ): { columnsToRerank: string[] } | undefined {\n const shouldRerank = requestReranker ?? indexConfig.reranker;\n if (!shouldRerank) return undefined;\n\n if (typeof indexConfig.reranker === \"object\") {\n return indexConfig.reranker;\n }\n // Auto-derive from returnable columns (excluding the id). With no columns\n // resolved there's nothing to rerank on, so skip it.\n const columnsToRerank = columns.filter((c) => c !== \"id\");\n return columnsToRerank.length > 0 ? { columnsToRerank } : undefined;\n }\n\n private _parseResponse<\n T extends Record<string, unknown> = Record<string, unknown>,\n >(raw: VsRawResponse, queryType: SearchQueryType): SearchResponse<T> {\n const columnNames = raw.manifest.columns.map((c) => c.name);\n const scoreIndex = columnNames.indexOf(\"score\");\n\n const results: SearchResult<T>[] = raw.result.data_array.map((row) => {\n const data: Record<string, unknown> = {};\n for (let i = 0; i < columnNames.length; i++) {\n if (i !== scoreIndex) data[columnNames[i]] = row[i];\n }\n return {\n score: scoreIndex >= 0 ? (row[scoreIndex] as number) : 0,\n data: data as T,\n };\n });\n\n return {\n results,\n totalCount: raw.result.row_count,\n queryTimeMs:\n raw.debug_info?.response_time ?? raw.debug_info?.latency_ms ?? 0,\n queryType,\n nextPageToken: raw.next_page_token ?? null,\n };\n }\n\n private _handleError(\n res: express.Response,\n error: unknown,\n fallbackMessage: string,\n ): void {\n logger.error(\"%s: %O\", fallbackMessage, error);\n // Match Plugin.execute(): the raw message is only exposed outside production.\n const isDev = process.env.NODE_ENV !== \"production\";\n const message =\n isDev && error instanceof Error ? error.message : fallbackMessage;\n res.status(500).json({ error: message, plugin: this.name });\n }\n}\n\nexport const aiSearch = toPlugin(AiSearchPlugin);\n"],"mappings":"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"}
|
|
@@ -3,10 +3,10 @@ import { IAppRouter, ToPlugin } from "../../shared/src/plugin.js";
|
|
|
3
3
|
import { SQLTypeMarker } from "../../shared/src/sql/types.js";
|
|
4
4
|
import "../../shared/src/index.js";
|
|
5
5
|
import { ToolkitEntry, ToolkitOptions } from "../../core/agent/types.js";
|
|
6
|
+
import { PluginManifest } from "../../registry/types.js";
|
|
6
7
|
import { Plugin } from "../../plugin/plugin.js";
|
|
7
8
|
import "../../plugin/index.js";
|
|
8
9
|
import { IAnalyticsConfig } from "./types.js";
|
|
9
|
-
import { PluginManifest } from "../../registry/types.js";
|
|
10
10
|
import "../../index.js";
|
|
11
11
|
import "../agents/index.js";
|
|
12
12
|
import express from "express";
|
|
@@ -48,7 +48,7 @@ declare class AnalyticsPlugin extends Plugin implements ToolProvider {
|
|
|
48
48
|
/**
|
|
49
49
|
* Fetch column names in the current execution context. Proxied by `asUser`,
|
|
50
50
|
* so `getWorkspaceClient()` resolves to the user's client when invoked via
|
|
51
|
-
* `
|
|
51
|
+
* `asUser(req)` and the service principal's otherwise.
|
|
52
52
|
*/
|
|
53
53
|
_getColumnNames(statementId: string): Promise<string[] | undefined>;
|
|
54
54
|
/**
|
|
@@ -140,7 +140,7 @@ declare class AnalyticsPlugin extends Plugin implements ToolProvider {
|
|
|
140
140
|
* const result = await analytics.query("SELECT * FROM table")
|
|
141
141
|
*
|
|
142
142
|
* // User context execution (in route handler)
|
|
143
|
-
* const result = await
|
|
143
|
+
* const result = await appkit.asUser(req).analytics.query("SELECT * FROM table")
|
|
144
144
|
* ```
|
|
145
145
|
*/
|
|
146
146
|
query(query: string, parameters?: Record<string, SQLTypeMarker | null | undefined>, formatParameters?: Record<string, any>, signal?: AbortSignal): Promise<any>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analytics.d.ts","names":[],"sources":["../../../src/plugins/analytics/analytics.ts"],"mappings":";;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"analytics.d.ts","names":[],"sources":["../../../src/plugins/analytics/analytics.ts"],"mappings":";;;;;;;;;;;;;;cA+Ga,eAAA,SAAwB,MAAA,YAAkB,YAAA;;SAE9C,QAAA,EAFoB,cAAA;EAAA,iBAIV,WAAA;EAAA,UACC,MAAA,EAAQ,gBAAA;EAAA,QAGlB,SAAA;EAAA,QACA,cAAA;;;;AATV;;;;;UAmBU,gBAAA;cAEI,MAAA,EAAQ,gBAAA;EAWpB,YAAA,CAAa,MAAA,EAAQ,UAAA;EA0Cd;;;;;;EAFD,mBAAA,CACJ,GAAA,EAAK,OAAA,CAAQ,OAAA,EACb,GAAA,EAAK,OAAA,CAAQ,QAAA,GACZ,OAAA;EA2RI;;;;;;;EAAA,QAtQO,mBAAA;EAs1BX;;;;;EA1zBG,eAAA,CAAgB,WAAA,WAAsB,OAAA;EA63BkB;;;;EAr3BxD,iBAAA,CACJ,GAAA,EAAK,OAAA,CAAQ,OAAA,EACb,GAAA,EAAK,OAAA,CAAQ,QAAA,GACZ,OAAA;EA8yBQ;;;;;;;;;;;;;;;EAjlBL,kBAAA,CACJ,GAAA,EAAK,OAAA,CAAQ,OAAA,EACb,GAAA,EAAK,OAAA,CAAQ,QAAA,GACZ,OAAA;;;;;;;UA8RW,qBAAA;EA5jBP;;;;;;;;;EAAA,QAylBC,sBAAA;EAtiBoC;;;;;;;;;;;EAAA,QAskB9B,uBAAA;EA7VZ;;;;;;;EAkfI,0BAAA,CAA2B,MAAA,EAAQ,WAAA,GAAc,OAAA;EAAjD;;;;;;;;;;;;;;;;;;EAAA,QAiCE,qBAAA;EA8GS;;;;;;;;;;;;;;;EArDX,KAAA,CACJ,KAAA,UACA,UAAA,GAAa,MAAA,SAAe,aAAA,sBAC5B,gBAAA,GAAmB,MAAA,eACnB,MAAA,GAAS,WAAA,GACR,OAAA;EAqBG,QAAA,CAAA,GAAY,OAAA;EAAA,QAIV,KAAA;EAuBR,aAAA,CAAA,GAAiB,mBAAA;EAIX,gBAAA,CACJ,IAAA,UACA,IAAA,WACA,MAAA,GAAS,WAAA,GACR,OAAA;EAzDQ;;;;;AA0Jb;;EAtFE,OAAA,CAAQ,IAAA,GAXE,cAAA,GAWoD,MAAA,SAAA,YAAA;EAsF1C;;;;EA9EpB,OAAA,CAAA;IA8EoB;;;2BA7JL,UAAA,GACA,MAAA,SAAe,aAAA,sBAAiC,gBAAA,GAC1C,MAAA,eAAmB,MAAA,GAC7B,WAAA,KACR,OAAA;EAAA;AAAA;;;;cAyJQ,SAAA,EAAS,QAAA,QAAA,eAAA,EAAA,gBAAA"}
|
|
@@ -2,14 +2,16 @@ import { makeResultMessage } from "../../shared/src/sse/analytics.js";
|
|
|
2
2
|
import { AppKitError } from "../../errors/base.js";
|
|
3
3
|
import { ExecutionError } from "../../errors/execution.js";
|
|
4
4
|
import "../../errors/index.js";
|
|
5
|
+
import { getWarehouseId } from "../../resources/warehouse.js";
|
|
5
6
|
import { createLogger } from "../../logging/logger.js";
|
|
6
|
-
import {
|
|
7
|
+
import { getWorkspaceClient } from "../../context/execution-context.js";
|
|
7
8
|
import "../../context/index.js";
|
|
8
9
|
import { Plugin } from "../../plugin/plugin.js";
|
|
9
10
|
import { toPlugin } from "../../plugin/to-plugin.js";
|
|
10
11
|
import "../../plugin/index.js";
|
|
11
12
|
import { defineManifest } from "../../registry/manifest-loader.js";
|
|
12
13
|
import "../../registry/index.js";
|
|
14
|
+
import "../../resources/index.js";
|
|
13
15
|
import { DEFAULT_WAREHOUSE_STARTUP_TIMEOUT_MS, SQLWarehouseConnector } from "../../connectors/sql-warehouse/client.js";
|
|
14
16
|
import "../../connectors/index.js";
|
|
15
17
|
import { buildToolkitEntries } from "../../core/agent/build-toolkit.js";
|
|
@@ -145,7 +147,7 @@ var AnalyticsPlugin = class extends Plugin {
|
|
|
145
147
|
* so the client falls back to the raw positional Arrow schema names.
|
|
146
148
|
*/
|
|
147
149
|
async _resolveColumnNames(req, statementId) {
|
|
148
|
-
const attempts = [() => this.
|
|
150
|
+
const attempts = [() => this._asUserScoped(req)._getColumnNames(statementId), () => this._getColumnNames(statementId)];
|
|
149
151
|
for (const attempt of attempts) try {
|
|
150
152
|
const columns = await attempt();
|
|
151
153
|
if (columns && columns.length > 0) return columns;
|
|
@@ -156,7 +158,7 @@ var AnalyticsPlugin = class extends Plugin {
|
|
|
156
158
|
/**
|
|
157
159
|
* Fetch column names in the current execution context. Proxied by `asUser`,
|
|
158
160
|
* so `getWorkspaceClient()` resolves to the user's client when invoked via
|
|
159
|
-
* `
|
|
161
|
+
* `asUser(req)` and the service principal's otherwise.
|
|
160
162
|
*/
|
|
161
163
|
async _getColumnNames(statementId) {
|
|
162
164
|
return this.SQLClient.getColumnNames(getWorkspaceClient(), statementId);
|
|
@@ -194,7 +196,7 @@ var AnalyticsPlugin = class extends Plugin {
|
|
|
194
196
|
await this._handleArrowStreamQuery(req, res, query_key, query, isAsUser, parameters);
|
|
195
197
|
return;
|
|
196
198
|
}
|
|
197
|
-
const executor = isAsUser ? this.
|
|
199
|
+
const executor = isAsUser ? this._asUserScoped(req) : this;
|
|
198
200
|
const executorKey = isAsUser ? this.resolveUserId(req) : "global";
|
|
199
201
|
const hashedQuery = this.queryProcessor.hashQuery(query);
|
|
200
202
|
const cacheConfig = {
|
|
@@ -325,7 +327,7 @@ var AnalyticsPlugin = class extends Plugin {
|
|
|
325
327
|
let executorKey;
|
|
326
328
|
try {
|
|
327
329
|
const isObo = registration.lane === "obo";
|
|
328
|
-
executor = isObo ? this.
|
|
330
|
+
executor = isObo ? this._asUserScoped(req) : this;
|
|
329
331
|
executorKey = deriveMetricExecutorKey({
|
|
330
332
|
lane: registration.lane,
|
|
331
333
|
userIdentity: isObo ? this.resolveUserId(req) : void 0
|
|
@@ -459,7 +461,7 @@ var AnalyticsPlugin = class extends Plugin {
|
|
|
459
461
|
* on a cold start. Runs under the user's context for `.obo.sql` queries.
|
|
460
462
|
*/
|
|
461
463
|
async _handleArrowStreamQuery(req, res, query_key, query, isAsUser, parameters) {
|
|
462
|
-
const executor = isAsUser ? this.
|
|
464
|
+
const executor = isAsUser ? this._asUserScoped(req) : this;
|
|
463
465
|
const executorKey = isAsUser ? this.resolveUserId(req) : "global";
|
|
464
466
|
const abortController = new AbortController();
|
|
465
467
|
const onClose = () => abortController.abort();
|
|
@@ -589,7 +591,7 @@ var AnalyticsPlugin = class extends Plugin {
|
|
|
589
591
|
* const result = await analytics.query("SELECT * FROM table")
|
|
590
592
|
*
|
|
591
593
|
* // User context execution (in route handler)
|
|
592
|
-
* const result = await
|
|
594
|
+
* const result = await appkit.asUser(req).analytics.query("SELECT * FROM table")
|
|
593
595
|
* ```
|
|
594
596
|
*/
|
|
595
597
|
async query(query, parameters, formatParameters, signal) {
|