Cohere Agent Mesh for Multi-Channel Customer Support Triage
Orchestrate multiple Cohere-powered specialist agents to triage customer support tickets across email, chat, and social media with confidence-based routing and cost tracking.
SMB support teams juggle inquiries from multiple channels—email, live chat, and social media—often with manual routing to specialists. They need an automated system that intelligently classifies and routes each request to the right agent, avoiding misrouting and high operational costs.
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 recipe builds a multi-channel customer support triage system powered by Cohere’s intent classification and a confidence-gated agent mesh. You’ll create an Express server that ingests tickets from email, chat, and social media, classifies them with Cohere’s API, evaluates routing confidence against configurable thresholds, dispatches to specialist agents (returns, billing, technical support) via MCP, and tracks per-channel LLM costs. The architecture uses five REAA packages for the agent-mesh scaffolding: @reaatech/agent-mesh (core types), @reaatech/agent-mesh-confidence (confidence gating), @reaatech/agent-mesh-router (MCP dispatch), @reaatech/confidence-router-core (fallback logic), and @reaatech/llm-cost-telemetry (cost tracking).
Prerequisites
Node.js >= 22 and pnpm 10 installed
A Cohere API key — set it as COHERE_API_KEY in your .env file
Familiarity with TypeScript, Express, and basic async/await patterns
Step 1: Create the project and install dependencies
Start in an empty directory. Create package.json with every dependency pinned to an exact version — no ranges.
Expected output: pnpm resolves all dependencies and writes pnpm-lock.yaml.
Step 2: Configure environment variables
Create a .env file (or copy from .env.example) with every variable the pipeline reads. Each AGENT_*_ENDPOINT points to a specialist agent’s MCP server — for development you can leave the default URLs.
env
# Env vars used by cohere-agent-mesh-for-multi-channel-customer-support-triage.# The builder adds entries here as it wires up each integration.# Keep placeholders only — never commit real values.NODE_ENV=developmentPORT=3001COHERE_API_KEY=<your-cohere-api-key>COHERE_MODEL=command-a-03-2025MCP_REQUEST_TIMEOUT_MS=30000MCP_MAX_RETRIES=3ENABLE_CLARIFICATION=trueDEFAULT_DAILY_BUDGET=100.0AGENT_RETURNS_ENDPOINT=http://localhost:4001AGENT_BILLING_ENDPOINT=http://localhost:4002AGENT_TECHNICAL_SUPPORT_ENDPOINT=http://localhost:4003
Expected output: A .env file with placeholders. Replace <your-cohere-api-key> with a real Cohere API key before running the server.
Step 3: Define core types
Create src/types.ts to hold the domain types your pipeline uses. These define the shape of incoming tickets, outgoing responses, and re-export the REAA package types consumed by the rest of src/.
typescript
export type TicketChannel = "email" | "chat" | "social";export interface TicketRequest { channel: TicketChannel; customerId: string; message: string; metadata?: Record<string, unknown>;}export interface TicketResponse { ticketId: string; action: "route" | "clarify" | "fallback"; agentId?: string; content: string; clarificationQuestion?: string; costUsd: number;}export type { AgentConfig } from "@reaatech/agent-mesh";export type { ClassifierOutput } from "@reaatech/agent-mesh";export type { ConfidenceDecision } from "@reaatech/agent-mesh";export type { CostSpan } from "@reaatech/llm-cost-telemetry";
Expected output: A types file that compiles cleanly. Run pnpm typecheck — it should pass with zero errors at this stage.
Step 4: Build the configuration module
Create src/config.ts to load and validate all runtime configuration. It imports dotenv/config at the top to populate process.env, uses env from @reaatech/agent-mesh for the shared mesh environment variables, and calls validateConfig + mergeConfig from @reaatech/confidence-router-core to fail-fast if thresholds are misconfigured.
Expected output: A frozen config object. The validateConfig call throws during module load if thresholds are invalid.
Step 5: Set up the agent mesh library
Create src/lib/agent-mesh.ts — the central library that re-exports everything from all five REAA packages and builds the agent registry. This is where you assemble the specialist agent configurations (returns, billing, technical-support), validate each against AgentConfigSchema, and expose helpers for looking up agents and building context packets.
typescript
import { IncomingRequestSchema, type IncomingRequest, AgentResponseSchema, type AgentResponse, ClassifierOutputSchema, type ClassifierOutput, ContextPacketSchema, type ContextPacket, AgentConfigSchema, type AgentConfig, SessionRecordSchema, type SessionRecord, TurnEntrySchema, type TurnEntry, SessionStatus, SERVICE_NAME, CACHE_TTL,} from "@reaatech/agent-mesh";import { dispatchToAgent, buildTurnEntry, formatAgentResponse, shouldCloseSession, getUpdatedWorkflowState, mcpClientFactory } from "@reaatech/agent-mesh-router";import { evaluateConfidenceGate, generateClarificationQuestion, clarificationCache } from "@reaatech/agent-mesh-confidence";import { DecisionEngine, mergeConfig, RouterError, RouterErrorType, DEFAULT_CONFIG, validateConfig } from "@reaatech/confidence-router-core";import { generateId, now, loadConfig, calculateCostFromTokens, type CostSpan, type TelemetryContext, CostSpanSchema, getWindowStart, getWindowEnd, percentage, roundTo, retryWithBackoff } from "@reaatech/llm-cost-telemetry";
Then define the three specialist agents and build the registry:
typescript
const agentDefinitions = [ { agent_id: "returns", display_name: "Returns Agent", description: "Handles product returns, refunds, and exchange requests", endpoint: process.env.AGENT_RETURNS_ENDPOINT || "http://localhost:4001", type: "mcp" as const, confidence_threshold: 0.7, clarification_required: true, examples: ["Process a return for order ABC123"], }, { agent_id: "billing", display_name: "Billing Agent", description: "Handles billing inquiries, payment issues, and invoice questions", endpoint: process.env.AGENT_BILLING_ENDPOINT || "http://localhost:4002", type: "mcp" as const, confidence_threshold: 0.7, clarification_required: true, examples: ["Check my invoice status for last month"], }, { agent_id: "technical-support", display_name: "Technical Support Agent", description: "Handles technical issues, troubleshooting, and product support", endpoint: process.env.AGENT_TECHNICAL_SUPPORT_ENDPOINT || "http://localhost:4003", type: "mcp" as const, confidence_threshold: 0.7, clarification_required: true, examples: ["My computer won't connect to the network"], },];const validatedAgents = agentDefinitions.map((def) => AgentConfigSchema.parse(def));export const agentRegistry = new Map<string, AgentConfig>( validatedAgents.map((agent) => [agent.agent_id, agent]),);
Expected output: A 170+ line library that re-exports the entire REAA package surface. Run pnpm typecheck — type errors here usually mean a missing import path suffix (.js).
Step 6: Create the logger
Create src/lib/logger.ts — a small structured JSON logger used throughout the application. Every call to the Cohere API, every routing decision, and every server lifecycle event flows through it.
Expected output: A logger that writes one JSON line per call, tagged with service: "agent-mesh" and a UTC timestamp.
Step 7: Create the Cohere classification client
Create src/lib/cohere-client.ts to wrap the Cohere API. The classifyIntent function sends a system prompt listing the three specialist agents, calls Cohere’s chat endpoint, and parses the structured JSON response into a ClassifierOutput.
typescript
import { CohereClientV2, CohereError, CohereTimeoutError } from "cohere-ai";import { RouterError, RouterErrorType } from "@reaatech/confidence-router-core";import { type ClassifierOutput, ClassifierOutputSchema } from "@reaatech/agent-mesh";import { config } from "../config.js";import { logger } from "./logger.js";export function createCohereClient(): CohereClientV2 { return new CohereClientV2({});}export async function classifyIntent( params: { client: CohereClientV2; message: string },): Promise<ClassifierOutput> { const { client, message } = params; const systemPrompt = `You are a customer support ticket classifier. Classify the following support request into one of these categories:- returns: Product returns, refunds, and exchange requests- billing: Billing inquiries, payment issues, and invoice questions- technical-support: Technical issues, troubleshooting, and product supportRespond with a JSON object containing:- agent_id: one of "returns", "billing", or "technical-support"- confidence: a number between 0 and 1 indicating how confident you are- ambiguous: boolean indicating if the request is ambiguous- detected_language: the ISO 639-1 language code of the message- intent_summary: a brief one-sentence summary of the intent- entities: an object with any extracted entitiesOnly respond with the JSON object, no other text.`; try { const response = await client.chat({ model: config.COHERE_MODEL, messages: [ { role: "system", content: systemPrompt }, { role: "user", content: message }, ], }); const contentBlock = response.message.content?.[0]; const textContent = contentBlock && "text" in contentBlock ? (contentBlock as { text: string }).text : ""; let parsed: Record<string, unknown>; try { parsed = JSON.parse(textContent) as Record<string, unknown>; } catch { parsed = { agent_id: "unknown", confidence: 0, ambiguous: true, detected_language: "en", intent_summary: "Failed to parse classification", entities: {}, }; } return ClassifierOutputSchema.parse(parsed); } catch (err) { if (err instanceof CohereTimeoutError || err instanceof CohereError) { throw new RouterError( RouterErrorType.CLASSIFICATION_ERROR, "Cohere API error: " + err.message, ); } throw err; }}
The classifyWithMastra function attempts a dynamic import of mastra (which may not be installed or importable at runtime), logs success or failure, then falls through to a direct CohereClientV2 call:
Expected output: Two exported functions ready for the classification service. CohereClientV2 reads the API key from process.env.COHERE_API_KEY at runtime — no manual key injection needed.
Step 8: Build the classification service
Create src/services/classification.ts to sit between the route handler and the Cohere client. It handles the empty-message edge case early and delegates real messages to classifyWithMastra.
Expected output: A service that returns a zero-confidence fallback for empty messages and delegates valid messages to the Cohere pipeline.
Step 9: Implement the session store
Create src/services/session.ts to manage conversation sessions. Each ticket creates a session that tracks conversation turns, workflow state, and lifecycle status.
Expected output: A tracker that pushes cost spans, computes per-channel breakdowns using getWindowStart/getWindowEnd for the daily window, and logs a warning when the daily budget is exceeded.
Step 11: Assemble the routing service
This is the core of the pipeline. Create src/services/routing.ts to orchestrate: Cohere classification → confidence gate evaluation → dispatch to the right specialist agent (with retries and circuit-breaker fallback) → session turn tracking.
typescript
import { evaluateConfidenceGate, generateClarificationQuestion } from "@reaatech/agent-mesh-confidence";import { dispatchToAgent, buildTurnEntry, shouldCloseSession, formatAgentResponse, getUpdatedWorkflowState, mcpClientFactory } from "@reaatech/agent-mesh-router";import { retryWithBackoff } from "@reaatech/llm-cost-telemetry";import type { SessionRecord, ClassifierOutput, AgentResponse } from "@reaatech/agent-mesh";import { agentRegistry, findAgent, getDefaultAgent, RouterError, RouterErrorType } from "../lib/agent-mesh.js";import { classifyTicket } from "./classification.js";import { sessionStore } from "./session.js";export async function routeTicket(params: { message: string; channel: string
Expected output: A single routeTicket function that encapsulates the entire triage pipeline. When dispatchToAgent throws a circuit-breaker error, the routing falls back to the default agent instead of crashing — a key reliability pattern.
Step 12: Create Express API middleware, routes, and server
Start with the error handler. Create src/api/middleware/error-handler.ts to map RouterError types to HTTP status codes:
typescript
import type { Request, Response, NextFunction } from "express";import { RouterError, RouterErrorType } from "@reaatech/confidence-router-core";import { logger } from "../../lib/logger.js";export function errorHandler( err: Error, req: Request, res: Response, _next: NextFunction,): void { void _next; logger.error({ event: "unhandled_error", error: err.message, method: req.method, path: req.path, }); if (err.constructor.name === "ZodError") { res.status(400).json({ error: err.message, type: "VALIDATION_ERROR", }); return; } if (err instanceof RouterError) { let status: number; switch (err.type) { case RouterErrorType.CLASSIFICATION_ERROR: status = 502; break; case RouterErrorType.CLASSIFIER_NOT_FOUND: status = 404; break; case RouterErrorType.CONFIGURATION_ERROR: case RouterErrorType.THRESHOLD_INVALID: status = 500; break; default: status = 500; } res.status(status).json({ error: err.message, type: err.type, }); return; } res.status(500).json({ error: err.message || "Internal Server Error", type: "INTERNAL_ERROR", });}
Create the ticket route handler at src/api/routes/tickets.ts:
import express, { type Request, type Response } from "express";import ticketRouter from "./api/routes/tickets.js";import { errorHandler } from "./api/middleware/error-handler.js";import { config } from "./config.js";import { logger } from "./lib/logger.js";import { SERVICE_NAME } from "@reaatech/agent-mesh";import { mcpClientFactory } from "@reaatech/agent-mesh-router";export function createApp() { const app = express(); app.use(express.json()); app.get("/health", (_req: Request, res: Response) => { res.json({ status: "ok", service: SERVICE_NAME }); }); app.use("/api/v1/tickets", ticketRouter); app.use(errorHandler); return app;}export function startServer(app: ReturnType<typeof express>) { const server = app.listen(config.PORT, () => { logger.info({ event: "server_started", port: config.PORT }); }); const shutdown = () => { logger.info({ event: "shutdown_initiated" }); void mcpClientFactory.closeAll().then(() => { server.close(() => { logger.info({ event: "shutdown_complete" }); process.exit(0); }); }); }; process.on("SIGTERM", shutdown); process.on("SIGINT", shutdown); return server;}
Expected output: A complete Express application with GET /health, POST /api/v1/tickets, and structured error handling that maps RouterError types to appropriate HTTP status codes.
Step 13: Wire up the entry point
Create src/index.ts as the server bootstrap:
typescript
import "dotenv/config";import { createApp, startServer } from "./server.js";startServer(createApp());
Expected output: Run the server with npx tsx src/index.ts — the Express server starts on port 3001. You can curl GET http://localhost:3001/health and get {"status":"ok","service":"agent-mesh"}.
Step 14: Run the test suite
The project includes a test suite covering every module. Tests mock external HTTP calls via vi.mock — no live Cohere or MCP endpoints required. Run them now:
terminal
pnpm test
Expected output: Vitest runs all 99 tests across 47 test suites with coverage and outputs a JSON report to vitest-report.json. You should see numFailedTests: 0 and numPassedTests: 99.
Verify type-safety and linting:
terminal
pnpm typecheckpnpm lint
Expected output:tsc --noEmit exits 0 with zero type errors. ESLint exits 0 with zero violations.
Next steps
Add a frontend dashboard — the project already includes a Next.js app scaffold under app/. Extend it to show real-time ticket metrics, per-channel cost breakdowns, and routing decisions using the data from costTracker.getPerChannelBreakdown()
Persist sessions to a database — swap the in-memory SessionStore for a Postgres or Redis-backed store so sessions survive server restarts
Integrate real specialist agents — replace the MCP stub agents with actual Cohere-powered agents that run on the AGENT_*_ENDPOINT URLs, each handling its domain with domain-specific prompts and tools