Files
crewAI/lib/crewai/tests/telemetry/test_coding_agent_detection.py
Joao Moura 6eef75c479 feat: surface AMP in AGENTS.md and detect coding agents in telemetry
Two related changes aimed at the OSS -> AMP bridge for projects built
with AI coding assistants.

AGENTS.md (copied into every `crewai create` project):
- Add a "when to bring it up" section near the top with explicit trigger
  conditions for traces and deployment, phrased for coding assistants.
- Add a full "Observability & Traces" section. Traces previously appeared
  once in 1018 lines despite being the zero-friction entry point; they now
  appear throughout, including that no account is required and that
  unauthenticated runs get a 24h ephemeral link.
- Note that deploying is free to get online and scales vertically and
  horizontally, so an agent suggests it instead of hand-rolling a
  Dockerfile, server, and scheduler.
- Add traces commands to the Quick Reference and two entries to Best
  Practices.
- Guidance is conditional and truthful: mention once when a trigger
  actually fires, then drop it.

Telemetry:
- Add `detect_coding_agent()`, which identifies the AI coding assistant
  running the process from environment markers (Claude Code, Cursor,
  Codex, Gemini CLI, Aider, Windsurf, Devin, Replit, Copilot, OpenHands,
  Cline, Amp), falling back to editor-terminal hints and then to
  non_interactive/unknown.
- Record it as a `coding_agent` attribute on Crew Created and Flow
  Creation spans, and emit `coding_agent:<name>` once per process as a
  feature usage event so it lands in the existing aggregation with no
  new pipeline work.
- Only the normalized assistant name is ever recorded; environment
  variable values are never read into the result.

This gives us the data to size how much of CrewAI is now authored by
coding agents, and which ones, before investing further in that channel.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UNumDnNbiyw3pv1WakAe6t
2026-08-02 09:08:20 -07:00

123 lines
3.5 KiB
Python

"""Tests for AI coding assistant detection in telemetry."""
import pytest
from crewai.telemetry.utils import detect_coding_agent
ALL_MARKERS = (
"CLAUDECODE",
"CLAUDE_CODE_ENTRYPOINT",
"CURSOR_TRACE_ID",
"CURSOR_AGENT",
"CODEX_SANDBOX",
"CODEX_SANDBOX_NETWORK_DISABLED",
"GEMINI_CLI",
"AIDER_MODEL",
"WINDSURF_SESSION_ID",
"DEVIN_SESSION_ID",
"REPLIT_AGENT",
"COPILOT_AGENT_ID",
"GITHUB_COPILOT_CLI",
"OPENHANDS_SESSION_ID",
"CLINE_ACTIVE",
"AMP_AGENT",
"TERM_PROGRAM",
"TERMINAL_EMULATOR",
)
@pytest.fixture
def clean_env(monkeypatch):
"""Remove every marker so each test starts from a known state."""
for var in ALL_MARKERS:
monkeypatch.delenv(var, raising=False)
return monkeypatch
@pytest.mark.parametrize(
("env_var", "expected"),
[
("CLAUDECODE", "claude_code"),
("CLAUDE_CODE_ENTRYPOINT", "claude_code"),
("CURSOR_TRACE_ID", "cursor"),
("CURSOR_AGENT", "cursor"),
("CODEX_SANDBOX", "codex"),
("GEMINI_CLI", "gemini_cli"),
("AIDER_MODEL", "aider"),
("WINDSURF_SESSION_ID", "windsurf"),
("DEVIN_SESSION_ID", "devin"),
("REPLIT_AGENT", "replit_agent"),
("COPILOT_AGENT_ID", "copilot"),
("OPENHANDS_SESSION_ID", "openhands"),
("CLINE_ACTIVE", "cline"),
("AMP_AGENT", "amp_code"),
],
)
def test_detects_each_coding_agent(clean_env, env_var, expected):
clean_env.setenv(env_var, "1")
assert detect_coding_agent() == expected
def test_editor_terminal_requires_exact_value(clean_env):
clean_env.setenv("TERM_PROGRAM", "vscode")
assert detect_coding_agent() == "vscode_terminal"
clean_env.setenv("TERM_PROGRAM", "iTerm.app")
assert detect_coding_agent() != "vscode_terminal"
def test_explicit_agent_marker_wins_over_editor_terminal(clean_env):
clean_env.setenv("TERM_PROGRAM", "vscode")
clean_env.setenv("CLAUDECODE", "1")
assert detect_coding_agent() == "claude_code"
def test_empty_marker_value_is_ignored(clean_env):
clean_env.setenv("CLAUDECODE", "")
assert detect_coding_agent() != "claude_code"
def test_falls_back_to_non_interactive_without_tty(clean_env, monkeypatch):
monkeypatch.setattr("sys.stdout", type("S", (), {"isatty": lambda self: False})())
assert detect_coding_agent() == "non_interactive"
def test_falls_back_to_unknown_with_tty(clean_env, monkeypatch):
monkeypatch.setattr("sys.stdout", type("S", (), {"isatty": lambda self: True})())
assert detect_coding_agent() == "unknown"
def test_never_returns_env_var_value(clean_env):
"""The detected name must never leak the environment variable's contents."""
secret = "sk-super-secret-token"
clean_env.setenv("CURSOR_TRACE_ID", secret)
assert secret not in detect_coding_agent()
def test_handles_broken_stdout(clean_env, monkeypatch):
class BrokenStdout:
def isatty(self):
raise ValueError("detached")
monkeypatch.setattr("sys.stdout", BrokenStdout())
assert detect_coding_agent() == "unknown"
def test_coding_agent_span_emits_once(clean_env, monkeypatch):
from crewai.telemetry.telemetry import Telemetry
clean_env.setenv("CLAUDECODE", "1")
telemetry = Telemetry()
telemetry._coding_agent_reported = False
emitted: list[str] = []
monkeypatch.setattr(telemetry, "feature_usage_span", emitted.append)
telemetry.coding_agent_span()
telemetry.coding_agent_span()
telemetry.coding_agent_span()
assert emitted == ["coding_agent:claude_code"]