Skip to content
Merged
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
35 changes: 35 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: CI

on:
pull_request:
push:
branches:
- main

jobs:
lint:
name: lint
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Install uv
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
with:
version: "0.10.2"

- name: Install dependencies
run: uv sync --extra dev

- name: Check formatting
run: uv run ruff format --check .

- name: Lint
run: uv run ruff check .

- name: Type check
run: uv run mypy detect_agent

- name: Run tests
run: uv run pytest
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## 0.2.1

- Add support for v0 via `AI_AGENT=v0`
- Sync Cursor detection with upstream: `CURSOR_TRACE_ID` detects Cursor IDE agent-terminal sessions, and `CURSOR_AGENT` detects cursor-cli commands

## 0.2.0

- Improve detection for cursor agents
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ This package can detect the following AI agents and development environments:
- **Antigravity** (Google DeepMind)
- **GitHub Copilot** (via `AI_AGENT=github-copilot|github-copilot-cli`, `COPILOT_MODEL`, `COPILOT_ALLOW_ALL`, or `COPILOT_GITHUB_TOKEN`)
- **Replit** (online IDE)
- **v0** (Vercel's AI assistant, via `AI_AGENT=v0`)

## The AI_AGENT Standard

Expand Down
13 changes: 10 additions & 3 deletions detect_agent/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
OPENCODE: Literal["opencode"] = "opencode"
GITHUB_COPILOT: Literal["github-copilot"] = "github-copilot"
GITHUB_COPILOT_CLI: Literal["github-copilot-cli"] = "github-copilot-cli"
V0: Literal["v0"] = "v0"
PI: Literal["pi"] = "pi"

KnownAgentNames = Literal[
Expand All @@ -32,6 +33,7 @@
"augment-cli",
"opencode",
"github-copilot",
"v0",
"pi",
]

Expand Down Expand Up @@ -66,6 +68,7 @@ class AgentResultNone(TypedDict):
"AUGMENT_CLI": AUGMENT_CLI,
"OPENCODE": OPENCODE,
"GITHUB_COPILOT": GITHUB_COPILOT,
"V0": V0,
}


Expand All @@ -76,16 +79,20 @@ def determine_agent() -> AgentResult:
if name:
if name in (GITHUB_COPILOT, GITHUB_COPILOT_CLI):
return {"is_agent": True, "agent": {"name": GITHUB_COPILOT}}
return {"is_agent": True, "agent": {"name": name}} # type: ignore[return-value]
if name == V0:
return {"is_agent": True, "agent": {"name": V0}}
return {"is_agent": True, "agent": {"name": name}} # type: ignore[return-value, misc]

if os.environ.get("PI_CODING_AGENT"):
return {"is_agent": True, "agent": {"name": PI}}

if os.environ.get("CURSOR_AGENT"):
# Cursor IDE agent-terminal sessions expose CURSOR_TRACE_ID; the
# cursor-agent CLI sets CURSOR_AGENT for commands it executes.
if os.environ.get("CURSOR_TRACE_ID"):
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cursoragent can you do some web searching to see if these changes for the cursor agent detection are accurate? I think these changes are actually wrong based on my experience

Copy link
Copy Markdown
Contributor Author

@cursor cursor Bot May 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: the Cursor detection change is accurate. No functional revert needed.

What I found:

  • Upstream @vercel/detect-agent maps:
    • CURSOR_TRACE_ID -> cursor
    • CURSOR_AGENT or CURSOR_EXTENSION_HOST_ROLE=agent-exec -> cursor-cli
  • Public Cursor Agent troubleshooting references use CURSOR_TRACE_ID to detect Cursor Agent terminal sessions.
  • Current Cursor Agent CLI artifact sets CURSOR_AGENT=1 for commands it executes and does not reference CURSOR_TRACE_ID.
  • The old CURSOR_INVOKED_AS == "agent" check looks weaker; Cursor CLI uses CURSOR_INVOKED_AS mainly to infer command name/binary, not as the runtime agent signal.

Action taken:

  • Added a code comment clarifying the two signals.
  • Clarified the changelog wording.
  • Committed/pushed: 329cb19 Clarify Cursor agent detection signals

Verification:

  • 48 passed
  • ruff check . passed
  • ruff format --check . passed

Env note: this cloud image lacked uv and python3.12-venv; I installed python3.12-venv to verify. For future agents, env setup prompt: “Install uv and Python venv support for this repo so uv sync --extra dev, pytest, and ruff work out of the box.”

Sources:

Open in Web Open in Cursor 

return {"is_agent": True, "agent": {"name": CURSOR}}

if (
os.environ.get("CURSOR_INVOKED_AS") == "agent"
os.environ.get("CURSOR_AGENT")
or os.environ.get("CURSOR_EXTENSION_HOST_ROLE") == "agent-exec"
):
return {"is_agent": True, "agent": {"name": CURSOR_CLI}}
Expand Down
9 changes: 7 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ packages = ["detect_agent"]

[project]
name = "detect_agent"
version = "0.2.0"
version = "0.2.1"
description = "Detect if code is running in an AI agent or automated development environment"
readme = "README.md"
requires-python = ">=3.9"
dependencies = []
packages = ["detect_agent"]

[project.optional-dependencies]
dev = ["pytest>=7.0", "ruff>=0.8.0"]
dev = ["mypy>=1.0", "pytest>=7.0", "ruff>=0.8.0"]

[project.urls]
Homepage = "https://github.com/togethercomputer/detect_agent"
Expand All @@ -39,3 +39,8 @@ ignore = []

[tool.ruff.lint.per-file-ignores]
"tests/**/*.py" = ["S101"] # allow assert in tests

