Expand AI agent detection in user-agent#768
Merged
Merged
Conversation
Add detection for Goose, Amp, Augment, VS Code Copilot, Kiro, and Windsurf. Also honor the agents.md standard AGENT env var with an "unknown" fallback when set to a value we don't recognize. Switches the detection data model from (envVar, product) pairs to agent records with a list of matchers. Each agent fires if any of its matchers fires (presence-only or exact value). Ambiguity is judged by unique product, not raw matcher hits, so the same agent setting both a bespoke var and AGENT=<name> is not ambiguous. Co-authored-by: Isaac Signed-off-by: simon <simon.faltum@databricks.com>
- Add NEXT_CHANGELOG.md entry covering the expanded agent list, the AGENT standard, and the empty-string semantics change. - When the main matcher loop finds no match and AGENT is set to a known product name, return that product name instead of "unknown" (implicit known-product fallback). Known matchers still win over the fallback, so AGENT=cursor + CLAUDECODE=1 still yields claude-code. - Restore alphabetical ordering: openclaw before opencode. - Add provenance comments on new agent entries (goose, amp, augment, copilot-vscode, kiro, windsurf). - New tests: testAgentProviderAgentEnvAmp, testAgentProviderAgentEnvCursor, testAgentProviderKnownMatcherWinsOverAgentFallback. Co-authored-by: Isaac Signed-off-by: simon <simon.faltum@databricks.com>
Previously, agents like amp and goose had dual matchers: their explicit env var (AMP_CURRENT_THREAD_ID, GOOSE_TERMINAL) plus AGENT=<name>. This caused asymmetric ambiguity: AGENT=goose + CLAUDECODE=1 resolved to "" (both matchers fired on different products), while AGENT=cursor + CLAUDECODE=1 resolved to "claude-code" (only claude-code matched, cursor was handled by the AGENT fallback which does not trigger once an explicit matcher has fired). The rule is now uniform: explicit env var matchers always take precedence over the generic AGENT=<name> signal. AGENT is treated purely as a fallback for agents without an explicit matcher, or for products we do not yet specifically recognize. Changes: - Remove per-agent AGENT=<name> matchers from amp and goose entries. Those products still set AGENT=<name>; the central fallback in lookupAgentProvider handles them. - Update the lookupAgentProvider doc comment to reflect the new rule. - Flip the existing AGENT=goose + CLAUDECODE=1 test to expect "claude-code" and rename accordingly. - Add test for GOOSE_TERMINAL=1 + AGENT=cursor -> "goose". - Add test for COPILOT_CLI=1 + COPILOT_MODEL=gpt-4 -> "" (documents the known, intentional ambiguity for Copilot CLI BYOK users). - Update NEXT_CHANGELOG entry to mention precedence rule. Signed-off-by: simon <simon.faltum@databricks.com>
Signed-off-by: simon <simon.faltum@databricks.com>
mihaimitrea-db
approved these changes
Apr 20, 2026
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>
github-merge-queue Bot
pushed a commit
to databricks/sdk-js
that referenced
this pull request
Apr 20, 2026
## Summary Adds detection for 15 AI coding agents (amp, antigravity, augment, claude-code, cline, codex, copilot-cli, copilot-vscode, cursor, gemini-cli, goose, kiro, openclaw, opencode, windsurf) so the SDK emits a single `agent/<name>` segment in its user-agent string when an agent is identified. Mirrors parallel work in the Go (databricks/databricks-sdk-go#1637), Java (databricks/databricks-sdk-java#768), and Python (databricks/databricks-sdk-py#1394) SDKs so all four SDKs ship the same canonical list and precedence rules. ## Why Databricks wants visibility into which AI coding agents are calling our APIs so that we can understand adoption, prioritize fixes for the environments our customers use, and detect compatibility issues early. The three sibling SDKs just landed this feature; the JS SDK has a smaller detection list (9 agents), emits one segment per detected agent instead of a single canonical segment, and does not honor the `AGENT=<name>` standard from agents.md. Without this change, traffic from JS SDK users running inside agents is invisible or reported inconsistently with the other SDKs. The library policy in `.agent/rules/libraries.mdc` prefers picking a dependency over hand-rolling. We intentionally deviate here: the canonical agent list, env var names, and precedence rules are coordinated across four SDKs, and existing libraries (`std-env`, `@vercel/detect-agent`) cover different subsets of agents, apply different precedence, and would re-introduce drift the moment we add a new agent. Implementation is ~80 lines with zero dependencies and matches the Go/Java/Python implementations. ## What changed ### Interface changes - **`packages/core/src/clientinfo/agent.ts`** (new) - Exports `agentProvider()` (cached for the process lifetime) and `lookupAgentProvider()` (uncached, primarily for tests). `clearAgentCache()` is exported from the module file (not the barrel) for tests only, matching the pattern documented in `.agent/rules/testing.mdc` for intentionally-unbarreled symbols. - **`packages/core/src/clientinfo/index.ts`** - Adds `agentProvider` to the public barrel. ### Behavioral changes - `createDefault()` now appends at most **one** `agent/<name>` segment instead of one per matching env var. When two explicit matchers fire simultaneously (ambiguity), no `agent/` segment is emitted. - `AGENT=<name>` is now honored as a fallback. When no explicit env var matches, `AGENT=<known-product>` maps to that product, any other non-empty `AGENT` value maps to `agent/unknown`, and an empty or unset `AGENT` emits nothing. - Explicit env vars always win over `AGENT=<name>` (e.g. `CLAUDECODE=1` + `AGENT=goose` reports `claude-code`). - Detection is cached for the process lifetime, matching Go's `sync.Once`, Java's volatile lazy init, and Python's `_agent_provider` sentinel. - Agent list grows from 9 to 15: adds amp, augment, copilot-vscode, goose, kiro, windsurf. Existing nine agents continue to work. ### Internal changes - The inlined `KNOWN_AGENTS` list and `detectAgents()` function in `packages/core/src/clientinfo/default.ts` move to the new module. - The existing `default.test.ts` test case `multiple agents all reported` is replaced by `multiple agents are ambiguous and omit the agent segment` to reflect the new ambiguity semantics. Two new cases cover the `AGENT` fallback path. Adds `clearAgentCache()` calls in `beforeEach`/`afterEach` since detection is now cached. - `packages/core/vitest.config.browser.ts` excludes `tests/clientinfo/agent.test.ts` for the same reason `default.test.ts` is excluded: agent detection reads `process.env` and is Node-only. ## How is this tested? - New `packages/core/tests/clientinfo/agent.test.ts` mirrors the Go test cases from `useragent/agent_test.go`: every agent detected via its primary env var, empty-string env values counting as set, ambiguity when two explicit matchers fire, `AGENT` fallback for known and unknown values, explicit env vars winning over `AGENT=<name>`, the pinned `COPILOT_CLI` + `COPILOT_MODEL` ambiguity case for Copilot CLI BYOK users, and cache persistence after env changes. - `npm run format:check`, `npm run lint`, `npm run typecheck`, `npm test`, and `npm run test:browser` all pass. Core package runs 240 unit tests (29 new) and 150 browser tests. --------- Signed-off-by: simon <simon.faltum@databricks.com>
…ection-expand Signed-off-by: simon <simon.faltum@databricks.com> # Conflicts: # NEXT_CHANGELOG.md
Merging main bumped the project version to 0.104.0 but left the committed lockfile.json files pinned to 0.103.0, failing the maven-lockfile validation step. Regenerate both lockfiles under JDK 11 (matching the CI configuration) and run fix-lockfile to rewrite JFrog proxy URLs back to Maven Central. Co-authored-by: Isaac Signed-off-by: simon <simon.faltum@databricks.com>
Contributor
|
If integration tests don't run automatically, an authorized user can run them manually by following the instructions below: Trigger: Inputs:
Checks will be approved automatically on success. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
We report an
agent/<name>segment in the SDK user-agent when we can identify an AI coding agent driving the SDK. The current list covers 8 agents. This PR fills obvious gaps (Goose, Amp, Augment, Kiro, Windsurf), adds best-effort detection for VS Code Copilot (distinct from the already-detected Copilot CLI), and honors the emergingAGENT=<name>agents.md standard with anunknownfallback.Identical changes are going out in parallel PRs for the Go and Python SDKs.
Changes
Before: each entry was a single
(envVar, product)pair. Presence of any non-empty value on the env var would fire the match. Multi-match returned empty.Now: each agent record holds a product name and a list of matchers. A matcher is either presence-only or an exact value match. An agent fires if any of its matchers fires. Ambiguity is judged by unique product (not raw matcher hits), so the same agent exposing both a bespoke env var and
AGENT=<name>is not ambiguous with itself. When zero known agents match andAGENTis set to a non-empty value, detection returnsunknown.New detections:
amp,augment,copilot-vscode,goose,kiro,windsurf. Goose and Amp also match onAGENT=gooseandAGENT=amprespectively. Presence-only matchers now treat an empty env value as set (matching the Go SDK'sos.LookupEnvsemantics), soCLAUDECODE=""counts as Claude Code.Test plan
AGENT=goosealone detectsgooseGOOSE_TERMINAL=1+AGENT=goosedetectsgoose(not ambiguous, same product)AMP_CURRENT_THREAD_ID+AGENT=ampdetectsamp(not ambiguous)AGENT=someweirdthingfalls back tounknownAGENT=""does not trigger the unknown fallbackAGENT=goose+CLAUDECODE=1returns empty (ambiguity between two distinct products)mvn -pl databricks-sdk-java test -Dtest=UserAgentTestpasses (34 tests)mvn -pl databricks-sdk-java spotless:checkclean