Skip to content

examples: MongoDB SessionStore reference adapter#1014

Open
alexbevi wants to merge 8 commits into
anthropics:mainfrom
alexbevi:mongodb-session-store
Open

examples: MongoDB SessionStore reference adapter#1014
alexbevi wants to merge 8 commits into
anthropics:mainfrom
alexbevi:mongodb-session-store

Conversation

@alexbevi
Copy link
Copy Markdown

@alexbevi alexbevi commented Jun 2, 2026

A self-contained reference SessionStore implementation for MongoDB under examples/session_stores/mongodb_session_store.py, alongside the existing S3, Redis, and Postgres adapters. Imports types from the published claude_agent_sdk package and passes the full 14-contract conformance suite at src/claude_agent_sdk/testing/session_store_conformance.py.

Adapter Backend client Unit tests Live tests
mongodb_session_store.py pymongo (>=4.13) constructor only env-gated SESSION_STORE_MONGODB_URL

Storage model

Two collections share one database. Entries — one document per JSONL transcript entry, ordered by the server-assigned _id (ObjectId):

{ "_id": ObjectId, "project_key": str, "session_id": str,
  "subpath": str,           # "" sentinel for the main transcript
  "entry": <opaque JSON>, "mtime": int }     # epoch ms

Summaries — one document per main session, maintained incrementally inside append() via fold_session_summary:

{ "_id": {"project_key": str, "session_id": str},
  "mtime": int, "data": <opaque SDK-owned dict> }

create_schema() creates three indexes (idempotent). append() is a single insert_many plus an atomic summary upsert; load() is find().sort("_id", 1).

What's different from the other adapters

This adapter implements the optional list_session_summaries method (S3/Redis/Postgres skip it via Protocol-default detection). That gives list_sessions_from_store() its fast path — one batch read for all summaries plus a cheap list_sessions() to gap-fill — instead of the fallback's N per-session load() calls (bounded at 16 concurrent). For projects with many sessions or remote-backend latency, the README explains the tradeoff.

The summary read-fold-write inside append() is serialized with a per-session asyncio.Lock to satisfy the protocol's concurrency requirement.

Driver

Uses pymongo's stable async API (pymongo.AsyncMongoClient, introduced in pymongo 4.13). pymongo>=4.13 is added to the [examples] optional-dependency group; the test module importorskips so default CI is unaffected.

