Small business support teams are overwhelmed by repetitive Tier‑1 questions across email and chat, leading to slow response times and high agent turnover. Simple keyword filters can’t handle ambiguous requests like “I have a problem with my bill and my service is down.”
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.
Small business support teams get buried under repetitive Tier-1 questions across email and chat, leading to slow response times and high agent turnover. Simple keyword filters can’t handle ambiguous requests like “I have a problem with my bill and my service is down.” This tutorial builds a multi-agent mesh that classifies each customer message, routes it to the right specialist (billing, tech support, or account services), and preserves conversation context across agent handoffs — all powered by the Grok model from xAI.
You’ll use @reaatech/agent-mesh as the service fabric, @reaatech/session-continuity for conversation state, and @reaatech/agent-mesh-classifier for intent classification. Specialist agents call Grok via @ai-sdk/xai. By the end you’ll have a working POST /api/chat endpoint you can hit with curl.
Prerequisites
Node.js 22+ and pnpm 10 installed on your machine
An xAI API key — sign up at console.xai.cloud to get one
Familiarity with TypeScript and basic Next.js App Router conventions
curl or any HTTP client for testing the API
Step 1: Scaffold the project and configure environment variables
The project scaffolding is already in place — a Next.js 16 App Router shell with TypeScript, Vitest, and ESLint configured. Start by installing dependencies and setting up your environment file.
terminal
pnpm install
Copy the example environment file and add your xAI API key:
terminal
cp
.env.example
.env
Open .env and set your actual xAI API key. The file should look like this:
env
# Env vars used by xai-grok-agent-mesh-for-small-business-customer-support.# The builder adds entries here as it wires up each integration.# Keep placeholders only — never commit real values.NODE_ENV=developmentXAI_API_KEY=<your-xai-api-key>SESSION_MAX_TOKENS=4096SESSION_TOKEN_RESERVE=512
Expected output:pnpm install exits cleanly, and .env exists with your API key set.
Step 2: Create the In-Memory Storage Adapter
The session continuity package needs a storage backend that implements the IStorageAdapter interface. You’ll build an in-memory adapter backed by Map objects with optimistic concurrency control.
Create src/lib/in-memory-adapter.ts:
ts
import { randomUUID } from "crypto";import { ConcurrencyError } from "@reaatech/session-continuity";import type { Session, Message, IStorageAdapter, SessionId, MessageId, HealthStatus } from "@reaatech/session-continuity";export default class InMemoryAdapter implements IStorageAdapter { private sessions: Map<string, Session> = new Map(); private messages: Map<string, Message[]> = new Map(); createSession(session: Omit<Session, "id" | "createdAt"
Expected output:pnpm typecheck passes. The adapter implements all 14 methods of IStorageAdapter including optimistic concurrency in updateSession and retry logic in addMessage.
Step 3: Create the Token Counter
The session manager needs a TokenCounter to track token usage. You’ll build a simple character-count approximation.
Create src/lib/simple-token-counter.ts:
ts
import type { Message, TokenCounter } from "@reaatech/session-continuity";export default class SimpleTokenCounter implements TokenCounter { readonly model = "grok-3"; readonly tokenizer = "simple-whitespace"; count(text: string): number { if (!text) return 0; const words = text.trim().split(/\s+/).filter(Boolean); return Math.ceil(words.length * 1.3); } countMessages(messages: Message[]): number { let total = 0; for (const msg of messages) { if (typeof msg.content === "string") { total += this.count(msg.content); } else if (Array.isArray(msg.content)) { for (const block of msg.content) { if ("text" in block && typeof block.text === "string") { total += this.count(block.text); } } } } return total; }}
Expected output:pnpm typecheck passes. The counter splits text on whitespace and applies a 1.3x heuristic multiplier for token overhead.
Step 4: Create the mesh types re-export barrel
Create a barrel file that re-exports the types and schemas from @reaatech/agent-mesh. This keeps imports clean across the rest of the codebase.
Create src/lib/mesh-types.ts:
ts
import { IncomingRequestSchema, type IncomingRequest, AgentResponseSchema, type AgentResponse, ClassifierOutputSchema, type ClassifierOutput, AgentConfigSchema, type AgentConfig, ContextPacketSchema, type ContextPacket, ConfidenceDecisionSchema, type ConfidenceDecision, TurnEntrySchema, type TurnEntry, type SessionStatus, type CircuitBreakerState, type CircuitState, SERVICE_NAME, SERVICE_VERSION,} from "@reaatech/agent-mesh";export { IncomingRequestSchema, type IncomingRequest, AgentResponseSchema, type AgentResponse, ClassifierOutputSchema, type ClassifierOutput, AgentConfigSchema, type AgentConfig, ContextPacketSchema, type ContextPacket, ConfidenceDecisionSchema, type ConfidenceDecision, TurnEntrySchema, type TurnEntry, type SessionStatus, type CircuitBreakerState, type CircuitState, SERVICE_NAME, SERVICE_VERSION,};
Expected output:pnpm typecheck passes. These types are now available via src/lib/mesh-types.js imports.
Step 5: Create the Grok Client
The specialist agents all need to call the Grok model. This module wraps @ai-sdk/xai and the Vercel AI SDK’s generateText into a reusable client.
Expected output:pnpm typecheck passes. The module exports callGrok (single prompt) and callGrokWithHistory (multi-turn array), both logging usage metrics to stdout.
Step 6: Create the Gateway Adapter
The agent-mesh-gateway package provides helper functions for health checks and test cleanup. Re-export them through a slim adapter file.
Expected output:pnpm typecheck passes. Test files will later call clearRateLimitBuckets, clearAuthCache, and clearProfileCache in their beforeEach blocks.
Step 7: Create the Agent Registry
The agent registry defines the three specialist agents as typed AgentConfig objects. The classifier uses this registry to match intents to agents.
Create src/services/agent-registry.ts:
ts
import type { AgentConfig } from "@reaatech/agent-mesh";const BILLING_AGENT: AgentConfig = { agent_id: "billing", display_name: "Billing Specialist", description: "Handles billing inquiries, invoices, payment issues, subscription changes, and refunds.", confidence_threshold: 0.6, endpoint: "local://billing", examples: [ "I was charged twice this month", "Can I get a refund?", "Update my payment method", "What's on my bill?", ],} as AgentConfig;const TECH_SUPPORT_AGENT: AgentConfig = { agent_id: "tech-support", display_name: "Tech Support Specialist", description: "Resolves technical issues, service outages, connectivity problems, and account access.", confidence_threshold: 0.6, endpoint: "local://tech-support", examples: [ "My internet is down", "I can't log in to my account", "The app keeps crashing", "My service is slow", ],} as AgentConfig;const ACCOUNT_SERVICES_AGENT: AgentConfig = { agent_id: "account-services", display_name: "Account Services Specialist", description: "Manages account updates, profile changes, security settings, and account closures.", confidence_threshold: 0.6, endpoint: "local://account-services", examples: [ "I want to close my account", "Change my email address", "Enable two-factor authentication", ],} as AgentConfig;const AGENTS: AgentConfig[] = [BILLING_AGENT, TECH_SUPPORT_AGENT, ACCOUNT_SERVICES_AGENT];function getAgentById(id: string): AgentConfig | undefined { return AGENTS.find((a) => a.agent_id === id);}function buildRegistryForClassifier(): { agents: AgentConfig[]; defaultAgent: AgentConfig } { return { agents: AGENTS, defaultAgent: TECH_SUPPORT_AGENT };}export { BILLING_AGENT, TECH_SUPPORT_AGENT, ACCOUNT_SERVICES_AGENT, AGENTS, getAgentById, buildRegistryForClassifier };
Expected output:pnpm typecheck passes. Three agents are registered: billing, tech-support (the default fallback), and account-services.
Step 8: Create the Session Service
The session service wraps SessionManager from @reaatech/session-continuity with the in-memory adapter and token counter you built. It exports convenience functions for the route handler.
Expected output:pnpm typecheck passes. The session manager is configured with a 4096-token budget, sliding-window compression, and a 1-hour session TTL with 5-minute cleanup interval.
Step 9: Create the Specialist Agents
Each specialist agent is a function that receives the user’s input and optional conversation history, calls Grok with a domain-specific system prompt, and returns a structured response.
Expected output:pnpm typecheck passes. Each agent has a distinct system prompt. When Grok fails, the agent returns a graceful error message with workflowComplete: false.
Step 10: Create the Agent Dispatcher
The dispatcher is the core orchestration function. It classifies the user’s message, routes to the correct specialist, handles agent handoffs, and validates responses.
Create src/services/agent-dispatcher.ts:
ts
import { classifierService, isRateLimitError, getClarificationQuestion } from "@reaatech/agent-mesh-classifier";import { buildTurnEntry, formatAgentResponse, shouldCloseSession, getUpdatedWorkflowState } from "@reaatech/agent-mesh-router";import { createHandoffConfig, withRetry, HandoffError as AgentHandoffError, pickDefined } from "@reaatech/agent-handoff";import type { CompressedContext, Intent, KeyFact, Entity, OpenItem, HandoffContext } from "@reaatech/agent-handoff";import { AgentResponseSchema } from "@reaatech/agent-mesh";import { getAgentById, AGENTS } from "./agent-registry.js";import { SPECIALISTS } from "./specialist-agents/index.js";import { handoffSession } from "./session-service.js";createHandoffConfig({ routing: { minConfidenceThreshold: 0.6 } });export
Expected output:pnpm typecheck passes. The dispatcher handles five flows: direct routing, low-confidence clarification, ambiguous fallback, agent handoff with retry, and unknown-agent default routing.
Step 11: Create the Chat API Route
The API route is a Next.js App Router route handler at app/api/chat/route.ts with POST (for chat) and GET (for health check).
Expected output:pnpm typecheck passes. The POST handler validates the body, creates or resumes a session, classifies the message, dispatches to the right agent, and returns a structured response with request_id, session_id, agent_id, response, workflow_complete, classification, routing, and duration_ms.
Step 12: Wire up the main entry point and run the tests
Update src/index.ts to re-export every public API from the recipe:
ts
export { getOrCreateSession, addUserMessage, addAssistantMessage, getContext, endSession, handoffSession } from "./services/session-service.js";export { processMessage } from "./services/agent-dispatcher.js";export { AGENTS, getAgentById } from "./services/agent-registry.js";export { handleBilling, handleTechSupport, handleAccountServices } from "./services/specialist-agents/index.js";export { callGrok, callGrokWithHistory } from "./lib/grok-client.js";export { default as InMemoryAdapter } from "./lib/in-memory-adapter.js";export { default as SimpleTokenCounter } from "./lib/simple-token-counter.js";
Now run the type checker and linter to confirm everything compiles:
terminal
pnpm typecheckpnpm lint
Both should exit with code 0. Now run the test suite:
terminal
pnpm vitest run --coverage --reporter=json --outputFile=vitest-report.json
Or use the convenience script:
terminal
pnpm test
Expected output: All 114 tests pass across 28 test suites, with 100% line, statement, and function coverage and 98%+ branch coverage.
Next steps
Add a frontend chat widget — Replace the placeholder app/page.tsx with a live chat interface that calls POST /api/chat and displays the agent’s responses in real time.
Persist sessions to a database — Swap InMemoryAdapter for a PostgreSQL or Redis-backed IStorageAdapter to make sessions survive server restarts.
Add Slack or email ingress — Wire a Slack bot or email ingest endpoint into the same processMessage dispatcher so customers can reach support from any channel.
Integrate Langfuse observability — Add Langfuse tracing (the dependency is already in package.json) to monitor agent performance, latency, and token consumption across all conversations.