Skip to content

Python: Return caller-owned checkpoints from InMemoryCheckpointStorage - #7712

Open
Shivani . (Shivani767) wants to merge 2 commits into
microsoft:mainfrom
Shivani767:fix/7685-inmemory-checkpoint-isolation
Open

Python: Return caller-owned checkpoints from InMemoryCheckpointStorage#7712
Shivani . (Shivani767) wants to merge 2 commits into
microsoft:mainfrom
Shivani767:fix/7685-inmemory-checkpoint-isolation

Conversation

@Shivani767

Copy link
Copy Markdown

Motivation & Context

InMemoryCheckpointStorage hands back the checkpoint objects it stores. The other backends
reconstruct a checkpoint from its serialized form on every read, so their callers get an object
they own. In-memory callers instead get a shared reference into stored state.

This is easy to miss because in-memory is the backend most workflows are developed and tested
against. Mutating a loaded checkpoint silently rewrites what is stored, and two workflow instances
restored from one checkpoint share mutable state. The same code then behaves differently once a
real backend is configured.

save() already deep-copies, so the asymmetry is on the read side only.

Description & Review Guide

  • What are the major changes?

    • load, list_checkpoints and get_latest on InMemoryCheckpointStorage return deep copies,
      matching the copy save already performs.
    • The CheckpointStorage protocol now documents its ownership contract. It previously said
      nothing about whether a returned checkpoint is owned by the caller, so there was no stated
      contract for a backend to diverge from.
    • Added a backend-parametrized conformance region to test_checkpoint.py — five tests covering
      both read and write ownership, run against each in-tree backend.
  • What is the impact of these changes?

    • No behaviour change for FileCheckpointStorage or the Cosmos backend. They already satisfied
      the contract, and the new tests pass against them unmodified.
    • On unfixed main the four read-path tests fail for in-memory and pass for file. The write-path
      test passes unfixed, which is what confirms save was already correct.
    • In-memory reads now allocate a copy. For large checkpoints that is a real cost. I have not
      benchmarked it and am not claiming a figure — happy to measure if you want it quantified.
  • What do you want reviewers to focus on?

    • Where the conformance suite belongs. It currently sits in core's test_checkpoint.py, so it
      only covers the two in-tree backends. CosmosCheckpointStorage lives in packages/azure-cosmos
      and is tested separately. Making the suite importable across packages is a larger structural
      change and I did not want to make that call unasked.
    • Whether the protocol docstring is the right home for the ownership contract.

Related Issue

Fixes #7685

