Multi-Agent Patterns
Compose agents into systems that collaborate, delegate, and share state. This guide assumes familiarity with single-agent setup — see Fundamentals for agent basics.
Overview
A single agent handles one task well. Real applications need multiple agents working together: researching, analyzing, routing, and summarizing. ElsiumAI provides composable primitives for multi-agent orchestration:
| Pattern | Use case |
|---|---|
| Sequential | Pipeline where each agent’s output feeds the next |
| Parallel | Fan-out where agents run concurrently and results are collected |
| Supervisor | Routing prompt where a coordinator reasons over worker descriptions |
| Shared memory | Cross-agent data sharing without tight coupling |
| State machine | Typed state transitions for complex workflows |
Sequential Pattern
Agents execute in order. Each receives the previous agent’s output as input. Use this for multi-step processing pipelines.
import {
defineAgent,
runSequential,
type AgentDependencies,
} from "@elsium-ai/agents";
import { gateway } from "@elsium-ai/gateway";
const llm = gateway({
provider: "anthropic",
apiKey: process.env.ANTHROPIC_API_KEY!,
});
const deps: AgentDependencies = {
complete: (request) => llm.complete(request),
stream: (request) => llm.stream(request),
};
const researcher = defineAgent(
{
name: "researcher",
system: "Find key facts about the given topic.",
},
deps,
);
const analyst = defineAgent(
{
name: "analyst",
system: "Identify trends and insights from research notes.",
},
deps,
);
const writer = defineAgent(
{
name: "writer",
system: "Produce a concise summary from the analysis.",
},
deps,
);
const results = await runSequential(
[researcher, analyst, writer],
"The current state of open-source AI frameworks",
);
// results[0] is the researcher result, results[1] is the analyst result,
// and results[2] is the writer's final result.
Parallel Pattern
Agents run concurrently and their results are collected. Use this when tasks are independent and you want lower latency or diverse perspectives.
import {
defineAgent,
runParallel,
type AgentDependencies,
} from "@elsium-ai/agents";
import { gateway } from "@elsium-ai/gateway";
const llm = gateway({
provider: "anthropic",
apiKey: process.env.ANTHROPIC_API_KEY!,
});
const deps: AgentDependencies = {
complete: (request) => llm.complete(request),
stream: (request) => llm.stream(request),
};
const factChecker = defineAgent(
{
name: "fact-checker",
system: "Verify the factual accuracy of the given claims.",
},
deps,
);
const sentimentAnalyzer = defineAgent(
{
name: "sentiment",
system: "Analyze the sentiment and tone of the given text.",
},
deps,
);
const topicClassifier = defineAgent(
{
name: "classifier",
system: "Classify the text into relevant topic categories.",
},
deps,
);
const results = await runParallel(
[factChecker, sentimentAnalyzer, topicClassifier],
"ElsiumAI provides enterprise-grade reliability for AI applications.",
);
// results is an array with one entry per agent.
Supervisor Pattern
A supervisor agent receives the user’s request plus descriptions of the available workers, then produces the response itself. The helper does not execute the worker agents; use it for lightweight routing, triage, and planning prompts where worker names and responsibilities are enough context.
import {
defineAgent,
runSupervisor,
type AgentDependencies,
} from "@elsium-ai/agents";
import { gateway } from "@elsium-ai/gateway";
const llm = gateway({
provider: "anthropic",
apiKey: process.env.ANTHROPIC_API_KEY!,
});
const deps: AgentDependencies = {
complete: (request) => llm.complete(request),
stream: (request) => llm.stream(request),
};
const supervisor = defineAgent(
{
name: "support-router",
system:
"Route support requests by choosing the best worker description and explaining the next step.",
},
deps,
);
const billingAgent = defineAgent(
{
name: "billing",
system: "Handle billing questions: invoices, payments, and refunds.",
},
deps,
);
const technicalAgent = defineAgent(
{
name: "technical",
system: "Handle technical support: bugs, configuration, and integrations.",
},
deps,
);
const generalAgent = defineAgent(
{
name: "general",
system: "Handle general inquiries that do not fit billing or technical.",
},
deps,
);
const result = await runSupervisor(
supervisor,
[billingAgent, technicalAgent, generalAgent],
"I was charged twice for my last invoice.",
);
// result is produced by the supervisor agent after seeing the worker descriptions.
Shared Memory
When agents need to read and write shared state without direct coupling, use createSharedMemory. Multi-agent helpers write each agent’s output into shared memory under that agent’s name.
import {
createSharedMemory,
defineAgent,
runSequential,
type AgentDependencies,
} from "@elsium-ai/agents";
import { gateway } from "@elsium-ai/gateway";
const llm = gateway({
provider: "anthropic",
apiKey: process.env.ANTHROPIC_API_KEY!,
});
const deps: AgentDependencies = {
complete: (request) => llm.complete(request),
stream: (request) => llm.stream(request),
};
const memory = createSharedMemory();
const researcher = defineAgent(
{
name: "researcher",
system: "Extract key facts from the input.",
},
deps,
);
const analyst = defineAgent(
{
name: "analyst",
system: "Analyze the sentiment of the facts provided.",
},
deps,
);
await runSequential(
[researcher, analyst],
"Review of the Q4 earnings report...",
{ sharedMemory: memory },
);
const researcherOutput = memory.get("researcher");
const analystOutput = memory.get("analyst");
State Machines
For workflows with branching logic and typed state transitions, run an agent through executeStateMachine. Each state supplies its own prompt and transition function.
import { executeStateMachine, type AgentResult } from "@elsium-ai/agents";
import type { CompletionRequest } from "@elsium-ai/core";
import { gateway } from "@elsium-ai/gateway";
const llm = gateway({
provider: "anthropic",
apiKey: process.env.ANTHROPIC_API_KEY!,
});
const deps = {
complete: (request: CompletionRequest) => llm.complete(request),
stream: (request: CompletionRequest) => llm.stream(request),
};
const result = await executeStateMachine(
{
name: "order-flow",
system: "Process customer orders safely.",
},
{
initialState: "validate",
states: {
validate: {
system:
'Validate the order details. Respond with "valid" or "invalid".',
transition: (stateResult: AgentResult) => {
const output = String(stateResult.message.content)
.trim()
.toLowerCase();
return output.startsWith("valid") ? "approve" : "reject";
},
},
approve: {
system: "Check whether the order can be approved.",
transition: () => "fulfill",
},
fulfill: {
system: "Summarize fulfillment steps.",
terminal: true,
transition: () => "fulfill",
},
reject: {
system: "Explain why the order cannot proceed.",
terminal: true,
transition: () => "reject",
},
},
},
deps,
"Order ORD-1234 contains two widgets and totals $299.99.",
);
// result.finalState is "fulfill" or "reject".
// result.stateHistory contains the state transitions taken.
Combining Patterns
Patterns compose naturally. A common setup uses shared memory with both parallel and sequential stages:
import {
createSharedMemory,
defineAgent,
runParallel,
runSequential,
type AgentResult,
type AgentDependencies,
} from "@elsium-ai/agents";
import { gateway } from "@elsium-ai/gateway";
const llm = gateway({
provider: "anthropic",
apiKey: process.env.ANTHROPIC_API_KEY!,
});
const deps: AgentDependencies = {
complete: (request) => llm.complete(request),
stream: (request) => llm.stream(request),
};
const memory = createSharedMemory();
const technicalResearcher = defineAgent(
{
name: "technical-researcher",
system: "Evaluate technical risks.",
},
deps,
);
const businessResearcher = defineAgent(
{
name: "business-researcher",
system: "Evaluate business risks.",
},
deps,
);
const synthesizer = defineAgent(
{
name: "synthesizer",
system: "Combine research findings into a decision brief.",
},
deps,
);
const editor = defineAgent(
{
name: "editor",
system: "Polish the decision brief for executives.",
},
deps,
);
const perspectives = await runParallel(
[technicalResearcher, businessResearcher],
"Evaluate the migration to a new database",
{ sharedMemory: memory },
);
const report = await runSequential(
[synthesizer, editor],
JSON.stringify(
perspectives.map((result: AgentResult) => result.message.content),
),
{ sharedMemory: memory },
);
Best Practices
- Keep agents focused. Each agent should have a single, clear responsibility. A “do everything” agent is harder to debug and optimize than a pipeline of specialists.
- Use shared memory for coordination. Prefer shared memory over passing large payloads between agents. It decouples agents and makes the data flow explicit.
- Handle failures around the pattern. Wrap
runSequential,runParallel, orrunSupervisorin your own retry, fallback, or partial-result policy. - Set budgets per agent. In multi-agent systems, costs can compound quickly. Assign per-agent token and cost budgets to stay within limits.
- Trace across agents. Use
xrayMiddlewareon the gateway so all agent calls share a trace ID. This makes debugging multi-agent flows straightforward. - Test with mock providers. Use
@elsium-ai/testingto mock LLM responses and test your orchestration logic without real API calls.
import { mockProvider } from "@elsium-ai/testing";
const mock = mockProvider({
responses: [
{ content: "Fact 1, Fact 2, Fact 3" },
{ content: "Positive trend detected" },
],
});