mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-08-10 08:21:54 +00:00
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
123 lines
3.5 KiB
Python
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"]
|