Session Persistence

Lesson 9 · Strands Agents (TypeScript) · ~12 minutes

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.

Why Sessions Matter

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:

SessionManager with FileStorage

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.

SessionManager with S3Storage

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.

What Gets Persisted

A session snapshot captures everything the agent needs to resume:

ComponentPersistedWhy
messagesFull conversation historyThe agent needs context of prior turns to continue coherently
appStateAll key-value pairsCustom state (user preferences, accumulated data, counters) survives restarts
systemPromptCurrent system promptAllows dynamic prompt evolution across sessions
conversationManagerManager state (summaries, window position)Compression state resumes correctly — no re-summarization
What doesn't get persisted

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.

Immutable Snapshots

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).

Multi-Agent Session Management

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.

Don't double-persist

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.

Custom Storage Backends

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'),
})

Production Pattern: Multi-Turn Lambda Assistant

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.

Key Takeaways

Your multi-agent Graph has a researcher and writer. Where should the SessionManager be attached?
Correct. The orchestrator (Graph) manages the full conversation flow — including what each inner agent produced. One session manager captures everything. Putting it on inner agents would cause duplicate storage and conflicting state on restore.
In multi-agent systems, only the outermost orchestrator gets a session manager. The Graph captures the full execution (researcher output → writer output) in its own conversation state. Inner agents contribute to the orchestrator's state, not their own independent sessions.
A Lambda function handles a request but the agent's tools and plugins aren't in the session snapshot. How does the agent have them on a resumed session?
Correct. Sessions store data (messages, appState), not code (tools, plugins). Your agent constructor always provides the full tool and plugin set. When a session is restored, the data is loaded and the code (tools, plugins) is already there from construction. This separation means your tools can evolve between sessions without migration.
Sessions persist data, not code. Tools and plugins are JavaScript functions — they exist in your deployed code, provided at agent construction. The session restores the conversation state (messages, appState), and the tools are always there because the agent constructor declares them. This is by design: it allows tool code to evolve between sessions.
📖 Primary Source

Session Management — Strands Agents Docs — Full API reference for SessionManager, storage backends, snapshots, and multi-agent session patterns. ~10 min read.

💬 Questions? Ask me about session lifecycle management, storage backend selection, snapshot strategies, multi-tenant isolation, or handling session migration when your agent's schema evolves.
← Back Next →