Building AI Agents with Open Source Models Track 3: Memory & Context
OS Track · Module 6 of 12 ~55 min Intermediate
🦙
Open Source Track — Mistral/Ollama Version All code examples use the openai SDK pointing at a local Ollama server. View Claude version → · OS Track Index →

M08: Conversation Management

Master the art of managing multi-turn conversations — from stateless API calls to production-grade context management with tokenThe basic unit of text that language models process. A token is roughly 3–4 characters in English. Both input (what you send) and output (what the model generates) are measured in tokens, and you pay per token. budgets, pruning, and summarization.

Prerequisites: M01–M04 (Foundations track) · Ollama running with Mistral: ollama run mistral

Learning Objectives

  • Explain why the OpenAI-compatible API (and all LLM APIs) are stateless and how the illusion of memory is created by replaying message history
  • Implement three conversation history management strategies: full history, sliding windowA strategy that keeps only the most recent N messages in the conversation history, discarding older ones. Like a window that slides forward over time, always showing only the latest portion., and summarization
  • Calculate token budget allocation across system promptA special instruction message with role "system" sent as the first message in the array. It sets the assistant's behavior, personality, and constraints., history, current message, and reserved response tokensInput tokens are what you send to the model (system message + conversation messages). Output tokens are what the model generates in response. Both count against the context window.
  • Apply message pruning strategies that preserve information density while staying within token limits
  • Build a production-grade ConversationManager class with automatic pruning, summarization, and serializationConverting an in-memory data structure (like a conversation history object) into a format that can be stored in a file or transmitted over a network — typically JSON. Deserialization is the reverse process.

The Stateless Reality: The Model Has No Memory

Everyday Analogy

BEFORE: Imagine you could hire a world-class expert for advice, but they have perfect amnesia — every time you walk into the room, they have zero memory of any previous conversation you have had with them.

PAIN: If you just said "so what do you think about option B?" they would stare blankly, because they have no idea what option A was, who you are, or what problem you are solving. Every interaction starts from absolute zero.

