Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/core/NEXT_CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions packages/core/src/clientinfo/default.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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);
}
63 changes: 63 additions & 0 deletions packages/core/src/clientinfo/meta-harness.ts
Original file line number Diff line number Diff line change
@@ -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/<name>` user-agent dimension
* alongside `agent/<name>`. 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;
}
16 changes: 15 additions & 1 deletion packages/core/tests/clientinfo/default.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,23 @@ 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;

beforeEach(() => {
resetBase();
clearAgentCache();
clearMetaHarnessCache();
savedEnv = process.env;
process.env = {...savedEnv};
});

afterEach(() => {
process.env = savedEnv;
clearAgentCache();
clearMetaHarnessCache();
});

const prefix = `${MODULE_NAME}/${VERSION} node/${CACHED_NODE_VERSION} os/${process.platform}`;
Expand Down Expand Up @@ -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'},
Expand Down Expand Up @@ -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`,
},
];

Expand Down
78 changes: 78 additions & 0 deletions packages/core/tests/clientinfo/meta-harness.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>;
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');
});
});
1 change: 1 addition & 0 deletions packages/core/vitest.config.browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
],
},
});
Loading