Building a Persistent Task Manager Agent with Agno: A Step-by-Step Guide

How I built a stateful, persistent AI agent to manage my tasks using Agno and SQLite

Why I Wanted a Smarter Task List

Like many developers, I've tried dozens of to-do apps. But what I really wanted was an AI-powered task manager that could remember my tasks, update their progress, and keep everything persistent—even if I closed the app or rebooted my machine. That's when I turned to Agno, a framework for building stateful, persistent AI agents.

In this post, I'll walk you through how I built a persistent task manager AI agent with Agno, step by step. We'll cover everything from the basics of Agno's session state to wiring up SQLite for real persistence. By the end, you'll have a working, interactive stateful and persistent AI agent that manages your tasks and remembers them across sessions.
 
Let's take a look at the final agent in action:

Step 1: Setting Up Your Project

First, let's get the basics out of the way. You'll need Python and a few packages:
pip install agno openai sqlalchemy
  • agno is the agent framework.
  • openai gives us access to powerful language models.
  • sqlalchemy is required for Agno's SQLite storage backend.

Step 2: Understanding Agno's Agent and Session State

Before we dive into code, let's talk about how Agno manages state.

Agents in Agno are smart, interactive entities that can use tools, remember information, and hold conversations. The magic behind their memory is the session_state—a Python dictionary that persists across runs (if you use storage).
  • Session State: This is where your agent stores any data it needs to remember, like a list of tasks.
  • Session ID: Each session has a unique ID. If you use the same session ID, Agno will restore the state from storage.
  • Storage: By default, state is in-memory. To persist it, you need to add a storage backend (like SQLite).

Step 3: Defining the Task Management Tools

Let's start by defining the core functions ("tools") our agent will use to manage tasks. Each tool is just a Python function that takes the agent and some arguments, modifies the session state, and returns a message.
def add_task(agent, description):
    """Add a new task with 0% completion."""
    if agent.session_state is None or "tasks" not in agent.session_state:
        agent.session_state = {"tasks": []}
    task = {"description": description, "completion": 0}
    agent.session_state["tasks"].append(task)
    return f"Added task: '{description}' (0% complete)"

def update_task(agent, index, completion):
    """Update the completion rate of a task by index (1-based)."""
    tasks = agent.session_state["tasks"]
    if 1 <= index <= len(tasks):
        tasks[index - 1]["completion"] = max(0, min(100, completion))
        return f"Updated task {index}: '{tasks[index - 1]['description']}' to {tasks[index - 1]['completion']}% complete"
    return f"Task {index} does not exist."

def remove_task(agent, index):
    """Remove a task by index (1-based)."""
    tasks = agent.session_state["tasks"]
    if 1 <= index <= len(tasks):
        removed = tasks.pop(index - 1)
        return f"Removed task: '{removed['description']}'"
    return f"Task {index} does not exist."

def list_tasks(agent):
    """List all tasks with their completion rates."""
    tasks = agent.session_state["tasks"]
    if not tasks:
        return "The task list is empty."
    return "Current tasks:\n" + "\n".join(
        [f"{i+1}. {t['description']} - {t['completion']}%" for i, t in enumerate(tasks)]
    )
What's happening here?
  • We make sure the tasks list exists in the session state.
  • Each tool modifies or reads from the session state.
  • The agent will call these tools in response to user commands.

Step 4: Adding Persistence with SQLite

By default, Agno agents forget everything when you restart the script. To fix that, we'll use Agno's built-in SQLite storage backend.
from agno.storage.agent.sqlite import SqliteAgentStorage

storage = SqliteAgentStorage(table_name="agent_sessions", db_file="tmp/agent_storage.db")
This tells Agno to store all session data in a local SQLite database. Now, your agent's memory will survive restarts!

Step 5: Managing Session IDs for True Persistence

 
To make sure we always restore the same session, we need to save and load the session ID. Here's a simple way to do it:
SESSION_FILE = "tmp/agent_session_id.txt"

def save_session_id(session_id):
    os.makedirs(os.path.dirname(SESSION_FILE), exist_ok=True)
    with open(SESSION_FILE, "w") as f:
        f.write(session_id)

def load_session_id():
    if os.path.exists(SESSION_FILE):
        with open(SESSION_FILE, "r") as f:
            return f.read().strip()
    return None
  • On first run, we let Agno generate a session ID, then save it.
  • On later runs, we load and reuse the same session ID, so the state is restored.

Step 6: Creating the Agent

Now, let's put it all together and create the agent. We'll only set the initial session state if this is a new session.
from agno.agent import Agent
from agno.models.openai import OpenAIChat
from textwrap import dedent

session_id = load_session_id()

agent_kwargs = dict(
    model=OpenAIChat(id="gpt-4o-mini"),
    session_id=session_id,
    tools=[add_task, update_task, remove_task, list_tasks],
    instructions=dedent("""
        You are a task manager agent. 
        You keep track of a list of tasks, each with a description and a completion rate (in percent).
        Users can add new tasks, update completion rates, remove tasks, and list all tasks.
        Current tasks: {tasks}
    """),
    show_tool_calls=True,
    add_state_in_messages=True,
    markdown=True,
    storage=storage,
)
if session_id is None:
    agent_kwargs["session_state"] = {"tasks": []}

agent = Agent(**agent_kwargs)
  • If there's no previous session, we start with an empty task list.
  • Otherwise, Agno restores the state from the database.

Step 7: Making the Agent Interactive

Let's make the agent interactive so you can add, update, remove, and list tasks in a loop. We'll also print the current task list after each command.
def print_current_tasks():
    print("\n--- Current Task List ---")
    print(list_tasks(agent))
    print("------------------------\n")

if __name__ == "__main__":
    # Force Agno to load session state from storage (and generate session_id if new)
    agent.print_response("list all tasks", stream=False)
    # Save the session_id if it's new and not already saved
    if agent.session_id and (not os.path.exists(SESSION_FILE) or not load_session_id()):
        save_session_id(agent.session_id)
    print_current_tasks()
    while True:
        user_input = input("Enter your command (or type 'exit' to quit): ")
        if user_input.strip().lower() in {"exit", "quit"}:
            print("Exiting. Your tasks are saved!")
            break
        agent.print_response(user_input, stream=True)
        print_current_tasks()
        # Save the session_id if it's new and not already saved
        if agent.session_id and (not os.path.exists(SESSION_FILE) or not load_session_id()):
            save_session_id(agent.session_id)
  • On startup, we force the agent to load its state and save the session ID if needed.
  • The user can interactively manage tasks, and the state is always up to date and persistent.

Step 8: The Complete Code

Here's the full implementation, ready to use:
 
import os
from textwrap import dedent
from agno.agent import Agent
from agno.models.openai import OpenAIChat
from agno.storage.agent.sqlite import SqliteAgentStorage

SESSION_FILE = "tmp/agent_session_id.txt"

def save_session_id(session_id):
    os.makedirs(os.path.dirname(SESSION_FILE), exist_ok=True)
    with open(SESSION_FILE, "w") as f:
        f.write(session_id)

def load_session_id():
    if os.path.exists(SESSION_FILE):
        with open(SESSION_FILE, "r") as f:
            return f.read().strip()
    return None

def ensure_session_state(agent):
    if agent.session_state is None:
        agent.session_state = {"tasks": []}
    elif "tasks" not in agent.session_state:
        agent.session_state["tasks"] = []

def add_task(agent: Agent, description: str) -> str:
    ensure_session_state(agent)
    task = {"description": description, "completion": 0}
    agent.session_state["tasks"].append(task)
    return f"Added task: '{description}' (0% complete)"

def update_task(agent: Agent, index: int, completion: int) -> str:
    ensure_session_state(agent)
    tasks = agent.session_state["tasks"]
    if 1 <= index <= len(tasks):
        tasks[index - 1]["completion"] = max(0, min(100, completion))
        return f"Updated task {index}: '{tasks[index - 1]['description']}' to {tasks[index - 1]['completion']}% complete"
    return f"Task {index} does not exist."

def remove_task(agent: Agent, index: int) -> str:
    ensure_session_state(agent)
    tasks = agent.session_state["tasks"]
    if 1 <= index <= len(tasks):
        removed = tasks.pop(index - 1)
        return f"Removed task: '{removed['description']}'"
    return f"Task {index} does not exist."

def list_tasks(agent: Agent) -> str:
    ensure_session_state(agent)
    tasks = agent.session_state["tasks"]
    if not tasks:
        return "The task list is empty."
    return "Current tasks:\n" + "\n".join(
        [f"{i+1}. {t['description']} - {t['completion']}%" for i, t in enumerate(tasks)]
    )

storage = SqliteAgentStorage(table_name="agent_sessions", db_file="tmp/agent_storage.db")
session_id = load_session_id()
agent_kwargs = dict(
    model=OpenAIChat(id="gpt-4o-mini"),
    session_id=session_id,
    tools=[add_task, update_task, remove_task, list_tasks],
    instructions=dedent("""
        You are a task manager agent. 
        You keep track of a list of tasks, each with a description and a completion rate (in percent).
        Users can add new tasks, update completion rates, remove tasks, and list all tasks.
        Current tasks: {tasks}
    """),
    show_tool_calls=True,
    add_state_in_messages=True,
    markdown=True,
    storage=storage,
)
if session_id is None:
    agent_kwargs["session_state"] = {"tasks": []}

agent = Agent(**agent_kwargs)

def print_current_tasks():
    print("\n--- Current Task List ---")
    print(list_tasks(agent))
    print("------------------------\n")

if __name__ == "__main__":
    agent.print_response("list all tasks", stream=False)
    if agent.session_id and (not os.path.exists(SESSION_FILE) or not load_session_id()):
        save_session_id(agent.session_id)
    print_current_tasks()
    while True:
        user_input = input("Enter your command (or type 'exit' to quit): ")
        if user_input.strip().lower() in {"exit", "quit"}:
            print("Exiting. Your tasks are saved!")
            break
        agent.print_response(user_input, stream=True)
        print_current_tasks()
        if agent.session_id and (not os.path.exists(SESSION_FILE) or not load_session_id()):
            save_session_id(agent.session_id)

Final Thoughts: What You've Learned

  • How to use Agno's session state to manage persistent data
  • How to wire up SQLite for real persistence
  • How to manage session IDs for restoring state
  • How to build and interact with a stateful, persistent AI agent

I hope this guide helps you build your own persistent AI-powered agents!