Skip to content

Chapter 14 · Session Management: session state and chat history ​

Learning objectives: Distinguish between "chat history" and "session state" mechanisms, master session_id, add_history_to_context, and session_state usage, and understand how tools read/write state via RunContext.

14.1 Three Concepts Easily Confused ​

Multi-turn conversation apps have three "memory" layers. The official docs clarify their boundaries with a single table:

MechanismStoresScopeTypical use
Chat historyMessages and tool calls from previous runssession_idConversation coherence within a session
Session stateApp data managed by code/toolssession_idShopping cart, task list, counter
Memory (user memory)Distilled user factsuser_id (cross-session)Preferences, profile (Chapter 16)

One sentence: history is "what we聊过 (chatted about)", state is "where we are in the process", memory is "who this person is". This chapter focuses on the first two.

14.2 session_id and Chat History ​

session_id is the ID card for a conversation thread. Multiple run() calls with the same session_id belong to the same conversation. For the model to "remember what was said before," two conditions must be met simultaneously:

  1. Configure a database — historical messages must persist (db=SqliteDb(...), covered in detail in the next chapter);
  2. Enable history injection — add_history_to_context=True.
python
# chat_history.py —— Multi-turn conversation memory
import os
from agno.agent import Agent
from agno.db.sqlite import SqliteDb
from agno.models.openai import OpenAIChat

agent = Agent(
    model=OpenAIChat(
        id="deepseek-chat",
        api_key=os.getenv("DEEPSEEK_API_KEY"),
        base_url="https://api.deepseek.com/v1",
    ),
    db=SqliteDb(db_file="tmp/agent.db"),   # Persist session runs
    add_history_to_context=True,           # Inject history messages on each run
    num_history_runs=3,                    # Carry only the most recent 3 turns
    markdown=True,
)

agent.print_response("My name is Xiaogao, and I'm preparing for a FastAPI interview", session_id="chat_001")
agent.print_response("What's my name? What am I preparing for?", session_id="chat_001")  # Can answer correctly
agent.print_response("What's my name?", session_id="chat_002")               # Different session: cannot answer

History injection comes at a cost: every turn shoves historical messages into the context, and tokens grow with conversation length. The official docs provide three control valves:

ParameterDescription
num_history_runsNumber of most recent runs to inject (default 3)
num_history_messagesCap by message count (mutually exclusive with above; when both set, num_history_runs takes precedence)
max_tool_calls_from_historyLimit the number of tool-call messages from history

A demand-fetch pattern needs just one line of config:

python
# on_demand_history.py —— Let the model decide when to look back at history
import os
from agno.agent import Agent
from agno.db.sqlite import SqliteDb
from agno.models.openai import OpenAIChat

agent = Agent(
    model=OpenAIChat(
        id="deepseek-chat",
        api_key=os.getenv("DEEPSEEK_API_KEY"),
        base_url="https://api.deepseek.com/v1",
    ),
    db=SqliteDb(db_file="tmp/agent.db"),
    read_chat_history=True,   # Model gets a get_chat_history() tool
    # Note: without add_history_to_context=True, normal questions don't carry history
)

agent.print_response("Did we agree on any secret codes earlier?", session_id="chat_001")
# Model judges it needs context -> calls get_chat_history() -> answers based on the result

