Skip to content

feat: add snapshot session manager to python - #3283

Merged
opieter-aws merged 15 commits into
strands-agents:mainfrom
opieter-aws:opieter-aws/session-checkpointing
Aug 5, 2026
Merged

feat: add snapshot session manager to python#3283
opieter-aws merged 15 commits into
strands-agents:mainfrom
opieter-aws:opieter-aws/session-checkpointing

Conversation

@opieter-aws

@opieter-aws opieter-aws commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Description

The Python SDK had no way to persist an agent as a whole. The shipped session managers are a message-log design — one file or S3 object per message — which cannot express "restore the agent to how it looked three turns ago." The Snapshot primitive (agent.take_snapshot/load_snapshot) already existed and is used by the A2A executor, but nothing persisted snapshots to storage. TypeScript has shipped snapshot-based sessions with checkpointing for a while; this closes that gap.

SnapshotSessionManager captures the agent as one versioned blob per save. A mutable snapshot_latest gives crash/restart resume; append-only immutable snapshots keyed by UUIDv7 give time-travel restore. It builds on the unified Storage primitive rather than its own backend, so one storage configuration serves sessions, memory, and anything else layered on Storage. The key layout is byte-identical to TS (session/<id>/scopes/agent/<agent_id>/snapshots/…), so both SDKs share a storage convention.

Single agents only. Graph/Swarm and BidiAgent are rejected at their initialization event with NotImplementedError pointing at the message-log managers, rather than attaching and silently persisting nothing. Multi-agent snapshot persistence needs upstream fixes to Graph/Swarm serialize_state/deserialize_state: those were built for interrupt-resume inside a live process and lose state across a real crash-restart boundary. They are pre-existing, being fixed separately, and deliberately out of scope here.

Nothing is deprecated. The message-log managers are untouched and remain the only option for orchestrators and Bidi.

Public API Changes

from strands import Agent
from strands.session import SnapshotSessionManager
from strands.storage import LocalFileStorage  # or S3Storage, InMemoryStorage

session = SnapshotSessionManager(
    "user-123",
    storage=LocalFileStorage(),
    save_latest_on="invocation",                        # or "message" | "trigger"
    snapshot_trigger=lambda *, agent_data, **_: True,   # optional: append a checkpoint per turn
)
agent = Agent(session_manager=session)
agent("Hello!")  # a fresh Agent over the same storage rehydrates this

Checkpointing and time travel:

snapshot_id = await session.save_snapshot(agent, is_latest=False)  # returns the id it minted
await session.restore_snapshot(agent, snapshot_id=snapshot_id)     # rewind
await session.restore_snapshot(agent)                              # omit id -> snapshot_latest
ids = await session.list_snapshot_ids(agent)                       # oldest first
await session.delete_session()

Also exported: SnapshotTrigger (Protocol) and SaveLatestStrategy.

session = SnapshotSessionManager(
    "user-123",
    storage=LocalFileStorage(),
)

Two intentional deviations from the TS shape, both additive: save_snapshot returns the new id so callers need not follow up with list_snapshot_ids, and the trigger argument is named agent_data to match TS's SnapshotTriggerParams.agentData.

Related Issues

#3182

Documentation PR

N/A will follow up

Type of Change

New feature

Testing

  • I ran hatch run prepare

Exercised the manager end-to-end against a real Bedrock agent across a genuine two-process restart (one process writes and exits; a fresh process over the same directory rehydrates the conversation and state and the model continues coherently), plus checkpointing/time-travel, save_latest_on cadence, guardrail-redaction flush, stateful-model message discard, image-bytes round-trip. The S3Storage backend and the stateful-model (OpenAI Responses) discard run in CI via tests_integ/.

