By Sagar Shankaran, Founder of CallSphere
Create an AI study planner agent that builds personalized schedules using spaced repetition algorithms, optimizes review timing, and adapts to individual learning pace and availability.
Key takeaways
Research in cognitive science consistently shows that distributing practice over time produces dramatically better retention than massing it into a single session. The spacing effect, first documented by Hermann Ebbinghaus in 1885, demonstrates that memories decay exponentially but each successful review extends the retention interval. A study planner agent leverages this principle to schedule reviews at optimal intervals — just before the student would forget.
The challenge is that optimal intervals vary per student and per topic. An AI agent can personalize these intervals by tracking individual performance and adjusting the schedule dynamically.
Start with a mathematical model of memory decay that the agent uses to predict when a student will forget a given topic:
Hear it before you finish reading
Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.
flowchart LR
CALLER(["Client"])
subgraph TEL["Telephony"]
SIP["Twilio SIP and PSTN"]
end
subgraph BRAIN["Salon AI Agent"]
STT["Streaming STT<br/>Deepgram or Whisper"]
NLU{"Intent and<br/>Entity Extraction"}
TOOLS["Tool Calls"]
TTS["Streaming TTS<br/>ElevenLabs or Rime"]
end
subgraph DATA["Live Data Plane"]
CRM[("CRM and Notes")]
CAL[("Calendar and<br/>Schedule")]
KB[("Knowledge Base<br/>and Policies")]
end
subgraph OUT["Outcomes"]
O1(["Appointment booked"])
O2(["Reschedule completed"])
O3(["Stylist handoff"])
end
CALLER --> SIP --> STT --> NLU
NLU -->|Lookup| TOOLS
TOOLS <--> CRM
TOOLS <--> CAL
TOOLS <--> KB
NLU --> TTS --> SIP --> CALLER
NLU -->|Resolved| O1
NLU -->|Schedule| O2
NLU -->|Escalate| O3
style CALLER fill:#f1f5f9,stroke:#64748b,color:#0f172a
style NLU fill:#4f46e5,stroke:#4338ca,color:#fff
style O1 fill:#059669,stroke:#047857,color:#fff
style O2 fill:#0ea5e9,stroke:#0369a1,color:#fff
style O3 fill:#f59e0b,stroke:#d97706,color:#1f2937
import math
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from typing import Optional
@dataclass
class StudyItem:
item_id: str
topic: str
subject: str
difficulty: float = 0.3 # 0.0 = easy, 1.0 = hard
stability: float = 1.0 # days until 90% recall probability
last_reviewed: Optional[datetime] = None
review_count: int = 0
easiness_factor: float = 2.5 # SM-2 style factor
consecutive_correct: int = 0
def recall_probability(self, now: Optional[datetime] = None) -> float:
"""Estimate probability the student remembers this item."""
if self.last_reviewed is None:
return 0.0
now = now or datetime.now()
days_elapsed = (now - self.last_reviewed).total_seconds() / 86400
# Exponential decay model
return math.exp(-days_elapsed / self.stability)
def next_review_date(self, target_recall: float = 0.85) -> datetime:
"""Calculate when recall probability drops to the target."""
if self.last_reviewed is None:
return datetime.now()
# Solve: target = exp(-days / stability) for days
days_until_target = -self.stability * math.log(target_recall)
return self.last_reviewed + timedelta(days=days_until_target)
def update_after_review(self, quality: int):
"""Update stability and easiness after a review.
quality: 0-5 scale (SM-2 convention)
0-2 = incorrect/difficult, 3 = correct with hesitation,
4 = correct, 5 = perfect
"""
self.review_count += 1
self.last_reviewed = datetime.now()
if quality >= 3:
self.consecutive_correct += 1
# Increase stability (longer intervals)
if self.consecutive_correct == 1:
self.stability = 1.0
elif self.consecutive_correct == 2:
self.stability = 6.0
else:
self.stability *= self.easiness_factor
else:
self.consecutive_correct = 0
self.stability = 1.0 # Reset to short interval
# Update easiness factor (SM-2 formula)
self.easiness_factor = max(
1.3,
self.easiness_factor
+ 0.1 - (5 - quality) * (0.08 + (5 - quality) * 0.02),
)
Given a student's available time slots and a set of items due for review, the agent needs to build an optimal daily schedule:
@dataclass
class TimeSlot:
start: datetime
end: datetime
energy_level: str = "medium" # low, medium, high
@property
def duration_minutes(self) -> int:
return int((self.end - self.start).total_seconds() / 60)
@dataclass
class ScheduleEntry:
item: StudyItem
scheduled_time: datetime
duration_minutes: int
priority: float
@dataclass
class StudySchedule:
date: datetime
entries: list[ScheduleEntry] = field(default_factory=list)
total_minutes: int = 0
def build_daily_schedule(
items: list[StudyItem],
available_slots: list[TimeSlot],
max_new_items: int = 5,
minutes_per_review: int = 5,
minutes_per_new: int = 15,
) -> StudySchedule:
"""Build an optimized study schedule for today."""
now = datetime.now()
schedule = StudySchedule(date=now)
# Separate overdue reviews from new items
overdue = [
item for item in items
if item.last_reviewed and item.recall_probability(now) < 0.85
]
new_items = [
item for item in items if item.last_reviewed is None
][:max_new_items]
# Sort overdue by urgency (lowest recall first)
overdue.sort(key=lambda i: i.recall_probability(now))
# Priority: overdue reviews first, then new items
# High-energy slots get difficult or new material
available_slots.sort(
key=lambda s: {"high": 0, "medium": 1, "low": 2}[s.energy_level]
)
slot_idx = 0
remaining_minutes = (
available_slots[0].duration_minutes if available_slots else 0
)
for item in overdue:
if slot_idx >= len(available_slots):
break
schedule.entries.append(ScheduleEntry(
item=item,
scheduled_time=available_slots[slot_idx].start,
duration_minutes=minutes_per_review,
priority=1.0 - item.recall_probability(now),
))
remaining_minutes -= minutes_per_review
schedule.total_minutes += minutes_per_review
if remaining_minutes <= 0:
slot_idx += 1
if slot_idx < len(available_slots):
remaining_minutes = (
available_slots[slot_idx].duration_minutes
)
# Add new material in high-energy slots
for item in new_items:
if slot_idx >= len(available_slots):
break
schedule.entries.append(ScheduleEntry(
item=item,
scheduled_time=available_slots[slot_idx].start,
duration_minutes=minutes_per_new,
priority=0.5,
))
remaining_minutes -= minutes_per_new
schedule.total_minutes += minutes_per_new
if remaining_minutes <= 0:
slot_idx += 1
if slot_idx < len(available_slots):
remaining_minutes = (
available_slots[slot_idx].duration_minutes
)
return schedule
The agent uses these scheduling algorithms as tools and communicates the plan in natural language:
from agents import Agent, Runner, function_tool
import json
@function_tool
def get_todays_schedule(
student_id: str,
available_hours: float,
energy_pattern: str,
) -> str:
"""Generate today's optimized study schedule."""
# In production, load from database
items = load_student_items(student_id)
slots = parse_availability(available_hours, energy_pattern)
schedule = build_daily_schedule(items, slots)
return json.dumps({
"total_minutes": schedule.total_minutes,
"review_count": len([
e for e in schedule.entries if e.item.review_count > 0
]),
"new_count": len([
e for e in schedule.entries if e.item.review_count == 0
]),
"entries": [
{
"topic": e.item.topic,
"type": "review" if e.item.review_count > 0 else "new",
"duration": e.duration_minutes,
"recall_probability": f"{e.item.recall_probability():.0%}",
}
for e in schedule.entries
],
})
study_planner = Agent(
name="Study Planner",
instructions="""You are a study planning assistant that creates
personalized schedules based on spaced repetition science.
When presenting a schedule:
1. Start with a brief overview of what is due today and why
2. List each study block with topic, duration, and purpose
3. Explain WHY certain topics are prioritized (low recall probability)
4. Suggest the best order based on energy levels
5. End with encouragement and what to expect tomorrow
Be specific about timing. Instead of 'review math', say 'review
quadratic equations — your recall has dropped to 40%, so this
needs attention today to prevent forgetting.'""",
tools=[get_todays_schedule],
)
The easiness factor in the SM-2 algorithm increases for items the student consistently rates highly, which extends the stability value exponentially. An item rated "perfect" five times in a row might not appear for review for several months. The system naturally concentrates review time on difficult material while letting easy items fade into long-interval maintenance reviews.
Still reading? Stop comparing — try CallSphere live.
CallSphere ships complete AI voice agents per industry — 14 tools for healthcare, 10 agents for real estate, 4 specialists for salons. See how it actually handles a call before you book a demo.
When a student returns after a gap, the recall probability for overdue items will have dropped significantly. The scheduler prioritizes these by urgency — items with the lowest recall probability appear first. However, it also respects the student's available time, so it will not schedule four hours of reviews just because everything is overdue. Instead, it triages the most critical items and spreads the backlog across several days.
Yes. You extend the scheduling logic with a deadline parameter that overrides the normal spaced repetition intervals. When an exam is approaching, the scheduler increases review frequency for exam-relevant topics and front-loads new material that needs to be learned before the deadline, while still maintaining minimum review intervals for retention.
#SpacedRepetition #StudyPlanning #EducationAI #Python #LearningScience #AgenticAI #LearnAI #AIEngineering

