Conversation Management

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

Every agent conversation grows. Each turn adds user messages, assistant responses, and tool results. Eventually, the context window fills up — and then the model either errors out or starts hallucinating because it can't see its own earlier reasoning. Conversation managers solve this by intelligently reducing the message history before it becomes a problem.

The Problem: Context Windows Are Finite

A long-running agent accumulates messages quickly. A single tool call adds 3 messages (assistant tool_use, tool result, assistant response). After 20 turns with tools, you might have 60+ messages. Large tool results (database queries, file contents) can fill thousands of tokens each.

When the context window fills:

Conversation managers prevent all three by reducing the message history to fit within limits while preserving the information that matters most.

Three Built-in Managers

NullConversationManager

Does nothing. Messages accumulate without bound. This is the default — fine for short-lived agents with predictable workloads where you know context won't be exceeded:

import { Agent, NullConversationManager } from '@strands-agents/sdk'

const agent = new Agent({
  name: 'short-task',
  model: 'us.anthropic.claude-sonnet-4-6-20250725-v1:0',
  systemPrompt: 'Answer the question briefly.',
  conversationManager: new NullConversationManager(),
  // This is the default — equivalent to not setting it at all
})

SlidingWindowConversationManager

Keeps the N most recent messages and drops everything older. Simple, fast, predictable — but loses context without replacement:

import { Agent, SlidingWindowConversationManager } from '@strands-agents/sdk'

const agent = new Agent({
  name: 'sliding-agent',
  model: 'us.anthropic.claude-sonnet-4-6-20250725-v1:0',
  systemPrompt: 'You are a helpful assistant.',
  conversationManager: new SlidingWindowConversationManager({
    windowSize: 20,                  // Keep last 20 messages
    shouldTruncateResults: true,     // Trim large tool results in kept messages
    maxResultLength: 2000,           // Truncate tool results beyond 2000 chars
  }),
})

The sliding window is appropriate when recent context matters more than historical context — chatbots, iterative coding assistants, real-time Q&A.

SummarizingConversationManager

The intelligent option. Instead of dropping old messages, it summarizes them — preserving the key information in compressed form. Uses a separate model call to create the summary:

import { Agent, SummarizingConversationManager } from '@strands-agents/sdk'

const agent = new Agent({
  name: 'summarizing-agent',
  model: 'us.anthropic.claude-sonnet-4-6-20250725-v1:0',
  systemPrompt: 'You are a project management assistant.',
  conversationManager: new SummarizingConversationManager({
    preserveRecentMessages: 10,   // Always keep the last 10 messages verbatim
    maxSummaryTokens: 500,        // Target length for the summary
    summarizationPrompt: `Summarize the following conversation history.
Focus on: decisions made, action items assigned, and key context.
Omit pleasantries, repeated questions, and tool call details.`,
  }),
})

When the conversation exceeds the threshold, messages older than preserveRecentMessages are replaced by a summary message. The agent sees: [summary of turns 1-30] + [verbatim turns 31-40]. Best of both worlds — compressed history plus full recent context.

Cost consideration

The SummarizingConversationManager makes an additional model call to produce the summary. This adds latency and cost per compression event. But it's far cheaper than hitting context limits and failing, or paying for a massive prompt on every turn. The summary call uses the full message history once, then subsequent turns use the compressed version.

Message Pinning

Both SlidingWindow and Summarizing managers support pinFirst — protecting the first N messages from eviction. Use this to ensure system instructions or critical context survive compression:

const agent = new Agent({
  name: 'pinned-agent',
  model: 'us.anthropic.claude-sonnet-4-6-20250725-v1:0',
  systemPrompt: 'You are a financial analyst.',
  conversationManager: new SlidingWindowConversationManager({
    windowSize: 15,
    pinFirst: 2,  // First 2 messages (system + initial context) never evicted
  }),
})

With pinFirst: 2, the conversation always contains the system prompt and whatever context message you injected first, regardless of how many messages are trimmed from the middle. This prevents the common failure mode where a long conversation causes the agent to "forget" its role.

Proactive Compression

By default, conversation managers trigger when the context would exceed the model's limit. Proactive compression triggers earlier — before you hit the wall — giving the model headroom to think:

const agent = new Agent({
  name: 'proactive-agent',
  model: 'us.anthropic.claude-sonnet-4-6-20250725-v1:0',
  systemPrompt: 'You are a research assistant.',
  conversationManager: new SummarizingConversationManager({
    preserveRecentMessages: 8,
    proactiveCompression: {
      enabled: true,
      compressionThreshold: 0.7,  // Compress when 70% of context is used
    },
  }),
})

With compressionThreshold: 0.7, compression fires when the conversation reaches 70% of the model's context window. This leaves 30% for the model's response and reasoning — preventing the case where the model has context for the question but no room to think about the answer.

Don't set too low

