diff --git a/packages/core/NEXT_CHANGELOG.md b/packages/core/NEXT_CHANGELOG.md index 03e865d3..cc26e694 100644 --- a/packages/core/NEXT_CHANGELOG.md +++ b/packages/core/NEXT_CHANGELOG.md @@ -4,6 +4,8 @@ ### New Features and Improvements +- Added a `meta-harness` user-agent dimension that reports the omnigent meta-harness (detected via the `OMNIGENT` environment variable) independently of agent detection. + ### Bug Fixes ### Documentation diff --git a/packages/core/src/clientinfo/default.ts b/packages/core/src/clientinfo/default.ts index d849d07d..e57800c1 100644 --- a/packages/core/src/clientinfo/default.ts +++ b/packages/core/src/clientinfo/default.ts @@ -1,6 +1,7 @@ import {ClientInfo, sanitize} from './clientinfo'; import {MODULE_NAME, VERSION, getBase} from './base'; import {agentProvider} from './agent'; +import {metaHarnessProvider} from './meta-harness'; interface EnvCheck { readonly name: string; @@ -124,5 +125,12 @@ export function createDefault(): ClientInfo { pairs.push({key: 'agent', value: agent}); } + // The meta-harness dimension is independent of agent detection: omnigent + // running Claude Code reports both agent/claude-code and meta-harness/omnigent. + const metaHarness = metaHarnessProvider(); + if (metaHarness !== '') { + pairs.push({key: 'meta-harness', value: metaHarness}); + } + return ClientInfo.EMPTY.with(...pairs); } diff --git a/packages/core/src/clientinfo/meta-harness.ts b/packages/core/src/clientinfo/meta-harness.ts new file mode 100644 index 00000000..7c9c7334 --- /dev/null +++ b/packages/core/src/clientinfo/meta-harness.ts @@ -0,0 +1,63 @@ +/** + * Detects the agent meta-harness (e.g. omnigent) running the current process. + * A meta-harness orchestrates AI coding agents rather than being one, so it is + * reported as an independent `meta-harness/` user-agent dimension + * alongside `agent/`. Kept in sync across the Go, Java, Python, and + * TypeScript SDKs. + * + * @module + */ + +interface KnownMetaHarness { + readonly envVar: string; + readonly product: string; +} + +// Canonical list of known meta-harnesses, detected by env var presence. Keep +// in sync with the Go, Java, and Python SDKs. +const KNOWN_META_HARNESSES: readonly KnownMetaHarness[] = [ + // OMNIGENT is set by the omnigent meta-harness + // (https://github.com/omnigent-ai/omnigent). + {envVar: 'OMNIGENT', product: 'omnigent'}, +]; + +/** + * Checks environment variables for known meta-harnesses. Returns the product + * name when exactly one is set, `"multiple"` when more than one is set, or `""` + * when none is set. Detection is by presence, so any value (including empty) + * counts. + */ +export function lookupMetaHarnessProvider(): string { + const matches: string[] = []; + for (const h of KNOWN_META_HARNESSES) { + if (h.envVar in process.env) { + matches.push(h.product); + } + } + if (matches.length === 1) { + return matches[0]; + } + if (matches.length > 1) { + return 'multiple'; + } + return ''; +} + +let cached: string | undefined; + +/** + * Returns the detected meta-harness name, cached for the process lifetime. + */ +export function metaHarnessProvider(): string { + cached ??= lookupMetaHarnessProvider(); + return cached; +} + +/** + * Clears the cached meta-harness detection result so that the next call to + * {@link metaHarnessProvider} re-evaluates the environment. Exported for + * testing only. + */ +export function clearMetaHarnessCache(): void { + cached = undefined; +} diff --git a/packages/core/tests/clientinfo/default.test.ts b/packages/core/tests/clientinfo/default.test.ts index 7e2d7cfc..5ac474b6 100644 --- a/packages/core/tests/clientinfo/default.test.ts +++ b/packages/core/tests/clientinfo/default.test.ts @@ -18,6 +18,7 @@ import { normalizeNodeVersion, } from '../../src/clientinfo/default'; import {clearAgentCache} from '../../src/clientinfo/agent'; +import {clearMetaHarnessCache} from '../../src/clientinfo/meta-harness'; describe('createDefault', () => { let savedEnv: NodeJS.ProcessEnv; @@ -25,6 +26,7 @@ describe('createDefault', () => { beforeEach(() => { resetBase(); clearAgentCache(); + clearMetaHarnessCache(); savedEnv = process.env; process.env = {...savedEnv}; }); @@ -32,6 +34,7 @@ describe('createDefault', () => { afterEach(() => { process.env = savedEnv; clearAgentCache(); + clearMetaHarnessCache(); }); const prefix = `${MODULE_NAME}/${VERSION} node/${CACHED_NODE_VERSION} os/${process.platform}`; @@ -86,6 +89,16 @@ describe('createDefault', () => { env: {AGENT: 'somethingweird'}, want: `${prefix} agent/somethingweird`, }, + { + name: 'meta-harness only', + env: {OMNIGENT: '1'}, + want: `${prefix} meta-harness/omnigent`, + }, + { + name: 'agent and meta-harness reported independently', + env: {CLAUDECODE: '1', OMNIGENT: '1'}, + want: `${prefix} agent/claude-code meta-harness/omnigent`, + }, { name: 'databricks runtime', env: {DATABRICKS_RUNTIME_VERSION: '15.5'}, @@ -122,8 +135,9 @@ describe('createDefault', () => { GITHUB_ACTIONS: 'true', DATABRICKS_RUNTIME_VERSION: '15.5', CLAUDECODE: '1', + OMNIGENT: '1', }, - want: `${prefix} upstream/terraform upstream-version/1.5.0 cicd/github runtime/15.5 agent/claude-code`, + want: `${prefix} upstream/terraform upstream-version/1.5.0 cicd/github runtime/15.5 agent/claude-code meta-harness/omnigent`, }, ]; diff --git a/packages/core/tests/clientinfo/meta-harness.test.ts b/packages/core/tests/clientinfo/meta-harness.test.ts new file mode 100644 index 00000000..f4d028d8 --- /dev/null +++ b/packages/core/tests/clientinfo/meta-harness.test.ts @@ -0,0 +1,78 @@ +import {describe, it, expect, beforeEach, afterEach} from 'vitest'; +import { + metaHarnessProvider, + clearMetaHarnessCache, + lookupMetaHarnessProvider, +} from '../../src/clientinfo/meta-harness'; + +describe('lookupMetaHarnessProvider', () => { + let savedEnv: NodeJS.ProcessEnv; + + beforeEach(() => { + clearMetaHarnessCache(); + savedEnv = process.env; + process.env = {}; + }); + + afterEach(() => { + process.env = savedEnv; + clearMetaHarnessCache(); + }); + + const testCases: { + name: string; + env: Record; + want: string; + }[] = [ + { + name: 'no meta-harness', + env: {}, + want: '', + }, + { + name: 'omnigent', + env: {OMNIGENT: '1'}, + want: 'omnigent', + }, + { + name: 'empty value still counts as set', + env: {OMNIGENT: ''}, + want: 'omnigent', + }, + { + name: 'an agent env var does not affect meta-harness detection', + env: {CLAUDECODE: '1'}, + want: '', + }, + ]; + + it.each(testCases)('$name', ({env, want}) => { + process.env = env; + expect(lookupMetaHarnessProvider()).toBe(want); + }); +}); + +describe('metaHarnessProvider', () => { + let savedEnv: NodeJS.ProcessEnv; + + beforeEach(() => { + clearMetaHarnessCache(); + savedEnv = process.env; + process.env = {}; + }); + + afterEach(() => { + process.env = savedEnv; + clearMetaHarnessCache(); + }); + + it('caches the detection result for the process lifetime', () => { + process.env = {OMNIGENT: '1'}; + expect(metaHarnessProvider()).toBe('omnigent'); + + // Changing the environment after the first call must not change the + // cached result. + process.env = {}; + expect(metaHarnessProvider()).toBe('omnigent'); + }); +}); diff --git a/packages/core/vitest.config.browser.ts b/packages/core/vitest.config.browser.ts index 0a89d223..c227d5f4 100644 --- a/packages/core/vitest.config.browser.ts +++ b/packages/core/vitest.config.browser.ts @@ -13,6 +13,7 @@ export default defineConfig({ 'tests/profiles/resolve.test.ts', 'tests/clientinfo/default.test.ts', 'tests/clientinfo/agent.test.ts', + 'tests/clientinfo/meta-harness.test.ts', ], }, });