ANTIGRAVITY LABJP
Articles/Agents & Manager
Agents & Manager/2026-03-14Advanced

Multi-Agent Orchestration with Antigravity — A Production Implementation Guide

Build production-grade multi-agent systems using Antigravity. Covers orchestrator/worker separation, DAG-based task management, parallel execution, retry logic, and cost optimization with real Python code.

Antigravity322multi-agent49orchestration21AI pipeline2production71

Why Multi-Agent Architecture?

Single-agent systems hit a ceiling with tasks requiring parallel data collection, large-scale refactoring, or long-form document generation. Multi-agent architecture solves this by decomposing complex work across specialized agents running concurrently.

The system we'll build:

[Orchestrator Agent]
    ├── [Research Agent × 3]     (parallel)
    ├── [Code Review Agent]       (sequential)
    └── [Report Generation Agent] (final aggregation)
ℹ️
You'll need an Antigravity API key. If you haven't obtained one yet, head to your [settings page](/settings).

1. Core Design Principles

Orchestrator vs Worker Separation

Orchestrator
  Role: Task decomposition, state management, result aggregation
  Characteristics: Long context window, infrequent calls, high-quality model

Worker Agents
  Role: Execute specific sub-tasks
  Characteristics: Short context, high frequency, parallelizable

Use a powerful model (e.g., claude-opus-4) for the orchestrator and fast/cheap models (e.g., claude-haiku-4-5) for workers to balance cost and latency.


2. Task Graph Engine

# task_graph.py
import asyncio
import json
from typing import Any
from dataclasses import dataclass, field
from enum import Enum
import anthropic
 
class TaskStatus(Enum):
    PENDING = "pending"
    RUNNING = "running"
    COMPLETED = "completed"
    FAILED = "failed"
    RETRYING = "retrying"
 
@dataclass
class Task:
    id: str
    name: str
    description: str
    dependencies: list[str] = field(default_factory=list)
    status: TaskStatus = TaskStatus.PENDING
    result: Any = None
    error: str | None = None
    retries: int = 0
    max_retries: int = 3
 
class TaskGraph:
    """Directed Acyclic Graph (DAG) for managing task execution order"""
 
    def __init__(self):
        self.tasks: dict[str, Task] = {}
        self.client = anthropic.Anthropic()
 
    def add_task(self, task: Task):
        self.tasks[task.id] = task
 
    def get_ready_tasks(self) -> list[Task]:
        """Return tasks whose dependencies are all complete"""
        ready = []
        for task in self.tasks.values():
            if task.status != TaskStatus.PENDING:
                continue
            deps_done = all(
                self.tasks[dep].status == TaskStatus.COMPLETED
                for dep in task.dependencies
                if dep in self.tasks
            )
            if deps_done:
                ready.append(task)
        return ready
 
    def is_complete(self) -> bool:
        return all(
            t.status in (TaskStatus.COMPLETED, TaskStatus.FAILED)
            for t in self.tasks.values()
        )
 
    def get_context_for_task(self, task: Task) -> dict:
        """Gather completed dependency results as context"""
        return {
            dep_id: self.tasks[dep_id].result
            for dep_id in task.dependencies
            if dep_id in self.tasks and self.tasks[dep_id].result
        }

3. Worker Agent Implementation

# worker_agent.py
import asyncio
import json
from anthropic import AsyncAnthropic
 
