orchestrator_patterns.py

script

← Back to skill

Content hash: 2d42b93094111e781b77c2d68cdc98db4a3965ffdde1804d0ddcc75bbc7f90a1
#!/usr/bin/env python3
"""Multi-agent orchestration patterns: demonstrating supervisor-worker.

This is a SKELETON that shows the structure. Plug in real agent/model calls
for production use. The value is the explicit state machine and guardrails.
"""

from __future__ import annotations

import time
import uuid
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Callable


# ── Agent primitives ──────────────────────────────────────────────────────


@dataclass
class Agent:
    name: str
    role: str  # "supervisor" | "worker" | "reviewer"
    tools: list[str] = field(default_factory=list)
    handler: Callable[[str], str] = lambda self, x: f"[{self.name}] processed {x!r}"


@dataclass
class Task:
    id: str
    description: str
    assigned_to: str | None = None
    result: str | None = None


# ── Patterns ──────────────────────────────────────────────────────────────


class SupervisorWorker:
    """Supervisor decomposes task, assigns to workers, collects results."""

    def __init__(self, supervisor: Agent, workers: list[Agent], max_steps: int = 10):
        self.supervisor = supervisor
        self.workers = {w.name: w for w in workers}
        self.max_steps = max_steps
        self.trace: list[str] = []

    def run(self, task_text: str) -> dict[str, str]:
        """Execute a task through the supervisor-worker pattern."""
        run_id = uuid.uuid4().hex[:8]
        self.trace.append(f"[{run_id}] START: {task_text[:60]}")

        step = 0
        subtasks = [
            Task(f"{run_id}-1", "Analyze input and identify sub-problems"),
            Task(f"{run_id}-2", "Solve each sub-problem"),
            Task(f"{run_id}-3", "Merge results and verify"),
        ]

        results: dict[str, str] = {}

        for task in subtasks:
            if step >= self.max_steps:
                self.trace.append(f"[{run_id}] HALT: step limit reached")
                break

            # Assign round-robin to workers
            worker = list(self.workers.values())[step % len(self.workers)]
            task.assigned_to = worker.name
            task.result = worker.handler(task.description)
            results[task.id] = task.result
            self.trace.append(f"[{run_id}] {worker.name}: {task.result[:60]}")
            step += 1

        self.trace.append(f"[{run_id}] END: {len(results)}/{len(subtasks)} done")
        return results


class FanOut:
    """Parallel fan-out: one task to all workers simultaneously."""

    def __init__(self, workers: list[Agent]):
        self.workers = workers

    def run(self, task_text: str) -> dict[str, str]:
        """In production: asyncio.gather over worker LLM calls."""
        results = {}
        for w in self.workers:
            results[w.name] = w.handler(task_text)
        return results


# ── Guardrails ────────────────────────────────────────────────────────────


class Guardrails:
    """Hard caps and validation that stop runaway agent loops."""

    def __init__(
        self,
        max_steps: int = 10,
        max_tokens: int = 100_000,
        max_spend: float = 5.0,
    ):
        self.max_steps = max_steps
        self.max_tokens = max_tokens
        self.max_spend = max_spend
        self.spent = 0.0
        self.tokens_used = 0
        self.steps = 0

    def check(self, step_tokens: int = 0, step_cost: float = 0.0) -> str | None:
        """Return error message if limit breached, else None."""
        self.steps += 1
        self.tokens_used += step_tokens
        self.spent += step_cost
        if self.steps > self.max_steps:
            return f"Step limit ({self.max_steps}) exceeded"
        if self.tokens_used > self.max_tokens:
            return f"Token budget ({self.max_tokens}) exceeded"
        if self.spent > self.max_spend:
            return f"Spend cap (${self.max_spend:.2f}) exceeded"
        return None


# ── Demo ───────────────────────────────────────────────────────────────────


def main() -> None:
    sup = Agent("supervisor", "supervisor", handler=lambda x: f"PLAN: {x[:40]}")
    workers = [
        Agent("coder", "worker", tools=["read", "write"], handler=lambda x: f"CODE: {x[:40]}"),
        Agent("reviewer", "worker", tools=["diff", "lint"], handler=lambda x: f"REVIEW: {x[:40]}"),
    ]

    print("=== Supervisor-Worker Pattern ===")
    orch = SupervisorWorker(sup, workers, max_steps=5)
    results = orch.run("Implement a todo-list REST API")
    for tid, result in results.items():
        print(f"  {tid}: {result}")

    print(f"\n  Trace: {len(orch.trace)} steps")
    for t in orch.trace:
        print(f"    {t}")

    print("\n=== Fan-Out Pattern ===")
    fan = FanOut(workers)
    for name, result in fan.run("What is the time complexity of quicksort?").items():
        print(f"  {name}: {result}")

    print("\n=== Guardrails ===")
    g = Guardrails(max_steps=3, max_tokens=1000, max_spend=0.50)
    for i in range(5):
        limit = g.check(step_tokens=800, step_cost=0.30)
        if limit:
            print(f"  BREACHED at step {i+1}: {limit}")
            break
    else:
        print("  All within limits")


if __name__ == "__main__":
    main()