MAPPING: This is exactly how the OpenAI-compatible API (and Ollama's Mistral) works. Each API call is a fresh room with a fresh expert. The only way to give the model "memory" is to hand it a written transcript of every previous exchange — your code replays the entire conversation from scratch on every single request.

What this actually looks like in code: On your third message to Mistral, the API request does not just send message 3. It sends all previous messages again from scratch:

# API call #3 — the ENTIRE payload your code sends: { "messages": [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Hi, I need help with Python."}, {"role": "assistant", "content": "Sure! What do you need help with?"}, {"role": "user", "content": "How do I read a CSV file?"}, {"role": "assistant", "content": "Use pandas: pd.read_csv('file.csv')"}, {"role": "user", "content": "Can you add filtering?"} ← NEW ] }

Notice: the system prompt and all previous messages are resent verbatim. Mistral does not "remember" them — your code replays them every time.

Technical Definition The Chat Completions APIThe OpenAI-compatible endpoint at /v1/chat/completions. Each request contains a messages array and returns the model's response. There is no server-side session state. is statelessEach API request is independent — the server does not store any information between requests. Every request must contain all the context the model needs.. Here is what that means, step by step.

First, each request is completely independent. There is no session ID. There is no server-side conversation state. There is no implicit memory of any kind.

Second, what users perceive as a continuous conversation is actually the client re-sending the entire message history in the messages array with every single request — including the system prompt as {"role": "system", ...}.

In other words, the assistant's "memory" of earlier messages exists only because the developer explicitly includes those messages in the next API call. If you leave a message out, it is gone — the model will never know it happened.

The Stateless Reality: Every Call Starts from Zero API Call 1 📶 msg 1: "Hi, I'm Alice" → Mistral processes → 📥 "Hello Alice!" 1 message sent no link API Call 2 📶 msg 1 (re-sent!) 📥 response 1 (re-sent!) 📶 msg 2: "What's my name?" → Mistral processes → 📥 "Your name is Alice" 3 messages sent no link API Call 3 msg 1 (re-sent again!) response 1 (re-sent again!) msg 2 (re-sent again!) response 2 (re-sent again!) 📶 msg 3: new question 📥 response 3 5 messages sent ⚠ Each call re-sends EVERYTHING. Token usage grows with every turn. Mistral has zero memory between API calls — your code manages all state.
Stateless API — Perception vs. Reality
What the User Sees
What Actually Happens (API Calls)
Why It Matters Statelessness is not a limitation — it is a superpower. Because you control the entire context, you can edit history, branch conversations, and inject context from external sources.

Concrete examples: A customer support agent handling 10,000 concurrent sessions does not need 10,000 server-side session stores — each request is self-contained. A healthcare agent can surgically remove PHI from history before sending it to a logging service. A debugging agent can "rewind" a conversation to turn 5 and try a different approach.

Understanding statelessness is the foundation of everything else in this module.

⚠️ Common Misconceptions

"The model remembers our previous conversation." — No. Each API call is completely stateless. There is no session, no server-side storage, no memory of any kind. If you do not include previous messages in the messages array, they do not exist as far as the model is concerned.

"Context window = memory." — No. The context window is temporary — it exists only for the duration of a single API request. Memory persists across requests. The context window is more like a whiteboard that gets erased after every meeting.

"Longer context = better results." — Not necessarily. Research shows that recall degrades with context length — a phenomenon called the "lost in the middle" effect. Information buried in the middle of a very long context gets lower attention than information at the start or end.

"Summarization is lossless." — No. Summarization is a lossy compression. It preserves the gist but loses specifics: exact names, account numbers, dates, dollar amounts, and nuanced phrasing. That is why production systems use "pinned facts" blocks that are never summarized.

Conversation History Management Patterns

Everyday Analogy

BEFORE: Imagine packing for a two-week trip, but your airline only allows one carry-on bag with a strict 7 kg weight limit.

PAIN: If you try to bring everything, the bag overflows and the airline rejects it at the gate. But if you only bring what fits right now, you might arrive at a formal dinner with nothing but hiking clothes because you dropped the dress shoes three days ago.

MAPPING: Your context window is that carry-on bag. Full history means cramming everything in (works for short trips). Sliding window means only packing the last few days of clothes (cheap but you lose early essentials). Summarization means writing a packing list of what you left at home plus bringing the current essentials — the best balance for long journeys.

What this actually looks like in practice: Suppose a 20-turn conversation has accumulated 20 messages. Here is how each strategy handles sending turn 21:

# Full History — sends all 20 messages + the new one (21 total) messages: [sys, msg1, msg2, ... msg20, NEW_MSG] → ~3,000 tokens # Sliding Window (N=6) — sends only the last 6 + the new one messages: [sys, msg15, msg16, msg17, msg18, msg19, msg20, NEW_MSG] → ~1,050 tokens # Summarization — a summary of msgs 1-14, then last 6 + new one messages: [ {"role":"system","content":"You are a helpful assistant."}, {"role":"user","content":"[Summary: user asked about Python CSV parsing, resolved encoding issue, moved to data filtering...]"}, {"role":"assistant","content":"Understood, I have the context."}, msg15, msg16, msg17, msg18, msg19, msg20, NEW_MSG ] → ~1,400 tokens
Technical Definition Three primary patterns exist for managing the conversation historyThe ordered array of messages (user + assistant turns) sent with each API request. This array IS the model's memory — nothing else persists between calls.:
  1. Full History — send all messages every time. Simple but hits token limitsThe maximum number of tokens a model can process in a single request. Ollama/Mistral context window defaults to 32K but varies by model and configuration. quickly. Best for short conversations (< 20 turns).
  2. Sliding Window — keep only the last N messages. Maintains recency but loses early context.
  3. Summarization — periodically compress older messages into a summary, prepend it to the history, and drop the originals. Preserves key information while staying within token budgets.
Most production agents use a hybrid approach: a summary of old turns + full recent turns + an always-present system message.

Summarization: The Strategy That Deserves a Closer Look

Summarization is the most powerful of the three strategies. When your conversation manager detects that token usage has crossed a threshold (say 10,000 tokens for a local Mistral setup), it splits the message history into "old" messages and "recent" messages (the last 4 turns, kept verbatim). It sends the old messages to the model with a special instruction: "Summarize this conversation. Preserve key decisions, user preferences, important facts. Skip greetings and filler." The model returns a compact summary, which gets injected as a synthetic user/assistant message pair, and the original old messages are discarded.

If you are thinking "this sounds a lot like a sliding window, just smarter" — you are right, and that is the key difference. A sliding window throws away old messages and loses their information forever. Summarization compresses them instead, preserving the meaning while discarding the verbatim text. The trade-off is that summarization costs an extra API call (to the local Ollama server), and some specific details may be paraphrased away. That is why production systems often combine summarization with "pinned facts" that are never summarized.

Three History Management Strategies Full History msg 1 response 1 ⋮ (all messages kept) ✓ Complete context ✓ Simplest code ✗ Tokens grow unbounded ✗ Breaks after ~20 turns Short chats only Sliding Window msg 1 (dropped) ⋮ (older messages gone) msg N-2 response N-2 ✓ Fixed token cost ✓ Easy to implement ✗ Loses early context ✗ "Who are you?" problem Summarization 📝 Summary "Alice asked about X, decided Y, prefers Z format..." msg N-2 (recent, full) response N-2 ✓ Preserves key facts ✓ Bounded token use ✗ Extra API call to summarize ✗ May lose specific details ⭐ Best for production
History Strategies Compared
Full History
0 tokens
Sliding (N=6)
0 tokens
Summarize
0 tokens
Why It Matters There is no single best strategy — the right approach depends on your conversation length, token budget, and how much early context matters. Real numbers: A 50-turn support conversation with full history sends ~75,000 tokens per request. A sliding window (N=10) cuts that to ~15,000 tokens. Summarization with the last 4 turns typically lands around 8,000–12,000 tokens while preserving far more context than a window alone. Choosing wrong means either burning resources on bloated payloads or losing critical context mid-conversation.

Token Budget Allocation

Everyday Analogy

BEFORE: Imagine you have a fixed-size suitcase for an international trip — exactly 32,000 units of space, not one more — and everything you need must fit inside it.

PAIN: You must pack a travel guide (system prompt), a photo album of memories (conversation history), the souvenirs you are buying today (current user message), and leave empty space to bring things home (response tokens). If you overstuff the album, there is no room left for today's souvenirs or the space to bring anything home.

MAPPING: Your context windowThe total number of tokens a model can process in one request. Ollama's Mistral defaults to 32K context but can be configured with --num-ctx. The four consumers (system prompt, history, current message, response) compete for the same fixed space. is that suitcase. The four consumers — system prompt, history, current message, and reserved response tokens — compete for the same fixed space.

What this looks like in your code: Here is a typical budget breakdown for a Mistral agent (32K context) at turn 25:

Context window total: 32,000 tokens (Ollama Mistral default) ───────────────────────────────────────────── System prompt: 400 tokens (fixed — your instructions) Conversation history (25 turns): 9,500 tokens (growing every turn!) Current user message: 200 tokens (variable) ───────────────────────────────────────────── REMAINING for response: 21,900 tokens ✓ plenty of room ...but at turn 80: Conversation history (80 turns): 30,000 tokens ← approaching limit! REMAINING: 1,400 tokens ⚠ barely fits a response
Technical Definition The context window (e.g., 32K tokens for Mistral via Ollama) is divided among four competing consumers:
  • System message — typically 200–800 tokens (fixed, sent first in the messages array)
  • Conversation history — variable, grows per turn
  • Current user message — variable
  • Response space — what remains after input. With Ollama you don't set max_tokens as a budget reservation — the model uses whatever context remains after your input. To stay safe, count your input tokens and ensure they leave headroom for the response.

Formula: available_for_response = context_window - system_tokens - history_tokens - current_message_tokens. Monitor this before each API call and prune history when the ratio drops below a comfortable threshold (e.g., less than 4,000 tokens remaining).

One subtlety that surprises beginners: the system message occupies budget space on every single request, not just the first one. A 500-token system prompt quietly consumes 500 × 50 = 25,000 tokens of cumulative input by turn 50. This is why production teams obsess over concise system prompts.

Total budget: 32,000 tokens (Ollama Mistral default) Increase context with: ollama run mistral --num-ctx 65536
⚠️ Common Misconceptions

"max_tokens limits what the model reads." — No. When set, max_tokens (or max_completion_tokens) only limits how much the model writes (output tokens). It does not affect input processing.

"Token counts are exact and predictable." — Not quite. Token counts depend on the model's tokenizer. Use the API response's usage field for exact counts.

"Local models have unlimited context." — No. Local models have configurable context windows, but larger windows require significantly more GPU/CPU memory. A 32K context window on Mistral 7B requires roughly 8 GB of RAM beyond the base model weight.

Local Model Advantage With Ollama, there's no per-token billing — but you still pay in latency and hardware. Longer contexts mean longer inference times. Aggressive pruning and summarization directly improve response speed on local models.
🎓 Cert Tip — Domain 5.1

The "lost in the middle" effect means information in the middle of long context gets lower recall than information at the start or end. Position critical data at the beginning (case facts) or end (current query).

Practical Context Window Management

Four moving pieces work together: counting tokens before you send, allocating by percentage, handling overflow, and dynamically sizing the sliding window.

1. Count tokens before the request. For Ollama/OpenAI SDK, approximate token count with a rough estimate (4 chars ≈ 1 token) or use a local tokenizer:

# Pseudocode — run BEFORE every API call estimated_tokens = sum(len(m["content"]) // 4 for m in messages) if estimated_tokens > CONTEXT_LIMIT * 0.85: messages = prune_to_budget( messages, target = int(CONTEXT_LIMIT * 0.75) ) response = client.chat.completions.create(...)

2. Slide on tokens, not message count. Walk newest-to-oldest and accumulate until you hit your history budget. Short chitchat keeps many turns, large responses keep fewer:

kept, used = [], 0 for msg in reversed(history): t = len(msg["content"]) // 4 # rough token estimate if used + t > budget_for_history: break kept.insert(0, msg) used += t return kept # newest preserved, oldest dropped
Token Budget Allocation
System
History
Message
Response
System (500)
History (0)
Current (300)
Response
Context window: 32,000 tokens — Available: 31,200
Pruning triggered! History exceeds 80% of available budget.

Message Pruning Strategies

Everyday Analogy

BEFORE: Imagine you are a film editor with 6 hours of raw footage that must become a 90-minute movie — you cannot simply cut from the end or beginning, because key plot points are scattered throughout.

PAIN: If you blindly cut the first 4.5 hours, you lose the character introductions. If you keep everything, the audience loses focus during the filler scenes.

MAPPING: Pruning a conversation works the same way. You score each message by importance: greetings and filler are cut first. Key facts (account numbers, decisions, tool results) are preserved no matter how old. And sometimes you replace a block of scenes with a narrator's summary.

Technical Definition PruningThe process of selectively removing messages from conversation history to stay within token limits while preserving the most important context for the model. is the art of selectively removing messages to stay within token limits while keeping the information the model actually needs. Five main strategies:
  1. FIFOFirst In, First Out — drop the oldest messages first. Simple but may lose critical early context. (first in, first out) — drop oldest messages first.
  2. Importance scoring — tag messages with metadataAdditional data attached to each message beyond its content — such as timestamps, token counts, importance scores, or tags. and drop low-importance messages first.
  3. Semantic deduplicationIdentifying and removing messages that convey the same meaning, even if worded differently. — remove redundant exchanges.
  4. Role-based retention — always keep certain message types (tool results, user preferences, key decisions).
  5. Summarize-and-replace — condense a block of messages into a single summary before dropping originals.

Critical implementation detail: after removing any messages, verify that the remaining array still has valid message alternationThe chat completions API requires messages to alternate between user and assistant roles, and the user role message must follow the system message. Removing a message can break this alternation, causing an API error.. The API requires messages to alternate between user and assistant roles, and the first non-system message must be from the user.

Importance-Based Pruning in Action
Messages: 0/20
Tokens: 0
Info retained: 100%
Why It Matters Smart pruning preserves the information density of your context. Real scenario: A 20-message support conversation (3,000 tokens) pruned to 7 messages (1,200 tokens) — a 60% token reduction — while retaining 91% of the actionable information. In production, a long-running agent can be pruned from ~30K tokens to ~6K tokens per request, cutting inference latency significantly while keeping every key fact intact.

Building a Production Conversation Manager

Everyday Analogy

BEFORE: Imagine a CEO who takes every meeting without a personal assistant — carrying a growing stack of every note from every past meeting, verbatim, unfiltered.

PAIN: By month three, the stack is so tall they spend more time flipping through old notes than actually engaging. Critical decisions from week one are buried under pages of "sounds good" and "let's circle back."

MAPPING: A conversation manager is that personal assistant. It sits between your application and the Ollama API, recording every exchange, scoring messages by importance, compressing old meetings into executive summaries, and ensuring the model walks into every meeting with exactly the right briefing.

Technical Definition A production conversation managerA class/module that sits between your application logic and the model API, handling message storage, token counting, automatic pruning, summarization, and serialization of conversation state. handles six responsibilities: message storage, token counting, automatic pruning, summarization, metadata tracking, and serialization to JSONJavaScript Object Notation — a lightweight text format for storing and exchanging structured data. The standard format for API requests and data persistence..
Conversation Manager Lifecycle (30 Turns)
0
Turns
0
Total Tokens
0
Tokens Saved
0
Prune Events
🎓 Cert Tip — Domain 5.1

Progressive summarization loses critical specifics: names, IDs, amounts, dates. For production systems, use immutable "case facts" blocks positioned at the START of context (high-recall position). These are never summarized.

Code Walkthrough

Concept → Code Bridge: You now understand the five core concepts: statelessness, history patterns, token budgets, pruning strategies, and the role of a conversation manager. We build in three incremental steps: (1) a basic manager that stores messages and talks to Mistral via Ollama, (2) a sliding window extension, and (3) a smart manager that auto-summarizes and persists state to disk. Each step builds on the previous class using inheritance.

Step 1: Basic Conversation Manager

Let's start with the simplest version: a class that stores messages, prepends the system prompt on each send, calls Mistral via Ollama, and keeps a running count of token usage. This is the "full history" strategy. With the OpenAI SDK, the system prompt is passed as the first element of the messages list rather than a top-level kwarg. Token usage is available in response.usage.prompt_tokens and response.usage.completion_tokens.

from openai import OpenAI
# pip install openai
from dataclasses import dataclass, field
from typing import Optional

client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")

@dataclass
class ConversationManager:
    """Manages multi-turn conversations with Mistral via Ollama."""
    system_prompt: str = "You are a helpful assistant."
    model: str = "mistral"
    messages: list = field(default_factory=list)

    def __post_init__(self):
        self.total_input_tokens = 0
        self.total_output_tokens = 0

    def add_user_message(self, content: str) -> None:
        """Add a user message to the conversation history."""
        self.messages.append({"role": "user", "content": content})

    def add_assistant_message(self, content: str) -> None:
        """Add an assistant message to the conversation history."""
        self.messages.append({"role": "assistant", "content": content})

    def get_messages(self) -> list:
        """Return messages with system prompt prepended."""
        return [{"role": "system", "content": self.system_prompt}] + list(self.messages)

    def send(self, user_message: str) -> str:
        """Send a message and get the model's response."""
        self.add_user_message(user_message)

        try:
            response = client.chat.completions.create(
                model=self.model,
                messages=self.get_messages(),
            )

            # Track token usage
            if response.usage:
                self.total_input_tokens += response.usage.prompt_tokens
                self.total_output_tokens += response.usage.completion_tokens

            assistant_text = response.choices[0].message.content
            self.add_assistant_message(assistant_text)
            return assistant_text

        except Exception as e:
            # Remove the user message we just added on failure
            self.messages.pop()
            raise RuntimeError(f"API call failed: {e}") from e

    def get_token_usage(self) -> dict:
        """Return cumulative token usage."""
        return {
            "total_input_tokens": self.total_input_tokens,
            "total_output_tokens": self.total_output_tokens,
            "message_count": len(self.messages),
        }


# Usage
manager = ConversationManager(
    system_prompt="You are a coding tutor. Be concise."
)

reply1 = manager.send("What is a list comprehension in Python?")
print(reply1)

reply2 = manager.send("Show me an example with filtering.")
print(reply2)

print(manager.get_token_usage())
import OpenAI from 'openai';
// npm install openai

const client = new OpenAI({ baseURL: 'http://localhost:11434/v1', apiKey: 'ollama' });

class ConversationManager {
  constructor({
    systemPrompt = "You are a helpful assistant.",
    model = "mistral",
  } = {}) {
    this.systemPrompt = systemPrompt;
    this.model = model;
    this.messages = [];
    this.totalInputTokens = 0;
    this.totalOutputTokens = 0;
  }

  addUserMessage(content) {
    this.messages.push({ role: "user", content });
  }

  addAssistantMessage(content) {
    this.messages.push({ role: "assistant", content });
  }

  getMessages() {
    return [{ role: "system", content: this.systemPrompt }, ...this.messages];
  }

  async send(userMessage) {
    this.addUserMessage(userMessage);

    try {
      const response = await client.chat.completions.create({
        model: this.model,
        messages: this.getMessages(),
      });

      if (response.usage) {
        this.totalInputTokens += response.usage.prompt_tokens;
        this.totalOutputTokens += response.usage.completion_tokens;
      }

      const assistantText = response.choices[0].message.content;
      this.addAssistantMessage(assistantText);
      return assistantText;
    } catch (error) {
      this.messages.pop();
      throw new Error(`API call failed: ${error.message}`);
    }
  }

  getTokenUsage() {
    return {
      totalInputTokens: this.totalInputTokens,
      totalOutputTokens: this.totalOutputTokens,
      messageCount: this.messages.length,
    };
  }
}

// Usage
const manager = new ConversationManager({
  systemPrompt: "You are a coding tutor. Be concise.",
});

const reply1 = await manager.send(
  "What is a list comprehension in Python?"
);
console.log(reply1);

const reply2 = await manager.send(
  "Show me an example with filtering."
);
console.log(reply2);

console.log(manager.getTokenUsage());
What Just Happened? You built a ConversationManager for Ollama/Mistral. The key difference from the model version: the system prompt lives in getMessages() as {"role": "system", ...} prepended to the messages array, rather than a top-level kwarg. Token usage comes from response.usage.prompt_tokens and response.usage.completion_tokens. Safety detail: if the API call fails, the manager pops the user message it just added, so your history stays clean.

Step 2: Sliding Window Mode

Now extend the manager with a sliding window. Instead of sending all messages, override get_messages() to return only the last N. The key subtlety: if the slice starts on an assistant message, drop it to maintain the required user-first alternation. The system prompt is always included regardless.

class SlidingWindowManager(ConversationManager):
    """Extends ConversationManager with a sliding window."""

    def __init__(self, window_size: int = 10, **kwargs):
        super().__init__(**kwargs)
        self.window_size = window_size

    def get_messages(self) -> list:
        """Return system prompt + only the most recent N messages."""
        if len(self.messages) <= self.window_size:
            windowed = list(self.messages)
        else:
            windowed = self.messages[-self.window_size:]

        # Ensure we start with a user message (valid alternation)
        if windowed and windowed[0]["role"] == "assistant":
            windowed = windowed[1:]

        return [{"role": "system", "content": self.system_prompt}] + windowed


# Usage — only last 6 messages sent per call
manager = SlidingWindowManager(
    window_size=6,
    system_prompt="You are a concise coding tutor.",
)
for q in [
    "What is Python?",
    "What are variables?",
    "Explain loops.",
    "What are functions?",
    "Explain classes.",
    "What is inheritance?",
]:
    print(f"Q: {q}")
    print(f"A: {manager.send(q)}\n")

# All 12 messages stored, but only last 6 sent to API
print(f"Stored: {len(manager.messages)} messages")
print(f"Sent (excl. system): {len(manager.get_messages()) - 1} messages")
class SlidingWindowManager extends ConversationManager {
  constructor({ windowSize = 10, ...opts } = {}) {
    super(opts);
    this.windowSize = windowSize;
  }

  getMessages() {
    let windowed = this.messages.length <= this.windowSize
      ? [...this.messages]
      : this.messages.slice(-this.windowSize);

    // Ensure we start with a user message
    if (windowed[0]?.role === "assistant") {
      windowed = windowed.slice(1);
    }
    return [{ role: "system", content: this.systemPrompt }, ...windowed];
  }
}

// Usage — only last 6 messages sent per call
const manager = new SlidingWindowManager({
  windowSize: 6,
  systemPrompt: "You are a concise coding tutor.",
});

for (const q of [
  "What is Python?",
  "What are variables?",
  "Explain loops.",
  "What are functions?",
  "Explain classes.",
  "What is inheritance?",
]) {
  console.log(`Q: ${q}`);
  console.log(`A: ${await manager.send(q)}\n`);
}

console.log(`Stored: ${manager.messages.length} messages`);
console.log(`Sent (excl. system): ${manager.getMessages().length - 1} messages`);
What Just Happened? The manager now stores all messages internally for audit purposes, but sends only the last N to the API via an overridden get_messages(). The system prompt is always included as the first message. Trade-off: cheap and fast, but loses early context permanently.
Bridge: The sliding window is fast but forgetful. Step 3 solves this by using the model itself to summarize old messages before discarding them, and adds JSON persistence so conversations survive server restarts.

Step 3: Auto-Summarization & Persistence

The production-grade version. Instead of just dropping old messages, we ask Mistral to summarize them first. The result is a much shorter history that still carries essential context. The save() and load() methods serialize full state to JSON. Note: the summarization call also goes through Ollama — no external API needed.

import json
import time
from pathlib import Path

class SmartConversationManager(ConversationManager):
    """Full-featured manager with summarization and persistence."""

    def __init__(
        self,
        token_threshold: int = 8_000,   # Lower for Mistral's 32K context
        recent_turns_to_keep: int = 4,
        **kwargs
    ):
        super().__init__(**kwargs)
        self.token_threshold = token_threshold
        self.recent_turns_to_keep = recent_turns_to_keep
        self.summary: Optional[str] = None
        self.summary_history: list = []
        self.last_input_tokens = 0

    def _should_summarize(self) -> bool:
        return self.last_input_tokens > self.token_threshold

    def _summarize_old_messages(self) -> None:
        """Use Mistral to summarize older messages."""
        keep_count = self.recent_turns_to_keep * 2  # user+assistant pairs
        if len(self.messages) <= keep_count:
            return

        old_messages = self.messages[:-keep_count]
        recent_messages = self.messages[-keep_count:]

        summary_prompt = (
            "Summarize this conversation concisely. "
            "Preserve: key decisions, user preferences, "
            "important facts. Skip: greetings, filler.\n\n"
        )
        for msg in old_messages:
            summary_prompt += f"{msg['role']}: {msg['content']}\n"

        try:
            response = client.chat.completions.create(
                model=self.model,
                messages=[
                    {"role": "system", "content": "You are a conversation summarizer. Be concise."},
                    {"role": "user", "content": summary_prompt}
                ],
            )
            new_summary = response.choices[0].message.content

            if self.summary:
                new_summary = f"Previous context: {self.summary}\n\nRecent: {new_summary}"

            self.summary = new_summary
            self.summary_history.append({
                "timestamp": time.time(),
                "messages_summarized": len(old_messages),
            })

            # Replace old messages with a summary message pair
            self.messages = [
                {"role": "user", "content": f"[Conversation summary: {self.summary}]"},
                {"role": "assistant", "content": "Understood. I have the conversation context."},
                *recent_messages,
            ]
        except Exception:
            # If summarization fails, fall back to sliding window
            self.messages = recent_messages

    def send(self, user_message: str) -> str:
        """Send with automatic summarization when needed."""
        self.add_user_message(user_message)

        try:
            msgs_to_send = [{"role": "system", "content": self.system_prompt}] + self.messages
            response = client.chat.completions.create(
                model=self.model,
                messages=msgs_to_send,
            )
            if response.usage:
                self.last_input_tokens = response.usage.prompt_tokens
                self.total_input_tokens += response.usage.prompt_tokens
                self.total_output_tokens += response.usage.completion_tokens

            assistant_text = response.choices[0].message.content
            self.add_assistant_message(assistant_text)

            # Check if we need to summarize
            if self._should_summarize():
                self._summarize_old_messages()

            return assistant_text

        except Exception as e:
            self.messages.pop()
            raise RuntimeError(f"API call failed: {e}") from e

    def save(self, filepath: str) -> None:
        """Persist conversation state to a JSON file."""
        state = {
            "system_prompt": self.system_prompt,
            "model": self.model,
            "messages": self.messages,
            "summary": self.summary,
            "summary_history": self.summary_history,
            "total_input_tokens": self.total_input_tokens,
            "total_output_tokens": self.total_output_tokens,
            "saved_at": time.time(),
        }
        Path(filepath).write_text(json.dumps(state, indent=2))

    @classmethod
    def load(cls, filepath: str) -> "SmartConversationManager":
        """Load conversation state from a JSON file."""
        data = json.loads(Path(filepath).read_text())
        mgr = cls(
            system_prompt=data["system_prompt"],
            model=data["model"],
        )
        mgr.messages = data["messages"]
        mgr.summary = data.get("summary")
        mgr.summary_history = data.get("summary_history", [])
        mgr.total_input_tokens = data.get("total_input_tokens", 0)
        mgr.total_output_tokens = data.get("total_output_tokens", 0)
        return mgr


# Usage
manager = SmartConversationManager(
    token_threshold=3_000,   # Low threshold for testing
    recent_turns_to_keep=4,
    system_prompt="You are a helpful coding assistant.",
)

reply = manager.send("Help me build a REST API with FastAPI.")
print(reply)

# Save and restore across sessions
manager.save("conversation_state.json")
restored = SmartConversationManager.load("conversation_state.json")
reply2 = restored.send("Where were we?")
print(reply2)
import OpenAI from 'openai';
import { readFileSync, writeFileSync } from "node:fs";

const client = new OpenAI({ baseURL: 'http://localhost:11434/v1', apiKey: 'ollama' });

class SmartConversationManager extends ConversationManager {
  constructor({
    tokenThreshold = 8_000,
    recentTurnsToKeep = 4,
    ...opts
  } = {}) {
    super(opts);
    this.tokenThreshold = tokenThreshold;
    this.recentTurnsToKeep = recentTurnsToKeep;
    this.summary = null;
    this.summaryHistory = [];
    this.lastInputTokens = 0;
  }

  _shouldSummarize() {
    return this.lastInputTokens > this.tokenThreshold;
  }

  async _summarizeOldMessages() {
    const keepCount = this.recentTurnsToKeep * 2;
    if (this.messages.length <= keepCount) return;

    const oldMessages = this.messages.slice(0, -keepCount);
    const recentMessages = this.messages.slice(-keepCount);

    let summaryPrompt =
      "Summarize this conversation concisely. " +
      "Preserve: key decisions, user preferences, important facts. " +
      "Skip: greetings, filler.\n\n";
    for (const msg of oldMessages) {
      summaryPrompt += `${msg.role}: ${msg.content}\n`;
    }

    try {
      const response = await client.chat.completions.create({
        model: this.model,
        messages: [
          { role: "system", content: "You are a conversation summarizer. Be concise." },
          { role: "user", content: summaryPrompt }
        ],
      });
      let newSummary = response.choices[0].message.content;

      if (this.summary) {
        newSummary = `Previous context: ${this.summary}\n\nRecent: ${newSummary}`;
      }

      this.summary = newSummary;
      this.summaryHistory.push({
        timestamp: Date.now(),
        messagesSummarized: oldMessages.length,
      });

      this.messages = [
        { role: "user", content: `[Conversation summary: ${this.summary}]` },
        { role: "assistant", content: "Understood. I have the conversation context." },
        ...recentMessages,
      ];
    } catch {
      this.messages = recentMessages;
    }
  }

  async send(userMessage) {
    this.addUserMessage(userMessage);

    try {
      const response = await client.chat.completions.create({
        model: this.model,
        messages: [{ role: 'system', content: this.systemPrompt }, ...this.messages],
      });

      if (response.usage) {
        this.lastInputTokens = response.usage.prompt_tokens;
        this.totalInputTokens += response.usage.prompt_tokens;
        this.totalOutputTokens += response.usage.completion_tokens;
      }

      const assistantText = response.choices[0].message.content;
      this.addAssistantMessage(assistantText);

      if (this._shouldSummarize()) {
        await this._summarizeOldMessages();
      }

      return assistantText;
    } catch (error) {
      this.messages.pop();
      throw new Error(`API call failed: ${error.message}`);
    }
  }

  save(filepath) {
    const state = {
      systemPrompt: this.systemPrompt,
      model: this.model,
      messages: this.messages,
      summary: this.summary,
      summaryHistory: this.summaryHistory,
      totalInputTokens: this.totalInputTokens,
      totalOutputTokens: this.totalOutputTokens,
      savedAt: Date.now(),
    };
    writeFileSync(filepath, JSON.stringify(state, null, 2));
  }

  static load(filepath) {
    const data = JSON.parse(readFileSync(filepath, "utf-8"));
    const mgr = new SmartConversationManager({
      systemPrompt: data.systemPrompt,
      model: data.model,
    });
    mgr.messages = data.messages;
    mgr.summary = data.summary ?? null;
    mgr.summaryHistory = data.summaryHistory ?? [];
    mgr.totalInputTokens = data.totalInputTokens ?? 0;
    mgr.totalOutputTokens = data.totalOutputTokens ?? 0;
    return mgr;
  }
}

// Usage
const manager = new SmartConversationManager({
  tokenThreshold: 3_000,
  recentTurnsToKeep: 4,
  systemPrompt: "You are a helpful coding assistant.",
});

const reply = await manager.send("Help me build a REST API.");
console.log(reply);

manager.save("conversation_state.json");
const restored = SmartConversationManager.load("conversation_state.json");
const reply2 = await restored.send("Where were we?");
console.log(reply2);
What Just Happened? You built a SmartConversationManager for Ollama/Mistral. The manager watches last_input_tokens after every API response. When tokens cross your threshold, it calls Mistral again (locally, no extra cost) to compress old messages into a summary, then replaces those messages with a summary/acknowledgment pair. The summarization prompt passes the system message inline. save() and load() serialize full state to JSON. Graceful fallback: if summarization fails, it slides to recent messages instead of crashing.
Production Warning The summarization step adds latency (a full Ollama inference round trip). For real-time applications, run summarization asynchronously or off the critical path. Also, keep the token_threshold comfortably below your model's context limit to leave room for the summarization prompt itself.

Hands-On Exercise

What You'll Build

A conversation manager that progresses through three strategies: full history, sliding window, and auto-summarization with JSON persistence. You'll see the token usage difference between each approach in real time against your local Ollama instance.

Time Estimate: 30–45 minutes

Prerequisites: Python 3.10+ (or Node.js 18+), Ollama installed and running (ollama run mistral)

Files You'll Create: conversation_manager.py

Environment Setup

mkdir conversation-lab && cd conversation-lab
python -m venv venv && source venv/bin/activate   # Windows: venv\Scripts\activate
pip install openai
# Make sure Ollama is running: ollama serve && ollama run mistral

Step 1: Basic ConversationManager with Token Tracking

What: Create a ConversationManager class that stores messages, prepends the system prompt as a role:"system" message, sends them to Mistral via Ollama, and tracks token usage.

Why: This implements the "full history" strategy. You will see input tokens grow with each turn.

Create a new file called conversation_manager.py and add the following:

from openai import OpenAI
import json
import time
from pathlib import Path
from dataclasses import dataclass, field
from typing import Optional

# pip install openai
client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")

# ── Step 1: Basic ConversationManager ────────────────────────
@dataclass
class ConversationManager:
    """Full-history conversation manager with token tracking."""
    system_prompt: str = "You are a helpful assistant."
    model: str = "mistral"
    messages: list = field(default_factory=list)

    def __post_init__(self):
        self.total_input_tokens = 0
        self.total_output_tokens = 0

    def add_user_message(self, content: str) -> None:
        self.messages.append({"role": "user", "content": content})

    def add_assistant_message(self, content: str) -> None:
        self.messages.append({"role": "assistant", "content": content})

    def get_messages(self) -> list:
        return [{"role": "system", "content": self.system_prompt}] + list(self.messages)

    def send(self, user_message: str) -> str:
        self.add_user_message(user_message)
        try:
            response = client.chat.completions.create(
                model=self.model,
                messages=self.get_messages(),
            )
            if response.usage:
                self.total_input_tokens += response.usage.prompt_tokens
                self.total_output_tokens += response.usage.completion_tokens
            assistant_text = response.choices[0].message.content
            self.add_assistant_message(assistant_text)
            return assistant_text
        except Exception as e:
            self.messages.pop()  # remove failed user message
            raise RuntimeError(f"API call failed: {e}") from e

    def get_token_usage(self) -> dict:
        return {
            "total_input": self.total_input_tokens,
            "total_output": self.total_output_tokens,
            "messages": len(self.messages),
        }

# ── Test Step 1 ──────────────────────────────────────────────
if __name__ == "__main__":
    print("═" * 50)
    print("TEST 1: Basic ConversationManager (Full History)")
    print("═" * 50)

    mgr = ConversationManager(system_prompt="You are a concise coding tutor. Answer in 1-2 sentences.")

    questions = [
        "What is a list in Python?",
        "How do I add an item to it?",
        "What about removing items?",
    ]

    for i, q in enumerate(questions, 1):
        print(f"\n  Turn {i}: {q}")
        reply = mgr.send(q)
        print(f"  Mistral: {reply[:100]}...")
        usage = mgr.get_token_usage()
        print(f"  [Messages: {usage['messages']} | Input tokens so far: {usage['total_input']}]")

    print(f"\n  ✓ Final: {usage['messages']} messages, {usage['total_input']} total input tokens")
    print(f"  Note: Input tokens grow each turn because ALL messages are resent!")

Run it:

Command
python conversation_manager.py
Expected Output (token counts will vary)
══════════════════════════════════════════════════ TEST 1: Basic ConversationManager (Full History) ══════════════════════════════════════════════════ Turn 1: What is a list in Python? Mistral: A list is an ordered, mutable collection... [Messages: 2 | Input tokens so far: 45] Turn 2: How do I add an item to it? Mistral: Use the .append() method... [Messages: 4 | Input tokens so far: 140] Turn 3: What about removing items? Mistral: Use .remove(value) or .pop(index)... [Messages: 6 | Input tokens so far: 285] ✓ Final: 6 messages, 285 total input tokens Note: Input tokens grow each turn because ALL messages are resent!
✅ Checkpoint

Watch the input token count grow with each turn. This is the stateless reality in action — and exactly why we need strategies in the next steps.

Troubleshooting
  • Connection refused → Make sure Ollama is running: ollama serve and ollama run mistral
  • ModuleNotFoundError: No module named 'openai' → Run pip install openai
  • Token usage shows as 0 → Some Ollama builds may not populate response.usage. Update Ollama to the latest version.

Step 2: Add Sliding Window & Smart Summarization

What: Add two more manager classes: a SlidingWindowManager and a SmartConversationManager that auto-summarizes old turns and persists state to JSON.

Add the following to the bottom of conversation_manager.py, replacing the if __name__ == "__main__" block:

# ── Step 2a: Sliding Window Manager ──────────────────────────
class SlidingWindowManager(ConversationManager):
    """Sends only the last N messages to the API."""

    def __init__(self, window_size: int = 6, **kwargs):
        super().__init__(**kwargs)
        self.window_size = window_size

    def get_messages(self) -> list:
        if len(self.messages) <= self.window_size:
            windowed = list(self.messages)
        else:
            windowed = self.messages[-self.window_size:]
        # Ensure we start with a user message (valid alternation)
        if windowed and windowed[0]["role"] == "assistant":
            windowed = windowed[1:]
        return [{"role": "system", "content": self.system_prompt}] + windowed


# ── Step 2b: Smart Manager with Summarization + Persistence ──
class SmartConversationManager(ConversationManager):
    """Auto-summarizes old turns and saves/loads state."""

    def __init__(self, token_threshold: int = 3_000, recent_turns: int = 4, **kwargs):
        super().__init__(**kwargs)
        self.token_threshold = token_threshold
        self.recent_turns = recent_turns
        self.summary: Optional[str] = None
        self.last_input_tokens = 0

    def send(self, user_message: str) -> str:
        self.add_user_message(user_message)
        try:
            msgs_to_send = [{"role": "system", "content": self.system_prompt}] + self.messages
            response = client.chat.completions.create(
                model=self.model,
                messages=msgs_to_send,
            )
            if response.usage:
                self.last_input_tokens = response.usage.prompt_tokens
                self.total_input_tokens += response.usage.prompt_tokens
                self.total_output_tokens += response.usage.completion_tokens

            assistant_text = response.choices[0].message.content
            self.add_assistant_message(assistant_text)

            # Auto-summarize if threshold exceeded
            if self.last_input_tokens > self.token_threshold:
                self._summarize()

            return assistant_text
        except Exception as e:
            self.messages.pop()
            raise RuntimeError(f"API call failed: {e}") from e

    def _summarize(self) -> None:
        keep_count = self.recent_turns * 2  # user+assistant pairs
        if len(self.messages) <= keep_count:
            return

        old_msgs = self.messages[:-keep_count]
        recent_msgs = self.messages[-keep_count:]

        prompt = "Summarize this conversation concisely. Preserve: key decisions, facts. Skip: greetings, filler.\n\n"
        for m in old_msgs:
            prompt += f"{m['role']}: {m['content']}\n"

        try:
            resp = client.chat.completions.create(
                model=self.model,
                messages=[
                    {"role": "system", "content": "You are a conversation summarizer."},
                    {"role": "user", "content": prompt}
                ],
            )
            summary_text = resp.choices[0].message.content
            if self.summary:
                summary_text = f"Previous: {self.summary}\n\nRecent: {summary_text}"
            self.summary = summary_text
            self.messages = [
                {"role": "user", "content": f"[Context summary: {self.summary}]"},
                {"role": "assistant", "content": "Understood. I have the context."},
                *recent_msgs,
            ]
            print(f"    ⚡ Summarized {len(old_msgs)} old messages → {len(self.messages)} total")
        except Exception:
            self.messages = recent_msgs  # fallback

    def save(self, filepath: str) -> None:
        state = {
            "system_prompt": self.system_prompt,
            "model": self.model,
            "messages": self.messages,
            "summary": self.summary,
            "total_input_tokens": self.total_input_tokens,
            "total_output_tokens": self.total_output_tokens,
        }
        Path(filepath).write_text(json.dumps(state, indent=2))
        print(f"    💾 Saved to {filepath}")

    @classmethod
    def load(cls, filepath: str) -> "SmartConversationManager":
        data = json.loads(Path(filepath).read_text())
        mgr = cls(system_prompt=data["system_prompt"], model=data["model"])
        mgr.messages = data["messages"]
        mgr.summary = data.get("summary")
        mgr.total_input_tokens = data.get("total_input_tokens", 0)
        mgr.total_output_tokens = data.get("total_output_tokens", 0)
        return mgr


# ── Test All Three Strategies ────────────────────────────────
if __name__ == "__main__":
    questions = [
        "What is a Python list?",
        "How do I sort a list?",
        "What about list comprehensions?",
        "How do dictionaries differ from lists?",
        "What are sets?",
        "When should I use tuples?",
    ]

    # --- Test 1: Full History ---
    print("═" * 55)
    print("TEST 1: Full History (all messages sent every time)")
    print("═" * 55)
    mgr1 = ConversationManager(system_prompt="Answer in 1 sentence.")
    for i, q in enumerate(questions, 1):
        mgr1.send(q)
        u = mgr1.get_token_usage()
        print(f"  Turn {i}: stored={u['messages']} sent={u['messages']+1} input_tokens={u['total_input']}")

    # --- Test 2: Sliding Window ---
    print(f"\n{'═' * 55}")
    print("TEST 2: Sliding Window (last 4 messages only)")
    print("═" * 55)
    mgr2 = SlidingWindowManager(window_size=4, system_prompt="Answer in 1 sentence.")
    for i, q in enumerate(questions, 1):
        mgr2.send(q)
        u = mgr2.get_token_usage()
        sent = len(mgr2.get_messages()) - 1  # exclude system
        print(f"  Turn {i}: stored={u['messages']} sent={sent} input_tokens={u['total_input']}")

    # --- Test 3: Smart Manager + Persistence ---
    print(f"\n{'═' * 55}")
    print("TEST 3: Smart Manager (auto-summarization + save/load)")
    print("═" * 55)
    mgr3 = SmartConversationManager(
        token_threshold=200,  # very low to force summarization in test
        recent_turns=2,
        system_prompt="Answer in 1 sentence.",
    )
    for i, q in enumerate(questions, 1):
        mgr3.send(q)
        u = mgr3.get_token_usage()
        print(f"  Turn {i}: messages={u['messages']} input_tokens={u['total_input']}")

    mgr3.save("test_session.json")
    restored = SmartConversationManager.load("test_session.json")
    reply = restored.send("What did we discuss?")
    print(f"\n  Restored reply: {reply[:120]}...")

    # --- Comparison ---
    print(f"\n{'═' * 55}")
    print("COMPARISON (total input tokens after 6 turns):")
    print(f"  Full History:    {mgr1.total_input_tokens:,} tokens")
    print(f"  Sliding Window:  {mgr2.total_input_tokens:,} tokens")
    print(f"  Smart Manager:   {mgr3.total_input_tokens:,} tokens")
    print("═" * 55)

Run the full test suite:

Command
python conversation_manager.py
✅ Checkpoint

Look for these key behaviors:

  • Test 1: Input tokens should grow significantly each turn (full history resends everything)
  • Test 2: The sent column should cap at 4 after turn 2
  • Test 3: You should see ⚡ Summarized messages and the restored manager should know what was discussed
Troubleshooting
  • Summarization never triggers → Lower token_threshold further, or check that Ollama is returning usage data (update Ollama if not)
  • FileNotFoundError on load → Make sure save() ran successfully first
  • Slow responses → Local inference takes time. For testing, keep prompts short. Mistral 7B typically runs 10-50 tokens/sec depending on hardware.

Verify Everything Works

Run the complete file end-to-end. All 3 tests should complete and the COMPARISON table should appear.

🎉 Congratulations

You've built three conversation management strategies and measured their real token usage side-by-side using a fully local, offline Ollama/Mistral stack. The SmartConversationManager pattern works identically with any OpenAI-compatible endpoint.

Stretch Goals (Optional)
  • Importance-based pruning: Add an importance field to each message and drop lowest-scored messages first when pruning
  • Pinned facts: Add a pinned_facts list that is always prepended to the context and never summarized
  • Token estimation: Compare rough char-based estimates vs. calling Ollama's tokenizer endpoint for more accurate counts

Knowledge Check

1. What happens if the client sends an API request to the model without including previous messages?

A The model returns an error because the conversation session has expired
B The model responds with no memory of the conversation — it's a fresh start
C The model retrieves the previous messages from its server-side session storage
D The model uses cached embeddings from the previous messages to maintain context
Correct! The API is stateless — there is no server-side session. If you don't send the history, the model has no memory of it. This is true for all LLM APIs including Ollama.
Not quite. The chat completions API is fully stateless — there is no session storage, no caching between requests. If you omit previous messages, the model treats it as a brand new conversation.

2. With a 32K context window: system message = 500 tokens, history = 20,000 tokens, current message = 300 tokens. How many tokens are available for the response?

A 32,000 tokens
B 12,000 tokens
C 11,200 tokens — calculated as 32,000 − 500 − 20,000 − 300
D 31,200 tokens
Correct! 32,000 − 500 (system) − 20,000 (history) − 300 (message) = 11,200 tokens remaining for the response.
Formula: available = context_window − system − history − current_message. That gives 32,000 − 500 − 20,000 − 300 = 11,200.

3. A customer support bot needs to remember the user's account number from message 1, but conversations can go 50+ turns. Which strategy is best?

A Full history — keep every message
B Sliding window (N=10) — drop messages older than 10 turns
C Summarization with importance-based retention — summarize old turns but always preserve key facts like account numbers
D FIFO pruning — drop the oldest messages first
Correct! Summarization with importance-based retention preserves critical facts while compressing less important exchanges. Pure sliding window or FIFO would lose the account number.
A sliding window or FIFO approach would drop message 1 (with the account number) after a few turns. Full history works but is wasteful at 50+ turns. The best fit is summarization with importance scoring.

4. When summarizing a 10-turn technical conversation about debugging a database query, which information is MOST likely to be lost?

A The final solution that fixed the query
B The database engine being used (PostgreSQL, MySQL, etc.)
C The exact error message that was reported
D The intermediate hypotheses and dead-end approaches that were tried and rejected
Correct! Summarization naturally compresses the narrative, keeping outcomes and key facts while discarding the exploratory process. Dead-end approaches rarely survive summarization.
Summarization preserves key outcomes and facts but tends to compress the exploratory process. Intermediate hypotheses and dead-end approaches are exactly the kind of "filler" that a summarizer drops.

5. Which of these message arrays would cause an API error when sent to the OpenAI-compatible endpoint?

A [{role:"system",...}, {role:"user",...}, {role:"assistant",...}, {role:"user",...}]
B [{role:"assistant",...}, {role:"user",...}, {role:"assistant",...}]
C [{role:"system",...}, {role:"user",...}] (single user message)
D [{role:"user",...}] (single message, no system)
Correct! The messages array must start with the system message (or directly with a user message). Starting with an assistant role message violates the required alternation and will cause an API error. This is a common bug when implementing pruning.
Array B starts with an assistant role message. The API requires the first non-system message to be a user message. Arrays A, C, and D are all valid. This is critical to check in your pruning logic!

Your Score

0/0

Module Summary

Key Concepts Recap

  • Statelessness — The API has no memory. You replay the full conversation every call. System prompt goes as {"role": "system", ...} in the messages array.
  • Full History — Send everything. Simple, but hits limits fast. Good for <20 turns.
  • Sliding Window — Keep last N messages. Cheap but loses early context.
  • Summarization — Compress old turns into a summary. Best balance of cost and context. Works with local Ollama inference.
  • Token Budget — System + History + Message must fit in the context window. Monitor and prune proactively.
  • Pruning — FIFO, importance scoring, semantic dedup, and summarize-and-replace. Always maintain valid message alternation.
  • ConversationManager — A production class that encapsulates storage, counting, pruning, summarization, and persistence. Works identically with any OpenAI-compatible endpoint.

What We Built

A full ConversationManager class (Python & Node.js) using the OpenAI SDK pointed at Ollama/Mistral that handles multi-turn conversations with automatic token tracking, sliding window mode, local-model-powered summarization, and JSON serialization for cross-session persistence.

Next Module Preview

M09: RAG — Retrieval-Augmented Generation takes conversation management further by connecting your agent to external knowledge bases. Instead of relying solely on conversation history, RAG lets your agent search documents, databases, and APIs to ground its responses in real data.