Skip to main content

Multi-Agent Orchestrator

7 min read

Multi-Agent Orchestrator

Coordinate multiple agents with concurrency control, per-agent configuration, and a reactive Directive System backbone.

The multi-agent orchestrator has full feature parity with the single-agent orchestrator: guardrails (orchestrator-level + per-agent), streaming, approval workflows, pause/resume, memory, hooks, retry, budget, plugins, time-travel debugging, constraints, and resolvers. Each registered agent becomes a namespaced module in a Directive System.


Setup

Multi-agent orchestration builds on the Agent Orchestrator adapter. Define your agents, a runner, and register them:

import { createMultiAgentOrchestrator, createPIIGuardrail } from '@directive-run/ai';
import type { AgentLike, AgentRunner, MultiAgentOrchestrator } from '@directive-run/ai';

const researcher: AgentLike = {
  name: 'researcher',
  instructions: 'You are a research assistant. Find relevant information on the given topic.',
  model: 'gpt-4',
};

const writer: AgentLike = {
  name: 'writer',
  instructions: 'You are a technical writer. Write clear, concise content from research notes.',
  model: 'gpt-4',
};

const reviewer: AgentLike = {
  name: 'reviewer',
  instructions: 'You review drafts for accuracy and clarity. Return "approve" or revision notes.',
  model: 'gpt-4',
};

const runner: AgentRunner = async (agent, input, options) => {
  return { output: '...', totalTokens: 0 };
};

Creating the Orchestrator

const orchestrator = createMultiAgentOrchestrator({
  runner,

  agents: {
    researcher: {
      agent: researcher,
      maxConcurrent: 3,
      timeout: 30000,
      capabilities: ['search', 'summarize'],
      description: 'Finds and summarizes information on any topic',
    },
    writer: {
      agent: writer,
      maxConcurrent: 1,
      timeout: 60000,
      guardrails: {
        output: [createPIIGuardrail({ redact: true })],
      },
    },
    reviewer: {
      agent: reviewer,
      maxConcurrent: 1,
      timeout: 30000,
    },
  },
});

Configuration Reference

OptionTypeDefaultDescription
runnerAgentRunnerrequiredBase LLM execution function
agentsAgentRegistryrequiredMap of agent ID to AgentRegistration
patternsRecord<string, ExecutionPattern>{}Named execution patterns
guardrailsGuardrailsConfigOrchestrator-level guardrails (applied to all agents)
hooksMultiAgentLifecycleHooksLifecycle hooks for observability
memoryAgentMemoryShared memory across all agents
agentRetryAgentRetryConfigDefault retry config for all agents
maxTokenBudgetnumberMaximum token budget across all agent runs
budgetWarningThresholdnumber0.8Fires onBudgetWarning at this fraction of budget
onBudgetWarning(event) => voidBudget warning callback
pluginsPlugin[][]Plugins for the underlying Directive System
onApprovalRequest(request) => voidApproval request callback
autoApproveToolCallsbooleantrueAuto-approve tool calls
approvalTimeoutMsnumber300000Approval timeout (ms)
constraintsRecord<string, OrchestratorConstraint>Orchestrator-level constraints
resolversRecord<string, OrchestratorResolver>Orchestrator-level resolvers
circuitBreakerCircuitBreakerOrchestrator-level circuit breaker
deriveRecord<string, CrossAgentDerivationFn>Cross-agent derivations
scratchpad{ init: Record<string, unknown> }Shared scratchpad
breakpointsBreakpointConfig[][]Breakpoints
onBreakpoint(request) => voidBreakpoint callback
breakpointTimeoutMsnumber300000Breakpoint auto-cancel timeout
onHandoff(request) => voidHandoff start callback
onHandoffComplete(result) => voidHandoff complete callback
maxHandoffHistorynumber1000Max completed handoff results to retain
debugbooleanfalseEnable debug logging and time-travel

Agent Registration

Each entry in the agents map is an AgentRegistration:

FieldTypeDefaultDescription
agentAgentLikerequiredThe agent instance
maxConcurrentnumber1Max parallel runs for this agent
timeoutnumberPer-run timeout (ms)
runOptionsOmit<RunOptions, 'signal'>Default run options
descriptionstringHuman-readable description
capabilitiesstring[]Capability tags for routing
guardrails.inputGuardrailFn[]Per-agent input guardrails
guardrails.outputGuardrailFn[]Per-agent output guardrails
guardrails.toolCallGuardrailFn[]Per-agent tool call guardrails
retryAgentRetryConfigPer-agent retry config
constraintsRecord<string, OrchestratorConstraint>Per-agent constraints
resolversRecord<string, OrchestratorResolver>Per-agent resolvers
memoryAgentMemoryPer-agent memory
circuitBreakerCircuitBreakerPer-agent circuit breaker

Running a Single Agent

const result = await orchestrator.runAgent<string>('researcher', 'What is WebAssembly?');

console.log(result.output);
console.log(result.totalTokens);

If all maxConcurrent slots are occupied, the call waits until a slot opens (async semaphore – no polling).

// With cancellation
const controller = new AbortController();
const result = await orchestrator.runAgent('researcher', 'Explain WASM', {
  signal: controller.signal,
});

run() and runStream() Aliases

const result = await orchestrator.run<string>('researcher', 'What is WebAssembly?');
const { stream } = orchestrator.runStream<string>('writer', 'Write about AI');

totalTokens Getter

console.log(orchestrator.totalTokens);  // Cumulative across all agents

Agent State

const state = orchestrator.getAgentState('researcher');
console.log(state.status);      // 'idle' | 'running' | 'completed' | 'error'
console.log(state.runCount);
console.log(state.totalTokens);
console.log(state.lastInput);
console.log(state.lastOutput);
console.log(state.lastError);

const allStates = orchestrator.getAllAgentStates();

Pause & Resume

orchestrator.pause();
orchestrator.resume();

When paused, runAgent() calls throw immediately.

Wait for Idle

await orchestrator.waitForIdle();
await orchestrator.waitForIdle(10000);  // With timeout

Reset and Dispose

orchestrator.reset();   // Reset states, drain semaphores, clear handoffs
orchestrator.dispose();  // Reset + destroy the Directive System

Dynamic Agent Management

orchestrator.registerAgent('editor', {
  agent: editor,
  maxConcurrent: 2,
  timeout: 30000,
  capabilities: ['proofread', 'format'],
});

const result = await orchestrator.runAgent('editor', 'Fix the grammar...');

console.log(orchestrator.getAgentIds());

orchestrator.unregisterAgent('editor');  // Must be idle

Guardrails

Guardrails run at two levels: orchestrator-level (all agents) then per-agent (additive):

const orchestrator = createMultiAgentOrchestrator({
  runner,
  agents: {
    researcher: {
      agent: researcher,
      guardrails: {
        output: [createOutputTypeGuardrail({ type: 'string', minStringLength: 10 })],
      },
    },
    writer: {
      agent: writer,
      guardrails: {
        input: [createPIIGuardrail({ redact: true })],
        output: [createPIIGuardrail()],
        toolCall: [createToolGuardrail({ denylist: ['shell'] })],
      },
    },
  },

  guardrails: {
    input: [createPIIGuardrail({ redact: true })],
    toolCall: [createToolGuardrail({ denylist: ['eval', 'exec'] })],
  },
});

See Guardrails for the full API.


Streaming

const { stream, result, abort } = orchestrator.runAgentStream<string>('writer', 'Write about AI');

for await (const chunk of stream) {
  switch (chunk.type) {
    case 'token':
      process.stdout.write(chunk.data);
      break;
    case 'done':
      console.log(`\n${chunk.totalTokens} tokens`);
      break;
  }
}

See Streaming for chunk types and stream operators.


Approval Workflow

const orchestrator = createMultiAgentOrchestrator({
  runner,
  agents: { researcher: { agent: researcher }, writer: { agent: writer } },
  autoApproveToolCalls: false,
  approvalTimeoutMs: 60000,

  onApprovalRequest: (request) => {
    broadcastToAdminDashboard(request);
  },
});

orchestrator.approve(requestId);
orchestrator.reject(requestId, 'Denied by reviewer');

Lifecycle Hooks

const orchestrator = createMultiAgentOrchestrator({
  runner,
  agents: { /* ... */ },

  hooks: {
    onAgentStart: ({ agentId, agentName, input, timestamp }) => {
      console.log(`[${agentId}] Starting`);
    },
    onAgentComplete: ({ agentId, tokenUsage, durationMs }) => {
      console.log(`[${agentId}] Done: ${tokenUsage} tokens in ${durationMs}ms`);
    },
    onAgentError: ({ agentId, error, durationMs }) => {
      console.error(`[${agentId}] Failed:`, error.message);
    },
    onGuardrailCheck: ({ agentId, guardrailName, guardrailType, passed, reason }) => { },
    onAgentRetry: ({ agentId, attempt, error, delayMs }) => { },
    onPatternStart: ({ patternId, patternType }) => { },
    onPatternComplete: ({ patternId, durationMs, error }) => { },
  },
});

Retries

const orchestrator = createMultiAgentOrchestrator({
  runner,
  agents: {
    researcher: {
      agent: researcher,
      retry: { attempts: 5, backoff: 'exponential', baseDelayMs: 500 },
    },
    writer: { agent: writer },
  },

  agentRetry: {
    attempts: 3,
    backoff: 'exponential',
    baseDelayMs: 1000,
    maxDelayMs: 30000,
    isRetryable: (error) => error.message.includes('429'),
  },
});

Budget Control

const orchestrator = createMultiAgentOrchestrator({
  runner,
  agents: { researcher: { agent: researcher }, writer: { agent: writer } },
  maxTokenBudget: 50000,
  budgetWarningThreshold: 0.75,
  onBudgetWarning: ({ currentTokens, maxBudget, percentage }) => {
    console.warn(`Budget: ${(percentage * 100).toFixed(0)}% used`);
  },
});

Constraints & Resolvers

import { requirementGuard } from '@directive-run/core/adapter-utils';

const orchestrator = createMultiAgentOrchestrator({
  runner,
  agents: {
    researcher: {
      agent: researcher,
      constraints: {
        lowConfidence: {
          when: (facts) => (facts.agent.output?.confidence ?? 1) < 0.5,
          require: { type: 'RUN_AGENT', agent: 'expert', input: 'Verify findings' },
        },
      },
    },
    expert: { agent: expert },
  },

  constraints: {
    budgetAlert: {
      priority: 100,
      when: (facts) => facts.globalTokens > 40000,
      require: { type: 'BUDGET_ALERT' },
    },
  },

  resolvers: {
    budgetAlert: {
      requirement: requirementGuard('BUDGET_ALERT'),
      resolve: async (req, context) => {
        console.warn('Approaching budget limit');
      },
    },
  },
});

Concurrency Control

Each agent gets its own Semaphore instance:

import { Semaphore } from '@directive-run/ai';

const sem = new Semaphore(3);

const release = await sem.acquire();
try {
  await doWork();
} finally {
  release();
}

console.log(sem.available);  // Free permits
console.log(sem.waiting);    // Queued callers
sem.drain();                 // Reject all waiters

Debug & Time-Travel

const orchestrator = createMultiAgentOrchestrator({
  runner,
  agents: { /* ... */ },
  debug: true,
});

const { system } = orchestrator;

Error Handling

Unregistered agents and patterns throw with descriptive errors:

// '[Directive MultiAgent] Unknown agent "nonexistent". Registered agents: researcher, writer'
await orchestrator.runAgent('nonexistent', 'hello');

// '[Directive MultiAgent] Unknown pattern "nonexistent". Available patterns: research'
await orchestrator.runPattern('nonexistent', 'hello');

See Execution Patterns for pattern-specific error handling (parallel minSuccess, sequential continueOnError, supervisor worker validation).


Framework Integration

The orchestrator exposes .system – a Directive System with namespaced modules. Each agent's state lives under its ID with bridge keys __agent, __approval, __conversation, __toolCalls.

React

import { useFact, useSelector, useInspect } from '@directive-run/react';

function MultiAgentPanel({ orchestrator }: { orchestrator: MultiAgentOrchestrator }) {
  const { system } = orchestrator;
  const researcherAgent = useFact(system, 'researcher::__agent');
  const writerAgent = useFact(system, 'writer::__agent');
  const { isSettled } = useInspect(system);

  return (
    <div>
      <p>Researcher: {researcherAgent?.status}</p>
      <p>Writer: {writerAgent?.status}</p>
      <p>{isSettled ? 'Idle' : 'Working...'}</p>
    </div>
  );
}

Framework adapters for Vue, Svelte, Solid, and Lit follow the same pattern – see Framework Adapters.


Next Steps

Previous
Memory

We care about your data. We'll never share your email.

Powered by Directive. This signup uses a Directive module with facts, derivations, constraints, and resolvers – zero useState, zero useEffect. Read how it works

Directive - Constraint-Driven State Management for TypeScript