feat: embed crewai_version in checkpoints with migration framework

Write the crewAI package version into every checkpoint blob. On restore,
run version-based migrations so older checkpoints can be transformed
forward to the current format. Adds crewai.utilities.version module.
This commit is contained in:
Greyson LaLonde
2026-04-10 01:13:30 +08:00
committed by GitHub
parent 68c754883d
commit 56cf8a4384
10 changed files with 94 additions and 12 deletions

View File

@@ -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"

View File

@@ -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()

View File

@@ -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:

View File

@@ -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):

View File

@@ -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:

View File

@@ -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__)

View File

@@ -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):

View File

@@ -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:

View File

@@ -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")

View File

@@ -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"