An in-memory agent dies when the process ends. For production agents — ones that serve multiple users, survive Lambda cold starts, or resume multi-turn conversations days later — you need session persistence. Strands provides a SessionManager that snapshots the full agent state to pluggable storage backends.
Without persistence, every agent invocation starts fresh. The user says "continue where we left off" and the agent has no idea what they're talking about. Session persistence solves three production problems:
For local development, use FileStorage — sessions persist to disk as JSON files:
import { Agent, SessionManager, FileStorage } from '@strands-agents/sdk'
const storage = new FileStorage({
directory: './sessions', // Writes to ./sessions/.json
})
const sessionManager = new SessionManager({ storage })
const agent = new Agent({
name: 'assistant',
model: 'us.anthropic.claude-sonnet-4-6-20250725-v1:0',
systemPrompt: 'You are a helpful project assistant.',
sessionManager,
})
// First conversation — creates a new session
const session = await sessionManager.create()
await agent.invoke('My project is called Atlas. We use TypeScript.', {
sessionId: session.id,
})
// Later — resume the same session
await agent.invoke('What language does Atlas use?', {
sessionId: session.id,
})
// Agent remembers: "TypeScript" — it loaded the session from disk
FileStorage is simple and debuggable — you can open the JSON file and inspect the conversation. But it's local-only, not suitable for distributed systems.
For production, use S3Storage — sessions persist to an S3 bucket, accessible from any compute instance:
import { Agent, SessionManager, S3Storage } from '@strands-agents/sdk'
const storage = new S3Storage({
bucket: 'my-agent-sessions',
prefix: 'v1/', // Optional: organize by version
region: 'us-east-1',
})
const sessionManager = new SessionManager({ storage })
const agent = new Agent({
name: 'production-assistant',
model: 'us.anthropic.claude-sonnet-4-6-20250725-v1:0',
systemPrompt: 'You are a customer support agent.',
sessionManager,
})
// In a Lambda handler:
export async function handler(event: APIGatewayEvent) {
const sessionId = event.headers['x-session-id']
const result = await agent.invoke(event.body, { sessionId })
return {
statusCode: 200,
body: JSON.stringify({ response: result.message?.content }),
}
}
S3Storage handles concurrent access, works across Lambda invocations, and scales to millions of sessions. The session is loaded at the start of each invocation and saved at the end — atomic per turn.
A session snapshot captures everything the agent needs to resume:
| Component | Persisted | Why |
|---|---|---|
messages | Full conversation history | The agent needs context of prior turns to continue coherently |
appState | All key-value pairs | Custom state (user preferences, accumulated data, counters) survives restarts |
systemPrompt | Current system prompt | Allows dynamic prompt evolution across sessions |
conversationManager | Manager state (summaries, window position) | Compression state resumes correctly — no re-summarization |
Tools and plugins are not serialized — they're re-attached when the agent is constructed. The session stores data, not code. This means your agent constructor must always provide the same tools and plugins, regardless of whether it's a fresh session or a resumed one.
Every save creates an immutable snapshot with a UUID v7 ID (time-ordered). This gives you time-travel — restore a session to any previous point:
// List snapshots for a session
const snapshots = await sessionManager.listSnapshots(sessionId)
// Returns: [{ id: '019...' , createdAt: '2026-07-03T...' }, ...]
// Restore to a specific snapshot (undo the last 3 turns)
await sessionManager.restore(sessionId, snapshots[2].id)
// The agent now sees the conversation as it was at snapshot 2
await agent.invoke('Let me try a different approach...', { sessionId })
UUID v7 ordering means snapshots sort chronologically by default. You can also set a snapshotTrigger callback to control when snapshots are created:
const sessionManager = new SessionManager({
storage,
snapshotTrigger: (event) => {
// Only snapshot after tool calls (not after every turn)
return event.type === 'afterToolCall'
},
})
This reduces storage costs for chatty conversations while still capturing state after meaningful actions (tool executions).
In a multi-agent system (Graph, Swarm, Workflow), only the orchestrator gets a session manager. Inner agents don't persist independently — their state flows through the orchestrator's conversation:
import { Agent, SessionManager, S3Storage, Graph } from '@strands-agents/sdk'
const researcher = new Agent({
name: 'researcher',
model: 'us.anthropic.claude-sonnet-4-6-20250725-v1:0',
systemPrompt: 'Research topics thoroughly.',
// No sessionManager — inner agent
})
const writer = new Agent({
name: 'writer',
model: 'us.anthropic.claude-sonnet-4-6-20250725-v1:0',
systemPrompt: 'Write clear articles from research.',
// No sessionManager — inner agent
})
const graph = new Graph({
nodes: [researcher, writer],
edges: [['researcher', 'writer']],
sessionManager: new SessionManager({ // Orchestrator persists
storage: new S3Storage({ bucket: 'agent-sessions' }),
}),
})
The orchestrator's session captures the full multi-agent conversation — including what each inner agent produced. When you restore the session, the orchestrator reconstructs the conversation context that includes all inner agent outputs.
If both the orchestrator and inner agents have session managers, you'll get duplicate storage, conflicting state, and confusing restore behavior. One session manager per system — always on the outermost agent or orchestrator.
Implement the SnapshotStorage interface to use any storage system — DynamoDB, Redis, PostgreSQL, or your own service:
import { SnapshotStorage, Snapshot } from '@strands-agents/sdk'
class DynamoDBStorage implements SnapshotStorage {
private tableName: string
private client: DynamoDBDocumentClient
constructor(tableName: string) {
this.tableName = tableName
this.client = DynamoDBDocumentClient.from(new DynamoDBClient({}))
}
async save(sessionId: string, snapshot: Snapshot): Promise {
await this.client.send(new PutCommand({
TableName: this.tableName,
Item: {
pk: sessionId,
sk: snapshot.id, // UUID v7 — sorts chronologically
data: JSON.stringify(snapshot),
ttl: Math.floor(Date.now() / 1000) + 86400 * 30, // 30-day TTL
},
}))
}
async load(sessionId: string): Promise {
const result = await this.client.send(new QueryCommand({
TableName: this.tableName,
KeyConditionExpression: 'pk = :pk',
ExpressionAttributeValues: { ':pk': sessionId },
ScanIndexForward: false, // Latest first
Limit: 1,
}))
if (!result.Items?.length) return null
return JSON.parse(result.Items[0].data)
}
async listSnapshots(sessionId: string): Promise {
const result = await this.client.send(new QueryCommand({
TableName: this.tableName,
KeyConditionExpression: 'pk = :pk',
ExpressionAttributeValues: { ':pk': sessionId },
}))
return result.Items?.map(item => JSON.parse(item.data)) ?? []
}
}
// Use it
const sessionManager = new SessionManager({
storage: new DynamoDBStorage('agent-sessions'),
})
Complete pattern for a multi-turn assistant deployed to Lambda with S3 session persistence:
import { Agent, SessionManager, S3Storage, SummarizingConversationManager } from '@strands-agents/sdk'
import { APIGatewayEvent, APIGatewayProxyResult } from 'aws-lambda'
// Agent is constructed once (reused across warm invocations)
const agent = new Agent({
name: 'support-assistant',
model: 'us.anthropic.claude-sonnet-4-6-20250725-v1:0',
systemPrompt: `You are a customer support assistant for Acme Corp.
Help users with account issues, billing questions, and product guidance.
Always verify the user's identity before discussing account details.`,
tools: [lookupAccount, checkBilling, createTicket],
sessionManager: new SessionManager({
storage: new S3Storage({
bucket: process.env.SESSION_BUCKET!,
prefix: 'support/v2/',
}),
}),
conversationManager: new SummarizingConversationManager({
preserveRecentMessages: 10,
proactiveCompression: { enabled: true, compressionThreshold: 0.7 },
}),
limits: { turns: 5 }, // Safety: max 5 turns per invocation
})
export async function handler(event: APIGatewayEvent): Promise {
const body = JSON.parse(event.body ?? '{}')
const sessionId = body.sessionId ?? crypto.randomUUID()
const userMessage = body.message
if (!userMessage) {
return { statusCode: 400, body: JSON.stringify({ error: 'No message provided' }) }
}
try {
const result = await agent.invoke(userMessage, { sessionId })
return {
statusCode: 200,
body: JSON.stringify({
sessionId,
response: result.message?.content,
turnCount: result.turns,
}),
}
} catch (error) {
return {
statusCode: 500,
body: JSON.stringify({ error: 'Agent failed', sessionId }),
}
}
}
This pattern handles: (1) New conversations — sessionId is generated. (2) Continued conversations — sessionId from the client resumes where it left off. (3) Cold starts — the session loads from S3 regardless of which Lambda instance handles the request. (4) Long conversations — the SummarizingConversationManager keeps context under control.
SnapshotStorage interface — DynamoDB, Redis, PostgreSQL, anything.Session Management — Strands Agents Docs — Full API reference for SessionManager, storage backends, snapshots, and multi-agent session patterns. ~10 min read.