From 4c3b83878933197e9151dbef6aa7d23c955ba866 Mon Sep 17 00:00:00 2001 From: simon Date: Thu, 12 Mar 2026 13:44:47 +0100 Subject: [PATCH 1/4] Add AI agent detection to user-agent string Detect known AI coding agents (Claude Code, Cursor, Cline, Codex, Gemini CLI, OpenCode, Antigravity) via environment variables and append agent/ to the user-agent header. Uses the same double-checked locking pattern as CI/CD detection. Returns empty when zero or multiple agents are detected (ambiguity guard). Co-authored-by: Isaac --- NEXT_CHANGELOG.md | 1 + .../com/databricks/sdk/core/UserAgent.java | 56 +++++++ .../databricks/sdk/core/UserAgentTest.java | 156 ++++++++++++++++++ 3 files changed, 213 insertions(+) diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index 0eab3a65f..63ddb9ecd 100755 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -3,6 +3,7 @@ ## Release v0.101.0 ### New Features and Improvements +* Added automatic detection of AI coding agents (Claude Code, Cursor, Cline, Codex, Gemini CLI, OpenCode, Antigravity) in the user-agent string. The SDK now appends `agent/` to HTTP request headers when running inside a known AI agent environment. ### Bug Fixes diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/UserAgent.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/UserAgent.java index e76baef33..2bc177823 100644 --- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/UserAgent.java +++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/UserAgent.java @@ -2,10 +2,12 @@ import com.databricks.sdk.core.utils.Environment; import java.io.File; +import java.util.AbstractMap; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -129,6 +131,10 @@ public static String asString() { if (!cicdProvider.isEmpty()) { segments.add(String.format("cicd/%s", cicdProvider)); } + String agent = agentProvider(); + if (agent != null && !agent.isEmpty()) { + segments.add(String.format("agent/%s", agent)); + } // Concurrent iteration over ArrayList must be guarded with synchronized. synchronized (otherInfo) { segments.addAll( @@ -168,6 +174,8 @@ private static List listCiCdProviders() { // reordering by the compiler. protected static volatile String cicdProvider = null; + protected static volatile String agentProvider = null; + protected static Environment env = null; // Represents an environment variable with its name and expected value @@ -231,6 +239,54 @@ private static String cicdProvider() { return cicdProvider; } + // Canonical list of known AI coding agents. + // Keep this list in sync with databricks-sdk-go and databricks-sdk-py. + private static List> listKnownAgents() { + return Arrays.asList( + new AbstractMap.SimpleEntry<>("ANTIGRAVITY_AGENT", "antigravity"), // Closed source (Google) + new AbstractMap.SimpleEntry<>( + "CLAUDECODE", "claude-code"), // https://github.com/anthropics/claude-code + new AbstractMap.SimpleEntry<>( + "CLINE_ACTIVE", "cline"), // https://github.com/cline/cline (v3.24.0+) + new AbstractMap.SimpleEntry<>("CODEX_CI", "codex"), // https://github.com/openai/codex + new AbstractMap.SimpleEntry<>("CURSOR_AGENT", "cursor"), // Closed source + new AbstractMap.SimpleEntry<>( + "GEMINI_CLI", "gemini-cli"), // https://google-gemini.github.io/gemini-cli + new AbstractMap.SimpleEntry<>( + "OPENCODE", "opencode")); // https://github.com/opencode-ai/opencode + } + + // Looks up the active agent provider based on environment variables. + // Returns the agent name if exactly one is set (non-empty). + // Returns empty string if zero or multiple agents detected. + private static String lookupAgentProvider(Environment env) { + String detected = ""; + int count = 0; + for (Map.Entry agent : listKnownAgents()) { + String value = env.get(agent.getKey()); + if (value != null && !value.isEmpty()) { + detected = agent.getValue(); + count++; + } + } + if (count == 1) { + return detected; + } + return ""; + } + + // Thread-safe lazy initialization of agent provider detection + private static String agentProvider() { + if (agentProvider == null) { + synchronized (UserAgent.class) { + if (agentProvider == null) { + agentProvider = lookupAgentProvider(env()); + } + } + } + return agentProvider; + } + private static Environment env() { if (env == null) { env = diff --git a/databricks-sdk-java/src/test/java/com/databricks/sdk/core/UserAgentTest.java b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/UserAgentTest.java index 9d0b7f6c2..0f4f06343 100644 --- a/databricks-sdk-java/src/test/java/com/databricks/sdk/core/UserAgentTest.java +++ b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/UserAgentTest.java @@ -3,10 +3,24 @@ import com.databricks.sdk.core.utils.Environment; import java.util.ArrayList; import java.util.HashMap; +import java.util.Map; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; public class UserAgentTest { + + private void setupAgentEnv(Map envMap) { + UserAgent.agentProvider = null; + UserAgent.cicdProvider = null; + UserAgent.env = new Environment(envMap, new ArrayList<>(), System.getProperty("os.name")); + } + + private void cleanupAgentEnv() { + UserAgent.env = null; + UserAgent.agentProvider = null; + UserAgent.cicdProvider = null; + } + @Test public void testUserAgent() { UserAgent.withProduct("product", "productVersion"); @@ -101,4 +115,146 @@ public void testUserAgentCicdTwoProvider() { Assertions.assertTrue(UserAgent.asString().contains("cicd/gitlab")); UserAgent.env = null; } + + @Test + public void testAgentProviderAntigravity() { + setupAgentEnv( + new HashMap() { + { + put("ANTIGRAVITY_AGENT", "1"); + } + }); + Assertions.assertTrue(UserAgent.asString().contains("agent/antigravity")); + cleanupAgentEnv(); + } + + @Test + public void testAgentProviderClaudeCode() { + setupAgentEnv( + new HashMap() { + { + put("CLAUDECODE", "1"); + } + }); + Assertions.assertTrue(UserAgent.asString().contains("agent/claude-code")); + cleanupAgentEnv(); + } + + @Test + public void testAgentProviderCline() { + setupAgentEnv( + new HashMap() { + { + put("CLINE_ACTIVE", "1"); + } + }); + Assertions.assertTrue(UserAgent.asString().contains("agent/cline")); + cleanupAgentEnv(); + } + + @Test + public void testAgentProviderCodex() { + setupAgentEnv( + new HashMap() { + { + put("CODEX_CI", "1"); + } + }); + Assertions.assertTrue(UserAgent.asString().contains("agent/codex")); + cleanupAgentEnv(); + } + + @Test + public void testAgentProviderCursor() { + setupAgentEnv( + new HashMap() { + { + put("CURSOR_AGENT", "1"); + } + }); + Assertions.assertTrue(UserAgent.asString().contains("agent/cursor")); + cleanupAgentEnv(); + } + + @Test + public void testAgentProviderGeminiCli() { + setupAgentEnv( + new HashMap() { + { + put("GEMINI_CLI", "1"); + } + }); + Assertions.assertTrue(UserAgent.asString().contains("agent/gemini-cli")); + cleanupAgentEnv(); + } + + @Test + public void testAgentProviderOpencode() { + setupAgentEnv( + new HashMap() { + { + put("OPENCODE", "1"); + } + }); + Assertions.assertTrue(UserAgent.asString().contains("agent/opencode")); + cleanupAgentEnv(); + } + + @Test + public void testAgentProviderNoAgent() { + setupAgentEnv(new HashMap<>()); + Assertions.assertFalse(UserAgent.asString().contains("agent/")); + cleanupAgentEnv(); + } + + @Test + public void testAgentProviderMultipleAgents() { + setupAgentEnv( + new HashMap() { + { + put("CLAUDECODE", "1"); + put("CURSOR_AGENT", "1"); + } + }); + Assertions.assertFalse(UserAgent.asString().contains("agent/")); + cleanupAgentEnv(); + } + + @Test + public void testAgentProviderEmptyValue() { + setupAgentEnv( + new HashMap() { + { + put("CLAUDECODE", ""); + } + }); + Assertions.assertFalse(UserAgent.asString().contains("agent/")); + cleanupAgentEnv(); + } + + @Test + public void testAgentProviderCached() { + // Set up with cursor agent + setupAgentEnv( + new HashMap() { + { + put("CURSOR_AGENT", "1"); + } + }); + Assertions.assertTrue(UserAgent.asString().contains("agent/cursor")); + + // Change env after caching. Cached result should persist. + UserAgent.env = + new Environment( + new HashMap() { + { + put("CLAUDECODE", "1"); + } + }, + new ArrayList<>(), + System.getProperty("os.name")); + Assertions.assertTrue(UserAgent.asString().contains("agent/cursor")); + Assertions.assertFalse(UserAgent.asString().contains("agent/claude-code")); + cleanupAgentEnv(); + } } From 5c3a1a5f7b23faa9c3604a233521d261282802f7 Mon Sep 17 00:00:00 2001 From: simon Date: Thu, 12 Mar 2026 13:52:58 +0100 Subject: [PATCH 2/4] Use @AfterEach for test cleanup instead of manual calls Move cleanupAgentEnv() to an @AfterEach tearDown method so test isolation is failure-safe. Previously, if an assertion failed before the manual cleanup call, static state would leak between tests. Co-authored-by: Isaac --- .../databricks/sdk/core/UserAgentTest.java | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/databricks-sdk-java/src/test/java/com/databricks/sdk/core/UserAgentTest.java b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/UserAgentTest.java index 0f4f06343..f925adedd 100644 --- a/databricks-sdk-java/src/test/java/com/databricks/sdk/core/UserAgentTest.java +++ b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/UserAgentTest.java @@ -4,11 +4,17 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.Map; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; public class UserAgentTest { + @AfterEach + void tearDown() { + cleanupAgentEnv(); + } + private void setupAgentEnv(Map envMap) { UserAgent.agentProvider = null; UserAgent.cicdProvider = null; @@ -80,7 +86,6 @@ public void testUserAgentCicdNoProvider() { UserAgent.env = new Environment(new HashMap<>(), new ArrayList<>(), System.getProperty("os.name")); Assertions.assertFalse(UserAgent.asString().contains("cicd")); - UserAgent.env = null; } @Test @@ -96,7 +101,6 @@ public void testUserAgentCicdOneProvider() { new ArrayList<>(), System.getProperty("os.name")); Assertions.assertTrue(UserAgent.asString().contains("cicd/github")); - UserAgent.env = null; } @Test @@ -113,7 +117,6 @@ public void testUserAgentCicdTwoProvider() { new ArrayList<>(), System.getProperty("os.name")); Assertions.assertTrue(UserAgent.asString().contains("cicd/gitlab")); - UserAgent.env = null; } @Test @@ -125,7 +128,6 @@ public void testAgentProviderAntigravity() { } }); Assertions.assertTrue(UserAgent.asString().contains("agent/antigravity")); - cleanupAgentEnv(); } @Test @@ -137,7 +139,6 @@ public void testAgentProviderClaudeCode() { } }); Assertions.assertTrue(UserAgent.asString().contains("agent/claude-code")); - cleanupAgentEnv(); } @Test @@ -149,7 +150,6 @@ public void testAgentProviderCline() { } }); Assertions.assertTrue(UserAgent.asString().contains("agent/cline")); - cleanupAgentEnv(); } @Test @@ -161,7 +161,6 @@ public void testAgentProviderCodex() { } }); Assertions.assertTrue(UserAgent.asString().contains("agent/codex")); - cleanupAgentEnv(); } @Test @@ -173,7 +172,6 @@ public void testAgentProviderCursor() { } }); Assertions.assertTrue(UserAgent.asString().contains("agent/cursor")); - cleanupAgentEnv(); } @Test @@ -185,7 +183,6 @@ public void testAgentProviderGeminiCli() { } }); Assertions.assertTrue(UserAgent.asString().contains("agent/gemini-cli")); - cleanupAgentEnv(); } @Test @@ -197,14 +194,12 @@ public void testAgentProviderOpencode() { } }); Assertions.assertTrue(UserAgent.asString().contains("agent/opencode")); - cleanupAgentEnv(); } @Test public void testAgentProviderNoAgent() { setupAgentEnv(new HashMap<>()); Assertions.assertFalse(UserAgent.asString().contains("agent/")); - cleanupAgentEnv(); } @Test @@ -217,7 +212,6 @@ public void testAgentProviderMultipleAgents() { } }); Assertions.assertFalse(UserAgent.asString().contains("agent/")); - cleanupAgentEnv(); } @Test @@ -229,7 +223,6 @@ public void testAgentProviderEmptyValue() { } }); Assertions.assertFalse(UserAgent.asString().contains("agent/")); - cleanupAgentEnv(); } @Test @@ -255,6 +248,5 @@ public void testAgentProviderCached() { System.getProperty("os.name")); Assertions.assertTrue(UserAgent.asString().contains("agent/cursor")); Assertions.assertFalse(UserAgent.asString().contains("agent/claude-code")); - cleanupAgentEnv(); } } From 6f6ebb8fdd8f11fafb0b7cd205bd35e85a2f7d86 Mon Sep 17 00:00:00 2001 From: simon Date: Thu, 12 Mar 2026 14:51:29 +0100 Subject: [PATCH 3/4] Address review feedback: AgentDef class, early exit, null check Replace AbstractMap.SimpleEntry with AgentDef inner class to match the CicdProvider/EnvVar style. Add early exit in lookupAgentProvider when count > 1. Remove redundant null check for agentProvider() return value to match cicdProvider() pattern. Co-authored-by: Isaac --- .../com/databricks/sdk/core/UserAgent.java | 44 +++++++++++-------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/UserAgent.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/UserAgent.java index 2bc177823..4e16354ca 100644 --- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/UserAgent.java +++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/UserAgent.java @@ -2,12 +2,10 @@ import com.databricks.sdk.core.utils.Environment; import java.io.File; -import java.util.AbstractMap; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -132,7 +130,7 @@ public static String asString() { segments.add(String.format("cicd/%s", cicdProvider)); } String agent = agentProvider(); - if (agent != null && !agent.isEmpty()) { + if (!agent.isEmpty()) { segments.add(String.format("agent/%s", agent)); } // Concurrent iteration over ArrayList must be guarded with synchronized. @@ -239,21 +237,28 @@ private static String cicdProvider() { return cicdProvider; } + // Maps an environment variable to an agent product name. + private static class AgentDef { + private final String envVar; + private final String product; + + AgentDef(String envVar, String product) { + this.envVar = envVar; + this.product = product; + } + } + // Canonical list of known AI coding agents. // Keep this list in sync with databricks-sdk-go and databricks-sdk-py. - private static List> listKnownAgents() { + private static List listKnownAgents() { return Arrays.asList( - new AbstractMap.SimpleEntry<>("ANTIGRAVITY_AGENT", "antigravity"), // Closed source (Google) - new AbstractMap.SimpleEntry<>( - "CLAUDECODE", "claude-code"), // https://github.com/anthropics/claude-code - new AbstractMap.SimpleEntry<>( - "CLINE_ACTIVE", "cline"), // https://github.com/cline/cline (v3.24.0+) - new AbstractMap.SimpleEntry<>("CODEX_CI", "codex"), // https://github.com/openai/codex - new AbstractMap.SimpleEntry<>("CURSOR_AGENT", "cursor"), // Closed source - new AbstractMap.SimpleEntry<>( - "GEMINI_CLI", "gemini-cli"), // https://google-gemini.github.io/gemini-cli - new AbstractMap.SimpleEntry<>( - "OPENCODE", "opencode")); // https://github.com/opencode-ai/opencode + new AgentDef("ANTIGRAVITY_AGENT", "antigravity"), // Closed source (Google) + new AgentDef("CLAUDECODE", "claude-code"), // https://github.com/anthropics/claude-code + new AgentDef("CLINE_ACTIVE", "cline"), // https://github.com/cline/cline (v3.24.0+) + new AgentDef("CODEX_CI", "codex"), // https://github.com/openai/codex + new AgentDef("CURSOR_AGENT", "cursor"), // Closed source + new AgentDef("GEMINI_CLI", "gemini-cli"), // https://google-gemini.github.io/gemini-cli + new AgentDef("OPENCODE", "opencode")); // https://github.com/opencode-ai/opencode } // Looks up the active agent provider based on environment variables. @@ -262,11 +267,14 @@ private static List> listKnownAgents() { private static String lookupAgentProvider(Environment env) { String detected = ""; int count = 0; - for (Map.Entry agent : listKnownAgents()) { - String value = env.get(agent.getKey()); + for (AgentDef agent : listKnownAgents()) { + String value = env.get(agent.envVar); if (value != null && !value.isEmpty()) { - detected = agent.getValue(); + detected = agent.product; count++; + if (count > 1) { + return ""; + } } } if (count == 1) { From d709d853f9b0e3da6ead8f69fbc6d1191464b037 Mon Sep 17 00:00:00 2001 From: simon Date: Thu, 12 Mar 2026 17:08:41 +0100 Subject: [PATCH 4/4] Add Copilot CLI to agent detection We have data from direct testing in Copilot CLI confirming it sets COPILOT_CLI=1 in its environment. Add it to the canonical agent list. Co-authored-by: Isaac --- NEXT_CHANGELOG.md | 2 +- .../main/java/com/databricks/sdk/core/UserAgent.java | 1 + .../java/com/databricks/sdk/core/UserAgentTest.java | 11 +++++++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index 63ddb9ecd..0d3f72e96 100755 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -3,7 +3,7 @@ ## Release v0.101.0 ### New Features and Improvements -* Added automatic detection of AI coding agents (Claude Code, Cursor, Cline, Codex, Gemini CLI, OpenCode, Antigravity) in the user-agent string. The SDK now appends `agent/` to HTTP request headers when running inside a known AI agent environment. +* Added automatic detection of AI coding agents (Antigravity, Claude Code, Cline, Codex, Copilot CLI, Cursor, Gemini CLI, OpenCode) in the user-agent string. The SDK now appends `agent/` to HTTP request headers when running inside a known AI agent environment. ### Bug Fixes diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/UserAgent.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/UserAgent.java index 4e16354ca..ef3b0c4f2 100644 --- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/UserAgent.java +++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/UserAgent.java @@ -256,6 +256,7 @@ private static List listKnownAgents() { new AgentDef("CLAUDECODE", "claude-code"), // https://github.com/anthropics/claude-code new AgentDef("CLINE_ACTIVE", "cline"), // https://github.com/cline/cline (v3.24.0+) new AgentDef("CODEX_CI", "codex"), // https://github.com/openai/codex + new AgentDef("COPILOT_CLI", "copilot-cli"), // https://github.com/features/copilot new AgentDef("CURSOR_AGENT", "cursor"), // Closed source new AgentDef("GEMINI_CLI", "gemini-cli"), // https://google-gemini.github.io/gemini-cli new AgentDef("OPENCODE", "opencode")); // https://github.com/opencode-ai/opencode diff --git a/databricks-sdk-java/src/test/java/com/databricks/sdk/core/UserAgentTest.java b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/UserAgentTest.java index f925adedd..90cf51060 100644 --- a/databricks-sdk-java/src/test/java/com/databricks/sdk/core/UserAgentTest.java +++ b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/UserAgentTest.java @@ -163,6 +163,17 @@ public void testAgentProviderCodex() { Assertions.assertTrue(UserAgent.asString().contains("agent/codex")); } + @Test + public void testAgentProviderCopilotCli() { + setupAgentEnv( + new HashMap() { + { + put("COPILOT_CLI", "1"); + } + }); + Assertions.assertTrue(UserAgent.asString().contains("agent/copilot-cli")); + } + @Test public void testAgentProviderCursor() { setupAgentEnv(