Checklist

  • I have read the CONTRIBUTING document
  • I have reviewed and understand every line of code in this PR, including any generated by AI tools, and I can explain why it works
  • My change is focused and reasonably small; I have split unrelated work into separate PRs
  • I have added any necessary tests that prove my fix is effective or my feature works
  • I have updated the documentation accordingly
  • I have added an appropriate example to the documentation to outline the feature, or no new docs are needed
  • My changes generate no new warnings
  • Any dependent changes have been merged and published

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@github-actions github-actions Bot added area-persistence Session management or checkpointing area-sessions Related to session or session managment python Pull requests that update python code enhancement New feature or request labels Jul 15, 2026
@codecov

codecov Bot commented Jul 15, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Comment thread strands-py/src/strands/session/snapshot_session_manager.py Outdated
Comment thread strands-py/src/strands/session/snapshot_session_manager.py Outdated
Comment thread strands-py/src/strands/session/snapshot_session_manager.py Outdated
Comment thread strands-py/src/strands/session/file_session_manager.py Outdated
Comment thread strands-py/src/strands/session/snapshot_session_manager.py
@github-actions

Copy link
Copy Markdown
Contributor

Assessment: Comment (a few items to resolve before merge)

Well-scoped, well-documented feature with strong test coverage (98%+) and a clear migration story. Feedback is mostly about a process gap around the public-API/deprecation surface and a couple of efficiency/robustness refinements in the save paths — nothing structurally wrong with the design.

Review Categories
  • API bar-raising (please address): This PR adds new public API (SnapshotSessionManager, SaveLatestStrategy, SnapshotTrigger) and deprecates three shipped public classes (FileSessionManager, S3SessionManager, RepositorySessionManager), but carries no needs-api-review/completed-api-review label. Both the new surface and the deprecation warrant API review per AGENTS.md / team/API_BAR_RAISING.md. The PR description's API section is a good starting point — it mostly needs the label and reviewer sign-off.
  • Deprecation mechanics: Align the deprecation with team/FEATURE_LIFECYCLE.md's implementation standard (@deprecated decorator + migration-guide link) rather than a raw warnings.warn in __init__. (inline)
  • Save-path efficiency/robustness: Redundant double capture+write of snapshot_latest on triggered invocations, and a non-atomic immutable/latest write. Both inline. delete_session's unbounded fan-out is a scale concern for S3. (inline)
  • Cross-SDK parity: The intentional redaction-flush divergence from TS is well documented — just confirm it's recorded as a decision. (inline)
  • Testing: Coverage and scenario breadth are excellent; a few restore-correctness tests could assert the full message shape instead of substring membership. (inline)

Nice work on the byte-identical key layout with the TS SDK and the read-only migration path — the design reasoning is easy to follow from the docstrings.

@opieter-aws

Copy link
Copy Markdown
Contributor Author

@strandly-the-agent Review this PR, do API BR and focus on feature parity with Typescript

@strandly-the-agent strandly-the-agent 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.

🔴 Changes requested — three reproduced blockers plus two persistence/parity defects; this substantial public primitive also needs formal API review before merge.

✅ Reviewed exact head 50bd89c8; Python snapshot/migration tests 43 passed, TS parity-oracle tests 72 passed, and git diff --check passed. Core CI jobs are green; label-size remains failed and some non-core jobs are waiting.

🔴 Reproduced: empty-ID cross-session deletion; message mode restoring pre-compaction state; no viable Graph/Swarm/Bidi replacement for the blanket deprecations.

🟡 Parity: pre-namespaced storage writes session/session/...; same-millisecond IDs violate the documented oldest-first order and differ from TS UUIDv7.

Blocking API decision: please add api/needs-review, complete the substantial-change review/meeting, and only then add api/review-complete. Should Python also expose TS-equivalent manual immutable save and latest restore (snapshot_id=None) before this becomes the recommended parity primitive?

API BR, docs, and verification details

API bar-raising

