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, andsession_stateusage, and understand how tools read/write state viaRunContext.
14.1 Three Concepts Easily Confused
Multi-turn conversation apps have three "memory" layers. The official docs clarify their boundaries with a single table:
| Mechanism | Stores | Scope | Typical use |
|---|---|---|---|
| Chat history | Messages and tool calls from previous runs | session_id | Conversation coherence within a session |
| Session state | App data managed by code/tools | session_id | Shopping cart, task list, counter |
| Memory (user memory) | Distilled user facts | user_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:
- Configure a database — historical messages must persist (
db=SqliteDb(...), covered in detail in the next chapter); - Enable history injection —
add_history_to_context=True.
# 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 answerHistory 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:
| Parameter | Description |
|---|---|
num_history_runs | Number of most recent runs to inject (default 3) |
num_history_messages | Cap by message count (mutually exclusive with above; when both set, num_history_runs takes precedence) |
max_tool_calls_from_history | Limit the number of tool-call messages from history |
A demand-fetch pattern needs just one line of config:
# 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 resultThis 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.
# 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 sideThree key mechanisms:
RunContextinjection: after declaring arun_context: RunContextparameter 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);- Automatic persistence: modifications to
run_context.session_stateare written to the database along with the session; the nextrun()with the samesession_idloads them automatically; - 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:
# 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_iddelimits a conversation thread; history/state scope by it, memory scope byuser_id;- Chat memory = database +
add_history_to_context=True; token budget controlled vianum_history_runs/num_history_messages, or switch toread_chat_history=Truefor on-demand lookup; session_stateis read/written by code (tools) viarun_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
- 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. - Construct a dialogue of 10+ turns; run it with
num_history_runs=1and=5respectively, and compare the token usage difference inresponse.metrics. - 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.