The academic director at a tutoring center struggles to get instructors to submit regular progress reports for each student. Parents expect weekly updates, but instructors forget or produce inconsistent quality. The director ends up chasing instructors and manually compiling reports, which is unsustainable as the center grows. This leads to parent frustration and churn.
A complete, working implementation of this recipe — downloadable as a zip or browsable file by file. Generated by our build pipeline; tested with full coverage before publishing.
This tutorial builds an automated progress report generator for tutoring businesses. You’ll wire 6 REAA packages (agent-runbook, agent-runbook-agent, agent-runbook-runbook, agent-runbook-observability, agents-markdown, agents-markdown-reporter) into a Next.js API that accepts student session data and returns LLM-generated progress reports — without requiring instructor write-ups.
You’ll create domain types with Zod, build a 7-stage document pipeline (validate, analyze, generate, assemble, validate output, format, persist), add observability with OpenTelemetry tracing and structured logging, and cover the whole thing with tests. By the end you’ll have a working /api/reports/generate endpoint ready for a tutoring center’s frontend.
Prerequisites
Node.js 22+ and pnpm 10 installed
OpenAI API key — set it in .env.local after copying from .env.example
Basic familiarity with TypeScript, Zod, and Next.js App Router route handlers
A terminal open in an empty project directory
Step 1: Scaffold the Next.js project
Create a new Next.js project with TypeScript and the App Router. Then change into the project directory.
Expected output: A new agnostic-progress-report-automation-2/ directory with a standard Next.js scaffold — app/, src/, tsconfig.json, next.config.ts, package.json.
Step 2: Pin exact dependencies
Add the recipe’s dependencies with exact versions. These replace whatever defaults the scaffold generated.
Expected output: Every dependency in package.json now appears without ^ or ~ — exact versions only.
Step 3: Configure environment variables
Create your local .env file from the example and add your API key. The .env.example should contain at least these entries:
env
# Env vars used by agnostic-progress-report-automation-2.# The builder adds entries here as it wires up each integration.# Keep placeholders only — never commit real values.NODE_ENV=developmentOPENAI_API_KEY=<your-openai-api-key>LLM_MODEL=gpt-5.2-miniLOG_LEVEL=infoOTEL_ENABLED=falseMETRICS_ENABLED=falseOTLP_ENDPOINT=http://localhost:4318REPORT_STORAGE_DIR=./data/reports
terminal
cp .env.example .env.local# Edit .env.local and replace <your-openai-api-key> with a real key
Expected output:.env.local exists with OPENAI_API_KEY set to your actual key.
Step 4: Define domain types and Zod schemas
Create src/types/report.ts — the domain model for the entire pipeline. You’ll define interfaces for students, sessions, reports, and the Zod schemas that validate them at runtime.
ts
import { z } from "zod";import type { ValidationResult, Finding } from "@reaatech/agents-markdown";import type { Runbook, RunbookSection, AnalysisContext, ExportFormat } from "@reaatech/agent-runbook";export interface StudentInfo { studentId: string; name: string; grade: string; subjects: SubjectInfo[];}export interface SubjectInfo { subject: string; instructor: string; sessions: SessionRecord[];
Expected output:src/types/report.ts exists with all 6 domain interfaces and 6 Zod schemas. Running pnpm typecheck produces no errors.
Step 5: Build the observability service
Create src/services/observability.ts — a thin wrapper around @reaatech/agent-runbook-observability that initialises logging, tracing, and metrics, and provides pipeline-specific helpers.
Expected output:src/services/observability.ts exists. pnpm typecheck reports no errors. The file re-exports every primitive you’ll need downstream — info, error, startReportSpan, setupRequestRunId, etc.
Step 6: Build the LLM adapter
Create src/services/llm-adapter.ts — the provider-agnostic LLM interface. It uses Vercel AI SDK v6 for generation and @reaatech/agent-runbook-agent for analysis.
ts
import { generateText, Output } from "ai";import { openai } from "@ai-sdk/openai";import { z } from "zod";import { LLMError, retry, type AnalysisContext } from "@reaatech/agent-runbook";import { createAnalysisAgent } from "@reaatech/agent-runbook-agent";import { startAgentSpan, endSpanSuccess, endSpanError, recordAgentCall, recordAgentCost,} from "@reaatech/agent-runbook-observability";const LLM_MODEL: string = process.env.LLM_MODEL ?? "gpt-5.2-mini";export function createLlmClient() {
Expected output:src/services/llm-adapter.ts exists. pnpm typecheck passes. The file exports four functions: createLlmClient, analyzeStudentPerformance, generateNarrativeReport, and generateStructuredReportContent.
Step 7: Build the validation service
Create src/services/validation.ts — validates incoming generation requests and outgoing reports using Zod schemas and REAA utils.
ts
import { validateInput, ValidationError, getErrorMessage } from "@reaatech/agent-runbook";import type { ValidationResult, LintResult, Finding } from "@reaatech/agents-markdown";import { reportMarkdownValidationResult, reportMarkdownLintResult,} from "@reaatech/agents-markdown-reporter";import { groupBy, truncate } from "@reaatech/agents-markdown";import { ReportGenerationRequestSchema, type ReportGenerationRequest,} from "../types/report.js";function validatedInput(schema: object, data: unknown): { success: boolean; data?: unknown; errors?: string
Expected output:src/services/validation.ts exists. pnpm typecheck passes. The file exports validateGenerationRequest, validateProgressReport, formatValidationOutput, and formatLintOutput.
Step 8: Build the 7-stage report generation pipeline
Create src/services/report-generator.ts — the main orchestrator. It runs 7 stages: setup, validate input, analyze with LLM, generate content, assemble runbook, validate output, and format/persist.
ts
import { generateId, retry, escapeMarkdown, AnalysisError, isAppError, readJsonFile, writeJsonFile, ensureDirectory, listFiles, ValidationError,} from "@reaatech/agent-runbook";import { buildRunbook, exportRunbook, validateCompleteness, generateTOC,} from "@reaatech/agent-runbook-runbook";import type { Runbook, RunbookSection } from "@reaatech/agent-runbook";import { setupRequestRunId, startReportSpan, recordReportOutcome, endSpanSuccess, endSpanError, info, error as
Expected output:src/services/report-generator.ts exists with three exports: generateProgressReport, getReportById, and listReports. pnpm typecheck passes.
Step 9: Create the API routes
Create four route handlers under app/api/. Start with the health check, then the main report generation endpoint, a listing endpoint, and a single-report lookup.
app/api/health/route.ts:
ts
import { NextResponse } from "next/server";export function GET() { return NextResponse.json({ status: "ok", service: "progress-report-generator", });}
app/api/reports/generate/route.ts — the main POST handler:
import { NextResponse } from "next/server";import { listReports } from "../../../src/services/report-generator";export function GET() { const reports = listReports(); return NextResponse.json({ reports });}
app/api/reports/[id]/route.ts — get a single report by ID:
ts
import { type NextRequest, NextResponse } from "next/server";import { getReportById } from "../../../../src/services/report-generator";export async function GET( _req: NextRequest, { params }: { params: Promise<{ id: string }> }) { const { id } = await params; const report = getReportById(id); if (!report) { return NextResponse.json( { error: "Report not found" }, { status: 404 } ); } return NextResponse.json({ report });}
Expected output: Four route files exist under app/api/. pnpm typecheck passes with zero errors. Every handler uses NextRequest/NextResponse — no bare Request or new Response() anywhere.
Step 10: Wire up the barrel exports
Create src/index.ts to re-export everything consumers of this package will need:
ts
export const SCAFFOLD_VERSION = "0.1.0" as const;export { generateProgressReport, getReportById, listReports } from "./services/report-generator.js";export { initObservability, setupRequestRunId, startReportSpan, recordReportOutcome,} from "./services/observability.js";export { createLlmClient, generateNarrativeReport } from "./services/llm-adapter.js";export { validateGenerationRequest, validateProgressReport, formatValidationOutput } from "./services/validation.js";export type { StudentInfo, SubjectInfo, SessionRecord, SubjectReport, ProgressReport, ReportGenerationRequest,} from "./types/report.js";
Expected output:src/index.ts re-exports every public function and type. pnpm typecheck passes.
Step 11: Write the test suite
Create tests covering all layers — Zod schema validation, services, API routes — with mocked externals. Here are two representative test files; the full recipe includes 8 test files total.
tests/types/report.test.ts:
ts
import { describe, it, expect } from "vitest";import { z } from "zod";import { StudentInfoSchema, SessionRecordSchema, ProgressReportSchema, ReportGenerationRequestSchema,} from "../../src/types/report.js";import { generateId } from "@reaatech/agent-runbook";async function testValidateInput(schema: object, data: unknown): Promise<{ success: boolean; data?: unknown; errors?: string[] }> { const mod = await import("@reaatech/agent-runbook")
The remaining test files (tests/services/validation.test.ts, tests/services/llm-adapter.test.ts, tests/services/report-generator.test.ts, tests/api/routes.test.ts, tests/api/reports.test.ts, tests/index.test.ts) follow the same pattern — mock externals, test each function, cover error paths. Create them the same way with the mocks and assertions that match your service code.
Expected output: All 8 test files exist under tests/. pnpm test runs without failures and reports coverage above 90% on all 4 metrics.
Step 12: Run the quality gates
Run typecheck, lint, and the full test suite with coverage.
terminal
pnpm typecheckpnpm lintpnpm test
Expected output:
pnpm typecheck — exits 0 with no TypeScript errors.
pnpm lint — exits 0 with no ESLint errors.
pnpm test — exits 0. Open vitest-report.json and confirm numFailedTests === 0, numTotalTests >= 50, and the coverage summary in coverage/coverage-summary.json shows lines, branches, functions, and statements all at 90% or above.
Next steps
Add a frontend — build a Next.js page with a form that collects student session data and calls POST /api/reports/generate, then renders the returned report
Add email delivery — wire Nodemailer or Resend to automatically send generated reports to parents
Support multiple LLM providers — switch from @ai-sdk/openai to Anthropic, Google Gemini, or a local model using the Vercel AI SDK provider interface
Add report scheduling — use a cron job to auto-generate weekly reports for every active student
Build a dashboard — show all generated reports, filter by student or date, track which have been sent to parents