A multi‑agent system that automates insurance quote gathering from multiple carriers, compares coverages, and recommends the best policy—all orchestrated by a Cohere‑powered agent mesh.
Independent insurance agents waste hours manually entering client data into each carrier’s portal, comparing coverages side‑by‑side, and often miss the optimal policy because the complexity hides the best deal.
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.
In this tutorial, you’ll build a multi-agent insurance quote comparison system using Cohere’s LLM, the @reaatech/agent-mesh ecosystem, and Next.js. The system orchestrates specialized agents — one per carrier (GEICO, Progressive, State Farm, Allstate), plus a coverage analyzer and a recommendation engine — to query multiple carriers in parallel, compare coverages, and recommend the best policy. An Express API handles quote processing, Langfuse provides observability tracing, and a Next.js dashboard lets users submit quote requests and view results.
This tutorial is for TypeScript developers familiar with Next.js, Express, and basic LLM concepts.
Prerequisites
Node.js 22+ and pnpm 10+
A Cohere API key — sign up at dash.cohere.com
A Langfuse account (optional, for tracing) — sign up at langfuse.com
A Google Cloud project with Firestore enabled (for session persistence)
Familiarity with TypeScript, Next.js App Router, and Express
Step 1: Scaffold the project
Create the Next.js project and install all dependencies. The scaffold includes the Next.js 16 App Router shell, TypeScript configuration, ESLint, Vitest, and all the packages you’ll need.
Expected output: A package.json with exact-pinned versions and no ^ or ~ prefixes.
Step 2: Define insurance types with Zod
Create the type system that validates insurance profiles, quote results, comparison data, and recommendations. Every input and output flowing through the system is validated at the boundaries by Zod schemas.
// src/types/index.tsexport * from "./insurance.js";
Expected output: Five Zod schemas and their inferred types — VehicleInfo, CoveragePrefs, InsuranceProfile, QuoteResult, ComparisonResult, and Recommendation.
Step 3: Create agent YAML configurations
The agent registry loads agent definitions from YAML files. Each carrier gets its own agent with examples for intent routing. Create an agents/ directory with one file per agent.
yaml
# agents/geico.yamlagent_id: "geico"display_name: "GEICO"description: "Provides auto insurance quotes for personal vehicles. Handles liability, collision, comprehensive, and uninsured motorist coverage."endpoint: "${GEICO_ENDPOINT:-http://localhost:8081}"type: mcpis_default: falseconfidence_threshold: 0.7clarification_required: falseexamples: - "Get me a GEICO quote for a 2024 Toyota Camry, clean driving record" - "Compare GEICO rates for a 35-year-old with good credit" - "What would GEICO charge for full coverage on a Honda Civic?"
Create the same shape for progressive.yaml, state-farm.yaml, and allstate.yaml, each with their own agent_id, display_name, and description. Then create the analyzer and recommender:
yaml
# agents/coverage-analyzer.yamlagent_id: "coverage-analyzer"display_name: "Coverage Analyzer"description: "Compares insurance quotes across carriers. Analyzes premium differences, coverage limits, deductible options, and identifies the best value."endpoint: "${ANALYZER_ENDPOINT:-http://localhost:8082}"type: mcpis_default: falseconfidence_threshold: 0.8clarification_required: falseexamples: - "Compare these quotes and tell me which has the best liability coverage per dollar" - "Show me the coverage differences between GEICO and Progressive" - "Which quote gives the most comprehensive coverage for the premium?"
yaml
# agents/recommender.yamlagent_id: "recommender"display_name: "Recommendation Engine"description: "Analyzes the comparison results and recommends the best insurance carrier based on coverage value, premium cost, and client needs."endpoint: "${RECOMMENDER_ENDPOINT:-http://localhost:8083}"type: mcpis_default: falseconfidence_threshold: 0.9clarification_required: falseexamples: - "Which carrier offers the best overall value for full coverage?" - "Recommend the cheapest option with adequate liability limits" - "Who has the best rates for a high-risk driver?"
Finally, create the default fallback agent:
yaml
# agents/default.yamlagent_id: "default"display_name: "Default Agent"description: "Handles general inquiries and routes requests to the appropriate specialized agent."endpoint: "${DEFAULT_ENDPOINT:-http://localhost:8084}"type: mcpis_default: trueconfidence_threshold: 0clarification_required: trueclarification_context: "I can help you compare insurance quotes. Please tell me about your vehicle and coverage needs."examples: - "I need car insurance" - "Help me find the best rate" - "Compare insurance companies for me"
Expected output: Seven YAML files in agents/ — four carrier agents, one coverage analyzer, one recommender, and one default agent. Exactly one agent has is_default: true.
Step 4: Set up the agent registry
Create a module that initializes the registry from the YAML files and exposes helpers for looking up carrier agents and the analyzer/recommender.
ts
// src/agents/registry.tsimport { initRegistry, registryState, setupSighupHandler, cleanupSighupHandler } from "@reaatech/agent-mesh-registry";import { mcpClientFactory } from "@reaatech/agent-mesh-router";export async function initAgentRegistry(): Promise<void> { await initRegistry(); setupSighupHandler();}export async function shutdownRegistry(): Promise<void> { await mcpClientFactory.closeAll(); cleanupSighupHandler();}export function getCarrierAgentIds(): string[] { const exclude = new Set(["coverage-analyzer", "recommender", "default"]); const allIds = registryState.getAgentIds(); return allIds.filter((id) => !exclude.has(id));}export function getAgentById(agentId: string) { return registryState.getAgent(agentId);}export function getAnalyzerAgent() { return registryState.getAgent("coverage-analyzer");}export function getRecommenderAgent() { return registryState.getAgent("recommender");}export function getDefaultAgent() { return registryState.defaultAgent;}
Expected output: A registry module that calls initRegistry() on startup, filters carrier agents from the loaded YAML configs, and cleans up MCP clients on shutdown.
Step 5: Implement session management
Session management uses the @reaatech/agent-mesh-session package backed by Firestore. Each quote request gets a session that tracks the conversation turns.
ts
// src/services/session-store.tsimport { createSession, getActiveSession, appendTurn, closeSession, updateWorkflowState } from "@reaatech/agent-mesh-session";import type { SessionRecord } from "@reaatech/agent-mesh";export async function createQuoteSession(userId: string, employeeId: string): Promise<SessionRecord> { return createSession({ userId, employeeId, activeAgent: "geico" });}export async function getOrCreateSession(userId: string, employeeId: string): Promise<SessionRecord> { const existing = await getActiveSession(userId); if (existing) { return existing; } return createQuoteSession(userId, employeeId);}export async function appendQuoteTurn(sessionId: string, role: "user" | "agent", content: string, intentSummary?: string): Promise<void> { await appendTurn(sessionId, { role, content, timestamp: new Date().toISOString(), intent_summary: intentSummary, });}export async function completeQuoteSession(sessionId: string): Promise<void> { await closeSession(sessionId, "completed");}export async function failQuoteSession(sessionId: string): Promise<void> { await closeSession(sessionId, "error");}export async function updateSessionWorkflow(sessionId: string, workflowState: Record<string, unknown>): Promise<void> { await updateWorkflowState(sessionId, workflowState);}
Create a Cohere client singleton that generates quote requests, analyzes coverage, and produces recommendations using the command-a-03-2025 model.
ts
// src/services/llm/cohere-client.tsimport { CohereClientV2, CohereError, CohereTimeoutError } from "cohere-ai";import { logger } from "@reaatech/agent-mesh-observability";import type { InsuranceProfile, QuoteResult } from "../../types/insurance.js";const cohere = new CohereClientV2({ token: process.env.COHERE_API_KEY });function buildQuotePrompt(profile: InsuranceProfile): string { const age = String(profile.age); const year = String(profile.vehicleInfo.year); const liability = String(profile.coveragePrefs?.liabilityLimit ?? 100000); const ded = String(profile.coveragePrefs?.deductible ?? 500); return `Generate an insurance quote request for a ${age}-year-old driver with a ${profile.drivingHistory} driving record. Vehicle: ${year} ${profile.vehicleInfo.make} ${profile.vehicleInfo.model}. Coverage preferences: liability limit $${liability}, deductible $${ded}.`;}export async function generateQuoteRequest(profile: InsuranceProfile): Promise<string> { const start = Date.now(); try { const response = await cohere.chat({ model: "command-a-03-2025", messages: [{ role: "user", content: buildQuotePrompt(profile) }], }); return (response as { message?: { content?: Array<{ text?: string }> } }).message?.content?.[0]?.text ?? ""; } catch (err) { if (err instanceof CohereTimeoutError) { logger.error("Cohere request timed out", { durationMs: Date.now() - start }); } else if (err instanceof CohereError) { logger.error("Cohere API error", { statusCode: err.statusCode, message: err.message }); } throw err; }}export async function* streamQuoteGeneration(profile: InsuranceProfile): AsyncGenerator<string> { const stream = await cohere.chatStream({ model: "command-a-03-2025", messages: [{ role: "user", content: buildQuotePrompt(profile) }], }); for await (const event of stream) { if (event.type === "content-delta") { const delta = event.delta?.message as { text?: string } | undefined; if (delta?.text) { yield delta.text; } } }}
The full Cohere client also exports analyzeCoverage and generateRecommendation functions that use the same pattern for coverage analysis and carrier recommendation prompts. These are used directly if you bypass the agent mesh dispatch pipeline.
Expected output: A Cohere client singleton that generates and streams quote requests, with proper error handling for CohereError and CohereTimeoutError.
Step 7: Create a Mastra agent with AI SDK tools
Create a Mastra-powered agent that uses @ai-sdk/cohere and the Vercel AI SDK to process insurance profiles with tool-supported reasoning.
Expected output: A Mastra agent with three AI SDK tools — lookupCarrierRates, validateCoverage, and calculatePremium.
Step 8: Implement carrier agents with mesh dispatch
The carrier agent module dispatches insurance profiles to each carrier’s MCP agent in parallel using dispatchToAgent from @reaatech/agent-mesh-router.
ts
// src/agents/carrier-agent.tsimport { dispatchToAgent, formatAgentResponse, shouldCloseSession, getUpdatedWorkflowState } from "@reaatech/agent-mesh-router";import { registryState } from "@reaatech/agent-mesh-registry";import { recordAgentDispatchDuration, recordAgentDispatchError } from "@reaatech/agent-mesh-observability";import type { InsuranceProfile, QuoteResult } from "../types/insurance.js";function buildRawInput(profile: InsuranceProfile): string { const prefs = profile.coveragePrefs; const age = String(profile.age); const year = String(profile.vehicleInfo.year); const base = `Provide an auto insurance quote for a ${age}-year-old driver with a ${
Expected output: A queryAllCarriers function that dispatches to every carrier agent in parallel via Promise.allSettled and returns collected QuoteResult objects or error details.
Step 9: Build the coverage analyzer and recommendation engine
The coverage analyzer compares quotes and produces a comparison matrix with savings estimates. The recommendation engine picks the best carrier. Both try AI-powered dispatch first, then fall back to deterministic algorithms when the MCP agents are unavailable.
// src/agents/recommendation.tsimport { dispatchToAgent, formatAgentResponse } from "@reaatech/agent-mesh-router";import { registryState } from "@reaatech/agent-mesh-registry";import { logger } from "@reaatech/agent-mesh-observability";import type { ComparisonResult, Recommendation } from "../types/insurance.js";export async function generateRecommendation( comparison: ComparisonResult, sessionId: string,): Promise<Recommendation> { const agent = registryState.getAgent("recommender"); if (!agent) { return deterministicRecommendation(comparison); } try { const response = await dispatchToAgent(agent, { sessionId, employeeId: "", displayName: "User", rawInput: `Recommend the best insurance carrier based on:\n${JSON.stringify(comparison, null, 2)}`, intentSummary: "recommendation_request", entities: { comparison }, detectedLanguage: "en", turnHistory: [], workflowState: {}, }); const content = formatAgentResponse(response); try { return JSON.parse(content) as Recommendation; } catch { return deterministicRecommendation(comparison); } } catch (err) { logger.warn("Recommender dispatch failed, using fallback", { error: (err as Error).message }); return deterministicRecommendation(comparison); }}function deterministicRecommendation(comparison: ComparisonResult): Recommendation { if (comparison.quotes.length === 0) { return { recommendedCarrier: "none", rationale: "No quotes available to compare.", potentialSavings: 0, coverageHighlights: [], disclaimers: ["No quotes were successfully retrieved."], }; } const sorted = [...comparison.quotes].sort((a, b) => a.premium - b.premium); const best = sorted[0]; const premium = String(best.premium); const liability = String(best.liabilityCoverage); return { recommendedCarrier: best.carrier, rationale: `${best.carrier} offers the lowest premium at $${premium}/year with ${best.coverageLevel} coverage.`, potentialSavings: comparison.savingsEstimate.potentialSavings, coverageHighlights: [ `${best.carrier} premium: $${premium}/year`, `Coverage level: ${best.coverageLevel}`, `Liability coverage: $${liability}`, ], disclaimers: [ "Rates are estimates based on provided information.", "Actual rates may vary based on additional factors.", ], };}
Expected output: Two agent modules that try AI-powered dispatch first, then fall back to deterministic algorithms when the MCP agents are unavailable.
Step 10: Create the orchestration pipeline
The orchestrator ties everything together into a single processQuote function. It creates or resumes a session, dispatches to all carriers in parallel, analyzes the results, generates a recommendation, and handles errors gracefully.
ts
// src/services/orchestrator.tsimport { createChildLogger, recordSessionLookupDuration, recordAgentDispatchDuration } from "@reaatech/agent-mesh-observability";import { getOrCreateSession, appendQuoteTurn, completeQuoteSession, failQuoteSession } from "./session-store.js";import { queryAllCarriers } from "../agents/carrier-agent.js";import { analyzeQuotes } from "../agents/coverage-analyzer.js";import { generateRecommendation } from "../agents/recommendation.js";import type { InsuranceProfile, ComparisonResult, Recommendation, QuoteResult } from "../types/insurance.js";export interface QuotePipelineResult { sessionId: string; quotes: QuoteResult[]; comparison: ComparisonResult; recommendation
Expected output: A pipeline that runs getOrCreateSession -> queryAllCarriers -> analyzeQuotes -> generateRecommendation -> completeQuoteSession, with a streaming variant for SSE consumption.
Step 11: Set up the Express API server
Create the Express application that exposes REST endpoints for quote processing, agent management, and health checks.
ts
// src/api/server.tsimport express from "express";import cors from "cors";import { sessionMiddleware } from "@reaatech/agent-mesh-session";import { initAgentRegistry, shutdownRegistry } from "../agents/registry.js";import { initObservability, shutdownObservability } from "../services/observability.js";import { quotesRouter } from "./routes/quotes.js";import { agentsRouter } from "./routes/agents.js";import { healthRouter } from "./routes/health.js";export function createApp(): express.Application { const app = express(); app.use(express.json()); app.use(cors()); app.use(sessionMiddleware); app.use("/api/quotes", quotesRouter); app.use("/api/agents", agentsRouter); app.use("/api/health", healthRouter); return app;}export async function startServer(): Promise<void> { await initAgentRegistry(); initObservability(); const app = createApp(); const port = Number(process.env.PORT) || 8080; const portStr = String(port); app.listen(port, () => { console.log(`Server listening on port ${portStr}`); }); process.on("SIGTERM", () => { void shutdownRegistry(); void shutdownObservability(); process.exit(0); });}
Create the route handlers. The quotes route validates the incoming insurance profile and invokes the pipeline:
Expected output: An Express server with three route groups — /api/quotes (POST, GET by ID, GET stream), /api/agents (GET list, POST reload), and /api/health.
Step 12: Configure observability with Langfuse
Set up OpenTelemetry tracing and Langfuse integration so every quote processing step is logged and traceable.
ts
// src/services/observability.tsimport { createChildLogger, initOtel, shutdownOtel, logAgentRouted, logCircuitBreakerChange, logSecurityEvent, type AuditEventType } from "@reaatech/agent-mesh-observability";import { Langfuse } from "langfuse";let langfuse: Langfuse;export function initObservability(): void { initOtel(); langfuse = new Langfuse({ secretKey: process.env.LANGFUSE_SECRET_KEY, publicKey: process.env.LANGFUSE_PUBLIC_KEY, baseUrl: process.env.LANGFUSE_BASE_URL, });}export async function shutdownObservability(): Promise<void> { await shutdownOtel(); await langfuse.shutdownAsync();}export function createRequestLogger(requestId: string, sessionId?: string) { return createChildLogger({ request_id: requestId, session_id: sessionId || "", });}export function createQuoteTrace(sessionId: string) { return langfuse.trace({ name: "insurance-quote-comparison", sessionId, });}export function createStageSpan(trace: ReturnType<typeof langfuse.trace>, name: string) { return trace.span({ name });}export function finalizeSpan(span: ReturnType<ReturnType<typeof langfuse.trace>["span"]>, output: unknown): void { span.end({ output });}export function finalizeTrace(trace: ReturnType<typeof langfuse.trace>): void { trace.update({}); void langfuse.flushAsync();}
The module also exports logRoutingEvent, logCircuitEvent, and logSecurityEventFn helpers for recording routing decisions, circuit breaker state changes, and security audit events throughout the agent mesh.
Expected output: An observability module that initializes OpenTelemetry and Langfuse, creates traces and spans per quote session, and flushes on shutdown.
Step 13: Build the Next.js dashboard
Replace the scaffolded app/page.tsx with a landing page that shows supported carriers and links to the dashboard:
Create the dashboard page (app/dashboard/page.tsx) that fetches the available agents from the Express API and offers navigation to submit new quotes or view history. Create app/dashboard/quotes/new/page.tsx with a form for driver age, driving history, vehicle info, liability limit, and deductible. Create app/dashboard/quotes/[id]/page.tsx to display a comparison table and recommendation card. Create app/dashboard/history/page.tsx to list past quote sessions.
Expected output: A Next.js dashboard with — landing page with supported carriers, agent list, quote submission form, result comparison table, and history view.
Step 14: Create Next.js API proxy routes
Forward requests from the Next.js client to the Express backend. These routes use NextRequest and NextResponse as required by the Next.js App Router.
ts
// app/api/quotes/route.tsimport { NextRequest, NextResponse } from "next/server";export async function POST(req: NextRequest) { const body = (await req.json()) as Record<string, unknown>; const expressUrl = process.env.EXPRESS_API_URL || "http://localhost:8080"; const response = await fetch(`${expressUrl}/api/quotes`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); const data = (await response.json()) as Record<string, unknown>; return NextResponse.json(data, { status: response.status });}
ts
// app/api/quotes/[id]/route.tsimport { NextRequest, NextResponse } from "next/server";export async function GET( _req: NextRequest, { params }: { params: Promise<{ id: string }> },) { const { id } = await params; const expressUrl = process.env.EXPRESS_API_URL || "http://localhost:8080"; const response = await fetch(`${expressUrl}/api/quotes/${id}`); if (!response.ok) { return NextResponse.json({ error: "Session not found" }, { status: 404 }); } const data = (await response.json()) as Record<string, unknown>; return NextResponse.json(data);}
Expected output: Two App Router route handlers that proxy POST and GET requests to the Express backend. Note the async params pattern matching Next 16 conventions.
Step 15: Configure environment variables
Populate .env.example with every environment variable the system reads:
Expected output: A JSON response with sessionId, quotes array, comparison object, and recommendation — or partial results with carrier-specific errors if the MCP agents aren’t running.
Run the test suite to verify everything works:
terminal
pnpm test
Next steps
Add real Playwright-based web scraping for carriers without API endpoints using src/agents/carrier-playwright.ts
Implement SSE streaming on the dashboard to show real-time progress as quotes arrive from each carrier
Wire up the Langfuse trace spans to the orchestrator pipeline for full observability into each quote processing stage
Add authentication and user session management so multiple agents can work independently
Deploy the Express API as a Cloud Run service and the Next.js dashboard to Vercel
profile
.
drivingHistory
} driving record. Vehicle: ${
year
} ${
profile
.
vehicleInfo
.
make
} ${
profile
.
vehicleInfo
.
model
}. `
;
if (prefs) {
const ll = String(prefs.liabilityLimit);
const ded = String(prefs.deductible);
return base + `Liability limit: $${ll}, deductible: $${ded}.`;