Skip to content

Commit 239e6f7

Browse files
Honor AI_AGENT and pass raw values through
Adds the Vercel @vercel/detect-agent AI_AGENT=<name> env var as a secondary fallback after the agents.md AGENT=<name> standard. AGENT wins when both are non-empty; empty is treated as unset for both. Also changes the fallback behavior to pass the raw value through (sanitized via sanitize() and capped at 64 chars) instead of coercing unrecognized names to the literal "unknown". Bucketing arbitrary tool names is an ETL concern, not the SDK's; the prior coercion buried useful signal such as versioned variants like "claude-code_2-1-141". Mirrors databricks-sdk-go#1683, databricks-sdk-py#1454, and databricks-sdk-java#815.
1 parent c911f53 commit 239e6f7

3 files changed

Lines changed: 94 additions & 23 deletions

File tree

packages/core/src/clientinfo/agent.ts

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,29 @@
1010
* @module
1111
*/
1212

13+
import {sanitize} from './clientinfo';
14+
1315
interface KnownAgent {
1416
readonly envVar: string;
1517
readonly product: string;
1618
}
1719

18-
// Name of the agents.md standard env var. When set to a value that no
19-
// known agent recognizes, detection falls back to "unknown".
20+
// Name of the agents.md standard env var.
2021
const AGENT_ENV_VAR = 'AGENT';
2122

