diff --git a/lib/crewai/src/crewai/cli/crew_chat.py b/lib/crewai/src/crewai/cli/crew_chat.py index ad1c65894..61d9b4d9e 100644 --- a/lib/crewai/src/crewai/cli/crew_chat.py +++ b/lib/crewai/src/crewai/cli/crew_chat.py @@ -13,7 +13,6 @@ from packaging import version import tomli from crewai.cli.utils import read_toml -from crewai.cli.version import get_crewai_version from crewai.crew import Crew from crewai.llm import LLM from crewai.llms.base_llm import BaseLLM @@ -21,6 +20,7 @@ from crewai.types.crew_chat import ChatInputField, ChatInputs from crewai.utilities.llm_utils import create_llm from crewai.utilities.printer import PRINTER from crewai.utilities.types import LLMMessage +from crewai.utilities.version import get_crewai_version MIN_REQUIRED_VERSION: Final[Literal["0.98.0"]] = "0.98.0" diff --git a/lib/crewai/src/crewai/cli/enterprise/main.py b/lib/crewai/src/crewai/cli/enterprise/main.py index 395de418b..2977868f2 100644 --- a/lib/crewai/src/crewai/cli/enterprise/main.py +++ b/lib/crewai/src/crewai/cli/enterprise/main.py @@ -7,7 +7,7 @@ from rich.console import Console from crewai.cli.authentication.main import Oauth2Settings, ProviderFactory from crewai.cli.command import BaseCommand from crewai.cli.settings.main import SettingsCommand -from crewai.cli.version import get_crewai_version +from crewai.utilities.version import get_crewai_version console = Console() diff --git a/lib/crewai/src/crewai/cli/plus_api.py b/lib/crewai/src/crewai/cli/plus_api.py index ac7acfda9..862ab81e8 100644 --- a/lib/crewai/src/crewai/cli/plus_api.py +++ b/lib/crewai/src/crewai/cli/plus_api.py @@ -6,7 +6,7 @@ import httpx from crewai.cli.config import Settings from crewai.cli.constants import DEFAULT_CREWAI_ENTERPRISE_URL -from crewai.cli.version import get_crewai_version +from crewai.utilities.version import get_crewai_version class PlusAPI: diff --git a/lib/crewai/src/crewai/cli/run_crew.py b/lib/crewai/src/crewai/cli/run_crew.py index 6f031f245..ba2202032 100644 --- a/lib/crewai/src/crewai/cli/run_crew.py +++ b/lib/crewai/src/crewai/cli/run_crew.py @@ -5,7 +5,7 @@ import click from packaging import version from crewai.cli.utils import build_env_with_all_tool_credentials, read_toml -from crewai.cli.version import get_crewai_version +from crewai.utilities.version import get_crewai_version class CrewType(Enum): diff --git a/lib/crewai/src/crewai/cli/version.py b/lib/crewai/src/crewai/cli/version.py index 60eb3a95a..232aa2423 100644 --- a/lib/crewai/src/crewai/cli/version.py +++ b/lib/crewai/src/crewai/cli/version.py @@ -3,7 +3,6 @@ from collections.abc import Mapping from datetime import datetime, timedelta from functools import lru_cache -import importlib.metadata import json from pathlib import Path from typing import Any @@ -13,6 +12,8 @@ from urllib.error import URLError import appdirs from packaging.version import InvalidVersion, Version, parse +from crewai.utilities.version import get_crewai_version + @lru_cache(maxsize=1) def _get_cache_file() -> Path: @@ -25,11 +26,6 @@ def _get_cache_file() -> Path: return cache_dir / "version_cache.json" -def get_crewai_version() -> str: - """Get the version number of CrewAI running the CLI.""" - return importlib.metadata.version("crewai") - - def _is_cache_valid(cache_data: Mapping[str, Any]) -> bool: """Check if the cache is still valid, less than 24 hours old.""" if "timestamp" not in cache_data: diff --git a/lib/crewai/src/crewai/events/listeners/tracing/trace_batch_manager.py b/lib/crewai/src/crewai/events/listeners/tracing/trace_batch_manager.py index 1a25b68a9..d2a0912f6 100644 --- a/lib/crewai/src/crewai/events/listeners/tracing/trace_batch_manager.py +++ b/lib/crewai/src/crewai/events/listeners/tracing/trace_batch_manager.py @@ -13,13 +13,13 @@ from crewai.cli.authentication.token import AuthError, get_auth_token from crewai.cli.config import Settings from crewai.cli.constants import DEFAULT_CREWAI_ENTERPRISE_URL from crewai.cli.plus_api import PlusAPI -from crewai.cli.version import get_crewai_version from crewai.events.listeners.tracing.types import TraceEvent from crewai.events.listeners.tracing.utils import ( get_user_id, is_tracing_enabled_in_context, should_auto_collect_first_time_traces, ) +from crewai.utilities.version import get_crewai_version logger = getLogger(__name__) 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 0e3b284c0..c4cc6cb71 100644 --- a/lib/crewai/src/crewai/events/listeners/tracing/trace_listener.py +++ b/lib/crewai/src/crewai/events/listeners/tracing/trace_listener.py @@ -7,7 +7,6 @@ import uuid from typing_extensions import Self from crewai.cli.authentication.token import AuthError, get_auth_token -from crewai.cli.version import get_crewai_version from crewai.events.base_event_listener import BaseEventListener from crewai.events.base_events import BaseEvent from crewai.events.event_bus import CrewAIEventsBus @@ -127,6 +126,7 @@ from crewai.events.types.tool_usage_events import ( ToolUsageStartedEvent, ) from crewai.events.utils.console_formatter import ConsoleFormatter +from crewai.utilities.version import get_crewai_version class TraceCollectionListener(BaseEventListener): diff --git a/lib/crewai/src/crewai/state/runtime.py b/lib/crewai/src/crewai/state/runtime.py index b4293ad39..79c836e1e 100644 --- a/lib/crewai/src/crewai/state/runtime.py +++ b/lib/crewai/src/crewai/state/runtime.py @@ -9,9 +9,11 @@ via ``RuntimeState.model_rebuild()``. from __future__ import annotations +import logging from typing import TYPE_CHECKING, Any import uuid +from packaging.version import Version from pydantic import ( ModelWrapValidatorHandler, PrivateAttr, @@ -24,6 +26,10 @@ from crewai.context import capture_execution_context from crewai.state.event_record import EventRecord from crewai.state.provider.core import BaseProvider from crewai.state.provider.json_provider import JsonProvider +from crewai.utilities.version import get_crewai_version + + +logger = logging.getLogger(__name__) if TYPE_CHECKING: @@ -61,6 +67,39 @@ def _sync_checkpoint_fields(entity: object) -> None: entity.checkpoint_kickoff_event_id = entity._kickoff_event_id +def _migrate(data: dict[str, Any]) -> dict[str, Any]: + """Apply version-based migrations to checkpoint data. + + Each block handles checkpoints older than a specific version, + transforming them forward to the current format. Blocks run in + version order so migrations compose. + + Args: + data: The raw deserialized checkpoint dict. + + Returns: + The migrated checkpoint dict. + """ + raw = data.get("crewai_version") + current = Version(get_crewai_version()) + stored = Version(raw) if raw else Version("0.0.0") + + if raw is None: + logger.warning("Checkpoint has no crewai_version — treating as 0.0.0") + elif stored != current: + logger.debug( + "Migrating checkpoint from crewAI %s to %s", + stored, + current, + ) + + # --- migrations in version order --- + # if stored < Version("X.Y.Z"): + # data.setdefault("some_field", "default") + + return data + + class RuntimeState(RootModel): # type: ignore[type-arg] root: list[Entity] _provider: BaseProvider = PrivateAttr(default_factory=JsonProvider) @@ -77,6 +116,7 @@ class RuntimeState(RootModel): # type: ignore[type-arg] @model_serializer(mode="plain") def _serialize(self) -> dict[str, Any]: return { + "crewai_version": get_crewai_version(), "parent_id": self._parent_id, "branch": self._branch, "entities": [e.model_dump(mode="json") for e in self.root], @@ -89,6 +129,7 @@ class RuntimeState(RootModel): # type: ignore[type-arg] cls, data: Any, handler: ModelWrapValidatorHandler[RuntimeState] ) -> RuntimeState: if isinstance(data, dict) and "entities" in data: + data = _migrate(data) record_data = data.get("event_record") state = handler(data["entities"]) if record_data: diff --git a/lib/crewai/src/crewai/utilities/version.py b/lib/crewai/src/crewai/utilities/version.py new file mode 100644 index 000000000..57a5c562d --- /dev/null +++ b/lib/crewai/src/crewai/utilities/version.py @@ -0,0 +1,12 @@ +"""Version utilities for crewAI.""" + +from __future__ import annotations + +from functools import cache +import importlib.metadata + + +@cache +def get_crewai_version() -> str: + """Get the installed crewAI version string.""" + return importlib.metadata.version("crewai") diff --git a/lib/crewai/tests/test_checkpoint.py b/lib/crewai/tests/test_checkpoint.py index cbea4b562..0a0c00027 100644 --- a/lib/crewai/tests/test_checkpoint.py +++ b/lib/crewai/tests/test_checkpoint.py @@ -196,6 +196,39 @@ class TestRuntimeStateLineage: assert state._parent_id is None assert state._branch == "main" + def test_serialize_includes_version(self) -> None: + from crewai.utilities.version import get_crewai_version + + state = self._make_state() + dumped = json.loads(state.model_dump_json()) + assert dumped["crewai_version"] == get_crewai_version() + + def test_deserialize_migrates_on_version_mismatch(self, caplog: Any) -> None: + import logging + + state = self._make_state() + raw = state.model_dump_json() + data = json.loads(raw) + data["crewai_version"] = "0.1.0" + with caplog.at_level(logging.DEBUG): + RuntimeState.model_validate_json( + json.dumps(data), context={"from_checkpoint": True} + ) + assert "Migrating checkpoint from crewAI 0.1.0" in caplog.text + + def test_deserialize_warns_on_missing_version(self, caplog: Any) -> None: + import logging + + state = self._make_state() + raw = state.model_dump_json() + data = json.loads(raw) + data.pop("crewai_version", None) + with caplog.at_level(logging.WARNING): + RuntimeState.model_validate_json( + json.dumps(data), context={"from_checkpoint": True} + ) + assert "treating as 0.0.0" in caplog.text + def test_serialize_includes_lineage(self) -> None: state = self._make_state() state._parent_id = "parent456"