class WorkerAgent:
    """Executes a single, well-scoped task"""
 
    def __init__(
        self,
        agent_id: str,
        system_prompt: str,
        model: str = "claude-haiku-4-5-20251001",
        max_tokens: int = 4096,
    ):
        self.agent_id = agent_id
        self.system_prompt = system_prompt
        self.model = model
        self.max_tokens = max_tokens
        self.client = AsyncAnthropic()
        self.tools = []
 
    def add_tool(self, tool_definition: dict):
        self.tools.append(tool_definition)
 
    async def execute(
        self,
        task_description: str,
        context: dict = None,
        timeout: float = 120.0,
    ) -> dict:
        messages = self._build_messages(task_description, context or {})
 
        try:
            response = await asyncio.wait_for(
                self._run_agent_loop(messages),
                timeout=timeout,
            )
            return {"status": "success", "result": response, "agent_id": self.agent_id}
        except asyncio.TimeoutError:
            return {"status": "timeout", "result": None, "agent_id": self.agent_id}
        except Exception as e:
            return {"status": "error", "result": None, "error": str(e), "agent_id": self.agent_id}
 
    def _build_messages(self, task: str, context: dict) -> list:
        context_str = ""
        if context:
            context_str = "\n\n## Context from completed dependencies\n"
            for dep_id, result in context.items():
                context_str += f"\n### {dep_id}\n{json.dumps(result, indent=2)}\n"
 
        return [
            {
                "role": "user",
                "content": f"{context_str}\n\n## Your task\n{task}"
            }
        ]
 
    async def _run_agent_loop(self, messages: list) -> str:
        max_iterations = 10
 
        for _ in range(max_iterations):
            kwargs = {
                "model": self.model,
                "max_tokens": self.max_tokens,
                "system": self.system_prompt,
                "messages": messages,
            }
            if self.tools:
                kwargs["tools"] = self.tools
 
            response = await self.client.messages.create(**kwargs)
            tool_uses = [b for b in response.content if b.type == "tool_use"]
 
            if not tool_uses or response.stop_reason == "end_turn":
                return "\n".join(b.text for b in response.content if hasattr(b, "text"))
 
            messages.append({"role": "assistant", "content": response.content})
            tool_results = await self._execute_tools(tool_uses)
            messages.append({"role": "user", "content": tool_results})
 
        return "Reached max iterations"
 
    async def _execute_tools(self, tool_uses: list) -> list:
        return [
            {
                "type": "tool_result",
                "tool_use_id": tool_use.id,
                "content": f"Tool {tool_use.name} not implemented",
            }
            for tool_use in tool_uses
        ]

4. Parallel Orchestrator

# parallel_orchestrator.py
import asyncio
from task_graph import TaskGraph, Task, TaskStatus
from worker_agent import WorkerAgent
 
class ParallelOrchestrator:
    """Runs tasks in parallel respecting DAG dependency order"""
 
    def __init__(self, max_concurrent: int = 5):
        self.max_concurrent = max_concurrent
        self.semaphore = asyncio.Semaphore(max_concurrent)
        self.graph = TaskGraph()
        self.workers: dict[str, WorkerAgent] = {}
 
    def register_worker(self, task_pattern: str, worker: WorkerAgent):
        self.workers[task_pattern] = worker
 
    def _get_worker_for_task(self, task: Task) -> WorkerAgent:
        for pattern, worker in self.workers.items():
            if pattern in task.name or pattern == "*":
                return worker
        return WorkerAgent(
            agent_id="default",
            system_prompt="You are a general-purpose task execution agent.",
        )
 
    async def execute_task(self, task: Task) -> None:
        async with self.semaphore:
            task.status = TaskStatus.RUNNING
            worker = self._get_worker_for_task(task)
            context = self.graph.get_context_for_task(task)
 
            while task.retries <= task.max_retries:
                if task.retries > 0:
                    task.status = TaskStatus.RETRYING
                    await asyncio.sleep(2 ** task.retries)  # exponential backoff
 
                result = await worker.execute(
                    task_description=task.description,
                    context=context,
                )
 
                if result["status"] == "success":
                    task.result = result["result"]
                    task.status = TaskStatus.COMPLETED
                    print(f"✅ Completed: {task.name}")
                    return
 
                task.retries += 1
                task.error = result.get("error", "Unknown error")
                print(f"⚠️ Failed (attempt {task.retries}/{task.max_retries}): {task.name}")
 
            task.status = TaskStatus.FAILED
            print(f"❌ Permanently failed: {task.name}")
 
    async def run(self) -> dict:
        print(f"🚀 Orchestrator starting: {len(self.graph.tasks)} tasks")
 
        while not self.graph.is_complete():
            ready_tasks = self.graph.get_ready_tasks()
 
            if not ready_tasks:
                running = [t for t in self.graph.tasks.values() if t.status == TaskStatus.RUNNING]
                if not running:
                    print("⛔ Deadlock detected — aborting")
                    break
                await asyncio.sleep(0.1)
                continue
 
            await asyncio.gather(
                *[self.execute_task(task) for task in ready_tasks],
                return_exceptions=True,
            )
 
        return {
            task_id: task.result
            for task_id, task in self.graph.tasks.items()
            if task.status == TaskStatus.COMPLETED
        }

5. Real-World Example: Codebase Analysis Pipeline

