English

The moment you have more than one AI agent in a system, you no longer have an AI problem — you have a distributed systems problem that happens to involve LLMs. Here's how we structure multi-agent orchestration so it survives production traffic instead of collapsing into an expensive infinite loop.

Why One Giant Agent Doesn't Scale

A single agent with forty tools and a sprawling system prompt tends to degrade in a specific way: tool selection accuracy drops as the tool list grows, the context window fills with irrelevant tool schemas, and debugging becomes archaeology. Splitting responsibilities across specialized agents fixes all three, but it introduces coordination overhead you have to design for explicitly.

The Supervisor-Worker Pattern

The pattern that scales best in practice is a supervisor agent that plans and delegates, with narrow worker agents that each own one domain:

  • Supervisor — decomposes the user's request, chooses which worker(s) to invoke, merges results, decides when the task is done
  • Workers — each has a small, focused tool set and a system prompt scoped to one job (e.g., a billing-agent, a scheduling-agent, a research-agent)
interface AgentResult {
  agentName: string;
  output: unknown;
  confidence: number;
  needsFollowUp: boolean;
}

class SupervisorAgent {
  private workers: Map<string, WorkerAgent> = new Map();

  async handle(task: string): Promise<string> {
    const plan = await this.planner.decompose(task);
    const results: AgentResult[] = [];

    for (const step of plan.steps) {
      const worker = this.workers.get(step.agentName);
      if (!worker) throw new Error(`Unknown worker: ${step.agentName}`);

      const result = await worker.run(step.instruction, {
        sharedContext: this.memory.snapshot(),
      });

      results.push(result);
      this.memory.append(step.agentName, result);
    }

    return this.synthesizer.merge(results);
  }
}

Keep the supervisor's tool list to exactly one tool per worker — "delegate to billing-agent", "delegate to scheduling-agent" — never expose worker-internal tools at the supervisor level. That single constraint prevents most of the routing confusion we've seen in early multi-agent designs.

Hand-Off Protocols

When one agent needs to pass control to another mid-conversation, define the hand-off as an explicit, structured event — not an implicit "the model decided to call another agent" side effect.

type HandOff = {
  fromAgent: string;
  toAgent: string;
  reason: string;
  contextPayload: Record<string, unknown>;
  conversationTurn: number;
};

A few rules that keep hand-offs sane:

  • The receiving agent gets a summary, not the full transcript — dumping raw history across agent boundaries blows up context and re-introduces noise the sender already filtered out
  • Every hand-off is logged with a reason string, so a bad routing decision is debuggable after the fact
  • Cap hand-offs per conversation (e.g., 4) and fail closed to a human if that cap is hit

Shared State and Memory Across Agents

Agents need a common view of what's already been tried, without stepping on each other's writes. A simple, effective approach is a shared, append-only memory store keyed by conversation ID, with each agent only allowed to append its own namespace:

class SharedMemory:
    def __init__(self, conversation_id: str, store):
        self.conversation_id = conversation_id
        self.store = store

    def append(self, agent_name: str, entry: dict):
        key = f"conv:{self.conversation_id}:{agent_name}"
        self.store.rpush(key, json.dumps(entry))

    def snapshot(self) -> dict:
        pattern = f"conv:{self.conversation_id}:*"
        return {
            key.split(":")[-1]: [json.loads(e) for e in self.store.lrange(key, 0, -1)]
            for key in self.store.scan_iter(pattern)
        }

This gives the supervisor a full picture without any agent overwriting another's findings, and it makes replay/debugging straightforward — you can reconstruct exactly what each agent saw at each step.

Avoiding Infinite Loops and Runaway Costs

This is the failure mode that actually takes down multi-agent systems in production: agent A hands off to agent B, which decides the task needs agent A again, forever. Guardrails that are non-negotiable:

  • Hard step budget per conversation (e.g., 15 total agent invocations) enforced by the orchestrator, not by the agents themselves
  • Token budget tracked centrally and checked before every LLM call — kill the run and escalate to a human once exceeded
  • Cycle detection — track the sequence of agent invocations and refuse a repeat of the same (agent, near-identical-input) pair
  • Timeouts per worker call, independent of the overall budget, so one stuck tool call doesn't hang the whole orchestration
class Orchestrator {
  private readonly maxSteps = 15;
  private readonly maxTokens = 200_000;
  private stepCount = 0;
  private tokensUsed = 0;

  async invoke(agent: WorkerAgent, input: string): Promise<AgentResult> {
    if (this.stepCount >= this.maxSteps) {
      throw new BudgetExceededError('step budget exhausted, escalating to human');
    }
    if (this.tokensUsed >= this.maxTokens) {
      throw new BudgetExceededError('token budget exhausted, escalating to human');
    }

    this.stepCount += 1;
    const result = await withTimeout(agent.run(input), 20_000);
    this.tokensUsed += result.tokenUsage;
    return result;
  }
}

A Minimal Working Orchestration

Putting it together — a small triage system with a supervisor and two workers:

const supervisor = new SupervisorAgent({
  workers: {
    'research-agent': new WorkerAgent({ tools: [webSearch, docLookup] }),
    'drafting-agent': new WorkerAgent({ tools: [templateFill] }),
  },
  maxSteps: 10,
});

const answer = await supervisor.handle(
  'Find our current refund policy and draft a customer-facing summary of it'
);

The research agent never sees the drafting agent's tools, and vice versa — each agent's tool schema stays small, which in practice is the single biggest lever for tool-call accuracy in multi-agent setups.

Failure Recovery

Assume any worker call can fail, time out, or return low-confidence output, because at scale it will:

  • Retry once with a slightly rephrased instruction before escalating
  • Surface confidence scores from workers and let the supervisor decide whether to accept, retry, or ask a different worker
  • On repeated failure, degrade gracefully to a human-in-the-loop handoff rather than returning a hallucinated answer

Wrapping Up

Multi-agent orchestration earns its complexity when tasks genuinely span multiple domains, but it demands the same rigor as any distributed system: explicit contracts between components, shared state with clear ownership, and hard budgets that prevent runaway loops. Get those three things right and a supervisor-worker architecture will outperform a single sprawling agent both on accuracy and on your monthly token bill.

0
0
0
0