How Agno's Agent Class Manages Session State and User Context Across Runs

Agno's Agent class manages session state and user context across runs by separating the runnable agent logic from persistent session data, providing explicit attributes like session_id and session_state, and exposing a public API that allows both developers and LLMs to read and mutate state stored in the database.

The agno-agi/agno repository provides a robust framework for building AI agents that maintain context across multiple interactions. Understanding how the Agno Agent session state system works is crucial for developing applications that require memory persistence and user-specific context management. The architecture cleanly separates the Agent's execution logic from the Session's data layer, enabling both programmatic and agentic state manipulation.

Core Session State Concepts

The session state system is built around several key attributes defined in libs/agno/agno/agent/agent.py that control how data persists and flows through the agent.

Session Identification with session_id

Every agent interaction occurs within a session identified by a UUID. In libs/agno/agno/agent/agent.py (lines 73-84), the session_id attribute stores this identifier. If omitted during agent initialization, the system auto-generates a unique session ID, ensuring that stateless runs remain isolated while stateful runs can retrieve their historical context from the database.

User Context Storage via session_state

The session_state attribute (lines 83-86) holds an arbitrary JSON-serializable dictionary that users can supply when calling run(). This dictionary merges with any persisted state retrieved from the database, allowing applications to inject user preferences, counters, or metadata into the agent's context. The merging behavior respects the overwrite_db_session_state flag (lines 89-91), which when set to True, replaces the database state entirely rather than performing a shallow merge.

Context Injection with add_session_state_to_context

When the add_session_state_to_context attribute is set to True (lines 84-86), the current session_state dictionary is automatically serialized and injected into the system message or context. This makes the session data visible to the LLM during inference, enabling the model to reference previous user preferences or application state without requiring explicit retrieval logic in the prompt.

Agentic State Management via enable_agentic_state

The enable_agentic_state attribute (lines 87-89) grants the LLM itself the ability to modify session state. When enabled, the agent exposes a tool named _session.update_session_state that the model can invoke during a run. This tool delegates to Agent.update_session_state, writing changes directly to the database and updating the cached session object, effectively allowing the agent to "remember" facts or update counters autonomously.

The Session State API

The Agent class exposes a public API for state management that wraps low-level utilities defined in libs/agno/agno/agent/_session.py. These methods handle both synchronous and asynchronous execution patterns.

Reading State with get_session_state

To retrieve the current persisted state, the agent provides:

def get_session_state(self, session_id: Optional[str] = None) -> Dict[str, Any]:
    session_id = session_id or self.session_id
    if session_id is None:
        raise Exception("Session ID is not set")
    return get_session_state_util(self, session_id=session_id)

Source: libs/agno/agno/agent/_session.py (lines 73-86)

The async variant aget_session_state provides non-blocking access for asynchronous applications.

Updating State with update_session_state

Programmatic updates merge new data into the existing state:

def update_session_state(
    self, 
    session_state_updates: Dict[str, Any], 
    session_id: Optional[str] = None
) -> str:
    # Merges updates into DB state (or overwrites if flag set)

    # Returns new version ID

    ...

This method handles the logic for merging versus overwriting based on the overwrite_db_session_state configuration. When called by the LLM via the agentic state tool, it updates the database and refreshes the _cached_session object if session caching is enabled.

Persisting Sessions with save_session

To persist the full session object including runs and metadata:

def save_session(self, session: AgentSession) -> None:
    # Persists to database via agno.session models

    ...

The get_session and aget_session methods retrieve full AgentSession objects, applying the in-memory cache (_cached_session) when cache_session=True is set during agent initialization (lines 134-136 in agent.py).

How State Flows Across Runs

