AWS Database Blog
Introducing strands-dynamodb-storage: Durable agent storage for the Strands Agents SDK
Today, we are announcing strands-dynamodb-storage, an open source Amazon DynamoDB storage backend for the Strands Agents SDK, available now for both Python (on PyPI) and TypeScript (on npm) under the Apache 2.0 license. Artificial intelligence (AI) agents are moving from single conversations into long-running relationships with the people they serve. Everything an agent must remember across those conversations has to live somewhere durable: session state, long-term memories, and transcripts.
In this post, we introduce strands-dynamodb-storage, wire it into a Strands agent, and give that agent long-term memory it can search by meaning, all from one DynamoDB table in your own account.
A quick look at Amazon DynamoDB
Amazon DynamoDB is a serverless, fully managed, distributed NoSQL database with single-digit millisecond performance at any scale. Because DynamoDB is built for low latency and high availability, it is often used to store session data, user profiles, metadata, or application state. Those same qualities make it a natural fit for agentic workloads. Agents are bursty: a fleet of sessions can go from idle to thousands of concurrent invocations in seconds. DynamoDB on-demand capacity absorbs that without pre-provisioning, and costs nothing when the agents are idle.
Agents increasingly run on ephemeral compute such as AWS Lambda and Amazon Bedrock AgentCore runtime, a capability of Amazon Bedrock AgentCore, where anything that must survive teardown has to leave the process. The DynamoDB HTTP data plane needs no connection pools to warm or exhaust. Because an agent loads its state before every model call, storage sits on the hot path of every turn. Single-digit millisecond point reads make sure storage is never the reason your agent feels slow.
Strands Agents
Strands Agents is an open source SDK that takes a model-driven approach to building AI agents in a few lines of code, with first-class support for Amazon Bedrock and other model providers. Among its building blocks is a unified Storage interface: a small byte-oriented contract (write, read, delete, list) that every stateful subsystem in the SDK speaks.
The Session Manager persists conversation snapshots through it, the Memory Manager stores long-term memories through it, and the context offloader and transcripts use the same four operations. A single implementation of that contract gives every one of those subsystems durability at the same time.
Introducing strands-dynamodb-storage
strands-dynamodb-storage implements the Strands Storage contract on a single DynamoDB table. The SDK’s /-separated keys map directly onto the DynamoDB key model. A key’s scope and ID (its first two segments) become the partition key, and the remainder becomes the sort key. Point operations are single-item PutItem, GetItem, and DeleteItem calls. Listing a prefix is a native partition Query with begins_with, never a table scan.
The following table shows how storage keys map onto the table’s key schema.
| Storage key | Written by | Partition key (pk) | Sort key (sk) |
session/user-42/snapshot.json |
SDK Session Manager | session/user-42 | snapshot.json |
| session/user-42/notes | Your own write() call | session/user-42 | notes |
| user/u1/memories/m1 | Your own write() call | user/u1 | memories/m1 |
On top of the byte contract, the package adds the following capabilities:
- S3 offload for large values. Offload is opt-in: pass a bucket name on the constructor. With a bucket configured, values above the 400 KB DynamoDB item size limit are transparently offloaded to Amazon Simple Storage Service (Amazon S3). A small pointer item remains in the table, so callers see one contract regardless of payload size. Without a bucket, a write above the limit fails with a validation error, and the snapshot of a long conversation can reach that size.
- Optional gzip compression. Applied before the offload size check, so compressible values stay inline at lower cost. Each item records whether it was compressed, which makes sure reads are correct regardless of the setting.
- Optional time to live (TTL). Writes stamp a DynamoDB-native expiry attribute, and reads and listings filter items whose expiry has already passed, covering the window before DynamoDB physically removes them.
- Multi-tenant prefixes. A constructor-bound key prefix pins every operation inside its own key space, so two tenants sharing a table resolve the same logical key to physically distinct partitions.
The package holds no CreateTable permission and never creates infrastructure. You create the table in advance using the console, the AWS Command Line Interface (AWS CLI), an SDK, or the infrastructure as code of your choice. You apply your own tagging, backup, and encryption settings.
At runtime it issues four DynamoDB operations, plus SearchVectors when you use semantic search and three S3 operations when you configure offload. The least-privilege IAM policy is therefore short. The README carries the full provisioning walkthrough and the complete policy.
Prerequisites
To follow along with this post, you must have the following prerequisites:
- An AWS account with permissions to create an Amazon DynamoDB table and, if you configure large-value offload, an Amazon S3 bucket.
- The AWS CLI configured with credentials, to run the table creation command in this post.
- Python 3.10 or later with strands-agents 1.48.0 and boto3 1.43.64 or later, or
Node.js20 or later with @strands-agents/sdk 1.10.0 and @aws-sdk/client-dynamodb 3.1103.0 or later. These floors are where the Storage contract and the SearchVectors API respectively landed. - AWS Identity and Access Management (IAM) permissions for the DynamoDB operations the package issues (the README carries the complete least-privilege policy).
- For the semantic memory sections: model access to Amazon Titan Text Embeddings V2, granted on the Model access page of the Amazon Bedrock console, and a table created with a vector index in a Region where DynamoDB vector indexes are available.
Getting started
A table with a string partition key pk and a string sort key sk is the only prerequisite. The following AWS CLI command creates it:
If you plan to follow the semantic memory section, declare the vector index in this same command: an index’s name, dimensions, and distance function cannot be changed after creation. The README’s provisioning section shows the full call.
Install the package (Python 3.10+ shown here. The TypeScript package is a feature-parity mirror):
Then hand it to your agent’s session manager. The SDK namespaces the keys, snapshots the conversation on every invocation, and restores it when the same session returns:
You can also set storage once on the Agent itself. Every subsystem that accepts a Storage then inherits it, each namespaced under its own key prefix, so one table carries the whole agent’s state:
You can also use the byte contract directly. The contract is async: inside an agent the SDK drives it for you, and in a plain script you wrap the calls in asyncio.run:
Semantic long-term memory with DynamoDB vector indexes
Session persistence solves half of the memory problem: your agent survives a restart and resumes the conversation. The harder half is recalling something a user told the agent weeks ago, in a new conversation that shares no keys with the old one. That requires searching memories by meaning rather than by key. DynamoDB vector indexes bring nearest-neighbor search to the same table that holds your agent’s state, so there is no separate vector database to provision, no pipeline copying data into it, and no reconciliation job to explain when the two disagree.
The index’s name, dimensions, and distance function are fixed at creation, so size the dimensions to your embedding model. The README’s provisioning section shows the create-table call that declares the index.
The examples that follow embed text with Amazon Titan Text Embeddings V2 through Amazon Bedrock. The model returns 1,024-dimension vectors by default, so the index for these examples is created with 1,024 dimensions. The following code defines the embed() function the rest of the post uses:
Writing a memory attaches an embedding and optional metadata alongside the bytes. The following code constructs the store with a per-tenant prefix, so the key memories/m1 lands in the physical partition user/u1:
Recalling by meaning is one call, scoped to the same partition:
Notice the pk argument. The vector index is partitioned the same way the table is, and every search is scoped to one partition. A tenant’s search never ranges over another tenant’s memories, and the work each search performs tracks the size of that tenant’s memory rather than the whole table. Keep in mind that the partition value is supplied by the caller, so this is query scoping rather than an authorization boundary. A principal holding dynamodb:SearchVectors on the table can search any partition, so tenant access control belongs in AWS IAM and your application layer.
Results return most similar first. The raw score’s direction follows the index’s distance function: lower is nearer for cosine and Euclidean distance, and higher is more similar for dot product.
Wiring memory into a Strands agent
So far you have called search() yourself. In a real agent you want retrieved memories to reach the model automatically, and the SDK’s Memory Manager handles that. It retrieves relevant entries before each model call and folds them into the model input, and it registers a search_memory tool the model can call on demand. The Memory Manager accepts any object implementing the SDK’s MemoryStore protocol. The package does not ship one, so you define a small class in your own application that embeds on write, embeds on search, and returns MemoryEntry values. The following code defines the store:
The following code seeds three memories and wires the store into the agent, so retrieved memories reach the model with no orchestration code on your side:
Running this against a live table, the agent called its search_memory tool, matched the Tokyo trip and the window-seat preference from DynamoDB, and recommended a window seat for the flight. Passing add_tool_config=True also registers an add_memory tool, so the model can store new facts through the same table it recalls from.
Pricing
strands-dynamodb-storage is open source and provided at no additional charge. You pay for the AWS resources it uses in your account. For the base table, standard DynamoDB pricing applies to reads, writes, and storage. A vector index bills in its own units: vector search units meter SearchVectors calls, and vector write units meter the writes replicated into the index. Both are metered on bytes processed and grow with the dimensions you choose at index creation. The index’s storage is billed separately from the base table’s storage.
Generating embeddings is a separate cost: each memory write and each search invokes your embedding model, billed at that model’s rates. When you configure offload, standard S3 request and storage pricing applies.
Considerations
This is a v0.1 release, suited for development, testing, and experimentation while the interfaces settle. Keep in mind the following:
- The vector index’s name, dimensions, and distance function are immutable after creation, and a newly created index backfills before it is searchable. A table supports up to five vector indexes, so adopting a different configuration later means adding an index, not rebuilding the table.
- Vector indexes are eventually consistent, the same model as a global secondary index. A memory written moments ago might take a short time to become searchable.
- TTL expiry filtering applies to read and list. Because TTL deletion is asynchronous,
search()can briefly return items whose expiry has passed but which DynamoDB has not yet physically removed. - Listing requires a prefix that covers at least a full scope and ID. Broad listings such as an empty prefix are rejected. The package never falls back to a table scan. The SDK’s subsystems always pass fully namespaced prefixes and are unaffected.
- If you enable TTL on offloaded values, add an S3 lifecycle rule: DynamoDB removes the expired pointer item, and the lifecycle rule is what reclaims the S3 object.
Clean up
To avoid incurring ongoing charges, delete the resources you created for this post. Deleting the table also removes its vector indexes:
If you configured offload, empty and delete the S3 bucket. Model access in Amazon Bedrock has no charge on its own, so there is nothing to remove there.
Conclusion
In this post, we introduced strands-dynamodb-storage and showed how one DynamoDB table can carry a Strands agent’s session state, oversized tool results, and long-term memories. A vector index on that same table gives the agent recall by meaning. The integration is a table, a pip or npm install, and a constructor argument, and everything the package touches lives in your own account under your own controls.
strands-dynamodb-storage is developed in the open at github.com/aws/strands-dynamodb-storage, shipping both language implementations from one code base with unit suites and live integration tests that run against real DynamoDB and S3. Explore the repository to get started, and issues and pull requests are welcome.