Multi-Agent Systems in TypeScript
Architecture patterns for multi-agent systems including supervisor topologies, agent-to-agent communication, task delegation, and shared state management in Klivvr Agent.
A single agent can handle straightforward tasks — look up a customer, check a balance, process a refund. But complex workflows often benefit from multiple specialized agents working together. A triage agent determines the customer's intent and routes to a billing agent, a technical support agent, or a fraud investigation agent. Each specialist has its own tools, system prompt, and domain expertise.
This article covers the multi-agent architecture patterns used in Klivvr Agent, from supervisor topologies to communication protocols to shared state management.
Agent Topologies
Multi-agent systems can be organized in different topologies, each suited to different use cases.
The supervisor topology has a central coordinator that delegates tasks to specialist agents. The supervisor decides which agent to invoke, passes context, and aggregates results.
interface AgentDefinition {
name: string;
description: string;
capabilities: string[];
agent: Agent;
}
class SupervisorAgent {
private specialists: Map<string, AgentDefinition> = new Map();
private router: LLMClient;
registerSpecialist(definition: AgentDefinition): void {
this.specialists.set(definition.name, definition);
}
async handleRequest(userMessage: string): Promise<string> {
// Step 1: Determine which specialist to invoke
const routing = await this.route(userMessage);
// Step 2: Delegate to the specialist
const specialist = this.specialists.get(routing.agentName);
if (!specialist) {
return "I'm unable to help with that request.";
}
// Step 3: Run the specialist with context
const result = await specialist.agent.run(
routing.enrichedMessage ?? userMessage
);
// Step 4: Extract the final response
const lastMessage = result.messages[result.messages.length - 1];
return lastMessage.content;
}
private async route(
message: string
): Promise<{ agentName: string; enrichedMessage?: string }> {
const agentDescriptions = [...this.specialists.values()]
.map(
(s) =>
`- ${s.name}: ${s.description} (capabilities: ${s.capabilities.join(", ")})`
)
.join("\n");
const response = await this.router.chat({
model: "claude-haiku-4-5-20251001",
messages: [
{
role: "system",
content:
`You are a routing agent. Based on the user message, ` +
`determine which specialist agent should handle the request.\n\n` +
`Available agents:\n${agentDescriptions}\n\n` +
`Respond with JSON: { "agentName": "...", "enrichedMessage": "..." }`,
},
{ role: "user", content: message },
],
tools: [],
temperature: 0,
maxTokens: 200,
});
return JSON.parse(response.content);
}
}
// Setup
const supervisor = new SupervisorAgent();
supervisor.registerSpecialist({
name: "billing_agent",
description: "Handles billing inquiries, refunds, and payment issues",
capabilities: ["lookup_invoice", "issue_refund", "payment_status"],
agent: createBillingAgent(),
});
supervisor.registerSpecialist({
name: "account_agent",
description: "Handles account management, profile updates, and settings",
capabilities: ["update_profile", "change_settings", "account_status"],
agent: createAccountAgent(),
});
supervisor.registerSpecialist({
name: "fraud_agent",
description: "Investigates suspicious activity and security concerns",
capabilities: ["review_transaction", "lock_account", "verify_identity"],
agent: createFraudAgent(),
});The pipeline topology chains agents sequentially, where each agent processes the output of the previous one. This is useful for workflows with distinct phases.
class PipelineAgent {
private stages: Array<{
name: string;
agent: Agent;
extractOutput: (result: AgentState) => string;
}> = [];
addStage(config: {
name: string;
agent: Agent;
extractOutput: (result: AgentState) => string;
}): void {
this.stages.push(config);
}
async process(input: string): Promise<PipelineResult> {
let currentInput = input;
const stageResults: Array<{ stage: string; output: string }> = [];
for (const stage of this.stages) {
const result = await stage.agent.run(currentInput);
if (result.status === "failed") {
return {
success: false,
failedStage: stage.name,
stageResults,
};
}
const output = stage.extractOutput(result);
stageResults.push({ stage: stage.name, output });
currentInput = output;
}
return {
success: true,
stageResults,
finalOutput: currentInput,
};
}
}
interface PipelineResult {
success: boolean;
failedStage?: string;
stageResults: Array<{ stage: string; output: string }>;
finalOutput?: string;
}
// Example: Customer complaint pipeline
const complaintPipeline = new PipelineAgent();
complaintPipeline.addStage({
name: "classifier",
agent: createClassifierAgent(), // Classifies complaint type
extractOutput: (result) => {
const last = result.messages[result.messages.length - 1];
return last.content;
},
});
complaintPipeline.addStage({
name: "investigator",
agent: createInvestigatorAgent(), // Gathers relevant data
extractOutput: (result) => {
const toolData = result.toolResults.map((r) => JSON.stringify(r.result));
return toolData.join("\n");
},
});
complaintPipeline.addStage({
name: "resolver",
agent: createResolverAgent(), // Proposes and executes resolution
extractOutput: (result) => {
const last = result.messages[result.messages.length - 1];
return last.content;
},
});Agent Communication Protocol
When agents need to communicate directly, a typed message protocol ensures that messages are structured and validated.
interface AgentMessage<T = unknown> {
id: string;
from: string;
to: string;
type: string;
payload: T;
replyTo?: string;
timestamp: Date;
}
class AgentMessageBus {
private handlers = new Map<
string,
Array<(message: AgentMessage) => Promise<AgentMessage | void>>
>();
subscribe(
agentName: string,
handler: (message: AgentMessage) => Promise<AgentMessage | void>
): void {
const existing = this.handlers.get(agentName) ?? [];
existing.push(handler);
this.handlers.set(agentName, existing);
}
async send(message: AgentMessage): Promise<AgentMessage | void> {
const handlers = this.handlers.get(message.to) ?? [];
for (const handler of handlers) {
const reply = await handler(message);
if (reply) return reply;
}
}
async broadcast(
from: string,
type: string,
payload: unknown
): Promise<void> {
const message: AgentMessage = {
id: crypto.randomUUID(),
from,
to: "*",
type,
payload,
timestamp: new Date(),
};
for (const [agentName, handlers] of this.handlers) {
if (agentName === from) continue;
for (const handler of handlers) {
await handler(message);
}
}
}
}Shared State
Agents in a multi-agent system often need access to shared state — the customer being served, the actions already taken, and the current conversation context.
class SharedAgentState {
private state: Map<string, unknown> = new Map();
private locks: Map<string, string> = new Map();
async get<T>(key: string): Promise<T | undefined> {
return this.state.get(key) as T | undefined;
}
async set(key: string, value: unknown, agentId: string): Promise<void> {
const lock = this.locks.get(key);
if (lock && lock !== agentId) {
throw new Error(`Key "${key}" is locked by agent "${lock}"`);
}
this.state.set(key, value);
}
async acquireLock(key: string, agentId: string): Promise<boolean> {
if (this.locks.has(key)) return false;
this.locks.set(key, agentId);
return true;
}
async releaseLock(key: string, agentId: string): Promise<void> {
if (this.locks.get(key) === agentId) {
this.locks.delete(key);
}
}
async getActions(): Promise<ActionRecord[]> {
return (this.state.get("_actions") as ActionRecord[]) ?? [];
}
async recordAction(action: ActionRecord): Promise<void> {
const actions = await this.getActions();
actions.push(action);
this.state.set("_actions", actions);
}
}
interface ActionRecord {
agentName: string;
toolName: string;
arguments: Record<string, unknown>;
result: unknown;
timestamp: Date;
}Error Handling and Fallbacks
When a specialist agent fails, the supervisor needs a recovery strategy. The simplest approach is to try a different agent or fall back to a general-purpose agent.
class ResilientSupervisor extends SupervisorAgent {
private fallbackAgent: Agent;
async handleRequest(userMessage: string): Promise<string> {
try {
return await super.handleRequest(userMessage);
} catch (error) {
console.error(`Specialist failed: ${error}`);
// Fall back to general-purpose agent
const result = await this.fallbackAgent.run(
`A specialist agent failed to handle this request. ` +
`Please help the customer directly: ${userMessage}`
);
const lastMessage = result.messages[result.messages.length - 1];
return lastMessage.content;
}
}
}Conclusion
Multi-agent systems extend the capabilities of AI agents by combining specialized expertise into coordinated workflows. The supervisor topology provides centralized routing and control. The pipeline topology enables sequential processing through distinct phases. Typed communication protocols keep inter-agent messages structured. Shared state gives agents visibility into each other's actions. And fallback strategies ensure that individual agent failures do not break the entire system. In Klivvr Agent, multi-agent patterns handle the most complex customer interactions — those that span billing, account management, and fraud investigation in a single conversation.
Related Articles
AI Agents in Fintech Operations
How AI agents automate fintech operational workflows including compliance monitoring, fraud detection, dispute resolution, and regulatory reporting — with insights from Klivvr Agent deployments.
Human-in-the-Loop Patterns for AI Agents
How to design effective human-in-the-loop workflows for AI agents, covering escalation policies, approval workflows, the autonomy ladder, and trust-building strategies.
Testing Strategies for AI Agents
A practical guide to testing AI agents including unit testing tools, integration testing agent loops, evaluation frameworks, and mock LLM strategies used in Klivvr Agent.