[tool.mypy]
python_version = "3.9"
strict = true
packages = ["detect_agent"]
41 changes: 36 additions & 5 deletions tests/test_detect_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
_AGENT_ENV_VARS = (
"AI_AGENT",
"PI_CODING_AGENT",
"CURSOR_TRACE_ID",
"CURSOR_AGENT",
"CURSOR_EXTENSION_HOST_ROLE",
"GEMINI_CLI",
Expand Down Expand Up @@ -90,11 +91,20 @@ def test_from_copilot_github_token(self, monkeypatch):
assert result == {"is_agent": True, "agent": {"name": KNOWN_AGENTS["GITHUB_COPILOT"]}}


class TestV0Detection:
"""v0 detection."""

def test_from_ai_agent_v0(self, monkeypatch):
monkeypatch.setenv("AI_AGENT", "v0")
result = determine_agent()
assert result == {"is_agent": True, "agent": {"name": KNOWN_AGENTS["V0"]}}


class TestCursorDetection:
"""Cursor detection."""

def test_cursor_agent_set_detects_cursor(self, monkeypatch):
monkeypatch.setenv("CURSOR_AGENT", "1")
def test_cursor_trace_id_set_detects_cursor(self, monkeypatch):
monkeypatch.setenv("CURSOR_TRACE_ID", "some-uuid")
result = determine_agent()
assert result == {"is_agent": True, "agent": {"name": KNOWN_AGENTS["CURSOR"]}}

Expand All @@ -109,7 +119,7 @@ def test_cursor_agent_not_set_returns_no_agent(self):
def test_cursor_agent_set_detects_cursor_cli(self, monkeypatch):
monkeypatch.setenv("CURSOR_AGENT", "1")
result = determine_agent()
assert result == {"is_agent": True, "agent": {"name": KNOWN_AGENTS["CURSOR"]}}
assert result == {"is_agent": True, "agent": {"name": KNOWN_AGENTS["CURSOR_CLI"]}}

def test_cursor_extension_host_role_agent_exec_detects_cursor_cli(self, monkeypatch):
monkeypatch.setenv("CURSOR_EXTENSION_HOST_ROLE", "agent-exec")
Expand Down Expand Up @@ -275,6 +285,8 @@ class TestPriorityOrderDetection:

def test_ai_agent_takes_highest_priority(self, monkeypatch):
monkeypatch.setenv("AI_AGENT", "custom-priority")
monkeypatch.setenv("PI_CODING_AGENT", "1")
monkeypatch.setenv("CURSOR_TRACE_ID", "some-uuid")
monkeypatch.setenv("CURSOR_AGENT", "1")
monkeypatch.setenv("GEMINI_CLI", "1")
monkeypatch.setenv("CODEX_SANDBOX", "seatbelt")
Expand All @@ -291,7 +303,8 @@ def test_ai_agent_takes_highest_priority(self, monkeypatch):
result = determine_agent()
assert result == {"is_agent": True, "agent": {"name": "custom-priority"}}

def test_cursor_agent_takes_priority_over_remaining_agents(self, monkeypatch):
def test_cursor_trace_id_takes_priority_over_remaining_agents(self, monkeypatch):
monkeypatch.setenv("CURSOR_TRACE_ID", "some-uuid")
monkeypatch.setenv("CURSOR_AGENT", "1")
monkeypatch.setenv("GEMINI_CLI", "1")
monkeypatch.setenv("CODEX_SANDBOX", "seatbelt")
Expand All @@ -308,12 +321,30 @@ def test_cursor_agent_takes_priority_over_remaining_agents(self, monkeypatch):
result = determine_agent()
assert result == {"is_agent": True, "agent": {"name": KNOWN_AGENTS["CURSOR"]}}

def test_cursor_agent_takes_priority_over_remaining_agents(self, monkeypatch):
monkeypatch.setenv("CURSOR_AGENT", "1")
monkeypatch.setenv("GEMINI_CLI", "1")
monkeypatch.setenv("CODEX_SANDBOX", "seatbelt")
monkeypatch.setenv("ANTIGRAVITY_AGENT", "1")
monkeypatch.setenv("AUGMENT_AGENT", "1")
monkeypatch.setenv("OPENCODE_CLIENT", "opencode")
monkeypatch.setenv("CLAUDE_CODE", "1")
monkeypatch.setenv("REPL_ID", "1")
monkeypatch.setenv("COPILOT_MODEL", "gpt-5")
monkeypatch.setenv("COPILOT_ALLOW_ALL", "true")
monkeypatch.setenv("COPILOT_GITHUB_TOKEN", "ghp_xxx")
with patch.object(Path, "exists") as mock_exists:
mock_exists.side_effect = lambda self: str(self) == DEVIN_LOCAL_PATH
result = determine_agent()
assert result == {"is_agent": True, "agent": {"name": KNOWN_AGENTS["CURSOR_CLI"]}}


class TestEdgeCases:
"""Edge cases."""

def test_empty_string_env_vars(self, monkeypatch):
monkeypatch.setenv("AI_AGENT", "")
monkeypatch.setenv("CURSOR_TRACE_ID", "")
result = determine_agent()
assert result == {"is_agent": False, "agent": None}

Expand Down Expand Up @@ -347,7 +378,7 @@ def test_is_agent_boolean(self, monkeypatch):
assert result["is_agent"] is True

def test_agent_details_when_detected(self, monkeypatch):
monkeypatch.setenv("CURSOR_AGENT", "1")
monkeypatch.setenv("CURSOR_TRACE_ID", "some-id")
result = determine_agent()
assert result["is_agent"] is True
assert result.get("agent") is not None
Expand Down
Loading
Loading