Written by
Sagar Shankaran· Founder, CallSphere
LinkedInSagar Shankaran is the founder of CallSphere, where he builds production AI voice and chat agents deployed across healthcare, hospitality, real estate, and home services. He writes about agentic AI, LLM engineering, and shipping voice agents that handle real calls in production.
See how AI voice agents work for your industry. Live demo available -- no signup required.
Step-by-step build of a working agent with the OpenAI Agents SDK — Agent class, tools, handoffs, tracing — plus an eval pipeline that catches regressions before merge.
Smolagents lets agents write Python instead of JSON. Why code-as-action reduces tool errors and where the security trade-offs are for production deployments.
Modal turns a Python function into autoscaling serverless compute with optional GPU. Deploy a LiveKit Agent with one command and get pay-per-second billing.
Pydantic AI's April release tightens the typed-agent loop and adds structured tool definitions. Why type-safe agents reduce production bugs and speed iteration.
Index a knowledge base with text-embedding-3-large into ChromaDB, expose a retrieve tool to your voice agent, and ground every answer in real documents — full Python tutorial.
Shrink an AI voice agent image from 950MB to 80MB with a Python 3.13 multi-stage build, uv for deps, and gcr.io/distroless/python3 nonroot. Real Dockerfile + benchmarks.
© 2026 CallSphere Inc. All rights reserved.
Made within San Francisco