Skip to content

Commit f6e161f

Browse files
committed
Report agent/multiple when agents are stacked
Nested agents (e.g. a Cursor CLI subagent spawned by Claude Code) set multiple agent env vars on the same process. The previous ambiguity guard silently dropped the signal in that case. Report "multiple" instead so the stacked case is visible in telemetry. Also collapse the known BYOK false positive where Copilot CLI users have COPILOT_MODEL set alongside COPILOT_CLI: that pair now reports "copilot-cli" rather than "multiple". Co-authored-by: Isaac Signed-off-by: simon <simon.faltum@databricks.com>
1 parent 3e12cec commit f6e161f

3 files changed

Lines changed: 53 additions & 18 deletions

File tree

NEXT_CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
### New Features and Improvements
66
* Add support for authentication through Azure Managed Service Identity (MSI) via the new `azure-msi` credential provider.
77
* Support `default_profile` in `[__settings__]` section of `.databrickscfg` for consistent default profile resolution across CLI and SDKs.
8-
* Added automatic detection of AI coding agents (Amp, Antigravity, Augment, Claude Code, Cline, Codex, Copilot CLI, Copilot VS Code, Cursor, Gemini CLI, Goose, Kiro, OpenClaw, OpenCode, Windsurf) in the user-agent string. The SDK now appends `agent/<name>` to HTTP request headers when running inside a known AI agent environment. Also honors the `AGENT=<name>` standard: when `AGENT` is set to a known product name the SDK reports that product, and when set to an unrecognized non-empty value the SDK reports `agent/unknown`. Environment variables set to the empty string (e.g. `CLAUDECODE=""`) now count as "set" for presence-only matchers, matching `databricks-sdk-go` semantics; previously they were treated as unset. Explicit agent env vars (e.g. `CLAUDECODE`, `GOOSE_TERMINAL`) always take precedence over the generic `AGENT=<name>` signal.
8+
* Added automatic detection of AI coding agents (Amp, Antigravity, Augment, Claude Code, Cline, Codex, Copilot CLI, Copilot VS Code, Cursor, Gemini CLI, Goose, Kiro, OpenClaw, OpenCode, Windsurf) in the user-agent string. The SDK now appends `agent/<name>` to HTTP request headers when running inside a known AI agent environment. Also honors the `AGENT=<name>` standard: when `AGENT` is set to a known product name the SDK reports that product, and when set to an unrecognized non-empty value the SDK reports `agent/unknown`. Environment variables set to the empty string (e.g. `CLAUDECODE=""`) now count as "set" for presence-only matchers, matching `databricks-sdk-go` semantics; previously they were treated as unset. Explicit agent env vars (e.g. `CLAUDECODE`, `GOOSE_TERMINAL`) always take precedence over the generic `AGENT=<name>` signal. When multiple agent env vars are present (e.g. a Cursor CLI subagent invoked from Claude Code), the user-agent reports `agent/multiple`.
99

1010
### Bug Fixes
1111
* Fixed non-JSON error responses (e.g. plain-text "Invalid Token" with HTTP 403) producing `Unknown` instead of the correct typed exception (`PermissionDenied`, `Unauthenticated`, etc.). The error message no longer contains Jackson deserialization internals.

