QuizMaster Agent
| File | lib/agents/quizMasterAgent.ts (runQuizMasterAgent) |
| Triggered by | app/api/live-quiz/route.ts |
| Model | gemini-2.5-pro (Vertex AI) |
What it does
Generates quiz questions from a story page. When a child taps "Quiz Time", all 5 questions are generated in parallel (Promise.all) at once — so the child waits once upfront instead of waiting between every question.
Each question includes pre-written encouragement and correction text used for instant feedback without a second LLM call.
Performance optimisations
- Agent cache — one
LlmAgentinstance is cached perlangName:quizTypekey (e.g.English:multiple_choice). The 3 question types reuse cached agents across questions, avoiding Vertex AI cold-start overhead on every call. - TTS non-blocking — after the LLM returns a question, TTS is fired with
.then().catch()so a TTS failure never blocks the quiz result from streaming to the client. - Reduced retries — max 1 retry with 500ms delay (previously 2 retries at 1500ms/3000ms). Combined with the fallback strategy this keeps worst-case failure latency low.
Question types
Rotated by question index to ensure variety across the 5 questions:
| Index | Type |
|---|---|
| 0 | multiple_choice |
| 1 | true_false |
| 2 | fill_blank |
| 3 | multiple_choice |
| 4 | true_false |
Tools
| Tool | Question type | Key rules |
|---|---|---|
submit_multiple_choice | multiple_choice | Exactly 3 options. correctAnswer must exactly match one option string |
submit_true_false | true_false | A statement (not a question). trueLabel/falseLabel translated to story language |
submit_fill_blank | fill_blank | One word in a sentence replaced by ___. Must include a hint and exactly 3 wordChoices (1 correct + 2 distractors) |
All question fields
Every question type includes these fields:
| Field | Description |
|---|---|
question | The question text or statement |
correctAnswer | Exact answer string |
encouragement | Short phrase shown/spoken when child is correct (e.g. "Excellent! That's right!") |
correction | Gentle phrase shown/spoken when child is wrong (e.g. "Good try! Luna found a golden key.") |
Fill-blank extra fields
| Field | Description |
|---|---|
hint | Short hint about the missing word |
wordChoices | Exactly 3 words: the correct word + 2 plausible distractors from the story, in random order |
The UI renders wordChoices as 3 colored chips (read-only word bank) so the child can see the options and type the answer in a text input below.
Language support
All text fields — question, options, encouragement, correction, trueLabel, falseLabel, hint — are written in the story's language.
Supports all 21 languages: English, Spanish, French, Portuguese (Brazil), Portuguese (Portugal), German, Italian, Japanese, Korean, Chinese, Arabic, Hindi, Indonesian, Russian, Vietnamese, Malay, Filipino, Tetum, Burmese, Lao, Khmer.
Examples for Indonesian (id):
trueLabel:"Benar",falseLabel:"Salah"question:"Apa yang ditemukan Luna di tepi danau?"encouragement:"Luar biasa! Kamu benar!"
TTS audio
After generating each question, the agent synthesizes speech for the question text and returns it as a base64 audio string. This audio plays automatically when the question appears on screen.
Audio format: data:audio/wav;base64,...Flow
Child taps "Quiz Time"
│
▼
Promise.all([
runWithRetry(index=0), ← multiple_choice ┐
runWithRetry(index=1), ← true_false │ all fire simultaneously
runWithRetry(index=2), ← fill_blank │ agent instances reused from cache
runWithRetry(index=3), ← multiple_choice │
runWithRetry(index=4), ← true_false ┘
]) ← each retries up to 1× on failure (500ms delay)
│ LLM + TTS run in parallel per question
▼ streams "question" event as each finishes
▼ sends "result" event when ALL 5 are ready
Questions shown to child (all 5 buffered, no API calls between)
│
▼
Q1 → Q2 → Q3 → Q4 → Q5Input / Output
// Input
{
pageText: string; // Story page text to base questions on
storyTitle: string;
questionIndex: number; // 0–4, determines question type
previousQuestions: string[]; // Always [] in practice — the route passes an empty array for all 5 parallel calls
language?: string; // default: "en"
}
// Output
{
quiz: Record<string, unknown> | null; // The question object
audioData?: string; // base64 WAV for TTS playback
error?: string;
}Retry logic
Each of the 5 parallel calls uses runWithRetry (defined in app/api/live-quiz/route.ts):
- Max 1 retry after the initial attempt (2 total tries)
- Delay: 500ms before retry
- Retries on: Vertex AI timeout, network error, or agent returning no quiz
Fill-blank fallback
If fill_blank (index 2) fails after all retries, app/api/live-quiz/route.ts automatically retries once with multiple_choice type instead. This is because fill_blank requires more complex tool output (wordChoices, hint) and is more likely to fail in non-English languages like Tetum. The fallback is implemented in the API route, not inside the agent itself.
Client-side graceful handling
If a question still fails despite the fallback, the client filters out null questions and runs the quiz with however many questions succeeded (e.g. 4 out of 5). Score is reported against the actual number of questions generated.
Stateless agent design
The LlmAgent is configured with includeContents: "none". This is critical.
InMemoryRunner (Google ADK) keeps a session history. Without this flag, the model receives its own previous tool calls and responses as context — causing it to call submit_* tools repeatedly in a loop (10+ times, 3000+ tokens). With includeContents: "none", the agent treats every invocation as a fresh call — it only sees the current user message and system instruction.
The same includeContents: "none" setting is applied to 4 agents total — QuizMaster, LearningAdvisor, ParentInsights, and StoryAdaptation. (QuizFeedback does not need it — see quiz-feedback.md.)
Observability span
Span name: agent/QuizMaster
| Attribute | Example value |
|---|---|
agent.name | QuizMaster |
agent.model | gemini-2.5-pro |
agent.language | id |
agent.quizType | multiple_choice |
agent.questionIndex | 2 |
agent.quiz_type_generated | fill_blank |
agent.success | true |
agent.duration_ms | 8300 |
Simulation test cases
Run with npm run simulate:
| Scenario | Expected |
|---|---|
| Valid quiz structure | question, correctAnswer, type all present |
| True/false type | type === "true_false", trueLabel and falseLabel present |
| Fill-in-the-blank | type === "fill_blank", question contains ___ |
| Indonesian quiz | question contains Indonesian words |
| Correct answer in options | correctAnswer matches one of the options strings |
| Question variety | Two questions generated from the same page have different text (deduplication via agent context, not previousQuestions — that field is always [] in production) |
| Audio is generated | audioData starts with data:audio/ |
