Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions lib/crewai/src/crewai/flow/persistence/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,16 @@ async def async_method(self):
}


class _MissingFlowStateError(ValueError):
"""Signal that a flow has no state without conflating validation errors."""


def _stamp_persistence_metadata(
target: Any,
persistence: FlowPersistence,
verbose: bool,
) -> None:
"""Attach persistence configuration metadata to a flow target."""
target.__flow_persistence_config__ = SimpleNamespace(
persistence=persistence,
verbose=verbose,
Expand Down Expand Up @@ -89,12 +94,11 @@ def persist_state(
Raises:
ValueError: If flow has no state or state lacks an ID
RuntimeError: If state persistence fails
AttributeError: If flow instance lacks required state attributes
"""
try:
state = getattr(flow_instance, "state", None)
if state is None:
raise ValueError("Flow instance has no state")
raise _MissingFlowStateError

flow_uuid: str | None = None
if isinstance(state, dict):
Expand Down Expand Up @@ -130,7 +134,7 @@ def persist_state(
PRINTER.print(error_msg, color="red")
logger.error(error_msg)
raise RuntimeError(f"State persistence failed: {e!s}") from e
except AttributeError as e:
except (_MissingFlowStateError, AttributeError) as e:
error_msg = LOG_MESSAGES["state_missing"]
if verbose:
PRINTER.print(error_msg, color="red")
Expand Down
29 changes: 29 additions & 0 deletions lib/crewai/tests/test_flow_persistence.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
"""Test flow state persistence functionality."""

import os
from types import SimpleNamespace
from typing import Dict, List

import pytest
from crewai.flow.flow import Flow, FlowState, listen, start
from crewai.flow.persistence import persist
from crewai.flow.persistence.decorators import PersistenceDecorator
from crewai.flow.persistence.sqlite import SQLiteFlowPersistence
from pydantic import BaseModel

Expand All @@ -17,6 +19,33 @@ class TestState(FlowState):
message: str = ""


@pytest.mark.parametrize(
("flow_instance", "expected_message"),
[
(SimpleNamespace(), "Flow instance has no state"),
(SimpleNamespace(state=None), "Flow instance has no state"),
(
SimpleNamespace(state={}),
"Flow state must have an 'id' field for persistence",
),
],
)
def test_persist_state_reports_specific_validation_error(
tmp_path, flow_instance, expected_message
):
"""Report whether the state itself or only its ID is missing."""
persistence = SQLiteFlowPersistence(str(tmp_path / "test_flows.db"))

with pytest.raises(ValueError) as exc_info:
PersistenceDecorator.persist_state(
flow_instance,
"test_method",
persistence,
)

assert str(exc_info.value) == expected_message


def test_persist_decorator_saves_state(tmp_path, caplog):
"""Test that @persist decorator saves state in SQLite."""
db_path = os.path.join(tmp_path, "test_flows.db")
Expand Down