Skip to content
Open
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
1 change: 1 addition & 0 deletions NEXT_CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## Release v0.101.0

### New Features and Improvements
* 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/<name>` to HTTP request headers when running inside a known AI agent environment.

### Bug Fixes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@ public static String asString() {
if (!cicdProvider.isEmpty()) {
segments.add(String.format("cicd/%s", cicdProvider));
}
String agent = agentProvider();
if (!agent.isEmpty()) {
segments.add(String.format("agent/%s", agent));
}
// Concurrent iteration over ArrayList must be guarded with synchronized.
synchronized (otherInfo) {
segments.addAll(
Expand Down Expand Up @@ -168,6 +172,8 @@ private static List<CicdProvider> 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
Expand Down Expand Up @@ -231,6 +237,65 @@ 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<AgentDef> listKnownAgents() {
return Arrays.asList(
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("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
}

// 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 (AgentDef agent : listKnownAgents()) {
String value = env.get(agent.envVar);
if (value != null && !value.isEmpty()) {
detected = agent.product;
count++;
if (count > 1) {
return "";
}
}
}
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 =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,30 @@
import com.databricks.sdk.core.utils.Environment;
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<String, String> 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");
Expand Down Expand Up @@ -66,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
Expand All @@ -82,7 +101,6 @@ public void testUserAgentCicdOneProvider() {
new ArrayList<>(),
System.getProperty("os.name"));
Assertions.assertTrue(UserAgent.asString().contains("cicd/github"));
UserAgent.env = null;
}

@Test
Expand All @@ -99,6 +117,147 @@ public void testUserAgentCicdTwoProvider() {
new ArrayList<>(),
System.getProperty("os.name"));
Assertions.assertTrue(UserAgent.asString().contains("cicd/gitlab"));
UserAgent.env = null;
}

@Test
public void testAgentProviderAntigravity() {
setupAgentEnv(
new HashMap<String, String>() {
{
put("ANTIGRAVITY_AGENT", "1");
}
});
Assertions.assertTrue(UserAgent.asString().contains("agent/antigravity"));
}

@Test
public void testAgentProviderClaudeCode() {
setupAgentEnv(
new HashMap<String, String>() {
{
put("CLAUDECODE", "1");
}
});
Assertions.assertTrue(UserAgent.asString().contains("agent/claude-code"));
}

@Test
public void testAgentProviderCline() {
setupAgentEnv(
new HashMap<String, String>() {
{
put("CLINE_ACTIVE", "1");
}
});
Assertions.assertTrue(UserAgent.asString().contains("agent/cline"));
}

@Test
public void testAgentProviderCodex() {
setupAgentEnv(
new HashMap<String, String>() {
{
put("CODEX_CI", "1");
}
});
Assertions.assertTrue(UserAgent.asString().contains("agent/codex"));
}

@Test
public void testAgentProviderCopilotCli() {
setupAgentEnv(
new HashMap<String, String>() {
{
put("COPILOT_CLI", "1");
}
});
Assertions.assertTrue(UserAgent.asString().contains("agent/copilot-cli"));
}

@Test
public void testAgentProviderCursor() {
setupAgentEnv(
new HashMap<String, String>() {
{
put("CURSOR_AGENT", "1");
}
});
Assertions.assertTrue(UserAgent.asString().contains("agent/cursor"));
}

@Test
public void testAgentProviderGeminiCli() {
setupAgentEnv(
new HashMap<String, String>() {
{
put("GEMINI_CLI", "1");
}
});
Assertions.assertTrue(UserAgent.asString().contains("agent/gemini-cli"));
}

@Test
public void testAgentProviderOpencode() {
setupAgentEnv(
new HashMap<String, String>() {
{
put("OPENCODE", "1");
}
});
Assertions.assertTrue(UserAgent.asString().contains("agent/opencode"));
}

@Test
public void testAgentProviderNoAgent() {
setupAgentEnv(new HashMap<>());
Assertions.assertFalse(UserAgent.asString().contains("agent/"));
}

@Test
public void testAgentProviderMultipleAgents() {
setupAgentEnv(
new HashMap<String, String>() {
{
put("CLAUDECODE", "1");
put("CURSOR_AGENT", "1");
}
});
Assertions.assertFalse(UserAgent.asString().contains("agent/"));
}

@Test
public void testAgentProviderEmptyValue() {
setupAgentEnv(
new HashMap<String, String>() {
{
put("CLAUDECODE", "");
}
});
Assertions.assertFalse(UserAgent.asString().contains("agent/"));
}

@Test
public void testAgentProviderCached() {
// Set up with cursor agent
setupAgentEnv(
new HashMap<String, String>() {
{
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<String, String>() {
{
put("CLAUDECODE", "1");
}
},
new ArrayList<>(),
System.getProperty("os.name"));
Assertions.assertTrue(UserAgent.asString().contains("agent/cursor"));
Assertions.assertFalse(UserAgent.asString().contains("agent/claude-code"));
}
}
Loading