diff --git a/lib/crewai/src/crewai/events/event_listener.py b/lib/crewai/src/crewai/events/event_listener.py index c4b514f7c..8e063f4d3 100644 --- a/lib/crewai/src/crewai/events/event_listener.py +++ b/lib/crewai/src/crewai/events/event_listener.py @@ -34,6 +34,12 @@ from crewai.events.types.crew_events import ( CrewTrainFailedEvent, CrewTrainStartedEvent, ) +from crewai.events.types.env_events import ( + CCEnvEvent, + CodexEnvEvent, + CursorEnvEvent, + DefaultEnvEvent, +) from crewai.events.types.flow_events import ( FlowCreatedEvent, FlowFinishedEvent, @@ -143,6 +149,23 @@ class EventListener(BaseEventListener): # ----------- CREW EVENTS ----------- def setup_listeners(self, crewai_event_bus: CrewAIEventsBus) -> None: + + @crewai_event_bus.on(CCEnvEvent) + def on_cc_env(_: Any, event: CCEnvEvent) -> None: + self._telemetry.env_context_span(event.type) + + @crewai_event_bus.on(CodexEnvEvent) + def on_codex_env(_: Any, event: CodexEnvEvent) -> None: + self._telemetry.env_context_span(event.type) + + @crewai_event_bus.on(CursorEnvEvent) + def on_cursor_env(_: Any, event: CursorEnvEvent) -> None: + self._telemetry.env_context_span(event.type) + + @crewai_event_bus.on(DefaultEnvEvent) + def on_default_env(_: Any, event: DefaultEnvEvent) -> None: + self._telemetry.env_context_span(event.type) + @crewai_event_bus.on(CrewKickoffStartedEvent) def on_crew_started(source: Any, event: CrewKickoffStartedEvent) -> None: self.formatter.handle_crew_started(event.crew_name or "Crew", source.id) diff --git a/lib/crewai/src/crewai/events/types/env_events.py b/lib/crewai/src/crewai/events/types/env_events.py new file mode 100644 index 000000000..3dad7b5f9 --- /dev/null +++ b/lib/crewai/src/crewai/events/types/env_events.py @@ -0,0 +1,36 @@ +from typing import Annotated, Literal + +from pydantic import Field, TypeAdapter + +from crewai.events.base_events import BaseEvent + + +class CCEnvEvent(BaseEvent): + type: Literal["cc_env"] = "cc_env" + + +class CodexEnvEvent(BaseEvent): + type: Literal["codex_env"] = "codex_env" + + +class CursorEnvEvent(BaseEvent): + type: Literal["cursor_env"] = "cursor_env" + + +class DefaultEnvEvent(BaseEvent): + type: Literal["default_env"] = "default_env" + + +EnvContextEvent = Annotated[ + CCEnvEvent | CodexEnvEvent | CursorEnvEvent | DefaultEnvEvent, + Field(discriminator="type"), +] + +env_context_event_adapter: TypeAdapter[EnvContextEvent] = TypeAdapter(EnvContextEvent) + +ENV_CONTEXT_EVENT_TYPES: tuple[type[BaseEvent], ...] = ( + CCEnvEvent, + CodexEnvEvent, + CursorEnvEvent, + DefaultEnvEvent, +) diff --git a/lib/crewai/src/crewai/telemetry/telemetry.py b/lib/crewai/src/crewai/telemetry/telemetry.py index 136a7d7d0..ff4977254 100644 --- a/lib/crewai/src/crewai/telemetry/telemetry.py +++ b/lib/crewai/src/crewai/telemetry/telemetry.py @@ -986,6 +986,22 @@ class Telemetry: self._safe_telemetry_operation(_operation) + def env_context_span(self, tool: str) -> None: + """Records the coding tool environment context.""" + + def _operation() -> None: + tracer = trace.get_tracer("crewai.telemetry") + span = tracer.start_span("Environment Context") + self._add_attribute( + span, + "crewai_version", + version("crewai"), + ) + self._add_attribute(span, "tool", tool) + close_span(span) + + self._safe_telemetry_operation(_operation) + def human_feedback_span( self, event_type: str, diff --git a/lib/crewai/src/crewai/utilities/constants.py b/lib/crewai/src/crewai/utilities/constants.py index f1fbcd4d0..366c1c4f2 100644 --- a/lib/crewai/src/crewai/utilities/constants.py +++ b/lib/crewai/src/crewai/utilities/constants.py @@ -8,6 +8,21 @@ TRAINED_AGENTS_DATA_FILE: Final[str] = "trained_agents_data.pkl" KNOWLEDGE_DIRECTORY: Final[str] = "knowledge" MAX_FILE_NAME_LENGTH: Final[int] = 255 EMITTER_COLOR: Final[PrinterColor] = "bold_blue" +CC_ENV_VAR: Final[str] = "CLAUDECODE" +CODEX_ENV_VARS: Final[tuple[str, ...]] = ( + "CODEX_CI", + "CODEX_MANAGED_BY_NPM", + "CODEX_SANDBOX", + "CODEX_SANDBOX_NETWORK_DISABLED", + "CODEX_THREAD_ID", +) +CURSOR_ENV_VARS: Final[tuple[str, ...]] = ( + "CURSOR_AGENT", + "CURSOR_EXTENSION_HOST_ROLE", + "CURSOR_SANDBOX", + "CURSOR_TRACE_ID", + "CURSOR_WORKSPACE_LABEL", +) class _NotSpecified: diff --git a/lib/crewai/src/crewai/utilities/env.py b/lib/crewai/src/crewai/utilities/env.py new file mode 100644 index 000000000..af77faefc --- /dev/null +++ b/lib/crewai/src/crewai/utilities/env.py @@ -0,0 +1,39 @@ +import contextvars +import os + +from crewai.events.event_bus import crewai_event_bus +from crewai.events.types.env_events import ( + CCEnvEvent, + CodexEnvEvent, + CursorEnvEvent, + DefaultEnvEvent, +) +from crewai.utilities.constants import CC_ENV_VAR, CODEX_ENV_VARS, CURSOR_ENV_VARS + + +_env_context_emitted: contextvars.ContextVar[bool] = contextvars.ContextVar( + "_env_context_emitted", default=False +) + + +def _is_codex_env() -> bool: + return any(os.environ.get(var) for var in CODEX_ENV_VARS) + + +def _is_cursor_env() -> bool: + return any(os.environ.get(var) for var in CURSOR_ENV_VARS) + + +def get_env_context() -> None: + if _env_context_emitted.get(): + return + _env_context_emitted.set(True) + + if os.environ.get(CC_ENV_VAR): + crewai_event_bus.emit(None, CCEnvEvent()) + elif _is_codex_env(): + crewai_event_bus.emit(None, CodexEnvEvent()) + elif _is_cursor_env(): + crewai_event_bus.emit(None, CursorEnvEvent()) + else: + crewai_event_bus.emit(None, DefaultEnvEvent())