Conformance with proposed CONTRIBUTING.md (#1005)

This PR was sanity-checked against the developer workflow proposed in #1005 — the package passes every check that guide will enforce on src/ and tests/:

  • ruff check src/ tests/ — clean (incl. ruff B023, which had flagged a closure-over-loop-variable bug in the concurrency test).
  • ruff format --check src/ tests/ — clean (the test file is ruff format-stable).
  • mypy src/ — clean (no SDK source touched).
  • ✅ Conventional Commits — every commit on the branch uses one of the prefixes the guide enumerates (feat:, chore:, test:, docs:).

The adapter module under examples/ is outside the guide's required surface but is formatted to the same standard since it's reference code users copy-paste.

Running

cd claude-agent-sdk-python
pip install -e '.[dev,examples]'
pytest tests/test_example_mongodb_session_store.py            # skips without env

docker run -d -p 27017:27017 mongo:latest
SESSION_STORE_MONGODB_URL=mongodb://localhost:27017 \
    pytest tests/test_example_mongodb_session_store.py -v     # live conformance + adapter tests

CI / packaging impact: none

Nothing under examples/ is packaged or built by repo CI. The test module importorskips on pymongo and module-level skips on the env var, so default CI is unaffected if [examples] isn't installed. No workflow files added; no CHANGELOG entry (examples are reference code, not versioned, per RELEASING.md).

Verified locally against mongo:latest (mongod 8.2.5)

  • 14-contract conformance harness — pass.
  • _store_implements duck-type probe — pass.
  • Constructor name validation (positional + dataclass) — pass.
  • create_schema() idempotence — pass.
  • options= constructor path equivalence — pass.
  • Subpath-targeted delete() does NOT drop the summary sidecar — pass.
  • Concurrent appends carrying disjoint summary fields both survive — pass (adversarially verified: 29/30 trials clobber without the lock).
  • End-to-end TranscriptMirrorBatcher → MongoDB → materialize_resume_session round-trip — pass.

Total: 8 passed in 0.89s.

Test plan

  • cd examples/session_stores && cat mongodb_session_store.py for review
  • pip install -e '.[dev,examples]'
  • ruff check src/ tests/ — clean
  • ruff format --check src/ tests/ — clean
  • mypy src/ — clean
  • pytest tests/test_example_mongodb_session_store.py — skips cleanly without env
  • docker run -d -p 27017:27017 mongo:latest then SESSION_STORE_MONGODB_URL=mongodb://localhost:27017 pytest tests/test_example_mongodb_session_store.py -v — 8 pass
  • pytest tests/test_example_redis_session_store.py tests/test_example_s3_session_store.py — no regressions in sibling adapters

alexbevi and others added 8 commits June 1, 2026 17:08
Adds a fourth reference SessionStore adapter under
examples/session_stores/mongodb_session_store.py, alongside the existing
S3, Redis, and Postgres adapters. Mirrors the Postgres adapter shape:
one document per JSONL entry with server-assigned ObjectId ordering,
plus a separate summaries collection maintained inside append() via
fold_session_summary.

Uses pymongo's stable async API (AsyncMongoClient, introduced in
pymongo 4.13). Implements all five SessionStore methods plus
list_session_summaries; serializes the read-fold-write summary update
with a per-session asyncio Lock to satisfy the protocol's concurrency
requirement.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Required by examples/session_stores/mongodb_session_store.py. Pinned
to >=4.13 (the first stable release of pymongo's async API,
AsyncMongoClient). The corresponding test module importorskips on
pymongo so default CI is unaffected when [examples] isn't installed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds tests/test_example_mongodb_session_store.py mirroring the Postgres
test module: live-only via SESSION_STORE_MONGODB_URL, full 14-contract
conformance harness, constructor validation, and an end-to-end
TranscriptMirrorBatcher → MongoDB → materialize_resume_session
round-trip.

Each run uses a random database name and drops it on teardown.

Verified locally against `docker run -d -p 27017:27017 mongo:latest`:
4 passed (conformance + duck-type probe + name validation + round-trip).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a MongoDB section to examples/session_stores/README.md mirroring
the S3/Redis/Postgres sections (Installation, Usage, Schema,
Concurrency, Retention, Resume, Live e2e, TS cross-reference) plus a
MongoDB entry under the umbrella production checklist (pool sizing,
multi-writer note, retention).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Calls out the divergence from the S3/Redis/Postgres adapters: this one
implements the optional list_session_summaries method, which lets
list_sessions_from_store() take its fast path (one batch read +
gap-fill) instead of N per-session load() calls. Also flags the
fold_session_summary import as a deliberate public-for-adapters use of
the SDK's _internal package, with a localized comment so a future
relocation is a single-line fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a TestAdapterSpecific class with four tests for behaviors the
shared conformance harness cannot probe:

- create_schema is idempotent (matches the Postgres adapter's extra
  test).
- The dataclass options= constructor path is equivalent to positional
  args.
- A targeted subpath delete leaves the main session's summary sidecar
  intact; only main delete cascades.
- Concurrent appends carrying disjoint summary fields (customTitle vs
  gitBranch) must both survive — the per-session asyncio.Lock keeps
  the read-fold-write atomic across appends. Adversarially verified:
  removing the lock causes 29/30 trials to clobber one field.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Anticipates the contributing guide proposed in PR anthropics#1005 by:

- Running ruff format on the adapter module and test file (previously
  passed `ruff check` but not `ruff format --check`).
- Fixing two ruff B023 findings in the concurrency test, where inner
  closures captured the loop's `key` variable. Bound via default arg
  so each iteration gets its own value — same fix the linter
  recommends, prevents a real future bug if the loop body grows.

After this change the package now passes the full set of checks the
guide enumerates: `ruff check src/ tests/`, `ruff format --check src/
tests/`, and `mypy src/`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@alexbevi
Copy link
Copy Markdown
Author

alexbevi commented Jun 2, 2026

Note this is a follow up to anthropics/claude-agent-sdk-typescript#341

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant