While some tools perform their tasks in a single, self-contained operation, many sophisticated interactions require tools to remember information across multiple steps or invocations. These are known as stateful tools, and managing their state effectively is significant for building more capable and context-aware LLM agents. Unlike stateless tools that treat every call as a fresh start, stateful tools maintain a memory, or "state," allowing them to build upon previous interactions, track progress, or accumulate information over time. This capability is particularly useful when an LLM agent needs to perform a sequence of related actions or engage in a multi-turn dialogue where context from earlier exchanges influences later ones.
LLM agents often operate by making a series of calls to different tools or to the same tool multiple times. If a tool needs to, for instance, gather several pieces of information before it can perform its final action (like collecting all necessary details for a flight booking), it must have a way to store these pieces of information temporarily. State management provides this mechanism.
Consider a scenario where an agent is helping a user plan a project. The agent might use a "task_manager" tool.
For the task_manager
tool to correctly associate the deadline and assignee with the "Write introduction" task, it needs to maintain state, specifically, it needs to remember the tasks it's currently managing and their properties.
Python offers several ways to manage state within your custom tools. The choice often depends on the complexity of the state, its required lifespan, and whether it needs to be shared or persisted.
For state that only needs to persist for the duration of a specific task or within a single "session" of agent activity, Python classes are an excellent choice. Instance attributes of a class can hold the state. Each time an LLM agent needs to use this stateful tool, it would interact with an instance of that tool's class.
class ConversationSummarizer:
def __init__(self, user_id):
self.user_id = user_id
self.conversation_history = []
self.key_points = set()
print(f"ConversationSummarizer initialized for user: {self.user_id}")
def add_message(self, sender, text):
"""Adds a message to the conversation history."""
if not isinstance(sender, str) or not isinstance(text, str):
return "Error: Sender and text must be strings."
message = {"sender": sender, "text": text}
self.conversation_history.append(message)
# A simple way to extract potential key points (e.g., nouns)
# In a real tool, this would be more sophisticated
for word in text.split():
if word.capitalize() == word and len(word) > 3: # Basic check for proper nouns
self.key_points.add(word)
return f"Message from {sender} added. Conversation length: {len(self.conversation_history)} messages."
def get_summary(self):
"""Provides a summary of key points from the conversation."""
if not self.conversation_history:
return "No conversation history to summarize."
summary = f"Key points for user {self.user_id}: {', '.join(self.key_points) if self.key_points else 'None yet'}."
return summary
def clear_history(self):
"""Clears the conversation history and key points."""
self.conversation_history = []
self.key_points = set()
return f"Conversation history cleared for user {self.user_id}."
# Example Usage (as an agent might orchestrate):
# user_session_id = "user123"
# summarizer_tool = ConversationSummarizer(user_session_id)
# print(summarizer_tool.add_message("User", "I need to book a flight to London next week."))
# print(summarizer_tool.add_message("Agent", "Okay, I can help with that. Which dates are you considering for London?"))
# print(summarizer_tool.add_message("User", "Around the 15th of March."))
# print(summarizer_tool.get_summary())
# print(summarizer_tool.clear_history())
In this example, conversation_history
and key_points
are instance attributes that store the state of the conversation for a particular user_id
. The agent framework would be responsible for instantiating this class (perhaps once per user session) and routing calls to the correct instance.
Advantages:
Considerations:
When state needs to outlive a single agent session, be shared across multiple agent instances, or handle larger volumes of data, you'll need to rely on external storage mechanisms. Common options include:
json
module) is often required.The choice of external storage depends on factors like the data structure, query requirements, scalability needs, and existing infrastructure.
import json
import os
STATE_FILE_DIR = "tool_states" # Ensure this directory exists
class PersistentNotepadTool:
def __init__(self, session_id):
self.session_id = session_id
self.state_file_path = os.path.join(STATE_FILE_DIR, f"{self.session_id}_notepad.json")
self._load_state()
def _load_state(self):
"""Loads notes from a JSON file."""
try:
if not os.path.exists(STATE_FILE_DIR):
os.makedirs(STATE_FILE_DIR)
with open(self.state_file_path, 'r') as f:
self.notes = json.load(f)
except FileNotFoundError:
self.notes = [] # Initialize with an empty list if no state file exists
except json.JSONDecodeError:
print(f"Warning: Could not decode JSON from {self.state_file_path}. Starting with empty notes.")
self.notes = []
def _save_state(self):
"""Saves notes to a JSON file."""
try:
if not os.path.exists(STATE_FILE_DIR):
os.makedirs(STATE_FILE_DIR)
with open(self.state_file_path, 'w') as f:
json.dump(self.notes, f, indent=4)
except IOError as e:
print(f"Error saving state for {self.session_id}: {e}")
# Potentially raise an exception or return an error to the agent
def add_note(self, content):
"""Adds a note to the persistent list."""
if not isinstance(content, str):
return "Error: Note content must be a string."
self.notes.append(content)
self._save_state()
return f"Note added. Total notes: {len(self.notes)}."
def view_notes(self):
"""Views all current notes."""
if not self.notes:
return "No notes to display."
return "Current notes:\n" + "\n".join(f"- {note}" for note in self.notes)
def clear_notes(self):
"""Clears all notes."""
self.notes = []
self._save_state()
return "All notes cleared."
# Example Usage (simulating agent interaction):
# notepad_for_user_A = PersistentNotepadTool("userA_session")
# print(notepad_for_user_A.add_note("Remember to buy milk"))
# print(notepad_for_user_A.add_note("Project deadline is Friday"))
# print(notepad_for_user_A.view_notes())
# # If another process or a later session uses the same session_id:
# notepad_for_user_A_later = PersistentNotepadTool("userA_session")
# print(notepad_for_user_A_later.view_notes()) # Should show the previously added notes
# print(notepad_for_user_A_later.clear_notes())
In this PersistentNotepadTool
, the notes
are loaded from and saved to a JSON file specific to the session_id
. This allows the state to persist across different instantiations of the tool, provided the session_id
remains consistent.
A diagram illustrating the interaction flow when using stateful tools, potentially with external storage:
This diagram shows an LLM Agent interacting with a Stateful Tool. The tool manages its internal state and can optionally interact with external storage to persist or retrieve state information before returning a result to the agent.
For stateful tools to function correctly, especially when multiple users or conversations are involved, they need a way to associate state with the correct context. This is typically handled through session identifiers:
PersistentNotepadTool
example, session_id
was used to name the state file. If using a database, the session ID might be a primary key or an indexed column.It's also important for the LLM's prompts to be constructed in a way that signals the ongoing nature of an interaction. Phrases like "add this to my previous list," "what did we discuss earlier about X?" or "continue with the previous plan" can guide the agent to use stateful tools appropriately. The tool's description provided to the LLM should also clearly indicate its stateful nature and how to interact with it over multiple turns.
When building stateful tools, keep these points in mind:
clear_history()
in the ConversationSummarizer
).Effectively managing state and context transforms your tools from simple one-shot executors into components that can support extended, coherent interactions. As you build more complex agents, the ability to create and manage stateful tools will become increasingly valuable, enabling your agents to handle tasks that require memory and continuity. The next chapter on Tool Selection and Orchestration will build upon these ideas, showing how agents can choose and sequence both stateless and stateful tools to achieve complex goals.
Was this section helpful?
© 2025 ApX Machine Learning