feat(strands): add Neo4jSessionManager — push-based conversation persistence for Strands agents#136
Conversation
Approved brainstorming output: Neo4jSessionManager/Neo4jSessionRepository backed by MemoryClient (bolt + NAMS), opt-in long-term retrieval injection, pure Session-to-Conversation mapping with no Strands-specific node types. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…hook registration, list formatting) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop the Neo4jSessionRepository layer: with no agent-state persistence and a write-behind buffer, the SessionRepository CRUD contract could not be honored honestly. Neo4jSessionManager now subclasses the SessionManager ABC directly; mapping helpers and the async bridge stay private. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Verified Strands API surface (strands-agents installed):
- Hook imports resolve from strands.hooks directly:
from strands.hooks import HookRegistry, MessageAddedEvent, AfterInvocationEvent
- MessageAddedEvent IS wired to append_message (and also sync_agent) in base SessionManager.register_hooks
- AfterInvocationEvent calls sync_agent only
- Full register_hooks also covers MultiAgent/BidiAgent lifecycle events
Creates the module skeleton at:
src/neo4j_agent_memory/integrations/strands/session_manager.py
with Neo4jRetrievalConfig dataclass (all defaults pass the unit test).
Unused scaffolding imports (SessionManager, SessionException, MemoryClient, MemorySettings,
_SESSION_KEY) retained with noqa: F401 / TYPE_CHECKING guards per plan; ruff clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extend the try/except import guard to cover the hooks surface (AfterInvocationEvent, HookRegistry, MessageAddedEvent) so a missing or incompatible strands-agents install fails fast at import with the install hint. Also adds StrandsMessage to the TYPE_CHECKING block and adds a reason string to pytest.importorskip. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… task noise Extend _AsyncBridge.close() docstring to explain in-flight-call blocking and the drain-before-close guarantee. Cancel pending asyncio tasks in test_timeout_raises before stopping the loop to prevent the spurious "Task was destroyed but it is pending!" stderr line. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Appends five pure functions (_message_text, _to_strands_message, _format_entity, _format_preference, _format_fact) to session_manager.py and creates tests/unit/integrations/strands_fakes.py with stateful FakeMemoryClient/FakeShortTerm/FakeLongTerm/FakeReasoning for use by future task tests. All 10 unit tests pass; ruff clean. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds Neo4jSessionManager (Strands SessionManager ABC subclass) with constructor validation, async bridge wiring, and initialize() that restores conversation history from bolt or NAMS backends. NAMS path scans conversation metadata to find-or-create by strands_session_id. Also hardens strands_fakes: NAMS get_conversation raises NamMemoryError on missing key, add_message raises NamMemoryError for unknown NAMS conversations, delete_message NAMS branch adds workaround kwarg. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…, close Replaces NotImplementedError stubs with the one-slot write-behind buffer machinery: append_message deep-copies into the buffer and flushes the previous slot; _flush_buffer persists text turns and raises SessionException on failure; sync_agent is a strict no-op; close() flushes then conditionally closes only the client we connected, then stops the bridge. Tool-call mirroring to reasoning memory is opt-in (record_tool_calls=True) and warns-only on failure. Adds cast() fix for _to_strands_message Literal type narrowing. 28 tests pass (12 new + 1 un-xfailed seeding test + 15 prior).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ge gaps - Clear `_last_persisted` to None before re-raising in the bolt late-redaction error path so a stale message id is never reused after a failed delete. - Assert `extract_entities=False` on the rewrite call in `test_late_redaction_on_bolt_deletes_and_rewrites`. - Add `test_redact_with_nothing_stored_warns_and_returns` to cover the no-op path when neither buffer nor persisted message is present. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ction slots Override `register_hooks` to call super() first (base wires initialize, append_message, sync_agent), then register our own `AfterInvocationEvent` callback that flushes the write-behind buffer. When a `Neo4jRetrievalConfig` is present, also register a `MessageAddedEvent` callback that will call `_inject_context` (stub for Task 9). Remove all stale `# noqa: F401` comments from the import block now that every name is genuinely referenced. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implements _inject_context and _aretrieve on Neo4jSessionManager so that user messages are prepended in-memory with relevant entities, preferences, and facts from long-term storage when retrieval_config is set. Individual search failures degrade gracefully via asyncio.gather(return_exceptions=True); bridge-level failures log a WARNING and leave the turn intact. Also fixes the register_hooks docstring to accurately describe AfterInvocationEvent callback ordering. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ency guard Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds Neo4jSessionManager and Neo4jRetrievalConfig to the public strands integration __init__.py. Also strengthens the _inject_context idempotency guard to use the full library-controlled header prefix rather than the bare tag, preventing false matches on user text. Reformats to ruff standards. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…fires Phase 1.5 directly seeds an ORGANIZATION entity and a compliance preference via MemoryClient, giving the demo something to retrieve. The <user_context> injection block now reliably appears in Phase 2 output, making the shared-brain headline feature visible without any LLM or extraction API key. README expected-output updated to match, including the new seeding print line. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…l injection; docs corrections NAMS raises NotSupportedError on search_preferences/search_facts, so the default retrieval config logged a WARNING on every user turn. Guard both conditions with `not self._client.is_nams` so those searches are never dispatched on the NAMS backend (entity search still works). Adds a unit test that hard-fails if either unsupported search is called on NAMS. Docs: add NOTE block in the Retrieval Injection section; fix the sync_agent Limitations clause (it is a no-op, not a flush trigger) in both the how-to and the design spec. CHANGELOG: change colon to em-dash to match sibling entry style. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… example session reset Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- pass user scoping + explicit limit to NAMS conversation resolution - accurate AfterInvocationEvent ordering docs (user hooks see unpersisted turn) - drop silently-swallowed **kwargs from constructor (typos now TypeError) - idempotency guard before retrieval (no wasted/contaminated re-search) - centralize backend capabilities (_BackendCapabilities) over per-site is_nams - _format_entity prefers Entity.full_type (matches get_context format) - split key resolution from history load; add restore_limit option - merge _owns_client/_we_connected into _should_close_client Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…idge to integrations.base, close docs-import validation hole Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- replace _BackendCapabilities dataclass+cache with one _is_nams property - dedupe the add_message call into _store_message (flush + redact share it) - single target-block lookup in _inject_context (was two identical loops) - table-driven _aretrieve (was three copy-pasted search blocks) - _ensure_session: one expression, drops the python -O-unsafe assert Behavior identical; 53 unit + 2 integration tests unchanged and green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
session_manager.py keeps only the SessionManager protocol implementation (lifecycle, hooks, write-behind buffer). Pure message mapping moves to _messages.py; Neo4jRetrievalConfig + search/formatting move to _retrieval.py (decoupled from session state — takes long_term + config). Mirrors the mcp package's _tools.py/_resources.py convention. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- hook callbacks as typed bound methods (HookCallback[TEvent] protocol matches param name 'event'; lambdas inferred as Unknown) - transport_mode typed as the nams TransportMode Literal in build_nams_settings and for_nams - rename module-level import-guard 'e' to stop shadowing inner handlers Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Stop appending the session manager as an afterthought: the intro now presents pull-based tools and push-based session manager as the two modes of one integration, the Quick Start shows them combined on one Agent, and the page is organized into two co-equal sections with the cross-cutting topics (multi-user, shared-brain) spanning both. No new content claims; existing snippets preserved verbatim where unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@Andy2003 is attempting to deploy a commit to the lyonwj's projects Team on Vercel. A member of the Team first needs to authorize it. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
The Strands integration leaned on `Any`/`list[Any]` and untyped override signatures even though the framework supplies precise types — so editors and type checkers could not see what was being passed. Adopt the real types throughout: - session_manager: override signatures use Strands `Agent` / `Message` (matching the base class), tool blocks are `list[ToolUse]`, internal state is typed (`MemoryClient`, `Message | None`, `UUID | None`), and `__exit__` uses the proper context-manager types. The NAMS-only `_aresolve_conversation` branch localizes the bolt/NAMS short-term polymorphism behind one annotated boundary instead of an `Any` client. - AsyncBridge.run is generic (`Coroutine[Any, Any, T] -> T`), so every `self._bridge.run(...)` call propagates the awaited value's real type. - _messages / _retrieval: typed against the memory models (`Message`, `Entity`, `Preference`, `Fact`, `LongTermMemory`); the per-type formatters are precisely typed. - tools: `_run_async` is generic, factories return `AgentTool`, the public `*_context_tools` return `list[AgentTool]`, and `transport_mode` is the `TransportMode` literal (fixes a real arg-type mismatch into `build_nams_settings`). - __init__: `llm_provider_from_strands` returns `LLMProvider`. The few remaining `Any`s are legitimate framework boundaries (`**kwargs` forwarding, arbitrary-JSON tool inputs/results, a heterogeneous dispatch table). Test fakes updated to model a real `Entity` (which always has the `full_type` property). mypy clean on the integration; the pre-existing `run_sync` typing debt in integrations/base.py is left for a follow-up. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Running `ty` over the typed integration surfaced real issues that the previous `Any`-heavy signatures had hidden: - **Bug:** `context_graph_tools` built a bolt client via `Neo4jConfig(user=..., password=<str>)`, but the field is `username` and `password` is a `SecretStr`, and the model forbids extras — so the direct-Neo4j tool path would raise a ValidationError at runtime. Fixed to `username=` + `SecretStr(...)`. - The `**config` dict unpacking collapsed per-argument types to `str | None`; pass the factory arguments explicitly so the narrowed `str` values (uri/password) survive. - `_aresolve_key` / cast narrowing: resolve the conversation key through a local so it narrows to `str`; reinterpret the NAMS short-term via `object` for the deliberate cross-hierarchy cast to `ShortTermProtocol`. - `_run_async` wraps `asyncio.run` in a zero-arg lambda so the result type flows through `Executor.submit`. `ty` and `mypy` are now clean on `integrations/strands/`. The remaining `run_sync` errors in `integrations/base.py` need a ParamSpec signature change to a shared helper used by other integrations — deferred. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The cast through object is the checker-sanctioned form for a deliberate cross-type cast; clarify in the comment that it is a stopgap until MemoryClient.short_term is widened to ShortTermProtocol (deferred follow-up), not an oversight. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
The Strands integration here is |
There was a problem hiding this comment.
Pull request overview
This PR adds a push-based Strands Agents integration by introducing Neo4jSessionManager, enabling automatic conversation persistence/restore backed by neo4j-agent-memory (bolt or hosted NAMS) and optional per-turn long-term memory injection (Neo4jRetrievalConfig). It complements the existing pull-based Strands @tool integration and documents both modes together.
Changes:
- Add
Neo4jSessionManagerwith write-behind buffering (for redaction), lifecycle/hook wiring, optional tool-call mirroring, and a NAMS factory constructor. - Add retrieval/context injection (
_retrieval.py) and message mapping helpers (_messages.py), plus a shared persistent async↔sync bridge (AsyncBridge). - Add extensive unit/integration/example/doc tests and update Strands documentation and changelog.
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/integrations/test_strands_session_manager.py | Comprehensive unit coverage for buffering/redaction, hook ordering, NAMS semantics, injection idempotency, and bridge behavior. |
| tests/unit/integrations/strands_fakes.py | Stateful fake MemoryClient surfaces used by the session-manager unit tests. |
| tests/integration/test_strands_session_manager.py | Bolt-backed integration tests for persist/restore and redaction invariants. |
| tests/examples/test_strands_session_manager_example.py | Smoke tests ensuring the runnable example structure, imports, and key calls remain intact. |
| tests/docs/test_code_snippets.py | Ensures new Strands integration exports are referenced and importable when Strands is installed. |
| src/neo4j_agent_memory/integrations/strands/tools.py | Tighten typing and deduplicate NAMS settings/env resolution via shared helpers. |
| src/neo4j_agent_memory/integrations/strands/session_manager.py | New Neo4jSessionManager implementation: persistence, buffering/redaction, injection, and NAMS resolution logic. |
| src/neo4j_agent_memory/integrations/strands/config.py | Add shared NAMS connection/settings helpers used by both tools and session manager. |
| src/neo4j_agent_memory/integrations/strands/_retrieval.py | New retrieval config + concurrent long-term searches + context block formatting. |
| src/neo4j_agent_memory/integrations/strands/_messages.py | Pure Strands ↔ stored message mapping helpers. |
| src/neo4j_agent_memory/integrations/strands/init.py | Export session manager + retrieval config and tighten llm_provider_from_strands typing. |
| src/neo4j_agent_memory/integrations/base.py | Add AsyncBridge for persistent background event loop bridging. |
| examples/strands-session-manager/README.md | Document the shared-brain demo and how to use the session manager (bolt + NAMS). |
| examples/strands-session-manager/main.py | Runnable three-phase demo (persist, seed LTM, inject, restore) without API keys. |
| docs/superpowers/specs/2026-06-05-strands-session-manager-design.md | Design spec capturing architecture, semantics, and constraints (bolt vs NAMS). |
| docs/modules/ROOT/pages/how-to/integrations/aws-strands.adoc | Restructure docs to present tools + session manager as complementary modes; add quick starts and limitations. |
| CHANGELOG.md | Add release notes entry for the new Strands session manager capabilities. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
# Conflicts: # src/neo4j_agent_memory/integrations/base.py # src/neo4j_agent_memory/integrations/strands/tools.py
- AsyncBridge.close(): skip loop.close() when the join times out (closing a
still-running loop raises RuntimeError); always reset references so the
bridge stays reusable instead of half-closed.
- session_manager.register_hooks: correct the AfterInvocationEvent ordering
docstring — under reverse dispatch, user hooks registered after this manager
run BEFORE our flush (that is why they observe the un-persisted turn).
- strands_fakes.FakeShortTerm.add_message: drop extra kwargs in NAMS mode to
match the real backend (accepts only {content, role}), so NAMS-mode tests
can't lean on kwargs the server silently discards.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Post-merge ruff (from main's tooling bump) collapses a SimpleNamespace call onto one line. Format-only; no behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…loaded) Merging main brought in #136's integrations/strands/__init__.py, whose `cast("LLMProvider", from_provider(model_id))` became redundant now that Phase 5 made `from_provider` @overload-typed (kind="llm" -> LLMProvider). mypy [redundant-cast] and ty flagged it in the PR-merge type-check. Drop the cast; the _passthrough cast (narrows Any) stays. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
Adds
Neo4jSessionManager, a Strands AgentsSessionManagerbacked byMemoryClient(self-hosted bolt and hosted NAMS) — the same posture AWS Bedrock AgentCore takes with
AgentCoreMemorySessionManager. The existing Strands integration is pull-based (@toolfunctions the agent must decide to call); this adds the push-based half: every turn is
persisted automatically, history is restored on agent restart, and relevant long-term
memories can be injected into each user message.
Design spec:
docs/superpowers/specs/2026-06-05-strands-session-manager-design.mdWhat's included
Neo4jSessionManager(integrations/strands/session_manager.py) — directSessionManagerABC implementation: lifecycle, hook wiring, write-behind buffer_retrieval.py—Neo4jRetrievalConfig+ concurrent long-term search and<user_context>block formatting (opt-in per-turn injection)_messages.py— pure Strands ↔ Memory message mappingAsyncBridge(integrations/base.py) — shared persistent-event-loop bridge forsync code driving loop-bound async transports
integrations/strands/config.py(deduplicates three previously-identical blocks)
aws-strands.adoc: both modes presented as one integration, combinedQuick Start, constructor reference, documented limitations
examples/strands-session-manager/, no API keys needed)Key design decisions
Conversation; messages map toMessage. Persistence is memory-grade: text turns arestored (and feed entity extraction / the shared brain); tool-use blocks and
agent.stateare not round-tripped — that'sFileSessionManager's job.endpoint; guardrail redaction rewrites the buffer before the original ever reaches
the backend.
sync_agentis a strict no-op because the Strands base class also fires iton
MessageAddedEvent— flushing there would defeat the redaction window.original message is persisted before the in-memory copy is augmented; injection is
idempotent per message and degrades (warn + skip) on retrieval failures.
(preference/fact search, message delete) are skipped or warned once, not retried per turn.
Limitations (documented in the guide)
agent.stateand conversation-manager window state don't survive restarts; redactionafter a flush falls back to delete-and-re-add on bolt and warns on NAMS; one agent per
manager instance (Graph/Swarm persistence out of scope for v1); searches are
workspace-scoped (
user_idscopes writes).Test plan
tests/unit/integrations/test_strands_session_manager.py) incl.buffer/redaction privacy invariants, hook-ordering, NAMS-mode semantics — green ×2
redaction never stores the original)
make lint/ format clean; zero new mypy errors (253 baseline errors pre-exist onmain);
uvx ty checkclean on all branch filestests/docsfailures pre-exist on main:7 need npm build output, 3 are stale-data assertions in unrelated pages)