Skip to content

[EPD-196] Add strict mode + machine-readable outcome for restore_from_state_id misses#6540

Open
joaomdmoura wants to merge 5 commits into
mainfrom
joao/epd-196-api-restorefromstateid-with-a-missing-state-id-silently
Open

[EPD-196] Add strict mode + machine-readable outcome for restore_from_state_id misses#6540
joaomdmoura wants to merge 5 commits into
mainfrom
joao/epd-196-api-restorefromstateid-with-a-missing-state-id-silently

Conversation

@joaomdmoura

@joaomdmoura joaomdmoura commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator
  • kickoff(restore_from_state_id=...) with an unknown state id used to log a warning and silently run a brand-new session — invisible to callers behind a remote API.
  • Adds an opt-in raise_on_missing_state: bool = False flag to kickoff, kickoff_async, akickoff, stream_events, and astream: when True, a restore miss (or missing persistence backend) raises ValueError: No flow state found for restore_from_state_id: '<id>' instead of silently dropping context.
  • Adds flow.last_restore_succeeded (None = no restore requested, True = hydrated, False = miss) so wrappers like the AMP deployment API can detect silent misses programmatically.
  • Default behavior is unchanged (misses still fall back silently); covered by new sync + async tests in test_flow_persistence.py and a docs note in the flow-state guide.
  • Linear: https://linear.app/crewai/issue/EPD-196

🤖 Generated with Claude Code


Note

Medium Risk
Touches persisted-state hydration at kickoff (including streaming), which can change failure modes for remote APIs when strict mode is enabled; defaults preserve prior silent-fallback behavior.

Overview
Adds opt-in strict handling when forking persisted flow state via restore_from_state_id: new raise_on_missing_state=False on kickoff, kickoff_async, akickoff, stream_events, and astream raises ValueError on a missing snapshot or missing persistence instead of silently starting fresh.

Introduces flow.last_restore_succeeded (None / True / False) so API wrappers can detect silent misses without log scraping. Streaming entry points reset the flag at session creation, run an eager pre-check when strict mode is on, and document when to read the signal after consuming frames.

Refactors restore conflict checks into shared helpers; default silent-miss behavior is unchanged. Docs note the new flag and property; persistence tests cover sync/async/streaming paths. A small OpenAI import test fix keeps sys.modules and package completion in sync after re-import.

Reviewed by Cursor Bugbot for commit 6e81617. Bugbot is set up for automated code reviews on this repo. Configure here.

…_state_id misses

- Add opt-in raise_on_missing_state flag to kickoff, kickoff_async,
  akickoff, stream_events, and astream: when True and the
  restore_from_state_id lookup misses (or no persistence backend is
  configured), kickoff raises a clear ValueError instead of silently
  running a fresh session.
- Expose flow.last_restore_succeeded (None = no restore requested,
  True = hydrated, False = miss) so API wrappers can detect silent
  restore misses without parsing logs.
- Default behavior is unchanged: misses still fall back silently.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@linear

linear Bot commented Jul 14, 2026

Copy link
Copy Markdown

EPD-196

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Flow persistence APIs now support strict missing-state handling through raise_on_missing_state and expose restore results through last_restore_succeeded. Sync, async, and streaming paths are covered by implementation, tests, and documentation; an OpenAI import test also restores a package-level module reference.

Changes

Restore handling

Layer / File(s) Summary
Public restore API
lib/crewai/src/crewai/flow/runtime/__init__.py
Execution and streaming methods accept and forward raise_on_missing_state, validate restore conflicts, and expose the latest restore result.
Restore outcome logic
lib/crewai/src/crewai/flow/runtime/__init__.py
Hydration records True for success and False for misses or unavailable persistence; strict mode raises ValueError, including during eager streaming validation.
Restore behavior validation
lib/crewai/tests/test_flow_persistence.py, docs/edge/en/guides/flows/mastering-flow-state.mdx
Tests cover synchronous, asynchronous, streaming, alias, precedence, and tri-state restore behavior; documentation describes fallback, strict errors, and detection.

OpenAI import test handling

Layer / File(s) Summary
Completion module rebinding
lib/crewai/tests/llms/openai/test_openai.py
The import test rebinds the package-level completion reference after module re-importing before applying patch targets.

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant Flow
  participant PersistenceBackend
  Caller->>Flow: kickoff with restore_from_state_id
  Flow->>Flow: reset last restore outcome
  Flow->>PersistenceBackend: load_state(restore_from_state_id)
  alt state found
    PersistenceBackend-->>Flow: persisted state
    Flow->>Flow: hydrate state and set outcome True
  else state missing or persistence unavailable
    PersistenceBackend-->>Flow: no state
    Flow->>Flow: set outcome False
    Flow-->>Caller: raise ValueError when strict mode is enabled
  end
Loading

Suggested reviewers: vinibrsl, lucasgomide

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.07% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: strict restore handling plus an outcome signal for restore_from_state_id misses.
Description check ✅ Passed The description matches the implemented strict restore flag, outcome property, tests, and docs update.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch joao/epd-196-api-restorefromstateid-with-a-missing-state-id-silently

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread lib/crewai/src/crewai/flow/runtime/__init__.py
@mintlify

mintlify Bot commented Jul 14, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
crewai 🟢 Ready View Preview Jul 14, 2026, 5:05 AM

💡 Tip: Enable Workflows to automatically generate PRs for you.

- stream_events/astream now pre-validate restore_from_state_id when
  raise_on_missing_state=True, so the ValueError surfaces at session
  creation time instead of only while consuming frames (Bugbot finding).
- Extract _missing_state_error/_no_persistence_error builders shared by
  the eager check and the runtime fork block so messages stay in sync.
- Add streaming tests: eager raise via stream_events, kickoff(stream=True),
  astream, kickoff_async(stream=True); valid-id strict streaming still
  hydrates and reports last_restore_succeeded=True.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions github-actions Bot added size/L and removed size/M labels Jul 14, 2026
Comment thread lib/crewai/src/crewai/flow/runtime/__init__.py
Comment thread lib/crewai/src/crewai/flow/runtime/__init__.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
lib/crewai/tests/test_flow_persistence.py (1)

508-512: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consistently assert the error message for strict restore failures.

Capture excinfo for the kickoff_async call to verify the error message, maintaining consistency with the other pytest.raises blocks in these tests. This prevents the test from accidentally passing on an unrelated ValueError.

🛠️ Proposed refactor
-    with pytest.raises(ValueError):
+    with pytest.raises(ValueError) as excinfo:
         await flow2.kickoff_async(
             restore_from_state_id="no-such-uuid",
             raise_on_missing_state=True,
         )
+    assert "no-such-uuid" in str(excinfo.value)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/crewai/tests/test_flow_persistence.py` around lines 508 - 512, Update the
pytest.raises block around flow2.kickoff_async to capture the raised exception
and assert that its message identifies the missing state ID, matching the
message-validation pattern used by nearby tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@lib/crewai/tests/test_flow_persistence.py`:
- Around line 508-512: Update the pytest.raises block around flow2.kickoff_async
to capture the raised exception and assert that its message identifies the
missing state ID, matching the message-validation pattern used by nearby tests.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e253e3f2-d597-4986-94ef-5f07bb2fc8b6

📥 Commits

Reviewing files that changed from the base of the PR and between 749701c and 2aae1fc.

📒 Files selected for processing (2)
  • lib/crewai/src/crewai/flow/runtime/__init__.py
  • lib/crewai/tests/test_flow_persistence.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/crewai/src/crewai/flow/runtime/init.py

…heck first

- _ensure_restorable_state now sets last_restore_succeeded=False before
  raising, matching the runtime path, so streaming callers failing in the
  pre-check see the miss signal instead of None or a stale True.
- stream_events/astream validate the from_checkpoint/restore_from_state_id
  mutual exclusion before the strict restore pre-check, so the conflict
  ValueError wins over the restore-miss one (matching non-streaming
  precedence). Conflict check extracted to _check_restore_conflict and
  reused by kickoff/kickoff_async.
