StoryAdaptation Agent
| File | lib/agents/adaptationAgent.ts (runAdaptationAgent) |
| Triggered by | app/api/adapt-story/route.ts (on Parent Insights → AI Story tab load) |
| Model | gemini-2.5-pro (Vertex AI) |
What it does
Analyzes the child's last 5 quiz records from Firestore and generates a fully adapted next story configuration — a ready-to-use story prompt calibrated to the child's current reading level, interests, and performance trend.
Outputs:
- Adapted prompt — a 2–3 sentence story prompt that is ready to pass directly to StoryWriter, including vocabulary hints and pacing instructions
- Vocabulary level —
simple/moderate/richmatched to quiz performance - Page count — 4 (struggling), 5 (average), or 6 (advanced)
- Focus theme — the central story theme
- Adaptation reason — one sentence explaining why this story was chosen (shown to parent)
- Encouragement — one warm sentence shown to the child before reading
Tools
| Tool | Parameters |
|---|---|
submit_adaptation | adaptedPrompt, vocabularyLevel, pageCount, focusTheme, adaptationReason, encouragement |
Vocabulary / difficulty logic
| Average score (last 5 quizzes) | Vocabulary level | Page count |
|---|---|---|
| < 50% | "simple" — short sentences, common words | 4 pages |
| 50–79% | "moderate" — varied vocabulary | 5 pages |
| ≥ 80% | "rich" — complex sentences, richer vocabulary | 6 pages |
The agent also considers the trend (improving / declining / stable) from the two most recent scores vs. the 5-quiz average.
Prompt crafting rules (agent instruction)
- Look at what topics the child has already read — suggest something new but related
- If they struggled, revisit the topic in a gentler, more engaging way
- If they excelled, push them into a slightly more complex world
- Include vocabulary hints in the prompt: e.g.
"use simple 2-syllable words only"or"introduce 3 new interesting words with context" - Include pacing hints:
"short punchy sentences"or"richer descriptive language"
Prompt structure:
[Story scenario]. [Vocabulary/pacing instruction for the AI]. [Emotional/learning goal].Example — struggling reader:
"A little turtle named Pip finds a magical shell that grants one wish. Use simple short sentences and common everyday words. The story should focus on kindness and making good choices."
Example — advanced reader:
"A young inventor discovers an ancient library hidden beneath the ocean, filled with books written by sea creatures. Use rich descriptive language and introduce words like 'luminescent', 'labyrinth', and 'ancient'. The story should explore curiosity, problem-solving, and the power of knowledge."
Caching — avoid repeat Gemini calls
The adaptation result is cached in Firestore at users/{uid}/adaptations:
- API fetches the most recent cached adaptation and the most recent quiz date
- If
latestQuizAt < cachedAt→ returns cached result immediately (0 Gemini tokens) - If a new quiz was completed since the last adaptation → runs the agent, saves the new result
Only the last 3 quizzed stories are used (not the full history) — this keeps the context small and generation fast (~2–4s vs. 10s+ with full history). The API route fetches the last 30 stories, filters to those with quiz scores, and passes the 3 most recent to the agent. The agent itself uses slice(0, 5) as a further safety cap.
Flow
Parent opens /parent-insights → AI Story tab
│
▼
GET /api/adapt-story { userId, language }
│
├─ Fetch last 30 stories, filter to those with quiz scores → take 3 most recent
│
├─ Fetch most recent adaptation from users/{uid}/adaptations
│
├─ latestQuizAt < cachedAt?
│ │ YES → return cached config (0 Gemini tokens)
│ │
│ ▼ NO
│ StoryAdaptationAgent LLM call
│ └── submit_adaptation({
│ adaptedPrompt: "...",
│ vocabularyLevel: "moderate",
│ pageCount: 5,
│ focusTheme: "ocean adventure",
│ adaptationReason: "...",
│ encouragement: "..."
│ })
│
├─ Save new adaptation to users/{uid}/adaptations
│
▼
Parent Insights → AI Story tab shows AdaptedStoryCard:
- Vocabulary level badge (Beginner / Intermediate / Advanced)
- Page count pill
- Focus theme pill
- Encouragement quote
- Full adapted story prompt
- Adaptation reason (for parent)
- "Start This Story" CTA → /create?prompt=<adaptedPrompt>Input / Output
// Input (QuizRecord[] — last 5 only)
{
storyTitle: string;
storyPrompt: string;
score: number;
total: number;
language: string;
completedAt: string; // ISO string
}
[];
// Output (AdaptedStoryConfig)
{
adaptedPrompt: string; // ready-to-use story prompt
vocabularyLevel: "simple" | "moderate" | "rich";
pageCount: number; // 4 | 5 | 6
focusTheme: string; // e.g. "ocean adventure"
adaptationReason: string; // shown to parent
encouragement: string; // shown to child
}ADK tool loop fix
The submit_adaptation FunctionTool execute returns { ...args, status: "submitted" } — the status field signals to the ADK runner that the tool has completed. Without this, Gemini re-invokes the tool in a loop (observed: 6+ calls before timeout). This fix applies to all three ADK agents: StoryAdaptationAgent, LearningAdvisor, and ParentInsights.
UI — AdaptedStoryCard
Rendered in components/dashboard/AdaptedStoryCard.tsx:
| Element | Content |
|---|---|
| Header | "Story Adapted for {childName}" |
| Subtitle | "Based on N quizzes · AI personalized" |
| Encouragement box | config.encouragement |
| Vocabulary badge | Beginner / Intermediate / Advanced (color-coded) |
| Page count pill | N pages |
| Theme pill | config.focusTheme |
| Suggested story box | config.adaptedPrompt |
| Why this story | config.adaptationReason |
| CTA button | "Start This Story" → /create?prompt=... |
| Refresh button | Re-runs agent, saves new adaptation to Firestore |
Observability span
Span name: agent/StoryAdaptationAgent
| Attribute | Example value |
|---|---|
agent.name | StoryAdaptationAgent |
agent.model | gemini-2.5-pro |
agent.language | en |
agent.avg_score | 76 |
agent.trend | improving |
agent.vocabulary_level | moderate |
agent.page_count | 5 |
agent.duration_ms | 3400 |
Cost tracking
Logged to log/token-usage.jsonl in development:
{
"sessionType": "learning_recommendation",
"agent": "StoryAdaptationAgent",
"model": "gemini-2.5-pro",
"inputTokens": ~750,
"outputTokens": ~200,
"durationMs": 3400,
"language": "en",
"metadata": { "quizCount": 3, "vocabularyLevel": "moderate" }
}