How to: integrate with AWS Strands Agents

The @neo4j-labs/agent-memory/integrations/strands subpath plugs into all three of Strands' orthogonal extension surfaces:

  • SnapshotStorage — session state persists to a NAMS conversation automatically. Real conversation messages land as Message graph nodes; non-message snapshot state (Strands' data minus messages, plus appData, plus manifests) rides on synthetic role: "user" marker messages whose content is strands_state:{base64-JSON}.

  • ConversationManager — three-tier context (reflections + observations from getContext()) is prepended to every model invocation, on top of a configurable inner manager (defaults to SlidingWindowConversationManager).

  • HookRegistry eventsBeforeInvocation, AfterInvocation, BeforeToolCall, and AfterToolCall are wired to the reasoning subclient so every agent turn becomes a queryable ReasoningStep with tool calls attached.

A runnable example lives at clients/typescript/examples/strands.

When to use this

You want any of:

  • Agent state that survives process restarts AND is queryable as a graph

  • Reflections/observations injected into the model context automatically

  • A full reasoning trace of every agent turn, browseable later via client.reasoning.getTraceByConversation(id)

  • Cross-agent memory sharing through the same conversation

One-line wiring with connectMemoryToAgent

import { Agent } from "@strands-agents/sdk";
import { OpenAIModel } from "@strands-agents/sdk/models/openai";
import { MemoryClient } from "@neo4j-labs/agent-memory";
import { connectMemoryToAgent } from "@neo4j-labs/agent-memory/integrations/strands";

const memory = new MemoryClient();
const conv = await memory.shortTerm.createConversation({ userId: "alice" });

const agent = new Agent({
  ...await connectMemoryToAgent(memory, { conversationId: conv.id }),
  model: new OpenAIModel({ modelId: "gpt-4o-mini" }),
  tools: [/* ... */],
});

await agent.invoke("Tell me about graph databases.");

The factory returns { sessionManager, conversationManager } — spread it into new Agent({…​}). Reasoning hooks attach automatically when the conversation manager’s initAgent runs.

Individual pieces (advanced)

For finer control:

import {
  SessionManager,
  SlidingWindowConversationManager,
} from "@strands-agents/sdk";
import {
  Neo4jSessionStorage,
  Neo4jConversationManager,
  registerReasoningHooks,
} from "@neo4j-labs/agent-memory/integrations/strands";

const sessionManager = new SessionManager({
  sessionId: conv.id,
  storage: { snapshot: new Neo4jSessionStorage(memory) },
});

const conversationManager = new Neo4jConversationManager(memory, {
  conversationId: conv.id,
  inner: new SlidingWindowConversationManager(),  // or any custom CM
  includeReflections: true,
  includeObservations: true,
});

const agent = new Agent({ sessionManager, conversationManager, model, tools });

// If you skipped connectMemoryToAgent, attach reasoning hooks explicitly:
await registerReasoningHooks(memory, agent, { conversationId: conv.id });

What gets stored where

Strands construct Where it lands in NAMS

Conversation messages

Message nodes attached to the Conversation

Agent state (Snapshot.data, non-message fields)

synthetic role: "user" message with content strands_state:{base64-JSON-blob}

Application state (Snapshot.appData)

inside the same synthetic message’s content blob

Tool calls

ToolCall nodes attached to a ReasoningStep

Per-turn reasoning

ReasoningStep nodes attached to the Conversation

Manifests (snapshot metadata)

synthetic role: "user" message with content strands_manifest:{base64-JSON-blob}

NAMS exposes no endpoint to update a Conversation’s metadata after creation, and we observed that per-message metadata doesn’t reliably round-trip via GET /conversations/{id}/messages either. So the integration stuffs the serialized blob directly into the message’s content field, base64-encoded after a recognizable prefix. Each saveSnapshot writes one synthetic message; listSnapshotIds walks the message list, filters by the strands_state: content prefix, and dedupes by snapshotId (last-write-wins for the isLatest flag).

Synthetic markers are filtered out before the Snapshot is handed back to Strands on loadSnapshot, so the agent never sees them in its prompt. Consumers walking the raw message list themselves (chat UIs, Cypher queries, custom analytics) should filter on the prefixes too — the integration exports isSyntheticStrandsMessage and SYNTHETIC_MESSAGE_PREFIXES for convenience:

import { isSyntheticStrandsMessage } from "@neo4j-labs/agent-memory/integrations/strands";

const conv = await memory.shortTerm.getConversation(conversationId);
const realMessages = conv.messages.filter((m) => !isSyntheticStrandsMessage(m));

The chat UI in demo/agents/spool/ doesn’t fetch raw conversation messages (it streams from agent.invoke directly) so it doesn’t need to filter; consumers that DO fetch raw messages must.

Behaviour details

ConversationManager layering

Neo4jConversationManager does NOT replace your inner manager’s trimming or summarization logic — it layers context injection on top of it. So:

  • Sliding-window trimming still happens (if you use SlidingWindowConversationManager)

  • Summarization still happens (if you use SummarizingConversationManager)

  • If getContext() fails (cold conversation, transient error), the inner manager’s output is used unchanged

Reasoning capture is best-effort

Every reasoning write (recordStep, recordToolCall) is wrapped in try/catch. A failed reasoning write logs through the constructor logger but never breaks the agent run. If you depend on the trace being complete, check getTraceByConversation(id) after the run and replay missing steps yourself.

Auth errors propagate

SnapshotStorage failures are not best-effort. If the integration can’t read or write the snapshot blob, Strands' own retry semantics kick in. An auth failure surfaces as AuthenticationError from saveSnapshot / loadSnapshot.

Version compatibility

We test against @strands-agents/sdk@^1.2.0. Strands' v1 API is stable under semver — the integration should track v1 minor releases without adjustment. We will not break the integration API across minor releases of @neo4j-labs/agent-memory without a CHANGELOG callout.