This pattern has a narrow适用面: suitable for audit and analysis scenarios (most questions don't depend on prior context, occasionally need to翻旧账); for chatty scenarios where every sentence has references, turning on add_history_to_context directly actually has lower latency — on-demand lookup costs an extra tool-call round.

Another money-saving approach

If most questions don't depend on prior context, skip history injection and use read_chat_history=True instead — this gives the model a get_chat_history() tool so it can fetch history on demand rather than carrying the full history every turn.

14.3 Session State: Giving the Agent a "Blackboard" ​

session_state is a dict attached to the session, initialized via Agent(session_state={...}). Its essential difference from chat history: state is read/written by your code (usually tools), not naturally accumulated by model messages.

python
# session_state_basic.py —— Official shopping list example adapted for third-party models
import os
from agno.agent import Agent
from agno.db.sqlite import SqliteDb
from agno.models.openai import OpenAIChat
from agno.run import RunContext


def add_item(run_context: RunContext, item: str) -> str:
    """Add an item to the shopping list.

    Args:
        item: The item to add to the shopping list.
    """
    # run_context.session_state is the current session's state dict
    if run_context.session_state is None:
        run_context.session_state = {}
    run_context.session_state["shopping_list"].append(item)
    return f"The shopping list is now {run_context.session_state['shopping_list']}"


agent = Agent(
    model=OpenAIChat(
        id="deepseek-chat",
        api_key=os.getenv("DEEPSEEK_API_KEY"),
        base_url="https://api.deepseek.com/v1",
    ),
    db=SqliteDb(db_file="tmp/agent.db"),
    session_state={"shopping_list": []},      # Initial state for a new session
    tools=[add_item],
    # instructions can reference state with {key}; substituted at injection time
    instructions="Current state (shopping list) is: {shopping_list}",
    markdown=True,
)

agent.print_response("Add milk, eggs, and bread to my shopping list", session_id="s_100")
print(f"Final session state: {agent.get_session_state()}")   # Read from code side

Three key mechanisms:

  1. RunContext injection: after declaring a run_context: RunContext parameter in a tool function, the framework automatically passes it at execution time, and this parameter does not appear in the schema sent to the model (the setup from Chapter 7 pays off here);
  2. Automatic persistence: modifications to run_context.session_state are written to the database along with the session; the next run() with the same session_id loads them automatically;
  3. Instructions template: placeholders like {shopping_list} are replaced with the actual state value at injection time, letting the model "see" the current state.

14.4 Multi-Session Isolation Experiment ​

The best way to understand state scope is to run an isolation experiment yourself:

python
# state_isolation.py —— Verify state is isolated per session
import os
from agno.agent import Agent
from agno.db.sqlite import SqliteDb
from agno.models.openai import OpenAIChat
from agno.run import RunContext


def set_counter(run_context: RunContext, value: int) -> str:
    """Set the counter to a value.

    Args:
        value: New counter value.
    """
    run_context.session_state["counter"] = value
    return f"counter = {value}"


agent = Agent(
    model=OpenAIChat(
        id="deepseek-chat",
        api_key=os.getenv("DEEPSEEK_API_KEY"),
        base_url="https://api.deepseek.com/v1",
    ),
    db=SqliteDb(db_file="tmp/agent.db"),
    session_state={"counter": 0},
    tools=[set_counter],
    instructions="The current value of counter is: {counter}",
)

agent.print_response("Set the counter to 42", session_id="session_A")
agent.print_response("What is the counter now?", session_id="session_A")  # 42
agent.print_response("What is the counter now?", session_id="session_B")  # 0 (initial value)

State between session_A and session_B is invisible to each other — this is the isolation foundation for "one session per user / per ticket." Additionally, you can temporarily pass session_state at run() time to override the default (higher priority than Agent constructor args); enable_agentic_state=True further lets the model update state itself via a JSON Patch protocol, suitable for letting Agents autonomously maintain complex task progress.

14.5 Selection Guide: When to Use History vs. State ​

  • Conversational tone, coreference resolution ("How much is it?" "Expand on the second point you mentioned") → chat history;
  • Business progress (shopping cart contents, approval stage, retry count) → session state;
  • Cross-session user preferences ("This user prefers concise replies") → memory (Chapter 16).

A standard combo for a customer-service Agent: memory records client communication preferences, history maintains the current ticket's conversation coherence, state stores ticket processing status. Each plays its own role and none is interchangeable.

Chapter Summary ​

  • session_id delimits a conversation thread; history/state scope by it, memory scope by user_id;
  • Chat memory = database + add_history_to_context=True; token budget controlled via num_history_runs / num_history_messages, or switch to read_chat_history=True for on-demand lookup;
  • session_state is read/written by code (tools) via run_context.session_state; modifications auto-persist to the database; state is referenced in instructions with {key};
  • State is naturally isolated per session — the cornerstone for building multi-user / multi-ticket applications.

🧪 随堂测验

点击你认为正确的选项。答错时会展示正确答案与原因解析。

1. What are the minimum two configs needed for a model to remember context across multi-turn conversations?

2. What happens when both num_history_runs and num_history_messages are set?

3. What happens after modifying run_context.session_state in a tool function?

4. In instructions, "Current list: {shopping_list}" — when does this placeholder take effect?

🛠️ Hands-On Exercises ​

  1. Extend the shopping list from 14.3 with a remove_item(item: str) tool; test whether "remove eggs" correctly deletes it, and observe the real-time changes of {shopping_list} in instructions.
  2. Construct a dialogue of 10+ turns; run it with num_history_runs=1 and =5 respectively, and compare the token usage difference in response.metrics.
  3. Build a "multi-turn ticket-booking assistant": use session_state to store origin / destination / date fields, and only call book_ticket() when all three are present (hint: validate state inside the tool).

After completing the hands-on exercises, proceed to Chapter 15: Storage — Session Persistence.