The session state lifecycle follows a predictable path through each agent invocation:

  1. Run Initialization – When Agent.run() is called, it accepts an optional session_state dictionary from the user.

  2. Session Resolution – The _session.get_session utility fetches an existing AgentSession from the database or creates a new one using the session_id. If cache_session=True, the result is stored in self._cached_session to speed up subsequent lookups.

  3. State Merging – The provided session_state merges with the persisted state from the database. The merge strategy depends on overwrite_db_session_state: when False (default), it performs a shallow merge; when True, the supplied state replaces the database entry entirely.

  4. Context Injection – If add_session_state_to_context=True, the merged session_state is serialized and injected into the system message via _messages.get_system_message, making the data available to the LLM.

  5. Agentic Updates – During inference, if enable_agentic_state=True, the model may invoke the _session.update_session_state tool to modify state mid-run. This writes immediately to the database and updates the cache.

  6. Persistence – Upon completion, Agent.save_session persists the updated AgentSession (including any new runs and the final session_state) to the database, ensuring the next run retrieves the current context.

Practical Implementation Example

The following example demonstrates a counter that persists across multiple agent invocations using the session state system:

from agno import Agent, Model

# Initialize agent with session state enabled

agent = Agent(
    model=Model("gpt-4o-mini"),
    session_id="demo-123",
    add_session_state_to_context=True,
    enable_agentic_state=True
)

# First call initializes the counter

agent.run({"counter": 0})

# The system prompt now contains: {"counter": 0}

# Model can update state via tool call:

# _session.update_session_state({"counter": 5})

# Second call automatically merges with persisted state

agent.run({"extra": "info"})

# System prompt now contains: {"counter": 5, "extra": "info"}

Sources: Agent constructor arguments (libs/agno/agno/agent/agent.py lines 70-90); session-state helpers (libs/agno/agno/agent/_session.py lines 73-102).

Summary

  • Session Isolation: Every Agent instance uses a session_id (UUID) to isolate context between conversations, auto-generating one if not provided.
  • Explicit State Management: The session_state dictionary provides a JSON-serializable container for user context that merges with database-persisted state on each run.
  • LLM Visibility: Setting add_session_state_to_context=True injects the state dictionary into the system message, allowing the model to reference user preferences and application data.
  • Agentic Mutation: With enable_agentic_state=True, the LLM can modify its own session state mid-run via the _session.update_session_state tool, enabling autonomous memory updates.
  • Persistence Layer: The _session.py utilities handle database operations, while optional in-memory caching (cache_session=True) improves performance for repeated lookups.

Frequently Asked Questions

How does Agno handle session state when the same session_id is used across different Agent instances?

When multiple Agent instances share the same session_id, they access the same underlying database record. Each instance retrieves the current session_state from the database at the start of run(), merges it with any locally provided state, and persists changes at the end. If cache_session=True, each instance maintains its own in-memory cache (_cached_session), so simultaneous modifications from different processes could lead to stale reads unless the database is the source of truth.

Can the LLM modify session state without explicit user intervention?

Yes, when enable_agentic_state=True, the agent exposes a tool named _session.update_session_state that the LLM can invoke during inference. This tool delegates to Agent.update_session_state, which writes changes directly to the database and updates the cached session object. This allows the model to autonomously "remember" facts, update counters, or modify user preferences based on conversation flow without requiring the user to manually pass state in subsequent calls.

What is the difference between session_state and the Agent's memory or storage systems?

The session_state is a specific JSON-serializable dictionary designed for structured user context and application data that persists across runs within a single session. It is distinct from the agent's memory (which typically handles conversation history and RAG contexts) and storage (which refers to the database backend where sessions are persisted). While memory systems handle what the model "remembers" conversationally, session_state provides a programmatic key-value store that developers can explicitly control and inject into the model's context via add_session_state_to_context.

How does state merging work when providing session_state to run()?

When calling agent.run(session_state={"key": "value"}), the provided dictionary merges with the existing state retrieved from the database. By default (overwrite_db_session_state=False), this performs a shallow merge where top-level keys in the provided dictionary overwrite those in the stored state, but nested structures may require careful handling. If overwrite_db_session_state=True, the provided session_state completely replaces the database entry, effectively resetting the session context to the provided values. This merge occurs during the session resolution phase at the start of each run, ensuring the agent operates with the most current combined state.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →