Real-world workflows require specialized agents collaborating. Instead of one monolithic agent, you create a network of specialists that discover, negotiate, and delegate tasks among themselves. This pattern enables scalability, autonomy, and fault isolation.
| Pattern | Description | When to Use |
|---|---|---|
| Direct Handoff | Agent passes control to another agent explicitly | Clear escalation paths, hierarchical teams |
| Pub/Sub (Broadcast) | Agent publishes a task; subscribers bid or claim | Open task queues, load balancing |
| Message Broker | Agents communicate via a shared queue or bus | Decoupled systems, async workflows |
| Blackboard | Shared state space where agents read/write | Collaborative problem solving |
MAF orchestrators natively support agent-to-agent handoffs. The orchestrator routes context and controls the conversation flow:
const triageAgent = new Agent({
name: "Triage",
instructions: "Route users to the right specialist: Billing or Support.",
tools: [
new Tool({
name: "transferToBilling",
handler: (ctx) => orchestrator.handoff("Billing", ctx)
}),
new Tool({
name: "transferToSupport",
handler: (ctx) => orchestrator.handoff("Support", ctx)
})
]
});
const billingAgent = new Agent({
name: "Billing",
instructions: "Handle billing inquiries, invoices, and payments."
});
const supportAgent = new Agent({
name: "Support",
instructions: "Handle technical support and troubleshooting."
});
const orchestrator = new Orchestrator({
agents: [triageAgent, billingAgent, supportAgent],
defaultAgent: triageAgent
});
For loosely coupled agent networks, use a message broker. Agents communicate asynchronously through topics and queues:
const { ServiceBusClient } = require("@azure/service-bus");
async function publishTask(task) {
const sender = sbClient.createSender("agent-tasks");
await sender.sendMessages({
body: task,
sessionId: task.type // enables session-based processing
});
}
class OrderProcessorAgent {
async handleMessage(message) {
const order = message.body;
// Process order, then publish to next stage
await publishTask({ type: "fulfillment", orderId: order.id });
}
}
Agents can register themselves in a service registry and discover peers dynamically:
class AgentRegistry {
constructor() {
this.agents = new Map();
}
register(name, capabilities, endpoint) {
this.agents.set(name, { capabilities, endpoint, status: "available" });
}
findAgents(capability) {
return Array.from(this.agents.entries())
.filter(([_, agent]) => agent.capabilities.includes(capability))
.map(([name, agent]) => ({ name, endpoint: agent.endpoint }));
}
}
// Agents discover peers at runtime
const registry = new AgentRegistry();
registry.register("DataAnalyzer", ["analysis", "charting"], "http://analyzer:5001");
registry.register("ReportGenerator", ["reporting"], "http://reporter:5002");
const analysts = registry.findAgents("analysis");
In advanced networks, agents negotiate who handles a task. A common approach is a contract-net protocol:
class ContractNetProtocol {
async solicitBids(task, availableAgents) {
const bids = await Promise.all(
availableAgents.map(agent => this.requestBid(agent, task))
);
const winner = bids.reduce((best, bid) =>
bid.confidence > best.confidence ? bid : best
);
return winner;
}
async requestBid(agent, task) {
const response = await fetch(`${agent.endpoint}/bid`, {
method: "POST",
body: JSON.stringify(task)
});
const { confidence, eta } = await response.json();
return { agent, confidence, eta };
}
}
Distributed tracing is critical. Use correlation IDs and OpenTelemetry to trace messages across agents:
const tracer = opentelemetry.trace.getTracer("agent-network");
async function handleMessage(message) {
const span = tracer.startSpan("agent.process", {
attributes: {
"agent.name": this.name,
"message.type": message.type,
"correlation.id": message.correlationId
}
});
try { await this.process(message); }
finally { span.end(); }
}