Contribution Checklist

  • The code builds clean without any errors or warnings
  • All unit tests pass, and I have added new tests where possible
  • The PR follows the Contribution Guidelines
  • This PR is linked to an issue and there is no other open PR for this issue (see Related Issue above).
  • This is not a breaking change. If it is a breaking change, add the breaking change label (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds an explicit “ownership” contract for CheckpointStorage backends and enforces it for the in-memory backend via defensive copying, with shared conformance tests to prevent regressions.

Changes:

  • Documented the ownership/snapshot semantics in the CheckpointStorage protocol docstring.
  • Updated InMemoryCheckpointStorage to return deep-copied checkpoints from read APIs.
  • Added conformance tests to validate backend behavior for both memory and file storages.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
python/packages/core/tests/workflow/test_checkpoint.py Adds shared conformance tests asserting caller-owned copies and save-time snapshotting across backends.
python/packages/core/agent_framework/_workflows/_checkpoint.py Documents ownership semantics and updates in-memory backend reads to copy.deepcopy to satisfy the contract.

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

async def list_checkpoints(self, *, workflow_name: str) -> list[WorkflowCheckpoint]:
"""List checkpoint objects for a given workflow name."""
return [cp for cp in self._checkpoints.values() if cp.workflow_name == workflow_name]
return [copy.deepcopy(cp) for cp in self._checkpoints.values() if cp.workflow_name == workflow_name]

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Leaving this as a one-liner — ruff format (line-length 120) wants it on a single line. Wrapping it is what fails the formatter; that's also the likely cause of the code-quality check on #7697, which wrapped the same comprehension.

Comment on lines +1772 to +1777
# region checkpoint storage conformance

# These tests define the ownership contract that every CheckpointStorage backend must satisfy:
# a checkpoint handed to the caller is owned by the caller, and a checkpoint handed to save() is
# snapshotted at call time. Backends that serialize (file, Cosmos) get this for free because
# decoding allocates a fresh object graph; backends that hold live objects must copy explicitly.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done — the conformance suite now lives in python/packages/core/tests/workflow/test_checkpoint_storage_conformance.py. Per Tao, the same file is suggested on #7697 so the bug can land in one PR.

Comment on lines +1788 to +1798
def _conformance_checkpoint(workflow_name: str = "conformance-workflow") -> WorkflowCheckpoint:
"""Build a checkpoint whose state holds nested mutable containers."""
return WorkflowCheckpoint(
workflow_name=workflow_name,
graph_signature_hash="conformance-hash",
state={
"shared": {"counter": 0, "history": ["initial"]},
"_executor_state": {"executor1": {"visits": ["first"]}},
},
metadata={"tags": ["initial"]},
)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in the dedicated module. _conformance_checkpoint now includes nested mutable messages (WorkflowMessage.data["tags"]) and pending_request_info_events (WorkflowEvent.data["payload"]), and every ownership/snapshot test mutates those fields as well as state and metadata.

@Shivani767

Copy link
Copy Markdown
Author

Thanks — I see #7685 was closed as a duplicate of #7683 after I'd started, and that #7697 already
covers this ground more broadly, including the State.import_state aliasing that I didn't touch.

Happy to close this if it's just noise. Before I do, one thing here might be worth keeping
independently of which PR lands the fix: the tests in this PR are parametrized across storage
backends rather than written against InMemoryCheckpointStorage directly, so the same ownership
contract is asserted for every backend and a new one can't quietly regress it. That's what let the
divergence show up as "four tests fail for in-memory, pass for file" rather than as a bug report.

The related point is that CheckpointStorage is a Protocol and currently documents no ownership
semantics at all, so there's no stated contract for a backend to diverge from. This PR adds that to
the protocol docstring.

Would you like me to:

  1. close this and re-raise the parametrized suite as a follow-up once Python: [Bug]: Checkpoint state is not isolated from live workflow state across restoration and storage boundaries #7683 is resolved,
  2. move the conformance tests over to Python: Isolate checkpoint state from live workflow state across restoration and storage boundaries #7697 as a review suggestion, or
  3. leave it as is?

Happy to go whichever way is least disruptive.

@TaoChenOSU

Copy link
Copy Markdown
Contributor

Hi Shivani . (@Shivani767),

Thank you for your contributions and suggestions!

I'd say let's do #2 so that we can address the bug in one PR.

@Shivani767

Copy link
Copy Markdown
Author

Tao Chen (@TaoChenOSU) thanks — moved the conformance suite over to #7697 as a review suggestion: #7697 (review)

Summary of what I suggested there:

  • Document the ownership contract on the CheckpointStorage protocol (so every backend is bound by it, not only in-memory).
  • Replace test_checkpoint_isolation_7683.py with test_checkpoint_storage_conformance.py (no issue number; async tests), parametrized across in-memory and file storage. Nested messages and pending_request_info_events are included as well as state / metadata.
  • Collapse the wrapped list_checkpoints comprehension — ruff format wants it on one line, which is likely the failing code quality check on that PR.

I also applied the dedicated-module split on this PR so the file is easy to copy: python/packages/core/tests/workflow/test_checkpoint_storage_conformance.py (18 tests passed locally, 9 × 2 backends).

Happy to close this once those tests are in #7697.

Keep the ownership contract tests out of the already-large test_checkpoint.py
and cover nested messages and pending request events as well as state/metadata.
@Shivani767
Shivani . (Shivani767) force-pushed the fix/7685-inmemory-checkpoint-isolation branch from efa9b8f to c0585ff Compare August 20, 2026 17:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

python Usage: [Issues, PRs], Target: Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Python: [Bug]: InMemoryCheckpointStorage returns its stored checkpoint objects, unlike the other backends

3 participants