diff --git a/.github/ci-path-filters.yml b/.github/ci-path-filters.yml index cc208eaa6d..a2fbed1092 100644 --- a/.github/ci-path-filters.yml +++ b/.github/ci-path-filters.yml @@ -147,6 +147,8 @@ node_package: - 'crates/node/build.rs' - 'crates/node/package.json' - 'crates/node/src/**' + - 'crates/node/ts/**' + - 'crates/node/tsconfig.json' - 'crates/observability/Cargo.toml' - 'crates/observability/src/**' - 'crates/plugin-host/Cargo.toml' @@ -280,6 +282,8 @@ docs: - 'crates/node/*.d.ts' - 'crates/node/*.js' - 'crates/node/package.json' + - 'crates/node/ts/**' + - 'crates/node/tsconfig.json' - 'crates/observability/Cargo.toml' - 'crates/observability/src/**' - 'crates/plugin-host/src/**' @@ -295,6 +299,7 @@ docs: - 'python/nemo_relay/**' - 'scripts/build-docs.sh' - 'scripts/docs/**' + - 'scripts/tests/test_node_library_reference.py' - 'scripts/tests/test_rust_library_reference.py' rust: @@ -330,7 +335,9 @@ node: - 'crates/node/build.rs' - 'crates/node/package.json' - 'crates/node/src/**' - - 'crates/node/tests/**/*.mjs' + - 'crates/node/tests/**' + - 'crates/node/ts/**' + - 'crates/node/tsconfig.json' - 'package-lock.json' - 'package.json' - 'scripts/test-support/**' diff --git a/.github/workflows/ci_node.yml b/.github/workflows/ci_node.yml index fc6cc8a37f..8e07d7fbc4 100644 --- a/.github/workflows/ci_node.yml +++ b/.github/workflows/ci_node.yml @@ -478,7 +478,7 @@ jobs: const assert = require('node:assert/strict'); const { readFileSync, readdirSync } = require('node:fs'); const path = require('node:path'); - const packageDir = path.dirname(require.resolve('nemo-relay-node')); + const packageDir = path.join(path.dirname(require.resolve('nemo-relay-node')), '..'); const manifest = JSON.parse(readFileSync(path.join(packageDir, 'package.json'), 'utf8')); const nativePackages = readdirSync('node_modules').filter( (name) => name.startsWith('nemo-relay-node-') && name !== 'nemo-relay-node', @@ -490,6 +490,19 @@ jobs: } NODE + - name: Type-check split Node package entry points + if: ${{ !contains(matrix.platform, 'musl') }} + working-directory: package-smoke + run: | + set -euo pipefail + typescript_version="$(node -p "require('../package.json').devDependencies.typescript")" + npm install --ignore-scripts --no-save "typescript@${typescript_version}" + cp ../crates/node/tests/public_entry_points_fixture.ts . + for resolution in "CommonJS Node10" "NodeNext NodeNext" "ESNext Bundler"; do + read -r module module_resolution <<<"$resolution" + npx tsc --noEmit --strict --target ES2022 --module "$module" --moduleResolution "$module_resolution" public_entry_points_fixture.ts + done + - name: Install and load musllinux split Node package if: ${{ contains(matrix.platform, 'musl') }} run: | @@ -516,7 +529,7 @@ jobs: const assert = require(\"node:assert/strict\"); const { readFileSync, readdirSync } = require(\"node:fs\"); const path = require(\"node:path\"); - const packageDir = path.dirname(require.resolve(\"nemo-relay-node\")); + const packageDir = path.join(path.dirname(require.resolve(\"nemo-relay-node\")), \"..\"); const packageManifest = JSON.parse(readFileSync(path.join(packageDir, \"package.json\"), \"utf8\")); const manifest = require(process.env.NEMO_RELAY_NODE_PACKAGE_NAME + \"/package.json\"); const nativePackages = readdirSync(\"node_modules\").filter( diff --git a/.gitignore b/.gitignore index 6f005ee198..26fab48159 100644 --- a/.gitignore +++ b/.gitignore @@ -24,8 +24,9 @@ go/nemo_relay/nemo_relay_test.exe # Node.js (NAPI-RS) node_modules/ crates/node/*.node -crates/node/index.js -crates/node/index.d.ts +crates/node/binding.js +crates/node/binding.d.ts +crates/node/dist/ crates/node/coverage/ crates/node/junit.xml integrations/openclaw/dist/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9bcd8630e1..252cdf7c76 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -283,5 +283,5 @@ repos: name: node docstrings check entry: node scripts/lint/check_public_docstrings.mjs all language: system - files: '^crates/node/(?:plugin|adaptive|typed)\.(?:js|d\.ts)$' + files: '^crates/node/ts/(?:plugin|adaptive|typed)\.ts$' pass_filenames: false diff --git a/codecov.yml b/codecov.yml index b727e03f74..5f8399a498 100644 --- a/codecov.yml +++ b/codecov.yml @@ -72,7 +72,8 @@ component_management: - component_id: node_binding name: Node Binding paths: - - "crates/node/*.js" + - "crates/node/binding.js" + - "crates/node/ts/**" - component_id: plugin_sdk name: Dynamic Plugin SDKs paths: diff --git a/crates/node/adaptive.d.ts b/crates/node/adaptive.d.ts deleted file mode 100644 index b3f55283d1..0000000000 --- a/crates/node/adaptive.d.ts +++ /dev/null @@ -1,390 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -import type { Json, ScopeHandle } from './index'; -import type { ConfigPolicy, ConfigDiagnostic, ConfigReport } from './plugin'; - -export { ConfigPolicy, ConfigDiagnostic, ConfigReport }; - -/** Supported LLM response-cache key derivation strategies. */ -export const ResponseCacheKeyStrategy: { - readonly ExactRequest: 'exact_request'; - readonly Logical: 'logical'; -}; -export type ResponseCacheKeyStrategy = (typeof ResponseCacheKeyStrategy)[keyof typeof ResponseCacheKeyStrategy]; - -/** Adaptive state backend selection. */ -export interface BackendSpec { - kind: string; - config?: Record; -} - -/** Adaptive state configuration. */ -export interface StateConfig { - backend: BackendSpec; -} - -/** Built-in adaptive telemetry settings. */ -export interface TelemetryConfig { - subscriber_name?: string; - learners?: string[]; -} - -/** Built-in adaptive hints injection settings. */ -export interface AdaptiveHintsConfig { - priority?: number; - break_chain?: boolean; - inject_header?: boolean; - inject_body_path?: string; -} - -/** Built-in adaptive tool scheduling settings. */ -export interface ToolParallelismConfig { - priority?: number; - mode?: 'observe_only' | 'inject_hints' | 'schedule' | string; -} - -/** ACG prompt-stability classification thresholds. */ -export interface AcgStabilityThresholds { - stable_threshold?: number; - semi_stable_threshold?: number; - min_observations_for_full_confidence?: number; -} - -/** Adaptive cache-governor settings. */ -export interface AcgConfig { - provider?: 'anthropic' | 'openai' | 'passthrough' | string; - observation_window?: number; - priority?: number; - stability_thresholds?: AcgStabilityThresholds; -} - -/** Opt-in LLM response and tool-result cache settings. */ -export interface ResponseCacheConfig { - ttlSeconds?: number; - /** - * Required non-empty cache trust domain. One configured cache must not span - * mutually untrusted tenants or upstreams. - */ - namespace?: string; - priority?: number; - bypassRate?: number; - cacheNondeterministic?: boolean; - keyStrategy?: ResponseCacheKeyStrategy; - headerAllowlist?: string[]; - backend?: BackendSpec; - /** Opt-in tool-result cache; omit to leave the tool surface off. */ - tools?: ToolCacheConfig; -} - -export interface ResponseCachePluginConfig { - ttl_seconds?: number; - /** Required non-empty cache trust domain. */ - namespace?: string; - priority?: number; - bypass_rate?: number; - cache_nondeterministic?: boolean; - key_strategy?: ResponseCacheKeyStrategy; - header_allowlist?: string[]; - backend?: BackendSpec; - tools?: ToolCachePluginConfig; -} - -/** Shared policy; omitted TTL and bypass rate inherit response-cache defaults. */ -export interface ToolClass { - cacheable?: boolean; - ttlSeconds?: number; - bypassRate?: number; - toolVersion?: string; - argSkip?: string[]; - /** Exact tool names or `prefix*`, `*suffix`, or `*contains*` patterns. */ - members?: string[]; -} - -/** Per-tool refinement; an explicit `argSkip: []` clears the class list. */ -export interface ToolOverride { - cacheable?: boolean; - ttlSeconds?: number; - bypassRate?: number; - toolVersion?: string; - argSkip?: string[]; -} - -/** Opt-in caching for tools that are read-only and stable for their TTL. */ -export interface ToolCacheConfig { - enabled?: boolean; - /** - * Tool execution-intercept priority. Omit to use Rust's default of 150. - */ - priority?: number; - /** Whether error-shaped tool results may be cached; defaults to false. */ - cacheErrors?: boolean; - default?: ToolClass; - classes?: Record; - /** Exact tool names or `prefix*`, `*suffix`, or `*contains*` patterns. */ - overrides?: Record; -} - -export type ToolClassPluginConfig = Omit & { - ttl_seconds?: number; - bypass_rate?: number; - tool_version?: string; - arg_skip?: string[]; -}; - -export type ToolOverridePluginConfig = Omit & { - ttl_seconds?: number; - bypass_rate?: number; - tool_version?: string; - arg_skip?: string[]; -}; - -export type ToolCachePluginConfig = Omit & { - cache_errors?: boolean; - default?: ToolClassPluginConfig; - classes?: Record; - overrides?: Record; -}; - -/** Canonical config object for the top-level adaptive component. */ -export interface Config { - version?: number; - agent_id?: string; - state?: StateConfig; - telemetry?: TelemetryConfig; - adaptive_hints?: AdaptiveHintsConfig; - tool_parallelism?: ToolParallelismConfig; - acg?: AcgConfig; - responseCache?: ResponseCacheConfig; - policy?: ConfigPolicy; -} - -export type AdaptivePluginConfig = Omit & { - response_cache?: ResponseCachePluginConfig; -}; - -/** Top-level adaptive component wrapper with fixed kind `adaptive`. */ -export interface ComponentSpec { - kind: 'adaptive'; - enabled?: boolean; - config: AdaptivePluginConfig; -} - -/** Prompt-cache write tokens retained for one cache duration. */ -export interface CacheWriteTokenCounts { - count: number; - /** Cache duration in seconds, omitted when the provider does not say. */ - duration?: number; -} - -/** Normalized LLM token usage for cache telemetry. */ -export interface CacheUsage { - prompt_tokens?: number; - completion_tokens?: number; - total_tokens?: number; - cache_read_tokens?: number; - cache_write_tokens?: CacheWriteTokenCounts[]; - cost?: Json; -} - -/** Identity of the agent associated with cache telemetry. */ -export interface AgentIdentity { - agent_id: string; - template_version: string; - toolset_hash: string; - model_family: string; - tenant_scope: string; -} - -/** Input for building cache request facts. */ -export interface CacheRequestFactsOptions { - provider: string; - requestId: string; - annotatedRequest: Json; - agentId: string; - timestamp?: string; -} - -/** Request-time facts used to classify cache misses. */ -export interface CacheRequestFacts { - provider: string; - stable_prefix_length: number; - stable_prefix_tokens?: number; - required_min_tokens?: number; - first_mismatch_span_id?: string; - first_mismatch_sequence_index?: number; - expected_hash_prefix?: string; - actual_hash_prefix?: string; - retention_window_secs?: number; - observed_gap_secs?: number; - missing_facts?: string[]; -} - -/** Input for building cache telemetry events. */ -export interface CacheTelemetryEventOptions { - provider: 'anthropic' | 'openai' | string; - requestId: string; - usage?: CacheUsage | null; - requestFacts?: CacheRequestFacts | null; - agentId: string; - templateVersion: string; - toolsetHash: string; - modelFamily: string; - tenantScope: string; - timestamp?: string; -} - -/** Normalized adaptive cache telemetry event. */ -export interface CacheTelemetryEvent { - request_id: string; - agent_identity: AgentIdentity; - cache_read_tokens: number; - cache_creation_tokens: number; - total_prompt_tokens: number; - hit_rate: number; - miss_reason?: Record; - miss_diagnosis?: Record; - provider: string; - timestamp: string; -} - -/** Owned adaptive runtime outside the generic plugin system. */ -export declare class AdaptiveRuntime { - constructor(config: Config); - /** Register all configured adaptive runtime features. */ - register(): Promise; - /** Deregister all previously registered adaptive runtime features. */ - deregister(): void; - /** Shut down the adaptive runtime and consume its Rust runtime state. */ - shutdown(): Promise; - /** Block until adaptive telemetry has processed pending events. */ - waitForIdle(): void; - /** Return the validation report captured during construction. */ - report(): ConfigReport; - /** Bind the runtime's ACG request rewrite to a scope. */ - bindScope(scopeHandle: ScopeHandle): void; - /** Build cache request facts for an annotated LLM request. */ - buildCacheRequestFacts(options: CacheRequestFactsOptions): CacheRequestFacts | null; -} - -export declare const ADAPTIVE_PLUGIN_KIND: 'adaptive'; -/** - * Create a default adaptive component config. - * - * Returns the minimal top-level adaptive config shape with `version = 1` so - * callers can add state, telemetry, and scheduler settings incrementally. - * - * @returns A new adaptive config object. - * @remarks The returned object is detached from runtime state until it is - * wrapped with `ComponentSpec` and activated through the plugin system. - */ -export declare function defaultConfig(): Config; -/** - * Create an in-memory adaptive state backend spec. - * - * Produces the backend descriptor for ephemeral adaptive state stored inside - * the current process rather than an external datastore. - * - * @returns An adaptive backend spec using in-memory storage. - * @remarks This backend does not persist state across process restarts. - */ -export declare function inMemoryBackend(): BackendSpec; -/** - * Create a Redis-backed adaptive state backend spec. - * - * Produces the backend descriptor expected by the adaptive plugin when state - * should be shared or persisted through Redis. - * - * @param url - Redis connection URL for the backend. - * @param keyPrefix - Prefix applied to Redis keys. - * @returns An adaptive backend spec using Redis storage. - * @remarks The default key prefix namespaces runtime records under - * `nemo_relay:` unless a different prefix is supplied. - */ -export declare function redisBackend(url: string, keyPrefix?: string): BackendSpec; -/** - * Create adaptive telemetry settings with runtime defaults applied. - * - * Merges caller-supplied overrides onto the built-in telemetry config shape - * used by the adaptive plugin. - * - * @param config - Partial telemetry settings to override. - * @returns A normalized adaptive telemetry config object. - * @remarks An empty `learners` array is supplied by default so callers can - * append learner names without checking for initialization first. - */ -export declare function telemetryConfig(config?: TelemetryConfig): TelemetryConfig; -/** - * Create adaptive hint-injection settings with defaults applied. - * - * Merges caller-supplied overrides onto the default config used by the - * adaptive hints injector. - * - * @param config - Partial adaptive hints settings to override. - * @returns A normalized adaptive hints config object. - * @remarks By default the injector runs at priority `100`, preserves the rest - * of the chain, and writes hints to `nvext.agent_hints`. - */ -export declare function adaptiveHintsConfig(config?: AdaptiveHintsConfig): AdaptiveHintsConfig; -/** - * Create adaptive tool-parallelism settings with defaults applied. - * - * Merges caller-supplied overrides onto the scheduler config shape used by the - * adaptive plugin's tool parallelism component. - * - * @param config - Partial tool scheduling settings to override. - * @returns A normalized tool-parallelism config object. - * @remarks The default mode is `observe_only`, so recommendations are produced - * without changing execution behavior unless the caller opts in. - */ -export declare function toolParallelismConfig(config?: ToolParallelismConfig): ToolParallelismConfig; -/** - * Create adaptive cache-governor settings with defaults applied. - * - * Merges caller-supplied overrides onto the Adaptive Cache Governor (ACG) - * config shape used by the adaptive plugin's LLM execution intercept. - * - * @param config - Partial Adaptive Cache Governor (ACG) settings to override. - * @returns A normalized adaptive cache-governor config object. - * @remarks Nested `stability_thresholds` values are defaulted individually so - * callers can override only the thresholds they need. - */ -export declare function acgConfig(config?: AcgConfig): AcgConfig; -/** - * Create response-cache settings with defaults applied. - * - * Merges caller-supplied overrides onto the opt-in LLM response and tool-result - * cache config shape used by the adaptive plugin. This is a section of - * the adaptive component, not a standalone plugin kind. - * - * @param config - Partial response-cache settings to override. - * @returns A normalized response-cache config object. - * @remarks The default backend is in-memory; pass a `backend` (e.g. - * `redisBackend(url)`) for a shared cache. `bypassRate` defaults to `0.0`, - * while caching nondeterministic requests is opt-in. - */ -export declare function responseCacheConfig(config?: ResponseCacheConfig): ResponseCacheConfig; -/** - * Wrap adaptive config as a top-level component. - * - * Produces the plugin component entry that can be inserted directly - * into `plugin.defaultConfig().components`. - * - * @param config - Adaptive component configuration document. - * @param options - Optional component-level flags. - * @returns A plugin component spec for the adaptive plugin. - * @remarks Setting `options.enabled = false` keeps the config in the plugin - * document for validation while skipping runtime activation. - */ -export declare function ComponentSpec( - config: Config, - options?: { - enabled?: boolean; - }, -): ComponentSpec; -/** Validate an adaptive config document without constructing a runtime. */ -export declare function validateConfig(config: Config): ConfigReport; -/** Build one adaptive cache telemetry event from normalized usage. */ -export declare function buildCacheTelemetryEvent(options: CacheTelemetryEventOptions): CacheTelemetryEvent | null; -/** Set manual latency sensitivity on the current scope. */ -export declare function setLatencySensitivity(value: number): void; diff --git a/crates/node/adaptive.js b/crates/node/adaptive.js deleted file mode 100644 index 3348df457e..0000000000 --- a/crates/node/adaptive.js +++ /dev/null @@ -1,318 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -'use strict'; - -const lib = require('./index.js'); -const plugin = require('./plugin.js'); - -const ADAPTIVE_PLUGIN_KIND = 'adaptive'; - -const ResponseCacheKeyStrategy = Object.freeze({ - ExactRequest: 'exact_request', - Logical: 'logical', -}); - -/** - * Create a default adaptive component config. - * - * Returns the minimal top-level adaptive config shape with `version = 1` so - * callers can add state, telemetry, and scheduler settings incrementally. - * - * @returns {object} A new adaptive config object. - * @remarks The returned object is detached from runtime state until it is - * wrapped with `ComponentSpec` and activated through the plugin system. - */ -function defaultConfig() { - return { - version: 1, - }; -} - -/** - * Create an in-memory adaptive state backend spec. - * - * Produces the backend descriptor for ephemeral adaptive state stored inside - * the current process rather than an external datastore. - * - * @returns {object} An adaptive backend spec using in-memory storage. - * @remarks This backend does not persist state across process restarts. - */ -function inMemoryBackend() { - return { - kind: 'in_memory', - config: {}, - }; -} - -/** - * Create a Redis-backed adaptive state backend spec. - * - * Produces the backend descriptor expected by the adaptive plugin when state - * should be shared or persisted through Redis. - * - * @param {string} url - Redis connection URL for the backend. - * @param {string} [keyPrefix='nemo_relay:'] - Prefix applied to Redis keys. - * @returns {object} An adaptive backend spec using Redis storage. - * @remarks The default key prefix namespaces runtime records under - * `nemo_relay:` unless a different prefix is supplied. - */ -function redisBackend(url, keyPrefix = 'nemo_relay:') { - return { - kind: 'redis', - config: { - url, - key_prefix: keyPrefix, - }, - }; -} - -/** - * Create adaptive telemetry settings with runtime defaults applied. - * - * Merges caller-supplied overrides onto the built-in telemetry config shape - * used by the adaptive plugin. - * - * @param {object} [config={}] - Partial telemetry settings to override. - * @returns {object} A normalized adaptive telemetry config object. - * @remarks An empty `learners` array is supplied by default so callers can - * append learner names without checking for initialization first. - */ -function telemetryConfig(config = {}) { - return { - learners: [], - ...config, - }; -} - -/** - * Create adaptive hint-injection settings with defaults applied. - * - * Merges caller-supplied overrides onto the default config used by the - * adaptive hints injector. - * - * @param {object} [config={}] - Partial adaptive hints settings to override. - * @returns {object} A normalized adaptive hints config object. - * @remarks By default the injector runs at priority `100`, preserves the rest - * of the chain, and writes hints to `nvext.agent_hints`. - */ -function adaptiveHintsConfig(config = {}) { - return { - priority: 100, - break_chain: false, - inject_header: true, - inject_body_path: 'nvext.agent_hints', - ...config, - }; -} - -/** - * Create adaptive tool-parallelism settings with defaults applied. - * - * Merges caller-supplied overrides onto the scheduler config shape used by the - * adaptive plugin's tool parallelism component. - * - * @param {object} [config={}] - Partial tool scheduling settings to override. - * @returns {object} A normalized tool-parallelism config object. - * @remarks The default mode is `observe_only`, so recommendations are produced - * without changing execution behavior unless the caller opts in. - */ -function toolParallelismConfig(config = {}) { - return { - priority: 100, - mode: 'observe_only', - ...config, - }; -} - -/** - * Create adaptive cache-governor settings with defaults applied. - * - * Merges caller-supplied overrides onto the ACG config shape used by the - * adaptive plugin's LLM execution intercept. - * - * @param {object} [config={}] - Partial ACG settings to override. - * @returns {object} A normalized adaptive cache-governor config object. - * @remarks Nested `stability_thresholds` values are defaulted individually so - * callers can override only the thresholds they need. - */ -function acgConfig(config = {}) { - const { stability_thresholds, ...rest } = config; - return { - provider: 'passthrough', - observation_window: 100, - priority: 50, - stability_thresholds: { - stable_threshold: 0.95, - semi_stable_threshold: 0.5, - min_observations_for_full_confidence: 20, - ...stability_thresholds, - }, - ...rest, - }; -} - -/** - * Create response-cache settings with defaults applied. - * - * Merges caller-supplied overrides onto the opt-in LLM response and tool-result - * cache config shape used by the adaptive plugin. This is a section of - * the adaptive component, not a standalone plugin kind. - * - * @param {object} [config={}] - Partial response-cache settings to override. - * @returns {object} A normalized response-cache config object. - * @remarks The default backend is in-memory; pass a `backend` (e.g. - * `redisBackend(url)`) for a shared cache. `bypassRate` defaults to `0`, - * while caching nondeterministic requests is opt-in. Set a non-empty - * `namespace` identifying one trusted cache-sharing domain before validation; - * the empty helper default is an unconfigured sentinel. - */ -function responseCacheConfig(config = {}) { - const { backend, ...rest } = config; - return { - ttlSeconds: 3600, - namespace: '', - priority: 150, - bypassRate: 0, - cacheNondeterministic: false, - keyStrategy: ResponseCacheKeyStrategy.ExactRequest, - headerAllowlist: [], - backend: backend ?? inMemoryBackend(), - ...rest, - }; -} - -const RESPONSE_CACHE_PLUGIN_FIELDS = { - ttlSeconds: 'ttl_seconds', - bypassRate: 'bypass_rate', - cacheNondeterministic: 'cache_nondeterministic', - keyStrategy: 'key_strategy', - headerAllowlist: 'header_allowlist', -}; - -const TOOL_CLASS_PLUGIN_FIELDS = { - ttlSeconds: 'ttl_seconds', - bypassRate: 'bypass_rate', - toolVersion: 'tool_version', - argSkip: 'arg_skip', -}; - -const TOOL_OVERRIDE_PLUGIN_FIELDS = { - ...TOOL_CLASS_PLUGIN_FIELDS, -}; - -const TOOL_CACHE_PLUGIN_FIELDS = { - cacheErrors: 'cache_errors', -}; - -function mapPluginFields(config, fields) { - if (config === null || typeof config !== 'object' || Array.isArray(config)) return config; - return Object.fromEntries(Object.entries(config).map(([key, value]) => [fields[key] ?? key, value])); -} - -function mapPluginRecord(config, fields) { - if (config === null || typeof config !== 'object' || Array.isArray(config)) return config; - return Object.fromEntries(Object.entries(config).map(([key, value]) => [key, mapPluginFields(value, fields)])); -} - -function toToolCachePluginConfig(config) { - const serialized = mapPluginFields(config, TOOL_CACHE_PLUGIN_FIELDS); - if (serialized === config) return config; - if (serialized.default !== undefined) { - serialized.default = mapPluginFields(serialized.default, TOOL_CLASS_PLUGIN_FIELDS); - } - if (serialized.classes !== undefined) { - serialized.classes = mapPluginRecord(serialized.classes, TOOL_CLASS_PLUGIN_FIELDS); - } - if (serialized.overrides !== undefined) { - serialized.overrides = mapPluginRecord(serialized.overrides, TOOL_OVERRIDE_PLUGIN_FIELDS); - } - return serialized; -} - -function toResponseCachePluginConfig(config) { - const serialized = mapPluginFields(config, RESPONSE_CACHE_PLUGIN_FIELDS); - if (serialized === config) return config; - if (serialized.tools !== undefined) { - serialized.tools = toToolCachePluginConfig(serialized.tools); - } - return serialized; -} - -function toPluginConfig(config) { - const { responseCache, ...rest } = config; - if (responseCache === undefined) return config; - return { ...rest, response_cache: toResponseCachePluginConfig(responseCache) }; -} - -class AdaptiveRuntime extends lib.AdaptiveRuntime { - constructor(config) { - super(toPluginConfig(config)); - } -} - -/** - * Wrap adaptive config as a top-level plugin component. - * - * Produces the plugin component entry that can be inserted directly - * into `plugin.defaultConfig().components`. - * - * @param {object} config - Adaptive component configuration document. - * @param {{ enabled?: boolean }} [options={}] - Optional component-level flags. - * @returns {object} A plugin component spec for the adaptive plugin. - * @remarks Setting `enabled` to `false` keeps the config in the plugin document - * for validation while skipping runtime activation. - */ -function ComponentSpec(config, { enabled = true } = {}) { - return plugin.ComponentSpec(ADAPTIVE_PLUGIN_KIND, toPluginConfig(config), { - enabled, - }); -} - -/** - * Validate an adaptive config document without constructing a runtime. - * - * @param {object} config - Adaptive runtime configuration document. - * @returns {object} A structured validation report with diagnostics. - */ -function validateConfig(config) { - return lib.validateAdaptiveConfig(toPluginConfig(config)); -} - -/** - * Build one adaptive cache telemetry event from normalized usage. - * - * @param {object} options - Cache telemetry event inputs. - * @returns {object|null} A telemetry event, or `null` when usage lacks prompt tokens. - */ -function buildCacheTelemetryEvent(options) { - return lib.buildCacheTelemetryEvent(options) ?? null; -} - -/** - * Set manual latency sensitivity on the current scope. - * - * @param {number} value - Positive sensitivity value to store on the current scope. - * @returns {void} Nothing. - */ -function setLatencySensitivity(value) { - return lib.setLatencySensitivity(value); -} - -module.exports = { - AdaptiveRuntime, - ADAPTIVE_PLUGIN_KIND, - ResponseCacheKeyStrategy, - defaultConfig, - inMemoryBackend, - redisBackend, - telemetryConfig, - adaptiveHintsConfig, - toolParallelismConfig, - acgConfig, - responseCacheConfig, - ComponentSpec, - validateConfig, - buildCacheTelemetryEvent, - setLatencySensitivity, -}; diff --git a/crates/node/binding-types.d.ts b/crates/node/binding-types.d.ts new file mode 100644 index 0000000000..ede6786dc1 --- /dev/null +++ b/crates/node/binding-types.d.ts @@ -0,0 +1,1381 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +/// + +export type JsonPrimitive = string | number | boolean | null; +export type JsonObject = { [Key in string]: JsonValue }; +export type JsonArray = JsonValue[] | readonly JsonValue[]; +export type JsonValue = JsonPrimitive | JsonObject | JsonArray; +export type Json = JsonValue; +export type LlmRequestShape = { + headers: JsonObject; + content: Json; +}; + +type NonJsonValue = + | ((...args: never[]) => unknown) + | (abstract new (...args: never[]) => unknown) + | bigint + | symbol + | undefined; +type BareObject = V extends unknown + ? [object] extends [V] + ? [V] extends [object] + ? [keyof V] extends [never] + ? V + : never + : never + : never + : never; +type Shaped = [Extract | BareObject] extends [never] + ? Mapped + : never; +type JsonProperty = [unknown] extends [V] + ? unknown + : V extends JsonValue + ? V + : V extends NonJsonValue + ? never + : V extends object + ? [BareObject] extends [never] + ? { [K in keyof V]: JsonProperty } + : never + : never; +type JsonObjectProperty = V extends readonly unknown[] ? never : V extends object ? JsonProperty : never; +type LlmRequestProperty = V extends object + ? { [K in keyof V]: K extends 'headers' ? JsonObjectProperty : JsonProperty } & Record< + 'headers' | 'content', + unknown + > + : never; +type PendingMarkSpecProperty = V extends object + ? { + [K in keyof V]: K extends 'data' | 'metadata' | 'categoryProfile' + ? JsonProperty + : K extends keyof PendingMarkSpec + ? PendingMarkSpec[K] + : JsonProperty; + } & Record<'name', unknown> + : never; +type PendingMarksProperty = V extends readonly unknown[] ? { [I in keyof V]: PendingMarkSpecProperty } : never; +type EventMetadataProperty = V extends EventMetadataValue ? V : never; +type MetricMeasurementProperty = V extends object + ? { + [K in keyof V]: K extends 'attributes' + ? JsonProperty + : K extends keyof MetricMeasurement + ? MetricMeasurement[K] + : unknown; + } & Record<'name' | 'kind' | 'valueType' | 'value', unknown> + : never; +interface LlmRequestOpen { + headers: JsonObject; + content: Json; + [key: string]: JsonValue; +} +interface ToolExecutionResultOpen { + result: Json; + annotation?: Json; + [key: string]: JsonValue | undefined; +} +interface EventSanitizeFieldsOpen { + data?: Json; + categoryProfile?: Json; + metadata?: Json; + [key: string]: JsonValue | undefined; +} + +export type JsonCompatible = + | JsonValue + | ([T] extends [{} | null] ? Shaped }> : never); +export type JsonObjectCompatible = + | JsonObject + | ([T] extends [object] + ? Shaped }, NonJsonValue | readonly unknown[]> + : never); +export type LlmRequestCompatible = + | LlmRequestOpen + | ([T] extends [object] + ? Shaped< + T, + { [K in keyof T]: K extends 'headers' ? JsonObjectProperty : JsonProperty } & Record< + 'headers' | 'content', + unknown + > + > + : never); +export type ToolExecutionResultCompatible = + | ToolExecutionResultOpen + | ([T] extends [object] ? Shaped } & Record<'result', unknown>> : never); +export type ToolExecutionInterceptOutcomeCompatible = + | ToolExecutionInterceptOutcome + | ([T] extends [object] + ? Shaped< + T, + { [K in keyof T]: K extends 'pendingMarks' ? PendingMarksProperty : JsonProperty } & Record< + 'result', + unknown + > + > + : never); +export type LlmRequestInterceptOutcomeCompatible = + | LlmRequestInterceptOutcome + | ([T] extends [object] + ? Shaped< + T, + { + [K in keyof T]: K extends 'request' + ? LlmRequestProperty + : K extends 'pendingMarks' + ? PendingMarksProperty + : K extends 'optimizationContributions' + ? LlmOptimizationContribution[] + : JsonProperty; + } & Record<'request', unknown> + > + : never); +export type EventSanitizeFieldsCompatible = + | EventSanitizeFieldsOpen + | ([T] extends [object] ? Shaped }> : never); +export type EventMetadataCompatible = + | EventMetadata + | ([T] extends [object] ? Shaped }> : never); +export type MetricMeasurementsCompatible = + | readonly MetricMeasurement[] + | ([T] extends [readonly unknown[]] ? { [I in keyof T]: MetricMeasurementProperty } : never); + +export type RuntimeRegistrationKindName = `${RuntimeRegistrationKind}`; + +export type ResourceMetricsMeasurementScope = 'runtime_default' | ResourceMeasurementScope; + +export type ResourceUnitKind = + | DurationUnit + | CapacityUnit + | DataUnit + | BandwidthUnit + | CpuUnit + | UtilizationUnit + | CountUnit; + +export type ResourceUnit = U | `${U}`; + +export interface ResourceMetricsPollingConfig { + enabled?: boolean; + interval_millis?: number; +} + +export interface ResourceMemoryInspectionConfig { + enabled?: boolean; + stale_after_millis?: number; +} + +export interface ResourceMetricsCpuConfig { + enabled?: boolean; +} + +export interface ResourceMetricsMemoryConfig { + enabled?: boolean; +} + +export interface ResourceMetricsProcessConfig { + enabled?: boolean; +} + +export interface ResourceMetricsDiskConfig { + enabled?: boolean; + process_io?: boolean; + filesystem_paths?: string[]; +} + +export interface ResourceMetricsGpuConfig { + enabled?: boolean; + devices?: string[]; + device_metrics?: boolean; + process_metrics?: boolean; +} + +export interface ResourceMetricsNetworkConfig { + enabled?: boolean; + interfaces?: string[]; +} + +export interface ResourceMetricsCpuUnits { + user_time?: ResourceUnit; + system_time?: ResourceUnit; + total_time?: ResourceUnit; + throttled_time?: ResourceUnit; + some_pressure_stall_time?: ResourceUnit; + full_pressure_stall_time?: ResourceUnit; + consumption_rate?: ResourceUnit; + effective_limit?: ResourceUnit; +} + +export interface ResourceMetricsMemoryUnits { + system_used?: ResourceUnit; + system_total?: ResourceUnit; + system_available?: ResourceUnit; + resident?: ResourceUnit; + private?: ResourceUnit; + physical_footprint?: ResourceUnit; + virtual_memory?: ResourceUnit; + peak_resident?: ResourceUnit; + limit?: ResourceUnit; + environment_accounted?: ResourceUnit; + some_pressure_stall_time?: ResourceUnit; + full_pressure_stall_time?: ResourceUnit; +} + +export interface ResourceMetricsDiskUnits { + read_data?: ResourceUnit; + write_data?: ResourceUnit; + filesystem_total_capacity?: ResourceUnit; + filesystem_available_capacity?: ResourceUnit; + filesystem_free_capacity?: ResourceUnit; + read_throughput?: ResourceUnit; + write_throughput?: ResourceUnit; +} + +export interface ResourceMetricsGpuUnits { + device_memory_used?: ResourceUnit; + process_memory_used?: ResourceUnit; + device_compute_utilization?: ResourceUnit; + process_compute_utilization?: ResourceUnit; +} + +export interface ResourceMetricsNetworkTrafficUnits { + received_data?: ResourceUnit; + transmitted_data?: ResourceUnit; + receive_throughput?: ResourceUnit; + transmit_throughput?: ResourceUnit; +} + +export interface ResourceMetricsNetworkUnits { + system?: ResourceMetricsNetworkTrafficUnits; + interface?: ResourceMetricsNetworkTrafficUnits; +} + +export interface ResourceMetricsUnits { + cpu?: ResourceMetricsCpuUnits; + memory?: ResourceMetricsMemoryUnits; + disk?: ResourceMetricsDiskUnits; + gpu?: ResourceMetricsGpuUnits; + network?: ResourceMetricsNetworkUnits; +} + +export interface ResourceMetricsConfig { + measurement_scope?: ResourceMetricsMeasurementScope; + polling?: ResourceMetricsPollingConfig; + memory_inspection?: ResourceMemoryInspectionConfig; + cpu?: ResourceMetricsCpuConfig; + memory?: ResourceMetricsMemoryConfig; + process?: ResourceMetricsProcessConfig; + disk?: ResourceMetricsDiskConfig; + gpu?: ResourceMetricsGpuConfig; + network?: ResourceMetricsNetworkConfig; + units?: ResourceMetricsUnits; +} + +export type ResourceOperatingSystem = 'linux' | 'macos' | 'windows' | 'unsupported'; +export type ResourceMeasurementScope = 'global' | 'application_process' | 'process_tree'; + +/** `bigint` above JavaScript's safe integer range. */ +export type ResourceNumeric = number | bigint; + +export interface ResourceMeasurement { + readonly value: T; + readonly unit: U; +} + +export interface ResourceLimitEventCount { + readonly resource: 'cpu' | 'memory' | 'processes'; + readonly event: 'throttled' | 'high' | 'maximum' | 'out_of_memory' | 'terminated'; + readonly count: ResourceMeasurement | null; +} + +export type AcceleratorVendor = 'nvidia' | 'amd' | 'intel' | 'apple' | 'other'; + +export interface AcceleratorDeviceMetrics { + readonly vendor: AcceleratorVendor; + readonly deviceIdentifier: string; + readonly deviceIndex: number | null; + readonly memoryUsed: ResourceMeasurement | null; + readonly computeUtilization: ResourceMeasurement | null; +} + +export interface AcceleratorProcessMetrics { + readonly vendor: AcceleratorVendor; + readonly deviceIdentifier: string; + readonly deviceIndex: number | null; + readonly processId: number; + readonly memoryUsed: ResourceMeasurement | null; + readonly computeUtilization: ResourceMeasurement | null; +} + +export interface FilesystemCapacityMetrics { + readonly path: string; + readonly totalCapacity: ResourceMeasurement | null; + readonly availableCapacity: ResourceMeasurement | null; + readonly freeCapacity: ResourceMeasurement | null; +} + +export interface CpuMetrics { + readonly userTime: ResourceMeasurement | null; + readonly systemTime: ResourceMeasurement | null; + readonly totalTime: ResourceMeasurement | null; + readonly consumptionRate: ResourceMeasurement | null; + readonly throttledTime: ResourceMeasurement | null; + readonly effectiveLimit: ResourceMeasurement | null; + readonly somePressureStallTime: ResourceMeasurement | null; + readonly fullPressureStallTime: ResourceMeasurement | null; + readonly limitEvents: readonly ResourceLimitEventCount[]; +} + +export interface MemoryMetrics { + readonly systemUsed: ResourceMeasurement | null; + readonly systemTotal: ResourceMeasurement | null; + readonly systemAvailable: ResourceMeasurement | null; + readonly resident: ResourceMeasurement | null; + readonly private: ResourceMeasurement | null; + readonly physicalFootprint: ResourceMeasurement | null; + readonly virtualMemory: ResourceMeasurement | null; + readonly peakResident: ResourceMeasurement | null; + readonly limit: ResourceMeasurement | null; + readonly environmentAccounted: ResourceMeasurement | null; + readonly somePressureStallTime: ResourceMeasurement | null; + readonly fullPressureStallTime: ResourceMeasurement | null; + readonly outOfMemoryEventCount: ResourceMeasurement | null; + readonly limitEvents: readonly ResourceLimitEventCount[]; +} + +export interface ProcessMetrics { + readonly activeCount: ResourceMeasurement | null; + readonly descendantCount: ResourceMeasurement | null; + readonly threadCount: ResourceMeasurement | null; + readonly lifetimeCreationCount: ResourceMeasurement | null; + readonly openFileDescriptorCount: ResourceMeasurement | null; + readonly windowsHandleCount: ResourceMeasurement | null; + readonly limitEvents: readonly ResourceLimitEventCount[]; +} + +export interface DiskMetrics { + readonly readData: ResourceMeasurement | null; + readonly writeData: ResourceMeasurement | null; + readonly readThroughput: ResourceMeasurement | null; + readonly writeThroughput: ResourceMeasurement | null; + readonly readOperations: ResourceMeasurement | null; + readonly writeOperations: ResourceMeasurement | null; + readonly filesystems: readonly FilesystemCapacityMetrics[]; +} + +export interface GpuMetrics { + readonly deviceMetrics: readonly AcceleratorDeviceMetrics[] | null; + readonly processMetrics: readonly AcceleratorProcessMetrics[] | null; +} + +export interface NetworkTrafficMetrics { + readonly receivedData: ResourceMeasurement | null; + readonly transmittedData: ResourceMeasurement | null; + readonly receiveThroughput: ResourceMeasurement | null; + readonly transmitThroughput: ResourceMeasurement | null; + readonly receivedPackets: ResourceMeasurement | null; + readonly transmittedPackets: ResourceMeasurement | null; + readonly receiveErrors: ResourceMeasurement | null; + readonly transmitErrors: ResourceMeasurement | null; +} + +export interface NetworkInterfaceMetrics { + readonly name: string; + readonly traffic: NetworkTrafficMetrics; +} + +export interface NetworkMetrics { + readonly measurementScope: 'global'; + readonly system: NetworkTrafficMetrics; + readonly interfaces: readonly NetworkInterfaceMetrics[]; +} + +export interface ProcessSamplingMetadata { + readonly visibleProcesses: ResourceNumeric; + readonly sampledProcesses: ResourceNumeric; + /** Process counts keyed by snake_case measurement path. */ + readonly fieldSampledProcesses: Readonly>; +} + +export interface ResourceMetricsSnapshot { + readonly timestamp: string; + readonly operatingSystem: ResourceOperatingSystem; + readonly measurementScope: ResourceMeasurementScope; + readonly processSampling: ProcessSamplingMetadata | null; + readonly cpu: CpuMetrics | null; + readonly memory: MemoryMetrics | null; + readonly process: ProcessMetrics | null; + readonly disk: DiskMetrics | null; + readonly gpu: GpuMetrics | null; + readonly network: NetworkMetrics | null; +} + +/** Codec identity during LLM event sanitization. */ +export type LlmCodecIdentity = + | { kind: 'none' } + | { + kind: 'builtin'; + id: 'openai_chat' | 'openai_responses' | 'anthropic_messages' | 'oci_genai' | 'gemini_generate_content'; + } + | { kind: 'runtime'; id: string } + | { kind: 'opaque' }; + +export interface LlmCodec< + TAnnotated extends JsonCompatible = JsonValue, + TEncoded extends LlmRequestCompatible = LlmRequestShape, +> { + decode: (request: LlmRequestShape) => TAnnotated; + encode: (annotated: JsonValue, original: LlmRequestShape) => TEncoded; +} + +export interface LlmResponseCodec = JsonValue> { + decodeResponse: (response: JsonValue) => TDecoded; +} + +/** Request codec context for sanitizer and execution callbacks. */ +export interface LlmSanitizeRequestContext { + codec: LlmCodecIdentity; + /** Active codec, valid until the callback returns. */ + resolveCodec(): LlmCodec | null; +} + +/** Response codec context for sanitizer and execution callbacks. */ +export interface LlmSanitizeResponseContext { + codec: LlmCodecIdentity; + /** Active codec, valid until the callback returns. */ + resolveCodec(): LlmResponseCodec | null; +} + +/** Request codec context for an LLM execution intercept. */ +export type LlmRequestContext = LlmSanitizeRequestContext; + +/** Response codec context for an LLM execution intercept. */ +export type LlmResponseContext = LlmSanitizeResponseContext; + +/** Codecs available to one LLM execution intercept call. */ +export interface LlmExecutionContext { + /** Request codec, with decode and encode when available. */ + requestCodec: LlmRequestContext; + /** Response codec, with decode when available; `null` for streaming. */ + responseCodec: LlmResponseContext | null; +} + +/** Schema tag for an optimization payload. */ +export interface LlmOptimizationDataSchema { + name: string; + version: string; +} + +/** Model identity kept for repricing. */ +export interface LlmOptimizationModel { + model: string; + provider?: string; +} + +/** Baseline and effective models of a routing optimization. */ +export interface LlmOptimizationModelTransition { + baseline?: LlmOptimizationModel; + effective?: LlmOptimizationModel; +} + +/** Token counts reported directly. */ +export interface LlmOptimizationTokens { + /** Non-negative safe integer. */ + prompt_tokens?: number; + /** Non-negative safe integer. */ + completion_tokens?: number; + /** Non-negative safe integer. */ + cache_read_tokens?: number; + /** Non-negative safe integer. */ + cache_write_tokens?: number; + /** Non-negative safe integer. */ + total_tokens?: number; +} + +/** Baseline, effective, and saved tokens of one optimization. */ +export interface LlmOptimizationTokenImpact { + baseline?: LlmOptimizationTokens; + effective?: LlmOptimizationTokens; + saved?: LlmOptimizationTokens; + quality?: 'observed' | 'estimated'; + estimation_method?: string; +} + +/** One plugin's optimization evidence. `kind` accepts any string and unknown fields are kept. */ +export interface LlmOptimizationContribution { + id?: string; + /** Safe integer. */ + sequence?: number; + producer: string; + kind: 'input_compression' | 'model_routing' | (string & {}); + applied: boolean; + model_transition?: LlmOptimizationModelTransition; + token_impact?: LlmOptimizationTokenImpact; + payload_schema?: LlmOptimizationDataSchema; + payload?: Json; + [key: string]: + | Json + | LlmOptimizationModelTransition + | LlmOptimizationTokenImpact + | LlmOptimizationDataSchema + | undefined; +} + +/** Result of an LLM request intercept. */ +export interface LlmRequestInterceptOutcome { + request: LlmRequestShape; + annotated?: Json | null; + pendingMarks?: PendingMarkSpec[]; + optimizationContributions?: LlmOptimizationContribution[]; + [key: string]: JsonValue | PendingMarkSpec[] | LlmOptimizationContribution[] | undefined; +} + +export interface ConfigDiagnostic { + level: 'warning' | 'error'; + code: string; + component?: string; + field?: string; + message: string; +} +export interface ToolExecutionInterceptOutcome extends ToolExecutionResult { + pendingMarks?: PendingMarkSpec[]; + [key: string]: JsonValue | PendingMarkSpec[] | undefined; +} +export interface PluginContext { + registerConditionalMiddlewareGuardrail( + name: string, + kinds: Array, + registrationName: string, + guardrail: (kinds: RuntimeRegistrationKind[], registrationName: string) => string | null, + ): void; + registerSubscriber(name: string, callback: (event: JsonObject) => void | Promise): void; + registerEventMetadataInjector>( + name: string, + priority: number, + callback: (event: JsonObject) => TMetadata | Promise, + ): void; + registerMarkSanitizeGuardrail>( + name: string, + priority: number, + callback: (event: JsonObject, fields: EventSanitizeFields) => TFields | Promise, + ): void; + registerScopeSanitizeStartGuardrail>( + name: string, + priority: number, + callback: (event: JsonObject, fields: EventSanitizeFields) => TFields | Promise, + ): void; + registerScopeSanitizeEndGuardrail>( + name: string, + priority: number, + callback: (event: JsonObject, fields: EventSanitizeFields) => TFields | Promise, + ): void; + registerToolSanitizeRequestGuardrail>( + name: string, + priority: number, + callback: (name: string, args: Json) => TValue | Promise, + ): void; + registerToolSanitizeResponseGuardrail>( + name: string, + priority: number, + callback: (name: string, result: Json) => TValue | Promise, + ): void; + registerToolConditionalExecutionGuardrail( + name: string, + priority: number, + callback: (name: string, args: Json) => string | null | Promise, + ): void; + registerLlmSanitizeRequestGuardrail = LlmRequestShape>( + name: string, + priority: number, + callback: ( + request: LlmRequestShape, + context: LlmSanitizeRequestContext, + ) => TRequest | null | Promise, + ): void; + registerLlmSanitizeResponseGuardrail = Json>( + name: string, + priority: number, + callback: (response: Json, context: LlmSanitizeResponseContext) => TResponse | null | Promise, + ): void; + registerLlmConditionalExecutionGuardrail( + name: string, + priority: number, + callback: (request: LlmRequestShape) => string | null | Promise, + ): void; + registerLlmRequestIntercept>( + name: string, + priority: number, + breakChain: boolean, + callback: (args: { + name: string; + request: LlmRequestShape; + annotated: Json | null; + }) => TOutcome | Promise, + ): void; + registerLlmExecutionIntercept>( + name: string, + priority: number, + callback: ( + request: LlmRequestShape, + context: LlmExecutionContext, + next: >(request: TNextRequest) => Json | Promise, + ) => TResponse | Promise, + ): void; + registerLlmStreamExecutionIntercept>( + name: string, + priority: number, + callback: ( + request: LlmRequestShape, + context: LlmExecutionContext, + next: >( + request: TNextRequest, + ) => Promise>, + ) => AsyncIterable | Promise>, + ): void; + registerToolRequestIntercept>( + name: string, + priority: number, + breakChain: boolean, + callback: (name: string, args: Json) => TArgs | Promise, + ): void; + registerToolExecutionIntercept>( + name: string, + priority: number, + callback: ( + context: ToolExecutionContext, + next: >( + args: TNextArgs, + ) => ToolExecutionResult | Promise, + ) => TOutcome | Promise, + ): void; +} + +/** Scalar accepted in event metadata. */ +export type EventMetadataScalar = string | number | boolean; + +/** Value accepted in event metadata; numeric arrays are all integers or all floats. */ +export type EventMetadataValue = EventMetadataScalar | readonly string[] | readonly number[] | readonly boolean[]; + +/** Metadata returned by an event metadata injector. */ +export type EventMetadata = Record; + +/** Validation or activation report for a plugin config. */ +export interface ConfigReport { + diagnostics: ConfigDiagnostic[]; + runtime_diagnostics?: RuntimeDiagnostic[]; +} + +/** One aggregated runtime plugin failure. */ +export interface RuntimeDiagnostic { + code: string; + component: string; + field?: string; + message: string; + session_id?: string; + count: number; +} + +/** How a dynamic plugin runs. */ +export type DynamicPluginKind = 'rust_dynamic' | 'worker'; + +export interface PluginHostReport { + config: ConfigReport; + dynamic_plugins: DynamicPluginValidationReport[]; + /** `plugins.toml` files merged into the resolved config. */ + config_paths: string[]; + /** Merged plugin config with sensitive values redacted. */ + resolved_config: Json; +} + +export type DynamicPluginCheckState = 'unknown' | 'valid' | 'invalid'; + +export interface DynamicPluginValidationStatus { + manifest: DynamicPluginCheckState; + compatibility: DynamicPluginCheckState; + integrity: DynamicPluginCheckState; + environment: DynamicPluginCheckState; + authenticity: DynamicPluginCheckState; + policy_satisfied: DynamicPluginCheckState; + checked_at?: string | null; + message?: string | null; +} + +export interface DynamicPluginFailure { + phase: string; + code: string; + message: string; +} + +export interface DynamicPluginValidationReport { + plugin_id: string; + manifest_ref: string; + kind: DynamicPluginKind; + status: DynamicPluginValidationStatus; + failure?: DynamicPluginFailure | null; + selected: boolean; +} + +/** Prompt-cache write tokens for one cache duration. */ +export interface CacheWriteTokenCounts { + count: number; + /** Cache duration in seconds, when the provider reports it. */ + duration?: number; +} + +/** Token usage for cache telemetry. */ +export interface CacheUsage { + prompt_tokens?: number; + completion_tokens?: number; + total_tokens?: number; + cache_read_tokens?: number; + cache_write_tokens?: CacheWriteTokenCounts[]; + cost?: Json; +} + +/** Agent identity in cache telemetry. */ +export interface AgentIdentity { + agent_id: string; + template_version: string; + toolset_hash: string; + model_family: string; + tenant_scope: string; +} + +/** Input to `buildCacheRequestFacts`. */ +export interface CacheRequestFactsOptions { + provider: string; + requestId: string; + annotatedRequest: Json; + agentId: string; + timestamp?: string; +} + +/** Request facts that classify cache misses. */ +export interface CacheRequestFacts { + provider: string; + stable_prefix_length: number; + stable_prefix_tokens?: number; + required_min_tokens?: number; + first_mismatch_span_id?: string; + first_mismatch_sequence_index?: number; + expected_hash_prefix?: string; + actual_hash_prefix?: string; + retention_window_secs?: number; + observed_gap_secs?: number; + missing_facts?: string[]; +} + +/** Input to `buildCacheTelemetryEvent`. */ +export interface CacheTelemetryEventOptions { + provider: 'anthropic' | 'openai' | string; + requestId: string; + usage?: CacheUsage | null; + requestFacts?: CacheRequestFacts | null; + agentId: string; + templateVersion: string; + toolsetHash: string; + modelFamily: string; + tenantScope: string; + timestamp?: string; +} + +/** Cache telemetry event. */ +export interface CacheTelemetryEvent { + request_id: string; + agent_identity: AgentIdentity; + cache_read_tokens: number; + cache_creation_tokens: number; + total_prompt_tokens: number; + hit_rate: number; + miss_reason?: Record; + miss_diagnosis?: Record; + provider: string; + timestamp: string; +} + +/** Diagnostic integer, written as a decimal string in canonical JSON. */ +export type DiagnosticNumber = bigint; +/** Producer of the evidence. */ +export interface SourceIdentity { + /** Unique id of this process run. */ + instanceId: string; + /** Operating system process ID, not an identity key. */ + processId: number | null; + /** Role, such as gateway or worker. */ + processRole: string; + /** Relay version that produced the evidence. */ + relayVersion: string; +} + +/** Component registration. */ +export interface ComponentIdentity { + /** Logical name without request or session ids. */ + key: string; + /** Unique id of this registration. */ + instanceId: string; + /** Generation, increased when a logical key is replaced. */ + generation: bigint; +} + +/** Owner and configuration of a component. */ +export interface ComponentOwnership { + /** Subscriber registration, when applicable. */ + subscriberInstanceId: string | null; + /** Plugin registration, when applicable. */ + pluginInstanceId: string | null; + /** Id of the committed configuration. */ + configurationId: string | null; + /** Runtime responsibility of the owner. */ + responsibility: string | null; +} + +/** Where an observation came from. */ +export interface DiagnosticOrigin { + /** Refresh generation of the observation. */ + refreshGeneration: bigint; + /** Original owner and configuration. */ + ownership: ComponentOwnership; +} + +/** Session and request of an observation. */ +export interface DiagnosticCorrelation { + /** Trajectory session, when known. */ + sessionId?: string | null; + /** Request, when known. */ + requestId?: string | null; +} + +/** Code and cause of an observation. */ +export interface Classification { + /** Built-in name or registered extension name. */ + canonicalCode: string; + /** Cause name, absent for successes and notices. */ + failureClass: string | null; +} + +/** Text attached to an observation. */ +export interface DiagnosticDetail { + /** Unsanitized text cut to the byte capacity. */ + message: string; + /** Whether the text was cut. */ + truncated: boolean; +} + +/** One retained observation. */ +export interface DiagnosticEvent { + /** Unique update sequence, not an occurrence count. */ + sequence: bigint; + /** Observation time at the source. */ + observedAt: string; + /** Registration that made the observation. */ + component: ComponentIdentity; + /** Configuration at observation time. */ + origin: DiagnosticOrigin; + /** Remote submission origin, absent for local observations. */ + remote?: RemoteDiagnosticOrigin | null; + /** Event classification. */ + classification: Classification; + /** Plugin operation facts, excluded from aggregation keys. */ + context: PluginObservationContext; + /** Severity, independent of outcome. */ + severity: string; + /** Operation outcome. */ + outcome: string; + /** Message attached to the observation, or `null`. */ + detail: DiagnosticDetail | null; + /** Occurrences added by this recording. */ + count: bigint; + /** Caller correlation, excluded from aggregation identity. */ + correlation: DiagnosticCorrelation; +} + +/** Origin of a remote submission. */ +export interface RemoteDiagnosticOrigin { + /** Host-issued session id. */ + sourceInstanceId: string; + /** Host-issued plugin activation id. */ + activationInstanceId: string; + /** Submission sequence, not the host event sequence. */ + submissionSequence: bigint; + /** Producer-reported wall time, if any. */ + observedAt: string | null; + /** Host wall time at acceptance, used as the event observation time. */ + receivedAt: string; + /** Host monotonic time at acceptance since context startup. */ + acceptedElapsedMillis: bigint; +} + +/** Count that may saturate. */ +export interface DiagnosticCount { + /** Exact count unless saturated. */ + value: bigint; + /** Whether overflow made the value a lower bound. */ + saturated: boolean; +} + +/** Occurrence totals by outcome. */ +export interface DiagnosticTotals { + /** Accepted recording calls. */ + recordedEventsTotal: DiagnosticCount; + /** Accepted occurrences across outcomes and codes. */ + occurrencesTotal: DiagnosticCount; + /** Retained events later evicted. */ + historyEvictionsTotal: DiagnosticCount; + /** Occurrences with a successful outcome. */ + successesTotal: DiagnosticCount; + /** Occurrences with a failed outcome. */ + failuresTotal: DiagnosticCount; + /** Occurrences with no outcome. */ + unspecifiedTotal: DiagnosticCount; +} + +/** Totals over a lifetime. */ +export interface DiagnosticLifetimeSummary { + /** Totals for the context or component. */ + totals: DiagnosticTotals; + /** Elapsed lifetime at capture. */ + elapsedMillis: bigint; + /** Wall-clock time of the latest failure. */ + lastFailureAt: string | null; + /** Elapsed time since the latest failure, or `null` when none was observed. */ + sinceLastFailureMillis: bigint | null; +} + +/** Totals since the last refresh. */ +export interface DiagnosticRefreshSummary { + /** Generation, increased by a successful refresh. */ + generation: bigint; + /** Wall-clock time of the refresh. */ + startedAt: string; + /** Elapsed time since the refresh. */ + elapsedMillis: bigint; + /** Configuration and owner for this period. */ + ownership: ComponentOwnership; + /** Counts for this period, separate from lifetime totals. */ + totals: DiagnosticTotals; +} + +/** Occurrences of one code. */ +export interface DiagnosticCounter { + /** Canonical code of the counted observations. */ + code: string; + /** Accepted occurrences. */ + occurrences: DiagnosticCount; + /** Time of the last update. */ + lastObservedAt: string; +} + +/** One time bucket of outcomes. */ +export interface DiagnosticWindowBucket { + /** Inclusive start offset from component start. */ + startElapsedMillis: bigint; + /** Exclusive end offset of the bucket. */ + endElapsedMillis: bigint; + /** Successful occurrences in this interval. */ + successes: DiagnosticCount; + /** Failed occurrences in this interval. */ + failures: DiagnosticCount; + /** Occurrences with no outcome in this interval. */ + unspecified: DiagnosticCount; +} + +/** Interval covered by a window. */ +export interface DiagnosticWindowCoverage { + /** Availability of the window series. */ + availability: string; + /** Earliest covered offset. */ + startElapsedMillis: bigint; + /** Latest covered offset at capture. */ + endElapsedMillis: bigint; + /** First bucket is only partly covered. */ + partialStart: boolean; + /** Last bucket is still open or partly selected. */ + partialEnd: boolean; + /** Known loss makes the interval incomplete. */ + incomplete: boolean; +} + +/** Fixed-bucket outcome series. */ +export interface DiagnosticWindow { + /** Bucket duration. */ + resolutionMillis: bigint; + /** Configured bucket count. */ + bucketCapacity: number; + /** Selected buckets, zero when idle. */ + buckets: Array; + /** Covered interval and edge limits. */ + coverage: DiagnosticWindowCoverage; +} + +/** Position for the next page. */ +export interface DiagnosticCursor { + /** Source run whose state was selected. */ + sourceInstanceId: string; + /** Source revision at the time the cursor was issued. */ + sequence: bigint; + /** Next component position. */ + componentOffset: bigint; + /** Next event position. */ + eventOffset: bigint; + /** Fingerprint of the query scope. */ + scope: bigint; +} + +/** Selection for a snapshot. */ +export interface DiagnosticQuery { + /** Snapshot schema version to request. */ + schemaVersion?: number | null; + /** Selects one component key. */ + componentKey?: string | null; + /** Selects one registration instance. */ + componentInstanceId?: string | null; + /** Selects recent events by refresh generation. */ + originGeneration?: bigint | null; + /** Selects one code. */ + code?: string | null; + /** Selects a catalogue domain. */ + domain?: string | null; + /** Selects a plugin catalogue family. */ + pluginFamily?: string | null; + /** Selects event causes. */ + failureClass?: string | null; + /** Selects components by current ownership and events by original ownership. */ + ownership?: Partial; + /** Selects events after this sequence. */ + afterSequence?: bigint | null; + /** Maximum number of component rows to return. */ + maxComponents?: number | null; + /** Maximum number of events to return. */ + maxEvents?: number | null; + /** Maximum encoded response size in bytes. */ + maxResponseBytes?: number | null; + /** Resumes a selection with the same scope. */ + continuation?: DiagnosticCursor | null; +} + +/** History bounds of a selection. */ +export interface DiagnosticRetention { + /** Oldest selected event sequence, if any. */ + oldestEventSequence: bigint | null; + /** Latest selected event sequence, if any. */ + latestEventSequence: bigint | null; + /** Returned event count. */ + eventsRetained: bigint; + /** Returned component count. */ + componentsRetained: bigint; + /** Whether query limits cut matching evidence. */ + queryTruncated: boolean; +} + +/** Cumulative loss counters. */ +export interface DiagnosticLoss { + /** Events evicted from history. */ + eventsEvicted: DiagnosticCount; + /** Observations accepted without retaining the event. */ + eventsOmitted: DiagnosticCount; + /** Closed components evicted with their evidence. */ + componentsEvicted: DiagnosticCount; + /** Component registrations rejected by limits. */ + componentsRejected: DiagnosticCount; + /** Code registrations rejected by limits. */ + keysRejected: DiagnosticCount; + /** Observations rejected before counting. */ + observationsRejected: DiagnosticCount; + /** Detail cut to its byte limit. */ + messagesTruncated: DiagnosticCount; + /** Detail dropped without evicting an event. */ + detailsOmitted: DiagnosticCount; + /** Correlation fields cut to their byte limit. */ + correlationsTruncated: DiagnosticCount; + /** Correlation fields dropped for lack of space. */ + correlationsOmitted: DiagnosticCount; + /** Metadata registrations rejected by budget. */ + metadataRejected: DiagnosticCount; + /** Counted occurrences whose classification was not admitted. */ + classificationsOverflowed: DiagnosticCount; + /** Window series rejected by budget. */ + windowsRejected: DiagnosticCount; + /** Queries rejected for lack of workspace. */ + queriesRejected: DiagnosticCount; + /** Recordings for closed or replaced registrations. */ + staleObservations: DiagnosticCount; + /** Projection deliveries dropped for lack of workspace. */ + projectionDeliveriesOmitted: DiagnosticCount; + /** Projection callbacks that panicked. */ + projectionsPanicked: DiagnosticCount; + /** Projection chains suppressed for cycles or depth. */ + projectionsSuppressed: DiagnosticCount; + /** Sequence numbers are exhausted. */ + sequenceSaturated: boolean; +} + +/** One producer integration. */ +export interface DiagnosticCapability { + /** Capability name, such as `otel`. */ + name: string; + /** Availability of this capability's evidence. */ + availability: string; + /** Reason, such as `not_integrated`. */ + reason: string | null; +} + +/** Limits of a snapshot. */ +export interface DiagnosticCoverage { + /** Availability of this diagnostic source. */ + availability: string; + /** Scope of component summaries and windows. */ + summaryScope: string; + /** Selected history bounds and query cuts. */ + retention: DiagnosticRetention; + /** Cumulative loss counters. */ + loss: DiagnosticLoss; + /** Producer integrations and whether each is supported. */ + capabilities: Array; +} + +/** One component row. */ +export interface ComponentSnapshot { + /** Logical name and registration instance. */ + identity: ComponentIdentity; + /** Registration time. */ + registeredAt: string; + /** Time the registration closed, or `null` while it is open. */ + closedAt: string | null; + /** Lifetime totals and elapsed time. */ + lifetime: DiagnosticLifetimeSummary; + /** Current refresh period with its owner. */ + sinceRefresh: DiagnosticRefreshSummary; + /** Selected lifetime counters by code. */ + counters: Array; + /** Outcome windows of the component. */ + windows: Array; +} + +/** One diagnostics page. */ +export interface DiagnosticSnapshot { + /** Schema version of this snapshot. */ + schemaVersion: number; + /** Revision of the code catalogue. */ + catalogueVersion: number; + /** Process that produced the snapshot. */ + source: SourceIdentity; + /** Capture time at the source. */ + capturedAt: string; + /** Latest update revision. */ + sequence: bigint; + /** Lifetime totals for the whole context, ignoring selectors. */ + lifetime: DiagnosticLifetimeSummary; + /** One row per selected component. */ + components: Array; + /** Selected recent events. */ + events: Array; + /** Availability, scope, integration, and loss limits. */ + coverage: DiagnosticCoverage; + /** Cursor for the next page, or `null` when there is none. */ + continuation: DiagnosticCursor | null; +} + +/** Plugin operation facts. */ +export interface PluginObservationContext { + /** Execution name, when known. */ + execution: string | null; + /** Lifecycle phase or policy gate, when known. */ + phase: string | null; +} + +/** Capabilities of the diagnostics core. */ +export interface DiagnosticDiscovery { + discoveryVersion: number; + snapshotVersion: number; + catalogueVersion: number; + source: SourceIdentity; + collectionEnabled: boolean; + queriesEnabled: boolean; + maxComponents: bigint; + maxEvents: bigint; + detailPoolBytes: bigint; + maxQueries: bigint; + maxSnapshotBytes: bigint; + maxOwnedBytes: bigint; + maxQueryComponents: bigint; + maxQueryEvents: bigint; + memoryInspection: boolean; + restrictedDetail: boolean; + producerSupport: Array<{ name: string; supported: boolean }>; + detailedMemoryInspection: boolean; + resourceCorrelation: boolean; +} + +export interface DiagnosticsInfo { + discovery: DiagnosticDiscovery; + limits: Record; + memory: DiagnosticMemoryUsage; + bindingCopyConcurrency: number; + bindingMaxPageBytes: number; + bindingReservedBytes: number; + loggingCoverage: string; +} + +/** Resource number with explicit precision. */ +export type ResourceMemoryNumber = + | { kind: 'integer'; value: bigint } + | { kind: 'decimal'; value: number } + | { kind: 'non_finite' }; +export interface ResourceMemoryUnit { + dimension: 'capacity' | 'duration' | 'count'; + name: string; +} + +/** Reserved bytes and occupancy gauges. */ +export interface DiagnosticMemoryUsage { + reservedBytes: bigint; + components: bigint; + counters: bigint; + events: bigint; + eventSlotBytes: bigint; + eventCapacity: bigint; + historyReservedBytes: bigint; + historyOccupiedBytes: bigint; + detailBytes: bigint; + detailCapacityBytes: bigint; + detailReservedBytes: bigint; + detailMetadataBytes: bigint; + detailLogicalBytes: bigint; + detailSlackBytes: bigint; + metadataBytes: bigint; + windowBuckets: bigint; + extensions: bigint; + queryWorkspaces: bigint; + projections: bigint; + projectionWorkspaces: bigint; +} + +/** One event header and its text. */ +export interface EntryMemoryFootprint { + headerBytes: bigint; + detailPayloadBytes: bigint; + detailMetadataBytes: bigint; + logicalTextBytes: bigint; + detailSlackBytes: bigint; + occupiedBytes: bigint; +} + +/** Storage owned by one registration, excluding plugin heap. */ +export interface ComponentMemoryFootprint { + registrationBytes: bigint; + counterBytes: bigint; + windowBytes: bigint; + extensionBytes: bigint; + retainedEventCount: bigint; + retainedEventBytes: bigint; + occupiedBytes: bigint; +} + +/** One event and its footprint. */ +export interface EntryMemoryUsage extends EntryMemoryFootprint { + sequence: bigint; + component: ComponentIdentity; +} + +/** One registration and its footprint. */ +export interface ComponentMemoryUsage extends ComponentMemoryFootprint { + component: ComponentIdentity; +} + +/** Row counts and byte totals. */ +export interface MemorySelectionTotals { + components: bigint; + events: bigint; + componentOccupiedBytes: bigint; + entryOccupiedBytes: bigint; +} + +/** One memory page with resource context. */ +export interface DiagnosticMemoryPage { + schemaVersion: number; + source: SourceIdentity; + capturedAt: string; + sequence: bigint; + aggregate: DiagnosticMemoryUsage; + loss: DiagnosticLoss; + componentOccupiedBytes: bigint; + sharedOrUnassignedReservedBytes: bigint; + selection: MemorySelectionTotals; + page: MemorySelectionTotals; + components: Array; + entries: Array; + resource: ResourceMemoryView; + continuation: DiagnosticCursor | null; +} + +/** Resource value with its number kind, unit, scope, and coverage. */ +export interface ResourceMemoryMeasurement { + value: ResourceMemoryNumber; + unit: ResourceMemoryUnit; + measurementScope: string; + sampledProcesses: bigint | null; +} + +/** One cumulative limit event. */ +export interface MemoryLimitEvent { + resource: string; + event: string; + count: ResourceMemoryMeasurement | null; +} + +/** Memory measurements and up to eight limit events. */ +export interface ResourceMemoryMetrics { + systemUsed: ResourceMemoryMeasurement | null; + systemTotal: ResourceMemoryMeasurement | null; + systemAvailable: ResourceMemoryMeasurement | null; + resident: ResourceMemoryMeasurement | null; + private: ResourceMemoryMeasurement | null; + physicalFootprint: ResourceMemoryMeasurement | null; + virtualMemory: ResourceMemoryMeasurement | null; + peakResident: ResourceMemoryMeasurement | null; + limit: ResourceMemoryMeasurement | null; + environmentAccounted: ResourceMemoryMeasurement | null; + somePressureStallTime: ResourceMemoryMeasurement | null; + fullPressureStallTime: ResourceMemoryMeasurement | null; + outOfMemoryEventCount: ResourceMemoryMeasurement | null; + limitEvents: Array; +} + +/** Root process identity checked during collection. */ +export interface ResourceProcessIdentity { + processId: number; + startIdentity: bigint; +} + +/** One collection and its memory result. */ +export interface ResourceMemorySample { + collectorInstanceId: string; + runtimeGeneration: bigint; + targetGeneration: bigint; + stream: string; + ordinal: bigint; + startedAt: string; + completedAt: string; + collectionMillis: bigint; + operatingSystem: string; + measurementScope: string; + root: ResourceProcessIdentity | null; + visibleProcesses: bigint | null; + sampledProcesses: bigint | null; + memory: ResourceMemoryMetrics; +} + +/** Collection availability, association, and freshness. */ +export interface ResourceMemoryView { + availability: string; + sample: ResourceMemorySample | null; + association: string; + ageMillis: bigint | null; + stale: boolean; + projectionSlotBytes: bigint; +} diff --git a/crates/node/diagnostics.d.ts b/crates/node/diagnostics.d.ts deleted file mode 100644 index bf2e291c84..0000000000 --- a/crates/node/diagnostics.d.ts +++ /dev/null @@ -1,602 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -/** Lossless native inspection values. Canonical JSON remains snake_case with decimal strings. */ -export type DiagnosticNumber = bigint; -/** SourceIdentity preserves the shared diagnostic contract. */ -export interface SourceIdentity { - /** Unique incarnation assigned before logging initialization. */ - instanceId: string; - /** Descriptive operating system process ID, never an identity key. */ - processId: number | null; - /** Producer-defined role, such as embedded runtime, gateway, broker, or worker. */ - processRole: string; - /** Relay package version producing the evidence. */ - relayVersion: string; -} - -/** ComponentIdentity preserves the shared diagnostic contract. */ -export interface ComponentIdentity { - /** Stable logical name, excluding request and session identifiers. */ - key: string; - /** Unique registration instance, distinguishing replacement components. */ - instanceId: string; - /** Source-local registration generation, increasing when a logical key is replaced. */ - generation: bigint; -} - -/** ComponentOwnership preserves the shared diagnostic contract. */ -export interface ComponentOwnership { - /** Subscriber registration associated with this attribution, when applicable. */ - subscriberInstanceId: string | null; - /** Plugin registration associated with this attribution, when applicable. */ - pluginInstanceId: string | null; - /** Bounded identity of the committed configuration. */ - configurationId: string | null; - /** Bounded runtime responsibility for an embedded or process owner. */ - responsibility: string | null; -} - -/** DiagnosticOrigin preserves the shared diagnostic contract. */ -export interface DiagnosticOrigin { - /** Refresh generation associated with the originating work or observation. */ - refreshGeneration: bigint; - /** Original bounded owner and configuration attribution. */ - ownership: ComponentOwnership; -} - -/** DiagnosticCorrelation preserves the shared diagnostic contract. */ -export interface DiagnosticCorrelation { - /** Trajectory session associated with the observation, when known. */ - sessionId?: string | null; - /** Request associated with the observation, when known. */ - requestId?: string | null; -} - -/** Classification preserves the shared diagnostic contract. */ -export interface Classification { - /** Stable built-in name or registered namespaced extension name. */ - canonicalCode: string; - /** Cause name; absent for successful observations and notices. */ - failureClass: string | null; -} - -/** DiagnosticDetail preserves the shared diagnostic contract. */ -export interface DiagnosticDetail { - /** Text copied within admitted byte capacity; bounding does not sanitize it. */ - message: string; - /** Whether the original text exceeded its retained byte allowance. */ - truncated: boolean; -} - -/** DiagnosticEvent preserves the shared diagnostic contract. */ -export interface DiagnosticEvent { - /** Unique source-local update sequence; it is not an occurrence counter. */ - sequence: bigint; - /** Source observation time, preserved through projections. */ - observedAt: string; - /** Registration that owned the original observation. */ - component: ComponentIdentity; - /** Original configuration attribution, preserved through component refresh. */ - origin: DiagnosticOrigin; - /** Authenticated remote submission provenance, absent for local observations. */ - remote?: RemoteDiagnosticOrigin | null; - /** Canonical event classification; never reconstructed from a latest message. */ - classification: Classification; - /** Bounded plugin-operation wire facts, excluded from aggregation keys. */ - context: PluginObservationContext; - /** Evidence severity, independent of its observed operation outcome. */ - severity: string; - /** Explicit observed operation outcome. */ - outcome: string; - /** Bounded optional evidence; absence does not imply a missing counter. */ - detail: DiagnosticDetail | null; - /** Input occurrence delta represented by this recording call. */ - count: bigint; - /** Restricted caller correlation, excluded from aggregation identity. */ - correlation: DiagnosticCorrelation; -} - -/** RemoteDiagnosticOrigin preserves the shared diagnostic contract. */ -export interface RemoteDiagnosticOrigin { - /** Host-issued diagnostic session/source incarnation. */ - sourceInstanceId: string; - /** Host-issued plugin activation incarnation, distinct from the session. */ - activationInstanceId: string; - /** Ordered terminal submission sequence; not the host event sequence. */ - submissionSequence: bigint; - /** Optional producer-reported wall time, with unknown clock uncertainty. */ - observedAt: string | null; - /** Host wall time at acceptance, also used as the event observation time. */ - receivedAt: string; - /** Host monotonic elapsed time at acceptance, relative to context startup. */ - acceptedElapsedMillis: bigint; -} - -/** DiagnosticCount preserves the shared diagnostic contract. */ -export interface DiagnosticCount { - /** Exact count unless saturation has occurred. */ - value: bigint; - /** Whether an overflowing increment made the value a lower bound. */ - saturated: boolean; -} - -/** DiagnosticTotals preserves the shared diagnostic contract. */ -export interface DiagnosticTotals { - /** Accepted recording calls, one per call regardless of occurrence delta. */ - recordedEventsTotal: DiagnosticCount; - /** Accepted occurrence deltas across all outcomes and codes. */ - occurrencesTotal: DiagnosticCount; - /** Previously retained events owned by this component or context and evicted. */ - historyEvictionsTotal: DiagnosticCount; - /** Occurrences whose owning operation explicitly succeeded. */ - successesTotal: DiagnosticCount; - /** Occurrences whose owning operation explicitly failed. */ - failuresTotal: DiagnosticCount; - /** Occurrences without an established operation outcome. */ - unspecifiedTotal: DiagnosticCount; -} - -/** DiagnosticLifetimeSummary preserves the shared diagnostic contract. */ -export interface DiagnosticLifetimeSummary { - /** Whole-context or whole-component-instance accounting, as named by the view. */ - totals: DiagnosticTotals; - /** Monotonic elapsed lifetime measured by the source at capture. */ - elapsedMillis: bigint; - /** Wall-clock correlation time of the latest observed lifetime failure. */ - lastFailureAt: string | null; - /** Monotonic elapsed time since that failure; absence never means recovery. */ - sinceLastFailureMillis: bigint | null; -} - -/** DiagnosticRefreshSummary preserves the shared diagnostic contract. */ -export interface DiagnosticRefreshSummary { - /** Component-local generation increased by a successful refresh. */ - generation: bigint; - /** Wall-clock correlation anchor for the successful refresh. */ - startedAt: string; - /** Monotonic elapsed duration since the refresh boundary. */ - elapsedMillis: bigint; - /** Current configuration and owner attribution for this counting period. */ - ownership: ComponentOwnership; - /** Counts for this current period, independently reserved from lifetime totals. */ - totals: DiagnosticTotals; -} - -/** DiagnosticCounter preserves the shared diagnostic contract. */ -export interface DiagnosticCounter { - /** Canonical code; the component instance comes from the containing row. */ - code: string; - /** Accepted occurrences, independent of optional recent detail. */ - occurrences: DiagnosticCount; - /** Source time when this retained counter was last updated. */ - lastObservedAt: string; -} - -/** DiagnosticWindowBucket preserves the shared diagnostic contract. */ -export interface DiagnosticWindowBucket { - /** Inclusive elapsed offset from the component's monotonic start. */ - startElapsedMillis: bigint; - /** Exclusive nominal bucket end; coverage describes the observed portion. */ - endElapsedMillis: bigint; - /** Explicit successful-operation occurrences observed in this interval. */ - successes: DiagnosticCount; - /** Explicit failed-operation occurrences observed in this interval. */ - failures: DiagnosticCount; - /** Occurrences with no established operation outcome in this interval. */ - unspecified: DiagnosticCount; -} - -/** DiagnosticWindowCoverage preserves the shared diagnostic contract. */ -export interface DiagnosticWindowCoverage { - /** Availability of the admitted window series. */ - availability: string; - /** Earliest covered monotonic elapsed offset within this component lifetime. */ - startElapsedMillis: bigint; - /** Latest covered monotonic elapsed offset at capture time. */ - endElapsedMillis: bigint; - /** The first returned bucket is only partly inside the covered interval. */ - partialStart: boolean; - /** The last returned bucket is still being observed or partly selected. */ - partialEnd: boolean; - /** Known loss prevents treating the aggregate as a complete interval. */ - incomplete: boolean; -} - -/** DiagnosticWindow preserves the shared diagnostic contract. */ -export interface DiagnosticWindow { - /** Duration of each fixed bucket. */ - resolutionMillis: bigint; - /** Configured bucket slots; memory is independent of observation rate. */ - bucketCapacity: number; - /** Bounded selected aggregates; idle covered intervals may have zero values. */ - buckets: Array; - /** Actual interval and edge limitations. */ - coverage: DiagnosticWindowCoverage; -} - -/** DiagnosticCursor preserves the shared diagnostic contract. */ -export interface DiagnosticCursor { - /** Source incarnation whose retained state was selected. */ - sourceInstanceId: string; - /** Source revision for detecting intervening mutations or reclamation. */ - sequence: bigint; - /** Next component-instance position in the bounded source ordering. */ - componentOffset: bigint; - /** Next recent-event position in the bounded source ordering. */ - eventOffset: bigint; - /** Source-validated fingerprint of the intersected query scope. */ - scope: bigint; -} - -/** DiagnosticQuery preserves the shared diagnostic contract. */ -export interface DiagnosticQuery { - /** Optional canonical snapshot version, rejected when unsupported. */ - schemaVersion?: number | null; - /** Select one logical component key across retained registrations. */ - componentKey?: string | null; - /** Select one registration incarnation. */ - componentInstanceId?: string | null; - /** Select recent events by original refresh generation only. Lifetime and current-refresh summaries keep their declared scope. */ - originGeneration?: bigint | null; - /** Select exact canonical code counters and events. */ - code?: string | null; - /** Select the authoritative catalogue parent domain. */ - domain?: string | null; - /** Select the plugin catalogue family derived from code ownership. */ - pluginFamily?: string | null; - /** Select event causes only; code counters do not retain cause breakdowns. */ - failureClass?: string | null; - /** Select component rows by current-refresh ownership and events by original ownership. Selected lifetime counters still span the entire component instance. */ - ownership?: Partial; - /** Select later recent events; summary and code counters remain cumulative. */ - afterSequence?: bigint | null; - /** Requested component-instance row cap, rejected above core limits. */ - maxComponents?: number | null; - /** Requested recent-event cap, rejected above core limits. */ - maxEvents?: number | null; - /** Requested encoded response cap, rejected above core limits. */ - maxResponseBytes?: number | null; - /** Resume a compatible bounded selection without granting additional scope. */ - continuation?: DiagnosticCursor | null; -} - -/** DiagnosticRetention preserves the shared diagnostic contract. */ -export interface DiagnosticRetention { - /** Oldest sequence in the selected retained event history, if present. */ - oldestEventSequence: bigint | null; - /** Latest sequence in the selected retained event history, if present. */ - latestEventSequence: bigint | null; - /** Number of returned recent events. */ - eventsRetained: bigint; - /** Number of returned component-instance rows. */ - componentsRetained: bigint; - /** Query limits omitted otherwise matching retained evidence. */ - queryTruncated: boolean; -} - -/** DiagnosticLoss preserves the shared diagnostic contract. */ -export interface DiagnosticLoss { - /** Previously retained events reclaimed from the circular history. */ - eventsEvicted: DiagnosticCount; - /** Accepted observations whose incoming event evidence was not retained. */ - eventsOmitted: DiagnosticCount; - /** Closed component metadata reclaimed with dependent evidence. */ - componentsEvicted: DiagnosticCount; - /** Component registrations rejected by admission limits. */ - componentsRejected: DiagnosticCount; - /** Code reservations or registrations rejected by admission limits. */ - keysRejected: DiagnosticCount; - /** Observations rejected before canonical counting. */ - observationsRejected: DiagnosticCount; - /** Detail shortened within its configured byte allowance. */ - messagesTruncated: DiagnosticCount; - /** Incoming optional detail omitted without evicting a retained event. */ - detailsOmitted: DiagnosticCount; - /** Correlation fields shortened within their own byte allowance. */ - correlationsTruncated: DiagnosticCount; - /** Safe correlation fields omitted because detail storage could not fit them. */ - correlationsOmitted: DiagnosticCount; - /** Metadata registrations rejected by their admission budget. */ - metadataRejected: DiagnosticCount; - /** Accepted summary-only deltas whose classification could not be admitted. */ - classificationsOverflowed: DiagnosticCount; - /** Window series rejected by their independent admission budget. */ - windowsRejected: DiagnosticCount; - /** Query materializations rejected by bounded workspace admission. */ - queriesRejected: DiagnosticCount; - /** Recording attempts for closed or replaced component registrations. */ - staleObservations: DiagnosticCount; - /** Optional projection deliveries omitted for lack of bounded workspace. */ - projectionDeliveriesOmitted: DiagnosticCount; - /** Projection callbacks whose panic was isolated after acceptance. */ - projectionsPanicked: DiagnosticCount; - /** Cyclic or excessively deep projection chains suppressed. */ - projectionsSuppressed: DiagnosticCount; - /** Unique sequence allocation has exhausted its representation. */ - sequenceSaturated: boolean; -} - -/** DiagnosticCapability preserves the shared diagnostic contract. */ -export interface DiagnosticCapability { - /** Bounded capability name, such as `otel`. */ - name: string; - /** Availability of evidence from this capability. */ - availability: string; - /** Bounded explanatory reason, such as `not_integrated`. */ - reason: string | null; -} - -/** DiagnosticCoverage preserves the shared diagnostic contract. */ -export interface DiagnosticCoverage { - /** Availability of this diagnostic source context. */ - availability: string; - /** Scope of component summaries and component outcome windows. */ - summaryScope: string; - /** Bounded selected history boundaries and query omission. */ - retention: DiagnosticRetention; - /** Reserved cumulative loss and capacity accounting. */ - loss: DiagnosticLoss; - /** Producer integrations whose support or absence is explicitly advertised. */ - capabilities: Array; -} - -/** ComponentSnapshot preserves the shared diagnostic contract. */ -export interface ComponentSnapshot { - /** Logical responsibility and registration incarnation. */ - identity: ComponentIdentity; - /** Source correlation time when the registration was admitted. */ - registeredAt: string; - /** Registration closure; retained summaries remain subject to bounded retention. */ - closedAt: string | null; - /** Whole-component-instance lifetime accounting and source elapsed time. */ - lifetime: DiagnosticLifetimeSummary; - /** The sole current refresh period, with explicitly scoped current ownership. */ - sinceRefresh: DiagnosticRefreshSummary; - /** Selected lifetime code counters; no latest-message set is retained per code. */ - counters: Array; - /** Admitted bounded component outcome windows, independent of refresh periods. */ - windows: Array; -} - -/** DiagnosticSnapshot preserves the shared diagnostic contract. */ -export interface DiagnosticSnapshot { - /** Supported envelope version, independent of the catalogue version. */ - schemaVersion: number; - /** Stable catalogue revision; unknown names stay opaque in this envelope. */ - catalogueVersion: number; - /** Source identity preserved through collection and projection. */ - source: SourceIdentity; - /** Source capture time; it is not a collector receipt timestamp. */ - capturedAt: string; - /** Latest source update revision; reads do not increment it. */ - sequence: bigint; - /** Whole-context lifetime accounting, independent of component/event selectors. Hosts must authorize access to this context-wide view before exposing it. */ - lifetime: DiagnosticLifetimeSummary; - /** Bounded rows, one per selected retained component instance. */ - components: Array; - /** Bounded selected recent evidence. */ - events: Array; - /** Availability, scope, integration, and loss limitations. */ - coverage: DiagnosticCoverage; - /** Next bounded page, subject to mutation and retention validation. */ - continuation: DiagnosticCursor | null; -} - -/** PluginObservationContext preserves the shared diagnostic contract. */ -export interface PluginObservationContext { - /** Observed execution name, when known; never inferred from a code. */ - execution: string | null; - /** Observed lifecycle phase or policy-gate context, when known. */ - phase: string | null; -} - -/** Core capability view; support does not imply active/admitted producers or evidence. */ -export interface DiagnosticDiscovery { - discoveryVersion: number; - snapshotVersion: number; - catalogueVersion: number; - source: SourceIdentity; - collectionEnabled: boolean; - queriesEnabled: boolean; - maxComponents: bigint; - maxEvents: bigint; - detailPoolBytes: bigint; - maxQueries: bigint; - maxSnapshotBytes: bigint; - maxOwnedBytes: bigint; - maxQueryComponents: bigint; - maxQueryEvents: bigint; - memoryInspection: boolean; - restrictedDetail: boolean; - producerSupport: Array<{ name: string; supported: boolean }>; - detailedMemoryInspection: boolean; - resourceCorrelation: boolean; -} - -/** Stable reader failure; messages contain no query or retained evidence. */ -export class DiagnosticsError extends Error { - readonly reason: string; -} -/** Context inspection authority. Never expose it to untrusted clients without authorization. */ -export class DiagnosticsReader { - /** Capture a native camelCase page. All diagnostic u64 values are bigint. */ - snapshot(query?: DiagnosticQuery): DiagnosticSnapshot; - /** Capture portable canonical JSON, retaining decimal-string diagnostic integers. */ - snapshotJson(query?: DiagnosticQuery): string; - /** Inspect bounded backing and existing scoped resource context. */ - inspectMemory(query?: DiagnosticQuery): DiagnosticMemoryPage; - /** Canonical memory JSON with lossless integer strings. */ - inspectMemoryJson(query?: DiagnosticQuery): string; - /** Inspect separate core/copy budgets, occupancy, and logging coverage. */ - info(): { - discovery: DiagnosticDiscovery; - limits: Record; - memory: DiagnosticMemoryUsage; - bindingCopyConcurrency: number; - bindingMaxPageBytes: number; - bindingReservedBytes: number; - loggingCoverage: string; - }; -} -/** Obtain a reader without initializing the service; may throw not_initialized. */ -export function reader(): DiagnosticsReader; - -/** Resource precision is explicit; non-finite values have no numeric value. */ -export type ResourceMemoryNumber = - | { kind: 'integer'; value: bigint } - | { kind: 'decimal'; value: number } - | { kind: 'non_finite' }; -export interface ResourceMemoryUnit { - dimension: 'capacity' | 'duration' | 'count'; - name: string; -} - -/** Reserved diagnostic backing and overlapping occupancy gauges. */ -export interface DiagnosticMemoryUsage { - reservedBytes: bigint; - components: bigint; - counters: bigint; - events: bigint; - eventSlotBytes: bigint; - eventCapacity: bigint; - historyReservedBytes: bigint; - historyOccupiedBytes: bigint; - detailBytes: bigint; - detailCapacityBytes: bigint; - detailReservedBytes: bigint; - detailMetadataBytes: bigint; - detailLogicalBytes: bigint; - detailSlackBytes: bigint; - metadataBytes: bigint; - windowBuckets: bigint; - extensions: bigint; - queryWorkspaces: bigint; - projections: bigint; - projectionWorkspaces: bigint; -} - -/** One retained event header and its exclusive packed text chain. */ -export interface EntryMemoryFootprint { - headerBytes: bigint; - detailPayloadBytes: bigint; - detailMetadataBytes: bigint; - logicalTextBytes: bigint; - detailSlackBytes: bigint; - occupiedBytes: bigint; -} - -/** Registration-owned diagnostic storage, independent of plugin heap usage. */ -export interface ComponentMemoryFootprint { - registrationBytes: bigint; - counterBytes: bigint; - windowBytes: bigint; - extensionBytes: bigint; - retainedEventCount: bigint; - retainedEventBytes: bigint; - occupiedBytes: bigint; -} - -/** Identity and backing footprint of a retained observation. */ -export interface EntryMemoryUsage extends EntryMemoryFootprint { - sequence: bigint; - component: ComponentIdentity; -} - -/** Identity and complete attributed backing of a registration. */ -export interface ComponentMemoryUsage extends ComponentMemoryFootprint { - component: ComponentIdentity; -} - -/** Matching row counts and overlapping byte totals. */ -export interface MemorySelectionTotals { - components: bigint; - events: bigint; - componentOccupiedBytes: bigint; - entryOccupiedBytes: bigint; -} - -/** One bounded attribution page with independent resource context. */ -export interface DiagnosticMemoryPage { - schemaVersion: number; - source: SourceIdentity; - capturedAt: string; - sequence: bigint; - aggregate: DiagnosticMemoryUsage; - loss: DiagnosticLoss; - componentOccupiedBytes: bigint; - sharedOrUnassignedReservedBytes: bigint; - selection: MemorySelectionTotals; - page: MemorySelectionTotals; - components: Array; - entries: Array; - resource: ResourceMemoryView; - continuation: DiagnosticCursor | null; -} - -/** A resource field with its original numeric kind, unit, scope, and coverage. */ -export interface ResourceMemoryMeasurement { - value: ResourceMemoryNumber; - unit: ResourceMemoryUnit; - measurementScope: string; - sampledProcesses: bigint | null; -} - -/** One cumulative environment limit event. */ -export interface MemoryLimitEvent { - resource: string; - event: string; - count: ResourceMemoryMeasurement | null; -} - -/** Memory-only measurements and at most eight limit-event slots. */ -export interface ResourceMemoryMetrics { - systemUsed: ResourceMemoryMeasurement | null; - systemTotal: ResourceMemoryMeasurement | null; - systemAvailable: ResourceMemoryMeasurement | null; - resident: ResourceMemoryMeasurement | null; - private: ResourceMemoryMeasurement | null; - physicalFootprint: ResourceMemoryMeasurement | null; - virtualMemory: ResourceMemoryMeasurement | null; - peakResident: ResourceMemoryMeasurement | null; - limit: ResourceMemoryMeasurement | null; - environmentAccounted: ResourceMemoryMeasurement | null; - somePressureStallTime: ResourceMemoryMeasurement | null; - fullPressureStallTime: ResourceMemoryMeasurement | null; - outOfMemoryEventCount: ResourceMemoryMeasurement | null; - limitEvents: Array; -} - -/** Root process incarnation verified during resource collection. */ -export interface ResourceProcessIdentity { - processId: number; - startIdentity: bigint; -} - -/** One collection identity, interval, scope, and memory result. */ -export interface ResourceMemorySample { - collectorInstanceId: string; - runtimeGeneration: bigint; - targetGeneration: bigint; - stream: string; - ordinal: bigint; - startedAt: string; - completedAt: string; - collectionMillis: bigint; - operatingSystem: string; - measurementScope: string; - root: ResourceProcessIdentity | null; - visibleProcesses: bigint | null; - sampledProcesses: bigint | null; - memory: ResourceMemoryMetrics; -} - -/** Independent collection availability, association, and freshness. */ -export interface ResourceMemoryView { - availability: string; - sample: ResourceMemorySample | null; - association: string; - ageMillis: bigint | null; - stale: boolean; - projectionSlotBytes: bigint; -} diff --git a/crates/node/diagnostics.js b/crates/node/diagnostics.js deleted file mode 100644 index 6a2e173273..0000000000 --- a/crates/node/diagnostics.js +++ /dev/null @@ -1,123 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -'use strict'; -const lib = require('./index.js'); -const queryContract = require('./diagnostics_query_contract.js'); - -/** Reader failure with a stable machine-readable reason. */ -class DiagnosticsError extends Error { - constructor(reason) { - super(reason); - this.name = 'DiagnosticsError'; - this.reason = reason; - } -} -const reasons = new Set([ - 'not_initialized', - 'fork_requires_exec', - 'configuration_conflict', - 'disabled', - 'busy', - 'query_disabled', - 'unsupported_schema', - 'internal', - 'invalid_cursor', - 'snapshot_capacity', - 'allocation_failed', - 'invalid_limits', - 'invalid_query', - 'invalid_snapshot', - 'invalid_metadata', - 'numeric_out_of_range', -]); -function call(operation) { - try { - return operation(); - } catch (error) { - if (error instanceof DiagnosticsError) throw error; - throw new DiagnosticsError(reasons.has(error.message) ? error.message : 'unavailable'); - } -} -function queryJson(query = {}) { - let size = 0; - let nodes = 0; - function normalize(value, key = '', depth = 0) { - if (++nodes > queryContract.MAX_QUERY_NODES || depth > queryContract.MAX_QUERY_DEPTH) - throw new DiagnosticsError('invalid_query'); - if (value !== null && typeof value === 'object') { - if (Array.isArray(value)) throw new DiagnosticsError('invalid_query'); - const out = Object.create(null); - let fields = 0; - for (const field in value) { - if (!Object.hasOwn(value, field)) continue; - if ( - ++fields > queryContract.MAX_QUERY_OBJECT_FIELDS || - Buffer.byteLength(field) > queryContract.MAX_QUERY_FIELD_NAME_BYTES - ) - throw new DiagnosticsError('invalid_query'); - size += Buffer.byteLength(field); - if (size > queryContract.MAX_QUERY_BYTES) throw new DiagnosticsError('invalid_query'); - const wire = field.replace(/[A-Z]/g, (letter) => '_' + letter.toLowerCase()); - if (Object.hasOwn(out, wire)) throw new DiagnosticsError('invalid_query'); - out[wire] = normalize(value[field], wire, depth + 1); - } - return out; - } - return normalizeScalar(value, key); - } - function normalizeScalar(value, key) { - if (queryContract.NUMERIC_QUERY_FIELDS.has(key) && typeof value === 'bigint') { - if (value < 0n || value > 18446744073709551615n) throw new DiagnosticsError('invalid_query'); - return value.toString(); - } - if (queryContract.NUMERIC_QUERY_FIELDS.has(key) && typeof value === 'number') - throw new DiagnosticsError('invalid_query'); - if (typeof value === 'string') { - size += Buffer.byteLength(value); - if (size > queryContract.MAX_QUERY_BYTES) throw new DiagnosticsError('invalid_query'); - } else if ( - value !== null && - typeof value !== 'boolean' && - !(typeof value === 'number' && Number.isSafeInteger(value)) - ) - throw new DiagnosticsError('invalid_query'); - return value; - } - const encoded = JSON.stringify(normalize(query)); - if (Buffer.byteLength(encoded) > queryContract.MAX_QUERY_BYTES) throw new DiagnosticsError('invalid_query'); - return encoded; -} - -/** Inspection authority. It does not initialize or enable diagnostics. */ -class DiagnosticsReader { - #native; - constructor(native) { - this.#native = native; - } - /** Return one camelCase page with every diagnostic u64 represented as bigint. */ - snapshot(query) { - return call(() => this.#native.snapshot(queryJson(query))); - } - /** Return portable canonical JSON (snake_case, decimal-string integers). */ - snapshotJson(query) { - return call(() => this.#native.snapshotJson(queryJson(query))); - } - /** Return one camelCase memory page with every diagnostic u64 represented as bigint. */ - inspectMemory(query) { - return call(() => this.#native.inspectMemory(queryJson(query))); - } - /** Return portable canonical JSON (snake_case, decimal-string integers). */ - inspectMemoryJson(query) { - return call(() => this.#native.inspectMemoryJson(queryJson(query))); - } - /** Return separate core/copy budgets, occupancy, and logging coverage. */ - info() { - return call(() => this.#native.info()); - } -} -/** Obtain a reader without starting the service; may throw not_initialized. */ -function reader() { - return call(() => new DiagnosticsReader(lib.diagnosticsReader())); -} -module.exports = { DiagnosticsError, DiagnosticsReader, reader }; diff --git a/crates/node/diagnostics_query_contract.js b/crates/node/diagnostics_query_contract.js deleted file mode 100644 index 85ec672020..0000000000 --- a/crates/node/diagnostics_query_contract.js +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -'use strict'; - -const lib = require('./index.js'); -const core = JSON.parse(lib._diagnosticsQueryContract()); - -// These traversal caps bound the pre-serialization object walk in this adapter. -module.exports = Object.freeze({ - MAX_QUERY_BYTES: core.max_query_bytes, - MAX_QUERY_NODES: 64, - MAX_QUERY_DEPTH: 4, - MAX_QUERY_OBJECT_FIELDS: 20, - MAX_QUERY_FIELD_NAME_BYTES: 128, - NUMERIC_QUERY_FIELDS: new Set(core.numeric_query_fields), -}); diff --git a/crates/node/model_pricing.d.ts b/crates/node/model_pricing.d.ts deleted file mode 100644 index f0e971f249..0000000000 --- a/crates/node/model_pricing.d.ts +++ /dev/null @@ -1,124 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -import type { Json, JsonObject } from './index'; -import type { ComponentSpecFor, ConfigReport } from './plugin'; - -/** Plugin kind used by the model pricing component. */ -export declare const PRICING_PLUGIN_KIND = 'pricing'; - -/** Prompt cache write price for a duration in seconds, or the default with no duration. */ -export interface CacheWriteRate { - duration?: number; - per_million: number; -} - -/** Token rates expressed per one million tokens. */ -export interface TokenPricingRates { - input_per_million: number; - output_per_million: number; - cache_read_per_million?: number; - cache_write_rates?: CacheWriteRate[]; -} - -/** Prompt-cache accounting settings for a catalog entry. */ -export interface PromptCachePricing { - read_accounting?: 'included_in_prompt_tokens' | 'separate'; -} - -/** One prompt-token threshold tier. */ -export interface TokenRateTier { - min_prompt_tokens?: number; - max_prompt_tokens?: number; - rates: TokenPricingRates; -} - -/** Threshold-based token rate schedule. */ -export interface PromptTokenThresholdRateSchedule { - type: 'prompt_token_threshold'; - applies_to?: 'full_request'; - tiers: TokenRateTier[]; -} - -/** One model pricing catalog entry. */ -export interface ModelPricing { - provider: string; - model_id: string; - aliases?: string[]; - currency?: string; - unit?: 'per_token' | 'per_request' | 'per_second' | 'gpu_hour'; - rates?: TokenPricingRates; - rate_schedule?: PromptTokenThresholdRateSchedule | JsonObject; - prompt_cache: PromptCachePricing; - pricing_as_of: string; - pricing_source: string; -} - -/** Inline model pricing catalog payload. */ -export interface PricingCatalog { - version?: number; - entries: Array; -} - -/** Inline model pricing source config. */ -export interface InlineSource { - type: 'inline'; - catalog: PricingCatalog | JsonObject; -} - -/** File-backed model pricing source config. */ -export interface FileSource { - type: 'file'; - path: string; -} - -/** Pricing source config. */ -export type PricingSource = InlineSource | FileSource | JsonObject; - -/** Canonical model pricing plugin config. */ -export interface PricingConfig { - sources?: PricingSource[]; -} - -/** Create a default model pricing component config. */ -export declare function defaultConfig(): PricingConfig; -/** Create per-token model pricing rates with defaults applied. */ -export declare function tokenRates(config?: Partial): TokenPricingRates; -/** Create prompt-cache accounting settings with defaults applied. */ -export declare function promptCache(config?: Partial): PromptCachePricing; -/** Create one prompt-token threshold rate tier. */ -export declare function tokenRateTier( - rates: TokenPricingRates, - config?: Omit, 'rates'>, -): TokenRateTier; -/** Create a prompt-token threshold rate schedule. */ -export declare function promptTokenThresholdRateSchedule( - tiers?: TokenRateTier[], - config?: Omit, 'type' | 'tiers'>, -): PromptTokenThresholdRateSchedule; -/** Create one model pricing catalog entry with defaults applied. */ -export declare function catalogEntry(config: Omit & Partial): ModelPricing; -/** Create an inline model pricing catalog payload. */ -export declare function inlineCatalog( - entries?: Array, - config?: Omit, 'entries'>, -): PricingCatalog; -/** Create an inline model pricing source. */ -export declare function inlineSource(catalog: PricingCatalog | JsonObject): InlineSource; -/** Create a file-backed model pricing source. */ -export declare function fileSource(path: string): FileSource; -/** Create a model pricing config from ordered sources. */ -export declare function pricingConfig(sources?: PricingSource[]): PricingConfig; -/** Wrap model pricing config as a top-level plugin component. */ -export declare function ComponentSpec( - config: PricingConfig, - options?: { - enabled?: boolean; - }, -): ComponentSpecFor<'pricing'>; -/** Validate a model pricing config document without activating it. */ -export declare function validateConfig(config: PricingConfig | JsonObject): ConfigReport; -/** Validate an untyped model pricing document and return it as a config; throws on error diagnostics. */ -export declare function parseConfig(document: JsonObject): PricingConfig; - -export type { Json }; diff --git a/crates/node/model_pricing.js b/crates/node/model_pricing.js deleted file mode 100644 index d73eb30ba5..0000000000 --- a/crates/node/model_pricing.js +++ /dev/null @@ -1,205 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -'use strict'; - -const plugin = require('./plugin.js'); - -const PRICING_PLUGIN_KIND = 'pricing'; - -/** - * Create a default model pricing component config. - * - * @returns {object} The minimal model pricing config with no sources. - */ -function defaultConfig() { - return { - sources: [], - }; -} - -/** - * Create per-token model pricing rates with defaults applied. - * - * @param {object} [config={}] - Partial token-rate fields to override. - * @returns {object} A normalized token model pricing rates object. - */ -function tokenRates(config = {}) { - return { - input_per_million: 0, - output_per_million: 0, - ...config, - }; -} - -/** - * Create prompt-cache accounting settings with defaults applied. - * - * @param {object} [config={}] - Partial prompt-cache settings to override. - * @returns {object} A normalized prompt-cache config object. - */ -function promptCache(config = {}) { - return { - read_accounting: 'included_in_prompt_tokens', - ...config, - }; -} - -/** - * Create one prompt-token threshold rate tier. - * - * @param {object} rates - Token rates selected by this tier. - * @param {object} [config={}] - Optional threshold fields to include. - * @returns {object} A normalized token rate tier object. - */ -function tokenRateTier(rates, config = {}) { - return { - ...config, - rates, - }; -} - -/** - * Create a prompt-token threshold rate schedule. - * - * @param {object[]} [tiers=[]] - Ordered threshold tiers. - * @param {object} [config={}] - Optional schedule fields to override. - * @returns {object} A normalized rate schedule object. - */ -function promptTokenThresholdRateSchedule(tiers = [], config = {}) { - return { - type: 'prompt_token_threshold', - applies_to: 'full_request', - tiers, - ...config, - }; -} - -/** - * Create one model pricing catalog entry with defaults applied. - * - * @param {object} config - Required and optional model pricing fields. - * @returns {object} A normalized model pricing entry. - */ -function catalogEntry(config) { - const { prompt_cache: promptCacheConfig, ...entryConfig } = config; - return { - aliases: [], - currency: 'USD', - unit: 'per_token', - ...entryConfig, - prompt_cache: promptCache(promptCacheConfig), - }; -} - -/** - * Create an inline model pricing catalog payload. - * - * @param {object[]} [entries=[]] - Pricing catalog entries. - * @param {object} [config={}] - Optional catalog fields to override. - * @returns {object} A normalized model pricing catalog object. - */ -function inlineCatalog(entries = [], config = {}) { - return { - version: 1, - entries, - ...config, - }; -} - -/** - * Create an inline model pricing source. - * - * @param {object} catalog - Pricing catalog payload. - * @returns {object} A normalized inline source config. - */ -function inlineSource(catalog) { - return { - type: 'inline', - catalog, - }; -} - -/** - * Create a file-backed model pricing source. - * - * @param {string} path - JSON catalog file path. - * @returns {object} A normalized file source config. - */ -function fileSource(path) { - return { - type: 'file', - path, - }; -} - -/** - * Create a model pricing config from ordered sources. - * - * @param {object[]} [sources=[]] - Pricing sources in precedence order. - * @returns {object} A normalized model pricing config object. - */ -function pricingConfig(sources = []) { - return { - sources, - }; -} - -/** - * Wrap model pricing config as a top-level plugin component. - * - * @param {object} config - Model pricing component configuration document. - * @param {{ enabled?: boolean }} [options={}] - Optional component-level flags. - * @returns {object} A plugin component spec for the model pricing plugin. - */ -function ComponentSpec(config, { enabled = true } = {}) { - return plugin.ComponentSpec(PRICING_PLUGIN_KIND, config, { - enabled, - }); -} - -/** - * Validate a model pricing config document without activating it. - * - * @param {object} config - Model pricing component configuration document. - * @returns {object} A structured validation report with diagnostics. - */ -function validateConfig(config) { - return plugin.validateExact({ - version: 1, - components: [ComponentSpec(config)], - }).config; -} - -/** - * Validate an untyped model pricing document and return it as a config. - * - * @param {object} document - Model pricing component configuration document. - * @returns {object} The same document once validation reports no errors. - */ -function parseConfig(document) { - const errors = validateConfig(document).diagnostics.filter((diagnostic) => diagnostic.level === 'error'); - if (errors.length > 0) { - throw new Error(`invalid model pricing config: ${errors.map((diagnostic) => diagnostic.message).join('; ')}`, { - cause: errors, - }); - } - return document; -} - -module.exports = { - PRICING_PLUGIN_KIND, - defaultConfig, - tokenRates, - promptCache, - tokenRateTier, - promptTokenThresholdRateSchedule, - catalogEntry, - inlineCatalog, - inlineSource, - fileSource, - pricingConfig, - ComponentSpec, - validateConfig, - parseConfig, -}; diff --git a/crates/node/observability.d.ts b/crates/node/observability.d.ts deleted file mode 100644 index 2717144bed..0000000000 --- a/crates/node/observability.d.ts +++ /dev/null @@ -1,226 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -import type { Json } from './index'; -import type { ConfigPolicy, ConfigDiagnostic, ConfigReport } from './plugin'; - -export { ConfigPolicy, ConfigDiagnostic, ConfigReport }; - -export interface AtofConfig { - enabled?: boolean; - sinks?: AtofSinkConfig[]; -} - -export type AtofSinkConfig = AtofFileSinkConfig | AtofStreamSinkConfig; - -export interface AtofFileSinkConfig { - type: 'file'; - output_directory?: string; - filename?: string; - mode?: 'append' | 'overwrite' | string; -} - -export interface AtofStreamSinkConfig { - type: 'stream'; - url: string; - transport?: 'http_post' | 'websocket' | 'ndjson' | string; - headers?: Record; - header_env?: Record; - header_file?: Record; - timeout_millis?: number; - field_name_policy?: 'preserve' | 'replace_dots' | string; - name?: string; -} - -/** @deprecated Use AtofStreamSinkConfig. */ -export type AtofEndpointConfig = AtofStreamSinkConfig; - -export interface S3StorageConfig { - type: 's3'; - bucket: string; - key_prefix?: string; - access_key_id?: string; - secret_access_key_var?: string; - session_token_var?: string; - region?: string; - endpoint_url?: string; - allow_http?: boolean; -} - -export interface HttpStorageConfig { - type: 'http'; - endpoint: string; - headers?: Record; - header_env?: Record; - header_file?: Record; - timeout_millis?: number; -} - -export interface AtifConfig { - enabled?: boolean; - agent_name?: string; - agent_version?: string; - model_name?: string; - tool_definitions?: Record[]; - extra?: Record; - output_directory?: string; - filename_template?: string; - storage?: S3StorageConfig | HttpStorageConfig | Array; - /** Keep raw events in `extra.observed_events`. Defaults to `true`. */ - include_observed_events?: boolean; - /** Keep full provider requests in trajectory steps. Defaults to `true`. */ - include_llm_requests?: boolean; -} - -export interface OpenTelemetryTlsConfig { - /** PEM CA bundle that replaces the system's trusted CAs. */ - certificate?: string; - /** PEM client certificate chain. Also set `private_key`. */ - client_certificate?: string; - /** PEM private key. Also set `client_certificate`. */ - private_key?: string; -} - -export interface OpenTelemetryEndpointConfig { - type: 'full' | 'gen_ai' | 'openinference'; - endpoint: string; - tls?: OpenTelemetryTlsConfig; - mark_projection?: 'inherit' | 'event' | 'tool'; - mark_exclude_names?: string[]; - attribute_mappings?: Array<{ key: string; alias: string }>; - promote_metadata_prefixes?: string[]; - promote_resource_metadata_prefixes?: string[]; - transport?: 'http_binary' | 'grpc'; - headers?: Record; - header_env?: Record; - header_file?: Record; - resource_attributes?: Record; - service_name?: string; - service_namespace?: string; - service_version?: string; - instrumentation_scope?: string; - timeout_millis?: number; - max_queue_size?: number; - max_export_batch_size?: number; - scheduled_delay_millis?: number; - completed_span_context_ttl_millis?: number; -} - -export interface OpenTelemetrySignalEndpointConfig { - endpoint: string; - tls?: OpenTelemetryTlsConfig; - transport?: 'http_binary' | 'grpc'; - headers?: Record; - header_env?: Record; - header_file?: Record; - resource_attributes?: Record; - service_name?: string; - service_namespace?: string; - service_version?: string; - instrumentation_scope?: string; - timeout_millis?: number; - /** Literal root-scope metadata prefixes promoted to resource attributes. */ - promote_resource_metadata_prefixes?: string[]; -} - -export interface OpenTelemetryLogSectionConfig { - enabled?: boolean; - endpoints?: OpenTelemetrySignalEndpointConfig[]; - minimum_severity?: 'trace' | 'debug' | 'info' | 'warn' | 'warning' | 'error'; - max_queue_size?: number; - max_export_batch_size?: number; - scheduled_delay_millis?: number; -} - -export interface OpenTelemetryMetricSectionConfig { - enabled?: boolean; - endpoints?: OpenTelemetrySignalEndpointConfig[]; - export_interval_millis?: number; - temporality?: 'cumulative' | 'delta' | 'low_memory'; - max_instruments?: number; - cardinality_limit?: number; -} - -export interface OpenTelemetryFileSinkConfig { - type?: 'full' | 'gen_ai' | 'openinference'; - output_directory: string; - filename?: string; - /** - * `json_lines` follows the OpenTelemetry Protocol File Exporter - * specification: one OTLP/JSON record per line. `proto` writes each record - * length-delimited. - */ - format?: 'json_lines' | 'proto'; - mode?: 'append' | 'overwrite'; - mark_projection?: 'inherit' | 'event' | 'tool'; - mark_exclude_names?: string[]; - attribute_mappings?: Array<{ key: string; alias: string }>; - promote_metadata_prefixes?: string[]; - promote_resource_metadata_prefixes?: string[]; - resource_attributes?: Record; - service_name?: string; - service_namespace?: string; - service_version?: string; - instrumentation_scope?: string; - max_queue_size?: number; - max_export_batch_size?: number; - scheduled_delay_millis?: number; - completed_span_context_ttl_millis?: number; -} - -export interface OpenTelemetrySectionConfig { - enabled?: boolean; - endpoints?: OpenTelemetryEndpointConfig[]; - file_sinks?: OpenTelemetryFileSinkConfig[]; - logs?: OpenTelemetryLogSectionConfig; - metrics?: OpenTelemetryMetricSectionConfig; -} - -export interface Config { - version?: number; - atof?: AtofConfig; - atif?: AtifConfig; - opentelemetry?: OpenTelemetrySectionConfig; - policy?: ConfigPolicy; - /** Retain complete sanitized request data on every LLM start event. */ - enable_full_payloads?: boolean; - /** Non-loopback hosts allowed unencrypted HTTP or WebSocket exports, including headers. */ - insecure_http_hosts?: string[]; -} - -export interface ComponentSpec { - kind: 'observability'; - enabled?: boolean; - config: Config; -} - -/** Top-level plugin kind used by the built-in observability component. */ -export declare const OBSERVABILITY_PLUGIN_KIND: 'observability'; -/** Create a default observability component config. */ -export declare function defaultConfig(): Config; -/** Create filesystem-backed Agent Trajectory Observability Format (ATOF) JSONL settings with defaults applied. */ -export declare function atofConfig(config?: AtofConfig): AtofConfig; -/** Create per-agent Agent Trajectory Interchange Format (ATIF) trajectory settings with defaults applied. */ -export declare function atifConfig(config?: AtifConfig): AtifConfig; -/** Create one typed OpenTelemetry endpoint. */ -export declare function openTelemetryEndpoint(config: OpenTelemetryEndpointConfig): OpenTelemetryEndpointConfig; -export declare function openTelemetryFileSink(config: OpenTelemetryFileSinkConfig): OpenTelemetryFileSinkConfig; -/** Create one signal-specific OpenTelemetry endpoint for logs or metrics. */ -export declare function openTelemetrySignalEndpoint( - config: OpenTelemetrySignalEndpointConfig, -): OpenTelemetrySignalEndpointConfig; -/** Create OTLP log pipeline settings with defaults applied. */ -export declare function openTelemetryLogConfig(config?: OpenTelemetryLogSectionConfig): OpenTelemetryLogSectionConfig; -/** Create OTLP metric pipeline settings with defaults applied. */ -export declare function openTelemetryMetricConfig( - config?: OpenTelemetryMetricSectionConfig, -): OpenTelemetryMetricSectionConfig; -/** Create multi-endpoint OpenTelemetry settings. */ -export declare function openTelemetryConfig(config?: OpenTelemetrySectionConfig): OpenTelemetrySectionConfig; -/** Wrap observability config as a top-level plugin component. */ -export declare function ComponentSpec( - config: Config, - options?: { - enabled?: boolean; - }, -): ComponentSpec; diff --git a/crates/node/observability.js b/crates/node/observability.js deleted file mode 100644 index dcc55b3df2..0000000000 --- a/crates/node/observability.js +++ /dev/null @@ -1,220 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -'use strict'; - -const plugin = require('./plugin.js'); - -const OBSERVABILITY_PLUGIN_KIND = 'observability'; -const DEFAULT_COMPLETED_SPAN_CONTEXT_TTL_MILLIS = 60_000; - -/** - * Create a default observability component config. - * - * @returns {object} The minimal observability config with schema version 4. - */ -function defaultConfig() { - return { - version: 4, - }; -} - -/** - * Create multi-sink ATOF settings with defaults applied. - * - * @param {object} [config={}] - Partial ATOF settings to override. - * @returns {object} A normalized ATOF config object. - */ -function atofConfig(config = {}) { - return { - enabled: false, - ...config, - }; -} - -/** - * Create per-agent ATIF trajectory settings with defaults applied. - * - * @param {object} [config={}] - Partial ATIF settings to override. - * @returns {object} A normalized ATIF config object. - */ -function atifConfig(config = {}) { - return { - enabled: false, - agent_name: 'NeMo Relay', - model_name: 'unknown', - filename_template: 'nemo-relay-atif-{session_id}.json', - include_observed_events: true, - include_llm_requests: true, - ...config, - }; -} - -/** - * Create one typed OpenTelemetry endpoint. - * - * @param {object} config - Endpoint settings including required `type` and `endpoint`. - * @returns {object} A normalized endpoint config object. - */ -function openTelemetryEndpoint(config) { - if (!config || typeof config !== 'object') { - throw new TypeError('OpenTelemetry endpoint config is required'); - } - if (!['full', 'gen_ai', 'openinference'].includes(config.type)) { - throw new TypeError('OpenTelemetry endpoint type must be "full", "gen_ai", or "openinference"'); - } - if (typeof config.endpoint !== 'string' || config.endpoint.trim() === '') { - throw new TypeError('OpenTelemetry endpoint must be a nonblank string'); - } - return { - transport: 'http_binary', - service_name: 'unknown_service', - instrumentation_scope: 'opentelemetry', - timeout_millis: 3000, - completed_span_context_ttl_millis: DEFAULT_COMPLETED_SPAN_CONTEXT_TTL_MILLIS, - headers: {}, - header_env: {}, - resource_attributes: {}, - promote_metadata_prefixes: [], - promote_resource_metadata_prefixes: [], - ...config, - }; -} - -/** - * Create one local file destination for projected OTLP spans. - * - * `json_lines` is the OpenTelemetry file-exporter specification's serialization: - * one OTLP/JSON record per line. `proto` writes each record length-delimited. - * - * @param {object} config - File sink settings including required `output_directory`. - * @returns {object} A normalized OpenTelemetry file sink. - */ -function openTelemetryFileSink(config) { - if (!config || typeof config !== 'object') { - throw new TypeError('OpenTelemetry file sink config is required'); - } - if (typeof config.output_directory !== 'string' || config.output_directory.trim() === '') { - throw new TypeError('OpenTelemetry file sink output_directory must be a nonblank string'); - } - if (config.format !== undefined && !['json_lines', 'proto'].includes(config.format)) { - throw new TypeError('OpenTelemetry file sink format must be "json_lines" or "proto"'); - } - if (config.mode !== undefined && !['append', 'overwrite'].includes(config.mode)) { - throw new TypeError('OpenTelemetry file sink mode must be "append" or "overwrite"'); - } - return { - type: 'full', - format: 'json_lines', - mode: 'overwrite', - service_name: 'unknown_service', - instrumentation_scope: 'opentelemetry', - completed_span_context_ttl_millis: DEFAULT_COMPLETED_SPAN_CONTEXT_TTL_MILLIS, - resource_attributes: {}, - promote_metadata_prefixes: [], - promote_resource_metadata_prefixes: [], - ...config, - }; -} - -/** - * Create one signal-specific OpenTelemetry endpoint for logs or metrics. - * - * @param {object} config - Endpoint settings including required `endpoint`. - * @returns {object} A normalized signal endpoint config object. - */ -function openTelemetrySignalEndpoint(config) { - if (!config || typeof config !== 'object') { - throw new TypeError('OpenTelemetry signal endpoint config is required'); - } - if (typeof config.endpoint !== 'string' || config.endpoint.trim() === '') { - throw new TypeError('OpenTelemetry signal endpoint must be a nonblank string'); - } - return { - transport: 'http_binary', - headers: {}, - header_env: {}, - resource_attributes: {}, - service_name: 'unknown_service', - instrumentation_scope: 'opentelemetry', - timeout_millis: 3000, - ...config, - }; -} - -/** - * Create OTLP log pipeline settings with defaults applied. - * - * @param {object} [config={}] - Partial log pipeline settings. - * @returns {object} A normalized OpenTelemetry log section. - */ -function openTelemetryLogConfig(config = {}) { - return { - enabled: false, - minimum_severity: 'info', - max_queue_size: 2048, - max_export_batch_size: 512, - scheduled_delay_millis: 1000, - completed_span_context_ttl_millis: DEFAULT_COMPLETED_SPAN_CONTEXT_TTL_MILLIS, - ...config, - }; -} - -/** - * Create OTLP metric pipeline settings with defaults applied. - * - * @param {object} [config={}] - Partial metric pipeline settings. - * @returns {object} A normalized OpenTelemetry metric section. - */ -function openTelemetryMetricConfig(config = {}) { - return { - enabled: false, - export_interval_millis: 60000, - temporality: 'cumulative', - max_instruments: 256, - cardinality_limit: 2000, - ...config, - }; -} - -/** - * Create multi-endpoint OpenTelemetry settings. - * - * @param {object} [config={}] - Partial section settings. - * @returns {object} A normalized OpenTelemetry section. - */ -function openTelemetryConfig(config = {}) { - return { - enabled: false, - endpoints: [], - file_sinks: [], - ...config, - }; -} - -/** - * Wrap observability config as a top-level plugin component. - * - * @param {object} config - Observability component configuration document. - * @param {{ enabled?: boolean }} [options={}] - Optional component-level flags. - * @returns {object} A plugin component spec for the observability plugin. - */ -function ComponentSpec(config, { enabled = true } = {}) { - return plugin.ComponentSpec(OBSERVABILITY_PLUGIN_KIND, config, { - enabled, - }); -} - -module.exports = { - OBSERVABILITY_PLUGIN_KIND, - defaultConfig, - atofConfig, - atifConfig, - openTelemetryEndpoint, - openTelemetryFileSink, - openTelemetrySignalEndpoint, - openTelemetryLogConfig, - openTelemetryMetricConfig, - openTelemetryConfig, - ComponentSpec, -}; diff --git a/crates/node/package.json b/crates/node/package.json index 15482ca82d..4c55d43937 100644 --- a/crates/node/package.json +++ b/crates/node/package.json @@ -22,40 +22,65 @@ "directory": "crates/node" }, "author": "NVIDIA Corporation & Affiliates", - "main": "index.js", - "types": "index.d.ts", + "main": "dist/index.js", + "types": "dist/index.d.ts", "exports": { ".": { - "types": "./index.d.ts", - "default": "./index.js" + "types": "./dist/index.d.ts", + "default": "./dist/index.js" }, "./typed": { - "types": "./typed.d.ts", - "default": "./typed.js" + "types": "./dist/typed.d.ts", + "default": "./dist/typed.js" }, "./plugin": { - "types": "./plugin.d.ts", - "default": "./plugin.js" + "types": "./dist/plugin.d.ts", + "default": "./dist/plugin.js" }, "./adaptive": { - "types": "./adaptive.d.ts", - "default": "./adaptive.js" + "types": "./dist/adaptive.d.ts", + "default": "./dist/adaptive.js" }, "./observability": { - "types": "./observability.d.ts", - "default": "./observability.js" + "types": "./dist/observability.d.ts", + "default": "./dist/observability.js" }, "./pii_redaction": { - "types": "./pii_redaction.d.ts", - "default": "./pii_redaction.js" + "types": "./dist/pii_redaction.d.ts", + "default": "./dist/pii_redaction.js" }, "./model_pricing": { - "types": "./model_pricing.d.ts", - "default": "./model_pricing.js" + "types": "./dist/model_pricing.d.ts", + "default": "./dist/model_pricing.js" }, "./diagnostics": { - "types": "./diagnostics.d.ts", - "default": "./diagnostics.js" + "types": "./dist/diagnostics.d.ts", + "default": "./dist/diagnostics.js" + } + }, + "typesVersions": { + "*": { + "typed": [ + "dist/typed.d.ts" + ], + "plugin": [ + "dist/plugin.d.ts" + ], + "adaptive": [ + "dist/adaptive.d.ts" + ], + "observability": [ + "dist/observability.d.ts" + ], + "pii_redaction": [ + "dist/pii_redaction.d.ts" + ], + "model_pricing": [ + "dist/model_pricing.d.ts" + ], + "diagnostics": [ + "dist/diagnostics.d.ts" + ] } }, "engines": { @@ -65,6 +90,7 @@ "binaryName": "nemo-relay", "constEnum": false, "runtimeStringEnum": true, + "dtsHeaderFile": "binding-types.d.ts", "targets": [ "x86_64-unknown-linux-gnu", "aarch64-unknown-linux-gnu", @@ -80,8 +106,8 @@ "build-debug": "node scripts/build.mjs", "check:docstrings": "node ../../scripts/lint/check_public_docstrings.mjs node", "check:docstrings:all": "node ../../scripts/lint/check_public_docstrings.mjs all", - "format": "prettier --write \"*.{js,ts,d.ts}\" \"tests/**/*.{mjs,cjs,js,mts,cts,ts,tsx}\"", - "format:check": "prettier --check \"*.{js,ts,d.ts}\" \"tests/**/*.{mjs,cjs,js,mts,cts,ts,tsx}\"", + "format": "prettier --write \"binding-types.d.ts\" \"{scripts,ts,tests}/**/*.{mjs,cjs,js,mts,cts,ts,tsx}\"", + "format:check": "prettier --check \"binding-types.d.ts\" \"{scripts,ts,tests}/**/*.{mjs,cjs,js,mts,cts,ts,tsx}\"", "precommit:format": "node ../../scripts/lint/prettier-precommit.mjs", "precoverage": "npm run build-debug -- --features __skip-implicit-config", "pretest": "npm run build-debug -- --features __skip-implicit-config", @@ -90,12 +116,13 @@ }, "devDependencies": { "@napi-rs/cli": "^3.10.9", + "@types/node": "^24.0.0", "c8": "^11.0.0", "prettier": "^3.8.2", "typescript": "^5.9.3" }, "peerDependencies": { - "typescript": ">=5.2" + "typescript": ">=5.7" }, "peerDependenciesMeta": { "typescript": { diff --git a/crates/node/pii_redaction.d.ts b/crates/node/pii_redaction.d.ts deleted file mode 100644 index ed67c85f4c..0000000000 --- a/crates/node/pii_redaction.d.ts +++ /dev/null @@ -1,72 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -import type { ConfigDiagnostic, ConfigReport } from './plugin.js'; - -export { ConfigDiagnostic, ConfigReport }; - -export interface ConfigPolicy { - unknown_component?: 'ignore' | 'warn' | 'error' | string; - unknown_field?: 'ignore' | 'warn' | 'error' | string; - unsupported_value?: 'ignore' | 'warn' | 'error' | string; -} - -export interface BuiltinConfig { - preset?: 'trajectory_context'; - action?: 'remove' | 'redact' | 'regex_replace' | 'hash' | 'mask' | string; - target_paths?: string[]; - target_path_globs?: string[]; - pattern?: string; - detector?: string; - replacement?: string; - mask_char?: string; - unmasked_prefix?: number; - unmasked_suffix?: number; - custom_mark_payload_policy?: 'preserve' | 'redact_all_leaves'; - metric_string_attribute_allowlist?: Record; -} - -export interface LocalModelConfig { - backend?: string; - model_id?: string; - detector_profile?: string; - allow_network?: boolean; - max_latency_ms?: number; -} - -export interface Config { - version?: number; - mode?: 'builtin' | 'local_model' | string; - input?: boolean; - output?: boolean; - tool_input?: boolean; - tool_output?: boolean; - mark?: boolean; - priority?: number; - codec?: 'openai_chat' | 'openai_responses' | 'anthropic_messages' | 'oci_genai' | 'gemini_generate_content' | string; - builtin?: BuiltinConfig; - local?: LocalModelConfig; - policy?: ConfigPolicy; -} - -export interface ComponentSpec { - kind: 'pii_redaction'; - enabled?: boolean; - config: Config; -} - -/** Top-level plugin kind used by the built-in PII redaction component. */ -export declare const PII_REDACTION_PLUGIN_KIND: 'pii_redaction'; -/** Create a default PII redaction component config. */ -export declare function defaultConfig(): Config; -/** Create deterministic built-in redaction backend settings with defaults applied. */ -export declare function builtinConfig(config?: BuiltinConfig): BuiltinConfig; -/** Create future local-model backend settings with defaults applied. */ -export declare function localModelConfig(config?: LocalModelConfig): LocalModelConfig; -/** Wrap PII redaction config as a top-level plugin component. */ -export declare function ComponentSpec( - config: Config, - options?: { - enabled?: boolean; - }, -): ComponentSpec; diff --git a/crates/node/pii_redaction.js b/crates/node/pii_redaction.js deleted file mode 100644 index 970a33b4f3..0000000000 --- a/crates/node/pii_redaction.js +++ /dev/null @@ -1,72 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -'use strict'; - -const plugin = require('./plugin.js'); - -const PII_REDACTION_PLUGIN_KIND = 'pii_redaction'; - -/** - * Create a default PII redaction component config. - * - * @returns {object} The minimal PII redaction config with schema version 1. - */ -function defaultConfig() { - return { - version: 1, - mode: 'builtin', - input: true, - output: true, - tool_input: true, - tool_output: true, - mark: true, - priority: 100, - }; -} - -/** - * Create deterministic built-in redaction backend settings with defaults applied. - * - * @param {object} [config={}] - Partial built-in settings to override. - * @returns {object} A normalized built-in backend config object. - */ -function builtinConfig(config = {}) { - return { - ...(config.preset === 'trajectory_context' ? {} : { action: 'remove' }), - ...config, - }; -} - -/** - * Create future local-model backend settings with defaults applied. - * - * @param {object} [config={}] - Partial local-model settings to override. - * @returns {object} A normalized local-model backend config object. - */ -function localModelConfig(config = {}) { - return { - ...config, - }; -} - -/** - * Wrap PII redaction config as a top-level plugin component. - * - * @param {object} config - PII redaction component configuration document. - * @param {{ enabled?: boolean }} [options={}] - Optional component-level flags. - * @returns {object} A plugin component spec for the PII redaction plugin. - */ -function ComponentSpec(config, { enabled = true } = {}) { - return plugin.ComponentSpec(PII_REDACTION_PLUGIN_KIND, config, { - enabled, - }); -} - -module.exports = { - PII_REDACTION_PLUGIN_KIND, - defaultConfig, - builtinConfig, - localModelConfig, - ComponentSpec, -}; diff --git a/crates/node/plugin.d.ts b/crates/node/plugin.d.ts deleted file mode 100644 index 0ab14d0fc9..0000000000 --- a/crates/node/plugin.d.ts +++ /dev/null @@ -1,291 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -/// - -import type { AdaptivePluginConfig } from './adaptive'; -import type { - ConfigDiagnostic, - Json, - JsonObject, - JsonObjectCompatible, - PluginContext, - ResourceMetricsConfig, -} from './index'; -import type { PricingConfig } from './model_pricing'; -import type { Config as ObservabilityConfig } from './observability'; -import type { Config as PiiRedactionConfig } from './pii_redaction'; - -export type { - ConfigDiagnostic, - EventMetadata, - EventMetadataScalar, - EventMetadataValue, - LlmCodecIdentity, - LlmExecutionContext, - LlmOptimizationContribution, - LlmOptimizationDataSchema, - LlmOptimizationModel, - LlmOptimizationModelTransition, - LlmOptimizationTokenImpact, - LlmOptimizationTokens, - LlmRequestInterceptOutcome, - LlmRequestContext, - LlmResponseContext, - LlmSanitizeRequestContext, - LlmSanitizeResponseContext, - PluginContext, - ToolExecutionInterceptOutcome, -} from './index'; - -/** Policy behavior for unsupported configuration. */ -export type UnsupportedBehavior = 'ignore' | 'warn' | 'error'; - -/** Plugin-level policy for unknown or unsupported plugin configuration. */ -export interface ConfigPolicy { - unknown_component?: UnsupportedBehavior; - unknown_field?: UnsupportedBehavior; - unsupported_value?: UnsupportedBehavior; -} - -/** Validation or activation report for a plugin configuration. */ -export interface ConfigReport { - diagnostics: ConfigDiagnostic[]; - runtime_diagnostics?: RuntimeDiagnostic[]; -} - -/** One bounded aggregate of a runtime plugin failure. */ -export interface RuntimeDiagnostic { - code: string; - component: string; - field?: string; - message: string; - session_id?: string; - count: number; -} - -export interface ComponentConfigs { - adaptive: AdaptivePluginConfig; - observability: ObservabilityConfig; - pii_redaction: PiiRedactionConfig; - pricing: PricingConfig; - resource_metrics: ResourceMetricsConfig; -} - -export type ComponentKind = keyof ComponentConfigs; - -export type KnownComponentSpec = { - [K in ComponentKind]: { kind: K; enabled?: boolean; config?: ComponentConfigs[K] }; -}[ComponentKind]; - -declare const openComponent: unique symbol; - -/** Config of a component created with `ComponentSpec` for a kind registered through `registerPlugin`. */ -export type OpenComponentConfig = { - readonly [openComponent]: true; -}; - -/** Component for a kind registered through `registerPlugin`; create one with `ComponentSpec`. */ -export interface OpenComponentSpec { - kind: string; - enabled?: boolean; - config?: C & OpenComponentConfig; -} - -export type ComponentConfigFor = K extends ComponentKind ? ComponentConfigs[K] : JsonObject; - -export type ComponentSpecFor = K extends ComponentKind - ? Extract - : OpenComponentSpec; - -/** One top-level plugin component. */ -export type ComponentSpec = KnownComponentSpec | OpenComponentSpec; - -/** Canonical plugin configuration document. */ -export interface PluginConfig { - version?: number; - components?: ComponentSpec[]; - policy?: ConfigPolicy; - /** Startup-only host service profile; omitted settings use file layers. */ - diagnostics?: { enabled?: boolean; limits?: Record }; -} - -/** Execution lane for a dynamically loaded Relay plugin. */ -export type DynamicPluginKind = 'rust_dynamic' | 'worker'; - -export interface PluginHostActivation extends AsyncDisposable { - /** Validation report produced by the successful activation. */ - readonly report: PluginHostReport; - /** Whether this activation remains open. Failed teardown can be retried. */ - readonly isActive: boolean; - /** Clear callbacks before unloading libraries and workers. Idempotent. */ - close(): Promise; - /** Delegate structured `await using` cleanup to `close()`. */ - [Symbol.asyncDispose](): Promise; -} - -export interface PluginHostReport { - config: ConfigReport; - dynamic_plugins: DynamicPluginValidationReport[]; - /** Existing plugins.toml files that contributed to the resolved configuration. */ - config_paths: string[]; - /** Fully merged plugin configuration with sensitive values redacted. */ - resolved_config: Json; -} - -export type DynamicPluginCheckState = 'unknown' | 'valid' | 'invalid'; - -export interface DynamicPluginValidationStatus { - manifest: DynamicPluginCheckState; - compatibility: DynamicPluginCheckState; - integrity: DynamicPluginCheckState; - environment: DynamicPluginCheckState; - authenticity: DynamicPluginCheckState; - policy_satisfied: DynamicPluginCheckState; - checked_at?: string | null; - message?: string | null; -} - -export interface DynamicPluginFailure { - phase: string; - code: string; - message: string; -} - -export interface DynamicPluginValidationReport { - plugin_id: string; - manifest_ref: string; - kind: DynamicPluginKind; - status: DynamicPluginValidationStatus; - failure?: DynamicPluginFailure | null; - selected: boolean; -} - -/** Plugin callback contract. */ -export interface Plugin { - /** Validate one component-local config object. */ - validate?(pluginConfig: JsonObject): ConfigDiagnostic[] | null | undefined; - /** - * Install middleware and subscribers for one component instance. - * - * Throwing aborts the current initialization and triggers rollback. - */ - register(pluginConfig: JsonObject, context: PluginContext): void; -} - -/** - * Create an empty plugin configuration. - * - * Returns the canonical top-level config shape with `version = 1` and no - * configured components so callers can build a document incrementally before - * validating or activating it. - * - * @returns A new `PluginConfig` object ready for mutation or validation. - * @remarks Mutating the returned object does not affect runtime state until it - * is passed to `initialize`. - */ -export declare function defaultConfig(): PluginConfig; -/** - * Create a plugin component entry for a plugin config document. - * - * Packages a plugin kind, component-local config, and enablement flag into the - * object shape expected by `PluginConfig.components`. - * - * @param kind - Registered plugin kind to reference. - * @param config - Component-local config passed to plugin hooks. - * @param options - Optional component-level flags. - * @returns A `ComponentSpec` ready to insert into a plugin config. - * @remarks Setting `options.enabled = false` preserves the component for - * validation while skipping runtime registration during `initialize`. - */ -export declare function ComponentSpec( - kind: K, - config?: ComponentConfigs[K], - options?: { - enabled?: boolean; - }, -): ComponentSpecFor; -export declare function ComponentSpec = JsonObject>( - kind: K extends ComponentKind ? never : K, - config?: C, - options?: { - enabled?: boolean; - }, -): OpenComponentSpec; -/** - * Initialize the core-owned static and dynamic plugin host. - * - * Resolves an explicit or discovered user file with the system configuration, - * then applies programmatic config and activates one owned lifetime. - * - * @param config - Programmatic configuration. It overrides file values. - * @param additionalPluginsToml - Optional explicit `plugins.toml` layer. A - * missing explicit file is reported as a `plugin.configuration_file_missing` - * warning in the host report. - * @returns An owned activation with the unified host report. - * @remarks Keep the returned activation alive while callbacks may run and call - * `close()` or use `await using` for deterministic teardown. - */ -export declare function initialize(config: PluginConfig, additionalPluginsToml?: string): Promise; -/** - * Validate the plugin host without loading plugin code. - * - * Resolves the same layered configuration and trust policy used by activation - * while leaving the process-wide host lease untouched. - * - * @param config - Programmatic configuration. It overrides file values. - * @param additionalPluginsToml - Optional explicit `plugins.toml` layer. A - * missing explicit file is reported as a `plugin.configuration_file_missing` - * warning in the host report. - * @returns Structured static and dynamic validation report. - * @remarks Validation performs no activation and does not acquire the host lease. - */ -export declare function validate(config: PluginConfig, additionalPluginsToml?: string): PluginHostReport; -/** - * Validate only the supplied static plugin configuration. - * - * Unlike `validate`, this does not discover or merge `plugins.toml` files. - * Use it for component-specific validation when `config` is the complete - * document to check. Host-only `diagnostics` settings are rejected; use - * `validate` to validate those settings. - * - * @param config - Complete static plugin configuration. - * @returns Static validation results with no dynamic plugins. - */ -export declare function validateExact(config: PluginConfig): PluginHostReport; -/** - * List registered plugin kinds. - * - * Returns the plugin kind identifiers currently known to the global registry - * so callers can inspect what can be referenced from plugin configs. - * - * @returns The registered plugin kind names. - * @remarks The list reflects registry state only; it does not indicate whether - * a plugin kind is currently active in the runtime configuration. - */ -export declare function listKinds(): string[]; -/** - * Register a plugin kind with JavaScript validation and registration hooks. - * - * Adapts the higher-level `Plugin` object contract to the native callback - * shape expected by the Node binding. - * - * @param pluginKind - Unique plugin kind identifier to register. - * @param plugin - Plugin implementation with `validate` and `register` hooks. - * @returns Nothing. - * @remarks Omitting `plugin.validate` makes the plugin permissive during - * validation; `plugin.register` still runs later during `initialize`. - */ -export declare function register(pluginKind: string, plugin: Plugin): void; -/** - * Remove a previously registered plugin kind. - * - * Deletes the plugin kind from the registry so future config validation and - * initialization calls can no longer reference it. - * - * @param pluginKind - Registered plugin kind identifier to remove. - * @returns `true` when a plugin kind was removed, otherwise `false`. - * @remarks Active runtime registrations remain until the owning plugin-host - * activation closes. - */ -export declare function deregister(pluginKind: string): boolean; diff --git a/crates/node/plugin.js b/crates/node/plugin.js deleted file mode 100644 index cc6986125e..0000000000 --- a/crates/node/plugin.js +++ /dev/null @@ -1,162 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -'use strict'; - -const { createRequire } = require('node:module'); -const path = require('node:path'); - -const nativeRequire = createRequire(path.join(__dirname, 'index.js')); -const lib = nativeRequire('./index.js'); - -/** - * Create an empty plugin configuration. - * - * Returns the canonical top-level config shape with `version = 1` and no - * configured components so callers can build a document incrementally before - * validating or activating it. - * - * @returns {object} A new plugin config object. - * @remarks Mutating the returned object does not affect runtime state until it - * is passed to `initialize`. - */ -function defaultConfig() { - return { - version: 1, - components: [], - }; -} - -/** - * Create a plugin component entry for a plugin config document. - * - * Packages a plugin kind, component-local config, and enablement flag into the - * object shape expected by `PluginConfig.components`. - * - * @param {string} kind - Registered plugin kind to reference. - * @param {object} [config={}] - Component-local config passed to plugin hooks. - * @param {{ enabled?: boolean }} [options={}] - Optional component-level flags. - * @returns {object} A component spec ready to insert into a plugin config. - * @remarks Setting `enabled` to `false` preserves the component for validation - * while skipping runtime registration during `initialize`. - */ -function ComponentSpec(kind, config = {}, { enabled = true } = {}) { - return { - kind, - enabled, - config, - }; -} - -/** - * Initialize the core-owned static and dynamic plugin host. - * - * Resolves programmatic config with either an explicit or discovered user - * file, then the system configuration, and activates one owned lifetime. - * - * @param {object} config - Lowest-precedence programmatic configuration. - * @param {string} [additionalPluginsToml] - Optional explicit `plugins.toml` layer. - * A missing explicit file is reported as a `plugin.configuration_file_missing` - * warning in the host report. - * @returns {Promise} An owned activation with the unified host report. - * @remarks Keep the returned activation alive while callbacks may run and call - * `close()` or use `await using` for deterministic teardown. - */ -function initialize(config, additionalPluginsToml) { - return lib.initialize(config, additionalPluginsToml); -} - -/** - * Validate the plugin host without loading plugin code. - * - * Resolves the same layered configuration and trust policy used by activation - * while leaving the process-wide host lease untouched. - * - * @param {object} config - Lowest-precedence programmatic configuration. - * @param {string} [additionalPluginsToml] - Optional explicit `plugins.toml` layer. - * A missing explicit file is reported as a `plugin.configuration_file_missing` - * warning in the host report. - * @returns {PluginHostReport} Structured static and dynamic validation report. - * @remarks Validation performs no activation and does not acquire the host lease. - */ -function validate(config, additionalPluginsToml) { - return lib.validate(config, additionalPluginsToml); -} - -/** - * Validate only the supplied static plugin configuration. - * - * Unlike `validate`, this does not discover or merge `plugins.toml` files. - * Use it for component-specific validation when `config` is the complete - * document to check. Host-only `diagnostics` settings are rejected; use - * `validate` to validate those settings. - * - * @param {object} config - Complete static plugin configuration. - * @returns {PluginHostReport} Static validation results with no dynamic plugins. - * @remarks This validates only the supplied document and intentionally skips - * plugin discovery from the filesystem. - */ -function validateExact(config) { - return lib.validateExact(config); -} - -/** - * List registered plugin kinds. - * - * Returns the plugin kind identifiers currently known to the global registry - * so callers can inspect what can be referenced from plugin configs. - * - * @returns {string[]} The registered plugin kind names. - * @remarks The list reflects registry state only; it does not indicate whether - * a plugin kind is currently active in the runtime configuration. - */ -function listKinds() { - return lib.listPluginKinds(); -} - -/** - * Register a plugin kind with JavaScript validation and registration hooks. - * - * Adapts the higher-level `Plugin` object contract to the native callback - * shape expected by the Node binding. - * - * @param {string} pluginKind - Unique plugin kind identifier to register. - * @param {object} plugin - Plugin implementation with `validate` and `register` hooks. - * @returns {void} Nothing. - * @remarks Omitting `plugin.validate` makes the plugin permissive during - * validation; `plugin.register` is still required and runs later during - * `initialize`. - */ -function register(pluginKind, plugin) { - return lib.registerPlugin( - pluginKind, - plugin.validate ? (pluginConfig) => plugin.validate(pluginConfig) : null, - (pluginConfig, context) => plugin.register(pluginConfig, context), - ); -} - -/** - * Remove a previously registered plugin kind. - * - * Deletes the plugin kind from the registry so future config validation and - * initialization calls can no longer reference it. - * - * @param {string} pluginKind - Registered plugin kind identifier to remove. - * @returns {boolean} `true` when a plugin kind was removed, otherwise `false`. - * @remarks Active runtime registrations remain in place until the owning - * plugin-host activation closes. - */ -function deregister(pluginKind) { - return lib.deregisterPlugin(pluginKind); -} - -module.exports = { - defaultConfig, - ComponentSpec, - initialize, - validate, - validateExact, - listKinds, - register, - deregister, -}; diff --git a/crates/node/root-types.d.ts b/crates/node/root-types.d.ts deleted file mode 100644 index e7bdeb958d..0000000000 --- a/crates/node/root-types.d.ts +++ /dev/null @@ -1,660 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -export type JsonPrimitive = string | number | boolean | null; -export type JsonObject = { [Key in string]: JsonValue }; -export type JsonArray = JsonValue[] | readonly JsonValue[]; -export type JsonValue = JsonPrimitive | JsonObject | JsonArray; -export type Json = JsonValue; -export type LlmRequestShape = { - headers: JsonObject; - content: Json; -}; - -type NonJsonValue = - | ((...args: never[]) => unknown) - | (abstract new (...args: never[]) => unknown) - | bigint - | symbol - | undefined; -type BareObject = V extends unknown - ? [object] extends [V] - ? [V] extends [object] - ? [keyof V] extends [never] - ? V - : never - : never - : never - : never; -type Shaped = [Extract | BareObject] extends [never] - ? Mapped - : never; -type JsonProperty = [unknown] extends [V] - ? unknown - : V extends JsonValue - ? V - : V extends NonJsonValue - ? never - : V extends object - ? [BareObject] extends [never] - ? { [K in keyof V]: JsonProperty } - : never - : never; -type JsonObjectProperty = V extends readonly unknown[] ? never : V extends object ? JsonProperty : never; -type LlmRequestProperty = V extends object - ? { [K in keyof V]: K extends 'headers' ? JsonObjectProperty : JsonProperty } & Record< - 'headers' | 'content', - unknown - > - : never; -type PendingMarkSpecProperty = V extends object - ? { - [K in keyof V]: K extends 'data' | 'metadata' | 'categoryProfile' - ? JsonProperty - : K extends keyof PendingMarkSpec - ? PendingMarkSpec[K] - : JsonProperty; - } & Record<'name', unknown> - : never; -type PendingMarksProperty = V extends readonly unknown[] ? { [I in keyof V]: PendingMarkSpecProperty } : never; -type EventMetadataProperty = V extends EventMetadataValue ? V : never; -type MetricMeasurementProperty = V extends object - ? { - [K in keyof V]: K extends 'attributes' - ? JsonProperty - : K extends keyof MetricMeasurement - ? MetricMeasurement[K] - : unknown; - } & Record<'name' | 'kind' | 'valueType' | 'value', unknown> - : never; -interface LlmRequestOpen { - headers: JsonObject; - content: Json; - [key: string]: JsonValue; -} -interface ToolExecutionResultOpen { - result: Json; - annotation?: Json; - [key: string]: JsonValue | undefined; -} -interface EventSanitizeFieldsOpen { - data?: Json; - categoryProfile?: Json; - metadata?: Json; - [key: string]: JsonValue | undefined; -} - -export type JsonCompatible = - | JsonValue - | ([T] extends [{} | null] ? Shaped }> : never); -export type JsonObjectCompatible = - | JsonObject - | ([T] extends [object] - ? Shaped }, NonJsonValue | readonly unknown[]> - : never); -export type LlmRequestCompatible = - | LlmRequestOpen - | ([T] extends [object] - ? Shaped< - T, - { [K in keyof T]: K extends 'headers' ? JsonObjectProperty : JsonProperty } & Record< - 'headers' | 'content', - unknown - > - > - : never); -export type ToolExecutionResultCompatible = - | ToolExecutionResultOpen - | ([T] extends [object] ? Shaped } & Record<'result', unknown>> : never); -export type ToolExecutionInterceptOutcomeCompatible = - | ToolExecutionInterceptOutcome - | ([T] extends [object] - ? Shaped< - T, - { [K in keyof T]: K extends 'pendingMarks' ? PendingMarksProperty : JsonProperty } & Record< - 'result', - unknown - > - > - : never); -export type LlmRequestInterceptOutcomeCompatible = - | LlmRequestInterceptOutcome - | ([T] extends [object] - ? Shaped< - T, - { - [K in keyof T]: K extends 'request' - ? LlmRequestProperty - : K extends 'pendingMarks' - ? PendingMarksProperty - : K extends 'optimizationContributions' - ? LlmOptimizationContribution[] - : JsonProperty; - } & Record<'request', unknown> - > - : never); -export type EventSanitizeFieldsCompatible = - | EventSanitizeFieldsOpen - | ([T] extends [object] ? Shaped }> : never); -export type EventMetadataCompatible = - | EventMetadata - | ([T] extends [object] ? Shaped }> : never); -export type MetricMeasurementsCompatible = - | readonly MetricMeasurement[] - | ([T] extends [readonly unknown[]] ? { [I in keyof T]: MetricMeasurementProperty } : never); - -export type RuntimeRegistrationKindName = `${RuntimeRegistrationKind}`; - -export type ResourceMetricsMeasurementScope = 'runtime_default' | ResourceMeasurementScope; - -export type ResourceUnitKind = - | DurationUnit - | CapacityUnit - | DataUnit - | BandwidthUnit - | CpuUnit - | UtilizationUnit - | CountUnit; - -export type ResourceUnit = U | `${U}`; - -export interface ResourceMetricsPollingConfig { - enabled?: boolean; - interval_millis?: number; -} - -export interface ResourceMemoryInspectionConfig { - enabled?: boolean; - stale_after_millis?: number; -} - -export interface ResourceMetricsCpuConfig { - enabled?: boolean; -} - -export interface ResourceMetricsMemoryConfig { - enabled?: boolean; -} - -export interface ResourceMetricsProcessConfig { - enabled?: boolean; -} - -export interface ResourceMetricsDiskConfig { - enabled?: boolean; - process_io?: boolean; - filesystem_paths?: string[]; -} - -export interface ResourceMetricsGpuConfig { - enabled?: boolean; - devices?: string[]; - device_metrics?: boolean; - process_metrics?: boolean; -} - -export interface ResourceMetricsNetworkConfig { - enabled?: boolean; - interfaces?: string[]; -} - -export interface ResourceMetricsCpuUnits { - user_time?: ResourceUnit; - system_time?: ResourceUnit; - total_time?: ResourceUnit; - throttled_time?: ResourceUnit; - some_pressure_stall_time?: ResourceUnit; - full_pressure_stall_time?: ResourceUnit; - consumption_rate?: ResourceUnit; - effective_limit?: ResourceUnit; -} - -export interface ResourceMetricsMemoryUnits { - system_used?: ResourceUnit; - system_total?: ResourceUnit; - system_available?: ResourceUnit; - resident?: ResourceUnit; - private?: ResourceUnit; - physical_footprint?: ResourceUnit; - virtual_memory?: ResourceUnit; - peak_resident?: ResourceUnit; - limit?: ResourceUnit; - environment_accounted?: ResourceUnit; - some_pressure_stall_time?: ResourceUnit; - full_pressure_stall_time?: ResourceUnit; -} - -export interface ResourceMetricsDiskUnits { - read_data?: ResourceUnit; - write_data?: ResourceUnit; - filesystem_total_capacity?: ResourceUnit; - filesystem_available_capacity?: ResourceUnit; - filesystem_free_capacity?: ResourceUnit; - read_throughput?: ResourceUnit; - write_throughput?: ResourceUnit; -} - -export interface ResourceMetricsGpuUnits { - device_memory_used?: ResourceUnit; - process_memory_used?: ResourceUnit; - device_compute_utilization?: ResourceUnit; - process_compute_utilization?: ResourceUnit; -} - -export interface ResourceMetricsNetworkTrafficUnits { - received_data?: ResourceUnit; - transmitted_data?: ResourceUnit; - receive_throughput?: ResourceUnit; - transmit_throughput?: ResourceUnit; -} - -export interface ResourceMetricsNetworkUnits { - system?: ResourceMetricsNetworkTrafficUnits; - interface?: ResourceMetricsNetworkTrafficUnits; -} - -export interface ResourceMetricsUnits { - cpu?: ResourceMetricsCpuUnits; - memory?: ResourceMetricsMemoryUnits; - disk?: ResourceMetricsDiskUnits; - gpu?: ResourceMetricsGpuUnits; - network?: ResourceMetricsNetworkUnits; -} - -export interface ResourceMetricsConfig { - measurement_scope?: ResourceMetricsMeasurementScope; - polling?: ResourceMetricsPollingConfig; - memory_inspection?: ResourceMemoryInspectionConfig; - cpu?: ResourceMetricsCpuConfig; - memory?: ResourceMetricsMemoryConfig; - process?: ResourceMetricsProcessConfig; - disk?: ResourceMetricsDiskConfig; - gpu?: ResourceMetricsGpuConfig; - network?: ResourceMetricsNetworkConfig; - units?: ResourceMetricsUnits; -} - -export type ResourceOperatingSystem = 'linux' | 'macos' | 'windows' | 'unsupported'; -export type ResourceMeasurementScope = 'global' | 'application_process' | 'process_tree'; - -/** Measurement values use bigint for exact integers above JavaScript's safe-integer range. */ -export type ResourceNumeric = number | bigint; - -export interface ResourceMeasurement { - readonly value: T; - readonly unit: U; -} - -export interface ResourceLimitEventCount { - readonly resource: 'cpu' | 'memory' | 'processes'; - readonly event: 'throttled' | 'high' | 'maximum' | 'out_of_memory' | 'terminated'; - readonly count: ResourceMeasurement | null; -} - -export type AcceleratorVendor = 'nvidia' | 'amd' | 'intel' | 'apple' | 'other'; - -export interface AcceleratorDeviceMetrics { - readonly vendor: AcceleratorVendor; - readonly deviceIdentifier: string; - readonly deviceIndex: number | null; - readonly memoryUsed: ResourceMeasurement | null; - readonly computeUtilization: ResourceMeasurement | null; -} - -export interface AcceleratorProcessMetrics { - readonly vendor: AcceleratorVendor; - readonly deviceIdentifier: string; - readonly deviceIndex: number | null; - readonly processId: number; - readonly memoryUsed: ResourceMeasurement | null; - readonly computeUtilization: ResourceMeasurement | null; -} - -export interface FilesystemCapacityMetrics { - readonly path: string; - readonly totalCapacity: ResourceMeasurement | null; - readonly availableCapacity: ResourceMeasurement | null; - readonly freeCapacity: ResourceMeasurement | null; -} - -export interface CpuMetrics { - readonly userTime: ResourceMeasurement | null; - readonly systemTime: ResourceMeasurement | null; - readonly totalTime: ResourceMeasurement | null; - readonly consumptionRate: ResourceMeasurement | null; - readonly throttledTime: ResourceMeasurement | null; - readonly effectiveLimit: ResourceMeasurement | null; - readonly somePressureStallTime: ResourceMeasurement | null; - readonly fullPressureStallTime: ResourceMeasurement | null; - readonly limitEvents: readonly ResourceLimitEventCount[]; -} - -export interface MemoryMetrics { - readonly systemUsed: ResourceMeasurement | null; - readonly systemTotal: ResourceMeasurement | null; - readonly systemAvailable: ResourceMeasurement | null; - readonly resident: ResourceMeasurement | null; - readonly private: ResourceMeasurement | null; - readonly physicalFootprint: ResourceMeasurement | null; - readonly virtualMemory: ResourceMeasurement | null; - readonly peakResident: ResourceMeasurement | null; - readonly limit: ResourceMeasurement | null; - readonly environmentAccounted: ResourceMeasurement | null; - readonly somePressureStallTime: ResourceMeasurement | null; - readonly fullPressureStallTime: ResourceMeasurement | null; - readonly outOfMemoryEventCount: ResourceMeasurement | null; - readonly limitEvents: readonly ResourceLimitEventCount[]; -} - -export interface ProcessMetrics { - readonly activeCount: ResourceMeasurement | null; - readonly descendantCount: ResourceMeasurement | null; - readonly threadCount: ResourceMeasurement | null; - readonly lifetimeCreationCount: ResourceMeasurement | null; - readonly openFileDescriptorCount: ResourceMeasurement | null; - readonly windowsHandleCount: ResourceMeasurement | null; - readonly limitEvents: readonly ResourceLimitEventCount[]; -} - -export interface DiskMetrics { - readonly readData: ResourceMeasurement | null; - readonly writeData: ResourceMeasurement | null; - readonly readThroughput: ResourceMeasurement | null; - readonly writeThroughput: ResourceMeasurement | null; - readonly readOperations: ResourceMeasurement | null; - readonly writeOperations: ResourceMeasurement | null; - readonly filesystems: readonly FilesystemCapacityMetrics[]; -} - -export interface GpuMetrics { - readonly deviceMetrics: readonly AcceleratorDeviceMetrics[] | null; - readonly processMetrics: readonly AcceleratorProcessMetrics[] | null; -} - -export interface NetworkTrafficMetrics { - readonly receivedData: ResourceMeasurement | null; - readonly transmittedData: ResourceMeasurement | null; - readonly receiveThroughput: ResourceMeasurement | null; - readonly transmitThroughput: ResourceMeasurement | null; - readonly receivedPackets: ResourceMeasurement | null; - readonly transmittedPackets: ResourceMeasurement | null; - readonly receiveErrors: ResourceMeasurement | null; - readonly transmitErrors: ResourceMeasurement | null; -} - -export interface NetworkInterfaceMetrics { - readonly name: string; - readonly traffic: NetworkTrafficMetrics; -} - -export interface NetworkMetrics { - readonly measurementScope: 'global'; - readonly system: NetworkTrafficMetrics; - readonly interfaces: readonly NetworkInterfaceMetrics[]; -} - -export interface ProcessSamplingMetadata { - readonly visibleProcesses: ResourceNumeric; - readonly sampledProcesses: ResourceNumeric; - /** Readable process counts keyed by canonical snake_case measurement paths. */ - readonly fieldSampledProcesses: Readonly>; -} - -export interface ResourceMetricsSnapshot { - readonly timestamp: string; - readonly operatingSystem: ResourceOperatingSystem; - readonly measurementScope: ResourceMeasurementScope; - readonly processSampling: ProcessSamplingMetadata | null; - readonly cpu: CpuMetrics | null; - readonly memory: MemoryMetrics | null; - readonly process: ProcessMetrics | null; - readonly disk: DiskMetrics | null; - readonly gpu: GpuMetrics | null; - readonly network: NetworkMetrics | null; -} - -/** Codec identity available while a managed LLM event is sanitized. */ -export type LlmCodecIdentity = - | { kind: 'none' } - | { - kind: 'builtin'; - id: 'openai_chat' | 'openai_responses' | 'anthropic_messages' | 'oci_genai' | 'gemini_generate_content'; - } - | { kind: 'runtime'; id: string } - | { kind: 'opaque' }; - -/** Request codec context shared by sanitizer and execution callbacks. */ -export interface LlmSanitizeRequestContext { - codec: LlmCodecIdentity; - /** Resolve the active codec for this callback. Do not retain the result after the callback returns. */ - resolveCodec(): import('./typed').LlmCodec | null; -} - -/** Response codec context shared by sanitizer and execution callbacks. */ -export interface LlmSanitizeResponseContext { - codec: LlmCodecIdentity; - /** Resolve the active codec for this callback. Do not retain the result after the callback returns. */ - resolveCodec(): import('./typed').LlmResponseCodec | null; -} - -/** Request codec context exposed to an LLM execution intercept. */ -export type LlmRequestContext = LlmSanitizeRequestContext; - -/** Response codec context exposed to an LLM execution intercept. */ -export type LlmResponseContext = LlmSanitizeResponseContext; - -/** Codec capabilities for one managed LLM execution intercept invocation. */ -export interface LlmExecutionContext { - /** Request codec identity plus optional decode and encode capability. */ - requestCodec: LlmRequestContext; - /** Unary response codec identity plus optional decode capability; `null` for streaming execution. */ - responseCodec: LlmResponseContext | null; -} - -/** Schema tag attached to an opaque optimization contribution payload. */ -export interface LlmOptimizationDataSchema { - name: string; - version: string; -} - -/** Model identity retained for counterfactual pricing and downstream repricing. */ -export interface LlmOptimizationModel { - model: string; - provider?: string; -} - -/** Baseline and effective model identities for a routing optimization. */ -export interface LlmOptimizationModelTransition { - baseline?: LlmOptimizationModel; - effective?: LlmOptimizationModel; -} - -/** Explicit token evidence, independent from a pricing catalog. */ -export interface LlmOptimizationTokens { - /** Token counts must be non-negative JavaScript safe integers. */ - prompt_tokens?: number; - /** Token counts must be non-negative JavaScript safe integers. */ - completion_tokens?: number; - /** Token counts must be non-negative JavaScript safe integers. */ - cache_read_tokens?: number; - /** Token counts must be non-negative JavaScript safe integers. */ - cache_write_tokens?: number; - /** Token counts must be non-negative JavaScript safe integers. */ - total_tokens?: number; -} - -/** Baseline, effective, and saved token evidence for one optimization. */ -export interface LlmOptimizationTokenImpact { - baseline?: LlmOptimizationTokens; - effective?: LlmOptimizationTokens; - saved?: LlmOptimizationTokens; - quality?: 'observed' | 'estimated'; - estimation_method?: string; -} - -/** - * One plugin's optimization evidence. - * - * `kind` is deliberately an open string so new optimizer categories round-trip - * without a Relay release. Unknown top-level fields are retained by the wire - * contract and represented by this interface's JSON extension surface. - */ -export interface LlmOptimizationContribution { - id?: string; - /** Relay ordering must remain within JavaScript's safe-integer range. */ - sequence?: number; - producer: string; - kind: 'input_compression' | 'model_routing' | (string & {}); - applied: boolean; - model_transition?: LlmOptimizationModelTransition; - token_impact?: LlmOptimizationTokenImpact; - payload_schema?: LlmOptimizationDataSchema; - payload?: Json; - [key: string]: - | Json - | LlmOptimizationModelTransition - | LlmOptimizationTokenImpact - | LlmOptimizationDataSchema - | undefined; -} - -/** Canonical result returned by an LLM request intercept. */ -export interface LlmRequestInterceptOutcome { - request: LlmRequestShape; - annotated?: Json | null; - pendingMarks?: PendingMarkSpec[]; - optimizationContributions?: LlmOptimizationContribution[]; - [key: string]: JsonValue | PendingMarkSpec[] | LlmOptimizationContribution[] | undefined; -} - -export interface ConfigDiagnostic { - level: 'warning' | 'error'; - code: string; - component?: string; - field?: string; - message: string; -} -export interface ToolExecutionInterceptOutcome extends ToolExecutionResult { - pendingMarks?: PendingMarkSpec[]; - [key: string]: JsonValue | PendingMarkSpec[] | undefined; -} -export interface PluginContext { - registerConditionalMiddlewareGuardrail( - name: string, - kinds: Array, - registrationName: string, - guardrail: (kinds: RuntimeRegistrationKind[], registrationName: string) => string | null, - ): void; - registerSubscriber(name: string, callback: (event: JsonObject) => void | Promise): void; - registerEventMetadataInjector>( - name: string, - priority: number, - callback: (event: JsonObject) => TMetadata | Promise, - ): void; - registerMarkSanitizeGuardrail>( - name: string, - priority: number, - callback: (event: JsonObject, fields: EventSanitizeFields) => TFields | Promise, - ): void; - registerScopeSanitizeStartGuardrail>( - name: string, - priority: number, - callback: (event: JsonObject, fields: EventSanitizeFields) => TFields | Promise, - ): void; - registerScopeSanitizeEndGuardrail>( - name: string, - priority: number, - callback: (event: JsonObject, fields: EventSanitizeFields) => TFields | Promise, - ): void; - registerToolSanitizeRequestGuardrail>( - name: string, - priority: number, - callback: (name: string, args: Json) => TValue | Promise, - ): void; - registerToolSanitizeResponseGuardrail>( - name: string, - priority: number, - callback: (name: string, result: Json) => TValue | Promise, - ): void; - registerToolConditionalExecutionGuardrail( - name: string, - priority: number, - callback: (name: string, args: Json) => string | null | Promise, - ): void; - registerLlmSanitizeRequestGuardrail = LlmRequestShape>( - name: string, - priority: number, - callback: ( - request: LlmRequestShape, - context: LlmSanitizeRequestContext, - ) => TRequest | null | Promise, - ): void; - registerLlmSanitizeResponseGuardrail = Json>( - name: string, - priority: number, - callback: (response: Json, context: LlmSanitizeResponseContext) => TResponse | null | Promise, - ): void; - registerLlmConditionalExecutionGuardrail( - name: string, - priority: number, - callback: (request: LlmRequestShape) => string | null | Promise, - ): void; - registerLlmRequestIntercept>( - name: string, - priority: number, - breakChain: boolean, - callback: (args: { - name: string; - request: LlmRequestShape; - annotated: Json | null; - }) => TOutcome | Promise, - ): void; - registerLlmExecutionIntercept>( - name: string, - priority: number, - callback: ( - request: LlmRequestShape, - context: LlmExecutionContext, - next: >(request: TNextRequest) => Json | Promise, - ) => TResponse | Promise, - ): void; - registerLlmStreamExecutionIntercept>( - name: string, - priority: number, - callback: ( - request: LlmRequestShape, - context: LlmExecutionContext, - next: >( - request: TNextRequest, - ) => Promise>, - ) => AsyncIterable | Promise>, - ): void; - registerToolRequestIntercept>( - name: string, - priority: number, - breakChain: boolean, - callback: (name: string, args: Json) => TArgs | Promise, - ): void; - registerToolExecutionIntercept>( - name: string, - priority: number, - callback: ( - context: ToolExecutionContext, - next: >( - args: TNextArgs, - ) => ToolExecutionResult | Promise, - ) => TOutcome | Promise, - ): void; -} - -/** Scalar value accepted in event metadata additions. */ -export type EventMetadataScalar = string | number | boolean; - -/** - * Flat value accepted in event metadata additions. After JSON conversion, - * numeric arrays must contain only integer values or only floating-point values. - */ -export type EventMetadataValue = EventMetadataScalar | readonly string[] | readonly number[] | readonly boolean[]; - -/** Metadata additions returned by an event metadata injector. */ -export type EventMetadata = Record; diff --git a/crates/node/scripts/append_root_types.mjs b/crates/node/scripts/append_root_types.mjs deleted file mode 100644 index d665e3f9cd..0000000000 --- a/crates/node/scripts/append_root_types.mjs +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { readFileSync, writeFileSync } from 'node:fs'; - -const [declarationsPath, additionsPath] = process.argv.slice(2); - -if (!declarationsPath || !additionsPath) { - throw new Error('usage: append_root_types.mjs '); -} - -const declarations = readFileSync(declarationsPath, 'utf8').trimEnd(); -const additions = readFileSync(additionsPath, 'utf8').trim(); - -writeFileSync(declarationsPath, `${declarations}\n\n${additions}\n`); diff --git a/crates/node/scripts/build.mjs b/crates/node/scripts/build.mjs index 8fe1f0a646..0119232a70 100644 --- a/crates/node/scripts/build.mjs +++ b/crates/node/scripts/build.mjs @@ -8,29 +8,17 @@ import path from 'node:path'; const require = createRequire(import.meta.url); const cliPackagePath = require.resolve('@napi-rs/cli/package.json'); const napiCli = path.join(path.dirname(cliPackagePath), require(cliPackagePath).bin.napi); - -const build = spawnSync( - process.execPath, - [napiCli, 'build', '--platform', '--dts', 'index.d.ts', ...process.argv.slice(2)], - { stdio: 'inherit' }, -); - -if (build.error) { - throw build.error; -} - -if (build.status !== 0) { - process.exit(build.status ?? 1); -} - -const appendTypes = spawnSync( - process.execPath, - ['scripts/append_root_types.mjs', 'index.d.ts', 'root-types.d.ts'], - { stdio: 'inherit' }, -); - -if (appendTypes.error) { - throw appendTypes.error; +const tsc = require.resolve('typescript/bin/tsc'); + +function run(args) { + const result = spawnSync(process.execPath, args, { stdio: 'inherit' }); + if (result.error) { + throw result.error; + } + if (result.status !== 0) { + process.exit(result.status ?? 1); + } } -process.exit(appendTypes.status ?? 1); +run([napiCli, 'build', '--platform', '--js', 'binding.js', '--dts', 'binding.d.ts', ...process.argv.slice(2)]); +run([tsc, '-p', 'tsconfig.json']); diff --git a/crates/node/src/api/mod.rs b/crates/node/src/api/mod.rs index abe8b795c6..5355646173 100644 --- a/crates/node/src/api/mod.rs +++ b/crates/node/src/api/mod.rs @@ -2242,7 +2242,9 @@ pub fn llm_call_execute_async<'env>( env: &'env Env, name: String, #[napi(ts_arg_type = "TRequest")] request: Json, - #[napi(ts_arg_type = "(request: LlmRequestShape) => TResponse | Promise")] + #[napi( + ts_arg_type = "(request: LlmRequestShape, signal: AbortSignal) => TResponse | Promise" + )] func: Callback, handle: Option<&ScopeHandle>, attributes: Option, @@ -2512,7 +2514,7 @@ pub fn deregister_conditional_middleware_guardrail(name: String) -> Result /// List global gateable runtime registrations. #[napi( ts_generic_types = "K extends RuntimeRegistrationKind | RuntimeRegistrationKindName = RuntimeRegistrationKind", - ts_return_type = "Array }>", + ts_return_type = "Array }>", catch_unwind )] pub fn list_runtime_registrations( @@ -3840,8 +3842,13 @@ impl AdaptiveRuntime { /// Create an adaptive runtime wrapper from config. /// /// The runtime is constructed lazily when `register()` is awaited. - #[napi(constructor, catch_unwind)] - pub fn new(config: Json) -> Result { + #[napi( + factory, + js_name = "create", + ts_generic_types = "TConfig extends JsonCompatible", + catch_unwind + )] + pub fn create(#[napi(ts_arg_type = "TConfig")] config: Json) -> Result { let config: AdaptiveConfig = serde_json::from_value(config) .map_err(|error| Error::invalid_argument(error.to_string()))?; let report = validate_adaptive_config_or_err(&config)?; @@ -3891,7 +3898,7 @@ impl AdaptiveRuntime { } /// Return the validation report captured during runtime construction. - #[napi(catch_unwind)] + #[napi(ts_return_type = "ConfigReport", catch_unwind)] pub fn report(&self) -> Result { let state = self.state()?; let report = match &*state { @@ -3910,8 +3917,16 @@ impl AdaptiveRuntime { } /// Build cache request facts for an annotated LLM request. - #[napi(js_name = "buildCacheRequestFacts", catch_unwind)] - pub fn build_cache_request_facts(&self, options: Json) -> Result> { + #[napi( + js_name = "buildCacheRequestFacts", + ts_generic_types = "TOptions extends JsonCompatible", + ts_return_type = "CacheRequestFacts | null", + catch_unwind + )] + pub fn build_cache_request_facts( + &self, + #[napi(ts_arg_type = "TOptions")] options: Json, + ) -> Result> { let options: CacheRequestFactsOptions = serde_json::from_value(options) .map_err(|error| Error::invalid_argument(error.to_string()))?; let _request_id = parse_uuid(&options.request_id, "requestId UUID")?; @@ -3969,16 +3984,28 @@ fn parse_cache_request_facts(value: Option) -> Result Result { +#[napi( + js_name = "validateAdaptiveConfig", + ts_generic_types = "TConfig extends JsonCompatible", + ts_return_type = "ConfigReport", + catch_unwind +)] +pub fn validate_adaptive_config(#[napi(ts_arg_type = "TConfig")] config: Json) -> Result { let config: AdaptiveConfig = serde_json::from_value(config) .map_err(|error| Error::invalid_argument(error.to_string()))?; serde_json::to_value(CoreAdaptiveRuntime::validate_config(&config)).map_err(Error::from) } /// Build one adaptive cache telemetry event from normalized usage. -#[napi(js_name = "buildCacheTelemetryEvent", catch_unwind)] -pub fn build_cache_telemetry_event(options: Json) -> Result> { +#[napi( + js_name = "buildCacheTelemetryEvent", + ts_generic_types = "TOptions extends JsonCompatible", + ts_return_type = "CacheTelemetryEvent | null", + catch_unwind +)] +pub fn build_cache_telemetry_event( + #[napi(ts_arg_type = "TOptions")] options: Json, +) -> Result> { let options: CacheTelemetryOptions = serde_json::from_value(options) .map_err(|error| Error::invalid_argument(error.to_string()))?; let provider = parse_cache_telemetry_provider(&options.provider)?; @@ -4225,7 +4252,7 @@ mod dynamic_plugin_close_state_tests; #[napi] impl PluginHostActivation { /// Return the validation report produced by activation. - #[napi(getter, catch_unwind)] + #[napi(getter, ts_return_type = "PluginHostReport", catch_unwind)] pub fn report(&self) -> Result { serde_json::to_value(self.close_state.report()).map_err(Error::from) } @@ -4270,10 +4297,13 @@ impl PluginHostActivation { } /// Initialize the core-owned static and dynamic plugin host from layered files. -#[napi(catch_unwind)] +#[napi( + ts_generic_types = "TConfig extends JsonCompatible", + catch_unwind +)] pub fn initialize<'env>( env: &'env Env, - config: Json, + #[napi(ts_arg_type = "TConfig")] config: Json, additional_plugins_toml: Option, ) -> Result> { spawn_promise( @@ -4304,8 +4334,15 @@ pub fn initialize<'env>( ) } /// Validate dynamic plugins without loading any plugin code. -#[napi(catch_unwind)] -pub fn validate(config: Json, additional_plugins_toml: Option) -> Result { +#[napi( + ts_generic_types = "TConfig extends JsonCompatible", + ts_return_type = "PluginHostReport", + catch_unwind +)] +pub fn validate( + #[napi(ts_arg_type = "TConfig")] config: Json, + additional_plugins_toml: Option, +) -> Result { let config: nemo_relay_plugin_host::dynamic::PluginHostConfig = parse_json(config, "plugin config")?; let report = @@ -4314,8 +4351,12 @@ pub fn validate(config: Json, additional_plugins_toml: Option) -> Result } /// Validate only the supplied static plugin configuration. -#[napi(catch_unwind)] -pub fn validate_exact(config: Json) -> Result { +#[napi( + ts_generic_types = "TConfig extends JsonCompatible", + ts_return_type = "PluginHostReport", + catch_unwind +)] +pub fn validate_exact(#[napi(ts_arg_type = "TConfig")] config: Json) -> Result { if config.get("diagnostics").is_some() { return Err(Error::invalid_argument( "diagnostics is a host-only setting; use layered validation", diff --git a/crates/node/src/diagnostics.rs b/crates/node/src/diagnostics.rs index bb281959e5..d1e4cb05aa 100644 --- a/crates/node/src/diagnostics.rs +++ b/crates/node/src/diagnostics.rs @@ -110,7 +110,7 @@ impl DiagnosticsReader { } /// Return a caller-owned camelCase object. Diagnostic u64 fields are always bigint. /// Use snapshotJson for portable serialization; JSON.stringify cannot encode bigint. - #[napi(ts_return_type = "object", catch_unwind)] + #[napi(ts_return_type = "DiagnosticSnapshot", catch_unwind)] pub fn snapshot<'env>( &self, env: &'env Env, @@ -129,7 +129,7 @@ impl DiagnosticsReader { } /// Return a caller-owned camelCase object. Diagnostic u64 fields are always bigint. /// Use inspectMemoryJson for portable serialization; JSON.stringify cannot encode bigint. - #[napi(ts_return_type = "object", catch_unwind)] + #[napi(ts_return_type = "DiagnosticMemoryPage", catch_unwind)] pub fn inspect_memory<'env>( &self, env: &'env Env, @@ -141,7 +141,7 @@ impl DiagnosticsReader { ) } /// Return independent core/adapter budgets, occupancy, and logging coverage. - #[napi(ts_return_type = "object", catch_unwind)] + #[napi(ts_return_type = "DiagnosticsInfo", catch_unwind)] pub fn info<'env>(&self, env: &'env Env) -> Result> { let value = serde_json::to_value(self.inner.info().map_err(Error::from)?) .map_err(|_| Error::generic_failure("invalid_metadata"))?; @@ -157,8 +157,25 @@ pub fn diagnostics_reader() -> Result { .map_err(Error::from) } +#[napi(object)] +pub struct DiagnosticsQueryContract { + pub max_query_bytes: u32, + pub numeric_query_fields: Vec, +} + +const MAX_QUERY_BYTES: u32 = { + assert!(embedded::MAX_BINDING_QUERY_BYTES <= u32::MAX as usize); + embedded::MAX_BINDING_QUERY_BYTES as u32 +}; + /// Internal descriptor consumed by the JavaScript query adapter. #[napi(js_name = "_diagnosticsQueryContract", catch_unwind)] -pub fn diagnostics_query_contract() -> String { - embedded::binding_query_contract_json() +pub fn diagnostics_query_contract() -> DiagnosticsQueryContract { + DiagnosticsQueryContract { + max_query_bytes: MAX_QUERY_BYTES, + numeric_query_fields: embedded::DIAGNOSTIC_QUERY_INTEGER_FIELDS + .iter() + .map(|field| (*field).to_owned()) + .collect(), + } } diff --git a/crates/node/src/types/mod.rs b/crates/node/src/types/mod.rs index c1c0e99ec5..4d14131c5c 100644 --- a/crates/node/src/types/mod.rs +++ b/crates/node/src/types/mod.rs @@ -433,58 +433,6 @@ impl From for LlmHandle { } } -/// An LLM request, encapsulating headers and content. -/// -/// Construct via `new LlmRequest(headers, content)`. -#[napi] -pub struct LlmRequest { - pub(crate) inner: CoreLlmRequest, -} - -#[napi] -impl LlmRequest { - /// Create a new LLM request from headers and content. - #[napi(constructor, catch_unwind)] - pub fn new( - #[napi(ts_arg_type = "JsonObject")] headers: serde_json::Value, - #[napi(ts_arg_type = "Json")] content: serde_json::Value, - ) -> Result { - let Json::Object(headers) = headers else { - return Err(Error::invalid_argument( - "LlmRequest headers must be a JSON object", - )); - }; - Ok(Self { - inner: CoreLlmRequest { headers, content }, - }) - } - - #[napi( - factory, - js_name = "from", - ts_generic_types = "THeaders extends JsonObjectCompatible, TContent extends JsonCompatible", - catch_unwind - )] - pub fn from_parts( - #[napi(ts_arg_type = "THeaders")] headers: serde_json::Value, - #[napi(ts_arg_type = "TContent")] content: serde_json::Value, - ) -> Result { - Self::new(headers, content) - } - - /// The metadata headers as a JSON object. - #[napi(getter, catch_unwind, ts_return_type = "JsonObject")] - pub fn headers(&self) -> serde_json::Value { - Json::Object(self.inner.headers.clone()) - } - - /// The request payload as a JSON value. - #[napi(getter, catch_unwind, ts_return_type = "Json")] - pub fn content(&self) -> serde_json::Value { - self.inner.content.clone() - } -} - /// Observability fields returned by mark and scope event sanitizers. #[napi(object)] #[derive(Deserialize, Serialize)] diff --git a/crates/node/tests/adaptive_runtime_tests.mjs b/crates/node/tests/adaptive_runtime_tests.mjs index 1a23b027b1..e62cc7ba68 100644 --- a/crates/node/tests/adaptive_runtime_tests.mjs +++ b/crates/node/tests/adaptive_runtime_tests.mjs @@ -6,7 +6,8 @@ import assert from 'node:assert/strict'; import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); -const adaptive = require('../adaptive.js'); +const adaptive = require('../dist/adaptive.js'); +const { ScopeType, withScope } = require('../dist/index.js'); function acgRuntimeConfig(provider = 'anthropic') { return { @@ -116,9 +117,15 @@ describe('adaptive runtime bridge', () => { it('registers an owned runtime and builds cache request facts', async () => { const runtime = new adaptive.AdaptiveRuntime(acgRuntimeConfig('openai')); + await withScope('unbound', ScopeType.Agent, (handle) => { + assert.throws(() => runtime.bindScope(handle), /binding ACG request intercepts/); + }); await runtime.register(); try { assert.deepEqual(runtime.report().diagnostics, []); + await withScope('bound', ScopeType.Agent, (handle) => { + runtime.bindScope(handle); + }); const facts = runtime.buildCacheRequestFacts({ provider: 'openai', requestId: '00000000-0000-0000-0000-000000000203', diff --git a/crates/node/tests/adaptive_tests.mjs b/crates/node/tests/adaptive_tests.mjs index 2d6cdf7dd8..57848f26d4 100644 --- a/crates/node/tests/adaptive_tests.mjs +++ b/crates/node/tests/adaptive_tests.mjs @@ -7,9 +7,9 @@ import { createRequire } from 'node:module'; import * as pluginHost from './plugin_host_test_helper.mjs'; const require = createRequire(import.meta.url); -const lib = require('../index.js'); -const plugin = require('../plugin.js'); -const adaptive = require('../adaptive.js'); +const lib = require('../dist/index.js'); +const plugin = require('../dist/plugin.js'); +const adaptive = require('../dist/adaptive.js'); describe('core plugins', () => { it('reports active config and lists registered plugin kinds', async () => { diff --git a/crates/node/tests/atif_tests.mjs b/crates/node/tests/atif_tests.mjs index c20c2f5522..680d75dfa0 100644 --- a/crates/node/tests/atif_tests.mjs +++ b/crates/node/tests/atif_tests.mjs @@ -6,7 +6,7 @@ import assert from 'node:assert/strict'; import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); -const lib = require('../index.js'); +const lib = require('../dist/index.js'); const { AtifExporter, ScopeType, pushScope, popScope, llmCall, llmCallEnd } = lib; diff --git a/crates/node/tests/atof_tests.mjs b/crates/node/tests/atof_tests.mjs index 3d1cbe4a8e..084cdfaa8c 100644 --- a/crates/node/tests/atof_tests.mjs +++ b/crates/node/tests/atof_tests.mjs @@ -10,7 +10,7 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; const require = createRequire(import.meta.url); -const { AtofExporter, ScopeType, pushScope, popScope, event } = require('../index.js'); +const { AtofExporter, ScopeType, pushScope, popScope, event } = require('../dist/index.js'); function tempDir(prefix) { return mkdtempSync(join(tmpdir(), `nemo-relay-${prefix}-`)); diff --git a/crates/node/tests/callback_error_tests.mjs b/crates/node/tests/callback_error_tests.mjs index 880b8b2822..4a8c45e457 100644 --- a/crates/node/tests/callback_error_tests.mjs +++ b/crates/node/tests/callback_error_tests.mjs @@ -6,7 +6,7 @@ import assert from 'node:assert/strict'; import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); -const lib = require('../index.js'); +const lib = require('../dist/index.js'); const { __testClosedCollectorCallback, diff --git a/crates/node/tests/codec_tests.mjs b/crates/node/tests/codec_tests.mjs index 071d5be18b..43769c8148 100644 --- a/crates/node/tests/codec_tests.mjs +++ b/crates/node/tests/codec_tests.mjs @@ -6,7 +6,7 @@ import assert from 'node:assert/strict'; import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); -const lib = require('../index.js'); +const lib = require('../dist/index.js'); const { pushScope, popScope, llmCallExecute, registerLlmRequestIntercept, deregisterLlmRequestIntercept, ScopeType } = lib; diff --git a/crates/node/tests/context_tests.mjs b/crates/node/tests/context_tests.mjs index a0ca0d89e0..0943058227 100644 --- a/crates/node/tests/context_tests.mjs +++ b/crates/node/tests/context_tests.mjs @@ -8,7 +8,7 @@ import { spawnSync } from 'node:child_process'; import { readFileSync } from 'node:fs'; const require = createRequire(import.meta.url); -const lib = require('../index.js'); +const lib = require('../dist/index.js'); const { createScopeStack, @@ -256,7 +256,7 @@ describe('scope_stack_snapshot', () => { '-e', ` const assert = require('node:assert/strict'); - const library = require(${JSON.stringify(require.resolve('../index.js'))}); + const library = require(${JSON.stringify(require.resolve('../dist/index.js'))}); assert.equal(library.currentScopeStackSnapshot(), null); assert.equal(library.currentScopeStackSnapshot(), null); assert.equal(library.scopeStackActive(), false); diff --git a/crates/node/tests/deregister_tests.mjs b/crates/node/tests/deregister_tests.mjs index 6bd2f30f8e..59c7588954 100644 --- a/crates/node/tests/deregister_tests.mjs +++ b/crates/node/tests/deregister_tests.mjs @@ -6,7 +6,7 @@ import assert from 'node:assert/strict'; import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); -const lib = require('../index.js'); +const lib = require('../dist/index.js'); const { deregisterToolSanitizeRequestGuardrail, diff --git a/crates/node/tests/diagnostics.mjs b/crates/node/tests/diagnostics.mjs index c584047f93..0aba87afc2 100644 --- a/crates/node/tests/diagnostics.mjs +++ b/crates/node/tests/diagnostics.mjs @@ -7,13 +7,16 @@ import { test } from 'node:test'; import { createRequire } from 'node:module'; import { Worker } from 'node:worker_threads'; const require = createRequire(import.meta.url); -const diagnostics = require('../diagnostics.js'); -const plugin = require('../plugin.js'); -const queryContract = require('../diagnostics_query_contract.js'); +const diagnostics = require('../dist/diagnostics.js'); +const plugin = require('../dist/plugin.js'); +const queryContract = require('../dist/diagnostics_query_contract.js'); test('query adapter matches the shared cross-binding contract fixture', () => { const fixture = JSON.parse( - fs.readFileSync(new URL('../../diagnostics/tests/fixtures/diagnostics_query_contract.json', import.meta.url), 'utf8'), + fs.readFileSync( + new URL('../../diagnostics/tests/fixtures/diagnostics_query_contract.json', import.meta.url), + 'utf8', + ), ); assert.equal(queryContract.MAX_QUERY_BYTES, fixture.max_query_bytes); assert.equal(queryContract.MAX_QUERY_NODES, fixture.adapter_limits.max_query_nodes); @@ -71,7 +74,7 @@ test('configured inspection survives plugin and Node environment cleanup', async assert.equal(typeof JSON.parse(reader.snapshotJson()).sequence, 'string'); assert.equal(reader.info().limits.maxEvents, 3); assert.equal(reader.info().loggingCoverage, 'attached_since_service_start'); - const modulePath = require.resolve('../diagnostics.js'); + const modulePath = require.resolve('../dist/diagnostics.js'); const source = await new Promise((resolve, reject) => { const worker = new Worker( `const {parentPort} = require("node:worker_threads"); const diagnostics = require(${JSON.stringify(modulePath)}); parentPort.postMessage(diagnostics.reader().snapshot().source.instanceId);`, @@ -101,7 +104,7 @@ test('configured inspection survives plugin and Node environment cleanup', async ], }); assert.equal(reader.inspectMemory().resource.availability, 'not_yet_sampled'); - await require('../index.js').collectResourceMetrics(); + await require('../dist/index.js').collectResourceMetrics(); const sample = reader.inspectMemory().resource.sample; assert(sample); assert.equal(typeof sample.ordinal, 'bigint'); @@ -133,6 +136,12 @@ test('configured inspection survives plugin and Node environment cleanup', async [{ maxEvents: 4294967295 }, 'invalid_query'], [{ maxResponseBytes: 1 }, 'snapshot_capacity'], [{ ownership: { unknown: 'restricted-input' } }, 'invalid_query'], + [{ componentKey: 1.5 }, 'invalid_query'], + [{ ['k'.repeat(queryContract.MAX_QUERY_FIELD_NAME_BYTES + 1)]: true }, 'invalid_query'], + [ + Object.fromEntries(Array.from({ length: queryContract.MAX_QUERY_OBJECT_FIELDS + 1 }, (_, i) => [`f${i}`, true])), + 'invalid_query', + ], ]) { assert.throws( () => reader.snapshot(query), diff --git a/crates/node/tests/dynamic_plugin_tests.mjs b/crates/node/tests/dynamic_plugin_tests.mjs index 0a0e429196..a0b4cd626c 100644 --- a/crates/node/tests/dynamic_plugin_tests.mjs +++ b/crates/node/tests/dynamic_plugin_tests.mjs @@ -12,8 +12,8 @@ import { fileURLToPath } from 'node:url'; import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); -const lib = require('../index.js'); -const plugin = require('../plugin.js'); +const lib = require('../dist/index.js'); +const plugin = require('../dist/plugin.js'); const nodeDir = fileURLToPath(new URL('..', import.meta.url)); const repoRoot = path.resolve(nodeDir, '../..'); @@ -210,8 +210,24 @@ before(() => { mkdirSync(isolatedConfigHome, { recursive: true }); process.chdir(tempRoot); process.env.XDG_CONFIG_HOME = isolatedConfigHome; - const nativeFixture = path.join(repoRoot, 'crates', 'plugin-host', 'tests', 'fixtures', 'native_plugin', 'Cargo.toml'); - const workerFixture = path.join(repoRoot, 'crates', 'plugin-host', 'tests', 'fixtures', 'worker_plugin', 'Cargo.toml'); + const nativeFixture = path.join( + repoRoot, + 'crates', + 'plugin-host', + 'tests', + 'fixtures', + 'native_plugin', + 'Cargo.toml', + ); + const workerFixture = path.join( + repoRoot, + 'crates', + 'plugin-host', + 'tests', + 'fixtures', + 'worker_plugin', + 'Cargo.toml', + ); buildFixture(nativeFixture); buildFixture(workerFixture); nativeManifestRef = writeNativeManifest(path.join(fixtureTarget, 'debug', nativeLibraryName())); @@ -587,7 +603,7 @@ enabled = true }); it('defensively clears a native activation during garbage collection', () => { - const pluginModule = path.join(nodeDir, 'plugin.js'); + const pluginModule = path.join(nodeDir, 'dist', 'plugin.js'); const pluginsToml = writePluginHostConfig([activationSpec('fixture_native', 'rust_dynamic', nativeManifestRef)]); const script = ` import { createRequire } from 'node:module'; @@ -642,7 +658,7 @@ enabled = true const pidFile = path.join(tempRoot, 'finalizer-worker.pid'); const wrapper = writeWorkerWrapper(workerBinary, pidFile, 'finalizer-worker'); const manifestRef = writeWorkerManifest(wrapper, 'finalizer-worker'); - const pluginModule = path.join(nodeDir, 'plugin.js'); + const pluginModule = path.join(nodeDir, 'dist', 'plugin.js'); const pluginsToml = writePluginHostConfig([activationSpec('fixture_worker', 'worker', manifestRef)]); const script = ` import { spawn } from 'node:child_process'; diff --git a/crates/node/tests/error_propagation_tests.mjs b/crates/node/tests/error_propagation_tests.mjs index e4ee0dbda3..49e376cca8 100644 --- a/crates/node/tests/error_propagation_tests.mjs +++ b/crates/node/tests/error_propagation_tests.mjs @@ -6,8 +6,8 @@ import assert from 'node:assert/strict'; import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); -const lib = require('../index.js'); -const adaptive = require('../adaptive.js'); +const lib = require('../dist/index.js'); +const adaptive = require('../dist/adaptive.js'); const { GeminiGenerateContentCodec, diff --git a/crates/node/tests/event_metadata_injection_tests.mjs b/crates/node/tests/event_metadata_injection_tests.mjs index d2752348df..f0ea1c9c63 100644 --- a/crates/node/tests/event_metadata_injection_tests.mjs +++ b/crates/node/tests/event_metadata_injection_tests.mjs @@ -14,8 +14,8 @@ process.env.XDG_CONFIG_HOME = configHome; process.on('exit', () => rmSync(configHome, { recursive: true, force: true })); const require = createRequire(import.meta.url); -const lib = require('../index.js'); -const plugin = require('../plugin.js'); +const lib = require('../dist/index.js'); +const plugin = require('../dist/plugin.js'); function capture(name) { const events = []; diff --git a/crates/node/tests/event_sanitizers_tests.mjs b/crates/node/tests/event_sanitizers_tests.mjs index 80c7304c7f..6398f7865e 100644 --- a/crates/node/tests/event_sanitizers_tests.mjs +++ b/crates/node/tests/event_sanitizers_tests.mjs @@ -10,8 +10,8 @@ import path from 'node:path'; import * as pluginHost from './plugin_host_test_helper.mjs'; const require = createRequire(import.meta.url); -const lib = require('../index.js'); -const plugin = require('../plugin.js'); +const lib = require('../dist/index.js'); +const plugin = require('../dist/plugin.js'); function capture(name) { const events = []; diff --git a/crates/node/tests/index_loader_tests.mjs b/crates/node/tests/index_loader_tests.mjs index 1fafb8c9ff..39fcaeb723 100644 --- a/crates/node/tests/index_loader_tests.mjs +++ b/crates/node/tests/index_loader_tests.mjs @@ -4,14 +4,17 @@ import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { createRequire } from 'node:module'; -import { readFileSync as readNodeFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { spawnSync } from 'node:child_process'; +import { copyFileSync, mkdirSync, mkdtempSync, readFileSync as readNodeFileSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; import path from 'node:path'; import vm from 'node:vm'; const require = createRequire(import.meta.url); -const realLib = require('../index.js'); -const indexFilename = new URL('../index.js', import.meta.url); -const indexDir = path.dirname(indexFilename.pathname); +const realLib = require('../binding.js'); +const indexFilename = new URL('../binding.js', import.meta.url); +const indexDir = path.dirname(fileURLToPath(indexFilename)); const indexSource = readNodeFileSync(indexFilename, 'utf8'); const PACKAGE = 'nemo-relay-node'; @@ -309,4 +312,40 @@ describe('index.js loader', () => { new RegExp(`Cannot find module '\\./${BINARY}\\.freebsd-x64\\.node'`), ); }); + + it('propagates loader failures through the public entry point', () => { + const packageRoot = mkdtempSync(path.join(tmpdir(), 'nemo-relay-node-')); + try { + mkdirSync(path.join(packageRoot, 'dist')); + for (const file of ['package.json', 'binding.js', 'dist/index.js']) { + copyFileSync(path.join(indexDir, file), path.join(packageRoot, file)); + } + const script = ` + try { + require(${JSON.stringify(path.join(packageRoot, 'dist', 'index.js'))}); + } catch (error) { + const messages = []; + for (let current = error; current; current = current.cause) { + messages.push(String(current.message ?? current)); + } + process.stdout.write(JSON.stringify(messages)); + process.exitCode = 1; + } + `; + const result = spawnSync(process.execPath, ['--eval', script], { encoding: 'utf8' }); + assert.equal(result.status, 1, result.stderr); + const messages = JSON.parse(result.stdout); + assert.match(messages[0], /Cannot find native binding/); + assert.ok( + messages.some((message) => new RegExp(`Cannot find module '${RegExp.escape(PACKAGE)}-`).test(message)), + `expected a platform package cause, got: ${JSON.stringify(messages)}`, + ); + assert.ok( + messages.some((message) => new RegExp(`Cannot find module '\\./${RegExp.escape(BINARY)}\\.`).test(message)), + `expected a local binary cause, got: ${JSON.stringify(messages)}`, + ); + } finally { + rmSync(packageRoot, { recursive: true, force: true }); + } + }); }); diff --git a/crates/node/tests/llm_tests.mjs b/crates/node/tests/llm_tests.mjs index f1b8258c77..4019a24094 100644 --- a/crates/node/tests/llm_tests.mjs +++ b/crates/node/tests/llm_tests.mjs @@ -10,7 +10,7 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; const require = createRequire(import.meta.url); -const lib = require('../index.js'); +const lib = require('../dist/index.js'); const nodeDir = fileURLToPath(new URL('..', import.meta.url)); const { @@ -236,7 +236,7 @@ describe('LLM execute', () => { stage('JavaScript startup'); const assert = require('node:assert/strict'); const { parentPort } = require('node:worker_threads'); - const lib = require(${JSON.stringify(path.join(nodeDir, 'index.js'))}); + const lib = require(${JSON.stringify(path.join(nodeDir, 'dist', 'index.js'))}); stage('addon loaded'); const request = () => ({ headers: {}, content: { messages: [], model: 'test-model' } }); @@ -1031,7 +1031,7 @@ describe('LLM guardrails', () => { }); it('releases custom stream codec references safely after early garbage collection', () => { - const modulePath = path.join(nodeDir, 'index.js'); + const modulePath = path.join(nodeDir, 'dist', 'index.js'); const script = ` import { createRequire } from 'node:module'; const require = createRequire(${JSON.stringify(path.join(nodeDir, 'package.json'))}); @@ -3405,7 +3405,7 @@ describe('LLM intercepts', () => { }); it('terminating a worker with a pending next() leaves the process running', () => { - const modulePath = JSON.stringify(path.join(nodeDir, 'index.js')); + const modulePath = JSON.stringify(path.join(nodeDir, 'dist', 'index.js')); const script = ` import { Worker } from 'node:worker_threads'; const worker = new Worker( @@ -3444,7 +3444,7 @@ describe('LLM intercepts', () => { }); it('ignored next() and close() rejections stay unhandled', () => { - const modulePath = JSON.stringify(path.join(nodeDir, 'index.js')); + const modulePath = JSON.stringify(path.join(nodeDir, 'dist', 'index.js')); const script = ` import assert from 'node:assert/strict'; import { createRequire } from 'node:module'; @@ -3521,7 +3521,7 @@ describe('LLM intercepts', () => { }); it('a pending next() keeps a collected handle alive until it settles', () => { - const modulePath = JSON.stringify(path.join(nodeDir, 'index.js')); + const modulePath = JSON.stringify(path.join(nodeDir, 'dist', 'index.js')); const script = ` import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); @@ -3601,7 +3601,7 @@ describe('LLM intercepts', () => { }); it('a collected stream handle closes its stream before END', () => { - const modulePath = JSON.stringify(path.join(nodeDir, 'index.js')); + const modulePath = JSON.stringify(path.join(nodeDir, 'dist', 'index.js')); const script = ` import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); @@ -3683,7 +3683,7 @@ describe('LLM intercepts', () => { }); it('a collected stream handle keeps the outcome of its pending close()', () => { - const modulePath = JSON.stringify(path.join(nodeDir, 'index.js')); + const modulePath = JSON.stringify(path.join(nodeDir, 'dist', 'index.js')); const script = ` import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); @@ -3857,7 +3857,7 @@ describe('LLM intercepts', () => { }); it('completed deregistered stream intercepts do not keep Node alive', () => { - const modulePath = JSON.stringify(path.join(nodeDir, 'index.js')); + const modulePath = JSON.stringify(path.join(nodeDir, 'dist', 'index.js')); const script = ` import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); @@ -3895,7 +3895,7 @@ describe('LLM intercepts', () => { }); it('awaiting collectors and finalizers do not starve the runtime', () => { - const modulePath = JSON.stringify(path.join(nodeDir, 'index.js')); + const modulePath = JSON.stringify(path.join(nodeDir, 'dist', 'index.js')); const script = ` import { availableParallelism } from 'node:os'; import { createRequire } from 'node:module'; @@ -3940,7 +3940,7 @@ describe('LLM intercepts', () => { }); it('abandoned streams with a collector and finalizer do not keep Node alive', () => { - const modulePath = JSON.stringify(path.join(nodeDir, 'index.js')); + const modulePath = JSON.stringify(path.join(nodeDir, 'dist', 'index.js')); const script = ` import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); @@ -3973,7 +3973,7 @@ describe('LLM intercepts', () => { }); it('a producer parked on a full buffer does not keep Node alive, in both modes', () => { - const modulePath = JSON.stringify(path.join(nodeDir, 'index.js')); + const modulePath = JSON.stringify(path.join(nodeDir, 'dist', 'index.js')); for (const forwarded of [false, true]) { const script = ` import { createRequire } from 'node:module'; @@ -4095,7 +4095,7 @@ describe('LLM intercepts', () => { }); it('generated request-intercept declarations reference the canonical open optimization type', () => { - const declarations = readFileSync(new URL('../index.d.ts', import.meta.url), 'utf8'); + const declarations = readFileSync(new URL('../binding.d.ts', import.meta.url), 'utf8'); const openKind = "kind: 'input_compression' | 'model_routing' | (string & {})"; assert.equal(declarations.split(openKind).length - 1, 1); @@ -4104,7 +4104,7 @@ describe('LLM intercepts', () => { }); it('generated LLM sanitizer declarations expose directional codec contexts', () => { - const declarations = readFileSync(new URL('../index.d.ts', import.meta.url), 'utf8'); + const declarations = readFileSync(new URL('../binding.d.ts', import.meta.url), 'utf8'); assert.equal( declarations.split('context: LlmSanitizeRequestContext').length - 1, @@ -4122,7 +4122,7 @@ describe('LLM intercepts', () => { }); it('generated streaming LLM declarations expose the producer, collector, and finalizer contracts', () => { - const declarations = readFileSync(new URL('../index.d.ts', import.meta.url), 'utf8'); + const declarations = readFileSync(new URL('../binding.d.ts', import.meta.url), 'utf8'); assert.match( declarations, @@ -4133,7 +4133,7 @@ describe('LLM intercepts', () => { }); it('generated middleware declarations expose Promise-aware callback types', () => { - const declarations = readFileSync(new URL('../index.d.ts', import.meta.url), 'utf8'); + const declarations = readFileSync(new URL('../binding.d.ts', import.meta.url), 'utf8'); const registrations = [ 'registerToolSanitizeRequestGuardrail', 'registerToolSanitizeResponseGuardrail', @@ -4201,12 +4201,12 @@ describe('LLM intercepts', () => { }); it('plugin declarations expose Promise middleware and the implemented stream contract', () => { - const declarations = readFileSync(new URL('../index.d.ts', import.meta.url), 'utf8'); - const pluginDeclarations = readFileSync(new URL('../plugin.d.ts', import.meta.url), 'utf8'); + const declarations = readFileSync(new URL('../binding.d.ts', import.meta.url), 'utf8'); + const pluginDeclarations = readFileSync(new URL('../dist/plugin.d.ts', import.meta.url), 'utf8'); const context = declarations.match(/export interface PluginContext \{[\s\S]*?\n\}/)?.[0]; - assert.ok(context, 'index.d.ts must declare PluginContext'); + assert.ok(context, 'binding.d.ts must declare PluginContext'); - assert.match(pluginDeclarations, /export type \{[\s\S]*?\bPluginContext,[\s\S]*?\} from '\.\/index'/); + assert.match(pluginDeclarations, /export type \{[\s\S]*?\bPluginContext,[\s\S]*?\} from '\.\.\/binding\.js'/); assert.match( declarations, /registerPlugin\([^\n]*register: \(pluginConfig: JsonObject, context: PluginContext\) => void/, @@ -4313,7 +4313,7 @@ describe('process exit with queued codec callbacks', () => { llmCallExecute, registerLlmSanitizeResponseGuardrail, OpenAIChatCodec, - } = require(${JSON.stringify(path.join(nodeDir, 'index.js'))}); + } = require(${JSON.stringify(path.join(nodeDir, 'dist', 'index.js'))}); let release; registerLlmSanitizeResponseGuardrail('exit_window', 10, (response) => new Promise((resolve) => { release = () => resolve(response); diff --git a/crates/node/tests/logging_tests.mjs b/crates/node/tests/logging_tests.mjs index aac9296ae7..4d8e68cddb 100644 --- a/crates/node/tests/logging_tests.mjs +++ b/crates/node/tests/logging_tests.mjs @@ -17,7 +17,7 @@ const loggingEnvironmentNames = [ 'NEMO_RELAY_LOG_CONFIG_PATH', ]; -function requireBinding(loggingEnvironment, source = "require('./index.js')") { +function requireBinding(loggingEnvironment, source = "require('./dist/index.js')") { const environment = { ...process.env }; for (const name of loggingEnvironmentNames) { delete environment[name]; @@ -106,10 +106,10 @@ format = "jsonl" queue_capacity = 16 `, ); - const workerSource = `require(${JSON.stringify(join(packageDirectory, 'index.js'))})`; + const workerSource = `require(${JSON.stringify(join(packageDirectory, 'dist', 'index.js'))})`; const source = ` const { Worker } = require('node:worker_threads'); -const relay = require('./index.js'); +const relay = require('./dist/index.js'); const worker = new Worker(${JSON.stringify(workerSource)}, { eval: true, }); @@ -154,7 +154,7 @@ queue_capacity = 16 `, ); const source = ` -const relay = require('./index.js'); +const relay = require('./dist/index.js'); relay.trace('trace message', 'binding', { ordinal: 0 }); relay.debug('debug message', 'binding', { ordinal: 1 }); relay.info('info message', 'binding', { ordinal: 2, nested: { ok: true } }); @@ -183,7 +183,7 @@ relay.info('default target'); it('rejects non-object structured fields', () => { const result = requireBinding( { NEMO_RELAY_LOG: 'trace' }, - "require('./index.js').info('invalid fields', 'binding', ['not-an-object'])", + "require('./dist/index.js').info('invalid fields', 'binding', ['not-an-object'])", ); assert.notEqual(result.status, 0); diff --git a/crates/node/tests/model_pricing_tests.mjs b/crates/node/tests/model_pricing_tests.mjs index a62aa95413..179cc1ce40 100644 --- a/crates/node/tests/model_pricing_tests.mjs +++ b/crates/node/tests/model_pricing_tests.mjs @@ -9,8 +9,8 @@ import { tmpdir } from 'node:os'; import path from 'node:path'; const require = createRequire(import.meta.url); -const plugin = require('../plugin.js'); -const pricing = require('../model_pricing.js'); +const plugin = require('../dist/plugin.js'); +const pricing = require('../dist/model_pricing.js'); function pricedEntry(overrides = {}) { return pricing.catalogEntry({ diff --git a/crates/node/tests/observability_plugin_tests.mjs b/crates/node/tests/observability_plugin_tests.mjs index 0bbca45530..5b1e6f9f32 100644 --- a/crates/node/tests/observability_plugin_tests.mjs +++ b/crates/node/tests/observability_plugin_tests.mjs @@ -10,9 +10,9 @@ import { join } from 'node:path'; import * as pluginHost from './plugin_host_test_helper.mjs'; const require = createRequire(import.meta.url); -const plugin = require('../plugin.js'); -const observability = require('../observability.js'); -const { ScopeType, pushScope, popScope, event } = require('../index.js'); +const plugin = require('../dist/plugin.js'); +const observability = require('../dist/observability.js'); +const { ScopeType, pushScope, popScope, event } = require('../dist/index.js'); function tempDir(prefix) { return mkdtempSync(join(tmpdir(), `nemo-relay-${prefix}-`)); diff --git a/crates/node/tests/openinference_tests.mjs b/crates/node/tests/openinference_tests.mjs index 1b0797041d..8952cda2e3 100644 --- a/crates/node/tests/openinference_tests.mjs +++ b/crates/node/tests/openinference_tests.mjs @@ -7,7 +7,7 @@ import { createRequire } from 'node:module'; import { startCollector } from '../../../scripts/test-support/otel_test_utils.mjs'; const require = createRequire(import.meta.url); -const { OpenTelemetrySubscriber, ScopeType, pushScope, popScope, event } = require('../index.js'); +const { OpenTelemetrySubscriber, ScopeType, pushScope, popScope, event } = require('../dist/index.js'); function unique(prefix) { return `${prefix}_${Date.now()}_${Math.random().toString(16).slice(2)}`; diff --git a/crates/node/tests/otel_tests.mjs b/crates/node/tests/otel_tests.mjs index 140186c848..3fd8778217 100644 --- a/crates/node/tests/otel_tests.mjs +++ b/crates/node/tests/otel_tests.mjs @@ -23,7 +23,7 @@ const { popScope, event, metric, -} = require('../index.js'); +} = require('../dist/index.js'); function uniqueId(prefix) { return `${prefix}_${Date.now()}_${Math.random().toString(16).slice(2)}`; diff --git a/crates/node/tests/pii_redaction_tests.mjs b/crates/node/tests/pii_redaction_tests.mjs index 92001ad65c..a03235fab6 100644 --- a/crates/node/tests/pii_redaction_tests.mjs +++ b/crates/node/tests/pii_redaction_tests.mjs @@ -6,8 +6,8 @@ import assert from 'node:assert/strict'; import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); -const plugin = require('../plugin.js'); -const piiRedaction = require('../pii_redaction.js'); +const plugin = require('../dist/plugin.js'); +const piiRedaction = require('../dist/pii_redaction.js'); describe('pii_redaction plugin helpers', () => { it('builds defaults and plugin component shape', () => { diff --git a/crates/node/tests/plugin_exact_validation.mjs b/crates/node/tests/plugin_exact_validation.mjs index 9276cad58e..d391748dc8 100644 --- a/crates/node/tests/plugin_exact_validation.mjs +++ b/crates/node/tests/plugin_exact_validation.mjs @@ -6,7 +6,7 @@ import { test } from 'node:test'; import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); -const plugin = require('../plugin.js'); +const plugin = require('../dist/plugin.js'); test('exact validation rejects host diagnostics instead of discarding them', () => { for (const diagnostics of [null, true, {}, { enabled: true }, { enabled: 'invalid' }]) { @@ -21,7 +21,6 @@ test('exact validation accepts static plugin configuration', () => { assert.deepEqual(plugin.validateExact(plugin.defaultConfig()).dynamic_plugins, []); }); - test('all built-in kinds are available without component helpers', () => { const kinds = new Set(plugin.listKinds()); for (const kind of ['observability', 'pricing', 'resource_metrics', 'adaptive', 'pii_redaction']) { diff --git a/crates/node/tests/plugin_host_test_helper.mjs b/crates/node/tests/plugin_host_test_helper.mjs index 1959f9d619..1d29126fae 100644 --- a/crates/node/tests/plugin_host_test_helper.mjs +++ b/crates/node/tests/plugin_host_test_helper.mjs @@ -4,7 +4,7 @@ import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); -const plugin = require('../plugin.js'); +const plugin = require('../dist/plugin.js'); let activation; diff --git a/crates/node/tests/public_diagnostics_api_fixture.ts b/crates/node/tests/public_diagnostics_api_fixture.ts index ff170ed622..554c82c7ac 100644 --- a/crates/node/tests/public_diagnostics_api_fixture.ts +++ b/crates/node/tests/public_diagnostics_api_fixture.ts @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { reader, type DiagnosticQuery, type DiagnosticSnapshot } from '../diagnostics'; +import { reader, type DiagnosticQuery, type DiagnosticSnapshot } from '../dist/diagnostics.js'; const query: DiagnosticQuery = { componentKey: 'logging', diff --git a/crates/node/tests/public_entry_points_fixture.ts b/crates/node/tests/public_entry_points_fixture.ts new file mode 100644 index 0000000000..a2bc4f743e --- /dev/null +++ b/crates/node/tests/public_entry_points_fixture.ts @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import type { ScopeStack } from 'nemo-relay-node'; +import type { Codec } from 'nemo-relay-node/typed'; +import type { ComponentSpec as PluginComponentSpec } from 'nemo-relay-node/plugin'; +import type { ComponentSpec as AdaptiveComponentSpec } from 'nemo-relay-node/adaptive'; +import type { AtofConfig } from 'nemo-relay-node/observability'; +import type { ComponentSpec as PiiComponentSpec } from 'nemo-relay-node/pii_redaction'; +import type { TokenPricingRates } from 'nemo-relay-node/model_pricing'; +import type { DiagnosticsReader } from 'nemo-relay-node/diagnostics'; + +export type EntryPoints = [ + ScopeStack, + Codec, + PluginComponentSpec, + AdaptiveComponentSpec, + AtofConfig, + PiiComponentSpec, + TokenPricingRates, + DiagnosticsReader, +]; diff --git a/crates/node/tests/public_event_metadata_api_fixture.ts b/crates/node/tests/public_event_metadata_api_fixture.ts index 56a17b8164..0b6d294e33 100644 --- a/crates/node/tests/public_event_metadata_api_fixture.ts +++ b/crates/node/tests/public_event_metadata_api_fixture.ts @@ -1,8 +1,8 @@ // SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { registerEventMetadataInjector, scopeRegisterEventMetadataInjector } from '../index.js'; -import type { EventMetadata, PluginContext } from '../plugin.js'; +import { registerEventMetadataInjector, scopeRegisterEventMetadataInjector } from '../dist/index.js'; +import type { EventMetadata, PluginContext } from '../dist/plugin.js'; const acceptedMetadata: EventMetadata = { 'fixture.string': 'value', diff --git a/crates/node/tests/public_json_api_fixture.ts b/crates/node/tests/public_json_api_fixture.ts index 4bd8d0b6f1..fef69d4173 100644 --- a/crates/node/tests/public_json_api_fixture.ts +++ b/crates/node/tests/public_json_api_fixture.ts @@ -14,10 +14,10 @@ import { type JsonValue, type LlmRequestShape, type ScopeHandle, -} from '../index.js'; -import type { JsonValue as TypedJsonValue } from '../typed.js'; +} from '../dist/index.js'; +import type { JsonValue as TypedJsonValue } from '../dist/typed.js'; -type AssertEqual = [T] extends [U] ? ([U] extends [T] ? true : false) : false; +type AssertEqual = (() => V extends T ? 1 : 2) extends () => V extends U ? 1 : 2 ? true : false; const jsonIsJsonValue: AssertEqual = true; const typedSharesJson: AssertEqual = true; diff --git a/crates/node/tests/public_json_compatible_api_fixture.ts b/crates/node/tests/public_json_compatible_api_fixture.ts index 10d5d01458..ecf89b0732 100644 --- a/crates/node/tests/public_json_compatible_api_fixture.ts +++ b/crates/node/tests/public_json_compatible_api_fixture.ts @@ -41,11 +41,11 @@ import { type ToolExecutionResult, type ToolExecutionResultCompatible, type ToolHandle, -} from '../index.js'; -import * as plugin from '../plugin.js'; -import { typedLlmExecute, typedToolExecute, type LlmCodec, type LlmResponseCodec } from '../typed.js'; +} from '../dist/index.js'; +import * as plugin from '../dist/plugin.js'; +import { typedLlmExecute, typedToolExecute, type LlmCodec, type LlmResponseCodec } from '../dist/typed.js'; -type AssertEqual = [T] extends [U] ? ([U] extends [T] ? true : false) : false; +type AssertEqual = (() => V extends T ? 1 : 2) extends () => V extends U ? 1 : 2 ? true : false; interface SdkParams { model: string; @@ -157,6 +157,9 @@ declare const undefinedMeta: UndefinedMeta; declare const withMethod: WithMethod; declare const withUndefined: WithUndefined; declare const paramsOrList: SdkParams | string[]; +declare function toJsonValue(value: SdkResponse): JsonValue; +declare function parseSdkParams(data: Json): SdkParams; +declare function parseSdkResponse(data: Json): SdkResponse; async function* chunks(): AsyncIterable { yield { id: '1', delta: { content: 'x' } }; } @@ -180,7 +183,7 @@ toolCall('empty', {}); toolCall('optional', options, null, null, options); llmCall('optional', { headers: options, content: options }); llmCall('tools', { headers: {}, content: paramsWithTools }); -llmCall('tools-factory', LlmRequest.from({}, paramsWithTools)); +llmCall('tools-instance', new LlmRequest({}, paramsWithTools)); toolCall('unknown-record', unknownRecord); llmCallExecuteAsync('unknown-record', shape, async () => unknownRecord); toolCall('empty-typed', emptyObject); @@ -195,8 +198,9 @@ toolCallExecuteAsync('extra-json', params, async () => ({ result: 1, extra: 2 }) llmCall('object', { headers: {}, content: params }); llmCall('headers', { headers, content: null }); llmCall('instance', new LlmRequest({}, { x: 1 })); -llmCall('factory', LlmRequest.from(headers, params)); -const built: LlmRequestShape = LlmRequest.from({}, params); +llmCall('instance-typed', new LlmRequest(headers, params)); +const built = new LlmRequest({}, params); +llmCall('built', built); info('fields', null, settings); info('json-fields', null, record); llmCall('extra-json', { headers: {}, content: null, extra: 1 }); @@ -294,15 +298,15 @@ const typedResponse: Promise = typedLlmExecute( 'typed', { headers, content: params }, async (request): Promise => ({ id: JSON.stringify(request.content), choices: [] }), - { toJson: (value) => value, fromJson: (value) => value }, + { toJson: (value) => value, fromJson: parseSdkResponse }, { codec, responseCodec, data: markData }, ); const typedTool: Promise<{ result: SdkResponse }> = typedToolExecute( 'typed', params, async () => ({ result: sdk }), - { toJson: (value) => value, fromJson: (value) => value }, - { toJson: (value) => value, fromJson: (value) => value }, + { toJson: (value) => value, fromJson: parseSdkParams }, + { toJson: toJsonValue, fromJson: parseSdkResponse }, { metadata: markData }, ); const open = plugin.ComponentSpec('example.add-tag', settings); @@ -440,11 +444,11 @@ plugin.ComponentSpec('example.add-tag', [1]); // @ts-expect-error an open kind rejects functions plugin.ComponentSpec('example.add-tag', () => 1); // @ts-expect-error headers must be an object -LlmRequest.from('x', null); +new LlmRequest('x', null); // @ts-expect-error content must be JSON -LlmRequest.from({}, new Map()); +new LlmRequest({}, new Map()); // @ts-expect-error a union with an array member is not a JSON object -LlmRequest.from(paramsOrList, null); +new LlmRequest(paramsOrList, null); // @ts-expect-error a union with an array member is not a JSON object plugin.ComponentSpec('example.add-tag', paramsOrList); // @ts-expect-error log fields must be a JSON object diff --git a/crates/node/tests/public_model_pricing_api_fixture.ts b/crates/node/tests/public_model_pricing_api_fixture.ts index 97f00b0dda..f41b9097ec 100644 --- a/crates/node/tests/public_model_pricing_api_fixture.ts +++ b/crates/node/tests/public_model_pricing_api_fixture.ts @@ -15,8 +15,8 @@ import { type ModelPricing, type PricingCatalog, type PricingConfig, -} from '../model_pricing.js'; -import type { JsonObject } from '../index.js'; +} from '../dist/model_pricing.js'; +import type { JsonObject } from '../dist/index.js'; const entry: ModelPricing = catalogEntry({ provider: 'openai', diff --git a/crates/node/tests/public_observability_api_fixture.ts b/crates/node/tests/public_observability_api_fixture.ts index e7c66fed59..e9461d3859 100644 --- a/crates/node/tests/public_observability_api_fixture.ts +++ b/crates/node/tests/public_observability_api_fixture.ts @@ -12,8 +12,12 @@ import { OpenTelemetryLogSubscriber, OpenTelemetryMetricSubscriber, OpenTelemetrySubscriber, -} from '../index.js'; -import { openTelemetrySignalEndpoint, openTelemetryLogConfig, openTelemetryMetricConfig } from '../observability.js'; +} from '../dist/index.js'; +import { + openTelemetrySignalEndpoint, + openTelemetryLogConfig, + openTelemetryMetricConfig, +} from '../dist/observability.js'; const dataSchema: DataSchema = { name: 'example.fixture', version: '1' }; diff --git a/crates/node/tests/public_pii_redaction_api_fixture.ts b/crates/node/tests/public_pii_redaction_api_fixture.ts index cef1378b7b..84482ed3cc 100644 --- a/crates/node/tests/public_pii_redaction_api_fixture.ts +++ b/crates/node/tests/public_pii_redaction_api_fixture.ts @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { builtinConfig, type BuiltinConfig } from '../pii_redaction.js'; +import { builtinConfig, type BuiltinConfig } from '../dist/pii_redaction.js'; const trajectoryContext: BuiltinConfig = { preset: 'trajectory_context', diff --git a/crates/node/tests/public_plugin_api_fixture.ts b/crates/node/tests/public_plugin_api_fixture.ts index 2b0b4bbbd1..a26092bb5c 100644 --- a/crates/node/tests/public_plugin_api_fixture.ts +++ b/crates/node/tests/public_plugin_api_fixture.ts @@ -1,26 +1,26 @@ // SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { ComponentSpec as adaptiveComponentSpec, defaultConfig as adaptiveDefaultConfig } from '../adaptive.js'; -import { ComponentSpec as pricingComponentSpec } from '../model_pricing.js'; -import { ComponentSpec as piiComponentSpec, defaultConfig as piiDefaultConfig } from '../pii_redaction.js'; +import { ComponentSpec as adaptiveComponentSpec, defaultConfig as adaptiveDefaultConfig } from '../dist/adaptive.js'; +import { ComponentSpec as pricingComponentSpec } from '../dist/model_pricing.js'; +import { ComponentSpec as piiComponentSpec, defaultConfig as piiDefaultConfig } from '../dist/pii_redaction.js'; import { listRuntimeRegistrations, registerConditionalMiddlewareGuardrail, RuntimeRegistrationKind, type JsonObject, type RuntimeRegistrationKindName, -} from '../index.js'; -import * as plugin from '../plugin.js'; +} from '../dist/index.js'; +import * as plugin from '../dist/plugin.js'; import type { ComponentKind, ComponentSpecFor, KnownComponentSpec, OpenComponentSpec, PluginConfig, -} from '../plugin.js'; +} from '../dist/plugin.js'; -type AssertEqual = [T] extends [U] ? ([U] extends [T] ? true : false) : false; +type AssertEqual = (() => V extends T ? 1 : 2) extends () => V extends U ? 1 : 2 ? true : false; const kinds: AssertEqual< ComponentKind, @@ -89,7 +89,6 @@ export const checked = [ kinds, pricingKind, openKind, - openTag, config, subscriberKind, everyKind, diff --git a/crates/node/tests/public_resource_metrics_api_fixture.ts b/crates/node/tests/public_resource_metrics_api_fixture.ts index c94f76749a..85df573ab4 100644 --- a/crates/node/tests/public_resource_metrics_api_fixture.ts +++ b/crates/node/tests/public_resource_metrics_api_fixture.ts @@ -19,7 +19,7 @@ import { UtilizationUnit, CountUnit, type ResourceOperatingSystem, -} from '../index.js'; +} from '../dist/index.js'; export const snapshotPromise: Promise = collectResourceMetrics(); declare const snapshot: ResourceMetricsSnapshot; diff --git a/crates/node/tests/resource_metrics_tests.mjs b/crates/node/tests/resource_metrics_tests.mjs index e0108b5210..04c420424c 100644 --- a/crates/node/tests/resource_metrics_tests.mjs +++ b/crates/node/tests/resource_metrics_tests.mjs @@ -6,8 +6,8 @@ import { describe, it } from 'node:test'; import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); -const relay = require('../index.js'); -const plugin = require('../plugin.js'); +const relay = require('../dist/index.js'); +const plugin = require('../dist/plugin.js'); const config = (resourceConfig = {}) => ({ version: 1, diff --git a/crates/node/tests/runtime_registrations_tests.mjs b/crates/node/tests/runtime_registrations_tests.mjs index bc439e40f9..89f6bb259f 100644 --- a/crates/node/tests/runtime_registrations_tests.mjs +++ b/crates/node/tests/runtime_registrations_tests.mjs @@ -6,7 +6,7 @@ import assert from 'node:assert/strict'; import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); -const lib = require('../index.js'); +const lib = require('../dist/index.js'); describe('conditional middleware guardrails', () => { it('discovers and dynamically toggles an existing registration', async () => { diff --git a/crates/node/tests/scope_local_tests.mjs b/crates/node/tests/scope_local_tests.mjs index e9c04a0c3c..11b5ef23f3 100644 --- a/crates/node/tests/scope_local_tests.mjs +++ b/crates/node/tests/scope_local_tests.mjs @@ -6,7 +6,7 @@ import assert from 'node:assert/strict'; import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); -const lib = require('../index.js'); +const lib = require('../dist/index.js'); const EVENT_DELIVERY_TIMEOUT_MS = process.env.CI ? 2000 : 200; diff --git a/crates/node/tests/scope_tests.mjs b/crates/node/tests/scope_tests.mjs index ee54d47277..34ccb616b7 100644 --- a/crates/node/tests/scope_tests.mjs +++ b/crates/node/tests/scope_tests.mjs @@ -9,7 +9,7 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; const require = createRequire(import.meta.url); -const lib = require('../index.js'); +const lib = require('../dist/index.js'); const nodeDir = fileURLToPath(new URL('..', import.meta.url)); const { @@ -43,7 +43,7 @@ function runSubscriberFailureChild({ callback, registration = 'global' }) { plugin: [ "process.chdir(require('node:os').tmpdir());", "process.env.XDG_CONFIG_HOME = require('node:fs').mkdtempSync(require('node:path').join(require('node:os').tmpdir(), 'nemo-relay-node-test-'));", - 'globalThis.plugin = require(' + JSON.stringify(path.join(nodeDir, 'plugin.js')) + ');', + 'globalThis.plugin = require(' + JSON.stringify(path.join(nodeDir, 'dist', 'plugin.js')) + ');', "globalThis.pluginKind = 'node.test.subscriber-failure';", 'plugin.register(pluginKind, {', ' register(_config, context) {', @@ -60,7 +60,7 @@ function runSubscriberFailureChild({ callback, registration = 'global' }) { plugin: 'await pluginHost.close(); plugin.deregister(pluginKind);', }[registration]; const script = ` - const lib = require(${JSON.stringify(path.join(nodeDir, 'index.js'))}); + const lib = require(${JSON.stringify(path.join(nodeDir, 'dist', 'index.js'))}); const { ScopeType, clearLastCallbackError, deregisterSubscriber, event, flushSubscribers, getLastCallbackError, pushScope, popScope, registerSubscriber, diff --git a/crates/node/tests/shared_runtime_tests.mjs b/crates/node/tests/shared_runtime_tests.mjs index 0697606b8d..010fd3cfe6 100644 --- a/crates/node/tests/shared_runtime_tests.mjs +++ b/crates/node/tests/shared_runtime_tests.mjs @@ -6,7 +6,7 @@ import assert from 'node:assert/strict'; import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); -const lib = require('../index.js'); +const lib = require('../dist/index.js'); describe('runtime surface', () => { it('does not expose manual runtime control helpers', () => { diff --git a/crates/node/tests/timestamp_tests.mjs b/crates/node/tests/timestamp_tests.mjs index ee228894cc..e4bf92a511 100644 --- a/crates/node/tests/timestamp_tests.mjs +++ b/crates/node/tests/timestamp_tests.mjs @@ -18,7 +18,7 @@ const { registerSubscriber, toolCall, toolCallEnd, -} = require('../index.js'); +} = require('../dist/index.js'); const timestampMicros = (value, micros = 0) => Date.parse(value) * 1000 + micros; const parseTimestampMicros = (value) => { diff --git a/crates/node/tests/tools_tests.mjs b/crates/node/tests/tools_tests.mjs index e4f6db2ad7..0e502f4a6d 100644 --- a/crates/node/tests/tools_tests.mjs +++ b/crates/node/tests/tools_tests.mjs @@ -7,7 +7,7 @@ import { readFileSync } from 'node:fs'; import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); -const lib = require('../index.js'); +const lib = require('../dist/index.js'); const { pushScope, @@ -479,14 +479,14 @@ describe('Tool execute', () => { }); it('generated execute declarations expose an additive trailing toolCallId', () => { - const declarations = readFileSync(new URL('../index.d.ts', import.meta.url), 'utf8'); + const declarations = readFileSync(new URL('../binding.d.ts', import.meta.url), 'utf8'); assert.match(declarations, /toolCallExecute<[^\n]*metadata\?: TMetadata \| null,[^\n]*toolCallId\?: string/); assert.match(declarations, /toolCallExecuteAsync<[^\n]*metadata\?: TMetadata \| null,[^\n]*toolCallId\?: string/); }); it('generated execution-intercept declarations use context and next parameters', () => { - const declarations = readFileSync(new URL('../index.d.ts', import.meta.url), 'utf8'); + const declarations = readFileSync(new URL('../binding.d.ts', import.meta.url), 'utf8'); assert.match( declarations, diff --git a/crates/node/tests/typed_tests.mjs b/crates/node/tests/typed_tests.mjs index b287a7f30a..28025f6b09 100644 --- a/crates/node/tests/typed_tests.mjs +++ b/crates/node/tests/typed_tests.mjs @@ -7,10 +7,10 @@ import { createRequire } from 'node:module'; import * as pluginHost from './plugin_host_test_helper.mjs'; const require = createRequire(import.meta.url); -const lib = require('../index.js'); -const { typedToolExecute, typedLlmExecute, typedLlmStreamExecute, JsonPassthrough } = require('../typed.js'); -const adaptive = require('../adaptive.js'); -const plugin = require('../plugin.js'); +const lib = require('../dist/index.js'); +const { typedToolExecute, typedLlmExecute, typedLlmStreamExecute, JsonPassthrough } = require('../dist/typed.js'); +const adaptive = require('../dist/adaptive.js'); +const plugin = require('../dist/plugin.js'); const TOOL_ATTR_LOCAL = 0b01; @@ -179,7 +179,7 @@ describe('JsonPassthrough', () => { describe('adaptive typed helpers', () => { it('keeps adaptive helpers out of typed.js', () => { - assert.equal('defaultAdaptiveConfig' in require('../typed.js'), false); + assert.equal('defaultAdaptiveConfig' in require('../dist/typed.js'), false); assert.equal(typeof adaptive.defaultConfig, 'function'); }); diff --git a/crates/node/tests/types_tests.mjs b/crates/node/tests/types_tests.mjs index 0d5ae40619..249d47a35a 100644 --- a/crates/node/tests/types_tests.mjs +++ b/crates/node/tests/types_tests.mjs @@ -8,7 +8,7 @@ import { createRequire } from 'node:module'; import { fileURLToPath } from 'node:url'; const require = createRequire(import.meta.url); -const lib = require('../index.js'); +const lib = require('../dist/index.js'); const { ScopeType, LlmRequest, ScopeStack } = lib; @@ -43,34 +43,34 @@ describe('Type constants', () => { assert.equal(ScopeType.Unknown, 10); }); + const tsc = fileURLToPath(new URL('../../../node_modules/typescript/bin/tsc', import.meta.url)); + const typecheck = (args) => + execFileSync(process.execPath, [tsc, '--noEmit', '--strict', '--target', 'ES2022', ...args], { + cwd: fileURLToPath(new URL('..', import.meta.url)), + stdio: 'pipe', + }); + it('type-checks the public API fixtures', () => { - execFileSync( - process.execPath, - [ - fileURLToPath(new URL('../../../node_modules/typescript/bin/tsc', import.meta.url)), - '--noEmit', - '--strict', - '--target', - 'ES2022', - '--module', - 'NodeNext', - '--moduleResolution', - 'NodeNext', - 'tests/public_observability_api_fixture.ts', - 'tests/public_event_metadata_api_fixture.ts', - 'tests/public_resource_metrics_api_fixture.ts', - 'tests/public_diagnostics_api_fixture.ts', - 'tests/public_pii_redaction_api_fixture.ts', - 'tests/public_model_pricing_api_fixture.ts', - 'tests/public_json_api_fixture.ts', - 'tests/public_plugin_api_fixture.ts', - 'tests/public_json_compatible_api_fixture.ts', - ], - { - cwd: fileURLToPath(new URL('..', import.meta.url)), - stdio: 'pipe', - }, - ); + typecheck([ + '--module', + 'NodeNext', + '--moduleResolution', + 'NodeNext', + 'tests/public_observability_api_fixture.ts', + 'tests/public_event_metadata_api_fixture.ts', + 'tests/public_resource_metrics_api_fixture.ts', + 'tests/public_diagnostics_api_fixture.ts', + 'tests/public_pii_redaction_api_fixture.ts', + 'tests/public_model_pricing_api_fixture.ts', + 'tests/public_json_api_fixture.ts', + 'tests/public_plugin_api_fixture.ts', + 'tests/public_json_compatible_api_fixture.ts', + 'tests/public_entry_points_fixture.ts', + ]); + }); + + it('resolves every entry point under node10 module resolution', () => { + typecheck(['--module', 'CommonJS', '--moduleResolution', 'Node10', 'tests/public_entry_points_fixture.ts']); }); }); @@ -359,7 +359,7 @@ describe('GeminiGenerateContentCodec', () => { '-e', ` const assert = require('node:assert/strict'); - const { GeminiGenerateContentCodec } = require('./index.js'); + const { GeminiGenerateContentCodec } = require('./dist/index.js'); const codec = new GeminiGenerateContentCodec(); const response = { candidates: [{ content: { parts: [{ text: 'hi' }] } }], diff --git a/crates/node/ts/adaptive.ts b/crates/node/ts/adaptive.ts new file mode 100644 index 0000000000..6bece8b2ee --- /dev/null +++ b/crates/node/ts/adaptive.ts @@ -0,0 +1,488 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import type { + CacheRequestFacts, + CacheRequestFactsOptions, + CacheTelemetryEvent, + CacheTelemetryEventOptions, + ConfigReport, + Json, + ScopeHandle, +} from '../binding.js'; +import * as lib from './index.js'; +import type { ConfigPolicy } from './plugin.js'; + +export type { + AgentIdentity, + CacheRequestFacts, + CacheRequestFactsOptions, + CacheTelemetryEvent, + CacheTelemetryEventOptions, + CacheUsage, + CacheWriteTokenCounts, + ConfigDiagnostic, + ConfigReport, +} from '../binding.js'; +export type { ConfigPolicy } from './plugin.js'; + +/** Response-cache key strategies. */ +export const ResponseCacheKeyStrategy: { readonly ExactRequest: 'exact_request'; readonly Logical: 'logical' } = + Object.freeze({ + ExactRequest: 'exact_request', + Logical: 'logical', + } as const); +export type ResponseCacheKeyStrategy = (typeof ResponseCacheKeyStrategy)[keyof typeof ResponseCacheKeyStrategy]; + +/** Adaptive state backend. */ +export interface BackendSpec { + kind: string; + config?: Record; +} + +/** Adaptive state configuration. */ +export interface StateConfig { + backend: BackendSpec; +} + +/** Adaptive telemetry settings. */ +export interface TelemetryConfig { + subscriber_name?: string; + learners?: string[]; +} + +/** Adaptive hints injection settings. */ +export interface AdaptiveHintsConfig { + priority?: number; + break_chain?: boolean; + inject_header?: boolean; + inject_body_path?: string; +} + +/** Adaptive tool scheduling settings. */ +export interface ToolParallelismConfig { + priority?: number; + mode?: 'observe_only' | 'inject_hints' | 'schedule' | (string & {}); +} + +/** Prompt-stability thresholds for the cache governor. */ +export interface AcgStabilityThresholds { + stable_threshold?: number; + semi_stable_threshold?: number; + min_observations_for_full_confidence?: number; +} + +/** Cache governor settings. */ +export interface AcgConfig { + provider?: 'anthropic' | 'openai' | 'passthrough' | (string & {}); + observation_window?: number; + priority?: number; + stability_thresholds?: AcgStabilityThresholds; +} + +/** LLM response and tool-result cache settings. */ +export interface ResponseCacheConfig { + ttlSeconds?: number; + /** Required cache trust domain; one cache must not span untrusted tenants or upstreams. */ + namespace?: string; + priority?: number; + bypassRate?: number; + cacheNondeterministic?: boolean; + keyStrategy?: ResponseCacheKeyStrategy; + headerAllowlist?: string[]; + backend?: BackendSpec; + /** Tool-result cache. */ + tools?: ToolCacheConfig; +} + +export interface ResponseCachePluginConfig { + ttl_seconds?: number; + /** Required cache trust domain. */ + namespace?: string; + priority?: number; + bypass_rate?: number; + cache_nondeterministic?: boolean; + key_strategy?: ResponseCacheKeyStrategy; + header_allowlist?: string[]; + backend?: BackendSpec; + tools?: ToolCachePluginConfig; +} + +/** Shared tool policy; omitted TTL and bypass rate use response-cache values. */ +export interface ToolClass { + cacheable?: boolean; + ttlSeconds?: number; + bypassRate?: number; + toolVersion?: string; + argSkip?: string[]; + /** Tool names, or `prefix*`, `*suffix`, or `*contains*` patterns. */ + members?: string[]; +} + +/** Per-tool override; `argSkip: []` clears the class list. */ +export interface ToolOverride { + cacheable?: boolean; + ttlSeconds?: number; + bypassRate?: number; + toolVersion?: string; + argSkip?: string[]; +} + +/** Caching for tools that are read-only and stable for their TTL. */ +export interface ToolCacheConfig { + enabled?: boolean; + /** Tool execution-intercept priority, default 150. */ + priority?: number; + /** Whether error results may be cached, default false. */ + cacheErrors?: boolean; + default?: ToolClass; + classes?: Record; + /** Tool names, or `prefix*`, `*suffix`, or `*contains*` patterns. */ + overrides?: Record; +} + +export type ToolClassPluginConfig = Omit & { + ttl_seconds?: number; + bypass_rate?: number; + tool_version?: string; + arg_skip?: string[]; +}; + +export type ToolOverridePluginConfig = Omit & { + ttl_seconds?: number; + bypass_rate?: number; + tool_version?: string; + arg_skip?: string[]; +}; + +export type ToolCachePluginConfig = Omit & { + cache_errors?: boolean; + default?: ToolClassPluginConfig; + classes?: Record; + overrides?: Record; +}; + +/** Adaptive component config. */ +export interface Config { + version?: number; + agent_id?: string; + state?: StateConfig; + telemetry?: TelemetryConfig; + adaptive_hints?: AdaptiveHintsConfig; + tool_parallelism?: ToolParallelismConfig; + acg?: AcgConfig; + responseCache?: ResponseCacheConfig; + policy?: ConfigPolicy; +} + +export type AdaptivePluginConfig = Omit & { + response_cache?: ResponseCachePluginConfig; +}; + +/** Adaptive component with kind `adaptive`. */ +export interface ComponentSpec { + kind: 'adaptive'; + enabled?: boolean; + config: AdaptivePluginConfig; +} + +export const ADAPTIVE_PLUGIN_KIND = 'adaptive'; + +/** + * Creates a default adaptive config. + * + * The config has `version` 1 and no sections. Add state, telemetry, and + * scheduler sections to it. + * + * @returns An adaptive config with no sections. + * @remarks The config does nothing until it is wrapped with `ComponentSpec` + * and activated. + */ +export function defaultConfig(): Config { + return { + version: 1, + }; +} + +/** + * Creates a backend spec that keeps adaptive state in process memory. + * + * The state is private to the process and is lost when it exits. + * + * @returns A backend spec with kind `in_memory`. + * @remarks Use `redisBackend` when several processes must share state. + */ +export function inMemoryBackend(): BackendSpec { + return { + kind: 'in_memory', + config: {}, + }; +} + +/** + * Creates a backend spec that keeps adaptive state in Redis. + * + * Processes that use the same Redis instance and key prefix share the state. + * + * @param url - Redis connection URL. + * @param keyPrefix - Prefix for every key the backend writes. + * @returns A backend spec with kind `redis`. + * @remarks `keyPrefix` defaults to `nemo_relay:`. + */ +export function redisBackend(url: string, keyPrefix: string = 'nemo_relay:'): BackendSpec { + return { + kind: 'redis', + config: { + url, + key_prefix: keyPrefix, + }, + }; +} + +/** + * Creates adaptive telemetry settings. + * + * Fields in `config` replace the defaults; fields left out keep them. + * + * @param config - Telemetry settings to set. + * @returns Complete telemetry settings. + * @remarks `learners` defaults to an empty list. + */ +export function telemetryConfig(config: TelemetryConfig = {}): TelemetryConfig { + return { + learners: [], + ...config, + }; +} + +/** + * Creates adaptive hint-injection settings. + * + * Fields in `config` replace the defaults; fields left out keep them. + * + * @param config - Hint-injection settings to set. + * @returns Complete hint-injection settings. + * @remarks By default the injector runs at priority 100, lets the chain + * continue, and writes hints to `nvext.agent_hints`. + */ +export function adaptiveHintsConfig(config: AdaptiveHintsConfig = {}): AdaptiveHintsConfig { + return { + priority: 100, + break_chain: false, + inject_header: true, + inject_body_path: 'nvext.agent_hints', + ...config, + }; +} + +/** + * Creates adaptive tool-scheduling settings. + * + * Fields in `config` replace the defaults; fields left out keep them. + * + * @param config - Tool-scheduling settings to set. + * @returns Complete tool-scheduling settings. + * @remarks The default mode `observe_only` records recommendations and leaves + * execution unchanged. + */ +export function toolParallelismConfig(config: ToolParallelismConfig = {}): ToolParallelismConfig { + return { + priority: 100, + mode: 'observe_only', + ...config, + }; +} + +/** + * Creates adaptive cache governor settings. + * + * Fields in `config` replace the defaults; fields left out keep them. + * + * @param config - Cache governor settings to set. + * @returns Complete cache governor settings. + * @remarks Thresholds in `stability_thresholds` default one by one, so one + * threshold can be set without the others. + */ +export function acgConfig(config: AcgConfig = {}): AcgConfig { + const { stability_thresholds, ...rest } = config; + return { + provider: 'passthrough', + observation_window: 100, + priority: 50, + stability_thresholds: { + stable_threshold: 0.95, + semi_stable_threshold: 0.5, + min_observations_for_full_confidence: 20, + ...stability_thresholds, + }, + ...rest, + }; +} + +/** + * Creates response-cache settings. + * + * Fields in `config` replace the defaults; fields left out keep them. The + * result is one section of an adaptive config, not a plugin kind. + * + * @param config - Response-cache settings to set. + * @returns Complete response-cache settings. + * @remarks The defaults are an in-memory backend, a `bypassRate` of 0, no + * caching of nondeterministic requests, and an empty `namespace`. Validation + * rejects the empty `namespace`, so set it before validating. + */ +export function responseCacheConfig(config: ResponseCacheConfig = {}): ResponseCacheConfig { + const { backend, ...rest } = config; + return { + ttlSeconds: 3600, + namespace: '', + priority: 150, + bypassRate: 0, + cacheNondeterministic: false, + keyStrategy: ResponseCacheKeyStrategy.ExactRequest, + headerAllowlist: [], + backend: backend ?? inMemoryBackend(), + ...rest, + }; +} + +function mapValues(record: Record, map: (value: V) => R): Record { + return Object.fromEntries(Object.entries(record).map(([key, value]) => [key, map(value)])); +} + +function toToolClassPluginConfig({ + ttlSeconds, + bypassRate, + toolVersion, + argSkip, + ...rest +}: ToolClass): ToolClassPluginConfig { + return { + ...rest, + ...(ttlSeconds === undefined ? {} : { ttl_seconds: ttlSeconds }), + ...(bypassRate === undefined ? {} : { bypass_rate: bypassRate }), + ...(toolVersion === undefined ? {} : { tool_version: toolVersion }), + ...(argSkip === undefined ? {} : { arg_skip: argSkip }), + }; +} + +function toToolCachePluginConfig({ + cacheErrors, + default: defaults, + classes, + overrides, + ...rest +}: ToolCacheConfig): ToolCachePluginConfig { + return { + ...rest, + ...(cacheErrors === undefined ? {} : { cache_errors: cacheErrors }), + ...(defaults === undefined ? {} : { default: toToolClassPluginConfig(defaults) }), + ...(classes === undefined ? {} : { classes: mapValues(classes, toToolClassPluginConfig) }), + ...(overrides === undefined ? {} : { overrides: mapValues(overrides, toToolClassPluginConfig) }), + }; +} + +function toResponseCachePluginConfig({ + ttlSeconds, + bypassRate, + cacheNondeterministic, + keyStrategy, + headerAllowlist, + tools, + ...rest +}: ResponseCacheConfig): ResponseCachePluginConfig { + return { + ...rest, + ...(ttlSeconds === undefined ? {} : { ttl_seconds: ttlSeconds }), + ...(bypassRate === undefined ? {} : { bypass_rate: bypassRate }), + ...(cacheNondeterministic === undefined ? {} : { cache_nondeterministic: cacheNondeterministic }), + ...(keyStrategy === undefined ? {} : { key_strategy: keyStrategy }), + ...(headerAllowlist === undefined ? {} : { header_allowlist: headerAllowlist }), + ...(tools === undefined ? {} : { tools: toToolCachePluginConfig(tools) }), + }; +} + +function toPluginConfig({ responseCache, ...rest }: Config): AdaptivePluginConfig { + return { + ...rest, + ...(responseCache === undefined ? {} : { response_cache: toResponseCachePluginConfig(responseCache) }), + }; +} + +/** Adaptive runtime owned by the caller. */ +export class AdaptiveRuntime { + readonly #native: lib.AdaptiveRuntime; + + constructor(config: Config) { + this.#native = lib.AdaptiveRuntime.create(toPluginConfig(config)); + } + + /** Registers configured runtime features. */ + register(): Promise { + return this.#native.register(); + } + + /** Removes registered runtime features. */ + deregister(): void { + this.#native.deregister(); + } + + /** Shuts the runtime down for good. */ + shutdown(): Promise { + return this.#native.shutdown(); + } + + /** Blocks until telemetry has processed pending events. */ + waitForIdle(): void { + this.#native.waitForIdle(); + } + + /** Validation report from construction. */ + report(): ConfigReport { + return this.#native.report(); + } + + /** Binds cache governor request rewrites to a scope. */ + bindScope(scopeHandle: ScopeHandle): void { + this.#native.bindScope(scopeHandle); + } + + /** Builds cache request facts for an annotated LLM request. */ + buildCacheRequestFacts(options: CacheRequestFactsOptions): CacheRequestFacts | null { + return this.#native.buildCacheRequestFacts(options); + } +} + +/** + * Wraps an adaptive config as a plugin component. + * + * The result is an entry for `PluginConfig.components`. + * + * @param config - Adaptive config. + * @param options - `enabled` controls whether the component activates. + * @returns A component entry with kind `adaptive`. + * @remarks With `enabled` false, `initialize` validates the component but does + * not activate it. + */ +export function ComponentSpec(config: Config, { enabled = true }: { enabled?: boolean } = {}): ComponentSpec { + return { + kind: ADAPTIVE_PLUGIN_KIND, + enabled, + config: toPluginConfig(config), + }; +} + +/** Validates adaptive config without creating a runtime. */ +export function validateConfig(config: Config): ConfigReport { + return lib.validateAdaptiveConfig(toPluginConfig(config)); +} + +/** Builds one cache telemetry event from token usage. */ +export function buildCacheTelemetryEvent(options: CacheTelemetryEventOptions): CacheTelemetryEvent | null { + return lib.buildCacheTelemetryEvent(options); +} + +/** Sets latency sensitivity on the current scope. */ +export function setLatencySensitivity(value: number): void { + lib.setLatencySensitivity(value); +} diff --git a/crates/node/ts/diagnostics.ts b/crates/node/ts/diagnostics.ts new file mode 100644 index 0000000000..eb20c598cd --- /dev/null +++ b/crates/node/ts/diagnostics.ts @@ -0,0 +1,198 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import type { + DiagnosticMemoryPage, + DiagnosticQuery, + DiagnosticSnapshot, + DiagnosticsInfo, + DiagnosticsReader as NativeDiagnosticsReader, +} from '../binding.js'; +import * as lib from './index.js'; +import { + MAX_QUERY_BYTES, + MAX_QUERY_DEPTH, + MAX_QUERY_FIELD_NAME_BYTES, + MAX_QUERY_NODES, + MAX_QUERY_OBJECT_FIELDS, + NUMERIC_QUERY_FIELDS, +} from './diagnostics_query_contract.js'; + +export type { + Classification, + ComponentIdentity, + ComponentMemoryFootprint, + ComponentMemoryUsage, + ComponentOwnership, + ComponentSnapshot, + DiagnosticCapability, + DiagnosticCorrelation, + DiagnosticCount, + DiagnosticCounter, + DiagnosticCoverage, + DiagnosticCursor, + DiagnosticDetail, + DiagnosticDiscovery, + DiagnosticEvent, + DiagnosticLifetimeSummary, + DiagnosticLoss, + DiagnosticMemoryPage, + DiagnosticMemoryUsage, + DiagnosticNumber, + DiagnosticOrigin, + DiagnosticQuery, + DiagnosticRefreshSummary, + DiagnosticRetention, + DiagnosticSnapshot, + DiagnosticTotals, + DiagnosticWindow, + DiagnosticWindowBucket, + DiagnosticWindowCoverage, + DiagnosticsInfo, + EntryMemoryFootprint, + EntryMemoryUsage, + MemoryLimitEvent, + MemorySelectionTotals, + PluginObservationContext, + RemoteDiagnosticOrigin, + ResourceMemoryMeasurement, + ResourceMemoryMetrics, + ResourceMemoryNumber, + ResourceMemorySample, + ResourceMemoryUnit, + ResourceMemoryView, + ResourceProcessIdentity, + SourceIdentity, +} from '../binding.js'; + +/** Reader failure with a stable `reason` and no evidence in the message. */ +export class DiagnosticsError extends Error { + readonly reason: DiagnosticsReason; + + constructor(reason: DiagnosticsReason) { + super(reason); + this.name = 'DiagnosticsError'; + this.reason = reason; + } +} + +const reasonNames = [ + 'not_initialized', + 'fork_requires_exec', + 'configuration_conflict', + 'disabled', + 'busy', + 'query_disabled', + 'unsupported_schema', + 'internal', + 'invalid_cursor', + 'snapshot_capacity', + 'allocation_failed', + 'invalid_limits', + 'invalid_query', + 'invalid_snapshot', + 'invalid_metadata', + 'numeric_out_of_range', +] as const; + +export type DiagnosticsReason = (typeof reasonNames)[number] | 'unavailable'; + +const reasons: ReadonlySet = new Set(reasonNames); + +function isReason(value: string): value is DiagnosticsReason { + return reasons.has(value); +} + +function call(operation: () => T): T { + try { + return operation(); + } catch (error) { + if (error instanceof DiagnosticsError) throw error; + throw new DiagnosticsError(error instanceof Error && isReason(error.message) ? error.message : 'unavailable'); + } +} + +function queryJson(query: DiagnosticQuery = {}): string { + let size = 0; + let nodes = 0; + function normalize(value: unknown, key: string, depth: number): unknown { + if (++nodes > MAX_QUERY_NODES || depth > MAX_QUERY_DEPTH) throw new DiagnosticsError('invalid_query'); + if (value !== null && typeof value === 'object') { + if (Array.isArray(value)) throw new DiagnosticsError('invalid_query'); + const out: Record = Object.create(null); + let fields = 0; + for (const [field, fieldValue] of Object.entries(value)) { + if (++fields > MAX_QUERY_OBJECT_FIELDS || Buffer.byteLength(field) > MAX_QUERY_FIELD_NAME_BYTES) { + throw new DiagnosticsError('invalid_query'); + } + size += Buffer.byteLength(field); + if (size > MAX_QUERY_BYTES) throw new DiagnosticsError('invalid_query'); + const wire = field.replace(/[A-Z]/g, (letter) => '_' + letter.toLowerCase()); + if (Object.hasOwn(out, wire)) throw new DiagnosticsError('invalid_query'); + out[wire] = normalize(fieldValue, wire, depth + 1); + } + return out; + } + return normalizeScalar(value, key); + } + function normalizeScalar(value: unknown, key: string): unknown { + if (NUMERIC_QUERY_FIELDS.has(key) && typeof value === 'bigint') { + if (value < 0n || value > 18446744073709551615n) throw new DiagnosticsError('invalid_query'); + return value.toString(); + } + if (NUMERIC_QUERY_FIELDS.has(key) && typeof value === 'number') throw new DiagnosticsError('invalid_query'); + if (typeof value === 'string') { + size += Buffer.byteLength(value); + if (size > MAX_QUERY_BYTES) throw new DiagnosticsError('invalid_query'); + } else if ( + value !== null && + typeof value !== 'boolean' && + !(typeof value === 'number' && Number.isSafeInteger(value)) + ) { + throw new DiagnosticsError('invalid_query'); + } + return value; + } + const encoded = JSON.stringify(normalize(query, '', 0)); + if (Buffer.byteLength(encoded) > MAX_QUERY_BYTES) throw new DiagnosticsError('invalid_query'); + return encoded; +} + +/** Reads diagnostics for this process; keep it from untrusted clients. */ +export class DiagnosticsReader { + readonly #native: NativeDiagnosticsReader; + + constructor(native: NativeDiagnosticsReader) { + this.#native = native; + } + + /** Returns one page as objects with `bigint` diagnostic integers. */ + snapshot(query?: DiagnosticQuery): DiagnosticSnapshot { + return call(() => this.#native.snapshot(queryJson(query))); + } + + /** Returns one page as canonical JSON with integers as decimal strings. */ + snapshotJson(query?: DiagnosticQuery): string { + return call(() => this.#native.snapshotJson(queryJson(query))); + } + + /** Returns one memory page as objects with `bigint` diagnostic integers. */ + inspectMemory(query?: DiagnosticQuery): DiagnosticMemoryPage { + return call(() => this.#native.inspectMemory(queryJson(query))); + } + + /** Returns one memory page as canonical JSON with integers as decimal strings. */ + inspectMemoryJson(query?: DiagnosticQuery): string { + return call(() => this.#native.inspectMemoryJson(queryJson(query))); + } + + /** Returns budgets, occupancy, and logging coverage. */ + info(): DiagnosticsInfo { + return call(() => this.#native.info()); + } +} + +/** Returns a reader, or throws `not_initialized`. */ +export function reader(): DiagnosticsReader { + return call(() => new DiagnosticsReader(lib.diagnosticsReader())); +} diff --git a/crates/node/ts/diagnostics_query_contract.ts b/crates/node/ts/diagnostics_query_contract.ts new file mode 100644 index 0000000000..d41a701d95 --- /dev/null +++ b/crates/node/ts/diagnostics_query_contract.ts @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import * as lib from './index.js'; + +const contract = lib._diagnosticsQueryContract(); + +export const MAX_QUERY_BYTES: number = contract.maxQueryBytes; +export const MAX_QUERY_NODES: number = 64; +export const MAX_QUERY_DEPTH: number = 4; +export const MAX_QUERY_OBJECT_FIELDS: number = 20; +export const MAX_QUERY_FIELD_NAME_BYTES: number = 128; +export const NUMERIC_QUERY_FIELDS: ReadonlySet = new Set(contract.numericQueryFields); diff --git a/crates/node/ts/index.ts b/crates/node/ts/index.ts new file mode 100644 index 0000000000..46dc04cc37 --- /dev/null +++ b/crates/node/ts/index.ts @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import type { Json, JsonCompatible, JsonObject, JsonObjectCompatible } from '../binding.js'; + +export * from '../binding.js'; + +export class LlmRequest< + THeaders extends JsonObjectCompatible = JsonObject, + TContent extends JsonCompatible = Json, +> { + readonly headers: THeaders; + readonly content: TContent; + + constructor(headers: THeaders, content: TContent) { + if (headers === null || typeof headers !== 'object' || Array.isArray(headers)) { + throw new TypeError('LlmRequest headers must be a JSON object'); + } + this.headers = headers; + this.content = content; + } +} diff --git a/crates/node/ts/model_pricing.ts b/crates/node/ts/model_pricing.ts new file mode 100644 index 0000000000..55a0eb47e9 --- /dev/null +++ b/crates/node/ts/model_pricing.ts @@ -0,0 +1,210 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import type { ConfigReport, JsonObject } from '../binding.js'; +import type { ComponentSpecFor } from './plugin.js'; +import * as lib from './index.js'; + +export type { Json } from '../binding.js'; + +/** Plugin kind of the model pricing component. */ +export const PRICING_PLUGIN_KIND = 'pricing'; + +/** Prompt-cache write price for a duration in seconds, or the default price without `duration`. */ +export interface CacheWriteRate { + duration?: number; + per_million: number; +} + +/** Token rates per million tokens. */ +export interface TokenPricingRates { + input_per_million: number; + output_per_million: number; + cache_read_per_million?: number; + cache_write_rates?: CacheWriteRate[]; +} + +/** Prompt-cache accounting for a catalog entry. */ +export interface PromptCachePricing { + read_accounting?: 'included_in_prompt_tokens' | 'separate'; +} + +/** One prompt-token threshold tier. */ +export interface TokenRateTier { + min_prompt_tokens?: number; + max_prompt_tokens?: number; + rates: TokenPricingRates; +} + +/** Token rate schedule selected by prompt-token thresholds. */ +export interface PromptTokenThresholdRateSchedule { + type: 'prompt_token_threshold'; + applies_to?: 'full_request'; + tiers: TokenRateTier[]; +} + +/** One model pricing catalog entry. */ +export interface ModelPricing { + provider: string; + model_id: string; + aliases?: string[]; + currency?: string; + unit?: 'per_token' | 'per_request' | 'per_second' | 'gpu_hour'; + rates?: TokenPricingRates; + rate_schedule?: PromptTokenThresholdRateSchedule | JsonObject; + prompt_cache: PromptCachePricing; + pricing_as_of: string; + pricing_source: string; +} + +/** Inline pricing catalog. */ +export interface PricingCatalog { + version?: number; + entries: Array; +} + +/** Pricing source holding an inline catalog. */ +export interface InlineSource { + type: 'inline'; + catalog: PricingCatalog | JsonObject; +} + +/** Pricing source read from a JSON file. */ +export interface FileSource { + type: 'file'; + path: string; +} + +/** Pricing source. */ +export type PricingSource = InlineSource | FileSource | JsonObject; + +/** Model pricing config. */ +export interface PricingConfig { + sources?: PricingSource[]; +} + +/** Creates a default model pricing config with no sources. */ +export function defaultConfig(): PricingConfig { + return { + sources: [], + }; +} + +/** Creates token rates with defaults applied. */ +export function tokenRates(config: Partial = {}): TokenPricingRates { + return { + input_per_million: 0, + output_per_million: 0, + ...config, + }; +} + +/** Creates prompt-cache accounting with defaults applied. */ +export function promptCache(config: Partial = {}): PromptCachePricing { + return { + read_accounting: 'included_in_prompt_tokens', + ...config, + }; +} + +/** Creates one rate tier. */ +export function tokenRateTier( + rates: TokenPricingRates, + config: Omit, 'rates'> = {}, +): TokenRateTier { + return { + ...config, + rates, + }; +} + +/** Creates a rate schedule from tiers. */ +export function promptTokenThresholdRateSchedule( + tiers: TokenRateTier[] = [], + config: Omit, 'type' | 'tiers'> = {}, +): PromptTokenThresholdRateSchedule { + return { + type: 'prompt_token_threshold', + applies_to: 'full_request', + tiers, + ...config, + }; +} + +/** Creates one catalog entry with defaults applied. */ +export function catalogEntry(config: Omit & Partial): ModelPricing { + const { prompt_cache: promptCacheConfig, ...entryConfig } = config; + return { + aliases: [], + currency: 'USD', + unit: 'per_token', + ...entryConfig, + prompt_cache: promptCache(promptCacheConfig), + }; +} + +/** Creates an inline catalog from entries. */ +export function inlineCatalog( + entries: Array = [], + config: Omit, 'entries'> = {}, +): PricingCatalog { + return { + version: 1, + entries, + ...config, + }; +} + +/** Creates a pricing source from an inline catalog. */ +export function inlineSource(catalog: PricingCatalog | JsonObject): InlineSource { + return { + type: 'inline', + catalog, + }; +} + +/** Creates a pricing source from a JSON file path. */ +export function fileSource(path: string): FileSource { + return { + type: 'file', + path, + }; +} + +/** Creates a model pricing config from sources in precedence order. */ +export function pricingConfig(sources: PricingSource[] = []): PricingConfig { + return { + sources, + }; +} + +/** Wraps model pricing config as a plugin component. */ +export function ComponentSpec( + config: PricingConfig, + { enabled = true }: { enabled?: boolean } = {}, +): ComponentSpecFor<'pricing'> { + return { + kind: PRICING_PLUGIN_KIND, + enabled, + config, + }; +} + +/** Validates model pricing config without activating it. */ +export function validateConfig(config: PricingConfig | JsonObject): ConfigReport { + return lib.validateExact({ + version: 1, + components: [{ kind: PRICING_PLUGIN_KIND, enabled: true, config }], + }).config; +} + +/** Validates an untyped model pricing document and returns it as a config; throws on error diagnostics. */ +export function parseConfig(document: JsonObject): PricingConfig { + const errors = validateConfig(document).diagnostics.filter((diagnostic) => diagnostic.level === 'error'); + if (errors.length > 0) { + throw new Error(`invalid model pricing config: ${errors.map((diagnostic) => diagnostic.message).join('; ')}`, { + cause: errors, + }); + } + return document as PricingConfig; +} diff --git a/crates/node/ts/observability.ts b/crates/node/ts/observability.ts new file mode 100644 index 0000000000..2a1132aa62 --- /dev/null +++ b/crates/node/ts/observability.ts @@ -0,0 +1,348 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import type { Json } from '../binding.js'; +import type { ConfigPolicy } from './plugin.js'; + +export type { ConfigDiagnostic, ConfigReport } from '../binding.js'; +export type { ConfigPolicy } from './plugin.js'; + +export interface AtofConfig { + enabled?: boolean; + sinks?: AtofSinkConfig[]; +} + +export type AtofSinkConfig = AtofFileSinkConfig | AtofStreamSinkConfig; + +export interface AtofFileSinkConfig { + type: 'file'; + output_directory?: string; + filename?: string; + mode?: 'append' | 'overwrite' | (string & {}); +} + +export interface AtofStreamSinkConfig { + type: 'stream'; + url: string; + transport?: 'http_post' | 'websocket' | 'ndjson' | (string & {}); + headers?: Record; + header_env?: Record; + header_file?: Record; + timeout_millis?: number; + field_name_policy?: 'preserve' | 'replace_dots' | (string & {}); + name?: string; +} + +/** @deprecated Use AtofStreamSinkConfig. */ +export type AtofEndpointConfig = AtofStreamSinkConfig; + +export interface S3StorageConfig { + type: 's3'; + bucket: string; + key_prefix?: string; + access_key_id?: string; + secret_access_key_var?: string; + session_token_var?: string; + region?: string; + endpoint_url?: string; + allow_http?: boolean; +} + +export interface HttpStorageConfig { + type: 'http'; + endpoint: string; + headers?: Record; + header_env?: Record; + header_file?: Record; + timeout_millis?: number; +} + +export interface AtifConfig { + enabled?: boolean; + agent_name?: string; + agent_version?: string; + model_name?: string; + tool_definitions?: Record[]; + extra?: Record; + output_directory?: string; + filename_template?: string; + storage?: S3StorageConfig | HttpStorageConfig | Array; + /** Keeps raw events in `extra.observed_events`, default `true`. */ + include_observed_events?: boolean; + /** Keeps full provider requests in trajectory steps, default `true`. */ + include_llm_requests?: boolean; +} + +export interface OpenTelemetryTlsConfig { + /** PEM CA bundle replacing system CAs. */ + certificate?: string; + /** PEM client certificate chain, used with `private_key`. */ + client_certificate?: string; + /** PEM private key, used with `client_certificate`. */ + private_key?: string; +} + +export interface OpenTelemetryEndpointConfig { + type: 'full' | 'gen_ai' | 'openinference'; + endpoint: string; + tls?: OpenTelemetryTlsConfig; + mark_projection?: 'inherit' | 'event' | 'tool'; + mark_exclude_names?: string[]; + attribute_mappings?: Array<{ key: string; alias: string }>; + promote_metadata_prefixes?: string[]; + promote_resource_metadata_prefixes?: string[]; + transport?: 'http_binary' | 'grpc'; + headers?: Record; + header_env?: Record; + header_file?: Record; + resource_attributes?: Record; + service_name?: string; + service_namespace?: string; + service_version?: string; + instrumentation_scope?: string; + timeout_millis?: number; + max_queue_size?: number; + max_export_batch_size?: number; + scheduled_delay_millis?: number; + completed_span_context_ttl_millis?: number; +} + +export interface OpenTelemetrySignalEndpointConfig { + endpoint: string; + tls?: OpenTelemetryTlsConfig; + transport?: 'http_binary' | 'grpc'; + headers?: Record; + header_env?: Record; + header_file?: Record; + resource_attributes?: Record; + service_name?: string; + service_namespace?: string; + service_version?: string; + instrumentation_scope?: string; + timeout_millis?: number; + /** Root-scope metadata prefixes copied to resource attributes. */ + promote_resource_metadata_prefixes?: string[]; +} + +export interface OpenTelemetryLogSectionConfig { + enabled?: boolean; + endpoints?: OpenTelemetrySignalEndpointConfig[]; + minimum_severity?: 'trace' | 'debug' | 'info' | 'warn' | 'warning' | 'error'; + max_queue_size?: number; + max_export_batch_size?: number; + scheduled_delay_millis?: number; + completed_span_context_ttl_millis?: number; +} + +export interface OpenTelemetryMetricSectionConfig { + enabled?: boolean; + endpoints?: OpenTelemetrySignalEndpointConfig[]; + export_interval_millis?: number; + temporality?: 'cumulative' | 'delta' | 'low_memory'; + max_instruments?: number; + cardinality_limit?: number; +} + +export interface OpenTelemetryFileSinkConfig { + type?: 'full' | 'gen_ai' | 'openinference'; + output_directory: string; + filename?: string; + /** `json_lines` for one OTLP/JSON record per line, `proto` for length-delimited records. */ + format?: 'json_lines' | 'proto'; + mode?: 'append' | 'overwrite'; + mark_projection?: 'inherit' | 'event' | 'tool'; + mark_exclude_names?: string[]; + attribute_mappings?: Array<{ key: string; alias: string }>; + promote_metadata_prefixes?: string[]; + promote_resource_metadata_prefixes?: string[]; + resource_attributes?: Record; + service_name?: string; + service_namespace?: string; + service_version?: string; + instrumentation_scope?: string; + max_queue_size?: number; + max_export_batch_size?: number; + scheduled_delay_millis?: number; + completed_span_context_ttl_millis?: number; +} + +export interface OpenTelemetrySectionConfig { + enabled?: boolean; + endpoints?: OpenTelemetryEndpointConfig[]; + file_sinks?: OpenTelemetryFileSinkConfig[]; + logs?: OpenTelemetryLogSectionConfig; + metrics?: OpenTelemetryMetricSectionConfig; +} + +export interface Config { + version?: number; + atof?: AtofConfig; + atif?: AtifConfig; + opentelemetry?: OpenTelemetrySectionConfig; + policy?: ConfigPolicy; + /** Keeps full sanitized request data on LLM start events. */ + enable_full_payloads?: boolean; + /** Non-loopback hosts allowed to receive unencrypted HTTP or WebSocket exports, headers included. */ + insecure_http_hosts?: string[]; +} + +export interface ComponentSpec { + kind: 'observability'; + enabled?: boolean; + config: Config; +} + +/** Plugin kind of the observability component. */ +export const OBSERVABILITY_PLUGIN_KIND = 'observability'; + +const DEFAULT_COMPLETED_SPAN_CONTEXT_TTL_MILLIS = 60_000; + +/** Creates a default observability config. */ +export function defaultConfig(): Config { + return { + version: 4, + }; +} + +/** Creates Agent Trajectory Observability Format (ATOF) settings with defaults applied. */ +export function atofConfig(config: AtofConfig = {}): AtofConfig { + return { + enabled: false, + ...config, + }; +} + +/** Creates Agent Trajectory Interchange Format (ATIF) settings with defaults applied. */ +export function atifConfig(config: AtifConfig = {}): AtifConfig { + return { + enabled: false, + agent_name: 'NeMo Relay', + model_name: 'unknown', + filename_template: 'nemo-relay-atif-{session_id}.json', + include_observed_events: true, + include_llm_requests: true, + ...config, + }; +} + +/** Creates one OpenTelemetry trace endpoint with defaults applied. */ +export function openTelemetryEndpoint(config: OpenTelemetryEndpointConfig): OpenTelemetryEndpointConfig { + if (config === null || typeof config !== 'object') { + throw new TypeError('OpenTelemetry endpoint config is required'); + } + if (!['full', 'gen_ai', 'openinference'].includes(config.type)) { + throw new TypeError('OpenTelemetry endpoint type must be "full", "gen_ai", or "openinference"'); + } + if (typeof config.endpoint !== 'string' || config.endpoint.trim() === '') { + throw new TypeError('OpenTelemetry endpoint must be a nonblank string'); + } + return { + transport: 'http_binary', + service_name: 'unknown_service', + instrumentation_scope: 'opentelemetry', + timeout_millis: 3000, + completed_span_context_ttl_millis: DEFAULT_COMPLETED_SPAN_CONTEXT_TTL_MILLIS, + headers: {}, + header_env: {}, + resource_attributes: {}, + promote_metadata_prefixes: [], + promote_resource_metadata_prefixes: [], + ...config, + }; +} + +/** Creates one file sink for OTLP spans with defaults applied. */ +export function openTelemetryFileSink(config: OpenTelemetryFileSinkConfig): OpenTelemetryFileSinkConfig { + if (config === null || typeof config !== 'object') { + throw new TypeError('OpenTelemetry file sink config is required'); + } + if (typeof config.output_directory !== 'string' || config.output_directory.trim() === '') { + throw new TypeError('OpenTelemetry file sink output_directory must be a nonblank string'); + } + if (config.format !== undefined && !['json_lines', 'proto'].includes(config.format)) { + throw new TypeError('OpenTelemetry file sink format must be "json_lines" or "proto"'); + } + if (config.mode !== undefined && !['append', 'overwrite'].includes(config.mode)) { + throw new TypeError('OpenTelemetry file sink mode must be "append" or "overwrite"'); + } + return { + type: 'full', + format: 'json_lines', + mode: 'overwrite', + service_name: 'unknown_service', + instrumentation_scope: 'opentelemetry', + completed_span_context_ttl_millis: DEFAULT_COMPLETED_SPAN_CONTEXT_TTL_MILLIS, + resource_attributes: {}, + promote_metadata_prefixes: [], + promote_resource_metadata_prefixes: [], + ...config, + }; +} + +/** Creates one OpenTelemetry log or metric endpoint with defaults applied. */ +export function openTelemetrySignalEndpoint( + config: OpenTelemetrySignalEndpointConfig, +): OpenTelemetrySignalEndpointConfig { + if (config === null || typeof config !== 'object') { + throw new TypeError('OpenTelemetry signal endpoint config is required'); + } + if (typeof config.endpoint !== 'string' || config.endpoint.trim() === '') { + throw new TypeError('OpenTelemetry signal endpoint must be a nonblank string'); + } + return { + transport: 'http_binary', + headers: {}, + header_env: {}, + resource_attributes: {}, + service_name: 'unknown_service', + instrumentation_scope: 'opentelemetry', + timeout_millis: 3000, + ...config, + }; +} + +/** Creates OTLP log settings with defaults applied. */ +export function openTelemetryLogConfig(config: OpenTelemetryLogSectionConfig = {}): OpenTelemetryLogSectionConfig { + return { + enabled: false, + minimum_severity: 'info', + max_queue_size: 2048, + max_export_batch_size: 512, + scheduled_delay_millis: 1000, + completed_span_context_ttl_millis: DEFAULT_COMPLETED_SPAN_CONTEXT_TTL_MILLIS, + ...config, + }; +} + +/** Creates OTLP metric settings with defaults applied. */ +export function openTelemetryMetricConfig( + config: OpenTelemetryMetricSectionConfig = {}, +): OpenTelemetryMetricSectionConfig { + return { + enabled: false, + export_interval_millis: 60000, + temporality: 'cumulative', + max_instruments: 256, + cardinality_limit: 2000, + ...config, + }; +} + +/** Creates OpenTelemetry settings with defaults applied. */ +export function openTelemetryConfig(config: OpenTelemetrySectionConfig = {}): OpenTelemetrySectionConfig { + return { + enabled: false, + endpoints: [], + file_sinks: [], + ...config, + }; +} + +/** Wraps observability config as a plugin component. */ +export function ComponentSpec(config: Config, { enabled = true }: { enabled?: boolean } = {}): ComponentSpec { + return { + kind: OBSERVABILITY_PLUGIN_KIND, + enabled, + config, + }; +} diff --git a/crates/node/ts/pii_redaction.ts b/crates/node/ts/pii_redaction.ts new file mode 100644 index 0000000000..a1a0ef026c --- /dev/null +++ b/crates/node/ts/pii_redaction.ts @@ -0,0 +1,101 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +export type { ConfigDiagnostic, ConfigReport } from '../binding.js'; + +export interface ConfigPolicy { + unknown_component?: 'ignore' | 'warn' | 'error' | (string & {}); + unknown_field?: 'ignore' | 'warn' | 'error' | (string & {}); + unsupported_value?: 'ignore' | 'warn' | 'error' | (string & {}); +} + +export interface BuiltinConfig { + preset?: 'trajectory_context'; + action?: 'remove' | 'redact' | 'regex_replace' | 'hash' | 'mask' | (string & {}); + target_paths?: string[]; + target_path_globs?: string[]; + pattern?: string; + detector?: string; + replacement?: string; + mask_char?: string; + unmasked_prefix?: number; + unmasked_suffix?: number; + custom_mark_payload_policy?: 'preserve' | 'redact_all_leaves'; + metric_string_attribute_allowlist?: Record; +} + +export interface LocalModelConfig { + backend?: string; + model_id?: string; + detector_profile?: string; + allow_network?: boolean; + max_latency_ms?: number; +} + +export interface Config { + version?: number; + mode?: 'builtin' | 'local_model' | (string & {}); + input?: boolean; + output?: boolean; + tool_input?: boolean; + tool_output?: boolean; + mark?: boolean; + priority?: number; + codec?: + | 'openai_chat' + | 'openai_responses' + | 'anthropic_messages' + | 'oci_genai' + | 'gemini_generate_content' + | (string & {}); + builtin?: BuiltinConfig; + local?: LocalModelConfig; + policy?: ConfigPolicy; +} + +export interface ComponentSpec { + kind: 'pii_redaction'; + enabled?: boolean; + config: Config; +} + +/** Plugin kind of the PII redaction component. */ +export const PII_REDACTION_PLUGIN_KIND = 'pii_redaction'; + +/** Creates a default PII redaction config. */ +export function defaultConfig(): Config { + return { + version: 1, + mode: 'builtin', + input: true, + output: true, + tool_input: true, + tool_output: true, + mark: true, + priority: 100, + }; +} + +/** Creates built-in backend settings with defaults applied. */ +export function builtinConfig(config: BuiltinConfig = {}): BuiltinConfig { + return { + ...(config.preset === 'trajectory_context' ? {} : { action: 'remove' }), + ...config, + }; +} + +/** Creates local-model backend settings with defaults applied. */ +export function localModelConfig(config: LocalModelConfig = {}): LocalModelConfig { + return { + ...config, + }; +} + +/** Wraps PII redaction config as a plugin component. */ +export function ComponentSpec(config: Config, { enabled = true }: { enabled?: boolean } = {}): ComponentSpec { + return { + kind: PII_REDACTION_PLUGIN_KIND, + enabled, + config, + }; +} diff --git a/crates/node/ts/plugin.ts b/crates/node/ts/plugin.ts new file mode 100644 index 0000000000..dfc0893f9b --- /dev/null +++ b/crates/node/ts/plugin.ts @@ -0,0 +1,264 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +/// + +import type { + ConfigDiagnostic, + JsonObject, + JsonObjectCompatible, + PluginContext, + PluginHostActivation, + PluginHostReport, + ResourceMetricsConfig, +} from '../binding.js'; +import type { AdaptivePluginConfig } from './adaptive.js'; +import * as lib from './index.js'; +import type { PricingConfig } from './model_pricing.js'; +import type { Config as ObservabilityConfig } from './observability.js'; +import type { Config as PiiRedactionConfig } from './pii_redaction.js'; + +export type { + ConfigDiagnostic, + ConfigReport, + DynamicPluginCheckState, + DynamicPluginFailure, + DynamicPluginKind, + DynamicPluginValidationReport, + DynamicPluginValidationStatus, + EventMetadata, + EventMetadataScalar, + EventMetadataValue, + LlmCodecIdentity, + LlmExecutionContext, + LlmOptimizationContribution, + LlmOptimizationDataSchema, + LlmOptimizationModel, + LlmOptimizationModelTransition, + LlmOptimizationTokenImpact, + LlmOptimizationTokens, + LlmRequestContext, + LlmRequestInterceptOutcome, + LlmResponseContext, + LlmSanitizeRequestContext, + LlmSanitizeResponseContext, + PluginContext, + PluginHostActivation, + PluginHostReport, + RuntimeDiagnostic, + ToolExecutionInterceptOutcome, +} from '../binding.js'; + +/** How unsupported configuration is handled. */ +export type UnsupportedBehavior = 'ignore' | 'warn' | 'error'; + +/** Handling for unknown or unsupported plugin configuration. */ +export interface ConfigPolicy { + unknown_component?: UnsupportedBehavior; + unknown_field?: UnsupportedBehavior; + unsupported_value?: UnsupportedBehavior; +} + +export interface ComponentConfigs { + adaptive: AdaptivePluginConfig; + observability: ObservabilityConfig; + pii_redaction: PiiRedactionConfig; + pricing: PricingConfig; + resource_metrics: ResourceMetricsConfig; +} + +export type ComponentKind = keyof ComponentConfigs; + +export type KnownComponentSpec = { + [K in ComponentKind]: { kind: K; enabled?: boolean; config?: ComponentConfigs[K] }; +}[ComponentKind]; + +declare const openComponent: unique symbol; + +/** Config returned by `ComponentSpec` for a kind registered with `registerPlugin`. */ +export type OpenComponentConfig = { + readonly [openComponent]: true; +}; + +/** Component created by `ComponentSpec` for a kind registered with `registerPlugin`. */ +export interface OpenComponentSpec { + kind: string; + enabled?: boolean; + config?: C & OpenComponentConfig; +} + +export type ComponentSpecFor = K extends ComponentKind + ? Extract + : OpenComponentSpec; + +/** One plugin component. */ +export type ComponentSpec = KnownComponentSpec | OpenComponentSpec; + +/** Plugin configuration document. */ +export interface PluginConfig { + version?: number; + components?: ComponentSpec[]; + policy?: ConfigPolicy; + /** Host service profile read at startup; omitted settings come from config files. */ + diagnostics?: { enabled?: boolean; limits?: Record }; +} + +/** Callbacks a plugin kind provides. */ +export interface Plugin { + /** Validates one component config. */ + validate?: (pluginConfig: JsonObject) => ConfigDiagnostic[] | null | undefined; + /** Installs middleware and subscribers for one component; throwing aborts initialization. */ + register: (pluginConfig: JsonObject, context: PluginContext) => void; +} + +/** + * Creates an empty plugin configuration. + * + * The document has `version` 1 and no components. Add components to it, then + * pass it to `validate` or `initialize`. + * + * @returns A plugin configuration with no components. + * @remarks Editing the returned object changes nothing until it is passed to + * `initialize`. + */ +export function defaultConfig(): PluginConfig { + return { + version: 1, + components: [], + }; +} + +/** + * Creates one component entry for `PluginConfig.components`. + * + * For a built-in kind, `config` is checked against that kind's config type. + * For a kind registered with `register`, `config` is any JSON object. + * + * @param kind - Plugin kind name. + * @param config - Config handed to the plugin's hooks. + * @param options - `enabled` controls whether the component activates. + * @returns A component entry for a plugin configuration. + * @remarks With `enabled` false, `initialize` validates the component but does + * not activate it. + */ +export function ComponentSpec( + kind: K, + config?: ComponentConfigs[K], + options?: { + enabled?: boolean; + }, +): ComponentSpecFor; +export function ComponentSpec = JsonObject>( + kind: K extends ComponentKind ? never : K, + config?: C, + options?: { + enabled?: boolean; + }, +): OpenComponentSpec; +export function ComponentSpec( + kind: string, + config: object = {}, + { enabled = true }: { enabled?: boolean } = {}, +): { kind: string; enabled?: boolean; config?: object } { + return { + kind, + enabled, + config, + }; +} + +/** + * Initializes the plugin host and activates the configured plugins. + * + * The system and user `plugins.toml` files are merged first and `config` is + * applied on top of them. + * + * @param config - Programmatic configuration. Its values override the files. + * @param additionalPluginsToml - Path of one more `plugins.toml` to merge. If + * the file is missing, the report carries a `plugin.configuration_file_missing` warning. + * @returns The activation, which holds the host report. + * @remarks Plugin callbacks run only while the activation is alive. End it + * with `close()` or `await using`. + */ +export function initialize(config: PluginConfig, additionalPluginsToml?: string): Promise { + return lib.initialize(config, additionalPluginsToml); +} + +/** + * Validates the plugin host configuration without loading plugin code. + * + * The files and `config` are merged exactly as `initialize` merges them, and + * the merged result is checked. + * + * @param config - Programmatic configuration. Its values override the files. + * @param additionalPluginsToml - Path of one more `plugins.toml` to merge. If + * the file is missing, the report carries a `plugin.configuration_file_missing` warning. + * @returns The report for static components and dynamic plugins. + * @remarks Nothing is activated and no host lease is taken. + */ +export function validate(config: PluginConfig, additionalPluginsToml?: string): PluginHostReport { + return lib.validate(config, additionalPluginsToml); +} + +/** + * Validates one plugin configuration document by itself. + * + * No `plugins.toml` file is read or merged, so `config` must be the whole + * document. + * + * @param config - Complete plugin configuration. + * @returns The report for the static components in `config`. + * @remarks Host-only `diagnostics` settings are rejected here. Use `validate` + * to check them. + */ +export function validateExact(config: PluginConfig): PluginHostReport { + return lib.validateExact(config); +} + +/** + * Lists the plugin kinds in the registry. + * + * A plugin configuration can reference any of these kinds. + * + * @returns The registered plugin kind names. + * @remarks A registered kind is not necessarily active in the current + * configuration. + */ +export function listKinds(): string[] { + return lib.listPluginKinds(); +} + +/** + * Registers a plugin kind implemented in JavaScript. + * + * `plugin.validate` runs when a configuration is validated. `plugin.register` + * runs when `initialize` activates a component of this kind. + * + * @param pluginKind - Unique plugin kind name. + * @param plugin - The `validate` and `register` hooks. + * @returns Nothing. + * @remarks Without `plugin.validate`, any config for this kind passes + * validation. + */ +export function register(pluginKind: string, plugin: Plugin): void { + lib.registerPlugin( + pluginKind, + plugin.validate === undefined ? null : (pluginConfig) => plugin.validate?.(pluginConfig), + (pluginConfig, context) => plugin.register(pluginConfig, context), + ); +} + +/** + * Removes a plugin kind from the registry. + * + * Configurations validated or initialized after this call cannot reference + * the kind. + * + * @param pluginKind - Name the kind was registered under. + * @returns `true` if the kind was registered, `false` if it was not. + * @remarks Components of this kind that are already active keep running until + * their activation closes. + */ +export function deregister(pluginKind: string): boolean { + return lib.deregisterPlugin(pluginKind); +} diff --git a/crates/node/ts/typed.ts b/crates/node/ts/typed.ts new file mode 100644 index 0000000000..9d5384f259 --- /dev/null +++ b/crates/node/ts/typed.ts @@ -0,0 +1,286 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +/** Typed wrappers for the execute APIs, converting values with a `Codec`. */ + +import type { + Json, + JsonCompatible, + JsonValue, + LlmCodec, + LlmRequestCompatible, + LlmRequestShape, + LlmResponseCodec, + LlmStream, + ScopeHandle, + ToolExecutionResult as JsonToolExecutionResult, +} from '../binding.js'; +import * as lib from './index.js'; + +export type { + Json, + JsonArray, + JsonCompatible, + JsonObject, + JsonPrimitive, + JsonValue, + LlmCodec, + LlmRequestCompatible, + LlmRequestShape, + LlmResponseCodec, +} from '../binding.js'; + +/** Result returned to the application after tool execution. */ +export interface ToolExecutionResult { + result: TResult; + annotation?: JsonValue; +} + +/** Converts between a typed value `T` and JSON. */ +export interface Codec = JsonValue> { + /** Converts a typed value to JSON. */ + toJson: (value: T) => TJson; + /** Rebuilds a typed value from JSON. */ + fromJson: (data: JsonValue) => T; +} + +/** Codec that returns values unchanged. */ +export class JsonPassthrough implements Codec { + toJson(value: JsonValue): JsonValue { + return value; + } + + fromJson(data: JsonValue): JsonValue { + return data; + } +} + +/** Options for `typedToolExecute`. */ +export interface TypedToolExecuteOptions< + TData extends JsonCompatible = JsonValue, + TMetadata extends JsonCompatible = JsonValue, +> { + handle?: ScopeHandle | null; + attributes?: number | null; + data?: TData; + metadata?: TMetadata; + toolCallId?: string | null; +} + +/** Options for `typedLlmExecute`. */ +export interface TypedLlmExecuteOptions< + TData extends JsonCompatible = JsonValue, + TMetadata extends JsonCompatible = JsonValue, + TAnnotated extends JsonCompatible = JsonValue, + TEncoded extends LlmRequestCompatible = LlmRequestShape, + TDecodedResponse extends JsonCompatible = JsonValue, +> { + handle?: ScopeHandle | null; + attributes?: number | null; + data?: TData; + metadata?: TMetadata; + modelName?: string | null; + codec?: LlmCodec | null; + responseCodec?: LlmResponseCodec | null; +} + +/** Options for `typedLlmStreamExecute`. */ +export interface TypedLlmStreamExecuteOptions< + TData extends JsonCompatible = JsonValue, + TMetadata extends JsonCompatible = JsonValue, + TAnnotated extends JsonCompatible = JsonValue, + TEncoded extends LlmRequestCompatible = LlmRequestShape, + TDecodedResponse extends JsonCompatible = JsonValue, +> { + handle?: ScopeHandle | null; + attributes?: number | null; + data?: TData; + metadata?: TMetadata; + modelName?: string | null; + codec?: LlmCodec | null; + responseCodec?: LlmResponseCodec | null; +} + +function encodeToolExecutionResult( + executionResult: ToolExecutionResult, + resultCodec: Codec, +): JsonToolExecutionResult { + if (executionResult === null || typeof executionResult !== 'object' || !('result' in executionResult)) { + throw new TypeError('typed tool callback must return ToolExecutionResult'); + } + const { result, annotation } = executionResult; + return { + result: resultCodec.toJson(result), + ...(annotation == null ? {} : { annotation }), + }; +} + +/** + * Executes a tool call with typed arguments and result. + * + * `args` is encoded with `argsCodec` before the tool lifecycle runs, and the + * JSON result is decoded with `resultCodec` afterwards. The annotation is + * returned as the pipeline produced it. + * + * @param name - Tool name. + * @param args - Typed tool arguments. + * @param func - The tool. It may return the result directly or in a promise. + * @param argsCodec - Converts the arguments to and from JSON. + * @param resultCodec - Converts the result to and from JSON. + * @param options - Scope handle, attributes, data, metadata, and tool call id. + * @returns The decoded result and its annotation. + * @remarks A codec error or an execution error rejects the returned promise. + */ +export async function typedToolExecute< + TArgs, + TResult, + TArgsJson extends JsonCompatible = JsonValue, + TResultJson extends JsonValue = JsonValue, + TData extends JsonCompatible = JsonValue, + TMetadata extends JsonCompatible = JsonValue, +>( + name: string, + args: TArgs, + func: (args: TArgs, signal: AbortSignal) => ToolExecutionResult | Promise>, + argsCodec: Codec, + resultCodec: Codec, + options: TypedToolExecuteOptions = {}, +): Promise> { + const jsonResult: JsonToolExecutionResult = await lib.toolCallExecuteAsync( + name, + argsCodec.toJson(args), + async (jsonArgs: Json, signal: AbortSignal) => + encodeToolExecutionResult(await func(argsCodec.fromJson(jsonArgs), signal), resultCodec), + options.handle, + options.attributes, + options.data, + options.metadata, + options.toolCallId, + ); + return { + result: resultCodec.fromJson(jsonResult.result), + ...(jsonResult.annotation == null ? {} : { annotation: jsonResult.annotation }), + }; +} + +/** + * Executes an LLM call with a typed response. + * + * The LLM lifecycle runs on `request`, and the JSON response is decoded with + * `responseCodec` before it is returned. + * + * @param name - Provider name. + * @param request - Request with `headers` and `content`. + * @param func - The LLM call. It may return the response directly or in a promise. + * @param responseCodec - Converts the response to and from JSON. + * @param options - Scope handle, attributes, data, metadata, model name, and codecs. + * @returns The decoded response. + * @remarks `options.responseCodec` decodes the response copied into events. It + * does not change the returned response. + */ +export async function typedLlmExecute< + TRequest extends LlmRequestCompatible, + TResponse, + TResponseJson extends JsonCompatible = JsonValue, + TData extends JsonCompatible = JsonValue, + TMetadata extends JsonCompatible = JsonValue, + TAnnotated extends JsonCompatible = JsonValue, + TEncoded extends LlmRequestCompatible = LlmRequestShape, + TDecodedResponse extends JsonCompatible = JsonValue, +>( + name: string, + request: TRequest, + func: (request: LlmRequestShape, signal: AbortSignal) => TResponse | Promise, + responseCodec: Codec, + options: TypedLlmExecuteOptions = {}, +): Promise { + const { codec, responseCodec: eventCodec } = options; + const jsonResult = await lib.llmCallExecuteAsync( + name, + request, + async (jsonRequest: LlmRequestShape, signal: AbortSignal) => responseCodec.toJson(await func(jsonRequest, signal)), + options.handle, + options.attributes, + options.data, + options.metadata, + options.modelName, + codec == null ? undefined : (jsonRequest: LlmRequestShape) => codec.decode(jsonRequest), + codec == null ? undefined : ({ annotated, original }) => codec.encode(annotated, original), + eventCodec == null ? undefined : (response: Json) => eventCodec.decodeResponse(response), + ); + return responseCodec.fromJson(jsonResult); +} + +async function* encodeChunks>( + source: AsyncIterable, + chunkCodec: Codec, +): AsyncIterable { + for await (const typedChunk of source) { + yield chunkCodec.toJson(typedChunk); + } +} + +/** + * Executes a streaming LLM call with typed chunks and response. + * + * Each chunk from `func` is encoded with `chunkCodec` before middleware sees it + * and decoded again before `collector` receives it. The value from `finalizer` + * is encoded with `responseCodec`. + * + * @param name - Provider name. + * @param request - Request with `headers` and `content`. + * @param func - The streaming LLM call. It receives the request and an `AbortSignal` and returns an async iterable of chunks. + * @param collector - Receives one decoded chunk at a time. It may return a promise. + * @param finalizer - Returns the final response once the stream ends. It may return a promise. + * @param chunkCodec - Converts chunks to and from JSON. + * @param responseCodec - Converts the final response to and from JSON. + * @param options - Scope handle, attributes, data, metadata, model name, and codecs. + * @returns The `LlmStream` to read chunks from. + * @remarks An error in `collector` fails the stream. An error in `finalizer` + * is thrown by `stream.close()`. A reader that stops early must await + * `stream.close()`, which aborts the signal and waits for cleanup. + */ +export function typedLlmStreamExecute< + TRequest extends LlmRequestCompatible, + TChunk, + TResponse, + TChunkJson extends JsonCompatible = JsonValue, + TResponseJson extends JsonCompatible = JsonValue, + TData extends JsonCompatible = JsonValue, + TMetadata extends JsonCompatible = JsonValue, + TAnnotated extends JsonCompatible = JsonValue, + TEncoded extends LlmRequestCompatible = LlmRequestShape, + TDecodedResponse extends JsonCompatible = JsonValue, +>( + name: string, + request: TRequest, + func: (request: LlmRequestShape, signal: AbortSignal) => AsyncIterable | Promise>, + collector: (chunk: TChunk) => void | Promise, + finalizer: () => TResponse | Promise, + chunkCodec: Codec, + responseCodec: Codec, + options: TypedLlmStreamExecuteOptions = {}, +): Promise { + const { codec, responseCodec: eventCodec } = options; + return lib.llmStreamCallExecute( + name, + request, + async (jsonRequest: LlmRequestShape, signal: AbortSignal) => { + const source = await func(jsonRequest, signal); + if (source == null || typeof source[Symbol.asyncIterator] !== 'function') { + throw new TypeError('typed streaming callback must return an AsyncIterable'); + } + return encodeChunks(source, chunkCodec); + }, + (jsonChunk: Json) => collector(chunkCodec.fromJson(jsonChunk)), + async () => responseCodec.toJson(await finalizer()), + options.handle, + options.attributes, + options.data, + options.metadata, + options.modelName, + codec == null ? undefined : (jsonRequest: LlmRequestShape) => codec.decode(jsonRequest), + codec == null ? undefined : ({ annotated, original }) => codec.encode(annotated, original), + eventCodec == null ? undefined : (response: Json) => eventCodec.decodeResponse(response), + ); +} diff --git a/crates/node/tsconfig.json b/crates/node/tsconfig.json new file mode 100644 index 0000000000..b37726ff2f --- /dev/null +++ b/crates/node/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2023", + "lib": ["ESNext"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "rootDir": "ts", + "outDir": "dist", + "declaration": true, + "sourceMap": true, + "strict": true, + "isolatedModules": true, + "isolatedDeclarations": true, + "erasableSyntaxOnly": true, + "esModuleInterop": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "types": ["node"] + }, + "include": ["ts/**/*.ts"] +} diff --git a/crates/node/typed.d.ts b/crates/node/typed.d.ts deleted file mode 100644 index 7035d2920f..0000000000 --- a/crates/node/typed.d.ts +++ /dev/null @@ -1,257 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -/** - * Typed wrappers for NeMo Relay Node.js execute APIs. - * - * Provides generic typed versions of `toolCallExecute` and `llmCallExecute` - * that use explicit `Codec` objects to serialize/deserialize at the API - * boundary. - */ - -import type { JsonCompatible, JsonValue, LlmRequestCompatible, LlmRequestShape, LlmStream, ScopeHandle } from './index'; - -export type { - Json, - JsonArray, - JsonCompatible, - JsonObject, - JsonPrimitive, - JsonValue, - LlmRequestCompatible, - LlmRequestShape, -} from './index'; - -/** Canonical application-visible result of tool execution. */ -export interface ToolExecutionResult { - result: TResult; - annotation?: JsonValue; -} - -/** - * A codec for annotating and unwrapping LLM JSON request payloads. - * - * Use when an LLM integration needs custom request parsing or normalization - * before the raw payload is passed through the NeMo Relay LLM middleware - * pipeline. - */ -export interface LlmCodec< - TAnnotated extends JsonCompatible = JsonValue, - TEncoded extends LlmRequestCompatible = LlmRequestShape, -> { - /** - * Decode a raw LLM request payload into the normalized JSON shape used by - * the codec. - * - * @param request - The raw request payload as a JSON-serializable value. - * @returns The decoded or normalized JSON request representation. - */ - decode(request: LlmRequestShape): TAnnotated; - /** - * Encode an annotated request payload back into the JSON shape expected by - * the underlying LLM implementation. - * - * @param annotated - The normalized or annotated JSON payload produced by middleware. - * @param original - The original JSON request payload before annotation. - * @returns A JSON payload ready to pass to the underlying LLM implementation. - */ - encode(annotated: JsonValue, original: LlmRequestShape): TEncoded; -} - -/** - * A codec for normalizing and decoding raw LLM responses into `JsonValue`. - * - * Use when model output requires custom parsing or normalization before the - * response is consumed as plain JSON by the library. - */ -export interface LlmResponseCodec = JsonValue> { - /** - * Decode a raw LLM response payload into the normalized JSON shape expected - * by the library. - * - * Implementations should return a JSON-serializable value and should prefer - * normalizing missing or provider-specific fields into a stable shape when - * possible. - * - * @param response - The raw response payload as a JSON-serializable value. - * @returns The decoded or normalized JSON response representation. - */ - decodeResponse(response: JsonValue): TDecoded; -} - -/** - * A codec that converts between a typed value `T` and a JSON-serializable - * representation (`JsonValue` by default). - */ -export interface Codec { - /** Convert a typed value to a JSON-serializable object. */ - toJson(value: T): TJson; - /** Reconstruct a typed value from a JSON-serializable object. */ - fromJson(data: TJson): T; -} - -/** - * A passthrough codec that performs no conversion. - * Use when arguments or results are already plain JSON objects. - */ -export declare class JsonPassthrough implements Codec { - toJson(value: JsonValue): JsonValue; - fromJson(data: JsonValue): JsonValue; -} - -/** Options for `typedToolExecute`. */ -export interface TypedToolExecuteOptions< - TData extends JsonCompatible = JsonValue, - TMetadata extends JsonCompatible = JsonValue, -> { - handle?: ScopeHandle | null; - attributes?: number | null; - data?: TData; - metadata?: TMetadata; - toolCallId?: string | null; -} - -/** Options for `typedLlmExecute`. */ -export interface TypedLlmExecuteOptions< - TData extends JsonCompatible = JsonValue, - TMetadata extends JsonCompatible = JsonValue, - TAnnotated extends JsonCompatible = JsonValue, - TEncoded extends LlmRequestCompatible = LlmRequestShape, - TDecodedResponse extends JsonCompatible = JsonValue, -> { - handle?: ScopeHandle | null; - attributes?: number | null; - data?: TData; - metadata?: TMetadata; - modelName?: string | null; - codec?: LlmCodec | null; - responseCodec?: LlmResponseCodec | null; -} - -/** - * Execute a typed tool call through the JSON middleware pipeline. - * - * Converts `args` to JSON, runs the native tool execution lifecycle, and - * decodes only `result` back into the caller's typed result shape while - * preserving `annotation` unchanged. - * - * @param name - Tool name. - * @param args - Typed tool arguments. - * @param func - The tool implementation. - * @param argsCodec - Codec for args serialization/deserialization. - * @param resultCodec - Codec for result serialization/deserialization. - * @param options - Optional scope handle, attributes, data, metadata, and toolCallId. - * @returns A promise resolving to the decoded typed result and opaque annotation. - * @remarks The wrapper accepts both synchronous and promise-returning tool - * implementations; codec failures and native execution errors propagate to the - * returned promise. - */ -export declare function typedToolExecute< - TArgs, - TResult, - TArgsJson extends JsonCompatible = JsonValue, - TResultJson extends JsonCompatible = JsonValue, - TData extends JsonCompatible = JsonValue, - TMetadata extends JsonCompatible = JsonValue, ->( - name: string, - args: TArgs, - func: (args: TArgs, signal: AbortSignal) => ToolExecutionResult | Promise>, - argsCodec: Codec, - resultCodec: Codec, - options?: TypedToolExecuteOptions, -): Promise>; - -/** - * Execute a typed LLM call through the JSON middleware pipeline. - * - * Forwards the JSON-shaped request payload into the native LLM lifecycle and - * decodes the final response with the supplied response codec before resolving. - * - * @param name - Model/provider name. - * @param request - The LLM request object ({headers, content}). - * @param func - The LLM implementation. - * @param responseCodec - Codec for response serialization/deserialization. - * @param options - Optional scope handle, attributes, data, metadata, modelName. - * @returns A promise resolving to the decoded typed LLM response. - * @remarks `options.responseCodec` only affects annotated response event - * payloads; failures while decoding those event payloads are downgraded to - * debug logging and do not rewrite the caller-visible response. - */ -export declare function typedLlmExecute< - TRequest extends LlmRequestCompatible, - TResponse, - TResponseJson extends JsonCompatible = JsonValue, - TData extends JsonCompatible = JsonValue, - TMetadata extends JsonCompatible = JsonValue, - TAnnotated extends JsonCompatible = JsonValue, - TEncoded extends LlmRequestCompatible = LlmRequestShape, - TDecodedResponse extends JsonCompatible = JsonValue, ->( - name: string, - request: TRequest, - func: (request: LlmRequestShape, signal: AbortSignal) => TResponse | Promise, - responseCodec: Codec, - options?: TypedLlmExecuteOptions, -): Promise; - -/** Options for `typedLlmStreamExecute`. */ -export interface TypedLlmStreamExecuteOptions< - TData extends JsonCompatible = JsonValue, - TMetadata extends JsonCompatible = JsonValue, - TAnnotated extends JsonCompatible = JsonValue, - TEncoded extends LlmRequestCompatible = LlmRequestShape, - TDecodedResponse extends JsonCompatible = JsonValue, -> { - handle?: ScopeHandle | null; - attributes?: number | null; - data?: TData; - metadata?: TMetadata; - modelName?: string | null; - codec?: LlmCodec | null; - responseCodec?: LlmResponseCodec | null; -} - -/** - * Execute a typed streaming LLM call through the JSON middleware pipeline. - * - * Chunks yielded by `func` are converted to JSON via `chunkCodec.toJson` - * before entering the middleware pipeline. After interception, chunks are - * converted back via `chunkCodec.fromJson` before reaching `collector`. - * The `finalizer` result is converted via `responseCodec.toJson`. - * - * @param name - Model/provider name. - * @param request - The LLM request object ({headers, content}). - * @param func - Async iterable producer for typed stream chunks; receives the request and an `AbortSignal`. - * @param collector - Callback invoked with each decoded typed chunk; may return a Promise. - * @param finalizer - Callback that returns the final typed aggregate response, directly or through a Promise. - * @param chunkCodec - Codec for stream chunk serialization/deserialization. - * @param responseCodec - Codec for final response serialization/deserialization. - * @param options - Optional scope handle, attributes, data, metadata, modelName. - * @returns A promise resolving to the native `LlmStream` handle. - * @remarks If `collector` throws or rejects, the stream fails with that error. - * If `finalizer` throws or rejects, the aggregate is `null` and `stream.close()` - * throws the failure. Consumers that stop reading early must await - * `stream.close()`, which aborts the signal and waits for producer cleanup. - */ -export declare function typedLlmStreamExecute< - TRequest extends LlmRequestCompatible, - TChunk, - TResponse, - TChunkJson extends JsonCompatible = JsonValue, - TResponseJson extends JsonCompatible = JsonValue, - TData extends JsonCompatible = JsonValue, - TMetadata extends JsonCompatible = JsonValue, - TAnnotated extends JsonCompatible = JsonValue, - TEncoded extends LlmRequestCompatible = LlmRequestShape, - TDecodedResponse extends JsonCompatible = JsonValue, ->( - name: string, - request: TRequest, - func: (request: LlmRequestShape, signal: AbortSignal) => AsyncIterable | Promise>, - collector: (chunk: TChunk) => void | Promise, - finalizer: () => TResponse | Promise, - chunkCodec: Codec, - responseCodec: Codec, - options?: TypedLlmStreamExecuteOptions, -): Promise; diff --git a/crates/node/typed.js b/crates/node/typed.js deleted file mode 100644 index aaaa134699..0000000000 --- a/crates/node/typed.js +++ /dev/null @@ -1,259 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -/** - * Typed wrappers for NeMo Relay Node.js execute APIs. - * - * Provides generic typed versions of `toolCallExecute` and `llmCallExecute` - * that use explicit `Codec` objects to serialize/deserialize at the API - * boundary. The native runtime operates on plain JSON throughout. - * - * @example - * const { typedToolExecute, JsonPassthrough } = require('./typed'); - * const myCodec = { - * toJson(val) { return { x: val.x }; }, - * fromJson(data) { return new MyClass(data.x); }, - * }; - * const result = await typedToolExecute('tool', myObj, fn, myCodec, myResultCodec); - */ - -'use strict'; - -const { createRequire } = require('node:module'); -const path = require('node:path'); - -// Load the native binding from the same directory as this file. -const nativeRequire = createRequire(path.join(__dirname, 'index.js')); -const lib = nativeRequire('./index.js'); - -/** - * A passthrough codec that performs no conversion (identity). - */ -class JsonPassthrough { - toJson(value) { - return value; - } - fromJson(data) { - return data; - } -} - -function collectCodecReturn(value) { - return value === undefined ? null : value; -} - -function encodeToolExecutionResult(executionResult, resultCodec) { - if (executionResult === null || typeof executionResult !== 'object' || !('result' in executionResult)) { - throw new TypeError('typed tool callback must return ToolExecutionResult'); - } - const result = executionResult.result; - const annotation = executionResult.annotation; - return { - result: resultCodec.toJson(result), - ...(annotation == null ? {} : { annotation }), - }; -} - -function decodeResponseWithCodec(codec, response) { - try { - return collectCodecReturn(codec.decodeResponse(response)); - } catch (err) { - if (typeof console !== 'undefined' && typeof console.debug === 'function') { - console.debug('decodeResponseWithCodec failed', { - error: err, - response, - }); - } - return null; - } -} - -/** - * Execute a typed tool call through the JSON middleware pipeline. - * - * Converts typed arguments to JSON, invokes the native tool execution - * lifecycle, and decodes the final JSON result back into the caller's typed - * result shape while preserving the opaque annotation unchanged. - * - * @template TArgs - * @template TResult - * @param {string} name - Tool name reported to the runtime. - * @param {TArgs} args - Typed tool arguments supplied by the caller. - * @param {function(TArgs, AbortSignal): { result: TResult, annotation?: * } | Promise<{ result: TResult, annotation?: * }>} func - Tool implementation to execute. - * @param {Codec} argsCodec - Codec used to serialize and deserialize tool args. - * @param {Codec} resultCodec - Codec used to serialize and deserialize tool results. - * @param {object} [options] - Optional execution-scoping metadata. - * @param {string} [options.toolCallId] - Provider-specific tool call correlation ID. - * @returns {Promise<{ result: TResult, annotation?: * }>} A promise resolving to the decoded typed tool result. - * @remarks The wrapper accepts both synchronous and promise-returning tool - * implementations; codec failures and native execution errors propagate to the - * returned promise. - */ -async function typedToolExecute(name, args, func, argsCodec, resultCodec, options) { - const opts = options || {}; - const jsonArgs = argsCodec.toJson(args); - - const jsonFunc = (jsonArgsInner, signal) => { - const typedArgs = argsCodec.fromJson(jsonArgsInner); - const typedResult = func(typedArgs, signal); - if (typedResult && typeof typedResult.then === 'function') { - return typedResult.then((result) => encodeToolExecutionResult(result, resultCodec)); - } - return encodeToolExecutionResult(typedResult, resultCodec); - }; - - const jsonResult = await lib.toolCallExecuteAsync( - name, - jsonArgs, - jsonFunc, - opts.handle ?? null, - opts.attributes ?? null, - opts.data ?? null, - opts.metadata ?? null, - opts.toolCallId ?? null, - ); - - return { - result: resultCodec.fromJson(jsonResult.result), - ...(jsonResult.annotation == null ? {} : { annotation: jsonResult.annotation }), - }; -} - -/** - * Execute a typed LLM call through the JSON middleware pipeline. - * - * Forwards the JSON-shaped request payload into the native LLM lifecycle and - * decodes the final response with the supplied response codec before resolving. - * - * @template TResponse - * @param {string} name - Model/provider name. - * @param {*} request - The LLM request object ({headers, content}). - * @param {function(*, AbortSignal): TResponse | Promise} func - The LLM implementation. - * @param {Codec} responseJsonCodec - Codec for serializing/deserializing the response. - * @param {object} [options] - Optional parameters. - * @param {JsScopeHandle} [options.handle] - Parent scope handle. - * @param {number} [options.attributes] - LLM attribute bitflags. - * @param {*} [options.data] - Application data. - * @param {*} [options.metadata] - Metadata. - * @param {string} [options.modelName] - Model name for ATIF export. - * @param {{ decode(request: *): *, encode(annotated: *, original: *): * }} [options.codec] - * Request codec for annotated request intercepts. - * @param {{ decodeResponse(response: *): * }} [options.responseCodec] - * Response codec for annotated response events. - * @returns {Promise} A promise resolving to the decoded typed LLM response. - * @remarks `options.responseCodec` only affects annotated response event - * payloads; failures while decoding those event payloads are downgraded to - * debug logging and do not rewrite the caller-visible response. - */ -async function typedLlmExecute(name, request, func, responseJsonCodec, options) { - const opts = options || {}; - - const jsonFunc = (req, signal) => { - const typedResult = func(req, signal); - if (typedResult && typeof typedResult.then === 'function') { - return typedResult.then((r) => responseJsonCodec.toJson(r)); - } - return responseJsonCodec.toJson(typedResult); - }; - - const jsonResult = await lib.llmCallExecuteAsync( - name, - request, - jsonFunc, - opts.handle ?? null, - opts.attributes ?? null, - opts.data ?? null, - opts.metadata ?? null, - opts.modelName ?? null, - opts.codec ? opts.codec.decode.bind(opts.codec) : null, - opts.codec ? (payload) => opts.codec.encode(payload.annotated, payload.original) : null, - opts.responseCodec ? (response) => decodeResponseWithCodec(opts.responseCodec, response) : null, - ); - - return responseJsonCodec.fromJson(jsonResult); -} - -/** - * Execute a typed streaming LLM call through the JSON middleware pipeline. - * - * Chunks yielded by `func` are converted to JSON via `chunkCodec.toJson` - * before entering the middleware pipeline. After interception, chunks are - * converted back via `chunkCodec.fromJson` before reaching `collector`. - * The `finalizer` result is converted via `responseCodec.toJson`. - * - * @template TChunk - * @template TResponse - * Accepts the public positional call shape - * `(name, request, func, collector, finalizer, chunkJsonCodec, responseJsonCodec, options)`. - * The implementation captures the trailing codec/options values via rest - * parameters to keep the external API stable while avoiding an oversized formal - * parameter list. - * - * @param {string} name - Model/provider name. - * @param {*} request - The LLM request object ({headers, content}). - * @param {function(*, AbortSignal): AsyncIterable | Promise>} func - The streaming LLM implementation. - * @param {function(TChunk): void | Promise} collector - Called with each typed chunk after intercepts. - * @param {function(): TResponse | Promise} finalizer - Called once when the stream is exhausted or closed early. - * @param {Codec} chunkJsonCodec - Codec for serializing/deserializing chunks. - * @param {Codec} responseJsonCodec - Codec for serializing/deserializing the final response. - * @param {object} [options] - Optional parameters. - * @param {JsScopeHandle} [options.handle] - Parent scope handle. - * @param {number} [options.attributes] - LLM attribute bitflags. - * @param {*} [options.data] - Application data. - * @param {*} [options.metadata] - Metadata. - * @param {string} [options.modelName] - Model name for ATIF export. - * @param {{ decode(request: *): *, encode(annotated: *, original: *): * }} [options.codec] - * Request codec for annotated request intercepts. - * @param {{ decodeResponse(response: *): * }} [options.responseCodec] - * Response codec for annotated response events. - * @returns {Promise} A promise resolving to the native stream handle. - * @remarks Chunks are encoded as they are pulled from `func`. A throw or - * rejection from `func` rejects the call, an iterator failure rejects the - * pending `next()`, and the native bridge aborts the signal and runs iterator - * cleanup when the consumer closes early. The producer must stop once the - * signal aborts. Consumers that stop reading early must await `stream.close()`, - * which waits for that cleanup without a timeout. - */ -async function typedLlmStreamExecute(name, request, func, collector, finalizer, ...streamArgs) { - const [chunkJsonCodec, responseJsonCodec, options] = streamArgs; - const opts = options || {}; - - const jsonFunc = async (jsonRequest, signal) => { - const source = await func(jsonRequest, signal); - if (source == null || typeof source[Symbol.asyncIterator] !== 'function') { - throw new TypeError('typed streaming callback must return an AsyncIterable'); - } - return encodeChunks(source, chunkJsonCodec); - }; - const jsonCollector = (jsonChunk) => collector(chunkJsonCodec.fromJson(jsonChunk)); - const jsonFinalizer = async () => collectCodecReturn(responseJsonCodec.toJson(await finalizer())); - - return lib.llmStreamCallExecute( - name, - request, - jsonFunc, - jsonCollector, - jsonFinalizer, - opts.handle ?? null, - opts.attributes ?? null, - opts.data ?? null, - opts.metadata ?? null, - opts.modelName ?? null, - opts.codec ? opts.codec.decode.bind(opts.codec) : null, - opts.codec ? (payload) => opts.codec.encode(payload.annotated, payload.original) : null, - opts.responseCodec ? (response) => decodeResponseWithCodec(opts.responseCodec, response) : null, - ); -} - -async function* encodeChunks(source, chunkJsonCodec) { - for await (const typedChunk of source) { - yield chunkJsonCodec.toJson(typedChunk); - } -} - -module.exports = { - JsonPassthrough, - typedToolExecute, - typedLlmExecute, - typedLlmStreamExecute, -}; diff --git a/justfile b/justfile index 062e937d60..8a480cc356 100644 --- a/justfile +++ b/justfile @@ -177,17 +177,19 @@ ensure_docs_dependencies() { ensure_node_docs_declarations() { cd "$NEMO_RELAY_REPO_ROOT" - local types_file="crates/node/index.d.ts" + local types_file="crates/node/dist/index.d.ts" + local binding_file="crates/node/binding.d.ts" local newest_source="" - if [[ -f "$types_file" ]]; then + if [[ -f "$types_file" && -f "$binding_file" ]]; then newest_source="$( - find crates/node/src crates/node/Cargo.toml crates/node/build.rs crates/node/root-types.d.ts \ + find crates/node/src crates/node/ts crates/node/Cargo.toml crates/node/build.rs \ + crates/node/binding-types.d.ts crates/node/tsconfig.json \ -type f -newer "$types_file" -print -quit )" fi - if [[ ! -f "$types_file" || -n "$newest_source" ]]; then + if [[ ! -f "$types_file" || ! -f "$binding_file" || -n "$newest_source" ]]; then npm run build-debug --workspace=nemo-relay-node fi } @@ -196,6 +198,7 @@ generate_docs_api_references() { cd "$NEMO_RELAY_REPO_ROOT" uv run --no-sync python scripts/docs/generate_python_library_reference.py ensure_node_docs_declarations + uv run --no-sync python -m unittest scripts.tests.test_node_library_reference uv run --no-sync python scripts/docs/generate_node_library_reference.py uv run --no-sync python -m unittest scripts.tests.test_rust_library_reference uv run --no-sync python scripts/docs/generate_rust_library_reference.py @@ -1310,8 +1313,9 @@ clean: crates/**/*.profraw \ crates/node/*.node \ crates/node/coverage \ - crates/node/index.d.ts \ - crates/node/index.js \ + crates/node/binding.d.ts \ + crates/node/binding.js \ + crates/node/dist \ crates/node/junit.xml \ crates/node/node_modules \ integrations/openclaw/*.profraw \ diff --git a/package-lock.json b/package-lock.json index 904da55e0c..94955cc668 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "license": "Apache-2.0", "devDependencies": { "@napi-rs/cli": "^3.10.9", + "@types/node": "^24.0.0", "c8": "^11.0.0", "prettier": "^3.8.2", "typescript": "^5.9.3" @@ -33,7 +34,7 @@ "node": ">=24.13.1" }, "peerDependencies": { - "typescript": ">=5.2" + "typescript": ">=5.7" }, "peerDependenciesMeta": { "typescript": { diff --git a/scripts/docs/generate_node_library_reference.py b/scripts/docs/generate_node_library_reference.py index b6b4911b8b..1a755d52b2 100644 --- a/scripts/docs/generate_node_library_reference.py +++ b/scripts/docs/generate_node_library_reference.py @@ -20,7 +20,8 @@ r"(?Penum|interface|class|function|type|const)\s+" r"(?P[A-Za-z_$][A-Za-z0-9_$]*)" ) -REEXPORT_RE = re.compile(r"^export\s+\{(?P.*?)\};?$") +REEXPORT_RE = re.compile(r"^export\s+(?:type\s+)?\{(?P.*?)\}(?:\s+from\s+['\"](?P[^'\"]+)['\"])?;?$") +STAR_REEXPORT_RE = re.compile(r"^export\s+\*\s+from\s+['\"](?P[^'\"]+)['\"];?$") INTERNAL_NAME_PREFIXES = ("__test",) SECTION_ORDER = ("interface", "class", "enum", "function", "type", "const") SECTION_TITLES = { @@ -94,6 +95,7 @@ class ModuleDoc: description: str items: list[ApiItem] = field(default_factory=list) reexports: list[str] = field(default_factory=list) + star_sources: list[Path] = field(default_factory=list) def _escape_text(value: str) -> str: @@ -231,7 +233,7 @@ def _collect_braced_declaration(lines: list[str], start: int) -> tuple[str, int] return "\n".join(collected).rstrip(), index -def _collect_statement_declaration(lines: list[str], start: int, kind: str) -> tuple[str, int]: +def _collect_statement_declaration(lines: list[str], start: int) -> tuple[str, int]: collected: list[str] = [] paren_depth = 0 brace_depth = 0 @@ -246,9 +248,9 @@ def _collect_statement_declaration(lines: list[str], start: int, kind: str) -> t stripped = line.strip() index += 1 - if stripped.endswith(";"): - break - if kind == "function" and paren_depth <= 0 and brace_depth <= 0 and bracket_depth <= 0: + if paren_depth > 0 or brace_depth > 0 or bracket_depth > 0: + continue + if stripped.endswith(";") or index >= len(lines) or not lines[index].startswith((" ", "\t")): break return "\n".join(collected).rstrip(), index @@ -257,17 +259,37 @@ def _collect_statement_declaration(lines: list[str], start: int, kind: str) -> t def _collect_declaration(lines: list[str], start: int, kind: str) -> tuple[str, int]: if kind in {"interface", "class", "enum"}: return _collect_braced_declaration(lines, start) - return _collect_statement_declaration(lines, start, kind) + return _collect_statement_declaration(lines, start) def _is_public_name(name: str) -> bool: return not name.startswith("_") and not any(name.startswith(prefix) for prefix in INTERNAL_NAME_PREFIXES) +def _resolve_declaration_source(source: Path, specifier: str) -> Path: + if not specifier.startswith("."): + raise SystemExit(f"{source}: re-export from a package specifier is not supported: {specifier}") + target = source.parent / specifier + if target.suffix == ".js": + target = target.with_suffix(".d.ts") + elif target.suffix != ".ts": + target = target.with_name(f"{target.name}.d.ts") + if not target.is_file(): + raise SystemExit(f"{source}: re-exported declaration file not found: {target}") + return target.resolve() + + +def _parse_star_reexport(source: Path, declaration: str) -> Path: + match = STAR_REEXPORT_RE.match(" ".join(declaration.split())) + if match is None: + raise SystemExit(f"{source}: unsupported export statement: {declaration}") + return _resolve_declaration_source(source, match.group("specifier")) + + def _parse_reexport_names(declaration: str) -> list[str]: match = REEXPORT_RE.match(" ".join(declaration.split())) if match is None: - return [] + raise SystemExit(f"unsupported export statement: {declaration}") names: list[str] = [] for item in match.group("names").split(","): @@ -313,8 +335,12 @@ def _parse_declaration_line( if pending_doc.summary and module_doc is None and not module.items: module_doc = pending_doc return JsDoc(), module_doc, index + 1 - if stripped.startswith("export {"): - declaration, next_index = _collect_statement_declaration(lines, index, "reexport") + if stripped.startswith("export *"): + declaration, next_index = _collect_statement_declaration(lines, index) + module.star_sources.append(_parse_star_reexport(module.source, declaration)) + return JsDoc(), module_doc, next_index + if stripped.startswith(("export {", "export type {")): + declaration, next_index = _collect_statement_declaration(lines, index) module.reexports.extend(_parse_reexport_names(declaration)) return JsDoc(), module_doc, next_index match = EXPORT_RE.match(stripped) @@ -328,6 +354,27 @@ def _parse_declaration_line( return JsDoc(), module_doc, next_index +def _resolve_star_exports(module: ModuleDoc, parsed: dict[Path, ModuleDoc], visiting: tuple[Path, ...] = ()) -> None: + source = module.source.resolve() + if source in visiting: + cycle = " -> ".join(str(path) for path in (*visiting, source)) + raise SystemExit(f"re-export cycle: {cycle}") + names = {item.name for item in module.items} + for target_path in module.star_sources: + target = parsed.get(target_path) + if target is None: + target = _parse_declaration_file(target_path, str(target_path)) + parsed[target_path] = target + _resolve_star_exports(target, parsed, (*visiting, source)) + for item in target.items: + if item.name not in names: + module.items.append(item) + names.add(item.name) + module.reexports.extend(name for name in target.reexports if name not in names) + module.star_sources = [] + module.reexports = sorted(set(module.reexports)) + + def _package_exports(package_json: Path) -> list[tuple[str, Path]]: data = json.loads(package_json.read_text(encoding="utf-8")) package_name = data["name"] @@ -429,6 +476,9 @@ def generate(package_root: Path, output_dir: Path) -> int: modules = [ _parse_declaration_file(types_path, import_path) for import_path, types_path in _package_exports(package_json) ] + parsed = {module.source.resolve(): module for module in modules} + for module in modules: + _resolve_star_exports(module, parsed) reset_output_dir(output_dir) _write_index(output_dir, modules) diff --git a/scripts/lint/check_public_docstrings.mjs b/scripts/lint/check_public_docstrings.mjs index f1643d7818..89f1cfbc7f 100644 --- a/scripts/lint/check_public_docstrings.mjs +++ b/scripts/lint/check_public_docstrings.mjs @@ -8,7 +8,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url'; const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..'); const nodeEntries = [ { - file: 'crates/node/plugin.js', + file: 'crates/node/ts/plugin.ts', functions: [ { name: 'defaultConfig', params: [] }, { name: 'ComponentSpec', params: ['kind', 'config', 'options'] }, @@ -21,7 +21,7 @@ const nodeEntries = [ ], }, { - file: 'crates/node/adaptive.js', + file: 'crates/node/ts/adaptive.ts', functions: [ { name: 'defaultConfig', params: [] }, { name: 'inMemoryBackend', params: [] }, @@ -30,47 +30,12 @@ const nodeEntries = [ { name: 'adaptiveHintsConfig', params: ['config'] }, { name: 'toolParallelismConfig', params: ['config'] }, { name: 'acgConfig', params: ['config'] }, + { name: 'responseCacheConfig', params: ['config'] }, { name: 'ComponentSpec', params: ['config', 'options'] }, ], }, { - file: 'crates/node/typed.js', - functions: [ - { name: 'typedToolExecute', params: ['name', 'args', 'func', 'argsCodec', 'resultCodec', 'options'] }, - { name: 'typedLlmExecute', params: ['name', 'request', 'func', 'responseJsonCodec', 'options'] }, - { - name: 'typedLlmStreamExecute', - params: ['name', 'request', 'func', 'collector', 'finalizer', 'chunkJsonCodec', 'responseJsonCodec', 'options'], - }, - ], - }, - { - file: 'crates/node/plugin.d.ts', - functions: [ - { name: 'defaultConfig', params: [] }, - { name: 'ComponentSpec', params: ['kind', 'config', 'options'] }, - { name: 'initialize', params: ['config', 'additionalPluginsToml'] }, - { name: 'validate', params: ['config', 'additionalPluginsToml'] }, - { name: 'listKinds', params: [] }, - { name: 'register', params: ['pluginKind', 'plugin'] }, - { name: 'deregister', params: ['pluginKind'] }, - ], - }, - { - file: 'crates/node/adaptive.d.ts', - functions: [ - { name: 'defaultConfig', params: [] }, - { name: 'inMemoryBackend', params: [] }, - { name: 'redisBackend', params: ['url', 'keyPrefix'] }, - { name: 'telemetryConfig', params: ['config'] }, - { name: 'adaptiveHintsConfig', params: ['config'] }, - { name: 'toolParallelismConfig', params: ['config'] }, - { name: 'acgConfig', params: ['config'] }, - { name: 'ComponentSpec', params: ['config', 'options'] }, - ], - }, - { - file: 'crates/node/typed.d.ts', + file: 'crates/node/ts/typed.ts', functions: [ { name: 'typedToolExecute', params: ['name', 'args', 'func', 'argsCodec', 'resultCodec', 'options'] }, { name: 'typedLlmExecute', params: ['name', 'request', 'func', 'responseCodec', 'options'] }, diff --git a/scripts/package-node-bin.py b/scripts/package-node-bin.py index d9b8652644..8d35bd148d 100755 --- a/scripts/package-node-bin.py +++ b/scripts/package-node-bin.py @@ -64,7 +64,14 @@ def repository_metadata(source: dict[str, object]) -> dict[str, object]: def metapackage_files(manifest: dict[str, object]) -> list[str]: """Return public entry points, declarations, and private runtime dependencies.""" - files = {str(manifest["main"]), str(manifest["types"]), "README.md", "diagnostics_query_contract.js"} + files = { + str(manifest["main"]), + str(manifest["types"]), + "README.md", + "binding.js", + "binding.d.ts", + "dist/diagnostics_query_contract.js", + } exports = manifest.get("exports") if not isinstance(exports, dict): raise ValueError("Node package manifest is missing exports") @@ -72,6 +79,17 @@ def metapackage_files(manifest: dict[str, object]) -> list[str]: if not isinstance(entry, dict): raise ValueError("Node package export must contain types and default paths") files.update(str(path).removeprefix("./") for path in entry.values()) + types_versions = manifest.get("typesVersions") + if not isinstance(types_versions, dict): + raise ValueError("Node package manifest is missing typesVersions") + for selector in types_versions.values(): + if not isinstance(selector, dict): + raise ValueError("Node package typesVersions selector must map subpaths to declaration paths") + for paths in selector.values(): + if not isinstance(paths, list): + raise ValueError("Node package typesVersions subpath must list declaration paths") + files.update(str(path) for path in paths) + files.update(f"{path}.map" for path in list(files) if path.endswith(".js") and path.startswith("dist/")) return sorted(files) @@ -113,6 +131,7 @@ def build_metapackage(node_dir: Path, version: str, output: Path) -> Path: "main": source_manifest["main"], "types": source_manifest["types"], "exports": source_manifest["exports"], + "typesVersions": source_manifest["typesVersions"], "files": metapackage_files(source_manifest), "optionalDependencies": {platform.package_name: version for platform in PLATFORMS.values()}, } diff --git a/scripts/tests/test_node_library_reference.py b/scripts/tests/test_node_library_reference.py new file mode 100644 index 0000000000..59b4ba7ecc --- /dev/null +++ b/scripts/tests/test_node_library_reference.py @@ -0,0 +1,123 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Keep re-exported declarations in generated Node.js reference pages.""" + +import importlib +import json +import sys +import tempfile +import textwrap +import unittest +from pathlib import Path +from unittest.mock import patch + +with patch.object(sys, "path", [str(Path(__file__).resolve().parents[1] / "docs"), *sys.path]): + REFERENCE = importlib.import_module("generate_node_library_reference") + +ROOT = Path(__file__).resolve().parents[2] + + +def write(path: Path, content: str) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(textwrap.dedent(content).lstrip(), encoding="utf-8") + + +class NodeReferenceTests(unittest.TestCase): + def generate(self, files: dict[str, str]) -> dict[str, str]: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + package_root = root / "package" + exports = { + "." if name == "index" else f"./{name}": { + "types": f"./dist/{name}.d.ts", + "default": f"./dist/{name}.js", + } + for name in ("index", "helper") + } + write(package_root / "package.json", json.dumps({"name": "sample", "exports": exports})) + for relative, content in files.items(): + write(package_root / relative, content) + output = root / "generated" + REFERENCE.generate(package_root, output) + return {path.name: path.read_text(encoding="utf-8") for path in output.glob("*.mdx")} + + def test_star_reexport_inlines_semicolon_free_declarations(self) -> None: + pages = self.generate( + { + "binding.d.ts": """ + /** Scope stack. */ + export declare class ScopeStack { + get depth(): number + } + export declare const __napiBindingTarget: 'native' | 'wasm32-wasi' + export type Visibility = + | 'public' + | 'private' + /** Emits one event. */ + export declare function event(name: string, handle?: ScopeStack | null): void + export interface Inner { + value: number + } + """, + "dist/index.d.ts": """ + export * from '../binding.js'; + export declare class ScopeStack { + wrapped: true; + } + export declare class LlmRequest { + readonly content: string; + } + """, + "dist/helper.d.ts": """ + export type { Inner, Visibility as Kind } from '../binding.js'; + export { event } from '../binding.js'; + """, + } + ) + + runtime = pages["sample.mdx"] + self.assertIn("### `LlmRequest`", runtime) + self.assertIn("### `Visibility`", runtime) + self.assertIn("### `event`", runtime) + self.assertIn("### `Inner`", runtime) + self.assertIn("export type Visibility =\n | 'public'\n | 'private'", runtime) + self.assertEqual(runtime.count("### `ScopeStack`"), 1) + self.assertIn("wrapped: true;", runtime) + self.assertNotIn("get depth(): number", runtime) + self.assertNotIn("__napiBindingTarget", runtime) + + helper = pages["sample-helper.mdx"] + self.assertIn("## Re-exports\n\n- `Inner`\n- `Kind`\n- `event`\n", helper) + + def test_missing_reexport_target_fails(self) -> None: + with self.assertRaises(SystemExit): + self.generate( + { + "dist/index.d.ts": "export * from './missing.js';\n", + "dist/helper.d.ts": "", + } + ) + + def test_runtime_page_lists_binding_declarations(self) -> None: + binding = ROOT / "crates" / "node" / "binding.d.ts" + self.assertTrue(binding.is_file(), f"build the Node package first: {binding} is missing") + with tempfile.TemporaryDirectory() as directory: + output = Path(directory) + REFERENCE.generate(ROOT / "crates" / "node", output) + runtime = (output / "nemo-relay-node.mdx").read_text(encoding="utf-8") + for name in ( + "ScopeStack", + "ScopeHandle", + "LlmRequest", + "withScope", + "toolCallExecute", + "llmCallExecute", + "AtofExporter", + "registerLlmExecutionIntercept", + ): + self.assertIn(f"### `{name}`", runtime) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/tests/test_package_node_bin.py b/scripts/tests/test_package_node_bin.py index 89a3b3df26..0dbdd762bd 100644 --- a/scripts/tests/test_package_node_bin.py +++ b/scripts/tests/test_package_node_bin.py @@ -46,6 +46,15 @@ def test_print_platforms_lists_platform_keys_and_package_names(self) -> None: [f"{platform.key}\t{platform.package_name}" for platform in PACKAGE_NODE_BIN.PLATFORMS.values()], ) + def test_metapackage_files_require_types_versions(self) -> None: + manifest = { + "main": "dist/index.js", + "types": "dist/index.d.ts", + "exports": {".": {"types": "./dist/index.d.ts", "default": "./dist/index.js"}}, + } + with self.assertRaisesRegex(ValueError, "typesVersions"): + PACKAGE_NODE_BIN.metapackage_files(manifest) + def test_builds_metapackage_and_linux_native_package(self) -> None: with tempfile.TemporaryDirectory() as temporary: output = Path(temporary) @@ -55,25 +64,35 @@ def test_builds_metapackage_and_linux_native_package(self) -> None: "name": "nemo-relay-node", "version": "0.7.0", "description": "Node bindings.", - "main": "index.js", - "types": "index.d.ts", + "main": "dist/index.js", + "types": "dist/index.d.ts", "exports": { - ".": {"types": "./index.d.ts", "default": "./index.js"}, - "./typed": {"types": "./typed.d.ts", "default": "./typed.js"}, - "./diagnostics": {"types": "./diagnostics.d.ts", "default": "./diagnostics.js"}, + ".": {"types": "./dist/index.d.ts", "default": "./dist/index.js"}, + "./typed": {"types": "./dist/typed.d.ts", "default": "./dist/typed.js"}, + "./diagnostics": {"types": "./dist/diagnostics.d.ts", "default": "./dist/diagnostics.js"}, + }, + "typesVersions": { + "*": {"typed": ["dist/typed.d.ts"], "diagnostics": ["dist/diagnostics.d.ts"]}, }, "engines": {"node": ">=24.0.0"}, "license": "Apache-2.0", } (node_dir / "package.json").write_text(json.dumps(source_manifest)) + (node_dir / "dist").mkdir() for filename in ( - "index.js", - "index.d.ts", - "typed.js", - "typed.d.ts", - "diagnostics.js", - "diagnostics.d.ts", - "diagnostics_query_contract.js", + "binding.js", + "binding.d.ts", + "dist/index.js", + "dist/index.js.map", + "dist/index.d.ts", + "dist/typed.js", + "dist/typed.js.map", + "dist/typed.d.ts", + "dist/diagnostics.js", + "dist/diagnostics.js.map", + "dist/diagnostics.d.ts", + "dist/diagnostics_query_contract.js", + "dist/diagnostics_query_contract.js.map", "README.md", ): (node_dir / filename).write_text(filename) @@ -93,7 +112,7 @@ def test_builds_metapackage_and_linux_native_package(self) -> None: self.assertEqual(manifest["libc"], ["glibc"]) self.assertEqual(manifest["main"], binary_name) self.assertEqual(required_member(archive, f"package/{binary_name}").read(), b"native") - self.assertNotIn("package/index.js", archive.getnames()) + self.assertNotIn("package/dist/index.js", archive.getnames()) self.assertEqual(metapackage.name, "nemo-relay-node-npm-0.7.0-rc.1.tgz") with tarfile.open(metapackage) as archive: @@ -102,13 +121,21 @@ def test_builds_metapackage_and_linux_native_package(self) -> None: manifest["optionalDependencies"]["nemo-relay-node-linux-x64-gnu"], "0.7.0-rc.1", ) - self.assertIn("package/index.js", archive.getnames()) - self.assertIn("package/diagnostics.js", archive.getnames()) + self.assertEqual(manifest["typesVersions"], source_manifest["typesVersions"]) + names = archive.getnames() + self.assertIn("package/binding.js", names) + self.assertIn("package/dist/typed.d.ts", names) + self.assertIn("package/binding.d.ts", names) + self.assertIn("package/dist/index.js", names) + self.assertIn("package/dist/index.js.map", names) + self.assertIn("package/dist/diagnostics.js", names) + self.assertNotIn("package/index.js", names) self.assertEqual( - required_member(archive, "package/diagnostics_query_contract.js").read(), - b"diagnostics_query_contract.js", + required_member(archive, "package/dist/diagnostics_query_contract.js").read(), + b"dist/diagnostics_query_contract.js", ) - self.assertNotIn("./diagnostics_query_contract.js", manifest["exports"]) + self.assertIn("dist/diagnostics_query_contract.js.map", manifest["files"]) + self.assertNotIn("./dist/diagnostics_query_contract.js", manifest["exports"]) self.assertFalse(any(name.endswith(".node") for name in archive.getnames())) development = PACKAGE_NODE_BIN.build_metapackage(node_dir, "0.7.0+deadbeef", output)