# codebase_analysis.py
import asyncio
from parallel_orchestrator import ParallelOrchestrator
from task_graph import Task
from worker_agent import WorkerAgent
 
async def analyze_codebase(repo_path: str) -> str:
    orchestrator = ParallelOrchestrator(max_concurrent=3)
 
    analyzer = WorkerAgent(
        agent_id="code-analyzer",
        system_prompt="""You are a code quality analyst.
Analyze code for quality issues, tech debt, and performance bottlenecks.
Return results as structured JSON.""",
        model="claude-haiku-4-5-20251001",
    )
 
    security_reviewer = WorkerAgent(
        agent_id="security-reviewer",
        system_prompt="""You are a security expert.
Review code from an OWASP Top 10 perspective.
Identify and categorize all security vulnerabilities.""",
        model="claude-haiku-4-5-20251001",
    )
 
    reporter = WorkerAgent(
        agent_id="report-generator",
        system_prompt="""You are a technical writer.
Synthesize analysis results into a prioritized executive summary
with actionable recommendations.""",
        model="claude-sonnet-4-6",  # Higher quality model for final aggregation
    )
 
    orchestrator.register_worker("analyze", analyzer)
    orchestrator.register_worker("security", security_reviewer)
    orchestrator.register_worker("report", reporter)
 
    tasks = [
        Task(
            id="analyze_frontend",
            name="analyze-frontend",
            description=f"Analyze {repo_path}/src/frontend for code quality issues.",
        ),
        Task(
            id="analyze_backend",
            name="analyze-backend",
            description=f"Analyze {repo_path}/src/backend for code quality issues.",
        ),
        Task(
            id="security_check",
            name="security-review",
            description=f"Perform a security audit of {repo_path}.",
        ),
        Task(
            id="final_report",
            name="report-generation",
            description="Aggregate all analysis results into a prioritized improvement report.",
            dependencies=["analyze_frontend", "analyze_backend", "security_check"],
        ),
    ]
 
    for task in tasks:
        orchestrator.graph.add_task(task)
 
    results = await orchestrator.run()
    return results.get("final_report", "Report generation failed")
 
if __name__ == "__main__":
    result = asyncio.run(analyze_codebase("/path/to/your/project"))
    print(result)

6. Production Checklist

"""
Production multi-agent checklist
 
✅ Timeouts
   - Every worker has asyncio.wait_for timeout (default: 120s)
   - Global pipeline timeout to prevent runaway costs
 
✅ Retry Strategy
   - max_retries: 3
   - Exponential backoff: 2^n seconds (2, 4, 8)
   - Idempotent tasks (safe to run multiple times)
 
✅ Cost Management
   - Workers: claude-haiku-4-5 (fast, cheap)
   - Orchestrator: claude-opus-4 (accurate planning)
   - Explicit max_tokens on every call
 
✅ Observability
   - Log task start/end/error with timestamps
   - Track token usage per task
   - Alert on consecutive failures
 
✅ Graceful Degradation
   - Non-critical task failures: continue with None result
   - Critical task failures: abort pipeline and notify
"""

Looking back

Multi-agent systems demand more than stringing agents together — you need DAG-based task management, exponential backoff retries, and thoughtful cost allocation.

The architecture in this guide scales from a 2-agent automation script to 50+ agent pipelines without structural changes. Start small: build a 2-agent pipeline today, validate it end-to-end, then layer in complexity.

Share

Thank You for Reading

Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Agents & Manager2026-07-06
When Parallel Agents Ran the Same Task Twice and Quietly Doubled the Bill — Field Notes on Measuring and Stopping Duplicates
The bill for our parallel agents came in about 1.9x higher than expected — because multiple workers were running the same task twice. These are field notes on measuring the duplication, stopping it with idempotency keys, and attributing cost per task.
Agents & Manager2026-06-15
Containing Failure in Antigravity Multi-Agent Systems: Three Boundaries That Stop Cascades
Antigravity multi-agent setups run beautifully in isolation but cascade in production, where one small failure drags the whole orchestration down. These notes organize the fix around three boundaries—layered control, trust separation, and observability with idempotency—down to the TOML and the correlation-ID wrapper.
Agents & Manager2026-04-10
Antigravity Multi-Agent Orchestration Guide: From Communication Errors to Production
Complete guide to designing and implementing multi-agent systems with Antigravity. Covers architecture patterns, communication error troubleshooting, and production stability.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →