The Agent Class: Tracking Conversation History

~15 min read

Before writing any ReAct logic, you need a minimal Agent class that wraps a conversational LLM and keeps track of its full message history — the foundation every subsequent ReAct step builds on.

Before implementing any ReAct-specific logic, this course's from-scratch walkthrough starts with something more basic: a minimal Agent class that wraps around a conversational LLM and keeps track of its full message history. This class doesn't know anything about Thought/Action/Observation yet — it's just the conversational memory foundation everything else gets built on top of.

The class holds two important pieces of state. system, an optional system prompt string that sets the agent's personality and behavioral constraints — if provided, it becomes the very first message in the conversation, using the special 'system' role, exactly as in the OpenAI Chat API format. And self.messages, a list that acts as the agent's conversation memory: every interaction, whether it's a user message or the assistant's own output, gets appended to this list. This history matters because LLMs are stateless between API calls — without re-sending the full message history on every turn, the model has no way to behave coherently across multiple turns.

The class's __call__ method is the core interface for interacting with the agent, and it does three things in one call: it records the incoming message by appending it to self.messages as a 'user' message, it calls a separate invoke() method that sends the entire conversation history to the LLM and gets back a reply, and it appends that reply to self.messages as an 'assistant' message before returning it to the caller. invoke() itself just handles the actual API call — sending the full message list and extracting the text content from the response.

This tiny class is deliberately unopinionated about ReAct — it's a general-purpose conversational memory wrapper. The ReAct behavior comes entirely from the system prompt you pass into it, which is exactly what the next subtopic covers.

💻 Code example

from litellm import completion

class Agent:
    def __init__(self, system: str = ""):
        self.system = system
        self.messages: list[dict] = []
        if self.system:
            self.messages.append({"role": "system", "content": self.system})

    def __call__(self, message: str = "") -> str:
        if message:
            self.messages.append({"role": "user", "content": message})
        result = self.invoke()
        self.messages.append({"role": "assistant", "content": result})
        return result

    def invoke(self) -> str:
        response = completion(model="openai/gpt-4o", messages=self.messages)
        return response.choices[0].message.content

# Quick sanity check — this alone already "remembers" prior turns:
agent = Agent(system="You are a helpful assistant.")
print(agent("My name is Alex."))
print(agent("What's my name?"))  # correctly recalls "Alex" from self.messages

💬 Deep Dive with AI

Key points

  • The Agent class is deliberately generic — it's a conversational memory wrapper, with zero ReAct-specific logic yet
  • self.messages holds the full conversation history, because LLM API calls are stateless without it
  • __call__ does three things per turn: record the user message, call invoke() for a reply, record the reply — all in one method
  • invoke() is the isolated piece that actually talks to the LLM API, making it easy to swap providers/models later
  • All the ReAct-specific behavior comes from what system prompt you pass in, not from this class itself