The new recommended manager and two supporting types are exported directly from strands.session, while three established public managers are deprecated. That meets the substantial-change bar in team/API_BAR_RAISING.md; the PR currently has neither API label. The PR description should also include the complete public signatures/defaults and export inventory required by that process.

TypeScript exposes saveSnapshot({ isLatest: false }) for an explicit immutable checkpoint and restoreSnapshot() without an ID for latest restore. Python currently creates immutable snapshots only through an invocation trigger, and restore_snapshot() requires an immutable ID. Please either close that gap or record the intentionally excluded workflows and rationale in the API decision.

Docs bundle

Please correct these together:

  • session-management.mdx:181: LocalFileStorage() defaults to ./.strands/, not a temporary directory.
  • session-management.mdx:264-277: use prefix="production" (the trailing slash currently produces production//) and remove the unsupported endpoint_url claim, or add that parameter to unified S3Storage.
  • session-management.mdx:196-225: qualify the layout claim. The adjacent TS examples use legacy { snapshot: FileStorage/S3Storage }; only unscoped unified TS storage adds the matching session/ namespace.

Scope

I did not repeat the four earlier inline findings that are now outdated/fixed. The existing redaction-divergence thread remains the right place to record that intentional behavioral difference.

Tests run locally:

Python: 43 passed (snapshot manager + migration + migration e2e)
TypeScript: 72 passed (SessionManager + SnapshotStorageAdapter)
git diff --check: passed

Comment thread strands-py/src/strands/session/snapshot_session_manager.py
Comment thread strands-py/src/strands/session/snapshot_session_manager.py Outdated
Comment thread strands-py/src/strands/session/file_session_manager.py
Comment thread strands-py/src/strands/session/snapshot_session_manager.py Outdated
Comment thread strands-py/src/strands/session/snapshot_session_manager.py Outdated
@opieter-aws opieter-aws linked an issue Jul 17, 2026 that may be closed by this pull request
@opieter-aws

Copy link
Copy Markdown
Contributor Author

@strandly-the-agent review the latest version

@strandly-the-agent strandly-the-agent 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.

🔴 CHANGES_REQUESTED: the new Graph/Swarm persistence can resume invalid state—causing early/double execution, replay, and terminal restore crashes—and the deprecation still sends Bidi users to a no-op replacement.

✅ Reviewed 2ffcf6ed vs ad5685f7; git diff --check passed. Focused Python suites: snapshot/migration/UUID 73, Graph/Swarm 110, legacy sessions 131 passed. CI Gate, Python matrix, lint, docs, and codecov are green.

✅ Fresh no-Bedrock repros confirmed Graph early/double join, Swarm handoff replay, terminal restore crash, and lossy serialization round-trip.

Blocking API process: this substantial public primitive needs explicit API review/meeting and api/needs-reviewapi/review-complete before merge.

Blocking parity: should Python expose TS-equivalent manual save/latest restore, multi-agent immutable restore, and configurable node-vs-invocation saves—or is each divergence intentional and approved?

Blocking wire contract: Python and TS use the same keys with incompatible payloads; should they share a canonical schema or use SDK/version-specific namespaces?

Additional verification and docs corrections

The exact latest TypeScript focused suite was not rerun; the earlier 72-pass result predates this multi-agent delta. The PR remains draft; label-size fails, and integration/preview/review jobs are waiting.

Please also correct the bundled documentation examples:

  • LocalFileStorage() defaults to ./.strands/, not a temporary directory.
  • S3Storage has no endpoint_url argument, and prefix="production/" currently creates a doubled slash.
  • The migration example points new storage at /path/to/sessions while the legacy FileSessionManager reads its unrelated default directory.

No style-only findings are included.

Comment thread strands-py/src/strands/session/snapshot_session_manager.py Outdated
Comment thread strands-py/src/strands/session/snapshot_session_manager.py Outdated
Comment thread strands-py/src/strands/session/snapshot_session_manager.py Outdated
Comment thread strands-py/src/strands/session/file_session_manager.py Outdated
Comment thread strands-py/src/strands/session/snapshot_session_manager.py Outdated
@github-actions

Copy link
Copy Markdown
Contributor

Re-review of b5a0bf1 — thanks for the quick turnaround on the API-shape questions.

✅ Resolved since last review
  • save_snapshot now returns the id (str | None) — immutable id when is_latest=False, None for latest. The is_latest=False path no longer needs a list_snapshot_ids round-trip to learn the id it just wrote.
  • restore_snapshot(*, snapshot_id=None) restores snapshot_latest — the write-latest/read-latest asymmetry is gone, and it correctly reuses the restore-on-init path (returns False when no latest exists).
  • Good, targeted tests for both (test_restore_snapshot_without_id_restores_latest, ..._returns_false_for_new_session, plus the id round-trip assertions). Local run: 65 passed.
  • needs-api-review label added — the API-process concern is addressed; this can now go through the bar-raising review.
🔴 Still blocking
  • New public API remains undocumented. session-management.mdx still has zero references to SnapshotSessionManager; b5a0bf1 didn't touch docs, and the checklist's two docs boxes are still unchecked. Since this ships as the recommended manager, it needs user-guide coverage + an example before merge.
  • The PR description is still inaccurate and now contains a broken example:
    1. It states the legacy managers "are deprecated (removal in 2.0)" and "now emit a DeprecationWarning" — neither is true in this revision (the legacy managers are untouched). Good that the code dropped the no-replacement deprecation; the description just needs to catch up.
    2. It says session-management.mdx was "Updated" under Documentation PR — it isn't changed by this PR.
    3. The snapshot_trigger example won't run: it uses lambda *, agent, **_: True, but the manager invokes the trigger as snapshot_trigger(agent_data=...). agent is a required keyword-only parameter with no default, so this raises TypeError at the first invocation. It should be lambda *, agent_data, **_: True (which is what the tests use). Worth fixing everywhere the example appears.

Net: correctness and API surface are in good shape and the two API questions are cleanly resolved — the remaining gates are documentation and reconciling the PR description with what the code actually does.

@opieter-aws
opieter-aws requested review from mkmeral and removed request for poshinchen July 31, 2026 18:32
Comment thread strands-py/src/strands/session/snapshot_session_manager.py
Comment thread strands-py/src/strands/session/snapshot_session_manager.py
Comment thread strands-py/src/strands/session/snapshot_session_manager.py Outdated
Comment thread strands-py/src/strands/session/snapshot_session_manager.py
@opieter-aws
opieter-aws requested a review from mkmeral July 31, 2026 20:36
mkmeral
mkmeral previously approved these changes Aug 3, 2026
Comment thread strands-py/src/strands/session/snapshot_session_manager.py
Comment thread strands-py/src/strands/session/snapshot_session_manager.py Outdated
Comment thread strands-py/src/strands/session/_snapshot_id.py Outdated
Comment thread strands-py/src/strands/session/snapshot_session_manager.py
Comment thread strands-py/src/strands/session/snapshot_session_manager.py Outdated
Comment thread strands-py/src/strands/session/snapshot_session_manager.py Outdated
Comment thread strands-py/tests_integ/test_snapshot_session.py Outdated
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Re-review of ed27173 ("Move uuidv7 to identifier file and address nits")

✅ Looks good
  • Clean refactor: the UUIDv7 generation/validation moves out of session/_snapshot_id.py into the shared _identifier.py (new_uuid7() / is_uuid7()). The old module is fully removed with no dangling references, and the manager keeps a thin local _validate_snapshot_id wrapper for the ValueError contract.
  • Tests relocated to test_identifier.py with the valuable coverage preserved — notably the 20,000-id intra-millisecond counter-overflow / monotonic-sort test, plus variant bits and the \A...\Z trailing-newline rejection. Local run: 61 passed.
  • Making storage optional (Storage | None = None) is a nice ergonomic win that matches the docstring's quick-start example.
🟡 One API-review consideration (non-blocking)

storage now defaults to LocalFileStorage(), which itself defaults to ./.strands/ (cwd-relative). So SnapshotSessionManager("my-session") silently persists under the current working directory. That's convenient, but for the recommended manager it makes the persistence location implicit and launch-dir-dependent — two processes started from different directories won't resume each other's state. Worth calling out explicitly in the docstring/user-guide (and a good item for the needs-api-review pass) so the default doesn't surprise anyone.

Still open from prior reviews (unchanged by this commit):

  • Docssession-management.mdx still has zero coverage of SnapshotSessionManager (this commit touched no site/ files).
  • PR description — still claims the legacy managers emit a DeprecationWarning and that session-management.mdx was updated (neither is true), and the snapshot_trigger example still uses lambda *, agent, **_ where the code requires agent_data (raises TypeError as written).

Nice incremental cleanup — the identifier extraction is a good reusability improvement.

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Re-review of 393f25d ("Remove migration path")

✅ Clean, well-scoped removal
  • The migrate_from param, _migrate_from state, _restore_from_legacy(), the RepositorySessionManager import, and the migration branch in _restore_on_init are all removed together — verified no dangling references remain in src/ (the only surviving RepositorySessionManager mention is a legitimate :class: prose cross-reference in the module docstring).
  • Inlining the now single-use _encode_json / _decode_json_object helpers back into _serialize_snapshot / _deserialize_snapshot is a reasonable simplification once migration (their second caller) is gone.
  • Both migration test files removed and the integ test tidied (import asyncio hoisted to module top). Unit suite still green locally: 61 passed.

This is a nice reduction in surface area — dropping migration removes the most complex/riskiest behavior (read-only delegation into the legacy manager) and leaves a tighter, easier-to-reason-about API.

Also resolved by recent commits (thanks!):

  • The snapshot_trigger example in the description now correctly uses agent_data, matching the SnapshotTrigger.__call__(*, agent_data=...) Protocol — the earlier example would have raised TypeError.
  • "Nothing is deprecated" is now consistent with the code.

🔴 One thing to fix — PR description is now out of date:
The description still documents the removed feature:

  • The "migrate_from accepts an existing FileSessionManager/S3SessionManager…" paragraph and its code example (migrate_from=FileSessionManager(...)) describe a parameter that no longer exists.
  • The Testing section still lists "the message-log → snapshot migration (lossless across sliding-window and summarizing conversation managers, verified read-only against the legacy store)."

Please update the description so reviewers/users aren't led to expect migrate_from.

🟡 Still open (carryover):

  • Docssession-management.mdx still uncovered (Documentation PR: N/A will follow up, checkbox unchecked — honestly flagged).
  • Storage default — omitting storage now persists to the cwd-relative ./.strands/; worth documenting explicitly for the recommended manager (good item for the needs-api-review pass).

@opieter-aws
opieter-aws merged commit a059fb9 into strands-agents:main Aug 5, 2026
36 of 38 checks passed
@opieter-aws
opieter-aws deleted the opieter-aws/session-checkpointing branch August 5, 2026 19:37
@opieter-aws

Copy link
Copy Markdown
Contributor Author

@strandly-the-agent create 3 follow-up issues:

  1. Support snapshot session managers for multi-agent
  2. Implement a migration for non-snapshot session managers
  3. A follow-up on those to deprecate the legacy session managers once we have a migration path

@strandly-the-agent

Copy link
Copy Markdown
Contributor

Filed the three follow-ups:

Two notes:

poshinchen pushed a commit to poshinchen/sdk-python that referenced this pull request Aug 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-persistence Session management or checkpointing area-sessions Related to session or session managment enhancement New feature or request needs-api-review python Pull requests that update python code size/xl

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] Add snapshot-based session management

4 participants