- Failing-first tests for the stale signal (incl. prior-success staleness
  and no-persistence path) and for conflict precedence on both stream
  entry points.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions github-actions Bot added size/XL and removed size/L labels Jul 14, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
lib/crewai/tests/test_flow_persistence.py (2)

586-603: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use match parameter in pytest.raises for conciseness.

Instead of manually extracting and asserting on the exception message, you can use the match parameter in pytest.raises for a more idiomatic and concise assertion.

♻️ Proposed refactor
-    with pytest.raises(ValueError) as excinfo:
+    with pytest.raises(ValueError, match="Cannot combine `from_checkpoint` and `restore_from_state_id`"):
         flow.stream_events(
             from_checkpoint=CheckpointConfig(),
             restore_from_state_id="no-such-uuid",
             raise_on_missing_state=True,
         )
-    msg = str(excinfo.value)
-    assert "Cannot combine" in msg
-    assert "from_checkpoint" in msg

     flow2 = StreamConflictFlow(persistence=persistence)
-    with pytest.raises(ValueError) as excinfo2:
+    with pytest.raises(ValueError, match="Cannot combine `from_checkpoint` and `restore_from_state_id`"):
         flow2.astream(
             from_checkpoint=CheckpointConfig(),
             restore_from_state_id="no-such-uuid",
             raise_on_missing_state=True,
         )
-    assert "Cannot combine" in str(excinfo2.value)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/crewai/tests/test_flow_persistence.py` around lines 586 - 603, Update the
two pytest.raises blocks for flow.stream_events and flow2.astream to use the
match parameter with a regex covering the expected “Cannot combine” and
“from_checkpoint” text, then remove the manual exception capture, message
conversion, and separate assertions.

539-567: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Assert specific exception messages in pytest.raises.

Catching a generic ValueError without validating its message could accidentally pass if an unrelated ValueError is raised (e.g., due to an initialization error). Consider using the match parameter to assert the specific error message to make the test more robust.

♻️ Proposed refactor
-    with pytest.raises(ValueError):
+    with pytest.raises(ValueError, match="No flow state found"):
         flow2.stream_events(
             restore_from_state_id="no-such-uuid",
             raise_on_missing_state=True,
         )
     assert flow2.last_restore_succeeded is False

     # Fresh instance failing in the astream pre-check: False, not None.
     flow3 = EagerSignalFlow(persistence=persistence)
-    with pytest.raises(ValueError):
+    with pytest.raises(ValueError, match="No flow state found"):
         flow3.astream(
             restore_from_state_id="no-such-uuid",
             raise_on_missing_state=True,
         )
     assert flow3.last_restore_succeeded is False

     # No-persistence eager failure records the miss too.
     class NoPersistenceStreamFlow(Flow[TestState]):
         `@start`()
         def step(self):
             self.state.counter += 1

     flow4 = NoPersistenceStreamFlow()
-    with pytest.raises(ValueError):
+    with pytest.raises(ValueError, match="no persistence backend"):
         flow4.stream_events(
             restore_from_state_id="some-uuid",
             raise_on_missing_state=True,
         )
     assert flow4.last_restore_succeeded is False
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/crewai/tests/test_flow_persistence.py` around lines 539 - 567, Update
each pytest.raises(ValueError) assertion in the persistence restore tests for
flow2, flow3, and flow4 to use the match parameter with the expected
missing-state error message. Keep the existing exception type and
last_restore_succeeded assertions unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@lib/crewai/tests/test_flow_persistence.py`:
- Around line 586-603: Update the two pytest.raises blocks for
flow.stream_events and flow2.astream to use the match parameter with a regex
covering the expected “Cannot combine” and “from_checkpoint” text, then remove
the manual exception capture, message conversion, and separate assertions.
- Around line 539-567: Update each pytest.raises(ValueError) assertion in the
persistence restore tests for flow2, flow3, and flow4 to use the match parameter
with the expected missing-state error message. Keep the existing exception type
and last_restore_succeeded assertions unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e4bd6c59-02c5-4516-8aed-a94ffab958ce

📥 Commits

Reviewing files that changed from the base of the PR and between 2aae1fc and 79488fd.

📒 Files selected for processing (2)
  • lib/crewai/src/crewai/flow/runtime/__init__.py
  • lib/crewai/tests/test_flow_persistence.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/crewai/src/crewai/flow/runtime/init.py

…es mocks

Not caused by this PR's change — a latent leak in
test_openai_completion_module_is_imported exposed by pytest-randomly's
per-run ordering (CI seed 756721157 placed it before the azure responses
tests on the same xdist worker of the py3.10 shard).

Mechanism: the test monkeypatch.delitem's
crewai.llms.providers.openai.completion from sys.modules and re-imports
it, which rebinds the parent package's `completion` attribute to the new
module object. monkeypatch teardown restores only the sys.modules entry,
leaving the package attribute pointing at the stale re-imported module.
On Python 3.10, `from crewai.llms.providers.openai.completion import
OpenAICompletion` resolves through the parent package attribute while
mock.patch targets the sys.modules entry, so the azure responses tests'
OpenAICompletion patch silently missed and a real delegate was built
(last_response_id None, reset_chain never called). Python 3.11+ resolves
via sys.modules, hiding the leak.

Fix: record the package attribute with monkeypatch.setattr alongside the
delitem so teardown restores both bindings consistently.

Reproduced on pristine origin/main (py3.10, -n0, culprit test followed by
the three azure responses tests) and verified green after the fix,
including a replay of the failing CI worker's exact 181-test order.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@joaomdmoura

Copy link
Copy Markdown
Collaborator Author

The CI failures in tests/llms/azure/test_azure_responses.py were a pre-existing test-isolation leak, not caused by this PR's flow-runtime change. Fixed in ad0523b as a separate commit.

Mechanism (reproduced on pristine origin/main, Python 3.10):

  • test_openai_completion_module_is_imported (tests/llms/openai/test_openai.py) removes crewai.llms.providers.openai.completion from sys.modules via monkeypatch.delitem and re-imports it. The re-import also rebinds the parent package attribute crewai.llms.providers.openai.completion to the new module object; monkeypatch teardown restores only the sys.modules entry, leaving the two bindings pointing at different module objects for the rest of the process.
  • On Python 3.10, from crewai.llms.providers.openai.completion import OpenAICompletion (the dynamic import inside AzureCompletion._init_responses_delegate) resolves through the stale package attribute, while the azure tests' mock.patch("crewai.llms.providers.openai.completion.OpenAICompletion", ...) patches the restored sys.modules entry — so the patch silently misses, a real delegate is constructed, and last_response_id/last_reasoning_items are None and reset_chain is never called. Python 3.11+ resolves via sys.modules, which is why only the 3.10 shard failed.
  • The trigger is ordering luck: pytest-randomly picks a fresh seed per run (this run: 756721157), and that seed put the openai module-import test before the azure responses tests on the same xdist worker (gw0, group 6). Any PR could have hit it.

Fix: record the parent package attribute with monkeypatch.setattr alongside the delitem, so teardown restores both bindings consistently. Verified by replaying the failing CI worker's exact 181-test order under Python 3.10: 3 failed before, all pass after.

🤖 Generated with Claude Code

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit ad0523b. Configure here.

Comment thread lib/crewai/src/crewai/flow/runtime/__init__.py
…eated

- stream_events/astream (and thus kickoff/kickoff_async with stream=True)
  now reset last_restore_succeeded to None before returning the session,
  matching kickoff_async's reset-at-start semantics. Previously the
  property held the prior kickoff's stale True/False until frames were
  consumed (Bugbot round 3).
- The eager strict-raise path is unchanged: with raise_on_missing_state a
  miss still sets the signal to False before raising at session creation.
- Document streaming timing on the last_restore_succeeded property.
- Failing-first sync + async tests: stale True from a prior restore reads
  None right after obtaining a new session, then False after consumption.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant