From 73b4061e9d197165596bbb0b7fe5261857afdd6f Mon Sep 17 00:00:00 2001 From: Ultronen <82553854+Ultronen@users.noreply.github.com> Date: Thu, 3 Sep 2026 03:24:04 +0800 Subject: [PATCH 1/3] fix(flow): report missing persistence state --- .../src/crewai/flow/persistence/decorators.py | 9 ++++--- lib/crewai/tests/test_flow_persistence.py | 26 +++++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/lib/crewai/src/crewai/flow/persistence/decorators.py b/lib/crewai/src/crewai/flow/persistence/decorators.py index 48b917760e..e028a27e62 100644 --- a/lib/crewai/src/crewai/flow/persistence/decorators.py +++ b/lib/crewai/src/crewai/flow/persistence/decorators.py @@ -53,6 +53,10 @@ 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, @@ -89,12 +93,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): @@ -130,7 +133,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") diff --git a/lib/crewai/tests/test_flow_persistence.py b/lib/crewai/tests/test_flow_persistence.py index b405cc64d3..02a455d5cb 100644 --- a/lib/crewai/tests/test_flow_persistence.py +++ b/lib/crewai/tests/test_flow_persistence.py @@ -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 @@ -17,6 +19,30 @@ class TestState(FlowState): message: str = "" +@pytest.mark.parametrize( + ("state", "expected_message"), + [ + (None, "Flow instance has no state"), + ({}, "Flow state must have an 'id' field for persistence"), + ], +) +def test_persist_state_reports_specific_validation_error( + tmp_path, state, expected_message +): + """Report whether the state itself or only its ID is missing.""" + persistence = SQLiteFlowPersistence(str(tmp_path / "test_flows.db")) + flow_instance = SimpleNamespace(state=state) + + 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") From 0035ec2d5903f4f3b971fc8b681e0ebefb583efe Mon Sep 17 00:00:00 2001 From: Ultronen <82553854+Ultronen@users.noreply.github.com> Date: Thu, 3 Sep 2026 03:31:38 +0800 Subject: [PATCH 2/3] test(flow): cover absent persistence state attribute --- lib/crewai/tests/test_flow_persistence.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/crewai/tests/test_flow_persistence.py b/lib/crewai/tests/test_flow_persistence.py index 02a455d5cb..6cc3b0980a 100644 --- a/lib/crewai/tests/test_flow_persistence.py +++ b/lib/crewai/tests/test_flow_persistence.py @@ -20,18 +20,21 @@ class TestState(FlowState): @pytest.mark.parametrize( - ("state", "expected_message"), + ("flow_instance", "expected_message"), [ - (None, "Flow instance has no state"), - ({}, "Flow state must have an 'id' field for persistence"), + (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, state, expected_message + 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")) - flow_instance = SimpleNamespace(state=state) with pytest.raises(ValueError) as exc_info: PersistenceDecorator.persist_state( From 5086c57d610c0cfa8c0b24d6040efe576e6f37a0 Mon Sep 17 00:00:00 2001 From: Ultronen <82553854+Ultronen@users.noreply.github.com> Date: Sat, 5 Sep 2026 01:56:32 +0800 Subject: [PATCH 3/3] docs(flow): document persistence metadata helper --- lib/crewai/src/crewai/flow/persistence/decorators.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/crewai/src/crewai/flow/persistence/decorators.py b/lib/crewai/src/crewai/flow/persistence/decorators.py index e028a27e62..635c8a5858 100644 --- a/lib/crewai/src/crewai/flow/persistence/decorators.py +++ b/lib/crewai/src/crewai/flow/persistence/decorators.py @@ -62,6 +62,7 @@ def _stamp_persistence_metadata( persistence: FlowPersistence, verbose: bool, ) -> None: + """Attach persistence configuration metadata to a flow target.""" target.__flow_persistence_config__ = SimpleNamespace( persistence=persistence, verbose=verbose,