databricks-sdk-java/src/main/java/com/databricks/sdk/core/UserAgent.java

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -287,19 +287,19 @@ private static List<KnownAgent> listKnownAgents() {
287287
// treated purely as a fallback for agents that have no explicit matcher, or
288288
// for agents we do not yet specifically recognize.
289289
//
290-
// For each agent, it fires if ANY of its matchers fires. The function counts
291-
// how many distinct agents matched:
290+
// The function counts how many distinct agents matched via explicit env vars:
292291
// - Exactly one agent matched: return its product name.
293-
// - More than one agent matched: return "" (ambiguity).
292+
// - More than one agent matched: return "multiple". Agent env vars can be
293+
// stacked when one agent invokes another as a subagent (e.g. Claude Code
294+
// spawning a Cursor CLI subprocess), so the child process inherits env
295+
// vars from multiple layers.
294296
// - Zero agents matched: if the agents.md standard AGENT env var is set to
295297
// a known product name, return that product name. If it is set to any
296298
// other non-empty value, return "unknown". Otherwise return "".
297299
//
298-
// Unlike CI/CD detection (which returns the first match), agent detection
299-
// uses an ambiguity guard because agent env vars can be stacked (e.g.,
300-
// running Cline inside Cursor). Because explicit matchers win over AGENT,
301-
// e.g. AGENT=cursor + CLAUDECODE=1 yields "claude-code", and
302-
// AGENT=goose + CLAUDECODE=1 also yields "claude-code".
300+
// Because explicit matchers win over AGENT, e.g. AGENT=cursor + CLAUDECODE=1
301+
// yields "claude-code", and AGENT=goose + CLAUDECODE=1 also yields
302+
// "claude-code".
303303
private static String lookupAgentProvider(Environment env) {
304304
List<KnownAgent> agents = listKnownAgents();
305305

@@ -310,11 +310,18 @@ private static String lookupAgentProvider(Environment env) {
310310
}
311311
}
312312

313+
// Known BYOK false positive: Copilot CLI users often set COPILOT_MODEL
314+
// alongside COPILOT_CLI. Treat that pair as a single copilot-cli signal
315+
// rather than a stacked multi-agent setup.
316+
if (matches.contains("copilot-cli") && matches.contains("copilot-vscode")) {
317+
matches.removeIf(m -> m.equals("copilot-vscode"));
318+
}
319+
313320
if (matches.size() == 1) {
314321
return matches.get(0);
315322
}
316323
if (matches.size() > 1) {
317-
return ""; // ambiguity
324+
return "multiple";
318325
}
319326
return agentEnvFallback(env, agents);
320327
}

databricks-sdk-java/src/test/java/com/databricks/sdk/core/UserAgentTest.java

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -415,20 +415,33 @@ public void testAgentProviderExplicitEnvWinsOverKnownAgentEnv() {
415415
}
416416

417417
@Test
418-
public void testAgentProviderCopilotCliAndCopilotVscodeAmbiguous() {
419-
// Copilot CLI can be invoked with BYOK models, which may also set
420-
// COPILOT_MODEL. In that case both copilot-cli and copilot-vscode
421-
// matchers fire on different products, so detection is ambiguous.
422-
// This is intentional: ambiguity is preferred over silently picking
423-
// one product.
418+
public void testAgentProviderCopilotCliAndCopilotVscodeCollapseToCopilotCli() {
419+
// Copilot CLI users (BYOK mode) often set COPILOT_MODEL alongside
420+
// COPILOT_CLI. Treat the pair as a single copilot-cli signal rather
421+
// than a stacked multi-agent setup.
424422
setupAgentEnv(
425423
new HashMap<String, String>() {
426424
{
427425
put("COPILOT_CLI", "1");
428426
put("COPILOT_MODEL", "gpt-4");
429427
}
430428
});
431-
Assertions.assertFalse(UserAgent.asString().contains("agent/"));
429+
Assertions.assertTrue(UserAgent.asString().contains("agent/copilot-cli"));
430+
}
431+
432+
@Test
433+
public void testAgentProviderCopilotByokCollapseStillMultiple() {
434+
// The Copilot BYOK collapse only drops the copilot-vscode match. If
435+
// another agent is also present, the result is still "multiple".
436+
setupAgentEnv(
437+
new HashMap<String, String>() {
438+
{
439+
put("COPILOT_CLI", "1");
440+
put("COPILOT_MODEL", "gpt-4");
441+
put("CLAUDECODE", "1");
442+
}
443+
});
444+
Assertions.assertTrue(UserAgent.asString().contains("agent/multiple"));
432445
}
433446

434447
@Test
@@ -439,14 +452,29 @@ public void testAgentProviderNoAgent() {
439452

440453
@Test
441454
public void testAgentProviderMultipleAgents() {
455+
// Nested agents (e.g. Claude Code spawning a Cursor CLI subagent) set
456+
// multiple explicit matchers on the same process.
442457
setupAgentEnv(
443458
new HashMap<String, String>() {
444459
{
445460
put("CLAUDECODE", "1");
446461
put("CURSOR_AGENT", "1");
447462
}
448463
});
449-
Assertions.assertFalse(UserAgent.asString().contains("agent/"));
464+
Assertions.assertTrue(UserAgent.asString().contains("agent/multiple"));
465+
}
466+
467+
@Test
468+
public void testAgentProviderThreeStackedAgents() {
469+
setupAgentEnv(
470+
new HashMap<String, String>() {
471+
{
472+
put("CLAUDECODE", "1");
473+
put("CURSOR_AGENT", "1");
474+
put("AUGMENT_AGENT", "1");
475+
}
476+
});
477+
Assertions.assertTrue(UserAgent.asString().contains("agent/multiple"));
450478
}
451479

452480
@Test

0 commit comments

Comments
 (0)