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 import tomli
from crewai.cli.utils import read_toml from crewai.cli.utils import read_toml
from crewai.cli.version import get_crewai_version
from crewai.crew import Crew from crewai.crew import Crew
from crewai.llm import LLM from crewai.llm import LLM
from crewai.llms.base_llm import BaseLLM 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.llm_utils import create_llm
from crewai.utilities.printer import PRINTER from crewai.utilities.printer import PRINTER
from crewai.utilities.types import LLMMessage 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" 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.authentication.main import Oauth2Settings, ProviderFactory
from crewai.cli.command import BaseCommand from crewai.cli.command import BaseCommand
from crewai.cli.settings.main import SettingsCommand 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() console = Console()

View File

@@ -6,7 +6,7 @@ import httpx
from crewai.cli.config import Settings from crewai.cli.config import Settings
from crewai.cli.constants import DEFAULT_CREWAI_ENTERPRISE_URL 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: class PlusAPI:

View File

@@ -5,7 +5,7 @@ import click
from packaging import version from packaging import version
from crewai.cli.utils import build_env_with_all_tool_credentials, read_toml 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): class CrewType(Enum):

View File

@@ -3,7 +3,6 @@
from collections.abc import Mapping from collections.abc import Mapping
from datetime import datetime, timedelta from datetime import datetime, timedelta
from functools import lru_cache from functools import lru_cache
import importlib.metadata
import json import json
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@@ -13,6 +12,8 @@ from urllib.error import URLError
import appdirs import appdirs
from packaging.version import InvalidVersion, Version, parse from packaging.version import InvalidVersion, Version, parse
from crewai.utilities.version import get_crewai_version
@lru_cache(maxsize=1) @lru_cache(maxsize=1)
def _get_cache_file() -> Path: def _get_cache_file() -> Path:
@@ -25,11 +26,6 @@ def _get_cache_file() -> Path:
return cache_dir / "version_cache.json" 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: def _is_cache_valid(cache_data: Mapping[str, Any]) -> bool:
"""Check if the cache is still valid, less than 24 hours old.""" """Check if the cache is still valid, less than 24 hours old."""
if "timestamp" not in cache_data: 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.config import Settings
from crewai.cli.constants import DEFAULT_CREWAI_ENTERPRISE_URL from crewai.cli.constants import DEFAULT_CREWAI_ENTERPRISE_URL
from crewai.cli.plus_api import PlusAPI 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.types import TraceEvent
from crewai.events.listeners.tracing.utils import ( from crewai.events.listeners.tracing.utils import (
get_user_id, get_user_id,
is_tracing_enabled_in_context, is_tracing_enabled_in_context,
should_auto_collect_first_time_traces, should_auto_collect_first_time_traces,
) )
from crewai.utilities.version import get_crewai_version
logger = getLogger(__name__) logger = getLogger(__name__)

View File

@@ -7,7 +7,6 @@ import uuid
from typing_extensions import Self from typing_extensions import Self
from crewai.cli.authentication.token import AuthError, get_auth_token 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_event_listener import BaseEventListener
from crewai.events.base_events import BaseEvent from crewai.events.base_events import BaseEvent
from crewai.events.event_bus import CrewAIEventsBus from crewai.events.event_bus import CrewAIEventsBus
@@ -127,6 +126,7 @@ from crewai.events.types.tool_usage_events import (
ToolUsageStartedEvent, ToolUsageStartedEvent,
) )
from crewai.events.utils.console_formatter import ConsoleFormatter from crewai.events.utils.console_formatter import ConsoleFormatter
from crewai.utilities.version import get_crewai_version
class TraceCollectionListener(BaseEventListener): class TraceCollectionListener(BaseEventListener):

View File

@@ -9,9 +9,11 @@ via ``RuntimeState.model_rebuild()``.
from __future__ import annotations from __future__ import annotations
import logging
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
import uuid import uuid
from packaging.version import Version
from pydantic import ( from pydantic import (
ModelWrapValidatorHandler, ModelWrapValidatorHandler,
PrivateAttr, PrivateAttr,
@@ -24,6 +26,10 @@ from crewai.context import capture_execution_context
from crewai.state.event_record import EventRecord from crewai.state.event_record import EventRecord
from crewai.state.provider.core import BaseProvider from crewai.state.provider.core import BaseProvider
from crewai.state.provider.json_provider import JsonProvider from crewai.state.provider.json_provider import JsonProvider
from crewai.utilities.version import get_crewai_version
logger = logging.getLogger(__name__)
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -61,6 +67,39 @@ def _sync_checkpoint_fields(entity: object) -> None:
entity.checkpoint_kickoff_event_id = entity._kickoff_event_id 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] class RuntimeState(RootModel): # type: ignore[type-arg]
root: list[Entity] root: list[Entity]
_provider: BaseProvider = PrivateAttr(default_factory=JsonProvider) _provider: BaseProvider = PrivateAttr(default_factory=JsonProvider)
@@ -77,6 +116,7 @@ class RuntimeState(RootModel): # type: ignore[type-arg]
@model_serializer(mode="plain") @model_serializer(mode="plain")
def _serialize(self) -> dict[str, Any]: def _serialize(self) -> dict[str, Any]:
return { return {
"crewai_version": get_crewai_version(),
"parent_id": self._parent_id, "parent_id": self._parent_id,
"branch": self._branch, "branch": self._branch,
"entities": [e.model_dump(mode="json") for e in self.root], "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] cls, data: Any, handler: ModelWrapValidatorHandler[RuntimeState]
) -> RuntimeState: ) -> RuntimeState:
if isinstance(data, dict) and "entities" in data: if isinstance(data, dict) and "entities" in data:
data = _migrate(data)
record_data = data.get("event_record") record_data = data.get("event_record")
state = handler(data["entities"]) state = handler(data["entities"])
if record_data: 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._parent_id is None
assert state._branch == "main" 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: def test_serialize_includes_lineage(self) -> None:
state = self._make_state() state = self._make_state()
state._parent_id = "parent456" state._parent_id = "parent456"