A compressionThreshold below 0.5 means you're compressing when the context is half full — wasteful. Between 0.6 and 0.8 is the sweet spot. Higher values (0.85+) risk cutting it too close and failing on turns with large tool results.

Custom ConversationManager

If the built-in managers don't fit your needs, extend the base class and implement reduce(). The reduce method receives the full message array and must return a shorter one:

import { ConversationManager, Message } from '@strands-agents/sdk'

class TopicBasedManager extends ConversationManager {
  private maxMessages = 30

  async reduce(messages: Message[]): Promise {
    if (messages.length <= this.maxMessages) {
      return messages // No reduction needed
    }

    // Custom logic: keep system messages, recent messages,
    // and any message containing a "DECISION:" marker
    const system = messages.filter(m => m.role === 'system')
    const recent = messages.slice(-10)
    const decisions = messages.filter(
      m => m.role === 'assistant' &&
           typeof m.content === 'string' &&
           m.content.includes('DECISION:')
    )

    // Deduplicate (recent might overlap with decisions)
    const kept = new Set([...system, ...decisions, ...recent])
    return Array.from(kept)
  }
}

const agent = new Agent({
  name: 'custom-managed',
  model: 'us.anthropic.claude-sonnet-4-6-20250725-v1:0',
  systemPrompt: 'Track decisions across a long conversation.',
  conversationManager: new TopicBasedManager(),
})

Custom managers let you implement domain-specific logic: keep messages tagged as important, preserve tool results from specific tools, or merge multiple messages into one.

Production Pattern: Choosing the Right Manager

Match your manager to your agent's lifecycle:

Agent TypeManagerWhy
Single-shot task (extract data, classify) NullConversationManager One turn, no accumulation. Don't pay for unnecessary management.
Short multi-turn (3-5 turns with tools) NullConversationManager Predictable size. If each turn adds ~2K tokens and you have 5 turns, you're well within limits.
Chat assistant (unbounded turns) SlidingWindow Fast, cheap, recent context is most relevant. Users don't reference turn 1 at turn 50.
Project assistant (long-running, decisions accumulate) SummarizingConversationManager Decisions from turn 5 matter at turn 50. Summary preserves them without full verbatim cost.
Research agent (large tool results) SlidingWindow + shouldTruncateResults Tool results are often huge but only the latest matters. Truncate old results, keep recent ones full.
// Production example: project management assistant
// Long-lived, decisions accumulate, recent context critical
const projectAssistant = new Agent({
  name: 'project-assistant',
  model: 'us.anthropic.claude-sonnet-4-6-20250725-v1:0',
  systemPrompt: `You help manage a software project.
Track decisions, action items, and blockers across conversations.`,
  conversationManager: new SummarizingConversationManager({
    preserveRecentMessages: 8,
    maxSummaryTokens: 800,
    proactiveCompression: {
      enabled: true,
      compressionThreshold: 0.7,
    },
    summarizationPrompt: `Summarize the conversation history for a project assistant.
Preserve: all decisions, action items with owners, unresolved blockers.
Discard: greetings, clarification exchanges, tool call mechanics.`,
  }),
})

Key Takeaways

Your agent processes support tickets in a single turn — user provides ticket text, agent classifies and routes it. Which conversation manager should you use?
Correct. A single-turn agent never accumulates enough messages to cause problems. Adding a conversation manager here is unnecessary overhead — no compression cost, no configuration complexity, no edge cases. The default NullConversationManager is perfect.
For single-turn agents, there's nothing to manage. The conversation has one user message and one assistant response — well within any context window. NullConversationManager (the default) is the right choice. Don't add machinery you don't need.
You set proactiveCompression.compressionThreshold: 0.7 on a SummarizingConversationManager. What does this mean?
Correct. The threshold is measured against the model's total context window. At 70% usage, compression fires proactively — before you hit the wall. The remaining 30% gives the model room for reasoning and response generation.
The compressionThreshold is a ratio of context window usage. At 0.7, it means: "when 70% of the model's context window is consumed by the conversation, trigger summarization." This leaves 30% free for the model's response. It's a proactive safety margin.
Your project assistant uses SummarizingConversationManager. A user asks "what did we decide about the database in our first conversation?" but that decision was in messages that have been summarized. What happens?
Correct. The quality of the summarization prompt directly determines what survives compression. A well-written prompt ("Preserve: all decisions, action items with owners") ensures key information persists in the summary. This is why the summarization prompt matters so much — it's your retention policy in natural language.
The SummarizingConversationManager replaces old messages with a summary. If your summarization prompt says "preserve all decisions," the decision will be in the summary message and the agent can reference it. The prompt is your retention policy — what you tell it to keep, it keeps.
📖 Primary Source

Conversation Management — Strands Agents Docs — Full API reference for all conversation managers, configuration options, and custom implementations. ~10 min read.

💬 Questions? Ask me about sizing window parameters, writing effective summarization prompts, handling edge cases with tool results, or building custom managers for specific domains.
← Back Next →