22-
// Canonical list of AI coding agents. Keep this list in sync with the
23-
// Go, Java, and Python SDKs. Agents are listed alphabetically by product
24-
// name.
23+
// Name of the Vercel @vercel/detect-agent convention env var. It serves
24+
// the same purpose as AGENT_ENV_VAR; agentEnvFallback consults it only when
25+
// AGENT_ENV_VAR is unset or empty.
26+
const AI_AGENT_ENV_VAR = 'AI_AGENT';
27+
28+
// Caps fallback values to keep the user-agent bounded. Explicit-matcher
29+
// products are short by construction; only the fallback path can carry
30+
// arbitrary lengths.
31+
const MAX_AGENT_FALLBACK_LEN = 64;
32+
33+
// Canonical list of AI coding agents. Keep this list, and the AGENT /
34+
// AI_AGENT fallback handling in agentEnvFallback, in sync with the Go,
35+
// Java, and Python SDKs. Agents are listed alphabetically by product name.
2536
const KNOWN_AGENTS: readonly KnownAgent[] = [
2637
// The amp agent also sets AGENT=amp, handled by the central fallback.
2738
{envVar: 'AMP_CURRENT_THREAD_ID', product: 'amp'},
@@ -45,24 +56,33 @@ const KNOWN_AGENTS: readonly KnownAgent[] = [
4556
{envVar: 'WINDSURF_AGENT', product: 'windsurf'},
4657
];
4758

59+
/**
60+
* Returns a sanitized, length-capped name from `AGENT` or `AI_AGENT`,
61+
* preferring `AGENT` when both are non-empty. Empty is treated as unset for
62+
* both. The value is passed through rather than categorized so that new
63+
* names are propagated without the need to update the list of known agents.
64+
*/
4865
function agentEnvFallback(): string {
49-
const v = process.env[AGENT_ENV_VAR];
66+
let v = process.env[AGENT_ENV_VAR];
67+
if (v === undefined || v === '') {
68+
v = process.env[AI_AGENT_ENV_VAR];
69+
}
5070
if (v === undefined || v === '') {
5171
return '';
5272
}
53-
if (KNOWN_AGENTS.some(a => a.product === v)) {
54-
return v;
73+
v = sanitize(v);
74+
if (v.length > MAX_AGENT_FALLBACK_LEN) {
75+
v = v.slice(0, MAX_AGENT_FALLBACK_LEN);
5576
}
56-
return 'unknown';
77+
return v;
5778
}
5879

5980
/**
6081
* Checks environment variables for known AI agents and returns the
6182
* detected product name.
6283
*
6384
* Explicit product-specific env vars always take precedence over the
64-
* generic agents.md `AGENT` env var. `AGENT` is consulted only as a
65-
* fallback when no explicit matcher fires, so that an explicit signal
85+
* generic `AGENT` and `AI_AGENT` env vars, so that an explicit signal
6686
* (e.g. `CLAUDECODE=1`) always wins over a conflicting `AGENT=<name>`
6787
* value.
6888
*
@@ -73,8 +93,8 @@ function agentEnvFallback(): string {
7393
* can be stacked when one agent invokes another as a subagent (e.g.
7494
* Claude Code spawning a Cursor CLI subprocess), so the child process
7595
* inherits env vars from multiple layers.
76-
* - When no known env var is set and `AGENT` is a non-empty value: the
77-
* value itself if it names a known product, otherwise `"unknown"`.
96+
* - A sanitized, length-capped value from `AGENT` or `AI_AGENT` when no
97+
* known env var is set (see {@link agentEnvFallback}).
7898
* - `""` when nothing is set.
7999
*/
80100
export function lookupAgentProvider(): string {
@@ -101,13 +121,12 @@ let cached: string | undefined;
101121
* Returns one of:
102122
*
103123
* - The known product name when exactly one agent is detected via
104-
* explicit env matchers, or when `AGENT` is set to a known product
105-
* name and no explicit matcher fired.
124+
* explicit env matchers.
106125
* - `"multiple"` when multiple explicit matchers fire for different
107126
* agents (typically nested agents, e.g. Cursor CLI running as a
108127
* Claude Code subagent).
109-
* - `"unknown"` when no explicit matcher fired and `AGENT` is set to a
110-
* value that is not a known product name.
128+
* - A sanitized, length-capped value from `AGENT` or `AI_AGENT` when no
129+
* explicit matcher fired (see {@link agentEnvFallback}).
111130
* - `""` when no agent is detected.
112131
*/
113132
export function agentProvider(): string {

packages/core/tests/clientinfo/agent.test.ts

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,9 +135,19 @@ describe('lookupAgentProvider', () => {
135135
want: 'cursor',
136136
},
137137
{
138-
name: 'AGENT with unknown value falls back to unknown',
139-
env: {AGENT: 'somethingweird'},
140-
want: 'unknown',
138+
name: 'AGENT with unrecognized value passes through (sanitized)',
139+
env: {AGENT: 'someweirdthing'},
140+
want: 'someweirdthing',
141+
},
142+
{
143+
name: 'AGENT with disallowed chars is sanitized to hyphens',
144+
env: {AGENT: 'claude code/agent'},
145+
want: 'claude-code-agent',
146+
},
147+
{
148+
name: 'AGENT longer than the cap is truncated',
149+
env: {AGENT: 'a'.repeat(100)},
150+
want: 'a'.repeat(64),
141151
},
142152
{
143153
name: 'AGENT empty string does not trigger fallback',
@@ -160,7 +170,7 @@ describe('lookupAgentProvider', () => {
160170
want: 'claude-code',
161171
},
162172
{
163-
name: 'known matcher wins over AGENT fallback to unknown',
173+
name: 'known matcher wins over unrecognized AGENT fallback',
164174
env: {AGENT: 'somethingunknown', CLAUDECODE: '1'},
165175
want: 'claude-code',
166176
},
@@ -169,6 +179,48 @@ describe('lookupAgentProvider', () => {
169179
env: {VSCODE_AGENT: '1', COPILOT_CLI: '1'},
170180
want: 'multiple',
171181
},
182+
// AI_AGENT fallback (Vercel @vercel/detect-agent convention).
183+
{
184+
name: 'AI_AGENT=cursor falls back to cursor',
185+
env: {AI_AGENT: 'cursor'},
186+
want: 'cursor',
187+
},
188+
{
189+
name: 'AI_AGENT empty string does not trigger fallback',
190+
env: {AI_AGENT: ''},
191+
want: '',
192+
},
193+
{
194+
name: 'known matcher wins over AI_AGENT fallback',
195+
env: {AI_AGENT: 'somethingunknown', CLAUDECODE: '1'},
196+
want: 'claude-code',
197+
},
198+
// AGENT vs AI_AGENT precedence: AGENT wins when both are non-empty.
199+
{
200+
name: 'AGENT wins over AI_AGENT when both are set to known products',
201+
env: {AGENT: 'claude-code', AI_AGENT: 'cursor'},
202+
want: 'claude-code',
203+
},
204+
{
205+
name: 'AGENT set to unrecognized non-empty value still wins over AI_AGENT',
206+
env: {AGENT: 'somethingunknown', AI_AGENT: 'cursor'},
207+
want: 'somethingunknown',
208+
},
209+
{
210+
name: 'AGENT set, AI_AGENT empty: AGENT value is used',
211+
env: {AGENT: 'cursor', AI_AGENT: ''},
212+
want: 'cursor',
213+
},
214+
{
215+
name: 'empty AGENT falls through to AI_AGENT',
216+
env: {AGENT: '', AI_AGENT: 'cursor'},
217+
want: 'cursor',
218+
},
219+
{
220+
name: 'both AGENT and AI_AGENT empty returns no agent',
221+
env: {AGENT: '', AI_AGENT: ''},
222+
want: '',
223+
},
172224
];
173225

174226
it.each(testCases)('$name', ({env, want}) => {

packages/core/tests/clientinfo/default.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,9 @@ describe('createDefault', () => {
8282
want: `${prefix} agent/goose`,
8383
},
8484
{
85-
name: 'AGENT fallback to unknown',
85+
name: 'AGENT fallback passes unrecognized value through',
8686
env: {AGENT: 'somethingweird'},
87-
want: `${prefix} agent/unknown`,
87+
want: `${prefix} agent/somethingweird`,
8888
},
8989
{
9090
name: 'databricks runtime',

0 commit comments

Comments
 (0)