diff --git a/lib/crewai/src/crewai/agent/core.py b/lib/crewai/src/crewai/agent/core.py index 8f3c80107..55eb807ef 100644 --- a/lib/crewai/src/crewai/agent/core.py +++ b/lib/crewai/src/crewai/agent/core.py @@ -75,6 +75,7 @@ from crewai.utilities.agent_utils import ( ) from crewai.utilities.constants import TRAINED_AGENTS_DATA_FILE, TRAINING_DATA_FILE from crewai.utilities.converter import Converter, ConverterError +from crewai.utilities.env import get_env_context from crewai.utilities.guardrail import process_guardrail from crewai.utilities.guardrail_types import GuardrailType from crewai.utilities.llm_utils import create_llm @@ -364,6 +365,7 @@ class Agent(BaseAgent): ValueError: If the max execution time is not a positive integer. RuntimeError: If the agent execution fails for other reasons. """ + get_env_context() # Only call handle_reasoning for legacy CrewAgentExecutor # For AgentExecutor, planning is handled in AgentExecutor.generate_plan() if self.executor_class is not AgentExecutor: diff --git a/lib/crewai/src/crewai/crew.py b/lib/crewai/src/crewai/crew.py index cdd371cbc..61d1f52cf 100644 --- a/lib/crewai/src/crewai/crew.py +++ b/lib/crewai/src/crewai/crew.py @@ -98,6 +98,7 @@ from crewai.types.streaming import CrewStreamingOutput from crewai.types.usage_metrics import UsageMetrics from crewai.utilities.constants import NOT_SPECIFIED, TRAINING_DATA_FILE from crewai.utilities.crew.models import CrewContext +from crewai.utilities.env import get_env_context from crewai.utilities.evaluators.crew_evaluator_handler import CrewEvaluator from crewai.utilities.evaluators.task_evaluator import TaskEvaluator from crewai.utilities.file_handler import FileHandler @@ -679,6 +680,7 @@ class Crew(FlowTrackable, BaseModel): Returns: CrewOutput or CrewStreamingOutput if streaming is enabled. """ + get_env_context() if self.stream: enable_agent_streaming(self.agents) ctx = StreamingContext() 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/listeners/tracing/trace_listener.py b/lib/crewai/src/crewai/events/listeners/tracing/trace_listener.py index b022eb582..b86d77aa1 100644 --- a/lib/crewai/src/crewai/events/listeners/tracing/trace_listener.py +++ b/lib/crewai/src/crewai/events/listeners/tracing/trace_listener.py @@ -58,6 +58,12 @@ from crewai.events.types.crew_events import ( CrewKickoffFailedEvent, CrewKickoffStartedEvent, ) +from crewai.events.types.env_events import ( + CCEnvEvent, + CodexEnvEvent, + CursorEnvEvent, + DefaultEnvEvent, +) from crewai.events.types.flow_events import ( FlowCreatedEvent, FlowFinishedEvent, @@ -192,6 +198,7 @@ class TraceCollectionListener(BaseEventListener): if self._listeners_setup: return + self._register_env_event_handlers(crewai_event_bus) self._register_flow_event_handlers(crewai_event_bus) self._register_context_event_handlers(crewai_event_bus) self._register_action_event_handlers(crewai_event_bus) @@ -200,6 +207,25 @@ class TraceCollectionListener(BaseEventListener): self._listeners_setup = True + def _register_env_event_handlers(self, event_bus: CrewAIEventsBus) -> None: + """Register handlers for environment context events.""" + + @event_bus.on(CCEnvEvent) + def on_cc_env(source: Any, event: CCEnvEvent) -> None: + self._handle_action_event("cc_env", source, event) + + @event_bus.on(CodexEnvEvent) + def on_codex_env(source: Any, event: CodexEnvEvent) -> None: + self._handle_action_event("codex_env", source, event) + + @event_bus.on(CursorEnvEvent) + def on_cursor_env(source: Any, event: CursorEnvEvent) -> None: + self._handle_action_event("cursor_env", source, event) + + @event_bus.on(DefaultEnvEvent) + def on_default_env(source: Any, event: DefaultEnvEvent) -> None: + self._handle_action_event("default_env", source, event) + def _register_flow_event_handlers(self, event_bus: CrewAIEventsBus) -> None: """Register handlers for flow events.""" 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/flow/flow.py b/lib/crewai/src/crewai/flow/flow.py index 8ef77e482..71bd31915 100644 --- a/lib/crewai/src/crewai/flow/flow.py +++ b/lib/crewai/src/crewai/flow/flow.py @@ -110,6 +110,7 @@ if TYPE_CHECKING: from crewai.flow.visualization import build_flow_structure, render_interactive from crewai.types.streaming import CrewStreamingOutput, FlowStreamingOutput +from crewai.utilities.env import get_env_context from crewai.utilities.streaming import ( TaskInfo, create_async_chunk_generator, @@ -1770,6 +1771,7 @@ class Flow(Generic[T], metaclass=FlowMeta): Returns: The final output from the flow or FlowStreamingOutput if streaming. """ + get_env_context() if self.stream: result_holder: list[Any] = [] current_task_info: TaskInfo = { 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())